@liquid-noise/core 0.1.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.
package/CONCEPT.md ADDED
@@ -0,0 +1,163 @@
1
+ # Liquid Design Framework — Concept & Technical Documentation
2
+
3
+ ---
4
+
5
+ ## 1. What is Liquid?
6
+
7
+ Liquid is a design framework where every UI element reacts to two global controls:
8
+
9
+ | Control | Range | Purpose |
10
+ |---------|-------|---------|
11
+ | **Noise** | 0–10 | Information density. What appears or disappears. |
12
+ | **Size** | 0–10 | Visual scale. How large typography and elements are. |
13
+
14
+ Users adjust these via knobs in the header. Widgets update in real time in the DOM — no navigation or reload.
15
+
16
+ ---
17
+
18
+ ## 2. Noise Concept
19
+
20
+ ### Definition
21
+
22
+ **Noise** = interface elements of secondary importance. Primary information is always visible; secondary information appears as noise increases.
23
+
24
+ ### Example: Task Tracker
25
+
26
+ | Noise | Visible |
27
+ |-------|---------|
28
+ | 1 | Task name, status |
29
+ | 2 | + Deadline |
30
+ | 4 | + Tag |
31
+ | 7 | + Assignee, description preview |
32
+
33
+ ### Example: Team Avatar (implemented)
34
+
35
+ | Noise | Visible |
36
+ |-------|---------|
37
+ | 0–1 | Hidden |
38
+ | 2–3 | Avatar only |
39
+ | 4–6 | Avatar + name |
40
+ | 7–10 | Avatar + name + role |
41
+
42
+ ### Implementation
43
+
44
+ - **`useNoise()`** — Hook returning `{ noise, setNoise }`
45
+ - **`<NoiseGate min={N}>`** — Renders children when `noise >= N`; animates in/out, then fully unmounts
46
+ - **`useDeferredUnmount(show)`** — Core hook: keeps element in DOM during exit transition, then unmounts it so it takes zero layout space. Returns `{ mounted, visible }`.
47
+ - Components use `useDeferredUnmount` for noise-driven show/hide: animate first, unmount after
48
+
49
+ ---
50
+
51
+ ## 3. Size Concept
52
+
53
+ ### Definition
54
+
55
+ **Size** = global scale. All typography, avatars, icons, and spacing follow a single knob. At size 2 everything is compact; at size 9 everything is large.
56
+
57
+ ### Typography System
58
+
59
+ Three layers:
60
+
61
+ 1. **Base values** — Fixed px scale: `8, 10, 12, 14, 16, 18, 20, 24, 28`
62
+ 2. **Semantic sizes** — Tokens: `xs`, `s`, `m`, `l`, `xl`
63
+ 3. **Mapping table** — Each semantic size maps to a base value per size-knob range
64
+
65
+ ### Size-Knob Ranges
66
+
67
+ | Knob range | Tier | Description |
68
+ |------------|------|-------------|
69
+ | 0–2 | 0 | Minimal |
70
+ | 3–4 | 1 | Small |
71
+ | 5–7 | 2 | Medium |
72
+ | 8–10 | 3 | Large |
73
+
74
+ ### Typography Mapping Table
75
+
76
+ | Semantic | Tier 0 (0–2) | Tier 1 (3–4) | Tier 2 (5–7) | Tier 3 (8–10) |
77
+ |----------|--------------|--------------|--------------|---------------|
78
+ | xs | 10px | 12px | 14px | 16px |
79
+ | s | 12px | 14px | 16px | 18px |
80
+ | m | 14px | 16px | 18px | 20px |
81
+ | l | 16px | 18px | 20px | 24px |
82
+ | xl | 18px | 20px | 24px | 28px |
83
+
84
+ ### Usage in Components
85
+
86
+ ```tsx
87
+ import { useSize, getFontSize, getAvatarSize } from "@liquid-noise/core";
88
+
89
+ function MyWidget() {
90
+ const { size } = useSize();
91
+ const nameSize = getFontSize("s", size);
92
+ const labelSize = getFontSize("xs", size);
93
+ const avatarPx = getAvatarSize(size);
94
+
95
+ return (
96
+ <div>
97
+ <span style={{ fontSize: `${nameSize}px` }}>Name</span>
98
+ <span style={{ fontSize: `${labelSize}px` }}>Label</span>
99
+ <div style={{ width: avatarPx, height: avatarPx }} />
100
+ </div>
101
+ );
102
+ }
103
+ ```
104
+
105
+ ### CSS Variables
106
+
107
+ `FrameworkSizeSync` updates `:root` when size changes:
108
+
109
+ - `--fw-font-xs`, `--fw-font-s`, `--fw-font-m`, `--fw-font-l`, `--fw-font-xl`
110
+ - `--fw-avatar-size`
111
+
112
+ Use `var(--fw-font-s)` in CSS when the component is inside the size context.
113
+
114
+ ---
115
+
116
+ ## 4. Global Easing
117
+
118
+ All transitions in Liquid elements use a **single global easing**: `--fw-ease` (CSS) / `FW_EASE` (TS). Any change — noise, size, appear, collapse — uses this curve. Do not add custom easing for framework elements.
119
+
120
+ ---
121
+
122
+ ## 5. File Structure
123
+
124
+ ```
125
+ liquid-noise-framework/
126
+ ├── src/
127
+ │ ├── index.ts # Barrel export
128
+ │ ├── providers/
129
+ │ │ ├── NoiseProvider.tsx # NoiseProvider, useNoise
130
+ │ │ ├── SizeProvider.tsx # SizeProvider, useSize
131
+ │ │ └── FrameworkSizeSync.tsx
132
+ │ ├── hooks/
133
+ │ │ └── useDeferredUnmount.ts
134
+ │ ├── components/
135
+ │ │ ├── NoiseGate.tsx
136
+ │ │ └── RotaryKnob.tsx
137
+ │ ├── tokens/
138
+ │ │ ├── transitions.ts
139
+ │ │ └── typography.ts
140
+ │ └── styles/
141
+ │ ├── framework.css
142
+ │ ├── transitions.css
143
+ │ └── typography.css
144
+ ├── MANIFEST.md
145
+ └── CONCEPT.md
146
+ ```
147
+
148
+ ---
149
+
150
+ ## 6. Creating a New Widget
151
+
152
+ 1. **Noise tiers** — Decide at which noise levels each element appears
153
+ 2. **Semantic sizes** — Assign xs/s/m/l/xl to text and labels
154
+ 3. **Avatar/icon scale** — Use `getAvatarSize(size)` if the widget has avatars or icons
155
+ 4. **Transitions** — Add CSS transitions for opacity, font-size, width, height when values change
156
+
157
+ ---
158
+
159
+ ## 7. Widget Registry (Current)
160
+
161
+ | Widget | Noise tiers | Size tokens |
162
+ |--------|-------------|-------------|
163
+ | Team avatar | 0–1 hidden, 2–3 avatar, 4–6 +name, 7+ +role | Avatar, name (s), role (xs), label (xs) |
package/MANIFEST.md ADDED
@@ -0,0 +1,48 @@
1
+ # Liquid Design Framework — Manifest
2
+
3
+ > See also: [CONCEPT.md](./CONCEPT.md) — full concept and technical documentation
4
+
5
+ ---
6
+
7
+ ## Vision
8
+
9
+ All UI elements follow a single principle: **information density is user-controlled**. The user adjusts two knobs in the header, and every widget in the app responds — showing or hiding secondary details, scaling typography and visuals — without page reloads or mode switches.
10
+
11
+ ---
12
+
13
+ ## Core Concepts
14
+
15
+ ### Noise (0–10)
16
+
17
+ **Noise** = elements of secondary importance. The higher the noise level, the more detail is visible.
18
+
19
+ - **Low noise (0–2):** Only the most essential information. For a task list: task name and status.
20
+ - **High noise (7–10):** Full detail: deadlines, tags, assignees, descriptions.
21
+
22
+ Each widget defines its own noise tiers. Elements appear or disappear in the DOM as the user turns the knob. Transitions are smooth (opacity, max-width).
23
+
24
+ ### Size (0–10)
25
+
26
+ **Size** = global scale token. Typography, avatars, icons, and spacing scale together.
27
+
28
+ - **Low size (0–2):** Compact view. Small text, small avatars.
29
+ - **High size (8–10):** Large, readable view. Big text, big avatars.
30
+
31
+ Size uses a semantic typography system: base values (8, 10, 12… px), semantic tokens (xs, s, m, l, xl), and a mapping table per knob range.
32
+
33
+ ---
34
+
35
+ ## Principles
36
+
37
+ 1. **Single global easing** — All transitions use `--fw-ease` / `FW_EASE`. Every change in framework elements uses this curve.
38
+ 2. **Widgets respond in-place** — No separate "compact" or "detail" views. The same DOM morphs.
39
+ 3. **Gradual implementation** — One widget at a time. Each widget is noise- and size-aware.
40
+ 4. **Semantic tokens** — Components assign semantic sizes (e.g. name = S, role = XS). Actual px values come from the framework.
41
+ 5. **User control** — The user, not the system, decides how much information to see and at what scale.
42
+
43
+ ---
44
+
45
+ ## Status
46
+
47
+ - **v0** — Noise + size infrastructure. First widget: team avatar (noise tiers + size token).
48
+ - **Next** — Tasks, projects, and other widgets.
@@ -0,0 +1,102 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type NoiseContextValue = {
5
+ noise: number;
6
+ setNoise: (v: number) => void;
7
+ };
8
+ declare function NoiseProvider({ children, initialValue, onSave, }: {
9
+ children: ReactNode;
10
+ initialValue?: number;
11
+ onSave?: (value: number) => void;
12
+ }): react_jsx_runtime.JSX.Element;
13
+ declare function useNoise(): NoiseContextValue;
14
+
15
+ type SizeContextValue = {
16
+ size: number;
17
+ setSize: (v: number) => void;
18
+ };
19
+ declare function SizeProvider({ children, initialValue, onSave, }: {
20
+ children: ReactNode;
21
+ initialValue?: number;
22
+ onSave?: (value: number) => void;
23
+ }): react_jsx_runtime.JSX.Element;
24
+ declare function useSize(): SizeContextValue;
25
+
26
+ /** Syncs size-knob value to CSS custom properties on :root. */
27
+ declare function FrameworkSizeSync(): null;
28
+
29
+ /**
30
+ * Liquid Framework — deferred unmount hook.
31
+ *
32
+ * Keeps a component mounted for the duration of the exit transition,
33
+ * then unmounts it so it takes zero layout space.
34
+ *
35
+ * Usage:
36
+ * const { mounted, visible } = useDeferredUnmount(showName);
37
+ * // mounted = true while element should exist in DOM (including exit animation)
38
+ * // visible = true when element should be fully shown (drives CSS values)
39
+ *
40
+ * {mounted && (
41
+ * <div style={{ opacity: visible ? 1 : 0, transition: FW_TRANSITION_APPEAR }}>
42
+ * ...
43
+ * </div>
44
+ * )}
45
+ *
46
+ * Flow:
47
+ * show=true → mounted=true, visible=true (element mounts, CSS transitions in)
48
+ * show=false → mounted=true, visible=false (CSS transitions out)
49
+ * → after duration → mounted=false (element unmounts, zero layout)
50
+ */
51
+ declare function useDeferredUnmount(show: boolean, durationMs?: number): {
52
+ mounted: boolean;
53
+ visible: boolean;
54
+ };
55
+
56
+ type NoiseGateProps = {
57
+ min: number;
58
+ children: ReactNode;
59
+ };
60
+ /** Renders children when noise >= min. Animates in/out, then fully unmounts. */
61
+ declare function NoiseGate({ min, children }: NoiseGateProps): react_jsx_runtime.JSX.Element | null;
62
+
63
+ declare function RotaryKnob({ label, defaultValue, value: controlledValue, onChange, }: {
64
+ label: string;
65
+ defaultValue?: number;
66
+ value?: number;
67
+ onChange?: (v: number) => void;
68
+ }): react_jsx_runtime.JSX.Element;
69
+
70
+ /**
71
+ * Liquid Framework — Global easing and transition values for inline styles.
72
+ * Must match styles/transitions.css.
73
+ *
74
+ * FW_EASE is the single easing used for ALL framework transitions.
75
+ * Any change in Liquid elements must use this.
76
+ */
77
+ /** Global easing — used by every transition in the framework */
78
+ declare const FW_EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
79
+ /** @deprecated Use FW_EASE */
80
+ declare const FW_EASE_OUT = "cubic-bezier(0.16, 1, 0.3, 1)";
81
+ declare const FW_DURATION_FAST = "0.15s";
82
+ declare const FW_DURATION_NORMAL = "0.25s";
83
+ declare const FW_DURATION_SLOW = "0.35s";
84
+ declare const FW_TRANSITION_APPEAR = "opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1), max-width 0.25s cubic-bezier(0.16, 1, 0.3, 1), max-height 0.25s cubic-bezier(0.16, 1, 0.3, 1), grid-template-rows 0.25s cubic-bezier(0.16, 1, 0.3, 1)";
85
+ declare const FW_TRANSITION_SIZE = "width 0.25s cubic-bezier(0.16, 1, 0.3, 1), height 0.25s cubic-bezier(0.16, 1, 0.3, 1), font-size 0.25s cubic-bezier(0.16, 1, 0.3, 1)";
86
+ /** Duration in ms for JS timers (matches FW_DURATION_NORMAL). */
87
+ declare const FW_DURATION_NORMAL_MS = 250;
88
+
89
+ /**
90
+ * Liquid Framework typography scale.
91
+ * Base values (px) and semantic sizes with mapping rules per size-knob range.
92
+ */
93
+ /** Base font sizes in px — fixed scale. */
94
+ declare const BASE_FONT_SIZES: readonly [8, 10, 12, 14, 16, 18, 20, 24, 28];
95
+ /** Semantic size tokens for component usage. */
96
+ type SemanticSize = "xs" | "s" | "m" | "l" | "xl";
97
+ /** Get font size (px) for a semantic size at the current knob value. */
98
+ declare function getFontSize(semantic: SemanticSize, sizeKnob: number): number;
99
+ /** Get avatar diameter (px) at the current knob value. */
100
+ declare function getAvatarSize(sizeKnob: number): number;
101
+
102
+ export { BASE_FONT_SIZES, FW_DURATION_FAST, FW_DURATION_NORMAL, FW_DURATION_NORMAL_MS, FW_DURATION_SLOW, FW_EASE, FW_EASE_OUT, FW_TRANSITION_APPEAR, FW_TRANSITION_SIZE, FrameworkSizeSync, NoiseGate, NoiseProvider, RotaryKnob, type SemanticSize, SizeProvider, getAvatarSize, getFontSize, useDeferredUnmount, useNoise, useSize };
package/dist/index.js ADDED
@@ -0,0 +1,349 @@
1
+ import { createContext, useState, useRef, useEffect, useCallback, useContext } from 'react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+
4
+ // src/providers/NoiseProvider.tsx
5
+ var NoiseContext = createContext(null);
6
+ var DEBOUNCE_MS = 500;
7
+ function NoiseProvider({
8
+ children,
9
+ initialValue = 0,
10
+ onSave
11
+ }) {
12
+ const [noise, setNoiseRaw] = useState(initialValue);
13
+ const timerRef = useRef(null);
14
+ useEffect(() => {
15
+ setNoiseRaw(initialValue);
16
+ }, [initialValue]);
17
+ const setNoise = useCallback(
18
+ (v) => {
19
+ setNoiseRaw(v);
20
+ if (!onSave) return;
21
+ if (timerRef.current) clearTimeout(timerRef.current);
22
+ timerRef.current = setTimeout(() => {
23
+ onSave(v);
24
+ timerRef.current = null;
25
+ }, DEBOUNCE_MS);
26
+ },
27
+ [onSave]
28
+ );
29
+ useEffect(() => {
30
+ return () => {
31
+ if (timerRef.current) clearTimeout(timerRef.current);
32
+ };
33
+ }, []);
34
+ return /* @__PURE__ */ jsx(NoiseContext.Provider, { value: { noise, setNoise }, children });
35
+ }
36
+ function useNoise() {
37
+ const ctx = useContext(NoiseContext);
38
+ if (!ctx) {
39
+ throw new Error("useNoise must be used within NoiseProvider");
40
+ }
41
+ return ctx;
42
+ }
43
+ var SizeContext = createContext(null);
44
+ var DEBOUNCE_MS2 = 500;
45
+ function SizeProvider({
46
+ children,
47
+ initialValue = 5,
48
+ onSave
49
+ }) {
50
+ const [size, setSizeRaw] = useState(initialValue);
51
+ const timerRef = useRef(null);
52
+ useEffect(() => {
53
+ setSizeRaw(initialValue);
54
+ }, [initialValue]);
55
+ const setSize = useCallback(
56
+ (v) => {
57
+ setSizeRaw(v);
58
+ if (!onSave) return;
59
+ if (timerRef.current) clearTimeout(timerRef.current);
60
+ timerRef.current = setTimeout(() => {
61
+ onSave(v);
62
+ timerRef.current = null;
63
+ }, DEBOUNCE_MS2);
64
+ },
65
+ [onSave]
66
+ );
67
+ useEffect(() => {
68
+ return () => {
69
+ if (timerRef.current) clearTimeout(timerRef.current);
70
+ };
71
+ }, []);
72
+ return /* @__PURE__ */ jsx(SizeContext.Provider, { value: { size, setSize }, children });
73
+ }
74
+ function useSize() {
75
+ const ctx = useContext(SizeContext);
76
+ if (!ctx) {
77
+ throw new Error("useSize must be used within SizeProvider");
78
+ }
79
+ return ctx;
80
+ }
81
+
82
+ // src/tokens/typography.ts
83
+ var BASE_FONT_SIZES = [8, 10, 12, 14, 16, 18, 20, 24, 28];
84
+ var SIZE_TABLE = {
85
+ xs: [10, 12, 14, 16],
86
+ s: [12, 14, 16, 18],
87
+ m: [14, 16, 18, 20],
88
+ l: [16, 18, 20, 24],
89
+ xl: [18, 20, 24, 28]
90
+ };
91
+ var AVATAR_SIZE_TABLE = [20, 24, 28, 36];
92
+ function sizeToTier(size) {
93
+ if (size <= 2) return 0;
94
+ if (size <= 4) return 1;
95
+ if (size <= 7) return 2;
96
+ return 3;
97
+ }
98
+ function getFontSize(semantic, sizeKnob) {
99
+ return SIZE_TABLE[semantic][sizeToTier(sizeKnob)];
100
+ }
101
+ function getAvatarSize(sizeKnob) {
102
+ return AVATAR_SIZE_TABLE[sizeToTier(sizeKnob)];
103
+ }
104
+
105
+ // src/providers/FrameworkSizeSync.tsx
106
+ function FrameworkSizeSync() {
107
+ const { size } = useSize();
108
+ useEffect(() => {
109
+ const root = document.documentElement;
110
+ const semanticSizes = ["xs", "s", "m", "l", "xl"];
111
+ semanticSizes.forEach((s) => {
112
+ root.style.setProperty(`--fw-font-${s}`, `${getFontSize(s, size)}px`);
113
+ });
114
+ root.style.setProperty(`--fw-avatar-size`, `${getAvatarSize(size)}px`);
115
+ }, [size]);
116
+ return null;
117
+ }
118
+
119
+ // src/tokens/transitions.ts
120
+ var FW_EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
121
+ var FW_EASE_OUT = FW_EASE;
122
+ var FW_DURATION_FAST = "0.15s";
123
+ var FW_DURATION_NORMAL = "0.25s";
124
+ var FW_DURATION_SLOW = "0.35s";
125
+ var FW_TRANSITION_APPEAR = `opacity ${FW_DURATION_NORMAL} ${FW_EASE}, max-width ${FW_DURATION_NORMAL} ${FW_EASE}, max-height ${FW_DURATION_NORMAL} ${FW_EASE}, grid-template-rows ${FW_DURATION_NORMAL} ${FW_EASE}`;
126
+ var FW_TRANSITION_SIZE = `width ${FW_DURATION_NORMAL} ${FW_EASE}, height ${FW_DURATION_NORMAL} ${FW_EASE}, font-size ${FW_DURATION_NORMAL} ${FW_EASE}`;
127
+ var FW_DURATION_NORMAL_MS = 250;
128
+
129
+ // src/hooks/useDeferredUnmount.ts
130
+ function useDeferredUnmount(show, durationMs = FW_DURATION_NORMAL_MS) {
131
+ const [mounted, setMounted] = useState(show);
132
+ const timerRef = useRef(null);
133
+ useEffect(() => {
134
+ if (show) {
135
+ if (timerRef.current) {
136
+ clearTimeout(timerRef.current);
137
+ timerRef.current = null;
138
+ }
139
+ setMounted(true);
140
+ } else {
141
+ timerRef.current = setTimeout(() => {
142
+ setMounted(false);
143
+ timerRef.current = null;
144
+ }, durationMs);
145
+ }
146
+ return () => {
147
+ if (timerRef.current) {
148
+ clearTimeout(timerRef.current);
149
+ timerRef.current = null;
150
+ }
151
+ };
152
+ }, [show, durationMs]);
153
+ return { mounted, visible: show };
154
+ }
155
+ function NoiseGate({ min, children }) {
156
+ const { noise } = useNoise();
157
+ const { mounted, visible } = useDeferredUnmount(noise >= min);
158
+ if (!mounted) return null;
159
+ return /* @__PURE__ */ jsx(
160
+ "div",
161
+ {
162
+ style: {
163
+ opacity: visible ? 1 : 0,
164
+ transition: FW_TRANSITION_APPEAR,
165
+ pointerEvents: visible ? "auto" : "none"
166
+ },
167
+ children
168
+ }
169
+ );
170
+ }
171
+ var MIN_ANGLE = -135;
172
+ var MAX_ANGLE = 135;
173
+ var STEPS = 10;
174
+ var KNOB_SIZE = 28;
175
+ function valueToAngle(value) {
176
+ return MIN_ANGLE + value / STEPS * (MAX_ANGLE - MIN_ANGLE);
177
+ }
178
+ function angleToValue(angle) {
179
+ const raw = (angle - MIN_ANGLE) / (MAX_ANGLE - MIN_ANGLE) * STEPS;
180
+ return Math.round(Math.max(0, Math.min(STEPS, raw)));
181
+ }
182
+ function getAngleFromEvent(e, rect) {
183
+ const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
184
+ const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
185
+ const cx = rect.left + rect.width / 2;
186
+ const cy = rect.top + rect.height / 2;
187
+ const rad = Math.atan2(clientX - cx, cy - clientY);
188
+ const deg = rad * (180 / Math.PI);
189
+ return Math.max(MIN_ANGLE, Math.min(MAX_ANGLE, deg));
190
+ }
191
+ function RotaryKnob({
192
+ label,
193
+ defaultValue = 0,
194
+ value: controlledValue,
195
+ onChange
196
+ }) {
197
+ const [internalValue, setInternalValue] = useState(defaultValue);
198
+ const value = controlledValue !== void 0 ? controlledValue : internalValue;
199
+ const knobRef = useRef(null);
200
+ const dragging = useRef(false);
201
+ const updateRef = useRef((v) => {
202
+ if (onChange) onChange(v);
203
+ else setInternalValue(v);
204
+ });
205
+ updateRef.current = (v) => {
206
+ if (onChange) onChange(v);
207
+ else setInternalValue(v);
208
+ };
209
+ const handleStart = useCallback((e) => {
210
+ e.preventDefault();
211
+ dragging.current = true;
212
+ const rect = knobRef.current.getBoundingClientRect();
213
+ const native = e.nativeEvent;
214
+ const angle = getAngleFromEvent(native, rect);
215
+ updateRef.current(angleToValue(angle));
216
+ }, []);
217
+ useEffect(() => {
218
+ const onMove = (e) => {
219
+ if (!dragging.current || !knobRef.current) return;
220
+ const rect = knobRef.current.getBoundingClientRect();
221
+ const angle = getAngleFromEvent(e, rect);
222
+ updateRef.current(angleToValue(angle));
223
+ };
224
+ const onUp = () => {
225
+ dragging.current = false;
226
+ };
227
+ window.addEventListener("mousemove", onMove);
228
+ window.addEventListener("mouseup", onUp);
229
+ window.addEventListener("touchmove", onMove);
230
+ window.addEventListener("touchend", onUp);
231
+ return () => {
232
+ window.removeEventListener("mousemove", onMove);
233
+ window.removeEventListener("mouseup", onUp);
234
+ window.removeEventListener("touchmove", onMove);
235
+ window.removeEventListener("touchend", onUp);
236
+ };
237
+ }, []);
238
+ const rotation = valueToAngle(value);
239
+ const ticks = Array.from({ length: STEPS + 1 }, (_, i) => {
240
+ const angle = valueToAngle(i);
241
+ const rad = (angle - 90) * Math.PI / 180;
242
+ const outer = KNOB_SIZE / 2 + 5;
243
+ const inner = KNOB_SIZE / 2 + 2;
244
+ return {
245
+ x1: Math.cos(rad) * inner,
246
+ y1: Math.sin(rad) * inner,
247
+ x2: Math.cos(rad) * outer,
248
+ y2: Math.sin(rad) * outer,
249
+ major: i === 0 || i === 5 || i === 10
250
+ };
251
+ });
252
+ const svgSize = KNOB_SIZE + 14;
253
+ const center = svgSize / 2;
254
+ return /* @__PURE__ */ jsxs(
255
+ "div",
256
+ {
257
+ style: {
258
+ display: "flex",
259
+ flexDirection: "column",
260
+ alignItems: "center",
261
+ gap: 1,
262
+ userSelect: "none",
263
+ WebkitUserSelect: "none"
264
+ },
265
+ children: [
266
+ /* @__PURE__ */ jsxs(
267
+ "svg",
268
+ {
269
+ ref: knobRef,
270
+ width: svgSize,
271
+ height: svgSize,
272
+ viewBox: `0 0 ${svgSize} ${svgSize}`,
273
+ style: { cursor: "grab", touchAction: "none" },
274
+ onMouseDown: handleStart,
275
+ onTouchStart: handleStart,
276
+ children: [
277
+ ticks.map((t, i) => /* @__PURE__ */ jsx(
278
+ "line",
279
+ {
280
+ x1: center + t.x1,
281
+ y1: center + t.y1,
282
+ x2: center + t.x2,
283
+ y2: center + t.y2,
284
+ stroke: i <= value ? "#111" : "#ccc",
285
+ strokeWidth: t.major ? 1.5 : 0.75,
286
+ strokeLinecap: "round"
287
+ },
288
+ i
289
+ )),
290
+ /* @__PURE__ */ jsx(
291
+ "circle",
292
+ {
293
+ cx: center,
294
+ cy: center,
295
+ r: KNOB_SIZE / 2,
296
+ fill: "#fff",
297
+ stroke: "#ddd",
298
+ strokeWidth: 1
299
+ }
300
+ ),
301
+ /* @__PURE__ */ jsx(
302
+ "circle",
303
+ {
304
+ cx: center,
305
+ cy: center,
306
+ r: KNOB_SIZE / 2 - 3,
307
+ fill: "none",
308
+ stroke: "#eee",
309
+ strokeWidth: 0.5
310
+ }
311
+ ),
312
+ /* @__PURE__ */ jsx(
313
+ "line",
314
+ {
315
+ x1: center,
316
+ y1: center,
317
+ x2: center + Math.cos((rotation - 90) * Math.PI / 180) * (KNOB_SIZE / 2 - 4),
318
+ y2: center + Math.sin((rotation - 90) * Math.PI / 180) * (KNOB_SIZE / 2 - 4),
319
+ stroke: "#111",
320
+ strokeWidth: 1.5,
321
+ strokeLinecap: "round"
322
+ }
323
+ ),
324
+ /* @__PURE__ */ jsx("circle", { cx: center, cy: center, r: 1.5, fill: "#111" })
325
+ ]
326
+ }
327
+ ),
328
+ /* @__PURE__ */ jsx(
329
+ "span",
330
+ {
331
+ style: {
332
+ fontSize: "0.55rem",
333
+ fontWeight: 500,
334
+ color: "#999",
335
+ textTransform: "uppercase",
336
+ letterSpacing: "0.06em",
337
+ lineHeight: 1
338
+ },
339
+ children: label
340
+ }
341
+ )
342
+ ]
343
+ }
344
+ );
345
+ }
346
+
347
+ export { BASE_FONT_SIZES, FW_DURATION_FAST, FW_DURATION_NORMAL, FW_DURATION_NORMAL_MS, FW_DURATION_SLOW, FW_EASE, FW_EASE_OUT, FW_TRANSITION_APPEAR, FW_TRANSITION_SIZE, FrameworkSizeSync, NoiseGate, NoiseProvider, RotaryKnob, SizeProvider, getAvatarSize, getFontSize, useDeferredUnmount, useNoise, useSize };
348
+ //# sourceMappingURL=index.js.map
349
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/providers/NoiseProvider.tsx","../src/providers/SizeProvider.tsx","../src/tokens/typography.ts","../src/providers/FrameworkSizeSync.tsx","../src/tokens/transitions.ts","../src/hooks/useDeferredUnmount.ts","../src/components/NoiseGate.tsx","../src/components/RotaryKnob.tsx"],"names":["createContext","DEBOUNCE_MS","useState","useRef","useEffect","useCallback","jsx","useContext"],"mappings":"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","file":"index.js","sourcesContent":["import {\n createContext,\n useContext,\n useState,\n useEffect,\n useRef,\n useCallback,\n type ReactNode,\n} from \"react\";\n\nexport type NoiseContextValue = {\n noise: number;\n setNoise: (v: number) => void;\n};\n\nconst NoiseContext = createContext<NoiseContextValue | null>(null);\n\nconst DEBOUNCE_MS = 500;\n\nexport function NoiseProvider({\n children,\n initialValue = 0,\n onSave,\n}: {\n children: ReactNode;\n initialValue?: number;\n onSave?: (value: number) => void;\n}) {\n const [noise, setNoiseRaw] = useState(initialValue);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Sync when initialValue changes (e.g. after loading from DB)\n useEffect(() => {\n setNoiseRaw(initialValue);\n }, [initialValue]);\n\n // Debounced save via callback\n const setNoise = useCallback(\n (v: number) => {\n setNoiseRaw(v);\n\n if (!onSave) return;\n\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = setTimeout(() => {\n onSave(v);\n timerRef.current = null;\n }, DEBOUNCE_MS);\n },\n [onSave],\n );\n\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, []);\n\n return (\n <NoiseContext.Provider value={{ noise, setNoise }}>\n {children}\n </NoiseContext.Provider>\n );\n}\n\nexport function useNoise(): NoiseContextValue {\n const ctx = useContext(NoiseContext);\n if (!ctx) {\n throw new Error(\"useNoise must be used within NoiseProvider\");\n }\n return ctx;\n}\n","import {\n createContext,\n useContext,\n useState,\n useEffect,\n useRef,\n useCallback,\n type ReactNode,\n} from \"react\";\n\nexport type SizeContextValue = {\n size: number;\n setSize: (v: number) => void;\n};\n\nconst SizeContext = createContext<SizeContextValue | null>(null);\n\nconst DEBOUNCE_MS = 500;\n\nexport function SizeProvider({\n children,\n initialValue = 5,\n onSave,\n}: {\n children: ReactNode;\n initialValue?: number;\n onSave?: (value: number) => void;\n}) {\n const [size, setSizeRaw] = useState(initialValue);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Sync when initialValue changes (e.g. after loading from DB)\n useEffect(() => {\n setSizeRaw(initialValue);\n }, [initialValue]);\n\n // Debounced save via callback\n const setSize = useCallback(\n (v: number) => {\n setSizeRaw(v);\n\n if (!onSave) return;\n\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = setTimeout(() => {\n onSave(v);\n timerRef.current = null;\n }, DEBOUNCE_MS);\n },\n [onSave],\n );\n\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, []);\n\n return (\n <SizeContext.Provider value={{ size, setSize }}>\n {children}\n </SizeContext.Provider>\n );\n}\n\nexport function useSize(): SizeContextValue {\n const ctx = useContext(SizeContext);\n if (!ctx) {\n throw new Error(\"useSize must be used within SizeProvider\");\n }\n return ctx;\n}\n","/**\n * Liquid Framework typography scale.\n * Base values (px) and semantic sizes with mapping rules per size-knob range.\n */\n\n/** Base font sizes in px — fixed scale. */\nexport const BASE_FONT_SIZES = [8, 10, 12, 14, 16, 18, 20, 24, 28] as const;\n\n/** Semantic size tokens for component usage. */\nexport type SemanticSize = \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\";\n\n/**\n * Size-knob ranges: 0-2, 3-4, 5-7, 8-10.\n * Each semantic size maps to a base value per range.\n * XS: 1-2 → 10, 3-4 → 12, 5-7 → 14, 8-10 → 16 (per user spec)\n */\nconst SIZE_TABLE: Record<SemanticSize, [number, number, number, number]> = {\n xs: [10, 12, 14, 16],\n s: [12, 14, 16, 18],\n m: [14, 16, 18, 20],\n l: [16, 18, 20, 24],\n xl: [18, 20, 24, 28],\n};\n\n/** Avatar size scale (px) per size-knob range. */\nconst AVATAR_SIZE_TABLE: [number, number, number, number] = [20, 24, 28, 36];\n\nfunction sizeToTier(size: number): 0 | 1 | 2 | 3 {\n if (size <= 2) return 0;\n if (size <= 4) return 1;\n if (size <= 7) return 2;\n return 3;\n}\n\n/** Get font size (px) for a semantic size at the current knob value. */\nexport function getFontSize(semantic: SemanticSize, sizeKnob: number): number {\n return SIZE_TABLE[semantic][sizeToTier(sizeKnob)];\n}\n\n/** Get avatar diameter (px) at the current knob value. */\nexport function getAvatarSize(sizeKnob: number): number {\n return AVATAR_SIZE_TABLE[sizeToTier(sizeKnob)];\n}\n","import { useEffect } from \"react\";\nimport { useSize } from \"./SizeProvider\";\nimport { getFontSize, getAvatarSize } from \"../tokens/typography\";\nimport type { SemanticSize } from \"../tokens/typography\";\n\n/** Syncs size-knob value to CSS custom properties on :root. */\nexport function FrameworkSizeSync() {\n const { size } = useSize();\n\n useEffect(() => {\n const root = document.documentElement;\n const semanticSizes: SemanticSize[] = [\"xs\", \"s\", \"m\", \"l\", \"xl\"];\n semanticSizes.forEach((s) => {\n root.style.setProperty(`--fw-font-${s}`, `${getFontSize(s, size)}px`);\n });\n root.style.setProperty(`--fw-avatar-size`, `${getAvatarSize(size)}px`);\n }, [size]);\n\n return null;\n}\n","/**\n * Liquid Framework — Global easing and transition values for inline styles.\n * Must match styles/transitions.css.\n *\n * FW_EASE is the single easing used for ALL framework transitions.\n * Any change in Liquid elements must use this.\n */\n\n/** Global easing — used by every transition in the framework */\nexport const FW_EASE = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** @deprecated Use FW_EASE */\nexport const FW_EASE_OUT = FW_EASE;\n\nexport const FW_DURATION_FAST = \"0.15s\";\nexport const FW_DURATION_NORMAL = \"0.25s\";\nexport const FW_DURATION_SLOW = \"0.35s\";\n\nexport const FW_TRANSITION_APPEAR = `opacity ${FW_DURATION_NORMAL} ${FW_EASE}, max-width ${FW_DURATION_NORMAL} ${FW_EASE}, max-height ${FW_DURATION_NORMAL} ${FW_EASE}, grid-template-rows ${FW_DURATION_NORMAL} ${FW_EASE}`;\nexport const FW_TRANSITION_SIZE = `width ${FW_DURATION_NORMAL} ${FW_EASE}, height ${FW_DURATION_NORMAL} ${FW_EASE}, font-size ${FW_DURATION_NORMAL} ${FW_EASE}`;\n\n/** Duration in ms for JS timers (matches FW_DURATION_NORMAL). */\nexport const FW_DURATION_NORMAL_MS = 250;\n","import { useState, useEffect, useRef } from \"react\";\nimport { FW_DURATION_NORMAL_MS } from \"../tokens/transitions\";\n\n/**\n * Liquid Framework — deferred unmount hook.\n *\n * Keeps a component mounted for the duration of the exit transition,\n * then unmounts it so it takes zero layout space.\n *\n * Usage:\n * const { mounted, visible } = useDeferredUnmount(showName);\n * // mounted = true while element should exist in DOM (including exit animation)\n * // visible = true when element should be fully shown (drives CSS values)\n *\n * {mounted && (\n * <div style={{ opacity: visible ? 1 : 0, transition: FW_TRANSITION_APPEAR }}>\n * ...\n * </div>\n * )}\n *\n * Flow:\n * show=true → mounted=true, visible=true (element mounts, CSS transitions in)\n * show=false → mounted=true, visible=false (CSS transitions out)\n * → after duration → mounted=false (element unmounts, zero layout)\n */\nexport function useDeferredUnmount(\n show: boolean,\n durationMs: number = FW_DURATION_NORMAL_MS,\n): { mounted: boolean; visible: boolean } {\n const [mounted, setMounted] = useState(show);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n if (show) {\n // Cancel any pending unmount\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n setMounted(true);\n } else {\n // Delay unmount so exit transition can play\n timerRef.current = setTimeout(() => {\n setMounted(false);\n timerRef.current = null;\n }, durationMs);\n }\n\n return () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n }, [show, durationMs]);\n\n return { mounted, visible: show };\n}\n","import type { ReactNode } from \"react\";\nimport { FW_TRANSITION_APPEAR } from \"../tokens/transitions\";\nimport { useDeferredUnmount } from \"../hooks/useDeferredUnmount\";\nimport { useNoise } from \"../providers/NoiseProvider\";\n\ntype NoiseGateProps = {\n min: number;\n children: ReactNode;\n};\n\n/** Renders children when noise >= min. Animates in/out, then fully unmounts. */\nexport function NoiseGate({ min, children }: NoiseGateProps) {\n const { noise } = useNoise();\n const { mounted, visible } = useDeferredUnmount(noise >= min);\n\n if (!mounted) return null;\n\n return (\n <div\n style={{\n opacity: visible ? 1 : 0,\n transition: FW_TRANSITION_APPEAR,\n pointerEvents: visible ? \"auto\" : \"none\",\n }}\n >\n {children}\n </div>\n );\n}\n","import { useCallback, useRef, useState, useEffect } from \"react\";\n\nconst MIN_ANGLE = -135;\nconst MAX_ANGLE = 135;\nconst STEPS = 10;\nconst KNOB_SIZE = 28;\n\nfunction valueToAngle(value: number): number {\n return MIN_ANGLE + (value / STEPS) * (MAX_ANGLE - MIN_ANGLE);\n}\n\nfunction angleToValue(angle: number): number {\n const raw = ((angle - MIN_ANGLE) / (MAX_ANGLE - MIN_ANGLE)) * STEPS;\n return Math.round(Math.max(0, Math.min(STEPS, raw)));\n}\n\nfunction getAngleFromEvent(\n e: MouseEvent | TouchEvent,\n rect: DOMRect,\n): number {\n const clientX = \"touches\" in e ? e.touches[0].clientX : e.clientX;\n const clientY = \"touches\" in e ? e.touches[0].clientY : e.clientY;\n const cx = rect.left + rect.width / 2;\n const cy = rect.top + rect.height / 2;\n const rad = Math.atan2(clientX - cx, cy - clientY);\n const deg = rad * (180 / Math.PI);\n return Math.max(MIN_ANGLE, Math.min(MAX_ANGLE, deg));\n}\n\nexport function RotaryKnob({\n label,\n defaultValue = 0,\n value: controlledValue,\n onChange,\n}: {\n label: string;\n defaultValue?: number;\n value?: number;\n onChange?: (v: number) => void;\n}) {\n const [internalValue, setInternalValue] = useState(defaultValue);\n const value =\n controlledValue !== undefined ? controlledValue : internalValue;\n const knobRef = useRef<SVGSVGElement>(null);\n const dragging = useRef(false);\n\n const updateRef = useRef((v: number) => {\n if (onChange) onChange(v);\n else setInternalValue(v);\n });\n updateRef.current = (v: number) => {\n if (onChange) onChange(v);\n else setInternalValue(v);\n };\n\n const handleStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {\n e.preventDefault();\n dragging.current = true;\n const rect = knobRef.current!.getBoundingClientRect();\n const native = e.nativeEvent as MouseEvent | TouchEvent;\n const angle = getAngleFromEvent(native, rect);\n updateRef.current(angleToValue(angle));\n }, []);\n\n useEffect(() => {\n const onMove = (e: MouseEvent | TouchEvent) => {\n if (!dragging.current || !knobRef.current) return;\n const rect = knobRef.current.getBoundingClientRect();\n const angle = getAngleFromEvent(e, rect);\n updateRef.current(angleToValue(angle));\n };\n\n const onUp = () => {\n dragging.current = false;\n };\n\n window.addEventListener(\"mousemove\", onMove);\n window.addEventListener(\"mouseup\", onUp);\n window.addEventListener(\"touchmove\", onMove);\n window.addEventListener(\"touchend\", onUp);\n\n return () => {\n window.removeEventListener(\"mousemove\", onMove);\n window.removeEventListener(\"mouseup\", onUp);\n window.removeEventListener(\"touchmove\", onMove);\n window.removeEventListener(\"touchend\", onUp);\n };\n }, []);\n\n const rotation = valueToAngle(value);\n\n // Generate tick marks\n const ticks = Array.from({ length: STEPS + 1 }, (_, i) => {\n const angle = valueToAngle(i);\n const rad = ((angle - 90) * Math.PI) / 180;\n const outer = KNOB_SIZE / 2 + 5;\n const inner = KNOB_SIZE / 2 + 2;\n return {\n x1: Math.cos(rad) * inner,\n y1: Math.sin(rad) * inner,\n x2: Math.cos(rad) * outer,\n y2: Math.sin(rad) * outer,\n major: i === 0 || i === 5 || i === 10,\n };\n });\n\n const svgSize = KNOB_SIZE + 14;\n const center = svgSize / 2;\n\n return (\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: 1,\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n }}\n >\n <svg\n ref={knobRef}\n width={svgSize}\n height={svgSize}\n viewBox={`0 0 ${svgSize} ${svgSize}`}\n style={{ cursor: \"grab\", touchAction: \"none\" }}\n onMouseDown={handleStart}\n onTouchStart={handleStart}\n >\n {/* Tick marks */}\n {ticks.map((t, i) => (\n <line\n key={i}\n x1={center + t.x1}\n y1={center + t.y1}\n x2={center + t.x2}\n y2={center + t.y2}\n stroke={i <= value ? \"#111\" : \"#ccc\"}\n strokeWidth={t.major ? 1.5 : 0.75}\n strokeLinecap=\"round\"\n />\n ))}\n\n {/* Knob body */}\n <circle\n cx={center}\n cy={center}\n r={KNOB_SIZE / 2}\n fill=\"#fff\"\n stroke=\"#ddd\"\n strokeWidth={1}\n />\n\n {/* Inner subtle ring */}\n <circle\n cx={center}\n cy={center}\n r={KNOB_SIZE / 2 - 3}\n fill=\"none\"\n stroke=\"#eee\"\n strokeWidth={0.5}\n />\n\n {/* Indicator line */}\n <line\n x1={center}\n y1={center}\n x2={\n center +\n Math.cos(((rotation - 90) * Math.PI) / 180) * (KNOB_SIZE / 2 - 4)\n }\n y2={\n center +\n Math.sin(((rotation - 90) * Math.PI) / 180) * (KNOB_SIZE / 2 - 4)\n }\n stroke=\"#111\"\n strokeWidth={1.5}\n strokeLinecap=\"round\"\n />\n\n {/* Center dot */}\n <circle cx={center} cy={center} r={1.5} fill=\"#111\" />\n </svg>\n\n <span\n style={{\n fontSize: \"0.55rem\",\n fontWeight: 500,\n color: \"#999\",\n textTransform: \"uppercase\",\n letterSpacing: \"0.06em\",\n lineHeight: 1,\n }}\n >\n {label}\n </span>\n </div>\n );\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Liquid Design Framework — global styles.
3
+ */
4
+
5
+ @import "./transitions.css";
6
+ @import "./typography.css";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Liquid Framework — Global easing and transitions.
3
+ *
4
+ * --fw-ease is the single easing used for ALL framework transitions.
5
+ * Any change in Liquid elements (noise, size, appear, collapse) uses this curve.
6
+ */
7
+
8
+ :root {
9
+ /* Global easing — used by every transition in the framework */
10
+ --fw-ease: cubic-bezier(0.16, 1, 0.3, 1);
11
+
12
+ /* Aliases (for semantic use; all resolve to --fw-ease for framework transitions) */
13
+ --fw-ease-out: var(--fw-ease);
14
+
15
+ /* Durations */
16
+ --fw-duration-fast: 0.15s;
17
+ --fw-duration-normal: 0.25s;
18
+ --fw-duration-slow: 0.35s;
19
+
20
+ /* Standard transitions — all use --fw-ease */
21
+ --fw-transition-appear: opacity var(--fw-duration-normal) var(--fw-ease),
22
+ max-width var(--fw-duration-normal) var(--fw-ease),
23
+ max-height var(--fw-duration-normal) var(--fw-ease),
24
+ grid-template-rows var(--fw-duration-normal) var(--fw-ease);
25
+ --fw-transition-size: width var(--fw-duration-normal) var(--fw-ease),
26
+ height var(--fw-duration-normal) var(--fw-ease),
27
+ font-size var(--fw-duration-normal) var(--fw-ease);
28
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Liquid Framework — Global typography.
3
+ * Base value variables; semantic sizes are resolved in JS via typography.ts.
4
+ */
5
+
6
+ :root {
7
+ /* Base font sizes (px) — fixed scale for the framework. */
8
+ --fw-font-base-8: 8px;
9
+ --fw-font-base-10: 10px;
10
+ --fw-font-base-12: 12px;
11
+ --fw-font-base-14: 14px;
12
+ --fw-font-base-16: 16px;
13
+ --fw-font-base-18: 18px;
14
+ --fw-font-base-20: 20px;
15
+ --fw-font-base-24: 24px;
16
+ --fw-font-base-28: 28px;
17
+
18
+ /* Semantic size vars — updated by FrameworkSizeSync when size knob changes. */
19
+ --fw-font-xs: 10px;
20
+ --fw-font-s: 12px;
21
+ --fw-font-m: 14px;
22
+ --fw-font-l: 16px;
23
+ --fw-font-xl: 18px;
24
+
25
+ --fw-avatar-size: 28px;
26
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@liquid-noise/core",
3
+ "version": "0.1.0",
4
+ "description": "Liquid Design Framework — noise and size controls for adaptive UI",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./styles/*": "./dist/styles/*"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "MANIFEST.md",
20
+ "CONCEPT.md"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "peerDependencies": {
26
+ "react": "^18.0.0 || ^19.0.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "^19.0.0",
34
+ "tsup": "^8.3.5",
35
+ "typescript": "~5.6.2"
36
+ }
37
+ }