@open-aippt/core 1.13.2

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 (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/bin.js +2 -0
  4. package/dist/build-DxTqmvsO.js +17 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +86 -0
  7. package/dist/config-CjzqjrEA.js +4280 -0
  8. package/dist/config-DIC-yVPp.d.ts +23 -0
  9. package/dist/design-cpzS8aud.js +35 -0
  10. package/dist/dev-BYuTeJbA.js +20 -0
  11. package/dist/format-BCeKbTOM.js +1605 -0
  12. package/dist/index.d.ts +134 -0
  13. package/dist/index.js +467 -0
  14. package/dist/locale/index.d.ts +24 -0
  15. package/dist/locale/index.js +3 -0
  16. package/dist/preview-DlQvnJPq.js +18 -0
  17. package/dist/sync-BPZ0m27m.js +139 -0
  18. package/dist/sync-EsYusbbL.js +3 -0
  19. package/dist/types-CHmFPIG_.d.ts +430 -0
  20. package/dist/vite/index.d.ts +14 -0
  21. package/dist/vite/index.js +4 -0
  22. package/env.d.ts +59 -0
  23. package/package.json +103 -0
  24. package/skills/apply-comments/SKILL.md +83 -0
  25. package/skills/create-slide/SKILL.md +91 -0
  26. package/skills/create-theme/SKILL.md +250 -0
  27. package/skills/current-slide/SKILL.md +110 -0
  28. package/skills/slide-authoring/SKILL.md +625 -0
  29. package/src/app/app.tsx +47 -0
  30. package/src/app/components/asset-view.tsx +966 -0
  31. package/src/app/components/history-provider.tsx +120 -0
  32. package/src/app/components/image-placeholder.tsx +243 -0
  33. package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
  34. package/src/app/components/inspector/comment-widget.tsx +93 -0
  35. package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
  36. package/src/app/components/inspector/inspect-overlay.tsx +387 -0
  37. package/src/app/components/inspector/inspector-panel.tsx +1115 -0
  38. package/src/app/components/inspector/inspector-provider.tsx +1218 -0
  39. package/src/app/components/inspector/save-bar.tsx +48 -0
  40. package/src/app/components/language-toggle.tsx +39 -0
  41. package/src/app/components/notes-drawer.tsx +120 -0
  42. package/src/app/components/overview-grid.tsx +363 -0
  43. package/src/app/components/panel/panel-fields.tsx +60 -0
  44. package/src/app/components/panel/panel-shell.tsx +80 -0
  45. package/src/app/components/panel/save-card.tsx +142 -0
  46. package/src/app/components/pdf-progress-toast.tsx +32 -0
  47. package/src/app/components/player.tsx +466 -0
  48. package/src/app/components/pptx-progress-toast.tsx +32 -0
  49. package/src/app/components/present/blackout-overlay.tsx +18 -0
  50. package/src/app/components/present/control-bar.tsx +315 -0
  51. package/src/app/components/present/help-overlay.tsx +57 -0
  52. package/src/app/components/present/jump-input.tsx +74 -0
  53. package/src/app/components/present/laser-pointer.tsx +39 -0
  54. package/src/app/components/present/progress-bar.tsx +26 -0
  55. package/src/app/components/present/use-idle.ts +46 -0
  56. package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
  57. package/src/app/components/present/use-presenter-channel.ts +66 -0
  58. package/src/app/components/present/use-touch-swipe.ts +66 -0
  59. package/src/app/components/shared-element.tsx +48 -0
  60. package/src/app/components/sidebar/folder-item.tsx +258 -0
  61. package/src/app/components/sidebar/icon-picker.tsx +61 -0
  62. package/src/app/components/sidebar/mobile-pill.tsx +34 -0
  63. package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
  64. package/src/app/components/sidebar/sidebar.tsx +284 -0
  65. package/src/app/components/slide-canvas.tsx +102 -0
  66. package/src/app/components/slide-transition-layer.tsx +844 -0
  67. package/src/app/components/style-panel/design-provider.tsx +148 -0
  68. package/src/app/components/style-panel/style-panel.tsx +349 -0
  69. package/src/app/components/style-panel/use-design.ts +112 -0
  70. package/src/app/components/theme-toggle.tsx +59 -0
  71. package/src/app/components/themes/theme-detail.tsx +305 -0
  72. package/src/app/components/themes/themes-gallery.tsx +149 -0
  73. package/src/app/components/thumbnail-rail.tsx +805 -0
  74. package/src/app/components/ui/badge.tsx +45 -0
  75. package/src/app/components/ui/button.tsx +99 -0
  76. package/src/app/components/ui/card.tsx +92 -0
  77. package/src/app/components/ui/context-menu.tsx +237 -0
  78. package/src/app/components/ui/dialog.tsx +157 -0
  79. package/src/app/components/ui/dropdown-menu.tsx +245 -0
  80. package/src/app/components/ui/input.tsx +25 -0
  81. package/src/app/components/ui/label.tsx +24 -0
  82. package/src/app/components/ui/popover.tsx +75 -0
  83. package/src/app/components/ui/progress.tsx +31 -0
  84. package/src/app/components/ui/scroll-area.tsx +53 -0
  85. package/src/app/components/ui/select.tsx +196 -0
  86. package/src/app/components/ui/separator.tsx +28 -0
  87. package/src/app/components/ui/slider.tsx +61 -0
  88. package/src/app/components/ui/sonner.tsx +48 -0
  89. package/src/app/components/ui/tabs.tsx +79 -0
  90. package/src/app/components/ui/textarea.tsx +22 -0
  91. package/src/app/components/ui/toggle-group.tsx +83 -0
  92. package/src/app/components/ui/toggle.tsx +45 -0
  93. package/src/app/components/ui/tooltip.tsx +58 -0
  94. package/src/app/favicon.ico +0 -0
  95. package/src/app/index.html +13 -0
  96. package/src/app/lib/assets.ts +242 -0
  97. package/src/app/lib/design-presets.ts +94 -0
  98. package/src/app/lib/design.ts +58 -0
  99. package/src/app/lib/export-html.ts +326 -0
  100. package/src/app/lib/export-pdf.ts +298 -0
  101. package/src/app/lib/export-pptx.ts +284 -0
  102. package/src/app/lib/folders.ts +239 -0
  103. package/src/app/lib/inspector/fiber.test.ts +154 -0
  104. package/src/app/lib/inspector/fiber.ts +85 -0
  105. package/src/app/lib/inspector/use-comments.ts +74 -0
  106. package/src/app/lib/inspector/use-editor.ts +73 -0
  107. package/src/app/lib/inspector/use-notes.ts +134 -0
  108. package/src/app/lib/locale-store.ts +67 -0
  109. package/src/app/lib/page-context.tsx +38 -0
  110. package/src/app/lib/print-ready.test.ts +32 -0
  111. package/src/app/lib/print-ready.ts +51 -0
  112. package/src/app/lib/sdk.test.ts +13 -0
  113. package/src/app/lib/sdk.ts +37 -0
  114. package/src/app/lib/slides.ts +26 -0
  115. package/src/app/lib/step-context.tsx +261 -0
  116. package/src/app/lib/themes.ts +22 -0
  117. package/src/app/lib/transition.ts +30 -0
  118. package/src/app/lib/use-agent-socket.ts +18 -0
  119. package/src/app/lib/use-click-page-navigation.ts +60 -0
  120. package/src/app/lib/use-is-mobile.ts +21 -0
  121. package/src/app/lib/use-locale.ts +8 -0
  122. package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
  123. package/src/app/lib/use-slide-module.ts +48 -0
  124. package/src/app/lib/use-wheel-page-navigation.ts +99 -0
  125. package/src/app/lib/utils.test.ts +25 -0
  126. package/src/app/lib/utils.ts +6 -0
  127. package/src/app/main.tsx +14 -0
  128. package/src/app/routes/assets.tsx +9 -0
  129. package/src/app/routes/home-shell.tsx +213 -0
  130. package/src/app/routes/home.tsx +807 -0
  131. package/src/app/routes/presenter.tsx +418 -0
  132. package/src/app/routes/slide.tsx +1108 -0
  133. package/src/app/routes/themes.tsx +34 -0
  134. package/src/app/styles.css +429 -0
  135. package/src/app/virtual.d.ts +51 -0
  136. package/src/locale/en.ts +416 -0
  137. package/src/locale/format.ts +12 -0
  138. package/src/locale/index.ts +6 -0
  139. package/src/locale/ja.ts +422 -0
  140. package/src/locale/types.ts +443 -0
  141. package/src/locale/zh-cn.ts +414 -0
  142. package/src/locale/zh-tw.ts +414 -0
@@ -0,0 +1,94 @@
1
+ import { type DesignSystem, defaultDesign } from './design';
2
+
3
+ const SANS_SYSTEM = '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif';
4
+ const SANS_INTER = '"Inter", system-ui, sans-serif';
5
+ const SANS_HELV = '"Helvetica Neue", Helvetica, Arial, sans-serif';
6
+ const SERIF_GEORGIA = 'Georgia, "Times New Roman", serif';
7
+ const SERIF_TIMES = '"Times New Roman", Times, serif';
8
+ const MONO_SF = '"SF Mono", "JetBrains Mono", Menlo, monospace';
9
+
10
+ const designPresets: DesignSystem[] = [
11
+ defaultDesign,
12
+ {
13
+ palette: { bg: '#0f1115', text: '#f5f3ee', accent: '#7cc4ff' },
14
+ fonts: { display: SERIF_GEORGIA, body: SANS_SYSTEM },
15
+ typeScale: { hero: 192, body: 32 },
16
+ radius: 6,
17
+ },
18
+ {
19
+ palette: { bg: '#eef1f4', text: '#1c2733', accent: '#ff6a5b' },
20
+ fonts: { display: SANS_HELV, body: SANS_SYSTEM },
21
+ typeScale: { hero: 156, body: 30 },
22
+ radius: 8,
23
+ },
24
+ {
25
+ palette: { bg: '#fdf6e3', text: '#073642', accent: '#b58900' },
26
+ fonts: { display: SERIF_GEORGIA, body: SANS_INTER },
27
+ typeScale: { hero: 144, body: 28 },
28
+ radius: 14,
29
+ },
30
+ {
31
+ palette: { bg: '#ede2cc', text: '#3a2a1a', accent: '#2f6e3a' },
32
+ fonts: { display: SERIF_TIMES, body: SERIF_GEORGIA },
33
+ typeScale: { hero: 168, body: 32 },
34
+ radius: 4,
35
+ },
36
+ {
37
+ palette: { bg: '#ffffff', text: '#0a0a0a', accent: '#e11d48' },
38
+ fonts: { display: SANS_HELV, body: SANS_HELV },
39
+ typeScale: { hero: 200, body: 28 },
40
+ radius: 0,
41
+ },
42
+ {
43
+ palette: { bg: '#fde9d9', text: '#3a1f3d', accent: '#f97316' },
44
+ fonts: { display: SERIF_GEORGIA, body: SANS_SYSTEM },
45
+ typeScale: { hero: 184, body: 36 },
46
+ radius: 24,
47
+ },
48
+ {
49
+ palette: { bg: '#e9f5ee', text: '#0f3324', accent: '#ec4899' },
50
+ fonts: { display: SANS_INTER, body: SANS_INTER },
51
+ typeScale: { hero: 160, body: 32 },
52
+ radius: 16,
53
+ },
54
+ {
55
+ palette: { bg: '#0a0a0a', text: '#f3edd9', accent: '#eab308' },
56
+ fonts: { display: SERIF_GEORGIA, body: SANS_HELV },
57
+ typeScale: { hero: 200, body: 32 },
58
+ radius: 2,
59
+ },
60
+ {
61
+ palette: { bg: '#ece2f5', text: '#2a1c4a', accent: '#facc15' },
62
+ fonts: { display: SERIF_GEORGIA, body: SANS_SYSTEM },
63
+ typeScale: { hero: 168, body: 34 },
64
+ radius: 20,
65
+ },
66
+ {
67
+ palette: { bg: '#101418', text: '#a7f3d0', accent: '#fbbf24' },
68
+ fonts: { display: MONO_SF, body: MONO_SF },
69
+ typeScale: { hero: 144, body: 24 },
70
+ radius: 4,
71
+ },
72
+ {
73
+ palette: { bg: '#fafafa', text: '#0a0a0a', accent: '#facc15' },
74
+ fonts: { display: SANS_HELV, body: SANS_HELV },
75
+ typeScale: { hero: 220, body: 32 },
76
+ radius: 0,
77
+ },
78
+ ];
79
+
80
+ function pickRandom(): DesignSystem {
81
+ const idx = Math.floor(Math.random() * designPresets.length);
82
+ return designPresets[idx] ?? defaultDesign;
83
+ }
84
+
85
+ export function shuffleDesign(current?: DesignSystem | null): DesignSystem {
86
+ if (designPresets.length === 0) return defaultDesign;
87
+ if (designPresets.length === 1) return designPresets[0] ?? defaultDesign;
88
+ const currentJson = current ? JSON.stringify(current) : null;
89
+ for (let i = 0; i < 8; i++) {
90
+ const pick = pickRandom();
91
+ if (JSON.stringify(pick) !== currentJson) return pick;
92
+ }
93
+ return pickRandom();
94
+ }
@@ -0,0 +1,58 @@
1
+ export type DesignPalette = {
2
+ bg: string;
3
+ text: string;
4
+ accent: string;
5
+ };
6
+
7
+ export type DesignFonts = {
8
+ display: string;
9
+ body: string;
10
+ };
11
+
12
+ export type DesignTypeScale = {
13
+ hero: number;
14
+ body: number;
15
+ };
16
+
17
+ export type DesignSystem = {
18
+ palette: DesignPalette;
19
+ fonts: DesignFonts;
20
+ typeScale: DesignTypeScale;
21
+ radius: number;
22
+ };
23
+
24
+ export function designToCssVars(d: DesignSystem): Record<string, string> {
25
+ return {
26
+ '--osd-bg': d.palette.bg,
27
+ '--osd-text': d.palette.text,
28
+ '--osd-accent': d.palette.accent,
29
+ '--osd-font-display': d.fonts.display,
30
+ '--osd-font-body': d.fonts.body,
31
+ '--osd-size-hero': `${d.typeScale.hero}px`,
32
+ '--osd-size-body': `${d.typeScale.body}px`,
33
+ '--osd-radius': `${d.radius}px`,
34
+ };
35
+ }
36
+
37
+ export function cssVarsToString(vars: Record<string, string>): string {
38
+ return Object.entries(vars)
39
+ .map(([k, v]) => ` ${k}: ${v};`)
40
+ .join('\n');
41
+ }
42
+
43
+ export const defaultDesign: DesignSystem = {
44
+ palette: {
45
+ bg: '#f7f5f0',
46
+ text: '#1a1814',
47
+ accent: '#6d4cff',
48
+ },
49
+ fonts: {
50
+ display: 'Georgia, "Times New Roman", serif',
51
+ body: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
52
+ },
53
+ typeScale: {
54
+ hero: 168,
55
+ body: 36,
56
+ },
57
+ radius: 12,
58
+ };
@@ -0,0 +1,326 @@
1
+ import { createElement } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { designToCssVars } from './design';
4
+ import { SlidePageProvider } from './page-context';
5
+ import type { SlideModule } from './sdk';
6
+
7
+ type AssetEntry = { name: string; bytes: Uint8Array };
8
+
9
+ const ASSET_EXT_RE =
10
+ /\.(?:png|jpe?g|gif|svg|webp|avif|mp4|webm|mov|woff2?|ttf|otf|mp3|wav|ogg)(?:\?[^#]*)?(?:#.*)?$/i;
11
+
12
+ export async function exportSlideAsHtml(slide: SlideModule, slideId: string): Promise<void> {
13
+ const pages = slide.default ?? [];
14
+ if (pages.length === 0) return;
15
+ const title = slide.meta?.title ?? slideId;
16
+
17
+ const pagesHtml = await renderPagesToHtml(pages);
18
+ const bundledCss = collectCss();
19
+ const externalLinks = collectExternalStylesheetLinks();
20
+
21
+ const assets = new Map<string, AssetEntry>();
22
+ const usedNames = new Set<string>();
23
+
24
+ const urls = new Set<string>([
25
+ ...findHtmlAssetUrls(pagesHtml.join('\n')),
26
+ ...findCssAssetUrls(bundledCss),
27
+ ]);
28
+
29
+ for (const url of urls) {
30
+ const absolute = toAbsolute(url);
31
+ if (!absolute) continue;
32
+ try {
33
+ const res = await fetch(absolute);
34
+ if (!res.ok) continue;
35
+ const buf = new Uint8Array(await res.arrayBuffer());
36
+ const name = uniqueAssetName(absolute, usedNames);
37
+ assets.set(url, { name, bytes: buf });
38
+ } catch {}
39
+ }
40
+
41
+ const rewrittenPages = pagesHtml.map((html) => rewriteUrls(html, assets, 'html'));
42
+ const rewrittenCss = rewriteUrls(bundledCss, assets, 'css');
43
+
44
+ const html = buildHtml({
45
+ title,
46
+ pagesHtml: rewrittenPages,
47
+ bundledCss: rewrittenCss,
48
+ externalLinks,
49
+ design: slide.design,
50
+ });
51
+
52
+ const htmlBytes = new TextEncoder().encode(html);
53
+
54
+ if (assets.size === 0) {
55
+ downloadBlob(new Blob([htmlBytes as BlobPart], { type: 'text/html' }), `${slideId}.html`);
56
+ return;
57
+ }
58
+
59
+ const { zipSync } = await import('fflate');
60
+ const zipTree: Record<string, Uint8Array | Record<string, Uint8Array>> = {
61
+ [`${slideId}.html`]: htmlBytes,
62
+ assets: {},
63
+ };
64
+ for (const { name, bytes } of assets.values()) {
65
+ (zipTree.assets as Record<string, Uint8Array>)[name] = bytes;
66
+ }
67
+ const zipped = zipSync(zipTree as Parameters<typeof zipSync>[0]);
68
+ downloadBlob(new Blob([zipped as BlobPart], { type: 'application/zip' }), `${slideId}.zip`);
69
+ }
70
+
71
+ async function renderPagesToHtml(pages: NonNullable<SlideModule['default']>): Promise<string[]> {
72
+ const container = document.createElement('div');
73
+ container.setAttribute('aria-hidden', 'true');
74
+ Object.assign(container.style, {
75
+ position: 'fixed',
76
+ left: '-99999px',
77
+ top: '0',
78
+ width: '1920px',
79
+ height: '1080px',
80
+ pointerEvents: 'none',
81
+ });
82
+ document.body.appendChild(container);
83
+
84
+ const result: string[] = [];
85
+ try {
86
+ for (let i = 0; i < pages.length; i++) {
87
+ const Page = pages[i];
88
+ if (!Page) continue;
89
+ const host = document.createElement('div');
90
+ host.style.width = '1920px';
91
+ host.style.height = '1080px';
92
+ container.appendChild(host);
93
+ const root = createRoot(host);
94
+ root.render(
95
+ createElement(SlidePageProvider, { index: i, total: pages.length }, createElement(Page)),
96
+ );
97
+ await nextPaint();
98
+ await nextPaint();
99
+ result.push(host.innerHTML);
100
+ root.unmount();
101
+ container.removeChild(host);
102
+ }
103
+ } finally {
104
+ container.remove();
105
+ }
106
+ return result;
107
+ }
108
+
109
+ function nextPaint(): Promise<void> {
110
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
111
+ }
112
+
113
+ function collectCss(): string {
114
+ const chunks: string[] = [];
115
+ for (const sheet of Array.from(document.styleSheets)) {
116
+ let rules: CSSRuleList | null = null;
117
+ try {
118
+ rules = sheet.cssRules;
119
+ } catch {
120
+ continue;
121
+ }
122
+ if (!rules) continue;
123
+ for (const rule of Array.from(rules)) {
124
+ chunks.push(rule.cssText);
125
+ }
126
+ }
127
+ return chunks.join('\n');
128
+ }
129
+
130
+ function collectExternalStylesheetLinks(): string {
131
+ const links: string[] = [];
132
+ for (const sheet of Array.from(document.styleSheets)) {
133
+ try {
134
+ void sheet.cssRules;
135
+ } catch {
136
+ if (sheet.href) {
137
+ links.push(`<link rel="stylesheet" href="${escapeAttr(sheet.href)}">`);
138
+ }
139
+ }
140
+ }
141
+ return links.join('\n');
142
+ }
143
+
144
+ function findHtmlAssetUrls(html: string): string[] {
145
+ const out: string[] = [];
146
+ const attrRe = /\s(?:src|href)="([^"]+)"/g;
147
+ for (const m of html.matchAll(attrRe)) {
148
+ if (looksLikeAsset(m[1])) out.push(m[1]);
149
+ }
150
+ const srcsetRe = /\ssrcset="([^"]+)"/g;
151
+ for (const m of html.matchAll(srcsetRe)) {
152
+ for (const part of m[1].split(',')) {
153
+ const url = part.trim().split(/\s+/)[0];
154
+ if (url && looksLikeAsset(url)) out.push(url);
155
+ }
156
+ }
157
+ return out;
158
+ }
159
+
160
+ function findCssAssetUrls(css: string): string[] {
161
+ const out: string[] = [];
162
+ const re = /url\(\s*(['"]?)([^)'"]+)\1\s*\)/g;
163
+ for (const m of css.matchAll(re)) {
164
+ const url = m[2].trim();
165
+ if (looksLikeAsset(url)) out.push(url);
166
+ }
167
+ return out;
168
+ }
169
+
170
+ function looksLikeAsset(url: string): boolean {
171
+ if (!url) return false;
172
+ if (url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('#')) return false;
173
+ if (url.startsWith('mailto:') || url.startsWith('javascript:')) return false;
174
+ const abs = toAbsolute(url);
175
+ if (!abs) return false;
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(
222
+ source: string,
223
+ assets: Map<string, AssetEntry>,
224
+ kind: 'html' | 'css',
225
+ ): string {
226
+ let out = source;
227
+ for (const [orig, { name }] of assets) {
228
+ const replacement = kind === 'css' ? `./assets/${name}` : `assets/${name}`;
229
+ out = out.split(orig).join(replacement);
230
+ }
231
+ return out;
232
+ }
233
+
234
+ function buildHtml(opts: {
235
+ title: string;
236
+ pagesHtml: string[];
237
+ bundledCss: string;
238
+ externalLinks: string;
239
+ design: SlideModule['design'];
240
+ }): string {
241
+ const pagesMarkup = opts.pagesHtml
242
+ .map(
243
+ (page, i) => `<div class="os-page" data-idx="${i}"${i === 0 ? '' : ' hidden'}>${page}</div>`,
244
+ )
245
+ .join('');
246
+
247
+ const frameStyle = opts.design
248
+ ? Object.entries(designToCssVars(opts.design))
249
+ .map(([k, v]) => `${k}: ${v};`)
250
+ .join(' ')
251
+ : '';
252
+
253
+ return `<!doctype html>
254
+ <html lang="en">
255
+ <head>
256
+ <meta charset="utf-8">
257
+ <meta name="viewport" content="width=device-width, initial-scale=1">
258
+ <title>${escapeHtml(opts.title)}</title>
259
+ ${opts.externalLinks}
260
+ <style>
261
+ html, body { margin: 0; height: 100%; background: #000; overflow: hidden; font-family: system-ui, sans-serif; }
262
+ .os-stage { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
263
+ .os-frame { width: 1920px; height: 1080px; flex-shrink: 0; background: #fff; color: #000; transform-origin: center center; overflow: hidden; position: relative; }
264
+ .os-page { position: absolute; inset: 0; }
265
+ .os-page[hidden] { display: none !important; }
266
+ .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; }
267
+ </style>
268
+ <style>${opts.bundledCss}</style>
269
+ </head>
270
+ <body>
271
+ <div class="os-stage"><div class="os-frame" id="os-frame" data-osd-canvas${frameStyle ? ` style="${escapeAttr(frameStyle)}"` : ''}>${pagesMarkup}</div></div>
272
+ <div class="os-counter"><span id="os-cur">1</span> / <span id="os-total">${opts.pagesHtml.length}</span></div>
273
+ <script>
274
+ (function () {
275
+ var pages = document.querySelectorAll('.os-page');
276
+ var idx = 0;
277
+ var frame = document.getElementById('os-frame');
278
+ var cur = document.getElementById('os-cur');
279
+ function fit() {
280
+ var s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
281
+ frame.style.transform = 'scale(' + s + ')';
282
+ }
283
+ function go(i) {
284
+ idx = Math.max(0, Math.min(pages.length - 1, i));
285
+ pages.forEach(function (p, n) { p.hidden = n !== idx; });
286
+ cur.textContent = String(idx + 1);
287
+ }
288
+ window.addEventListener('resize', fit);
289
+ window.addEventListener('keydown', function (e) {
290
+ if (['ArrowRight','ArrowDown','PageDown',' '].indexOf(e.key) >= 0) { e.preventDefault(); go(idx + 1); }
291
+ else if (['ArrowLeft','ArrowUp','PageUp'].indexOf(e.key) >= 0) { e.preventDefault(); go(idx - 1); }
292
+ else if (e.key === 'Home') { e.preventDefault(); go(0); }
293
+ else if (e.key === 'End') { e.preventDefault(); go(pages.length - 1); }
294
+ });
295
+ fit();
296
+ go(0);
297
+ })();
298
+ </script>
299
+ </body>
300
+ </html>`;
301
+ }
302
+
303
+ function escapeHtml(s: string): string {
304
+ return s
305
+ .replace(/&/g, '&amp;')
306
+ .replace(/</g, '&lt;')
307
+ .replace(/>/g, '&gt;')
308
+ .replace(/"/g, '&quot;')
309
+ .replace(/'/g, '&#39;');
310
+ }
311
+
312
+ function escapeAttr(s: string): string {
313
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
314
+ }
315
+
316
+ function downloadBlob(blob: Blob, filename: string): void {
317
+ const url = URL.createObjectURL(blob);
318
+ const a = document.createElement('a');
319
+ a.href = url;
320
+ a.download = filename;
321
+ a.rel = 'noopener';
322
+ document.body.appendChild(a);
323
+ a.click();
324
+ a.remove();
325
+ setTimeout(() => URL.revokeObjectURL(url), 0);
326
+ }