@ponchia/ui 0.4.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +352 -12
  2. package/MIGRATIONS.json +92 -0
  3. package/README.md +27 -17
  4. package/annotations/index.d.ts +280 -0
  5. package/annotations/index.js +522 -0
  6. package/behaviors/carousel.js +197 -0
  7. package/behaviors/combobox.js +195 -0
  8. package/behaviors/command.js +187 -0
  9. package/behaviors/connectors.js +96 -0
  10. package/behaviors/crosshair.js +58 -0
  11. package/behaviors/dialog.js +73 -0
  12. package/behaviors/disclosure.js +25 -0
  13. package/behaviors/dismissible.js +24 -0
  14. package/behaviors/forms.js +158 -0
  15. package/behaviors/glyph.js +109 -0
  16. package/behaviors/index.d.ts +82 -1
  17. package/behaviors/index.js +18 -1336
  18. package/behaviors/internal.js +50 -0
  19. package/behaviors/legend.js +46 -0
  20. package/behaviors/menu.js +46 -0
  21. package/behaviors/popover.js +108 -0
  22. package/behaviors/spotlight.js +53 -0
  23. package/behaviors/table.js +109 -0
  24. package/behaviors/tabs.js +103 -0
  25. package/behaviors/theme.js +82 -0
  26. package/behaviors/toast.js +152 -0
  27. package/classes/index.d.ts +318 -0
  28. package/classes/index.js +352 -0
  29. package/classes/vscode.css-custom-data.json +18 -14
  30. package/connectors/index.d.ts +71 -0
  31. package/connectors/index.js +179 -0
  32. package/css/analytical.css +21 -0
  33. package/css/annotations.css +292 -0
  34. package/css/command.css +97 -0
  35. package/css/connectors.css +93 -0
  36. package/css/crosshair.css +100 -0
  37. package/css/disclosure.css +29 -0
  38. package/css/dots.css +2 -0
  39. package/css/feedback.css +104 -0
  40. package/css/fonts.css +11 -7
  41. package/css/generated.css +117 -0
  42. package/css/legend.css +268 -0
  43. package/css/marks.css +144 -0
  44. package/css/motion.css +88 -0
  45. package/css/overlay.css +52 -1
  46. package/css/primitives.css +18 -0
  47. package/css/report.css +363 -0
  48. package/css/selection.css +46 -0
  49. package/css/sources.css +179 -0
  50. package/css/spotlight.css +104 -0
  51. package/css/state.css +121 -0
  52. package/css/tokens.css +78 -60
  53. package/css/workbench.css +83 -0
  54. package/dist/bronto.css +1 -1
  55. package/dist/css/analytical.css +1 -0
  56. package/dist/css/annotations.css +1 -0
  57. package/dist/css/command.css +1 -0
  58. package/dist/css/connectors.css +1 -0
  59. package/dist/css/crosshair.css +1 -0
  60. package/dist/css/disclosure.css +1 -1
  61. package/dist/css/dots.css +1 -1
  62. package/dist/css/feedback.css +1 -1
  63. package/dist/css/fonts.css +1 -1
  64. package/dist/css/generated.css +1 -0
  65. package/dist/css/legend.css +1 -0
  66. package/dist/css/marks.css +1 -0
  67. package/dist/css/motion.css +1 -1
  68. package/dist/css/overlay.css +1 -1
  69. package/dist/css/primitives.css +1 -1
  70. package/dist/css/report.css +1 -0
  71. package/dist/css/selection.css +1 -0
  72. package/dist/css/sources.css +1 -0
  73. package/dist/css/spotlight.css +1 -0
  74. package/dist/css/state.css +1 -0
  75. package/dist/css/tokens.css +1 -1
  76. package/dist/css/workbench.css +1 -0
  77. package/docs/adr/0001-color-system.md +26 -27
  78. package/docs/adr/0002-scope-and-2026-baseline.md +104 -0
  79. package/docs/adr/0003-theme-model.md +97 -0
  80. package/docs/annotations.md +345 -0
  81. package/docs/architecture.md +202 -0
  82. package/docs/command.md +95 -0
  83. package/docs/connectors.md +91 -0
  84. package/docs/contrast.md +42 -42
  85. package/docs/crosshair.md +63 -0
  86. package/docs/generated.md +91 -0
  87. package/docs/legends.md +168 -0
  88. package/docs/marks.md +86 -0
  89. package/docs/reference.md +418 -15
  90. package/docs/reporting.md +305 -0
  91. package/docs/selection.md +40 -0
  92. package/docs/sources.md +110 -0
  93. package/docs/spotlight.md +78 -0
  94. package/docs/stability.md +52 -0
  95. package/docs/state.md +85 -0
  96. package/docs/theming.md +18 -6
  97. package/docs/usage.md +43 -3
  98. package/docs/workbench.md +72 -0
  99. package/fonts/doto-400.woff2 +0 -0
  100. package/fonts/doto-500.woff2 +0 -0
  101. package/fonts/doto-600.woff2 +0 -0
  102. package/fonts/doto-700.woff2 +0 -0
  103. package/fonts/doto-800.woff2 +0 -0
  104. package/fonts/doto-900.woff2 +0 -0
  105. package/llms.txt +288 -8
  106. package/package.json +95 -5
  107. package/qwik/index.d.ts +60 -0
  108. package/qwik/index.js +149 -0
  109. package/react/index.d.ts +40 -14
  110. package/react/index.js +32 -5
  111. package/solid/index.d.ts +40 -14
  112. package/solid/index.js +31 -3
  113. package/tokens/index.d.ts +3 -3
  114. package/tokens/index.js +25 -19
  115. package/tokens/index.json +32 -28
  116. package/tokens/resolved.json +34 -32
  117. package/tokens/tokens.dtcg.json +22 -14
  118. package/fonts/doto-400.ttf +0 -0
  119. package/fonts/doto-500.ttf +0 -0
  120. package/fonts/doto-600.ttf +0 -0
  121. package/fonts/doto-700.ttf +0 -0
  122. package/fonts/doto-800.ttf +0 -0
  123. package/fonts/doto-900.ttf +0 -0
