@particle-academy/react-fancy 5.20.0 → 5.21.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.
@@ -0,0 +1,121 @@
1
+ import { useId as useId$1, useRef, useState, useCallback, useEffect } from 'react';
2
+
3
+ // src/hooks/use-id.ts
4
+ function useId(prefix) {
5
+ const id = useId$1();
6
+ return prefix ? `${prefix}-${id}` : id;
7
+ }
8
+ function useNodeRegistry() {
9
+ const rectsRef = useRef(/* @__PURE__ */ new Map());
10
+ const [version, setVersion] = useState(0);
11
+ const registerNode = useCallback((id, rect) => {
12
+ rectsRef.current.set(id, rect);
13
+ setVersion((v) => v + 1);
14
+ }, []);
15
+ const unregisterNode = useCallback((id) => {
16
+ rectsRef.current.delete(id);
17
+ setVersion((v) => v + 1);
18
+ }, []);
19
+ return { registerNode, unregisterNode, nodeRects: rectsRef.current, version };
20
+ }
21
+
22
+ // src/theme.ts
23
+ var THEME_STORAGE_KEY = "fancy-ui.theme";
24
+ var DARK_QUERY = "(prefers-color-scheme: dark)";
25
+ var listeners = /* @__PURE__ */ new Set();
26
+ var storageKey = THEME_STORAGE_KEY;
27
+ var stopWatchingSystem = null;
28
+ function canUseDom() {
29
+ return typeof window !== "undefined" && typeof document !== "undefined";
30
+ }
31
+ function getThemePreference() {
32
+ if (!canUseDom()) return "system";
33
+ try {
34
+ const saved = window.localStorage.getItem(storageKey);
35
+ return saved === "light" || saved === "dark" ? saved : "system";
36
+ } catch {
37
+ return "system";
38
+ }
39
+ }
40
+ function systemTheme() {
41
+ if (!canUseDom() || typeof window.matchMedia !== "function") return "light";
42
+ return window.matchMedia(DARK_QUERY).matches ? "dark" : "light";
43
+ }
44
+ function resolveTheme(preference = getThemePreference()) {
45
+ return preference === "system" ? systemTheme() : preference;
46
+ }
47
+ function applyToDocument(resolved) {
48
+ if (!canUseDom()) return;
49
+ const root = document.documentElement;
50
+ root.classList.toggle("dark", resolved === "dark");
51
+ root.setAttribute("data-theme", resolved);
52
+ }
53
+ function publish() {
54
+ const preference = getThemePreference();
55
+ const resolved = resolveTheme(preference);
56
+ applyToDocument(resolved);
57
+ listeners.forEach((listener) => listener(resolved, preference));
58
+ }
59
+ function setThemePreference(preference) {
60
+ if (canUseDom()) {
61
+ try {
62
+ if (preference === "system") {
63
+ window.localStorage.removeItem(storageKey);
64
+ } else {
65
+ window.localStorage.setItem(storageKey, preference);
66
+ }
67
+ } catch {
68
+ }
69
+ }
70
+ publish();
71
+ }
72
+ function subscribeTheme(listener) {
73
+ listeners.add(listener);
74
+ return () => {
75
+ listeners.delete(listener);
76
+ };
77
+ }
78
+ function initTheme(options = {}) {
79
+ if (options.storageKey) storageKey = options.storageKey;
80
+ if (!canUseDom()) return () => {
81
+ };
82
+ applyToDocument(resolveTheme());
83
+ if (!stopWatchingSystem && typeof window.matchMedia === "function") {
84
+ const media = window.matchMedia(DARK_QUERY);
85
+ const onSystemChange = () => {
86
+ if (getThemePreference() === "system") publish();
87
+ };
88
+ if (typeof media.addEventListener === "function") {
89
+ media.addEventListener("change", onSystemChange);
90
+ stopWatchingSystem = () => media.removeEventListener("change", onSystemChange);
91
+ } else {
92
+ media.addListener(onSystemChange);
93
+ stopWatchingSystem = () => media.removeListener(onSystemChange);
94
+ }
95
+ }
96
+ return () => {
97
+ stopWatchingSystem?.();
98
+ stopWatchingSystem = null;
99
+ };
100
+ }
101
+ function useTheme() {
102
+ const [state, setState] = useState({
103
+ preference: "system",
104
+ resolved: "light"
105
+ });
106
+ useEffect(() => {
107
+ const sync = () => {
108
+ setState({ preference: getThemePreference(), resolved: resolveTheme() });
109
+ };
110
+ sync();
111
+ return subscribeTheme(sync);
112
+ }, []);
113
+ const setPreference = useCallback((preference) => {
114
+ setThemePreference(preference);
115
+ }, []);
116
+ return { ...state, setPreference };
117
+ }
118
+
119
+ export { THEME_STORAGE_KEY, getThemePreference, initTheme, resolveTheme, setThemePreference, subscribeTheme, useId, useNodeRegistry, useTheme };
120
+ //# sourceMappingURL=chunk-FWCREAIT.js.map
121
+ //# sourceMappingURL=chunk-FWCREAIT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/use-id.ts","../src/hooks/use-node-registry.ts","../src/theme.ts","../src/hooks/use-theme.ts"],"names":["useReactId","useState","useCallback"],"mappings":"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","file":"chunk-FWCREAIT.js","sourcesContent":["import { useId as useReactId } from \"react\";\n\nexport function useId(prefix?: string): string {\n const id = useReactId();\n return prefix ? `${prefix}-${id}` : id;\n}\n","import { useCallback, useRef, useState } from \"react\";\n\nexport interface NodeRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface UseNodeRegistryReturn {\n registerNode: (id: string, rect: NodeRect) => void;\n unregisterNode: (id: string) => void;\n nodeRects: Map<string, NodeRect>;\n version: number;\n}\n\nexport function useNodeRegistry(): UseNodeRegistryReturn {\n const rectsRef = useRef(new Map<string, NodeRect>());\n const [version, setVersion] = useState(0);\n\n const registerNode = useCallback((id: string, rect: NodeRect) => {\n rectsRef.current.set(id, rect);\n setVersion((v) => v + 1);\n }, []);\n\n const unregisterNode = useCallback((id: string) => {\n rectsRef.current.delete(id);\n setVersion((v) => v + 1);\n }, []);\n\n return { registerNode, unregisterNode, nodeRects: rectsRef.current, version };\n}\n","/**\n * Light / dark / **system** theming.\n *\n * `styles.css` in this package defines the dark contract — it redefines the\n * whole `--color-secondary-*` scale under `:where(.dark)` so that \"900\" always\n * means strongest foreground and \"50\" always means faintest surface. This module\n * is the other half of that contract: the thing that decides when `.dark` is on.\n *\n * It lives here rather than in each app because every consumer was hand-rolling\n * it, and every hand-rolled copy had the same two holes:\n *\n * - **No live listener.** The OS preference was read once at boot, so changing\n * the system theme with the page open did nothing. That is a default, not a\n * \"system\" mode.\n * - **No way back.** Choosing light or dark wrote to storage forever, so\n * \"follow my system\" was unreachable after a single click.\n *\n * Both are fixed by treating **\"system\" as the absence of a stored choice**\n * rather than as a third stored value.\n *\n * Every export is safe to call during SSR, where it is a no-op that resolves to\n * `\"light\"`. Pair it with `useTheme`, which deliberately renders `system/light`\n * on the server and syncs in an effect — this project has paid for hydration\n * mismatches caused by reading the real theme during render.\n */\n\nexport type ThemePreference = \"light\" | \"dark\" | \"system\";\nexport type ResolvedTheme = \"light\" | \"dark\";\n\n/**\n * Deliberately the key the showcase already used, so adopting this module keeps\n * a returning visitor's saved choice instead of silently resetting it.\n */\nexport const THEME_STORAGE_KEY = \"fancy-ui.theme\";\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\ntype ThemeListener = (resolved: ResolvedTheme, preference: ThemePreference) => void;\n\nconst listeners = new Set<ThemeListener>();\nlet storageKey = THEME_STORAGE_KEY;\nlet stopWatchingSystem: (() => void) | null = null;\n\nfunction canUseDom(): boolean {\n return typeof window !== \"undefined\" && typeof document !== \"undefined\";\n}\n\n/** The stored choice, or `\"system\"` when there isn't one. */\nexport function getThemePreference(): ThemePreference {\n if (!canUseDom()) return \"system\";\n try {\n const saved = window.localStorage.getItem(storageKey);\n return saved === \"light\" || saved === \"dark\" ? saved : \"system\";\n } catch {\n // Private browsing and blocked storage both throw here. A theme is not\n // worth taking the page down over.\n return \"system\";\n }\n}\n\nfunction systemTheme(): ResolvedTheme {\n if (!canUseDom() || typeof window.matchMedia !== \"function\") return \"light\";\n return window.matchMedia(DARK_QUERY).matches ? \"dark\" : \"light\";\n}\n\n/** What is actually on screen, as opposed to what the user asked for. */\nexport function resolveTheme(preference: ThemePreference = getThemePreference()): ResolvedTheme {\n return preference === \"system\" ? systemTheme() : preference;\n}\n\nfunction applyToDocument(resolved: ResolvedTheme): void {\n if (!canUseDom()) return;\n const root = document.documentElement;\n // The class is what this package's CSS keys off; the attribute is for\n // consumers whose own styles key off `[data-theme]`.\n root.classList.toggle(\"dark\", resolved === \"dark\");\n root.setAttribute(\"data-theme\", resolved);\n}\n\nfunction publish(): void {\n const preference = getThemePreference();\n const resolved = resolveTheme(preference);\n applyToDocument(resolved);\n listeners.forEach((listener) => listener(resolved, preference));\n}\n\n/**\n * Choose a theme. `\"system\"` REMOVES the stored value rather than storing the\n * string — an absent choice is what lets the OS keep speaking for the user.\n */\nexport function setThemePreference(preference: ThemePreference): void {\n if (canUseDom()) {\n try {\n if (preference === \"system\") {\n window.localStorage.removeItem(storageKey);\n } else {\n window.localStorage.setItem(storageKey, preference);\n }\n } catch {\n // As above: still apply it for this page even if it cannot persist.\n }\n }\n publish();\n}\n\n/** Observe theme changes. Returns an unsubscribe. */\nexport function subscribeTheme(listener: ThemeListener): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/**\n * Apply the current theme and start following the OS. Call once, as early as\n * possible — before first paint if you want to avoid a flash of the wrong\n * theme. Returns a disposer; calling it again is harmless.\n */\nexport function initTheme(options: { storageKey?: string } = {}): () => void {\n if (options.storageKey) storageKey = options.storageKey;\n if (!canUseDom()) return () => {};\n\n applyToDocument(resolveTheme());\n\n if (!stopWatchingSystem && typeof window.matchMedia === \"function\") {\n const media = window.matchMedia(DARK_QUERY);\n const onSystemChange = (): void => {\n // Only \"system\" follows the OS. An explicit choice has to survive\n // the user switching their OS theme, or it isn't a choice.\n if (getThemePreference() === \"system\") publish();\n };\n\n if (typeof media.addEventListener === \"function\") {\n media.addEventListener(\"change\", onSystemChange);\n stopWatchingSystem = () => media.removeEventListener(\"change\", onSystemChange);\n } else {\n // Safari < 14 and friends.\n media.addListener(onSystemChange);\n stopWatchingSystem = () => media.removeListener(onSystemChange);\n }\n }\n\n return () => {\n stopWatchingSystem?.();\n stopWatchingSystem = null;\n };\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport {\n getThemePreference,\n resolveTheme,\n setThemePreference,\n subscribeTheme,\n type ResolvedTheme,\n type ThemePreference,\n} from \"../theme\";\n\nexport type UseThemeResult = {\n /** What the user asked for, including `\"system\"`. */\n preference: ThemePreference;\n /** What is actually on screen. */\n resolved: ResolvedTheme;\n setPreference: (preference: ThemePreference) => void;\n};\n\n/**\n * Read and set the theme from a component, re-rendering when it changes —\n * including when the OS theme changes underneath a `\"system\"` preference.\n *\n * The initial state is deliberately `system` / `light` rather than the real\n * theme: the server has neither storage nor an OS preference, so reading the\n * truth during render is exactly the hydration mismatch this project has\n * already chased down more than once. The effect syncs immediately on mount.\n */\nexport function useTheme(): UseThemeResult {\n const [state, setState] = useState<{ preference: ThemePreference; resolved: ResolvedTheme }>({\n preference: \"system\",\n resolved: \"light\",\n });\n\n useEffect(() => {\n const sync = (): void => {\n setState({ preference: getThemePreference(), resolved: resolveTheme() });\n };\n sync();\n return subscribeTheme(sync);\n }, []);\n\n const setPreference = useCallback((preference: ThemePreference): void => {\n setThemePreference(preference);\n }, []);\n\n return { ...state, setPreference };\n}\n"]}
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/hooks/use-id.ts
6
+ function useId(prefix) {
7
+ const id = react.useId();
8
+ return prefix ? `${prefix}-${id}` : id;
9
+ }
10
+ function useNodeRegistry() {
11
+ const rectsRef = react.useRef(/* @__PURE__ */ new Map());
12
+ const [version, setVersion] = react.useState(0);
13
+ const registerNode = react.useCallback((id, rect) => {
14
+ rectsRef.current.set(id, rect);
15
+ setVersion((v) => v + 1);
16
+ }, []);
17
+ const unregisterNode = react.useCallback((id) => {
18
+ rectsRef.current.delete(id);
19
+ setVersion((v) => v + 1);
20
+ }, []);
21
+ return { registerNode, unregisterNode, nodeRects: rectsRef.current, version };
22
+ }
23
+
24
+ // src/theme.ts
25
+ var THEME_STORAGE_KEY = "fancy-ui.theme";
26
+ var DARK_QUERY = "(prefers-color-scheme: dark)";
27
+ var listeners = /* @__PURE__ */ new Set();
28
+ var storageKey = THEME_STORAGE_KEY;
29
+ var stopWatchingSystem = null;
30
+ function canUseDom() {
31
+ return typeof window !== "undefined" && typeof document !== "undefined";
32
+ }
33
+ function getThemePreference() {
34
+ if (!canUseDom()) return "system";
35
+ try {
36
+ const saved = window.localStorage.getItem(storageKey);
37
+ return saved === "light" || saved === "dark" ? saved : "system";
38
+ } catch {
39
+ return "system";
40
+ }
41
+ }
42
+ function systemTheme() {
43
+ if (!canUseDom() || typeof window.matchMedia !== "function") return "light";
44
+ return window.matchMedia(DARK_QUERY).matches ? "dark" : "light";
45
+ }
46
+ function resolveTheme(preference = getThemePreference()) {
47
+ return preference === "system" ? systemTheme() : preference;
48
+ }
49
+ function applyToDocument(resolved) {
50
+ if (!canUseDom()) return;
51
+ const root = document.documentElement;
52
+ root.classList.toggle("dark", resolved === "dark");
53
+ root.setAttribute("data-theme", resolved);
54
+ }
55
+ function publish() {
56
+ const preference = getThemePreference();
57
+ const resolved = resolveTheme(preference);
58
+ applyToDocument(resolved);
59
+ listeners.forEach((listener) => listener(resolved, preference));
60
+ }
61
+ function setThemePreference(preference) {
62
+ if (canUseDom()) {
63
+ try {
64
+ if (preference === "system") {
65
+ window.localStorage.removeItem(storageKey);
66
+ } else {
67
+ window.localStorage.setItem(storageKey, preference);
68
+ }
69
+ } catch {
70
+ }
71
+ }
72
+ publish();
73
+ }
74
+ function subscribeTheme(listener) {
75
+ listeners.add(listener);
76
+ return () => {
77
+ listeners.delete(listener);
78
+ };
79
+ }
80
+ function initTheme(options = {}) {
81
+ if (options.storageKey) storageKey = options.storageKey;
82
+ if (!canUseDom()) return () => {
83
+ };
84
+ applyToDocument(resolveTheme());
85
+ if (!stopWatchingSystem && typeof window.matchMedia === "function") {
86
+ const media = window.matchMedia(DARK_QUERY);
87
+ const onSystemChange = () => {
88
+ if (getThemePreference() === "system") publish();
89
+ };
90
+ if (typeof media.addEventListener === "function") {
91
+ media.addEventListener("change", onSystemChange);
92
+ stopWatchingSystem = () => media.removeEventListener("change", onSystemChange);
93
+ } else {
94
+ media.addListener(onSystemChange);
95
+ stopWatchingSystem = () => media.removeListener(onSystemChange);
96
+ }
97
+ }
98
+ return () => {
99
+ stopWatchingSystem?.();
100
+ stopWatchingSystem = null;
101
+ };
102
+ }
103
+ function useTheme() {
104
+ const [state, setState] = react.useState({
105
+ preference: "system",
106
+ resolved: "light"
107
+ });
108
+ react.useEffect(() => {
109
+ const sync = () => {
110
+ setState({ preference: getThemePreference(), resolved: resolveTheme() });
111
+ };
112
+ sync();
113
+ return subscribeTheme(sync);
114
+ }, []);
115
+ const setPreference = react.useCallback((preference) => {
116
+ setThemePreference(preference);
117
+ }, []);
118
+ return { ...state, setPreference };
119
+ }
120
+
121
+ exports.THEME_STORAGE_KEY = THEME_STORAGE_KEY;
122
+ exports.getThemePreference = getThemePreference;
123
+ exports.initTheme = initTheme;
124
+ exports.resolveTheme = resolveTheme;
125
+ exports.setThemePreference = setThemePreference;
126
+ exports.subscribeTheme = subscribeTheme;
127
+ exports.useId = useId;
128
+ exports.useNodeRegistry = useNodeRegistry;
129
+ exports.useTheme = useTheme;
130
+ //# sourceMappingURL=chunk-HPNIEYSJ.cjs.map
131
+ //# sourceMappingURL=chunk-HPNIEYSJ.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/use-id.ts","../src/hooks/use-node-registry.ts","../src/theme.ts","../src/hooks/use-theme.ts"],"names":["useReactId","useRef","useState","useCallback","useEffect"],"mappings":"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","file":"chunk-HPNIEYSJ.cjs","sourcesContent":["import { useId as useReactId } from \"react\";\n\nexport function useId(prefix?: string): string {\n const id = useReactId();\n return prefix ? `${prefix}-${id}` : id;\n}\n","import { useCallback, useRef, useState } from \"react\";\n\nexport interface NodeRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface UseNodeRegistryReturn {\n registerNode: (id: string, rect: NodeRect) => void;\n unregisterNode: (id: string) => void;\n nodeRects: Map<string, NodeRect>;\n version: number;\n}\n\nexport function useNodeRegistry(): UseNodeRegistryReturn {\n const rectsRef = useRef(new Map<string, NodeRect>());\n const [version, setVersion] = useState(0);\n\n const registerNode = useCallback((id: string, rect: NodeRect) => {\n rectsRef.current.set(id, rect);\n setVersion((v) => v + 1);\n }, []);\n\n const unregisterNode = useCallback((id: string) => {\n rectsRef.current.delete(id);\n setVersion((v) => v + 1);\n }, []);\n\n return { registerNode, unregisterNode, nodeRects: rectsRef.current, version };\n}\n","/**\n * Light / dark / **system** theming.\n *\n * `styles.css` in this package defines the dark contract — it redefines the\n * whole `--color-secondary-*` scale under `:where(.dark)` so that \"900\" always\n * means strongest foreground and \"50\" always means faintest surface. This module\n * is the other half of that contract: the thing that decides when `.dark` is on.\n *\n * It lives here rather than in each app because every consumer was hand-rolling\n * it, and every hand-rolled copy had the same two holes:\n *\n * - **No live listener.** The OS preference was read once at boot, so changing\n * the system theme with the page open did nothing. That is a default, not a\n * \"system\" mode.\n * - **No way back.** Choosing light or dark wrote to storage forever, so\n * \"follow my system\" was unreachable after a single click.\n *\n * Both are fixed by treating **\"system\" as the absence of a stored choice**\n * rather than as a third stored value.\n *\n * Every export is safe to call during SSR, where it is a no-op that resolves to\n * `\"light\"`. Pair it with `useTheme`, which deliberately renders `system/light`\n * on the server and syncs in an effect — this project has paid for hydration\n * mismatches caused by reading the real theme during render.\n */\n\nexport type ThemePreference = \"light\" | \"dark\" | \"system\";\nexport type ResolvedTheme = \"light\" | \"dark\";\n\n/**\n * Deliberately the key the showcase already used, so adopting this module keeps\n * a returning visitor's saved choice instead of silently resetting it.\n */\nexport const THEME_STORAGE_KEY = \"fancy-ui.theme\";\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\ntype ThemeListener = (resolved: ResolvedTheme, preference: ThemePreference) => void;\n\nconst listeners = new Set<ThemeListener>();\nlet storageKey = THEME_STORAGE_KEY;\nlet stopWatchingSystem: (() => void) | null = null;\n\nfunction canUseDom(): boolean {\n return typeof window !== \"undefined\" && typeof document !== \"undefined\";\n}\n\n/** The stored choice, or `\"system\"` when there isn't one. */\nexport function getThemePreference(): ThemePreference {\n if (!canUseDom()) return \"system\";\n try {\n const saved = window.localStorage.getItem(storageKey);\n return saved === \"light\" || saved === \"dark\" ? saved : \"system\";\n } catch {\n // Private browsing and blocked storage both throw here. A theme is not\n // worth taking the page down over.\n return \"system\";\n }\n}\n\nfunction systemTheme(): ResolvedTheme {\n if (!canUseDom() || typeof window.matchMedia !== \"function\") return \"light\";\n return window.matchMedia(DARK_QUERY).matches ? \"dark\" : \"light\";\n}\n\n/** What is actually on screen, as opposed to what the user asked for. */\nexport function resolveTheme(preference: ThemePreference = getThemePreference()): ResolvedTheme {\n return preference === \"system\" ? systemTheme() : preference;\n}\n\nfunction applyToDocument(resolved: ResolvedTheme): void {\n if (!canUseDom()) return;\n const root = document.documentElement;\n // The class is what this package's CSS keys off; the attribute is for\n // consumers whose own styles key off `[data-theme]`.\n root.classList.toggle(\"dark\", resolved === \"dark\");\n root.setAttribute(\"data-theme\", resolved);\n}\n\nfunction publish(): void {\n const preference = getThemePreference();\n const resolved = resolveTheme(preference);\n applyToDocument(resolved);\n listeners.forEach((listener) => listener(resolved, preference));\n}\n\n/**\n * Choose a theme. `\"system\"` REMOVES the stored value rather than storing the\n * string — an absent choice is what lets the OS keep speaking for the user.\n */\nexport function setThemePreference(preference: ThemePreference): void {\n if (canUseDom()) {\n try {\n if (preference === \"system\") {\n window.localStorage.removeItem(storageKey);\n } else {\n window.localStorage.setItem(storageKey, preference);\n }\n } catch {\n // As above: still apply it for this page even if it cannot persist.\n }\n }\n publish();\n}\n\n/** Observe theme changes. Returns an unsubscribe. */\nexport function subscribeTheme(listener: ThemeListener): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/**\n * Apply the current theme and start following the OS. Call once, as early as\n * possible — before first paint if you want to avoid a flash of the wrong\n * theme. Returns a disposer; calling it again is harmless.\n */\nexport function initTheme(options: { storageKey?: string } = {}): () => void {\n if (options.storageKey) storageKey = options.storageKey;\n if (!canUseDom()) return () => {};\n\n applyToDocument(resolveTheme());\n\n if (!stopWatchingSystem && typeof window.matchMedia === \"function\") {\n const media = window.matchMedia(DARK_QUERY);\n const onSystemChange = (): void => {\n // Only \"system\" follows the OS. An explicit choice has to survive\n // the user switching their OS theme, or it isn't a choice.\n if (getThemePreference() === \"system\") publish();\n };\n\n if (typeof media.addEventListener === \"function\") {\n media.addEventListener(\"change\", onSystemChange);\n stopWatchingSystem = () => media.removeEventListener(\"change\", onSystemChange);\n } else {\n // Safari < 14 and friends.\n media.addListener(onSystemChange);\n stopWatchingSystem = () => media.removeListener(onSystemChange);\n }\n }\n\n return () => {\n stopWatchingSystem?.();\n stopWatchingSystem = null;\n };\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport {\n getThemePreference,\n resolveTheme,\n setThemePreference,\n subscribeTheme,\n type ResolvedTheme,\n type ThemePreference,\n} from \"../theme\";\n\nexport type UseThemeResult = {\n /** What the user asked for, including `\"system\"`. */\n preference: ThemePreference;\n /** What is actually on screen. */\n resolved: ResolvedTheme;\n setPreference: (preference: ThemePreference) => void;\n};\n\n/**\n * Read and set the theme from a component, re-rendering when it changes —\n * including when the OS theme changes underneath a `\"system\"` preference.\n *\n * The initial state is deliberately `system` / `light` rather than the real\n * theme: the server has neither storage nor an OS preference, so reading the\n * truth during render is exactly the hydration mismatch this project has\n * already chased down more than once. The effect syncs immediately on mount.\n */\nexport function useTheme(): UseThemeResult {\n const [state, setState] = useState<{ preference: ThemePreference; resolved: ResolvedTheme }>({\n preference: \"system\",\n resolved: \"light\",\n });\n\n useEffect(() => {\n const sync = (): void => {\n setState({ preference: getThemePreference(), resolved: resolveTheme() });\n };\n sync();\n return subscribeTheme(sync);\n }, []);\n\n const setPreference = useCallback((preference: ThemePreference): void => {\n setThemePreference(preference);\n }, []);\n\n return { ...state, setPreference };\n}\n"]}
package/dist/editor.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkWT72KW5M_cjs = require('./chunk-WT72KW5M.cjs');
4
- require('./chunk-G34OBTU3.cjs');
4
+ require('./chunk-HPNIEYSJ.cjs');
5
5
  require('./chunk-T6YELZPJ.cjs');
