@hilum/ui 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +4 -0
  2. package/dist/{chunk-MAHPAP65.js → chunk-RI6QJR2L.js} +6 -3
  3. package/dist/chunk-RI6QJR2L.js.map +1 -0
  4. package/dist/index.d.ts +789 -115
  5. package/dist/index.js +10404 -1099
  6. package/dist/index.js.map +1 -1
  7. package/dist/tokens.css +17 -0
  8. package/dist/tokens.d.ts +3 -0
  9. package/dist/tokens.js +1 -1
  10. package/llms.txt +18 -4
  11. package/package.json +19 -1
  12. package/src/components/accordion.tsx +746 -58
  13. package/src/components/alert-dialog.tsx +11 -5
  14. package/src/components/ask-user-questions.tsx +1366 -0
  15. package/src/components/badge.tsx +105 -24
  16. package/src/components/button.tsx +207 -38
  17. package/src/components/card.tsx +3 -2
  18. package/src/components/chat-message.tsx +116 -0
  19. package/src/components/checkbox-group.tsx +403 -0
  20. package/src/components/checkbox.tsx +6 -3
  21. package/src/components/color-input.tsx +3 -1
  22. package/src/components/color-picker.tsx +1992 -80
  23. package/src/components/dialog.tsx +141 -81
  24. package/src/components/dropdown-menu.tsx +21 -13
  25. package/src/components/dropdown.tsx +245 -0
  26. package/src/components/file-thumbnail.tsx +156 -0
  27. package/src/components/hover-card.tsx +2 -0
  28. package/src/components/input-copy.tsx +307 -0
  29. package/src/components/input-group.tsx +303 -108
  30. package/src/components/input-message.tsx +883 -0
  31. package/src/components/input.tsx +6 -3
  32. package/src/components/menu-item.tsx +156 -0
  33. package/src/components/mobile-drawer.tsx +50 -0
  34. package/src/components/nav-item.tsx +43 -0
  35. package/src/components/nav-menu.tsx +47 -0
  36. package/src/components/popover.tsx +3 -1
  37. package/src/components/radio-group.tsx +381 -35
  38. package/src/components/scroll-area.tsx +9 -2
  39. package/src/components/select.tsx +708 -121
  40. package/src/components/sheet.tsx +15 -6
  41. package/src/components/slider.tsx +1583 -20
  42. package/src/components/status-badge.tsx +2 -0
  43. package/src/components/switch.tsx +245 -28
  44. package/src/components/table.tsx +191 -77
  45. package/src/components/tabs-subtle.tsx +411 -0
  46. package/src/components/tabs.tsx +459 -49
  47. package/src/components/textarea.tsx +7 -3
  48. package/src/components/thinking-indicator.tsx +106 -0
  49. package/src/components/thinking-steps.tsx +382 -0
  50. package/src/components/tooltip.tsx +158 -33
  51. package/src/hooks/use-merge-split.tsx +359 -0
  52. package/src/hooks/use-proximity-hover.ts +208 -0
  53. package/src/index.ts +14 -0
  54. package/src/lib/elevated.tsx +52 -0
  55. package/src/lib/font-weight.ts +18 -0
  56. package/src/lib/icon-context.tsx +98 -0
  57. package/src/lib/icon-map.tsx +669 -0
  58. package/src/lib/interaction.ts +102 -0
  59. package/src/lib/scroll-fade.tsx +241 -0
  60. package/src/lib/shape-context.tsx +121 -0
  61. package/src/lib/springs.ts +30 -0
  62. package/src/lib/surface-classes.ts +43 -0
  63. package/src/lib/surface-context.tsx +17 -0
  64. package/src/tokens/tokens.ts +3 -0
  65. package/dist/chunk-MAHPAP65.js.map +0 -1
