@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.
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
@@ -1,39 +1,385 @@
1
1
  "use client";
2
2
 
3
- import * as React from "react";
4
- import { RadioGroup } from "radix-ui";
3
+ import {
4
+ Children,
5
+ useRef,
6
+ useState,
7
+ useEffect,
8
+ createContext,
9
+ useContext,
10
+ forwardRef,
11
+ isValidElement,
12
+ cloneElement,
13
+ type ReactNode,
14
+ type HTMLAttributes,
15
+ } from "react";
16
+ import { motion, AnimatePresence } from "framer-motion";
17
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5
18
  import { cn } from "../lib/utils";
19
+ import { spring } from "../lib/springs";
20
+ import { fontWeights } from "../lib/font-weight";
21
+ import { useProximityHover } from "../hooks/use-proximity-hover";
22
+ import { useShape } from "../lib/shape-context";
6
23
 
7
- const RadioGroupRoot = React.forwardRef<
8
- React.ComponentRef<typeof RadioGroup.Root>,
9
- React.ComponentPropsWithoutRef<typeof RadioGroup.Root>
10
- >(({ className, ...props }, ref) => (
11
- <RadioGroup.Root ref={ref} className={cn("grid gap-2", className)} {...props} />
12
- ));
13
- RadioGroupRoot.displayName = "RadioGroup";
14
-
15
- const RadioGroupItem = React.forwardRef<
16
- React.ComponentRef<typeof RadioGroup.Item>,
17
- React.ComponentPropsWithoutRef<typeof RadioGroup.Item>
18
- >(({ className, ...props }, ref) => (
19
- <RadioGroup.Item
20
- ref={ref}
21
- className={cn(
22
- "relative aspect-square size-4 rounded-full border border-border text-foreground",
23
- "after:absolute after:left-1/2 after:top-1/2 after:size-10 after:-translate-x-1/2 after:-translate-y-1/2",
24
- "shadow-none transition-colors",
25
- "focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/20",
26
- "disabled:cursor-not-allowed disabled:opacity-50",
27
- "data-[state=checked]:bg-foreground data-[state=checked]:border-foreground",
28
- className,
29
- )}
30
- {...props}
31
- >
32
- <RadioGroup.Indicator className="flex items-center justify-center">
33
- <div className="size-2 rounded-full bg-card" />
34
- </RadioGroup.Indicator>
35
- </RadioGroup.Item>
36
- ));
37
- RadioGroupItem.displayName = "RadioGroupItem";
38
-
39
- export { RadioGroupRoot as RadioGroup, RadioGroupItem };
24
+ interface RadioGroupContextValue {
25
+ registerItem: (index: number, element: HTMLElement | null) => void;
26
+ activeIndex: number | null;
27
+ selectedIndex: number | null;
28
+ selectedValue?: string;
29
+ onValueChange?: (value: string) => void;
30
+ }
31
+
32
+ const RadioGroupContext = createContext<RadioGroupContextValue | null>(null);
33
+
34
+ function useRadioGroupContext() {
35
+ const ctx = useContext(RadioGroupContext);
36
+ if (!ctx) throw new Error("useRadioGroup must be used within a RadioGroup");
37
+ return ctx;
38
+ }
39
+
40
+ interface RadioGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
41
+ children: ReactNode;
42
+ selectedIndex?: number;
43
+ value?: string;
44
+ defaultValue?: string;
45
+ onValueChange?: (value: string) => void;
46
+ }
47
+
48
+ const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
49
+ ({ children, selectedIndex, value, defaultValue, onValueChange, className, ...props }, ref) => {
50
+ const containerRef = useRef<HTMLDivElement>(null);
51
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
52
+ const currentValue = value ?? uncontrolledValue;
53
+ const childValues = Children.toArray(children)
54
+ .filter(isValidElement)
55
+ .map((child) => (child.props as { value?: string }).value);
56
+ const {
57
+ activeIndex,
58
+ setActiveIndex,
59
+ itemRects,
60
+ sessionRef,
61
+ handlers,
62
+ registerItem,
63
+ measureItems,
64
+ } = useProximityHover(containerRef);
65
+
66
+ useEffect(() => {
67
+ measureItems();
68
+ }, [measureItems, children]);
69
+
70
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
71
+ const resolvedSelectedIndex =
72
+ currentValue !== undefined
73
+ ? childValues.findIndex((childValue) => childValue === currentValue)
74
+ : (selectedIndex ?? -1);
75
+
76
+ const activeRect = activeIndex !== null ? itemRects[activeIndex] : null;
77
+ const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null;
78
+ const selectedRect = resolvedSelectedIndex >= 0 ? itemRects[resolvedSelectedIndex] : null;
79
+ const isHoveringOther = activeIndex !== null && activeIndex !== resolvedSelectedIndex;
80
+ const shape = useShape();
81
+
82
+ const indexedChildren = Children.map(children, (child, index) => {
83
+ if (!isValidElement(child)) return child;
84
+ return cloneElement(child, { index } as Record<string, unknown>);
85
+ });
86
+
87
+ const handleValueChange = (nextValue: string) => {
88
+ if (value === undefined) setUncontrolledValue(nextValue);
89
+ onValueChange?.(nextValue);
90
+ };
91
+
92
+ const content = (
93
+ <div
94
+ ref={(node) => {
95
+ (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
96
+ if (typeof ref === "function") ref(node);
97
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
98
+ }}
99
+ onMouseEnter={handlers.onMouseEnter}
100
+ onMouseMove={handlers.onMouseMove}
101
+ onMouseLeave={handlers.onMouseLeave}
102
+ onFocus={(e) => {
103
+ const indexAttr = (e.target as HTMLElement)
104
+ .closest("[data-proximity-index]")
105
+ ?.getAttribute("data-proximity-index");
106
+ if (indexAttr != null) {
107
+ const idx = Number(indexAttr);
108
+ setActiveIndex(idx);
109
+ setFocusedIndex((e.target as HTMLElement).matches(":focus-visible") ? idx : null);
110
+ }
111
+ }}
112
+ onBlur={(e) => {
113
+ if (containerRef.current?.contains(e.relatedTarget as Node)) return;
114
+ setFocusedIndex(null);
115
+ setActiveIndex(null);
116
+ }}
117
+ onKeyDown={(e) => {
118
+ const items = Array.from(
119
+ containerRef.current?.querySelectorAll('[role="radio"]') ?? [],
120
+ ) as HTMLElement[];
121
+ const currentIdx = items.indexOf(e.target as HTMLElement);
122
+ if (currentIdx === -1) return;
123
+
124
+ if (["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft"].includes(e.key)) {
125
+ e.preventDefault();
126
+ const next = ["ArrowDown", "ArrowRight"].includes(e.key)
127
+ ? (currentIdx + 1) % items.length
128
+ : (currentIdx - 1 + items.length) % items.length;
129
+ items[next].focus();
130
+ items[next].click();
131
+ } else if (e.key === "Home") {
132
+ e.preventDefault();
133
+ items[0]?.focus();
134
+ items[0]?.click();
135
+ } else if (e.key === "End") {
136
+ e.preventDefault();
137
+ items[items.length - 1]?.focus();
138
+ items[items.length - 1]?.click();
139
+ }
140
+ }}
141
+ role="radiogroup"
142
+ className={cn("relative flex flex-col w-72 max-w-full select-none", className)}
143
+ {...props}
144
+ >
145
+ {/* Selected background */}
146
+ {selectedRect && (
147
+ <motion.div
148
+ className={`absolute ${shape.bg} bg-active pointer-events-none`}
149
+ initial={false}
150
+ animate={{
151
+ top: selectedRect.top,
152
+ left: selectedRect.left,
153
+ width: selectedRect.width,
154
+ height: selectedRect.height,
155
+ opacity: isHoveringOther ? 0.8 : 1,
156
+ }}
157
+ transition={{
158
+ ...spring.moderate,
159
+ opacity: { duration: 0.08 },
160
+ }}
161
+ />
162
+ )}
163
+
164
+ {/* Hover background */}
165
+ <AnimatePresence>
166
+ {activeRect && (
167
+ <motion.div
168
+ key={sessionRef.current}
169
+ className={`absolute ${shape.bg} bg-hover pointer-events-none`}
170
+ initial={{
171
+ opacity: 0,
172
+ top: activeRect.top,
173
+ left: activeRect.left,
174
+ width: activeRect.width,
175
+ height: activeRect.height,
176
+ }}
177
+ animate={{
178
+ opacity: 1,
179
+ top: activeRect.top,
180
+ left: activeRect.left,
181
+ width: activeRect.width,
182
+ height: activeRect.height,
183
+ }}
184
+ exit={{ opacity: 0, transition: spring.fast.exit }}
185
+ transition={{
186
+ ...spring.fast,
187
+ opacity: { duration: 0.08 },
188
+ }}
189
+ />
190
+ )}
191
+ </AnimatePresence>
192
+
193
+ {/* Focus ring */}
194
+ <AnimatePresence>
195
+ {focusRect && (
196
+ <motion.div
197
+ className={`absolute ${shape.focusRing} pointer-events-none z-20 border border-[#6B97FF]`}
198
+ initial={false}
199
+ animate={{
200
+ left: focusRect.left - 2,
201
+ top: focusRect.top - 2,
202
+ width: focusRect.width + 4,
203
+ height: focusRect.height + 4,
204
+ }}
205
+ exit={{ opacity: 0, transition: spring.fast.exit }}
206
+ transition={{
207
+ ...spring.fast,
208
+ opacity: { duration: 0.08 },
209
+ }}
210
+ />
211
+ )}
212
+ </AnimatePresence>
213
+
214
+ {indexedChildren}
215
+ </div>
216
+ );
217
+
218
+ // If Radix value/onValueChange provided, wrap with Radix RadioGroup
219
+ if (currentValue !== undefined) {
220
+ return (
221
+ <RadioGroupContext.Provider
222
+ value={{
223
+ registerItem,
224
+ activeIndex,
225
+ selectedIndex: resolvedSelectedIndex >= 0 ? resolvedSelectedIndex : null,
226
+ selectedValue: currentValue,
227
+ onValueChange: handleValueChange,
228
+ }}
229
+ >
230
+ <RadioGroupPrimitive.Root value={currentValue} onValueChange={handleValueChange} asChild>
231
+ {content}
232
+ </RadioGroupPrimitive.Root>
233
+ </RadioGroupContext.Provider>
234
+ );
235
+ }
236
+
237
+ return (
238
+ <RadioGroupContext.Provider
239
+ value={{
240
+ registerItem,
241
+ activeIndex,
242
+ selectedIndex: selectedIndex ?? null,
243
+ }}
244
+ >
245
+ {content}
246
+ </RadioGroupContext.Provider>
247
+ );
248
+ },
249
+ );
250
+
251
+ RadioGroup.displayName = "RadioGroup";
252
+
253
+ interface RadioItemProps extends HTMLAttributes<HTMLDivElement> {
254
+ label?: string;
255
+ index?: number;
256
+ selected?: boolean;
257
+ onSelect?: () => void;
258
+ value?: string;
259
+ }
260
+
261
+ const RadioItem = forwardRef<HTMLDivElement, RadioItemProps>(
262
+ ({ label, index = 0, selected, onSelect, value, className, children, ...props }, ref) => {
263
+ const internalRef = useRef<HTMLDivElement>(null);
264
+ const hasMounted = useRef(false);
265
+ const { registerItem, activeIndex, selectedIndex, selectedValue, onValueChange } =
266
+ useRadioGroupContext();
267
+
268
+ useEffect(() => {
269
+ registerItem(index, internalRef.current);
270
+ return () => registerItem(index, null);
271
+ }, [index, registerItem]);
272
+
273
+ useEffect(() => {
274
+ hasMounted.current = true;
275
+ }, []);
276
+
277
+ const isActive = activeIndex === index;
278
+ const skipAnimation = !hasMounted.current;
279
+ const shape = useShape();
280
+ const isSelected =
281
+ value !== undefined && selectedValue !== undefined
282
+ ? selectedValue === value
283
+ : (selected ?? selectedIndex === index);
284
+
285
+ const handleSelect = () => {
286
+ if (value !== undefined) {
287
+ onValueChange?.(value);
288
+ }
289
+ onSelect?.();
290
+ };
291
+
292
+ return (
293
+ <div
294
+ ref={(node) => {
295
+ (internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
296
+ if (typeof ref === "function") ref(node);
297
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
298
+ }}
299
+ data-proximity-index={index}
300
+ tabIndex={isSelected ? 0 : -1}
301
+ role="radio"
302
+ aria-checked={isSelected}
303
+ aria-label={label ?? (typeof children === "string" ? children : value)}
304
+ onClick={handleSelect}
305
+ onKeyDown={(e) => {
306
+ if (e.key === " " || e.key === "Enter") {
307
+ e.preventDefault();
308
+ handleSelect();
309
+ }
310
+ }}
311
+ className={cn(
312
+ `relative z-10 flex items-center gap-2.5 ${shape.item} px-3 py-1.5 cursor-pointer outline-none`,
313
+ className,
314
+ )}
315
+ {...props}
316
+ >
317
+ {/* Radio circle */}
318
+ <div className="relative w-[15px] h-[15px] shrink-0">
319
+ {/* Border */}
320
+ <div
321
+ className={cn(
322
+ "absolute inset-0 rounded-full border-solid transition-all duration-80",
323
+ isSelected
324
+ ? "border-[1.5px] border-transparent"
325
+ : isActive
326
+ ? "border-[1.5px] border-neutral-400 dark:border-neutral-500"
327
+ : "border-[1.5px] border-border",
328
+ )}
329
+ />
330
+ {/* Dot */}
331
+ <AnimatePresence>
332
+ {isSelected && (
333
+ <motion.div
334
+ className="absolute inset-0 flex items-center justify-center"
335
+ initial={{
336
+ opacity: skipAnimation ? 1 : 0,
337
+ scale: skipAnimation ? 1 : 0.3,
338
+ }}
339
+ animate={{ opacity: 1, scale: 1 }}
340
+ exit={{ opacity: 0, scale: 0.3, transition: { duration: 0.04 } }}
341
+ transition={spring.fast}
342
+ >
343
+ <div className="w-[8px] h-[8px] rounded-full bg-foreground" />
344
+ </motion.div>
345
+ )}
346
+ </AnimatePresence>
347
+ </div>
348
+
349
+ {/* Label */}
350
+ <span className="inline-grid text-[13px]">
351
+ <span
352
+ className="col-start-1 row-start-1 invisible"
353
+ style={{ fontVariationSettings: fontWeights.semibold }}
354
+ aria-hidden="true"
355
+ >
356
+ {label ?? children ?? value}
357
+ </span>
358
+ <span
359
+ className={cn(
360
+ "col-start-1 row-start-1 transition-[color,font-variation-settings] duration-80",
361
+ isSelected || isActive ? "text-foreground" : "text-muted-foreground",
362
+ )}
363
+ style={{
364
+ fontVariationSettings: isSelected ? fontWeights.semibold : fontWeights.normal,
365
+ }}
366
+ >
367
+ {label ?? children ?? value}
368
+ </span>
369
+ </span>
370
+
371
+ {/* Hidden Radix radio input for accessibility */}
372
+ {value !== undefined && (
373
+ <RadioGroupPrimitive.Item value={value} className="sr-only" tabIndex={-1} aria-hidden />
374
+ )}
375
+ </div>
376
+ );
377
+ },
378
+ );
379
+
380
+ RadioItem.displayName = "RadioItem";
381
+
382
+ const RadioGroupItem = RadioItem;
383
+
384
+ export { RadioGroup, RadioItem, RadioGroupItem };
385
+ export default RadioGroup;
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { ScrollArea } from "radix-ui";
5
5
  import { cn } from "../lib/utils";
6
+ import { motionClasses } from "../lib/interaction";
6
7
 
7
8
  const ScrollAreaRoot = React.forwardRef<
8
9
  React.ComponentRef<typeof ScrollArea.Root>,
@@ -27,14 +28,20 @@ const ScrollAreaScrollbar = React.forwardRef<
27
28
  ref={ref}
28
29
  orientation={orientation}
29
30
  className={cn(
30
- "flex touch-none select-none transition-colors",
31
+ "flex touch-none select-none",
32
+ motionClasses,
31
33
  orientation === "vertical" && "h-full w-2 border-l border-l-transparent p-px",
32
34
  orientation === "horizontal" && "h-2 flex-col border-t border-t-transparent p-px",
33
35
  className,
34
36
  )}
35
37
  {...props}
36
38
  >
37
- <ScrollArea.Thumb className="relative flex-1 rounded-full bg-muted hover:bg-muted transition-colors" />
39
+ <ScrollArea.Thumb
40
+ className={cn(
41
+ "relative flex-1 rounded-full bg-muted hover:bg-muted-foreground/30",
42
+ motionClasses,
43
+ )}
44
+ />
38
45
  </ScrollArea.Scrollbar>
39
46
  ));
40
47
  ScrollAreaScrollbar.displayName = "ScrollAreaScrollbar";