@sigx/zero-daisyui 0.7.0 → 0.9.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 (49) hide show
  1. package/dist/audit.json +3868 -2
  2. package/dist/components.d.ts +37 -1
  3. package/dist/components.js +4 -0
  4. package/dist/css/components/avatar-group.css +168 -0
  5. package/dist/css/components/breadcrumbs.css +30 -0
  6. package/dist/css/components/card.css +26 -0
  7. package/dist/css/components/chat-log.css +172 -0
  8. package/dist/css/components/divider.css +68 -3
  9. package/dist/css/components/hover-card.css +174 -0
  10. package/dist/css/components/menu.css +7 -0
  11. package/dist/css/components/menubar.css +75 -0
  12. package/dist/css/components/pagination.css +83 -0
  13. package/dist/css/components/steps.css +103 -0
  14. package/dist/css/components/timeline.css +14 -0
  15. package/dist/css/components/toast.css +65 -0
  16. package/dist/css/components/tree-view.css +118 -0
  17. package/dist/css/index.css +1165 -69
  18. package/dist/lynx/components/accordion.css +3 -1
  19. package/dist/lynx/components/avatar-group.css +97 -0
  20. package/dist/lynx/components/breadcrumbs.css +44 -0
  21. package/dist/lynx/components/button.css +71 -12
  22. package/dist/lynx/components/card.css +9 -0
  23. package/dist/lynx/components/chat-log.css +147 -0
  24. package/dist/lynx/components/dialog.css +9 -3
  25. package/dist/lynx/components/divider.css +12 -2
  26. package/dist/lynx/components/hover-card.css +211 -0
  27. package/dist/lynx/components/menu.css +7 -0
  28. package/dist/lynx/components/menubar.css +199 -0
  29. package/dist/lynx/components/pagination.css +107 -0
  30. package/dist/lynx/components/popover.css +7 -2
  31. package/dist/lynx/components/select.css +40 -3
  32. package/dist/lynx/components/slider.css +3 -2
  33. package/dist/lynx/components/steps.css +103 -0
  34. package/dist/lynx/components/switch.css +2 -1
  35. package/dist/lynx/components/tabs.css +7 -1
  36. package/dist/lynx/components/timeline.css +32 -0
  37. package/dist/lynx/components/toast.css +70 -2
  38. package/dist/lynx/components/tree-view.css +28 -0
  39. package/dist/lynx/index.css +1297 -114
  40. package/dist/lynx/manifest.json +112 -4
  41. package/dist/manifest.json +110 -2
  42. package/dist/recipes.d.ts +20 -1
  43. package/dist/recipes.d.ts.map +1 -1
  44. package/dist/recipes.js +853 -99
  45. package/dist/recipes.js.map +1 -1
  46. package/dist/register.d.ts +36 -0
  47. package/dist/report.json +1320 -213
  48. package/package.json +4 -4
  49. package/src/recipes.ts +877 -99
package/src/recipes.ts CHANGED
@@ -39,6 +39,41 @@ const focusRing: Record<string, NonNullable<PartStyles['base']>> = {
39
39
  },
40
40
  };
41
41
 
42
+ /**
43
+ * The focus ring on lynx, for `targets.lynx` (signalxjs/lynx#1163): the same
44
+ * 2px ring 2px clear of the part, spelled as spread box-shadows.
45
+ *
46
+ * The web ring is `outline` + `outline-offset`, and neither survives on
47
+ * lynx: its `outline` ignores `border-radius` (a square box around a
48
+ * rounded button, a rectangle around the switch pill) and it has no
49
+ * `outline-offset` property at all, so the ring sat flush on the fill. A
50
+ * zero-blur shadow's spread follows the element's radius. Shadows stack
51
+ * rather than mask, so the offset gap cannot be transparent: the inner
52
+ * shadow paints it in `gap` — the surface the part sits on, which is what
53
+ * the web's gap shows through to. `outline: none` retires the shared ring
54
+ * in the same merged declaration block.
55
+ *
56
+ * `under` appends the part's own lynx shadows (a knob's depth relief), which
57
+ * the focus rule would otherwise replace. `inset` draws the ring inside the
58
+ * part, flush with its edge, for a part whose parent clips an outside ring
59
+ * away (signalxjs/lynx#1164, the accordion trigger inside its card).
60
+ */
61
+ const lynxFocusRing = (
62
+ ink: string,
63
+ { gap = 'var(--color-base-100)', under, inset = false }: { gap?: string; under?: string; inset?: boolean } = {},
64
+ ): CssProps => ({
65
+ outline: 'none',
66
+ boxShadow: [
67
+ ...(inset ? [`inset 0 0 0 2px ${ink}`] : [`0 0 0 2px ${gap}`, `0 0 0 4px ${ink}`]),
68
+ ...(under ? [under] : []),
69
+ ].join(', '),
70
+ });
71
+
72
+ /** `focusRing`'s lynx spelling — the base-content ring, as a states entry. */
73
+ const lynxFocus = (opts?: Parameters<typeof lynxFocusRing>[1]): Record<string, CssProps> => ({
74
+ 'focus-visible': lynxFocusRing('var(--color-base-content)', opts),
75
+ });
76
+
42
77
  /**
43
78
  * How far each role survives in the button's INK — the colour a transparent or
44
79
  * tinted fill draws its label, border and focus ring with.
@@ -524,7 +559,7 @@ export const tabs: RecipeInput = {
524
559
  parts: {
525
560
  // No hover on a touch platform: the held tab takes the ink
526
561
  // daisy's hover gives it.
527
- tab: { states: { pressed: { color: 'var(--color-base-content)' } } },
562
+ tab: { states: { pressed: { color: 'var(--color-base-content)' }, ...lynxFocus() } },
528
563
  // The list is the indicator's containing block.
529
564
  list: { base: { position: 'relative' } },
530
565
  // The underline's ink (read by the `border` flavor below) —
@@ -577,6 +612,14 @@ export const tabs: RecipeInput = {
577
612
  base: { '--tab-radius-limit': 'var(--radius-field)' },
578
613
  },
579
614
  },
615
+ // The box flavor's tabs sit on the list's base-200
616
+ // surface, so the ring's gap is that surface. Stated on
617
+ // the variant (one class more than the part rule) so it
618
+ // also outranks the active pill's `--shadow-sm`, which
619
+ // is a box-shadow too.
620
+ box: {
621
+ tab: { states: lynxFocus({ gap: 'var(--color-base-200)' }) },
622
+ },
580
623
  },
581
624
  },
582
625
  },
@@ -931,6 +974,9 @@ export const switchRecipe: RecipeInput = {
931
974
  borderColor: 'var(--switch-accent)',
932
975
  backgroundColor: 'var(--color-base-100)',
933
976
  },
977
+ // The pill ring (signalxjs/lynx#1163), keeping the
978
+ // track's inset depth line under it.
979
+ 'focus-visible': lynxFocusRing('var(--switch-accent)', { under: '0 1px var(--depth-shade) inset' }),
934
980
  },
935
981
  },
936
982
  thumb: {
@@ -1056,6 +1102,43 @@ const lynxBtnPad = (mult: number): NonNullable<PartStyles['base']> => ({
1056
1102
  paddingRight: `calc(var(--size-field) * ${mult})`,
1057
1103
  });
1058
1104
 
1105
+ /**
1106
+ * A four-side property spelled as its physical longhands, for `targets.lynx`
1107
+ * sections that must not ship a `var()`-bearing shorthand.
1108
+ *
1109
+ * Lynx keeps a declaration holding `var()` under its own property id,
1110
+ * unexpanded, and expands it only after the cascade has merged every rule
1111
+ * into one insertion-ordered style map — in that map's order, writing over
1112
+ * the longhands already there (`CSSVariableHandler::HandleCSSVariables`). A
1113
+ * static shorthand, by contrast, is expanded into its longhands at parse
1114
+ * time. So a var-bearing `border-color` and a static `border-top-color` never
1115
+ * meet in the cascade at all: whichever key the map saw later wins, whatever
1116
+ * the specificity (signalxjs/lynx#1161, #1162). Longhands share one key per
1117
+ * side, and the cascade decides again.
1118
+ */
1119
+ const lynxSides = (
1120
+ prop: 'border{}Width' | 'border{}Color' | 'padding{}',
1121
+ value: string,
1122
+ sides: readonly ('Top' | 'Right' | 'Bottom' | 'Left')[] = ['Top', 'Right', 'Bottom', 'Left'],
1123
+ ): NonNullable<PartStyles['base']> =>
1124
+ Object.fromEntries(sides.map((side) => [prop.replace('{}', side), value]));
1125
+
1126
+ /**
1127
+ * daisy's `.btn` is content-sized and never squeezed: `display: inline-flex`
1128
+ * with `flex-shrink: 0; flex-wrap: nowrap`, so a narrow row makes the button
1129
+ * overflow, never break its label. A lynx view has no inline formatting
1130
+ * context: it stretches to a linear or column parent, and a squeezed label
1131
+ * `<text>` then wraps per character ("Bt / n", signalxjs/lynx#1165).
1132
+ * `width: max-content` is the intrinsic keyword starlight implements (an
1133
+ * intrinsic width is also exempt from the cross-axis stretch), so the box
1134
+ * sizes to its content as the web's does; the other two are daisy's own.
1135
+ */
1136
+ const lynxBtnFit: NonNullable<PartStyles['base']> = {
1137
+ width: 'max-content',
1138
+ flexShrink: '0',
1139
+ flexWrap: 'nowrap',
1140
+ };
1141
+
1059
1142
  /** The btn size ramp's physical inline padding, for `targets.lynx.variants`. */
