@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
@@ -1,151 +1,346 @@
1
- import * as React from "react";
2
- import { AlertCircle } from "lucide-react";
1
+ "use client";
2
+
3
+ import {
4
+ useRef,
5
+ useState,
6
+ useCallback,
7
+ useEffect,
8
+ createContext,
9
+ useContext,
10
+ forwardRef,
11
+ type ReactNode,
12
+ type HTMLAttributes,
13
+ type InputHTMLAttributes,
14
+ type ChangeEventHandler,
15
+ } from "react";
16
+ import type { IconComponent } from "../lib/icon-context";
3
17
  import { cn } from "../lib/utils";
18
+ import { fontWeights } from "../lib/font-weight";
19
+ import { useShape } from "../lib/shape-context";
4
20
 
5
- interface InputGroupProps extends React.InputHTMLAttributes<HTMLInputElement> {
6
- leadingAddon?: React.ReactNode;
7
- trailingAddon?: React.ReactNode;
8
- leadingIcon?: React.ReactNode;
9
- trailingIcon?: React.ReactNode;
10
- trailingAction?: React.ReactNode;
11
- trailingActionClassName?: string;
12
- trailingButton?: React.ReactNode;
13
- error?: boolean;
14
- pill?: boolean;
21
+ interface InputGroupContextValue {
22
+ registerItem: (index: number, element: HTMLLabelElement | null) => void;
23
+ activeIndex: number | null;
24
+ }
25
+
26
+ const InputGroupContext = createContext<InputGroupContextValue | null>(null);
27
+
28
+ function useInputGroup() {
29
+ const ctx = useContext(InputGroupContext);
30
+ if (!ctx) throw new Error("useInputGroup must be used within an InputGroup");
31
+ return ctx;
32
+ }
33
+
34
+ interface InputGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
35
+ children?: ReactNode;
36
+ placeholder?: string;
37
+ value?: string;
38
+ onChange?: ChangeEventHandler<HTMLInputElement>;
39
+ leadingAddon?: ReactNode;
40
+ trailingAddon?: ReactNode;
41
+ trailingAction?: ReactNode;
42
+ leadingIcon?: ReactNode;
43
+ trailingIcon?: ReactNode;
44
+ trailingButton?: ReactNode;
45
+ error?: boolean | string;
46
+ disabled?: boolean;
15
47
  wrapperClassName?: string;
48
+ pill?: boolean;
49
+ type?: InputHTMLAttributes<HTMLInputElement>["type"];
50
+ defaultValue?: InputHTMLAttributes<HTMLInputElement>["defaultValue"];
51
+ id?: string;
16
52
  }
17
53
 
