actual-css 0.10.0 → 0.11.1
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/CHANGELOG.md +99 -1
- package/README.md +6 -2
- package/components.json +14 -3
- package/dist/actual.full.css +483 -128
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/enhancement-contract.md +16 -7
- package/docs/design-notes/platform-alignment.md +8 -4
- package/docs/design-notes/rich-text.md +61 -0
- package/docs/design-notes/steps.md +2 -2
- package/docs/navigation.json +4 -1
- package/docs/pages/components/aura.md +12 -13
- package/docs/pages/components/badge.md +10 -0
- package/docs/pages/components/bleed.md +5 -4
- package/docs/pages/components/button.md +1 -1
- package/docs/pages/components/chat.md +5 -0
- package/docs/pages/components/close.md +9 -3
- package/docs/pages/components/dialog.md +145 -13
- package/docs/pages/components/meter.md +2 -0
- package/docs/pages/components/pagination.md +15 -15
- package/docs/pages/components/shimmer.md +57 -0
- package/docs/pages/components/skeleton.md +2 -1
- package/docs/pages/components/spinner.md +11 -6
- package/docs/pages/components/table.md +13 -0
- package/docs/pages/enhancements/flyout.md +2 -0
- package/docs/pages/enhancements/tabs.md +4 -0
- package/docs/pages/examples/overview.md +1 -0
- package/docs/pages/forms/choice-cards.md +77 -4
- package/docs/pages/forms/combobox.md +16 -1
- package/docs/pages/forms/overview.md +4 -1
- package/docs/pages/forms/range.md +110 -5
- package/docs/pages/forms/rich-text.md +110 -0
- package/docs/pages/forms/validation.md +5 -1
- package/docs/pages/foundations/theming.md +49 -3
- package/docs/pages/foundations/tokens.md +15 -5
- package/docs/pages/guides/bootstrap.md +2 -1
- package/docs/pages/guides/design-tools.md +36 -0
- package/docs/pages/guides/introduction.md +1 -1
- package/docs/pages/guides/modular-import.md +1 -1
- package/docs/pages/guides/progressive-enhancement.md +13 -14
- package/docs/pages/guides/tailwind.md +4 -2
- package/llms.txt +4 -3
- package/package.json +6 -1
- package/reserved-classes.json +1 -0
- package/skills/actual-css/references/enhancements.md +35 -0
- package/src/cli/actual-css.js +49 -6
- package/src/css/components/avatar.css +1 -1
- package/src/css/components/bleed.css +5 -5
- package/src/css/components/button.css +14 -1
- package/src/css/components/close.css +17 -6
- package/src/css/components/flyout.css +4 -1
- package/src/css/components/meter.css +6 -3
- package/src/css/components/modal.css +123 -7
- package/src/css/components/pagination.css +3 -2
- package/src/css/components/rating.css +1 -1
- package/src/css/components/status-bar.css +2 -2
- package/src/css/components/steps.css +3 -3
- package/src/css/components/tab.css +3 -3
- package/src/css/components/table.css +6 -0
- package/src/css/components/tooltip.css +1 -1
- package/src/css/core/focus.css +14 -4
- package/src/css/core/theme.css +8 -0
- package/src/css/core/tokens.css +8 -4
- package/src/css/core/variants.css +2 -1
- package/src/css/effects/aura.css +51 -33
- package/src/css/effects/index.css +1 -0
- package/src/css/effects/shimmer.css +57 -0
- package/src/css/forms/choice-card.css +71 -23
- package/src/css/forms/control.css +2 -2
- package/src/css/forms/native.css +5 -1
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +64 -21
- package/src/js/command.js +61 -32
- package/src/js/dialog.js +8 -23
- package/src/js/dismiss.js +1 -2
- package/src/js/enhance.js +84 -123
- package/src/js/full.js +1 -0
- package/src/js/password.js +1 -8
- package/src/js/range.js +151 -0
- package/src/js/scrollspy.js +4 -5
- package/src/js/status.js +2 -10
- package/src/js/tooltip.js +11 -1
- package/src/js/validation.js +4 -10
- package/src/tooling/design-components.js +286 -0
- package/src/tooling/design-export.js +69 -0
- package/src/tooling/design-tokens.js +466 -0
- package/src/tooling/penpot-plugin/manifest.json +7 -0
- package/src/tooling/penpot-plugin/plugin.js +272 -0
- package/src/tooling/penpot-plugin/ui.html +58 -0
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Each .choice-card is a <label> wrapping a native radio or checkbox. When
|
|
7
7
|
* :has() is supported the input is visually hidden and the card mirrors its
|
|
8
|
-
* state:
|
|
9
|
-
*
|
|
10
|
-
* native input stays visible
|
|
8
|
+
* state: an indicator on the leading edge (a ring for radio, a box for
|
|
9
|
+
* checkbox) that fills with a check, checked border and tint, focus ring,
|
|
10
|
+
* disabled dimming. Without :has() the native input stays visible in the
|
|
11
|
+
* indicator's slot, so selection state never disappears.
|
|
11
12
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* The card is a two-column grid: the indicator in the first column, level
|
|
14
|
+
* with the first row, and each child element of the label on its own row in
|
|
15
|
+
* the second. Wrap loose text in an element. A corner badge for tiles is a
|
|
16
|
+
* documented recipe on these same layers, not a variant.
|
|
15
17
|
*
|
|
16
18
|
* .chip is the compact pill form (filter chips, selectable tags): control
|
|
17
19
|
* height, and a leading dot that turns into the check in a fixed slot, so
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
*
|
|
20
22
|
* Public hooks:
|
|
21
23
|
* --choice-card-pad
|
|
24
|
+
* --choice-card-gap
|
|
22
25
|
* --choice-card-radius
|
|
23
26
|
* --choice-card-border
|
|
24
27
|
* --choice-card-bg
|
|
@@ -33,19 +36,25 @@
|
|
|
33
36
|
|
|
34
37
|
.choice-card {
|
|
35
38
|
--choice-card-pad: var(--density-space, var(--space-40));
|
|
39
|
+
--choice-card-gap: var(--space-30);
|
|
36
40
|
--choice-card-radius: var(--radius);
|
|
37
41
|
--choice-card-border: var(--border);
|
|
38
42
|
--choice-card-bg: var(--surface);
|
|
39
|
-
--choice-card-check-size: 1.
|
|
43
|
+
--choice-card-check-size: 1.25em;
|
|
40
44
|
|
|
41
45
|
position: relative;
|
|
42
|
-
display:
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-columns: var(--choice-card-check-size) minmax(0, 1fr);
|
|
48
|
+
/* why: a card stretched by a taller grid neighbour keeps its rows at the
|
|
49
|
+
top instead of spreading them. */
|
|
50
|
+
align-content: start;
|
|
51
|
+
column-gap: var(--choice-card-gap);
|
|
43
52
|
padding: var(--choice-card-pad);
|
|
44
53
|
border: var(--border-width) solid var(--form-invalid-border, var(--choice-card-border));
|
|
45
54
|
border-radius: var(--choice-card-radius);
|
|
46
55
|
background: var(--choice-card-bg);
|
|
47
56
|
color: var(--text);
|
|
48
|
-
/* Consumed so .sm/.lg reach the card; the
|
|
57
|
+
/* Consumed so .sm/.lg reach the card; the indicator's em sizing follows. */
|
|
49
58
|
font-size: var(--control-font-size);
|
|
50
59
|
cursor: pointer;
|
|
51
60
|
transition:
|
|
@@ -54,6 +63,18 @@
|
|
|
54
63
|
box-shadow var(--duration);
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
.choice-card > * {
|
|
67
|
+
grid-column: 2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* The input owns the indicator's cell: visible there without :has(), hidden
|
|
71
|
+
in it with :has(). */
|
|
72
|
+
.choice-card > :is(input[type="radio"], input[type="checkbox"]) {
|
|
73
|
+
grid-area: 1 / 1;
|
|
74
|
+
place-self: center;
|
|
75
|
+
margin: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
/* The native input is visually hidden but stays focusable and participates in
|
|
58
79
|
form submission and keyboard navigation (sr-only pattern). Guarded: without
|
|
59
80
|
:has() the card cannot mirror the input's state, so the visible native
|
|
@@ -99,24 +120,31 @@
|
|
|
99
120
|
}
|
|
100
121
|
}
|
|
101
122
|
|
|
102
|
-
/*
|
|
103
|
-
the
|
|
123
|
+
/* Indicator: ::before is the ring or box, ::after the check. Both share the
|
|
124
|
+
input's cell, centred on the first line — not the first row, which a long
|
|
125
|
+
label wraps — and stay hidden until :has() can mirror the input. The em
|
|
126
|
+
margin is the lh-less fallback at the default 1.5 line-height. */
|
|
104
127
|
.choice-card::before,
|
|
105
128
|
.choice-card::after {
|
|
106
129
|
content: "";
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
grid-area: 1 / 1;
|
|
131
|
+
align-self: start;
|
|
132
|
+
margin-block-start: calc((1.5em - var(--choice-card-check-size)) / 2);
|
|
133
|
+
margin-block-start: calc((1lh - var(--choice-card-check-size)) / 2);
|
|
110
134
|
inline-size: var(--choice-card-check-size);
|
|
111
135
|
block-size: var(--choice-card-check-size);
|
|
112
136
|
transform: scale(0);
|
|
113
|
-
transition:
|
|
137
|
+
transition:
|
|
138
|
+
transform var(--duration),
|
|
139
|
+
background-color var(--duration),
|
|
140
|
+
border-color var(--duration);
|
|
114
141
|
}
|
|
115
142
|
|
|
116
|
-
/*
|
|
143
|
+
/* A border, not an inset shadow: forced colors drop shadows, and the resting
|
|
144
|
+
ring is the only sign of an unchecked option. The invalid border reaches it
|
|
145
|
+
through the same hook as the card's. */
|
|
117
146
|
.choice-card::before {
|
|
118
|
-
|
|
119
|
-
box-shadow: 0 0 0 2px var(--surface);
|
|
147
|
+
border: max(var(--border-width), 1.5px) solid var(--form-invalid-border, var(--text-muted));
|
|
120
148
|
z-index: 1;
|
|
121
149
|
}
|
|
122
150
|
|
|
@@ -138,7 +166,19 @@
|
|
|
138
166
|
z-index: 2;
|
|
139
167
|
}
|
|
140
168
|
|
|
141
|
-
|
|
169
|
+
/* Guarded with the input's hiding above: a resting ring beside a visible
|
|
170
|
+
native input would draw the indicator twice. */
|
|
171
|
+
@supports selector(:has(*)) {
|
|
172
|
+
.choice-card::before {
|
|
173
|
+
transform: none;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.choice-card:has(:checked)::before {
|
|
178
|
+
border-color: var(--intent, var(--state-selected));
|
|
179
|
+
background: var(--intent, var(--state-selected));
|
|
180
|
+
}
|
|
181
|
+
|
|
142
182
|
.choice-card:has(:checked)::after {
|
|
143
183
|
transform: scale(1);
|
|
144
184
|
}
|
|
@@ -164,11 +204,16 @@
|
|
|
164
204
|
.choice-card.chip::before {
|
|
165
205
|
--choice-chip-dot: calc(var(--control-pad-x) + var(--choice-card-check-size) / 2) 50%;
|
|
166
206
|
|
|
207
|
+
position: absolute;
|
|
167
208
|
inset: 0;
|
|
209
|
+
/* why: an absolute box honours self-alignment — the card's start would
|
|
210
|
+
shrink this layer to 0 height and the dot with it. */
|
|
211
|
+
align-self: auto;
|
|
212
|
+
margin: 0;
|
|
168
213
|
inline-size: auto;
|
|
169
214
|
block-size: auto;
|
|
215
|
+
border: 0;
|
|
170
216
|
border-radius: inherit;
|
|
171
|
-
box-shadow: none;
|
|
172
217
|
background: var(--text-subtle);
|
|
173
218
|
transform: none;
|
|
174
219
|
clip-path: circle(0.1875em at var(--choice-chip-dot));
|
|
@@ -182,10 +227,11 @@
|
|
|
182
227
|
|
|
183
228
|
/* The check keeps the card's scale-in, in flow ahead of the label. */
|
|
184
229
|
.choice-card.chip::after {
|
|
185
|
-
position: static;
|
|
186
230
|
order: -1;
|
|
187
231
|
flex: none;
|
|
188
|
-
|
|
232
|
+
align-self: center;
|
|
233
|
+
margin-block: 0;
|
|
234
|
+
margin-inline: 0 0.375em;
|
|
189
235
|
background: var(--intent, var(--state-selected));
|
|
190
236
|
-webkit-mask-size: contain;
|
|
191
237
|
mask-size: contain;
|
|
@@ -195,9 +241,11 @@
|
|
|
195
241
|
this repair, forced colors pushes disc and check to the same system color
|
|
196
242
|
and the state disappears. Highlight/Canvas/HighlightText are preserved as-is,
|
|
197
243
|
so no forced-color-adjust is needed. The pseudo is not focusable — the
|
|
198
|
-
outline
|
|
244
|
+
outline separates the disc from whatever it overlaps (the card's border,
|
|
245
|
+
for a corner-badge recipe), not a focus channel. */
|
|
199
246
|
@media (forced-colors: active) {
|
|
200
247
|
.choice-card:has(:checked)::before {
|
|
248
|
+
border-color: Highlight;
|
|
201
249
|
background: Highlight;
|
|
202
250
|
outline: 2px solid Canvas;
|
|
203
251
|
}
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
cursor: var(--control-cursor);
|
|
54
54
|
/* No outline: none here — the core :focus fallback must survive for browsers
|
|
55
55
|
without :focus-visible. Pointer focus is hidden by core's
|
|
56
|
-
:focus:not(:focus-visible); keyboard focus
|
|
57
|
-
|
|
56
|
+
:focus:not(:focus-visible); keyboard focus draws the inset outline below
|
|
57
|
+
(invalid fields take --form-invalid-border, the documented exception). */
|
|
58
58
|
transition:
|
|
59
59
|
border-color var(--duration),
|
|
60
60
|
box-shadow var(--duration),
|
package/src/css/forms/native.css
CHANGED
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
the whole control, not just the filename text. */
|
|
20
20
|
min-block-size: var(--control-size);
|
|
21
21
|
padding-block: var(--space-10);
|
|
22
|
-
|
|
22
|
+
/* Border-box height: both borders come out of the padding, as on .btn. */
|
|
23
|
+
padding-block: max(
|
|
24
|
+
var(--space-10),
|
|
25
|
+
calc((var(--control-size) - 1lh - 2 * var(--border-width)) / 2)
|
|
26
|
+
);
|
|
23
27
|
padding-inline: var(--space-20);
|
|
24
28
|
border: var(--border-width) solid var(--border);
|
|
25
29
|
border-radius: var(--radius-sm);
|
package/src/css/forms/otp.css
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
padding-block: 0;
|
|
65
65
|
padding-inline: calc((var(--otp-cell-size) - 1ch) / 2) 0;
|
|
66
66
|
border: 0;
|
|
67
|
-
outline:
|
|
67
|
+
outline: none;
|
|
68
68
|
box-shadow: none;
|
|
69
69
|
background: transparent;
|
|
70
70
|
color: var(--text);
|
|
@@ -89,6 +89,14 @@
|
|
|
89
89
|
character after it: the state rules select the cells as following siblings
|
|
90
90
|
(input:focus ~ span, input:disabled ~ span, …), so that order is part of the
|
|
91
91
|
contract. */
|
|
92
|
+
/* The documented markup puts .input on the native input, so the field ring
|
|
93
|
+
(.input:focus-visible:not(:disabled), control.css) would outrank the reset
|
|
94
|
+
above and draw that group ring; this selector must stay one step above it.
|
|
95
|
+
No transparent outline either: forced colors would paint it as a ring. */
|
|
96
|
+
.otp > input:focus-visible:not(:disabled) {
|
|
97
|
+
outline: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
.otp > input:focus ~ span {
|
|
93
101
|
--otp-cell-border: var(--focus);
|
|
94
102
|
--otp-cell-border-width: calc(var(--border-width) * 2);
|
package/src/css/forms/range.css
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* --range-track-height
|
|
14
14
|
* --range-thumb-bg
|
|
15
15
|
* --range-track-bg
|
|
16
|
+
* --range-fill-bg
|
|
17
|
+
*
|
|
18
|
+
* Runtime-written (by actual-css/js/range, never set by hand):
|
|
19
|
+
* --range-progress
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
/* Intent boundary — inherited intents do not tint this component.
|
|
@@ -22,10 +26,17 @@
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
.range {
|
|
25
|
-
--range-thumb-size: 1.
|
|
26
|
-
--range-track-height: 0.
|
|
29
|
+
--range-thumb-size: 1.125rem;
|
|
30
|
+
--range-track-height: 0.625rem;
|
|
27
31
|
--range-thumb-bg: var(--intent, var(--primary));
|
|
28
32
|
--range-track-bg: var(--border);
|
|
33
|
+
--range-fill-bg: var(--intent, var(--primary));
|
|
34
|
+
/* Fill share, written by actual-css/js/range. 0% with no runtime means a
|
|
35
|
+
plain track — the control stays fully usable without JavaScript. */
|
|
36
|
+
--range-progress: 0%;
|
|
37
|
+
/* Logical fill direction: the gradient below cannot use flow-relative
|
|
38
|
+
keywords, so RTL flips the angle through this hook instead. */
|
|
39
|
+
--range-fill-from: to right;
|
|
29
40
|
|
|
30
41
|
appearance: none;
|
|
31
42
|
-webkit-appearance: none;
|
|
@@ -35,16 +46,29 @@
|
|
|
35
46
|
cursor: pointer;
|
|
36
47
|
}
|
|
37
48
|
|
|
49
|
+
:where([dir="rtl"]) .range,
|
|
50
|
+
.range:dir(rtl) {
|
|
51
|
+
--range-fill-from: to left;
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
.range::-webkit-slider-runnable-track {
|
|
39
55
|
block-size: var(--range-track-height);
|
|
40
56
|
border-radius: var(--radius-full);
|
|
41
|
-
background:
|
|
57
|
+
background: linear-gradient(
|
|
58
|
+
var(--range-fill-from),
|
|
59
|
+
var(--range-fill-bg) var(--range-progress),
|
|
60
|
+
var(--range-track-bg) var(--range-progress)
|
|
61
|
+
);
|
|
42
62
|
}
|
|
43
63
|
|
|
44
64
|
.range::-moz-range-track {
|
|
45
65
|
block-size: var(--range-track-height);
|
|
46
66
|
border-radius: var(--radius-full);
|
|
47
|
-
background:
|
|
67
|
+
background: linear-gradient(
|
|
68
|
+
var(--range-fill-from),
|
|
69
|
+
var(--range-fill-bg) var(--range-progress),
|
|
70
|
+
var(--range-track-bg) var(--range-progress)
|
|
71
|
+
);
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
.range::-webkit-slider-thumb {
|
|
@@ -57,6 +81,10 @@
|
|
|
57
81
|
background: var(--range-thumb-bg);
|
|
58
82
|
margin-block-start: calc((var(--range-track-height) - var(--range-thumb-size)) / 2);
|
|
59
83
|
cursor: inherit;
|
|
84
|
+
/* Permanent surface separator: the fill defaults to the thumb color, so
|
|
85
|
+
without this the thumb would melt into the filled part of the track.
|
|
86
|
+
Focus-visible keeps this ring and adds the --focus ring outside it. */
|
|
87
|
+
box-shadow: 0 0 0 2px var(--surface-opaque);
|
|
60
88
|
transition: box-shadow var(--duration);
|
|
61
89
|
}
|
|
62
90
|
|
|
@@ -67,6 +95,7 @@
|
|
|
67
95
|
border: 0;
|
|
68
96
|
background: var(--range-thumb-bg);
|
|
69
97
|
cursor: inherit;
|
|
98
|
+
box-shadow: 0 0 0 2px var(--surface-opaque);
|
|
70
99
|
transition: box-shadow var(--duration);
|
|
71
100
|
}
|
|
72
101
|
|
|
@@ -77,20 +106,21 @@
|
|
|
77
106
|
/*
|
|
78
107
|
* A thumb pseudo-element takes no reliable outline, so its ring is a solid
|
|
79
108
|
* --focus shadow (the transparent outline above covers forced colors). The
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* read as an unwanted void.
|
|
109
|
+
* surface-coloured separator keeps the ring visually distinct from the
|
|
110
|
+
* coloured thumb — unlike a full-size input where the same gap would
|
|
111
|
+
* read as an unwanted void. It doubles as the resting separator between
|
|
112
|
+
* the thumb and the same-coloured fill.
|
|
83
113
|
*/
|
|
84
114
|
.range:focus-visible::-webkit-slider-thumb {
|
|
85
115
|
box-shadow:
|
|
86
|
-
0 0 0
|
|
87
|
-
0 0 0 calc(
|
|
116
|
+
0 0 0 2px var(--surface-opaque),
|
|
117
|
+
0 0 0 calc(2px + var(--focus-ring-width)) var(--focus);
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
.range:focus-visible::-moz-range-thumb {
|
|
91
121
|
box-shadow:
|
|
92
|
-
0 0 0
|
|
93
|
-
0 0 0 calc(
|
|
122
|
+
0 0 0 2px var(--surface-opaque),
|
|
123
|
+
0 0 0 calc(2px + var(--focus-ring-width)) var(--focus);
|
|
94
124
|
}
|
|
95
125
|
|
|
96
126
|
.range:disabled {
|
|
@@ -98,28 +128,41 @@
|
|
|
98
128
|
cursor: not-allowed;
|
|
99
129
|
opacity: var(--disabled-opacity);
|
|
100
130
|
}
|
|
101
|
-
|
|
102
131
|
/* Forced colors: the track must stay a visible line against the page (Canvas
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
would merge with it) and the thumb needs a real boundary to remain
|
|
133
|
+
distinguishable from the track. The fill keeps its two-tone split with
|
|
134
|
+
system colors so the value stays readable. Disabled keeps GrayText via
|
|
135
|
+
:disabled. Focus stays on the transparent outline of :focus-visible. Track
|
|
136
|
+
and thumb fills are written on the properties, not only the hooks: an
|
|
137
|
+
application hook override at the same specificity loads after this file
|
|
138
|
+
and would win the variable, leaving a Canvas-remapped fill.
|
|
139
|
+
The split lives in a background-image, which forced-color-adjust: auto
|
|
140
|
+
drops — so the tracks opt out locally. The colors stay system keywords,
|
|
141
|
+
keeping native contrast adaptation; only the host keeps adjusting. */
|
|
109
142
|
@media (forced-colors: active) {
|
|
110
143
|
.range {
|
|
111
144
|
--range-track-bg: CanvasText;
|
|
112
145
|
--range-thumb-bg: ButtonFace;
|
|
146
|
+
--range-fill-bg: Highlight;
|
|
113
147
|
}
|
|
114
148
|
|
|
115
149
|
.range::-webkit-slider-runnable-track {
|
|
116
|
-
|
|
150
|
+
forced-color-adjust: none;
|
|
151
|
+
background: linear-gradient(
|
|
152
|
+
var(--range-fill-from),
|
|
153
|
+
Highlight var(--range-progress),
|
|
154
|
+
CanvasText var(--range-progress)
|
|
155
|
+
);
|
|
117
156
|
}
|
|
118
157
|
|
|
119
158
|
.range::-moz-range-track {
|
|
120
|
-
|
|
159
|
+
forced-color-adjust: none;
|
|
160
|
+
background: linear-gradient(
|
|
161
|
+
var(--range-fill-from),
|
|
162
|
+
Highlight var(--range-progress),
|
|
163
|
+
CanvasText var(--range-progress)
|
|
164
|
+
);
|
|
121
165
|
}
|
|
122
|
-
|
|
123
166
|
.range::-webkit-slider-thumb {
|
|
124
167
|
background: ButtonFace;
|
|
125
168
|
border: var(--border-width) solid ButtonText;
|
package/src/js/command.js
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* late targets, changed commandfor values, and same-id replacements therefore
|
|
8
8
|
* work immediately without an observer, scan, refresh call, or per-element
|
|
9
9
|
* listener.
|
|
10
|
+
*
|
|
11
|
+
* A routed command cancels the click, so the browser never dispatches the
|
|
12
|
+
* native `command` event to the target, and the router does not re-dispatch
|
|
13
|
+
* one. Listen to the behavior's own events instead. Accessibility state that
|
|
14
|
+
* must exist before interaction (`aria-controls`, `aria-haspopup`,
|
|
15
|
+
* `aria-pressed`) belongs in the markup: the router has no connection step.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
/* One registry per document owns its delegated listener and command handlers.
|
|
@@ -14,18 +20,45 @@
|
|
|
14
20
|
* behavior definitions only, never DOM triggers or resolved targets. */
|
|
15
21
|
const registries = new WeakMap();
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
// The HTML `command` keywords. Any other value that does not start with "--"
|
|
24
|
+
// is the invalid state, which the browser ignores. Extend this list when HTML
|
|
25
|
+
// adds a keyword: registerCommands() throws on an unknown non-"--" name, so a
|
|
26
|
+
// stale list fails loudly instead of silently swallowing the new keyword.
|
|
27
|
+
// A future path that listens to the native `command` event instead of the
|
|
28
|
+
// click would need a per-tier matrix (native support varies) plus
|
|
29
|
+
// no-double-execution tests — the click stays the single routing event until
|
|
30
|
+
// then, which is why a routed command cancels it by design (see above).
|
|
31
|
+
const NATIVE_COMMANDS = new Set([
|
|
32
|
+
"toggle-popover",
|
|
33
|
+
"show-popover",
|
|
34
|
+
"hide-popover",
|
|
35
|
+
"close",
|
|
36
|
+
"request-close",
|
|
37
|
+
"show-modal",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function isCustom(command) {
|
|
41
|
+
return command.startsWith("--");
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
// Native command keywords are ASCII case-insensitive. Custom commands keep
|
|
26
45
|
// their exact spelling, as required by the command invoker contract.
|
|
27
46
|
function commandKey(command) {
|
|
28
|
-
return command
|
|
47
|
+
return isCustom(command) ? command : command.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function commandNames(commands, caller) {
|
|
51
|
+
const names = Array.isArray(commands) ? commands : [commands];
|
|
52
|
+
if (!names.length || names.some((name) => typeof name !== "string")) {
|
|
53
|
+
throw new TypeError(`${caller} requires one or more command names.`);
|
|
54
|
+
}
|
|
55
|
+
const invalid = names.find((name) => !isCustom(name) && !NATIVE_COMMANDS.has(commandKey(name)));
|
|
56
|
+
if (invalid !== undefined) {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
`${caller}: "${invalid}" is neither a native command nor a custom "--" command.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return [...new Set(names.map(commandKey))];
|
|
29
62
|
}
|
|
30
63
|
|
|
31
64
|
/**
|
|
@@ -46,23 +79,25 @@ export function targetFor(trigger) {
|
|
|
46
79
|
/**
|
|
47
80
|
* Build the button selector corresponding to one or more command names.
|
|
48
81
|
*
|
|
82
|
+
* Native keywords match case-insensitively, custom commands exactly — the
|
|
83
|
+
* same rule the router applies.
|
|
84
|
+
*
|
|
49
85
|
* @param {string | string[]} commands Command name or names to include.
|
|
50
86
|
* @returns {string} A selector matching command buttons with `commandfor`.
|
|
51
|
-
* @throws {TypeError} When
|
|
87
|
+
* @throws {TypeError} When a name is neither native nor a custom `--` command.
|
|
52
88
|
*/
|
|
53
89
|
export function commandSelector(commands) {
|
|
54
|
-
const attributes = commandNames(commands, "commandSelector()").map(
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
const attributes = commandNames(commands, "commandSelector()").map((name) => {
|
|
91
|
+
const value = name.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
92
|
+
return `[command="${value}"${isCustom(name) ? "" : " i"}]`;
|
|
93
|
+
});
|
|
57
94
|
return `button[commandfor]:is(${attributes.join(", ")})`;
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
function triggerFromEvent(event, doc) {
|
|
61
|
-
const ElementClass = doc.defaultView?.Element;
|
|
62
|
-
if (!ElementClass) return null;
|
|
63
|
-
|
|
64
98
|
for (const node of event.composedPath?.() ?? [event.target]) {
|
|
65
|
-
|
|
99
|
+
// nodeType rather than instanceof: nodes from another realm still count.
|
|
100
|
+
if (node.nodeType === 1 && node.matches("button[commandfor][command]")) {
|
|
66
101
|
return node;
|
|
67
102
|
}
|
|
68
103
|
if (node === doc) break;
|
|
@@ -78,7 +113,7 @@ function createRegistry(doc) {
|
|
|
78
113
|
if (event.defaultPrevented) return;
|
|
79
114
|
|
|
80
115
|
const trigger = triggerFromEvent(event, doc);
|
|
81
|
-
if (!trigger || trigger.disabled) return;
|
|
116
|
+
if (!trigger || trigger.matches(":disabled")) return;
|
|
82
117
|
|
|
83
118
|
const command = commandKey(trigger.getAttribute("command"));
|
|
84
119
|
const registration = commands.get(command);
|
|
@@ -87,7 +122,10 @@ function createRegistry(doc) {
|
|
|
87
122
|
const target = registration.resolve(trigger);
|
|
88
123
|
if (!target) return;
|
|
89
124
|
|
|
90
|
-
|
|
125
|
+
// One routed command owns the click, even when the handler throws. This
|
|
126
|
+
// is also what stops a second copy of this module, with its own registry
|
|
127
|
+
// and listener, from handling the same activation.
|
|
128
|
+
event.preventDefault();
|
|
91
129
|
registration.handle(event, trigger, target, command);
|
|
92
130
|
}
|
|
93
131
|
|
|
@@ -98,31 +136,22 @@ function createRegistry(doc) {
|
|
|
98
136
|
/**
|
|
99
137
|
* Register one behavior for one or more command names.
|
|
100
138
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* resolved target, without introducing a connection lifecycle.
|
|
139
|
+
* The router cancels the click once `resolve` returns a target, so `handle`
|
|
140
|
+
* runs with `event.defaultPrevented` already true.
|
|
104
141
|
*
|
|
105
142
|
* @param {string | string[]} commands Command name or names owned by this behavior.
|
|
106
143
|
* @param {object} options Behavior callbacks.
|
|
107
144
|
* @param {(trigger: HTMLButtonElement) => Element | null} [options.resolve=targetFor]
|
|
108
145
|
* Resolver evaluated for every action.
|
|
109
|
-
* @param {(trigger: HTMLButtonElement, target: Element, command: string) => void} [options.prepare]
|
|
110
|
-
* Optional idempotent callback run immediately before the handler.
|
|
111
146
|
* @param {(event: MouseEvent, trigger: HTMLButtonElement, target: Element, command: string) => void} options.handle
|
|
112
147
|
* Command handler.
|
|
113
148
|
* @returns {{ disconnect: () => void }} An idempotent registration teardown handle.
|
|
114
149
|
* @throws {TypeError} When command names or callbacks are invalid.
|
|
115
150
|
* @throws {Error} When a command already has an owner in the current document.
|
|
116
151
|
*/
|
|
117
|
-
export function registerCommands(commands, { resolve = targetFor,
|
|
118
|
-
const names =
|
|
119
|
-
|
|
120
|
-
];
|
|
121
|
-
if (
|
|
122
|
-
typeof resolve !== "function" ||
|
|
123
|
-
(prepare !== undefined && typeof prepare !== "function") ||
|
|
124
|
-
typeof handle !== "function"
|
|
125
|
-
) {
|
|
152
|
+
export function registerCommands(commands, { resolve = targetFor, handle } = {}) {
|
|
153
|
+
const names = commandNames(commands, "registerCommands()");
|
|
154
|
+
if (typeof resolve !== "function" || typeof handle !== "function") {
|
|
126
155
|
throw new TypeError("registerCommands() requires function callbacks.");
|
|
127
156
|
}
|
|
128
157
|
if (typeof document === "undefined") return { disconnect() {} };
|
|
@@ -139,7 +168,7 @@ export function registerCommands(commands, { resolve = targetFor, prepare, handl
|
|
|
139
168
|
throw new Error(`Command "${conflict}" is already registered in this document.`);
|
|
140
169
|
}
|
|
141
170
|
|
|
142
|
-
const registration = { resolve,
|
|
171
|
+
const registration = { resolve, handle };
|
|
143
172
|
for (const name of names) registry.commands.set(name, registration);
|
|
144
173
|
|
|
145
174
|
let connected = true;
|
package/src/js/dialog.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Close button: <button commandfor="dialog-id" command="request-close">
|
|
6
6
|
* Dialog: <dialog id="dialog-id" data-dialog-dismissible>
|
|
7
7
|
*
|
|
8
|
-
* Command semantics are explicit: show-modal calls showModal(), show calls
|
|
8
|
+
* Command semantics are explicit: show-modal calls showModal(), --show calls
|
|
9
9
|
* show(). data-dialog-modal still defines the default for direct openDialog()
|
|
10
10
|
* calls when no command mode is forced.
|
|
11
11
|
* data-dialog-dismissible gates backdrop click only: with it, the runtime
|
|
@@ -34,7 +34,9 @@ import { CLASSES } from "./selectors.js";
|
|
|
34
34
|
|
|
35
35
|
const dialogMap = new WeakMap();
|
|
36
36
|
const wiredDialogs = new Set();
|
|
37
|
-
|
|
37
|
+
// HTML has no non-modal dialog keyword, so non-modal opening is the custom
|
|
38
|
+
// `--show` command.
|
|
39
|
+
const DIALOG_COMMANDS = ["show-modal", "--show", "request-close", "close"];
|
|
38
40
|
const DIALOG_SELECTOR = "dialog";
|
|
39
41
|
const DIALOG_TITLE_SELECTOR = "[data-title], h1, h2, h3, h4, h5, h6";
|
|
40
42
|
const STATIC_SHAKE_MS = 250;
|
|
@@ -519,28 +521,11 @@ registerCommands(DIALOG_COMMANDS, {
|
|
|
519
521
|
const dialog = targetFor(trigger);
|
|
520
522
|
return isDialogElement(dialog) ? dialog : null;
|
|
521
523
|
},
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
trigger.setAttribute("aria-controls", dialog.id);
|
|
528
|
-
|
|
529
|
-
if (
|
|
530
|
-
!trigger.hasAttribute("aria-haspopup") &&
|
|
531
|
-
(command === "show-modal" || command === "show")
|
|
532
|
-
) {
|
|
533
|
-
trigger.setAttribute("aria-haspopup", "dialog");
|
|
534
|
-
}
|
|
535
|
-
},
|
|
536
|
-
handle: (event, trigger, dialog, command) => {
|
|
537
|
-
event.preventDefault();
|
|
538
|
-
|
|
539
|
-
if (!isDialogElement(dialog)) {
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
524
|
+
handle: (_event, trigger, dialog, command) => {
|
|
525
|
+
// A dialog inserted since the last enhance() pass is wired on first use.
|
|
526
|
+
ensureDialogWired(dialog);
|
|
542
527
|
|
|
543
|
-
if (command === "show-modal" || command === "show") {
|
|
528
|
+
if (command === "show-modal" || command === "--show") {
|
|
544
529
|
openDialogWithViewTransition(dialog, trigger, command === "show-modal");
|
|
545
530
|
return;
|
|
546
531
|
}
|
package/src/js/dismiss.js
CHANGED
|
@@ -7,8 +7,7 @@ import { registerCommands } from "./command.js";
|
|
|
7
7
|
import { EVENTS } from "./events.js";
|
|
8
8
|
|
|
9
9
|
registerCommands("--dismiss", {
|
|
10
|
-
handle(
|
|
11
|
-
event.preventDefault();
|
|
10
|
+
handle(_event, trigger, target) {
|
|
12
11
|
target.hidden = true;
|
|
13
12
|
target.dispatchEvent(
|
|
14
13
|
new CustomEvent(EVENTS.dismiss, {
|