@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
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
|
|
28
|
+
/** @typedef {{ destroy: () => void }} Controller */
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {Document | Element | ShadowRoot} [root=document]
|
|
32
|
+
* @returns {Controller}
|
|
33
|
+
*/
|
|
34
|
+
export function enhanceTabs(root = document) {
|
|
35
|
+
if (root == null) {
|
|
36
|
+
throw new TypeError("enhanceTabs: root must be a Document, Element, or ShadowRoot");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @type {Array<{ host: Element; cleanup: () => void }>} */
|
|
40
|
+
const hosts = [];
|
|
41
|
+
|
|
42
|
+
// Include the root itself when it carries the marker (e.g. a custom element host).
|
|
43
|
+
if (root instanceof Element && /** @type {Element} */ (root).matches?.("[data-re-tabs]")) {
|
|
44
|
+
const cleanup = wireOne(/** @type {HTMLElement} */ (root));
|
|
45
|
+
hosts.push({ host: root, cleanup });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @type {NodeListOf<Element>} */
|
|
49
|
+
const tabsList = root.querySelectorAll("[data-re-tabs]");
|
|
50
|
+
tabsList.forEach((host) => {
|
|
51
|
+
const cleanup = wireOne(/** @type {HTMLElement} */ (host));
|
|
52
|
+
hosts.push({ host, cleanup });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
destroy() {
|
|
57
|
+
while (hosts.length) {
|
|
58
|
+
const entry = hosts.pop();
|
|
59
|
+
entry?.cleanup();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {HTMLElement} host
|
|
67
|
+
* @returns {() => void}
|
|
68
|
+
*/
|
|
69
|
+
function wireOne(host) {
|
|
70
|
+
const tablist = host.querySelector('[role="tablist"]');
|
|
71
|
+
if (!tablist) return () => {};
|
|
72
|
+
/** @type {HTMLElement[]} */
|
|
73
|
+
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
|
74
|
+
if (tabs.length === 0) return () => {};
|
|
75
|
+
|
|
76
|
+
// Ensure roving tabindex matches aria-selected.
|
|
77
|
+
const syncRoving = () => {
|
|
78
|
+
for (const t of tabs) {
|
|
79
|
+
t.tabIndex = t.getAttribute("aria-selected") === "true" ? 0 : -1;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
syncRoving();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {HTMLElement} tab
|
|
86
|
+
* @param {{ focus?: boolean }} [opts]
|
|
87
|
+
*/
|
|
88
|
+
const select = (tab, opts = {}) => {
|
|
89
|
+
if (tab.getAttribute("aria-selected") === "true") {
|
|
90
|
+
if (opts.focus) tab.focus();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const panelId = tab.getAttribute("aria-controls");
|
|
94
|
+
const cancelled = !host.dispatchEvent(
|
|
95
|
+
new CustomEvent("re-change", {
|
|
96
|
+
bubbles: true,
|
|
97
|
+
cancelable: true,
|
|
98
|
+
detail: { tabId: tab.id, panelId },
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
if (cancelled) return;
|
|
102
|
+
|
|
103
|
+
for (const t of tabs) {
|
|
104
|
+
const isMe = t === tab;
|
|
105
|
+
t.setAttribute("aria-selected", isMe ? "true" : "false");
|
|
106
|
+
t.tabIndex = isMe ? 0 : -1;
|
|
107
|
+
const pid = t.getAttribute("aria-controls");
|
|
108
|
+
if (pid) {
|
|
109
|
+
const panel = host.querySelector(`#${cssEscape(pid)}`);
|
|
110
|
+
if (panel) /** @type {HTMLElement} */ (panel).hidden = !isMe;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (opts.focus) tab.focus();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** @param {Event} event */
|
|
117
|
+
const onClick = (event) => {
|
|
118
|
+
const t = /** @type {Element | null} */ (event.target);
|
|
119
|
+
const tab = t?.closest('[role="tab"]');
|
|
120
|
+
if (tab && tabs.includes(/** @type {HTMLElement} */ (tab))) {
|
|
121
|
+
select(/** @type {HTMLElement} */ (tab), { focus: true });
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** @param {KeyboardEvent} event */
|
|
126
|
+
const onKey = (event) => {
|
|
127
|
+
const current = document.activeElement;
|
|
128
|
+
if (!current || !tabs.includes(/** @type {HTMLElement} */ (current))) return;
|
|
129
|
+
const idx = tabs.indexOf(/** @type {HTMLElement} */ (current));
|
|
130
|
+
if (idx === -1) return;
|
|
131
|
+
let nextIdx = idx;
|
|
132
|
+
switch (event.key) {
|
|
133
|
+
case "ArrowRight":
|
|
134
|
+
nextIdx = (idx + 1) % tabs.length;
|
|
135
|
+
break;
|
|
136
|
+
case "ArrowLeft":
|
|
137
|
+
nextIdx = (idx - 1 + tabs.length) % tabs.length;
|
|
138
|
+
break;
|
|
139
|
+
case "Home":
|
|
140
|
+
nextIdx = 0;
|
|
141
|
+
break;
|
|
142
|
+
case "End":
|
|
143
|
+
nextIdx = tabs.length - 1;
|
|
144
|
+
break;
|
|
145
|
+
case "Enter":
|
|
146
|
+
case " ":
|
|
147
|
+
case "Spacebar":
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
select(/** @type {HTMLElement} */ (current), { focus: true });
|
|
150
|
+
return;
|
|
151
|
+
default:
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
tabs[nextIdx].focus();
|
|
156
|
+
select(tabs[nextIdx]);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
tablist.addEventListener("click", onClick);
|
|
160
|
+
tablist.addEventListener("keydown", /** @type {EventListener} */ (onKey));
|
|
161
|
+
|
|
162
|
+
return () => {
|
|
163
|
+
tablist.removeEventListener("click", onClick);
|
|
164
|
+
tablist.removeEventListener("keydown", /** @type {EventListener} */ (onKey));
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @param {string} value */
|
|
169
|
+
function cssEscape(value) {
|
|
170
|
+
return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value;
|
|
171
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
/**
|
|
17
|
+
* @typedef {object} ToastOptions
|
|
18
|
+
* @property {"default"|"success"|"warning"|"danger"} [tone="default"]
|
|
19
|
+
* @property {number} [duration=4000] Auto-dismiss in ms. Pass 0 to disable.
|
|
20
|
+
* @property {Document|Element} [root] Override the host to search for a region in.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} message
|
|
25
|
+
* @param {ToastOptions} [options]
|
|
26
|
+
* @returns {{ dismiss: () => void; element: HTMLDivElement }}
|
|
27
|
+
*/
|
|
28
|
+
export function showToast(message, options = {}) {
|
|
29
|
+
const { tone = "default", duration = 4000, root = document } = options;
|
|
30
|
+
|
|
31
|
+
const list = ensureRegion(root).querySelector(".re-toast-list");
|
|
32
|
+
if (!list) {
|
|
33
|
+
throw new Error("showToast: toast region missing a `.re-toast-list`");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const item = document.createElement("div");
|
|
37
|
+
item.className = "re-toast";
|
|
38
|
+
if (tone !== "default") item.dataset.tone = tone;
|
|
39
|
+
item.setAttribute("role", tone === "danger" ? "alert" : "status");
|
|
40
|
+
|
|
41
|
+
const body = document.createElement("div");
|
|
42
|
+
body.className = "re-toast__body";
|
|
43
|
+
body.textContent = message;
|
|
44
|
+
item.appendChild(body);
|
|
45
|
+
|
|
46
|
+
const dismissBtn = document.createElement("button");
|
|
47
|
+
dismissBtn.type = "button";
|
|
48
|
+
dismissBtn.className = "re-toast__dismiss";
|
|
49
|
+
dismissBtn.setAttribute("aria-label", "Dismiss notification");
|
|
50
|
+
dismissBtn.textContent = "×";
|
|
51
|
+
item.appendChild(dismissBtn);
|
|
52
|
+
|
|
53
|
+
list.appendChild(item);
|
|
54
|
+
|
|
55
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
56
|
+
let timer;
|
|
57
|
+
const dismiss = () => {
|
|
58
|
+
if (timer != null) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
timer = undefined;
|
|
61
|
+
}
|
|
62
|
+
item.dispatchEvent(new CustomEvent("re-toast-dismiss", { bubbles: true }));
|
|
63
|
+
item.remove();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
dismissBtn.addEventListener("click", dismiss);
|
|
67
|
+
|
|
68
|
+
if (duration > 0) {
|
|
69
|
+
timer = setTimeout(dismiss, duration);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { dismiss, element: item };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {Document|Element} root
|
|
77
|
+
* @returns {HTMLElement}
|
|
78
|
+
*/
|
|
79
|
+
function ensureRegion(root) {
|
|
80
|
+
const scope =
|
|
81
|
+
root instanceof Document ? root : /** @type {Element} */ (root.ownerDocument ?? document);
|
|
82
|
+
/** @type {HTMLElement | null} */
|
|
83
|
+
let region = /** @type {HTMLElement | null} */ (scope.querySelector("[data-re-toast-region]"));
|
|
84
|
+
if (region) return region;
|
|
85
|
+
region = scope.createElement("div");
|
|
86
|
+
region.className = "re-toast-region";
|
|
87
|
+
region.setAttribute("role", "region");
|
|
88
|
+
region.setAttribute("aria-label", "Notifications");
|
|
89
|
+
region.dataset.reToastRegion = "";
|
|
90
|
+
const list = scope.createElement("div");
|
|
91
|
+
list.className = "re-toast-list";
|
|
92
|
+
list.setAttribute("aria-live", "polite");
|
|
93
|
+
list.setAttribute("aria-relevant", "additions");
|
|
94
|
+
region.appendChild(list);
|
|
95
|
+
scope.body.appendChild(region);
|
|
96
|
+
return region;
|
|
97
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relements button.
|
|
3
|
+
*
|
|
4
|
+
* Applies to <button>, <a class="re-button">, and <input type="submit|button|reset">.
|
|
5
|
+
* Native semantics stay intact — this is purely styling on top of the host element.
|
|
6
|
+
*
|
|
7
|
+
* Variants (data-variant):
|
|
8
|
+
* primary — accent fill, white text. Default if data-variant absent.
|
|
9
|
+
* secondary — outlined neutral
|
|
10
|
+
* ghost — transparent, neutral text, no border
|
|
11
|
+
* danger — danger fill
|
|
12
|
+
*
|
|
13
|
+
* Sizes (data-size):
|
|
14
|
+
* sm, md (default), lg
|
|
15
|
+
*
|
|
16
|
+
* States:
|
|
17
|
+
* :disabled, :hover, :active, :focus-visible, [aria-disabled="true"]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
@layer re.components {
|
|
21
|
+
.re-button {
|
|
22
|
+
/* token-bound layout */
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
gap: var(--re-space-2);
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
|
|
29
|
+
/* default = primary md */
|
|
30
|
+
block-size: var(--re-control-height-md);
|
|
31
|
+
padding-inline: var(--re-space-4);
|
|
32
|
+
padding-block: 0;
|
|
33
|
+
|
|
34
|
+
/* type */
|
|
35
|
+
font: inherit;
|
|
36
|
+
font-weight: var(--re-font-weight-medium);
|
|
37
|
+
font-size: var(--re-size-text-md);
|
|
38
|
+
line-height: 1;
|
|
39
|
+
text-align: center;
|
|
40
|
+
text-decoration: none;
|
|
41
|
+
white-space: nowrap;
|
|
42
|
+
user-select: none;
|
|
43
|
+
|
|
44
|
+
/* surface */
|
|
45
|
+
color: var(--re-color-text-on-accent);
|
|
46
|
+
background-color: var(--re-color-accent-600);
|
|
47
|
+
border: var(--re-border-width) solid transparent;
|
|
48
|
+
border-radius: var(--re-radius-md);
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
|
|
51
|
+
transition:
|
|
52
|
+
background-color var(--re-duration-fast) var(--re-easing-standard),
|
|
53
|
+
color var(--re-duration-fast) var(--re-easing-standard),
|
|
54
|
+
border-color var(--re-duration-fast) var(--re-easing-standard),
|
|
55
|
+
box-shadow var(--re-duration-fast) var(--re-easing-standard);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Make link-as-button visually identical and behave on tap. */
|
|
59
|
+
a.re-button {
|
|
60
|
+
-webkit-tap-highlight-color: transparent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.re-button:hover {
|
|
64
|
+
background-color: var(--re-color-accent-700);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.re-button:active {
|
|
68
|
+
background-color: var(--re-color-accent-800);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.re-button:focus-visible {
|
|
72
|
+
outline: none;
|
|
73
|
+
box-shadow: var(--re-shadow-focus);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ---- Disabled (native + aria) ---------------------------------------- */
|
|
77
|
+
.re-button:disabled,
|
|
78
|
+
.re-button[aria-disabled="true"] {
|
|
79
|
+
cursor: not-allowed;
|
|
80
|
+
opacity: 0.55;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
}
|
|
83
|
+
/* Allow aria-disabled link buttons to keep keyboard focus. */
|
|
84
|
+
a.re-button[aria-disabled="true"] {
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ---- Variants -------------------------------------------------------- */
|
|
89
|
+
.re-button[data-variant="secondary"] {
|
|
90
|
+
color: var(--re-color-text);
|
|
91
|
+
background-color: var(--re-color-bg);
|
|
92
|
+
border-color: var(--re-color-border-strong);
|
|
93
|
+
}
|
|
94
|
+
.re-button[data-variant="secondary"]:hover {
|
|
95
|
+
background-color: var(--re-color-bg-subtle);
|
|
96
|
+
}
|
|
97
|
+
.re-button[data-variant="secondary"]:active {
|
|
98
|
+
background-color: var(--re-color-bg-muted);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.re-button[data-variant="ghost"] {
|
|
102
|
+
color: var(--re-color-text);
|
|
103
|
+
background-color: transparent;
|
|
104
|
+
border-color: transparent;
|
|
105
|
+
}
|
|
106
|
+
.re-button[data-variant="ghost"]:hover {
|
|
107
|
+
background-color: var(--re-color-bg-subtle);
|
|
108
|
+
}
|
|
109
|
+
.re-button[data-variant="ghost"]:active {
|
|
110
|
+
background-color: var(--re-color-bg-muted);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.re-button[data-variant="danger"] {
|
|
114
|
+
color: var(--re-color-text-on-accent);
|
|
115
|
+
background-color: var(--re-color-danger-600);
|
|
116
|
+
}
|
|
117
|
+
.re-button[data-variant="danger"]:hover {
|
|
118
|
+
background-color: var(--re-color-danger-700);
|
|
119
|
+
}
|
|
120
|
+
.re-button[data-variant="danger"]:active {
|
|
121
|
+
background-color: var(--re-color-danger-700);
|
|
122
|
+
filter: brightness(0.95);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ---- Sizes ----------------------------------------------------------- */
|
|
126
|
+
.re-button[data-size="sm"] {
|
|
127
|
+
block-size: var(--re-control-height-sm);
|
|
128
|
+
padding-inline: var(--re-space-3);
|
|
129
|
+
font-size: var(--re-size-text-sm);
|
|
130
|
+
}
|
|
131
|
+
.re-button[data-size="lg"] {
|
|
132
|
+
block-size: var(--re-control-height-lg);
|
|
133
|
+
padding-inline: var(--re-space-5);
|
|
134
|
+
font-size: var(--re-size-text-lg);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ---- Full-width modifier -------------------------------------------- */
|
|
138
|
+
.re-button[data-full-width] {
|
|
139
|
+
inline-size: 100%;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relements dialog.
|
|
3
|
+
*
|
|
4
|
+
* Styled native <dialog>. The browser owns showModal/show/close, focus
|
|
5
|
+
* trap, backdrop, and Escape-to-dismiss. This stylesheet supplies
|
|
6
|
+
* surface, header/body/footer slots, and the backdrop look.
|
|
7
|
+
*
|
|
8
|
+
* <dialog class="re-dialog">
|
|
9
|
+
* <header class="re-dialog__header">
|
|
10
|
+
* <h2 class="re-dialog__title">…</h2>
|
|
11
|
+
* <button class="re-dialog__close" aria-label="Close">×</button>
|
|
12
|
+
* </header>
|
|
13
|
+
* <div class="re-dialog__body">…</div>
|
|
14
|
+
* <footer class="re-dialog__footer">…</footer>
|
|
15
|
+
* </dialog>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
@layer re.components {
|
|
19
|
+
/* Restore the native hide-when-closed behaviour.
|
|
20
|
+
CSS layers beat the UA stylesheet, so display:flex without this
|
|
21
|
+
override makes ALL .re-dialog elements visible at all times. */
|
|
22
|
+
.re-dialog:not([open]) {
|
|
23
|
+
display: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.re-dialog {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
border: var(--re-border-default);
|
|
29
|
+
border-radius: var(--re-radius-lg);
|
|
30
|
+
background-color: var(--re-color-surface);
|
|
31
|
+
color: var(--re-color-text);
|
|
32
|
+
padding: 0;
|
|
33
|
+
inline-size: min(90vw, 32rem);
|
|
34
|
+
max-block-size: min(85vh, 40rem);
|
|
35
|
+
box-shadow: var(--re-shadow-lg);
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Backdrop owned by the browser; tint with tokens. */
|
|
42
|
+
.re-dialog::backdrop {
|
|
43
|
+
background-color: rgb(0 0 0 / 0.4);
|
|
44
|
+
backdrop-filter: blur(2px);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.re-dialog:focus-visible {
|
|
48
|
+
outline: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.re-dialog__header {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: space-between;
|
|
55
|
+
gap: var(--re-space-3);
|
|
56
|
+
padding: var(--re-space-4);
|
|
57
|
+
border-block-end: var(--re-border-default);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.re-dialog__title {
|
|
61
|
+
margin: 0;
|
|
62
|
+
font-size: var(--re-size-text-lg);
|
|
63
|
+
font-weight: var(--re-font-weight-semibold);
|
|
64
|
+
line-height: var(--re-line-height-tight);
|
|
65
|
+
color: var(--re-color-text);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.re-dialog__close {
|
|
69
|
+
appearance: none;
|
|
70
|
+
background: transparent;
|
|
71
|
+
border: 0;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
padding: var(--re-space-1) var(--re-space-2);
|
|
74
|
+
border-radius: var(--re-radius-md);
|
|
75
|
+
color: var(--re-color-text-muted);
|
|
76
|
+
font: inherit;
|
|
77
|
+
font-size: var(--re-size-text-xl);
|
|
78
|
+
line-height: 1;
|
|
79
|
+
transition: background-color var(--re-duration-fast) var(--re-easing-standard);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.re-dialog__close:hover {
|
|
83
|
+
background-color: var(--re-color-bg-subtle);
|
|
84
|
+
color: var(--re-color-text);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.re-dialog__close:focus-visible {
|
|
88
|
+
outline: none;
|
|
89
|
+
box-shadow: var(--re-shadow-focus);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.re-dialog__body {
|
|
93
|
+
padding: var(--re-space-4);
|
|
94
|
+
overflow-y: auto;
|
|
95
|
+
line-height: var(--re-line-height-normal);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.re-dialog__footer {
|
|
99
|
+
display: flex;
|
|
100
|
+
justify-content: flex-end;
|
|
101
|
+
gap: var(--re-space-3);
|
|
102
|
+
padding: var(--re-space-4);
|
|
103
|
+
border-block-start: var(--re-border-default);
|
|
104
|
+
background-color: var(--re-color-bg-subtle);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relements disclosure.
|
|
3
|
+
*
|
|
4
|
+
* Styled native <details>/<summary>. Toggle behavior, keyboard, and
|
|
5
|
+
* accessibility are owned by the browser; this stylesheet supplies layout
|
|
6
|
+
* and visual affordances only.
|
|
7
|
+
*
|
|
8
|
+
* <details class="re-disclosure">
|
|
9
|
+
* <summary class="re-disclosure__summary">Title</summary>
|
|
10
|
+
* <div class="re-disclosure__body">…</div>
|
|
11
|
+
* </details>
|
|
12
|
+
*
|
|
13
|
+
* Variants (data-variant on <details>):
|
|
14
|
+
* default — bordered card
|
|
15
|
+
* plain — borderless, just a hover background on the summary
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
@layer re.components {
|
|
19
|
+
.re-disclosure {
|
|
20
|
+
border: var(--re-border-default);
|
|
21
|
+
border-radius: var(--re-radius-md);
|
|
22
|
+
background-color: var(--re-color-surface);
|
|
23
|
+
overflow: hidden; /* keep summary corners aligned with the border-radius */
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.re-disclosure__summary {
|
|
27
|
+
list-style: none; /* hide default marker; replaced with our chevron */
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
padding: var(--re-space-3) var(--re-space-4);
|
|
30
|
+
font-weight: var(--re-font-weight-medium);
|
|
31
|
+
color: var(--re-color-text);
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: var(--re-space-3);
|
|
35
|
+
user-select: none;
|
|
36
|
+
transition: background-color var(--re-duration-fast) var(--re-easing-standard);
|
|
37
|
+
}
|
|
38
|
+
.re-disclosure__summary::-webkit-details-marker {
|
|
39
|
+
display: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.re-disclosure__summary::before {
|
|
43
|
+
content: "";
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
inline-size: 0.6rem;
|
|
46
|
+
block-size: 0.6rem;
|
|
47
|
+
border-inline-end: 2px solid currentColor;
|
|
48
|
+
border-block-end: 2px solid currentColor;
|
|
49
|
+
transform: rotate(-45deg);
|
|
50
|
+
transition: transform var(--re-duration-fast) var(--re-easing-standard);
|
|
51
|
+
margin-inline-end: var(--re-space-1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.re-disclosure[open] > .re-disclosure__summary::before {
|
|
55
|
+
transform: rotate(45deg);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.re-disclosure__summary:hover {
|
|
59
|
+
background-color: var(--re-color-bg-subtle);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.re-disclosure__summary:focus-visible {
|
|
63
|
+
outline: none;
|
|
64
|
+
box-shadow: var(--re-shadow-focus);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.re-disclosure__body {
|
|
68
|
+
padding: var(--re-space-3) var(--re-space-4) var(--re-space-4);
|
|
69
|
+
border-block-start: var(--re-border-default);
|
|
70
|
+
color: var(--re-color-text);
|
|
71
|
+
line-height: var(--re-line-height-normal);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* ---- Variants -------------------------------------------------------- */
|
|
75
|
+
.re-disclosure[data-variant="plain"] {
|
|
76
|
+
border: 0;
|
|
77
|
+
background: transparent;
|
|
78
|
+
}
|
|
79
|
+
.re-disclosure[data-variant="plain"] > .re-disclosure__body {
|
|
80
|
+
border-block-start: 0;
|
|
81
|
+
padding-block-start: var(--re-space-2);
|
|
82
|
+
}
|
|
83
|
+
}
|