@ponchia/ui 0.3.0 → 0.3.2

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/css/forms.css CHANGED
@@ -92,6 +92,108 @@
92
92
  color: var(--danger);
93
93
  }
94
94
 
95
+ /* --- Input group: prefix/suffix affix around a control --- */
96
+ .ui-input-group {
97
+ display: flex;
98
+ isolation: isolate;
99
+ }
100
+
101
+ .ui-input-group > .ui-input,
102
+ .ui-input-group > .ui-select {
103
+ border-radius: 0;
104
+ flex: 1;
105
+ margin-inline-start: -1px;
106
+ }
107
+
108
+ .ui-input-group > :first-child {
109
+ border-start-start-radius: var(--radius-md);
110
+ border-end-start-radius: var(--radius-md);
111
+ margin-inline-start: 0;
112
+ }
113
+
114
+ .ui-input-group > :last-child {
115
+ border-start-end-radius: var(--radius-md);
116
+ border-end-end-radius: var(--radius-md);
117
+ }
118
+
119
+ .ui-input-group > .ui-input:focus-visible {
120
+ z-index: 1; /* keep the focus ring above the adjacent addon border */
121
+ }
122
+
123
+ .ui-input-group__addon {
124
+ align-items: center;
125
+ background: var(--panel-soft);
126
+ border: 1px solid var(--line);
127
+ color: var(--text-dim);
128
+ display: flex;
129
+ font-size: var(--text-sm);
130
+ padding-inline: var(--space-sm);
131
+ white-space: nowrap;
132
+ }
133
+
134
+ /* --- File input --- */
135
+ .ui-file {
136
+ color: var(--text-soft);
137
+ font-size: var(--text-sm);
138
+ }
139
+
140
+ .ui-file::file-selector-button {
141
+ background: var(--panel-soft);
142
+ border: 1px solid var(--line-strong);
143
+ border-radius: var(--radius-md);
144
+ color: var(--text);
145
+ cursor: pointer;
146
+ font: inherit;
147
+ margin-inline-end: var(--space-sm);
148
+ padding: 0.5rem 0.85rem;
149
+ }
150
+
151
+ .ui-file::file-selector-button:hover {
152
+ border-color: var(--accent);
153
+ }
154
+
155
+ /* --- Range --- */
156
+ .ui-range {
157
+ accent-color: var(--accent);
158
+ inline-size: 100%;
159
+ }
160
+
161
+ .ui-range:focus-visible {
162
+ outline: 2px solid var(--focus-ring);
163
+ outline-offset: 4px;
164
+ }
165
+
166
+ /* --- Error summary: one focusable, assertive list of form errors --- */
167
+ .ui-error-summary {
168
+ border: 1px solid var(--danger);
169
+ border-radius: var(--radius-md);
170
+ color: var(--text);
171
+ padding: var(--space-md);
172
+ }
173
+
174
+ .ui-error-summary[hidden] {
175
+ display: none;
176
+ }
177
+
178
+ .ui-error-summary__title {
179
+ color: var(--danger);
180
+ font-family: var(--display);
181
+ font-size: var(--text-sm);
182
+ font-weight: 700;
183
+ letter-spacing: var(--tracking-wide);
184
+ margin: 0 0 var(--space-xs);
185
+ text-transform: uppercase;
186
+ }
187
+
188
+ .ui-error-summary__list {
189
+ margin: 0;
190
+ padding-inline-start: var(--space-md);
191
+ }
192
+
193
+ .ui-error-summary__list a {
194
+ color: var(--danger);
195
+ }
196
+
95
197
  /* Search field with leading dot. */
96
198
  .ui-search {
97
199
  align-items: center;
@@ -187,6 +289,30 @@
187
289
  outline-offset: 2px;
188
290
  }
189
291
 
