@sigx/zero-daisyui 0.9.0 → 0.11.0

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/src/recipes.ts CHANGED
@@ -148,6 +148,14 @@ const roleInk = (role: string): string =>
148
148
  + `${ROLE_INK_KEEP[role as keyof typeof roles] ?? 55}%, var(--color-base-content))`;
149
149
 
150
150
  // daisy "tabs-box" flavor: a rounded container, lifted active tab.
151
+ /**
152
+ * The swipe-to-dismiss offset (#293): `createSwipe` publishes the drag as
153
+ * `--swipe-x` / `--swipe-y` on a toast root or a drawer sheet, and this is
154
+ * the one place it is read — composed first into the part's `transform`, so
155
+ * an enter/exit offset or a slide rides on top of it. Unset, it is nothing.
156
+ */
157
+ const SWIPE = 'translate(var(--swipe-x, 0px), var(--swipe-y, 0px))';
158
+
151
159
  /**
152
160
  * Enter/exit presence for a top-layer popup.
153
161
  *
@@ -2934,10 +2942,36 @@ const fieldControl: NonNullable<PartStyles['base']> = {
2934
2942
  };
2935
2943
 
2936
2944
  /**
2937
- * A select whose clear-trigger is rendered (#280) — anchored on the root, so
2938
- * a part inside the trigger can make room for the button laid over it.
2945
+ * The select trigger's lynx steps — `[inline padding in --size-field units,
2946
+ * font-size step]` — as the size variants set them (`lynxBtnPad` and the
2947
+ * web ramp's `fontSize`). One table, so the clear-trigger's offset below
2948
+ * cannot drift from the trigger it sits in.
2949
+ */
2950
+ const SELECT_LYNX_STEPS = {
2951
+ xs: [2, 'xs'],
2952
+ sm: [3, 'sm'],
2953
+ md: [4, 'sm'],
2954
+ lg: [5, 'md'],
2955
+ xl: [6, 'lg'],
2956
+ } as const satisfies Record<keyof typeof FIELD_STEPS, readonly [number, string]>;
2957
+
2958
+ /**
2959
+ * Where the lynx clear-trigger's 1.5rem chip sits at one size step
2960
+ * (signalxjs/lynx#1184): centred in the field — the root is exactly the
2961
+ * trigger's height, so half the field less half the chip, from the top —
2962
+ * and one `--space-xs` short of the chevron's 1em box, which sits inside the
2963
+ * trigger's border and end padding at that step's font size. Physical
2964
+ * `top`/`right` and direct `calc(var())` only: lynx resolves no logical
2965
+ * insets on Android and no `min()`, and drops a `var()` that holds a
2966
+ * `calc()`.
2939
2967
  */
2940
- const SELECT_CLEARABLE = '[data-scope="select"][data-part="root"]:has(> [data-scope="select"][data-part="clear-trigger"]) &';
2968
+ const lynxSelectClear = (step: keyof typeof SELECT_LYNX_STEPS): NonNullable<PartStyles['base']> => {
2969
+ const [pad, text] = SELECT_LYNX_STEPS[step];
2970
+ return {
2971
+ top: `calc(var(--size-field) * ${FIELD_STEPS[step] / 2} - 0.75rem)`,
2972
+ right: `calc(var(--border) + var(--size-field) * ${pad} + var(--text-${text}) + var(--space-xs))`,
2973
+ };
2974
+ };
2941
2975
 
