@sigx/lynx-daisyui 0.4.1 → 0.4.3
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 +117 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +11 -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 +88 -0
- package/dist/theme/ThemeProvider.d.ts +97 -37
- package/dist/theme/ThemeProvider.js +164 -46
- package/dist/theme/registry.d.ts +101 -0
- package/dist/theme/registry.js +185 -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 -6
- package/dist/styles/themes/light.css +0 -95
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
|
|
|
@@ -143,6 +221,44 @@ For a fully-custom design, build directly on
|
|
|
143
221
|
`useScreenChrome()` from `@sigx/lynx-navigation` — `NavHeader` is just
|
|
144
222
|
one consumer of that hook.
|
|
145
223
|
|
|
224
|
+
### `<SwiperIndicator>`
|
|
225
|
+
|
|
226
|
+
Themed wrapper around the headless `useSwiperDot*` hooks from
|
|
227
|
+
[`@sigx/lynx-gestures`](https://github.com/signalxjs/lynx/tree/main/packages/lynx-gestures#swiper-and-headless-dot-hooks).
|
|
228
|
+
Reads colours from the active daisy theme so the indicator follows light
|
|
229
|
+
/ dark mode automatically.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { Swiper } from '@sigx/lynx-gestures';
|
|
233
|
+
import { SwiperIndicator } from '@sigx/lynx-daisyui';
|
|
234
|
+
|
|
235
|
+
<Swiper offset={offset} index={pageIdx} width={pageWidth}>{pages}</Swiper>
|
|
236
|
+
<SwiperIndicator
|
|
237
|
+
variant="dots"
|
|
238
|
+
count={pages.length}
|
|
239
|
+
offset={offset}
|
|
240
|
+
pageWidth={pageWidth}
|
|
241
|
+
index={pageIdx}
|
|
242
|
+
color="primary"
|
|
243
|
+
onDotPress={(i) => { pageIdx.value = i }}
|
|
244
|
+
/>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| Variant | Animated channel | Notes |
|
|
248
|
+
| ------------- | ---------------------------- | ------------------------------------------------------------------ |
|
|
249
|
+
| `dots` | `opacity` crossfade | Default. Two-colour overlay per dot. |
|
|
250
|
+
| `bar` | `translateX` (single thumb) | One MT binding regardless of page count — cheapest for long lists. |
|
|
251
|
+
| `pill` | `scaleX` + `opacity` | Active dot stretches into a pill while overlay fades in. |
|
|
252
|
+
| `scale-pulse` | uniform `scale` | Monochrome pulse — no colour crossfade. |
|
|
253
|
+
| `numbered` | none (BG-thread text) | Renders `n / total`. Requires `index` signal. |
|
|
254
|
+
|
|
255
|
+
Props: `count`, `offset` (`SharedValue<number>`), `pageWidth`, `index`
|
|
256
|
+
(`PrimitiveSignal<number>`, required for `numbered`), `color`, `inactiveColor`
|
|
257
|
+
(daisy tokens), `size` (`'xs' | 'sm' | 'md' | 'lg'`), `onDotPress`.
|
|
258
|
+
|
|
259
|
+
For a non-standard visual, skip this component and call the headless
|
|
260
|
+
hooks directly — they're the same primitives this component composes.
|
|
261
|
+
|
|
146
262
|
## Layout primitives
|
|
147
263
|
|
|
148
264
|
Daisy's flex primitives (`Center`, `Col`, `Row`) accept a `flex={n}`
|
package/dist/index.d.ts
CHANGED
|
@@ -50,8 +50,16 @@ 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 {
|
|
54
|
-
export type {
|
|
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';
|
|
61
|
+
export { themeController } from './theme/theme-state.js';
|
|
62
|
+
export { useScreenTheme } from './theme/use-screen-theme.js';
|
|
55
63
|
export { Avatar } from './data/Avatar.js';
|
|
56
64
|
export type { AvatarProps, AvatarSize } from './data/Avatar.js';
|
|
57
65
|
export { Text } from './typography/Text.js';
|
package/dist/index.js
CHANGED
|
@@ -29,8 +29,18 @@ 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';
|
|
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';
|
|
34
44
|
// Data
|
|
35
45
|
export { Avatar } from './data/Avatar.js';
|
|
36
46
|
// 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
|
-
/**
|
|
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
|
-
:
|
|
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
|
-
|
|
56
|
-
|
|
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, {}>;
|