292
+ /* ARIA contract: `.ui-switch` is a visually-styled checkbox. Author
293
+ `role="switch"` on the `<input>` so AT announces "switch, on/off"
294
+ rather than "checkbox, checked" — the native `checked` state then
295
+ drives `aria-checked` automatically, so no JS is required. In
296
+ forced-colors mode the colour cues are dropped by the OS, so convey
297
+ on/off with system-colour border + thumb instead. */
298
+ @media (forced-colors: active) {
299
+ .ui-switch__track {
300
+ border-color: ButtonBorder;
301
+ }
302
+
303
+ .ui-switch__thumb {
304
+ background: ButtonText;
305
+ }
306
+
307
+ .ui-switch input:checked + .ui-switch__track {
308
+ border-color: Highlight;
309
+ }
310
+
311
+ .ui-switch input:checked + .ui-switch__track .ui-switch__thumb {
312
+ background: Highlight;
313
+ }
314
+ }
315
+
190
316
  /* Checkbox / radio — square accent. */
191
317
  .ui-check {
192
318
  align-items: center;
package/css/overlay.css CHANGED
@@ -117,7 +117,7 @@
117
117
  position: absolute;
118
118
  inset-inline-end: 0;
119
119
  inset-block-start: calc(100% + 0.4rem);
120
- z-index: 30;
120
+ z-index: var(--z-overlay);
121
121
  }
122
122
 
123
123
  .ui-menu__label {
@@ -172,6 +172,69 @@
172
172
  }
173
173
  }
174
174
 
175
+ /* --- Combobox: an input with a filtered listbox popup (APG pattern,
176
+ wired by initCombobox). Reuses the menu surface tokens. --- */
177
+ .ui-combobox {
178
+ display: inline-block;
179
+ min-inline-size: 14rem;
180
+ position: relative;
181
+ }
182
+
183
+ .ui-combobox__input {
184
+ inline-size: 100%;
185
+ }
186
+
187
+ .ui-combobox__list {
188
+ background: var(--panel-strong);
189
+ border: 1px solid var(--line-strong);
190
+ border-radius: var(--radius-md);
191
+ box-shadow: var(--shadow-raised);
192
+ display: grid;
193
+ gap: 1px;
194
+ list-style: none;
195
+ margin: 0;
196
+ max-block-size: 16rem;
197
+ overflow-y: auto;
198
+ padding: 0.3rem;
199
+ position: absolute;
200
+ inset-block-start: calc(100% + 0.4rem);
201
+ inset-inline: 0;
202
+ z-index: var(--z-overlay);
203
+ }
204
+
205
+ .ui-combobox__list[hidden] {
206
+ display: none;
207
+ }
208
+
209
+ .ui-combobox__option {
210
+ border-radius: var(--radius-sm);
211
+ color: var(--text-soft);
212
+ cursor: pointer;
213
+ font-family: var(--mono);
214
+ font-size: var(--text-sm);
215
+ padding: 0.5rem 0.55rem;
216
+ }
217
+
218
+ .ui-combobox__option[hidden] {
219
+ display: none;
220
+ }
221
+
222
+ .ui-combobox__option.is-active,
223
+ .ui-combobox__option[aria-selected='true'] {
224
+ background: var(--bg-accent);
225
+ color: var(--text);
226
+ }
227
+
228
+ .ui-combobox__empty {
229
+ color: var(--text-dim);
230
+ font-size: var(--text-2xs);
231
+ padding: 0.5rem 0.55rem;
232
+ }
233
+
234
+ .ui-combobox__empty[hidden] {
235
+ display: none;
236
+ }
237
+
175
238
  /* --- Mobile: modal becomes a bottom sheet, drawer goes full-width --- */
176
239
 
177
240
  @media (max-width: 560px) {
@@ -27,6 +27,75 @@
27
27
  grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--grid-min, 16rem)), 1fr));
28
28
  }
29
29
 