2942
2976
  export const select: RecipeInput = {
2943
2977
  component: 'select',
@@ -2997,9 +3031,13 @@ export const select: RecipeInput = {
2997
3031
  },
2998
3032
  indicator: {
2999
3033
  base: { opacity: '0.6', transition: 'transform var(--duration-normal) var(--ease-standard)' },
3000
- states: { open: { transform: 'rotate(180deg)' }, closed: {} },
3001
- // Room for the clear-trigger laid over the field (#280).
3002
- selectors: { [SELECT_CLEARABLE]: { marginInlineStart: 'calc(var(--space-2xl) + var(--space-sm))' } },
3034
+ states: {
3035
+ open: { transform: 'rotate(180deg)' },
3036
+ closed: {},
3037
+ // Room for the clear-trigger laid over the field (#280) — the
3038
+ // `clearable` flag the runtime stamps while it renders (#387).
3039
+ clearable: { marginInlineStart: 'calc(var(--space-2xl) + var(--space-sm))' },
3040
+ },
3003
3041
  },
3004
3042
  // Clears the selection (#280): daisy's ghost-btn over the field's
3005
3043
  // inline end, just before the chevron — muted like it, the base-200
@@ -3144,9 +3182,41 @@ export const select: RecipeInput = {
3144
3182
  // A touch list has no highlight: the pressed item takes the
3145
3183
  // highlighted item's wash.
3146
3184
  item: { states: { pressed: { background: 'var(--color-base-200)' } } },
3185
+ // The × and the ▾ get a spacing contract (signalxjs/lynx#1184).
3186
+ // The web offsets the clear-trigger by "the md padding + 1em",
3187
+ // and the 1em is the clear-trigger's own text-sm: at lg/xl the
3188
+ // trigger's padding and chevron both grow, so the × slid onto
3189
+ // the ▾. Here the chevron is a 1em box (right-aligned, so an
3190
+ // unclearable select looks as before) and the clear-trigger a
3191
+ // 1.5rem square (WCAG 2.5.8's target) placed per size beside
3192
+ // it — see `lynxSelectClear`. The web's `min()` height and
3193
+ // `margin-block: auto` centring are dropped on lynx, which
3194
+ // left the box stretched to the field's full height: its
3195
+ // focus ring was a tall pill. The ring is inset, so it hugs
3196
+ // the chip and can never reach the chevron, and no gap shadow
3197
+ // paints past the chip's rounded corners.
3198
+ indicator: {
3199
+ base: { width: '1em', textAlign: 'right' },
3200
+ // The web's room for the chip (`margin-inline-start`) is a
3201
+ // logical spelling Android never resolves, and it is
3202
+ // sized off the web's own offset. Here the chip is a
3203
+ // 1.5rem square one `--space-xs` short of the chevron
3204
+ // at every size, so the room is exactly that — and the
3205
+ // trigger's `gap` stays between the value and the chip.
3206
+ // Before the `clearable` flag the web reached this with
3207
+ // `:has()`, which lynx cannot express: at lg/xl the value
3208
+ // ran under the chip and its focus ring
3209
+ // (signalxjs/lynx#1191).
3210
+ states: { clearable: { marginLeft: 'calc(1.5rem + var(--space-xs))' } },
3211
+ },
3212
+ // The value gives way to the chip instead of running under
3213
+ // it: one line, clipped with an ellipsis — the web's
3214
+ // `nowrap` button text, with a flex item's `min-width: auto`
3215
+ // lifted so it can shrink below its content at all.
3216
+ value: { base: { flexShrink: '1', minWidth: '0', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' } },
3147
3217
  'clear-trigger': {
3148
- base: { top: 'var(--space-xs)', bottom: 'var(--space-xs)', right: 'calc(var(--size-field) * 4 + 1em + var(--space-xs))', minWidth: '1.5rem' },
3149
- states: lynxFocus(),
3218
+ base: { ...lynxSelectClear('md'), width: '1.5rem', height: '1.5rem', minWidth: '1.5rem', boxSizing: 'border-box' },
3219
+ states: lynxFocus({ inset: true }),
3150
3220
  },
3151
3221
  },
3152
3222
  variants: {
@@ -3155,13 +3225,15 @@ export const select: RecipeInput = {
3155
3225
  color: Object.fromEntries(ROLES.map((c) => [c, { popup: { base: {
3156
3226
  '--select-accent': `var(--color-${c})`,
3157
3227
  } } }])),
3158
- size: { ...lynxBtnSizes('trigger'), md: { trigger: { base: lynxBtnPad(4) } } },
3228
+ size: Object.fromEntries((Object.keys(SELECT_LYNX_STEPS) as (keyof typeof SELECT_LYNX_STEPS)[]).map((step) => [step, {
3229
+ trigger: { base: lynxBtnPad(SELECT_LYNX_STEPS[step][0]) },
3230
+ 'clear-trigger': { base: lynxSelectClear(step) },
3231
+ }])),
3159
3232
  },
3160
3233
  },
3161
3234
  },
3162
3235
  };
3163
3236
 
3164
-
3165
3237
  /**
3166
3238
  * daisy's BUTTON ramp — `.btn`'s fixed height per size step, in `--size-field`
3167
3239
  * units, exactly as daisyUI 5 ships it (`components/button.css`: `.btn-xs`
@@ -3780,6 +3852,8 @@ export const toast: RecipeInput = {
3780
3852
  states: {
3781
3853
  open: { opacity: '1', transform: 'none' },
3782
3854
  closed: {},
3855
+ // Styled in `targets.web` — the swipe is web-only (#293).
3856
+ swiping: {},
3783
3857
  },
3784
3858
  at: {
3785
3859
  'reduced-motion': { base: { transition: 'none' }, states: { open: { transform: 'none' } } },
@@ -3905,6 +3979,22 @@ export const toast: RecipeInput = {
3905
3979
  defaultVariants: { size: 'md' },
3906
3980
  keyframes: { 'zero-daisyui-toast-spin': 'to { transform: rotate(360deg); }' },
3907
3981
  targets: {
3982
+ // The swipe (#293) — web only, since the runtime's `--swipe-*` offset
3983
+ // is: composed ahead of the enter/exit offset, and no pan or pinch
3984
+ // starts on a toast, so a touch drag is the swipe's.
3985
+ web: {
3986
+ parts: {
3987
+ root: {
3988
+ base: { transform: `${SWIPE} translateY(var(--toast-from))`, touchAction: 'none' },
3989
+ states: { open: { transform: SWIPE } },
3990
+ selectors: {
3991
+ // Mid-swipe the toast tracks the pointer, not a transition.
3992
+ '&[data-swiping]': { transition: 'none', userSelect: 'none' },
3993
+ },
3994
+ at: { 'reduced-motion': { states: { open: { transform: SWIPE } } } },
3995
+ },
3996
+ },
3997
+ },
3908
3998
  lynx: {
3909
3999
  parts: {
3910
4000
  // The viewport is a full-width strip on lynx (the runtime pins
@@ -7337,9 +7427,11 @@ export const steps: RecipeInput = {
7337
7427
  /**
7338
7428
  * The modal sheet's slide (#83): in from its edge, back out to it.
7339
7429
  *
7340
- * `translate` is physical, so the travel is a custom property that flips with
7341
- * the placement AND the direction — off the reading start is leftward in LTR
7342
- * and rightward in RTL (the `rtl` hedge, as everywhere here). Keyed on
7430
+ * `translate` is physical, so the travel is a custom property — the whole
7431
+ * `x y` pair — that flips with the placement AND the direction: off the
7432
+ * reading start is leftward in LTR and rightward in RTL (the `rtl` hedge, as
7433
+ * everywhere here), while a `top` / `bottom` sheet travels on Y and mirrors
7434
+ * in neither (#291). Keyed on
7343
7435
  * `data-l-dock="sheet"`, the regime, which holds through the exit where
7344
7436
  * `:modal` does not, so the slide-out leaves from the sheet's own box.
7345
7437
  * Opacity stays at 1: the travel is the transition. Each direction rides the
@@ -7350,19 +7442,23 @@ const sheetSlide = (enter: string, exit: string): PartStyles => {
7350
7442
  const open = `${sheet}[data-state="open"]`;
7351
7443
  const travel = (tempo: string): string => {
7352
7444
  const duration = tempo.split(' ')[0];
7353
- return `translate ${tempo}, display ${duration} allow-discrete, overlay ${duration} allow-discrete`;
7445
+ // `transform` rides along for the swipe's spring back (#293).
7446
+ return `translate ${tempo}, transform ${tempo}, display ${duration} allow-discrete, overlay ${duration} allow-discrete`;
7354
7447
  };
7355
7448
  return {
7356
- base: { '--drawer-travel': '-100%' },
7449
+ base: { '--drawer-travel': '-100% 0' },
7357
7450
  selectors: {
7358
- '&[data-placement="end"]': { '--drawer-travel': '100%' },
7359
- [`&[data-placement="start"]${rtl}`]: { '--drawer-travel': '100%' },
7360
- [`&[data-placement="end"]${rtl}`]: { '--drawer-travel': '-100%' },
7361
- [sheet]: { opacity: '1', translate: 'var(--drawer-travel) 0', transition: travel(exit) },
7451
+ '&[data-placement="end"]': { '--drawer-travel': '100% 0' },
7452
+ [`&[data-placement="start"]${rtl}`]: { '--drawer-travel': '100% 0' },
7453
+ [`&[data-placement="end"]${rtl}`]: { '--drawer-travel': '-100% 0' },
7454
+ // The block edges travel on Y, and no direction mirrors them (#291).
7455
+ '&[data-placement="top"]': { '--drawer-travel': '0 -100%' },
7456
+ '&[data-placement="bottom"]': { '--drawer-travel': '0 100%' },
7457
+ [sheet]: { opacity: '1', translate: 'var(--drawer-travel)', transition: travel(exit) },
7362
7458
  [open]: { translate: 'none', transition: travel(enter) },
7363
7459
  },
7364
7460
  at: {
7365
- 'starting-style': { selectors: { [open]: { opacity: '1', translate: 'var(--drawer-travel) 0' } } },
7461
+ 'starting-style': { selectors: { [open]: { opacity: '1', translate: 'var(--drawer-travel)' } } },
7366
7462
  'reduced-motion': { selectors: { [sheet]: { transition: 'none' }, [open]: { transition: 'none' } } },
7367
7463
  },
7368
7464
  };
@@ -7432,6 +7528,38 @@ export const drawer: RecipeInput = {
7432
7528
  },
7433
7529
  '&[data-placement="start"][data-l-dock="sheet"]': { insetInlineStart: '0', insetInlineEnd: 'auto' },
7434
7530
  '&[data-placement="end"][data-l-dock="sheet"]': { insetInlineStart: 'auto', insetInlineEnd: '0' },
7531
+ /**
7532
+ * The block edges (#291): a sheet across the viewport's width,
7533
+ * pinned with `inset-block-*` — nothing here mirrors in RTL. Its
7534
+ * height is this recipe's: content-sized up to most of the
7535
+ * viewport, so the scrim stays in reach. Full width by default —
7536
+ * `measure` still caps the INLINE size (centred by the auto
7537
+ * margins), which the `:where()` keeps below the layout step
7538
+ * table's specificity whatever order the files load in.
7539
+ */
7540
+ '&:where([data-placement="top"], [data-placement="bottom"])': { '--l-measure': 'none' },
7541
+ '&[data-placement="top"][data-l-dock="sheet"]': {
7542
+ insetBlockStart: '0',
7543
+ insetBlockEnd: 'auto',
7544
+ insetInlineStart: '0',
7545
+ insetInlineEnd: '0',
7546
+ marginInline: 'auto',
7547
+ blockSize: 'auto',
7548
+ maxBlockSize: '85dvh',
7549
+ borderEndStartRadius: 'var(--radius-box)',
7550
+ borderEndEndRadius: 'var(--radius-box)',
7551
+ },
7552
+ '&[data-placement="bottom"][data-l-dock="sheet"]': {
7553
+ insetBlockStart: 'auto',
7554
+ insetBlockEnd: '0',
7555
+ insetInlineStart: '0',
7556
+ insetInlineEnd: '0',
7557
+ marginInline: 'auto',
7558
+ blockSize: 'auto',
7559
+ maxBlockSize: '85dvh',
7560
+ borderStartStartRadius: 'var(--radius-box)',
7561
+ borderStartEndRadius: 'var(--radius-box)',
7562
+ },
7435
7563
  },
