@warkypublic/svelix 0.1.44 → 0.1.47

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 (109) hide show
  1. package/README.md +93 -33
  2. package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
  3. package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
  4. package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
  5. package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
  6. package/dist/components/Boxer/Boxer.svelte +488 -602
  7. package/dist/components/Boxer/BoxerTarget.svelte +6 -46
  8. package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
  9. package/dist/components/Boxer/types.d.ts +0 -1
  10. package/dist/components/Button.svelte +19 -43
  11. package/dist/components/ButtonPreview.svelte +2 -11
  12. package/dist/components/CardGrid/CardGrid.svelte +1141 -0
  13. package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
  14. package/dist/components/CardGrid/CardGridFilterPanel.svelte +320 -0
  15. package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
  16. package/dist/components/CardGrid/DefaultCard.svelte +287 -0
  17. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
  18. package/dist/components/CardGrid/ImageCardStory.svelte +308 -0
  19. package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
  20. package/dist/components/CardGrid/cardGrid.d.ts +33 -0
  21. package/dist/components/CardGrid/cardGrid.js +21 -0
  22. package/dist/components/CardGrid/index.d.ts +4 -0
  23. package/dist/components/CardGrid/index.js +4 -0
  24. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  25. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  26. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  27. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  28. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  29. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  30. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  31. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  32. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  33. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  34. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  35. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  36. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  37. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  38. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  39. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  40. package/dist/components/Former/Former.svelte +306 -424
  41. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  42. package/dist/components/Former/FormerDrawer.svelte +26 -78
  43. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  44. package/dist/components/Former/FormerModal.svelte +24 -70
  45. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  46. package/dist/components/Former/FormerPreview.svelte +76 -78
  47. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  48. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -44
  49. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
  50. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  51. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  52. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  53. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -42
  54. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  55. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  56. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
  57. package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
  58. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
  59. package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
  60. package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
  61. package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
  62. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  63. package/dist/components/Gridler/components/Gridler.svelte +338 -442
  64. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  65. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  66. package/dist/components/Gridler/components/GridlerFull.svelte +520 -841
  67. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  68. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  69. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  70. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  71. package/dist/components/Gridler/types.d.ts +1 -1
  72. package/dist/components/Icons/IconAdd.svelte +1 -3
  73. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  74. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  75. package/dist/components/Icons/IconCamera.svelte +1 -3
  76. package/dist/components/Icons/IconClose.svelte +1 -3
  77. package/dist/components/Icons/IconEdit.svelte +1 -3
  78. package/dist/components/Icons/IconFilter.svelte +1 -3
  79. package/dist/components/Icons/IconSearch.svelte +1 -3
  80. package/dist/components/Icons/IconSort.svelte +1 -3
  81. package/dist/components/Icons/IconTrash.svelte +1 -3
  82. package/dist/components/Portal/Portal.svelte +14 -38
  83. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  84. package/dist/components/Svark/Svark.svelte +680 -852
  85. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  86. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  87. package/dist/components/Svark/SvarkPager.svelte +2 -28
  88. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  89. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  90. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  91. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  92. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  93. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  94. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  95. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  96. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  97. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  98. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  99. package/dist/components/VTree/VTree.svelte +349 -431
  100. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  101. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  102. package/dist/components/VTree/VTreeRow.svelte +13 -65
  103. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  104. package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
  105. package/dist/components/index.d.ts +1 -0
  106. package/dist/components/index.js +1 -0
  107. package/llm/COMPONENT_GUIDE.md +240 -0
  108. package/llm/plans/card_grid.plan.md +305 -0
  109. package/package.json +48 -46
