@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,403 @@
1
+ "use client";
2
+
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 CheckboxPrimitive from "@radix-ui/react-checkbox";
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 { useMergeSplitBlocks, SelectionBackgrounds } from "../hooks/use-merge-split";
23
+ import { useShape } from "../lib/shape-context";
24
+
25
+ interface CheckboxGroupContextValue {
26
+ registerItem: (index: number, element: HTMLElement | null) => void;
27
+ activeIndex: number | null;
28
+ }
29
+
30
+ const CheckboxGroupContext = createContext<CheckboxGroupContextValue | null>(null);
31
+
32
+ function useCheckboxGroup() {
33
+ const ctx = useContext(CheckboxGroupContext);
34
+ if (!ctx) throw new Error("useCheckboxGroup must be used within a CheckboxGroup");
35
+ return ctx;
36
+ }
37
+
38
+ interface CheckboxGroupProps extends HTMLAttributes<HTMLDivElement> {
39
+ children?: ReactNode;
40
+ checkedIndices?: Set<number>;
41
+ options?: Array<{ value: string; label: string; disabled?: boolean }>;
42
+ value?: string[];
43
+ onValueChange?: (value: string[]) => void;
44
+ }
45
+
46
+ const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
47
+ ({ children, checkedIndices, options, value = [], onValueChange, className, ...props }, ref) => {
48
+ const containerRef = useRef<HTMLDivElement>(null);
49
+ const groupIdCounter = useRef(0);
50
+ const prevGroupMap = useRef(new Map<number, number>());
51
+
52
+ const {
53
+ activeIndex,
54
+ setActiveIndex,
55
+ itemRects,
56
+ sessionRef,
57
+ handlers,
58
+ registerItem,
59
+ measureItems,
60
+ } = useProximityHover(containerRef);
61
+
62
+ useEffect(() => {
63
+ measureItems();
64
+ }, [measureItems, children]);
65
+
66
+ // Group contiguous checked indices into runs with stable IDs
67
+ const runs: { start: number; end: number }[] = [];
68
+ const optionCheckedIndices = new Set(
69
+ options
70
+ ?.map((option, index) => (value.includes(option.value) ? index : -1))
71
+ .filter((index) => index >= 0) ?? [],
72
+ );
73
+ const resolvedCheckedIndices = checkedIndices ?? optionCheckedIndices;
74
+ const sortedChecked = [...resolvedCheckedIndices].sort((a, b) => a - b);
75
+ for (const idx of sortedChecked) {
76
+ const last = runs[runs.length - 1];
77
+ if (last && idx === last.end + 1) {
78
+ last.end = idx;
79
+ } else {
80
+ runs.push({ start: idx, end: idx });
81
+ }
82
+ }
83
+
84
+ // Assign stable IDs: reuse previous ID if any member overlaps
85
+ const usedIds = new Set<number>();
86
+ const newGroupMap = new Map<number, number>();
87
+ const checkedGroups = runs.map((run) => {
88
+ let stableId: number | null = null;
89
+ for (let i = run.start; i <= run.end; i++) {
90
+ const prevId = prevGroupMap.current.get(i);
91
+ if (prevId !== undefined && !usedIds.has(prevId)) {
92
+ stableId = prevId;
93
+ break;
94
+ }
95
+ }
96
+ const id = stableId ?? ++groupIdCounter.current;
97
+ usedIds.add(id);
98
+ for (let i = run.start; i <= run.end; i++) {
99
+ newGroupMap.set(i, id);
100
+ }
101
+ return { ...run, id };
102
+ });
103
+ prevGroupMap.current = newGroupMap;
104
+
105
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
106
+
107
+ const activeRect = activeIndex !== null ? itemRects[activeIndex] : null;
108
+ const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null;
109
+ const isHoveringOther = activeIndex !== null && !resolvedCheckedIndices.has(activeIndex);
110
+ const shape = useShape();
111
+
112
+ // Selected backgrounds, with the merge/split boundary animation when one
113
+ // unchecked row bridges or splits two checked runs.
114
+ const blocks = useMergeSplitBlocks(checkedGroups, itemRects, shape.mergedRadius);
115
+ const renderedChildren =
116
+ options?.map((option, index) => (
117
+ <CheckboxItem
118
+ key={option.value}
119
+ index={index}
120
+ label={option.label}
121
+ checked={value.includes(option.value)}
122
+ onToggle={() => {
123
+ if (option.disabled) return;
124
+ const next = value.includes(option.value)
125
+ ? value.filter((item) => item !== option.value)
126
+ : [...value, option.value];
127
+ onValueChange?.(next);
128
+ }}
129
+ aria-disabled={option.disabled || undefined}
130
+ />
131
+ )) ??
132
+ Children.map(children, (child, index) => {
133
+ if (!isValidElement(child)) return child;
134
+ const childProps = child.props as { index?: number };
135
+ if (childProps.index !== undefined) return child;
136
+ return cloneElement(child, { index } as Record<string, unknown>);
137
+ });
138
+
139
+ return (
140
+ <CheckboxGroupContext.Provider value={{ registerItem, activeIndex }}>
141
+ <div
142
+ ref={(node) => {
143
+ (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
144
+ if (typeof ref === "function") ref(node);
145
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
146
+ }}
147
+ onMouseEnter={handlers.onMouseEnter}
148
+ onMouseMove={handlers.onMouseMove}
149
+ onMouseLeave={handlers.onMouseLeave}
150
+ onFocus={(e) => {
151
+ const indexAttr = (e.target as HTMLElement)
152
+ .closest("[data-proximity-index]")
153
+ ?.getAttribute("data-proximity-index");
154
+ if (indexAttr != null) {
155
+ const idx = Number(indexAttr);
156
+ setActiveIndex(idx);
157
+ setFocusedIndex((e.target as HTMLElement).matches(":focus-visible") ? idx : null);
158
+ }
159
+ }}
160
+ onBlur={(e) => {
161
+ // Don't clear hover when focus moves to another item within the group
162
+ if (containerRef.current?.contains(e.relatedTarget as Node)) return;
163
+ setFocusedIndex(null);
164
+ setActiveIndex(null);
165
+ }}
166
+ onKeyDown={(e) => {
167
+ // Scope to row wrappers only. The inner checkbox primitive also
168
+ // carries role="checkbox", so a bare [role="checkbox"] selector
169
+ // matches twice per row and arrows skip onto the hidden control.
170
+ const items = Array.from(
171
+ containerRef.current?.querySelectorAll("[data-proximity-index]") ?? [],
172
+ ) as HTMLElement[];
173
+ const currentIdx = items.indexOf(e.target as HTMLElement);
174
+ if (currentIdx === -1) return;
175
+
176
+ if (["ArrowDown", "ArrowUp"].includes(e.key)) {
177
+ e.preventDefault();
178
+ const next =
179
+ e.key === "ArrowDown"
180
+ ? (currentIdx + 1) % items.length
181
+ : (currentIdx - 1 + items.length) % items.length;
182
+ items[next].focus();
183
+ } else if (e.key === "Home") {
184
+ e.preventDefault();
185
+ items[0]?.focus();
186
+ } else if (e.key === "End") {
187
+ e.preventDefault();
188
+ items[items.length - 1]?.focus();
189
+ }
190
+ }}
191
+ role="group"
192
+ className={cn("relative flex flex-col w-72 max-w-full select-none", className)}
193
+ {...props}
194
+ >
195
+ {/* Selected backgrounds (merged for contiguous checked items).
196
+ A run is normally one block; mid merge/split it is drawn as two
197
+ abutting halves — see useMergeSplitBlocks. */}
198
+ <SelectionBackgrounds blocks={blocks} dimmed={isHoveringOther} />
199
+
200
+ {/* Hover background */}
201
+ <AnimatePresence>
202
+ {activeRect && (
203
+ <motion.div
204
+ key={sessionRef.current}
205
+ className={`absolute ${shape.bg} bg-hover pointer-events-none`}
206
+ initial={{
207
+ opacity: 0,
208
+ top: activeRect.top,
209
+ left: activeRect.left,
210
+ width: activeRect.width,
211
+ height: activeRect.height,
212
+ }}
213
+ animate={{
214
+ opacity: 1,
215
+ top: activeRect.top,
216
+ left: activeRect.left,
217
+ width: activeRect.width,
218
+ height: activeRect.height,
219
+ }}
220
+ exit={{ opacity: 0, transition: spring.fast.exit }}
221
+ transition={{
222
+ ...spring.fast,
223
+ opacity: { duration: 0.08 },
224
+ }}
225
+ />
226
+ )}
227
+ </AnimatePresence>
228
+
229
+ {/* Focus ring */}
230
+ <AnimatePresence>
231
+ {focusRect && (
232
+ <motion.div
233
+ className={`absolute ${shape.focusRing} pointer-events-none z-20 border border-[#6B97FF]`}
234
+ initial={false}
235
+ animate={{
236
+ left: focusRect.left - 2,
237
+ top: focusRect.top - 2,
238
+ width: focusRect.width + 4,
239
+ height: focusRect.height + 4,
240
+ }}
241
+ exit={{ opacity: 0, transition: spring.fast.exit }}
242
+ transition={{
243
+ ...spring.fast,
244
+ opacity: { duration: 0.08 },
245
+ }}
246
+ />
247
+ )}
248
+ </AnimatePresence>
249
+
250
+ {renderedChildren}
251
+ </div>
252
+ </CheckboxGroupContext.Provider>
253
+ );
254
+ },
255
+ );
256
+
257
+ CheckboxGroup.displayName = "CheckboxGroup";
258
+
259
+ interface CheckboxItemProps extends HTMLAttributes<HTMLDivElement> {
260
+ label: string;
261
+ index: number;
262
+ checked: boolean;
263
+ onToggle: () => void;
264
+ }
265
+
266
+ const CheckboxItem = forwardRef<HTMLDivElement, CheckboxItemProps>(
267
+ ({ label, index, checked, onToggle, className, ...props }, ref) => {
268
+ const internalRef = useRef<HTMLDivElement>(null);
269
+ const hasMounted = useRef(false);
270
+ const { registerItem, activeIndex } = useCheckboxGroup();
271
+
272
+ useEffect(() => {
273
+ registerItem(index, internalRef.current);
274
+ return () => registerItem(index, null);
275
+ }, [index, registerItem]);
276
+
277
+ useEffect(() => {
278
+ hasMounted.current = true;
279
+ }, []);
280
+
281
+ const isActive = activeIndex === index;
282
+ const skipAnimation = !hasMounted.current;
283
+ const shape = useShape();
284
+
285
+ return (
286
+ <div
287
+ ref={(node) => {
288
+ (internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
289
+ if (typeof ref === "function") ref(node);
290
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
291
+ }}
292
+ data-proximity-index={index}
293
+ tabIndex={0}
294
+ role="checkbox"
295
+ aria-checked={checked}
296
+ aria-label={label}
297
+ onClick={onToggle}
298
+ onKeyDown={(e) => {
299
+ if (e.key === " " || e.key === "Enter") {
300
+ e.preventDefault();
301
+ onToggle();
302
+ }
303
+ }}
304
+ className={cn(
305
+ `relative z-10 flex items-center gap-2.5 ${shape.item} px-3 py-1.5 cursor-pointer outline-none`,
306
+ className,
307
+ )}
308
+ {...props}
309
+ >
310
+ {/* Checkbox — Radix primitive for accessibility */}
311
+ <CheckboxPrimitive.Root
312
+ checked={checked}
313
+ onCheckedChange={() => onToggle()}
314
+ tabIndex={-1}
315
+ aria-hidden
316
+ className="relative w-[15px] h-[15px] shrink-0 appearance-none bg-transparent p-0 border-0 outline-none cursor-pointer"
317
+ onClick={(e) => e.stopPropagation()}
318
+ >
319
+ {/* Border */}
320
+ <div
321
+ className={cn(
322
+ "absolute inset-0 rounded-[5px] border-solid transition-all duration-80",
323
+ checked
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
+ {/* Check mark */}
331
+ <AnimatePresence>
332
+ {checked && (
333
+ <CheckboxPrimitive.Indicator forceMount asChild>
334
+ <motion.svg
335
+ width={18}
336
+ height={18}
337
+ viewBox="0 0 24 24"
338
+ fill="none"
339
+ stroke="currentColor"
340
+ strokeWidth={2}
341
+ strokeLinecap="round"
342
+ strokeLinejoin="round"
343
+ className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-foreground"
344
+ initial={{ opacity: 1 }}
345
+ animate={{ opacity: 1 }}
346
+ exit={{ opacity: 1 }}
347
+ >
348
+ <motion.path
349
+ d="M6 12L10 16L18 8"
350
+ initial={{
351
+ pathLength: skipAnimation ? 1 : 0,
352
+ }}
353
+ animate={{
354
+ pathLength: 1,
355
+ transition: {
356
+ duration: 0.08,
357
+ ease: "easeOut",
358
+ },
359
+ }}
360
+ exit={{
361
+ pathLength: 0,
362
+ transition: {
363
+ duration: 0.04,
364
+ ease: "easeIn",
365
+ },
366
+ }}
367
+ />
368
+ </motion.svg>
369
+ </CheckboxPrimitive.Indicator>
370
+ )}
371
+ </AnimatePresence>
372
+ </CheckboxPrimitive.Root>
373
+
374
+ {/* Label */}
375
+ <span className="inline-grid text-[13px]">
376
+ <span
377
+ className="col-start-1 row-start-1 invisible"
378
+ style={{ fontVariationSettings: fontWeights.semibold }}
379
+ aria-hidden="true"
380
+ >
381
+ {label}
382
+ </span>
383
+ <span
384
+ className={cn(
385
+ "col-start-1 row-start-1 transition-[color,font-variation-settings] duration-80",
386
+ checked || isActive ? "text-foreground" : "text-muted-foreground",
387
+ )}
388
+ style={{
389
+ fontVariationSettings: checked ? fontWeights.semibold : fontWeights.normal,
390
+ }}
391
+ >
392
+ {label}
393
+ </span>
394
+ </span>
395
+ </div>
396
+ );
397
+ },
398
+ );
399
+
400
+ CheckboxItem.displayName = "CheckboxItem";
401
+
402
+ export { CheckboxGroup, CheckboxItem };
403
+ export default CheckboxGroup;
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { Checkbox } from "radix-ui";
5
5
  import { Check, Minus } from "lucide-react";
6
6
  import { cn } from "../lib/utils";
7
+ import { focusRingClasses, motionClasses, pressClasses } from "../lib/interaction";
7
8
 
8
9
  const CheckboxRoot = React.forwardRef<
9
10
  React.ComponentRef<typeof Checkbox.Root>,
@@ -12,9 +13,11 @@ const CheckboxRoot = React.forwardRef<
12
13
  <Checkbox.Root
13
14
  ref={ref}
14
15
  className={cn(
15
- "peer relative size-4 shrink-0 rounded border border-border bg-card transition-colors",
16
+ "peer relative size-4 shrink-0 rounded border border-border bg-card",
17
+ motionClasses,
18
+ pressClasses,
16
19
  "after:absolute after:left-1/2 after:top-1/2 after:size-10 after:-translate-x-1/2 after:-translate-y-1/2",
17
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30",
20
+ focusRingClasses,
18
21
  "disabled:cursor-not-allowed disabled:opacity-50",
19
22
  "data-[state=checked]:bg-brand-primary data-[state=checked]:border-brand-primary",
20
23
  "data-[state=indeterminate]:bg-brand-primary data-[state=indeterminate]:border-brand-primary",
@@ -22,7 +25,7 @@ const CheckboxRoot = React.forwardRef<
22
25
  )}
23
26
  {...props}
24
27
  >
25
- <Checkbox.Indicator className="flex items-center justify-center text-background">
28
+ <Checkbox.Indicator className="flex items-center justify-center text-background data-[state=checked]:animate-in data-[state=checked]:zoom-in-75">
26
29
  {props.checked === "indeterminate" ? (
27
30
  <Minus size={11} strokeWidth={3} />
28
31
  ) : (
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { cn } from "../lib/utils";
5
+ import { motionClasses } from "../lib/interaction";
5
6
  import { ColorPicker } from "./color-picker";
6
7
 
7
8
  interface ColorInputProps {
@@ -43,7 +44,8 @@ function ColorInput({
43
44
  <div
44
45
  className={cn(
45
46
  "inline-flex h-8 items-stretch gap-0 rounded-md border border-border bg-card overflow-hidden",
46
- "focus-within:ring-2 focus-within:ring-brand-primary/20 focus-within:border-brand-primary",
47
+ "shadow-natural focus-within:ring-2 focus-within:ring-brand-primary/20 focus-within:border-brand-primary",
48
+ motionClasses,
47
49
  disabled && "opacity-50 pointer-events-none",
48
50
  className,
49
51
  )}