actual-css 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +65 -44
  3. package/components.json +33 -13
  4. package/dist/actual.full.css +585 -499
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/app-ui.md +1 -1
  8. package/docs/design-notes/column-layout.md +103 -520
  9. package/docs/design-notes/forced-colors.md +12 -1
  10. package/docs/design-notes/naming.md +1 -1
  11. package/docs/design-notes/surface-reparenting.md +8 -9
  12. package/docs/navigation.json +2 -0
  13. package/docs/pages/components/accordion.md +55 -1
  14. package/docs/pages/components/alert.md +9 -15
  15. package/docs/pages/components/badge.md +6 -7
  16. package/docs/pages/components/bleed.md +42 -0
  17. package/docs/pages/components/button.md +30 -4
  18. package/docs/pages/components/card.md +21 -86
  19. package/docs/pages/components/close.md +48 -0
  20. package/docs/pages/components/data-list.md +26 -1
  21. package/docs/pages/components/dialog.md +16 -16
  22. package/docs/pages/components/drawer.md +9 -9
  23. package/docs/pages/components/navbar.md +5 -5
  24. package/docs/pages/components/spinner.md +3 -3
  25. package/docs/pages/enhancements/overview.md +5 -0
  26. package/docs/pages/enhancements/tooltip.md +3 -1
  27. package/docs/pages/examples/overview.md +2 -1
  28. package/docs/pages/forms/choice-cards.md +39 -0
  29. package/docs/pages/forms/combobox.md +23 -11
  30. package/docs/pages/forms/joined-controls.md +2 -5
  31. package/docs/pages/forms/overview.md +6 -0
  32. package/docs/pages/foundations/theming.md +64 -18
  33. package/docs/pages/foundations/tokens.md +28 -10
  34. package/docs/pages/guides/introduction.md +15 -23
  35. package/docs/pages/guides/modular-import.md +53 -6
  36. package/docs/pages/guides/progressive-enhancement.md +6 -3
  37. package/docs/pages/guides/tailwind.md +17 -5
  38. package/docs/pages/layout/column-layout.md +92 -54
  39. package/docs/pages/layout/media.md +1 -1
  40. package/docs/pages/layout/overview.md +1 -1
  41. package/docs/pages/utilities/overview.md +1 -1
  42. package/docs/pages/utilities/sizing-wrapping.md +5 -3
  43. package/llms.txt +24 -4
  44. package/package.json +6 -6
  45. package/reserved-classes.json +5 -30
  46. package/skills/actual-css/PROJECT.md.example +65 -0
  47. package/skills/actual-css/README.md +64 -0
  48. package/skills/actual-css/SKILL.md +234 -0
  49. package/skills/actual-css/references/accessibility.md +44 -0
  50. package/skills/actual-css/references/api-and-vocabulary.md +94 -0
  51. package/skills/actual-css/references/enhancements.md +59 -0
  52. package/skills/actual-css/references/forms.md +49 -0
  53. package/skills/actual-css/references/layout-and-theming.md +82 -0
  54. package/skills/actual-css/references/review-checklist.md +51 -0
  55. package/skills/actual-css/scripts/inspect-actual-css.mjs +171 -0
  56. package/src/css/components/accordion.css +44 -7
  57. package/src/css/components/alert.css +23 -43
  58. package/src/css/components/app-nav.css +9 -0
  59. package/src/css/components/badge.css +11 -39
  60. package/src/css/components/bleed.css +79 -0
  61. package/src/css/components/button.css +6 -17
  62. package/src/css/components/card.css +7 -40
  63. package/src/css/components/close.css +91 -0
  64. package/src/css/components/data-list.css +18 -0
  65. package/src/css/components/drawer.css +15 -56
  66. package/src/css/components/fab.css +10 -0
  67. package/src/css/components/flyout.css +1 -0
  68. package/src/css/components/index.css +2 -0
  69. package/src/css/components/join.css +36 -20
  70. package/src/css/components/menu.css +12 -0
  71. package/src/css/components/modal.css +10 -57
  72. package/src/css/components/navbar.css +27 -0
  73. package/src/css/core/focus.css +9 -3
  74. package/src/css/core/theme.css +25 -43
  75. package/src/css/core/tokens.css +1 -1
  76. package/src/css/core/variants.css +3 -33
  77. package/src/css/forms/choice-card.css +74 -4
  78. package/src/css/forms/choice.css +2 -3
  79. package/src/css/forms/control.css +15 -8
  80. package/src/css/forms/custom-select.css +4 -4
  81. package/src/css/forms/native.css +4 -4
  82. package/src/css/forms/range.css +23 -8
  83. package/src/css/forms/switch.css +2 -3
  84. package/src/css/layout/app-layout.css +1 -1
  85. package/src/css/layout/column-layout.css +26 -111
  86. package/src/css/utilities/extra.css +7 -6
  87. package/src/js/dialog.js +95 -24
  88. package/src/js/enhance.js +7 -3
  89. package/src/js/escape.js +1 -0
  90. package/src/js/flyout.js +10 -1
  91. package/src/js/focus-group.js +2 -1
  92. package/src/js/keys.js +14 -0
  93. package/src/js/menu.js +2 -1
  94. package/src/js/status.js +19 -0
  95. package/src/js/surface.js +12 -8
  96. package/src/js/tab.js +2 -1
  97. package/src/js/tooltip.js +18 -1
  98. package/src/js/validation.js +5 -6
  99. package/dist/actual.css +0 -876
  100. package/dist/actual.js +0 -1
  101. package/dist/actual.min.css +0 -1