6
6
  require('./chunk-RNHIDZLE.cjs');
7
7
  require('./chunk-TXTRTTG7.cjs');
package/dist/editor.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Editor, useEditor } from './chunk-RID7CYGD.js';
2
- import './chunk-RNPFSVFN.js';
2
+ import './chunk-FWCREAIT.js';
3
3
  import './chunk-CPY7NHXM.js';
4
4
  import './chunk-FVV76TRF.js';
5
5
  import './chunk-5U2DZ7AV.js';
package/dist/index.cjs CHANGED
@@ -71,7 +71,7 @@ var chunk5HOQ2PHR_cjs = require('./chunk-5HOQ2PHR.cjs');
71
71
  var chunkF6NEPU67_cjs = require('./chunk-F6NEPU67.cjs');
72
72
  var chunkORFHF6PT_cjs = require('./chunk-ORFHF6PT.cjs');
73
73
  var chunkWT72KW5M_cjs = require('./chunk-WT72KW5M.cjs');
74
- var chunkG34OBTU3_cjs = require('./chunk-G34OBTU3.cjs');
74
+ var chunkHPNIEYSJ_cjs = require('./chunk-HPNIEYSJ.cjs');
75
75
  var chunkT6YELZPJ_cjs = require('./chunk-T6YELZPJ.cjs');
