@livenetworks/ashlar 2.1.12 → 2.2.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 (108) hide show
  1. package/README.md +25 -2
  2. package/components/COMPONENTS.md +29 -25
  3. package/components/ln-api-connector/ln-api-connector.schema.json +105 -105
  4. package/components/ln-chart/ln-chart-dev.scss +0 -4
  5. package/components/ln-core/README.md +16 -60
  6. package/components/ln-core/index.js +1 -1
  7. package/components/ln-core/ln-core.schema.json +120 -120
  8. package/components/ln-core/query.js +113 -113
  9. package/components/ln-core/reactive.js +0 -59
  10. package/components/ln-data-coordinator/README.md +2 -0
  11. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -1
  12. package/components/ln-data-coordinator/ln-data-coordinator.schema.json +196 -196
  13. package/components/ln-data-coordinator/src/ln-data-coordinator.js +457 -432
  14. package/components/ln-data-store/ln-data-store.schema.json +92 -92
  15. package/components/ln-date/ln-date.js +1 -1
  16. package/components/ln-date/src/ln-date.js +1 -1
  17. package/components/ln-debug/ln-debug.js +1 -1
  18. package/components/ln-debug/ln-debug.schema.json +28 -28
  19. package/components/ln-debug/src/generated-attributes.js +304 -302
  20. package/components/ln-dropdown/ln-dropdown.schema.json +90 -90
  21. package/components/ln-external-links/README.md +19 -19
  22. package/components/ln-external-links/ln-external-links.js +1 -1
  23. package/components/ln-external-links/ln-external-links.schema.json +9 -0
  24. package/components/ln-external-links/ln-external-links.scss +12 -0
  25. package/components/ln-external-links/src/ln-external-links.js +3 -2
  26. package/components/ln-filter/README.md +2 -1
  27. package/components/ln-filter/ln-filter.js +1 -1
  28. package/components/ln-filter/ln-filter.schema.json +157 -169
  29. package/components/ln-filter/src/ln-filter.js +4 -4
  30. package/components/ln-form/ln-form.schema.json +57 -57
  31. package/components/ln-key/ln-key.js +1 -1
  32. package/components/ln-key/ln-key.schema.json +8 -0
  33. package/components/ln-key/src/ln-key.js +8 -5
  34. package/components/ln-obfuscator/ln-obfuscator.js +1 -1
  35. package/components/ln-obfuscator/ln-obfuscator.schema.json +6 -0
  36. package/components/ln-obfuscator/src/ln-obfuscator.js +2 -2
  37. package/components/ln-options/ln-options.schema.json +47 -47
  38. package/components/ln-popover/ln-popover.schema.json +82 -82
  39. package/components/ln-progress/ln-progress.schema.json +0 -13
  40. package/components/ln-search/ln-search.schema.json +111 -111
  41. package/components/ln-sort/README.md +1 -0
  42. package/components/ln-sort/ln-sort.js +1 -1
  43. package/components/ln-sort/ln-sort.schema.json +100 -112
  44. package/components/ln-sort/src/sort-model.js +1 -6
  45. package/components/ln-sortable/ln-sortable.schema.json +38 -38
  46. package/components/ln-sortable/ln-sortable.scss +25 -25
  47. package/components/ln-table/README.md +8 -1
  48. package/components/ln-table/ln-table.js +1 -1
  49. package/components/ln-table/ln-table.schema.json +195 -195
  50. package/components/ln-table/src/ln-table.js +43 -7
  51. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -1
  52. package/components/ln-table-coordinator/ln-table-coordinator.schema.json +18 -0
  53. package/components/ln-table-coordinator/src/ln-table-coordinator.js +104 -2
  54. package/components/ln-toast/README.md +1 -1
  55. package/components/ln-toast/ln-toast.js +1 -1
  56. package/components/ln-toast/ln-toast.schema.json +6 -0
  57. package/components/ln-toast/src/ln-toast.js +1 -1
  58. package/components/ln-validate/README.md +6 -6
  59. package/components/ln-validate/ln-validate-dev.scss +0 -5
  60. package/components/ln-validate/ln-validate.js +1 -1
  61. package/components/ln-validate/ln-validate.schema.json +0 -6
  62. package/components/ln-validate/src/ln-validate.js +48 -29
  63. package/ln-ashlar.html-data.json +1824 -1814
  64. package/package.json +15 -5
  65. package/theme/README.md +74 -0
  66. package/theme/base/_global.scss +2 -2
  67. package/theme/base/_reset.scss +11 -0
  68. package/theme/components/_app-shell.scss +0 -8
  69. package/theme/components/_form.scss +6 -6
  70. package/theme/components/_ln-table.scss +6 -3
  71. package/theme/components/_scrollbar.scss +5 -4
  72. package/theme/components/_sortable.scss +19 -19
  73. package/theme/components/_stat-card.scss +1 -0
  74. package/theme/components/_table.scss +11 -35
  75. package/theme/config/_density.scss +110 -65
  76. package/theme/config/_palette.scss +71 -49
  77. package/theme/config/_theme.scss +27 -31
  78. package/theme/config/_tokens.scss +28 -26
  79. package/theme/config/mixins/_app-shell.scss +17 -21
  80. package/theme/config/mixins/_avatar.scss +2 -2
  81. package/theme/config/mixins/_btn.scss +4 -2
  82. package/theme/config/mixins/_card.scss +88 -71
  83. package/theme/config/mixins/_colors.scss +2 -1
  84. package/theme/config/mixins/_display.scss +12 -0
  85. package/theme/config/mixins/_footer.scss +3 -1
  86. package/theme/config/mixins/_form.scss +74 -55
  87. package/theme/config/mixins/_layout.scss +9 -0
  88. package/theme/config/mixins/_ln-table.scss +22 -15
  89. package/theme/config/mixins/_modal.scss +3 -0
  90. package/theme/config/mixins/_nav.scss +31 -8
  91. package/theme/config/mixins/_sidebar.scss +2 -1
  92. package/theme/config/mixins/_spacing.scss +4 -4
  93. package/theme/config/mixins/_stat-card.scss +33 -2
  94. package/theme/config/mixins/_status-badge.scss +1 -0
  95. package/theme/config/mixins/_table.scss +87 -69
  96. package/theme/config/mixins/_tabs.scss +31 -0
  97. package/theme/config/mixins/_toast.scss +0 -1
  98. package/theme/designs/relaxed/_card.scss +12 -0
  99. package/theme/designs/relaxed/_layout.scss +24 -0
  100. package/theme/ln-ashlar-core.scss +1 -0
  101. package/theme/ln-ashlar-dev.scss +0 -1
  102. package/theme/ln-ashlar-relaxed.scss +67 -0
  103. package/theme/tailwind/components.css +2198 -0
  104. package/theme/tailwind/entry.css +3 -0
  105. package/theme/tailwind/theme.css +23 -0
  106. package/theme/utilities/_utilities.scss +1 -11
  107. package/web-types.json +2351 -2337
  108. package/components/ln-progress/ln-progress-dev.scss +0 -8
