@panphora/clayjs 1.8.1 → 1.9.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/clay.standalone.js +2477 -1549
- package/package.json +8 -4
- package/src/core/host-attrs.js +11 -0
- package/src/core/host-meta.js +47 -12
- package/src/core/is-edit-mode.js +5 -2
- package/src/events/onpagemutation.js +4 -2
- package/src/lib/root-attrs.js +6 -1
- package/src/loader-logic.js +4 -1
- package/src/loader.js +11 -3
- package/src/plugins/people.js +424 -0
- package/src/plugins/sortable.js +68 -0
- package/src/plugins/upload.js +6 -0
- package/src/ui/bevel-controls.js +70 -10
- package/src/ui/bevel-dialog.js +70 -21
- package/src/ui/dialogs.js +16 -8
- package/src/ui/index.js +3 -0
- package/src/ui/modal.js +18 -3
- package/src/ui/theme-parts.js +122 -0
- package/src/ui/theme.js +277 -0
- package/src/vendor/hyper-morph.vendor.js +4 -4
package/src/ui/bevel-dialog.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// The frame every ClayJS dialog draws: a backdrop, a Bevel panel, an optional corner
|
|
2
2
|
// close. It holds no behaviour. The modal and the crop adapter own focus, Escape,
|
|
3
3
|
// backdrop clicks and settling, so each keeps the rules it already had.
|
|
4
|
-
import { set } from "../lib/hostile-css.js";
|
|
5
|
-
import { bevelBox, bevelSurface, bevelText, pageScheme, protectIcon } from "./bevel-controls.js";
|
|
4
|
+
import { style, set } from "../lib/hostile-css.js";
|
|
5
|
+
import { bevelBox, bevelSurface, bevelText, pageScheme, protectIcon, restyleBox } from "./bevel-controls.js";
|
|
6
6
|
import { TOKENS, FONT_SANS, RULES, GLYPHS } from "./bevel.js";
|
|
7
|
+
import { themed, tokenRulesFor, partRules, markParts } from "./theme-parts.js";
|
|
7
8
|
|
|
8
9
|
// A frame's own way to settle itself. The modal is a singleton and clears every frame
|
|
9
10
|
// when it opens; a frame it does not own (the crop dialog) registers here, so it is
|
|
@@ -35,45 +36,88 @@ const LINE = `1px solid ${TOKENS["line-2"]}`;
|
|
|
35
36
|
|
|
36
37
|
// The close: a column at the header's right edge in the button face, or a 62px square
|
|
37
38
|
// at the panel's corner when there is no header.
|
|
38
|
-
function dialogClose(label, inHeader) {
|
|
39
|
+
function dialogClose(label, inHeader, theme = null, names = [], iconNames = []) {
|
|
39
40
|
const hover = RULES.dialogCloseHover[0].slice("background:".length);
|
|
40
|
-
const
|
|
41
|
+
const base = [
|
|
41
42
|
"position:absolute", "top:-1px", "right:-1px", "z-index:2",
|
|
42
43
|
"display:grid", "place-items:center", inHeader ? "height:calc(100% + 1px)" : "height:62px",
|
|
43
44
|
"margin:0", "padding:0", "border:0", ...RULES.dialogClose, ...(inHeader ? [] : [`border-bottom:${LINE}`]),
|
|
44
45
|
"cursor:pointer", "outline:none",
|
|
45
|
-
]
|
|
46
|
+
];
|
|
47
|
+
const b = bevelBox("button", base);
|
|
46
48
|
b.type = "button";
|
|
47
49
|
b.setAttribute("aria-label", label);
|
|
48
50
|
b.title = label;
|
|
49
51
|
b.innerHTML = GLYPHS.dialogClose;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (!themed(theme)) {
|
|
53
|
+
protectIcon(b.firstElementChild, 16);
|
|
54
|
+
b.addEventListener("pointerenter", () => set(b, "background", hover));
|
|
55
|
+
b.addEventListener("pointerleave", () => set(b, "background", TOKENS.face));
|
|
56
|
+
b.addEventListener("focus", () => { set(b, "outline", `2px solid ${TOKENS.brass}`); set(b, "outline-offset", "-4px"); });
|
|
57
|
+
b.addEventListener("blur", () => set(b, "outline", "none"));
|
|
58
|
+
return b;
|
|
59
|
+
}
|
|
60
|
+
const allNames = ["close", ...names];
|
|
61
|
+
const icon = ["icon", ...iconNames];
|
|
62
|
+
markParts(b, theme, allNames);
|
|
63
|
+
markParts(b.firstElementChild, theme, icon);
|
|
64
|
+
const flags = { hover: false, focus: false, pressed: false };
|
|
65
|
+
const paint = () => {
|
|
66
|
+
const act = { hover: flags.hover, active: flags.pressed, focus: flags.focus };
|
|
67
|
+
const svg = b.firstElementChild;
|
|
68
|
+
svg.style.cssText = "";
|
|
69
|
+
protectIcon(svg, 16);
|
|
70
|
+
style(svg, partRules(theme, icon, act));
|
|
71
|
+
restyleBox(b, [
|
|
72
|
+
...base,
|
|
73
|
+
...(flags.hover ? [`background:${hover}`] : []),
|
|
74
|
+
...(flags.focus ? [`outline:2px solid ${TOKENS.brass}`, "outline-offset:-4px"] : []),
|
|
75
|
+
...tokenRulesFor(theme, "close", act),
|
|
76
|
+
...partRules(theme, allNames, act),
|
|
77
|
+
]);
|
|
78
|
+
};
|
|
79
|
+
b.addEventListener("pointerenter", () => { flags.hover = true; paint(); });
|
|
80
|
+
b.addEventListener("pointerleave", () => { flags.hover = false; flags.pressed = false; paint(); });
|
|
81
|
+
b.addEventListener("pointerdown", (e) => {
|
|
82
|
+
if (e.button !== undefined && e.button !== 0) return;
|
|
83
|
+
flags.pressed = true;
|
|
84
|
+
paint();
|
|
85
|
+
});
|
|
86
|
+
b.addEventListener("pointerup", () => { flags.pressed = false; paint(); });
|
|
87
|
+
b.addEventListener("focus", () => { flags.focus = true; paint(); });
|
|
88
|
+
b.addEventListener("blur", () => { flags.focus = false; flags.pressed = false; paint(); });
|
|
89
|
+
paint();
|
|
55
90
|
return b;
|
|
56
91
|
}
|
|
57
92
|
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
// `theme` is a captured resolveTheme() result, or null for the exact Bevel look.
|
|
94
|
+
// `surface` names the dialog for part overrides: "modal" uses dialog.* parts only;
|
|
95
|
+
// "people" and "crop" add people.* or crop.* after them, so those win.
|
|
96
|
+
export function bevelDialog({ zIndex = "100", width = "600px", closable = false, titled = false, theme = null, surface = "modal" } = {}) {
|
|
97
|
+
const on = themed(theme);
|
|
98
|
+
const t = on ? theme.tokens : {};
|
|
99
|
+
const S = t.spacing;
|
|
100
|
+
const names = (part) => (surface === "modal" ? [`dialog.${part}`] : [`dialog.${part}`, `${surface}.${part}`]);
|
|
101
|
+
|
|
102
|
+
const root = bevelBox("div", ["display:block"], { theme, parts: names("root") });
|
|
60
103
|
root.setAttribute("data-clay-modal", "");
|
|
104
|
+
if (on) root.setAttribute("data-clay-ui", surface);
|
|
61
105
|
root.setAttribute("aria-hidden", "true");
|
|
62
|
-
set(root, "color-scheme", pageScheme());
|
|
106
|
+
set(root, "color-scheme", on ? theme.scheme : pageScheme());
|
|
63
107
|
|
|
64
108
|
const overlay = bevelBox("div", [
|
|
65
109
|
"position:fixed", "inset:0", `z-index:${zIndex}`, "overflow:auto",
|
|
66
110
|
"display:flex", "align-items:flex-start", "justify-content:center",
|
|
67
111
|
"padding:min(96px, 10vh) 16px 16px",
|
|
68
112
|
`background:color-mix(in srgb, ${TOKENS.ground} 72%, transparent)`,
|
|
69
|
-
]);
|
|
113
|
+
], { theme, parts: names("overlay"), role: "overlay" });
|
|
70
114
|
overlay.tabIndex = -1;
|
|
71
115
|
|
|
72
116
|
const panel = bevelSurface("form", [
|
|
73
117
|
"position:relative", "display:flex", "flex-direction:column", "margin:0",
|
|
74
118
|
`width:min(${width}, 100%)`, "max-height:calc(100dvh - min(96px, 10vh) - 16px)",
|
|
75
119
|
...RULES.dialogPanel,
|
|
76
|
-
]);
|
|
120
|
+
], { theme, parts: names("panel") });
|
|
77
121
|
panel.setAttribute("role", "dialog");
|
|
78
122
|
panel.setAttribute("aria-modal", "true");
|
|
79
123
|
|
|
@@ -83,8 +127,10 @@ export function bevelDialog({ zIndex = "100", width = "600px", closable = false,
|
|
|
83
127
|
header = bevelBox("div", [
|
|
84
128
|
"position:relative", "display:block", "flex:none",
|
|
85
129
|
`padding:20px ${closable ? "68px" : "22px"} 16px 22px`, ...RULES.dialogHeader,
|
|
86
|
-
|
|
87
|
-
|
|
130
|
+
...(t.border ? [`border-bottom-color:${t.border}`] : []),
|
|
131
|
+
...(S ? [`padding:${S} ${closable ? `calc(${S} + 46px)` : S} calc(${S} * .7) ${S}`] : []),
|
|
132
|
+
], { theme, parts: names("header") });
|
|
133
|
+
heading = bevelText("div", ["display:block", ...RULES.dialogHeading, "overflow-wrap:anywhere"], undefined, { theme, parts: names("title"), role: "heading" });
|
|
88
134
|
heading.setAttribute("role", "heading");
|
|
89
135
|
heading.setAttribute("aria-level", "2");
|
|
90
136
|
header.append(heading);
|
|
@@ -95,16 +141,19 @@ export function bevelDialog({ zIndex = "100", width = "600px", closable = false,
|
|
|
95
141
|
"display:block", "flex:1 1 auto", "min-height:0", "overflow-y:auto", "overflow-x:hidden",
|
|
96
142
|
`padding:20px ${closable && !titled ? "84px" : "22px"} 20px 22px`,
|
|
97
143
|
"overflow-wrap:anywhere", `font:14.5px/1.55 ${FONT_SANS}`,
|
|
98
|
-
|
|
144
|
+
...(S ? [`padding:${S} ${closable && !titled ? `calc(${S} + 62px)` : S} ${S} ${S}`] : []),
|
|
145
|
+
], undefined, { theme, parts: names("body") });
|
|
99
146
|
const footer = bevelBox("div", [
|
|
100
147
|
"display:flex", "flex:none", "flex-wrap:wrap", "justify-content:flex-end", "align-items:center", ...RULES.dialogFooter,
|
|
101
|
-
|
|
148
|
+
...(t.border ? [`border-top-color:${t.border}`] : []),
|
|
149
|
+
...(S ? [`padding:calc(${S} * .65) ${S}`, `gap:calc(${S} / 2)`] : []),
|
|
150
|
+
], { theme, parts: [...names("footer"), ...names("actions")] });
|
|
102
151
|
panel.append(body, footer);
|
|
103
152
|
|
|
104
|
-
const close = closable ? dialogClose("Close", titled) : null;
|
|
153
|
+
const close = closable ? dialogClose("Close", titled, theme, names("close"), names("closeIcon")) : null;
|
|
105
154
|
if (close) (header || panel).append(close);
|
|
106
155
|
|
|
107
156
|
overlay.append(panel);
|
|
108
157
|
root.append(overlay);
|
|
109
|
-
return { root, overlay, panel, header, heading, body, footer, close };
|
|
158
|
+
return { root, overlay, panel, header, heading, body, footer, close, theme };
|
|
110
159
|
}
|
package/src/ui/dialogs.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import themodal from "./modal.js";
|
|
1
|
+
import themodal, { SURFACE_THEME } from "./modal.js";
|
|
2
2
|
import onDomReady from "../lib/dom-ready.js";
|
|
3
3
|
import toast from "./toast.js";
|
|
4
4
|
import copyToClipboard from "../utils/copy-to-clipboard.js";
|
|
5
5
|
import { bevelBox, bevelButton, bevelInput, bevelText } from "./bevel-controls.js";
|
|
6
6
|
import { TOKENS, FONT_SANS, FONT_MONO } from "./bevel.js";
|
|
7
|
+
import { resolveTheme } from "./theme.js";
|
|
7
8
|
|
|
8
9
|
// Caller markup (promptText, extraContent, tell's paragraphs) goes in as it is: those
|
|
9
10
|
// are markup by design, and callers pass elements through them. Only the frame around
|
|
10
11
|
// it is ClayJS's to style. The prompt is the dialog's title, as in the dashboard.
|
|
11
12
|
function createModal(promptText, yesCallback, extraContent = "", includeInput = false, defaultValue = "", yesLabel = "OK") {
|
|
12
|
-
const
|
|
13
|
+
const theme = resolveTheme();
|
|
14
|
+
const content = bevelBox("div", ["display:flex", "flex-direction:column", "gap:14px", `font:14.5px/1.55 ${FONT_SANS}`, `color:${TOKENS["ink-2"]}`], { theme, parts: ["dialog.content"], role: "text" });
|
|
13
15
|
let input = null;
|
|
14
16
|
if (includeInput) {
|
|
15
|
-
input = bevelInput("input", { rules: ["display:block", "width:100%", "margin:0"] });
|
|
17
|
+
input = bevelInput("input", { rules: ["display:block", "width:100%", "margin:0"], theme, parts: ["dialog.input"] });
|
|
16
18
|
input.setAttribute("value", String(defaultValue));
|
|
17
19
|
input.required = true;
|
|
18
20
|
content.append(input);
|
|
@@ -22,6 +24,7 @@ function createModal(promptText, yesCallback, extraContent = "", includeInput =
|
|
|
22
24
|
themodal.title = promptText;
|
|
23
25
|
themodal.html = includeInput || extraContent ? content : "";
|
|
24
26
|
themodal.width = "440px";
|
|
27
|
+
themodal[SURFACE_THEME] = theme;
|
|
25
28
|
themodal.closeHtml = "x";
|
|
26
29
|
themodal.no = "Cancel";
|
|
27
30
|
themodal.yes = yesLabel;
|
|
@@ -95,9 +98,10 @@ export function consent(promptText, yesCallback, extraContent = "") {
|
|
|
95
98
|
* @returns {Promise} Resolves when user confirms, rejects on close
|
|
96
99
|
*/
|
|
97
100
|
export function tell(promptText, ...content) {
|
|
98
|
-
const
|
|
101
|
+
const theme = resolveTheme();
|
|
102
|
+
const box = bevelBox("div", ["display:flex", "flex-direction:column", "gap:12px"], { theme, parts: ["dialog.content"] });
|
|
99
103
|
for (const c of content) {
|
|
100
|
-
const paragraph = bevelText("div", [`font:14.5px/1.55 ${FONT_SANS}`, `color:${TOKENS["ink-2"]}`, "overflow-wrap:anywhere"]);
|
|
104
|
+
const paragraph = bevelText("div", [`font:14.5px/1.55 ${FONT_SANS}`, `color:${TOKENS["ink-2"]}`, "overflow-wrap:anywhere"], undefined, { theme });
|
|
101
105
|
paragraph.innerHTML = c;
|
|
102
106
|
box.append(paragraph);
|
|
103
107
|
}
|
|
@@ -105,6 +109,7 @@ export function tell(promptText, ...content) {
|
|
|
105
109
|
themodal.title = promptText;
|
|
106
110
|
themodal.html = content.length ? box : "";
|
|
107
111
|
themodal.width = "470px";
|
|
112
|
+
themodal[SURFACE_THEME] = theme;
|
|
108
113
|
themodal.closeHtml = "x";
|
|
109
114
|
themodal.yes = "OK";
|
|
110
115
|
|
|
@@ -136,17 +141,19 @@ export function tell(promptText, ...content) {
|
|
|
136
141
|
* Callers style their own container.
|
|
137
142
|
*/
|
|
138
143
|
export function snippet(title, content, extraContent = '') {
|
|
139
|
-
const
|
|
144
|
+
const theme = resolveTheme();
|
|
145
|
+
const box = bevelBox("div", ["display:block"], { theme, parts: ["dialog.content"] });
|
|
140
146
|
const well = bevelBox("div", [
|
|
141
147
|
"display:block", "max-width:100%", "overflow-x:auto", "margin:0 0 14px", "padding:14px 16px",
|
|
142
148
|
`background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`,
|
|
143
|
-
]);
|
|
144
|
-
const pre = bevelText("pre", ["display:block", "margin:0", "white-space:nowrap", `font:13px/1.6 ${FONT_MONO}`]);
|
|
149
|
+
], { theme, parts: ["dialog.well"], role: "well" });
|
|
150
|
+
const pre = bevelText("pre", ["display:block", "margin:0", "white-space:nowrap", `font:13px/1.6 ${FONT_MONO}`], undefined, { theme, parts: ["dialog.code"], role: "code" });
|
|
145
151
|
pre.innerHTML = content;
|
|
146
152
|
well.append(pre);
|
|
147
153
|
const copy = bevelButton("Copy", {
|
|
148
154
|
small: true,
|
|
149
155
|
extra: ["margin:0 0 14px"],
|
|
156
|
+
theme, parts: ["dialog.button", "dialog.copyButton"], labelParts: ["dialog.buttonLabel"],
|
|
150
157
|
onClick: () => {
|
|
151
158
|
copyToClipboard(content);
|
|
152
159
|
toast('Copied to clipboard!', 'success');
|
|
@@ -158,6 +165,7 @@ export function snippet(title, content, extraContent = '') {
|
|
|
158
165
|
themodal.title = title;
|
|
159
166
|
themodal.html = box;
|
|
160
167
|
themodal.width = "540px";
|
|
168
|
+
themodal[SURFACE_THEME] = theme;
|
|
161
169
|
themodal.closeHtml = "x";
|
|
162
170
|
themodal.yes = '';
|
|
163
171
|
|
package/src/ui/index.js
CHANGED
|
@@ -2,9 +2,12 @@ import toast, { toastPersistent, dismissPersistent } from "./toast.js";
|
|
|
2
2
|
import themodal from "./modal.js";
|
|
3
3
|
import { ask, consent, tell, snippet } from "./dialogs.js";
|
|
4
4
|
import { saveFeedback } from "../core/save-feedback.js";
|
|
5
|
+
import { initTheme } from "./theme.js";
|
|
5
6
|
|
|
6
7
|
// Attach the public surface explicitly (§2.3). We do NOT rely on toast.js /
|
|
7
8
|
// dialogs.js evaluation side effects — their window auto-exports are stripped.
|
|
9
|
+
initTheme();
|
|
10
|
+
|
|
8
11
|
const clay = (window.clay = window.clay || {});
|
|
9
12
|
clay.toast = toast;
|
|
10
13
|
clay.toastPersistent = toastPersistent;
|
package/src/ui/modal.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { bevelButton, setShown } from "./bevel-controls.js";
|
|
2
2
|
import { set } from "../lib/hostile-css.js";
|
|
3
3
|
import { bevelDialog, dismissOf } from "./bevel-dialog.js";
|
|
4
|
+
import { resolveTheme } from "./theme.js";
|
|
5
|
+
|
|
6
|
+
// The theme one open() draws with. Internal: only dialogs.js sets it, so no page can
|
|
7
|
+
// leave a value behind that makes the next open() throw.
|
|
8
|
+
export const SURFACE_THEME = Symbol("clay.surfaceTheme");
|
|
4
9
|
|
|
5
10
|
/*
|
|
6
11
|
|
|
@@ -299,6 +304,7 @@ const themodal = (() => {
|
|
|
299
304
|
let closeHtml = "";
|
|
300
305
|
let title = "";
|
|
301
306
|
let width = "";
|
|
307
|
+
let theme;
|
|
302
308
|
|
|
303
309
|
let enableClickOutsideCloses = true;
|
|
304
310
|
let disableScroll = true;
|
|
@@ -347,15 +353,17 @@ const themodal = (() => {
|
|
|
347
353
|
// Expose this modal's dismiss so a later open()/close can settle it.
|
|
348
354
|
this._dismiss = dismiss;
|
|
349
355
|
|
|
350
|
-
const
|
|
356
|
+
const surfaceTheme = theme === undefined ? resolveTheme() : theme;
|
|
357
|
+
const shell = bevelDialog({ zIndex, closable: !!closeHtml, titled: !!title, ...(width ? { width } : {}), theme: surfaceTheme });
|
|
351
358
|
const modalRootElem = shell.root;
|
|
352
359
|
const modalOverlayElem = shell.overlay;
|
|
353
360
|
const modalContainerElem = shell.panel;
|
|
354
361
|
const modalContentElem = shell.body;
|
|
355
362
|
const modalButtonsElem = shell.footer;
|
|
356
363
|
const modalCloseElem = shell.close;
|
|
357
|
-
const
|
|
358
|
-
const
|
|
364
|
+
const buttonParts = { theme: surfaceTheme, parts: ['dialog.button'], labelParts: ['dialog.buttonLabel'] };
|
|
365
|
+
const modalNoElem = bevelButton('', buttonParts);
|
|
366
|
+
const modalYesElem = bevelButton('', { variant: 'primary', ...buttonParts });
|
|
359
367
|
modalYesElem.type = 'submit';
|
|
360
368
|
modalButtonsElem.append(modalNoElem, modalYesElem);
|
|
361
369
|
|
|
@@ -468,6 +476,7 @@ const themodal = (() => {
|
|
|
468
476
|
closeHtml = "";
|
|
469
477
|
title = "";
|
|
470
478
|
width = "";
|
|
479
|
+
theme = undefined;
|
|
471
480
|
|
|
472
481
|
// reset to defaults
|
|
473
482
|
enableClickOutsideCloses = true;
|
|
@@ -528,6 +537,12 @@ const themodal = (() => {
|
|
|
528
537
|
set width(newVal) {
|
|
529
538
|
width = newVal;
|
|
530
539
|
},
|
|
540
|
+
get [SURFACE_THEME]() {
|
|
541
|
+
return theme;
|
|
542
|
+
},
|
|
543
|
+
set [SURFACE_THEME](newVal) {
|
|
544
|
+
theme = newVal;
|
|
545
|
+
},
|
|
531
546
|
get yes() {
|
|
532
547
|
return yes;
|
|
533
548
|
},
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Theme declarations for the controls ClayJS draws. tokenRules() maps a theme's tokens
|
|
2
|
+
// onto one semantic role in one state; partRules() applies a page's explicit part
|
|
3
|
+
// overrides, general to specific. Both return "prop:value" strings for hostile-css,
|
|
4
|
+
// and both return nothing for no theme or the exact Bevel theme.
|
|
5
|
+
|
|
6
|
+
export const themed = (theme) => !!theme && !theme.bevel;
|
|
7
|
+
|
|
8
|
+
const mix = (a, b, pct) => `color-mix(in srgb, ${a}, ${b} ${pct}%)`;
|
|
9
|
+
const decl = (prop, value) => (value ? [`${prop}:${value}`] : []);
|
|
10
|
+
|
|
11
|
+
// States in the order they apply. Disabled suppresses hover and pressed.
|
|
12
|
+
export function stateList({ hover = false, active = false, focus = false, disabled = false } = {}) {
|
|
13
|
+
const list = ["base"];
|
|
14
|
+
if (!disabled && hover) list.push("hover");
|
|
15
|
+
if (!disabled && active) list.push("active");
|
|
16
|
+
if (focus) list.push("focus");
|
|
17
|
+
if (disabled) list.push("disabled");
|
|
18
|
+
return list;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Hover and press on neutral controls: a page's own background colour when it gave one
|
|
22
|
+
// that differs from the surface, else the surface tinted toward the text (or the
|
|
23
|
+
// control's own colour when the theme has no text).
|
|
24
|
+
function neutral(t, pct) {
|
|
25
|
+
if (t.background && t.background !== t.surface) return pct > 8 ? mix(t.background, t.text || "currentColor", 6) : t.background;
|
|
26
|
+
if (t.surface) return mix(t.surface, t.text || "currentColor", pct);
|
|
27
|
+
return t.background;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function button(t, variant, state) {
|
|
31
|
+
const r = [];
|
|
32
|
+
const fill = { primary: ["accent", "accentText"], danger: ["danger", "dangerText"] }[variant];
|
|
33
|
+
if (state === "base") r.push(...decl("font-family", t.font), ...decl("border-radius", t.radius));
|
|
34
|
+
if (state === "base" && t.buttonHeight) r.push("height:auto", "block-size:auto", `min-height:${t.buttonHeight}`);
|
|
35
|
+
if (fill) {
|
|
36
|
+
const bg = t[fill[0]];
|
|
37
|
+
const fg = t[fill[1]];
|
|
38
|
+
if (state === "base") r.push(...decl("background", bg), ...decl("border-color", bg), ...decl("color", fg));
|
|
39
|
+
if (state === "hover" && bg && fg) r.push(`background:${mix(bg, fg, 12)}`);
|
|
40
|
+
if (state === "active" && bg) r.push(`border-color:${bg}`, ...(fg ? [`background:${mix(bg, fg, 20)}`] : []));
|
|
41
|
+
if (state === "disabled" && bg) r.push("opacity:.55");
|
|
42
|
+
} else if (variant === "quiet") {
|
|
43
|
+
if (state === "base") r.push(...decl("color", t.text));
|
|
44
|
+
if (state === "hover") r.push(...decl("background", neutral(t, 7)));
|
|
45
|
+
if (state === "active") r.push(...decl("background", neutral(t, 12)));
|
|
46
|
+
if (state === "disabled") r.push(...decl("color", t.mutedText));
|
|
47
|
+
} else {
|
|
48
|
+
if (state === "base") r.push(...decl("background", t.surface), ...decl("color", t.text), ...decl("border-color", t.border));
|
|
49
|
+
if (state === "hover") r.push(...decl("background", neutral(t, 7)));
|
|
50
|
+
if (state === "active") r.push(...decl("background", neutral(t, 12)), ...decl("border-color", t.border));
|
|
51
|
+
if (state === "disabled") r.push(...decl("color", t.mutedText));
|
|
52
|
+
}
|
|
53
|
+
if (state === "focus" && t.accent) r.push(`outline:2px solid ${t.accent}`);
|
|
54
|
+
return r;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ROLES = {
|
|
58
|
+
input: (t, s) =>
|
|
59
|
+
s === "base" ? [...decl("font-family", t.font), ...decl("color", t.text), ...decl("background", t.surface), ...decl("border-color", t.border), ...decl("border-radius", t.radius)]
|
|
60
|
+
: s === "hover" ? decl("border-color", t.mutedText)
|
|
61
|
+
: s === "focus" && t.accent ? [`outline:2px solid ${t.accent}`, `border-color:${t.accent}`]
|
|
62
|
+
: [],
|
|
63
|
+
surface: (t, s) =>
|
|
64
|
+
s === "base" ? [...decl("background", t.surface), ...decl("color", t.text), ...decl("border-color", t.border), ...decl("box-shadow", t.shadow), ...decl("border-radius", t.radius), ...decl("font-family", t.font)] : [],
|
|
65
|
+
text: (t, s) => (s === "base" ? [...decl("color", t.text), ...decl("font-family", t.font)] : []),
|
|
66
|
+
mutedText: (t, s) => (s === "base" ? [...decl("color", t.mutedText), ...decl("font-family", t.font)] : []),
|
|
67
|
+
heading: (t, s) => (s === "base" ? [...decl("font-family", t.headingFont || t.font), ...decl("color", t.text)] : []),
|
|
68
|
+
code: (t, s) => (s === "base" ? decl("color", t.text) : []),
|
|
69
|
+
well: (t, s) =>
|
|
70
|
+
s === "base" ? [...decl("background", t.background), ...(t.border ? [`border:1px solid ${t.border}`] : []), ...decl("border-radius", t.radius)] : [],
|
|
71
|
+
overlay: (t, s) => (s === "base" ? decl("background", t.overlay) : []),
|
|
72
|
+
close: (t, s) =>
|
|
73
|
+
s === "base" ? [...decl("background", t.surface), ...decl("color", t.mutedText), ...decl("border-color", t.border)]
|
|
74
|
+
: s === "hover" ? decl("background", neutral(t, 7))
|
|
75
|
+
: s === "focus" && t.accent ? [`outline:2px solid ${t.accent}`] : [],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Token declarations for `role` ("button.primary", "input", "surface", ...) in `state`. */
|
|
79
|
+
export function tokenRules(theme, role, state = "base") {
|
|
80
|
+
if (!themed(theme)) return [];
|
|
81
|
+
if (role.startsWith("button.")) return button(theme.tokens, role.slice(7), state);
|
|
82
|
+
return ROLES[role] ? ROLES[role](theme.tokens, state) : [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Token declarations for every applicable state, in order. */
|
|
86
|
+
export function tokenRulesFor(theme, role, active) {
|
|
87
|
+
return stateList(active).flatMap((s) => tokenRules(theme, role, s));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A page's explicit part declarations: each name general to specific, base then states. */
|
|
91
|
+
export function partRules(theme, names, active) {
|
|
92
|
+
if (!themed(theme) || !names?.length) return [];
|
|
93
|
+
const states = stateList(active);
|
|
94
|
+
const r = [];
|
|
95
|
+
for (const name of names) {
|
|
96
|
+
const part = theme.parts[name];
|
|
97
|
+
if (!part) continue;
|
|
98
|
+
for (const s of states) for (const [prop, value] of Object.entries(part[s] || {})) r.push(`${prop}:${value}`);
|
|
99
|
+
}
|
|
100
|
+
return r;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Mark a themed element with its part names, so the contract can be inspected. */
|
|
104
|
+
export function markParts(el, theme, names) {
|
|
105
|
+
if (themed(theme) && names?.length) el.setAttribute("data-clay-part", names.join(" "));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A floor goes on after the theme: first the plain floor, then the larger of the floor
|
|
109
|
+
// and what the theme asked for. When the theme's value cannot go inside max() (a keyword
|
|
110
|
+
// such as auto), the browser drops that second declaration and the plain floor stands.
|
|
111
|
+
export function floorRules(rules, prop, px) {
|
|
112
|
+
if (!px) return [];
|
|
113
|
+
const last = [...rules].reverse().find((r) => r.startsWith(`${prop}:`));
|
|
114
|
+
const value = last ? last.slice(prop.length + 1).trim() : "";
|
|
115
|
+
const out = [`${prop}:${px}px`];
|
|
116
|
+
if (value && value !== `${px}px`) out.push(`${prop}:max(${px}px, ${value})`);
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function heightFloor(rules, narrow) {
|
|
121
|
+
return narrow ? floorRules(rules, "min-height", 44) : [];
|
|
122
|
+
}
|