30
+ /* Every-Layout set — the layouts consumers otherwise hand-roll. All
31
+ intrinsic (no media queries): they respond to content + available
32
+ space, so they work the same in a viewport or a container. */
33
+
34
+ /* Sidebar + main; the sidebar is its natural width, main takes the
35
+ rest, and they stack once main would go below --sidebar-min. */
36
+ .ui-sidebar {
37
+ display: flex;
38
+ flex-wrap: wrap;
39
+ gap: var(--sidebar-gap, var(--space-md));
40
+ }
41
+
42
+ .ui-sidebar > :first-child {
43
+ flex-basis: var(--sidebar-width, 16rem);
44
+ flex-grow: 1;
45
+ }
46
+
47
+ .ui-sidebar > :last-child {
48
+ flex-basis: 0;
49
+ flex-grow: 999;
50
+ min-inline-size: var(--sidebar-min, 60%);
51
+ }
52
+
53
+ /* All children in one row until they no longer fit --switcher-min,
54
+ then all switch to a column together (no half-states). */
55
+ .ui-switcher {
56
+ display: flex;
57
+ flex-wrap: wrap;
58
+ gap: var(--switcher-gap, var(--space-md));
59
+ }
60
+
61
+ .ui-switcher > * {
62
+ flex-basis: calc((var(--switcher-min, 24rem) - 100%) * 999);
63
+ flex-grow: 1;
64
+ }
65
+
66
+ /* Centered measure-bounded column. */
67
+ .ui-center {
68
+ box-sizing: content-box;
69
+ margin-inline: auto;
70
+ max-inline-size: var(--center-max, 64rem);
71
+ padding-inline: var(--center-gutter, var(--space-md));
72
+ }
73
+
74
+ /* Intrinsic aspect-ratio box; the media child fills it. */
75
+ .ui-ratio {
76
+ aspect-ratio: var(--ratio, 16 / 9);
77
+ }
78
+
79
+ .ui-ratio > * {
80
+ block-size: 100%;
81
+ inline-size: 100%;
82
+ object-fit: cover;
83
+ }
84
+
85
+ /* Opt-in containment: add .ui-cq to a wrapper to make descendant
86
+ .ui-grid / .ui-app-metrics adapt to THIS box, not the viewport
87
+ (island-safe). Inert until applied, so existing layouts/baselines
88
+ are unaffected. */
89
+ .ui-cq {
90
+ container: var(--cq-name, bronto) / inline-size;
91
+ }
92
+
93
+ @container bronto (max-width: 34rem) {
94
+ .ui-grid {
95
+ --grid-min: 100%;
96
+ }
97
+ }
98
+
30
99
  /* --- Surfaces --- */
31
100
 
32
101
  .ui-surface,
@@ -59,6 +128,13 @@
59
128
  border-color: var(--accent);
60
129
  }
61
130
 
131
+ /* Interactive cards usually wrap a single link; keyboard users get no
132
+ hover, so surface the focused state on the card itself (not pointer-
133
+ gated — lives outside the @media (hover) block below). */
134
+ .ui-card--interactive:focus-within {
135
+ border-color: var(--line-strong);
136
+ }
137
+
62
138
  .ui-card__head,
63
139
  .ui-panel__head {
64
140
  align-items: flex-start;
@@ -73,6 +149,95 @@
73
149
  margin: 0;
74
150
  }
75
151
 
