@ponchia/ui 0.3.4 → 0.3.5

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.
@@ -85,6 +85,9 @@ export declare const cls: {
85
85
  readonly hintError: 'ui-hint--error';
86
86
  readonly inputGroup: 'ui-input-group';
87
87
  readonly inputGroupAddon: 'ui-input-group__addon';
88
+ readonly inputIcon: 'ui-input-icon';
89
+ readonly inputIconSlot: 'ui-input-icon__icon';
90
+ readonly inputIconEnd: 'ui-input-icon--end';
88
91
  readonly file: 'ui-file';
89
92
  readonly range: 'ui-range';
90
93
  readonly errorSummary: 'ui-error-summary';
@@ -115,6 +118,15 @@ export declare const cls: {
115
118
  readonly progress: 'ui-progress';
116
119
  readonly progressBar: 'ui-progress__bar';
117
120
  readonly progressIndeterminate: 'ui-progress--indeterminate';
121
+ readonly meter: 'ui-meter';
122
+ readonly meterFill: 'ui-meter__fill';
123
+ readonly meterAccent: 'ui-meter--accent';
124
+ readonly meterSuccess: 'ui-meter--success';
125
+ readonly meterWarning: 'ui-meter--warning';
126
+ readonly meterDanger: 'ui-meter--danger';
127
+ readonly steps: 'ui-steps';
128
+ readonly stepsItem: 'ui-steps__item';
129
+ readonly stepsItemDone: 'ui-steps__item--done';
118
130
  readonly modal: 'ui-modal';
119
131
  readonly modalHead: 'ui-modal__head';
120
132
  readonly modalTitle: 'ui-modal__title';
@@ -132,6 +144,8 @@ export declare const cls: {
132
144
  readonly comboboxList: 'ui-combobox__list';
133
145
  readonly comboboxOption: 'ui-combobox__option';
134
146
  readonly comboboxEmpty: 'ui-combobox__empty';
147
+ readonly lightbox: 'ui-lightbox';
148
+ readonly lightboxClose: 'ui-lightbox__close';
135
149
  readonly tabs: 'ui-tabs';
136
150
  readonly tabsList: 'ui-tabs__list';
137
151
  readonly tab: 'ui-tab';
@@ -150,6 +164,15 @@ export declare const cls: {
150
164
  readonly avatarSm: 'ui-avatar--sm';
151
165
  readonly avatarLg: 'ui-avatar--lg';
152
166
  readonly avatarGroup: 'ui-avatar-group';
167
+ readonly carousel: 'ui-carousel';
168
+ readonly carouselStage: 'ui-carousel__stage';
169
+ readonly carouselViewport: 'ui-carousel__viewport';
170
+ readonly carouselSlide: 'ui-carousel__slide';
171
+ readonly carouselPrev: 'ui-carousel__prev';
172
+ readonly carouselNext: 'ui-carousel__next';
173
+ readonly carouselStatus: 'ui-carousel__status';
174
+ readonly carouselThumbs: 'ui-carousel__thumbs';
175
+ readonly carouselThumb: 'ui-carousel__thumb';
153
176
  readonly table: 'ui-table';
154
177
  readonly tableDense: 'ui-table--dense';
155
178
  readonly tableComfortable: 'ui-table--comfortable';
@@ -190,6 +213,9 @@ export declare const cls: {
190
213
  readonly siteheaderSticky: 'ui-siteheader--sticky';
191
214
  readonly siteheaderBrand: 'ui-siteheader__brand';
192
215
  readonly siteheaderActions: 'ui-siteheader__actions';
216
+ readonly pagehead: 'ui-pagehead';
217
+ readonly pageheadTitle: 'ui-pagehead__title';
218
+ readonly pageheadActions: 'ui-pagehead__actions';
193
219
  readonly sitenav: 'ui-sitenav';
194
220
  readonly sitemenu: 'ui-sitemenu';
195
221
  readonly sitemenuPanel: 'ui-sitemenu__panel';
@@ -200,6 +226,10 @@ export declare const cls: {
200
226
  readonly tagAccent: 'ui-tag--accent';
201
227
  readonly meta: 'ui-meta';
202
228
  readonly metaItem: 'ui-meta__item';
229
+ readonly timeline: 'ui-timeline';
230
+ readonly timelineItem: 'ui-timeline__item';
231
+ readonly timelineTime: 'ui-timeline__time';
232
+ readonly kbd: 'ui-kbd';
203
233
  readonly display: 'ui-display';
204
234
  readonly mono: 'ui-mono';
205
235
  readonly muted: 'ui-muted';
@@ -307,6 +337,9 @@ export interface ToastOpts {
307
337
  export interface ProgressOpts {
308
338
  indeterminate?: boolean;
309
339
  }
340
+ export interface MeterOpts {
341
+ tone?: 'accent' | 'success' | 'warning' | 'danger';
342
+ }
310
343
  export interface DotspinnerOpts {
311
344
  size?: 'sm' | 'lg';
312
345
  }
@@ -334,6 +367,10 @@ export interface ContainerOpts {
334
367
  export interface TagOpts {
335
368
  accent?: boolean;
336
369
  }
370
+ export interface InputIconOpts {
371
+ /** Place the icon at the inline-end instead of the start. */
372
+ end?: boolean;
373
+ }
337
374
 
338
375
  export interface Ui {
339
376
  button(opts?: ButtonOpts): string;
@@ -352,6 +389,7 @@ export interface Ui {
352
389
  alert(opts?: AlertOpts): string;
353
390
  toast(opts?: ToastOpts): string;
354
391
  progress(opts?: ProgressOpts): string;
392
+ meter(opts?: MeterOpts): string;
355
393
  dotspinner(opts?: DotspinnerOpts): string;
356
394
  dotbar(opts?: DotbarOpts): string;
357
395
  modal(opts?: ModalOpts): string;
@@ -360,6 +398,7 @@ export interface Ui {
360
398
  prose(opts?: ProseOpts): string;
361
399
  container(opts?: ContainerOpts): string;
362
400
  tag(opts?: TagOpts): string;
401
+ inputIcon(opts?: InputIconOpts): string;
363
402
  }
364
403
 
365
404
  export declare const ui: Ui;
package/classes/index.js CHANGED
@@ -91,6 +91,9 @@ export const cls = Object.freeze({
91
91
  hintError: 'ui-hint--error',
92
92
  inputGroup: 'ui-input-group',
93
93
  inputGroupAddon: 'ui-input-group__addon',
94
+ inputIcon: 'ui-input-icon',
95
+ inputIconSlot: 'ui-input-icon__icon',
96
+ inputIconEnd: 'ui-input-icon--end',
94
97
  file: 'ui-file',
95
98
  range: 'ui-range',
96
99
  errorSummary: 'ui-error-summary',
@@ -122,6 +125,15 @@ export const cls = Object.freeze({
122
125
  progress: 'ui-progress',
123
126
  progressBar: 'ui-progress__bar',
124
127
  progressIndeterminate: 'ui-progress--indeterminate',
128
+ meter: 'ui-meter',
129
+ meterFill: 'ui-meter__fill',
130
+ meterAccent: 'ui-meter--accent',
131
+ meterSuccess: 'ui-meter--success',
132
+ meterWarning: 'ui-meter--warning',
133
+ meterDanger: 'ui-meter--danger',
134
+ steps: 'ui-steps',
135
+ stepsItem: 'ui-steps__item',
136
+ stepsItemDone: 'ui-steps__item--done',
125
137
  // overlay
126
138
  modal: 'ui-modal',
127
139
  modalHead: 'ui-modal__head',
@@ -140,6 +152,8 @@ export const cls = Object.freeze({
140
152
  comboboxList: 'ui-combobox__list',
141
153
  comboboxOption: 'ui-combobox__option',
142
154
  comboboxEmpty: 'ui-combobox__empty',
155
+ lightbox: 'ui-lightbox',
156
+ lightboxClose: 'ui-lightbox__close',
143
157
  // disclosure
144
158
  tabs: 'ui-tabs',
145
159
  tabsList: 'ui-tabs__list',
@@ -159,6 +173,15 @@ export const cls = Object.freeze({
159
173
  avatarSm: 'ui-avatar--sm',
160
174
  avatarLg: 'ui-avatar--lg',
161
175
  avatarGroup: 'ui-avatar-group',
176
+ carousel: 'ui-carousel',
177
+ carouselStage: 'ui-carousel__stage',
178
+ carouselViewport: 'ui-carousel__viewport',
179
+ carouselSlide: 'ui-carousel__slide',
180
+ carouselPrev: 'ui-carousel__prev',
181
+ carouselNext: 'ui-carousel__next',
182
+ carouselStatus: 'ui-carousel__status',
183
+ carouselThumbs: 'ui-carousel__thumbs',
184
+ carouselThumb: 'ui-carousel__thumb',
162
185
  // table
163
186
  table: 'ui-table',
164
187
  tableDense: 'ui-table--dense',
@@ -203,6 +226,9 @@ export const cls = Object.freeze({
203
226
  siteheaderSticky: 'ui-siteheader--sticky',
204
227
  siteheaderBrand: 'ui-siteheader__brand',
205
228
  siteheaderActions: 'ui-siteheader__actions',
229
+ pagehead: 'ui-pagehead',
230
+ pageheadTitle: 'ui-pagehead__title',
231
+ pageheadActions: 'ui-pagehead__actions',
206
232
  sitenav: 'ui-sitenav',
207
233
  sitemenu: 'ui-sitemenu',
208
234
  sitemenuPanel: 'ui-sitemenu__panel',
@@ -213,6 +239,10 @@ export const cls = Object.freeze({
213
239
  tagAccent: 'ui-tag--accent',
214
240
  meta: 'ui-meta',
215
241
  metaItem: 'ui-meta__item',
242
+ timeline: 'ui-timeline',
243
+ timelineItem: 'ui-timeline__item',
244
+ timelineTime: 'ui-timeline__time',
245
+ kbd: 'ui-kbd',
216
246
  display: 'ui-display',
217
247
  mono: 'ui-mono',
218
248
  muted: 'ui-muted',
@@ -346,6 +376,14 @@ export const ui = {
346
376
  tone === 'info' && cls.toastInfo,
347
377
  ),
348
378
  progress: ({ indeterminate } = {}) => j(cls.progress, indeterminate && cls.progressIndeterminate),
379
+ meter: ({ tone } = {}) =>
380
+ j(
381
+ cls.meter,
382
+ tone === 'accent' && cls.meterAccent,
383
+ tone === 'success' && cls.meterSuccess,
384
+ tone === 'warning' && cls.meterWarning,
385
+ tone === 'danger' && cls.meterDanger,
386
+ ),
349
387
  dotspinner: ({ size } = {}) =>
350
388
  j(cls.dotspinner, size === 'sm' && cls.dotspinnerSm, size === 'lg' && cls.dotspinnerLg),
351
389
  dotbar: ({ indeterminate } = {}) => j(cls.dotbar, indeterminate && cls.dotbarIndeterminate),
@@ -357,6 +395,7 @@ export const ui = {
357
395
  container: ({ narrow, wide } = {}) =>
358
396
  j(cls.container, narrow && cls.containerNarrow, wide && cls.containerWide),
359
397
  tag: ({ accent } = {}) => j(cls.tag, accent && cls.tagAccent),
398
+ inputIcon: ({ end } = {}) => j(cls.inputIcon, end && cls.inputIconEnd),
360
399
  };
361
400
 
362
401
  export default ui;
@@ -321,3 +321,183 @@
321
321
  .ui-avatar-group .ui-avatar:not(:first-child) {
322
322
  margin-inline-start: -0.5rem;
323
323
  }
324
+
325
+ /* --- Carousel — a scroll-snap track of slides with an optional thumbnail
326
+ strip. Native horizontal scroll gives touch/trackpad swipe + momentum
327
+ for free; initCarousel adds prev/next, keyboard, thumbnail sync and the
328
+ position counter. A full-screen gallery is this same carousel inside a
329
+ native <dialog class="ui-lightbox"> (see overlay.css) — the dialog
330
+ brings the top layer, focus-trap and Esc; nothing is re-implemented. --- */
331
+
332
+ .ui-carousel {
333
+ display: grid;
334
+ gap: var(--space-sm);
335
+ }
336
+
337
+ /* Positioned frame for the viewport + overlay controls. The arrows can't
338
+ live inside the scroll viewport (absolute children of a scroll container
339
+ scroll away with the content), so they anchor to this stage instead. */
340
+ .ui-carousel__stage {
341
+ position: relative;
342
+ }
343
+
344
+ .ui-carousel__viewport {
345
+ display: flex;
346
+ overflow-x: auto;
347
+ scroll-behavior: smooth;
348
+ scroll-snap-type: x mandatory;
349
+ scrollbar-width: none;
350
+ }
351
+
352
+ .ui-carousel__viewport::-webkit-scrollbar {
353
+ display: none;
354
+ }
355
+
356
+ .ui-carousel__viewport:focus-visible {
357
+ outline: 2px solid var(--focus-ring);
358
+ outline-offset: -2px;
359
+ }
360
+
361
+ .ui-carousel__slide {
362
+ flex: 0 0 100%;
363
+ scroll-snap-align: center;
364
+ }
365
+
366
+ .ui-carousel__slide > :is(img, picture, .ui-ratio) {
367
+ display: block;
368
+ inline-size: 100%;
369
+ object-fit: cover;
370
+ }
371
+
372
+ /* Prev / next — hairline round controls over the stage edges. */
373
+ .ui-carousel__prev,
374
+ .ui-carousel__next {
375
+ align-items: center;
376
+ background: color-mix(in srgb, var(--panel-strong) 80%, transparent);
377
+ border: 1px solid var(--line-strong);
378
+ border-radius: var(--radius-pill);
379
+ color: var(--text);
380
+ cursor: pointer;
381
+ display: inline-flex;
382
+ block-size: 2.2rem;
383
+ inline-size: 2.2rem;
384
+ inset-block-start: 50%;
385
+ justify-content: center;
386
+ position: absolute;
387
+ transform: translateY(-50%);
388
+ z-index: var(--z-raised);
389
+ }
390
+
391
+ .ui-carousel__prev {
392
+ inset-inline-start: var(--space-xs);
393
+ }
394
+
395
+ .ui-carousel__next {
396
+ inset-inline-end: var(--space-xs);
397
+ }
398
+
399
+ /* Chevron glyph, mirrors for RTL via the logical border edges. */
400
+ .ui-carousel__prev::before,
401
+ .ui-carousel__next::before {
402
+ block-size: 0.5rem;
403
+ border-block-start: 1.5px solid currentColor;
404
+ border-inline-end: 1.5px solid currentColor;
405
+ content: '';
406
+ inline-size: 0.5rem;
407
+ }
408
+
409
+ .ui-carousel__prev::before {
410
+ margin-inline-start: 0.2rem;
411
+ transform: rotate(-135deg);
412
+ }
413
+
414
+ .ui-carousel__next::before {
415
+ margin-inline-end: 0.2rem;
416
+ transform: rotate(45deg);
417
+ }
418
+
419
+ /* The chevron direction is a physical rotation, so flip it for RTL (the
420
+ buttons already swap sides via the logical insets above). */
421
+ [dir='rtl'] .ui-carousel__prev::before {
422
+ transform: rotate(45deg);
423
+ }
424
+
425
+ [dir='rtl'] .ui-carousel__next::before {
426
+ transform: rotate(-135deg);
427
+ }
428
+
429
+ .ui-carousel__prev:disabled,
430
+ .ui-carousel__next:disabled {
431
+ cursor: not-allowed;
432
+ opacity: 0.35;
433
+ }
434
+
435
+ .ui-carousel__prev:focus-visible,
436
+ .ui-carousel__next:focus-visible {
437
+ outline: 2px solid var(--focus-ring);
438
+ outline-offset: 2px;
439
+ }
440
+
441
+ /* Position counter ("3 / 12") — initCarousel fills the text; aria-live. */
442
+ .ui-carousel__status {
443
+ background: color-mix(in srgb, var(--panel-strong) 75%, transparent);
444
+ border-radius: var(--radius-pill);
445
+ color: var(--text);
446
+ font-family: var(--mono);
447
+ font-size: var(--text-2xs);
448
+ inset-block-end: var(--space-xs);
449
+ inset-inline-start: var(--space-xs);
450
+ letter-spacing: var(--tracking-wide);
451
+ padding: 0.2rem 0.5rem;
452
+ position: absolute;
453
+ }
454
+
455
+ /* Thumbnail strip — clicking a thumb jumps the track; active = accent. */
456
+ .ui-carousel__thumbs {
457
+ display: flex;
458
+ gap: 0.4rem;
459
+ list-style: none;
460
+ margin: 0;
461
+ overflow-x: auto;
462
+ padding: 0;
463
+ scroll-behavior: smooth;
464
+ scrollbar-width: none;
465
+ }
466
+
467
+ .ui-carousel__thumbs::-webkit-scrollbar {
468
+ display: none;
469
+ }
470
+
471
+ .ui-carousel__thumb {
472
+ background: transparent;
473
+ border: 1px solid var(--line);
474
+ border-radius: var(--radius-sm);
475
+ cursor: pointer;
476
+ flex: 0 0 auto;
477
+ block-size: 3rem;
478
+ inline-size: 4rem;
479
+ overflow: hidden;
480
+ padding: 0;
481
+ }
482
+
483
+ .ui-carousel__thumb > img {
484
+ block-size: 100%;
485
+ inline-size: 100%;
486
+ object-fit: cover;
487
+ }
488
+
489
+ .ui-carousel__thumb[aria-current='true'] {
490
+ border-color: var(--accent);
491
+ }
492
+
493
+ .ui-carousel__thumb:focus-visible {
494
+ outline: 2px solid var(--focus-ring);
495
+ outline-offset: 2px;
496
+ }
497
+
498
+ @media (prefers-reduced-motion: reduce) {
499
+ .ui-carousel__viewport,
500
+ .ui-carousel__thumbs {
501
+ scroll-behavior: auto;
502
+ }
503
+ }
package/css/feedback.css CHANGED
@@ -338,3 +338,114 @@
338
338
  animation: none;
339
339
  }
340
340
  }
341
+
342
+ /* --- Meter — a labelled proportion / threshold bar. Unlike ui-progress
343
+ (task progress, can be indeterminate), a meter shows a measured static
344
+ value for data display (coverage, capacity, a KPI against a target).
345
+ Drive the fill with the same --value knob as progress; tone the fill by
346
+ threshold. Author role="meter" + aria-valuenow/min/max for AT. --- */
347
+
348
+ .ui-meter {
349
+ background: var(--panel-soft);
350
+ border: 1px solid var(--line);
351
+ border-radius: var(--radius-pill);
352
+ block-size: 0.5rem;
353
+ overflow: hidden;
354
+ inline-size: 100%;
355
+ }
356
+
357
+ /* Default (untoned) fill is --text-dim, not --line-strong: the fill
358
+ conveys the measured value, so it must clear the 3:1 non-text floor
359
+ against the --panel-soft track (--line-strong is only ~2:1). --text-dim
360
+ on --surface-muted is already a gated contrast pairing (≥4.5:1). */
361
+ .ui-meter__fill {
362
+ background: var(--text-dim);
363
+ block-size: 100%;
364
+ transition: inline-size var(--duration-base) var(--ease-out);
365
+ inline-size: calc(clamp(0, var(--value, 0), 100) * 1%);
366
+ }
367
+
368
+ .ui-meter--accent .ui-meter__fill {
369
+ background: var(--accent);
370
+ }
371
+
372
+ .ui-meter--success .ui-meter__fill {
373
+ background: var(--success);
374
+ }
375
+
376
+ .ui-meter--warning .ui-meter__fill {
377
+ background: var(--warning);
378
+ }
379
+
380
+ .ui-meter--danger .ui-meter__fill {
381
+ background: var(--danger);
382
+ }
383
+
384
+ /* --- Steps — progress through a multi-step flow. Use an <ol>; the
385
+ current step is aria-current="step" (no class), completed steps take
386
+ --done. Counter-numbered markers, hairline connectors. --- */
387
+
388
+ .ui-steps {
389
+ counter-reset: ui-step;
390
+ display: flex;
391
+ flex-wrap: wrap;
392
+ gap: var(--space-sm);
393
+ list-style: none;
394
+ margin: 0;
395
+ padding: 0;
396
+ }
397
+
398
+ .ui-steps__item {
399
+ align-items: center;
400
+ color: var(--text-dim);
401
+ counter-increment: ui-step;
402
+ display: inline-flex;
403
+ flex: 1 1 auto;
404
+ font-family: var(--mono);
405
+ font-size: var(--text-xs);
406
+ gap: 0.5rem;
407
+ letter-spacing: var(--tracking-wide);
408
+ min-inline-size: max-content;
409
+ text-transform: uppercase;
410
+ }
411
+
412
+ .ui-steps__item::before {
413
+ align-items: center;
414
+ border: 1px solid var(--line-strong);
415
+ border-radius: 50%;
416
+ color: var(--text-dim);
417
+ content: counter(ui-step);
418
+ display: inline-flex;
419
+ flex: 0 0 auto;
420
+ block-size: 1.5rem;
421
+ inline-size: 1.5rem;
422
+ justify-content: center;
423
+ }
424
+
425
+ /* Connector hairline trailing every item except the last. */
426
+ .ui-steps__item:not(:last-child)::after {
427
+ background: var(--line);
428
+ block-size: 1px;
429
+ content: '';
430
+ flex: 1 1 1.5rem;
431
+ min-inline-size: 1rem;
432
+ }
433
+
434
+ .ui-steps__item[aria-current='step'] {
435
+ color: var(--text);
436
+ }
437
+
438
+ .ui-steps__item[aria-current='step']::before {
439
+ border-color: var(--accent);
440
+ color: var(--accent-text);
441
+ }
442
+
443
+ .ui-steps__item--done {
444
+ color: var(--text-soft);
445
+ }
446
+
447
+ .ui-steps__item--done::before {
448
+ background: var(--accent);
449
+ border-color: var(--accent);
450
+ color: var(--button-text);
451
+ }
package/css/forms.css CHANGED
@@ -131,6 +131,50 @@
131
131
  white-space: nowrap;
132
132
  }
133
133
 
134
+ /* --- Input icon: a leading (or trailing) glyph *inside* a single
135
+ control, distinct from input-group's adjacent affix. Wrap the input:
136
+ <span class="ui-input-icon"><svg class="ui-input-icon__icon"/>
137
+ <input class="ui-input"></span>. The icon is a decorative overlay; the
138
+ input keeps full width and gains padding on the icon side. --end moves
139
+ the icon to the inline-end. RTL falls out of the logical props. --- */
140
+ .ui-input-icon {
141
+ display: block;
142
+ position: relative;
143
+ }
144
+
145
+ .ui-input-icon > .ui-input {
146
+ padding-inline-start: 2.2rem;
147
+ }
148
+
149
+ .ui-input-icon--end > .ui-input {
150
+ padding-inline: 0.7rem 2.2rem;
151
+ }
152
+
153
+ .ui-input-icon__icon {
154
+ align-items: center;
155
+ color: var(--text-dim);
156
+ display: inline-flex;
157
+ inset-block-start: 50%;
158
+ inset-inline-start: 0.7rem;
159
+ justify-content: center;
160
+ pointer-events: none;
161
+ position: absolute;
162
+ transform: translateY(-50%);
163
+ }
164
+
165
+ .ui-input-icon--end .ui-input-icon__icon {
166
+ inset-inline: auto 0.7rem;
167
+ }
168
+
169
+ .ui-input-icon__icon svg {
170
+ block-size: 1rem;
171
+ inline-size: 1rem;
172
+ }
173
+
174
+ .ui-input-icon:focus-within .ui-input-icon__icon {
175
+ color: var(--accent);
176
+ }
177
+
134
178
  /* --- File input --- */
135
179
  .ui-file {
136
180
  color: var(--text-soft);
package/css/overlay.css CHANGED
@@ -105,6 +105,54 @@
105
105
  }
106
106
  }
107
107
 
108
+ /* --- Lightbox — a full-viewport <dialog> wrapping a .ui-carousel. The
109
+ native <dialog> brings the top layer, focus-trap and Escape; initDialog
110
+ adds open (`data-bronto-open`) + focus-return; initCarousel drives the
111
+ inner carousel. No bespoke overlay/focus code — that is the whole point
112
+ of building it on <dialog>. --- */
113
+ .ui-lightbox {
114
+ background: color-mix(in srgb, var(--bg) 92%, #000);
115
+ border: 0;
116
+ block-size: 100vh;
117
+ color: var(--text);
118
+ max-block-size: 100vh;
119
+ max-inline-size: 100vw;
120
+ padding: 0;
121
+ inline-size: 100vw;
122
+ }
123
+
124
+ .ui-lightbox::backdrop {
125
+ background: color-mix(in srgb, #000 80%, transparent);
126
+ }
127
+
128
+ .ui-lightbox[open] {
129
+ align-content: center;
130
+ display: grid;
131
+ }
132
+
133
+ .ui-lightbox .ui-carousel {
134
+ inline-size: 100%;
135
+ margin: auto;
136
+ max-inline-size: min(64rem, 100vw);
137
+ }
138
+
139
+ /* Lightbox images are shown whole (contain), not cropped like the inline
140
+ carousel's cover thumbnails. */
141
+ .ui-lightbox .ui-carousel__slide > img {
142
+ margin-inline: auto;
143
+ max-block-size: 80vh;
144
+ max-inline-size: 100%;
145
+ inline-size: auto;
146
+ object-fit: contain;
147
+ }
148
+
149
+ .ui-lightbox__close {
150
+ inset-block-start: var(--space-sm);
151
+ inset-inline-end: var(--space-sm);
152
+ position: fixed;
153
+ z-index: var(--z-raised);
154
+ }
155
+
108
156
  /* --- Dropdown menu — pairs with native <details> --- */
109
157
 
110
158
  /* The positioned host for a <details> dropdown: .ui-menu is absolute, so
@@ -631,3 +631,63 @@
631
631
  color: var(--text-dim);
632
632
  margin: 0.3rem 0 0;
633
633
  }
634
+
635
+ /* --- Keyboard key — inline glyph for shortcut hints. Wrap a <kbd>. --- */
636
+
637
+ .ui-kbd {
638
+ align-items: center;
639
+ background: var(--panel-soft);
640
+ border: 1px solid var(--line-strong);
641
+ border-radius: var(--radius-sm);
642
+ color: var(--text-soft);
643
+ display: inline-flex;
644
+ font-family: var(--mono);
645
+ font-size: var(--text-2xs);
646
+ justify-content: center;
647
+ line-height: 1;
648
+ min-inline-size: 1.4rem;
649
+ padding: 0.18rem 0.4rem;
650
+ }
651
+
652
+ /* --- Timeline — vertical event list on a hairline spine. Use an <ol>;
653
+ each <li> is a __item with an optional __time label. aria-current marks
654
+ the live/most-recent event (accent marker). --- */
655
+
656
+ .ui-timeline {
657
+ display: grid;
658
+ gap: var(--space-md);
659
+ list-style: none;
660
+ margin: 0;
661
+ padding: 0;
662
+ }
663
+
664
+ .ui-timeline__item {
665
+ border-inline-start: 1px solid var(--line);
666
+ display: grid;
667
+ gap: 0.2rem;
668
+ padding-inline-start: var(--space-md);
669
+ position: relative;
670
+ }
671
+
672
+ .ui-timeline__item::before {
673
+ background: var(--line-strong);
674
+ border-radius: 50%;
675
+ block-size: 0.5rem;
676
+ content: '';
677
+ inline-size: 0.5rem;
678
+ inset-block-start: 0.18rem;
679
+ inset-inline-start: calc(-0.25rem - 0.5px);
680
+ position: absolute;
681
+ }
682
+
683
+ .ui-timeline__item[aria-current]::before {
684
+ background: var(--accent);
685
+ }
686
+
687
+ .ui-timeline__time {
688
+ color: var(--text-dim);
689
+ font-family: var(--mono);
690
+ font-size: var(--text-2xs);
691
+ letter-spacing: var(--tracking-wide);
692
+ text-transform: uppercase;
693
+ }
package/css/site.css CHANGED
@@ -91,6 +91,39 @@
91
91
  gap: var(--space-xs);
92
92
  }
93
93
 
94
+ /* --- Page header — the in-page title bar every view re-hand-rolls:
95
+ eyebrow/breadcrumb + title on the start, actions on the end, one
96
+ hairline beneath. Compose the existing ui-eyebrow / ui-breadcrumb in
97
+ the title block (don't duplicate them); __actions is a cluster. --- */
98
+
99
+ .ui-pagehead {
100
+ align-items: flex-end;
101
+ border-block-end: 1px solid var(--line);
102
+ display: flex;
103
+ flex-wrap: wrap;
104
+ gap: var(--space-md);
105
+ justify-content: space-between;
106
+ padding-block-end: var(--space-md);
107
+ }
108
+
109
+ .ui-pagehead__title {
110
+ color: var(--text);
111
+ font-family: var(--display);
112
+ font-size: var(--text-xl);
113
+ letter-spacing: var(--tracking-wide);
114
+ line-height: 1.1;
115
+ margin: 0;
116
+ text-transform: uppercase;
117
+ text-wrap: balance;
118
+ }
119
+
120
+ .ui-pagehead__actions {
121
+ align-items: center;
122
+ display: inline-flex;
123
+ flex-wrap: wrap;
124
+ gap: var(--space-xs);
125
+ }
126
+
94
127
  /* --- Nav — desktop inline, dot active cue via aria-current --- */
95
128
 
96
129
  .ui-sitenav {