@panphora/clayjs 1.13.0 → 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.
Files changed (137) hide show
  1. package/THIRD-PARTY-NOTICES.md +19 -0
  2. package/dist/clay.standalone.js +69739 -41784
  3. package/dist/server/checkpoint.cjs +70690 -0
  4. package/dist/server/checkpoint.mjs +8 -0
  5. package/dist/server/rooms.cjs +72039 -0
  6. package/dist/server/rooms.mjs +66 -0
  7. package/docs/guide.md +95 -94
  8. package/entries/clay.js +22 -0
  9. package/package.json +16 -2
  10. package/packed-contract.json +17 -3
  11. package/src/core/conflict-download.js +11 -2
  12. package/src/core/host-meta.js +75 -0
  13. package/src/core/persist.js +11 -0
  14. package/src/core/save-core.js +266 -31
  15. package/src/core/save.js +62 -32
  16. package/src/core/snapshot.js +39 -13
  17. package/src/core/style-comparisons.js +37 -0
  18. package/src/dom/insert-style-tag.js +11 -12
  19. package/src/lib/effect.js +130 -0
  20. package/src/lib/style-adoption.js +9 -0
  21. package/src/lib/style.js +44 -0
  22. package/src/live/fast-lane.js +347 -60
  23. package/src/live/local-lane.js +32 -6
  24. package/src/live/ops.js +77 -0
  25. package/src/live/provenance.js +129 -0
  26. package/src/live/save-history.js +188 -0
  27. package/src/live/server-lane.js +9 -2
  28. package/src/live/store.js +194 -16
  29. package/src/loader-logic.js +7 -3
  30. package/src/loader.js +6 -0
  31. package/src/page/address.js +20 -0
  32. package/src/page/adopt-boot.js +189 -0
  33. package/src/page/adopt.js +240 -0
  34. package/src/page/ai-tool.js +188 -0
  35. package/src/page/announce.js +21 -0
  36. package/src/page/blocks.js +1121 -0
  37. package/src/page/blocksel.js +269 -0
  38. package/src/page/board-drag.js +226 -0
  39. package/src/page/board-panel.js +353 -0
  40. package/src/page/board-style.js +146 -0
  41. package/src/page/board-view.js +447 -0
  42. package/src/page/board.js +41 -0
  43. package/src/page/chart.js +220 -0
  44. package/src/page/checklist.js +143 -0
  45. package/src/page/chrome.js +587 -0
  46. package/src/page/csv-import.js +169 -0
  47. package/src/page/drag.js +202 -0
  48. package/src/page/drop.js +112 -0
  49. package/src/page/editor.js +384 -0
  50. package/src/page/highlight.js +268 -0
  51. package/src/page/inline.js +60 -1
  52. package/src/page/items.js +159 -0
  53. package/src/page/link.js +247 -0
  54. package/src/page/markdown.js +71 -0
  55. package/src/page/media.js +570 -0
  56. package/src/page/merge.js +130 -0
  57. package/src/page/model.js +71 -9
  58. package/src/page/placeholder.js +23 -0
  59. package/src/page/projection.js +647 -0
  60. package/src/page/recover.js +193 -0
  61. package/src/page/rich/cell.js +16 -0
  62. package/src/page/rich/chart.js +104 -0
  63. package/src/page/rich/csv.js +118 -0
  64. package/src/page/rich/page-tool.js +86 -0
  65. package/src/page/rich/tool-runtime.js +195 -0
  66. package/src/page/save-projection.js +53 -0
  67. package/src/page/save-states.js +85 -0
  68. package/src/page/schema.js +12 -5
  69. package/src/page/table-kinds.js +20 -0
  70. package/src/page/table-paste.js +57 -0
  71. package/src/page/table-select.js +244 -0
  72. package/src/page/table-style.js +25 -0
  73. package/src/page/table.js +650 -0
  74. package/src/page/themes/content.css +46 -0
  75. package/src/page/themes/light-1.css +269 -0
  76. package/src/page/themes/light-2.css +211 -0
  77. package/src/page/themes/light-3.css +130 -0
  78. package/src/page/themes/light-4.css +299 -0
  79. package/src/page/tool-data.js +25 -0
  80. package/src/page/tool-view.js +68 -0
  81. package/src/page/tools.js +133 -0
  82. package/src/page/view.js +136 -0
  83. package/src/page/ybind/author.js +25 -0
  84. package/src/page/ybind/binding.js +92 -0
  85. package/src/page/ybind/board-commands.js +170 -0
  86. package/src/page/ybind/board.js +89 -0
  87. package/src/page/ybind/bridge.js +382 -0
  88. package/src/page/ybind/conversion.js +26 -0
  89. package/src/page/ybind/core.js +7 -0
  90. package/src/page/ybind/decode.js +208 -0
  91. package/src/page/ybind/doc.js +58 -0
  92. package/src/page/ybind/external.js +190 -0
  93. package/src/page/ybind/group.js +124 -0
  94. package/src/page/ybind/index.js +12 -0
  95. package/src/page/ybind/layout.js +27 -0
  96. package/src/page/ybind/marks.js +18 -0
  97. package/src/page/ybind/migrate.js +152 -0
  98. package/src/page/ybind/moves.js +288 -0
  99. package/src/page/ybind/origins.js +257 -0
  100. package/src/page/ybind/parts.js +44 -0
  101. package/src/page/ybind/projection-map.js +92 -0
  102. package/src/page/ybind/provider.js +285 -0
  103. package/src/page/ybind/recovery.js +411 -0
  104. package/src/page/ybind/seed.js +27 -0
  105. package/src/page/ybind/table.js +166 -0
  106. package/src/page/ybind/tokens.js +93 -0
  107. package/src/page/ybind/walk.js +333 -0
  108. package/src/page/ybind/writer.js +307 -0
  109. package/src/plugins/indicator.js +16 -6
  110. package/src/plugins/live.js +742 -65
  111. package/src/plugins/page.js +547 -0
  112. package/src/plugins/undo.js +60 -3
  113. package/src/plugins/visitor-drafts/prompt.js +4 -6
  114. package/src/server/checkpoint/index.js +218 -0
  115. package/src/server/rooms/index.js +59 -0
  116. package/src/server/rooms/live-data-ops.js +369 -0
  117. package/src/server/rooms/live-data-rooms.js +965 -0
  118. package/src/server/rooms/live-data-tags.js +49 -0
  119. package/src/server/rooms/live-page-history.js +41 -0
  120. package/src/server/rooms/live-provenance.js +142 -0
  121. package/src/server/rooms/page-admission.js +79 -0
  122. package/src/server/rooms/page-conversion.js +62 -0
  123. package/src/server/rooms/page-rebase.js +108 -0
  124. package/src/server/rooms/page-state.js +91 -0
  125. package/src/server/rooms/page-write-admission.js +63 -0
  126. package/src/server/rooms/page-write-guard.js +23 -0
  127. package/src/server/rooms/recent.js +18 -0
  128. package/src/server/rooms/spec-wire.js +17 -0
  129. package/src/standalone.js +22 -0
  130. package/src/sync/live-sync.js +13 -2
  131. package/src/sync/stream.js +62 -16
  132. package/src/ui/index.js +4 -0
  133. package/src/ui/toast.js +21 -5
  134. package/src/vendor/pierre/pierre.vendor.js +3 -0
  135. package/src/vendor/yjs.vendor.js +7 -0
  136. package/src/page/binding.js +0 -647
  137. 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;
@@ -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
- this._sharedSync = extensions.includes('sync') && extensions.includes('sync-worker') && !extensions.includes('presence');
705
- return extensions.includes('sync') ? WIRE_PROFILES.spec : WIRE_PROFILES.legacy;
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.
@@ -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?.port.postMessage({ v: 1, type: 'hidden' });
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.port.postMessage({ v: 1, type: 'visible' });
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(new URL('/_/sync/worker.js', window.location.origin).href, 'clay-sync');
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 = 1;
87
- this._emit('open');
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
- worker.port.postMessage({ v: 1, type: pageHidden() ? 'hidden' : 'visible' });
111
- worker.port.postMessage({ v: 1, type: 'subscribe', document: this._documentURL, lane: this._lane, since: this._since });
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 worker.port.postMessage({ v: 1, type: 'ping' });
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 { worker.port.postMessage({ v: 1, type: 'unsubscribe' }); } catch {}
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
- el.append(glyph, text);
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();