@ponchia/ui 0.6.6 → 0.6.8
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 +175 -6
- package/README.md +38 -25
- package/annotations/index.d.ts.map +1 -1
- package/annotations/index.js +21 -3
- package/behaviors/carousel.d.ts.map +1 -1
- package/behaviors/carousel.js +91 -32
- package/behaviors/combobox.d.ts.map +1 -1
- package/behaviors/combobox.js +117 -43
- 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 +92 -9
- 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 +37 -16
- 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 +78 -5
- package/behaviors/glyph.d.ts.map +1 -1
- package/behaviors/glyph.js +17 -2
- package/behaviors/index.d.ts +2 -0
- package/behaviors/index.d.ts.map +1 -1
- package/behaviors/index.js +2 -0
- package/behaviors/inert.js +3 -2
- package/behaviors/internal.d.ts +2 -1
- package/behaviors/internal.d.ts.map +1 -1
- package/behaviors/internal.js +25 -4
- package/behaviors/legend.d.ts +0 -5
- package/behaviors/legend.d.ts.map +1 -1
- package/behaviors/legend.js +78 -14
- 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 +89 -9
- package/behaviors/sources.d.ts.map +1 -1
- package/behaviors/sources.js +14 -2
- package/behaviors/splitter.d.ts +26 -0
- package/behaviors/splitter.d.ts.map +1 -0
- package/behaviors/splitter.js +239 -0
- package/behaviors/spotlight.d.ts.map +1 -1
- package/behaviors/spotlight.js +28 -2
- package/behaviors/table.d.ts.map +1 -1
- package/behaviors/table.js +105 -13
- package/behaviors/tabs.d.ts.map +1 -1
- package/behaviors/tabs.js +82 -18
- package/behaviors/theme.d.ts.map +1 -1
- package/behaviors/theme.js +26 -6
- package/classes/classes.json +230 -4
- package/classes/index.d.ts +64 -3
- package/classes/index.js +56 -2
- package/classes/vscode.css-custom-data.json +1 -1
- package/connectors/index.d.ts +39 -6
- package/connectors/index.d.ts.map +1 -1
- package/connectors/index.js +67 -9
- package/css/analytical.css +3 -1
- package/css/annotations.css +12 -0
- package/css/app.css +4 -4
- package/css/clamp.css +92 -0
- package/css/crosshair.css +27 -2
- package/css/feedback.css +2 -30
- package/css/figure.css +102 -0
- package/css/highlights.css +50 -0
- package/css/interval.css +90 -0
- package/css/navigation.css +12 -0
- package/css/primitives.css +2 -3
- package/css/report-kit.css +38 -0
- package/css/report.css +23 -4
- package/css/sidenote.css +12 -2
- package/css/site.css +2 -1
- package/css/sources.css +5 -0
- package/css/state.css +120 -1
- package/css/table.css +4 -0
- package/css/tokens.css +25 -9
- package/css/workbench.css +101 -8
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -1
- package/dist/css/annotations.css +1 -1
- package/dist/css/app.css +1 -1
- package/dist/css/clamp.css +1 -0
- package/dist/css/crosshair.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/figure.css +1 -0
- package/dist/css/highlights.css +1 -0
- package/dist/css/interval.css +1 -0
- package/dist/css/navigation.css +1 -1
- package/dist/css/primitives.css +1 -1
- package/dist/css/report-kit.css +1 -0
- package/dist/css/report.css +1 -1
- package/dist/css/sidenote.css +1 -1
- package/dist/css/site.css +1 -1
- package/dist/css/sources.css +1 -1
- package/dist/css/state.css +1 -1
- package/dist/css/table.css +1 -1
- package/dist/css/tokens.css +1 -1
- package/dist/css/workbench.css +1 -1
- package/docs/adr/0001-color-system.md +3 -2
- package/docs/adr/0002-scope-and-2026-baseline.md +1 -1
- package/docs/annotations.md +12 -1
- package/docs/architecture.md +105 -48
- package/docs/clamp.md +49 -0
- package/docs/command.md +4 -1
- package/docs/connectors.md +16 -0
- package/docs/contrast.md +34 -24
- package/docs/crosshair.md +1 -1
- package/docs/d2.md +37 -0
- package/docs/dots.md +4 -1
- package/docs/figure.md +71 -0
- package/docs/frontier-primitives.md +25 -24
- package/docs/glyphs.md +11 -0
- package/docs/highlights.md +52 -0
- package/docs/interop/tailwind.md +148 -0
- package/docs/interval.md +55 -0
- package/docs/legends.md +3 -2
- package/docs/mermaid.md +6 -0
- package/docs/migrations/0.2-to-0.3.md +80 -0
- package/docs/migrations/0.3-to-0.4.md +48 -0
- package/docs/migrations/0.4-to-0.5.md +96 -0
- package/docs/migrations/0.5-to-0.6.md +82 -0
- package/docs/package-contract.md +44 -6
- package/docs/reference.md +78 -5
- package/docs/reporting.md +126 -60
- package/docs/sidenote.md +7 -1
- package/docs/sources.md +1 -1
- package/docs/stability.md +23 -5
- package/docs/state.md +67 -10
- package/docs/theming.md +12 -4
- package/docs/usage.md +47 -13
- package/docs/vega.md +4 -4
- package/docs/workbench.md +59 -18
- package/llms.txt +89 -16
- package/package.json +82 -6
- package/qwik/index.d.ts +1 -0
- package/qwik/index.d.ts.map +1 -1
- package/qwik/index.js +26 -21
- package/react/index.d.ts +1 -0
- package/react/index.d.ts.map +1 -1
- package/react/index.js +4 -1
- package/schemas/report-claims.v1.schema.json +137 -0
- package/solid/index.d.ts +2 -0
- package/solid/index.d.ts.map +1 -1
- package/solid/index.js +3 -0
- package/svelte/index.d.ts +114 -0
- package/svelte/index.d.ts.map +1 -0
- package/svelte/index.js +193 -0
- package/tailwind.css +87 -0
- package/tokens/figma.variables.json +2241 -0
- package/tokens/index.js +1 -1
- package/tokens/index.json +2 -2
- package/tokens/resolved.json +3 -3
- package/tokens/tokens.dtcg.json +1 -1
- package/vue/index.d.ts +116 -0
- package/vue/index.d.ts.map +1 -0
- package/vue/index.js +228 -0
package/behaviors/modal.js
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
hasDom,
|
|
3
|
+
resolveHost,
|
|
4
|
+
noop,
|
|
5
|
+
bindOnce,
|
|
6
|
+
collectHosts,
|
|
7
|
+
focusInto,
|
|
8
|
+
closestSafe,
|
|
9
|
+
} from './internal.js';
|
|
10
|
+
|
|
11
|
+
function insideOpenPopover(target, modal) {
|
|
12
|
+
const classPanel = closestSafe(target, '.ui-popover.is-open');
|
|
13
|
+
if (classPanel && modal.contains(classPanel)) return true;
|
|
14
|
+
|
|
15
|
+
const nativePanel = closestSafe(target, '[popover]');
|
|
16
|
+
if (!nativePanel || !modal.contains(nativePanel)) return false;
|
|
17
|
+
try {
|
|
18
|
+
return nativePanel.matches(':popover-open');
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const activeModals = [];
|
|
25
|
+
|
|
26
|
+
const snapshotAttrs = (el, names) => {
|
|
27
|
+
const attrs = {};
|
|
28
|
+
for (const name of names) {
|
|
29
|
+
attrs[name] = {
|
|
30
|
+
had: el.hasAttribute(name),
|
|
31
|
+
value: el.getAttribute(name),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return attrs;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const restoreAttrs = (el, attrs) => {
|
|
38
|
+
for (const [name, state] of Object.entries(attrs)) {
|
|
39
|
+
if (state.had) el.setAttribute(name, state.value);
|
|
40
|
+
else el.removeAttribute(name);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const pushActiveModal = (modal) => {
|
|
45
|
+
const index = activeModals.indexOf(modal);
|
|
46
|
+
if (index !== -1) activeModals.splice(index, 1);
|
|
47
|
+
activeModals.push(modal);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const removeActiveModal = (modal) => {
|
|
51
|
+
const index = activeModals.indexOf(modal);
|
|
52
|
+
if (index !== -1) activeModals.splice(index, 1);
|
|
53
|
+
};
|
|
2
54
|
|
|
3
55
|
/**
|
|
4
56
|
* @typedef {object} ModalCloseDetail
|
|
@@ -45,22 +97,6 @@ export function initModal({ root } = {}) {
|
|
|
45
97
|
let opener = null;
|
|
46
98
|
let inerted = [];
|
|
47
99
|
|
|
48
|
-
// A controlled modal must announce AS a modal dialog, not a generic group —
|
|
49
|
-
// parity with initPopover. Apply a dialog role + aria-modal (unless the
|
|
50
|
-
// author set a role), and dev-warn on a missing accessible name since we
|
|
51
|
-
// can't invent a good one. (component audit C13.)
|
|
52
|
-
if (!modal.hasAttribute('role')) modal.setAttribute('role', 'dialog');
|
|
53
|
-
if (!modal.hasAttribute('aria-modal')) modal.setAttribute('aria-modal', 'true');
|
|
54
|
-
const named =
|
|
55
|
-
modal.hasAttribute('aria-label') ||
|
|
56
|
-
modal.hasAttribute('aria-labelledby') ||
|
|
57
|
-
modal.hasAttribute('title');
|
|
58
|
-
if (!named && typeof console !== 'undefined') {
|
|
59
|
-
console.warn(
|
|
60
|
-
`[bronto] initModal(): a [data-bronto-modal] has no accessible name — add aria-label or aria-labelledby so it is announced as a named dialog.`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
100
|
// Inert every sibling at each ancestor level up to <body>: the rest of the
|
|
65
101
|
// page becomes non-focusable/non-interactive while the modal subtree stays
|
|
66
102
|
// live. Skip already-inert nodes so release() can't un-inert something the
|
|
@@ -68,6 +104,7 @@ export function initModal({ root } = {}) {
|
|
|
68
104
|
const trap = () => {
|
|
69
105
|
if (opener) return; // already trapped
|
|
70
106
|
opener = document.activeElement;
|
|
107
|
+
pushActiveModal(modal);
|
|
71
108
|
let el = modal;
|
|
72
109
|
while (el && el.parentElement && el !== document.body) {
|
|
73
110
|
for (const sib of el.parentElement.children) {
|
|
@@ -83,6 +120,7 @@ export function initModal({ root } = {}) {
|
|
|
83
120
|
|
|
84
121
|
const release = () => {
|
|
85
122
|
if (!opener) return;
|
|
123
|
+
removeActiveModal(modal);
|
|
86
124
|
for (const el of inerted) el.inert = false;
|
|
87
125
|
inerted = [];
|
|
88
126
|
const back = opener;
|
|
@@ -94,6 +132,8 @@ export function initModal({ root } = {}) {
|
|
|
94
132
|
|
|
95
133
|
const onKey = (e) => {
|
|
96
134
|
if (e.key === 'Escape' && opener) {
|
|
135
|
+
if (activeModals.at(-1) !== modal) return;
|
|
136
|
+
if (insideOpenPopover(e.target, modal)) return;
|
|
97
137
|
modal.dispatchEvent(
|
|
98
138
|
new CustomEvent('bronto:modal:close', {
|
|
99
139
|
detail: { reason: 'escape' },
|
|
@@ -104,10 +144,27 @@ export function initModal({ root } = {}) {
|
|
|
104
144
|
}
|
|
105
145
|
};
|
|
106
146
|
|
|
107
|
-
const observer = typeof MutationObserver === 'function' ? new MutationObserver(sync) : null;
|
|
108
|
-
|
|
109
147
|
cleanups.push(
|
|
110
148
|
bindOnce(modal, 'modal', () => {
|
|
149
|
+
const attrs = snapshotAttrs(modal, ['role', 'aria-modal', 'tabindex']);
|
|
150
|
+
|
|
151
|
+
// A controlled modal must announce AS a modal dialog, not a generic group —
|
|
152
|
+
// parity with initPopover. Apply a dialog role + aria-modal (unless the
|
|
153
|
+
// author set a role), and dev-warn on a missing accessible name since we
|
|
154
|
+
// can't invent a good one. (component audit C13.)
|
|
155
|
+
if (!modal.hasAttribute('role')) modal.setAttribute('role', 'dialog');
|
|
156
|
+
if (!modal.hasAttribute('aria-modal')) modal.setAttribute('aria-modal', 'true');
|
|
157
|
+
const named =
|
|
158
|
+
modal.hasAttribute('aria-label') ||
|
|
159
|
+
modal.hasAttribute('aria-labelledby') ||
|
|
160
|
+
modal.hasAttribute('title');
|
|
161
|
+
if (!named && typeof console !== 'undefined') {
|
|
162
|
+
console.warn(
|
|
163
|
+
`[bronto] initModal(): a [data-bronto-modal] has no accessible name — add aria-label or aria-labelledby so it is announced as a named dialog.`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const observer = typeof MutationObserver === 'function' ? new MutationObserver(sync) : null;
|
|
111
168
|
observer?.observe(modal, { attributes: true, attributeFilter: ['class'] });
|
|
112
169
|
document.addEventListener('keydown', onKey, true);
|
|
113
170
|
if (modal.classList.contains('is-open')) trap(); // already open at init
|
|
@@ -115,6 +172,7 @@ export function initModal({ root } = {}) {
|
|
|
115
172
|
observer?.disconnect();
|
|
116
173
|
document.removeEventListener('keydown', onKey, true);
|
|
117
174
|
release();
|
|
175
|
+
restoreAttrs(modal, attrs);
|
|
118
176
|
};
|
|
119
177
|
}),
|
|
120
178
|
);
|
package/behaviors/popover.d.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Collision-aware popover, dependency-free. A `[data-bronto-popover]`
|
|
3
3
|
* trigger toggles the `.ui-popover` panel whose id it names. The panel
|
|
4
4
|
* is placed under the trigger and **flips above** when it would
|
|
5
|
-
* overflow the viewport, with its inline edge clamped on-screen
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* overflow the viewport, with its inline edge clamped on-screen and tall
|
|
6
|
+
* panels constrained to scroll inside the viewport — the thing the CSS-only
|
|
7
|
+
* tooltip can't do near edges / inside scroll containers. If the panel has
|
|
8
|
+
* the native `popover` attribute and the
|
|
8
9
|
* Popover API is available it is shown in the top layer (never
|
|
9
10
|
* clipped); otherwise an `.is-open` class is toggled. Manages
|
|
10
11
|
* `aria-expanded` / `aria-controls`, closes on Escape and outside
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["popover.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["popover.js"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAkM3C"}
|
package/behaviors/popover.js
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
hasDom,
|
|
3
|
+
resolveHost,
|
|
4
|
+
noop,
|
|
5
|
+
bindOnce,
|
|
6
|
+
byIdInHost,
|
|
7
|
+
focusInto,
|
|
8
|
+
collectHosts,
|
|
9
|
+
closestSafe,
|
|
10
|
+
} from './internal.js';
|
|
11
|
+
|
|
12
|
+
const snapshotAttrs = (el, names) => {
|
|
13
|
+
const attrs = {};
|
|
14
|
+
for (const name of names) {
|
|
15
|
+
attrs[name] = {
|
|
16
|
+
had: el.hasAttribute(name),
|
|
17
|
+
value: el.getAttribute(name),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return attrs;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const restoreAttrs = (el, attrs) => {
|
|
24
|
+
for (const [name, state] of Object.entries(attrs)) {
|
|
25
|
+
if (state.had) el.setAttribute(name, state.value);
|
|
26
|
+
else el.removeAttribute(name);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const snapshotStyle = (el, names) => {
|
|
31
|
+
const styles = {};
|
|
32
|
+
for (const name of names) styles[name] = el.style[name];
|
|
33
|
+
return styles;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const restoreStyle = (el, styles) => {
|
|
37
|
+
for (const [name, value] of Object.entries(styles)) el.style[name] = value;
|
|
38
|
+
};
|
|
2
39
|
|
|
3
40
|
/**
|
|
4
41
|
* Collision-aware popover, dependency-free. A `[data-bronto-popover]`
|
|
5
42
|
* trigger toggles the `.ui-popover` panel whose id it names. The panel
|
|
6
43
|
* is placed under the trigger and **flips above** when it would
|
|
7
|
-
* overflow the viewport, with its inline edge clamped on-screen
|
|
8
|
-
*
|
|
9
|
-
*
|
|
44
|
+
* overflow the viewport, with its inline edge clamped on-screen and tall
|
|
45
|
+
* panels constrained to scroll inside the viewport — the thing the CSS-only
|
|
46
|
+
* tooltip can't do near edges / inside scroll containers. If the panel has
|
|
47
|
+
* the native `popover` attribute and the
|
|
10
48
|
* Popover API is available it is shown in the top layer (never
|
|
11
49
|
* clipped); otherwise an `.is-open` class is toggled. Manages
|
|
12
50
|
* `aria-expanded` / `aria-controls`, closes on Escape and outside
|
|
@@ -34,6 +72,27 @@ export function initPopover({ root } = {}) {
|
|
|
34
72
|
const GAP = 8;
|
|
35
73
|
let openPanel = null;
|
|
36
74
|
let openTrigger = null;
|
|
75
|
+
const triggerStates = new Map();
|
|
76
|
+
const panelStates = new Map();
|
|
77
|
+
|
|
78
|
+
const rememberTrigger = (trigger) => {
|
|
79
|
+
if (!triggerStates.has(trigger)) {
|
|
80
|
+
triggerStates.set(
|
|
81
|
+
trigger,
|
|
82
|
+
snapshotAttrs(trigger, ['aria-haspopup', 'aria-controls', 'aria-expanded']),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const rememberPanel = (panel) => {
|
|
88
|
+
if (!panelStates.has(panel)) {
|
|
89
|
+
panelStates.set(panel, {
|
|
90
|
+
attrs: snapshotAttrs(panel, ['role', 'tabindex']),
|
|
91
|
+
open: panel.classList.contains('is-open'),
|
|
92
|
+
style: snapshotStyle(panel, ['maxBlockSize', 'top', 'left']),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
37
96
|
|
|
38
97
|
// The trigger advertises `aria-haspopup="dialog"`, so the open panel must BE a
|
|
39
98
|
// dialog: a role, an accessible name, and focus moved into it (C6) — see the
|
|
@@ -41,12 +100,20 @@ export function initPopover({ root } = {}) {
|
|
|
41
100
|
|
|
42
101
|
const place = (trigger, panel) => {
|
|
43
102
|
const r = trigger.getBoundingClientRect();
|
|
103
|
+
panel.style.maxBlockSize = 'none';
|
|
44
104
|
const pw = panel.offsetWidth;
|
|
45
|
-
const ph = panel.offsetHeight;
|
|
105
|
+
const ph = Math.max(panel.offsetHeight, panel.scrollHeight);
|
|
46
106
|
const vw = view?.innerWidth ?? 0;
|
|
47
107
|
const vh = view?.innerHeight ?? 0;
|
|
48
|
-
|
|
49
|
-
|
|
108
|
+
const maxHeight = Math.max(0, vh - GAP * 2);
|
|
109
|
+
const below = Math.max(0, vh - r.bottom - GAP * 2);
|
|
110
|
+
const above = Math.max(0, r.top - GAP * 2);
|
|
111
|
+
const placeAbove = ph > below && above > below;
|
|
112
|
+
const available = placeAbove ? above : below;
|
|
113
|
+
const height = Math.min(ph, available || maxHeight);
|
|
114
|
+
panel.style.maxBlockSize = `${height}px`;
|
|
115
|
+
let top = placeAbove ? r.top - GAP - height : r.bottom + GAP;
|
|
116
|
+
if (vh) top = Math.max(GAP, Math.min(top, vh - height - GAP));
|
|
50
117
|
let left = r.left;
|
|
51
118
|
if (vw) left = Math.max(GAP, Math.min(left, vw - pw - GAP));
|
|
52
119
|
panel.style.top = `${Math.max(GAP, top)}px`;
|
|
@@ -77,6 +144,8 @@ export function initPopover({ root } = {}) {
|
|
|
77
144
|
|
|
78
145
|
const open = (trigger, panel) => {
|
|
79
146
|
close();
|
|
147
|
+
rememberTrigger(trigger);
|
|
148
|
+
rememberPanel(panel);
|
|
80
149
|
// Live up to the advertised `aria-haspopup="dialog"`: give the panel a
|
|
81
150
|
// dialog role (unless the author set one) so AT announces it as the promised
|
|
82
151
|
// dialog rather than a generic group (C6).
|
|
@@ -99,7 +168,7 @@ export function initPopover({ root } = {}) {
|
|
|
99
168
|
};
|
|
100
169
|
|
|
101
170
|
const onClick = (e) => {
|
|
102
|
-
const trigger = e.target
|
|
171
|
+
const trigger = closestSafe(e.target, '[data-bronto-popover]');
|
|
103
172
|
if (trigger && host.contains(trigger)) {
|
|
104
173
|
const panel = byIdInHost(host, trigger.getAttribute('data-bronto-popover'));
|
|
105
174
|
if (!panel) return;
|
|
@@ -133,9 +202,11 @@ export function initPopover({ root } = {}) {
|
|
|
133
202
|
// never routes through close(), so aria-expanded would otherwise go stale).
|
|
134
203
|
const seedTeardowns = [];
|
|
135
204
|
const seed = () => {
|
|
136
|
-
for (const trigger of host
|
|
205
|
+
for (const trigger of collectHosts(host, '[data-bronto-popover]')) {
|
|
137
206
|
const panel = byIdInHost(host, trigger.getAttribute('data-bronto-popover'));
|
|
138
207
|
if (!panel) continue;
|
|
208
|
+
rememberTrigger(trigger);
|
|
209
|
+
rememberPanel(panel);
|
|
139
210
|
if (!trigger.hasAttribute('aria-haspopup')) trigger.setAttribute('aria-haspopup', 'dialog');
|
|
140
211
|
trigger.setAttribute('aria-controls', panel.id);
|
|
141
212
|
if (!trigger.hasAttribute('aria-expanded')) trigger.setAttribute('aria-expanded', 'false');
|
|
@@ -169,7 +240,16 @@ export function initPopover({ root } = {}) {
|
|
|
169
240
|
view?.addEventListener('scroll', onReflow, true);
|
|
170
241
|
view?.addEventListener('resize', onReflow);
|
|
171
242
|
return () => {
|
|
243
|
+
close();
|
|
172
244
|
for (const t of seedTeardowns.splice(0)) t();
|
|
245
|
+
for (const [trigger, attrs] of triggerStates) restoreAttrs(trigger, attrs);
|
|
246
|
+
triggerStates.clear();
|
|
247
|
+
for (const [panel, state] of panelStates) {
|
|
248
|
+
restoreAttrs(panel, state.attrs);
|
|
249
|
+
panel.classList.toggle('is-open', state.open);
|
|
250
|
+
restoreStyle(panel, state.style);
|
|
251
|
+
}
|
|
252
|
+
panelStates.clear();
|
|
173
253
|
document.removeEventListener('click', onClick);
|
|
174
254
|
document.removeEventListener('keydown', onKey);
|
|
175
255
|
view?.removeEventListener('scroll', onReflow, true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["sources.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["sources.js"],"names":[],"mappings":"AA4CA;;;;;;;;;;;GAWG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAmH3C;;;;;QA5Ja,MAAM;;;;cACN,OAAO;;;;YACP,OAAO"}
|
package/behaviors/sources.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
byIdInHost,
|
|
7
7
|
collectHosts,
|
|
8
8
|
scrollIntoViewSafe,
|
|
9
|
+
closestSafe,
|
|
9
10
|
} from './internal.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -64,6 +65,7 @@ export function initSources({ root } = {}) {
|
|
|
64
65
|
for (const island of islands) {
|
|
65
66
|
const timers = new Set();
|
|
66
67
|
const seeded = [];
|
|
68
|
+
const activeSources = new Set();
|
|
67
69
|
|
|
68
70
|
const targetFor = (ref) => {
|
|
69
71
|
const id = sourceId(ref);
|
|
@@ -101,17 +103,25 @@ export function initSources({ root } = {}) {
|
|
|
101
103
|
}
|
|
102
104
|
};
|
|
103
105
|
|
|
106
|
+
const clearGeneratedActive = () => {
|
|
107
|
+
for (const source of activeSources) source.classList.remove(ACTIVE);
|
|
108
|
+
activeSources.clear();
|
|
109
|
+
};
|
|
110
|
+
|
|
104
111
|
const focusSource = (ref, source) => {
|
|
105
112
|
for (const card of island.querySelectorAll(`.${ACTIVE}`)) card.classList.remove(ACTIVE);
|
|
113
|
+
clearGeneratedActive();
|
|
106
114
|
for (const timer of timers) clearTimeout(timer);
|
|
107
115
|
timers.clear();
|
|
108
116
|
|
|
109
117
|
source.classList.add(ACTIVE);
|
|
118
|
+
activeSources.add(source);
|
|
110
119
|
source.focus?.({ preventScroll: true });
|
|
111
120
|
scrollIntoViewSafe(source);
|
|
112
121
|
|
|
113
122
|
const timer = setTimeout(() => {
|
|
114
123
|
source.classList.remove(ACTIVE);
|
|
124
|
+
activeSources.delete(source);
|
|
115
125
|
timers.delete(timer);
|
|
116
126
|
}, 1600);
|
|
117
127
|
timers.add(timer);
|
|
@@ -125,7 +135,7 @@ export function initSources({ root } = {}) {
|
|
|
125
135
|
};
|
|
126
136
|
|
|
127
137
|
const onClick = (e) => {
|
|
128
|
-
const ref = e.target
|
|
138
|
+
const ref = closestSafe(e.target, REF_SELECTOR);
|
|
129
139
|
if (!ref || !island.contains(ref)) return;
|
|
130
140
|
const source = targetFor(ref);
|
|
131
141
|
if (!source) return;
|
|
@@ -134,12 +144,14 @@ export function initSources({ root } = {}) {
|
|
|
134
144
|
};
|
|
135
145
|
|
|
136
146
|
const cleanup = bindOnce(island, 'sources', () => {
|
|
147
|
+
const activeState = Array.from(island.querySelectorAll(`.${ACTIVE}`));
|
|
137
148
|
seed();
|
|
138
149
|
island.addEventListener('click', onClick);
|
|
139
150
|
return () => {
|
|
140
151
|
island.removeEventListener('click', onClick);
|
|
141
152
|
for (const timer of timers) clearTimeout(timer);
|
|
142
153
|
timers.clear();
|
|
154
|
+
clearGeneratedActive();
|
|
143
155
|
for (const item of seeded.splice(0)) {
|
|
144
156
|
if (item.hadDescribedBy) item.ref.setAttribute('aria-describedby', item.describedBy);
|
|
145
157
|
else item.ref.removeAttribute('aria-describedby');
|
|
@@ -147,7 +159,7 @@ export function initSources({ root } = {}) {
|
|
|
147
159
|
else item.ref.removeAttribute('title');
|
|
148
160
|
if (item.source && item.hadTabindex === false) item.source.removeAttribute('tabindex');
|
|
149
161
|
}
|
|
150
|
-
for (const
|
|
162
|
+
for (const source of activeState) source.classList.add(ACTIVE);
|
|
151
163
|
};
|
|
152
164
|
});
|
|
153
165
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire focusable ARIA splitters. Each `[data-bronto-splitter]` host contains
|
|
3
|
+
* two `.ui-splitter__pane` elements separated by one `.ui-splitter__handle`
|
|
4
|
+
* (`role="separator"`). The behavior keeps `--splitter-pos` and
|
|
5
|
+
* `aria-valuenow` in sync for keyboard and pointer resizing, then dispatches
|
|
6
|
+
* `bronto:splitter:resize` with `{ value, orientation }`.
|
|
7
|
+
*
|
|
8
|
+
* Bronto owns the control affordance only. The host owns pane content,
|
|
9
|
+
* persistence, min/max policy, collapse behavior, and any saved layout state.
|
|
10
|
+
* SSR-safe and idempotent per splitter; returns a cleanup function.
|
|
11
|
+
*
|
|
12
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
13
|
+
* @returns {import('./internal.js').Cleanup}
|
|
14
|
+
*/
|
|
15
|
+
export function initSplitter({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
16
|
+
export type SplitterResizeDetail = {
|
|
17
|
+
/**
|
|
18
|
+
* The first pane size as a 0..100 percentage.
|
|
19
|
+
*/
|
|
20
|
+
value: number;
|
|
21
|
+
/**
|
|
22
|
+
* Splitter orientation.
|
|
23
|
+
*/
|
|
24
|
+
orientation: "vertical" | "horizontal";
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=splitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["splitter.js"],"names":[],"mappings":"AAwNA;;;;;;;;;;;;;GAaG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAU3C;;;;;WAlOa,MAAM;;;;iBACN,UAAU,GAAG,YAAY"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js';
|
|
2
|
+
|
|
3
|
+
const SELECTOR = '[data-bronto-splitter]';
|
|
4
|
+
const HANDLE_SELECTOR = '.ui-splitter__handle';
|
|
5
|
+
const DEFAULT_MIN = 20;
|
|
6
|
+
const DEFAULT_MAX = 80;
|
|
7
|
+
const DEFAULT_VALUE = 50;
|
|
8
|
+
const STEP = 2;
|
|
9
|
+
const LARGE_STEP = 10;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} SplitterResizeDetail
|
|
13
|
+
* @property {number} value The first pane size as a 0..100 percentage.
|
|
14
|
+
* @property {'vertical' | 'horizontal'} orientation Splitter orientation.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const num = (v, fallback) => {
|
|
18
|
+
const n = Number.parseFloat(String(v ?? '').trim());
|
|
19
|
+
return Number.isFinite(n) ? n : fallback;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const fmt = (v) => String(Math.round(v * 10) / 10);
|
|
23
|
+
|
|
24
|
+
const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
|
|
25
|
+
|
|
26
|
+
const readCssValue = (splitter) => splitter.style.getPropertyValue('--splitter-pos');
|
|
27
|
+
|
|
28
|
+
const readOrientation = (splitter, handle) => {
|
|
29
|
+
const data = splitter.getAttribute('data-bronto-splitter');
|
|
30
|
+
if (data === 'horizontal' || data === 'vertical') return data;
|
|
31
|
+
if (splitter.classList.contains('ui-splitter--horizontal')) return 'horizontal';
|
|
32
|
+
if (splitter.classList.contains('ui-splitter--vertical')) return 'vertical';
|
|
33
|
+
return handle.getAttribute('aria-orientation') === 'horizontal' ? 'horizontal' : 'vertical';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getView = (el) => el.ownerDocument?.defaultView || null;
|
|
37
|
+
|
|
38
|
+
const dispatchResize = (splitter, detail) => {
|
|
39
|
+
splitter.dispatchEvent(
|
|
40
|
+
new CustomEvent('bronto:splitter:resize', {
|
|
41
|
+
bubbles: true,
|
|
42
|
+
detail,
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const snapshotAttrs = (el, names) => {
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const name of names) {
|
|
50
|
+
out[name] = {
|
|
51
|
+
had: el.hasAttribute(name),
|
|
52
|
+
value: el.getAttribute(name),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const restoreAttrs = (el, attrs) => {
|
|
59
|
+
for (const [name, attr] of Object.entries(attrs)) {
|
|
60
|
+
if (attr.had) el.setAttribute(name, attr.value);
|
|
61
|
+
else el.removeAttribute(name);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const snapshotStyleProp = (el, name) => ({
|
|
66
|
+
value: el.style.getPropertyValue(name),
|
|
67
|
+
priority: el.style.getPropertyPriority(name),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const restoreStyleProp = (el, name, prop) => {
|
|
71
|
+
if (prop.value) el.style.setProperty(name, prop.value, prop.priority);
|
|
72
|
+
else el.style.removeProperty(name);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function wireSplitter(splitter) {
|
|
76
|
+
const handle = splitter.querySelector(HANDLE_SELECTOR);
|
|
77
|
+
if (!handle) return noop;
|
|
78
|
+
|
|
79
|
+
return bindOnce(splitter, 'splitter', () => {
|
|
80
|
+
const handleAttrs = snapshotAttrs(handle, [
|
|
81
|
+
'role',
|
|
82
|
+
'tabindex',
|
|
83
|
+
'aria-orientation',
|
|
84
|
+
'aria-valuemin',
|
|
85
|
+
'aria-valuemax',
|
|
86
|
+
'aria-valuenow',
|
|
87
|
+
]);
|
|
88
|
+
const splitterPos = snapshotStyleProp(splitter, '--splitter-pos');
|
|
89
|
+
const orientation = readOrientation(splitter, handle);
|
|
90
|
+
const min = num(handle.getAttribute('aria-valuemin'), DEFAULT_MIN);
|
|
91
|
+
const max = Math.max(min, num(handle.getAttribute('aria-valuemax'), DEFAULT_MAX));
|
|
92
|
+
let value = clamp(
|
|
93
|
+
num(handle.getAttribute('aria-valuenow'), num(readCssValue(splitter), DEFAULT_VALUE)),
|
|
94
|
+
min,
|
|
95
|
+
max,
|
|
96
|
+
);
|
|
97
|
+
let activePointer = null;
|
|
98
|
+
|
|
99
|
+
const apply = (next, { emit = true } = {}) => {
|
|
100
|
+
value = clamp(next, min, max);
|
|
101
|
+
const label = fmt(value);
|
|
102
|
+
splitter.style.setProperty('--splitter-pos', `${label}%`);
|
|
103
|
+
handle.setAttribute('aria-valuenow', label);
|
|
104
|
+
if (emit) dispatchResize(splitter, { value, orientation });
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (!handle.hasAttribute('role')) handle.setAttribute('role', 'separator');
|
|
108
|
+
if (!handle.hasAttribute('tabindex')) handle.tabIndex = 0;
|
|
109
|
+
if (!handle.hasAttribute('aria-orientation'))
|
|
110
|
+
handle.setAttribute('aria-orientation', orientation);
|
|
111
|
+
if (!handle.hasAttribute('aria-valuemin')) handle.setAttribute('aria-valuemin', fmt(min));
|
|
112
|
+
if (!handle.hasAttribute('aria-valuemax')) handle.setAttribute('aria-valuemax', fmt(max));
|
|
113
|
+
apply(value, { emit: false });
|
|
114
|
+
|
|
115
|
+
const fromPointer = (event) => {
|
|
116
|
+
const rect = splitter.getBoundingClientRect();
|
|
117
|
+
const size = orientation === 'horizontal' ? rect.height : rect.width;
|
|
118
|
+
if (!size) return value;
|
|
119
|
+
if (orientation === 'horizontal') {
|
|
120
|
+
return ((event.clientY - rect.top) / size) * 100;
|
|
121
|
+
}
|
|
122
|
+
const view = getView(splitter);
|
|
123
|
+
const dir = view?.getComputedStyle?.(splitter).direction;
|
|
124
|
+
const x = dir === 'rtl' ? rect.right - event.clientX : event.clientX - rect.left;
|
|
125
|
+
return (x / size) * 100;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const capturePointer = (pointerId) => {
|
|
129
|
+
if (pointerId === undefined || pointerId === null) return;
|
|
130
|
+
try {
|
|
131
|
+
handle.setPointerCapture?.(pointerId);
|
|
132
|
+
} catch {
|
|
133
|
+
/* Pointer capture is an affordance; drag still works through document listeners. */
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const releasePointer = (pointerId = activePointer) => {
|
|
138
|
+
if (pointerId === undefined || pointerId === null) return;
|
|
139
|
+
try {
|
|
140
|
+
if (!handle.hasPointerCapture || handle.hasPointerCapture(pointerId)) {
|
|
141
|
+
handle.releasePointerCapture?.(pointerId);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
/* The element may have been removed or capture may already be gone. */
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const onKeydown = (event) => {
|
|
149
|
+
let next = value;
|
|
150
|
+
if (event.key === 'Home') next = min;
|
|
151
|
+
else if (event.key === 'End') next = max;
|
|
152
|
+
else if (event.key === 'PageUp') next += LARGE_STEP;
|
|
153
|
+
else if (event.key === 'PageDown') next -= LARGE_STEP;
|
|
154
|
+
else if (event.key === 'ArrowRight' || event.key === 'ArrowDown')
|
|
155
|
+
next += event.shiftKey ? LARGE_STEP : STEP;
|
|
156
|
+
else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp')
|
|
157
|
+
next -= event.shiftKey ? LARGE_STEP : STEP;
|
|
158
|
+
else return;
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
apply(next);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const onPointerMove = (event) => {
|
|
164
|
+
if (
|
|
165
|
+
activePointer !== null &&
|
|
166
|
+
event.pointerId !== undefined &&
|
|
167
|
+
event.pointerId !== activePointer
|
|
168
|
+
)
|
|
169
|
+
return;
|
|
170
|
+
apply(fromPointer(event));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const onPointerUp = (event) => {
|
|
174
|
+
if (
|
|
175
|
+
activePointer !== null &&
|
|
176
|
+
event.pointerId !== undefined &&
|
|
177
|
+
event.pointerId !== activePointer
|
|
178
|
+
)
|
|
179
|
+
return;
|
|
180
|
+
releasePointer(event.pointerId);
|
|
181
|
+
activePointer = null;
|
|
182
|
+
handle.classList.remove('is-active');
|
|
183
|
+
splitter.ownerDocument.removeEventListener('pointermove', onPointerMove);
|
|
184
|
+
splitter.ownerDocument.removeEventListener('pointerup', onPointerUp);
|
|
185
|
+
splitter.ownerDocument.removeEventListener('pointercancel', onPointerUp);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const onPointerDown = (event) => {
|
|
189
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
activePointer = event.pointerId ?? null;
|
|
192
|
+
capturePointer(activePointer);
|
|
193
|
+
handle.classList.add('is-active');
|
|
194
|
+
apply(fromPointer(event));
|
|
195
|
+
splitter.ownerDocument.addEventListener('pointermove', onPointerMove);
|
|
196
|
+
splitter.ownerDocument.addEventListener('pointerup', onPointerUp);
|
|
197
|
+
splitter.ownerDocument.addEventListener('pointercancel', onPointerUp);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
handle.addEventListener('keydown', onKeydown);
|
|
201
|
+
handle.addEventListener('pointerdown', onPointerDown);
|
|
202
|
+
return () => {
|
|
203
|
+
handle.removeEventListener('keydown', onKeydown);
|
|
204
|
+
handle.removeEventListener('pointerdown', onPointerDown);
|
|
205
|
+
splitter.ownerDocument.removeEventListener('pointermove', onPointerMove);
|
|
206
|
+
splitter.ownerDocument.removeEventListener('pointerup', onPointerUp);
|
|
207
|
+
splitter.ownerDocument.removeEventListener('pointercancel', onPointerUp);
|
|
208
|
+
releasePointer();
|
|
209
|
+
handle.classList.remove('is-active');
|
|
210
|
+
activePointer = null;
|
|
211
|
+
restoreAttrs(handle, handleAttrs);
|
|
212
|
+
restoreStyleProp(splitter, '--splitter-pos', splitterPos);
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Wire focusable ARIA splitters. Each `[data-bronto-splitter]` host contains
|
|
219
|
+
* two `.ui-splitter__pane` elements separated by one `.ui-splitter__handle`
|
|
220
|
+
* (`role="separator"`). The behavior keeps `--splitter-pos` and
|
|
221
|
+
* `aria-valuenow` in sync for keyboard and pointer resizing, then dispatches
|
|
222
|
+
* `bronto:splitter:resize` with `{ value, orientation }`.
|
|
223
|
+
*
|
|
224
|
+
* Bronto owns the control affordance only. The host owns pane content,
|
|
225
|
+
* persistence, min/max policy, collapse behavior, and any saved layout state.
|
|
226
|
+
* SSR-safe and idempotent per splitter; returns a cleanup function.
|
|
227
|
+
*
|
|
228
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
229
|
+
* @returns {import('./internal.js').Cleanup}
|
|
230
|
+
*/
|
|
231
|
+
export function initSplitter({ root } = {}) {
|
|
232
|
+
if (!hasDom()) return noop;
|
|
233
|
+
const host = resolveHost(root);
|
|
234
|
+
if (!host) return noop;
|
|
235
|
+
const splitters = collectHosts(host, SELECTOR);
|
|
236
|
+
if (!splitters.length) return noop;
|
|
237
|
+
const cleanups = splitters.map(wireSplitter).filter(Boolean);
|
|
238
|
+
return () => cleanups.forEach((fn) => fn());
|
|
239
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spotlight.d.ts","sourceRoot":"","sources":["spotlight.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"spotlight.d.ts","sourceRoot":"","sources":["spotlight.js"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;GAcG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6C3C"}
|