@ponchia/ui 0.2.0 → 0.2.2
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 +62 -37
- package/behaviors/index.d.ts +29 -0
- package/behaviors/index.js +172 -0
- package/classes/index.d.ts +37 -0
- package/classes/index.js +85 -0
- package/css/app.css +23 -23
- package/css/base.css +130 -10
- package/css/cards.css +21 -21
- package/css/content.css +289 -0
- package/css/core.css +4 -0
- package/css/disclosure.css +311 -0
- package/css/dots.css +125 -8
- package/css/feedback.css +243 -0
- package/css/fonts.css +11 -0
- package/css/forms.css +39 -12
- package/css/layout.css +5 -5
- package/css/motion.css +29 -5
- package/css/navigation.css +14 -9
- package/css/overlay.css +198 -0
- package/css/primitives.css +33 -16
- package/css/responsive.css +14 -14
- package/css/table.css +10 -10
- package/css/tokens.css +77 -20
- package/css/typography.css +9 -9
- package/dist/bronto-core.css +1 -0
- package/dist/bronto.css +1 -0
- package/package.json +21 -4
- package/tokens/index.js +14 -14
- package/tokens/index.json +28 -28
- package/tokens/tokens.dtcg.json +575 -0
package/README.md
CHANGED
|
@@ -18,19 +18,6 @@ npm i @ponchia/ui
|
|
|
18
18
|
> deliberately distinct from the package name. See
|
|
19
19
|
> [`docs/architecture.md`](docs/architecture.md).
|
|
20
20
|
|
|
21
|
-
> Not published yet — the only remaining blocker is npm-account side
|
|
22
|
-
> (create the `@ponchia` scope + `NPM_TOKEN`; see [Release](#release)).
|
|
23
|
-
> Until the first publish, depend on a `file:` link to a checkout or a
|
|
24
|
-
> pinned git tag:
|
|
25
|
-
>
|
|
26
|
-
> ```json
|
|
27
|
-
> { "dependencies": { "@ponchia/ui": "file:../bronto-ui" } }
|
|
28
|
-
> ```
|
|
29
|
-
>
|
|
30
|
-
> ```json
|
|
31
|
-
> { "dependencies": { "@ponchia/ui": "github:Ponchia/bronto-ui#semver:^0.2.0" } }
|
|
32
|
-
> ```
|
|
33
|
-
|
|
34
21
|
Import the full theme (includes responsive breakpoints):
|
|
35
22
|
|
|
36
23
|
```css
|
|
@@ -43,6 +30,27 @@ Or the core bundle if the app manages its own responsive layer:
|
|
|
43
30
|
@import '@ponchia/ui/css/core.css';
|
|
44
31
|
```
|
|
45
32
|
|
|
33
|
+
**Prebuilt single file (recommended for apps without a CSS bundler).**
|
|
34
|
+
`@ponchia/ui/css` is a 17-deep `@import` graph — fine through a bundler,
|
|
35
|
+
a load waterfall over plain HTTP. The package also ships flattened,
|
|
36
|
+
minified bundles with no `@import` chain:
|
|
37
|
+
|
|
38
|
+
```css
|
|
39
|
+
@import '@ponchia/ui'; /* → dist/bronto.css, full */
|
|
40
|
+
@import '@ponchia/ui/dist/bronto-core.css'; /* core, no responsive */
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
~62 kB raw / ~11 kB gzip, one request, same `@layer bronto`. Source CSS,
|
|
44
|
+
tokens/classes/behaviors entrypoints are unchanged — use whichever fits.
|
|
45
|
+
|
|
46
|
+
### Browser support
|
|
47
|
+
|
|
48
|
+
Evergreen only — the framework relies on cascade layers (`@layer`),
|
|
49
|
+
`:has()`, `color-mix()`, CSS logical properties and native `<dialog>`.
|
|
50
|
+
Floor: **Chrome/Edge 111+, Safari 16.4+, Firefox 121+** (early 2023
|
|
51
|
+
onward). No build-time fallback is shipped; pin an older tag if you must
|
|
52
|
+
support below this.
|
|
53
|
+
|
|
46
54
|
The Doto `@font-face` ships in `css/fonts.css` (bundled by both `css` and
|
|
47
55
|
`css/core.css`) with URLs relative to the package, so it resolves through a
|
|
48
56
|
bundler or static serving with no `/fonts` path assumption. To self-host the
|
|
@@ -59,6 +67,11 @@ escape hatch, not the default path.
|
|
|
59
67
|
Set `data-theme="light"` or `data-theme="dark"` on `<html>`; defaults follow
|
|
60
68
|
`prefers-color-scheme`.
|
|
61
69
|
|
|
70
|
+
**Re-brand with one knob:** `--accent` drives the whole accent family
|
|
71
|
+
(`color-mix`-derived). `:root { --accent: #2f6df6 }` — or scope it to a
|
|
72
|
+
subtree — restyles everything, both themes. Plus `data-density` and
|
|
73
|
+
`data-contrast` presets. Full contract: [`docs/theming.md`](docs/theming.md).
|
|
74
|
+
|
|
62
75
|
## Entrypoints
|
|
63
76
|
|
|
64
77
|
The CSS is the framework. These optional sibling entrypoints are thin layers
|
|
@@ -77,9 +90,12 @@ themeColor('dark').accent; // → "#ff3b41"
|
|
|
77
90
|
```
|
|
78
91
|
|
|
79
92
|
`behaviors` wires `[data-bronto-theme-toggle]`, `[data-bronto-dismiss]` /
|
|
80
|
-
`[data-bronto-dismissible]`,
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
`[data-bronto-dismissible]`, `[data-bronto-disclosure]`, and native
|
|
94
|
+
`<dialog>` glue (`initDialog`: `[data-bronto-open]` / `[data-bronto-close]`
|
|
95
|
+
/ `[data-bronto-dialog-light]`). `toast(message, { tone, title, duration })`
|
|
96
|
+
pushes into a shared, body-anchored stack. Each initializer is SSR-safe and
|
|
97
|
+
returns a cleanup function. `demo/index.html` drives itself with these
|
|
98
|
+
modules, so it is also a live integration test.
|
|
83
99
|
|
|
84
100
|
## Layout
|
|
85
101
|
|
|
@@ -89,14 +105,18 @@ with these modules, so it is also a live integration test.
|
|
|
89
105
|
| `fonts.css` | Doto `@font-face` (relative URLs; optional if self-hosting) |
|
|
90
106
|
| `base.css` | reset, element defaults, focus, scrollbars |
|
|
91
107
|
| `motion.css` | keyframes + animation utilities + reduced-motion |
|
|
92
|
-
| `dots.css` | dot-grid, dot rule, status dot, dot loader
|
|
108
|
+
| `dots.css` | dot-grid, dot rule, status dot, dot loader, orbital dot spinner, dot bar (+ indeterminate), matrix reveal |
|
|
93
109
|
| `primitives.css` | `ui-*` buttons, cards, chips, badges, links, key/value |
|
|
94
110
|
| `forms.css` | inputs, select, textarea, search, switch, checkbox |
|
|
111
|
+
| `feedback.css` | alert / callout, toast, tooltip, linear progress |
|
|
112
|
+
| `overlay.css` | modal + drawer (native `<dialog>`), dropdown menu |
|
|
113
|
+
| `disclosure.css` | tabs, accordion (`<details>`), segmented, breadcrumb, pagination, avatar |
|
|
95
114
|
| `table.css` | `ui-table` dense / comfortable |
|
|
96
115
|
| `app.css` | admin shell: rail, topbar, toolbar, panel, metrics |
|
|
97
116
|
| `navigation.css` | site nav, menu, theme toggle (dot indicator) |
|
|
98
117
|
| `typography.css` | display headings, eyebrows, legacy `.button` |
|
|
99
|
-
| `
|
|
118
|
+
| `content.css` | `.ui-prose` — Markdown/raw-HTML long-form, zero per-element classes |
|
|
119
|
+
| `cards.css` | semantic content cards (token-driven) |
|
|
100
120
|
| `layout.css` | site shell, hero, grids |
|
|
101
121
|
| `responsive.css` | breakpoint overrides |
|
|
102
122
|
|
|
@@ -113,18 +133,30 @@ python3 -m http.server -d . 8080 # then open http://localhost:8080/demo/
|
|
|
113
133
|
|
|
114
134
|
```bash
|
|
115
135
|
npm install # stylelint + jsdom (test only)
|
|
116
|
-
npm run check # lint +
|
|
136
|
+
npm run check # lint + 5 integrity checks (exports, tokens, classes, dist, pack)
|
|
117
137
|
npm test # node:test — pure modules + jsdom behavior tests
|
|
118
|
-
npm run lint:fix # auto-fix the safe stylistic rules
|
|
138
|
+
npm run lint:fix # auto-fix the safe stylistic rules + logical properties
|
|
119
139
|
npm run tokens:build # regenerate tokens/index.json from tokens/index.js
|
|
140
|
+
npm run dist:build # rebuild the flattened dist/ bundles
|
|
120
141
|
```
|
|
121
142
|
|
|
122
143
|
`npm run check` enforces that the data mirrors cannot drift from the CSS:
|
|
123
144
|
exports/import-graph integrity, `tokens.css` ⇄ `tokens/index.{js,json}`, the
|
|
124
|
-
`classes` registry ⇄ the `.ui-*` selectors,
|
|
125
|
-
ships only the intended files.
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
`classes` registry ⇄ the `.ui-*` selectors, `dist/` ⇄ `css/` (fresh + in
|
|
146
|
+
budget), and that the published tarball ships only the intended files.
|
|
147
|
+
|
|
148
|
+
Visual + a11y regression is a separate suite (real browser, so not in the
|
|
149
|
+
zero-dep core check):
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npx playwright test # demo screenshots (light/dark/RTL) + axe-core WCAG
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
It is pinned to a Playwright container so baselines are byte-stable; the
|
|
156
|
+
committed baselines under `test/e2e/__screenshots__` were authored in that
|
|
157
|
+
same image. CI (`.github/workflows/ci.yml`) runs both the `check` job and a
|
|
158
|
+
containerised `visual` job on every branch push and PR. It never publishes —
|
|
159
|
+
a push to `main` ships nothing.
|
|
128
160
|
|
|
129
161
|
## Release
|
|
130
162
|
|
|
@@ -142,19 +174,12 @@ the version never reaches npm, so consumers never resolve it. GitHub also
|
|
|
142
174
|
serves the raw tag tarball ungated, but that is a legacy/fallback path, not
|
|
143
175
|
the documented install. See [`docs/architecture.md`](docs/architecture.md).
|
|
144
176
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(an npm automation token with publish rights to the scope).
|
|
149
|
-
|
|
150
|
-
Done already: MIT `LICENSE`, `version` `0.2.0`, `publishConfig`, the
|
|
151
|
-
gated publish workflow. After publishing, switch the consumer repos'
|
|
152
|
-
dependency specifier to `@ponchia/ui`.
|
|
177
|
+
Published: `@ponchia/ui` is live on npm (latest `0.2.1`), released by CI
|
|
178
|
+
with provenance. The `@ponchia` scope and the `NPM_TOKEN` repo secret are
|
|
179
|
+
in place, so a pushed `vX.Y.Z` tag is all a release needs.
|
|
153
180
|
|
|
154
181
|
## Consumers
|
|
155
182
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Both still pin the old tarball URL; they switch their dependency to
|
|
160
|
-
`@ponchia/ui` once it is published (separate repos — not changed here).
|
|
183
|
+
Built for two shapes of app: a content/marketing site (imports
|
|
184
|
+
`@ponchia/ui/css/core.css`) and an admin dashboard (imports the full
|
|
185
|
+
`@ponchia/ui/css`). Consuming apps depend on it via `@ponchia/ui`.
|
package/behaviors/index.d.ts
CHANGED
|
@@ -38,3 +38,32 @@ export declare function dismissible(opts?: DelegateOpts): Cleanup;
|
|
|
38
38
|
|
|
39
39
|
/** Wire `[data-bronto-disclosure]` triggers. Returns a cleanup function. */
|
|
40
40
|
export declare function initDisclosure(opts?: DelegateOpts): Cleanup;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Wire `[data-bronto-tabs]` groups with the WAI-ARIA Tabs keyboard
|
|
44
|
+
* pattern (roving tabindex, Arrow/Home/End, aria-selected, panel sync).
|
|
45
|
+
* Returns a cleanup function.
|
|
46
|
+
*/
|
|
47
|
+
export declare function initTabs(opts?: DelegateOpts): Cleanup;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Wire native <dialog> open/close glue: `[data-bronto-open="id"]`,
|
|
51
|
+
* `[data-bronto-close]`, and backdrop light-dismiss for dialogs marked
|
|
52
|
+
* `[data-bronto-dialog-light]`. Returns a cleanup function.
|
|
53
|
+
*/
|
|
54
|
+
export declare function initDialog(opts?: DelegateOpts): Cleanup;
|
|
55
|
+
|
|
56
|
+
export interface ToastOpts {
|
|
57
|
+
/** Status tone — maps to `ui-toast--<tone>`. */
|
|
58
|
+
tone?: 'accent' | 'success' | 'warning' | 'danger';
|
|
59
|
+
/** Optional uppercase label rendered above the message. */
|
|
60
|
+
title?: string;
|
|
61
|
+
/** Auto-dismiss delay in ms. 0 keeps it until dismissed. Default: 4000. */
|
|
62
|
+
duration?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Push a transient toast into a shared, body-anchored stack. Returns a
|
|
67
|
+
* function that dismisses it early. No-op (returns noop) without a DOM.
|
|
68
|
+
*/
|
|
69
|
+
export declare function toast(message: string, opts?: ToastOpts): Cleanup;
|
package/behaviors/index.js
CHANGED
|
@@ -116,6 +116,178 @@ export function dismissible({ root } = {}) {
|
|
|
116
116
|
return () => host.removeEventListener('click', onClick);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Wire `[data-bronto-tabs]` groups for full keyboard a11y. The framework
|
|
121
|
+
* ships the look + the ARIA/`.is-active` contract; this adds the WAI-ARIA
|
|
122
|
+
* Tabs pattern: roving `tabindex`, `aria-selected`, Arrow/Home/End
|
|
123
|
+
* navigation with automatic activation, and panel `hidden` sync. Tabs are
|
|
124
|
+
* `.ui-tab[data-tab]`; panels are `.ui-tabs__panel[data-panel]` with
|
|
125
|
+
* matching values. SSR-safe; returns a cleanup function.
|
|
126
|
+
*/
|
|
127
|
+
export function initTabs({ root } = {}) {
|
|
128
|
+
if (!hasDom()) return noop;
|
|
129
|
+
const host = root || document;
|
|
130
|
+
const cleanups = [];
|
|
131
|
+
let uid = 0;
|
|
132
|
+
// querySelectorAll only matches descendants, so a `root` that *is* a
|
|
133
|
+
// tab group would be skipped — include it explicitly.
|
|
134
|
+
const groups = [];
|
|
135
|
+
if (host !== document && host.matches?.('[data-bronto-tabs]')) groups.push(host);
|
|
136
|
+
groups.push(...host.querySelectorAll('[data-bronto-tabs]'));
|
|
137
|
+
for (const group of groups) {
|
|
138
|
+
// Own group only — a tab/panel inside a nested [data-bronto-tabs]
|
|
139
|
+
// belongs to that inner group, not this one.
|
|
140
|
+
const owned = (el) => el.closest('[data-bronto-tabs]') === group;
|
|
141
|
+
const tabs = [...group.querySelectorAll('.ui-tab')].filter(owned);
|
|
142
|
+
const panels = [...group.querySelectorAll('.ui-tabs__panel')].filter(owned);
|
|
143
|
+
if (!tabs.length) continue;
|
|
144
|
+
const list = group.querySelector('.ui-tabs__list');
|
|
145
|
+
if (list) list.setAttribute('role', 'tablist');
|
|
146
|
+
|
|
147
|
+
// APG: bind each tab to its panel (aria-controls) and back
|
|
148
|
+
// (aria-labelledby), minting stable ids only where absent.
|
|
149
|
+
for (const t of tabs) {
|
|
150
|
+
const p = panels.find((x) => x.dataset.panel === t.dataset.tab);
|
|
151
|
+
if (!p) continue;
|
|
152
|
+
const n = ++uid;
|
|
153
|
+
if (!t.id) t.id = `bronto-tab-${n}`;
|
|
154
|
+
if (!p.id) p.id = `bronto-tabpanel-${n}`;
|
|
155
|
+
t.setAttribute('aria-controls', p.id);
|
|
156
|
+
p.setAttribute('aria-labelledby', t.id);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const select = (tab) => {
|
|
160
|
+
for (const t of tabs) {
|
|
161
|
+
const on = t === tab;
|
|
162
|
+
t.classList.toggle('is-active', on);
|
|
163
|
+
t.setAttribute('role', 'tab');
|
|
164
|
+
t.setAttribute('aria-selected', String(on));
|
|
165
|
+
t.tabIndex = on ? 0 : -1;
|
|
166
|
+
}
|
|
167
|
+
for (const p of panels) {
|
|
168
|
+
p.setAttribute('role', 'tabpanel');
|
|
169
|
+
p.hidden = p.dataset.panel !== tab.dataset.tab;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const onClick = (e) => {
|
|
173
|
+
// `tabs` is filtered to this group, so membership (not mere DOM
|
|
174
|
+
// containment) is what isolates nested [data-bronto-tabs] groups.
|
|
175
|
+
const tab = e.target.closest('.ui-tab');
|
|
176
|
+
if (tab && tabs.includes(tab)) {
|
|
177
|
+
select(tab);
|
|
178
|
+
tab.focus();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const onKey = (e) => {
|
|
182
|
+
const i = tabs.indexOf(e.target.closest('.ui-tab'));
|
|
183
|
+
if (i < 0) return;
|
|
184
|
+
let n = i;
|
|
185
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') n = (i + 1) % tabs.length;
|
|
186
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') n = (i - 1 + tabs.length) % tabs.length;
|
|
187
|
+
else if (e.key === 'Home') n = 0;
|
|
188
|
+
else if (e.key === 'End') n = tabs.length - 1;
|
|
189
|
+
else return;
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
select(tabs[n]);
|
|
192
|
+
tabs[n].focus();
|
|
193
|
+
};
|
|
194
|
+
group.addEventListener('click', onClick);
|
|
195
|
+
group.addEventListener('keydown', onKey);
|
|
196
|
+
select(tabs.find((t) => t.classList.contains('is-active')) || tabs[0]);
|
|
197
|
+
cleanups.push(() => {
|
|
198
|
+
group.removeEventListener('click', onClick);
|
|
199
|
+
group.removeEventListener('keydown', onKey);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return () => cleanups.forEach((fn) => fn());
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Wire native <dialog> open/close glue (the one bit <dialog> can't do
|
|
207
|
+
* declaratively). Click `[data-bronto-open="dialogId"]` calls
|
|
208
|
+
* `showModal()` on `#dialogId`; click `[data-bronto-close]` closes the
|
|
209
|
+
* nearest enclosing <dialog>. Clicking the backdrop of a dialog that has
|
|
210
|
+
* `[data-bronto-dialog-light]` closes it too. SSR-safe; returns cleanup.
|
|
211
|
+
*
|
|
212
|
+
* `root` scopes which triggers are delegated (default `document`); the
|
|
213
|
+
* dialog itself is still resolved by id document-wide, because a modal
|
|
214
|
+
* <dialog> is promoted to the top layer and is inherently document-global
|
|
215
|
+
* (same model as `initThemeToggle`, where `root` scopes controls but the
|
|
216
|
+
* theme applies to <html>).
|
|
217
|
+
*/
|
|
218
|
+
export function initDialog({ root } = {}) {
|
|
219
|
+
if (!hasDom()) return noop;
|
|
220
|
+
const host = root || document;
|
|
221
|
+
const onClick = (e) => {
|
|
222
|
+
const opener = e.target.closest('[data-bronto-open]');
|
|
223
|
+
if (opener && host.contains(opener)) {
|
|
224
|
+
const dlg = document.getElementById(opener.getAttribute('data-bronto-open'));
|
|
225
|
+
if (dlg && typeof dlg.showModal === 'function' && !dlg.open) dlg.showModal();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const closer = e.target.closest('[data-bronto-close]');
|
|
229
|
+
if (closer && host.contains(closer)) {
|
|
230
|
+
const dlg = closer.closest('dialog');
|
|
231
|
+
if (dlg && dlg.open) dlg.close();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Light-dismiss: a click whose target is the <dialog> itself is the
|
|
235
|
+
// backdrop (content sits in child elements).
|
|
236
|
+
const dlg = e.target;
|
|
237
|
+
if (
|
|
238
|
+
dlg.tagName === 'DIALOG' &&
|
|
239
|
+
dlg.open &&
|
|
240
|
+
dlg.hasAttribute('data-bronto-dialog-light') &&
|
|
241
|
+
host.contains(dlg)
|
|
242
|
+
) {
|
|
243
|
+
dlg.close();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
host.addEventListener('click', onClick);
|
|
247
|
+
return () => host.removeEventListener('click', onClick);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Push a transient toast into a shared, screen-anchored stack (created
|
|
252
|
+
* once and appended to <body>, `aria-live="polite"`). `tone` is one of
|
|
253
|
+
* accent/success/warning/danger; `title` is an optional uppercase label;
|
|
254
|
+
* `duration` ms before auto-dismiss (0 keeps it until dismissed). Returns
|
|
255
|
+
* a function that dismisses the toast early. SSR-safe (no-op).
|
|
256
|
+
*/
|
|
257
|
+
export function toast(message, { tone, title, duration = 4000 } = {}) {
|
|
258
|
+
if (!hasDom()) return noop;
|
|
259
|
+
let stack = document.querySelector('.ui-toast-stack');
|
|
260
|
+
if (!stack) {
|
|
261
|
+
stack = document.createElement('div');
|
|
262
|
+
stack.className = 'ui-toast-stack';
|
|
263
|
+
stack.setAttribute('aria-live', 'polite');
|
|
264
|
+
document.body.appendChild(stack);
|
|
265
|
+
}
|
|
266
|
+
const el = document.createElement('div');
|
|
267
|
+
el.className = tone ? `ui-toast ui-toast--${tone}` : 'ui-toast';
|
|
268
|
+
// No per-item role: the stack is already aria-live=polite; a nested
|
|
269
|
+
// status live region risks double announcement in some SRs.
|
|
270
|
+
if (title) {
|
|
271
|
+
const t = document.createElement('p');
|
|
272
|
+
t.className = 'ui-toast__title';
|
|
273
|
+
t.textContent = title;
|
|
274
|
+
el.appendChild(t);
|
|
275
|
+
}
|
|
276
|
+
const body = document.createElement('div');
|
|
277
|
+
body.textContent = message;
|
|
278
|
+
el.appendChild(body);
|
|
279
|
+
stack.appendChild(el);
|
|
280
|
+
|
|
281
|
+
let timer;
|
|
282
|
+
const dismiss = () => {
|
|
283
|
+
if (timer) clearTimeout(timer);
|
|
284
|
+
el.remove();
|
|
285
|
+
if (stack && !stack.childElementCount) stack.remove();
|
|
286
|
+
};
|
|
287
|
+
if (duration > 0) timer = setTimeout(dismiss, duration);
|
|
288
|
+
return dismiss;
|
|
289
|
+
}
|
|
290
|
+
|
|
119
291
|
/**
|
|
120
292
|
* Disclosure: a `[data-bronto-disclosure]` trigger toggles the element
|
|
121
293
|
* referenced by its `aria-controls` id, keeping `aria-expanded` and the
|
package/classes/index.d.ts
CHANGED
|
@@ -49,6 +49,34 @@ export interface ClusterOpts {
|
|
|
49
49
|
export interface StaggerOpts {
|
|
50
50
|
auto?: boolean;
|
|
51
51
|
}
|
|
52
|
+
export type Tone = 'accent' | 'success' | 'warning' | 'danger';
|
|
53
|
+
export interface AlertOpts {
|
|
54
|
+
tone?: Tone;
|
|
55
|
+
}
|
|
56
|
+
export interface ToastOpts {
|
|
57
|
+
tone?: Tone;
|
|
58
|
+
}
|
|
59
|
+
export interface ProgressOpts {
|
|
60
|
+
indeterminate?: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface DotspinnerOpts {
|
|
63
|
+
size?: 'sm' | 'lg';
|
|
64
|
+
}
|
|
65
|
+
export interface DotbarOpts {
|
|
66
|
+
indeterminate?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface ModalOpts {
|
|
69
|
+
drawer?: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface TabOpts {
|
|
72
|
+
active?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface AvatarOpts {
|
|
75
|
+
size?: 'sm' | 'lg';
|
|
76
|
+
}
|
|
77
|
+
export interface ProseOpts {
|
|
78
|
+
compact?: boolean;
|
|
79
|
+
}
|
|
52
80
|
|
|
53
81
|
export interface Ui {
|
|
54
82
|
button(opts?: ButtonOpts): string;
|
|
@@ -63,6 +91,15 @@ export interface Ui {
|
|
|
63
91
|
hint(opts?: HintOpts): string;
|
|
64
92
|
cluster(opts?: ClusterOpts): string;
|
|
65
93
|
stagger(opts?: StaggerOpts): string;
|
|
94
|
+
alert(opts?: AlertOpts): string;
|
|
95
|
+
toast(opts?: ToastOpts): string;
|
|
96
|
+
progress(opts?: ProgressOpts): string;
|
|
97
|
+
dotspinner(opts?: DotspinnerOpts): string;
|
|
98
|
+
dotbar(opts?: DotbarOpts): string;
|
|
99
|
+
modal(opts?: ModalOpts): string;
|
|
100
|
+
tab(opts?: TabOpts): string;
|
|
101
|
+
avatar(opts?: AvatarOpts): string;
|
|
102
|
+
prose(opts?: ProseOpts): string;
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
export declare const ui: Ui;
|
package/classes/index.js
CHANGED
|
@@ -50,7 +50,11 @@ export const cls = Object.freeze({
|
|
|
50
50
|
dotfield: 'ui-dotfield',
|
|
51
51
|
dotrule: 'ui-dotrule',
|
|
52
52
|
dotbar: 'ui-dotbar',
|
|
53
|
+
dotbarIndeterminate: 'ui-dotbar--indeterminate',
|
|
53
54
|
dotloader: 'ui-dotloader',
|
|
55
|
+
dotspinner: 'ui-dotspinner',
|
|
56
|
+
dotspinnerSm: 'ui-dotspinner--sm',
|
|
57
|
+
dotspinnerLg: 'ui-dotspinner--lg',
|
|
54
58
|
// forms
|
|
55
59
|
field: 'ui-field',
|
|
56
60
|
label: 'ui-label',
|
|
@@ -64,6 +68,58 @@ export const cls = Object.freeze({
|
|
|
64
68
|
switchThumb: 'ui-switch__thumb',
|
|
65
69
|
hint: 'ui-hint',
|
|
66
70
|
hintError: 'ui-hint--error',
|
|
71
|
+
// feedback
|
|
72
|
+
alert: 'ui-alert',
|
|
73
|
+
alertTitle: 'ui-alert__title',
|
|
74
|
+
alertBody: 'ui-alert__body',
|
|
75
|
+
alertDismiss: 'ui-alert__dismiss',
|
|
76
|
+
alertAccent: 'ui-alert--accent',
|
|
77
|
+
alertSuccess: 'ui-alert--success',
|
|
78
|
+
alertWarning: 'ui-alert--warning',
|
|
79
|
+
alertDanger: 'ui-alert--danger',
|
|
80
|
+
toastStack: 'ui-toast-stack',
|
|
81
|
+
toast: 'ui-toast',
|
|
82
|
+
toastTitle: 'ui-toast__title',
|
|
83
|
+
toastAccent: 'ui-toast--accent',
|
|
84
|
+
toastSuccess: 'ui-toast--success',
|
|
85
|
+
toastWarning: 'ui-toast--warning',
|
|
86
|
+
toastDanger: 'ui-toast--danger',
|
|
87
|
+
tooltip: 'ui-tooltip',
|
|
88
|
+
tooltipBubble: 'ui-tooltip__bubble',
|
|
89
|
+
progress: 'ui-progress',
|
|
90
|
+
progressBar: 'ui-progress__bar',
|
|
91
|
+
progressIndeterminate: 'ui-progress--indeterminate',
|
|
92
|
+
// overlay
|
|
93
|
+
modal: 'ui-modal',
|
|
94
|
+
modalHead: 'ui-modal__head',
|
|
95
|
+
modalTitle: 'ui-modal__title',
|
|
96
|
+
modalBody: 'ui-modal__body',
|
|
97
|
+
modalFoot: 'ui-modal__foot',
|
|
98
|
+
modalClose: 'ui-modal__close',
|
|
99
|
+
modalDrawer: 'ui-modal--drawer',
|
|
100
|
+
menu: 'ui-menu',
|
|
101
|
+
menuLabel: 'ui-menu__label',
|
|
102
|
+
menuItem: 'ui-menu__item',
|
|
103
|
+
menuSep: 'ui-menu__sep',
|
|
104
|
+
// disclosure
|
|
105
|
+
tabs: 'ui-tabs',
|
|
106
|
+
tabsList: 'ui-tabs__list',
|
|
107
|
+
tab: 'ui-tab',
|
|
108
|
+
tabsPanel: 'ui-tabs__panel',
|
|
109
|
+
accordion: 'ui-accordion',
|
|
110
|
+
accordionItem: 'ui-accordion__item',
|
|
111
|
+
accordionSummary: 'ui-accordion__summary',
|
|
112
|
+
accordionBody: 'ui-accordion__body',
|
|
113
|
+
segmented: 'ui-segmented',
|
|
114
|
+
segmentedOption: 'ui-segmented__option',
|
|
115
|
+
breadcrumb: 'ui-breadcrumb',
|
|
116
|
+
breadcrumbItem: 'ui-breadcrumb__item',
|
|
117
|
+
pagination: 'ui-pagination',
|
|
118
|
+
paginationItem: 'ui-pagination__item',
|
|
119
|
+
avatar: 'ui-avatar',
|
|
120
|
+
avatarSm: 'ui-avatar--sm',
|
|
121
|
+
avatarLg: 'ui-avatar--lg',
|
|
122
|
+
avatarGroup: 'ui-avatar-group',
|
|
67
123
|
// table
|
|
68
124
|
table: 'ui-table',
|
|
69
125
|
tableDense: 'ui-table--dense',
|
|
@@ -84,6 +140,8 @@ export const cls = Object.freeze({
|
|
|
84
140
|
// typography / utilities
|
|
85
141
|
eyebrow: 'ui-eyebrow',
|
|
86
142
|
eyebrowMuted: 'ui-eyebrow--muted',
|
|
143
|
+
prose: 'ui-prose',
|
|
144
|
+
proseCompact: 'ui-prose--compact',
|
|
87
145
|
display: 'ui-display',
|
|
88
146
|
mono: 'ui-mono',
|
|
89
147
|
muted: 'ui-muted',
|
|
@@ -154,6 +212,33 @@ export const ui = {
|
|
|
154
212
|
hint: ({ error } = {}) => j(cls.hint, error && cls.hintError),
|
|
155
213
|
cluster: ({ between } = {}) => j(cls.cluster, between && cls.clusterBetween),
|
|
156
214
|
stagger: ({ auto } = {}) => j(cls.stagger, auto && cls.staggerAuto),
|
|
215
|
+
alert: ({ tone } = {}) =>
|
|
216
|
+
j(
|
|
217
|
+
cls.alert,
|
|
218
|
+
tone === 'accent' && cls.alertAccent,
|
|
219
|
+
tone === 'success' && cls.alertSuccess,
|
|
220
|
+
tone === 'warning' && cls.alertWarning,
|
|
221
|
+
tone === 'danger' && cls.alertDanger
|
|
222
|
+
),
|
|
223
|
+
toast: ({ tone } = {}) =>
|
|
224
|
+
j(
|
|
225
|
+
cls.toast,
|
|
226
|
+
tone === 'accent' && cls.toastAccent,
|
|
227
|
+
tone === 'success' && cls.toastSuccess,
|
|
228
|
+
tone === 'warning' && cls.toastWarning,
|
|
229
|
+
tone === 'danger' && cls.toastDanger
|
|
230
|
+
),
|
|
231
|
+
progress: ({ indeterminate } = {}) =>
|
|
232
|
+
j(cls.progress, indeterminate && cls.progressIndeterminate),
|
|
233
|
+
dotspinner: ({ size } = {}) =>
|
|
234
|
+
j(cls.dotspinner, size === 'sm' && cls.dotspinnerSm, size === 'lg' && cls.dotspinnerLg),
|
|
235
|
+
dotbar: ({ indeterminate } = {}) =>
|
|
236
|
+
j(cls.dotbar, indeterminate && cls.dotbarIndeterminate),
|
|
237
|
+
modal: ({ drawer } = {}) => j(cls.modal, drawer && cls.modalDrawer),
|
|
238
|
+
tab: ({ active } = {}) => j(cls.tab, active && 'is-active'),
|
|
239
|
+
avatar: ({ size } = {}) =>
|
|
240
|
+
j(cls.avatar, size === 'sm' && cls.avatarSm, size === 'lg' && cls.avatarLg),
|
|
241
|
+
prose: ({ compact } = {}) => j(cls.prose, compact && cls.proseCompact),
|
|
157
242
|
};
|
|
158
243
|
|
|
159
244
|
export default ui;
|
package/css/app.css
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
2
|
app — admin shell (sidebar + topbar + panels + metrics)
|
|
3
|
-
|
|
3
|
+
For admin / operator dashboards.
|
|
4
4
|
========================================================================== */
|
|
5
5
|
|
|
6
6
|
.app-shell {
|
|
7
7
|
display: grid;
|
|
8
8
|
grid-template-columns: var(--app-rail, 14rem) minmax(0, 1fr);
|
|
9
|
-
min-
|
|
9
|
+
min-block-size: 100vh;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.app-shell--full {
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
|
|
18
18
|
.app-rail {
|
|
19
19
|
background: var(--panel);
|
|
20
|
-
border-
|
|
20
|
+
border-inline-end: 1px solid var(--line);
|
|
21
21
|
display: flex;
|
|
22
22
|
flex-direction: column;
|
|
23
23
|
gap: var(--space-md);
|
|
24
24
|
padding: var(--space-md);
|
|
25
25
|
position: sticky;
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
inset-block-start: 0;
|
|
27
|
+
block-size: 100vh;
|
|
28
28
|
overflow-y: auto;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
background: var(--accent);
|
|
45
45
|
border-radius: 50%;
|
|
46
46
|
content: '';
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
block-size: 0.5rem;
|
|
48
|
+
inline-size: 0.5rem;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
.app-nav {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
|
|
66
66
|
.app-nav a {
|
|
67
67
|
align-items: center;
|
|
68
|
-
border-
|
|
68
|
+
border-inline-start: 2px solid transparent;
|
|
69
69
|
color: var(--text-soft);
|
|
70
70
|
display: flex;
|
|
71
71
|
font-family: var(--mono);
|
|
@@ -82,9 +82,9 @@
|
|
|
82
82
|
background: currentColor;
|
|
83
83
|
border-radius: 50%;
|
|
84
84
|
content: '';
|
|
85
|
-
|
|
85
|
+
block-size: 0.34rem;
|
|
86
86
|
opacity: 0.45;
|
|
87
|
-
|
|
87
|
+
inline-size: 0.34rem;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
.app-nav a:hover {
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
|
|
95
95
|
.app-nav a.is-active {
|
|
96
96
|
background: var(--accent-soft);
|
|
97
|
-
border-
|
|
97
|
+
border-inline-start-color: var(--accent);
|
|
98
98
|
color: var(--accent);
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -103,12 +103,12 @@
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
.app-rail__foot {
|
|
106
|
-
border-
|
|
106
|
+
border-block-start: 1px solid var(--line);
|
|
107
107
|
color: var(--text-dim);
|
|
108
108
|
font-family: var(--mono);
|
|
109
109
|
font-size: var(--text-2xs);
|
|
110
|
-
margin-
|
|
111
|
-
padding-
|
|
110
|
+
margin-block-start: auto;
|
|
111
|
+
padding-block-start: var(--space-sm);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/* --- Main column --- */
|
|
@@ -116,20 +116,20 @@
|
|
|
116
116
|
.app-main {
|
|
117
117
|
display: flex;
|
|
118
118
|
flex-direction: column;
|
|
119
|
-
min-
|
|
119
|
+
min-inline-size: 0;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
.app-topbar {
|
|
123
123
|
align-items: center;
|
|
124
124
|
background: color-mix(in srgb, var(--bg) 86%, transparent);
|
|
125
125
|
backdrop-filter: blur(8px);
|
|
126
|
-
border-
|
|
126
|
+
border-block-end: 1px solid var(--line);
|
|
127
127
|
display: flex;
|
|
128
128
|
gap: var(--space-md);
|
|
129
129
|
justify-content: space-between;
|
|
130
130
|
padding: 0.75rem var(--space-lg);
|
|
131
131
|
position: sticky;
|
|
132
|
-
|
|
132
|
+
inset-block-start: 0;
|
|
133
133
|
z-index: 10;
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -272,10 +272,10 @@
|
|
|
272
272
|
|
|
273
273
|
.app-rail {
|
|
274
274
|
flex-direction: row;
|
|
275
|
-
|
|
275
|
+
block-size: auto;
|
|
276
276
|
overflow-x: auto;
|
|
277
277
|
position: sticky;
|
|
278
|
-
|
|
278
|
+
inset-block-start: 0;
|
|
279
279
|
z-index: 11;
|
|
280
280
|
}
|
|
281
281
|
|
|
@@ -293,14 +293,14 @@
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
.app-nav a {
|
|
296
|
-
border-
|
|
297
|
-
border-
|
|
296
|
+
border-inline-start: 0;
|
|
297
|
+
border-block-end: 2px solid transparent;
|
|
298
298
|
white-space: nowrap;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
.app-nav a.is-active {
|
|
302
|
-
border-
|
|
303
|
-
border-
|
|
302
|
+
border-inline-start: 0;
|
|
303
|
+
border-block-end-color: var(--accent);
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
.app-content {
|