@hilum/ui 3.4.0 → 3.5.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/README.md +4 -0
- package/dist/{chunk-MAHPAP65.js → chunk-RI6QJR2L.js} +6 -3
- package/dist/chunk-RI6QJR2L.js.map +1 -0
- package/dist/index.d.ts +789 -115
- package/dist/index.js +10404 -1099
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +17 -0
- package/dist/tokens.d.ts +3 -0
- package/dist/tokens.js +1 -1
- package/llms.txt +18 -4
- package/package.json +19 -1
- package/src/components/accordion.tsx +746 -58
- package/src/components/alert-dialog.tsx +11 -5
- package/src/components/ask-user-questions.tsx +1366 -0
- package/src/components/badge.tsx +105 -24
- package/src/components/button.tsx +207 -38
- package/src/components/card.tsx +3 -2
- package/src/components/chat-message.tsx +116 -0
- package/src/components/checkbox-group.tsx +403 -0
- package/src/components/checkbox.tsx +6 -3
- package/src/components/color-input.tsx +3 -1
- package/src/components/color-picker.tsx +1992 -80
- package/src/components/dialog.tsx +141 -81
- package/src/components/dropdown-menu.tsx +21 -13
- package/src/components/dropdown.tsx +245 -0
- package/src/components/file-thumbnail.tsx +156 -0
- package/src/components/hover-card.tsx +2 -0
- package/src/components/input-copy.tsx +307 -0
- package/src/components/input-group.tsx +303 -108
- package/src/components/input-message.tsx +883 -0
- package/src/components/input.tsx +6 -3
- package/src/components/menu-item.tsx +156 -0
- package/src/components/mobile-drawer.tsx +50 -0
- package/src/components/nav-item.tsx +43 -0
- package/src/components/nav-menu.tsx +47 -0
- package/src/components/popover.tsx +3 -1
- package/src/components/radio-group.tsx +381 -35
- package/src/components/scroll-area.tsx +9 -2
- package/src/components/select.tsx +708 -121
- package/src/components/sheet.tsx +15 -6
- package/src/components/slider.tsx +1583 -20
- package/src/components/status-badge.tsx +2 -0
- package/src/components/switch.tsx +245 -28
- package/src/components/table.tsx +191 -77
- package/src/components/tabs-subtle.tsx +411 -0
- package/src/components/tabs.tsx +459 -49
- package/src/components/textarea.tsx +7 -3
- package/src/components/thinking-indicator.tsx +106 -0
- package/src/components/thinking-steps.tsx +382 -0
- package/src/components/tooltip.tsx +158 -33
- package/src/hooks/use-merge-split.tsx +359 -0
- package/src/hooks/use-proximity-hover.ts +208 -0
- package/src/index.ts +14 -0
- package/src/lib/elevated.tsx +52 -0
- package/src/lib/font-weight.ts +18 -0
- package/src/lib/icon-context.tsx +98 -0
- package/src/lib/icon-map.tsx +669 -0
- package/src/lib/interaction.ts +102 -0
- package/src/lib/scroll-fade.tsx +241 -0
- package/src/lib/shape-context.tsx +121 -0
- package/src/lib/springs.ts +30 -0
- package/src/lib/surface-classes.ts +43 -0
- package/src/lib/surface-context.tsx +17 -0
- package/src/tokens/tokens.ts +3 -0
- package/dist/chunk-MAHPAP65.js.map +0 -1
package/src/components/tabs.tsx
CHANGED
|
@@ -1,55 +1,465 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
createContext,
|
|
10
|
+
useContext,
|
|
11
|
+
forwardRef,
|
|
12
|
+
Children,
|
|
13
|
+
cloneElement,
|
|
14
|
+
isValidElement,
|
|
15
|
+
type ComponentPropsWithoutRef,
|
|
16
|
+
} from "react";
|
|
17
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
18
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
19
|
+
import type { IconComponent } from "../lib/icon-context";
|
|
5
20
|
import { cn } from "../lib/utils";
|
|
21
|
+
import { spring } from "../lib/springs";
|
|
22
|
+
import { fontWeights } from "../lib/font-weight";
|
|
23
|
+
import { useShape } from "../lib/shape-context";
|
|
24
|
+
import { useSurface } from "../lib/surface-context";
|
|
25
|
+
import { surfaceClasses } from "../lib/surface-classes";
|
|
26
|
+
import { useProximityHover } from "../hooks/use-proximity-hover";
|
|
27
|
+
|
|
28
|
+
/* ─────────────────────── Contexts ─────────────────────── */
|
|
29
|
+
|
|
30
|
+
interface TabsValueOrderContextValue {
|
|
31
|
+
valueOrder: string[];
|
|
32
|
+
setValueOrder: (order: string[]) => void;
|
|
33
|
+
selectedValue: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TabsValueOrderContext = createContext<TabsValueOrderContextValue | null>(null);
|
|
37
|
+
|
|
38
|
+
interface TabsListContextValue {
|
|
39
|
+
registerTab: (index: number, value: string, el: HTMLElement | null) => void;
|
|
40
|
+
hoveredIndex: number | null;
|
|
41
|
+
selectedValue: string | undefined;
|
|
42
|
+
/** Optimistically set selectedIdx so the indicator moves immediately on click. */
|
|
43
|
+
setOptimisticIdx: (index: number) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const TabsListContext = createContext<TabsListContextValue | null>(null);
|
|
47
|
+
|
|
48
|
+
function useTabsList() {
|
|
49
|
+
const ctx = useContext(TabsListContext);
|
|
50
|
+
if (!ctx) throw new Error("TabItem must be used within a TabsList");
|
|
51
|
+
return ctx;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ─────────────────────── Tabs (Root) ─────────────────────── */
|
|
55
|
+
|
|
56
|
+
interface TabsProps extends Omit<
|
|
57
|
+
ComponentPropsWithoutRef<typeof TabsPrimitive.Root>,
|
|
58
|
+
"onValueChange" | "onSelect"
|
|
59
|
+
> {
|
|
60
|
+
/** Controlled value (takes precedence over selectedIndex). */
|
|
61
|
+
value?: string;
|
|
62
|
+
/** Called when the active tab changes. */
|
|
63
|
+
onValueChange?: (value: string) => void;
|
|
64
|
+
/** Index-based controlled alternative. */
|
|
65
|
+
selectedIndex?: number;
|
|
66
|
+
/** Called with the new index when the active tab changes. */
|
|
67
|
+
onSelect?: (index: number) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const Tabs = forwardRef<HTMLDivElement, TabsProps>(
|
|
71
|
+
({ value, onValueChange, selectedIndex, onSelect, defaultValue, children, ...props }, ref) => {
|
|
72
|
+
const [valueOrder, setValueOrder] = useState<string[]>([]);
|
|
73
|
+
const [uncontrolledValue, setUncontrolledValue] = useState<string | undefined>(defaultValue);
|
|
74
|
+
const updateValueOrder = useCallback((order: string[]) => {
|
|
75
|
+
setValueOrder((current) => {
|
|
76
|
+
if (
|
|
77
|
+
current.length === order.length &&
|
|
78
|
+
current.every((value, index) => value === order[index])
|
|
79
|
+
) {
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
return order;
|
|
83
|
+
});
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
// Resolve value: explicit value > selectedIndex lookup > defaultValue
|
|
87
|
+
const resolvedValue =
|
|
88
|
+
value ?? (selectedIndex != null ? valueOrder[selectedIndex] : uncontrolledValue);
|
|
89
|
+
|
|
90
|
+
const handleValueChange = useCallback(
|
|
91
|
+
(newValue: string) => {
|
|
92
|
+
if (value === undefined && selectedIndex == null) {
|
|
93
|
+
setUncontrolledValue(newValue);
|
|
94
|
+
}
|
|
95
|
+
onValueChange?.(newValue);
|
|
96
|
+
if (onSelect) {
|
|
97
|
+
const idx = valueOrder.indexOf(newValue);
|
|
98
|
+
if (idx !== -1) onSelect(idx);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[onValueChange, onSelect, valueOrder],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<TabsValueOrderContext.Provider
|
|
106
|
+
value={{
|
|
107
|
+
valueOrder,
|
|
108
|
+
setValueOrder: updateValueOrder,
|
|
109
|
+
selectedValue: resolvedValue,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<TabsPrimitive.Root
|
|
113
|
+
ref={ref}
|
|
114
|
+
onValueChange={handleValueChange}
|
|
115
|
+
{...(resolvedValue !== undefined ? { value: resolvedValue } : {})}
|
|
116
|
+
{...(resolvedValue == null && defaultValue !== undefined ? { defaultValue } : {})}
|
|
117
|
+
activationMode="automatic"
|
|
118
|
+
{...props}
|
|
119
|
+
>
|
|
120
|
+
{children}
|
|
121
|
+
</TabsPrimitive.Root>
|
|
122
|
+
</TabsValueOrderContext.Provider>
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
Tabs.displayName = "Tabs";
|
|
128
|
+
|
|
129
|
+
/* ─────────────────────── TabsList ─────────────────────── */
|
|
130
|
+
|
|
131
|
+
type TabsListProps = ComponentPropsWithoutRef<typeof TabsPrimitive.List>;
|
|
132
|
+
|
|
133
|
+
const TabsList = forwardRef<HTMLDivElement, TabsListProps>(
|
|
134
|
+
({ children, className, ...props }, ref) => {
|
|
135
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
136
|
+
const isMouseInside = useRef(false);
|
|
137
|
+
const shape = useShape();
|
|
138
|
+
const substrate = useSurface();
|
|
139
|
+
// Active pill lifts 3 levels above substrate (1 above the muted track + 2 for pop).
|
|
140
|
+
// On the page (substrate 1) this lands on surface 4 — matches the original design.
|
|
141
|
+
// Inside a dialog (substrate 5) it lifts to surface 8 instead of staying at 4.
|
|
142
|
+
const indicatorLevel = Math.min(substrate + 3, 8);
|
|
143
|
+
const valueOrderCtx = useContext(TabsValueOrderContext);
|
|
144
|
+
const [optimisticIdx, setOptimisticIdx] = useState<number | null>(null);
|
|
145
|
+
|
|
146
|
+
// Derive value order from children synchronously
|
|
147
|
+
const values = Children.toArray(children)
|
|
148
|
+
.filter(isValidElement)
|
|
149
|
+
.map((child) => (child.props as { value?: string }).value)
|
|
150
|
+
.filter((v): v is string => typeof v === "string");
|
|
151
|
+
const valueOrderKey = values.join(",");
|
|
152
|
+
const setValueOrder = valueOrderCtx?.setValueOrder;
|
|
153
|
+
|
|
154
|
+
// Report value order up to Tabs root
|
|
155
|
+
useLayoutEffect(() => {
|
|
156
|
+
setValueOrder?.(values);
|
|
157
|
+
}, [setValueOrder, valueOrderKey]);
|
|
158
|
+
|
|
159
|
+
// Proximity hover
|
|
160
|
+
const {
|
|
161
|
+
activeIndex: hoveredIndex,
|
|
162
|
+
setActiveIndex: setHoveredIndex,
|
|
163
|
+
itemRects,
|
|
164
|
+
handlers,
|
|
165
|
+
registerItem,
|
|
166
|
+
measureItems,
|
|
167
|
+
} = useProximityHover(containerRef, { axis: "x" });
|
|
168
|
+
|
|
169
|
+
// Register items: bridge from (index, value, el) → registerItem(index, el)
|
|
170
|
+
const registerTab = useCallback(
|
|
171
|
+
(index: number, _value: string, el: HTMLElement | null) => {
|
|
172
|
+
registerItem(index, el);
|
|
173
|
+
},
|
|
174
|
+
[registerItem],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Measure on children change
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
measureItems();
|
|
180
|
+
}, [measureItems, children]);
|
|
181
|
+
|
|
182
|
+
// Remeasure on resize
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
const el = containerRef.current;
|
|
185
|
+
if (!el) return;
|
|
186
|
+
const ro = new ResizeObserver(() => measureItems());
|
|
187
|
+
ro.observe(el);
|
|
188
|
+
return () => ro.disconnect();
|
|
189
|
+
}, [measureItems]);
|
|
190
|
+
|
|
191
|
+
// Track mouse inside
|
|
192
|
+
const handleMouseMove = useCallback(
|
|
193
|
+
(e: React.MouseEvent) => {
|
|
194
|
+
isMouseInside.current = true;
|
|
195
|
+
handlers.onMouseMove(e);
|
|
196
|
+
},
|
|
197
|
+
[handlers],
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const handleMouseLeave = useCallback(() => {
|
|
201
|
+
isMouseInside.current = false;
|
|
202
|
+
handlers.onMouseLeave();
|
|
203
|
+
}, [handlers]);
|
|
204
|
+
|
|
205
|
+
// Focus ring
|
|
206
|
+
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
207
|
+
const selectedValue = valueOrderCtx?.selectedValue;
|
|
208
|
+
const selectedIdx = selectedValue !== undefined ? values.indexOf(selectedValue) : -1;
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
setOptimisticIdx(selectedIdx >= 0 ? selectedIdx : null);
|
|
212
|
+
}, [selectedIdx]);
|
|
213
|
+
|
|
214
|
+
const activeSelectedIdx = optimisticIdx;
|
|
215
|
+
const selectedRect = activeSelectedIdx !== null ? itemRects[activeSelectedIdx] : null;
|
|
216
|
+
const hoverRect = hoveredIndex !== null ? itemRects[hoveredIndex] : null;
|
|
217
|
+
const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null;
|
|
218
|
+
const isHoveringSelected = hoveredIndex === activeSelectedIdx;
|
|
219
|
+
const isHovering = hoveredIndex !== null && !isHoveringSelected;
|
|
220
|
+
|
|
221
|
+
// Auto-assign _index to children
|
|
222
|
+
const indexedChildren = Children.map(children, (child, i) => {
|
|
223
|
+
if (isValidElement(child)) {
|
|
224
|
+
return cloneElement(child, { _index: i } as Record<string, unknown>);
|
|
225
|
+
}
|
|
226
|
+
return child;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<TabsListContext.Provider
|
|
231
|
+
value={{
|
|
232
|
+
registerTab,
|
|
233
|
+
hoveredIndex,
|
|
234
|
+
selectedValue,
|
|
235
|
+
setOptimisticIdx,
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
<TabsPrimitive.List
|
|
239
|
+
ref={(node) => {
|
|
240
|
+
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
241
|
+
if (typeof ref === "function") ref(node);
|
|
242
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
243
|
+
}}
|
|
244
|
+
onMouseMove={handleMouseMove}
|
|
245
|
+
onMouseLeave={handleMouseLeave}
|
|
246
|
+
onFocus={(e) => {
|
|
247
|
+
const trigger = (e.target as HTMLElement).closest('[role="tab"]');
|
|
248
|
+
if (!trigger) return;
|
|
249
|
+
const indexAttr = trigger.getAttribute("data-proximity-index");
|
|
250
|
+
if (indexAttr != null) {
|
|
251
|
+
const idx = Number(indexAttr);
|
|
252
|
+
setHoveredIndex(idx);
|
|
253
|
+
setFocusedIndex((e.target as HTMLElement).matches(":focus-visible") ? idx : null);
|
|
254
|
+
}
|
|
255
|
+
}}
|
|
256
|
+
onBlur={(e) => {
|
|
257
|
+
if (containerRef.current?.contains(e.relatedTarget as Node)) return;
|
|
258
|
+
setFocusedIndex(null);
|
|
259
|
+
if (isMouseInside.current) return;
|
|
260
|
+
setHoveredIndex(null);
|
|
261
|
+
}}
|
|
262
|
+
className={cn(
|
|
263
|
+
"relative inline-flex items-center gap-0.5 p-1 select-none bg-muted",
|
|
264
|
+
shape.container,
|
|
265
|
+
className,
|
|
266
|
+
)}
|
|
267
|
+
{...props}
|
|
268
|
+
>
|
|
269
|
+
{/* Active segment indicator */}
|
|
270
|
+
{selectedRect && (
|
|
271
|
+
<motion.div
|
|
272
|
+
className={cn(
|
|
273
|
+
"absolute pointer-events-none",
|
|
274
|
+
surfaceClasses(indicatorLevel),
|
|
275
|
+
shape.bg,
|
|
276
|
+
)}
|
|
277
|
+
initial={false}
|
|
278
|
+
animate={{
|
|
279
|
+
left: selectedRect.left,
|
|
280
|
+
width: selectedRect.width,
|
|
281
|
+
top: selectedRect.top,
|
|
282
|
+
height: selectedRect.height,
|
|
283
|
+
opacity: isHovering ? 0.85 : 1,
|
|
284
|
+
}}
|
|
285
|
+
transition={{
|
|
286
|
+
...spring.moderate,
|
|
287
|
+
opacity: { duration: 0.08 },
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
{/* Hover indicator */}
|
|
293
|
+
<AnimatePresence>
|
|
294
|
+
{hoverRect && !isHoveringSelected && selectedRect && (
|
|
295
|
+
<motion.div
|
|
296
|
+
className={cn("absolute pointer-events-none bg-hover", shape.bg)}
|
|
297
|
+
initial={{
|
|
298
|
+
left: selectedRect.left,
|
|
299
|
+
width: selectedRect.width,
|
|
300
|
+
top: selectedRect.top,
|
|
301
|
+
height: selectedRect.height,
|
|
302
|
+
opacity: 0,
|
|
303
|
+
}}
|
|
304
|
+
animate={{
|
|
305
|
+
left: hoverRect.left,
|
|
306
|
+
width: hoverRect.width,
|
|
307
|
+
top: hoverRect.top,
|
|
308
|
+
height: hoverRect.height,
|
|
309
|
+
opacity: 0.4,
|
|
310
|
+
}}
|
|
311
|
+
exit={
|
|
312
|
+
!isMouseInside.current && selectedRect
|
|
313
|
+
? {
|
|
314
|
+
left: selectedRect.left,
|
|
315
|
+
width: selectedRect.width,
|
|
316
|
+
top: selectedRect.top,
|
|
317
|
+
height: selectedRect.height,
|
|
318
|
+
opacity: 0,
|
|
319
|
+
transition: {
|
|
320
|
+
...spring.moderate,
|
|
321
|
+
opacity: { duration: 0.06 },
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
: { opacity: 0, transition: spring.fast.exit }
|
|
325
|
+
}
|
|
326
|
+
transition={{
|
|
327
|
+
...spring.fast,
|
|
328
|
+
opacity: { duration: 0.08 },
|
|
329
|
+
}}
|
|
330
|
+
/>
|
|
331
|
+
)}
|
|
332
|
+
</AnimatePresence>
|
|
333
|
+
|
|
334
|
+
{/* Focus ring */}
|
|
335
|
+
<AnimatePresence>
|
|
336
|
+
{focusRect && (
|
|
337
|
+
<motion.div
|
|
338
|
+
className={cn(
|
|
339
|
+
"absolute pointer-events-none z-20 border border-[#6B97FF]",
|
|
340
|
+
shape.focusRing,
|
|
341
|
+
)}
|
|
342
|
+
initial={false}
|
|
343
|
+
animate={{
|
|
344
|
+
left: focusRect.left - 2,
|
|
345
|
+
top: focusRect.top - 2,
|
|
346
|
+
width: focusRect.width + 4,
|
|
347
|
+
height: focusRect.height + 4,
|
|
348
|
+
}}
|
|
349
|
+
exit={{ opacity: 0, transition: spring.fast.exit }}
|
|
350
|
+
transition={{
|
|
351
|
+
...spring.fast,
|
|
352
|
+
opacity: { duration: 0.08 },
|
|
353
|
+
}}
|
|
354
|
+
/>
|
|
355
|
+
)}
|
|
356
|
+
</AnimatePresence>
|
|
357
|
+
|
|
358
|
+
{indexedChildren}
|
|
359
|
+
</TabsPrimitive.List>
|
|
360
|
+
</TabsListContext.Provider>
|
|
361
|
+
);
|
|
362
|
+
},
|
|
363
|
+
);
|
|
6
364
|
|
|
7
|
-
const TabsRoot = React.forwardRef<
|
|
8
|
-
React.ComponentRef<typeof Tabs.Root>,
|
|
9
|
-
React.ComponentPropsWithoutRef<typeof Tabs.Root>
|
|
10
|
-
>(({ className, ...props }, ref) => (
|
|
11
|
-
<Tabs.Root ref={ref} className={cn("w-full", className)} {...props} />
|
|
12
|
-
));
|
|
13
|
-
TabsRoot.displayName = "Tabs";
|
|
14
|
-
|
|
15
|
-
const TabsList = React.forwardRef<
|
|
16
|
-
React.ComponentRef<typeof Tabs.List>,
|
|
17
|
-
React.ComponentPropsWithoutRef<typeof Tabs.List>
|
|
18
|
-
>(({ className, ...props }, ref) => (
|
|
19
|
-
<Tabs.List
|
|
20
|
-
ref={ref}
|
|
21
|
-
className={cn("inline-flex items-center gap-1 border-b border-border w-full", className)}
|
|
22
|
-
{...props}
|
|
23
|
-
/>
|
|
24
|
-
));
|
|
25
365
|
TabsList.displayName = "TabsList";
|
|
26
366
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
367
|
+
/* ─────────────────────── TabItem ─────────────────────── */
|
|
368
|
+
|
|
369
|
+
interface TabItemProps extends ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> {
|
|
370
|
+
/** Unique value for this tab. */
|
|
371
|
+
value: string;
|
|
372
|
+
/** Optional leading icon. */
|
|
373
|
+
icon?: IconComponent;
|
|
374
|
+
/** Text label. */
|
|
375
|
+
label?: string;
|
|
376
|
+
/** @internal Auto-assigned by TabsList. */
|
|
377
|
+
_index?: number;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const TabItem = forwardRef<HTMLButtonElement, TabItemProps>(
|
|
381
|
+
({ value, icon: Icon, label, children, _index = 0, className, ...props }, ref) => {
|
|
382
|
+
const internalRef = useRef<HTMLButtonElement>(null);
|
|
383
|
+
const { registerTab, hoveredIndex, selectedValue, setOptimisticIdx } = useTabsList();
|
|
384
|
+
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
registerTab(_index, value, internalRef.current);
|
|
387
|
+
return () => registerTab(_index, value, null);
|
|
388
|
+
}, [_index, value, registerTab]);
|
|
389
|
+
|
|
390
|
+
const isSelected = selectedValue === value;
|
|
391
|
+
const isActive = hoveredIndex === _index || isSelected;
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<TabsPrimitive.Trigger
|
|
395
|
+
onClick={() => setOptimisticIdx(_index)}
|
|
396
|
+
ref={(node) => {
|
|
397
|
+
(internalRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;
|
|
398
|
+
if (typeof ref === "function") ref(node);
|
|
399
|
+
else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;
|
|
400
|
+
}}
|
|
401
|
+
value={value}
|
|
402
|
+
data-proximity-index={_index}
|
|
403
|
+
className={cn(
|
|
404
|
+
"relative z-10 flex items-center gap-2 px-3 py-1.5 cursor-pointer bg-transparent border-none outline-none",
|
|
405
|
+
className,
|
|
406
|
+
)}
|
|
407
|
+
{...props}
|
|
408
|
+
>
|
|
409
|
+
{Icon && (
|
|
410
|
+
<Icon
|
|
411
|
+
size={16}
|
|
412
|
+
strokeWidth={isActive ? 2 : 1.5}
|
|
413
|
+
className={cn(
|
|
414
|
+
"transition-[color,stroke-width] duration-80",
|
|
415
|
+
isActive ? "text-foreground" : "text-muted-foreground",
|
|
416
|
+
)}
|
|
417
|
+
/>
|
|
418
|
+
)}
|
|
419
|
+
<span className="inline-grid text-[13px] whitespace-nowrap">
|
|
420
|
+
<span
|
|
421
|
+
className="col-start-1 row-start-1 invisible"
|
|
422
|
+
style={{ fontVariationSettings: fontWeights.semibold }}
|
|
423
|
+
aria-hidden="true"
|
|
424
|
+
>
|
|
425
|
+
{label ?? children}
|
|
426
|
+
</span>
|
|
427
|
+
<span
|
|
428
|
+
className={cn(
|
|
429
|
+
"col-start-1 row-start-1 transition-[color,font-variation-settings] duration-80",
|
|
430
|
+
isActive ? "text-foreground" : "text-muted-foreground",
|
|
431
|
+
)}
|
|
432
|
+
style={{
|
|
433
|
+
fontVariationSettings: isSelected ? fontWeights.semibold : fontWeights.normal,
|
|
434
|
+
}}
|
|
435
|
+
>
|
|
436
|
+
{label ?? children}
|
|
437
|
+
</span>
|
|
438
|
+
</span>
|
|
439
|
+
</TabsPrimitive.Trigger>
|
|
440
|
+
);
|
|
441
|
+
},
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
TabItem.displayName = "TabItem";
|
|
445
|
+
|
|
446
|
+
/* ─────────────────────── TabPanel ─────────────────────── */
|
|
447
|
+
|
|
448
|
+
interface TabPanelProps extends ComponentPropsWithoutRef<typeof TabsPrimitive.Content> {
|
|
449
|
+
/** Must match a TabItem value. */
|
|
450
|
+
value: string;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(({ className, ...props }, ref) => {
|
|
454
|
+
return <TabsPrimitive.Content ref={ref} className={cn("outline-none", className)} {...props} />;
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
TabPanel.displayName = "TabPanel";
|
|
458
|
+
|
|
459
|
+
/* ─────────────────────── Exports ─────────────────────── */
|
|
460
|
+
|
|
461
|
+
const TabsTrigger = TabItem;
|
|
462
|
+
const TabsContent = TabPanel;
|
|
463
|
+
|
|
464
|
+
export { Tabs, TabsList, TabItem, TabPanel, TabsTrigger, TabsContent };
|
|
465
|
+
export type { TabsProps, TabsListProps, TabItemProps, TabPanelProps };
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cn } from "../lib/utils";
|
|
3
|
+
import { controlSurfaceClasses, focusRingClasses, motionClasses } from "../lib/interaction";
|
|
3
4
|
|
|
4
5
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
5
6
|
return (
|
|
6
7
|
<textarea
|
|
7
8
|
data-slot="textarea"
|
|
8
9
|
className={cn(
|
|
9
|
-
"flex min-h-[80px] w-full rounded-md
|
|
10
|
+
"flex min-h-[80px] w-full rounded-md px-3 py-2",
|
|
10
11
|
"body text-foreground placeholder:text-muted-foreground",
|
|
11
|
-
"resize-none
|
|
12
|
-
|
|
12
|
+
"resize-none",
|
|
13
|
+
controlSurfaceClasses,
|
|
14
|
+
motionClasses,
|
|
15
|
+
focusRingClasses,
|
|
16
|
+
"focus-visible:border-brand-primary",
|
|
13
17
|
"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted",
|
|
14
18
|
className,
|
|
15
19
|
)}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useState, useEffect, type HTMLAttributes } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { fontWeights } from "../lib/font-weight";
|
|
7
|
+
|
|
8
|
+
const circleA =
|
|
9
|
+
"M 12 8 C 14.21 8 16 9.79 16 12 C 16 14.21 14.21 16 12 16 C 9.79 16 8 14.21 8 12 C 8 9.79 9.79 8 12 8 Z";
|
|
10
|
+
|
|
11
|
+
const infinity =
|
|
12
|
+
"M 12 12 C 14 8.5 19 8.5 19 12 C 19 15.5 14 15.5 12 12 C 10 8.5 5 8.5 5 12 C 5 15.5 10 15.5 12 12 Z";
|
|
13
|
+
|
|
14
|
+
const circleB =
|
|
15
|
+
"M 12 16 C 14.21 16 16 14.21 16 12 C 16 9.79 14.21 8 12 8 C 9.79 8 8 9.79 8 12 C 8 14.21 9.79 16 12 16 Z";
|
|
16
|
+
|
|
17
|
+
const words = ["Thinking", "Moonwalking", "Planning", "Refining"];
|
|
18
|
+
|
|
19
|
+
interface ThinkingIndicatorProps extends HTMLAttributes<HTMLDivElement> {
|
|
20
|
+
label?: string;
|
|
21
|
+
/** Show the morphing circle⇄infinity glyph before the label. Set to `false`
|
|
22
|
+
* for a text-only indicator (e.g. inline before a streamed reply). */
|
|
23
|
+
showIcon?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ThinkingIndicator = forwardRef<HTMLDivElement, ThinkingIndicatorProps>(
|
|
27
|
+
({ className, label, showIcon = true, ...props }, ref) => {
|
|
28
|
+
const [index, setIndex] = useState(0);
|
|
29
|
+
const currentLabel = label ?? words[index];
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const interval = setInterval(() => {
|
|
33
|
+
setIndex((i) => (i + 1) % words.length);
|
|
34
|
+
}, 4000);
|
|
35
|
+
return () => clearInterval(interval);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
ref={ref}
|
|
41
|
+
role="status"
|
|
42
|
+
aria-label={currentLabel}
|
|
43
|
+
className={cn("flex items-center gap-2 px-3 py-2", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{showIcon && (
|
|
47
|
+
<motion.svg
|
|
48
|
+
aria-hidden
|
|
49
|
+
width={20}
|
|
50
|
+
height={20}
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
strokeWidth={1.5}
|
|
55
|
+
strokeLinecap="round"
|
|
56
|
+
strokeLinejoin="round"
|
|
57
|
+
className="text-muted-foreground shrink-0"
|
|
58
|
+
>
|
|
59
|
+
<motion.path
|
|
60
|
+
animate={{
|
|
61
|
+
d: [circleA, infinity, circleB, infinity, circleA],
|
|
62
|
+
}}
|
|
63
|
+
transition={{
|
|
64
|
+
d: {
|
|
65
|
+
duration: 6,
|
|
66
|
+
ease: "easeInOut",
|
|
67
|
+
repeat: Infinity,
|
|
68
|
+
times: [0, 0.25, 0.5, 0.75, 1.0],
|
|
69
|
+
},
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
</motion.svg>
|
|
73
|
+
)}
|
|
74
|
+
<span
|
|
75
|
+
className="inline-grid text-[13px] overflow-hidden"
|
|
76
|
+
style={{ fontVariationSettings: fontWeights.medium }}
|
|
77
|
+
>
|
|
78
|
+
<span className="col-start-1 row-start-1 invisible shimmer-text" aria-hidden="true">
|
|
79
|
+
{"\u00a0"}
|
|
80
|
+
</span>
|
|
81
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
82
|
+
<motion.span
|
|
83
|
+
key={currentLabel}
|
|
84
|
+
className="col-start-1 row-start-1 shimmer-text"
|
|
85
|
+
initial={{ y: "80%", opacity: 0 }}
|
|
86
|
+
animate={{ y: 0, opacity: 1, transition: { duration: 0.24, ease: [0.4, 0, 0.2, 1] } }}
|
|
87
|
+
exit={{
|
|
88
|
+
y: "-80%",
|
|
89
|
+
opacity: 0,
|
|
90
|
+
transition: { duration: 0.16, ease: [0.4, 0, 0.2, 1] },
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{currentLabel}
|
|
94
|
+
</motion.span>
|
|
95
|
+
</AnimatePresence>
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
ThinkingIndicator.displayName = "ThinkingIndicator";
|
|
103
|
+
|
|
104
|
+
export { ThinkingIndicator };
|
|
105
|
+
export type { ThinkingIndicatorProps };
|
|
106
|
+
export default ThinkingIndicator;
|