@sigx/zero-daisyui 0.2.0-beta.3 → 0.2.0-beta.4

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 (58) hide show
  1. package/README.md +8 -0
  2. package/dist/components.d.ts +3 -2
  3. package/dist/css/components/button.css +6 -0
  4. package/dist/css/components/tabs.css +191 -19
  5. package/dist/css/index.css +197 -19
  6. package/dist/lynx/components/alert.css +1 -1
  7. package/dist/lynx/components/avatar.css +22 -7
  8. package/dist/lynx/components/badge.css +1 -1
  9. package/dist/lynx/components/button.css +6 -1
  10. package/dist/lynx/components/checkbox.css +23 -9
  11. package/dist/lynx/components/combobox.css +2 -2
  12. package/dist/lynx/components/countdown.css +1 -1
  13. package/dist/lynx/components/dialog.css +3 -3
  14. package/dist/lynx/components/divider.css +18 -19
  15. package/dist/lynx/components/drawer.css +2 -2
  16. package/dist/lynx/components/file-upload.css +18 -7
  17. package/dist/lynx/components/indicator.css +1 -1
  18. package/dist/lynx/components/input.css +2 -2
  19. package/dist/lynx/components/join.css +1 -1
  20. package/dist/lynx/components/kbd.css +1 -1
  21. package/dist/lynx/components/menu.css +1 -1
  22. package/dist/lynx/components/native-select.css +1 -1
  23. package/dist/lynx/components/number-input.css +2 -2
  24. package/dist/lynx/components/pagination.css +92 -23
  25. package/dist/lynx/components/popover.css +2 -2
  26. package/dist/lynx/components/progress.css +17 -25
  27. package/dist/lynx/components/radial-progress.css +22 -24
  28. package/dist/lynx/components/radio-group.css +23 -9
  29. package/dist/lynx/components/rating-group.css +19 -23
  30. package/dist/lynx/components/select.css +2 -2
  31. package/dist/lynx/components/slider.css +3 -0
  32. package/dist/lynx/components/spinner.css +22 -19
  33. package/dist/lynx/components/status.css +27 -20
  34. package/dist/lynx/components/steps.css +67 -18
  35. package/dist/lynx/components/switch.css +79 -37
  36. package/dist/lynx/components/table.css +51 -8
  37. package/dist/lynx/components/tabs.css +222 -21
  38. package/dist/lynx/components/textarea.css +1 -1
  39. package/dist/lynx/components/timeline.css +27 -20
  40. package/dist/lynx/components/toggle-group.css +2 -2
  41. package/dist/lynx/components/toggle.css +1 -1
  42. package/dist/lynx/components/tooltip.css +1 -1
  43. package/dist/lynx/index.css +786 -318
  44. package/dist/lynx/manifest.json +33 -7
  45. package/dist/manifest.json +31 -5
  46. package/dist/recipes.d.ts +28 -0
  47. package/dist/recipes.d.ts.map +1 -1
  48. package/dist/recipes.js +465 -80
  49. package/dist/recipes.js.map +1 -1
  50. package/dist/register.d.ts +2 -3
  51. package/dist/report.json +401 -34
  52. package/dist/tokens.d.ts +17 -5
  53. package/dist/tokens.d.ts.map +1 -1
  54. package/dist/tokens.js +22 -8
  55. package/dist/tokens.js.map +1 -1
  56. package/package.json +4 -4
  57. package/src/recipes.ts +468 -81
  58. package/src/tokens.ts +24 -8
package/dist/recipes.js CHANGED
@@ -162,6 +162,34 @@ const withPresence = (presence, styles) => ({
162
162
  selectors: mergeKeyed(presence.selectors, styles.selectors),
163
163
  at: Object.fromEntries([...new Set([...Object.keys(presence.at ?? {}), ...Object.keys(styles.at ?? {})])].map((key) => [key, withPresence(presence.at?.[key] ?? {}, styles.at?.[key] ?? {})])),
164
164
  });
