@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/dist/recipes.js CHANGED
@@ -1348,7 +1348,17 @@ export const tooltip = {
1348
1348
  targets: {
1349
1349
  web: floatingArrow('tooltip', { background: 'var(--color-neutral)' }, '0.5rem'),
1350
1350
  lynx: {
1351
- parts: { trigger: { base: lynxBtnPad(4) } },
1351
+ parts: {
1352
+ // Content-sized like daisy's btn, never broken mid-word
1353
+ // (signalxjs/lynx#1165). No pressed rule: the anatomy
1354
+ // declares none — the long press paints the `open` state.
1355
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit } },
1356
+ // The arrow's paint: the bubble's neutral, continued. The
1357
+ // web's `floatingArrow` geometry rides runtime custom
1358
+ // properties lynx does not write, so lynx-zero places the
1359
+ // 8px square and turns it itself (signalxjs/lynx#1277).
1360
+ arrow: { base: { background: 'var(--color-neutral)' } },
1361
+ },
1352
1362
  variants: { size: lynxBtnSizes('trigger') },
1353
1363
  },
1354
1364
  },
@@ -1558,12 +1568,34 @@ export const menu = {
1558
1568
  // Trigger-carried axes — same wiring as dialog, same reason. The dropdown
1559
1569
  // and its items are top-layer siblings the donut cannot reach.
1560
1570
  variants: { color: btnColors(), size: btnSizes },
1561
- // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
1571
+ // The btn paddings, restated physically — see `lynxBtnPad` (#1084) —
1572
+ // and the trigger's press rendering, as popover's (`lynxBtnPressed`).
1562
1573
  targets: {
1563
1574
  web: floatingArrow('menu', panelArrowPaint),
1564
1575
  lynx: {
1565
1576
  parts: {
1566
- trigger: { base: lynxBtnPad(4) },
1577
+ // Content-sized like daisy's btn (signalxjs/lynx#1165), an
1578
+ // explicit row (lynx does not default the direction the way a
1579
+ // browser does), the held state in place of the dropped
1580
+ // `:hover`, and the box-shadow ring (signalxjs/lynx#1163).
1581
+ trigger: {
1582
+ base: { ...lynxBtnPad(4), ...lynxBtnFit, flexDirection: 'row' },
1583
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
1584
+ },
1585
+ // The popups' rows stack and span the panel: a flex column
1586
+ // (a lynx view's default is a shrink-wrapping linear box).
1587
+ popup: { base: { display: 'flex', flexDirection: 'column' } },
1588
+ 'sub-popup': { base: { display: 'flex', flexDirection: 'column' } },
1589
+ // Every row kind is a mark / label / shortcut / chevron row.
1590
+ // A touch list has no hover: lynx-zero stamps `highlighted` on
1591
+ // the row under the finger, so the web's highlight wash is the
1592
+ // press rendering as it stands. The ✓ (item-indicator) and ›
1593
+ // (sub-trigger) the web draws with `::after` render as
1594
+ // `<text>` glyphs in lynx-zero — there is no pseudo-element.
1595
+ item: { base: { flexDirection: 'row' } },
1596
+ 'checkbox-item': { base: { flexDirection: 'row' } },
1597
+ 'radio-item': { base: { flexDirection: 'row' } },
1598
+ 'sub-trigger': { base: { flexDirection: 'row' } },
1567
1599
  // The shortcut's reading-end push, restated physically (#1084).
1568
1600
  shortcut: { base: { marginLeft: 'auto', paddingLeft: 'var(--space-lg)' } },
1569
1601
  },
@@ -4505,6 +4537,70 @@ export const combobox = {
4505
4537
  },
4506
4538
  targets: {
4507
4539
  web: { parts: { popup: { base: anchoredListbox } } },
4540
+ // lynx-zero's combobox (signalxjs/lynx#1278): a native text field in
4541
+ // a painted `control` view, the list in the overlay outlet.
4542
+ lynx: {
4543
+ parts: {
4544
+ // select's bounds (signalxjs/lynx#1146): 20rem, never wider
4545
+ // than the container. The web's 13rem control floor
4546
+ // overflowed any cell narrower than 208px on a phone; the
4547
+ // control fills the root instead.
4548
+ root: { base: { width: '20rem', maxWidth: '100%', minWidth: '3rem' } },
4549
+ // The web `:hover` border is dropped on a touch platform. The
4550
+ // ring is the box-shadow spelling (signalxjs/lynx#1163): lynx
4551
+ // has no `outline-offset`, and its outline ignores the radius.
4552
+ control: {
4553
+ base: { minWidth: '3rem', width: '100%' },
4554
+ states: lynxFocus(),
4555
+ },
4556
+ // `color: inherit` never reaches a native text field, and
4557
+ // `::placeholder` does not exist: the ink is explicit, and the
4558
+ // placeholder takes lynx's `-x-placeholder-color` — the web's
4559
+ // 60% mix, baked per theme (input's spelling; its constant is
4560
+ // declared below this recipe, so the value is restated).
4561
+ input: {
4562
+ base: {
4563
+ color: 'var(--color-base-content)',
4564
+ '-x-placeholder-color': 'color-mix(in oklab, var(--color-base-content) 60%, transparent)',
4565
+ flexGrow: '1',
4566
+ flexShrink: '1',
4567
+ },
4568
+ },
4569
+ // The chevron and the × are `<view>`s holding a glyph: centred
4570
+ // in the stretched row (a lynx view lays its child out at the
4571
+ // top), in explicit ink. A held chevron comes up to full ink —
4572
+ // the touch answer to the web's resting 60%. The clear-trigger
4573
+ // declares no pressed flag; its feedback is the text leaving.
4574
+ trigger: {
4575
+ base: { color: 'var(--color-base-content)', display: 'flex', alignItems: 'center', justifyContent: 'center' },
4576
+ states: { pressed: { opacity: '1' } },
4577
+ },
4578
+ 'clear-trigger': {
4579
+ base: { color: 'var(--color-base-content)', display: 'flex', alignItems: 'center', justifyContent: 'center' },
4580
+ },
4581
+ // The chip's ring is the lynx one; its remove button's ring is
4582
+ // drawn inside, so it never paints over the chip's label.
4583
+ tag: { states: lynxFocus() },
4584
+ 'tag-remove': {
4585
+ base: { color: 'var(--color-base-content)' },
4586
+ states: lynxFocus({ inset: true }),
4587
+ },
4588
+ // The popup renders in the overlay outlet, outside the root,
4589
+ // so it cannot inherit `--combobox-accent`, and an unresolved
4590
+ // `var()` on lynx has no fallback (select's
4591
+ // signalxjs/lynx#1168). The popup restates the accent; its
4592
+ // colour-axis twins are in `variants.color` below.
4593
+ popup: { base: { '--combobox-accent': 'var(--color-primary)' } },
4594
+ // A touch list has no pointer highlight: a held option takes
4595
+ // the highlighted wash.
4596
+ item: { states: { pressed: { background: 'var(--color-base-200)' } } },
4597
+ },
4598
+ variants: {
4599
+ color: Object.fromEntries(ROLES.map((c) => [c, { popup: { base: {
4600
+ '--combobox-accent': `var(--color-${c})`,
4601
+ } } }])),
4602
+ },
4603
+ },
4508
4604
  },
4509
4605
  };
4510
4606
  // daisy "btn" that stays pressed: the off state is the neutral btn, the on
@@ -5253,6 +5349,28 @@ const treeRowSelectors = {
5253
5349
  background: 'var(--color-base-300)',
5254
5350
  },
5255
5351
  };
5352
+ /**
5353
+ * The tree rows' lynx states (signalxjs/lynx#1256). The web's press wash is
5354
+ * `[data-pressed]:not([data-disabled]):not([data-selected])`; lynx has no
5355
+ * absence classes, so the wash is the plain `pressed` state and a selected
5356
+ * row restores its accent fill below. The web's descendant rule that swaps
5357
+ * the check box's pair on a selected row (`[data-selected] &` on the box)
5358
+ * becomes the ROW's own custom properties: they inherit to the box inside
5359
+ * it. The focus ring is inset (rows sit flush) and drawn as a box-shadow,
5360
+ * which follows the row's radius where lynx's `outline` does not.
5361
+ */
5362
+ const lynxTreeRowStates = {
5363
+ pressed: { background: 'var(--color-base-300)' },
5364
+ selected: {
5365
+ '--tree-check': 'var(--tree-on-accent)',
5366
+ '--tree-on-check': 'var(--tree-accent)',
5367
+ },
5368
+ ...lynxFocus({ inset: true }),
5369
+ };
5370
+ /** A held selected row keeps its accent fill (the web's `:not([data-selected])`). */
5371
+ const lynxTreeRowSelectors = {
5372
+ '&[data-selected][data-pressed]': { background: 'var(--tree-accent)' },
5373
+ };
5256
5374
  /**
5257
5375
  * `tickGlyphFallback` for the node check box, whose tick is its `::after`:
5258
5376
  * the geometry drops and the same pseudo carries the glyph, in the medium's
@@ -5402,12 +5520,37 @@ export const treeView = {
5402
5520
  xl: { root: { base: { '--tree-text': 'var(--text-lg)' } } },
5403
5521
  },
5404
5522
  },
5405
- // The indent, restated physically: logical spellings resolve on iOS but
5406
- // not on Android (measured, signalxjs/lynx#1084), so the emitter refuses
5407
- // them. Physical is the lynx target's norm — no RTL flow there.
5523
+ // lynx (signalxjs/lynx#1256, zero#478). No hover on a touch platform, no
5524
+ // `:not()`, no pseudo-elements, no descendant selectors: each web rule
5525
+ // those carry is restated on the part the runtime stamps. The skin's
5526
+ // `:dir(rtl)` mirror and the `@media` stops have no lynx form (no RTL
5527
+ // flow, no conditions) and stay dropped.
5408
5528
  targets: {
5409
5529
  lynx: {
5410
5530
  parts: {
5531
+ item: { states: lynxTreeRowStates, selectors: lynxTreeRowSelectors },
5532
+ 'branch-trigger': { states: lynxTreeRowStates, selectors: lynxTreeRowSelectors },
5533
+ // `inline-block` is not a lynx display value. The glyph is a
5534
+ // `<text>` part: a 1em box centred on it, so the quarter turn
5535
+ // pivots on the chevron instead of swinging it sideways.
5536
+ 'branch-indicator': { base: { display: 'flex', width: '1em', textAlign: 'center' } },
5537
+ // The mark is a `<text>` child lynx-zero renders (`✓` checked,
5538
+ // `−` indeterminate) in place of the web's clip-path `::after`
5539
+ // tick — clip-path and pseudo-elements are both dropped on
5540
+ // lynx. The glyph is the forced-colors fallback's shape
5541
+ // (`nodeTickFallback`), inked with the box's `-on` pair and
5542
+ // sized by inheritance from the row.
5543
+ 'node-checkbox': {
5544
+ base: {
5545
+ display: 'flex',
5546
+ alignItems: 'center',
5547
+ justifyContent: 'center',
5548
+ color: 'var(--tree-on-check)',
5549
+ },
5550
+ },
5551
+ // The indent, restated physically: logical spellings resolve
5552
+ // on iOS but not on Android (measured, signalxjs/lynx#1084),
5553
+ // so the emitter refuses them.
5411
5554
  'branch-content': { base: { paddingLeft: 'var(--space-lg)' } },
5412
5555
  },
5413
5556
  },
@@ -5508,6 +5651,10 @@ export const input = {
5508
5651
  selectors: {
5509
5652
  // Same muting, same reason as `number-input/input` (#264).
5510
5653
  '&::placeholder': { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' },
5654
+ // zero draws its own ClearTrigger and clears on Escape, and Firefox
5655
+ // draws no native clear: hide the engine's cancel button everywhere (#446).
5656
+ '&::-webkit-search-cancel-button': { appearance: 'none', display: 'none' },
5657
+ '&::-webkit-search-decoration': { appearance: 'none' },
5511
5658
  },
5512
5659
  },
5513
5660
  // daisy's `label` inside `.input`: a quiet prefix/suffix at one edge,
@@ -6024,7 +6171,13 @@ export const alert = {
6024
6171
  },
6025
6172
  },
6026
6173
  // Pinned top-right, its glyph on the padding edge. `inherit`
6027
- // is spelled as the root's own ink.
6174
+ // is spelled as the root's own ink. A square chip, the
6175
+ // glyph centred in it: the web's `line-height: 1` + 2xs
6176
+ // padding make an almost-square box, but lynx does not carry
6177
+ // the line-height into the glyph's `<text>`, so the padded
6178
+ // box was a narrow × in a tall line — a 14 × 20pt pill whose
6179
+ // press wash and focus ring drew as vertical ovals
6180
+ // (signalxjs/lynx#1253). The box is sized outright instead.
6028
6181
  close: {
6029
6182
  base: {
6030
6183
  position: 'absolute',
@@ -6032,6 +6185,13 @@ export const alert = {
6032
6185
  right: 'calc(var(--alert-pad-x) - var(--space-2xs))',
6033
6186
  color: 'var(--color-base-content)',
6034
6187
  fontSize: 'var(--text-md)',
6188
+ display: 'flex',
6189
+ alignItems: 'center',
6190
+ justifyContent: 'center',
6191
+ boxSizing: 'border-box',
6192
+ padding: '0',
6193
+ width: 'calc(var(--text-md) + var(--space-2xs) * 2)',
6194
+ height: 'calc(var(--text-md) + var(--space-2xs) * 2)',
6035
6195
  },
6036
6196
  states: lynxFocus({ gap: 'var(--alert-tint)' }),
6037
6197
  },
@@ -6448,7 +6608,11 @@ export const divider = {
6448
6608
  // no fill, its cross size following the label, a row when
6449
6609
  // horizontal. Both are lynx-zero rendering details, not
6450
6610
  // modifiers an author sets, so they are styled from raw lynx css
6451
- // (the toggle-group join ends' reasoning).
6611
+ // (the toggle-group join ends' reasoning). A segment centres
6612
+ // itself on the label (signalxjs/lynx#1272): it carries the
6613
+ // root's `align-self: stretch`, and a stretched item with a
6614
+ // definite thickness lands at cross-start on lynx — the top of
6615
+ // the label row, not the middle daisy's rules sit on.
6452
6616
  css: `
6453
6617
  .zx-divider__root.zx-m-labelled {
6454
6618
  --divider-ink: transparent;
@@ -6462,6 +6626,7 @@ export const divider = {
6462
6626
  }
6463
6627
  .zx-divider__root.zx-m-segment {
6464
6628
  flex: 1 1 0;
6629
+ align-self: center;
6465
6630
  }
6466
6631
  .zx-divider__root.zx-m-segment.zx-o-horizontal {
6467
6632
  min-width: 0;
@@ -6670,6 +6835,15 @@ export const kbd = {
6670
6835
  ...lynxSides('border{}Width', 'var(--border)', ['Top', 'Right', 'Left']),
6671
6836
  borderBottomWidth: 'calc(var(--border) * 2)',
6672
6837
  ...lynxSides('border{}Color', 'var(--color-base-300)'),
6838
+ // The web's monospace cap comes from the browser's
6839
+ // UA style for `<kbd>`, not the recipe; lynx has no
6840
+ // UA sheet, so the face is spelled here
6841
+ // (signalxjs/lynx#1254). Concrete names, not a
6842
+ // token: iOS Lynx walks the list with
6843
+ // `UIFont fontWithName:` (Menlo ships on iOS, the
6844
+ // CSS generics do not resolve there), and
6845
+ // `monospace` serves the lynx web target.
6846
+ fontFamily: 'Menlo, monospace',
6673
6847
  },
6674
6848
  },
6675
6849
  },
@@ -7382,14 +7556,43 @@ export const chat = {
7382
7556
  xl: { bubble: { base: { fontSize: 'var(--text-lg)', padding: 'var(--space-md) var(--space-xl)' } } },
7383
7557
  },
7384
7558
  },
7385
- // The row's vertical padding, restated physically: logical spellings
7386
- // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
7387
- // the emitter refuses them.
7559
+ // Lynx (signalxjs/lynx#1276):
7560
+ // - the row's vertical padding, restated physically: logical spellings
7561
+ // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
7562
+ // the emitter refuses them;
7563
+ // - lynx has no grid, so the row is a flex column (header, bubble,
7564
+ // footer), gathered to the placement's side. The avatar leaves the
7565
+ // flow: lynx-zero pins it to the row's bottom corner and reserves its
7566
+ // measured width beside the column;
7567
+ // - the `[data-placement] > &` selectors have no class form on the
7568
+ // parts (placement is the root's), so the bubble's square tail corner
7569
+ // is raw lynx css, a descendant of the placed root — the shape the
7570
+ // theme restatements are proven to match. Physical corners: lynx has
7571
+ // no RTL flow.
7388
7572
  targets: {
7389
7573
  lynx: {
7390
7574
  parts: {
7391
- root: { base: { paddingTop: 'var(--space-2xs)', paddingBottom: 'var(--space-2xs)' } },
7575
+ root: {
7576
+ base: {
7577
+ display: 'flex',
7578
+ flexDirection: 'column',
7579
+ alignItems: 'flex-start',
7580
+ paddingTop: 'var(--space-2xs)',
7581
+ paddingBottom: 'var(--space-2xs)',
7582
+ },
7583
+ selectors: {
7584
+ '&[data-placement="end"]': { alignItems: 'flex-end' },
7585
+ },
7586
+ },
7392
7587
  },
7588
+ css: `
7589
+ .zx-chat__root.zx-p-start .zx-chat__bubble {
7590
+ border-bottom-left-radius: 0;
7591
+ }
7592
+ .zx-chat__root.zx-p-end .zx-chat__bubble {
7593
+ border-bottom-right-radius: 0;
7594
+ }
7595
+ `,
7393
7596
  },
7394
7597
  },
7395
7598
  };
@@ -7481,6 +7684,59 @@ export const chatLog = {
7481
7684
  xl: { content: { base: { rowGap: 'var(--space-md)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 10)', fontSize: 'var(--text-sm)' } } },
7482
7685
  },
7483
7686
  },
7687
+ // Lynx (signalxjs/lynx#1276). lynx-zero's root is the frame and a native
7688
+ // scroll-view inside Content does the scrolling, so:
7689
+ // - the frame's padding moves onto the content (physical — logical
7690
+ // spellings do not resolve on Android, signalxjs/lynx#1084), so rows
7691
+ // scroll to the frame's edge instead of clipping at an inset;
7692
+ // - the content's grid is a flex column (lynx has no grid); a short
7693
+ // transcript sits at the top — `margin-block-start: auto` has nothing
7694
+ // to push against inside a scroll-view;
7695
+ // - the jump trigger floats in lynx-zero's absolute dock at the frame's
7696
+ // foot, so it drops `position: sticky` and the negative margin, and
7697
+ // restates its inline padding physically;
7698
+ // - `:hover` is dropped on touch: the held trigger shows the hover's
7699
+ // 90% darkening (restated per colour — a colour function over
7700
+ // `--chat-log-fill` cannot bake), with the existing 1px sink;
7701
+ // - the focus rings as `lynxFocus` (lynx#1163), the frame's drawn inside
7702
+ // like the web's negative offset.
7703
+ targets: {
7704
+ lynx: {
7705
+ parts: {
7706
+ root: {
7707
+ base: { padding: '0', overflow: 'hidden' },
7708
+ states: { 'focus-visible': lynxFocusRing('var(--color-base-content)', { inset: true }) },
7709
+ },
7710
+ content: {
7711
+ base: {
7712
+ display: 'flex',
7713
+ flexDirection: 'column',
7714
+ paddingTop: 'var(--space-sm)',
7715
+ paddingBottom: 'var(--space-sm)',
7716
+ paddingLeft: 'var(--space-md)',
7717
+ paddingRight: 'var(--space-md)',
7718
+ },
7719
+ },
7720
+ 'jump-trigger': {
7721
+ base: {
7722
+ position: 'relative',
7723
+ flexDirection: 'row',
7724
+ paddingLeft: 'var(--space-md)',
7725
+ paddingRight: 'var(--space-md)',
7726
+ },
7727
+ states: {
7728
+ pressed: { background: 'color-mix(in oklab, var(--color-primary) 90%, black)' },
7729
+ ...lynxFocus(),
7730
+ },
7731
+ },
7732
+ },
7733
+ variants: {
7734
+ color: Object.fromEntries(ROLES.map((c) => [c, { 'jump-trigger': { states: {
7735
+ pressed: { background: `color-mix(in oklab, var(--color-${c}) 90%, black)` },
7736
+ } } }])),
7737
+ },
7738
+ },
7739
+ },
7484
7740
  };
7485
7741
  /** daisy radial-progress: --value became --progress-percent; same ring. */
7486
7742
  export const radialProgress = {
@@ -7729,6 +7985,41 @@ export const navbar = {
7729
7985
  xl: { root: { base: { minBlockSize: '6rem', fontSize: 'var(--text-lg)' } } },
7730
7986
  },
7731
7987
  },
7988
+ // Lynx (signalxjs/lynx#1257): `min-block-size` is not a lynx property,
7989
+ // so the bar's height ramp is restated as `min-height`; and a lynx flex
7990
+ // container is only a row when it says so, so the bar and its three
7991
+ // sections spell `flex-direction: row`.
7992
+ //
7993
+ // No section squeezes a word (signalxjs/lynx#1274, zero#499). On the web
7994
+ // `flex: 1 1 0%` is floored by the flex item's automatic minimum (its
7995
+ // min-content width), so "Acme" never breaks. Lynx's flex layout has no
7996
+ // automatic minimum, and its `min-width` takes no intrinsic keyword, so
7997
+ // a zero basis shrank the start below its text and the `<text>` wrapped
7998
+ // per character ("Acm / e"). On lynx the ends start from their content
7999
+ // (`flex-basis: auto`), never shrink, and share the slack equally; the
8000
+ // centre takes daisy's own `flex-shrink: 0`. A bar too narrow for its
8001
+ // content overflows whole, as daisy's does — the btn rule (lynx#1165).
8002
+ // The cost: with a centre and unequal ends, the centre sits off-centre
8003
+ // by half the ends' difference (the web's is exact until a section
8004
+ // reaches its min-content).
8005
+ targets: {
8006
+ lynx: {
8007
+ parts: {
8008
+ root: { base: { flexDirection: 'row', minHeight: '4rem' } },
8009
+ start: { base: { flexDirection: 'row', flexBasis: 'auto', flexShrink: '0' } },
8010
+ center: { base: { flexDirection: 'row', flexShrink: '0' } },
8011
+ end: { base: { flexDirection: 'row', flexBasis: 'auto', flexShrink: '0' } },
8012
+ },
8013
+ variants: {
8014
+ size: {
8015
+ xs: { root: { base: { minHeight: '2.5rem' } } },
8016
+ sm: { root: { base: { minHeight: '3rem' } } },
8017
+ lg: { root: { base: { minHeight: '5rem' } } },
8018
+ xl: { root: { base: { minHeight: '6rem' } } },
8019
+ },
8020
+ },
8021
+ },
8022
+ },
7732
8023
  };
