@sigx/zero-daisyui 0.6.0 → 0.8.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 (56) hide show
  1. package/README.md +1 -2
  2. package/dist/audit.json +5618 -72
  3. package/dist/components.d.ts +37 -1
  4. package/dist/components.js +4 -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/fieldset.css +91 -0
  10. package/dist/css/components/hover-card.css +174 -0
  11. package/dist/css/components/menu.css +73 -0
  12. package/dist/css/components/menubar.css +75 -0
  13. package/dist/css/components/pagination.css +83 -0
  14. package/dist/css/components/popover.css +72 -0
  15. package/dist/css/components/progress.css +0 -4
  16. package/dist/css/components/steps.css +103 -0
  17. package/dist/css/components/table.css +70 -0
  18. package/dist/css/components/timeline.css +14 -0
  19. package/dist/css/components/toast.css +65 -0
  20. package/dist/css/components/tooltip.css +65 -0
  21. package/dist/css/components/tree-view.css +118 -0
  22. package/dist/css/index.css +1330 -42
  23. package/dist/lynx/components/accordion.css +7 -1
  24. package/dist/lynx/components/breadcrumbs.css +44 -0
  25. package/dist/lynx/components/button.css +121 -10
  26. package/dist/lynx/components/card.css +9 -0
  27. package/dist/lynx/components/chat-log.css +147 -0
  28. package/dist/lynx/components/dialog.css +28 -3
  29. package/dist/lynx/components/divider.css +8 -17
  30. package/dist/lynx/components/fieldset.css +219 -0
  31. package/dist/lynx/components/hover-card.css +211 -0
  32. package/dist/lynx/components/menu.css +7 -0
  33. package/dist/lynx/components/menubar.css +199 -0
  34. package/dist/lynx/components/pagination.css +107 -0
  35. package/dist/lynx/components/popover.css +44 -2
  36. package/dist/lynx/components/progress.css +0 -20
  37. package/dist/lynx/components/select.css +51 -3
  38. package/dist/lynx/components/slider.css +295 -12
  39. package/dist/lynx/components/steps.css +103 -0
  40. package/dist/lynx/components/switch.css +4 -1
  41. package/dist/lynx/components/table.css +52 -0
  42. package/dist/lynx/components/tabs.css +109 -94
  43. package/dist/lynx/components/timeline.css +90 -1
  44. package/dist/lynx/components/toast.css +111 -8
  45. package/dist/lynx/components/tree-view.css +28 -0
  46. package/dist/lynx/index.css +2081 -255
  47. package/dist/lynx/manifest.json +112 -4
  48. package/dist/manifest.json +110 -2
  49. package/dist/recipes.d.ts +29 -1
  50. package/dist/recipes.d.ts.map +1 -1
  51. package/dist/recipes.js +1225 -129
  52. package/dist/recipes.js.map +1 -1
  53. package/dist/register.d.ts +36 -0
  54. package/dist/report.json +1353 -188
  55. package/package.json +4 -4
  56. package/src/recipes.ts +1254 -130
package/dist/recipes.js CHANGED
@@ -1,4 +1,4 @@
1
- import { axisRoles, tableStackAt } from '@sigx/zero-kit/define';
1
+ import { axisRoles, popupArrow, popupArrowHost, tableStackAt } from '@sigx/zero-kit/define';
2
2
  import { roles, tokens } from './tokens.js';
3
3
  /**
4
4
  * Every role a consumer can pass as `color`, derived from the declaration
@@ -28,6 +28,36 @@ const focusRing = {
28
28
  outlineOffset: '2px',
29
29
  },
30
30
  };
31
+ /**
32
+ * The focus ring on lynx, for `targets.lynx` (signalxjs/lynx#1163): the same
33
+ * 2px ring 2px clear of the part, spelled as spread box-shadows.
34
+ *
35
+ * The web ring is `outline` + `outline-offset`, and neither survives on
36
+ * lynx: its `outline` ignores `border-radius` (a square box around a
37
+ * rounded button, a rectangle around the switch pill) and it has no
38
+ * `outline-offset` property at all, so the ring sat flush on the fill. A
39
+ * zero-blur shadow's spread follows the element's radius. Shadows stack
40
+ * rather than mask, so the offset gap cannot be transparent: the inner
41
+ * shadow paints it in `gap` — the surface the part sits on, which is what
42
+ * the web's gap shows through to. `outline: none` retires the shared ring
43
+ * in the same merged declaration block.
44
+ *
45
+ * `under` appends the part's own lynx shadows (a knob's depth relief), which
46
+ * the focus rule would otherwise replace. `inset` draws the ring inside the
47
+ * part, flush with its edge, for a part whose parent clips an outside ring
48
+ * away (signalxjs/lynx#1164, the accordion trigger inside its card).
49
+ */
50
+ const lynxFocusRing = (ink, { gap = 'var(--color-base-100)', under, inset = false } = {}) => ({
51
+ outline: 'none',
52
+ boxShadow: [
53
+ ...(inset ? [`inset 0 0 0 2px ${ink}`] : [`0 0 0 2px ${gap}`, `0 0 0 4px ${ink}`]),
54
+ ...(under ? [under] : []),
55
+ ].join(', '),
56
+ });
57
+ /** `focusRing`'s lynx spelling — the base-content ring, as a states entry. */
58
+ const lynxFocus = (opts) => ({
59
+ 'focus-visible': lynxFocusRing('var(--color-base-content)', opts),
60
+ });
31
61
  /**
32
62
  * How far each role survives in the button's INK — the colour a transparent or
33
63
  * tinted fill draws its label, border and focus ring with.
@@ -289,7 +319,9 @@ export const tabs = {
289
319
  },
290
320
  // daisy draws each flavor's active mark on the tab itself (the
291
321
  // underline, the lift, the pill) and none of them slides, so a
292
- // `Tabs.Indicator` renders nothing here (#283).
322
+ // `Tabs.Indicator` renders nothing here (#283). On lynx, which has
323
+ // no pseudo-elements, the `border` flavor's underline is drawn on
324
+ // this part instead (see `targets.lynx`).
293
325
  indicator: { base: { display: 'none' } },
294
326
  panel: {
295
327
  base: { fontSize: 'var(--text-md)' },
@@ -491,11 +523,17 @@ export const tabs = {
491
523
  targets: {
492
524
  lynx: {
493
525
  parts: {
494
- // The default active ink — base-content, exactly what the
495
- // shared `states.active.color` paints. One class, so any
526
+ // No hover on a touch platform: the held tab takes the ink
527
+ // daisy's hover gives it.
528
+ tab: { states: { pressed: { color: 'var(--color-base-content)' }, ...lynxFocus() } },
529
+ // The list is the indicator's containing block.
530
+ list: { base: { position: 'relative' } },
531
+ // The underline's ink (read by the `border` flavor below) —
532
+ // base-content by default, exactly what the shared
533
+ // `states.active.color` paints on the tab. One class, so any
496
534
  // color-axis definition below (host + two classes, baked per
497
535
  // theme) wins on specificity.
498
- tab: { base: { '--tab-active-ink': 'var(--color-base-content)' } },
536
+ indicator: { base: { '--tab-active-ink': 'var(--color-base-content)' } },
499
537
  },
500
538
  variants: {
501
539
  // `currentColor` never resolves on lynx (measured on device
@@ -505,18 +543,28 @@ export const tabs = {
505
543
  // theme colors through color-mix(), which the emitter bakes
506
544
  // once per theme — the same per-color/per-theme literal the
507
545
  // active `color:` rules already land as.
508
- color: Object.fromEntries(ROLES.map((c) => [c, { tab: { base: { '--tab-active-ink': roleInk(c) } } }])),
546
+ color: Object.fromEntries(ROLES.map((c) => [c, { indicator: { base: { '--tab-active-ink': roleInk(c) } } }])),
509
547
  variant: {
510
548
  // The underline is a ::before on the web, which lynx
511
- // drops — redrawn as a real bottom border on the tab
512
- // itself (transparent at rest so activation never moves
513
- // the row). The active ink rides `--tab-active-ink`
514
- // above — a plain var() chain, device-proven — instead
515
- // of `currentColor`, which lynx never resolves.
549
+ // drops. It is redrawn on the `indicator` part (zero#324),
550
+ // which lynx-zero renders in every list and positions
551
+ // over the active tab from measured geometry (the web's
552
+ // `--tabs-indicator-*` properties are web-only). The part
553
+ // IS the tab's box, so daisy's bar is its bottom edge:
554
+ // 3px, 80% wide and centred, like `left: 10%; width: 80%`
555
+ // on the web. The ink rides `--tab-active-ink` — a plain
556
+ // var() chain, device-proven — instead of `currentColor`,
557
+ // which lynx never resolves.
516
558
  border: {
517
- tab: {
518
- base: { borderBottom: '3px solid transparent' },
519
- states: { active: { borderBottomColor: 'var(--tab-active-ink)' } },
559
+ indicator: {
560
+ base: {
561
+ display: 'flex',
562
+ boxSizing: 'border-box',
563
+ borderBottomWidth: '3px',
564
+ borderBottomStyle: 'solid',
565
+ borderBottomColor: 'var(--tab-active-ink)',
566
+ transform: 'scaleX(0.8)',
567
+ },
520
568
  },
521
569
  },
522
570
  // `min()` does not exist on lynx (signalxjs/lynx#1066) and
@@ -528,6 +576,14 @@ export const tabs = {
528
576
  base: { '--tab-radius-limit': 'var(--radius-field)' },
529
577
  },
530
578
  },
579
+ // The box flavor's tabs sit on the list's base-200
580
+ // surface, so the ring's gap is that surface. Stated on
581
+ // the variant (one class more than the part rule) so it
582
+ // also outranks the active pill's `--shadow-sm`, which
583
+ // is a box-shadow too.
584
+ box: {
585
+ tab: { states: lynxFocus({ gap: 'var(--color-base-200)' }) },
586
+ },
531
587
  },
532
588
  },
533
589
  },
@@ -864,6 +920,10 @@ export const switchRecipe = {
864
920
  // No currentColor on this target — the ink is named
865
921
  // directly, and `checked` moves border + fill below.
866
922
  border: 'var(--border) solid var(--switch-ink)',
923
+ // daisy's 1px inset depth line along the track's top
924
+ // edge — `currentColor` at `--depth` × 10% on the
925
+ // web, the theme's own depth shade here.
926
+ boxShadow: '0 1px var(--depth-shade) inset',
867
927
  userSelect: 'none',
868
928
  transition: 'border-color var(--duration-slow) var(--ease-standard), '
869
929
  + 'background-color var(--duration-normal) var(--ease-standard)',
@@ -873,6 +933,9 @@ export const switchRecipe = {
873
933
  borderColor: 'var(--switch-accent)',
874
934
  backgroundColor: 'var(--color-base-100)',
875
935
  },
936
+ // The pill ring (signalxjs/lynx#1163), keeping the
937
+ // track's inset depth line under it.
938
+ 'focus-visible': lynxFocusRing('var(--switch-accent)', { under: '0 1px var(--depth-shade) inset' }),
876
939
  },
877
940
  },
878
941
  thumb: {
@@ -884,6 +947,12 @@ export const switchRecipe = {
884
947
  height: 'calc(var(--switch-size) - (var(--border) + var(--switch-p)) * 2)',
885
948
  borderRadius: 'var(--radius-selector)',
886
949
  backgroundColor: 'var(--switch-ink)',
950
+ // daisy's knob relief (`.toggle:before`): a shade
951
+ // along the bottom edge, a sheen across the top, and
952
+ // a 1px drop — the web's `currentColor` drop spelled
953
+ // as the theme's depth shade. The noise layer has no
954
+ // lynx spelling and stays web-only.
955
+ boxShadow: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset, 0 1px var(--depth-shade)',
887
956
  transition: 'transform var(--duration-normal) var(--ease-standard), '
888
957
  + 'background-color var(--duration-instant) var(--ease-standard)',
889
958
  },
@@ -983,6 +1052,36 @@ const lynxBtnPad = (mult) => ({
983
1052
  paddingLeft: `calc(var(--size-field) * ${mult})`,
984
1053
  paddingRight: `calc(var(--size-field) * ${mult})`,
985
1054
  });
1055
+ /**
1056
+ * A four-side property spelled as its physical longhands, for `targets.lynx`
1057
+ * sections that must not ship a `var()`-bearing shorthand.
1058
+ *
1059
+ * Lynx keeps a declaration holding `var()` under its own property id,
1060
+ * unexpanded, and expands it only after the cascade has merged every rule
1061
+ * into one insertion-ordered style map — in that map's order, writing over
1062
+ * the longhands already there (`CSSVariableHandler::HandleCSSVariables`). A
1063
+ * static shorthand, by contrast, is expanded into its longhands at parse
1064
+ * time. So a var-bearing `border-color` and a static `border-top-color` never
1065
+ * meet in the cascade at all: whichever key the map saw later wins, whatever
1066
+ * the specificity (signalxjs/lynx#1161, #1162). Longhands share one key per
1067
+ * side, and the cascade decides again.
1068
+ */
1069
+ const lynxSides = (prop, value, sides = ['Top', 'Right', 'Bottom', 'Left']) => Object.fromEntries(sides.map((side) => [prop.replace('{}', side), value]));
1070
+ /**
1071
+ * daisy's `.btn` is content-sized and never squeezed: `display: inline-flex`
1072
+ * with `flex-shrink: 0; flex-wrap: nowrap`, so a narrow row makes the button
1073
+ * overflow, never break its label. A lynx view has no inline formatting
1074
+ * context: it stretches to a linear or column parent, and a squeezed label
1075
+ * `<text>` then wraps per character ("Bt / n", signalxjs/lynx#1165).
1076
+ * `width: max-content` is the intrinsic keyword starlight implements (an
1077
+ * intrinsic width is also exempt from the cross-axis stretch), so the box
1078
+ * sizes to its content as the web's does; the other two are daisy's own.
1079
+ */
1080
+ const lynxBtnFit = {
1081
+ width: 'max-content',
1082
+ flexShrink: '0',
1083
+ flexWrap: 'nowrap',
1084
+ };
986
1085
  /** The btn size ramp's physical inline padding, for `targets.lynx.variants`. */
