@webority/theme 0.3.0 → 0.5.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.
@@ -0,0 +1,113 @@
1
+ // Accordion / FAQ list.
2
+ //
3
+ // Part of the wui-components layer — see _wui-components.scss for the whole
4
+ // picture and the load order. Order within the layer is LOAD-BEARING: these
5
+ // partials are a split of one stylesheet, not independent modules, so a rule
6
+ // here may be overridden by a later partial. Never reorder the imports.
7
+
8
+ // ── Accordion / FAQ list ──
9
+ // Height animates with `grid-template-rows: 0fr -> 1fr`, so a pane opens to its
10
+ // natural height with NO JavaScript measuring and no max-height guess. The only
11
+ // thing script does is toggle `.open`.
12
+ //
13
+ // Markup (identical on both surfaces):
14
+ // <div class="faq-list">
15
+ // <div class="faq-item"> (+ .open when expanded)
16
+ // <button class="faq-q" aria-expanded>…<span class="faq-q-icon">…</span></button>
17
+ // <div class="faq-a-wrap"><div class="faq-a-inner">…</div></div>
18
+ // </div>
19
+ // </div>
20
+ .faq-list {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 8px;
24
+ }
25
+ .faq-item {
26
+ border: 1px solid var(--color-border);
27
+ border-radius: var(--radius-lg);
28
+ corner-shape: squircle;
29
+ background: var(--color-surface);
30
+ overflow: hidden;
31
+ transition: border-color 0.15s ease;
32
+ }
33
+ .faq-item:hover:not(.open) {
34
+ border-color: var(--color-border-strong);
35
+ }
36
+ .faq-item.open {
37
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
38
+ }
39
+ .faq-q {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ gap: 16px;
44
+ width: 100%;
45
+ padding: 16px 18px;
46
+ border: 0;
47
+ background: transparent;
48
+ font-family: inherit;
49
+ font-size: 15px;
50
+ font-weight: 600;
51
+ line-height: 1.45;
52
+ text-align: left;
53
+ color: var(--color-text);
54
+ cursor: pointer;
55
+ transition: color 0.12s ease;
56
+ }
57
+ .faq-q:hover {
58
+ color: var(--color-primary);
59
+ }
60
+ .faq-q:focus-visible {
61
+ outline: 2px solid var(--color-primary);
62
+ outline-offset: -2px;
63
+ }
64
+ .faq-item.open .faq-q {
65
+ color: var(--color-primary);
66
+ }
67
+ // Chevron rotates to point up when the pane is open.
68
+ .faq-q-icon {
69
+ flex: none;
70
+ display: grid;
71
+ place-items: center;
72
+ color: var(--color-text-subtle);
73
+ transition: transform 0.2s var(--wui-ease-out-quart);
74
+ }
75
+ .faq-item.open .faq-q-icon {
76
+ transform: rotate(180deg);
77
+ color: var(--color-primary);
78
+ }
79
+
80
+ // The animating track. `min-height: 0` on the inner element is required — a grid
81
+ // item's default `min-height: auto` refuses to shrink below its content, which
82
+ // would defeat the 0fr row entirely.
83
+ .faq-a-wrap {
84
+ display: grid;
85
+ grid-template-rows: 0fr;
86
+ transition: grid-template-rows 0.28s var(--wui-ease-out-quart);
87
+ }
88
+ .faq-item.open .faq-a-wrap {
89
+ grid-template-rows: 1fr;
90
+ }
91
+ .faq-a-inner {
92
+ min-height: 0;
93
+ overflow: hidden;
94
+ font-size: 14px;
95
+ line-height: 1.6;
96
+ color: var(--color-text-muted);
97
+ }
98
+ // Padding lives on a child of the clipped element so it does not add height while
99
+ // the row is collapsed to 0fr.
100
+ .faq-a-inner > :first-child {
101
+ margin-top: 0;
102
+ }
103
+ .faq-item.open .faq-a-inner {
104
+ padding: 0 18px 16px;
105
+ }
106
+
107
+ @media (prefers-reduced-motion: reduce) {
108
+ .faq-a-wrap,
109
+ .faq-q-icon,
110
+ .faq-item {
111
+ transition: none;
112
+ }
113
+ }
@@ -0,0 +1,508 @@
1
+ // KPI tile, info list, segmented, colour field, timeline, compare table, resource card.
2
+ //
3
+ // Part of the wui-components layer — see _wui-components.scss for the whole
4
+ // picture and the load order. Order within the layer is LOAD-BEARING: these
5
+ // partials are a split of one stylesheet, not independent modules, so a rule
6
+ // here may be overridden by a later partial. Never reorder the imports.
7
+
8
+ // ============================================================================
9
+ // KPI / stat tile — a single derived number with its label and an optional delta.
10
+ // ============================================================================
11
+ .kpi {
12
+ display: flex;
13
+ flex-direction: column;
14
+ padding: 18px 20px;
15
+ border: 1px solid var(--color-border);
16
+ border-radius: var(--radius-lg);
17
+ corner-shape: squircle;
18
+ background: var(--color-surface);
19
+ }
20
+ .kpi-icon {
21
+ flex: none;
22
+ width: 40px;
23
+ height: 40px;
24
+ display: inline-grid;
25
+ place-items: center;
26
+ margin-bottom: 12px;
27
+ border-radius: 999px;
28
+ background: var(--color-primary-soft);
29
+ color: var(--color-primary);
30
+ }
31
+ .kpi-label {
32
+ font-size: 13px;
33
+ font-weight: 500;
34
+ color: var(--color-text-subtle);
35
+ }
36
+ .kpi-value {
37
+ margin-top: 6px;
38
+ font-size: 28px;
39
+ font-weight: 700;
40
+ // Large numerals need negative tracking to stop reading as separate digits;
41
+ // tabular figures stop a live-updating value from shifting width.
42
+ letter-spacing: -0.025em;
43
+ font-variant-numeric: tabular-nums;
44
+ color: var(--color-text);
45
+ }
46
+ .kpi-delta {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ align-self: flex-start;
50
+ gap: 4px;
51
+ margin-top: 6px;
52
+ font-size: 12px;
53
+ font-weight: 500;
54
+ color: var(--color-text-muted);
55
+ }
56
+ .kpi-delta.up {
57
+ color: var(--color-success-text);
58
+ }
59
+ .kpi-delta.down {
60
+ color: var(--color-danger-text);
61
+ }
62
+
63
+ // Icon-left layout: the icon spans the label/value/delta stack instead of sitting above it.
64
+ .kpi.kpi-inline {
65
+ display: grid;
66
+ grid-template-columns: auto minmax(0, 1fr);
67
+ column-gap: 14px;
68
+ align-items: center;
69
+ }
70
+ .kpi.kpi-inline .kpi-icon {
71
+ grid-row: 1 / span 3;
72
+ align-self: center;
73
+ margin-bottom: 0;
74
+ }
75
+ .kpi.kpi-inline .kpi-value {
76
+ margin-top: 2px;
77
+ }
78
+
79
+ // A row of tiles. Wraps rather than scrolls — a KPI strip that scrolls hides numbers.
80
+ .kpi-row {
81
+ display: grid;
82
+ gap: 16px;
83
+ grid-template-columns: 1fr;
84
+ }
85
+ @include media-breakpoint-up(sm) {
86
+ .kpi-row.kpi-row {
87
+ grid-template-columns: repeat(2, minmax(0, 1fr));
88
+ }
89
+ }
90
+ @include media-breakpoint-up(lg) {
91
+ .kpi-row.kpi-row {
92
+ grid-template-columns: repeat(4, minmax(0, 1fr));
93
+ }
94
+ }
95
+
96
+ // ============================================================================
97
+ // Icon-tile info list — label/value rows, each with a leading round icon.
98
+ // The row is an <a> when it links (mail/tel), a <div> otherwise.
99
+ // ============================================================================
100
+ .info-list {
101
+ display: flex;
102
+ flex-direction: column;
103
+ }
104
+ .info-row {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 16px;
108
+ padding: 16px 0;
109
+ border-bottom: 1px solid var(--color-border);
110
+ color: var(--color-text);
111
+ text-decoration: none;
112
+ }
113
+ // First and last rows shed their outer padding so the list sits flush in a card body.
114
+ .info-row:first-child {
115
+ padding-top: 0;
116
+ }
117
+ .info-row:last-child {
118
+ padding-bottom: 0;
119
+ border-bottom: none;
120
+ }
121
+ .info-icon {
122
+ flex: none;
123
+ width: 36px;
124
+ height: 36px;
125
+ display: inline-grid;
126
+ place-items: center;
127
+ border-radius: 999px;
128
+ background: var(--color-primary-soft);
129
+ color: var(--color-primary);
130
+ }
131
+ .info-meta {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 2px;
135
+ min-width: 0;
136
+ }
137
+ .info-key {
138
+ font-size: 12px;
139
+ font-weight: 600;
140
+ text-transform: uppercase;
141
+ letter-spacing: 0.04em;
142
+ color: var(--color-text-subtle);
143
+ }
144
+ .info-value {
145
+ font-size: 14px;
146
+ font-weight: 500;
147
+ color: var(--color-text);
148
+ overflow: hidden;
149
+ text-overflow: ellipsis;
150
+ white-space: nowrap;
151
+ }
152
+ a.info-row,
153
+ a.info-row:hover {
154
+ text-decoration: none;
155
+ }
156
+ a.info-row:hover .info-value {
157
+ color: var(--color-primary);
158
+ text-decoration: underline;
159
+ }
160
+
161
+ // ============================================================================
162
+ // Segmented control — 2–4 mutually-exclusive options shown side by side.
163
+ // The theme toggle (light/dark/system) is the canonical use.
164
+ // ============================================================================
165
+ .segmented {
166
+ display: inline-flex;
167
+ gap: 2px;
168
+ padding: 3px;
169
+ border: 1px solid var(--color-border);
170
+ border-radius: 999px;
171
+ background: var(--color-bg-muted);
172
+ }
173
+ .segmented-option {
174
+ display: inline-flex;
175
+ align-items: center;
176
+ gap: 5px;
177
+ padding: 5px 12px;
178
+ border: 0;
179
+ border-radius: 999px;
180
+ background: transparent;
181
+ font-family: inherit;
182
+ font-size: 11.5px;
183
+ font-weight: 600;
184
+ line-height: 1;
185
+ white-space: nowrap;
186
+ color: var(--color-text-muted);
187
+ cursor: pointer;
188
+ transition:
189
+ background 0.14s ease,
190
+ color 0.14s ease;
191
+ }
192
+ // The real radio stays in the accessibility tree and keeps focus — only its box is
193
+ // hidden. `display: none` would drop it from the tab order and break arrow-key
194
+ // navigation, which is the whole reason for using a native radio here.
195
+ .segmented-input {
196
+ position: absolute;
197
+ width: 1px;
198
+ height: 1px;
199
+ margin: -1px;
200
+ padding: 0;
201
+ overflow: hidden;
202
+ clip-path: inset(50%);
203
+ white-space: nowrap;
204
+ }
205
+ .segmented-option:hover {
206
+ color: var(--color-text);
207
+ }
208
+ .segmented-option:has(.segmented-input:checked) {
209
+ background: var(--color-primary);
210
+ // Fixed white, not a token: the pill is always the brand fill, so its label
211
+ // must contrast with that fill and not with the page.
212
+ color: #fff;
213
+ }
214
+ // The ring is drawn on the label because the input it belongs to has no box.
215
+ .segmented-option:has(.segmented-input:focus-visible) {
216
+ outline: 2px solid var(--color-primary);
217
+ outline-offset: 2px;
218
+ }
219
+ .segmented-sm .segmented-option {
220
+ padding: 4px 9px;
221
+ font-size: 11px;
222
+ }
223
+ @media (prefers-reduced-motion: reduce) {
224
+ .segmented-option {
225
+ transition: none;
226
+ }
227
+ }
228
+
229
+ // ============================================================================
230
+ // Colour field — a native colour swatch beside a hex text input, one control.
231
+ // ============================================================================
232
+ .color-field {
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 10px;
236
+ }
237
+ .color-field-swatch {
238
+ flex: none;
239
+ width: 40px;
240
+ height: 40px;
241
+ padding: 0;
242
+ border: 1px solid var(--color-border);
243
+ border-radius: var(--radius);
244
+ background: var(--color-surface);
245
+ cursor: pointer;
246
+ }
247
+ // The UA draws its own inset frame around the colour area; strip it so the
248
+ // swatch reads as one flat chip inside our border.
249
+ .color-field-swatch::-webkit-color-swatch-wrapper {
250
+ padding: 3px;
251
+ }
252
+ .color-field-swatch::-webkit-color-swatch {
253
+ border: none;
254
+ border-radius: calc(var(--radius) - 3px);
255
+ }
256
+ .color-field-swatch::-moz-color-swatch {
257
+ border: none;
258
+ border-radius: calc(var(--radius) - 3px);
259
+ }
260
+ .color-field-swatch:focus-visible {
261
+ outline: 2px solid var(--color-primary);
262
+ outline-offset: 2px;
263
+ }
264
+ .color-field-hex {
265
+ flex: 1;
266
+ max-width: 160px;
267
+ font-family: var(--bs-font-monospace);
268
+ text-transform: uppercase;
269
+ }
270
+
271
+ // ============================================================================
272
+ // Timeline / activity feed. The rail is drawn on the LIST, not per item, so it
273
+ // cannot break into segments when an entry wraps to two lines.
274
+ // ============================================================================
275
+ .timeline {
276
+ position: relative;
277
+ margin: 0;
278
+ padding: 0 0 0 6px;
279
+ list-style: none;
280
+ }
281
+ .timeline::before {
282
+ content: "";
283
+ position: absolute;
284
+ // Inset top and bottom so the rail starts and ends inside the first/last dot.
285
+ top: 10px;
286
+ bottom: 10px;
287
+ left: 17px;
288
+ width: 2px;
289
+ background: var(--color-border);
290
+ }
291
+ .timeline-item {
292
+ position: relative;
293
+ display: flex;
294
+ gap: 14px;
295
+ padding-bottom: 18px;
296
+ }
297
+ .timeline-item:last-child {
298
+ padding-bottom: 0;
299
+ }
300
+ .timeline-dot {
301
+ position: relative;
302
+ z-index: 1;
303
+ flex: none;
304
+ width: 24px;
305
+ height: 24px;
306
+ display: inline-grid;
307
+ place-items: center;
308
+ border-radius: 999px;
309
+ color: var(--color-text-muted);
310
+ background: color-mix(in srgb, var(--color-text-muted) 14%, var(--color-surface));
311
+ // A surface-coloured ring punches the rail out behind the dot.
312
+ box-shadow: 0 0 0 3px var(--color-surface);
313
+ }
314
+ .timeline-item.success .timeline-dot {
315
+ color: var(--color-success-text);
316
+ background: color-mix(in srgb, var(--color-success-strong) 16%, var(--color-surface));
317
+ }
318
+ .timeline-item.warning .timeline-dot {
319
+ color: var(--color-warning-text);
320
+ background: color-mix(in srgb, var(--color-warning-strong) 18%, var(--color-surface));
321
+ }
322
+ .timeline-item.danger .timeline-dot {
323
+ color: var(--color-danger-text);
324
+ background: color-mix(in srgb, var(--color-danger) 16%, var(--color-surface));
325
+ }
326
+ .timeline-item.info .timeline-dot {
327
+ color: var(--color-info-text);
328
+ background: color-mix(in srgb, var(--color-info-strong) 16%, var(--color-surface));
329
+ }
330
+ .timeline-body {
331
+ flex: 1;
332
+ min-width: 0;
333
+ }
334
+ .timeline-title {
335
+ font-size: 14px;
336
+ font-weight: 600;
337
+ color: var(--color-text);
338
+ }
339
+ // Wraps rather than truncates: an audit line must never hide half the timestamp.
340
+ .timeline-meta {
341
+ display: flex;
342
+ flex-wrap: wrap;
343
+ align-items: baseline;
344
+ gap: 6px;
345
+ margin-top: 2px;
346
+ font-size: 12.5px;
347
+ color: var(--color-text-muted);
348
+ }
349
+ .timeline-at {
350
+ color: var(--color-text-subtle);
351
+ font-variant-numeric: tabular-nums;
352
+ }
353
+ .timeline-by {
354
+ color: var(--color-text-muted);
355
+ overflow-wrap: anywhere;
356
+ }
357
+ .timeline-note {
358
+ margin: 6px 0 0;
359
+ padding: 8px 10px;
360
+ border-radius: var(--radius);
361
+ background: var(--color-bg-muted);
362
+ font-size: 13px;
363
+ line-height: 1.5;
364
+ color: var(--color-text-muted);
365
+ // Preserve the author's line breaks; break long unspaced strings rather than overflow.
366
+ white-space: pre-wrap;
367
+ overflow-wrap: anywhere;
368
+ }
369
+
370
+ // ============================================================================
371
+ // Feature-comparison table — us vs them. The highlighted column is tinted so
372
+ // the reader's eye lands on it without a legend.
373
+ // ============================================================================
374
+ .compare-table {
375
+ overflow: hidden;
376
+ border: 1px solid var(--color-border);
377
+ border-radius: var(--radius-lg);
378
+ corner-shape: squircle;
379
+ background: var(--color-surface);
380
+ }
381
+ .compare-table table {
382
+ width: 100%;
383
+ border-collapse: collapse;
384
+ }
385
+ .compare-table th,
386
+ .compare-table td {
387
+ padding: 14px 18px;
388
+ text-align: left;
389
+ border-bottom: 1px solid var(--color-border);
390
+ font-size: 14px;
391
+ }
392
+ .compare-table tr:last-child td {
393
+ border-bottom: none;
394
+ }
395
+ .compare-table th {
396
+ background: var(--color-bg-subtle);
397
+ font-size: 13px;
398
+ font-weight: 600;
399
+ color: var(--color-text);
400
+ }
401
+ .compare-table th.is-highlight {
402
+ background: var(--color-primary-soft);
403
+ color: var(--color-primary);
404
+ }
405
+ .compare-table td.is-highlight {
406
+ background: color-mix(in srgb, var(--color-primary) 4%, var(--color-surface));
407
+ font-weight: 500;
408
+ }
409
+ .compare-yes {
410
+ color: var(--color-success-text);
411
+ }
412
+ .compare-no {
413
+ color: var(--color-text-subtle);
414
+ }
415
+
416
+ // ============================================================================
417
+ // Resource card — an icon-tile link to a doc, guide or template.
418
+ // ============================================================================
419
+ .res-grid {
420
+ display: grid;
421
+ gap: 12px;
422
+ grid-template-columns: 1fr;
423
+ }
424
+ @include media-breakpoint-up(sm) {
425
+ .res-grid.res-grid {
426
+ grid-template-columns: repeat(2, minmax(0, 1fr));
427
+ }
428
+ }
429
+ @include media-breakpoint-up(lg) {
430
+ .res-grid.res-grid {
431
+ grid-template-columns: repeat(3, minmax(0, 1fr));
432
+ }
433
+ }
434
+ .res-card {
435
+ display: flex;
436
+ align-items: flex-start;
437
+ gap: 12px;
438
+ padding: 14px 16px;
439
+ border: 1px solid var(--color-border);
440
+ border-radius: var(--radius-xl);
441
+ corner-shape: squircle;
442
+ background: var(--color-surface);
443
+ color: inherit;
444
+ text-decoration: none;
445
+ transition:
446
+ border-color 0.3s var(--wui-ease-out-quart),
447
+ background 0.3s var(--wui-ease-out-quart);
448
+ }
449
+ .res-card,
450
+ .res-card:hover {
451
+ text-decoration: none;
452
+ }
453
+ .res-card:hover {
454
+ border-color: var(--color-primary);
455
+ background: var(--color-primary-soft);
456
+ }
457
+ .res-card:focus-visible {
458
+ outline: 2px solid var(--color-primary);
459
+ outline-offset: 2px;
460
+ }
461
+ .res-card-icon {
462
+ flex: none;
463
+ width: 40px;
464
+ height: 40px;
465
+ display: inline-grid;
466
+ place-items: center;
467
+ border-radius: 999px;
468
+ background: var(--color-primary-soft);
469
+ color: var(--color-primary);
470
+ transition: background 0.3s var(--wui-ease-out-quart);
471
+ }
472
+ // On hover the card takes the soft tint, so the icon inverts to the surface
473
+ // colour to stay distinct from it.
474
+ .res-card:hover .res-card-icon {
475
+ background: var(--color-surface);
476
+ }
477
+ // The text column is a flex container so the title and text blockify without
478
+ // each needing its own `display` — they are inline elements inside an <a>.
479
+ .res-card-body {
480
+ display: flex;
481
+ flex-direction: column;
482
+ min-width: 0;
483
+ }
484
+ .res-card-title {
485
+ font-size: 14px;
486
+ font-weight: 600;
487
+ line-height: 1.4;
488
+ color: var(--color-text);
489
+ transition: color 0.3s var(--wui-ease-out-quart);
490
+ }
491
+ .res-card:hover .res-card-title {
492
+ color: var(--color-primary);
493
+ }
494
+ .res-card-text {
495
+ margin-top: 2px;
496
+ font-size: 12px;
497
+ line-height: 1.5;
498
+ color: var(--color-text-subtle);
499
+ }
500
+ @media (prefers-reduced-motion: reduce) {
501
+ .res-card,
502
+ .res-card-icon,
503
+ .res-card-title {
504
+ transition: none;
505
+ }
506
+ }
507
+
508
+ // ============================================================================