@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 CHANGED
@@ -143,6 +143,44 @@ For a fully-custom design, build directly on
143
143
  `useScreenChrome()` from `@sigx/lynx-navigation` — `NavHeader` is just
144
144
  one consumer of that hook.
145
145
 
146
+ ### `<SwiperIndicator>`
147
+
148
+ Themed wrapper around the headless `useSwiperDot*` hooks from
149
+ [`@sigx/lynx-gestures`](../lynx-gestures#swiper-and-headless-dot-hooks).
150
+ Reads colours from the active daisy theme so the indicator follows light
151
+ / dark mode automatically.
152
+
153
+ ```tsx
154
+ import { Swiper } from '@sigx/lynx-gestures';
155
+ import { SwiperIndicator } from '@sigx/lynx-daisyui';
156
+
157
+ <Swiper offset={offset} index={pageIdx} width={pageWidth}>{pages}</Swiper>
158
+ <SwiperIndicator
159
+ variant="dots"
160
+ count={pages.length}
161
+ offset={offset}
162
+ pageWidth={pageWidth}
163
+ index={pageIdx}
164
+ color="primary"
165
+ onDotPress={(i) => { pageIdx.value = i }}
166
+ />
167
+ ```
168
+
169
+ | Variant | Animated channel | Notes |
170
+ | ------------- | ---------------------------- | ------------------------------------------------------------------ |
171
+ | `dots` | `opacity` crossfade | Default. Two-colour overlay per dot. |
172
+ | `bar` | `translateX` (single thumb) | One MT binding regardless of page count — cheapest for long lists. |
173
+ | `pill` | `scaleX` + `opacity` | Active dot stretches into a pill while overlay fades in. |
174
+ | `scale-pulse` | uniform `scale` | Monochrome pulse — no colour crossfade. |
175
+ | `numbered` | none (BG-thread text) | Renders `n / total`. Requires `index` signal. |
176
+
177
+ Props: `count`, `offset` (`SharedValue<number>`), `pageWidth`, `index`
178
+ (`PrimitiveSignal<number>`, required for `numbered`), `color`, `inactiveColor`
179
+ (daisy tokens), `size` (`'xs' | 'sm' | 'md' | 'lg'`), `onDotPress`.
180
+
181
+ For a non-standard visual, skip this component and call the headless
182
+ hooks directly — they're the same primitives this component composes.
183
+
146
184
  ## Layout primitives
147
185
 
148
186
  Daisy's flex primitives (`Center`, `Col`, `Row`) accept a `flex={n}`
package/dist/index.d.ts CHANGED
@@ -50,8 +50,14 @@ export { NavTabBar } from './navigation/NavTabBar.js';
50
50
  export type { NavTabBarProps, NavTabBarPosition, NavTabBarBackground, NavTabRenderContext, } from './navigation/NavTabBar.js';
51
51
  export { NavHeader } from './navigation/NavHeader.js';
52
52
  export type { NavHeaderProps, NavHeaderBackground, } from './navigation/NavHeader.js';
53
- export { ThemeProvider, useTheme } from './theme/ThemeProvider.js';
54
- export type { DaisyTheme, ThemeController, ThemeProviderProps, } from './theme/ThemeProvider.js';
53
+ export { NavDrawer } from './navigation/NavDrawer.js';
54
+ export type { NavDrawerProps, NavDrawerSide, } from './navigation/NavDrawer.js';
55
+ export { SwiperIndicator } from './navigation/SwiperIndicator.js';
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';
59
+ export { StatusBarSync } from './theme/StatusBarSync.js';
60
+ export type { StatusBarSyncProps } from './theme/StatusBarSync.js';
55
61
  export { Avatar } from './data/Avatar.js';
56
62
  export type { AvatarProps, AvatarSize } from './data/Avatar.js';
57
63
  export { Text } from './typography/Text.js';
package/dist/index.js CHANGED
@@ -29,8 +29,11 @@ export { Steps } from './feedback/Steps.js';
29
29
  export { Tabs } from './navigation/Tabs.js';
30
30
  export { NavTabBar } from './navigation/NavTabBar.js';
31
31
  export { NavHeader } from './navigation/NavHeader.js';
32
+ export { NavDrawer } from './navigation/NavDrawer.js';
33
+ export { SwiperIndicator } from './navigation/SwiperIndicator.js';
32
34
  // Theme
33
- export { ThemeProvider, useTheme } from './theme/ThemeProvider.js';
35
+ export { ThemeProvider, useTheme, listThemes, registerTheme, extendTheme, pickThemeFor, pairOf, variantOf, colorsOf, radiusOf, } from './theme/ThemeProvider.js';
36
+ export { StatusBarSync } from './theme/StatusBarSync.js';
34
37
  // Data
35
38
  export { Avatar } from './data/Avatar.js';
36
39
  // Typography
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `<NavDrawer>` — daisy-themed off-canvas drawer for `@sigx/lynx-navigation`.
3
+ *
4
+ * Composes the primitive `<Drawer>` purely as the state provider (so
5
+ * `useDrawer()` resolves for descendants) and drives its own
6
+ * `SharedValue`-backed slide + fade transition via `@sigx/lynx-motion`.
7
+ *
8
+ * Behavior:
9
+ * - Panel translates from off-screen on the configured `side` to `0`
10
+ * on open (and back on close). Default side is `'left'`.
11
+ * - Backdrop fades 0 → 0.3 in tandem.
12
+ * - Chrome mounts on open and unmounts after the exit animation completes,
13
+ * so the closed-state drawer doesn't intercept taps to underlying tabs.
14
+ * - Backdrop is a plain `<view bindtap>` — no Pressable scale/opacity
15
+ * feedback (which flickers an opaque scrim).
16
+ *
17
+ * Usage:
18
+ *
19
+ * ```tsx
20
+ * <NavigationRoot routes={routes}>
21
+ * <NavDrawer slots={{ sidebar: () => <MyMenu /> }}>
22
+ * <Stack />
23
+ * </NavDrawer>
24
+ * </NavigationRoot>
25
+ * ```
26
+ *
27
+ * Inside descendants, `useDrawer()` from `@sigx/lynx-navigation` returns
28
+ * `{ isOpen, open, close, toggle }`.
29
+ *
30
+ * The primitive's own `<Drawer />` is intentionally minimal (state +
31
+ * `display: none` overlay only); this component is the
32
+ * batteries-included variant for daisyui consumers.
33
+ */
34
+ import { type Define, type JSXElement } from '@sigx/lynx';
35
+ import { type BackgroundValue } from '../shared/styles.js';
36
+ export type NavDrawerSide = 'left' | 'right';
37
+ export type NavDrawerProps =
38
+ /** Which edge the panel slides in from. Default 'left'. */
39
+ Define.Prop<'side', NavDrawerSide, false>
40
+ /** Panel surface color. Accepts daisy tokens ('base-100', 'primary', …)
41
+ * — applied as a `bg-<token>` Tailwind class so the daisy preset's
42
+ * CSS-pipeline rule resolves the `var(--color-<token>)`. Also accepts
43
+ * raw CSS color strings ('#facc15', 'rgb(...)') — applied as inline
44
+ * `backgroundColor`. Default 'base-100'. */
45
+ & Define.Prop<'background', BackgroundValue, false>
46
+ /** Show a separator line on the panel's inner edge. Default true. */
47
+ & Define.Prop<'bordered', boolean, false>
48
+ /** Render a dismiss-on-tap scrim over the main content when open. Default true. */
49
+ & Define.Prop<'backdrop', boolean, false>
50
+ /** Panel width in pixels. Default 280. */
51
+ & Define.Prop<'width', number, false>
52
+ /** Open the drawer at mount. Default false. Passthrough to primitive `<Drawer>`. */
53
+ & Define.Prop<'initialOpen', boolean, false>
54
+ /** Drawer panel contents — your menu UI. */
55
+ & Define.Slot<'sidebar'>
56
+ /** Main content — usually a `<Stack>` or `<Tabs>`. */
57
+ & Define.Slot<'default'>;
58
+ export declare const NavDrawer: import("@sigx/runtime-core").ComponentFactory<NavDrawerProps, void, {
59
+ sidebar: () => JSXElement | JSXElement[] | null;
60
+ } & {
61
+ default: () => JSXElement | JSXElement[] | null;
62
+ }>;
@@ -0,0 +1,205 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@sigx/lynx/jsx-runtime";
2
+ /**
3
+ * `<NavDrawer>` — daisy-themed off-canvas drawer for `@sigx/lynx-navigation`.
4
+ *
5
+ * Composes the primitive `<Drawer>` purely as the state provider (so
6
+ * `useDrawer()` resolves for descendants) and drives its own
7
+ * `SharedValue`-backed slide + fade transition via `@sigx/lynx-motion`.
8
+ *
9
+ * Behavior:
10
+ * - Panel translates from off-screen on the configured `side` to `0`
11
+ * on open (and back on close). Default side is `'left'`.
12
+ * - Backdrop fades 0 → 0.3 in tandem.
13
+ * - Chrome mounts on open and unmounts after the exit animation completes,
14
+ * so the closed-state drawer doesn't intercept taps to underlying tabs.
15
+ * - Backdrop is a plain `<view bindtap>` — no Pressable scale/opacity
16
+ * feedback (which flickers an opaque scrim).
17
+ *
18
+ * Usage:
19
+ *
20
+ * ```tsx
21
+ * <NavigationRoot routes={routes}>
22
+ * <NavDrawer slots={{ sidebar: () => <MyMenu /> }}>
23
+ * <Stack />
24
+ * </NavDrawer>
25
+ * </NavigationRoot>
26
+ * ```
27
+ *
28
+ * Inside descendants, `useDrawer()` from `@sigx/lynx-navigation` returns
29
+ * `{ isOpen, open, close, toggle }`.
30
+ *
31
+ * The primitive's own `<Drawer />` is intentionally minimal (state +
32
+ * `display: none` overlay only); this component is the
33
+ * batteries-included variant for daisyui consumers.
34
+ */
35
+ import { component, effect, onUnmounted, runOnMainThread, signal, untrack, useAnimatedStyle, useMainThreadRef, useSharedValue, } from '@sigx/lynx';
36
+ import { withTiming } from '@sigx/lynx-motion';
37
+ import { Drawer, useDrawer } from '@sigx/lynx-navigation';
38
+ import { resolveDaisyColor } from '../shared/styles.js';
39
+ /**
40
+ * Slide-in / fade-in timing. Slightly longer than the slide-out so the
41
+ * drawer feels deliberate on open and snappy on dismiss — matches the
42
+ * convention used by Stack's push/pop transitions in `lynx-navigation`.
43
+ */
44
+ const ENTER_DURATION_SEC = 0.28;
45
+ const EXIT_DURATION_SEC = 0.22;
46
+ const EXIT_DURATION_MS = Math.round(EXIT_DURATION_SEC * 1000);
47
+ const BACKDROP_OPACITY = 0.3;
48
+ export const NavDrawer = component(({ props, slots }) => {
49
+ return () => (_jsx(Drawer, { initialOpen: props.initialOpen, children: _jsx(NavDrawerShell, { side: props.side ?? 'left', background: props.background ?? 'base-100', bordered: props.bordered ?? true, backdrop: props.backdrop ?? true, width: props.width ?? 280, renderSidebar: slots.sidebar, children: slots.default?.() }) }));
50
+ });
51
+ const NavDrawerShell = component(({ props, slots }) => {
52
+ const drawer = useDrawer();
53
+ // Seed progress from current open state so `initialOpen=true` mounts
54
+ // already-open without a slide-in flash.
55
+ const progress = useSharedValue(drawer.isOpen ? 1 : 0);
56
+ const shouldRender = signal(drawer.isOpen);
57
+ // Track whether the chrome is currently mounted (or animating out) so the
58
+ // initial effect tick on a closed drawer doesn't kick a no-op close
59
+ // animation + unmount timer.
60
+ let chromeMounted = drawer.isOpen;
61
+ let exitTimer = null;
62
+ // Pre-register the worklets at setup so the SWC main-thread transform
63
+ // captures `progress` once. Re-registering on every effect tick would
64
+ // re-ship the worklet body across the bridge unnecessarily.
65
+ const openAnim = runOnMainThread(() => {
66
+ 'main thread';
67
+ withTiming(progress, 1, { duration: ENTER_DURATION_SEC });
68
+ });
69
+ const closeAnim = runOnMainThread(() => {
70
+ 'main thread';
71
+ withTiming(progress, 0, { duration: EXIT_DURATION_SEC });
72
+ });
73
+ const animRunner = effect(() => {
74
+ const open = drawer.isOpen;
75
+ if (open) {
76
+ if (exitTimer != null) {
77
+ clearTimeout(exitTimer);
78
+ exitTimer = null;
79
+ }
80
+ chromeMounted = true;
81
+ untrack(() => {
82
+ shouldRender.value = true;
83
+ });
84
+ openAnim();
85
+ }
86
+ else if (chromeMounted) {
87
+ chromeMounted = false;
88
+ closeAnim();
89
+ // Wait for the exit animation to finish before unmounting the
90
+ // chrome — otherwise the panel pops out instead of sliding,
91
+ // and the backdrop's bindtap area disappears mid-fade.
92
+ exitTimer = setTimeout(() => {
93
+ untrack(() => {
94
+ shouldRender.value = false;
95
+ });
96
+ exitTimer = null;
97
+ }, EXIT_DURATION_MS);
98
+ }
99
+ // else: drawer is closed and the chrome was never mounted (the
100
+ // common initial-mount case) — nothing to animate or schedule.
101
+ });
102
+ onUnmounted(() => {
103
+ animRunner.stop();
104
+ if (exitTimer != null)
105
+ clearTimeout(exitTimer);
106
+ });
107
+ return () => {
108
+ return (_jsxs("view", { style: {
109
+ display: 'flex',
110
+ flexDirection: 'column',
111
+ position: 'relative',
112
+ width: '100%',
113
+ height: '100%',
114
+ }, children: [slots.default?.(), shouldRender.value
115
+ ? (_jsx(DrawerChrome
116
+ // Key by side+width — `useAnimatedStyle`
117
+ // snapshots `outputRange` at setup, so a
118
+ // runtime change to either (panel slide
119
+ // distance is signed by side, magnitude by
120
+ // width) needs a remount + rebind. Width
121
+ // changes mid-open are vanishingly rare;
122
+ // toggling `side` likewise. The explicit
123
+ // remount keeps the binding consistent if
124
+ // a consumer wires either to a reactive
125
+ // value.
126
+ , { side: props.side, progress: progress, width: props.width, background: props.background, bordered: props.bordered, backdrop: props.backdrop, renderSidebar: props.renderSidebar, onBackdropPress: () => drawer.close() }, `drawer-chrome-${props.side}-${props.width}`))
127
+ : null] }));
128
+ };
129
+ });
130
+ const DrawerChrome = component(({ props }) => {
131
+ const panelRef = useMainThreadRef(null);
132
+ const backdropRef = useMainThreadRef(null);
133
+ // Slide range mirrors `side`: left-side starts at `-width` (off-screen
134
+ // left) and lands at `0`; right-side starts at `+width` and lands at `0`.
135
+ // Capture once — NavDrawerShell remounts on side/width change to rebind.
136
+ const closedTx = props.side === 'right' ? props.width : -props.width;
137
+ // Bind once at setup. `useAnimatedStyle` snapshots its mapper/range
138
+ // params at registration time; NavDrawerShell keys DrawerChrome by
139
+ // side+width so a change to either forces a remount + rebind here.
140
+ useAnimatedStyle(panelRef, props.progress, 'translateX', {
141
+ inputRange: [0, 1],
142
+ outputRange: [closedTx, 0],
143
+ });
144
+ // Register unconditionally so a runtime `backdrop` toggle works
145
+ // both directions. `useAnimatedStyle` only binds once at setup; if
146
+ // this lived inside `if (props.backdrop)` a false→true toggle would
147
+ // mount a backdrop view with no opacity binding, leaving it stuck
148
+ // at the inline `opacity: 0` seed. When the backdrop view isn't
149
+ // rendered, `backdropRef.current` is null and the MT bridge's
150
+ // `setStyleProperties` apply silently skips — no harm.
151
+ useAnimatedStyle(backdropRef, props.progress, 'opacity', {
152
+ inputRange: [0, 1],
153
+ outputRange: [0, BACKDROP_OPACITY],
154
+ });
155
+ return () => {
156
+ const isRight = props.side === 'right';
157
+ // Lynx resolves `var(--color-*)` inside CSS-pipeline rules (Tailwind
158
+ // classes, stylesheet imports) but NOT inside inline `style.backgroundColor`
159
+ // — an inline `'var(--color-base-100)'` paints transparent. So for known
160
+ // daisy tokens we apply the surface via the Tailwind class `bg-<token>`
161
+ // (which the daisy preset compiles to a `var()` rule that DOES resolve);
162
+ // raw CSS strings ('#facc15', 'rgb(...)', 'var(--my-custom)') fall through
163
+ // to inline because there's no compiled class to use for them.
164
+ const resolved = resolveDaisyColor(props.background);
165
+ const isDaisyToken = resolved !== props.background;
166
+ const bgClass = isDaisyToken ? `bg-${props.background}` : '';
167
+ // Border lives on the panel's *inner* edge (the one facing the
168
+ // main content). Daisy class names are still the cleanest way to
169
+ // pick up `--color-base-300` for the separator hairline.
170
+ const borderClass = props.bordered
171
+ ? (isRight ? 'border-l border-base-300' : 'border-r border-base-300')
172
+ : '';
173
+ const panelClass = [bgClass, borderClass].filter(Boolean).join(' ');
174
+ const panelStyle = {
175
+ position: 'absolute',
176
+ top: 0,
177
+ bottom: 0,
178
+ width: props.width,
179
+ };
180
+ if (!isDaisyToken)
181
+ panelStyle.backgroundColor = props.background;
182
+ // Only the side-relevant inset is set; omitting the other lets
183
+ // the panel size to `width` rather than stretching edge-to-edge.
184
+ if (isRight)
185
+ panelStyle.right = 0;
186
+ else
187
+ panelStyle.left = 0;
188
+ return (_jsxs("view", { style: {
189
+ position: 'absolute',
190
+ top: 0,
191
+ left: 0,
192
+ right: 0,
193
+ bottom: 0,
194
+ }, children: [props.backdrop
195
+ ? (_jsx("view", { "main-thread:ref": backdropRef, bindtap: () => props.onBackdropPress(), class: "bg-base-content", style: {
196
+ position: 'absolute',
197
+ top: 0,
198
+ left: 0,
199
+ right: 0,
200
+ bottom: 0,
201
+ opacity: 0,
202
+ }, "accessibility-element": true, "accessibility-label": "Close drawer", "accessibility-trait": "button" }))
203
+ : null, _jsx("view", { "main-thread:ref": panelRef, class: panelClass, style: panelStyle, children: props.renderSidebar?.() })] }));
204
+ };
205
+ });
@@ -19,13 +19,24 @@
19
19
  * for daisyui consumers.