@@ -0,0 +1,51 @@
1
+ # Consumer review checklist
2
+
3
+ ## Vocabulary and imports
4
+
5
+ - Every framework-looking class exists in the installed Actual CSS version.
6
+ - Intent/variant composition is used instead of invented compound names.
7
+ - Runtime `is-*` state is not authored as application API.
8
+ - The project imports the family/module that owns the markup it uses.
9
+ - `forms` vs `forms/all` is intentional.
10
+ - JS imports match the behavior actually enabled.
11
+
12
+ ## CSS ownership
13
+
14
+ - Application CSS is not recreating an existing layout/component/focus behavior unnecessarily.
15
+ - Public hooks were considered before replacement CSS.
16
+ - Palette values remain centralized in theme/token definitions.
17
+ - `--gap` is only rebound when inherited rhythm should change.
18
+ - Component-scoped hooks are not read elsewhere without an intentional fallback.
19
+
20
+ ## Layout
21
+
22
+ - Primitive choice follows child relationships, not a single mockup's item count.
23
+ - `.app-layout` is used only for its actual shell contract.
24
+ - Exact track needs use the appropriate grid hook before a new generic primitive is invented.
25
+
26
+ ## Forms
27
+
28
+ - `inputmode` is not treated as validation/filtering.
29
+ - `data-filter` is used only when destructive rewriting is intended.
30
+ - Masks are not mistaken for semantic validity.
31
+ - Server/application validation remains authoritative for domain rules.
32
+ - Help/errors and invalid state are connected accessibly.
33
+
34
+ ## Enhancements
35
+
36
+ - Presentation, semantics, behavior opt-in, and configuration remain separate.
37
+ - Enhancement tokens and feature attributes are documented by the installed version.
38
+ - Application code does not fight Actual for Popover or positioning ownership.
39
+ - Interactive floating content is not placed in a tooltip.
40
+
41
+ ## Accessibility
42
+
43
+ - Keyboard focus remains visible.
44
+ - Icon-only controls are named.
45
+ - Disabled/current/selected/expanded semantics match visible state.
46
+ - Keyboard behavior matches claimed ARIA roles.
47
+ - Custom states consider forced colors and reduced motion where relevant.
48
+
49
+ ## Version safety
50
+
51
+ When uncertain, inspect the installed manifests, package exports, source, and docs instead of extrapolating from this starter skill.
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ function readJson(file) {
8
+ return JSON.parse(fs.readFileSync(file, "utf8"));
9
+ }
10
+
11
+ function exists(file) {
12
+ try {
13
+ return fs.existsSync(file);
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function detectRoot(start = process.cwd()) {
20
+ const explicit = process.env.ACTUAL_CSS_ROOT;
21
+ if (explicit) {
22
+ const root = path.resolve(explicit);
23
+ if (!exists(path.join(root, "package.json"))) {
24
+ throw new Error(`ACTUAL_CSS_ROOT does not contain package.json: ${root}`);
25
+ }
26
+ return root;
27
+ }
28
+
29
+ let dir = path.resolve(start);
30
+ while (true) {
31
+ const localPkg = path.join(dir, "package.json");
32
+ if (exists(localPkg)) {
33
+ try {
34
+ const pkg = readJson(localPkg);
35
+ if (pkg.name === "actual-css" && exists(path.join(dir, "components.json"))) {
36
+ return dir;
37
+ }
38
+ } catch {
39
+ // Ignore malformed unrelated package.json files while walking.
40
+ }
41
+ }
42
+
43
+ const installed = path.join(dir, "node_modules", "actual-css");
44
+ if (exists(path.join(installed, "package.json"))) {
45
+ return installed;
46
+ }
47
+
48
+ const parent = path.dirname(dir);
49
+ if (parent === dir) break;
50
+ dir = parent;
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ function fail(message) {
57
+ process.stderr.write(`${message}\n`);
58
+ process.exitCode = 1;
59
+ }
60
+
61
+ function print(value) {
62
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
63
+ }
64
+
65
+ function usage() {
66
+ process.stdout.write(`Actual CSS inspector\n\n`);
67
+ process.stdout.write(`Usage:\n`);
68
+ process.stdout.write(` inspect-actual-css.mjs --source\n`);
69
+ process.stdout.write(` inspect-actual-css.mjs <component>\n`);
70
+ process.stdout.write(` inspect-actual-css.mjs --class <class>\n`);
71
+ process.stdout.write(` inspect-actual-css.mjs --exports\n`);
72
+ process.stdout.write(` inspect-actual-css.mjs --enhancements\n`);
73
+ process.stdout.write(` inspect-actual-css.mjs --list\n\n`);
74
+ process.stdout.write(`Set ACTUAL_CSS_ROOT to inspect a specific checkout/package.\n`);
75
+ }
76
+
77
+ const args = process.argv.slice(2);
78
+ if (args.includes("--help") || args.includes("-h")) {
79
+ usage();
80
+ process.exit(0);
81
+ }
82
+
83
+ const root = detectRoot();
84
+ if (!root) {
85
+ fail("Could not find Actual CSS. Run inside its repository/a consuming project or set ACTUAL_CSS_ROOT.");
86
+ process.exit(1);
87
+ }
88
+
89
+ const pkg = readJson(path.join(root, "package.json"));
90
+ const componentsPath = path.join(root, "components.json");
91
+ const reservedPath = path.join(root, "reserved-classes.json");
92
+ const components = exists(componentsPath) ? readJson(componentsPath) : [];
93
+ const reservedRaw = exists(reservedPath) ? readJson(reservedPath) : [];
94
+ const reserved = Array.isArray(reservedRaw)
95
+ ? reservedRaw
96
+ : Array.isArray(reservedRaw.classes)
97
+ ? reservedRaw.classes
98
+ : Object.keys(reservedRaw);
99
+
100
+ if (args.length === 0 || args[0] === "--list") {
101
+ print({
102
+ version: pkg.version,
103
+ root,
104
+ components: components.map((item) => item.name),
105
+ });
106
+ process.exit(0);
107
+ }
108
+
109
+ if (args[0] === "--source") {
110
+ print({
111
+ name: pkg.name,
112
+ version: pkg.version,
113
+ root,
114
+ packageJson: path.join(root, "package.json"),
115
+ componentsJson: exists(componentsPath) ? componentsPath : null,
116
+ reservedClassesJson: exists(reservedPath) ? reservedPath : null,
117
+ });
118
+ process.exit(0);
119
+ }
120
+
121
+ if (args[0] === "--exports") {
122
+ print({ version: pkg.version, exports: pkg.exports ?? {} });
123
+ process.exit(0);
124
+ }
125
+
126
+ if (args[0] === "--enhancements") {
127
+ const matches = components
128
+ .filter((item) => item.jsImport || (item.enhancements && item.enhancements.length))
129
+ .map((item) => ({
130
+ name: item.name,
131
+ jsImport: item.jsImport ?? null,
132
+ enhancements: item.enhancements ?? [],
133
+ states: item.states ?? [],
134
+ }));
135
+ print({ version: pkg.version, components: matches });
136
+ process.exit(0);
137
+ }
138
+
139
+ if (args[0] === "--class") {
140
+ const className = (args[1] ?? "").replace(/^\./, "");
141
+ if (!className) {
142
+ fail("--class requires a class name");
143
+ process.exit(1);
144
+ }
145
+ const owners = components
146
+ .filter((item) => (item.classes ?? []).includes(className))
147
+ .map((item) => item.name);
148
+ print({
149
+ version: pkg.version,
150
+ class: className,
151
+ reserved: reserved.includes(className),
152
+ owners,
153
+ });
154
+ process.exit(0);
155
+ }
156
+
157
+ const name = args[0];
158
+ const exact = components.find((item) => item.name === name);
159
+ if (exact) {
160
+ print({ version: pkg.version, component: exact });
161
+ process.exit(0);
162
+ }
163
+
164
+ const byClass = components.filter((item) => (item.classes ?? []).includes(name));
165
+ if (byClass.length) {
166
+ print({ version: pkg.version, query: name, matchedByClass: byClass });
167
+ process.exit(0);
168
+ }
169
+
170
+ fail(`No component or owned class matched: ${name}`);
171
+ process.exit(1);
@@ -15,11 +15,15 @@
15
15
  *
16
16
  * Public hooks:
17
17
  * --accordion-radius
18
+ * --accordion-pad
18
19
  * --accordion-marker-color
20
+ * --accordion-marker-size
19
21
  */
20
22
  .accordion {
21
23
  --accordion-radius: var(--radius);
24
+ --accordion-pad: var(--space-40);
22
25
  --accordion-marker-color: var(--text-muted);
26
+ --accordion-marker-size: 1rem;
23
27
 
24
28
  border: var(--border-width) solid var(--border);
25
29
  border-radius: var(--accordion-radius);
@@ -36,7 +40,7 @@
36
40
  gap: var(--space-30);
37
41
  cursor: pointer;
38
42
  list-style: none;
39
- padding: var(--space-40);
43
+ padding: var(--accordion-pad);
40
44
  color: var(--text);
41
45
  font-weight: var(--font-weight-medium);
42
46
  }
@@ -52,12 +56,17 @@
52
56
  display: none;
53
57
  }
54
58
 
59
+ /* The marker is the chevron itself: the box is filled with
60
+ --accordion-marker-color and then clipped to the glyph by the mask. Anything
61
+ painted on this pseudo-element — a background, a ring, a shadow — is clipped
62
+ with it, so a decorated marker (a chevron in a pill) needs a real element;
63
+ see the accordion docs for the opt-out. */
55
64
  .accordion summary::after {
56
65
  content: "";
57
66
  flex: 0 0 auto;
58
67
  margin-inline-start: auto;
59
- inline-size: 1rem;
60
- block-size: 1rem;
68
+ inline-size: var(--accordion-marker-size);
69
+ block-size: var(--accordion-marker-size);
61
70
  background-color: var(--accordion-marker-color);
62
71
  -webkit-mask: var(--icon-chevron) center / contain no-repeat;
63
72
  mask: var(--icon-chevron) center / contain no-repeat;
@@ -68,9 +77,14 @@
68
77
  transform: rotate(180deg);
69
78
  }
70
79
 
80
+ /* The panel, not the item, owns the inset here — .separated paints a card on
81
+ <details> but leaves it unpadded — so the panel is what a .bleed child
82
+ escapes (components/bleed.css). */
71
83
  .accordion details > summary + * {
72
- padding-block: 0 var(--space-40);
73
- padding-inline: var(--space-40);
84
+ --surface-pad: var(--accordion-pad);
85
+
86
+ padding-block: 0 var(--accordion-pad);
87
+ padding-inline: var(--accordion-pad);
74
88
  color: var(--text-muted);
75
89
  margin-block: 0;
76
90
  }
@@ -79,6 +93,9 @@
79
93
  padding-block-start: var(--space-10);
80
94
  }
81
95
 
96
+ /* Also cancels the lift of a first .bleed child (bleed.css, lower
97
+ specificity): the panel's top edge is the summary, not the item edge, so a
98
+ bleed reaches only the sides and the bottom. */
82
99
  .accordion details > summary + * > :first-child {
83
100
  margin-block-start: 0;
84
101
  }
@@ -103,6 +120,12 @@
103
120
  padding-inline: 0;
104
121
  }
105
122
 
123
+ /* Flush already sits at the embedding surface's edge, so there is no inline
124
+ inset left for a .bleed child to cancel. */
125
+ .accordion.flush details > summary + * {
126
+ --surface-pad: 0px;
127
+ }
128
+
106
129
  /* Separated — each item owns a full card. The item's own border beats the
107
130
  shared details + details separator on specificity alone, so nothing needs
108
131
  undoing for the second item onwards. */
@@ -119,8 +142,22 @@
119
142
  background: var(--surface-raised);
120
143
  }
