@ponchia/ui 0.6.7 → 0.6.9
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 +129 -4
- package/README.md +4 -4
- package/annotations/index.d.ts.map +1 -1
- package/annotations/index.js +26 -9
- package/behaviors/carousel.d.ts.map +1 -1
- package/behaviors/carousel.js +145 -49
- package/behaviors/combobox.d.ts.map +1 -1
- package/behaviors/combobox.js +220 -92
- package/behaviors/command.d.ts.map +1 -1
- package/behaviors/command.js +74 -14
- package/behaviors/connectors.d.ts.map +1 -1
- package/behaviors/connectors.js +131 -32
- package/behaviors/crosshair.d.ts.map +1 -1
- package/behaviors/crosshair.js +47 -1
- package/behaviors/dialog.d.ts.map +1 -1
- package/behaviors/dialog.js +24 -9
- package/behaviors/disclosure.d.ts.map +1 -1
- package/behaviors/disclosure.js +33 -3
- package/behaviors/dismissible.d.ts.map +1 -1
- package/behaviors/dismissible.js +3 -2
- package/behaviors/forms.d.ts.map +1 -1
- package/behaviors/forms.js +211 -140
- package/behaviors/glyph.d.ts.map +1 -1
- package/behaviors/glyph.js +172 -132
- package/behaviors/inert.d.ts +1 -1
- package/behaviors/inert.d.ts.map +1 -1
- package/behaviors/inert.js +4 -3
- package/behaviors/internal.d.ts.map +1 -1
- package/behaviors/internal.js +4 -3
- package/behaviors/legend.d.ts +0 -5
- package/behaviors/legend.d.ts.map +1 -1
- package/behaviors/legend.js +45 -13
- package/behaviors/menu.d.ts.map +1 -1
- package/behaviors/menu.js +13 -8
- package/behaviors/modal.d.ts.map +1 -1
- package/behaviors/modal.js +77 -19
- package/behaviors/popover.d.ts +4 -3
- package/behaviors/popover.d.ts.map +1 -1
- package/behaviors/popover.js +94 -14
- package/behaviors/sources.d.ts.map +1 -1
- package/behaviors/sources.js +14 -2
- package/behaviors/splitter.d.ts.map +1 -1
- package/behaviors/splitter.js +149 -110
- package/behaviors/spotlight.d.ts.map +1 -1
- package/behaviors/spotlight.js +28 -2
- package/behaviors/table.d.ts +1 -1
- package/behaviors/table.d.ts.map +1 -1
- package/behaviors/table.js +108 -17
- package/behaviors/tabs.d.ts.map +1 -1
- package/behaviors/tabs.js +84 -20
- package/behaviors/theme.d.ts.map +1 -1
- package/behaviors/theme.js +25 -5
- package/behaviors/toast.js +5 -5
- package/classes/index.d.ts +15 -2
- package/classes/index.js +48 -35
- package/connectors/index.d.ts +41 -8
- package/connectors/index.d.ts.map +1 -1
- package/connectors/index.js +74 -19
- package/css/annotations.css +12 -0
- package/css/app.css +3 -4
- package/css/base.css +1 -1
- package/css/content.css +3 -3
- package/css/crosshair.css +27 -2
- package/css/disclosure.css +3 -3
- package/css/dots.css +4 -4
- package/css/feedback.css +8 -37
- package/css/forms.css +9 -12
- package/css/legend.css +1 -1
- package/css/marks.css +1 -1
- package/css/motion.css +6 -6
- package/css/navigation.css +12 -0
- package/css/overlay.css +5 -7
- package/css/primitives.css +14 -16
- package/css/sidenote.css +2 -2
- package/css/table.css +2 -2
- package/css/tokens.css +16 -0
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -1
- package/dist/css/annotations.css +1 -1
- package/dist/css/crosshair.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/navigation.css +1 -1
- package/dist/css/report-kit.css +1 -1
- package/dist/css/tokens.css +1 -1
- package/docs/adr/0001-color-system.md +3 -2
- package/docs/annotations.md +21 -1
- package/docs/architecture.md +74 -13
- package/docs/command.md +4 -1
- package/docs/connectors.md +16 -0
- package/docs/crosshair.md +1 -1
- package/docs/dots.md +4 -1
- package/docs/glyphs.md +11 -0
- package/docs/interop/react-flow.md +89 -0
- package/docs/migrations/0.2-to-0.3.md +1 -1
- package/docs/package-contract.md +7 -5
- package/docs/reporting.md +23 -12
- package/docs/stability.md +85 -9
- package/docs/theming.md +2 -2
- package/docs/usage.md +16 -2
- package/docs/vega.md +4 -4
- package/glyphs/glyphs.js +43 -33
- package/llms.txt +19 -8
- package/package.json +23 -4
- package/schemas/report-claims.v1.schema.json +1 -1
- package/svelte/index.d.ts +71 -45
- package/svelte/index.d.ts.map +1 -1
- package/svelte/index.js +29 -2
- package/tokens/index.js +2 -2
- package/vue/index.d.ts +42 -5
- package/vue/index.d.ts.map +1 -1
- package/vue/index.js +32 -1
package/behaviors/glyph.js
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import { hasDom, resolveHost, noop, collectHosts } from './internal.js';
|
|
2
2
|
import { GLYPH_SIZE, glyphCells, glyphMask } from '../glyphs/glyphs.js';
|
|
3
3
|
|
|
4
|
+
const GLYPH_CLEANUP = Symbol('bronto-glyph-cleanup');
|
|
5
|
+
|
|
4
6
|
function restoreAttr(el, name, prev) {
|
|
5
7
|
if (prev === null) el.removeAttribute(name);
|
|
6
8
|
else el.setAttribute(name, prev);
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
function restoreStyleProp(el, name, prev) {
|
|
12
|
+
if (prev) el.style.setProperty(name, prev);
|
|
13
|
+
else el.style.removeProperty(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cleanupEmptyClassAndStyle(el) {
|
|
17
|
+
if (el.getAttribute('class') === '') el.removeAttribute('class');
|
|
18
|
+
if (el.getAttribute('style') === '') el.removeAttribute('style');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function rememberCleanup(el, cleanups, cleanup) {
|
|
22
|
+
let done = false;
|
|
23
|
+
const wrapped = () => {
|
|
24
|
+
if (done) return;
|
|
25
|
+
done = true;
|
|
26
|
+
cleanup();
|
|
27
|
+
if (el[GLYPH_CLEANUP] === wrapped) delete el[GLYPH_CLEANUP];
|
|
28
|
+
};
|
|
29
|
+
el[GLYPH_CLEANUP] = wrapped;
|
|
30
|
+
cleanups.push(wrapped);
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
// `dot`/`gap`/`size` land in inline CSS, so allow only length/calc syntax —
|
|
10
34
|
// drop anything with a `;`/`{` that could open a second declaration (mirrors
|
|
11
35
|
// glyphs.js cssLen). Used for the mask path's --icon-size.
|
|
@@ -13,6 +37,151 @@ function cssLen(v) {
|
|
|
13
37
|
return v && /^[\w.%+\-*/()\s,]+$/.test(v) ? v : '';
|
|
14
38
|
}
|
|
15
39
|
|
|
40
|
+
function applyGlyphA11y(el, label) {
|
|
41
|
+
if (label) {
|
|
42
|
+
el.setAttribute('role', 'img');
|
|
43
|
+
el.setAttribute('aria-label', label);
|
|
44
|
+
el.removeAttribute('aria-hidden');
|
|
45
|
+
} else {
|
|
46
|
+
el.setAttribute('aria-hidden', 'true');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function expandMaskGlyph(el, name, label, cleanups) {
|
|
51
|
+
if (el.classList.contains('ui-icon') && el.style.getPropertyValue('--icon-mask')) return;
|
|
52
|
+
const mask = glyphMask(name);
|
|
53
|
+
if (!mask) return; // unknown glyph — leave the placeholder as-is
|
|
54
|
+
|
|
55
|
+
const hadIcon = el.classList.contains('ui-icon');
|
|
56
|
+
const hadMask = el.style.getPropertyValue('--icon-mask');
|
|
57
|
+
const hadSize = el.style.getPropertyValue('--icon-size');
|
|
58
|
+
const hadAriaHidden = el.getAttribute('aria-hidden');
|
|
59
|
+
const hadRole = el.getAttribute('role');
|
|
60
|
+
const hadAriaLabel = el.getAttribute('aria-label');
|
|
61
|
+
const size = cssLen(el.getAttribute('data-bronto-glyph-size'));
|
|
62
|
+
|
|
63
|
+
el.classList.add('ui-icon');
|
|
64
|
+
el.style.setProperty('--icon-mask', mask);
|
|
65
|
+
if (size) el.style.setProperty('--icon-size', size);
|
|
66
|
+
applyGlyphA11y(el, label);
|
|
67
|
+
|
|
68
|
+
rememberCleanup(el, cleanups, () => {
|
|
69
|
+
if (!hadIcon) el.classList.remove('ui-icon');
|
|
70
|
+
restoreStyleProp(el, '--icon-mask', hadMask);
|
|
71
|
+
restoreStyleProp(el, '--icon-size', hadSize);
|
|
72
|
+
restoreAttr(el, 'aria-hidden', hadAriaHidden);
|
|
73
|
+
restoreAttr(el, 'role', hadRole);
|
|
74
|
+
restoreAttr(el, 'aria-label', hadAriaLabel);
|
|
75
|
+
cleanupEmptyClassAndStyle(el);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function glyphAnimClass(animAttr) {
|
|
80
|
+
if (animAttr === 'reveal') return 'ui-dotmatrix--reveal';
|
|
81
|
+
if (animAttr === 'pulse') return 'ui-dotmatrix--pulse';
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function authoredDotSize(el) {
|
|
86
|
+
return (
|
|
87
|
+
el.style.getPropertyValue('--dotmatrix-dot') ||
|
|
88
|
+
(typeof getComputedStyle === 'function'
|
|
89
|
+
? getComputedStyle(el).getPropertyValue('--dotmatrix-dot').trim()
|
|
90
|
+
: '')
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function applyDefaultDotScale(el, solid, hadGap) {
|
|
95
|
+
const setDefaultDot = !authoredDotSize(el);
|
|
96
|
+
let setDefaultGap = false;
|
|
97
|
+
if (setDefaultDot) {
|
|
98
|
+
el.style.setProperty('--dotmatrix-dot', '0.08em');
|
|
99
|
+
if (!solid && !hadGap) {
|
|
100
|
+
el.style.setProperty('--dotmatrix-gap', '0.02em'); // tight, so it reads as one glyph
|
|
101
|
+
setDefaultGap = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { setDefaultDot, setDefaultGap };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dotCellClass(cell) {
|
|
108
|
+
if (!cell.on) return 'ui-dotmatrix__cell';
|
|
109
|
+
if (cell.tone === 'hot') return 'ui-dotmatrix__cell ui-dotmatrix__cell--hot';
|
|
110
|
+
if (cell.tone === 'accent') return 'ui-dotmatrix__cell ui-dotmatrix__cell--accent';
|
|
111
|
+
return 'ui-dotmatrix__cell';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function appendGlyphCells(el, cells, { solid, animAttr }) {
|
|
115
|
+
const frag = document.createDocumentFragment();
|
|
116
|
+
cells.forEach((cell, i) => {
|
|
117
|
+
const span = document.createElement('span');
|
|
118
|
+
span.className = dotCellClass(cell);
|
|
119
|
+
if (!cell.on && solid) span.style.background = 'transparent'; // glyph-only
|
|
120
|
+
if (animAttr === 'reveal') span.style.setProperty('--i', String(i)); // scan stagger
|
|
121
|
+
frag.appendChild(span);
|
|
122
|
+
});
|
|
123
|
+
el.appendChild(frag);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function expandCellGlyph(el, name, label, cleanups) {
|
|
127
|
+
// Scope to DIRECT-child cells (the ones we append) — so a placeholder that
|
|
128
|
+
// legitimately nests its own .ui-dotmatrix is neither mis-read as already
|
|
129
|
+
// expanded here nor have its inner cells removed by cleanup below.
|
|
130
|
+
if (el.querySelector(':scope > .ui-dotmatrix__cell')) return; // already expanded
|
|
131
|
+
const cells = glyphCells(name);
|
|
132
|
+
if (!cells.length) return; // unknown glyph — leave the placeholder as-is
|
|
133
|
+
|
|
134
|
+
// `data-bronto-glyph-solid` → square, gapless pixel glyph (legible small),
|
|
135
|
+
// the DOM counterpart to renderGlyph's `solid` option. Implies glyph-only.
|
|
136
|
+
const solid = el.hasAttribute('data-bronto-glyph-solid');
|
|
137
|
+
// `data-bronto-glyph-anim="reveal|pulse"` → decorative animation (the DOM
|
|
138
|
+
// counterpart to renderGlyph's `anim`; reduced-motion-safe via CSS).
|
|
139
|
+
const animAttr = el.getAttribute('data-bronto-glyph-anim');
|
|
140
|
+
const animClass = glyphAnimClass(animAttr);
|
|
141
|
+
const hadAnimClass = animClass ? el.classList.contains(animClass) : false;
|
|
142
|
+
const hadMatrix = el.classList.contains('ui-dotmatrix');
|
|
143
|
+
const hadCols = el.style.getPropertyValue('--dotmatrix-cols');
|
|
144
|
+
const hadRadius = el.style.getPropertyValue('--dotmatrix-dot-radius');
|
|
145
|
+
const hadGap = el.style.getPropertyValue('--dotmatrix-gap');
|
|
146
|
+
const hadAriaHidden = el.getAttribute('aria-hidden');
|
|
147
|
+
const hadRole = el.getAttribute('role');
|
|
148
|
+
const hadAriaLabel = el.getAttribute('aria-label');
|
|
149
|
+
|
|
150
|
+
el.classList.add('ui-dotmatrix');
|
|
151
|
+
if (animClass) el.classList.add(animClass);
|
|
152
|
+
el.style.setProperty('--dotmatrix-cols', String(GLYPH_SIZE));
|
|
153
|
+
if (solid) {
|
|
154
|
+
el.style.setProperty('--dotmatrix-dot-radius', '0');
|
|
155
|
+
el.style.setProperty('--dotmatrix-gap', '0');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Without a track size the grid cells default to `1fr`, so the 16×16 matrix
|
|
159
|
+
// balloons to fill its container (full-bleed) — asymmetric with the mask
|
|
160
|
+
// path's safe 1em. If the author set no `--dotmatrix-dot` (inline OR via the
|
|
161
|
+
// cascade), default it to an intrinsic icon scale so a forgotten size
|
|
162
|
+
// degrades to ~icon, not full-bleed.
|
|
163
|
+
const defaults = applyDefaultDotScale(el, solid, hadGap);
|
|
164
|
+
applyGlyphA11y(el, label);
|
|
165
|
+
appendGlyphCells(el, cells, { solid, animAttr });
|
|
166
|
+
|
|
167
|
+
rememberCleanup(el, cleanups, () => {
|
|
168
|
+
el.querySelectorAll(':scope > .ui-dotmatrix__cell').forEach((n) => n.remove());
|
|
169
|
+
if (!hadMatrix) el.classList.remove('ui-dotmatrix');
|
|
170
|
+
if (animClass && !hadAnimClass) el.classList.remove(animClass);
|
|
171
|
+
if (solid) {
|
|
172
|
+
restoreStyleProp(el, '--dotmatrix-dot-radius', hadRadius);
|
|
173
|
+
restoreStyleProp(el, '--dotmatrix-gap', hadGap);
|
|
174
|
+
}
|
|
175
|
+
if (defaults.setDefaultDot) el.style.removeProperty('--dotmatrix-dot');
|
|
176
|
+
if (defaults.setDefaultGap) el.style.removeProperty('--dotmatrix-gap');
|
|
177
|
+
restoreStyleProp(el, '--dotmatrix-cols', hadCols);
|
|
178
|
+
restoreAttr(el, 'aria-hidden', hadAriaHidden);
|
|
179
|
+
restoreAttr(el, 'role', hadRole);
|
|
180
|
+
restoreAttr(el, 'aria-label', hadAriaLabel);
|
|
181
|
+
cleanupEmptyClassAndStyle(el);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
16
185
|
/**
|
|
17
186
|
* Expand `[data-bronto-glyph="name"]` placeholders into a `.ui-dotmatrix`
|
|
18
187
|
* grid of GLYPH_SIZE² cells — the DOM counterpart to renderGlyph() from
|
|
@@ -40,146 +209,17 @@ export function initDotGlyph({ root } = {}) {
|
|
|
40
209
|
const cleanups = [];
|
|
41
210
|
|
|
42
211
|
for (const el of els) {
|
|
212
|
+
el[GLYPH_CLEANUP]?.();
|
|
43
213
|
const name = el.getAttribute('data-bronto-glyph');
|
|
44
214
|
const label = el.getAttribute('data-bronto-glyph-label');
|
|
45
215
|
|
|
46
216
|
// One-node mask path — the icon-at-scale counterpart to the 256-cell grid.
|
|
47
217
|
if (el.getAttribute('data-bronto-glyph-render') === 'mask') {
|
|
48
|
-
|
|
49
|
-
const mask = glyphMask(name);
|
|
50
|
-
if (!mask) continue; // unknown glyph — leave the placeholder as-is
|
|
51
|
-
const hadIcon = el.classList.contains('ui-icon');
|
|
52
|
-
const hadMask = el.style.getPropertyValue('--icon-mask');
|
|
53
|
-
const hadSize = el.style.getPropertyValue('--icon-size');
|
|
54
|
-
const hadAriaHiddenM = el.getAttribute('aria-hidden');
|
|
55
|
-
const hadRoleM = el.getAttribute('role');
|
|
56
|
-
const hadAriaLabelM = el.getAttribute('aria-label');
|
|
57
|
-
const sizeM = cssLen(el.getAttribute('data-bronto-glyph-size'));
|
|
58
|
-
|
|
59
|
-
el.classList.add('ui-icon');
|
|
60
|
-
el.style.setProperty('--icon-mask', mask);
|
|
61
|
-
if (sizeM) el.style.setProperty('--icon-size', sizeM);
|
|
62
|
-
if (label) {
|
|
63
|
-
el.setAttribute('role', 'img');
|
|
64
|
-
el.setAttribute('aria-label', label);
|
|
65
|
-
el.removeAttribute('aria-hidden');
|
|
66
|
-
} else {
|
|
67
|
-
el.setAttribute('aria-hidden', 'true');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
cleanups.push(() => {
|
|
71
|
-
if (!hadIcon) el.classList.remove('ui-icon');
|
|
72
|
-
if (hadMask) el.style.setProperty('--icon-mask', hadMask);
|
|
73
|
-
else el.style.removeProperty('--icon-mask');
|
|
74
|
-
if (sizeM && !hadSize) el.style.removeProperty('--icon-size');
|
|
75
|
-
else if (hadSize) el.style.setProperty('--icon-size', hadSize);
|
|
76
|
-
restoreAttr(el, 'aria-hidden', hadAriaHiddenM);
|
|
77
|
-
restoreAttr(el, 'role', hadRoleM);
|
|
78
|
-
restoreAttr(el, 'aria-label', hadAriaLabelM);
|
|
79
|
-
if (el.getAttribute('class') === '') el.removeAttribute('class');
|
|
80
|
-
if (el.getAttribute('style') === '') el.removeAttribute('style');
|
|
81
|
-
});
|
|
218
|
+
expandMaskGlyph(el, name, label, cleanups);
|
|
82
219
|
continue;
|
|
83
220
|
}
|
|
84
221
|
|
|
85
|
-
|
|
86
|
-
// legitimately nests its own .ui-dotmatrix is neither mis-read as already
|
|
87
|
-
// expanded here nor have its inner cells removed by cleanup below.
|
|
88
|
-
if (el.querySelector(':scope > .ui-dotmatrix__cell')) continue; // already expanded
|
|
89
|
-
const cells = glyphCells(name);
|
|
90
|
-
if (!cells.length) continue; // unknown glyph — leave the placeholder as-is
|
|
91
|
-
// `data-bronto-glyph-solid` → square, gapless pixel glyph (legible small),
|
|
92
|
-
// the DOM counterpart to renderGlyph's `solid` option. Implies glyph-only.
|
|
93
|
-
const solid = el.hasAttribute('data-bronto-glyph-solid');
|
|
94
|
-
// `data-bronto-glyph-anim="reveal|pulse"` → decorative animation (the DOM
|
|
95
|
-
// counterpart to renderGlyph's `anim`; reduced-motion-safe via CSS).
|
|
96
|
-
const animAttr = el.getAttribute('data-bronto-glyph-anim');
|
|
97
|
-
const animClass =
|
|
98
|
-
animAttr === 'reveal'
|
|
99
|
-
? 'ui-dotmatrix--reveal'
|
|
100
|
-
: animAttr === 'pulse'
|
|
101
|
-
? 'ui-dotmatrix--pulse'
|
|
102
|
-
: null;
|
|
103
|
-
const hadAnimClass = animClass ? el.classList.contains(animClass) : false;
|
|
104
|
-
const hadMatrix = el.classList.contains('ui-dotmatrix');
|
|
105
|
-
const hadCols = el.style.getPropertyValue('--dotmatrix-cols');
|
|
106
|
-
const hadRadius = el.style.getPropertyValue('--dotmatrix-dot-radius');
|
|
107
|
-
const hadGap = el.style.getPropertyValue('--dotmatrix-gap');
|
|
108
|
-
const hadAriaHidden = el.getAttribute('aria-hidden');
|
|
109
|
-
const hadRole = el.getAttribute('role');
|
|
110
|
-
const hadAriaLabel = el.getAttribute('aria-label');
|
|
111
|
-
|
|
112
|
-
el.classList.add('ui-dotmatrix');
|
|
113
|
-
if (animClass) el.classList.add(animClass);
|
|
114
|
-
el.style.setProperty('--dotmatrix-cols', String(GLYPH_SIZE));
|
|
115
|
-
if (solid) {
|
|
116
|
-
el.style.setProperty('--dotmatrix-dot-radius', '0');
|
|
117
|
-
el.style.setProperty('--dotmatrix-gap', '0');
|
|
118
|
-
}
|
|
119
|
-
// Without a track size the grid cells default to `1fr`, so the 16×16 matrix
|
|
120
|
-
// balloons to fill its container (full-bleed) — asymmetric with the mask
|
|
121
|
-
// path's safe 1em. If the author set no `--dotmatrix-dot` (inline OR via the
|
|
122
|
-
// cascade), default it to an intrinsic icon scale so a forgotten size
|
|
123
|
-
// degrades to ~icon, not full-bleed. (component audit C9.)
|
|
124
|
-
const authoredDot =
|
|
125
|
-
el.style.getPropertyValue('--dotmatrix-dot') ||
|
|
126
|
-
(typeof getComputedStyle === 'function'
|
|
127
|
-
? getComputedStyle(el).getPropertyValue('--dotmatrix-dot').trim()
|
|
128
|
-
: '');
|
|
129
|
-
const setDefaultDot = !authoredDot;
|
|
130
|
-
let setDefaultGap = false;
|
|
131
|
-
if (setDefaultDot) {
|
|
132
|
-
el.style.setProperty('--dotmatrix-dot', '0.08em');
|
|
133
|
-
if (!solid && !hadGap) {
|
|
134
|
-
el.style.setProperty('--dotmatrix-gap', '0.02em'); // tight, so it reads as one glyph
|
|
135
|
-
setDefaultGap = true;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (label) {
|
|
139
|
-
el.setAttribute('role', 'img');
|
|
140
|
-
el.setAttribute('aria-label', label);
|
|
141
|
-
el.removeAttribute('aria-hidden'); // a labelled img must not also be hidden
|
|
142
|
-
} else {
|
|
143
|
-
el.setAttribute('aria-hidden', 'true');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const frag = document.createDocumentFragment();
|
|
147
|
-
cells.forEach((c, i) => {
|
|
148
|
-
const span = document.createElement('span');
|
|
149
|
-
span.className = !c.on
|
|
150
|
-
? 'ui-dotmatrix__cell'
|
|
151
|
-
: c.tone === 'hot'
|
|
152
|
-
? 'ui-dotmatrix__cell ui-dotmatrix__cell--hot'
|
|
153
|
-
: c.tone === 'accent'
|
|
154
|
-
? 'ui-dotmatrix__cell ui-dotmatrix__cell--accent'
|
|
155
|
-
: 'ui-dotmatrix__cell';
|
|
156
|
-
if (!c.on && solid) span.style.background = 'transparent'; // glyph-only
|
|
157
|
-
if (animAttr === 'reveal') span.style.setProperty('--i', String(i)); // scan stagger
|
|
158
|
-
frag.appendChild(span);
|
|
159
|
-
});
|
|
160
|
-
el.appendChild(frag);
|
|
161
|
-
|
|
162
|
-
cleanups.push(() => {
|
|
163
|
-
el.querySelectorAll(':scope > .ui-dotmatrix__cell').forEach((n) => n.remove());
|
|
164
|
-
if (!hadMatrix) el.classList.remove('ui-dotmatrix');
|
|
165
|
-
if (animClass && !hadAnimClass) el.classList.remove(animClass);
|
|
166
|
-
if (solid) {
|
|
167
|
-
if (hadRadius) el.style.setProperty('--dotmatrix-dot-radius', hadRadius);
|
|
168
|
-
else el.style.removeProperty('--dotmatrix-dot-radius');
|
|
169
|
-
if (hadGap) el.style.setProperty('--dotmatrix-gap', hadGap);
|
|
170
|
-
else el.style.removeProperty('--dotmatrix-gap');
|
|
171
|
-
}
|
|
172
|
-
if (setDefaultDot) el.style.removeProperty('--dotmatrix-dot');
|
|
173
|
-
if (setDefaultGap) el.style.removeProperty('--dotmatrix-gap');
|
|
174
|
-
if (hadCols) el.style.setProperty('--dotmatrix-cols', hadCols);
|
|
175
|
-
else el.style.removeProperty('--dotmatrix-cols');
|
|
176
|
-
restoreAttr(el, 'aria-hidden', hadAriaHidden);
|
|
177
|
-
restoreAttr(el, 'role', hadRole);
|
|
178
|
-
restoreAttr(el, 'aria-label', hadAriaLabel);
|
|
179
|
-
// Don't leave behind empty class=""/style="" we ourselves created.
|
|
180
|
-
if (el.getAttribute('class') === '') el.removeAttribute('class');
|
|
181
|
-
if (el.getAttribute('style') === '') el.removeAttribute('style');
|
|
182
|
-
});
|
|
222
|
+
expandCellGlyph(el, name, label, cleanups);
|
|
183
223
|
}
|
|
184
224
|
|
|
185
225
|
return () => cleanups.forEach((fn) => fn());
|
package/behaviors/inert.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Wire once near the root, like {@link applyStoredTheme}. Capturing listeners
|
|
13
13
|
* intercept activation before any component handler (tabs, pagination, menus)
|
|
14
|
-
* sees it.
|
|
14
|
+
* sees it.
|
|
15
15
|
*
|
|
16
16
|
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
17
17
|
* @returns {import('./internal.js').Cleanup}
|
package/behaviors/inert.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inert.d.ts","sourceRoot":"","sources":["inert.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AACH,6CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"inert.d.ts","sourceRoot":"","sources":["inert.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AACH,6CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA0B3C"}
|
package/behaviors/inert.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce, closestSafe } from './internal.js';
|
|
2
2
|
|
|
3
3
|
const DISABLED = '[aria-disabled="true"]';
|
|
4
4
|
|
|
@@ -15,7 +15,7 @@ const DISABLED = '[aria-disabled="true"]';
|
|
|
15
15
|
*
|
|
16
16
|
* Wire once near the root, like {@link applyStoredTheme}. Capturing listeners
|
|
17
17
|
* intercept activation before any component handler (tabs, pagination, menus)
|
|
18
|
-
* sees it.
|
|
18
|
+
* sees it.
|
|
19
19
|
*
|
|
20
20
|
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
21
21
|
* @returns {import('./internal.js').Cleanup}
|
|
@@ -25,9 +25,10 @@ export function initDisabledGuard({ root } = {}) {
|
|
|
25
25
|
const host = resolveHost(root);
|
|
26
26
|
if (!host) return noop;
|
|
27
27
|
const block = (e) => {
|
|
28
|
-
const el = e.target
|
|
28
|
+
const el = closestSafe(e.target, DISABLED);
|
|
29
29
|
if (el && host.contains(el)) {
|
|
30
30
|
e.preventDefault();
|
|
31
|
+
e.stopImmediatePropagation?.();
|
|
31
32
|
e.stopPropagation();
|
|
32
33
|
}
|
|
33
34
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AAiDA,iEAIC;AAeD,sEAUC;AAED,oDAQC;AAED,
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AAiDA,iEAIC;AAeD,sEAUC;AAED,oDAQC;AAED,yDAOC;AAMD,8DAIC;AAID;;SAMC;AAUD,gDAQC;AAMD,+DAKC;AAlIM,6BAAqB;AAErB,kCAAoD;AAyCpD,uCAAqC;;;;;sBArD/B,MAAM,IAAI"}
|
package/behaviors/internal.js
CHANGED
|
@@ -90,7 +90,8 @@ export function byIdInHost(host, id) {
|
|
|
90
90
|
|
|
91
91
|
export function closestSafe(el, selector) {
|
|
92
92
|
try {
|
|
93
|
-
|
|
93
|
+
const start = el?.nodeType === 1 ? el : el?.parentElement;
|
|
94
|
+
return start?.closest?.(selector) ?? null;
|
|
94
95
|
} catch {
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
@@ -120,7 +121,7 @@ export function scrollIntoViewSafe(el, opts = { block: 'nearest' }) {
|
|
|
120
121
|
// by the modal and popover focus paths (a dialog/modal must move focus into
|
|
121
122
|
// itself on open). Focus the first focusable descendant, else make the
|
|
122
123
|
// container programmatically focusable and focus it, so a content-only
|
|
123
|
-
// panel/modal still receives focus.
|
|
124
|
+
// panel/modal still receives focus.
|
|
124
125
|
const FOCUSABLE =
|
|
125
126
|
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
126
127
|
|
|
@@ -137,7 +138,7 @@ export function focusInto(container) {
|
|
|
137
138
|
// Wrap an index by `delta` within [0, len), the roving keyboard math shared by
|
|
138
139
|
// the combobox and command listboxes (a -1 `cur` lands on the first/last as
|
|
139
140
|
// before). Only this core is shared — the surrounding setActive/filter/group
|
|
140
|
-
// logic diverges between the two for real reasons.
|
|
141
|
+
// logic diverges between the two for real reasons.
|
|
141
142
|
export function wrapIndex(cur, delta, len) {
|
|
142
143
|
let next = cur + delta;
|
|
143
144
|
if (next < 0) next = len - 1;
|
package/behaviors/legend.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {object} LegendToggleDetail
|
|
3
|
-
* @property {string | number} series The entry's `data-series`, or its 0-based index when unset.
|
|
4
|
-
* @property {boolean} active The new state (`true` ⇒ series shown).
|
|
5
|
-
*/
|
|
6
1
|
/**
|
|
7
2
|
* Wire `[data-bronto-legend]` interactive legends. Each entry is a
|
|
8
3
|
* `.ui-legend__item` authored as a `<button aria-pressed>`; clicking it (or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"legend.d.ts","sourceRoot":"","sources":["legend.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"legend.d.ts","sourceRoot":"","sources":["legend.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;GAiBG;AACH,sCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAiH3C;;;;;YAvIa,MAAM,GAAG,MAAM;;;;YACf,OAAO"}
|
package/behaviors/legend.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce, collectHosts, closestSafe } from './internal.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {object} LegendToggleDetail
|
|
@@ -6,6 +6,8 @@ import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
|
|
|
6
6
|
* @property {boolean} active The new state (`true` ⇒ series shown).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const handledEvents = new WeakSet();
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Wire `[data-bronto-legend]` interactive legends. Each entry is a
|
|
11
13
|
* `.ui-legend__item` authored as a `<button aria-pressed>`; clicking it (or
|
|
@@ -28,6 +30,26 @@ export function initLegend({ root } = {}) {
|
|
|
28
30
|
if (!hasDom()) return noop;
|
|
29
31
|
const host = resolveHost(root);
|
|
30
32
|
if (!host) return noop;
|
|
33
|
+
const snapshotAttrs = (el, names) => {
|
|
34
|
+
const attrs = {};
|
|
35
|
+
for (const name of names) {
|
|
36
|
+
attrs[name] = {
|
|
37
|
+
had: el.hasAttribute(name),
|
|
38
|
+
value: el.getAttribute(name),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return attrs;
|
|
42
|
+
};
|
|
43
|
+
const restoreAttrs = (el, attrs) => {
|
|
44
|
+
for (const [name, state] of Object.entries(attrs)) {
|
|
45
|
+
if (state.had) el.setAttribute(name, state.value);
|
|
46
|
+
else el.removeAttribute(name);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const directItems = (legend) =>
|
|
50
|
+
[...legend.querySelectorAll('.ui-legend__item')].filter(
|
|
51
|
+
(el) => el.closest('[data-bronto-legend]') === legend,
|
|
52
|
+
);
|
|
31
53
|
const isButton = (el) => el.tagName === 'BUTTON' || el.getAttribute('role') === 'button';
|
|
32
54
|
const legendFor = (item) => {
|
|
33
55
|
if (!item || !host.contains(item)) return;
|
|
@@ -37,11 +59,11 @@ export function initLegend({ root } = {}) {
|
|
|
37
59
|
};
|
|
38
60
|
const toggle = (item) => {
|
|
39
61
|
const legend = legendFor(item);
|
|
40
|
-
if (!legend) return;
|
|
62
|
+
if (!legend) return false;
|
|
41
63
|
// The contract requires a real `<button>` (keyboard-operable, focusable). A
|
|
42
64
|
// non-button item is mouse-only unless role=button is keyboard-normalized
|
|
43
65
|
// below — refuse anything else rather than ship a pointer-only control.
|
|
44
|
-
if (!isButton(item)) return;
|
|
66
|
+
if (!isButton(item)) return false;
|
|
45
67
|
const active = item.getAttribute('aria-pressed') !== 'false';
|
|
46
68
|
const next = !active;
|
|
47
69
|
item.setAttribute('aria-pressed', String(next));
|
|
@@ -57,25 +79,33 @@ export function initLegend({ root } = {}) {
|
|
|
57
79
|
detail: { series: item.dataset.series ?? items.indexOf(item), active: next },
|
|
58
80
|
}),
|
|
59
81
|
);
|
|
82
|
+
return true;
|
|
60
83
|
};
|
|
61
84
|
const onClick = (e) => {
|
|
62
|
-
|
|
85
|
+
if (handledEvents.has(e)) return;
|
|
86
|
+
if (toggle(closestSafe(e.target, '.ui-legend__item'))) handledEvents.add(e);
|
|
63
87
|
};
|
|
64
88
|
const onKey = (e) => {
|
|
89
|
+
if (handledEvents.has(e)) return;
|
|
65
90
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
66
|
-
const item = e.target
|
|
91
|
+
const item = closestSafe(e.target, '.ui-legend__item');
|
|
67
92
|
if (!item || item.tagName === 'BUTTON' || item.getAttribute('role') !== 'button') return;
|
|
68
93
|
e.preventDefault();
|
|
69
|
-
toggle(item);
|
|
94
|
+
if (toggle(item)) handledEvents.add(e);
|
|
70
95
|
};
|
|
71
96
|
return bindOnce(host, 'legend', () => {
|
|
72
97
|
// Normalize role=button entries and warn once per unsupported non-button
|
|
73
98
|
// item present at bind. A real <button> remains the recommended markup.
|
|
74
|
-
const legends =
|
|
99
|
+
const legends = collectHosts(host, '[data-bronto-legend]');
|
|
100
|
+
const itemStates = [];
|
|
75
101
|
for (const legend of legends) {
|
|
76
|
-
for (const el of legend
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
for (const el of directItems(legend)) {
|
|
103
|
+
itemStates.push({
|
|
104
|
+
el,
|
|
105
|
+
attrs: snapshotAttrs(el, ['type', 'tabindex', 'aria-pressed']),
|
|
106
|
+
inactive: el.classList.contains('is-inactive'),
|
|
107
|
+
});
|
|
108
|
+
if (el.tagName === 'BUTTON' && !el.hasAttribute('type')) el.setAttribute('type', 'button');
|
|
79
109
|
if (
|
|
80
110
|
el.tagName !== 'BUTTON' &&
|
|
81
111
|
el.getAttribute('role') === 'button' &&
|
|
@@ -87,9 +117,7 @@ export function initLegend({ root } = {}) {
|
|
|
87
117
|
}
|
|
88
118
|
if (typeof console !== 'undefined') {
|
|
89
119
|
for (const legend of legends) {
|
|
90
|
-
const stray =
|
|
91
|
-
(el) => el.closest('[data-bronto-legend]') === legend && !isButton(el),
|
|
92
|
-
);
|
|
120
|
+
const stray = directItems(legend).some((el) => !isButton(el));
|
|
93
121
|
if (stray) {
|
|
94
122
|
console.warn(
|
|
95
123
|
'[bronto] initLegend(): interactive legend entries must be <button> or role="button" — unsupported .ui-legend__item controls are ignored.',
|
|
@@ -103,6 +131,10 @@ export function initLegend({ root } = {}) {
|
|
|
103
131
|
return () => {
|
|
104
132
|
host.removeEventListener('click', onClick);
|
|
105
133
|
host.removeEventListener('keydown', onKey);
|
|
134
|
+
for (const state of itemStates) {
|
|
135
|
+
restoreAttrs(state.el, state.attrs);
|
|
136
|
+
state.el.classList.toggle('is-inactive', state.inactive);
|
|
137
|
+
}
|
|
106
138
|
};
|
|
107
139
|
});
|
|
108
140
|
}
|
package/behaviors/menu.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["menu.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["menu.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAwC3C"}
|
package/behaviors/menu.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce, closestSafe, collectHosts } from './internal.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Dropdown-menu close affordances for a native `<details data-bronto-menu>`
|
|
@@ -18,32 +18,37 @@ export function initMenu({ root } = {}) {
|
|
|
18
18
|
if (!hasDom()) return noop;
|
|
19
19
|
const host = resolveHost(root);
|
|
20
20
|
if (!host) return noop;
|
|
21
|
-
const
|
|
21
|
+
const doc = host.nodeType === 9 ? host : host.ownerDocument || document;
|
|
22
|
+
const openMenus = () => collectHosts(host, '[data-bronto-menu][open]');
|
|
22
23
|
const shut = (menu) => {
|
|
23
24
|
if (!menu || !menu.open) return;
|
|
24
25
|
menu.open = false;
|
|
25
26
|
menu.querySelector('summary')?.focus();
|
|
26
27
|
};
|
|
27
28
|
const onClick = (e) => {
|
|
28
|
-
const
|
|
29
|
+
const target = e.target;
|
|
30
|
+
const menu = closestSafe(target, '[data-bronto-menu]');
|
|
29
31
|
// Activate an item → close its menu (and return focus to summary).
|
|
30
|
-
if (menu &&
|
|
32
|
+
if (menu && host.contains(menu) && closestSafe(target, '.ui-menu__item')) {
|
|
31
33
|
shut(menu);
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
// Click outside any open menu → close them all (no focus move).
|
|
35
|
-
for (const m of openMenus()) if (!m.contains(
|
|
37
|
+
for (const m of openMenus()) if (!m.contains(target)) m.open = false;
|
|
36
38
|
};
|
|
37
39
|
const onKey = (e) => {
|
|
38
40
|
if (e.key !== 'Escape') return;
|
|
39
|
-
const menu = e.target
|
|
41
|
+
const menu = closestSafe(e.target, '[data-bronto-menu][open]') || openMenus()[0];
|
|
42
|
+
if (!menu) return;
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
e.stopPropagation();
|
|
40
45
|
shut(menu);
|
|
41
46
|
};
|
|
42
47
|
return bindOnce(host, 'menu', () => {
|
|
43
|
-
|
|
48
|
+
doc.addEventListener('click', onClick);
|
|
44
49
|
host.addEventListener('keydown', onKey);
|
|
45
50
|
return () => {
|
|
46
|
-
|
|
51
|
+
doc.removeEventListener('click', onClick);
|
|
47
52
|
host.removeEventListener('keydown', onKey);
|
|
48
53
|
};
|
|
49
54
|
});
|
package/behaviors/modal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"AAsDA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA+F3C;;;;;YA7Ha,QAAQ"}
|