7733
8024
  /**
7734
8025
  * NavList (zero#132) — daisy's `menu` idiom for a sidebar: rounded rows, a
@@ -7787,6 +8078,14 @@ export const navList = {
7787
8078
  alignItems: 'center',
7788
8079
  gap: 'var(--space-sm)',
7789
8080
  textDecoration: 'none',
8081
+ // Without an href the link is a <button> (#451): drop its chrome.
8082
+ appearance: 'none',
8083
+ background: 'none',
8084
+ border: '0',
8085
+ font: 'inherit',
8086
+ textAlign: 'start',
8087
+ inlineSize: '100%',
8088
+ cursor: 'pointer',
7790
8089
  color: 'var(--nav-ink)',
7791
8090
  padding: 'var(--space-sm) var(--space-lg)',
7792
8091
  borderRadius: 'var(--radius-field)',
@@ -7834,6 +8133,27 @@ export const navList = {
7834
8133
  xl: { root: { base: { fontSize: 'var(--text-lg)' } }, link: { base: { padding: 'var(--space-lg) var(--space-xl)' } } },
7835
8134
  },
7836
8135
  },
8136
+ // The web's block and inline boxes, restated for lynx (signalxjs/lynx#1259).
8137
+ // No `:hover` on a touch platform, and the link's anatomy declares no
8138
+ // `pressed` flag: a held link has lynx-zero's main-thread press scale.
8139
+ targets: {
8140
+ lynx: {
8141
+ parts: {
8142
+ // `display: block` has no lynx projection: a column, so the
8143
+ // link spans the list as a block `<li>` makes it.
8144
+ item: { base: { display: 'flex', flexDirection: 'column' } },
8145
+ // The icon / label / meta row, explicit (lynx does not
8146
+ // default the direction the way a browser does).
8147
+ link: { base: { flexDirection: 'row' } },
8148
+ // `inline-size` restated physically; the glyph centred in
8149
+ // its column on both axes.
8150
+ icon: { base: { width: '1.25em', flexDirection: 'row', alignItems: 'center' } },
8151
+ // The count's push to the far edge, restated physically —
8152
+ // `margin-inline-start` does not resolve on Android (#1084).
8153
+ meta: { base: { marginLeft: 'auto', flexDirection: 'row' } },
8154
+ },
8155
+ },
8156
+ },
7837
8157
  };
7838
8158
  /**
7839
8159
  * Breadcrumbs — daisy's breadcrumbs: a compact text-sm trail, links
@@ -7942,6 +8262,37 @@ export const breadcrumbs = {
7942
8262
  xl: { root: { base: { fontSize: 'var(--text-lg)' } } },
7943
8263
  },
7944
8264
  },
8265
+ // Lynx (signalxjs/lynx#1257):
8266
+ // - The list, the items and the ellipsis are explicit rows (a lynx flex
8267
+ // container is only a row when it says so).
8268
+ // - The trigger's `hover` ink is dropped on a touch platform; the
8269
+ // `pressed` flag carries it instead. Its resting ink moves onto a
8270
+ // lynx-only token so the base rule stays a plain var() read: the
8271
+ // emitter bakes a `color-mix` declaration per theme under the host
8272
+ // class, and that baked rule would outrank the pressed one.
8273
+ // - The focus ring is the lynx box-shadow ring (outline has no offset
8274
+ // and ignores the radius there).
8275
+ // - The link's `hover` stays dropped: the link anatomy declares no
8276
+ // pressed flag, so there is nothing on lynx to carry it.
8277
+ // - The separator is a real part on both targets (lynx-zero renders it
8278
+ // as a text glyph), so daisy's `::before` chevron needs no lynx form.
8279
+ targets: {
8280
+ lynx: {
8281
+ tokens: { '--bc-trigger-ink': 'color-mix(in oklch, var(--color-base-content) 75%, transparent)' },
8282
+ parts: {
8283
+ list: { base: { flexDirection: 'row' } },
8284
+ item: { base: { flexDirection: 'row' } },
8285
+ ellipsis: { base: { flexDirection: 'row' } },
8286
+ 'ellipsis-trigger': {
8287
+ base: { color: 'var(--bc-trigger-ink)' },
8288
+ states: {
8289
+ pressed: { color: 'var(--color-base-content)' },
8290
+ 'focus-visible': lynxFocusRing('var(--color-primary)'),
8291
+ },
8292
+ },
8293
+ },
8294
+ },
8295
+ },
7945
8296
  };
7946
8297
  /**
7947
8298
  * The four pagination triggers — prev/next and, with `withEdges`, the
@@ -7978,6 +8329,17 @@ const pageTrigger = {
7978
8329
  [`&${rtl}`]: { scale: '-1 1' },
7979
8330
  },
7980
8331
  };
8332
+ /** A pagination cell's box on lynx — see the recipe's lynx section. */
8333
+ const pageCellLynx = {
8334
+ minWidth: 'var(--pg-size)',
8335
+ height: 'var(--pg-size)',
8336
+ flexShrink: '0',
8337
+ };
8338
+ /** The four triggers on lynx: the cell box, the held wash + sink, the ring. */
8339
+ const pageTriggerLynx = {
8340
+ base: pageCellLynx,
8341
+ states: { pressed: { background: 'var(--color-base-300)' }, ...lynxFocus() },
8342
+ };
7981
8343
  /**
7982
8344
  * Pagination — daisy's join-of-buttons made of standalone btn cells: the
7983
8345
  * quiet base-200 fill, base-300 hover, the current page inverted into the
@@ -8074,13 +8436,35 @@ export const pagination = {
8074
8436
  xl: { root: { base: { '--pg-size': 'calc(var(--size-field) * 14)', '--pg-font': 'var(--text-lg)' } } },
8075
8437
  },
8076
8438
  },
8077
- // The cell's inline padding, restated physically: logical spellings
8078
- // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
8079
- // the emitter refuses them.
8439
+ // Lynx (signalxjs/lynx#1258):
8440
+ // - the cells' size, restated physically: `min-inline-size` and
8441
+ // `block-size` do not resolve on lynx (lynx#1145), so every cell
8442
+ // collapsed to its glyph. Read through `--pg-size`, so the compile
8443
+ // inlines the size steps per part, as it does the web's;
8444
+ // - the item's inline padding, physical: logical spellings resolve on
8445
+ // iOS but not on Android (measured, lynx#1084);
8446
+ // - the press: the web answers a pointer with `:hover`, which a touch
8447
+ // platform drops, so the held cell paints daisy's hover wash with the
8448
+ // 1px sink (`lynxBtnPressed`'s pair) — the current page keeps its
8449
+ // accent fill and only sinks;
8450
+ // - the focus ring as `lynxFocus`: lynx's outline has no offset and
8451
+ // ignores the radius (lynx#1163);
8452
+ // - no row shrink: a cell never squeezes below its size in a narrow row.
8453
+ // The glyphs' rtl flip has no replacement: lynx has no RTL flow.
8080
8454
  targets: {
8081
8455
  lynx: {
8082
8456
  parts: {
8083
- item: { base: { paddingLeft: 'var(--space-2xs)', paddingRight: 'var(--space-2xs)' } },
8457
+ root: { base: { flexDirection: 'row' } },
8458
+ item: {
8459
+ base: { ...pageCellLynx, paddingLeft: 'var(--space-2xs)', paddingRight: 'var(--space-2xs)' },
8460
+ states: lynxFocus(),
8461
+ selectors: { '&[data-pressed][data-state="inactive"]': { background: 'var(--color-base-300)' } },
8462
+ },
8463
+ ellipsis: { base: pageCellLynx },
8464
+ 'first-trigger': pageTriggerLynx,
8465
+ 'prev-trigger': pageTriggerLynx,
8466
+ 'next-trigger': pageTriggerLynx,
8467
+ 'last-trigger': pageTriggerLynx,
8084
8468
  },
8085
8469
  },
8086
8470
  },
@@ -8097,6 +8481,11 @@ export const pagination = {
8097
8481
  */
