@inui/design-system 0.0.0-stage → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +360 -2
- package/dist/components/ui/accordion.d.ts +44 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +86 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/ai-avatar.d.ts +46 -0
- package/dist/components/ui/ai-avatar.d.ts.map +1 -0
- package/dist/components/ui/ai-avatar.js +43 -0
- package/dist/components/ui/ai-avatar.js.map +1 -0
- package/dist/components/ui/ai-label.d.ts +118 -0
- package/dist/components/ui/ai-label.d.ts.map +1 -0
- package/dist/components/ui/ai-label.js +184 -0
- package/dist/components/ui/ai-label.js.map +1 -0
- package/dist/components/ui/ai-sparkle.d.ts +91 -0
- package/dist/components/ui/ai-sparkle.d.ts.map +1 -0
- package/dist/components/ui/ai-sparkle.js +200 -0
- package/dist/components/ui/ai-sparkle.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +79 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.js +147 -0
- package/dist/components/ui/alert-dialog.js.map +1 -0
- package/dist/components/ui/alert.d.ts +71 -0
- package/dist/components/ui/alert.d.ts.map +1 -0
- package/dist/components/ui/alert.js +108 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/aspect-ratio.d.ts +30 -0
- package/dist/components/ui/aspect-ratio.d.ts.map +1 -0
- package/dist/components/ui/aspect-ratio.js +40 -0
- package/dist/components/ui/aspect-ratio.js.map +1 -0
- package/dist/components/ui/attachment.d.ts +74 -0
- package/dist/components/ui/attachment.d.ts.map +1 -0
- package/dist/components/ui/attachment.js +142 -0
- package/dist/components/ui/attachment.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +82 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +123 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +130 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +226 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +43 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +98 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/browser-tabs.d.ts +218 -0
- package/dist/components/ui/browser-tabs.d.ts.map +1 -0
- package/dist/components/ui/browser-tabs.js +528 -0
- package/dist/components/ui/browser-tabs.js.map +1 -0
- package/dist/components/ui/bubble.d.ts +80 -0
- package/dist/components/ui/bubble.d.ts.map +1 -0
- package/dist/components/ui/bubble.js +235 -0
- package/dist/components/ui/bubble.js.map +1 -0
- package/dist/components/ui/button-group.d.ts +60 -0
- package/dist/components/ui/button-group.d.ts.map +1 -0
- package/dist/components/ui/button-group.js +89 -0
- package/dist/components/ui/button-group.js.map +1 -0
- package/dist/components/ui/button.d.ts +157 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +144 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/calendar.d.ts +57 -0
- package/dist/components/ui/calendar.d.ts.map +1 -0
- package/dist/components/ui/calendar.js +163 -0
- package/dist/components/ui/calendar.js.map +1 -0
- package/dist/components/ui/card.d.ts +96 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +131 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/carousel.d.ts +86 -0
- package/dist/components/ui/carousel.d.ts.map +1 -0
- package/dist/components/ui/carousel.js +207 -0
- package/dist/components/ui/carousel.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +54 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +78 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/collapsible.d.ts +45 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +65 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +111 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +264 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/command.d.ts +71 -0
- package/dist/components/ui/command.d.ts.map +1 -0
- package/dist/components/ui/command.js +133 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +70 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +163 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/count-badge.d.ts +40 -0
- package/dist/components/ui/count-badge.d.ts.map +1 -0
- package/dist/components/ui/count-badge.js +43 -0
- package/dist/components/ui/count-badge.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +1026 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +3423 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +73 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +145 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/direction.d.ts +35 -0
- package/dist/components/ui/direction.d.ts.map +1 -0
- package/dist/components/ui/direction.js +35 -0
- package/dist/components/ui/direction.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +53 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +115 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +75 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +173 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/dropzone.d.ts +141 -0
- package/dist/components/ui/dropzone.d.ts.map +1 -0
- package/dist/components/ui/dropzone.js +257 -0
- package/dist/components/ui/dropzone.js.map +1 -0
- package/dist/components/ui/empty.d.ts +94 -0
- package/dist/components/ui/empty.d.ts.map +1 -0
- package/dist/components/ui/empty.js +149 -0
- package/dist/components/ui/empty.js.map +1 -0
- package/dist/components/ui/field.d.ts +122 -0
- package/dist/components/ui/field.d.ts.map +1 -0
- package/dist/components/ui/field.js +254 -0
- package/dist/components/ui/field.js.map +1 -0
- package/dist/components/ui/hover-card.d.ts +51 -0
- package/dist/components/ui/hover-card.d.ts.map +1 -0
- package/dist/components/ui/hover-card.js +101 -0
- package/dist/components/ui/hover-card.js.map +1 -0
- package/dist/components/ui/input-group.d.ts +62 -0
- package/dist/components/ui/input-group.d.ts.map +1 -0
- package/dist/components/ui/input-group.js +115 -0
- package/dist/components/ui/input-group.js.map +1 -0
- package/dist/components/ui/input-otp.d.ts +52 -0
- package/dist/components/ui/input-otp.d.ts.map +1 -0
- package/dist/components/ui/input-otp.js +89 -0
- package/dist/components/ui/input-otp.js.map +1 -0
- package/dist/components/ui/input.d.ts +47 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +47 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/item.d.ts +74 -0
- package/dist/components/ui/item.d.ts.map +1 -0
- package/dist/components/ui/item.js +176 -0
- package/dist/components/ui/item.js.map +1 -0
- package/dist/components/ui/kbd.d.ts +41 -0
- package/dist/components/ui/kbd.d.ts.map +1 -0
- package/dist/components/ui/kbd.js +58 -0
- package/dist/components/ui/kbd.js.map +1 -0
- package/dist/components/ui/key-value-pair.d.ts +56 -0
- package/dist/components/ui/key-value-pair.d.ts.map +1 -0
- package/dist/components/ui/key-value-pair.js +110 -0
- package/dist/components/ui/key-value-pair.js.map +1 -0
- package/dist/components/ui/label.d.ts +83 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +85 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/marker.d.ts +54 -0
- package/dist/components/ui/marker.d.ts.map +1 -0
- package/dist/components/ui/marker.js +79 -0
- package/dist/components/ui/marker.js.map +1 -0
- package/dist/components/ui/menubar.d.ts +72 -0
- package/dist/components/ui/menubar.d.ts.map +1 -0
- package/dist/components/ui/menubar.js +174 -0
- package/dist/components/ui/menubar.js.map +1 -0
- package/dist/components/ui/message-scroller.d.ts +53 -0
- package/dist/components/ui/message-scroller.d.ts.map +1 -0
- package/dist/components/ui/message-scroller.js +95 -0
- package/dist/components/ui/message-scroller.js.map +1 -0
- package/dist/components/ui/message.d.ts +63 -0
- package/dist/components/ui/message.d.ts.map +1 -0
- package/dist/components/ui/message.js +92 -0
- package/dist/components/ui/message.js.map +1 -0
- package/dist/components/ui/meta-list.d.ts +55 -0
- package/dist/components/ui/meta-list.d.ts.map +1 -0
- package/dist/components/ui/meta-list.js +75 -0
- package/dist/components/ui/meta-list.js.map +1 -0
- package/dist/components/ui/native-select.d.ts +51 -0
- package/dist/components/ui/native-select.d.ts.map +1 -0
- package/dist/components/ui/native-select.js +75 -0
- package/dist/components/ui/native-select.js.map +1 -0
- package/dist/components/ui/navigation-menu.d.ts +56 -0
- package/dist/components/ui/navigation-menu.d.ts.map +1 -0
- package/dist/components/ui/navigation-menu.js +116 -0
- package/dist/components/ui/navigation-menu.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +223 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +233 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +68 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +145 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +61 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +144 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/progress.d.ts +35 -0
- package/dist/components/ui/progress.d.ts.map +1 -0
- package/dist/components/ui/progress.js +46 -0
- package/dist/components/ui/progress.js.map +1 -0
- package/dist/components/ui/questionnaire.d.ts +92 -0
- package/dist/components/ui/questionnaire.d.ts.map +1 -0
- package/dist/components/ui/questionnaire.js +277 -0
- package/dist/components/ui/questionnaire.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +50 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +65 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/resizable.d.ts +44 -0
- package/dist/components/ui/resizable.d.ts.map +1 -0
- package/dist/components/ui/resizable.js +73 -0
- package/dist/components/ui/resizable.js.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +55 -0
- package/dist/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/components/ui/scroll-area.js +72 -0
- package/dist/components/ui/scroll-area.js.map +1 -0
- package/dist/components/ui/select.d.ts +274 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +609 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/separator.d.ts +31 -0
- package/dist/components/ui/separator.d.ts.map +1 -0
- package/dist/components/ui/separator.js +38 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/sheet.d.ts +59 -0
- package/dist/components/ui/sheet.d.ts.map +1 -0
- package/dist/components/ui/sheet.js +128 -0
- package/dist/components/ui/sheet.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +166 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +512 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +35 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +46 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/slider.d.ts +39 -0
- package/dist/components/ui/slider.d.ts.map +1 -0
- package/dist/components/ui/slider.js +75 -0
- package/dist/components/ui/slider.js.map +1 -0
- package/dist/components/ui/sonner.d.ts +56 -0
- package/dist/components/ui/sonner.d.ts.map +1 -0
- package/dist/components/ui/sonner.js +96 -0
- package/dist/components/ui/sonner.js.map +1 -0
- package/dist/components/ui/spinner.d.ts +37 -0
- package/dist/components/ui/spinner.d.ts.map +1 -0
- package/dist/components/ui/spinner.js +51 -0
- package/dist/components/ui/spinner.js.map +1 -0
- package/dist/components/ui/stepper.d.ts +84 -0
- package/dist/components/ui/stepper.d.ts.map +1 -0
- package/dist/components/ui/stepper.js +145 -0
- package/dist/components/ui/stepper.js.map +1 -0
- package/dist/components/ui/switch.d.ts +36 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +44 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +80 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +130 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +87 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +128 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +35 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +38 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/time-picker.d.ts +58 -0
- package/dist/components/ui/time-picker.d.ts.map +1 -0
- package/dist/components/ui/time-picker.js +188 -0
- package/dist/components/ui/time-picker.js.map +1 -0
- package/dist/components/ui/toggle-group.d.ts +52 -0
- package/dist/components/ui/toggle-group.d.ts.map +1 -0
- package/dist/components/ui/toggle-group.js +94 -0
- package/dist/components/ui/toggle-group.js.map +1 -0
- package/dist/components/ui/toggle.d.ts +50 -0
- package/dist/components/ui/toggle.d.ts.map +1 -0
- package/dist/components/ui/toggle.js +70 -0
- package/dist/components/ui/toggle.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +51 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +78 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/hooks/use-dark-mode.d.ts +2 -0
- package/dist/hooks/use-dark-mode.d.ts.map +1 -0
- package/dist/hooks/use-dark-mode.js +36 -0
- package/dist/hooks/use-dark-mode.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +2 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +20 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/lib/scope.d.ts +64 -0
- package/dist/lib/scope.d.ts.map +1 -0
- package/dist/lib/scope.js +74 -0
- package/dist/lib/scope.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +11 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/theme-tokens.css +537 -0
- package/dist/theme.css +417 -0
- package/package.json +103 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-tabs.js","names":[],"sources":["../../../src/components/ui/browser-tabs.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * BrowserTabs — persistent, closable document tabs, browser-style.\n *\n * The same Radix Tabs primitive `tabs.tsx` wraps, plus Popover for the \"+\"\n * menu, Tooltip for truncated names, and Button for the per-tab close\n * action — a different visual/interaction language from Tabs, not a\n * variant of it: Tabs switches between named views of one entity (2-6 fixed\n * tabs); this is for many independent, closable documents/views open at\n * once (an IDE's editor tabs, a browser's own tab strip) — always\n * horizontal, and always the topmost chrome of whatever surface it sits on.\n * Each tab floats as a rounded pill on the strip's own bg-secondary track,\n * flush against its bottom edge — the active tab's bg-card fill then\n * touches the card content's own bg-card directly beneath it with no grey\n * left in between, merging the two Chrome-style, rather than reading as a\n * colored/selected state the way Tabs' own default variant does. Pinned\n * tabs render icon-only — a fixed favicon-size square, falling back to a\n * generic FileIcon when the tab itself has none — same as a real browser's\n * own pinned tabs, and never part of the overflow behavior below. Once\n * unpinned tabs stop fitting, they shrink together down to a floor, then\n * the strip scrolls that pool horizontally past that — the same overflow\n * model a real browser's own tab strip uses, not a \"More\" menu hiding the\n * rest.\n * An optional \"+\" pill at the strip's end, outside the scrolling region\n * (`addViews` on BrowserTabsList) opens a popover of product-defined views\n * the user can open as new tabs — a worklist's saved filters, for\n * instance. `addTrigger`/`renderAddContent` swap its trigger or its\n * popover content for something fully custom.\n */\n\nimport * as React from \"react\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\nimport {\n CopyXIcon,\n FileIcon,\n PinIcon,\n PinOffIcon,\n PlusIcon,\n XIcon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n ContextMenu,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuSeparator,\n ContextMenuTrigger,\n} from \"@/components/ui/context-menu\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { TabsContent as BrowserTabsContent } from \"@/components/ui/tabs\"\n\n// Tracks the active tab's value outside of Radix's own `data-state`\n// attribute — BrowserTab wraps its Trigger in a Tooltip via a second\n// `asChild`, and Tooltip.Trigger sets its own `data-state` (open/closed of\n// the tooltip) which clobbers Tabs.Trigger's `data-state` (active/inactive)\n// when both are merged onto the same DOM node. Reading the active value\n// from context instead of that attribute sidesteps the collision entirely.\nconst BrowserTabsActiveContext = React.createContext<string | undefined>(\n undefined\n)\n\nfunction BrowserTabs({\n className,\n value,\n defaultValue,\n onValueChange,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)\n const activeValue = value ?? uncontrolledValue\n\n return (\n <BrowserTabsActiveContext.Provider value={activeValue}>\n <TabsPrimitive.Root\n data-slot=\"browser-tabs\"\n className={cn(\"flex flex-col\", className)}\n value={value}\n defaultValue={defaultValue}\n onValueChange={(next) => {\n setUncontrolledValue(next)\n onValueChange?.(next)\n }}\n // Radix Tabs defaults to \"automatic\" (focus alone activates a\n // tab) — arrow-keying across the strip to find one would\n // silently switch the active tab (and so the visible content)\n // on every step along the way. \"manual\" requires an actual\n // click/Enter/Space, which is what a tab strip should do anyway.\n activationMode=\"manual\"\n {...props}\n />\n </BrowserTabsActiveContext.Provider>\n )\n}\n\n// The pill chrome every item on the strip shares — real tabs, the items\n// inside the \"+\" popover, and (minus the text padding) the \"+\" trigger\n// itself — so everything the strip renders reads as one family.\n// Active/pinned/disabled modifiers layer on top per use. No `shrink-0`\n// here — unlike the \"+\" trigger (fixed-size), every tab, pinned or not,\n// needs to actually shrink under space pressure down to its own\n// `min-w-16` floor (see BrowserTabShell's own className below), so\n// leaving flex-shrink at its default (1) here is deliberate, not an\n// oversight.\nconst BROWSER_TAB_PILL_CLASS = cn(\n // `text-left` overrides the browser's own default `button { text-align:\n // center }` — invisible on a real tab (its label sits in a `<div>` via\n // `asChild`, never a raw `<button>`), but without it every popover item\n // below (a real `<button>`) centered its label in the flex-1 space\n // after the icon instead of reading flush against it.\n \"group/browser-tab relative flex h-7 max-w-52 cursor-pointer items-center gap-1.5 rounded-md border border-transparent px-2.5 text-left text-sm font-medium text-muted-foreground transition-colors outline-none select-none\",\n // Same hover fill Tabs' own default variant uses against its\n // bg-secondary track — bg-background/60 is nearly invisible here\n // since --color-background and --color-secondary are the same\n // value in light mode.\n \"hover:bg-muted hover:text-foreground dark:hover:text-foreground\",\n \"focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset\"\n)\n\n// Past this many `addViews`, the \"+\" popover grows a search box above the\n// list — same \"only past a real threshold\" convention DataTable's own\n// long option lists use, rather than always showing one even for 2-3\n// views where it'd just be clutter.\nconst ADD_VIEWS_SEARCH_THRESHOLD = 6\n\n/** One entry in the \"+\" popover — a product-defined view the user can open as a new tab. */\ntype BrowserTabsAddView = {\n id: string\n label: string\n /** A leading icon, same convention as BrowserTab's own `icon`. */\n icon?: React.ComponentType<{ className?: string }>\n /** A muted line of secondary metadata under the label — a description, a count, a last-modified date. Same convention as DataTable's `FilterOption.description`. */\n description?: string\n disabled?: boolean\n /** A hint that this view should open as a pinned tab (`\"user\"`/`\"product\"`, same meaning as `BrowserTab`'s own `pin`) — shown here as a small pin icon trailing the label. Purely informational: this component doesn't open tabs itself, so acting on it (passing the matching `pin` to the `BrowserTab` you add in `onAddView`) is the consumer's job. */\n pin?: \"user\" | \"product\"\n}\n\n/** Children as a flat list of elements, looking through Fragments so tabs grouped in `<>…</>` still split into pinned/unpinned. */\nfunction flattenTabChildren(children: React.ReactNode): React.ReactElement[] {\n return React.Children.toArray(children).flatMap((child) => {\n if (!React.isValidElement(child)) return []\n if (child.type === React.Fragment) {\n // The nested `toArray` restarts its keys at `.0`, so prefix each with\n // the Fragment's own key — otherwise tabs from different Fragments\n // (or a Fragment and its siblings) collide.\n return flattenTabChildren(\n (child.props as { children?: React.ReactNode }).children\n ).map((inner) =>\n React.cloneElement(inner, { key: `${child.key}${inner.key}` })\n )\n }\n return [child]\n })\n}\n\nfunction BrowserTabsList({\n className,\n children,\n addViews,\n onAddView,\n addLabel = \"Open a view in a new tab\",\n addTrigger,\n renderAddContent,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> & {\n /** Product-defined views the user can open as new tabs — rendered in the \"+\" pill's default popover list. Omit entirely (with `renderAddContent` covering the \"+\" pill's content instead) for a fully custom popover. Opening is the consumer's job — `onAddView` only reports the pick; add the tab to your own list (or, if that view is already open, just activate it). A view's own `pin` is a hint for *that*: open it pinned (`\"user\"`/`\"product\"`, same meaning as `BrowserTab`'s own `pin`) rather than as a regular tab — shown as a small pin icon next to it here, not enforced by this component. */\n addViews?: BrowserTabsAddView[]\n /** Fires with the picked view; the popover closes itself. */\n onAddView?: (view: BrowserTabsAddView) => void\n /** Accessible name of the \"+\" pill (it's icon-only). Defaults to \"Open a view in a new tab\". Ignored when `addTrigger` is given — that node is responsible for its own accessible name. */\n addLabel?: string\n /** A custom trigger in place of the default icon-only \"+\" pill — still opens the same popover (`addViews`' list, or `renderAddContent`). Needs its own accessible name (a plain icon button still needs `aria-label`, same as any custom Popover trigger). */\n addTrigger?: React.ReactNode\n /** Replaces the \"+\" popover's default views list with fully custom content — a search box, a grouped menu, anything. Called with `close` to dismiss the popover from inside it (the default list's own item clicks do this automatically; custom content must call it itself). Takes over the popover entirely: `addViews`/`onAddView` don't render alongside it. */\n renderAddContent?: (close: () => void) => React.ReactNode\n}) {\n const hasAdd = Boolean((addViews && addViews.length > 0) || renderAddContent)\n const items = React.useMemo(() => flattenTabChildren(children), [children])\n const pinnedItems = React.useMemo(\n () => items.filter((item) => (item.props as { pin?: unknown }).pin),\n [items]\n )\n const restItems = React.useMemo(\n () => items.filter((item) => !(item.props as { pin?: unknown }).pin),\n [items]\n )\n const hasDivider = pinnedItems.length > 0 && restItems.length > 0\n\n // Chrome-style overflow, not a \"More\" popover: every unpinned tab\n // shrinks together (flex-shrink, down to BrowserTabShell's own\n // `min-w-16`) as more open, and only once they're already at that floor\n // does this region scroll horizontally instead of shrinking further — no\n // width measurement needed for any of that, the browser's own flex\n // layout does it. Pinned tabs are already icon-only fixed squares (see\n // BrowserTabShell), so they never compete for shrink space or enter this\n // scrolling region themselves, and the \"+\" pill stays a fixed-size\n // sibling outside it too — same as a real browser's own pinned tabs and\n // \"+\" button, neither of which shrink or scroll away.\n const restScrollRef = React.useRef<HTMLDivElement>(null)\n const activeValue = React.useContext(BrowserTabsActiveContext)\n\n React.useEffect(() => {\n const container = restScrollRef.current\n if (!container || activeValue === undefined) return\n const activeTab = container.querySelector<HTMLElement>(\n `[data-slot=\"browser-tab\"][data-value=\"${CSS.escape(activeValue)}\"]`\n )\n activeTab?.scrollIntoView({ block: \"nearest\", inline: \"nearest\" })\n }, [activeValue])\n\n // The visual fade itself is handled entirely by the `scroll-fade-x`\n // class below (a native scroll-driven CSS animation — no JS). This\n // state exists only to tell the \"+\" pill below whether the rest-tabs\n // region is actually overflowing right now, so it knows whether to\n // hug the last tab or pin to the strip's far right edge.\n const [canScrollLeft, setCanScrollLeft] = React.useState(false)\n const [canScrollRight, setCanScrollRight] = React.useState(false)\n React.useEffect(() => {\n const container = restScrollRef.current\n if (!container) return\n function measure() {\n if (!container) return\n setCanScrollLeft(container.scrollLeft > 0)\n setCanScrollRight(\n container.scrollLeft + container.clientWidth < container.scrollWidth\n )\n }\n measure()\n container.addEventListener(\"scroll\", measure)\n const observer = new ResizeObserver(measure)\n observer.observe(container)\n return () => {\n container.removeEventListener(\"scroll\", measure)\n observer.disconnect()\n }\n // Re-measure when tabs are added/closed too: the container's own box\n // doesn't resize then (it flex-fills the strip), so neither the scroll\n // listener nor the ResizeObserver above would fire.\n }, [restItems.length])\n // Whichever single edge is or isn't currently scrollable, *either* one\n // being true means the rest tabs are structurally overflowing right\n // now (the only way both are false is scrollWidth <= clientWidth, i.e.\n // no overflow at all) — that's the signal \"+\" itself pins to the\n // strip's right edge on, below.\n const hasOverflow = canScrollLeft || canScrollRight\n\n return (\n // A single TabsPrimitive.List wraps every BrowserTab — Radix's Trigger\n // needs to be a React descendant of List for its internal roving-focus\n // group context.\n <TabsPrimitive.List\n data-slot=\"browser-tabs-list\"\n // The strip is NOT the tablist. Radix's List would be, but the strip\n // also holds the \"+\" button — and a tablist may only contain tabs\n // (aria-required-children). So the strip is presentational and the\n // real `role=\"tablist\"` is the inner div below, wrapping only tabs.\n // Radix's roving focus and context still come from this List.\n // `aria-orientation` isn't allowed on a presentational element, so\n // Radix's own is cleared here and set on the inner tablist instead.\n role=\"presentation\"\n aria-orientation={undefined}\n className={cn(\n // Unlike DataTable's toolbar/footer (nested inside CardContent,\n // which has real px-(--card-spacing) padding to pull out of),\n // this sits directly against a p-0 Card with no padding at all —\n // so it's already flush with the Card's edges without a pull-out.\n // `pt` is deliberately derived from the same --card-spacing token\n // as px (half of it), not a flat py-1.5 — that keeps the strip's\n // left/top inset at a fixed 2:1 ratio regardless of which --card-\n // spacing value it inherits (default Card, size=\"sm\", or the\n // 0.5rem/0.25rem fallback outside one entirely). No `pb` and no\n // explicit border: the tray is `bg-secondary` (a distinct grey from\n // the `bg-card` content below it), the boundary between them reads\n // from that color contrast alone. Inactive pills stop 4px short of the\n // tray's bottom edge; the active tab (see BrowserTabShell) is\n // 4px taller and reaches it, so its `bg-card` fill touches the\n // content's `bg-card` with nothing grey left between them,\n // Chrome-style. A `pb` here would reintroduce exactly that gap as\n // a visible grey sliver under the active tab.\n \"relative flex items-start gap-1 bg-secondary px-(--card-spacing,0.5rem) pt-[calc(var(--card-spacing,0.5rem)*0.5)]\",\n className\n )}\n {...props}\n >\n <div\n role=\"tablist\"\n aria-orientation=\"horizontal\"\n // The outer List is presentational, which strips ARIA naming —\n // so the consumer's label has to land on the real tablist.\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n // No `flex-1` on this row itself — sized to its own content\n // (pinned group + divider + however wide the rest tabs currently\n // are), not stretched to fill the strip. That's what lets \"+\"\n // sit immediately after the last tab below when there's room to\n // spare, instead of always being shoved to the strip's far right\n // with a gap in between.\n //\n // `data-overflowing` (set from `hasOverflow` below) is the hook\n // every tab — pinned or nested inside the rest-tabs scroll region\n // — reads via `group-data-overflowing/tablist:flex-1` to switch\n // from hugging its own content to claiming an equal share of the\n // strip once tabs no longer all fit at their natural size. Kept\n // off otherwise so a lone short tab doesn't stretch to fill idle\n // space when there's nothing to compete for.\n data-overflowing={hasOverflow || undefined}\n className=\"group/tablist flex min-h-8 min-w-0 items-start gap-1\"\n >\n {\n // Rendered as direct children of this row, not wrapped in their\n // own nested flex div — each pinned tab is a fixed-size\n // icon-only square (see BrowserTabShell) that never shrinks, so\n // no wrapper is needed to keep it out of the unpinned pool's own\n // shrink/scroll competition.\n pinnedItems\n }\n {hasDivider && (\n <div aria-hidden=\"true\" className=\"h-7 w-px shrink-0 bg-border\" />\n )}\n <div\n ref={restScrollRef}\n // No `tabIndex` here, and stays `role=\"presentation\"`\n // unconditionally — a `role=\"tablist\"` ancestor requires every\n // child to be `role=\"tab\"` (or exempted \"presentation\"/\"none\"),\n // and a *focusable* \"presentation\" element isn't a valid\n // instance of that exemption (a role meant to hide something\n // from the accessibility tree, on an element that's also a\n // keyboard tab stop, is self-contradictory — axe flags it as\n // aria-required-children, correctly). Keyboard reach into this\n // region already comes from the real, focusable `role=\"tab\"`\n // elements inside it (Radix's own roving tabindex) plus the\n // active-tab `scrollIntoView` above — not from the wrapper\n // itself needing to be a tab stop too.\n role=\"presentation\"\n // `-mt-*` cancels the strip's own `pt` (reclaiming it, the same\n // bleed-to-edge trick Table/DataTable use for their own edge-to-\n // edge treatments) and `pt-*` puts the identical amount back as\n // this element's own padding — net zero for where the tab pills\n // themselves sit, but now *this* box's true top edge reaches the\n // strip's real top edge instead of stopping short at the pills'\n // own content height. Without it, both the scroll fade and the\n // actually-scrollable area were confined to a thin band no\n // taller than the tabs themselves, well short of the full strip\n // — a mouse over the padding above a tab couldn't scroll it, and\n // the fade never touched the strip's real top edge. No matching\n // bottom bleed: the strip has no `pb` of its own to reclaim (see\n // above) — this box's bottom edge already sits flush with the\n // strip's real bottom edge, which is exactly what the merge\n // effect needs.\n className={cn(\n // NOTE: this design system does ship a shared `scroll-fade-x`\n // mask utility (also referenced by AttachmentGroup) that would\n // be the more correct tool here — a mask-image gradient driven\n // by a native scroll-linked CSS animation, fading the tabs to\n // true transparency instead of tinting them, with no JS scroll\n // listener needed. It was tried here and reverted: verified via\n // Storybook (both this strip and AttachmentGroup's own usage)\n // that its scroll-driven animation never engages in this build\n // — `--scroll-fade-s`/`-e` stay `0px` and no fade paints at any\n // scroll position — so it's a pre-existing, sitewide bug in\n // that utility, not something safe to depend on yet. Revisit\n // once that's fixed.\n \"-mt-[calc(var(--card-spacing,0.5rem)*0.5)] flex min-w-0 [scrollbar-width:thin] items-start gap-1 overflow-x-auto pt-[calc(var(--card-spacing,0.5rem)*0.5)] focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none focus-visible:ring-inset\",\n // Same bleed, horizontally — only on whichever side actually\n // borders the strip's own raw edge. A pinned group (if any)\n // already sits flush against the left edge on its own, so\n // the left bleed only applies without one; the right bleed\n // only applies without a \"+\" pill, which otherwise always\n // sits between this and the strip's true right edge (hugging\n // it at rest, pinned to it once overflowing — either way,\n // never this element itself). Without this, the strip's own\n // `px` padding left an unfaded, unscrollable sliver of dead\n // space between the true edge and wherever the fade/scroll\n // actually started.\n pinnedItems.length === 0 &&\n \"-ml-(--card-spacing,0.5rem) pl-(--card-spacing,0.5rem)\",\n !hasAdd && \"-mr-(--card-spacing,0.5rem) pr-(--card-spacing,0.5rem)\",\n // The active tab's inverted corners hang 0.5rem outside its\n // left/right edges, and this box clips overflow — so on a side\n // that doesn't already carry the bleed padding above (next to\n // the pinned group / the \"+\" pill), reserve exactly that much\n // room, cancelled by an equal negative margin so tab positions\n // don't move.\n pinnedItems.length > 0 && \"-ml-2 pl-2\",\n hasAdd && \"-mr-2 pr-2\"\n )}\n // Both edges can need a fade at once (scrolled to the middle of\n // a long strip) — that's two comma-separated shadows in one\n // `box-shadow` value, not two separate Tailwind `shadow-*`\n // classes: `cn`/tailwind-merge treats those as the same utility\n // group and would just keep the last one, silently dropping the\n // other edge's fade. The color is `--color-foreground` through\n // `color-mix` (near-black in light mode, near-white in dark\n // mode via the token's own dark-mode override) rather than a\n // literal `rgba(0,0,0,...)` — a hardcoded black reads as a gray\n // smudge and stays black even in dark mode, where the strip's\n // own surface is dark and the fade should trend light instead.\n style={{\n boxShadow: [\n canScrollLeft &&\n \"inset 8px 0 8px -8px color-mix(in srgb, var(--color-foreground) 20%, transparent)\",\n canScrollRight &&\n \"inset -8px 0 8px -8px color-mix(in srgb, var(--color-foreground) 20%, transparent)\",\n ]\n .filter(Boolean)\n .join(\", \"),\n // This container is one flex item in the outer row, but it\n // bundles `restItems.length` actual tabs inside it — each of\n // which claims an equal share (`flex-1`, i.e. `1 1 0%`) among\n // themselves once `data-overflowing` is set (see the pinned\n // tabs' own className for the same mechanism). For that\n // equal share to come out the same size as a *pinned* tab's\n // own equal share in the outer row, this container's own\n // growth needs to be worth exactly that many shares — plain\n // `flex-1` here would only ever claim one share total, no\n // matter how many tabs it holds, undersizing every tab\n // inside it once there's more than one.\n ...(hasOverflow && restItems.length > 0\n ? { flexGrow: restItems.length, flexShrink: 1, flexBasis: 0 }\n : undefined),\n }}\n >\n {restItems}\n </div>\n </div>\n {hasAdd && (\n <BrowserTabsAdd\n views={addViews ?? []}\n onAddView={onAddView}\n label={addLabel}\n trigger={addTrigger}\n renderContent={renderAddContent}\n // Immediately after the last tab at rest (the tablist above no\n // longer stretches to claim the strip's full width) — but\n // pinned to the strip's own right edge, same as a real\n // browser's \"+\" button, once the rest tabs actually overflow\n // and start scrolling.\n className={hasOverflow ? \"ml-auto\" : undefined}\n />\n )}\n </TabsPrimitive.List>\n )\n}\n\n// The \"+\" pill: same pill chrome and the same Popover as every real tab.\n// Icon-only by default (a lone PlusIcon) — the strip is where horizontal\n// room is scarcest, and \"+\" at the end of a tab row is a browser\n// convention that needs no visible text; `label` names it for AT. Always\n// fixed-size and outside the shrinking/scrolling tab pool, same as a real\n// browser's own \"+\" button never scrolls away with its tabs.\nfunction BrowserTabsAdd({\n views,\n onAddView,\n label,\n trigger,\n renderContent,\n className,\n}: {\n views: BrowserTabsAddView[]\n onAddView?: (view: BrowserTabsAddView) => void\n label: string\n trigger?: React.ReactNode\n renderContent?: (close: () => void) => React.ReactNode\n className?: string\n}) {\n const [open, setOpen] = React.useState(false)\n const [query, setQuery] = React.useState(\"\")\n const close = React.useCallback(() => {\n setOpen(false)\n setQuery(\"\")\n }, [])\n const showSearch = views.length > ADD_VIEWS_SEARCH_THRESHOLD\n const visibleViews = React.useMemo(() => {\n if (!showSearch || !query.trim()) return views\n const q = query.trim().toLowerCase()\n return views.filter(\n (view) =>\n view.label.toLowerCase().includes(q) ||\n view.description?.toLowerCase().includes(q)\n )\n }, [views, showSearch, query])\n\n return (\n <div className={cn(\"shrink-0\", className)}>\n <Popover\n open={open}\n onOpenChange={(next) => {\n setOpen(next)\n if (!next) setQuery(\"\")\n }}\n >\n <PopoverTrigger asChild>\n {trigger ?? (\n <button\n type=\"button\"\n data-slot=\"browser-tabs-add\"\n aria-label={label}\n className={cn(\n // A square h-7 pill with the same hover fill every tab\n // pill uses, and the same \"stays pressed while its\n // popover is open\" as Select/DropdownMenu triggers.\n \"relative flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent text-muted-foreground transition-colors outline-none select-none\",\n \"hover:bg-muted hover:text-foreground dark:hover:text-foreground\",\n \"focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset\",\n open && \"bg-muted text-foreground\"\n )}\n >\n <PlusIcon className=\"size-4 shrink-0\" />\n </button>\n )}\n </PopoverTrigger>\n {/* align=\"start\": this pill trails the last tab wherever that\n lands, so its popover should open to the right of it, not\n hang off to the left. No forced width/padding once\n `renderContent` takes over — that content owns its own\n sizing entirely. */}\n <PopoverContent\n align=\"start\"\n className={renderContent ? undefined : \"w-64 p-1.5\"}\n >\n {renderContent ? (\n renderContent(close)\n ) : (\n <div className=\"flex flex-col gap-1.5\">\n {showSearch && (\n <Input\n autoFocus\n placeholder=\"Search...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n />\n )}\n {/* Each view is the exact pill it will become as a tab\n (BROWSER_TAB_PILL_CLASS, icon + label, stretched\n full-width and left-aligned) — opening it is the only\n thing a click here does differently from a real tab. A\n `description` grows the row to two lines instead of\n the shared `h-7`, same as DataTable's own filter\n options with metadata. */}\n <div className=\"flex flex-col gap-0.5\" role=\"menu\">\n {visibleViews.length === 0 && (\n <p className=\"p-1.5 text-sm text-muted-foreground\">\n No matches\n </p>\n )}\n {visibleViews.map((view) => {\n const Icon = view.icon\n return (\n <button\n key={view.id}\n type=\"button\"\n role=\"menuitem\"\n disabled={view.disabled}\n onClick={() => {\n onAddView?.(view)\n close()\n }}\n className={cn(\n BROWSER_TAB_PILL_CLASS,\n \"w-full max-w-none justify-start\",\n view.description && \"h-auto py-1.5\",\n \"disabled:pointer-events-none disabled:opacity-50\"\n )}\n >\n {Icon && <Icon className=\"size-3.5 shrink-0\" />}\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate\">{view.label}</span>\n {view.description && (\n <span className=\"truncate text-xs text-muted-foreground\">\n {view.description}\n </span>\n )}\n </span>\n {view.pin && (\n <PinIcon className=\"size-3.5 shrink-0 text-muted-foreground\" />\n )}\n </button>\n )\n })}\n </div>\n </div>\n )}\n </PopoverContent>\n </Popover>\n </div>\n )\n}\n\ntype BrowserTabProps = {\n value: string\n /** A leading icon (e.g. a favicon-style entity icon) — rendered outside the truncating label, so it's never clipped. */\n icon?: React.ComponentType<{ className?: string }>\n disabled?: boolean\n /** Groups this tab into the strip's pinned section, ahead of a divider from the rest. \"user\" tabs were pinned by the user and can still take `onClose` (closing is how the user unpins/removes them); \"product\" tabs are pinned by the product and never render a close button, regardless of `onClose`. */\n pin?: \"user\" | \"product\"\n /** Omit to render this tab with no close button — e.g. a pinned tab that can't be dismissed. Ignored when `pin=\"product\"`. The close button is hover/focus-revealed over the end of the name (which truncates underneath it), so the tab is content-width at rest and never changes width on hover; it's also offered as \"Close\" in the tab's right-click menu. */\n onClose?: () => void\n /** Accessible label for the close button. Defaults to \"Close {children}\" when `children` is a plain string; pass this explicitly when it isn't (an icon, a custom node). */\n closeLabel?: string\n /** Enables \"Pin tab\" / \"Unpin tab\" in this tab's right-click menu (the item is always shown, disabled without a handler or on a `pin=\"product\"` tab). Fires regardless of direction; read the tab's own current `pin` to decide whether you're pinning or unpinning it, then move it into (or to the front of) the appropriate section — BrowserTabs only renders what you give it, it doesn't reorder your list. There is no visible pin button on the tab itself. */\n onPinToggle?: () => void\n /** Enables \"Close other tabs\" in this tab's right-click menu (the item is always shown, disabled without a handler). Like `onClose`, it only reports the intent — close every other UNPINNED tab in your own list and keep this one active. Pinned tabs (`pin=\"user\"` or `\"product\"`) are never closed by \"close others\"; the user pinned them precisely so they'd survive that. */\n onCloseOthers?: () => void\n className?: string\n children: React.ReactNode\n}\n\n// The pill's visual markup, shared between the real (interactive) tab and\n// the hidden measuring copy in BrowserTabsList — the measuring copy must\n// NOT wrap a second TabsPrimitive.Trigger for the same `value`, since Radix\n// Tabs' active-state/roving-focus bookkeeping assumes one Trigger per value.\n// forwardRef + spreading `...rest` is required, not just style: this is the\n// direct child of `TabsPrimitive.Trigger asChild`, which merges its own\n// props (ref, data-state, role, aria-selected, onClick, onKeyDown, ...) onto\n// whatever it's given via cloneElement — a plain function component that\n// doesn't forward them would silently drop Radix's own trigger behavior.\n/** The truncating label — wraps itself in a Tooltip showing the full name, but only once its content actually overflows (`scrollWidth > clientWidth`), and only for the real interactive tab (never the invisible measuring copy). */\nfunction BrowserTabLabel({\n name,\n interactive,\n hoverReveal,\n children,\n}: {\n name?: string\n interactive: boolean\n /** True when a hover-revealed close button will need room at the label's end. */\n hoverReveal?: boolean\n children: React.ReactNode\n}) {\n const labelRef = React.useRef<HTMLSpanElement>(null)\n const [truncated, setTruncated] = React.useState(false)\n const [tooltipOpen, setTooltipOpen] = React.useState(false)\n\n React.useLayoutEffect(() => {\n if (!interactive) return\n const el = labelRef.current\n if (!el) return\n const check = () => {\n // A momentary 0-width read (seen under React StrictMode's dev-only\n // double-invoke of effects, mid remount) isn't a real \"not\n // truncated\" — ResizeObserver won't fire again once the box settles\n // at its true size without changing further, so latching onto a\n // bogus zero here would get stuck. Skip and retry next frame instead.\n if (el.clientWidth === 0) {\n rafId = requestAnimationFrame(check)\n return\n }\n setTruncated(el.scrollWidth > el.clientWidth)\n }\n let rafId = requestAnimationFrame(check)\n const observer = new ResizeObserver(check)\n observer.observe(el)\n return () => {\n cancelAnimationFrame(rafId)\n observer.disconnect()\n }\n }, [interactive, children])\n\n // Two copies of the name: an invisible one in normal flow that gives\n // the label (and so the tab) its content-based width, and the visible\n // one laid over it with `inset-0`. The tab is therefore exactly as wide\n // as its name at rest — no reserved slot, no extra space on the right.\n // On hover the visible copy gains right padding inside that fixed box,\n // so the text truncates under the close button that appears there and\n // the tab's width never changes.\n const label = (\n <span className=\"relative min-w-0 flex-1\">\n <span aria-hidden=\"true\" className=\"invisible block truncate\">\n {children}\n </span>\n <span\n ref={labelRef}\n className={cn(\n \"absolute inset-0 truncate\",\n hoverReveal &&\n \"transition-[padding] group-focus-within/browser-tab:pr-5 group-hover/browser-tab:pr-5\"\n )}\n >\n {children}\n </span>\n </span>\n )\n\n if (!name) return label\n\n // Always wrapped (never conditionally) so the label span keeps its\n // identity when `truncated` flips — a remount would leave the observer\n // above attached to a detached node. Always controlled (never flipping\n // between controlled and uncontrolled); `truncated` gates it so the\n // tooltip stays closed while the name fits.\n return (\n <Tooltip open={truncated && tooltipOpen} onOpenChange={setTooltipOpen}>\n <TooltipTrigger asChild>{label}</TooltipTrigger>\n <TooltipContent>{name}</TooltipContent>\n </Tooltip>\n )\n}\n\n/** A pinned tab's entire visible content — icon only, no label, favicon-style\n * like a real browser's own pinned tabs. Falls back to `FileIcon` — this\n * design system's existing entity-agnostic \"generic item\" icon (Dropzone's\n * own fallback for a file type it doesn't otherwise recognize, as opposed to\n * `FileTextIcon`, which Dropzone reserves specifically for documents) — when\n * the tab has no `icon`, so a pinned tab is never a blank square. A tab can\n * represent any kind of view, not just documents, so the fallback shouldn't\n * imply one. `PinIcon` itself would be the wrong fallback here: this same\n * file already uses it for the \"pin\" *action* (the context menu's \"Pin tab\"\n * item, and the `addViews` pin hint), so reusing it as a tab's identity icon\n * would read as \"pin this\" rather than \"generic tab.\" The name still\n * surfaces on hover/focus via Tooltip (there's no visible text left to\n * convey it), and unconditionally rather than only-when-truncated the way\n * BrowserTabLabel's does — icon-only content is \"truncated\" by definition. */\nfunction BrowserPinnedTabIcon({\n icon: Icon,\n}: {\n icon?: React.ComponentType<{ className?: string }>\n}) {\n const PinnedIcon = Icon ?? FileIcon\n return <PinnedIcon className=\"size-3.5 shrink-0\" />\n}\n\nconst BrowserTabShell = React.forwardRef<\n HTMLDivElement,\n BrowserTabProps & {\n interactive: boolean\n } & React.HTMLAttributes<HTMLDivElement>\n>(function BrowserTabShell(\n {\n value,\n icon: Icon,\n pin,\n disabled,\n onClose,\n closeLabel,\n onPinToggle,\n onCloseOthers,\n className,\n children,\n interactive,\n ...rest\n },\n ref\n) {\n const activeValue = React.useContext(BrowserTabsActiveContext)\n const isActive = interactive && value === activeValue\n // Pinned tabs render icon-only (favicon-style, like a real browser's own\n // pinned tabs) — there's no room left for a hover-revealed close button\n // at that width, and no visible label for it to truncate under anyway.\n // Unpinning/closing a pinned tab still goes through the context menu or\n // Delete/Backspace (see `showHoverClose` below and the context menu item).\n const isPinned = Boolean(pin)\n const canClose = Boolean(onClose)\n const showHoverClose = canClose && !isPinned\n // Product pins are permanent — never user-toggleable, regardless of\n // whether a consumer passes onPinToggle for this tab.\n const canTogglePin = Boolean(onPinToggle) && pin !== \"product\"\n const canCloseOthers = Boolean(onCloseOthers)\n // Every interactive tab gets the same three-item menu; items the tab\n // wasn't given a handler for (or a product pin, which can't be unpinned)\n // are disabled rather than missing, so the menu always reads the same.\n const hasMenu = interactive\n const name = typeof children === \"string\" ? children : undefined\n\n const pill = (\n <div\n ref={ref}\n {...rest}\n // After `rest`, not before: our own Tooltip wrapper hardcodes\n // `data-slot=\"tooltip-trigger\"` on whatever it wraps, which flows\n // into `rest` through the nested-asChild prop merge — ours must win.\n data-slot=\"browser-tab\"\n // Not read by Radix (it tracks the active value internally) — this\n // is purely so BrowserTabsList can find *this* tab's DOM node by\n // value to scroll it into view when it becomes active.\n data-value={value}\n // `asChild` swaps in this `<div>` for whatever real element Radix's\n // Trigger would have rendered — its native `disabled` HTML attribute\n // (still cloned on via rest, harmlessly, since a div ignores it) has\n // no semantic effect here, so nothing told AT this tab was disabled\n // beyond the `data-disabled` styling hook.\n aria-disabled={disabled || undefined}\n // Keyboard path for closing, since the close button below is taken\n // out of the accessibility tree (a focusable control nested inside a\n // `role=\"tab\"` is invalid — nested-interactive). Composed with the\n // handler Radix's Trigger merged in via `rest`, so Enter/Space\n // activation keeps working.\n onKeyDown={(event) => {\n rest.onKeyDown?.(event)\n if (event.defaultPrevented || !interactive || !canClose) return\n if (event.key === \"Delete\" || event.key === \"Backspace\") {\n event.preventDefault()\n onClose?.()\n }\n }}\n // Icon-only pinned tabs have no visible label to expose their name —\n // give assistive tech the name directly instead. Unpinned tabs still\n // get theirs from the visible label text, same as before.\n aria-label={isPinned ? (name ?? rest[\"aria-label\"]) : rest[\"aria-label\"]}\n className={cn(\n BROWSER_TAB_PILL_CLASS,\n isPinned\n ? // Fixed favicon-size square, never shrinking/growing with the\n // rest of the strip's overflow dance below — a pinned tab is\n // already at its smallest useful size, there's nothing left to\n // shrink.\n \"size-7 shrink-0 justify-center px-0\"\n : [\n // `min-w-16` (64px) unconditionally — unpinned tabs shrink\n // under space pressure. This has to be a definite value, not\n // `min-w-0`/omitted: a flex item's automatic minimum size\n // (`min-width: auto`) resolves to its own content-based\n // minimum whenever its own `overflow` is visible — which it\n // is here — regardless of `min-w-0` on a *descendant* (the\n // label wrapper below); Chrome does not look through that to\n // shrink the pill itself, so without an explicit value here,\n // tabs nested inside the scrollable region (see\n // restScrollRef in BrowserTabsList) render at their full\n // content width and jump straight to scrolling instead of\n // shrinking first. Verified via Storybook: only a definite\n // `min-width` actually re-enables the shrink-before-scroll\n // behavior for that nested pool.\n \"min-w-16\",\n // Content-hugging (`flex-basis: auto`, the default) at\n // rest, so a short tab doesn't stretch to fill idle space —\n // but once tabs no longer all fit at their natural size\n // (`data-overflowing` on the shared `group/tablist`\n // ancestor above, set from `hasOverflow`), every tab\n // switches to `flex-1` (`1 1 0%`) instead, claiming an\n // equal share of the strip rather than each keeping its own\n // content-driven width. Reaches unpinned tabs (nested inside\n // the rest-tabs scroll region) since a `group-data-*`\n // selector matches through any depth of descendant, not\n // just direct children.\n \"group-data-overflowing/tablist:flex-1\",\n ],\n \"data-disabled:pointer-events-none data-disabled:opacity-50\",\n // The active tab merges into the card below it, Chrome-style,\n // rather than reading as a colored/selected state: `bg-card`/\n // `text-card-foreground` match the content surface exactly (both\n // already flip correctly in dark mode via the token itself, so no\n // manual `dark:` override is needed the way the old primary tint\n // required one), and `rounded-b-none` squares off the bottom\n // corners so no rounded sliver of the tray's `bg-secondary` shows\n // through where the pill meets the strip's flush bottom edge (see\n // BrowserTabsList's own `pt`-only padding). The `hover:` pair\n // repeats the same fill/text so hovering the already-active tab\n // doesn't flash into the generic `hover:bg-muted` treatment above.\n //\n // Chrome's tab-strip spec: inactive tabs are 28px pills inset 4px\n // from the strip's bottom edge, while the active tab is that same\n // pill plus the 4px — `h-8` — so it reaches the edge and fuses with\n // the content below. Every row above is `items-start`, so the top\n // edge stays put and only the active tab grows downward; inactive\n // tabs never shift when selection changes. `pb-1` keeps the\n // label/icon centered in the original 28px, not the taller box.\n isActive &&\n \"h-8 rounded-b-none bg-card pb-1 text-card-foreground hover:bg-card hover:text-card-foreground\",\n // Chrome's inverted corners: two 8px squares hung off the active\n // tab's bottom corners, filled with the card color except for a\n // quarter-circle cut-out centered on the square's top outer\n // corner — so the tab's edge curves outward into the content\n // surface instead of meeting it at a hard right angle. The -1px\n // offsets step outside the pill's 1px transparent border.\n isActive &&\n \"before:absolute before:-bottom-px before:left-[calc(-0.5rem-1px)] before:size-2 before:bg-[radial-gradient(circle_at_0_0,transparent_0.5rem,var(--color-card)_calc(0.5rem+0.5px))] before:content-[''] after:absolute after:right-[calc(-0.5rem-1px)] after:-bottom-px after:size-2 after:bg-[radial-gradient(circle_at_100%_0,transparent_0.5rem,var(--color-card)_calc(0.5rem+0.5px))] after:content-['']\",\n className\n )}\n >\n {isPinned ? (\n <BrowserPinnedTabIcon icon={Icon} />\n ) : (\n <>\n {Icon && <Icon className=\"size-3.5 shrink-0\" />}\n <BrowserTabLabel\n name={name}\n interactive={interactive}\n hoverReveal={showHoverClose}\n >\n {children}\n </BrowserTabLabel>\n </>\n )}\n {showHoverClose && (\n // Pointer-only affordance rendered as a <span>, not a <button>: a\n // focusable element inside a `role=\"tab\"` is a nested interactive\n // control (and a <button> stays programmatically focusable even\n // with tabIndex=-1). aria-hidden keeps it out of the accessibility\n // tree; keyboard users close with Delete/Backspace on the focused\n // tab (see the pill's onKeyDown) or via the right-click menu.\n // Button `asChild` keeps the secondary icon-button look.\n <Button asChild variant=\"secondary\" size=\"xs\" iconOnly>\n <span\n aria-hidden=\"true\"\n title={closeLabel ?? `Close ${name ?? value}`}\n // Absolutely positioned over the label's end, never a flex\n // participant, and hidden until the tab is hovered — so it\n // neither takes nor releases width when it appears; the name\n // truncates underneath it instead (see BrowserTabLabel).\n // Centered via `inset-y-0 my-auto`, not `top-1/2\n // -translate-y-1/2` — Button's own built-in press effect\n // (`active:translate-y-px`) sets the exact same `transform`\n // property, and on `:active` that rule's higher cascade\n // position wins outright rather than composing with ours,\n // wiping out the centering translate entirely for as long as\n // the mouse is held down (the button visibly drops by half its\n // own height mid-press). Margin-auto centering needs no\n // transform at all, so there's nothing left for the press\n // effect to clobber.\n className=\"absolute inset-y-0 right-1 my-auto size-5 cursor-pointer opacity-0 transition-opacity group-hover/browser-tab:opacity-100\"\n onClick={\n interactive\n ? (event) => {\n event.stopPropagation()\n onClose?.()\n }\n : undefined\n }\n >\n <XIcon className=\"size-3\" />\n </span>\n </Button>\n )}\n </div>\n )\n\n // The tooltip trigger is the whole pinned tab (not just its icon), so the\n // name shows on hover anywhere over the square and on keyboard focus of the\n // tab itself, and works with any `icon` component.\n const pinnedTooltip = isPinned && name\n const trigger = pinnedTooltip ? (\n <TooltipTrigger asChild>{pill}</TooltipTrigger>\n ) : (\n pill\n )\n const withTooltip = (node: React.ReactElement) =>\n pinnedTooltip ? (\n <Tooltip>\n {node}\n <TooltipContent>{name}</TooltipContent>\n </Tooltip>\n ) : (\n node\n )\n\n if (!hasMenu) return withTooltip(trigger)\n\n // Pin/unpin and \"close others\" live in a right-click menu rather than as\n // more hover icons: one hover-revealed control (close) is all the pill\n // has room for without truncating every name by default. Close is\n // repeated in the menu so every action here has a non-hover path too.\n return withTooltip(\n <ContextMenu>\n <ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>\n <ContextMenuContent>\n <ContextMenuItem\n disabled={!canTogglePin}\n onSelect={() => onPinToggle?.()}\n >\n {pin ? <PinOffIcon /> : <PinIcon />}\n {pin ? \"Unpin tab\" : \"Pin tab\"}\n </ContextMenuItem>\n <ContextMenuSeparator />\n <ContextMenuItem disabled={!canClose} onSelect={() => onClose?.()}>\n <XIcon />\n Close\n </ContextMenuItem>\n <ContextMenuItem\n disabled={!canCloseOthers}\n onSelect={() => onCloseOthers?.()}\n >\n <CopyXIcon />\n Close other tabs\n </ContextMenuItem>\n </ContextMenuContent>\n </ContextMenu>\n )\n})\n\nfunction BrowserTab(props: BrowserTabProps) {\n const { value, disabled } = props\n const canClose = Boolean(props.onClose) && props.pin !== \"product\"\n\n return (\n <TabsPrimitive.Trigger value={value} disabled={disabled} asChild>\n <BrowserTabShell\n {...props}\n onClose={canClose ? props.onClose : undefined}\n interactive\n />\n </TabsPrimitive.Trigger>\n )\n}\n\nexport { BrowserTabs, BrowserTabsList, BrowserTab, BrowserTabsContent }\nexport type { BrowserTabsAddView }\n\n/**\n * WHEN TO USE\n * - Many independent, addable/closable documents or views open at once —\n * an IDE's editor tabs, a browser's own tab strip, a multi-record\n * workspace where each record opens in its own tab.\n * - Always as the topmost chrome of whatever surface hosts it (a Card,\n * a page shell) — it's edge-to-edge and expects to sit flush at that\n * surface's top.\n *\n * NOT WHEN\n * - Switching between a fixed 2-6 named views of *one* entity (an\n * Overview/History/Notes split) — use Tabs instead; it reads as\n * neutral navigation, not as separate closable documents.\n * - Vertical tab placement — not supported; always horizontal.\n *\n * KEY RULES\n * - Composition mirrors Tabs: `BrowserTabs` (Root, controlled or\n * uncontrolled via `value`/`onValueChange`/`defaultValue`) wraps\n * `BrowserTabsList` (the strip) and one `BrowserTabsContent` per tab\n * (re-exported directly from Tabs — same primitive, nothing about it\n * is strip-specific).\n * - `pin`: omit for a normal tab, `\"user\"` for one the user pinned\n * (closable — closing is how they unpin it), `\"product\"` for one the\n * product configured (never closable, no close button rendered even if\n * `onClose` is passed). Pinned tabs of either kind render first, in a\n * section separated from the rest by a divider, and are never subject\n * to the overflow behavior below.\n * - Pinned tabs render icon-only — a fixed favicon-size square, same as a\n * real browser's own pinned tabs — falling back to `FileIcon`\n * when the tab itself has no `icon`, so a pinned tab is never blank.\n * The full name still surfaces via Tooltip on hover/focus; there's no\n * hover-revealed close button at that width, so unpinning/closing goes\n * through the right-click menu or Delete/Backspace on the focused tab.\n * - Every tab has the same three-item right-click menu (ContextMenu):\n * \"Pin tab\"/\"Unpin tab\", \"Close\", and \"Close other tabs\" — always\n * visible, each disabled when the tab wasn't given the matching\n * handler. `onPinToggle` drives the first (read the tab's current `pin`\n * to know the direction, then move it into — or, unpinning, to the\n * front of — the right section in your own list; always disabled on\n * `pin=\"product\"`), `onClose` the second, `onCloseOthers` the third\n * (close every other UNPINNED tab yourself and keep this one active —\n * \"close others\" never closes a pinned tab of either kind; pinning is\n * exactly the promise that a tab survives bulk closing).\n * No visible pin button on the tab; pinning is menu-only, and pinned\n * tabs are identified by sitting in the pinned section ahead of the\n * divider.\n * - Each `BrowserTab` needs its own `onClose` to get a close button at\n * all — omit it for a tab that can't be dismissed. A tab is exactly as\n * wide as its name at rest (nothing reserved on the right); on hover\n * the close button appears over the end of the name, which truncates\n * underneath it, so the tab's width never changes between rest and\n * hover. That button is pointer-only (a focusable control can't sit\n * inside a `role=\"tab\"`): keyboard users press Delete or Backspace on\n * the focused tab, or use the right-click menu's \"Close\".\n * - Closing a tab is entirely the consumer's responsibility: `onClose`\n * only reports the intent. Removing it from whatever list backs your\n * tabs, and deciding which tab becomes active next, is app logic this\n * component has no way to know on its own.\n * - Overflow is browser-style, not a \"More\" menu: once unpinned tabs stop\n * fitting the available width, they shrink together (flex-shrink) down\n * to `min-w-16` (64px) each; past that floor the strip scrolls that\n * pool horizontally instead of shrinking further. Pinned tabs are\n * already fixed-size icon-only squares (see above), so they never\n * shrink, grow, or join the scrolling region. The active tab scrolls\n * into view\n * automatically whenever it changes, so switching tabs never leaves the\n * newly-active one scrolled out of sight. Only the \"+\" pill is truly\n * fixed-size, living outside the shrink pool entirely. A soft inset\n * shadow fades in on\n * whichever edge(s) still have more tabs scrolled past them (both at\n * once, mid-strip) and disappears once there's nothing left that way —\n * a direction-aware cue, not a static \"this can scroll\" marker.\n * - A tab's name overflowing `max-w-52` is truncated in the strip but\n * shown in full in a Tooltip on hover/focus.\n * - `addViews` on `BrowserTabsList` adds a \"+\" pill that opens a popover of\n * product-defined views the user can open as new tabs — a worklist's\n * saved filters, a set of report layouts — `{ id, label, icon?,\n * description?, disabled?, pin? }` each. Sits immediately after the\n * last tab at rest; pins to the strip's own right edge once the tabs\n * overflow into their shrink/scroll region instead, same as a real\n * browser's \"+\" button. Each is the same full-width tab pill (icon +\n * label) it will become as a tab; `description` grows that row to a\n * second, muted line underneath (a filter's own scope, a last-modified\n * date). Past `ADD_VIEWS_SEARCH_THRESHOLD` (6) views the popover grows\n * a search box filtering by label and description. A `pin` hint renders\n * a small pin icon trailing the label (purely visual — opening it\n * pinned is still your own `onAddView` handler's job). `onAddView` only\n * reports the pick: add a tab for that view to your own list and make\n * it active — or, if that view is already open, just activate the\n * existing tab instead of opening a duplicate. Icon-only by default;\n * `addLabel` names it for assistive tech (default \"Open a view in a new\n * tab\"). `addTrigger` swaps in a custom trigger node in place of the\n * default \"+\" pill (still needs its own `aria-label`). `renderAddContent`\n * replaces the *entire* popover body — search box, view list, and all —\n * with something fully custom (a grouped menu, anything) — called with\n * a `close` function to dismiss the popover\n * from inside it; `addViews`/`onAddView` don't render alongside it.\n * - Each `BrowserTab` floats as a rounded pill inset from the strip's\n * `bg-secondary` track. Unlike Tabs' own default variant (a translucent\n * `bg-primary/10` fill), the active tab here reads as merged into the\n * card below it, Chrome-style: `bg-card`/`text-card-foreground` (matching\n * the content surface exactly) plus squared-off bottom corners, sitting\n * flush against the track's own bottom edge with nothing grey left\n * between tab and content. Inactive tabs just fill `bg-muted` on hover\n * and otherwise float clear of the track's edges.\n * - A leading icon uses `icon`, not the `data-icon=\"inline-start\"` slot\n * convention Button/Tabs use — it needs to sit outside the label's own\n * truncation, which a plain child node sharing that flex row wouldn't.\n *\n * USAGE\n * <BrowserTabs value={activeId} onValueChange={setActiveId}>\n * <BrowserTabsList\n * addViews={worklistViews} // [{ id: \"overdue\", label: \"Overdue\", icon: ClockIcon }, ...]\n * onAddView={(view) => openViewTab(view)} // add to openDocs (or activate if already open)\n * >\n * <BrowserTab value=\"home\" pin=\"product\">Home</BrowserTab>\n * {openDocs.map((doc) => (\n * <BrowserTab\n * key={doc.id}\n * value={doc.id}\n * icon={doc.icon}\n * pin={doc.pinnedByUser ? \"user\" : undefined}\n * onClose={() => closeDoc(doc.id)}\n * >\n * {doc.title}\n * </BrowserTab>\n * ))}\n * </BrowserTabsList>\n * <BrowserTabsContent value=\"home\">...</BrowserTabsContent>\n * {openDocs.map((doc) => (\n * <BrowserTabsContent key={doc.id} value={doc.id}>\n * <DocView doc={doc} />\n * </BrowserTabsContent>\n * ))}\n * </BrowserTabs>\n */\n"],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bubble — chat speech-bubble container.
|
|
3
|
+
*
|
|
4
|
+
* A colored, rounded container whose fill *is* the message surface — no
|
|
5
|
+
* avatar or metadata slots. BubbleContent holds the text/children;
|
|
6
|
+
* BubbleReactions overlays an emoji/count pill on a bubble. BubbleGroup
|
|
7
|
+
* stacks a conversation with consistent spacing. Shares the `align` prop
|
|
8
|
+
* convention with Message, and a Bubble dropped inside a Message's
|
|
9
|
+
* MessageContent will self-align to match Message's `align` automatically
|
|
10
|
+
* (they share a CSS group selector) — but most surfaces pick one shell,
|
|
11
|
+
* either bare bubbles or the structured Message, rather than mixing both.
|
|
12
|
+
*/
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import { type VariantProps } from "class-variance-authority";
|
|
15
|
+
declare function BubbleGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
16
|
+
declare const bubbleVariants: (props?: ({
|
|
17
|
+
tone?: "default" | "secondary" | "destructive" | "muted" | "tinted" | null | undefined;
|
|
18
|
+
emphasis?: "outline" | "ghost" | "filled" | null | undefined;
|
|
19
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
20
|
+
type BubbleTone = NonNullable<VariantProps<typeof bubbleVariants>["tone"]>;
|
|
21
|
+
type BubbleEmphasis = NonNullable<VariantProps<typeof bubbleVariants>["emphasis"]>;
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated The single `variant` list mixed colour with surface. Use
|
|
24
|
+
* `tone` + `emphasis`. Still accepted and renders unchanged: the five colour
|
|
25
|
+
* names map to filled tones; the old neutral `outline` and `ghost` map to
|
|
26
|
+
* tone "muted" at that emphasis.
|
|
27
|
+
*/
|
|
28
|
+
type BubbleLegacyVariant = "default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive";
|
|
29
|
+
declare function Bubble({ tone, emphasis, variant, align, className, ...props }: React.ComponentProps<"div"> & VariantProps<typeof bubbleVariants> & {
|
|
30
|
+
align?: "start" | "end";
|
|
31
|
+
/** @deprecated use `tone` + `emphasis` */
|
|
32
|
+
variant?: BubbleLegacyVariant;
|
|
33
|
+
}): React.JSX.Element;
|
|
34
|
+
declare function BubbleContent({ asChild, className, ...props }: React.ComponentProps<"div"> & {
|
|
35
|
+
asChild?: boolean;
|
|
36
|
+
}): React.JSX.Element;
|
|
37
|
+
declare function BubbleReactions({ side, align, className, ...props }: React.ComponentProps<"div"> & {
|
|
38
|
+
align?: "start" | "end";
|
|
39
|
+
side?: "top" | "bottom";
|
|
40
|
+
}): React.JSX.Element;
|
|
41
|
+
export { BubbleGroup, Bubble, BubbleContent, BubbleReactions };
|
|
42
|
+
export type { BubbleTone, BubbleEmphasis };
|
|
43
|
+
/**
|
|
44
|
+
* WHEN TO USE
|
|
45
|
+
* - Chat UIs where the bubble shape itself signals "this is a message"
|
|
46
|
+
* - Compact conversational surfaces that don't need avatar/author/timestamp chrome
|
|
47
|
+
*
|
|
48
|
+
* NOT WHEN
|
|
49
|
+
* - Messages need avatar, author, or timestamp structure — use Message
|
|
50
|
+
* - AI reasoning disclosure — that's a separate disclosure pattern, not a bubble
|
|
51
|
+
* - Transient system notifications — use Toaster
|
|
52
|
+
*
|
|
53
|
+
* KEY RULES
|
|
54
|
+
* - Two axes, not one list. tone: default | secondary | muted | tinted | destructive — the colour the
|
|
55
|
+
* bubble speaks in. emphasis: filled | outline | ghost — how much surface it gets. Every pairing is
|
|
56
|
+
* styled; omit both for a filled primary bubble. The old single `variant` prop still renders
|
|
57
|
+
* unchanged but is deprecated (its neutral `outline`/`ghost` were the muted tone at that emphasis).
|
|
58
|
+
* - align: "start" (incoming) or "end" (outgoing) — default "start"
|
|
59
|
+
* - align is positional only — it does NOT imply a color. In a two-party conversation,
|
|
60
|
+
* pair align="start" with a neutral tone (muted or secondary) and leave align="end"
|
|
61
|
+
* on the default primary fill, or every bubble renders the same color regardless of
|
|
62
|
+
* who sent it
|
|
63
|
+
* - emphasis="ghost" strips the bubble surface (transparent, no padding, max-w-full) — use for bare inline content that still needs align/grouping
|
|
64
|
+
* - BubbleReactions is absolutely positioned relative to Bubble — it must be a direct child
|
|
65
|
+
* - Bubbles never exceed 80% width except at emphasis="ghost"
|
|
66
|
+
* - A Bubble rendered inside a Message's MessageContent picks up Message's `data-align`
|
|
67
|
+
* automatically via a shared group selector — no need to also set Bubble's own `align`
|
|
68
|
+
*
|
|
69
|
+
* USAGE
|
|
70
|
+
* <BubbleGroup>
|
|
71
|
+
* <Bubble align="start" tone="muted"><BubbleContent>How do I override the primary color?</BubbleContent></Bubble>
|
|
72
|
+
* <Bubble align="end"><BubbleContent>Set `--primary` in an override layer.</BubbleContent></Bubble>
|
|
73
|
+
* </BubbleGroup>
|
|
74
|
+
*
|
|
75
|
+
* <Bubble align="start" tone="muted">
|
|
76
|
+
* <BubbleContent>Shipping the token layer today.</BubbleContent>
|
|
77
|
+
* <BubbleReactions>🎉 2</BubbleReactions>
|
|
78
|
+
* </Bubble>
|
|
79
|
+
*/
|
|
80
|
+
//# sourceMappingURL=bubble.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bubble.d.ts","sourceRoot":"","sources":["../../../src/components/ui/bubble.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAKjE,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQxE;AAKD,QAAA,MAAM,cAAc;;;8EA0GnB,CAAA;AAED,KAAK,UAAU,GAAG,WAAW,CAAC,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;AAC1E,KAAK,cAAc,GAAG,WAAW,CAC/B,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC,UAAU,CAAC,CAChD,CAAA;AACD;;;;;GAKG;AACH,KAAK,mBAAmB,GACpB,SAAS,GACT,WAAW,GACX,OAAO,GACP,QAAQ,GACR,SAAS,GACT,OAAO,GACP,aAAa,CAAA;AAejB,iBAAS,MAAM,CAAC,EACd,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,KAAe,EACf,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAC5B,YAAY,CAAC,OAAO,cAAc,CAAC,GAAG;IACpC,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAA;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,mBAAmB,CAAA;CAC9B,qBAiBF;AAED,iBAAS,aAAa,CAAC,EACrB,OAAe,EACf,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,qBAaA;AAsBD,iBAAS,eAAe,CAAC,EACvB,IAAe,EACf,KAAa,EACb,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG;IAC/B,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAA;IACvB,IAAI,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAA;CACxB,qBAUA;AAED,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,eAAe,EAAE,CAAA;AAC9D,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils.js";
|
|
2
|
+
import "react";
|
|
3
|
+
import { Slot } from "radix-ui";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { cva } from "class-variance-authority";
|
|
6
|
+
//#region src/components/ui/bubble.tsx
|
|
7
|
+
function BubbleGroup({ className, ...props }) {
|
|
8
|
+
return /* @__PURE__ */ jsx("div", {
|
|
9
|
+
"data-slot": "bubble-group",
|
|
10
|
+
className: cn("flex min-w-0 flex-col gap-2", className),
|
|
11
|
+
...props
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
var C = "*:data-[slot=bubble-content]:";
|
|
15
|
+
var H = "[&>[data-slot=bubble-content]:is(button,a):hover]:";
|
|
16
|
+
var bubbleVariants = cva("group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[emphasis=ghost]:max-w-full", {
|
|
17
|
+
variants: {
|
|
18
|
+
tone: {
|
|
19
|
+
default: "",
|
|
20
|
+
secondary: "",
|
|
21
|
+
muted: "",
|
|
22
|
+
tinted: "",
|
|
23
|
+
destructive: ""
|
|
24
|
+
},
|
|
25
|
+
emphasis: {
|
|
26
|
+
filled: "",
|
|
27
|
+
outline: `${C}bg-background`,
|
|
28
|
+
ghost: `border-none ${C}rounded-none ${C}bg-transparent ${C}p-0`
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
compoundVariants: [
|
|
32
|
+
{
|
|
33
|
+
tone: "default",
|
|
34
|
+
emphasis: "filled",
|
|
35
|
+
class: `${C}bg-primary ${C}text-primary-foreground ${H}bg-primary-hover`
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
tone: "secondary",
|
|
39
|
+
emphasis: "filled",
|
|
40
|
+
class: `${C}bg-secondary ${C}text-secondary-foreground ${H}bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]`
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
tone: "muted",
|
|
44
|
+
emphasis: "filled",
|
|
45
|
+
class: `${C}bg-muted ${H}bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]`
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
tone: "tinted",
|
|
49
|
+
emphasis: "filled",
|
|
50
|
+
class: `${C}bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] ${C}text-foreground dark:${C}bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] ${H}bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:${H}bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]`
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
tone: "destructive",
|
|
54
|
+
emphasis: "filled",
|
|
55
|
+
class: `${C}bg-destructive/10 ${C}text-destructive dark:${C}bg-destructive/20 ${H}bg-destructive/20 dark:${H}bg-destructive/30`
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
tone: "default",
|
|
59
|
+
emphasis: "outline",
|
|
60
|
+
class: `${C}border-primary/40 ${C}text-foreground ${H}bg-muted`
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
tone: "secondary",
|
|
64
|
+
emphasis: "outline",
|
|
65
|
+
class: `${C}border-border ${C}text-secondary-foreground ${H}bg-muted`
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
tone: "muted",
|
|
69
|
+
emphasis: "outline",
|
|
70
|
+
class: `${C}border-border ${H}bg-muted ${H}text-foreground dark:${H}bg-input/30`
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
tone: "tinted",
|
|
74
|
+
emphasis: "outline",
|
|
75
|
+
class: `${C}border-primary/30 ${C}text-foreground ${H}bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)]`
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
tone: "destructive",
|
|
79
|
+
emphasis: "outline",
|
|
80
|
+
class: `${C}border-destructive/40 ${C}text-destructive ${H}bg-destructive/10`
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
tone: "default",
|
|
84
|
+
emphasis: "ghost",
|
|
85
|
+
class: `${C}text-primary ${H}bg-muted dark:${H}bg-muted/50`
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
tone: "secondary",
|
|
89
|
+
emphasis: "ghost",
|
|
90
|
+
class: `${C}text-secondary-foreground ${H}bg-muted dark:${H}bg-muted/50`
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
tone: "muted",
|
|
94
|
+
emphasis: "ghost",
|
|
95
|
+
class: `${H}bg-muted ${H}text-foreground dark:${H}bg-muted/50`
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
tone: "tinted",
|
|
99
|
+
emphasis: "ghost",
|
|
100
|
+
class: `${C}text-foreground ${H}bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)]`
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
tone: "destructive",
|
|
104
|
+
emphasis: "ghost",
|
|
105
|
+
class: `${C}text-destructive ${H}bg-destructive/10 dark:${H}bg-destructive/20`
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
defaultVariants: {
|
|
109
|
+
tone: "default",
|
|
110
|
+
emphasis: "filled"
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
var LEGACY_VARIANT = {
|
|
114
|
+
default: {
|
|
115
|
+
tone: "default",
|
|
116
|
+
emphasis: "filled"
|
|
117
|
+
},
|
|
118
|
+
secondary: {
|
|
119
|
+
tone: "secondary",
|
|
120
|
+
emphasis: "filled"
|
|
121
|
+
},
|
|
122
|
+
muted: {
|
|
123
|
+
tone: "muted",
|
|
124
|
+
emphasis: "filled"
|
|
125
|
+
},
|
|
126
|
+
tinted: {
|
|
127
|
+
tone: "tinted",
|
|
128
|
+
emphasis: "filled"
|
|
129
|
+
},
|
|
130
|
+
destructive: {
|
|
131
|
+
tone: "destructive",
|
|
132
|
+
emphasis: "filled"
|
|
133
|
+
},
|
|
134
|
+
outline: {
|
|
135
|
+
tone: "muted",
|
|
136
|
+
emphasis: "outline"
|
|
137
|
+
},
|
|
138
|
+
ghost: {
|
|
139
|
+
tone: "muted",
|
|
140
|
+
emphasis: "ghost"
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
function Bubble({ tone, emphasis, variant, align = "start", className, ...props }) {
|
|
144
|
+
const legacy = variant ? LEGACY_VARIANT[variant] : void 0;
|
|
145
|
+
const resolvedTone = tone ?? legacy?.tone ?? "default";
|
|
146
|
+
const resolvedEmphasis = emphasis ?? legacy?.emphasis ?? "filled";
|
|
147
|
+
return /* @__PURE__ */ jsx("div", {
|
|
148
|
+
"data-slot": "bubble",
|
|
149
|
+
"data-tone": resolvedTone,
|
|
150
|
+
"data-emphasis": resolvedEmphasis,
|
|
151
|
+
"data-align": align,
|
|
152
|
+
className: cn(bubbleVariants({
|
|
153
|
+
tone: resolvedTone,
|
|
154
|
+
emphasis: resolvedEmphasis
|
|
155
|
+
}), className),
|
|
156
|
+
...props
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function BubbleContent({ asChild = false, className, ...props }) {
|
|
160
|
+
const Comp = asChild ? Slot.Root : "div";
|
|
161
|
+
return /* @__PURE__ */ jsx(Comp, {
|
|
162
|
+
"data-slot": "bubble-content",
|
|
163
|
+
className: cn("w-fit max-w-full min-w-0 overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50", className),
|
|
164
|
+
...props
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
var bubbleReactionsVariants = cva("absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0", {
|
|
168
|
+
variants: {
|
|
169
|
+
side: {
|
|
170
|
+
top: "top-0 -translate-y-3/4",
|
|
171
|
+
bottom: "bottom-0 translate-y-3/4"
|
|
172
|
+
},
|
|
173
|
+
align: {
|
|
174
|
+
start: "left-3",
|
|
175
|
+
end: "right-3"
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
defaultVariants: {
|
|
179
|
+
side: "bottom",
|
|
180
|
+
align: "end"
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
function BubbleReactions({ side = "bottom", align = "end", className, ...props }) {
|
|
184
|
+
return /* @__PURE__ */ jsx("div", {
|
|
185
|
+
"data-slot": "bubble-reactions",
|
|
186
|
+
"data-align": align,
|
|
187
|
+
"data-side": side,
|
|
188
|
+
className: cn(bubbleReactionsVariants({
|
|
189
|
+
side,
|
|
190
|
+
align
|
|
191
|
+
}), className),
|
|
192
|
+
...props
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* WHEN TO USE
|
|
197
|
+
* - Chat UIs where the bubble shape itself signals "this is a message"
|
|
198
|
+
* - Compact conversational surfaces that don't need avatar/author/timestamp chrome
|
|
199
|
+
*
|
|
200
|
+
* NOT WHEN
|
|
201
|
+
* - Messages need avatar, author, or timestamp structure — use Message
|
|
202
|
+
* - AI reasoning disclosure — that's a separate disclosure pattern, not a bubble
|
|
203
|
+
* - Transient system notifications — use Toaster
|
|
204
|
+
*
|
|
205
|
+
* KEY RULES
|
|
206
|
+
* - Two axes, not one list. tone: default | secondary | muted | tinted | destructive — the colour the
|
|
207
|
+
* bubble speaks in. emphasis: filled | outline | ghost — how much surface it gets. Every pairing is
|
|
208
|
+
* styled; omit both for a filled primary bubble. The old single `variant` prop still renders
|
|
209
|
+
* unchanged but is deprecated (its neutral `outline`/`ghost` were the muted tone at that emphasis).
|
|
210
|
+
* - align: "start" (incoming) or "end" (outgoing) — default "start"
|
|
211
|
+
* - align is positional only — it does NOT imply a color. In a two-party conversation,
|
|
212
|
+
* pair align="start" with a neutral tone (muted or secondary) and leave align="end"
|
|
213
|
+
* on the default primary fill, or every bubble renders the same color regardless of
|
|
214
|
+
* who sent it
|
|
215
|
+
* - emphasis="ghost" strips the bubble surface (transparent, no padding, max-w-full) — use for bare inline content that still needs align/grouping
|
|
216
|
+
* - BubbleReactions is absolutely positioned relative to Bubble — it must be a direct child
|
|
217
|
+
* - Bubbles never exceed 80% width except at emphasis="ghost"
|
|
218
|
+
* - A Bubble rendered inside a Message's MessageContent picks up Message's `data-align`
|
|
219
|
+
* automatically via a shared group selector — no need to also set Bubble's own `align`
|
|
220
|
+
*
|
|
221
|
+
* USAGE
|
|
222
|
+
* <BubbleGroup>
|
|
223
|
+
* <Bubble align="start" tone="muted"><BubbleContent>How do I override the primary color?</BubbleContent></Bubble>
|
|
224
|
+
* <Bubble align="end"><BubbleContent>Set `--primary` in an override layer.</BubbleContent></Bubble>
|
|
225
|
+
* </BubbleGroup>
|
|
226
|
+
*
|
|
227
|
+
* <Bubble align="start" tone="muted">
|
|
228
|
+
* <BubbleContent>Shipping the token layer today.</BubbleContent>
|
|
229
|
+
* <BubbleReactions>🎉 2</BubbleReactions>
|
|
230
|
+
* </Bubble>
|
|
231
|
+
*/
|
|
232
|
+
//#endregion
|
|
233
|
+
export { Bubble, BubbleContent, BubbleGroup, BubbleReactions };
|
|
234
|
+
|
|
235
|
+
//# sourceMappingURL=bubble.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bubble.js","names":[],"sources":["../../../src/components/ui/bubble.tsx"],"sourcesContent":["/**\n * Bubble — chat speech-bubble container.\n *\n * A colored, rounded container whose fill *is* the message surface — no\n * avatar or metadata slots. BubbleContent holds the text/children;\n * BubbleReactions overlays an emoji/count pill on a bubble. BubbleGroup\n * stacks a conversation with consistent spacing. Shares the `align` prop\n * convention with Message, and a Bubble dropped inside a Message's\n * MessageContent will self-align to match Message's `align` automatically\n * (they share a CSS group selector) — but most surfaces pick one shell,\n * either bare bubbles or the structured Message, rather than mixing both.\n */\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction BubbleGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"bubble-group\"\n className={cn(\"flex min-w-0 flex-col gap-2\", className)}\n {...props}\n />\n )\n}\n\nconst C = \"*:data-[slot=bubble-content]:\"\nconst H = \"[&>[data-slot=bubble-content]:is(button,a):hover]:\"\n\nconst bubbleVariants = cva(\n \"group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[emphasis=ghost]:max-w-full\",\n {\n // Two axes: `tone` is the colour the bubble speaks in (primary,\n // secondary, muted, tinted, destructive), `emphasis` is how much\n // surface it gets (a filled bubble, an outlined one, or bare text with\n // no bubble at all). Every pairing is defined below.\n variants: {\n tone: {\n default: \"\",\n secondary: \"\",\n muted: \"\",\n tinted: \"\",\n destructive: \"\",\n },\n emphasis: {\n filled: \"\",\n outline: `${C}bg-background`,\n ghost: `border-none ${C}rounded-none ${C}bg-transparent ${C}p-0`,\n },\n },\n compoundVariants: [\n // filled\n {\n tone: \"default\",\n emphasis: \"filled\",\n class: `${C}bg-primary ${C}text-primary-foreground ${H}bg-primary-hover`,\n },\n {\n tone: \"secondary\",\n emphasis: \"filled\",\n class: `${C}bg-secondary ${C}text-secondary-foreground ${H}bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]`,\n },\n {\n tone: \"muted\",\n emphasis: \"filled\",\n class: `${C}bg-muted ${H}bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]`,\n },\n {\n tone: \"tinted\",\n emphasis: \"filled\",\n class: `${C}bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] ${C}text-foreground dark:${C}bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] ${H}bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:${H}bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]`,\n },\n {\n tone: \"destructive\",\n emphasis: \"filled\",\n class: `${C}bg-destructive/10 ${C}text-destructive dark:${C}bg-destructive/20 ${H}bg-destructive/20 dark:${H}bg-destructive/30`,\n },\n // outline\n {\n tone: \"default\",\n emphasis: \"outline\",\n class: `${C}border-primary/40 ${C}text-foreground ${H}bg-muted`,\n },\n {\n tone: \"secondary\",\n emphasis: \"outline\",\n class: `${C}border-border ${C}text-secondary-foreground ${H}bg-muted`,\n },\n {\n tone: \"muted\",\n emphasis: \"outline\",\n class: `${C}border-border ${H}bg-muted ${H}text-foreground dark:${H}bg-input/30`,\n },\n {\n tone: \"tinted\",\n emphasis: \"outline\",\n class: `${C}border-primary/30 ${C}text-foreground ${H}bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)]`,\n },\n {\n tone: \"destructive\",\n emphasis: \"outline\",\n class: `${C}border-destructive/40 ${C}text-destructive ${H}bg-destructive/10`,\n },\n // ghost\n {\n tone: \"default\",\n emphasis: \"ghost\",\n class: `${C}text-primary ${H}bg-muted dark:${H}bg-muted/50`,\n },\n {\n tone: \"secondary\",\n emphasis: \"ghost\",\n class: `${C}text-secondary-foreground ${H}bg-muted dark:${H}bg-muted/50`,\n },\n {\n tone: \"muted\",\n emphasis: \"ghost\",\n class: `${H}bg-muted ${H}text-foreground dark:${H}bg-muted/50`,\n },\n {\n tone: \"tinted\",\n emphasis: \"ghost\",\n class: `${C}text-foreground ${H}bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)]`,\n },\n {\n tone: \"destructive\",\n emphasis: \"ghost\",\n class: `${C}text-destructive ${H}bg-destructive/10 dark:${H}bg-destructive/20`,\n },\n ],\n defaultVariants: {\n tone: \"default\",\n emphasis: \"filled\",\n },\n }\n)\n\ntype BubbleTone = NonNullable<VariantProps<typeof bubbleVariants>[\"tone\"]>\ntype BubbleEmphasis = NonNullable<\n VariantProps<typeof bubbleVariants>[\"emphasis\"]\n>\n/**\n * @deprecated The single `variant` list mixed colour with surface. Use\n * `tone` + `emphasis`. Still accepted and renders unchanged: the five colour\n * names map to filled tones; the old neutral `outline` and `ghost` map to\n * tone \"muted\" at that emphasis.\n */\ntype BubbleLegacyVariant =\n | \"default\"\n | \"secondary\"\n | \"muted\"\n | \"tinted\"\n | \"outline\"\n | \"ghost\"\n | \"destructive\"\n\nconst LEGACY_VARIANT: Record<\n BubbleLegacyVariant,\n { tone: BubbleTone; emphasis: BubbleEmphasis }\n> = {\n default: { tone: \"default\", emphasis: \"filled\" },\n secondary: { tone: \"secondary\", emphasis: \"filled\" },\n muted: { tone: \"muted\", emphasis: \"filled\" },\n tinted: { tone: \"tinted\", emphasis: \"filled\" },\n destructive: { tone: \"destructive\", emphasis: \"filled\" },\n outline: { tone: \"muted\", emphasis: \"outline\" },\n ghost: { tone: \"muted\", emphasis: \"ghost\" },\n}\n\nfunction Bubble({\n tone,\n emphasis,\n variant,\n align = \"start\",\n className,\n ...props\n}: React.ComponentProps<\"div\"> &\n VariantProps<typeof bubbleVariants> & {\n align?: \"start\" | \"end\"\n /** @deprecated use `tone` + `emphasis` */\n variant?: BubbleLegacyVariant\n }) {\n const legacy = variant ? LEGACY_VARIANT[variant] : undefined\n const resolvedTone = tone ?? legacy?.tone ?? \"default\"\n const resolvedEmphasis = emphasis ?? legacy?.emphasis ?? \"filled\"\n return (\n <div\n data-slot=\"bubble\"\n data-tone={resolvedTone}\n data-emphasis={resolvedEmphasis}\n data-align={align}\n className={cn(\n bubbleVariants({ tone: resolvedTone, emphasis: resolvedEmphasis }),\n className\n )}\n {...props}\n />\n )\n}\n\nfunction BubbleContent({\n asChild = false,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n asChild?: boolean\n}) {\n const Comp = asChild ? Slot.Root : \"div\"\n\n return (\n <Comp\n data-slot=\"bubble-content\"\n className={cn(\n \"w-fit max-w-full min-w-0 overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst bubbleReactionsVariants = cva(\n \"absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0\",\n {\n variants: {\n side: {\n top: \"top-0 -translate-y-3/4\",\n bottom: \"bottom-0 translate-y-3/4\",\n },\n align: {\n start: \"left-3\",\n end: \"right-3\",\n },\n },\n defaultVariants: {\n side: \"bottom\",\n align: \"end\",\n },\n }\n)\n\nfunction BubbleReactions({\n side = \"bottom\",\n align = \"end\",\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n align?: \"start\" | \"end\"\n side?: \"top\" | \"bottom\"\n}) {\n return (\n <div\n data-slot=\"bubble-reactions\"\n data-align={align}\n data-side={side}\n className={cn(bubbleReactionsVariants({ side, align }), className)}\n {...props}\n />\n )\n}\n\nexport { BubbleGroup, Bubble, BubbleContent, BubbleReactions }\nexport type { BubbleTone, BubbleEmphasis }\n\n/**\n * WHEN TO USE\n * - Chat UIs where the bubble shape itself signals \"this is a message\"\n * - Compact conversational surfaces that don't need avatar/author/timestamp chrome\n *\n * NOT WHEN\n * - Messages need avatar, author, or timestamp structure — use Message\n * - AI reasoning disclosure — that's a separate disclosure pattern, not a bubble\n * - Transient system notifications — use Toaster\n *\n * KEY RULES\n * - Two axes, not one list. tone: default | secondary | muted | tinted | destructive — the colour the\n * bubble speaks in. emphasis: filled | outline | ghost — how much surface it gets. Every pairing is\n * styled; omit both for a filled primary bubble. The old single `variant` prop still renders\n * unchanged but is deprecated (its neutral `outline`/`ghost` were the muted tone at that emphasis).\n * - align: \"start\" (incoming) or \"end\" (outgoing) — default \"start\"\n * - align is positional only — it does NOT imply a color. In a two-party conversation,\n * pair align=\"start\" with a neutral tone (muted or secondary) and leave align=\"end\"\n * on the default primary fill, or every bubble renders the same color regardless of\n * who sent it\n * - emphasis=\"ghost\" strips the bubble surface (transparent, no padding, max-w-full) — use for bare inline content that still needs align/grouping\n * - BubbleReactions is absolutely positioned relative to Bubble — it must be a direct child\n * - Bubbles never exceed 80% width except at emphasis=\"ghost\"\n * - A Bubble rendered inside a Message's MessageContent picks up Message's `data-align`\n * automatically via a shared group selector — no need to also set Bubble's own `align`\n *\n * USAGE\n * <BubbleGroup>\n * <Bubble align=\"start\" tone=\"muted\"><BubbleContent>How do I override the primary color?</BubbleContent></Bubble>\n * <Bubble align=\"end\"><BubbleContent>Set `--primary` in an override layer.</BubbleContent></Bubble>\n * </BubbleGroup>\n *\n * <Bubble align=\"start\" tone=\"muted\">\n * <BubbleContent>Shipping the token layer today.</BubbleContent>\n * <BubbleReactions>🎉 2</BubbleReactions>\n * </Bubble>\n */\n"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ButtonGroup — joins adjacent buttons into one segmented control.
|
|
3
|
+
*
|
|
4
|
+
* A plain flex container (`role="group"`) that strips inner borders/radii so
|
|
5
|
+
* a row (or column) of Buttons reads as a single control. ButtonGroupText
|
|
6
|
+
* and ButtonGroupSeparator are the non-interactive-affix and divider
|
|
7
|
+
* sub-parts.
|
|
8
|
+
*/
|
|
9
|
+
import { type VariantProps } from "class-variance-authority";
|
|
10
|
+
import { Separator } from "../../components/ui/separator";
|
|
11
|
+
declare const buttonGroupVariants: (props?: ({
|
|
12
|
+
orientation?: "horizontal" | "vertical" | null | undefined;
|
|
13
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
14
|
+
declare function ButtonGroup({ className, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>): import("react").JSX.Element;
|
|
15
|
+
declare function ButtonGroupText({ className, asChild, ...props }: React.ComponentProps<"div"> & {
|
|
16
|
+
asChild?: boolean;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
declare function ButtonGroupSeparator({ className, orientation, ...props }: React.ComponentProps<typeof Separator>): import("react").JSX.Element;
|
|
19
|
+
export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants, };
|
|
20
|
+
/**
|
|
21
|
+
* WHEN TO USE
|
|
22
|
+
* - Segmented view switchers (Day / Week / Month) rendered as adjacent Buttons
|
|
23
|
+
* - Split buttons: a primary action followed by a menu-trigger button, separated by ButtonGroupSeparator
|
|
24
|
+
* - An input with an attached prefix/suffix action (e.g. a URL scheme label + input + copy button)
|
|
25
|
+
*
|
|
26
|
+
* NOT WHEN
|
|
27
|
+
* - Actions that aren't visually or functionally related — use separate Buttons with normal spacing
|
|
28
|
+
* - A single action — use Button directly
|
|
29
|
+
* - Exclusive selection of one value from a set — use ToggleGroup type="single"
|
|
30
|
+
*
|
|
31
|
+
* KEY RULES
|
|
32
|
+
* - orientation: horizontal (default) | vertical — controls which edges collapse
|
|
33
|
+
* - Children must be direct DOM siblings (Button, Input, ButtonGroupText, ButtonGroupSeparator, a nested
|
|
34
|
+
* ButtonGroup, or a Select) — the border/radius-collapsing CSS only targets immediate children
|
|
35
|
+
* - ButtonGroupText renders a static, non-interactive affix — it isn't a button and takes no onClick
|
|
36
|
+
* - ButtonGroupSeparator defaults to a vertical divider — pass orientation="horizontal" inside a
|
|
37
|
+
* vertical ButtonGroup
|
|
38
|
+
* - role="group" is applied automatically; add aria-label when the group's purpose isn't clear from
|
|
39
|
+
* its visible content
|
|
40
|
+
*
|
|
41
|
+
* USAGE
|
|
42
|
+
* <ButtonGroup>
|
|
43
|
+
* <Button variant="outline">Day</Button>
|
|
44
|
+
* <Button variant="outline">Week</Button>
|
|
45
|
+
* <Button variant="outline">Month</Button>
|
|
46
|
+
* </ButtonGroup>
|
|
47
|
+
*
|
|
48
|
+
* <ButtonGroup>
|
|
49
|
+
* <Button><ShareIcon />Share</Button>
|
|
50
|
+
* <ButtonGroupSeparator />
|
|
51
|
+
* <Button iconOnly aria-label="More share options"><ChevronDownIcon /></Button>
|
|
52
|
+
* </ButtonGroup>
|
|
53
|
+
*
|
|
54
|
+
* <ButtonGroup>
|
|
55
|
+
* <ButtonGroupText>https://</ButtonGroupText>
|
|
56
|
+
* <Input placeholder="example.com" />
|
|
57
|
+
* <Button variant="outline" iconOnly aria-label="Copy URL"><CopyIcon /></Button>
|
|
58
|
+
* </ButtonGroup>
|
|
59
|
+
*/
|
|
60
|
+
//# sourceMappingURL=button-group.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button-group.d.ts","sourceRoot":"","sources":["../../../src/components/ui/button-group.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAErD,QAAA,MAAM,mBAAmB;;8EAexB,CAAA;AAED,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,WAAW,EACX,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,+BAUxE;AAED,iBAAS,eAAe,CAAC,EACvB,SAAS,EACT,OAAe,EACf,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,+BAYA;AAED,iBAAS,oBAAoB,CAAC,EAC5B,SAAS,EACT,WAAwB,EACxB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,SAAS,CAAC,+BAYxC;AAED,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,eAAe,EACf,mBAAmB,GACpB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG"}
|