@tangle-network/ui 11.51.20 → 11.51.22

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.
@@ -15,10 +15,14 @@ const namedModes = [
15
15
  { dark: "agents", light: "agents-light" },
16
16
  ] as const;
17
17
 
18
- // Client-only preference cache also keeps the control usable when storage is
19
- // denied, and lets multiple useTheme consumers share the same preference.
20
- // Server renders ONLY read getServerTheme; no request state is stored here.
21
- let preference: Theme | undefined;
18
+ export interface ThemeControllerOptions {
19
+ storageKey?: string;
20
+ defaultTheme?: Theme;
21
+ /** Persisted spellings, including null to remove a system preference. */
22
+ storageValues?: Partial<Record<Theme, string | null>>;
23
+ /** Product mode attribute; absent mode values remove the attribute. */
24
+ attribute?: { name: string; light?: string; dark?: string };
25
+ }
22
26
 
23
27
  function isTheme(value: unknown): value is Theme {
24
28
  return value === "light" || value === "dark" || value === "system";
@@ -29,72 +33,6 @@ function getSystemTheme(): Mode {
29
33
  return window.matchMedia(systemQuery).matches ? "dark" : "light";
30
34
  }
31
35
 
32
- function getServerTheme(): Theme { return "system"; }
33
- function getServerMode(): Mode { return "dark"; }
34
-
35
- function readInitialTheme(): Theme {
36
- try {
37
- const stored = window.localStorage.getItem("theme");
38
- if (isTheme(stored)) return stored;
39
- } catch { /* Storage is optional, not a prerequisite for changing mode. */ }
40
-
41
- const root = document.documentElement;
42
- const named = root.getAttribute("data-theme");
43
- if (named === "light" || named === "dark") return named;
44
- for (const pair of namedModes) {
45
- if (named === pair.light) return "light";
46
- if (named === pair.dark) return "dark";
47
- }
48
- if (root.classList.contains("light")) return "light";
49
- if (root.classList.contains("dark")) return "dark";
50
- const legacy = root.getAttribute("data-sandbox-theme");
51
- if (legacy === "vault" || legacy === "dawn") return "light";
52
- return "system";
53
- }
54
-
55
- function getTheme(): Theme {
56
- if (typeof window === "undefined") return "system";
57
- return preference ??= readInitialTheme();
58
- }
59
-
60
- function applyMode(mode: Mode) {
61
- const root = document.documentElement;
62
- root.classList.toggle("dark", mode === "dark");
63
- root.classList.toggle("light", mode === "light");
64
- const named = root.getAttribute("data-theme");
65
- if (named === "dark" || named === "light") {
66
- root.setAttribute("data-theme", mode);
67
- } else {
68
- const pair = namedModes.find((pair) => named === pair.dark || named === pair.light);
69
- if (pair) root.setAttribute("data-theme", pair[mode]);
70
- }
71
- }
72
-
73
- function setTheme(next: Theme) {
74
- if (!isTheme(next) || typeof window === "undefined") return;
75
- preference = next;
76
- try {
77
- if (next === "system") window.localStorage.removeItem("theme");
78
- else window.localStorage.setItem("theme", next);
79
- } catch { /* Keep the in-memory preference when storage is unavailable. */ }
80
- applyMode(next === "system" ? getSystemTheme() : next);
81
- window.dispatchEvent(new Event(preferenceEvent));
82
- }
83
-
84
- function subscribeTheme(notify: () => void) {
85
- const onStorage = (event: StorageEvent) => {
86
- if (event.key !== "theme" && event.key !== null) return;
87
- preference = isTheme(event.newValue) ? event.newValue : "system";
88
- notify();
89
- };
90
- window.addEventListener(preferenceEvent, notify);
91
- window.addEventListener("storage", onStorage);
92
- return () => {
93
- window.removeEventListener(preferenceEvent, notify);
94
- window.removeEventListener("storage", onStorage);
95
- };
96
- }
97
-
98
36
  function subscribeSystem(notify: () => void) {
99
37
  if (!window.matchMedia) return () => {};
100
38
  const media = window.matchMedia(systemQuery);
@@ -102,20 +40,109 @@ function subscribeSystem(notify: () => void) {
102
40
  return () => media.removeEventListener("change", notify);
103
41
  }
104
42
 
105
- export function useTheme() {
106
- const theme = useSyncExternalStore(subscribeTheme, getTheme, getServerTheme);
107
- const system = useSyncExternalStore(subscribeSystem, getSystemTheme, getServerMode);
108
- const resolvedTheme = theme === "system" ? system : theme;
109
- useEffect(() => {
110
- // Hydration first renders the stable server snapshot. Apply the real client
111
- // preference, not that placeholder, so we never repaint a persisted light
112
- // page dark while React is catching up.
113
- const selected = getTheme();
114
- applyMode(selected === "system" ? getSystemTheme() : selected);
115
- }, [resolvedTheme]);
116
- return { theme, setTheme, resolvedTheme };
43
+ /** Create once at module scope. Each product owns its persisted names, not a second theme engine. */
44
+ export function createThemeController({ storageKey = "theme", defaultTheme = "system", storageValues, attribute }: ThemeControllerOptions = {}) {
45
+ const values = { light: "light", dark: "dark", system: null, ...storageValues };
46
+ const eventName = `${preferenceEvent}:${storageKey}`;
47
+ // Client-only cache keeps storage-denied controls usable. SSR never writes it.
48
+ let preference: Theme | undefined;
49
+ let persisted: string | null | undefined;
50
+ const decode = (value: string | null): Theme =>
51
+ (Object.keys(values) as Theme[]).find(theme => value !== null && values[theme] === value) ??
52
+ (value === "system" && values.system === null ? "system" : defaultTheme);
53
+ const serverTheme = () => defaultTheme;
54
+ const serverMode = (): Mode => defaultTheme === "system" ? "dark" : defaultTheme;
55
+ function readInitialTheme(): Theme {
56
+ try {
57
+ const stored = window.localStorage.getItem(storageKey);
58
+ persisted = stored;
59
+ if (stored !== null && (Object.values(values).includes(stored) || (stored === "system" && values.system === null))) return decode(stored);
60
+ } catch { /* Storage is optional. */ }
61
+ if (attribute || defaultTheme !== "system") return defaultTheme;
62
+ const root = document.documentElement;
63
+ const named = root.getAttribute("data-theme");
64
+ if (named === "light" || named === "dark") return named;
65
+ for (const pair of namedModes) {
66
+ if (named === pair.light) return "light";
67
+ if (named === pair.dark) return "dark";
68
+ }
69
+ if (root.classList.contains("light")) return "light";
70
+ if (root.classList.contains("dark")) return "dark";
71
+ const legacy = root.getAttribute("data-sandbox-theme");
72
+ return legacy === "vault" || legacy === "dawn" ? "light" : defaultTheme;
73
+ }
74
+ function getTheme(): Theme {
75
+ if (typeof window === "undefined") return defaultTheme;
76
+ if (preference === undefined) return preference = readInitialTheme();
77
+ try {
78
+ const stored = window.localStorage.getItem(storageKey);
79
+ if (stored !== persisted) { persisted = stored; preference = decode(stored); }
80
+ } catch { /* Keep the last choice when storage becomes unavailable. */ }
81
+ return preference;
82
+ }
83
+ /** Apply a temporary page mode without overwriting the user's preference. */
84
+ function applyTheme(theme: Theme = getTheme()) {
85
+ if (typeof document === "undefined") return;
86
+ const mode = theme === "system" ? getSystemTheme() : theme;
87
+ const root = document.documentElement;
88
+ root.classList.toggle("dark", mode === "dark");
89
+ root.classList.toggle("light", !attribute && mode === "light");
90
+ if (attribute) {
91
+ const value = attribute[mode];
92
+ if (value === undefined) root.removeAttribute(attribute.name);
93
+ else root.setAttribute(attribute.name, value);
94
+ } else {
95
+ const named = root.getAttribute("data-theme");
96
+ if (named === "dark" || named === "light") root.setAttribute("data-theme", mode);
97
+ else {
98
+ const pair = namedModes.find(pair => named === pair.dark || named === pair.light);
99
+ if (pair) root.setAttribute("data-theme", pair[mode]);
100
+ }
101
+ }
102
+ }
103
+ function setTheme(next: Theme) {
104
+ if (!isTheme(next) || typeof window === "undefined") return;
105
+ preference = next;
106
+ try {
107
+ const value = values[next];
108
+ if (value === null) window.localStorage.removeItem(storageKey);
109
+ else window.localStorage.setItem(storageKey, value);
110
+ persisted = value;
111
+ } catch { /* Keep the in-memory preference when storage is unavailable. */ }
112
+ applyTheme(next);
113
+ window.dispatchEvent(new CustomEvent(eventName, { detail: next }));
114
+ }
115
+ function subscribeTheme(notify: () => void) {
116
+ const onStorage = (event: StorageEvent) => {
117
+ if (event.key !== storageKey && event.key !== null) return;
118
+ persisted = event.newValue;
119
+ preference = decode(event.newValue);
120
+ notify();
121
+ };
122
+ const onPreference = (event: Event) => {
123
+ const next = (event as CustomEvent<Theme>).detail;
124
+ if (isTheme(next)) preference = next;
125
+ notify();
126
+ };
127
+ window.addEventListener(eventName, onPreference);
128
+ window.addEventListener("storage", onStorage);
129
+ return () => {
130
+ window.removeEventListener(eventName, onPreference);
131
+ window.removeEventListener("storage", onStorage);
132
+ };
133
+ }
134
+ function useTheme() {
135
+ const theme = useSyncExternalStore(subscribeTheme, getTheme, serverTheme);
136
+ const system = useSyncExternalStore(subscribeSystem, getSystemTheme, serverMode);
137
+ const resolvedTheme = theme === "system" ? system : theme;
138
+ useEffect(() => { applyTheme(getTheme()); }, [resolvedTheme]);
139
+ return { theme, setTheme, resolvedTheme };
140
+ }
141
+ return { useTheme, getTheme, setTheme, applyTheme };
117
142
  }
118
143
 
144
+ export const { useTheme } = createThemeController();
145
+
119
146
  const iconClass = "h-4 w-4";
120
147
 
121
148
  function SunIcon() {
@@ -1,6 +1,7 @@
1
1
  export { copyText } from './copy-text';
2
2
  export * from './format';
3
3
  export { timeAgo } from './time-ago';
4
+ export { tokenMilliseconds } from './token-milliseconds';
4
5
  export {
5
6
  getToolCategory,
6
7
  getToolDisplayMetadata,
@@ -0,0 +1,11 @@
1
+ /** Resolve a scoped CSS time, including calc(); absent tokens remain unknown. */
2
+ export function tokenMilliseconds(token: string, element?: Element | null) {
3
+ const scope = element ?? (typeof document === 'undefined' ? undefined : document.documentElement), view = scope?.ownerDocument.defaultView
4
+ if (!scope || !view || !view.getComputedStyle(scope).getPropertyValue(token).trim()) return undefined
5
+ const probe = scope.ownerDocument.createElement('i'); probe.hidden = true; scope.append(probe)
6
+ try {
7
+ probe.style.animationDuration = `var(${token})`
8
+ const time = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(view.getComputedStyle(probe).animationDuration)
9
+ return time ? Number(time[1]) * (time[2] === 's' ? 1000 : 1) : undefined
10
+ } finally { probe.remove() }
11
+ }