20
20
  */
21
21
  import { type Define, type JSXElement } from '@sigx/lynx';
22
+ import { type IconSpec } from '@sigx/lynx-icons';
22
23
  export type NavHeaderBackground = 'base-100' | 'base-200' | 'base-300' | 'transparent';
23
24
  export type NavHeaderProps =
24
25
  /** Surface color token. Default 'base-200'. */
25
26
  Define.Prop<'background', NavHeaderBackground, false>
26
27
  /** Show a separator line at the bottom. Default true. */
27
28
  & Define.Prop<'bordered', boolean, false>
28
- /** Replace the back button entirely. Receives `pop` so the custom node can wire its own tap handler. */
29
+ /**
30
+ * Render the back chevron from an `IconSpec` (e.g. `{ set: 'lucide',
31
+ * name: 'chevron-left' }`). The icon is rendered with
32
+ * `variant="primary"`, which `<ThemeProvider>`'s color resolver maps
33
+ * to the daisy primary hex and substitutes into the SVG `fill=`.
34
+ * Wrapped in a Pressable wired to the stack's pop. Falls back to the
35
+ * default "‹ Back" text when not provided. Ignored when `renderBack`
36
+ * or `<Screen.HeaderLeft>` is also supplied — those win.
37
+ */
38
+ & Define.Prop<'backIcon', IconSpec, false>
39
+ /** Full override: render any JSX for the back button. Takes priority over `backIcon`. */
29
40
  & Define.Prop<'renderBack', (ctx: {
30
41
  pop: () => void;
31
42
  }) => JSXElement, false>;
