@sigx/zero-daisyui 0.14.0 → 0.16.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.
Files changed (47) hide show
  1. package/dist/css/components/container.css +1 -0
  2. package/dist/css/components/input.css +9 -0
  3. package/dist/css/components/nav-list.css +7 -0
  4. package/dist/css/index.css +17 -0
  5. package/dist/lynx/components/alert.css +7 -1
  6. package/dist/lynx/components/badge.css +2 -4
  7. package/dist/lynx/components/breadcrumbs.css +30 -21
  8. package/dist/lynx/components/button.css +2 -7
  9. package/dist/lynx/components/card.css +1 -2
  10. package/dist/lynx/components/carousel.css +62 -11
  11. package/dist/lynx/components/chat-log.css +201 -10
  12. package/dist/lynx/components/chat.css +11 -2
  13. package/dist/lynx/components/combobox.css +82 -8
  14. package/dist/lynx/components/container.css +3 -2
  15. package/dist/lynx/components/diff.css +1 -1
  16. package/dist/lynx/components/divider.css +11 -10
  17. package/dist/lynx/components/drawer.css +66 -11
  18. package/dist/lynx/components/empty-state.css +1 -1
  19. package/dist/lynx/components/fieldset.css +1 -1
  20. package/dist/lynx/components/file-upload.css +212 -5
  21. package/dist/lynx/components/grid.css +2 -2
  22. package/dist/lynx/components/kbd.css +2 -3
  23. package/dist/lynx/components/menu.css +20 -1
  24. package/dist/lynx/components/nav-list.css +15 -2
  25. package/dist/lynx/components/navbar.css +14 -5
  26. package/dist/lynx/components/pagination.css +97 -65
  27. package/dist/lynx/components/radial-progress.css +10 -10
  28. package/dist/lynx/components/select.css +1 -2
  29. package/dist/lynx/components/spinner.css +2 -12
  30. package/dist/lynx/components/stack.css +2 -2
  31. package/dist/lynx/components/status.css +10 -20
  32. package/dist/lynx/components/steps.css +114 -22
  33. package/dist/lynx/components/table.css +401 -3
  34. package/dist/lynx/components/tabs.css +1 -1
  35. package/dist/lynx/components/timeline.css +12 -25
  36. package/dist/lynx/components/toast.css +2 -4
  37. package/dist/lynx/components/tooltip.css +7 -0
  38. package/dist/lynx/components/tree-view.css +31 -4
  39. package/dist/lynx/index.css +1448 -292
  40. package/dist/lynx/manifest.json +3 -3
  41. package/dist/manifest.json +1 -1
  42. package/dist/recipes.d.ts.map +1 -1
  43. package/dist/recipes.js +710 -28
  44. package/dist/recipes.js.map +1 -1
  45. package/dist/report.json +555 -56
  46. package/package.json +4 -4
  47. package/src/recipes.ts +717 -28
package/src/recipes.ts CHANGED
@@ -1413,7 +1413,17 @@ export const tooltip: RecipeInput = {
1413
1413
  targets: {
1414
1414
  web: floatingArrow('tooltip', { background: 'var(--color-neutral)' }, '0.5rem'),
1415
1415
  lynx: {
1416
- parts: { trigger: { base: lynxBtnPad(4) } },
1416
+ parts: {
1417
+ // Content-sized like daisy's btn, never broken mid-word
1418
+ // (signalxjs/lynx#1165). No pressed rule: the anatomy
1419
+ // declares none — the long press paints the `open` state.
1420
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit } },
1421
+ // The arrow's paint: the bubble's neutral, continued. The
1422
+ // web's `floatingArrow` geometry rides runtime custom
1423
+ // properties lynx does not write, so lynx-zero places the
1424
+ // 8px square and turns it itself (signalxjs/lynx#1277).
1425
+ arrow: { base: { background: 'var(--color-neutral)' } },
1426
+ },
1417
1427
  variants: { size: lynxBtnSizes('trigger') },
1418
1428
  },
1419
1429
  },
