@open-slide/core 0.0.2 → 0.0.4
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/bin.js +2 -0
- package/dist/{build-DJGuOT6x.js → build-CuoESF2g.js} +1 -1
- package/dist/cli/bin.js +5 -5
- package/dist/config-DF58h0l4.js +641 -0
- package/dist/{dev-0SG0ArzD.js → dev-rlOZacWo.js} +1 -1
- package/dist/index.d.ts +7 -9
- package/dist/{preview-61Aawrlg.js → preview-DCrD9X36.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +7 -4
- package/src/app/App.tsx +2 -2
- package/src/app/components/ClickNavZones.tsx +34 -0
- package/src/app/components/Player.tsx +26 -7
- package/src/app/components/ThumbnailRail.tsx +5 -5
- package/src/app/components/inspector/CommentPopover.tsx +3 -11
- package/src/app/components/inspector/InspectOverlay.tsx +15 -4
- package/src/app/components/inspector/InspectorProvider.tsx +12 -5
- package/src/app/components/sidebar/FolderItem.tsx +188 -0
- package/src/app/components/sidebar/IconPicker.tsx +59 -0
- package/src/app/components/sidebar/Sidebar.tsx +118 -0
- package/src/app/components/ui/dialog.tsx +141 -0
- package/src/app/components/ui/dropdown-menu.tsx +228 -0
- package/src/app/components/ui/popover.tsx +72 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/lib/export-html.ts +313 -0
- package/src/app/lib/folders.ts +166 -0
- package/src/app/lib/inspector/fiber.ts +2 -2
- package/src/app/lib/inspector/useComments.ts +8 -8
- package/src/app/lib/sdk.ts +18 -5
- package/src/app/lib/slides.ts +8 -0
- package/src/app/routes/Home.tsx +540 -63
- package/src/app/routes/Slide.tsx +298 -0
- package/src/app/virtual.d.ts +4 -4
- package/dist/config-Opp2R1Jf.js +0 -335
- package/src/app/lib/decks.ts +0 -8
- package/src/app/routes/Deck.tsx +0 -185
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// Exports a slide as a standalone HTML file (or a .zip bundle if the slide
|
|
2
|
+
// references bundled assets). The export is a static snapshot of each page's
|
|
3
|
+
// post-mount DOM — runtime interactivity (useState click handlers, timers,
|
|
4
|
+
// etc.) is captured at snapshot time only.
|
|
5
|
+
|
|
6
|
+
import { createElement } from 'react';
|
|
7
|
+
import { createRoot } from 'react-dom/client';
|
|
8
|
+
import type { SlideModule } from './sdk';
|
|
9
|
+
|
|
10
|
+
type AssetEntry = { name: string; bytes: Uint8Array };
|
|
11
|
+
|
|
12
|
+
const ASSET_EXT_RE = /\.(?:png|jpe?g|gif|svg|webp|avif|mp4|webm|mov|woff2?|ttf|otf|mp3|wav|ogg)(?:\?[^#]*)?(?:#.*)?$/i;
|
|
13
|
+
|
|
14
|
+
export async function exportSlideAsHtml(slide: SlideModule, slideId: string): Promise<void> {
|
|
15
|
+
const pages = slide.default ?? [];
|
|
16
|
+
if (pages.length === 0) return;
|
|
17
|
+
const title = slide.meta?.title ?? slideId;
|
|
18
|
+
|
|
19
|
+
const pagesHtml = await renderPagesToHtml(pages);
|
|
20
|
+
const bundledCss = collectCss();
|
|
21
|
+
const externalLinks = collectExternalStylesheetLinks();
|
|
22
|
+
|
|
23
|
+
const assets = new Map<string, AssetEntry>();
|
|
24
|
+
const usedNames = new Set<string>();
|
|
25
|
+
|
|
26
|
+
const urls = new Set<string>([
|
|
27
|
+
...findHtmlAssetUrls(pagesHtml.join('\n')),
|
|
28
|
+
...findCssAssetUrls(bundledCss),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
for (const url of urls) {
|
|
32
|
+
const absolute = toAbsolute(url);
|
|
33
|
+
if (!absolute) continue;
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(absolute);
|
|
36
|
+
if (!res.ok) continue;
|
|
37
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
38
|
+
const name = uniqueAssetName(absolute, usedNames);
|
|
39
|
+
assets.set(url, { name, bytes: buf });
|
|
40
|
+
} catch {
|
|
41
|
+
// ignore unreachable assets
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rewrittenPages = pagesHtml.map((html) => rewriteUrls(html, assets, 'html'));
|
|
46
|
+
const rewrittenCss = rewriteUrls(bundledCss, assets, 'css');
|
|
47
|
+
|
|
48
|
+
const html = buildHtml({
|
|
49
|
+
title,
|
|
50
|
+
pagesHtml: rewrittenPages,
|
|
51
|
+
bundledCss: rewrittenCss,
|
|
52
|
+
externalLinks,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const htmlBytes = new TextEncoder().encode(html);
|
|
56
|
+
|
|
57
|
+
if (assets.size === 0) {
|
|
58
|
+
downloadBlob(new Blob([htmlBytes as BlobPart], { type: 'text/html' }), `${slideId}.html`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { zipSync } = await import('fflate');
|
|
63
|
+
const zipTree: Record<string, Uint8Array | Record<string, Uint8Array>> = {
|
|
64
|
+
[`${slideId}.html`]: htmlBytes,
|
|
65
|
+
assets: {},
|
|
66
|
+
};
|
|
67
|
+
for (const { name, bytes } of assets.values()) {
|
|
68
|
+
(zipTree.assets as Record<string, Uint8Array>)[name] = bytes;
|
|
69
|
+
}
|
|
70
|
+
const zipped = zipSync(zipTree as Parameters<typeof zipSync>[0]);
|
|
71
|
+
downloadBlob(new Blob([zipped as BlobPart], { type: 'application/zip' }), `${slideId}.zip`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function renderPagesToHtml(pages: NonNullable<SlideModule['default']>): Promise<string[]> {
|
|
75
|
+
const container = document.createElement('div');
|
|
76
|
+
container.setAttribute('aria-hidden', 'true');
|
|
77
|
+
Object.assign(container.style, {
|
|
78
|
+
position: 'fixed',
|
|
79
|
+
left: '-99999px',
|
|
80
|
+
top: '0',
|
|
81
|
+
width: '1920px',
|
|
82
|
+
height: '1080px',
|
|
83
|
+
pointerEvents: 'none',
|
|
84
|
+
});
|
|
85
|
+
document.body.appendChild(container);
|
|
86
|
+
|
|
87
|
+
const result: string[] = [];
|
|
88
|
+
try {
|
|
89
|
+
for (const Page of pages) {
|
|
90
|
+
const host = document.createElement('div');
|
|
91
|
+
host.style.width = '1920px';
|
|
92
|
+
host.style.height = '1080px';
|
|
93
|
+
container.appendChild(host);
|
|
94
|
+
const root = createRoot(host);
|
|
95
|
+
root.render(createElement(Page));
|
|
96
|
+
await nextPaint();
|
|
97
|
+
await nextPaint();
|
|
98
|
+
result.push(host.innerHTML);
|
|
99
|
+
root.unmount();
|
|
100
|
+
container.removeChild(host);
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
container.remove();
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function nextPaint(): Promise<void> {
|
|
109
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function collectCss(): string {
|
|
113
|
+
const chunks: string[] = [];
|
|
114
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
115
|
+
let rules: CSSRuleList | null = null;
|
|
116
|
+
try {
|
|
117
|
+
rules = sheet.cssRules;
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!rules) continue;
|
|
122
|
+
for (const rule of Array.from(rules)) {
|
|
123
|
+
chunks.push(rule.cssText);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return chunks.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function collectExternalStylesheetLinks(): string {
|
|
130
|
+
const links: string[] = [];
|
|
131
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
132
|
+
try {
|
|
133
|
+
void sheet.cssRules;
|
|
134
|
+
} catch {
|
|
135
|
+
if (sheet.href) {
|
|
136
|
+
links.push(`<link rel="stylesheet" href="${escapeAttr(sheet.href)}">`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return links.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function findHtmlAssetUrls(html: string): string[] {
|
|
144
|
+
const out: string[] = [];
|
|
145
|
+
const attrRe = /\s(?:src|href)="([^"]+)"/g;
|
|
146
|
+
for (const m of html.matchAll(attrRe)) {
|
|
147
|
+
if (looksLikeAsset(m[1])) out.push(m[1]);
|
|
148
|
+
}
|
|
149
|
+
const srcsetRe = /\ssrcset="([^"]+)"/g;
|
|
150
|
+
for (const m of html.matchAll(srcsetRe)) {
|
|
151
|
+
for (const part of m[1].split(',')) {
|
|
152
|
+
const url = part.trim().split(/\s+/)[0];
|
|
153
|
+
if (url && looksLikeAsset(url)) out.push(url);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function findCssAssetUrls(css: string): string[] {
|
|
160
|
+
const out: string[] = [];
|
|
161
|
+
const re = /url\(\s*(['"]?)([^)'"]+)\1\s*\)/g;
|
|
162
|
+
for (const m of css.matchAll(re)) {
|
|
163
|
+
const url = m[2].trim();
|
|
164
|
+
if (looksLikeAsset(url)) out.push(url);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function looksLikeAsset(url: string): boolean {
|
|
170
|
+
if (!url) return false;
|
|
171
|
+
if (url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('#')) return false;
|
|
172
|
+
if (url.startsWith('mailto:') || url.startsWith('javascript:')) return false;
|
|
173
|
+
const abs = toAbsolute(url);
|
|
174
|
+
if (!abs) return false;
|
|
175
|
+
// Same-origin only: we can only fetch local assets.
|
|
176
|
+
try {
|
|
177
|
+
const u = new URL(abs);
|
|
178
|
+
if (u.origin !== window.location.origin) return false;
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return ASSET_EXT_RE.test(url);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function toAbsolute(url: string): string | null {
|
|
186
|
+
try {
|
|
187
|
+
return new URL(url, window.location.href).toString();
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function uniqueAssetName(absoluteUrl: string, used: Set<string>): string {
|
|
194
|
+
let base: string;
|
|
195
|
+
try {
|
|
196
|
+
const u = new URL(absoluteUrl);
|
|
197
|
+
base = u.pathname.split('/').pop() || 'asset';
|
|
198
|
+
} catch {
|
|
199
|
+
base = 'asset';
|
|
200
|
+
}
|
|
201
|
+
if (!used.has(base)) {
|
|
202
|
+
used.add(base);
|
|
203
|
+
return base;
|
|
204
|
+
}
|
|
205
|
+
const hash = shortHash(absoluteUrl);
|
|
206
|
+
const dot = base.lastIndexOf('.');
|
|
207
|
+
const name = dot > 0 ? `${base.slice(0, dot)}-${hash}${base.slice(dot)}` : `${base}-${hash}`;
|
|
208
|
+
used.add(name);
|
|
209
|
+
return name;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function shortHash(input: string): string {
|
|
213
|
+
let h = 2166136261;
|
|
214
|
+
for (let i = 0; i < input.length; i++) {
|
|
215
|
+
h ^= input.charCodeAt(i);
|
|
216
|
+
h = Math.imul(h, 16777619);
|
|
217
|
+
}
|
|
218
|
+
return (h >>> 0).toString(36).slice(0, 6);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function rewriteUrls(source: string, assets: Map<string, AssetEntry>, kind: 'html' | 'css'): string {
|
|
222
|
+
let out = source;
|
|
223
|
+
for (const [orig, { name }] of assets) {
|
|
224
|
+
const replacement = kind === 'css' ? `./assets/${name}` : `assets/${name}`;
|
|
225
|
+
out = out.split(orig).join(replacement);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildHtml(opts: {
|
|
231
|
+
title: string;
|
|
232
|
+
pagesHtml: string[];
|
|
233
|
+
bundledCss: string;
|
|
234
|
+
externalLinks: string;
|
|
235
|
+
}): string {
|
|
236
|
+
const pagesMarkup = opts.pagesHtml
|
|
237
|
+
.map((page, i) => `<div class="os-page" data-idx="${i}"${i === 0 ? '' : ' hidden'}>${page}</div>`)
|
|
238
|
+
.join('');
|
|
239
|
+
|
|
240
|
+
return `<!doctype html>
|
|
241
|
+
<html lang="en">
|
|
242
|
+
<head>
|
|
243
|
+
<meta charset="utf-8">
|
|
244
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
245
|
+
<title>${escapeHtml(opts.title)}</title>
|
|
246
|
+
${opts.externalLinks}
|
|
247
|
+
<style>
|
|
248
|
+
html, body { margin: 0; height: 100%; background: #000; overflow: hidden; font-family: system-ui, sans-serif; }
|
|
249
|
+
.os-stage { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
250
|
+
.os-frame { width: 1920px; height: 1080px; background: #fff; color: #000; transform-origin: center center; overflow: hidden; position: relative; }
|
|
251
|
+
.os-page { position: absolute; inset: 0; }
|
|
252
|
+
.os-page[hidden] { display: none !important; }
|
|
253
|
+
.os-counter { position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%); color: #fff; background: rgba(0,0,0,.5); padding: 2px 10px; border-radius: 999px; font-size: 12px; z-index: 10; font-variant-numeric: tabular-nums; }
|
|
254
|
+
</style>
|
|
255
|
+
<style>${opts.bundledCss}</style>
|
|
256
|
+
</head>
|
|
257
|
+
<body>
|
|
258
|
+
<div class="os-stage"><div class="os-frame" id="os-frame">${pagesMarkup}</div></div>
|
|
259
|
+
<div class="os-counter"><span id="os-cur">1</span> / <span id="os-total">${opts.pagesHtml.length}</span></div>
|
|
260
|
+
<script>
|
|
261
|
+
(function () {
|
|
262
|
+
var pages = document.querySelectorAll('.os-page');
|
|
263
|
+
var idx = 0;
|
|
264
|
+
var frame = document.getElementById('os-frame');
|
|
265
|
+
var cur = document.getElementById('os-cur');
|
|
266
|
+
function fit() {
|
|
267
|
+
var s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
|
|
268
|
+
frame.style.transform = 'scale(' + s + ')';
|
|
269
|
+
}
|
|
270
|
+
function go(i) {
|
|
271
|
+
idx = Math.max(0, Math.min(pages.length - 1, i));
|
|
272
|
+
pages.forEach(function (p, n) { p.hidden = n !== idx; });
|
|
273
|
+
cur.textContent = String(idx + 1);
|
|
274
|
+
}
|
|
275
|
+
window.addEventListener('resize', fit);
|
|
276
|
+
window.addEventListener('keydown', function (e) {
|
|
277
|
+
if (['ArrowRight','ArrowDown','PageDown',' '].indexOf(e.key) >= 0) { e.preventDefault(); go(idx + 1); }
|
|
278
|
+
else if (['ArrowLeft','ArrowUp','PageUp'].indexOf(e.key) >= 0) { e.preventDefault(); go(idx - 1); }
|
|
279
|
+
else if (e.key === 'Home') { e.preventDefault(); go(0); }
|
|
280
|
+
else if (e.key === 'End') { e.preventDefault(); go(pages.length - 1); }
|
|
281
|
+
});
|
|
282
|
+
fit();
|
|
283
|
+
go(0);
|
|
284
|
+
})();
|
|
285
|
+
</script>
|
|
286
|
+
</body>
|
|
287
|
+
</html>`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function escapeHtml(s: string): string {
|
|
291
|
+
return s
|
|
292
|
+
.replace(/&/g, '&')
|
|
293
|
+
.replace(/</g, '<')
|
|
294
|
+
.replace(/>/g, '>')
|
|
295
|
+
.replace(/"/g, '"')
|
|
296
|
+
.replace(/'/g, ''');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function escapeAttr(s: string): string {
|
|
300
|
+
return s.replace(/&/g, '&').replace(/"/g, '"');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function downloadBlob(blob: Blob, filename: string): void {
|
|
304
|
+
const url = URL.createObjectURL(blob);
|
|
305
|
+
const a = document.createElement('a');
|
|
306
|
+
a.href = url;
|
|
307
|
+
a.download = filename;
|
|
308
|
+
a.rel = 'noopener';
|
|
309
|
+
document.body.appendChild(a);
|
|
310
|
+
a.click();
|
|
311
|
+
a.remove();
|
|
312
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
313
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import type { Folder, FolderIcon, FoldersManifest } from './sdk';
|
|
3
|
+
|
|
4
|
+
const EMPTY: FoldersManifest = { folders: [], assignments: {} };
|
|
5
|
+
|
|
6
|
+
async function getManifest(): Promise<FoldersManifest> {
|
|
7
|
+
const res = await fetch('/__folders');
|
|
8
|
+
if (!res.ok) throw new Error(`GET /__folders ${res.status}`);
|
|
9
|
+
const raw = (await res.json()) as Partial<FoldersManifest>;
|
|
10
|
+
return {
|
|
11
|
+
folders: raw.folders ?? [],
|
|
12
|
+
assignments: raw.assignments ?? {},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function patchSlideName(slideId: string, name: string): Promise<void> {
|
|
17
|
+
const res = await fetch(`/__slides/${slideId}`, {
|
|
18
|
+
method: 'PATCH',
|
|
19
|
+
headers: { 'content-type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ name }),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) throw new Error(`PATCH /__slides/${slideId} ${res.status}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function deleteSlideReq(slideId: string): Promise<void> {
|
|
26
|
+
const res = await fetch(`/__slides/${slideId}`, { method: 'DELETE' });
|
|
27
|
+
if (!res.ok) throw new Error(`DELETE /__slides/${slideId} ${res.status}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function postFolder(name: string, icon: FolderIcon): Promise<Folder> {
|
|
31
|
+
const res = await fetch('/__folders', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'content-type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ name, icon }),
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) throw new Error(`POST /__folders ${res.status}`);
|
|
37
|
+
return (await res.json()) as Folder;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function patchFolder(
|
|
41
|
+
id: string,
|
|
42
|
+
patch: { name?: string; icon?: FolderIcon },
|
|
43
|
+
): Promise<Folder> {
|
|
44
|
+
const res = await fetch(`/__folders/${id}`, {
|
|
45
|
+
method: 'PATCH',
|
|
46
|
+
headers: { 'content-type': 'application/json' },
|
|
47
|
+
body: JSON.stringify(patch),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) throw new Error(`PATCH /__folders/${id} ${res.status}`);
|
|
50
|
+
return (await res.json()) as Folder;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function deleteFolder(id: string): Promise<void> {
|
|
54
|
+
const res = await fetch(`/__folders/${id}`, { method: 'DELETE' });
|
|
55
|
+
if (!res.ok) throw new Error(`DELETE /__folders/${id} ${res.status}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function putAssign(slideId: string, folderId: string | null): Promise<void> {
|
|
59
|
+
const res = await fetch('/__folders/assign', {
|
|
60
|
+
method: 'PUT',
|
|
61
|
+
headers: { 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ slideId, folderId }),
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) throw new Error(`PUT /__folders/assign ${res.status}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type UseFoldersResult = {
|
|
68
|
+
manifest: FoldersManifest;
|
|
69
|
+
loading: boolean;
|
|
70
|
+
create: (name: string, icon: FolderIcon) => Promise<Folder>;
|
|
71
|
+
update: (id: string, patch: { name?: string; icon?: FolderIcon }) => Promise<void>;
|
|
72
|
+
remove: (id: string) => Promise<void>;
|
|
73
|
+
assign: (slideId: string, folderId: string | null) => Promise<void>;
|
|
74
|
+
renameSlide: (slideId: string, name: string) => Promise<void>;
|
|
75
|
+
deleteSlide: (slideId: string) => Promise<void>;
|
|
76
|
+
refresh: () => Promise<void>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function useFolders(): UseFoldersResult {
|
|
80
|
+
const [manifest, setManifest] = useState<FoldersManifest>(EMPTY);
|
|
81
|
+
const [loading, setLoading] = useState(true);
|
|
82
|
+
|
|
83
|
+
const refresh = useCallback(async () => {
|
|
84
|
+
const m = await getManifest();
|
|
85
|
+
setManifest(m);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
let cancelled = false;
|
|
90
|
+
getManifest()
|
|
91
|
+
.then((m) => {
|
|
92
|
+
if (!cancelled) {
|
|
93
|
+
setManifest(m);
|
|
94
|
+
setLoading(false);
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.catch(() => {
|
|
98
|
+
if (!cancelled) setLoading(false);
|
|
99
|
+
});
|
|
100
|
+
return () => {
|
|
101
|
+
cancelled = true;
|
|
102
|
+
};
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!import.meta.hot) return;
|
|
107
|
+
const handler = () => {
|
|
108
|
+
refresh().catch(() => {});
|
|
109
|
+
};
|
|
110
|
+
import.meta.hot.on('open-slide:files-changed', handler);
|
|
111
|
+
return () => {
|
|
112
|
+
import.meta.hot?.off('open-slide:files-changed', handler);
|
|
113
|
+
};
|
|
114
|
+
}, [refresh]);
|
|
115
|
+
|
|
116
|
+
const create = useCallback(
|
|
117
|
+
async (name: string, icon: FolderIcon) => {
|
|
118
|
+
const folder = await postFolder(name, icon);
|
|
119
|
+
await refresh();
|
|
120
|
+
return folder;
|
|
121
|
+
},
|
|
122
|
+
[refresh],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const update = useCallback(
|
|
126
|
+
async (id: string, patch: { name?: string; icon?: FolderIcon }) => {
|
|
127
|
+
await patchFolder(id, patch);
|
|
128
|
+
await refresh();
|
|
129
|
+
},
|
|
130
|
+
[refresh],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const remove = useCallback(
|
|
134
|
+
async (id: string) => {
|
|
135
|
+
await deleteFolder(id);
|
|
136
|
+
await refresh();
|
|
137
|
+
},
|
|
138
|
+
[refresh],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const assign = useCallback(
|
|
142
|
+
async (slideId: string, folderId: string | null) => {
|
|
143
|
+
await putAssign(slideId, folderId);
|
|
144
|
+
await refresh();
|
|
145
|
+
},
|
|
146
|
+
[refresh],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const renameSlide = useCallback(
|
|
150
|
+
async (slideId: string, name: string) => {
|
|
151
|
+
await patchSlideName(slideId, name);
|
|
152
|
+
await refresh();
|
|
153
|
+
},
|
|
154
|
+
[refresh],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const deleteSlide = useCallback(
|
|
158
|
+
async (slideId: string) => {
|
|
159
|
+
await deleteSlideReq(slideId);
|
|
160
|
+
await refresh();
|
|
161
|
+
},
|
|
162
|
+
[refresh],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return { manifest, loading, create, update, remove, assign, renameSlide, deleteSlide, refresh };
|
|
166
|
+
}
|
|
@@ -21,8 +21,8 @@ function getSource(fiber: FiberLike) {
|
|
|
21
21
|
return fiber._debugSource ?? fiber.memoizedProps?.__source;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function findSlideSource(el: HTMLElement,
|
|
25
|
-
const needle = `/slides/${
|
|
24
|
+
export function findSlideSource(el: HTMLElement, slideId: string): SlideSourceHit | null {
|
|
25
|
+
const needle = `/slides/${slideId}/index.tsx`;
|
|
26
26
|
let fiber = getFiber(el);
|
|
27
27
|
let anchor: HTMLElement = el;
|
|
28
28
|
while (fiber) {
|
|
@@ -10,14 +10,14 @@ export type SlideComment = {
|
|
|
10
10
|
|
|
11
11
|
type ListResponse = { comments: SlideComment[] };
|
|
12
12
|
|
|
13
|
-
export function useComments(
|
|
13
|
+
export function useComments(slideId: string) {
|
|
14
14
|
const [comments, setComments] = useState<SlideComment[]>([]);
|
|
15
15
|
const [error, setError] = useState<string | null>(null);
|
|
16
16
|
|
|
17
17
|
const refetch = useCallback(async () => {
|
|
18
|
-
if (!
|
|
18
|
+
if (!slideId) return;
|
|
19
19
|
try {
|
|
20
|
-
const res = await fetch(`/__comments?
|
|
20
|
+
const res = await fetch(`/__comments?slideId=${encodeURIComponent(slideId)}`);
|
|
21
21
|
if (!res.ok) {
|
|
22
22
|
setError(`GET /__comments → ${res.status}`);
|
|
23
23
|
return;
|
|
@@ -28,14 +28,14 @@ export function useComments(deckId: string) {
|
|
|
28
28
|
} catch (e) {
|
|
29
29
|
setError(String((e as Error).message ?? e));
|
|
30
30
|
}
|
|
31
|
-
}, [
|
|
31
|
+
}, [slideId]);
|
|
32
32
|
|
|
33
33
|
const add = useCallback(
|
|
34
34
|
async (line: number, column: number, text: string) => {
|
|
35
35
|
const res = await fetch('/__comments/add', {
|
|
36
36
|
method: 'POST',
|
|
37
37
|
headers: { 'content-type': 'application/json' },
|
|
38
|
-
body: JSON.stringify({
|
|
38
|
+
body: JSON.stringify({ slideId, line, column, text }),
|
|
39
39
|
});
|
|
40
40
|
if (!res.ok) {
|
|
41
41
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
@@ -43,18 +43,18 @@ export function useComments(deckId: string) {
|
|
|
43
43
|
}
|
|
44
44
|
await refetch();
|
|
45
45
|
},
|
|
46
|
-
[
|
|
46
|
+
[slideId, refetch],
|
|
47
47
|
);
|
|
48
48
|
|
|
49
49
|
const remove = useCallback(
|
|
50
50
|
async (id: string) => {
|
|
51
|
-
const res = await fetch(`/__comments/${id}?
|
|
51
|
+
const res = await fetch(`/__comments/${id}?slideId=${encodeURIComponent(slideId)}`, {
|
|
52
52
|
method: 'DELETE',
|
|
53
53
|
});
|
|
54
54
|
if (!res.ok) throw new Error(`DELETE /__comments/${id} → ${res.status}`);
|
|
55
55
|
await refetch();
|
|
56
56
|
},
|
|
57
|
-
[
|
|
57
|
+
[slideId, refetch],
|
|
58
58
|
);
|
|
59
59
|
|
|
60
60
|
useEffect(() => {
|
package/src/app/lib/sdk.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import type { ComponentType } from 'react';
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type Page = ComponentType;
|
|
4
4
|
|
|
5
|
-
export type
|
|
5
|
+
export type SlideMeta = {
|
|
6
6
|
title?: string;
|
|
7
7
|
theme?: 'light' | 'dark';
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export type
|
|
11
|
-
default:
|
|
12
|
-
meta?:
|
|
10
|
+
export type SlideModule = {
|
|
11
|
+
default: Page[];
|
|
12
|
+
meta?: SlideMeta;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type FolderIcon = { type: 'emoji'; value: string } | { type: 'color'; value: string };
|
|
16
|
+
|
|
17
|
+
export type Folder = {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
icon: FolderIcon;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type FoldersManifest = {
|
|
24
|
+
folders: Folder[];
|
|
25
|
+
assignments: Record<string, string>;
|
|
13
26
|
};
|
|
14
27
|
|
|
15
28
|
export const CANVAS_WIDTH = 1920;
|