@@ -21,8 +21,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "@sigx/lynx/jsx-runtime";
21
21
  */
22
22
  import { component } from '@sigx/lynx';
23
23
  import { Pressable } from '@sigx/lynx-gestures';
24
+ import { Icon } from '@sigx/lynx-icons';
24
25
  import { useScreenChrome } from '@sigx/lynx-navigation';
25
26
  import { PRESSED_SCALE, PRESSED_OPACITY } from '../shared/press.js';
27
+ /** Pixel size used when rendering the back-button icon from an `IconSpec`. */
28
+ const NAV_HEADER_ICON_SIZE = 22;
26
29
  const backgroundClass = {
27
30
  'base-100': 'bg-base-100',
28
31
  'base-200': 'bg-base-200',
@@ -47,11 +50,17 @@ export const NavHeader = component(({ props }) => {
47
50
  bg,
48
51
  borderClass,
49
52
  ].filter(Boolean).join(' ');
53
+ // Resolution order: <Screen.HeaderLeft> slot fill → custom renderBack
54
+ // → backIcon spec → default text. The spec path renders `<Icon>`
55
+ // with `variant="primary"`, which the daisy resolver maps to the
56
+ // primary hex and substitutes into the SVG `fill=`.
50
57
  const left = chrome.headerLeft?.()
51
58
  ?? (chrome.canGoBack
52
59
  ? (props.renderBack
53
60
  ? props.renderBack({ pop: chrome.pop })
54
- : _jsx(DefaultBackButton, { onPress: chrome.pop }))
61
+ : (props.backIcon
62
+ ? _jsx(BackIconButton, { spec: props.backIcon, onPress: chrome.pop })
63
+ : _jsx(DefaultBackButton, { onPress: chrome.pop })))
55
64
  : null);
56
65
  const right = chrome.headerRight?.() ?? null;
57
66
  return (_jsxs("view", { class: containerClass, children: [_jsx("view", { class: "flex flex-row items-center", style: { minWidth: 56 }, children: left }), _jsx("view", { class: "flex-1 items-center justify-center", children: _jsx("text", { class: "text-base-content text-base font-semibold", children: chrome.title }) }), _jsx("view", { class: "flex flex-row items-center justify-end", style: { minWidth: 56 }, children: right })] }));
@@ -60,3 +69,6 @@ export const NavHeader = component(({ props }) => {
60
69
  const DefaultBackButton = component(({ props }) => {
61
70
  return () => (_jsx(Pressable, { class: "px-2 py-2", pressedScale: PRESSED_SCALE, pressedOpacity: PRESSED_OPACITY, longPressDuration: 0, "accessibility-element": true, "accessibility-label": "Back", "accessibility-trait": "button", onPress: () => props.onPress(), children: _jsx("text", { class: "text-primary text-base", children: "\u2039 Back" }) }));
62
71
  });
72
+ const BackIconButton = component(({ props }) => {
73
+ return () => (_jsx(Pressable, { class: "px-2 py-2", pressedScale: PRESSED_SCALE, pressedOpacity: PRESSED_OPACITY, longPressDuration: 0, "accessibility-element": true, "accessibility-label": "Back", "accessibility-trait": "button", onPress: () => props.onPress(), children: _jsx(Icon, { set: props.spec.set, name: props.spec.name, size: NAV_HEADER_ICON_SIZE, variant: "primary" }) }));
74
+ });
@@ -16,8 +16,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "@sigx/lynx/jsx-runtime";
16
16
  */
17
17
  import { component } from '@sigx/lynx';
18
18
  import { Pressable } from '@sigx/lynx-gestures';
19
+ import { Icon } from '@sigx/lynx-icons';
19
20
  import { useTabs } from '@sigx/lynx-navigation';
20
21
  import { PRESSED_SCALE, PRESSED_OPACITY } from '../shared/press.js';
22
+ /** Narrow `TabInfo.icon` to its `IconSpec` variant — the bar renders `<Icon>` for these. */
23
+ const isIconSpec = (v) => typeof v === 'object' && v !== null && 'set' in v && 'name' in v
24
+ && typeof v.set === 'string'
25
+ && typeof v.name === 'string';
21
26
  const backgroundClass = {
22
27
  'base-100': 'bg-base-100',
23
28
  'base-200': 'bg-base-200',
@@ -48,11 +53,38 @@ export const NavTabBar = component(({ props }) => {
48
53
  }) }));
49
54
  };