152
+ /* --- Stat / metric tile — shell-agnostic ---
153
+ The canonical metric primitive. `.ui-app-metric*` / `.ui-app-metrics`
154
+ are permanent aliases grouped here so the admin shell keeps working
155
+ with byte-identical output (no baseline drift). */
156
+
157
+ .ui-statgrid,
158
+ .ui-app-metrics {
159
+ display: grid;
160
+ gap: var(--space-md);
161
+ grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
162
+ }
163
+
164
+ /* Inside an opt-in .ui-cq container, collapse to one column when the
165
+ container (not the viewport) is narrow — keeps tiles usable in a slim
166
+ panel. Inert without .ui-cq, so baselines are unaffected. */
167
+ @container bronto (max-width: 30rem) {
168
+ .ui-statgrid,
169
+ .ui-app-metrics {
170
+ grid-template-columns: 1fr;
171
+ }
172
+ }
173
+
174
+ .ui-stat,
175
+ .ui-app-metric {
176
+ background: var(--panel);
177
+ border: 1px solid var(--line);
178
+ border-radius: var(--radius-md);
179
+ display: grid;
180
+ gap: 0.4rem;
181
+ padding: var(--space-md);
182
+ }
183
+
184
+ .ui-stat__label,
185
+ .ui-app-metric__label {
186
+ color: var(--text-dim);
187
+ font-family: var(--mono);
188
+ font-size: var(--text-2xs);
189
+ letter-spacing: var(--tracking-wide);
190
+ text-transform: uppercase;
191
+ }
192
+
193
+ .ui-stat__value,
194
+ .ui-app-metric__value {
195
+ color: var(--text);
196
+ font-family: var(--display);
197
+ font-size: 1.9rem;
198
+ font-variant-numeric: tabular-nums;
199
+ letter-spacing: 0.01em;
200
+ line-height: 1;
201
+ }
202
+
203
+ .ui-stat__delta,
204
+ .ui-app-metric__delta {
205
+ font-family: var(--mono);
206
+ font-size: var(--text-xs);
207
+ }
208
+
209
+ .ui-stat__delta.is-pos,
210
+ .ui-app-metric__delta.is-pos {
211
+ color: var(--success);
212
+ }
213
+
214
+ .ui-stat__delta.is-neg,
215
+ .ui-app-metric__delta.is-neg {
216
+ color: var(--danger);
217
+ }
218
+
219
+ /* --- Numeric value vocabulary ---
220
+ The same tabular/aligned/tone intent the table has shipped since
221
+ 0.1.0, but freed from `.ui-table` so cards, stats and inline figures
222
+ share one P&L vocabulary. Token-identical to the table's is-num/
223
+ is-pos/is-neg (which stay table-local). */
224
+ .ui-num {
225
+ font-variant-numeric: tabular-nums;
226
+ text-align: end;
227
+ }
228
+
229
+ .ui-num--pos {
230
+ color: var(--success);
231
+ }
232
+
233
+ .ui-num--neg {
234
+ color: var(--danger);
235
+ }
236
+
237
+ .ui-num--muted {
238
+ color: var(--text-dim);
239
+ }
240
+
76
241
  /* --- Eyebrow / section label — dot-matrix face --- */
77
242
 
78
243
  .ui-eyebrow {
@@ -91,6 +256,12 @@
91
256
  color: var(--text-dim);
92
257
  }
93
258
 
259
+ /* Dense contexts (cards, table toolbars) — restores the size step that
260
+ 0.2.2 had as eyebrow--tight. Composes with --muted. */
261
+ .ui-eyebrow--sm {
262
+ font-size: var(--text-2xs);
263
+ }
264
+
94
265
  /* --- Buttons --- */
95
266
 
96
267
  .ui-button {
@@ -153,6 +324,32 @@
153
324
  transform: translateY(1px);
154
325
  }
155
326
 
327
+ /* Async/pending state — set aria-busy="true" (and disable) on submit.
328
+ A leading ring spinner is injected with no extra markup; the label
329
+ stays for context. Inert until aria-busy is set, so default buttons
330
+ and visual baselines are unaffected. */
331
+ .ui-button[aria-busy='true'] {
332
+ cursor: progress;
333
+ pointer-events: none;
334
+ }
335
+
336
+ .ui-button[aria-busy='true']::before {
337
+ animation: uiSpin 0.6s linear infinite;
338
+ block-size: 0.9em;
339
+ border: 2px solid currentcolor;
340
+ border-block-start-color: transparent;
341
+ border-radius: var(--radius-pill);
342
+ content: '';
343
+ flex: none;
344
+ inline-size: 0.9em;
345
+ }
346
+
347
+ @media (prefers-reduced-motion: reduce) {
348
+ .ui-button[aria-busy='true']::before {
349
+ animation-duration: 1.4s;
350
+ }
351
+ }
352
+
156
353
  /* --- Link --- */