@@ -7,6 +7,7 @@
7
7
  @use 'shadows' as *;
8
8
  @use 'transitions' as *;
9
9
  @use 'position' as *;
10
+ @use 'focus' as *;
10
11
 
11
12
  // Card & Section — composite mixins
12
13
 
@@ -18,31 +19,25 @@
18
19
  @include border;
19
20
  @include w-full;
20
21
  @include flex-col;
21
- border-radius: var(--radius);
22
- @include overflow-hidden;
22
+ border-radius: var(--panel-radius, var(--radius));
23
+ // `clip`, not `hidden`: no scroll container, so a table's sticky thead
24
+ // binds to the page scroller. min-inline-size keeps the grid/flex shrink
25
+ // that `hidden` gave for free.
26
+ overflow: clip;
27
+ min-inline-size: 0;
23
28
  @include transition;
24
29
  box-shadow: var(--shadow), var(--surface-highlight, inset 0 0 0 0 transparent);
25
30
 
26
- &:hover {
31
+ // Only whole-card links/buttons signal interaction. Passive containers
32
+ // must not respond merely because the pointer crosses their content.
33
+ &:is(a, button):hover:not(:disabled),
34
+ &:has(> a:hover) {
27
35
  --color-border: color-mix(in srgb, var(--color-accent) 35%, transparent);
28
- --shadow: var(--nested-card-shadow-hover, var(--shadow-floating));
29
- }
30
-
31
- // Nesting ladder: when nested inside a section-card or another card,
32
- // climb to elevated surface to preserve visual depth.
33
- .section-card &,
34
- .section &,
35
- .card & {
36
- --color-bg: var(--bg-elevated);
37
- --shadow: var(--shadow-floating);
38
36
  }
39
37
 
40
- // Bulletproof nesting support for custom/semantic selectors:
41
- // sets the elevated background variables on all descendants.
42
- & * {
43
- --nested-card-bg: var(--bg-elevated);
44
- --nested-card-shadow: var(--shadow-floating);
45
- --nested-card-shadow-hover: var(--shadow-floating);
38
+ &:is(a, button):focus-visible,
39
+ &:has(> a:focus-visible) {
40
+ @include focus-ring;
46
41
  }
47
42
 
48
43
  // Direct-child selectors only — nested <header>/<main>/<footer>
@@ -81,37 +76,65 @@
81
76
  // without a <header> wrapper, the global h3 cascade (heading-sm)
82
77
  // is too large for tile context. Tighten to tile-title sizing.
83
78
  > main h3 {
84
- @include typography(body-sm);
79
+ @include typography(title-sm);
85
80
  @include font-semibold;
86
81
  color: inherit;
87
82
  margin: 0;
88
83
  }
89
84
  }
90
85
 
91
- @mixin panel-header {
86
+ // $group: true — title row of a transparent group: no inline padding, no
87
+ // divider, no surface (it sits on the canvas).
88
+ @mixin panel-header($group: false) {
92
89
  @include flex;
93
90
  @include items-center;
94
91
  @include justify-between;
95
- padding: var(--size-xs-up) var(--padding-x);
92
+ @if $group {
93
+ padding: var(--card-padding-y) 0;
94
+ } @else {
95
+ padding: var(--card-padding-y) var(--card-padding-x);
96
+ }
96
97
  min-height: 2.5rem;
97
- @include border-b;
98
- --color-bg: var(--bg-sunken);
99
- background: var(--color-bg);
98
+ @if $group {
99
+ background: transparent;
100
+ } @else {
101
+ @include border-b;
102
+ // Header shares the panel surface (inherited --color-bg) — a tinted
103
+ // caption bar reads as legacy window chrome. Themes may opt in to a
104
+ // tint via --panel-header-bg. Opaque, so sticky toolbars stay solid.
105
+ background: var(--panel-header-bg, var(--color-bg));
106
+ }
100
107
  @include transition;
101
108
  margin-bottom: 0;
102
109
 
103
110
  :is(h1, h2, h3, h4, h5, h6) {
104
- @include typography(body-sm);
105
- @include font-medium;
111
+ @include typography(title-sm);
112
+ @include font-semibold;
106
113
  color: var(--color-fg);
107
114
  margin: 0;
108
115
  }
109
116
 
117
+ // Title + description: <hgroup><h3>…</h3><p>…</p></hgroup>
118
+ hgroup {
119
+ @include flex-col;
120
+ gap: var(--size-2xs);
121
+ min-inline-size: 0;
122
+ margin: 0;
123
+
124
+ > p {
125
+ @include typography(caption);
126
+ color: var(--fg-muted);
127
+ margin: 0;
128
+ }
129
+ }
130
+
110
131
  label {
111
132
  --padding-y: var(--size-xs);
133
+ min-height: calc(var(--control-line-height) + 2 * var(--padding-y) + 2 * var(--border-width));
112
134
  }
113
135
 
114
- // Header actions compact to a 28px button. This is a DESCENDANT selector,
136
+ // Header actions lock to the control height: --control-line-height +
137
+ // 2 × --padding-y + 2 × border-width. This is a DESCENDANT selector,
115
138
  // so it also reaches a button that belongs to a nested field — that button
116
139
  // is the field's own chrome, not a header action, and form-field-group
117
140
  // stretches it to the field's full height. An explicit height beats
@@ -135,19 +158,9 @@
135
158
  // rule to (0,1,3) and silently outrank consumers that override it
136
159
  // at equal weight by source order.
137
160
  // Wrapped, the rule stays exactly (0,1,2); only its match set narrows.
138
- //
139
- // NOT written as [class*="group"]: that spelling means "field group" in
140
- // components/_form.scss only because it is scoped under .form-element there.
141
- // Unscoped it would also catch .btn-group and stop compacting a segmented
142
- // group in a header — a regression, and those buttons already outrank this.
143
161
  button:where(:not(:where([data-ln-date-field]) > button)) {
144
- --btn-padding-y: var(--size-2xs);
145
- --btn-padding-x: var(--size-xs);
146
- font-size: var(--text-xs);
147
- height: 1.75rem;
148
- display: inline-flex;
149
- align-items: center;
150
- justify-content: center;
162
+ --btn-padding-y: var(--size-xs);
163
+ min-height: calc(var(--control-line-height) + 2 * var(--padding-y) + 2 * var(--border-width));
151
164
  }
152
165
 
153
166
  // Close/dismiss and toggle buttons inside the header should be tight and flat.
@@ -157,24 +170,21 @@
157
170
  --btn-padding-y: var(--size-2xs);
158
171
  --btn-padding-x: var(--size-2xs);
159
172
  --shadow: none;
173
+ min-height: 0;
160
174
  height: 1.75rem;
161
175
  width: 1.75rem;
162
176
  }
163
177
  }
164
178
 
165
179
  @mixin panel-body {
166
- padding: var(--padding-y) var(--padding-x);
180
+ padding: var(--card-padding-y) var(--card-padding-x);
167
181
 
168
- &:has(> table),
169
- &:has(> .table-container) {
170
- overflow-x: auto;
171
- max-width: 100%;
172
- -webkit-overflow-scrolling: touch;
173
- }
182
+ // A card in a panel body sits on a surface — climb to elevated.
183
+ --nested-card-bg: var(--bg-elevated);
184
+ --nested-card-shadow: none;
174
185
 
175
- // ln-table brings its own chrome and sticky thead/footer — `clip` keeps
176
- // this cell from becoming the scroll container that would trap them.
177
- &:has(> [data-ln-table]) {
186
+ // `clip`, not `auto` — a scrolling cell would trap the sticky thead.
187
+ &:has(> :is(table, .table-container, [data-ln-table])) {
178
188
  overflow-x: clip;
179
189
  max-width: 100%;
180
190
  }
@@ -192,9 +202,7 @@
192
202
  // If main has mixed content (intro <p>, demo-split, <h4>,
193
203
  // then <table>), :only-child doesn't match, padding stays,
194
204
  // and the table keeps its own inset.
195
- &:has(> table:only-child),
196
- &:has(> .table-container:only-child),
197
- &:has(> [data-ln-table]:only-child) {
205
+ &:has(> :is(table, .table-container, [data-ln-table]):only-child) {
198
206
  padding: 0;
199
207
 
200
208
  > table,
@@ -208,10 +216,10 @@
208
216
  }
209
217
 
210
218
  @mixin panel-footer {
211
- padding: var(--padding-y) var(--padding-x);
219
+ padding: var(--card-padding-y) var(--card-padding-x);
212
220
  @include border-t;
213
- --color-bg: var(--bg-sunken);
214
- background: var(--color-bg);
221
+ // Same surface as the panel; opt-in tint via --panel-footer-bg.
222
+ background: var(--panel-footer-bg, var(--color-bg));
215
223
  @include flex;
216
224
  @include justify-end;
217
225
  gap: var(--gap);
@@ -254,7 +262,8 @@
254
262
  // Usage: #my-card { @include card; @include card-accent-top; }
255
263
 
256
264
  @mixin _card-accent-base {
257
- &:hover {
265
+ &:is(a, button):hover:not(:disabled),
266
+ &:has(> a:hover) {
258
267
  background-color: color-mix(in srgb, var(--color-accent) var(--tint-hover), transparent);
259
268
  }
260
269
  }
@@ -312,27 +321,35 @@
312
321
  }
313
322
  }
314
323
 
315
- @mixin section-card {
316
- background: var(--color-bg);
317
- @include border;
318
- border-radius: var(--radius);
319
- // `overflow: clip` not `overflow: hidden` — clips at rounded corners
320
- // without establishing a scroll container, so sticky descendants
321
- // (e.g. ln-table's <thead>) bind to the outer .app-main > section
322
- // scroll surface instead of being trapped inside this card.
323
- overflow: clip;
324
- @include transition;
325
- --shadow: var(--shadow-resting);
326
- box-shadow: var(--shadow);
327
- --margin-block: var(--size-md);
324
+ // $group: true — a transparent titled group: no surface, border, shadow or
325
+ // radius; content sits directly on the canvas.
326
+ @mixin section-card($group: false) {
327
+ @if not $group {
328
+ background: var(--color-bg);
329
+ @include border;
330
+ border-radius: var(--panel-radius, var(--radius));
331
+ // `overflow: clip` not `overflow: hidden` — clips at rounded corners
332
+ // without establishing a scroll container, so sticky descendants
333
+ // (e.g. ln-table's <thead>) bind to the outer .app-main > section
334
+ // scroll surface instead of being trapped inside this card.
335
+ overflow: clip;
336
+ @include transition;
337
+ --shadow: var(--shadow-resting);
338
+ box-shadow: var(--shadow);
339
+ }
340
+ --margin-block: var(--panel-margin-block, var(--size-md));
328
341
  margin-bottom: var(--margin-block);
329
342
 
330
343
  // Direct-child selectors only — nested <header>/<main>/<footer>
331
344
  // elements inside the card's content are consumer-owned and
332
345
  // must not inherit the card's own chrome styles.
333
- > header { @include panel-header; }
346
+ > header { @include panel-header($group); }
334
347
  > main { @include panel-body; }
335
348
  > footer { @include panel-footer; }
349
+
350
+ @if $group {
351
+ > main { padding: 0; }
352
+ }
336
353
  }
337
354
 
338
355
  // ─── Frosted Glass ──────────────────────────────────────
@@ -33,7 +33,8 @@
33
33
  // Re-declare accent vocabulary at this scope so var() resolves
34
34
  // here instead of staying frozen at an ancestor scope. Without this, descendants
35
35
  // reading --color-accent get the ancestor's primary blue regardless of the
36
- // semantic class. --color-accent-fg resolves to white from the derivation chain.
36
+ // semantic class. --color-accent-fg stays inherited — white in light
37
+ // polarity, ink in the dark lift.
37
38
  --color-accent: hsl(var(--color-primary));
38
39
  --color-accent-tint: hsl(var(--color-#{$family}-lighter));
39
40
  --color-accent-tint-strong: hsl(var(--color-#{$family}-light));
@@ -7,6 +7,18 @@
7
7
  @mixin hidden { display: none; }
8
8
  @mixin invisible { visibility: hidden; }
9
9
 
10
+ @mixin sr-only {
11
+ position: absolute;
12
+ width: 1px;
13
+ height: 1px;
14
+ padding: 0;
15
+ margin: -1px;
16
+ overflow: hidden;
17
+ clip: rect(0, 0, 0, 0);
18
+ white-space: nowrap;
19
+ border-width: 0;
20
+ }
21
+
10
22
  @mixin flex-col { display: flex; flex-direction: column; }
11
23
  @mixin flex-row { display: flex; flex-direction: row; }
12
24
  @mixin flex-wrap { flex-wrap: wrap; }
@@ -24,7 +24,9 @@
24
24
  --color-border: var(--border-subtle);
25
25
  padding: var(--padding-y) var(--padding-x);
26
26
  border-block-start: var(--border-block-start, var(--border-width) solid var(--color-border));
27
- background: var(--color-bg);
27
+ // Sits on the surrounding canvas — an opaque white strip pinned to the
28
+ // bottom edge reads as a desktop status bar. Opt in via --app-footer-bg.
29
+ background: var(--app-footer-bg, transparent);
28
30
  @include typography(body-sm);
29
31
  color: var(--color-fg);
30
32
  @include flex;
@@ -17,19 +17,18 @@
17
17
 
18
18
  @mixin form-label {
19
19
  @include block;
20
- @include typography(label-sm);
20
+ // label-md / medium — a 12px semibold caption over every field reads
21
+ // as dialog-box chrome; field labels sit one step under body text.
22
+ @include typography(label-md);
21
23
  @include font-medium;
22
24
  color: var(--color-fg);
23
25
  // Structural label-to-input gap (not shell rhythm).
24
26
  --margin-block: 0.375rem;
25
27
  margin-bottom: var(--margin-block);
26
28
 
27
- // Required indicator — red * after label text.
28
- // Supports standard wrapping classes/tags (.form-element, .form-group, label)
29
- // and general siblings preceding a required input.
30
- :is(.form-element, .form-group, .form-field):has([required]) > &,
31
- label:has([required]) &,
32
- &:has(~ :is(input, select, textarea)[required]) {
29
+ // Required indicator — red * when any control under the label's parent is required
30
+ // (siblings, nested field shells).
31
+ :has([required]) > & {
33
32
  &::after {
34
33
  content: ' *';
35
34
  color: hsl(var(--color-error));
@@ -38,6 +37,21 @@
38
37
  }
39
38
  }
40
39
 
40
+ // Field wrapper — zero-specificity rules so form-grid's `> * { margin: 0 }` wins.
41
+ @mixin form-element {
42
+ // Stacked outside a form-grid (modal bodies, settings panels).
43
+ :where(&) + :where(&) {
44
+ --margin-block: var(--size-md);
45
+ margin-top: var(--margin-block);
46
+ }
47
+
48
+ // Trailing feedback after the control gets slot treatment.
49
+ :where(& > :is(input, select, textarea, [data-ln-date-field], search) ~ :not(input)) {
50
+ @include form-slot;
51
+ color: var(--fg-muted);
52
+ }
53
+ }
54
+
41
55
  @mixin form-grid {
42
56
  --gap: var(--size-md);
43
57
  container-type: inline-size;
@@ -83,28 +97,42 @@
83
97
 
84
98
  @mixin _form-focus-style {
85
99
  border-color: var(--color-accent);
86
- @include focus-background-shift;
100
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 20%, transparent);
87
101
  }
88
102
 
89
103
  // ─── Text inputs, textareas, selects ────────────────────────────
90
104
 
91
105
  @mixin form-input {
92
106
  @include w-full;
93
- --padding-y: var(--input-padding-y, var(--size-sm));
94
- --padding-x: var(--size-md);
107
+ --padding-y: var(--input-padding-y, var(--size-xs-up));
108
+ --padding-x: var(--size-sm-up);
95
109
  padding: var(--padding-y) var(--padding-x);
96
110
  font-size: var(--font-size);
97
- line-height: var(--line-height);
111
+ // Shared control line box — same height as a sibling button.
112
+ line-height: var(--control-line-height);
98
113
  color: var(--color-fg);
99
- --color-bg: var(--bg-sunken);
114
+ // Enabled fields read as writable surfaces: base fill, hairline shadow.
115
+ // A grey fill is reserved for :disabled.
116
+ --color-bg: var(--input-bg, var(--bg-base));
100
117
  background: var(--color-bg);
101
118
  // Control boundary, not a divider — 1.4.11 wants 3:1 here.
102
- --color-border: var(--border-ui);
119
+ --color-border: var(--input-border, var(--border-ui));
103
120
  @include border;
104
121
  border-radius: var(--radius);
105
- @include transition;
122
+ box-shadow: var(--input-shadow, var(--shadow-xs));
123
+ // Colour + ring only — `all` would animate padding/font-size when the
124
+ // density tier switches, making fields visibly "breathe".
125
+ transition:
126
+ color var(--transition-base),
127
+ background-color var(--transition-base),
128
+ border-color var(--transition-base),
129
+ box-shadow var(--transition-base);
106
130
  outline: none;
107
131
 
132
+ &:hover:not(:disabled):not(:focus-visible) {
133
+ --color-border: var(--input-border-hover, var(--border-strong-hover));
134
+ }
135
+
108
136
  &:focus-visible {
109
137
  @include _form-focus-style;
110
138
  }
@@ -119,6 +147,7 @@
119
147
  @include cursor-not-allowed;
120
148
  --color-bg: var(--bg-sunken);
121
149
  background: var(--color-bg);
150
+ box-shadow: none;
122
151
  }
123
152
 
124
153
  // Hide native UA clear button on <input type="search"> — we never use it;
@@ -130,6 +159,8 @@
130
159
  }
131
160
 
132
161
  @mixin form-textarea {
162
+ // Multi-line text keeps the reading line-height, not the control box.
163
+ line-height: var(--line-height);
133
164
  min-height: 6rem;
134
165
  resize: vertical;
135
166
 
@@ -205,9 +236,7 @@
205
236
  }
206
237
 
207
238
  // Propagate disabled states to the parent visual shell
208
- &:has(> input:disabled),
209
- &:has(> select:disabled),
210
- &:has(> button:disabled) {
239
+ &:has(> :is(input, select, button):disabled) {
211
240
  @include opacity-50;
212
241
  @include cursor-not-allowed;
213
242
  --color-bg: var(--bg-sunken);
@@ -442,12 +471,13 @@
442
471
  @include flex;
443
472
  @include items-center;
444
473
  gap: var(--gap);
445
- --padding-y: var(--input-padding-y, var(--size-sm));
474
+ --padding-y: var(--input-padding-y, var(--size-xs-up));
446
475
  --padding-x: var(--size-md);
447
476
  padding: var(--padding-y) var(--padding-x);
448
477
  @include cursor-pointer;
449
478
  @include transition;
450
479
  @include text-sm;
480
+ line-height: var(--control-line-height);
451
481
  @include font-medium;
452
482
  margin-bottom: 0;
453
483
  border-radius: var(--radius);
@@ -461,16 +491,16 @@
461
491
  // pill's `display: none` when pill-outline is applied as an override.
462
492
  > input { display: revert; }
463
493
 
464
- &:has(> input:checked) {
494
+ &:hover {
465
495
  border-color: var(--color-accent);
466
- color: var(--color-accent);
467
- // Reset pill's accent fill on checked — outline stays transparent.
468
- background: transparent;
469
496
  z-index: 1;
470
497
  }
471
498
 
472
- &:hover:not(:has(> input:checked)) {
499
+ &:has(> input:checked) {
473
500
  border-color: var(--color-accent);
501
+ color: var(--color-accent);
502
+ // Reset pill's accent fill on checked — outline stays transparent.
503
+ background: transparent;
474
504
  z-index: 1;
475
505
  }
476
506
  }
@@ -484,24 +514,24 @@
484
514
  // Lets .error/.success/.warning/.info parents cascade to the checked-
485
515
  // state hover via the .success { --color-primary } chain. See
486
516
  // _btn.scss for the syntax rationale (number form, not percentage).
487
- --color-accent-hover: hsl(from var(--color-accent) h s calc(l - 8));
517
+ --color-accent-hover: hsl(from var(--color-accent) h s calc(l + var(--accent-hover-shift)));
488
518
 
489
519
  background-color: var(--color-bg);
490
520
  border-color: transparent;
491
521
 
492
522
  > input { display: none; }
493
523
 
524
+ &:hover {
525
+ border-color: var(--color-border);
526
+ }
527
+
494
528
  &:has(> input:checked) {
495
529
  background-color: var(--color-accent);
496
530
  color: var(--color-accent-fg);
497
- }
498
531
 
499
- &:hover:not(:has(> input:checked)) {
500
- border-color: var(--color-border);
501
- }
502
-
503
- &:hover:has(> input:checked) {
504
- background-color: var(--color-accent-hover);
532
+ &:hover {
533
+ background-color: var(--color-accent-hover);
534
+ }
505
535
  }
506
536
  }
507
537
 
@@ -549,24 +579,20 @@
549
579
  @mixin pills {
550
580
  @include inline-flex;
551
581
 
552
- > li > label:has(> input[type="checkbox"]),
553
- > li > label:has(> input[type="radio"]) {
582
+ > li > label:has(> input:is([type="checkbox"], [type="radio"])) {
554
583
  @include pill;
555
584
  border-radius: 0;
556
585
  }
557
586
 
558
- > li:first-child > label:has(> input[type="checkbox"]),
559
- > li:first-child > label:has(> input[type="radio"]) {
587
+ > li:first-child > label:has(> input:is([type="checkbox"], [type="radio"])) {
560
588
  border-radius: var(--radius) 0 0 var(--radius);
561
589
  }
562
590
 
563
- > li:last-child > label:has(> input[type="checkbox"]),
564
- > li:last-child > label:has(> input[type="radio"]) {
591
+ > li:last-child > label:has(> input:is([type="checkbox"], [type="radio"])) {
565
592
  border-radius: 0 var(--radius) var(--radius) 0;
566
593
  }
567
594
 
568
- > li:only-child > label:has(> input[type="checkbox"]),
569
- > li:only-child > label:has(> input[type="radio"]) {
595
+ > li:only-child > label:has(> input:is([type="checkbox"], [type="radio"])) {
570
596
  border-radius: var(--radius);
571
597
  }
572
598
  }
@@ -587,24 +613,20 @@
587
613
  }
588
614
  }
589
615
 
590
- > li > label:has(> input[type="checkbox"]),
591
- > li > label:has(> input[type="radio"]) {
616
+ > li > label:has(> input:is([type="checkbox"], [type="radio"])) {
592
617
  @include pill-outline;
593
618
  border-radius: 0;
594
619
  }
595
620
 
596
- > li:first-child > label:has(> input[type="checkbox"]),
597
- > li:first-child > label:has(> input[type="radio"]) {
621
+ > li:first-child > label:has(> input:is([type="checkbox"], [type="radio"])) {
598
622
  border-radius: var(--radius) 0 0 var(--radius);
599
623
  }
600
624
 
601
- > li:last-child > label:has(> input[type="checkbox"]),
602
- > li:last-child > label:has(> input[type="radio"]) {
625
+ > li:last-child > label:has(> input:is([type="checkbox"], [type="radio"])) {
603
626
  border-radius: 0 var(--radius) var(--radius) 0;
604
627
  }
605
628
 
606
- > li:only-child > label:has(> input[type="checkbox"]),
607
- > li:only-child > label:has(> input[type="radio"]) {
629
+ > li:only-child > label:has(> input:is([type="checkbox"], [type="radio"])) {
608
630
  border-radius: var(--radius);
609
631
  }
610
632
  }
@@ -628,8 +650,7 @@
628
650
  display: block;
629
651
  }
630
652
 
631
- > li > label:has(> input[type="checkbox"]),
632
- > li > label:has(> input[type="radio"]) {
653
+ > li > label:has(> input:is([type="checkbox"], [type="radio"])) {
633
654
  @include flex;
634
655
  @include items-center;
635
656
  @include justify-center;
@@ -799,8 +820,7 @@
799
820
  --padding-y: var(--size-xs-up);
800
821
  padding: var(--padding-y) var(--padding-x);
801
822
 
802
- // Hover (unchecked) — subtle accent wash (nav hover).
803
- &:hover:not(:has(> input:checked)) {
823
+ &:hover {
804
824
  background-color: color-mix(in srgb, var(--color-accent) var(--tint-hover), transparent);
805
825
  }
806
826
 
@@ -809,11 +829,10 @@
809
829
  background-color: var(--color-accent-tint-strong);
810
830
  color: var(--color-accent);
811
831
  @include font-semibold;
812
- }
813
832
 
814
- // Selected + hover — keep the wash, don't darken (nav has no darken).
815
- &:hover:has(> input:checked) {
816
- background-color: var(--color-accent-tint-strong);
833
+ &:hover {
834
+ background-color: var(--color-accent-tint-strong);
835
+ }
817
836
  }
818
837
  }
819
838
  }
@@ -4,6 +4,14 @@
4
4
 
5
5
  // Layout — grid, stack, container
6
6
 
7
+ // Respond to available parent width, including sidebars and panel gutters.
8
+ // The min() allows a single item to shrink below its preferred minimum.
9
+ @mixin grid-auto($min: var(--grid-item-min-width)) {
10
+ display: grid;
11
+ gap: var(--gap);
12
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, #{$min}), 1fr));
13
+ }
14
+
7
15
  @mixin _grid-base {
8
16
  display: grid;
9
17
  --gap: var(--size-lg);
@@ -110,6 +118,7 @@
110
118
 
111
119
  > * {
112
120
  --radius: 0;
121
+ --panel-radius: 0;
113
122
  --shadow: none;
114
123
  }
115
124