987
1086
  const lynxBtnSizes = (part) => ({
988
1087
  xs: { [part]: { base: lynxBtnPad(2) } },
@@ -990,6 +1089,21 @@ const lynxBtnSizes = (part) => ({
990
1089
  lg: { [part]: { base: lynxBtnPad(5) } },
991
1090
  xl: { [part]: { base: lynxBtnPad(6) } },
992
1091
  });
1092
+ /**
1093
+ * The press rendering of the overlay btns (dialog, popover), for their
1094
+ * `targets.lynx` sections. The web answers a pointer with `:hover`, which
1095
+ * the lynx emitter drops (a touch platform), and these parts' `pressed`
1096
+ * flag — stamped by the lynx runtime on every touch — had no rule, so a
1097
+ * tap showed nothing (signalxjs/lynx#1146). daisy's `:hover` wash plus the
1098
+ * btn's 1px sink, the same pair `button` paints for its own `pressed`. A
1099
+ * colour variant restates the background after the states at equal
1100
+ * specificity, so a coloured trigger keeps its fill and sinks.
1101
+ */
1102
+ const lynxBtnPressed = {
1103
+ background: 'var(--color-base-300)',
1104
+ transform: 'translateY(1px)',
1105
+ boxShadow: 'none',
1106
+ };
993
1107
  export const dialog = {
994
1108
  component: 'dialog',
995
1109
  // Public to a design system derived from this one (#73).
@@ -1081,14 +1195,17 @@ export const dialog = {
1081
1195
  // reach and the trigger is the whole story here.
1082
1196
  variants: { color: btnColors(), size: btnSizes },
1083
1197
  // The btn paddings and the footer's push-down, restated physically —
1084
- // see `lynxBtnPad` for the #1084 verdict this answers.
1198
+ // see `lynxBtnPad` for the #1084 verdict this answers — and the btns'
1199
+ // press rendering (`lynxBtnPressed`). The footer states its row: on lynx
1200
+ // a row container says so next to `display: flex`.
1085
1201
  targets: {
1086
1202
  lynx: {
1087
1203
  parts: {
1088
- trigger: { base: lynxBtnPad(4) },
1089
- close: { base: lynxBtnPad(4) },
1090
- cancel: { base: lynxBtnPad(4) },
1091
- footer: { base: { marginTop: 'var(--space-2xl)' } },
1204
+ // Content-sized like daisy's btn, never broken mid-word (signalxjs/lynx#1165).
1205
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1206
+ close: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1207
+ cancel: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1208
+ footer: { base: { marginTop: 'var(--space-2xl)', flexDirection: 'row' } },
1092
1209
  },
1093
1210
  variants: { size: lynxBtnSizes('trigger') },
1094
1211
  },
@@ -1102,6 +1219,21 @@ const floatingPanel = {
1102
1219
  borderRadius: 'var(--radius-box)',
1103
1220
  boxShadow: 'var(--shadow-lg)',
1104
1221
  };
1222
+ /**
1223
+ * The arrow a floating surface grows toward its anchor (#279), in the
1224
+ * surface's own paint: the dropdown's base-100 and border for popover and
1225
+ * menu, the tooltip's borderless neutral for the tooltip — daisy's
1226
+ * `tooltip` draws exactly that tail under its bubble. Web-only (placed by
1227
+ * the runtime's `--arrow-x`/`--arrow-y`), so it rides `targets.web` with
1228
+ * the popup's permission to let it out.
1229
+ */
1230
+ const floatingArrow = (scope, paint, size = '0.625rem') => ({
1231
+ parts: {
1232
+ popup: { selectors: popupArrowHost(scope) },
1233
+ arrow: popupArrow(scope, { size, paint }),
1234
+ },
1235
+ });
1236
+ const panelArrowPaint = { background: 'var(--color-base-100)', border: 'var(--border) solid var(--color-base-300)' };
1105
1237
  /**
1106
1238
  * daisy's dropdown menu for a listbox, sized by the geometry the
1107
1239
  * anchored-position strategy publishes (#278): at least the width of the
@@ -1138,6 +1270,14 @@ export const popover = {
1138
1270
  title: {
1139
1271
  base: { margin: '0 0 var(--space-md)', fontSize: 'var(--text-md)', fontWeight: 'var(--weight-bold)' },
1140
1272
  },
1273
+ // The modal's muted body line, at the dropdown's tighter rhythm.
1274
+ description: {
1275
+ base: {
1276
+ margin: '0 0 var(--space-lg)',
1277
+ fontSize: 'var(--text-sm)',
1278
+ color: 'color-mix(in oklab, var(--color-base-content) 75%, transparent)',
1279
+ },
1280
+ },
1141
1281
  close: {
1142
1282
  base: { ...btn, height: 'calc(var(--size-field) * 8)', paddingInline: 'calc(var(--size-field) * 3)', fontSize: 'var(--text-xs)' },
1143
1283
  states: {
@@ -1149,12 +1289,15 @@ export const popover = {
1149
1289
  },
1150
1290
  // Trigger-carried axes — same wiring as dialog, same reason.
1151
1291
  variants: { color: btnColors(), size: btnSizes },
1152
- // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
1292
+ // The btn paddings, restated physically — see `lynxBtnPad` (#1084) —
1293
+ // and the btns' press rendering (`lynxBtnPressed`).
1153
1294
  targets: {
1295
+ web: floatingArrow('popover', panelArrowPaint),
1154
1296
  lynx: {
1155
1297
  parts: {
1156
- trigger: { base: lynxBtnPad(4) },
1157
- close: { base: lynxBtnPad(3) },
1298
+ // Content-sized like daisy's btn, never broken mid-word (signalxjs/lynx#1165).
1299
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1300
+ close: { base: lynxBtnPad(3), states: { pressed: lynxBtnPressed, ...lynxFocus() } },
1158
1301
  },
1159
1302
  variants: { size: lynxBtnSizes('trigger') },
1160
1303
  },
@@ -1196,12 +1339,63 @@ export const tooltip = {
1196
1339
  variants: { color: btnColors(), size: btnSizes },
1197
1340
  // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
1198
1341
  targets: {
1342
+ web: floatingArrow('tooltip', { background: 'var(--color-neutral)' }, '0.5rem'),
1199
1343
  lynx: {
1200
1344
  parts: { trigger: { base: lynxBtnPad(4) } },
1201
1345
  variants: { size: lynxBtnSizes('trigger') },
1202
1346
  },
1203
1347
  },
1204
1348
  };
1349
+ /**
1350
+ * Hover card — daisy's `link` opening a `dropdown-content` card. The trigger
1351
+ * is running text, not a btn: underlined in the ink it sits in (daisy's bare
1352
+ * `link`), and a colour is `link-<role>` in the role's readable ink
1353
+ * (`roleInk`). The underline thickens on hover and while the card is up,
1354
+ * rather than the ink dimming — the `btn-link` reasoning. The card is the
1355
+ * floating panel popover and menu wear.
1356
+ */
1357
+ export const hoverCard = {
1358
+ component: 'hover-card',
1359
+ parts: {
1360
+ trigger: {
1361
+ base: {
1362
+ color: 'inherit',
1363
+ textDecorationLine: 'underline',
1364
+ textUnderlineOffset: '2px',
1365
+ cursor: 'pointer',
1366
+ borderRadius: 'var(--radius-selector)',
1367
+ },
1368
+ states: {
1369
+ hover: { textDecorationThickness: '2px' },
1370
+ open: { textDecorationThickness: '2px' },
1371
+ closed: {},
1372
+ ...focusRing,
1373
+ },
1374
+ },
1375
+ popup: withPresence(popupPresence('translateY(-4px)'), {
1376
+ base: {
1377
+ ...floatingPanel,
1378
+ padding: 'var(--space-xl)',
1379
+ maxWidth: '20rem',
1380
+ fontSize: 'var(--text-sm)',
1381
+ },
1382
+ states: { open: {}, closed: {} },
1383
+ }),
1384
+ },
1385
+ variants: {
1386
+ color: Object.fromEntries(ROLES.map((c) => [c, { trigger: { base: { color: roleInk(c) } } }])),
1387
+ size: {
1388
+ xs: { trigger: { base: { fontSize: 'var(--text-xs)' } } },
1389
+ sm: { trigger: { base: { fontSize: 'var(--text-sm)' } } },
1390
+ md: {},
1391
+ lg: { trigger: { base: { fontSize: 'var(--text-lg)' } } },
1392
+ xl: { trigger: { base: { fontSize: 'var(--text-xl)' } } },
1393
+ },
1394
+ },
1395
+ // Web-only: the popover top layer and the runtime's arrow geometry have
1396
+ // no lynx counterpart.
1397
+ targets: { web: floatingArrow('hover-card', panelArrowPaint) },
1398
+ };
1205
1399
  // daisy "menu in a dropdown" look.
1206
1400
  export const menu = {
1207
1401
  component: 'menu',
@@ -1335,6 +1529,17 @@ export const menu = {
1335
1529
  opacity: '0.6',
1336
1530
  },
1337
1531
  },
1532
+ // daisy's menu hint: the group label's faded ink, pushed to the
1533
+ // row's reading end — faded less, since 0.6 leaves nord's paper
1534
+ // under the 4.5:1 a line read by eye owes.
1535
+ shortcut: {
1536
+ base: {
1537
+ marginInlineStart: 'auto',
1538
+ paddingInlineStart: 'var(--space-lg)',
1539
+ fontSize: 'var(--text-xs)',
1540
+ opacity: '0.75',
1541
+ },
1542
+ },
1338
1543
  separator: {
1339
1544
  base: {
1340
1545
  height: 'var(--border)',
@@ -1348,12 +1553,55 @@ export const menu = {
1348
1553
  variants: { color: btnColors(), size: btnSizes },
1349
1554
  // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
1350
1555
  targets: {
1556
+ web: floatingArrow('menu', panelArrowPaint),
1351
1557
  lynx: {
1352
- parts: { trigger: { base: lynxBtnPad(4) } },
1558
+ parts: {
1559
+ trigger: { base: lynxBtnPad(4) },
1560
+ // The shortcut's reading-end push, restated physically (#1084).
1561
+ shortcut: { base: { marginLeft: 'auto', paddingLeft: 'var(--space-lg)' } },
1562
+ },
1353
1563
  variants: { size: lynxBtnSizes('trigger') },
1354
1564
  },
1355
1565
  },
1356
1566
  };
1567
+ // The menubar row: daisy's `menu-horizontal` strip — a base-200 box the
1568
+ // menu triggers sit in. The triggers are ordinary `menu` triggers.
1569
+ export const menubar = {
1570
+ component: 'menubar',
1571
+ tokens: { '--menubar-surface': 'var(--color-base-200)' },
1572
+ parts: {
1573
+ root: {
1574
+ base: {
1575
+ display: 'flex',
1576
+ alignItems: 'center',
1577
+ gap: 'var(--space-xs)',
1578
+ padding: 'var(--space-xs)',
1579
+ width: 'max-content',
1580
+ maxWidth: '100%',
1581
+ flexWrap: 'wrap',
1582
+ background: 'var(--menubar-surface)',
1583
+ borderRadius: 'var(--radius-box)',
1584
+ },
1585
+ // The triggers fade themselves — see basic.
1586
+ states: { disabled: {} },
1587
+ selectors: {
1588
+ '&[data-orientation="vertical"]': { flexDirection: 'column', alignItems: 'stretch' },
1589
+ },
1590
+ },
1591
+ },
1592
+ variants: {
1593
+ color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
1594
+ '--menubar-surface': `color-mix(in oklch, var(--color-${c}) 12%, var(--color-base-200))`,
1595
+ } } }])),
1596
+ size: {
1597
+ xs: { root: { base: { gap: '0', padding: '0' } } },
1598
+ sm: { root: { base: { padding: 'var(--space-2xs)' } } },
1599
+ md: {},
1600
+ lg: { root: { base: { gap: 'var(--space-sm)', padding: 'var(--space-sm)' } } },
1601
+ xl: { root: { base: { gap: 'var(--space-md)', padding: 'var(--space-sm)' } } },
1602
+ },
1603
+ },
1604
+ };
1357
1605
  export const field = {
1358
1606
  component: 'field',
1359
1607
  // The label's accent ink — base-content by default, so the un-attributed
@@ -1403,6 +1651,60 @@ export const field = {
1403
1651
  },
1404
1652
  },
1405
1653
  };
