@streamsai/ui 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +57 -2
- package/dist/components/ui/agent-avatar.d.ts +50 -0
- package/dist/components/ui/agent-avatar.js +38 -0
- package/dist/components/ui/alert-dialog.d.ts +12 -0
- package/dist/components/ui/alert-dialog.js +37 -0
- package/dist/components/ui/alert.d.ts +10 -0
- package/dist/components/ui/alert.js +28 -0
- package/dist/components/ui/assets/loader.json.js +1 -0
- package/dist/components/ui/avatar-frame.d.ts +11 -0
- package/dist/components/ui/avatar-frame.js +20 -0
- package/dist/components/ui/avatar.d.ts +23 -0
- package/dist/components/ui/avatar.js +54 -0
- package/dist/components/ui/badge.d.ts +7 -0
- package/dist/components/ui/badge.js +36 -0
- package/dist/components/ui/bar-list.d.ts +40 -0
- package/dist/components/ui/bar-list.js +37 -0
- package/dist/components/ui/breadcrumb.d.ts +8 -0
- package/dist/components/ui/breadcrumb.js +21 -0
- package/dist/components/ui/button-group.d.ts +10 -0
- package/dist/components/ui/button-group.js +41 -0
- package/dist/components/ui/button.d.ts +9 -0
- package/dist/components/ui/button.js +39 -0
- package/dist/components/ui/card.d.ts +9 -0
- package/dist/components/ui/card.js +25 -0
- package/dist/components/ui/checkbox.d.ts +3 -0
- package/dist/components/ui/checkbox.js +16 -0
- package/dist/components/ui/chip.d.ts +13 -0
- package/dist/components/ui/chip.js +22 -0
- package/dist/components/ui/clickable.d.ts +27 -0
- package/dist/components/ui/clickable.js +54 -0
- package/dist/components/ui/code-block.d.ts +68 -0
- package/dist/components/ui/code-block.js +112 -0
- package/dist/components/ui/code-view.d.ts +21 -0
- package/dist/components/ui/code-view.js +20 -0
- package/dist/components/ui/collapsible.d.ts +6 -0
- package/dist/components/ui/collapsible.js +18 -0
- package/dist/components/ui/combobox.d.ts +14 -0
- package/dist/components/ui/combobox.js +37 -0
- package/dist/components/ui/command.d.ts +15 -0
- package/dist/components/ui/command.js +34 -0
- package/dist/components/ui/context-menu.d.ts +21 -0
- package/dist/components/ui/context-menu.js +60 -0
- package/dist/components/ui/counter.d.ts +11 -0
- package/dist/components/ui/counter.js +20 -0
- package/dist/components/ui/dialog.d.ts +17 -0
- package/dist/components/ui/dialog.js +41 -0
- package/dist/components/ui/disabled-reason.d.ts +22 -0
- package/dist/components/ui/disabled-reason.js +16 -0
- package/dist/components/ui/dropdown-menu.d.ts +33 -0
- package/dist/components/ui/dropdown-menu.js +58 -0
- package/dist/components/ui/editable-text.d.ts +95 -0
- package/dist/components/ui/editable-text.js +191 -0
- package/dist/components/ui/heading.d.ts +24 -0
- package/dist/components/ui/heading.js +92 -0
- package/dist/components/ui/hover-card.d.ts +9 -0
- package/dist/components/ui/hover-card.js +16 -0
- package/dist/components/ui/icon-button.d.ts +23 -0
- package/dist/components/ui/icon-button.js +32 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/input.js +8 -0
- package/dist/components/ui/kbd.d.ts +12 -0
- package/dist/components/ui/kbd.js +12 -0
- package/dist/components/ui/label.d.ts +3 -0
- package/dist/components/ui/label.js +7 -0
- package/dist/components/ui/loader.d.ts +19 -0
- package/dist/components/ui/loader.js +157 -0
- package/dist/components/ui/message-toolbar.d.ts +20 -0
- package/dist/components/ui/message-toolbar.js +17 -0
- package/dist/components/ui/modal.d.ts +30 -0
- package/dist/components/ui/modal.js +41 -0
- package/dist/components/ui/nav-item.d.ts +25 -0
- package/dist/components/ui/nav-item.js +66 -0
- package/dist/components/ui/panel-section.d.ts +22 -0
- package/dist/components/ui/panel-section.js +26 -0
- package/dist/components/ui/popover.d.ts +14 -0
- package/dist/components/ui/popover.js +30 -0
- package/dist/components/ui/progress.d.ts +5 -0
- package/dist/components/ui/progress.js +16 -0
- package/dist/components/ui/provider-badge.d.ts +22 -0
- package/dist/components/ui/provider-badge.js +38 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/radio-group.js +21 -0
- package/dist/components/ui/reply-summary.d.ts +16 -0
- package/dist/components/ui/reply-summary.js +8 -0
- package/dist/components/ui/resizable.d.ts +8 -0
- package/dist/components/ui/resizable.js +15 -0
- package/dist/components/ui/rich-text-editor.d.ts +447 -0
- package/dist/components/ui/rich-text-editor.js +2419 -0
- package/dist/components/ui/running-indicator.d.ts +17 -0
- package/dist/components/ui/running-indicator.js +34 -0
- package/dist/components/ui/sandboxed-frame.d.ts +29 -0
- package/dist/components/ui/sandboxed-frame.js +77 -0
- package/dist/components/ui/scroll-area.d.ts +14 -0
- package/dist/components/ui/scroll-area.js +17 -0
- package/dist/components/ui/section-header.d.ts +12 -0
- package/dist/components/ui/section-header.js +12 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +40 -0
- package/dist/components/ui/separator.d.ts +3 -0
- package/dist/components/ui/separator.js +13 -0
- package/dist/components/ui/sheet.d.ts +23 -0
- package/dist/components/ui/sheet.js +48 -0
- package/dist/components/ui/shimmer-text.d.ts +26 -0
- package/dist/components/ui/shimmer-text.js +32 -0
- package/dist/components/ui/sidebar.d.ts +63 -0
- package/dist/components/ui/sidebar.js +283 -0
- package/dist/components/ui/skeleton.d.ts +14 -0
- package/dist/components/ui/skeleton.js +12 -0
- package/dist/components/ui/sonner.d.ts +4 -0
- package/dist/components/ui/sonner.js +14 -0
- package/dist/components/ui/spark-bars.d.ts +29 -0
- package/dist/components/ui/spark-bars.js +24 -0
- package/dist/components/ui/spinner.d.ts +16 -0
- package/dist/components/ui/spinner.js +26 -0
- package/dist/components/ui/stacked-bar.d.ts +37 -0
- package/dist/components/ui/stacked-bar.js +56 -0
- package/dist/components/ui/status-dot.d.ts +16 -0
- package/dist/components/ui/status-dot.js +32 -0
- package/dist/components/ui/switch.d.ts +5 -0
- package/dist/components/ui/switch.js +13 -0
- package/dist/components/ui/tabs.d.ts +14 -0
- package/dist/components/ui/tabs.js +55 -0
- package/dist/components/ui/text.d.ts +27 -0
- package/dist/components/ui/text.js +100 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/textarea.js +7 -0
- package/dist/components/ui/theme-toggle.d.ts +21 -0
- package/dist/components/ui/theme-toggle.js +17 -0
- package/dist/components/ui/toggle-group.d.ts +6 -0
- package/dist/components/ui/toggle-group.js +12 -0
- package/dist/components/ui/tooltip.d.ts +11 -0
- package/dist/components/ui/tooltip.js +21 -0
- package/dist/components/ui/trace.d.ts +24 -0
- package/dist/components/ui/trace.js +57 -0
- package/dist/components/ui/trend-line.d.ts +93 -0
- package/dist/components/ui/trend-line.js +215 -0
- package/dist/components/ui/truncate.d.ts +30 -0
- package/dist/components/ui/truncate.js +54 -0
- package/dist/components/ui/viewer-shell.d.ts +55 -0
- package/dist/components/ui/viewer-shell.js +15 -0
- package/dist/components/ui/virtual-list.d.ts +91 -0
- package/dist/components/ui/virtual-list.js +406 -0
- package/dist/components/ui/visually-hidden.d.ts +15 -0
- package/dist/components/ui/visually-hidden.js +19 -0
- package/dist/direction/index.d.ts +19 -0
- package/dist/direction/index.js +32 -0
- package/dist/hooks/use-code-highlighting.d.ts +22 -0
- package/dist/hooks/use-code-highlighting.js +47 -0
- package/dist/hooks/use-is-truncated.d.ts +7 -0
- package/dist/hooks/use-is-truncated.js +23 -0
- package/dist/hooks/use-mobile.d.ts +2 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/icons/custom.d.ts +29 -0
- package/dist/icons/custom.js +48 -0
- package/dist/icons/index.d.ts +49 -0
- package/dist/icons/index.js +50 -0
- package/dist/icons/provider-logos.d.ts +17 -0
- package/dist/icons/provider-logos.js +28 -0
- package/dist/icons/providers.d.ts +9 -0
- package/dist/icons/providers.js +51 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +124 -0
- package/dist/lib/code-highlighter.d.ts +9 -0
- package/dist/lib/code-highlighter.js +65 -0
- package/dist/lib/code-language.d.ts +13 -0
- package/dist/lib/code-language.js +103 -0
- package/dist/lib/code-tokens.d.ts +9 -0
- package/dist/lib/code-tokens.js +27 -0
- package/dist/lib/copy-text.d.ts +6 -0
- package/dist/lib/copy-text.js +46 -0
- package/dist/lib/home-tile.d.ts +12 -0
- package/dist/lib/home-tile.js +45 -0
- package/dist/lib/reveal-mask.d.ts +9 -0
- package/dist/lib/reveal-mask.js +16 -0
- package/dist/lib/text-direction.d.ts +28 -0
- package/dist/lib/text-direction.js +119 -0
- package/dist/lib/utils.d.ts +11 -0
- package/dist/lib/utils.js +30 -0
- package/dist/motion/components.d.ts +184 -0
- package/dist/motion/components.js +211 -0
- package/dist/motion/config.d.ts +849 -0
- package/dist/motion/config.js +429 -0
- package/dist/motion/index.d.ts +8 -0
- package/dist/motion/index.js +8 -0
- package/dist/motion/modal-presence.d.ts +5 -0
- package/dist/motion/modal-presence.js +27 -0
- package/dist/motion/popup.d.ts +77 -0
- package/dist/motion/popup.js +103 -0
- package/dist/platform/index.d.ts +29 -0
- package/dist/platform/index.js +63 -0
- package/dist/styles/base.css +319 -0
- package/dist/styles/fonts/Figtree-Variable.woff2 +0 -0
- package/dist/styles/fonts/OFL.txt +93 -0
- package/dist/styles/fonts/Poppins-Medium.woff2 +0 -0
- package/dist/styles/fonts/Poppins-Regular.woff2 +0 -0
- package/dist/styles/fonts/Poppins-SemiBold.woff2 +0 -0
- package/dist/styles/fonts.css +28 -0
- package/dist/styles/globals.css +8 -0
- package/dist/styles/mcp-host.css +76 -0
- package/dist/styles/tokens.css +387 -0
- package/dist/styles/tokens.d.mts +57 -0
- package/dist/styles/tokens.mjs +59 -0
- package/dist/theme/index.d.ts +28 -0
- package/dist/theme/index.js +65 -0
- package/package.json +136 -3
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { defaultRangeExtractor, measureElement as defaultMeasureElement, useVirtualizer, } from '@tanstack/react-virtual';
|
|
4
|
+
import { animate } from '../../motion/index.js';
|
|
5
|
+
import { glide, presets, transitions } from '../../motion/config.js';
|
|
6
|
+
import { cn } from '../../lib/utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* The virtualizer re-renders the list on every scroll and resize frame (a composer growing, a pane
|
|
9
|
+
* sliding) to move rows; memoising each row's content keeps those frames to repositioning instead
|
|
10
|
+
* of re-rendering every visible row, which in a chat timeline is enough work to drop frames.
|
|
11
|
+
*/
|
|
12
|
+
/** Sub-pixel slack: an offset this close to the end, or to where it was, has not moved. */
|
|
13
|
+
const END_EPSILON_PX = 1;
|
|
14
|
+
/** TanStack's default `resizeItem` predicate, which a custom one replaces. */
|
|
15
|
+
const defaultAdjusts = (item, instance) => {
|
|
16
|
+
const offset = (instance.scrollOffset ?? 0) + instance.scrollAdjustments;
|
|
17
|
+
const measured = instance.itemSizeCache.get(item.key);
|
|
18
|
+
return measured === undefined
|
|
19
|
+
? item.start < offset
|
|
20
|
+
: item.start + measured <= offset && instance.scrollDirection !== 'backward';
|
|
21
|
+
};
|
|
22
|
+
/** virtual-core's iOS WebKit test (not exported), where it holds scroll corrections until rest. */
|
|
23
|
+
const isIOSWebKit = () => typeof navigator !== 'undefined' &&
|
|
24
|
+
(/iP(hone|od|ad)/.test(navigator.userAgent) ||
|
|
25
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 0));
|
|
26
|
+
/** virtual-core defers scroll corrections for 150ms after a touch ends; this adds margin for a late timer. */
|
|
27
|
+
const TOUCH_END_GRACE_MS = 250;
|
|
28
|
+
/** How long after a width change the rows are treated as re-wrapping (the next frames' measures). */
|
|
29
|
+
const REFLOW_WINDOW_MS = 100;
|
|
30
|
+
const NO_INDEXES = [];
|
|
31
|
+
/**
|
|
32
|
+
* While the box changes width its rows re-wrap, and a reader who is not at the end would see the
|
|
33
|
+
* lines they are reading slide down out of view if only rows above the fold were compensated.
|
|
34
|
+
* Compensating every row that lies fully above the visible bottom edge keeps that edge's last
|
|
35
|
+
* line where it is instead. A row whose size was never cached (its first measure matched the
|
|
36
|
+
* estimate, so the library stored nothing) still has that size in `item.size`.
|
|
37
|
+
*/
|
|
38
|
+
const adjustsBottomEdge = (item, instance) => {
|
|
39
|
+
const before = instance.itemSizeCache.get(item.key) ?? item.size;
|
|
40
|
+
const offset = (instance.scrollOffset ?? 0) + instance.scrollAdjustments;
|
|
41
|
+
const visibleEnd = offset + (instance.scrollElement?.clientHeight ?? 0) - (instance.options.paddingEnd ?? 0);
|
|
42
|
+
return item.start + before <= visibleEnd;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Grows `row` from nothing to the height it has now. The virtualizer measures the row every frame
|
|
46
|
+
* (its observer fires before paint) and, on the end, shifts the scroll by each change, so the rows
|
|
47
|
+
* above rise with the growth instead of jumping by the row's whole height at once. The clip is
|
|
48
|
+
* dropped at the end so a later change in the row's height is not cut off.
|
|
49
|
+
*/
|
|
50
|
+
const glideIn = (row) => {
|
|
51
|
+
const natural = row.getBoundingClientRect().height;
|
|
52
|
+
if (natural <= 0)
|
|
53
|
+
return;
|
|
54
|
+
// Before the first paint: `animate` only starts on the next frame, which would show the full height once.
|
|
55
|
+
row.style.overflow = 'hidden';
|
|
56
|
+
row.style.maxHeight = '0px';
|
|
57
|
+
void animate(row, { maxHeight: [0, natural] }, glide).then(() => {
|
|
58
|
+
row.style.overflow = '';
|
|
59
|
+
row.style.maxHeight = '';
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
const VirtualRow = React.memo(function VirtualRow({ item, index, renderItem }) {
|
|
63
|
+
return renderItem(item, index);
|
|
64
|
+
});
|
|
65
|
+
function VirtualList({ items, estimateSize, renderItem, getKey, height, overscan = 3, keepMountedKeys, onEndReached, className, initialOffset, paddingStart, paddingEnd, anchorTo, followOnAppend = false, glideItem, scrollEndThreshold, isScrollingResetDelay, anchorPrependBoundary = false, ref, scrollToIndex = null, scrollToAlign = 'center', scrollPaddingStart, onDebug, }) {
|
|
66
|
+
const scrollRef = React.useRef(null);
|
|
67
|
+
const rowsRef = React.useRef(null);
|
|
68
|
+
const onEndReachedFiredRef = React.useRef(-1);
|
|
69
|
+
const lastFocusedKeyRef = React.useRef(null);
|
|
70
|
+
// `scrollTop` when last seen at the end, else `null`. A reader who moved since is not kept there.
|
|
71
|
+
const endScrollTopRef = React.useRef(null);
|
|
72
|
+
const touchEndedAtRef = React.useRef(-Infinity);
|
|
73
|
+
// Fixed at mount: the boundary hook below is a virtualizer option it captures once.
|
|
74
|
+
const [debug] = React.useState(() => onDebug !== undefined);
|
|
75
|
+
const [boundaryEnabled] = React.useState(anchorPrependBoundary);
|
|
76
|
+
const onDebugRef = React.useRef(onDebug);
|
|
77
|
+
React.useLayoutEffect(() => {
|
|
78
|
+
onDebugRef.current = onDebug;
|
|
79
|
+
}, [onDebug]);
|
|
80
|
+
// The row that was first before the last prepend, until its next measure (`anchorPrependBoundary`).
|
|
81
|
+
const boundaryKeyRef = React.useRef(null);
|
|
82
|
+
// Scroller width at the last row size change, and until when its rows count as re-wrapping.
|
|
83
|
+
const widthRef = React.useRef(null);
|
|
84
|
+
const reflowUntilRef = React.useRef(0);
|
|
85
|
+
// Retires the boundary row when it measures unchanged. Spread only when set: an explicit
|
|
86
|
+
// `undefined` would replace the library's default reader.
|
|
87
|
+
const [hookOptions] = React.useState(() => ({
|
|
88
|
+
...(boundaryEnabled
|
|
89
|
+
? {
|
|
90
|
+
measureElement: (element, entry, instance) => {
|
|
91
|
+
const size = defaultMeasureElement(element, entry, instance);
|
|
92
|
+
const item = instance.measurementsCache[instance.indexFromElement(element)];
|
|
93
|
+
if (!item || item.key !== boundaryKeyRef.current)
|
|
94
|
+
return size;
|
|
95
|
+
const from = instance.itemSizeCache.get(item.key) ?? item.size;
|
|
96
|
+
if (from === size)
|
|
97
|
+
boundaryKeyRef.current = null;
|
|
98
|
+
return size;
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
}));
|
|
103
|
+
const getItemKey = React.useCallback((index) => getKey(items[index], index), [items, getKey]);
|
|
104
|
+
const keptIndexes = React.useMemo(() => {
|
|
105
|
+
if (!keepMountedKeys || keepMountedKeys.size === 0)
|
|
106
|
+
return NO_INDEXES;
|
|
107
|
+
const found = [];
|
|
108
|
+
items.forEach((item, index) => {
|
|
109
|
+
if (keepMountedKeys.has(getKey(item, index)))
|
|
110
|
+
found.push(index);
|
|
111
|
+
});
|
|
112
|
+
return found.length > 0 ? found : NO_INDEXES;
|
|
113
|
+
}, [items, getKey, keepMountedKeys]);
|
|
114
|
+
const rangeExtractor = React.useCallback((range) => [...new Set([...defaultRangeExtractor(range), ...keptIndexes])].sort((a, b) => a - b), [keptIndexes]);
|
|
115
|
+
// eslint-disable-next-line react-hooks/incompatible-library
|
|
116
|
+
const virtualizer = useVirtualizer({
|
|
117
|
+
count: items.length,
|
|
118
|
+
getScrollElement: () => scrollRef.current,
|
|
119
|
+
estimateSize,
|
|
120
|
+
overscan,
|
|
121
|
+
initialOffset,
|
|
122
|
+
paddingStart,
|
|
123
|
+
scrollPaddingStart,
|
|
124
|
+
paddingEnd,
|
|
125
|
+
anchorTo,
|
|
126
|
+
followOnAppend,
|
|
127
|
+
scrollEndThreshold,
|
|
128
|
+
isScrollingResetDelay,
|
|
129
|
+
getItemKey,
|
|
130
|
+
// Spread only when set: an explicit `undefined` would replace the library's default.
|
|
131
|
+
...(keptIndexes.length > 0 ? { rangeExtractor } : {}),
|
|
132
|
+
...hookOptions,
|
|
133
|
+
});
|
|
134
|
+
// Replaces the library's default rule (which `defaultAdjusts` mirrors) with one that also knows
|
|
135
|
+
// the prepend boundary row and the re-wrap window after a width change.
|
|
136
|
+
React.useLayoutEffect(() => {
|
|
137
|
+
const scroller = scrollRef.current;
|
|
138
|
+
// Called from the row measures and from a scroller observer, whichever the browser delivers
|
|
139
|
+
// first in a frame, so the window is open before the first re-wrapped row is judged.
|
|
140
|
+
const noteWidth = () => {
|
|
141
|
+
const width = scroller?.clientWidth ?? null;
|
|
142
|
+
if (widthRef.current !== null && width !== widthRef.current)
|
|
143
|
+
reflowUntilRef.current = performance.now() + REFLOW_WINDOW_MS;
|
|
144
|
+
widthRef.current = width;
|
|
145
|
+
};
|
|
146
|
+
noteWidth();
|
|
147
|
+
const observer = scroller ? new ResizeObserver(noteWidth) : null;
|
|
148
|
+
if (scroller)
|
|
149
|
+
observer?.observe(scroller);
|
|
150
|
+
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item, _delta, instance) => {
|
|
151
|
+
if (item.key === boundaryKeyRef.current) {
|
|
152
|
+
boundaryKeyRef.current = null;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
noteWidth();
|
|
156
|
+
// iOS would write the sum of these at rest, one jump after the rows already moved in motion.
|
|
157
|
+
if (instance.isScrolling && isIOSWebKit())
|
|
158
|
+
return false;
|
|
159
|
+
return performance.now() < reflowUntilRef.current
|
|
160
|
+
? adjustsBottomEdge(item, instance)
|
|
161
|
+
: defaultAdjusts(item, instance);
|
|
162
|
+
};
|
|
163
|
+
return () => {
|
|
164
|
+
observer?.disconnect();
|
|
165
|
+
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = undefined;
|
|
166
|
+
};
|
|
167
|
+
}, [virtualizer]);
|
|
168
|
+
// virtual-core ends its iOS touch window on `touchend` only, so a cancelled touch (a system
|
|
169
|
+
// gesture, an alert) would leave it deferring scroll corrections until the next scroll event.
|
|
170
|
+
React.useLayoutEffect(() => {
|
|
171
|
+
const scroller = scrollRef.current;
|
|
172
|
+
if (!scroller)
|
|
173
|
+
return;
|
|
174
|
+
const onTouchEnd = () => {
|
|
175
|
+
touchEndedAtRef.current = performance.now();
|
|
176
|
+
};
|
|
177
|
+
const onTouchCancel = () => {
|
|
178
|
+
onDebugRef.current?.('touchcancel ended the virtualizer touch window', {});
|
|
179
|
+
scroller.dispatchEvent(new Event('touchend'));
|
|
180
|
+
};
|
|
181
|
+
scroller.addEventListener('touchend', onTouchEnd, { passive: true });
|
|
182
|
+
scroller.addEventListener('touchcancel', onTouchCancel, { passive: true });
|
|
183
|
+
return () => {
|
|
184
|
+
scroller.removeEventListener('touchend', onTouchEnd);
|
|
185
|
+
scroller.removeEventListener('touchcancel', onTouchCancel);
|
|
186
|
+
};
|
|
187
|
+
}, []);
|
|
188
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
189
|
+
const totalSize = virtualizer.getTotalSize();
|
|
190
|
+
const focusKey = scrollToIndex != null && items[scrollToIndex]
|
|
191
|
+
? getKey(items[scrollToIndex], scrollToIndex)
|
|
192
|
+
: null;
|
|
193
|
+
const scrollToEnd = React.useCallback((options) => {
|
|
194
|
+
virtualizer.scrollToEnd(options);
|
|
195
|
+
const scroller = scrollRef.current;
|
|
196
|
+
// A smooth scroll is not there yet; its arrival's scroll event records the end.
|
|
197
|
+
endScrollTopRef.current =
|
|
198
|
+
scroller && options?.behavior !== 'smooth' ? scroller.scrollTop : null;
|
|
199
|
+
}, [virtualizer]);
|
|
200
|
+
React.useImperativeHandle(ref, () => ({
|
|
201
|
+
// Not a raw `scrollTop` assignment: `estimateSize`'s guess for not-yet-rendered rows can be
|
|
202
|
+
// wrong, and the virtualizer's own measurement-correction logic then fights a manual
|
|
203
|
+
// assignment made before it has measured the real rows near that offset — same reason
|
|
204
|
+
// `scrollToIndex` below goes through the virtualizer instead of the DOM directly.
|
|
205
|
+
scrollToOffset: (offset) => virtualizer.scrollToOffset(offset, { align: 'start' }),
|
|
206
|
+
scrollToIndex: (index, options) => virtualizer.scrollToIndex(index, options),
|
|
207
|
+
scrollToEnd,
|
|
208
|
+
isAtEnd: (threshold) => virtualizer.isAtEnd(threshold),
|
|
209
|
+
isScrolling: () => virtualizer.isScrolling || performance.now() - touchEndedAtRef.current < TOUCH_END_GRACE_MS,
|
|
210
|
+
cancelScroll: () => virtualizer.cancelScroll(),
|
|
211
|
+
getItemForOffset: (offset) => {
|
|
212
|
+
const item = virtualizer.getVirtualItemForOffset(offset);
|
|
213
|
+
return item ? { key: String(item.key), index: item.index, start: item.start } : undefined;
|
|
214
|
+
},
|
|
215
|
+
getItemStart: (index) => virtualizer.measurementsCache[index]?.start,
|
|
216
|
+
scrollToKey: (key, offset) => {
|
|
217
|
+
const index = items.findIndex((item, i) => getKey(item, i) === key);
|
|
218
|
+
if (index === -1)
|
|
219
|
+
return false;
|
|
220
|
+
const start = virtualizer.measurementsCache[index]?.start;
|
|
221
|
+
if (offset === undefined || start === undefined)
|
|
222
|
+
virtualizer.scrollToIndex(index, { align: 'start' });
|
|
223
|
+
else
|
|
224
|
+
virtualizer.scrollToOffset(start + offset, { align: 'start' });
|
|
225
|
+
return true;
|
|
226
|
+
},
|
|
227
|
+
}), [virtualizer, items, getKey, scrollToEnd]);
|
|
228
|
+
React.useEffect(() => {
|
|
229
|
+
if (!onEndReached || virtualItems.length === 0)
|
|
230
|
+
return;
|
|
231
|
+
// A kept row far from the viewport is not the reader reaching the end.
|
|
232
|
+
const range = virtualizer.range;
|
|
233
|
+
const lastIndex = keptIndexes.length > 0 && range
|
|
234
|
+
? Math.min(range.endIndex + overscan, items.length - 1)
|
|
235
|
+
: virtualItems[virtualItems.length - 1]?.index;
|
|
236
|
+
if (lastIndex === undefined)
|
|
237
|
+
return;
|
|
238
|
+
if (lastIndex === items.length - 1 && onEndReachedFiredRef.current !== items.length) {
|
|
239
|
+
onEndReachedFiredRef.current = items.length;
|
|
240
|
+
onEndReached();
|
|
241
|
+
}
|
|
242
|
+
}, [virtualItems, virtualizer, keptIndexes, overscan, items.length, onEndReached]);
|
|
243
|
+
React.useLayoutEffect(() => {
|
|
244
|
+
if (scrollToIndex == null || focusKey == null)
|
|
245
|
+
return;
|
|
246
|
+
// Keyed, not indexed: a prepend shifts the same item's index and must not re-scroll to it.
|
|
247
|
+
if (lastFocusedKeyRef.current === focusKey)
|
|
248
|
+
return;
|
|
249
|
+
lastFocusedKeyRef.current = focusKey;
|
|
250
|
+
onDebugRef.current?.('scrollToIndex (deep link)', { index: scrollToIndex, key: focusKey });
|
|
251
|
+
virtualizer.scrollToIndex(scrollToIndex, { align: scrollToAlign });
|
|
252
|
+
}, [scrollToIndex, scrollToAlign, focusKey, virtualizer]);
|
|
253
|
+
// Debug: log item and padding changes per commit. Runs before the end keeper.
|
|
254
|
+
const debugSnapshotRef = React.useRef(null);
|
|
255
|
+
React.useLayoutEffect(() => {
|
|
256
|
+
if (!debug)
|
|
257
|
+
return;
|
|
258
|
+
const keyAt = (index) => (items[index] ? getKey(items[index], index) : null);
|
|
259
|
+
const next = {
|
|
260
|
+
items,
|
|
261
|
+
count: items.length,
|
|
262
|
+
firstKey: keyAt(0),
|
|
263
|
+
lastKey: keyAt(items.length - 1),
|
|
264
|
+
totalSize,
|
|
265
|
+
paddingStart,
|
|
266
|
+
paddingEnd,
|
|
267
|
+
};
|
|
268
|
+
const prev = debugSnapshotRef.current;
|
|
269
|
+
debugSnapshotRef.current = next;
|
|
270
|
+
if (!prev)
|
|
271
|
+
return;
|
|
272
|
+
// A refetch with the same edges is not a prepend, append or reset.
|
|
273
|
+
if (prev.items !== items &&
|
|
274
|
+
(prev.count !== next.count ||
|
|
275
|
+
prev.firstKey !== next.firstKey ||
|
|
276
|
+
prev.lastKey !== next.lastKey))
|
|
277
|
+
onDebugRef.current?.('items', {
|
|
278
|
+
countBefore: prev.count,
|
|
279
|
+
countAfter: next.count,
|
|
280
|
+
firstKeyBefore: prev.firstKey,
|
|
281
|
+
firstKeyAfter: next.firstKey,
|
|
282
|
+
lastKeyBefore: prev.lastKey,
|
|
283
|
+
lastKeyAfter: next.lastKey,
|
|
284
|
+
totalSizeBefore: prev.totalSize,
|
|
285
|
+
totalSizeAfter: totalSize,
|
|
286
|
+
});
|
|
287
|
+
if (prev.paddingStart !== paddingStart || prev.paddingEnd !== paddingEnd)
|
|
288
|
+
onDebugRef.current?.('padding', {
|
|
289
|
+
paddingStartBefore: prev.paddingStart,
|
|
290
|
+
paddingStartAfter: paddingStart,
|
|
291
|
+
paddingEndBefore: prev.paddingEnd,
|
|
292
|
+
paddingEndAfter: paddingEnd,
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
// The virtualizer sees row size changes; these are the three it does not see.
|
|
296
|
+
const keepEnd = React.useCallback((cause) => {
|
|
297
|
+
const scroller = scrollRef.current;
|
|
298
|
+
const endTop = endScrollTopRef.current;
|
|
299
|
+
if (!scroller || endTop === null)
|
|
300
|
+
return;
|
|
301
|
+
if (Math.abs(scroller.scrollTop - endTop) >= END_EPSILON_PX)
|
|
302
|
+
return;
|
|
303
|
+
if (scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop < END_EPSILON_PX)
|
|
304
|
+
return;
|
|
305
|
+
onDebugRef.current?.(`scrollToEnd (keeper ${cause})`, {});
|
|
306
|
+
scrollToEnd();
|
|
307
|
+
}, [scrollToEnd]);
|
|
308
|
+
React.useLayoutEffect(() => {
|
|
309
|
+
const scroller = scrollRef.current;
|
|
310
|
+
if (!followOnAppend || !scroller)
|
|
311
|
+
return;
|
|
312
|
+
// Registered after the virtualizer's own listener, so `isAtEnd` reads this event's offset.
|
|
313
|
+
const onScroll = () => {
|
|
314
|
+
endScrollTopRef.current = virtualizer.isAtEnd() ? scroller.scrollTop : null;
|
|
315
|
+
};
|
|
316
|
+
const observer = new ResizeObserver(() => keepEnd('box'));
|
|
317
|
+
observer.observe(scroller);
|
|
318
|
+
scroller.addEventListener('scroll', onScroll, { passive: true });
|
|
319
|
+
return () => {
|
|
320
|
+
observer.disconnect();
|
|
321
|
+
scroller.removeEventListener('scroll', onScroll);
|
|
322
|
+
};
|
|
323
|
+
}, [followOnAppend, virtualizer, keepEnd]);
|
|
324
|
+
// The library follows only when the count grows; a last row that changes without the count
|
|
325
|
+
// growing (a swap) leaves the reader behind by the height difference.
|
|
326
|
+
const lastKey = items.length > 0 ? getKey(items[items.length - 1], items.length - 1) : null;
|
|
327
|
+
const edgeRef = React.useRef({ lastKey, count: items.length });
|
|
328
|
+
React.useLayoutEffect(() => {
|
|
329
|
+
const prev = edgeRef.current;
|
|
330
|
+
edgeRef.current = { lastKey, count: items.length };
|
|
331
|
+
if (!followOnAppend || prev.lastKey === null || lastKey === prev.lastKey)
|
|
332
|
+
return;
|
|
333
|
+
if (items.length > prev.count)
|
|
334
|
+
return;
|
|
335
|
+
keepEnd('swap');
|
|
336
|
+
}, [lastKey, items.length, followOnAppend, keepEnd]);
|
|
337
|
+
const paddingEndRef = React.useRef(paddingEnd);
|
|
338
|
+
React.useLayoutEffect(() => {
|
|
339
|
+
if (paddingEndRef.current === paddingEnd)
|
|
340
|
+
return;
|
|
341
|
+
paddingEndRef.current = paddingEnd;
|
|
342
|
+
if (followOnAppend)
|
|
343
|
+
keepEnd('paddingEnd');
|
|
344
|
+
}, [paddingEnd, followOnAppend, keepEnd]);
|
|
345
|
+
// After a prepend, track the old first row until it measures (`anchorPrependBoundary`).
|
|
346
|
+
const firstKeyRef = React.useRef(null);
|
|
347
|
+
React.useLayoutEffect(() => {
|
|
348
|
+
if (!boundaryEnabled)
|
|
349
|
+
return;
|
|
350
|
+
const first = items[0] ? getKey(items[0], 0) : null;
|
|
351
|
+
const previous = firstKeyRef.current;
|
|
352
|
+
firstKeyRef.current = first;
|
|
353
|
+
if (previous === null || previous === first)
|
|
354
|
+
return;
|
|
355
|
+
if (!items.some((item, i) => i > 0 && getKey(item, i) === previous))
|
|
356
|
+
return;
|
|
357
|
+
boundaryKeyRef.current = previous;
|
|
358
|
+
}, [boundaryEnabled, items, getKey]);
|
|
359
|
+
// New keys after the first known key are appends; they fade in.
|
|
360
|
+
const knownKeysRef = React.useRef(null);
|
|
361
|
+
const glideItemRef = React.useRef(glideItem);
|
|
362
|
+
React.useLayoutEffect(() => {
|
|
363
|
+
glideItemRef.current = glideItem;
|
|
364
|
+
}, [glideItem]);
|
|
365
|
+
React.useLayoutEffect(() => {
|
|
366
|
+
const rowsEl = rowsRef.current;
|
|
367
|
+
const keys = items.map((item, i) => getKey(item, i));
|
|
368
|
+
const known = knownKeysRef.current;
|
|
369
|
+
knownKeysRef.current = new Set(keys);
|
|
370
|
+
if (!rowsEl || !known)
|
|
371
|
+
return;
|
|
372
|
+
const firstKnown = keys.findIndex((key) => known.has(key));
|
|
373
|
+
if (firstKnown === -1)
|
|
374
|
+
return;
|
|
375
|
+
// Only a list that stays on its end glides: a reader higher up does not see the row arrive.
|
|
376
|
+
const scroller = scrollRef.current;
|
|
377
|
+
const gliding = followOnAppend &&
|
|
378
|
+
scroller !== null &&
|
|
379
|
+
!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches &&
|
|
380
|
+
scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop <=
|
|
381
|
+
(scrollEndThreshold ?? 1);
|
|
382
|
+
for (let index = firstKnown + 1; index < keys.length; index++) {
|
|
383
|
+
if (known.has(keys[index]))
|
|
384
|
+
continue;
|
|
385
|
+
const row = rowsEl.querySelector(`[data-index="${index}"]`);
|
|
386
|
+
if (!row)
|
|
387
|
+
continue;
|
|
388
|
+
const glides = gliding && glideItemRef.current?.(items[index]) === true;
|
|
389
|
+
void animate(row, { opacity: [presets.fade.initial.opacity, 1] }, glides ? glide : transitions.enter);
|
|
390
|
+
if (glides)
|
|
391
|
+
glideIn(row);
|
|
392
|
+
}
|
|
393
|
+
}, [items, getKey, followOnAppend, scrollEndThreshold]);
|
|
394
|
+
React.useEffect(() => {
|
|
395
|
+
if (scrollToIndex == null)
|
|
396
|
+
lastFocusedKeyRef.current = null;
|
|
397
|
+
}, [scrollToIndex]);
|
|
398
|
+
return (_jsx("div", { ref: scrollRef, "data-slot": "virtual-list", role: "list", className: cn('overflow-auto', className), style: { height }, children: _jsx("div", { ref: rowsRef, style: { height: totalSize, width: '100%', position: 'relative' }, children: virtualItems.map((virtualItem) => (_jsx("div", { ref: virtualizer.measureElement, "data-index": virtualItem.index, "data-slot": "virtual-list-item", role: "listitem", style: {
|
|
399
|
+
position: 'absolute',
|
|
400
|
+
top: 0,
|
|
401
|
+
left: 0,
|
|
402
|
+
width: '100%',
|
|
403
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
404
|
+
}, children: _jsx(VirtualRow, { item: items[virtualItem.index], index: virtualItem.index, renderItem: renderItem }) }, virtualItem.key))) }) }));
|
|
405
|
+
}
|
|
406
|
+
export { VirtualList };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type VariantProps } from 'class-variance-authority';
|
|
2
|
+
/**
|
|
3
|
+
* Applies the standard clip-rect visually-hidden technique (Tailwind `sr-only`).
|
|
4
|
+
*
|
|
5
|
+
* Content is removed from the visual viewport but remains present in the DOM
|
|
6
|
+
* and fully accessible to screen readers and the ARIA tree. Use this instead
|
|
7
|
+
* of `display:none` or `visibility:hidden`, which also remove the element from
|
|
8
|
+
* assistive technology.
|
|
9
|
+
*
|
|
10
|
+
* Common uses: supplemental labels for icon-only buttons, skip-link anchors,
|
|
11
|
+
* table summary text, and any case where visible context is missing.
|
|
12
|
+
*/
|
|
13
|
+
declare const visuallyHiddenVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
14
|
+
declare function VisuallyHidden({ className, ...props }: React.ComponentProps<'span'> & VariantProps<typeof visuallyHiddenVariants>): import("react").JSX.Element;
|
|
15
|
+
export { VisuallyHidden, visuallyHiddenVariants };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cva } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../../lib/utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Applies the standard clip-rect visually-hidden technique (Tailwind `sr-only`).
|
|
6
|
+
*
|
|
7
|
+
* Content is removed from the visual viewport but remains present in the DOM
|
|
8
|
+
* and fully accessible to screen readers and the ARIA tree. Use this instead
|
|
9
|
+
* of `display:none` or `visibility:hidden`, which also remove the element from
|
|
10
|
+
* assistive technology.
|
|
11
|
+
*
|
|
12
|
+
* Common uses: supplemental labels for icon-only buttons, skip-link anchors,
|
|
13
|
+
* table summary text, and any case where visible context is missing.
|
|
14
|
+
*/
|
|
15
|
+
const visuallyHiddenVariants = cva('sr-only');
|
|
16
|
+
function VisuallyHidden({ className, ...props }) {
|
|
17
|
+
return (_jsx("span", { "data-slot": "visually-hidden", className: cn(visuallyHiddenVariants({ className })), ...props }));
|
|
18
|
+
}
|
|
19
|
+
export { VisuallyHidden, visuallyHiddenVariants };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @no-story Context provider — renders no UI; sets `dir` on <html>.
|
|
3
|
+
*/
|
|
4
|
+
import { type ReactNode } from 'react';
|
|
5
|
+
export type TextDirection = 'ltr' | 'rtl';
|
|
6
|
+
interface DirectionContextValue {
|
|
7
|
+
direction: TextDirection;
|
|
8
|
+
}
|
|
9
|
+
interface DirectionProviderProps {
|
|
10
|
+
/** Direction implied by the active i18n language (`i18next.dir(lng)`). Defaults to `'ltr'`. */
|
|
11
|
+
localeDirection?: TextDirection;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
declare function DirectionProvider({ localeDirection, children }: DirectionProviderProps): import("react").JSX.Element;
|
|
15
|
+
declare function useDirection(): DirectionContextValue;
|
|
16
|
+
/** `useDirection` for primitives that may render outside a provider (stories, tests): falls back to `'ltr'`. */
|
|
17
|
+
declare function useTextDirection(): TextDirection;
|
|
18
|
+
export type { DirectionProviderProps };
|
|
19
|
+
export { DirectionProvider, useDirection, useTextDirection };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @no-story Context provider — renders no UI; sets `dir` on <html>.
|
|
4
|
+
*/
|
|
5
|
+
import { createContext, useContext, useEffect, useMemo } from 'react';
|
|
6
|
+
const DirectionContext = createContext(null);
|
|
7
|
+
function applyDirection(direction) {
|
|
8
|
+
if (typeof document === 'undefined')
|
|
9
|
+
return;
|
|
10
|
+
document.documentElement.dir = direction;
|
|
11
|
+
}
|
|
12
|
+
function DirectionProvider({ localeDirection = 'ltr', children }) {
|
|
13
|
+
const direction = localeDirection;
|
|
14
|
+
// Synchronous during render on the client so portals mounted in the same commit see `dir`.
|
|
15
|
+
applyDirection(direction);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
applyDirection(direction);
|
|
18
|
+
}, [direction]);
|
|
19
|
+
const value = useMemo(() => ({ direction }), [direction]);
|
|
20
|
+
return _jsx(DirectionContext.Provider, { value: value, children: children });
|
|
21
|
+
}
|
|
22
|
+
function useDirection() {
|
|
23
|
+
const ctx = useContext(DirectionContext);
|
|
24
|
+
if (ctx === null)
|
|
25
|
+
throw new Error('useDirection() must be called inside a <DirectionProvider>.');
|
|
26
|
+
return ctx;
|
|
27
|
+
}
|
|
28
|
+
/** `useDirection` for primitives that may render outside a provider (stories, tests): falls back to `'ltr'`. */
|
|
29
|
+
function useTextDirection() {
|
|
30
|
+
return useContext(DirectionContext)?.direction ?? 'ltr';
|
|
31
|
+
}
|
|
32
|
+
export { DirectionProvider, useDirection, useTextDirection };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CodeLanguage } from '../lib/code-language.js';
|
|
2
|
+
import type { CodeLines } from '../lib/code-highlighter.js';
|
|
3
|
+
/** Above either limit the text stays plain: tokenising it would stall the main thread. */
|
|
4
|
+
export declare const CODE_VIEW_MAX_CHARS = 500000;
|
|
5
|
+
export declare const CODE_VIEW_MAX_LINES = 10000;
|
|
6
|
+
export declare const exceedsHighlightLimits: (code: string) => boolean;
|
|
7
|
+
type UseCodeHighlightingOptions = {
|
|
8
|
+
code: string;
|
|
9
|
+
/** `null` (unknown language) never loads the highlighter. */
|
|
10
|
+
language: CodeLanguage | null;
|
|
11
|
+
/** False keeps the text plain, e.g. while a fenced block is still being written. */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** Called when the highlighter or a grammar fails to load or throws; the plain text stays. */
|
|
14
|
+
onError?: (error: unknown) => void;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Tokens for `code`, or `null` while plain text should be shown (not ready, disabled, too large,
|
|
18
|
+
* unknown language, or failed). Loads the Shiki engine lazily. A result for earlier code or
|
|
19
|
+
* language is never returned against the current ones.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useCodeHighlighting({ code, language, enabled, onError, }: UseCodeHighlightingOptions): CodeLines | null;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
/** Above either limit the text stays plain: tokenising it would stall the main thread. */
|
|
3
|
+
export const CODE_VIEW_MAX_CHARS = 500_000;
|
|
4
|
+
export const CODE_VIEW_MAX_LINES = 10_000;
|
|
5
|
+
export const exceedsHighlightLimits = (code) => {
|
|
6
|
+
if (code.length > CODE_VIEW_MAX_CHARS)
|
|
7
|
+
return true;
|
|
8
|
+
let lines = 1;
|
|
9
|
+
for (let i = code.indexOf('\n'); i >= 0; i = code.indexOf('\n', i + 1)) {
|
|
10
|
+
if (++lines > CODE_VIEW_MAX_LINES)
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Tokens for `code`, or `null` while plain text should be shown (not ready, disabled, too large,
|
|
17
|
+
* unknown language, or failed). Loads the Shiki engine lazily. A result for earlier code or
|
|
18
|
+
* language is never returned against the current ones.
|
|
19
|
+
*/
|
|
20
|
+
export function useCodeHighlighting({ code, language, enabled = true, onError, }) {
|
|
21
|
+
const skip = !enabled || language === null || exceedsHighlightLimits(code);
|
|
22
|
+
const [highlighted, setHighlighted] = useState(null);
|
|
23
|
+
const onErrorRef = useRef(onError);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
onErrorRef.current = onError;
|
|
26
|
+
});
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (skip || language === null)
|
|
29
|
+
return;
|
|
30
|
+
let cancelled = false;
|
|
31
|
+
import('../lib/code-highlighter.js')
|
|
32
|
+
.then(({ highlightCode }) => highlightCode(code, language))
|
|
33
|
+
.then((lines) => {
|
|
34
|
+
if (!cancelled)
|
|
35
|
+
setHighlighted({ code, lang: language, lines });
|
|
36
|
+
})
|
|
37
|
+
.catch((error) => {
|
|
38
|
+
if (!cancelled)
|
|
39
|
+
onErrorRef.current?.(error);
|
|
40
|
+
});
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true;
|
|
43
|
+
};
|
|
44
|
+
}, [code, language, skip]);
|
|
45
|
+
const current = !skip && highlighted !== null && highlighted.code === code && highlighted.lang === language;
|
|
46
|
+
return current ? highlighted.lines : null;
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* True while the referenced element (or a descendant that truncates on its own, e.g. a nested
|
|
4
|
+
* `truncate` span) is clipped with an ellipsis. Re-measures on resize and whenever `content`
|
|
5
|
+
* changes. `content` is the text being shown; pass `undefined` to opt out.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useIsTruncated(ref: RefObject<HTMLElement | null>, content: string | undefined): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from 'react';
|
|
2
|
+
const isOverflowing = (node) => node.scrollWidth > node.clientWidth;
|
|
3
|
+
/**
|
|
4
|
+
* True while the referenced element (or a descendant that truncates on its own, e.g. a nested
|
|
5
|
+
* `truncate` span) is clipped with an ellipsis. Re-measures on resize and whenever `content`
|
|
6
|
+
* changes. `content` is the text being shown; pass `undefined` to opt out.
|
|
7
|
+
*/
|
|
8
|
+
export function useIsTruncated(ref, content) {
|
|
9
|
+
const [truncated, setTruncated] = useState(false);
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
const el = ref.current;
|
|
12
|
+
if (!el || content === undefined) {
|
|
13
|
+
setTruncated(false);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const measure = () => setTruncated(isOverflowing(el) || Array.from(el.querySelectorAll('*')).some(isOverflowing));
|
|
17
|
+
measure();
|
|
18
|
+
const observer = new ResizeObserver(measure);
|
|
19
|
+
observer.observe(el);
|
|
20
|
+
return () => observer.disconnect();
|
|
21
|
+
}, [ref, content]);
|
|
22
|
+
return truncated;
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
const MOBILE_BREAKPOINT = 768;
|
|
3
|
+
const QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
|
|
4
|
+
const subscribe = (onChange) => {
|
|
5
|
+
if (typeof window.matchMedia !== 'function')
|
|
6
|
+
return () => { };
|
|
7
|
+
const mql = window.matchMedia(QUERY);
|
|
8
|
+
mql.addEventListener('change', onChange);
|
|
9
|
+
return () => mql.removeEventListener('change', onChange);
|
|
10
|
+
};
|
|
11
|
+
const getSnapshot = () => typeof window.matchMedia === 'function' ? window.matchMedia(QUERY).matches : false;
|
|
12
|
+
/** True below the tablet breakpoint. Environments without `matchMedia` (jsdom) count as desktop. */
|
|
13
|
+
export function useIsMobile() {
|
|
14
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SVGAttributes } from 'react';
|
|
2
|
+
export interface CustomIconProps extends SVGAttributes<SVGElement> {
|
|
3
|
+
size?: string | number;
|
|
4
|
+
}
|
|
5
|
+
/** The stream glyph in the Figma left pane ("Wave-1 Streamline Ultimate"): one soft wave. */
|
|
6
|
+
export declare const Wave: (props: CustomIconProps) => import("react").JSX.Element;
|
|
7
|
+
/** Figma "Icon / Views / Workload": a hollow ring, used for Catch up and for sessions with no status. */
|
|
8
|
+
export declare const Workload: (props: CustomIconProps) => import("react").JSX.Element;
|
|
9
|
+
/** Figma "Icon / Platform / AI Agent": a robot head with a sparkle. */
|
|
10
|
+
export declare const AIAgent: (props: CustomIconProps) => import("react").JSX.Element;
|
|
11
|
+
/** monday icons "SlashCommand": a slash in a rounded box, ahead of a run's `/trigger` name. */
|
|
12
|
+
export declare const SlashCommand: (props: CustomIconProps) => import("react").JSX.Element;
|
|
13
|
+
/** Figma "stop-circle" (cycle run Stop): a ring around a rounded square. */
|
|
14
|
+
export declare const StopCircle: (props: CustomIconProps) => import("react").JSX.Element;
|
|
15
|
+
/** Stream menu Unpin: pinned pin with a strike-through (design export Unpin.svg). */
|
|
16
|
+
export declare const Unpin: (props: CustomIconProps) => import("react").JSX.Element;
|
|
17
|
+
/** Stream menu Kanban: three uneven columns (design export Kanban.svg). Vibe has no Kanban glyph. */
|
|
18
|
+
export declare const Kanban: (props: CustomIconProps) => import("react").JSX.Element;
|
|
19
|
+
export declare const ChartLine: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
20
|
+
export declare const ChartArea: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
21
|
+
export declare const ChartBar: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
22
|
+
export declare const ChartBarHorizontal: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
23
|
+
export declare const ChartStacked: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
24
|
+
export declare const ChartDonut: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
25
|
+
export declare const ChartScatter: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
26
|
+
export declare const ChartFunnel: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
27
|
+
export declare const ChartGauge: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
28
|
+
export declare const ChartHeatmap: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|
|
29
|
+
export declare const ChartGantt: ({ size, ...props }: CustomIconProps) => import("react").JSX.Element;
|