@evinvest/uikit 0.30.0 → 0.31.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 (204) hide show
  1. package/README.md +266 -72
  2. package/dist/{chunk-3IPB4QQJ.js → chunk-35SPIU4U.js} +10 -6
  3. package/dist/chunk-35SPIU4U.js.map +1 -0
  4. package/dist/components/account-list.js +128 -0
  5. package/dist/components/account-list.js.map +1 -0
  6. package/dist/components/account-menu-profile.js +114 -0
  7. package/dist/components/account-menu-profile.js.map +1 -0
  8. package/dist/components/account-menu.js +50 -23
  9. package/dist/components/account-menu.js.map +1 -1
  10. package/dist/components/account-settings-dialog.js +84 -0
  11. package/dist/components/account-settings-dialog.js.map +1 -0
  12. package/dist/components/alert-dialog.js +3 -2
  13. package/dist/components/alert-dialog.js.map +1 -1
  14. package/dist/components/animated-number-columns.js +105 -0
  15. package/dist/components/animated-number-columns.js.map +1 -0
  16. package/dist/components/animated-number.js +157 -0
  17. package/dist/components/animated-number.js.map +1 -0
  18. package/dist/components/app-shell.js +9 -0
  19. package/dist/components/app-shell.js.map +1 -1
  20. package/dist/components/aside.js +140 -0
  21. package/dist/components/aside.js.map +1 -0
  22. package/dist/components/badge.js +1 -1
  23. package/dist/components/badge.js.map +1 -1
  24. package/dist/components/button.js +3 -11
  25. package/dist/components/button.js.map +1 -1
  26. package/dist/components/chat-composer.js +141 -0
  27. package/dist/components/chat-composer.js.map +1 -0
  28. package/dist/components/chat-message.js +195 -0
  29. package/dist/components/chat-message.js.map +1 -0
  30. package/dist/components/chat-panel.js +137 -0
  31. package/dist/components/chat-panel.js.map +1 -0
  32. package/dist/components/chat-stream-text.js +69 -0
  33. package/dist/components/chat-stream-text.js.map +1 -0
  34. package/dist/components/chat-typing.js +26 -0
  35. package/dist/components/chat-typing.js.map +1 -0
  36. package/dist/components/command-palette.js +119 -0
  37. package/dist/components/command-palette.js.map +1 -0
  38. package/dist/components/command.js +3 -1
  39. package/dist/components/command.js.map +1 -1
  40. package/dist/components/confirm-dialog.js +123 -0
  41. package/dist/components/confirm-dialog.js.map +1 -0
  42. package/dist/components/context-menu.js +53 -5
  43. package/dist/components/context-menu.js.map +1 -1
  44. package/dist/components/dialog.js +3 -2
  45. package/dist/components/dialog.js.map +1 -1
  46. package/dist/components/drawer.js +3 -1
  47. package/dist/components/drawer.js.map +1 -1
  48. package/dist/components/dropdown-menu.js +64 -5
  49. package/dist/components/dropdown-menu.js.map +1 -1
  50. package/dist/components/form-row.js +51 -0
  51. package/dist/components/form-row.js.map +1 -0
  52. package/dist/components/header-controls.js +149 -0
  53. package/dist/components/header-controls.js.map +1 -0
  54. package/dist/components/icon-button.js +35 -0
  55. package/dist/components/icon-button.js.map +1 -0
  56. package/dist/components/key-value.js +68 -0
  57. package/dist/components/key-value.js.map +1 -0
  58. package/dist/components/menubar.js +39 -5
  59. package/dist/components/menubar.js.map +1 -1
  60. package/dist/components/native-select.js +3 -4
  61. package/dist/components/native-select.js.map +1 -1
  62. package/dist/components/nav-collapse.js +142 -0
  63. package/dist/components/nav-collapse.js.map +1 -0
  64. package/dist/components/nav-model.js.map +1 -1
  65. package/dist/components/navigation-menu.js +4 -5
  66. package/dist/components/navigation-menu.js.map +1 -1
  67. package/dist/components/page-frame.js +2 -5
  68. package/dist/components/page-frame.js.map +1 -1
  69. package/dist/components/popover-form.js +60 -0
  70. package/dist/components/popover-form.js.map +1 -0
  71. package/dist/components/resource-error.js +1 -1
  72. package/dist/components/resource-error.js.map +1 -1
  73. package/dist/components/section-nav.js +73 -20
  74. package/dist/components/section-nav.js.map +1 -1
  75. package/dist/components/select.js +16 -6
  76. package/dist/components/select.js.map +1 -1
  77. package/dist/components/settings.js +94 -0
  78. package/dist/components/settings.js.map +1 -0
  79. package/dist/components/sheet.js +3 -2
  80. package/dist/components/sheet.js.map +1 -1
  81. package/dist/components/shell-nav.js +80 -38
  82. package/dist/components/shell-nav.js.map +1 -1
  83. package/dist/components/smart-header.js +53 -0
  84. package/dist/components/smart-header.js.map +1 -0
  85. package/dist/components/status-item.js +42 -0
  86. package/dist/components/status-item.js.map +1 -0
  87. package/dist/components/system-banner.js +22 -12
  88. package/dist/components/system-banner.js.map +1 -1
  89. package/dist/components/top-bar.js +4 -3
  90. package/dist/components/top-bar.js.map +1 -1
  91. package/dist/components/ui-icons.js +77 -0
  92. package/dist/components/ui-icons.js.map +1 -0
  93. package/dist/components/use-count-tween.js +42 -0
  94. package/dist/components/use-count-tween.js.map +1 -0
  95. package/dist/components/workspace-tab.js +140 -0
  96. package/dist/components/workspace-tab.js.map +1 -0
  97. package/dist/generated/accent.js +2 -2
  98. package/dist/generated/accent.js.map +1 -1
  99. package/dist/generated/account-list.js +35 -0
  100. package/dist/generated/account-list.js.map +1 -0
  101. package/dist/generated/account-settings-dialog.js +27 -0
  102. package/dist/generated/account-settings-dialog.js.map +1 -0
  103. package/dist/generated/alert-dialog.js +1 -1
  104. package/dist/generated/alert-dialog.js.map +1 -1
  105. package/dist/generated/alert.js +2 -2
  106. package/dist/generated/alert.js.map +1 -1
  107. package/dist/generated/aside.js +36 -0
  108. package/dist/generated/aside.js.map +1 -0
  109. package/dist/generated/badge.js +7 -6
  110. package/dist/generated/badge.js.map +1 -1
  111. package/dist/generated/band.js +1 -1
  112. package/dist/generated/band.js.map +1 -1
  113. package/dist/generated/breadcrumb.js +2 -2
  114. package/dist/generated/breadcrumb.js.map +1 -1
  115. package/dist/generated/button-group.js +2 -2
  116. package/dist/generated/button-group.js.map +1 -1
  117. package/dist/generated/button.js +4 -4
  118. package/dist/generated/button.js.map +1 -1
  119. package/dist/generated/card.js +6 -6
  120. package/dist/generated/card.js.map +1 -1
  121. package/dist/generated/chat-composer.js +21 -0
  122. package/dist/generated/chat-composer.js.map +1 -0
  123. package/dist/generated/chat-message.js +86 -0
  124. package/dist/generated/chat-message.js.map +1 -0
  125. package/dist/generated/chat-typing.js +9 -0
  126. package/dist/generated/chat-typing.js.map +1 -0
  127. package/dist/generated/command-palette.js +17 -0
  128. package/dist/generated/command-palette.js.map +1 -0
  129. package/dist/generated/command.js +2 -2
  130. package/dist/generated/command.js.map +1 -1
  131. package/dist/generated/confirm-dialog.js +28 -0
  132. package/dist/generated/confirm-dialog.js.map +1 -0
  133. package/dist/generated/context-menu.js +1 -1
  134. package/dist/generated/context-menu.js.map +1 -1
  135. package/dist/generated/date-time-picker.js +1 -1
  136. package/dist/generated/date-time-picker.js.map +1 -1
  137. package/dist/generated/dialog.js +1 -1
  138. package/dist/generated/dialog.js.map +1 -1
  139. package/dist/generated/drawer.js +1 -1
  140. package/dist/generated/drawer.js.map +1 -1
  141. package/dist/generated/dropdown-menu.js +1 -1
  142. package/dist/generated/dropdown-menu.js.map +1 -1
  143. package/dist/generated/form-row.js +17 -0
  144. package/dist/generated/form-row.js.map +1 -0
  145. package/dist/generated/header-controls.js +56 -0
  146. package/dist/generated/header-controls.js.map +1 -0
  147. package/dist/generated/icon-button.js +13 -0
  148. package/dist/generated/icon-button.js.map +1 -0
  149. package/dist/generated/key-value.js +15 -0
  150. package/dist/generated/key-value.js.map +1 -0
  151. package/dist/generated/menubar.js +1 -1
  152. package/dist/generated/menubar.js.map +1 -1
  153. package/dist/generated/navigation-menu.js +1 -1
  154. package/dist/generated/navigation-menu.js.map +1 -1
  155. package/dist/generated/popover-form.js +13 -0
  156. package/dist/generated/popover-form.js.map +1 -0
  157. package/dist/generated/section-label.js +10 -0
  158. package/dist/generated/section-label.js.map +1 -0
  159. package/dist/generated/select.js +15 -3
  160. package/dist/generated/select.js.map +1 -1
  161. package/dist/generated/settings.js +36 -0
  162. package/dist/generated/settings.js.map +1 -0
  163. package/dist/generated/sheet.js +1 -1
  164. package/dist/generated/sheet.js.map +1 -1
  165. package/dist/generated/slider.js +1 -1
  166. package/dist/generated/slider.js.map +1 -1
  167. package/dist/generated/smart-header.js +11 -0
  168. package/dist/generated/smart-header.js.map +1 -0
  169. package/dist/generated/sonner.js +4 -4
  170. package/dist/generated/sonner.js.map +1 -1
  171. package/dist/generated/status-item.js +17 -0
  172. package/dist/generated/status-item.js.map +1 -0
  173. package/dist/generated/system-banner.js +33 -0
  174. package/dist/generated/system-banner.js.map +1 -0
  175. package/dist/generated/top-bar.js +9 -0
  176. package/dist/generated/top-bar.js.map +1 -0
  177. package/dist/generated/workspace-tab.js +17 -0
  178. package/dist/generated/workspace-tab.js.map +1 -0
  179. package/dist/index.d.ts +852 -156
  180. package/dist/index.js +21 -1
  181. package/dist/lib/cn.js +8 -1
  182. package/dist/lib/cn.js.map +1 -1
  183. package/dist/lib/number-parts.js +90 -0
  184. package/dist/lib/number-parts.js.map +1 -0
  185. package/dist/palette-cli.js +1 -1
  186. package/dist/palette.d.ts +6 -4
  187. package/dist/palette.js +3 -1
  188. package/dist/primitives/dismissable-layer.js +76 -4
  189. package/dist/primitives/dismissable-layer.js.map +1 -1
  190. package/dist/primitives/focus-scope.js +20 -5
  191. package/dist/primitives/focus-scope.js.map +1 -1
  192. package/dist/primitives/use-floating.js +16 -1
  193. package/dist/primitives/use-floating.js.map +1 -1
  194. package/dist/primitives/use-reduced-motion.js +20 -0
  195. package/dist/primitives/use-reduced-motion.js.map +1 -0
  196. package/package.json +1 -1
  197. package/styles/ev.css +281 -49
  198. package/styles/theme.css +335 -39
  199. package/styles/tokens.css +643 -103
  200. package/dist/chunk-3IPB4QQJ.js.map +0 -1
  201. package/dist/components/terminal.js +0 -206
  202. package/dist/components/terminal.js.map +0 -1
  203. package/dist/generated/terminal.js +0 -70
  204. package/dist/generated/terminal.js.map +0 -1
