@sigx/lynx-daisyui 0.4.1 → 0.4.2
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 +38 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +4 -1
- package/dist/navigation/NavDrawer.d.ts +62 -0
- package/dist/navigation/NavDrawer.js +205 -0
- package/dist/navigation/NavHeader.d.ts +12 -1
- package/dist/navigation/NavHeader.js +13 -1
- package/dist/navigation/NavTabBar.js +34 -2
- package/dist/navigation/SwiperIndicator.d.ts +59 -0
- package/dist/navigation/SwiperIndicator.js +232 -0
- package/dist/shared/styles.d.ts +1 -1
- package/dist/styles/components/typography.css +36 -2
- package/dist/styles/index.css +8 -4
- package/dist/styles/themes/shapes.css +2 -1
- package/dist/styles/themes/{dark.css → tokens.css} +9 -33
- package/dist/theme/StatusBarSync.d.ts +41 -0
- package/dist/theme/StatusBarSync.js +85 -0
- package/dist/theme/ThemeProvider.d.ts +91 -35
- package/dist/theme/ThemeProvider.js +137 -37
- package/dist/theme/registry.d.ts +101 -0
- package/dist/theme/registry.js +185 -0
- package/package.json +9 -6
- package/dist/styles/themes/light.css +0 -95
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@sigx/lynx/jsx-runtime";
|
|
2
|
+
import { component, effect, signal, } from '@sigx/lynx';
|
|
3
|
+
import { useSwiperDotProgress, useSwiperDotScale, useSwiperDotGrowX, useSwiperDotTranslate, } from '@sigx/lynx-gestures';
|
|
4
|
+
import { resolveDaisyColor } from '../shared/styles.js';
|
|
5
|
+
const SIZE_TABLE = {
|
|
6
|
+
xs: { dot: 4, gap: 4, barHeight: 3, fontSize: 11 },
|
|
7
|
+
sm: { dot: 6, gap: 6, barHeight: 4, fontSize: 12 },
|
|
8
|
+
md: { dot: 8, gap: 8, barHeight: 5, fontSize: 14 },
|
|
9
|
+
lg: { dot: 12, gap: 10, barHeight: 6, fontSize: 16 },
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Themed swiper page indicator with five preset variants. Each variant
|
|
13
|
+
* is a thin shell over a headless hook from `@sigx/lynx-gestures` (see
|
|
14
|
+
* `useSwiperDotProgress`, `useSwiperDotScale`, `useSwiperDotGrowX`,
|
|
15
|
+
* `useSwiperDotTranslate`). For a fully custom indicator, compose the
|
|
16
|
+
* hooks yourself rather than forking this file.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const offset = useSharedValue(0);
|
|
21
|
+
* const idx = signal({ value: 0 });
|
|
22
|
+
* <Swiper offset={offset} index={idx} width={W}>…</Swiper>
|
|
23
|
+
* <SwiperIndicator
|
|
24
|
+
* variant="pill"
|
|
25
|
+
* offset={offset}
|
|
26
|
+
* pageWidth={W}
|
|
27
|
+
* count={photos.length}
|
|
28
|
+
* index={idx}
|
|
29
|
+
* color="primary"
|
|
30
|
+
* onDotPress={(i) => { idx.value = i; }}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const SwiperIndicator = component(({ props }) => {
|
|
35
|
+
return () => {
|
|
36
|
+
const variant = props.variant ?? 'dots';
|
|
37
|
+
const size = SIZE_TABLE[props.size ?? 'md'];
|
|
38
|
+
const activeColor = resolveDaisyColor(props.color ?? 'primary');
|
|
39
|
+
const inactiveColor = resolveDaisyColor(props.inactiveColor ?? 'base-content');
|
|
40
|
+
if (variant === 'numbered') {
|
|
41
|
+
return (_jsx(NumberedIndicator, { count: props.count, index: props.index ?? FALLBACK_INDEX, color: activeColor, fontSize: size.fontSize, class: props.class, style: props.style }));
|
|
42
|
+
}
|
|
43
|
+
if (variant === 'bar') {
|
|
44
|
+
if (props.offset == null || props.pageWidth == null)
|
|
45
|
+
return null;
|
|
46
|
+
return (_jsx(BarIndicator, { offset: props.offset, pageWidth: props.pageWidth, count: props.count, activeColor: activeColor, inactiveColor: inactiveColor, barHeight: size.barHeight, dotSize: size.dot, gap: size.gap, onDotPress: props.onDotPress, class: props.class, style: props.style }));
|
|
47
|
+
}
|
|
48
|
+
if (props.offset == null || props.pageWidth == null)
|
|
49
|
+
return null;
|
|
50
|
+
return (_jsx("view", { class: props.class, style: {
|
|
51
|
+
display: 'flex',
|
|
52
|
+
flexDirection: 'row',
|
|
53
|
+
alignItems: 'center',
|
|
54
|
+
justifyContent: 'center',
|
|
55
|
+
gap: size.gap + 'px',
|
|
56
|
+
...(props.style || {}),
|
|
57
|
+
}, children: Array.from({ length: props.count }, (_, i) => (_jsx(Dot, { index: i, offset: props.offset, pageWidth: props.pageWidth, variant: variant, size: size, activeColor: activeColor, inactiveColor: inactiveColor, onPress: props.onDotPress }, i))) }));
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
61
|
+
// Per-variant pieces. Each owns a single `useAnimatedStyle` call-site
|
|
62
|
+
// (per-iteration call inside `.map()` is fine — call-sites are stable).
|
|
63
|
+
const FALLBACK_INDEX = signal({ value: 0 });
|
|
64
|
+
const Dot = component(({ props }) => {
|
|
65
|
+
// Each branch picks a different headless hook. Variants that need
|
|
66
|
+
// *two* simultaneous channels (opacity AND scale, or scale AND scaleX)
|
|
67
|
+
// need two refs — one per element — because `useAnimatedStyle` is
|
|
68
|
+
// one-binding-per-element.
|
|
69
|
+
if (props.variant === 'dots') {
|
|
70
|
+
return DotsBody(props);
|
|
71
|
+
}
|
|
72
|
+
if (props.variant === 'pill') {
|
|
73
|
+
return PillBody(props);
|
|
74
|
+
}
|
|
75
|
+
// scale-pulse
|
|
76
|
+
return ScalePulseBody(props);
|
|
77
|
+
});
|
|
78
|
+
function DotsBody(props) {
|
|
79
|
+
const overlayRef = useSwiperDotProgress({
|
|
80
|
+
offset: props.offset,
|
|
81
|
+
pageWidth: props.pageWidth,
|
|
82
|
+
index: props.index,
|
|
83
|
+
});
|
|
84
|
+
return () => (_jsx("view", { catchtap: props.onPress ? () => props.onPress?.(props.index) : undefined, style: {
|
|
85
|
+
width: props.size.dot + 'px',
|
|
86
|
+
height: props.size.dot + 'px',
|
|
87
|
+
borderRadius: (props.size.dot / 2) + 'px',
|
|
88
|
+
backgroundColor: withAlpha(props.inactiveColor, 0.4),
|
|
89
|
+
position: 'relative',
|
|
90
|
+
overflow: 'hidden',
|
|
91
|
+
}, children: _jsx("view", { "main-thread:ref": overlayRef, style: {
|
|
92
|
+
position: 'absolute',
|
|
93
|
+
left: '0',
|
|
94
|
+
top: '0',
|
|
95
|
+
right: '0',
|
|
96
|
+
bottom: '0',
|
|
97
|
+
backgroundColor: props.activeColor,
|
|
98
|
+
opacity: '0',
|
|
99
|
+
} }) }));
|
|
100
|
+
}
|
|
101
|
+
function PillBody(props) {
|
|
102
|
+
// Pill stretches horizontally via scaleX (no layout cost) and brightens
|
|
103
|
+
// via opacity on the active-colour overlay. Both channels target the
|
|
104
|
+
// same dot — but each needs its own bound element, so we wrap the
|
|
105
|
+
// overlay inside a scaling shell.
|
|
106
|
+
const shellRef = useSwiperDotGrowX({
|
|
107
|
+
offset: props.offset,
|
|
108
|
+
pageWidth: props.pageWidth,
|
|
109
|
+
index: props.index,
|
|
110
|
+
inactive: 1,
|
|
111
|
+
active: 3,
|
|
112
|
+
});
|
|
113
|
+
const overlayRef = useSwiperDotProgress({
|
|
114
|
+
offset: props.offset,
|
|
115
|
+
pageWidth: props.pageWidth,
|
|
116
|
+
index: props.index,
|
|
117
|
+
});
|
|
118
|
+
return () => (_jsx("view", { catchtap: props.onPress ? () => props.onPress?.(props.index) : undefined, "main-thread:ref": shellRef, style: {
|
|
119
|
+
width: props.size.dot + 'px',
|
|
120
|
+
height: props.size.dot + 'px',
|
|
121
|
+
borderRadius: (props.size.dot / 2) + 'px',
|
|
122
|
+
backgroundColor: withAlpha(props.inactiveColor, 0.4),
|
|
123
|
+
position: 'relative',
|
|
124
|
+
overflow: 'hidden',
|
|
125
|
+
transformOrigin: 'center center',
|
|
126
|
+
}, children: _jsx("view", { "main-thread:ref": overlayRef, style: {
|
|
127
|
+
position: 'absolute',
|
|
128
|
+
left: '0',
|
|
129
|
+
top: '0',
|
|
130
|
+
right: '0',
|
|
131
|
+
bottom: '0',
|
|
132
|
+
backgroundColor: props.activeColor,
|
|
133
|
+
opacity: '0',
|
|
134
|
+
} }) }));
|
|
135
|
+
}
|
|
136
|
+
function ScalePulseBody(props) {
|
|
137
|
+
// No colour crossfade — pure scale. Active dot uses `activeColor`,
|
|
138
|
+
// inactive uses `inactiveColor` at low alpha. Visual is monochrome
|
|
139
|
+
// friendly.
|
|
140
|
+
const scaleRef = useSwiperDotScale({
|
|
141
|
+
offset: props.offset,
|
|
142
|
+
pageWidth: props.pageWidth,
|
|
143
|
+
index: props.index,
|
|
144
|
+
inactive: 1,
|
|
145
|
+
active: 1.6,
|
|
146
|
+
});
|
|
147
|
+
const opacityRef = useSwiperDotProgress({
|
|
148
|
+
offset: props.offset,
|
|
149
|
+
pageWidth: props.pageWidth,
|
|
150
|
+
index: props.index,
|
|
151
|
+
});
|
|
152
|
+
return () => (_jsx("view", { catchtap: props.onPress ? () => props.onPress?.(props.index) : undefined, "main-thread:ref": scaleRef, style: {
|
|
153
|
+
width: props.size.dot + 'px',
|
|
154
|
+
height: props.size.dot + 'px',
|
|
155
|
+
borderRadius: (props.size.dot / 2) + 'px',
|
|
156
|
+
backgroundColor: withAlpha(props.inactiveColor, 0.4),
|
|
157
|
+
position: 'relative',
|
|
158
|
+
overflow: 'hidden',
|
|
159
|
+
}, children: _jsx("view", { "main-thread:ref": opacityRef, style: {
|
|
160
|
+
position: 'absolute',
|
|
161
|
+
left: '0',
|
|
162
|
+
top: '0',
|
|
163
|
+
right: '0',
|
|
164
|
+
bottom: '0',
|
|
165
|
+
backgroundColor: props.activeColor,
|
|
166
|
+
opacity: '0',
|
|
167
|
+
} }) }));
|
|
168
|
+
}
|
|
169
|
+
const BarIndicator = component(({ props }) => {
|
|
170
|
+
// The thumb advances by (dot + gap) per page. We use the headless
|
|
171
|
+
// translate hook — a single MT binding regardless of page count.
|
|
172
|
+
const step = props.dotSize + props.gap;
|
|
173
|
+
const thumbRef = useSwiperDotTranslate({
|
|
174
|
+
offset: props.offset,
|
|
175
|
+
pageWidth: props.pageWidth,
|
|
176
|
+
step,
|
|
177
|
+
});
|
|
178
|
+
return () => {
|
|
179
|
+
const trackWidth = props.count * props.dotSize + Math.max(0, props.count - 1) * props.gap;
|
|
180
|
+
return (_jsxs("view", { class: props.class, style: {
|
|
181
|
+
position: 'relative',
|
|
182
|
+
width: trackWidth + 'px',
|
|
183
|
+
height: props.barHeight + 'px',
|
|
184
|
+
borderRadius: (props.barHeight / 2) + 'px',
|
|
185
|
+
backgroundColor: withAlpha(props.inactiveColor, 0.25),
|
|
186
|
+
overflow: 'visible',
|
|
187
|
+
...(props.style || {}),
|
|
188
|
+
}, children: [props.onDotPress
|
|
189
|
+
? (_jsx("view", { style: {
|
|
190
|
+
position: 'absolute',
|
|
191
|
+
inset: '0',
|
|
192
|
+
display: 'flex',
|
|
193
|
+
flexDirection: 'row',
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
}, children: Array.from({ length: props.count }, (_, i) => (_jsx("view", { catchtap: () => props.onDotPress?.(i), style: {
|
|
196
|
+
width: (props.dotSize + props.gap) + 'px',
|
|
197
|
+
height: '100%',
|
|
198
|
+
} }, i))) }))
|
|
199
|
+
: null, _jsx("view", { "main-thread:ref": thumbRef, style: {
|
|
200
|
+
position: 'absolute',
|
|
201
|
+
left: '0',
|
|
202
|
+
top: '0',
|
|
203
|
+
width: props.dotSize + 'px',
|
|
204
|
+
height: '100%',
|
|
205
|
+
borderRadius: (props.barHeight / 2) + 'px',
|
|
206
|
+
backgroundColor: props.activeColor,
|
|
207
|
+
} })] }));
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
const NumberedIndicator = component(({ props }) => {
|
|
211
|
+
const label = signal({ value: '' });
|
|
212
|
+
effect(() => {
|
|
213
|
+
label.value = `${(props.index.value | 0) + 1} / ${props.count}`;
|
|
214
|
+
});
|
|
215
|
+
return () => (_jsx("text", { class: props.class, style: {
|
|
216
|
+
color: props.color,
|
|
217
|
+
fontSize: props.fontSize + 'px',
|
|
218
|
+
fontWeight: '600',
|
|
219
|
+
...(props.style || {}),
|
|
220
|
+
}, children: label.value }));
|
|
221
|
+
});
|
|
222
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
223
|
+
// Helpers
|
|
224
|
+
/**
|
|
225
|
+
* Apply an alpha to a CSS colour value. Works for `var(--color-*)`
|
|
226
|
+
* (uses `color-mix`) and for raw rgb/hex strings (uses `color-mix`
|
|
227
|
+
* too — broadly supported on the platforms Lynx targets).
|
|
228
|
+
*/
|
|
229
|
+
function withAlpha(color, alpha) {
|
|
230
|
+
const pct = Math.round(Math.max(0, Math.min(1, alpha)) * 100);
|
|
231
|
+
return `color-mix(in srgb, ${color} ${pct}%, transparent)`;
|
|
232
|
+
}
|
package/dist/shared/styles.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* removing a token in one place is impossible.
|
|
13
13
|
*/
|
|
14
14
|
declare const DAISY_COLOR_TOKEN_LIST: readonly ['primary', 'primary-content', 'secondary', 'secondary-content', 'accent', 'accent-content', 'neutral', 'neutral-content', 'base-100', 'base-200', 'base-300', 'base-content', 'info', 'info-content', 'success', 'success-content', 'warning', 'warning-content', 'error', 'error-content'];
|
|
15
|
-
export type DaisyColor =
|
|
15
|
+
export type DaisyColor = typeof DAISY_COLOR_TOKEN_LIST[number];
|
|
16
16
|
/**
|
|
17
17
|
* Resolve a `background` prop value to a CSS color string.
|
|
18
18
|
*
|
|
@@ -1,5 +1,30 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/*
|
|
2
|
+
* Daisy semantic color utilities.
|
|
3
|
+
*
|
|
4
|
+
* bg-<token>: every token in DAISY_COLOR_TOKEN_LIST (semantics +
|
|
5
|
+
* their -content variants, plus the three base surfaces
|
|
6
|
+
* and base-content).
|
|
7
|
+
* text-<token>: semantic tokens + their -content variants, plus
|
|
8
|
+
* base-content. The three base surface tokens
|
|
9
|
+
* (base-100/200/300) are deliberately omitted — text the
|
|
10
|
+
* colour of the surface it sits on is never useful, and
|
|
11
|
+
* keeping the class out prevents accidental "invisible
|
|
12
|
+
* text" footguns.
|
|
13
|
+
*
|
|
14
|
+
* Hand-written rules rather than Tailwind-generated utilities, so they
|
|
15
|
+
* survive purge regardless of whether a consumer's source statically
|
|
16
|
+
* references the class. This matters for daisy components that compose
|
|
17
|
+
* the class dynamically — e.g. `class={`bg-${props.background}`}` in
|
|
18
|
+
* `<NavDrawer>` — which Tailwind's source scanner can't see. Without
|
|
19
|
+
* these always-on rules, `<NavDrawer background="primary">` paints
|
|
20
|
+
* transparent because `bg-primary` got purged.
|
|
21
|
+
*
|
|
22
|
+
* Lynx specifics: `var(--color-*)` resolves only from CSS-pipeline rules
|
|
23
|
+
* (this file, daisy themes), never from inline `style.backgroundColor`.
|
|
24
|
+
* Keep daisy surface tokens flowing through a class.
|
|
25
|
+
*/
|
|
2
26
|
|
|
27
|
+
/* Text */
|
|
3
28
|
.text-primary { color: var(--color-primary); }
|
|
4
29
|
.text-primary-content { color: var(--color-primary-content); }
|
|
5
30
|
.text-secondary { color: var(--color-secondary); }
|
|
@@ -18,15 +43,24 @@
|
|
|
18
43
|
.text-error { color: var(--color-error); }
|
|
19
44
|
.text-error-content { color: var(--color-error-content); }
|
|
20
45
|
|
|
21
|
-
/* Background
|
|
46
|
+
/* Background */
|
|
22
47
|
.bg-primary { background-color: var(--color-primary); }
|
|
48
|
+
.bg-primary-content { background-color: var(--color-primary-content); }
|
|
23
49
|
.bg-secondary { background-color: var(--color-secondary); }
|
|
50
|
+
.bg-secondary-content { background-color: var(--color-secondary-content); }
|
|
24
51
|
.bg-accent { background-color: var(--color-accent); }
|
|
52
|
+
.bg-accent-content { background-color: var(--color-accent-content); }
|
|
25
53
|
.bg-neutral { background-color: var(--color-neutral); }
|
|
54
|
+
.bg-neutral-content { background-color: var(--color-neutral-content); }
|
|
26
55
|
.bg-base-100 { background-color: var(--color-base-100); }
|
|
27
56
|
.bg-base-200 { background-color: var(--color-base-200); }
|
|
28
57
|
.bg-base-300 { background-color: var(--color-base-300); }
|
|
58
|
+
.bg-base-content { background-color: var(--color-base-content); }
|
|
29
59
|
.bg-info { background-color: var(--color-info); }
|
|
60
|
+
.bg-info-content { background-color: var(--color-info-content); }
|
|
30
61
|
.bg-success { background-color: var(--color-success); }
|
|
62
|
+
.bg-success-content { background-color: var(--color-success-content); }
|
|
31
63
|
.bg-warning { background-color: var(--color-warning); }
|
|
64
|
+
.bg-warning-content { background-color: var(--color-warning-content); }
|
|
32
65
|
.bg-error { background-color: var(--color-error); }
|
|
66
|
+
.bg-error-content { background-color: var(--color-error-content); }
|
package/dist/styles/index.css
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
/* @sigx/lynx-daisyui —
|
|
1
|
+
/* @sigx/lynx-daisyui — base tokens + component styles.
|
|
2
|
+
*
|
|
3
|
+
* Theme COLORS live in the theme registry (src/theme/registry.ts) and are
|
|
4
|
+
* applied as inline CSS custom properties by <ThemeProvider>; only the
|
|
5
|
+
* theme-agnostic structural tokens ship as CSS, under the `.daisy` base
|
|
6
|
+
* class. */
|
|
2
7
|
|
|
3
|
-
/*
|
|
4
|
-
@import './themes/
|
|
5
|
-
@import './themes/dark.css';
|
|
8
|
+
/* Structural design tokens (.daisy) + composable shape modifiers */
|
|
9
|
+
@import './themes/tokens.css';
|
|
6
10
|
@import './themes/shapes.css';
|
|
7
11
|
|
|
8
12
|
/* Base reset */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Shape variants — override roundness tokens */
|
|
2
|
-
/*
|
|
2
|
+
/* Compose with the base via ThemeProvider's `class` prop, e.g.
|
|
3
|
+
<ThemeProvider class="daisy-rounded"> → host class="daisy daisy-rounded" */
|
|
3
4
|
|
|
4
5
|
.daisy-flat {
|
|
5
6
|
--rounded-box: 0px;
|
|
@@ -1,37 +1,13 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/* Semantic colors */
|
|
10
|
-
--color-primary: #7582ff;
|
|
11
|
-
--color-primary-content: #050617;
|
|
12
|
-
--color-secondary: #ff71cf;
|
|
13
|
-
--color-secondary-content: #190211;
|
|
14
|
-
--color-accent: #00e7d0;
|
|
15
|
-
--color-accent-content: #001210;
|
|
16
|
-
--color-neutral: #2a323c;
|
|
17
|
-
--color-neutral-content: #a6adbb;
|
|
18
|
-
|
|
19
|
-
/* Base colors */
|
|
20
|
-
--color-base-100: #1d232a;
|
|
21
|
-
--color-base-200: #191e24;
|
|
22
|
-
--color-base-300: #343b46;
|
|
23
|
-
--color-base-content: #a6adbb;
|
|
24
|
-
|
|
25
|
-
/* Status colors */
|
|
26
|
-
--color-info: #00b4fa;
|
|
27
|
-
--color-info-content: #000000;
|
|
28
|
-
--color-success: #00a96e;
|
|
29
|
-
--color-success-content: #000000;
|
|
30
|
-
--color-warning: #ffc100;
|
|
31
|
-
--color-warning-content: #000000;
|
|
32
|
-
--color-error: #ff676a;
|
|
33
|
-
--color-error-content: #000000;
|
|
1
|
+
/* Theme-agnostic structural design tokens.
|
|
2
|
+
*
|
|
3
|
+
* Theme COLORS come from the theme registry (src/theme/registry.ts) and are
|
|
4
|
+
* applied as inline CSS custom properties by <ThemeProvider>. These
|
|
5
|
+
* radius / sizing / component tokens are identical across themes, so they
|
|
6
|
+
* ship once here under the `.daisy` base class that <ThemeProvider> puts on
|
|
7
|
+
* its host view; CSS inheritance propagates them to every descendant. A theme
|
|
8
|
+
* may still override roundness via its `radius` field. */
|
|
34
9
|
|
|
10
|
+
.daisy {
|
|
35
11
|
/* ── Roundness ── */
|
|
36
12
|
--rounded-box: 16px;
|
|
37
13
|
--rounded-btn: 8px;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<StatusBarSync />` — keeps the device status-bar (and Android's
|
|
3
|
+
* navigation-bar) tint legible against the active daisyui theme.
|
|
4
|
+
*
|
|
5
|
+
* Reads the current theme via `useTheme()`, looks up its variant in the
|
|
6
|
+
* theme registry, and pushes the appropriate tint to the OS via
|
|
7
|
+
* `@sigx/lynx-appearance`:
|
|
8
|
+
*
|
|
9
|
+
* light theme → dark status-bar icons (legible against light bg)
|
|
10
|
+
* dark theme → light status-bar icons (legible against dark bg)
|
|
11
|
+
*
|
|
12
|
+
* Mount once, inside `<ThemeProvider>`:
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <ThemeProvider>
|
|
16
|
+
* <StatusBarSync />
|
|
17
|
+
* <App />
|
|
18
|
+
* </ThemeProvider>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Renders nothing — it's a side-effect-only component that drives a
|
|
22
|
+
* reactive `effect()` reading `theme.name`. The `matchBackground` prop is
|
|
23
|
+
* reserved for a follow-up that pushes the active theme's
|
|
24
|
+
* `--color-base-100` as the Android system-bar background; today it's a
|
|
25
|
+
* declared no-op so the API surface is stable across the rev that wires
|
|
26
|
+
* CSS-var resolution.
|
|
27
|
+
*/
|
|
28
|
+
import { type Define } from '@sigx/lynx';
|
|
29
|
+
export type StatusBarSyncProps =
|
|
30
|
+
/**
|
|
31
|
+
* Reserved — will (in a follow-up) push the active theme's
|
|
32
|
+
* `--color-base-100` as the Android status- and navigation-bar
|
|
33
|
+
* background. Currently a no-op; the prop ships so consumers can opt
|
|
34
|
+
* in without an API break later. iOS and Android 15+ ignore the
|
|
35
|
+
* background regardless (no equivalent on iOS; edge-to-edge on
|
|
36
|
+
* Android 15+).
|
|
37
|
+
*/
|
|
38
|
+
Define.Prop<'matchBackground', boolean, false>;
|
|
39
|
+
export declare const StatusBarSync: import("@sigx/runtime-core").ComponentFactory<{
|
|
40
|
+
matchBackground?: boolean | undefined;
|
|
41
|
+
}, void, {}>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* `<StatusBarSync />` — keeps the device status-bar (and Android's
|
|
4
|
+
* navigation-bar) tint legible against the active daisyui theme.
|
|
5
|
+
*
|
|
6
|
+
* Reads the current theme via `useTheme()`, looks up its variant in the
|
|
7
|
+
* theme registry, and pushes the appropriate tint to the OS via
|
|
8
|
+
* `@sigx/lynx-appearance`:
|
|
9
|
+
*
|
|
10
|
+
* light theme → dark status-bar icons (legible against light bg)
|
|
11
|
+
* dark theme → light status-bar icons (legible against dark bg)
|
|
12
|
+
*
|
|
13
|
+
* Mount once, inside `<ThemeProvider>`:
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <ThemeProvider>
|
|
17
|
+
* <StatusBarSync />
|
|
18
|
+
* <App />
|
|
19
|
+
* </ThemeProvider>
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Renders nothing — it's a side-effect-only component that drives a
|
|
23
|
+
* reactive `effect()` reading `theme.name`. The `matchBackground` prop is
|
|
24
|
+
* reserved for a follow-up that pushes the active theme's
|
|
25
|
+
* `--color-base-100` as the Android system-bar background; today it's a
|
|
26
|
+
* declared no-op so the API surface is stable across the rev that wires
|
|
27
|
+
* CSS-var resolution.
|
|
28
|
+
*/
|
|
29
|
+
import { component, effect, onMounted, onUnmounted } from '@sigx/lynx';
|
|
30
|
+
import { isAvailable, setSystemBarsStyle } from '@sigx/lynx-appearance';
|
|
31
|
+
import { useTheme } from './ThemeProvider.js';
|
|
32
|
+
import { variantOf } from './registry.js';
|
|
33
|
+
export const StatusBarSync = component(({ props }) => {
|
|
34
|
+
const theme = useTheme();
|
|
35
|
+
let lastApplied = null;
|
|
36
|
+
let runner;
|
|
37
|
+
function apply(name) {
|
|
38
|
+
if (name === lastApplied)
|
|
39
|
+
return;
|
|
40
|
+
lastApplied = name;
|
|
41
|
+
const variant = variantOf(name);
|
|
42
|
+
// For unregistered themes we can't infer a variant — leave the
|
|
43
|
+
// system bars alone. Consumers can register their custom theme via
|
|
44
|
+
// `registerTheme()` to opt in.
|
|
45
|
+
if (!variant)
|
|
46
|
+
return;
|
|
47
|
+
const style = variant === 'dark' ? 'light' : 'dark';
|
|
48
|
+
// Fire-and-forget: `setSystemBarsStyle` is non-throwing (it
|
|
49
|
+
// resolves `{ ok: false, reason: 'unsupported' }` when the native
|
|
50
|
+
// module isn't registered, and silently filters per-leg
|
|
51
|
+
// `unsupported` results — e.g. nav-bar on iOS — so an aggregate
|
|
52
|
+
// `ok: true` is still reachable on partial platforms). Either way,
|
|
53
|
+
// void-discarding the promise here can't surface as an unhandled
|
|
54
|
+
// rejection.
|
|
55
|
+
void setSystemBarsStyle({
|
|
56
|
+
statusBar: style,
|
|
57
|
+
navigationBar: { style },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
onMounted(() => {
|
|
61
|
+
if (!isAvailable())
|
|
62
|
+
return;
|
|
63
|
+
// A reactive effect that reads `theme.name` so the effect re-runs
|
|
64
|
+
// whenever the theme controller's underlying signal changes —
|
|
65
|
+
// including the live system-flip path inside ThemeProvider. No
|
|
66
|
+
// side effects in render; nothing to subscribe/unsubscribe by
|
|
67
|
+
// hand.
|
|
68
|
+
runner = effect(() => {
|
|
69
|
+
apply(theme.name);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
onUnmounted(() => {
|
|
73
|
+
runner?.stop();
|
|
74
|
+
runner = undefined;
|
|
75
|
+
});
|
|
76
|
+
// Reference the prop so the type checker doesn't flag it as unused
|
|
77
|
+
// while it's still reserved. Drop this when the matchBackground
|
|
78
|
+
// implementation lands.
|
|
79
|
+
void props.matchBackground;
|
|
80
|
+
// Zero-size, out-of-flow placeholder. Avoids `display: none` —
|
|
81
|
+
// Lynx can leak unstyled text paint through display:none overlays in
|
|
82
|
+
// some builds (see lynx-display-none caveat); zero-size + absolute is
|
|
83
|
+
// the safer shape.
|
|
84
|
+
return () => (_jsx("view", { style: { position: 'absolute', width: '0px', height: '0px', opacity: 0 } }));
|
|
85
|
+
});
|