157
354
 
158
355
  .ui-link {
@@ -166,14 +363,26 @@
166
363
  text-decoration-color var(--duration-fast) var(--ease-standard);
167
364
  }
168
365
 
169
- .ui-link--arrow {
366
+ .ui-link--arrow,
367
+ .ui-link--cta {
170
368
  align-items: center;
171
369
  display: inline-flex;
172
370
  gap: 0.4rem;
173
371
  text-decoration: none;
174
372
  }
175
373
 
176
- .ui-link--arrow::after {
374
+ /* CTA: the eyebrow-faced action link (accent · display · uppercase),
375
+ composed from the same tokens as .ui-eyebrow + the arrow glyph. */
376
+ .ui-link--cta {
377
+ color: var(--accent-text);
378
+ font-family: var(--display);
379
+ font-weight: 700;
380
+ letter-spacing: var(--tracking-wide);
381
+ text-transform: uppercase;
382
+ }
383
+
384
+ .ui-link--arrow::after,
385
+ .ui-link--cta::after {
177
386
  border-inline-end: 1px solid currentColor;
178
387
  border-block-start: 1px solid currentColor;
179
388
  content: '';
@@ -250,6 +459,42 @@
250
459
  border-color: color-mix(in srgb, var(--danger) 40%, var(--line));
251
460
  }
252
461
 
462
+ /* Idle / unknown — the honest "no signal yet" tone (distinct from the
463
+ default tinted badge). Token-safe; no new hue. */
464
+ .ui-badge--muted {
465
+ background: transparent;
466
+ border-color: var(--line);
467
+ color: var(--text-dim);
468
+ }
469
+
470
+ /* Leading state dot — composes with any tone modifier; the dot takes
471
+ the tone via currentColor-adjacent text, but stays neutral-strong so
472
+ it reads on the soft tint (same WCAG rationale as the tone note). */
473
+ .ui-badge--dot::before {
474
+ background: var(--text-soft);
475
+ border-radius: 50%;
476
+ content: '';
477
+ flex: 0 0 auto;
478
+ block-size: 0.4rem;
479
+ inline-size: 0.4rem;
480
+ }
481
+
482
+ .ui-badge--accent.ui-badge--dot::before {
483
+ background: var(--accent);
484
+ }
485
+
486
+ .ui-badge--success.ui-badge--dot::before {
487
+ background: var(--success);
488
+ }
489
+
490
+ .ui-badge--warning.ui-badge--dot::before {
491
+ background: var(--warning);
492
+ }
493
+
494
+ .ui-badge--danger.ui-badge--dot::before {
495
+ background: var(--danger);
496
+ }
497
+
253
498
  /* --- Key/value list --- */
254
499
 
255
500
  .ui-key-value {
@@ -289,6 +534,7 @@
289
534
  }
290
535
 
291
536
  .ui-card--interactive:hover {
537
+ box-shadow: var(--shadow-raised);
292
538
  transform: translateY(-2px);
293
539
  }
294
540
 
@@ -313,11 +559,13 @@
313
559
  text-decoration-color: var(--accent);
314
560
  }
315
561
 
316
- .ui-link--arrow:hover::after {
562
+ .ui-link--arrow:hover::after,
563
+ .ui-link--cta:hover::after {
317
564
  transform: translateX(0.14rem) rotate(45deg);
318
565
  }
319
566
 
320
- [dir='rtl'] .ui-link--arrow:hover::after {
567
+ [dir='rtl'] .ui-link--arrow:hover::after,
568
+ [dir='rtl'] .ui-link--cta:hover::after {
321
569
  transform: translateX(-0.14rem) rotate(45deg);
322
570
  }
323
571
 
package/css/site.css CHANGED
@@ -21,6 +21,12 @@
21
21
  max-inline-size: var(--container-narrow, 44rem);
22
22
  }
23
23
 
24
+ /* App / marketing shells run wider than the 72rem reading measure.
25
+ Override the cap per-consumer via --container-wide. */
26
+ .ui-container--wide {
27
+ max-inline-size: var(--container-wide, 82rem);
28
+ }
29
+
24
30
  /* --- Skip link — first focusable, jumps to main --- */
25
31
 
26
32
  .ui-skiplink {
@@ -32,7 +38,7 @@
32
38
  inset-inline-start: var(--space-md);
33
39
  padding: 0.7rem 1rem;
34
40
  position: fixed;
35
- z-index: 60;
41
+ z-index: var(--z-toast);
36
42
  }
37
43
 
38
44
  .ui-skiplink:focus {
@@ -51,6 +57,14 @@
51
57
  padding-block: var(--space-sm);
52
58
  }
53
59
 
60
+ /* Structural sticky only — the floating-card skin (radius/shadow/panel
61
+ bg) is consumer identity, not a framework concern; layer it yourself. */
62
+ .ui-siteheader--sticky {
63
+ position: sticky;
64
+ inset-block-start: 0;
65
+ z-index: var(--z-sticky);
66
+ }
67
+
54
68
  .ui-siteheader__brand {
55
69
  align-items: center;
56
70
  color: var(--text);
@@ -182,7 +196,7 @@
182
196
  min-inline-size: 12rem;
183
197
  padding: 0.4rem;
184
198
  position: absolute;
185
- z-index: 30;
199
+ z-index: var(--z-overlay);
186
200
  }
187
201
 
188
202
  /* Below the breakpoint the inline nav collapses into the menu. */
package/css/table.css CHANGED
@@ -100,3 +100,62 @@
100
100
  padding: 2rem;
101
101
  text-align: center;
102
102
  }
103
+
104
+ /* --- Sortable headers (wired by initTableSort) --- */
105
+ .ui-table__sort {
106
+ align-items: center;
107
+ background: none;
108
+ border: 0;
109
+ color: inherit;
110
+ cursor: pointer;
111
+ display: flex;
112
+ font: inherit;
113
+ gap: 0.4rem;
114
+ letter-spacing: inherit;
115
+ padding: 0;
116
+ text-transform: inherit;
117
+ inline-size: 100%;
118
+ }
119
+
120
+ .ui-table__sort::after {
121
+ content: '↕';
122
+ opacity: 0.4;
123
+ }
124
+
125
+ .ui-table th[aria-sort='ascending'] .ui-table__sort::after {
126
+ content: '↑';
127
+ opacity: 1;
128
+ }
129
+
130
+ .ui-table th[aria-sort='descending'] .ui-table__sort::after {
131
+ content: '↓';
132
+ opacity: 1;
133
+ }
134
+
135
+ /* --- Row selection --- */
136
+ .ui-table__select {
137
+ inline-size: 1px; /* shrink-to-fit checkbox column */
138
+ white-space: nowrap;
139
+ }
140
+
141
+ .ui-table--selectable tbody tr[aria-selected='true'] td {
142
+ background: var(--bg-accent);
143
+ }
144
+
145
+ /* --- Bulk-action toolbar --- */
146
+ .ui-table__toolbar {
147
+ align-items: center;
148
+ border: 1px solid var(--line);
149
+ border-block-end: 0;
150
+ border-start-start-radius: var(--radius-md);
151
+ border-start-end-radius: var(--radius-md);
152
+ display: flex;
153
+ gap: var(--space-sm);
154
+ padding: var(--space-sm) var(--space-md);
155
+ }
156
+
157
+ /* --- Loading state: set aria-busy + .ui-table--loading on the wrap --- */
158
+ .ui-table--loading {
159
+ opacity: 0.6;
160
+ pointer-events: none;
161
+ }