@@ -0,0 +1,411 @@
1
+ "use client";
2
+
3
+ import {
4
+ useId,
5
+ useRef,
6
+ useState,
7
+ useCallback,
8
+ useEffect,
9
+ createContext,
10
+ useContext,
11
+ forwardRef,
12
+ type ReactNode,
13
+ type HTMLAttributes,
14
+ } from "react";
15
+ import { motion, AnimatePresence } from "framer-motion";
16
+ import type { IconComponent } from "../lib/icon-context";
17
+ import { cn } from "../lib/utils";
18
+ import { spring } from "../lib/springs";
19
+ import { fontWeights } from "../lib/font-weight";
20
+ import { useShape } from "../lib/shape-context";
21
+ import { useProximityHover } from "../hooks/use-proximity-hover";
22
+
23
+ interface TabsSubtleContextValue {
24
+ registerTab: (index: number, element: HTMLElement | null) => void;
25
+ hoveredIndex: number | null;
26
+ selectedIndex: number;
27
+ onSelect: (index: number) => void;
28
+ idPrefix: string;
29
+ activeLabel: boolean;
30
+ }
31
+
32
+ const TabsSubtleContext = createContext<TabsSubtleContextValue | null>(null);
33
+
34
+ function useTabsSubtle() {
35
+ const ctx = useContext(TabsSubtleContext);
36
+ if (!ctx) throw new Error("useTabsSubtle must be used within a TabsSubtle");
37
+ return ctx;
38
+ }
39
+
40
+ interface TabsSubtleProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
41
+ children: ReactNode;
42
+ selectedIndex: number;
43
+ onSelect: (index: number) => void;
44
+ idPrefix?: string;
45
+ /** When true, only the selected tab shows its text label. Requires icons on tabs. */
46
+ activeLabel?: boolean;
47
+ }
48
+
49
+ const TabsSubtle = forwardRef<HTMLDivElement, TabsSubtleProps>(
50
+ (
51
+ {
52
+ children,
53
+ selectedIndex,
54
+ onSelect,
55
+ idPrefix: idPrefixProp,
56
+ activeLabel = false,
57
+ className,
58
+ ...props
59
+ },
60
+ ref,
61
+ ) => {
62
+ const containerRef = useRef<HTMLDivElement>(null);
63
+ const isMouseInside = useRef(false);
64
+ const generatedId = useId();
65
+ const shape = useShape();
66
+ const idPrefix = idPrefixProp || generatedId;
67
+
68
+ const {
69
+ activeIndex: hoveredIndex,
70
+ setActiveIndex: setHoveredIndex,
71
+ itemRects: tabRects,
72
+ handlers,
73
+ registerItem,
74
+ measureItems: measureTabs,
75
+ } = useProximityHover(containerRef, { axis: "x" });
76
+
77
+ // Track tab elements locally so we can observe their individual resizes
78
+ const tabElementsRef = useRef(new Map<number, HTMLElement>());
79
+ const registerTab = useCallback(
80
+ (index: number, element: HTMLElement | null) => {
81
+ registerItem(index, element);
82
+ if (element) {
83
+ tabElementsRef.current.set(index, element);
84
+ } else {
85
+ tabElementsRef.current.delete(index);
86
+ }
87
+ },
88
+ [registerItem],
89
+ );
90
+
91
+ useEffect(() => {
92
+ measureTabs();
93
+ }, [measureTabs, children]);
94
+
95
+ // Observe individual tab buttons for resize (label expand/collapse in activeLabel mode)
96
+ useEffect(() => {
97
+ const elements = tabElementsRef.current;
98
+ if (elements.size === 0) return;
99
+ const ro = new ResizeObserver(() => measureTabs());
100
+ elements.forEach((el) => ro.observe(el));
101
+ return () => ro.disconnect();
102
+ }, [measureTabs, children]);
103
+
104
+ // Wrap handlers to track isMouseInside
105
+ const handleMouseMove = useCallback(
106
+ (e: React.MouseEvent) => {
107
+ isMouseInside.current = true;
108
+ handlers.onMouseMove(e);
109
+ },
110
+ [handlers],
111
+ );
112
+
113
+ const handleMouseLeave = useCallback(() => {
114
+ isMouseInside.current = false;
115
+ handlers.onMouseLeave();
116
+ }, [handlers]);
117
+
118
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
119
+
120
+ const selectedRect = tabRects[selectedIndex];
121
+ const hoverRect = hoveredIndex !== null ? tabRects[hoveredIndex] : null;
122
+ const focusRect = focusedIndex !== null ? tabRects[focusedIndex] : null;
123
+ const isHoveringSelected = hoveredIndex === selectedIndex;
124
+ const isHovering = hoveredIndex !== null && !isHoveringSelected;
125
+
126
+ return (
127
+ <TabsSubtleContext.Provider
128
+ value={{ registerTab, hoveredIndex, selectedIndex, onSelect, idPrefix, activeLabel }}
129
+ >
130
+ <div
131
+ ref={(node) => {
132
+ (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
133
+ if (typeof ref === "function") ref(node);
134
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
135
+ }}
136
+ onMouseMove={handleMouseMove}
137
+ onMouseLeave={handleMouseLeave}
138
+ onFocus={(e) => {
139
+ const indexAttr = (e.target as HTMLElement)
140
+ .closest("[data-proximity-index]")
141
+ ?.getAttribute("data-proximity-index");
142
+ if (indexAttr != null) {
143
+ const idx = Number(indexAttr);
144
+ setHoveredIndex(idx);
145
+ setFocusedIndex((e.target as HTMLElement).matches(":focus-visible") ? idx : null);
146
+ }
147
+ }}
148
+ onBlur={(e) => {
149
+ if (containerRef.current?.contains(e.relatedTarget as Node)) return;
150
+ setFocusedIndex(null);
151
+ if (isMouseInside.current) return;
152
+ setHoveredIndex(null);
153
+ }}
154
+ onKeyDown={(e) => {
155
+ const items = Array.from(
156
+ containerRef.current?.querySelectorAll('[role="tab"]') ?? [],
157
+ ) as HTMLElement[];
158
+ const currentIdx = items.indexOf(e.target as HTMLElement);
159
+ if (currentIdx === -1) return;
160
+
161
+ if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
162
+ e.preventDefault();
163
+ const next = ["ArrowRight", "ArrowDown"].includes(e.key)
164
+ ? (currentIdx + 1) % items.length
165
+ : (currentIdx - 1 + items.length) % items.length;
166
+ items[next].focus();
167
+ } else if (e.key === "Home") {
168
+ e.preventDefault();
169
+ items[0]?.focus();
170
+ } else if (e.key === "End") {
171
+ e.preventDefault();
172
+ items[items.length - 1]?.focus();
173
+ }
174
+ }}
175
+ className={cn(
176
+ "relative flex items-center gap-0.5 select-none overflow-x-auto max-w-full scrollbar-hide -my-1 py-1",
177
+ className,
178
+ )}
179
+ role="tablist"
180
+ {...props}
181
+ >
182
+ {/* Selected pill */}
183
+ {selectedRect && (
184
+ <motion.div
185
+ className={cn("absolute bg-active pointer-events-none", shape.bg)}
186
+ initial={false}
187
+ animate={{
188
+ left: selectedRect.left,
189
+ width: selectedRect.width,
190
+ top: selectedRect.top,
191
+ height: selectedRect.height,
192
+ opacity: isHovering ? 0.8 : 1,
193
+ }}
194
+ transition={{
195
+ ...spring.moderate,
196
+ opacity: { duration: 0.08 },
197
+ }}
198
+ />
199
+ )}
200
+
201
+ {/* Hover pill */}
202
+ <AnimatePresence>
203
+ {hoverRect && !isHoveringSelected && selectedRect && (
204
+ <motion.div
205
+ className={cn("absolute bg-active pointer-events-none", shape.bg)}
206
+ initial={{
207
+ left: selectedRect.left,
208
+ width: selectedRect.width,
209
+ top: selectedRect.top,
210
+ height: selectedRect.height,
211
+ opacity: 0,
212
+ }}
213
+ animate={{
214
+ left: hoverRect.left,
215
+ width: hoverRect.width,
216
+ top: hoverRect.top,
217
+ height: hoverRect.height,
218
+ opacity: 0.4,
219
+ }}
220
+ exit={
221
+ !isMouseInside.current && selectedRect
222
+ ? {
223
+ left: selectedRect.left,
224
+ width: selectedRect.width,
225
+ top: selectedRect.top,
226
+ height: selectedRect.height,
227
+ opacity: 0,
228
+ transition: { ...spring.moderate, opacity: { duration: 0.06 } },
229
+ }
230
+ : { opacity: 0, transition: spring.fast.exit }
231
+ }
232
+ transition={{
233
+ ...spring.fast,
234
+ opacity: { duration: 0.08 },
235
+ }}
236
+ />
237
+ )}
238
+ </AnimatePresence>
239
+
240
+ {/* Focus ring */}
241
+ <AnimatePresence>
242
+ {focusRect && (
243
+ <motion.div
244
+ className={cn(
245
+ "absolute pointer-events-none z-20 border border-[#6B97FF]",
246
+ shape.focusRing,
247
+ )}
248
+ initial={false}
249
+ animate={{
250
+ left: focusRect.left - 2,
251
+ top: focusRect.top - 2,
252
+ width: focusRect.width + 4,
253
+ height: focusRect.height + 4,
254
+ }}
255
+ exit={{ opacity: 0, transition: spring.fast.exit }}
256
+ transition={{
257
+ ...spring.fast,
258
+ opacity: { duration: 0.08 },
259
+ }}
260
+ />
261
+ )}
262
+ </AnimatePresence>
263
+
264
+ {children}
265
+ </div>
266
+ </TabsSubtleContext.Provider>
267
+ );
268
+ },
269
+ );
270
+
271
+ TabsSubtle.displayName = "TabsSubtle";
272
+
273
+ interface TabsSubtleItemProps extends HTMLAttributes<HTMLButtonElement> {
274
+ icon?: IconComponent;
275
+ label: string;
276
+ index: number;
277
+ }
278
+
279
+ const TabsSubtleItem = forwardRef<HTMLButtonElement, TabsSubtleItemProps>(
280
+ ({ icon: Icon, label, index, className, ...props }, ref) => {
281
+ const internalRef = useRef<HTMLButtonElement>(null);
282
+ const shape = useShape();
283
+ const { registerTab, hoveredIndex, selectedIndex, onSelect, idPrefix, activeLabel } =
284
+ useTabsSubtle();
285
+
286
+ useEffect(() => {
287
+ registerTab(index, internalRef.current);
288
+ return () => registerTab(index, null);
289
+ }, [index, registerTab]);
290
+
291
+ const isSelected = selectedIndex === index;
292
+ const isActive = hoveredIndex === index || isSelected;
293
+ const collapseLabel = activeLabel && !!Icon;
294
+ const showLabel = !collapseLabel || isSelected;
295
+
296
+ const labelContent = (
297
+ <span className="inline-grid text-[13px] whitespace-nowrap">
298
+ <span
299
+ className="col-start-1 row-start-1 invisible"
300
+ style={{ fontVariationSettings: fontWeights.semibold }}
301
+ aria-hidden="true"
302
+ >
303
+ {label}
304
+ </span>
305
+ <span
306
+ className={cn(
307
+ "col-start-1 row-start-1 transition-[color,font-variation-settings] duration-80",
308
+ isActive ? "text-foreground" : "text-muted-foreground",
309
+ )}
310
+ style={{
311
+ fontVariationSettings: isSelected ? fontWeights.semibold : fontWeights.normal,
312
+ }}
313
+ >
314
+ {label}
315
+ </span>
316
+ </span>
317
+ );
318
+
319
+ return (
320
+ <button
321
+ ref={(node) => {
322
+ (internalRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;
323
+ if (typeof ref === "function") ref(node);
324
+ else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;
325
+ }}
326
+ data-proximity-index={index}
327
+ id={`${idPrefix}-tab-${index}`}
328
+ role="tab"
329
+ aria-selected={isSelected}
330
+ aria-controls={`${idPrefix}-panel-${index}`}
331
+ aria-label={collapseLabel && !showLabel ? label : undefined}
332
+ tabIndex={isSelected ? 0 : -1}
333
+ onClick={() => onSelect(index)}
334
+ className={cn(
335
+ "relative z-10 flex items-center px-3 py-2 cursor-pointer bg-transparent border-none outline-none",
336
+ collapseLabel ? "h-8" : "gap-2",
337
+ shape.bg,
338
+ className,
339
+ )}
340
+ {...props}
341
+ >
342
+ {Icon && (
343
+ <Icon
344
+ size={16}
345
+ strokeWidth={isActive ? 2 : 1.5}
346
+ className={cn(
347
+ "shrink-0 transition-[color,stroke-width] duration-80",
348
+ isActive ? "text-foreground" : "text-muted-foreground",
349
+ )}
350
+ />
351
+ )}
352
+ {collapseLabel ? (
353
+ <AnimatePresence initial={false}>
354
+ {showLabel && (
355
+ <motion.span
356
+ key="label"
357
+ className="overflow-hidden"
358
+ initial={{ width: 0, opacity: 0, marginLeft: 0 }}
359
+ animate={{ width: "auto", opacity: 1, marginLeft: 8 }}
360
+ exit={{ width: 0, opacity: 0, marginLeft: 0 }}
361
+ transition={{
362
+ ...spring.fast,
363
+ opacity: { duration: 0.06 },
364
+ }}
365
+ >
366
+ {labelContent}
367
+ </motion.span>
368
+ )}
369
+ </AnimatePresence>
370
+ ) : (
371
+ labelContent
372
+ )}
373
+ </button>
374
+ );
375
+ },
376
+ );
377
+
378
+ TabsSubtleItem.displayName = "TabsSubtleItem";
379
+
380
+ interface TabsSubtlePanelProps extends HTMLAttributes<HTMLDivElement> {
381
+ index: number;
382
+ selectedIndex: number;
383
+ idPrefix: string;
384
+ children: ReactNode;
385
+ }
386
+
387
+ const TabsSubtlePanel = forwardRef<HTMLDivElement, TabsSubtlePanelProps>(
388
+ ({ index, selectedIndex, idPrefix, children, className, ...props }, ref) => {
389
+ const isSelected = selectedIndex === index;
390
+
391
+ return (
392
+ <div
393
+ ref={ref}
394
+ id={`${idPrefix}-panel-${index}`}
395
+ role="tabpanel"
396
+ aria-labelledby={`${idPrefix}-tab-${index}`}
397
+ hidden={!isSelected}
398
+ tabIndex={-1}
399
+ className={cn("outline-none", className)}
400
+ {...props}
401
+ >
402
+ {isSelected && children}
403
+ </div>
404
+ );
405
+ },
406
+ );
407
+
408
+ TabsSubtlePanel.displayName = "TabsSubtlePanel";
409
+
410
+ export { TabsSubtle, TabsSubtleItem, TabsSubtlePanel };
411
+ export default TabsSubtle;