@kingsoftcloud/agentkit-harness-ui 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +116 -0
  3. package/README.md +292 -2
  4. package/markdown/build/build-markdown.cjs +66 -0
  5. package/markdown/dist/THIRD_PARTY_LICENSES.txt +2993 -0
  6. package/markdown/dist/markdown.css +1 -0
  7. package/markdown/dist/markdown.mjs +325 -0
  8. package/markdown/source/markdown-factory.mjs +613 -0
  9. package/markdown/source/markdown.css +288 -0
  10. package/markdown/source/table-factory.mjs +1588 -0
  11. package/markdown/source/table.css +487 -0
  12. package/package.json +96 -4
  13. package/src/activity-groups.mjs +74 -0
  14. package/src/browser-response-groups.mjs +57 -0
  15. package/src/command-output.mjs +57 -0
  16. package/src/components.mjs +9 -0
  17. package/src/context-markers.mjs +19 -0
  18. package/src/context-status.mjs +34 -0
  19. package/src/diff.mjs +240 -0
  20. package/src/explore-tool-call.mjs +139 -0
  21. package/src/file-preview.mjs +50 -0
  22. package/src/host/native-a2a.mjs +90 -0
  23. package/src/host/native-child-file-pane.mjs +49 -0
  24. package/src/host/native-child-pane.mjs +94 -0
  25. package/src/host/native-command-pane.mjs +64 -0
  26. package/src/host/native-run-panel.mjs +202 -0
  27. package/src/host/session-task-events.mjs +247 -0
  28. package/src/host/state/native-a2a.mjs +122 -0
  29. package/src/host/state/native-child-observation.mjs +35 -0
  30. package/src/host/state/native-children.mjs +93 -0
  31. package/src/host/state/native-outcome.mjs +35 -0
  32. package/src/host/state/native-recovery.mjs +187 -0
  33. package/src/host/state/native-runtime-route.mjs +26 -0
  34. package/src/host/state/native-session-stop.mjs +40 -0
  35. package/src/host/state/native-status-panel.mjs +23 -0
  36. package/src/host/state/native-task-read-model.mjs +89 -0
  37. package/src/host/state/task-links.mjs +40 -0
  38. package/src/host/task-timeline.mjs +185 -0
  39. package/src/index.mjs +52 -0
  40. package/src/input-preview.mjs +44 -0
  41. package/src/inspector.mjs +40 -0
  42. package/src/project.mjs +132 -0
  43. package/src/protocol.mjs +362 -0
  44. package/src/react-binding.mjs +486 -0
  45. package/src/reducer.mjs +355 -0
  46. package/src/renderers.mjs +644 -0
  47. package/src/status-panel.mjs +1156 -0
  48. package/src/styles.mjs +278 -0
  49. package/src/summary-playback.mjs +39 -0
  50. package/src/summary.mjs +160 -0
  51. package/src/terminal.mjs +157 -0
  52. package/src/tool-detail.mjs +208 -0
  53. package/src/tool-error.mjs +33 -0
  54. package/src/tool-identity.mjs +40 -0
  55. package/src/view.mjs +364 -0
  56. package/src/work-segments.mjs +41 -0