@@ -1,312 +1,247 @@
1
- <script lang="ts">
2
- import { onMount, untrack } from "svelte";
3
- import type { CollaboraEditorProps } from "../types.js";
4
- import { getMimeFromExt, getExtFromFilename } from "../utils.js";
5
- import { SvelteURLSearchParams } from "svelte/reactivity";
6
-
7
- let {
8
- value,
9
- filename,
10
- readonly = false,
11
- wopiUrl,
12
- discoverUrl,
13
- adminKey = "",
14
- userid = 0,
15
- username = "user",
16
- viewtype: _viewtype = "tabbed",
17
- headers,
18
- onChange,
19
- onSave,
20
- insertText = $bindable(),
21
- insertHtml = $bindable(),
22
- }: CollaboraEditorProps = $props();
23
-
24
- let frameEl = $state<HTMLIFrameElement | undefined>(undefined);
25
- let ready = $state(false);
26
- let loading = $state(false);
27
- let errmsg = $state("");
28
-
29
- // Stable IDs for this editor instance — untrack prevents Svelte from treating
30
- // these as reactive dependencies (they are intentional one-time reads).
31
- const editorId = `collabora_${crypto.randomUUID().slice(0, 8)}`;
32
- // const ext = untrack(() => (filename ? getExtFromFilename(filename) : ""));
33
- const fileId = `${untrack(() => userid)}_${crypto.randomUUID()}`;
34
-
35
- // Mutable buffer — not reactive; mirrors React's useRef<Blob>
36
- let buffer: Blob | undefined = untrack(() => value);
37
- let saveRequestComplete = true;
38
- let signedToken = "";
39
- let signedWopiSrc = "";
40
-
41
- const contentType = $derived(
42
- getMimeFromExt(filename ? getExtFromFilename(filename) : ""),
43
- );
44
-
45
- // ── WOPI helpers ────────────────────────────────────────────────────────────
46
-
47
- async function fetchToken(
48
- canWrite: boolean,
49
- ): Promise<{ token: string; wopiSrc: string }> {
50
- const base = wopiUrl.replace(/\/$/, "");
51
- const reqHeaders: Record<string, string> = {
52
- "Content-Type": "application/json",
53
- ...headers,
54
- };
55
- if (adminKey) reqHeaders["X-API-Key"] = adminKey;
56
- const res = await fetch(`${base}/api/tokens`, {
57
- method: "POST",
58
- headers: reqHeaders,
59
- body: JSON.stringify({
60
- resource_type: "file",
61
- file_id: fileId,
62
- user_id: String(userid),
63
- user_name: username,
64
- can_write: canWrite,
65
- expires_in_seconds: 3600,
66
- }),
67
- });
68
- if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
69
- const data = await res.json();
70
- return { token: data.access_token, wopiSrc: data.wopi_src };
71
- }
72
-
73
- async function fileExistsInWopi(token: string): Promise<boolean> {
74
- const params = new URLSearchParams({ access_token: token });
75
- const base = wopiUrl.replace(/\/$/, "");
76
- const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
77
- headers,
78
- method: "GET",
79
- });
80
- return res.ok;
81
- }
82
-
83
- async function uploadToWopi(blob: Blob, token: string): Promise<void> {
84
- const params = new SvelteURLSearchParams();
85
- params.append("access_token", token);
86
- if (filename !== undefined) {
87
- params.append("filename", filename);
88
- params.append(
89
- "contenttype",
90
- getMimeFromExt(getExtFromFilename(filename)),
91
- );
92
- }
93
- if (userid !== undefined) params.append("userid", String(userid));
94
- if (username !== undefined) params.append("username", username);
95
-
96
- const base = wopiUrl.replace(/\/$/, "");
97
- const fullurl = `${base}/wopi/files/${fileId}/contents?${params.toString()}`;
98
- const res = await fetch(fullurl, {
99
- headers: { ...headers, "X-WOPI-Override": "PUT" },
100
- method: "POST",
101
- body: blob,
102
- });
103
- if (!res.ok) throw new Error(`WOPI upload failed: ${res.status}`);
104
- }
105
-
106
- async function downloadFromWopi(): Promise<Blob | undefined> {
107
- const params = new SvelteURLSearchParams();
108
- params.append("access_token", signedToken);
109
- if (userid !== undefined) params.append("userid", String(userid));
110
-
111
- const base = wopiUrl.replace(/\/$/, "");
112
- const fullurl = `${base}/wopi/files/${fileId}/contents?${params.toString()}`;
113
- const res = await fetch(fullurl, { headers, method: "GET" });
114
- if (res.ok) return res.blob();
115
- return undefined;
116
- }
117
-
118
- // ── Discovery ────────────────────────────────────────────────────────────────
119
-
120
- async function findEditorUrl(mimeType: string): Promise<string> {
121
- const res = await fetch(discoverUrl);
122
- if (!res.ok) throw new Error(`Discovery request failed: ${res.status}`);
123
- const xml = await res.text();
124
- const doc = new DOMParser().parseFromString(xml, "text/xml");
125
- const action = doc.querySelector(`app[name="${mimeType}"] action`);
126
- const urlsrc = action?.getAttribute("urlsrc");
127
- if (!urlsrc)
128
- throw new Error(`No Collabora editor found for MIME type: ${mimeType}`);
129
- const params = new URLSearchParams({
130
- WOPISrc: signedWopiSrc,
131
- access_token: signedToken,
132
- });
133
- return `${urlsrc}${params}`;
134
- }
135
-
136
- // ── Load ─────────────────────────────────────────────────────────────────────
137
-
138
- async function loadEditor(): Promise<void> {
139
- if (!filename || !buffer) return;
140
- errmsg = "";
141
- loading = true;
142
- ready = false;
143
- insertText = undefined;
144
- insertHtml = undefined;
145
- try {
146
- try {
147
- localStorage.setItem("WSDWelcomeDisabled", "true");
148
- } catch {
149
- /* unavailable */
150
- }
151
- // Always use a write token for the initial upload regardless of readonly
152
- const { token: writeToken } = await fetchToken(true);
153
- const exists = await fileExistsInWopi(writeToken);
154
- if (!exists) await uploadToWopi(buffer, writeToken);
155
-
156
- // Editor token respects readonly
157
- const { token, wopiSrc } = await fetchToken(!readonly);
158
- signedToken = token;
159
- signedWopiSrc = wopiSrc;
160
-
161
- const url = await findEditorUrl(contentType);
162
- // Navigate iframe to Collabora — Collabora will fetch the file from WOPI
163
- if (frameEl) frameEl.src = url;
164
- } catch (e) {
165
- errmsg = String(e);
166
- loading = false;
167
- }
168
- }
169
-
170
- // ── Save flow ─────────────────────────────────────────────────────────────────
171
-
172
- function sendSaveAction(): Promise<void> {
173
- return new Promise((resolve, reject) => {
174
- if (!frameEl?.contentWindow) {
175
- reject(new Error("No iframe"));
176
- return;
177
- }
178
- saveRequestComplete = false;
179
- frameEl.contentWindow.postMessage(
180
- JSON.stringify({
181
- MessageId: "Action_Save",
182
- SendTime: Date.now(),
183
- Values: {
184
- Notify: true,
185
- DontSaveIfUnmodified: false,
186
- DontTerminateEdit: false,
187
- },
188
- }),
189
- "*",
190
- );
191
- let cnt = 0;
192
- const itv = setInterval(() => {
193
- cnt++;
194
- if (saveRequestComplete) {
195
- clearInterval(itv);
196
- resolve();
197
- }
198
- if (cnt > 60) {
199
- clearInterval(itv);
200
- reject(new Error("Save timeout after 30s"));
201
- }
202
- }, 500);
203
- });
204
- }
205
-
206
- function handleSaveResponse(): void {
207
- setTimeout(async () => {
208
- try {
209
- const blob = await downloadFromWopi();
210
- if (blob) {
211
- buffer = blob;
212
- onChange?.(blob);
213
- }
214
- } catch (e) {
215
- errmsg = `Could not retrieve saved file: ${e}`;
216
- } finally {
217
- saveRequestComplete = true;
218
- onSave?.(buffer ?? new Blob([]));
219
- }
220
- }, 100);
221
- }
222
-
223
- // ── PostMessage handler ───────────────────────────────────────────────────────
224
-
225
- function handlePostMessage(e: MessageEvent): void {
226
- try {
227
- const msg =
228
- typeof e.data === "string"
229
- ? JSON.parse(e.data)
230
- : typeof e.data === "object"
231
- ? e.data
232
- : null;
233
- if (!msg?.MessageId || !frameEl?.contentWindow) return;
234
-
235
- const msgId: string = msg.MessageId;
236
- const msgData = msg.Values;
237
-
238
- if (msgId === "App_LoadingStatus" && msgData?.Status === "Frame_Ready") {
239
- frameEl.contentWindow.postMessage(
240
- JSON.stringify({
241
- MessageId: "Host_PostmessageReady",
242
- SendTime: Date.now(),
243
- }),
244
- "*",
245
- );
246
- frameEl.contentWindow.postMessage(
247
- JSON.stringify({
248
- MessageId: "Action_ChangeEditorMode",
249
- SendTime: Date.now(),
250
- Values: { Mode: readonly ? "view" : "edit" },
251
- }),
252
- "*",
253
- );
254
- ready = true;
255
- loading = false;
256
- insertText = (text: string) => postPaste("text/plain", text);
257
- insertHtml = (html: string) => postPaste("text/html", html);
258
- return;
259
- }
260
- if (msgId === "UI_Save") {
261
- sendSaveAction();
262
- return;
263
- }
264
- if (msgId === "Doc_ModifiedStatus" && msgData?.Modified) {
265
- handleSaveResponse();
266
- return;
267
- }
268
- if (msgId === "Action_Save_Resp" && msgData?.success) {
269
- handleSaveResponse();
270
- }
271
- } catch {
272
- // ignore malformed messages from other origins
273
- }
274
- }
275
-
276
- onMount(() => {
277
- window.addEventListener("message", handlePostMessage);
278
- return () => window.removeEventListener("message", handlePostMessage);
279
- });
280
-
281
- // ── Insert actions (bound by parent via bind:insertText / bind:insertHtml) ────
282
-
283
- function postPaste(mimetype: string, data: string): void {
284
- if (!frameEl?.contentWindow) return;
285
- // Restore focus to the iframe so Collabora has an active cursor position
286
- frameEl.focus();
287
- frameEl.contentWindow.postMessage(
288
- JSON.stringify({
289
- MessageId: "Action_Paste",
290
- SendTime: Date.now(),
291
- Values: { Mimetype: mimetype, Data: data },
292
- }),
293
- "*",
294
- );
295
- }
296
-
297
- // ── Reactive reload ───────────────────────────────────────────────────────────
298
-
299
- $effect(() => {
300
- // Track value, filename and WOPI config as dependencies.
301
- // Update buffer when value prop changes, then reload.
302
- const v = value;
303
- const f = filename;
304
- void wopiUrl;
305
- void discoverUrl;
306
-
307
- if (v !== undefined) buffer = v;
308
- if (f && buffer) void loadEditor();
309
- });
1
+ <script lang="ts">import { onMount, untrack } from "svelte";
2
+ import { getMimeFromExt, getExtFromFilename } from "../utils.js";
3
+ import { SvelteURLSearchParams } from "svelte/reactivity";
4
+ let { value, filename, readonly = false, wopiUrl, discoverUrl, adminKey = "", userid = 0, username = "user", viewtype: _viewtype = "tabbed", headers, onChange, onSave, insertText = $bindable(), insertHtml = $bindable() } = $props();
5
+ let frameEl = $state(undefined);
6
+ let ready = $state(false);
7
+ let loading = $state(false);
8
+ let errmsg = $state("");
9
+ // Stable IDs for this editor instance — untrack prevents Svelte from treating
10
+ // these as reactive dependencies (they are intentional one-time reads).
11
+ const editorId = `collabora_${crypto.randomUUID().slice(0, 8)}`;
12
+ // const ext = untrack(() => (filename ? getExtFromFilename(filename) : ""));
13
+ const fileId = `${untrack(() => userid)}_${crypto.randomUUID()}`;
14
+ // Mutable buffer — not reactive; mirrors React's useRef<Blob>
15
+ let buffer = untrack(() => value);
16
+ let saveRequestComplete = true;
17
+ let signedToken = "";
18
+ let signedWopiSrc = "";
19
+ const contentType = $derived(getMimeFromExt(filename ? getExtFromFilename(filename) : ""));
20
+ // ── WOPI helpers ────────────────────────────────────────────────────────────
21
+ async function fetchToken(canWrite) {
22
+ const base = wopiUrl.replace(/\/$/, "");
23
+ const reqHeaders = {
24
+ "Content-Type": "application/json",
25
+ ...headers
26
+ };
27
+ if (adminKey) reqHeaders["X-API-Key"] = adminKey;
28
+ const res = await fetch(`${base}/api/tokens`, {
29
+ method: "POST",
30
+ headers: reqHeaders,
31
+ body: JSON.stringify({
32
+ resource_type: "file",
33
+ file_id: fileId,
34
+ user_id: String(userid),
35
+ user_name: username,
36
+ can_write: canWrite,
37
+ expires_in_seconds: 3600
38
+ })
39
+ });
40
+ if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
41
+ const data = await res.json();
42
+ return {
43
+ token: data.access_token,
44
+ wopiSrc: data.wopi_src
45
+ };
46
+ }
47
+ async function fileExistsInWopi(token) {
48
+ const params = new URLSearchParams({ access_token: token });
49
+ const base = wopiUrl.replace(/\/$/, "");
50
+ const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
51
+ headers,
52
+ method: "GET"
53
+ });
54
+ return res.ok;
55
+ }
56
+ async function uploadToWopi(blob, token) {
57
+ const params = new SvelteURLSearchParams();
58
+ params.append("access_token", token);
59
+ if (filename !== undefined) {
60
+ params.append("filename", filename);
61
+ params.append("contenttype", getMimeFromExt(getExtFromFilename(filename)));
62
+ }
63
+ if (userid !== undefined) params.append("userid", String(userid));
64
+ if (username !== undefined) params.append("username", username);
65
+ const base = wopiUrl.replace(/\/$/, "");
66
+ const fullurl = `${base}/wopi/files/${fileId}/contents?${params.toString()}`;
67
+ const res = await fetch(fullurl, {
68
+ headers: {
69
+ ...headers,
70
+ "X-WOPI-Override": "PUT"
71
+ },
72
+ method: "POST",
73
+ body: blob
74
+ });
75
+ if (!res.ok) throw new Error(`WOPI upload failed: ${res.status}`);
76
+ }
77
+ async function downloadFromWopi() {
78
+ const params = new SvelteURLSearchParams();
79
+ params.append("access_token", signedToken);
80
+ if (userid !== undefined) params.append("userid", String(userid));
81
+ const base = wopiUrl.replace(/\/$/, "");
82
+ const fullurl = `${base}/wopi/files/${fileId}/contents?${params.toString()}`;
83
+ const res = await fetch(fullurl, {
84
+ headers,
85
+ method: "GET"
86
+ });
87
+ if (res.ok) return res.blob();
88
+ return undefined;
89
+ }
90
+ // ── Discovery ────────────────────────────────────────────────────────────────
91
+ async function findEditorUrl(mimeType) {
92
+ const res = await fetch(discoverUrl);
93
+ if (!res.ok) throw new Error(`Discovery request failed: ${res.status}`);
94
+ const xml = await res.text();
95
+ const doc = new DOMParser().parseFromString(xml, "text/xml");
96
+ const action = doc.querySelector(`app[name="${mimeType}"] action`);
97
+ const urlsrc = action?.getAttribute("urlsrc");
98
+ if (!urlsrc) throw new Error(`No Collabora editor found for MIME type: ${mimeType}`);
99
+ const params = new URLSearchParams({
100
+ WOPISrc: signedWopiSrc,
101
+ access_token: signedToken
102
+ });
103
+ return `${urlsrc}${params}`;
104
+ }
105
+ // ── Load ─────────────────────────────────────────────────────────────────────
106
+ async function loadEditor() {
107
+ if (!filename || !buffer) return;
108
+ errmsg = "";
109
+ loading = true;
110
+ ready = false;
111
+ insertText = undefined;
112
+ insertHtml = undefined;
113
+ try {
114
+ try {
115
+ localStorage.setItem("WSDWelcomeDisabled", "true");
116
+ } catch {}
117
+ // Always use a write token for the initial upload regardless of readonly
118
+ const { token: writeToken } = await fetchToken(true);
119
+ const exists = await fileExistsInWopi(writeToken);
120
+ if (!exists) await uploadToWopi(buffer, writeToken);
121
+ // Editor token respects readonly
122
+ const { token, wopiSrc } = await fetchToken(!readonly);
123
+ signedToken = token;
124
+ signedWopiSrc = wopiSrc;
125
+ const url = await findEditorUrl(contentType);
126
+ // Navigate iframe to Collabora — Collabora will fetch the file from WOPI
127
+ if (frameEl) frameEl.src = url;
128
+ } catch (e) {
129
+ errmsg = String(e);
130
+ loading = false;
131
+ }
132
+ }
133
+ // ── Save flow ─────────────────────────────────────────────────────────────────
134
+ function sendSaveAction() {
135
+ return new Promise((resolve, reject) => {
136
+ if (!frameEl?.contentWindow) {
137
+ reject(new Error("No iframe"));
138
+ return;
139
+ }
140
+ saveRequestComplete = false;
141
+ frameEl.contentWindow.postMessage(JSON.stringify({
142
+ MessageId: "Action_Save",
143
+ SendTime: Date.now(),
144
+ Values: {
145
+ Notify: true,
146
+ DontSaveIfUnmodified: false,
147
+ DontTerminateEdit: false
148
+ }
149
+ }), "*");
150
+ let cnt = 0;
151
+ const itv = setInterval(() => {
152
+ cnt++;
153
+ if (saveRequestComplete) {
154
+ clearInterval(itv);
155
+ resolve();
156
+ }
157
+ if (cnt > 60) {
158
+ clearInterval(itv);
159
+ reject(new Error("Save timeout after 30s"));
160
+ }
161
+ }, 500);
162
+ });
163
+ }
164
+ function handleSaveResponse() {
165
+ setTimeout(async () => {
166
+ try {
167
+ const blob = await downloadFromWopi();
168
+ if (blob) {
169
+ buffer = blob;
170
+ onChange?.(blob);
171
+ }
172
+ } catch (e) {
173
+ errmsg = `Could not retrieve saved file: ${e}`;
174
+ } finally {
175
+ saveRequestComplete = true;
176
+ onSave?.(buffer ?? new Blob([]));
177
+ }
178
+ }, 100);
179
+ }
180
+ // ── PostMessage handler ───────────────────────────────────────────────────────
181
+ function handlePostMessage(e) {
182
+ try {
183
+ const msg = typeof e.data === "string" ? JSON.parse(e.data) : typeof e.data === "object" ? e.data : null;
184
+ if (!msg?.MessageId || !frameEl?.contentWindow) return;
185
+ const msgId = msg.MessageId;
186
+ const msgData = msg.Values;
187
+ if (msgId === "App_LoadingStatus" && msgData?.Status === "Frame_Ready") {
188
+ frameEl.contentWindow.postMessage(JSON.stringify({
189
+ MessageId: "Host_PostmessageReady",
190
+ SendTime: Date.now()
191
+ }), "*");
192
+ frameEl.contentWindow.postMessage(JSON.stringify({
193
+ MessageId: "Action_ChangeEditorMode",
194
+ SendTime: Date.now(),
195
+ Values: { Mode: readonly ? "view" : "edit" }
196
+ }), "*");
197
+ ready = true;
198
+ loading = false;
199
+ insertText = (text) => postPaste("text/plain", text);
200
+ insertHtml = (html) => postPaste("text/html", html);
201
+ return;
202
+ }
203
+ if (msgId === "UI_Save") {
204
+ sendSaveAction();
205
+ return;
206
+ }
207
+ if (msgId === "Doc_ModifiedStatus" && msgData?.Modified) {
208
+ handleSaveResponse();
209
+ return;
210
+ }
211
+ if (msgId === "Action_Save_Resp" && msgData?.success) {
212
+ handleSaveResponse();
213
+ }
214
+ } catch {}
215
+ }
216
+ onMount(() => {
217
+ window.addEventListener("message", handlePostMessage);
218
+ return () => window.removeEventListener("message", handlePostMessage);
219
+ });
220
+ // ── Insert actions (bound by parent via bind:insertText / bind:insertHtml) ────
221
+ function postPaste(mimetype, data) {
222
+ if (!frameEl?.contentWindow) return;
223
+ // Restore focus to the iframe so Collabora has an active cursor position
224
+ frameEl.focus();
225
+ frameEl.contentWindow.postMessage(JSON.stringify({
226
+ MessageId: "Action_Paste",
227
+ SendTime: Date.now(),
228
+ Values: {
229
+ Mimetype: mimetype,
230
+ Data: data
231
+ }
232
+ }), "*");
233
+ }
234
+ // ── Reactive reload ───────────────────────────────────────────────────────────
235
+ $effect(() => {
236
+ // Track value, filename and WOPI config as dependencies.
237
+ // Update buffer when value prop changes, then reload.
238
+ const v = value;
239
+ const f = filename;
240
+ void wopiUrl;
241
+ void discoverUrl;
242
+ if (v !== undefined) buffer = v;
243
+ if (f && buffer) void loadEditor();
244
+ });
310
245
  </script>