1060
1143
  const lynxBtnSizes = (part: string): Record<string, Record<string, PartStyles>> => ({
1061
1144
  xs: { [part]: { base: lynxBtnPad(2) } },
@@ -1177,9 +1260,10 @@ export const dialog: RecipeInput = {
1177
1260
  targets: {
1178
1261
  lynx: {
1179
1262
  parts: {
1180
- trigger: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed } },
1181
- close: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed } },
1182
- cancel: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed } },
1263
+ // Content-sized like daisy's btn, never broken mid-word (signalxjs/lynx#1165).
1264
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1265
+ close: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1266
+ cancel: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1183
1267
  footer: { base: { marginTop: 'var(--space-2xl)', flexDirection: 'row' } },
1184
1268
  },
1185
1269
  variants: { size: lynxBtnSizes('trigger') },
@@ -1274,8 +1358,9 @@ export const popover: RecipeInput = {
1274
1358
  web: floatingArrow('popover', panelArrowPaint),
1275
1359
  lynx: {
1276
1360
  parts: {
1277
- trigger: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed } },
1278
- close: { base: lynxBtnPad(3), states: { pressed: lynxBtnPressed } },
1361
+ // Content-sized like daisy's btn, never broken mid-word (signalxjs/lynx#1165).
1362
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1363
+ close: { base: lynxBtnPad(3), states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1279
1364
  },
1280
1365
  variants: { size: lynxBtnSizes('trigger') },
1281
1366
  },
@@ -1326,6 +1411,57 @@ export const tooltip: RecipeInput = {
1326
1411
  },
1327
1412
  };
1328
1413
 
1414
+ /**
1415
+ * Hover card — daisy's `link` opening a `dropdown-content` card. The trigger
1416
+ * is running text, not a btn: underlined in the ink it sits in (daisy's bare
1417
+ * `link`), and a colour is `link-<role>` in the role's readable ink
1418
+ * (`roleInk`). The underline thickens on hover and while the card is up,
1419
+ * rather than the ink dimming — the `btn-link` reasoning. The card is the
1420
+ * floating panel popover and menu wear.
1421
+ */
1422
+ export const hoverCard: RecipeInput = {
1423
+ component: 'hover-card',
1424
+ parts: {
1425
+ trigger: {
1426
+ base: {
1427
+ color: 'inherit',
1428
+ textDecorationLine: 'underline',
1429
+ textUnderlineOffset: '2px',
1430
+ cursor: 'pointer',
1431
+ borderRadius: 'var(--radius-selector)',
1432
+ },
1433
+ states: {
1434
+ hover: { textDecorationThickness: '2px' },
1435
+ open: { textDecorationThickness: '2px' },
1436
+ closed: {},
1437
+ ...focusRing,
1438
+ },
1439
+ },
1440
+ popup: withPresence(popupPresence('translateY(-4px)'), {
1441
+ base: {
1442
+ ...floatingPanel,
1443
+ padding: 'var(--space-xl)',
1444
+ maxWidth: '20rem',
1445
+ fontSize: 'var(--text-sm)',
1446
+ },
1447
+ states: { open: {}, closed: {} },
1448
+ }),
1449
+ },
1450
+ variants: {
1451
+ color: Object.fromEntries(ROLES.map((c) => [c, { trigger: { base: { color: roleInk(c) } } }])),
1452
+ size: {
1453
+ xs: { trigger: { base: { fontSize: 'var(--text-xs)' } } },
1454
+ sm: { trigger: { base: { fontSize: 'var(--text-sm)' } } },
1455
+ md: {},
1456
+ lg: { trigger: { base: { fontSize: 'var(--text-lg)' } } },
1457
+ xl: { trigger: { base: { fontSize: 'var(--text-xl)' } } },
1458
+ },
1459
+ },
1460
+ // Web-only: the popover top layer and the runtime's arrow geometry have
1461
+ // no lynx counterpart.
1462
+ targets: { web: floatingArrow('hover-card', panelArrowPaint) },
1463
+ };
1464
+
1329
1465
  // daisy "menu in a dropdown" look.
1330
1466
  export const menu: RecipeInput = {
1331
1467
  component: 'menu',
@@ -1459,6 +1595,17 @@ export const menu: RecipeInput = {
1459
1595
  opacity: '0.6',
1460
1596
  },
1461
1597
  },
1598
+ // daisy's menu hint: the group label's faded ink, pushed to the
1599
+ // row's reading end — faded less, since 0.6 leaves nord's paper
1600
+ // under the 4.5:1 a line read by eye owes.
1601
+ shortcut: {
1602
+ base: {
1603
+ marginInlineStart: 'auto',
1604
+ paddingInlineStart: 'var(--space-lg)',
1605
+ fontSize: 'var(--text-xs)',
1606
+ opacity: '0.75',
1607
+ },
1608
+ },
1462
1609
  separator: {
1463
1610
  base: {
1464
1611
  height: 'var(--border)',
@@ -1474,12 +1621,55 @@ export const menu: RecipeInput = {
1474
1621
  targets: {
1475
1622
  web: floatingArrow('menu', panelArrowPaint),
1476
1623
  lynx: {
1477
- parts: { trigger: { base: lynxBtnPad(4) } },
1624
+ parts: {
1625
+ trigger: { base: lynxBtnPad(4) },
1626
+ // The shortcut's reading-end push, restated physically (#1084).
1627
+ shortcut: { base: { marginLeft: 'auto', paddingLeft: 'var(--space-lg)' } },
1628
+ },
1478
1629
  variants: { size: lynxBtnSizes('trigger') },
1479
1630
  },
1480
1631
  },
1481
1632
  };
1482
1633
 
1634
+ // The menubar row: daisy's `menu-horizontal` strip — a base-200 box the
1635
+ // menu triggers sit in. The triggers are ordinary `menu` triggers.
1636
+ export const menubar: RecipeInput = {
1637
+ component: 'menubar',
1638
+ tokens: { '--menubar-surface': 'var(--color-base-200)' },
1639
+ parts: {
1640
+ root: {
1641
+ base: {
1642
+ display: 'flex',
1643
+ alignItems: 'center',
1644
+ gap: 'var(--space-xs)',
1645
+ padding: 'var(--space-xs)',
1646
+ width: 'max-content',
1647
+ maxWidth: '100%',
1648
+ flexWrap: 'wrap',
1649
+ background: 'var(--menubar-surface)',
1650
+ borderRadius: 'var(--radius-box)',
1651
+ },
1652
+ // The triggers fade themselves — see basic.
1653
+ states: { disabled: {} },
1654
+ selectors: {
1655
+ '&[data-orientation="vertical"]': { flexDirection: 'column', alignItems: 'stretch' },
1656
+ },
1657
+ },
1658
+ },
1659
+ variants: {
1660
+ color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
1661
+ '--menubar-surface': `color-mix(in oklch, var(--color-${c}) 12%, var(--color-base-200))`,
1662
+ } } }])),
1663
+ size: {
1664
+ xs: { root: { base: { gap: '0', padding: '0' } } },
1665
+ sm: { root: { base: { padding: 'var(--space-2xs)' } } },
1666
+ md: {},
1667
+ lg: { root: { base: { gap: 'var(--space-sm)', padding: 'var(--space-sm)' } } },
1668
+ xl: { root: { base: { gap: 'var(--space-md)', padding: 'var(--space-sm)' } } },
1669
+ },
1670
+ },
1671
+ };
1672
+
1483
1673
  export const field: RecipeInput = {
1484
1674
  component: 'field',
1485
1675
  // The label's accent ink — base-content by default, so the un-attributed
@@ -2498,6 +2688,12 @@ export const slider: RecipeInput = {
2498
2688
  control: {
2499
2689
  states: {
2500
2690
  invalid: { '--slider-fill': sliderFill('var(--color-error)') },
2691
+ // The control's ring is the NATIVE input's on the web;
2692
+ // lynx always renders the composed parts, where the
2693
+ // thumb is what takes focus and wears the ring below
2694
+ // (signalxjs/lynx#1163 — both at once read as two
2695
+ // nested boxes).
2696
+ 'focus-visible': { outline: 'none' },
2501
2697
  },
2502
2698
  selectors: {
2503
2699
  '&[data-orientation="vertical"]': {
@@ -2595,6 +2791,13 @@ export const slider: RecipeInput = {
2595
2791
  transform: 'translateY(-50%)',
2596
2792
  marginLeft: 'calc(var(--slider-thumb-size) / -2)',
2597
2793
  },
2794
+ // The round ring (signalxjs/lynx#1163), over the knob's
2795
+ // depth relief.
2796
+ states: {
2797
+ 'focus-visible': lynxFocusRing('var(--slider-accent)', {
2798
+ under: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset, 0 1px var(--depth-shade)',
2799
+ }),
2800
+ },
2598
2801
  },
2599
2802
  },
2600
2803
  },
@@ -2611,7 +2814,20 @@ export const accordion: RecipeInput = {
2611
2814
  // hover fill. The ::after chevron has no lynx counterpart (no
2612
2815
  // pseudo-elements, and the anatomy declares no part for it) —
2613
2816
  // an app draws its own glyph in the trigger.
2614
- trigger: { states: { pressed: { background: 'var(--color-base-200)' } } },
2817
+ //
2818
+ // The item card clips (`overflow: hidden`) and the trigger
2819
+ // fills it, so an outside ring is clipped away entirely
2820
+ // (signalxjs/lynx#1164): the ring is drawn inset, rounded to
2821
+ // the card's inner corners.
2822
+ trigger: {
2823
+ states: {
2824
+ pressed: { background: 'var(--color-base-200)' },
2825
+ 'focus-visible': {
2826
+ ...lynxFocusRing('var(--color-base-content)', { inset: true }),
2827
+ borderRadius: 'calc(var(--radius-box) - var(--border))',
2828
+ },
2829
+ },
2830
+ },
2615
2831
  },
2616
2832
  },
2617
2833
  },
