@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
@@ -0,0 +1,102 @@
1
+ import * as React from "react";
2
+
3
+ export const motionClasses =
4
+ "transition-[background-color,border-color,box-shadow,color,opacity,transform] duration-150 ease-out motion-reduce:transition-none";
5
+
6
+ export const springMotionClasses =
7
+ "transition-[background-color,border-color,box-shadow,color,opacity,transform] duration-200 ease-[cubic-bezier(0.2,0.9,0.2,1.15)] motion-reduce:transition-none";
8
+
9
+ export const pressClasses = "active:scale-[0.97] motion-reduce:active:scale-100";
10
+
11
+ export const focusRingClasses =
12
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30 focus-visible:ring-offset-1";
13
+
14
+ export const iconStrokeClasses =
15
+ "[&_svg]:transition-[stroke-width,transform,color] [&_svg]:duration-150 [&_svg]:ease-out group-hover:[&_svg]:stroke-[2]";
16
+
17
+ export const controlSurfaceClasses =
18
+ "border border-border bg-card shadow-natural hover:border-ground-200 hover:shadow-elevated";
19
+
20
+ export const surfaceElevationClasses = {
21
+ flat: "border border-border bg-card",
22
+ raised: "border border-border bg-card shadow-natural",
23
+ floating: "border border-border bg-card shadow-elevated",
24
+ } as const;
25
+
26
+ export const radiusClasses = {
27
+ control: "rounded-md",
28
+ panel: "rounded-xl",
29
+ pill: "rounded-full",
30
+ } as const;
31
+
32
+ export const menuItemClasses =
33
+ "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 body outline-none transition-[background-color,color,box-shadow] duration-150 ease-out";
34
+
35
+ export const menuItemActiveClasses =
36
+ "focus:bg-muted focus:text-foreground data-[highlighted]:bg-muted";
37
+
38
+ export function useProximityIndex<T extends HTMLElement>(axis: "x" | "y" = "y") {
39
+ const containerRef = React.useRef<T | null>(null);
40
+ const itemRefs = React.useRef(new Map<number, HTMLElement>());
41
+ const frameRef = React.useRef<number | null>(null);
42
+ const [activeIndex, setActiveIndex] = React.useState<number | null>(null);
43
+
44
+ const registerItem = React.useCallback((index: number, node: HTMLElement | null) => {
45
+ if (node) itemRefs.current.set(index, node);
46
+ else itemRefs.current.delete(index);
47
+ }, []);
48
+
49
+ const onPointerMove = React.useCallback(
50
+ (event: React.PointerEvent<T>) => {
51
+ if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
52
+ const pointer = axis === "x" ? event.clientX : event.clientY;
53
+
54
+ frameRef.current = requestAnimationFrame(() => {
55
+ frameRef.current = null;
56
+ let closestIndex: number | null = null;
57
+ let closestDistance = Number.POSITIVE_INFINITY;
58
+
59
+ itemRefs.current.forEach((node, index) => {
60
+ const rect = node.getBoundingClientRect();
61
+ const start = axis === "x" ? rect.left : rect.top;
62
+ const size = axis === "x" ? rect.width : rect.height;
63
+ const center = start + size / 2;
64
+ const distance = Math.abs(pointer - center);
65
+
66
+ if (distance < closestDistance) {
67
+ closestDistance = distance;
68
+ closestIndex = index;
69
+ }
70
+ });
71
+
72
+ setActiveIndex(closestIndex);
73
+ });
74
+ },
75
+ [axis],
76
+ );
77
+
78
+ const clearActiveIndex = React.useCallback(() => {
79
+ if (frameRef.current !== null) {
80
+ cancelAnimationFrame(frameRef.current);
81
+ frameRef.current = null;
82
+ }
83
+ setActiveIndex(null);
84
+ }, []);
85
+
86
+ React.useEffect(
87
+ () => () => {
88
+ if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
89
+ },
90
+ [],
91
+ );
92
+
93
+ return {
94
+ activeIndex,
95
+ containerRef,
96
+ registerItem,
97
+ handlers: {
98
+ onPointerMove,
99
+ onPointerLeave: clearActiveIndex,
100
+ },
101
+ };
102
+ }
@@ -0,0 +1,241 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type CSSProperties, type RefObject } from "react";
4
+ import { useSurface } from "./surface-context";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Scroll-edge primitives
8
+ //
9
+ // useScrollEdges tracks which edges of a scroll container have more content
10
+ // beyond them; ScrollEdgeCue renders the affordance for one edge — a
11
+ // surface-colour gradient fading the content out toward the edge, with a
12
+ // small chevron hinting at the scroll direction. Together they give any
13
+ // scrolling surface the same "there's more" cue the Select menu uses.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface ScrollEdges {
17
+ top: boolean;
18
+ bottom: boolean;
19
+ left: boolean;
20
+ right: boolean;
21
+ }
22
+
23
+ const NO_EDGES: ScrollEdges = {
24
+ top: false,
25
+ bottom: false,
26
+ left: false,
27
+ right: false,
28
+ };
29
+
30
+ export interface UseScrollEdgesOptions {
31
+ /** Attach/detach tracking. While false all edges read false. Callers whose
32
+ * scroller mounts late (portals) must fold that into `enabled` so the
33
+ * hook re-attaches once the element exists. Defaults to `true`. */
34
+ enabled?: boolean;
35
+ /** Which axes to measure. Defaults to `"vertical"`. */
36
+ axis?: "vertical" | "horizontal" | "both";
37
+ }
38
+
39
+ export function useScrollEdges(
40
+ ref: RefObject<HTMLElement | null>,
41
+ { enabled = true, axis = "vertical" }: UseScrollEdgesOptions = {},
42
+ ): ScrollEdges {
43
+ const [edges, setEdges] = useState<ScrollEdges>(NO_EDGES);
44
+
45
+ useEffect(() => {
46
+ if (!enabled) {
47
+ setEdges(NO_EDGES);
48
+ return;
49
+ }
50
+ const el = ref.current;
51
+ if (!el) return;
52
+
53
+ const update = () => {
54
+ const next = { ...NO_EDGES };
55
+ if (axis !== "horizontal") {
56
+ const { scrollTop, scrollHeight, clientHeight } = el;
57
+ const overflowing = scrollHeight - clientHeight > 1;
58
+ next.top = overflowing && scrollTop > 1;
59
+ next.bottom = overflowing && scrollTop + clientHeight < scrollHeight - 1;
60
+ }
61
+ if (axis !== "vertical") {
62
+ const { scrollLeft, scrollWidth, clientWidth } = el;
63
+ const overflowing = scrollWidth - clientWidth > 1;
64
+ next.left = overflowing && scrollLeft > 1;
65
+ next.right = overflowing && scrollLeft + clientWidth < scrollWidth - 1;
66
+ }
67
+ // Bail out on no-op updates so observer churn doesn't re-render.
68
+ setEdges((prev) =>
69
+ prev.top === next.top &&
70
+ prev.bottom === next.bottom &&
71
+ prev.left === next.left &&
72
+ prev.right === next.right
73
+ ? prev
74
+ : next,
75
+ );
76
+ };
77
+
78
+ update();
79
+ // Recompute once layout settles after enter animations.
80
+ const raf = requestAnimationFrame(update);
81
+ el.addEventListener("scroll", update, { passive: true });
82
+ const ro = new ResizeObserver(update);
83
+ ro.observe(el);
84
+ // Async content (items loading in, streamed text) changes scrollHeight
85
+ // without resizing the container itself. Coalesce to one update per
86
+ // frame: update() reads layout, and doing that synchronously after
87
+ // every mutation forces a reflow per insertion in streaming content.
88
+ let moRaf = 0;
89
+ const scheduleUpdate = () => {
90
+ if (moRaf) return;
91
+ moRaf = requestAnimationFrame(() => {
92
+ moRaf = 0;
93
+ update();
94
+ });
95
+ };
96
+ const mo = new MutationObserver(scheduleUpdate);
97
+ mo.observe(el, { childList: true, subtree: true, characterData: true });
98
+ return () => {
99
+ cancelAnimationFrame(raf);
100
+ if (moRaf) cancelAnimationFrame(moRaf);
101
+ el.removeEventListener("scroll", update);
102
+ ro.disconnect();
103
+ mo.disconnect();
104
+ };
105
+ }, [ref, enabled, axis]);
106
+
107
+ return edges;
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // ScrollEdgeCue
112
+ // ---------------------------------------------------------------------------
113
+
114
+ const CHEVRON_PATHS: Record<string, string> = {
115
+ top: "M6 15l6-6 6 6",
116
+ bottom: "M6 9l6 6 6-6",
117
+ left: "M15 6l-6 6 6 6",
118
+ right: "M9 6l6 6-6 6",
119
+ };
120
+
121
+ // Band size presets along the scroll axis. The chevron stays 16px in both.
122
+ const CUE_SIZES = { tight: 32, comfortable: 60 } as const;
123
+
124
+ export type ScrollEdgeCueSize = keyof typeof CUE_SIZES;
125
+
126
+ export interface ScrollEdgeCueProps {
127
+ edge: "top" | "bottom" | "left" | "right";
128
+ visible: boolean;
129
+ /** `"sticky"` renders a zero-size sticky element placed inside the scroller
130
+ * itself (before/after the scrolling content); `"absolute"` renders a plain
131
+ * positioned band for an `absolute inset-0 pointer-events-none` overlay
132
+ * sitting over the viewport. Defaults to `"sticky"`. */
133
+ mode?: "sticky" | "absolute";
134
+ /** Surface ladder level the gradient fades toward. Defaults to the level
135
+ * provided by the nearest SurfaceProvider/Elevated — override only when
136
+ * the scroller sits on a background outside the ladder. */
137
+ surfaceLevel?: number;
138
+ /** Band size along the scroll axis: `"tight"` (32px) or `"comfortable"`
139
+ * (60px). The chevron is 16px in either. Defaults to `"comfortable"`. */
140
+ size?: ScrollEdgeCueSize;
141
+ /** Sticky-mode bleed in px so the band covers the scroller's padding
142
+ * (e.g. 4 for `p-1`, 16 for `p-4`). Defaults to 4. */
143
+ inset?: number;
144
+ /** Show the directional chevron in the band. The gradient fade always
145
+ * renders; set to `false` for a fade-only cue. Defaults to `true`. */
146
+ chevron?: boolean;
147
+ }
148
+
149
+ export function ScrollEdgeCue({
150
+ edge,
151
+ visible,
152
+ mode = "sticky",
153
+ surfaceLevel,
154
+ size = "comfortable",
155
+ inset = 4,
156
+ chevron = true,
157
+ }: ScrollEdgeCueProps) {
158
+ const contextLevel = useSurface();
159
+ // Clamp to the ladder (1–8), mirroring SurfaceProvider — an out-of-range
160
+ // override would interpolate an invalid var and silently kill the gradient.
161
+ const level = Math.max(1, Math.min(8, surfaceLevel ?? contextLevel));
162
+ const surface = `var(--surface-${level})`;
163
+ const vertical = edge === "top" || edge === "bottom";
164
+ const sizePx = CUE_SIZES[size];
165
+ // Gradient direction where 100% == the hard outer edge.
166
+ const dir = `to ${edge}`;
167
+
168
+ const band = (
169
+ <div
170
+ style={
171
+ {
172
+ position: "absolute",
173
+ opacity: visible ? 1 : 0,
174
+ // Exit slightly faster than enter, per the animation guidelines.
175
+ transition: `opacity ${visible ? 160 : 120}ms ease`,
176
+ ...(mode === "sticky"
177
+ ? vertical
178
+ ? { left: -inset, right: -inset, [edge]: -inset, height: sizePx }
179
+ : { top: -inset, bottom: -inset, [edge]: -inset, width: sizePx }
180
+ : vertical
181
+ ? { left: 0, right: 0, [edge]: 0, height: sizePx }
182
+ : { top: 0, bottom: 0, [edge]: 0, width: sizePx }),
183
+ } as CSSProperties
184
+ }
185
+ >
186
+ <div
187
+ style={{
188
+ position: "absolute",
189
+ inset: 0,
190
+ background: `linear-gradient(${dir}, transparent 0%, color-mix(in srgb, ${surface} 75%, transparent) 65%, ${surface} 100%)`,
191
+ }}
192
+ />
193
+ {chevron && (
194
+ <svg
195
+ width={16}
196
+ height={16}
197
+ viewBox="0 0 24 24"
198
+ fill="none"
199
+ stroke="currentColor"
200
+ strokeWidth={2}
201
+ strokeLinecap="round"
202
+ strokeLinejoin="round"
203
+ className="text-muted-foreground"
204
+ style={
205
+ {
206
+ position: "absolute",
207
+ ...(vertical
208
+ ? { left: "50%", transform: "translateX(-50%)" }
209
+ : { top: "50%", transform: "translateY(-50%)" }),
210
+ [edge]: 8,
211
+ } as CSSProperties
212
+ }
213
+ >
214
+ <path d={CHEVRON_PATHS[edge]} />
215
+ </svg>
216
+ )}
217
+ </div>
218
+ );
219
+
220
+ if (mode === "absolute") {
221
+ return <div aria-hidden>{band}</div>;
222
+ }
223
+
224
+ // Sticky: a zero-size sticky anchor so the cue adds no layout extent.
225
+ return (
226
+ <div
227
+ aria-hidden
228
+ style={
229
+ {
230
+ position: "sticky",
231
+ [edge]: 0,
232
+ ...(vertical ? { height: 0 } : { width: 0 }),
233
+ zIndex: 30,
234
+ pointerEvents: "none",
235
+ } as CSSProperties
236
+ }
237
+ >
238
+ {band}
239
+ </div>
240
+ );
241
+ }
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
4
+
5
+ type ShapeVariant = "pill" | "rounded";
6
+
7
+ const shapeOrder: ShapeVariant[] = ["rounded", "pill"];
8
+
9
+ interface ShapeClasses {
10
+ item: string;
11
+ bg: string;
12
+ focusRing: string;
13
+ mergedBg: string;
14
+ container: string;
15
+ button: string;
16
+ input: string;
17
+ // Numeric counterparts of `bg` / `mergedBg`, in px. Needed where individual
18
+ // corners are animated (e.g. the selected-background merge/split animation),
19
+ // which requires per-corner numeric border-radii rather than a class.
20
+ bgRadius: number;
21
+ mergedRadius: number;
22
+ }
23
+
24
+ const shapeMap: Record<ShapeVariant, ShapeClasses> = {
25
+ pill: {
26
+ item: "rounded-[20px]",
27
+ bg: "rounded-[20px]",
28
+ // +2px over `item` because the focus ring sits 2px outside the element
29
+ // (top/left -2, width/height +4); this keeps the corners concentric so a
30
+ // pill element gets a pill ring (matches the rounded-mode 8px→10px bump).
31
+ focusRing: "rounded-[22px]",
32
+ mergedBg: "rounded-2xl",
33
+ container: "rounded-3xl",
34
+ button: "rounded-[20px]",
35
+ input: "rounded-[20px]",
36
+ bgRadius: 20,
37
+ mergedRadius: 16,
38
+ },
39
+ rounded: {
40
+ item: "rounded-lg",
41
+ bg: "rounded-lg",
42
+ focusRing: "rounded-[10px]",
43
+ mergedBg: "rounded-lg",
44
+ container: "rounded-xl",
45
+ button: "rounded-lg",
46
+ input: "rounded-lg",
47
+ bgRadius: 8,
48
+ mergedRadius: 8,
49
+ },
50
+ };
51
+
52
+ interface ShapeContextValue {
53
+ shape: ShapeVariant;
54
+ setShape: (shape: ShapeVariant) => void;
55
+ classes: ShapeClasses;
56
+ }
57
+
58
+ const ShapeContext = createContext<ShapeContextValue | null>(null);
59
+
60
+ function useShape(): ShapeClasses {
61
+ const ctx = useContext(ShapeContext);
62
+ if (!ctx) return shapeMap.pill;
63
+ return ctx.classes;
64
+ }
65
+
66
+ function useShapeContext() {
67
+ const ctx = useContext(ShapeContext);
68
+ if (!ctx) throw new Error("useShapeContext must be used within a ShapeProvider");
69
+ return ctx;
70
+ }
71
+
72
+ function transitionShape(callback: () => void) {
73
+ const root = document.documentElement;
74
+ root.classList.add("transitioning");
75
+ void root.offsetHeight;
76
+ callback();
77
+ setTimeout(() => root.classList.remove("transitioning"), 200);
78
+ }
79
+
80
+ function ShapeProvider({
81
+ children,
82
+ defaultShape = "pill",
83
+ }: {
84
+ children: ReactNode;
85
+ defaultShape?: ShapeVariant;
86
+ }) {
87
+ const [shape, setShapeState] = useState<ShapeVariant>(defaultShape);
88
+
89
+ const setShape = useCallback((next: ShapeVariant) => {
90
+ transitionShape(() => setShapeState(next));
91
+ }, []);
92
+
93
+ // Global keyboard shortcut: R to cycle radius
94
+ useEffect(() => {
95
+ const onKeyDown = (e: KeyboardEvent) => {
96
+ if (e.key !== "r" && e.key !== "R") return;
97
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
98
+ const tag = (e.target as HTMLElement)?.tagName;
99
+ if (tag === "INPUT" || tag === "TEXTAREA" || (e.target as HTMLElement)?.isContentEditable)
100
+ return;
101
+ e.preventDefault();
102
+ transitionShape(() => {
103
+ setShapeState((prev) => {
104
+ const idx = shapeOrder.indexOf(prev);
105
+ return shapeOrder[(idx + 1) % shapeOrder.length];
106
+ });
107
+ });
108
+ };
109
+ document.addEventListener("keydown", onKeyDown);
110
+ return () => document.removeEventListener("keydown", onKeyDown);
111
+ }, []);
112
+
113
+ return (
114
+ <ShapeContext.Provider value={{ shape, setShape, classes: shapeMap[shape] }}>
115
+ {children}
116
+ </ShapeContext.Provider>
117
+ );
118
+ }
119
+
120
+ export { ShapeProvider, useShape, useShapeContext, shapeMap };
121
+ export type { ShapeVariant, ShapeClasses };
@@ -0,0 +1,30 @@
1
+ // Motion tokens. Each tier's value is the ENTER transition — a spring, with a
2
+ // little bounce on the larger tiers. Its `.exit` is the matching EXIT transition
3
+ // — a plain tween, no bounce, one tier quicker — so a dismissal reads as crisp
4
+ // and final rather than replaying the entrance in reverse.
5
+ //
6
+ // transition={spring.fast} // enter
7
+ // exit={{ opacity: 0, transition: spring.fast.exit }} // leave
8
+ //
9
+ // The bigger the thing that moves, the slower the spring. Never hand-write a
10
+ // duration — always reach for a tier.
11
+ export const spring = {
12
+ fast: {
13
+ type: "spring" as const,
14
+ duration: 0.08,
15
+ bounce: 0,
16
+ exit: { duration: 0.06 },
17
+ },
18
+ moderate: {
19
+ type: "spring" as const,
20
+ duration: 0.16,
21
+ bounce: 0.08,
22
+ exit: { duration: 0.12 },
23
+ },
24
+ slow: {
25
+ type: "spring" as const,
26
+ duration: 0.24,
27
+ bounce: 0.12,
28
+ exit: { duration: 0.16 },
29
+ },
30
+ } as const;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Static lookup tables for surface tokens.
3
+ *
4
+ * Tailwind v4's static scanner only generates utility classes for literal
5
+ * strings it sees in source. Template-literal class names like
6
+ * `bg-surface-${level}` are invisible to the scanner, so the matching
7
+ * utility never gets generated and the background renders transparent.
8
+ *
9
+ * Use these maps when picking a surface level at runtime:
10
+ *
11
+ * <div className={surfaceClasses(level)} />
12
+ *
13
+ * Each entry below contains the literal class name, which is enough for
14
+ * Tailwind to detect and emit the utility.
15
+ */
16
+
17
+ export const SURFACE_BG: Record<number, string> = {
18
+ 1: "bg-surface-1",
19
+ 2: "bg-surface-2",
20
+ 3: "bg-surface-3",
21
+ 4: "bg-surface-4",
22
+ 5: "bg-surface-5",
23
+ 6: "bg-surface-6",
24
+ 7: "bg-surface-7",
25
+ 8: "bg-surface-8",
26
+ };
27
+
28
+ export const SURFACE_SHADOW: Record<number, string> = {
29
+ 1: "shadow-surface-1",
30
+ 2: "shadow-surface-2",
31
+ 3: "shadow-surface-3",
32
+ 4: "shadow-surface-4",
33
+ 5: "shadow-surface-5",
34
+ 6: "shadow-surface-6",
35
+ 7: "shadow-surface-7",
36
+ 8: "shadow-surface-8",
37
+ };
38
+
39
+ export function surfaceClasses(bgLevel: number, shadowLevel: number = bgLevel): string {
40
+ const bg = Math.max(1, Math.min(8, bgLevel));
41
+ const shadow = Math.max(1, Math.min(8, shadowLevel));
42
+ return `${SURFACE_BG[bg]} ${SURFACE_SHADOW[shadow]}`;
43
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, type ReactNode } from "react";
4
+
5
+ const SurfaceContext = createContext<number>(1);
6
+
7
+ export function useSurface(): number {
8
+ return useContext(SurfaceContext);
9
+ }
10
+
11
+ export function SurfaceProvider({ value, children }: { value: number; children: ReactNode }) {
12
+ return (
13
+ <SurfaceContext.Provider value={Math.max(1, Math.min(8, value))}>
14
+ {children}
15
+ </SurfaceContext.Provider>
16
+ );
17
+ }
@@ -323,6 +323,9 @@ export const tokens = {
323
323
  "accordion-down": "accordion-down 0.2s ease-out",
324
324
  "accordion-up": "accordion-up 0.2s ease-out",
325
325
  "caret-blink": "caret-blink 1.25s ease-out infinite",
326
+ "hilum-orbit": "hilum-orbit 1.4s ease-in-out infinite",
327
+ "hilum-spring-in": "hilum-spring-in 180ms cubic-bezier(0.2, 0.9, 0.2, 1.15)",
328
+ "hilum-thinking": "hilum-thinking 1.1s ease-in-out infinite",
326
329
  },
327
330
  } as const;
