@livenetworks/ashlar 2.1.13 → 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 (101) hide show
  1. package/components/COMPONENTS.md +80 -80
  2. package/components/ln-api-connector/ln-api-connector.schema.json +105 -105
  3. package/components/ln-chart/ln-chart-dev.scss +0 -4
  4. package/components/ln-core/README.md +21 -21
  5. package/components/ln-core/ln-core.schema.json +120 -120
  6. package/components/ln-core/query.js +113 -113
  7. package/components/ln-data-coordinator/README.md +2 -0
  8. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -1
  9. package/components/ln-data-coordinator/ln-data-coordinator.schema.json +196 -196
  10. package/components/ln-data-coordinator/src/ln-data-coordinator.js +457 -432
  11. package/components/ln-data-store/ln-data-store.schema.json +92 -92
  12. package/components/ln-date/ln-date.js +1 -1
  13. package/components/ln-date/src/ln-date.js +1 -1
  14. package/components/ln-debug/ln-debug.js +1 -1
  15. package/components/ln-debug/ln-debug.schema.json +28 -28
  16. package/components/ln-debug/src/generated-attributes.js +304 -302
  17. package/components/ln-dropdown/ln-dropdown.schema.json +90 -90
  18. package/components/ln-external-links/README.md +19 -19
  19. package/components/ln-external-links/ln-external-links.js +1 -1
  20. package/components/ln-external-links/ln-external-links.schema.json +9 -0
  21. package/components/ln-external-links/ln-external-links.scss +12 -0
  22. package/components/ln-external-links/src/ln-external-links.js +3 -2
  23. package/components/ln-filter/README.md +2 -1
  24. package/components/ln-filter/ln-filter.js +1 -1
  25. package/components/ln-filter/ln-filter.schema.json +157 -169
  26. package/components/ln-filter/src/ln-filter.js +1 -3
  27. package/components/ln-form/ln-form.schema.json +57 -57
  28. package/components/ln-key/ln-key.js +1 -1
  29. package/components/ln-key/ln-key.schema.json +8 -0
  30. package/components/ln-key/src/ln-key.js +8 -5
  31. package/components/ln-obfuscator/ln-obfuscator.js +1 -1
  32. package/components/ln-obfuscator/ln-obfuscator.schema.json +6 -0
  33. package/components/ln-obfuscator/src/ln-obfuscator.js +2 -2
  34. package/components/ln-options/ln-options.schema.json +47 -47
  35. package/components/ln-popover/ln-popover.schema.json +82 -82
  36. package/components/ln-progress/ln-progress.schema.json +0 -13
  37. package/components/ln-search/ln-search.schema.json +111 -111
  38. package/components/ln-sort/README.md +1 -0
  39. package/components/ln-sort/ln-sort.js +1 -1
  40. package/components/ln-sort/ln-sort.schema.json +100 -112
  41. package/components/ln-sort/src/sort-model.js +1 -6
  42. package/components/ln-sortable/ln-sortable.schema.json +38 -38
  43. package/components/ln-sortable/ln-sortable.scss +25 -25
  44. package/components/ln-table/README.md +8 -1
  45. package/components/ln-table/ln-table.js +1 -1
  46. package/components/ln-table/ln-table.schema.json +195 -195
  47. package/components/ln-table/src/ln-table.js +43 -7
  48. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -1
  49. package/components/ln-table-coordinator/ln-table-coordinator.schema.json +18 -0
  50. package/components/ln-table-coordinator/src/ln-table-coordinator.js +104 -2
  51. package/components/ln-toast/README.md +1 -1
  52. package/components/ln-toast/ln-toast.js +1 -1
  53. package/components/ln-toast/ln-toast.schema.json +6 -0
  54. package/components/ln-toast/src/ln-toast.js +1 -1
  55. package/components/ln-validate/README.md +6 -6
  56. package/components/ln-validate/ln-validate-dev.scss +0 -5
  57. package/components/ln-validate/ln-validate.js +1 -1
  58. package/components/ln-validate/ln-validate.schema.json +0 -6
  59. package/components/ln-validate/src/ln-validate.js +48 -29
  60. package/ln-ashlar.html-data.json +1824 -1814
  61. package/package.json +95 -95
  62. package/theme/README.md +7 -6
  63. package/theme/base/_global.scss +2 -2
  64. package/theme/base/_reset.scss +11 -0
  65. package/theme/components/_app-shell.scss +0 -8
  66. package/theme/components/_form.scss +6 -6
  67. package/theme/components/_ln-table.scss +6 -3
  68. package/theme/components/_scrollbar.scss +5 -4
  69. package/theme/components/_sortable.scss +19 -19
  70. package/theme/components/_stat-card.scss +1 -0
  71. package/theme/components/_table.scss +10 -33
  72. package/theme/config/_density.scss +236 -104
  73. package/theme/config/_palette.scss +71 -49
  74. package/theme/config/_theme.scss +27 -31
  75. package/theme/config/_tokens.scss +28 -26
  76. package/theme/config/mixins/_app-shell.scss +17 -21
  77. package/theme/config/mixins/_btn.scss +4 -2
  78. package/theme/config/mixins/_card.scss +87 -67
  79. package/theme/config/mixins/_colors.scss +2 -1
  80. package/theme/config/mixins/_display.scss +12 -0
  81. package/theme/config/mixins/_footer.scss +3 -1
  82. package/theme/config/mixins/_form.scss +48 -16
  83. package/theme/config/mixins/_layout.scss +9 -0
  84. package/theme/config/mixins/_ln-table.scss +22 -15
  85. package/theme/config/mixins/_modal.scss +3 -0
  86. package/theme/config/mixins/_nav.scss +27 -4
  87. package/theme/config/mixins/_sidebar.scss +2 -1
  88. package/theme/config/mixins/_stat-card.scss +33 -2
  89. package/theme/config/mixins/_status-badge.scss +1 -0
  90. package/theme/config/mixins/_table.scss +81 -39
  91. package/theme/config/mixins/_tabs.scss +31 -0
  92. package/theme/config/mixins/_toast.scss +0 -1
  93. package/theme/designs/relaxed/_card.scss +12 -0
  94. package/theme/designs/relaxed/_layout.scss +24 -0
  95. package/theme/ln-ashlar-core.scss +1 -0
  96. package/theme/ln-ashlar-dev.scss +0 -1
  97. package/theme/ln-ashlar-relaxed.scss +67 -0
  98. package/theme/tailwind/components.css +561 -24
  99. package/theme/utilities/_utilities.scss +1 -11
  100. package/web-types.json +2351 -2337
  101. package/components/ln-progress/ln-progress-dev.scss +0 -8