package/css/legend.css ADDED
@@ -0,0 +1,268 @@
1
+ /* ==========================================================================
2
+ legend — opt-in data keys for charts, reports, and analytical figures.
3
+
4
+ A standalone, portable successor to the report kit's inline chart legend:
5
+ `.ui-legend` reads the Tier-4 `--chart-*` tokens and pairs with any chart
6
+ (Bronto's own `.ui-chart` bars, an SVG/canvas figure, or an external chart
7
+ engine). Not imported by core.css — import it beside css/dataviz.css.
8
+
9
+ Bronto paints and positions; it owns no scales, data mapping, or series
10
+ state. Colour is never the sole channel: a swatch mirrors its chart mark
11
+ (its colour, plus a `--chart-pattern-*` where the mark uses one) and is
12
+ always read out by its text label (WCAG 1.4.1). An interactive,
13
+ series-toggling legend is an optional behavior; the host owns what a
14
+ toggle hides and announces.
15
+ ========================================================================== */
16
+
17
+ .ui-legend {
18
+ color: var(--text-soft);
19
+ display: flex;
20
+ flex-wrap: wrap;
21
+ font-family: var(--mono);
22
+ font-size: var(--text-xs);
23
+ gap: var(--space-2xs) var(--space-md);
24
+ list-style: none;
25
+ margin: 0;
26
+ padding: 0;
27
+ }
28
+
29
+ .ui-legend--vertical {
30
+ flex-direction: column;
31
+ }
32
+
33
+ .ui-legend--compact {
34
+ font-size: var(--text-2xs);
35
+ gap: var(--space-2xs) var(--space-sm);
36
+ }
37
+
38
+ .ui-legend__title {
39
+ color: var(--text-dim);
40
+ font-size: var(--text-2xs);
41
+ letter-spacing: 0;
42
+ margin: 0;
43
+ text-transform: uppercase;
44
+ }
45
+
46
+ .ui-legend__item {
47
+ align-items: center;
48
+ color: inherit;
49
+ display: inline-flex;
50
+ gap: 0.45rem;
51
+ }
52
+
53
+ .ui-legend__label {
54
+ color: inherit;
55
+ }
56
+
57
+ .ui-legend__value {
58
+ color: var(--text-dim);
59
+ font-variant-numeric: tabular-nums;
60
+ margin-inline-start: auto;
61
+ }
62
+
63
+ .ui-legend__caption {
64
+ color: var(--text-dim);
65
+ font-size: var(--text-2xs);
66
+ letter-spacing: 0;
67
+ text-transform: uppercase;
68
+ }
69
+
70
+ /* Swatch — same token contract as the chart fill: set `--chart-color` (and,
71
+ where the mark is patterned, `--chart-pattern`) inline, or use a `--N` index
72
+ helper for the categorical palette. */
73
+ .ui-legend__swatch {
74
+ background: var(--chart-color, var(--chart-1, var(--accent)));
75
+ background-image: var(--chart-pattern, none);
76
+ background-size: var(--chart-pattern-size, 8px);
77
+ block-size: 0.8rem;
78
+ border: 1px solid var(--line-strong);
79
+ flex: 0 0 auto;
80
+ inline-size: 0.8rem;
81
+ }
82
+
83
+ .ui-legend__swatch--circle {
84
+ border-radius: 50%;
85
+ }
86
+
87
+ .ui-legend__swatch--line {
88
+ block-size: 0.2rem;
89
+ border: 0;
90
+ inline-size: 1.1rem;
91
+ }
92
+
93
+ /* Glyph/symbol swatch — fill an `.ui-icon` mask with the series colour. */
94
+ .ui-legend__symbol {
95
+ block-size: 0.95rem;
96
+ color: var(--chart-color, var(--accent));
97
+ flex: 0 0 auto;
98
+ inline-size: 0.95rem;
99
+ }
100
+
101
+ /* Categorical index helpers: exactly one per palette series (gated against
102
+ tokens/charts.js by check-legend). They set only the colour, so a swatch
103
+ stays a faithful key — add `--chart-pattern` (inline, or from dataviz.css)
104
+ only where the chart mark itself is patterned. */
105
+ .ui-legend__swatch--1 {
106
+ --chart-color: var(--chart-1);
107
+ }
108
+
109
+ .ui-legend__swatch--2 {
110
+ --chart-color: var(--chart-2);
111
+ }
112
+
113
+ .ui-legend__swatch--3 {
114
+ --chart-color: var(--chart-3);
115
+ }
116
+
117
+ .ui-legend__swatch--4 {
118
+ --chart-color: var(--chart-4);
119
+ }
120
+
121
+ .ui-legend__swatch--5 {
122
+ --chart-color: var(--chart-5);
123
+ }
124
+
125
+ .ui-legend__swatch--6 {
126
+ --chart-color: var(--chart-6);
127
+ }
128
+
129
+ .ui-legend__swatch--7 {
130
+ --chart-color: var(--chart-7);
131
+ }
132
+
133
+ .ui-legend__swatch--8 {
134
+ --chart-color: var(--chart-8);
135
+ }
136
+
137
+ /* Continuous colour ramp. Author supplies the min/mid/max tick *text*; the
138
+ track interpolates the sequential ramp in OKLCH (a single perceptual hue,
139
+ so no banding and no muddy midpoint). `--diverging` swaps in the 7-stop
140
+ diverging ramp around its neutral centre. */
141
+ .ui-legend--gradient {
142
+ display: grid;
143
+ gap: var(--space-2xs);
144
+ }
145
+
146
+ .ui-legend__track {
147
+ background: linear-gradient(
148
+ 90deg in oklch,
149
+ var(--chart-seq-1),
150
+ var(--chart-seq-2),
151
+ var(--chart-seq-3),
152
+ var(--chart-seq-4),
153
+ var(--chart-seq-5),
154
+ var(--chart-seq-6)
155
+ );
156
+ block-size: 0.7rem;
157
+ border: 1px solid var(--line);
158
+ }
159
+
160
+ .ui-legend--diverging .ui-legend__track {
161
+ background: linear-gradient(
162
+ 90deg in oklch,
163
+ var(--chart-div-1),
164
+ var(--chart-div-2),
165
+ var(--chart-div-3),
166
+ var(--chart-div-4),
167
+ var(--chart-div-5),
168
+ var(--chart-div-6),
169
+ var(--chart-div-7)
170
+ );
171
+ }
172
+
173
+ .ui-legend__ticks {
174
+ color: var(--text-dim);
175
+ display: flex;
176
+ font-size: var(--text-2xs);
177
+ justify-content: space-between;
178
+ }
179
+
180
+ .ui-legend__tick {
181
+ font-variant-numeric: tabular-nums;
182
+ }
183
+
184
+ /* Threshold / binned key — a `swatch | range-label` grid. Author the swatch
185
+ and label as direct children (no `__item` wrapper) so the columns align. */
186
+ .ui-legend--threshold {
187
+ align-items: center;
188
+ display: grid;
189
+ gap: 0.35rem var(--space-sm);
190
+ grid-template-columns: auto 1fr;
191
+ }
192
+
193
+ /* With-values — align swatch | label | value columns across every row. */
194
+ .ui-legend--with-values {
195
+ display: grid;
196
+ gap: 0.35rem var(--space-md);
197
+ grid-template-columns: auto 1fr auto;
198
+ }
199
+
200
+ .ui-legend--with-values .ui-legend__item {
201
+ display: grid;
202
+ grid-column: 1 / -1;
203
+ grid-template-columns: subgrid;
204
+ }
205
+
206
+ .ui-legend--with-values .ui-legend__value {
207
+ margin-inline-start: 0;
208
+ }
209
+
210
+ /* Interactive entries are real <button aria-pressed> controls. Bronto styles
211
+ the control + the inactive state; behaviors/legend.js (optional) flips
212
+ aria-pressed and emits an event. The host hides the series and announces it. */
213
+ .ui-legend--interactive .ui-legend__item {
214
+ background: none;
215
+ border: 0;
216
+ border-radius: var(--radius-sm);
217
+ color: inherit;
218
+ cursor: pointer;
219
+ font: inherit;
220
+ padding-block: 0.15rem;
221
+ padding-inline: 0.3rem;
222
+ text-align: start;
223
+ }
224
+
225
+ .ui-legend--interactive .ui-legend__item:focus-visible {
226
+ outline: 2px solid var(--accent);
227
+ outline-offset: 2px;
228
+ }
229
+
230
+ .ui-legend__item.is-inactive,
231
+ .ui-legend__item[aria-pressed='false'] {
232
+ opacity: 0.45;
233
+ }
234
+
235
+ .ui-legend__item.is-inactive .ui-legend__label,
236
+ .ui-legend__item[aria-pressed='false'] .ui-legend__label {
237
+ text-decoration: line-through;
238
+ }
239
+
240
+ /* Forced colours: keep the real swatch hue (it mirrors the chart mark) behind
241
+ a system-coloured border so it survives, and drop the opacity dim (it does
242
+ not read) — the line-through still marks an inactive series. */
243
+ @media (forced-colors: active) {
244
+ .ui-legend__swatch {
245
+ border: 1px solid CanvasText;
246
+ forced-color-adjust: none;
247
+ }
248
+
249
+ /* Keep the ramp's hues instead of collapsing to one system colour (the tick
250
+ labels still carry the scale either way). */
251
+ .ui-legend__track {
252
+ forced-color-adjust: none;
253
+ }
254
+
255
+ .ui-legend__item.is-inactive,
256
+ .ui-legend__item[aria-pressed='false'] {
257
+ opacity: 1;
258
+ }
259
+ }
260
+
261
+ /* Print: meaning-carrying colour must survive the "economy" default. */
262
+ @media print {
263
+ .ui-legend__swatch,
264
+ .ui-legend__track {
265
+ -webkit-print-color-adjust: exact;
266
+ print-color-adjust: exact;
267
+ }
268
+ }
package/css/marks.css ADDED
@@ -0,0 +1,144 @@
1
+ /* ==========================================================================
2
+ marks — opt-in evidence/emphasis marks for running text.
3
+
4
+ The prose counterpart to the SVG annotations layer: annotations call out a
5
+ figure, marks call out a sentence. Sober, report-grade highlights for docs,
6
+ audits, and generated (LLM) reports — not a hand-drawn Rough-Notation look.
7
+ Not imported by core.css.
8
+
9
+ `.ui-mark` is an inline highlight (put it on a `<mark>`); `.ui-bracket-note`
10
+ brackets a whole passage. Monochrome by default — the rationed accent is
11
+ opt-in via `--accent`; status tones are only for status-bearing emphasis.
12
+ ========================================================================== */
13
+
14
+ .ui-mark {
15
+ --mark-color: var(--line-strong);
16
+
17
+ background-image: linear-gradient(
18
+ 90deg,
19
+ color-mix(in srgb, var(--mark-color) 30%, transparent),
20
+ color-mix(in srgb, var(--mark-color) 30%, transparent)
21
+ );
22
+ background-repeat: no-repeat;
23
+ background-size: 100% 100%;
24
+ border-radius: 0.12em;
25
+ box-decoration-break: clone;
26
+ color: inherit;
27
+ padding-block: 0.05em;
28
+ padding-inline: 0.18em;
29
+ }
30
+
31
+ /* Tones set the mark colour. Default is neutral ink; `--accent` is the
32
+ rationed accent; status tones only for status-bearing emphasis. */
33
+ .ui-mark--accent {
34
+ --mark-color: var(--accent);
35
+ }
36
+
37
+ .ui-mark--success {
38
+ --mark-color: var(--success);
39
+ }
40
+
41
+ .ui-mark--warning {
42
+ --mark-color: var(--warning);
43
+ }
44
+
45
+ .ui-mark--danger {
46
+ --mark-color: var(--danger);
47
+ }
48
+
49
+ .ui-mark--info {
50
+ --mark-color: var(--info);
51
+ }
52
+
53
+ .ui-mark--muted {
54
+ --mark-color: var(--text-dim);
55
+ }
56
+
57
+ /* Styles change how the mark is drawn (no highlight fill). They wrap across
58
+ lines natively (text-decoration) or with the cloned box (border). */
59
+ .ui-mark--underline {
60
+ background: none;
61
+ padding: 0;
62
+ text-decoration-color: var(--mark-color);
63
+ text-decoration-line: underline;
64
+ text-decoration-thickness: 0.12em;
65
+ text-underline-offset: 0.18em;
66
+ }
67
+
68
+ .ui-mark--box {
69
+ background: none;
70
+ border: 1px solid var(--mark-color);
71
+ border-radius: var(--radius-sm);
72
+ }
73
+
74
+ .ui-mark--strike {
75
+ background: none;
76
+ padding: 0;
77
+ text-decoration-color: var(--mark-color);
78
+ text-decoration-line: line-through;
79
+ }
80
+
81
+ /* Draw-on highlight sweep. Only the highlight fill animates, and only when
82
+ motion is welcome — reduced-motion keeps the resting full highlight. */
83
+ @media (prefers-reduced-motion: no-preference) {
84
+ .ui-mark--draw {
85
+ animation: ui-mark-draw 0.6s var(--ease, ease) both;
86
+ }
87
+ }
88
+
89
+ @keyframes ui-mark-draw {
90
+ from {
91
+ background-size: 0% 100%;
92
+ }
93
+
94
+ to {
95
+ background-size: 100% 100%;
96
+ }
97
+ }
98
+
99
+ /* A passage bracket: a tick down the inline-start with an optional label.
100
+ The prose analogue of `ui-annotation--bracket`. */
101
+ .ui-bracket-note {
102
+ --mark-color: var(--line-strong);
103
+
104
+ border-inline-start: 2px solid var(--mark-color);
105
+ display: block;
106
+ padding-inline-start: var(--space-md);
107
+ }
108
+
109
+ .ui-bracket-note__label {
110
+ color: var(--mark-color);
111
+ display: block;
112
+ font-family: var(--mono);
113
+ font-size: var(--text-2xs);
114
+ letter-spacing: 0;
115
+ margin-block-end: var(--space-2xs);
116
+ text-transform: uppercase;
117
+ }
118
+
119
+ .ui-bracket-note--accent {
120
+ --mark-color: var(--accent);
121
+ }
122
+
123
+ .ui-bracket-note--warning {
124
+ --mark-color: var(--warning);
125
+ }
126
+
127
+ .ui-bracket-note--danger {
128
+ --mark-color: var(--danger);
129
+ }
130
+
131
+ .ui-bracket-note--info {
132
+ --mark-color: var(--info);
133
+ }
134
+
135
+ /* Forced colours: only the highlight `background` is dropped, so add an
136
+ underline to the plain highlight mark to keep it visible. The styled marks
137
+ already carry a surviving channel — `--underline`/`--strike` use
138
+ text-decoration and `--box` a border — so they're excluded (otherwise the
139
+ underline would overwrite `--strike`'s line-through and lose its meaning). */
140
+ @media (forced-colors: active) {
141
+ .ui-mark:not(.ui-mark--underline, .ui-mark--box, .ui-mark--strike) {
142
+ text-decoration-line: underline;
143
+ }
144
+ }
package/css/motion.css CHANGED
@@ -172,6 +172,15 @@
172
172
  }
