actual-css 0.11.1 → 0.13.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/CHANGELOG.md +96 -0
- package/README.md +25 -6
- package/components.json +12 -18
- package/dist/actual.full.css +780 -479
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/brand/favicon.svg +5 -0
- package/docs/brand/lockup.svg +12 -0
- package/docs/brand/logo-dark.svg +9 -0
- package/docs/brand/logo-light.svg +9 -0
- package/docs/brand/logo.svg +11 -0
- package/docs/brand/mark.svg +11 -0
- package/docs/design-notes/architecture.md +67 -0
- package/docs/design-notes/steps.md +45 -28
- package/docs/navigation.json +3 -2
- package/docs/pages/components/alert.md +2 -10
- package/docs/pages/components/avatar.md +17 -11
- package/docs/pages/components/badge.md +3 -9
- package/docs/pages/components/breadcrumb.md +16 -2
- package/docs/pages/components/card.md +31 -2
- package/docs/pages/components/dialog.md +121 -33
- package/docs/pages/components/drawer.md +2 -2
- package/docs/pages/components/list.md +9 -13
- package/docs/pages/components/navbar.md +49 -30
- package/docs/pages/components/pagination.md +19 -0
- package/docs/pages/components/steps.md +24 -20
- package/docs/pages/components/table.md +3 -3
- package/docs/pages/enhancements/flyout.md +2 -2
- package/docs/pages/enhancements/scrollspy.md +7 -7
- package/docs/pages/enhancements/status-bar.md +2 -2
- package/docs/pages/enhancements/tabs.md +74 -3
- package/docs/pages/examples/overview.md +2 -1
- package/docs/pages/forms/date-picker.md +90 -0
- package/docs/pages/forms/detached-actions.md +37 -30
- package/docs/pages/forms/floating-field.md +21 -7
- package/docs/pages/forms/form-actions.md +58 -28
- package/docs/pages/forms/input-policy.md +1 -1
- package/docs/pages/forms/overview.md +6 -5
- package/docs/pages/forms/states-and-sizes.md +15 -15
- package/docs/pages/forms/sticky-actions.md +85 -83
- package/docs/pages/forms/validation.md +5 -5
- package/docs/pages/foundations/philosophy.md +102 -4
- package/docs/pages/foundations/theming.md +15 -4
- package/docs/pages/foundations/tokens.md +89 -36
- package/docs/pages/foundations/typography.md +4 -7
- package/docs/pages/guides/bootstrap.md +2 -2
- package/docs/pages/guides/design-tools.md +2 -2
- package/docs/pages/guides/integrating-actual-css.md +79 -9
- package/docs/pages/guides/introduction.md +5 -5
- package/docs/pages/guides/modular-import.md +3 -0
- package/docs/pages/guides/progressive-enhancement.md +27 -9
- package/docs/pages/guides/tailwind.md +70 -0
- package/docs/pages/guides/upgrading.md +151 -0
- package/docs/pages/layout/center.md +2 -1
- package/docs/pages/layout/cluster.md +37 -4
- package/docs/pages/layout/grid.md +2 -0
- package/docs/pages/layout/overview.md +1 -1
- package/docs/pages/layout/sidebar-layout.md +3 -0
- package/docs/pages/layout/stack.md +21 -16
- package/docs/pages/layout/switcher.md +2 -0
- package/docs/pages/layout/topbar.md +7 -5
- package/docs/pages/patterns/icon-slot.md +17 -1
- package/docs/pages/patterns/navigation-list.md +16 -46
- package/docs/pages/patterns/overview.md +3 -4
- package/docs/pages/utilities/flex.md +30 -20
- package/docs/pages/utilities/sizing-wrapping.md +52 -7
- package/docs/pages/utilities/spacing.md +4 -3
- package/package.json +6 -6
- package/reserved-classes.json +3 -3
- package/skills/actual-css/references/enhancements.md +10 -7
- package/src/cli/actual-css.js +39 -13
- package/src/css/components/alert.css +34 -34
- package/src/css/components/avatar.css +8 -17
- package/src/css/components/badge.css +1 -6
- package/src/css/components/breadcrumb.css +74 -8
- package/src/css/components/button.css +3 -9
- package/src/css/components/card.css +18 -10
- package/src/css/components/chat.css +4 -5
- package/src/css/components/drawer.css +42 -45
- package/src/css/components/flyout.css +3 -1
- package/src/css/components/index.css +0 -1
- package/src/css/components/join.css +2 -2
- package/src/css/components/list.css +10 -10
- package/src/css/components/modal.css +113 -134
- package/src/css/components/navbar.css +22 -31
- package/src/css/components/pagination.css +5 -0
- package/src/css/components/scrollspy.css +1 -1
- package/src/css/components/steps.css +66 -12
- package/src/css/components/tab.css +134 -4
- package/src/css/core/base.css +1 -1
- package/src/css/core/focus.css +8 -6
- package/src/css/core/reset.css +2 -1
- package/src/css/core/theme.css +10 -1
- package/src/css/core/tokens.css +32 -12
- package/src/css/core/variants.css +41 -14
- package/src/css/forms/choice-card.css +1 -1
- package/src/css/forms/choice.css +1 -1
- package/src/css/forms/control.css +1 -1
- package/src/css/forms/floating-field.css +20 -18
- package/src/css/forms/form-actions.css +14 -12
- package/src/css/forms/native.css +2 -2
- package/src/css/forms/otp.css +4 -5
- package/src/css/forms/switch.css +1 -1
- package/src/css/forms/validation.css +23 -30
- package/src/css/layout/center.css +2 -0
- package/src/css/layout/cluster.css +25 -0
- package/src/css/layout/grid.css +7 -0
- package/src/css/layout/scroller.css +19 -3
- package/src/css/layout/sidebar-layout.css +3 -0
- package/src/css/layout/switcher.css +3 -0
- package/src/css/layout/topbar.css +12 -6
- package/src/css/utilities/base.css +5 -0
- package/src/css/utilities/extra.css +3 -7
- package/src/js/full.js +4 -0
- package/src/js/inline-visible.js +54 -0
- package/src/js/parsed.js +21 -0
- package/src/js/tab.js +33 -7
- package/src/tooling/color.js +65 -0
- package/src/tooling/design-export.js +1 -1
- package/src/tooling/design-tokens.js +53 -34
- package/src/tooling/theme-contrast.js +217 -0
- package/docs/pages/patterns/actions.md +0 -52
- package/src/css/components/actions.css +0 -27
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* messages stay in the surrounding .field, not inside the cell, so the
|
|
10
10
|
* resting label can center on the control alone.
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The cell reads its control's state through :has(), so the label may come
|
|
13
|
+
* before or after the control (a form theme rendering label then widget needs
|
|
14
|
+
* no reordering). Injected sibling nodes are tolerated (password managers,
|
|
15
|
+
* extension wrappers) as long as the control stays a direct child; the cell
|
|
16
|
+
* contract stays a single control plus one label, since a second control
|
|
17
|
+
* would float the label on either one's state.
|
|
18
|
+
* Every .input/.textarea needs
|
|
16
19
|
* placeholder=" " to expose a reliable empty state — without it the label
|
|
17
20
|
* only reacts to focus.
|
|
18
21
|
*
|
|
@@ -62,23 +65,22 @@
|
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/* Single-line controls rest in the middle until they receive a value. */
|
|
65
|
-
.floating-field
|
|
68
|
+
.floating-field:has(> :is(.input, .select)) > .field-label {
|
|
66
69
|
inset-block-start: 50%;
|
|
67
70
|
transform: translateY(-50%);
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
/* An empty textarea label follows the control's normal first-line position;
|
|
71
74
|
the control itself keeps the extra headroom reserved for its floated state. */
|
|
72
|
-
.floating-field
|
|
75
|
+
.floating-field:has(> .textarea) > .field-label {
|
|
73
76
|
inset-block-start: calc(1em + var(--border-width));
|
|
74
77
|
transform: none;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
/* :autofill covers prefilled values before interaction, when the browser may
|
|
78
81
|
not have propagated the value to the placeholder state yet. */
|
|
79
|
-
.floating-field
|
|
80
|
-
|
|
81
|
-
~ .field-label {
|
|
82
|
+
.floating-field:has(> :is(.input, .textarea):is(:focus, :autofill, :not(:placeholder-shown)))
|
|
83
|
+
> .field-label {
|
|
82
84
|
inset-block-start: var(--floating-label-top);
|
|
83
85
|
transform: none;
|
|
84
86
|
font-size: var(--font-size-xs);
|
|
@@ -88,16 +90,16 @@
|
|
|
88
90
|
/* Controls without a dependable :placeholder-shown empty state stay floated:
|
|
89
91
|
select always, and date/time inputs whose placeholder rendering is
|
|
90
92
|
inconsistent across engines (Firefox shows none). */
|
|
91
|
-
.floating-field
|
|
93
|
+
.floating-field:has(
|
|
92
94
|
> :is(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
.select,
|
|
96
|
+
.input[type="date"],
|
|
97
|
+
.input[type="time"],
|
|
98
|
+
.input[type="datetime-local"],
|
|
99
|
+
.input[type="month"],
|
|
100
|
+
.input[type="week"]
|
|
101
|
+
)
|
|
102
|
+
) > .field-label {
|
|
101
103
|
inset-block-start: var(--floating-label-top);
|
|
102
104
|
transform: none;
|
|
103
105
|
font-size: var(--font-size-xs);
|
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Form actions —
|
|
2
|
+
* Form actions — the role of a .cluster action row that closes a form.
|
|
3
3
|
*
|
|
4
4
|
* Docs: docs/pages/forms/form-actions.md
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Markup: `<div class="cluster form-actions">`. The cluster owns the row
|
|
7
|
+
* (flex, wrap, gap); this role owns the form-specific alignment policy and the
|
|
8
|
+
* opt-in sticky variant. The distance to the fields above belongs to the
|
|
9
|
+
* parent layout (.stack on the form), never to a margin here: a margin would
|
|
10
|
+
* add to a stack's gap. The sticky variant docks to the viewport bottom with a
|
|
11
|
+
* translucent blur where supported; at rest it stays visually neutral so it
|
|
12
|
+
* does not double as a separator.
|
|
9
13
|
*
|
|
10
14
|
* Public hooks:
|
|
11
15
|
* --form-actions-align
|
|
12
16
|
* --form-actions-justify
|
|
13
|
-
* --form-actions-margin-block-start
|
|
14
17
|
*
|
|
15
18
|
* Internal:
|
|
16
19
|
* --form-actions-sticky-block-offset (owned by .form-actions.sticky)
|
|
17
20
|
* --form-actions-sticky-inline-offset (owned by .form-actions.sticky)
|
|
18
21
|
* --form-actions-sticky-padding (owned by .form-actions.sticky)
|
|
19
22
|
*/
|
|
23
|
+
|
|
24
|
+
/* The role relays its policy into the row's cluster hooks. Cluster hooks stop
|
|
25
|
+
at every nested .cluster, so the relay cannot leak, and this class beats the
|
|
26
|
+
cluster's zero-specificity reset in any import order. */
|
|
20
27
|
.form-actions {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
flex-wrap: wrap;
|
|
24
|
-
gap: var(--gap);
|
|
25
|
-
align-items: var(--form-actions-align, center);
|
|
26
|
-
justify-content: var(--form-actions-justify, flex-start);
|
|
27
|
-
margin-block-start: var(--form-actions-margin-block-start, var(--space-50));
|
|
28
|
+
--cluster-align: var(--form-actions-align, center);
|
|
29
|
+
--cluster-justify: var(--form-actions-justify, flex-start);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/* Opt-in sticky variant. When the bar is not stuck (at rest) the translucent
|
package/src/css/forms/native.css
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
calc((var(--control-size) - 1lh - 2 * var(--border-width)) / 2)
|
|
26
26
|
);
|
|
27
27
|
padding-inline: var(--space-20);
|
|
28
|
-
border: var(--border-width) solid var(--border);
|
|
28
|
+
border: var(--border-width) solid var(--border-control);
|
|
29
29
|
border-radius: var(--radius-sm);
|
|
30
30
|
background: var(--surface-subtle);
|
|
31
31
|
color: var(--text);
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
inline-size: var(--control-size);
|
|
60
60
|
block-size: var(--control-size);
|
|
61
61
|
padding: var(--border-width);
|
|
62
|
-
border: var(--border-width) solid var(--border);
|
|
62
|
+
border: var(--border-width) solid var(--border-control);
|
|
63
63
|
border-radius: var(--radius);
|
|
64
64
|
background: var(--surface);
|
|
65
65
|
cursor: pointer;
|
package/src/css/forms/otp.css
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
--otp-cell-size: var(--control-size);
|
|
22
22
|
--otp-gap: var(--space-10);
|
|
23
23
|
--otp-font-size: 1.5em;
|
|
24
|
-
--otp-cell-border: var(--border);
|
|
24
|
+
--otp-cell-border: var(--border-control);
|
|
25
25
|
--otp-cell-bg: var(--surface);
|
|
26
26
|
--otp-caret-space: calc((var(--otp-cell-size) - 1ch) / 2 + var(--otp-gap) + 1px);
|
|
27
27
|
|
|
@@ -107,10 +107,9 @@
|
|
|
107
107
|
--otp-cell-border: var(--danger);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
110
|
+
/* biome-ignore lint/style/noDescendingSpecificity: all invalid routes write the same border, so their order cannot change the state. Keep :user-invalid separate so unsupported engines retain the other routes. */
|
|
111
|
+
.needs-validation .otp > input:user-invalid ~ span {
|
|
112
|
+
--otp-cell-border: var(--danger);
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
.otp > input:disabled {
|
package/src/css/forms/switch.css
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
.switch {
|
|
31
31
|
--switch-bg: var(--border);
|
|
32
|
-
--switch-border: var(--border);
|
|
32
|
+
--switch-border: var(--border-control);
|
|
33
33
|
--switch-knob: var(--surface-raised);
|
|
34
34
|
--switch-block-size: calc(var(--choice-control-size) + 0.125em);
|
|
35
35
|
--switch-width: calc(var(--switch-block-size) * 1.8);
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Imported by default through forms/index.css. Custom builds may omit this
|
|
7
7
|
* file when an app wants to own validation styling completely.
|
|
8
|
+
* Without :has(), automatic error display and choice-card borders drop;
|
|
9
|
+
* native control states and the explicit .field.danger route stay available.
|
|
8
10
|
*
|
|
9
11
|
* State coverage (which states each component ships) is audited in
|
|
10
12
|
* tests/validation.test.js and summarized in docs/pages/forms/validation.md —
|
|
@@ -26,11 +28,14 @@
|
|
|
26
28
|
display: block;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
.field:has([aria-invalid="true"]) > .field-error,
|
|
32
|
+
.needs-validation.was-validated .field:has(:invalid) > .field-error {
|
|
33
|
+
display: block;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* biome-ignore lint/style/noDescendingSpecificity: every invalid route reveals the error; keep :user-invalid separate so unsupported engines retain the other routes. */
|
|
37
|
+
.needs-validation .field:has(:user-invalid) > .field-error {
|
|
38
|
+
display: block;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
:where(.input, .textarea, .select, .check, .radio, .switch)[aria-invalid="true"],
|
|
@@ -54,39 +59,27 @@
|
|
|
54
59
|
--switch-bg: var(--danger);
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
+
.needs-validation .check:is(:checked, :indeterminate):user-invalid,
|
|
63
|
+
.needs-validation .radio:checked:user-invalid {
|
|
64
|
+
--choice-bg: var(--danger);
|
|
65
|
+
}
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
67
|
+
.needs-validation .switch:checked:user-invalid {
|
|
68
|
+
--switch-bg: var(--danger);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
/* Choice cards hide their native input, so invalid state must surface on the
|
|
69
72
|
card itself. Sets the same --form-invalid-border hook the card's border
|
|
70
73
|
consumes. */
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
--form-invalid-border: var(--danger);
|
|
75
|
-
}
|
|
74
|
+
.choice-card:has([aria-invalid="true"]),
|
|
75
|
+
.needs-validation.was-validated .choice-card:has(:invalid) {
|
|
76
|
+
--form-invalid-border: var(--danger);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
--form-invalid-border: var(--danger);
|
|
81
|
-
}
|
|
79
|
+
.needs-validation :is(.input, .textarea, .select, .check, .radio, .switch):user-invalid {
|
|
80
|
+
--form-invalid-border: var(--danger);
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
display: block;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.needs-validation .choice-card:has(:user-invalid) {
|
|
90
|
-
--form-invalid-border: var(--danger);
|
|
91
|
-
}
|
|
83
|
+
.needs-validation .choice-card:has(:user-invalid) {
|
|
84
|
+
--form-invalid-border: var(--danger);
|
|
92
85
|
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* --center-size
|
|
8
8
|
* --center-pad
|
|
9
9
|
*/
|
|
10
|
+
/* The hooks inherit on purpose: a site's content width is page policy, so a
|
|
11
|
+
theme sets them once on :root for every .center. */
|
|
10
12
|
.center {
|
|
11
13
|
box-sizing: content-box;
|
|
12
14
|
inline-size: calc(100% - var(--center-pad, 1rem) * 2);
|
|
@@ -9,7 +9,22 @@
|
|
|
9
9
|
* --cluster-wrap
|
|
10
10
|
*
|
|
11
11
|
* Child contract: direct-child margins are reset, spacing comes from --gap.
|
|
12
|
+
* A ul/ol/menu keeps its semantics but loses list chrome (see below).
|
|
13
|
+
*
|
|
14
|
+
* The hooks describe this row's own geometry, so they stop at each cluster:
|
|
15
|
+
* set them on the cluster itself, or target nested rows with a selector. The
|
|
16
|
+
* gap token is context rhythm and keeps inheriting.
|
|
12
17
|
*/
|
|
18
|
+
|
|
19
|
+
/* Instance boundary: an ancestor's value never reaches a nested cluster.
|
|
20
|
+
Zero specificity, so any class or inline style on the cluster wins without
|
|
21
|
+
depending on import order; `initial` makes the var() fallbacks apply. */
|
|
22
|
+
:where(.cluster) {
|
|
23
|
+
--cluster-align: initial;
|
|
24
|
+
--cluster-justify: initial;
|
|
25
|
+
--cluster-wrap: initial;
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
.cluster {
|
|
14
29
|
display: flex;
|
|
15
30
|
flex-direction: row;
|
|
@@ -22,3 +37,13 @@
|
|
|
22
37
|
.cluster > * {
|
|
23
38
|
margin: 0;
|
|
24
39
|
}
|
|
40
|
+
|
|
41
|
+
/* Horizontal, wrapping layout is incompatible with list markers and native
|
|
42
|
+
indentation: a ul/ol/menu applied with .cluster stays a list in the DOM, but
|
|
43
|
+
its visual chrome would be accidental. .stack intentionally leaves this
|
|
44
|
+
alone — a vertical list can keep its numbers. */
|
|
45
|
+
:where(ul, ol, menu).cluster {
|
|
46
|
+
margin: 0;
|
|
47
|
+
padding: 0;
|
|
48
|
+
list-style: none;
|
|
49
|
+
}
|
package/src/css/layout/grid.css
CHANGED
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
* the thresholds were arrived at: docs/design-notes/grid.md.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
/* Instance boundary: a nested grid never takes its parent's track template.
|
|
26
|
+
Zero specificity, so a class or inline style on the grid itself wins. */
|
|
27
|
+
:where(.grid) {
|
|
28
|
+
--grid-columns: initial;
|
|
29
|
+
--grid-min: initial;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
/* --grid-columns replaces the intrinsic auto-fit template: once set, the grid
|
|
26
33
|
no longer collapses on its own and the author owns narrow-container
|
|
27
34
|
collapse. */
|
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
* draw their native scrollbar — the intended fallback, not a broken state.
|
|
14
14
|
*
|
|
15
15
|
* Styles the scrollbar only; it does not create the overflow. Pair it with
|
|
16
|
-
* .overflow-auto or a component that already scrolls
|
|
16
|
+
* .overflow-auto or a component that already scrolls (horizontal steps, a
|
|
17
|
+
* table wrap; tabs hide their bar and fade their own edges). No edge fade
|
|
18
|
+
* here: a mask would dictate the strip's scroll-padding and focus geometry for
|
|
19
|
+
* a decorative cue, and an item clipped at the edge already says the strip
|
|
20
|
+
* continues.
|
|
21
|
+
*
|
|
22
|
+
* Not on touch: Chromium trades the overlay scrollbar of a touch screen,
|
|
23
|
+
* hidden at rest, for a classic always-visible one as soon as scrollbar-color
|
|
24
|
+
* or scrollbar-width is set. On a phone that painted a permanent bar under
|
|
25
|
+
* every strip; the native overlay is the better cue there.
|
|
17
26
|
*
|
|
18
27
|
* Public hooks:
|
|
19
28
|
* --scroller-track
|
|
@@ -22,9 +31,16 @@
|
|
|
22
31
|
.scroller {
|
|
23
32
|
--scroller-track: transparent;
|
|
24
33
|
--scroller-thumb: var(--border);
|
|
34
|
+
}
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
/* Any primary pointer but touch: a keyboard-only or pointerless device
|
|
37
|
+
(pointer: none) draws classic scrollbars too. `all and` keeps the negation
|
|
38
|
+
valid in Media Queries 3 parsers. */
|
|
39
|
+
@media not all and (pointer: coarse) {
|
|
40
|
+
.scroller {
|
|
41
|
+
scrollbar-color: var(--scroller-thumb) var(--scroller-track);
|
|
42
|
+
scrollbar-width: thin;
|
|
43
|
+
}
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
/* Opt-in gutter: reserves space so the layout does not shift when
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* visual, reading and keyboard order all agree — the role rules swap so the
|
|
18
18
|
* aside still takes its preferred width and the main still reclaims the row.
|
|
19
19
|
*/
|
|
20
|
+
/* No instance reset, unlike .cluster and .grid: the regions read the hooks,
|
|
21
|
+
and a layout nested as a region takes its own size from its parent's
|
|
22
|
+
values, so the hooks must reach it. */
|
|
20
23
|
.sidebar-layout {
|
|
21
24
|
display: flex;
|
|
22
25
|
flex-direction: row;
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Child contract: every direct child is a peer; the flex-basis applies to all.
|
|
14
14
|
*/
|
|
15
|
+
/* No instance reset, unlike .cluster and .grid: the children read the hook,
|
|
16
|
+
and a switcher nested as a direct child takes its own flex-basis from its
|
|
17
|
+
parent's value, so the hook must reach it. */
|
|
15
18
|
.switcher {
|
|
16
19
|
display: flex;
|
|
17
20
|
flex-direction: row;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Sticky, frosted top bar shell for app/dashboard shells (persistent
|
|
3
3
|
* sidebar + topbar layouts). Owns positioning, stacking, and the frosted
|
|
4
|
-
* background
|
|
5
|
-
*
|
|
4
|
+
* background; it also lays its children out as a default row that a layout
|
|
5
|
+
* primitive on the same element (such as .cluster) replaces. Row content
|
|
6
|
+
* (search, breadcrumb, actions) stays local to the page.
|
|
6
7
|
*
|
|
7
8
|
* Docs: docs/pages/layout/topbar.md
|
|
8
9
|
*
|
|
@@ -16,16 +17,21 @@
|
|
|
16
17
|
inset-block-start: 0;
|
|
17
18
|
z-index: var(--z-sticky);
|
|
18
19
|
isolation: isolate;
|
|
19
|
-
display: flex;
|
|
20
|
-
flex-direction: row;
|
|
21
|
-
align-items: center;
|
|
22
|
-
gap: var(--gap);
|
|
23
20
|
padding-block: var(--space-30);
|
|
24
21
|
padding-inline: var(--space-40);
|
|
25
22
|
border-block-end: var(--border-width) solid var(--border);
|
|
26
23
|
background: var(--surface);
|
|
27
24
|
}
|
|
28
25
|
|
|
26
|
+
/* The row is a default at zero specificity: .topbar and the primitives share
|
|
27
|
+
layout/, so at class weight their file order alone would pick the winner. */
|
|
28
|
+
:where(.topbar) {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: row;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: var(--gap);
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
/* Both conditions are required: the translucent background contains var(),
|
|
30
36
|
so a browser with backdrop-filter but not color-mix (e.g. Safari < 16.2)
|
|
31
37
|
would accept the declaration at parse time, fail it at computed-value
|
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
* - utilities consume the current context instead of defining their own -sm/-lg scale.
|
|
11
11
|
* - optional utilities extend core utilities, they do not re-express them.
|
|
12
12
|
* - follow verbose .{property}-{value} naming.
|
|
13
|
-
*
|
|
14
|
-
* Internal:
|
|
15
|
-
* --cluster-justify (local relay consumed by .cluster)
|
|
16
13
|
*/
|
|
17
14
|
|
|
18
15
|
/* Display */
|
|
@@ -87,23 +84,22 @@
|
|
|
87
84
|
flex-wrap: nowrap;
|
|
88
85
|
}
|
|
89
86
|
|
|
87
|
+
/* The property is written directly, so the utility works on any flex row, not
|
|
88
|
+
only a .cluster. Utilities load last, so the direct declaration beats
|
|
89
|
+
.cluster at equal specificity. */
|
|
90
90
|
.justify-content-start {
|
|
91
|
-
--cluster-justify: flex-start;
|
|
92
91
|
justify-content: flex-start;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
.justify-content-center {
|
|
96
|
-
--cluster-justify: center;
|
|
97
95
|
justify-content: center;
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
.justify-content-end {
|
|
101
|
-
--cluster-justify: flex-end;
|
|
102
99
|
justify-content: flex-end;
|
|
103
100
|
}
|
|
104
101
|
|
|
105
102
|
.justify-content-space-between {
|
|
106
|
-
--cluster-justify: space-between;
|
|
107
103
|
justify-content: space-between;
|
|
108
104
|
}
|
|
109
105
|
|
package/src/js/full.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Each module self-registers via observer: importing this bundle enables
|
|
5
5
|
* common behaviors. No DOMContentLoaded ceremony, no init calls.
|
|
6
6
|
* Injected content wires automatically; removal cleans up automatically.
|
|
7
|
+
* The entry also exposes the public enhancement API from the same module
|
|
8
|
+
* graph, so adopters extend the runtime they already run.
|
|
7
9
|
*
|
|
8
10
|
* A plain `<script src="actual.full.js"></script>` (no bundler, no import) still
|
|
9
11
|
* gets the full runtime, including declarative triggers like status's
|
|
@@ -25,3 +27,5 @@ import "./range.js";
|
|
|
25
27
|
import "./validation.js";
|
|
26
28
|
import "./status.js";
|
|
27
29
|
import "./index.js";
|
|
30
|
+
|
|
31
|
+
export { applyEnhancement, default as enhance, registerEnhancement } from "./enhance.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Brings a target into view inside one horizontal scrollport — internal, not
|
|
3
|
+
* a package export. Tabs call it at connect, so a selected tab rendered
|
|
4
|
+
* off-screen starts visible, and on every selection.
|
|
5
|
+
*
|
|
6
|
+
* Contract:
|
|
7
|
+
* - Scrolls `container` only. scrollIntoView() would also scroll ancestors and
|
|
8
|
+
* the page, so a strip below the fold would yank the reader down to it.
|
|
9
|
+
* - Horizontal, nearest only: a target already visible stays put; one that
|
|
10
|
+
* overflows a single edge moves the minimum to show that edge; one wider
|
|
11
|
+
* than the visible area that covers it on both sides stays put too.
|
|
12
|
+
* - The visible area is the scrollport minus its computed scroll-padding, the
|
|
13
|
+
* native vocabulary also honoured by focus scrolling: a component that
|
|
14
|
+
* keeps an edge clear (a strip's focus line) declares it there, not here.
|
|
15
|
+
* - The delta comes from physical rects and goes through scrollBy(), so RTL
|
|
16
|
+
* needs no scrollLeft sign convention.
|
|
17
|
+
* - Rects are in transformed pixels; clientWidth, scroll-padding and scrollBy()
|
|
18
|
+
* are in layout pixels. The strip's own scale converts between them, so a
|
|
19
|
+
* strip under transform: scale() reveals the same item. Rotation is out of
|
|
20
|
+
* scope.
|
|
21
|
+
* - Inline axis only: the block scroll position is left as it is. Callers are
|
|
22
|
+
* one-row strips whose block axis does not scroll.
|
|
23
|
+
* - Requires a laid-out, visible container and target. A strip hidden at
|
|
24
|
+
* connect (inside a closed dialog or drawer) or a target without a box (a
|
|
25
|
+
* current link inside a closed flyout) gets a no-op, not a deferred retry.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Sub-pixel layout must not read as overflow and nudge an aligned strip.
|
|
29
|
+
const TOLERANCE = 1;
|
|
30
|
+
|
|
31
|
+
// scroll-padding computes to `auto`, a length, or a percentage of the
|
|
32
|
+
// scrollport; auto adds no inset. A calc() mixing the two reads as none.
|
|
33
|
+
const inset = (value, scrollport) =>
|
|
34
|
+
value.endsWith("%")
|
|
35
|
+
? (Number.parseFloat(value) * scrollport) / 100
|
|
36
|
+
: Number.parseFloat(value) || 0;
|
|
37
|
+
|
|
38
|
+
export function ensureInlineVisible(container, target) {
|
|
39
|
+
const width = container?.clientWidth;
|
|
40
|
+
if (!width || !target?.getClientRects().length) return;
|
|
41
|
+
const style = getComputedStyle(container);
|
|
42
|
+
const box = container.getBoundingClientRect();
|
|
43
|
+
const scale = box.width / container.offsetWidth || 1;
|
|
44
|
+
const start = box.left + (container.clientLeft + inset(style.scrollPaddingLeft, width)) * scale;
|
|
45
|
+
const end =
|
|
46
|
+
box.left + (container.clientLeft + width - inset(style.scrollPaddingRight, width)) * scale;
|
|
47
|
+
const rect = target.getBoundingClientRect();
|
|
48
|
+
const before = rect.left < start - TOLERANCE;
|
|
49
|
+
const after = rect.right > end + TOLERANCE;
|
|
50
|
+
if (before === after) return;
|
|
51
|
+
const delta = before ? rect.left - start : rect.right - end;
|
|
52
|
+
// A placement, not a transition: it must not animate under scroll-behavior: smooth.
|
|
53
|
+
container.scrollBy({ left: delta / scale, behavior: "instant" });
|
|
54
|
+
}
|
package/src/js/parsed.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Runs a callback once the document has finished parsing — internal, not a
|
|
3
|
+
* package export.
|
|
4
|
+
*
|
|
5
|
+
* The runtime connects an element as soon as the parser inserts it. With a
|
|
6
|
+
* classic script in <head>, a <script> inside the element hands control back
|
|
7
|
+
* mid-parse, so an enhancer that reads its children at connect can find them
|
|
8
|
+
* missing. Deferring that one read to DOMContentLoaded needs no observer; after
|
|
9
|
+
* parsing the callback runs at once. Returns a cancel for the enhancer's
|
|
10
|
+
* cleanup, so an element removed before the document is parsed does nothing.
|
|
11
|
+
*/
|
|
12
|
+
export function afterParse(node, callback) {
|
|
13
|
+
const doc = node.ownerDocument;
|
|
14
|
+
if (doc.readyState !== "loading") {
|
|
15
|
+
callback();
|
|
16
|
+
return () => {};
|
|
17
|
+
}
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
doc.addEventListener("DOMContentLoaded", callback, { once: true, signal: controller.signal });
|
|
20
|
+
return () => controller.abort();
|
|
21
|
+
}
|
package/src/js/tab.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* ArrowUp/Down for aria-orientation="vertical"
|
|
10
10
|
* ArrowDown (focus selected panel)
|
|
11
11
|
* [hidden], disabled and panel-less tabs are skipped
|
|
12
|
+
* Overflow: a horizontal strip shows its whole selected tab, at connect and
|
|
13
|
+
* on every selection
|
|
12
14
|
*
|
|
13
15
|
* Self-registers via registerEnhancement: injected tablists wire automatically.
|
|
14
16
|
* The tab→panel map is rebuilt from the live tablist on each interaction,
|
|
@@ -17,9 +19,13 @@
|
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import { registerEnhancement } from "./enhance.js";
|
|
22
|
+
import { ensureInlineVisible } from "./inline-visible.js";
|
|
20
23
|
import { itemForKey, shouldIgnoreKey } from "./keys.js";
|
|
24
|
+
import { afterParse } from "./parsed.js";
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
// Ownership follows the ARIA structure, not the activation token: a tab
|
|
27
|
+
// belongs to its closest role="tablist", enhanced or not.
|
|
28
|
+
const TABLIST_SELECTOR = '[role="tablist"]';
|
|
23
29
|
|
|
24
30
|
// A tablist owns only the tabs directly beneath it, not those of a nested
|
|
25
31
|
// tablist. Filtering by closest owner keeps the outer list from operating on
|
|
@@ -85,10 +91,19 @@ function activate(tab) {
|
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
/* An overflowing horizontal strip shows the whole tab. At connect a
|
|
95
|
+
server-selected tab may render off-screen; on arrow keys and clicks the
|
|
96
|
+
native focus scroll is not enough: Chrome skips a tab already partly inside
|
|
97
|
+
the scrollport, so one showing a sliver at the edge stayed clipped. */
|
|
98
|
+
function reveal(list, tab) {
|
|
99
|
+
if (list.getAttribute("aria-orientation") !== "vertical") ensureInlineVisible(list, tab);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function activateAndFocus(list, tab) {
|
|
89
103
|
if (!tab) return;
|
|
90
104
|
activate(tab);
|
|
91
105
|
tab.focus();
|
|
106
|
+
reveal(list, tab);
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
function initialize(list) {
|
|
@@ -96,6 +111,7 @@ function initialize(list) {
|
|
|
96
111
|
if (!tabs.length) return;
|
|
97
112
|
const selected = tabs.find((tab) => tab.getAttribute("aria-selected") === "true") || tabs[0];
|
|
98
113
|
activate(selected);
|
|
114
|
+
reveal(list, selected);
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
function onKeydown(e) {
|
|
@@ -133,7 +149,7 @@ function onKeydown(e) {
|
|
|
133
149
|
if (!target) return;
|
|
134
150
|
|
|
135
151
|
e.preventDefault();
|
|
136
|
-
activateAndFocus(target);
|
|
152
|
+
activateAndFocus(list, target);
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
function onClick(e) {
|
|
@@ -146,14 +162,24 @@ function onClick(e) {
|
|
|
146
162
|
}
|
|
147
163
|
e.preventDefault();
|
|
148
164
|
if (tab.getAttribute("aria-selected") === "true") return;
|
|
149
|
-
|
|
150
|
-
tab.focus();
|
|
165
|
+
activateAndFocus(list, tab);
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
registerEnhancement("tabs", (list) => {
|
|
169
|
+
// Tabs find their list through role="tablist"; without it nothing would
|
|
170
|
+
// respond, so say why instead of wiring a dead strip.
|
|
171
|
+
if (list.getAttribute("role") !== "tablist") {
|
|
172
|
+
console.warn('data-enhance="tabs" needs role="tablist" on the same element', list);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
154
175
|
const controller = new AbortController();
|
|
155
|
-
|
|
176
|
+
// Once parsed: a tablist connected mid-parse had no tabs yet, so the roving
|
|
177
|
+
// tabindex and the selected tab's placement were skipped for good.
|
|
178
|
+
const cancelInit = afterParse(list, () => initialize(list));
|
|
156
179
|
list.addEventListener("click", onClick, { signal: controller.signal });
|
|
157
180
|
list.addEventListener("keydown", onKeydown, { signal: controller.signal });
|
|
158
|
-
return () =>
|
|
181
|
+
return () => {
|
|
182
|
+
cancelInit();
|
|
183
|
+
controller.abort();
|
|
184
|
+
};
|
|
159
185
|
});
|