@sigx/zero-daisyui 0.5.0 → 0.7.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.
- package/README.md +62 -3
- package/dist/audit.json +8018 -2748
- package/dist/components.d.ts +23 -2
- package/dist/components.js +2 -0
- package/dist/css/components/accordion.css +26 -2
- package/dist/css/components/alert.css +2 -0
- package/dist/css/components/avatar.css +2 -0
- package/dist/css/components/badge.css +2 -0
- package/dist/css/components/box.css +2 -0
- package/dist/css/components/breadcrumbs.css +2 -0
- package/dist/css/components/button.css +2 -0
- package/dist/css/components/card.css +2 -0
- package/dist/css/components/carousel.css +2 -0
- package/dist/css/components/center.css +2 -0
- package/dist/css/components/chat.css +2 -0
- package/dist/css/components/checkbox-group.css +98 -0
- package/dist/css/components/checkbox.css +10 -0
- package/dist/css/components/collapsible.css +16 -2
- package/dist/css/components/combobox.css +36 -1
- package/dist/css/components/container.css +2 -0
- package/dist/css/components/countdown.css +2 -0
- package/dist/css/components/dialog.css +2 -0
- package/dist/css/components/diff.css +7 -0
- package/dist/css/components/divider.css +2 -0
- package/dist/css/components/drawer.css +2 -0
- package/dist/css/components/empty-state.css +2 -0
- package/dist/css/components/field.css +2 -0
- package/dist/css/components/fieldset.css +91 -0
- package/dist/css/components/file-upload.css +34 -0
- package/dist/css/components/grid.css +2 -0
- package/dist/css/components/indicator.css +2 -0
- package/dist/css/components/input.css +137 -0
- package/dist/css/components/join.css +2 -0
- package/dist/css/components/kbd.css +2 -0
- package/dist/css/components/menu.css +68 -0
- package/dist/css/components/nav-list.css +2 -0
- package/dist/css/components/navbar.css +2 -0
- package/dist/css/components/number-input.css +2 -0
- package/dist/css/components/pagination.css +6 -0
- package/dist/css/components/popover.css +74 -0
- package/dist/css/components/progress.css +2 -4
- package/dist/css/components/radial-progress.css +2 -0
- package/dist/css/components/radio-group.css +10 -4
- package/dist/css/components/rating-group.css +2 -0
- package/dist/css/components/select.css +54 -1
- package/dist/css/components/skeleton.css +2 -0
- package/dist/css/components/slider.css +154 -25
- package/dist/css/components/spacer.css +2 -0
- package/dist/css/components/spinner.css +2 -0
- package/dist/css/components/stack.css +2 -0
- package/dist/css/components/stats.css +68 -16
- package/dist/css/components/status.css +2 -0
- package/dist/css/components/steps.css +2 -0
- package/dist/css/components/swap.css +2 -0
- package/dist/css/components/switch.css +23 -0
- package/dist/css/components/table.css +77 -0
- package/dist/css/components/tabs.css +15 -0
- package/dist/css/components/textarea.css +2 -0
- package/dist/css/components/timeline.css +3 -0
- package/dist/css/components/toast.css +2 -0
- package/dist/css/components/toggle-group.css +2 -0
- package/dist/css/components/toggle.css +2 -0
- package/dist/css/components/tooltip.css +67 -0
- package/dist/css/components/tree-view.css +6 -0
- package/dist/css/index.css +1053 -70
- package/dist/css/tokens.css +13 -13
- package/dist/lynx/components/accordion.css +9 -0
- package/dist/lynx/components/alert.css +20 -0
- package/dist/lynx/components/button.css +58 -1
- package/dist/lynx/components/checkbox-group.css +209 -0
- package/dist/lynx/components/checkbox.css +8 -0
- package/dist/lynx/components/combobox.css +46 -0
- package/dist/lynx/components/dialog.css +19 -0
- package/dist/lynx/components/diff.css +5 -0
- package/dist/lynx/components/drawer.css +4 -0
- package/dist/lynx/components/fieldset.css +219 -0
- package/dist/lynx/components/file-upload.css +47 -0
- package/dist/lynx/components/input.css +178 -0
- package/dist/lynx/components/number-input.css +8 -0
- package/dist/lynx/components/pagination.css +16 -0
- package/dist/lynx/components/popover.css +37 -0
- package/dist/lynx/components/progress.css +4 -24
- package/dist/lynx/components/radio-group.css +8 -0
- package/dist/lynx/components/rating-group.css +3 -3
- package/dist/lynx/components/select.css +70 -1
- package/dist/lynx/components/slider.css +391 -28
- package/dist/lynx/components/stats.css +146 -16
- package/dist/lynx/components/steps.css +4 -0
- package/dist/lynx/components/switch.css +6 -0
- package/dist/lynx/components/table.css +57 -0
- package/dist/lynx/components/tabs.css +115 -93
- package/dist/lynx/components/timeline.css +84 -1
- package/dist/lynx/components/toast.css +41 -6
- package/dist/lynx/components/toggle.css +5 -0
- package/dist/lynx/components/tree-view.css +4 -0
- package/dist/lynx/index.css +1912 -262
- package/dist/lynx/manifest.json +67 -5
- package/dist/lynx/tokens.css +4 -4
- package/dist/manifest.json +65 -3
- package/dist/recipes.d.ts +17 -0
- package/dist/recipes.d.ts.map +1 -1
- package/dist/recipes.js +1141 -110
- package/dist/recipes.js.map +1 -1
- package/dist/register.d.ts +18 -0
- package/dist/report.json +902 -270
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +18 -4
- package/dist/tokens.js.map +1 -1
- package/package.json +5 -5
- package/src/recipes.ts +1156 -111
- package/src/tokens.ts +18 -4
package/src/recipes.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* value of that data.
|
|
6
6
|
*/
|
|
7
7
|
import type { CssProps, PartStyles, RecipeInput } from '@sigx/zero-kit';
|
|
8
|
-
import { axisRoles, tableStackAt } from '@sigx/zero-kit/define';
|
|
8
|
+
import { axisRoles, popupArrow, popupArrowHost, tableStackAt } from '@sigx/zero-kit/define';
|
|
9
9
|
import { roles, tokens } from './tokens.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -155,14 +155,48 @@ const disclosurePresence: PartStyles = {
|
|
|
155
155
|
'&::details-content': {
|
|
156
156
|
blockSize: '0',
|
|
157
157
|
overflow: 'hidden',
|
|
158
|
+
},
|
|
159
|
+
// Open only: a close removes `open`, which hides the content before
|
|
160
|
+
// any transition could run — the panel animates that half (#276).
|
|
161
|
+
'&[open]::details-content': {
|
|
162
|
+
blockSize: 'auto',
|
|
158
163
|
transition: 'block-size var(--duration-normal) var(--ease-standard), '
|
|
159
|
-
|
|
164
|
+
+ 'content-visibility var(--duration-normal) allow-discrete',
|
|
160
165
|
},
|
|
161
|
-
'&[open]::details-content': { blockSize: 'auto' },
|
|
162
166
|
},
|
|
163
|
-
at: { 'reduced-motion': { selectors: { '
|
|
167
|
+
at: { 'reduced-motion': { selectors: { '&[open]::details-content': { transition: 'none' } } } },
|
|
164
168
|
};
|
|
165
169
|
|
|
170
|
+
/**
|
|
171
|
+
* The panel's close (#276). Removing `open` hides a `<details>` at once, so
|
|
172
|
+
* the close cannot live on `::details-content` the way the open does: zero
|
|
173
|
+
* flips the panel to `closed` first and holds the element open until the
|
|
174
|
+
* panel's own animation has played. The runtime publishes the panel's
|
|
175
|
+
* measured height as `--<scope>-panel-height`, which is the one value this
|
|
176
|
+
* needs — `auto` is not an endpoint any engine but Chromium can animate.
|
|
177
|
+
* `border-box` so the measured height (content + padding) is the whole box.
|
|
178
|
+
* Web-only: the property is written by the DOM runtime (`RUNTIME_PROPERTIES`).
|
|
179
|
+
*/
|
|
180
|
+
const disclosureExit = (scope: 'collapsible' | 'accordion'): RecipeInput['targets'] => ({
|
|
181
|
+
web: {
|
|
182
|
+
parts: {
|
|
183
|
+
panel: {
|
|
184
|
+
states: {
|
|
185
|
+
closed: {
|
|
186
|
+
boxSizing: 'border-box',
|
|
187
|
+
animation: `${scope}-panel-exit var(--duration-normal) var(--ease-standard)`,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
at: { 'reduced-motion': { states: { closed: { animation: 'none' } } } },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
keyframes: {
|
|
194
|
+
[`${scope}-panel-exit`]:
|
|
195
|
+
`from { block-size: var(--${scope}-panel-height); } to { block-size: 0; padding-block: 0; }`,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
166
200
|
/**
|
|
167
201
|
* Merge presence into a part's own styles per KEY, not per block: a recipe
|
|
168
202
|
* that already writes `states: { open: {} }` — the "deliberately unstyled"
|
|
@@ -216,14 +250,33 @@ const withPresence = (presence: PartStyles, styles: PartStyles): PartStyles => (
|
|
|
216
250
|
*/
|
|
217
251
|
export const tabs: RecipeInput = {
|
|
218
252
|
component: 'tabs',
|
|
253
|
+
tokens: {
|
|
254
|
+
// How far the focus ring reaches outside a tab (2px + 2px offset):
|
|
255
|
+
// the list's scroll box pads by it so the ring is never clipped (#45).
|
|
256
|
+
'--tabs-ring-room': '4px',
|
|
257
|
+
},
|
|
219
258
|
parts: {
|
|
220
259
|
root: {
|
|
221
260
|
base: { display: 'flex', flexDirection: 'column', gap: 'var(--space-lg)' },
|
|
222
261
|
},
|
|
223
262
|
list: {
|
|
263
|
+
// The row's own scroll box (#45), Pagination's and Stats' answer:
|
|
264
|
+
// a row of labels wider than its column (a phone, a wide fallback
|
|
265
|
+
// face) scrolls instead of pushing the page sideways.
|
|
266
|
+
// `max-inline-size` (border-box, so it includes the padding)
|
|
267
|
+
// gives the shrink-to-fit list a width to scroll in. The padding is room for the focus ring, which the
|
|
268
|
+
// scroll box would otherwise clip at the row's edges; the
|
|
269
|
+
// matching scroll padding keeps it clear when focus scrolls a
|
|
270
|
+
// tab into view. The box variant's own padding (space-xs, the
|
|
271
|
+
// same 4px) replaces it there.
|
|
224
272
|
base: {
|
|
225
273
|
display: 'inline-flex',
|
|
226
274
|
alignSelf: 'flex-start',
|
|
275
|
+
boxSizing: 'border-box',
|
|
276
|
+
maxInlineSize: '100%',
|
|
277
|
+
overflowX: 'auto',
|
|
278
|
+
padding: 'var(--tabs-ring-room)',
|
|
279
|
+
scrollPaddingInline: 'var(--tabs-ring-room)',
|
|
227
280
|
},
|
|
228
281
|
},
|
|
229
282
|
tab: {
|
|
@@ -261,6 +314,12 @@ export const tabs: RecipeInput = {
|
|
|
261
314
|
...focusRing,
|
|
262
315
|
},
|
|
263
316
|
},
|
|
317
|
+
// daisy draws each flavor's active mark on the tab itself (the
|
|
318
|
+
// underline, the lift, the pill) and none of them slides, so a
|
|
319
|
+
// `Tabs.Indicator` renders nothing here (#283). On lynx, which has
|
|
320
|
+
// no pseudo-elements, the `border` flavor's underline is drawn on
|
|
321
|
+
// this part instead (see `targets.lynx`).
|
|
322
|
+
indicator: { base: { display: 'none' } },
|
|
264
323
|
panel: {
|
|
265
324
|
base: { fontSize: 'var(--text-md)' },
|
|
266
325
|
states: { active: {}, inactive: {} },
|
|
@@ -463,11 +522,17 @@ export const tabs: RecipeInput = {
|
|
|
463
522
|
targets: {
|
|
464
523
|
lynx: {
|
|
465
524
|
parts: {
|
|
466
|
-
//
|
|
467
|
-
//
|
|
525
|
+
// No hover on a touch platform: the held tab takes the ink
|
|
526
|
+
// daisy's hover gives it.
|
|
527
|
+
tab: { states: { pressed: { color: 'var(--color-base-content)' } } },
|
|
528
|
+
// The list is the indicator's containing block.
|
|
529
|
+
list: { base: { position: 'relative' } },
|
|
530
|
+
// The underline's ink (read by the `border` flavor below) —
|
|
531
|
+
// base-content by default, exactly what the shared
|
|
532
|
+
// `states.active.color` paints on the tab. One class, so any
|
|
468
533
|
// color-axis definition below (host + two classes, baked per
|
|
469
534
|
// theme) wins on specificity.
|
|
470
|
-
|
|
535
|
+
indicator: { base: { '--tab-active-ink': 'var(--color-base-content)' } },
|
|
471
536
|
},
|
|
472
537
|
variants: {
|
|
473
538
|
// `currentColor` never resolves on lynx (measured on device
|
|
@@ -478,19 +543,29 @@ export const tabs: RecipeInput = {
|
|
|
478
543
|
// once per theme — the same per-color/per-theme literal the
|
|
479
544
|
// active `color:` rules already land as.
|
|
480
545
|
color: Object.fromEntries(
|
|
481
|
-
ROLES.map((c) => [c, {
|
|
546
|
+
ROLES.map((c) => [c, { indicator: { base: { '--tab-active-ink': roleInk(c) } } }]),
|
|
482
547
|
),
|
|
483
548
|
variant: {
|
|
484
549
|
// The underline is a ::before on the web, which lynx
|
|
485
|
-
// drops
|
|
486
|
-
//
|
|
487
|
-
// the
|
|
488
|
-
//
|
|
489
|
-
//
|
|
550
|
+
// drops. It is redrawn on the `indicator` part (zero#324),
|
|
551
|
+
// which lynx-zero renders in every list and positions
|
|
552
|
+
// over the active tab from measured geometry (the web's
|
|
553
|
+
// `--tabs-indicator-*` properties are web-only). The part
|
|
554
|
+
// IS the tab's box, so daisy's bar is its bottom edge:
|
|
555
|
+
// 3px, 80% wide and centred, like `left: 10%; width: 80%`
|
|
556
|
+
// on the web. The ink rides `--tab-active-ink` — a plain
|
|
557
|
+
// var() chain, device-proven — instead of `currentColor`,
|
|
558
|
+
// which lynx never resolves.
|
|
490
559
|
border: {
|
|
491
|
-
|
|
492
|
-
base: {
|
|
493
|
-
|
|
560
|
+
indicator: {
|
|
561
|
+
base: {
|
|
562
|
+
display: 'flex',
|
|
563
|
+
boxSizing: 'border-box',
|
|
564
|
+
borderBottomWidth: '3px',
|
|
565
|
+
borderBottomStyle: 'solid',
|
|
566
|
+
borderBottomColor: 'var(--tab-active-ink)',
|
|
567
|
+
transform: 'scaleX(0.8)',
|
|
568
|
+
},
|
|
494
569
|
},
|
|
495
570
|
},
|
|
496
571
|
// `min()` does not exist on lynx (signalxjs/lynx#1066) and
|
|
@@ -536,6 +611,7 @@ const disclosureSizes: Record<string, Record<string, PartStyles>> = {
|
|
|
536
611
|
// daisy "collapse collapse-arrow" flavor.
|
|
537
612
|
export const collapsible: RecipeInput = {
|
|
538
613
|
component: 'collapsible',
|
|
614
|
+
targets: disclosureExit('collapsible'),
|
|
539
615
|
// Public to a design system derived from this one (#73).
|
|
540
616
|
hooks: {
|
|
541
617
|
properties: {
|
|
@@ -653,6 +729,8 @@ export const switchRecipe: RecipeInput = {
|
|
|
653
729
|
root: {
|
|
654
730
|
base: { display: 'inline-flex', alignItems: 'center', gap: 'var(--space-lg)', cursor: 'pointer' },
|
|
655
731
|
states: {
|
|
732
|
+
// Readonly answers to nothing, so it does not invite a click.
|
|
733
|
+
readonly: { cursor: 'default' },
|
|
656
734
|
disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
|
|
657
735
|
checked: {},
|
|
658
736
|
unchecked: {},
|
|
@@ -787,6 +865,29 @@ export const switchRecipe: RecipeInput = {
|
|
|
787
865
|
+ '0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000)',
|
|
788
866
|
transition: 'background-color var(--duration-instant) var(--ease-standard)',
|
|
789
867
|
},
|
|
868
|
+
at: {
|
|
869
|
+
// The knob is `currentColor` background plus shadows:
|
|
870
|
+
// forced colours revalue the one to Canvas and drop the
|
|
871
|
+
// others, leaving two empty pill outlines that read the
|
|
872
|
+
// same on and off (#189). Opt the knob out of forcing
|
|
873
|
+
// and paint it in system colours — CanvasText at rest,
|
|
874
|
+
// Highlight once checked. The noise and the shadows go
|
|
875
|
+
// too: unforced, they would mix the forced ink back in.
|
|
876
|
+
'forced-colors': {
|
|
877
|
+
base: {
|
|
878
|
+
forcedColorAdjust: 'none',
|
|
879
|
+
backgroundColor: 'CanvasText',
|
|
880
|
+
backgroundImage: 'none',
|
|
881
|
+
boxShadow: 'none',
|
|
882
|
+
},
|
|
883
|
+
states: { checked: { backgroundColor: 'Highlight' } },
|
|
884
|
+
// A disabled switch draws in GrayText, the forced palette's
|
|
885
|
+
// own disabled ink, not full-strength ink faded by an
|
|
886
|
+
// author opacity the user's theme cannot style. Keyed off
|
|
887
|
+
// the control's flag, which outranks `checked`.
|
|
888
|
+
selectors: { '[data-scope="switch"][data-part="control"][data-disabled] &': { backgroundColor: 'GrayText' } },
|
|
889
|
+
},
|
|
890
|
+
},
|
|
790
891
|
},
|
|
791
892
|
},
|
|
792
893
|
},
|
|
@@ -817,6 +918,10 @@ export const switchRecipe: RecipeInput = {
|
|
|
817
918
|
// No currentColor on this target — the ink is named
|
|
818
919
|
// directly, and `checked` moves border + fill below.
|
|
819
920
|
border: 'var(--border) solid var(--switch-ink)',
|
|
921
|
+
// daisy's 1px inset depth line along the track's top
|
|
922
|
+
// edge — `currentColor` at `--depth` × 10% on the
|
|
923
|
+
// web, the theme's own depth shade here.
|
|
924
|
+
boxShadow: '0 1px var(--depth-shade) inset',
|
|
820
925
|
userSelect: 'none',
|
|
821
926
|
transition: 'border-color var(--duration-slow) var(--ease-standard), '
|
|
822
927
|
+ 'background-color var(--duration-normal) var(--ease-standard)',
|
|
@@ -837,6 +942,12 @@ export const switchRecipe: RecipeInput = {
|
|
|
837
942
|
height: 'calc(var(--switch-size) - (var(--border) + var(--switch-p)) * 2)',
|
|
838
943
|
borderRadius: 'var(--radius-selector)',
|
|
839
944
|
backgroundColor: 'var(--switch-ink)',
|
|
945
|
+
// daisy's knob relief (`.toggle:before`): a shade
|
|
946
|
+
// along the bottom edge, a sheen across the top, and
|
|
947
|
+
// a 1px drop — the web's `currentColor` drop spelled
|
|
948
|
+
// as the theme's depth shade. The noise layer has no
|
|
949
|
+
// lynx spelling and stays web-only.
|
|
950
|
+
boxShadow: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset, 0 1px var(--depth-shade)',
|
|
840
951
|
transition: 'transform var(--duration-normal) var(--ease-standard), '
|
|
841
952
|
+ 'background-color var(--duration-instant) var(--ease-standard)',
|
|
842
953
|
},
|
|
@@ -953,6 +1064,22 @@ const lynxBtnSizes = (part: string): Record<string, Record<string, PartStyles>>
|
|
|
953
1064
|
xl: { [part]: { base: lynxBtnPad(6) } },
|
|
954
1065
|
});
|
|
955
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* The press rendering of the overlay btns (dialog, popover), for their
|
|
1069
|
+
* `targets.lynx` sections. The web answers a pointer with `:hover`, which
|
|
1070
|
+
* the lynx emitter drops (a touch platform), and these parts' `pressed`
|
|
1071
|
+
* flag — stamped by the lynx runtime on every touch — had no rule, so a
|
|
1072
|
+
* tap showed nothing (signalxjs/lynx#1146). daisy's `:hover` wash plus the
|
|
1073
|
+
* btn's 1px sink, the same pair `button` paints for its own `pressed`. A
|
|
1074
|
+
* colour variant restates the background after the states at equal
|
|
1075
|
+
* specificity, so a coloured trigger keeps its fill and sinks.
|
|
1076
|
+
*/
|
|
1077
|
+
const lynxBtnPressed: NonNullable<PartStyles['base']> = {
|
|
1078
|
+
background: 'var(--color-base-300)',
|
|
1079
|
+
transform: 'translateY(1px)',
|
|
1080
|
+
boxShadow: 'none',
|
|
1081
|
+
};
|
|
1082
|
+
|
|
956
1083
|
export const dialog: RecipeInput = {
|
|
957
1084
|
component: 'dialog',
|
|
958
1085
|
// Public to a design system derived from this one (#73).
|
|
@@ -1044,14 +1171,16 @@ export const dialog: RecipeInput = {
|
|
|
1044
1171
|
// reach and the trigger is the whole story here.
|
|
1045
1172
|
variants: { color: btnColors(), size: btnSizes },
|
|
1046
1173
|
// The btn paddings and the footer's push-down, restated physically —
|
|
1047
|
-
// see `lynxBtnPad` for the #1084 verdict this answers
|
|
1174
|
+
// see `lynxBtnPad` for the #1084 verdict this answers — and the btns'
|
|
1175
|
+
// press rendering (`lynxBtnPressed`). The footer states its row: on lynx
|
|
1176
|
+
// a row container says so next to `display: flex`.
|
|
1048
1177
|
targets: {
|
|
1049
1178
|
lynx: {
|
|
1050
1179
|
parts: {
|
|
1051
|
-
trigger: { base: lynxBtnPad(4) },
|
|
1052
|
-
close: { base: lynxBtnPad(4) },
|
|
1053
|
-
cancel: { base: lynxBtnPad(4) },
|
|
1054
|
-
footer: { base: { marginTop: 'var(--space-2xl)' } },
|
|
1180
|
+
trigger: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed } },
|
|
1181
|
+
close: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed } },
|
|
1182
|
+
cancel: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed } },
|
|
1183
|
+
footer: { base: { marginTop: 'var(--space-2xl)', flexDirection: 'row' } },
|
|
1055
1184
|
},
|
|
1056
1185
|
variants: { size: lynxBtnSizes('trigger') },
|
|
1057
1186
|
},
|
|
@@ -1067,6 +1196,39 @@ const floatingPanel: NonNullable<PartStyles['base']> = {
|
|
|
1067
1196
|
boxShadow: 'var(--shadow-lg)',
|
|
1068
1197
|
};
|
|
1069
1198
|
|
|
1199
|
+
/**
|
|
1200
|
+
* The arrow a floating surface grows toward its anchor (#279), in the
|
|
1201
|
+
* surface's own paint: the dropdown's base-100 and border for popover and
|
|
1202
|
+
* menu, the tooltip's borderless neutral for the tooltip — daisy's
|
|
1203
|
+
* `tooltip` draws exactly that tail under its bubble. Web-only (placed by
|
|
1204
|
+
* the runtime's `--arrow-x`/`--arrow-y`), so it rides `targets.web` with
|
|
1205
|
+
* the popup's permission to let it out.
|
|
1206
|
+
*/
|
|
1207
|
+
const floatingArrow = (scope: string, paint: CssProps, size = '0.625rem'): NonNullable<NonNullable<RecipeInput['targets']>['web']> => ({
|
|
1208
|
+
parts: {
|
|
1209
|
+
popup: { selectors: popupArrowHost(scope) },
|
|
1210
|
+
arrow: popupArrow(scope, { size, paint }),
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
const panelArrowPaint: CssProps = { background: 'var(--color-base-100)', border: 'var(--border) solid var(--color-base-300)' };
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* daisy's dropdown menu for a listbox, sized by the geometry the
|
|
1217
|
+
* anchored-position strategy publishes (#278): at least the width of the
|
|
1218
|
+
* control that opened it (`--anchor-width`, daisy's 13rem floor otherwise)
|
|
1219
|
+
* and never taller than the room on the side it opened to
|
|
1220
|
+
* (`--available-height`), scrolling past a 20rem cap. Border-box, so both
|
|
1221
|
+
* bounds measure the box the strategy positions. Runtime-published
|
|
1222
|
+
* properties are web-only, so this is each listbox recipe's `targets.web`;
|
|
1223
|
+
* lynx keeps the shared 13rem.
|
|
1224
|
+
*/
|
|
1225
|
+
const anchoredListbox: CssProps = {
|
|
1226
|
+
boxSizing: 'border-box',
|
|
1227
|
+
minWidth: 'var(--anchor-width, 13rem)',
|
|
1228
|
+
maxHeight: 'min(20rem, var(--available-height, 20rem))',
|
|
1229
|
+
overflowY: 'auto',
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1070
1232
|
export const popover: RecipeInput = {
|
|
1071
1233
|
component: 'popover',
|
|
1072
1234
|
parts: {
|
|
@@ -1087,6 +1249,14 @@ export const popover: RecipeInput = {
|
|
|
1087
1249
|
title: {
|
|
1088
1250
|
base: { margin: '0 0 var(--space-md)', fontSize: 'var(--text-md)', fontWeight: 'var(--weight-bold)' },
|
|
1089
1251
|
},
|
|
1252
|
+
// The modal's muted body line, at the dropdown's tighter rhythm.
|
|
1253
|
+
description: {
|
|
1254
|
+
base: {
|
|
1255
|
+
margin: '0 0 var(--space-lg)',
|
|
1256
|
+
fontSize: 'var(--text-sm)',
|
|
1257
|
+
color: 'color-mix(in oklab, var(--color-base-content) 75%, transparent)',
|
|
1258
|
+
},
|
|
1259
|
+
},
|
|
1090
1260
|
close: {
|
|
1091
1261
|
base: { ...btn, height: 'calc(var(--size-field) * 8)', paddingInline: 'calc(var(--size-field) * 3)', fontSize: 'var(--text-xs)' },
|
|
1092
1262
|
states: {
|
|
@@ -1098,12 +1268,14 @@ export const popover: RecipeInput = {
|
|
|
1098
1268
|
},
|
|
1099
1269
|
// Trigger-carried axes — same wiring as dialog, same reason.
|
|
1100
1270
|
variants: { color: btnColors(), size: btnSizes },
|
|
1101
|
-
// The btn paddings, restated physically — see `lynxBtnPad` (#1084)
|
|
1271
|
+
// The btn paddings, restated physically — see `lynxBtnPad` (#1084) —
|
|
1272
|
+
// and the btns' press rendering (`lynxBtnPressed`).
|
|
1102
1273
|
targets: {
|
|
1274
|
+
web: floatingArrow('popover', panelArrowPaint),
|
|
1103
1275
|
lynx: {
|
|
1104
1276
|
parts: {
|
|
1105
|
-
trigger: { base: lynxBtnPad(4) },
|
|
1106
|
-
close: { base: lynxBtnPad(3) },
|
|
1277
|
+
trigger: { base: lynxBtnPad(4), states: { pressed: lynxBtnPressed } },
|
|
1278
|
+
close: { base: lynxBtnPad(3), states: { pressed: lynxBtnPressed } },
|
|
1107
1279
|
},
|
|
1108
1280
|
variants: { size: lynxBtnSizes('trigger') },
|
|
1109
1281
|
},
|
|
@@ -1146,6 +1318,7 @@ export const tooltip: RecipeInput = {
|
|
|
1146
1318
|
variants: { color: btnColors(), size: btnSizes },
|
|
1147
1319
|
// The btn paddings, restated physically — see `lynxBtnPad` (#1084).
|
|
1148
1320
|
targets: {
|
|
1321
|
+
web: floatingArrow('tooltip', { background: 'var(--color-neutral)' }, '0.5rem'),
|
|
1149
1322
|
lynx: {
|
|
1150
1323
|
parts: { trigger: { base: lynxBtnPad(4) } },
|
|
1151
1324
|
variants: { size: lynxBtnSizes('trigger') },
|
|
@@ -1299,6 +1472,7 @@ export const menu: RecipeInput = {
|
|
|
1299
1472
|
variants: { color: btnColors(), size: btnSizes },
|
|
1300
1473
|
// The btn paddings, restated physically — see `lynxBtnPad` (#1084).
|
|
1301
1474
|
targets: {
|
|
1475
|
+
web: floatingArrow('menu', panelArrowPaint),
|
|
1302
1476
|
lynx: {
|
|
1303
1477
|
parts: { trigger: { base: lynxBtnPad(4) } },
|
|
1304
1478
|
variants: { size: lynxBtnSizes('trigger') },
|
|
@@ -1356,6 +1530,61 @@ export const field: RecipeInput = {
|
|
|
1356
1530
|
},
|
|
1357
1531
|
};
|
|
1358
1532
|
|
|
1533
|
+
/**
|
|
1534
|
+
* Fieldset (#285), daisyUI 5's `.fieldset` / `.fieldset-legend`: a
|
|
1535
|
+
* one-column grid with a tight row gap and a little block padding, no frame
|
|
1536
|
+
* of its own (daisy draws one only when the app adds `border`), and a
|
|
1537
|
+
* semibold legend with block padding standing in for its margin. The UA
|
|
1538
|
+
* `min-inline-size: min-content` is undone so a wide control cannot push the
|
|
1539
|
+
* group past its column. Colour accents the legend through `roleInk`, the
|
|
1540
|
+
* Field way.
|
|
1541
|
+
*/
|
|
1542
|
+
export const fieldset: RecipeInput = {
|
|
1543
|
+
component: 'fieldset',
|
|
1544
|
+
tokens: { '--fieldset-accent': 'var(--color-base-content)' },
|
|
1545
|
+
parts: {
|
|
1546
|
+
root: {
|
|
1547
|
+
base: {
|
|
1548
|
+
display: 'grid',
|
|
1549
|
+
gridTemplateColumns: '1fr',
|
|
1550
|
+
gridAutoRows: 'max-content',
|
|
1551
|
+
gap: 'var(--space-sm)',
|
|
1552
|
+
minInlineSize: '0',
|
|
1553
|
+
margin: '0',
|
|
1554
|
+
paddingBlock: 'var(--space-xs)',
|
|
1555
|
+
paddingInline: '0',
|
|
1556
|
+
border: 'none',
|
|
1557
|
+
},
|
|
1558
|
+
},
|
|
1559
|
+
legend: {
|
|
1560
|
+
base: {
|
|
1561
|
+
paddingBlock: 'var(--space-sm)',
|
|
1562
|
+
paddingInline: '0',
|
|
1563
|
+
fontSize: 'var(--text-sm)',
|
|
1564
|
+
fontWeight: 'var(--weight-semibold)',
|
|
1565
|
+
color: 'var(--fieldset-accent)',
|
|
1566
|
+
},
|
|
1567
|
+
states: {
|
|
1568
|
+
disabled: { opacity: 'var(--disabled-opacity)' },
|
|
1569
|
+
invalid: { color: roleInk('error') },
|
|
1570
|
+
},
|
|
1571
|
+
},
|
|
1572
|
+
},
|
|
1573
|
+
variants: {
|
|
1574
|
+
color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
|
|
1575
|
+
'--fieldset-accent': roleInk(c),
|
|
1576
|
+
} } }])),
|
|
1577
|
+
size: {
|
|
1578
|
+
xs: { legend: { base: { fontSize: 'var(--text-xs)' } } },
|
|
1579
|
+
sm: { legend: { base: { fontSize: 'var(--text-xs)' } } },
|
|
1580
|
+
// `md` is the un-attributed render.
|
|
1581
|
+
md: {},
|
|
1582
|
+
lg: { legend: { base: { fontSize: 'var(--text-md)' } } },
|
|
1583
|
+
xl: { legend: { base: { fontSize: 'var(--text-lg)' } } },
|
|
1584
|
+
},
|
|
1585
|
+
},
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1359
1588
|
/**
|
|
1360
1589
|
* daisy's tick, drawn by a clip-path that GROWS.
|
|
1361
1590
|
*
|
|
@@ -1432,6 +1661,8 @@ export const checkbox: RecipeInput = {
|
|
|
1432
1661
|
root: {
|
|
1433
1662
|
base: { display: 'inline-flex', alignItems: 'center', gap: 'var(--space-md)', cursor: 'pointer' },
|
|
1434
1663
|
states: {
|
|
1664
|
+
// Readonly answers to nothing, so it does not invite a click.
|
|
1665
|
+
readonly: { cursor: 'default' },
|
|
1435
1666
|
disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
|
|
1436
1667
|
checked: {}, unchecked: {}, indeterminate: {},
|
|
1437
1668
|
},
|
|
@@ -1504,6 +1735,8 @@ export const checkbox: RecipeInput = {
|
|
|
1504
1735
|
// `invalid` is semantic, not an accent: it stays error under
|
|
1505
1736
|
// every colour variant, on purpose.
|
|
1506
1737
|
invalid: { borderColor: 'var(--color-error)' },
|
|
1738
|
+
// Readonly answers to nothing, so it does not invite a click.
|
|
1739
|
+
readonly: { cursor: 'default' },
|
|
1507
1740
|
disabled: {},
|
|
1508
1741
|
},
|
|
1509
1742
|
},
|
|
@@ -1630,13 +1863,9 @@ export const radioGroup: RecipeInput = {
|
|
|
1630
1863
|
parts: {
|
|
1631
1864
|
root: {
|
|
1632
1865
|
base: { display: 'flex', flexDirection: 'column', gap: 'var(--space-md)' },
|
|
1866
|
+
// `invalid` paints on each `item-control`, which carries the flag
|
|
1867
|
+
// itself (#267) — the root only groups the items.
|
|
1633
1868
|
states: { invalid: {}, required: {} },
|
|
1634
|
-
selectors: {
|
|
1635
|
-
// `invalid` is a fact about the GROUP — `item-control` carries
|
|
1636
|
-
// no flag of its own — and it is semantic: it stays error
|
|
1637
|
-
// under every colour variant.
|
|
1638
|
-
'&[data-invalid] [data-part="item-control"]': { borderColor: 'var(--color-error)' },
|
|
1639
|
-
},
|
|
1640
1869
|
},
|
|
1641
1870
|
label: {
|
|
1642
1871
|
base: { fontSize: 'var(--text-sm)', fontWeight: 'var(--weight-semibold)' },
|
|
@@ -1645,6 +1874,8 @@ export const radioGroup: RecipeInput = {
|
|
|
1645
1874
|
item: {
|
|
1646
1875
|
base: { display: 'inline-flex', alignItems: 'center', gap: 'var(--space-md)', cursor: 'pointer' },
|
|
1647
1876
|
states: {
|
|
1877
|
+
// Readonly answers to nothing, so it does not invite a click.
|
|
1878
|
+
readonly: { cursor: 'default' },
|
|
1648
1879
|
disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
|
|
1649
1880
|
checked: {}, unchecked: {},
|
|
1650
1881
|
},
|
|
@@ -1679,6 +1910,10 @@ export const radioGroup: RecipeInput = {
|
|
|
1679
1910
|
animation: 'var(--duration-fast) var(--ease-standard) zero-daisy-radio',
|
|
1680
1911
|
},
|
|
1681
1912
|
unchecked: {},
|
|
1913
|
+
// `invalid` is semantic: it stays error under every colour
|
|
1914
|
+
// variant, and after `checked` so it holds whichever radio is
|
|
1915
|
+
// chosen.
|
|
1916
|
+
invalid: { borderColor: 'var(--color-error)' },
|
|
1682
1917
|
'focus-visible': { outline: '2px solid var(--radio-accent)', outlineOffset: '2px' },
|
|
1683
1918
|
disabled: {},
|
|
1684
1919
|
},
|
|
@@ -1750,6 +1985,48 @@ export const radioGroup: RecipeInput = {
|
|
|
1750
1985
|
},
|
|
1751
1986
|
};
|
|
1752
1987
|
|
|
1988
|
+
/**
|
|
1989
|
+
* CheckboxGroup (#282) — daisy has no group component; its forms stack
|
|
1990
|
+
* `.checkbox` rows under a `fieldset-legend`. The boxes keep the checkbox
|
|
1991
|
+
* recipe; the group owns the stack and the legend, which speaks Field's
|
|
1992
|
+
* label (colour through `roleInk`, #210).
|
|
1993
|
+
*/
|
|
1994
|
+
export const checkboxGroup: RecipeInput = {
|
|
1995
|
+
component: 'checkbox-group',
|
|
1996
|
+
tokens: { '--checkbox-group-accent': 'var(--color-base-content)' },
|
|
1997
|
+
parts: {
|
|
1998
|
+
root: {
|
|
1999
|
+
base: { display: 'flex', flexDirection: 'column', gap: 'var(--space-md)' },
|
|
2000
|
+
// `invalid` and `readonly` paint on each box — the root only
|
|
2001
|
+
// lays the boxes out.
|
|
2002
|
+
states: { invalid: {}, required: {}, readonly: {} },
|
|
2003
|
+
selectors: {
|
|
2004
|
+
'&[data-orientation="horizontal"]': { flexDirection: 'row', flexWrap: 'wrap', columnGap: 'var(--space-lg)' },
|
|
2005
|
+
},
|
|
2006
|
+
},
|
|
2007
|
+
label: {
|
|
2008
|
+
base: { fontSize: 'var(--text-sm)', fontWeight: 'var(--weight-semibold)', color: 'var(--checkbox-group-accent)' },
|
|
2009
|
+
states: { disabled: { opacity: 'var(--disabled-opacity)' } },
|
|
2010
|
+
selectors: {
|
|
2011
|
+
'&[data-required]::after': { content: '" *"', color: roleInk('error') },
|
|
2012
|
+
},
|
|
2013
|
+
},
|
|
2014
|
+
},
|
|
2015
|
+
variants: {
|
|
2016
|
+
color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
|
|
2017
|
+
'--checkbox-group-accent': roleInk(c),
|
|
2018
|
+
} } }])),
|
|
2019
|
+
size: {
|
|
2020
|
+
xs: { root: { base: { gap: 'var(--space-sm)' } }, label: { base: { fontSize: 'var(--text-xs)' } } },
|
|
2021
|
+
sm: { label: { base: { fontSize: 'var(--text-xs)' } } },
|
|
2022
|
+
md: {},
|
|
2023
|
+
lg: { label: { base: { fontSize: 'var(--text-md)' } } },
|
|
2024
|
+
xl: { root: { base: { gap: 'var(--space-lg)' } }, label: { base: { fontSize: 'var(--text-lg)' } } },
|
|
2025
|
+
},
|
|
2026
|
+
},
|
|
2027
|
+
skipStates: { label: ['invalid'] },
|
|
2028
|
+
};
|
|
2029
|
+
|
|
1753
2030
|
// --------------------------------------------------------------------------
|
|
1754
2031
|
// 4. progress — replace the whole export (only the two radius lines move)
|
|
1755
2032
|
// --------------------------------------------------------------------------
|
|
@@ -1814,9 +2091,13 @@ export const progress: RecipeInput = {
|
|
|
1814
2091
|
transition: 'width var(--duration-slow) var(--ease-standard)',
|
|
1815
2092
|
},
|
|
1816
2093
|
states: {
|
|
1817
|
-
// `complete`
|
|
1818
|
-
//
|
|
1819
|
-
|
|
2094
|
+
// `complete` keeps the colour axis. daisyUI 5 has no complete
|
|
2095
|
+
// tint — a full `progress-primary` stays primary — and a
|
|
2096
|
+
// `color="secondary"` bar that turns green at 100% reads as a
|
|
2097
|
+
// bug, not as meaning (signalxjs/lynx#1144). A caller who wants
|
|
2098
|
+
// "done" to look like success says so: `color="success"`.
|
|
2099
|
+
// Declared equal to `loading` in `sameAs` below.
|
|
2100
|
+
complete: {},
|
|
1820
2101
|
loading: {},
|
|
1821
2102
|
indeterminate: { width: '40%', animation: 'zero-daisy-indeterminate 1.2s ease-in-out infinite' },
|
|
1822
2103
|
},
|
|
@@ -1852,6 +2133,8 @@ export const progress: RecipeInput = {
|
|
|
1852
2133
|
// so the manifest states the default for runtimes with no `:not()`
|
|
1853
2134
|
// fallback (the lynx target; signalxjs/lynx#1070).
|
|
1854
2135
|
defaultVariants: { color: 'primary', size: 'md' },
|
|
2136
|
+
// A finished bar is a full bar of the same ink (see `complete` above).
|
|
2137
|
+
sameAs: { root: { complete: 'loading' }, range: { complete: 'loading' } },
|
|
1855
2138
|
keyframes: {
|
|
1856
2139
|
// Logical, so the sweep runs the way the bar fills — `margin-inline-start`
|
|
1857
2140
|
// is animatable and direction-aware on its own. The determinate `width`,
|
|
@@ -1873,9 +2156,21 @@ export const progress: RecipeInput = {
|
|
|
1873
2156
|
},
|
|
1874
2157
|
};
|
|
1875
2158
|
|
|
2159
|
+
/** The slider's deepened fill — the web's `--slider-fill` mix, for a given accent. */
|
|
2160
|
+
const sliderFill = (accent: string): string =>
|
|
2161
|
+
`color-mix(in oklab, ${accent} 90%, var(--color-base-content))`;
|
|
2162
|
+
|
|
1876
2163
|
export const slider: RecipeInput = {
|
|
1877
2164
|
component: 'slider',
|
|
1878
|
-
|
|
2165
|
+
// daisy's `.range` metrics: the thumb is `--range-thumb-size`
|
|
2166
|
+
// (×6 of `--size-selector` at md, ×4…×8 across the ramp — the same
|
|
2167
|
+
// selector ramp checkbox rides) and the track is half of it. Both the
|
|
2168
|
+
// native control and the composed parts read these.
|
|
2169
|
+
tokens: {
|
|
2170
|
+
'--slider-accent': 'var(--color-primary)',
|
|
2171
|
+
'--slider-track-size': 'calc(var(--size-selector) * 3)',
|
|
2172
|
+
'--slider-thumb-size': 'calc(var(--size-selector) * 6)',
|
|
2173
|
+
},
|
|
1879
2174
|
parts: {
|
|
1880
2175
|
root: {
|
|
1881
2176
|
base: { display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)', width: '100%' },
|
|
@@ -1885,30 +2180,106 @@ export const slider: RecipeInput = {
|
|
|
1885
2180
|
base: { fontSize: 'var(--text-sm)', fontWeight: 'var(--weight-semibold)' },
|
|
1886
2181
|
states: { disabled: {} },
|
|
1887
2182
|
},
|
|
2183
|
+
// A custom skin (`appearance: none`), ported from daisyUI 5's
|
|
2184
|
+
// `range.css`: a `--radius-selector` track half the thumb's height,
|
|
2185
|
+
// and daisy's real range thumb — a `base-100` knob inside a
|
|
2186
|
+
// `.25rem` ring of the fill ink (daisy's `--range-p`), carrying the
|
|
2187
|
+
// same `--depth` inset shading the checkbox and radio wear. daisy
|
|
2188
|
+
// throws the elapsed fill as a 100cqw box-shadow off the thumb and
|
|
2189
|
+
// clips it with `overflow: hidden`; here the track reads the
|
|
2190
|
+
// runtime-published `--slider-percent` as a gradient stop instead,
|
|
2191
|
+
// as every other skin does. The fill is deepened toward
|
|
2192
|
+
// `base-content` the way `progressFill` deepens daisy's bar — a raw
|
|
2193
|
+
// role on the base-300 rail is exactly the #210/#228 bug class.
|
|
2194
|
+
//
|
|
2195
|
+
// The rebuild is also the correctness move (see the kit skill): Blink
|
|
2196
|
+
// treats range inputs as ALWAYS `:focus-visible`, even on mouse
|
|
2197
|
+
// focus, so the base kills the native outline and the
|
|
2198
|
+
// `focus-visible` state — compiled to the runtime's keyboard-only
|
|
2199
|
+
// `[data-focus-visible]` flag — draws the ring only when it means it.
|
|
1888
2200
|
control: {
|
|
1889
|
-
base: {
|
|
2201
|
+
base: {
|
|
2202
|
+
appearance: 'none',
|
|
2203
|
+
width: '100%',
|
|
2204
|
+
height: 'var(--slider-thumb-size)',
|
|
2205
|
+
margin: '0',
|
|
2206
|
+
background: 'transparent',
|
|
2207
|
+
cursor: 'pointer',
|
|
2208
|
+
outline: 'none',
|
|
2209
|
+
// Kept for the forced-colors fallback below, where native
|
|
2210
|
+
// rendering takes over and still honours the accent.
|
|
2211
|
+
accentColor: 'var(--slider-accent)',
|
|
2212
|
+
// `--slider-fill` and `--slider-track` live in targets.web
|
|
2213
|
+
// below: the fill is a color-mix() the lynx emitter drops,
|
|
2214
|
+
// and the track reads the runtime-published
|
|
2215
|
+
// `--slider-percent`, a web-only mechanism.
|
|
2216
|
+
},
|
|
1890
2217
|
states: {
|
|
2218
|
+
// Readonly answers to nothing, so it does not invite a click.
|
|
2219
|
+
readonly: { cursor: 'default' },
|
|
1891
2220
|
disabled: { cursor: 'not-allowed' },
|
|
1892
2221
|
'focus-visible': { outline: '2px solid var(--slider-accent)', outlineOffset: '2px' },
|
|
1893
2222
|
// `invalid` is semantic: it stays error under every colour
|
|
1894
|
-
// variant, on purpose
|
|
1895
|
-
|
|
2223
|
+
// variant, on purpose — the accent indirection swaps the
|
|
2224
|
+
// fill, the thumb ring, the focus ring and the forced-colors
|
|
2225
|
+
// fallback together.
|
|
2226
|
+
invalid: { '--slider-accent': 'var(--color-error)' },
|
|
2227
|
+
},
|
|
2228
|
+
selectors: {
|
|
2229
|
+
// Vendor pseudos cannot share a selector list — one unknown
|
|
2230
|
+
// selector invalidates the whole rule — so each engine gets
|
|
2231
|
+
// its own copy reading the shared custom properties.
|
|
2232
|
+
'&::-webkit-slider-runnable-track': {
|
|
2233
|
+
height: 'var(--slider-track-size)',
|
|
2234
|
+
borderRadius: 'var(--radius-selector)',
|
|
2235
|
+
background: 'var(--slider-track)',
|
|
2236
|
+
},
|
|
2237
|
+
'&::-webkit-slider-thumb': {
|
|
2238
|
+
appearance: 'none',
|
|
2239
|
+
boxSizing: 'border-box',
|
|
2240
|
+
width: 'var(--slider-thumb-size)',
|
|
2241
|
+
height: 'var(--slider-thumb-size)',
|
|
2242
|
+
marginTop: 'calc((var(--slider-track-size) - var(--slider-thumb-size)) / 2)',
|
|
2243
|
+
borderRadius: 'calc(var(--radius-selector) + min(0.25rem, var(--radius-selector) * 3))',
|
|
2244
|
+
border: '0.25rem solid var(--slider-fill)',
|
|
2245
|
+
background: 'var(--color-base-100)',
|
|
2246
|
+
boxShadow: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset',
|
|
2247
|
+
},
|
|
2248
|
+
'&::-moz-range-track': {
|
|
2249
|
+
height: 'var(--slider-track-size)',
|
|
2250
|
+
borderRadius: 'var(--radius-selector)',
|
|
2251
|
+
background: 'var(--slider-track)',
|
|
2252
|
+
},
|
|
2253
|
+
'&::-moz-range-thumb': {
|
|
2254
|
+
boxSizing: 'border-box',
|
|
2255
|
+
width: 'var(--slider-thumb-size)',
|
|
2256
|
+
height: 'var(--slider-thumb-size)',
|
|
2257
|
+
borderRadius: 'calc(var(--radius-selector) + min(0.25rem, var(--radius-selector) * 3))',
|
|
2258
|
+
border: '0.25rem solid var(--slider-fill)',
|
|
2259
|
+
background: 'var(--color-base-100)',
|
|
2260
|
+
boxShadow: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset',
|
|
2261
|
+
},
|
|
2262
|
+
},
|
|
2263
|
+
at: {
|
|
2264
|
+
// Native rendering knows forced colors better than a custom
|
|
2265
|
+
// skin; the retained accentColor keeps the fallback branded.
|
|
2266
|
+
'forced-colors': { base: { appearance: 'auto' } },
|
|
1896
2267
|
},
|
|
1897
2268
|
},
|
|
1898
|
-
// The composed range projection (#325)
|
|
1899
|
-
//
|
|
1900
|
-
//
|
|
1901
|
-
// `base-content` the way `progressFill` deepens daisy's bar — a raw
|
|
1902
|
-
// role on the base-300 rail is exactly the #210/#228 bug class.
|
|
2269
|
+
// The composed range projection (#325): the same metrics as the
|
|
2270
|
+
// native control above, restating its accent as real boxes. The
|
|
2271
|
+
// fill is the same deepened ink the native track paints.
|
|
1903
2272
|
track: {
|
|
1904
2273
|
base: {
|
|
1905
|
-
height: '
|
|
1906
|
-
|
|
2274
|
+
height: 'var(--slider-track-size)',
|
|
2275
|
+
// Reserve the thumb's overhang so the row's box matches the
|
|
2276
|
+
// native control's.
|
|
2277
|
+
marginBlock: 'calc((var(--slider-thumb-size) - var(--slider-track-size)) / 2)',
|
|
1907
2278
|
borderRadius: '9999px',
|
|
1908
2279
|
background: 'var(--color-base-300)',
|
|
1909
2280
|
cursor: 'pointer',
|
|
1910
2281
|
},
|
|
1911
|
-
states: { disabled: { cursor: 'not-allowed' } },
|
|
2282
|
+
states: { readonly: { cursor: 'default' }, disabled: { cursor: 'not-allowed' } },
|
|
1912
2283
|
},
|
|
1913
2284
|
range: {
|
|
1914
2285
|
base: {
|
|
@@ -1921,11 +2292,11 @@ export const slider: RecipeInput = {
|
|
|
1921
2292
|
thumb: {
|
|
1922
2293
|
base: {
|
|
1923
2294
|
boxSizing: 'border-box',
|
|
1924
|
-
width: '
|
|
1925
|
-
height: '
|
|
2295
|
+
width: 'var(--slider-thumb-size)',
|
|
2296
|
+
height: 'var(--slider-thumb-size)',
|
|
1926
2297
|
insetBlockStart: '50%',
|
|
1927
2298
|
translate: '0 -50%',
|
|
1928
|
-
marginInlineStart: 'calc(var(--size
|
|
2299
|
+
marginInlineStart: 'calc(var(--slider-thumb-size) / -2)',
|
|
1929
2300
|
borderRadius: '9999px',
|
|
1930
2301
|
background: 'color-mix(in oklab, var(--slider-accent) 90%, var(--color-base-content))',
|
|
1931
2302
|
cursor: 'pointer',
|
|
@@ -1935,12 +2306,14 @@ export const slider: RecipeInput = {
|
|
|
1935
2306
|
states: {
|
|
1936
2307
|
'focus-visible': { outline: '2px solid var(--slider-accent)', outlineOffset: '2px' },
|
|
1937
2308
|
pressed: {},
|
|
2309
|
+
// Readonly answers to nothing, so it does not invite a click.
|
|
2310
|
+
readonly: { cursor: 'default' },
|
|
1938
2311
|
disabled: { cursor: 'not-allowed' },
|
|
1939
2312
|
},
|
|
1940
2313
|
},
|
|
1941
2314
|
mark: {
|
|
1942
2315
|
base: {
|
|
1943
|
-
paddingBlockStart: 'calc(var(--size
|
|
2316
|
+
paddingBlockStart: 'calc(var(--slider-track-size) + var(--space-2xs))',
|
|
1944
2317
|
fontSize: 'var(--text-xs)',
|
|
1945
2318
|
lineHeight: '1',
|
|
1946
2319
|
whiteSpace: 'nowrap',
|
|
@@ -1954,7 +2327,7 @@ export const slider: RecipeInput = {
|
|
|
1954
2327
|
insetBlockStart: '0',
|
|
1955
2328
|
insetInlineStart: '-1px',
|
|
1956
2329
|
width: '2px',
|
|
1957
|
-
height: '
|
|
2330
|
+
height: 'var(--slider-track-size)',
|
|
1958
2331
|
background: 'var(--color-base-content)',
|
|
1959
2332
|
},
|
|
1960
2333
|
},
|
|
@@ -1967,57 +2340,224 @@ export const slider: RecipeInput = {
|
|
|
1967
2340
|
color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
|
|
1968
2341
|
'--slider-accent': `var(--color-${c})`,
|
|
1969
2342
|
} } }])),
|
|
1970
|
-
//
|
|
1971
|
-
//
|
|
2343
|
+
// daisy's `.range-{xs…xl}` ramp: `--range-thumb-size` at ×4…×8 of
|
|
2344
|
+
// `--size-selector` (1 / 1.25 / 1.5 / 1.75 / 2rem), the track half
|
|
2345
|
+
// of it — so every step moves the whole widget; the un-attributed
|
|
2346
|
+
// (and `md`) render takes the middle step from `tokens:`.
|
|
1972
2347
|
size: {
|
|
1973
|
-
xs: {
|
|
1974
|
-
sm: {
|
|
2348
|
+
xs: { root: { base: { '--slider-track-size': 'calc(var(--size-selector) * 2)', '--slider-thumb-size': 'calc(var(--size-selector) * 4)' } }, label: { base: { fontSize: 'var(--text-xs)' } } },
|
|
2349
|
+
sm: { root: { base: { '--slider-track-size': 'calc(var(--size-selector) * 2.5)', '--slider-thumb-size': 'calc(var(--size-selector) * 5)' } }, label: { base: { fontSize: 'var(--text-sm)' } } },
|
|
2350
|
+
// `md`'s metrics ARE the `tokens:` defaults — one source of truth.
|
|
1975
2351
|
md: { label: { base: { fontSize: 'var(--text-sm)' } } },
|
|
1976
|
-
lg: {
|
|
1977
|
-
xl: {
|
|
2352
|
+
lg: { root: { base: { '--slider-track-size': 'calc(var(--size-selector) * 3.5)', '--slider-thumb-size': 'calc(var(--size-selector) * 7)' } }, label: { base: { fontSize: 'var(--text-md)' } } },
|
|
2353
|
+
xl: { root: { base: { '--slider-track-size': 'calc(var(--size-selector) * 4)', '--slider-thumb-size': 'calc(var(--size-selector) * 8)' } }, label: { base: { fontSize: 'var(--text-lg)' } } },
|
|
1978
2354
|
},
|
|
1979
2355
|
},
|
|
1980
|
-
// Base equals the default — `tokens:` binds the primary accent
|
|
1981
|
-
// only restates the label font-size the
|
|
1982
|
-
//
|
|
2356
|
+
// Base equals the default — `tokens:` binds the primary accent and the
|
|
2357
|
+
// middle-step metrics, and `md` only restates the label font-size the
|
|
2358
|
+
// base already declares — so the twins restate it;
|
|
1983
2359
|
// declared for the manifest (signalxjs/lynx#1070).
|
|
1984
2360
|
defaultVariants: { color: 'primary', size: 'md' },
|
|
1985
2361
|
skipStates: { root: ['invalid', 'focus-visible'] },
|
|
1986
2362
|
// ONE design decision, two paints. On the web the accent reaches the
|
|
1987
|
-
// screen twice
|
|
1988
|
-
//
|
|
1989
|
-
// renders neither
|
|
1990
|
-
// `<input type=range
|
|
1991
|
-
//
|
|
1992
|
-
//
|
|
1993
|
-
//
|
|
1994
|
-
//
|
|
1995
|
-
//
|
|
2363
|
+
// screen twice, both times deepened with `color-mix()`: the native
|
|
2364
|
+
// control's gradient track and thumb ring (via `--slider-fill`, below),
|
|
2365
|
+
// and the composed range/thumb. Lynx renders neither — it has no native
|
|
2366
|
+
// `<input type=range>`, and the emitter drops `color-mix()` as
|
|
2367
|
+
// unresolvable — so the compiled range and thumb once shipped with no
|
|
2368
|
+
// paint at all: grey track, invisible fill, invisible knob (measured on
|
|
2369
|
+
// device, signalxjs/lynx#1075). The geometry all survives (the sizes are
|
|
2370
|
+
// direct calc() over the metric tokens); paint is the only gap, so the
|
|
2371
|
+
// lynx section only restates paint, in lynx-expressible spellings, plus
|
|
2372
|
+
// the physical restatement of the logical geometry.
|
|
1996
2373
|
targets: {
|
|
2374
|
+
web: {
|
|
2375
|
+
parts: {
|
|
2376
|
+
control: {
|
|
2377
|
+
base: {
|
|
2378
|
+
// The fill ink, deepened like the composed range.
|
|
2379
|
+
'--slider-fill': 'color-mix(in oklab, var(--slider-accent) 90%, var(--color-base-content))',
|
|
2380
|
+
// The filled track reads the runtime-published
|
|
2381
|
+
// `--slider-percent` — web-only (`RUNTIME_PROPERTIES`);
|
|
2382
|
+
// the lynx runtime paints its own track/range parts.
|
|
2383
|
+
'--slider-track':
|
|
2384
|
+
'linear-gradient(to right, var(--slider-fill) var(--slider-percent, 50%), var(--color-base-300) 0)',
|
|
2385
|
+
},
|
|
2386
|
+
selectors: {
|
|
2387
|
+
// A vertical native range is spelled through writing
|
|
2388
|
+
// mode by the runtime (#170), so the long axis is the
|
|
2389
|
+
// box's HEIGHT and the channel's WIDTH; the fill
|
|
2390
|
+
// grows from the foot.
|
|
2391
|
+
'&[data-orientation="vertical"]': {
|
|
2392
|
+
width: 'var(--slider-thumb-size)',
|
|
2393
|
+
height: 'var(--slider-length)',
|
|
2394
|
+
'--slider-track':
|
|
2395
|
+
'linear-gradient(to top, var(--slider-fill) var(--slider-percent, 50%), var(--color-base-300) 0)',
|
|
2396
|
+
},
|
|
2397
|
+
'&[data-orientation="vertical"]::-webkit-slider-runnable-track': {
|
|
2398
|
+
width: 'var(--slider-track-size)',
|
|
2399
|
+
height: 'auto',
|
|
2400
|
+
},
|
|
2401
|
+
// Blink lays the thumb against the channel's block
|
|
2402
|
+
// axis — in vertical-lr that is its left edge.
|
|
2403
|
+
'&[data-orientation="vertical"]::-webkit-slider-thumb': {
|
|
2404
|
+
marginTop: '0',
|
|
2405
|
+
marginBlockStart: 'calc((var(--slider-track-size) - var(--slider-thumb-size)) / 2)',
|
|
2406
|
+
},
|
|
2407
|
+
'&[data-orientation="vertical"]::-moz-range-track': {
|
|
2408
|
+
width: 'var(--slider-track-size)',
|
|
2409
|
+
height: '100%',
|
|
2410
|
+
},
|
|
2411
|
+
},
|
|
2412
|
+
},
|
|
2413
|
+
// Vertical (#170): the runtime positions the moving parts by
|
|
2414
|
+
// physical `bottom` percents; these turn the channel upright and
|
|
2415
|
+
// re-centre the knob on the other axis. Web-only — the lynx
|
|
2416
|
+
// runtime ships no vertical slider.
|
|
2417
|
+
root: {
|
|
2418
|
+
selectors: {
|
|
2419
|
+
'&[data-orientation="vertical"]': {
|
|
2420
|
+
width: 'auto',
|
|
2421
|
+
alignItems: 'center',
|
|
2422
|
+
'--slider-length': 'calc(var(--size-selector) * 40)',
|
|
2423
|
+
},
|
|
2424
|
+
},
|
|
2425
|
+
},
|
|
2426
|
+
track: {
|
|
2427
|
+
selectors: {
|
|
2428
|
+
'&[data-orientation="vertical"]': {
|
|
2429
|
+
width: 'var(--slider-track-size)',
|
|
2430
|
+
height: 'var(--slider-length)',
|
|
2431
|
+
marginBlock: '0',
|
|
2432
|
+
marginInline: 'calc((var(--slider-thumb-size) - var(--slider-track-size)) / 2)',
|
|
2433
|
+
},
|
|
2434
|
+
},
|
|
2435
|
+
},
|
|
2436
|
+
range: {
|
|
2437
|
+
selectors: {
|
|
2438
|
+
'&[data-orientation="vertical"]': { insetInlineStart: '0', width: '100%' },
|
|
2439
|
+
},
|
|
2440
|
+
},
|
|
2441
|
+
thumb: {
|
|
2442
|
+
selectors: {
|
|
2443
|
+
'&[data-orientation="vertical"]': {
|
|
2444
|
+
insetBlockStart: 'auto',
|
|
2445
|
+
insetInlineStart: '50%',
|
|
2446
|
+
translate: 'none',
|
|
2447
|
+
marginInlineStart: 'calc(var(--slider-thumb-size) / -2)',
|
|
2448
|
+
marginBlockEnd: 'calc(var(--slider-thumb-size) / -2)',
|
|
2449
|
+
},
|
|
2450
|
+
},
|
|
2451
|
+
},
|
|
2452
|
+
// The label sits beside the channel, centred on its tick.
|
|
2453
|
+
mark: {
|
|
2454
|
+
selectors: {
|
|
2455
|
+
'&[data-orientation="vertical"]': {
|
|
2456
|
+
insetInlineStart: '0',
|
|
2457
|
+
paddingBlockStart: '0',
|
|
2458
|
+
paddingInlineStart: 'calc(var(--slider-track-size) + var(--space-2xs))',
|
|
2459
|
+
translate: '0 50%',
|
|
2460
|
+
},
|
|
2461
|
+
'&[data-orientation="vertical"]::before': {
|
|
2462
|
+
insetBlockStart: '50%',
|
|
2463
|
+
insetInlineStart: '0',
|
|
2464
|
+
width: 'var(--slider-track-size)',
|
|
2465
|
+
height: '2px',
|
|
2466
|
+
translate: '0 -50%',
|
|
2467
|
+
},
|
|
2468
|
+
},
|
|
2469
|
+
},
|
|
2470
|
+
},
|
|
2471
|
+
},
|
|
1997
2472
|
lynx: {
|
|
2473
|
+
// The web's deepened fill, theme-baked: `color-mix()` over theme
|
|
2474
|
+
// colours cannot resolve on device, but the emitter restates a
|
|
2475
|
+
// colour function per theme as literals (as `progress` does for
|
|
2476
|
+
// its accent), so each colour step gets the same 90/10 mix the
|
|
2477
|
+
// web track and knob ring wear. Bound on the root like
|
|
2478
|
+
// `--slider-accent`, rebound by `color` below and by `invalid`
|
|
2479
|
+
// on the control (which wraps the rail on lynx).
|
|
2480
|
+
tokens: { '--slider-fill': sliderFill('var(--color-primary)') },
|
|
2481
|
+
variants: {
|
|
2482
|
+
color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
|
|
2483
|
+
'--slider-fill': sliderFill(`var(--color-${c})`),
|
|
2484
|
+
} } }])),
|
|
2485
|
+
},
|
|
1998
2486
|
parts: {
|
|
2487
|
+
// Vertical (lynx#1144): the lynx runtime positions the moving
|
|
2488
|
+
// parts itself — `top` percentages, the knob centred by
|
|
2489
|
+
// `left: 50%` plus the base's `translateY(-50%)` and
|
|
2490
|
+
// negative `margin-left` — so the class rules only turn the
|
|
2491
|
+
// channel upright: the control is a thumb-wide column of
|
|
2492
|
+
// daisy's rail length, the track fills it.
|
|
2493
|
+
root: {
|
|
2494
|
+
selectors: {
|
|
2495
|
+
'&[data-orientation="vertical"]': { width: 'auto', alignItems: 'center' },
|
|
2496
|
+
},
|
|
2497
|
+
},
|
|
2498
|
+
control: {
|
|
2499
|
+
states: {
|
|
2500
|
+
invalid: { '--slider-fill': sliderFill('var(--color-error)') },
|
|
2501
|
+
},
|
|
2502
|
+
selectors: {
|
|
2503
|
+
'&[data-orientation="vertical"]': {
|
|
2504
|
+
display: 'flex',
|
|
2505
|
+
flexDirection: 'row',
|
|
2506
|
+
justifyContent: 'center',
|
|
2507
|
+
width: 'var(--slider-thumb-size)',
|
|
2508
|
+
height: 'calc(var(--size-selector) * 40)',
|
|
2509
|
+
},
|
|
2510
|
+
},
|
|
2511
|
+
},
|
|
1999
2512
|
track: {
|
|
2000
2513
|
// The web track's `margin-block` breathing room,
|
|
2001
2514
|
// restated physically: logical spellings resolve on iOS
|
|
2002
2515
|
// but not on Android (measured, signalxjs/lynx#1084), so
|
|
2003
2516
|
// the emitter refuses them. Physical is the lynx
|
|
2004
2517
|
// target's norm — no RTL flow there.
|
|
2518
|
+
//
|
|
2519
|
+
// daisy's channel radius is the selector radius (the
|
|
2520
|
+
// native control's runnable-track), not a pill.
|
|
2005
2521
|
base: {
|
|
2006
|
-
marginTop: 'calc(var(--size-
|
|
2007
|
-
marginBottom: 'calc(var(--size-
|
|
2522
|
+
marginTop: 'calc((var(--slider-thumb-size) - var(--slider-track-size)) / 2)',
|
|
2523
|
+
marginBottom: 'calc((var(--slider-thumb-size) - var(--slider-track-size)) / 2)',
|
|
2524
|
+
borderRadius: 'var(--radius-selector)',
|
|
2525
|
+
},
|
|
2526
|
+
selectors: {
|
|
2527
|
+
'&[data-orientation="vertical"]': {
|
|
2528
|
+
width: 'var(--slider-track-size)',
|
|
2529
|
+
height: '100%',
|
|
2530
|
+
marginTop: '0',
|
|
2531
|
+
marginBottom: '0',
|
|
2532
|
+
},
|
|
2008
2533
|
},
|
|
2009
2534
|
},
|
|
2010
2535
|
mark: {
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
|
|
2536
|
+
// The web draws the tick with `::before`, which lynx has
|
|
2537
|
+
// no projection for — the mark was an empty, invisible
|
|
2538
|
+
// box. Here the mark IS the tick: a 2px rule across the
|
|
2539
|
+
// channel, centred on its runtime-set `left` percent.
|
|
2540
|
+
// (Lynx marks carry no label text.)
|
|
2541
|
+
base: {
|
|
2542
|
+
top: '0',
|
|
2543
|
+
width: '2px',
|
|
2544
|
+
height: 'var(--slider-track-size)',
|
|
2545
|
+
marginLeft: '-1px',
|
|
2546
|
+
paddingTop: '0',
|
|
2547
|
+
backgroundColor: 'var(--color-base-content)',
|
|
2548
|
+
},
|
|
2549
|
+
selectors: {
|
|
2550
|
+
'&[data-orientation="vertical"]': {
|
|
2551
|
+
left: '0',
|
|
2552
|
+
width: 'var(--slider-track-size)',
|
|
2553
|
+
height: '2px',
|
|
2554
|
+
marginLeft: '0',
|
|
2555
|
+
marginTop: '-1px',
|
|
2556
|
+
},
|
|
2557
|
+
},
|
|
2014
2558
|
},
|
|
2015
2559
|
range: {
|
|
2016
|
-
|
|
2017
|
-
// base-content (`progressFill`'s recipe); the plain
|
|
2018
|
-
// accent — a var() chain, proven working on device — is
|
|
2019
|
-
// the closest lynx-expressible equivalent.
|
|
2020
|
-
base: { background: 'var(--slider-accent)' },
|
|
2560
|
+
base: { background: 'var(--slider-fill)', borderRadius: 'var(--radius-selector)' },
|
|
2021
2561
|
},
|
|
2022
2562
|
thumb: {
|
|
2023
2563
|
// daisy's real range thumb, ported from daisyUI 5
|
|
@@ -2028,13 +2568,16 @@ export const slider: RecipeInput = {
|
|
|
2028
2568
|
// `--range-thumb` defaults to `var(--color-base-100)`,
|
|
2029
2569
|
// `--range-p` is daisy's literal `.25rem` (deliberately
|
|
2030
2570
|
// not the theme's size token), and the border ink is the
|
|
2031
|
-
//
|
|
2032
|
-
//
|
|
2033
|
-
//
|
|
2034
|
-
//
|
|
2035
|
-
//
|
|
2036
|
-
//
|
|
2037
|
-
//
|
|
2571
|
+
// deepened fill the web's native thumb ring reads. daisy
|
|
2572
|
+
// paints the progress fill itself as a 100cqw box-shadow
|
|
2573
|
+
// thrown off this thumb — a native-input trick with no
|
|
2574
|
+
// lynx equivalent; the composed `.zx-slider__range` box
|
|
2575
|
+
// above IS that fill here, so the knob only needs its own
|
|
2576
|
+
// paints. Its radius is daisy's formula minus the min()
|
|
2577
|
+
// clamp lynx cannot evaluate — exact for every shipped
|
|
2578
|
+
// theme (all set `--radius-selector` ≥ .25rem / 3) — and
|
|
2579
|
+
// it wears daisy's depth shading (`currentColor`'s 10%
|
|
2580
|
+
// outer shadow spelled as the theme's shade).
|
|
2038
2581
|
//
|
|
2039
2582
|
// The centering geometry is restated physically beside
|
|
2040
2583
|
// the paint: `inset-block-start`/`translate`/
|
|
@@ -2045,10 +2588,12 @@ export const slider: RecipeInput = {
|
|
|
2045
2588
|
// proven on both platforms.
|
|
2046
2589
|
base: {
|
|
2047
2590
|
background: 'var(--color-base-100)',
|
|
2048
|
-
border: '0.25rem solid var(--slider-
|
|
2591
|
+
border: '0.25rem solid var(--slider-fill)',
|
|
2592
|
+
borderRadius: 'calc(var(--radius-selector) + 0.25rem)',
|
|
2593
|
+
boxShadow: '0 -1px var(--depth-shade) inset, 0 8px 0 -4px var(--depth-sheen) inset, 0 1px var(--depth-shade)',
|
|
2049
2594
|
top: '50%',
|
|
2050
2595
|
transform: 'translateY(-50%)',
|
|
2051
|
-
marginLeft: 'calc(var(--size
|
|
2596
|
+
marginLeft: 'calc(var(--slider-thumb-size) / -2)',
|
|
2052
2597
|
},
|
|
2053
2598
|
},
|
|
2054
2599
|
},
|
|
@@ -2058,12 +2603,26 @@ export const slider: RecipeInput = {
|
|
|
2058
2603
|
|
|
2059
2604
|
export const accordion: RecipeInput = {
|
|
2060
2605
|
component: 'accordion',
|
|
2606
|
+
targets: {
|
|
2607
|
+
...disclosureExit('accordion'),
|
|
2608
|
+
lynx: {
|
|
2609
|
+
parts: {
|
|
2610
|
+
// No hover on a touch platform: the held trigger takes daisy's
|
|
2611
|
+
// hover fill. The ::after chevron has no lynx counterpart (no
|
|
2612
|
+
// pseudo-elements, and the anatomy declares no part for it) —
|
|
2613
|
+
// an app draws its own glyph in the trigger.
|
|
2614
|
+
trigger: { states: { pressed: { background: 'var(--color-base-200)' } } },
|
|
2615
|
+
},
|
|
2616
|
+
},
|
|
2617
|
+
},
|
|
2061
2618
|
// Same accent grammar as collapsible: the open heading's ink, defaulting
|
|
2062
2619
|
// to base-content so the un-attributed render is unchanged.
|
|
2063
2620
|
tokens: { '--accordion-accent': 'var(--color-base-content)' },
|
|
2064
2621
|
parts: {
|
|
2065
2622
|
root: {
|
|
2066
2623
|
base: { display: 'flex', flexDirection: 'column', gap: 'var(--space-md)' },
|
|
2624
|
+
// `orientation="horizontal"` (#276): the cards sit in a row.
|
|
2625
|
+
selectors: { '&[data-orientation="horizontal"]': { flexDirection: 'row', alignItems: 'start' } },
|
|
2067
2626
|
},
|
|
2068
2627
|
item: withPresence(disclosurePresence, {
|
|
2069
2628
|
base: {
|
|
@@ -2073,6 +2632,7 @@ export const accordion: RecipeInput = {
|
|
|
2073
2632
|
overflow: 'hidden',
|
|
2074
2633
|
},
|
|
2075
2634
|
states: { open: {}, closed: {} },
|
|
2635
|
+
selectors: { '[data-scope="accordion"][data-part="root"][data-orientation="horizontal"] > &': { flex: '1 1 0', minInlineSize: '0' } },
|
|
2076
2636
|
}),
|
|
2077
2637
|
trigger: {
|
|
2078
2638
|
base: {
|
|
@@ -2157,14 +2717,21 @@ const fieldControl: NonNullable<PartStyles['base']> = {
|
|
|
2157
2717
|
boxShadow: 'var(--shadow-xs)',
|
|
2158
2718
|
};
|
|
2159
2719
|
|
|
2720
|
+
/**
|
|
2721
|
+
* A select whose clear-trigger is rendered (#280) — anchored on the root, so
|
|
2722
|
+
* a part inside the trigger can make room for the button laid over it.
|
|
2723
|
+
*/
|
|
2724
|
+
const SELECT_CLEARABLE = '[data-scope="select"][data-part="root"]:has(> [data-scope="select"][data-part="clear-trigger"]) &';
|
|
2725
|
+
|
|
2160
2726
|
export const select: RecipeInput = {
|
|
2161
2727
|
component: 'select',
|
|
2162
2728
|
// Accent as text/border ink only — daisy's highlighted item is a neutral
|
|
2163
2729
|
// base-200 wash, so no `-on-accent` content colour is consumed here.
|
|
2164
2730
|
tokens: { '--select-accent': 'var(--color-primary)' },
|
|
2165
2731
|
parts: {
|
|
2732
|
+
// Positioned: the clear-trigger sits over the trigger's inline end.
|
|
2166
2733
|
root: {
|
|
2167
|
-
base: { display: 'inline-flex', flexDirection: 'column' },
|
|
2734
|
+
base: { display: 'inline-flex', flexDirection: 'column', position: 'relative' },
|
|
2168
2735
|
},
|
|
2169
2736
|
trigger: {
|
|
2170
2737
|
base: {
|
|
@@ -2177,6 +2744,8 @@ export const select: RecipeInput = {
|
|
|
2177
2744
|
},
|
|
2178
2745
|
states: {
|
|
2179
2746
|
hover: { borderColor: 'var(--color-base-content)' },
|
|
2747
|
+
// Readonly answers to nothing, so it does not invite a click.
|
|
2748
|
+
readonly: { cursor: 'default' },
|
|
2180
2749
|
disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
|
|
2181
2750
|
open: { borderColor: 'var(--select-accent)' },
|
|
2182
2751
|
closed: {},
|
|
@@ -2213,6 +2782,41 @@ export const select: RecipeInput = {
|
|
|
2213
2782
|
indicator: {
|
|
2214
2783
|
base: { opacity: '0.6', transition: 'transform var(--duration-normal) var(--ease-standard)' },
|
|
2215
2784
|
states: { open: { transform: 'rotate(180deg)' }, closed: {} },
|
|
2785
|
+
// Room for the clear-trigger laid over the field (#280).
|
|
2786
|
+
selectors: { [SELECT_CLEARABLE]: { marginInlineStart: 'calc(var(--space-2xl) + var(--space-sm))' } },
|
|
2787
|
+
},
|
|
2788
|
+
// Clears the selection (#280): daisy's ghost-btn over the field's
|
|
2789
|
+
// inline end, just before the chevron — muted like it, the base-200
|
|
2790
|
+
// wash on hover, the base-content ring on focus.
|
|
2791
|
+
'clear-trigger': {
|
|
2792
|
+
base: {
|
|
2793
|
+
appearance: 'none',
|
|
2794
|
+
position: 'absolute',
|
|
2795
|
+
insetBlock: '0',
|
|
2796
|
+
// Centred in the field at WCAG 2.5.8's 24px target, shrinking
|
|
2797
|
+
// only with a field too short to hold it.
|
|
2798
|
+
marginBlock: 'auto',
|
|
2799
|
+
blockSize: 'min(1.5rem, 100%)',
|
|
2800
|
+
minInlineSize: '1.5rem',
|
|
2801
|
+
insetInlineEnd: 'calc(var(--size-field) * 4 + 1em + var(--space-xs))',
|
|
2802
|
+
display: 'inline-flex',
|
|
2803
|
+
alignItems: 'center',
|
|
2804
|
+
justifyContent: 'center',
|
|
2805
|
+
padding: '0 var(--space-xs)',
|
|
2806
|
+
border: 'none',
|
|
2807
|
+
background: 'transparent',
|
|
2808
|
+
color: 'color-mix(in oklab, var(--color-base-content) 70%, transparent)',
|
|
2809
|
+
font: 'inherit',
|
|
2810
|
+
fontSize: 'var(--text-sm)',
|
|
2811
|
+
lineHeight: 'var(--leading-none)',
|
|
2812
|
+
borderRadius: 'var(--radius-field)',
|
|
2813
|
+
cursor: 'pointer',
|
|
2814
|
+
transition: 'background var(--duration-fast) var(--ease-standard)',
|
|
2815
|
+
},
|
|
2816
|
+
states: {
|
|
2817
|
+
hover: { background: 'var(--color-base-200)', color: 'var(--color-base-content)' },
|
|
2818
|
+
...focusRing,
|
|
2819
|
+
},
|
|
2216
2820
|
},
|
|
2217
2821
|
popup: withPresence(popupPresence('translateY(-4px)'), {
|
|
2218
2822
|
base: { ...floatingPanel, padding: 'var(--space-md)', minWidth: '13rem' },
|
|
@@ -2261,6 +2865,14 @@ export const select: RecipeInput = {
|
|
|
2261
2865
|
base: { fontSize: 'var(--text-xs)', color: 'var(--select-accent)' },
|
|
2262
2866
|
states: { selected: {} },
|
|
2263
2867
|
},
|
|
2868
|
+
// The menu's rule between runs of options (#280).
|
|
2869
|
+
separator: {
|
|
2870
|
+
base: {
|
|
2871
|
+
height: 'var(--border)',
|
|
2872
|
+
margin: 'var(--space-sm) var(--space-md)',
|
|
2873
|
+
background: 'var(--color-base-300)',
|
|
2874
|
+
},
|
|
2875
|
+
},
|
|
2264
2876
|
},
|
|
2265
2877
|
variants: {
|
|
2266
2878
|
color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
|
|
@@ -2283,8 +2895,28 @@ export const select: RecipeInput = {
|
|
|
2283
2895
|
// The trigger ramp restates `md` too (unlike `btnSizes`), so its lynx
|
|
2284
2896
|
// counterpart does as well.
|
|
2285
2897
|
targets: {
|
|
2898
|
+
web: { parts: { popup: { base: anchoredListbox } } },
|
|
2286
2899
|
lynx: {
|
|
2287
|
-
parts: {
|
|
2900
|
+
parts: {
|
|
2901
|
+
// daisy's `.select` is `width: clamp(3rem, 20rem, 100%)`: 20rem,
|
|
2902
|
+
// never wider than its container. lynx has no `clamp()`, so the
|
|
2903
|
+
// same bounds are spelled out — and the base's 13rem floor
|
|
2904
|
+
// comes down to daisy's 3rem, or the trigger overflowed any
|
|
2905
|
+
// cell narrower than 208px (signalxjs/lynx#1146). The root
|
|
2906
|
+
// caps too, so a shrink-wrapped root cannot push it back out.
|
|
2907
|
+
root: { base: { maxWidth: '100%' } },
|
|
2908
|
+
trigger: {
|
|
2909
|
+
base: { ...lynxBtnPad(4), width: '20rem', maxWidth: '100%', minWidth: '3rem' },
|
|
2910
|
+
// The web's `:hover` border is dropped on a touch
|
|
2911
|
+
// platform; a press gets a wash instead — a background,
|
|
2912
|
+
// so it never fights the open/invalid border inks.
|
|
2913
|
+
states: { pressed: { background: 'var(--color-base-200)' } },
|
|
2914
|
+
},
|
|
2915
|
+
// A touch list has no highlight: the pressed item takes the
|
|
2916
|
+
// highlighted item's wash.
|
|
2917
|
+
item: { states: { pressed: { background: 'var(--color-base-200)' } } },
|
|
2918
|
+
'clear-trigger': { base: { top: 'var(--space-xs)', bottom: 'var(--space-xs)', right: 'calc(var(--size-field) * 4 + 1em + var(--space-xs))', minWidth: '1.5rem' } },
|
|
2919
|
+
},
|
|
2288
2920
|
variants: {
|
|
2289
2921
|
size: { ...lynxBtnSizes('trigger'), md: { trigger: { base: lynxBtnPad(4) } } },
|
|
2290
2922
|
},
|
|
@@ -2310,6 +2942,23 @@ const BTN_STEPS = { xs: 6, sm: 8, md: 10, lg: 12, xl: 14 } as const;
|
|
|
2310
2942
|
const btnHeight = (step: keyof typeof BTN_STEPS): string =>
|
|
2311
2943
|
`calc(var(--size-field) * ${BTN_STEPS[step]})`;
|
|
2312
2944
|
|
|
2945
|
+
/**
|
|
2946
|
+
* daisy 5's disabled btn, for the lynx target (signalxjs/lynx#1143). The web
|
|
2947
|
+
* fades the whole button (`opacity: var(--disabled-opacity)`, 0.3 on the
|
|
2948
|
+
* dark themes), which on device left a solid button's label barely legible.
|
|
2949
|
+
* daisy's own `.btn:disabled` paints instead: a base-content wash (not on
|
|
2950
|
+
* ghost/link), a transparent border, no shadow, and a muted base-content
|
|
2951
|
+
* label. daisy mutes that label to 20%; this keeps it at 50%, still plainly
|
|
2952
|
+
* inert but readable on a phone at arm's length.
|
|
2953
|
+
*/
|
|
2954
|
+
const lynxBtnDisabled: CssProps = {
|
|
2955
|
+
opacity: '1',
|
|
2956
|
+
background: 'color-mix(in oklab, var(--color-base-content) 10%, transparent)',
|
|
2957
|
+
color: 'color-mix(in oklab, var(--color-base-content) 50%, transparent)',
|
|
2958
|
+
borderColor: 'transparent',
|
|
2959
|
+
boxShadow: 'none',
|
|
2960
|
+
};
|
|
2961
|
+
|
|
2313
2962
|
export const button: RecipeInput = {
|
|
2314
2963
|
component: 'button',
|
|
2315
2964
|
// Public to a design system derived from this one (#73).
|
|
@@ -2525,11 +3174,39 @@ export const button: RecipeInput = {
|
|
|
2525
3174
|
},
|
|
2526
3175
|
defaultVariants: { color: 'primary', variant: 'solid', size: 'md' },
|
|
2527
3176
|
keyframes: { 'zero-daisyui-btn-spin': 'to { transform: rotate(360deg) }' },
|
|
2528
|
-
// The icon chips' zeroed inline padding, restated physically: logical
|
|
2529
|
-
// spellings resolve on iOS but not on Android (measured,
|
|
2530
|
-
// signalxjs/lynx#1084), so the emitter refuses them.
|
|
2531
3177
|
targets: {
|
|
2532
3178
|
lynx: {
|
|
3179
|
+
parts: {
|
|
3180
|
+
// The ring, restated: `currentColor` never resolves on lynx
|
|
3181
|
+
// (signalxjs/lynx#1079) and the logical sizes/edge are
|
|
3182
|
+
// unproven there, so the lynx ring is physical longhands in
|
|
3183
|
+
// the variant's own ink (the variants below pick it).
|
|
3184
|
+
spinner: {
|
|
3185
|
+
base: {
|
|
3186
|
+
width: '1em',
|
|
3187
|
+
height: '1em',
|
|
3188
|
+
border: 'calc(var(--border) * 2) solid var(--btn-ink)',
|
|
3189
|
+
borderTopColor: 'transparent',
|
|
3190
|
+
},
|
|
3191
|
+
},
|
|
3192
|
+
root: { states: { disabled: lynxBtnDisabled } },
|
|
3193
|
+
},
|
|
3194
|
+
variants: {
|
|
3195
|
+
// The baked disabled paint above outranks every variant's fill
|
|
3196
|
+
// (the theme host adds a class), but its plain declarations
|
|
3197
|
+
// do not: outline/dash restate the border, ghost/link drop
|
|
3198
|
+
// the wash (as in daisy).
|
|
3199
|
+
variant: {
|
|
3200
|
+
solid: { spinner: { base: { borderColor: 'var(--btn-on-accent)', borderTopColor: 'transparent' } } },
|
|
3201
|
+
outline: { root: { states: { disabled: { borderColor: 'transparent' } } } },
|
|
3202
|
+
dash: { root: { states: { disabled: { borderColor: 'transparent' } } } },
|
|
3203
|
+
ghost: { root: { states: { disabled: { background: 'transparent' } } } },
|
|
3204
|
+
link: { root: { states: { disabled: { background: 'transparent' } } } },
|
|
3205
|
+
},
|
|
3206
|
+
},
|
|
3207
|
+
// The icon chips' zeroed inline padding, restated physically: logical
|
|
3208
|
+
// spellings resolve on iOS but not on Android (measured,
|
|
3209
|
+
// signalxjs/lynx#1084), so the emitter refuses them.
|
|
2533
3210
|
modifiers: {
|
|
2534
3211
|
square: { root: { base: { paddingLeft: '0', paddingRight: '0' } } },
|
|
2535
3212
|
circle: { root: { base: { paddingLeft: '0', paddingRight: '0' } } },
|
|
@@ -2770,21 +3447,66 @@ export const toast: RecipeInput = {
|
|
|
2770
3447
|
// signalxjs/lynx#1070. No `color` default: the un-attributed toast is a
|
|
2771
3448
|
// neutral base-200 surface, outside the color vocabulary.
|
|
2772
3449
|
defaultVariants: { size: 'md' },
|
|
2773
|
-
// The corner placements' physical anchors: logical inset spellings
|
|
2774
|
-
// resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
|
|
2775
|
-
// the emitter refuses them. Physical is the lynx target's norm — no RTL
|
|
2776
|
-
// flow there, so `start` IS left and `end` IS right.
|
|
2777
3450
|
targets: {
|
|
2778
3451
|
lynx: {
|
|
2779
3452
|
parts: {
|
|
3453
|
+
// The viewport is a full-width strip on lynx (the runtime pins
|
|
3454
|
+
// it to the outlet's top or bottom edge). What the web gets
|
|
3455
|
+
// from `:popover-open` and `min(24rem, 100vw)` is restated:
|
|
3456
|
+
// flex (a lynx view defaults to linear layout, which ignores
|
|
3457
|
+
// flex-direction and gap), and the cards aligned to the
|
|
3458
|
+
// placement's edge. The centred pair's `left: 50%` +
|
|
3459
|
+
// `translateX(-50%)` pull-back is for a shrink-wrapped popover;
|
|
3460
|
+
// on the strip it shifted everything half a width left, so it
|
|
3461
|
+
// is cancelled. The corner anchors are physical: logical
|
|
3462
|
+
// insets resolve on iOS but not on Android (measured,
|
|
3463
|
+
// signalxjs/lynx#1084), and lynx has no RTL flow, so `start`
|
|
3464
|
+
// IS left and `end` IS right.
|
|
2780
3465
|
viewport: {
|
|
3466
|
+
base: { display: 'flex', width: 'auto' },
|
|
3467
|
+
selectors: {
|
|
3468
|
+
'&[data-placement="top-start"]': { left: '0', alignItems: 'flex-start' },
|
|
3469
|
+
'&[data-placement="top"]': { left: '0', right: '0', transform: 'none', alignItems: 'center' },
|
|
3470
|
+
'&[data-placement="top-end"]': { right: '0', alignItems: 'flex-end' },
|
|
3471
|
+
'&[data-placement="bottom-start"]': { left: '0', alignItems: 'flex-start' },
|
|
3472
|
+
'&[data-placement="bottom"]': { left: '0', right: '0', transform: 'none', alignItems: 'center' },
|
|
3473
|
+
'&[data-placement="bottom-end"]': { right: '0', alignItems: 'flex-end' },
|
|
3474
|
+
},
|
|
3475
|
+
},
|
|
3476
|
+
// No grid on lynx (signalxjs/lynx#1075): the card is a flex
|
|
3477
|
+
// column (title, description, then the action), with the
|
|
3478
|
+
// close button pinned to the top-right corner and the title
|
|
3479
|
+
// kept clear of it. The web's 24rem viewport cap becomes the
|
|
3480
|
+
// card's own cap. The `^="top"` entry offset has no class
|
|
3481
|
+
// form, so each top placement restates it.
|
|
3482
|
+
root: {
|
|
3483
|
+
base: {
|
|
3484
|
+
display: 'flex',
|
|
3485
|
+
flexDirection: 'column',
|
|
3486
|
+
alignItems: 'flex-start',
|
|
3487
|
+
position: 'relative',
|
|
3488
|
+
boxSizing: 'border-box',
|
|
3489
|
+
width: '100%',
|
|
3490
|
+
maxWidth: '24rem',
|
|
3491
|
+
},
|
|
2781
3492
|
selectors: {
|
|
2782
|
-
'&[data-placement="top-start"]': {
|
|
2783
|
-
'&[data-placement="top
|
|
2784
|
-
'&[data-placement="
|
|
2785
|
-
'&[data-placement="bottom-end"]': { right: '0' },
|
|
3493
|
+
'&[data-placement="top-start"]': { '--toast-from': '-8px' },
|
|
3494
|
+
'&[data-placement="top"]': { '--toast-from': '-8px' },
|
|
3495
|
+
'&[data-placement="top-end"]': { '--toast-from': '-8px' },
|
|
2786
3496
|
},
|
|
2787
3497
|
},
|
|
3498
|
+
title: { base: { paddingRight: '1.75em' } },
|
|
3499
|
+
// The 75% ink mix over a recipe-local property cannot bake;
|
|
3500
|
+
// the same ink at 75% opacity reads the same on the soft fill.
|
|
3501
|
+
description: { base: { color: 'var(--toast-ink)', opacity: '0.75' } },
|
|
3502
|
+
// The 25% ink border cannot bake either: it takes the full ink.
|
|
3503
|
+
action: {
|
|
3504
|
+
base: {
|
|
3505
|
+
marginTop: 'var(--space-xs)',
|
|
3506
|
+
border: 'var(--border) solid var(--toast-ink)',
|
|
3507
|
+
},
|
|
3508
|
+
},
|
|
3509
|
+
close: { base: { position: 'absolute', top: 'var(--space-xs)', right: 'var(--space-xs)' } },
|
|
2788
3510
|
},
|
|
2789
3511
|
},
|
|
2790
3512
|
},
|
|
@@ -2916,6 +3638,23 @@ export const combobox: RecipeInput = {
|
|
|
2916
3638
|
disabled: { cursor: 'not-allowed' },
|
|
2917
3639
|
},
|
|
2918
3640
|
},
|
|
3641
|
+
// Clears the value and the text (#280): the chevron's ghost, one
|
|
3642
|
+
// step before it — muted, full ink on hover.
|
|
3643
|
+
'clear-trigger': {
|
|
3644
|
+
base: {
|
|
3645
|
+
appearance: 'none',
|
|
3646
|
+
border: 'none',
|
|
3647
|
+
background: 'transparent',
|
|
3648
|
+
color: 'inherit',
|
|
3649
|
+
font: 'inherit',
|
|
3650
|
+
opacity: '0.6',
|
|
3651
|
+
padding: '0 var(--space-xs)',
|
|
3652
|
+
lineHeight: 'var(--leading-none)',
|
|
3653
|
+
cursor: 'pointer',
|
|
3654
|
+
transition: 'opacity var(--duration-fast) var(--ease-standard)',
|
|
3655
|
+
},
|
|
3656
|
+
states: { hover: { opacity: '1' } },
|
|
3657
|
+
},
|
|
2919
3658
|
popup: withPresence(popupPresence('translateY(-4px)'), {
|
|
2920
3659
|
base: { ...floatingPanel, padding: 'var(--space-md)', minWidth: '13rem' },
|
|
2921
3660
|
states: { open: {}, closed: {} },
|
|
@@ -2971,6 +3710,23 @@ export const combobox: RecipeInput = {
|
|
|
2971
3710
|
opacity: '0.6',
|
|
2972
3711
|
},
|
|
2973
3712
|
},
|
|
3713
|
+
// The list still arriving (#280): the empty row's muted grammar.
|
|
3714
|
+
loading: {
|
|
3715
|
+
base: {
|
|
3716
|
+
padding: 'var(--space-lg)',
|
|
3717
|
+
fontSize: 'var(--text-sm)',
|
|
3718
|
+
textAlign: 'center',
|
|
3719
|
+
opacity: '0.6',
|
|
3720
|
+
},
|
|
3721
|
+
},
|
|
3722
|
+
// The menu's rule between runs of options (#280).
|
|
3723
|
+
separator: {
|
|
3724
|
+
base: {
|
|
3725
|
+
height: 'var(--border)',
|
|
3726
|
+
margin: 'var(--space-sm) var(--space-md)',
|
|
3727
|
+
background: 'var(--color-base-300)',
|
|
3728
|
+
},
|
|
3729
|
+
},
|
|
2974
3730
|
},
|
|
2975
3731
|
variants: {
|
|
2976
3732
|
color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
|
|
@@ -2993,6 +3749,9 @@ export const combobox: RecipeInput = {
|
|
|
2993
3749
|
input: ['focus-visible'],
|
|
2994
3750
|
trigger: ['focus-visible'],
|
|
2995
3751
|
},
|
|
3752
|
+
targets: {
|
|
3753
|
+
web: { parts: { popup: { base: anchoredListbox } } },
|
|
3754
|
+
},
|
|
2996
3755
|
};
|
|
2997
3756
|
|
|
2998
3757
|
// daisy "btn" that stays pressed: the off state is the neutral btn, the on
|
|
@@ -3606,7 +4365,9 @@ export const treeView: RecipeInput = {
|
|
|
3606
4365
|
opacity: '0.6',
|
|
3607
4366
|
transition: 'transform var(--duration-fast) var(--ease-standard)',
|
|
3608
4367
|
},
|
|
3609
|
-
|
|
4368
|
+
// Loading: the chevron halts at daisy's own collapse-arrow angle —
|
|
4369
|
+
// static (no loop), ink unchanged, so contrast is untouched.
|
|
4370
|
+
states: { open: { transform: 'rotate(90deg)' }, closed: {}, loading: { transform: 'rotate(45deg)' } },
|
|
3610
4371
|
// The glyph is element text the runtime renders (`TreeView.tsx`), not
|
|
3611
4372
|
// `content:`, so the `:dir(rtl)` swap the submenu chevron uses is not
|
|
3612
4373
|
// available here — a mirror is its equivalent. `scale` composes
|
|
@@ -3625,6 +4386,9 @@ export const treeView: RecipeInput = {
|
|
|
3625
4386
|
states: { open: {}, closed: {} },
|
|
3626
4387
|
},
|
|
3627
4388
|
},
|
|
4389
|
+
// Open-and-loading content lays out exactly as open content does — the
|
|
4390
|
+
// indicator and the treeitem's aria-busy carry the state.
|
|
4391
|
+
sameAs: { 'branch-content': { loading: 'open' } },
|
|
3628
4392
|
variants: {
|
|
3629
4393
|
// A tree colours one thing: the selected row. Everything else is
|
|
3630
4394
|
// structure, and tinting it would fight the content.
|
|
@@ -3654,6 +4418,33 @@ export const treeView: RecipeInput = {
|
|
|
3654
4418
|
},
|
|
3655
4419
|
};
|
|
3656
4420
|
|
|
4421
|
+
/**
|
|
4422
|
+
* The affordances inside a text field (#281): daisy's in-field label ink and
|
|
4423
|
+
* the ghost circle button, both following the size step.
|
|
4424
|
+
*/
|
|
4425
|
+
const affixInk = 'color-mix(in oklab, var(--color-base-content) 70%, transparent)';
|
|
4426
|
+
const fieldButton: NonNullable<PartStyles['base']> = {
|
|
4427
|
+
appearance: 'none',
|
|
4428
|
+
border: 'none',
|
|
4429
|
+
background: 'transparent',
|
|
4430
|
+
color: affixInk,
|
|
4431
|
+
borderRadius: '9999px',
|
|
4432
|
+
alignSelf: 'center',
|
|
4433
|
+
flex: 'none',
|
|
4434
|
+
order: '1',
|
|
4435
|
+
marginInlineEnd: 'var(--space-xs)',
|
|
4436
|
+
padding: 'var(--space-2xs) var(--space-xs)',
|
|
4437
|
+
fontSize: 'var(--text-sm)',
|
|
4438
|
+
fontWeight: 'var(--weight-semibold)',
|
|
4439
|
+
lineHeight: 'var(--leading-none)',
|
|
4440
|
+
cursor: 'pointer',
|
|
4441
|
+
};
|
|
4442
|
+
const affixSize = (fontSize: string) => ({
|
|
4443
|
+
adornment: { base: { fontSize } },
|
|
4444
|
+
'clear-trigger': { base: { fontSize } },
|
|
4445
|
+
'visibility-trigger': { base: { fontSize } },
|
|
4446
|
+
});
|
|
4447
|
+
|
|
3657
4448
|
/**
|
|
3658
4449
|
* daisy "input" flavor: the shared field box with nothing in it but the text.
|
|
3659
4450
|
* Same `fieldControl` metrics as select's trigger, combobox's control and the
|
|
@@ -3680,6 +4471,7 @@ export const input: RecipeInput = {
|
|
|
3680
4471
|
base: {
|
|
3681
4472
|
display: 'inline-flex',
|
|
3682
4473
|
alignItems: 'stretch',
|
|
4474
|
+
gap: 'var(--space-2xs)',
|
|
3683
4475
|
...fieldControl,
|
|
3684
4476
|
overflow: 'hidden',
|
|
3685
4477
|
transition: 'border-color var(--duration-fast) var(--ease-standard)',
|
|
@@ -3716,6 +4508,43 @@ export const input: RecipeInput = {
|
|
|
3716
4508
|
'&::placeholder': { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' },
|
|
3717
4509
|
},
|
|
3718
4510
|
},
|
|
4511
|
+
// daisy's `label` inside `.input`: a quiet prefix/suffix at one edge,
|
|
4512
|
+
// ordered logically so it flips with the reading direction.
|
|
4513
|
+
adornment: {
|
|
4514
|
+
base: {
|
|
4515
|
+
display: 'inline-flex',
|
|
4516
|
+
alignItems: 'center',
|
|
4517
|
+
flex: 'none',
|
|
4518
|
+
color: affixInk,
|
|
4519
|
+
fontSize: 'var(--text-sm)',
|
|
4520
|
+
lineHeight: 'var(--leading-none)',
|
|
4521
|
+
},
|
|
4522
|
+
states: { disabled: {} },
|
|
4523
|
+
selectors: {
|
|
4524
|
+
'&[data-placement="start"]': { order: '-1', paddingInlineStart: 'var(--space-md)' },
|
|
4525
|
+
'&[data-placement="end"]': { order: '1', paddingInlineEnd: 'var(--space-md)' },
|
|
4526
|
+
},
|
|
4527
|
+
},
|
|
4528
|
+
// `btn btn-ghost btn-circle btn-xs` inside the field.
|
|
4529
|
+
'clear-trigger': {
|
|
4530
|
+
base: fieldButton,
|
|
4531
|
+
states: {
|
|
4532
|
+
hover: { color: 'var(--color-base-content)', background: 'var(--color-base-200)' },
|
|
4533
|
+
disabled: { cursor: 'not-allowed' },
|
|
4534
|
+
...focusRing,
|
|
4535
|
+
},
|
|
4536
|
+
},
|
|
4537
|
+
// Shown: the ghost button's active fill and full ink.
|
|
4538
|
+
'visibility-trigger': {
|
|
4539
|
+
base: fieldButton,
|
|
4540
|
+
states: {
|
|
4541
|
+
on: { color: 'var(--color-base-content)', background: 'var(--color-base-200)' },
|
|
4542
|
+
off: {},
|
|
4543
|
+
hover: { color: 'var(--color-base-content)', background: 'var(--color-base-200)' },
|
|
4544
|
+
disabled: { cursor: 'not-allowed' },
|
|
4545
|
+
...focusRing,
|
|
4546
|
+
},
|
|
4547
|
+
},
|
|
3719
4548
|
},
|
|
3720
4549
|
// The visible ring lives on `control`; the input delegates.
|
|
3721
4550
|
skipStates: { input: ['focus-visible'] },
|
|
@@ -3728,13 +4557,13 @@ export const input: RecipeInput = {
|
|
|
3728
4557
|
// The control carries the height off the shared field ramp; the text
|
|
3729
4558
|
// follows so the box stays proportional.
|
|
3730
4559
|
size: {
|
|
3731
|
-
xs: { control: { base: { height: fieldHeight('xs') } }, input: { base: { fontSize: 'var(--text-xs)', padding: 'var(--space-2xs) var(--space-xs)' } } },
|
|
3732
|
-
sm: { control: { base: { height: fieldHeight('sm') } }, input: { base: { fontSize: 'var(--text-xs)', padding: 'var(--space-xs) var(--space-sm)' } } },
|
|
4560
|
+
xs: { control: { base: { height: fieldHeight('xs') } }, input: { base: { fontSize: 'var(--text-xs)', padding: 'var(--space-2xs) var(--space-xs)' } }, ...affixSize('var(--text-xs)') },
|
|
4561
|
+
sm: { control: { base: { height: fieldHeight('sm') } }, input: { base: { fontSize: 'var(--text-xs)', padding: 'var(--space-xs) var(--space-sm)' } }, ...affixSize('var(--text-xs)') },
|
|
3733
4562
|
// `md` is the un-attributed render: the base already IS the
|
|
3734
4563
|
// middle step.
|
|
3735
4564
|
md: {},
|
|
3736
|
-
lg: { control: { base: { height: fieldHeight('lg') } }, input: { base: { fontSize: 'var(--text-md)', padding: 'var(--space-md) var(--space-lg)' } } },
|
|
3737
|
-
xl: { control: { base: { height: fieldHeight('xl') } }, input: { base: { fontSize: 'var(--text-lg)', padding: 'var(--space-lg) var(--space-xl)' } } },
|
|
4565
|
+
lg: { control: { base: { height: fieldHeight('lg') } }, input: { base: { fontSize: 'var(--text-md)', padding: 'var(--space-md) var(--space-lg)' } }, ...affixSize('var(--text-md)') },
|
|
4566
|
+
xl: { control: { base: { height: fieldHeight('xl') } }, input: { base: { fontSize: 'var(--text-lg)', padding: 'var(--space-lg) var(--space-xl)' } }, ...affixSize('var(--text-lg)') },
|
|
3738
4567
|
},
|
|
3739
4568
|
},
|
|
3740
4569
|
};
|
|
@@ -4462,8 +5291,17 @@ export const stats: RecipeInput = {
|
|
|
4462
5291
|
tokens: { '--stats-accent': 'var(--color-base-content)' },
|
|
4463
5292
|
parts: {
|
|
4464
5293
|
root: {
|
|
5294
|
+
// The row's own scroll box (#43): a value never wraps or shrinks
|
|
5295
|
+
// (`nowrap` below), so a row too wide for its container scrolls
|
|
5296
|
+
// inside the root — daisyUI's own `.stats` behaviour, and Table's
|
|
5297
|
+
// and Pagination's answer — instead of pushing the page sideways.
|
|
5298
|
+
// Items keep their flex automatic minimum — their min-content
|
|
5299
|
+
// width — so the overflow lands here rather than in the next
|
|
5300
|
+
// item. No ring room: a stat is not focusable, and anything a
|
|
5301
|
+
// consumer puts in one sits inside the item's own padding.
|
|
4465
5302
|
base: {
|
|
4466
5303
|
display: 'flex',
|
|
5304
|
+
overflowX: 'auto',
|
|
4467
5305
|
border: 'var(--border) solid var(--color-base-200)',
|
|
4468
5306
|
borderRadius: 'var(--radius-box)',
|
|
4469
5307
|
background: 'var(--color-base-100)',
|
|
@@ -4500,6 +5338,8 @@ export const stats: RecipeInput = {
|
|
|
4500
5338
|
value: {
|
|
4501
5339
|
base: {
|
|
4502
5340
|
gridColumn: '1',
|
|
5341
|
+
// A figure is read whole: `$12 930` never breaks at its space.
|
|
5342
|
+
whiteSpace: 'nowrap',
|
|
4503
5343
|
fontSize: 'var(--text-2xl)',
|
|
4504
5344
|
fontWeight: 'var(--weight-bold)',
|
|
4505
5345
|
fontVariantNumeric: 'tabular-nums',
|
|
@@ -4522,8 +5362,19 @@ export const stats: RecipeInput = {
|
|
|
4522
5362
|
},
|
|
4523
5363
|
},
|
|
4524
5364
|
variants: {
|
|
4525
|
-
|
|
4526
|
-
|
|
5365
|
+
// Keyed on the ITEM, not the root (#161): the item re-carries
|
|
5366
|
+
// `color`, so a stat's own value outranks the row's, and the value
|
|
5367
|
+
// inside it inherits whichever won. A root colour still reaches
|
|
5368
|
+
// every item through the carrier's donut.
|
|
5369
|
+
//
|
|
5370
|
+
// The value is TEXT on the page, so the raw role is not always a
|
|
5371
|
+
// readable ink: measured by the text matrix once the item re-carried
|
|
5372
|
+
// the axis (#161), light warning is 1.76:1 and dark neutral 1.26:1
|
|
5373
|
+
// on base-100. `roleInk` is the shared answer for a role used as ink
|
|
5374
|
+
// (a per-role share of the role over base-content, which flips with
|
|
5375
|
+
// the scheme), the same one the field and button text use.
|
|
5376
|
+
color: Object.fromEntries(ROLES.map((c) => [c, { item: { base: {
|
|
5377
|
+
'--stats-accent': roleInk(c),
|
|
4527
5378
|
} } }])),
|
|
4528
5379
|
size: {
|
|
4529
5380
|
xs: { value: { base: { fontSize: 'var(--text-lg)' } } },
|
|
@@ -4571,7 +5422,11 @@ export const timeline: RecipeInput = {
|
|
|
4571
5422
|
padding: '0',
|
|
4572
5423
|
},
|
|
4573
5424
|
selectors: {
|
|
4574
|
-
|
|
5425
|
+
// A horizontal timeline is a row of steps whose width follows
|
|
5426
|
+
// the step count, not the container: at phone width it scrolls
|
|
5427
|
+
// inside its own box, the answer Table and Pagination give
|
|
5428
|
+
// (#45), rather than widening the page.
|
|
5429
|
+
'&[data-orientation="horizontal"]': { flexDirection: 'row', overflowX: 'auto' },
|
|
4575
5430
|
},
|
|
4576
5431
|
},
|
|
4577
5432
|
/**
|
|
@@ -4715,6 +5570,87 @@ export const timeline: RecipeInput = {
|
|
|
4715
5570
|
xl: { marker: { base: { '--timeline-marker-size': 'calc(var(--size-selector) * 4)' } }, content: { base: { fontSize: 'var(--text-md)' } } },
|
|
4716
5571
|
},
|
|
4717
5572
|
},
|
|
5573
|
+
/**
|
|
5574
|
+
* Lynx has no grid, and it does not resolve `inline-size`/`block-size`:
|
|
5575
|
+
* the item fell back to the engine's default layout and the marker,
|
|
5576
|
+
* sized only logically, stretched into a pill (signalxjs/lynx#1145).
|
|
5577
|
+
*
|
|
5578
|
+
* The item is a wrapping flex row instead. Vertical: line one is the
|
|
5579
|
+
* marker beside its content (start-side content ordered before the
|
|
5580
|
+
* marker), and the connector takes line two by itself (`flex-basis:
|
|
5581
|
+
* 100%`), drawn as a left border under the marker's centre, which is the
|
|
5582
|
+
* web's row-2 segment. Horizontal: line one is the marker with the
|
|
5583
|
+
* connector running on to the item's end, and the content wraps under it
|
|
5584
|
+
* (start-side content above). Content grows from a zero basis, so long
|
|
5585
|
+
* text never wraps onto its own line.
|
|
5586
|
+
*
|
|
5587
|
+
* The web collapses the start track when no item has start-side content
|
|
5588
|
+
* (`:has()`); lynx has no such test. The vertical connector sits under a
|
|
5589
|
+
* marker at the item's start edge, which is where the marker is unless
|
|
5590
|
+
* that item has start-side content: an alternating vertical timeline is
|
|
5591
|
+
* web-only for now.
|
|
5592
|
+
*/
|
|
5593
|
+
targets: {
|
|
5594
|
+
lynx: {
|
|
5595
|
+
parts: {
|
|
5596
|
+
item: { base: { display: 'flex', flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center' } },
|
|
5597
|
+
marker: {
|
|
5598
|
+
base: {
|
|
5599
|
+
width: 'var(--timeline-marker-size)',
|
|
5600
|
+
height: 'var(--timeline-marker-size)',
|
|
5601
|
+
flexGrow: '0',
|
|
5602
|
+
flexShrink: '0',
|
|
5603
|
+
},
|
|
5604
|
+
},
|
|
5605
|
+
connector: {
|
|
5606
|
+
selectors: {
|
|
5607
|
+
'&[data-orientation="vertical"]': {
|
|
5608
|
+
flexGrow: '0',
|
|
5609
|
+
flexShrink: '1',
|
|
5610
|
+
flexBasis: '100%',
|
|
5611
|
+
height: 'var(--space-lg)',
|
|
5612
|
+
background: 'transparent',
|
|
5613
|
+
borderLeftWidth: 'var(--border)',
|
|
5614
|
+
borderLeftStyle: 'solid',
|
|
5615
|
+
borderLeftColor: 'var(--color-base-300)',
|
|
5616
|
+
marginLeft: 'calc(var(--timeline-marker-size) / 2 - var(--border) / 2)',
|
|
5617
|
+
},
|
|
5618
|
+
'&[data-orientation="horizontal"]': {
|
|
5619
|
+
order: '1',
|
|
5620
|
+
flexGrow: '1',
|
|
5621
|
+
flexShrink: '1',
|
|
5622
|
+
flexBasis: '0',
|
|
5623
|
+
height: 'var(--border)',
|
|
5624
|
+
minWidth: 'var(--space-lg)',
|
|
5625
|
+
},
|
|
5626
|
+
},
|
|
5627
|
+
},
|
|
5628
|
+
content: {
|
|
5629
|
+
selectors: {
|
|
5630
|
+
'&[data-orientation="vertical"][data-placement="start"]': {
|
|
5631
|
+
order: '-1',
|
|
5632
|
+
flexGrow: '1',
|
|
5633
|
+
flexShrink: '1',
|
|
5634
|
+
flexBasis: '0',
|
|
5635
|
+
},
|
|
5636
|
+
'&[data-orientation="vertical"][data-placement="end"]': {
|
|
5637
|
+
flexGrow: '1',
|
|
5638
|
+
flexShrink: '1',
|
|
5639
|
+
flexBasis: '0',
|
|
5640
|
+
},
|
|
5641
|
+
'&[data-orientation="horizontal"][data-placement="start"]': {
|
|
5642
|
+
order: '-1',
|
|
5643
|
+
flexBasis: '100%',
|
|
5644
|
+
},
|
|
5645
|
+
'&[data-orientation="horizontal"][data-placement="end"]': {
|
|
5646
|
+
order: '2',
|
|
5647
|
+
flexBasis: '100%',
|
|
5648
|
+
},
|
|
5649
|
+
},
|
|
5650
|
+
},
|
|
5651
|
+
},
|
|
5652
|
+
},
|
|
5653
|
+
},
|
|
4718
5654
|
};
|
|
4719
5655
|
|
|
4720
5656
|
/** daisy chat: the classic bubble, colour-refilled per role. */
|
|
@@ -5272,9 +6208,27 @@ export const pagination: RecipeInput = {
|
|
|
5272
6208
|
'--pg-accent-content': 'var(--color-primary-content)',
|
|
5273
6209
|
'--pg-size': 'calc(var(--size-field) * 10)',
|
|
5274
6210
|
'--pg-font': 'var(--text-sm)',
|
|
6211
|
+
// How far the focus ring reaches outside a control: the root's
|
|
6212
|
+
// scroll box pads by it so the ring is never clipped (#44).
|
|
6213
|
+
'--pg-ring-room': '4px',
|
|
5275
6214
|
},
|
|
5276
6215
|
parts: {
|
|
5277
|
-
root: {
|
|
6216
|
+
root: {
|
|
6217
|
+
// The row's own scroll box (#44): the window is constant-width by
|
|
6218
|
+
// design, so a narrow container scrolls it rather than clipping
|
|
6219
|
+
// it — Table's answer. The padding is room for the focus ring's
|
|
6220
|
+
// 2px + 2px offset, which the scroll box would otherwise clip at
|
|
6221
|
+
// the row's edges; the matching scroll padding keeps it clear
|
|
6222
|
+
// when focus scrolls a page into view.
|
|
6223
|
+
base: {
|
|
6224
|
+
display: 'flex',
|
|
6225
|
+
alignItems: 'center',
|
|
6226
|
+
gap: 'var(--space-2xs)',
|
|
6227
|
+
overflowX: 'auto',
|
|
6228
|
+
padding: 'var(--pg-ring-room)',
|
|
6229
|
+
scrollPaddingInline: 'var(--pg-ring-room)',
|
|
6230
|
+
},
|
|
6231
|
+
},
|
|
5278
6232
|
item: {
|
|
5279
6233
|
base: {
|
|
5280
6234
|
display: 'inline-flex',
|
|
@@ -5741,6 +6695,9 @@ export const drawer: RecipeInput = {
|
|
|
5741
6695
|
*/
|
|
5742
6696
|
export const table: RecipeInput = {
|
|
5743
6697
|
component: 'table',
|
|
6698
|
+
// A sortable header cell carries the sort state for `aria-sort`'s sake;
|
|
6699
|
+
// its trigger and indicator paint it.
|
|
6700
|
+
skipStates: { 'header-cell': ['ascending', 'descending', 'none'] },
|
|
5744
6701
|
// Public to a design system derived from this one (#73).
|
|
5745
6702
|
hooks: {
|
|
5746
6703
|
properties: {
|
|
@@ -5768,6 +6725,11 @@ export const table: RecipeInput = {
|
|
|
5768
6725
|
borderRadius: 'var(--radius-box)',
|
|
5769
6726
|
background: 'var(--color-base-100)',
|
|
5770
6727
|
},
|
|
6728
|
+
// The scroll box is a keyboard stop (#270): daisy's ink ring,
|
|
6729
|
+
// drawn inside the box (negative offset) — the root is as wide
|
|
6730
|
+
// as its column, so an outward ring would be clipped by
|
|
6731
|
+
// whatever scrolls around it.
|
|
6732
|
+
states: { 'focus-visible': { outline: '2px solid var(--color-base-content)', outlineOffset: '-2px' } },
|
|
5771
6733
|
// Stacked (Table.Root stack, #55): zero lays each row out as a block
|
|
5772
6734
|
// below the table's breakpoint; the card it becomes is drawn here.
|
|
5773
6735
|
at: tableStackAt(tokens, 'root', { border: '0', borderRadius: '0', background: 'transparent' }),
|
|
@@ -5821,6 +6783,55 @@ export const table: RecipeInput = {
|
|
|
5821
6783
|
},
|
|
5822
6784
|
at: tableStackAt(tokens, 'header-cell', { paddingInline: '0' }),
|
|
5823
6785
|
},
|
|
6786
|
+
// Sorting (#286): daisy's header label made a button — the sorted
|
|
6787
|
+
// column's label takes the full accent ink, and the mark beside it
|
|
6788
|
+
// turns with the direction.
|
|
6789
|
+
'sort-trigger': {
|
|
6790
|
+
base: {
|
|
6791
|
+
appearance: 'none',
|
|
6792
|
+
display: 'inline-flex',
|
|
6793
|
+
alignItems: 'center',
|
|
6794
|
+
margin: '0',
|
|
6795
|
+
border: '0',
|
|
6796
|
+
font: 'inherit',
|
|
6797
|
+
letterSpacing: 'inherit',
|
|
6798
|
+
textTransform: 'inherit',
|
|
6799
|
+
textAlign: 'inherit',
|
|
6800
|
+
cursor: 'pointer',
|
|
6801
|
+
gap: 'var(--space-2xs)',
|
|
6802
|
+
padding: '0',
|
|
6803
|
+
borderRadius: 'var(--radius-selector)',
|
|
6804
|
+
background: 'transparent',
|
|
6805
|
+
color: 'inherit',
|
|
6806
|
+
transition: 'color var(--duration-fast) var(--ease-standard)',
|
|
6807
|
+
},
|
|
6808
|
+
states: {
|
|
6809
|
+
ascending: { color: 'var(--table-accent)' },
|
|
6810
|
+
descending: { color: 'var(--table-accent)' },
|
|
6811
|
+
none: {},
|
|
6812
|
+
hover: { color: 'var(--table-accent)' },
|
|
6813
|
+
disabled: { cursor: 'not-allowed', opacity: 'var(--disabled-opacity)' },
|
|
6814
|
+
...focusRing,
|
|
6815
|
+
},
|
|
6816
|
+
at: { 'reduced-motion': { base: { transition: 'none' } } },
|
|
6817
|
+
},
|
|
6818
|
+
// zero's ▲, turned for descending; an unsorted column shows it only
|
|
6819
|
+
// under a hovered or keyboard-focused trigger.
|
|
6820
|
+
'sort-indicator': {
|
|
6821
|
+
base: {
|
|
6822
|
+
display: 'inline-block',
|
|
6823
|
+
fontSize: '0.75em',
|
|
6824
|
+
lineHeight: '1',
|
|
6825
|
+
transition: 'transform var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard)',
|
|
6826
|
+
},
|
|
6827
|
+
states: {
|
|
6828
|
+
ascending: {},
|
|
6829
|
+
descending: { transform: 'rotate(180deg)' },
|
|
6830
|
+
none: { opacity: '0' },
|
|
6831
|
+
},
|
|
6832
|
+
selectors: { '[data-scope="table"][data-part="sort-trigger"]:is(:hover, [data-focus-visible]) > &[data-state="none"]': { opacity: '0.6' } },
|
|
6833
|
+
at: { 'reduced-motion': { base: { transition: 'none' } } },
|
|
6834
|
+
},
|
|
5824
6835
|
cell: {
|
|
5825
6836
|
base: {
|
|
5826
6837
|
padding: 'var(--table-pad-block) var(--table-pad-inline)',
|
|
@@ -5958,7 +6969,15 @@ export const fileUpload: RecipeInput = {
|
|
|
5958
6969
|
borderRadius: 'var(--radius-field)',
|
|
5959
6970
|
background: 'var(--color-base-100)',
|
|
5960
6971
|
},
|
|
5961
|
-
states: {
|
|
6972
|
+
states: {
|
|
6973
|
+
disabled: { opacity: 'var(--disabled-opacity)' },
|
|
6974
|
+
// A rejected file the app renders through Item: daisy's
|
|
6975
|
+
// error border over a faint error wash.
|
|
6976
|
+
invalid: {
|
|
6977
|
+
borderColor: 'var(--color-error)',
|
|
6978
|
+
background: 'color-mix(in oklab, var(--color-error) 8%, var(--color-base-100))',
|
|
6979
|
+
},
|
|
6980
|
+
},
|
|
5962
6981
|
},
|
|
5963
6982
|
'item-name': {
|
|
5964
6983
|
base: {
|
|
@@ -5994,6 +7013,26 @@ export const fileUpload: RecipeInput = {
|
|
|
5994
7013
|
...focusRing,
|
|
5995
7014
|
},
|
|
5996
7015
|
},
|
|
7016
|
+
// daisy's btn-ghost btn-xs: the remove button's pill, at text size.
|
|
7017
|
+
'clear-trigger': {
|
|
7018
|
+
base: {
|
|
7019
|
+
appearance: 'none',
|
|
7020
|
+
border: 'none',
|
|
7021
|
+
background: 'transparent',
|
|
7022
|
+
color: 'inherit',
|
|
7023
|
+
borderRadius: '9999px',
|
|
7024
|
+
padding: 'var(--space-2xs) var(--space-sm)',
|
|
7025
|
+
fontSize: 'var(--fu-font)',
|
|
7026
|
+
fontWeight: 'var(--weight-semibold)',
|
|
7027
|
+
lineHeight: 'var(--leading-none)',
|
|
7028
|
+
cursor: 'pointer',
|
|
7029
|
+
},
|
|
7030
|
+
states: {
|
|
7031
|
+
hover: { background: 'var(--color-base-200)' },
|
|
7032
|
+
disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
|
|
7033
|
+
...focusRing,
|
|
7034
|
+
},
|
|
7035
|
+
},
|
|
5997
7036
|
},
|
|
5998
7037
|
variants: {
|
|
5999
7038
|
color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
|
|
@@ -6338,6 +7377,9 @@ export const diff: RecipeInput = {
|
|
|
6338
7377
|
background: 'var(--color-base-100)',
|
|
6339
7378
|
borderRadius: 'var(--radius-box)',
|
|
6340
7379
|
},
|
|
7380
|
+
// The root holds the images, which are content: disabled is the
|
|
7381
|
+
// handle's to show (#272).
|
|
7382
|
+
states: { disabled: {} },
|
|
6341
7383
|
},
|
|
6342
7384
|
before: {
|
|
6343
7385
|
base: { gridArea: '1 / 1', minWidth: '0' },
|
|
@@ -6367,6 +7409,9 @@ export const diff: RecipeInput = {
|
|
|
6367
7409
|
},
|
|
6368
7410
|
states: {
|
|
6369
7411
|
...focusRing,
|
|
7412
|
+
// daisy's disabled look (#272): the uniform fade, and no grab
|
|
7413
|
+
// cursor — the images are content and stay unfaded.
|
|
7414
|
+
disabled: { opacity: 'var(--disabled-opacity)', cursor: 'not-allowed' },
|
|
6370
7415
|
pressed: { '--diff-accent': 'color-mix(in oklab, var(--color-base-content) 85%, var(--color-base-100))' },
|
|
6371
7416
|
},
|
|
6372
7417
|
selectors: {
|
|
@@ -6441,7 +7486,7 @@ export const diff: RecipeInput = {
|
|
|
6441
7486
|
|
|
6442
7487
|
export const recipes: RecipeInput[] = [
|
|
6443
7488
|
tabs, collapsible, switchRecipe, dialog, popover, tooltip, menu,
|
|
6444
|
-
field, checkbox, radioGroup, progress, slider, accordion, select, button, avatar, toast, combobox,
|
|
7489
|
+
field, fieldset, checkbox, checkboxGroup, radioGroup, progress, slider, accordion, select, button, avatar, toast, combobox,
|
|
6445
7490
|
toggle, toggleGroup, numberInput, ratingGroup, treeView, input, textarea,
|
|
6446
7491
|
card, alert, emptyState, badge, divider, skeleton, spinner,
|
|
6447
7492
|
kbd, status, indicator, stats, timeline, chat, radialProgress, join,
|