@@ -1,5 +1,3 @@
1
- @use 'mixins/breakpoints' as *;
2
-
3
1
  // ==========================================================================
4
2
  // Design Tokens — ln-ashlar
5
3
  // ==========================================================================
@@ -53,23 +51,24 @@
53
51
  --max-w-form: 32rem;
54
52
  --max-w-content: 48rem;
55
53
  --max-w-container: 80rem;
54
+ --grid-item-min-width: 14rem; // intrinsic minimum for auto-fit card grids
56
55
 
57
56
  // -----------------------------------------------------------------------
58
57
  // Border Radius
59
58
  // -----------------------------------------------------------------------
60
59
  --radius-none: 0;
61
- --radius-xs: 0.125rem; // 2px
62
- --radius-sm: 0.25rem; // 4px
63
- --radius-md: 0.375rem; // 6px
64
- --radius-lg: 0.5rem; // 8px
65
- --radius-xl: 0.75rem; // 12px
66
- --radius-2xl: 1rem; // 16px
60
+ --radius-xs: 0.25rem; // 4px
61
+ --radius-sm: 0.375rem; // 6px
62
+ --radius-md: 0.625rem; // 10px — controls (--radius)
63
+ --radius-lg: 1rem; // 16px — panels, modals
64
+ --radius-xl: 1.375rem; // 22px
65
+ --radius-2xl: 1.75rem; // 28px
67
66
  --radius-full: 9999px;