76
76
  var chunkMDUSUJWY_cjs = require('./chunk-MDUSUJWY.cjs');
77
77
  var chunk6FBFE66M_cjs = require('./chunk-6FBFE66M.cjs');
@@ -512,13 +512,41 @@ Object.defineProperty(exports, "useEditor", {
512
512
  enumerable: true,
513
513
  get: function () { return chunkWT72KW5M_cjs.useEditor; }
514
514
  });
515
+ Object.defineProperty(exports, "THEME_STORAGE_KEY", {
516
+ enumerable: true,
517
+ get: function () { return chunkHPNIEYSJ_cjs.THEME_STORAGE_KEY; }
518
+ });
519
+ Object.defineProperty(exports, "getThemePreference", {
520
+ enumerable: true,
521
+ get: function () { return chunkHPNIEYSJ_cjs.getThemePreference; }
522
+ });
523
+ Object.defineProperty(exports, "initTheme", {
524
+ enumerable: true,
525
+ get: function () { return chunkHPNIEYSJ_cjs.initTheme; }
526
+ });
527
+ Object.defineProperty(exports, "resolveTheme", {
528
+ enumerable: true,
529
+ get: function () { return chunkHPNIEYSJ_cjs.resolveTheme; }
530
+ });
531
+ Object.defineProperty(exports, "setThemePreference", {
532
+ enumerable: true,
533
+ get: function () { return chunkHPNIEYSJ_cjs.setThemePreference; }
534
+ });
535
+ Object.defineProperty(exports, "subscribeTheme", {
536
+ enumerable: true,
537
+ get: function () { return chunkHPNIEYSJ_cjs.subscribeTheme; }
538
+ });
515
539
  Object.defineProperty(exports, "useId", {
516
540
  enumerable: true,
517
- get: function () { return chunkG34OBTU3_cjs.useId; }
541
+ get: function () { return chunkHPNIEYSJ_cjs.useId; }
518
542
  });