@@ -2902,22 +3118,43 @@ export const select: RecipeInput = {
2902
3118
  // never wider than its container. lynx has no `clamp()`, so the
2903
3119
  // same bounds are spelled out — and the base's 13rem floor
2904
3120
  // comes down to daisy's 3rem, or the trigger overflowed any
2905
- // cell narrower than 208px (signalxjs/lynx#1146). The root
2906
- // caps too, so a shrink-wrapped root cannot push it back out.
2907
- root: { base: { maxWidth: '100%' } },
3121
+ // cell narrower than 208px (signalxjs/lynx#1146).
3122
+ //
3123
+ // The bounds sit on the ROOT, the box daisy's `.select` is,
3124
+ // and the trigger fills it. The web root is `inline-flex`, so
3125
+ // it shrink-wraps the trigger; lynx rewrites that to `flex`,
3126
+ // and a root stretched by its parent while the trigger stayed
3127
+ // capped left the clear-trigger — positioned against the root
3128
+ // — floating past the trigger's end (signalxjs/lynx#1166).
3129
+ root: { base: { width: '20rem', maxWidth: '100%', minWidth: '3rem' } },
2908
3130
  trigger: {
2909
- base: { ...lynxBtnPad(4), width: '20rem', maxWidth: '100%', minWidth: '3rem' },
3131
+ base: { ...lynxBtnPad(4), width: '100%', maxWidth: '100%', minWidth: '3rem' },
2910
3132
  // The web's `:hover` border is dropped on a touch
2911
3133
  // platform; a press gets a wash instead — a background,
2912
3134
  // so it never fights the open/invalid border inks.
2913
- states: { pressed: { background: 'var(--color-base-200)' } },
2914
- },
3135
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
3136
+ },
3137
+ // The popup renders in the overlay outlet, outside the root,
3138
+ // so it cannot inherit the root's `--select-accent`, and an
3139
+ // unresolved `var()` on lynx has no fallback: the selected
3140
+ // item and its tick fell back to base-content
3141
+ // (signalxjs/lynx#1168). The popup restates the accent; its
3142
+ // colour-axis twins are in `variants.color` below.
3143
+ popup: { base: { '--select-accent': 'var(--color-primary)' } },
2915
3144
  // A touch list has no highlight: the pressed item takes the
2916
3145
  // highlighted item's wash.
2917
3146
  item: { states: { pressed: { background: 'var(--color-base-200)' } } },
2918
- 'clear-trigger': { base: { top: 'var(--space-xs)', bottom: 'var(--space-xs)', right: 'calc(var(--size-field) * 4 + 1em + var(--space-xs))', minWidth: '1.5rem' } },
3147
+ 'clear-trigger': {
3148
+ base: { top: 'var(--space-xs)', bottom: 'var(--space-xs)', right: 'calc(var(--size-field) * 4 + 1em + var(--space-xs))', minWidth: '1.5rem' },
3149
+ states: lynxFocus(),
3150
+ },
2919
3151
  },
2920
3152
  variants: {
3153
+ // The popup's accent follows the root's colour axis — the
3154
+ // popup carries the axis class itself (lynx stamps every part).
3155
+ color: Object.fromEntries(ROLES.map((c) => [c, { popup: { base: {
3156
+ '--select-accent': `var(--color-${c})`,
3157
+ } } }])),
2921
3158
  size: { ...lynxBtnSizes('trigger'), md: { trigger: { base: lynxBtnPad(4) } } },
2922
3159
  },
2923
3160
  },
@@ -2942,6 +3179,23 @@ const BTN_STEPS = { xs: 6, sm: 8, md: 10, lg: 12, xl: 14 } as const;
2942
3179
  const btnHeight = (step: keyof typeof BTN_STEPS): string =>
2943
3180
  `calc(var(--size-field) * ${BTN_STEPS[step]})`;
2944
3181
 