68
67
 
69
68
  // -----------------------------------------------------------------------
70
69
  // Transitions — durations
71
70
  // -----------------------------------------------------------------------
72
- --transition-base: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
71
+ --transition-base: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
73
72
  --transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1);
74
73
  --transition-slow: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
75
74
 
@@ -85,7 +84,9 @@
85
84
  // -----------------------------------------------------------------------
86
85
  // Typography
87
86
  // -----------------------------------------------------------------------
88
- --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
87
+ // Plus Jakarta Sans is not bundled — the page loads it, or the stack
88
+ // falls back to Inter / system UI.
89
+ --font-sans: 'Plus Jakarta Sans', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
89
90
  --font-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace;
90
91
  --font-features: "cv02", "cv03", "cv04", "cv11", "tnum";
91
92
 
@@ -214,6 +215,21 @@
214
215
  --btn-padding-x: var(--size-sm-up);
215
216
  --btn-padding-y: var(--size-xs-up);
216
217
 
218
+ // Control size contract — buttons, text inputs, selects and pills share
219
+ // one line box and one vertical padding per density tier:
220
+ // height = --control-line-height + 2 × padding-y + 2 × border
221
+ // compact 22 + 2×6 + 2 = 36px · comfortable 44px · spacious 54px
222
+ // Density rebinds all three in _density.scss.
223
+ --control-line-height: 1.375rem; // 22px
224
+ --input-padding-y: var(--btn-padding-y);
225
+
226
+ // Card and table padding — structural rhythm, density-reactive via
227
+ // _density.scss (compact values here).
228
+ --card-padding-y: var(--size-sm-up); // 12px
229
+ --card-padding-x: var(--size-md); // 16px
230
+ --table-cell-padding-y: var(--size-sm); // 8px
231
+ --table-cell-padding-x: var(--size-md); // 16px
232
+
217
233
  // -- Typography — primitives -----------------------------------------
218
234
  // Mixins read these two primitives ONLY (via @include typography(role),
219
235
  // which rebinds them per role on the consuming element's scope — mirror
@@ -267,19 +283,5 @@
267
283
  // spacing — that is --gap.
268
284
  }
269
285
 
270
- // ---------------------------------------------------------------------------
271
- // Mobile rhythm — logical tokens tighten at ≤ md viewport.
272
- // Not the same as `[data-density]` (that's a user opt-in).
273
- // This is viewport-driven structural compression: content dominates chrome
274
- // at narrow widths. Components reading --padding-x/--padding-y/--gap
275
- // react via the cascade.
276
- //
277
- // Interaction with [data-density]: stacks. Compact density + mobile viewport =
278
- // both apply; the underlying --padding-y tightens further from the mobile rebind.
279
- // ---------------------------------------------------------------------------
280
- @include mq-down(md) {
281
- :root {
282
- --padding-y: var(--size-xs); // 6px → 4px
283
- --gap: var(--size-xs-up); // 6px → 6px (already tight)
284
- }
285
- }
286
+ // Mobile rhythm (viewport-driven tightening) lives at the end of
287
+ // _density.scss so it stacks on top of every density tier.
@@ -125,13 +125,13 @@
125
125
 
126
126
  // Equalize height across icon-only and text-only siblings AND
127
127
  // search-icon labels. Geometric — locks the border-box chrome to
128
- // the icon-content height (1.25rem) plus rebound --padding-y and
128
+ // the shared control line box (--control-line-height) plus rebound --padding-y and
129
129
  // border-width on both edges. Buttons rebind --padding-y below;
130
130
  // search labels read --padding-y from @mixin form-input-icon-group
131
131
  // (same var(--size-xs) value), so the formula resolves identically
132
132
  // for both. Border-box is the project default (scss/base/_reset.scss).
133
133
  :is(button, label) {
134
- min-height: calc(1.25rem + 2 * var(--padding-y) + 2 * var(--border-width));
134
+ min-height: calc(var(--control-line-height) + 2 * var(--padding-y) + 2 * var(--border-width));
135
135
  }
136
136
 