165
+ /**
166
+ * daisy's three tab flavors (#377): `tabs-border` — daisy's own default look,
167
+ * an underline under the active tab — plus `tabs-lift` (bordered tab lifting
168
+ * out of a baseline) and `tabs-box` (list on a base-200 surface, active tab a
169
+ * raised base-100 pill). All three ported from daisyUI 5.5.19
170
+ * `components/tab.css`.
171
+ *
172
+ * The base holds only flavor-neutral structure. Every flavor's chrome —
173
+ * including the box surface this recipe used to keep in `parts` — lives in
174
+ * its own `variant` entry, because the web emitter twins the DEFAULT value's
175
+ * rules as `:not([data-variant])`: a default that leaned on base styles would
176
+ * leak them into the other two flavors, and the box chrome in base is exactly
177
+ * what `variant="border"` must not inherit.
178
+ *
179
+ * Sizing goes through a `--tab-py`/`--tab-px` pair rather than a literal
180
+ * `padding` per size — daisy's own mechanism (`--tab-p`/`--tab-paddings` in
181
+ * tab.css). It is what lets `lift` move `var(--border)` between border and
182
+ * padding without restating the size ramp per flavor: axis rules all target
183
+ * the same part with equal specificity, so a `lift` padding literal would
184
+ * either lose to or beat the size axis by source order alone.
185
+ *
186
+ * variant × color: `color` paints the active tab's INK (`roleInk`, not the
187
+ * raw role — #210: raw roles are fill colours with no contrast floor as ink).
188
+ * `border`'s underline and `lift`'s label follow it via `currentColor`;
189
+ * `box` is the one flavor where daisy fills the active pill with the role
190
+ * (`checked:bg-primary checked:text-primary-content` in its docs), so a
191
+ * compound per role restores the fill+`-content` pair there.
192
+ */
165
193
  export const tabs = {
166
194
  component: 'tabs',
167
195
  parts: {
@@ -172,10 +200,6 @@ export const tabs = {
172
200
  base: {
173
201
  display: 'inline-flex',
174
202
  alignSelf: 'flex-start',
175
- padding: 'var(--space-xs)',
176
- gap: 'var(--space-xs)',
177
- background: 'var(--color-base-200)',
178
- borderRadius: 'var(--radius-field)',
179
203
  },
180
204
  },
181
205
  tab: {
@@ -183,21 +207,31 @@ export const tabs = {
183
207
  appearance: 'none',
184
208
  border: 'none',
185
209
  background: 'transparent',
186
- padding: 'var(--space-sm) var(--space-xl)',
210
+ '--tab-py': 'var(--space-sm)',
211
+ '--tab-px': 'var(--space-xl)',
212
+ // daisy's per-size underline/corner cap: `--tab-radius-min`
213
+ // in tab.css, `calc(.75rem - var(--border))` at md. Declared
214
+ // here (not in `lift`, its only reader) so the size axis can
215
+ // override it — a definition inside the variant entry would
216
+ // beat the size rules on source order.
217
+ '--tab-radius-min': 'calc(0.75rem - var(--border))',
218
+ padding: 'var(--tab-py) var(--tab-px)',
187
219
  fontSize: 'var(--text-sm)',
188
220
  fontWeight: 'var(--weight-semibold)',
189
221
  color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)',
190
- borderRadius: 'calc(var(--radius-field) - 0.25rem)',
191
222
  cursor: 'pointer',
223
+ // For the flavors' ::before marks (border's underline, lift's
224
+ // corner shoulders) — daisy's base `.tab` is `position:
225
+ // relative` for the same reason.
226
+ position: 'relative',
192
227
  transition: 'background var(--duration-normal) var(--ease-standard), color var(--duration-normal) var(--ease-standard)',
193
228
  },
194
229
  states: {
195
230
  hover: { color: 'var(--color-base-content)' },
196
- active: {
197
- background: 'var(--color-base-100)',
198
- color: 'var(--color-base-content)',
199
- boxShadow: 'var(--shadow-sm)',
200
- },
231
+ // Full-strength ink is the one active mark all three flavors
232
+ // share; each flavor's own chrome (underline, lift, pill)
233
+ // lives in its `variant` entry.
234
+ active: { color: 'var(--color-base-content)' },
201
235
  inactive: {},
202
236
  disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
203
237
  ...focusRing,
@@ -210,38 +244,225 @@ export const tabs = {
210
244
  },
211
245
  variants: {
212
246
  size: {
213
- xs: { tab: { base: { fontSize: 'var(--text-xs)', padding: 'var(--space-2xs) var(--space-md)' } } },
214
- sm: { tab: { base: { fontSize: 'var(--text-xs)', padding: 'var(--space-xs) var(--space-lg)' } } },
247
+ xs: { tab: { base: { fontSize: 'var(--text-xs)', '--tab-py': 'var(--space-2xs)', '--tab-px': 'var(--space-md)', '--tab-radius-min': 'calc(0.5rem - var(--border))' } } },
248
+ sm: { tab: { base: { fontSize: 'var(--text-xs)', '--tab-py': 'var(--space-xs)', '--tab-px': 'var(--space-lg)', '--tab-radius-min': 'calc(0.5rem - var(--border))' } } },
215
249
  // `md` is the un-attributed render: the base already IS the
216
250
  // middle step, so restating it here would be a second copy free
217
251
  // to drift. An empty entry emits no rule and keeps the base.
218
252
  md: {},
219
- lg: { tab: { base: { fontSize: 'var(--text-md)', padding: 'var(--space-md) var(--space-2xl)' } } },
220
- xl: { tab: { base: { fontSize: 'var(--text-lg)', padding: 'var(--space-lg) var(--space-2xl)' } } },
253
+ lg: { tab: { base: { fontSize: 'var(--text-md)', '--tab-py': 'var(--space-md)', '--tab-px': 'var(--space-2xl)', '--tab-radius-min': 'calc(1.5rem - var(--border))' } } },
254
+ xl: { tab: { base: { fontSize: 'var(--text-lg)', '--tab-py': 'var(--space-lg)', '--tab-px': 'var(--space-2xl)', '--tab-radius-min': 'calc(2rem - var(--border))' } } },
221
255
  },
222
256
  // Every role, not just primary. `data-color` passes through whatever a
223
257
  // consumer sets, so a one-role axis made `<Tabs.Root color="success">`
224
258
  // type-check, emit the attribute, and match nothing — the tab just
225
259
  // stayed primary with no diagnostic anywhere.
260
+ //
261
+ // The role lands as the active tab's INK — `border`'s underline and
262
+ // everything drawn `currentColor` follow it for free. `box` refines
263
+ // this per role through `compoundVariants` below (fill + `-content`).
226
264
  color: Object.fromEntries(ROLES.map((c) => [
227
265
  c,
228
266
  {
229
267
  tab: {
268
+ states: {
269
+ active: { color: roleInk(c) },
270
+ },
271
+ },
272
+ },
273
+ ])),
274
+ variant: {
275
+ // daisy `.tabs-border`: an always-present ::before bar under each
276
+ // tab — transparent at rest (daisy reaches transparent through an
277
+ // invalid-at-computed-value-time 4-value custom property; the
278
+ // rendered result is the same), currentColor + a 3px top border
279
+ // when active. The border is daisy's forced-colors survival for a
280
+ // background-painted mark, kept verbatim so the bar's geometry
281
+ // matches daisy's exactly.
282
+ border: {
283
+ tab: {
284
+ base: { borderRadius: 'var(--radius-field)' },
285
+ selectors: {
286
+ '&::before': {
287
+ content: '""',
288
+ position: 'absolute',
289
+ bottom: '0',
290
+ insetInlineStart: '10%',
291
+ width: '80%',
292
+ // daisy renders under Tailwind preflight, whose
293
+ // `*,::before,::after { box-sizing: border-box }`
294
+ // makes the active `border-top: 3px` swallow the
295
+ // 3px height rather than stack on it. zero ships
296
+ // no such reset, so the bar carries the one
297
+ // declaration itself — without it the active
298
+ // underline doubles to 6px (review catch).
299
+ boxSizing: 'border-box',
300
+ height: '3px',
301
+ borderRadius: 'var(--radius-field)',
302
+ backgroundColor: 'transparent',
303
+ transition: 'background-color var(--duration-normal) var(--ease-standard)',
304
+ },
305
+ '&[data-state="active"]:not([data-disabled])::before': {
306
+ backgroundColor: 'currentColor',
307
+ borderTop: '3px solid',
308
+ },
309
+ },
310
+ },
311
+ },
312
+ // daisy `.tabs-lift`: every tab sits on a base-300 baseline; the
313
+ // active one swaps that bottom hairline for top/side borders, a
314
+ // base-100 fill, rounded top corners, and the radial-gradient
315
+ // "shoulders" that curve its bottom corners outward into the
316
+ // baseline. Widths and paddings trade `var(--border)` exactly as
317
+ // daisy's `--tab-border`/`--tab-paddings` do, so activating a tab
318
+ // never changes its box size.
319
+ lift: {
320
+ tab: {
321
+ base: {
322
+ '--tab-radius-limit': 'min(var(--radius-field), var(--tab-radius-min))',
323
+ borderStartStartRadius: 'var(--tab-radius-limit)',
324
+ borderStartEndRadius: 'var(--tab-radius-limit)',
325
+ borderWidth: '0 0 var(--border) 0',
326
+ borderStyle: 'solid',
327
+ borderColor: 'var(--color-base-300)',
328
+ padding: 'calc(var(--tab-py) + var(--border)) var(--tab-px) var(--tab-py)',
329
+ },
230
330
  states: {
231
331
  active: {
232
- background: `var(--color-${c})`,
233
- color: `var(--color-${c}-content)`,
332
+ background: 'var(--color-base-100)',
333
+ borderWidth: 'var(--border) var(--border) 0 var(--border)',
334
+ padding: 'var(--tab-py) calc(var(--tab-px) - var(--border)) calc(var(--tab-py) + var(--border))',
335
+ },
336
+ },
337
+ selectors: {
338
+ '&[data-state="active"]::before': {
339
+ // daisy's `--tab-radius-grad` stop list:
340
+ // transparent inside the corner arc, a base-300
341
+ // ring of the border's width, base-100 outside.
342
+ // Declared here rather than on the tab: every
343
+ // shoulder rule below styles this same
344
+ // pseudo-element, so they all read it — and on
345
+ // lynx it drops with the ::before selector
346
+ // instead of surviving as a dead declaration.
347
+ '--tab-grad': '#0000 calc(69% - var(--border)), var(--color-base-300) calc(69% - var(--border) + 0.25px), var(--color-base-300) 69%, var(--color-base-100) calc(69% + 0.25px)',
348
+ content: '""',
349
+ position: 'absolute',
350
+ zIndex: '1',
351
+ bottom: '0',
352
+ // daisy centres the shoulders through the flex
353
+ // static position; an explicit symmetric overhang
354
+ // of one corner radius per side is the same
355
+ // geometry without needing the tab to be a flex
356
+ // container.
357
+ insetInlineStart: 'calc(var(--tab-radius-limit) * -1)',
358
+ width: 'calc(100% + var(--tab-radius-limit) * 2)',
359
+ height: 'var(--tab-radius-limit)',
360
+ backgroundImage: 'radial-gradient(circle at top left, var(--tab-grad)), radial-gradient(circle at top right, var(--tab-grad))',
361
+ backgroundPosition: 'top left, top right',
362
+ backgroundSize: 'var(--tab-radius-limit) var(--tab-radius-limit)',
363
+ backgroundRepeat: 'no-repeat',
364
+ },
365
+ // No shoulder past the row's edges — daisy's
366
+ // `--radius-start: none` / `--radius-end: none` on
367
+ // first/last, both suppressed for an only tab.
368
+ '&[data-state="active"]:first-child::before': {
369
+ backgroundImage: 'none, radial-gradient(circle at top right, var(--tab-grad))',
370
+ },
371
+ '&[data-state="active"]:last-child::before': {
372
+ backgroundImage: 'radial-gradient(circle at top left, var(--tab-grad)), none',
373
+ },
374
+ '&[data-state="active"]:only-child::before': {
375
+ backgroundImage: 'none',
376
+ },
377
+ // The gradients' corners are physical; when one side
378
+ // is suppressed the image is asymmetric, so RTL needs
379
+ // daisy's mirror on exactly the first/last tabs.
380
+ [`&[data-state="active"]:first-child${rtl}::before`]: {
381
+ transform: 'rotateY(180deg)',
382
+ },
383
+ [`&[data-state="active"]:last-child${rtl}::before`]: {
384
+ transform: 'rotateY(180deg)',
234
385
  },
235
386
  },
236
387
  },
237
388
  },
238
- ])),
389
+ // daisy `.tabs-box`: the styles this recipe shipped as its only
390
+ // look, moved verbatim out of `parts` — list on a padded base-200
391
+ // surface, active tab a raised base-100 pill.
392
+ box: {
393
+ list: {
394
+ base: {
395
+ padding: 'var(--space-xs)',
396
+ gap: 'var(--space-xs)',
397
+ background: 'var(--color-base-200)',
398
+ borderRadius: 'var(--radius-field)',
399
+ },
400
+ },
401
+ tab: {
402
+ base: { borderRadius: 'calc(var(--radius-field) - 0.25rem)' },
403
+ states: {
404
+ active: {
405
+ background: 'var(--color-base-100)',
406
+ boxShadow: 'var(--shadow-sm)',
407
+ },
408
+ },
409
+ },
410
+ },
411
+ },
412
+ },
413
+ // box × color: daisy's box flavor fills the active pill with the role and
414
+ // flips the label to `-content` — the fill+pair form is contrast-safe by
415
+ // construction, so the raw role is right here (unlike the ink the single-
416
+ // axis rule paints). Emitted after the single-axis rules, so it wins the
417
+ // `color` tie on source order.
418
+ compoundVariants: ROLES.map((c) => ({
419
+ match: { variant: 'box', color: c },
420
+ parts: {
421
+ tab: {
422
+ states: {
423
+ active: {
424
+ background: `var(--color-${c})`,
425
+ color: `var(--color-${c}-content)`,
426
+ },
427
+ },
428
+ },
429
+ },
430
+ })),
431
+ // `md: {}` emits nothing — it states the size default for the manifest
432
+ // (signalxjs/lynx#1070). `border` is daisy's own default tab look, and a
433
+ // DELIBERATE web visual change (#377): the un-attributed render used to
434
+ // be the box pill. Deliberately no `color` default: the un-attributed
435
+ // active ink is base-content, outside the color vocabulary.
436
+ defaultVariants: { size: 'md', variant: 'border' },
437
+ targets: {
438
+ lynx: {
439
+ variants: {
440
+ variant: {
441
+ // The underline is a ::before on the web, which lynx
442
+ // drops — redrawn as a real bottom border on the tab
443
+ // itself (transparent at rest so activation never moves
444
+ // the row). `currentColor` keeps it following the color
445
+ // axis ink, the same mechanism switch's shared border
446
+ // already relies on for lynx.
447
+ border: {
448
+ tab: {
449
+ base: { borderBottom: '3px solid transparent' },
450
+ states: { active: { borderBottomColor: 'currentColor' } },
451
+ },
452
+ },
453
+ // `min()` does not exist on lynx (signalxjs/lynx#1066) and
454
+ // the shared `--tab-radius-limit` would drop, leaving its
455
+ // readers dangling — the uncapped field radius is the
456
+ // closest expressible value.
457
+ lift: {
458
+ tab: {
459
+ base: { '--tab-radius-limit': 'var(--radius-field)' },
460
+ },
461
+ },
462
+ },
463
+ },
464
+ },
239
465
  },
