snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
522
apps/v4/registry/bases/aria/examples/bubble-example.tsx
Normal file
522
apps/v4/registry/bases/aria/examples/bubble-example.tsx
Normal file
@@ -0,0 +1,522 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import {
|
||||
Example,
|
||||
ExampleWrapper,
|
||||
} from "@/registry/bases/aria/components/example"
|
||||
import {
|
||||
Bubble,
|
||||
BubbleContent,
|
||||
BubbleGroup,
|
||||
BubbleReactions,
|
||||
} from "@/registry/bases/aria/ui/bubble"
|
||||
import { Button } from "@/registry/bases/aria/ui/button"
|
||||
import { Collapsible } from "@/registry/bases/aria/ui/collapsible"
|
||||
import { Marker, MarkerContent } from "@/registry/bases/aria/ui/marker"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export default function BubbleExample() {
|
||||
return (
|
||||
<ExampleWrapper>
|
||||
<BubbleSizes />
|
||||
<BubbleVariants />
|
||||
<BubbleAlignment />
|
||||
<BubbleGrouped />
|
||||
<BubbleCollapsible />
|
||||
<BubbleButtonLinks />
|
||||
<BubbleWithReactions />
|
||||
<BubbleReactionsButtons />
|
||||
</ExampleWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function BubbleVariants() {
|
||||
return (
|
||||
<Example title="Variants">
|
||||
<div className="flex w-full max-w-md flex-col gap-8">
|
||||
<Bubble>
|
||||
<BubbleContent>
|
||||
Default bubbles use the primary color for the active user side of a
|
||||
chat.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="secondary">
|
||||
<BubbleContent>
|
||||
Secondary bubbles are the standard neutral surface for assistant and
|
||||
conversation content.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="muted">
|
||||
<BubbleContent>
|
||||
Muted bubbles lower the emphasis for quiet system notes or for
|
||||
displaying supporting content.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="tinted" align="end">
|
||||
<BubbleContent>
|
||||
Tinted bubbles use a softer primary tint when primary fill is too
|
||||
strong.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="outline">
|
||||
<BubbleContent>
|
||||
Outline bubbles can be used to frame message content and give it a
|
||||
border.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="destructive">
|
||||
<BubbleContent>
|
||||
Destructive bubbles flag errors or failed actions in a conversation.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="ghost">
|
||||
<BubbleContent>
|
||||
<span className="whitespace-pre-wrap">
|
||||
{`Ghost bubbles work for assistant text and other content that should not be framed.
|
||||
|
||||
This is perfect for assistant messages that should not have a frame and can take the full width of the container.
|
||||
|
||||
Ghost bubbles are full width and can take the full width of the container.
|
||||
`}
|
||||
</span>
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
</div>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
|
||||
function BubbleSizes() {
|
||||
return (
|
||||
<Example title="Sizes">
|
||||
<div className="flex w-full max-w-md flex-col gap-8">
|
||||
<Bubble>
|
||||
<BubbleContent>This is a one line bubble.</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble>
|
||||
<BubbleContent>
|
||||
This bubble has multiple lines. It should wrap to the next line and
|
||||
you should see a different radius on the corners.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble>
|
||||
<BubbleContent>
|
||||
<p>This bubble has multiple lines.</p>
|
||||
<p>
|
||||
It should wrap to the next line and you should see a different
|
||||
radius on the corners.
|
||||
</p>
|
||||
<p>Here is some more text to see how it wraps.</p>
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
</div>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
|
||||
function BubbleGrouped() {
|
||||
return (
|
||||
<Example title="Grouped">
|
||||
<div className="flex w-full max-w-md flex-col gap-8">
|
||||
<BubbleGroup>
|
||||
<Bubble variant="secondary">
|
||||
<BubbleContent>I finished the audit pass.</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="secondary">
|
||||
<BubbleContent>
|
||||
The registry output looks clean, but I found one stale route.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="secondary">
|
||||
<BubbleContent>Want me to remove it now?</BubbleContent>
|
||||
</Bubble>
|
||||
</BubbleGroup>
|
||||
<BubbleGroup>
|
||||
<Bubble variant="tinted" align="end">
|
||||
<BubbleContent>Yes, clean that up.</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="tinted" align="end">
|
||||
<BubbleContent>Then rerun the registry build.</BubbleContent>
|
||||
</Bubble>
|
||||
</BubbleGroup>
|
||||
</div>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
|
||||
const text = `The accessibility review found two focus states that were visually too subtle in dark mode.
|
||||
|
||||
I checked the dialog, menu, and drawer paths because each one renders focusable controls inside a layered surface.
|
||||
|
||||
The dialog and drawer are fine. The menu needs the hover and focus tokens split so keyboard focus stays visible when the pointer is not involved.
|
||||
|
||||
I also recommend keeping the change in the style file instead of the primitive so the other themes can choose their own focus treatment later.`
|
||||
|
||||
const previewLength = 180
|
||||
|
||||
function BubbleCollapsible() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const isLong = text.length > previewLength
|
||||
const preview = `${text.slice(0, previewLength)}...`
|
||||
|
||||
return (
|
||||
<Example title="Collapsible">
|
||||
<div className="flex w-full max-w-md flex-col gap-8">
|
||||
<Collapsible isExpanded={open} onExpandedChange={setOpen}>
|
||||
<Bubble variant="muted" align="end">
|
||||
<BubbleContent className="whitespace-pre-line">
|
||||
<div>{open || !isLong ? text : preview}</div>
|
||||
{isLong ? (
|
||||
<Button
|
||||
slot="trigger"
|
||||
variant="link"
|
||||
className="gap-1 p-0 text-muted-foreground"
|
||||
>
|
||||
{open ? "Show less" : "Show more"}
|
||||
<IconPlaceholder
|
||||
lucide="ChevronDownIcon"
|
||||
tabler="IconChevronDown"
|
||||
hugeicons="ArrowDown01Icon"
|
||||
phosphor="CaretDownIcon"
|
||||
remixicon="RiArrowDownSLine"
|
||||
data-icon="inline-end"
|
||||
className="group-data-panel-open/button:rotate-180"
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
</Collapsible>
|
||||
<Bubble variant="ghost">
|
||||
<BubbleContent>
|
||||
<span className="whitespace-pre-wrap">
|
||||
{`Ghost bubbles work for assistant text and other content that should not be framed.
|
||||
|
||||
This is perfect for assistant messages that should not have a frame and can take the full width of the container.
|
||||
|
||||
Use this for content that needs the whole row.`}
|
||||
</span>
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
</div>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
|
||||
function BubbleWithReactions() {
|
||||
return (
|
||||
<Example title="Reaction Placement">
|
||||
<div className="flex w-full max-w-md flex-col gap-12">
|
||||
<Marker variant="separator">
|
||||
<MarkerContent>side=bottom align=end</MarkerContent>
|
||||
</Marker>
|
||||
<Bubble>
|
||||
<BubbleContent>This is a one line message.</BubbleContent>
|
||||
<BubbleReactions
|
||||
side="bottom"
|
||||
align="end"
|
||||
role="img"
|
||||
aria-label="Reaction: thumbs up"
|
||||
>
|
||||
<span>👍</span>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Bubble variant="secondary" align="end">
|
||||
<BubbleContent>
|
||||
A longer message that wraps across lines so the reaction offset is
|
||||
easier to inspect.
|
||||
</BubbleContent>
|
||||
<BubbleReactions
|
||||
side="bottom"
|
||||
align="start"
|
||||
role="img"
|
||||
aria-label="Reactions: thumbs up, surprised"
|
||||
>
|
||||
<span>👍</span>
|
||||
<span>😮</span>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Bubble variant="tinted">
|
||||
<BubbleContent>
|
||||
A longer message that wraps across lines so the reaction offset is
|
||||
easier to inspect.
|
||||
</BubbleContent>
|
||||
<BubbleReactions
|
||||
side="bottom"
|
||||
align="end"
|
||||
role="img"
|
||||
aria-label="Reactions: thumbs up, surprised, fire, eyes, and 8 more"
|
||||
>
|
||||
<span>👍</span>
|
||||
<span>😮</span>
|
||||
<span>🔥</span>
|
||||
<span>👀</span>
|
||||
<span>+8</span>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Marker variant="separator">
|
||||
<MarkerContent>side=bottom align=start</MarkerContent>
|
||||
</Marker>
|
||||
<Bubble variant="secondary">
|
||||
<BubbleContent>This is a one line message.</BubbleContent>
|
||||
<BubbleReactions
|
||||
side="bottom"
|
||||
align="start"
|
||||
role="img"
|
||||
aria-label="Reaction: fire"
|
||||
>
|
||||
<span>🔥</span>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Bubble variant="secondary">
|
||||
<BubbleContent>
|
||||
A longer message that wraps across lines so the reaction offset is
|
||||
easier to inspect.
|
||||
</BubbleContent>
|
||||
<BubbleReactions
|
||||
side="bottom"
|
||||
align="start"
|
||||
role="img"
|
||||
aria-label="Reactions: thumbs up, surprised, fire, eyes"
|
||||
>
|
||||
<span>👍</span>
|
||||
<span>😮</span>
|
||||
<span>🔥</span>
|
||||
<span>👀</span>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Marker variant="separator">
|
||||
<MarkerContent>side=top align=start</MarkerContent>
|
||||
</Marker>
|
||||
<Bubble variant="secondary">
|
||||
<BubbleContent>This is a one line message.</BubbleContent>
|
||||
<BubbleReactions
|
||||
side="top"
|
||||
align="start"
|
||||
role="img"
|
||||
aria-label="Reaction: fire"
|
||||
>
|
||||
<span>🔥</span>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Bubble variant="secondary">
|
||||
<BubbleContent>
|
||||
A longer message that wraps across lines so the reaction offset is
|
||||
easier to inspect.
|
||||
</BubbleContent>
|
||||
<BubbleReactions
|
||||
side="top"
|
||||
align="start"
|
||||
role="img"
|
||||
aria-label="Reactions: thumbs up, surprised, fire, eyes"
|
||||
>
|
||||
<span>👍</span>
|
||||
<span>😮</span>
|
||||
<span>🔥</span>
|
||||
<span>👀</span>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Marker variant="separator">
|
||||
<MarkerContent>side=bottom align=end</MarkerContent>
|
||||
</Marker>
|
||||
<Bubble variant="muted">
|
||||
<BubbleContent>This is a one line message.</BubbleContent>
|
||||
<BubbleReactions
|
||||
side="top"
|
||||
align="end"
|
||||
role="img"
|
||||
aria-label="Reaction: thumbs up"
|
||||
>
|
||||
<span>👍</span>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Bubble variant="muted">
|
||||
<BubbleContent>
|
||||
A longer message that wraps across lines so the reaction offset.
|
||||
</BubbleContent>
|
||||
<BubbleReactions
|
||||
side="top"
|
||||
align="end"
|
||||
role="img"
|
||||
aria-label="Reactions: thumbs up, surprised, fire, eyes"
|
||||
className="px-1.5 py-0.5"
|
||||
>
|
||||
<span>👍</span>
|
||||
<span>😮</span>
|
||||
<span>🔥</span>
|
||||
<span>👀</span>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
</div>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
|
||||
function BubbleReactionsButtons() {
|
||||
return (
|
||||
<Example title="Reactions Buttons">
|
||||
<div className="flex w-full max-w-md flex-col gap-8">
|
||||
<Bubble>
|
||||
<BubbleContent>This is a one line message.</BubbleContent>
|
||||
<BubbleReactions>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
toast("You clicked the button in the bubble reaction")
|
||||
}
|
||||
>
|
||||
Button
|
||||
</Button>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Bubble align="end">
|
||||
<BubbleContent>This is a one line message.</BubbleContent>
|
||||
<BubbleReactions align="start">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => toast("Confetti!")}
|
||||
>
|
||||
🎉
|
||||
</Button>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
<Bubble variant="tinted">
|
||||
<BubbleContent>
|
||||
We are going to the movies first then dinner. Are you in?
|
||||
</BubbleContent>
|
||||
<BubbleReactions className="gap-1 bg-background">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon-xs"
|
||||
aria-label="Thumbs up"
|
||||
onClick={() => toast("You agree!")}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="ThumbsUpIcon"
|
||||
tabler="IconThumbUp"
|
||||
hugeicons="ThumbsUpIcon"
|
||||
phosphor="ThumbsUpIcon"
|
||||
remixicon="RiThumbUpLine"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon-xs"
|
||||
aria-label="Thumbs down"
|
||||
onClick={() => toast("You disagree!")}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="ThumbsDownIcon"
|
||||
tabler="IconThumbDown"
|
||||
hugeicons="ThumbsDownIcon"
|
||||
phosphor="ThumbsDownIcon"
|
||||
remixicon="RiThumbDownLine"
|
||||
/>
|
||||
</Button>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
</div>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
|
||||
function BubbleAlignment() {
|
||||
return (
|
||||
<Example title="Alignment">
|
||||
<div className="flex w-full max-w-md flex-col gap-8">
|
||||
<Bubble variant="muted">
|
||||
<BubbleContent>This bubble is aligned to the start.</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble align="end">
|
||||
<BubbleContent>This bubble is aligned to the end.</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="muted">
|
||||
<BubbleContent>
|
||||
This multiline bubble is aligned to the start. The corners should
|
||||
adjust when the text wraps to show the grouped side of the
|
||||
conversation.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble align="end">
|
||||
<BubbleContent>
|
||||
This multiline bubble is aligned to the end. It should sit on the
|
||||
opposite side with the matching corner radius for wrapped text.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
</div>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
|
||||
const quickReplies = [
|
||||
{
|
||||
label: "I need help with my account.",
|
||||
message: "I need help with my account.",
|
||||
},
|
||||
{
|
||||
label: "I forgot my password.",
|
||||
message: "I forgot my password.",
|
||||
},
|
||||
{
|
||||
label:
|
||||
"I have another question. I'd like to talk to a human. Can you help me?",
|
||||
message: "I have another question.",
|
||||
},
|
||||
]
|
||||
|
||||
function BubbleButtonLinks() {
|
||||
return (
|
||||
<Example title="Button & Links">
|
||||
<div className="flex w-full max-w-md flex-col gap-8">
|
||||
<Bubble>
|
||||
<BubbleContent render={(props) => <a href="#" {...props} />}>
|
||||
This bubble is a link.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="secondary">
|
||||
<BubbleContent
|
||||
render={(props) => <button type="button" {...props} />}
|
||||
>
|
||||
This one is a button you can click.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Bubble variant="muted">
|
||||
<BubbleContent
|
||||
render={(props) => <button type="button" {...props} />}
|
||||
>
|
||||
You can also do tinted buttons. Even ones that are multilines.
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
<Marker variant="separator">
|
||||
<MarkerContent>Chat Suggestions</MarkerContent>
|
||||
</Marker>
|
||||
<Bubble>
|
||||
<BubbleContent>How can I help you today?</BubbleContent>
|
||||
</Bubble>
|
||||
<BubbleGroup>
|
||||
{quickReplies.map((reply) => (
|
||||
<Bubble key={reply.label} variant="outline" align="end">
|
||||
<BubbleContent
|
||||
className="border-dashed border-primary"
|
||||
render={(props) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toast(reply.message)}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{reply.label}
|
||||
</BubbleContent>
|
||||
</Bubble>
|
||||
))}
|
||||
</BubbleGroup>
|
||||
</div>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user