@lovett/ui 0.0.10 → 0.1.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 (158) hide show
  1. package/dist/chunk-RBYWGBQ2.js +2752 -0
  2. package/dist/chunk-RBYWGBQ2.js.map +1 -0
  3. package/dist/index.d.ts +6064 -80
  4. package/dist/index.js +21561 -9696
  5. package/dist/index.js.map +1 -1
  6. package/dist/rich-composer-impl-5NO443A6.js +1859 -0
  7. package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
  8. package/dist/styles.css +1586 -2
  9. package/dist/theme-v2.css +228 -0
  10. package/dist/tokens.css +121 -0
  11. package/package.json +8 -1
  12. package/src/__tests__/anchor.test.tsx +422 -0
  13. package/src/__tests__/avatar.test.tsx +272 -0
  14. package/src/__tests__/bar-chart.test.tsx +809 -0
  15. package/src/__tests__/board.test.tsx +420 -0
  16. package/src/__tests__/chart-math.test.ts +922 -0
  17. package/src/__tests__/chart-series.test.ts +339 -0
  18. package/src/__tests__/code-block.test.tsx +134 -0
  19. package/src/__tests__/combobox.test.tsx +677 -0
  20. package/src/__tests__/display-popover.test.tsx +195 -0
  21. package/src/__tests__/display-store.test.tsx +208 -0
  22. package/src/__tests__/donut-chart.test.tsx +397 -0
  23. package/src/__tests__/dropdown-menu.test.tsx +572 -0
  24. package/src/__tests__/filter-menu.test.tsx +175 -0
  25. package/src/__tests__/gauge-ring.test.tsx +233 -0
  26. package/src/__tests__/helpers/geometry.ts +58 -0
  27. package/src/__tests__/layer-stack.test.tsx +228 -0
  28. package/src/__tests__/line-chart.test.tsx +612 -0
  29. package/src/__tests__/popover.test.tsx +460 -0
  30. package/src/__tests__/ranked-bars.test.tsx +343 -0
  31. package/src/__tests__/remark-underline.test.ts +194 -0
  32. package/src/__tests__/select.test.tsx +543 -0
  33. package/src/__tests__/sparkline.test.tsx +368 -0
  34. package/src/__tests__/stat-layer.test.tsx +271 -0
  35. package/src/__tests__/stat-strip.test.tsx +175 -0
  36. package/src/__tests__/status.test.tsx +293 -0
  37. package/src/__tests__/tabs.test.tsx +303 -0
  38. package/src/__tests__/token-shape.test.ts +132 -2
  39. package/src/__tests__/tooltip.test.tsx +355 -0
  40. package/src/avatar.tsx +352 -0
  41. package/src/bar-chart.tsx +1214 -0
  42. package/src/board.tsx +658 -0
  43. package/src/calculator-shell-v2.tsx +19 -39
  44. package/src/chart-frame.tsx +960 -0
  45. package/src/chart-legend.tsx +304 -0
  46. package/src/chart-tooltip.tsx +267 -0
  47. package/src/code-block.tsx +62 -8
  48. package/src/combobox.tsx +796 -0
  49. package/src/delta-chip.tsx +263 -0
  50. package/src/detail/__tests__/activity-pane.test.tsx +186 -0
  51. package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
  52. package/src/detail/__tests__/detail-surface.test.tsx +529 -0
  53. package/src/detail/__tests__/field-row.test.tsx +357 -0
  54. package/src/detail/activity-pane.tsx +507 -0
  55. package/src/detail/calendar.tsx +355 -0
  56. package/src/detail/detail-divider.tsx +261 -0
  57. package/src/detail/detail-header.tsx +287 -0
  58. package/src/detail/detail-menu.tsx +254 -0
  59. package/src/detail/detail-surface.tsx +1110 -0
  60. package/src/detail/field-list.tsx +196 -0
  61. package/src/detail/field-row.tsx +1131 -0
  62. package/src/detail/index.ts +58 -0
  63. package/src/detail/segmented-choice.tsx +94 -0
  64. package/src/detail/types.ts +129 -0
  65. package/src/display-popover.tsx +487 -0
  66. package/src/display-store.tsx +242 -0
  67. package/src/donut-chart.tsx +988 -0
  68. package/src/dropdown-menu.tsx +412 -151
  69. package/src/filter-core/EXPORTS.md +85 -0
  70. package/src/filter-core/__tests__/columns.test.ts +159 -0
  71. package/src/filter-core/__tests__/faceting.test.ts +193 -0
  72. package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
  73. package/src/filter-core/__tests__/operators.test.ts +235 -0
  74. package/src/filter-core/__tests__/state.test.ts +268 -0
  75. package/src/filter-core/__tests__/url.test.ts +350 -0
  76. package/src/filter-core/columns.ts +134 -0
  77. package/src/filter-core/date-utils.ts +38 -0
  78. package/src/filter-core/examples/task-filter-columns.ts +121 -0
  79. package/src/filter-core/faceting.ts +120 -0
  80. package/src/filter-core/filter-fns.ts +335 -0
  81. package/src/filter-core/index.ts +105 -0
  82. package/src/filter-core/operators.ts +433 -0
  83. package/src/filter-core/state.ts +280 -0
  84. package/src/filter-core/types.ts +247 -0
  85. package/src/filter-core/url.ts +261 -0
  86. package/src/filter-dropdown.tsx +12 -0
  87. package/src/filter-menu.tsx +649 -0
  88. package/src/floating-drawer.tsx +19 -1
  89. package/src/gauge-ring.tsx +435 -0
  90. package/src/hue.ts +52 -0
  91. package/src/index.ts +402 -0
  92. package/src/kbd.tsx +27 -4
  93. package/src/lib/anchor.ts +427 -0
  94. package/src/lib/chart.ts +866 -0
  95. package/src/lib/focus.ts +74 -0
  96. package/src/lib/layer-stack.ts +215 -0
  97. package/src/lib/refs.ts +31 -0
  98. package/src/lib/remark-underline.ts +443 -0
  99. package/src/lib/series.ts +169 -0
  100. package/src/line-chart.tsx +1176 -0
  101. package/src/markdown.tsx +26 -7
  102. package/src/metric-card.tsx +57 -22
  103. package/src/modal.tsx +73 -66
  104. package/src/popover.tsx +407 -0
  105. package/src/progress-ledger.tsx +304 -0
  106. package/src/ranked-bars.tsx +386 -0
  107. package/src/segmented-pill.tsx +32 -9
  108. package/src/select.tsx +646 -0
  109. package/src/sortable.tsx +315 -1
  110. package/src/sparkline.tsx +416 -0
  111. package/src/stat-card.tsx +376 -0
  112. package/src/stat-row.tsx +108 -70
  113. package/src/stat-strip.tsx +327 -0
  114. package/src/status.tsx +215 -0
  115. package/src/styles.css +1586 -2
  116. package/src/tabs.tsx +206 -25
  117. package/src/task-card.tsx +610 -0
  118. package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
  119. package/src/thread/__tests__/comment-tree.test.ts +151 -0
  120. package/src/thread/__tests__/emoji.test.ts +187 -0
  121. package/src/thread/__tests__/fixtures/thread-fixture.ts +235 -0
  122. package/src/thread/__tests__/link-preview-source.test.ts +120 -0
  123. package/src/thread/__tests__/link-preview.test.tsx +600 -0
  124. package/src/thread/__tests__/markdown-format.test.ts +82 -0
  125. package/src/thread/__tests__/markdown-spec.test.ts +469 -0
  126. package/src/thread/__tests__/relative-time.test.ts +71 -0
  127. package/src/thread/__tests__/rich-composer.test.tsx +799 -0
  128. package/src/thread/__tests__/scroll-caret.test.ts +58 -0
  129. package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
  130. package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
  131. package/src/thread/__tests__/thread.test.tsx +662 -0
  132. package/src/thread/__tests__/use-attachments.test.tsx +679 -0
  133. package/src/thread/actions.tsx +196 -0
  134. package/src/thread/attachments.tsx +1071 -0
  135. package/src/thread/comment-body.tsx +148 -0
  136. package/src/thread/comment-tree.ts +182 -0
  137. package/src/thread/comment.tsx +915 -0
  138. package/src/thread/composer-footer.tsx +125 -0
  139. package/src/thread/composer.tsx +319 -0
  140. package/src/thread/emoji.ts +283 -0
  141. package/src/thread/index.ts +153 -0
  142. package/src/thread/link-preview.tsx +341 -0
  143. package/src/thread/markdown-format.ts +155 -0
  144. package/src/thread/markdown-spec.ts +754 -0
  145. package/src/thread/rail.tsx +372 -0
  146. package/src/thread/reactions.tsx +415 -0
  147. package/src/thread/relative-time.tsx +94 -0
  148. package/src/thread/rich-composer-impl.tsx +1601 -0
  149. package/src/thread/rich-composer.tsx +195 -0
  150. package/src/thread/scroll-caret.ts +37 -0
  151. package/src/thread/suggestion-list.tsx +182 -0
  152. package/src/thread/thread.tsx +718 -0
  153. package/src/thread/types.ts +218 -0
  154. package/src/thread/use-attachments.ts +598 -0
  155. package/src/thread/use-now.ts +73 -0
  156. package/src/thread/use-thread.ts +316 -0
  157. package/src/tokens.css +121 -0
  158. package/src/tooltip.tsx +297 -0