240
- // Size only: `md: {}` emits nothing, so this changes no CSS — it states
241
- // the default for the manifest (signalxjs/lynx#1070). Deliberately no
242
- // `color` default: the un-attributed active tab is a neutral base-100
243
- // pill, which is outside the color vocabulary.
244
- defaultVariants: { size: 'md' },
245
466
  };
246
467
  /**
247
468
  * The disclosure size ramp collapsible and accordion share (#321): the same
@@ -377,46 +598,21 @@ export const switchRecipe = {
377
598
  },
378
599
  },
379
600
  control: {
380
- base: {
381
- // The knob slides because the grid's leading column GROWS:
382
- // `0fr 1fr 1fr` → `1fr 1fr 0fr`, with the knob parked in
383
- // column 2. That is daisy's own mechanism, and unlike a
384
- // `translateX` it is RTL-correct for free.
385
- display: 'inline-grid',
386
- gridTemplateColumns: '0fr 1fr 1fr',
387
- placeContent: 'center',
388
- position: 'relative',
389
- verticalAlign: 'middle',
390
- flexShrink: '0',
391
- boxSizing: 'border-box',
392
- width: 'calc((var(--switch-size) * 2) - (var(--border) + var(--switch-p)) * 2)',
393
- height: 'var(--switch-size)',
394
- padding: 'var(--switch-p)',
395
- // daisy's radius formula: the selector radius GROWN by the
396
- // padding and the border so the knob's own corner and the
397
- // track's stay concentric, each clamped at 3× the token so a
398
- // square-cornered theme (cyberpunk, wireframe) stays square.
399
- '--switch-radius-max': 'calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector))',
400
- borderRadius: 'calc(var(--radius-selector) + min(var(--switch-p), var(--switch-radius-max)) + min(var(--border), var(--switch-radius-max)))',
401
- border: 'var(--border) solid currentColor',
402
- color: 'var(--switch-ink)',
403
- boxShadow: '0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset',
404
- userSelect: 'none',
405
- transition: 'color var(--duration-slow) var(--ease-standard), '
406
- + 'background-color var(--duration-normal) var(--ease-standard), '
407
- + 'grid-template-columns var(--duration-normal) var(--ease-standard)',
408
- },
601
+ // The track's whole box lives in `targets`: the web mechanism is
602
+ // an inline-grid whose leading column grows, spelled with grid
603
+ // and `min()` — both measured unresolvable by lynx's engine
604
+ // (signalxjs/lynx#1066), so the lynx section lays the same
605
+ // geometry out with flex + calc() instead. Whole blocks move
606
+ // (not single declarations) so the merged web view keeps its
607
+ // declaration order and the web goldens stay byte-identical.
409
608
  states: {
410
609
  // daisy's checked toggle empties its track to base-100 and paints
411
610
  // the KNOB with the accent — so the checked knob's contrast is
412
611
  // accent-on-base-100, not accent-content-on-accent. Same
413
612
  // trade-off as the checkbox's unchecked outline: 1.11:1 at worst
414
613
  // (sunset/`neutral`), 7.68 at best, daisy's own numbers.
415
- checked: {
416
- gridTemplateColumns: '1fr 1fr 0fr',
417
- color: 'var(--switch-accent)',
418
- backgroundColor: 'var(--color-base-100)',
419
- },
614
+ // Both targets restate the paint in their own spelling below.
615
+ checked: {},
420
616
  unchecked: {},
421
617
  // `invalid` is semantic: it stays error under every colour
422
618
  // variant. Stated as a longhand because the track's border is
@@ -428,24 +624,9 @@ export const switchRecipe = {
428
624
  },
429
625
  },
430
626
  thumb: {
431
- base: {
432
- // daisy's `.toggle:before`: a square knob that fills its grid
433
- // cell and takes the UNGROWN selector radius, so it is a
434
- // rounded square in daisy's rounded-square themes and a circle
435
- // only where the theme says so.
436
- gridRowStart: '1',
437
- gridColumnStart: '2',
438
- aspectRatio: '1',
439
- width: '100%',
440
- height: '100%',
441
- borderRadius: 'var(--radius-selector)',
442
- backgroundColor: 'currentColor',
443
- backgroundSize: 'auto, calc(var(--noise) * 100%)',
444
- backgroundImage: 'none, var(--fx-noise)',
445
- boxShadow: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset, '
446
- + '0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000)',
447
- transition: 'background-color var(--duration-instant) var(--ease-standard)',
448
- },
627
+ // Like `control`, the knob's box is per-target: the web knob fills
628
+ // its grid cell, the lynx knob is an explicit calc() square that
629
+ // `translateX`es — see `targets` below.
449
630
  states: {
450
631
  checked: {},
451
632
  unchecked: {},
@@ -475,6 +656,146 @@ export const switchRecipe = {
475
656
  // control and its text. Declared rather than left implicit so the
476
657
  // delegation reads as a decision.
477
658
  skipStates: { root: ['focus-visible'] },
659
+ // ONE design decision, two spellings. The web track is daisy's own
660
+ // mechanism — an inline-grid whose leading column grows (`0fr 1fr 1fr` →
661
+ // `1fr 1fr 0fr`) with a min()-clamped radius — and lynx's engine resolves
662
+ // neither grid nor the comparison functions (measured on device,
663
+ // signalxjs/lynx#1066; the emitter refuses min()/max()/clamp() on that
664
+ // evidence). So the whole box moves here per target: the web section is
665
+ // the original spelling verbatim, the lynx section rebuilds the same
666
+ // geometry with flex + calc() and slides the knob with `translateX`.
667
+ targets: {
668
+ web: {
669
+ parts: {
670
+ control: {
671
+ base: {
672
+ // The knob slides because the grid's leading column GROWS:
673
+ // `0fr 1fr 1fr` → `1fr 1fr 0fr`, with the knob parked in
674
+ // column 2. That is daisy's own mechanism, and unlike a
675
+ // `translateX` it is RTL-correct for free.
676
+ display: 'inline-grid',
677
+ gridTemplateColumns: '0fr 1fr 1fr',
678
+ placeContent: 'center',
679
+ position: 'relative',
680
+ verticalAlign: 'middle',
681
+ flexShrink: '0',
682
+ boxSizing: 'border-box',
683
+ width: 'calc((var(--switch-size) * 2) - (var(--border) + var(--switch-p)) * 2)',
684
+ height: 'var(--switch-size)',
685
+ padding: 'var(--switch-p)',
686
+ // daisy's radius formula: the selector radius GROWN by the
687
+ // padding and the border so the knob's own corner and the
688
+ // track's stay concentric, each clamped at 3× the token so a
689
+ // square-cornered theme (cyberpunk, wireframe) stays square.
690
+ '--switch-radius-max': 'calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector))',
691
+ borderRadius: 'calc(var(--radius-selector) + min(var(--switch-p), var(--switch-radius-max)) + min(var(--border), var(--switch-radius-max)))',
692
+ border: 'var(--border) solid currentColor',
693
+ color: 'var(--switch-ink)',
694
+ boxShadow: '0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset',
695
+ userSelect: 'none',
696
+ transition: 'color var(--duration-slow) var(--ease-standard), '
697
+ + 'background-color var(--duration-normal) var(--ease-standard), '
698
+ + 'grid-template-columns var(--duration-normal) var(--ease-standard)',
699
+ },
700
+ states: {
701
+ checked: {
702
+ gridTemplateColumns: '1fr 1fr 0fr',
703
+ color: 'var(--switch-accent)',
704
+ backgroundColor: 'var(--color-base-100)',
705
+ },
706
+ },
707
+ },
708
+ thumb: {
709
+ base: {
710
+ // daisy's `.toggle:before`: a square knob that fills its grid
711
+ // cell and takes the UNGROWN selector radius, so it is a
712
+ // rounded square in daisy's rounded-square themes and a circle
713
+ // only where the theme says so.
714
+ gridRowStart: '1',
715
+ gridColumnStart: '2',
716
+ aspectRatio: '1',
717
+ width: '100%',
718
+ height: '100%',
719
+ borderRadius: 'var(--radius-selector)',
720
+ backgroundColor: 'currentColor',
721
+ backgroundSize: 'auto, calc(var(--noise) * 100%)',
722
+ backgroundImage: 'none, var(--fx-noise)',
723
+ boxShadow: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset, '
724
+ + '0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000)',
725
+ transition: 'background-color var(--duration-instant) var(--ease-standard)',
726
+ },
727
+ },
728
+ },
729
+ },
730
+ lynx: {
731
+ parts: {
732
+ root: {
733
+ // `inline-flex` has no lynx projection; restated as plain
734
+ // flex with the direction explicit (lynx does not default
735
+ // it the way a browser does).
736
+ base: { display: 'flex', flexDirection: 'row', alignItems: 'center' },
737
+ },
738
+ control: {
739
+ base: {
740
+ display: 'flex',
741
+ alignItems: 'center',
742
+ flexShrink: '0',
743
+ boxSizing: 'border-box',
744
+ // Same geometry as the web track: daisy's derived
745
+ // width, ONE `--switch-size` driving everything.
746
+ width: 'calc((var(--switch-size) * 2) - (var(--border) + var(--switch-p)) * 2)',
747
+ height: 'var(--switch-size)',
748
+ padding: 'var(--switch-p)',
749
+ // The radius formula minus its min() clamps: exact for
750
+ // every shipped theme (the clamp only bites when the
751
+ // padding or border exceeds 3× the selector radius,
752
+ // i.e. near-square themes this package does not ship).
753
+ borderRadius: 'calc(var(--radius-selector) + var(--switch-p) + var(--border))',
754
+ // No currentColor on this target — the ink is named
755
+ // directly, and `checked` moves border + fill below.
756
+ border: 'var(--border) solid var(--switch-ink)',
757
+ userSelect: 'none',
758
+ transition: 'border-color var(--duration-slow) var(--ease-standard), '
759
+ + 'background-color var(--duration-normal) var(--ease-standard)',
760
+ },
761
+ states: {
762
+ checked: {
763
+ borderColor: 'var(--switch-accent)',
764
+ backgroundColor: 'var(--color-base-100)',
765
+ },
766
+ },
767
+ },
768
+ thumb: {
769
+ base: {
770
+ // The web knob fills its square grid cell; here the
771
+ // same square is explicit: track height minus border
772
+ // and padding on both sides.
773
+ width: 'calc(var(--switch-size) - (var(--border) + var(--switch-p)) * 2)',
774
+ height: 'calc(var(--switch-size) - (var(--border) + var(--switch-p)) * 2)',
775
+ borderRadius: 'var(--radius-selector)',
776
+ backgroundColor: 'var(--switch-ink)',
777
+ transition: 'transform var(--duration-normal) var(--ease-standard), '
778
+ + 'background-color var(--duration-instant) var(--ease-standard)',
779
+ },
780
+ states: {
781
+ // The knob's travel is exactly one knob-width: the
782
+ // content box is two knobs wide, and the knob starts
783
+ // at the near end. `translateX` is measured working
784
+ // on lynx. The axis is PHYSICAL where the web's
785
+ // growing grid column was logical — an RTL layout
786
+ // would need a direction-aware multiplier, but the
787
+ // lynx grammar carries no direction hook yet and the
788
+ // runtime does not stamp one; revisit with the RTL
789
+ // plumbing rather than guessing at it here.
790
+ checked: {
791
+ backgroundColor: 'var(--switch-accent)',
792
+ transform: 'translateX(calc(var(--switch-size) - (var(--border) + var(--switch-p)) * 2))',
793
+ },
794
+ },
795
+ },
796
+ },
797
+ },
798
+ },
478
799
  };
479
800
  // --------------------------------------------------------------------------
480
801
  // 2. checkbox — the three clip-paths, the fallback helper, then the recipe
@@ -1439,6 +1760,50 @@ export const slider = {
1439
1760
  // declared for the manifest (signalxjs/lynx#1070).
1440
1761
  defaultVariants: { color: 'primary', size: 'md' },
1441
1762
  skipStates: { root: ['invalid', 'focus-visible'] },
1763
+ // ONE design decision, two paints. On the web the accent reaches the
1764
+ // screen twice: the native control is UA-drawn behind `accent-color`, and
1765
+ // the composed range/thumb deepen the accent with `color-mix()`. Lynx
1766
+ // renders neither spelling — `accent-color` styles a native
1767
+ // `<input type=range>` lynx does not have, and the emitter drops
1768
+ // `color-mix()` as unresolvable — so the compiled range and thumb shipped
1769
+ // with no paint at all: grey track, invisible fill, invisible knob
1770
+ // (measured on device, signalxjs/lynx#1075). The geometry all survives
1771
+ // (the sizes are direct calc(), proven working); paint is the only gap,
1772
+ // so this section only restates paint, in lynx-expressible spellings.
1773
+ targets: {
1774
+ lynx: {
1775
+ parts: {
1776
+ range: {
1777
+ // The web fill deepens the accent 90/10 toward
1778
+ // base-content (`progressFill`'s recipe); the plain
1779
+ // accent — a var() chain, proven working on device — is
1780
+ // the closest lynx-expressible equivalent.
1781
+ base: { background: 'var(--slider-accent)' },
1782
+ },
1783
+ thumb: {
1784
+ // daisy's real range thumb, ported from daisyUI 5
1785
+ // `range.css` `::-webkit-slider-thumb`. The source reads
1786
+ // `background-color: var(--range-thumb)` +
1787
+ // `border: var(--range-p) solid` in the range's
1788
+ // currentColor; here each resolves to its value —
1789
+ // `--range-thumb` defaults to `var(--color-base-100)`,
1790
+ // `--range-p` is daisy's literal `.25rem` (deliberately
1791
+ // not the theme's size token), and the border ink is the
1792
+ // accent (currentColor on a daisy `.range-*` IS the
1793
+ // accent role) — because on lynx a named value is a
1794
+ // var-chain fewer to resolve. daisy paints the progress
1795
+ // fill itself as a 100cqw box-shadow thrown off this
1796
+ // thumb — a native-input trick with no lynx equivalent;
1797
+ // the composed `.zx-slider__range` box above IS that
1798
+ // fill here, so the knob only needs its own two paints.
1799
+ base: {
1800
+ background: 'var(--color-base-100)',
1801
+ border: '0.25rem solid var(--slider-accent)',
1802
+ },
1803
+ },
1804
+ },
1805
+ },
1806
+ },
1442
1807
  };
1443
1808
  export const accordion = {
1444
1809
  component: 'accordion',
@@ -1652,6 +2017,21 @@ export const select = {
1652
2017
  // so the twins restate it; declared for the manifest (signalxjs/lynx#1070).
1653
2018
  defaultVariants: { color: 'primary', size: 'md' },
1654
2019
  };
2020
+ /**
2021
+ * daisy's BUTTON ramp — `.btn`'s fixed height per size step, in `--size-field`
2022
+ * units, exactly as daisyUI 5 ships it (`components/button.css`: `.btn-xs`
2023
+ * `--size: calc(var(--size-field, .25rem) * 6)` through `.btn-xl` `* 14`,
2024
+ * `.btn`/`.btn-md` `* 10` — 1.5 / 2 / 2.5 / 3 / 3.5rem at the default
2025
+ * `--size-field: 0.25rem`).
2026
+ *
2027
+ * Its own table, not `FIELD_STEPS`: this repo's field ramp sits one step
2028
+ * higher than daisy's (md = 12 units, 3rem), and restating the button off it
2029
+ * would bake that offset into a second component. The button states daisy's
2030
+ * own numbers; #376 is exactly what happened while it had no height at all —
2031
+ * padding alone rendered md at ~30px against daisy's 40px.
2032
+ */
2033
+ const BTN_STEPS = { xs: 6, sm: 8, md: 10, lg: 12, xl: 14 };
2034
+ const btnHeight = (step) => `calc(var(--size-field) * ${BTN_STEPS[step]})`;
1655
2035
  export const button = {
1656
2036
  component: 'button',
1657
2037
  // The two axes meet here instead of multiplying. `color` sets the accent
@@ -1787,12 +2167,17 @@ export const button = {
1787
2167
  },
1788
2168
  },
