@ponchia/ui 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -176
- package/behaviors/index.d.ts +16 -0
- package/behaviors/index.js +196 -0
- package/classes/index.d.ts +39 -0
- package/classes/index.js +39 -0
- package/css/disclosure.css +180 -0
- package/css/feedback.css +111 -0
- package/css/forms.css +44 -0
- package/css/overlay.css +48 -0
- package/css/primitives.css +60 -0
- package/css/site.css +33 -0
- package/dist/bronto.css +1 -1
- package/dist/css/disclosure.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/forms.css +1 -1
- package/dist/css/overlay.css +1 -1
- package/dist/css/primitives.css +1 -1
- package/dist/css/site.css +1 -1
- package/docs/reference.md +71 -1
- package/docs/usage.md +52 -0
- package/fonts/OFL.txt +93 -0
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -2,220 +2,152 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@ponchia/ui)
|
|
4
4
|
[](https://www.npmjs.com/package/@ponchia/ui#provenance)
|
|
5
|
-
[](package.json)
|
|
6
|
-
[](https://github.com/Ponchia/bronto-ui/blob/main/package.json)
|
|
6
|
+
[](https://github.com/Ponchia/bronto-ui/blob/main/scripts/check-dist.mjs)
|
|
7
7
|
[](https://github.com/Ponchia/bronto-ui/actions/workflows/ci.yml)
|
|
8
|
-
[](https://scorecard.dev/viewer/?uri=github.com/Ponchia/bronto-ui)
|
|
9
|
+
[](https://github.com/Ponchia/bronto-ui/blob/main/LICENSE)
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
monochrome surfaces, a single red accent, dot-matrix display type (Doto),
|
|
12
|
-
flat hairline borders, restrained motion. CSS-first and framework-agnostic.
|
|
11
|
+
**A CSS-first, framework-agnostic UI framework with a "Nothing"-inspired look — monochrome surfaces, one rationed red accent, dot-matrix display type, hairline borders, restrained motion. Zero runtime dependencies. Re-brand the whole thing with one CSS variable.**
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
[Reference](docs/reference.md) · [Usage](docs/usage.md) ·
|
|
16
|
-
[Theming](docs/theming.md) · [Contrast](docs/contrast.md) ·
|
|
17
|
-
[Roadmap](ROADMAP.md) · [Contributing](CONTRIBUTING.md)
|
|
13
|
+
### [Live demo →](https://ponchia.github.io/bronto-ui/) · [Theme playground →](https://ponchia.github.io/bronto-ui/demo/theme-playground.html)
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
> file so every design token autocompletes inside `var(--…)`. Add to
|
|
21
|
-
> your `.vscode/settings.json`:
|
|
22
|
-
>
|
|
23
|
-
> ```json
|
|
24
|
-
> {
|
|
25
|
-
> "css.customData": ["node_modules/@ponchia/ui/classes/vscode.css-custom-data.json"]
|
|
26
|
-
> }
|
|
27
|
-
> ```
|
|
15
|
+
The demo is the kitchen sink — every component, light/dark, RTL, live theming.
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
> point a coding agent at `node_modules/@ponchia/ui/llms.txt` for a
|
|
31
|
-
> self-contained orientation. The full class catalog
|
|
32
|
-
> (`docs/reference.md`) and the token contract (`docs/theming.md`) also
|
|
33
|
-
> ship in the tarball, so an offline agent never has to guess at the API.
|
|
17
|
+
---
|
|
34
18
|
|
|
35
|
-
|
|
36
|
-
sink (every component, light/dark, RTL, theming) deployed from `demo/`.
|
|
19
|
+
## What it is
|
|
37
20
|
|
|
38
|
-
|
|
21
|
+
`@ponchia/ui` ships its design as **CSS**, not components. You drop in one stylesheet and style with semantic `ui-*` classes; an optional thin layer of typed class-name recipes and SSR-safe vanilla behaviors sits on top for the few things that genuinely need JS (theme persistence, dialogs, toasts, disclosure). The guiding principle is **color is rationed, structure carries meaning** — layout, type weight and the hairline do the work before a hue does, and the accent is a spotlight, not a paint bucket. Because everything lives in a single `@layer bronto`, your own un-layered CSS overrides the framework with no specificity fight and no `!important`.
|
|
39
22
|
|
|
40
|
-
Install
|
|
23
|
+
## Install
|
|
41
24
|
|
|
42
25
|
```bash
|
|
43
26
|
npm i @ponchia/ui
|
|
44
27
|
```
|
|
45
28
|
|
|
46
|
-
|
|
47
|
-
> ownable). The **CSS layer** and behavior attributes stay `bronto`
|
|
48
|
-
> (`@layer bronto`, `data-bronto-*`) — that's the design-system namespace,
|
|
49
|
-
> deliberately distinct from the package name. See
|
|
50
|
-
> [`docs/architecture.md`](docs/architecture.md).
|
|
29
|
+
Or drop it in with no build step, straight from a CDN:
|
|
51
30
|
|
|
52
|
-
|
|
53
|
-
|
|
31
|
+
```html
|
|
32
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui/dist/bronto.css">
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
**1. Load the CSS.** One flattened, minified bundle — the whole framework, one request (~73 kB raw / ~13 kB gzip):
|
|
54
38
|
|
|
55
39
|
```css
|
|
56
|
-
@import '@ponchia/ui
|
|
40
|
+
@import '@ponchia/ui'; /* via a bundler */
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<!-- or a plain <link>, no bundler -->
|
|
45
|
+
<link rel="stylesheet" href="/node_modules/@ponchia/ui/dist/bronto.css">
|
|
57
46
|
```
|
|
58
47
|
|
|
59
|
-
|
|
60
|
-
`@ponchia/ui/css` is a wide `@import` fan-out (~14 leaves, one level
|
|
61
|
-
deep) — fine through a bundler, a load waterfall over plain HTTP. The
|
|
62
|
-
package also ships one flattened, minified bundle with no `@import`
|
|
63
|
-
chain:
|
|
48
|
+
> Prefer source leaves through a bundler? Use `@import '@ponchia/ui/css'` (a thin `@import` fan-out) instead. Both resolve the Doto `@font-face` with relative URLs, so there's no `/fonts` path assumption.
|
|
64
49
|
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
**2. Write markup with `ui-*` classes** (primary is the default button; modifiers are opt-in):
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<button class="ui-button">Save</button>
|
|
54
|
+
<button class="ui-button ui-button--ghost">Cancel</button>
|
|
55
|
+
<div class="ui-card">
|
|
56
|
+
<span class="ui-eyebrow">Status</span>
|
|
57
|
+
<span class="ui-badge ui-badge--success">Online</span>
|
|
58
|
+
</div>
|
|
67
59
|
```
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
enforced ceiling lives in `scripts/check-dist.mjs`, not this prose —
|
|
71
|
-
treat these figures as indicative.) Source CSS, tokens/classes/behaviors
|
|
72
|
-
entrypoints are unchanged — use whichever fits.
|
|
61
|
+
**3. (Optional) typed recipes — build class strings in JS/TS:**
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
> `import '@ponchia/ui'` in JS. There is no JS module at the root; the
|
|
77
|
-
> JS entrypoints are the explicit subpaths `@ponchia/ui/tokens`,
|
|
78
|
-
> `/classes`, and `/behaviors` (see [Entrypoints](#entrypoints)).
|
|
63
|
+
```js
|
|
64
|
+
import { ui, cx } from '@ponchia/ui/classes';
|
|
79
65
|
|
|
80
|
-
|
|
66
|
+
ui.button({ variant: 'ghost' }); // → "ui-button ui-button--ghost"
|
|
67
|
+
ui.meter({ tone: 'warning' }); // → "ui-meter ui-meter--warning"
|
|
68
|
+
```
|
|
81
69
|
|
|
82
|
-
|
|
83
|
-
`:has()`, `color-mix()`, CSS logical properties and native `<dialog>`.
|
|
84
|
-
Floor: **Chrome/Edge 111+, Safari 16.4+, Firefox 121+** (early 2023
|
|
85
|
-
onward). No build-time fallback is shipped; pin an older tag if you must
|
|
86
|
-
support below this.
|
|
70
|
+
**4. (Optional) behaviors — SSR-safe vanilla JS, each returns a cleanup function:**
|
|
87
71
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
bundler or static serving with no `/fonts` path assumption. To self-host the
|
|
91
|
-
font instead, import everything except `fonts.css` and override `--display` /
|
|
92
|
-
`--dot-font`.
|
|
72
|
+
```js
|
|
73
|
+
import { initThemeToggle, initDialog, toast } from '@ponchia/ui/behaviors';
|
|
93
74
|
|
|
94
|
-
|
|
95
|
-
|
|
75
|
+
initThemeToggle(); // wires [data-bronto-theme-toggle] + localStorage
|
|
76
|
+
initDialog(); // native <dialog> glue: [data-bronto-open] / [data-bronto-close]
|
|
77
|
+
toast('Saved', { tone: 'success' }); // body-anchored stack, no markup needed
|
|
78
|
+
```
|
|
96
79
|
|
|
97
|
-
|
|
98
|
-
> `@ponchia/ui/css/primitives.css`, etc. — is self-wrapped in
|
|
99
|
-
> `@layer bronto`, so mixing the bundle with individual leaves (e.g.
|
|
100
|
-
> per-route CSS splitting in SvelteKit/Astro) is safe: no silent
|
|
101
|
-
> cascade inversion. The deliberate full-specificity escape hatch is
|
|
102
|
-
> the explicit `@ponchia/ui/css/unlayered/<leaf>.css` path — use it
|
|
103
|
-
> only when you *want* an unlayered override, never by accident.
|
|
80
|
+
Behaviors cover theme persistence, disclosure, dropdown menus, native-`<dialog>` modals/drawers, tabs, combobox, form validation, table sort, carousel and toasts — wired by `data-bronto-*` attributes.
|
|
104
81
|
|
|
105
|
-
|
|
106
|
-
`prefers-color-scheme`.
|
|
82
|
+
## What's in the box
|
|
107
83
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
84
|
+
- **Primitives** — buttons, cards, chips, badges, links, key/value, `ui-num`, avatars.
|
|
85
|
+
- **Forms** — inputs, select, textarea, search, switch, checkbox, `ui-input-icon`, `ui-input-group`.
|
|
86
|
+
- **Feedback** — alert/callout, toast, tooltip, `ui-progress` (task) and `ui-meter` (measured value).
|
|
87
|
+
- **Overlay** — modal + drawer on native `<dialog>`, dropdown menu, `ui-carousel` + `ui-lightbox` (gallery, user-driven — not an auto-slider).
|
|
88
|
+
- **Disclosure & nav** — tabs, accordion, segmented, breadcrumb, pagination, `ui-steps`, `ui-timeline`, `ui-pagehead`, `ui-kbd`.
|
|
89
|
+
- **Shells** — an admin dashboard shell (`ui-app-*`) and a content/marketing site shell (`ui-site*`, `ui-container`).
|
|
90
|
+
- **Prose** — `.ui-prose` styles raw, unclassed semantic HTML (Markdown / CMS / LLM output) with zero classes.
|
|
91
|
+
- **Motion & dots** — the dot-matrix motif kit: dot grid, status dots, dot loaders, the orbital spinner, matrix reveal — all reduced-motion aware.
|
|
112
92
|
|
|
113
|
-
|
|
93
|
+
Full generated catalog of every class: **[docs/reference.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/reference.md)**. The decision guide (which primitive when): **[docs/usage.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/usage.md)**.
|
|
114
94
|
|
|
115
|
-
|
|
116
|
-
on top of it — none pull in a UI framework. See
|
|
117
|
-
[`docs/architecture.md`](docs/architecture.md) for the rationale.
|
|
95
|
+
## Theming: one knob
|
|
118
96
|
|
|
119
|
-
|
|
120
|
-
import tokens, { cssVars, themeColor } from '@ponchia/ui/tokens'; // tokens as data (+ /tokens.json)
|
|
121
|
-
import { ui, cx } from '@ponchia/ui/classes'; // typed class-name recipes
|
|
122
|
-
import { initThemeToggle, dismissible } from '@ponchia/ui/behaviors'; // vanilla, SSR-safe
|
|
123
|
-
```
|
|
97
|
+
Everything accent-colored derives from a single `--accent` variable via `color-mix()`. Re-brand the entire app — both light and dark — with one declaration, globally or scoped to any subtree:
|
|
124
98
|
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
|
|
99
|
+
```css
|
|
100
|
+
:root { --accent: #2f6df6; } /* whole app blue */
|
|
101
|
+
.promo { --accent: #16a34a; } /* …or just this section green */
|
|
128
102
|
```
|
|
129
103
|
|
|
130
|
-
`
|
|
131
|
-
`[data-bronto-dismissible]`, `[data-bronto-disclosure]`,
|
|
132
|
-
`[data-bronto-menu]` (`initMenu`: Escape / outside-click /
|
|
133
|
-
close-on-activate for a `<details>` `.ui-menu` dropdown), and native
|
|
134
|
-
`<dialog>` glue (`initDialog`: `[data-bronto-open]` / `[data-bronto-close]`
|
|
135
|
-
/ `[data-bronto-dialog-light]`). `toast(message, { tone, title, duration })`
|
|
136
|
-
pushes into a shared, body-anchored stack. Each initializer is SSR-safe and
|
|
137
|
-
returns a cleanup function. `demo/index.html` drives itself with these
|
|
138
|
-
modules, so it is also a live integration test.
|
|
139
|
-
|
|
140
|
-
## Layout
|
|
141
|
-
|
|
142
|
-
| File | Contents |
|
|
143
|
-
| ---------------- | ------------------------------------------------------------- |
|
|
144
|
-
| `tokens.css` | palette (dual light/dark), spacing, type, motion, dot tokens |
|
|
145
|
-
| `fonts.css` | Doto `@font-face` (relative URLs; optional if self-hosting) |
|
|
146
|
-
| `base.css` | reset, element defaults, focus, scrollbars |
|
|
147
|
-
| `motion.css` | keyframes + animation utilities + reduced-motion |
|
|
148
|
-
| `dots.css` | dot-grid, dot rule, status dot, dot loader, orbital dot spinner, dot bar (+ indeterminate), matrix reveal |
|
|
149
|
-
| `primitives.css` | `ui-*` buttons, cards, chips, badges, links, key/value |
|
|
150
|
-
| `forms.css` | inputs, select, textarea, search, switch, checkbox |
|
|
151
|
-
| `feedback.css` | alert / callout, toast, tooltip, linear progress |
|
|
152
|
-
| `overlay.css` | modal + drawer (native `<dialog>`), dropdown menu |
|
|
153
|
-
| `disclosure.css` | tabs, accordion (`<details>`), segmented, breadcrumb, pagination, avatar |
|
|
154
|
-
| `table.css` | `ui-table` dense / comfortable |
|
|
155
|
-
| `app.css` | admin shell: `ui-app-shell`/`-rail`/`-topbar`/`-toolbar`/`-panel`/`-nav`/`-metrics` |
|
|
156
|
-
| `navigation.css` | `ui-themetoggle` (dot-thumb switch) |
|
|
157
|
-
| `site.css` | content-site shell: `ui-container`, `ui-siteheader`/`ui-sitenav` (aria-current), `ui-sitemenu`, `ui-sitefooter`, `ui-skiplink`, `ui-tags`, `ui-meta` |
|
|
158
|
-
| `content.css` | `.ui-prose` Markdown/raw-HTML long-form (zero classes) + `ui-quote` pull-quote |
|
|
159
|
-
|
|
160
|
-
## Getting started
|
|
161
|
-
|
|
162
|
-
| Consumer | Guide |
|
|
163
|
-
| ----------------------- | ------------------------------------------------------------------ |
|
|
164
|
-
| Astro | [`docs/getting-started/astro.md`](docs/getting-started/astro.md) |
|
|
165
|
-
| SvelteKit | [`docs/getting-started/sveltekit.md`](docs/getting-started/sveltekit.md) |
|
|
166
|
-
| Vanilla / Vite / plain | [`docs/getting-started/vanilla.md`](docs/getting-started/vanilla.md) |
|
|
167
|
-
| React / Solid (snippet) | [`docs/getting-started/react-solid.md`](docs/getting-started/react-solid.md) |
|
|
168
|
-
| Tailwind / cascade-layer interop | [`docs/interop/tailwind.md`](docs/interop/tailwind.md) |
|
|
169
|
-
|
|
170
|
-
Each covers the CSS import location, the **no-flash** `applyStoredTheme`
|
|
171
|
-
head-script pattern, behavior init/cleanup in that framework's lifecycle,
|
|
172
|
-
and SSR caveats. Index: [`docs/integration.md`](docs/integration.md).
|
|
173
|
-
|
|
174
|
-
## Demo
|
|
175
|
-
|
|
176
|
-
`demo/index.html` is a kitchen sink covering every primitive in both
|
|
177
|
-
themes — it drives itself with the real behavior modules, so it is also a
|
|
178
|
-
live integration test. Serve the package root and open `/demo/`:
|
|
104
|
+
Buttons, focus rings, dot motifs, accent borders and soft fills all follow automatically. Light/dark is `data-theme="light"` / `"dark"` on `<html>` (defaults to `prefers-color-scheme`); `data-density` and `data-contrast` give density and contrast presets. A full re-skin (radius, display face, dot density, surfaces) is a handful more token overrides — the "Nothing" look is the **default skin, not the architecture**.
|
|
179
105
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
106
|
+
> When you change `--accent`, contrast becomes yours: verify your hue in the **[theme playground](https://ponchia.github.io/bronto-ui/demo/theme-playground.html)** (it shows the derived family and computed WCAG ratios). Full contract: **[docs/theming.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/theming.md)**.
|
|
107
|
+
|
|
108
|
+
## Works with anything
|
|
109
|
+
|
|
110
|
+
The CSS is the framework, so it works with React, Svelte/SvelteKit, Astro, Vue, Solid or plain HTML — there's no component runtime to adopt. The optional `classes` and `behaviors` entrypoints pull in **no** UI framework and are SSR-safe.
|
|
111
|
+
|
|
112
|
+
Per-framework getting-started guides + runnable example apps live in the repo:
|
|
113
|
+
|
|
114
|
+
| Stack | Guide | Example |
|
|
115
|
+
| ----- | ----- | ------- |
|
|
116
|
+
| Vanilla / Vite / plain HTML | [vanilla.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/vanilla.md) | [`examples/vanilla-vite`](https://github.com/Ponchia/bronto-ui/tree/main/examples/vanilla-vite) |
|
|
117
|
+
| Astro | [astro.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/astro.md) | [`examples/astro`](https://github.com/Ponchia/bronto-ui/tree/main/examples/astro) |
|
|
118
|
+
| SvelteKit | [sveltekit.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/sveltekit.md) | [`examples/sveltekit`](https://github.com/Ponchia/bronto-ui/tree/main/examples/sveltekit) |
|
|
119
|
+
| React / Solid (snippet) | [react-solid.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/react-solid.md) | — |
|
|
120
|
+
| Tailwind / cascade-layer interop | [tailwind.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/interop/tailwind.md) | — |
|
|
121
|
+
|
|
122
|
+
## Extras
|
|
123
|
+
|
|
124
|
+
- **Tokens as data** — `import tokens, { themeColor, cssVars } from '@ponchia/ui/tokens'` (plus `tokens.json`, W3C DTCG `tokens.dtcg.json`, and `tokens/resolved.json` for charts/data-viz).
|
|
125
|
+
- **Editor IntelliSense** — point VS Code at the shipped custom-data file so every token autocompletes in `var(--…)`:
|
|
126
|
+
```json
|
|
127
|
+
{ "css.customData": ["node_modules/@ponchia/ui/classes/vscode.css-custom-data.json"] }
|
|
128
|
+
```
|
|
129
|
+
- **For AI coding agents** — the package ships `llms.txt` at its root plus `docs/reference.md`, `docs/usage.md` and `docs/theming.md` inside the tarball, so an offline agent has the full API without guessing.
|
|
183
130
|
|
|
184
|
-
|
|
131
|
+
> The package root is **CSS-only**: `@import '@ponchia/ui'` in CSS, never `import '@ponchia/ui'` in JS. The JS entrypoints are the explicit subpaths `/tokens`, `/classes`, `/behaviors`.
|
|
185
132
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
scope: **[ROADMAP.md](ROADMAP.md)**.
|
|
133
|
+
## Browser support
|
|
134
|
+
|
|
135
|
+
Evergreen only. The framework relies on cascade layers, `:has()`, `color-mix()`, CSS logical properties and native `<dialog>`. Floor: **Chrome/Edge 111+, Safari 16.4+, Firefox 121+** (early 2023 onward). No build-time fallback ships; pin an older tag if you need below this.
|
|
190
136
|
|
|
191
137
|
## Versioning
|
|
192
138
|
|
|
193
|
-
Pre-1.0 and deliberately so. **Until `1.0.0`, breaking changes ship in
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
derivation and token **names** (incl. `--accent-text`, `--focus-ring`);
|
|
209
|
-
the `.ui-*` class names and `cls`/recipe names; the `data-bronto-*`
|
|
210
|
-
behavior attributes; each behavior's return-cleanup contract. **What is
|
|
211
|
-
not** (may change in any release): token _values_ (visual tuning), the
|
|
212
|
-
internal leaf-file boundaries and `@layer` internals, and anything
|
|
213
|
-
explicitly marked legacy/deprecated. Full token contract:
|
|
214
|
-
[`docs/theming.md`](docs/theming.md).
|
|
215
|
-
|
|
216
|
-
## Consumers
|
|
217
|
-
|
|
218
|
-
Built for two shapes of app: a content/marketing site (`ui-site*`,
|
|
219
|
-
`ui-prose`) and an admin dashboard (`ui-app-*` shell). Both import the
|
|
220
|
-
one bundle `@ponchia/ui` (or `@ponchia/ui/css`); consuming apps depend
|
|
221
|
-
on it via `@ponchia/ui`.
|
|
139
|
+
Pre-1.0 and deliberately so. **Until `1.0.0`, breaking changes ship in the _minor_** (`0.x.0`); patches (`0.x.y`) are always non-breaking. Pin with the patch range — at `0.x`, `~0.3.0` (and equivalently `^0.3.0`) resolves to `>=0.3.0 <0.4.0`, giving you safe patches while holding back the next breaking minor. Every breaking change is called out under a **BREAKING** heading in the **[CHANGELOG](https://github.com/Ponchia/bronto-ui/blob/main/CHANGELOG.md)** with a migration note.
|
|
140
|
+
|
|
141
|
+
Contractual (changes are breaking): the `--accent` derivation and token **names**, the `.ui-*` class and recipe names, the `data-bronto-*` attributes, and each behavior's cleanup contract. Not contractual (may change any release): token **values** (visual tuning) and internal leaf-file / `@layer` boundaries.
|
|
142
|
+
|
|
143
|
+
## Links
|
|
144
|
+
|
|
145
|
+
- **[Live demo](https://ponchia.github.io/bronto-ui/)** · **[Theme playground](https://ponchia.github.io/bronto-ui/demo/theme-playground.html)**
|
|
146
|
+
- **[Class reference](https://github.com/Ponchia/bronto-ui/blob/main/docs/reference.md)** · **[Usage guide](https://github.com/Ponchia/bronto-ui/blob/main/docs/usage.md)** · **[Theming](https://github.com/Ponchia/bronto-ui/blob/main/docs/theming.md)** · **[Contrast](https://github.com/Ponchia/bronto-ui/blob/main/docs/contrast.md)**
|
|
147
|
+
- **[CHANGELOG](https://github.com/Ponchia/bronto-ui/blob/main/CHANGELOG.md)** · **[Roadmap](https://github.com/Ponchia/bronto-ui/blob/main/ROADMAP.md)** · **[Contributing](https://github.com/Ponchia/bronto-ui/blob/main/CONTRIBUTING.md)**
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
[MIT](https://github.com/Ponchia/bronto-ui/blob/main/LICENSE) © Ponchia.
|
|
152
|
+
|
|
153
|
+
The bundled **Doto** font (`fonts/*.ttf`) is © 2024 The Doto Project Authors and licensed separately under the [SIL Open Font License 1.1](https://github.com/Ponchia/bronto-ui/blob/main/fonts/OFL.txt) — see `fonts/OFL.txt`.
|
package/behaviors/index.d.ts
CHANGED
|
@@ -106,6 +106,22 @@ export declare function initTabs(opts?: DelegateOpts): Cleanup;
|
|
|
106
106
|
*/
|
|
107
107
|
export declare function initDialog(opts?: DelegateOpts): Cleanup;
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Image carousel / gallery built on CSS scroll-snap (touch/trackpad swipe
|
|
111
|
+
* is the browser's). Wires `[data-bronto-carousel]`: prev/next
|
|
112
|
+
* (`[data-bronto-carousel-prev|next]`), keyboard (Arrow/Home/End on the
|
|
113
|
+
* focused `.ui-carousel__viewport`), a `.ui-carousel__thumb` strip with
|
|
114
|
+
* `aria-current` sync, the `.ui-carousel__status` counter, and ARIA. Keeps
|
|
115
|
+
* a JS index in sync with the scroll position both ways (via
|
|
116
|
+
* IntersectionObserver where available). `data-bronto-carousel-loop` wraps
|
|
117
|
+
* at the ends; `data-bronto-carousel-label` names the region. A
|
|
118
|
+
* full-screen lightbox is the same markup inside a native
|
|
119
|
+
* `<dialog class="ui-lightbox">` opened by `initDialog` (focus-trap/Escape
|
|
120
|
+
* come from the dialog). Emits `bronto:change` ({ detail: { index } }).
|
|
121
|
+
* SSR-safe, idempotent per carousel. Returns a cleanup function.
|
|
122
|
+
*/
|
|
123
|
+
export declare function initCarousel(opts?: DelegateOpts): Cleanup;
|
|
124
|
+
|
|
109
125
|
export interface ToastOpts {
|
|
110
126
|
/** Status tone — maps to `ui-toast--<tone>`. */
|
|
111
127
|
tone?: 'accent' | 'success' | 'warning' | 'danger' | 'info';
|
package/behaviors/index.js
CHANGED
|
@@ -1044,3 +1044,199 @@ export function initTableSort({ root } = {}) {
|
|
|
1044
1044
|
|
|
1045
1045
|
return () => cleanups.forEach((fn) => fn());
|
|
1046
1046
|
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Image carousel / gallery, built on CSS scroll-snap so touch + trackpad
|
|
1050
|
+
* swipe (and momentum) are the browser's, not hand-rolled. This wires the
|
|
1051
|
+
* parts scroll-snap can't do alone: prev/next buttons, keyboard nav, a
|
|
1052
|
+
* thumbnail strip, the position counter, and ARIA — keeping a JS index in
|
|
1053
|
+
* sync with the scroll position both ways.
|
|
1054
|
+
*
|
|
1055
|
+
* Markup: `[data-bronto-carousel]` containing a `.ui-carousel__viewport`
|
|
1056
|
+
* of `.ui-carousel__slide` children; optionally
|
|
1057
|
+
* `[data-bronto-carousel-prev]` / `[data-bronto-carousel-next]` controls,
|
|
1058
|
+
* a `.ui-carousel__thumbs` list of `.ui-carousel__thumb` buttons, and a
|
|
1059
|
+
* `.ui-carousel__status` counter slot. Add `data-bronto-carousel-loop` to
|
|
1060
|
+
* wrap at the ends, `data-bronto-carousel-label` to name the region.
|
|
1061
|
+
*
|
|
1062
|
+
* A full-screen **lightbox** is the same markup inside a native
|
|
1063
|
+
* `<dialog class="ui-lightbox">` opened by {@link initDialog}: the
|
|
1064
|
+
* `<dialog>` provides the top layer, focus-trap, Escape and focus-return,
|
|
1065
|
+
* so this behavior never touches focus management.
|
|
1066
|
+
*
|
|
1067
|
+
* Emits `bronto:change` ({ detail: { index } }) on every index change
|
|
1068
|
+
* (button, key, thumbnail, or swipe). SSR-safe, idempotent per carousel;
|
|
1069
|
+
* returns a cleanup function.
|
|
1070
|
+
*/
|
|
1071
|
+
export function initCarousel({ root } = {}) {
|
|
1072
|
+
if (!hasDom()) return noop;
|
|
1073
|
+
const host = root || document;
|
|
1074
|
+
const boxes = [];
|
|
1075
|
+
if (host !== document && host.matches?.('[data-bronto-carousel]')) boxes.push(host);
|
|
1076
|
+
boxes.push(...(host.querySelectorAll?.('[data-bronto-carousel]') ?? []));
|
|
1077
|
+
const cleanups = [];
|
|
1078
|
+
|
|
1079
|
+
for (const box of boxes) {
|
|
1080
|
+
const viewport = box.querySelector('.ui-carousel__viewport');
|
|
1081
|
+
if (!viewport) continue;
|
|
1082
|
+
const slides = [...viewport.children].filter((el) =>
|
|
1083
|
+
el.classList.contains('ui-carousel__slide'),
|
|
1084
|
+
);
|
|
1085
|
+
if (!slides.length) continue;
|
|
1086
|
+
const n = slides.length;
|
|
1087
|
+
const thumbs = [...box.querySelectorAll('.ui-carousel__thumb')];
|
|
1088
|
+
const status = box.querySelector('.ui-carousel__status');
|
|
1089
|
+
const prevBtn = box.querySelector('[data-bronto-carousel-prev]');
|
|
1090
|
+
const nextBtn = box.querySelector('[data-bronto-carousel-next]');
|
|
1091
|
+
const loop = box.hasAttribute('data-bronto-carousel-loop');
|
|
1092
|
+
|
|
1093
|
+
// ARIA scaffolding — pragmatic carousel semantics (not the full APG
|
|
1094
|
+
// tablist), the same restraint initMenu takes.
|
|
1095
|
+
viewport.setAttribute('role', 'group');
|
|
1096
|
+
viewport.setAttribute('aria-roledescription', 'carousel');
|
|
1097
|
+
if (!viewport.hasAttribute('aria-label'))
|
|
1098
|
+
viewport.setAttribute(
|
|
1099
|
+
'aria-label',
|
|
1100
|
+
box.getAttribute('data-bronto-carousel-label') || 'Carousel',
|
|
1101
|
+
);
|
|
1102
|
+
if (!viewport.hasAttribute('tabindex')) viewport.tabIndex = 0;
|
|
1103
|
+
slides.forEach((s, i) => {
|
|
1104
|
+
s.setAttribute('role', 'group');
|
|
1105
|
+
s.setAttribute('aria-roledescription', 'slide');
|
|
1106
|
+
if (!s.hasAttribute('aria-label')) s.setAttribute('aria-label', `${i + 1} of ${n}`);
|
|
1107
|
+
});
|
|
1108
|
+
if (status) status.setAttribute('aria-live', 'polite');
|
|
1109
|
+
for (const b of [prevBtn, nextBtn]) {
|
|
1110
|
+
if (!b) continue;
|
|
1111
|
+
if (b.tagName === 'BUTTON' && !b.hasAttribute('type')) b.type = 'button';
|
|
1112
|
+
}
|
|
1113
|
+
if (prevBtn && !prevBtn.hasAttribute('aria-label'))
|
|
1114
|
+
prevBtn.setAttribute('aria-label', 'Previous');
|
|
1115
|
+
if (nextBtn && !nextBtn.hasAttribute('aria-label')) nextBtn.setAttribute('aria-label', 'Next');
|
|
1116
|
+
|
|
1117
|
+
let index = Math.max(
|
|
1118
|
+
0,
|
|
1119
|
+
slides.findIndex((s) => s.hasAttribute('data-bronto-carousel-current')),
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
// While a button/keyboard nav is smooth-scrolling, the IntersectionObserver
|
|
1123
|
+
// would observe the intermediate slides crossing its threshold and re-fire
|
|
1124
|
+
// `bronto:change` for each — a feedback burst on a single Home→End jump.
|
|
1125
|
+
// This flag makes the IO drive the index on *user* swipes only; a timeout
|
|
1126
|
+
// (not the patchy `scrollend` event) releases it once the scroll settles.
|
|
1127
|
+
let programmatic = false;
|
|
1128
|
+
let progTimer = null;
|
|
1129
|
+
const holdProgrammatic = () => {
|
|
1130
|
+
programmatic = true;
|
|
1131
|
+
if (progTimer) clearTimeout(progTimer);
|
|
1132
|
+
progTimer = setTimeout(() => {
|
|
1133
|
+
programmatic = false;
|
|
1134
|
+
}, 500);
|
|
1135
|
+
progTimer?.unref?.(); // don't keep a Node test process alive
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
const render = () => {
|
|
1139
|
+
if (status) status.textContent = `${index + 1} / ${n}`;
|
|
1140
|
+
thumbs.forEach((t, i) => {
|
|
1141
|
+
if (i === index) t.setAttribute('aria-current', 'true');
|
|
1142
|
+
else t.removeAttribute('aria-current');
|
|
1143
|
+
});
|
|
1144
|
+
if (prevBtn && !loop) prevBtn.disabled = index === 0;
|
|
1145
|
+
if (nextBtn && !loop) nextBtn.disabled = index === n - 1;
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
const emit = () =>
|
|
1149
|
+
box.dispatchEvent(new CustomEvent('bronto:change', { detail: { index }, bubbles: true }));
|
|
1150
|
+
|
|
1151
|
+
// jsdom (and any layout-less env) has no scrollIntoView; it's a pure
|
|
1152
|
+
// affordance, so never let it break index/aria sync — same guard as
|
|
1153
|
+
// initCombobox.
|
|
1154
|
+
const reveal = (el) => {
|
|
1155
|
+
try {
|
|
1156
|
+
el?.scrollIntoView({ block: 'nearest', inline: 'center' });
|
|
1157
|
+
} catch {
|
|
1158
|
+
/* no layout — ignore */
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
const goTo = (i, { emitChange = true } = {}) => {
|
|
1163
|
+
const next = loop ? (i + n) % n : Math.max(0, Math.min(n - 1, i));
|
|
1164
|
+
const changed = next !== index;
|
|
1165
|
+
index = next;
|
|
1166
|
+
holdProgrammatic(); // suppress IO echo from the smooth-scroll this triggers
|
|
1167
|
+
reveal(slides[index]);
|
|
1168
|
+
reveal(thumbs[index]);
|
|
1169
|
+
render();
|
|
1170
|
+
if (changed && emitChange) emit();
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
const onKey = (e) => {
|
|
1174
|
+
let target = null;
|
|
1175
|
+
if (e.key === 'ArrowRight') target = index + 1;
|
|
1176
|
+
else if (e.key === 'ArrowLeft') target = index - 1;
|
|
1177
|
+
else if (e.key === 'Home') target = 0;
|
|
1178
|
+
else if (e.key === 'End') target = n - 1;
|
|
1179
|
+
else return;
|
|
1180
|
+
e.preventDefault();
|
|
1181
|
+
goTo(target);
|
|
1182
|
+
};
|
|
1183
|
+
const onClick = (e) => {
|
|
1184
|
+
if (prevBtn && e.target.closest('[data-bronto-carousel-prev]')) {
|
|
1185
|
+
goTo(index - 1);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (nextBtn && e.target.closest('[data-bronto-carousel-next]')) {
|
|
1189
|
+
goTo(index + 1);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const thumb = e.target.closest('.ui-carousel__thumb');
|
|
1193
|
+
if (thumb) {
|
|
1194
|
+
const i = thumbs.indexOf(thumb);
|
|
1195
|
+
if (i >= 0) goTo(i);
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
// Swipe sync (enhancement): when the user scrolls the viewport, snap
|
|
1200
|
+
// the JS index to the slide that's settled into view. Feature-detected
|
|
1201
|
+
// so the buttons/keyboard still work where IntersectionObserver is
|
|
1202
|
+
// absent (jsdom, older engines).
|
|
1203
|
+
let io = null;
|
|
1204
|
+
if (typeof IntersectionObserver === 'function') {
|
|
1205
|
+
io = new IntersectionObserver(
|
|
1206
|
+
(entries) => {
|
|
1207
|
+
if (programmatic) return; // ignore the echo of a button/key-driven scroll
|
|
1208
|
+
let best = null;
|
|
1209
|
+
for (const ent of entries) {
|
|
1210
|
+
if (ent.isIntersecting && (!best || ent.intersectionRatio > best.intersectionRatio))
|
|
1211
|
+
best = ent;
|
|
1212
|
+
}
|
|
1213
|
+
if (!best) return;
|
|
1214
|
+
const i = slides.indexOf(best.target);
|
|
1215
|
+
if (i >= 0 && i !== index) {
|
|
1216
|
+
index = i;
|
|
1217
|
+
render();
|
|
1218
|
+
reveal(thumbs[index]);
|
|
1219
|
+
emit();
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
{ root: viewport, threshold: 0.6 },
|
|
1223
|
+
);
|
|
1224
|
+
slides.forEach((s) => io.observe(s));
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
render();
|
|
1228
|
+
const bound = bindOnce(box, 'carousel', () => {
|
|
1229
|
+
viewport.addEventListener('keydown', onKey);
|
|
1230
|
+
box.addEventListener('click', onClick);
|
|
1231
|
+
return () => {
|
|
1232
|
+
viewport.removeEventListener('keydown', onKey);
|
|
1233
|
+
box.removeEventListener('click', onClick);
|
|
1234
|
+
io?.disconnect();
|
|
1235
|
+
if (progTimer) clearTimeout(progTimer);
|
|
1236
|
+
};
|
|
1237
|
+
});
|
|
1238
|
+
cleanups.push(bound);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return () => cleanups.forEach((fn) => fn());
|
|
1242
|
+
}
|