@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.
@@ -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
+ }
@@ -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 = (typeof DAISY_COLOR_TOKEN_LIST)[number];
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
- /* Typography — text color utilities for semantic colors */
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 color utilities for semantic colors */
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); }
@@ -1,8 +1,12 @@
1
- /* @sigx/lynx-daisyui — all themes + component styles */
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
- /* Themes */
4
- @import './themes/light.css';
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
- /* Combine with a color theme: class="daisy-light daisy-rounded" */
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
- /* DaisyUI Dark Theme overrides only color tokens */
2
- /* Scoped under .daisy-dark; CSS inheritance propagates to descendants */
3
-
4
- .daisy-dark {
5
- /* Apply base colors to the theme root */
6
- background-color: #1d232a;
7
- color: #a6adbb;
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
+ });