@burtson-labs/ui 0.5.0 → 0.6.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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/components/alert-dialog.js +2 -2
  3. package/dist/components/alert-dialog.js.map +1 -1
  4. package/dist/components/composer.d.ts +30 -0
  5. package/dist/components/composer.js +95 -0
  6. package/dist/components/composer.js.map +1 -0
  7. package/dist/components/conversation.d.ts +14 -0
  8. package/dist/components/conversation.js +78 -0
  9. package/dist/components/conversation.js.map +1 -0
  10. package/dist/components/dialog.js +2 -2
  11. package/dist/components/dialog.js.map +1 -1
  12. package/dist/components/markdown.d.ts +33 -0
  13. package/dist/components/markdown.js +177 -0
  14. package/dist/components/markdown.js.map +1 -0
  15. package/dist/components/message.d.ts +21 -0
  16. package/dist/components/message.js +56 -0
  17. package/dist/components/message.js.map +1 -0
  18. package/dist/components/navigation-menu.js +2 -2
  19. package/dist/components/navigation-menu.js.map +1 -1
  20. package/dist/components/popover.d.ts +1 -1
  21. package/dist/components/popover.js +1 -1
  22. package/dist/components/popover.js.map +1 -1
  23. package/dist/components/reasoning.d.ts +14 -0
  24. package/dist/components/reasoning.js +61 -0
  25. package/dist/components/reasoning.js.map +1 -0
  26. package/dist/components/sheet.d.ts +4 -2
  27. package/dist/components/sheet.js +5 -3
  28. package/dist/components/sheet.js.map +1 -1
  29. package/dist/components/tabs.js +1 -1
  30. package/dist/components/tabs.js.map +1 -1
  31. package/dist/components/toast.js +1 -1
  32. package/dist/components/toast.js.map +1 -1
  33. package/dist/components/tool-call.d.ts +32 -0
  34. package/dist/components/tool-call.js +152 -0
  35. package/dist/components/tool-call.js.map +1 -0
  36. package/dist/components/tooltip.js +1 -1
  37. package/dist/components/tooltip.js.map +1 -1
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +8 -2
  40. package/dist/styles.css +1 -1
  41. package/dist/theme.css +22 -11
  42. package/dist/tokens.d.ts +167 -0
  43. package/dist/tokens.js +53 -1
  44. package/dist/tokens.js.map +1 -1
  45. package/package.json +1 -1
package/README.md CHANGED
@@ -58,7 +58,7 @@ npx shadcn@latest add https://ui.burtson.ai/r/button.json
58
58
 
59
59
  ## <picture><source media="(prefers-color-scheme: dark)" srcset="https://icons.burtson.ai/svg-white/panel-grid.svg"/><img src="https://icons.burtson.ai/svg-black/panel-grid.svg" align="center" alt=""/></picture> Components
60
60
 
61
- Accordion · Alert · Alert Dialog · App Shell · Avatar · Badge · Breadcrumb · Button · Card · Checkbox · Collapsible · Combobox · Command · Dialog · Dropdown Menu · Empty State · Field · Icon Button · Input · Kbd · Label · Navigation Menu · Page Header · Pagination · Popover · Progress · Radio Group · Scroll Area · Select · Separator · Sheet · Skeleton · Spinner · Stat Card · Status · Steps · Switch · Table · Tabs · Textarea · Toast · Toolbar · Tooltip
61
+ Accordion · Alert · Alert Dialog · App Shell · Avatar · Badge · Breadcrumb · Button · Card · Checkbox · Collapsible · Combobox · Command · Dialog · Dropdown Menu · Empty State · Field · Icon Button · Input · Kbd · Label · Navigation Menu · Page Header · Pagination · Popover · Progress · Radio Group · Reasoning · Scroll Area · Select · Separator · Sheet · Skeleton · Spinner · Stat Card · Status · Steps · Switch · Table · Tabs · Textarea · Toast · Toolbar · Tooltip
62
62
 
