Skip to content

Commit

Permalink
feat: AttachmentPrimitive (#838)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yonom authored Oct 10, 2024
1 parent 3d31f10 commit cf872da
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-trainers-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@assistant-ui/react": patch
---

feat: AttachmentPrimitive
113 changes: 113 additions & 0 deletions apps/docs/content/docs/ui/primitives/Attachment.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
title: ActionBar
---

Buttons to interact with attachments.

import { ParametersTable, DataAttributesTable } from "@/components/docs";
import { Code } from "@radix-ui/themes";
import { Callout } from "fumadocs-ui/components/callout";

<Callout>
**Dual Use!** Attachments can appear in both messages and composers.
</Callout>

## Anatomy

```tsx
import { AttachmentPrimitive } from "@assistant-ui/react";

const MyMessageAttachment = () => (
<AttachmentPrimitive.Root>
<AttachmentPrimitive.Thumbnail />
<AttachmentPrimitive.Name />
</AttachmentPrimitive.Root>
);

const MyComposerAttachment = () => (
<AttachmentPrimitive.Root>
<AttachmentPrimitive.Thumbnail />
<AttachmentPrimitive.Name />
<AttachmentPrimitive.Remove />
</AttachmentPrimitive.Root>
);
```

## API Reference

### Container

Containts all parts of the attachment.

This primitive renders a `<div>` element unless `asChild` is set.

<ParametersTable
type="AttachmentPrimitiveRootProps"
parameters={[
{
name: "asChild",
},
]}
/>

### Thumbnail

The thumbnail of the attachment.

This primitive renders a `<div>` element unless `asChild` is set.

<ParametersTable
type="AttachmentPrimitiveThumbnailProps"
parameters={[
{
name: "asChild",
},
]}
/>

### Name

The name of the attachment.

This primitive renders a `<div>` element unless `asChild` is set.

<ParametersTable
type="AttachmentPrimitiveNameProps"
parameters={[
{
name: "asChild",
},
]}
/>

### Remove

Removes the attachment.

This primitive renders a `<button>` element unless `asChild` is set.

<ParametersTable
type="AttachmentPrimitiveRemoveProps"
parameters={[
{
name: "asChild",
},
]}
/>

#### `useAttachmentRemove`

Provides the `Remove` functionality as a hook.

```tsx
import { useAttachmentRemove } from "@assistant-ui/react";

const Remove = () => {
const remove = useAttachmentRemove();

// remove action is not available
if (!remove) return null;

return <button onClick={remove}>Remove</button>;
};
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useCallback } from "react";
import { useAttachmentRuntime } from "../../context/react/AttachmentContext";

export const useAttachmentRemove = () => {
const attachmentRuntime = useAttachmentRuntime();

const handleRemoveAttachment = useCallback(() => {
attachmentRuntime.remove();
}, [attachmentRuntime]);

return handleRemoveAttachment;
};
17 changes: 17 additions & 0 deletions packages/react/src/primitives/attachment/AttachmentName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client";

import type { FC } from "react";
import { useAttachment } from "../../context/react/AttachmentContext";

export namespace AttachmentPrimitiveName {
export type Props = Record<string, never>;
}

export const AttachmentPrimitiveName: FC<
AttachmentPrimitiveName.Props
> = () => {
const name = useAttachment((a) => a.name);
return <>{name}</>;
};

AttachmentPrimitiveName.displayName = "AttachmentPrimitive.Name";
16 changes: 16 additions & 0 deletions packages/react/src/primitives/attachment/AttachmentRemove.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use client";

import { useAttachmentRemove } from "../../primitive-hooks/attachment/useAttachmentRemove";
import {
ActionButtonProps,
createActionButton,
} from "../../utils/createActionButton";

export namespace AttachmentPrimitiveRemove {
export type Props = ActionButtonProps<typeof useAttachmentRemove>;
}

export const AttachmentPrimitiveRemove = createActionButton(
"AttachmentPrimitive.Remove",
useAttachmentRemove,
);
18 changes: 18 additions & 0 deletions packages/react/src/primitives/attachment/AttachmentRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Primitive } from "@radix-ui/react-primitive";
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react";

type PrimitiveDivProps = ComponentPropsWithoutRef<typeof Primitive.div>;
type AttachmentPrimitiveRootElement = ElementRef<typeof Primitive.div>;

export namespace AttachmentPrimitiveRoot {
export type Props = PrimitiveDivProps;
}

export const AttachmentPrimitiveRoot = forwardRef<
AttachmentPrimitiveRootElement,
AttachmentPrimitiveRoot.Props
>((props, ref) => {
return <Primitive.div {...props} ref={ref} />;
});

AttachmentPrimitiveRoot.displayName = "AttachmentPrimitive.Root";
22 changes: 22 additions & 0 deletions packages/react/src/primitives/attachment/AttachmentThumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { ComponentPropsWithoutRef, forwardRef, type ElementRef } from "react";
import { useAttachment } from "../../context/react/AttachmentContext";
import { Primitive } from "@radix-ui/react-primitive";

type AttachmentPrimitiveThumbElement = ElementRef<typeof Primitive.div>;
type PrimitiveDivProps = ComponentPropsWithoutRef<typeof Primitive.div>;

export namespace AttachmentPrimitiveThumb {
export type Props = PrimitiveDivProps;
}

export const AttachmentPrimitiveThumb = forwardRef<
AttachmentPrimitiveThumbElement,
AttachmentPrimitiveThumb.Props
>(() => {
const ext = useAttachment((a) => a.name.split(".").pop());
return <Primitive.div>.{ext}</Primitive.div>;
});

AttachmentPrimitiveThumb.displayName = "AttachmentPrimitive.Thumb";
4 changes: 4 additions & 0 deletions packages/react/src/primitives/attachment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { AttachmentPrimitiveRoot as Root } from "./AttachmentRoot";
export { AttachmentPrimitiveThumb as unstable_Thumb } from "./AttachmentThumb";
export { AttachmentPrimitiveName as Name } from "./AttachmentName";
export { AttachmentPrimitiveRemove as Remove } from "./AttachmentRemove";
1 change: 1 addition & 0 deletions packages/react/src/primitives/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * as ActionBarPrimitive from "./actionBar";
export * as AssistantModalPrimitive from "./assistantModal";
export * as AttachmentPrimitive from "./attachment";
export * as BranchPickerPrimitive from "./branchPicker";
export * as ComposerPrimitive from "./composer";
export * as ContentPartPrimitive from "./contentPart";
Expand Down
37 changes: 14 additions & 23 deletions packages/react/src/ui/composer-attachment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,18 @@ import {
TooltipIconButton,
TooltipIconButtonProps,
} from "./base/tooltip-icon-button";
import {
useAttachmentRuntime,
useThreadComposerAttachment,
} from "../context/react/AttachmentContext";
import { AttachmentPrimitive } from "../primitives";

const ComposerAttachmentRoot = withDefaults("div", {
const ComposerAttachmentRoot = withDefaults(AttachmentPrimitive.Root, {
className: "aui-composer-attachment-root",
});

ComposerAttachmentRoot.displayName = "ComposerAttachmentRoot";

const ComposerAttachment: FC = () => {
const attachment = useThreadComposerAttachment((a) => a.attachment);

return (
<ComposerAttachmentRoot>
.{attachment.name.split(".").pop()}
<AttachmentPrimitive.unstable_Thumb />
<ComposerAttachmentRemove />
</ComposerAttachmentRoot>
);
Expand All @@ -42,22 +37,18 @@ const ComposerAttachmentRemove = forwardRef<
} = {},
} = useThreadConfig();

const attachmentRuntime = useAttachmentRuntime();
const handleRemoveAttachment = () => {
attachmentRuntime.remove();
};

return (
<TooltipIconButton
tooltip={tooltip}
className="aui-composer-attachment-remove"
side="top"
{...props}
onClick={handleRemoveAttachment}
ref={ref}
>
{props.children ?? <CircleXIcon />}
</TooltipIconButton>
<AttachmentPrimitive.Remove asChild>
<TooltipIconButton
tooltip={tooltip}
className="aui-composer-attachment-remove"
side="top"
{...props}
ref={ref}
>
{props.children ?? <CircleXIcon />}
</TooltipIconButton>
</AttachmentPrimitive.Remove>
);
});

Expand Down
8 changes: 3 additions & 5 deletions packages/react/src/ui/user-message-attachment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@
import { type FC } from "react";

import { withDefaults } from "./utils/withDefaults";
import { useAttachment } from "../context/react/AttachmentContext";
import { AttachmentPrimitive } from "../primitives";

const UserMessageAttachmentRoot = withDefaults("div", {
const UserMessageAttachmentRoot = withDefaults(AttachmentPrimitive.Root, {
className: "aui-user-message-attachment-root",
});

UserMessageAttachmentRoot.displayName = "UserMessageAttachmentRoot";

const UserMessageAttachment: FC = () => {
const attachment = useAttachment((a) => a.attachment);

return (
<UserMessageAttachmentRoot>
.{attachment.name.split(".").pop()}
<AttachmentPrimitive.unstable_Thumb />
</UserMessageAttachmentRoot>
);
};
Expand Down

0 comments on commit cf872da

Please sign in to comment.