@warkypublic/svelix 0.1.26 → 0.1.28

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 (39) hide show
  1. package/dist/components/BetterMenu/MenuRenderer.svelte +8 -7
  2. package/dist/components/Boxer/Boxer.svelte +39 -6
  3. package/dist/components/Boxer/BoxerTarget.svelte +19 -3
  4. package/dist/components/Boxer/BoxerTarget.svelte.d.ts +4 -0
  5. package/dist/components/ContentEditor/ContentEditor.svelte +7 -1
  6. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +0 -1
  7. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +1 -1
  8. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +1 -1
  9. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +33 -8
  10. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +10 -20
  11. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +7 -3
  12. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +4 -5
  13. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +194 -185
  14. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +0 -1
  15. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +1 -1
  16. package/dist/components/ContentEditor/types.d.ts +7 -0
  17. package/dist/components/ContentEditor/utils.d.ts +2 -0
  18. package/dist/components/ContentEditor/utils.js +71 -0
  19. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +2 -2
  20. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +1 -1
  21. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +3 -3
  22. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +18 -11
  23. package/dist/components/Gridler/components/Gridler.svelte +9 -4
  24. package/dist/components/Gridler/components/GridlerCanvas.svelte +188 -6
  25. package/dist/components/Gridler/components/GridlerFull.svelte +43 -33
  26. package/dist/components/Gridler/components/GridlerSearch.svelte +4 -2
  27. package/dist/components/Gridler/utils/canvasRender.js +4 -2
  28. package/dist/components/Gridler/utils/helpers.d.ts +16 -0
  29. package/dist/components/Gridler/utils/helpers.js +29 -0
  30. package/dist/components/Portal/Portal.svelte +1 -3
  31. package/dist/components/Svark/Svark.svelte +8 -1
  32. package/dist/components/Svark/SvarkTopBar.svelte +18 -18
  33. package/dist/components/SvarkGrid/SvarkGrid.svelte +1 -2
  34. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +2 -1
  35. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +1 -1
  36. package/dist/components/SvarkGrid/utils/svarGridMapping.js +0 -1
  37. package/dist/components/VTree/VTreeResolveSpecAdapter.js +5 -10
  38. package/dist/css/kinathka.theme.css +205 -0
  39. package/package.json +26 -26
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import { getContext } from 'svelte';
3
4
  import type { BetterMenuInstanceItem, BetterMenuStoreState } from './types';
4
5
  import type { BetterMenuStore } from './store';
@@ -54,9 +55,9 @@
54
55
 