@@ -1625,12 +1635,34 @@ export const menu: RecipeInput = {
1625
1635
  // Trigger-carried axes — same wiring as dialog, same reason. The dropdown
1626
1636
  // and its items are top-layer siblings the donut cannot reach.
1627
1637
  variants: { color: btnColors(), size: btnSizes },
1628
- // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
1638
+ // The btn paddings, restated physically — see `lynxBtnPad` (#1084) —
1639
+ // and the trigger's press rendering, as popover's (`lynxBtnPressed`).
1629
1640
  targets: {
1630
1641
  web: floatingArrow('menu', panelArrowPaint),
1631
1642
  lynx: {
1632
1643
  parts: {
1633
- trigger: { base: lynxBtnPad(4) },
1644
+ // Content-sized like daisy's btn (signalxjs/lynx#1165), an
1645
+ // explicit row (lynx does not default the direction the way a
1646
+ // browser does), the held state in place of the dropped
1647
+ // `:hover`, and the box-shadow ring (signalxjs/lynx#1163).
1648
+ trigger: {
1649
+ base: { ...lynxBtnPad(4), ...lynxBtnFit, flexDirection: 'row' },
1650
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
1651
+ },
1652
+ // The popups' rows stack and span the panel: a flex column
1653
+ // (a lynx view's default is a shrink-wrapping linear box).
1654
+ popup: { base: { display: 'flex', flexDirection: 'column' } },
1655
+ 'sub-popup': { base: { display: 'flex', flexDirection: 'column' } },
1656
+ // Every row kind is a mark / label / shortcut / chevron row.
1657
+ // A touch list has no hover: lynx-zero stamps `highlighted` on
1658
+ // the row under the finger, so the web's highlight wash is the
1659
+ // press rendering as it stands. The ✓ (item-indicator) and ›
1660
+ // (sub-trigger) the web draws with `::after` render as
1661
+ // `<text>` glyphs in lynx-zero — there is no pseudo-element.
1662
+ item: { base: { flexDirection: 'row' } },
1663
+ 'checkbox-item': { base: { flexDirection: 'row' } },
1664
+ 'radio-item': { base: { flexDirection: 'row' } },
1665
+ 'sub-trigger': { base: { flexDirection: 'row' } },
1634
1666
  // The shortcut's reading-end push, restated physically (#1084).
1635
1667
  shortcut: { base: { marginLeft: 'auto', paddingLeft: 'var(--space-lg)' } },
1636
1668
  },
@@ -4610,6 +4642,70 @@ export const combobox: RecipeInput = {
4610
4642
  },
4611
4643
  targets: {
4612
4644
  web: { parts: { popup: { base: anchoredListbox } } },
4645
+ // lynx-zero's combobox (signalxjs/lynx#1278): a native text field in
4646
+ // a painted `control` view, the list in the overlay outlet.
4647
+ lynx: {
4648
+ parts: {
4649
+ // select's bounds (signalxjs/lynx#1146): 20rem, never wider
4650
+ // than the container. The web's 13rem control floor
4651
+ // overflowed any cell narrower than 208px on a phone; the
4652
+ // control fills the root instead.
4653
+ root: { base: { width: '20rem', maxWidth: '100%', minWidth: '3rem' } },
4654
+ // The web `:hover` border is dropped on a touch platform. The
4655
+ // ring is the box-shadow spelling (signalxjs/lynx#1163): lynx
4656
+ // has no `outline-offset`, and its outline ignores the radius.
4657
+ control: {
4658
+ base: { minWidth: '3rem', width: '100%' },
4659
+ states: lynxFocus(),
4660
+ },
4661
+ // `color: inherit` never reaches a native text field, and
4662
+ // `::placeholder` does not exist: the ink is explicit, and the
4663
+ // placeholder takes lynx's `-x-placeholder-color` — the web's
4664
+ // 60% mix, baked per theme (input's spelling; its constant is
4665
+ // declared below this recipe, so the value is restated).
4666
+ input: {
4667
+ base: {
4668
+ color: 'var(--color-base-content)',
4669
+ '-x-placeholder-color': 'color-mix(in oklab, var(--color-base-content) 60%, transparent)',
4670
+ flexGrow: '1',
4671
+ flexShrink: '1',
4672
+ },
4673
+ },
4674
+ // The chevron and the × are `<view>`s holding a glyph: centred
4675
+ // in the stretched row (a lynx view lays its child out at the
4676
+ // top), in explicit ink. A held chevron comes up to full ink —
4677
+ // the touch answer to the web's resting 60%. The clear-trigger
4678
+ // declares no pressed flag; its feedback is the text leaving.
4679
+ trigger: {
4680
+ base: { color: 'var(--color-base-content)', display: 'flex', alignItems: 'center', justifyContent: 'center' },
4681
+ states: { pressed: { opacity: '1' } },
4682
+ },
4683
+ 'clear-trigger': {
4684
+ base: { color: 'var(--color-base-content)', display: 'flex', alignItems: 'center', justifyContent: 'center' },
4685
+ },
4686
+ // The chip's ring is the lynx one; its remove button's ring is
4687
+ // drawn inside, so it never paints over the chip's label.
4688
+ tag: { states: lynxFocus() },
4689
+ 'tag-remove': {
4690
+ base: { color: 'var(--color-base-content)' },
4691
+ states: lynxFocus({ inset: true }),
4692
+ },
4693
+ // The popup renders in the overlay outlet, outside the root,
4694
+ // so it cannot inherit `--combobox-accent`, and an unresolved
4695
+ // `var()` on lynx has no fallback (select's
4696
+ // signalxjs/lynx#1168). The popup restates the accent; its
4697
+ // colour-axis twins are in `variants.color` below.
4698
+ popup: { base: { '--combobox-accent': 'var(--color-primary)' } },
4699
+ // A touch list has no pointer highlight: a held option takes
4700
+ // the highlighted wash.
4701
+ item: { states: { pressed: { background: 'var(--color-base-200)' } } },
4702
+ },
4703
+ variants: {
4704
+ color: Object.fromEntries(ROLES.map((c) => [c, { popup: { base: {
4705
+ '--combobox-accent': `var(--color-${c})`,
4706
+ } } }])),
4707
+ },
4708
+ },
4613
4709
  },
4614
4710
  };
4615
4711
 
@@ -5374,6 +5470,30 @@ const treeRowSelectors: NonNullable<PartStyles['selectors']> = {
5374
5470
  },
5375
5471
  };
5376
5472
 
5473
+ /**
5474
+ * The tree rows' lynx states (signalxjs/lynx#1256). The web's press wash is
5475
+ * `[data-pressed]:not([data-disabled]):not([data-selected])`; lynx has no
5476
+ * absence classes, so the wash is the plain `pressed` state and a selected
5477
+ * row restores its accent fill below. The web's descendant rule that swaps
5478
+ * the check box's pair on a selected row (`[data-selected] &` on the box)
5479
+ * becomes the ROW's own custom properties: they inherit to the box inside
5480
+ * it. The focus ring is inset (rows sit flush) and drawn as a box-shadow,
5481
+ * which follows the row's radius where lynx's `outline` does not.
5482
+ */
5483
+ const lynxTreeRowStates: Record<string, CssProps> = {
5484
+ pressed: { background: 'var(--color-base-300)' },
5485
+ selected: {
5486
+ '--tree-check': 'var(--tree-on-accent)',
5487
+ '--tree-on-check': 'var(--tree-accent)',
5488
+ },
5489
+ ...lynxFocus({ inset: true }),
5490
+ };
5491
+
5492
+ /** A held selected row keeps its accent fill (the web's `:not([data-selected])`). */
5493
+ const lynxTreeRowSelectors: Record<string, CssProps> = {
5494
+ '&[data-selected][data-pressed]': { background: 'var(--tree-accent)' },
5495
+ };
5496
+
5377
5497
  /**
5378
5498
  * `tickGlyphFallback` for the node check box, whose tick is its `::after`:
5379
5499
  * the geometry drops and the same pseudo carries the glyph, in the medium's
@@ -5524,12 +5644,37 @@ export const treeView: RecipeInput = {
5524
5644
  xl: { root: { base: { '--tree-text': 'var(--text-lg)' } } },
5525
5645
  },
5526
5646
  },
5527
- // The indent, restated physically: logical spellings resolve on iOS but
5528
- // not on Android (measured, signalxjs/lynx#1084), so the emitter refuses
5529
- // them. Physical is the lynx target's norm — no RTL flow there.
5647
+ // lynx (signalxjs/lynx#1256, zero#478). No hover on a touch platform, no
5648
+ // `:not()`, no pseudo-elements, no descendant selectors: each web rule
5649
+ // those carry is restated on the part the runtime stamps. The skin's
5650
+ // `:dir(rtl)` mirror and the `@media` stops have no lynx form (no RTL
5651
+ // flow, no conditions) and stay dropped.
5530
5652
  targets: {
5531
5653
  lynx: {
5532
5654
  parts: {
5655
+ item: { states: lynxTreeRowStates, selectors: lynxTreeRowSelectors },
5656
+ 'branch-trigger': { states: lynxTreeRowStates, selectors: lynxTreeRowSelectors },
5657
+ // `inline-block` is not a lynx display value. The glyph is a
5658
+ // `<text>` part: a 1em box centred on it, so the quarter turn
5659
+ // pivots on the chevron instead of swinging it sideways.
5660
+ 'branch-indicator': { base: { display: 'flex', width: '1em', textAlign: 'center' } },
5661
+ // The mark is a `<text>` child lynx-zero renders (`✓` checked,
5662
+ // `−` indeterminate) in place of the web's clip-path `::after`
5663
+ // tick — clip-path and pseudo-elements are both dropped on
5664
+ // lynx. The glyph is the forced-colors fallback's shape
5665
+ // (`nodeTickFallback`), inked with the box's `-on` pair and
5666
+ // sized by inheritance from the row.
5667
+ 'node-checkbox': {
5668
+ base: {
5669
+ display: 'flex',
5670
+ alignItems: 'center',
5671
+ justifyContent: 'center',
5672
+ color: 'var(--tree-on-check)',
5673
+ },
5674
+ },
5675
+ // The indent, restated physically: logical spellings resolve
5676
+ // on iOS but not on Android (measured, signalxjs/lynx#1084),
5677
+ // so the emitter refuses them.
5533
5678
  'branch-content': { base: { paddingLeft: 'var(--space-lg)' } },
5534
5679
  },
5535
5680
  },
@@ -5633,6 +5778,10 @@ export const input: RecipeInput = {
5633
5778
  selectors: {
5634
5779
  // Same muting, same reason as `number-input/input` (#264).
5635
5780
  '&::placeholder': { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' },
5781
+ // zero draws its own ClearTrigger and clears on Escape, and Firefox
5782
+ // draws no native clear: hide the engine's cancel button everywhere (#446).
5783
+ '&::-webkit-search-cancel-button': { appearance: 'none', display: 'none' },
5784
+ '&::-webkit-search-decoration': { appearance: 'none' },
5636
5785
  },
5637
5786
  },
5638
5787
  // daisy's `label` inside `.input`: a quiet prefix/suffix at one edge,
@@ -6154,7 +6303,13 @@ export const alert: RecipeInput = {
6154
6303
  },
6155
6304
  },
6156
6305
  // Pinned top-right, its glyph on the padding edge. `inherit`
6157
- // is spelled as the root's own ink.
6306
+ // is spelled as the root's own ink. A square chip, the
6307
+ // glyph centred in it: the web's `line-height: 1` + 2xs
6308
+ // padding make an almost-square box, but lynx does not carry
6309
+ // the line-height into the glyph's `<text>`, so the padded
6310
+ // box was a narrow × in a tall line — a 14 × 20pt pill whose
6311
+ // press wash and focus ring drew as vertical ovals
6312
+ // (signalxjs/lynx#1253). The box is sized outright instead.
6158
6313
  close: {
6159
6314
  base: {
6160
6315
  position: 'absolute',
@@ -6162,6 +6317,13 @@ export const alert: RecipeInput = {
6162
6317
  right: 'calc(var(--alert-pad-x) - var(--space-2xs))',
6163
6318
  color: 'var(--color-base-content)',
6164
6319
  fontSize: 'var(--text-md)',
6320
+ display: 'flex',
6321
+ alignItems: 'center',
6322
+ justifyContent: 'center',
6323
+ boxSizing: 'border-box',
6324
+ padding: '0',
6325
+ width: 'calc(var(--text-md) + var(--space-2xs) * 2)',
6326
+ height: 'calc(var(--text-md) + var(--space-2xs) * 2)',
6165
6327
  },
6166
6328
  states: lynxFocus({ gap: 'var(--alert-tint)' }),
6167
6329
  },
@@ -6581,7 +6743,11 @@ export const divider: RecipeInput = {
6581
6743
  // no fill, its cross size following the label, a row when
6582
6744
  // horizontal. Both are lynx-zero rendering details, not
6583
6745
  // modifiers an author sets, so they are styled from raw lynx css
6584
- // (the toggle-group join ends' reasoning).
6746
+ // (the toggle-group join ends' reasoning). A segment centres
6747
+ // itself on the label (signalxjs/lynx#1272): it carries the
6748
+ // root's `align-self: stretch`, and a stretched item with a
6749
+ // definite thickness lands at cross-start on lynx — the top of
6750
+ // the label row, not the middle daisy's rules sit on.
6585
6751
  css: `
6586
6752
  .zx-divider__root.zx-m-labelled {
6587
6753
  --divider-ink: transparent;
@@ -6595,6 +6761,7 @@ export const divider: RecipeInput = {
6595
6761
  }
6596
6762
  .zx-divider__root.zx-m-segment {
6597
6763
  flex: 1 1 0;
6764
+ align-self: center;
6598
6765
  }
6599
6766
  .zx-divider__root.zx-m-segment.zx-o-horizontal {
6600
6767
  min-width: 0;
@@ -6806,6 +6973,15 @@ export const kbd: RecipeInput = {
6806
6973
  ...lynxSides('border{}Width', 'var(--border)', ['Top', 'Right', 'Left']),
6807
6974
  borderBottomWidth: 'calc(var(--border) * 2)',
6808
6975
  ...lynxSides('border{}Color', 'var(--color-base-300)'),
6976
+ // The web's monospace cap comes from the browser's
6977
+ // UA style for `<kbd>`, not the recipe; lynx has no
6978
+ // UA sheet, so the face is spelled here
6979
+ // (signalxjs/lynx#1254). Concrete names, not a
6980
+ // token: iOS Lynx walks the list with
6981
+ // `UIFont fontWithName:` (Menlo ships on iOS, the
6982
+ // CSS generics do not resolve there), and
6983
+ // `monospace` serves the lynx web target.
6984
+ fontFamily: 'Menlo, monospace',
6809
6985
  },
6810
6986
  },
6811
6987
  },
@@ -7524,14 +7700,43 @@ export const chat: RecipeInput = {
7524
7700
  xl: { bubble: { base: { fontSize: 'var(--text-lg)', padding: 'var(--space-md) var(--space-xl)' } } },
7525
7701
  },
7526
7702
  },
7527
- // The row's vertical padding, restated physically: logical spellings
7528
- // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
7529
- // the emitter refuses them.
7703
+ // Lynx (signalxjs/lynx#1276):
7704
+ // - the row's vertical padding, restated physically: logical spellings
7705
+ // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
7706
+ // the emitter refuses them;
7707
+ // - lynx has no grid, so the row is a flex column (header, bubble,
7708
+ // footer), gathered to the placement's side. The avatar leaves the
7709
+ // flow: lynx-zero pins it to the row's bottom corner and reserves its
7710
+ // measured width beside the column;
7711
+ // - the `[data-placement] > &` selectors have no class form on the
7712
+ // parts (placement is the root's), so the bubble's square tail corner
7713
+ // is raw lynx css, a descendant of the placed root — the shape the
7714
+ // theme restatements are proven to match. Physical corners: lynx has
7715
+ // no RTL flow.
7530
7716
  targets: {
7531
7717
  lynx: {
7532
7718
  parts: {
7533
- root: { base: { paddingTop: 'var(--space-2xs)', paddingBottom: 'var(--space-2xs)' } },
7719
+ root: {
7720
+ base: {
7721
+ display: 'flex',
7722
+ flexDirection: 'column',
7723
+ alignItems: 'flex-start',
7724
+ paddingTop: 'var(--space-2xs)',
7725
+ paddingBottom: 'var(--space-2xs)',
7726
+ },
7727
+ selectors: {
7728
+ '&[data-placement="end"]': { alignItems: 'flex-end' },
7729
+ },
7730
+ },
7534
7731
  },
7732
+ css: `
7733
+ .zx-chat__root.zx-p-start .zx-chat__bubble {
7734
+ border-bottom-left-radius: 0;
7735
+ }
7736
+ .zx-chat__root.zx-p-end .zx-chat__bubble {
7737
+ border-bottom-right-radius: 0;
7738
+ }
7739
+ `,
7535
7740
  },
7536
7741
  },
7537
7742
  };
@@ -7624,6 +7829,59 @@ export const chatLog: RecipeInput = {
7624
7829
  xl: { content: { base: { rowGap: 'var(--space-md)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 10)', fontSize: 'var(--text-sm)' } } },
7625
7830
  },
7626
7831
  },
7832
+ // Lynx (signalxjs/lynx#1276). lynx-zero's root is the frame and a native
7833
+ // scroll-view inside Content does the scrolling, so:
7834
+ // - the frame's padding moves onto the content (physical — logical
7835
+ // spellings do not resolve on Android, signalxjs/lynx#1084), so rows
7836
+ // scroll to the frame's edge instead of clipping at an inset;
7837
+ // - the content's grid is a flex column (lynx has no grid); a short
7838
+ // transcript sits at the top — `margin-block-start: auto` has nothing
7839
+ // to push against inside a scroll-view;
7840
+ // - the jump trigger floats in lynx-zero's absolute dock at the frame's
7841
+ // foot, so it drops `position: sticky` and the negative margin, and
7842
+ // restates its inline padding physically;
7843
+ // - `:hover` is dropped on touch: the held trigger shows the hover's
7844
+ // 90% darkening (restated per colour — a colour function over
7845
+ // `--chat-log-fill` cannot bake), with the existing 1px sink;
7846
+ // - the focus rings as `lynxFocus` (lynx#1163), the frame's drawn inside
7847
+ // like the web's negative offset.
7848
+ targets: {
7849
+ lynx: {
7850
+ parts: {
7851
+ root: {
7852
+ base: { padding: '0', overflow: 'hidden' },
7853
+ states: { 'focus-visible': lynxFocusRing('var(--color-base-content)', { inset: true }) },
7854
+ },
7855
+ content: {
7856
+ base: {
7857
+ display: 'flex',
7858
+ flexDirection: 'column',
7859
+ paddingTop: 'var(--space-sm)',
7860
+ paddingBottom: 'var(--space-sm)',
7861
+ paddingLeft: 'var(--space-md)',
7862
+ paddingRight: 'var(--space-md)',
7863
+ },
7864
+ },
7865
+ 'jump-trigger': {
7866
+ base: {
7867
+ position: 'relative',
7868
+ flexDirection: 'row',
7869
+ paddingLeft: 'var(--space-md)',
7870
+ paddingRight: 'var(--space-md)',
7871
+ },
7872
+ states: {
7873
+ pressed: { background: 'color-mix(in oklab, var(--color-primary) 90%, black)' },
7874
+ ...lynxFocus(),
7875
+ },
7876
+ },
7877
+ },
7878
+ variants: {
7879
+ color: Object.fromEntries(ROLES.map((c) => [c, { 'jump-trigger': { states: {
7880
+ pressed: { background: `color-mix(in oklab, var(--color-${c}) 90%, black)` },
7881
+ } } }])),
7882
+ },
7883
+ },
7884
+ },
7627
7885
  };
7628
7886
 
7629
7887
  /** daisy radial-progress: --value became --progress-percent; same ring. */
@@ -7875,6 +8133,41 @@ export const navbar: RecipeInput = {
7875
8133
  xl: { root: { base: { minBlockSize: '6rem', fontSize: 'var(--text-lg)' } } },
7876
8134
  },
7877
8135
  },
