Skip to content

Commit

Permalink
feat: AssistantModalPrimitive (#321)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yonom authored Jun 26, 2024
1 parent 904556d commit 33ae8f9
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/rich-lizards-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@assistant-ui/react": patch
---

feat: AssistantModalPrimitive
119 changes: 119 additions & 0 deletions apps/www/pages/reference/primitives/AssistantModal.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
title: AssistantModalPrimitive
description: A modal chat UI usually displayed in the bottom right corner of the screen.
---

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

## Anatomy

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

const Thread = () => (
<AssistantModalPrimitive.Root>
<AssistantModalPrimitive.Trigger>
<FloatingAssistantButton />
</AssistantModalPrimitive.Trigger>
<AssistantModalPrimitive.Content>
<Thread />
</AssistantModalPrimitive.Content>
</ThreadPrimitive.Root>
);
```

## API Reference

### Root

Contains all parts of the assistant modal.

<ParametersTable
type="AssistantModalRootProps"
parameters={[
{
name: "defaultOpen",
type: "boolean",
default: "false",
description: "The open state of the assistant modal when it is initially rendered. Use when you do not need to control its open state.",
},
{
name: "open",
type: "boolean",
description: "Not recommended. The controlled open state of the assistant modal. Must be used in conjunction with onOpenChange.",
},
{
name: "onOpenChange",
type: "(open: boolean) => void",
description: "Event handler called when the open state of the assistant modal changes.",
},
{
name: "modal",
type: "boolean",
default: "false",
description: "The modality of the assistant modal. When set to true, interaction with outside elements will be disabled and only modal content will be visible to screen readers."
}
]}
/>

### Trigger

A button that toggles the open state of the assistant modal. `AssistantModalPrimitive.Content` will position itself against this button.

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

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

<DataAttributesTable
data={[
{
attribute: "[data-state]",
values: <Code>"open" | "closed"</Code>,
},
]}
/>

### Content

The component that pops out when the assistant modal is open.

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

<ParametersTable
type="AssistantModalContentProps"
parameters={[
{
name: "asChild",
},
{
name: "side",
type: "'top' | 'right' | 'bottom' | 'left'",
default: "'top'",
description: "The side of the assistant modal to position against.",
},
{
name: "align",
type: "'start' | 'center' | 'end'",
default: "'end'",
description: "The alignment of the assistant modal to position against.",
},
{
name: "dissmissOnInteractOutside",
type: "boolean",
default: "false",
description: "Dismiss the assistant modal when the user interacts outside of it.",
},
]}
/>

Refer to radix-ui's Documentation for [Popover.Content](https://www.radix-ui.com/primitives/docs/components/popover#content) for more details.


4 changes: 4 additions & 0 deletions apps/www/pages/reference/primitives/_meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
faCodeBranch,
faCommentDots,
faComments,
faCube,
faPenNib,
faPuzzlePiece,
} from "@fortawesome/free-solid-svg-icons";
Expand All @@ -25,6 +26,9 @@ const IconTitle: FC<IconTitleProps> = ({ icon, name }) => {

const meta = {
composition: "Composition",
AssistantModal: {
title: <IconTitle icon={faCube} name="AssistantModal" />,
},
Thread: {
title: <IconTitle icon={faComments} name="Thread" />,
},
Expand Down
2 changes: 2 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
"dependencies": {
"@radix-ui/primitive": "^1.1.0",
"@radix-ui/react-compose-refs": "^1.1.0",
"@radix-ui/react-context": "^1.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-primitive": "^2.0.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-use-callback-ref": "^1.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { ScopedProps, usePopoverScope } from "./AssistantModalRoot";
import { composeEventHandlers } from "@radix-ui/primitive";

type AssistantModalContentElement = ElementRef<typeof PopoverPrimitive.Content>;
type AssistantModalContentProps = ComponentPropsWithoutRef<
typeof PopoverPrimitive.Content
> & {
dissmissOnInteractOutside?: boolean;
};

export const AssistantModalContent = forwardRef<
AssistantModalContentElement,
AssistantModalContentProps
>(
(
{
__scopeAssistantModal,
side,
align,
onInteractOutside,
dissmissOnInteractOutside = false,
...props
}: ScopedProps<AssistantModalContentProps>,
forwardedRef,
) => {
const scope = usePopoverScope(__scopeAssistantModal);

return (
<PopoverPrimitive.Portal {...scope}>
<PopoverPrimitive.Content
{...scope}
{...props}
ref={forwardedRef}
side={side ?? "top"}
align={align ?? "end"}
onInteractOutside={composeEventHandlers(
onInteractOutside,
dissmissOnInteractOutside ? undefined : (e) => e.preventDefault(),
)}
/>
</PopoverPrimitive.Portal>
);
},
);
AssistantModalContent.displayName = "AssistantModalContent";
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FC, useState } from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import type { Scope } from "@radix-ui/react-context";
import { composeEventHandlers } from "@radix-ui/primitive";
import { useOnComposerFocus } from "../../utils/hooks/useOnComposerFocus";

export type ScopedProps<P> = P & { __scopeAssistantModal?: Scope };
export const usePopoverScope = PopoverPrimitive.createPopoverScope();

type AssistantModalRootProps = PopoverPrimitive.PopoverProps;

const useAssistantModalOpenState = (defaultOpen = false) => {
const state = useState(defaultOpen);

const [, setOpen] = state;
useOnComposerFocus(() => {
setOpen(true);
});

return state;
};

export const AssistantModalRoot: FC<AssistantModalRootProps> = ({
__scopeAssistantModal,
defaultOpen,
open,
onOpenChange,
...rest
}: ScopedProps<AssistantModalRootProps>) => {
const scope = usePopoverScope(__scopeAssistantModal);

const [modalOpen, setOpen] = useAssistantModalOpenState(defaultOpen);

return (
<PopoverPrimitive.Root
{...scope}
open={open === undefined ? modalOpen : open}
onOpenChange={composeEventHandlers(onOpenChange, setOpen)}
{...rest}
/>
);
};

AssistantModalRoot.displayName = "AssistantModalRoot";
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { ScopedProps, usePopoverScope } from "./AssistantModalRoot";

type AssistantModalTriggerElement = ElementRef<typeof PopoverPrimitive.Trigger>;
type AssistantModalTriggerProps = ComponentPropsWithoutRef<
typeof PopoverPrimitive.Trigger
>;

export const AssistantModalTrigger = forwardRef<
AssistantModalTriggerElement,
AssistantModalTriggerProps
>(
(
{ __scopeAssistantModal, ...rest }: ScopedProps<AssistantModalTriggerProps>,
ref,
) => {
const scope = usePopoverScope(__scopeAssistantModal);

return <PopoverPrimitive.Trigger {...scope} {...rest} ref={ref} />;
},
);
AssistantModalTrigger.displayName = "AssistantModalTrigger";
3 changes: 3 additions & 0 deletions packages/react/src/primitives/assistantModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { AssistantModalRoot as Root } from "./AssistantModalRoot";
export { AssistantModalTrigger as Trigger } from "./AssistantModalTrigger";
export { AssistantModalContent as Content } from "./AssistantModalContent";
9 changes: 5 additions & 4 deletions packages/react/src/primitives/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * as ThreadPrimitive from "./thread";
export * as ComposerPrimitive from "./composer";
export * as MessagePrimitive from "./message";
export * as BranchPickerPrimitive from "./branchPicker";
export * as ActionBarPrimitive from "./actionBar";
export * as AssistantModalPrimitive from "./assistantModal";
export * as BranchPickerPrimitive from "./branchPicker";
export * as ComposerPrimitive from "./composer";
export * as ContentPartPrimitive from "./contentPart";
export * as MessagePrimitive from "./message";
export * as ThreadPrimitive from "./thread";
2 changes: 1 addition & 1 deletion packages/shadcn-registry/registry/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const registry: RegistryIndex = [
name: "modal",
type: "components:ui",
files: ["assistant-ui/assistant-modal.tsx"],
registryDependencies: ["thread", "button", "popover", "tooltip"],
registryDependencies: ["thread", "button", "tooltip"],
dependencies: ["@assistant-ui/react", "lucide-react"],
},
{
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 33ae8f9

Please sign in to comment.