@panphora/clayjs 1.13.1 → 1.14.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/THIRD-PARTY-NOTICES.md +19 -0
- package/dist/clay.standalone.js +69667 -41708
- package/dist/server/checkpoint.cjs +70690 -0
- package/dist/server/checkpoint.mjs +8 -0
- package/dist/server/rooms.cjs +72039 -0
- package/dist/server/rooms.mjs +66 -0
- package/docs/guide.md +94 -93
- package/entries/clay.js +22 -0
- package/package.json +16 -2
- package/packed-contract.json +17 -3
- package/src/core/conflict-download.js +11 -2
- package/src/core/host-meta.js +75 -0
- package/src/core/persist.js +11 -0
- package/src/core/save-core.js +266 -31
- package/src/core/save.js +62 -32
- package/src/core/snapshot.js +39 -13
- package/src/core/style-comparisons.js +37 -0
- package/src/dom/insert-style-tag.js +11 -12
- package/src/lib/effect.js +130 -0
- package/src/lib/style-adoption.js +9 -0
- package/src/lib/style.js +44 -0
- package/src/live/fast-lane.js +347 -60
- package/src/live/local-lane.js +32 -6
- package/src/live/ops.js +77 -0
- package/src/live/provenance.js +129 -0
- package/src/live/save-history.js +188 -0
- package/src/live/server-lane.js +9 -2
- package/src/live/store.js +194 -16
- package/src/loader-logic.js +7 -3
- package/src/loader.js +6 -0
- package/src/page/address.js +20 -0
- package/src/page/adopt-boot.js +189 -0
- package/src/page/adopt.js +240 -0
- package/src/page/ai-tool.js +188 -0
- package/src/page/announce.js +21 -0
- package/src/page/blocks.js +1121 -0
- package/src/page/blocksel.js +269 -0
- package/src/page/board-drag.js +226 -0
- package/src/page/board-panel.js +353 -0
- package/src/page/board-style.js +146 -0
- package/src/page/board-view.js +447 -0
- package/src/page/board.js +41 -0
- package/src/page/chart.js +220 -0
- package/src/page/checklist.js +143 -0
- package/src/page/chrome.js +587 -0
- package/src/page/csv-import.js +169 -0
- package/src/page/drag.js +202 -0
- package/src/page/drop.js +112 -0
- package/src/page/editor.js +384 -0
- package/src/page/highlight.js +268 -0
- package/src/page/inline.js +60 -1
- package/src/page/items.js +159 -0
- package/src/page/link.js +247 -0
- package/src/page/markdown.js +71 -0
- package/src/page/media.js +570 -0
- package/src/page/merge.js +130 -0
- package/src/page/model.js +71 -9
- package/src/page/placeholder.js +23 -0
- package/src/page/projection.js +647 -0
- package/src/page/recover.js +193 -0
- package/src/page/rich/cell.js +16 -0
- package/src/page/rich/chart.js +104 -0
- package/src/page/rich/csv.js +118 -0
- package/src/page/rich/page-tool.js +86 -0
- package/src/page/rich/tool-runtime.js +195 -0
- package/src/page/save-projection.js +53 -0
- package/src/page/save-states.js +85 -0
- package/src/page/schema.js +12 -5
- package/src/page/table-kinds.js +20 -0
- package/src/page/table-paste.js +57 -0
- package/src/page/table-select.js +244 -0
- package/src/page/table-style.js +25 -0
- package/src/page/table.js +650 -0
- package/src/page/themes/content.css +46 -0
- package/src/page/themes/light-1.css +269 -0
- package/src/page/themes/light-2.css +211 -0
- package/src/page/themes/light-3.css +130 -0
- package/src/page/themes/light-4.css +299 -0
- package/src/page/tool-data.js +25 -0
- package/src/page/tool-view.js +68 -0
- package/src/page/tools.js +133 -0
- package/src/page/view.js +136 -0
- package/src/page/ybind/author.js +25 -0
- package/src/page/ybind/binding.js +92 -0
- package/src/page/ybind/board-commands.js +170 -0
- package/src/page/ybind/board.js +89 -0
- package/src/page/ybind/bridge.js +382 -0
- package/src/page/ybind/conversion.js +26 -0
- package/src/page/ybind/core.js +7 -0
- package/src/page/ybind/decode.js +208 -0
- package/src/page/ybind/doc.js +58 -0
- package/src/page/ybind/external.js +190 -0
- package/src/page/ybind/group.js +124 -0
- package/src/page/ybind/index.js +12 -0
- package/src/page/ybind/layout.js +27 -0
- package/src/page/ybind/marks.js +18 -0
- package/src/page/ybind/migrate.js +152 -0
- package/src/page/ybind/moves.js +288 -0
- package/src/page/ybind/origins.js +257 -0
- package/src/page/ybind/parts.js +44 -0
- package/src/page/ybind/projection-map.js +92 -0
- package/src/page/ybind/provider.js +285 -0
- package/src/page/ybind/recovery.js +411 -0
- package/src/page/ybind/seed.js +27 -0
- package/src/page/ybind/table.js +166 -0
- package/src/page/ybind/tokens.js +93 -0
- package/src/page/ybind/walk.js +333 -0
- package/src/page/ybind/writer.js +307 -0
- package/src/plugins/indicator.js +16 -6
- package/src/plugins/live.js +742 -65
- package/src/plugins/page.js +547 -0
- package/src/plugins/undo.js +60 -3
- package/src/server/checkpoint/index.js +218 -0
- package/src/server/rooms/index.js +59 -0
- package/src/server/rooms/live-data-ops.js +369 -0
- package/src/server/rooms/live-data-rooms.js +965 -0
- package/src/server/rooms/live-data-tags.js +49 -0
- package/src/server/rooms/live-page-history.js +41 -0
- package/src/server/rooms/live-provenance.js +142 -0
- package/src/server/rooms/page-admission.js +79 -0
- package/src/server/rooms/page-conversion.js +62 -0
- package/src/server/rooms/page-rebase.js +108 -0
- package/src/server/rooms/page-state.js +91 -0
- package/src/server/rooms/page-write-admission.js +63 -0
- package/src/server/rooms/page-write-guard.js +23 -0
- package/src/server/rooms/recent.js +18 -0
- package/src/server/rooms/spec-wire.js +17 -0
- package/src/standalone.js +22 -0
- package/src/sync/live-sync.js +13 -2
- package/src/sync/stream.js +62 -16
- package/src/ui/index.js +4 -0
- package/src/ui/toast.js +21 -5
- package/src/vendor/pierre/pierre.vendor.js +3 -0
- package/src/vendor/yjs.vendor.js +7 -0
- package/src/page/binding.js +0 -647
- package/src/page/replay.js +0 -578
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A function of one string that remembers its last few answers. A save hands the
|
|
2
|
+
// same document to several readers in turn, and each would otherwise parse it again.
|
|
3
|
+
export function recent(size, fn) {
|
|
4
|
+
const answers = new Map();
|
|
5
|
+
return (html) => {
|
|
6
|
+
if (typeof html !== 'string') return fn(html);
|
|
7
|
+
if (answers.has(html)) {
|
|
8
|
+
const answer = answers.get(html);
|
|
9
|
+
answers.delete(html);
|
|
10
|
+
answers.set(html, answer);
|
|
11
|
+
return answer;
|
|
12
|
+
}
|
|
13
|
+
const answer = fn(html);
|
|
14
|
+
answers.set(html, answer);
|
|
15
|
+
if (answers.size > size) answers.delete(answers.keys().next().value);
|
|
16
|
+
return answer;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export function documentEtag (storedHtml) {
|
|
4
|
+
return crypto.createHash('sha256').update(storedHtml || '').digest('hex').substring(0, 16);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function ifMatchSatisfied (fieldValue, storedBytes) {
|
|
8
|
+
const stored = storedBytes || '';
|
|
9
|
+
const field = String(fieldValue ?? '').trim();
|
|
10
|
+
if (field === '*') return stored.length > 0;
|
|
11
|
+
|
|
12
|
+
const current = documentEtag(stored);
|
|
13
|
+
return field.split(',').some(entry => {
|
|
14
|
+
const tag = entry.trim().replace(/^W\//i, '').replace(/^"(.*)"$/, '$1');
|
|
15
|
+
return tag !== '' && tag === current;
|
|
16
|
+
});
|
|
17
|
+
}
|
package/src/standalone.js
CHANGED
|
@@ -35,6 +35,28 @@ import { noteBootParams } from "./core/draft-mode.js";
|
|
|
35
35
|
|
|
36
36
|
if (!clay.ready) mintReady();
|
|
37
37
|
|
|
38
|
+
// clay.start runs app code once per page, after boot. A live-sync merge can
|
|
39
|
+
// re-insert an inline script and run it again; the repeat call is a no-op.
|
|
40
|
+
// It lives in the bootstrap so an inline script can call it before boot ends.
|
|
41
|
+
if (!clay.start) {
|
|
42
|
+
var started = Object.create(null);
|
|
43
|
+
clay.start = function (name, fn) {
|
|
44
|
+
if (typeof name === "function") { fn = name; name = ""; }
|
|
45
|
+
if (started[name]) return;
|
|
46
|
+
started[name] = true;
|
|
47
|
+
// A failed boot rejects the old promise after minting a new one, so a call
|
|
48
|
+
// made before the failure waits for the corrected tag's boot instead.
|
|
49
|
+
var wait = function () {
|
|
50
|
+
var ready = clay.ready;
|
|
51
|
+
return ready.then(fn, function (err) {
|
|
52
|
+
if (clay.ready === ready) throw err;
|
|
53
|
+
return wait();
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
return wait();
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
// clay.js needs its own URL to find the loader; this file needs it only for the
|
|
39
61
|
// query string, so pasted inline into the page it boots with the defaults.
|
|
40
62
|
var script = document.currentScript;
|
package/src/sync/live-sync.js
CHANGED
|
@@ -287,6 +287,9 @@ class LiveSync {
|
|
|
287
287
|
// document, and hostMeta() memoizes the request itself.
|
|
288
288
|
this._profile = null;
|
|
289
289
|
this._profilePromise = null;
|
|
290
|
+
this._sharedData = false;
|
|
291
|
+
this._sharedSync = false;
|
|
292
|
+
this._liveData = false;
|
|
290
293
|
this._editing = null;
|
|
291
294
|
this._ready = null;
|
|
292
295
|
// Bumped by every start(), so a discovery that resolves after a stop/restart
|
|
@@ -701,8 +704,13 @@ class LiveSync {
|
|
|
701
704
|
this._profilePromise = hostMeta()
|
|
702
705
|
.then((meta) => {
|
|
703
706
|
const extensions = meta?.extensions || [];
|
|
704
|
-
|
|
705
|
-
|
|
707
|
+
const hasSync = extensions.includes('sync');
|
|
708
|
+
const hasLiveData = extensions.includes('live-data');
|
|
709
|
+
const hasPresence = extensions.includes('presence');
|
|
710
|
+
this._sharedData = hasSync && hasLiveData && extensions.includes('sync-worker-data') && !hasPresence;
|
|
711
|
+
this._sharedSync = hasSync && !hasPresence && (this._sharedData || (extensions.includes('sync-worker') && !hasLiveData));
|
|
712
|
+
this._liveData = hasLiveData;
|
|
713
|
+
return hasSync ? WIRE_PROFILES.spec : WIRE_PROFILES.legacy;
|
|
706
714
|
})
|
|
707
715
|
.catch(() => WIRE_PROFILES.legacy)
|
|
708
716
|
.then((profile) => {
|
|
@@ -787,8 +795,11 @@ class LiveSync {
|
|
|
787
795
|
// would otherwise point the sync stream at an origin the document chose.
|
|
788
796
|
this.sse = new SyncStream(new URL(path, window.location.origin).href, {
|
|
789
797
|
shared: this._sharedSync,
|
|
798
|
+
sharedData: this._sharedData,
|
|
790
799
|
documentURL: window.location.href,
|
|
791
800
|
lane: this.lane,
|
|
801
|
+
clientId: this.clientId,
|
|
802
|
+
keepAliveWhenHidden: this._liveData && this.lane === 'live',
|
|
792
803
|
// Only a response that named its own version gives this tab anything to
|
|
793
804
|
// compare against. On a host that served none there is no provenance to
|
|
794
805
|
// check, and the page keeps its pre-1.9.0 startup behavior.
|
package/src/sync/stream.js
CHANGED
|
@@ -9,7 +9,7 @@ document.addEventListener('clay:workspace-foreground', () => { workspaceBackgrou
|
|
|
9
9
|
const pageHidden = () => document.hidden || workspaceBackground;
|
|
10
10
|
|
|
11
11
|
export class SyncStream extends EventTarget {
|
|
12
|
-
constructor(url, { shared = false, documentURL, lane, startupCheck = false } = {}) {
|
|
12
|
+
constructor(url, { shared = false, sharedData = false, documentURL, lane, clientId, startupCheck = false, keepAliveWhenHidden = false } = {}) {
|
|
13
13
|
super();
|
|
14
14
|
this.url = url;
|
|
15
15
|
this.readyState = 0;
|
|
@@ -17,8 +17,11 @@ export class SyncStream extends EventTarget {
|
|
|
17
17
|
this.onmessage = null;
|
|
18
18
|
this.onerror = null;
|
|
19
19
|
this._shared = shared && typeof SharedWorker === 'function';
|
|
20
|
+
this._sharedData = sharedData;
|
|
20
21
|
this._documentURL = documentURL;
|
|
21
22
|
this._lane = lane;
|
|
23
|
+
this._clientId = clientId;
|
|
24
|
+
this._keepAliveWhenHidden = keepAliveWhenHidden;
|
|
22
25
|
this._since = 0;
|
|
23
26
|
this._closed = false;
|
|
24
27
|
this._suspended = false;
|
|
@@ -35,17 +38,17 @@ export class SyncStream extends EventTarget {
|
|
|
35
38
|
this._visibility = () => {
|
|
36
39
|
clearTimeout(this._hiddenTimer);
|
|
37
40
|
if (pageHidden()) {
|
|
38
|
-
this._worker
|
|
39
|
-
this._hiddenTimer = setTimeout(() => this._suspend(), HIDDEN_DELAY);
|
|
41
|
+
if (this._worker) this._postWorker(this._worker, 'hidden');
|
|
42
|
+
if (!this._keepAliveWhenHidden) this._hiddenTimer = setTimeout(() => this._suspend(), HIDDEN_DELAY);
|
|
40
43
|
} else {
|
|
41
44
|
if (this._suspended) this._resume();
|
|
42
45
|
else if (this._worker) {
|
|
43
|
-
this._worker
|
|
46
|
+
this._postWorker(this._worker, 'visible');
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
};
|
|
47
|
-
this._pagehide = () => this._suspend();
|
|
48
|
-
this._pageshow = () => { if (!pageHidden()) this._resume(); };
|
|
50
|
+
this._pagehide = () => this._suspend(true);
|
|
51
|
+
this._pageshow = () => { if (!pageHidden() || this._keepAliveWhenHidden) this._resume(); };
|
|
49
52
|
document.addEventListener('visibilitychange', this._visibility);
|
|
50
53
|
document.addEventListener('clay:workspace-background', this._visibility);
|
|
51
54
|
document.addEventListener('clay:workspace-foreground', this._visibility);
|
|
@@ -62,14 +65,22 @@ export class SyncStream extends EventTarget {
|
|
|
62
65
|
this[`on${type}`]?.(event);
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
_postWorker(worker, type, fields = {}) {
|
|
69
|
+
worker.port.postMessage({ v: this._sharedData ? 2 : 1, type, ...fields });
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
_open() {
|
|
66
73
|
if (this._closed || this._suspended) return;
|
|
67
74
|
this.readyState = 0;
|
|
68
75
|
if (!this._shared) return this._openDirect();
|
|
69
76
|
try {
|
|
70
|
-
const worker = new SharedWorker(
|
|
77
|
+
const worker = new SharedWorker(
|
|
78
|
+
new URL(this._sharedData ? '/_/sync/data-worker.js' : '/_/sync/worker.js', window.location.origin).href,
|
|
79
|
+
this._sharedData ? 'clay-sync-data' : 'clay-sync'
|
|
80
|
+
);
|
|
71
81
|
this._worker = worker;
|
|
72
82
|
let lastReply = Date.now();
|
|
83
|
+
let greeted = false;
|
|
73
84
|
worker.onerror = () => {
|
|
74
85
|
if (this._worker !== worker) return;
|
|
75
86
|
this._shared = false;
|
|
@@ -78,13 +89,39 @@ export class SyncStream extends EventTarget {
|
|
|
78
89
|
};
|
|
79
90
|
this._startTimer = setTimeout(worker.onerror, START_TIMEOUT);
|
|
80
91
|
worker.port.onmessage = ({ data }) => {
|
|
81
|
-
if (this._worker !== worker || data?.v !== 1) return;
|
|
92
|
+
if (this._worker !== worker || data?.v !== (this._sharedData ? 2 : 1)) return;
|
|
93
|
+
if (this._sharedData) {
|
|
94
|
+
if (data.type === 'unsupported') {
|
|
95
|
+
worker.onerror();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (data.type === 'gone') {
|
|
99
|
+
this.readyState = 2;
|
|
100
|
+
this._emit('error');
|
|
101
|
+
this.close();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!greeted) {
|
|
105
|
+
if (data.type !== 'hello') return;
|
|
106
|
+
if (!Array.isArray(data.features) || !data.features.includes('live-data')) {
|
|
107
|
+
worker.onerror();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
greeted = true;
|
|
111
|
+
clearTimeout(this._startTimer);
|
|
112
|
+
lastReply = Date.now();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (data.type === 'hello') return;
|
|
116
|
+
}
|
|
82
117
|
lastReply = Date.now();
|
|
83
|
-
if (['status', 'pong', 'cursor', 'frame', 'gone'].includes(data.type)) clearTimeout(this._startTimer);
|
|
118
|
+
if (!this._sharedData && ['status', 'pong', 'cursor', 'frame', 'gone'].includes(data.type)) clearTimeout(this._startTimer);
|
|
84
119
|
if (data.type === 'status') {
|
|
85
120
|
if (data.state === 'open') {
|
|
86
|
-
this.readyState
|
|
87
|
-
|
|
121
|
+
if (!this._sharedData || this.readyState !== 1) {
|
|
122
|
+
this.readyState = 1;
|
|
123
|
+
this._emit('open');
|
|
124
|
+
}
|
|
88
125
|
} else if (data.state === 'connecting') {
|
|
89
126
|
this.readyState = 0;
|
|
90
127
|
this._emit('error');
|
|
@@ -100,6 +137,9 @@ export class SyncStream extends EventTarget {
|
|
|
100
137
|
} else if (data.type === 'frame' && typeof data.data === 'string') {
|
|
101
138
|
this._remember(data.data);
|
|
102
139
|
this._emit('message', data.data);
|
|
140
|
+
} else if (this._sharedData && data.type === 'event' && ['data', 'data-state', 'data-writer'].includes(data.event)
|
|
141
|
+
&& data.data !== null && typeof data.data === 'object' && !Array.isArray(data.data)) {
|
|
142
|
+
try { this._emit(data.event, JSON.stringify(data.data)); } catch {}
|
|
103
143
|
} else if (data.type === 'gone') {
|
|
104
144
|
this.readyState = 2;
|
|
105
145
|
this._emit('error');
|
|
@@ -107,14 +147,16 @@ export class SyncStream extends EventTarget {
|
|
|
107
147
|
}
|
|
108
148
|
};
|
|
109
149
|
worker.port.start();
|
|
110
|
-
|
|
111
|
-
|
|
150
|
+
this._postWorker(worker, pageHidden() ? 'hidden' : 'visible');
|
|
151
|
+
const subscription = { document: this._documentURL, lane: this._lane, since: this._since };
|
|
152
|
+
if (this._sharedData && this._lane === 'live') subscription.clientId = this._clientId;
|
|
153
|
+
this._postWorker(worker, 'subscribe', subscription);
|
|
112
154
|
this._pingTimer = setInterval(() => {
|
|
113
155
|
if (this._worker !== worker) return;
|
|
114
156
|
if (Date.now() - lastReply > PING_INTERVAL * 3) {
|
|
115
157
|
this._repair = true;
|
|
116
158
|
this._reopen();
|
|
117
|
-
} else
|
|
159
|
+
} else this._postWorker(worker, 'ping');
|
|
118
160
|
}, PING_INTERVAL);
|
|
119
161
|
} catch {
|
|
120
162
|
this._shared = false;
|
|
@@ -170,7 +212,7 @@ export class SyncStream extends EventTarget {
|
|
|
170
212
|
this._worker = null;
|
|
171
213
|
worker.onerror = null;
|
|
172
214
|
worker.port.onmessage = null;
|
|
173
|
-
try {
|
|
215
|
+
try { this._postWorker(worker, 'unsubscribe'); } catch {}
|
|
174
216
|
worker.port.close();
|
|
175
217
|
}
|
|
176
218
|
if (this._source) {
|
|
@@ -180,16 +222,20 @@ export class SyncStream extends EventTarget {
|
|
|
180
222
|
}
|
|
181
223
|
|
|
182
224
|
_reopen() {
|
|
225
|
+
const notify = (this._sharedData || this._keepAliveWhenHidden) && this.readyState === 1;
|
|
226
|
+
this.readyState = 0;
|
|
227
|
+
if (notify) this._emit('error');
|
|
183
228
|
this._disconnect();
|
|
184
229
|
this._open();
|
|
185
230
|
}
|
|
186
231
|
|
|
187
|
-
_suspend() {
|
|
232
|
+
_suspend(pageExit = false) {
|
|
188
233
|
clearTimeout(this._hiddenTimer);
|
|
189
234
|
if (this._closed || this._suspended) return;
|
|
190
235
|
this._suspended = true;
|
|
191
236
|
this._repair = true;
|
|
192
237
|
this.readyState = 0;
|
|
238
|
+
if (pageExit && this._keepAliveWhenHidden) this._emit('error');
|
|
193
239
|
this._disconnect();
|
|
194
240
|
}
|
|
195
241
|
|
package/src/ui/index.js
CHANGED
|
@@ -45,5 +45,9 @@ document.addEventListener("clay:save-saved", (e) => {
|
|
|
45
45
|
});
|
|
46
46
|
document.addEventListener("clay:save-error", () => toastPersistent("Couldn't save", "error"));
|
|
47
47
|
document.addEventListener("clay:save-offline", () => toastPersistent("Offline, not saved", "warning"));
|
|
48
|
+
document.addEventListener("clay:live-notice", (e) => {
|
|
49
|
+
if (e.detail?.code === "save-uncertain") toastPersistent(e.detail.message, "warning");
|
|
50
|
+
if (e.detail?.code === "op-too-large") toast(e.detail.message, "warning");
|
|
51
|
+
});
|
|
48
52
|
|
|
49
53
|
export { toast, toastPersistent, ask, consent, tell, snippet, themodal };
|
package/src/ui/toast.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// The Hyperclay dashboard's toast (Bevel's React toast with the dashboard's overrides),
|
|
5
5
|
// drawn inline: a 3px tone edge, a 16px Bevel glyph, 13.5px semibold text, a quiet close.
|
|
6
6
|
import { set } from "../lib/hostile-css.js";
|
|
7
|
-
import { bevelBox, bevelSurface, bevelText, pageScheme, protectIcon } from "./bevel-controls.js";
|
|
7
|
+
import { bevelBox, bevelSurface, bevelText, bevelButton, pageScheme, protectIcon } from "./bevel-controls.js";
|
|
8
8
|
import { TOKENS, RULES, GLYPHS } from "./bevel.js";
|
|
9
9
|
|
|
10
10
|
const TONES = {
|
|
@@ -39,7 +39,7 @@ function stack() {
|
|
|
39
39
|
return box;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function build(message, type) {
|
|
42
|
+
function build(message, type, { detail = "", actions = [] } = {}) {
|
|
43
43
|
const tone = TONES[type];
|
|
44
44
|
const el = bevelSurface("div", [
|
|
45
45
|
"display:flex", "align-items:flex-start", ...RULES.toast, "width:100%",
|
|
@@ -48,13 +48,29 @@ function build(message, type) {
|
|
|
48
48
|
...HIDDEN, `transition:${motion()}`,
|
|
49
49
|
]);
|
|
50
50
|
el.setAttribute("data-clay-toast", type);
|
|
51
|
+
const body = bevelBox("div", ["flex:1 1 auto", "min-width:0", "display:flex", "flex-direction:column", "gap:8px"]);
|
|
52
|
+
const head = bevelBox("div", ["display:flex", "align-items:flex-start", "gap:8px"]);
|
|
51
53
|
const glyph = bevelText("span", ["display:inline-grid", "flex:none", "padding-top:1px", `color:${tone.edge}`]);
|
|
52
54
|
glyph.innerHTML = tone.icon;
|
|
53
55
|
protectIcon(glyph.firstElementChild, 16);
|
|
54
56
|
// A message can carry a filename, and a filename is whatever whoever wrote the
|
|
55
57
|
// file chose. It goes in as text so markup in a name can never become markup here.
|
|
56
58
|
const text = bevelText("span", ["flex:1 1 auto", "min-width:120px", ...RULES.toastTitle, "overflow-wrap:anywhere"], message);
|
|
57
|
-
|
|
59
|
+
head.append(glyph, text);
|
|
60
|
+
body.append(head);
|
|
61
|
+
// Detail and actions are the same untrusted text as the title: textContent for
|
|
62
|
+
// both, so a reason string can never become markup in the toast either.
|
|
63
|
+
if (detail) {
|
|
64
|
+
body.append(bevelText("span", ["display:block", `color:${TOKENS.muted}`, "overflow-wrap:anywhere"], detail));
|
|
65
|
+
}
|
|
66
|
+
if (actions.length) {
|
|
67
|
+
const row = bevelBox("div", ["display:flex", "flex-wrap:wrap", "gap:8px"]);
|
|
68
|
+
for (const { label, onClick } of actions) {
|
|
69
|
+
row.append(bevelButton(label, { small: true, variant: "quiet", onClick }));
|
|
70
|
+
}
|
|
71
|
+
body.append(row);
|
|
72
|
+
}
|
|
73
|
+
el.append(body);
|
|
58
74
|
return el;
|
|
59
75
|
}
|
|
60
76
|
|
|
@@ -109,14 +125,14 @@ function toast(message, messageType = "success") {
|
|
|
109
125
|
const activePersistentToasts = new Map();
|
|
110
126
|
|
|
111
127
|
// Persistent toast function - doesn't auto-dismiss, requires click to close
|
|
112
|
-
function toastPersistent(message, messageType = "warning") {
|
|
128
|
+
function toastPersistent(message, messageType = "warning", { detail = "", actions = [] } = {}) {
|
|
113
129
|
const existing = activePersistentToasts.get(message);
|
|
114
130
|
if (existing) {
|
|
115
131
|
dismiss(existing);
|
|
116
132
|
activePersistentToasts.delete(message);
|
|
117
133
|
}
|
|
118
134
|
|
|
119
|
-
const el = build(message, TONES[messageType] ? messageType : "warning");
|
|
135
|
+
const el = build(message, TONES[messageType] ? messageType : "warning", { detail, actions });
|
|
120
136
|
set(el, "cursor", "default");
|
|
121
137
|
const close = closeButton((e) => {
|
|
122
138
|
e.stopPropagation();
|