@panphora/clayjs 1.5.3 → 1.6.1
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/README.md +5 -3
- package/THIRD-PARTY-NOTICES.md +6 -0
- package/dist/clay.standalone.js +15041 -10657
- package/package.json +9 -4
- package/src/core/conflict-download.js +91 -0
- package/src/core/conflict-notice.js +720 -0
- package/src/core/conflict-presentation.js +301 -0
- package/src/core/save-conflict-notice.js +2 -192
- package/src/core/save-core.js +35 -2
- package/src/core/save-feedback.js +6 -0
- package/src/core/save.js +157 -3
- package/src/core/snapshot.js +21 -0
- package/src/core/stale-host-notice.js +21 -24
- package/src/core/unsaved-warning.js +64 -16
- package/src/lib/content-dom.js +4 -2
- package/src/lib/dirty-gate.js +37 -1
- package/src/lib/unsaved-state.js +55 -0
- package/src/loader-logic.js +26 -15
- package/src/loader.js +10 -3
- package/src/plugins/ai-edit.js +489 -179
- package/src/plugins/cms.js +12 -0
- package/src/plugins/indicator.js +35 -16
- package/src/plugins/quickcrop.js +103 -0
- package/src/plugins/richclay.js +21 -0
- package/src/plugins/source.js +13 -6
- package/src/plugins/wire.js +4 -0
- package/src/sync/conflict-revert.js +1138 -0
- package/src/sync/conflicts.js +159 -0
- package/src/sync/live-sync.js +130 -109
- package/src/sync/presence.js +42 -36
- package/src/sync/section-notice.js +53 -28
- package/src/ui/bevel-controls.js +277 -0
- package/src/ui/bevel-dialog.js +112 -0
- package/src/ui/bevel.js +247 -0
- package/src/ui/dialogs.js +63 -86
- package/src/ui/index.js +22 -11
- package/src/ui/modal.js +80 -387
- package/src/ui/skins/cms-toggle.js +15 -0
- package/src/ui/skins/cms.js +4 -0
- package/src/ui/skins/quickcrop.js +4 -0
- package/src/ui/skins/richclay.js +4 -0
- package/src/ui/toast.js +116 -401
- package/src/ui/vendor-skin.js +65 -0
- package/src/vendor/hyper-morph.vendor.js +4 -4
- package/src/vendor/quickcrop.vendor.js +14 -8
- package/src/vendor/richclay.vendor.js +22 -41
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// The frame every ClayJS dialog draws: a backdrop, a Bevel panel, an optional corner
|
|
2
|
+
// close. It holds no behaviour. The modal and the crop adapter own focus, Escape,
|
|
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";
|
|
6
|
+
import { TOKENS, FONT_SANS, RULES } from "./bevel.js";
|
|
7
|
+
|
|
8
|
+
// A frame's own way to settle itself. The modal is a singleton and clears every frame
|
|
9
|
+
// when it opens; a frame it does not own (the crop dialog) registers here, so it is
|
|
10
|
+
// cancelled properly instead of vanishing with its caller still waiting.
|
|
11
|
+
export const dismissOf = new WeakMap();
|
|
12
|
+
|
|
13
|
+
const FOCUSABLE = 'a[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [contenteditable], [tabindex]:not([tabindex^="-"])';
|
|
14
|
+
|
|
15
|
+
// Tab and Shift+Tab wrap inside `container`, the rule MicroModal applies to the modal.
|
|
16
|
+
export function keepFocusIn(container, event) {
|
|
17
|
+
const nodes = [...container.querySelectorAll(FOCUSABLE)].filter((node) => node.offsetParent !== null);
|
|
18
|
+
if (!nodes.length) return;
|
|
19
|
+
const at = nodes.indexOf(document.activeElement);
|
|
20
|
+
if (at === -1) {
|
|
21
|
+
nodes[0].focus();
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
} else if (event.shiftKey && at === 0) {
|
|
24
|
+
nodes[nodes.length - 1].focus();
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
} else if (!event.shiftKey && at === nodes.length - 1) {
|
|
27
|
+
nodes[0].focus();
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The dashboard's dialog (Bevel's React dialog with the dashboard's overrides), drawn
|
|
33
|
+
// inline. ClayJS loads no webfonts, so the title's Newsreader falls back to Georgia.
|
|
34
|
+
const LINE = `1px solid ${TOKENS["line-2"]}`;
|
|
35
|
+
const CLOSE_X =
|
|
36
|
+
'<svg viewBox="0 0 256 256" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></svg>';
|
|
37
|
+
|
|
38
|
+
// The close: a column at the header's right edge in the button face, or a 62px square
|
|
39
|
+
// at the panel's corner when there is no header.
|
|
40
|
+
function dialogClose(label, inHeader) {
|
|
41
|
+
const hover = RULES.dialogCloseHover[0].slice("background:".length);
|
|
42
|
+
const b = bevelBox("button", [
|
|
43
|
+
"position:absolute", "top:-1px", "right:-1px", "z-index:2",
|
|
44
|
+
"display:grid", "place-items:center", inHeader ? "height:calc(100% + 1px)" : "height:62px",
|
|
45
|
+
"margin:0", "padding:0", "border:0", ...RULES.dialogClose, ...(inHeader ? [] : [`border-bottom:${LINE}`]),
|
|
46
|
+
"cursor:pointer", "outline:none",
|
|
47
|
+
]);
|
|
48
|
+
b.type = "button";
|
|
49
|
+
b.setAttribute("aria-label", label);
|
|
50
|
+
b.title = label;
|
|
51
|
+
b.innerHTML = CLOSE_X;
|
|
52
|
+
protectIcon(b.firstElementChild, 16);
|
|
53
|
+
b.addEventListener("pointerenter", () => set(b, "background", hover));
|
|
54
|
+
b.addEventListener("pointerleave", () => set(b, "background", TOKENS.face));
|
|
55
|
+
b.addEventListener("focus", () => { set(b, "outline", `2px solid ${TOKENS.brass}`); set(b, "outline-offset", "-4px"); });
|
|
56
|
+
b.addEventListener("blur", () => set(b, "outline", "none"));
|
|
57
|
+
return b;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function bevelDialog({ zIndex = "100", width = "600px", closable = false, titled = false } = {}) {
|
|
61
|
+
const root = bevelBox("div", ["display:block"]);
|
|
62
|
+
root.setAttribute("data-clay-modal", "");
|
|
63
|
+
root.setAttribute("aria-hidden", "true");
|
|
64
|
+
set(root, "color-scheme", pageScheme());
|
|
65
|
+
|
|
66
|
+
const overlay = bevelBox("div", [
|
|
67
|
+
"position:fixed", "inset:0", `z-index:${zIndex}`, "overflow:auto",
|
|
68
|
+
"display:flex", "align-items:flex-start", "justify-content:center",
|
|
69
|
+
"padding:min(96px, 10vh) 16px 16px",
|
|
70
|
+
`background:color-mix(in srgb, ${TOKENS.ground} 72%, transparent)`,
|
|
71
|
+
]);
|
|
72
|
+
overlay.tabIndex = -1;
|
|
73
|
+
|
|
74
|
+
const panel = bevelSurface("form", [
|
|
75
|
+
"position:relative", "display:flex", "flex-direction:column", "margin:0",
|
|
76
|
+
`width:min(${width}, 100%)`, "max-height:calc(100dvh - min(96px, 10vh) - 16px)",
|
|
77
|
+
...RULES.dialogPanel,
|
|
78
|
+
]);
|
|
79
|
+
panel.setAttribute("role", "dialog");
|
|
80
|
+
panel.setAttribute("aria-modal", "true");
|
|
81
|
+
|
|
82
|
+
let header = null;
|
|
83
|
+
let heading = null;
|
|
84
|
+
if (titled) {
|
|
85
|
+
header = bevelBox("div", [
|
|
86
|
+
"position:relative", "display:block", "flex:none",
|
|
87
|
+
`padding:20px ${closable ? "68px" : "22px"} 16px 22px`, ...RULES.dialogHeader,
|
|
88
|
+
]);
|
|
89
|
+
heading = bevelText("div", ["display:block", ...RULES.dialogHeading, "overflow-wrap:anywhere"]);
|
|
90
|
+
heading.setAttribute("role", "heading");
|
|
91
|
+
heading.setAttribute("aria-level", "2");
|
|
92
|
+
header.append(heading);
|
|
93
|
+
panel.append(header);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const body = bevelText("div", [
|
|
97
|
+
"display:block", "flex:1 1 auto", "min-height:0", "overflow-y:auto", "overflow-x:hidden",
|
|
98
|
+
`padding:20px ${closable && !titled ? "84px" : "22px"} 20px 22px`,
|
|
99
|
+
"overflow-wrap:anywhere", `font:14.5px/1.55 ${FONT_SANS}`,
|
|
100
|
+
]);
|
|
101
|
+
const footer = bevelBox("div", [
|
|
102
|
+
"display:flex", "flex:none", "flex-wrap:wrap", "justify-content:flex-end", "align-items:center", ...RULES.dialogFooter,
|
|
103
|
+
]);
|
|
104
|
+
panel.append(body, footer);
|
|
105
|
+
|
|
106
|
+
const close = closable ? dialogClose("Close", titled) : null;
|
|
107
|
+
if (close) (header || panel).append(close);
|
|
108
|
+
|
|
109
|
+
overlay.append(panel);
|
|
110
|
+
root.append(overlay);
|
|
111
|
+
return { root, overlay, panel, header, heading, body, footer, close };
|
|
112
|
+
}
|
package/src/ui/bevel.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// GENERATED from bevel/bevel.css by scripts/build-bevel-subset.mjs. Run `npm run build:bevel`.
|
|
2
|
+
export const SOURCE_SHA256 = "4532d98386c76070626c0cccea91f512dafd0314c0c846dbf1eea9372ddde541";
|
|
3
|
+
export const FONT_SANS = "system-ui,-apple-system,\"Segoe UI\",sans-serif";
|
|
4
|
+
export const FONT_MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
5
|
+
export const SHADOW = "0 1px 2px rgba(0,0,0,.12),0 10px 28px -12px rgba(0,0,0,.35)";
|
|
6
|
+
export const TOKENS = {
|
|
7
|
+
"ground": "light-dark(#FDF8F0, #0B0C13)",
|
|
8
|
+
"surface": "light-dark(#FFFCF6, #11131E)",
|
|
9
|
+
"sunk": "light-dark(#F4ECDF, #1A1D2C)",
|
|
10
|
+
"ink": "light-dark(#2B241B, #ECEAF2)",
|
|
11
|
+
"ink-2": "light-dark(#4A4033, #C9CDE0)",
|
|
12
|
+
"muted": "light-dark(#6B5942, #8A90AB)",
|
|
13
|
+
"faint": "light-dark(#9A8A70, #6A7090)",
|
|
14
|
+
"line": "light-dark(#E8DCC6, #262B42)",
|
|
15
|
+
"line-2": "light-dark(#D8C8AF, #353B52)",
|
|
16
|
+
"line-3": "light-dark(#C4B192, #46506E)",
|
|
17
|
+
"brass": "light-dark(#8F6310, #D9A445)",
|
|
18
|
+
"brass-soft": "light-dark(#F0E4CC, #3A2F17)",
|
|
19
|
+
"ox": "light-dark(#A83A2C, #FF5566)",
|
|
20
|
+
"ox-soft": "light-dark(#F6DED6, #3A2226)",
|
|
21
|
+
"teal": "light-dark(#256056, #63AE9E)",
|
|
22
|
+
"teal-soft": "light-dark(#DCEBE7, #17332E)",
|
|
23
|
+
"face": "light-dark(#F1E7D4, #22273E)",
|
|
24
|
+
"edge-hi": "light-dark(#FFF9ED, #3C4260)",
|
|
25
|
+
"edge-lo": "light-dark(#D6C3A5, #1B2033)",
|
|
26
|
+
"face-in": "light-dark(#E3D0B3, #28304D)",
|
|
27
|
+
"edge-in": "light-dark(#B89F7B, #1F2339)",
|
|
28
|
+
"edge-in-hi": "light-dark(#F0E1C9, #4D587A)",
|
|
29
|
+
"knob": "light-dark(#FCF8F1, #4A5275)",
|
|
30
|
+
"knob-hi": "light-dark(#FFF9ED, #6C759A)",
|
|
31
|
+
"ox-face": "light-dark(#7A2A1F, #842E22)",
|
|
32
|
+
"ox-edge-hi": "light-dark(#AE4F3E, #B85A47)",
|
|
33
|
+
"ox-edge-lo": "light-dark(#451510, #3E1410)",
|
|
34
|
+
"ox-ink": "light-dark(#FBEEE8, #FBEEE8)",
|
|
35
|
+
};
|
|
36
|
+
export const RULES = {
|
|
37
|
+
"button": [
|
|
38
|
+
"display:inline-flex",
|
|
39
|
+
"align-items:center",
|
|
40
|
+
"justify-content:center",
|
|
41
|
+
"gap:7px",
|
|
42
|
+
"font-family:system-ui,-apple-system,\"Segoe UI\",sans-serif",
|
|
43
|
+
"font-size:13.5px",
|
|
44
|
+
"font-weight:500",
|
|
45
|
+
"line-height:1.5",
|
|
46
|
+
"cursor:pointer",
|
|
47
|
+
"user-select:none",
|
|
48
|
+
"text-align:center",
|
|
49
|
+
"text-decoration:none",
|
|
50
|
+
"color:light-dark(#2B241B, #ECEAF2)",
|
|
51
|
+
"background:light-dark(#F1E7D4, #22273E)",
|
|
52
|
+
"border:2px solid",
|
|
53
|
+
"border-color:light-dark(#FFF9ED, #3C4260) light-dark(#D6C3A5, #1B2033) light-dark(#D6C3A5, #1B2033) light-dark(#FFF9ED, #3C4260)",
|
|
54
|
+
"border-radius:0",
|
|
55
|
+
"padding:0 12px",
|
|
56
|
+
"height:32px",
|
|
57
|
+
"outline:none",
|
|
58
|
+
],
|
|
59
|
+
"buttonLabel": [
|
|
60
|
+
"white-space:nowrap",
|
|
61
|
+
"user-select:none",
|
|
62
|
+
"display:inline-block",
|
|
63
|
+
],
|
|
64
|
+
"buttonHover": [
|
|
65
|
+
"background:color-mix(in srgb, light-dark(#F1E7D4, #22273E), light-dark(#FFF9ED, #3C4260) 45%)",
|
|
66
|
+
],
|
|
67
|
+
"buttonActive": [
|
|
68
|
+
"border-color:light-dark(#D6C3A5, #1B2033) light-dark(#FFF9ED, #3C4260) light-dark(#FFF9ED, #3C4260) light-dark(#D6C3A5, #1B2033)",
|
|
69
|
+
],
|
|
70
|
+
"buttonActiveLabel": [
|
|
71
|
+
"translate:1px 1px",
|
|
72
|
+
],
|
|
73
|
+
"buttonDisabled": [
|
|
74
|
+
"color:light-dark(#9A8A70, #6A7090)",
|
|
75
|
+
"cursor:default",
|
|
76
|
+
"opacity:1",
|
|
77
|
+
],
|
|
78
|
+
"buttonDisabledLabel": [
|
|
79
|
+
"translate:none",
|
|
80
|
+
],
|
|
81
|
+
"buttonSmall": [
|
|
82
|
+
"height:26px",
|
|
83
|
+
"font-size:12.5px",
|
|
84
|
+
"padding:0 9px",
|
|
85
|
+
],
|
|
86
|
+
"buttonPrimary": [
|
|
87
|
+
"display:inline-flex",
|
|
88
|
+
"align-items:center",
|
|
89
|
+
"justify-content:center",
|
|
90
|
+
"gap:7px",
|
|
91
|
+
"font-family:system-ui,-apple-system,\"Segoe UI\",sans-serif",
|
|
92
|
+
"font-size:13.5px",
|
|
93
|
+
"font-weight:500",
|
|
94
|
+
"line-height:1.5",
|
|
95
|
+
"cursor:pointer",
|
|
96
|
+
"user-select:none",
|
|
97
|
+
"text-align:center",
|
|
98
|
+
"text-decoration:none",
|
|
99
|
+
"color:light-dark(#2B241B, #ECEAF2)",
|
|
100
|
+
"background:light-dark(#2B241B, #ECEAF2)",
|
|
101
|
+
"border:2px solid",
|
|
102
|
+
"border-color:color-mix(in srgb, light-dark(#2B241B, #ECEAF2), white 24%) color-mix(in srgb, light-dark(#2B241B, #ECEAF2), black 32%) color-mix(in srgb, light-dark(#2B241B, #ECEAF2), black 32%) color-mix(in srgb, light-dark(#2B241B, #ECEAF2), white 24%)",
|
|
103
|
+
"border-radius:0",
|
|
104
|
+
"padding:0 12px",
|
|
105
|
+
"height:32px",
|
|
106
|
+
"outline:none",
|
|
107
|
+
"color:light-dark(#FDF8F0, #0B0C13)",
|
|
108
|
+
],
|
|
109
|
+
"buttonPrimaryHover": [
|
|
110
|
+
"background:color-mix(in srgb, light-dark(#2B241B, #ECEAF2), light-dark(#FDF8F0, #0B0C13) 12%)",
|
|
111
|
+
],
|
|
112
|
+
"buttonPrimaryHoverBase": [
|
|
113
|
+
"background:color-mix(in srgb, light-dark(#2B241B, #ECEAF2), color-mix(in srgb, light-dark(#2B241B, #ECEAF2), white 24%) 45%)",
|
|
114
|
+
],
|
|
115
|
+
"buttonPrimaryActive": [
|
|
116
|
+
"border-color:color-mix(in srgb, light-dark(#2B241B, #ECEAF2), black 32%) color-mix(in srgb, light-dark(#2B241B, #ECEAF2), white 24%) color-mix(in srgb, light-dark(#2B241B, #ECEAF2), white 24%) color-mix(in srgb, light-dark(#2B241B, #ECEAF2), black 32%)",
|
|
117
|
+
],
|
|
118
|
+
"buttonDanger": [
|
|
119
|
+
"display:inline-flex",
|
|
120
|
+
"align-items:center",
|
|
121
|
+
"justify-content:center",
|
|
122
|
+
"gap:7px",
|
|
123
|
+
"font-family:system-ui,-apple-system,\"Segoe UI\",sans-serif",
|
|
124
|
+
"font-size:13.5px",
|
|
125
|
+
"font-weight:500",
|
|
126
|
+
"line-height:1.5",
|
|
127
|
+
"cursor:pointer",
|
|
128
|
+
"user-select:none",
|
|
129
|
+
"text-align:center",
|
|
130
|
+
"text-decoration:none",
|
|
131
|
+
"color:light-dark(#2B241B, #ECEAF2)",
|
|
132
|
+
"background:light-dark(#7A2A1F, #842E22)",
|
|
133
|
+
"border:2px solid",
|
|
134
|
+
"border-color:light-dark(#AE4F3E, #B85A47) light-dark(#451510, #3E1410) light-dark(#451510, #3E1410) light-dark(#AE4F3E, #B85A47)",
|
|
135
|
+
"border-radius:0",
|
|
136
|
+
"padding:0 12px",
|
|
137
|
+
"height:32px",
|
|
138
|
+
"outline:none",
|
|
139
|
+
"color:light-dark(#FBEEE8, #FBEEE8)",
|
|
140
|
+
],
|
|
141
|
+
"buttonDangerHover": [
|
|
142
|
+
"background:color-mix(in srgb, light-dark(#7A2A1F, #842E22), light-dark(#AE4F3E, #B85A47) 45%)",
|
|
143
|
+
],
|
|
144
|
+
"buttonDangerActive": [
|
|
145
|
+
"border-color:light-dark(#451510, #3E1410) light-dark(#AE4F3E, #B85A47) light-dark(#AE4F3E, #B85A47) light-dark(#451510, #3E1410)",
|
|
146
|
+
],
|
|
147
|
+
"buttonQuiet": [
|
|
148
|
+
"background:none",
|
|
149
|
+
"border-color:transparent",
|
|
150
|
+
"color:light-dark(#8F6310, #D9A445)",
|
|
151
|
+
],
|
|
152
|
+
"buttonQuietHover": [
|
|
153
|
+
"background:light-dark(#F4ECDF, #1A1D2C)",
|
|
154
|
+
"color:light-dark(#8F6310, #D9A445)",
|
|
155
|
+
],
|
|
156
|
+
"buttonQuietActive": [
|
|
157
|
+
"border-color:transparent",
|
|
158
|
+
],
|
|
159
|
+
"focus": [
|
|
160
|
+
"outline:2px solid light-dark(#8F6310, #D9A445)",
|
|
161
|
+
"outline-offset:2px",
|
|
162
|
+
],
|
|
163
|
+
"recess": [
|
|
164
|
+
"border:2px solid",
|
|
165
|
+
"border-color:light-dark(#B89F7B, #1F2339) light-dark(#F0E1C9, #4D587A) light-dark(#F0E1C9, #4D587A) light-dark(#B89F7B, #1F2339)",
|
|
166
|
+
"background:light-dark(#E3D0B3, #28304D)",
|
|
167
|
+
],
|
|
168
|
+
"input": [
|
|
169
|
+
"width:100%",
|
|
170
|
+
"display:block",
|
|
171
|
+
"font-family:system-ui,-apple-system,\"Segoe UI\",sans-serif",
|
|
172
|
+
"font-size:13.5px",
|
|
173
|
+
"font-weight:400",
|
|
174
|
+
"line-height:normal",
|
|
175
|
+
"color:light-dark(#2B241B, #ECEAF2)",
|
|
176
|
+
"background:light-dark(#FDF8F0, #0B0C13)",
|
|
177
|
+
"border:1px solid light-dark(#D8C8AF, #353B52)",
|
|
178
|
+
"border-radius:0",
|
|
179
|
+
"padding:0 10px",
|
|
180
|
+
"block-size:34px",
|
|
181
|
+
"outline:none",
|
|
182
|
+
"transition:border-color .14s, background-color .14s",
|
|
183
|
+
],
|
|
184
|
+
"inputHover": [
|
|
185
|
+
"border-color:light-dark(#9A8A70, #6A7090)",
|
|
186
|
+
],
|
|
187
|
+
"inputFocus": [
|
|
188
|
+
"outline:2px solid light-dark(#8F6310, #D9A445)",
|
|
189
|
+
"outline-offset:1px",
|
|
190
|
+
"border-color:light-dark(#8F6310, #D9A445)",
|
|
191
|
+
],
|
|
192
|
+
"toast": [
|
|
193
|
+
"gap:12px",
|
|
194
|
+
"padding:13px 12px 13px 15px",
|
|
195
|
+
],
|
|
196
|
+
"toastTitle": [
|
|
197
|
+
"font:600 13.5px/1.3 system-ui,-apple-system,\"Segoe UI\",sans-serif",
|
|
198
|
+
],
|
|
199
|
+
"toastClose": [
|
|
200
|
+
"margin:-2px -2px 0 0",
|
|
201
|
+
"inline-size:24px",
|
|
202
|
+
"block-size:24px",
|
|
203
|
+
"padding:0",
|
|
204
|
+
"border:0",
|
|
205
|
+
"background:transparent",
|
|
206
|
+
"color:light-dark(#6B5942, #8A90AB)",
|
|
207
|
+
],
|
|
208
|
+
"dialogPanel": [
|
|
209
|
+
"border-radius:0",
|
|
210
|
+
"box-shadow:6px 6px 0 color-mix(in srgb, light-dark(#2B241B, #ECEAF2) 10%, transparent)",
|
|
211
|
+
],
|
|
212
|
+
"dialogHeader": [
|
|
213
|
+
"border-bottom:1px solid light-dark(#D8C8AF, #353B52)",
|
|
214
|
+
],
|
|
215
|
+
"dialogHeading": [
|
|
216
|
+
"margin:0",
|
|
217
|
+
"font:400 24px/1.15 Georgia,\"Times New Roman\",serif",
|
|
218
|
+
],
|
|
219
|
+
"dialogFooter": [
|
|
220
|
+
"border-top:1px solid light-dark(#D8C8AF, #353B52)",
|
|
221
|
+
"padding:14px 22px",
|
|
222
|
+
"gap:10px",
|
|
223
|
+
],
|
|
224
|
+
"dialogClose": [
|
|
225
|
+
"inline-size:62px",
|
|
226
|
+
"border-left:1px solid light-dark(#D8C8AF, #353B52)",
|
|
227
|
+
"background:light-dark(#F1E7D4, #22273E)",
|
|
228
|
+
"color:light-dark(#2B241B, #ECEAF2)",
|
|
229
|
+
],
|
|
230
|
+
"dialogCloseHover": [
|
|
231
|
+
"background:color-mix(in srgb, light-dark(#F1E7D4, #22273E), light-dark(#FFF9ED, #3C4260) 45%)",
|
|
232
|
+
],
|
|
233
|
+
"surface": [
|
|
234
|
+
"background:light-dark(#FFFCF6, #11131E)",
|
|
235
|
+
"color:light-dark(#2B241B, #ECEAF2)",
|
|
236
|
+
"border:1px solid light-dark(#D8C8AF, #353B52)",
|
|
237
|
+
"box-shadow:0 1px 2px rgba(0,0,0,.12),0 10px 28px -12px rgba(0,0,0,.35)",
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
export const MEDIA = {
|
|
241
|
+
"reducedMotion": [
|
|
242
|
+
"translate:none",
|
|
243
|
+
],
|
|
244
|
+
"forcedColors": [
|
|
245
|
+
"border:1px solid ButtonText",
|
|
246
|
+
],
|
|
247
|
+
};
|
package/src/ui/dialogs.js
CHANGED
|
@@ -2,37 +2,35 @@ import themodal 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
themodal.html =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
themodal.closeHtml = CLOSE_BUTTON_SVG;
|
|
29
|
-
themodal.yes = CONFIRM_BUTTON_SVG;
|
|
5
|
+
import { bevelBox, bevelButton, bevelInput, bevelText } from "./bevel-controls.js";
|
|
6
|
+
import { TOKENS, FONT_SANS, FONT_MONO } from "./bevel.js";
|
|
7
|
+
|
|
8
|
+
// Caller markup (promptText, extraContent, tell's paragraphs) goes in as it is: those
|
|
9
|
+
// are markup by design, and callers pass elements through them. Only the frame around
|
|
10
|
+
// it is ClayJS's to style. The prompt is the dialog's title, as in the dashboard.
|
|
11
|
+
function createModal(promptText, yesCallback, extraContent = "", includeInput = false, defaultValue = "", yesLabel = "OK") {
|
|
12
|
+
const content = bevelBox("div", ["display:flex", "flex-direction:column", "gap:14px"]);
|
|
13
|
+
let input = null;
|
|
14
|
+
if (includeInput) {
|
|
15
|
+
input = bevelInput("input", { rules: ["display:block", "width:100%", "margin:0"] });
|
|
16
|
+
input.setAttribute("value", String(defaultValue));
|
|
17
|
+
input.required = true;
|
|
18
|
+
content.append(input);
|
|
19
|
+
}
|
|
20
|
+
content.insertAdjacentHTML("beforeend", extraContent);
|
|
21
|
+
|
|
22
|
+
themodal.title = promptText;
|
|
23
|
+
themodal.html = includeInput || extraContent ? content : "";
|
|
24
|
+
themodal.width = "440px";
|
|
25
|
+
themodal.closeHtml = "x";
|
|
26
|
+
themodal.no = "Cancel";
|
|
27
|
+
themodal.yes = yesLabel;
|
|
30
28
|
|
|
31
29
|
const promise = new Promise((resolve, reject) => {
|
|
32
30
|
themodal.onYes(() => {
|
|
33
31
|
let promptResult;
|
|
34
32
|
if (includeInput) {
|
|
35
|
-
promptResult =
|
|
33
|
+
promptResult = input.value;
|
|
36
34
|
if (!promptResult) return false; // keep modal open on empty input
|
|
37
35
|
}
|
|
38
36
|
// Run the validation callback synchronously so a throw can keep the
|
|
@@ -61,7 +59,7 @@ function createModal(promptText, yesCallback, extraContent = "", includeInput =
|
|
|
61
59
|
themodal.open();
|
|
62
60
|
|
|
63
61
|
setTimeout(() => {
|
|
64
|
-
const modalContainer = document.querySelector('
|
|
62
|
+
const modalContainer = document.querySelector('[data-clay-modal]');
|
|
65
63
|
if (modalContainer) {
|
|
66
64
|
modalContainer.addEventListener('click', (event) => {
|
|
67
65
|
const btn = event.target.closest('[data-copy]');
|
|
@@ -83,11 +81,11 @@ function createModal(promptText, yesCallback, extraContent = "", includeInput =
|
|
|
83
81
|
|
|
84
82
|
// Public API functions
|
|
85
83
|
export function ask(promptText, yesCallback, defaultValue = "", extraContent = "") {
|
|
86
|
-
return createModal(promptText, yesCallback, extraContent, true, defaultValue);
|
|
84
|
+
return createModal(promptText, yesCallback, extraContent, true, defaultValue, "OK");
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
export function consent(promptText, yesCallback, extraContent = "") {
|
|
90
|
-
return createModal(promptText, yesCallback, extraContent, false);
|
|
88
|
+
return createModal(promptText, yesCallback, extraContent, false, "", "Confirm");
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
/**
|
|
@@ -97,16 +95,18 @@ export function consent(promptText, yesCallback, extraContent = "") {
|
|
|
97
95
|
* @returns {Promise} Resolves when user confirms, rejects on close
|
|
98
96
|
*/
|
|
99
97
|
export function tell(promptText, ...content) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
: "";
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
themodal.
|
|
109
|
-
themodal.
|
|
98
|
+
const box = bevelBox("div", ["display:flex", "flex-direction:column", "gap:12px"]);
|
|
99
|
+
for (const c of content) {
|
|
100
|
+
const paragraph = bevelText("div", [`font:14.5px/1.55 ${FONT_SANS}`, `color:${TOKENS["ink-2"]}`, "overflow-wrap:anywhere"]);
|
|
101
|
+
paragraph.innerHTML = c;
|
|
102
|
+
box.append(paragraph);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
themodal.title = promptText;
|
|
106
|
+
themodal.html = content.length ? box : "";
|
|
107
|
+
themodal.width = "470px";
|
|
108
|
+
themodal.closeHtml = "x";
|
|
109
|
+
themodal.yes = "OK";
|
|
110
110
|
|
|
111
111
|
const promise = new Promise((resolve, reject) => {
|
|
112
112
|
themodal.onYes(() => {
|
|
@@ -133,64 +133,41 @@ export function tell(promptText, ...content) {
|
|
|
133
133
|
* @param {string} title - The modal heading
|
|
134
134
|
* @param {string} content - The code to display
|
|
135
135
|
* @param {string} extraContent - Optional raw HTML rendered below the copy button.
|
|
136
|
-
* Callers style their own container
|
|
137
|
-
* for the standard yellow-bordered warning box.
|
|
136
|
+
* Callers style their own container.
|
|
138
137
|
*/
|
|
139
138
|
export function snippet(title, content, extraContent = '') {
|
|
139
|
+
const box = bevelBox("div", ["display:block"]);
|
|
140
|
+
const well = bevelBox("div", [
|
|
141
|
+
"display:block", "max-width:100%", "overflow-x:auto", "margin:0 0 14px", "padding:14px 16px",
|
|
142
|
+
`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}`]);
|
|
145
|
+
pre.innerHTML = content;
|
|
146
|
+
well.append(pre);
|
|
147
|
+
const copy = bevelButton("Copy", {
|
|
148
|
+
small: true,
|
|
149
|
+
extra: ["margin:0 0 14px"],
|
|
150
|
+
onClick: () => {
|
|
151
|
+
copyToClipboard(content);
|
|
152
|
+
toast('Copied to clipboard!', 'success');
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
box.append(well, copy);
|
|
156
|
+
box.insertAdjacentHTML("beforeend", extraContent || "");
|
|
140
157
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
<button type="button" class="micromodal__secondary-btn copy-snippet-btn" style="margin-bottom: 14px;">copy</button>
|
|
148
|
-
|
|
149
|
-
${extraContent || ''}
|
|
150
|
-
`;
|
|
151
|
-
|
|
152
|
-
// Use the existing modal system
|
|
153
|
-
themodal.html = `<div>
|
|
154
|
-
<div class="micromodal__heading">${title}</div>
|
|
155
|
-
${modalContent}
|
|
156
|
-
</div>`;
|
|
157
|
-
|
|
158
|
-
themodal.closeHtml = CLOSE_BUTTON_SVG;
|
|
158
|
+
themodal.title = title;
|
|
159
|
+
themodal.html = box;
|
|
160
|
+
themodal.width = "540px";
|
|
161
|
+
themodal.closeHtml = "x";
|
|
159
162
|
themodal.yes = '';
|
|
160
163
|
|
|
161
164
|
const promise = new Promise((resolve) => {
|
|
162
|
-
// Local copy function
|
|
163
|
-
const handleCopy = function(event) {
|
|
164
|
-
if (event.target.closest('.copy-snippet-btn')) {
|
|
165
|
-
copyToClipboard(content);
|
|
166
|
-
toast('Copied to clipboard!', 'success');
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
// Add event listener to the modal container after it opens
|
|
171
|
-
setTimeout(() => {
|
|
172
|
-
const modalContainer = document.querySelector('.micromodal-parent');
|
|
173
|
-
if (modalContainer) {
|
|
174
|
-
modalContainer.addEventListener('click', handleCopy);
|
|
175
|
-
}
|
|
176
|
-
}, 0);
|
|
177
|
-
|
|
178
165
|
themodal.onYes(() => {
|
|
179
|
-
// Clean up the event listener synchronously — the DOM may be torn down
|
|
180
|
-
// before our deferred resolve fires otherwise.
|
|
181
|
-
const modalContainer = document.querySelector('.micromodal-parent');
|
|
182
|
-
if (modalContainer) {
|
|
183
|
-
modalContainer.removeEventListener('click', handleCopy);
|
|
184
|
-
}
|
|
185
166
|
setTimeout(resolve, 0);
|
|
186
167
|
return true;
|
|
187
168
|
});
|
|
188
169
|
|
|
189
170
|
themodal.onNo = () => {
|
|
190
|
-
const modalContainer = document.querySelector('.micromodal-parent');
|
|
191
|
-
if (modalContainer) {
|
|
192
|
-
modalContainer.removeEventListener('click', handleCopy);
|
|
193
|
-
}
|
|
194
171
|
setTimeout(resolve, 0);
|
|
195
172
|
};
|
|
196
173
|
});
|
|
@@ -203,7 +180,7 @@ export function snippet(title, content, extraContent = '') {
|
|
|
203
180
|
// Auto-initialize - cleanup any leftover modal elements
|
|
204
181
|
export function init() {
|
|
205
182
|
onDomReady(() => {
|
|
206
|
-
const micromodalParentElem = document.querySelector("
|
|
183
|
+
const micromodalParentElem = document.querySelector("[data-clay-modal]");
|
|
207
184
|
if (micromodalParentElem) {
|
|
208
185
|
micromodalParentElem.remove();
|
|
209
186
|
document.body.style.overflow = "";
|
package/src/ui/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import toast, { toastPersistent } from "./toast.js";
|
|
1
|
+
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
|
+
import { saveFeedback } from "../core/save-feedback.js";
|
|
4
5
|
|
|
5
6
|
// Attach the public surface explicitly (§2.3). We do NOT rely on toast.js /
|
|
6
7
|
// dialogs.js evaluation side effects — their window auto-exports are stripped.
|
|
@@ -19,17 +20,27 @@ if (typeof window.toast === "undefined") window.toast = toast;
|
|
|
19
20
|
if (typeof window.toastPersistent === "undefined") window.toastPersistent = toastPersistent;
|
|
20
21
|
|
|
21
22
|
// Automatic save feedback. clay:save-saving stays deliberately silent (a toast for
|
|
22
|
-
// a sub-second transient is noise).
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// a sub-second transient is noise). With the indicator plugin loaded, saved belongs
|
|
24
|
+
// to its chip; a failed or offline save stays up as a toast until the next save
|
|
25
|
+
// lands. Keep your own toast lib? The events are public. clay.saveToast moves saved
|
|
26
|
+
// from the chip to a toast.
|
|
27
|
+
saveFeedback.toasts = true;
|
|
28
|
+
// clay.saveToast = true shows Saved as a toast (with the host's message) instead of on
|
|
29
|
+
// the chip. A page may set it before this loads (window.clay = { saveToast: true }).
|
|
30
|
+
saveFeedback.savedToast = !!clay.saveToast;
|
|
31
|
+
Object.defineProperty(clay, "saveToast", {
|
|
32
|
+
get: () => saveFeedback.savedToast,
|
|
33
|
+
set: (on) => { saveFeedback.savedToast = !!on; },
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
});
|
|
37
|
+
document.addEventListener("clay:save-saved", (e) => {
|
|
38
|
+
dismissPersistent("Couldn't save");
|
|
39
|
+
dismissPersistent("Offline, not saved");
|
|
40
|
+
if (e.detail?.msgType === "warning") toast(e.detail.msg || "Saved", "warning");
|
|
41
|
+
else if (!saveFeedback.chip || saveFeedback.savedToast) toast(e.detail?.msg || "Saved", "success");
|
|
42
|
+
});
|
|
25
43
|
document.addEventListener("clay:save-error", () => toastPersistent("Couldn't save", "error"));
|
|
26
|
-
// Spec §6. Persistent, and carrying the host's own words, because this is the one
|
|
27
|
-
// save outcome the person has to act on: their edits are safe and unsaved, and
|
|
28
|
-
// autosave has stopped until they choose. Deliberately a toast and not a dialog:
|
|
29
|
-
// most conflicts surface on an autosave nobody asked for, and a modal thrown over
|
|
30
|
-
// the page a person is typing into is worse than the problem it reports.
|
|
31
|
-
document.addEventListener("clay:save-conflict", (e) =>
|
|
32
|
-
toastPersistent(e.detail?.msg || "This document changed since you opened it", "warning"));
|
|
33
44
|
document.addEventListener("clay:save-offline", () => toastPersistent("Offline, not saved", "warning"));
|
|
34
45
|
|
|
35
46
|
export { toast, toastPersistent, ask, consent, tell, snippet, themodal };
|