@lovett/ui 0.0.11 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-RBYWGBQ2.js +2752 -0
- package/dist/chunk-RBYWGBQ2.js.map +1 -0
- package/dist/index.d.ts +5574 -57
- package/dist/index.js +21650 -11206
- package/dist/index.js.map +1 -1
- package/dist/rich-composer-impl-5NO443A6.js +1859 -0
- package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
- package/dist/styles.css +1570 -0
- package/dist/tokens.css +112 -0
- package/package.json +8 -1
- package/src/__tests__/avatar.test.tsx +272 -0
- package/src/__tests__/bar-chart.test.tsx +809 -0
- package/src/__tests__/board.test.tsx +420 -0
- package/src/__tests__/chart-math.test.ts +922 -0
- package/src/__tests__/chart-series.test.ts +339 -0
- package/src/__tests__/code-block.test.tsx +134 -0
- package/src/__tests__/display-popover.test.tsx +195 -0
- package/src/__tests__/display-store.test.tsx +307 -0
- package/src/__tests__/donut-chart.test.tsx +397 -0
- package/src/__tests__/dropdown-menu.test.tsx +156 -2
- package/src/__tests__/filter-menu.test.tsx +175 -0
- package/src/__tests__/gauge-ring.test.tsx +233 -0
- package/src/__tests__/line-chart.test.tsx +612 -0
- package/src/__tests__/ranked-bars.test.tsx +343 -0
- package/src/__tests__/remark-underline.test.ts +194 -0
- package/src/__tests__/sortable.test.tsx +394 -0
- package/src/__tests__/sparkline.test.tsx +368 -0
- package/src/__tests__/stat-layer.test.tsx +271 -0
- package/src/__tests__/stat-strip.test.tsx +175 -0
- package/src/__tests__/status.test.tsx +293 -0
- package/src/__tests__/tabs.test.tsx +303 -0
- package/src/__tests__/token-shape.test.ts +132 -2
- package/src/avatar.tsx +352 -0
- package/src/bar-chart.tsx +1214 -0
- package/src/board.tsx +658 -0
- package/src/chart-frame.tsx +960 -0
- package/src/chart-legend.tsx +304 -0
- package/src/chart-tooltip.tsx +267 -0
- package/src/code-block.tsx +62 -8
- package/src/delta-chip.tsx +263 -0
- package/src/detail/__tests__/activity-pane.test.tsx +369 -0
- package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
- package/src/detail/__tests__/detail-surface.test.tsx +529 -0
- package/src/detail/__tests__/field-row.test.tsx +357 -0
- package/src/detail/activity-pane.tsx +611 -0
- package/src/detail/calendar.tsx +355 -0
- package/src/detail/detail-divider.tsx +261 -0
- package/src/detail/detail-header.tsx +287 -0
- package/src/detail/detail-menu.tsx +254 -0
- package/src/detail/detail-surface.tsx +1110 -0
- package/src/detail/field-list.tsx +196 -0
- package/src/detail/field-row.tsx +1131 -0
- package/src/detail/index.ts +58 -0
- package/src/detail/segmented-choice.tsx +94 -0
- package/src/detail/types.ts +129 -0
- package/src/display-popover.tsx +487 -0
- package/src/display-store.tsx +301 -0
- package/src/donut-chart.tsx +988 -0
- package/src/dropdown-menu.tsx +290 -19
- package/src/filter-core/EXPORTS.md +85 -0
- package/src/filter-core/__tests__/columns.test.ts +159 -0
- package/src/filter-core/__tests__/faceting.test.ts +193 -0
- package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
- package/src/filter-core/__tests__/operators.test.ts +235 -0
- package/src/filter-core/__tests__/state.test.ts +268 -0
- package/src/filter-core/__tests__/url.test.ts +350 -0
- package/src/filter-core/columns.ts +134 -0
- package/src/filter-core/date-utils.ts +38 -0
- package/src/filter-core/examples/task-filter-columns.ts +121 -0
- package/src/filter-core/faceting.ts +120 -0
- package/src/filter-core/filter-fns.ts +335 -0
- package/src/filter-core/index.ts +105 -0
- package/src/filter-core/operators.ts +433 -0
- package/src/filter-core/state.ts +280 -0
- package/src/filter-core/types.ts +247 -0
- package/src/filter-core/url.ts +261 -0
- package/src/filter-dropdown.tsx +12 -0
- package/src/filter-menu.tsx +649 -0
- package/src/floating-drawer.tsx +19 -1
- package/src/gauge-ring.tsx +435 -0
- package/src/hue.ts +52 -0
- package/src/index.ts +303 -0
- package/src/kbd.tsx +27 -4
- package/src/lib/chart.ts +866 -0
- package/src/lib/focus.ts +43 -1
- package/src/lib/layer-stack.ts +30 -3
- package/src/lib/remark-underline.ts +443 -0
- package/src/lib/series.ts +169 -0
- package/src/line-chart.tsx +1176 -0
- package/src/markdown.tsx +26 -7
- package/src/modal.tsx +42 -18
- package/src/progress-ledger.tsx +304 -0
- package/src/ranked-bars.tsx +386 -0
- package/src/segmented-pill.tsx +32 -9
- package/src/sortable.tsx +520 -1
- package/src/sparkline.tsx +416 -0
- package/src/stat-card.tsx +376 -0
- package/src/stat-strip.tsx +327 -0
- package/src/status.tsx +215 -0
- package/src/styles.css +1570 -0
- package/src/tabs.tsx +206 -25
- package/src/task-card.tsx +610 -0
- package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
- package/src/thread/__tests__/comment-tree.test.ts +151 -0
- package/src/thread/__tests__/emoji.test.ts +187 -0
- package/src/thread/__tests__/fixtures/thread-fixture.ts +252 -0
- package/src/thread/__tests__/link-preview-source.test.ts +120 -0
- package/src/thread/__tests__/link-preview.test.tsx +600 -0
- package/src/thread/__tests__/markdown-format.test.ts +82 -0
- package/src/thread/__tests__/markdown-spec.test.ts +469 -0
- package/src/thread/__tests__/relative-time.test.ts +71 -0
- package/src/thread/__tests__/rich-composer.test.tsx +799 -0
- package/src/thread/__tests__/scroll-caret.test.ts +58 -0
- package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
- package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
- package/src/thread/__tests__/thread.test.tsx +742 -0
- package/src/thread/__tests__/use-attachments.test.tsx +679 -0
- package/src/thread/actions.tsx +196 -0
- package/src/thread/attachments.tsx +1071 -0
- package/src/thread/comment-body.tsx +148 -0
- package/src/thread/comment-tree.ts +182 -0
- package/src/thread/comment.tsx +967 -0
- package/src/thread/composer-footer.tsx +125 -0
- package/src/thread/composer.tsx +319 -0
- package/src/thread/emoji.ts +283 -0
- package/src/thread/index.ts +153 -0
- package/src/thread/link-preview.tsx +341 -0
- package/src/thread/markdown-format.ts +155 -0
- package/src/thread/markdown-spec.ts +754 -0
- package/src/thread/rail.tsx +372 -0
- package/src/thread/reactions.tsx +415 -0
- package/src/thread/relative-time.tsx +94 -0
- package/src/thread/rich-composer-impl.tsx +1601 -0
- package/src/thread/rich-composer.tsx +195 -0
- package/src/thread/scroll-caret.ts +37 -0
- package/src/thread/suggestion-list.tsx +182 -0
- package/src/thread/thread.tsx +718 -0
- package/src/thread/types.ts +232 -0
- package/src/thread/use-attachments.ts +598 -0
- package/src/thread/use-now.ts +73 -0
- package/src/thread/use-thread.ts +316 -0
- package/src/tokens.css +112 -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 `` 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
|