50
55
  });
56
+ /**
57
+ * Pixel size the bar uses when rendering `<Icon>` from an `IconSpec`.
58
+ * Matches the default tab-row height visually.
59
+ */
60
+ const TAB_ICON_SIZE = 22;
51
61
  const DefaultNavTab = component(({ props }) => {
52
62
  return () => {
53
63
  const label = props.info.label ?? props.info.name;
54
64
  const a11y = props.info.accessibilityLabel ?? label;
55
- const textColor = props.active ? 'text-primary font-semibold' : 'text-base-content opacity-60';
56
- return (_jsxs(Pressable, { class: "flex-1 items-center justify-center py-3", pressedScale: PRESSED_SCALE, pressedOpacity: PRESSED_OPACITY, longPressDuration: 0, "accessibility-element": true, "accessibility-label": a11y, "accessibility-trait": "button", "accessibility-status": props.active ? 'selected' : undefined, onPress: () => props.onPress(), children: [props.info.icon ?? null, _jsx("text", { class: `text-sm ${textColor}`, children: label })] }));
65
+ // Label uses native CSS color via daisy `text-*` classes — Lynx's
66
+ // `<text>` honors color inheritance normally. The icon path below
67
+ // can't rely on the same trick (see comment there for why).
68
+ const labelTone = props.active ? 'text-primary' : 'text-base-content opacity-60';
69
+ const weight = props.active ? 'font-semibold' : '';
70
+ const icon = props.info.icon;
71
+ // For an `IconSpec`, render `<Icon>` with the matching daisy
72
+ // variant. `<ThemeProvider>`'s color resolver maps `'primary'` /
73
+ // `'base-content'` (etc.) to the current theme's hex value, which
74
+ // `<Icon>` substitutes directly into the SVG `fill=` attribute —
75
+ // Lynx's `<svg content=…>` parses inline SVG in isolation and
76
+ // doesn't inherit host `color`, so class-based theming doesn't
77
+ // reach the SVG content. Inactive layers `opacity-60` as a class
78
+ // on the outer element (opacity does propagate to the raster).
79
+ //
80
+ // For a `JSXElement`, the consumer is in charge of styling — we
81
+ // leave it untouched. They can opt into the same theming by
82
+ // passing `variant="primary"` themselves.
83
+ const iconVariant = props.active ? 'primary' : 'base-content';
84
+ const iconClass = props.active ? undefined : 'opacity-60';
85
+ const renderedIcon = isIconSpec(icon)
86
+ ? _jsx(Icon, { set: icon.set, name: icon.name, size: TAB_ICON_SIZE, variant: iconVariant, class: iconClass })
87
+ : (icon ?? null);
88
+ return (_jsxs(Pressable, { class: "flex-1 items-center justify-center py-3", pressedScale: PRESSED_SCALE, pressedOpacity: PRESSED_OPACITY, longPressDuration: 0, "accessibility-element": true, "accessibility-label": a11y, "accessibility-trait": "button", "accessibility-status": props.active ? 'selected' : undefined, onPress: () => props.onPress(), children: [renderedIcon, _jsx("text", { class: `text-sm ${labelTone} ${weight}`, children: label })] }));
57
89
  };
58
90
  });
