@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.
- package/CHANGELOG.md +16 -0
- package/README.md +6 -0
- package/dist/hooks/use-event-stream.d.ts +11 -1
- package/dist/hooks/use-event-stream.js +6 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +2 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/lib/code-presentation.js +8 -22
- package/dist/primitives/hover-card.d.ts +0 -1
- package/dist/primitives/hover-card.js +66 -53
- package/dist/primitives/table.d.ts +10 -1
- package/dist/primitives/table.js +15 -3
- package/dist/primitives/theme-toggle.d.ts +25 -3
- package/dist/primitives/theme-toggle.js +114 -74
- package/dist/primitives.d.ts +3 -3
- package/dist/primitives.js +2 -2
- package/dist/utils/token-milliseconds.d.ts +2 -0
- package/dist/utils/token-milliseconds.js +17 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +2 -1
- package/package.json +2 -2
- package/src/hooks/index.ts +1 -1
- package/src/hooks/use-event-stream.ts +1 -2
- package/src/lib/code-presentation.ts +3 -11
- package/src/primitives/hover-card.tsx +19 -19
- package/src/primitives/index.ts +4 -1
- package/src/primitives/table.tsx +32 -13
- package/src/primitives/theme-toggle.tsx +109 -82
- package/src/utils/index.ts +1 -0
- package/src/utils/token-milliseconds.ts +11 -0
|
@@ -15,10 +15,14 @@ const namedModes = [
|
|
|
15
15
|
{ dark: "agents", light: "agents-light" },
|
|
16
16
|
] as const;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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() {
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
+
}
|