@sigx/lynx-daisyui 0.4.2 → 0.4.4
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/README.md +80 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -1
- package/dist/styles/components/alert.css +1 -1
- package/dist/styles/components/badge.css +1 -1
- package/dist/styles/components/button.css +1 -1
- package/dist/styles/components/card.css +1 -1
- package/dist/styles/components/checkbox.css +1 -1
- package/dist/styles/components/input.css +1 -1
- package/dist/styles/components/modal.css +1 -1
- package/dist/styles/components/select.css +2 -2
- package/dist/styles/components/skeleton.css +1 -1
- package/dist/styles/components/textarea.css +1 -1
- package/dist/styles/components/toggle.css +4 -4
- package/dist/styles/index.css +5 -0
- package/dist/styles/themes/builtins.generated.css +140 -0
- package/dist/styles/themes/shapes.css +6 -12
- package/dist/styles/themes/tokens.css +35 -29
- package/dist/theme/StatusBarSync.js +5 -2
- package/dist/theme/ThemeProvider.d.ts +8 -4
- package/dist/theme/ThemeProvider.js +176 -85
- package/dist/theme/registry.d.ts +33 -6
- package/dist/theme/registry.js +22 -0
- package/dist/theme/theme-state.d.ts +28 -0
- package/dist/theme/theme-state.js +72 -0
- package/dist/theme/use-screen-theme.d.ts +3 -0
- package/dist/theme/use-screen-theme.js +42 -0
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -83,10 +83,88 @@ layout role. For multi-class compositions (color + modifier),
|
|
|
83
83
|
`theme.set('daisy-light daisy-rounded')` works — the class string is
|
|
84
84
|
applied verbatim to the host view.
|
|
85
85
|
|
|
86
|
+
### Two layers: content vs. OS chrome
|
|
87
|
+
|
|
88
|
+
A theme drives two different things, and they scope differently:
|
|
89
|
+
|
|
90
|
+
1. **In-app content** — the `--color-*` / radius variables and icon
|
|
91
|
+
tints. These live on a host view and inherit down a subtree, so they
|
|
92
|
+
are genuinely *scopable*.
|
|
93
|
+
2. **OS chrome** — the status- and navigation-bar tint (pushed by
|
|
94
|
+
`<StatusBarSync>`). This is a global OS singleton; it can only reflect
|
|
95
|
+
one theme at a time.
|
|
96
|
+
|
|
97
|
+
The rule:
|
|
98
|
+
|
|
99
|
+
> `useTheme()` is the theme for the **content you render** — the nearest
|
|
100
|
+
> `<ThemeProvider>`, or the app-global theme at the root / in headless
|
|
101
|
+
> code. **System chrome always follows the global theme.** `StatusBarSync`
|
|
102
|
+
> binds to the global controller, so a nested provider can't hijack the
|
|
103
|
+
> bars.
|
|
104
|
+
>
|
|
105
|
+
> *Scopes recolor pixels you draw; only the global theme touches the OS.*
|
|
106
|
+
|
|
107
|
+
This mirrors Flutter, where `Theme` nests freely for content while system
|
|
108
|
+
chrome goes through a separate channel (`AnnotatedRegion`/`SystemChrome`).
|
|
109
|
+
|
|
110
|
+
### Headless control (no provider required)
|
|
111
|
+
|
|
112
|
+
The active theme lives in a module-level singleton, so you can read and
|
|
113
|
+
set it from anywhere — a store, a service, app-boot logic, an effect —
|
|
114
|
+
without a mounted `<ThemeProvider>` ancestor. `useTheme()` resolves to
|
|
115
|
+
this same controller when no provider is in scope (it never throws).
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { themeController } from '@sigx/lynx-daisyui';
|
|
119
|
+
|
|
120
|
+
// From any non-component module:
|
|
121
|
+
themeController.set('daisy-dark');
|
|
122
|
+
themeController.toggle();
|
|
123
|
+
themeController.followSystem();
|
|
124
|
+
themeController.name; // current selection
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A mounted root `<ThemeProvider>` binds this singleton, so headless
|
|
128
|
+
mutations render and the OS bars follow.
|
|
129
|
+
|
|
130
|
+
### Per-screen themes
|
|
131
|
+
|
|
132
|
+
Different screens can use different themes — and the status-bar icons
|
|
133
|
+
follow the active screen so they stay legible. Because this drives the
|
|
134
|
+
**global** theme, the bars update automatically:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { useScreenTheme } from '@sigx/lynx-daisyui';
|
|
138
|
+
|
|
139
|
+
const Gallery = component(() => {
|
|
140
|
+
useScreenTheme('daisy-dark'); // dark (incl. status bar) while focused; restored on blur
|
|
141
|
+
return () => <view>…</view>;
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`useScreenTheme` is built on `@sigx/lynx-navigation`'s `useFocusEffect`
|
|
146
|
+
(an optional peer) and must be called from a routed screen.
|
|
147
|
+
|
|
148
|
+
### Scoped sub-overrides
|
|
149
|
+
|
|
150
|
+
To recolor just a **region** without touching the OS bars, nest a
|
|
151
|
+
`<ThemeProvider>`. Its subtree (content + icons) re-themes; the status
|
|
152
|
+
bar stays on the global theme.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<ThemeProvider initial="daisy-light">
|
|
156
|
+
<App />
|
|
157
|
+
{/* this card renders synthwave; the status bar stays light */}
|
|
158
|
+
<ThemeProvider initial="daisy-synthwave">
|
|
159
|
+
<PreviewCard />
|
|
160
|
+
</ThemeProvider>
|
|
161
|
+
</ThemeProvider>
|
|
162
|
+
```
|
|
163
|
+
|
|
86
164
|
## Navigation chrome
|
|
87
165
|
|
|
88
166
|
Two daisy-themed components that pair with
|
|
89
|
-
[`@sigx/lynx-navigation`](
|
|
167
|
+
[`@sigx/lynx-navigation`](https://github.com/signalxjs/lynx/tree/main/packages/lynx-navigation). Both read state via the
|
|
90
168
|
navigation package's hooks (no internal-module imports), so swapping
|
|
91
169
|
in custom designs later is a one-component change.
|
|
92
170
|
|
|
@@ -146,7 +224,7 @@ one consumer of that hook.
|
|
|
146
224
|
### `<SwiperIndicator>`
|
|
147
225
|
|
|
148
226
|
Themed wrapper around the headless `useSwiperDot*` hooks from
|
|
149
|
-
[`@sigx/lynx-gestures`](
|
|
227
|
+
[`@sigx/lynx-gestures`](https://github.com/signalxjs/lynx/tree/main/packages/lynx-gestures#swiper-and-headless-dot-hooks).
|
|
150
228
|
Reads colours from the active daisy theme so the indicator follows light
|
|
151
229
|
/ dark mode automatically.
|
|
152
230
|
|
package/dist/index.d.ts
CHANGED
|
@@ -54,10 +54,12 @@ export { NavDrawer } from './navigation/NavDrawer.js';
|
|
|
54
54
|
export type { NavDrawerProps, NavDrawerSide, } from './navigation/NavDrawer.js';
|
|
55
55
|
export { SwiperIndicator } from './navigation/SwiperIndicator.js';
|
|
56
56
|
export type { SwiperIndicatorProps, SwiperIndicatorVariant, SwiperIndicatorSize, } from './navigation/SwiperIndicator.js';
|
|
57
|
-
export { ThemeProvider, useTheme, listThemes, registerTheme, extendTheme, pickThemeFor, pairOf, variantOf, colorsOf, radiusOf, } from './theme/ThemeProvider.js';
|
|
58
|
-
export type { DaisyTheme, ThemeController, ThemeProviderProps, Theme, ThemePalette, ThemeRadius, ThemeVariant, } from './theme/ThemeProvider.js';
|
|
57
|
+
export { ThemeProvider, useTheme, listThemes, registerTheme, extendTheme, pickThemeFor, pairOf, variantOf, colorsOf, radiusOf, sizesOf, } from './theme/ThemeProvider.js';
|
|
58
|
+
export type { DaisyTheme, ThemeController, ThemeProviderProps, Theme, ThemePalette, ThemeRadius, ThemeSizes, ThemeVariant, } from './theme/ThemeProvider.js';
|
|
59
59
|
export { StatusBarSync } from './theme/StatusBarSync.js';
|
|
60
60
|
export type { StatusBarSyncProps } from './theme/StatusBarSync.js';
|
|
61
|
+
export { themeController } from './theme/theme-state.js';
|
|
62
|
+
export { useScreenTheme } from './theme/use-screen-theme.js';
|
|
61
63
|
export { Avatar } from './data/Avatar.js';
|
|
62
64
|
export type { AvatarProps, AvatarSize } from './data/Avatar.js';
|
|
63
65
|
export { Text } from './typography/Text.js';
|
package/dist/index.js
CHANGED
|
@@ -32,8 +32,15 @@ export { NavHeader } from './navigation/NavHeader.js';
|
|
|
32
32
|
export { NavDrawer } from './navigation/NavDrawer.js';
|
|
33
33
|
export { SwiperIndicator } from './navigation/SwiperIndicator.js';
|
|
34
34
|
// Theme
|
|
35
|
-
export { ThemeProvider, useTheme, listThemes, registerTheme, extendTheme, pickThemeFor, pairOf, variantOf, colorsOf, radiusOf, } from './theme/ThemeProvider.js';
|
|
35
|
+
export { ThemeProvider, useTheme, listThemes, registerTheme, extendTheme, pickThemeFor, pairOf, variantOf, colorsOf, radiusOf, sizesOf, } from './theme/ThemeProvider.js';
|
|
36
36
|
export { StatusBarSync } from './theme/StatusBarSync.js';
|
|
37
|
+
// Headless theme handle (issue #113): import and call from anywhere — stores,
|
|
38
|
+
// services, effects, app-boot — with no `<ThemeProvider>` ancestor required.
|
|
39
|
+
// `useTheme()` resolves to this when no provider is in scope.
|
|
40
|
+
export { themeController } from './theme/theme-state.js';
|
|
41
|
+
// Per-screen theming: pin the global theme while a navigation screen is focused
|
|
42
|
+
// (requires the optional `@sigx/lynx-navigation` peer).
|
|
43
|
+
export { useScreenTheme } from './theme/use-screen-theme.js';
|
|
37
44
|
// Data
|
|
38
45
|
export { Avatar } from './data/Avatar.js';
|
|
39
46
|
// Typography
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
height: var(--size-md);
|
|
5
5
|
padding-left: var(--padding-btn-md);
|
|
6
6
|
padding-right: var(--padding-btn-md);
|
|
7
|
-
border-radius: var(--
|
|
7
|
+
border-radius: var(--radius-field);
|
|
8
8
|
font-size: var(--font-md);
|
|
9
9
|
background-color: var(--color-base-100);
|
|
10
10
|
color: var(--color-base-content);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
height: var(--size-md);
|
|
8
8
|
padding-left: var(--padding-btn-md);
|
|
9
9
|
padding-right: var(--padding-btn-md);
|
|
10
|
-
border-radius: var(--
|
|
10
|
+
border-radius: var(--radius-field);
|
|
11
11
|
font-size: var(--font-md);
|
|
12
12
|
background-color: var(--color-base-100);
|
|
13
13
|
color: var(--color-base-content);
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
background-color: var(--color-base-100);
|
|
39
39
|
border-width: var(--border-btn);
|
|
40
40
|
border-color: var(--color-base-300);
|
|
41
|
-
border-radius: var(--
|
|
41
|
+
border-radius: var(--radius-field);
|
|
42
42
|
overflow: hidden;
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
padding: 12px;
|
|
5
5
|
padding-left: var(--padding-btn-md);
|
|
6
6
|
padding-right: var(--padding-btn-md);
|
|
7
|
-
border-radius: var(--
|
|
7
|
+
border-radius: var(--radius-field);
|
|
8
8
|
font-size: var(--font-md);
|
|
9
9
|
background-color: var(--color-base-100);
|
|
10
10
|
color: var(--color-base-content);
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/* Size variants — track dimensions */
|
|
11
|
-
.toggle-xs { width: var(--toggle-width-xs); height: var(--toggle-height-xs); border-radius: var(--
|
|
12
|
-
.toggle-sm { width: var(--toggle-width-sm); height: var(--toggle-height-sm); border-radius: var(--
|
|
13
|
-
.toggle-md { width: var(--toggle-width-md); height: var(--toggle-height-md); border-radius: var(--
|
|
14
|
-
.toggle-lg { width: var(--toggle-width-lg); height: var(--toggle-height-lg); border-radius: var(--
|
|
11
|
+
.toggle-xs { width: var(--toggle-width-xs); height: var(--toggle-height-xs); border-radius: var(--radius-selector); }
|
|
12
|
+
.toggle-sm { width: var(--toggle-width-sm); height: var(--toggle-height-sm); border-radius: var(--radius-selector); }
|
|
13
|
+
.toggle-md { width: var(--toggle-width-md); height: var(--toggle-height-md); border-radius: var(--radius-selector); }
|
|
14
|
+
.toggle-lg { width: var(--toggle-width-lg); height: var(--toggle-height-lg); border-radius: var(--radius-selector); }
|
|
15
15
|
|
|
16
16
|
/* Thumb */
|
|
17
17
|
.toggle-thumb {
|
package/dist/styles/index.css
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
/* Structural design tokens (.daisy) + composable shape modifiers */
|
|
9
9
|
@import './themes/tokens.css';
|
|
10
10
|
@import './themes/shapes.css';
|
|
11
|
+
/* Per-theme color tokens for the built-in themes — generated from the theme
|
|
12
|
+
* registry at build time (scripts/gen-theme-css.mjs). Declared as CSS rules so
|
|
13
|
+
* `var(--color-*)` resolves on the first paint; the runtime `setProperty` path
|
|
14
|
+
* can't set inheritable custom properties before descendants have painted. */
|
|
15
|
+
@import './themes/builtins.generated.css';
|
|
11
16
|
|
|
12
17
|
/* Base reset */
|
|
13
18
|
@import './base.css';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* AUTO-GENERATED from src/theme/registry.ts — do not edit.
|
|
2
|
+
* Regenerated by scripts/gen-theme-css.mjs on every build. */
|
|
3
|
+
|
|
4
|
+
.daisy-light {
|
|
5
|
+
--color-primary: #491dff;
|
|
6
|
+
--color-primary-content: #d3dbff;
|
|
7
|
+
--color-secondary: #ff20cc;
|
|
8
|
+
--color-secondary-content: #fff8fc;
|
|
9
|
+
--color-accent: #00cfbd;
|
|
10
|
+
--color-accent-content: #00100d;
|
|
11
|
+
--color-neutral: #2b3440;
|
|
12
|
+
--color-neutral-content: #d7dde4;
|
|
13
|
+
--color-base-100: #ffffff;
|
|
14
|
+
--color-base-200: #f2f2f2;
|
|
15
|
+
--color-base-300: #e5e6e6;
|
|
16
|
+
--color-base-content: #1f2937;
|
|
17
|
+
--color-info: #00b4fa;
|
|
18
|
+
--color-info-content: #000000;
|
|
19
|
+
--color-success: #00a96e;
|
|
20
|
+
--color-success-content: #000000;
|
|
21
|
+
--color-warning: #ffc100;
|
|
22
|
+
--color-warning-content: #000000;
|
|
23
|
+
--color-error: #ff676a;
|
|
24
|
+
--color-error-content: #000000;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.daisy-cupcake {
|
|
28
|
+
--color-primary: #65c3c8;
|
|
29
|
+
--color-primary-content: #052124;
|
|
30
|
+
--color-secondary: #ef9fbc;
|
|
31
|
+
--color-secondary-content: #2d0a16;
|
|
32
|
+
--color-accent: #eeaf3a;
|
|
33
|
+
--color-accent-content: #2d1c00;
|
|
34
|
+
--color-neutral: #291334;
|
|
35
|
+
--color-neutral-content: #f5f1f8;
|
|
36
|
+
--color-base-100: #faf7f5;
|
|
37
|
+
--color-base-200: #efeae6;
|
|
38
|
+
--color-base-300: #e7e2df;
|
|
39
|
+
--color-base-content: #291334;
|
|
40
|
+
--color-info: #00b4fa;
|
|
41
|
+
--color-info-content: #000000;
|
|
42
|
+
--color-success: #00a96e;
|
|
43
|
+
--color-success-content: #000000;
|
|
44
|
+
--color-warning: #ffc100;
|
|
45
|
+
--color-warning-content: #000000;
|
|
46
|
+
--color-error: #ff676a;
|
|
47
|
+
--color-error-content: #000000;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.daisy-emerald {
|
|
51
|
+
--color-primary: #66cc8a;
|
|
52
|
+
--color-primary-content: #06200f;
|
|
53
|
+
--color-secondary: #377cfb;
|
|
54
|
+
--color-secondary-content: #02112d;
|
|
55
|
+
--color-accent: #f68067;
|
|
56
|
+
--color-accent-content: #2d0a02;
|
|
57
|
+
--color-neutral: #333c4d;
|
|
58
|
+
--color-neutral-content: #e9eaed;
|
|
59
|
+
--color-base-100: #ffffff;
|
|
60
|
+
--color-base-200: #f3f4f6;
|
|
61
|
+
--color-base-300: #e5e7eb;
|
|
62
|
+
--color-base-content: #333c4d;
|
|
63
|
+
--color-info: #1c92f2;
|
|
64
|
+
--color-info-content: #000a14;
|
|
65
|
+
--color-success: #00a96e;
|
|
66
|
+
--color-success-content: #000a05;
|
|
67
|
+
--color-warning: #ff9900;
|
|
68
|
+
--color-warning-content: #261600;
|
|
69
|
+
--color-error: #ff5724;
|
|
70
|
+
--color-error-content: #000000;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.daisy-dark {
|
|
74
|
+
--color-primary: #7582ff;
|
|
75
|
+
--color-primary-content: #050617;
|
|
76
|
+
--color-secondary: #ff71cf;
|
|
77
|
+
--color-secondary-content: #190211;
|
|
78
|
+
--color-accent: #00e7d0;
|
|
79
|
+
--color-accent-content: #001210;
|
|
80
|
+
--color-neutral: #2a323c;
|
|
81
|
+
--color-neutral-content: #a6adbb;
|
|
82
|
+
--color-base-100: #1d232a;
|
|
83
|
+
--color-base-200: #191e24;
|
|
84
|
+
--color-base-300: #343b46;
|
|
85
|
+
--color-base-content: #a6adbb;
|
|
86
|
+
--color-info: #00b4fa;
|
|
87
|
+
--color-info-content: #000000;
|
|
88
|
+
--color-success: #00a96e;
|
|
89
|
+
--color-success-content: #000000;
|
|
90
|
+
--color-warning: #ffc100;
|
|
91
|
+
--color-warning-content: #000000;
|
|
92
|
+
--color-error: #ff676a;
|
|
93
|
+
--color-error-content: #000000;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.daisy-synthwave {
|
|
97
|
+
--color-primary: #e779c1;
|
|
98
|
+
--color-primary-content: #2a0a1f;
|
|
99
|
+
--color-secondary: #58c7f3;
|
|
100
|
+
--color-secondary-content: #02141d;
|
|
101
|
+
--color-accent: #f3cc30;
|
|
102
|
+
--color-accent-content: #2a1f00;
|
|
103
|
+
--color-neutral: #20134e;
|
|
104
|
+
--color-neutral-content: #e3e0f5;
|
|
105
|
+
--color-base-100: #2d1b69;
|
|
106
|
+
--color-base-200: #261159;
|
|
107
|
+
--color-base-300: #1f0f4a;
|
|
108
|
+
--color-base-content: #f9f7fd;
|
|
109
|
+
--color-info: #53c0f3;
|
|
110
|
+
--color-info-content: #02151e;
|
|
111
|
+
--color-success: #71ead2;
|
|
112
|
+
--color-success-content: #002721;
|
|
113
|
+
--color-warning: #f3cc30;
|
|
114
|
+
--color-warning-content: #2a1f00;
|
|
115
|
+
--color-error: #e24056;
|
|
116
|
+
--color-error-content: #ffffff;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.daisy-dracula {
|
|
120
|
+
--color-primary: #ff79c6;
|
|
121
|
+
--color-primary-content: #2d0414;
|
|
122
|
+
--color-secondary: #bd93f9;
|
|
123
|
+
--color-secondary-content: #160226;
|
|
124
|
+
--color-accent: #50fa7b;
|
|
125
|
+
--color-accent-content: #002a0e;
|
|
126
|
+
--color-neutral: #414558;
|
|
127
|
+
--color-neutral-content: #f8f8f2;
|
|
128
|
+
--color-base-100: #282a36;
|
|
129
|
+
--color-base-200: #21222c;
|
|
130
|
+
--color-base-300: #181920;
|
|
131
|
+
--color-base-content: #f8f8f2;
|
|
132
|
+
--color-info: #8be9fd;
|
|
133
|
+
--color-info-content: #002a31;
|
|
134
|
+
--color-success: #50fa7b;
|
|
135
|
+
--color-success-content: #002a0e;
|
|
136
|
+
--color-warning: #f1fa8c;
|
|
137
|
+
--color-warning-content: #2a2900;
|
|
138
|
+
--color-error: #ff5555;
|
|
139
|
+
--color-error-content: #2a0000;
|
|
140
|
+
}
|
|
@@ -3,19 +3,13 @@
|
|
|
3
3
|
<ThemeProvider class="daisy-rounded"> → host class="daisy daisy-rounded" */
|
|
4
4
|
|
|
5
5
|
.daisy-flat {
|
|
6
|
-
--
|
|
7
|
-
--
|
|
8
|
-
--
|
|
9
|
-
--rounded-tab: 0px;
|
|
10
|
-
--rounded-selector: 0px;
|
|
11
|
-
--rounded-toggle: 0px;
|
|
6
|
+
--radius-selector: 0px;
|
|
7
|
+
--radius-field: 0px;
|
|
8
|
+
--radius-box: 0px;
|
|
12
9
|
}
|
|
13
10
|
|
|
14
11
|
.daisy-rounded {
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--rounded-tab: 9999px;
|
|
19
|
-
--rounded-selector: 9999px;
|
|
20
|
-
--rounded-toggle: 9999px;
|
|
12
|
+
--radius-selector: 9999px;
|
|
13
|
+
--radius-field: 9999px;
|
|
14
|
+
--radius-box: 24px;
|
|
21
15
|
}
|
|
@@ -8,15 +8,20 @@
|
|
|
8
8
|
* may still override roundness via its `radius` field. */
|
|
9
9
|
|
|
10
10
|
.daisy {
|
|
11
|
-
/* ── Roundness ── */
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--rounded-tab: 8px;
|
|
16
|
-
--rounded-selector: 8px;
|
|
17
|
-
--rounded-toggle: 9999px;
|
|
11
|
+
/* ── Roundness (DaisyUI v5 contract) ── */
|
|
12
|
+
--radius-selector: 8px;
|
|
13
|
+
--radius-field: 8px;
|
|
14
|
+
--radius-box: 16px;
|
|
18
15
|
|
|
19
|
-
/* ──
|
|
16
|
+
/* ── Base size units (DaisyUI v5 contract) ──
|
|
17
|
+
* A theme's `sizes` ({ field, selector }) overrides these; <ThemeProvider>
|
|
18
|
+
* re-derives the component dimensions below from them in JS. We deliberately
|
|
19
|
+
* do NOT use `calc(var() * n)` in CSS — it's unproven in Lynx's runtime CSS
|
|
20
|
+
* engine. Defaults below are base 4px × the per-token multiples shown. */
|
|
21
|
+
--size-selector: 4px;
|
|
22
|
+
--size-field: 4px;
|
|
23
|
+
|
|
24
|
+
/* ── Field sizing scale (button / input / select) — field × 6/8/12/16 ── */
|
|
20
25
|
--size-xs: 24px;
|
|
21
26
|
--size-sm: 32px;
|
|
22
27
|
--size-md: 48px;
|
|
@@ -41,27 +46,28 @@
|
|
|
41
46
|
--border-btn: 1px;
|
|
42
47
|
--border-card: 1px;
|
|
43
48
|
|
|
44
|
-
/* ──
|
|
45
|
-
|
|
46
|
-
--checkbox-
|
|
47
|
-
--checkbox-
|
|
48
|
-
--checkbox-
|
|
49
|
-
--
|
|
50
|
-
--toggle-width-
|
|
51
|
-
--toggle-width-
|
|
52
|
-
--toggle-width-
|
|
53
|
-
--toggle-
|
|
54
|
-
--toggle-height-
|
|
55
|
-
--toggle-height-
|
|
56
|
-
--toggle-height-
|
|
57
|
-
--toggle-
|
|
58
|
-
--toggle-thumb-
|
|
59
|
-
--toggle-thumb-
|
|
60
|
-
--toggle-thumb-
|
|
61
|
-
--
|
|
62
|
-
--badge-
|
|
63
|
-
--badge-
|
|
64
|
-
--badge-
|
|
49
|
+
/* ── Selector-driven sizes (checkbox / toggle / badge) — selector × the
|
|
50
|
+
* multiples shown; <ThemeProvider> re-derives these from `sizes.selector`. ── */
|
|
51
|
+
--checkbox-xs: 16px; /* ×4 */
|
|
52
|
+
--checkbox-sm: 20px; /* ×5 */
|
|
53
|
+
--checkbox-md: 24px; /* ×6 */
|
|
54
|
+
--checkbox-lg: 32px; /* ×8 */
|
|
55
|
+
--toggle-width-xs: 32px; /* ×8 */
|
|
56
|
+
--toggle-width-sm: 40px; /* ×10 */
|
|
57
|
+
--toggle-width-md: 48px; /* ×12 */
|
|
58
|
+
--toggle-width-lg: 56px; /* ×14 */
|
|
59
|
+
--toggle-height-xs: 24px; /* ×6 */
|
|
60
|
+
--toggle-height-sm: 24px; /* ×6 */
|
|
61
|
+
--toggle-height-md: 28px; /* ×7 */
|
|
62
|
+
--toggle-height-lg: 32px; /* ×8 */
|
|
63
|
+
--toggle-thumb-xs: 16px; /* ×4 */
|
|
64
|
+
--toggle-thumb-sm: 16px; /* ×4 */
|
|
65
|
+
--toggle-thumb-md: 20px; /* ×5 */
|
|
66
|
+
--toggle-thumb-lg: 24px; /* ×6 */
|
|
67
|
+
--badge-xs: 16px; /* ×4 */
|
|
68
|
+
--badge-sm: 20px; /* ×5 */
|
|
69
|
+
--badge-md: 24px; /* ×6 */
|
|
70
|
+
--badge-lg: 32px; /* ×8 */
|
|
65
71
|
--step-indicator: 32px;
|
|
66
72
|
--progress-height: 8px;
|
|
67
73
|
--modal-max-width: 400px;
|
|
@@ -28,10 +28,13 @@ import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
|
|
|
28
28
|
*/
|
|
29
29
|
import { component, effect, onMounted, onUnmounted } from '@sigx/lynx';
|
|
30
30
|
import { isAvailable, setSystemBarsStyle } from '@sigx/lynx-appearance';
|
|
31
|
-
import {
|
|
31
|
+
import { themeController } from './theme-state.js';
|
|
32
32
|
import { variantOf } from './registry.js';
|
|
33
33
|
export const StatusBarSync = component(({ props }) => {
|
|
34
|
-
|
|
34
|
+
// Bind to the *global* theme — not `useTheme()` — so the OS bars always
|
|
35
|
+
// track the app/screen theme and can't be hijacked by a content sub-scope
|
|
36
|
+
// (a nested `<ThemeProvider>` recolors its subtree but leaves the bars put).
|
|
37
|
+
const theme = themeController;
|
|
35
38
|
let lastApplied = null;
|
|
36
39
|
let runner;
|
|
37
40
|
function apply(name) {
|
|
@@ -90,8 +90,12 @@ export interface ThemeController {
|
|
|
90
90
|
followSystem(): void;
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
* Access the
|
|
94
|
-
*
|
|
93
|
+
* Access the active daisyui theme controller. Resolves to the nearest
|
|
94
|
+
* `<ThemeProvider>`'s controller (a content sub-scope), or — at the app root
|
|
95
|
+
* and in *headless* code with no provider mounted — the global controller
|
|
96
|
+
* (`themeController`). Never throws: theme control is reachable from anywhere
|
|
97
|
+
* (issue #113). For control that must always target the app/OS theme
|
|
98
|
+
* regardless of scope (e.g. a status-bar sync), import `themeController`.
|
|
95
99
|
*/
|
|
96
100
|
export declare const useTheme: import("@sigx/runtime-core").InjectableFunction<ThemeController>;
|
|
97
101
|
export type ThemeProviderProps =
|
|
@@ -134,5 +138,5 @@ Define.Prop<'initial', DaisyTheme, false>
|
|
|
134
138
|
export declare const ThemeProvider: import("@sigx/runtime-core").ComponentFactory<ThemeProviderProps, void, {
|
|
135
139
|
default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
|
|
136
140
|
}>;
|
|
137
|
-
export { listThemes, registerTheme, extendTheme, pickThemeFor, pairOf, variantOf, colorsOf, radiusOf, } from './registry.js';
|
|
138
|
-
export type { Theme, ThemePalette, ThemeRadius, ThemeVariant } from './registry.js';
|
|
141
|
+
export { listThemes, registerTheme, extendTheme, pickThemeFor, pairOf, variantOf, colorsOf, radiusOf, sizesOf, } from './registry.js';
|
|
142
|
+
export type { Theme, ThemePalette, ThemeRadius, ThemeSizes, ThemeVariant, } from './registry.js';
|
|
@@ -35,17 +35,94 @@ import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
|
|
|
35
35
|
* <ThemeProvider light="daisy-cupcake" dark="daisy-synthwave">…</ThemeProvider>
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
|
-
import { component, defineInjectable, defineProvide, onMounted, onUnmounted, signal, } from '@sigx/lynx';
|
|
38
|
+
import { component, defineInjectable, defineProvide, effect, onMounted, onUnmounted, signal, untrack, } from '@sigx/lynx';
|
|
39
39
|
import { useIconColorResolver } from '@sigx/lynx-icons';
|
|
40
40
|
import { useSystemColorScheme } from '@sigx/lynx-appearance';
|
|
41
|
-
import { colorsOf,
|
|
41
|
+
import { colorsOf, isBuiltInTheme, pickThemeFor, radiusOf, sizesOf, variantOf, } from './registry.js';
|
|
42
|
+
import { globalThemeState, makeThemeController, themeController, } from './theme-state.js';
|
|
43
|
+
// DaisyUI v5 expresses control dimensions as multiples of two base units
|
|
44
|
+
// (`--size-field`, `--size-selector`). Lynx's runtime CSS engine is unproven
|
|
45
|
+
// for `calc(var() * n)`, so when a theme overrides a base unit we do the
|
|
46
|
+
// multiplication here and emit literal px. Bases must be px (engine-safe, like
|
|
47
|
+
// colors); a non-px base sets only the base var and leaves the `.daisy`
|
|
48
|
+
// defaults in place. Multiples mirror the defaults in `styles/themes/tokens.css`.
|
|
49
|
+
const FIELD_STEPS = { xs: 6, sm: 8, md: 12, lg: 16 };
|
|
50
|
+
const SELECTOR_STEPS = {
|
|
51
|
+
'checkbox-xs': 4, 'checkbox-sm': 5, 'checkbox-md': 6, 'checkbox-lg': 8,
|
|
52
|
+
'toggle-width-xs': 8, 'toggle-width-sm': 10, 'toggle-width-md': 12, 'toggle-width-lg': 14,
|
|
53
|
+
'toggle-height-xs': 6, 'toggle-height-sm': 6, 'toggle-height-md': 7, 'toggle-height-lg': 8,
|
|
54
|
+
'toggle-thumb-xs': 4, 'toggle-thumb-sm': 4, 'toggle-thumb-md': 5, 'toggle-thumb-lg': 6,
|
|
55
|
+
'badge-xs': 4, 'badge-sm': 5, 'badge-md': 6, 'badge-lg': 8,
|
|
56
|
+
};
|
|
57
|
+
const pxValue = (v) => {
|
|
58
|
+
const m = /^\s*(\d+(?:\.\d+)?)px\s*$/.exec(v);
|
|
59
|
+
return m ? Number(m[1]) : undefined;
|
|
60
|
+
};
|
|
61
|
+
/** Emit a theme's `sizes` overrides as literal-px CSS custom properties. */
|
|
62
|
+
function applySizeVars(style, sizes) {
|
|
63
|
+
if (sizes.field) {
|
|
64
|
+
style['--size-field'] = sizes.field;
|
|
65
|
+
const base = pxValue(sizes.field);
|
|
66
|
+
if (base !== undefined) {
|
|
67
|
+
for (const k in FIELD_STEPS)
|
|
68
|
+
style[`--size-${k}`] = `${base * FIELD_STEPS[k]}px`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (sizes.selector) {
|
|
72
|
+
style['--size-selector'] = sizes.selector;
|
|
73
|
+
const base = pxValue(sizes.selector);
|
|
74
|
+
if (base !== undefined) {
|
|
75
|
+
for (const k in SELECTOR_STEPS)
|
|
76
|
+
style[`--${k}`] = `${base * SELECTOR_STEPS[k]}px`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
42
80
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
81
|
+
* The full custom-property set for a theme — colors plus any radius/size
|
|
82
|
+
* overrides. Applied at runtime via the Lynx `setProperty` API (see
|
|
83
|
+
* `<ThemeProvider>`), NOT the inline `style` attribute: Lynx does not honor
|
|
84
|
+
* custom properties declared inline in this toolchain, but `setProperty`
|
|
85
|
+
* registers real, inheritable ones — the documented way to theme via CSS
|
|
86
|
+
* variables (https://lynxjs.org/guide/styling/custom-theming).
|
|
45
87
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
88
|
+
function buildThemeVars(name) {
|
|
89
|
+
const palette = colorsOf(name) ?? colorsOf('daisy-light');
|
|
90
|
+
const radius = radiusOf(name);
|
|
91
|
+
const sizes = sizesOf(name);
|
|
92
|
+
const vars = {};
|
|
93
|
+
for (const key in palette)
|
|
94
|
+
vars[`--color-${key}`] = palette[key];
|
|
95
|
+
if (radius) {
|
|
96
|
+
if (radius.selector)
|
|
97
|
+
vars['--radius-selector'] = radius.selector;
|
|
98
|
+
if (radius.field)
|
|
99
|
+
vars['--radius-field'] = radius.field;
|
|
100
|
+
if (radius.box)
|
|
101
|
+
vars['--radius-box'] = radius.box;
|
|
102
|
+
}
|
|
103
|
+
if (sizes)
|
|
104
|
+
applySizeVars(vars, sizes);
|
|
105
|
+
return vars;
|
|
106
|
+
}
|
|
107
|
+
/** Unique host id per provider instance so `getElementById` targets its own subtree. */
|
|
108
|
+
let themeIdSeq = 0;
|
|
109
|
+
/**
|
|
110
|
+
* Access the active daisyui theme controller. Resolves to the nearest
|
|
111
|
+
* `<ThemeProvider>`'s controller (a content sub-scope), or — at the app root
|
|
112
|
+
* and in *headless* code with no provider mounted — the global controller
|
|
113
|
+
* (`themeController`). Never throws: theme control is reachable from anywhere
|
|
114
|
+
* (issue #113). For control that must always target the app/OS theme
|
|
115
|
+
* regardless of scope (e.g. a status-bar sync), import `themeController`.
|
|
116
|
+
*/
|
|
117
|
+
export const useTheme = defineInjectable(() => themeController);
|
|
118
|
+
/**
|
|
119
|
+
* Nesting-depth marker. The outermost `<ThemeProvider>` sees depth 0 and binds
|
|
120
|
+
* the global singleton (so headless `themeController` mutations render and the
|
|
121
|
+
* OS bars track it); a nested provider sees >= 1 and creates its own local
|
|
122
|
+
* state — a content sub-scope that recolors its subtree without touching the
|
|
123
|
+
* global theme or the system bars.
|
|
124
|
+
*/
|
|
125
|
+
const useThemeDepth = defineInjectable(() => 0);
|
|
49
126
|
/**
|
|
50
127
|
* Wraps children in a `<view class={theme}>` so the daisyui CSS variables
|
|
51
128
|
* defined inside the theme class inherit down to every descendant.
|
|
@@ -64,44 +141,45 @@ export const ThemeProvider = component(({ props, slots }) => {
|
|
|
64
141
|
// The underlying signal widens to PrimitiveSignal<string> via Widen<T>;
|
|
65
142
|
// cast at read sites to keep the narrow union throughout the component.
|
|
66
143
|
const readScheme = () => systemScheme.value;
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
144
|
+
// Root vs. nested. The outermost provider (depth 0) binds the global
|
|
145
|
+
// singleton — so headless `themeController` mutations render here and the OS
|
|
146
|
+
// bars (via StatusBarSync) follow this theme. A nested provider gets its own
|
|
147
|
+
// local state: a content sub-scope that overrides its subtree only.
|
|
148
|
+
const depth = useThemeDepth();
|
|
149
|
+
const isRoot = depth === 0;
|
|
150
|
+
defineProvide(useThemeDepth, () => depth + 1);
|
|
151
|
+
// Stable id for the host view so the runtime `setProperty` call (below) can
|
|
152
|
+
// target it. Unique per instance so nested providers theme their own subtree.
|
|
153
|
+
const hostId = `daisy-theme-${++themeIdSeq}`;
|
|
154
|
+
const state = isRoot
|
|
155
|
+
? globalThemeState
|
|
156
|
+
: signal(props.initial
|
|
157
|
+
? { name: props.initial, following: false }
|
|
158
|
+
: {
|
|
159
|
+
name: readScheme() === 'dark'
|
|
160
|
+
? (props.dark ?? pickThemeFor('dark'))
|
|
161
|
+
: (props.light ?? pickThemeFor('light')),
|
|
162
|
+
following: true,
|
|
163
|
+
});
|
|
164
|
+
// Seed the root from props/system. An explicit `initial` pin is author
|
|
165
|
+
// intent and wins. With no `initial`, reflect the current system scheme into
|
|
166
|
+
// the first render — but only while `following`, so a theme a headless
|
|
167
|
+
// caller set before this mounted is respected, not clobbered. The follow
|
|
168
|
+
// effect below keeps it in sync afterwards.
|
|
169
|
+
if (isRoot) {
|
|
170
|
+
if (props.initial) {
|
|
171
|
+
state.name = props.initial;
|
|
172
|
+
state.following = false;
|
|
173
|
+
}
|
|
174
|
+
else if (state.following) {
|
|
175
|
+
state.name = readScheme() === 'dark'
|
|
72
176
|
? (props.dark ?? pickThemeFor('dark'))
|
|
73
|
-
: (props.light ?? pickThemeFor('light'))
|
|
74
|
-
|
|
75
|
-
};
|
|
76
|
-
const state = signal(initialState);
|
|
77
|
-
// Guard against re-applying the same theme on stray re-fires.
|
|
78
|
-
let lastApplied = state.following ? readScheme() : null;
|
|
79
|
-
function applySystem(scheme, force = false) {
|
|
80
|
-
if (!state.following)
|
|
81
|
-
return;
|
|
82
|
-
if (!force && lastApplied === scheme)
|
|
83
|
-
return;
|
|
84
|
-
lastApplied = scheme;
|
|
85
|
-
state.name = scheme === 'dark'
|
|
86
|
-
? (props.dark ?? pickThemeFor('dark'))
|
|
87
|
-
: (props.light ?? pickThemeFor('light'));
|
|
177
|
+
: (props.light ?? pickThemeFor('light'));
|
|
178
|
+
}
|
|
88
179
|
}
|
|
89
|
-
const controller =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
set(next) {
|
|
93
|
-
state.name = next;
|
|
94
|
-
state.following = false;
|
|
95
|
-
},
|
|
96
|
-
toggle() {
|
|
97
|
-
state.name = pairOf(state.name);
|
|
98
|
-
state.following = false;
|
|
99
|
-
},
|
|
100
|
-
followSystem() {
|
|
101
|
-
state.following = true;
|
|
102
|
-
applySystem(readScheme(), /* force */ true);
|
|
103
|
-
},
|
|
104
|
-
};
|
|
180
|
+
const controller = isRoot
|
|
181
|
+
? themeController
|
|
182
|
+
: makeThemeController(state);
|
|
105
183
|
defineProvide(useTheme, () => controller);
|
|
106
184
|
// Wire the daisy color resolver into `@sigx/lynx-icons`'s injectable
|
|
107
185
|
// so any `<Icon variant="primary">` rendered inside this subtree gets
|
|
@@ -118,35 +196,65 @@ export const ThemeProvider = component(({ props, slots }) => {
|
|
|
118
196
|
return palette?.[variant];
|
|
119
197
|
};
|
|
120
198
|
defineProvide(useIconColorResolver, () => resolver);
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
199
|
+
// Follow the system color scheme while `following`. Reactive: re-runs when
|
|
200
|
+
// `following` flips true (e.g. `controller.followSystem()`, including the
|
|
201
|
+
// headless `themeController`) or when the OS scheme changes, and writes the
|
|
202
|
+
// matching theme. Reading `state.following` and `systemScheme.value` tracks
|
|
203
|
+
// them; the `name` write is `untrack`ed so it can't re-trigger the effect.
|
|
204
|
+
// Created on mount (the native publisher may populate the scheme between
|
|
205
|
+
// setup and mount) and torn down on unmount.
|
|
206
|
+
let follow;
|
|
207
|
+
let applyVars;
|
|
126
208
|
onMounted(() => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
209
|
+
follow = effect(() => {
|
|
210
|
+
const following = state.following;
|
|
211
|
+
const scheme = readScheme();
|
|
212
|
+
if (!following)
|
|
213
|
+
return;
|
|
214
|
+
const next = scheme === 'dark'
|
|
215
|
+
? (props.dark ?? pickThemeFor('dark'))
|
|
216
|
+
: (props.light ?? pickThemeFor('light'));
|
|
217
|
+
untrack(() => {
|
|
218
|
+
if (state.name !== next)
|
|
219
|
+
state.name = next;
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
// Built-in themes are themed by their generated CSS class (applied on
|
|
223
|
+
// the host below), which resolves on the very first frame. This
|
|
224
|
+
// `setProperty` path additionally serves runtime-registered themes
|
|
225
|
+
// (`registerTheme`, no shipped CSS class) — applied once they're
|
|
226
|
+
// selected post-mount, where it lands reliably. Reading `state.name`
|
|
227
|
+
// (via buildThemeVars) tracks it, so this re-runs on every theme change.
|
|
228
|
+
applyVars = effect(() => {
|
|
229
|
+
const vars = buildThemeVars(state.name);
|
|
230
|
+
if (typeof lynx !== 'undefined') {
|
|
231
|
+
lynx.getElementById(hostId)?.setProperty(vars);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
134
234
|
});
|
|
135
235
|
onUnmounted(() => {
|
|
136
|
-
|
|
137
|
-
|
|
236
|
+
follow?.stop();
|
|
237
|
+
follow = undefined;
|
|
238
|
+
applyVars?.stop();
|
|
239
|
+
applyVars = undefined;
|
|
138
240
|
});
|
|
139
241
|
return () => {
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
// `
|
|
242
|
+
// Theme COLORS and any radius/size overrides are applied as real,
|
|
243
|
+
// inheritable CSS custom properties via the Lynx `setProperty` runtime
|
|
244
|
+
// API (see the `applyVars` effect above) — Lynx does NOT honor custom
|
|
245
|
+
// properties declared through the inline `style` attribute in this
|
|
246
|
+
// toolchain. The root background/text are painted here from palette
|
|
247
|
+
// literals (real properties, not custom props) so the surface is themed
|
|
248
|
+
// on first paint; descendants resolve `var(--color-*)` once setProperty
|
|
249
|
+
// has run. The `daisy` base class supplies structural token defaults.
|
|
148
250
|
const palette = colorsOf(state.name) ?? colorsOf('daisy-light');
|
|
149
|
-
|
|
251
|
+
// Built-ins ship a generated CSS class, so `state.name` alone paints on
|
|
252
|
+
// the first frame. A runtime-registered theme has no class — fall back
|
|
253
|
+
// to its variant's built-in class for the first frame; the `setProperty`
|
|
254
|
+
// effect above then swaps in its exact palette post-mount.
|
|
255
|
+
const themeClass = isBuiltInTheme(state.name)
|
|
256
|
+
? state.name
|
|
257
|
+
: `${pickThemeFor(variantOf(state.name) ?? 'light')} ${state.name}`;
|
|
150
258
|
const style = {
|
|
151
259
|
flexGrow: 1,
|
|
152
260
|
flexShrink: 1,
|
|
@@ -157,27 +265,10 @@ export const ThemeProvider = component(({ props, slots }) => {
|
|
|
157
265
|
backgroundColor: palette['base-100'],
|
|
158
266
|
color: palette['base-content'],
|
|
159
267
|
};
|
|
160
|
-
for (const key in palette) {
|
|
161
|
-
style[`--color-${key}`] = palette[key];
|
|
162
|
-
}
|
|
163
|
-
if (radius) {
|
|
164
|
-
if (radius.box)
|
|
165
|
-
style['--rounded-box'] = radius.box;
|
|
166
|
-
if (radius.btn)
|
|
167
|
-
style['--rounded-btn'] = radius.btn;
|
|
168
|
-
if (radius.badge)
|
|
169
|
-
style['--rounded-badge'] = radius.badge;
|
|
170
|
-
if (radius.tab)
|
|
171
|
-
style['--rounded-tab'] = radius.tab;
|
|
172
|
-
if (radius.selector)
|
|
173
|
-
style['--rounded-selector'] = radius.selector;
|
|
174
|
-
if (radius.toggle)
|
|
175
|
-
style['--rounded-toggle'] = radius.toggle;
|
|
176
|
-
}
|
|
177
268
|
if (props.style)
|
|
178
269
|
Object.assign(style, props.style);
|
|
179
|
-
return (_jsx("view", { class: `daisy${props.class ? ' ' + props.class : ''}`, style: style, children: slots.default?.() }));
|
|
270
|
+
return (_jsx("view", { id: hostId, class: `daisy ${themeClass}${props.class ? ' ' + props.class : ''}`, style: style, children: slots.default?.() }));
|
|
180
271
|
};
|
|
181
272
|
});
|
|
182
273
|
// Re-export registry helpers so consumers only need `@sigx/lynx-daisyui`.
|
|
183
|
-
export { listThemes, registerTheme, extendTheme, pickThemeFor, pairOf, variantOf, colorsOf, radiusOf, } from './registry.js';
|
|
274
|
+
export { listThemes, registerTheme, extendTheme, pickThemeFor, pairOf, variantOf, colorsOf, radiusOf, sizesOf, } from './registry.js';
|
package/dist/theme/registry.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Structural tokens (radius, sizing, component dimensions) are theme-agnostic
|
|
19
19
|
* and ship once in the bundled `.daisy` base class (`styles/themes/tokens.css`);
|
|
20
|
-
* a theme may override roundness via `radius`.
|
|
20
|
+
* a theme may override roundness via `radius` and base size units via `sizes`.
|
|
21
21
|
*
|
|
22
22
|
* Colors are engine-safe strings — hex or `rgb()`. Lynx's CSS engine does not
|
|
23
23
|
* parse `oklch()`, so convert before registering.
|
|
@@ -26,14 +26,29 @@ import type { DaisyColor } from '../shared/styles.js';
|
|
|
26
26
|
export type ThemeVariant = 'light' | 'dark';
|
|
27
27
|
/** Full daisy color palette — every semantic token, no holes. */
|
|
28
28
|
export type ThemePalette = Record<DaisyColor, string>;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Roundness token overrides (DaisyUI v5 contract). Emitted as
|
|
31
|
+
* `--radius-selector` / `--radius-field` / `--radius-box`. Defaults live in
|
|
32
|
+
* the bundled `.daisy` base.
|
|
33
|
+
*/
|
|
30
34
|
export interface ThemeRadius {
|
|
35
|
+
/** Small selectable controls — checkbox, toggle, badge. */
|
|
36
|
+
selector?: string;
|
|
37
|
+
/** Fields — button, input, select, textarea. */
|
|
38
|
+
field?: string;
|
|
39
|
+
/** Boxes — card, modal, alert. */
|
|
31
40
|
box?: string;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Base size-unit overrides (DaisyUI v5 contract). Emitted as
|
|
44
|
+
* `--size-selector` / `--size-field`; component dimensions are integer
|
|
45
|
+
* multiples of these. Defaults live in the bundled `.daisy` base.
|
|
46
|
+
*/
|
|
47
|
+
export interface ThemeSizes {
|
|
48
|
+
/** Base unit for selector controls (checkbox, toggle, badge). */
|
|
35
49
|
selector?: string;
|
|
36
|
-
|
|
50
|
+
/** Base unit for fields (button, input, select). */
|
|
51
|
+
field?: string;
|
|
37
52
|
}
|
|
38
53
|
export interface Theme {
|
|
39
54
|
/** Unique id — also the value of `theme.name`. */
|
|
@@ -49,7 +64,16 @@ export interface Theme {
|
|
|
49
64
|
pair?: string;
|
|
50
65
|
/** Optional roundness overrides; unspecified tokens fall back to `.daisy`. */
|
|
51
66
|
radius?: ThemeRadius;
|
|
67
|
+
/** Optional base size-unit overrides; unspecified tokens fall back to `.daisy`. */
|
|
68
|
+
sizes?: ThemeSizes;
|
|
52
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether `name` is a built-in theme that ships a CSS class — i.e. it paints
|
|
72
|
+
* correctly on the first frame. Runtime-registered themes return `false`;
|
|
73
|
+
* `<ThemeProvider>` falls back to their variant's built-in class for first
|
|
74
|
+
* paint and swaps in the exact palette via `setProperty`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function isBuiltInTheme(name: string | undefined): boolean;
|
|
53
77
|
/**
|
|
54
78
|
* All registered themes in insertion order. Returns a shallow copy so callers
|
|
55
79
|
* can't mutate the internal registry — re-registration goes through
|
|
@@ -80,6 +104,7 @@ export declare function extendTheme(base: string, patch: {
|
|
|
80
104
|
pair?: string;
|
|
81
105
|
colors?: Partial<ThemePalette>;
|
|
82
106
|
radius?: ThemeRadius;
|
|
107
|
+
sizes?: ThemeSizes;
|
|
83
108
|
}): Theme;
|
|
84
109
|
/** The variant of a registered theme, or `undefined` if not registered. */
|
|
85
110
|
export declare function variantOf(name: string | undefined): ThemeVariant | undefined;
|
|
@@ -87,6 +112,8 @@ export declare function variantOf(name: string | undefined): ThemeVariant | unde
|
|
|
87
112
|
export declare function colorsOf(name: string | undefined): ThemePalette | undefined;
|
|
88
113
|
/** The roundness overrides of a registered theme, if any. */
|
|
89
114
|
export declare function radiusOf(name: string | undefined): ThemeRadius | undefined;
|
|
115
|
+
/** The base size-unit overrides of a registered theme, if any. */
|
|
116
|
+
export declare function sizesOf(name: string | undefined): ThemeSizes | undefined;
|
|
90
117
|
/**
|
|
91
118
|
* Pick a default theme for a given system color scheme — the first registered
|
|
92
119
|
* theme of that variant (`daisy-light` / `daisy-dark` under the seeded
|
package/dist/theme/registry.js
CHANGED
|
@@ -89,6 +89,23 @@ const registry = [
|
|
|
89
89
|
},
|
|
90
90
|
},
|
|
91
91
|
];
|
|
92
|
+
/**
|
|
93
|
+
* Names of the themes seeded at module load — the built-ins that ship a
|
|
94
|
+
* generated CSS class (`scripts/gen-theme-css.mjs`). Captured before any
|
|
95
|
+
* runtime `registerTheme()`, so it distinguishes "has a first-paint CSS class"
|
|
96
|
+
* from runtime-registered themes (which only have their palette as data and
|
|
97
|
+
* apply via `setProperty` post-mount).
|
|
98
|
+
*/
|
|
99
|
+
const BUILTIN_NAMES = new Set(registry.map((t) => t.name));
|
|
100
|
+
/**
|
|
101
|
+
* Whether `name` is a built-in theme that ships a CSS class — i.e. it paints
|
|
102
|
+
* correctly on the first frame. Runtime-registered themes return `false`;
|
|
103
|
+
* `<ThemeProvider>` falls back to their variant's built-in class for first
|
|
104
|
+
* paint and swaps in the exact palette via `setProperty`.
|
|
105
|
+
*/
|
|
106
|
+
export function isBuiltInTheme(name) {
|
|
107
|
+
return name != null && BUILTIN_NAMES.has(name);
|
|
108
|
+
}
|
|
92
109
|
/**
|
|
93
110
|
* Resolve a `theme.name` to its registered `Theme`. Supports multi-class names
|
|
94
111
|
* like `'daisy-light daisy-rounded'` by matching the first registered id found.
|
|
@@ -147,6 +164,7 @@ export function extendTheme(base, patch) {
|
|
|
147
164
|
pair: patch.pair ?? src.pair,
|
|
148
165
|
colors: { ...src.colors, ...patch.colors },
|
|
149
166
|
radius: patch.radius ?? src.radius,
|
|
167
|
+
sizes: patch.sizes ?? src.sizes,
|
|
150
168
|
};
|
|
151
169
|
}
|
|
152
170
|
/** The variant of a registered theme, or `undefined` if not registered. */
|
|
@@ -161,6 +179,10 @@ export function colorsOf(name) {
|
|
|
161
179
|
export function radiusOf(name) {
|
|
162
180
|
return findTheme(name)?.radius;
|
|
163
181
|
}
|
|
182
|
+
/** The base size-unit overrides of a registered theme, if any. */
|
|
183
|
+
export function sizesOf(name) {
|
|
184
|
+
return findTheme(name)?.sizes;
|
|
185
|
+
}
|
|
164
186
|
/**
|
|
165
187
|
* Pick a default theme for a given system color scheme — the first registered
|
|
166
188
|
* theme of that variant (`daisy-light` / `daisy-dark` under the seeded
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DaisyTheme, ThemeController } from './ThemeProvider.js';
|
|
2
|
+
/** The mutable selection a `ThemeController` reads from and writes to. */
|
|
3
|
+
export interface ThemeState {
|
|
4
|
+
name: DaisyTheme;
|
|
5
|
+
following: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Build a `ThemeController` over a given state object. Used for both the global
|
|
9
|
+
* singleton (below) and each nested `<ThemeProvider>`'s local state — same
|
|
10
|
+
* behaviour, different backing store. `followSystem()` only flips the flag; the
|
|
11
|
+
* owning provider's follow effect performs the re-apply.
|
|
12
|
+
*/
|
|
13
|
+
export declare function makeThemeController(state: ThemeState): ThemeController;
|
|
14
|
+
/**
|
|
15
|
+
* The backing signal for the global theme. Read/written by the root
|
|
16
|
+
* `<ThemeProvider>` and shared with `themeController`; not part of the public
|
|
17
|
+
* API.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare const globalThemeState: import("@sigx/reactivity").Signal<ThemeState>;
|
|
21
|
+
/**
|
|
22
|
+
* The global theme controller — the headless handle for issue #113. Import and
|
|
23
|
+
* call from anywhere (no `<ThemeProvider>` ancestor required); `useTheme()`'s
|
|
24
|
+
* default factory returns this same instance, and the root `<ThemeProvider>`
|
|
25
|
+
* provides it to its subtree. `StatusBarSync` binds to it so the OS bars always
|
|
26
|
+
* follow the global/screen theme, never a content sub-scope.
|
|
27
|
+
*/
|
|
28
|
+
export declare const themeController: ThemeController;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global theme state — the headless DI singleton behind `useTheme()`.
|
|
3
|
+
*
|
|
4
|
+
* The active selection (current theme name + follow-system flag) lives here as
|
|
5
|
+
* a module-level signal, mirroring how `./registry.ts` is already a global
|
|
6
|
+
* module singleton. This is what makes theme control reachable from *headless*
|
|
7
|
+
* code — a store, a service, app-boot logic, an effect — not just from a
|
|
8
|
+
* component mounted under `<ThemeProvider>`.
|
|
9
|
+
*
|
|
10
|
+
* The root `<ThemeProvider>` (depth 0) binds to this state: it renders its host
|
|
11
|
+
* view from it, owns the system-color-scheme follow effect that writes to it
|
|
12
|
+
* while `following`, and seeds an `initial` prop into it. Nested providers
|
|
13
|
+
* (depth >= 1) build their own local state via `makeThemeController` so a
|
|
14
|
+
* subtree can be overridden without touching the global — see
|
|
15
|
+
* `./ThemeProvider.tsx`.
|
|
16
|
+
*
|
|
17
|
+
* `followSystem()` here only flips the flag; the actual re-apply on an OS color
|
|
18
|
+
* scheme change is driven by the root provider's follow effect (which has the
|
|
19
|
+
* appearance signal in scope).
|
|
20
|
+
*/
|
|
21
|
+
import { signal } from '@sigx/lynx';
|
|
22
|
+
import { pairOf, pickThemeFor } from './registry.js';
|
|
23
|
+
/**
|
|
24
|
+
* Build a `ThemeController` over a given state object. Used for both the global
|
|
25
|
+
* singleton (below) and each nested `<ThemeProvider>`'s local state — same
|
|
26
|
+
* behaviour, different backing store. `followSystem()` only flips the flag; the
|
|
27
|
+
* owning provider's follow effect performs the re-apply.
|
|
28
|
+
*/
|
|
29
|
+
export function makeThemeController(state) {
|
|
30
|
+
return {
|
|
31
|
+
get name() {
|
|
32
|
+
return state.name;
|
|
33
|
+
},
|
|
34
|
+
get followingSystem() {
|
|
35
|
+
return state.following;
|
|
36
|
+
},
|
|
37
|
+
set(next) {
|
|
38
|
+
state.name = next;
|
|
39
|
+
state.following = false;
|
|
40
|
+
},
|
|
41
|
+
toggle() {
|
|
42
|
+
state.name = pairOf(state.name);
|
|
43
|
+
state.following = false;
|
|
44
|
+
},
|
|
45
|
+
followSystem() {
|
|
46
|
+
state.following = true;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Object signal (not primitive) so the `DaisyTheme` literal union survives —
|
|
51
|
+
// `signal<T>` widens primitive literals to plain `string` via `Widen<T>`.
|
|
52
|
+
// Seeded to a sane default; the root <ThemeProvider> re-seeds from the system
|
|
53
|
+
// color scheme + its props on mount.
|
|
54
|
+
const state = signal({
|
|
55
|
+
name: pickThemeFor('light'),
|
|
56
|
+
following: true,
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* The backing signal for the global theme. Read/written by the root
|
|
60
|
+
* `<ThemeProvider>` and shared with `themeController`; not part of the public
|
|
61
|
+
* API.
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export const globalThemeState = state;
|
|
65
|
+
/**
|
|
66
|
+
* The global theme controller — the headless handle for issue #113. Import and
|
|
67
|
+
* call from anywhere (no `<ThemeProvider>` ancestor required); `useTheme()`'s
|
|
68
|
+
* default factory returns this same instance, and the root `<ThemeProvider>`
|
|
69
|
+
* provides it to its subtree. `StatusBarSync` binds to it so the OS bars always
|
|
70
|
+
* follow the global/screen theme, never a content sub-scope.
|
|
71
|
+
*/
|
|
72
|
+
export const themeController = makeThemeController(state);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useScreenTheme(name)` — pin the **global** daisy theme while a navigation
|
|
3
|
+
* screen is focused, restoring the previous selection when it blurs.
|
|
4
|
+
*
|
|
5
|
+
* This is the right tool for *per-screen* theming — "this screen is dark, that
|
|
6
|
+
* one is light." Because it drives the global theme (not a content sub-scope),
|
|
7
|
+
* the OS status/navigation bars follow automatically via `<StatusBarSync>`, so
|
|
8
|
+
* the bar icons stay legible against each screen's background. For recoloring a
|
|
9
|
+
* *region within* a screen without touching the bars, nest a `<ThemeProvider>`
|
|
10
|
+
* instead.
|
|
11
|
+
*
|
|
12
|
+
* Built on `useFocusEffect` from `@sigx/lynx-navigation` (an optional peer
|
|
13
|
+
* dependency): it must be called from inside a component rendered as a route by
|
|
14
|
+
* `<Stack>` / `<Tabs>` — the same constraint as `useFocusEffect`/`useIsFocused`.
|
|
15
|
+
*
|
|
16
|
+
* Save/restore composes with the stack (LIFO focus/blur): pushing a themed
|
|
17
|
+
* screen saves whatever was live, applies its own theme, and restores on pop —
|
|
18
|
+
* including resuming follow-system if that's what was active.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const Gallery = component(() => {
|
|
22
|
+
* useScreenTheme('daisy-dark'); // dark while this screen is on top
|
|
23
|
+
* return () => <view>…</view>;
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { useFocusEffect } from '@sigx/lynx-navigation';
|
|
28
|
+
import { themeController } from './theme-state.js';
|
|
29
|
+
/** Pin the global theme to `name` while this screen is focused; restore on blur. */
|
|
30
|
+
export function useScreenTheme(name) {
|
|
31
|
+
useFocusEffect(() => {
|
|
32
|
+
const prevName = themeController.name;
|
|
33
|
+
const prevFollowing = themeController.followingSystem;
|
|
34
|
+
themeController.set(name);
|
|
35
|
+
return () => {
|
|
36
|
+
if (prevFollowing)
|
|
37
|
+
themeController.followSystem();
|
|
38
|
+
else
|
|
39
|
+
themeController.set(prevName);
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigx/lynx-daisyui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "DaisyUI integration for sigx-lynx",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -37,15 +37,15 @@
|
|
|
37
37
|
"LICENSE"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@sigx/lynx": "^0.4.
|
|
41
|
-
"@sigx/lynx-appearance": "^0.4.
|
|
42
|
-
"@sigx/lynx-
|
|
43
|
-
"@sigx/lynx-
|
|
44
|
-
"@sigx/lynx-
|
|
40
|
+
"@sigx/lynx": "^0.4.4",
|
|
41
|
+
"@sigx/lynx-appearance": "^0.4.4",
|
|
42
|
+
"@sigx/lynx-gestures": "^0.4.4",
|
|
43
|
+
"@sigx/lynx-icons": "^0.4.4",
|
|
44
|
+
"@sigx/lynx-motion": "^0.4.4"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"tailwindcss": "^3.0.0 || ^4.0.0",
|
|
48
|
-
"@sigx/lynx-navigation": "^0.4.
|
|
48
|
+
"@sigx/lynx-navigation": "^0.4.4"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"@sigx/lynx-navigation": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@typescript/native-preview": "7.0.0-dev.20260521.1",
|
|
57
57
|
"tailwindcss": "^4.0.0",
|
|
58
58
|
"typescript": "^6.0.3",
|
|
59
|
-
"@sigx/lynx-navigation": "^0.4.
|
|
59
|
+
"@sigx/lynx-navigation": "^0.4.4"
|
|
60
60
|
},
|
|
61
61
|
"publishConfig": {
|
|
62
62
|
"access": "public"
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"author": "Andreas Ekdahl",
|
|
73
73
|
"license": "MIT",
|
|
74
74
|
"scripts": {
|
|
75
|
-
"build": "node ../../scripts/clean.mjs dist && tsgo && node ../../scripts/copy-assets.mjs src/styles dist/styles",
|
|
75
|
+
"build": "node ../../scripts/clean.mjs dist && tsgo && node ../../scripts/copy-assets.mjs src/styles dist/styles && node scripts/gen-theme-css.mjs",
|
|
76
76
|
"dev": "tsgo --watch",
|
|
77
77
|
"clean": "node ../../scripts/clean.mjs dist .turbo"
|
|
78
78
|
}
|