8098
8482
  const stepInk = (accent, role) => `color-mix(in oklab, ${accent} `
8099
8483
  + `${Math.min(ROLE_INK_KEEP[role] ?? 55, 70)}%, var(--color-base-content))`;
8484
+ /**
8485
+ * The complete disc's fill: 20% of the accent over base-100, in OKLAB (see
8486
+ * the indicator's `complete` state for why not OKLCH).
8487
+ */
8488
+ const stepTint = (accent) => `color-mix(in oklab, ${accent} 20%, var(--color-base-100))`;
8100
8489
  /** A Steps root holding wizard parts (#296) — the panel or the triggers. */
8101
8490
  const STEPS_WIZARD = '&:has(> [data-scope="steps"]:is([data-part="content"], [data-part="prev-trigger"], [data-part="next-trigger"]))';
8102
8491
  /** Steps' Back/Next (#296): the neutral daisy btn, with its 1px sink. */
@@ -8212,7 +8601,7 @@ export const steps = {
8212
8601
  // (warning's yellow came out pink) — which the browser parity
8213
8602
  // check caught once per-step colour put all eight roles on
8214
8603
  // the disc (#112).
8215
- complete: { background: 'color-mix(in oklab, var(--steps-accent) 20%, var(--color-base-100))', color: 'var(--steps-complete-ink)' },
8604
+ complete: { background: stepTint('var(--steps-accent)'), color: 'var(--steps-complete-ink)' },
8216
8605
  inactive: { background: 'var(--color-base-200)', color: 'var(--color-base-content)' },
8217
8606
  invalid: { background: 'var(--color-error)', color: 'var(--color-error-content)' },
8218
8607
  },
