@womp/kookie-ui 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components.css +411 -592
- package/dist/cjs/hooks/appearance-script.d.ts +16 -0
- package/dist/cjs/hooks/appearance-script.d.ts.map +1 -0
- package/dist/cjs/hooks/appearance-script.js +12 -0
- package/dist/cjs/hooks/appearance-script.js.map +7 -0
- package/dist/cjs/hooks/index.d.ts +3 -0
- package/dist/cjs/hooks/index.d.ts.map +1 -1
- package/dist/cjs/hooks/index.js +12 -2
- package/dist/cjs/hooks/index.js.map +4 -4
- package/dist/cjs/hooks/use-appearance.d.ts +30 -0
- package/dist/cjs/hooks/use-appearance.d.ts.map +1 -0
- package/dist/cjs/hooks/use-appearance.js +12 -0
- package/dist/cjs/hooks/use-appearance.js.map +7 -0
- package/dist/cjs/index.js +15 -5
- package/dist/cjs/index.js.map +4 -4
- package/dist/esm/chunk-2KZ4NI3X.js +1 -0
- package/dist/esm/chunk-HT7RFHLW.js +2 -0
- package/dist/esm/chunk-HT7RFHLW.js.map +7 -0
- package/dist/esm/chunk-IUER6BNH.js +12 -0
- package/dist/esm/chunk-IUER6BNH.js.map +7 -0
- package/dist/esm/hooks/appearance-script.d.ts +16 -0
- package/dist/esm/hooks/appearance-script.d.ts.map +1 -0
- package/dist/esm/hooks/appearance-script.js +2 -0
- package/dist/esm/hooks/appearance-script.js.map +7 -0
- package/dist/esm/hooks/index.d.ts +3 -0
- package/dist/esm/hooks/index.d.ts.map +1 -1
- package/dist/esm/hooks/index.js +1 -1
- package/dist/esm/hooks/use-appearance.d.ts +30 -0
- package/dist/esm/hooks/use-appearance.d.ts.map +1 -0
- package/dist/esm/hooks/use-appearance.js +2 -0
- package/dist/esm/hooks/use-appearance.js.map +7 -0
- package/dist/esm/index.js +1 -1
- package/layout/tokens.css +4 -0
- package/layout.css +4 -0
- package/package.json +11 -1
- package/src/components/_internal/base-button.css +4 -4
- package/src/components/_internal/base-menu.css +3 -3
- package/src/components/_internal/base-tab-list.css +2 -2
- package/src/components/segmented-control.css +4 -4
- package/src/components/select.css +8 -5
- package/src/components/sidebar.css +41 -97
- package/src/components/text-field.css +3 -3
- package/src/components/toolbar.css +4 -4
- package/src/hooks/appearance-script.ts +28 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-appearance.ts +119 -0
- package/src/styles/tokens/space.css +7 -0
- package/styles.css +415 -592
- package/tokens/base.css +4 -0
- package/tokens.css +4 -0
- package/dist/esm/chunk-EL7YGOTY.js +0 -1
- /package/dist/esm/{chunk-EL7YGOTY.js.map → chunk-2KZ4NI3X.js.map} +0 -0
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
@breakpoints {
|
|
215
215
|
.rt-TextFieldRoot {
|
|
216
216
|
&:where(.rt-r-size-1) {
|
|
217
|
-
--text-field-height: var(--
|
|
217
|
+
--text-field-height: var(--control-height-1);
|
|
218
218
|
--text-field-padding: calc(var(--space-1) * 1.5 - var(--text-field-border-width));
|
|
219
219
|
--text-field-border-radius: max(var(--radius-1), var(--radius-full));
|
|
220
220
|
--text-field-native-icon-size: var(--space-3);
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
&:where(.rt-r-size-2) {
|
|
250
|
-
--text-field-height: var(--
|
|
250
|
+
--text-field-height: var(--control-height-2);
|
|
251
251
|
--text-field-padding: calc(var(--space-2) - var(--text-field-border-width));
|
|
252
252
|
--text-field-border-radius: max(var(--radius-2), var(--radius-full));
|
|
253
253
|
--text-field-native-icon-size: var(--space-4);
|
|
@@ -281,7 +281,7 @@
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
&:where(.rt-r-size-3) {
|
|
284
|
-
--text-field-height: var(--
|
|
284
|
+
--text-field-height: var(--control-height-3);
|
|
285
285
|
--text-field-padding: calc(var(--space-3) - var(--text-field-border-width));
|
|
286
286
|
--text-field-border-radius: max(var(--radius-3), var(--radius-full));
|
|
287
287
|
--text-field-native-icon-size: var(--space-4);
|
|
@@ -26,28 +26,28 @@
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
&:where(.rt-r-size-1) {
|
|
29
|
-
--toolbar-control-height: var(--
|
|
29
|
+
--toolbar-control-height: var(--control-height-1);
|
|
30
30
|
--toolbar-radius: max(var(--radius-1), var(--radius-full));
|
|
31
31
|
--toolbar-gap: var(--space-1);
|
|
32
32
|
--toolbar-title-gap: var(--space-2);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
&:where(.rt-r-size-2) {
|
|
36
|
-
--toolbar-control-height: var(--
|
|
36
|
+
--toolbar-control-height: var(--control-height-2);
|
|
37
37
|
--toolbar-radius: max(var(--radius-2), var(--radius-full));
|
|
38
38
|
--toolbar-gap: var(--space-2);
|
|
39
39
|
--toolbar-title-gap: var(--space-3);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
&:where(.rt-r-size-3) {
|
|
43
|
-
--toolbar-control-height: var(--
|
|
43
|
+
--toolbar-control-height: var(--control-height-3);
|
|
44
44
|
--toolbar-radius: max(var(--radius-3), var(--radius-full));
|
|
45
45
|
--toolbar-gap: var(--space-2);
|
|
46
46
|
--toolbar-title-gap: var(--space-4);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
&:where(.rt-r-size-4) {
|
|
50
|
-
--toolbar-control-height: var(--
|
|
50
|
+
--toolbar-control-height: var(--control-height-4);
|
|
51
51
|
--toolbar-radius: max(var(--radius-4), var(--radius-full));
|
|
52
52
|
--toolbar-gap: var(--space-3);
|
|
53
53
|
--toolbar-title-gap: var(--space-5);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-paint half of light and dark mode (see `use-appearance.ts`). Kept in its own module
|
|
3
|
+
* with no client directive, so a server component such as a Next.js root layout can import the
|
|
4
|
+
* string and inline it in `<head>`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const APPEARANCE_STORAGE_KEY = 'kookie-appearance';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Inline this in `<head>`, before any stylesheet paints, e.g.
|
|
11
|
+
* `<script dangerouslySetInnerHTML={{ __html: appearanceScript }} />`.
|
|
12
|
+
*
|
|
13
|
+
* Storage is read inside its own try/catch: browsers that block site data throw on access,
|
|
14
|
+
* and that must not skip the stamp. `matchMedia` always answers.
|
|
15
|
+
*/
|
|
16
|
+
const appearanceScript = `(function () {
|
|
17
|
+
var e = document.documentElement;
|
|
18
|
+
var a = null;
|
|
19
|
+
try { a = localStorage.getItem(${JSON.stringify(APPEARANCE_STORAGE_KEY)}); } catch (err) {}
|
|
20
|
+
var dark = a === "light" ? false : a === "dark" ? true : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
21
|
+
var mode = dark ? "dark" : "light";
|
|
22
|
+
e.classList.remove("light", "dark");
|
|
23
|
+
e.classList.add(mode);
|
|
24
|
+
e.setAttribute("data-appearance", mode);
|
|
25
|
+
e.style.colorScheme = mode;
|
|
26
|
+
})();`;
|
|
27
|
+
|
|
28
|
+
export { appearanceScript, APPEARANCE_STORAGE_KEY };
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,3 +2,6 @@ export { useLiveAnnouncer } from './use-live-announcer.js';
|
|
|
2
2
|
export { useBodyPointerEventsCleanup } from './use-body-pointer-events-cleanup.js';
|
|
3
3
|
export { useTooltipWrapper } from './use-tooltip-wrapper.js';
|
|
4
4
|
export { useToggleState } from './use-toggle-state.js';
|
|
5
|
+
export { useAppearance, setAppearance } from './use-appearance.js';
|
|
6
|
+
export { appearanceScript, APPEARANCE_STORAGE_KEY } from './appearance-script.js';
|
|
7
|
+
export type { AppearanceChoice, ResolvedAppearance } from './use-appearance.js';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { APPEARANCE_STORAGE_KEY } from './appearance-script.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Light and dark mode, decided before the first paint and kept on `<html>`.
|
|
8
|
+
*
|
|
9
|
+
* The server cannot know the visitor's mode, so `appearanceScript` runs synchronously in
|
|
10
|
+
* `<head>`: the stored choice first, `prefers-color-scheme` otherwise. It writes the `light`
|
|
11
|
+
* or `dark` class (which every token and component stylesheet already keys off) and
|
|
12
|
+
* `data-appearance` on `<html>`. The root `<Theme>` keeps `appearance="inherit"`, so it
|
|
13
|
+
* writes no class of its own and `<html>` is the one source of truth. Nested themes and
|
|
14
|
+
* portals inherit it from there.
|
|
15
|
+
*
|
|
16
|
+
* `useAppearance` is the client half: an external store over `<html>` and the same storage
|
|
17
|
+
* key. It never mirrors the mode into React state that could disagree with the DOM.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type AppearanceChoice = 'system' | 'light' | 'dark';
|
|
21
|
+
type ResolvedAppearance = 'light' | 'dark';
|
|
22
|
+
|
|
23
|
+
const listeners = new Set<() => void>();
|
|
24
|
+
|
|
25
|
+
function emit() {
|
|
26
|
+
for (const listener of listeners) listener();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readChoice(): AppearanceChoice {
|
|
30
|
+
try {
|
|
31
|
+
const value = globalThis.localStorage?.getItem(APPEARANCE_STORAGE_KEY);
|
|
32
|
+
return value === 'light' || value === 'dark' ? value : 'system';
|
|
33
|
+
} catch {
|
|
34
|
+
return 'system';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function systemAppearance(): ResolvedAppearance {
|
|
39
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stamp(mode: ResolvedAppearance) {
|
|
43
|
+
const root = document.documentElement;
|
|
44
|
+
root.classList.remove('light', 'dark');
|
|
45
|
+
root.classList.add(mode);
|
|
46
|
+
root.setAttribute('data-appearance', mode);
|
|
47
|
+
root.style.colorScheme = mode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readResolved(): ResolvedAppearance {
|
|
51
|
+
const root = document.documentElement;
|
|
52
|
+
if (root.classList.contains('dark') || root.classList.contains('dark-theme')) return 'dark';
|
|
53
|
+
if (root.classList.contains('light') || root.classList.contains('light-theme')) return 'light';
|
|
54
|
+
return systemAppearance();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let snapshot: { choice: AppearanceChoice; resolved: ResolvedAppearance } | null = null;
|
|
58
|
+
|
|
59
|
+
function getSnapshot() {
|
|
60
|
+
const choice = readChoice();
|
|
61
|
+
const resolved = readResolved();
|
|
62
|
+
if (!snapshot || snapshot.choice !== choice || snapshot.resolved !== resolved) {
|
|
63
|
+
snapshot = { choice, resolved };
|
|
64
|
+
}
|
|
65
|
+
return snapshot;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const serverSnapshot = { choice: 'system' as AppearanceChoice, resolved: 'light' as ResolvedAppearance };
|
|
69
|
+
|
|
70
|
+
function subscribe(listener: () => void) {
|
|
71
|
+
listeners.add(listener);
|
|
72
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
73
|
+
// With no stored choice, follow the system as it changes.
|
|
74
|
+
const onSystemChange = () => {
|
|
75
|
+
if (readChoice() === 'system') {
|
|
76
|
+
stamp(systemAppearance());
|
|
77
|
+
emit();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
// Another tab changed the choice.
|
|
81
|
+
const onStorage = (event: StorageEvent) => {
|
|
82
|
+
if (event.key !== APPEARANCE_STORAGE_KEY) return;
|
|
83
|
+
const choice = readChoice();
|
|
84
|
+
stamp(choice === 'system' ? systemAppearance() : choice);
|
|
85
|
+
emit();
|
|
86
|
+
};
|
|
87
|
+
media.addEventListener('change', onSystemChange);
|
|
88
|
+
window.addEventListener('storage', onStorage);
|
|
89
|
+
return () => {
|
|
90
|
+
listeners.delete(listener);
|
|
91
|
+
media.removeEventListener('change', onSystemChange);
|
|
92
|
+
window.removeEventListener('storage', onStorage);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Applies a choice to `<html>` and stores it. The change applies even if storage is blocked. */
|
|
97
|
+
function setAppearance(choice: AppearanceChoice) {
|
|
98
|
+
try {
|
|
99
|
+
if (choice === 'system') localStorage.removeItem(APPEARANCE_STORAGE_KEY);
|
|
100
|
+
else localStorage.setItem(APPEARANCE_STORAGE_KEY, choice);
|
|
101
|
+
} catch {
|
|
102
|
+
// Storage is blocked: the mode still changes for this page.
|
|
103
|
+
}
|
|
104
|
+
stamp(choice === 'system' ? systemAppearance() : choice);
|
|
105
|
+
emit();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The page's light or dark mode. `choice` is what the person picked (`system` when they
|
|
110
|
+
* haven't); `appearance` is what is showing. On the server and in the first client render it
|
|
111
|
+
* reports `system` / `light`, then corrects without a hydration mismatch.
|
|
112
|
+
*/
|
|
113
|
+
function useAppearance() {
|
|
114
|
+
const { choice, resolved } = React.useSyncExternalStore(subscribe, getSnapshot, () => serverSnapshot);
|
|
115
|
+
return { choice, appearance: resolved, setAppearance } as const;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { useAppearance, setAppearance };
|
|
119
|
+
export type { AppearanceChoice, ResolvedAppearance };
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
--space-7: calc(40px * var(--scaling));
|
|
13
13
|
--space-8: calc(48px * var(--scaling));
|
|
14
14
|
|
|
15
|
+
/* Control heights. One ladder for every control: size N is the same height in
|
|
16
|
+
Button, Menu, Sidebar, Tabs, TextField, Select, SegmentedControl and Toolbar. */
|
|
17
|
+
--control-height-1: var(--space-5);
|
|
18
|
+
--control-height-2: var(--space-6);
|
|
19
|
+
--control-height-3: var(--space-7);
|
|
20
|
+
--control-height-4: var(--space-8);
|
|
21
|
+
|
|
15
22
|
/* */
|
|
16
23
|
--space-9: calc(64px * var(--scaling));
|
|
17
24
|
--space-10: calc(80px * var(--scaling));
|