8136
+ // Lynx (signalxjs/lynx#1257): `min-block-size` is not a lynx property,
8137
+ // so the bar's height ramp is restated as `min-height`; and a lynx flex
8138
+ // container is only a row when it says so, so the bar and its three
8139
+ // sections spell `flex-direction: row`.
8140
+ //
8141
+ // No section squeezes a word (signalxjs/lynx#1274, zero#499). On the web
8142
+ // `flex: 1 1 0%` is floored by the flex item's automatic minimum (its
8143
+ // min-content width), so "Acme" never breaks. Lynx's flex layout has no
8144
+ // automatic minimum, and its `min-width` takes no intrinsic keyword, so
8145
+ // a zero basis shrank the start below its text and the `<text>` wrapped
8146
+ // per character ("Acm / e"). On lynx the ends start from their content
8147
+ // (`flex-basis: auto`), never shrink, and share the slack equally; the
8148
+ // centre takes daisy's own `flex-shrink: 0`. A bar too narrow for its
8149
+ // content overflows whole, as daisy's does — the btn rule (lynx#1165).
8150
+ // The cost: with a centre and unequal ends, the centre sits off-centre
8151
+ // by half the ends' difference (the web's is exact until a section
8152
+ // reaches its min-content).
8153
+ targets: {
8154
+ lynx: {
8155
+ parts: {
8156
+ root: { base: { flexDirection: 'row', minHeight: '4rem' } },
8157
+ start: { base: { flexDirection: 'row', flexBasis: 'auto', flexShrink: '0' } },
8158
+ center: { base: { flexDirection: 'row', flexShrink: '0' } },
8159
+ end: { base: { flexDirection: 'row', flexBasis: 'auto', flexShrink: '0' } },
8160
+ },
8161
+ variants: {
8162
+ size: {
8163
+ xs: { root: { base: { minHeight: '2.5rem' } } },
8164
+ sm: { root: { base: { minHeight: '3rem' } } },
8165
+ lg: { root: { base: { minHeight: '5rem' } } },
8166
+ xl: { root: { base: { minHeight: '6rem' } } },
8167
+ },
8168
+ },
8169
+ },
8170
+ },
7878
8171
  };