@@ -0,0 +1,59 @@
1
+ import { type Define, type PrimitiveSignal, type SharedValue } from '@sigx/lynx';
2
+ import { type DaisyColor } from '../shared/styles.js';
3
+ /**
4
+ * Visual style for the swiper page indicator.
5
+ *
6
+ * - `dots` — equally-spaced circles, the active one fades in via opacity.
7
+ * Today's default. Cheap (opacity-only MT mapper, no layout each frame).
8
+ * - `bar` — fixed track with a single sliding thumb. Single MT binding
9
+ * regardless of page count, so cheapest for very long carousels.
10
+ * - `pill` — the active dot stretches horizontally into a pill while
11
+ * neighbours stay circular. Uses `scaleX` so siblings don't reflow.
12
+ * - `numbered` — text counter like `2 / 5`. Pure BG-thread, no animation.
13
+ * - `scale-pulse` — circles where the active one scales up. No colour
14
+ * crossfade — pairs well with monochrome palettes.
15
+ */
16
+ export type SwiperIndicatorVariant = 'dots' | 'bar' | 'pill' | 'numbered' | 'scale-pulse';
17
+ export type SwiperIndicatorSize = 'xs' | 'sm' | 'md' | 'lg';
18
+ export type SwiperIndicatorProps = Define.Prop<'variant', SwiperIndicatorVariant, false>
19
+ /** Live MT pixel offset from the parent `<Swiper>`. Required for all animated variants. */
20
+ & Define.Prop<'offset', SharedValue<number>, false>
21
+ /** Page width in CSS px. Must match the Swiper's effective page width. */
22
+ & Define.Prop<'pageWidth', number, false>
23
+ /** Total page count. */
24
+ & Define.Prop<'count', number, true>
25
+ /**
26
+ * Current page (whole-units). Required for `numbered`, used by `bar`
27
+ * as fallback when `offset` isn't wired, and consumed by all variants
28
+ * for tap-to-jump.
29
+ */
30
+ & Define.Prop<'index', PrimitiveSignal<number>, false> & Define.Prop<'color', DaisyColor, false> & Define.Prop<'inactiveColor', DaisyColor, false> & Define.Prop<'size', SwiperIndicatorSize, false>
31
+ /**
32
+ * Tap-to-jump handler. The receiver should typically write
33
+ * `index.value = i` to glide the swiper to that page.
34
+ */
35
+ & Define.Prop<'onDotPress', (index: number) => void, false> & Define.Prop<'class', string, false> & Define.Prop<'style', Record<string, string | number>, false>;
36
+ /**
37
+ * Themed swiper page indicator with five preset variants. Each variant
38
+ * is a thin shell over a headless hook from `@sigx/lynx-gestures` (see
39
+ * `useSwiperDotProgress`, `useSwiperDotScale`, `useSwiperDotGrowX`,
40
+ * `useSwiperDotTranslate`). For a fully custom indicator, compose the
41
+ * hooks yourself rather than forking this file.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * const offset = useSharedValue(0);
46
+ * const idx = signal({ value: 0 });
47
+ * <Swiper offset={offset} index={idx} width={W}>…</Swiper>
48
+ * <SwiperIndicator
49
+ * variant="pill"
50
+ * offset={offset}
51
+ * pageWidth={W}
52
+ * count={photos.length}
53
+ * index={idx}
54
+ * color="primary"
55
+ * onDotPress={(i) => { idx.value = i; }}
56
+ * />
57
+ * ```
58
+ */
59
+ export declare const SwiperIndicator: import("@sigx/runtime-core").ComponentFactory<SwiperIndicatorProps, void, {}>;