3182
+ /**
3183
+ * The btn padding ramp — `[size, block space, inline space]` per step. One
3184
+ * table for the web's `padding` shorthand and the lynx section's longhands,
3185
+ * so the two spellings cannot drift apart.
3186
+ */
3187
+ const BTN_PAD = [
3188
+ ['xs', '2xs', 'xs'],
3189
+ ['sm', 'xs', 'sm'],
3190
+ ['md', 'sm', 'lg'],
3191
+ ['lg', 'md', 'xl'],
3192
+ ['xl', 'lg', '2xl'],
3193
+ ] as const satisfies readonly (readonly [keyof typeof BTN_STEPS, string, string])[];
3194
+ const btnPadding = (step: keyof typeof BTN_STEPS): string => {
3195
+ const [, block, inline] = BTN_PAD.find(([size]) => size === step)!;
3196
+ return `var(--space-${block}) var(--space-${inline})`;
3197
+ };
3198
+
2945
3199
  /**
2946
3200
  * daisy 5's disabled btn, for the lynx target (signalxjs/lynx#1143). The web
2947
3201
  * fades the whole button (`opacity: var(--disabled-opacity)`, 0.3 on the
@@ -3139,11 +3393,11 @@ export const button: RecipeInput = {
3139
3393
  // The `<button>` element is UA `border-box`, so the stated height is
3140
3394
  // the rendered height, border included — as in daisy.
3141
3395
  size: {
3142
- xs: { root: { base: { height: btnHeight('xs'), padding: 'var(--space-2xs) var(--space-xs)', fontSize: 'var(--text-xs)' } } },
3143
- sm: { root: { base: { height: btnHeight('sm'), padding: 'var(--space-xs) var(--space-sm)', fontSize: 'var(--text-sm)' } } },
3144
- md: { root: { base: { height: btnHeight('md'), padding: 'var(--space-sm) var(--space-lg)', fontSize: 'var(--text-md)' } } },
3145
- lg: { root: { base: { height: btnHeight('lg'), padding: 'var(--space-md) var(--space-xl)', fontSize: 'var(--text-lg)' } } },
3146
- xl: { root: { base: { height: btnHeight('xl'), padding: 'var(--space-lg) var(--space-2xl)', fontSize: 'var(--text-xl)' } } },
3396
+ xs: { root: { base: { height: btnHeight('xs'), padding: btnPadding('xs'), fontSize: 'var(--text-xs)' } } },
3397
+ sm: { root: { base: { height: btnHeight('sm'), padding: btnPadding('sm'), fontSize: 'var(--text-sm)' } } },
3398
+ md: { root: { base: { height: btnHeight('md'), padding: btnPadding('md'), fontSize: 'var(--text-md)' } } },
3399
+ lg: { root: { base: { height: btnHeight('lg'), padding: btnPadding('lg'), fontSize: 'var(--text-lg)' } } },
3400
+ xl: { root: { base: { height: btnHeight('xl'), padding: btnPadding('xl'), fontSize: 'var(--text-xl)' } } },
3147
3401
  },
3148
3402
  },
3149
3403
  /**
@@ -3180,36 +3434,90 @@ export const button: RecipeInput = {
3180
3434
  // The ring, restated: `currentColor` never resolves on lynx
3181
3435
  // (signalxjs/lynx#1079) and the logical sizes/edge are
3182
3436
  // unproven there, so the lynx ring is physical longhands in
3183
- // the variant's own ink (the variants below pick it).
3437
+ // the variant's own ink (the variants below pick it). Every
3438
+ // var-bearing edge is a longhand (`lynxSides`): a var-bearing
3439
+ // `border` expanded after the transparent top and closed the
3440
+ // ring (signalxjs/lynx#1161). The shorthand stays, static, so
3441
+ // the shared `currentColor` spelling never ships.
3184
3442
  spinner: {
3185
3443
  base: {
3186
3444
  width: '1em',
3187
3445
  height: '1em',
3188
- border: 'calc(var(--border) * 2) solid var(--btn-ink)',
3446
+ border: '0 solid transparent',
3447
+ ...lynxSides('border{}Width', 'calc(var(--border) * 2)'),
3448
+ ...lynxSides('border{}Color', 'var(--btn-ink)', ['Right', 'Bottom', 'Left']),
3189
3449
  borderTopColor: 'transparent',
3190
3450
  },
3191
3451
  },
3192
- root: { states: { disabled: lynxBtnDisabled } },
3452
+ root: {
3453
+ // daisy's content-sized, never-squeezed btn (#1165), and
3454
+ // the hairline as longhands: a var-bearing `border`
3455
+ // shorthand would expand over the variants' and the
3456
+ // disabled state's edge colours (see `lynxSides`).
3457
+ base: {
3458
+ ...lynxBtnFit,
3459
+ border: '0 solid transparent',
3460
+ ...lynxSides('border{}Width', 'var(--border)'),
3461
+ },
3462
+ // The ring in the variant's own ink (signalxjs/lynx#1163).
3463
+ states: { disabled: lynxBtnDisabled, 'focus-visible': lynxFocusRing('var(--btn-ink)') },
3464
+ },
3193
3465
  },
3194
3466
  variants: {
3195
3467
  // The baked disabled paint above outranks every variant's fill
3196
3468
  // (the theme host adds a class), but its plain declarations
3197
3469
  // do not: outline/dash restate the border, ghost/link drop
3198
- // the wash (as in daisy).
3470
+ // the wash (as in daisy). outline/dash draw their ink edge as
3471
+ // longhands, so that transparent restatement wins by
3472
+ // specificity instead of losing to a var-bearing
3473
+ // `border-color` (signalxjs/lynx#1162); the shared shorthand
3474
+ // is neutralised to a static value.
3199
3475
  variant: {
3200
- solid: { spinner: { base: { borderColor: 'var(--btn-on-accent)', borderTopColor: 'transparent' } } },
3201
- outline: { root: { states: { disabled: { borderColor: 'transparent' } } } },
3202
- dash: { root: { states: { disabled: { borderColor: 'transparent' } } } },
3476
+ solid: { spinner: { base: lynxSides('border{}Color', 'var(--btn-on-accent)', ['Right', 'Bottom', 'Left']) } },
3477
+ outline: {
3478
+ root: {
3479
+ base: { borderColor: 'transparent', ...lynxSides('border{}Color', 'var(--btn-ink)') },
3480
+ states: { disabled: { borderColor: 'transparent' } },
3481
+ },
3482
+ },
3483
+ dash: {
3484
+ root: {
3485
+ base: { borderColor: 'transparent', ...lynxSides('border{}Color', 'var(--btn-ink)') },
3486
+ states: { disabled: { borderColor: 'transparent' } },
3487
+ },
3488
+ },
3203
3489
  ghost: { root: { states: { disabled: { background: 'transparent' } } } },
3204
- link: { root: { states: { disabled: { background: 'transparent' } } } },
3205
- },
3490
+ // `link` zeroes the box-shadow in a variant rule that
3491
+ // lands after the ring, so it restates the ring.
3492
+ link: { root: { states: { disabled: { background: 'transparent' }, 'focus-visible': lynxFocusRing('var(--btn-ink)') } } },
3493
+ },
3494
+ // The size ramp's paddings as longhands, so square/circle's
3495
+ // zeroed sides below win by the cascade rather than by the
3496
+ // order lynx happens to expand a var-bearing `padding` in.
3497
+ size: Object.fromEntries(BTN_PAD.map(([size, block, inline]) => [size, {
3498
+ root: {
3499
+ base: {
3500
+ padding: '0',
3501
+ ...lynxSides('padding{}', `var(--space-${block})`, ['Top', 'Bottom']),
3502
+ ...lynxSides('padding{}', `var(--space-${inline})`, ['Right', 'Left']),
3503
+ },
3504
+ },
3505
+ }])),
3206
3506
  },
3207
3507
  // The icon chips' zeroed inline padding, restated physically: logical
3208
3508
  // spellings resolve on iOS but not on Android (measured,
3209
- // signalxjs/lynx#1084), so the emitter refuses them.
3509
+ // signalxjs/lynx#1084), so the emitter refuses them. `width: auto`
3510
+ // undoes the root's `max-content`, which would otherwise switch
3511
+ // off the `aspect-ratio` that squares the chip. `wide`/`block`
3512
+ // restate their width physically: `inline-size` and
3513
+ // `max-inline-size` are not lynx properties at all.
3210
3514
  modifiers: {
3211
- square: { root: { base: { paddingLeft: '0', paddingRight: '0' } } },
3212
- circle: { root: { base: { paddingLeft: '0', paddingRight: '0' } } },
3515
+ wide: { root: { base: { width: '100%', maxWidth: '16rem' } } },
3516
+ block: { root: { base: { width: '100%' } } },
3517
+ square: { root: { base: { width: 'auto', paddingLeft: '0', paddingRight: '0' } } },
3518
+ circle: { root: { base: { width: 'auto', paddingLeft: '0', paddingRight: '0' } } },
3519
+ // `active` zeroes the box-shadow too (the held press look).
3520
+ active: { root: { states: { 'focus-visible': lynxFocusRing('var(--btn-ink)') } } },
3213
3521
  },
3214
3522
  },
3215
3523
  },
@@ -3291,6 +3599,118 @@ export const avatar: RecipeInput = {
3291
3599
  },
3292
3600
  };
3293
3601
 
3602
+ /**
3603
+ * Avatar group (#297) — daisy's `avatar-group`: the faces overlap by a
3604
+ * quarter, each already cut out by the avatar's own base-100 gap ring, and the
3605
+ * "+N" chip is daisy's `avatar-placeholder` (neutral fill, neutral-content
3606
+ * ink, a circle) wearing the same ring. The group's `size` borrows the
3607
+ * avatar's own size step (`composes`), so a group sizes its faces without a
3608
+ * prop on each, and an avatar's own `size` still wins. Colour is daisy's
3609
+ * ring colour: it rings the chip, and it is borrowed by the avatars too, so
3610
+ * one `color` on the group rings every face alike.
3611
+ */
3612
+ const avatarGroupSteps: Record<string, [string, string]> = {
3613
+ xs: ['calc(var(--size-selector) * 6)', 'var(--text-xs)'],
3614
+ sm: ['calc(var(--size-selector) * 8)', 'var(--text-xs)'],
3615
+ lg: ['calc(var(--size-selector) * 12)', 'var(--text-md)'],
3616
+ xl: ['calc(var(--size-selector) * 16)', 'var(--text-lg)'],
3617
+ };
3618
+
3619
+ export const avatarGroup: RecipeInput = {
3620
+ component: 'avatar-group',
3621
+ tokens: {
3622
+ '--avatar-group-size': 'calc(var(--size-selector) * 10)',
3623
+ '--avatar-group-text': 'var(--text-sm)',
3624
+ '--avatar-group-ring': 'var(--color-primary)',
3625
+ },
3626
+ parts: {
3627
+ root: {
3628
+ base: {
3629
+ display: 'inline-flex',
3630
+ alignItems: 'center',
3631
+ verticalAlign: 'middle',
3632
+ isolation: 'isolate',
3633
+ },
3634
+ },
3635
+ overflow: {
3636
+ base: {
3637
+ // Positioned like the avatars, or it would paint under the
3638
+ // face it overlaps.
3639
+ position: 'relative',
3640
+ display: 'inline-grid',
3641
+ placeItems: 'center',
3642
+ boxSizing: 'border-box',
3643
+ minWidth: 'var(--avatar-group-size)',
3644
+ height: 'var(--avatar-group-size)',
3645
+ paddingInline: 'var(--space-2xs)',
3646
+ marginInlineStart: 'calc(var(--avatar-group-size) * -0.25)',
3647
+ borderRadius: '9999px',
3648
+ boxShadow: '0 0 0 2px var(--color-base-100), 0 0 0 4px var(--avatar-group-ring)',
3649
+ background: 'var(--color-neutral)',
3650
+ color: 'var(--color-neutral-content)',
3651
+ fontSize: 'var(--avatar-group-text)',
3652
+ fontWeight: 'var(--weight-semibold)',
3653
+ fontVariantNumeric: 'tabular-nums',
3654
+ whiteSpace: 'nowrap',
3655
+ userSelect: 'none',
3656
+ },
3657
+ },
3658
+ },
3659
+ composes: {
3660
+ avatar: {
3661
+ parts: {
3662
+ root: {
3663
+ selectors: {
3664
+ '&:not(:first-child)': { marginInlineStart: 'calc(var(--avatar-group-size) * -0.25)' },
3665
+ },
3666
+ },
3667
+ },
3668
+ },
3669
+ },
3670
+ compoundVariants: [
3671
+ ...Object.keys(avatarGroupSteps).map((size) => ({
3672
+ match: { size },
3673
+ parts: {},
3674
+ composes: { avatar: { axes: { size } } },
3675
+ })),
3676
+ ...ROLES.map((color) => ({
3677
+ match: { color },
3678
+ parts: {},
3679
+ composes: { avatar: { axes: { color } } },
3680
+ })),
3681
+ ],
3682
+ variants: {
3683
+ color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
3684
+ '--avatar-group-ring': `var(--color-${c})`,
3685
+ } } }])),
3686
+ size: {
3687
+ ...Object.fromEntries(Object.entries(avatarGroupSteps).map(([size, [box, text]]) => [size, { root: { base: {
3688
+ '--avatar-group-size': box, '--avatar-group-text': text,
3689
+ } } }])),
3690
+ // `md` is the un-attributed render — the defaults in `tokens:`.
3691
+ md: {},
3692
+ },
3693
+ },
3694
+ };
3695
+
3696
+ /**
3697
+ * A toast whose indicator is rendered (#292): the grid grows a leading column
3698
+ * for the mark, and the text, action and close step one column along.
3699
+ */
3700
+ const TOAST_MARKED = '[data-scope="toast"][data-part="root"]:has(> [data-scope="toast"][data-part="indicator"]) > &';
3701
+ /** The indicator's marks, cut from its fill — not mirrored in RTL (a tick is not a direction). */
3702
+ const TOAST_CHECK = 'polygon(15.1% 41.3%, 1% 55%, 37.6% 92.8%, 99% 19.8%, 83.9% 7.2%, 36.6% 63.5%)';
3703
+ const TOAST_CROSS = 'polygon(20% 8%, 50% 38%, 80% 8%, 92% 20%, 62% 50%, 92% 80%, 80% 92%, 50% 62%, 20% 92%, 8% 80%, 38% 50%, 8% 20%)';
3704
+
3705
+ /**
3706
+ * daisy's success and error are FILLS — light enough in `light` and `nord`
3707
+ * that a bare mark in them reads under 2:1 on the toast's base-200. Deepened
3708
+ * toward base-content (which flips with the theme), the hue survives and the
3709
+ * mark clears the 3:1 non-text floor in every theme.
3710
+ */
3711
+ const TOAST_SUCCESS_INK = 'color-mix(in oklab, var(--color-success) 55%, var(--color-base-content))';
3712
+ const TOAST_ERROR_INK = 'color-mix(in oklab, var(--color-error) 55%, var(--color-base-content))';
3713
+
3294
3714
  /**
3295
3715
  * Toast presence is runtime-managed (see the SKILL's Toast section): plain
3296
3716
  * two-state transitions only — no `@starting-style`, no `allow-discrete`.
@@ -3301,6 +3721,7 @@ export const toast: RecipeInput = {
3301
3721
  '--toast-bg': 'var(--color-base-200)',
3302
3722
  '--toast-ink': 'var(--color-base-content)',
3303
3723
  '--toast-from': '8px',
3724
+ '--toast-mark': '1rem',
3304
3725
  },
3305
3726
  parts: {
3306
3727
  viewport: {
@@ -3354,6 +3775,7 @@ export const toast: RecipeInput = {
3354
3775
  },
3355
3776
  selectors: {
3356
3777
  '&[data-placement^="top"]': { '--toast-from': '-8px' },
3778
+ '&:has(> [data-scope="toast"][data-part="indicator"])': { gridTemplateColumns: 'auto 1fr auto auto' },
3357
3779
  },
3358
3780
  states: {
3359
3781
  open: { opacity: '1', transform: 'none' },
@@ -3363,8 +3785,39 @@ export const toast: RecipeInput = {
3363
3785
  'reduced-motion': { base: { transition: 'none' }, states: { open: { transform: 'none' } } },
3364
3786
  },
3365
3787
  },
3788
+ // daisy's `loading-spinner` ring in the alert's own ink, then the
3789
+ // success tick or the error cross — the promise toast's status.
3790
+ indicator: {
3791
+ base: {
3792
+ gridColumn: '1',
3793
+ gridRow: '1 / span 2',
3794
+ inlineSize: 'var(--toast-mark)',
3795
+ blockSize: 'var(--toast-mark)',
3796
+ boxSizing: 'border-box',
3797
+ },
3798
+ states: {
3799
+ loading: {
3800
+ borderRadius: '50%',
3801
+ border: 'calc(var(--border) * 2) solid color-mix(in oklab, var(--toast-ink) 25%, transparent)',
3802
+ borderBlockStartColor: 'var(--toast-ink)',
3803
+ animation: 'zero-daisyui-toast-spin 0.75s linear infinite',
3804
+ },
3805
+ complete: { background: TOAST_SUCCESS_INK, clipPath: TOAST_CHECK },
3806
+ error: { background: TOAST_ERROR_INK, clipPath: TOAST_CROSS },
3807
+ },
3808
+ at: {
3809
+ 'reduced-motion': { states: { loading: { animation: 'none' } } },
3810
+ 'forced-colors': {
3811
+ states: {
3812
+ complete: { background: 'CanvasText', forcedColorAdjust: 'none' },
3813
+ error: { background: 'CanvasText', forcedColorAdjust: 'none' },
3814
+ },
3815
+ },
3816
+ },
3817
+ },
3366
3818
  title: {
3367
3819
  base: { gridColumn: '1', fontWeight: 'var(--weight-semibold)' },
3820
+ selectors: { [TOAST_MARKED]: { gridColumn: '2' } },
3368
3821
  },
3369
3822
  description: {
3370
3823
  base: {
@@ -3372,6 +3825,7 @@ export const toast: RecipeInput = {
3372
3825
  fontSize: 'var(--text-xs)',
3373
3826
  color: 'color-mix(in oklab, var(--toast-ink) 75%, transparent)',
3374
3827
  },
3828
+ selectors: { [TOAST_MARKED]: { gridColumn: '2' } },
3375
3829
  },
3376
3830
  action: {
3377
3831
  base: {
@@ -3392,6 +3846,7 @@ export const toast: RecipeInput = {
3392
3846
  disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
3393
3847
  ...focusRing,
3394
3848
  },
3849
+ selectors: { [TOAST_MARKED]: { gridColumn: '3' } },
3395
3850
  },
3396
3851
  close: {
3397
3852
  base: {
@@ -3411,6 +3866,7 @@ export const toast: RecipeInput = {
3411
3866
  disabled: { opacity: 'var(--disabled-opacity)' },
3412
3867
  ...focusRing,
3413
3868
  },
3869
+ selectors: { [TOAST_MARKED]: { gridColumn: '4' } },
3414
3870
  },
3415
3871
  },
3416
3872
  variants: {
@@ -3447,6 +3903,7 @@ export const toast: RecipeInput = {
3447
3903
  // signalxjs/lynx#1070. No `color` default: the un-attributed toast is a
3448
3904
  // neutral base-200 surface, outside the color vocabulary.
3449
3905
  defaultVariants: { size: 'md' },
3906
+ keyframes: { 'zero-daisyui-toast-spin': 'to { transform: rotate(360deg); }' },
3450
3907
  targets: {
3451
3908
  lynx: {
3452
3909
  parts: {
@@ -3500,16 +3957,25 @@ export const toast: RecipeInput = {
3500
3957
  // the same ink at 75% opacity reads the same on the soft fill.
3501
3958
  description: { base: { color: 'var(--toast-ink)', opacity: '0.75' } },
3502
3959
  // The 25% ink border cannot bake either: it takes the full ink.
3960
+ // Their rings' gap is the card's own fill.
3503
3961
  action: {
3504
3962
  base: {
3505
3963
  marginTop: 'var(--space-xs)',
3506
3964
  border: 'var(--border) solid var(--toast-ink)',
3507
3965
  },
3966
+ states: lynxFocus({ gap: 'var(--toast-bg)' }),
3967
+ },
3968
+ close: {
3969
+ base: { position: 'absolute', top: 'var(--space-xs)', right: 'var(--space-xs)' },
3970
+ states: lynxFocus({ gap: 'var(--toast-bg)' }),
3508
3971
  },
3509
- close: { base: { position: 'absolute', top: 'var(--space-xs)', right: 'var(--space-xs)' } },
3510
3972
  },
3511
3973
  },
3512
3974
  },
3975
+ // The viewport's `open` (the stack expanded) is a fact this skin has no
3976
+ // use for: its toasts are always a plain column, so both states look the
3977
+ // same by design (#292).
3978
+ skipStates: { viewport: ['open', 'closed'] },
3513
3979
  };
3514
3980
 
3515
3981
  export const combobox: RecipeInput = {
@@ -4325,12 +4791,29 @@ const treeRowSelectors: NonNullable<PartStyles['selectors']> = {
4325
4791
  },
4326
4792
  };
4327
4793
 
4794
+ /**
4795
+ * `tickGlyphFallback` for the node check box, whose tick is its `::after`:
4796
+ * the geometry drops and the same pseudo carries the glyph, in the medium's
4797
+ * own ink (the #233 divergence, restated).
4798
+ */
4799
+ const nodeTickFallback = (ink: string): PartStyles => ({
4800
+ selectors: {
4801
+ '&::after': { clipPath: 'none', backgroundColor: '#0000', rotate: '0deg', inset: '0', display: 'grid', placeItems: 'center', color: ink, fontSize: '0.85em', lineHeight: '1' },
4802
+ '&[data-state="checked"]::after': { content: '"✔︎"', clipPath: 'none', rotate: '0deg' },
4803
+ '&[data-state="indeterminate"]::after': { content: '"−"', clipPath: 'none', rotate: '0deg', translate: 'none' },
4804
+ },
4805
+ });
4806
+
4328
4807
  export const treeView: RecipeInput = {
4329
4808
  component: 'tree-view',
4330
4809
  tokens: {
4331
4810
  '--tree-accent': 'var(--color-primary)',
4332
4811
  '--tree-text': 'var(--text-sm)',
4333
4812
  '--tree-on-accent': 'var(--color-primary-content)',
4813
+ // The node check box's pair — the tree's own accent, so a colour
4814
+ // variant retints the box with the row.
4815
+ '--tree-check': 'var(--tree-accent)',
4816
+ '--tree-on-check': 'var(--tree-on-accent)',
4334
4817
  },
4335
4818
  parts: {
4336
4819
  root: {
@@ -4379,6 +4862,58 @@ export const treeView: RecipeInput = {
4379
4862
  'reduced-motion': { base: { transition: 'none' } },
4380
4863
  },
4381
4864
  },
4865
+ // daisy's `.checkbox` at row scale: the accent outline, the recessed
4866
+ // depth shade, the fill on check — and daisy's own tick, the six
4867
+ // clip-path points rotated 45°, drawn on the box's `::after` since
4868
+ // the part is one span. On a selected row (the accent fill) the pair
4869
+ // swaps, so the box never vanishes into the row it sits on.
4870
+ 'node-checkbox': {
4871
+ base: {
4872
+ display: 'inline-block',
4873
+ position: 'relative',
4874
+ flexShrink: '0',
4875
+ boxSizing: 'border-box',
4876
+ width: '1.25em',
4877
+ height: '1.25em',
4878
+ border: 'var(--border) solid var(--tree-check)',
4879
+ borderRadius: 'var(--radius-selector)',
4880
+ boxShadow: '0 1px var(--depth-shade) inset',
4881
+ cursor: 'pointer',
4882
+ transition: 'background-color var(--duration-normal) var(--ease-standard)',
4883
+ },
4884
+ states: {
4885
+ checked: { backgroundColor: 'var(--tree-check)' },
4886
+ indeterminate: { backgroundColor: 'var(--tree-check)' },
4887
+ unchecked: {},
4888
+ disabled: { cursor: 'not-allowed' },
4889
+ },
4890
+ selectors: {
4891
+ '&::after': {
4892
+ content: '""',
4893
+ position: 'absolute',
4894
+ inset: '15%',
4895
+ opacity: '0',
4896
+ rotate: '45deg',
4897
+ backgroundColor: 'var(--tree-on-check)',
4898
+ clipPath: TICK_COLLAPSED,
4899
+ transition: 'clip-path var(--duration-slow) var(--ease-standard) var(--duration-instant), '
4900
+ + 'opacity var(--duration-instant) var(--ease-standard) var(--duration-instant), '
4901
+ + 'rotate var(--duration-slow) var(--ease-standard) var(--duration-instant), '
4902
+ + 'translate var(--duration-slow) var(--ease-standard) var(--duration-instant)',
4903
+ },
4904
+ '&[data-state="checked"]::after': { clipPath: TICK_DRAWN, opacity: '1' },
4905
+ '&[data-state="indeterminate"]::after': { clipPath: DASH_DRAWN, opacity: '1', translate: '0 -35%', rotate: '0deg' },
4906
+ '[data-scope="tree-view"][data-selected] &': {
4907
+ '--tree-check': 'var(--tree-on-accent)',
4908
+ '--tree-on-check': 'var(--tree-accent)',
4909
+ },
4910
+ },
4911
+ at: {
4912
+ 'reduced-motion': { base: { transition: 'none' }, selectors: { '&::after': { transition: 'none' } } },
4913
+ 'forced-colors': nodeTickFallback('CanvasText'),
4914
+ print: nodeTickFallback('var(--print-ink)'),
4915
+ },
4916
+ },
4382
4917
  // Indentation comes from this inline padding — depth is the DOM
4383
4918
  // nesting, no per-level rules needed.
4384
4919
  'branch-content': {
@@ -4663,6 +5198,36 @@ export const card: RecipeInput = {
4663
5198
  overflow: 'hidden',
4664
5199
  },
4665
5200
  },
5201
+ /**
5202
+ * Media (#302) — daisy's `card > figure`: a centred flex box, flush
5203
+ * to the card's edges, the image filling it.
5204
+ */
5205
+ media: {
5206
+ base: {
5207
+ display: 'flex',
5208
+ alignItems: 'center',
5209
+ justifyContent: 'center',
5210
+ margin: '0',
5211
+ inlineSize: '100%',
5212
+ objectFit: 'cover',
5213
+ },
5214
+ selectors: {
5215
+ // The media's own image, when the band wraps one (a div or a
5216
+ // figure): block-level, the band's full width, cropped rather
5217
+ // than stretched when the band is given a height.
5218
+ '& > :is(img, picture, video, svg, canvas)': {
5219
+ display: 'block',
5220
+ inlineSize: '100%',
5221
+ blockSize: '100%',
5222
+ objectFit: 'cover',
5223
+ },
5224
+ // The corners it shares with the card (#302): the root clips
5225
+ // too, but an asChild <img> is the band itself and rounds on
5226
+ // its own.
5227
+ '&:first-child': { borderStartStartRadius: 'var(--radius-box)', borderStartEndRadius: 'var(--radius-box)' },
5228
+ '&:last-child': { borderEndStartRadius: 'var(--radius-box)', borderEndEndRadius: 'var(--radius-box)' },
5229
+ },
5230
+ },
4666
5231
  header: {
4667
5232
  base: {
4668
5233
  display: 'flex',
@@ -4996,16 +5561,51 @@ export const divider: RecipeInput = {
4996
5561
  tokens: { '--divider-ink': 'var(--color-base-300)', '--divider-thickness': 'var(--border)' },
4997
5562
  parts: {
4998
5563
  root: {
4999
- base: { border: 'none', background: 'var(--divider-ink)', alignSelf: 'stretch' },
5564
+ // The rule is two flex segments (#298), drawn as borders: with no
5565
+ // label they meet and read as one line, and a label sits between
5566
+ // them. Borders rather than a background, so forced colours keep
5567
+ // the line instead of revaluing it to Canvas.
5568
+ base: {
5569
+ display: 'flex',
5570
+ alignItems: 'center',
5571
+ border: 'none',
5572
+ alignSelf: 'stretch',
5573
+ },
5000
5574
  selectors: {
5001
- '&[data-orientation="horizontal"]': {
5002
- inlineSize: '100%',
5003
- blockSize: 'var(--divider-thickness)',
5004
- },
5005
- '&[data-orientation="vertical"]': {
5006
- inlineSize: 'var(--divider-thickness)',
5007
- minBlockSize: '1em',
5575
+ '&::before': { content: '""', flex: '1 1 0', minInlineSize: '0' },
5576
+ '&::after': { content: '""', flex: '1 1 0', minInlineSize: '0' },
5577
+ '&[data-orientation="horizontal"]': { inlineSize: '100%' },
5578
+ '&[data-orientation="horizontal"]::before': { borderBlockStart: 'var(--divider-thickness) solid var(--divider-ink)' },
5579
+ '&[data-orientation="horizontal"]::after': { borderBlockStart: 'var(--divider-thickness) solid var(--divider-ink)' },
5580
+ '&[data-orientation="vertical"]': { flexDirection: 'column', minBlockSize: '1em' },
5581
+ '&[data-orientation="vertical"]::before': { borderInlineStart: 'var(--divider-thickness) solid var(--divider-ink)' },
5582
+ '&[data-orientation="vertical"]::after': { borderInlineStart: 'var(--divider-thickness) solid var(--divider-ink)' },
5583
+ // A placed label sits flush at its edge: the segment on
5584
+ // that side goes, and the other one takes the whole run.
5585
+ '&:has(> [data-scope="divider"][data-part="label"][data-placement="start"])::before': { display: 'none' },
5586
+ '&:has(> [data-scope="divider"][data-part="label"][data-placement="end"])::after': { display: 'none' },
5587
+ },
5588
+ },
5589
+ label: {
5590
+ base: {
5591
+ // daisy's `divider` text: the content colour, the rule
5592
+ // stopping a gap short of it on either side.
5593
+ flex: 'none',
5594
+ paddingInline: 'var(--space-md)',
5595
+ fontSize: 'var(--text-sm)',
5596
+ whiteSpace: 'nowrap',
5597
+ color: 'var(--color-base-content)',
5598
+ },
5599
+ selectors: {
5600
+ '[data-scope="divider"][data-part="root"][data-orientation="vertical"] > &': {
5601
+ paddingInline: '0',
5602
+ paddingBlock: 'var(--space-sm)',
5008
5603
  },
5604
+ // Flush means flush: no inset on the side the rule gave up.
5605
+ '&[data-placement="start"]': { paddingInlineStart: '0' },
5606
+ '&[data-placement="end"]': { paddingInlineEnd: '0' },
5607
+ '[data-scope="divider"][data-part="root"][data-orientation="vertical"] > &[data-placement="start"]': { paddingBlockStart: '0' },
5608
+ '[data-scope="divider"][data-part="root"][data-orientation="vertical"] > &[data-placement="end"]': { paddingBlockEnd: '0' },
5009
5609
  },
5010
5610
  },
5011
5611
  },
@@ -5021,6 +5621,25 @@ export const divider: RecipeInput = {
5021
5621
  xl: { root: { base: { '--divider-thickness': 'calc(var(--border) * 3)' } } },
5022
5622
  },
5023
5623
  },
5624
+ targets: {
5625
+ lynx: {
5626
+ parts: {
5627
+ // Lynx drops the ::before/::after segments the rule is drawn
5628
+ // with above, which left the lynx divider with no line and,
5629
+ // since only those rules read `--divider-thickness`, no size
5630
+ // ramp either (#375). There the root itself is the line, as
5631
+ // it was before the segments (#356): filled with the ink and
5632
+ // one thickness across.
5633
+ root: {
5634
+ base: { background: 'var(--divider-ink)' },
5635
+ selectors: {
5636
+ '&[data-orientation="horizontal"]': { blockSize: 'var(--divider-thickness)' },
5637
+ '&[data-orientation="vertical"]': { inlineSize: 'var(--divider-thickness)' },
5638
+ },
5639
+ },
5640
+ },
5641
+ },
5642
+ },
5024
5643
  };
5025
5644
 
5026
5645
  // ── Loading (#314) ────────────────────────────────────────────────────────
@@ -5556,6 +6175,10 @@ export const timeline: RecipeInput = {
5556
6175
  },
5557
6176
  },
5558
6177
  },
6178
+ // Title and description (#302): daisy's timeline-box leads with a
6179
+ // bold line; the detail under it in the card description's ink.
6180
+ title: { base: { display: 'block', margin: '0', fontWeight: 'var(--weight-semibold)', lineHeight: 'var(--leading-tight)' } },
6181
+ description: { base: { display: 'block', margin: '0', marginBlockStart: 'var(--space-2xs)', color: 'color-mix(in oklab, var(--color-base-content) 70%, transparent)' } },
5559
6182
  },
5560
6183
  variants: {
5561
6184
  color: Object.fromEntries(ROLES.map((c) => [c, { marker: { base: {
@@ -5762,6 +6385,96 @@ export const chat: RecipeInput = {
5762
6385
  },
5763
6386
  };
5764
6387
 
6388
+ /**
6389
+ * ChatLog — daisy's `bg-base-200 rounded-box` well that the chat bubbles
6390
+ * already sit in, scrolling; the jump trigger is a `btn btn-sm` pill in the
6391
+ * role's solid pair with daisy's `shadow-xs` and its 1px press sink, floated
6392
+ * over the rows by `position: sticky` at the box's foot. Its negative block
6393
+ * margin gives back the line it would take, so appearing moves no row.
6394
+ */
6395
+ export const chatLog: RecipeInput = {
6396
+ component: 'chat-log',
6397
+ tokens: {
6398
+ '--chat-log-fill': 'var(--color-primary)',
6399
+ '--chat-log-on-fill': 'var(--color-primary-content)',
6400
+ '--chat-log-jump-size': 'calc(var(--size-field) * 8)',
6401
+ },
6402
+ parts: {
6403
+ root: {
6404
+ base: {
6405
+ display: 'flex',
6406
+ flexDirection: 'column',
6407
+ overflowY: 'auto',
6408
+ overscrollBehaviorY: 'contain',
6409
+ borderRadius: 'var(--radius-box)',
6410
+ background: 'var(--color-base-100)',
6411
+ color: 'var(--color-base-content)',
6412
+ border: 'var(--border) solid var(--color-base-300)',
6413
+ padding: 'var(--space-sm) var(--space-md)',
6414
+ },
6415
+ // Drawn inside the box (negative offset): an outward ring would
6416
+ // be clipped by whatever scrolls around the transcript.
6417
+ states: { 'focus-visible': { outline: '2px solid var(--color-base-content)', outlineOffset: '-2px' } },
6418
+ },
6419
+ content: {
6420
+ base: {
6421
+ // `auto` gathers a short transcript at the foot; a long one
6422
+ // overflows and the margin resolves to nothing.
6423
+ marginBlockStart: 'auto',
6424
+ flex: 'none',
6425
+ display: 'grid',
6426
+ rowGap: 'var(--space-xs)',
6427
+ },
6428
+ },
6429
+ 'jump-trigger': {
6430
+ base: {
6431
+ position: 'sticky',
6432
+ insetBlockEnd: 'var(--space-sm)',
6433
+ alignSelf: 'center',
6434
+ flex: 'none',
6435
+ marginBlockStart: 'calc(var(--chat-log-jump-size) * -1)',
6436
+ blockSize: 'var(--chat-log-jump-size)',
6437
+ appearance: 'none',
6438
+ display: 'inline-flex',
6439
+ alignItems: 'center',
6440
+ gap: 'var(--space-2xs)',
6441
+ paddingInline: 'var(--space-md)',
6442
+ border: 'var(--border) solid transparent',
6443
+ borderRadius: '9999px',
6444
+ background: 'var(--chat-log-fill)',
6445
+ color: 'var(--chat-log-on-fill)',
6446
+ boxShadow: 'var(--shadow-xs)',
6447
+ fontFamily: 'inherit',
6448
+ fontSize: 'var(--text-xs)',
6449
+ fontWeight: 'var(--weight-semibold)',
6450
+ lineHeight: 'var(--leading-none)',
6451
+ cursor: 'pointer',
6452
+ transition: 'background var(--duration-fast) var(--ease-standard)',
6453
+ },
6454
+ states: {
6455
+ open: {},
6456
+ closed: {},
6457
+ hover: { background: 'color-mix(in oklab, var(--chat-log-fill) 90%, black)' },
6458
+ 'focus-visible': { outline: '2px solid var(--color-base-content)', outlineOffset: '2px' },
6459
+ },
6460
+ selectors: { '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)', boxShadow: 'none' } },
6461
+ },
6462
+ },
6463
+ variants: {
6464
+ color: Object.fromEntries(ROLES.map((c) => [c, { 'jump-trigger': { base: {
6465
+ '--chat-log-fill': `var(--color-${c})`,
6466
+ '--chat-log-on-fill': `var(--color-${c}-content)`,
6467
+ } } }])),
6468
+ size: {
6469
+ xs: { content: { base: { rowGap: 'var(--space-2xs)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 6)', fontSize: 'var(--text-xs)' } } },
6470
+ sm: { content: { base: { rowGap: 'var(--space-2xs)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 7)', fontSize: 'var(--text-xs)' } } },
6471
+ md: {},
6472
+ lg: { content: { base: { rowGap: 'var(--space-sm)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 9)', fontSize: 'var(--text-sm)' } } },
6473
+ xl: { content: { base: { rowGap: 'var(--space-md)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 10)', fontSize: 'var(--text-sm)' } } },
6474
+ },
6475
+ },
6476
+ };
6477
+
5765
6478
  /** daisy radial-progress: --value became --progress-percent; same ring. */
5766
6479
  export const radialProgress: RecipeInput = {
5767
6480
  component: 'radial-progress',
@@ -6152,6 +6865,8 @@ export const breadcrumbs: RecipeInput = {
6152
6865
  alignItems: 'center',
6153
6866
  gap: 'var(--space-xs)',
6154
6867
  },
6868
+ // `closed` is presence: the runtime hides the part (#295).
6869
+ states: { open: {}, closed: {} },
6155
6870
  },
6156
6871
  link: {
6157
6872
  base: {
@@ -6180,6 +6895,37 @@ export const breadcrumbs: RecipeInput = {
6180
6895
  userSelect: 'none',
6181
6896
  },
6182
6897
  },
6898
+ ellipsis: {
6899
+ base: {
6900
+ display: 'flex',
6901
+ alignItems: 'center',
6902
+ gap: 'var(--space-xs)',
6903
+ },
6904
+ // `closed` is presence: the runtime hides the part (#295).
6905
+ states: { open: {}, closed: {} },
6906
+ },
6907
+ 'ellipsis-trigger': {
6908
+ base: {
6909
+ appearance: 'none',
6910
+ background: 'transparent',
6911
+ border: '0',
6912
+ padding: '0 var(--space-2xs)',
6913
+ margin: '0',
6914
+ font: 'inherit',
6915
+ lineHeight: 'inherit',
6916
+ color: 'color-mix(in oklch, var(--color-base-content) 75%, transparent)',
6917
+ borderRadius: 'var(--radius-selector)',
6918
+ cursor: 'pointer',
6919
+ transition: 'color var(--duration-fast) var(--ease-standard)',
6920
+ },
6921
+ states: {
6922
+ hover: { color: 'var(--color-base-content)', textDecoration: 'underline' },
6923
+ 'focus-visible': {
6924
+ outline: '2px solid var(--color-primary)',
6925
+ outlineOffset: '2px',
6926
+ },
6927
+ },
6928
+ },
6183
6929
  },
6184
6930
  variants: {
6185
6931
  color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
@@ -6195,11 +6941,47 @@ export const breadcrumbs: RecipeInput = {
6195
6941
  },
6196
6942
  };
6197
6943
 
6944
+ /**
6945
+ * The four pagination triggers — prev/next and, with `withEdges`, the
6946
+ * first/last jumps (#294) — are one cell: the same box, the same
6947
+ * glyph-sized type, and the same flip under the rtl guard, since every
6948
+ * glyph (`‹ › « »`) is physical ink pointing at a reading edge.
6949
+ */
6950
+ const pageTrigger: PartStyles = {
6951
+ base: {
6952
+ display: 'inline-flex',
6953
+ alignItems: 'center',
6954
+ justifyContent: 'center',
6955
+ minInlineSize: 'var(--pg-size)',
6956
+ blockSize: 'var(--pg-size)',
6957
+ background: 'var(--color-base-200)',
6958
+ color: 'var(--color-base-content)',
6959
+ border: 'none',
6960
+ borderRadius: 'var(--radius-field)',
6961
+ fontSize: 'calc(var(--pg-font) * 1.2)',
6962
+ lineHeight: 'var(--leading-none)',
6963
+ appearance: 'none',
6964
+ // Link mode renders an <a> (#294): no UA underline.
6965
+ textDecoration: 'none',
6966
+ cursor: 'pointer',
6967
+ transition: 'background var(--duration-fast) var(--ease-standard)',
6968
+ },
6969
+ states: {
6970
+ hover: { background: 'var(--color-base-300)' },
6971
+ disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
6972
+ ...focusRing,
6973
+ },
6974
+ selectors: {
6975
+ '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)' },
6976
+ [`&${rtl}`]: { scale: '-1 1' },
6977
+ },
6978
+ };
6979
+
6198
6980
  /**
6199
6981
  * Pagination — daisy's join-of-buttons made of standalone btn cells: the
6200
6982
  * quiet base-200 fill, base-300 hover, the current page inverted into the
6201
6983
  * accent pair (primary by default). Pressed is the daisy 1px sink. The
6202
- * `‹`/`›` glyphs flip under the shared rtl guard.
6984
+ * `‹ › « »` glyphs flip under the shared rtl guard.
6203
6985
  */
6204
6986
  export const pagination: RecipeInput = {
6205
6987
  component: 'pagination',
@@ -6245,6 +7027,7 @@ export const pagination: RecipeInput = {
6245
7027
  fontWeight: 'var(--weight-semibold)',
6246
7028
  fontVariantNumeric: 'tabular-nums',
6247
7029
  appearance: 'none',
7030
+ textDecoration: 'none',
6248
7031
  cursor: 'pointer',
6249
7032
  transition: 'background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard)',
6250
7033
  },
@@ -6272,60 +7055,10 @@ export const pagination: RecipeInput = {
6272
7055
  userSelect: 'none',
6273
7056
  },
6274
7057
  },
6275
- 'prev-trigger': {
6276
- base: {
6277
- display: 'inline-flex',
6278
- alignItems: 'center',
6279
- justifyContent: 'center',
6280
- minInlineSize: 'var(--pg-size)',
6281
- blockSize: 'var(--pg-size)',
6282
- background: 'var(--color-base-200)',
6283
- color: 'var(--color-base-content)',
6284
- border: 'none',
6285
- borderRadius: 'var(--radius-field)',
6286
- fontSize: 'calc(var(--pg-font) * 1.2)',
6287
- lineHeight: 'var(--leading-none)',
6288
- appearance: 'none',
6289
- cursor: 'pointer',
6290
- transition: 'background var(--duration-fast) var(--ease-standard)',
6291
- },
6292
- states: {
6293
- hover: { background: 'var(--color-base-300)' },
6294
- disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
6295
- ...focusRing,
6296
- },
6297
- selectors: {
6298
- '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)' },
6299
- [`&${rtl}`]: { scale: '-1 1' },
6300
- },
6301
- },
6302
- 'next-trigger': {
6303
- base: {
6304
- display: 'inline-flex',
6305
- alignItems: 'center',
6306
- justifyContent: 'center',
6307
- minInlineSize: 'var(--pg-size)',
6308
- blockSize: 'var(--pg-size)',
6309
- background: 'var(--color-base-200)',
6310
- color: 'var(--color-base-content)',
6311
- border: 'none',
6312
- borderRadius: 'var(--radius-field)',
6313
- fontSize: 'calc(var(--pg-font) * 1.2)',
6314
- lineHeight: 'var(--leading-none)',
6315
- appearance: 'none',
6316
- cursor: 'pointer',
6317
- transition: 'background var(--duration-fast) var(--ease-standard)',
6318
- },
6319
- states: {
6320
- hover: { background: 'var(--color-base-300)' },
6321
- disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
6322
- ...focusRing,
6323
- },
6324
- selectors: {
6325
- '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)' },
6326
- [`&${rtl}`]: { scale: '-1 1' },
6327
- },
6328
- },
7058
+ 'first-trigger': pageTrigger,
7059
+ 'prev-trigger': pageTrigger,
7060
+ 'next-trigger': pageTrigger,
7061
+ 'last-trigger': pageTrigger,
6329
7062
  },
6330
7063
  variants: {
6331
7064
  color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
@@ -6366,6 +7099,22 @@ const stepInk = (accent: string, role: string): string =>
6366
7099
  `color-mix(in oklab, ${accent} `
6367
7100
  + `${Math.min(ROLE_INK_KEEP[role as keyof typeof roles] ?? 55, 70)}%, var(--color-base-content))`;
6368
7101
 
7102
+ /** A Steps root holding wizard parts (#296) — the panel or the triggers. */
7103
+ const STEPS_WIZARD = '&:has(> [data-scope="steps"]:is([data-part="content"], [data-part="prev-trigger"], [data-part="next-trigger"]))';
7104
+
7105
+ /** Steps' Back/Next (#296): the neutral daisy btn, with its 1px sink. */
7106
+ const stepsTrigger: PartStyles = {
7107
+ base: { ...btn, flex: '0 0 auto', alignSelf: 'flex-start' },
7108
+ states: {
7109
+ hover: { background: 'var(--color-base-300)' },
7110
+ disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
7111
+ ...focusRing,
7112
+ },
7113
+ selectors: {
7114
+ '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)' },
7115
+ },
7116
+ };
7117
+
6369
7118
  /**
6370
7119
  * Steps — daisy's steps translated to the richer rail: bold numbered discs
6371
7120
  * on base-200, the walked disc and line refilled with the accent pair
@@ -6392,6 +7141,10 @@ export const steps: RecipeInput = {
6392
7141
  },
6393
7142
  selectors: {
6394
7143
  '&[data-orientation="vertical"]': { flexDirection: 'column' },
7144
+ // A wizard (#296): the rail keeps its own line and the panel
7145
+ // and triggers wrap below it. Only then — a bare rail never
7146
+ // wraps, so its steps stay one row at any width.
7147
+ [STEPS_WIZARD]: { flexWrap: 'wrap', rowGap: 'var(--space-md)' },
6395
7148
  },
6396
7149
  },
6397
7150
  /**
@@ -6430,6 +7183,10 @@ export const steps: RecipeInput = {
6430
7183
  active: { color: 'var(--steps-accent-ink)', fontWeight: 'var(--weight-semibold)' },
6431
7184
  complete: { color: 'var(--color-base-content)' },
6432
7185
  inactive: { color: 'color-mix(in oklch, var(--color-base-content) 65%, transparent)' },
7186
+ // daisy's `step-error`, whatever the phase: after the phases,
7187
+ // so it wins at equal weight. `stepInk`, not `roleInk`: the
7188
+ // 85% error ink measured 3.79:1 on light's page as text.
7189
+ invalid: { color: stepInk('var(--color-error)', 'error') },
6433
7190
  disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
6434
7191
  ...focusRing,
6435
7192
  },
@@ -6461,6 +7218,7 @@ export const steps: RecipeInput = {
6461
7218
  // the disc (#112).
6462
7219
  complete: { background: 'color-mix(in oklab, var(--steps-accent) 20%, var(--color-base-100))', color: 'var(--steps-complete-ink)' },
6463
7220
  inactive: { background: 'var(--color-base-200)', color: 'var(--color-base-content)' },
7221
+ invalid: { background: 'var(--color-error)', color: 'var(--color-error-content)' },
6464
7222
  },
6465
7223
  },
6466
7224
  /**
@@ -6492,6 +7250,7 @@ export const steps: RecipeInput = {
6492
7250
  states: {
6493
7251
  complete: { background: 'var(--steps-accent)' },
6494
7252
  inactive: { background: 'var(--color-base-300)' },
7253
+ invalid: { background: 'var(--color-error)' },
6495
7254
  },
6496
7255
  },
6497
7256
  title: {
@@ -6506,6 +7265,25 @@ export const steps: RecipeInput = {
6506
7265
  fontWeight: 'var(--weight-normal)',
6507
7266
  },
6508
7267
  },
7268
+ /**
7269
+ * The active step's panel: a full-width line under the rail (the
7270
+ * wizard wrap above). Inactive panels are `hidden`, so they need no
7271
+ * rule.
7272
+ */
7273
+ content: {
7274
+ base: {
7275
+ flex: '1 0 100%',
7276
+ fontSize: 'var(--text-md)',
7277
+ color: 'var(--color-base-content)',
7278
+ },
7279
+ states: { active: {}, inactive: {} },
7280
+ },
7281
+ // Back/Next are daisy btns; Next takes the far end of their line.
7282
+ 'prev-trigger': stepsTrigger,
7283
+ 'next-trigger': {
7284
+ ...stepsTrigger,
7285
+ base: { ...stepsTrigger.base, marginInlineStart: 'auto' },
7286
+ },
6509
7287
  },
6510
7288
  variants: {
6511
7289
  // Keyed on the ITEM, not the root (#112): the item re-carries
@@ -7485,11 +8263,11 @@ export const diff: RecipeInput = {
7485
8263
  };
7486
8264
 
7487
8265
  export const recipes: RecipeInput[] = [
7488
- tabs, collapsible, switchRecipe, dialog, popover, tooltip, menu,
7489
- field, fieldset, checkbox, checkboxGroup, radioGroup, progress, slider, accordion, select, button, avatar, toast, combobox,
8266
+ tabs, collapsible, switchRecipe, dialog, popover, tooltip, hoverCard, menu, menubar,
8267
+ field, fieldset, checkbox, checkboxGroup, radioGroup, progress, slider, accordion, select, button, avatar, avatarGroup, toast, combobox,
7490
8268
  toggle, toggleGroup, numberInput, ratingGroup, treeView, input, textarea,
7491
8269
  card, alert, emptyState, badge, divider, skeleton, spinner,
7492
- kbd, status, indicator, stats, timeline, chat, radialProgress, join,
8270
+ kbd, status, indicator, stats, timeline, chat, chatLog, radialProgress, join,
7493
8271
  navbar, navList, breadcrumbs, pagination, steps, drawer,
7494
8272
  table,
7495
8273
  fileUpload,