7879
8172
 
7880
8173
  /**
@@ -7934,6 +8227,14 @@ export const navList: RecipeInput = {
7934
8227
  alignItems: 'center',
7935
8228
  gap: 'var(--space-sm)',
7936
8229
  textDecoration: 'none',
8230
+ // Without an href the link is a <button> (#451): drop its chrome.
8231
+ appearance: 'none',
8232
+ background: 'none',
8233
+ border: '0',
8234
+ font: 'inherit',
8235
+ textAlign: 'start',
8236
+ inlineSize: '100%',
8237
+ cursor: 'pointer',
7937
8238
  color: 'var(--nav-ink)',
7938
8239
  padding: 'var(--space-sm) var(--space-lg)',
7939
8240
  borderRadius: 'var(--radius-field)',
@@ -7981,6 +8282,27 @@ export const navList: RecipeInput = {
7981
8282
  xl: { root: { base: { fontSize: 'var(--text-lg)' } }, link: { base: { padding: 'var(--space-lg) var(--space-xl)' } } },
7982
8283
  },
7983
8284
  },
8285
+ // The web's block and inline boxes, restated for lynx (signalxjs/lynx#1259).
8286
+ // No `:hover` on a touch platform, and the link's anatomy declares no
8287
+ // `pressed` flag: a held link has lynx-zero's main-thread press scale.
8288
+ targets: {
8289
+ lynx: {
8290
+ parts: {
8291
+ // `display: block` has no lynx projection: a column, so the
8292
+ // link spans the list as a block `<li>` makes it.
8293
+ item: { base: { display: 'flex', flexDirection: 'column' } },
8294
+ // The icon / label / meta row, explicit (lynx does not
8295
+ // default the direction the way a browser does).
8296
+ link: { base: { flexDirection: 'row' } },
8297
+ // `inline-size` restated physically; the glyph centred in
8298
+ // its column on both axes.
8299
+ icon: { base: { width: '1.25em', flexDirection: 'row', alignItems: 'center' } },
8300
+ // The count's push to the far edge, restated physically —
8301
+ // `margin-inline-start` does not resolve on Android (#1084).
8302
+ meta: { base: { marginLeft: 'auto', flexDirection: 'row' } },
8303
+ },
8304
+ },
8305
+ },
7984
8306
  };
7985
8307
 
7986
8308
  /**
@@ -8090,6 +8412,37 @@ export const breadcrumbs: RecipeInput = {
8090
8412
  xl: { root: { base: { fontSize: 'var(--text-lg)' } } },
8091
8413
  },
8092
8414
  },
8415
+ // Lynx (signalxjs/lynx#1257):
8416
+ // - The list, the items and the ellipsis are explicit rows (a lynx flex
8417
+ // container is only a row when it says so).
8418
+ // - The trigger's `hover` ink is dropped on a touch platform; the
8419
+ // `pressed` flag carries it instead. Its resting ink moves onto a
8420
+ // lynx-only token so the base rule stays a plain var() read: the
8421
+ // emitter bakes a `color-mix` declaration per theme under the host
8422
+ // class, and that baked rule would outrank the pressed one.
8423
+ // - The focus ring is the lynx box-shadow ring (outline has no offset
8424
+ // and ignores the radius there).
8425
+ // - The link's `hover` stays dropped: the link anatomy declares no
8426
+ // pressed flag, so there is nothing on lynx to carry it.
8427
+ // - The separator is a real part on both targets (lynx-zero renders it
8428
+ // as a text glyph), so daisy's `::before` chevron needs no lynx form.
8429
+ targets: {
8430
+ lynx: {
8431
+ tokens: { '--bc-trigger-ink': 'color-mix(in oklch, var(--color-base-content) 75%, transparent)' },
8432
+ parts: {
8433
+ list: { base: { flexDirection: 'row' } },
8434
+ item: { base: { flexDirection: 'row' } },
8435
+ ellipsis: { base: { flexDirection: 'row' } },
8436
+ 'ellipsis-trigger': {
8437
+ base: { color: 'var(--bc-trigger-ink)' },
8438
+ states: {
8439
+ pressed: { color: 'var(--color-base-content)' },
8440
+ 'focus-visible': lynxFocusRing('var(--color-primary)'),
8441
+ },
8442
+ },
8443
+ },
8444
+ },
8445
+ },
8093
8446
  };
8094
8447
 
8095
8448
  /**
@@ -8128,6 +8481,19 @@ const pageTrigger: PartStyles = {
8128
8481
  },
8129
8482
  };
8130
8483
 
8484
+ /** A pagination cell's box on lynx — see the recipe's lynx section. */
8485
+ const pageCellLynx: NonNullable<PartStyles['base']> = {
8486
+ minWidth: 'var(--pg-size)',
8487
+ height: 'var(--pg-size)',
8488
+ flexShrink: '0',
8489
+ };
8490
+
8491
+ /** The four triggers on lynx: the cell box, the held wash + sink, the ring. */
8492
+ const pageTriggerLynx: PartStyles = {
8493
+ base: pageCellLynx,
8494
+ states: { pressed: { background: 'var(--color-base-300)' }, ...lynxFocus() },
8495
+ };
8496
+
8131
8497
  /**
8132
8498
  * Pagination — daisy's join-of-buttons made of standalone btn cells: the
8133
8499
  * quiet base-200 fill, base-300 hover, the current page inverted into the
@@ -8224,13 +8590,35 @@ export const pagination: RecipeInput = {
8224
8590
  xl: { root: { base: { '--pg-size': 'calc(var(--size-field) * 14)', '--pg-font': 'var(--text-lg)' } } },
8225
8591
  },
8226
8592
  },
8227
- // The cell's inline padding, restated physically: logical spellings
8228
- // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
8229
- // the emitter refuses them.
8593
+ // Lynx (signalxjs/lynx#1258):
8594
+ // - the cells' size, restated physically: `min-inline-size` and
8595
+ // `block-size` do not resolve on lynx (lynx#1145), so every cell
8596
+ // collapsed to its glyph. Read through `--pg-size`, so the compile
8597
+ // inlines the size steps per part, as it does the web's;
8598
+ // - the item's inline padding, physical: logical spellings resolve on
8599
+ // iOS but not on Android (measured, lynx#1084);
8600
+ // - the press: the web answers a pointer with `:hover`, which a touch
8601
+ // platform drops, so the held cell paints daisy's hover wash with the
8602
+ // 1px sink (`lynxBtnPressed`'s pair) — the current page keeps its
8603
+ // accent fill and only sinks;
8604
+ // - the focus ring as `lynxFocus`: lynx's outline has no offset and
8605
+ // ignores the radius (lynx#1163);
8606
+ // - no row shrink: a cell never squeezes below its size in a narrow row.
8607
+ // The glyphs' rtl flip has no replacement: lynx has no RTL flow.
8230
8608
  targets: {
8231
8609
  lynx: {
8232
8610
  parts: {
8233
- item: { base: { paddingLeft: 'var(--space-2xs)', paddingRight: 'var(--space-2xs)' } },
8611
+ root: { base: { flexDirection: 'row' } },
8612
+ item: {
8613
+ base: { ...pageCellLynx, paddingLeft: 'var(--space-2xs)', paddingRight: 'var(--space-2xs)' },
8614
+ states: lynxFocus(),
8615
+ selectors: { '&[data-pressed][data-state="inactive"]': { background: 'var(--color-base-300)' } },
8616
+ },
8617
+ ellipsis: { base: pageCellLynx },
8618
+ 'first-trigger': pageTriggerLynx,
8619
+ 'prev-trigger': pageTriggerLynx,
8620
+ 'next-trigger': pageTriggerLynx,
8621
+ 'last-trigger': pageTriggerLynx,
8234
8622
  },
8235
8623
  },
8236
8624
  },
@@ -8250,6 +8638,12 @@ const stepInk = (accent: string, role: string): string =>
8250
8638
  `color-mix(in oklab, ${accent} `
8251
8639
  + `${Math.min(ROLE_INK_KEEP[role as keyof typeof roles] ?? 55, 70)}%, var(--color-base-content))`;
8252
8640
 
8641
+ /**
8642
+ * The complete disc's fill: 20% of the accent over base-100, in OKLAB (see
8643
+ * the indicator's `complete` state for why not OKLCH).
8644
+ */
8645
+ const stepTint = (accent: string): string => `color-mix(in oklab, ${accent} 20%, var(--color-base-100))`;
8646
+
8253
8647
  /** A Steps root holding wizard parts (#296) — the panel or the triggers. */
