@relements/core 0.1.0
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/dist/base.css +1 -0
- package/dist/behaviors/dialog.d.ts +39 -0
- package/dist/behaviors/dialog.js +1 -0
- package/dist/behaviors/dismissible.d.ts +37 -0
- package/dist/behaviors/dismissible.js +1 -0
- package/dist/behaviors/menu-button.d.ts +36 -0
- package/dist/behaviors/menu-button.js +1 -0
- package/dist/behaviors/popover.d.ts +28 -0
- package/dist/behaviors/popover.js +1 -0
- package/dist/behaviors/tabs.d.ts +37 -0
- package/dist/behaviors/tabs.js +1 -0
- package/dist/behaviors/toast.d.ts +42 -0
- package/dist/behaviors/toast.js +1 -0
- package/dist/chunk-GMICGIQW.js +149 -0
- package/dist/chunk-J4EGUBPP.js +68 -0
- package/dist/chunk-PIDPGDBZ.js +62 -0
- package/dist/chunk-PSODVT3V.js +67 -0
- package/dist/chunk-TC4TFP7Y.js +40 -0
- package/dist/chunk-ZHRJNWMH.js +174 -0
- package/dist/components/button.css +1 -0
- package/dist/components/dialog.css +1 -0
- package/dist/components/disclosure.css +1 -0
- package/dist/components/form.css +1 -0
- package/dist/components/link.css +1 -0
- package/dist/components/menu.css +1 -0
- package/dist/components/popover.css +1 -0
- package/dist/components/progress.css +1 -0
- package/dist/components/tabs.css +1 -0
- package/dist/components/toast.css +1 -0
- package/dist/elements/re-menu.d.ts +10 -0
- package/dist/elements/re-menu.js +36 -0
- package/dist/elements/re-popover.d.ts +12 -0
- package/dist/elements/re-popover.js +35 -0
- package/dist/elements/re-tabs.d.ts +20 -0
- package/dist/elements/re-tabs.js +60 -0
- package/dist/elements/re-toast.d.ts +15 -0
- package/dist/elements/re-toast.js +30 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/reset.css +1 -0
- package/dist/themes/renascent.css +1 -0
- package/dist/tokens.css +1 -0
- package/package.json +84 -0
- package/src/base.css +129 -0
- package/src/behaviors/dialog.js +106 -0
- package/src/behaviors/dismissible.js +68 -0
- package/src/behaviors/menu-button.js +199 -0
- package/src/behaviors/popover.js +103 -0
- package/src/behaviors/tabs.js +171 -0
- package/src/behaviors/toast.js +97 -0
- package/src/components/button.css +141 -0
- package/src/components/dialog.css +106 -0
- package/src/components/disclosure.css +83 -0
- package/src/components/form.css +334 -0
- package/src/components/link.css +61 -0
- package/src/components/menu.css +78 -0
- package/src/components/popover.css +50 -0
- package/src/components/progress.css +112 -0
- package/src/components/tabs.css +86 -0
- package/src/components/toast.css +87 -0
- package/src/elements/re-menu.js +54 -0
- package/src/elements/re-popover.js +59 -0
- package/src/elements/re-tabs.js +92 -0
- package/src/elements/re-toast.js +46 -0
- package/src/index.css +30 -0
- package/src/index.js +13 -0
- package/src/reset.css +103 -0
- package/src/themes/renascent.css +198 -0
- package/src/tokens.css +196 -0
package/dist/base.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer re.base{:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}html{font-family:var(--re-font-sans);font-size:var(--re-size-text-md);line-height:var(--re-line-height-normal);color:var(--re-color-text);background-color:var(--re-color-bg)}::selection{background-color:var(--re-color-selection-bg);color:var(--re-color-selection-text)}h1,h2,h3,h4,h5,h6{line-height:var(--re-line-height-tight);font-weight:var(--re-font-weight-semibold);color:var(--re-color-text)}h1{font-size:var(--re-size-text-4xl)}h2{font-size:var(--re-size-text-3xl)}h3{font-size:var(--re-size-text-2xl)}h4{font-size:var(--re-size-text-xl)}h5{font-size:var(--re-size-text-lg)}h6{font-size:var(--re-size-text-md)}p{line-height:var(--re-line-height-normal);color:var(--re-color-text)}small{font-size:var(--re-size-text-sm);color:var(--re-color-text-muted)}code,kbd,samp,pre{font-family:var(--re-font-mono);font-size:.95em}pre{padding:var(--re-space-3) var(--re-space-4);background-color:var(--re-color-bg-muted);border-radius:var(--re-radius-md);overflow:auto}code{background-color:var(--re-color-bg-muted);border-radius:var(--re-radius-sm);padding:.1em .3em}pre code{background:0 0;border-radius:0;padding:0}hr{border:0;border-top:var(--re-border-default);margin-block:var(--re-space-6)}a{color:var(--re-color-link);text-underline-offset:.2em;text-decoration-thickness:.08em}a:hover{color:var(--re-color-link-hover)}a:visited{color:var(--re-color-link-visited)}:focus-visible{box-shadow:var(--re-shadow-focus);border-radius:var(--re-radius-sm);outline:none}input,textarea,select{color:var(--re-color-text);background-color:var(--re-color-bg)}}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enhanceDialog
|
|
3
|
+
* -------------
|
|
4
|
+
* Lightweight ergonomic helpers for native <dialog>.
|
|
5
|
+
*
|
|
6
|
+
* Wires:
|
|
7
|
+
* - `[data-re-dialog-trigger]` → opens the <dialog> referenced by its
|
|
8
|
+
* `data-re-dialog-target="dialog-id"` attribute (or aria-controls).
|
|
9
|
+
* - `[data-re-dialog-close]` inside a <dialog> → closes the parent dialog,
|
|
10
|
+
* setting the dialog's returnValue to the element's value attribute.
|
|
11
|
+
* - Click on the dialog's backdrop closes the dialog when the dialog
|
|
12
|
+
* element itself has `data-re-dialog-close-on-backdrop`.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
*
|
|
16
|
+
* <button type="button" data-re-dialog-trigger data-re-dialog-target="confirm">
|
|
17
|
+
* Open
|
|
18
|
+
* </button>
|
|
19
|
+
* <dialog id="confirm" data-re-dialog-close-on-backdrop>
|
|
20
|
+
* …
|
|
21
|
+
* <button data-re-dialog-close value="cancel">Cancel</button>
|
|
22
|
+
* </dialog>
|
|
23
|
+
*
|
|
24
|
+
* import { enhanceDialog } from "@relements/core/behaviors/dialog";
|
|
25
|
+
* const c = enhanceDialog(document);
|
|
26
|
+
* c.destroy();
|
|
27
|
+
*
|
|
28
|
+
* Native semantics are preserved: showModal/close/Escape behavior all
|
|
29
|
+
* come from the browser.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* @param {Document | Element | ShadowRoot} [root=document]
|
|
33
|
+
* @returns {{ destroy: () => void }}
|
|
34
|
+
*/
|
|
35
|
+
declare function enhanceDialog(root?: Document | Element | ShadowRoot): {
|
|
36
|
+
destroy: () => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export { enhanceDialog };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { enhanceDialog } from '../chunk-J4EGUBPP.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enhanceDismissible
|
|
3
|
+
* ------------------
|
|
4
|
+
* Wires up dismiss buttons inside any element marked `[data-re-dismissible]`.
|
|
5
|
+
*
|
|
6
|
+
* Usage in HTML:
|
|
7
|
+
*
|
|
8
|
+
* <aside data-re-dismissible>
|
|
9
|
+
* Banner copy…
|
|
10
|
+
* <button type="button" data-re-dismiss aria-label="Dismiss">×</button>
|
|
11
|
+
* </aside>
|
|
12
|
+
*
|
|
13
|
+
* Usage in JavaScript:
|
|
14
|
+
*
|
|
15
|
+
* import { enhanceDismissible } from "@relements/core/behaviors/dismissible";
|
|
16
|
+
* const controller = enhanceDismissible(document);
|
|
17
|
+
* // …later
|
|
18
|
+
* controller.destroy();
|
|
19
|
+
*
|
|
20
|
+
* Behavior:
|
|
21
|
+
* - Click or Enter/Space on `[data-re-dismiss]` hides the closest
|
|
22
|
+
* `[data-re-dismissible]` ancestor (sets `hidden`).
|
|
23
|
+
* - Dispatches a `re-dismiss` `CustomEvent` (bubbles, cancelable) on the
|
|
24
|
+
* dismissible element before hiding; calling `preventDefault()` cancels.
|
|
25
|
+
* - `controller.destroy()` removes all listeners.
|
|
26
|
+
*
|
|
27
|
+
* Root can be a Document, Element, or ShadowRoot.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* @param {Document | Element | ShadowRoot} [root=document]
|
|
31
|
+
* @returns {{ destroy: () => void }}
|
|
32
|
+
*/
|
|
33
|
+
declare function enhanceDismissible(root?: Document | Element | ShadowRoot): {
|
|
34
|
+
destroy: () => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { enhanceDismissible };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { enhanceDismissible } from '../chunk-TC4TFP7Y.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enhanceMenuButton
|
|
3
|
+
* -----------------
|
|
4
|
+
* ARIA menu-button pattern over markup like:
|
|
5
|
+
*
|
|
6
|
+
* <div class="re-menu" data-re-menu>
|
|
7
|
+
* <button aria-haspopup="menu" aria-expanded="false" aria-controls="m-1" id="b-1">…</button>
|
|
8
|
+
* <div role="menu" id="m-1" aria-labelledby="b-1" hidden>
|
|
9
|
+
* <button role="menuitem">Rename</button>
|
|
10
|
+
* …
|
|
11
|
+
* </div>
|
|
12
|
+
* </div>
|
|
13
|
+
*
|
|
14
|
+
* Behavior:
|
|
15
|
+
* - Click the button toggles the menu.
|
|
16
|
+
* - ArrowDown opens the menu and focuses the first item.
|
|
17
|
+
* - Up/Down on items moves focus within the menu (wraps).
|
|
18
|
+
* - Home/End jump.
|
|
19
|
+
* - Escape closes the menu and returns focus to the button.
|
|
20
|
+
* - Tab outside the menu closes it.
|
|
21
|
+
* - Clicking outside closes it.
|
|
22
|
+
*
|
|
23
|
+
* Dispatches `re-select` (bubbles, cancelable) when a menuitem is activated:
|
|
24
|
+
* detail = { item: HTMLElement, value: string }
|
|
25
|
+
*/
|
|
26
|
+
/** @typedef {{ destroy: () => void }} Controller */
|
|
27
|
+
/**
|
|
28
|
+
* @param {Document | Element | ShadowRoot} [root=document]
|
|
29
|
+
* @returns {Controller}
|
|
30
|
+
*/
|
|
31
|
+
declare function enhanceMenuButton(root?: Document | Element | ShadowRoot): Controller;
|
|
32
|
+
type Controller = {
|
|
33
|
+
destroy: () => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export { type Controller, enhanceMenuButton };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { enhanceMenuButton } from '../chunk-ZHRJNWMH.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enhancePopover
|
|
3
|
+
* --------------
|
|
4
|
+
* Thin helper for native [popover]. Provides:
|
|
5
|
+
* - Anchored positioning: places the popover below its `popovertarget`
|
|
6
|
+
* button when the popover opens. (Native CSS Anchor Positioning isn't
|
|
7
|
+
* universally supported yet; this is a tiny JS fallback.)
|
|
8
|
+
* - `re-toggle` CustomEvent on the popover element when it opens/closes
|
|
9
|
+
* (detail = { open: boolean }), mirroring the native `toggle` event so
|
|
10
|
+
* consumers don't need feature-detect it.
|
|
11
|
+
*
|
|
12
|
+
* <button class="re-button" popovertarget="tip">Toggle</button>
|
|
13
|
+
* <div class="re-popover" id="tip" popover data-re-popover>Hello</div>
|
|
14
|
+
*
|
|
15
|
+
* import { enhancePopover } from "@relements/core/behaviors/popover";
|
|
16
|
+
* const c = enhancePopover(document);
|
|
17
|
+
*/
|
|
18
|
+
/** @typedef {{ destroy: () => void }} Controller */
|
|
19
|
+
/**
|
|
20
|
+
* @param {Document | Element | ShadowRoot} [root=document]
|
|
21
|
+
* @returns {Controller}
|
|
22
|
+
*/
|
|
23
|
+
declare function enhancePopover(root?: Document | Element | ShadowRoot): Controller;
|
|
24
|
+
type Controller = {
|
|
25
|
+
destroy: () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export { type Controller, enhancePopover };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { enhancePopover } from '../chunk-PSODVT3V.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enhanceTabs
|
|
3
|
+
* -----------
|
|
4
|
+
* Wires the ARIA tabs pattern over server-rendered markup.
|
|
5
|
+
*
|
|
6
|
+
* <div class="re-tabs" data-re-tabs>
|
|
7
|
+
* <div role="tablist" aria-label="Settings">
|
|
8
|
+
* <button role="tab" id="t-1" aria-controls="p-1" aria-selected="true">…</button>
|
|
9
|
+
* <button role="tab" id="t-2" aria-controls="p-2" aria-selected="false" tabindex="-1">…</button>
|
|
10
|
+
* </div>
|
|
11
|
+
* <section role="tabpanel" id="p-1" aria-labelledby="t-1">…</section>
|
|
12
|
+
* <section role="tabpanel" id="p-2" aria-labelledby="t-2" hidden>…</section>
|
|
13
|
+
* </div>
|
|
14
|
+
*
|
|
15
|
+
* Keyboard:
|
|
16
|
+
* ArrowLeft / ArrowRight — move focus across tabs (roving tabindex)
|
|
17
|
+
* Home / End — jump to first / last
|
|
18
|
+
* Enter / Space — activate focused tab (manual activation)
|
|
19
|
+
*
|
|
20
|
+
* Dispatches `re-change` (bubbles, cancelable) on the host `[data-re-tabs]`
|
|
21
|
+
* with `detail = { tabId, panelId }` whenever the selected tab changes.
|
|
22
|
+
*
|
|
23
|
+
* import { enhanceTabs } from "@relements/core/behaviors/tabs";
|
|
24
|
+
* const c = enhanceTabs(document);
|
|
25
|
+
* c.destroy();
|
|
26
|
+
*/
|
|
27
|
+
/** @typedef {{ destroy: () => void }} Controller */
|
|
28
|
+
/**
|
|
29
|
+
* @param {Document | Element | ShadowRoot} [root=document]
|
|
30
|
+
* @returns {Controller}
|
|
31
|
+
*/
|
|
32
|
+
declare function enhanceTabs(root?: Document | Element | ShadowRoot): Controller;
|
|
33
|
+
type Controller = {
|
|
34
|
+
destroy: () => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { type Controller, enhanceTabs };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { enhanceTabs } from '../chunk-GMICGIQW.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast helpers.
|
|
3
|
+
*
|
|
4
|
+
* <div class="re-toast-region" data-re-toast-region role="region" aria-label="Notifications">
|
|
5
|
+
* <ul class="re-toast-list" aria-live="polite" aria-relevant="additions"></ul>
|
|
6
|
+
* </div>
|
|
7
|
+
*
|
|
8
|
+
* import { showToast } from "@relements/core/behaviors/toast";
|
|
9
|
+
* showToast("Saved", { tone: "success" });
|
|
10
|
+
* showToast("Network error", { tone: "danger", duration: 8000 });
|
|
11
|
+
*
|
|
12
|
+
* If no `[data-re-toast-region]` exists, one is created and appended to
|
|
13
|
+
* `document.body` on first call.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} ToastOptions
|
|
17
|
+
* @property {"default"|"success"|"warning"|"danger"} [tone="default"]
|
|
18
|
+
* @property {number} [duration=4000] Auto-dismiss in ms. Pass 0 to disable.
|
|
19
|
+
* @property {Document|Element} [root] Override the host to search for a region in.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} message
|
|
23
|
+
* @param {ToastOptions} [options]
|
|
24
|
+
* @returns {{ dismiss: () => void; element: HTMLDivElement }}
|
|
25
|
+
*/
|
|
26
|
+
declare function showToast(message: string, options?: ToastOptions): {
|
|
27
|
+
dismiss: () => void;
|
|
28
|
+
element: HTMLDivElement;
|
|
29
|
+
};
|
|
30
|
+
type ToastOptions = {
|
|
31
|
+
tone?: "default" | "success" | "warning" | "danger" | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Auto-dismiss in ms. Pass 0 to disable.
|
|
34
|
+
*/
|
|
35
|
+
duration?: number | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Override the host to search for a region in.
|
|
38
|
+
*/
|
|
39
|
+
root?: Document | Element | undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { type ToastOptions, showToast };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { showToast } from '../chunk-PIDPGDBZ.js';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/behaviors/tabs.js
|
|
2
|
+
function enhanceTabs(root = document) {
|
|
3
|
+
if (root == null) {
|
|
4
|
+
throw new TypeError("enhanceTabs: root must be a Document, Element, or ShadowRoot");
|
|
5
|
+
}
|
|
6
|
+
const hosts = [];
|
|
7
|
+
if (root instanceof Element && /** @type {Element} */
|
|
8
|
+
root.matches?.("[data-re-tabs]")) {
|
|
9
|
+
const cleanup = wireOne(
|
|
10
|
+
/** @type {HTMLElement} */
|
|
11
|
+
root
|
|
12
|
+
);
|
|
13
|
+
hosts.push({ host: root, cleanup });
|
|
14
|
+
}
|
|
15
|
+
const tabsList = root.querySelectorAll("[data-re-tabs]");
|
|
16
|
+
tabsList.forEach((host) => {
|
|
17
|
+
const cleanup = wireOne(
|
|
18
|
+
/** @type {HTMLElement} */
|
|
19
|
+
host
|
|
20
|
+
);
|
|
21
|
+
hosts.push({ host, cleanup });
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
destroy() {
|
|
25
|
+
while (hosts.length) {
|
|
26
|
+
const entry = hosts.pop();
|
|
27
|
+
entry?.cleanup();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function wireOne(host) {
|
|
33
|
+
const tablist = host.querySelector('[role="tablist"]');
|
|
34
|
+
if (!tablist) return () => {
|
|
35
|
+
};
|
|
36
|
+
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
|
37
|
+
if (tabs.length === 0) return () => {
|
|
38
|
+
};
|
|
39
|
+
const syncRoving = () => {
|
|
40
|
+
for (const t of tabs) {
|
|
41
|
+
t.tabIndex = t.getAttribute("aria-selected") === "true" ? 0 : -1;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
syncRoving();
|
|
45
|
+
const select = (tab, opts = {}) => {
|
|
46
|
+
if (tab.getAttribute("aria-selected") === "true") {
|
|
47
|
+
if (opts.focus) tab.focus();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const panelId = tab.getAttribute("aria-controls");
|
|
51
|
+
const cancelled = !host.dispatchEvent(
|
|
52
|
+
new CustomEvent("re-change", {
|
|
53
|
+
bubbles: true,
|
|
54
|
+
cancelable: true,
|
|
55
|
+
detail: { tabId: tab.id, panelId }
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
if (cancelled) return;
|
|
59
|
+
for (const t of tabs) {
|
|
60
|
+
const isMe = t === tab;
|
|
61
|
+
t.setAttribute("aria-selected", isMe ? "true" : "false");
|
|
62
|
+
t.tabIndex = isMe ? 0 : -1;
|
|
63
|
+
const pid = t.getAttribute("aria-controls");
|
|
64
|
+
if (pid) {
|
|
65
|
+
const panel = host.querySelector(`#${cssEscape(pid)}`);
|
|
66
|
+
if (panel) panel.hidden = !isMe;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (opts.focus) tab.focus();
|
|
70
|
+
};
|
|
71
|
+
const onClick = (event) => {
|
|
72
|
+
const t = (
|
|
73
|
+
/** @type {Element | null} */
|
|
74
|
+
event.target
|
|
75
|
+
);
|
|
76
|
+
const tab = t?.closest('[role="tab"]');
|
|
77
|
+
if (tab && tabs.includes(
|
|
78
|
+
/** @type {HTMLElement} */
|
|
79
|
+
tab
|
|
80
|
+
)) {
|
|
81
|
+
select(
|
|
82
|
+
/** @type {HTMLElement} */
|
|
83
|
+
tab,
|
|
84
|
+
{ focus: true }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const onKey = (event) => {
|
|
89
|
+
const current = document.activeElement;
|
|
90
|
+
if (!current || !tabs.includes(
|
|
91
|
+
/** @type {HTMLElement} */
|
|
92
|
+
current
|
|
93
|
+
)) return;
|
|
94
|
+
const idx = tabs.indexOf(
|
|
95
|
+
/** @type {HTMLElement} */
|
|
96
|
+
current
|
|
97
|
+
);
|
|
98
|
+
if (idx === -1) return;
|
|
99
|
+
let nextIdx = idx;
|
|
100
|
+
switch (event.key) {
|
|
101
|
+
case "ArrowRight":
|
|
102
|
+
nextIdx = (idx + 1) % tabs.length;
|
|
103
|
+
break;
|
|
104
|
+
case "ArrowLeft":
|
|
105
|
+
nextIdx = (idx - 1 + tabs.length) % tabs.length;
|
|
106
|
+
break;
|
|
107
|
+
case "Home":
|
|
108
|
+
nextIdx = 0;
|
|
109
|
+
break;
|
|
110
|
+
case "End":
|
|
111
|
+
nextIdx = tabs.length - 1;
|
|
112
|
+
break;
|
|
113
|
+
case "Enter":
|
|
114
|
+
case " ":
|
|
115
|
+
case "Spacebar":
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
select(
|
|
118
|
+
/** @type {HTMLElement} */
|
|
119
|
+
current,
|
|
120
|
+
{ focus: true }
|
|
121
|
+
);
|
|
122
|
+
return;
|
|
123
|
+
default:
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
tabs[nextIdx].focus();
|
|
128
|
+
select(tabs[nextIdx]);
|
|
129
|
+
};
|
|
130
|
+
tablist.addEventListener("click", onClick);
|
|
131
|
+
tablist.addEventListener(
|
|
132
|
+
"keydown",
|
|
133
|
+
/** @type {EventListener} */
|
|
134
|
+
onKey
|
|
135
|
+
);
|
|
136
|
+
return () => {
|
|
137
|
+
tablist.removeEventListener("click", onClick);
|
|
138
|
+
tablist.removeEventListener(
|
|
139
|
+
"keydown",
|
|
140
|
+
/** @type {EventListener} */
|
|
141
|
+
onKey
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function cssEscape(value) {
|
|
146
|
+
return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { enhanceTabs };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/behaviors/dialog.js
|
|
2
|
+
function enhanceDialog(root = document) {
|
|
3
|
+
if (root == null) {
|
|
4
|
+
throw new TypeError("enhanceDialog: root must be a Document, Element, or ShadowRoot");
|
|
5
|
+
}
|
|
6
|
+
const onClick = (event) => {
|
|
7
|
+
const target = (
|
|
8
|
+
/** @type {Element | null} */
|
|
9
|
+
event.target
|
|
10
|
+
);
|
|
11
|
+
if (!target) return;
|
|
12
|
+
const trigger = target.closest("[data-re-dialog-trigger]");
|
|
13
|
+
if (trigger) {
|
|
14
|
+
const id = trigger.getAttribute("data-re-dialog-target") ?? trigger.getAttribute("aria-controls");
|
|
15
|
+
if (!id) return;
|
|
16
|
+
const ownerDoc = trigger.ownerDocument;
|
|
17
|
+
const dialog = (
|
|
18
|
+
/** @type {HTMLDialogElement | null} */
|
|
19
|
+
ownerDoc.getElementById(id)
|
|
20
|
+
);
|
|
21
|
+
if (dialog && typeof dialog.showModal === "function" && !dialog.open) {
|
|
22
|
+
dialog.showModal();
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const closeBtn = target.closest("[data-re-dialog-close]");
|
|
27
|
+
if (closeBtn) {
|
|
28
|
+
const dialog = (
|
|
29
|
+
/** @type {HTMLDialogElement | null} */
|
|
30
|
+
closeBtn.closest("dialog")
|
|
31
|
+
);
|
|
32
|
+
if (dialog && dialog.open) {
|
|
33
|
+
const value = (
|
|
34
|
+
/** @type {HTMLButtonElement} */
|
|
35
|
+
closeBtn.value || ""
|
|
36
|
+
);
|
|
37
|
+
dialog.close(value);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (target.tagName === "DIALOG") {
|
|
42
|
+
const dialog = (
|
|
43
|
+
/** @type {HTMLDialogElement} */
|
|
44
|
+
target
|
|
45
|
+
);
|
|
46
|
+
if (dialog.open && dialog.hasAttribute("data-re-dialog-close-on-backdrop") && isEventOnBackdrop(
|
|
47
|
+
/** @type {MouseEvent} */
|
|
48
|
+
event,
|
|
49
|
+
dialog
|
|
50
|
+
)) {
|
|
51
|
+
dialog.close("backdrop");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
root.addEventListener("click", onClick);
|
|
56
|
+
return {
|
|
57
|
+
destroy() {
|
|
58
|
+
root.removeEventListener("click", onClick);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function isEventOnBackdrop(event, dialog) {
|
|
63
|
+
const rect = dialog.getBoundingClientRect();
|
|
64
|
+
const { clientX: x, clientY: y } = event;
|
|
65
|
+
return x < rect.left || x > rect.right || y < rect.top || y > rect.bottom;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { enhanceDialog };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/behaviors/toast.js
|
|
2
|
+
function showToast(message, options = {}) {
|
|
3
|
+
const { tone = "default", duration = 4e3, root = document } = options;
|
|
4
|
+
const list = ensureRegion(root).querySelector(".re-toast-list");
|
|
5
|
+
if (!list) {
|
|
6
|
+
throw new Error("showToast: toast region missing a `.re-toast-list`");
|
|
7
|
+
}
|
|
8
|
+
const item = document.createElement("div");
|
|
9
|
+
item.className = "re-toast";
|
|
10
|
+
if (tone !== "default") item.dataset.tone = tone;
|
|
11
|
+
item.setAttribute("role", tone === "danger" ? "alert" : "status");
|
|
12
|
+
const body = document.createElement("div");
|
|
13
|
+
body.className = "re-toast__body";
|
|
14
|
+
body.textContent = message;
|
|
15
|
+
item.appendChild(body);
|
|
16
|
+
const dismissBtn = document.createElement("button");
|
|
17
|
+
dismissBtn.type = "button";
|
|
18
|
+
dismissBtn.className = "re-toast__dismiss";
|
|
19
|
+
dismissBtn.setAttribute("aria-label", "Dismiss notification");
|
|
20
|
+
dismissBtn.textContent = "\xD7";
|
|
21
|
+
item.appendChild(dismissBtn);
|
|
22
|
+
list.appendChild(item);
|
|
23
|
+
let timer;
|
|
24
|
+
const dismiss = () => {
|
|
25
|
+
if (timer != null) {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
timer = void 0;
|
|
28
|
+
}
|
|
29
|
+
item.dispatchEvent(new CustomEvent("re-toast-dismiss", { bubbles: true }));
|
|
30
|
+
item.remove();
|
|
31
|
+
};
|
|
32
|
+
dismissBtn.addEventListener("click", dismiss);
|
|
33
|
+
if (duration > 0) {
|
|
34
|
+
timer = setTimeout(dismiss, duration);
|
|
35
|
+
}
|
|
36
|
+
return { dismiss, element: item };
|
|
37
|
+
}
|
|
38
|
+
function ensureRegion(root) {
|
|
39
|
+
const scope = root instanceof Document ? root : (
|
|
40
|
+
/** @type {Element} */
|
|
41
|
+
root.ownerDocument ?? document
|
|
42
|
+
);
|
|
43
|
+
let region = (
|
|
44
|
+
/** @type {HTMLElement | null} */
|
|
45
|
+
scope.querySelector("[data-re-toast-region]")
|
|
46
|
+
);
|
|
47
|
+
if (region) return region;
|
|
48
|
+
region = scope.createElement("div");
|
|
49
|
+
region.className = "re-toast-region";
|
|
50
|
+
region.setAttribute("role", "region");
|
|
51
|
+
region.setAttribute("aria-label", "Notifications");
|
|
52
|
+
region.dataset.reToastRegion = "";
|
|
53
|
+
const list = scope.createElement("div");
|
|
54
|
+
list.className = "re-toast-list";
|
|
55
|
+
list.setAttribute("aria-live", "polite");
|
|
56
|
+
list.setAttribute("aria-relevant", "additions");
|
|
57
|
+
region.appendChild(list);
|
|
58
|
+
scope.body.appendChild(region);
|
|
59
|
+
return region;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { showToast };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/behaviors/popover.js
|
|
2
|
+
function enhancePopover(root = document) {
|
|
3
|
+
if (root == null) {
|
|
4
|
+
throw new TypeError("enhancePopover: root must be a Document, Element, or ShadowRoot");
|
|
5
|
+
}
|
|
6
|
+
const cleanups = [];
|
|
7
|
+
if (root instanceof Element && /** @type {Element} */
|
|
8
|
+
root.matches?.("[data-re-popover]")) {
|
|
9
|
+
cleanups.push(wireOne(
|
|
10
|
+
/** @type {HTMLElement} */
|
|
11
|
+
root
|
|
12
|
+
));
|
|
13
|
+
}
|
|
14
|
+
const popovers = root.querySelectorAll("[data-re-popover]");
|
|
15
|
+
popovers.forEach((pop) => {
|
|
16
|
+
cleanups.push(wireOne(pop));
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
destroy() {
|
|
20
|
+
while (cleanups.length) cleanups.pop()?.();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function wireOne(popover) {
|
|
25
|
+
if (!("popover" in popover) || typeof popover.showPopover !== "function") {
|
|
26
|
+
return () => {
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const onToggle = (event) => {
|
|
30
|
+
const e = (
|
|
31
|
+
/** @type {ToggleEvent} */
|
|
32
|
+
event
|
|
33
|
+
);
|
|
34
|
+
const open = e.newState === "open";
|
|
35
|
+
if (open) {
|
|
36
|
+
positionUnderTrigger(popover);
|
|
37
|
+
}
|
|
38
|
+
popover.dispatchEvent(
|
|
39
|
+
new CustomEvent("re-toggle", {
|
|
40
|
+
bubbles: true,
|
|
41
|
+
detail: { open }
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
popover.addEventListener("toggle", onToggle);
|
|
46
|
+
return () => {
|
|
47
|
+
popover.removeEventListener("toggle", onToggle);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function positionUnderTrigger(popover) {
|
|
51
|
+
const id = popover.id;
|
|
52
|
+
if (!id) return;
|
|
53
|
+
const trigger = document.querySelector(`[popovertarget="${cssEscape(id)}"]`);
|
|
54
|
+
if (!trigger) return;
|
|
55
|
+
const tRect = trigger.getBoundingClientRect();
|
|
56
|
+
popover.style.position = "fixed";
|
|
57
|
+
popover.style.top = `${tRect.bottom + 4}px`;
|
|
58
|
+
popover.style.left = `${tRect.left}px`;
|
|
59
|
+
popover.style.right = "auto";
|
|
60
|
+
popover.style.bottom = "auto";
|
|
61
|
+
popover.style.margin = "0";
|
|
62
|
+
}
|
|
63
|
+
function cssEscape(value) {
|
|
64
|
+
return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { enhancePopover };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/behaviors/dismissible.js
|
|
2
|
+
function enhanceDismissible(root = document) {
|
|
3
|
+
if (root == null) {
|
|
4
|
+
throw new TypeError("enhanceDismissible: root must be a Document, Element, or ShadowRoot");
|
|
5
|
+
}
|
|
6
|
+
const handle = (event) => {
|
|
7
|
+
const target = (
|
|
8
|
+
/** @type {Element | null} */
|
|
9
|
+
event.target
|
|
10
|
+
);
|
|
11
|
+
if (!target) return;
|
|
12
|
+
const trigger = target.closest("[data-re-dismiss]");
|
|
13
|
+
if (!trigger) return;
|
|
14
|
+
if (event.type === "keydown") {
|
|
15
|
+
const ke = (
|
|
16
|
+
/** @type {KeyboardEvent} */
|
|
17
|
+
event
|
|
18
|
+
);
|
|
19
|
+
if (ke.key !== "Enter" && ke.key !== " " && ke.key !== "Spacebar") return;
|
|
20
|
+
ke.preventDefault();
|
|
21
|
+
}
|
|
22
|
+
const host = trigger.closest("[data-re-dismissible]");
|
|
23
|
+
if (!host) return;
|
|
24
|
+
const cancelable = host.dispatchEvent(
|
|
25
|
+
new CustomEvent("re-dismiss", { bubbles: true, cancelable: true })
|
|
26
|
+
);
|
|
27
|
+
if (!cancelable) return;
|
|
28
|
+
host.hidden = true;
|
|
29
|
+
};
|
|
30
|
+
root.addEventListener("click", handle);
|
|
31
|
+
root.addEventListener("keydown", handle);
|
|
32
|
+
return {
|
|
33
|
+
destroy() {
|
|
34
|
+
root.removeEventListener("click", handle);
|
|
35
|
+
root.removeEventListener("keydown", handle);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { enhanceDismissible };
|