@ponchia/ui 0.6.0 → 0.6.3
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.
- package/CHANGELOG.md +64 -4
- package/README.md +1 -1
- package/annotations/index.d.ts.map +1 -1
- package/annotations/index.js +36 -33
- package/behaviors/carousel.d.ts +28 -0
- package/behaviors/carousel.d.ts.map +1 -0
- package/behaviors/carousel.js +3 -0
- package/behaviors/combobox.d.ts +40 -0
- package/behaviors/combobox.d.ts.map +1 -0
- package/behaviors/combobox.js +71 -20
- package/behaviors/command.d.ts +41 -0
- package/behaviors/command.d.ts.map +1 -0
- package/behaviors/command.js +9 -0
- package/behaviors/connectors.d.ts +17 -0
- package/behaviors/connectors.d.ts.map +1 -0
- package/behaviors/connectors.js +3 -0
- package/behaviors/crosshair.d.ts +42 -0
- package/behaviors/crosshair.d.ts.map +1 -0
- package/behaviors/crosshair.js +19 -1
- package/behaviors/dialog.d.ts +20 -0
- package/behaviors/dialog.d.ts.map +1 -0
- package/behaviors/dialog.js +3 -0
- package/behaviors/disclosure.d.ts +10 -0
- package/behaviors/disclosure.d.ts.map +1 -0
- package/behaviors/disclosure.js +3 -0
- package/behaviors/dismissible.d.ts +10 -0
- package/behaviors/dismissible.d.ts.map +1 -0
- package/behaviors/dismissible.js +3 -0
- package/behaviors/forms.d.ts +27 -0
- package/behaviors/forms.d.ts.map +1 -0
- package/behaviors/forms.js +18 -5
- package/behaviors/glyph.d.ts +14 -0
- package/behaviors/glyph.d.ts.map +1 -0
- package/behaviors/glyph.js +24 -0
- package/behaviors/index.d.ts +31 -237
- package/behaviors/index.d.ts.map +1 -0
- package/behaviors/index.js +17 -0
- package/behaviors/inert.d.ts +20 -0
- package/behaviors/inert.d.ts.map +1 -0
- package/behaviors/inert.js +46 -0
- package/behaviors/internal.d.ts +25 -0
- package/behaviors/internal.d.ts.map +1 -0
- package/behaviors/internal.js +30 -1
- package/behaviors/legend.d.ts +35 -0
- package/behaviors/legend.d.ts.map +1 -0
- package/behaviors/legend.js +9 -0
- package/behaviors/menu.d.ts +16 -0
- package/behaviors/menu.d.ts.map +1 -0
- package/behaviors/menu.js +3 -0
- package/behaviors/modal.d.ts +41 -0
- package/behaviors/modal.d.ts.map +1 -0
- package/behaviors/modal.js +124 -0
- package/behaviors/popover.d.ts +28 -0
- package/behaviors/popover.d.ts.map +1 -0
- package/behaviors/popover.js +17 -17
- package/behaviors/spotlight.d.ts +17 -0
- package/behaviors/spotlight.d.ts.map +1 -0
- package/behaviors/spotlight.js +3 -0
- package/behaviors/table.d.ts +36 -0
- package/behaviors/table.d.ts.map +1 -0
- package/behaviors/table.js +48 -8
- package/behaviors/tabs.d.ts +20 -0
- package/behaviors/tabs.d.ts.map +1 -0
- package/behaviors/tabs.js +3 -0
- package/behaviors/theme.d.ts +54 -0
- package/behaviors/theme.d.ts.map +1 -0
- package/behaviors/theme.js +17 -0
- package/behaviors/toast.d.ts +49 -0
- package/behaviors/toast.d.ts.map +1 -0
- package/behaviors/toast.js +34 -2
- package/classes/classes.json +683 -13
- package/classes/index.d.ts +106 -2
- package/classes/index.js +249 -65
- package/connectors/index.d.ts +12 -0
- package/connectors/index.d.ts.map +1 -1
- package/connectors/index.js +23 -2
- package/css/app.css +26 -0
- package/css/bullet.css +108 -0
- package/css/code.css +98 -0
- package/css/content.css +15 -2
- package/css/crosshair.css +7 -7
- package/css/diff.css +153 -0
- package/css/disclosure.css +18 -4
- package/css/dots.css +37 -7
- package/css/feedback.css +39 -7
- package/css/forms.css +71 -3
- package/css/legend.css +5 -2
- package/css/motion.css +79 -14
- package/css/overlay.css +59 -2
- package/css/primitives.css +67 -8
- package/css/report.css +40 -0
- package/css/sidenote.css +67 -0
- package/css/spark.css +62 -0
- package/css/table.css +9 -2
- package/css/term.css +110 -0
- package/css/textref.css +63 -0
- package/css/toc.css +91 -0
- package/css/tokens.css +14 -1
- package/css/tree.css +134 -0
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -1
- package/dist/css/app.css +1 -1
- package/dist/css/bullet.css +1 -0
- package/dist/css/code.css +1 -0
- package/dist/css/content.css +1 -1
- package/dist/css/crosshair.css +1 -1
- package/dist/css/diff.css +1 -0
- package/dist/css/disclosure.css +1 -1
- package/dist/css/dots.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/forms.css +1 -1
- package/dist/css/legend.css +1 -1
- package/dist/css/motion.css +1 -1
- package/dist/css/overlay.css +1 -1
- package/dist/css/primitives.css +1 -1
- package/dist/css/report.css +1 -1
- package/dist/css/sidenote.css +1 -0
- package/dist/css/spark.css +1 -0
- package/dist/css/table.css +1 -1
- package/dist/css/term.css +1 -0
- package/dist/css/textref.css +1 -0
- package/dist/css/toc.css +1 -0
- package/dist/css/tokens.css +1 -1
- package/dist/css/tree.css +1 -0
- package/docs/annotations.md +39 -0
- package/docs/architecture.md +2 -3
- package/docs/bullet.md +78 -0
- package/docs/code.md +76 -0
- package/docs/d2.md +4 -3
- package/docs/diff.md +146 -0
- package/docs/legends.md +8 -4
- package/docs/mermaid.md +21 -4
- package/docs/reference.md +127 -8
- package/docs/reporting.md +35 -14
- package/docs/sidenote.md +64 -0
- package/docs/spark.md +78 -0
- package/docs/stability.md +1 -0
- package/docs/term.md +81 -0
- package/docs/textref.md +78 -0
- package/docs/theming.md +44 -5
- package/docs/toc.md +83 -0
- package/docs/tree.md +74 -0
- package/docs/usage.md +264 -23
- package/docs/vega.md +22 -3
- package/glyphs/glyphs.js +7 -1
- package/llms.txt +159 -13
- package/package.json +47 -7
- package/qwik/index.d.ts +4 -2
- package/qwik/index.d.ts.map +1 -1
- package/qwik/index.js +10 -0
- package/react/index.d.ts +4 -2
- package/react/index.d.ts.map +1 -1
- package/react/index.js +6 -0
- package/solid/index.d.ts +6 -2
- package/solid/index.d.ts.map +1 -1
- package/solid/index.js +6 -0
package/docs/usage.md
CHANGED
|
@@ -26,6 +26,14 @@ into a preset on `<html>` or any subtree:
|
|
|
26
26
|
Scope it, don't globalize blindly: a dashboard with one marketing-style
|
|
27
27
|
hero can set `compact` on `<html>` and `comfortable` on the hero section.
|
|
28
28
|
|
|
29
|
+
> **`data-density` is the global preset; per-component density verbs differ by
|
|
30
|
+
> family.** Some components carry their own local density modifier, and the verb
|
|
31
|
+
> is **not** uniform: it's `--dense` on `ui-table`/`ui-dotgrid` but `--compact`
|
|
32
|
+
> on `ui-prose`/`ui-legend`/`ui-report`. So `ui-prose--dense` and
|
|
33
|
+
> `ui-table--compact` both no-op. When in doubt, reach for the global
|
|
34
|
+
> `data-density` preset; use the local modifier only where it exists (check the
|
|
35
|
+
> base's `modifiers` in classes.json).
|
|
36
|
+
|
|
29
37
|
## Badge vs chip vs status dot
|
|
30
38
|
|
|
31
39
|
All three are small. They are **not** interchangeable:
|
|
@@ -35,18 +43,39 @@ All three are small. They are **not** interchangeable:
|
|
|
35
43
|
| **status dot** | a single piece of state on something else (row online, build ok). Smallest possible signal; pair with text for a11y, never color-only. |
|
|
36
44
|
| **badge** | a label *classifying* the thing it sits on (count, tone, "BETA"). Static, not actionable. `ui.badge({ tone })`. |
|
|
37
45
|
| **chip** | a discrete, often removable/selectable token the user manipulated (a filter, a tag input value). Interactive affordance implied. |
|
|
46
|
+
| **tag** | a static keyword/category label (`ui.tag`), like a badge but reads as a non-interactive tag. Use `chip` for anything the user selects/removes; `tag` for a fixed label. |
|
|
38
47
|
|
|
39
48
|
Rule of thumb: state → dot, classification → badge, user-controlled value
|
|
40
|
-
→ chip.
|
|
49
|
+
→ chip, fixed keyword → tag. (`ui-property` is a **workbench** primitive — a
|
|
50
|
+
key/value spec row scoped to `@ponchia/ui/css/workbench.css`, not a general
|
|
51
|
+
content label; don't reach for it to tag prose.)
|
|
41
52
|
|
|
42
53
|
**Tone vocabulary varies by family — by design.** Colour is rationed, so not
|
|
43
|
-
every component carries every tone
|
|
44
|
-
(
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
every component carries every tone. The status tones
|
|
55
|
+
(`--success`/`--warning`/`--danger`) plus `--info` ride the status families —
|
|
56
|
+
`ui-alert`, `ui-toast`, `ui-meter`, `ui-dot`, and `ui-badge` all carry `--info`.
|
|
57
|
+
The neutral `--muted` is **not** a status tone: it rides several *neutral*
|
|
58
|
+
bases (`ui-badge`, `ui-num`, `ui-eyebrow`, `ui-mark`, `ui-annotation`,
|
|
59
|
+
`ui-connector`, `ui-crosshair`) but is deliberately absent from the status
|
|
60
|
+
families `ui-dot`/`ui-alert`/`ui-toast`/`ui-meter`. Because the CSS is a
|
|
61
|
+
plain class list, a hand-written unknown modifier (e.g. `ui-dot--muted`,
|
|
62
|
+
`ui-meter--muted`) **silently no-ops** — CSS can't warn. The `ui.*` builders are
|
|
63
|
+
safer on both ends: TypeScript rejects an out-of-set tone at author time, and at
|
|
64
|
+
runtime they `console.warn` (then drop the tone) so a JS caller sees the mistake
|
|
65
|
+
rather than shipping a bare, untoned element. The authoritative
|
|
66
|
+
per-component tone list is each base's `modifiers` array in
|
|
47
67
|
[`@ponchia/ui/classes.json`](../classes/classes.json) — read it rather than
|
|
48
68
|
extrapolating a tone you saw on one component onto another.
|
|
49
69
|
|
|
70
|
+
**Tone is a colour channel, so it collapses under Windows High-Contrast Mode.**
|
|
71
|
+
A `ui-badge--success` and a `ui-badge--danger` render identically once HCM
|
|
72
|
+
remaps colours (the tinted background + tone border are flattened to one system
|
|
73
|
+
colour). Status dots and the meter fill re-assert a distinct system colour, but
|
|
74
|
+
the badge/chip/tag tints cannot carry five differentiable tones — so treat badge
|
|
75
|
+
tone as **decorative emphasis** and make sure the badge's text label carries the
|
|
76
|
+
status word ("Failed", not a bare red pill). Same WCAG 1.4.1 reasoning the dots
|
|
77
|
+
follow.
|
|
78
|
+
|
|
50
79
|
## Numbers: `ui-num` vs the table state classes
|
|
51
80
|
|
|
52
81
|
- Inside `.ui-table`, a numeric cell is `.is-num` (+ `.is-pos` /
|
|
@@ -57,6 +86,21 @@ extrapolating a tone you saw on one component onto another.
|
|
|
57
86
|
freed from the table. Do not hand-roll right-align + `text-green`;
|
|
58
87
|
that's the duplication `ui-num` exists to kill.
|
|
59
88
|
|
|
89
|
+
**The positive/negative vocabulary is spelled three ways — match the mechanism
|
|
90
|
+
to the host, they are not interchangeable:**
|
|
91
|
+
|
|
92
|
+
| Where | Positive / negative | Kind |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `ui-num` primitive (anywhere) | `ui-num--pos` / `ui-num--neg` (`ui.num({ tone: 'pos' })`) | **modifier** |
|
|
95
|
+
| Inside `.ui-table` | `is-pos` / `is-neg` | **state hook** (table-scoped) |
|
|
96
|
+
| `ui-delta` trend | `ui-delta--up` / `ui-delta--down` (`ui.delta({ dir })`) | **direction** |
|
|
97
|
+
|
|
98
|
+
They share the same tone tokens but **don't cross over**: `ui-num is-pos`
|
|
99
|
+
no-ops (the `is-pos` rule is scoped to table cells), and `ui-delta--pos` doesn't
|
|
100
|
+
exist. Use the modifier on `ui-num`, the `is-*` state inside a table, and
|
|
101
|
+
`--up`/`--down` on `ui-delta` (which also flips with `ui.delta({ invert })` when
|
|
102
|
+
up is the *bad* direction — latency, error rate, cost).
|
|
103
|
+
|
|
60
104
|
## Prose vs primitives, and prose inside a card
|
|
61
105
|
|
|
62
106
|
- `ui-prose` styles **raw, unclassed semantic HTML** (MDX / CMS / LLM
|
|
@@ -68,6 +112,42 @@ extrapolating a tone you saw on one component onto another.
|
|
|
68
112
|
`.ui-card` itself, so card padding/border stays the card's and prose
|
|
69
113
|
rhythm stays the content's. One responsibility per element.
|
|
70
114
|
|
|
115
|
+
## Centred width: `ui-center` vs `ui-container`
|
|
116
|
+
|
|
117
|
+
Both cap a centred column, but they are **different primitives with different
|
|
118
|
+
box models** — pick by intent:
|
|
119
|
+
|
|
120
|
+
- **`ui-center`** — a *reading measure*. `--center-max` is the **inner** content
|
|
121
|
+
width (content-box); the `--center-gutter` padding adds *outside* it. Use it to
|
|
122
|
+
hold prose/body to a comfortable line length.
|
|
123
|
+
- **`ui-container`** — a *page frame*. `--container` (/`--container-narrow`
|
|
124
|
+
/`--container-wide`) is the **total** max width (border-box). Use it as the
|
|
125
|
+
outer wrapper that aligns a page's sections to a shared edge.
|
|
126
|
+
|
|
127
|
+
Rule of thumb: measure of text → `ui-center`; page-level frame → `ui-container`.
|
|
128
|
+
Don't nest one inside the other expecting the caps to compose — they measure
|
|
129
|
+
different boxes.
|
|
130
|
+
|
|
131
|
+
## Container queries: `ui-cq`
|
|
132
|
+
|
|
133
|
+
`ui-cq` is the one primitive that changes how *other* primitives respond. Add it
|
|
134
|
+
to a wrapper and its descendants adapt to **that box's** inline size, not the
|
|
135
|
+
viewport — so the same `ui-grid` / `ui-statgrid` / `ui-app-metrics` collapses to
|
|
136
|
+
one column inside a slim panel even when the window is wide (island-safe; it
|
|
137
|
+
nests). Two thresholds are built in: `ui-grid` drops to a single column at
|
|
138
|
+
**34rem** and `ui-statgrid`/`ui-app-metrics` at **30rem**, measured on the `ui-cq`
|
|
139
|
+
box. Note `rem` in a container query resolves against the **root** font size, not
|
|
140
|
+
16px — at Bronto's 15px root that's ≈**510px** and ≈**450px**, ~6% tighter than a
|
|
141
|
+
16px mental model. And be aware `ui-grid` already collapses on its own via an
|
|
142
|
+
intrinsic `auto-fit` minmax, so `ui-cq` barely changes it — the primitive that
|
|
143
|
+
genuinely *needs* `ui-cq` to collapse by container (not viewport) is
|
|
144
|
+
`ui-statgrid`/`ui-app-metrics`. The container is named `bronto` (hardcoded — there
|
|
145
|
+
is no `--cq-name` knob; an author-set one is ignored), so an outer query never
|
|
146
|
+
accidentally matches an inner grid. `ui-cq` is inert until applied, so adding it
|
|
147
|
+
never shifts an existing layout. Reach for it whenever a layout must respond to
|
|
148
|
+
its container (a resizable pane, a sidebar widget, an embedded card) rather than
|
|
149
|
+
the page.
|
|
150
|
+
|
|
71
151
|
## Static reports
|
|
72
152
|
|
|
73
153
|
Use the opt-in `@ponchia/ui/css/report.css` layer for static, PDF-first
|
|
@@ -126,10 +206,22 @@ Both are a thin horizontal bar; they mean different things.
|
|
|
126
206
|
indeterminate (`ui-progress--indeterminate`). The fill is always accent.
|
|
127
207
|
- **`ui-meter`** — a *measured static value*: coverage, disk, capacity, a
|
|
128
208
|
KPI against a target. Never indeterminate. Tone the fill by threshold
|
|
129
|
-
(`ui.meter({ tone })` → accent/success/warning/danger); the unset
|
|
130
|
-
default is neutral. Drive the width with the shared `--value` knob
|
|
131
|
-
(`style="--value: 72"`,
|
|
132
|
-
|
|
209
|
+
(`ui.meter({ tone })` → accent/success/warning/danger/info); the unset
|
|
210
|
+
default is neutral. Drive the width with the shared `--value` knob — a
|
|
211
|
+
**unitless number 0–100** (`style="--value: 72"`, *not* `72%`: a `%` is
|
|
212
|
+
invalid against the registered `<number>` type and the fill drops to empty).
|
|
213
|
+
The class string paints a 0-width, unannounced bar on its own, so set the
|
|
214
|
+
value **and** its ARIA together with `attrs.meter(72)` (or `attrs.progress`)
|
|
215
|
+
from `@ponchia/ui/classes` — it returns `role="meter"` +
|
|
216
|
+
`aria-valuenow/min/max` + the `--value` style, normalized to your
|
|
217
|
+
`{ min, max }`. Spread it: `<div class={ui.meter({ tone })} {...attrs.meter(72)}>`.
|
|
218
|
+
|
|
219
|
+
**Indeterminate progress** is the one exception: call `attrs.progress()` with
|
|
220
|
+
**no argument**. ARIA requires `aria-valuenow` be *omitted* for an indeterminate
|
|
221
|
+
bar — emitting `0` would announce "0%", indistinguishable from a real stalled-at-
|
|
222
|
+
zero bar — so the helper returns just `role="progressbar"` + `aria-busy="true"`
|
|
223
|
+
(no `aria-valuenow`, no `--value`). Pair it with the class:
|
|
224
|
+
`<div class={ui.progress({ indeterminate: true })} {...attrs.progress()}>`.
|
|
133
225
|
|
|
134
226
|
Rule of thumb: *something is happening* → progress; *something measures
|
|
135
227
|
this much* → meter.
|
|
@@ -139,7 +231,10 @@ this much* → meter.
|
|
|
139
231
|
- **`ui-steps`** — a stepper for a multi-step flow. Use an `<ol>`. State is
|
|
140
232
|
ARIA-driven (the framework rule): the active step is `aria-current="step"`
|
|
141
233
|
(no class); completed steps take `ui-steps__item--done`. Markers are
|
|
142
|
-
auto-numbered by CSS counter.
|
|
234
|
+
auto-numbered by CSS counter. `--done` is a **visual** state only — it isn't
|
|
235
|
+
announced, so if "completed" must reach AT, add visually-hidden text
|
|
236
|
+
(e.g. `<span class="ui-visually-hidden">completed</span>`) or an `aria-label`
|
|
237
|
+
on the step.
|
|
143
238
|
- **`ui-timeline`** — a vertical event list on a hairline spine (`<ol>` of
|
|
144
239
|
`ui-timeline__item`, optional `ui-timeline__time`). `aria-current` on an
|
|
145
240
|
item marks the live/most-recent event.
|
|
@@ -161,18 +256,76 @@ without it these widgets are unlabelled or unannounced:
|
|
|
161
256
|
- **`ui-pagination`** — wrap it in `<nav aria-label="Pagination">`; give the
|
|
162
257
|
current page `aria-current="page"`; label icon-only prev/next controls
|
|
163
258
|
(`aria-label="Previous page"`). Disable a control with native `disabled`
|
|
164
|
-
(a `<button>`) **or** `aria-disabled="true"`
|
|
165
|
-
|
|
259
|
+
(a `<button>`) for full inertness, **or** `aria-disabled="true"` for a control
|
|
260
|
+
that stays focusable/announced (e.g. a disabled `<a>`). CSS dims both and makes
|
|
261
|
+
`aria-disabled` pointer-inert, but only native `disabled` is keyboard-inert on
|
|
262
|
+
its own — to stop an `aria-disabled` control from activating on Enter/Space,
|
|
263
|
+
wire `initDisabledGuard()` once near your root (see Behaviors).
|
|
166
264
|
- **`ui-tabs`** — `initTabs` adds the full APG wiring (roles, roving tabindex,
|
|
167
265
|
`aria-selected`, panel `hidden`, focusable panel). If you wire tabs yourself,
|
|
168
266
|
name the `ui-tabs__list` (`role="tablist"` + an `aria-label`) and pair each
|
|
169
|
-
tab with its panel via `aria-controls`/`aria-labelledby`.
|
|
267
|
+
tab with its panel via `aria-controls`/`aria-labelledby`. Every tab needs a
|
|
268
|
+
matching panel — a tab with no `aria-controls` target is an orphan that
|
|
269
|
+
announces as selected but reveals nothing.
|
|
270
|
+
- **Icon-only buttons** (`ui-button--icon` and any glyph-only control) carry no
|
|
271
|
+
text node, so they're nameless to AT — give them an `aria-label`
|
|
272
|
+
(`<button class="ui-button ui-button--icon" aria-label="Delete">`).
|
|
170
273
|
- **`ui-sitenav` / `ui-app-nav`** — signal the current link with
|
|
171
274
|
`aria-current="page"` (both honour it; `ui-app-nav` also accepts the
|
|
172
275
|
visual-only `.is-active`, but prefer `aria-current`).
|
|
173
276
|
- **`ui-skiplink`** — keep it the first focusable element and point its `href`
|
|
174
277
|
at the `id` of your main landmark.
|
|
175
278
|
|
|
279
|
+
## App shell: the admin dashboard frame
|
|
280
|
+
|
|
281
|
+
`ui-app-shell` is a CSS-only two-column admin frame (sidebar rail + main
|
|
282
|
+
column) that collapses to a single column with a horizontal rail below 880px —
|
|
283
|
+
no behavior required. The nesting matters; the rail is `ui-app-rail` and the
|
|
284
|
+
content side is `ui-app-main`:
|
|
285
|
+
|
|
286
|
+
```html
|
|
287
|
+
<div class="ui-app-shell">
|
|
288
|
+
<aside class="ui-app-rail">
|
|
289
|
+
<span class="ui-app-rail__brand">Acme</span>
|
|
290
|
+
<nav class="ui-app-nav" aria-label="Primary">
|
|
291
|
+
<span class="ui-app-nav__section">Main</span>
|
|
292
|
+
<a href="/overview" aria-current="page">Overview</a>
|
|
293
|
+
<a href="/reports">Reports</a>
|
|
294
|
+
</nav>
|
|
295
|
+
<div class="ui-app-rail__account">…</div>
|
|
296
|
+
</aside>
|
|
297
|
+
<main class="ui-app-main">
|
|
298
|
+
<header class="ui-app-topbar"><h1 class="ui-app-topbar__title">Overview</h1></header>
|
|
299
|
+
<div class="ui-app-content">
|
|
300
|
+
<section class="ui-app-panel">
|
|
301
|
+
<div class="ui-app-panel__head"><h2 class="ui-app-panel__title">KPIs</h2></div>
|
|
302
|
+
<div class="ui-app-metrics">…<div class="ui-app-metric">…</div></div>
|
|
303
|
+
</section>
|
|
304
|
+
</div>
|
|
305
|
+
</main>
|
|
306
|
+
</div>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Knobs: `--app-rail` sets the rail width (default 14rem); `ui-app-shell--full`
|
|
310
|
+
drops the rail for a single-column app. `ui-app-nav` honours `aria-current="page"`
|
|
311
|
+
(preferred) and the visual-only `.is-active`.
|
|
312
|
+
|
|
313
|
+
## Menus: `data-bronto-menu` + `initMenu`
|
|
314
|
+
|
|
315
|
+
A dropdown menu is a native `<details data-bronto-menu>` styled as
|
|
316
|
+
`ui-menu-host` → `ui-menu` (with `ui-menu__item` / `ui-menu__sep` /
|
|
317
|
+
`ui-menu__label`). It opens/closes natively, but `initMenu()` adds the close
|
|
318
|
+
affordances a menu needs: outside-click close, Escape, and closing on item
|
|
319
|
+
activation. Without the behavior the menu opens but never dismisses itself.
|
|
320
|
+
|
|
321
|
+
**Clipping limitation.** The menu panel is positioned in normal flow (not the
|
|
322
|
+
top layer), so an ancestor with `overflow: hidden`/`auto` or a transform
|
|
323
|
+
**clips** it — the same edge case the tooltip has. If your menu lives inside a
|
|
324
|
+
scroll container or a clipped card and the panel gets cut off, either lift the
|
|
325
|
+
`ui-menu-host` out of the clipping ancestor, or reach for `initPopover` (which
|
|
326
|
+
escapes to the browser top layer via the native `popover` attribute) for that
|
|
327
|
+
control instead.
|
|
328
|
+
|
|
176
329
|
## Avatar: it's an unlabelled blob until you name it
|
|
177
330
|
|
|
178
331
|
`ui-avatar` is a presentation box. Give it an accessible name yourself: an
|
|
@@ -185,11 +338,24 @@ convey identity to AT. Keep initials to ~2 characters — the box is
|
|
|
185
338
|
## Modal: native `<dialog>` vs `is-open`
|
|
186
339
|
|
|
187
340
|
Prefer the **native `<dialog>`** path — you get top-layer, backdrop and
|
|
188
|
-
focus-trap free
|
|
189
|
-
|
|
190
|
-
|
|
341
|
+
focus-trap free (wire it with `initDialog` for open-triggers + focus-return).
|
|
342
|
+
Only use `ui-modal.is-open` (`ui.modal({ open: true })`) when a portal/React
|
|
343
|
+
modal genuinely can't be a `<dialog>`. The **backdrop and top-layer stacking
|
|
344
|
+
stay yours**, but you no longer have to hand-roll the focus trap: mark the
|
|
345
|
+
overlay `data-bronto-modal` and call `initModal()`. While `is-open` it traps
|
|
346
|
+
focus with `inert` (the rest of the page goes non-interactive), returns focus to
|
|
347
|
+
the opener on close, and dispatches a cancelable `bronto:modal:close` on Escape —
|
|
348
|
+
you still own the `is-open` class, so drop it in response. On bind `initModal`
|
|
349
|
+
also gives the overlay `role="dialog"` + `aria-modal="true"` and dev-warns if it
|
|
350
|
+
has no accessible name (add `aria-label`/`aria-labelledby`). A drawer is a modal
|
|
191
351
|
that enters from an edge — same rule.
|
|
192
352
|
|
|
353
|
+
**Scroll-lock is not automatic on either path.** Neither the native `<dialog>`
|
|
354
|
+
nor the `is-open` path freezes the background — the page behind an open modal can
|
|
355
|
+
still scroll. If that matters, toggle a lock yourself while the modal is open
|
|
356
|
+
(`document.documentElement.style.overflow = 'hidden'`, restored on close), or add
|
|
357
|
+
`html:has(dialog[open]) { overflow: hidden }` for the native path.
|
|
358
|
+
|
|
193
359
|
## Carousel & lightbox: one primitive, two skins
|
|
194
360
|
|
|
195
361
|
`ui-carousel` is a scroll-snap track of `__slide`s wired by `initCarousel`
|
|
@@ -347,11 +513,25 @@ authoring engine.
|
|
|
347
513
|
`ui-button`): the browser greys it, blocks activation, and skips it in tab
|
|
348
514
|
order, and bronto styles the disabled cue. Use `aria-disabled="true"` **only**
|
|
349
515
|
when the control must stay focusable/announced — bronto then adds
|
|
350
|
-
`pointer-events: none` to `ui-button`/`ui-link` so
|
|
351
|
-
|
|
516
|
+
`pointer-events: none` to `ui-button`/`ui-link` so the pointer can't activate
|
|
517
|
+
it. That is **pointer-inert, not keyboard-inert**: CSS can't stop Enter/Space,
|
|
518
|
+
so wire `initDisabledGuard()` (Behaviors) to block keyboard activation across
|
|
519
|
+
every `aria-disabled` control. Either way you still own removing it from the
|
|
520
|
+
submit logic.
|
|
521
|
+
- **Read-only ≠ disabled.** A `readonly` input keeps its value in form submission
|
|
522
|
+
and stays focusable/selectable; `disabled` does neither. Bronto gives a
|
|
523
|
+
read-only field a quiet muted fill so it doesn't read as a live editable field —
|
|
524
|
+
reach for `readonly` when the value matters but mustn't be edited, `disabled`
|
|
525
|
+
when it should be inert and skipped.
|
|
352
526
|
- **Combobox** (`data-bronto-combobox`) reads its options from the DOM at
|
|
353
|
-
`initCombobox()` time — re-run it after you replace the option list
|
|
354
|
-
|
|
527
|
+
`initCombobox()` time — re-run it after you replace the option list (or add
|
|
528
|
+
`data-bronto-combobox-live`). The selected **option's text label** is shown in
|
|
529
|
+
the input while the `bronto:change` event carries the option's `data-value`
|
|
530
|
+
code — so put the human label in the `<li>` text and the code in `data-value`.
|
|
531
|
+
The `.ui-combobox__empty` ("No matches") is hidden until a filter empties the
|
|
532
|
+
list. Two intentional single-select APG deviations: ArrowDown on a closed list
|
|
533
|
+
filters rather than pre-selecting the first option, and Tab closes without
|
|
534
|
+
committing a merely-highlighted option (Enter/click commits).
|
|
355
535
|
- **Validation** is opt-in via `data-bronto-validate` on the form plus
|
|
356
536
|
`initFormValidation()`; it surfaces messages into a `ui-error-summary` you
|
|
357
537
|
provide. The summary's title is the legible sans, not the display face — it's
|
|
@@ -366,6 +546,16 @@ With scripting disabled it degrades to fully visible, but if scripting is *on*
|
|
|
366
546
|
and nothing toggles `is-visible`, the content stays hidden — so only use
|
|
367
547
|
`ui-reveal` when you are wiring that toggle.
|
|
368
548
|
|
|
549
|
+
**View transitions (`ui-vt`).** `ui-vt` names an element via `--ui-vt-name` so it
|
|
550
|
+
morphs across a `document.startViewTransition()` or a cross-document navigation.
|
|
551
|
+
The name must be **unique per document** at the moment a transition runs: applying
|
|
552
|
+
`ui-vt` with one shared `--ui-vt-name` across every card in a list (the obvious
|
|
553
|
+
loop) makes the browser skip the transition — and it is **not** silent: it logs a
|
|
554
|
+
console error and **rejects `vt.ready`** (with an `InvalidStateError`). If you
|
|
555
|
+
don't `vt.ready.catch(…)`, that surfaces as an unhandled promise rejection. Give
|
|
556
|
+
each element its own name (`--ui-vt-name: card-7`) or only mark the single element
|
|
557
|
+
that actually morphs.
|
|
558
|
+
|
|
369
559
|
## Loading affordances need a role you supply
|
|
370
560
|
|
|
371
561
|
`ui-spinner`, `ui-dotspinner`, `ui-skeleton`, and an indeterminate `ui-progress`
|
|
@@ -382,13 +572,64 @@ without it, it falls back to an `is-open` class that a clipping ancestor can cut
|
|
|
382
572
|
off. Add `popover` to the panel for the robust path — the `is-open` form is a
|
|
383
573
|
fallback, not the default to copy.
|
|
384
574
|
|
|
575
|
+
It is a **non-modal** dialog by design: the panel gets `role="dialog"` and focus
|
|
576
|
+
moves into it, but there is **no focus trap** and the rest of the page stays
|
|
577
|
+
interactive — Tab moves *out* of the panel (it does not cycle), and it closes on
|
|
578
|
+
Escape or outside-click. Don't assume `<dialog>`-modal semantics; if you need a
|
|
579
|
+
trap and an inert backdrop, use a real modal (`<dialog>` + `initDialog`, or
|
|
580
|
+
`initModal`). And the `is-open` fallback is a plain stacked element, so it sits
|
|
581
|
+
*under* any open native `<dialog>`'s top layer — another reason to prefer the
|
|
582
|
+
native `popover` attribute when a popover and a dialog can be open together.
|
|
583
|
+
|
|
584
|
+
## Two tiers: CSS-native vs behavior-required
|
|
585
|
+
|
|
586
|
+
Not every component works with JavaScript off — know which tier you are shipping
|
|
587
|
+
before you rely on the no-JS path.
|
|
588
|
+
|
|
589
|
+
**CSS-native — fully operable with JS off.** Safe in static or LLM-authored
|
|
590
|
+
HTML, print/PDF, and before any hydration:
|
|
591
|
+
|
|
592
|
+
| Component | How it works without JS |
|
|
593
|
+
| --- | --- |
|
|
594
|
+
| Tooltip (`ui-tooltip`) | `:hover` / `:focus-within` (+ anchor positioning where supported) |
|
|
595
|
+
| Accordion | native `<details>` / `<summary>` |
|
|
596
|
+
| Segmented control (`ui-segmented`) | `:has(input:checked)` over a radio group |
|
|
597
|
+
| Scroll-reveal (`ui-scroll-reveal`) | scroll-driven animation, zero JS |
|
|
598
|
+
| Modal via native `<dialog>` | the element brings focus-trap + Escape; `initDialog` only adds open-triggers + focus-return |
|
|
599
|
+
|
|
600
|
+
**Behavior-required — a CSS skin that needs its `init*` to be interactive.**
|
|
601
|
+
These are JS widgets wearing the Bronto look; without the behavior they are inert
|
|
602
|
+
(and a couple are *worse* than inert — see the tabs row):
|
|
603
|
+
|
|
604
|
+
| Component | Behavior | With the behavior absent |
|
|
605
|
+
| --- | --- | --- |
|
|
606
|
+
| Tabs (`ui-tabs`) | `initTabs` | **author panels visible** — ship `hidden` panels and if `initTabs` never runs the content is unreachable |
|
|
607
|
+
| Combobox (`ui-combobox`) | `initCombobox` | a plain text input beside an unfiltered list |
|
|
608
|
+
| Command palette (`ui-command`) | `initCommand` | a static, unfiltered list |
|
|
609
|
+
| Table sort/select (`[data-bronto-sortable]`) | `initTableSort` | a static table (still readable) |
|
|
610
|
+
| Popover (`ui-popover`) | `initPopover` | no placement/ARIA — prefer the native `popover` attribute |
|
|
611
|
+
| Carousel (`ui-carousel`) | `initCarousel` | a native scroll-snap track (usable, no controls) |
|
|
612
|
+
| Controlled modal (`ui-modal.is-open`) | `initModal` | no focus trap — provide one or use native `<dialog>` |
|
|
613
|
+
| Menu (`data-bronto-menu`) | `initMenu` | a button next to a list with no open/close, outside-click, or Escape |
|
|
614
|
+
| Toast | `toast()` | nothing — it is imperative-only |
|
|
615
|
+
|
|
616
|
+
One cross-cutting guard, not tied to a single component:
|
|
617
|
+
|
|
618
|
+
| Concern | Behavior | With the behavior absent |
|
|
619
|
+
| --- | --- | --- |
|
|
620
|
+
| `aria-disabled="true"` controls | `initDisabledGuard` | dimmed + pointer-inert via CSS, but still **keyboard**-activatable on Enter/Space (native `disabled` is already fully inert) |
|
|
621
|
+
|
|
622
|
+
Rule of thumb: if a component needs ARIA-state sync, focus management, a keyboard
|
|
623
|
+
model, or persisted/dynamic state, it is behavior-required — that is the exact
|
|
624
|
+
boundary of what CSS alone cannot do.
|
|
625
|
+
|
|
385
626
|
## When to add a behavior
|
|
386
627
|
|
|
387
628
|
The CSS is the framework; `@ponchia/ui/behaviors` is the *sanctioned*
|
|
388
629
|
home for the little JS that genuinely needs scripting (theme persistence,
|
|
389
|
-
disclosure, dialog glue, toast, combobox, form-validation,
|
|
390
|
-
Reach for it instead of reimplementing — every initializer is
|
|
391
|
-
idempotent, and returns a cleanup. If you find yourself writing focus
|
|
630
|
+
disclosure, dialog glue, modal focus-trap, toast, combobox, form-validation,
|
|
631
|
+
table-sort). Reach for it instead of reimplementing — every initializer is
|
|
632
|
+
SSR-safe, idempotent, and returns a cleanup. If you find yourself writing focus
|
|
392
633
|
management or `aria-expanded` toggling by hand, there is probably already
|
|
393
634
|
a behavior for it.
|
|
394
635
|
|
package/docs/vega.md
CHANGED
|
@@ -65,8 +65,9 @@ vega-embed 7), so don't mix a Vega-Lite 6 with a Vega 5 runtime:
|
|
|
65
65
|
<script src="https://cdn.jsdelivr.net/npm/vega-lite@6.4.3/build/vega-lite.min.js"></script>
|
|
66
66
|
<script src="https://cdn.jsdelivr.net/npm/vega-embed@7.1.0/build/vega-embed.min.js"></script>
|
|
67
67
|
<script>
|
|
68
|
-
// INLINE the config
|
|
69
|
-
//
|
|
68
|
+
// INLINE the config — generate the paste-ready literal with
|
|
69
|
+
// `npm run emit:theme vega light` (mirrors the annotations author-time-copy
|
|
70
|
+
// pattern). This is the only path that also works from a file:// report.
|
|
70
71
|
const brontoLight = {
|
|
71
72
|
/* …paste tokens/vega.json → light here… */
|
|
72
73
|
};
|
|
@@ -78,12 +79,30 @@ vega-embed 7), so don't mix a Vega-Lite 6 with a Vega 5 runtime:
|
|
|
78
79
|
> `import` the `@ponchia/ui/vega` module **nor** `fetch('…/vega.json')` — the
|
|
79
80
|
> browser blocks both across the `null`/file origin (CORS). So for a
|
|
80
81
|
> double-clickable or PDF-bound report, **inline the resolved config object**
|
|
81
|
-
> (as above) rather than fetching it.
|
|
82
|
+
> (as above) rather than fetching it. Generate the paste-ready, sentinel-tagged
|
|
83
|
+
> literal with `npm run emit:theme vega light` (or `dark`); re-running
|
|
84
|
+
> `npm run emit:theme:check <file>` re-derives every tagged block and fails if a
|
|
85
|
+
> token change has left an inlined copy stale. Over an `http(s)` origin (a dev server, a
|
|
82
86
|
> static host, a bundler), the `import { brontoVegaConfig }` form and a
|
|
83
87
|
> `fetch('https://cdn.jsdelivr.net/npm/@ponchia/ui@VERSION/tokens/vega.json')`
|
|
84
88
|
> both work — pin the package version in the URL, since the unversioned latest
|
|
85
89
|
> may predate this target.
|
|
86
90
|
|
|
91
|
+
**Over `http(s)`, skip the inline copy — import the helper as an ES module.**
|
|
92
|
+
`tokens/vega.js` (the `@ponchia/ui/vega` entry) has **zero dependencies**, so it
|
|
93
|
+
loads straight from a CDN as a browser ES module with no bundler and no
|
|
94
|
+
import-map. You get `brontoVegaConfig(theme)` itself (live theme switching), not
|
|
95
|
+
a frozen object to keep in sync. Pin the package version; this needs a real
|
|
96
|
+
origin (it does **not** work from `file://` — use the inline form above there):
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<script type="module">
|
|
100
|
+
import { brontoVegaConfig } from 'https://cdn.jsdelivr.net/npm/@ponchia/ui@VERSION/tokens/vega.js';
|
|
101
|
+
// vegaEmbed loaded from its UMD bundle above (window.vegaEmbed), or import it as ESM too.
|
|
102
|
+
vegaEmbed('#chart', spec, { config: brontoVegaConfig('light'), renderer: 'svg', actions: false });
|
|
103
|
+
</script>
|
|
104
|
+
```
|
|
105
|
+
|
|
87
106
|
For a build step or non-JS host, read `@ponchia/ui/vega.json` directly
|
|
88
107
|
(`{ light, dark }`, each a ready Vega-Lite `config`).
|
|
89
108
|
|
package/glyphs/glyphs.js
CHANGED
|
@@ -1027,7 +1027,13 @@ export function renderGlyph(name, options = {}) {
|
|
|
1027
1027
|
const style = [`--dotmatrix-cols:${GLYPH_SIZE}`];
|
|
1028
1028
|
const dotLen = dot && cssLen(dot);
|
|
1029
1029
|
const gapLen = gap && cssLen(gap);
|
|
1030
|
-
|
|
1030
|
+
// Default the dot track to an intrinsic icon scale when the author set no
|
|
1031
|
+
// (valid) `--dotmatrix-dot`. Without it the CSS grid falls back to
|
|
1032
|
+
// `minmax(0, 1fr)` and the 16×16 matrix balloons to fill its container
|
|
1033
|
+
// (full-bleed, ~1250px) — the string API would then render an icon-intent call
|
|
1034
|
+
// very differently from the DOM `initDotGlyph` path, which already defaults to
|
|
1035
|
+
// 0.08em. Mirror that default here so both paths render the same icon. (C7.)
|
|
1036
|
+
style.push(`--dotmatrix-dot:${dotLen || '0.08em'}`);
|
|
1031
1037
|
// Solid mode fuses the dots into a crisp pixel glyph: square cells, no gap.
|
|
1032
1038
|
if (solid) style.push('--dotmatrix-dot-radius:0', '--dotmatrix-gap:0');
|
|
1033
1039
|
else if (gapLen) style.push(`--dotmatrix-gap:${gapLen}`);
|