18
- const InputGroup = React.forwardRef<HTMLInputElement, InputGroupProps>(
54
+ const InputGroup = forwardRef<HTMLDivElement, InputGroupProps>(
19
55
  (
20
56
  {
57
+ children,
58
+ className,
59
+ placeholder,
60
+ value,
61
+ onChange,
21
62
  leadingAddon,
22
63
  trailingAddon,
64
+ trailingAction,
23
65
  leadingIcon,
24
66
  trailingIcon,
25
- trailingAction,
26
- trailingActionClassName,
27
67
  trailingButton,
28
68
  error,
29
- pill,
30
- className,
69
+ disabled,
31
70
  wrapperClassName,
71
+ pill,
72
+ type,
73
+ defaultValue,
74
+ id,
32
75
  ...props
33
76
  },
34
77
  ref,
35
78
  ) => {
36
- const radius = pill ? "rounded-full" : "rounded-lg";
37
- const hasLeading = leadingAddon || leadingIcon;
38
- const hasInsetTrailing = trailingIcon || trailingAction || error;
39
- const hasTrailing = trailingAddon || hasInsetTrailing || trailingButton;
40
-
41
- const baseInput = cn(
42
- "flex h-10 w-full border bg-card body text-foreground placeholder:text-muted-foreground",
43
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:disabled:cursor-not-allowed disabled:opacity-50",
44
- error
45
- ? "border-destructive focus-visible:ring-destructive/20 focus-visible:border-destructive"
46
- : "border-border focus-visible:ring-brand-primary/20 focus-visible:border-brand-primary",
79
+ const itemsRef = useRef(new Map<number, HTMLLabelElement>());
80
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
81
+
82
+ const registerItem = useCallback((index: number, element: HTMLLabelElement | null) => {
83
+ if (element) {
84
+ itemsRef.current.set(index, element);
85
+ } else {
86
+ itemsRef.current.delete(index);
87
+ }
88
+ }, []);
89
+
90
+ const handleMouseMove = useCallback((e: React.MouseEvent) => {
91
+ const mouseY = e.clientY;
92
+
93
+ let closestIndex: number | null = null;
94
+ let closestDistance = Infinity;
95
+
96
+ itemsRef.current.forEach((element, index) => {
97
+ const rect = element.getBoundingClientRect();
98
+ const itemCenterY = rect.top + rect.height / 2;
99
+ const distance = Math.abs(mouseY - itemCenterY);
100
+
101
+ if (distance < closestDistance) {
102
+ closestDistance = distance;
103
+ closestIndex = index;
104
+ }
105
+ });
106
+
107
+ setActiveIndex(closestIndex);
108
+ }, []);
109
+
110
+ const handleMouseLeave = useCallback(() => {
111
+ setActiveIndex(null);
112
+ }, []);
113
+
114
+ const rendersInput =
115
+ placeholder !== undefined ||
116
+ value !== undefined ||
117
+ onChange !== undefined ||
118
+ leadingAddon !== undefined ||
119
+ trailingAddon !== undefined ||
120
+ trailingAction !== undefined ||
121
+ leadingIcon !== undefined ||
122
+ trailingIcon !== undefined ||
123
+ trailingButton !== undefined ||
124
+ error !== undefined;
125
+
126
+ if (rendersInput) {
127
+ return (
128
+ <div
129
+ ref={ref}
130
+ className={cn(
131
+ "relative flex items-center gap-2 rounded-[var(--radius-input)] border bg-background px-3 py-2 text-[13px] transition-colors",
132
+ error ? "border-destructive/50" : "border-border",
133
+ disabled && "opacity-50",
134
+ pill && "rounded-full",
135
+ wrapperClassName,
136
+ )}
137
+ {...props}
138
+ >
139
+ {leadingAddon && <span className="shrink-0 text-muted-foreground">{leadingAddon}</span>}
140
+ {leadingIcon && <span className="shrink-0 text-muted-foreground">{leadingIcon}</span>}
141
+ <input
142
+ id={id}
143
+ type={type}
144
+ value={value}
145
+ defaultValue={defaultValue}
146
+ onChange={onChange}
147
+ placeholder={placeholder}
148
+ disabled={disabled}
149
+ aria-invalid={!!error || undefined}
150
+ className={cn(
151
+ "min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground",
152
+ (trailingAction || trailingIcon) && "pr-16",
153
+ className,
154
+ )}
155
+ />
156
+ {trailingAddon && <span className="shrink-0 text-muted-foreground">{trailingAddon}</span>}
157
+ {trailingIcon && <span className="shrink-0 text-muted-foreground">{trailingIcon}</span>}
158
+ {trailingButton}
159
+ {trailingAction && (
160
+ <span className="absolute right-2 top-1/2 -translate-y-1/2">{trailingAction}</span>
161
+ )}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ return (
167
+ <InputGroupContext.Provider value={{ registerItem, activeIndex }}>
168
+ <div
169
+ ref={ref}
170
+ onMouseMove={handleMouseMove}
171
+ onMouseLeave={handleMouseLeave}
172
+ className={cn("flex flex-col gap-3 w-72 max-w-full", className)}
173
+ {...props}
174
+ >
175
+ {children}
176
+ </div>
177
+ </InputGroupContext.Provider>
47
178
  );
179
+ },
180
+ );
181
+
182
+ InputGroup.displayName = "InputGroup";
183
+
184
+ interface InputFieldProps extends Omit<
185
+ InputHTMLAttributes<HTMLInputElement>,
186
+ "onChange" | "index"
187
+ > {
188
+ label: string;
189
+ placeholder?: string;
190
+ icon?: IconComponent;
191
+ index: number;
192
+ value: string;
193
+ onChange: (value: string) => void;
194
+ error?: string;
195
+ disabled?: boolean;
196
+ className?: string;
197
+ }
198
+
199
+ const InputField = forwardRef<HTMLLabelElement, InputFieldProps>(
200
+ (
201
+ {
202
+ label,
203
+ placeholder,
204
+ icon: Icon,
205
+ index,
206
+ value,
207
+ onChange,
208
+ error,
209
+ disabled,
210
+ className,
211
+ ...props
212
+ },
213
+ ref,
214
+ ) => {
215
+ const internalRef = useRef<HTMLLabelElement>(null);
216
+ const { registerItem, activeIndex } = useInputGroup();
217
+ const [isFocused, setIsFocused] = useState(false);
218
+ const shape = useShape();
219
+
220
+ useEffect(() => {
221
+ registerItem(index, internalRef.current);
222
+ return () => registerItem(index, null);
223
+ }, [index, registerItem]);
48
224
 
49
- if (!hasLeading && !hasTrailing) {
50
- return <input ref={ref} className={cn(baseInput, radius, "px-3", className)} {...props} />;
225
+ const isActive = activeIndex === index;
226
+ const labelActive = isActive || isFocused;
227
+
228
+ const errorId = error ? `input-error-${index}` : undefined;
229
+
230
+ const handleFocus = () => {
231
+ setIsFocused(true);
232
+ };
233
+
234
+ const handleBlur = () => {
235
+ setIsFocused(false);
236
+ };
237
+
238
+ // Input container classes
239
+ let bgClass: string;
240
+ let ringClass: string;
241
+
242
+ if (disabled) {
243
+ bgClass = "bg-transparent";
244
+ ringClass = "ring-border";
245
+ } else if (error) {
246
+ bgClass = isFocused ? "bg-card" : isActive ? "bg-destructive-light/60" : "bg-transparent";
247
+ ringClass = isFocused || isActive ? "ring-destructive/50" : "ring-transparent";
248
+ } else if (isFocused) {
249
+ bgClass = "bg-card";
250
+ ringClass = "ring-border";
251
+ } else if (isActive) {
252
+ bgClass = "bg-muted/50";
253
+ ringClass = "ring-border";
254
+ } else {
255
+ bgClass = "bg-transparent";
256
+ ringClass = "ring-transparent";
51
257
  }
52
258
 
53
259
  return (
54
- <div className={cn("flex", wrapperClassName)}>
55
- {/* Leading addon (text) */}
56
- {leadingAddon && (
260
+ <label
261
+ ref={(node) => {
262
+ (internalRef as React.MutableRefObject<HTMLLabelElement | null>).current = node;
263
+ if (typeof ref === "function") ref(node);
264
+ else if (ref) (ref as React.MutableRefObject<HTMLLabelElement | null>).current = node;
265
+ }}
266
+ className={cn(
267
+ "flex flex-col gap-1 cursor-text",
268
+ disabled && "opacity-50 pointer-events-none",
269
+ className,
270
+ )}
271
+ >
272
+ {/* Label */}
273
+ <span className="inline-grid text-[13px] pl-3">
57
274
  <span
58
- className={cn(
59
- "inline-flex items-center border border-r-0 bg-muted px-3 body text-muted-foreground select-none",
60
- error ? "border-destructive" : "border-border",
61
- pill ? "rounded-l-full" : "rounded-l-lg",
62
- )}
275
+ className="col-start-1 row-start-1 invisible"
276
+ style={{ fontVariationSettings: fontWeights.semibold }}
277
+ aria-hidden="true"
63
278
  >
64
- {leadingAddon}
279
+ {label}
65
280
  </span>
66
- )}
67
-
68
- {/* Relative wrapper for icon-only inputs */}
69
- <div className="relative flex-1">
70
- {leadingIcon && (
71
- <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground">
72
- {leadingIcon}
73
- </div>
74
- )}
75
-
76
- <input
77
- ref={ref}
281
+ <span
78
282
  className={cn(
79
- baseInput,
80
- // Border radius
81
- leadingAddon
82
- ? trailingAddon || trailingButton
83
- ? "rounded-none px-3"
84
- : pill
85
- ? "rounded-none rounded-r-full px-3"
86
- : "rounded-none rounded-r-lg px-3"
87
- : leadingIcon
88
- ? trailingAddon
89
- ? pill
90
- ? "rounded-l-full rounded-r-none pl-9 pr-3"
91
- : "rounded-l-lg rounded-r-none pl-9 pr-3"
92
- : hasInsetTrailing
93
- ? cn(radius, "pl-9 pr-9")
94
- : cn(radius, "pl-9 pr-3")
95
- : trailingAddon
96
- ? pill
97
- ? "rounded-l-full rounded-r-none px-3"
98
- : "rounded-l-lg rounded-r-none px-3"
99
- : hasInsetTrailing
100
- ? cn(radius, "pl-3 pr-9")
101
- : cn(radius, "px-3"),
102
- trailingAction && "pr-16",
103
- className,
283
+ "col-start-1 row-start-1",
284
+ error ? "text-destructive" : "text-muted-foreground",
104
285
  )}
105
- {...props}
106
- />
286
+ style={{
287
+ fontVariationSettings: fontWeights.normal,
288
+ }}
289
+ >
290
+ {label}
291
+ </span>
292
+ </span>
107
293
 
108
- {(trailingIcon || error) && (
109
- <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
110
- {error ? (
111
- <AlertCircle size={15} className="text-destructive" />
112
- ) : (
113
- <span className="text-muted-foreground">{trailingIcon}</span>
114
- )}
115
- </div>
294
+ {/* Input container */}
295
+ <div
296
+ className={cn(
297
+ `flex items-center gap-2 ${shape.input} px-3 py-2 ring-1 transition-all duration-80`,
298
+ bgClass,
299
+ ringClass,
116
300
  )}
117
-
118
- {trailingAction && (
119
- <div
301
+ >
302
+ {Icon && (
303
+ <Icon
304
+ size={16}
305
+ strokeWidth={labelActive ? 2 : 1.5}
120
306
  className={cn(
121
- "absolute right-2 top-1/2 flex -translate-y-1/2 items-center",
122
- trailingActionClassName,
307
+ "shrink-0 transition-[color,stroke-width] duration-80",
308
+ labelActive ? "text-foreground" : "text-muted-foreground",
123
309
  )}
124
- >
125
- {trailingAction}
126
- </div>
310
+ />
127
311
  )}
312
+ <input
313
+ type="text"
314
+ value={value}
315
+ onChange={(e) => onChange(e.target.value)}
316
+ onFocus={handleFocus}
317
+ onBlur={handleBlur}
318
+ placeholder={placeholder}
319
+ disabled={disabled}
320
+ aria-invalid={!!error || undefined}
321
+ aria-describedby={errorId}
322
+ className="w-full bg-transparent text-[13px] text-foreground placeholder:text-muted-foreground outline-none font-[inherit]"
323
+ style={{ fontVariationSettings: fontWeights.normal }}
324
+ {...props}
325
+ />
128
326
  </div>
129
327
 
130
- {/* Trailing addon (text) */}
131
- {trailingAddon && (
328
+ {/* Error message */}
329
+ {error && (
132
330
  <span
133
- className={cn(
134
- "inline-flex items-center border border-l-0 bg-muted px-3 body text-muted-foreground select-none",
135
- error ? "border-destructive" : "border-border",
136
- pill ? "rounded-r-full" : "rounded-r-lg",
137
- )}
331
+ id={errorId}
332
+ className="text-[12px] text-destructive pl-3"
333
+ style={{ fontVariationSettings: fontWeights.medium }}
138
334
  >
139
- {trailingAddon}
335
+ {error}
140
336
  </span>
141
337
  )}
142
-
143
- {/* Trailing button */}
144
- {trailingButton && <div className="flex">{trailingButton}</div>}
145
- </div>
338
+ </label>
146
339
  );
147
340
  },
148
341
  );
149
- InputGroup.displayName = "InputGroup";
150
342
 
151
- export { InputGroup };
343
+ InputField.displayName = "InputField";
344
+
345
+ export { InputGroup, InputField };
346
+ export default InputGroup;