519
543
  Object.defineProperty(exports, "useNodeRegistry", {
520
544
  enumerable: true,
521
- get: function () { return chunkG34OBTU3_cjs.useNodeRegistry; }
545
+ get: function () { return chunkHPNIEYSJ_cjs.useNodeRegistry; }
546
+ });
547
+ Object.defineProperty(exports, "useTheme", {
548
+ enumerable: true,
549
+ get: function () { return chunkHPNIEYSJ_cjs.useTheme; }
522
550
  });
523
551
  Object.defineProperty(exports, "usePanZoom", {
524
552
  enumerable: true,
package/dist/index.d.cts CHANGED
@@ -200,4 +200,75 @@ interface UseNodeRegistryReturn {
200
200
  }
201
201
  declare function useNodeRegistry(): UseNodeRegistryReturn;
202
202
 
203
- export { type NodeRect, Placement, cn, sanitizeHref, sanitizeHtml, useAnimation, useControllableState, useEscapeKey, useFloatingPosition, useFocusTrap, useId, useNodeRegistry, useOutsideClick, usePanZoom };
203
+ /**
204
+ * Light / dark / **system** theming.
205
+ *
206
+ * `styles.css` in this package defines the dark contract — it redefines the
207
+ * whole `--color-secondary-*` scale under `:where(.dark)` so that "900" always
208
+ * means strongest foreground and "50" always means faintest surface. This module
209
+ * is the other half of that contract: the thing that decides when `.dark` is on.
210
+ *
211
+ * It lives here rather than in each app because every consumer was hand-rolling
212
+ * it, and every hand-rolled copy had the same two holes:
213
+ *
214
+ * - **No live listener.** The OS preference was read once at boot, so changing
215
+ * the system theme with the page open did nothing. That is a default, not a
216
+ * "system" mode.
217
+ * - **No way back.** Choosing light or dark wrote to storage forever, so
218
+ * "follow my system" was unreachable after a single click.
219
+ *
220
+ * Both are fixed by treating **"system" as the absence of a stored choice**
221
+ * rather than as a third stored value.
222
+ *
223
+ * Every export is safe to call during SSR, where it is a no-op that resolves to
224
+ * `"light"`. Pair it with `useTheme`, which deliberately renders `system/light`
225
+ * on the server and syncs in an effect — this project has paid for hydration
226
+ * mismatches caused by reading the real theme during render.
227
+ */
228
+ type ThemePreference = "light" | "dark" | "system";
229
+ type ResolvedTheme = "light" | "dark";
230
+ /**
231
+ * Deliberately the key the showcase already used, so adopting this module keeps
232
+ * a returning visitor's saved choice instead of silently resetting it.
233
+ */
234
+ declare const THEME_STORAGE_KEY = "fancy-ui.theme";
235
+ type ThemeListener = (resolved: ResolvedTheme, preference: ThemePreference) => void;
236
+ /** The stored choice, or `"system"` when there isn't one. */
237
+ declare function getThemePreference(): ThemePreference;
238
+ /** What is actually on screen, as opposed to what the user asked for. */
239
+ declare function resolveTheme(preference?: ThemePreference): ResolvedTheme;
240
+ /**
241
+ * Choose a theme. `"system"` REMOVES the stored value rather than storing the
242
+ * string — an absent choice is what lets the OS keep speaking for the user.
243
+ */
244
+ declare function setThemePreference(preference: ThemePreference): void;
245
+ /** Observe theme changes. Returns an unsubscribe. */
246
+ declare function subscribeTheme(listener: ThemeListener): () => void;
247
+ /**
248
+ * Apply the current theme and start following the OS. Call once, as early as
249
+ * possible — before first paint if you want to avoid a flash of the wrong
250
+ * theme. Returns a disposer; calling it again is harmless.
251
+ */
252
+ declare function initTheme(options?: {
253
+ storageKey?: string;
254
+ }): () => void;
255
+
256
+ type UseThemeResult = {
257
+ /** What the user asked for, including `"system"`. */
258
+ preference: ThemePreference;
259
+ /** What is actually on screen. */
260
+ resolved: ResolvedTheme;
261
+ setPreference: (preference: ThemePreference) => void;
262
+ };
263
+ /**
264
+ * Read and set the theme from a component, re-rendering when it changes —
265
+ * including when the OS theme changes underneath a `"system"` preference.
266
+ *
267
+ * The initial state is deliberately `system` / `light` rather than the real
268
+ * theme: the server has neither storage nor an OS preference, so reading the
269
+ * truth during render is exactly the hydration mismatch this project has
270
+ * already chased down more than once. The effect syncs immediately on mount.
271
+ */
272
+ declare function useTheme(): UseThemeResult;
273
+
274
+ export { type NodeRect, Placement, type ResolvedTheme, THEME_STORAGE_KEY, type ThemePreference, type UseThemeResult, cn, getThemePreference, initTheme, resolveTheme, sanitizeHref, sanitizeHtml, setThemePreference, subscribeTheme, useAnimation, useControllableState, useEscapeKey, useFloatingPosition, useFocusTrap, useId, useNodeRegistry, useOutsideClick, usePanZoom, useTheme };
package/dist/index.d.ts CHANGED
@@ -200,4 +200,75 @@ interface UseNodeRegistryReturn {
200
200
  }
201
201
  declare function useNodeRegistry(): UseNodeRegistryReturn;
202
202
 
203
- export { type NodeRect, Placement, cn, sanitizeHref, sanitizeHtml, useAnimation, useControllableState, useEscapeKey, useFloatingPosition, useFocusTrap, useId, useNodeRegistry, useOutsideClick, usePanZoom };
203
+ /**
204
+ * Light / dark / **system** theming.
205
+ *
206
+ * `styles.css` in this package defines the dark contract — it redefines the
207
+ * whole `--color-secondary-*` scale under `:where(.dark)` so that "900" always
208
+ * means strongest foreground and "50" always means faintest surface. This module
209
+ * is the other half of that contract: the thing that decides when `.dark` is on.
210
+ *
211
+ * It lives here rather than in each app because every consumer was hand-rolling
212
+ * it, and every hand-rolled copy had the same two holes:
213
+ *
214
+ * - **No live listener.** The OS preference was read once at boot, so changing
215
+ * the system theme with the page open did nothing. That is a default, not a
216
+ * "system" mode.
217
+ * - **No way back.** Choosing light or dark wrote to storage forever, so
218
+ * "follow my system" was unreachable after a single click.
219
+ *
220
+ * Both are fixed by treating **"system" as the absence of a stored choice**
221
+ * rather than as a third stored value.
222
+ *
223
+ * Every export is safe to call during SSR, where it is a no-op that resolves to
224
+ * `"light"`. Pair it with `useTheme`, which deliberately renders `system/light`
225
+ * on the server and syncs in an effect — this project has paid for hydration
226
+ * mismatches caused by reading the real theme during render.
227
+ */
228
+ type ThemePreference = "light" | "dark" | "system";
229
+ type ResolvedTheme = "light" | "dark";
230
+ /**
231
+ * Deliberately the key the showcase already used, so adopting this module keeps
232
+ * a returning visitor's saved choice instead of silently resetting it.
233
+ */
234
+ declare const THEME_STORAGE_KEY = "fancy-ui.theme";
235
+ type ThemeListener = (resolved: ResolvedTheme, preference: ThemePreference) => void;
236
+ /** The stored choice, or `"system"` when there isn't one. */
237
+ declare function getThemePreference(): ThemePreference;
238
+ /** What is actually on screen, as opposed to what the user asked for. */
239
+ declare function resolveTheme(preference?: ThemePreference): ResolvedTheme;
240
+ /**
241
+ * Choose a theme. `"system"` REMOVES the stored value rather than storing the
242
+ * string — an absent choice is what lets the OS keep speaking for the user.
243
+ */
244
+ declare function setThemePreference(preference: ThemePreference): void;
245
+ /** Observe theme changes. Returns an unsubscribe. */
246
+ declare function subscribeTheme(listener: ThemeListener): () => void;
247
+ /**
248
+ * Apply the current theme and start following the OS. Call once, as early as
249
+ * possible — before first paint if you want to avoid a flash of the wrong
250
+ * theme. Returns a disposer; calling it again is harmless.
251
+ */
252
+ declare function initTheme(options?: {
253
+ storageKey?: string;
254
+ }): () => void;
255
+
256
+ type UseThemeResult = {
257
+ /** What the user asked for, including `"system"`. */
258
+ preference: ThemePreference;
259
+ /** What is actually on screen. */
260
+ resolved: ResolvedTheme;
261
+ setPreference: (preference: ThemePreference) => void;
262
+ };
263
+ /**
264
+ * Read and set the theme from a component, re-rendering when it changes —
265
+ * including when the OS theme changes underneath a `"system"` preference.
266
+ *
267
+ * The initial state is deliberately `system` / `light` rather than the real
268
+ * theme: the server has neither storage nor an OS preference, so reading the
269
+ * truth during render is exactly the hydration mismatch this project has
270
+ * already chased down more than once. The effect syncs immediately on mount.
271
+ */
272
+ declare function useTheme(): UseThemeResult;
273
+
274
+ export { type NodeRect, Placement, type ResolvedTheme, THEME_STORAGE_KEY, type ThemePreference, type UseThemeResult, cn, getThemePreference, initTheme, resolveTheme, sanitizeHref, sanitizeHtml, setThemePreference, subscribeTheme, useAnimation, useControllableState, useEscapeKey, useFloatingPosition, useFocusTrap, useId, useNodeRegistry, useOutsideClick, usePanZoom, useTheme };
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ export { ContextMenu, useContextMenu } from './chunk-NLJGNPNN.js';
69
69
  export { Drawer, useDrawer } from './chunk-PRDWJ4AR.js';
70
70
  export { Dropdown, useDropdown } from './chunk-JIYP73OJ.js';
71
71
  export { Editor, useEditor } from './chunk-RID7CYGD.js';
72
- export { useId, useNodeRegistry } from './chunk-RNPFSVFN.js';
72
+ export { THEME_STORAGE_KEY, getThemePreference, initTheme, resolveTheme, setThemePreference, subscribeTheme, useId, useNodeRegistry, useTheme } from './chunk-FWCREAIT.js';
73
73
  export { usePanZoom } from './chunk-CPY7NHXM.js';
74
74
  export { Emoji } from './chunk-O4MKOSU2.js';
75
75
  export { EmojiSelect } from './chunk-GKGL7F36.js';
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkKUKYJZIU_cjs = require('./chunk-KUKYJZIU.cjs');
4
- require('./chunk-G34OBTU3.cjs');
4
+ require('./chunk-HPNIEYSJ.cjs');
5
5
  require('./chunk-T6YELZPJ.cjs');
6
6
  require('./chunk-TXTRTTG7.cjs');
7
7
  require('./chunk-3WGRVDQR.cjs');
@@ -1,5 +1,5 @@
1
1
  export { MobileMenu, useMobileMenu } from './chunk-F6SCZCJE.js';
2
- import './chunk-RNPFSVFN.js';
2
+ import './chunk-FWCREAIT.js';
3
3
  import './chunk-CPY7NHXM.js';
4
4
  import './chunk-5U2DZ7AV.js';
5
5
  import './chunk-CHADHN6M.js';
package/dist/sidebar.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkM2S5GCXY_cjs = require('./chunk-M2S5GCXY.cjs');
4
- require('./chunk-G34OBTU3.cjs');
4
+ require('./chunk-HPNIEYSJ.cjs');
5
5
  require('./chunk-T6YELZPJ.cjs');
6
6
  require('./chunk-TXTRTTG7.cjs');
7
7
  require('./chunk-3WGRVDQR.cjs');
package/dist/sidebar.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Sidebar, useSidebar } from './chunk-4CM2VMLR.js';
2
- import './chunk-RNPFSVFN.js';
2
+ import './chunk-FWCREAIT.js';
3
3
  import './chunk-CPY7NHXM.js';
4
4
  import './chunk-5U2DZ7AV.js';
5
5
  import './chunk-CHADHN6M.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@particle-academy/react-fancy",
3
- "version": "5.20.0",
4
- "description": "React UI component library for Human+ UX \u2014 controlled, agent-bridgeable primitives",
3
+ "version": "5.21.0",
4
+ "description": "React UI component library for Human+ UX controlled, agent-bridgeable primitives",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Particle-Academy/react-fancy.git"
@@ -1,27 +0,0 @@
1
- 'use strict';
2
-
3
- var react = require('react');
4
-
5
- // src/hooks/use-id.ts
6
- function useId(prefix) {
7
- const id = react.useId();
8
- return prefix ? `${prefix}-${id}` : id;
9
- }
10
- function useNodeRegistry() {
11
- const rectsRef = react.useRef(/* @__PURE__ */ new Map());
12
- const [version, setVersion] = react.useState(0);
13
- const registerNode = react.useCallback((id, rect) => {
14
- rectsRef.current.set(id, rect);
15
- setVersion((v) => v + 1);
16
- }, []);
17
- const unregisterNode = react.useCallback((id) => {
18
- rectsRef.current.delete(id);
19
- setVersion((v) => v + 1);
20
- }, []);
21
- return { registerNode, unregisterNode, nodeRects: rectsRef.current, version };
22
- }
23
-
24
- exports.useId = useId;
25
- exports.useNodeRegistry = useNodeRegistry;
26
- //# sourceMappingURL=chunk-G34OBTU3.cjs.map
27
- //# sourceMappingURL=chunk-G34OBTU3.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hooks/use-id.ts","../src/hooks/use-node-registry.ts"],"names":["useReactId","useRef","useState","useCallback"],"mappings":";;;;;AAEO,SAAS,MAAM,MAAA,EAAyB;AAC7C,EAAA,MAAM,KAAKA,WAAA,EAAW;AACtB,EAAA,OAAO,MAAA,GAAS,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA,GAAK,EAAA;AACtC;ACWO,SAAS,eAAA,GAAyC;AACvD,EAAA,MAAM,QAAA,GAAWC,YAAA,iBAAO,IAAI,GAAA,EAAuB,CAAA;AACnD,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAIC,eAAS,CAAC,CAAA;AAExC,EAAA,MAAM,YAAA,GAAeC,iBAAA,CAAY,CAAC,EAAA,EAAY,IAAA,KAAmB;AAC/D,IAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,EAAA,EAAI,IAAI,CAAA;AAC7B,IAAA,UAAA,CAAW,CAAC,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA;AAAA,EACzB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,cAAA,GAAiBA,iBAAA,CAAY,CAAC,EAAA,KAAe;AACjD,IAAA,QAAA,CAAS,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC1B,IAAA,UAAA,CAAW,CAAC,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA;AAAA,EACzB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,EAAE,YAAA,EAAc,cAAA,EAAgB,SAAA,EAAW,QAAA,CAAS,SAAS,OAAA,EAAQ;AAC9E","file":"chunk-G34OBTU3.cjs","sourcesContent":["import { useId as useReactId } from \"react\";\n\nexport function useId(prefix?: string): string {\n const id = useReactId();\n return prefix ? `${prefix}-${id}` : id;\n}\n","import { useCallback, useRef, useState } from \"react\";\n\nexport interface NodeRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface UseNodeRegistryReturn {\n registerNode: (id: string, rect: NodeRect) => void;\n unregisterNode: (id: string) => void;\n nodeRects: Map<string, NodeRect>;\n version: number;\n}\n\nexport function useNodeRegistry(): UseNodeRegistryReturn {\n const rectsRef = useRef(new Map<string, NodeRect>());\n const [version, setVersion] = useState(0);\n\n const registerNode = useCallback((id: string, rect: NodeRect) => {\n rectsRef.current.set(id, rect);\n setVersion((v) => v + 1);\n }, []);\n\n const unregisterNode = useCallback((id: string) => {\n rectsRef.current.delete(id);\n setVersion((v) => v + 1);\n }, []);\n\n return { registerNode, unregisterNode, nodeRects: rectsRef.current, version };\n}\n"]}
@@ -1,24 +0,0 @@
1
- import { useId as useId$1, useRef, useState, useCallback } from 'react';
2
-
3
- // src/hooks/use-id.ts
4
- function useId(prefix) {
5
- const id = useId$1();
6
- return prefix ? `${prefix}-${id}` : id;
7
- }
8
- function useNodeRegistry() {
9
- const rectsRef = useRef(/* @__PURE__ */ new Map());
10
- const [version, setVersion] = useState(0);
11
- const registerNode = useCallback((id, rect) => {
12
- rectsRef.current.set(id, rect);
13
- setVersion((v) => v + 1);
14
- }, []);
15
- const unregisterNode = useCallback((id) => {
16
- rectsRef.current.delete(id);
17
- setVersion((v) => v + 1);
18
- }, []);
19
- return { registerNode, unregisterNode, nodeRects: rectsRef.current, version };
20
- }
21
-
22
- export { useId, useNodeRegistry };
23
- //# sourceMappingURL=chunk-RNPFSVFN.js.map
24
- //# sourceMappingURL=chunk-RNPFSVFN.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hooks/use-id.ts","../src/hooks/use-node-registry.ts"],"names":["useReactId"],"mappings":";;;AAEO,SAAS,MAAM,MAAA,EAAyB;AAC7C,EAAA,MAAM,KAAKA,OAAA,EAAW;AACtB,EAAA,OAAO,MAAA,GAAS,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA,GAAK,EAAA;AACtC;ACWO,SAAS,eAAA,GAAyC;AACvD,EAAA,MAAM,QAAA,GAAW,MAAA,iBAAO,IAAI,GAAA,EAAuB,CAAA;AACnD,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,CAAC,CAAA;AAExC,EAAA,MAAM,YAAA,GAAe,WAAA,CAAY,CAAC,EAAA,EAAY,IAAA,KAAmB;AAC/D,IAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,EAAA,EAAI,IAAI,CAAA;AAC7B,IAAA,UAAA,CAAW,CAAC,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA;AAAA,EACzB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,cAAA,GAAiB,WAAA,CAAY,CAAC,EAAA,KAAe;AACjD,IAAA,QAAA,CAAS,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC1B,IAAA,UAAA,CAAW,CAAC,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA;AAAA,EACzB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,EAAE,YAAA,EAAc,cAAA,EAAgB,SAAA,EAAW,QAAA,CAAS,SAAS,OAAA,EAAQ;AAC9E","file":"chunk-RNPFSVFN.js","sourcesContent":["import { useId as useReactId } from \"react\";\n\nexport function useId(prefix?: string): string {\n const id = useReactId();\n return prefix ? `${prefix}-${id}` : id;\n}\n","import { useCallback, useRef, useState } from \"react\";\n\nexport interface NodeRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface UseNodeRegistryReturn {\n registerNode: (id: string, rect: NodeRect) => void;\n unregisterNode: (id: string) => void;\n nodeRects: Map<string, NodeRect>;\n version: number;\n}\n\nexport function useNodeRegistry(): UseNodeRegistryReturn {\n const rectsRef = useRef(new Map<string, NodeRect>());\n const [version, setVersion] = useState(0);\n\n const registerNode = useCallback((id: string, rect: NodeRect) => {\n rectsRef.current.set(id, rect);\n setVersion((v) => v + 1);\n }, []);\n\n const unregisterNode = useCallback((id: string) => {\n rectsRef.current.delete(id);\n setVersion((v) => v + 1);\n }, []);\n\n return { registerNode, unregisterNode, nodeRects: rectsRef.current, version };\n}\n"]}