311
246
 
312
247
  <div class="relative flex h-full w-full flex-1 flex-col overflow-hidden">
@@ -1,6 +1,5 @@
1
- <script lang="ts">
2
- import type { ImageViewerProps } from '../types.js';
3
- let { value: _value, filename }: ImageViewerProps = $props();
1
+ <script lang="ts">let { value: _value, filename } = $props();
2
+ export {};
4
3
  </script>
5
4
 
6
5
  <div class="card p-4 text-surface-700-300">
@@ -1,18 +1,17 @@
1
- <script lang="ts">
2
- import type { ImageViewerProps } from '../types.js';
3
- import IconAlertTriangle from '../../Icons/IconAlertTriangle.svelte';
4
- let { value, filename }: ImageViewerProps = $props();
5
-
6
- let src = $state('');
7
- let err = $state(false);
8
-
9
- $effect(() => {
10
- if (!value) { src = ''; return; }
11
- err = false;
12
- const url = URL.createObjectURL(value);
13
- src = url;
14
- return () => URL.revokeObjectURL(url);
15
- });
1
+ <script lang="ts">import IconAlertTriangle from "../../Icons/IconAlertTriangle.svelte";
2
+ let { value, filename } = $props();
3
+ let src = $state("");
4
+ let err = $state(false);
5
+ $effect(() => {
6
+ if (!value) {
7
+ src = "";
8
+ return;
9
+ }
10
+ err = false;
11
+ const url = URL.createObjectURL(value);
12
+ src = url;
13
+ return () => URL.revokeObjectURL(url);
14
+ });
16
15
  </script>
17
16
 
18
17
  <div class="relative flex h-full w-full items-center justify-center overflow-hidden bg-surface-50-950">