@kingsoftcloud/agentkit-harness-ui 0.0.0-stage → 0.0.1
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/LICENSE +201 -0
- package/NOTICE +116 -0
- package/README.md +292 -2
- package/markdown/build/build-markdown.cjs +66 -0
- package/markdown/dist/THIRD_PARTY_LICENSES.txt +2993 -0
- package/markdown/dist/markdown.css +1 -0
- package/markdown/dist/markdown.mjs +325 -0
- package/markdown/source/markdown-factory.mjs +613 -0
- package/markdown/source/markdown.css +288 -0
- package/markdown/source/table-factory.mjs +1588 -0
- package/markdown/source/table.css +487 -0
- package/package.json +96 -4
- package/src/activity-groups.mjs +74 -0
- package/src/browser-response-groups.mjs +57 -0
- package/src/command-output.mjs +57 -0
- package/src/components.mjs +9 -0
- package/src/context-markers.mjs +19 -0
- package/src/context-status.mjs +34 -0
- package/src/diff.mjs +240 -0
- package/src/explore-tool-call.mjs +139 -0
- package/src/file-preview.mjs +50 -0
- package/src/host/native-a2a.mjs +90 -0
- package/src/host/native-child-file-pane.mjs +49 -0
- package/src/host/native-child-pane.mjs +94 -0
- package/src/host/native-command-pane.mjs +64 -0
- package/src/host/native-run-panel.mjs +202 -0
- package/src/host/session-task-events.mjs +247 -0
- package/src/host/state/native-a2a.mjs +122 -0
- package/src/host/state/native-child-observation.mjs +35 -0
- package/src/host/state/native-children.mjs +93 -0
- package/src/host/state/native-outcome.mjs +35 -0
- package/src/host/state/native-recovery.mjs +187 -0
- package/src/host/state/native-runtime-route.mjs +26 -0
- package/src/host/state/native-session-stop.mjs +40 -0
- package/src/host/state/native-status-panel.mjs +23 -0
- package/src/host/state/native-task-read-model.mjs +89 -0
- package/src/host/state/task-links.mjs +40 -0
- package/src/host/task-timeline.mjs +185 -0
- package/src/index.mjs +52 -0
- package/src/input-preview.mjs +44 -0
- package/src/inspector.mjs +40 -0
- package/src/project.mjs +132 -0
- package/src/protocol.mjs +362 -0
- package/src/react-binding.mjs +486 -0
- package/src/reducer.mjs +355 -0
- package/src/renderers.mjs +644 -0
- package/src/status-panel.mjs +1156 -0
- package/src/styles.mjs +278 -0
- package/src/summary-playback.mjs +39 -0
- package/src/summary.mjs +160 -0
- package/src/terminal.mjs +157 -0
- package/src/tool-detail.mjs +208 -0
- package/src/tool-error.mjs +33 -0
- package/src/tool-identity.mjs +40 -0
- package/src/view.mjs +364 -0
- 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
|
+
}
|