1654
+ /**
1655
+ * Fieldset (#285), daisyUI 5's `.fieldset` / `.fieldset-legend`: a
1656
+ * one-column grid with a tight row gap and a little block padding, no frame
1657
+ * of its own (daisy draws one only when the app adds `border`), and a
1658
+ * semibold legend with block padding standing in for its margin. The UA
1659
+ * `min-inline-size: min-content` is undone so a wide control cannot push the
1660
+ * group past its column. Colour accents the legend through `roleInk`, the
1661
+ * Field way.
1662
+ */
1663
+ export const fieldset = {
1664
+ component: 'fieldset',
1665
+ tokens: { '--fieldset-accent': 'var(--color-base-content)' },
1666
+ parts: {
1667
+ root: {
1668
+ base: {
1669
+ display: 'grid',
1670
+ gridTemplateColumns: '1fr',
1671
+ gridAutoRows: 'max-content',
1672
+ gap: 'var(--space-sm)',
1673
+ minInlineSize: '0',
1674
+ margin: '0',
1675
+ paddingBlock: 'var(--space-xs)',
1676
+ paddingInline: '0',
1677
+ border: 'none',
1678
+ },
1679
+ },
1680
+ legend: {
1681
+ base: {
1682
+ paddingBlock: 'var(--space-sm)',
1683
+ paddingInline: '0',
1684
+ fontSize: 'var(--text-sm)',
1685
+ fontWeight: 'var(--weight-semibold)',
1686
+ color: 'var(--fieldset-accent)',
1687
+ },
1688
+ states: {
1689
+ disabled: { opacity: 'var(--disabled-opacity)' },
1690
+ invalid: { color: roleInk('error') },
1691
+ },
1692
+ },
1693
+ },
1694
+ variants: {
1695
+ color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
1696
+ '--fieldset-accent': roleInk(c),
1697
+ } } }])),
1698
+ size: {
1699
+ xs: { legend: { base: { fontSize: 'var(--text-xs)' } } },
1700
+ sm: { legend: { base: { fontSize: 'var(--text-xs)' } } },
1701
+ // `md` is the un-attributed render.
1702
+ md: {},
1703
+ lg: { legend: { base: { fontSize: 'var(--text-md)' } } },
1704
+ xl: { legend: { base: { fontSize: 'var(--text-lg)' } } },
1705
+ },
1706
+ },
1707
+ };
1406
1708
  /**
1407
1709
  * daisy's tick, drawn by a clip-path that GROWS.
1408
1710
  *
@@ -1900,9 +2202,13 @@ export const progress = {
1900
2202
  transition: 'width var(--duration-slow) var(--ease-standard)',
1901
2203
  },
1902
2204
  states: {
1903
- // `complete` is a semantic state, not an accent: it stays
1904
- // success regardless of the colour variant, on purpose.
1905
- complete: { background: progressFill('success', 55) },
2205
+ // `complete` keeps the colour axis. daisyUI 5 has no complete
2206
+ // tint — a full `progress-primary` stays primary — and a
2207
+ // `color="secondary"` bar that turns green at 100% reads as a
2208
+ // bug, not as meaning (signalxjs/lynx#1144). A caller who wants
2209
+ // "done" to look like success says so: `color="success"`.
2210
+ // Declared equal to `loading` in `sameAs` below.
2211
+ complete: {},
1906
2212
  loading: {},
1907
2213
  indeterminate: { width: '40%', animation: 'zero-daisy-indeterminate 1.2s ease-in-out infinite' },
1908
2214
  },
@@ -1938,6 +2244,8 @@ export const progress = {
1938
2244
  // so the manifest states the default for runtimes with no `:not()`
1939
2245
  // fallback (the lynx target; signalxjs/lynx#1070).
1940
2246
  defaultVariants: { color: 'primary', size: 'md' },
2247
+ // A finished bar is a full bar of the same ink (see `complete` above).
2248
+ sameAs: { root: { complete: 'loading' }, range: { complete: 'loading' } },
1941
2249
  keyframes: {
1942
2250
  // Logical, so the sweep runs the way the bar fills — `margin-inline-start`
1943
2251
  // is animatable and direction-aware on its own. The determinate `width`,
@@ -1958,6 +2266,8 @@ export const progress = {
1958
2266
  },
1959
2267
  },
1960
2268
  };
2269
+ /** The slider's deepened fill — the web's `--slider-fill` mix, for a given accent. */
2270
+ const sliderFill = (accent) => `color-mix(in oklab, ${accent} 90%, var(--color-base-content))`;
1961
2271
  export const slider = {
1962
2272
  component: 'slider',
1963
2273
  // daisy's `.range` metrics: the thumb is `--range-thumb-size`
@@ -2266,29 +2576,100 @@ export const slider = {
2266
2576
  },
2267
2577
  },