8254
8648
  const STEPS_WIZARD = '&:has(> [data-scope="steps"]:is([data-part="content"], [data-part="prev-trigger"], [data-part="next-trigger"]))';
8255
8649
 
@@ -8367,7 +8761,7 @@ export const steps: RecipeInput = {
8367
8761
  // (warning's yellow came out pink) — which the browser parity
8368
8762
  // check caught once per-step colour put all eight roles on
8369
8763
  // the disc (#112).
8370
- complete: { background: 'color-mix(in oklab, var(--steps-accent) 20%, var(--color-base-100))', color: 'var(--steps-complete-ink)' },
8764
+ complete: { background: stepTint('var(--steps-accent)'), color: 'var(--steps-complete-ink)' },
8371
8765
  inactive: { background: 'var(--color-base-200)', color: 'var(--color-base-content)' },
8372
8766
  invalid: { background: 'var(--color-error)', color: 'var(--color-error-content)' },
8373
8767
  },
@@ -8455,32 +8849,82 @@ export const steps: RecipeInput = {
8455
8849
  xl: { root: { base: { '--steps-ind': 'calc(var(--size-selector) * 9)', '--steps-font': 'var(--text-md)' } } },
8456
8850
  },
8457
8851
  },
8458
- // The bridge's anchors and the vertical item's tail padding, restated
8459
- // physically: logical spellings resolve on iOS but not on Android
8460
- // (measured, signalxjs/lynx#1084), so the emitter refuses them. Physical
8461
- // is the lynx target's norm — no RTL flow there to mirror with.
8852
+ // Lynx. Physical spellings throughout: logical insets, margins and
8853
+ // paddings resolve on iOS but not on Android (measured,
8854
+ // signalxjs/lynx#1084), and `inline-size` / `block-size` do not resolve
8855
+ // at all (lynx#1145) — without the restatement the discs and lines did
8856
+ // not paint. Also (signalxjs/lynx#1258):
8857
+ // - the complete disc's tint and ink read `--steps-accent` through a
8858
+ // colour function, which cannot bake; the base pair is restated over
8859
+ // primary, and each colour already restates the ink on its item, so
8860
+ // the tint joins it there;
8861
+ // - the bridge runs from this disc's edge to the next disc's edge —
8862
+ // `50% + ind/2` for `100% - ind` — instead of centre to centre under
8863
+ // the disc, so it needs no z-index across sibling items; the item
8864
+ // stops clipping (a lynx view's `overflow` defaults to hidden) so the
8865
+ // bridge can reach past its box;
8866
+ // - the `:has()` wizard wrap becomes the `wizard` stamp lynx-zero puts on
8867
+ // the root while a panel or trigger is mounted (raw css below);
8868
+ // - the focus ring as `lynxFocus` (lynx#1163), and Back/Next's press as
8869
+ // daisy's hover wash + sink (`lynxBtnPressed`), `:hover` being dropped.
8462
8870
  targets: {
8463
8871
  lynx: {
8872
+ tokens: {
8873
+ '--steps-complete-ink': stepInk('var(--color-primary)', 'primary'),
8874
+ '--steps-complete-tint': stepTint('var(--color-primary)'),
8875
+ },
8464
8876
  parts: {
8877
+ root: { base: { flexDirection: 'row', overflow: 'visible' } },
8465
8878
  item: {
8879
+ base: { overflow: 'visible' },
8880
+ states: lynxFocus(),
8466
8881
  selectors: {
8467
8882
  '&[data-orientation="vertical"]': { paddingBottom: 'var(--space-lg)' },
8468
8883
  },
8469
8884
  },
8885
+ indicator: {
8886
+ base: { width: 'var(--steps-ind)', height: 'var(--steps-ind)' },
8887
+ states: { complete: { background: 'var(--steps-complete-tint)' } },
8888
+ },
8470
8889
  separator: {
8471
8890
  selectors: {
8472
8891
  '&[data-orientation="horizontal"]': {
8473
- top: 'calc(var(--space-xs) + var(--steps-ind) / 2)',
8474
- left: '50%',
8892
+ top: 'calc(var(--space-xs) + (var(--steps-ind) - var(--border)) / 2)',
8893
+ left: 'calc(50% + var(--steps-ind) / 2)',
8894
+ width: 'calc(100% - var(--steps-ind))',
8895
+ height: 'var(--border)',
8475
8896
  },
8476
8897
  '&[data-orientation="vertical"]': {
8477
8898
  left: 'calc(var(--space-xs) + (var(--steps-ind) - var(--border)) / 2)',
8478
8899
  top: 'calc(var(--space-xs) + var(--steps-ind))',
8479
8900
  bottom: 'calc(var(--space-xs) * -1)',
8901
+ width: 'var(--border)',
8480
8902
  },
8481
8903
  },
8482
8904
  },
8905
+ 'prev-trigger': {
8906
+ base: { ...lynxBtnPad(4), ...lynxBtnFit },
8907
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
8908
+ },
8909
+ 'next-trigger': {
8910
+ base: { ...lynxBtnPad(4), ...lynxBtnFit, marginLeft: 'auto' },
8911
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
8912
+ },
8483
8913
  },
8914
+ variants: {
8915
+ color: Object.fromEntries(ROLES.map((c) => [c, { item: { base: {
8916
+ '--steps-complete-tint': stepTint(`var(--color-${c})`),
8917
+ } } }])),
8918
+ },
8919
+ // lynx-zero stamps `wizard` on the root while a Content,
8920
+ // PrevTrigger or NextTrigger is mounted — the web's `:has()`.
8921
+ // A lynx-zero rendering detail, not a modifier an author sets.
8922
+ css: `
8923
+ .zx-steps__root.zx-m-wizard {
8924
+ flex-wrap: wrap;
8925
+ row-gap: var(--space-md);
8926
+ }
8927
+ `,
8484
8928
  },
8485
8929
  },
