@ponchia/ui 0.3.4 → 0.3.6

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.
@@ -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/dots.css CHANGED
@@ -47,17 +47,21 @@
47
47
  /* Data-bound dot matrix — a grid of state cells (activity / heatmap),
48
48
  the on-brand counterpart to the decorative .ui-dotgrid background.
49
49
  This is the grid + cell tones only; the data→cell mapping is the
50
- consumer's. Tune density via --dotmatrix-cols / --dotmatrix-gap. */
50
+ consumer's. Tune density via --dotmatrix-cols / --dotmatrix-gap; set
51
+ --dotmatrix-dot to a fixed dot length (intrinsic sizing, e.g. for the
52
+ display glyphs in @ponchia/ui/glyphs) instead of stretching to fit;
53
+ set --dotmatrix-dot-radius: 0 (with --dotmatrix-gap: 0) to fuse the dots
54
+ into a crisp pixel glyph that stays legible at small/inline sizes. */
51
55
  .ui-dotmatrix {
52
56
  display: grid;
53
57
  gap: var(--dotmatrix-gap, 0.5rem);
54
- grid-template-columns: repeat(var(--dotmatrix-cols, 12), minmax(0, 1fr));
58
+ grid-template-columns: repeat(var(--dotmatrix-cols, 12), var(--dotmatrix-dot, minmax(0, 1fr)));
55
59
  }
56
60
 
57
61
  .ui-dotmatrix__cell {
58
62
  aspect-ratio: 1;
59
63
  background: var(--field-dot);
60
- border-radius: 50%;
64
+ border-radius: var(--dotmatrix-dot-radius, 50%);
61
65
  }
62
66
 
63
67
  .ui-dotmatrix__cell--hot {
@@ -68,6 +72,38 @@
68
72
  background: var(--field-dot-accent);
69
73
  }
70
74
 
75
+ /* Animation (opt-in, decorative): a dot-matrix that "plays". Meaning stays
76
+ in the static cells + the element's label — motion is enhancement only,
77
+ and the reduced-motion block below switches it off. */
78
+
79
+ /* --reveal: cells power on in a left→right, top→bottom scan. The per-cell
80
+ delay reads `--i` (the row-major index) which the renderer sets. */
81
+ .ui-dotmatrix--reveal .ui-dotmatrix__cell {
82
+ animation: dotmatrixCellOn var(--duration-base) var(--ease-out) both;
83
+ animation-delay: calc(var(--i, 0) * var(--dotmatrix-reveal-step, 3ms));
84
+ }
85
+
86
+ /* `from`-only on purpose: when reduced-motion kills the animation the cells
87
+ stay at their natural opacity:1 — i.e. the settled end-state still shows. */
88
+ @keyframes dotmatrixCellOn {
89
+ from {
90
+ opacity: 0;
91
+ transform: scale(0.35);
92
+ }
93
+ }
94
+
95
+ /* --pulse: the whole glyph breathes, for a "live"/attention state. */
96
+ .ui-dotmatrix--pulse {
97
+ animation: dotmatrixPulse 1.8s var(--ease-standard) infinite;
98
+ }
99
+
100
+ @keyframes dotmatrixPulse {
101
+ 50% {
102
+ opacity: 0.55;
103
+ transform: scale(0.94);
104
+ }
105
+ }
106
+
71
107
  /* Status dot — the glyph-style state indicator. */
72
108
  .ui-dot {
73
109
  background: var(--text-dim);
@@ -289,7 +325,9 @@
289
325
  .ui-dot--live::after,
290
326
  .ui-dotloader span,
291
327
  .ui-dotspinner,
292
- .ui-dotbar--indeterminate i {
328
+ .ui-dotbar--indeterminate i,
329
+ .ui-dotmatrix--reveal .ui-dotmatrix__cell,
330
+ .ui-dotmatrix--pulse {
293
331
  animation: none;
294
332
  }
295
333
 
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 {