@@ -8300,32 +8689,82 @@ export const steps = {
8300
8689
  xl: { root: { base: { '--steps-ind': 'calc(var(--size-selector) * 9)', '--steps-font': 'var(--text-md)' } } },
8301
8690
  },
8302
8691
  },
8303
- // The bridge's anchors and the vertical item's tail padding, restated
8304
- // physically: logical spellings resolve on iOS but not on Android
8305
- // (measured, signalxjs/lynx#1084), so the emitter refuses them. Physical
8306
- // is the lynx target's norm — no RTL flow there to mirror with.
8692
+ // Lynx. Physical spellings throughout: logical insets, margins and
8693
+ // paddings resolve on iOS but not on Android (measured,
8694
+ // signalxjs/lynx#1084), and `inline-size` / `block-size` do not resolve
8695
+ // at all (lynx#1145) — without the restatement the discs and lines did
8696
+ // not paint. Also (signalxjs/lynx#1258):
8697
+ // - the complete disc's tint and ink read `--steps-accent` through a
8698
+ // colour function, which cannot bake; the base pair is restated over
8699
+ // primary, and each colour already restates the ink on its item, so
8700
+ // the tint joins it there;
8701
+ // - the bridge runs from this disc's edge to the next disc's edge —
8702
+ // `50% + ind/2` for `100% - ind` — instead of centre to centre under
8703
+ // the disc, so it needs no z-index across sibling items; the item
8704
+ // stops clipping (a lynx view's `overflow` defaults to hidden) so the
8705
+ // bridge can reach past its box;
8706
+ // - the `:has()` wizard wrap becomes the `wizard` stamp lynx-zero puts on
8707
+ // the root while a panel or trigger is mounted (raw css below);
8708
+ // - the focus ring as `lynxFocus` (lynx#1163), and Back/Next's press as
8709
+ // daisy's hover wash + sink (`lynxBtnPressed`), `:hover` being dropped.
8307
8710
  targets: {
8308
8711
  lynx: {
8712
+ tokens: {
8713
+ '--steps-complete-ink': stepInk('var(--color-primary)', 'primary'),
8714
+ '--steps-complete-tint': stepTint('var(--color-primary)'),
8715
+ },
8309
8716
  parts: {
8717
+ root: { base: { flexDirection: 'row', overflow: 'visible' } },
8310
8718
  item: {
8719
+ base: { overflow: 'visible' },
8720
+ states: lynxFocus(),
8311
8721
  selectors: {
8312
8722
  '&[data-orientation="vertical"]': { paddingBottom: 'var(--space-lg)' },
8313
8723
  },
8314
8724
  },
8725
+ indicator: {
8726
+ base: { width: 'var(--steps-ind)', height: 'var(--steps-ind)' },
8727
+ states: { complete: { background: 'var(--steps-complete-tint)' } },
8728
+ },
8315
8729
  separator: {
8316
8730
  selectors: {
8317
8731
  '&[data-orientation="horizontal"]': {
8318
- top: 'calc(var(--space-xs) + var(--steps-ind) / 2)',
8319
- left: '50%',
8732
+ top: 'calc(var(--space-xs) + (var(--steps-ind) - var(--border)) / 2)',
8733
+ left: 'calc(50% + var(--steps-ind) / 2)',
8734
+ width: 'calc(100% - var(--steps-ind))',
8735
+ height: 'var(--border)',
8320
8736
  },
8321
8737
  '&[data-orientation="vertical"]': {
8322
8738
  left: 'calc(var(--space-xs) + (var(--steps-ind) - var(--border)) / 2)',
8323
8739
  top: 'calc(var(--space-xs) + var(--steps-ind))',
8324
8740
  bottom: 'calc(var(--space-xs) * -1)',
8741
+ width: 'var(--border)',
8325
8742
  },
8326
8743
  },
8327
8744
  },
8745
+ 'prev-trigger': {
8746
+ base: { ...lynxBtnPad(4), ...lynxBtnFit },
8747
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
8748
+ },
8749
+ 'next-trigger': {
8750
+ base: { ...lynxBtnPad(4), ...lynxBtnFit, marginLeft: 'auto' },
8751
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
8752
+ },
8753
+ },
8754
+ variants: {
8755
+ color: Object.fromEntries(ROLES.map((c) => [c, { item: { base: {
8756
+ '--steps-complete-tint': stepTint(`var(--color-${c})`),
8757
+ } } }])),
8328
8758
  },
8759
+ // lynx-zero stamps `wizard` on the root while a Content,
8760
+ // PrevTrigger or NextTrigger is mounted — the web's `:has()`.
8761
+ // A lynx-zero rendering detail, not a modifier an author sets.
8762
+ css: `
8763
+ .zx-steps__root.zx-m-wizard {
8764
+ flex-wrap: wrap;
8765
+ row-gap: var(--space-md);
8766
+ }
8767
+ `,
8329
8768
  },
8330
8769
  },