8486
8930
  };
@@ -8662,11 +9106,68 @@ export const drawer: RecipeInput = {
8662
9106
  },
8663
9107
  },
8664
9108
  },
8665
- // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
9109
+ // The btn paddings, restated physically — see `lynxBtnPad` (#1084) —
9110
+ // and the btns' press rendering (`lynxBtnPressed`), as dialog wears
9111
+ // them. The edge sheet (signalxjs/lynx#1277): lynx-zero lays the
9112
+ // panel on its edge with flexbox inside the backdrop, so the sheet
9113
+ // stays in flow (`position: relative`, never `fixed`) and sizes to
9114
+ // it — no viewport units, which lynx does not resolve against the
9115
+ // overlay outlet. The skin keeps the cap, the corners and the slide.
8666
9116
  lynx: {
9117
+ keyframes: {
9118
+ // `translate` is Android-asymmetric (#1084): the travel is a
9119
+ // transform function, one per edge.
9120
+ 'zero-daisy-drawer-in-start': 'from { transform: translateX(-100%); } to { transform: translateX(0); }',
9121
+ 'zero-daisy-drawer-in-end': 'from { transform: translateX(100%); } to { transform: translateX(0); }',
9122
+ 'zero-daisy-drawer-in-top': 'from { transform: translateY(-100%); } to { transform: translateY(0); }',
9123
+ 'zero-daisy-drawer-in-bottom': 'from { transform: translateY(100%); } to { transform: translateY(0); }',
9124
+ },
8667
9125
  parts: {
8668
- trigger: { base: lynxBtnPad(4) },
8669
- close: { base: lynxBtnPad(4) },
9126
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
9127
+ close: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
9128
+ panel: {
9129
+ // The cap alone: `min(20rem, 85vw)` has no lynx spelling
9130
+ // (no min()); lynx-zero states the 85% share inline and
9131
+ // this caps it — together, the web's width.
9132
+ base: { '--l-measure': '20rem' },
9133
+ selectors: {
9134
+ '&[data-l-dock="sheet"]': {
9135
+ position: 'relative',
9136
+ blockSize: 'auto',
9137
+ maxBlockSize: 'none',
9138
+ },
9139
+ // The block edges run full width (the web's `:where()`
9140
+ // pair, one selector each — no `:where()` on lynx),
9141
+ // content-sized up to 85% of the outlet.
9142
+ '&[data-placement="top"]': { '--l-measure': 'none' },
9143
+ '&[data-placement="bottom"]': { '--l-measure': 'none' },
9144
+ '&[data-placement="top"][data-l-dock="sheet"]': {
9145
+ maxBlockSize: '85%',
9146
+ borderBottomLeftRadius: 'var(--radius-box)',
9147
+ borderBottomRightRadius: 'var(--radius-box)',
9148
+ },
9149
+ '&[data-placement="bottom"][data-l-dock="sheet"]': {
9150
+ maxBlockSize: '85%',
9151
+ borderTopLeftRadius: 'var(--radius-box)',
9152
+ borderTopRightRadius: 'var(--radius-box)',
9153
+ },
9154
+ // The slide in from the edge (the web's `translate`
9155
+ // over `@starting-style`, both dropped here). The
9156
+ // exit is an unmount on lynx, so only the entry runs.
9157
+ '&[data-placement="start"][data-l-dock="sheet"][data-state="open"]': {
9158
+ animation: 'zero-daisy-drawer-in-start var(--duration-slow) var(--ease-standard)',
9159
+ },
9160
+ '&[data-placement="end"][data-l-dock="sheet"][data-state="open"]': {
9161
+ animation: 'zero-daisy-drawer-in-end var(--duration-slow) var(--ease-standard)',
9162
+ },
9163
+ '&[data-placement="top"][data-l-dock="sheet"][data-state="open"]': {
9164
+ animation: 'zero-daisy-drawer-in-top var(--duration-slow) var(--ease-standard)',
9165
+ },
9166
+ '&[data-placement="bottom"][data-l-dock="sheet"][data-state="open"]': {
9167
+ animation: 'zero-daisy-drawer-in-bottom var(--duration-slow) var(--ease-standard)',
9168
+ },
9169
+ },
9170
+ },
8670
9171
  },
8671
9172
  variants: { size: lynxBtnSizes('trigger') },
8672
9173
  },
