@ponchia/ui 0.4.0 → 0.4.1

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 (44) hide show
  1. package/CHANGELOG.md +128 -10
  2. package/README.md +20 -13
  3. package/behaviors/index.d.ts +3 -1
  4. package/behaviors/index.js +179 -106
  5. package/classes/index.d.ts +40 -0
  6. package/classes/index.js +41 -0
  7. package/classes/vscode.css-custom-data.json +18 -14
  8. package/css/disclosure.css +29 -0
  9. package/css/dots.css +2 -0
  10. package/css/feedback.css +53 -0
  11. package/css/motion.css +88 -0
  12. package/css/overlay.css +52 -1
  13. package/css/report.css +382 -0
  14. package/css/tokens.css +56 -26
  15. package/dist/bronto.css +1 -1
  16. package/dist/css/disclosure.css +1 -1
  17. package/dist/css/dots.css +1 -1
  18. package/dist/css/feedback.css +1 -1
  19. package/dist/css/motion.css +1 -1
  20. package/dist/css/overlay.css +1 -1
  21. package/dist/css/report.css +1 -0
  22. package/dist/css/tokens.css +1 -1
  23. package/docs/adr/0001-color-system.md +26 -27
  24. package/docs/adr/0002-scope-and-2026-baseline.md +104 -0
  25. package/docs/adr/0003-theme-model.md +94 -0
  26. package/docs/contrast.md +42 -42
  27. package/docs/reference.md +112 -15
  28. package/docs/reporting.md +270 -0
  29. package/docs/stability.md +37 -0
  30. package/docs/theming.md +18 -6
  31. package/docs/usage.md +21 -3
  32. package/llms.txt +61 -4
  33. package/package.json +28 -3
  34. package/qwik/index.d.ts +55 -0
  35. package/qwik/index.js +129 -0
  36. package/react/index.d.ts +35 -14
  37. package/react/index.js +22 -5
  38. package/solid/index.d.ts +35 -14
  39. package/solid/index.js +21 -3
  40. package/tokens/index.d.ts +3 -3
  41. package/tokens/index.js +16 -14
  42. package/tokens/index.json +32 -28
  43. package/tokens/resolved.json +34 -32
  44. package/tokens/tokens.dtcg.json +22 -14
package/css/feedback.css CHANGED
@@ -135,6 +135,14 @@
135
135
  .ui-toast {
136
136
  align-items: flex-start;
137
137
  animation: uiToastIn var(--duration-base) var(--ease-spring) both;
138
+
139
+ /* Exit is a transition (not a keyframe): the behavior adds `.is-leaving`
140
+ and removes the node when this transition ends. Entrance stays the
141
+ keyframe above — the transition only fires on the `.is-leaving`
142
+ property change, never on insert. */
143
+ transition:
144
+ opacity var(--duration-base) var(--ease-out),
145
+ transform var(--duration-base) var(--ease-out);
138
146
  background: var(--panel-strong);
139
147
  border: 1px solid var(--line-strong);
140
148
  border-radius: var(--radius-md);
@@ -149,6 +157,15 @@
149
157
  inline-size: 100%;
150
158
  }
151
159
 
160
+ /* Leaving — fade + drop out. `animation: none` releases the entrance
161
+ keyframe's `both` fill (a filling animation outranks normal property
162
+ declarations, so without this the exit values would be ignored). */
163
+ .ui-toast.is-leaving {
164
+ animation: none;
165
+ opacity: 0;
166
+ transform: translateY(6px) scale(0.98);
167
+ }
168
+
152
169
  .ui-toast::before {
153
170
  background: var(--text-dim);
154
171
  border-radius: 50%;
@@ -302,6 +319,41 @@
302
319
  display: none;
303
320
  }
304
321
 