137
137
  button {
@@ -190,20 +190,12 @@
190
190
  height: calc(100dvh - var(--app-header-height));
191
191
  overflow: hidden;
192
192
 
193
- // When the sidebar drawer is open on desktop, reserve its space
194
- // on THIS element (the parent) so both content and footer shift
195
- // together. Padding on the parent keeps child widths at 100% and
196
- // avoids the horizontal overflow caused by margin-left on
197
- // width:100% children.
198
- &:has(.app-sidebar[data-ln-toggle="open"]) {
199
- padding-left: var(--app-sidebar-width);
200
- }
201
-
202
- // On mobile the sidebar overlays (scrim covers content) —
203
- // no content shift.
204
- @include mq-down(md) {
205
- &:has(.app-sidebar[data-ln-toggle="open"]) {
206
- padding-left: 0;
193
+ // Open drawer on desktop: reserve its width on the parent so content
194
+ // and footer shift together (padding, not child margin — no overflow).
195
+ // On mobile the drawer overlays the content instead.
196
+ @include mq-up(md) {
197
+ &:has(> aside[data-ln-toggle="open"]) {
198
+ padding-left: var(--app-sidebar-width);
207
199
  }
208
200
  }
209
201
 
@@ -222,10 +214,13 @@
222
214
  // `> *:first-child` / `> *:last-child` margins below — those
223
215
  // margins scroll past with the content, leaving sticky to pin
224
216
  // against the actual section edges.
225
- padding-inline: var(--padding-x);
217
+ padding-inline: clamp(var(--size-md), 3vw, var(--size-xl));
226
218
  @include overflow-auto;
227
219
  @include w-full;
228
- @include stack(var(--gap));
220
+ @include stack(var(--size-lg));
221
+ // The stack gap owns vertical rhythm here — section-cards drop
222
+ // their own bottom margin so the two do not add up.
223
+ --panel-margin-block: 0;
229
224
  @include transition;
230
225
 
231
226
  // Children keep intrinsic height — wrapper's overflow-auto
@@ -243,11 +238,11 @@
243
238
  // descendants (sticky top:0 toolbar/thead, sticky bottom:0 footer)
244
239
  // to the inside of the padding edge.
245
240
  > *:first-child {
246
- margin-block-start: var(--padding-x);
241
+ margin-block-start: clamp(var(--size-md), 3vw, var(--size-xl));
247
242
  }
248
243
 
249
244
  > *:last-child {
250
- margin-block-end: var(--padding-x);
245
+ margin-block-end: clamp(var(--size-md), 3vw, var(--size-xl));
251
246
  }
252
247
  }
253
248
  }
@@ -309,6 +304,7 @@
309
304
  max-width: var(--max-w-container);
310
305
  @include mx(auto);
311
306
  @include w-full;
312
- @include stack(var(--gap));
307
+ @include stack(var(--size-lg));
308
+ --panel-margin-block: 0;
313
309
  }
314
310
 
@@ -37,6 +37,8 @@
37
37
  --padding-x: var(--btn-padding-x, var(--size-md));
38
38
  padding: var(--padding-y) var(--padding-x);
39
39
  @include text-sm;
40
+ // Shared control line box — aligns with inputs/selects (see _tokens.scss).
41
+ line-height: var(--control-line-height);
40
42
  @include font-medium;
41
43
  border-radius: var(--radius);
42
44
  white-space: nowrap;
@@ -69,14 +71,14 @@
69
71
  @mixin btn {
70
72
  // Accent variant — rebinds the --btn-* token surface to accent values.
71
73
  --color-accent: hsl(var(--color-primary));
72
- --color-accent-hover: hsl(from var(--color-accent) h s calc(l - 6));
74
+ --color-accent-hover: hsl(from var(--color-accent) h s calc(l + var(--accent-hover-shift)));
73
75
 
74
76
  --btn-bg: var(--color-accent-bg, var(--color-accent));
75
77
  --btn-fg: var(--color-accent-bg-fg, var(--color-accent-fg));
76
78
  --btn-border: hsl(from var(--color-accent) h s calc(l - 4));
77
79
  --btn-bg-hover: var(--color-accent-bg-hover, var(--color-accent-hover));
78
80
  --btn-fg-hover: var(--color-accent-bg-fg, var(--color-accent-fg));
79
- --btn-border-hover: hsl(from var(--color-accent) h s calc(l - 6));
81
+ --btn-border-hover: hsl(from var(--color-accent) h s calc(l + var(--accent-hover-shift)));
80
82
  }