@@ -0,0 +1,2752 @@
1
+ import { clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import { forwardRef, useState, createContext, isValidElement, cloneElement, useRef, useEffect, useMemo, lazy, useContext, useCallback, useLayoutEffect, useId, Suspense, Component } from 'react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { EyeOff, Eye, Check, Copy, PencilLine, Type, Bold, Italic, Code, Link, List, CornerDownRight, X, Image, FileText, Paperclip, Search, Play, ImageOff, Info } from 'lucide-react';
6
+ import { createPortal } from 'react-dom';
7
+ import { PrismLight } from 'react-syntax-highlighter';
8
+ import { oneLight, vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
9
+ import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
10
+ import css from 'react-syntax-highlighter/dist/esm/languages/prism/css';
11
+ import diff from 'react-syntax-highlighter/dist/esm/languages/prism/diff';
12
+ import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript';
13
+ import json from 'react-syntax-highlighter/dist/esm/languages/prism/json';
14
+ import jsx3 from 'react-syntax-highlighter/dist/esm/languages/prism/jsx';
15
+ import markdown from 'react-syntax-highlighter/dist/esm/languages/prism/markdown';
16
+ import markup from 'react-syntax-highlighter/dist/esm/languages/prism/markup';
17
+ import python from 'react-syntax-highlighter/dist/esm/languages/prism/python';
18
+ import sql from 'react-syntax-highlighter/dist/esm/languages/prism/sql';
19
+ import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx';
20
+ import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript';
21
+ import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml';
22
+ import ReactMarkdown, { defaultUrlTransform } from 'react-markdown';
23
+ import remarkGfm from 'remark-gfm';
24
+
25
+ var __defProp = Object.defineProperty;
26
+ var __export = (target, all) => {
27
+ for (var name in all)
28
+ __defProp(target, name, { get: all[name], enumerable: true });
29
+ };
30
+ function cn(...inputs) {
31
+ return twMerge(clsx(inputs));
32
+ }
33
+ var VARIANT_CLASS = {
34
+ primary: "btn-primary",
35
+ secondary: "btn-secondary",
36
+ outline: "btn-outline",
37
+ ghost: "btn-ghost",
38
+ destructive: "btn-destructive",
39
+ "destructive-soft": "btn-destructive-soft",
40
+ icon: "btn-icon",
41
+ toolbar: "btn-toolbar"
42
+ };
43
+ var SIZE_CLASS = {
44
+ sm: "size-sm",
45
+ md: "",
46
+ lg: "size-lg"
47
+ };
48
+ var Button = forwardRef(function Button2({
49
+ variant = "primary",
50
+ size = "md",
51
+ shape = "default",
52
+ loading,
53
+ leadingIcon,
54
+ trailingIcon,
55
+ disabled,
56
+ className,
57
+ children,
58
+ type = "button",
59
+ ...rest
60
+ }, ref) {
61
+ return /* @__PURE__ */ jsxs(
62
+ "button",
63
+ {
64
+ ref,
65
+ type,
66
+ disabled: disabled || loading,
67
+ className: cn(
68
+ "btn",
69
+ VARIANT_CLASS[variant],
70
+ SIZE_CLASS[size],
71
+ shape === "circle" && variant === "icon" && "is-circle",
72
+ loading && "is-loading",
73
+ className
74
+ ),
75
+ ...rest,
76
+ children: [
77
+ leadingIcon,
78
+ children,
79
+ trailingIcon
80
+ ]
81
+ }
82
+ );
83
+ });
84
+ var SIZE_CLASS2 = {
85
+ sm: "size-sm",
86
+ md: "",
87
+ lg: "size-lg"
88
+ };
89
+ var Input = forwardRef(function Input2({
90
+ inputSize = "md",
91
+ error,
92
+ leadingAffix,
93
+ trailingAffix,
94
+ shellClassName,
95
+ className,
96
+ disabled,
97
+ type,
98
+ showPasswordToggle,
99
+ numeric,
100
+ ...rest
101
+ }, ref) {
102
+ const [revealed, setRevealed] = useState(false);
103
+ const isPasswordToggle = showPasswordToggle === true && type === "password" && !trailingAffix;
104
+ const resolvedType = isPasswordToggle && revealed ? "text" : type;
105
+ const resolvedTrailingAffix = isPasswordToggle ? /* @__PURE__ */ jsx(
106
+ "button",
107
+ {
108
+ type: "button",
109
+ onClick: () => setRevealed((v) => !v),
110
+ "aria-label": revealed ? "Hide password" : "Show password",
111
+ "aria-pressed": revealed,
112
+ className: "affix action",
113
+ tabIndex: -1,
114
+ children: revealed ? /* @__PURE__ */ jsx(EyeOff, { className: "h-4 w-4", "aria-hidden": true }) : /* @__PURE__ */ jsx(Eye, { className: "h-4 w-4", "aria-hidden": true })
115
+ }
116
+ ) : trailingAffix;
117
+ return /* @__PURE__ */ jsxs(
118
+ "div",
119
+ {
120
+ className: cn(
121
+ "input-shell",
122
+ SIZE_CLASS2[inputSize],
123
+ numeric && "is-numeric",
124
+ leadingAffix && "has-leading",
125
+ resolvedTrailingAffix && "has-trailing",
126
+ error && "is-error",
127
+ disabled && "is-disabled",
128
+ shellClassName
129
+ ),
130
+ children: [
131
+ leadingAffix && /* @__PURE__ */ jsx("span", { className: "affix", children: leadingAffix }),
132
+ /* @__PURE__ */ jsx(
133
+ "input",
134
+ {
135
+ ref,
136
+ type: resolvedType,
137
+ disabled,
138
+ className,
139
+ ...rest
140
+ }
141
+ ),
142
+ resolvedTrailingAffix && /* @__PURE__ */ jsx("span", { className: "affix", children: resolvedTrailingAffix })
143
+ ]
144
+ }
145
+ );
146
+ });
147
+ var stack = [];
148
+ var listening = false;
149
+ function onDocumentKeyDown(event) {
150
+ if (event.key !== "Escape") return;
151
+ const top = stack[stack.length - 1];
152
+ if (!top) return;
153
+ event.stopPropagation();
154
+ top.onEscape(event);
155
+ }
156
+ function pushLayer(record) {
157
+ stack.push(record);
158
+ if (!listening) {
159
+ document.addEventListener("keydown", onDocumentKeyDown);
160
+ listening = true;
161
+ }
162
+ return () => {
163
+ const at = stack.lastIndexOf(record);
164
+ if (at !== -1) stack.splice(at, 1);
165
+ if (stack.length === 0 && listening) {
166
+ document.removeEventListener("keydown", onDocumentKeyDown);
167
+ listening = false;
168
+ }
169
+ };
170
+ }
171
+ function useLayer(options) {
172
+ const { enabled, kind, elementRef, onEscape } = options;
173
+ const onEscapeRef = useRef(onEscape);
174
+ useEffect(() => {
175
+ onEscapeRef.current = onEscape;
176
+ });
177
+ const kindRef = useRef(kind);
178
+ kindRef.current = kind;
179
+ const recordRef = useRef(null);
180
+ useEffect(() => {
181
+ if (!enabled) return;
182
+ const record = {
183
+ kind: kindRef.current,
184
+ element: () => elementRef?.current ?? null,
185
+ onEscape: (event) => onEscapeRef.current(event)
186
+ };
187
+ recordRef.current = record;
188
+ const pop = pushLayer(record);
189
+ return () => {
190
+ pop();
191
+ if (recordRef.current === record) recordRef.current = null;
192
+ };
193
+ }, [enabled, elementRef]);
194
+ useEffect(() => {
195
+ const record = recordRef.current;
196
+ if (record !== null) record.kind = kind;
197
+ }, [kind]);
198
+ return useMemo(
199
+ () => ({
200
+ isTop: () => {
201
+ const record = recordRef.current;
202
+ return record !== null && stack[stack.length - 1] === record;
203
+ },
204
+ isTopOfKind: () => {
205
+ const record = recordRef.current;
206
+ if (!record) return false;
207
+ for (let i = stack.length - 1; i >= 0; i--) {
208
+ const layer = stack[i];
209
+ if (layer === record) return true;
210
+ if (layer?.kind === record.kind) return false;
211
+ }
212
+ return false;
213
+ },
214
+ containsInLayerAbove: (node) => {
215
+ const record = recordRef.current;
216
+ if (!record) return false;
217
+ const at = stack.indexOf(record);
218
+ if (at === -1) return false;
219
+ for (let i = at + 1; i < stack.length; i++) {
220
+ if (stack[i]?.element()?.contains(node)) return true;
221
+ }
222
+ return false;
223
+ }
224
+ }),
225
+ []
226
+ );
227
+ }
228
+ function useEscapeKey(onEscape, options = {}) {
229
+ const { enabled = true, kind = "popover" } = options;
230
+ return useLayer({ enabled, kind, onEscape });
231
+ }
232
+ var OPPOSITE = {
233
+ top: "bottom",
234
+ bottom: "top",
235
+ left: "right",
236
+ right: "left"
237
+ };
238
+ function isVertical(side) {
239
+ return side === "top" || side === "bottom";
240
+ }
241
+ function clampNumber(value, min, max) {
242
+ return Math.max(min, Math.min(value, max));
243
+ }
244
+ function computeAnchoredPosition(anchor, floating, viewport, options = {}) {
245
+ const {
246
+ side: preferred = "bottom",
247
+ align = "start",
248
+ offset = 6,
249
+ alignOffset = 0,
250
+ flip = true,
251
+ clampToViewport = true,
252
+ gutter = 8
253
+ } = options;
254
+ const anchorRight = anchor.left + anchor.width;
255
+ const anchorBottom = anchor.top + anchor.height;
256
+ const room = {
257
+ top: anchor.top - gutter,
258
+ bottom: viewport.height - anchorBottom - gutter,
259
+ left: anchor.left - gutter,
260
+ right: viewport.width - anchorRight - gutter
261
+ };
262
+ const needed = (side2) => (isVertical(side2) ? floating.height : floating.width) + offset;
263
+ let side = preferred;
264
+ if (flip && room[preferred] < needed(preferred)) {
265
+ const opposite = OPPOSITE[preferred];
266
+ if (room[opposite] >= needed(opposite) || room[opposite] > room[preferred]) {
267
+ side = opposite;
268
+ }
269
+ }
270
+ let left;
271
+ let top;
272
+ if (isVertical(side)) {
273
+ top = side === "bottom" ? anchorBottom + offset : anchor.top - floating.height - offset;
274
+ switch (align) {
275
+ case "start":
276
+ left = anchor.left + alignOffset;
277
+ break;
278
+ case "center":
279
+ left = anchor.left + anchor.width / 2 - floating.width / 2 + alignOffset;
280
+ break;
281
+ case "end":
282
+ left = anchorRight - floating.width - alignOffset;
283
+ break;
284
+ }
285
+ } else {
286
+ left = side === "right" ? anchorRight + offset : anchor.left - floating.width - offset;
287
+ switch (align) {
288
+ case "start":
289
+ top = anchor.top + alignOffset;
290
+ break;
291
+ case "center":
292
+ top = anchor.top + anchor.height / 2 - floating.height / 2 + alignOffset;
293
+ break;
294
+ case "end":
295
+ top = anchorBottom - floating.height - alignOffset;
296
+ break;
297
+ }
298
+ }
299
+ if (clampToViewport) {
300
+ left = clampNumber(left, gutter, viewport.width - floating.width - gutter);
301
+ top = clampNumber(top, gutter, viewport.height - floating.height - gutter);
302
+ }
303
+ return {
304
+ left: Math.round(left),
305
+ top: Math.round(top),
306
+ placement: { side, align }
307
+ };
308
+ }
309
+ var PARKED_STYLE = {
310
+ position: "fixed",
311
+ // Parked far off-screen, not at (0, 0): if a renderer paints the
312
+ // unpositioned frame despite `visibility: hidden`, nothing flashes at the
313
+ // viewport's top-left corner.
314
+ left: -99999,
315
+ top: -99999,
316
+ visibility: "hidden",
317
+ opacity: 0,
318
+ pointerEvents: "none"
319
+ };
320
+ function viewportSize() {
321
+ const root = document.documentElement;
322
+ return {
323
+ width: root.clientWidth || window.innerWidth,
324
+ height: root.clientHeight || window.innerHeight
325
+ };
326
+ }
327
+ function samePosition(a, b) {
328
+ return a !== null && a.left === b.left && a.top === b.top && a.placement.side === b.placement.side && a.placement.align === b.placement.align;
329
+ }
330
+ function useAnchoredPosition(options) {
331
+ const {
332
+ anchorRef,
333
+ anchorRect = null,
334
+ enabled = true,
335
+ side = "bottom",
336
+ align = "start",
337
+ offset = 6,
338
+ alignOffset = 0,
339
+ flip = true,
340
+ clampToViewport = true,
341
+ gutter = 8
342
+ } = options;
343
+ const floatingRef = useRef(null);
344
+ const [floating, setFloating] = useState(null);
345
+ const ref = useCallback((node) => {
346
+ floatingRef.current = node;
347
+ setFloating(node);
348
+ }, []);
349
+ const [position, setPosition] = useState(null);
350
+ const anchorRectRef = useRef(anchorRect);
351
+ useLayoutEffect(() => {
352
+ anchorRectRef.current = anchorRect;
353
+ });
354
+ const rectKey = anchorRect ? `${anchorRect.left}|${anchorRect.top}|${anchorRect.width}|${anchorRect.height}` : "";
355
+ const measure = useCallback(() => {
356
+ const node = floatingRef.current;
357
+ if (!node) return;
358
+ const rect = anchorRectRef.current ?? anchorRef?.current?.getBoundingClientRect() ?? null;
359
+ if (!rect) return;
360
+ const next = computeAnchoredPosition(
361
+ rect,
362
+ { width: node.offsetWidth, height: node.offsetHeight },
363
+ viewportSize(),
364
+ { side, align, offset, alignOffset, flip, clampToViewport, gutter }
365
+ );
366
+ setPosition((prev) => samePosition(prev, next) ? prev : next);
367
+ }, [anchorRef, side, align, offset, alignOffset, flip, clampToViewport, gutter]);
368
+ useLayoutEffect(() => {
369
+ if (!enabled || !floating) {
370
+ setPosition((prev) => prev === null ? prev : null);
371
+ return;
372
+ }
373
+ measure();
374
+ window.addEventListener("resize", measure);
375
+ window.addEventListener("scroll", measure, true);
376
+ let observer = null;
377
+ if (typeof ResizeObserver !== "undefined") {
378
+ observer = new ResizeObserver(() => measure());
379
+ observer.observe(floating);
380
+ const anchorEl = anchorRef?.current;
381
+ if (anchorEl) observer.observe(anchorEl);
382
+ }
383
+ return () => {
384
+ window.removeEventListener("resize", measure);
385
+ window.removeEventListener("scroll", measure, true);
386
+ observer?.disconnect();
387
+ };
388
+ }, [enabled, floating, measure, anchorRef, rectKey]);
389
+ const positioned = position !== null;
390
+ const style = positioned ? { position: "fixed", left: position.left, top: position.top } : PARKED_STYLE;
391
+ return {
392
+ ref,
393
+ floatingRef,
394
+ style,
395
+ placement: position?.placement ?? null,
396
+ positioned,
397
+ update: measure
398
+ };
399
+ }
400
+ function useOutsideClick(targets, onOutside, options = {}) {
401
+ const { enabled = true, layer } = options;
402
+ const targetsRef = useRef(targets);
403
+ const handlerRef = useRef(onOutside);
404
+ useEffect(() => {
405
+ targetsRef.current = targets;
406
+ handlerRef.current = onOutside;
407
+ });
408
+ useEffect(() => {
409
+ if (!enabled) return;
410
+ const onPointerDown = (event) => {
411
+ const target = event.target;
412
+ if (!(target instanceof Node)) return;
413
+ for (const candidate of targetsRef.current) {
414
+ const el = candidate instanceof Node ? candidate : candidate?.current;
415
+ if (el?.contains(target)) return;
416
+ }
417
+ if (layer?.containsInLayerAbove(target)) return;
418
+ handlerRef.current(event);
419
+ };
420
+ document.addEventListener("pointerdown", onPointerDown);
421
+ return () => document.removeEventListener("pointerdown", onPointerDown);
422
+ }, [enabled, layer]);
423
+ }
424
+
425
+ // src/lib/refs.ts
426
+ function assignRef(ref, node) {
427
+ if (!ref) return;
428
+ if (typeof ref === "function") {
429
+ ref(node);
430
+ return;
431
+ }
432
+ ref.current = node;
433
+ }
434
+ function composeRefs(...refs) {
435
+ return (node) => {
436
+ for (const ref of refs) assignRef(ref, node);
437
+ };
438
+ }
439
+
440
+ // src/lib/focus.ts
441
+ var FOCUSABLE_SELECTOR = [
442
+ "a[href]",
443
+ "area[href]",
444
+ "button:not([disabled])",
445
+ 'input:not([disabled]):not([type="hidden"])',
446
+ "select:not([disabled])",
447
+ "textarea:not([disabled])",
448
+ "iframe",
449
+ "summary",
450
+ '[contenteditable="true"]',
451
+ "[tabindex]"
452
+ ].join(",");
453
+ function tabbablesWithin(root) {
454
+ return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
455
+ (el) => el.tabIndex >= 0 && !el.hasAttribute("inert") && el.getAttribute("aria-hidden") !== "true" && !el.closest("[hidden]") && // `inert` is INHERITED by the subtree in a browser, so a control inside
456
+ // an inert ancestor cannot take focus — a trap that hands it focus
457
+ // anyway simply stops moving. The detail surface collapses a pane by
458
+ // making it inert rather than unmounting it (its scroll and its drafts
459
+ // have to survive), which is the case that made this ancestor check
460
+ // load-bearing rather than tidy.
461
+ !el.closest("[inert]")
462
+ );
463
+ }
464
+ function handleTabTrap(event, container) {
465
+ if (event.key !== "Tab" || !container) return;
466
+ const tabbables = tabbablesWithin(container);
467
+ if (tabbables.length < 2) return;
468
+ const first = tabbables[0];
469
+ const last = tabbables[tabbables.length - 1];
470
+ if (!first || !last) return;
471
+ const active = document.activeElement;
472
+ if (event.shiftKey && (active === first || !container.contains(active))) {
473
+ event.preventDefault();
474
+ last.focus();
475
+ return;
476
+ }
477
+ if (!event.shiftKey && active === last) {
478
+ event.preventDefault();
479
+ first.focus();
480
+ }
481
+ }
482
+ var POPOVER_SURFACE_STYLE = {
483
+ background: "rgb(var(--popover))",
484
+ color: "rgb(var(--popover-foreground))",
485
+ border: "1px solid rgb(var(--border))",
486
+ boxShadow: "var(--shadow-lg)"
487
+ };
488
+ var PopoverCtx = createContext(null);
489
+ function usePopoverCtx(consumer) {
490
+ const ctx = useContext(PopoverCtx);
491
+ if (!ctx) {
492
+ throw new Error(
493
+ `${consumer} must be rendered inside <Popover>. Wrap your trigger + content with the Popover root component.`
494
+ );
495
+ }
496
+ return ctx;
497
+ }
498
+ function PopoverRoot({
499
+ open: controlledOpen,
500
+ defaultOpen,
501
+ onOpenChange,
502
+ anchorRect = null,
503
+ children
504
+ }) {
505
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(Boolean(defaultOpen));
506
+ const isControlled = controlledOpen !== void 0;
507
+ const open = isControlled ? Boolean(controlledOpen) : uncontrolledOpen;
508
+ const triggerRef = useRef(null);
509
+ const interactedOutsideRef = useRef(false);
510
+ const generatedId = useId();
511
+ const contentId = `popover-${generatedId.replace(/:/g, "")}`;
512
+ const setOpen = useCallback(
513
+ (next) => {
514
+ if (!isControlled) setUncontrolledOpen(next);
515
+ onOpenChange?.(next);
516
+ },
517
+ [isControlled, onOpenChange]
518
+ );
519
+ const value = useMemo(
520
+ () => ({ open, setOpen, triggerRef, contentId, anchorRect, interactedOutsideRef }),
521
+ [open, setOpen, contentId, anchorRect]
522
+ );
523
+ return /* @__PURE__ */ jsx(PopoverCtx.Provider, { value, children });
524
+ }
525
+ var PopoverTrigger = forwardRef(
526
+ function PopoverTrigger2({ asChild, onClick, children, ...rest }, forwardedRef) {
527
+ const { open, setOpen, triggerRef, contentId } = usePopoverCtx("Popover.Trigger");
528
+ const ariaProps = {
529
+ "aria-haspopup": "dialog",
530
+ "aria-expanded": open,
531
+ "aria-controls": open ? contentId : void 0,
532
+ "data-state": open ? "open" : "closed"
533
+ };
534
+ if (asChild) {
535
+ if (!isValidElement(children)) {
536
+ throw new Error("Popover.Trigger with `asChild` expects exactly one element child.");
537
+ }
538
+ const child = children;
539
+ const childOnClick = child.props.onClick;
540
+ return cloneElement(child, {
541
+ ...rest,
542
+ ...ariaProps,
543
+ ref: composeRefs(child.props.ref, forwardedRef, triggerRef),
544
+ onClick: (event) => {
545
+ childOnClick?.(event);
546
+ onClick?.(event);
547
+ if (!event.defaultPrevented) setOpen(!open);
548
+ }
549
+ });
550
+ }
551
+ return /* @__PURE__ */ jsx(
552
+ "button",
553
+ {
554
+ ref: composeRefs(forwardedRef, triggerRef),
555
+ type: "button",
556
+ ...ariaProps,
557
+ onClick: (event) => {
558
+ onClick?.(event);
559
+ if (!event.defaultPrevented) setOpen(!open);
560
+ },
561
+ ...rest,
562
+ children
563
+ }
564
+ );
565
+ }
566
+ );
567
+ var PopoverContent = forwardRef(
568
+ function PopoverContent2({
569
+ children,
570
+ side = "bottom",
571
+ align = "start",
572
+ offset = 6,
573
+ alignOffset = 0,
574
+ flip = true,
575
+ clampToViewport = true,
576
+ role = "dialog",
577
+ unstyled,
578
+ className,
579
+ style,
580
+ ...rest
581
+ }, forwardedRef) {
582
+ const { open, setOpen, triggerRef, contentId, anchorRect, interactedOutsideRef } = usePopoverCtx("Popover.Content");
583
+ const contentRef = useRef(null);
584
+ const {
585
+ ref: positionRef,
586
+ style: positionStyle,
587
+ placement,
588
+ positioned
589
+ } = useAnchoredPosition({
590
+ anchorRef: triggerRef,
591
+ anchorRect,
592
+ side,
593
+ align,
594
+ offset,
595
+ alignOffset,
596
+ flip,
597
+ clampToViewport,
598
+ enabled: open
599
+ });
600
+ const layer = useLayer({
601
+ enabled: open,
602
+ kind: "popover",
603
+ elementRef: contentRef,
604
+ onEscape: () => setOpen(false)
605
+ });
606
+ useOutsideClick(
607
+ [triggerRef, contentRef],
608
+ () => {
609
+ interactedOutsideRef.current = true;
610
+ setOpen(false);
611
+ },
612
+ { enabled: open, layer }
613
+ );
614
+ useEffect(() => {
615
+ if (!open || !positioned) return;
616
+ const node = contentRef.current;
617
+ if (!node) return;
618
+ interactedOutsideRef.current = false;
619
+ const trigger = triggerRef.current;
620
+ const previous = document.activeElement instanceof HTMLElement && document.activeElement !== document.body ? document.activeElement : null;
621
+ const initial = node.querySelector("[data-autofocus]") ?? tabbablesWithin(node)[0] ?? node;
622
+ initial.focus();
623
+ return () => {
624
+ if (interactedOutsideRef.current) return;
625
+ const active = document.activeElement;
626
+ if (active !== null && active !== document.body && !node.contains(active)) return;
627
+ const target = trigger ?? previous;
628
+ if (target?.isConnected) target.focus();
629
+ };
630
+ }, [open, positioned, triggerRef, interactedOutsideRef]);
631
+ const ref = useMemo(
632
+ () => composeRefs(forwardedRef, contentRef, positionRef),
633
+ [forwardedRef, positionRef]
634
+ );
635
+ if (!open || typeof document === "undefined") return null;
636
+ return createPortal(
637
+ /* @__PURE__ */ jsx(
638
+ "div",
639
+ {
640
+ ref,
641
+ id: contentId,
642
+ role: role === "none" ? void 0 : role,
643
+ tabIndex: -1,
644
+ "data-slot": "popover-content",
645
+ "data-state": "open",
646
+ "data-side": placement?.side,
647
+ "data-align": placement?.align,
648
+ "data-positioned": positioned ? "true" : "false",
649
+ className: cn(
650
+ "fixed z-[100] outline-none",
651
+ !unstyled && "rounded-[var(--radius-lg)] p-3",
652
+ positioned && "ds-enter-pop",
653
+ className
654
+ ),
655
+ style: {
656
+ ...positionStyle,
657
+ ...unstyled ? void 0 : POPOVER_SURFACE_STYLE,
658
+ ...style
659
+ },
660
+ ...rest,
661
+ children
662
+ }
663
+ ),
664
+ document.body
665
+ );
666
+ }
667
+ );
668
+ var Popover = Object.assign(PopoverRoot, {
669
+ Trigger: PopoverTrigger,
670
+ Content: PopoverContent
671
+ });
672
+ function useDataTheme() {
673
+ const read = () => {
674
+ if (typeof document === "undefined") return "dark";
675
+ const attr = document.documentElement.getAttribute("data-theme");
676
+ return attr === "light" ? "light" : "dark";
677
+ };
678
+ const [theme, setTheme] = useState(read);
679
+ useEffect(() => {
680
+ if (typeof window === "undefined") return;
681
+ const observer = new MutationObserver(() => setTheme(read()));
682
+ observer.observe(document.documentElement, {
683
+ attributes: true,
684
+ attributeFilter: ["data-theme"]
685
+ });
686
+ return () => observer.disconnect();
687
+ }, []);
688
+ return theme;
689
+ }
690
+ var LANGUAGES = {
691
+ bash,
692
+ shell: bash,
693
+ sh: bash,
694
+ css,
695
+ diff,
696
+ javascript,
697
+ js: javascript,
698
+ json,
699
+ jsx: jsx3,
700
+ markdown,
701
+ md: markdown,
702
+ html: markup,
703
+ xml: markup,
704
+ python,
705
+ py: python,
706
+ sql,
707
+ tsx,
708
+ typescript,
709
+ ts: typescript,
710
+ yaml,
711
+ yml: yaml
712
+ };
713
+ for (const [name, lang] of Object.entries(LANGUAGES)) {
714
+ PrismLight.registerLanguage(name, lang);
715
+ }
716
+ var LANGUAGE_LABELS = {
717
+ bash: "bash",
718
+ sh: "shell",
719
+ css: "css",
720
+ diff: "diff",
721
+ html: "html",
722
+ javascript: "javascript",
723
+ js: "javascript",
724
+ json: "json",
725
+ jsx: "jsx",
726
+ markdown: "markdown",
727
+ md: "markdown",
728
+ python: "python",
729
+ py: "python",
730
+ shell: "shell",
731
+ sql: "sql",
732
+ tsx: "tsx",
733
+ ts: "typescript",
734
+ typescript: "typescript",
735
+ xml: "xml",
736
+ yaml: "yaml",
737
+ yml: "yaml"
738
+ };
739
+ function CodeBlock({ language, children }) {
740
+ const [copied, setCopied] = useState(false);
741
+ const theme = useDataTheme();
742
+ const syntaxStyle = theme === "light" ? oneLight : vscDarkPlus;
743
+ const lang = (language ?? "").toLowerCase();
744
+ const supported = lang in LANGUAGES;
745
+ const label = (LANGUAGE_LABELS[lang] ?? lang) || "plaintext";
746
+ const source = String(children).replace(/\n$/, "");
747
+ const handleCopy = async () => {
748
+ try {
749
+ await navigator.clipboard.writeText(source);
750
+ setCopied(true);
751
+ setTimeout(() => setCopied(false), 2e3);
752
+ } catch {
753
+ }
754
+ };
755
+ return /* @__PURE__ */ jsxs(
756
+ "div",
757
+ {
758
+ className: "my-4 overflow-hidden text-sm",
759
+ style: {
760
+ background: "rgb(var(--bg-elevated))",
761
+ border: "1px solid rgb(var(--border))",
762
+ borderRadius: "var(--radius-md)"
763
+ },
764
+ children: [
765
+ /* @__PURE__ */ jsxs(
766
+ "div",
767
+ {
768
+ className: "flex items-center justify-between px-3 py-1.5 text-xs",
769
+ style: {
770
+ background: "rgb(var(--muted))",
771
+ borderBottom: "1px solid rgb(var(--border))",
772
+ color: "rgb(var(--muted-foreground))"
773
+ },
774
+ children: [
775
+ /* @__PURE__ */ jsx("span", { className: "font-mono lowercase", children: label }),
776
+ /* @__PURE__ */ jsx(
777
+ "button",
778
+ {
779
+ type: "button",
780
+ onClick: handleCopy,
781
+ className: cn(
782
+ "flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors",
783
+ !copied && "hover:text-[rgb(var(--foreground))]"
784
+ ),
785
+ style: copied ? { color: "rgb(var(--success))" } : void 0,
786
+ "aria-label": copied ? "Copied" : "Copy code",
787
+ children: copied ? /* @__PURE__ */ jsxs(Fragment, { children: [
788
+ /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }),
789
+ " Copied"
790
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
791
+ /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" }),
792
+ " Copy"
793
+ ] })
794
+ }
795
+ )
796
+ ]
797
+ }
798
+ ),
799
+ supported ? /* @__PURE__ */ jsx(
800
+ PrismLight,
801
+ {
802
+ language: lang,
803
+ style: syntaxStyle,
804
+ customStyle: {
805
+ margin: 0,
806
+ padding: "0.875rem 1rem",
807
+ background: "transparent",
808
+ fontSize: "0.8125rem",
809
+ lineHeight: 1.6
810
+ },
811
+ codeTagProps: {
812
+ style: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
813
+ },
814
+ children: source
815
+ }
816
+ ) : /* @__PURE__ */ jsx(
817
+ "pre",
818
+ {
819
+ className: "overflow-x-auto px-4 py-3.5 font-mono text-[0.8125rem] leading-relaxed",
820
+ style: { color: "rgb(var(--foreground))" },
821
+ children: source
822
+ }
823
+ )
824
+ ]
825
+ }
826
+ );
827
+ }
828
+ var PreContext = createContext(false);
829
+ function MarkdownPre({ children }) {
830
+ return /* @__PURE__ */ jsx(PreContext.Provider, { value: true, children });
831
+ }
832
+ function MarkdownCode({ className, children }) {
833
+ const inPre = useContext(PreContext);
834
+ const block = inPre || Boolean(className);
835
+ if (!block) {
836
+ return /* @__PURE__ */ jsx(
837
+ "code",
838
+ {
839
+ className: "rounded px-1.5 py-0.5 font-mono text-[0.85em]",
840
+ style: {
841
+ background: "rgb(var(--muted))",
842
+ color: "rgb(var(--foreground))",
843
+ borderRadius: "var(--radius-xs)"
844
+ },
845
+ children
846
+ }
847
+ );
848
+ }
849
+ const match = className === void 0 ? null : /language-([\w-]+)/.exec(className);
850
+ const language = match?.[1];
851
+ return /* @__PURE__ */ jsx(CodeBlock, { ...language !== void 0 ? { language } : {}, children: sourceOf(children) });
852
+ }
853
+ function sourceOf(children) {
854
+ if (typeof children === "string") return children;
855
+ if (typeof children === "number") return String(children);
856
+ if (children === void 0 || children === null || typeof children === "boolean") return "";
857
+ return String(children);
858
+ }
859
+
860
+ // src/lib/remark-underline.ts
861
+ var PLUS = 43;
862
+ var SPACE = 32;
863
+ var SEQUENCE_TEMPORARY = "underlineSequenceTemporary";
864
+ var SEQUENCE = "underlineSequence";
865
+ var UNDERLINE = "underline";
866
+ var UNDERLINE_TEXT = "underlineText";
867
+ var CHARACTER_ESCAPE = "characterEscape";
868
+ var DATA = "data";
869
+ var SPLICE_CHUNK = 1e4;
870
+ var UNICODE_WHITESPACE = /\s/;
871
+ var UNICODE_PUNCTUATION = /\p{P}|\p{S}/u;
872
+ var GROUP_WHITESPACE = 1;
873
+ var GROUP_PUNCTUATION = 2;
874
+ function classifyCharacter(code) {
875
+ if (code === null || code < 0 || code === SPACE) return GROUP_WHITESPACE;
876
+ const character = String.fromCharCode(code);
877
+ if (UNICODE_WHITESPACE.test(character)) return GROUP_WHITESPACE;
878
+ if (UNICODE_PUNCTUATION.test(character)) return GROUP_PUNCTUATION;
879
+ return void 0;
880
+ }
881
+ function spliceChunked(list, start, remove, items) {
882
+ const end = list.length;
883
+ let at = start < 0 ? -start > end ? 0 : end + start : start > end ? end : start;
884
+ const count = remove > 0 ? remove : 0;
885
+ if (items.length < SPLICE_CHUNK) {
886
+ list.splice(at, count, ...items);
887
+ return;
888
+ }
889
+ if (count > 0) list.splice(at, count);
890
+ let from = 0;
891
+ while (from < items.length) {
892
+ list.splice(at, 0, ...items.slice(from, from + SPLICE_CHUNK));
893
+ from += SPLICE_CHUNK;
894
+ at += SPLICE_CHUNK;
895
+ }
896
+ }
897
+ function resolveAll(constructs, events, context) {
898
+ const called = [];
899
+ let result = events;
900
+ for (const construct of constructs) {
901
+ const resolve = construct.resolveAll;
902
+ if (resolve !== void 0 && !called.includes(resolve)) {
903
+ result = resolve(result, context);
904
+ called.push(resolve);
905
+ }
906
+ }
907
+ return result;
908
+ }
909
+ function tokenizeUnderline(effects, ok, nok) {
910
+ const previous = this.previous;
911
+ const events = this.events;
912
+ let size = 0;
913
+ return start;
914
+ function start(code) {
915
+ if (previous === PLUS && events[events.length - 1]?.[1].type !== CHARACTER_ESCAPE) {
916
+ return nok(code);
917
+ }
918
+ effects.enter(SEQUENCE_TEMPORARY);
919
+ return more(code);
920
+ }
921
+ function more(code) {
922
+ if (code === PLUS) {
923
+ if (size > 1) return nok(code);
924
+ effects.consume(code);
925
+ size++;
926
+ return more;
927
+ }
928
+ if (size < 2) return nok(code);
929
+ const token = effects.exit(SEQUENCE_TEMPORARY);
930
+ const before = classifyCharacter(previous);
931
+ const after = classifyCharacter(code);
932
+ token._open = after === void 0 || after === GROUP_PUNCTUATION && before !== void 0;
933
+ token._close = before === void 0 || before === GROUP_PUNCTUATION && after !== void 0;
934
+ return ok(code);
935
+ }
936
+ }
937
+ function resolveAllUnderline(events, context) {
938
+ let index = -1;
939
+ while (++index < events.length) {
940
+ const closer = events[index];
941
+ if (closer === void 0 || closer[0] !== "enter" || closer[1].type !== SEQUENCE_TEMPORARY || closer[1]._close !== true) {
942
+ continue;
943
+ }
944
+ let open = index;
945
+ while (open--) {
946
+ const opener = events[open];
947
+ if (opener === void 0 || opener[0] !== "exit" || opener[1].type !== SEQUENCE_TEMPORARY || opener[1]._open !== true || // Sizes must match — always two here, kept for parity with the model.
948
+ closer[1].end.offset - closer[1].start.offset !== opener[1].end.offset - opener[1].start.offset) {
949
+ continue;
950
+ }
951
+ closer[1].type = SEQUENCE;
952
+ opener[1].type = SEQUENCE;
953
+ const underline = {
954
+ type: UNDERLINE,
955
+ start: { ...opener[1].start },
956
+ end: { ...closer[1].end }
957
+ };
958
+ const text = {
959
+ type: UNDERLINE_TEXT,
960
+ start: { ...opener[1].end },
961
+ end: { ...closer[1].start }
962
+ };
963
+ const next = [
964
+ ["enter", underline, context],
965
+ ["enter", opener[1], context],
966
+ ["exit", opener[1], context],
967
+ ["enter", text, context]
968
+ ];
969
+ const insideSpan = context.parser.constructs.insideSpan.null;
970
+ if (insideSpan !== void 0) {
971
+ spliceChunked(
972
+ next,
973
+ next.length,
974
+ 0,
975
+ resolveAll(insideSpan, events.slice(open + 1, index), context)
976
+ );
977
+ }
978
+ spliceChunked(next, next.length, 0, [
979
+ ["exit", text, context],
980
+ ["enter", closer[1], context],
981
+ ["exit", closer[1], context],
982
+ ["exit", underline, context]
983
+ ]);
984
+ spliceChunked(events, open - 1, index - open + 3, next);
985
+ index = open + next.length - 2;
986
+ break;
987
+ }
988
+ }
989
+ for (const event of events) {
990
+ if (event[1].type === SEQUENCE_TEMPORARY) event[1].type = DATA;
991
+ }
992
+ return events;
993
+ }
994
+ function underlineSyntax() {
995
+ const construct = {
996
+ name: "underline",
997
+ tokenize: tokenizeUnderline,
998
+ resolveAll: resolveAllUnderline
999
+ };
1000
+ return {
1001
+ text: { [PLUS]: construct },
1002
+ insideSpan: { null: [construct] },
1003
+ attentionMarkers: { null: [PLUS] }
1004
+ };
1005
+ }
1006
+ function enterUnderline(token) {
1007
+ this.enter({ type: UNDERLINE, children: [], data: { hName: "u" } }, token);
1008
+ }
1009
+ function exitUnderline(token) {
1010
+ this.exit(token);
1011
+ }
1012
+ function underlineFromMarkdown() {
1013
+ return {
1014
+ // A soft line break inside `++a\nb++` is kept as text, like `delete`.
1015
+ canContainEols: [UNDERLINE],
1016
+ enter: { [UNDERLINE]: enterUnderline },
1017
+ exit: { [UNDERLINE]: exitUnderline }
1018
+ };
1019
+ }
1020
+ function remarkUnderline() {
1021
+ const data = this.data();
1022
+ const micromark = data.micromarkExtensions ?? (data.micromarkExtensions = []);
1023
+ const fromMarkdown = data.fromMarkdownExtensions ?? (data.fromMarkdownExtensions = []);
1024
+ micromark.push(underlineSyntax());
1025
+ fromMarkdown.push(underlineFromMarkdown());
1026
+ }
1027
+ var MARKDOWN_COMPONENTS = {
1028
+ h1: ({ children }) => /* @__PURE__ */ jsx(
1029
+ "h1",
1030
+ {
1031
+ className: "mt-6 mb-3 text-xl font-bold first:mt-0",
1032
+ style: { color: "rgb(var(--foreground))" },
1033
+ children
1034
+ }
1035
+ ),
1036
+ h2: ({ children }) => /* @__PURE__ */ jsx(
1037
+ "h2",
1038
+ {
1039
+ className: "mt-5 mb-2 text-lg font-bold first:mt-0",
1040
+ style: { color: "rgb(var(--foreground))" },
1041
+ children
1042
+ }
1043
+ ),
1044
+ h3: ({ children }) => /* @__PURE__ */ jsx(
1045
+ "h3",
1046
+ {
1047
+ className: "mt-4 mb-2 text-base font-semibold first:mt-0",
1048
+ style: { color: "rgb(var(--foreground))" },
1049
+ children
1050
+ }
1051
+ ),
1052
+ p: ({ children }) => /* @__PURE__ */ jsx(
1053
+ "p",
1054
+ {
1055
+ className: "mb-4 leading-relaxed last:mb-0",
1056
+ style: { color: "rgb(var(--foreground))" },
1057
+ children
1058
+ }
1059
+ ),
1060
+ // list-outside + left padding so wrapped lines indent under the text (not the
1061
+ // marker). Bullets use text-tertiary; ordered markers use --accent + bold so
1062
+ // the numbering reads as deliberate hierarchy rather than browser-default grey.
1063
+ ul: ({ children }) => /* @__PURE__ */ jsx(
1064
+ "ul",
1065
+ {
1066
+ className: "mb-4 list-disc list-outside space-y-2 pl-6",
1067
+ style: { color: "rgb(var(--text-tertiary))" },
1068
+ children
1069
+ }
1070
+ ),
1071
+ ol: ({ children }) => /* @__PURE__ */ jsx(
1072
+ "ol",
1073
+ {
1074
+ className: "mb-4 list-decimal list-outside space-y-2 pl-6 marker:font-semibold",
1075
+ style: { color: "rgb(var(--foreground))" },
1076
+ children
1077
+ }
1078
+ ),
1079
+ li: ({ children }) => /* @__PURE__ */ jsx(
1080
+ "li",
1081
+ {
1082
+ className: "pl-1 leading-relaxed",
1083
+ style: { color: "rgb(var(--foreground))" },
1084
+ children
1085
+ }
1086
+ ),
1087
+ strong: ({ children }) => /* @__PURE__ */ jsx("strong", { className: "font-semibold", style: { color: "rgb(var(--foreground))" }, children }),
1088
+ em: ({ children }) => /* @__PURE__ */ jsx("em", { className: "italic", style: { color: "rgb(var(--foreground))" }, children }),
1089
+ // code: inline chip or code block (CodeBlock via MarkdownCode). pre renders
1090
+ // no element — CodeBlock owns its container — but it is what tells the code
1091
+ // inside it that it is a block, language or not (see PreContext).
1092
+ code: MarkdownCode,
1093
+ pre: MarkdownPre,
1094
+ blockquote: ({ children }) => /* @__PURE__ */ jsx(
1095
+ "blockquote",
1096
+ {
1097
+ className: "my-4 border-l-4 py-1 pl-4 italic",
1098
+ style: {
1099
+ borderColor: "rgb(var(--accent))",
1100
+ color: "rgb(var(--muted-foreground))"
1101
+ },
1102
+ children
1103
+ }
1104
+ ),
1105
+ hr: () => /* @__PURE__ */ jsx("hr", { className: "my-6", style: { borderColor: "rgb(var(--border))" } }),
1106
+ a: ({ href, children }) => /* @__PURE__ */ jsx(
1107
+ "a",
1108
+ {
1109
+ href,
1110
+ className: "hover:underline",
1111
+ style: { color: "rgb(var(--accent))" },
1112
+ target: "_blank",
1113
+ rel: "noopener noreferrer",
1114
+ children
1115
+ }
1116
+ ),
1117
+ // GFM additions — tables, task lists, strikethrough.
1118
+ table: ({ children }) => /* @__PURE__ */ jsx(
1119
+ "div",
1120
+ {
1121
+ className: "my-4 overflow-x-auto",
1122
+ style: {
1123
+ borderRadius: "var(--radius-md)",
1124
+ border: "1px solid rgb(var(--border))"
1125
+ },
1126
+ children: /* @__PURE__ */ jsx("table", { className: "w-full border-collapse text-sm", children })
1127
+ }
1128
+ ),
1129
+ thead: ({ children }) => /* @__PURE__ */ jsx(
1130
+ "thead",
1131
+ {
1132
+ style: {
1133
+ background: "rgb(var(--muted))",
1134
+ color: "rgb(var(--muted-foreground))"
1135
+ },
1136
+ children
1137
+ }
1138
+ ),
1139
+ tbody: ({ children }) => /* @__PURE__ */ jsx("tbody", { children }),
1140
+ tr: ({ children }) => /* @__PURE__ */ jsx(
1141
+ "tr",
1142
+ {
1143
+ className: "first:border-t-0",
1144
+ style: { borderTop: "1px solid rgb(var(--border))" },
1145
+ children
1146
+ }
1147
+ ),
1148
+ th: ({ children }) => /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children }),
1149
+ td: ({ children }) => /* @__PURE__ */ jsx(
1150
+ "td",
1151
+ {
1152
+ className: "px-3 py-2 align-top",
1153
+ style: { color: "rgb(var(--foreground))" },
1154
+ children
1155
+ }
1156
+ ),
1157
+ del: ({ children }) => /* @__PURE__ */ jsx("del", { className: "line-through", style: { color: "rgb(var(--muted-foreground))" }, children }),
1158
+ // Underline — `++text++` via remarkUnderline (ADR-148 D13). The line is a
1159
+ // step softer than the ink so it reads as a deliberate underline, not a link.
1160
+ u: ({ children }) => /* @__PURE__ */ jsx(
1161
+ "u",
1162
+ {
1163
+ className: "underline-offset-2",
1164
+ style: {
1165
+ color: "rgb(var(--foreground))",
1166
+ textDecorationColor: "rgb(var(--text-tertiary))"
1167
+ },
1168
+ children
1169
+ }
1170
+ ),
1171
+ // Task-list checkboxes rendered by remark-gfm for "- [ ] ..." / "- [x] ...".
1172
+ input: (props) => props.type === "checkbox" ? /* @__PURE__ */ jsx(
1173
+ "input",
1174
+ {
1175
+ ...props,
1176
+ disabled: true,
1177
+ className: "mr-1.5 -translate-y-px",
1178
+ style: { accentColor: "rgb(var(--accent))" }
1179
+ }
1180
+ ) : /* @__PURE__ */ jsx("input", { ...props })
1181
+ };
1182
+ var REMARK_PLUGINS = [remarkGfm, remarkUnderline];
1183
+ function MarkdownRenderer({ children, className }) {
1184
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(
1185
+ ReactMarkdown,
1186
+ {
1187
+ components: MARKDOWN_COMPONENTS,
1188
+ remarkPlugins: REMARK_PLUGINS,
1189
+ children
1190
+ }
1191
+ ) });
1192
+ }
1193
+ function Kbd({ children, size = "md", tone = "neutral" }) {
1194
+ const onAccent = tone === "on-accent";
1195
+ const style = {
1196
+ display: "inline-flex",
1197
+ alignItems: "center",
1198
+ justifyContent: "center",
1199
+ gap: "0.125rem",
1200
+ // On an accent fill the INK stays at full strength and the container does
1201
+ // the quieting. Fading the text instead is the tempting move and it is
1202
+ // wrong: white at 85% over --accent measures 4.33:1, under AA, while full
1203
+ // white is 5.64:1. The translucent fill also lightens the local ground, so
1204
+ // it raises the text's contrast rather than lowering it.
1205
+ background: onAccent ? "rgb(var(--accent-foreground) / 0.16)" : "rgb(var(--surface-overlay-soft))",
1206
+ border: `1px solid ${onAccent ? "rgb(var(--accent-foreground) / 0.4)" : "rgb(var(--border))"}`,
1207
+ color: onAccent ? "rgb(var(--accent-foreground))" : "rgb(var(--text-tertiary))",
1208
+ fontFamily: "var(--font-mono)",
1209
+ fontSize: size === "sm" ? "10.5px" : "11.5px",
1210
+ padding: size === "sm" ? "1px 6px" : "2px 8px",
1211
+ borderRadius: "var(--radius-xs)",
1212
+ lineHeight: 1
1213
+ };
1214
+ return /* @__PURE__ */ jsx("kbd", { style, children });
1215
+ }
1216
+ var MENTION_SCHEMES = ["user", "task_item", "task_board"];
1217
+ var MENTION_PATTERN = new RegExp(`^(${MENTION_SCHEMES.join("|")}):(.+)$`);
1218
+ function isMentionScheme(value) {
1219
+ return MENTION_SCHEMES.some((scheme) => scheme === value);
1220
+ }
1221
+ function parseMentionHref(href) {
1222
+ if (href === void 0) return null;
1223
+ const match = MENTION_PATTERN.exec(href);
1224
+ if (match === null) return null;
1225
+ const type = match[1];
1226
+ const id = match[2];
1227
+ if (type === void 0 || id === void 0) return null;
1228
+ if (!isMentionScheme(type)) return null;
1229
+ return { type, id };
1230
+ }
1231
+ function urlTransform(value) {
1232
+ return parseMentionHref(value) === null ? defaultUrlTransform(value) : value;
1233
+ }
1234
+ function CommentBody({ bodyMd, onSelectMention, className }) {
1235
+ const components = {
1236
+ ...MARKDOWN_COMPONENTS,
1237
+ // Paragraph spacing inside a comment is tighter than the document default
1238
+ // the shared map assumes — a comment is a remark, not an article.
1239
+ p: ({ children }) => /* @__PURE__ */ jsx("p", { className: "mb-[var(--space-2)] leading-[1.55] last:mb-0", style: { color: "rgb(var(--foreground))" }, children }),
1240
+ // Images are outside the composer's subset — ADR-148 D3: "images-by-markdown
1241
+ // out; ADR-151 owns attachments" — but `![alt](url)` is still typeable. It
1242
+ // renders as its alt text and nothing else: no `<img>`, so no request to a
1243
+ // user-supplied URL and no empty-`src` element when the transform strips a
1244
+ // dangerous one. ADR-151: this is the entry to revisit when attachments land.
1245
+ img: ({ alt }) => alt === void 0 || alt === "" ? null : /* @__PURE__ */ jsx("span", { children: alt }),
1246
+ a: ({ href, children }) => {
1247
+ const mention = parseMentionHref(href);
1248
+ if (mention === null) {
1249
+ return /* @__PURE__ */ jsx(
1250
+ "a",
1251
+ {
1252
+ href,
1253
+ className: "underline-offset-2 hover:underline",
1254
+ style: { color: "rgb(var(--accent-ink))" },
1255
+ target: "_blank",
1256
+ rel: "noopener noreferrer",
1257
+ children
1258
+ }
1259
+ );
1260
+ }
1261
+ const interactive = onSelectMention !== void 0;
1262
+ const Tag = interactive ? "button" : "span";
1263
+ return /* @__PURE__ */ jsx(
1264
+ Tag,
1265
+ {
1266
+ ...interactive ? { type: "button", onClick: () => {
1267
+ onSelectMention(mention);
1268
+ } } : {},
1269
+ className: cn(
1270
+ "inline-flex items-center align-baseline text-[12.5px] font-semibold",
1271
+ "transition-[background-color,border-color] duration-[var(--dur-fast)] ease-[var(--ease-out)]",
1272
+ "motion-reduce:transition-none",
1273
+ "border border-[rgb(var(--border))] bg-[rgb(var(--surface-inset))] text-[rgb(var(--accent-ink))]",
1274
+ interactive && "hover:border-[rgb(var(--accent))] hover:bg-[rgb(var(--surface-hover))] focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)]"
1275
+ ),
1276
+ style: {
1277
+ borderRadius: "var(--radius-sm)",
1278
+ paddingInline: "var(--space-1)"
1279
+ },
1280
+ children
1281
+ }
1282
+ );
1283
+ }
1284
+ };
1285
+ return /* @__PURE__ */ jsx(
1286
+ "div",
1287
+ {
1288
+ className: cn("text-[13px]", className),
1289
+ style: { color: "rgb(var(--foreground))" },
1290
+ children: /* @__PURE__ */ jsx(
1291
+ ReactMarkdown,
1292
+ {
1293
+ components,
1294
+ remarkPlugins: REMARK_PLUGINS,
1295
+ urlTransform,
1296
+ children: bodyMd
1297
+ }
1298
+ )
1299
+ }
1300
+ );
1301
+ }
1302
+ function ComposerFooter({
1303
+ toolbar,
1304
+ mode,
1305
+ onModeChange,
1306
+ modeDisabled = false,
1307
+ meta,
1308
+ actions
1309
+ }) {
1310
+ const showSwitcher = onModeChange !== void 0 && mode !== void 0;
1311
+ const next = mode === "rich" ? "plain" : "rich";
1312
+ return /* @__PURE__ */ jsxs(
1313
+ "div",
1314
+ {
1315
+ className: "ds-composer-footer flex min-w-0 flex-nowrap items-center gap-[var(--space-1)]",
1316
+ style: { padding: "0 var(--space-2) var(--space-2)" },
1317
+ children: [
1318
+ showSwitcher ? /* @__PURE__ */ jsxs(Fragment, { children: [
1319
+ /* @__PURE__ */ jsx(
1320
+ Button,
1321
+ {
1322
+ variant: "icon",
1323
+ size: "sm",
1324
+ className: "shrink-0",
1325
+ "aria-label": `Switch to the ${next} editor`,
1326
+ title: `Switch to the ${next} editor`,
1327
+ disabled: modeDisabled,
1328
+ onMouseDown: (event) => {
1329
+ event.preventDefault();
1330
+ },
1331
+ onClick: () => {
1332
+ onModeChange(next);
1333
+ },
1334
+ children: mode === "rich" ? /* @__PURE__ */ jsx(PencilLine, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Type, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })
1335
+ }
1336
+ ),
1337
+ /* @__PURE__ */ jsx(
1338
+ "span",
1339
+ {
1340
+ "aria-hidden": "true",
1341
+ className: "shrink-0",
1342
+ style: {
1343
+ inlineSize: "1px",
1344
+ blockSize: "var(--space-4)",
1345
+ marginInline: "var(--space-1)",
1346
+ background: "rgb(var(--border))"
1347
+ }
1348
+ }
1349
+ )
1350
+ ] }) : null,
1351
+ /* @__PURE__ */ jsx(
1352
+ "div",
1353
+ {
1354
+ className: cn(
1355
+ "flex min-w-0 flex-1 items-center gap-[var(--space-1)]",
1356
+ "overflow-x-auto overscroll-x-contain",
1357
+ // A scrollbar in a 24px-tall control strip is louder than the
1358
+ // controls; the row is short enough to drag or shift-wheel.
1359
+ "[scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
1360
+ ),
1361
+ children: toolbar
1362
+ }
1363
+ ),
1364
+ /* @__PURE__ */ jsx("span", { className: "ds-composer-meta contents", children: meta }),
1365
+ actions
1366
+ ]
1367
+ }
1368
+ );
1369
+ }
1370
+
1371
+ // src/thread/markdown-format.ts
1372
+ var WRAPPERS = {
1373
+ bold: "**",
1374
+ italic: "_",
1375
+ code: "`"
1376
+ };
1377
+ var LIST_PREFIX = "- ";
1378
+ var LINK_PLACEHOLDER = "url";
1379
+ function clamp(n, min, max) {
1380
+ return Math.min(Math.max(n, min), max);
1381
+ }
1382
+ function applyWrapper(value, start, end, marker) {
1383
+ const selected = value.slice(start, end);
1384
+ const width = marker.length;
1385
+ if (selected.length >= width * 2 && selected.startsWith(marker) && selected.endsWith(marker)) {
1386
+ const inner = selected.slice(width, selected.length - width);
1387
+ return {
1388
+ value: value.slice(0, start) + inner + value.slice(end),
1389
+ selectionStart: start,
1390
+ selectionEnd: start + inner.length
1391
+ };
1392
+ }
1393
+ if (start >= width && value.slice(start - width, start) === marker && value.slice(end, end + width) === marker) {
1394
+ return {
1395
+ value: value.slice(0, start - width) + selected + value.slice(end + width),
1396
+ selectionStart: start - width,
1397
+ selectionEnd: start - width + selected.length
1398
+ };
1399
+ }
1400
+ return {
1401
+ value: value.slice(0, start) + marker + selected + marker + value.slice(end),
1402
+ selectionStart: start + width,
1403
+ selectionEnd: end + width
1404
+ };
1405
+ }
1406
+ function applyLink(value, start, end) {
1407
+ const selected = value.slice(start, end);
1408
+ const inserted = `[${selected}](${LINK_PLACEHOLDER})`;
1409
+ const next = value.slice(0, start) + inserted + value.slice(end);
1410
+ if (selected === "") {
1411
+ return { value: next, selectionStart: start + 1, selectionEnd: start + 1 };
1412
+ }
1413
+ const urlStart = start + selected.length + 3;
1414
+ return {
1415
+ value: next,
1416
+ selectionStart: urlStart,
1417
+ selectionEnd: urlStart + LINK_PLACEHOLDER.length
1418
+ };
1419
+ }
1420
+ function applyList(value, start, end) {
1421
+ const lineStart = value.lastIndexOf("\n", Math.max(start - 1, 0)) + 1;
1422
+ const nextBreak = value.indexOf("\n", end);
1423
+ const lineEnd = nextBreak === -1 ? value.length : nextBreak;
1424
+ const block = value.slice(lineStart, lineEnd);
1425
+ const lines = block.split("\n");
1426
+ const meaningful = lines.filter((line) => line.trim() !== "");
1427
+ const allPrefixed = meaningful.length > 0 && meaningful.every((line) => line.startsWith(LIST_PREFIX));
1428
+ const next = lines.map((line) => {
1429
+ if (allPrefixed) return line.startsWith(LIST_PREFIX) ? line.slice(LIST_PREFIX.length) : line;
1430
+ return line.trim() === "" ? line : LIST_PREFIX + line;
1431
+ }).join("\n");
1432
+ return {
1433
+ value: value.slice(0, lineStart) + next + value.slice(lineEnd),
1434
+ selectionStart: lineStart,
1435
+ selectionEnd: lineStart + next.length
1436
+ };
1437
+ }
1438
+ function applyMarkdownFormat(value, selectionStart, selectionEnd, format) {
1439
+ const start = clamp(Math.min(selectionStart, selectionEnd), 0, value.length);
1440
+ const end = clamp(Math.max(selectionStart, selectionEnd), 0, value.length);
1441
+ switch (format) {
1442
+ case "bold":
1443
+ return applyWrapper(value, start, end, WRAPPERS.bold);
1444
+ case "italic":
1445
+ return applyWrapper(value, start, end, WRAPPERS.italic);
1446
+ case "code":
1447
+ return applyWrapper(value, start, end, WRAPPERS.code);
1448
+ case "link":
1449
+ return applyLink(value, start, end);
1450
+ case "list":
1451
+ return applyList(value, start, end);
1452
+ }
1453
+ }
1454
+ var SEND_KEYCAP = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) ? "\u2318\u21B5" : "Ctrl \u21B5";
1455
+ var TOOLBAR = [
1456
+ { format: "bold", label: "Bold", icon: Bold, shortcut: "b" },
1457
+ { format: "italic", label: "Italic", icon: Italic, shortcut: "i" },
1458
+ { format: "code", label: "Code", icon: Code },
1459
+ { format: "link", label: "Link", icon: Link, shortcut: "k" },
1460
+ { format: "list", label: "Bulleted list", icon: List }
1461
+ ];
1462
+ function ThreadComposer({
1463
+ value,
1464
+ onChange,
1465
+ onSubmit,
1466
+ onCancel,
1467
+ placeholder = "Add a comment\u2026",
1468
+ submitLabel = "Comment",
1469
+ cancelLabel = "Cancel",
1470
+ sending = false,
1471
+ disabled = false,
1472
+ replyingTo,
1473
+ avatar,
1474
+ rows = 3,
1475
+ autoFocus = false,
1476
+ toolbar = true,
1477
+ maxLength,
1478
+ mode,
1479
+ onModeChange,
1480
+ label,
1481
+ className
1482
+ }) {
1483
+ const textareaRef = useRef(null);
1484
+ const pendingSelection = useRef(null);
1485
+ useLayoutEffect(() => {
1486
+ const selection = pendingSelection.current;
1487
+ const element = textareaRef.current;
1488
+ if (selection === null || element === null) return;
1489
+ pendingSelection.current = null;
1490
+ element.focus();
1491
+ element.setSelectionRange(selection.start, selection.end);
1492
+ }, [value]);
1493
+ const format = useCallback(
1494
+ (next) => {
1495
+ const element = textareaRef.current;
1496
+ if (element === null) return;
1497
+ const edit = applyMarkdownFormat(
1498
+ value,
1499
+ element.selectionStart,
1500
+ element.selectionEnd,
1501
+ next
1502
+ );
1503
+ pendingSelection.current = { start: edit.selectionStart, end: edit.selectionEnd };
1504
+ onChange(edit.value);
1505
+ },
1506
+ [onChange, value]
1507
+ );
1508
+ const onKeyDown = useCallback(
1509
+ (event) => {
1510
+ const mod = event.metaKey || event.ctrlKey;
1511
+ if (mod && event.key === "Enter") {
1512
+ event.preventDefault();
1513
+ onSubmit();
1514
+ return;
1515
+ }
1516
+ if (event.key === "Escape" && onCancel !== void 0) {
1517
+ event.preventDefault();
1518
+ onCancel();
1519
+ return;
1520
+ }
1521
+ if (!mod) return;
1522
+ const action = TOOLBAR.find((entry) => entry.shortcut === event.key.toLowerCase());
1523
+ if (action === void 0) return;
1524
+ event.preventDefault();
1525
+ format(action.format);
1526
+ },
1527
+ [format, onCancel, onSubmit]
1528
+ );
1529
+ const canSend = value.trim() !== "" && !sending && !disabled;
1530
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-[var(--space-1)]", className), children: [
1531
+ replyingTo === void 0 ? null : /* @__PURE__ */ jsxs(
1532
+ "p",
1533
+ {
1534
+ className: "flex items-center gap-[var(--space-1)] text-[12px]",
1535
+ style: { color: "rgb(var(--text-tertiary))" },
1536
+ children: [
1537
+ /* @__PURE__ */ jsx(CornerDownRight, { size: 10, strokeWidth: 1.5, "aria-hidden": "true" }),
1538
+ "Replying to",
1539
+ " ",
1540
+ /* @__PURE__ */ jsx("span", { className: "font-semibold", style: { color: "rgb(var(--text-secondary))" }, children: replyingTo })
1541
+ ]
1542
+ }
1543
+ ),
1544
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-[var(--space-2)]", children: [
1545
+ avatar,
1546
+ /* @__PURE__ */ jsxs(
1547
+ "div",
1548
+ {
1549
+ className: cn(
1550
+ "min-w-0 flex-1 overflow-hidden border",
1551
+ "transition-[border-color,box-shadow] duration-[var(--dur-fast)] ease-[var(--ease-out)]",
1552
+ "motion-reduce:transition-none",
1553
+ "focus-within:border-[rgb(var(--accent))] focus-within:[box-shadow:var(--ring-focus)]"
1554
+ ),
1555
+ style: {
1556
+ // Concentric: the host panel declares --thread-inner-radius as its
1557
+ // own radius minus its padding (interface standards §1). Nothing
1558
+ // here eyeballs a corner.
1559
+ borderRadius: "var(--thread-inner-radius, var(--radius-sm))",
1560
+ borderColor: "rgb(var(--border))",
1561
+ background: "rgb(var(--surface-card))"
1562
+ },
1563
+ children: [
1564
+ /* @__PURE__ */ jsx(
1565
+ "textarea",
1566
+ {
1567
+ ref: textareaRef,
1568
+ "aria-label": label,
1569
+ className: "block w-full resize-none bg-transparent outline-none",
1570
+ placeholder,
1571
+ rows,
1572
+ value,
1573
+ disabled,
1574
+ autoFocus,
1575
+ onChange: (event) => {
1576
+ onChange(event.target.value);
1577
+ },
1578
+ onKeyDown,
1579
+ ...maxLength === void 0 ? {} : { maxLength },
1580
+ style: {
1581
+ // Optical: vertical is a step tighter than horizontal, because the
1582
+ // line box already carries space the glyphs do not fill.
1583
+ padding: "var(--space-2) var(--space-3)",
1584
+ color: "rgb(var(--foreground))",
1585
+ fontFamily: "var(--font-sans)",
1586
+ fontSize: "13px",
1587
+ lineHeight: 1.55
1588
+ }
1589
+ }
1590
+ ),
1591
+ /* @__PURE__ */ jsx(
1592
+ ComposerFooter,
1593
+ {
1594
+ ...mode === void 0 ? {} : { mode },
1595
+ ...onModeChange === void 0 ? {} : { onModeChange },
1596
+ modeDisabled: disabled,
1597
+ toolbar: /* @__PURE__ */ jsx(Fragment, { children: toolbar ? TOOLBAR.map((action) => {
1598
+ const Icon = action.icon;
1599
+ return /* @__PURE__ */ jsx(
1600
+ Button,
1601
+ {
1602
+ variant: "icon",
1603
+ size: "sm",
1604
+ "aria-label": action.label,
1605
+ title: action.label,
1606
+ disabled: disabled || sending,
1607
+ onClick: () => {
1608
+ format(action.format);
1609
+ },
1610
+ children: /* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })
1611
+ },
1612
+ action.format
1613
+ );
1614
+ }) : null }),
1615
+ meta: /* @__PURE__ */ jsx(Fragment, { children: maxLength === void 0 ? null : /* @__PURE__ */ jsxs(
1616
+ "span",
1617
+ {
1618
+ className: "tabular-nums text-right",
1619
+ style: {
1620
+ minInlineSize: "7ch",
1621
+ fontSize: "11.5px",
1622
+ color: value.length > maxLength * 0.9 ? "rgb(var(--warning-ink))" : "rgb(var(--text-tertiary))"
1623
+ },
1624
+ children: [
1625
+ value.length,
1626
+ "/",
1627
+ maxLength
1628
+ ]
1629
+ }
1630
+ ) }),
1631
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
1632
+ onCancel === void 0 ? null : /* @__PURE__ */ jsx(
1633
+ Button,
1634
+ {
1635
+ variant: "secondary",
1636
+ size: "sm",
1637
+ onClick: onCancel,
1638
+ disabled: disabled || sending,
1639
+ children: cancelLabel
1640
+ }
1641
+ ),
1642
+ /* @__PURE__ */ jsx(
1643
+ Button,
1644
+ {
1645
+ variant: "primary",
1646
+ size: "sm",
1647
+ onClick: () => {
1648
+ onSubmit();
1649
+ },
1650
+ disabled: !canSend,
1651
+ "aria-label": sending ? "Sending\u2026" : submitLabel,
1652
+ trailingIcon: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ds-composer-keycap", children: /* @__PURE__ */ jsx(Kbd, { size: "sm", tone: "on-accent", children: SEND_KEYCAP }) }),
1653
+ children: sending ? "Sending\u2026" : submitLabel
1654
+ }
1655
+ )
1656
+ ] })
1657
+ }
1658
+ )
1659
+ ]
1660
+ }
1661
+ )
1662
+ ] })
1663
+ ] });
1664
+ }
1665
+ var PASTE_ATTACHMENT_THRESHOLD = 2500;
1666
+ var GIF_SEARCH_DEBOUNCE_MS = 300;
1667
+ var ATTACHMENT_LIMITS = {
1668
+ fileMaxBytes: 25 * 1024 * 1024,
1669
+ commentMaxBytes: 50 * 1024 * 1024,
1670
+ maxPerComment: 10
1671
+ };
1672
+ var localIdCounter = 0;
1673
+ function nextLocalId() {
1674
+ localIdCounter += 1;
1675
+ return `att-local-${String(localIdCounter)}`;
1676
+ }
1677
+ function pastedTextFilename(index) {
1678
+ return `pasted-text-${String(index)}.txt`;
1679
+ }
1680
+ function shouldAttachPaste(text) {
1681
+ return text.length >= PASTE_ATTACHMENT_THRESHOLD;
1682
+ }
1683
+ function kindOf(ticketKind) {
1684
+ if (ticketKind === "image" || ticketKind === "gif") return ticketKind;
1685
+ return "file";
1686
+ }
1687
+ function previewFor(file) {
1688
+ if (!file.type.startsWith("image/")) return void 0;
1689
+ try {
1690
+ return URL.createObjectURL(file);
1691
+ } catch {
1692
+ return void 0;
1693
+ }
1694
+ }
1695
+ function useAttachments(options) {
1696
+ const {
1697
+ upload,
1698
+ attachGif,
1699
+ onError,
1700
+ maxPerComment = ATTACHMENT_LIMITS.maxPerComment,
1701
+ maxFileBytes = ATTACHMENT_LIMITS.fileMaxBytes,
1702
+ maxTotalBytes = ATTACHMENT_LIMITS.commentMaxBytes
1703
+ } = options;
1704
+ const [attachments, setAttachments] = useState([]);
1705
+ const listRef = useRef([]);
1706
+ const commit = useCallback(
1707
+ (next) => {
1708
+ const value = next(listRef.current);
1709
+ listRef.current = value;
1710
+ setAttachments(value);
1711
+ },
1712
+ []
1713
+ );
1714
+ const controllers = useRef(/* @__PURE__ */ new Map());
1715
+ const objectUrls = useRef(/* @__PURE__ */ new Set());
1716
+ const pasteCount = useRef(0);
1717
+ const uploadRef = useRef(upload);
1718
+ const errorRef = useRef(onError);
1719
+ uploadRef.current = upload;
1720
+ errorRef.current = onError;
1721
+ const gifRef = useRef(attachGif);
1722
+ gifRef.current = attachGif;
1723
+ useEffect(() => {
1724
+ const urls = objectUrls.current;
1725
+ const inFlight = controllers.current;
1726
+ return () => {
1727
+ for (const url of urls) URL.revokeObjectURL(url);
1728
+ urls.clear();
1729
+ for (const controller of inFlight.values()) controller.abort();
1730
+ inFlight.clear();
1731
+ };
1732
+ }, []);
1733
+ const releasePreview = useCallback((url) => {
1734
+ if (url === void 0 || !objectUrls.current.has(url)) return;
1735
+ objectUrls.current.delete(url);
1736
+ setTimeout(() => {
1737
+ URL.revokeObjectURL(url);
1738
+ }, 0);
1739
+ }, []);
1740
+ const patch = useCallback(
1741
+ (localId, next) => {
1742
+ commit((current) => current.map((a) => a.localId === localId ? { ...a, ...next } : a));
1743
+ },
1744
+ [commit]
1745
+ );
1746
+ const fail = useCallback((message) => {
1747
+ errorRef.current?.(message);
1748
+ }, []);
1749
+ const start = useCallback(
1750
+ (blob, meta) => {
1751
+ const uploader = uploadRef.current;
1752
+ if (uploader === void 0) return;
1753
+ const localId = nextLocalId();
1754
+ const controller = new AbortController();
1755
+ controllers.current.set(localId, controller);
1756
+ if (meta.previewUrl !== void 0) objectUrls.current.add(meta.previewUrl);
1757
+ commit((current) => [
1758
+ ...current,
1759
+ {
1760
+ localId,
1761
+ name: meta.name,
1762
+ sizeBytes: blob.size,
1763
+ kind: meta.kind,
1764
+ state: "uploading",
1765
+ progress: 0,
1766
+ previewUrl: meta.previewUrl,
1767
+ pastedText: meta.pastedText,
1768
+ charCount: meta.charCount
1769
+ }
1770
+ ]);
1771
+ void uploader(blob, {
1772
+ filename: meta.name,
1773
+ signal: controller.signal,
1774
+ onProgress: (fraction) => {
1775
+ patch(localId, { progress: Math.max(0, Math.min(1, fraction)) });
1776
+ }
1777
+ }).then((ticket) => {
1778
+ controllers.current.delete(localId);
1779
+ patch(localId, {
1780
+ state: "ready",
1781
+ progress: 1,
1782
+ ticketId: ticket.id,
1783
+ // The server's own account wins over the local guess: it read the
1784
+ // magic number and we read a filename.
1785
+ name: ticket.name,
1786
+ sizeBytes: ticket.sizeBytes,
1787
+ kind: kindOf(ticket.kind),
1788
+ ...ticket.aspectRatio === null ? {} : { aspectRatio: ticket.aspectRatio }
1789
+ });
1790
+ }).catch((err) => {
1791
+ controllers.current.delete(localId);
1792
+ if (err instanceof DOMException && err.name === "AbortError") return;
1793
+ const message = err instanceof Error && err.message.length > 0 ? err.message : "That file couldn't be attached.";
1794
+ patch(localId, { state: "failed", error: message });
1795
+ fail(message);
1796
+ });
1797
+ return localId;
1798
+ },
1799
+ [commit, fail, patch]
1800
+ );
1801
+ const admits = useCallback(
1802
+ (size, label) => {
1803
+ const current = listRef.current;
1804
+ if (current.length >= maxPerComment) {
1805
+ fail(`You can attach up to ${String(maxPerComment)} files to a comment.`);
1806
+ return false;
1807
+ }
1808
+ if (size > maxFileBytes) {
1809
+ fail(`${label} is too large. Files can be up to 25 MB.`);
1810
+ return false;
1811
+ }
1812
+ const total = current.reduce((sum, a) => sum + a.sizeBytes, 0);
1813
+ if (total + size > maxTotalBytes) {
1814
+ fail("That would put this comment over 50 MB of attachments.");
1815
+ return false;
1816
+ }
1817
+ return true;
1818
+ },
1819
+ [fail, maxFileBytes, maxPerComment, maxTotalBytes]
1820
+ );
1821
+ const addFiles = useCallback(
1822
+ (files) => {
1823
+ if (uploadRef.current === void 0) return;
1824
+ for (const file of files) {
1825
+ if (!admits(file.size, file.name)) continue;
1826
+ const preview = previewFor(file);
1827
+ start(file, {
1828
+ name: file.name,
1829
+ kind: file.type.startsWith("image/") ? "image" : "file",
1830
+ previewUrl: preview
1831
+ });
1832
+ }
1833
+ },
1834
+ [admits, start]
1835
+ );
1836
+ const addPastedText = useCallback(
1837
+ (text) => {
1838
+ if (uploadRef.current === void 0) return false;
1839
+ if (!shouldAttachPaste(text)) return false;
1840
+ pasteCount.current += 1;
1841
+ const name = pastedTextFilename(pasteCount.current);
1842
+ const blob = new Blob([text], { type: "text/plain" });
1843
+ if (!admits(blob.size, "That paste")) {
1844
+ pasteCount.current -= 1;
1845
+ return false;
1846
+ }
1847
+ start(blob, { name, kind: "file", pastedText: text, charCount: text.length });
1848
+ return true;
1849
+ },
1850
+ [admits, start]
1851
+ );
1852
+ const addGif = useCallback(
1853
+ (gif) => {
1854
+ const attach = gifRef.current;
1855
+ if (attach === void 0) return;
1856
+ if (listRef.current.length >= maxPerComment) {
1857
+ fail(`You can attach up to ${String(maxPerComment)} files to a comment.`);
1858
+ return;
1859
+ }
1860
+ const localId = nextLocalId();
1861
+ commit((current) => [
1862
+ ...current,
1863
+ {
1864
+ localId,
1865
+ name: gif.title.length > 0 ? gif.title : "GIF",
1866
+ sizeBytes: 0,
1867
+ kind: "gif",
1868
+ state: "uploading",
1869
+ progress: 0,
1870
+ previewUrl: gif.stillUrl,
1871
+ ...gif.height > 0 ? { aspectRatio: gif.width / gif.height } : {}
1872
+ }
1873
+ ]);
1874
+ void attach(gif.id).then((ticket) => {
1875
+ patch(localId, {
1876
+ state: "ready",
1877
+ progress: 1,
1878
+ ticketId: ticket.id,
1879
+ name: ticket.name,
1880
+ sizeBytes: ticket.sizeBytes,
1881
+ kind: "gif",
1882
+ ...ticket.aspectRatio === null ? {} : { aspectRatio: ticket.aspectRatio }
1883
+ });
1884
+ }).catch((err) => {
1885
+ const message = err instanceof Error && err.message.length > 0 ? err.message : "That GIF couldn't be attached.";
1886
+ patch(localId, { state: "failed", error: message });
1887
+ fail(message);
1888
+ });
1889
+ },
1890
+ [commit, fail, maxPerComment, patch]
1891
+ );
1892
+ const remove = useCallback(
1893
+ (localId) => {
1894
+ controllers.current.get(localId)?.abort();
1895
+ controllers.current.delete(localId);
1896
+ releasePreview(listRef.current.find((a) => a.localId === localId)?.previewUrl);
1897
+ commit((current) => current.filter((a) => a.localId !== localId));
1898
+ },
1899
+ [commit, releasePreview]
1900
+ );
1901
+ const insertAsText = useCallback(
1902
+ (localId) => {
1903
+ const found = listRef.current.find((a) => a.localId === localId);
1904
+ const text = found?.pastedText ?? null;
1905
+ if (text === null) return null;
1906
+ controllers.current.get(localId)?.abort();
1907
+ controllers.current.delete(localId);
1908
+ releasePreview(found?.previewUrl);
1909
+ commit((current) => current.filter((a) => a.localId !== localId));
1910
+ return text;
1911
+ },
1912
+ [commit, releasePreview]
1913
+ );
1914
+ const clear = useCallback(() => {
1915
+ for (const controller of controllers.current.values()) controller.abort();
1916
+ controllers.current.clear();
1917
+ for (const attachment of listRef.current) releasePreview(attachment.previewUrl);
1918
+ commit(() => []);
1919
+ }, [commit, releasePreview]);
1920
+ return {
1921
+ attachments,
1922
+ ticketIds: attachments.filter((a) => a.state === "ready" && a.ticketId !== void 0).map((a) => a.ticketId ?? ""),
1923
+ isUploading: attachments.some((a) => a.state === "uploading"),
1924
+ addFiles,
1925
+ addPastedText,
1926
+ addGif,
1927
+ remove,
1928
+ insertAsText,
1929
+ clear
1930
+ };
1931
+ }
1932
+ function useAttachmentUrl(attachmentId, resolve) {
1933
+ const [url, setUrl] = useState(null);
1934
+ const [failed, setFailed] = useState(false);
1935
+ const [loading, setLoading] = useState(resolve !== void 0);
1936
+ const resolveRef = useRef(resolve);
1937
+ resolveRef.current = resolve;
1938
+ const enabled = resolve !== void 0;
1939
+ useEffect(() => {
1940
+ const current = resolveRef.current;
1941
+ if (current === void 0) return;
1942
+ let live = true;
1943
+ setUrl(null);
1944
+ setFailed(false);
1945
+ setLoading(true);
1946
+ void current(attachmentId).then((next) => {
1947
+ if (!live) return;
1948
+ if (next === null || next.length === 0) setFailed(true);
1949
+ else setUrl(next);
1950
+ setLoading(false);
1951
+ }).catch(() => {
1952
+ if (!live) return;
1953
+ setFailed(true);
1954
+ setLoading(false);
1955
+ });
1956
+ return () => {
1957
+ live = false;
1958
+ };
1959
+ }, [attachmentId, enabled]);
1960
+ return { url, failed, loading };
1961
+ }
1962
+ var QUIET_ICON = cn(
1963
+ "inline-flex shrink-0 items-center justify-center border-0 bg-transparent",
1964
+ "text-[rgb(var(--text-tertiary))]",
1965
+ "transition-[background-color,color] duration-[var(--dur-fast)] ease-[var(--ease-out)]",
1966
+ "motion-reduce:transition-none",
1967
+ "hover:bg-[rgb(var(--surface-overlay))] hover:text-[rgb(var(--foreground))]",
1968
+ "focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)]",
1969
+ "disabled:cursor-default disabled:opacity-60"
1970
+ );
1971
+ var QUIET_ICON_BOX = {
1972
+ minBlockSize: "var(--space-6)",
1973
+ minInlineSize: "var(--space-6)",
1974
+ borderRadius: "var(--radius-xs)"
1975
+ };
1976
+ function formatCharacterCount(count) {
1977
+ const n = Math.max(0, Math.round(count));
1978
+ return `${n.toLocaleString("en-US")} ${n === 1 ? "character" : "characters"}`;
1979
+ }
1980
+ function formatAttachmentSize(bytes) {
1981
+ if (!Number.isFinite(bytes) || bytes <= 0) return "";
1982
+ if (bytes < 1024) return `${String(bytes)} B`;
1983
+ if (bytes < 1024 * 1024) return `${String(Math.round(bytes / 1024))} KB`;
1984
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1985
+ }
1986
+ function usePrefersReducedMotion() {
1987
+ const [reduced, setReduced] = useState(false);
1988
+ useEffect(() => {
1989
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
1990
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
1991
+ setReduced(query.matches);
1992
+ const onChange = (event) => {
1993
+ setReduced(event.matches);
1994
+ };
1995
+ query.addEventListener("change", onChange);
1996
+ return () => {
1997
+ query.removeEventListener("change", onChange);
1998
+ };
1999
+ }, []);
2000
+ return reduced;
2001
+ }
2002
+ function AttachmentTray({
2003
+ attachments,
2004
+ onRemove,
2005
+ onInsertAsText,
2006
+ className
2007
+ }) {
2008
+ if (attachments.length === 0) return null;
2009
+ return /* @__PURE__ */ jsx(
2010
+ "ul",
2011
+ {
2012
+ className: cn("flex list-none flex-wrap gap-[var(--space-2)] p-0", className),
2013
+ style: { marginBlockStart: "var(--space-2)" },
2014
+ children: attachments.map((attachment) => /* @__PURE__ */ jsx(
2015
+ AttachmentChip,
2016
+ {
2017
+ attachment,
2018
+ onRemove,
2019
+ ...onInsertAsText === void 0 ? {} : { onInsertAsText }
2020
+ },
2021
+ attachment.localId
2022
+ ))
2023
+ }
2024
+ );
2025
+ }
2026
+ function AttachmentChip({
2027
+ attachment,
2028
+ onRemove,
2029
+ onInsertAsText
2030
+ }) {
2031
+ const uploading = attachment.state === "uploading";
2032
+ const failed = attachment.state === "failed";
2033
+ const restorable = onInsertAsText !== void 0 && attachment.pastedText !== void 0;
2034
+ return /* @__PURE__ */ jsxs(
2035
+ "li",
2036
+ {
2037
+ className: cn(
2038
+ "flex min-w-0 items-center gap-[var(--space-2)] border px-[var(--space-2)] py-[var(--space-1)]",
2039
+ "text-[12px]"
2040
+ ),
2041
+ style: {
2042
+ borderRadius: "var(--radius-sm)",
2043
+ borderColor: failed ? "rgb(var(--destructive))" : "rgb(var(--border))",
2044
+ // `--muted`, not `--surface-overlay*`. CLAUDE.md §2 names chips as
2045
+ // exactly what the flat opaque grey is for; the translucent overlay
2046
+ // tints wash out when they stack on another layer, and a chip inside a
2047
+ // composer inside a card is two layers deep already.
2048
+ background: "rgb(var(--muted))",
2049
+ // The chip's height cannot depend on whether it holds a thumbnail, a
2050
+ // progress bar or an error, or the composer would jump on every state
2051
+ // change. One declared minimum, three contents.
2052
+ minBlockSize: "var(--ctrl-md)",
2053
+ maxInlineSize: "calc(var(--space-12) * 5)"
2054
+ },
2055
+ children: [
2056
+ /* @__PURE__ */ jsx(AttachmentChipThumb, { attachment }),
2057
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
2058
+ /* @__PURE__ */ jsx("span", { className: "truncate", style: { color: "rgb(var(--foreground))" }, children: attachment.name }),
2059
+ /* @__PURE__ */ jsx(
2060
+ "span",
2061
+ {
2062
+ className: "flex items-center gap-[var(--space-2)] tabular-nums",
2063
+ style: {
2064
+ color: failed ? "rgb(var(--destructive))" : "rgb(var(--text-tertiary))",
2065
+ // Reserved so the second line exists before there is anything on it.
2066
+ minBlockSize: "var(--space-4)"
2067
+ },
2068
+ children: failed ? /* @__PURE__ */ jsx("span", { className: "truncate", children: attachment.error ?? "Upload failed" }) : uploading ? /* @__PURE__ */ jsx(AttachmentProgress, { value: attachment.progress, label: attachment.name }) : attachment.charCount === void 0 ? /* @__PURE__ */ jsx("span", { children: formatAttachmentSize(attachment.sizeBytes) }) : (
2069
+ // D5, verbatim: "the character count shown on the chip". A byte
2070
+ // size would be the wrong unit here — the person pasted characters,
2071
+ // and for anything non-ASCII the two numbers differ.
2072
+ /* @__PURE__ */ jsx("span", { children: formatCharacterCount(attachment.charCount) })
2073
+ )
2074
+ }
2075
+ )
2076
+ ] }),
2077
+ restorable ? /* @__PURE__ */ jsx(
2078
+ Button,
2079
+ {
2080
+ type: "button",
2081
+ variant: "secondary",
2082
+ size: "sm",
2083
+ onClick: () => {
2084
+ onInsertAsText?.(attachment.localId);
2085
+ },
2086
+ children: "Insert as text instead"
2087
+ }
2088
+ ) : null,
2089
+ /* @__PURE__ */ jsx(
2090
+ "button",
2091
+ {
2092
+ type: "button",
2093
+ "aria-label": uploading ? `Cancel ${attachment.name}` : `Remove ${attachment.name}`,
2094
+ className: QUIET_ICON,
2095
+ style: QUIET_ICON_BOX,
2096
+ onClick: () => {
2097
+ onRemove(attachment.localId);
2098
+ },
2099
+ children: /* @__PURE__ */ jsx(X, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" })
2100
+ }
2101
+ )
2102
+ ]
2103
+ }
2104
+ );
2105
+ }
2106
+ function AttachmentChipThumb({ attachment }) {
2107
+ const visual = attachment.kind === "image" || attachment.kind === "gif";
2108
+ return /* @__PURE__ */ jsx(
2109
+ "span",
2110
+ {
2111
+ className: "flex shrink-0 items-center justify-center overflow-hidden border",
2112
+ style: {
2113
+ inlineSize: "var(--space-6)",
2114
+ blockSize: "var(--space-6)",
2115
+ borderRadius: "var(--radius-xs)",
2116
+ borderColor: "rgb(var(--border))",
2117
+ background: "rgb(var(--muted))",
2118
+ color: "rgb(var(--text-tertiary))"
2119
+ },
2120
+ children: visual && attachment.previewUrl !== void 0 ? /* @__PURE__ */ jsx("img", { src: attachment.previewUrl, alt: "", className: "h-full w-full object-cover" }) : visual ? /* @__PURE__ */ jsx(Image, { size: 12, strokeWidth: 1.5, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(FileText, { size: 12, strokeWidth: 1.5, "aria-hidden": "true" })
2121
+ }
2122
+ );
2123
+ }
2124
+ function AttachmentProgress({ value, label }) {
2125
+ const percent = Math.round(Math.max(0, Math.min(1, value)) * 100);
2126
+ return /* @__PURE__ */ jsx(
2127
+ "span",
2128
+ {
2129
+ role: "progressbar",
2130
+ "aria-label": `Uploading ${label}`,
2131
+ "aria-valuemin": 0,
2132
+ "aria-valuemax": 100,
2133
+ "aria-valuenow": percent,
2134
+ className: "block overflow-hidden",
2135
+ style: {
2136
+ inlineSize: "calc(var(--space-12) * 2)",
2137
+ blockSize: "var(--space-1)",
2138
+ borderRadius: "var(--radius-full)",
2139
+ background: "rgb(var(--muted))"
2140
+ },
2141
+ children: /* @__PURE__ */ jsx(
2142
+ "span",
2143
+ {
2144
+ className: "block transition-[inline-size] duration-[var(--dur-base)] ease-[var(--ease-out)] motion-reduce:transition-none",
2145
+ style: {
2146
+ inlineSize: `${String(percent)}%`,
2147
+ blockSize: "100%",
2148
+ background: "rgb(var(--accent))"
2149
+ }
2150
+ }
2151
+ )
2152
+ }
2153
+ );
2154
+ }
2155
+ function AttachmentButton({
2156
+ onFiles,
2157
+ accept,
2158
+ disabled = false,
2159
+ label = "Attach a file"
2160
+ }) {
2161
+ const inputRef = useRef(null);
2162
+ const inputId = useId();
2163
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2164
+ /* @__PURE__ */ jsx(
2165
+ "input",
2166
+ {
2167
+ ref: inputRef,
2168
+ id: inputId,
2169
+ type: "file",
2170
+ multiple: true,
2171
+ hidden: true,
2172
+ ...accept === void 0 ? {} : { accept },
2173
+ onChange: (event) => {
2174
+ const { files } = event.currentTarget;
2175
+ if (files !== null && files.length > 0) onFiles(files);
2176
+ event.currentTarget.value = "";
2177
+ }
2178
+ }
2179
+ ),
2180
+ /* @__PURE__ */ jsx(
2181
+ "button",
2182
+ {
2183
+ type: "button",
2184
+ "aria-label": label,
2185
+ className: QUIET_ICON,
2186
+ style: QUIET_ICON_BOX,
2187
+ disabled,
2188
+ onClick: () => {
2189
+ inputRef.current?.click();
2190
+ },
2191
+ children: /* @__PURE__ */ jsx(Paperclip, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })
2192
+ }
2193
+ )
2194
+ ] });
2195
+ }
2196
+ function AttachmentDropZone({
2197
+ onFiles,
2198
+ disabled = false,
2199
+ children,
2200
+ className
2201
+ }) {
2202
+ const depth = useRef(0);
2203
+ const [over, setOver] = useState(false);
2204
+ const onDragEnter = useCallback(
2205
+ (event) => {
2206
+ if (disabled || !Array.from(event.dataTransfer.types).includes("Files")) return;
2207
+ depth.current += 1;
2208
+ setOver(true);
2209
+ },
2210
+ [disabled]
2211
+ );
2212
+ const onDragLeave = useCallback(() => {
2213
+ depth.current = Math.max(0, depth.current - 1);
2214
+ if (depth.current === 0) setOver(false);
2215
+ }, []);
2216
+ return /* @__PURE__ */ jsxs(
2217
+ "div",
2218
+ {
2219
+ className: cn("relative", className),
2220
+ onDragEnter,
2221
+ onDragOver: (event) => {
2222
+ if (!disabled && Array.from(event.dataTransfer.types).includes("Files")) {
2223
+ event.preventDefault();
2224
+ }
2225
+ },
2226
+ onDragLeave,
2227
+ onDrop: (event) => {
2228
+ depth.current = 0;
2229
+ setOver(false);
2230
+ if (disabled) return;
2231
+ const { files } = event.dataTransfer;
2232
+ if (files.length === 0) return;
2233
+ event.preventDefault();
2234
+ onFiles(files);
2235
+ },
2236
+ children: [
2237
+ children,
2238
+ /* @__PURE__ */ jsx(
2239
+ "div",
2240
+ {
2241
+ "aria-hidden": "true",
2242
+ className: "pointer-events-none absolute inset-0 flex items-center justify-center border border-dashed transition-opacity duration-[var(--dur-fast)] ease-[var(--ease-out)] motion-reduce:transition-none",
2243
+ style: {
2244
+ visibility: over ? "visible" : "hidden",
2245
+ opacity: over ? 1 : 0,
2246
+ borderRadius: "var(--radius-md)",
2247
+ borderColor: "rgb(var(--accent))",
2248
+ background: "rgb(var(--accent-subtle))",
2249
+ color: "rgb(var(--accent-ink))"
2250
+ },
2251
+ children: "Drop to attach"
2252
+ }
2253
+ )
2254
+ ]
2255
+ }
2256
+ );
2257
+ }
2258
+ function GifPicker({ search, onPick, className }) {
2259
+ const [query, setQuery] = useState("");
2260
+ const [results, setResults] = useState([]);
2261
+ const [state, setState] = useState("idle");
2262
+ const reduced = usePrefersReducedMotion();
2263
+ const listId = useId();
2264
+ const searchRef = useRef(search);
2265
+ searchRef.current = search;
2266
+ useEffect(() => {
2267
+ const trimmed = query.trim();
2268
+ if (trimmed.length === 0) {
2269
+ setResults([]);
2270
+ setState("idle");
2271
+ return;
2272
+ }
2273
+ let live = true;
2274
+ setState("searching");
2275
+ const timer = setTimeout(() => {
2276
+ void searchRef.current(trimmed).then((next) => {
2277
+ if (!live) return;
2278
+ setResults(next);
2279
+ setState("idle");
2280
+ }).catch(() => {
2281
+ if (!live) return;
2282
+ setResults([]);
2283
+ setState("error");
2284
+ });
2285
+ }, GIF_SEARCH_DEBOUNCE_MS);
2286
+ return () => {
2287
+ live = false;
2288
+ clearTimeout(timer);
2289
+ };
2290
+ }, [query]);
2291
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-[var(--space-3)]", className), children: [
2292
+ /* @__PURE__ */ jsx(
2293
+ Input,
2294
+ {
2295
+ value: query,
2296
+ onChange: (event) => {
2297
+ setQuery(event.currentTarget.value);
2298
+ },
2299
+ placeholder: "Search GIFs",
2300
+ "aria-label": "Search GIFs",
2301
+ "aria-controls": listId,
2302
+ leadingAffix: /* @__PURE__ */ jsx(Search, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })
2303
+ }
2304
+ ),
2305
+ /* @__PURE__ */ jsxs(
2306
+ "div",
2307
+ {
2308
+ id: listId,
2309
+ className: "grid grid-cols-3 gap-[var(--space-2)] overflow-y-auto",
2310
+ style: {
2311
+ // Declared, so the panel is the same size empty, searching and full.
2312
+ blockSize: "calc(var(--space-12) * 6)"
2313
+ },
2314
+ children: [
2315
+ results.map((gif) => /* @__PURE__ */ jsx(
2316
+ "button",
2317
+ {
2318
+ type: "button",
2319
+ onClick: () => {
2320
+ onPick(gif);
2321
+ },
2322
+ className: "block overflow-hidden border transition-[border-color] duration-[var(--dur-fast)] ease-[var(--ease-out)] hover:border-[rgb(var(--border-strong))] focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)] motion-reduce:transition-none",
2323
+ style: {
2324
+ borderRadius: "var(--radius-sm)",
2325
+ borderColor: "rgb(var(--border))",
2326
+ background: "rgb(var(--muted))",
2327
+ aspectRatio: gif.height > 0 ? String(gif.width / gif.height) : "1"
2328
+ },
2329
+ children: reduced ? /* @__PURE__ */ jsx("img", { src: gif.stillUrl, alt: gif.title, className: "h-full w-full object-cover" }) : /* @__PURE__ */ jsx(
2330
+ "video",
2331
+ {
2332
+ src: gif.previewUrl,
2333
+ poster: gif.stillUrl,
2334
+ autoPlay: true,
2335
+ loop: true,
2336
+ muted: true,
2337
+ playsInline: true,
2338
+ "aria-label": gif.title,
2339
+ className: "h-full w-full object-cover"
2340
+ }
2341
+ )
2342
+ },
2343
+ gif.id
2344
+ )),
2345
+ results.length === 0 ? /* @__PURE__ */ jsx(
2346
+ "p",
2347
+ {
2348
+ className: "col-span-3 self-center text-center text-[12px]",
2349
+ style: { color: "rgb(var(--text-tertiary))" },
2350
+ children: state === "error" ? "GIF search isn't available right now." : state === "searching" ? "Searching\u2026" : query.trim().length === 0 ? "Type to search." : "No GIFs matched."
2351
+ }
2352
+ ) : null
2353
+ ]
2354
+ }
2355
+ ),
2356
+ /* @__PURE__ */ jsx(
2357
+ "p",
2358
+ {
2359
+ className: "text-[11px] uppercase",
2360
+ style: { color: "rgb(var(--text-tertiary))", letterSpacing: "0.06em" },
2361
+ children: "Powered by GIPHY"
2362
+ }
2363
+ )
2364
+ ] });
2365
+ }
2366
+ function AttachmentGif({ attachment, resolveUrl, className }) {
2367
+ const reduced = usePrefersReducedMotion();
2368
+ const { url, failed, loading } = useAttachmentUrl(attachment.id, resolveUrl);
2369
+ const [playing, setPlaying] = useState(false);
2370
+ const videoRef = useRef(null);
2371
+ useEffect(() => {
2372
+ if (reduced) setPlaying(false);
2373
+ }, [reduced]);
2374
+ const shouldPlay = !reduced || playing;
2375
+ if (url === null || failed) {
2376
+ return /* @__PURE__ */ jsx(
2377
+ AttachmentPlaceholder,
2378
+ {
2379
+ attachment,
2380
+ state: loading && !failed ? "loading" : "broken",
2381
+ className
2382
+ }
2383
+ );
2384
+ }
2385
+ return /* @__PURE__ */ jsxs(
2386
+ "span",
2387
+ {
2388
+ className: cn("relative block overflow-hidden border", className),
2389
+ style: {
2390
+ aspectRatio: String(attachment.aspectRatio ?? 16 / 10),
2391
+ borderRadius: "var(--thread-inner-radius, var(--radius-sm))",
2392
+ borderColor: "rgb(var(--border))",
2393
+ background: "rgb(var(--surface-inset))",
2394
+ inlineSize: "calc(var(--space-12) * 3)",
2395
+ minInlineSize: "calc(var(--space-12) * 3)"
2396
+ },
2397
+ children: [
2398
+ /* @__PURE__ */ jsx(
2399
+ "video",
2400
+ {
2401
+ ref: videoRef,
2402
+ src: url,
2403
+ loop: true,
2404
+ muted: true,
2405
+ playsInline: true,
2406
+ autoPlay: shouldPlay,
2407
+ "aria-label": attachment.name,
2408
+ className: "h-full w-full object-cover"
2409
+ }
2410
+ ),
2411
+ shouldPlay ? null : /* @__PURE__ */ jsx(
2412
+ "button",
2413
+ {
2414
+ type: "button",
2415
+ "aria-label": `Play ${attachment.name}`,
2416
+ onClick: () => {
2417
+ setPlaying(true);
2418
+ void videoRef.current?.play();
2419
+ },
2420
+ className: "absolute inset-0 flex items-center justify-center focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)]",
2421
+ style: { background: "rgb(var(--media-scrim-mid))", color: "rgb(var(--media-on-scrim))" },
2422
+ children: /* @__PURE__ */ jsx(Play, { size: 20, strokeWidth: 1.75, "aria-hidden": "true" })
2423
+ }
2424
+ )
2425
+ ]
2426
+ }
2427
+ );
2428
+ }
2429
+ function AttachmentPlaceholder({
2430
+ attachment,
2431
+ state = "broken",
2432
+ className
2433
+ }) {
2434
+ return /* @__PURE__ */ jsx(
2435
+ "span",
2436
+ {
2437
+ className: cn("flex items-center justify-center overflow-hidden border", className),
2438
+ style: {
2439
+ aspectRatio: String(attachment.aspectRatio ?? 16 / 10),
2440
+ borderRadius: "var(--thread-inner-radius, var(--radius-sm))",
2441
+ borderColor: "rgb(var(--border))",
2442
+ background: "rgb(var(--surface-inset))",
2443
+ color: "rgb(var(--text-tertiary))",
2444
+ inlineSize: "calc(var(--space-12) * 3)",
2445
+ minInlineSize: "calc(var(--space-12) * 3)"
2446
+ },
2447
+ "aria-label": state === "loading" ? `Loading ${attachment.name}` : void 0,
2448
+ children: state === "broken" ? /* @__PURE__ */ jsx(ImageOff, { size: 14, strokeWidth: 1.5, "aria-hidden": "true" }) : null
2449
+ }
2450
+ );
2451
+ }
2452
+ function AttachmentImage({
2453
+ attachment,
2454
+ onOpen,
2455
+ resolveUrl,
2456
+ className
2457
+ }) {
2458
+ const reduced = usePrefersReducedMotion();
2459
+ const [broken, setBroken] = useState(false);
2460
+ const [playing, setPlaying] = useState(false);
2461
+ const [capturedSrc, setCapturedSrc] = useState(null);
2462
+ const imageRef = useRef(null);
2463
+ const canvasRef = useRef(null);
2464
+ const capturedSrcRef = useRef(null);
2465
+ const resolved = useAttachmentUrl(attachment.id, attachment.url === void 0 ? resolveUrl : void 0);
2466
+ const src = attachment.url ?? resolved.url ?? void 0;
2467
+ const uploadedGif = attachment.mime?.toLowerCase() === "image/gif";
2468
+ const captureStill = useCallback(
2469
+ (image) => {
2470
+ if (!uploadedGif || src === void 0 || capturedSrcRef.current === src) return;
2471
+ const canvas = canvasRef.current;
2472
+ const width = image.naturalWidth;
2473
+ const height = image.naturalHeight;
2474
+ if (canvas === null || width <= 0 || height <= 0) return;
2475
+ canvas.width = width;
2476
+ canvas.height = height;
2477
+ const context = canvas.getContext("2d");
2478
+ if (context === null) return;
2479
+ context.drawImage(image, 0, 0, width, height);
2480
+ capturedSrcRef.current = src;
2481
+ setCapturedSrc(src);
2482
+ },
2483
+ [src, uploadedGif]
2484
+ );
2485
+ useEffect(() => {
2486
+ if (!reduced) return;
2487
+ setPlaying(false);
2488
+ const image = imageRef.current;
2489
+ if (image?.complete === true) captureStill(image);
2490
+ }, [captureStill, reduced]);
2491
+ const missing = broken || resolved.failed || src === void 0;
2492
+ const pending = resolved.loading && !broken && !resolved.failed;
2493
+ const interactive = onOpen !== void 0 && !missing;
2494
+ const showStill = uploadedGif && reduced && !playing && !missing;
2495
+ const body = pending ? null : missing ? /* @__PURE__ */ jsx(
2496
+ "span",
2497
+ {
2498
+ className: "flex h-full w-full items-center justify-center",
2499
+ style: { color: "rgb(var(--text-tertiary))" },
2500
+ children: /* @__PURE__ */ jsx(ImageOff, { size: 14, strokeWidth: 1.5, "aria-hidden": "true" })
2501
+ }
2502
+ ) : /* @__PURE__ */ jsxs("span", { className: "relative block h-full w-full", children: [
2503
+ /* @__PURE__ */ jsx(
2504
+ "img",
2505
+ {
2506
+ ref: imageRef,
2507
+ src,
2508
+ alt: attachment.name,
2509
+ loading: "lazy",
2510
+ className: "h-full w-full object-cover",
2511
+ style: { visibility: showStill ? "hidden" : "visible" },
2512
+ onLoad: (event) => {
2513
+ captureStill(event.currentTarget);
2514
+ },
2515
+ onError: () => {
2516
+ setBroken(true);
2517
+ }
2518
+ }
2519
+ ),
2520
+ uploadedGif ? /* @__PURE__ */ jsx(
2521
+ "canvas",
2522
+ {
2523
+ ref: canvasRef,
2524
+ "aria-hidden": "true",
2525
+ hidden: !showStill || capturedSrc !== src,
2526
+ className: "absolute inset-0 h-full w-full object-cover"
2527
+ }
2528
+ ) : null
2529
+ ] });
2530
+ return /* @__PURE__ */ jsxs(
2531
+ "span",
2532
+ {
2533
+ className: cn(
2534
+ "relative block overflow-hidden border",
2535
+ "transition-[border-color] duration-[var(--dur-fast)] ease-[var(--ease-out)] motion-reduce:transition-none",
2536
+ interactive && !showStill && "hover:border-[rgb(var(--border-strong))]",
2537
+ (interactive || showStill) && "focus-within:outline-none focus-within:[box-shadow:var(--ring-focus)]",
2538
+ className
2539
+ ),
2540
+ style: {
2541
+ // Declared up front so the row cannot reflow when the bytes land.
2542
+ aspectRatio: String(attachment.aspectRatio ?? 16 / 10),
2543
+ borderRadius: "var(--thread-inner-radius, var(--radius-sm))",
2544
+ borderColor: "rgb(var(--border))",
2545
+ background: "rgb(var(--surface-inset))",
2546
+ inlineSize: "calc(var(--space-12) * 3)",
2547
+ minInlineSize: "calc(var(--space-12) * 3)"
2548
+ },
2549
+ children: [
2550
+ interactive ? /* @__PURE__ */ jsx(
2551
+ "button",
2552
+ {
2553
+ type: "button",
2554
+ "aria-label": `Open ${attachment.name}`,
2555
+ "aria-hidden": showStill ? true : void 0,
2556
+ disabled: showStill,
2557
+ onClick: () => {
2558
+ onOpen?.(attachment);
2559
+ },
2560
+ className: "block h-full w-full focus-visible:outline-none",
2561
+ children: body
2562
+ }
2563
+ ) : body,
2564
+ showStill ? /* @__PURE__ */ jsx(
2565
+ "button",
2566
+ {
2567
+ type: "button",
2568
+ "aria-label": `Play ${attachment.name}`,
2569
+ onClick: () => {
2570
+ setPlaying(true);
2571
+ },
2572
+ className: "absolute inset-0 flex items-center justify-center focus-visible:outline-none",
2573
+ style: { background: "rgb(var(--media-scrim-mid))", color: "rgb(var(--media-on-scrim))" },
2574
+ children: /* @__PURE__ */ jsx(Play, { size: 20, strokeWidth: 1.75, "aria-hidden": "true" })
2575
+ }
2576
+ ) : null
2577
+ ]
2578
+ }
2579
+ );
2580
+ }
2581
+ function AttachmentLightbox({ attachment, onClose, resolveUrl }) {
2582
+ const closeRef = useRef(null);
2583
+ const id = attachment?.id ?? "";
2584
+ const { url, failed } = useAttachmentUrl(id, attachment === null ? void 0 : resolveUrl);
2585
+ const surfaceRef = useRef(null);
2586
+ useEffect(() => {
2587
+ if (attachment === null) return;
2588
+ closeRef.current?.focus();
2589
+ const onKey = (event) => {
2590
+ if (event.key === "Escape") {
2591
+ onClose();
2592
+ return;
2593
+ }
2594
+ if (event.key !== "Tab") return;
2595
+ const root = surfaceRef.current;
2596
+ if (root === null) return;
2597
+ const focusable = Array.from(
2598
+ root.querySelectorAll(
2599
+ 'a[href], button:not([disabled]), video[controls], [tabindex]:not([tabindex="-1"])'
2600
+ )
2601
+ );
2602
+ if (focusable.length === 0) {
2603
+ event.preventDefault();
2604
+ return;
2605
+ }
2606
+ const first = focusable[0];
2607
+ const last = focusable[focusable.length - 1];
2608
+ const active = document.activeElement;
2609
+ if (event.shiftKey && (active === first || !root.contains(active))) {
2610
+ event.preventDefault();
2611
+ last?.focus();
2612
+ } else if (!event.shiftKey && (active === last || !root.contains(active))) {
2613
+ event.preventDefault();
2614
+ first?.focus();
2615
+ }
2616
+ };
2617
+ document.addEventListener("keydown", onKey);
2618
+ return () => {
2619
+ document.removeEventListener("keydown", onKey);
2620
+ };
2621
+ }, [attachment, onClose]);
2622
+ if (attachment === null) return null;
2623
+ return /* @__PURE__ */ jsx(
2624
+ "div",
2625
+ {
2626
+ role: "dialog",
2627
+ "aria-modal": "true",
2628
+ "aria-label": attachment.name,
2629
+ className: "fixed inset-0 z-50 flex items-center justify-center p-[var(--space-6)]",
2630
+ style: { background: "rgb(var(--media-scrim-strong))" },
2631
+ onClick: onClose,
2632
+ children: /* @__PURE__ */ jsxs(
2633
+ "div",
2634
+ {
2635
+ ref: surfaceRef,
2636
+ className: "relative flex max-h-full max-w-full items-center justify-center",
2637
+ onClick: (event) => {
2638
+ event.stopPropagation();
2639
+ },
2640
+ children: [
2641
+ url === null || failed ? /* @__PURE__ */ jsx("p", { style: { color: "rgb(var(--media-on-scrim))" }, children: failed ? "This file isn't available." : "Loading\u2026" }) : attachment.kind === "gif" ? /* @__PURE__ */ jsx(
2642
+ "video",
2643
+ {
2644
+ src: url,
2645
+ controls: true,
2646
+ loop: true,
2647
+ muted: true,
2648
+ playsInline: true,
2649
+ "aria-label": attachment.name,
2650
+ className: "max-h-full max-w-full"
2651
+ }
2652
+ ) : /* @__PURE__ */ jsx("img", { src: url, alt: attachment.name, className: "max-h-full max-w-full object-contain" }),
2653
+ /* @__PURE__ */ jsx("span", { className: "absolute", style: { insetBlockStart: 0, insetInlineEnd: 0 }, children: /* @__PURE__ */ jsx(
2654
+ "button",
2655
+ {
2656
+ ref: closeRef,
2657
+ type: "button",
2658
+ "aria-label": "Close",
2659
+ className: QUIET_ICON,
2660
+ style: { ...QUIET_ICON_BOX, color: "rgb(var(--media-on-scrim))" },
2661
+ onClick: onClose,
2662
+ children: /* @__PURE__ */ jsx(X, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })
2663
+ }
2664
+ ) })
2665
+ ]
2666
+ }
2667
+ )
2668
+ }
2669
+ );
2670
+ }
2671
+ var ATTACHMENT_ACCEPT = ".png,.jpg,.jpeg,.webp,.gif,.pdf,.txt,.md,.csv,.json";
2672
+ var RichThreadComposerImpl = lazy(() => import('./rich-composer-impl-5NO443A6.js'));
2673
+ var RICH_FALLBACK_NOTICE = "Rich editor unavailable, plain editor loaded";
2674
+ function ComposerNotice({ children }) {
2675
+ return /* @__PURE__ */ jsxs(
2676
+ "p",
2677
+ {
2678
+ className: "flex items-center gap-[var(--space-1)] text-[12px]",
2679
+ style: { color: "rgb(var(--text-tertiary))" },
2680
+ children: [
2681
+ /* @__PURE__ */ jsx(Info, { size: 12, strokeWidth: 1.75, "aria-hidden": "true" }),
2682
+ /* @__PURE__ */ jsx("span", { children })
2683
+ ]
2684
+ }
2685
+ );
2686
+ }
2687
+ var ChunkBoundary = class extends Component {
2688
+ state = { failed: false };
2689
+ static getDerivedStateFromError() {
2690
+ return { failed: true };
2691
+ }
2692
+ componentDidCatch(error, _info) {
2693
+ this.props.onError(error);
2694
+ }
2695
+ render() {
2696
+ return this.state.failed ? this.props.fallback : this.props.children;
2697
+ }
2698
+ };
2699
+ function RichThreadComposer({
2700
+ mentionItems,
2701
+ uploadAttachment,
2702
+ attachGif,
2703
+ searchGifs,
2704
+ attachmentAccept,
2705
+ attachmentMaxPerComment,
2706
+ attachmentMaxFileBytes,
2707
+ attachmentMaxTotalBytes,
2708
+ ...props
2709
+ }) {
2710
+ const [failed, setFailed] = useState(false);
2711
+ const [announcement, setAnnouncement] = useState({ text: "", seq: 0 });
2712
+ const announce = useCallback((text) => {
2713
+ setAnnouncement((prev) => ({ text, seq: prev.seq + 1 }));
2714
+ }, []);
2715
+ const onError = useCallback(
2716
+ (error) => {
2717
+ console.warn(
2718
+ "rich composer: the editor chunk could not load; the plain composer is rendered instead",
2719
+ error
2720
+ );
2721
+ setFailed(true);
2722
+ announce(RICH_FALLBACK_NOTICE);
2723
+ },
2724
+ [announce]
2725
+ );
2726
+ const spoken = announcement.text === "" ? "" : announcement.seq % 2 === 0 ? `${announcement.text}\xA0` : announcement.text;
2727
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--space-1)]", children: [
2728
+ /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: spoken }),
2729
+ failed ? /* @__PURE__ */ jsxs(Fragment, { children: [
2730
+ /* @__PURE__ */ jsx(ComposerNotice, { children: RICH_FALLBACK_NOTICE }),
2731
+ /* @__PURE__ */ jsx(ThreadComposer, { ...props })
2732
+ ] }) : /* @__PURE__ */ jsx(ChunkBoundary, { onError, fallback: /* @__PURE__ */ jsx(ThreadComposer, { ...props }), children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ThreadComposer, { ...props, disabled: true }), children: /* @__PURE__ */ jsx(
2733
+ RichThreadComposerImpl,
2734
+ {
2735
+ ...props,
2736
+ announce,
2737
+ ...mentionItems === void 0 ? {} : { mentionItems },
2738
+ ...uploadAttachment === void 0 ? {} : { uploadAttachment },
2739
+ ...attachGif === void 0 ? {} : { attachGif },
2740
+ ...searchGifs === void 0 ? {} : { searchGifs },
2741
+ ...attachmentAccept === void 0 ? {} : { attachmentAccept },
2742
+ ...attachmentMaxPerComment === void 0 ? {} : { attachmentMaxPerComment },
2743
+ ...attachmentMaxFileBytes === void 0 ? {} : { attachmentMaxFileBytes },
2744
+ ...attachmentMaxTotalBytes === void 0 ? {} : { attachmentMaxTotalBytes }
2745
+ }
2746
+ ) }) })
2747
+ ] });
2748
+ }
2749
+
2750
+ export { ATTACHMENT_ACCEPT, ATTACHMENT_LIMITS, AttachmentButton, AttachmentDropZone, AttachmentGif, AttachmentImage, AttachmentLightbox, AttachmentPlaceholder, AttachmentTray, Button, CodeBlock, CommentBody, ComposerFooter, ComposerNotice, GIF_SEARCH_DEBOUNCE_MS, GifPicker, Input, Kbd, MarkdownCode, MarkdownRenderer, PASTE_ATTACHMENT_THRESHOLD, POPOVER_SURFACE_STYLE, Popover, QUIET_ICON, QUIET_ICON_BOX, RichThreadComposer, ThreadComposer, __export, applyMarkdownFormat, cn, composeRefs, computeAnchoredPosition, formatAttachmentSize, handleTabTrap, parseMentionHref, pastedTextFilename, shouldAttachPaste, tabbablesWithin, useAnchoredPosition, useAttachmentUrl, useAttachments, useEscapeKey, useLayer, useOutsideClick, usePrefersReducedMotion };
2751
+ //# sourceMappingURL=chunk-RBYWGBQ2.js.map
2752
+ //# sourceMappingURL=chunk-RBYWGBQ2.js.map