328
331
 
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/tokens/tokens.ts"],"names":[],"mappings":"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","file":"chunk-MAHPAP65.js","sourcesContent":["// @hilum/ui/tokens — JS-first design tokens (D6).\n//\n// This module is the single source of truth for the Hilum visual identity.\n// A build step (scripts/build-tokens.mjs) reads the compiled output of this\n// file and emits dist/tokens.css for consumers to `@import`.\n//\n// Brand model: D8 — base palette is fixed here. Per-product overrides are\n// applied at runtime via createTheme() in @hilum/ui/create-theme.\n// Color modes: D7 — light + dark, both auto (`prefers-color-scheme`) and\n// explicit via `[data-theme]`.\n//\n// Core brand colors:\n// brand.primary #C100F1 — vivid purple — purple-500 on the scale below\n// brand.secondary #FFF5BF — pale lemon — butter-200 on the scale below\n//\n// ground — neutral spine for text, borders, and structure (values: Tailwind neutral)\n\nexport const tokens = {\n /* ============================================================== *\n * PALETTE — concrete colors *\n * ============================================================== */\n\n ground: {\n 50: \"#fafafa\",\n 100: \"#f5f5f5\",\n 200: \"#e5e5e5\",\n 300: \"#d4d4d4\",\n 400: \"#a3a3a3\",\n 500: \"#737373\",\n 600: \"#525252\",\n 700: \"#404040\",\n 800: \"#262626\",\n 900: \"#171717\",\n 950: \"#0a0a0a\",\n },\n\n // Pale lemon — anchor: butter-200 = #FFF5BF\n butter: {\n 50: \"#ffffe8\",\n 100: \"#fffad0\",\n 200: \"#fff5bf\",\n 300: \"#fde870\",\n 400: \"#f6d42a\",\n 500: \"#dab010\",\n 600: \"#a88008\",\n 700: \"#7c5c04\",\n 800: \"#543e02\",\n 900: \"#382801\",\n 950: \"#221600\",\n },\n\n // Vivid purple — anchor: purple-500 = #C100F1 (brand.primary)\n purple: {\n 50: \"#fdf0ff\",\n 100: \"#f5d6ff\",\n 200: \"#e8a8ff\",\n 300: \"#d870f9\",\n 400: \"#c840f6\",\n 500: \"#c100f1\",\n 600: \"#9c00c0\",\n 700: \"#740092\",\n 800: \"#4e0062\",\n 900: \"#330040\",\n 950: \"#200028\",\n },\n\n // Legacy functional colors — used by components not yet migrated to the new palette.\n brand: {\n primary: \"#C100F1\", // purple-500\n secondary: \"#FFF5BF\", // butter-200\n },\n\n destructive: \"#dc2626\", // red-600\n\n /* ============================================================== *\n * SEMANTIC — what components reference *\n * ============================================================== */\n\n semantic: {\n light: {\n background: \"#ffffff\",\n foreground: \"#171717\", // ground-900\n card: \"#ffffff\",\n cardForeground: \"#171717\",\n surface: \"#fafafa\", // ground-50\n surfaceForeground: \"#262626\", // ground-800\n border: \"#f5f5f5\", // ground-100\n input: \"#f5f5f5\",\n muted: \"#fafafa\",\n mutedForeground: \"#737373\", // ground-500\n accent: \"#fdf0ff\", // purple-50\n accentForeground: \"#740092\", // purple-700\n primary: \"#c100f1\", // purple-500 = brand.primary\n primaryForeground: \"#ffffff\",\n secondary: \"#fafafa\", // ground-50\n secondaryForeground: \"#404040\", // ground-700\n destructive: \"#dc2626\",\n destructiveForeground: \"#ffffff\",\n success: \"#CDEA19\", // lime — functional semantic only\n successForeground: \"#171717\",\n warning: \"#fff5bf\", // butter-200 = brand.secondary\n warningForeground: \"#171717\", // ground-900\n ring: \"#c100f1\", // purple-500 = brand.primary\n },\n // Mid — a neutral medium-gray theme (from the Pappery designer \"mid\" palette,\n // mapped onto the ground scale). Sits between light and dark.\n mid: {\n background: \"#737373\", // ground-500 — main surface (designer panel)\n foreground: \"#fafafa\", // ground-50\n card: \"#525252\", // ground-600 — elevated items (designer item-bg)\n cardForeground: \"#fafafa\",\n surface: \"#525252\", // ground-600 (designer canvas/pane)\n surfaceForeground: \"#fafafa\",\n border: \"#a3a3a3\", // ground-400 (designer border — lighter so it reads on the gray)\n input: \"#404040\", // ground-700 (designer input-bg)\n muted: \"#525252\", // ground-600\n mutedForeground: \"#e5e5e5\", // ground-200 (designer text-secondary)\n accent: \"#404040\", // ground-700 (designer item-hover-bg)\n accentForeground: \"#fafafa\",\n primary: \"#c100f1\", // brand stays consistent across themes\n primaryForeground: \"#ffffff\",\n secondary: \"#525252\", // ground-600\n secondaryForeground: \"#fafafa\",\n destructive: \"#ef4444\", // red-500\n destructiveForeground: \"#ffffff\",\n success: \"#CDEA19\", // lime — functional semantic only\n successForeground: \"#171717\",\n warning: \"#fff5bf\", // butter-200\n warningForeground: \"#171717\",\n ring: \"#c100f1\",\n },\n dark: {\n background: \"#171717\", // ground-900 — designer canvas (deepest)\n foreground: \"#fafafa\", // ground-50\n card: \"#262626\", // ground-800 — designer panel-bg (elevated over bg)\n cardForeground: \"#fafafa\",\n surface: \"#1a1a1a\", // designer item-hover depth\n surfaceForeground: \"#f5f5f5\",\n border: \"#404040\", // ground-700 — designer border (reads on the dark)\n input: \"#262626\", // ground-800\n muted: \"#262626\", // ground-800\n mutedForeground: \"#a3a3a3\", // ground-400\n accent: \"#404040\", // ground-700 — neutral elevated/hover (designer item-hover)\n accentForeground: \"#fafafa\",\n primary: \"#c100f1\", // purple-500 — brand stays consistent (D8)\n primaryForeground: \"#ffffff\",\n secondary: \"#262626\", // ground-800\n secondaryForeground: \"#f5f5f5\",\n destructive: \"#ef4444\", // red-500 (slightly lighter for dark)\n destructiveForeground: \"#ffffff\",\n success: \"#CDEA19\", // lime — functional semantic only\n successForeground: \"#0a0a0a\",\n warning: \"#221600\", // butter-950 surface\n warningForeground: \"#fffad0\", // butter-100\n ring: \"#c100f1\", // purple-500 = brand.primary\n },\n },\n\n /* ============================================================== *\n * TYPOGRAPHY *\n * ============================================================== */\n\n fontFamily: {\n sans: 'Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif',\n display:\n 'Gabarito, Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif',\n mono: 'ui-monospace, \"SFMono-Regular\", \"Menlo\", \"Consolas\", monospace',\n },\n\n /** Type scale utilities — emitted as Tailwind v4 `@utility` blocks. */\n typeScale: {\n \"display-xl\": {\n family: \"display\",\n size: \"3rem\",\n weight: 400,\n lineHeight: \"1.15\",\n textWrap: \"balance\",\n },\n display: {\n family: \"display\",\n size: \"2.25rem\",\n weight: 400,\n lineHeight: \"1.15\",\n textWrap: \"balance\",\n },\n \"heading-xl\": {\n family: \"display\",\n size: \"1.875rem\",\n weight: 400,\n lineHeight: \"1.2\",\n textWrap: \"balance\",\n },\n heading: {\n family: \"display\",\n size: \"1.5rem\",\n weight: 400,\n lineHeight: \"1.2\",\n textWrap: \"balance\",\n },\n subheading: {\n family: \"display\",\n size: \"1.25rem\",\n weight: 400,\n lineHeight: \"1.4\",\n textWrap: \"balance\",\n },\n \"body-lg\": {\n family: \"sans\",\n size: \"1rem\",\n weight: 400,\n lineHeight: \"1.625\",\n textWrap: \"pretty\",\n },\n body: {\n family: \"sans\",\n size: \"0.875rem\",\n weight: 400,\n lineHeight: \"1.625\",\n textWrap: \"pretty\",\n },\n \"body-sm\": {\n family: \"sans\",\n size: \"0.75rem\",\n weight: 500,\n lineHeight: \"1.625\",\n textWrap: \"pretty\",\n },\n caption: {\n family: \"sans\",\n size: \"0.75rem\",\n weight: 400,\n lineHeight: \"1.625\",\n textWrap: \"pretty\",\n },\n \"caption-xs\": { family: \"sans\", size: \"0.625rem\", weight: 400, lineHeight: \"1.625\" },\n\n /* --- \"Eyebrow\" family — uppercase tracked label used above headlines, --- *\n * on badges, and on section markers. `eyebrow` is the canonical name; *\n * `overline` (Material parlance), `kicker` (editorial parlance) and *\n * `label` (legacy) are aliases of the same scale so consumers can pick *\n * whichever vocabulary reads best in their context. `eyebrow-sm` is *\n * the compact variant for badge-sized chips and tiny status pills. */\n eyebrow: {\n family: \"sans\",\n size: \"0.75rem\",\n weight: 600,\n lineHeight: \"1\",\n letterSpacing: \"0.1em\",\n textTransform: \"uppercase\" as const,\n },\n \"eyebrow-sm\": {\n family: \"sans\",\n size: \"0.625rem\",\n weight: 600,\n lineHeight: \"1\",\n letterSpacing: \"0.1em\",\n textTransform: \"uppercase\" as const,\n },\n overline: {\n family: \"sans\",\n size: \"0.75rem\",\n weight: 600,\n lineHeight: \"1\",\n letterSpacing: \"0.1em\",\n textTransform: \"uppercase\" as const,\n },\n kicker: {\n family: \"sans\",\n size: \"0.75rem\",\n weight: 600,\n lineHeight: \"1\",\n letterSpacing: \"0.1em\",\n textTransform: \"uppercase\" as const,\n },\n label: {\n family: \"sans\",\n size: \"0.75rem\",\n weight: 600,\n lineHeight: \"1\",\n letterSpacing: \"0.1em\",\n textTransform: \"uppercase\" as const,\n },\n },\n\n /* ============================================================== *\n * GEOMETRY *\n * ============================================================== */\n\n radius: {\n base: \"0.5rem\",\n sm: \"calc(0.5rem - 4px)\",\n md: \"calc(0.5rem - 2px)\",\n lg: \"calc(0.5rem + 2px)\",\n xl: \"calc(0.5rem + 4px)\",\n full: \"9999px\",\n },\n\n /* ============================================================== *\n * Z-INDEX — semantic layering scale *\n * ============================================================== */\n\n zIndex: {\n dropdown: 40,\n sticky: 50,\n fixed: 60,\n modal: 100,\n popover: 110,\n tooltip: 120,\n },\n\n shadow: {\n natural:\n \"0 0px 0px 1px rgba(0, 0, 0, 0.06), 0 1px 1px -0.5px rgba(0, 0, 0, 0.06), 0 3px 3px -1.5px rgba(0, 0, 0, 0.06)\",\n elevated:\n \"0 0px 0px 1px rgba(0, 0, 0, 0.06), 0 1px 1px -0.5px rgba(0, 0, 0, 0.06), 0 3px 3px -1.5px rgba(0, 0, 0, 0.06), 0 6px 6px -3px rgba(0, 0, 0, 0.06), 0 12px 12px -6px rgba(0, 0, 0, 0.04), 0 24px 24px -12px rgba(0, 0, 0, 0.04), 0 24px 24px 2px rgba(0, 0, 0, 0.1)\",\n },\n\n /* ============================================================== *\n * ANIMATION *\n * ============================================================== */\n\n animation: {\n \"accordion-down\": \"accordion-down 0.2s ease-out\",\n \"accordion-up\": \"accordion-up 0.2s ease-out\",\n \"caret-blink\": \"caret-blink 1.25s ease-out infinite\",\n },\n} as const;\n\nexport type Tokens = typeof tokens;\nexport type SemanticTokens = typeof tokens.semantic.light;\n"]}