63
63
  Live previews and code for each one are at [ui.burtson.ai](https://ui.burtson.ai/docs/components/button).
64
64
 
@@ -24,10 +24,10 @@ function AlertDialogTrigger(props) {
24
24
  function AlertDialogContent({ className, ...props }) {
25
25
  return /* @__PURE__ */ jsxs(AlertDialog.Portal, { children: [/* @__PURE__ */ jsx(AlertDialog.Overlay, {
26
26
  "data-slot": "alert-dialog-overlay",
27
- className: "fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-out data-[state=open]:animate-in"
27
+ className: "fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in"
28
28
  }), /* @__PURE__ */ jsx(AlertDialog.Content, {
29
29
  "data-slot": "alert-dialog-content",
30
- className: cn("fixed top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-5 rounded-xl border border-border-strong bg-surface-raised p-5 text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-out data-[state=open]:animate-in sm:p-6", className),
30
+ className: cn("fixed top-1/2 left-1/2 z-50 grid [--bl-scale:0.95] [--bl-ty:8px] w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-5 rounded-xl border border-border-strong bg-surface-raised p-5 text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-out data-[state=open]:animate-in sm:p-6", className),
31
31
  ...props
32
32
  })] });
33
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"alert-dialog.js","names":["AlertDialog","AlertDialogPrimitive"],"sources":["../../src/components/alert-dialog.tsx"],"sourcesContent":["import { AlertDialog as AlertDialogPrimitive } from 'radix-ui';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { buttonVariants } from './button';\n\n/**\n * A confirmation that interrupts: \"Delete 3 keys?\", \"Revoke access?\". Unlike\n * Dialog it has no close button and ignores outside clicks, so the person\n * answers it; Escape still cancels. Focus starts on Cancel.\n */\nfunction AlertDialog(props: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {\n return <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" {...props} />;\n}\n\nfunction AlertDialogTrigger(props: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {\n return <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />;\n}\n\nfunction AlertDialogContent({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {\n return (\n <AlertDialogPrimitive.Portal>\n <AlertDialogPrimitive.Overlay\n data-slot=\"alert-dialog-overlay\"\n className=\"fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-out data-[state=open]:animate-in\"\n />\n <AlertDialogPrimitive.Content\n data-slot=\"alert-dialog-content\"\n className={cn(\n 'fixed top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-5 rounded-xl border border-border-strong bg-surface-raised p-5 text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-out data-[state=open]:animate-in sm:p-6',\n className,\n )}\n {...props}\n />\n </AlertDialogPrimitive.Portal>\n );\n}\n\nfunction AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div data-slot=\"alert-dialog-header\" className={cn('grid gap-1.5', className)} {...props} />\n );\n}\n\nfunction AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}\n {...props}\n />\n );\n}\n\nfunction AlertDialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {\n return (\n <AlertDialogPrimitive.Title\n data-slot=\"alert-dialog-title\"\n className={cn('text-base font-semibold tracking-[-0.02em]', className)}\n {...props}\n />\n );\n}\n\nfunction AlertDialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {\n return (\n <AlertDialogPrimitive.Description\n data-slot=\"alert-dialog-description\"\n className={cn('text-sm leading-5 text-muted-foreground', className)}\n {...props}\n />\n );\n}\n\n/** The confirming button. `destructive` paints it as a danger action. */\nfunction AlertDialogAction({\n className,\n destructive = false,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Action> & { destructive?: boolean }) {\n return (\n <AlertDialogPrimitive.Action\n data-slot=\"alert-dialog-action\"\n className={cn(\n buttonVariants({ variant: destructive ? 'destructive' : 'default' }),\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogCancel({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {\n return (\n <AlertDialogPrimitive.Cancel\n data-slot=\"alert-dialog-cancel\"\n className={cn(buttonVariants({ variant: 'outline' }), className)}\n {...props}\n />\n );\n}\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n};\n"],"mappings":";;;;;;;;;;;AAYA,SAASA,cAAY,OAA+D;CAClF,OAAO,oBAACC,YAAqB,MAAtB;EAA2B,aAAU;EAAe,GAAI;CAAQ,CAAA;AACzE;AAEA,SAAS,mBAAmB,OAAkE;CAC5F,OAAO,oBAACA,YAAqB,SAAtB;EAA8B,aAAU;EAAuB,GAAI;CAAQ,CAAA;AACpF;AAEA,SAAS,mBAAmB,EAC1B,WACA,GAAG,SACyD;CAC5D,OACE,qBAACA,YAAqB,QAAtB,EAAA,UAAA,CACE,oBAACA,YAAqB,SAAtB;EACE,aAAU;EACV,WAAU;CACX,CAAA,GACD,oBAACA,YAAqB,SAAtB;EACE,aAAU;EACV,WAAW,GACT,8SACA,SACF;EACA,GAAI;CACL,CAAA,CAC0B,EAAA,CAAA;AAEjC;AAEA,SAAS,kBAAkB,EAAE,WAAW,GAAG,SAAsC;CAC/E,OACE,oBAAC,OAAD;EAAK,aAAU;EAAsB,WAAW,GAAG,gBAAgB,SAAS;EAAG,GAAI;CAAQ,CAAA;AAE/F;AAEA,SAAS,kBAAkB,EAAE,WAAW,GAAG,SAAsC;CAC/E,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,0DAA0D,SAAS;EACjF,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,iBAAiB,EACxB,WACA,GAAG,SACuD;CAC1D,OACE,oBAACA,YAAqB,OAAtB;EACE,aAAU;EACV,WAAW,GAAG,8CAA8C,SAAS;EACrE,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,uBAAuB,EAC9B,WACA,GAAG,SAC6D;CAChE,OACE,oBAACA,YAAqB,aAAtB;EACE,aAAU;EACV,WAAW,GAAG,2CAA2C,SAAS;EAClE,GAAI;CACL,CAAA;AAEL;;AAGA,SAAS,kBAAkB,EACzB,WACA,cAAc,OACd,GAAG,SACoF;CACvF,OACE,oBAACA,YAAqB,QAAtB;EACE,aAAU;EACV,WAAW,GACT,eAAe,EAAE,SAAS,cAAc,gBAAgB,UAAU,CAAC,GACnE,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,kBAAkB,EACzB,WACA,GAAG,SACwD;CAC3D,OACE,oBAACA,YAAqB,QAAtB;EACE,aAAU;EACV,WAAW,GAAG,eAAe,EAAE,SAAS,UAAU,CAAC,GAAG,SAAS;EAC/D,GAAI;CACL,CAAA;AAEL"}
1
+ {"version":3,"file":"alert-dialog.js","names":["AlertDialog","AlertDialogPrimitive"],"sources":["../../src/components/alert-dialog.tsx"],"sourcesContent":["import { AlertDialog as AlertDialogPrimitive } from 'radix-ui';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { buttonVariants } from './button';\n\n/**\n * A confirmation that interrupts: \"Delete 3 keys?\", \"Revoke access?\". Unlike\n * Dialog it has no close button and ignores outside clicks, so the person\n * answers it; Escape still cancels. Focus starts on Cancel.\n */\nfunction AlertDialog(props: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {\n return <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" {...props} />;\n}\n\nfunction AlertDialogTrigger(props: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {\n return <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />;\n}\n\nfunction AlertDialogContent({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {\n return (\n <AlertDialogPrimitive.Portal>\n <AlertDialogPrimitive.Overlay\n data-slot=\"alert-dialog-overlay\"\n className=\"fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in\"\n />\n <AlertDialogPrimitive.Content\n data-slot=\"alert-dialog-content\"\n className={cn(\n 'fixed top-1/2 left-1/2 z-50 grid [--bl-scale:0.95] [--bl-ty:8px] w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-5 rounded-xl border border-border-strong bg-surface-raised p-5 text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-out data-[state=open]:animate-in sm:p-6',\n className,\n )}\n {...props}\n />\n </AlertDialogPrimitive.Portal>\n );\n}\n\nfunction AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div data-slot=\"alert-dialog-header\" className={cn('grid gap-1.5', className)} {...props} />\n );\n}\n\nfunction AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}\n {...props}\n />\n );\n}\n\nfunction AlertDialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {\n return (\n <AlertDialogPrimitive.Title\n data-slot=\"alert-dialog-title\"\n className={cn('text-base font-semibold tracking-[-0.02em]', className)}\n {...props}\n />\n );\n}\n\nfunction AlertDialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {\n return (\n <AlertDialogPrimitive.Description\n data-slot=\"alert-dialog-description\"\n className={cn('text-sm leading-5 text-muted-foreground', className)}\n {...props}\n />\n );\n}\n\n/** The confirming button. `destructive` paints it as a danger action. */\nfunction AlertDialogAction({\n className,\n destructive = false,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Action> & { destructive?: boolean }) {\n return (\n <AlertDialogPrimitive.Action\n data-slot=\"alert-dialog-action\"\n className={cn(\n buttonVariants({ variant: destructive ? 'destructive' : 'default' }),\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogCancel({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {\n return (\n <AlertDialogPrimitive.Cancel\n data-slot=\"alert-dialog-cancel\"\n className={cn(buttonVariants({ variant: 'outline' }), className)}\n {...props}\n />\n );\n}\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n};\n"],"mappings":";;;;;;;;;;;AAYA,SAASA,cAAY,OAA+D;CAClF,OAAO,oBAACC,YAAqB,MAAtB;EAA2B,aAAU;EAAe,GAAI;CAAQ,CAAA;AACzE;AAEA,SAAS,mBAAmB,OAAkE;CAC5F,OAAO,oBAACA,YAAqB,SAAtB;EAA8B,aAAU;EAAuB,GAAI;CAAQ,CAAA;AACpF;AAEA,SAAS,mBAAmB,EAC1B,WACA,GAAG,SACyD;CAC5D,OACE,qBAACA,YAAqB,QAAtB,EAAA,UAAA,CACE,oBAACA,YAAqB,SAAtB;EACE,aAAU;EACV,WAAU;CACX,CAAA,GACD,oBAACA,YAAqB,SAAtB;EACE,aAAU;EACV,WAAW,GACT,8UACA,SACF;EACA,GAAI;CACL,CAAA,CAC0B,EAAA,CAAA;AAEjC;AAEA,SAAS,kBAAkB,EAAE,WAAW,GAAG,SAAsC;CAC/E,OACE,oBAAC,OAAD;EAAK,aAAU;EAAsB,WAAW,GAAG,gBAAgB,SAAS;EAAG,GAAI;CAAQ,CAAA;AAE/F;AAEA,SAAS,kBAAkB,EAAE,WAAW,GAAG,SAAsC;CAC/E,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,0DAA0D,SAAS;EACjF,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,iBAAiB,EACxB,WACA,GAAG,SACuD;CAC1D,OACE,oBAACA,YAAqB,OAAtB;EACE,aAAU;EACV,WAAW,GAAG,8CAA8C,SAAS;EACrE,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,uBAAuB,EAC9B,WACA,GAAG,SAC6D;CAChE,OACE,oBAACA,YAAqB,aAAtB;EACE,aAAU;EACV,WAAW,GAAG,2CAA2C,SAAS;EAClE,GAAI;CACL,CAAA;AAEL;;AAGA,SAAS,kBAAkB,EACzB,WACA,cAAc,OACd,GAAG,SACoF;CACvF,OACE,oBAACA,YAAqB,QAAtB;EACE,aAAU;EACV,WAAW,GACT,eAAe,EAAE,SAAS,cAAc,gBAAgB,UAAU,CAAC,GACnE,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,kBAAkB,EACzB,WACA,GAAG,SACwD;CAC3D,OACE,oBAACA,YAAqB,QAAtB;EACE,aAAU;EACV,WAAW,GAAG,eAAe,EAAE,SAAS,UAAU,CAAC,GAAG,SAAS;EAC/D,GAAI;CACL,CAAA;AAEL"}
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+ export interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSubmit' | 'onChange'> {
3
+ /** Called with the trimmed text; the composer clears itself afterwards. */
4
+ onSubmit: (text: string) => void;
5
+ /** Controlled value; leave out to let the composer keep its own. */
6
+ value?: string;
7
+ onValueChange?: (value: string) => void;
8
+ /** While a reply streams, the send button becomes Stop. */
9
+ streaming?: boolean;
10
+ onStop?: () => void;
11
+ disabled?: boolean;
12
+ placeholder?: string;
13
+ /** Attachment chips or a file button, shown under the text. */
14
+ attachments?: React.ReactNode;
15
+ /** Extra controls left of the send button (model picker, tools). */
16
+ actions?: React.ReactNode;
17
+ label?: string;
18
+ }
19
+ /**
20
+ * The message box: grows with its text, Enter sends, Shift+Enter adds a
21
+ * line, and it never sends mid-composition (IME input).
22
+ */
23
+ declare function Composer({ onSubmit, value: controlled, onValueChange, streaming, onStop, disabled, placeholder, attachments, actions, label, className, ...props }: ComposerProps): React.JSX.Element;
24
+ export interface SuggestionsProps extends Omit<React.ComponentProps<'div'>, 'onSelect'> {
25
+ items: string[];
26
+ onSelect: (item: string) => void;
27
+ }
28
+ /** Starter prompts as chips. */
29
+ declare function Suggestions({ items, onSelect, className, ...props }: SuggestionsProps): React.JSX.Element;
30
+ export { Composer, Suggestions };
@@ -0,0 +1,95 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { Button } from "./button.js";
3
+ import * as React from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import ArrowUp from "@burtson-labs/icons/react/arrow-up";
6
+ import Square from "@burtson-labs/icons/react/square";
7
+ //#region src/components/composer.tsx
8
+ /**
9
+ * The message box: grows with its text, Enter sends, Shift+Enter adds a
10
+ * line, and it never sends mid-composition (IME input).
11
+ */
12
+ function Composer({ onSubmit, value: controlled, onValueChange, streaming = false, onStop, disabled = false, placeholder = "Ask anything…", attachments, actions, label = "Message", className, ...props }) {
13
+ const [own, setOwn] = React.useState("");
14
+ const value = controlled ?? own;
15
+ const setValue = (v) => {
16
+ if (controlled === void 0) setOwn(v);
17
+ onValueChange?.(v);
18
+ };
19
+ const canSend = !disabled && !streaming && value.trim().length > 0;
20
+ const send = () => {
21
+ if (!canSend) return;
22
+ onSubmit(value.trim());
23
+ setValue("");
24
+ };
25
+ return /* @__PURE__ */ jsxs("form", {
26
+ "data-slot": "composer",
27
+ onSubmit: (e) => {
28
+ e.preventDefault();
29
+ send();
30
+ },
31
+ className: cn("grid gap-2 rounded-lg border border-input bg-surface p-2 shadow-xs transition-[border-color,box-shadow] focus-within:border-brand focus-within:ring-[3px] focus-within:ring-ring/15 dark:bg-surface-raised", disabled && "opacity-60", className),
32
+ ...props,
33
+ children: [
34
+ /* @__PURE__ */ jsx("textarea", {
35
+ "aria-label": label,
36
+ rows: 1,
37
+ value,
38
+ disabled,
39
+ placeholder,
40
+ onChange: (e) => setValue(e.target.value),
41
+ onKeyDown: (e) => {
42
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
43
+ e.preventDefault();
44
+ send();
45
+ }
46
+ },
47
+ className: "field-sizing-content max-h-48 min-h-9 w-full resize-none bg-transparent px-2 py-1.5 text-sm leading-6 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
48
+ }),
49
+ attachments && /* @__PURE__ */ jsx("div", {
50
+ className: "flex flex-wrap gap-1.5 px-1",
51
+ children: attachments
52
+ }),
53
+ /* @__PURE__ */ jsxs("div", {
54
+ className: "flex items-center gap-1",
55
+ children: [actions, /* @__PURE__ */ jsx("div", {
56
+ className: "ml-auto",
57
+ children: streaming ? /* @__PURE__ */ jsx(Button, {
58
+ type: "button",
59
+ size: "icon-sm",
60
+ variant: "secondary",
61
+ "aria-label": "Stop",
62
+ onClick: onStop,
63
+ children: /* @__PURE__ */ jsx(Square, { className: "fill-current" })
64
+ }) : /* @__PURE__ */ jsx(Button, {
65
+ type: "submit",
66
+ size: "icon-sm",
67
+ "aria-label": "Send",
68
+ disabled: !canSend,
69
+ children: /* @__PURE__ */ jsx(ArrowUp, {})
70
+ })
71
+ })]
72
+ })
73
+ ]
74
+ });
75
+ }
76
+ /** Starter prompts as chips. */
77
+ function Suggestions({ items, onSelect, className, ...props }) {
78
+ return /* @__PURE__ */ jsx("div", {
79
+ "data-slot": "suggestions",
80
+ role: "group",
81
+ "aria-label": "Suggestions",
82
+ className: cn("flex flex-wrap gap-2", className),
83
+ ...props,
84
+ children: items.map((item) => /* @__PURE__ */ jsx("button", {
85
+ type: "button",
86
+ onClick: () => onSelect(item),
87
+ className: "animate-in rounded-full border border-border-strong bg-surface px-3 py-1.5 text-[13px] text-foreground outline-none transition-colors hover:border-brand/40 hover:bg-brand-soft hover:text-brand-soft-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20",
88
+ children: item
89
+ }, item))
90
+ });
91
+ }
92
+ //#endregion
93
+ export { Composer, Suggestions };
94
+
95
+ //# sourceMappingURL=composer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer.js","names":[],"sources":["../../src/components/composer.tsx"],"sourcesContent":["import ArrowUp from '@burtson-labs/icons/react/arrow-up';\nimport Square from '@burtson-labs/icons/react/square';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\n\nexport interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSubmit' | 'onChange'> {\n /** Called with the trimmed text; the composer clears itself afterwards. */\n onSubmit: (text: string) => void;\n /** Controlled value; leave out to let the composer keep its own. */\n value?: string;\n onValueChange?: (value: string) => void;\n /** While a reply streams, the send button becomes Stop. */\n streaming?: boolean;\n onStop?: () => void;\n disabled?: boolean;\n placeholder?: string;\n /** Attachment chips or a file button, shown under the text. */\n attachments?: React.ReactNode;\n /** Extra controls left of the send button (model picker, tools). */\n actions?: React.ReactNode;\n label?: string;\n}\n\n/**\n * The message box: grows with its text, Enter sends, Shift+Enter adds a\n * line, and it never sends mid-composition (IME input).\n */\nfunction Composer({\n onSubmit,\n value: controlled,\n onValueChange,\n streaming = false,\n onStop,\n disabled = false,\n placeholder = 'Ask anything…',\n attachments,\n actions,\n label = 'Message',\n className,\n ...props\n}: ComposerProps) {\n const [own, setOwn] = React.useState('');\n const value = controlled ?? own;\n const setValue = (v: string) => {\n if (controlled === undefined) setOwn(v);\n onValueChange?.(v);\n };\n const canSend = !disabled && !streaming && value.trim().length > 0;\n const send = () => {\n if (!canSend) return;\n onSubmit(value.trim());\n setValue('');\n };\n\n return (\n <form\n data-slot=\"composer\"\n onSubmit={(e) => {\n e.preventDefault();\n send();\n }}\n className={cn(\n 'grid gap-2 rounded-lg border border-input bg-surface p-2 shadow-xs transition-[border-color,box-shadow] focus-within:border-brand focus-within:ring-[3px] focus-within:ring-ring/15 dark:bg-surface-raised',\n disabled && 'opacity-60',\n className,\n )}\n {...props}\n >\n <textarea\n aria-label={label}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n send();\n }\n }}\n className=\"field-sizing-content max-h-48 min-h-9 w-full resize-none bg-transparent px-2 py-1.5 text-sm leading-6 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n {attachments && <div className=\"flex flex-wrap gap-1.5 px-1\">{attachments}</div>}\n <div className=\"flex items-center gap-1\">\n {actions}\n <div className=\"ml-auto\">\n {streaming ? (\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"secondary\"\n aria-label=\"Stop\"\n onClick={onStop}\n >\n <Square className=\"fill-current\" />\n </Button>\n ) : (\n <Button type=\"submit\" size=\"icon-sm\" aria-label=\"Send\" disabled={!canSend}>\n <ArrowUp />\n </Button>\n )}\n </div>\n </div>\n </form>\n );\n}\n\nexport interface SuggestionsProps extends Omit<React.ComponentProps<'div'>, 'onSelect'> {\n items: string[];\n onSelect: (item: string) => void;\n}\n\n/** Starter prompts as chips. */\nfunction Suggestions({ items, onSelect, className, ...props }: SuggestionsProps) {\n return (\n <div\n data-slot=\"suggestions\"\n role=\"group\"\n aria-label=\"Suggestions\"\n className={cn('flex flex-wrap gap-2', className)}\n {...props}\n >\n {items.map((item) => (\n <button\n key={item}\n type=\"button\"\n onClick={() => onSelect(item)}\n className=\"animate-in rounded-full border border-border-strong bg-surface px-3 py-1.5 text-[13px] text-foreground outline-none transition-colors hover:border-brand/40 hover:bg-brand-soft hover:text-brand-soft-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20\"\n >\n {item}\n </button>\n ))}\n </div>\n );\n}\n\nexport { Composer, Suggestions };\n"],"mappings":";;;;;;;;;;;AA8BA,SAAS,SAAS,EAChB,UACA,OAAO,YACP,eACA,YAAY,OACZ,QACA,WAAW,OACX,cAAc,iBACd,aACA,SACA,QAAQ,WACR,WACA,GAAG,SACa;CAChB,MAAM,CAAC,KAAK,UAAU,MAAM,SAAS,EAAE;CACvC,MAAM,QAAQ,cAAc;CAC5B,MAAM,YAAY,MAAc;EAC9B,IAAI,eAAe,KAAA,GAAW,OAAO,CAAC;EACtC,gBAAgB,CAAC;CACnB;CACA,MAAM,UAAU,CAAC,YAAY,CAAC,aAAa,MAAM,KAAK,CAAC,CAAC,SAAS;CACjE,MAAM,aAAa;EACjB,IAAI,CAAC,SAAS;EACd,SAAS,MAAM,KAAK,CAAC;EACrB,SAAS,EAAE;CACb;CAEA,OACE,qBAAC,QAAD;EACE,aAAU;EACV,WAAW,MAAM;GACf,EAAE,eAAe;GACjB,KAAK;EACP;EACA,WAAW,GACT,8MACA,YAAY,cACZ,SACF;EACA,GAAI;EAXN,UAAA;GAaE,oBAAC,YAAD;IACE,cAAY;IACZ,MAAM;IACC;IACG;IACG;IACb,WAAW,MAAM,SAAS,EAAE,OAAO,KAAK;IACxC,YAAY,MAAM;KAChB,IAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,YAAY,CAAC,EAAE,YAAY,aAAa;MAClE,EAAE,eAAe;MACjB,KAAK;KACP;IACF;IACA,WAAU;GACX,CAAA;GACA,eAAe,oBAAC,OAAD;IAAK,WAAU;IAA+B,UAAA;GAAiB,CAAA;GAC/E,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACG,SACD,oBAAC,OAAD;KAAK,WAAU;KACZ,UAAA,YACC,oBAAC,QAAD;MACE,MAAK;MACL,MAAK;MACL,SAAQ;MACR,cAAW;MACX,SAAS;MAET,UAAA,oBAAC,QAAD,EAAQ,WAAU,eAAgB,CAAA;KAC5B,CAAA,IAER,oBAAC,QAAD;MAAQ,MAAK;MAAS,MAAK;MAAU,cAAW;MAAO,UAAU,CAAC;MAChE,UAAA,oBAAC,SAAD,CAAU,CAAA;KACJ,CAAA;IAEP,CAAA,CACF;;EACD;;AAEV;;AAQA,SAAS,YAAY,EAAE,OAAO,UAAU,WAAW,GAAG,SAA2B;CAC/E,OACE,oBAAC,OAAD;EACE,aAAU;EACV,MAAK;EACL,cAAW;EACX,WAAW,GAAG,wBAAwB,SAAS;EAC/C,GAAI;EAEH,UAAA,MAAM,KAAK,SACV,oBAAC,UAAD;GAEE,MAAK;GACL,eAAe,SAAS,IAAI;GAC5B,WAAU;GAET,UAAA;EACK,GAND,IAMC,CACT;CACE,CAAA;AAET"}
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+ export interface ConversationProps extends React.ComponentProps<'div'> {
3
+ /** Shown instead of the messages when there are none. */
4
+ empty?: React.ReactNode;
5
+ /** Accessible name for the message log. */
6
+ label?: string;
7
+ }
8
+ /**
9
+ * A scrolling message log that stays pinned to the newest message while it
10
+ * streams, unless the reader has scrolled up; then a "Jump to latest" button
11
+ * appears instead of yanking them back down.
12
+ */
13
+ declare function Conversation({ children, empty, label, className, ...props }: ConversationProps): React.JSX.Element;
14
+ export { Conversation };
@@ -0,0 +1,78 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import * as React from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import ArrowDown from "@burtson-labs/icons/react/arrow-down";
5
+ //#region src/components/conversation.tsx
6
+ /**
7
+ * A scrolling message log that stays pinned to the newest message while it
8
+ * streams, unless the reader has scrolled up; then a "Jump to latest" button
9
+ * appears instead of yanking them back down.
10
+ */
11
+ function Conversation({ children, empty, label = "Conversation", className, ...props }) {
12
+ const scroller = React.useRef(null);
13
+ const content = React.useRef(null);
14
+ const pinned = React.useRef(true);
15
+ const [atBottom, setAtBottom] = React.useState(true);
16
+ const hasMessages = React.Children.toArray(children).length > 0;
17
+ const scrollToBottom = React.useCallback((behavior = "auto") => {
18
+ const el = scroller.current;
19
+ if (!el) return;
20
+ if (typeof el.scrollTo === "function") el.scrollTo({
21
+ top: el.scrollHeight,
22
+ behavior
23
+ });
24
+ else el.scrollTop = el.scrollHeight;
25
+ }, []);
26
+ React.useEffect(() => {
27
+ const el = content.current;
28
+ if (!el) return;
29
+ const ro = new ResizeObserver(() => {
30
+ if (pinned.current) scrollToBottom();
31
+ });
32
+ ro.observe(el);
33
+ return () => ro.disconnect();
34
+ }, [scrollToBottom]);
35
+ React.useEffect(() => {
36
+ if (pinned.current) scrollToBottom();
37
+ }, [children, scrollToBottom]);
38
+ const onScroll = () => {
39
+ const el = scroller.current;
40
+ if (!el) return;
41
+ const bottom = el.scrollHeight - el.scrollTop - el.clientHeight < 32;
42
+ pinned.current = bottom;
43
+ setAtBottom(bottom);
44
+ };
45
+ return /* @__PURE__ */ jsxs("div", {
46
+ "data-slot": "conversation",
47
+ className: cn("relative min-h-0", className),
48
+ ...props,
49
+ children: [/* @__PURE__ */ jsx("div", {
50
+ ref: scroller,
51
+ role: "log",
52
+ "aria-label": label,
53
+ "aria-live": "polite",
54
+ "aria-relevant": "additions text",
55
+ tabIndex: 0,
56
+ onScroll,
57
+ className: "size-full overflow-y-auto outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20",
58
+ children: /* @__PURE__ */ jsx("div", {
59
+ ref: content,
60
+ className: "grid gap-5 p-4",
61
+ children: hasMessages ? children : empty
62
+ })
63
+ }), !atBottom && /* @__PURE__ */ jsxs("button", {
64
+ type: "button",
65
+ onClick: () => {
66
+ pinned.current = true;
67
+ setAtBottom(true);
68
+ scrollToBottom("smooth");
69
+ },
70
+ className: "absolute bottom-3 left-1/2 inline-flex -translate-x-1/2 animate-in items-center gap-1.5 rounded-full border border-border-strong bg-surface-raised px-3 py-1.5 text-xs font-semibold shadow-md outline-none hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/20 [&_svg]:size-3.5",
71
+ children: [/* @__PURE__ */ jsx(ArrowDown, { "aria-hidden": true }), " Jump to latest"]
72
+ })]
73
+ });
74
+ }
75
+ //#endregion
76
+ export { Conversation };
77
+
78
+ //# sourceMappingURL=conversation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation.js","names":[],"sources":["../../src/components/conversation.tsx"],"sourcesContent":["import ArrowDown from '@burtson-labs/icons/react/arrow-down';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nexport interface ConversationProps extends React.ComponentProps<'div'> {\n /** Shown instead of the messages when there are none. */\n empty?: React.ReactNode;\n /** Accessible name for the message log. */\n label?: string;\n}\n\n/**\n * A scrolling message log that stays pinned to the newest message while it\n * streams, unless the reader has scrolled up; then a \"Jump to latest\" button\n * appears instead of yanking them back down.\n */\nfunction Conversation({\n children,\n empty,\n label = 'Conversation',\n className,\n ...props\n}: ConversationProps) {\n const scroller = React.useRef<HTMLDivElement>(null);\n const content = React.useRef<HTMLDivElement>(null);\n const pinned = React.useRef(true);\n const [atBottom, setAtBottom] = React.useState(true);\n const hasMessages = React.Children.toArray(children).length > 0;\n\n const scrollToBottom = React.useCallback((behavior: ScrollBehavior = 'auto') => {\n const el = scroller.current;\n if (!el) return;\n if (typeof el.scrollTo === 'function') el.scrollTo({ top: el.scrollHeight, behavior });\n else el.scrollTop = el.scrollHeight;\n }, []);\n\n React.useEffect(() => {\n const el = content.current;\n if (!el) return;\n // Content grows while a reply streams; follow it only if pinned.\n const ro = new ResizeObserver(() => {\n if (pinned.current) scrollToBottom();\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, [scrollToBottom]);\n\n React.useEffect(() => {\n if (pinned.current) scrollToBottom();\n }, [children, scrollToBottom]);\n\n const onScroll = () => {\n const el = scroller.current;\n if (!el) return;\n const bottom = el.scrollHeight - el.scrollTop - el.clientHeight < 32;\n pinned.current = bottom;\n setAtBottom(bottom);\n };\n\n return (\n <div data-slot=\"conversation\" className={cn('relative min-h-0', className)} {...props}>\n <div\n ref={scroller}\n role=\"log\"\n aria-label={label}\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n // A scrollable region must be reachable by keyboard (WCAG 2.1.1).\n // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n tabIndex={0}\n onScroll={onScroll}\n className=\"size-full overflow-y-auto outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20\"\n >\n <div ref={content} className=\"grid gap-5 p-4\">\n {hasMessages ? children : empty}\n </div>\n </div>\n {!atBottom && (\n <button\n type=\"button\"\n onClick={() => {\n pinned.current = true;\n setAtBottom(true);\n scrollToBottom('smooth');\n }}\n className=\"absolute bottom-3 left-1/2 inline-flex -translate-x-1/2 animate-in items-center gap-1.5 rounded-full border border-border-strong bg-surface-raised px-3 py-1.5 text-xs font-semibold shadow-md outline-none hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/20 [&_svg]:size-3.5\"\n >\n <ArrowDown aria-hidden /> Jump to latest\n </button>\n )}\n </div>\n );\n}\n\nexport { Conversation };\n"],"mappings":";;;;;;;;;;AAiBA,SAAS,aAAa,EACpB,UACA,OACA,QAAQ,gBACR,WACA,GAAG,SACiB;CACpB,MAAM,WAAW,MAAM,OAAuB,IAAI;CAClD,MAAM,UAAU,MAAM,OAAuB,IAAI;CACjD,MAAM,SAAS,MAAM,OAAO,IAAI;CAChC,MAAM,CAAC,UAAU,eAAe,MAAM,SAAS,IAAI;CACnD,MAAM,cAAc,MAAM,SAAS,QAAQ,QAAQ,CAAC,CAAC,SAAS;CAE9D,MAAM,iBAAiB,MAAM,aAAa,WAA2B,WAAW;EAC9E,MAAM,KAAK,SAAS;EACpB,IAAI,CAAC,IAAI;EACT,IAAI,OAAO,GAAG,aAAa,YAAY,GAAG,SAAS;GAAE,KAAK,GAAG;GAAc;EAAS,CAAC;OAChF,GAAG,YAAY,GAAG;CACzB,GAAG,CAAC,CAAC;CAEL,MAAM,gBAAgB;EACpB,MAAM,KAAK,QAAQ;EACnB,IAAI,CAAC,IAAI;EAET,MAAM,KAAK,IAAI,qBAAqB;GAClC,IAAI,OAAO,SAAS,eAAe;EACrC,CAAC;EACD,GAAG,QAAQ,EAAE;EACb,aAAa,GAAG,WAAW;CAC7B,GAAG,CAAC,cAAc,CAAC;CAEnB,MAAM,gBAAgB;EACpB,IAAI,OAAO,SAAS,eAAe;CACrC,GAAG,CAAC,UAAU,cAAc,CAAC;CAE7B,MAAM,iBAAiB;EACrB,MAAM,KAAK,SAAS;EACpB,IAAI,CAAC,IAAI;EACT,MAAM,SAAS,GAAG,eAAe,GAAG,YAAY,GAAG,eAAe;EAClE,OAAO,UAAU;EACjB,YAAY,MAAM;CACpB;CAEA,OACE,qBAAC,OAAD;EAAK,aAAU;EAAe,WAAW,GAAG,oBAAoB,SAAS;EAAG,GAAI;EAAhF,UAAA,CACE,oBAAC,OAAD;GACE,KAAK;GACL,MAAK;GACL,cAAY;GACZ,aAAU;GACV,iBAAc;GAGd,UAAU;GACA;GACV,WAAU;GAEV,UAAA,oBAAC,OAAD;IAAK,KAAK;IAAS,WAAU;IAC1B,UAAA,cAAc,WAAW;GACvB,CAAA;EACF,CAAA,GACJ,CAAC,YACA,qBAAC,UAAD;GACE,MAAK;GACL,eAAe;IACb,OAAO,UAAU;IACjB,YAAY,IAAI;IAChB,eAAe,QAAQ;GACzB;GACA,WAAU;GAPZ,UAAA,CASE,oBAAC,WAAD,EAAW,eAAA,KAAa,CAAA,GAAC,iBACnB;EAEP,CAAA,CAAA;;AAET"}
@@ -31,14 +31,14 @@ function DialogPortal(props) {
31
31
  function DialogOverlay({ className, ...props }) {
32
32
  return /* @__PURE__ */ jsx(Dialog.Overlay, {
33
33
  "data-slot": "dialog-overlay",
34
- className: cn("fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-out data-[state=open]:animate-in", className),
34
+ className: cn("fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in", className),
35
35
  ...props
36
36
  });
37
37
  }
38
38
  function DialogContent({ className, children, showCloseButton = true, ...props }) {
39
39
  return /* @__PURE__ */ jsxs(DialogPortal, { children: [/* @__PURE__ */ jsx(DialogOverlay, {}), /* @__PURE__ */ jsxs(Dialog.Content, {
40
40
  "data-slot": "dialog-content",
41
- className: cn("fixed top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-5 rounded-xl border border-border-strong bg-surface-raised p-5 text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-out data-[state=open]:animate-in sm:p-6", className),
41
+ className: cn("fixed top-1/2 left-1/2 z-50 grid [--bl-scale:0.95] [--bl-ty:8px] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-5 rounded-xl border border-border-strong bg-surface-raised p-5 text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-out data-[state=open]:animate-in sm:p-6", className),
42
42
  ...props,
43
43
  children: [children, showCloseButton ? /* @__PURE__ */ jsxs(Dialog.Close, {
44
44
  "data-slot": "dialog-close",
@@ -1 +1 @@
1
- {"version":3,"file":"dialog.js","names":["Dialog","DialogPrimitive"],"sources":["../../src/components/dialog.tsx"],"sourcesContent":["import X from '@burtson-labs/icons/react/x';\nimport { Dialog as DialogPrimitive } from 'radix-ui';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nfunction Dialog(props: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger(props: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogClose(props: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n 'fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-out data-[state=open]:animate-in',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) {\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n 'fixed top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-5 rounded-xl border border-border-strong bg-surface-raised p-5 text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-out data-[state=open]:animate-in sm:p-6',\n className,\n )}\n {...props}\n >\n {children}\n {showCloseButton ? (\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n className=\"absolute top-3.5 right-3.5 grid size-8 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20 [&_svg]:size-4\"\n >\n <X aria-hidden />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n ) : null}\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div data-slot=\"dialog-header\" className={cn('grid gap-1.5 pr-7', className)} {...props} />\n );\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\n 'flex flex-col-reverse gap-2 border-t border-border pt-4 sm:flex-row sm:justify-end',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn('text-base font-semibold tracking-[-0.02em]', className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn('text-sm leading-5 text-muted-foreground', className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n"],"mappings":";;;;;;AAMA,SAASA,SAAO,OAA0D;CACxE,OAAO,oBAACC,OAAgB,MAAjB;EAAsB,aAAU;EAAS,GAAI;CAAQ,CAAA;AAC9D;AAEA,SAAS,cAAc,OAA6D;CAClF,OAAO,oBAACA,OAAgB,SAAjB;EAAyB,aAAU;EAAiB,GAAI;CAAQ,CAAA;AACzE;AAEA,SAAS,YAAY,OAA2D;CAC9E,OAAO,oBAACA,OAAgB,OAAjB;EAAuB,aAAU;EAAe,GAAI;CAAQ,CAAA;AACrE;AAEA,SAAS,aAAa,OAA4D;CAChF,OAAO,oBAACA,OAAgB,QAAjB;EAAwB,aAAU;EAAgB,GAAI;CAAQ,CAAA;AACvE;AAEA,SAAS,cAAc,EACrB,WACA,GAAG,SACoD;CACvD,OACE,oBAACA,OAAgB,SAAjB;EACE,aAAU;EACV,WAAW,GACT,mHACA,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,cAAc,EACrB,WACA,UACA,kBAAkB,MAClB,GAAG,SACoF;CACvF,OACE,qBAAC,cAAD,EAAA,UAAA,CACE,oBAAC,eAAD,CAAgB,CAAA,GAChB,qBAACA,OAAgB,SAAjB;EACE,aAAU;EACV,WAAW,GACT,8SACA,SACF;EACA,GAAI;EANN,UAAA,CAQG,UACA,kBACC,qBAACA,OAAgB,OAAjB;GACE,aAAU;GACV,WAAU;GAFZ,UAAA,CAIE,oBAAC,GAAD,EAAG,eAAA,KAAa,CAAA,GAChB,oBAAC,QAAD;IAAM,WAAU;IAAU,UAAA;GAAW,CAAA,CAChB;EACrB,CAAA,IAAA,IACmB;CACb,CAAA,CAAA,EAAA,CAAA;AAElB;AAEA,SAAS,aAAa,EAAE,WAAW,GAAG,SAAsC;CAC1E,OACE,oBAAC,OAAD;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,SAAS;EAAG,GAAI;CAAQ,CAAA;AAE9F;AAEA,SAAS,aAAa,EAAE,WAAW,GAAG,SAAsC;CAC1E,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GACT,sFACA,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,SAA6D;CAChG,OACE,oBAACA,OAAgB,OAAjB;EACE,aAAU;EACV,WAAW,GAAG,8CAA8C,SAAS;EACrE,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,kBAAkB,EACzB,WACA,GAAG,SACwD;CAC3D,OACE,oBAACA,OAAgB,aAAjB;EACE,aAAU;EACV,WAAW,GAAG,2CAA2C,SAAS;EAClE,GAAI;CACL,CAAA;AAEL"}
1
+ {"version":3,"file":"dialog.js","names":["Dialog","DialogPrimitive"],"sources":["../../src/components/dialog.tsx"],"sourcesContent":["import X from '@burtson-labs/icons/react/x';\nimport { Dialog as DialogPrimitive } from 'radix-ui';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nfunction Dialog(props: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger(props: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogClose(props: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n 'fixed inset-0 z-50 bg-black/55 backdrop-blur-[3px] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) {\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n 'fixed top-1/2 left-1/2 z-50 grid [--bl-scale:0.95] [--bl-ty:8px] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-5 rounded-xl border border-border-strong bg-surface-raised p-5 text-foreground shadow-[0_24px_80px_rgb(0_0_0_/_0.28)] outline-none data-[state=closed]:animate-out data-[state=open]:animate-in sm:p-6',\n className,\n )}\n {...props}\n >\n {children}\n {showCloseButton ? (\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n className=\"absolute top-3.5 right-3.5 grid size-8 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20 [&_svg]:size-4\"\n >\n <X aria-hidden />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n ) : null}\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div data-slot=\"dialog-header\" className={cn('grid gap-1.5 pr-7', className)} {...props} />\n );\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\n 'flex flex-col-reverse gap-2 border-t border-border pt-4 sm:flex-row sm:justify-end',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn('text-base font-semibold tracking-[-0.02em]', className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn('text-sm leading-5 text-muted-foreground', className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n"],"mappings":";;;;;;AAMA,SAASA,SAAO,OAA0D;CACxE,OAAO,oBAACC,OAAgB,MAAjB;EAAsB,aAAU;EAAS,GAAI;CAAQ,CAAA;AAC9D;AAEA,SAAS,cAAc,OAA6D;CAClF,OAAO,oBAACA,OAAgB,SAAjB;EAAyB,aAAU;EAAiB,GAAI;CAAQ,CAAA;AACzE;AAEA,SAAS,YAAY,OAA2D;CAC9E,OAAO,oBAACA,OAAgB,OAAjB;EAAuB,aAAU;EAAe,GAAI;CAAQ,CAAA;AACrE;AAEA,SAAS,aAAa,OAA4D;CAChF,OAAO,oBAACA,OAAgB,QAAjB;EAAwB,aAAU;EAAgB,GAAI;CAAQ,CAAA;AACvE;AAEA,SAAS,cAAc,EACrB,WACA,GAAG,SACoD;CACvD,OACE,oBAACA,OAAgB,SAAjB;EACE,aAAU;EACV,WAAW,GACT,6HACA,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,cAAc,EACrB,WACA,UACA,kBAAkB,MAClB,GAAG,SACoF;CACvF,OACE,qBAAC,cAAD,EAAA,UAAA,CACE,oBAAC,eAAD,CAAgB,CAAA,GAChB,qBAACA,OAAgB,SAAjB;EACE,aAAU;EACV,WAAW,GACT,8UACA,SACF;EACA,GAAI;EANN,UAAA,CAQG,UACA,kBACC,qBAACA,OAAgB,OAAjB;GACE,aAAU;GACV,WAAU;GAFZ,UAAA,CAIE,oBAAC,GAAD,EAAG,eAAA,KAAa,CAAA,GAChB,oBAAC,QAAD;IAAM,WAAU;IAAU,UAAA;GAAW,CAAA,CAChB;EACrB,CAAA,IAAA,IACmB;CACb,CAAA,CAAA,EAAA,CAAA;AAElB;AAEA,SAAS,aAAa,EAAE,WAAW,GAAG,SAAsC;CAC1E,OACE,oBAAC,OAAD;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,SAAS;EAAG,GAAI;CAAQ,CAAA;AAE9F;AAEA,SAAS,aAAa,EAAE,WAAW,GAAG,SAAsC;CAC1E,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GACT,sFACA,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,SAA6D;CAChG,OACE,oBAACA,OAAgB,OAAjB;EACE,aAAU;EACV,WAAW,GAAG,8CAA8C,SAAS;EACrE,GAAI;CACL,CAAA;AAEL;AAEA,SAAS,kBAAkB,EACzB,WACA,GAAG,SACwD;CAC3D,OACE,oBAACA,OAAgB,aAAjB;EACE,aAAU;EACV,WAAW,GAAG,2CAA2C,SAAS;EAClE,GAAI;CACL,CAAA;AAEL"}
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ /** A fenced code block with a language label and a copy button. */
3
+ declare function CodeBlock({ code, lang, className }: {
4
+ code: string;
5
+ lang?: string;
6
+ className?: string;
7
+ }): React.JSX.Element;
8
+ type Block = {
9
+ type: 'p';
10
+ text: string;
11
+ } | {
12
+ type: 'h';
13
+ level: 1 | 2 | 3;
14
+ text: string;
15
+ } | {
16
+ type: 'ul' | 'ol';
17
+ items: string[];
18
+ } | {
19
+ type: 'quote';
20
+ text: string;
21
+ } | {
22
+ type: 'code';
23
+ lang: string;
24
+ code: string;
25
+ };
26
+ /** Split Markdown into blocks. Exported for tests. */
27
+ export declare function parseMarkdown(source: string): Block[];
28
+ export interface MarkdownProps extends Omit<React.ComponentProps<'div'>, 'children'> {
29
+ children: string;
30
+ }
31
+ /** Renders a Markdown string safely, for assistant messages. */
32
+ declare function Markdown({ children, className, ...props }: MarkdownProps): React.JSX.Element;
33
+ export { CodeBlock, Markdown };
@@ -0,0 +1,177 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import * as React from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import Check from "@burtson-labs/icons/react/check";
5
+ import Copy from "@burtson-labs/icons/react/copy";
6
+ //#region src/components/markdown.tsx
7
+ var SAFE_HREF = /^(https?:\/\/|mailto:)/i;
8
+ function inline(text, key) {
9
+ const out = [];
10
+ const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*|__[^_]+__)|(\*[^*\s][^*]*\*|_[^_\s][^_]*_)|(\[[^\]]+\]\([^)\s]+\))/g;
11
+ let last = 0;
12
+ let n = 0;
13
+ for (const m of text.matchAll(pattern)) {
14
+ const i = m.index;
15
+ if (i > last) out.push(text.slice(last, i));
16
+ const token = m[0];
17
+ const k = `${key}-${n++}`;
18
+ if (m[1]) out.push(/* @__PURE__ */ jsx("code", {
19
+ className: "rounded-xs bg-muted px-1 py-0.5 font-mono text-[0.85em]",
20
+ children: token.slice(1, -1)
21
+ }, k));
22
+ else if (m[2]) out.push(/* @__PURE__ */ jsx("strong", { children: inline(token.slice(2, -2), k) }, k));
23
+ else if (m[3]) out.push(/* @__PURE__ */ jsx("em", { children: inline(token.slice(1, -1), k) }, k));
24
+ else {
25
+ const [, label = "", href = ""] = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(token) ?? [];
26
+ out.push(SAFE_HREF.test(href) ? /* @__PURE__ */ jsx("a", {
27
+ href,
28
+ target: "_blank",
29
+ rel: "noreferrer noopener",
30
+ className: "text-brand underline underline-offset-2 hover:text-brand-hover",
31
+ children: label
32
+ }, k) : label);
33
+ }
34
+ last = i + token.length;
35
+ }
36
+ if (last < text.length) out.push(text.slice(last));
37
+ return out;
38
+ }
39
+ /** A fenced code block with a language label and a copy button. */
40
+ function CodeBlock({ code, lang, className }) {
41
+ const [copied, setCopied] = React.useState(false);
42
+ return /* @__PURE__ */ jsxs("div", {
43
+ "data-slot": "code-block",
44
+ className: cn("overflow-hidden rounded-md border bg-code text-code-foreground", className),
45
+ children: [/* @__PURE__ */ jsxs("div", {
46
+ className: "flex h-8 items-center justify-between border-b border-white/10 px-3 text-[11px] text-code-foreground/60",
47
+ children: [/* @__PURE__ */ jsx("span", {
48
+ className: "font-mono",
49
+ children: lang || "text"
50
+ }), /* @__PURE__ */ jsx("button", {
51
+ type: "button",
52
+ "aria-label": copied ? "Copied" : "Copy code",
53
+ className: "grid size-6 place-items-center rounded-xs outline-none hover:bg-white/10 focus-visible:ring-[3px] focus-visible:ring-ring/30 [&_svg]:size-3.5",
54
+ onClick: () => {
55
+ navigator.clipboard?.writeText(code).then(() => {
56
+ setCopied(true);
57
+ setTimeout(() => setCopied(false), 1400);
58
+ });
59
+ },
60
+ children: copied ? /* @__PURE__ */ jsx(Check, { "aria-hidden": true }) : /* @__PURE__ */ jsx(Copy, { "aria-hidden": true })
61
+ })]
62
+ }), /* @__PURE__ */ jsx("pre", {
63
+ className: "overflow-x-auto p-3 font-mono text-[12.5px] leading-5",
64
+ children: /* @__PURE__ */ jsx("code", { children: code })
65
+ })]
66
+ });
67
+ }
68
+ /** Split Markdown into blocks. Exported for tests. */
69
+ function parseMarkdown(source) {
70
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
71
+ const blocks = [];
72
+ let para = [];
73
+ const flush = () => {
74
+ if (para.length) blocks.push({
75
+ type: "p",
76
+ text: para.join(" ")
77
+ });
78
+ para = [];
79
+ };
80
+ for (let i = 0; i < lines.length; i++) {
81
+ const line = lines[i] ?? "";
82
+ const fence = /^```\s*([\w+-]*)\s*$/.exec(line);
83
+ if (fence) {
84
+ flush();
85
+ const code = [];
86
+ i++;
87
+ while (i < lines.length && !/^```\s*$/.test(lines[i] ?? "")) code.push(lines[i++] ?? "");
88
+ blocks.push({
89
+ type: "code",
90
+ lang: fence[1] ?? "",
91
+ code: code.join("\n")
92
+ });
93
+ continue;
94
+ }
95
+ const heading = /^(#{1,3})\s+(.*)$/.exec(line);
96
+ if (heading) {
97
+ flush();
98
+ blocks.push({
99
+ type: "h",
100
+ level: heading[1]?.length ?? 1,
101
+ text: heading[2] ?? ""
102
+ });
103
+ continue;
104
+ }
105
+ const list = /^\s*([-*]|\d+[.)])\s+(.*)$/.exec(line);
106
+ if (list) {
107
+ flush();
108
+ const type = /\d/.test(list[1] ?? "") ? "ol" : "ul";
109
+ const prev = blocks.at(-1);
110
+ if (prev && prev.type === type) prev.items.push(list[2] ?? "");
111
+ else blocks.push({
112
+ type,
113
+ items: [list[2] ?? ""]
114
+ });
115
+ continue;
116
+ }
117
+ const quote = /^>\s?(.*)$/.exec(line);
118
+ if (quote) {
119
+ flush();
120
+ blocks.push({
121
+ type: "quote",
122
+ text: quote[1] ?? ""
123
+ });
124
+ continue;
125
+ }
126
+ if (!line.trim()) flush();
127
+ else para.push(line.trim());
128
+ }
129
+ flush();
130
+ return blocks;
131
+ }
132
+ /** Renders a Markdown string safely, for assistant messages. */
133
+ function Markdown({ children, className, ...props }) {
134
+ const blocks = React.useMemo(() => parseMarkdown(children), [children]);
135
+ return /* @__PURE__ */ jsx("div", {
136
+ "data-slot": "markdown",
137
+ className: cn("grid gap-3 text-sm leading-6 [&_li]:leading-6", className),
138
+ ...props,
139
+ children: blocks.map((b, i) => {
140
+ const k = `b${i}`;
141
+ switch (b.type) {
142
+ case "h": {
143
+ const Tag = [
144
+ "h3",
145
+ "h4",
146
+ "h5"
147
+ ][b.level - 1] ?? "h5";
148
+ return /* @__PURE__ */ jsx(Tag, {
149
+ className: "font-semibold tracking-[-0.01em]",
150
+ children: inline(b.text, k)
151
+ }, k);
152
+ }
153
+ case "ul":
154
+ case "ol": {
155
+ const Tag = b.type;
156
+ return /* @__PURE__ */ jsx(Tag, {
157
+ className: cn("grid gap-1 pl-5", b.type === "ul" ? "list-disc" : "list-decimal"),
158
+ children: b.items.map((item, j) => /* @__PURE__ */ jsx("li", { children: inline(item, `${k}-${j}`) }, `${k}-${j}`))
159
+ }, k);
160
+ }
161
+ case "quote": return /* @__PURE__ */ jsx("blockquote", {
162
+ className: "border-l-2 border-border-strong pl-3 text-muted-foreground",
163
+ children: inline(b.text, k)
164
+ }, k);
165
+ case "code": return /* @__PURE__ */ jsx(CodeBlock, {
166
+ code: b.code,
167
+ lang: b.lang
168
+ }, k);
169
+ default: return /* @__PURE__ */ jsx("p", { children: inline(b.text, k) }, k);
170
+ }
171
+ })
172
+ });
173
+ }
174
+ //#endregion
175
+ export { CodeBlock, Markdown, parseMarkdown };
176
+
177
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","names":[],"sources":["../../src/components/markdown.tsx"],"sourcesContent":["import Check from '@burtson-labs/icons/react/check';\nimport Copy from '@burtson-labs/icons/react/copy';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\n/*\n * A small, safe Markdown renderer for chat: paragraphs, headings, bullet and\n * numbered lists, block quotes, fenced code, inline code, bold, italic and\n * links. Everything becomes React elements (never HTML strings), and links\n * only keep http(s) and mailto targets, so model output cannot inject markup\n * or script.\n */\n\nconst SAFE_HREF = /^(https?:\\/\\/|mailto:)/i;\n\nfunction inline(text: string, key: string): React.ReactNode[] {\n const out: React.ReactNode[] = [];\n const pattern =\n /(`[^`]+`)|(\\*\\*[^*]+\\*\\*|__[^_]+__)|(\\*[^*\\s][^*]*\\*|_[^_\\s][^_]*_)|(\\[[^\\]]+\\]\\([^)\\s]+\\))/g;\n let last = 0;\n let n = 0;\n for (const m of text.matchAll(pattern)) {\n const i = m.index;\n if (i > last) out.push(text.slice(last, i));\n const token = m[0];\n const k = `${key}-${n++}`;\n if (m[1]) {\n out.push(\n <code key={k} className=\"rounded-xs bg-muted px-1 py-0.5 font-mono text-[0.85em]\">\n {token.slice(1, -1)}\n </code>,\n );\n } else if (m[2]) {\n out.push(<strong key={k}>{inline(token.slice(2, -2), k)}</strong>);\n } else if (m[3]) {\n out.push(<em key={k}>{inline(token.slice(1, -1), k)}</em>);\n } else {\n const [, label = '', href = ''] = /^\\[([^\\]]+)\\]\\(([^)\\s]+)\\)$/.exec(token) ?? [];\n out.push(\n SAFE_HREF.test(href) ? (\n <a\n key={k}\n href={href}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"text-brand underline underline-offset-2 hover:text-brand-hover\"\n >\n {label}\n </a>\n ) : (\n label\n ),\n );\n }\n last = i + token.length;\n }\n if (last < text.length) out.push(text.slice(last));\n return out;\n}\n\n/** A fenced code block with a language label and a copy button. */\nfunction CodeBlock({ code, lang, className }: { code: string; lang?: string; className?: string }) {\n const [copied, setCopied] = React.useState(false);\n return (\n <div\n data-slot=\"code-block\"\n className={cn('overflow-hidden rounded-md border bg-code text-code-foreground', className)}\n >\n <div className=\"flex h-8 items-center justify-between border-b border-white/10 px-3 text-[11px] text-code-foreground/60\">\n <span className=\"font-mono\">{lang || 'text'}</span>\n <button\n type=\"button\"\n aria-label={copied ? 'Copied' : 'Copy code'}\n className=\"grid size-6 place-items-center rounded-xs outline-none hover:bg-white/10 focus-visible:ring-[3px] focus-visible:ring-ring/30 [&_svg]:size-3.5\"\n onClick={() => {\n void navigator.clipboard?.writeText(code).then(() => {\n setCopied(true);\n setTimeout(() => setCopied(false), 1400);\n });\n }}\n >\n {copied ? <Check aria-hidden /> : <Copy aria-hidden />}\n </button>\n </div>\n <pre className=\"overflow-x-auto p-3 font-mono text-[12.5px] leading-5\">\n <code>{code}</code>\n </pre>\n </div>\n );\n}\n\ntype Block =\n | { type: 'p'; text: string }\n | { type: 'h'; level: 1 | 2 | 3; text: string }\n | { type: 'ul' | 'ol'; items: string[] }\n | { type: 'quote'; text: string }\n | { type: 'code'; lang: string; code: string };\n\n/** Split Markdown into blocks. Exported for tests. */\nexport function parseMarkdown(source: string): Block[] {\n const lines = source.replace(/\\r\\n?/g, '\\n').split('\\n');\n const blocks: Block[] = [];\n let para: string[] = [];\n const flush = () => {\n if (para.length) blocks.push({ type: 'p', text: para.join(' ') });\n para = [];\n };\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i] ?? '';\n const fence = /^```\\s*([\\w+-]*)\\s*$/.exec(line);\n if (fence) {\n flush();\n const code: string[] = [];\n i++;\n while (i < lines.length && !/^```\\s*$/.test(lines[i] ?? '')) code.push(lines[i++] ?? '');\n blocks.push({ type: 'code', lang: fence[1] ?? '', code: code.join('\\n') });\n continue;\n }\n const heading = /^(#{1,3})\\s+(.*)$/.exec(line);\n if (heading) {\n flush();\n blocks.push({\n type: 'h',\n level: (heading[1]?.length ?? 1) as 1 | 2 | 3,\n text: heading[2] ?? '',\n });\n continue;\n }\n const list = /^\\s*([-*]|\\d+[.)])\\s+(.*)$/.exec(line);\n if (list) {\n flush();\n const type = /\\d/.test(list[1] ?? '') ? 'ol' : 'ul';\n const prev = blocks.at(-1);\n if (prev && prev.type === type) prev.items.push(list[2] ?? '');\n else blocks.push({ type, items: [list[2] ?? ''] });\n continue;\n }\n const quote = /^>\\s?(.*)$/.exec(line);\n if (quote) {\n flush();\n blocks.push({ type: 'quote', text: quote[1] ?? '' });\n continue;\n }\n if (!line.trim()) flush();\n else para.push(line.trim());\n }\n flush();\n return blocks;\n}\n\nexport interface MarkdownProps extends Omit<React.ComponentProps<'div'>, 'children'> {\n children: string;\n}\n\n/** Renders a Markdown string safely, for assistant messages. */\nfunction Markdown({ children, className, ...props }: MarkdownProps) {\n const blocks = React.useMemo(() => parseMarkdown(children), [children]);\n return (\n <div\n data-slot=\"markdown\"\n className={cn('grid gap-3 text-sm leading-6 [&_li]:leading-6', className)}\n {...props}\n >\n {blocks.map((b, i) => {\n const k = `b${i}`;\n switch (b.type) {\n case 'h': {\n const Tag = (['h3', 'h4', 'h5'] as const)[b.level - 1] ?? 'h5';\n return (\n <Tag key={k} className=\"font-semibold tracking-[-0.01em]\">\n {inline(b.text, k)}\n </Tag>\n );\n }\n case 'ul':\n case 'ol': {\n const Tag = b.type;\n return (\n <Tag\n key={k}\n className={cn('grid gap-1 pl-5', b.type === 'ul' ? 'list-disc' : 'list-decimal')}\n >\n {b.items.map((item, j) => (\n <li key={`${k}-${j}`}>{inline(item, `${k}-${j}`)}</li>\n ))}\n </Tag>\n );\n }\n case 'quote':\n return (\n <blockquote\n key={k}\n className=\"border-l-2 border-border-strong pl-3 text-muted-foreground\"\n >\n {inline(b.text, k)}\n </blockquote>\n );\n case 'code':\n return <CodeBlock key={k} code={b.code} lang={b.lang} />;\n default:\n return <p key={k}>{inline(b.text, k)}</p>;\n }\n })}\n </div>\n );\n}\n\nexport { CodeBlock, Markdown };\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+ export type MessageRole = 'user' | 'assistant' | 'system';
3
+ export interface MessageProps extends React.ComponentProps<'div'> {
4
+ /** Who wrote it. (Not `role`, which is the ARIA attribute.) */
5
+ from: MessageRole;
6
+ /** Avatar or agent mark beside assistant messages. */
7
+ avatar?: React.ReactNode;
8
+ /** Sender name shown above the message, e.g. the agent's name. */
9
+ name?: React.ReactNode;
10
+ /** Timestamp or status text. */
11
+ meta?: React.ReactNode;
12
+ /** Actions such as copy and retry, shown on hover and focus. */
13
+ actions?: React.ReactNode;
14
+ }
15
+ /**
16
+ * One turn in a conversation. User turns sit on the right in a quiet bubble,
17
+ * assistant turns read full width with an optional avatar, and system notes
18
+ * are centred and small.
19
+ */
20
+ declare function Message({ from, avatar, name, meta, actions, children, className, ...props }: MessageProps): React.JSX.Element;
21
+ export { Message };