8331
8770
  };
@@ -8502,11 +8941,68 @@ export const drawer = {
8502
8941
  },
8503
8942
  },
8504
8943
  },
8505
- // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
8944
+ // The btn paddings, restated physically — see `lynxBtnPad` (#1084) —
8945
+ // and the btns' press rendering (`lynxBtnPressed`), as dialog wears
8946
+ // them. The edge sheet (signalxjs/lynx#1277): lynx-zero lays the
8947
+ // panel on its edge with flexbox inside the backdrop, so the sheet
8948
+ // stays in flow (`position: relative`, never `fixed`) and sizes to
8949
+ // it — no viewport units, which lynx does not resolve against the
8950
+ // overlay outlet. The skin keeps the cap, the corners and the slide.
8506
8951
  lynx: {
8952
+ keyframes: {
8953
+ // `translate` is Android-asymmetric (#1084): the travel is a
8954
+ // transform function, one per edge.
8955
+ 'zero-daisy-drawer-in-start': 'from { transform: translateX(-100%); } to { transform: translateX(0); }',
8956
+ 'zero-daisy-drawer-in-end': 'from { transform: translateX(100%); } to { transform: translateX(0); }',
8957
+ 'zero-daisy-drawer-in-top': 'from { transform: translateY(-100%); } to { transform: translateY(0); }',
8958
+ 'zero-daisy-drawer-in-bottom': 'from { transform: translateY(100%); } to { transform: translateY(0); }',
8959
+ },
8507
8960
  parts: {
8508
- trigger: { base: lynxBtnPad(4) },
8509
- close: { base: lynxBtnPad(4) },
8961
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
8962
+ close: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
8963
+ panel: {
8964
+ // The cap alone: `min(20rem, 85vw)` has no lynx spelling
8965
+ // (no min()); lynx-zero states the 85% share inline and
8966
+ // this caps it — together, the web's width.
8967
+ base: { '--l-measure': '20rem' },
8968
+ selectors: {
8969
+ '&[data-l-dock="sheet"]': {
8970
+ position: 'relative',
8971
+ blockSize: 'auto',
8972
+ maxBlockSize: 'none',
8973
+ },
8974
+ // The block edges run full width (the web's `:where()`
8975
+ // pair, one selector each — no `:where()` on lynx),
8976
+ // content-sized up to 85% of the outlet.
8977
+ '&[data-placement="top"]': { '--l-measure': 'none' },
8978
+ '&[data-placement="bottom"]': { '--l-measure': 'none' },
8979
+ '&[data-placement="top"][data-l-dock="sheet"]': {
8980
+ maxBlockSize: '85%',
8981
+ borderBottomLeftRadius: 'var(--radius-box)',
8982
+ borderBottomRightRadius: 'var(--radius-box)',
8983
+ },
8984
+ '&[data-placement="bottom"][data-l-dock="sheet"]': {
8985
+ maxBlockSize: '85%',
8986
+ borderTopLeftRadius: 'var(--radius-box)',
8987
+ borderTopRightRadius: 'var(--radius-box)',
8988
+ },
8989
+ // The slide in from the edge (the web's `translate`
8990
+ // over `@starting-style`, both dropped here). The
8991
+ // exit is an unmount on lynx, so only the entry runs.
8992
+ '&[data-placement="start"][data-l-dock="sheet"][data-state="open"]': {
8993
+ animation: 'zero-daisy-drawer-in-start var(--duration-slow) var(--ease-standard)',
8994
+ },
8995
+ '&[data-placement="end"][data-l-dock="sheet"][data-state="open"]': {
8996
+ animation: 'zero-daisy-drawer-in-end var(--duration-slow) var(--ease-standard)',
8997
+ },
8998
+ '&[data-placement="top"][data-l-dock="sheet"][data-state="open"]': {
8999
+ animation: 'zero-daisy-drawer-in-top var(--duration-slow) var(--ease-standard)',
9000
+ },
9001
+ '&[data-placement="bottom"][data-l-dock="sheet"][data-state="open"]': {
9002
+ animation: 'zero-daisy-drawer-in-bottom var(--duration-slow) var(--ease-standard)',
9003
+ },
9004
+ },
9005
+ },
8510
9006
  },
8511
9007
  variants: { size: lynxBtnSizes('trigger') },
8512
9008
  },