81
83
 
82
84
  @mixin btn-soft {
@@ -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,23 +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(> :is(table, .table-container)) {
169
- overflow-x: auto;
170
- max-width: 100%;
171
- -webkit-overflow-scrolling: touch;
172
- }
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;
173
185
 
174
- // ln-table brings its own chrome and sticky thead/footer — `clip` keeps
175
- // this cell from becoming the scroll container that would trap them.
176
- &: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])) {
177
188
  overflow-x: clip;
178
189
  max-width: 100%;
179
190
  }
@@ -205,10 +216,10 @@
205
216
  }
206
217
 
207
218
  @mixin panel-footer {
208
- padding: var(--padding-y) var(--padding-x);
219
+ padding: var(--card-padding-y) var(--card-padding-x);
209
220
  @include border-t;
210
- --color-bg: var(--bg-sunken);
211
- 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));
212
223
  @include flex;
213
224
  @include justify-end;
214
225
  gap: var(--gap);
@@ -251,7 +262,8 @@
251
262
  // Usage: #my-card { @include card; @include card-accent-top; }
252
263
 
253
264
  @mixin _card-accent-base {
254
- &:hover {
265
+ &:is(a, button):hover:not(:disabled),
266
+ &:has(> a:hover) {
255
267
  background-color: color-mix(in srgb, var(--color-accent) var(--tint-hover), transparent);
256
268
  }
257
269
  }
@@ -309,27 +321,35 @@
309
321
  }
310
322
  }
311
323
 
312
- @mixin section-card {
313
- background: var(--color-bg);
314
- @include border;
315
- border-radius: var(--radius);
316
- // `overflow: clip` not `overflow: hidden` — clips at rounded corners
317
- // without establishing a scroll container, so sticky descendants
318
- // (e.g. ln-table's <thead>) bind to the outer .app-main > section
319
- // scroll surface instead of being trapped inside this card.
320
- overflow: clip;
321
- @include transition;
322
- --shadow: var(--shadow-resting);
323
- box-shadow: var(--shadow);
324
- --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));
325
341
  margin-bottom: var(--margin-block);
326
342
 
327
343
  // Direct-child selectors only — nested <header>/<main>/<footer>
328
344
  // elements inside the card's content are consumer-owned and
329
345
  // must not inherit the card's own chrome styles.
330
- > header { @include panel-header; }
346
+ > header { @include panel-header($group); }
331
347
  > main { @include panel-body; }
332
348
  > footer { @include panel-footer; }
349
+
350
+ @if $group {
351
+ > main { padding: 0; }
352
+ }
333
353
  }
334
354
 
335
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
 
@@ -440,12 +471,13 @@
440
471
  @include flex;
441
472
  @include items-center;
442
473
  gap: var(--gap);
443
- --padding-y: var(--input-padding-y, var(--size-sm));
474
+ --padding-y: var(--input-padding-y, var(--size-xs-up));
444
475
  --padding-x: var(--size-md);
445
476
  padding: var(--padding-y) var(--padding-x);
446
477
  @include cursor-pointer;
447
478
  @include transition;
448
479
  @include text-sm;
480
+ line-height: var(--control-line-height);
449
481
  @include font-medium;
450
482
  margin-bottom: 0;
451
483
  border-radius: var(--radius);
@@ -482,7 +514,7 @@
482
514
  // Lets .error/.success/.warning/.info parents cascade to the checked-
483
515
  // state hover via the .success { --color-primary } chain. See
484
516
  // _btn.scss for the syntax rationale (number form, not percentage).
485
- --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)));
486
518
 
487
519
  background-color: var(--color-bg);
488
520
  border-color: transparent;
@@ -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