1789
2169
  },
2170
+ // daisy sizes a btn by fixed height (`--size`), not by padding: each
2171
+ // step states its `BTN_STEPS` height and keeps the padding/font ramps
2172
+ // for inline air; the root's flex centering absorbs the block axis.
2173
+ // The `<button>` element is UA `border-box`, so the stated height is
2174
+ // the rendered height, border included — as in daisy.
1790
2175
  size: {
1791
- xs: { root: { base: { padding: 'var(--space-2xs) var(--space-xs)', fontSize: 'var(--text-xs)' } } },
1792
- sm: { root: { base: { padding: 'var(--space-xs) var(--space-sm)', fontSize: 'var(--text-sm)' } } },
1793
- md: { root: { base: { padding: 'var(--space-sm) var(--space-lg)', fontSize: 'var(--text-md)' } } },
1794
- lg: { root: { base: { padding: 'var(--space-md) var(--space-xl)', fontSize: 'var(--text-lg)' } } },
1795
- xl: { root: { base: { padding: 'var(--space-lg) var(--space-2xl)', fontSize: 'var(--text-xl)' } } },
2176
+ xs: { root: { base: { height: btnHeight('xs'), padding: 'var(--space-2xs) var(--space-xs)', fontSize: 'var(--text-xs)' } } },
2177
+ sm: { root: { base: { height: btnHeight('sm'), padding: 'var(--space-xs) var(--space-sm)', fontSize: 'var(--text-sm)' } } },
2178
+ md: { root: { base: { height: btnHeight('md'), padding: 'var(--space-sm) var(--space-lg)', fontSize: 'var(--text-md)' } } },
2179
+ lg: { root: { base: { height: btnHeight('lg'), padding: 'var(--space-md) var(--space-xl)', fontSize: 'var(--text-lg)' } } },
2180
+ xl: { root: { base: { height: btnHeight('xl'), padding: 'var(--space-lg) var(--space-2xl)', fontSize: 'var(--text-xl)' } } },
1796
2181
  },
1797
2182
  },
1798
2183
  /**