7436
7564
  }),
7437
7565
  backdrop: {
@@ -7453,8 +7581,27 @@ export const drawer: RecipeInput = {
7453
7581
  // Trigger-carried axes — see `btnColors` for why the panel is out of
7454
7582
  // reach and the trigger is the whole story here.
7455
7583
  variants: { color: btnColors(), size: btnSizes },
7456
- // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
7457
7584
  targets: {
7585
+ // Swipe to dismiss (#293) — web only, since the runtime's `--swipe-*`
7586
+ // offset is: the sheet wears the drag offset (`SWIPE`) in every
7587
+ // state — so a swiped sheet leaves from where it was let go — at a
7588
+ // weight above the presence's own open `transform`. Mid-swipe no
7589
+ // transition runs. A touch pan across the dismiss axis stays the
7590
+ // page's; along it is the gesture's, so content taller than a
7591
+ // block-edge sheet scrolls in a box of its own inside the panel.
7592
+ web: {
7593
+ parts: {
7594
+ panel: {
7595
+ selectors: {
7596
+ '&[data-l-dock="sheet"][data-state]': { transform: SWIPE },
7597
+ '&[data-l-dock="sheet"][data-state="open"][data-swiping]': { transition: 'none', userSelect: 'none' },
7598
+ '&[data-l-dock="sheet"]:where([data-placement="start"], [data-placement="end"])': { touchAction: 'pan-y' },
7599
+ '&[data-l-dock="sheet"]:where([data-placement="top"], [data-placement="bottom"])': { touchAction: 'pan-x' },
7600
+ },
7601
+ },
7602
+ },
7603
+ },
7604
+ // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
7458
7605
  lynx: {
7459
7606
  parts: {
7460
7607
  trigger: { base: lynxBtnPad(4) },