@sigx/zero-daisyui 0.15.0 → 0.17.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/dist/recipes.js CHANGED
@@ -216,6 +216,97 @@ const disclosureExit = (scope) => ({
216
216
  },
217
217
  },
218
218
  });
219
+ /**
220
+ * Collapsible's non-native mode (#453): a `<div>` root, a `<button>` trigger
221
+ * and a panel hidden with `hidden="until-found"`. Nothing lives on
222
+ * `::details-content` there, so the panel does that wrapper's work itself:
223
+ * collapsed while `hidden` (the UA's `content-visibility: hidden` keeps the
224
+ * box — padding and border included — on the page), clipped so its content
225
+ * never spills out of a box that is growing or shrinking, and grown
226
+ * from collapsed to `auto` on the open state (`interpolate-size` is the
227
+ * root's, and inherits); the close is `disclosureExit`'s, as in native mode.
228
+ * Inert in native mode: a native panel is never `hidden`, and its block size
229
+ * never moves.
230
+ *
231
+ * The trigger sheds the paint a UA `<button>` has and a `<summary>` never
232
+ * had — only where the recipe's own base leaves a property unset, since this
233
+ * rule (`:where(button)`, the base's specificity) is emitted after it.
234
+ * Web-only, like `hidden="until-found"` itself.
235
+ */
236
+ const BUTTON_RESET = {
237
+ appearance: 'none',
238
+ margin: '0',
239
+ padding: '0',
240
+ border: '0',
241
+ background: 'none',
242
+ color: 'inherit',
243
+ fontFamily: 'inherit',
244
+ fontSize: 'inherit',
245
+ fontWeight: 'inherit',
246
+ fontStyle: 'inherit',
247
+ lineHeight: 'inherit',
248
+ letterSpacing: 'inherit',
249
+ textTransform: 'inherit',
250
+ textAlign: 'start',
251
+ inlineSize: '100%',
252
+ boxSizing: 'border-box',
253
+ };
254
+ /** The reset minus what `base` states itself (a `borderRadius` is no border). */
255
+ const summaryLike = (base = {}) => {
256
+ const own = Object.keys(base).filter((key) => !key.endsWith('Radius'));
257
+ return Object.fromEntries(Object.entries(BUTTON_RESET)
258
+ .filter(([key]) => !own.some((k) => k === key || k.startsWith(key))));
259
+ };
260
+ const withNonNative = (recipe) => {
261
+ const web = recipe.targets?.web ?? {};
262
+ const trigger = web.parts?.trigger ?? {};
263
+ const panel = web.parts?.panel ?? {};
264
+ const reduced = panel.at?.['reduced-motion'] ?? {};
265
+ return {
266
+ ...recipe,
267
+ targets: {
268
+ ...recipe.targets,
269
+ web: {
270
+ ...web,
271
+ parts: {
272
+ ...web.parts,
273
+ trigger: {
274
+ ...trigger,
275
+ selectors: { ...trigger.selectors, '&:where(button)': summaryLike(recipe.parts.trigger?.base) },
276
+ },
277
+ panel: {
278
+ ...panel,
279
+ base: { ...panel.base, overflow: 'clip' },
280
+ selectors: {
281
+ ...panel.selectors,
282
+ '&[data-state="closed"][hidden]': {
283
+ blockSize: '0',
284
+ paddingBlock: '0',
285
+ borderBlockWidth: '0',
286
+ animation: 'none',
287
+ },
288
+ },
289
+ // The open half: from the collapsed box to `auto`.
290
+ states: {
291
+ ...panel.states,
292
+ open: {
293
+ ...panel.states?.open,
294
+ transition: 'block-size var(--duration-normal) var(--ease-standard), padding-block var(--duration-normal) var(--ease-standard)',
295
+ },
296
+ },
297
+ at: {
298
+ ...panel.at,
299
+ 'reduced-motion': {
300
+ ...reduced,
301
+ states: { ...reduced.states, open: { transition: 'none' } },
302
+ },
303
+ },
304
+ },
305
+ },
306
+ },
307
+ },
308
+ };
309
+ };
219
310
  /**
220
311
  * Merge presence into a part's own styles per KEY, not per block: a recipe
221
312
  * that already writes `states: { open: {} }` — the "deliberately unstyled"
@@ -621,7 +712,7 @@ const disclosureSizes = {
621
712
  },
622
713
  };
623
714
  // daisy "collapse collapse-arrow" flavor.
624
- export const collapsible = {
715
+ export const collapsible = withNonNative({
625
716
  component: 'collapsible',
626
717
  targets: disclosureExit('collapsible'),
627
718
  // Public to a design system derived from this one (#73).
@@ -700,7 +791,7 @@ export const collapsible = {
700
791
  // signalxjs/lynx#1070. No `color` default: the un-attributed accent is
701
792
  // base-content ink, outside the color vocabulary.
702
793
  defaultVariants: { size: 'md' },
703
- };
794
+ });
704
795
  // daisy "toggle" flavor.
705
796
  export const switchRecipe = {
706
797
  component: 'switch',
@@ -1348,7 +1439,17 @@ export const tooltip = {
1348
1439
  targets: {
1349
1440
  web: floatingArrow('tooltip', { background: 'var(--color-neutral)' }, '0.5rem'),
1350
1441
  lynx: {
1351
- parts: { trigger: { base: lynxBtnPad(4) } },
1442
+ parts: {
1443
+ // Content-sized like daisy's btn, never broken mid-word
1444
+ // (signalxjs/lynx#1165). No pressed rule: the anatomy
1445
+ // declares none — the long press paints the `open` state.
1446
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit } },
1447
+ // The arrow's paint: the bubble's neutral, continued. The
1448
+ // web's `floatingArrow` geometry rides runtime custom
1449
+ // properties lynx does not write, so lynx-zero places the
1450
+ // 8px square and turns it itself (signalxjs/lynx#1277).
1451
+ arrow: { base: { background: 'var(--color-neutral)' } },
1452
+ },
1352
1453
  variants: { size: lynxBtnSizes('trigger') },
1353
1454
  },
1354
1455
  },
@@ -4527,6 +4628,70 @@ export const combobox = {
4527
4628
  },
4528
4629
  targets: {
4529
4630
  web: { parts: { popup: { base: anchoredListbox } } },
4631
+ // lynx-zero's combobox (signalxjs/lynx#1278): a native text field in
4632
+ // a painted `control` view, the list in the overlay outlet.
4633
+ lynx: {
4634
+ parts: {
4635
+ // select's bounds (signalxjs/lynx#1146): 20rem, never wider
4636
+ // than the container. The web's 13rem control floor
4637
+ // overflowed any cell narrower than 208px on a phone; the
4638
+ // control fills the root instead.
4639
+ root: { base: { width: '20rem', maxWidth: '100%', minWidth: '3rem' } },
4640
+ // The web `:hover` border is dropped on a touch platform. The
4641
+ // ring is the box-shadow spelling (signalxjs/lynx#1163): lynx
4642
+ // has no `outline-offset`, and its outline ignores the radius.
4643
+ control: {
4644
+ base: { minWidth: '3rem', width: '100%' },
4645
+ states: lynxFocus(),
4646
+ },
4647
+ // `color: inherit` never reaches a native text field, and
4648
+ // `::placeholder` does not exist: the ink is explicit, and the
4649
+ // placeholder takes lynx's `-x-placeholder-color` — the web's
4650
+ // 60% mix, baked per theme (input's spelling; its constant is
4651
+ // declared below this recipe, so the value is restated).
4652
+ input: {
4653
+ base: {
4654
+ color: 'var(--color-base-content)',
4655
+ '-x-placeholder-color': 'color-mix(in oklab, var(--color-base-content) 60%, transparent)',
4656
+ flexGrow: '1',
4657
+ flexShrink: '1',
4658
+ },
4659
+ },
4660
+ // The chevron and the × are `<view>`s holding a glyph: centred
4661
+ // in the stretched row (a lynx view lays its child out at the
4662
+ // top), in explicit ink. A held chevron comes up to full ink —
4663
+ // the touch answer to the web's resting 60%. The clear-trigger
4664
+ // declares no pressed flag; its feedback is the text leaving.
4665
+ trigger: {
4666
+ base: { color: 'var(--color-base-content)', display: 'flex', alignItems: 'center', justifyContent: 'center' },
4667
+ states: { pressed: { opacity: '1' } },
4668
+ },
4669
+ 'clear-trigger': {
4670
+ base: { color: 'var(--color-base-content)', display: 'flex', alignItems: 'center', justifyContent: 'center' },
4671
+ },
4672
+ // The chip's ring is the lynx one; its remove button's ring is
4673
+ // drawn inside, so it never paints over the chip's label.
4674
+ tag: { states: lynxFocus() },
4675
+ 'tag-remove': {
4676
+ base: { color: 'var(--color-base-content)' },
4677
+ states: lynxFocus({ inset: true }),
4678
+ },
4679
+ // The popup renders in the overlay outlet, outside the root,
4680
+ // so it cannot inherit `--combobox-accent`, and an unresolved
4681
+ // `var()` on lynx has no fallback (select's
4682
+ // signalxjs/lynx#1168). The popup restates the accent; its
4683
+ // colour-axis twins are in `variants.color` below.
4684
+ popup: { base: { '--combobox-accent': 'var(--color-primary)' } },
4685
+ // A touch list has no pointer highlight: a held option takes
4686
+ // the highlighted wash.
4687
+ item: { states: { pressed: { background: 'var(--color-base-200)' } } },
4688
+ },
4689
+ variants: {
4690
+ color: Object.fromEntries(ROLES.map((c) => [c, { popup: { base: {
4691
+ '--combobox-accent': `var(--color-${c})`,
4692
+ } } }])),
4693
+ },
4694
+ },
4530
4695
  },
4531
4696
  };
4532
4697
  // daisy "btn" that stays pressed: the off state is the neutral btn, the on
@@ -5293,6 +5458,14 @@ const lynxTreeRowStates = {
5293
5458
  },
5294
5459
  ...lynxFocus({ inset: true }),
5295
5460
  };
5461
+ /**
5462
+ * The tree rows' lynx base: the fill still eases, the ink snaps — a
5463
+ * transitioned `color` never reaches the row's inheriting label on lynx
5464
+ * (signalxjs/lynx#1292; see the tree-view recipe's `targets`).
5465
+ */
5466
+ const lynxTreeRowBase = {
5467
+ transition: 'background var(--duration-fast) var(--ease-standard)',
5468
+ };
5296
5469
  /** A held selected row keeps its accent fill (the web's `:not([data-selected])`). */