@@ -8705,6 +9201,89 @@ export const table = {
8705
9201
  },
8706
9202
  },
8707
9203
  },
9204
+ // signalxjs/lynx#1279. Lynx has no table layout: lynx-zero lays the
9205
+ // parts out as flex rows itself. The `hover` modifier has no lynx
9206
+ // answer (a touch screen has no hover), nor has the viewport-width
9207
+ // `@media` of the stacked mode — lynx-zero resolves `stack` in JS and
9208
+ // stamps the parts instead (see `css` below).
9209
+ targets: {
9210
+ lynx: {
9211
+ parts: {
9212
+ // The web's `overflow-x: auto` box: a lynx view clips its
9213
+ // rounded frame; a table of fixed columns scrolls in a
9214
+ // scroll-view lynx-zero renders.
9215
+ root: {
9216
+ base: { overflow: 'hidden' },
9217
+ // daisy's ring drawn inside the box, as on the web
9218
+ // (outline-offset: -2px) — spelled as an inset shadow.
9219
+ states: { 'focus-visible': lynxFocusRing('var(--color-base-content)', { inset: true }) },
9220
+ },
9221
+ // The muted header ink: the colour function over
9222
+ // `--table-accent` cannot bake (a recipe-local property),
9223
+ // so the default accent's mix is restated here and each
9224
+ // colour's below.
9225
+ 'header-cell': { base: { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' } },
9226
+ 'cell-label': { base: { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' } },
9227
+ // The web's hover ink, while held; the lynx ring.
9228
+ 'sort-trigger': {
9229
+ base: { flexDirection: 'row' },
9230
+ states: { pressed: { color: 'var(--table-accent)' }, ...lynxFocus() },
9231
+ },
9232
+ },
9233
+ variants: {
9234
+ color: Object.fromEntries(ROLES.map((c) => {
9235
+ const muted = `color-mix(in oklab, ${roleInk(c)} 60%, transparent)`;
9236
+ return [c, {
9237
+ 'header-cell': { base: { color: muted } },
9238
+ 'cell-label': { base: { color: muted } },
9239
+ }];
9240
+ })),
9241
+ },
9242
+ // lynx-zero rendering details, not modifiers an author sets:
9243
+ // - `stripe`: lynx has no `:nth-child`, so the body stamps its
9244
+ // even, unselected rows (mount order) under `zebra`.
9245
+ // - `held`: the unsorted mark the web reveals under a hovered or
9246
+ // keyboard-focused trigger, shown while the trigger is held.
9247
+ // - `stacked`: the stacked mode's card chrome (the web's
9248
+ // below-breakpoint rules). Zero widths and paddings keep a
9249
+ // var() so they beat the var-bearing base values (lynx expands
9250
+ // var() declarations after the cascade, signalxjs/lynx#1161);
9251
+ // the root keeps its base-100 fill (a var-free transparent
9252
+ // would lose the same way).
9253
+ css: `
9254
+ .zx-table__root.zx-m-stacked {
9255
+ border-width: calc(var(--border) * 0);
9256
+ border-radius: calc(var(--radius-box) * 0);
9257
+ }
9258
+ .zx-table__caption.zx-m-stacked {
9259
+ padding-left: calc(var(--table-pad-inline) * 0);
9260
+ padding-right: calc(var(--table-pad-inline) * 0);
9261
+ }
9262
+ .zx-table__row.zx-m-stacked {
9263
+ border: var(--border) solid var(--color-base-200);
9264
+ border-radius: var(--radius-box);
9265
+ background: var(--color-base-100);
9266
+ padding: var(--space-sm) var(--table-pad-inline);
9267
+ margin-bottom: var(--space-md);
9268
+ }
9269
+ .zx-table__row.zx-m-stacked.zx-f-selected {
9270
+ background: var(--color-base-300);
9271
+ }
9272
+ .zx-table__cell.zx-m-stacked {
9273
+ padding-left: calc(var(--table-pad-inline) * 0);
9274
+ padding-right: calc(var(--table-pad-inline) * 0);
9275
+ padding-top: var(--space-xs);
9276
+ padding-bottom: var(--space-xs);
9277
+ }
9278
+ .zx-table__row.zx-m-stripe {
9279
+ background: var(--color-base-200);
9280
+ }
9281
+ .zx-table__sort-indicator.zx-m-held.zx-s-none {
9282
+ opacity: 0.6;
9283
+ }
9284
+ `,
9285
+ },
9286
+ },
8708
9287
  };
8709
9288
  /**
8710
9289
  * FileUpload — daisy's file-input family reshaped for the composed anatomy:
@@ -8870,6 +9449,63 @@ export const fileUpload = {
8870
9449
  xl: { root: { base: { '--fu-pad': 'calc(var(--space-xl) * 1.25)', '--fu-font': 'var(--text-md)' } } },
8871
9450
  },
8872
9451
  },
9452
+ // Lynx (signalxjs/lynx#1276):
9453
+ // - lynx has no grid: the root and the item list are flex columns, and
9454
+ // the stretched parts (dropzone, item list) say so with `align-self`;
9455
+ // the rows (trigger, item) state `flex-direction: row`;
9456
+ // - the dropzone centres its words in a column (lynx text alignment is
9457
+ // the text's own), and its highlighted wash — a colour function over
9458
+ // `--fu-accent`, which cannot bake — is restated per colour over the
9459
+ // role. lynx never drives `highlighted` (no drag source); it shows
9460
+ // through `ForceStates` in the gallery;
9461
+ // - `:hover` is dropped on touch: the held trigger shows daisy's
9462
+ // base-300 wash + 1px sink (`lynxBtnPressed`, in place of the
9463
+ // standalone `translate`, which Android does not resolve), and the
9464
+ // held × and clear show the ghost's base-200 wash;
9465
+ // - the focus rings as `lynxFocus` (lynx#1163).
9466
+ targets: {
9467
+ lynx: {
9468
+ parts: {
9469
+ root: { base: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start' } },
9470
+ trigger: {
9471
+ base: { flexDirection: 'row' },
9472
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
9473
+ },
9474
+ dropzone: {
9475
+ base: { alignSelf: 'stretch', display: 'flex', flexDirection: 'column', alignItems: 'center' },
9476
+ states: {
9477
+ highlighted: { background: 'color-mix(in oklab, var(--color-primary) 8%, var(--color-base-100))' },
9478
+ },
9479
+ },
9480
+ 'item-group': {
9481
+ base: { alignSelf: 'stretch', display: 'flex', flexDirection: 'column' },
9482
+ },
9483
+ item: { base: { flexDirection: 'row' } },
9484
+ 'item-remove': {
9485
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
9486
+ },
9487
+ 'clear-trigger': {
9488
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
9489
+ },
9490
+ },
9491
+ variants: {
9492
+ color: Object.fromEntries(ROLES.map((c) => [c, { dropzone: { states: {
9493
+ highlighted: { background: `color-mix(in oklab, var(--color-${c}) 8%, var(--color-base-100))` },
9494
+ } } }])),
9495
+ },
9496
+ },
9497
+ },
9498
+ };
9499
+ /** A carousel nav circle on lynx: the glyph centred as flex (no `grid`). */
9500
+ const carouselTriggerLynx = {
9501
+ display: 'flex',
9502
+ alignItems: 'center',
9503
+ justifyContent: 'center',
9504
+ };
9505
+ /** A held carousel nav circle on lynx: daisy's hover wash plus the 1px sink, and the lynx ring. */
9506
+ const carouselTriggerLynxStates = {
9507
+ pressed: { background: 'var(--color-base-300)', transform: 'translateY(1px)' },
9508
+ ...lynxFocus(),
8873
9509
  };
8874
9510
  /**
8875
9511
  * Carousel — daisy's carousel is exactly a scroll-snap box; the additions
@@ -9021,14 +9657,60 @@ export const carousel = {
9021
9657
  // spellings resolve on iOS but not on Android (measured,
9022
9658
  // signalxjs/lynx#1084), so the emitter refuses them. Physical is the
9023
9659
  // lynx target's norm — no RTL flow there, so `start` IS left.
9660
+ //
9661
+ // signalxjs/lynx#1279 — the rest of the lynx shape:
9662
+ // - `grid` does not lay out on lynx: the root is a flex column, and a
9663
+ // trigger centres its glyph as flex.
9664
+ // - The dot: the web draws it on `::before` inside a
9665
+ // `max(dot, 1.5rem)` button; lynx has neither pseudo-elements nor
9666
+ // `max()`, so lynx-zero renders the dot AS the indicator (its touch
9667
+ // area widened with `hit-slop`) and the ring and the active fill ride
9668
+ // the part.
9669
+ // - The web sinks a held trigger with the standalone `translate`,
9670
+ // which Android ignores (#1084): a transform function here, over
9671
+ // daisy's hover wash (no hover on touch).
9672
+ // - Rings: lynx's outline ignores radius and has no offset (#1163).
9673
+ // No replacement for the viewport's `:focus-visible` ring: there is no
9674
+ // keyboard focus on lynx, and the part declares no flag to key it on.
9024
9675
  targets: {
9025
9676
  lynx: {
9026
9677
  parts: {
9678
+ root: { base: { display: 'flex', flexDirection: 'column' } },
9027
9679
  'prev-trigger': {
9028
- base: { top: 'calc(50% - var(--carousel-nav) / 2)', left: 'var(--space-sm)' },
9680
+ base: {
9681
+ top: 'calc(50% - var(--carousel-nav) / 2)',
9682
+ left: 'var(--space-sm)',
9683
+ ...carouselTriggerLynx,
9684
+ },
9685
+ states: carouselTriggerLynxStates,
9029
9686
  },
9030
9687
  'next-trigger': {
9031
- base: { top: 'calc(50% - var(--carousel-nav) / 2)', right: 'var(--space-sm)' },
9688
+ base: {
9689
+ top: 'calc(50% - var(--carousel-nav) / 2)',
9690
+ right: 'var(--space-sm)',
9691
+ ...carouselTriggerLynx,
9692
+ },
9693
+ states: carouselTriggerLynxStates,
9694
+ },
9695
+ 'indicator-group': { base: { flexDirection: 'row', alignItems: 'center' } },
9696
+ indicator: {
9697
+ base: {
9698
+ display: 'flex',
9699
+ width: 'var(--carousel-dot)',
9700
+ height: 'var(--carousel-dot)',
9701
+ flexShrink: '0',
9702
+ // Longhands: the ring's colour bakes per theme only
9703
+ // on its own (a shorthand mixing it with the
9704
+ // --border calc cannot).
9705
+ borderWidth: 'calc(var(--border) * 2)',
9706
+ borderStyle: 'solid',
9707
+ borderColor: 'color-mix(in oklab, var(--color-base-content) 70%, transparent)',
9708
+ borderRadius: '9999px',
9709
+ },
9710
+ states: {
9711
+ active: { background: 'var(--carousel-accent)', borderColor: 'var(--carousel-accent)' },
9712
+ ...lynxFocus(),
9713
+ },
9032
9714
  },
9033
9715
  },
9034
9716
  },