@@ -0,0 +1,141 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import {
5
+ CHAT_COMPOSER_ATTACHMENTS,
6
+ CHAT_COMPOSER_BASE,
7
+ CHAT_COMPOSER_FIELD,
8
+ CHAT_COMPOSER_HINT,
9
+ CHAT_COMPOSER_IDLE,
10
+ CHAT_COMPOSER_SPACER,
11
+ CHAT_COMPOSER_STREAMING,
12
+ CHAT_COMPOSER_TOOLBAR,
13
+ MODEL_CHIP_BASE
14
+ } from "../generated/chat-composer.js";
15
+ import { cn } from "../lib/cn.js";
16
+ import { useControllableState } from "../primitives/use-controllable-state.js";
17
+ import {
18
+ DropdownMenu,
19
+ DropdownMenuContent,
20
+ DropdownMenuRadioGroup,
21
+ DropdownMenuRadioItem,
22
+ DropdownMenuTrigger
23
+ } from "./dropdown-menu.js";
24
+ import { IconButton } from "./icon-button.js";
25
+ import { Textarea } from "./textarea.js";
26
+ import { UiIcon } from "./ui-icons.js";
27
+ function ModelChip({ models, value, onValueChange, "aria-label": ariaLabel = "Model", className }) {
28
+ const current = models.find((m) => m.id === value);
29
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
30
+ /* @__PURE__ */ jsxs(
31
+ DropdownMenuTrigger,
32
+ {
33
+ "aria-label": `${ariaLabel}: ${current?.label ?? value}`,
34
+ "data-slot": "model-chip",
35
+ className: cn(MODEL_CHIP_BASE, className),
36
+ children: [
37
+ current?.label ?? value,
38
+ /* @__PURE__ */ jsx(UiIcon, { name: "chevron-down" })
39
+ ]
40
+ }
41
+ ),
42
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", children: /* @__PURE__ */ jsx(DropdownMenuRadioGroup, { value, onValueChange, children: models.map((m) => /* @__PURE__ */ jsx(DropdownMenuRadioItem, { value: m.id, children: m.label }, m.id)) }) })
43
+ ] });
44
+ }
45
+ function ChatComposer({
46
+ value,
47
+ defaultValue = "",
48
+ onValueChange,
49
+ onSend,
50
+ streaming = false,
51
+ onStop,
52
+ placeholder = "Ask the assistant\u2026",
53
+ onAttach,
54
+ attachments,
55
+ tools,
56
+ hint,
57
+ disabled = false,
58
+ labels = {},
59
+ className,
60
+ ...props
61
+ }) {
62
+ const [text, setText] = useControllableState({
63
+ ...value !== void 0 ? { value } : {},
64
+ defaultValue,
65
+ ...onValueChange ? { onChange: onValueChange } : {}
66
+ });
67
+ const trimmed = text.trim();
68
+ const root = React.useRef(null);
69
+ const send = () => {
70
+ if (trimmed === "" || disabled) return;
71
+ onSend(trimmed);
72
+ setText("");
73
+ };
74
+ return /* @__PURE__ */ jsxs(
75
+ "div",
76
+ {
77
+ ref: root,
78
+ "data-slot": "chat-composer",
79
+ "data-state": streaming ? "streaming" : "default",
80
+ className: cn(
81
+ CHAT_COMPOSER_BASE,
82
+ streaming ? CHAT_COMPOSER_STREAMING : CHAT_COMPOSER_IDLE,
83
+ className
84
+ ),
85
+ ...props,
86
+ children: [
87
+ attachments != null && /* @__PURE__ */ jsx("div", { className: CHAT_COMPOSER_ATTACHMENTS, children: attachments }),
88
+ /* @__PURE__ */ jsx(
89
+ Textarea,
90
+ {
91
+ rows: 1,
92
+ value: text,
93
+ disabled,
94
+ placeholder,
95
+ "aria-label": labels.field ?? "Message",
96
+ onChange: (e) => setText(e.target.value),
97
+ onKeyDown: (e) => {
98
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && e.keyCode !== 229) {
99
+ e.preventDefault();
100
+ send();
101
+ }
102
+ },
103
+ className: CHAT_COMPOSER_FIELD
104
+ }
105
+ ),
106
+ /* @__PURE__ */ jsxs("div", { className: CHAT_COMPOSER_TOOLBAR, children: [
107
+ onAttach && /* @__PURE__ */ jsx(IconButton, { "aria-label": labels.attach ?? "Attach file", size: "sm", onClick: onAttach, disabled, children: /* @__PURE__ */ jsx(UiIcon, { name: "plus" }) }),
108
+ tools,
109
+ /* @__PURE__ */ jsx("span", { className: CHAT_COMPOSER_SPACER }),
110
+ hint != null && /* @__PURE__ */ jsx("span", { className: CHAT_COMPOSER_HINT, children: hint }),
111
+ streaming ? /* @__PURE__ */ jsx(
112
+ IconButton,
113
+ {
114
+ variant: "outline",
115
+ "aria-label": labels.stop ?? "Stop",
116
+ onClick: () => {
117
+ onStop?.();
118
+ root.current?.querySelector("textarea")?.focus();
119
+ },
120
+ children: /* @__PURE__ */ jsx(UiIcon, { name: "stop" })
121
+ }
122
+ ) : /* @__PURE__ */ jsx(
123
+ IconButton,
124
+ {
125
+ variant: "outline",
126
+ "aria-label": labels.send ?? "Send",
127
+ onClick: send,
128
+ disabled: disabled || trimmed === "",
129
+ children: /* @__PURE__ */ jsx(UiIcon, { name: "arrow-up" })
130
+ }
131
+ )
132
+ ] })
133
+ ]
134
+ }
135
+ );
136
+ }
137
+ export {
138
+ ChatComposer,
139
+ ModelChip
140
+ };
141
+ //# sourceMappingURL=chat-composer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/chat-composer.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport {\n CHAT_COMPOSER_ATTACHMENTS,\n CHAT_COMPOSER_BASE,\n CHAT_COMPOSER_FIELD,\n CHAT_COMPOSER_HINT,\n CHAT_COMPOSER_IDLE,\n CHAT_COMPOSER_SPACER,\n CHAT_COMPOSER_STREAMING,\n CHAT_COMPOSER_TOOLBAR,\n MODEL_CHIP_BASE,\n} from \"../generated/chat-composer\";\nimport { cn } from \"../lib/cn\";\nimport { useControllableState } from \"../primitives/use-controllable-state\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\";\nimport { IconButton } from \"./icon-button\";\nimport { Textarea } from \"./textarea\";\nimport { UiIcon } from \"./ui-icons\";\n\nexport interface ModelOption {\n id: string;\n label: string;\n}\n\nexport interface ModelChipProps {\n models: readonly ModelOption[];\n value: string;\n onValueChange: (id: string) => void;\n /** Names the menu. Default `\"Model\"`. */\n \"aria-label\"?: string;\n className?: string;\n}\n\n/** The small bordered button naming the model; opens a radio menu of the options. */\nexport function ModelChip({ models, value, onValueChange, \"aria-label\": ariaLabel = \"Model\", className }: ModelChipProps) {\n const current = models.find((m) => m.id === value);\n return (\n <DropdownMenu>\n <DropdownMenuTrigger\n aria-label={`${ariaLabel}: ${current?.label ?? value}`}\n data-slot=\"model-chip\"\n className={cn(MODEL_CHIP_BASE, className)}\n >\n {current?.label ?? value}\n <UiIcon name=\"chevron-down\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n <DropdownMenuRadioGroup value={value} onValueChange={onValueChange}>\n {models.map((m) => (\n <DropdownMenuRadioItem key={m.id} value={m.id}>\n {m.label}\n </DropdownMenuRadioItem>\n ))}\n </DropdownMenuRadioGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\nexport interface ChatComposerLabels {\n /** Names the message field. Default `\"Message\"`. */\n field?: string;\n send?: string;\n stop?: string;\n attach?: string;\n}\n\nexport interface ChatComposerProps extends Omit<React.ComponentProps<\"div\">, \"onChange\" | \"defaultValue\"> {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n /** Enter or the send button, with the trimmed text. The field clears afterwards. */\n onSend: (text: string) => void;\n /** The assistant is answering: the button becomes Stop. Enter still queues a follow-up. */\n streaming?: boolean;\n onStop?: () => void;\n placeholder?: string;\n /** Shows the attach button. */\n onAttach?: () => void;\n /** A row of `ChatAttachment`s above the field. */\n attachments?: React.ReactNode;\n /** Model chips and other toolbar controls, after the attach button. */\n tools?: React.ReactNode;\n /** A caption in the toolbar: \"Enter to send · Shift+Enter for a new line\". */\n hint?: React.ReactNode;\n disabled?: boolean;\n labels?: ChatComposerLabels;\n}\n\nexport function ChatComposer({\n value,\n defaultValue = \"\",\n onValueChange,\n onSend,\n streaming = false,\n onStop,\n placeholder = \"Ask the assistant…\",\n onAttach,\n attachments,\n tools,\n hint,\n disabled = false,\n labels = {},\n className,\n ...props\n}: ChatComposerProps) {\n const [text, setText] = useControllableState<string>({\n ...(value !== undefined ? { value } : {}),\n defaultValue,\n ...(onValueChange ? { onChange: onValueChange } : {}),\n });\n const trimmed = text.trim();\n const root = React.useRef<HTMLDivElement>(null);\n\n const send = () => {\n if (trimmed === \"\" || disabled) return;\n onSend(trimmed);\n setText(\"\");\n };\n\n return (\n <div\n ref={root}\n data-slot=\"chat-composer\"\n data-state={streaming ? \"streaming\" : \"default\"}\n className={cn(\n CHAT_COMPOSER_BASE,\n streaming ? CHAT_COMPOSER_STREAMING : CHAT_COMPOSER_IDLE,\n className,\n )}\n {...props}\n >\n {attachments != null && <div className={CHAT_COMPOSER_ATTACHMENTS}>{attachments}</div>}\n <Textarea\n rows={1}\n value={text}\n disabled={disabled}\n placeholder={placeholder}\n aria-label={labels.field ?? \"Message\"}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n // Enter during IME composition confirms the candidate; it must not send. Safari reports the\n // confirming Enter after compositionend, with isComposing already false and keyCode 229.\n if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing && e.keyCode !== 229) {\n e.preventDefault();\n send();\n }\n }}\n className={CHAT_COMPOSER_FIELD}\n />\n <div className={CHAT_COMPOSER_TOOLBAR}>\n {onAttach && (\n <IconButton aria-label={labels.attach ?? \"Attach file\"} size=\"sm\" onClick={onAttach} disabled={disabled}>\n <UiIcon name=\"plus\" />\n </IconButton>\n )}\n {tools}\n <span className={CHAT_COMPOSER_SPACER} />\n {hint != null && <span className={CHAT_COMPOSER_HINT}>{hint}</span>}\n {streaming ? (\n <IconButton\n variant=\"outline\"\n aria-label={labels.stop ?? \"Stop\"}\n onClick={() => {\n onStop?.();\n // The Stop button turns into Send, so focus would otherwise stay on a swapped-out element.\n root.current?.querySelector(\"textarea\")?.focus();\n }}\n >\n <UiIcon name=\"stop\" />\n </IconButton>\n ) : (\n <IconButton\n variant=\"outline\"\n aria-label={labels.send ?? \"Send\"}\n onClick={send}\n disabled={disabled || trimmed === \"\"}\n >\n <UiIcon name=\"arrow-up\" />\n </IconButton>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AA8CM,SAME,KANF;AA5CN,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,UAAU;AACnB,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,cAAc;AAiBhB,SAAS,UAAU,EAAE,QAAQ,OAAO,eAAe,cAAc,YAAY,SAAS,UAAU,GAAmB;AACxH,QAAM,UAAU,OAAO,KAAK,CAAC,MAAM,EAAE,OAAO,KAAK;AACjD,SACE,qBAAC,gBACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,cAAY,GAAG,SAAS,KAAK,SAAS,SAAS,KAAK;AAAA,QACpD,aAAU;AAAA,QACV,WAAW,GAAG,iBAAiB,SAAS;AAAA,QAEvC;AAAA,mBAAS,SAAS;AAAA,UACnB,oBAAC,UAAO,MAAK,gBAAe;AAAA;AAAA;AAAA,IAC9B;AAAA,IACA,oBAAC,uBAAoB,OAAM,SACzB,8BAAC,0BAAuB,OAAc,eACnC,iBAAO,IAAI,CAAC,MACX,oBAAC,yBAAiC,OAAO,EAAE,IACxC,YAAE,SADuB,EAAE,EAE9B,CACD,GACH,GACF;AAAA,KACF;AAEJ;AAgCO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,SAAS,CAAC;AAAA,EACV;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,CAAC,MAAM,OAAO,IAAI,qBAA6B;AAAA,IACnD,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,IACvC;AAAA,IACA,GAAI,gBAAgB,EAAE,UAAU,cAAc,IAAI,CAAC;AAAA,EACrD,CAAC;AACD,QAAM,UAAU,KAAK,KAAK;AAC1B,QAAM,OAAO,MAAM,OAAuB,IAAI;AAE9C,QAAM,OAAO,MAAM;AACjB,QAAI,YAAY,MAAM,SAAU;AAChC,WAAO,OAAO;AACd,YAAQ,EAAE;AAAA,EACZ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,aAAU;AAAA,MACV,cAAY,YAAY,cAAc;AAAA,MACtC,WAAW;AAAA,QACT;AAAA,QACA,YAAY,0BAA0B;AAAA,QACtC;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA,uBAAe,QAAQ,oBAAC,SAAI,WAAW,2BAA4B,uBAAY;AAAA,QAChF;AAAA,UAAC;AAAA;AAAA,YACC,MAAM;AAAA,YACN,OAAO;AAAA,YACP;AAAA,YACA;AAAA,YACA,cAAY,OAAO,SAAS;AAAA,YAC5B,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,YACvC,WAAW,CAAC,MAAM;AAGhB,kBAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,YAAY,CAAC,EAAE,YAAY,eAAe,EAAE,YAAY,KAAK;AACvF,kBAAE,eAAe;AACjB,qBAAK;AAAA,cACP;AAAA,YACF;AAAA,YACA,WAAW;AAAA;AAAA,QACb;AAAA,QACA,qBAAC,SAAI,WAAW,uBACb;AAAA,sBACC,oBAAC,cAAW,cAAY,OAAO,UAAU,eAAe,MAAK,MAAK,SAAS,UAAU,UACnF,8BAAC,UAAO,MAAK,QAAO,GACtB;AAAA,UAED;AAAA,UACD,oBAAC,UAAK,WAAW,sBAAsB;AAAA,UACtC,QAAQ,QAAQ,oBAAC,UAAK,WAAW,oBAAqB,gBAAK;AAAA,UAC3D,YACC;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,cAAY,OAAO,QAAQ;AAAA,cAC3B,SAAS,MAAM;AACb,yBAAS;AAET,qBAAK,SAAS,cAAc,UAAU,GAAG,MAAM;AAAA,cACjD;AAAA,cAEA,8BAAC,UAAO,MAAK,QAAO;AAAA;AAAA,UACtB,IAEA;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,cAAY,OAAO,QAAQ;AAAA,cAC3B,SAAS;AAAA,cACT,UAAU,YAAY,YAAY;AAAA,cAElC,8BAAC,UAAO,MAAK,YAAW;AAAA;AAAA,UAC1B;AAAA,WAEJ;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,195 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import {
6
+ CHAT_ATTACHMENT_BASE,
7
+ CHAT_ATTACHMENT_BODY,
8
+ CHAT_ATTACHMENT_ICON,
9
+ CHAT_ATTACHMENT_META,
10
+ CHAT_ATTACHMENT_NAME,
11
+ CHAT_ATTACHMENT_PROGRESS,
12
+ CHAT_ATTACHMENT_PROGRESS_FILL,
13
+ CHAT_ATTACHMENT_ROW,
14
+ CHAT_BUBBLE_BASE,
15
+ CHAT_MARKER_DOT,
16
+ CHAT_MARKER_RULE,
17
+ CHAT_MARKER_SEPARATOR,
18
+ CHAT_MARKER_SEPARATOR_LABEL,
19
+ CHAT_MARKER_STATUS,
20
+ CHAT_MARKER_TOOL,
21
+ CHAT_MARKER_TOOL_BODY,
22
+ CHAT_MARKER_TOOL_BORDER,
23
+ CHAT_MARKER_TOOL_CHECK,
24
+ CHAT_MARKER_TOOL_DETAIL,
25
+ CHAT_MARKER_TOOL_ERROR_BORDER,
26
+ CHAT_MARKER_TOOL_ICON,
27
+ CHAT_MARKER_TOOL_META,
28
+ CHAT_MARKER_TOOL_TITLE,
29
+ CHAT_MESSAGE_AVATAR,
30
+ CHAT_MESSAGE_AVATAR_TILE,
31
+ CHAT_MESSAGE_BASE,
32
+ CHAT_MESSAGE_BODY,
33
+ CHAT_MESSAGE_BODY_ASSISTANT,
34
+ CHAT_MESSAGE_BODY_USER,
35
+ CHAT_MESSAGE_HEADER,
36
+ CHAT_MESSAGE_NAME,
37
+ CHAT_MESSAGE_TIME,
38
+ CHAT_MESSAGE_USER,
39
+ chatBubbleVariantClasses,
40
+ chatStatusDotClasses,
41
+ chatToolMetaClasses
42
+ } from "../generated/chat-message.js";
43
+ import { ChatStreamText } from "./chat-stream-text.js";
44
+ import { IconButton } from "./icon-button.js";
45
+ import { UiIcon } from "./ui-icons.js";
46
+ function ChatBubble({ variant = "assistant", streaming = false, className, children, ...props }) {
47
+ const [live, setLive] = React.useState(streaming);
48
+ if (streaming && !live) setLive(true);
49
+ return /* @__PURE__ */ jsx(
50
+ "div",
51
+ {
52
+ "data-slot": "chat-bubble",
53
+ "data-variant": variant,
54
+ "data-streaming": streaming ? "" : void 0,
55
+ "data-caret": live ? "" : void 0,
56
+ className: cn(CHAT_BUBBLE_BASE, chatBubbleVariantClasses[variant], className),
57
+ ...props,
58
+ children: live && typeof children === "string" ? /* @__PURE__ */ jsx(ChatStreamText, { text: children, streaming }) : children
59
+ }
60
+ );
61
+ }
62
+ function ChatMessage({ role, name, time, avatar, compact = false, className, children, ...props }) {
63
+ const user = role === "user";
64
+ return /* @__PURE__ */ jsxs(
65
+ "div",
66
+ {
67
+ "data-slot": "chat-message",
68
+ "data-role": role,
69
+ "data-enter": "rise",
70
+ className: cn(CHAT_MESSAGE_BASE, user && CHAT_MESSAGE_USER, className),
71
+ ...props,
72
+ children: [
73
+ !user && !compact && /* @__PURE__ */ jsx("div", { className: CHAT_MESSAGE_AVATAR, children: avatar ?? /* @__PURE__ */ jsx("span", { className: CHAT_MESSAGE_AVATAR_TILE, children: /* @__PURE__ */ jsx(UiIcon, { name: "sparkles" }) }) }),
74
+ /* @__PURE__ */ jsxs("div", { className: cn(CHAT_MESSAGE_BODY, user ? CHAT_MESSAGE_BODY_USER : CHAT_MESSAGE_BODY_ASSISTANT), children: [
75
+ !compact && (name != null || time != null) && /* @__PURE__ */ jsxs("div", { className: CHAT_MESSAGE_HEADER, children: [
76
+ name != null && /* @__PURE__ */ jsx("span", { className: CHAT_MESSAGE_NAME, children: name }),
77
+ time != null && /* @__PURE__ */ jsx("span", { className: CHAT_MESSAGE_TIME, children: time })
78
+ ] }),
79
+ children
80
+ ] })
81
+ ]
82
+ }
83
+ );
84
+ }
85
+ function ChatMarker(allProps) {
86
+ if (allProps.type === "separator") {
87
+ const { type: _type2, className: className2, children: children2, ...rest2 } = allProps;
88
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "chat-marker", "data-type": "separator", className: cn(CHAT_MARKER_SEPARATOR, className2), ...rest2, children: [
89
+ /* @__PURE__ */ jsx("span", { className: CHAT_MARKER_RULE }),
90
+ /* @__PURE__ */ jsx("span", { className: CHAT_MARKER_SEPARATOR_LABEL, children: children2 }),
91
+ /* @__PURE__ */ jsx("span", { className: CHAT_MARKER_RULE })
92
+ ] });
93
+ }
94
+ if (allProps.type === "status") {
95
+ const { type: _type2, state: state2 = "done", className: className2, children: children2, ...rest2 } = allProps;
96
+ return /* @__PURE__ */ jsxs(
97
+ "div",
98
+ {
99
+ "data-slot": "chat-marker",
100
+ "data-type": "status",
101
+ "data-state": state2,
102
+ role: state2 === "running" ? "status" : void 0,
103
+ className: cn(CHAT_MARKER_STATUS, className2),
104
+ ...rest2,
105
+ children: [
106
+ /* @__PURE__ */ jsx(
107
+ "span",
108
+ {
109
+ "aria-hidden": true,
110
+ className: cn(CHAT_MARKER_DOT, chatStatusDotClasses[state2])
111
+ }
112
+ ),
113
+ children2
114
+ ]
115
+ }
116
+ );
117
+ }
118
+ const { type: _type, state = "done", title, detail, meta, className, children, ...rest } = allProps;
119
+ return /* @__PURE__ */ jsxs(
120
+ "div",
121
+ {
122
+ "data-slot": "chat-marker",
123
+ "data-type": "tool",
124
+ "data-state": state,
125
+ className: cn(
126
+ CHAT_MARKER_TOOL,
127
+ state === "error" ? CHAT_MARKER_TOOL_ERROR_BORDER : CHAT_MARKER_TOOL_BORDER,
128
+ className
129
+ ),
130
+ ...rest,
131
+ children: [
132
+ /* @__PURE__ */ jsx(UiIcon, { name: state === "error" ? "alert" : "sparkles", className: CHAT_MARKER_TOOL_ICON }),
133
+ /* @__PURE__ */ jsxs("div", { className: CHAT_MARKER_TOOL_BODY, children: [
134
+ /* @__PURE__ */ jsx("span", { className: CHAT_MARKER_TOOL_TITLE, children: title }),
135
+ detail != null && /* @__PURE__ */ jsx("span", { className: CHAT_MARKER_TOOL_DETAIL, children: detail }),
136
+ children
137
+ ] }),
138
+ meta != null && /* @__PURE__ */ jsx("span", { className: cn(CHAT_MARKER_TOOL_META, chatToolMetaClasses[state]), children: meta }),
139
+ state === "done" && /* @__PURE__ */ jsx(UiIcon, { name: "check", className: CHAT_MARKER_TOOL_CHECK })
140
+ ]
141
+ }
142
+ );
143
+ }
144
+ function ChatAttachment({
145
+ type = "file",
146
+ state = "ready",
147
+ name,
148
+ meta,
149
+ progress = 0,
150
+ onRemove,
151
+ removeLabel,
152
+ className,
153
+ ...props
154
+ }) {
155
+ const pct = Math.min(100, Math.max(0, Math.round(progress)));
156
+ return /* @__PURE__ */ jsxs(
157
+ "div",
158
+ {
159
+ "data-slot": "chat-attachment",
160
+ "data-type": type,
161
+ "data-state": state,
162
+ className: cn(CHAT_ATTACHMENT_BASE, className),
163
+ ...props,
164
+ children: [
165
+ /* @__PURE__ */ jsxs("div", { className: CHAT_ATTACHMENT_ROW, children: [
166
+ /* @__PURE__ */ jsx("span", { className: CHAT_ATTACHMENT_ICON, children: /* @__PURE__ */ jsx(UiIcon, { name: type === "image" ? "image" : "file" }) }),
167
+ /* @__PURE__ */ jsxs("div", { className: CHAT_ATTACHMENT_BODY, children: [
168
+ /* @__PURE__ */ jsx("span", { className: CHAT_ATTACHMENT_NAME, children: name }),
169
+ /* @__PURE__ */ jsx("span", { className: CHAT_ATTACHMENT_META, children: meta ?? (state === "uploading" ? `Uploading\u2026 ${pct}%` : null) })
170
+ ] }),
171
+ onRemove && /* @__PURE__ */ jsx(IconButton, { "aria-label": removeLabel ?? `Remove ${name}`, size: "sm", onClick: onRemove, children: /* @__PURE__ */ jsx(UiIcon, { name: "x" }) })
172
+ ] }),
173
+ state === "uploading" && /* @__PURE__ */ jsx(
174
+ "div",
175
+ {
176
+ role: "progressbar",
177
+ "aria-label": name,
178
+ "aria-valuemin": 0,
179
+ "aria-valuemax": 100,
180
+ "aria-valuenow": pct,
181
+ className: CHAT_ATTACHMENT_PROGRESS,
182
+ children: /* @__PURE__ */ jsx("div", { className: CHAT_ATTACHMENT_PROGRESS_FILL, style: { width: `${pct}%` } })
183
+ }
184
+ )
185
+ ]
186
+ }
187
+ );
188
+ }
189
+ export {
190
+ ChatAttachment,
191
+ ChatBubble,
192
+ ChatMarker,
193
+ ChatMessage
194
+ };
195
+ //# sourceMappingURL=chat-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/chat-message.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../lib/cn\";\nimport {\n CHAT_ATTACHMENT_BASE,\n CHAT_ATTACHMENT_BODY,\n CHAT_ATTACHMENT_ICON,\n CHAT_ATTACHMENT_META,\n CHAT_ATTACHMENT_NAME,\n CHAT_ATTACHMENT_PROGRESS,\n CHAT_ATTACHMENT_PROGRESS_FILL,\n CHAT_ATTACHMENT_ROW,\n CHAT_BUBBLE_BASE,\n CHAT_MARKER_DOT,\n CHAT_MARKER_RULE,\n CHAT_MARKER_SEPARATOR,\n CHAT_MARKER_SEPARATOR_LABEL,\n CHAT_MARKER_STATUS,\n CHAT_MARKER_TOOL,\n CHAT_MARKER_TOOL_BODY,\n CHAT_MARKER_TOOL_BORDER,\n CHAT_MARKER_TOOL_CHECK,\n CHAT_MARKER_TOOL_DETAIL,\n CHAT_MARKER_TOOL_ERROR_BORDER,\n CHAT_MARKER_TOOL_ICON,\n CHAT_MARKER_TOOL_META,\n CHAT_MARKER_TOOL_TITLE,\n CHAT_MESSAGE_AVATAR,\n CHAT_MESSAGE_AVATAR_TILE,\n CHAT_MESSAGE_BASE,\n CHAT_MESSAGE_BODY,\n CHAT_MESSAGE_BODY_ASSISTANT,\n CHAT_MESSAGE_BODY_USER,\n CHAT_MESSAGE_HEADER,\n CHAT_MESSAGE_NAME,\n CHAT_MESSAGE_TIME,\n CHAT_MESSAGE_USER,\n chatBubbleVariantClasses,\n chatStatusDotClasses,\n chatToolMetaClasses,\n} from \"../generated/chat-message\";\nimport type { ChatBubbleVariant, ChatStatusState, ChatToolState } from \"../generated/chat-message\";\nimport { ChatStreamText } from \"./chat-stream-text\";\nimport { IconButton } from \"./icon-button\";\nimport { UiIcon } from \"./ui-icons\";\n\nexport type { ChatBubbleVariant };\n\nexport interface ChatBubbleProps extends React.ComponentProps<\"div\"> {\n variant?: ChatBubbleVariant;\n /**\n * Still receiving text: a soft caret pulses after it, and string children\n * fade in word by word (`ChatStreamText`). Stays set until the text is complete.\n */\n streaming?: boolean;\n}\n\n/** One block of message text; `assistant` has no box so long answers read as a page. */\nexport function ChatBubble({ variant = \"assistant\", streaming = false, className, children, ...props }: ChatBubbleProps) {\n // Once a bubble has streamed it keeps its word wrappers and its caret slot, so\n // the last words finish landing and the caret fades out instead of vanishing.\n const [live, setLive] = React.useState(streaming);\n if (streaming && !live) setLive(true);\n return (\n <div\n data-slot=\"chat-bubble\"\n data-variant={variant}\n data-streaming={streaming ? \"\" : undefined}\n data-caret={live ? \"\" : undefined}\n className={cn(CHAT_BUBBLE_BASE, chatBubbleVariantClasses[variant], className)}\n {...props}\n >\n {live && typeof children === \"string\" ? <ChatStreamText text={children} streaming={streaming} /> : children}\n </div>\n );\n}\n\nexport interface ChatMessageProps extends Omit<React.ComponentProps<\"div\">, \"role\"> {\n role: \"assistant\" | \"user\";\n /** \"Assistant\", \"You\". */\n name?: React.ReactNode;\n /** Already formatted: \"13:04\". */\n time?: React.ReactNode;\n /** The assistant's tile; a default sparkle tile when omitted. */\n avatar?: React.ReactNode;\n /** Drops the name row and the avatar, for a narrow dock. */\n compact?: boolean;\n}\n\n/** A turn: who spoke, when, then the bubbles and markers. Fades in and rises once when it mounts. */\nexport function ChatMessage({ role, name, time, avatar, compact = false, className, children, ...props }: ChatMessageProps) {\n const user = role === \"user\";\n return (\n <div\n data-slot=\"chat-message\"\n data-role={role}\n data-enter=\"rise\"\n className={cn(CHAT_MESSAGE_BASE, user && CHAT_MESSAGE_USER, className)}\n {...props}\n >\n {!user && !compact && (\n <div className={CHAT_MESSAGE_AVATAR}>\n {avatar ?? (\n <span className={CHAT_MESSAGE_AVATAR_TILE}>\n <UiIcon name=\"sparkles\" />\n </span>\n )}\n </div>\n )}\n <div className={cn(CHAT_MESSAGE_BODY, user ? CHAT_MESSAGE_BODY_USER : CHAT_MESSAGE_BODY_ASSISTANT)}>\n {!compact && (name != null || time != null) && (\n <div className={CHAT_MESSAGE_HEADER}>\n {name != null && <span className={CHAT_MESSAGE_NAME}>{name}</span>}\n {time != null && <span className={CHAT_MESSAGE_TIME}>{time}</span>}\n </div>\n )}\n {children}\n </div>\n </div>\n );\n}\n\nexport type ChatMarkerProps = Omit<React.ComponentProps<\"div\">, \"title\"> &\n (\n | { type: \"status\"; state?: ChatStatusState }\n | {\n type: \"tool\";\n state?: ChatToolState;\n /** The tool's name. */\n title: React.ReactNode;\n detail?: React.ReactNode;\n /** The right-hand note: \"Running…\", \"0.4s\", \"Failed · 1.2s\". */\n meta?: React.ReactNode;\n }\n | { type: \"separator\" }\n );\n\n/** The quiet lines between messages: progress, a tool call, or a day break. */\nexport function ChatMarker(allProps: ChatMarkerProps) {\n if (allProps.type === \"separator\") {\n const { type: _type, className, children, ...rest } = allProps;\n return (\n <div data-slot=\"chat-marker\" data-type=\"separator\" className={cn(CHAT_MARKER_SEPARATOR, className)} {...rest}>\n <span className={CHAT_MARKER_RULE} />\n <span className={CHAT_MARKER_SEPARATOR_LABEL}>{children}</span>\n <span className={CHAT_MARKER_RULE} />\n </div>\n );\n }\n if (allProps.type === \"status\") {\n const { type: _type, state = \"done\", className, children, ...rest } = allProps;\n return (\n <div\n data-slot=\"chat-marker\"\n data-type=\"status\"\n data-state={state}\n role={state === \"running\" ? \"status\" : undefined}\n className={cn(CHAT_MARKER_STATUS, className)}\n {...rest}\n >\n <span\n aria-hidden\n className={cn(CHAT_MARKER_DOT, chatStatusDotClasses[state])}\n />\n {children}\n </div>\n );\n }\n const { type: _type, state = \"done\", title, detail, meta, className, children, ...rest } = allProps;\n return (\n <div\n data-slot=\"chat-marker\"\n data-type=\"tool\"\n data-state={state}\n className={cn(\n CHAT_MARKER_TOOL,\n state === \"error\" ? CHAT_MARKER_TOOL_ERROR_BORDER : CHAT_MARKER_TOOL_BORDER,\n className,\n )}\n {...rest}\n >\n <UiIcon name={state === \"error\" ? \"alert\" : \"sparkles\"} className={CHAT_MARKER_TOOL_ICON} />\n <div className={CHAT_MARKER_TOOL_BODY}>\n <span className={CHAT_MARKER_TOOL_TITLE}>{title}</span>\n {detail != null && <span className={CHAT_MARKER_TOOL_DETAIL}>{detail}</span>}\n {children}\n </div>\n {meta != null && (\n <span className={cn(CHAT_MARKER_TOOL_META, chatToolMetaClasses[state])}>{meta}\n </span>\n )}\n {state === \"done\" && <UiIcon name=\"check\" className={CHAT_MARKER_TOOL_CHECK} />}\n </div>\n );\n}\n\nexport interface ChatAttachmentProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n type?: \"file\" | \"image\";\n state?: \"ready\" | \"uploading\";\n name: string;\n /** \"PDF · 1.2 MB\"; while uploading, defaults to the percentage. */\n meta?: React.ReactNode;\n /** 0 to 100, while uploading. */\n progress?: number;\n /** Shows the remove button. */\n onRemove?: () => void;\n /** Default `\"Remove {name}\"`. */\n removeLabel?: string;\n}\n\n/** A card for a file in a message or in the composer. */\nexport function ChatAttachment({\n type = \"file\",\n state = \"ready\",\n name,\n meta,\n progress = 0,\n onRemove,\n removeLabel,\n className,\n ...props\n}: ChatAttachmentProps) {\n const pct = Math.min(100, Math.max(0, Math.round(progress)));\n return (\n <div\n data-slot=\"chat-attachment\"\n data-type={type}\n data-state={state}\n className={cn(CHAT_ATTACHMENT_BASE, className)}\n {...props}\n >\n <div className={CHAT_ATTACHMENT_ROW}>\n <span className={CHAT_ATTACHMENT_ICON}>\n <UiIcon name={type === \"image\" ? \"image\" : \"file\"} />\n </span>\n <div className={CHAT_ATTACHMENT_BODY}>\n <span className={CHAT_ATTACHMENT_NAME}>{name}</span>\n <span className={CHAT_ATTACHMENT_META}>{meta ?? (state === \"uploading\" ? `Uploading… ${pct}%` : null)}</span>\n </div>\n {onRemove && (\n <IconButton aria-label={removeLabel ?? `Remove ${name}`} size=\"sm\" onClick={onRemove}>\n <UiIcon name=\"x\" />\n </IconButton>\n )}\n </div>\n {state === \"uploading\" && (\n <div\n role=\"progressbar\"\n aria-label={name}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={pct}\n className={CHAT_ATTACHMENT_PROGRESS}\n >\n <div className={CHAT_ATTACHMENT_PROGRESS_FILL} style={{ width: `${pct}%` }} />\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["_type","className","children","rest","state"]}
@@ -0,0 +1,137 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { AsideHeader } from "./aside.js";
6
+ import { IconButton } from "./icon-button.js";
7
+ import { UiIcon } from "./ui-icons.js";
8
+ const PIN_SLACK = 24;
9
+ function ChatPanel({ children, empty, composer, streaming = false, labels = {}, className, ...props }) {
10
+ const scroller = React.useRef(null);
11
+ const pinned = React.useRef(true);
12
+ const [away, setAway] = React.useState(false);
13
+ const [scrolled, setScrolled] = React.useState(false);
14
+ const isEmpty = React.Children.toArray(children).length === 0 && empty != null;
15
+ const toEnd = () => {
16
+ const el = scroller.current;
17
+ if (el) el.scrollTop = el.scrollHeight;
18
+ };
19
+ React.useEffect(() => {
20
+ const el = scroller.current;
21
+ if (!el) return;
22
+ const follow = () => {
23
+ if (pinned.current) toEnd();
24
+ };
25
+ const mo = new MutationObserver(follow);
26
+ mo.observe(el, { childList: true, subtree: true, characterData: true });
27
+ const ro = typeof ResizeObserver === "function" ? new ResizeObserver(follow) : null;
28
+ ro?.observe(el);
29
+ toEnd();
30
+ return () => {
31
+ mo.disconnect();
32
+ ro?.disconnect();
33
+ };
34
+ }, [isEmpty]);
35
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "chat-panel", className: cn("flex min-h-0 flex-1 flex-col", className), ...props, children: [
36
+ /* @__PURE__ */ jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col", children: [
37
+ isEmpty ? /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col justify-center px-4", children: empty }) : /* @__PURE__ */ jsx(
38
+ "div",
39
+ {
40
+ ref: scroller,
41
+ role: "log",
42
+ "aria-live": "polite",
43
+ "aria-busy": streaming,
44
+ "aria-label": labels.log ?? "Conversation",
45
+ tabIndex: 0,
46
+ "data-slot": "chat-log",
47
+ "data-scrolled": scrolled ? "" : void 0,
48
+ className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
49
+ onScroll: (e) => {
50
+ const el = e.currentTarget;
51
+ const atEnd = el.scrollHeight - el.scrollTop - el.clientHeight <= PIN_SLACK;
52
+ pinned.current = atEnd;
53
+ setAway(!atEnd);
54
+ setScrolled(el.scrollTop > 0);
55
+ },
56
+ children
57
+ }
58
+ ),
59
+ !isEmpty && // Stays mounted so it can fade; hidden from the tree and the tab order until it matters.
60
+ /* @__PURE__ */ jsx(
61
+ IconButton,
62
+ {
63
+ variant: "outline",
64
+ "aria-label": labels.jump ?? "Jump to latest",
65
+ "data-chat-jump": away ? "visible" : "hidden",
66
+ "aria-hidden": !away,
67
+ tabIndex: away ? 0 : -1,
68
+ disabled: !away,
69
+ className: "absolute bottom-3 left-1/2 -translate-x-1/2",
70
+ onClick: () => {
71
+ pinned.current = true;
72
+ setAway(false);
73
+ toEnd();
74
+ scroller.current?.focus();
75
+ },
76
+ children: /* @__PURE__ */ jsx(UiIcon, { name: "chevron-down" })
77
+ }
78
+ )
79
+ ] }),
80
+ composer != null && /* @__PURE__ */ jsx("div", { className: "shrink-0 px-4 pb-4", children: composer })
81
+ ] });
82
+ }
83
+ function ChatDock({
84
+ title,
85
+ expanded,
86
+ defaultExpanded = true,
87
+ onExpandedChange,
88
+ actions,
89
+ prompt = "Ask the assistant\u2026",
90
+ children,
91
+ empty,
92
+ composer,
93
+ streaming = false,
94
+ labels = {},
95
+ className,
96
+ ...props
97
+ }) {
98
+ const [inner, setInner] = React.useState(defaultExpanded);
99
+ const open = expanded ?? inner;
100
+ const root = React.useRef(null);
101
+ const moved = React.useRef(false);
102
+ const set = (next) => {
103
+ moved.current = true;
104
+ setInner(next);
105
+ onExpandedChange?.(next);
106
+ };
107
+ React.useEffect(() => {
108
+ if (!moved.current) return;
109
+ moved.current = false;
110
+ const target = open ? root.current?.querySelector("textarea") ?? root.current?.querySelector("[role=log]") : root.current?.querySelector("button");
111
+ target?.focus();
112
+ }, [open]);
113
+ if (!open) {
114
+ return /* @__PURE__ */ jsx("section", { ref: root, "data-slot": "chat-dock", "data-state": "collapsed", className: cn("bg-card", className), ...props, children: /* @__PURE__ */ jsxs(
115
+ "button",
116
+ {
117
+ type: "button",
118
+ "aria-label": labels.open ?? "Open assistant",
119
+ onClick: () => set(true),
120
+ className: "flex h-11 w-full items-center gap-2 border-t border-border px-4 text-left text-body text-ink-soft outline-none hover:bg-hover focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
121
+ children: [
122
+ /* @__PURE__ */ jsx(UiIcon, { name: "sparkles" }),
123
+ prompt
124
+ ]
125
+ }
126
+ ) });
127
+ }
128
+ return /* @__PURE__ */ jsxs("section", { ref: root, "data-slot": "chat-dock", "data-state": "expanded", className: cn("flex h-full min-h-0 flex-col bg-card", className), ...props, children: [
129
+ /* @__PURE__ */ jsx(AsideHeader, { title, actions, onClose: () => set(false), labels: { close: labels.collapse ?? "Collapse" } }),
130
+ /* @__PURE__ */ jsx(ChatPanel, { empty, composer, streaming, labels, children })
131
+ ] });
132
+ }
133
+ export {
134
+ ChatDock,
135
+ ChatPanel
136
+ };
137
+ //# sourceMappingURL=chat-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/chat-panel.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { AsideHeader } from \"./aside\";\nimport { IconButton } from \"./icon-button\";\nimport { UiIcon } from \"./ui-icons\";\n\n// Within this many pixels of the end still counts as \"at the end\": sub-pixel\n// scroll positions and a growing last line would otherwise unpin the reader.\nconst PIN_SLACK = 24;\n\nexport interface ChatPanelLabels {\n /** The scroll-to-end button. Default `\"Jump to latest\"`. */\n jump?: string;\n /** Names the log region. Default `\"Conversation\"`. */\n log?: string;\n}\n\nexport interface ChatPanelProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** `ChatMessage`s and `ChatMarker`s, oldest first. */\n children?: React.ReactNode;\n /** Shown instead of the log while there is nothing to read. */\n empty?: React.ReactNode;\n /** The composer, under the log. */\n composer?: React.ReactNode;\n /**\n * The last turn is still arriving. The log is marked busy, so a screen reader\n * waits for the finished text instead of announcing every chunk.\n */\n streaming?: boolean;\n labels?: ChatPanelLabels;\n}\n\n/**\n * The conversation: a scrolling log that follows new content while the reader\n * is at the end, and lets go the moment they scroll up. The jump button brings\n * them back. The log is a polite live region, so new turns are announced\n * without taking focus.\n */\nexport function ChatPanel({ children, empty, composer, streaming = false, labels = {}, className, ...props }: ChatPanelProps) {\n const scroller = React.useRef<HTMLDivElement>(null);\n const pinned = React.useRef(true);\n const [away, setAway] = React.useState(false);\n const [scrolled, setScrolled] = React.useState(false);\n // `count` would call `false` a child, and `{list.length > 0 && …}` is common.\n const isEmpty = React.Children.toArray(children).length === 0 && empty != null;\n\n const toEnd = () => {\n const el = scroller.current;\n if (el) el.scrollTop = el.scrollHeight;\n };\n\n // Streaming grows the last node's text without adding nodes, so watch characters too.\n React.useEffect(() => {\n const el = scroller.current;\n if (!el) return;\n const follow = () => {\n if (pinned.current) toEnd();\n };\n const mo = new MutationObserver(follow);\n mo.observe(el, { childList: true, subtree: true, characterData: true });\n const ro = typeof ResizeObserver === \"function\" ? new ResizeObserver(follow) : null;\n ro?.observe(el);\n toEnd();\n return () => {\n mo.disconnect();\n ro?.disconnect();\n };\n }, [isEmpty]);\n\n return (\n <div data-slot=\"chat-panel\" className={cn(\"flex min-h-0 flex-1 flex-col\", className)} {...props}>\n <div className=\"relative flex min-h-0 flex-1 flex-col\">\n {isEmpty ? (\n <div className=\"flex flex-1 flex-col justify-center px-4\">{empty}</div>\n ) : (\n <div\n ref={scroller}\n role=\"log\"\n aria-live=\"polite\"\n aria-busy={streaming}\n aria-label={labels.log ?? \"Conversation\"}\n tabIndex={0}\n data-slot=\"chat-log\"\n data-scrolled={scrolled ? \"\" : undefined}\n className=\"flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\"\n onScroll={(e) => {\n const el = e.currentTarget;\n const atEnd = el.scrollHeight - el.scrollTop - el.clientHeight <= PIN_SLACK;\n pinned.current = atEnd;\n setAway(!atEnd);\n setScrolled(el.scrollTop > 0);\n }}\n >\n {children}\n </div>\n )}\n {!isEmpty && (\n // Stays mounted so it can fade; hidden from the tree and the tab order until it matters.\n <IconButton\n variant=\"outline\"\n aria-label={labels.jump ?? \"Jump to latest\"}\n data-chat-jump={away ? \"visible\" : \"hidden\"}\n aria-hidden={!away}\n tabIndex={away ? 0 : -1}\n disabled={!away}\n className=\"absolute bottom-3 left-1/2 -translate-x-1/2\"\n onClick={() => {\n pinned.current = true;\n setAway(false);\n toEnd();\n // The button unmounts with `away`; keep focus in the conversation.\n scroller.current?.focus();\n }}\n >\n <UiIcon name=\"chevron-down\" />\n </IconButton>\n )}\n </div>\n {composer != null && <div className=\"shrink-0 px-4 pb-4\">{composer}</div>}\n </div>\n );\n}\n\nexport interface ChatDockLabels extends ChatPanelLabels {\n /** The launcher bar's name when collapsed. Default `\"Open assistant\"`. */\n open?: string;\n /** Default `\"Collapse\"`. */\n collapse?: string;\n}\n\nexport interface ChatDockProps extends Omit<React.ComponentProps<\"section\">, \"title\" | \"children\"> {\n title: React.ReactNode;\n expanded?: boolean;\n defaultExpanded?: boolean;\n onExpandedChange?: (expanded: boolean) => void;\n /** Header buttons before collapse: history. */\n actions?: React.ReactNode;\n /** The launcher bar's text when collapsed. */\n prompt?: string;\n children?: React.ReactNode;\n empty?: React.ReactNode;\n composer?: React.ReactNode;\n streaming?: boolean;\n labels?: ChatDockLabels;\n}\n\n/** The assistant docked in a column: a launcher bar when collapsed, header + conversation + composer when open. */\nexport function ChatDock({\n title,\n expanded,\n defaultExpanded = true,\n onExpandedChange,\n actions,\n prompt = \"Ask the assistant…\",\n children,\n empty,\n composer,\n streaming = false,\n labels = {},\n className,\n ...props\n}: ChatDockProps) {\n const [inner, setInner] = React.useState(defaultExpanded);\n const open = expanded ?? inner;\n const root = React.useRef<HTMLElement>(null);\n const moved = React.useRef(false);\n const set = (next: boolean) => {\n moved.current = true;\n setInner(next);\n onExpandedChange?.(next);\n };\n // The button that was pressed unmounts with its state; hand focus to the same job in the other one.\n React.useEffect(() => {\n if (!moved.current) return;\n moved.current = false;\n const target = open\n ? (root.current?.querySelector<HTMLElement>(\"textarea\") ?? root.current?.querySelector<HTMLElement>(\"[role=log]\"))\n : root.current?.querySelector<HTMLElement>(\"button\");\n target?.focus();\n }, [open]);\n if (!open) {\n return (\n <section ref={root} data-slot=\"chat-dock\" data-state=\"collapsed\" className={cn(\"bg-card\", className)} {...props}>\n <button\n type=\"button\"\n aria-label={labels.open ?? \"Open assistant\"}\n onClick={() => set(true)}\n className=\"flex h-11 w-full items-center gap-2 border-t border-border px-4 text-left text-body text-ink-soft outline-none hover:bg-hover focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\"\n >\n <UiIcon name=\"sparkles\" />\n {prompt}\n </button>\n </section>\n );\n }\n return (\n <section ref={root} data-slot=\"chat-dock\" data-state=\"expanded\" className={cn(\"flex h-full min-h-0 flex-col bg-card\", className)} {...props}>\n <AsideHeader title={title} actions={actions} onClose={() => set(false)} labels={{ close: labels.collapse ?? \"Collapse\" }} />\n <ChatPanel empty={empty} composer={composer} streaming={streaming} labels={labels}>\n {children}\n </ChatPanel>\n </section>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ const STAGGER_STEP_MS = 28;
5
+ const STAGGER_CAP = 6;
6
+ const SETTLE_FALLBACK_MS = 900;
7
+ function split(text) {
8
+ const lead = /^\s*/.exec(text)?.[0] ?? "";
9
+ const units = [];
10
+ for (const m of text.slice(lead.length).matchAll(/(\S+)(\s*)/g)) units.push({ word: m[1] ?? "", gap: m[2] ?? "" });
11
+ return { lead, units };
12
+ }
13
+ function delaysFor(from, to) {
14
+ return Array.from({ length: to - from }, (_, j) => Math.min(j, STAGGER_CAP) * STAGGER_STEP_MS);
15
+ }
16
+ function ChatStreamText({ text, streaming = false, ...props }) {
17
+ const parsed = split(text);
18
+ const [progress, setProgress] = React.useState(() => ({
19
+ text,
20
+ settled: streaming ? 0 : parsed.units.length,
21
+ delays: delaysFor(0, parsed.units.length)
22
+ }));
23
+ let cur = progress;
24
+ if (text !== progress.text) {
25
+ const appended = text.startsWith(progress.text);
26
+ const known = appended ? split(progress.text).units.length : 0;
27
+ cur = {
28
+ text,
29
+ settled: appended ? progress.settled : 0,
30
+ delays: [...appended ? progress.delays.slice(0, known) : [], ...delaysFor(known, parsed.units.length)]
31
+ };
32
+ setProgress(cur);
33
+ }
34
+ const settle = React.useCallback((upTo) => {
35
+ setProgress((p) => upTo > p.settled ? { ...p, settled: upTo } : p);
36
+ }, []);
37
+ const total = parsed.units.length;
38
+ React.useEffect(() => {
39
+ if (streaming) return;
40
+ const id = setTimeout(() => settle(total), SETTLE_FALLBACK_MS);
41
+ return () => clearTimeout(id);
42
+ }, [streaming, total, settle]);
43
+ const settledText = parsed.lead + parsed.units.slice(0, cur.settled).map((u) => u.word + u.gap).join("");
44
+ return /* @__PURE__ */ jsxs(
45
+ "span",
46
+ {
47
+ "data-slot": "chat-stream-text",
48
+ onAnimationEnd: (e) => {
49
+ const i = Number(e.target.dataset["word"]);
50
+ if (Number.isInteger(i)) settle(i + 1);
51
+ },
52
+ ...props,
53
+ children: [
54
+ settledText,
55
+ parsed.units.slice(cur.settled).map((u, k) => {
56
+ const i = cur.settled + k;
57
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
58
+ /* @__PURE__ */ jsx("span", { "data-slot": "chat-word", "data-word": i, style: { animationDelay: `${cur.delays[i] ?? 0}ms` }, children: u.word }),
59
+ u.gap
60
+ ] }, i);
61
+ })
62
+ ]
63
+ }
64
+ );
65
+ }
66
+ export {
67
+ ChatStreamText
68
+ };
69
+ //# sourceMappingURL=chat-stream-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/chat-stream-text.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n// Words in one chunk start this far apart, up to STAGGER_CAP of them, so a burst\n// of text reads as a ripple instead of landing at once.\nconst STAGGER_STEP_MS = 28;\nconst STAGGER_CAP = 6;\n// Safety net for when `animationend` never fires (hidden tab, display: none).\nconst SETTLE_FALLBACK_MS = 900;\n\ninterface Unit {\n word: string;\n /** The whitespace that follows the word, kept outside the animated span. */\n gap: string;\n}\n\nfunction split(text: string): { lead: string; units: Unit[] } {\n const lead = /^\\s*/.exec(text)?.[0] ?? \"\";\n const units: Unit[] = [];\n for (const m of text.slice(lead.length).matchAll(/(\\S+)(\\s*)/g)) units.push({ word: m[1] ?? \"\", gap: m[2] ?? \"\" });\n return { lead, units };\n}\n\ninterface Progress {\n text: string;\n /** Units before this index are plain text; the rest are animating. */\n settled: number;\n /** Start delay per unit, fixed when the unit appears so re-renders never restart it. */\n delays: number[];\n}\n\nfunction delaysFor(from: number, to: number): number[] {\n return Array.from({ length: to - from }, (_, j) => Math.min(j, STAGGER_CAP) * STAGGER_STEP_MS);\n}\n\nexport interface ChatStreamTextProps extends Omit<React.ComponentProps<\"span\">, \"children\"> {\n /** The text so far. Pass the whole growing string; only the appended tail animates. */\n text: string;\n /**\n * The text is still growing. A string that is already complete when this\n * mounts as `false` shows at once, with no entrance.\n */\n streaming?: boolean;\n}\n\n/**\n * Text that arrives word by word. New words fade in from a soft blur and settle\n * (motion.css); words already on screen are never touched again. Once a word has\n * landed it folds into one plain text node, so a long reply costs a handful of\n * elements, not thousands. The text content stays exactly `text`.\n */\nexport function ChatStreamText({ text, streaming = false, ...props }: ChatStreamTextProps) {\n const parsed = split(text);\n const [progress, setProgress] = React.useState<Progress>(() => ({\n text,\n settled: streaming ? 0 : parsed.units.length,\n delays: delaysFor(0, parsed.units.length),\n }));\n\n let cur = progress;\n if (text !== progress.text) {\n // Anything but an append (edit, regenerate) starts over rather than patching words in place.\n const appended = text.startsWith(progress.text);\n const known = appended ? split(progress.text).units.length : 0;\n cur = {\n text,\n settled: appended ? progress.settled : 0,\n delays: [...(appended ? progress.delays.slice(0, known) : []), ...delaysFor(known, parsed.units.length)],\n };\n setProgress(cur);\n }\n\n const settle = React.useCallback((upTo: number) => {\n setProgress((p) => (upTo > p.settled ? { ...p, settled: upTo } : p));\n }, []);\n\n const total = parsed.units.length;\n React.useEffect(() => {\n if (streaming) return;\n const id = setTimeout(() => settle(total), SETTLE_FALLBACK_MS);\n return () => clearTimeout(id);\n }, [streaming, total, settle]);\n\n const settledText = parsed.lead + parsed.units.slice(0, cur.settled).map((u) => u.word + u.gap).join(\"\");\n return (\n <span\n data-slot=\"chat-stream-text\"\n onAnimationEnd={(e) => {\n const i = Number((e.target as HTMLElement).dataset[\"word\"]);\n if (Number.isInteger(i)) settle(i + 1);\n }}\n {...props}\n >\n {settledText}\n {parsed.units.slice(cur.settled).map((u, k) => {\n const i = cur.settled + k;\n return (\n <React.Fragment key={i}>\n <span data-slot=\"chat-word\" data-word={i} style={{ animationDelay: `${cur.delays[i] ?? 0}ms` }}>\n {u.word}\n </span>\n {u.gap}\n </React.Fragment>\n );\n })}\n </span>\n );\n}\n"],"mappings":";AAkGU,SACE,KADF;AAhGV,YAAY,WAAW;AAIvB,MAAM,kBAAkB;AACxB,MAAM,cAAc;AAEpB,MAAM,qBAAqB;AAQ3B,SAAS,MAAM,MAA+C;AAC5D,QAAM,OAAO,OAAO,KAAK,IAAI,IAAI,CAAC,KAAK;AACvC,QAAM,QAAgB,CAAC;AACvB,aAAW,KAAK,KAAK,MAAM,KAAK,MAAM,EAAE,SAAS,aAAa,EAAG,OAAM,KAAK,EAAE,MAAM,EAAE,CAAC,KAAK,IAAI,KAAK,EAAE,CAAC,KAAK,GAAG,CAAC;AACjH,SAAO,EAAE,MAAM,MAAM;AACvB;AAUA,SAAS,UAAU,MAAc,IAAsB;AACrD,SAAO,MAAM,KAAK,EAAE,QAAQ,KAAK,KAAK,GAAG,CAAC,GAAG,MAAM,KAAK,IAAI,GAAG,WAAW,IAAI,eAAe;AAC/F;AAkBO,SAAS,eAAe,EAAE,MAAM,YAAY,OAAO,GAAG,MAAM,GAAwB;AACzF,QAAM,SAAS,MAAM,IAAI;AACzB,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAmB,OAAO;AAAA,IAC9D;AAAA,IACA,SAAS,YAAY,IAAI,OAAO,MAAM;AAAA,IACtC,QAAQ,UAAU,GAAG,OAAO,MAAM,MAAM;AAAA,EAC1C,EAAE;AAEF,MAAI,MAAM;AACV,MAAI,SAAS,SAAS,MAAM;AAE1B,UAAM,WAAW,KAAK,WAAW,SAAS,IAAI;AAC9C,UAAM,QAAQ,WAAW,MAAM,SAAS,IAAI,EAAE,MAAM,SAAS;AAC7D,UAAM;AAAA,MACJ;AAAA,MACA,SAAS,WAAW,SAAS,UAAU;AAAA,MACvC,QAAQ,CAAC,GAAI,WAAW,SAAS,OAAO,MAAM,GAAG,KAAK,IAAI,CAAC,GAAI,GAAG,UAAU,OAAO,OAAO,MAAM,MAAM,CAAC;AAAA,IACzG;AACA,gBAAY,GAAG;AAAA,EACjB;AAEA,QAAM,SAAS,MAAM,YAAY,CAAC,SAAiB;AACjD,gBAAY,CAAC,MAAO,OAAO,EAAE,UAAU,EAAE,GAAG,GAAG,SAAS,KAAK,IAAI,CAAE;AAAA,EACrE,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,OAAO,MAAM;AAC3B,QAAM,UAAU,MAAM;AACpB,QAAI,UAAW;AACf,UAAM,KAAK,WAAW,MAAM,OAAO,KAAK,GAAG,kBAAkB;AAC7D,WAAO,MAAM,aAAa,EAAE;AAAA,EAC9B,GAAG,CAAC,WAAW,OAAO,MAAM,CAAC;AAE7B,QAAM,cAAc,OAAO,OAAO,OAAO,MAAM,MAAM,GAAG,IAAI,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE;AACvG,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAgB,CAAC,MAAM;AACrB,cAAM,IAAI,OAAQ,EAAE,OAAuB,QAAQ,MAAM,CAAC;AAC1D,YAAI,OAAO,UAAU,CAAC,EAAG,QAAO,IAAI,CAAC;AAAA,MACvC;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,QACA,OAAO,MAAM,MAAM,IAAI,OAAO,EAAE,IAAI,CAAC,GAAG,MAAM;AAC7C,gBAAM,IAAI,IAAI,UAAU;AACxB,iBACE,qBAAC,MAAM,UAAN,EACC;AAAA,gCAAC,UAAK,aAAU,aAAY,aAAW,GAAG,OAAO,EAAE,gBAAgB,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,GAC1F,YAAE,MACL;AAAA,YACC,EAAE;AAAA,eAJgB,CAKrB;AAAA,QAEJ,CAAC;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}