173
173
  }
174
174
 
175
+ /* Scroll progress: a bar that fills as the page scrolls. Driven by a
176
+ scroll() timeline, not time — the base scaleX(0) is the static
177
+ (unsupported) end state. */
178
+ @keyframes uiScrollGrow {
179
+ to {
180
+ transform: scaleX(1);
181
+ }
182
+ }
183
+
175
184
  /* --- Utilities --- */
176
185
 
177
186
  .ui-animate-in {
@@ -272,6 +281,85 @@
272
281
  margin-inline-start: 0.1em;
273
282
  }
274
283
 
284
+ /* --- Scroll-driven (progressive enhancement) — the scroll/view timeline
285
+ IS the engine, no JS. Everything is gated on `@supports
286
+ (animation-timeline: …)` so engines without it (today, Firefox/Safari)
287
+ keep the static end state, and on `prefers-reduced-motion: no-preference`
288
+ (a scroll timeline ignores animation-duration, so the global reduced-motion
289
+ reset below can't neutralise it — it must be gated here). --- */
290
+
291
+ /* Reading-progress bar. Fixed hairline that fills with document scroll;
292
+ unsupported → a static, empty (scaleX(0)) bar. Pair with role="progressbar"
293
+ if you surface it to AT. */
294
+ .ui-scroll-progress {
295
+ background: var(--accent);
296
+ block-size: 3px;
297
+ inset-block-start: 0;
298
+ inset-inline: 0;
299
+ position: fixed;
300
+ transform: scaleX(0);
301
+ transform-origin: 0 50%;
302
+ z-index: var(--z-overlay);
303
+ }
304
+
305
+ /* RTL: fill from the inline-start (right) edge. */
306
+ [dir='rtl'] .ui-scroll-progress {
307
+ transform-origin: 100% 50%;
308
+ }
309
+
310
+ @supports (animation-timeline: scroll()) {
311
+ @media (prefers-reduced-motion: no-preference) {
312
+ .ui-scroll-progress {
313
+ animation: uiScrollGrow linear;
314
+
315
+ /* longhand AFTER the shorthand — `animation:` resets the timeline to
316
+ auto, so this must follow it to take effect. */
317
+ animation-timeline: scroll(root block);
318
+ }
319
+ }
320
+ }
321
+
322
+ /* Reveal-on-scroll with no JS and no IntersectionObserver: the element
323
+ rises + fades as it scrolls into view. Unsupported → fully visible
324
+ (the `uiRise` end state), same graceful default as `.ui-reveal`. */
325
+ @supports (animation-timeline: view()) {
326
+ @media (prefers-reduced-motion: no-preference) {
327
+ .ui-scroll-reveal {
328
+ animation: uiRise linear both;
329
+ animation-timeline: view();
330
+ animation-range: entry 0% cover 40%;
331
+ }
332
+ }
333
+ }
334
+
335
+ /* --- View Transitions (progressive enhancement) — `.ui-vt` names an
336
+ element so it morphs across a same-document `document.startViewTransition()`
337
+ or a cross-document navigation. Opt into the cross-document kind with a
338
+ top-level, *unlayered* `@view-transition { navigation: auto; }` in your own
339
+ CSS (it is document-global, so the framework can't ship or scope it). --- */
340
+ .ui-vt {
341
+ view-transition-name: var(--ui-vt-name, none);
342
+ }
343
+
344
+ @media (prefers-reduced-motion: no-preference) {
345
+ /* On-brand default for the root cross-fade, inert until a transition runs. */
346
+ ::view-transition-old(root),
347
+ ::view-transition-new(root) {
348
+ animation-duration: var(--duration-base);
349
+ animation-timing-function: var(--ease-standard);
350
+ }
351
+ }
352
+
353
+ @media (prefers-reduced-motion: reduce) {
354
+ /* View transitions do NOT honour reduced-motion automatically and the
355
+ ::view-transition tree is outside the `*` reset below — kill it here. */
356
+ ::view-transition-group(*),
357
+ ::view-transition-old(*),
358
+ ::view-transition-new(*) {
359
+ animation: none !important;
360
+ }
361
+ }
362
+
275
363
  @media (prefers-reduced-motion: reduce) {
276
364
  html {
277
365
  scroll-behavior: auto;
package/css/overlay.css CHANGED
@@ -26,11 +26,56 @@
26
26
  }
27
27
 
28
28
  .ui-modal[open] {
29
- animation: uiToastIn var(--duration-base) var(--ease-spring) both;
30
29
  display: grid;
31
30
  grid-template-rows: auto 1fr auto;
32
31
  }
33
32
 
33
+ /* Enter AND exit, zero JS — `@starting-style` + `transition-behavior:
34
+ allow-discrete` keep the native <dialog> (and its backdrop) in the top
35
+ layer while it transitions out on close, so it fades/scales both ways
36
+ instead of only animating in. Scoped to `dialog.ui-modal` so the
37
+ controlled `.is-open` (non-<dialog>) path keeps its own entrance.
38
+ Needs a 2026-era floor (see CONTRIBUTING → Browser floor). */
39
+ dialog.ui-modal {
40
+ opacity: 0;
41
+ transform: translateY(6px) scale(0.98);
42
+ transition:
43
+ opacity var(--duration-base) var(--ease-spring),
44
+ transform var(--duration-base) var(--ease-spring),
45
+ overlay var(--duration-base) allow-discrete,
46
+ display var(--duration-base) allow-discrete;
47
+ }
48
+
49
+ dialog.ui-modal[open] {
50
+ opacity: 1;
51
+ transform: none;
52
+ }
53
+
54
+ @starting-style {
55
+ dialog.ui-modal[open] {
56
+ opacity: 0;
57
+ transform: translateY(6px) scale(0.98);
58
+ }
59
+ }
60
+
61
+ dialog.ui-modal::backdrop {
62
+ opacity: 0;
63
+ transition:
64
+ opacity var(--duration-base) var(--ease-out),
65
+ overlay var(--duration-base) allow-discrete,
66
+ display var(--duration-base) allow-discrete;
67
+ }
68
+
69
+ dialog.ui-modal[open]::backdrop {
70
+ opacity: 1;
71
+ }
72
+
73
+ @starting-style {
74
+ dialog.ui-modal[open]::backdrop {
75
+ opacity: 0;
76
+ }
77
+ }
78
+
34
79
  /* Controlled (non-<dialog>) usage. A portal/React modal that can't be a
35
80
  native <dialog> wears the same skin + open layout via `.is-open`.
36
81
  Backdrop, top-layer stacking and focus-trapping are then the
@@ -324,4 +369,10 @@
324
369
  .ui-modal[open] {
325
370
  animation: none;
326
371
  }
372
+
373
+ /* No enter/exit transition — snap to the open state (no flash). */
374
+ dialog.ui-modal,
375
+ dialog.ui-modal::backdrop {
376
+ transition: none;
377
+ }
327
378
  }
@@ -649,6 +649,24 @@
649
649
  padding: 0.18rem 0.4rem;
650
650
  }
651
651
 
652
+ /* --- Shortcut hint — one or more .ui-kbd keys as a chord or sequence, with a
653
+ connective __sep ("+" for a chord, "then" for a sequence). The reusable
654
+ keyboard-hint primitive (menu items, buttons, tooltips, command lists). --- */
655
+
656
+ .ui-shortcut {
657
+ align-items: center;
658
+ display: inline-flex;
659
+ gap: 0.25rem;
660
+ vertical-align: middle;
661
+ white-space: nowrap;
662
+ }
663
+
664
+ .ui-shortcut__sep {
665
+ color: var(--text-dim);
666
+ font-family: var(--mono);
667
+ font-size: var(--text-2xs);
668
+ }
669
+
652
670
  /* --- Timeline — vertical event list on a hairline spine. Use an <ol>;
653
671
  each <li> is a __item with an optional __time label. aria-current marks
654
672
  the live/most-recent event (accent marker). --- */