@@ -0,0 +1,1588 @@
1
+ // Independent Markdown table controls. React and portals are supplied by the host.
2
+ // Third-party attribution is retained in NOTICE.
3
+
4
+ const MARKDOWN_TABLE_ROOT_SELECTOR =
5
+ '[data-markdown-table-layout-root="true"], [data-testid="chat-view"]';
6
+ const MARKDOWN_TABLE_STICKY_DISABLED_SELECTOR = '[data-markdown-table-sticky-scrollbar="disabled"]';
7
+ const MARKDOWN_TABLE_V4_COMPOSER_DOCK_SELECTOR = '[data-v4-composer-dock="true"]';
8
+ const MARKDOWN_TABLE_V4_BACK_TO_BOTTOM_SELECTOR =
9
+ '[data-v4-back-to-bottom-anchor="composer-dock"] button';
10
+ const MARKDOWN_TABLE_V4_BACK_TO_BOTTOM_GAP_PX = 8;
11
+ const MARKDOWN_TABLE_CONTENT_PADDING_DEFAULT_PX = 32;
12
+ const MARKDOWN_TABLE_CONTENT_PADDING_LG_PX = 16;
13
+ const MARKDOWN_TABLE_CONTENT_PADDING_MD_PX = 8;
14
+ const MARKDOWN_TABLE_STICKY_SCROLLBAR_HEIGHT_RATIO = 0.8;
15
+ const MARKDOWN_TABLE_LAYOUT_LEFT_INSET_PROPERTY = "--markdown-table-layout-left-inset";
16
+ const MARKDOWN_TABLE_LAYOUT_RIGHT_INSET_PROPERTY = "--markdown-table-layout-right-inset";
17
+
18
+ function normalizeTableCellText(value) {
19
+ return value.replace(/\s+/g, " ").trim();
20
+ }
21
+
22
+ function escapeMarkdownTableCell(value) {
23
+ return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
24
+ }
25
+
26
+ function escapeCsvCell(value) {
27
+ // assistant markdown is untrusted input; CSV opened in Excel/Numbers/LibreOffice
28
+ // interprets formula prefixes. Downgrade formula-shaped cells to plain text
29
+ // before download (mirrors upstream mitigation).
30
+ const safeValue = /^[\t\r\n]/u.test(value) || /^[\s]*[=+\-@]/u.test(value) ? `'${value}` : value;
31
+
32
+ if (!/[",\r\n]/u.test(safeValue)) {
33
+ return safeValue;
34
+ }
35
+
36
+ return `"${safeValue.replace(/"/g, '""')}"`;
37
+ }
38
+
39
+ export function buildMarkdownTableText(rows) {
40
+ if (rows.length === 0) {
41
+ return "";
42
+ }
43
+
44
+ const columnCount = Math.max(...rows.map((row) => row.length), 1);
45
+ const normalizedRows = rows.map((row) =>
46
+ Array.from({ length: columnCount }, (_, index) => escapeMarkdownTableCell(row[index] ?? "")),
47
+ );
48
+ const header = normalizedRows[0] ?? [];
49
+ const separator = Array.from({ length: columnCount }, () => "---");
50
+ const body = normalizedRows.slice(1);
51
+
52
+ return [header, separator, ...body].map((row) => `| ${row.join(" | ")} |`).join("\n");
53
+ }
54
+
55
+ export function buildCsvTableText(rows) {
56
+ const csv = rows.map((row) => row.map((cell) => escapeCsvCell(cell)).join(",")).join("\r\n");
57
+ // Blob's charset is not written into the file bytes; Excel parses BOM-less
58
+ // UTF-8 CSV by the local code page, mangling CJK. Prefix a UTF-8 BOM so
59
+ // spreadsheet apps reliably detect the encoding.
60
+ return `${csv}`;
61
+ }
62
+
63
+ function readRowsFromTable(table) {
64
+ if (!table) {
65
+ return [];
66
+ }
67
+
68
+ return Array.from(table.querySelectorAll("tr"))
69
+ .map((row) =>
70
+ Array.from(row.querySelectorAll("th,td")).map((cell) =>
71
+ normalizeTableCellText(cell.textContent ?? ""),
72
+ ),
73
+ )
74
+ .filter((row) => row.length > 0);
75
+ }
76
+
77
+ function resolveMarkdownTableRoot(frame, fallbackRoot) {
78
+ // The layout root is the explicit boundary (V4) or the legacy chat-view; the
79
+ // table falls back to its own width when neither is present, so enhanced
80
+ // horizontal scroll never over-promises available space.
81
+ return frame?.closest?.(MARKDOWN_TABLE_ROOT_SELECTOR) ?? fallbackRoot;
82
+ }
83
+
84
+ function getMarkdownTableContentInlineInsetPx() {
85
+ if (typeof window === "undefined") {
86
+ return MARKDOWN_TABLE_CONTENT_PADDING_DEFAULT_PX;
87
+ }
88
+
89
+ // Borrowing the chat root's right space must subtract the message column's
90
+ // responsive right padding (ChatConversationContent px-8 max-lg:px-4 max-md:px-2).
91
+ if (window.innerWidth < 768) {
92
+ return MARKDOWN_TABLE_CONTENT_PADDING_MD_PX;
93
+ }
94
+
95
+ if (window.innerWidth < 1024) {
96
+ return MARKDOWN_TABLE_CONTENT_PADDING_LG_PX;
97
+ }
98
+
99
+ return MARKDOWN_TABLE_CONTENT_PADDING_DEFAULT_PX;
100
+ }
101
+
102
+ function getMarkdownTableRootInlineInsets(root) {
103
+ if (typeof window !== "undefined" && root.matches?.('[data-markdown-table-layout-root="true"]')) {
104
+ return {
105
+ leftInset: readCssPixelValue(root, MARKDOWN_TABLE_LAYOUT_LEFT_INSET_PROPERTY),
106
+ rightInset: readCssPixelValue(root, MARKDOWN_TABLE_LAYOUT_RIGHT_INSET_PROPERTY),
107
+ };
108
+ }
109
+
110
+ const legacyInset = getMarkdownTableContentInlineInsetPx();
111
+ return { leftInset: legacyInset, rightInset: legacyInset };
112
+ }
113
+
114
+ function readMarkdownTableObservedRect(element) {
115
+ if (!element) {
116
+ return null;
117
+ }
118
+
119
+ const rect = element.getBoundingClientRect();
120
+ return {
121
+ left: Math.round(rect.left),
122
+ right: Math.round(rect.right),
123
+ width: Math.round(rect.width),
124
+ };
125
+ }
126
+
127
+ function isMarkdownTableObservedRectEqual(leftRect, rightRect) {
128
+ return (
129
+ leftRect?.left === rightRect?.left &&
130
+ leftRect?.right === rightRect?.right &&
131
+ leftRect?.width === rightRect?.width
132
+ );
133
+ }
134
+
135
+ function readCssPixelValue(element, propertyName) {
136
+ if (!element || typeof window === "undefined") {
137
+ return 0;
138
+ }
139
+
140
+ const value = window.getComputedStyle(element).getPropertyValue(propertyName);
141
+ const parsedValue = Number.parseFloat(value);
142
+ return Number.isFinite(parsedValue) ? Math.max(0, parsedValue) : 0;
143
+ }
144
+
145
+ function readMarkdownTableDockHeight(root) {
146
+ const legacyDockHeight = readCssPixelValue(root, "--chat-bottom-dock-height");
147
+ if (legacyDockHeight > 0) return legacyDockHeight;
148
+
149
+ // V4 composer dock lives inside the timeline scroll viewport but no longer
150
+ // sets the legacy ChatView CSS variable; read the real dock height so the
151
+ // sticky scrollbar is not covered by the input area.
152
+ return (
153
+ root?.querySelector?.(MARKDOWN_TABLE_V4_COMPOSER_DOCK_SELECTOR)?.getBoundingClientRect().height ?? 0
154
+ );
155
+ }
156
+
157
+ function readMarkdownTableBackToBottomClearance(root) {
158
+ const button = root?.querySelector?.(MARKDOWN_TABLE_V4_BACK_TO_BOTTOM_SELECTOR);
159
+ if (!button) return 0;
160
+
161
+ return button.getBoundingClientRect().height + MARKDOWN_TABLE_V4_BACK_TO_BOTTOM_GAP_PX;
162
+ }
163
+
164
+ function resolveMarkdownTableVerticalViewport(element, fallbackRoot) {
165
+ if (typeof window === "undefined") {
166
+ return fallbackRoot;
167
+ }
168
+
169
+ let currentElement = element?.parentElement ?? null;
170
+ while (currentElement) {
171
+ const style = window.getComputedStyle(currentElement);
172
+ const canScrollY = /auto|scroll|overlay/u.test(style.overflowY);
173
+ // A horizontal overflow-x-auto viewport may make the browser compute
174
+ // overflow-y as auto; without confirming real vertical scroll range a short
175
+ // table would mistake its own height for the viewport and the 80% rule
176
+ // would always be true.
177
+ const hasVerticalScrollRange = currentElement.scrollHeight > currentElement.clientHeight + 1;
178
+ if (canScrollY && currentElement.clientHeight > 0 && hasVerticalScrollRange) {
179
+ return currentElement;
180
+ }
181
+ currentElement = currentElement.parentElement;
182
+ }
183
+
184
+ return fallbackRoot;
185
+ }
186
+
187
+ export function resolveMarkdownTableStickyScrollbarMode({
188
+ stickyScrollbarDisabled,
189
+ viewportHeight,
190
+ dockHeight,
191
+ tableHeight,
192
+ }) {
193
+ if (stickyScrollbarDisabled) {
194
+ return false;
195
+ }
196
+
197
+ const visibleHeight = Math.max(0, viewportHeight - dockHeight);
198
+ return (
199
+ visibleHeight > 0 && tableHeight > visibleHeight * MARKDOWN_TABLE_STICKY_SCROLLBAR_HEIGHT_RATIO
200
+ );
201
+ }
202
+
203
+ export function resolveMarkdownTableStickyScrollbarOffset({
204
+ frameTop,
205
+ frameBottom,
206
+ scrollbarHeight,
207
+ viewportBottom,
208
+ }) {
209
+ const minimumBottom = Math.min(frameBottom, frameTop + Math.max(0, scrollbarHeight));
210
+ const pinnedBottom = Math.min(frameBottom, Math.max(minimumBottom, viewportBottom));
211
+ return Math.min(0, pinnedBottom - frameBottom);
212
+ }
213
+
214
+ function shouldUseMarkdownTableStickyScrollbar({ root, table }) {
215
+ if (!table) {
216
+ return false;
217
+ }
218
+
219
+ const viewport = resolveMarkdownTableVerticalViewport(table, root);
220
+ const viewportHeight =
221
+ viewport?.clientHeight ||
222
+ viewport?.getBoundingClientRect().height ||
223
+ (typeof window === "undefined" ? 0 : window.innerHeight);
224
+ const dockHeight = readMarkdownTableDockHeight(root);
225
+ const tableHeight = table.getBoundingClientRect().height;
226
+
227
+ // Sub-agent prompt/output inherits the conversation dock height; if sticky
228
+ // were enabled there too the virtual scrollbar would float up into the
229
+ // middle of the table inside a nested container. Explicit opt-out does not
230
+ // depend on whether the container currently has vertical overflow.
231
+ return resolveMarkdownTableStickyScrollbarMode({
232
+ stickyScrollbarDisabled: table.closest?.(MARKDOWN_TABLE_STICKY_DISABLED_SELECTOR) !== null,
233
+ viewportHeight,
234
+ dockHeight,
235
+ tableHeight,
236
+ });
237
+ }
238
+
239
+ export function resolveMarkdownTableFrameDistances({
240
+ frameWidth,
241
+ frameLeft,
242
+ frameRight,
243
+ rootLeft,
244
+ rootRight,
245
+ leftInset = 0,
246
+ rightInset = 0,
247
+ }) {
248
+ const leftDistance = Math.max(0, frameLeft - rootLeft);
249
+ const rightDistance = Math.max(0, rootRight - frameRight);
250
+ const maxLeftOffset = Math.max(0, leftDistance - leftInset);
251
+ const scrollbarWidth = Math.max(0, frameWidth + rightDistance - rightInset);
252
+
253
+ return {
254
+ leftDistance,
255
+ maxLeftOffset,
256
+ scrollbarWidth,
257
+ rightDistance,
258
+ maxViewportWidth: scrollbarWidth,
259
+ };
260
+ }
261
+
262
+ export function resolveMarkdownTableVirtualScrollbarMetrics({
263
+ scrollLeft,
264
+ scrollMax,
265
+ tableWidth,
266
+ trackWidth,
267
+ }) {
268
+ const safeTableWidth = Math.max(0, tableWidth);
269
+ const safeTrackWidth = Math.max(0, trackWidth);
270
+ const safeScrollMax = Math.max(0, scrollMax);
271
+ const contentWidth = Math.max(safeTrackWidth, safeTableWidth);
272
+
273
+ if (safeTrackWidth <= 0 || safeScrollMax <= 1 || safeTableWidth <= safeTrackWidth) {
274
+ return {
275
+ contentWidth: safeTrackWidth,
276
+ thumbLeft: 0,
277
+ thumbWidth: safeTrackWidth,
278
+ };
279
+ }
280
+
281
+ const thumbWidth = Math.min(
282
+ safeTrackWidth,
283
+ Math.max(0, safeTrackWidth * (safeTrackWidth / safeTableWidth)),
284
+ );
285
+ const maxThumbLeft = Math.max(0, safeTrackWidth - thumbWidth);
286
+ const thumbLeft = Math.min(
287
+ maxThumbLeft,
288
+ Math.max(0, Math.max(0, scrollLeft) * (safeTrackWidth / safeTableWidth)),
289
+ );
290
+
291
+ return {
292
+ contentWidth,
293
+ thumbLeft,
294
+ thumbWidth,
295
+ };
296
+ }
297
+
298
+ export function resolveMarkdownTableVirtualScrollMax({ tableWidth, trackWidth }) {
299
+ return Math.max(0, tableWidth - trackWidth);
300
+ }
301
+
302
+ function cx(...parts) {
303
+ return parts.filter(Boolean).join(" ");
304
+ }
305
+
306
+ function formatTemplate(template, error) {
307
+ return String(template || "").replace("{error}", error == null ? "" : String(error));
308
+ }
309
+
310
+ /**
311
+ * Build the Markdown table React components from host-injected React/ReactDOM.
312
+ * The returned `table`/`thead`/`tbody`/`tr`/`th`/`td` components replace
313
+ * Streamdown's default table renderer (and its copy/download/fullscreen
314
+ * toolbar) so the layer owns the full table interaction surface.
315
+ *
316
+ * @param {object} React Host React (createElement + hooks).
317
+ * @param {object} ReactDOM Host ReactDOM (createPortal for the fullscreen dialog).
318
+ * @param {object} options
319
+ * @param {object} options.labels Resolved label strings (copyMarkdown, …).
320
+ * @param {(msg: {message: string, kind: "success"|"error"}) => void} [options.onNotify]
321
+ * Optional host toast hook; when absent an inline aria-live status region
322
+ * is rendered instead (copy/download never report success on failure).
323
+ * @returns {{ table: Function, thead: Function, tbody: Function, tr: Function, th: Function, td: Function }}
324
+ */
325
+ export function createMarkdownTableComponents(React, ReactDOM, options) {
326
+ const {
327
+ createElement,
328
+ useState,
329
+ useCallback,
330
+ useEffect,
331
+ useRef,
332
+ useLayoutEffect,
333
+ useId,
334
+ } = React;
335
+ const opts = options || {};
336
+ const labels = opts.labels || {};
337
+ const onNotify = typeof opts.onNotify === "function" ? opts.onNotify : null;
338
+ // Effective theme ("light" | "dark"); applied to the portaled dialog overlay
339
+ // so its tokens resolve outside the .ks-markdown scope.
340
+ const theme = opts.theme === "dark" || opts.theme === "light" ? opts.theme : null;
341
+ const createPortal =
342
+ ReactDOM && typeof ReactDOM.createPortal === "function" ? ReactDOM.createPortal : null;
343
+ // React 18 useId generates stable unique ids for the dialog's aria-labelledby
344
+ // / aria-describedby pair; fall back to static ids if the host React predates
345
+ // useId (the dialog is rare to open twice simultaneously, but unique ids keep
346
+ // the a11y tree valid when it does).
347
+ const useDialogId = typeof useId === "function" ? useId : null;
348
+
349
+ // useLayoutEffect is a no-op on the server; fall back to useEffect during SSR
350
+ // (renderToStaticMarkup) so no warning fires and measurements simply defer to
351
+ // the client, where they belong.
352
+ const useIsoLayoutEffect =
353
+ typeof window !== "undefined" && typeof useLayoutEffect === "function"
354
+ ? useLayoutEffect
355
+ : useEffect;
356
+
357
+ // ── inline SVG icons (lucide not resolvable from the factory bundle) ───────
358
+
359
+ function svgBase(children) {
360
+ return createElement(
361
+ "svg",
362
+ {
363
+ xmlns: "http://www.w3.org/2000/svg",
364
+ width: 14,
365
+ height: 14,
366
+ viewBox: "0 0 24 24",
367
+ fill: "none",
368
+ stroke: "currentColor",
369
+ strokeWidth: 2,
370
+ strokeLinecap: "round",
371
+ strokeLinejoin: "round",
372
+ "aria-hidden": "true",
373
+ focusable: "false",
374
+ },
375
+ children,
376
+ );
377
+ }
378
+
379
+ function CopyIcon() {
380
+ return svgBase([
381
+ createElement("rect", { key: "r", width: 14, height: 14, x: 8, y: 8, rx: 2, ry: 2 }),
382
+ createElement("path", {
383
+ key: "p",
384
+ d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",
385
+ }),
386
+ ]);
387
+ }
388
+
389
+ function DownloadIcon() {
390
+ return svgBase([
391
+ createElement("path", { key: "p", d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
392
+ createElement("polyline", { key: "l1", points: "7 10 12 15 17 10" }),
393
+ createElement("line", { key: "l2", x1: 12, y1: 15, x2: 12, y2: 3 }),
394
+ ]);
395
+ }
396
+
397
+ function MaximizeIcon() {
398
+ return svgBase([
399
+ createElement("polyline", { key: "p1", points: "15 3 21 3 21 9" }),
400
+ createElement("polyline", { key: "p2", points: "9 21 3 21 3 15" }),
401
+ createElement("line", { key: "l1", x1: 21, y1: 3, x2: 14, y2: 10 }),
402
+ createElement("line", { key: "l2", x1: 3, y1: 21, x2: 10, y2: 14 }),
403
+ ]);
404
+ }
405
+
406
+ // Expand (collapsed state): arrow pointing right toward a vertical line on
407
+ // the right → "extend scroll area toward the boundary".
408
+ function ExpandScrollIcon() {
409
+ return svgBase([
410
+ createElement("line", { key: "v", x1: 19, y1: 6, x2: 19, y2: 18 }),
411
+ createElement("line", { key: "h", x1: 5, y1: 12, x2: 15, y2: 12 }),
412
+ createElement("polyline", { key: "ah", points: "11 8 15 12 11 16" }),
413
+ ]);
414
+ }
415
+
416
+ // Collapse (expanded state): arrow pointing left away from the vertical line
417
+ // on the right → "pull back to the normal scroll range".
418
+ function CollapseScrollIcon() {
419
+ return svgBase([
420
+ createElement("line", { key: "v", x1: 19, y1: 6, x2: 19, y2: 18 }),
421
+ createElement("line", { key: "h", x1: 5, y1: 12, x2: 15, y2: 12 }),
422
+ createElement("polyline", { key: "ah", points: "9 8 5 12 9 16" }),
423
+ ]);
424
+ }
425
+
426
+ function CloseIcon() {
427
+ return svgBase([
428
+ createElement("line", { key: "l1", x1: 18, y1: 6, x2: 6, y2: 18 }),
429
+ createElement("line", { key: "l2", x1: 6, y1: 6, x2: 18, y2: 18 }),
430
+ ]);
431
+ }
432
+
433
+ // ── toolbar action button (ghost icon button + CSS tooltip via data-tip) ──
434
+
435
+ function MarkdownTableActionButton({ label, onClick, children, pressed }) {
436
+ return createElement(
437
+ "button",
438
+ {
439
+ type: "button",
440
+ className: "ks-md-table-btn",
441
+ "aria-label": label,
442
+ "aria-pressed": pressed == null ? undefined : pressed ? "true" : "false",
443
+ "data-tip": label,
444
+ onClick,
445
+ },
446
+ children,
447
+ );
448
+ }
449
+
450
+ // ── edge shadow (left/right inset shadow when content overflows) ───────────
451
+
452
+ function MarkdownTableEdgeShadow({ side, state }) {
453
+ const visible = state === side || state === "both";
454
+ if (!visible) {
455
+ return null;
456
+ }
457
+ return createElement("div", {
458
+ "aria-hidden": "true",
459
+ className: "ks-md-table-edge-shadow",
460
+ "data-side": side,
461
+ "data-markdown-table-edge-shadow": side,
462
+ });
463
+ }
464
+
465
+ // ── fullscreen preview dialog (portal; replaces Radix Dialog) ─────────────
466
+
467
+ function MarkdownTablePreviewDialog({ open, onClose, title, description, tableClassName, children, tableProps, dialogTheme }) {
468
+ const titleId = useDialogId ? useDialogId() : "ks-md-table-preview-title";
469
+ const descId = useDialogId ? useDialogId() : "ks-md-table-preview-desc";
470
+ const dialogRef = useRef(null), closeRef = useRef(onClose);
471
+ closeRef.current = onClose;
472
+ useEffect(() => {
473
+ if (!open) return undefined;
474
+ if (typeof window === "undefined") return undefined;
475
+ const previousFocus = document.activeElement;
476
+ const previousOverflow = document.body.style.overflow;
477
+ document.body.style.overflow = "hidden";
478
+ const focusables = () => Array.from(dialogRef.current?.querySelectorAll('button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])') ?? [])
479
+ .filter(element => element.getClientRects().length > 0);
480
+ const focusFirst = () => (focusables()[0] ?? dialogRef.current)?.focus();
481
+ focusFirst();
482
+ const onKey = (event) => {
483
+ if (event.key === "Escape") {
484
+ event.preventDefault();
485
+ closeRef.current();
486
+ } else if (event.key === "Tab") {
487
+ const nodes = focusables(), first = nodes[0], last = nodes.at(-1);
488
+ if (!nodes.length) { event.preventDefault(); dialogRef.current?.focus(); }
489
+ else if (event.shiftKey && (document.activeElement === first || !dialogRef.current?.contains(document.activeElement))) {
490
+ event.preventDefault(); last.focus();
491
+ } else if (!event.shiftKey && (document.activeElement === last || !dialogRef.current?.contains(document.activeElement))) {
492
+ event.preventDefault(); first.focus();
493
+ }
494
+ }
495
+ };
496
+ const containFocus = event => { if (!dialogRef.current?.contains(event.target)) focusFirst(); };
497
+ window.addEventListener("keydown", onKey);
498
+ document.addEventListener("focusin", containFocus);
499
+ return () => {
500
+ window.removeEventListener("keydown", onKey);
501
+ document.removeEventListener("focusin", containFocus);
502
+ document.body.style.overflow = previousOverflow;
503
+ if (previousFocus?.isConnected) previousFocus.focus();
504
+ };
505
+ }, [open]);
506
+
507
+ if (!open || typeof document === "undefined" || !createPortal) {
508
+ return null;
509
+ }
510
+
511
+ const overlayClass = cx(
512
+ "ks-md-table-dialog-overlay",
513
+ dialogTheme === "dark" && "ks-theme-dark",
514
+ dialogTheme === "light" && "ks-theme-light",
515
+ );
516
+
517
+ return createPortal(
518
+ createElement(
519
+ "div",
520
+ {
521
+ className: overlayClass,
522
+ "data-markdown-table-preview": "overlay",
523
+ onMouseDown: (event) => {
524
+ // Close when the backdrop (not the content) is clicked.
525
+ if (event.target === event.currentTarget) onClose();
526
+ },
527
+ },
528
+ createElement(
529
+ "div",
530
+ {
531
+ className: "ks-md-table-dialog",
532
+ ref: dialogRef,
533
+ tabIndex: -1,
534
+ role: "dialog",
535
+ "aria-modal": "true",
536
+ "aria-labelledby": titleId,
537
+ "aria-describedby": descId,
538
+ "data-markdown-table-preview": "content",
539
+ },
540
+ createElement(
541
+ "div",
542
+ { className: "ks-md-table-dialog-header" },
543
+ createElement("h2", { id: titleId, className: "ks-md-table-dialog-title" }, title),
544
+ createElement("p", { id: descId, className: "ks-md-table-dialog-desc" }, description),
545
+ createElement(
546
+ "button",
547
+ {
548
+ type: "button",
549
+ className: "ks-md-table-btn ks-md-table-dialog-close",
550
+ "aria-label": labels.closePreview || "Close",
551
+ "data-tip": labels.closePreview || "Close",
552
+ onClick: onClose,
553
+ },
554
+ createElement(CloseIcon, null),
555
+ ),
556
+ ),
557
+ createElement(
558
+ "div",
559
+ { className: "ks-md-table-dialog-scroll" },
560
+ createElement(
561
+ "table",
562
+ {
563
+ ...tableProps,
564
+ className: cx("ks-md-table ks-md-table-preview", tableClassName),
565
+ "data-streamdown": "table-preview",
566
+ },
567
+ children,
568
+ ),
569
+ ),
570
+ ),
571
+ ),
572
+ document.body,
573
+ );
574
+ }
575
+
576
+ // ── the table (ported state machine + DOM structure) ──────────────────────
577
+
578
+ function MarkdownTable({ className, children, node: _node, ...props }) {
579
+ const [previewOpen, setPreviewOpen] = useState(false);
580
+ const [expandedScrollEnabled, setExpandedScrollEnabled] = useState(false);
581
+ const [canToggleExpandedScroll, setCanToggleExpandedScroll] = useState(false);
582
+ const [edgeShadowState, setEdgeShadowState] = useState("none");
583
+ const [viewportMaxWidth, setViewportMaxWidth] = useState("100%");
584
+ const [scrollbarWidth, setScrollbarWidth] = useState("100%");
585
+ const [viewportLeftOffset, setViewportLeftOffset] = useState(0);
586
+ const [virtualScrollbarSticky, setVirtualScrollbarSticky] = useState(false);
587
+ const [virtualScrollbarVisible, setVirtualScrollbarVisible] = useState(false);
588
+ const [virtualScrollThumbStyle, setVirtualScrollThumbStyle] = useState({
589
+ transform: "translateX(0px)",
590
+ width: "100%",
591
+ });
592
+ // Inline status region (replaces toast). statusTokenRef guards against an
593
+ // older timer clearing a newer message.
594
+ const [status, setStatus] = useState(null);
595
+ const statusTokenRef = useRef(0);
596
+ const statusTimerRef = useRef(null);
597
+
598
+ const edgeShadowStateRef = useRef(edgeShadowState);
599
+ const expandedScrollEnabledRef = useRef(expandedScrollEnabled);
600
+ const canToggleExpandedScrollRef = useRef(canToggleExpandedScroll);
601
+ const viewportMaxWidthRef = useRef(viewportMaxWidth);
602
+ const scrollbarWidthRef = useRef(scrollbarWidth);
603
+ const viewportLeftOffsetRef = useRef(viewportLeftOffset);
604
+ const virtualScrollbarStickyRef = useRef(virtualScrollbarSticky);
605
+ const virtualScrollbarVisibleRef = useRef(virtualScrollbarVisible);
606
+ const maxViewportLeftOffsetRef = useRef(0);
607
+ const virtualScrollTrackWidthRef = useRef(0);
608
+ const virtualScrollLeftRef = useRef(0);
609
+ const virtualScrollThumbLeftRef = useRef(0);
610
+ const virtualScrollThumbStyleRef = useRef(virtualScrollThumbStyle);
611
+ const isVirtualScrollbarDraggingRef = useRef(false);
612
+ const virtualScrollbarDragOffsetRef = useRef(null);
613
+ const rootRef = useRef(null);
614
+ const frameRef = useRef(null);
615
+ const virtualScrollTrackRef = useRef(null);
616
+ const virtualScrollbarWrapperRef = useRef(null);
617
+ const scrollViewportRef = useRef(null);
618
+ const tableRef = useRef(null);
619
+
620
+ // Clear the status timer on unmount.
621
+ useEffect(
622
+ () => () => {
623
+ if (statusTimerRef.current && typeof clearTimeout === "function") {
624
+ clearTimeout(statusTimerRef.current);
625
+ }
626
+ },
627
+ [],
628
+ );
629
+
630
+ const notify = useCallback(
631
+ (message, kind) => {
632
+ if (onNotify) {
633
+ try {
634
+ onNotify({ message: String(message), kind });
635
+ } catch {
636
+ /* host notification must never break the action */
637
+ }
638
+ return;
639
+ }
640
+ const token = ++statusTokenRef.current;
641
+ setStatus({ kind, message: String(message) });
642
+ if (statusTimerRef.current && typeof clearTimeout === "function") {
643
+ clearTimeout(statusTimerRef.current);
644
+ }
645
+ statusTimerRef.current = setTimeout(() => {
646
+ if (statusTokenRef.current === token) setStatus(null);
647
+ }, 2500);
648
+ },
649
+ [],
650
+ );
651
+
652
+ const setNextEdgeShadowState = useCallback(
653
+ (nextEdgeShadowState) => {
654
+ if (edgeShadowStateRef.current === nextEdgeShadowState) {
655
+ return;
656
+ }
657
+ edgeShadowStateRef.current = nextEdgeShadowState;
658
+ setEdgeShadowState(nextEdgeShadowState);
659
+ },
660
+ [],
661
+ );
662
+ const commitExpandedScrollEnabled = useCallback((nextExpandedScrollEnabled) => {
663
+ if (expandedScrollEnabledRef.current === nextExpandedScrollEnabled) {
664
+ return;
665
+ }
666
+ expandedScrollEnabledRef.current = nextExpandedScrollEnabled;
667
+ setExpandedScrollEnabled(nextExpandedScrollEnabled);
668
+ }, []);
669
+ const commitCanToggleExpandedScroll = useCallback((nextCanToggleExpandedScroll) => {
670
+ if (canToggleExpandedScrollRef.current === nextCanToggleExpandedScroll) {
671
+ return;
672
+ }
673
+ canToggleExpandedScrollRef.current = nextCanToggleExpandedScroll;
674
+ setCanToggleExpandedScroll(nextCanToggleExpandedScroll);
675
+ }, []);
676
+ const commitViewportMaxWidth = useCallback((nextViewportMaxWidth) => {
677
+ if (viewportMaxWidthRef.current === nextViewportMaxWidth) {
678
+ return;
679
+ }
680
+ viewportMaxWidthRef.current = nextViewportMaxWidth;
681
+ setViewportMaxWidth(nextViewportMaxWidth);
682
+ }, []);
683
+ const commitScrollbarWidth = useCallback((nextScrollbarWidth) => {
684
+ if (scrollbarWidthRef.current === nextScrollbarWidth) {
685
+ return;
686
+ }
687
+ scrollbarWidthRef.current = nextScrollbarWidth;
688
+ setScrollbarWidth(nextScrollbarWidth);
689
+ }, []);
690
+ const commitViewportLeftOffset = useCallback((nextViewportLeftOffset) => {
691
+ if (viewportLeftOffsetRef.current === nextViewportLeftOffset) {
692
+ return;
693
+ }
694
+ viewportLeftOffsetRef.current = nextViewportLeftOffset;
695
+ setViewportLeftOffset(nextViewportLeftOffset);
696
+ }, []);
697
+ const commitVirtualScrollbarSticky = useCallback((nextVirtualScrollbarSticky) => {
698
+ if (virtualScrollbarStickyRef.current === nextVirtualScrollbarSticky) {
699
+ return;
700
+ }
701
+ virtualScrollbarStickyRef.current = nextVirtualScrollbarSticky;
702
+ setVirtualScrollbarSticky(nextVirtualScrollbarSticky);
703
+ }, []);
704
+ const commitVirtualScrollbarVisible = useCallback((nextVirtualScrollbarVisible) => {
705
+ if (virtualScrollbarVisibleRef.current === nextVirtualScrollbarVisible) {
706
+ return;
707
+ }
708
+ virtualScrollbarVisibleRef.current = nextVirtualScrollbarVisible;
709
+ setVirtualScrollbarVisible(nextVirtualScrollbarVisible);
710
+ }, []);
711
+ const commitVirtualScrollThumbStyle = useCallback((nextVirtualScrollThumbStyle) => {
712
+ const currentStyle = virtualScrollThumbStyleRef.current;
713
+ if (
714
+ currentStyle.width === nextVirtualScrollThumbStyle.width &&
715
+ currentStyle.transform === nextVirtualScrollThumbStyle.transform
716
+ ) {
717
+ return;
718
+ }
719
+ virtualScrollThumbStyleRef.current = nextVirtualScrollThumbStyle;
720
+ setVirtualScrollThumbStyle(nextVirtualScrollThumbStyle);
721
+ }, []);
722
+ const resolveTableWidth = useCallback(
723
+ (viewport) => {
724
+ return tableRef.current?.getBoundingClientRect().width ?? viewport.scrollWidth;
725
+ },
726
+ [],
727
+ );
728
+ const resolveVirtualScrollLayoutWidth = useCallback((viewport) => {
729
+ return virtualScrollTrackWidthRef.current || viewport.clientWidth;
730
+ }, []);
731
+ const resolveRenderedVirtualScrollTrackWidth = useCallback(
732
+ (viewport) => {
733
+ const trackWidth = virtualScrollTrackRef.current?.getBoundingClientRect().width ?? 0;
734
+ if (trackWidth > 0) {
735
+ return trackWidth;
736
+ }
737
+ return resolveVirtualScrollLayoutWidth(viewport);
738
+ },
739
+ [resolveVirtualScrollLayoutWidth],
740
+ );
741
+ const resolveVirtualScrollMax = useCallback(
742
+ (viewport) => {
743
+ // Virtual scroll distance is only the <table> width minus the track
744
+ // slot width; mixing viewportLeftOffset in previously changed the max
745
+ // when borrowing left space, desyncing the thumb from content progress.
746
+ // The conditionally-mounted DOM track uses a CSS width; if that differs
747
+ // from the saved floating-point layout width, visibility would toggle
748
+ // around the threshold. Overflow must always read the same stable slot.
749
+ return resolveMarkdownTableVirtualScrollMax({
750
+ tableWidth: resolveTableWidth(viewport),
751
+ trackWidth: resolveVirtualScrollLayoutWidth(viewport),
752
+ });
753
+ },
754
+ [resolveTableWidth, resolveVirtualScrollLayoutWidth],
755
+ );
756
+ const commitVirtualScrollLeft = useCallback(
757
+ (nextVirtualScrollLeft) => {
758
+ const viewport = scrollViewportRef.current;
759
+ if (!viewport) {
760
+ virtualScrollLeftRef.current = 0;
761
+ return;
762
+ }
763
+
764
+ const virtualScrollMax = resolveVirtualScrollMax(viewport);
765
+ const clampedVirtualScrollLeft = Math.min(
766
+ Math.max(0, nextVirtualScrollLeft),
767
+ virtualScrollMax,
768
+ );
769
+ const nextViewportLeftOffset = Math.min(
770
+ clampedVirtualScrollLeft,
771
+ maxViewportLeftOffsetRef.current,
772
+ );
773
+ const nextViewportScrollLeft = clampedVirtualScrollLeft - nextViewportLeftOffset;
774
+
775
+ virtualScrollLeftRef.current = clampedVirtualScrollLeft;
776
+ commitViewportLeftOffset(nextViewportLeftOffset);
777
+ viewport.scrollLeft = nextViewportScrollLeft;
778
+ },
779
+ [commitViewportLeftOffset, resolveVirtualScrollMax],
780
+ );
781
+ const syncVirtualScrollMetrics = useCallback(() => {
782
+ const viewport = scrollViewportRef.current;
783
+ if (!viewport) {
784
+ commitVirtualScrollbarVisible(false);
785
+ commitVirtualScrollThumbStyle({
786
+ transform: "translateX(0px)",
787
+ width: "100%",
788
+ });
789
+ virtualScrollLeftRef.current = 0;
790
+ virtualScrollThumbLeftRef.current = 0;
791
+ return;
792
+ }
793
+
794
+ const virtualScrollMax = resolveVirtualScrollMax(viewport);
795
+ // The virtual scrollbar only matters when the table exceeds the slot;
796
+ // rendering the pill for a small table would mislead users into thinking
797
+ // there is more horizontal content to scroll.
798
+ commitVirtualScrollbarVisible(virtualScrollMax > 1);
799
+ const nextVirtualScrollLeft = viewportLeftOffsetRef.current + viewport.scrollLeft;
800
+ const clampedVirtualScrollLeft = Math.min(Math.max(0, nextVirtualScrollLeft), virtualScrollMax);
801
+ if (clampedVirtualScrollLeft !== nextVirtualScrollLeft) {
802
+ // Collapsing/expanding the side panel or a resize changes the slot; the
803
+ // old virtual scroll position may now exceed the new boundary. Write
804
+ // back through the single entry point so the thumb and DOM scrollLeft
805
+ // never drift apart at the edge.
806
+ commitVirtualScrollLeft(clampedVirtualScrollLeft);
807
+ return;
808
+ }
809
+
810
+ virtualScrollLeftRef.current = clampedVirtualScrollLeft;
811
+ // The rendered track width only feeds thumb ratio/position; it must not
812
+ // decide whether the track is mounted.
813
+ const virtualScrollViewportWidth = resolveRenderedVirtualScrollTrackWidth(viewport);
814
+ const metrics = resolveMarkdownTableVirtualScrollbarMetrics({
815
+ scrollLeft: clampedVirtualScrollLeft,
816
+ scrollMax: virtualScrollMax,
817
+ tableWidth: resolveTableWidth(viewport),
818
+ trackWidth: virtualScrollViewportWidth,
819
+ });
820
+ commitVirtualScrollThumbStyle({
821
+ transform: `translateX(${Math.round(metrics.thumbLeft)}px)`,
822
+ width: `${Math.ceil(metrics.thumbWidth)}px`,
823
+ });
824
+ virtualScrollThumbLeftRef.current = metrics.thumbLeft;
825
+ }, [
826
+ commitVirtualScrollLeft,
827
+ commitVirtualScrollbarVisible,
828
+ commitVirtualScrollThumbStyle,
829
+ resolveTableWidth,
830
+ resolveVirtualScrollMax,
831
+ resolveRenderedVirtualScrollTrackWidth,
832
+ ]);
833
+ const measureViewportMaxWidth = useCallback(() => {
834
+ const frame = frameRef.current;
835
+ const root = resolveMarkdownTableRoot(frame, rootRef.current);
836
+ if (!frame) {
837
+ commitViewportMaxWidth("100%");
838
+ commitScrollbarWidth("100%");
839
+ commitCanToggleExpandedScroll(false);
840
+ commitVirtualScrollbarVisible(false);
841
+ commitVirtualScrollbarSticky(false);
842
+ maxViewportLeftOffsetRef.current = 0;
843
+ virtualScrollTrackWidthRef.current = 0;
844
+ commitViewportLeftOffset(0);
845
+ return;
846
+ }
847
+
848
+ const frameRect = frame.getBoundingClientRect();
849
+ let maxLeftOffset = 0;
850
+ let maxViewportWidth = frameRect.width;
851
+ let measuredScrollbarWidth = frameRect.width;
852
+ let expandedMaxLeftOffset = 0;
853
+ let expandedScrollbarWidth = frameRect.width;
854
+
855
+ if (root) {
856
+ const rootRect = root.getBoundingClientRect();
857
+ // V4's expanded boundary is the full timeline viewport, not the
858
+ // centered message column; the explicit root supplies responsive safe
859
+ // margins via CSS variables, the legacy root keeps the old rule.
860
+ const { leftInset, rightInset } = getMarkdownTableRootInlineInsets(root);
861
+ const measuredDistances = resolveMarkdownTableFrameDistances({
862
+ frameWidth: frameRect.width,
863
+ frameLeft: frameRect.left,
864
+ frameRight: frameRect.right,
865
+ rootLeft: rootRect.left,
866
+ rootRight: rootRect.right,
867
+ leftInset,
868
+ rightInset,
869
+ });
870
+ expandedMaxLeftOffset = measuredDistances.maxLeftOffset;
871
+ expandedScrollbarWidth = measuredDistances.scrollbarWidth;
872
+
873
+ if (expandedScrollEnabledRef.current) {
874
+ maxLeftOffset = measuredDistances.maxLeftOffset;
875
+ maxViewportWidth = measuredDistances.maxViewportWidth;
876
+ measuredScrollbarWidth = measuredDistances.scrollbarWidth;
877
+ }
878
+ }
879
+
880
+ const tableWidth = tableRef.current?.getBoundingClientRect().width ?? 0;
881
+ const hasFrameHorizontalOverflow = tableWidth > frameRect.width + 1;
882
+ const hasExpandedScrollBenefit =
883
+ expandedScrollbarWidth > frameRect.width + 1 || expandedMaxLeftOffset > 1;
884
+ // The toggle only matters when the normal frame width actually overflows
885
+ // AND expanded mode adds visible width or a left borrow; once enabled it
886
+ // stays so the user can collapse back to the normal scroll range.
887
+ commitCanToggleExpandedScroll(
888
+ expandedScrollEnabledRef.current || (hasFrameHorizontalOverflow && hasExpandedScrollBenefit),
889
+ );
890
+
891
+ maxViewportLeftOffsetRef.current = maxLeftOffset;
892
+ virtualScrollTrackWidthRef.current = measuredScrollbarWidth;
893
+ commitViewportMaxWidth(`${Math.ceil(maxViewportWidth)}px`);
894
+ // Ceil may make the mounted track up to 1px wider than the slot used for
895
+ // overflow judgement; sub-pixel overflow near the 1px threshold would
896
+ // mount/unmount the track each frame and perturb the timeline height.
897
+ commitScrollbarWidth(`${measuredScrollbarWidth}px`);
898
+ commitVirtualScrollbarSticky(
899
+ shouldUseMarkdownTableStickyScrollbar({
900
+ root,
901
+ table: tableRef.current,
902
+ }),
903
+ );
904
+ if (scrollViewportRef.current) {
905
+ commitVirtualScrollLeft(virtualScrollLeftRef.current);
906
+ } else {
907
+ commitViewportLeftOffset(Math.min(viewportLeftOffsetRef.current, maxLeftOffset));
908
+ }
909
+ syncVirtualScrollMetrics();
910
+ }, [
911
+ commitScrollbarWidth,
912
+ commitCanToggleExpandedScroll,
913
+ commitVirtualScrollLeft,
914
+ commitVirtualScrollbarSticky,
915
+ commitVirtualScrollbarVisible,
916
+ commitViewportLeftOffset,
917
+ commitViewportMaxWidth,
918
+ syncVirtualScrollMetrics,
919
+ ]);
920
+
921
+ const measureEdgeShadowState = useCallback(() => {
922
+ const viewport = scrollViewportRef.current;
923
+ const table = tableRef.current;
924
+ if (!viewport || !table) {
925
+ setNextEdgeShadowState("none");
926
+ return;
927
+ }
928
+
929
+ const maxScrollLeft = resolveVirtualScrollMax(viewport);
930
+ if (maxScrollLeft <= 1) {
931
+ setNextEdgeShadowState("none");
932
+ return;
933
+ }
934
+
935
+ const currentVirtualScrollLeft = viewportLeftOffsetRef.current + viewport.scrollLeft;
936
+ // Borrowing left only widens the table area to the left; it does not mean
937
+ // table content is already hidden at the scroll container's left edge.
938
+ // The left shadow appears only after a real internal scrollLeft; the
939
+ // right shadow follows the virtual progress.
940
+ const hasHiddenLeft = viewport.scrollLeft > 1;
941
+ const hasHiddenRight = currentVirtualScrollLeft < maxScrollLeft - 1;
942
+ setNextEdgeShadowState(
943
+ hasHiddenLeft && hasHiddenRight
944
+ ? "both"
945
+ : hasHiddenLeft
946
+ ? "left"
947
+ : hasHiddenRight
948
+ ? "right"
949
+ : "none",
950
+ );
951
+ }, [resolveVirtualScrollMax, setNextEdgeShadowState]);
952
+ const latestMeasureEdgeShadowStateRef = useRef(measureEdgeShadowState);
953
+
954
+ useEffect(() => {
955
+ latestMeasureEdgeShadowStateRef.current = measureEdgeShadowState;
956
+ });
957
+
958
+ useIsoLayoutEffect(() => {
959
+ const frame = frameRef.current;
960
+ const root = resolveMarkdownTableRoot(frame, rootRef.current);
961
+ const viewport = scrollViewportRef.current;
962
+ const table = tableRef.current;
963
+ if (!root || !frame || !viewport || !table) {
964
+ commitViewportMaxWidth("100%");
965
+ commitScrollbarWidth("100%");
966
+ return;
967
+ }
968
+
969
+ // The table viewport lays out at natural content width first, then can
970
+ // grow up to the current frame width plus the borrowable right space; a
971
+ // direct w-full would lose the small-table shrink-to-fit semantics.
972
+ let rafId = null;
973
+ let rectWatchRafId = null;
974
+ let rectWatchUntil = 0;
975
+ let latestRootRect = readMarkdownTableObservedRect(root);
976
+ let latestFrameRect = readMarkdownTableObservedRect(frame);
977
+ const scheduleMeasure = () => {
978
+ if (rafId !== null) return;
979
+ rafId = requestAnimationFrame(() => {
980
+ rafId = null;
981
+ latestRootRect = readMarkdownTableObservedRect(root);
982
+ latestFrameRect = readMarkdownTableObservedRect(frame);
983
+ measureViewportMaxWidth();
984
+ latestMeasureEdgeShadowStateRef.current();
985
+ });
986
+ };
987
+ const watchRectChanges = () => {
988
+ rectWatchRafId = null;
989
+ const nextRootRect = readMarkdownTableObservedRect(root);
990
+ const nextFrameRect = readMarkdownTableObservedRect(frame);
991
+ const rectChanged =
992
+ !isMarkdownTableObservedRectEqual(latestRootRect, nextRootRect) ||
993
+ !isMarkdownTableObservedRectEqual(latestFrameRect, nextFrameRect);
994
+
995
+ if (rectChanged) {
996
+ // Collapsing/expanding the status panel may only change chat root
997
+ // left/right; ResizeObserver may not fire. Polling the rect
998
+ // signature lets the left borrow and scroll slot follow the layout
999
+ // animation.
1000
+ latestRootRect = nextRootRect;
1001
+ latestFrameRect = nextFrameRect;
1002
+ scheduleMeasure();
1003
+ }
1004
+
1005
+ if (performance.now() < rectWatchUntil) {
1006
+ rectWatchRafId = requestAnimationFrame(watchRectChanges);
1007
+ }
1008
+ };
1009
+ const scheduleMeasureAndWatchRect = () => {
1010
+ scheduleMeasure();
1011
+ rectWatchUntil = performance.now() + 800;
1012
+ if (rectWatchRafId === null) {
1013
+ rectWatchRafId = requestAnimationFrame(watchRectChanges);
1014
+ }
1015
+ };
1016
+ const mutationObserver =
1017
+ typeof MutationObserver === "undefined" ? null : new MutationObserver(scheduleMeasureAndWatchRect);
1018
+ mutationObserver?.observe(root, {
1019
+ attributeFilter: ["style"],
1020
+ attributes: true,
1021
+ });
1022
+
1023
+ measureViewportMaxWidth();
1024
+ scheduleMeasureAndWatchRect();
1025
+
1026
+ if (typeof ResizeObserver === "undefined") {
1027
+ window.addEventListener("resize", scheduleMeasureAndWatchRect);
1028
+ return () => {
1029
+ window.removeEventListener("resize", scheduleMeasureAndWatchRect);
1030
+ mutationObserver?.disconnect();
1031
+ if (rafId !== null) cancelAnimationFrame(rafId);
1032
+ if (rectWatchRafId !== null) cancelAnimationFrame(rectWatchRafId);
1033
+ };
1034
+ }
1035
+
1036
+ const resizeObserver = new ResizeObserver(scheduleMeasureAndWatchRect);
1037
+ resizeObserver.observe(root);
1038
+ resizeObserver.observe(frame);
1039
+ resizeObserver.observe(viewport);
1040
+ resizeObserver.observe(table);
1041
+ window.addEventListener("resize", scheduleMeasureAndWatchRect);
1042
+
1043
+ return () => {
1044
+ resizeObserver.disconnect();
1045
+ window.removeEventListener("resize", scheduleMeasureAndWatchRect);
1046
+ mutationObserver?.disconnect();
1047
+ if (rafId !== null) cancelAnimationFrame(rafId);
1048
+ if (rectWatchRafId !== null) cancelAnimationFrame(rectWatchRafId);
1049
+ };
1050
+ }, [commitScrollbarWidth, commitViewportMaxWidth, measureViewportMaxWidth]);
1051
+
1052
+ useEffect(() => {
1053
+ const viewport = scrollViewportRef.current;
1054
+ const table = tableRef.current;
1055
+ if (!viewport || !table) {
1056
+ setNextEdgeShadowState("none");
1057
+ return;
1058
+ }
1059
+
1060
+ const updateViewportLayout = () => {
1061
+ measureViewportMaxWidth();
1062
+ latestMeasureEdgeShadowStateRef.current();
1063
+ };
1064
+ const updateEdgeShadowState = () => {
1065
+ syncVirtualScrollMetrics();
1066
+ latestMeasureEdgeShadowStateRef.current();
1067
+ };
1068
+ const handleWheel = (event) => {
1069
+ const horizontalDelta = event.deltaX || (event.shiftKey ? event.deltaY : 0);
1070
+ if (horizontalDelta === 0) {
1071
+ return;
1072
+ }
1073
+
1074
+ const virtualScrollMax = resolveVirtualScrollMax(viewport);
1075
+ if (virtualScrollMax <= 1) {
1076
+ return;
1077
+ }
1078
+
1079
+ event.preventDefault();
1080
+ // Left-borrow and content scroll used to keep separate positions and
1081
+ // overwrite each other at the boundary so rollback was impossible; now a
1082
+ // single virtual scrollLeft maps: consume leftOffset first, then hand
1083
+ // the remainder to the real viewport.scrollLeft.
1084
+ commitVirtualScrollLeft(virtualScrollLeftRef.current + horizontalDelta);
1085
+ updateViewportLayout();
1086
+ };
1087
+
1088
+ // Performance: p12 trace showed streaming table children changes
1089
+ // rebuilding the observer and synchronously reading layout. The observer
1090
+ // lifecycle is bound to the DOM nodes; content changes are merged into the
1091
+ // next frame by ResizeObserver.
1092
+ let rafId = null;
1093
+ const debouncedUpdate = () => {
1094
+ if (rafId !== null) return;
1095
+ rafId = requestAnimationFrame(() => {
1096
+ rafId = null;
1097
+ updateEdgeShadowState();
1098
+ });
1099
+ };
1100
+
1101
+ updateEdgeShadowState();
1102
+ viewport.addEventListener("scroll", updateEdgeShadowState, {
1103
+ passive: true,
1104
+ });
1105
+ viewport.addEventListener("wheel", handleWheel, { passive: false });
1106
+
1107
+ if (typeof ResizeObserver === "undefined") {
1108
+ window.addEventListener("resize", debouncedUpdate);
1109
+ return () => {
1110
+ viewport.removeEventListener("scroll", updateEdgeShadowState);
1111
+ viewport.removeEventListener("wheel", handleWheel);
1112
+ window.removeEventListener("resize", debouncedUpdate);
1113
+ if (rafId !== null) cancelAnimationFrame(rafId);
1114
+ };
1115
+ }
1116
+
1117
+ const resizeObserver = new ResizeObserver(() => {
1118
+ debouncedUpdate();
1119
+ });
1120
+ resizeObserver.observe(viewport);
1121
+ resizeObserver.observe(table);
1122
+ window.addEventListener("resize", debouncedUpdate);
1123
+
1124
+ return () => {
1125
+ viewport.removeEventListener("scroll", updateEdgeShadowState);
1126
+ viewport.removeEventListener("wheel", handleWheel);
1127
+ resizeObserver.disconnect();
1128
+ window.removeEventListener("resize", debouncedUpdate);
1129
+ if (rafId !== null) cancelAnimationFrame(rafId);
1130
+ };
1131
+ }, [
1132
+ commitVirtualScrollLeft,
1133
+ measureViewportMaxWidth,
1134
+ resolveVirtualScrollMax,
1135
+ setNextEdgeShadowState,
1136
+ syncVirtualScrollMetrics,
1137
+ ]);
1138
+
1139
+ useIsoLayoutEffect(() => {
1140
+ const frame = frameRef.current;
1141
+ const table = tableRef.current;
1142
+ const wrapper = virtualScrollbarWrapperRef.current;
1143
+ const root = resolveMarkdownTableRoot(frame, rootRef.current);
1144
+ if (!frame || !table || !wrapper || !virtualScrollbarSticky) {
1145
+ if (wrapper) wrapper.style.transform = "";
1146
+ return;
1147
+ }
1148
+
1149
+ const verticalViewport = resolveMarkdownTableVerticalViewport(table, root);
1150
+ if (!verticalViewport) return;
1151
+
1152
+ let rafId = null;
1153
+ let observedDock = null;
1154
+ const updateStickyOffset = () => {
1155
+ rafId = null;
1156
+ const frameRect = frame.getBoundingClientRect();
1157
+ const viewportRect = verticalViewport.getBoundingClientRect();
1158
+ const dockHeight = readMarkdownTableDockHeight(root);
1159
+ // Off the bottom, the "back to bottom" button sits above the composer
1160
+ // dock; subtracting only the dock would let the long-table sticky
1161
+ // scrollbar share the horizontal band with the button, hiding it under
1162
+ // the wide slot.
1163
+ const backToBottomClearance = readMarkdownTableBackToBottomClearance(root);
1164
+ const offset = resolveMarkdownTableStickyScrollbarOffset({
1165
+ frameTop: frameRect.top,
1166
+ frameBottom: frameRect.bottom,
1167
+ scrollbarHeight: wrapper.getBoundingClientRect().height,
1168
+ viewportBottom: viewportRect.bottom - dockHeight - backToBottomClearance,
1169
+ });
1170
+ wrapper.style.transform = offset === 0 ? "" : `translateY(${Math.round(offset)}px)`;
1171
+ };
1172
+ const scheduleStickyOffset = () => {
1173
+ if (rafId !== null) return;
1174
+ rafId = requestAnimationFrame(updateStickyOffset);
1175
+ };
1176
+
1177
+ const resizeObserver =
1178
+ typeof ResizeObserver === "undefined" ? null : new ResizeObserver(scheduleStickyOffset);
1179
+ const syncDockObservation = () => {
1180
+ const nextDock = root?.querySelector?.(MARKDOWN_TABLE_V4_COMPOSER_DOCK_SELECTOR) ?? null;
1181
+ // The MutationObserver also senses the back-to-bottom button mounting
1182
+ // inside the dock; even when the dock node itself is unchanged a
1183
+ // geometry update must be scheduled.
1184
+ if (nextDock === observedDock) {
1185
+ scheduleStickyOffset();
1186
+ return;
1187
+ }
1188
+ if (observedDock) resizeObserver?.unobserve(observedDock);
1189
+ observedDock = nextDock;
1190
+ if (observedDock) resizeObserver?.observe(observedDock);
1191
+ scheduleStickyOffset();
1192
+ };
1193
+
1194
+ // V4 virtual rows use transform for positioning; native sticky would be
1195
+ // clamped by the transformed ancestor to its natural position. Instead we
1196
+ // follow the timeline scroll to compute a vertical offset, still bounded
1197
+ // by the table's top/bottom edges.
1198
+ updateStickyOffset();
1199
+ verticalViewport.addEventListener("scroll", scheduleStickyOffset, { passive: true });
1200
+ window.addEventListener("resize", scheduleStickyOffset);
1201
+ resizeObserver?.observe(frame);
1202
+ resizeObserver?.observe(verticalViewport);
1203
+ syncDockObservation();
1204
+ // Draft/empty-state switching conditionally mounts or replaces the V4
1205
+ // composer dock; observing only the initial node would miss a new dock's
1206
+ // taller input, so track root subtree changes and rebind.
1207
+ const dockMutationObserver =
1208
+ typeof MutationObserver === "undefined" || !root
1209
+ ? null
1210
+ : new MutationObserver(syncDockObservation);
1211
+ if (dockMutationObserver && root) {
1212
+ dockMutationObserver.observe(root, { childList: true, subtree: true });
1213
+ }
1214
+
1215
+ return () => {
1216
+ verticalViewport.removeEventListener("scroll", scheduleStickyOffset);
1217
+ window.removeEventListener("resize", scheduleStickyOffset);
1218
+ resizeObserver?.disconnect();
1219
+ dockMutationObserver?.disconnect();
1220
+ if (rafId !== null) cancelAnimationFrame(rafId);
1221
+ wrapper.style.transform = "";
1222
+ };
1223
+ }, [virtualScrollbarSticky, virtualScrollbarVisible]);
1224
+
1225
+ const getRows = useCallback(() => readRowsFromTable(tableRef.current), []);
1226
+
1227
+ const commitVirtualScrollLeftFromClientX = useCallback(
1228
+ (clientX, thumbDragOffset = null) => {
1229
+ const track = virtualScrollTrackRef.current;
1230
+ const viewport = scrollViewportRef.current;
1231
+ if (!track || !viewport) return;
1232
+
1233
+ const trackRect = track.getBoundingClientRect();
1234
+ const trackWidth = trackRect.width;
1235
+ const virtualScrollMax = resolveVirtualScrollMax(viewport);
1236
+ const metrics = resolveMarkdownTableVirtualScrollbarMetrics({
1237
+ scrollLeft: virtualScrollLeftRef.current,
1238
+ scrollMax: virtualScrollMax,
1239
+ tableWidth: resolveTableWidth(viewport),
1240
+ trackWidth,
1241
+ });
1242
+ const maxThumbLeft = Math.max(0, trackWidth - metrics.thumbWidth);
1243
+ const rawThumbLeft =
1244
+ thumbDragOffset === null
1245
+ ? clientX - trackRect.left - metrics.thumbWidth / 2
1246
+ : clientX - trackRect.left - thumbDragOffset;
1247
+ const nextThumbLeft = Math.min(maxThumbLeft, Math.max(0, rawThumbLeft));
1248
+ const nextVirtualScrollLeft =
1249
+ maxThumbLeft <= 0 ? 0 : (nextThumbLeft / maxThumbLeft) * virtualScrollMax;
1250
+
1251
+ commitVirtualScrollLeft(nextVirtualScrollLeft);
1252
+ measureViewportMaxWidth();
1253
+ latestMeasureEdgeShadowStateRef.current();
1254
+ },
1255
+ [commitVirtualScrollLeft, measureViewportMaxWidth, resolveTableWidth, resolveVirtualScrollMax],
1256
+ );
1257
+ const handleVirtualScrollbarPointerDown = useCallback(
1258
+ (event) => {
1259
+ isVirtualScrollbarDraggingRef.current = true;
1260
+ virtualScrollbarDragOffsetRef.current = null;
1261
+ event.currentTarget.setPointerCapture(event.pointerId);
1262
+ commitVirtualScrollLeftFromClientX(event.clientX);
1263
+ },
1264
+ [commitVirtualScrollLeftFromClientX],
1265
+ );
1266
+ const handleVirtualScrollbarPointerMove = useCallback(
1267
+ (event) => {
1268
+ if (!isVirtualScrollbarDraggingRef.current) return;
1269
+ commitVirtualScrollLeftFromClientX(event.clientX, virtualScrollbarDragOffsetRef.current);
1270
+ },
1271
+ [commitVirtualScrollLeftFromClientX],
1272
+ );
1273
+ const handleVirtualScrollbarPointerEnd = useCallback((event) => {
1274
+ isVirtualScrollbarDraggingRef.current = false;
1275
+ virtualScrollbarDragOffsetRef.current = null;
1276
+ if (event.currentTarget && event.currentTarget.hasPointerCapture && event.currentTarget.hasPointerCapture(event.pointerId)) {
1277
+ event.currentTarget.releasePointerCapture(event.pointerId);
1278
+ }
1279
+ }, []);
1280
+ const handleVirtualScrollbarThumbPointerDown = useCallback((event) => {
1281
+ event.stopPropagation();
1282
+ const track = virtualScrollTrackRef.current;
1283
+ if (!track) return;
1284
+
1285
+ const trackRect = track.getBoundingClientRect();
1286
+ isVirtualScrollbarDraggingRef.current = true;
1287
+ // The thumb is a track child; pressing it must not bubble to the track's
1288
+ // click-to-jump logic. Record the pointer offset within the thumb so
1289
+ // dragging preserves that relative offset instead of jumping to center.
1290
+ virtualScrollbarDragOffsetRef.current = Math.max(
1291
+ 0,
1292
+ event.clientX - trackRect.left - virtualScrollThumbLeftRef.current,
1293
+ );
1294
+ event.currentTarget.setPointerCapture(event.pointerId);
1295
+ }, []);
1296
+ const handleVirtualScrollbarThumbPointerMove = useCallback(
1297
+ (event) => {
1298
+ event.stopPropagation();
1299
+ if (!isVirtualScrollbarDraggingRef.current) return;
1300
+ commitVirtualScrollLeftFromClientX(event.clientX, virtualScrollbarDragOffsetRef.current);
1301
+ },
1302
+ [commitVirtualScrollLeftFromClientX],
1303
+ );
1304
+ const handleVirtualScrollbarThumbPointerEnd = useCallback((event) => {
1305
+ event.stopPropagation();
1306
+ handleVirtualScrollbarPointerEnd(event);
1307
+ }, [handleVirtualScrollbarPointerEnd]);
1308
+ const handleVirtualScrollbarWheel = useCallback(
1309
+ (event) => {
1310
+ const horizontalDelta = event.deltaX || (event.shiftKey ? event.deltaY : 0);
1311
+ if (horizontalDelta === 0) return;
1312
+
1313
+ const viewport = scrollViewportRef.current;
1314
+ if (!viewport || resolveVirtualScrollMax(viewport) <= 1) return;
1315
+
1316
+ event.preventDefault();
1317
+ commitVirtualScrollLeft(virtualScrollLeftRef.current + horizontalDelta);
1318
+ measureViewportMaxWidth();
1319
+ latestMeasureEdgeShadowStateRef.current();
1320
+ },
1321
+ [commitVirtualScrollLeft, measureViewportMaxWidth, resolveVirtualScrollMax],
1322
+ );
1323
+
1324
+ const handleCopyMarkdown = useCallback(async () => {
1325
+ if (typeof navigator === "undefined" || !navigator || !navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
1326
+ notify(formatTemplate(labels.copyFailed, "clipboard-unavailable"), "error");
1327
+ return;
1328
+ }
1329
+
1330
+ try {
1331
+ await navigator.clipboard.writeText(buildMarkdownTableText(getRows()));
1332
+ notify(labels.copySucceeded, "success");
1333
+ } catch (error) {
1334
+ const msg = error instanceof Error ? error.message : String(error);
1335
+ notify(formatTemplate(labels.copyFailed, msg), "error");
1336
+ }
1337
+ }, [getRows, labels, notify]);
1338
+
1339
+ const handleDownloadCsv = useCallback(() => {
1340
+ if (
1341
+ typeof document === "undefined" ||
1342
+ typeof Blob === "undefined" ||
1343
+ typeof URL === "undefined" ||
1344
+ typeof URL.createObjectURL !== "function"
1345
+ ) {
1346
+ notify(labels.downloadFailed, "error");
1347
+ return;
1348
+ }
1349
+
1350
+ // Wrap in try/catch (upstream did not): a click()/createObjectURL failure
1351
+ // must surface as an error, never silently succeed.
1352
+ try {
1353
+ const blob = new Blob([buildCsvTableText(getRows())], {
1354
+ type: "text/csv;charset=utf-8",
1355
+ });
1356
+ const url = URL.createObjectURL(blob);
1357
+ const link = document.createElement("a");
1358
+ link.href = url;
1359
+ link.download = "table.csv";
1360
+ document.body.appendChild(link);
1361
+ link.click();
1362
+ link.remove();
1363
+ URL.revokeObjectURL(url);
1364
+ } catch (error) {
1365
+ notify(labels.downloadFailed, "error");
1366
+ }
1367
+ }, [getRows, labels, notify]);
1368
+
1369
+ const handleToggleExpandedScroll = useCallback(() => {
1370
+ const nextExpandedScrollEnabled = !expandedScrollEnabledRef.current;
1371
+ commitExpandedScrollEnabled(nextExpandedScrollEnabled);
1372
+
1373
+ if (!nextExpandedScrollEnabled) {
1374
+ maxViewportLeftOffsetRef.current = 0;
1375
+ commitViewportLeftOffset(0);
1376
+ const viewport = scrollViewportRef.current;
1377
+ if (viewport) {
1378
+ // Collapsing back to normal mode: the existing virtual scroll
1379
+ // position may be consumed by the left borrow; clear leftOffset then
1380
+ // map the same virtual scrollLeft back to the real scrollLeft so the
1381
+ // table content does not jump to the start.
1382
+ viewport.scrollLeft = virtualScrollLeftRef.current;
1383
+ }
1384
+ }
1385
+
1386
+ requestAnimationFrame(() => {
1387
+ measureViewportMaxWidth();
1388
+ latestMeasureEdgeShadowStateRef.current();
1389
+ });
1390
+ }, [commitExpandedScrollEnabled, commitViewportLeftOffset, measureViewportMaxWidth]);
1391
+
1392
+ const copyLabel = labels.copyMarkdown;
1393
+ const downloadLabel = labels.downloadCsv;
1394
+ const previewLabel = labels.openPreview;
1395
+ const scrollModeLabel = expandedScrollEnabled ? labels.collapseScrollMode : labels.expandScrollMode;
1396
+ const previewTitle = labels.previewTitle;
1397
+ const previewDescription = labels.previewDescription;
1398
+
1399
+ // viewportMaxWidth is the virtual scroll slot width, used for virtualScrollMax
1400
+ // and the thumb; the content layer itself translateX-borrows left, so the
1401
+ // borrowed width must be added back on the right or the visual right edge
1402
+ // would slide left with it.
1403
+ const contentViewportMaxWidth =
1404
+ viewportLeftOffset > 0
1405
+ ? `calc(${viewportMaxWidth} + ${Math.ceil(viewportLeftOffset)}px)`
1406
+ : viewportMaxWidth;
1407
+
1408
+ return createElement(
1409
+ "div",
1410
+ { ref: rootRef, className: "ks-md-table-root" },
1411
+ createElement(
1412
+ "div",
1413
+ { className: "ks-md-table-toolbar", "data-markdown-table-toolbar": "" },
1414
+ createElement(
1415
+ MarkdownTableActionButton,
1416
+ { label: copyLabel, onClick: () => {
1417
+ // Fire-and-forget; failures are surfaced via notify().
1418
+ handleCopyMarkdown();
1419
+ } },
1420
+ createElement(CopyIcon, null),
1421
+ ),
1422
+ createElement(
1423
+ MarkdownTableActionButton,
1424
+ { label: downloadLabel, onClick: handleDownloadCsv },
1425
+ createElement(DownloadIcon, null),
1426
+ ),
1427
+ createElement(
1428
+ MarkdownTableActionButton,
1429
+ { label: previewLabel, onClick: () => setPreviewOpen(true) },
1430
+ createElement(MaximizeIcon, null),
1431
+ ),
1432
+ canToggleExpandedScroll
1433
+ ? createElement(
1434
+ MarkdownTableActionButton,
1435
+ {
1436
+ label: scrollModeLabel,
1437
+ onClick: handleToggleExpandedScroll,
1438
+ pressed: expandedScrollEnabled,
1439
+ },
1440
+ expandedScrollEnabled ? createElement(CollapseScrollIcon, null) : createElement(ExpandScrollIcon, null),
1441
+ )
1442
+ : null,
1443
+ !onNotify && status
1444
+ ? createElement(
1445
+ "div",
1446
+ {
1447
+ className: cx("ks-md-table-status", `ks-md-table-status-${status.kind}`),
1448
+ role: "status",
1449
+ "aria-live": "polite",
1450
+ },
1451
+ status.message,
1452
+ )
1453
+ : null,
1454
+ ),
1455
+ createElement(
1456
+ "div",
1457
+ {
1458
+ ref: frameRef,
1459
+ className: "ks-md-table-frame",
1460
+ "data-markdown-table-frame": "",
1461
+ "data-markdown-table-virtual-scroll-sticky": virtualScrollbarSticky ? "true" : "false",
1462
+ },
1463
+ createElement(
1464
+ "div",
1465
+ {
1466
+ className: cx(
1467
+ "ks-md-table-width",
1468
+ viewportLeftOffset <= 0 && "ks-md-table-width-transition",
1469
+ ),
1470
+ style: {
1471
+ maxWidth: contentViewportMaxWidth,
1472
+ transform: viewportLeftOffset > 0 ? `translateX(-${viewportLeftOffset}px)` : undefined,
1473
+ },
1474
+ },
1475
+ createElement(
1476
+ "div",
1477
+ { className: "ks-md-table-viewport-wrap" },
1478
+ createElement(
1479
+ "div",
1480
+ { ref: scrollViewportRef, className: "ks-md-table-scroll" },
1481
+ createElement(
1482
+ "table",
1483
+ {
1484
+ ...props,
1485
+ ref: tableRef,
1486
+ className: cx("ks-md-table", className),
1487
+ "data-streamdown": "table",
1488
+ },
1489
+ children,
1490
+ ),
1491
+ ),
1492
+ createElement(MarkdownTableEdgeShadow, { side: "left", state: edgeShadowState }),
1493
+ createElement(MarkdownTableEdgeShadow, { side: "right", state: edgeShadowState }),
1494
+ ),
1495
+ ),
1496
+ createElement(
1497
+ "div",
1498
+ {
1499
+ ref: virtualScrollbarWrapperRef,
1500
+ "aria-hidden": virtualScrollbarVisible ? "false" : "true",
1501
+ // Under a V4 transformed virtual row, sticky is simulated with
1502
+ // relative + translateY; computed position cannot express the
1503
+ // product semantics, so an explicit state is exposed for cross-
1504
+ // implementation E2E and a11y diagnostics.
1505
+ "data-markdown-table-virtual-scroll-sticky": virtualScrollbarSticky ? "true" : "false",
1506
+ "data-markdown-table-virtual-scroll-visible": virtualScrollbarVisible ? "true" : "false",
1507
+ // The track width must equal frameRect.width + rightDistance minus
1508
+ // the message column's responsive right padding; long tables need
1509
+ // horizontal scrolling while reading vertically, and the sticky
1510
+ // bottom must avoid the composer and bottom panels. When overflow
1511
+ // measurement changes only visibility toggles; the node and its
1512
+ // vertical footprint are preserved so timeline height is not
1513
+ // perturbed.
1514
+ style: { width: scrollbarWidth },
1515
+ className: cx(
1516
+ "ks-md-table-vbar",
1517
+ virtualScrollbarVisible ? "ks-md-table-vbar-visible" : "ks-md-table-vbar-hidden",
1518
+ virtualScrollbarSticky && "ks-md-table-vbar-sticky",
1519
+ ),
1520
+ },
1521
+ createElement(
1522
+ "div",
1523
+ {
1524
+ ref: virtualScrollTrackRef,
1525
+ "aria-hidden": "true",
1526
+ className: "ks-md-table-vtrack",
1527
+ "data-markdown-table-virtual-scroll-track": "",
1528
+ onPointerCancel: handleVirtualScrollbarPointerEnd,
1529
+ onPointerDown: handleVirtualScrollbarPointerDown,
1530
+ onPointerMove: handleVirtualScrollbarPointerMove,
1531
+ onPointerUp: handleVirtualScrollbarPointerEnd,
1532
+ onWheel: handleVirtualScrollbarWheel,
1533
+ },
1534
+ createElement("div", {
1535
+ className: "ks-md-table-vthumb",
1536
+ onPointerCancel: handleVirtualScrollbarThumbPointerEnd,
1537
+ onPointerDown: handleVirtualScrollbarThumbPointerDown,
1538
+ onPointerMove: handleVirtualScrollbarThumbPointerMove,
1539
+ onPointerUp: handleVirtualScrollbarThumbPointerEnd,
1540
+ style: virtualScrollThumbStyle,
1541
+ }),
1542
+ ),
1543
+ ),
1544
+ ),
1545
+ createElement(MarkdownTablePreviewDialog, {
1546
+ open: previewOpen,
1547
+ onClose: () => setPreviewOpen(false),
1548
+ title: previewTitle,
1549
+ description: previewDescription,
1550
+ tableClassName: className,
1551
+ tableProps: props,
1552
+ dialogTheme: theme,
1553
+ children,
1554
+ }),
1555
+ );
1556
+ }
1557
+
1558
+ // ── thead / tbody / tr / th / td (strip the `node` prop Streamdown passes) ──
1559
+
1560
+ function MarkdownTableHeader({ className, node: _node, ...props }) {
1561
+ return createElement("thead", { className: cx("ks-md-table-thead", className), ...props });
1562
+ }
1563
+
1564
+ function MarkdownTableBody({ className, node: _node, ...props }) {
1565
+ return createElement("tbody", { className: cx("ks-md-table-tbody", className), ...props });
1566
+ }
1567
+
1568
+ function MarkdownTableRow({ className, node: _node, ...props }) {
1569
+ return createElement("tr", { className: cx("ks-md-table-tr", className), ...props });
1570
+ }
1571
+
1572
+ function MarkdownTableHead({ className, node: _node, ...props }) {
1573
+ return createElement("th", { className: cx("ks-md-table-th", className), ...props });
1574
+ }
1575
+
1576
+ function MarkdownTableCell({ className, node: _node, ...props }) {
1577
+ return createElement("td", { className: cx("ks-md-table-td", className), ...props });
1578
+ }
1579
+
1580
+ return {
1581
+ table: MarkdownTable,
1582
+ thead: MarkdownTableHeader,
1583
+ tbody: MarkdownTableBody,
1584
+ tr: MarkdownTableRow,
1585
+ th: MarkdownTableHead,
1586
+ td: MarkdownTableCell,
1587
+ };
1588
+ }