5297
5470
  const lynxTreeRowSelectors = {
5298
5471
  '&[data-selected][data-pressed]': { background: 'var(--tree-accent)' },
@@ -5451,11 +5624,23 @@ export const treeView = {
5451
5624
  // those carry is restated on the part the runtime stamps. The skin's
5452
5625
  // `:dir(rtl)` mirror and the `@media` stops have no lynx form (no RTL
5453
5626
  // flow, no conditions) and stay dropped.
5627
+ //
5628
+ // The rows transition `background` only (signalxjs/lynx#1292, zero#521).
5629
+ // A row's label is the author's `<text>`, inked by inheritance from the
5630
+ // row. Lynx's new animator (on by default) takes a transitioned `color`
5631
+ // change away from the element's own style pass and ticks it into the
5632
+ // row's paint alone, and the row's children inherit its computed colour
5633
+ // only in that pass, so they kept the OLD ink: a row selected live
5634
+ // painted base-content text on the accent fill, while a row selected at
5635
+ // mount (no transition runs) was right. Read in Lynx's
5636
+ // `fiber_element.cc` (`ConsumeStyleInternal`) and
5637
+ // `css_transition_manager.cc` (`ConsumeCSSProperty`). Without a `color`
5638
+ // transition the new ink is set in the style pass and reaches the label.
5454
5639
  targets: {
5455
5640
  lynx: {
5456
5641
  parts: {
5457
- item: { states: lynxTreeRowStates, selectors: lynxTreeRowSelectors },
5458
- 'branch-trigger': { states: lynxTreeRowStates, selectors: lynxTreeRowSelectors },
5642
+ item: { base: lynxTreeRowBase, states: lynxTreeRowStates, selectors: lynxTreeRowSelectors },
5643
+ 'branch-trigger': { base: lynxTreeRowBase, states: lynxTreeRowStates, selectors: lynxTreeRowSelectors },
5459
5644
  // `inline-block` is not a lynx display value. The glyph is a
5460
5645
  // `<text>` part: a 1em box centred on it, so the quarter turn
5461
5646
  // pivots on the chevron instead of swinging it sideways.
@@ -6534,7 +6719,11 @@ export const divider = {
6534
6719
  // no fill, its cross size following the label, a row when
6535
6720
  // horizontal. Both are lynx-zero rendering details, not
6536
6721
  // modifiers an author sets, so they are styled from raw lynx css
6537
- // (the toggle-group join ends' reasoning).
6722
+ // (the toggle-group join ends' reasoning). A segment centres
6723
+ // itself on the label (signalxjs/lynx#1272): it carries the
6724
+ // root's `align-self: stretch`, and a stretched item with a
6725
+ // definite thickness lands at cross-start on lynx — the top of
6726
+ // the label row, not the middle daisy's rules sit on.
6538
6727
  css: `
6539
6728
  .zx-divider__root.zx-m-labelled {
6540
6729
  --divider-ink: transparent;
@@ -6548,6 +6737,7 @@ export const divider = {
6548
6737
  }
6549
6738
  .zx-divider__root.zx-m-segment {
6550
6739
  flex: 1 1 0;
6740
+ align-self: center;
6551
6741
  }
6552
6742
  .zx-divider__root.zx-m-segment.zx-o-horizontal {
6553
6743
  min-width: 0;
@@ -7477,14 +7667,43 @@ export const chat = {
7477
7667
  xl: { bubble: { base: { fontSize: 'var(--text-lg)', padding: 'var(--space-md) var(--space-xl)' } } },
7478
7668
  },
7479
7669
  },
7480
- // The row's vertical padding, restated physically: logical spellings
7481
- // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
7482
- // the emitter refuses them.
7670
+ // Lynx (signalxjs/lynx#1276):
7671
+ // - the row's vertical padding, restated physically: logical spellings
7672
+ // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
7673
+ // the emitter refuses them;
7674
+ // - lynx has no grid, so the row is a flex column (header, bubble,
7675
+ // footer), gathered to the placement's side. The avatar leaves the
7676
+ // flow: lynx-zero pins it to the row's bottom corner and reserves its
7677
+ // measured width beside the column;
7678
+ // - the `[data-placement] > &` selectors have no class form on the
7679
+ // parts (placement is the root's), so the bubble's square tail corner
7680
+ // is raw lynx css, a descendant of the placed root — the shape the
7681
+ // theme restatements are proven to match. Physical corners: lynx has
7682
+ // no RTL flow.
7483
7683
  targets: {
7484
7684
  lynx: {
7485
7685
  parts: {
7486
- root: { base: { paddingTop: 'var(--space-2xs)', paddingBottom: 'var(--space-2xs)' } },
7686
+ root: {
7687
+ base: {
7688
+ display: 'flex',
7689
+ flexDirection: 'column',
7690
+ alignItems: 'flex-start',
7691
+ paddingTop: 'var(--space-2xs)',
7692
+ paddingBottom: 'var(--space-2xs)',
7693
+ },
7694
+ selectors: {
7695
+ '&[data-placement="end"]': { alignItems: 'flex-end' },
7696
+ },
7697
+ },
7487
7698
  },
7699
+ css: `
7700
+ .zx-chat__root.zx-p-start .zx-chat__bubble {
7701
+ border-bottom-left-radius: 0;
7702
+ }
7703
+ .zx-chat__root.zx-p-end .zx-chat__bubble {
7704
+ border-bottom-right-radius: 0;
7705
+ }
7706
+ `,
7488
7707
  },
7489
7708
  },
7490
7709
  };
@@ -7576,6 +7795,59 @@ export const chatLog = {
7576
7795
  xl: { content: { base: { rowGap: 'var(--space-md)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 10)', fontSize: 'var(--text-sm)' } } },
7577
7796
  },
7578
7797
  },
7798
+ // Lynx (signalxjs/lynx#1276). lynx-zero's root is the frame and a native
7799
+ // scroll-view inside Content does the scrolling, so:
7800
+ // - the frame's padding moves onto the content (physical — logical
7801
+ // spellings do not resolve on Android, signalxjs/lynx#1084), so rows
7802
+ // scroll to the frame's edge instead of clipping at an inset;
7803
+ // - the content's grid is a flex column (lynx has no grid); a short
7804
+ // transcript sits at the top — `margin-block-start: auto` has nothing
7805
+ // to push against inside a scroll-view;
7806
+ // - the jump trigger floats in lynx-zero's absolute dock at the frame's
7807
+ // foot, so it drops `position: sticky` and the negative margin, and
7808
+ // restates its inline padding physically;
7809
+ // - `:hover` is dropped on touch: the held trigger shows the hover's
7810
+ // 90% darkening (restated per colour — a colour function over
7811
+ // `--chat-log-fill` cannot bake), with the existing 1px sink;
7812
+ // - the focus rings as `lynxFocus` (lynx#1163), the frame's drawn inside
7813
+ // like the web's negative offset.
7814
+ targets: {
7815
+ lynx: {
7816
+ parts: {
7817
+ root: {
7818
+ base: { padding: '0', overflow: 'hidden' },
7819
+ states: { 'focus-visible': lynxFocusRing('var(--color-base-content)', { inset: true }) },
7820
+ },
7821
+ content: {
7822
+ base: {
7823
+ display: 'flex',
7824
+ flexDirection: 'column',
7825
+ paddingTop: 'var(--space-sm)',
7826
+ paddingBottom: 'var(--space-sm)',
7827
+ paddingLeft: 'var(--space-md)',
7828
+ paddingRight: 'var(--space-md)',
7829
+ },
7830
+ },
7831
+ 'jump-trigger': {
7832
+ base: {
7833
+ position: 'relative',
7834
+ flexDirection: 'row',
7835
+ paddingLeft: 'var(--space-md)',
7836
+ paddingRight: 'var(--space-md)',
7837
+ },
7838
+ states: {
7839
+ pressed: { background: 'color-mix(in oklab, var(--color-primary) 90%, black)' },
7840
+ ...lynxFocus(),
7841
+ },
7842
+ },
7843
+ },
7844
+ variants: {
7845
+ color: Object.fromEntries(ROLES.map((c) => [c, { 'jump-trigger': { states: {
7846
+ pressed: { background: `color-mix(in oklab, var(--color-${c}) 90%, black)` },
7847
+ } } }])),
7848
+ },
7849
+ },
7850
+ },
7579
7851
  };
7580
7852
  /** daisy radial-progress: --value became --progress-percent; same ring. */
7581
7853
  export const radialProgress = {
@@ -7828,13 +8100,26 @@ export const navbar = {
7828
8100
  // so the bar's height ramp is restated as `min-height`; and a lynx flex
7829
8101
  // container is only a row when it says so, so the bar and its three
7830
8102
  // sections spell `flex-direction: row`.
8103
+ //
8104
+ // No section squeezes a word (signalxjs/lynx#1274, zero#499). On the web
8105
+ // `flex: 1 1 0%` is floored by the flex item's automatic minimum (its
8106
+ // min-content width), so "Acme" never breaks. Lynx's flex layout has no
8107
+ // automatic minimum, and its `min-width` takes no intrinsic keyword, so
8108
+ // a zero basis shrank the start below its text and the `<text>` wrapped
8109
+ // per character ("Acm / e"). On lynx the ends start from their content
8110
+ // (`flex-basis: auto`), never shrink, and share the slack equally; the
8111
+ // centre takes daisy's own `flex-shrink: 0`. A bar too narrow for its
8112
+ // content overflows whole, as daisy's does — the btn rule (lynx#1165).
8113
+ // The cost: with a centre and unequal ends, the centre sits off-centre
8114
+ // by half the ends' difference (the web's is exact until a section
8115
+ // reaches its min-content).
7831
8116
  targets: {
7832
8117
  lynx: {
7833
8118
  parts: {
7834
8119
  root: { base: { flexDirection: 'row', minHeight: '4rem' } },
7835
- start: { base: { flexDirection: 'row' } },
7836
- center: { base: { flexDirection: 'row' } },
7837
- end: { base: { flexDirection: 'row' } },
8120
+ start: { base: { flexDirection: 'row', flexBasis: 'auto', flexShrink: '0' } },
8121
+ center: { base: { flexDirection: 'row', flexShrink: '0' } },
8122
+ end: { base: { flexDirection: 'row', flexBasis: 'auto', flexShrink: '0' } },
7838
8123
  },
7839
8124
  variants: {
7840
8125
  size: {
@@ -8767,11 +9052,68 @@ export const drawer = {
8767
9052
  },
8768
9053
  },
8769
9054
  },
8770
- // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
9055
+ // The btn paddings, restated physically — see `lynxBtnPad` (#1084) —
9056
+ // and the btns' press rendering (`lynxBtnPressed`), as dialog wears
9057
+ // them. The edge sheet (signalxjs/lynx#1277): lynx-zero lays the
9058
+ // panel on its edge with flexbox inside the backdrop, so the sheet
9059
+ // stays in flow (`position: relative`, never `fixed`) and sizes to
9060
+ // it — no viewport units, which lynx does not resolve against the
9061
+ // overlay outlet. The skin keeps the cap, the corners and the slide.
8771
9062
  lynx: {
9063
+ keyframes: {
9064
+ // `translate` is Android-asymmetric (#1084): the travel is a
9065
+ // transform function, one per edge.
9066
+ 'zero-daisy-drawer-in-start': 'from { transform: translateX(-100%); } to { transform: translateX(0); }',
9067
+ 'zero-daisy-drawer-in-end': 'from { transform: translateX(100%); } to { transform: translateX(0); }',
9068
+ 'zero-daisy-drawer-in-top': 'from { transform: translateY(-100%); } to { transform: translateY(0); }',
9069
+ 'zero-daisy-drawer-in-bottom': 'from { transform: translateY(100%); } to { transform: translateY(0); }',
9070
+ },
8772
9071
  parts: {
8773
- trigger: { base: lynxBtnPad(4) },
8774
- close: { base: lynxBtnPad(4) },
9072
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
9073
+ close: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
9074
+ panel: {
9075
+ // The cap alone: `min(20rem, 85vw)` has no lynx spelling
9076
+ // (no min()); lynx-zero states the 85% share inline and
9077
+ // this caps it — together, the web's width.
9078
+ base: { '--l-measure': '20rem' },
9079
+ selectors: {
9080
+ '&[data-l-dock="sheet"]': {
9081
+ position: 'relative',
9082
+ blockSize: 'auto',
9083
+ maxBlockSize: 'none',
9084
+ },
9085
+ // The block edges run full width (the web's `:where()`
9086
+ // pair, one selector each — no `:where()` on lynx),
9087
+ // content-sized up to 85% of the outlet.
9088
+ '&[data-placement="top"]': { '--l-measure': 'none' },
9089
+ '&[data-placement="bottom"]': { '--l-measure': 'none' },
9090
+ '&[data-placement="top"][data-l-dock="sheet"]': {
9091
+ maxBlockSize: '85%',
9092
+ borderBottomLeftRadius: 'var(--radius-box)',
9093
+ borderBottomRightRadius: 'var(--radius-box)',
9094
+ },
9095
+ '&[data-placement="bottom"][data-l-dock="sheet"]': {
9096
+ maxBlockSize: '85%',
9097
+ borderTopLeftRadius: 'var(--radius-box)',
9098
+ borderTopRightRadius: 'var(--radius-box)',
9099
+ },
9100
+ // The slide in from the edge (the web's `translate`
9101
+ // over `@starting-style`, both dropped here). The
9102
+ // exit is an unmount on lynx, so only the entry runs.
9103
+ '&[data-placement="start"][data-l-dock="sheet"][data-state="open"]': {
9104
+ animation: 'zero-daisy-drawer-in-start var(--duration-slow) var(--ease-standard)',
9105
+ },
9106
+ '&[data-placement="end"][data-l-dock="sheet"][data-state="open"]': {
9107
+ animation: 'zero-daisy-drawer-in-end var(--duration-slow) var(--ease-standard)',
9108
+ },
9109
+ '&[data-placement="top"][data-l-dock="sheet"][data-state="open"]': {
9110
+ animation: 'zero-daisy-drawer-in-top var(--duration-slow) var(--ease-standard)',
9111
+ },
9112
+ '&[data-placement="bottom"][data-l-dock="sheet"][data-state="open"]': {
9113
+ animation: 'zero-daisy-drawer-in-bottom var(--duration-slow) var(--ease-standard)',
9114
+ },
9115
+ },
9116
+ },
8775
9117
  },
8776
9118
  variants: { size: lynxBtnSizes('trigger') },
8777
9119
  },
@@ -8970,6 +9312,96 @@ export const table = {
8970
9312
  },
8971
9313
  },
8972
9314
  },
9315
+ // signalxjs/lynx#1279. Lynx has no table layout: lynx-zero lays the
9316
+ // parts out as flex rows itself. The `hover` modifier has no lynx
9317
+ // answer (a touch screen has no hover), nor has the viewport-width
9318
+ // `@media` of the stacked mode — lynx-zero resolves `stack` in JS and
9319
+ // stamps the parts instead (see `css` below).
9320
+ targets: {
9321
+ lynx: {
9322
+ parts: {
9323
+ // The web's `overflow-x: auto` box: a lynx view clips its
9324
+ // rounded frame; a table of fixed columns scrolls in a
9325
+ // scroll-view lynx-zero renders.
9326
+ root: {
9327
+ base: { overflow: 'hidden' },
9328
+ // daisy's ring drawn inside the box, as on the web
9329
+ // (outline-offset: -2px) — spelled as an inset shadow.
9330
+ states: { 'focus-visible': lynxFocusRing('var(--color-base-content)', { inset: true }) },
9331
+ },
9332
+ // The muted header ink: the colour function over
9333
+ // `--table-accent` cannot bake (a recipe-local property),
9334
+ // so the default accent's mix is restated here and each
9335
+ // colour's below.
9336
+ 'header-cell': { base: { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' } },
9337
+ 'cell-label': { base: { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' } },
9338
+ // The web's hover ink, while held; the lynx ring.
9339
+ 'sort-trigger': {
9340
+ base: { flexDirection: 'row' },
9341
+ states: { pressed: { color: 'var(--table-accent)' }, ...lynxFocus() },
9342
+ },
9343
+ // The mark never takes its width out of the header's word
9344
+ // (signalxjs/lynx#1299): lynx's flex has no automatic
9345
+ // minimum, so the ▲ (kept in layout while unsorted) squeezed
9346
+ // "Name" into "Nam / e" at xl. lynx-zero holds the label's
9347
+ // `<text>` at its content width; a header too narrow for
9348
+ // both overflows whole, as the web's column grows.
9349
+ 'sort-indicator': { base: { flexShrink: '0' } },
9350
+ },
9351
+ variants: {
9352
+ color: Object.fromEntries(ROLES.map((c) => {
9353
+ const muted = `color-mix(in oklab, ${roleInk(c)} 60%, transparent)`;
9354
+ return [c, {
9355
+ 'header-cell': { base: { color: muted } },
9356
+ 'cell-label': { base: { color: muted } },
9357
+ }];
9358
+ })),
9359
+ },
9360
+ // lynx-zero rendering details, not modifiers an author sets:
9361
+ // - `stripe`: lynx has no `:nth-child`, so the body stamps its
9362
+ // even, unselected rows (mount order) under `zebra`.
9363
+ // - `held`: the unsorted mark the web reveals under a hovered or
9364
+ // keyboard-focused trigger, shown while the trigger is held.
9365
+ // - `stacked`: the stacked mode's card chrome (the web's
9366
+ // below-breakpoint rules). Zero widths and paddings keep a
9367
+ // var() so they beat the var-bearing base values (lynx expands
9368
+ // var() declarations after the cascade, signalxjs/lynx#1161);
9369
+ // the root keeps its base-100 fill (a var-free transparent
9370
+ // would lose the same way).
9371
+ css: `
9372
+ .zx-table__root.zx-m-stacked {
9373
+ border-width: calc(var(--border) * 0);
9374
+ border-radius: calc(var(--radius-box) * 0);
9375
+ }
9376
+ .zx-table__caption.zx-m-stacked {
9377
+ padding-left: calc(var(--table-pad-inline) * 0);
9378
+ padding-right: calc(var(--table-pad-inline) * 0);
9379
+ }
9380
+ .zx-table__row.zx-m-stacked {
9381
+ border: var(--border) solid var(--color-base-200);
9382
+ border-radius: var(--radius-box);
9383
+ background: var(--color-base-100);
9384
+ padding: var(--space-sm) var(--table-pad-inline);
9385
+ margin-bottom: var(--space-md);
9386
+ }
9387
+ .zx-table__row.zx-m-stacked.zx-f-selected {
9388
+ background: var(--color-base-300);
9389
+ }
9390
+ .zx-table__cell.zx-m-stacked {
9391
+ padding-left: calc(var(--table-pad-inline) * 0);
9392
+ padding-right: calc(var(--table-pad-inline) * 0);
9393
+ padding-top: var(--space-xs);
9394
+ padding-bottom: var(--space-xs);
9395
+ }
9396
+ .zx-table__row.zx-m-stripe {
9397
+ background: var(--color-base-200);
9398
+ }
9399
+ .zx-table__sort-indicator.zx-m-held.zx-s-none {
9400
+ opacity: 0.6;
9401
+ }
9402
+ `,
9403
+ },
9404
+ },
8973
9405
  };
8974
9406
  /**
8975
9407
  * FileUpload — daisy's file-input family reshaped for the composed anatomy:
@@ -9135,6 +9567,71 @@ export const fileUpload = {
9135
9567
  xl: { root: { base: { '--fu-pad': 'calc(var(--space-xl) * 1.25)', '--fu-font': 'var(--text-md)' } } },
9136
9568
  },
9137
9569
  },
9570
+ // Lynx (signalxjs/lynx#1276):
9571
+ // - lynx has no grid: the root and the item list are flex columns, and
9572
+ // the stretched parts (dropzone, item list) say so with `align-self`;
9573
+ // the rows (trigger, item) state `flex-direction: row`;
9574
+ // - the dropzone centres its words in a column (lynx text alignment is
9575
+ // the text's own), and its highlighted wash — a colour function over
9576
+ // `--fu-accent`, which cannot bake — is restated per colour over the
9577
+ // role. lynx never drives `highlighted` (no drag source); it shows
9578
+ // through `ForceStates` in the gallery;
9579
+ // - `:hover` is dropped on touch: the held trigger shows daisy's
9580
+ // base-300 wash + 1px sink (`lynxBtnPressed`, in place of the
9581
+ // standalone `translate`, which Android does not resolve), and the
9582
+ // held × and clear show the ghost's base-200 wash;
9583
+ // - the focus rings as `lynxFocus` (lynx#1163).
9584
+ targets: {
9585
+ lynx: {
9586
+ parts: {
9587
+ root: { base: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start' } },
9588
+ trigger: {
9589
+ base: { flexDirection: 'row' },
9590
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
9591
+ },
9592
+ dropzone: {
9593
+ base: { alignSelf: 'stretch', display: 'flex', flexDirection: 'column', alignItems: 'center' },
9594
+ states: {
9595
+ highlighted: { background: 'color-mix(in oklab, var(--color-primary) 8%, var(--color-base-100))' },
9596
+ },
9597
+ },
9598
+ 'item-group': {
9599
+ base: { alignSelf: 'stretch', display: 'flex', flexDirection: 'column' },
9600
+ },
9601
+ item: { base: { flexDirection: 'row' } },
9602
+ // Only the name gives way in a narrow row (signalxjs/lynx#1294).
9603
+ // Lynx's flex has no automatic minimum, so every item
9604
+ // shrank below its content: "1.5 kB" wrapped to two lines
9605
+ // and the × squeezed into an oval. The size and the × keep
9606
+ // their content width, as the web's do; the name
9607
+ // (`flex: 1 1 auto; min-width: 0`) ellipsizes.
9608
+ 'item-size': { base: { flexShrink: '0' } },
9609
+ 'item-remove': {
9610
+ base: { flexShrink: '0' },
9611
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
9612
+ },
9613
+ 'clear-trigger': {
9614
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
9615
+ },
9616
+ },
9617
+ variants: {
9618
+ color: Object.fromEntries(ROLES.map((c) => [c, { dropzone: { states: {
9619
+ highlighted: { background: `color-mix(in oklab, var(--color-${c}) 8%, var(--color-base-100))` },
9620
+ } } }])),
9621
+ },
9622
+ },
9623
+ },
9624
+ };
9625
+ /** A carousel nav circle on lynx: the glyph centred as flex (no `grid`). */
9626
+ const carouselTriggerLynx = {
9627
+ display: 'flex',
9628
+ alignItems: 'center',
9629
+ justifyContent: 'center',
9630
+ };
9631
+ /** A held carousel nav circle on lynx: daisy's hover wash plus the 1px sink, and the lynx ring. */
9632
+ const carouselTriggerLynxStates = {
9633
+ pressed: { background: 'var(--color-base-300)', transform: 'translateY(1px)' },
9634
+ ...lynxFocus(),
9138
9635
  };
9139
9636
  /**
9140
9637
  * Carousel — daisy's carousel is exactly a scroll-snap box; the additions
@@ -9286,14 +9783,60 @@ export const carousel = {
9286
9783
  // spellings resolve on iOS but not on Android (measured,
9287
9784
  // signalxjs/lynx#1084), so the emitter refuses them. Physical is the
9288
9785
  // lynx target's norm — no RTL flow there, so `start` IS left.
9786
+ //
9787
+ // signalxjs/lynx#1279 — the rest of the lynx shape:
9788
+ // - `grid` does not lay out on lynx: the root is a flex column, and a
9789
+ // trigger centres its glyph as flex.
9790
+ // - The dot: the web draws it on `::before` inside a
9791
+ // `max(dot, 1.5rem)` button; lynx has neither pseudo-elements nor
9792
+ // `max()`, so lynx-zero renders the dot AS the indicator (its touch
9793
+ // area widened with `hit-slop`) and the ring and the active fill ride
9794
+ // the part.
9795
+ // - The web sinks a held trigger with the standalone `translate`,
9796
+ // which Android ignores (#1084): a transform function here, over
9797
+ // daisy's hover wash (no hover on touch).
9798
+ // - Rings: lynx's outline ignores radius and has no offset (#1163).
9799
+ // No replacement for the viewport's `:focus-visible` ring: there is no
9800
+ // keyboard focus on lynx, and the part declares no flag to key it on.
9289
9801
  targets: {
9290
9802
  lynx: {
9291
9803
  parts: {
9804
+ root: { base: { display: 'flex', flexDirection: 'column' } },
9292
9805
  'prev-trigger': {
9293
- base: { top: 'calc(50% - var(--carousel-nav) / 2)', left: 'var(--space-sm)' },
9806
+ base: {
9807
+ top: 'calc(50% - var(--carousel-nav) / 2)',
9808
+ left: 'var(--space-sm)',
9809
+ ...carouselTriggerLynx,
9810
+ },
9811
+ states: carouselTriggerLynxStates,
9294
9812
  },
9295
9813
  'next-trigger': {
9296
- base: { top: 'calc(50% - var(--carousel-nav) / 2)', right: 'var(--space-sm)' },
9814
+ base: {
9815
+ top: 'calc(50% - var(--carousel-nav) / 2)',
9816
+ right: 'var(--space-sm)',
9817
+ ...carouselTriggerLynx,
9818
+ },
9819
+ states: carouselTriggerLynxStates,
9820
+ },
9821
+ 'indicator-group': { base: { flexDirection: 'row', alignItems: 'center' } },
9822
+ indicator: {
9823
+ base: {
9824
+ display: 'flex',
9825
+ width: 'var(--carousel-dot)',
9826
+ height: 'var(--carousel-dot)',
9827
+ flexShrink: '0',
9828
+ // Longhands: the ring's colour bakes per theme only
9829
+ // on its own (a shorthand mixing it with the
9830
+ // --border calc cannot).
9831
+ borderWidth: 'calc(var(--border) * 2)',
9832
+ borderStyle: 'solid',
9833
+ borderColor: 'color-mix(in oklab, var(--color-base-content) 70%, transparent)',
9834
+ borderRadius: '9999px',
9835
+ },
9836
+ states: {
9837
+ active: { background: 'var(--carousel-accent)', borderColor: 'var(--carousel-accent)' },
9838
+ ...lynxFocus(),
9839
+ },
9297
9840
  },
9298
9841
  },
9299
9842
  },