322
+ /* Enter AND exit motion — zero JS, the same `@starting-style` +
323
+ `transition-behavior: allow-discrete` recipe as the modal (overlay.css).
324
+ It covers BOTH paths initPopover can take: the native `[popover]` top
325
+ layer (where the `overlay` transition keeps the panel painted while it
326
+ animates out before leaving the layer) and the `.is-open` fallback (a
327
+ regular element, so `display allow-discrete` alone carries the exit).
328
+ Gated on `prefers-reduced-motion: no-preference`, so reduced-motion users
329
+ keep the instant open/close they had before. JS positions the panel via
330
+ `top`/`left`, so the entrance `transform` is free to animate. */
331
+ @media (prefers-reduced-motion: no-preference) {
332
+ .ui-popover {
333
+ opacity: 0;
334
+ transform: translateY(-4px) scale(0.98);
335
+ transition:
336
+ opacity var(--duration-fast) var(--ease-out),
337
+ transform var(--duration-fast) var(--ease-out),
338
+ overlay var(--duration-fast) allow-discrete,
339
+ display var(--duration-fast) allow-discrete;
340
+ }
341
+
342
+ .ui-popover.is-open,
343
+ .ui-popover[popover]:popover-open {
344
+ opacity: 1;
345
+ transform: none;
346
+ }
347
+
348
+ @starting-style {
349
+ .ui-popover.is-open,
350
+ .ui-popover[popover]:popover-open {
351
+ opacity: 0;
352
+ transform: translateY(-4px) scale(0.98);
353
+ }
354
+ }
355
+ }
356
+
305
357
  /* --- Progress — linear, determinate or indeterminate --- */
306
358
 
307
359
  .ui-progress {
@@ -336,6 +388,7 @@
336
388
 
337
389
  .ui-toast {
338
390
  animation: none;
391
+ transition: none;
339
392
  }
340
393
  }
341
394
 
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
  }