55
56
  {#snippet menuItem(item: BetterMenuInstanceItem, onClose: () => void)}
56
57
  {#if item.renderer}
57
- <div class="px-3 py-2">
58
- {@render (item.renderer as any)({ ...item, onClose })}
59
- </div>
58
+ <div class="px-3 py-2">
59
+ {@render (item.renderer as unknown as Snippet<[Record<string, unknown>]>)({ ...item, onClose })}
60
+ </div>
60
61
  {:else if item.isDivider}
61
62
  <hr class="border-slate-200 dark:border-slate-700 my-1" />
62
63
  {:else if item.onClick || item.onClickAsync}
@@ -100,10 +101,10 @@
100
101
  style:width={menuState.width ? `${menuState.width}px` : undefined}
101
102
  style:z-index={menuZ}
102
103
  >
103
- {#if menu.renderer}
104
- <div class="py-2">
105
- {@render (menu.renderer as any)({ id: menu.id, onClose: () => hideMenu(menu.id) })}
106
- </div>
104
+ {#if menu.renderer}
105
+ <div class="py-2">
106
+ {@render (menu.renderer as unknown as Snippet<[Record<string, unknown>]>)({ id: menu.id, onClose: () => hideMenu(menu.id) })}
107
+ </div>
107
108
  {:else}
108
109
  {#each menu.items ?? [] as item, itemIndex (`${menu.id}_item_${item.id ?? itemIndex}`)}
109
110
  {@render menuItem(item, () => hideMenu(menu.id))}
@@ -37,6 +37,10 @@
37
37
  value = $bindable<any>(undefined),
38
38
  }: BoxerProps = $props();
39
39
 
40
+ const instanceId = `boxer-${Math.random().toString(36).slice(2, 10)}`;
41
+ const inputId = `${instanceId}-input`;
42
+ const listboxId = `${instanceId}-listbox`;
43
+
40
44
  // Derive effective dataSource and onAPICall from adapter when not explicitly provided
41
45
  const dataSource = dataSourceProp ?? (adapter ? "server" : "local");
42
46
  const onAPICall =
@@ -88,6 +92,7 @@
88
92
  let popupTop = $state(0);
89
93
  let popupLeft = $state(0);
90
94
  let popupWidth = $state(0);
95
+ let activeOptionIndex = $state<number | null>(null);
91
96
  let backdropZ = $state(1090);
92
97
  let dropdownZ = $state(1100);
93
98
  let pointerInteractingWithDropdown = $state(false);
@@ -209,6 +214,8 @@
209
214
  const option = $store.boxerData?.[index];
210
215
  if (!option) return;
211
216
 
217
+ activeOptionIndex = index;
218
+
212
219
  if (multiSelect) {
213
220
  const currentValues = Array.isArray(value) ? value : [];
214
221
  const isSelected = currentValues.includes(option.value);
@@ -227,6 +234,7 @@
227
234
  store.setSearch("");
228
235
  store.setInput(option.label);
229
236
  store.setOpened(false);
237
+ activeOptionIndex = null;
230
238
  }
231
239
  }
232
240
 
@@ -238,6 +246,7 @@
238
246
  onChange?.(value);
239
247
  store.setSearch("");
240
248
  store.setInput("");
249
+ activeOptionIndex = null;
241
250
  targetRef?.focus();
242
251
  }
243
252
  if (openOnClear) {
@@ -247,6 +256,7 @@
247
256
 
248
257
  function focusDropdownItem(index: number) {
249
258
  const clamped = Math.max(0, Math.min(index, $store.boxerData.length - 1));
259
+ activeOptionIndex = clamped;
250
260
  requestAnimationFrame(() => {
251
261
  const el = parentEl?.querySelector<HTMLDivElement>(
252
262
  `[data-index="${clamped}"]`,
@@ -277,17 +287,20 @@
277
287
  );
278
288
  if (selectedIndex >= 0) {
279
289
  store.setOpened(false);
290
+ activeOptionIndex = null;
280
291
  } else {
281
292
  onOptionSubmit(0);
282
293
  }
283
294
  } else {
284
295
  store.setOpened(false);
296
+ activeOptionIndex = null;
285
297
  }
286
298
  break;
287
299
  case "Escape":
288
300
  if ($store.opened) {
289
301
  e.preventDefault();
290
302
  store.setOpened(false);
303
+ activeOptionIndex = null;
291
304
  }
292
305
  break;
293
306
  }
@@ -302,6 +315,7 @@
302
315
  case "ArrowUp":
303
316
  e.preventDefault();
304
317
  if (index === 0) {
318
+ activeOptionIndex = null;
305
319
  targetRef?.focus();
306
320
  } else {
307
321
  focusDropdownItem(index - 1);
@@ -316,6 +330,7 @@
316
330
  case "Escape":
317
331
  e.preventDefault();
318
332
  store.setOpened(false);
333
+ activeOptionIndex = null;
319
334
  targetRef?.focus();
320
335
  break;
321
336
  }
@@ -326,7 +341,8 @@
326
341
  const rect = anchorEl.getBoundingClientRect();
327
342
  const viewportWidth = typeof window === "undefined" ? rect.width : window.innerWidth;
328
343
  const horizontalPadding = 8;
329
- const nextWidth = Math.max(220, rect.width);
344
+ const maxWidth = Math.max(160, viewportWidth - horizontalPadding * 2);
345
+ const nextWidth = Math.min(Math.max(220, rect.width), maxWidth);
330
346
  const maxLeft = viewportWidth - nextWidth - horizontalPadding;
331
347
 
332
348
  popupTop = rect.bottom + 4;
@@ -368,6 +384,7 @@
368
384
  }
369
385
  export function close() {
370
386
  store.setOpened(false);
387
+ activeOptionIndex = null;
371
388
  }
372
389
  export function focus() {
373
390
  targetRef?.focus();
@@ -377,6 +394,7 @@
377
394
  }
378
395
  export function open() {
379
396
  store.setOpened(true);
397
+ activeOptionIndex = null;
380
398
  }
381
399
  export function setValue(val: unknown) {
382
400
  value = val;
@@ -427,8 +445,8 @@
427
445
  });
428
446
 
429
447
  $effect(() => {
430
- anchorEl;
431
- storeOpened;
448
+ void anchorEl;
449
+ void storeOpened;
432
450
  syncInsideDialogFlag();
433
451
  });
434
452
 
@@ -462,6 +480,7 @@
462
480
  const relatedTarget = e.relatedTarget as Node | null;
463
481
  const insideDropdown = !!(relatedTarget && dropdownEl?.contains(relatedTarget));
464
482
  if (!currentTarget.contains(relatedTarget) && !insideDropdown) {
483
+ activeOptionIndex = null;
465
484
  if (!value && !multiSelect) {
466
485
  store.setSearch("");
467
486
  store.setInput("");
@@ -472,9 +491,13 @@
472
491
  >
473
492
  <BoxerTarget
474
493
  bind:this={targetRef}
494
+ activeDescendant={activeOptionIndex !== null ? `${instanceId}-option-${activeOptionIndex}` : undefined}
475
495
  {clearable}
496
+ controlsId={listboxId}
476
497
  {disabled}
477
498
  {error}
499
+ expanded={$store.opened}
500
+ {inputId}
478
501
  {label}
479
502
  {leftSection}
480
503
  {placeholder}
@@ -486,6 +509,7 @@
486
509
  onsearch={(v) => {
487
510
  store.setSearch(v);
488
511
  store.setInput(v);
512
+ activeOptionIndex = null;
489
513
  store.setOpened(true);
490
514
  }}
491
515
  />
@@ -493,10 +517,9 @@
493
517
  <Portal disabled={effectiveDisablePortal}>
494
518
  {#if $store.opened}
495
519
  {#if !effectiveDisablePortal}
496
- <!-- svelte-ignore a11y_click_events_have_key_events -->
497
- <!-- svelte-ignore a11y_no_static_element_interactions -->
498
520
  <div
499
521
  class="fixed inset-0"
522
+ data-testid="boxer-backdrop"
500
523
  onpointerdown={onBackdropPointerDown}
501
524
  style:z-index={backdropZ}
502
525
  ></div>
@@ -507,6 +530,8 @@
507
530
  class={`${effectiveDisablePortal
508
531
  ? "absolute left-0 right-0 top-full mt-1"
509
532
  : "fixed"} card bg-surface-50-950 shadow-lg border border-surface-300-700 overflow-hidden`}
533
+ data-testid="boxer-dropdown"
534
+ id={listboxId}
510
535
  role="listbox"
511
536
  aria-label={label ?? "Options"}
512
537
  onpointerdown={markDropdownPointerInteraction}
@@ -541,6 +566,7 @@
541
566
  class:bg-primary-100={isSelected}
542
567
  class:dark:bg-primary-900={isSelected}
543
568
  data-index={vRow.index}
569
+ id={`${instanceId}-option-${vRow.index}`}
544
570
  role="option"
545
571
  aria-selected={isSelected}
546
572
  tabindex="-1"
@@ -563,7 +589,14 @@
563
589
  </div>
564
590
  </div>
565
591
  {:else}
566
- <div class="px-3 py-2 text-sm text-surface-500">Nothing found</div>
592
+ <div
593
+ aria-live="polite"
594
+ class="px-3 py-2 text-sm text-surface-500"
595
+ data-testid="boxer-empty"
596
+ role="status"
597
+ >
598
+ no data
599
+ </div>
567
600
  {/if}
568
601
  </div>
569
602
  {/if}
@@ -2,9 +2,13 @@
2
2
  import type { Snippet } from "svelte";
3
3
 
4
4
  export interface Props {
5
+ activeDescendant?: string;
6
+ controlsId?: string;
5
7
  clearable?: boolean;
6
8
  disabled?: boolean;
7
9
  error?: string;
10
+ expanded?: boolean;
11
+ inputId?: string;
8
12
  isFetching?: boolean;
9
13
  label?: string;
10
14
  leftSection?: Snippet;
@@ -17,9 +21,13 @@
17
21
  }
18
22
 
19
23
  let {
24
+ activeDescendant,
25
+ controlsId,
20
26
  clearable = true,
21
27
  disabled,
22
28
  error,
29
+ expanded = false,
30
+ inputId = 'boxer-input',
23
31
  isFetching,
24
32
  label,
25
33
  leftSection,
@@ -40,7 +48,7 @@
40
48
 
41
49
  <div class="flex flex-col gap-1">
42
50
  {#if label}
43
- <label class="label text-sm font-medium" for="boxer-input">{label}</label>
51
+ <label class="label text-sm font-medium" for={inputId}>{label}</label>
44
52
  {/if}
45
53
  <div class="flex items-center relative">
46
54
  {#if leftSection}
@@ -49,9 +57,17 @@
49
57
  </div>
50
58
  {/if}
51
59
  <input
52
- id="boxer-input"
60
+ id={inputId}
53
61
  bind:this={inputEl}
54
62
  bind:value={search}
63
+ aria-activedescendant={activeDescendant}
64
+ aria-autocomplete="list"
65
+ aria-controls={controlsId}
66
+ aria-describedby={error ? `${inputId}-error` : undefined}
67
+ aria-expanded={expanded}
68
+ aria-haspopup="listbox"
69
+ aria-invalid={!!error}
70
+ role="combobox"
55
71
  class="input pr-8"
56
72
  class:input-error={!!error}
57
73
  class:pl-8={!!leftSection}
@@ -82,6 +98,6 @@
82
98
  </div>
83
99
  </div>
84
100
  {#if error}
85
- <p class="text-error-500 text-xs">{error}</p>
101
+ <p class="text-error-500 text-xs" id={`${inputId}-error`}>{error}</p>
86
102
  {/if}
87
103
  </div>
@@ -1,8 +1,12 @@
1
1
  import type { Snippet } from "svelte";
2
2
  export interface Props {
3
+ activeDescendant?: string;
4
+ controlsId?: string;
3
5
  clearable?: boolean;
4
6
  disabled?: boolean;
5
7
  error?: string;
8
+ expanded?: boolean;
9
+ inputId?: string;
6
10
  isFetching?: boolean;
7
11
  label?: string;
8
12
  leftSection?: Snippet;
@@ -19,6 +19,9 @@
19
19
  filename,
20
20
  contentType,
21
21
  mode = "value",
22
+ hideHeader = false,
23
+ theme,
24
+ colorScheme = "auto",
22
25
  readonly = false,
23
26
  onChange,
24
27
  onSave,
@@ -36,6 +39,7 @@
36
39
  }: ContentEditorProps = $props();
37
40
 
38
41
  const editorType = $derived(getEditorType({ filename, contentType }));
42
+ const forcedColorScheme = $derived(colorScheme === "auto" ? undefined : colorScheme);
39
43
 
40
44
  $effect(() => {
41
45
  if (mode === "filepointer") {
@@ -47,6 +51,8 @@
47
51
  </script>
48
52
 
49
53
  <div
54
+ data-theme={theme}
55
+ style:color-scheme={forcedColorScheme}
50
56
  style="display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 200px; overflow: hidden;"
51
57
  >
52
58
  {#if editorType === "monaco"}
@@ -67,7 +73,7 @@
67
73
  {:else if editorType === "video"}
68
74
  <VideoPlayer {value} {filename} />
69
75
  {:else if editorType === "text"}
70
- <TextEditor {value} {filename} {readonly} {onChange} bind:insertText bind:insertHtml />
76
+ <TextEditor {value} {filename} {hideHeader} {readonly} {onChange} bind:insertText bind:insertHtml />
71
77
  {:else if editorType === "markdown"}
72
78
  <MarkdownViewer {value} {filename} {readonly} {onChange} {onUpload} bind:insertText bind:insertHtml />
73
79
  {:else if editorType === "pdf"}
@@ -31,7 +31,6 @@
31
31
  {/if}
32
32
 
33
33
  {#if src}
34
- <!-- svelte-ignore a11y_media_has_caption -->
35
34
  <audio
36
35
  controls
37
36
  autoplay
@@ -13,7 +13,7 @@
13
13
  adminKey = "",
14
14
  userid = 0,
15
15
  username = "user",
16
- viewtype = "tabbed",
16
+ viewtype: _viewtype = "tabbed",
17
17
  headers,
18
18
  onChange,
19
19
  onSave,
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { ImageViewerProps } from '../types.js';
3
- let { value, filename }: ImageViewerProps = $props();
3
+ let { value: _value, filename }: ImageViewerProps = $props();
4
4
  </script>
5
5
 
6
6
  <div class="card p-4 text-surface-700-300">
@@ -14,12 +14,14 @@
14
14
  import '@cartamd/plugin-attachment/default.css';
15
15
  import 'katex/dist/katex.min.css';
16
16
  import 'github-markdown-css/github-markdown.css';
17
- import { blobToString } from '../utils.js';
17
+ import { blobToString, watchSkeletonDarkMode } from '../utils.js';
18
18
  import type { MarkdownViewerProps } from '../types.js';
19
19
 
20
20
  let { value, readonly = false, onChange, onUpload, insertText = $bindable(), insertHtml = $bindable() }: MarkdownViewerProps = $props();
21
21
 
22
22
  let carta = $state<Carta | undefined>(undefined);
23
+ let hostEl = $state<HTMLDivElement | undefined>(undefined);
24
+ let isDark = $state(false);
23
25
 
24
26
  onMount(() => {
25
27
  carta = new Carta({
@@ -34,6 +36,13 @@
34
36
  });
35
37
  });
36
38
 
39
+ onMount(() => {
40
+ const stopWatchingTheme = watchSkeletonDarkMode(hostEl, (nextIsDark) => {
41
+ isDark = nextIsDark;
42
+ });
43
+ return () => stopWatchingTheme();
44
+ });
45
+
37
46
  let text = $state('');
38
47
  let lastBlobText = $state('');
39
48
 
@@ -56,8 +65,18 @@
56
65
  insertHtml = undefined;
57
66
  return;
58
67
  }
68
+ type EditorViewLike = {
69
+ state: { selection: { main: { from: number; to: number } } };
70
+ dispatch: (payload: {
71
+ changes: { from: number; to: number; insert: string };
72
+ selection: { anchor: number };
73
+ }) => void;
74
+ focus: () => void;
75
+ };
76
+ type CartaEditorLike = Carta & { editor?: { editorView?: EditorViewLike } };
77
+
59
78
  function insertAtCursor(content: string): void {
60
- const view = carta!.editor?.editorView;
79
+ const view = (carta as CartaEditorLike).editor?.editorView;
61
80
  if (!view) return;
62
81
  const { from, to } = view.state.selection.main;
63
82
  view.dispatch({
@@ -71,7 +90,13 @@
71
90
  });
72
91
  </script>
73
92
 
74
- <div class="md-host" style="display: flex; flex-direction: column; width: 100%; height: 100%;">
93
+ <div
94
+ bind:this={hostEl}
95
+ class="md-host"
96
+ class:theme-dark={isDark}
97
+ class:theme-light={!isDark}
98
+ style="display: flex; flex-direction: column; width: 100%; height: 100%;"
99
+ >
75
100
  {#if carta}
76
101
  {#if readonly}
77
102
  {#key text}
@@ -110,8 +135,8 @@
110
135
  }
111
136
 
112
137
  /* Shiki dual-theme: dark colors only when .dark class is present */
113
- :global(.dark .shiki),
114
- :global(.dark .shiki span) {
138
+ :global(.md-host.theme-dark .shiki),
139
+ :global(.md-host.theme-dark .shiki span) {
115
140
  color: var(--shiki-dark) !important;
116
141
  background-color: var(--shiki-dark-bg) !important;
117
142
  font-style: var(--shiki-dark-font-style) !important;
@@ -120,7 +145,7 @@
120
145
  }
121
146
 
122
147
  /* carta-md dark mode: remap dark variable aliases when .dark is active */
123
- :global(.dark .carta-theme__default) {
148
+ :global(.md-host.theme-dark .carta-theme__default) {
124
149
  --border-color: var(--border-color-dark);
125
150
  --selection-color: var(--selection-color-dark);
126
151
  --focus-outline: var(--focus-outline-dark);
@@ -131,7 +156,7 @@
131
156
 
132
157
  /* github-markdown-css dark mode: override CSS vars when .dark class is active.
133
158
  github-markdown.css uses @media (prefers-color-scheme) which ignores the .dark class. */
134
- :global(.dark .markdown-body) {
159
+ :global(.md-host.theme-dark .markdown-body) {
135
160
  color-scheme: dark;
136
161
  --fgColor-default: #f0f6fc;
137
162
  --fgColor-muted: #9198a1;
@@ -186,7 +211,7 @@
186
211
  }
187
212
 
188
213
  /* github-markdown-css light mode: force light vars when OS may be dark but app is light */
189
- :global(html:not(.dark) .markdown-body) {
214
+ :global(.md-host.theme-light .markdown-body) {
190
215
  color-scheme: light;
191
216
  --fgColor-default: #1f2328;
192
217
  --fgColor-muted: #59636e;
@@ -1,7 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
3
  import * as monaco from 'monaco-editor';
4
- import { getLangFromExt, getExtFromFilename, blobToString } from '../utils.js';
4
+ import {
5
+ getLangFromExt,
6
+ getExtFromFilename,
7
+ blobToString,
8
+ watchSkeletonDarkMode,
9
+ } from '../utils.js';
5
10
  import type { MonacoEditorProps } from '../types.js';
6
11
 
7
12
  let {
@@ -26,23 +31,9 @@
26
31
  );
27
32
 
28
33
  onMount(() => {
29
- // Dark mode detection priority: inline color-scheme style > .dark class > OS media query
30
- const mq = window.matchMedia('(prefers-color-scheme: dark)');
31
- const html = document.documentElement;
32
-
33
- function updateDark() {
34
- const explicit = html.style.colorScheme;
35
- if (explicit === 'dark' || explicit === 'light') {
36
- isDark = explicit === 'dark';
37
- } else {
38
- isDark = html.classList.contains('dark') || mq.matches;
39
- }
40
- }
41
-
42
- updateDark();
43
- mq.addEventListener('change', updateDark);
44
- const observer = new MutationObserver(updateDark);
45
- observer.observe(html, { attributeFilter: ['class', 'style'] });
34
+ const stopWatchingTheme = watchSkeletonDarkMode(container, (nextIsDark) => {
35
+ isDark = nextIsDark;
36
+ });
46
37
 
47
38
  editor = monaco.editor.create(container!, {
48
39
  value: '',
@@ -73,8 +64,7 @@
73
64
  insertHtml = (html: string) => doInsertText(html.replace(/<[^>]+>/g, ''));
74
65
 
75
66
  return () => {
76
- mq.removeEventListener('change', updateDark);
77
- observer.disconnect();
67
+ stopWatchingTheme();
78
68
  insertText = undefined;
79
69
  insertHtml = undefined;
80
70
  editor?.dispose();
@@ -2,6 +2,7 @@
2
2
  import { onMount, untrack, tick } from "svelte";
3
3
  import type { Office365EditorProps } from "../types.js";
4
4
  import { getExtFromFilename } from "../utils.js";
5
+ import { SvelteURLSearchParams } from "svelte/reactivity";
5
6
 
6
7
  let {
7
8
  value,
@@ -71,7 +72,8 @@
71
72
  // ── WOPI helpers ─────────────────────────────────────────────────────────────
72
73
 
73
74
  async function fileExistsInWopi(token: string): Promise<boolean> {
74
- const params = new URLSearchParams({ access_token: token });
75
+ const params = new SvelteURLSearchParams();
76
+ params.append("access_token", token);
75
77
  const base = wopiUrl.replace(/\/$/, "");
76
78
  const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
77
79
  headers,
@@ -81,7 +83,8 @@
81
83
  }
82
84
 
83
85
  async function uploadToWopi(blob: Blob, token: string): Promise<void> {
84
- const params = new URLSearchParams({ access_token: token });
86
+ const params = new SvelteURLSearchParams();
87
+ params.append("access_token", token);
85
88
  if (filename !== undefined) params.append("filename", filename);
86
89
  const base = wopiUrl.replace(/\/$/, "");
87
90
  const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
@@ -93,7 +96,8 @@
93
96
  }
94
97
 
95
98
  async function downloadFromWopi(): Promise<Blob | undefined> {
96
- const params = new URLSearchParams({ access_token: signedToken });
99
+ const params = new SvelteURLSearchParams();
100
+ params.append("access_token", signedToken);
97
101
  const base = wopiUrl.replace(/\/$/, "");
98
102
  const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
99
103
  headers,
@@ -36,14 +36,13 @@
36
36
  >
37
37
  <div class="flex h-full w-full flex-col items-center justify-center gap-3 p-6 text-center text-surface-700-300">
38
38
  <p class="text-sm opacity-70">This browser could not display the PDF inline.</p>
39
- <a
40
- href={src}
41
- target="_blank"
42
- rel="noopener noreferrer"
39
+ <button
40
+ type="button"
43
41
  class="btn variant-outline"
42
+ onclick={() => window.open(src, '_blank', 'noopener,noreferrer')}
44
43
  >
45
44
  Open PDF in new tab
46
- </a>
45
+ </button>
47
46
  </div>
48
47
  </object>
49
48
  {:else}