2268
2578
  lynx: {
2579
+ // The web's deepened fill, theme-baked: `color-mix()` over theme
2580
+ // colours cannot resolve on device, but the emitter restates a
2581
+ // colour function per theme as literals (as `progress` does for
2582
+ // its accent), so each colour step gets the same 90/10 mix the
2583
+ // web track and knob ring wear. Bound on the root like
2584
+ // `--slider-accent`, rebound by `color` below and by `invalid`
2585
+ // on the control (which wraps the rail on lynx).
2586
+ tokens: { '--slider-fill': sliderFill('var(--color-primary)') },
2587
+ variants: {
2588
+ color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
2589
+ '--slider-fill': sliderFill(`var(--color-${c})`),
2590
+ } } }])),
2591
+ },
2269
2592
  parts: {
2593
+ // Vertical (lynx#1144): the lynx runtime positions the moving
2594
+ // parts itself — `top` percentages, the knob centred by
2595
+ // `left: 50%` plus the base's `translateY(-50%)` and
2596
+ // negative `margin-left` — so the class rules only turn the
2597
+ // channel upright: the control is a thumb-wide column of
2598
+ // daisy's rail length, the track fills it.
2599
+ root: {
2600
+ selectors: {
2601
+ '&[data-orientation="vertical"]': { width: 'auto', alignItems: 'center' },
2602
+ },
2603
+ },
2604
+ control: {
2605
+ states: {
2606
+ invalid: { '--slider-fill': sliderFill('var(--color-error)') },
2607
+ // The control's ring is the NATIVE input's on the web;
2608
+ // lynx always renders the composed parts, where the
2609
+ // thumb is what takes focus and wears the ring below
2610
+ // (signalxjs/lynx#1163 — both at once read as two
2611
+ // nested boxes).
2612
+ 'focus-visible': { outline: 'none' },
2613
+ },
2614
+ selectors: {
2615
+ '&[data-orientation="vertical"]': {
2616
+ display: 'flex',
2617
+ flexDirection: 'row',
2618
+ justifyContent: 'center',
2619
+ width: 'var(--slider-thumb-size)',
2620
+ height: 'calc(var(--size-selector) * 40)',
2621
+ },
2622
+ },
2623
+ },
2270
2624
  track: {
2271
2625
  // The web track's `margin-block` breathing room,
2272
2626
  // restated physically: logical spellings resolve on iOS
2273
2627
  // but not on Android (measured, signalxjs/lynx#1084), so
2274
2628
  // the emitter refuses them. Physical is the lynx
2275
2629
  // target's norm — no RTL flow there.
2630
+ //
2631
+ // daisy's channel radius is the selector radius (the
2632
+ // native control's runnable-track), not a pill.
2276
2633
  base: {
2277
2634
  marginTop: 'calc((var(--slider-thumb-size) - var(--slider-track-size)) / 2)',
2278
2635
  marginBottom: 'calc((var(--slider-thumb-size) - var(--slider-track-size)) / 2)',
2636
+ borderRadius: 'var(--radius-selector)',
2637
+ },
2638
+ selectors: {
2639
+ '&[data-orientation="vertical"]': {
2640
+ width: 'var(--slider-track-size)',
2641
+ height: '100%',
2642
+ marginTop: '0',
2643
+ marginBottom: '0',
2644
+ },
2279
2645
  },
2280
2646
  },
2281
2647
  mark: {
2282
- // Same #1084 verdict: the tick labels hang below the
2283
- // track off a physical padding.
2284
- base: { paddingTop: 'calc(var(--slider-track-size) + var(--space-2xs))' },
2648
+ // The web draws the tick with `::before`, which lynx has
2649
+ // no projection for — the mark was an empty, invisible
2650
+ // box. Here the mark IS the tick: a 2px rule across the
2651
+ // channel, centred on its runtime-set `left` percent.
2652
+ // (Lynx marks carry no label text.)
2653
+ base: {
2654
+ top: '0',
2655
+ width: '2px',
2656
+ height: 'var(--slider-track-size)',
2657
+ marginLeft: '-1px',
2658
+ paddingTop: '0',
2659
+ backgroundColor: 'var(--color-base-content)',
2660
+ },
2661
+ selectors: {
2662
+ '&[data-orientation="vertical"]': {
2663
+ left: '0',
2664
+ width: 'var(--slider-track-size)',
2665
+ height: '2px',
2666
+ marginLeft: '0',
2667
+ marginTop: '-1px',
2668
+ },
2669
+ },
2285
2670
  },
2286
2671
  range: {
2287
- // The web fill deepens the accent 90/10 toward
2288
- // base-content (`progressFill`'s recipe); the plain
2289
- // accent — a var() chain, proven working on device — is
2290
- // the closest lynx-expressible equivalent.
2291
- base: { background: 'var(--slider-accent)' },
2672
+ base: { background: 'var(--slider-fill)', borderRadius: 'var(--radius-selector)' },
2292
2673
  },
2293
2674
  thumb: {
2294
2675
  // daisy's real range thumb, ported from daisyUI 5
@@ -2299,13 +2680,16 @@ export const slider = {
2299
2680
  // `--range-thumb` defaults to `var(--color-base-100)`,
2300
2681
  // `--range-p` is daisy's literal `.25rem` (deliberately
2301
2682
  // not the theme's size token), and the border ink is the
2302
- // accent (currentColor on a daisy `.range-*` IS the
2303
- // accent role) — because on lynx a named value is a
2304
- // var-chain fewer to resolve. daisy paints the progress
2305
- // fill itself as a 100cqw box-shadow thrown off this
2306
- // thumb — a native-input trick with no lynx equivalent;
2307
- // the composed `.zx-slider__range` box above IS that
2308
- // fill here, so the knob only needs its own two paints.
2683
+ // deepened fill the web's native thumb ring reads. daisy
2684
+ // paints the progress fill itself as a 100cqw box-shadow
2685
+ // thrown off this thumb — a native-input trick with no
2686
+ // lynx equivalent; the composed `.zx-slider__range` box
2687
+ // above IS that fill here, so the knob only needs its own
2688
+ // paints. Its radius is daisy's formula minus the min()
2689
+ // clamp lynx cannot evaluate — exact for every shipped
2690
+ // theme (all set `--radius-selector` ≥ .25rem / 3) — and
2691
+ // it wears daisy's depth shading (`currentColor`'s 10%
2692
+ // outer shadow spelled as the theme's shade).
2309
2693
  //
2310
2694
  // The centering geometry is restated physically beside
2311
2695
  // the paint: `inset-block-start`/`translate`/
@@ -2316,11 +2700,20 @@ export const slider = {
2316
2700
  // proven on both platforms.
2317
2701
  base: {
2318
2702
  background: 'var(--color-base-100)',
2319
- border: '0.25rem solid var(--slider-accent)',
2703
+ border: '0.25rem solid var(--slider-fill)',
2704
+ borderRadius: 'calc(var(--radius-selector) + 0.25rem)',
2705
+ boxShadow: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset, 0 1px var(--depth-shade)',
2320
2706
  top: '50%',
2321
2707
  transform: 'translateY(-50%)',
2322
2708
  marginLeft: 'calc(var(--slider-thumb-size) / -2)',
2323
2709
  },
2710
+ // The round ring (signalxjs/lynx#1163), over the knob's
2711
+ // depth relief.
2712
+ states: {
2713
+ 'focus-visible': lynxFocusRing('var(--slider-accent)', {
2714
+ under: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset, 0 1px var(--depth-shade)',
2715
+ }),
2716
+ },
2324
2717
  },
2325
2718
  },
2326
2719
  },
@@ -2328,7 +2721,31 @@ export const slider = {
2328
2721
  };
2329
2722
  export const accordion = {
2330
2723
  component: 'accordion',
2331
- targets: disclosureExit('accordion'),
2724
+ targets: {
2725
+ ...disclosureExit('accordion'),
2726
+ lynx: {
2727
+ parts: {
2728
+ // No hover on a touch platform: the held trigger takes daisy's
2729
+ // hover fill. The ::after chevron has no lynx counterpart (no
2730
+ // pseudo-elements, and the anatomy declares no part for it) —
2731
+ // an app draws its own glyph in the trigger.
2732
+ //
2733
+ // The item card clips (`overflow: hidden`) and the trigger
2734
+ // fills it, so an outside ring is clipped away entirely
2735
+ // (signalxjs/lynx#1164): the ring is drawn inset, rounded to
2736
+ // the card's inner corners.
2737
+ trigger: {
2738
+ states: {
2739
+ pressed: { background: 'var(--color-base-200)' },
2740
+ 'focus-visible': {
2741
+ ...lynxFocusRing('var(--color-base-content)', { inset: true }),
2742
+ borderRadius: 'calc(var(--radius-box) - var(--border))',
2743
+ },
2744
+ },
2745
+ },
2746
+ },
2747
+ },
2748
+ },
2332
2749
  // Same accent grammar as collapsible: the open heading's ink, defaulting
2333
2750
  // to base-content so the un-attributed render is unchanged.
2334
2751
  tokens: { '--accordion-accent': 'var(--color-base-content)' },
@@ -2609,10 +3026,47 @@ export const select = {
2609
3026
  web: { parts: { popup: { base: anchoredListbox } } },
2610
3027
  lynx: {
2611
3028
  parts: {
2612
- trigger: { base: lynxBtnPad(4) },
2613
- 'clear-trigger': { base: { top: 'var(--space-xs)', bottom: 'var(--space-xs)', right: 'calc(var(--size-field) * 4 + 1em + var(--space-xs))', minWidth: '1.5rem' } },
3029
+ // daisy's `.select` is `width: clamp(3rem, 20rem, 100%)`: 20rem,
3030
+ // never wider than its container. lynx has no `clamp()`, so the
3031
+ // same bounds are spelled out — and the base's 13rem floor
3032
+ // comes down to daisy's 3rem, or the trigger overflowed any
3033
+ // cell narrower than 208px (signalxjs/lynx#1146).
3034
+ //
3035
+ // The bounds sit on the ROOT, the box daisy's `.select` is,
3036
+ // and the trigger fills it. The web root is `inline-flex`, so
3037
+ // it shrink-wraps the trigger; lynx rewrites that to `flex`,
3038
+ // and a root stretched by its parent while the trigger stayed
3039
+ // capped left the clear-trigger — positioned against the root
3040
+ // — floating past the trigger's end (signalxjs/lynx#1166).
3041
+ root: { base: { width: '20rem', maxWidth: '100%', minWidth: '3rem' } },
3042
+ trigger: {
3043
+ base: { ...lynxBtnPad(4), width: '100%', maxWidth: '100%', minWidth: '3rem' },
3044
+ // The web's `:hover` border is dropped on a touch
3045
+ // platform; a press gets a wash instead — a background,
3046
+ // so it never fights the open/invalid border inks.
3047
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
3048
+ },
3049
+ // The popup renders in the overlay outlet, outside the root,
3050
+ // so it cannot inherit the root's `--select-accent`, and an
3051
+ // unresolved `var()` on lynx has no fallback: the selected
3052
+ // item and its tick fell back to base-content
3053
+ // (signalxjs/lynx#1168). The popup restates the accent; its
3054
+ // colour-axis twins are in `variants.color` below.
3055
+ popup: { base: { '--select-accent': 'var(--color-primary)' } },
3056
+ // A touch list has no highlight: the pressed item takes the
3057
+ // highlighted item's wash.
3058
+ item: { states: { pressed: { background: 'var(--color-base-200)' } } },
3059
+ 'clear-trigger': {
3060
+ base: { top: 'var(--space-xs)', bottom: 'var(--space-xs)', right: 'calc(var(--size-field) * 4 + 1em + var(--space-xs))', minWidth: '1.5rem' },
3061
+ states: lynxFocus(),
3062
+ },
2614
3063
  },
2615
3064
  variants: {
3065
+ // The popup's accent follows the root's colour axis — the
3066
+ // popup carries the axis class itself (lynx stamps every part).
3067
+ color: Object.fromEntries(ROLES.map((c) => [c, { popup: { base: {
3068
+ '--select-accent': `var(--color-${c})`,
3069
+ } } }])),
2616
3070
  size: { ...lynxBtnSizes('trigger'), md: { trigger: { base: lynxBtnPad(4) } } },
2617
3071
  },
2618
3072
  },
@@ -2633,6 +3087,38 @@ export const select = {
2633
3087
  */
2634
3088
  const BTN_STEPS = { xs: 6, sm: 8, md: 10, lg: 12, xl: 14 };
2635
3089
  const btnHeight = (step) => `calc(var(--size-field) * ${BTN_STEPS[step]})`;
3090
+ /**
3091
+ * The btn padding ramp — `[size, block space, inline space]` per step. One
3092
+ * table for the web's `padding` shorthand and the lynx section's longhands,
3093
+ * so the two spellings cannot drift apart.
3094
+ */
3095
+ const BTN_PAD = [
3096
+ ['xs', '2xs', 'xs'],
3097
+ ['sm', 'xs', 'sm'],
3098
+ ['md', 'sm', 'lg'],
3099
+ ['lg', 'md', 'xl'],
3100
+ ['xl', 'lg', '2xl'],
3101
+ ];
3102
+ const btnPadding = (step) => {
3103
+ const [, block, inline] = BTN_PAD.find(([size]) => size === step);
3104
+ return `var(--space-${block}) var(--space-${inline})`;
3105
+ };
3106
+ /**
3107
+ * daisy 5's disabled btn, for the lynx target (signalxjs/lynx#1143). The web
3108
+ * fades the whole button (`opacity: var(--disabled-opacity)`, 0.3 on the
3109
+ * dark themes), which on device left a solid button's label barely legible.
3110
+ * daisy's own `.btn:disabled` paints instead: a base-content wash (not on
3111
+ * ghost/link), a transparent border, no shadow, and a muted base-content
3112
+ * label. daisy mutes that label to 20%; this keeps it at 50%, still plainly
3113
+ * inert but readable on a phone at arm's length.
3114
+ */
3115
+ const lynxBtnDisabled = {
3116
+ opacity: '1',
3117
+ background: 'color-mix(in oklab, var(--color-base-content) 10%, transparent)',
3118
+ color: 'color-mix(in oklab, var(--color-base-content) 50%, transparent)',
3119
+ borderColor: 'transparent',
3120
+ boxShadow: 'none',
3121
+ };
2636
3122
  export const button = {
2637
3123
  component: 'button',
2638
3124
  // Public to a design system derived from this one (#73).
@@ -2811,11 +3297,11 @@ export const button = {
2811
3297
  // The `<button>` element is UA `border-box`, so the stated height is
2812
3298
  // the rendered height, border included — as in daisy.
2813
3299
  size: {
2814
- xs: { root: { base: { height: btnHeight('xs'), padding: 'var(--space-2xs) var(--space-xs)', fontSize: 'var(--text-xs)' } } },
2815
- sm: { root: { base: { height: btnHeight('sm'), padding: 'var(--space-xs) var(--space-sm)', fontSize: 'var(--text-sm)' } } },
2816
- md: { root: { base: { height: btnHeight('md'), padding: 'var(--space-sm) var(--space-lg)', fontSize: 'var(--text-md)' } } },
2817
- lg: { root: { base: { height: btnHeight('lg'), padding: 'var(--space-md) var(--space-xl)', fontSize: 'var(--text-lg)' } } },
2818
- xl: { root: { base: { height: btnHeight('xl'), padding: 'var(--space-lg) var(--space-2xl)', fontSize: 'var(--text-xl)' } } },
3300
+ xs: { root: { base: { height: btnHeight('xs'), padding: btnPadding('xs'), fontSize: 'var(--text-xs)' } } },
3301
+ sm: { root: { base: { height: btnHeight('sm'), padding: btnPadding('sm'), fontSize: 'var(--text-sm)' } } },
3302
+ md: { root: { base: { height: btnHeight('md'), padding: btnPadding('md'), fontSize: 'var(--text-md)' } } },
3303
+ lg: { root: { base: { height: btnHeight('lg'), padding: btnPadding('lg'), fontSize: 'var(--text-lg)' } } },
3304
+ xl: { root: { base: { height: btnHeight('xl'), padding: btnPadding('xl'), fontSize: 'var(--text-xl)' } } },
2819
3305
  },
2820
3306
  },
2821
3307
  /**
@@ -2846,14 +3332,96 @@ export const button = {
2846
3332
  },
2847
3333
  defaultVariants: { color: 'primary', variant: 'solid', size: 'md' },
2848
3334
  keyframes: { 'zero-daisyui-btn-spin': 'to { transform: rotate(360deg) }' },
2849
- // The icon chips' zeroed inline padding, restated physically: logical
2850
- // spellings resolve on iOS but not on Android (measured,
2851
- // signalxjs/lynx#1084), so the emitter refuses them.
2852
3335
  targets: {
2853
3336
  lynx: {
3337
+ parts: {
3338
+ // The ring, restated: `currentColor` never resolves on lynx
3339
+ // (signalxjs/lynx#1079) and the logical sizes/edge are
3340
+ // unproven there, so the lynx ring is physical longhands in
3341
+ // the variant's own ink (the variants below pick it). Every
3342
+ // var-bearing edge is a longhand (`lynxSides`): a var-bearing
3343
+ // `border` expanded after the transparent top and closed the
3344
+ // ring (signalxjs/lynx#1161). The shorthand stays, static, so
3345
+ // the shared `currentColor` spelling never ships.
3346
+ spinner: {
3347
+ base: {
3348
+ width: '1em',
3349
+ height: '1em',
3350
+ border: '0 solid transparent',
3351
+ ...lynxSides('border{}Width', 'calc(var(--border) * 2)'),
3352
+ ...lynxSides('border{}Color', 'var(--btn-ink)', ['Right', 'Bottom', 'Left']),
3353
+ borderTopColor: 'transparent',
3354
+ },
3355
+ },
3356
+ root: {
3357
+ // daisy's content-sized, never-squeezed btn (#1165), and
3358
+ // the hairline as longhands: a var-bearing `border`
3359
+ // shorthand would expand over the variants' and the
3360
+ // disabled state's edge colours (see `lynxSides`).
3361
+ base: {
3362
+ ...lynxBtnFit,
3363
+ border: '0 solid transparent',
3364
+ ...lynxSides('border{}Width', 'var(--border)'),
3365
+ },
3366
+ // The ring in the variant's own ink (signalxjs/lynx#1163).
3367
+ states: { disabled: lynxBtnDisabled, 'focus-visible': lynxFocusRing('var(--btn-ink)') },
3368
+ },
3369
+ },
3370
+ variants: {
3371
+ // The baked disabled paint above outranks every variant's fill
3372
+ // (the theme host adds a class), but its plain declarations
3373
+ // do not: outline/dash restate the border, ghost/link drop
3374
+ // the wash (as in daisy). outline/dash draw their ink edge as
3375
+ // longhands, so that transparent restatement wins by
3376
+ // specificity instead of losing to a var-bearing
3377
+ // `border-color` (signalxjs/lynx#1162); the shared shorthand
3378
+ // is neutralised to a static value.
3379
+ variant: {
3380
+ solid: { spinner: { base: lynxSides('border{}Color', 'var(--btn-on-accent)', ['Right', 'Bottom', 'Left']) } },
3381
+ outline: {
3382
+ root: {
3383
+ base: { borderColor: 'transparent', ...lynxSides('border{}Color', 'var(--btn-ink)') },
3384
+ states: { disabled: { borderColor: 'transparent' } },
3385
+ },
3386
+ },
3387
+ dash: {
3388
+ root: {
3389
+ base: { borderColor: 'transparent', ...lynxSides('border{}Color', 'var(--btn-ink)') },
3390
+ states: { disabled: { borderColor: 'transparent' } },
3391
+ },
3392
+ },
3393
+ ghost: { root: { states: { disabled: { background: 'transparent' } } } },
3394
+ // `link` zeroes the box-shadow in a variant rule that
3395
+ // lands after the ring, so it restates the ring.
3396
+ link: { root: { states: { disabled: { background: 'transparent' }, 'focus-visible': lynxFocusRing('var(--btn-ink)') } } },
3397
+ },
3398
+ // The size ramp's paddings as longhands, so square/circle's
3399
+ // zeroed sides below win by the cascade rather than by the
3400
+ // order lynx happens to expand a var-bearing `padding` in.
3401
+ size: Object.fromEntries(BTN_PAD.map(([size, block, inline]) => [size, {
3402
+ root: {
3403
+ base: {
3404
+ padding: '0',
3405
+ ...lynxSides('padding{}', `var(--space-${block})`, ['Top', 'Bottom']),
3406
+ ...lynxSides('padding{}', `var(--space-${inline})`, ['Right', 'Left']),
3407
+ },
3408
+ },
3409
+ }])),
3410
+ },
3411
+ // The icon chips' zeroed inline padding, restated physically: logical
3412
+ // spellings resolve on iOS but not on Android (measured,
3413
+ // signalxjs/lynx#1084), so the emitter refuses them. `width: auto`
3414
+ // undoes the root's `max-content`, which would otherwise switch
3415
+ // off the `aspect-ratio` that squares the chip. `wide`/`block`
3416
+ // restate their width physically: `inline-size` and
3417
+ // `max-inline-size` are not lynx properties at all.
2854
3418
  modifiers: {
2855
- square: { root: { base: { paddingLeft: '0', paddingRight: '0' } } },
2856
- circle: { root: { base: { paddingLeft: '0', paddingRight: '0' } } },
3419
+ wide: { root: { base: { width: '100%', maxWidth: '16rem' } } },
3420
+ block: { root: { base: { width: '100%' } } },
3421
+ square: { root: { base: { width: 'auto', paddingLeft: '0', paddingRight: '0' } } },
3422
+ circle: { root: { base: { width: 'auto', paddingLeft: '0', paddingRight: '0' } } },
3423
+ // `active` zeroes the box-shadow too (the held press look).
3424
+ active: { root: { states: { 'focus-visible': lynxFocusRing('var(--btn-ink)') } } },
2857
3425
  },
2858
3426
  },
2859
3427
  },
@@ -2933,6 +3501,22 @@ export const avatar = {
2933
3501
  },
2934
3502
  },
2935
3503
  };
3504
+ /**
3505
+ * A toast whose indicator is rendered (#292): the grid grows a leading column
3506
+ * for the mark, and the text, action and close step one column along.
3507
+ */
3508
+ const TOAST_MARKED = '[data-scope="toast"][data-part="root"]:has(> [data-scope="toast"][data-part="indicator"]) > &';
3509
+ /** The indicator's marks, cut from its fill — not mirrored in RTL (a tick is not a direction). */
3510
+ 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%)';
3511
+ 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%)';
3512
+ /**
3513
+ * daisy's success and error are FILLS — light enough in `light` and `nord`
3514
+ * that a bare mark in them reads under 2:1 on the toast's base-200. Deepened
3515
+ * toward base-content (which flips with the theme), the hue survives and the
3516
+ * mark clears the 3:1 non-text floor in every theme.
3517
+ */
3518
+ const TOAST_SUCCESS_INK = 'color-mix(in oklab, var(--color-success) 55%, var(--color-base-content))';
3519
+ const TOAST_ERROR_INK = 'color-mix(in oklab, var(--color-error) 55%, var(--color-base-content))';
2936
3520
  /**
2937
3521
  * Toast presence is runtime-managed (see the SKILL's Toast section): plain
2938
3522
  * two-state transitions only — no `@starting-style`, no `allow-discrete`.
@@ -2943,6 +3527,7 @@ export const toast = {
2943
3527
  '--toast-bg': 'var(--color-base-200)',
2944
3528
  '--toast-ink': 'var(--color-base-content)',
2945
3529
  '--toast-from': '8px',
3530
+ '--toast-mark': '1rem',
2946
3531
  },
2947
3532
  parts: {
2948
3533
  viewport: {
@@ -2996,6 +3581,7 @@ export const toast = {
2996
3581
  },
2997
3582
  selectors: {
2998
3583
  '&[data-placement^="top"]': { '--toast-from': '-8px' },
3584
+ '&:has(> [data-scope="toast"][data-part="indicator"])': { gridTemplateColumns: 'auto 1fr auto auto' },
2999
3585
  },
3000
3586
  states: {
3001
3587
  open: { opacity: '1', transform: 'none' },
@@ -3005,8 +3591,39 @@ export const toast = {
3005
3591
  'reduced-motion': { base: { transition: 'none' }, states: { open: { transform: 'none' } } },
3006
3592
  },
3007
3593
  },
3594
+ // daisy's `loading-spinner` ring in the alert's own ink, then the
3595
+ // success tick or the error cross — the promise toast's status.
3596
+ indicator: {
3597
+ base: {
3598
+ gridColumn: '1',
3599
+ gridRow: '1 / span 2',
3600
+ inlineSize: 'var(--toast-mark)',
3601
+ blockSize: 'var(--toast-mark)',
3602
+ boxSizing: 'border-box',
3603
+ },
3604
+ states: {
3605
+ loading: {
3606
+ borderRadius: '50%',
3607
+ border: 'calc(var(--border) * 2) solid color-mix(in oklab, var(--toast-ink) 25%, transparent)',
3608
+ borderBlockStartColor: 'var(--toast-ink)',
3609
+ animation: 'zero-daisyui-toast-spin 0.75s linear infinite',
3610
+ },
3611
+ complete: { background: TOAST_SUCCESS_INK, clipPath: TOAST_CHECK },
3612
+ error: { background: TOAST_ERROR_INK, clipPath: TOAST_CROSS },
3613
+ },
3614
+ at: {
3615
+ 'reduced-motion': { states: { loading: { animation: 'none' } } },
3616
+ 'forced-colors': {
3617
+ states: {
3618
+ complete: { background: 'CanvasText', forcedColorAdjust: 'none' },
3619
+ error: { background: 'CanvasText', forcedColorAdjust: 'none' },
3620
+ },
3621
+ },
3622
+ },
3623
+ },
3008
3624
  title: {
3009
3625
  base: { gridColumn: '1', fontWeight: 'var(--weight-semibold)' },
3626
+ selectors: { [TOAST_MARKED]: { gridColumn: '2' } },
3010
3627
  },
3011
3628
  description: {
3012
3629
  base: {
@@ -3014,6 +3631,7 @@ export const toast = {
3014
3631
  fontSize: 'var(--text-xs)',
3015
3632
  color: 'color-mix(in oklab, var(--toast-ink) 75%, transparent)',
3016
3633
  },
3634
+ selectors: { [TOAST_MARKED]: { gridColumn: '2' } },
3017
3635
  },
3018
3636
  action: {
3019
3637
  base: {
@@ -3034,6 +3652,7 @@ export const toast = {
3034
3652
  disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
3035
3653
  ...focusRing,
3036
3654
  },
3655
+ selectors: { [TOAST_MARKED]: { gridColumn: '3' } },
3037
3656
  },
3038
3657
  close: {
3039
3658
  base: {
@@ -3053,6 +3672,7 @@ export const toast = {
3053
3672
  disabled: { opacity: 'var(--disabled-opacity)' },
3054
3673
  ...focusRing,
3055
3674
  },
3675
+ selectors: { [TOAST_MARKED]: { gridColumn: '4' } },
3056
3676
  },
3057
3677
  },
3058
3678
  variants: {
@@ -3089,24 +3709,79 @@ export const toast = {
3089
3709
  // signalxjs/lynx#1070. No `color` default: the un-attributed toast is a
3090
3710
  // neutral base-200 surface, outside the color vocabulary.
3091
3711
  defaultVariants: { size: 'md' },
3092
- // The corner placements' physical anchors: logical inset spellings
3093
- // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
3094
- // the emitter refuses them. Physical is the lynx target's norm — no RTL
3095
- // flow there, so `start` IS left and `end` IS right.
3712
+ keyframes: { 'zero-daisyui-toast-spin': 'to { transform: rotate(360deg); }' },
3096
3713
  targets: {
3097
3714
  lynx: {
3098
3715
  parts: {
3716
+ // The viewport is a full-width strip on lynx (the runtime pins
3717
+ // it to the outlet's top or bottom edge). What the web gets
3718
+ // from `:popover-open` and `min(24rem, 100vw)` is restated:
3719
+ // flex (a lynx view defaults to linear layout, which ignores
3720
+ // flex-direction and gap), and the cards aligned to the
3721
+ // placement's edge. The centred pair's `left: 50%` +
3722
+ // `translateX(-50%)` pull-back is for a shrink-wrapped popover;
3723
+ // on the strip it shifted everything half a width left, so it
3724
+ // is cancelled. The corner anchors are physical: logical
3725
+ // insets resolve on iOS but not on Android (measured,
3726
+ // signalxjs/lynx#1084), and lynx has no RTL flow, so `start`
3727
+ // IS left and `end` IS right.
3099
3728
  viewport: {
3729
+ base: { display: 'flex', width: 'auto' },
3100
3730
  selectors: {
3101
- '&[data-placement="top-start"]': { left: '0' },
3102
- '&[data-placement="top-end"]': { right: '0' },
3103
- '&[data-placement="bottom-start"]': { left: '0' },
3104
- '&[data-placement="bottom-end"]': { right: '0' },
3731
+ '&[data-placement="top-start"]': { left: '0', alignItems: 'flex-start' },
3732
+ '&[data-placement="top"]': { left: '0', right: '0', transform: 'none', alignItems: 'center' },
3733
+ '&[data-placement="top-end"]': { right: '0', alignItems: 'flex-end' },
3734
+ '&[data-placement="bottom-start"]': { left: '0', alignItems: 'flex-start' },
3735
+ '&[data-placement="bottom"]': { left: '0', right: '0', transform: 'none', alignItems: 'center' },
3736
+ '&[data-placement="bottom-end"]': { right: '0', alignItems: 'flex-end' },
3105
3737
  },
3106
3738
  },
3739
+ // No grid on lynx (signalxjs/lynx#1075): the card is a flex
3740
+ // column (title, description, then the action), with the
3741
+ // close button pinned to the top-right corner and the title
3742
+ // kept clear of it. The web's 24rem viewport cap becomes the
3743
+ // card's own cap. The `^="top"` entry offset has no class
3744
+ // form, so each top placement restates it.
3745
+ root: {
3746
+ base: {
3747
+ display: 'flex',
3748
+ flexDirection: 'column',
3749
+ alignItems: 'flex-start',
3750
+ position: 'relative',
3751
+ boxSizing: 'border-box',
3752
+ width: '100%',
3753
+ maxWidth: '24rem',
3754
+ },
3755
+ selectors: {
3756
+ '&[data-placement="top-start"]': { '--toast-from': '-8px' },
3757
+ '&[data-placement="top"]': { '--toast-from': '-8px' },
3758
+ '&[data-placement="top-end"]': { '--toast-from': '-8px' },
3759
+ },
3760
+ },
3761
+ title: { base: { paddingRight: '1.75em' } },
3762
+ // The 75% ink mix over a recipe-local property cannot bake;
3763
+ // the same ink at 75% opacity reads the same on the soft fill.
3764
+ description: { base: { color: 'var(--toast-ink)', opacity: '0.75' } },
3765
+ // The 25% ink border cannot bake either: it takes the full ink.
3766
+ // Their rings' gap is the card's own fill.
3767
+ action: {
3768
+ base: {
3769
+ marginTop: 'var(--space-xs)',
3770
+ border: 'var(--border) solid var(--toast-ink)',
3771
+ },
3772
+ states: lynxFocus({ gap: 'var(--toast-bg)' }),
3773
+ },
3774
+ close: {
3775
+ base: { position: 'absolute', top: 'var(--space-xs)', right: 'var(--space-xs)' },
3776
+ states: lynxFocus({ gap: 'var(--toast-bg)' }),
3777
+ },
3107
3778
  },
3108
3779
  },
3109
3780
  },
3781
+ // The viewport's `open` (the stack expanded) is a fact this skin has no
3782
+ // use for: its toasts are always a plain column, so both states look the
3783
+ // same by design (#292).
3784
+ skipStates: { viewport: ['open', 'closed'] },
3110
3785
  };
3111
3786
  export const combobox = {
3112
3787
  component: 'combobox',
@@ -3906,12 +4581,28 @@ const treeRowSelectors = {
3906
4581
  background: 'var(--color-base-300)',
3907
4582
  },
3908
4583
  };
4584
+ /**
4585
+ * `tickGlyphFallback` for the node check box, whose tick is its `::after`:
4586
+ * the geometry drops and the same pseudo carries the glyph, in the medium's
4587
+ * own ink (the #233 divergence, restated).
4588
+ */
4589
+ const nodeTickFallback = (ink) => ({
4590
+ selectors: {
4591
+ '&::after': { clipPath: 'none', backgroundColor: '#0000', rotate: '0deg', inset: '0', display: 'grid', placeItems: 'center', color: ink, fontSize: '0.85em', lineHeight: '1' },
4592
+ '&[data-state="checked"]::after': { content: '"✔︎"', clipPath: 'none', rotate: '0deg' },
4593
+ '&[data-state="indeterminate"]::after': { content: '"−"', clipPath: 'none', rotate: '0deg', translate: 'none' },
4594
+ },
4595
+ });
3909
4596
  export const treeView = {
3910
4597
  component: 'tree-view',
3911
4598
  tokens: {
3912
4599
  '--tree-accent': 'var(--color-primary)',
3913
4600
  '--tree-text': 'var(--text-sm)',
3914
4601
  '--tree-on-accent': 'var(--color-primary-content)',
4602
+ // The node check box's pair — the tree's own accent, so a colour
4603
+ // variant retints the box with the row.
4604
+ '--tree-check': 'var(--tree-accent)',
4605
+ '--tree-on-check': 'var(--tree-on-accent)',
3915
4606
  },
3916
4607
  parts: {
3917
4608
  root: {
@@ -3960,6 +4651,58 @@ export const treeView = {
3960
4651
  'reduced-motion': { base: { transition: 'none' } },
3961
4652
  },
3962
4653
  },
4654
+ // daisy's `.checkbox` at row scale: the accent outline, the recessed
4655
+ // depth shade, the fill on check — and daisy's own tick, the six
4656
+ // clip-path points rotated 45°, drawn on the box's `::after` since
4657
+ // the part is one span. On a selected row (the accent fill) the pair
4658
+ // swaps, so the box never vanishes into the row it sits on.
4659
+ 'node-checkbox': {
4660
+ base: {
4661
+ display: 'inline-block',
4662
+ position: 'relative',
4663
+ flexShrink: '0',
4664
+ boxSizing: 'border-box',
4665
+ width: '1.25em',
4666
+ height: '1.25em',
4667
+ border: 'var(--border) solid var(--tree-check)',
4668
+ borderRadius: 'var(--radius-selector)',
4669
+ boxShadow: '0 1px var(--depth-shade) inset',
4670
+ cursor: 'pointer',
4671
+ transition: 'background-color var(--duration-normal) var(--ease-standard)',
4672
+ },
4673
+ states: {
4674
+ checked: { backgroundColor: 'var(--tree-check)' },
4675
+ indeterminate: { backgroundColor: 'var(--tree-check)' },
4676
+ unchecked: {},
4677
+ disabled: { cursor: 'not-allowed' },
4678
+ },
4679
+ selectors: {
4680
+ '&::after': {
4681
+ content: '""',
4682
+ position: 'absolute',
4683
+ inset: '15%',
4684
+ opacity: '0',
4685
+ rotate: '45deg',
4686
+ backgroundColor: 'var(--tree-on-check)',
4687
+ clipPath: TICK_COLLAPSED,
4688
+ transition: 'clip-path var(--duration-slow) var(--ease-standard) var(--duration-instant), '
4689
+ + 'opacity var(--duration-instant) var(--ease-standard) var(--duration-instant), '
4690
+ + 'rotate var(--duration-slow) var(--ease-standard) var(--duration-instant), '
4691
+ + 'translate var(--duration-slow) var(--ease-standard) var(--duration-instant)',
4692
+ },
4693
+ '&[data-state="checked"]::after': { clipPath: TICK_DRAWN, opacity: '1' },
4694
+ '&[data-state="indeterminate"]::after': { clipPath: DASH_DRAWN, opacity: '1', translate: '0 -35%', rotate: '0deg' },
4695
+ '[data-scope="tree-view"][data-selected] &': {
4696
+ '--tree-check': 'var(--tree-on-accent)',
4697
+ '--tree-on-check': 'var(--tree-accent)',
4698
+ },
4699
+ },
4700
+ at: {
4701
+ 'reduced-motion': { base: { transition: 'none' }, selectors: { '&::after': { transition: 'none' } } },
4702
+ 'forced-colors': nodeTickFallback('CanvasText'),
4703
+ print: nodeTickFallback('var(--print-ink)'),
4704
+ },
4705
+ },
3963
4706
  // Indentation comes from this inline padding — depth is the DOM
3964
4707
  // nesting, no per-level rules needed.
3965
4708
  'branch-content': {
@@ -4240,6 +4983,36 @@ export const card = {
4240
4983
  overflow: 'hidden',
4241
4984
  },
4242
4985
  },
4986
+ /**
4987
+ * Media (#302) — daisy's `card > figure`: a centred flex box, flush
4988
+ * to the card's edges, the image filling it.
4989
+ */
4990
+ media: {
4991
+ base: {
4992
+ display: 'flex',
4993
+ alignItems: 'center',
4994
+ justifyContent: 'center',
4995
+ margin: '0',
4996
+ inlineSize: '100%',
4997
+ objectFit: 'cover',
4998
+ },
4999
+ selectors: {
5000
+ // The media's own image, when the band wraps one (a div or a
5001
+ // figure): block-level, the band's full width, cropped rather
5002
+ // than stretched when the band is given a height.
5003
+ '& > :is(img, picture, video, svg, canvas)': {
5004
+ display: 'block',
5005
+ inlineSize: '100%',
5006
+ blockSize: '100%',
5007
+ objectFit: 'cover',
5008
+ },
5009
+ // The corners it shares with the card (#302): the root clips
5010
+ // too, but an asChild <img> is the band itself and rounds on
5011
+ // its own.
5012
+ '&:first-child': { borderStartStartRadius: 'var(--radius-box)', borderStartEndRadius: 'var(--radius-box)' },
5013
+ '&:last-child': { borderEndStartRadius: 'var(--radius-box)', borderEndEndRadius: 'var(--radius-box)' },
5014
+ },
5015
+ },
4243
5016
  header: {
4244
5017
  base: {
4245
5018
  display: 'flex',
@@ -4569,16 +5342,51 @@ export const divider = {
4569
5342
  tokens: { '--divider-ink': 'var(--color-base-300)', '--divider-thickness': 'var(--border)' },
4570
5343
  parts: {
4571
5344
  root: {
4572
- base: { border: 'none', background: 'var(--divider-ink)', alignSelf: 'stretch' },
5345
+ // The rule is two flex segments (#298), drawn as borders: with no
5346
+ // label they meet and read as one line, and a label sits between
5347
+ // them. Borders rather than a background, so forced colours keep
5348
+ // the line instead of revaluing it to Canvas.
5349
+ base: {
5350
+ display: 'flex',
5351
+ alignItems: 'center',
5352
+ border: 'none',
5353
+ alignSelf: 'stretch',
5354
+ },
4573
5355
  selectors: {
4574
- '&[data-orientation="horizontal"]': {
4575
- inlineSize: '100%',
4576
- blockSize: 'var(--divider-thickness)',
4577
- },
4578
- '&[data-orientation="vertical"]': {
4579
- inlineSize: 'var(--divider-thickness)',
4580
- minBlockSize: '1em',
5356
+ '&::before': { content: '""', flex: '1 1 0', minInlineSize: '0' },
5357
+ '&::after': { content: '""', flex: '1 1 0', minInlineSize: '0' },
5358
+ '&[data-orientation="horizontal"]': { inlineSize: '100%' },
5359
+ '&[data-orientation="horizontal"]::before': { borderBlockStart: 'var(--divider-thickness) solid var(--divider-ink)' },
5360
+ '&[data-orientation="horizontal"]::after': { borderBlockStart: 'var(--divider-thickness) solid var(--divider-ink)' },
5361
+ '&[data-orientation="vertical"]': { flexDirection: 'column', minBlockSize: '1em' },
5362
+ '&[data-orientation="vertical"]::before': { borderInlineStart: 'var(--divider-thickness) solid var(--divider-ink)' },
5363
+ '&[data-orientation="vertical"]::after': { borderInlineStart: 'var(--divider-thickness) solid var(--divider-ink)' },
5364
+ // A placed label sits flush at its edge: the segment on
5365
+ // that side goes, and the other one takes the whole run.
5366
+ '&:has(> [data-scope="divider"][data-part="label"][data-placement="start"])::before': { display: 'none' },
5367
+ '&:has(> [data-scope="divider"][data-part="label"][data-placement="end"])::after': { display: 'none' },
5368
+ },
5369
+ },
5370
+ label: {
5371
+ base: {
5372
+ // daisy's `divider` text: the content colour, the rule
5373
+ // stopping a gap short of it on either side.
5374
+ flex: 'none',
5375
+ paddingInline: 'var(--space-md)',
5376
+ fontSize: 'var(--text-sm)',
5377
+ whiteSpace: 'nowrap',
5378
+ color: 'var(--color-base-content)',
5379
+ },
5380
+ selectors: {
5381
+ '[data-scope="divider"][data-part="root"][data-orientation="vertical"] > &': {
5382
+ paddingInline: '0',
5383
+ paddingBlock: 'var(--space-sm)',
4581
5384
  },
5385
+ // Flush means flush: no inset on the side the rule gave up.
5386
+ '&[data-placement="start"]': { paddingInlineStart: '0' },
5387
+ '&[data-placement="end"]': { paddingInlineEnd: '0' },
5388
+ '[data-scope="divider"][data-part="root"][data-orientation="vertical"] > &[data-placement="start"]': { paddingBlockStart: '0' },
5389
+ '[data-scope="divider"][data-part="root"][data-orientation="vertical"] > &[data-placement="end"]': { paddingBlockEnd: '0' },
4582
5390
  },
4583
5391
  },
4584
5392
  },
@@ -5121,6 +5929,10 @@ export const timeline = {
5121
5929
  },
5122
5930
  },
5123
5931
  },
5932
+ // Title and description (#302): daisy's timeline-box leads with a
5933
+ // bold line; the detail under it in the card description's ink.
5934
+ title: { base: { display: 'block', margin: '0', fontWeight: 'var(--weight-semibold)', lineHeight: 'var(--leading-tight)' } },
5935
+ description: { base: { display: 'block', margin: '0', marginBlockStart: 'var(--space-2xs)', color: 'color-mix(in oklab, var(--color-base-content) 70%, transparent)' } },
5124
5936
  },
5125
5937
  variants: {
5126
5938
  color: Object.fromEntries(ROLES.map((c) => [c, { marker: { base: {
@@ -5135,6 +5947,87 @@ export const timeline = {
5135
5947
  xl: { marker: { base: { '--timeline-marker-size': 'calc(var(--size-selector) * 4)' } }, content: { base: { fontSize: 'var(--text-md)' } } },
5136
5948
  },
5137
5949
  },
5950
+ /**
5951
+ * Lynx has no grid, and it does not resolve `inline-size`/`block-size`:
5952
+ * the item fell back to the engine's default layout and the marker,
5953
+ * sized only logically, stretched into a pill (signalxjs/lynx#1145).
5954
+ *
5955
+ * The item is a wrapping flex row instead. Vertical: line one is the
5956
+ * marker beside its content (start-side content ordered before the
5957
+ * marker), and the connector takes line two by itself (`flex-basis:
5958
+ * 100%`), drawn as a left border under the marker's centre, which is the
5959
+ * web's row-2 segment. Horizontal: line one is the marker with the
5960
+ * connector running on to the item's end, and the content wraps under it
5961
+ * (start-side content above). Content grows from a zero basis, so long
5962
+ * text never wraps onto its own line.
5963
+ *
5964
+ * The web collapses the start track when no item has start-side content
5965
+ * (`:has()`); lynx has no such test. The vertical connector sits under a
5966
+ * marker at the item's start edge, which is where the marker is unless
5967
+ * that item has start-side content: an alternating vertical timeline is
5968
+ * web-only for now.
5969
+ */
5970
+ targets: {
5971
+ lynx: {
5972
+ parts: {
5973
+ item: { base: { display: 'flex', flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center' } },
5974
+ marker: {
5975
+ base: {
5976
+ width: 'var(--timeline-marker-size)',
5977
+ height: 'var(--timeline-marker-size)',
5978
+ flexGrow: '0',
5979
+ flexShrink: '0',
5980
+ },
5981
+ },
5982
+ connector: {
5983
+ selectors: {
5984
+ '&[data-orientation="vertical"]': {
5985
+ flexGrow: '0',
5986
+ flexShrink: '1',
5987
+ flexBasis: '100%',
5988
+ height: 'var(--space-lg)',
5989
+ background: 'transparent',
5990
+ borderLeftWidth: 'var(--border)',
5991
+ borderLeftStyle: 'solid',
5992
+ borderLeftColor: 'var(--color-base-300)',
5993
+ marginLeft: 'calc(var(--timeline-marker-size) / 2 - var(--border) / 2)',
5994
+ },
5995
+ '&[data-orientation="horizontal"]': {
5996
+ order: '1',
5997
+ flexGrow: '1',
5998
+ flexShrink: '1',
5999
+ flexBasis: '0',
6000
+ height: 'var(--border)',
6001
+ minWidth: 'var(--space-lg)',
6002
+ },
6003
+ },
6004
+ },
6005
+ content: {
6006
+ selectors: {
6007
+ '&[data-orientation="vertical"][data-placement="start"]': {
6008
+ order: '-1',
6009
+ flexGrow: '1',
6010
+ flexShrink: '1',
6011
+ flexBasis: '0',
6012
+ },
6013
+ '&[data-orientation="vertical"][data-placement="end"]': {
6014
+ flexGrow: '1',
6015
+ flexShrink: '1',
6016
+ flexBasis: '0',
6017
+ },
6018
+ '&[data-orientation="horizontal"][data-placement="start"]': {
6019
+ order: '-1',
6020
+ flexBasis: '100%',
6021
+ },
6022
+ '&[data-orientation="horizontal"][data-placement="end"]': {
6023
+ order: '2',
6024
+ flexBasis: '100%',
6025
+ },
6026
+ },
6027
+ },
6028
+ },
6029
+ },
6030
+ },
5138
6031
  };
5139
6032
  /** daisy chat: the classic bubble, colour-refilled per role. */
5140
6033
  export const chat = {
@@ -5244,6 +6137,95 @@ export const chat = {
5244
6137
  },
5245
6138
  },
5246
6139
  };
6140
+ /**
6141
+ * ChatLog — daisy's `bg-base-200 rounded-box` well that the chat bubbles
6142
+ * already sit in, scrolling; the jump trigger is a `btn btn-sm` pill in the
6143
+ * role's solid pair with daisy's `shadow-xs` and its 1px press sink, floated
6144
+ * over the rows by `position: sticky` at the box's foot. Its negative block
6145
+ * margin gives back the line it would take, so appearing moves no row.
6146
+ */
6147
+ export const chatLog = {
6148
+ component: 'chat-log',
6149
+ tokens: {
6150
+ '--chat-log-fill': 'var(--color-primary)',
6151
+ '--chat-log-on-fill': 'var(--color-primary-content)',
6152
+ '--chat-log-jump-size': 'calc(var(--size-field) * 8)',
6153
+ },
6154
+ parts: {
6155
+ root: {
6156
+ base: {
6157
+ display: 'flex',
6158
+ flexDirection: 'column',
6159
+ overflowY: 'auto',
6160
+ overscrollBehaviorY: 'contain',
6161
+ borderRadius: 'var(--radius-box)',
6162
+ background: 'var(--color-base-100)',
6163
+ color: 'var(--color-base-content)',
6164
+ border: 'var(--border) solid var(--color-base-300)',
6165
+ padding: 'var(--space-sm) var(--space-md)',
6166
+ },
6167
+ // Drawn inside the box (negative offset): an outward ring would
6168
+ // be clipped by whatever scrolls around the transcript.
6169
+ states: { 'focus-visible': { outline: '2px solid var(--color-base-content)', outlineOffset: '-2px' } },
6170
+ },
6171
+ content: {
6172
+ base: {
6173
+ // `auto` gathers a short transcript at the foot; a long one
6174
+ // overflows and the margin resolves to nothing.
6175
+ marginBlockStart: 'auto',
6176
+ flex: 'none',
6177
+ display: 'grid',
6178
+ rowGap: 'var(--space-xs)',
6179
+ },
6180
+ },
6181
+ 'jump-trigger': {
6182
+ base: {
6183
+ position: 'sticky',
6184
+ insetBlockEnd: 'var(--space-sm)',
6185
+ alignSelf: 'center',
6186
+ flex: 'none',
6187
+ marginBlockStart: 'calc(var(--chat-log-jump-size) * -1)',
6188
+ blockSize: 'var(--chat-log-jump-size)',
6189
+ appearance: 'none',
6190
+ display: 'inline-flex',
6191
+ alignItems: 'center',
6192
+ gap: 'var(--space-2xs)',
6193
+ paddingInline: 'var(--space-md)',
6194
+ border: 'var(--border) solid transparent',
6195
+ borderRadius: '9999px',
6196
+ background: 'var(--chat-log-fill)',
6197
+ color: 'var(--chat-log-on-fill)',
6198
+ boxShadow: 'var(--shadow-xs)',
6199
+ fontFamily: 'inherit',
6200
+ fontSize: 'var(--text-xs)',
6201
+ fontWeight: 'var(--weight-semibold)',
6202
+ lineHeight: 'var(--leading-none)',
6203
+ cursor: 'pointer',
6204
+ transition: 'background var(--duration-fast) var(--ease-standard)',
6205
+ },
6206
+ states: {
6207
+ open: {},
6208
+ closed: {},
6209
+ hover: { background: 'color-mix(in oklab, var(--chat-log-fill) 90%, black)' },
6210
+ 'focus-visible': { outline: '2px solid var(--color-base-content)', outlineOffset: '2px' },
6211
+ },
6212
+ selectors: { '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)', boxShadow: 'none' } },
6213
+ },
6214
+ },
6215
+ variants: {
6216
+ color: Object.fromEntries(ROLES.map((c) => [c, { 'jump-trigger': { base: {
6217
+ '--chat-log-fill': `var(--color-${c})`,
6218
+ '--chat-log-on-fill': `var(--color-${c}-content)`,
6219
+ } } }])),
6220
+ size: {
6221
+ xs: { content: { base: { rowGap: 'var(--space-2xs)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 6)', fontSize: 'var(--text-xs)' } } },
6222
+ sm: { content: { base: { rowGap: 'var(--space-2xs)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 7)', fontSize: 'var(--text-xs)' } } },
6223
+ md: {},
6224
+ lg: { content: { base: { rowGap: 'var(--space-sm)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 9)', fontSize: 'var(--text-sm)' } } },
6225
+ xl: { content: { base: { rowGap: 'var(--space-md)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 10)', fontSize: 'var(--text-sm)' } } },
6226
+ },
6227
+ },
6228
+ };
5247
6229
  /** daisy radial-progress: --value became --progress-percent; same ring. */
5248
6230
  export const radialProgress = {
5249
6231
  component: 'radial-progress',
@@ -5630,6 +6612,8 @@ export const breadcrumbs = {
5630
6612
  alignItems: 'center',
5631
6613
  gap: 'var(--space-xs)',
5632
6614
  },
6615
+ // `closed` is presence: the runtime hides the part (#295).
6616
+ states: { open: {}, closed: {} },
5633
6617
  },
5634
6618
  link: {
5635
6619
  base: {
@@ -5658,6 +6642,37 @@ export const breadcrumbs = {
5658
6642
  userSelect: 'none',
5659
6643
  },
5660
6644
  },
6645
+ ellipsis: {
6646
+ base: {
6647
+ display: 'flex',
6648
+ alignItems: 'center',
6649
+ gap: 'var(--space-xs)',
6650
+ },
6651
+ // `closed` is presence: the runtime hides the part (#295).
6652
+ states: { open: {}, closed: {} },
6653
+ },
6654
+ 'ellipsis-trigger': {
6655
+ base: {
6656
+ appearance: 'none',
6657
+ background: 'transparent',
6658
+ border: '0',
6659
+ padding: '0 var(--space-2xs)',
6660
+ margin: '0',
6661
+ font: 'inherit',
6662
+ lineHeight: 'inherit',
6663
+ color: 'color-mix(in oklch, var(--color-base-content) 75%, transparent)',
6664
+ borderRadius: 'var(--radius-selector)',
6665
+ cursor: 'pointer',
6666
+ transition: 'color var(--duration-fast) var(--ease-standard)',
6667
+ },
6668
+ states: {
6669
+ hover: { color: 'var(--color-base-content)', textDecoration: 'underline' },
6670
+ 'focus-visible': {
6671
+ outline: '2px solid var(--color-primary)',
6672
+ outlineOffset: '2px',
6673
+ },
6674
+ },
6675
+ },
5661
6676
  },
5662
6677
  variants: {
5663
6678
  color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
@@ -5672,11 +6687,46 @@ export const breadcrumbs = {
5672
6687
  },
5673
6688
  },
5674
6689
  };
6690
+ /**
6691
+ * The four pagination triggers — prev/next and, with `withEdges`, the
6692
+ * first/last jumps (#294) — are one cell: the same box, the same
6693
+ * glyph-sized type, and the same flip under the rtl guard, since every
6694
+ * glyph (`‹ › « »`) is physical ink pointing at a reading edge.
6695
+ */
6696
+ const pageTrigger = {
6697
+ base: {
6698
+ display: 'inline-flex',
6699
+ alignItems: 'center',
6700
+ justifyContent: 'center',
6701
+ minInlineSize: 'var(--pg-size)',
6702
+ blockSize: 'var(--pg-size)',
6703
+ background: 'var(--color-base-200)',
6704
+ color: 'var(--color-base-content)',
6705
+ border: 'none',
6706
+ borderRadius: 'var(--radius-field)',
6707
+ fontSize: 'calc(var(--pg-font) * 1.2)',
6708
+ lineHeight: 'var(--leading-none)',
6709
+ appearance: 'none',
6710
+ // Link mode renders an <a> (#294): no UA underline.
6711
+ textDecoration: 'none',
6712
+ cursor: 'pointer',
6713
+ transition: 'background var(--duration-fast) var(--ease-standard)',
6714
+ },
6715
+ states: {
6716
+ hover: { background: 'var(--color-base-300)' },
6717
+ disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
6718
+ ...focusRing,
6719
+ },
6720
+ selectors: {
6721
+ '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)' },
6722
+ [`&${rtl}`]: { scale: '-1 1' },
6723
+ },
6724
+ };
5675
6725
  /**
5676
6726
  * Pagination — daisy's join-of-buttons made of standalone btn cells: the
5677
6727
  * quiet base-200 fill, base-300 hover, the current page inverted into the
5678
6728
  * accent pair (primary by default). Pressed is the daisy 1px sink. The
5679
- * `‹`/`›` glyphs flip under the shared rtl guard.
6729
+ * `‹ › « »` glyphs flip under the shared rtl guard.
5680
6730
  */
5681
6731
  export const pagination = {
5682
6732
  component: 'pagination',
@@ -5722,6 +6772,7 @@ export const pagination = {
5722
6772
  fontWeight: 'var(--weight-semibold)',
5723
6773
  fontVariantNumeric: 'tabular-nums',
5724
6774
  appearance: 'none',
6775
+ textDecoration: 'none',
5725
6776
  cursor: 'pointer',
5726
6777
  transition: 'background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard)',
5727
6778
  },
@@ -5749,60 +6800,10 @@ export const pagination = {
5749
6800
  userSelect: 'none',
5750
6801
  },
5751
6802
  },
5752
- 'prev-trigger': {
5753
- base: {
5754
- display: 'inline-flex',
5755
- alignItems: 'center',
5756
- justifyContent: 'center',
5757
- minInlineSize: 'var(--pg-size)',
5758
- blockSize: 'var(--pg-size)',
5759
- background: 'var(--color-base-200)',
5760
- color: 'var(--color-base-content)',
5761
- border: 'none',
5762
- borderRadius: 'var(--radius-field)',
5763
- fontSize: 'calc(var(--pg-font) * 1.2)',
5764
- lineHeight: 'var(--leading-none)',
5765
- appearance: 'none',
5766
- cursor: 'pointer',
5767
- transition: 'background var(--duration-fast) var(--ease-standard)',
5768
- },
5769
- states: {
5770
- hover: { background: 'var(--color-base-300)' },
5771
- disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
5772
- ...focusRing,
5773
- },
5774
- selectors: {
5775
- '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)' },
5776
- [`&${rtl}`]: { scale: '-1 1' },
5777
- },
5778
- },
5779
- 'next-trigger': {
5780
- base: {
5781
- display: 'inline-flex',
5782
- alignItems: 'center',
5783
- justifyContent: 'center',
5784
- minInlineSize: 'var(--pg-size)',
5785
- blockSize: 'var(--pg-size)',
5786
- background: 'var(--color-base-200)',
5787
- color: 'var(--color-base-content)',
5788
- border: 'none',
5789
- borderRadius: 'var(--radius-field)',
5790
- fontSize: 'calc(var(--pg-font) * 1.2)',
5791
- lineHeight: 'var(--leading-none)',
5792
- appearance: 'none',
5793
- cursor: 'pointer',
5794
- transition: 'background var(--duration-fast) var(--ease-standard)',
5795
- },
5796
- states: {
5797
- hover: { background: 'var(--color-base-300)' },
5798
- disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
5799
- ...focusRing,
5800
- },
5801
- selectors: {
5802
- '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)' },
5803
- [`&${rtl}`]: { scale: '-1 1' },
5804
- },
5805
- },
6803
+ 'first-trigger': pageTrigger,
6804
+ 'prev-trigger': pageTrigger,
6805
+ 'next-trigger': pageTrigger,
6806
+ 'last-trigger': pageTrigger,
5806
6807
  },
5807
6808
  variants: {
5808
6809
  color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
@@ -5840,6 +6841,20 @@ export const pagination = {
5840
6841
  */
5841
6842
  const stepInk = (accent, role) => `color-mix(in oklab, ${accent} `
5842
6843
  + `${Math.min(ROLE_INK_KEEP[role] ?? 55, 70)}%, var(--color-base-content))`;
6844
+ /** A Steps root holding wizard parts (#296) — the panel or the triggers. */
6845
+ const STEPS_WIZARD = '&:has(> [data-scope="steps"]:is([data-part="content"], [data-part="prev-trigger"], [data-part="next-trigger"]))';
6846
+ /** Steps' Back/Next (#296): the neutral daisy btn, with its 1px sink. */
6847
+ const stepsTrigger = {
6848
+ base: { ...btn, flex: '0 0 auto', alignSelf: 'flex-start' },
6849
+ states: {
6850
+ hover: { background: 'var(--color-base-300)' },
6851
+ disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
6852
+ ...focusRing,
6853
+ },
6854
+ selectors: {
6855
+ '&[data-pressed]:not([data-disabled])': { transform: 'translateY(1px)' },
6856
+ },
6857
+ };
5843
6858
  /**
5844
6859
  * Steps — daisy's steps translated to the richer rail: bold numbered discs
5845
6860
  * on base-200, the walked disc and line refilled with the accent pair
@@ -5866,6 +6881,10 @@ export const steps = {
5866
6881
  },
5867
6882
  selectors: {
5868
6883
  '&[data-orientation="vertical"]': { flexDirection: 'column' },
6884
+ // A wizard (#296): the rail keeps its own line and the panel
6885
+ // and triggers wrap below it. Only then — a bare rail never
6886
+ // wraps, so its steps stay one row at any width.
6887
+ [STEPS_WIZARD]: { flexWrap: 'wrap', rowGap: 'var(--space-md)' },
5869
6888
  },
5870
6889
  },
5871
6890
  /**
@@ -5904,6 +6923,10 @@ export const steps = {
5904
6923
  active: { color: 'var(--steps-accent-ink)', fontWeight: 'var(--weight-semibold)' },
5905
6924
  complete: { color: 'var(--color-base-content)' },
5906
6925
  inactive: { color: 'color-mix(in oklch, var(--color-base-content) 65%, transparent)' },
6926
+ // daisy's `step-error`, whatever the phase: after the phases,
6927
+ // so it wins at equal weight. `stepInk`, not `roleInk`: the
6928
+ // 85% error ink measured 3.79:1 on light's page as text.
6929
+ invalid: { color: stepInk('var(--color-error)', 'error') },
5907
6930
  disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
5908
6931
  ...focusRing,
5909
6932
  },
@@ -5935,6 +6958,7 @@ export const steps = {
5935
6958
  // the disc (#112).
5936
6959
  complete: { background: 'color-mix(in oklab, var(--steps-accent) 20%, var(--color-base-100))', color: 'var(--steps-complete-ink)' },
5937
6960
  inactive: { background: 'var(--color-base-200)', color: 'var(--color-base-content)' },
6961
+ invalid: { background: 'var(--color-error)', color: 'var(--color-error-content)' },
5938
6962
  },
5939
6963
  },
5940
6964
  /**
@@ -5966,6 +6990,7 @@ export const steps = {
5966
6990
  states: {
5967
6991
  complete: { background: 'var(--steps-accent)' },
5968
6992
  inactive: { background: 'var(--color-base-300)' },
6993
+ invalid: { background: 'var(--color-error)' },
5969
6994
  },
5970
6995
  },
5971
6996
  title: {
@@ -5980,6 +7005,25 @@ export const steps = {
5980
7005
  fontWeight: 'var(--weight-normal)',
5981
7006
  },
5982
7007
  },
7008
+ /**
7009
+ * The active step's panel: a full-width line under the rail (the
7010
+ * wizard wrap above). Inactive panels are `hidden`, so they need no
7011
+ * rule.
7012
+ */
7013
+ content: {
7014
+ base: {
7015
+ flex: '1 0 100%',
7016
+ fontSize: 'var(--text-md)',
7017
+ color: 'var(--color-base-content)',
7018
+ },
7019
+ states: { active: {}, inactive: {} },
7020
+ },
7021
+ // Back/Next are daisy btns; Next takes the far end of their line.
7022
+ 'prev-trigger': stepsTrigger,
7023
+ 'next-trigger': {
7024
+ ...stepsTrigger,
7025
+ base: { ...stepsTrigger.base, marginInlineStart: 'auto' },
7026
+ },
5983
7027
  },
5984
7028
  variants: {
5985
7029
  // Keyed on the ITEM, not the root (#112): the item re-carries
@@ -6163,6 +7207,9 @@ export const drawer = {
6163
7207
  */
6164
7208
  export const table = {
6165
7209
  component: 'table',
7210
+ // A sortable header cell carries the sort state for `aria-sort`'s sake;
7211
+ // its trigger and indicator paint it.
7212
+ skipStates: { 'header-cell': ['ascending', 'descending', 'none'] },
6166
7213
  // Public to a design system derived from this one (#73).
6167
7214
  hooks: {
6168
7215
  properties: {
@@ -6248,6 +7295,55 @@ export const table = {
6248
7295
  },
6249
7296
  at: tableStackAt(tokens, 'header-cell', { paddingInline: '0' }),
6250
7297
  },
7298
+ // Sorting (#286): daisy's header label made a button — the sorted
7299
+ // column's label takes the full accent ink, and the mark beside it
7300
+ // turns with the direction.
7301
+ 'sort-trigger': {
7302
+ base: {
7303
+ appearance: 'none',
7304
+ display: 'inline-flex',
7305
+ alignItems: 'center',
7306
+ margin: '0',
7307
+ border: '0',
7308
+ font: 'inherit',
7309
+ letterSpacing: 'inherit',
7310
+ textTransform: 'inherit',
7311
+ textAlign: 'inherit',
7312
+ cursor: 'pointer',
7313
+ gap: 'var(--space-2xs)',
7314
+ padding: '0',
7315
+ borderRadius: 'var(--radius-selector)',
7316
+ background: 'transparent',
7317
+ color: 'inherit',
7318
+ transition: 'color var(--duration-fast) var(--ease-standard)',
7319
+ },
7320
+ states: {
7321
+ ascending: { color: 'var(--table-accent)' },
7322
+ descending: { color: 'var(--table-accent)' },
7323
+ none: {},
7324
+ hover: { color: 'var(--table-accent)' },
7325
+ disabled: { cursor: 'not-allowed', opacity: 'var(--disabled-opacity)' },
7326
+ ...focusRing,
7327
+ },
7328
+ at: { 'reduced-motion': { base: { transition: 'none' } } },
7329
+ },
7330
+ // zero's ▲, turned for descending; an unsorted column shows it only
7331
+ // under a hovered or keyboard-focused trigger.
7332
+ 'sort-indicator': {
7333
+ base: {
7334
+ display: 'inline-block',
7335
+ fontSize: '0.75em',
7336
+ lineHeight: '1',
7337
+ transition: 'transform var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard)',
7338
+ },
7339
+ states: {
7340
+ ascending: {},
7341
+ descending: { transform: 'rotate(180deg)' },
7342
+ none: { opacity: '0' },
7343
+ },
7344
+ selectors: { '[data-scope="table"][data-part="sort-trigger"]:is(:hover, [data-focus-visible]) > &[data-state="none"]': { opacity: '0.6' } },
7345
+ at: { 'reduced-motion': { base: { transition: 'none' } } },
7346
+ },
6251
7347
  cell: {
6252
7348
  base: {
6253
7349
  padding: 'var(--table-pad-block) var(--table-pad-inline)',
@@ -6895,11 +7991,11 @@ export const diff = {
6895
7991
  },
6896
7992
  };
6897
7993
  export const recipes = [
6898
- tabs, collapsible, switchRecipe, dialog, popover, tooltip, menu,
6899
- field, checkbox, checkboxGroup, radioGroup, progress, slider, accordion, select, button, avatar, toast, combobox,
7994
+ tabs, collapsible, switchRecipe, dialog, popover, tooltip, hoverCard, menu, menubar,
7995
+ field, fieldset, checkbox, checkboxGroup, radioGroup, progress, slider, accordion, select, button, avatar, toast, combobox,
6900
7996
  toggle, toggleGroup, numberInput, ratingGroup, treeView, input, textarea,
6901
7997
  card, alert, emptyState, badge, divider, skeleton, spinner,
6902
- kbd, status, indicator, stats, timeline, chat, radialProgress, join,
7998
+ kbd, status, indicator, stats, timeline, chat, chatLog, radialProgress, join,
6903
7999
  navbar, navList, breadcrumbs, pagination, steps, drawer,
6904
8000
  table,
6905
8001
  fileUpload,