121
144
 
145
+ /* A last .bleed child inherits its bottom corners from the panel, but the
146
+ rounded box is the item (.separated) or the group (last item) — relay the
147
+ radius to the panel. Explicit rather than inherit: in browsers with
148
+ ::details-content the panel's flat-tree parent is that slot, not
149
+ <details>. */
150
+ .accordion.separated > details > summary + *,
151
+ .accordion:not(.flush) > details:last-child > summary + * {
152
+ border-end-start-radius: var(--accordion-radius);
153
+ border-end-end-radius: var(--accordion-radius);
154
+ }
155
+
122
156
  @media (forced-colors: active) {
123
- .accordion {
124
- --accordion-marker-color: CanvasText;
157
+ /* Written on the property, not the hook: an application hook override at
158
+ the same specificity loads after this file and would win the variable,
159
+ leaving a Canvas-remapped fill. The property keeps CanvasText. */
160
+ .accordion summary::after {
161
+ background-color: CanvasText;
125
162
  }
126
163
  }
@@ -16,7 +16,6 @@
16
16
  * --alert-font-size
17
17
  * --alert-radius
18
18
  * --alert-icon-size
19
- * --alert-dismiss-size
20
19
  * --alert-border-inline-start-color
21
20
  * --alert-border-inline-start-width
22
21
  *
@@ -27,7 +26,6 @@
27
26
  * --alert-bg
28
27
  * --alert-fg
29
28
  * --alert-border
30
- * --alert-dismiss-icon-size
31
29
  */
32
30
  /* Intent boundary — inherited intents do not tint this component.
33
31
  Local intent classes still win because :where() has zero specificity. */
@@ -76,13 +74,13 @@
76
74
  The slots are excluded because they are anatomy, not content: an
77
75
  admonition's title bar has to stay flush against its body. The list follows
78
76
  the anatomy, so it grows with it. */
79
- .alert > :not(:where(.alert-icon, .alert-dismiss, .alert-title, .alert-body)) {
77
+ .alert > :not(:where(.alert-icon, .close, .alert-title, .alert-body)) {
80
78
  margin-block: 0;
81
79
  }
82
80
 
83
81
  .alert
84
- > :not(:where(.alert-icon, .alert-dismiss, .alert-title, .alert-body))
85
- + :not(:where(.alert-icon, .alert-dismiss, .alert-title, .alert-body)) {
82
+ > :not(:where(.alert-icon, .close, .alert-title, .alert-body))
83
+ + :not(:where(.alert-icon, .close, .alert-title, .alert-body)) {
86
84
  margin-block-start: var(--space-10);
87
85
  }
88
86
 
@@ -120,42 +118,24 @@
120
118
  vertical-align: -0.125em;
121
119
  }
122
120
 
123
- /* Dismiss — compact close button for inline alerts. Transparent at rest with
124
- a reduced opacity glyph so it stays secondary inside an already-colored
125
- alert; hover reinforces it via the shared overlay. Uses the shared
126
- --icon-close mask, so no icon font or text glyph is required in the markup.
127
- Standard alerts place it as a direct trailing child; admonitions place it
128
- inside .alert-title (see below). */
129
- .alert-dismiss {
130
- --alert-dismiss-size: 1.75rem;
131
- --alert-dismiss-icon-size: 0.875rem;
132
-
133
- display: inline-grid;
134
- place-items: center;
135
- inline-size: var(--alert-dismiss-size);
136
- block-size: var(--alert-dismiss-size);
137
- flex: none;
138
- padding: 0;
139
- border: 0;
140
- border-radius: var(--radius);
141
- background: transparent;
142
- color: currentColor;
143
- opacity: 0.65;
144
- cursor: pointer;
121
+ /* Close (components/close.css) — compact, and at reduced opacity so it stays
122
+ secondary inside an already-colored alert. Standard alerts place it as a
123
+ direct trailing child; admonitions place it inside .alert-title (see
124
+ below). The sizes are declared on the alert so an author can still set
125
+ them on the alert or on the button. */
126
+ .alert {
127
+ --close-size: 1.75rem;
128
+ --close-icon-size: 0.875rem;
145
129
  }
146
130
 
147
- .alert-dismiss::before {
148
- content: "";
149
- inline-size: var(--alert-dismiss-icon-size);
150
- block-size: var(--alert-dismiss-icon-size);
151
- background: currentColor;
152
- -webkit-mask: var(--icon-close) center / contain no-repeat;
153
- mask: var(--icon-close) center / contain no-repeat;
131
+ .alert > .close,
132
+ .alert > .alert-title > .close {
133
+ opacity: 0.65;
154
134
  }
155
135
 
156
136
  @media (hover: hover) {
157
- .alert-dismiss:hover {
158
- background: var(--hover-overlay);
137
+ .alert > .close:hover,
138
+ .alert > .alert-title > .close:hover {
159
139
  opacity: 1;
160
140
  }
161
141
  }
@@ -164,7 +144,7 @@
164
144
  both. Icon and dismiss are pinned to the outer columns; anything else (the
165
145
  message content) flows into the remaining text column. */
166
146
  @supports selector(.alert:has(> *)) {
167
- .alert:has(> .alert-icon, > .alert-dismiss) {
147
+ .alert:has(> .alert-icon, > .close) {
168
148
  display: grid;
169
149
  grid-template-columns: minmax(0, 1fr);
170
150
  align-items: var(--items-align, start);
@@ -183,19 +163,19 @@
183
163
  margin-block-start: 0.125em;
184
164
  }
185
165
 
186
- .alert:has(> .alert-icon) > :not(.alert-icon, .alert-dismiss) {
166
+ .alert:has(> .alert-icon) > :not(.alert-icon, .close) {
187
167
  grid-column: 2;
188
168
  }
189
169
 
190
- .alert:has(> .alert-dismiss) {
170
+ .alert:has(> .close) {
191
171
  grid-template-columns: minmax(0, 1fr) auto;
192
172
  }
193
173
 
194
- .alert:has(> .alert-icon):has(> .alert-dismiss) {
174
+ .alert:has(> .alert-icon):has(> .close) {
195
175
  grid-template-columns: auto minmax(0, 1fr) auto;
196
176
  }
197
177
 
198
- .alert:has(> .alert-dismiss) > .alert-dismiss {
178
+ .alert:has(> .close) > .close {
199
179
  grid-column: -2 / -1;
200
180
  grid-row: 1;
201
181
  }
@@ -243,8 +223,8 @@
243
223
  border-start-end-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
244
224
  }
245
225
 
246
- /* Dismiss inside an admonition title bar rides the inline end. */
247
- .alert.admonition > .alert-title > .alert-dismiss {
226
+ /* Close inside an admonition title bar rides the inline end. */
227
+ .alert.admonition > .alert-title > .close {
248
228
  margin-inline-start: auto;
249
229
  }
250
230
 
@@ -70,6 +70,15 @@
70
70
  color: var(--text);
71
71
  }
72
72
 
73
+ /* Same contextual hover as .nav-link (navbar.css): a tint of the tile's ink
74
+ reads on any --ui-* bar; the baseline keeps a legible light chip. */
75
+ @supports (color: color-mix(in oklch, red, white)) {
76
+ .app-nav > a:hover {
77
+ background: color-mix(in oklch, currentColor 10%, transparent);
78
+ color: var(--ui-fg, var(--text));
79
+ }
80
+ }
81
+
73
82
  /* Current tile is already a full selected surface. Selection never changes
74
83
  text metrics: the shared medium weight is kept, no permanent indicator is
75
84
  added here. */
@@ -14,7 +14,6 @@
14
14
  * --badge-icon-size
15
15
  * --badge-dot-size
16
16
  * --badge-radius
17
- * --badge-dismiss-icon-size
18
17
  *
19
18
  * Internal:
20
19
  * --badge-default-bg
@@ -45,7 +44,10 @@
45
44
  --badge-font-size: var(--font-size-sm);
46
45
  --badge-icon-size: 1em;
47
46
  --badge-dot-size: 0.5rem;
48
- --badge-dismiss-icon-size: 0.875em;
47
+ /* Declared on the badge, not the button, so an author can still set them
48
+ on either. Same element as --badge-size, so .sm/.lg carry through. */
49
+ --close-size: max(var(--badge-size), 24px);
50
+ --close-icon-size: 0.875em;
49
51
  --badge-default-bg: var(--surface-subtle);
50
52
  --badge-default-fg: var(--intent, var(--text));
51
53
  --badge-default-border: var(--border);
@@ -95,8 +97,8 @@
95
97
  --badge-dot-size: 0.625rem;
96
98
  }
97
99
 
98
- /* Decorative children follow the badge's optical size. Text and interactive
99
- dismiss buttons retain their own boxes. */
100
+ /* Decorative children follow the badge's optical size. Text and the .close
101
+ button retain their own boxes. */
100
102
  .badge > :where(svg, img, [aria-hidden="true"]:not(.dot)) {
101
103
  flex: none;
102
104
  /* The shared rule maps --badge-icon-size onto font-size. One em is therefore
@@ -149,45 +151,15 @@
149
151
  background-color: currentColor;
150
152
  }
151
153
 
152
- .badge > button {
153
- display: inline-grid;
154
- place-items: center;
155
- flex: 0 0 auto;
156
- /* WCAG 2.5.8 floors pointer targets at 24 CSS px. Density and local size may
157
- keep the badge visually compact through the negative margins, but never
158
- shrink its action target below that literal floor. */
159
- min-inline-size: max(var(--badge-size), 24px);
160
- min-block-size: max(var(--badge-size), 24px);
154
+ /* Removable tag — a .close (components/close.css) inside the pill. WCAG 2.5.8
155
+ floors pointer targets at 24 CSS px: density and local size may keep the
156
+ badge visually compact through the negative margins, but never shrink the
157
+ target below that literal floor. */
158
+ .badge > .close {
161
159
  margin-block: -0.25rem;
162
160
  margin-inline-start: -0.15em;
163
161
  margin-inline-end: -0.45em;
164
- padding: 0.25rem;
165
- border: 0;
166
162
  border-radius: var(--radius-full);
167
- background: transparent;
168
- color: inherit;
169
- font: inherit;
170
- line-height: 1;
171
- cursor: pointer;
172
- }
173
-
174
- /* Dismiss mark — an empty dismiss button paints its own X from the shared
175
- --icon-close mask, so no icon font or text glyph is required in the markup
176
- (same contract as .alert-dismiss and .dialog-close). A mask box is a block
177
- box, so place-items centers the ink itself; a text glyph would only center
178
- its line box and land the ink low, because a lowercase x has no descender
179
- and only x-height of ascent. A button with its own content keeps it. */
180
- .badge > button:empty::before {
181
- content: "";
182
- inline-size: var(--badge-dismiss-icon-size);
183
- block-size: var(--badge-dismiss-icon-size);
184
- background: currentColor;
185
- -webkit-mask: var(--icon-close) center / contain no-repeat;
186
- mask: var(--icon-close) center / contain no-repeat;
187
- }
188
-
189
- .badge > button:not(:disabled):hover {
190
- background: var(--hover-overlay);
191
163
  }
192
164
 
193
165
  /* Hover recipes — sets --ui-hover-bg consumed by the hover rule below.
@@ -0,0 +1,79 @@
1
+ /*
2
+ * Full-bleed children — the shared escape hatch for a padded surface.
3
+ *
4
+ * Docs: docs/pages/components/bleed.md
5
+ *
6
+ * A surface that insets its direct children relays that inset to them as
7
+ * --surface-pad; a direct .bleed child cancels it and reaches the surface
8
+ * edge. Card and drawer pad at the container level, so they own the token
9
+ * there; the accordion pads its summary and its panel instead, so the panel
10
+ * owns it and the item does not.
11
+ *
12
+ * Why the token is handed to the children rather than declared on the surface:
13
+ * custom properties inherit, so a surface-level declaration would also reach a
14
+ * grandchild, which sits at its wrapper's edge rather than the surface's and
15
+ * would pull a negative margin out of the wrong box. Relaying it one level and
16
+ * clearing it below keeps the child combinator's guarantee while leaving the
17
+ * .bleed rules themselves free of the surface list.
18
+ *
19
+ * Why the clearing rule comes first: both rules are zero-specificity, so
20
+ * source order decides where they overlap, and they overlap on exactly one
21
+ * element — a direct child of a surface nested inside another surface. That
22
+ * child must keep the inner surface's inset, so the relay has to win.
23
+ *
24
+ * Public hooks:
25
+ * --bleed-pad (read-only: the inset relayed to a surface's direct children;
26
+ * pad a band that is not a header/footer/figure with it)
27
+ *
28
+ * Framework plumbing:
29
+ * --surface-pad (declared by the surface)
30
+ */
31
+ :where(.card, .drawer, .accordion details > summary + *) > * * {
32
+ --bleed-pad: initial;
33
+ }
34
+
35
+ :where(.card, .drawer, .accordion details > summary + *) > * {
36
+ --bleed-pad: var(--surface-pad);
37
+ }
38
+
39
+ /* Inert without a surface: the fallback resolves the negative margin to zero
40
+ rather than dropping the declaration and inheriting a stray inset. */
41
+ .bleed {
42
+ margin-inline: calc(var(--bleed-pad, 0px) * -1);
43
+ max-inline-size: none;
44
+ }
45
+
46
+ .bleed:first-child {
47
+ margin-block-start: calc(var(--bleed-pad, 0px) * -1);
48
+ border-start-start-radius: inherit;
49
+ border-start-end-radius: inherit;
50
+ }
51
+
52
+ .bleed:last-child {
53
+ margin-block-end: calc(var(--bleed-pad, 0px) * -1);
54
+ border-end-start-radius: inherit;
55
+ border-end-end-radius: inherit;
56
+ }
57
+
58
+ /* Media bleed */
59
+ :where(img, picture, video, canvas, svg).bleed {
60
+ display: block;
61
+ inline-size: calc(100% + var(--bleed-pad, 0px) * 2);
62
+ max-inline-size: none;
63
+ }
64
+
65
+ picture.bleed {
66
+ overflow: hidden;
67
+ }
68
+
69
+ picture.bleed > img {
70
+ display: block;
71
+ inline-size: 100%;
72
+ block-size: auto;
73
+ }
74
+
75
+ /* Padded structural bleed — the band keeps the surface's own rhythm while its
76
+ background reaches the edge. */
77
+ :where(header, footer, figure).bleed {
78
+ padding: var(--bleed-pad, 0px);
79
+ }