@warkypublic/svelix 0.1.17 → 0.1.18
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/dist/components/ContentEditor/ContentEditor.svelte +98 -0
- package/dist/components/ContentEditor/ContentEditor.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/index.d.ts +2 -0
- package/dist/components/ContentEditor/index.js +2 -0
- package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +45 -0
- package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +221 -0
- package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +9 -0
- package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +33 -0
- package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +222 -0
- package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte.d.ts +11 -0
- package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +103 -0
- package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +54 -0
- package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +340 -0
- package/dist/components/ContentEditor/subcomponents/TextEditor.svelte.d.ts +5 -0
- package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +184 -0
- package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +46 -0
- package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +9 -0
- package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte.d.ts +4 -0
- package/dist/components/ContentEditor/tofix.md +9 -0
- package/dist/components/ContentEditor/types.d.ts +73 -0
- package/dist/components/ContentEditor/types.js +1 -0
- package/dist/components/ContentEditor/utils.d.ts +12 -0
- package/dist/components/ContentEditor/utils.js +108 -0
- package/dist/components/Svark/Svark.svelte +48 -7
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/package.json +15 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ContentEditorProps } from "./types.js";
|
|
3
|
+
import { getEditorType } from "./utils.js";
|
|
4
|
+
import MonacoEditor from "./subcomponents/MonacoEditor.svelte";
|
|
5
|
+
import ImageViewer from "./subcomponents/ImageViewer.svelte";
|
|
6
|
+
import AudioPlayer from "./subcomponents/AudioPlayer.svelte";
|
|
7
|
+
import VideoPlayer from "./subcomponents/VideoPlayer.svelte";
|
|
8
|
+
import TextEditor from "./subcomponents/TextEditor.svelte";
|
|
9
|
+
import MarkdownViewer from "./subcomponents/MarkdownViewer.svelte";
|
|
10
|
+
import PdfViewer from "./subcomponents/PdfViewer.svelte";
|
|
11
|
+
import EmailViewer from "./subcomponents/EmailViewer.svelte";
|
|
12
|
+
import ZipViewer from "./subcomponents/ZipViewer.svelte";
|
|
13
|
+
import CollaboraEditor from "./subcomponents/CollaboraEditor.svelte";
|
|
14
|
+
import UnknownFile from "./subcomponents/UnknownFile.svelte";
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
value,
|
|
18
|
+
filename,
|
|
19
|
+
contentType,
|
|
20
|
+
mode = "value",
|
|
21
|
+
readonly = false,
|
|
22
|
+
onChange,
|
|
23
|
+
onSave,
|
|
24
|
+
onUpload,
|
|
25
|
+
wopiUrl,
|
|
26
|
+
discoverUrl,
|
|
27
|
+
wopiToken,
|
|
28
|
+
userid,
|
|
29
|
+
username,
|
|
30
|
+
viewtype,
|
|
31
|
+
}: ContentEditorProps = $props();
|
|
32
|
+
|
|
33
|
+
const editorType = $derived(getEditorType({ filename, contentType }));
|
|
34
|
+
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (mode === "filepointer") {
|
|
37
|
+
console.warn(
|
|
38
|
+
'[ContentEditor] mode="filepointer" is not yet implemented; falling back to value mode.',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<div
|
|
45
|
+
style="display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 200px; overflow: hidden;"
|
|
46
|
+
>
|
|
47
|
+
{#if editorType === "monaco"}
|
|
48
|
+
<MonacoEditor
|
|
49
|
+
{value}
|
|
50
|
+
{filename}
|
|
51
|
+
{readonly}
|
|
52
|
+
{onChange}
|
|
53
|
+
{onSave}
|
|
54
|
+
height="100%"
|
|
55
|
+
/>
|
|
56
|
+
{:else if editorType === "image"}
|
|
57
|
+
<ImageViewer {value} {filename} />
|
|
58
|
+
{:else if editorType === "audio"}
|
|
59
|
+
<AudioPlayer {value} {filename} />
|
|
60
|
+
{:else if editorType === "video"}
|
|
61
|
+
<VideoPlayer {value} {filename} />
|
|
62
|
+
{:else if editorType === "text"}
|
|
63
|
+
<TextEditor {value} {filename} {readonly} {onChange} />
|
|
64
|
+
{:else if editorType === "markdown"}
|
|
65
|
+
<MarkdownViewer {value} {filename} {readonly} {onChange} {onUpload} />
|
|
66
|
+
{:else if editorType === "pdf"}
|
|
67
|
+
<PdfViewer {value} {filename} />
|
|
68
|
+
{:else if editorType === "email"}
|
|
69
|
+
<EmailViewer {value} {filename} />
|
|
70
|
+
{:else if editorType === "zip"}
|
|
71
|
+
<ZipViewer {value} {filename} />
|
|
72
|
+
{:else if editorType === "collabora"}
|
|
73
|
+
{#if wopiUrl && discoverUrl}
|
|
74
|
+
<CollaboraEditor
|
|
75
|
+
{value}
|
|
76
|
+
{filename}
|
|
77
|
+
{readonly}
|
|
78
|
+
{onChange}
|
|
79
|
+
{onSave}
|
|
80
|
+
{wopiUrl}
|
|
81
|
+
{discoverUrl}
|
|
82
|
+
{wopiToken}
|
|
83
|
+
{userid}
|
|
84
|
+
{username}
|
|
85
|
+
{viewtype}
|
|
86
|
+
/>
|
|
87
|
+
{:else}
|
|
88
|
+
<UnknownFile
|
|
89
|
+
{value}
|
|
90
|
+
{filename}
|
|
91
|
+
{contentType}
|
|
92
|
+
note="wopiUrl and discoverUrl are required for office documents."
|
|
93
|
+
/>
|
|
94
|
+
{/if}
|
|
95
|
+
{:else}
|
|
96
|
+
<UnknownFile {value} {filename} {contentType} />
|
|
97
|
+
{/if}
|
|
98
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AudioPlayerProps } from '../types.js';
|
|
3
|
+
let { value, filename }: AudioPlayerProps = $props();
|
|
4
|
+
|
|
5
|
+
let src = $state('');
|
|
6
|
+
let srcType = $state<string | undefined>(undefined);
|
|
7
|
+
let err = $state<string | null>(null);
|
|
8
|
+
|
|
9
|
+
$effect(() => {
|
|
10
|
+
if (!value) { src = ''; srcType = undefined; return; }
|
|
11
|
+
err = null;
|
|
12
|
+
const url = URL.createObjectURL(value);
|
|
13
|
+
src = url;
|
|
14
|
+
srcType = value.type || undefined;
|
|
15
|
+
return () => URL.revokeObjectURL(url);
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class="flex h-full w-full flex-col items-center justify-center gap-4 p-6">
|
|
20
|
+
{#if err}
|
|
21
|
+
<div class="flex items-center gap-2 rounded-container-token bg-error-500/10 px-4 py-3 text-sm text-error-700-300">
|
|
22
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
23
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
|
24
|
+
</svg>
|
|
25
|
+
{err}
|
|
26
|
+
</div>
|
|
27
|
+
{/if}
|
|
28
|
+
|
|
29
|
+
{#if filename}
|
|
30
|
+
<p class="text-sm font-medium text-surface-700-300 opacity-70">{filename}</p>
|
|
31
|
+
{/if}
|
|
32
|
+
|
|
33
|
+
{#if src}
|
|
34
|
+
<!-- svelte-ignore a11y_media_has_caption -->
|
|
35
|
+
<audio
|
|
36
|
+
controls
|
|
37
|
+
autoplay
|
|
38
|
+
crossorigin="anonymous"
|
|
39
|
+
onerror={() => { err = 'Playback error — the format may not be supported by this browser.'; }}
|
|
40
|
+
class="w-full max-w-xl"
|
|
41
|
+
>
|
|
42
|
+
<source {src} type={srcType} />
|
|
43
|
+
</audio>
|
|
44
|
+
{/if}
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
|
|
6
|
+
let {
|
|
7
|
+
value,
|
|
8
|
+
filename,
|
|
9
|
+
readonly = false,
|
|
10
|
+
wopiUrl,
|
|
11
|
+
discoverUrl,
|
|
12
|
+
wopiToken = '',
|
|
13
|
+
userid = 0,
|
|
14
|
+
username = 'user',
|
|
15
|
+
viewtype = 'tabbed',
|
|
16
|
+
headers,
|
|
17
|
+
onChange,
|
|
18
|
+
onSave,
|
|
19
|
+
}: CollaboraEditorProps = $props();
|
|
20
|
+
|
|
21
|
+
let frameEl = $state<HTMLIFrameElement | undefined>(undefined);
|
|
22
|
+
let ready = $state(false);
|
|
23
|
+
let loading = $state(false);
|
|
24
|
+
let errmsg = $state('');
|
|
25
|
+
|
|
26
|
+
// Stable IDs for this editor instance — untrack prevents Svelte from treating
|
|
27
|
+
// these as reactive dependencies (they are intentional one-time reads).
|
|
28
|
+
const editorId = `collabora_${crypto.randomUUID().slice(0, 8)}`;
|
|
29
|
+
const fileId = `${untrack(() => userid)}_${crypto.randomUUID()}`;
|
|
30
|
+
|
|
31
|
+
// Mutable buffer — not reactive; mirrors React's useRef<Blob>
|
|
32
|
+
let buffer: Blob | undefined = untrack(() => value);
|
|
33
|
+
let saveRequestComplete = true;
|
|
34
|
+
|
|
35
|
+
const contentType = $derived(getMimeFromExt(filename ? getExtFromFilename(filename) : ''));
|
|
36
|
+
|
|
37
|
+
// ── WOPI helpers ────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
async function uploadToWopi(blob: Blob): Promise<void> {
|
|
40
|
+
const params = new URLSearchParams();
|
|
41
|
+
params.append('access_token', wopiToken);
|
|
42
|
+
if (filename !== undefined) {
|
|
43
|
+
params.append('filename', filename);
|
|
44
|
+
params.append('contenttype', getMimeFromExt(getExtFromFilename(filename)));
|
|
45
|
+
}
|
|
46
|
+
if (userid !== undefined) params.append('userid', String(userid));
|
|
47
|
+
if (username !== undefined) params.append('username', username);
|
|
48
|
+
|
|
49
|
+
let url = wopiUrl;
|
|
50
|
+
if (url.endsWith('/')) url = url.substring(0, url.length - 1);
|
|
51
|
+
const fullurl = `${url.trimEnd()}/files/${fileId}/contents?${params.toString()}`;
|
|
52
|
+
const res = await fetch(fullurl, { headers, method: 'POST', body: blob });
|
|
53
|
+
if (!res.ok) throw new Error(`WOPI upload failed: ${res.status}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function downloadFromWopi(): Promise<Blob | undefined> {
|
|
57
|
+
const params = new URLSearchParams();
|
|
58
|
+
params.append('access_token', wopiToken);
|
|
59
|
+
if (userid !== undefined) params.append('userid', String(userid));
|
|
60
|
+
|
|
61
|
+
let url = wopiUrl;
|
|
62
|
+
if (url.endsWith('/')) url = url.substring(0, url.length - 1);
|
|
63
|
+
const fullurl = `${url.trimEnd()}/files/${fileId}/contents?${params.toString()}`;
|
|
64
|
+
const res = await fetch(fullurl, { headers, method: 'GET' });
|
|
65
|
+
if (res.ok) return res.blob();
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Discovery ────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
async function findEditorUrl(mimeType: string): Promise<string> {
|
|
72
|
+
const res = await fetch(discoverUrl);
|
|
73
|
+
if (!res.ok) throw new Error(`Discovery request failed: ${res.status}`);
|
|
74
|
+
const xml = await res.text();
|
|
75
|
+
const doc = new DOMParser().parseFromString(xml, 'text/xml');
|
|
76
|
+
const action = doc.querySelector(`app[name="${mimeType}"] action`);
|
|
77
|
+
const urlsrc = action?.getAttribute('urlsrc');
|
|
78
|
+
if (!urlsrc) throw new Error(`No Collabora editor found for MIME type: ${mimeType}`);
|
|
79
|
+
const params = new URLSearchParams({ WOPISrc: `${wopiUrl.replace(/\/$/, '')}/files/${fileId}` });
|
|
80
|
+
return `${urlsrc}${params}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Load ─────────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
async function loadEditor(): Promise<void> {
|
|
86
|
+
if (!filename || !buffer) return;
|
|
87
|
+
errmsg = '';
|
|
88
|
+
loading = true;
|
|
89
|
+
ready = false;
|
|
90
|
+
try {
|
|
91
|
+
await uploadToWopi(buffer);
|
|
92
|
+
const url = await findEditorUrl(contentType);
|
|
93
|
+
// Navigate iframe to Collabora — Collabora will fetch the file from WOPI
|
|
94
|
+
if (frameEl) frameEl.src = url;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
errmsg = String(e);
|
|
97
|
+
loading = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Save flow ─────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function sendSaveAction(): Promise<void> {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
if (!frameEl?.contentWindow) { reject(new Error('No iframe')); return; }
|
|
106
|
+
saveRequestComplete = false;
|
|
107
|
+
frameEl.contentWindow.postMessage(JSON.stringify({
|
|
108
|
+
MessageId: 'Action_Save',
|
|
109
|
+
SendTime: Date.now(),
|
|
110
|
+
Values: { Notify: true, DontSaveIfUnmodified: false, DontTerminateEdit: false },
|
|
111
|
+
}), '*');
|
|
112
|
+
let cnt = 0;
|
|
113
|
+
const itv = setInterval(() => {
|
|
114
|
+
cnt++;
|
|
115
|
+
if (saveRequestComplete) { clearInterval(itv); resolve(); }
|
|
116
|
+
if (cnt > 60) { clearInterval(itv); reject(new Error('Save timeout after 30s')); }
|
|
117
|
+
}, 500);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function handleSaveResponse(): void {
|
|
122
|
+
setTimeout(async () => {
|
|
123
|
+
try {
|
|
124
|
+
const blob = await downloadFromWopi();
|
|
125
|
+
if (blob) {
|
|
126
|
+
buffer = blob;
|
|
127
|
+
onChange?.(blob);
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
errmsg = `Could not retrieve saved file: ${e}`;
|
|
131
|
+
} finally {
|
|
132
|
+
saveRequestComplete = true;
|
|
133
|
+
onSave?.(buffer ?? new Blob([]));
|
|
134
|
+
}
|
|
135
|
+
}, 100);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── PostMessage handler ───────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function handlePostMessage(e: MessageEvent): void {
|
|
141
|
+
try {
|
|
142
|
+
const msg =
|
|
143
|
+
typeof e.data === 'string' ? JSON.parse(e.data) :
|
|
144
|
+
typeof e.data === 'object' ? e.data : null;
|
|
145
|
+
if (!msg?.MessageId || !frameEl?.contentWindow) return;
|
|
146
|
+
|
|
147
|
+
const msgId: string = msg.MessageId;
|
|
148
|
+
const msgData = msg.Values;
|
|
149
|
+
|
|
150
|
+
if (msgId === 'App_LoadingStatus' && msgData?.Status === 'Frame_Ready') {
|
|
151
|
+
frameEl.contentWindow.postMessage(
|
|
152
|
+
JSON.stringify({ MessageId: 'Host_PostmessageReady', SendTime: Date.now() }),
|
|
153
|
+
'*'
|
|
154
|
+
);
|
|
155
|
+
ready = true;
|
|
156
|
+
loading = false;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (msgId === 'UI_Save') { sendSaveAction(); return; }
|
|
160
|
+
if (msgId === 'Doc_ModifiedStatus' && msgData?.Modified) { handleSaveResponse(); return; }
|
|
161
|
+
if (msgId === 'Action_Save_Resp' && msgData?.success) { handleSaveResponse(); }
|
|
162
|
+
} catch {
|
|
163
|
+
// ignore malformed messages from other origins
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
onMount(() => {
|
|
168
|
+
window.addEventListener('message', handlePostMessage);
|
|
169
|
+
return () => window.removeEventListener('message', handlePostMessage);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── Reactive reload ───────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
$effect(() => {
|
|
175
|
+
// Track value, filename and WOPI config as dependencies.
|
|
176
|
+
// Update buffer when value prop changes, then reload.
|
|
177
|
+
const v = value;
|
|
178
|
+
const f = filename;
|
|
179
|
+
void wopiUrl;
|
|
180
|
+
void discoverUrl;
|
|
181
|
+
|
|
182
|
+
if (v !== undefined) buffer = v;
|
|
183
|
+
if (f && buffer) void loadEditor();
|
|
184
|
+
});
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<div class="relative flex h-full w-full flex-col overflow-hidden">
|
|
188
|
+
<!-- Error state -->
|
|
189
|
+
{#if errmsg}
|
|
190
|
+
<div class="flex items-start gap-3 bg-error-500/10 px-4 py-3 text-sm text-error-700-300">
|
|
191
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="mt-0.5 size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
192
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
|
193
|
+
</svg>
|
|
194
|
+
<span>{errmsg}</span>
|
|
195
|
+
</div>
|
|
196
|
+
{/if}
|
|
197
|
+
|
|
198
|
+
<!-- Loading overlay -->
|
|
199
|
+
{#if loading && !errmsg}
|
|
200
|
+
<div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-50-950/80 backdrop-blur-sm">
|
|
201
|
+
<div class="flex flex-col items-center gap-3">
|
|
202
|
+
<svg class="size-8 animate-spin text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
203
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
204
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
|
|
205
|
+
</svg>
|
|
206
|
+
<span class="text-sm text-surface-700-300">Loading editor…</span>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
{/if}
|
|
210
|
+
|
|
211
|
+
<!-- Collabora iframe — src is set imperatively via loadEditor() -->
|
|
212
|
+
<iframe
|
|
213
|
+
bind:this={frameEl}
|
|
214
|
+
id="{editorId}-viewer"
|
|
215
|
+
name="{editorId}-viewer"
|
|
216
|
+
title="Collabora Online Editor"
|
|
217
|
+
class="h-full w-full flex-1 border-0"
|
|
218
|
+
style:visibility={ready && !loading ? 'visible' : 'hidden'}
|
|
219
|
+
style:min-height="300px"
|
|
220
|
+
></iframe>
|
|
221
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ImageViewerProps } from '../types.js';
|
|
3
|
+
let { value, filename }: ImageViewerProps = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div class="card p-4 text-surface-700-300">
|
|
7
|
+
<p class="font-semibold">EmailViewer</p>
|
|
8
|
+
<p class="text-sm opacity-60">Not yet implemented. filename: {filename}</p>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ImageViewerProps } from '../types.js';
|
|
3
|
+
let { value, filename }: ImageViewerProps = $props();
|
|
4
|
+
|
|
5
|
+
let src = $state('');
|
|
6
|
+
let err = $state(false);
|
|
7
|
+
|
|
8
|
+
$effect(() => {
|
|
9
|
+
if (!value) { src = ''; return; }
|
|
10
|
+
err = false;
|
|
11
|
+
const url = URL.createObjectURL(value);
|
|
12
|
+
src = url;
|
|
13
|
+
return () => URL.revokeObjectURL(url);
|
|
14
|
+
});
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="relative flex h-full w-full items-center justify-center overflow-hidden bg-surface-50-950">
|
|
18
|
+
{#if err}
|
|
19
|
+
<div class="flex flex-col items-center gap-2 text-error-500">
|
|
20
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="size-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
21
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
|
22
|
+
</svg>
|
|
23
|
+
<span class="text-sm">Failed to load image</span>
|
|
24
|
+
</div>
|
|
25
|
+
{:else if src}
|
|
26
|
+
<img
|
|
27
|
+
{src}
|
|
28
|
+
alt={filename ?? 'image'}
|
|
29
|
+
class="max-h-full max-w-full object-contain"
|
|
30
|
+
onerror={() => { err = true; }}
|
|
31
|
+
/>
|
|
32
|
+
{/if}
|
|
33
|
+
</div>
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { Carta, Markdown, MarkdownEditor } from 'carta-md';
|
|
4
|
+
import { code } from '@cartamd/plugin-code';
|
|
5
|
+
import { anchor } from '@cartamd/plugin-anchor';
|
|
6
|
+
import { attachment } from '@cartamd/plugin-attachment';
|
|
7
|
+
import { emoji } from '@cartamd/plugin-emoji';
|
|
8
|
+
import { math } from '@cartamd/plugin-math';
|
|
9
|
+
import { sanitize } from 'isomorphic-dompurify';
|
|
10
|
+
import 'carta-md/default.css';
|
|
11
|
+
import '@cartamd/plugin-code/default.css';
|
|
12
|
+
import '@cartamd/plugin-anchor/default.css';
|
|
13
|
+
import '@cartamd/plugin-emoji/default.css';
|
|
14
|
+
import '@cartamd/plugin-attachment/default.css';
|
|
15
|
+
import 'katex/dist/katex.min.css';
|
|
16
|
+
import 'github-markdown-css/github-markdown.css';
|
|
17
|
+
import { blobToString } from '../utils.js';
|
|
18
|
+
import type { MarkdownViewerProps } from '../types.js';
|
|
19
|
+
|
|
20
|
+
let { value, readonly = false, onChange, onUpload }: MarkdownViewerProps = $props();
|
|
21
|
+
|
|
22
|
+
let carta = $state<Carta | undefined>(undefined);
|
|
23
|
+
|
|
24
|
+
onMount(() => {
|
|
25
|
+
carta = new Carta({
|
|
26
|
+
sanitizer: sanitize,
|
|
27
|
+
extensions: [
|
|
28
|
+
code(),
|
|
29
|
+
anchor(),
|
|
30
|
+
emoji(),
|
|
31
|
+
math(),
|
|
32
|
+
...(onUpload ? [attachment({ upload: onUpload })] : []),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let text = $state('');
|
|
38
|
+
let lastBlobText = $state('');
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
blobToString(value).then((v) => {
|
|
42
|
+
lastBlobText = v;
|
|
43
|
+
if (text !== v) text = v;
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
$effect(() => {
|
|
48
|
+
if (!readonly && text !== lastBlobText) {
|
|
49
|
+
onChange?.(new Blob([text]));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<div class="md-host" style="display: flex; flex-direction: column; width: 100%; height: 100%;">
|
|
55
|
+
{#if carta}
|
|
56
|
+
{#if readonly}
|
|
57
|
+
{#key text}
|
|
58
|
+
<Markdown {carta} value={text} />
|
|
59
|
+
{/key}
|
|
60
|
+
{:else}
|
|
61
|
+
<MarkdownEditor {carta} bind:value={text} mode="auto" />
|
|
62
|
+
{/if}
|
|
63
|
+
{/if}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
:global(.carta-font-code) {
|
|
68
|
+
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
69
|
+
font-size: 0.9rem;
|
|
70
|
+
line-height: 1.5;
|
|
71
|
+
letter-spacing: normal;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:global(.md-host .carta-editor) {
|
|
75
|
+
flex: 1 1 auto;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:global(.md-host .carta-viewer) {
|
|
79
|
+
padding: 1rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Shiki dual-theme: explicitly apply light variables (required for preview rendering) */
|
|
83
|
+
:global(.shiki),
|
|
84
|
+
:global(.shiki span) {
|
|
85
|
+
color: var(--shiki-light);
|
|
86
|
+
background-color: var(--shiki-light-bg);
|
|
87
|
+
font-style: var(--shiki-light-font-style);
|
|
88
|
+
font-weight: var(--shiki-light-font-weight);
|
|
89
|
+
text-decoration: var(--shiki-light-text-decoration);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Shiki dual-theme: dark colors only when .dark class is present */
|
|
93
|
+
:global(.dark .shiki),
|
|
94
|
+
:global(.dark .shiki span) {
|
|
95
|
+
color: var(--shiki-dark) !important;
|
|
96
|
+
background-color: var(--shiki-dark-bg) !important;
|
|
97
|
+
font-style: var(--shiki-dark-font-style) !important;
|
|
98
|
+
font-weight: var(--shiki-dark-font-weight) !important;
|
|
99
|
+
text-decoration: var(--shiki-dark-text-decoration) !important;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* carta-md dark mode: remap dark variable aliases when .dark is active */
|
|
103
|
+
:global(.dark .carta-theme__default) {
|
|
104
|
+
--border-color: var(--border-color-dark);
|
|
105
|
+
--selection-color: var(--selection-color-dark);
|
|
106
|
+
--focus-outline: var(--focus-outline-dark);
|
|
107
|
+
--hover-color: var(--hover-color-dark);
|
|
108
|
+
--caret-color: var(--caret-color-dark);
|
|
109
|
+
--text-color: var(--text-color-dark);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* github-markdown-css dark mode: override CSS vars when .dark class is active.
|
|
113
|
+
github-markdown.css uses @media (prefers-color-scheme) which ignores the .dark class. */
|
|
114
|
+
:global(.dark .markdown-body) {
|
|
115
|
+
color-scheme: dark;
|
|
116
|
+
--fgColor-default: #f0f6fc;
|
|
117
|
+
--fgColor-muted: #9198a1;
|
|
118
|
+
--fgColor-accent: #4493f8;
|
|
119
|
+
--fgColor-attention: #d29922;
|
|
120
|
+
--fgColor-danger: #f85149;
|
|
121
|
+
--fgColor-done: #ab7df8;
|
|
122
|
+
--fgColor-success: #3fb950;
|
|
123
|
+
--bgColor-default: #0d1117;
|
|
124
|
+
--bgColor-muted: #151b23;
|
|
125
|
+
--bgColor-attention-muted: #bb800926;
|
|
126
|
+
--bgColor-neutral-muted: #656c7633;
|
|
127
|
+
--borderColor-default: #3d444d;
|
|
128
|
+
--borderColor-muted: #3d444db3;
|
|
129
|
+
--borderColor-neutral-muted: var(--borderColor-muted);
|
|
130
|
+
--borderColor-accent-emphasis: #1f6feb;
|
|
131
|
+
--borderColor-attention-emphasis: #9e6a03;
|
|
132
|
+
--borderColor-danger-emphasis: #da3633;
|
|
133
|
+
--borderColor-done-emphasis: #8957e5;
|
|
134
|
+
--borderColor-success-emphasis: #238636;
|
|
135
|
+
--focus-outlineColor: var(--borderColor-accent-emphasis);
|
|
136
|
+
--color-prettylights-syntax-comment: #9198a1;
|
|
137
|
+
--color-prettylights-syntax-constant: #79c0ff;
|
|
138
|
+
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
|
|
139
|
+
--color-prettylights-syntax-entity: #d2a8ff;
|
|
140
|
+
--color-prettylights-syntax-entity-tag: #7ee787;
|
|
141
|
+
--color-prettylights-syntax-keyword: #ff7b72;
|
|
142
|
+
--color-prettylights-syntax-string: #a5d6ff;
|
|
143
|
+
--color-prettylights-syntax-string-regexp: #7ee787;
|
|
144
|
+
--color-prettylights-syntax-variable: #ffa657;
|
|
145
|
+
--color-prettylights-syntax-storage-modifier-import: #f0f6fc;
|
|
146
|
+
--color-prettylights-syntax-markup-heading: #1f6feb;
|
|
147
|
+
--color-prettylights-syntax-markup-italic: #f0f6fc;
|
|
148
|
+
--color-prettylights-syntax-markup-bold: #f0f6fc;
|
|
149
|
+
--color-prettylights-syntax-markup-list: #f2cc60;
|
|
150
|
+
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
|
|
151
|
+
--color-prettylights-syntax-markup-deleted-bg: #67060c;
|
|
152
|
+
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
|
|
153
|
+
--color-prettylights-syntax-markup-inserted-bg: #033a16;
|
|
154
|
+
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
|
|
155
|
+
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
|
|
156
|
+
--color-prettylights-syntax-markup-ignored-text: #f0f6fc;
|
|
157
|
+
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
|
|
158
|
+
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
|
|
159
|
+
--color-prettylights-syntax-brackethighlighter-angle: #9198a1;
|
|
160
|
+
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
|
|
161
|
+
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
|
|
162
|
+
--color-prettylights-syntax-carriage-return-bg: #b62324;
|
|
163
|
+
--color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d;
|
|
164
|
+
--color-prettylights-syntax-invalid-illegal-text: var(--fgColor-danger);
|
|
165
|
+
--color-prettylights-syntax-invalid-illegal-bg: var(--bgColor-danger-muted);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* github-markdown-css light mode: force light vars when OS may be dark but app is light */
|
|
169
|
+
:global(html:not(.dark) .markdown-body) {
|
|
170
|
+
color-scheme: light;
|
|
171
|
+
--fgColor-default: #1f2328;
|
|
172
|
+
--fgColor-muted: #59636e;
|
|
173
|
+
--fgColor-accent: #0969da;
|
|
174
|
+
--fgColor-attention: #9a6700;
|
|
175
|
+
--fgColor-danger: #d1242f;
|
|
176
|
+
--fgColor-done: #8250df;
|
|
177
|
+
--fgColor-success: #1a7f37;
|
|
178
|
+
--bgColor-default: #ffffff;
|
|
179
|
+
--bgColor-muted: #f6f8fa;
|
|
180
|
+
--bgColor-attention-muted: #fff8c5;
|
|
181
|
+
--bgColor-neutral-muted: #818b981f;
|
|
182
|
+
--borderColor-default: #d1d9e0;
|
|
183
|
+
--borderColor-muted: #d1d9e0b3;
|
|
184
|
+
--borderColor-neutral-muted: var(--borderColor-muted);
|
|
185
|
+
--borderColor-accent-emphasis: #0969da;
|
|
186
|
+
--borderColor-attention-emphasis: #9a6700;
|
|
187
|
+
--borderColor-danger-emphasis: #cf222e;
|
|
188
|
+
--borderColor-done-emphasis: #8250df;
|
|
189
|
+
--borderColor-success-emphasis: #1a7f37;
|
|
190
|
+
--focus-outlineColor: var(--borderColor-accent-emphasis);
|
|
191
|
+
--color-prettylights-syntax-comment: #59636e;
|
|
192
|
+
--color-prettylights-syntax-constant: #0550ae;
|
|
193
|
+
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
|
|
194
|
+
--color-prettylights-syntax-entity: #6639ba;
|
|
195
|
+
--color-prettylights-syntax-entity-tag: #0550ae;
|
|
196
|
+
--color-prettylights-syntax-keyword: #cf222e;
|
|
197
|
+
--color-prettylights-syntax-string: #0a3069;
|
|
198
|
+
--color-prettylights-syntax-string-regexp: #116329;
|
|
199
|
+
--color-prettylights-syntax-variable: #953800;
|
|
200
|
+
--color-prettylights-syntax-storage-modifier-import: #1f2328;
|
|
201
|
+
--color-prettylights-syntax-markup-heading: #0550ae;
|
|
202
|
+
--color-prettylights-syntax-markup-italic: #1f2328;
|
|
203
|
+
--color-prettylights-syntax-markup-bold: #1f2328;
|
|
204
|
+
--color-prettylights-syntax-markup-list: #3b2300;
|
|
205
|
+
--color-prettylights-syntax-markup-deleted-text: #82071e;
|
|
206
|
+
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
|
|
207
|
+
--color-prettylights-syntax-markup-inserted-text: #116329;
|
|
208
|
+
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
|
|
209
|
+
--color-prettylights-syntax-markup-changed-text: #953800;
|
|
210
|
+
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
|
|
211
|
+
--color-prettylights-syntax-markup-ignored-text: #d1d9e0;
|
|
212
|
+
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
|
|
213
|
+
--color-prettylights-syntax-meta-diff-range: #8250df;
|
|
214
|
+
--color-prettylights-syntax-brackethighlighter-angle: #59636e;
|
|
215
|
+
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
|
|
216
|
+
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
|
|
217
|
+
--color-prettylights-syntax-carriage-return-bg: #cf222e;
|
|
218
|
+
--color-prettylights-syntax-sublimelinter-gutter-mark: #818b98;
|
|
219
|
+
--color-prettylights-syntax-invalid-illegal-text: var(--fgColor-danger);
|
|
220
|
+
--color-prettylights-syntax-invalid-illegal-bg: var(--bgColor-danger-muted);
|
|
221
|
+
}
|
|
222
|
+
</style>
|