package/css/report.css ADDED
@@ -0,0 +1,382 @@
1
+ /* ==========================================================================
2
+ report — opt-in static/PDF-first report layer.
3
+
4
+ This leaf is deliberately not imported by core.css. It gives LLM-authored
5
+ and hand-authored reports a small document grammar without turning the
6
+ framework into a report renderer or chart engine.
7
+ ========================================================================== */
8
+
9
+ .ui-report {
10
+ --report-gap: var(--space-lg);
11
+ --report-measure: 74ch;
12
+ --report-page-margin: 18mm;
13
+
14
+ color: var(--text-soft);
15
+ display: grid;
16
+ gap: var(--report-gap);
17
+ inline-size: min(100%, var(--report-width, 72rem));
18
+ margin-inline: auto;
19
+ padding: var(--report-padding-block, var(--space-2xl)) var(--space-md);
20
+ }
21
+
22
+ .ui-report--compact {
23
+ --report-gap: var(--space-md);
24
+ --report-padding-block: var(--space-xl);
25
+ }
26
+
27
+ .ui-report--numbered {
28
+ counter-reset: report-section;
29
+ }
30
+
31
+ .ui-report__cover {
32
+ align-content: end;
33
+ border-block-end: 1px solid var(--line);
34
+ display: grid;
35
+ gap: var(--space-md);
36
+ min-block-size: min(62vh, 34rem);
37
+ padding-block-end: var(--space-xl);
38
+ }
39
+
40
+ .ui-report__cover--compact {
41
+ min-block-size: auto;
42
+ padding-block: var(--space-xl) var(--space-lg);
43
+ }
44
+
45
+ .ui-report__header {
46
+ align-items: end;
47
+ border-block-end: 1px solid var(--line);
48
+ display: flex;
49
+ flex-wrap: wrap;
50
+ gap: var(--space-md);
51
+ justify-content: space-between;
52
+ padding-block-end: var(--space-md);
53
+ }
54
+
55
+ .ui-report__title {
56
+ color: var(--text);
57
+ font-family: var(--display);
58
+ font-size: calc(var(--text-xl) * 1.35);
59
+ letter-spacing: 0;
60
+ line-height: 1.05;
61
+ margin: 0;
62
+ max-inline-size: var(--report-measure);
63
+ text-transform: uppercase;
64
+ text-wrap: balance;
65
+ }
66
+
67
+ .ui-report__subtitle {
68
+ color: var(--text-dim);
69
+ font-size: var(--text-lg);
70
+ line-height: 1.5;
71
+ margin: 0;
72
+ max-inline-size: var(--report-measure);
73
+ }
74
+
75
+ .ui-report__meta {
76
+ align-items: center;
77
+ color: var(--text-dim);
78
+ display: flex;
79
+ flex-wrap: wrap;
80
+ font-family: var(--mono);
81
+ font-size: var(--text-2xs);
82
+ gap: 0.55rem;
83
+ letter-spacing: 0;
84
+ list-style: none;
85
+ margin: 0;
86
+ padding: 0;
87
+ text-transform: uppercase;
88
+ }
89
+
90
+ .ui-report__meta > * {
91
+ align-items: center;
92
+ display: inline-flex;
93
+ gap: 0.55rem;
94
+ }
95
+
96
+ .ui-report__meta > :not(:last-child)::after {
97
+ background: var(--line-strong);
98
+ border-radius: 50%;
99
+ block-size: 0.22rem;
100
+ content: '';
101
+ inline-size: 0.22rem;
102
+ }
103
+
104
+ .ui-report__toc {
105
+ border: 1px solid var(--line);
106
+ border-radius: var(--radius-md);
107
+ display: grid;
108
+ gap: var(--space-xs);
109
+ padding: var(--space-md);
110
+ }
111
+
112
+ .ui-report__toc ol,
113
+ .ui-report__toc ul {
114
+ display: grid;
115
+ gap: 0.35rem;
116
+ margin: 0;
117
+ padding-inline-start: var(--space-md);
118
+ }
119
+
120
+ .ui-report__toc a {
121
+ color: var(--text);
122
+ text-decoration: underline;
123
+ text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent);
124
+ text-underline-offset: 0.2rem;
125
+ }
126
+
127
+ .ui-report__summary,
128
+ .ui-report__finding,
129
+ .ui-report__evidence {
130
+ background: var(--panel);
131
+ border: 1px solid var(--line);
132
+ border-radius: var(--radius-md);
133
+ display: grid;
134
+ gap: var(--space-sm);
135
+ padding: var(--space-md);
136
+ }
137
+
138
+ .ui-report__summary {
139
+ border-inline-start: 2px solid var(--accent);
140
+ }
141
+
142
+ .ui-report__finding {
143
+ border-inline-start: 2px solid var(--line-strong);
144
+ }
145
+
146
+ .ui-report__evidence {
147
+ background: var(--panel-soft);
148
+ }
149
+
150
+ .ui-report__evidence:has(> .ui-table-wrap:only-child) {
151
+ padding: 0;
152
+ }
153
+
154
+ .ui-report__evidence:has(> .ui-table-wrap:only-child) > .ui-table-wrap {
155
+ border: 0;
156
+ border-radius: var(--radius-md);
157
+ }
158
+
159
+ .ui-report__section {
160
+ display: grid;
161
+ gap: var(--space-md);
162
+ scroll-margin-block-start: 6rem;
163
+ }
164
+
165
+ .ui-report__section + .ui-report__section {
166
+ border-block-start: 1px solid var(--line);
167
+ padding-block-start: var(--space-xl);
168
+ }
169
+
170
+ .ui-report__section-head {
171
+ color: var(--text);
172
+ font-family: var(--display);
173
+ font-size: var(--text-xl);
174
+ letter-spacing: 0;
175
+ line-height: 1.1;
176
+ margin: 0;
177
+ text-transform: uppercase;
178
+ text-wrap: balance;
179
+ }
180
+
181
+ .ui-report--numbered .ui-report__section-head::before {
182
+ color: var(--accent-text);
183
+ content: counter(report-section, decimal-leading-zero) ' / ';
184
+ counter-increment: report-section;
185
+ }
186
+
187
+ .ui-report--numbered .ui-report__section--unnumbered > .ui-report__section-head::before {
188
+ content: none;
189
+ counter-increment: none;
190
+ }
191
+
192
+ .ui-report__figure {
193
+ display: grid;
194
+ gap: var(--space-sm);
195
+ margin: 0;
196
+ }
197
+
198
+ .ui-report__caption,
199
+ .ui-chart__caption {
200
+ color: var(--text-dim);
201
+ font-family: var(--mono);
202
+ font-size: var(--text-2xs);
203
+ letter-spacing: 0;
204
+ line-height: 1.5;
205
+ text-transform: uppercase;
206
+ }
207
+
208
+ .ui-report__sources,
209
+ .ui-report__appendix,
210
+ .ui-report__footnotes {
211
+ border-block-start: 1px solid var(--line);
212
+ color: var(--text-dim);
213
+ display: grid;
214
+ font-size: var(--text-sm);
215
+ gap: var(--space-sm);
216
+ padding-block-start: var(--space-md);
217
+ }
218
+
219
+ .ui-chart {
220
+ border: 1px solid var(--line);
221
+ border-radius: var(--radius-md);
222
+ display: grid;
223
+ gap: var(--space-sm);
224
+ padding: var(--space-md);
225
+ }
226
+
227
+ .ui-chart__legend {
228
+ display: flex;
229
+ flex-wrap: wrap;
230
+ gap: var(--space-xs);
231
+ list-style: none;
232
+ margin: 0;
233
+ padding: 0;
234
+ }
235
+
236
+ .ui-chart__legend > * {
237
+ align-items: center;
238
+ color: var(--text-soft);
239
+ display: inline-flex;
240
+ font-family: var(--mono);
241
+ font-size: var(--text-xs);
242
+ gap: 0.45rem;
243
+ }
244
+
245
+ .ui-chart__swatch {
246
+ background: var(--chart-color, var(--chart-1, var(--accent)));
247
+ background-image: var(--chart-pattern, var(--chart-pattern-1, none));
248
+ background-size: var(--chart-pattern-size, 8px);
249
+ border: 1px solid var(--line-strong);
250
+ block-size: 0.8rem;
251
+ flex: 0 0 auto;
252
+ inline-size: 0.8rem;
253
+ }
254
+
255
+ .ui-chart__plot {
256
+ display: grid;
257
+ gap: var(--space-xs);
258
+ }
259
+
260
+ .ui-chart__bar {
261
+ --chart-value: 0%;
262
+
263
+ display: grid;
264
+ gap: 0.35rem;
265
+ }
266
+
267
+ .ui-chart__label {
268
+ align-items: center;
269
+ color: var(--text-soft);
270
+ display: flex;
271
+ font-family: var(--mono);
272
+ font-size: var(--text-xs);
273
+ gap: var(--space-sm);
274
+ justify-content: space-between;
275
+ }
276
+
277
+ .ui-chart__track {
278
+ background: var(--panel-soft);
279
+ border: 1px solid var(--line);
280
+ block-size: 0.8rem;
281
+ min-inline-size: 0;
282
+ }
283
+
284
+ .ui-chart__fill {
285
+ background: var(--chart-color, var(--chart-1, var(--accent)));
286
+ background-image: var(--chart-pattern, var(--chart-pattern-1, none));
287
+ background-size: var(--chart-pattern-size, 8px);
288
+ block-size: 100%;
289
+ inline-size: clamp(0%, var(--chart-value), 100%);
290
+ }
291
+
292
+ .ui-chart__fallback {
293
+ margin-block-start: var(--space-sm);
294
+ }
295
+
296
+ .ui-print-only {
297
+ display: none !important;
298
+ }
299
+
300
+ .ui-print-exact {
301
+ -webkit-print-color-adjust: exact;
302
+ print-color-adjust: exact;
303
+ }
304
+
305
+ @media print {
306
+ /* Re-tokenises the base 18mm default (base.css) so report margins stay
307
+ themeable via --report-page-margin. */
308
+ @page {
309
+ margin: var(--report-page-margin);
310
+ }
311
+
312
+ .ui-report {
313
+ --report-padding-block: 0;
314
+
315
+ /* Force an ink-on-white document regardless of the on-screen theme: the
316
+ palette tokens are [data-theme]-scoped raw values, so `color-scheme`
317
+ alone can't flip them. Re-point the handful the report renders with to
318
+ neutral (achromatic) ink/paper values so a dark-theme report doesn't
319
+ print light-grey text on white. */
320
+ color-scheme: light;
321
+
322
+ --text: #111;
323
+ --text-soft: #2a2a2a;
324
+ --text-dim: #555;
325
+ --panel: #fff;
326
+ --panel-soft: #f7f7f7;
327
+ --line: #d9d9d9;
328
+ --line-strong: #b3b3b3;
329
+
330
+ inline-size: auto;
331
+ padding: 0;
332
+ }
333
+
334
+ /* Meaning-carrying chart fills must survive the print "economy" default
335
+ (which drops backgrounds) without relying on a `.ui-print-exact`
336
+ ancestor. The accent on `.ui-report__summary` is a border, so it already
337
+ prints. */
338
+ .ui-chart__fill,
339
+ .ui-chart__swatch {
340
+ -webkit-print-color-adjust: exact;
341
+ print-color-adjust: exact;
342
+ }
343
+
344
+ .ui-report__cover {
345
+ min-block-size: auto;
346
+ }
347
+
348
+ .ui-report__cover,
349
+ .ui-report__header,
350
+ .ui-report__section,
351
+ .ui-report__summary,
352
+ .ui-report__finding,
353
+ .ui-report__evidence,
354
+ .ui-report__figure,
355
+ .ui-chart {
356
+ break-inside: avoid;
357
+ }
358
+
359
+ .ui-report__section-head {
360
+ break-after: avoid;
361
+ }
362
+
363
+ .ui-print-only {
364
+ display: var(--print-display, block) !important;
365
+ }
366
+
367
+ .ui-screen-only {
368
+ display: none !important;
369
+ }
370
+
371
+ .ui-break-before {
372
+ break-before: page;
373
+ }
374
+
375
+ .ui-break-after {
376
+ break-after: page;
377
+ }
378
+
379
+ .ui-keep {
380
+ break-inside: avoid;
381
+ }
382
+ }