@@ -8866,6 +9367,89 @@ export const table: RecipeInput = {
8866
9367
  },
8867
9368
  },
8868
9369
  },
9370
+ // signalxjs/lynx#1279. Lynx has no table layout: lynx-zero lays the
9371
+ // parts out as flex rows itself. The `hover` modifier has no lynx
9372
+ // answer (a touch screen has no hover), nor has the viewport-width
9373
+ // `@media` of the stacked mode — lynx-zero resolves `stack` in JS and
9374
+ // stamps the parts instead (see `css` below).
9375
+ targets: {
9376
+ lynx: {
9377
+ parts: {
9378
+ // The web's `overflow-x: auto` box: a lynx view clips its
9379
+ // rounded frame; a table of fixed columns scrolls in a
9380
+ // scroll-view lynx-zero renders.
9381
+ root: {
9382
+ base: { overflow: 'hidden' },
9383
+ // daisy's ring drawn inside the box, as on the web
9384
+ // (outline-offset: -2px) — spelled as an inset shadow.
9385
+ states: { 'focus-visible': lynxFocusRing('var(--color-base-content)', { inset: true }) },
9386
+ },
9387
+ // The muted header ink: the colour function over
9388
+ // `--table-accent` cannot bake (a recipe-local property),
9389
+ // so the default accent's mix is restated here and each
9390
+ // colour's below.
9391
+ 'header-cell': { base: { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' } },
9392
+ 'cell-label': { base: { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' } },
9393
+ // The web's hover ink, while held; the lynx ring.
9394
+ 'sort-trigger': {
9395
+ base: { flexDirection: 'row' },
9396
+ states: { pressed: { color: 'var(--table-accent)' }, ...lynxFocus() },
9397
+ },
9398
+ },
9399
+ variants: {
9400
+ color: Object.fromEntries(ROLES.map((c) => {
9401
+ const muted = `color-mix(in oklab, ${roleInk(c)} 60%, transparent)`;
9402
+ return [c, {
9403
+ 'header-cell': { base: { color: muted } },
9404
+ 'cell-label': { base: { color: muted } },
9405
+ }];
9406
+ })),
9407
+ },
9408
+ // lynx-zero rendering details, not modifiers an author sets:
9409
+ // - `stripe`: lynx has no `:nth-child`, so the body stamps its
9410
+ // even, unselected rows (mount order) under `zebra`.
9411
+ // - `held`: the unsorted mark the web reveals under a hovered or
9412
+ // keyboard-focused trigger, shown while the trigger is held.
9413
+ // - `stacked`: the stacked mode's card chrome (the web's
9414
+ // below-breakpoint rules). Zero widths and paddings keep a
9415
+ // var() so they beat the var-bearing base values (lynx expands
9416
+ // var() declarations after the cascade, signalxjs/lynx#1161);
9417
+ // the root keeps its base-100 fill (a var-free transparent
9418
+ // would lose the same way).
9419
+ css: `
9420
+ .zx-table__root.zx-m-stacked {
9421
+ border-width: calc(var(--border) * 0);
9422
+ border-radius: calc(var(--radius-box) * 0);
9423
+ }
9424
+ .zx-table__caption.zx-m-stacked {
9425
+ padding-left: calc(var(--table-pad-inline) * 0);
9426
+ padding-right: calc(var(--table-pad-inline) * 0);
9427
+ }
9428
+ .zx-table__row.zx-m-stacked {
9429
+ border: var(--border) solid var(--color-base-200);
9430
+ border-radius: var(--radius-box);
9431
+ background: var(--color-base-100);
9432
+ padding: var(--space-sm) var(--table-pad-inline);
9433
+ margin-bottom: var(--space-md);
9434
+ }
9435
+ .zx-table__row.zx-m-stacked.zx-f-selected {
9436
+ background: var(--color-base-300);
9437
+ }
9438
+ .zx-table__cell.zx-m-stacked {
9439
+ padding-left: calc(var(--table-pad-inline) * 0);
9440
+ padding-right: calc(var(--table-pad-inline) * 0);
9441
+ padding-top: var(--space-xs);
9442
+ padding-bottom: var(--space-xs);
9443
+ }
9444
+ .zx-table__row.zx-m-stripe {
9445
+ background: var(--color-base-200);
9446
+ }
9447
+ .zx-table__sort-indicator.zx-m-held.zx-s-none {
9448
+ opacity: 0.6;
9449
+ }
9450
+ `,
9451
+ },
9452
+ },
8869
9453
  };
8870
9454
 
8871
9455
  /**
@@ -9032,6 +9616,65 @@ export const fileUpload: RecipeInput = {
9032
9616
  xl: { root: { base: { '--fu-pad': 'calc(var(--space-xl) * 1.25)', '--fu-font': 'var(--text-md)' } } },
9033
9617
  },
9034
9618
  },
9619
+ // Lynx (signalxjs/lynx#1276):
9620
+ // - lynx has no grid: the root and the item list are flex columns, and
9621
+ // the stretched parts (dropzone, item list) say so with `align-self`;
9622
+ // the rows (trigger, item) state `flex-direction: row`;
9623
+ // - the dropzone centres its words in a column (lynx text alignment is
9624
+ // the text's own), and its highlighted wash — a colour function over
9625
+ // `--fu-accent`, which cannot bake — is restated per colour over the
9626
+ // role. lynx never drives `highlighted` (no drag source); it shows
9627
+ // through `ForceStates` in the gallery;
9628
+ // - `:hover` is dropped on touch: the held trigger shows daisy's
9629
+ // base-300 wash + 1px sink (`lynxBtnPressed`, in place of the
9630
+ // standalone `translate`, which Android does not resolve), and the
9631
+ // held × and clear show the ghost's base-200 wash;
9632
+ // - the focus rings as `lynxFocus` (lynx#1163).
9633
+ targets: {
9634
+ lynx: {
9635
+ parts: {
9636
+ root: { base: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start' } },
9637
+ trigger: {
9638
+ base: { flexDirection: 'row' },
9639
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
9640
+ },
9641
+ dropzone: {
9642
+ base: { alignSelf: 'stretch', display: 'flex', flexDirection: 'column', alignItems: 'center' },
9643
+ states: {
9644
+ highlighted: { background: 'color-mix(in oklab, var(--color-primary) 8%, var(--color-base-100))' },
9645
+ },
9646
+ },
9647
+ 'item-group': {
9648
+ base: { alignSelf: 'stretch', display: 'flex', flexDirection: 'column' },
9649
+ },
9650
+ item: { base: { flexDirection: 'row' } },
9651
+ 'item-remove': {
9652
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
9653
+ },
9654
+ 'clear-trigger': {
9655
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
9656
+ },
9657
+ },
9658
+ variants: {
9659
+ color: Object.fromEntries(ROLES.map((c) => [c, { dropzone: { states: {
9660
+ highlighted: { background: `color-mix(in oklab, var(--color-${c}) 8%, var(--color-base-100))` },
9661
+ } } }])),
9662
+ },
9663
+ },
9664
+ },
9665
+ };
9666
+
9667
+ /** A carousel nav circle on lynx: the glyph centred as flex (no `grid`). */
9668
+ const carouselTriggerLynx: CssProps = {
9669
+ display: 'flex',
9670
+ alignItems: 'center',
9671
+ justifyContent: 'center',
9672
+ };
9673
+
9674
+ /** A held carousel nav circle on lynx: daisy's hover wash plus the 1px sink, and the lynx ring. */
9675
+ const carouselTriggerLynxStates: Record<string, CssProps> = {
9676
+ pressed: { background: 'var(--color-base-300)', transform: 'translateY(1px)' },
9677
+ ...lynxFocus(),
9035
9678
  };
9036
9679
 
9037
9680
  /**
@@ -9184,14 +9827,60 @@ export const carousel: RecipeInput = {
9184
9827
  // spellings resolve on iOS but not on Android (measured,
9185
9828
  // signalxjs/lynx#1084), so the emitter refuses them. Physical is the
9186
9829
  // lynx target's norm — no RTL flow there, so `start` IS left.
9830
+ //
9831
+ // signalxjs/lynx#1279 — the rest of the lynx shape:
9832
+ // - `grid` does not lay out on lynx: the root is a flex column, and a
9833
+ // trigger centres its glyph as flex.
9834
+ // - The dot: the web draws it on `::before` inside a
9835
+ // `max(dot, 1.5rem)` button; lynx has neither pseudo-elements nor
9836
+ // `max()`, so lynx-zero renders the dot AS the indicator (its touch
9837
+ // area widened with `hit-slop`) and the ring and the active fill ride
9838
+ // the part.
9839
+ // - The web sinks a held trigger with the standalone `translate`,
9840
+ // which Android ignores (#1084): a transform function here, over
9841
+ // daisy's hover wash (no hover on touch).
9842
+ // - Rings: lynx's outline ignores radius and has no offset (#1163).
9843
+ // No replacement for the viewport's `:focus-visible` ring: there is no
9844
+ // keyboard focus on lynx, and the part declares no flag to key it on.
9187
9845
  targets: {
9188
9846
  lynx: {
9189
9847
  parts: {
9848
+ root: { base: { display: 'flex', flexDirection: 'column' } },
9190
9849
  'prev-trigger': {
9191
- base: { top: 'calc(50% - var(--carousel-nav) / 2)', left: 'var(--space-sm)' },
9850
+ base: {
9851
+ top: 'calc(50% - var(--carousel-nav) / 2)',
9852
+ left: 'var(--space-sm)',
9853
+ ...carouselTriggerLynx,
9854
+ },
9855
+ states: carouselTriggerLynxStates,
9192
9856
  },
9193
9857
  'next-trigger': {
9194
- base: { top: 'calc(50% - var(--carousel-nav) / 2)', right: 'var(--space-sm)' },
9858
+ base: {
9859
+ top: 'calc(50% - var(--carousel-nav) / 2)',
9860
+ right: 'var(--space-sm)',
9861
+ ...carouselTriggerLynx,
9862
+ },
9863
+ states: carouselTriggerLynxStates,
9864
+ },
9865
+ 'indicator-group': { base: { flexDirection: 'row', alignItems: 'center' } },
9866
+ indicator: {
9867
+ base: {
9868
+ display: 'flex',
9869
+ width: 'var(--carousel-dot)',
9870
+ height: 'var(--carousel-dot)',
9871
+ flexShrink: '0',
9872
+ // Longhands: the ring's colour bakes per theme only
9873
+ // on its own (a shorthand mixing it with the
9874
+ // --border calc cannot).
9875
+ borderWidth: 'calc(var(--border) * 2)',
9876
+ borderStyle: 'solid',
9877
+ borderColor: 'color-mix(in oklab, var(--color-base-content) 70%, transparent)',
9878
+ borderRadius: '9999px',
9879
+ },
9880
+ states: {
9881
+ active: { background: 'var(--carousel-accent)', borderColor: 'var(--carousel-accent)' },
9882
+ ...lynxFocus(),
9883
+ },
9195
9884
  },
9196
9885
  },
9197
9886
  },