@open-slide/core 1.0.4 → 1.0.6

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 (55) hide show
  1. package/dist/{build-DqfKmw9h.js → build-4wOJF1l4.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-DweCbRkQ.d.ts → config-D2y1AXaN.d.ts} +3 -0
  4. package/dist/{config-CN7J0RDO.js → config-evLWCV1-.js} +378 -222
  5. package/dist/{dev-jWxtWHAG.js → dev-BUr0S-Ij.js} +1 -1
  6. package/dist/index.d.ts +3 -2
  7. package/dist/locale/index.d.ts +24 -0
  8. package/dist/locale/index.js +1189 -0
  9. package/dist/{preview-CSA05Gfm.js → preview-DP_gIphz.js} +1 -1
  10. package/dist/types-BVvl_xup.d.ts +314 -0
  11. package/dist/vite/index.d.ts +2 -1
  12. package/dist/vite/index.js +1 -1
  13. package/package.json +7 -1
  14. package/src/app/app.tsx +6 -2
  15. package/src/app/components/asset-view.tsx +87 -64
  16. package/src/app/components/click-nav-zones.tsx +4 -2
  17. package/src/app/components/inspector/comment-widget.tsx +9 -7
  18. package/src/app/components/inspector/inspect-overlay.tsx +79 -17
  19. package/src/app/components/inspector/inspector-panel.tsx +68 -39
  20. package/src/app/components/inspector/inspector-provider.tsx +185 -58
  21. package/src/app/components/inspector/save-bar.tsx +6 -5
  22. package/src/app/components/panel/save-card.tsx +12 -9
  23. package/src/app/components/pdf-progress-toast.tsx +11 -4
  24. package/src/app/components/player.tsx +7 -25
  25. package/src/app/components/present/control-bar.tsx +17 -10
  26. package/src/app/components/present/help-overlay.tsx +18 -17
  27. package/src/app/components/present/overview-grid.tsx +6 -9
  28. package/src/app/components/present/use-presenter-channel.ts +3 -10
  29. package/src/app/components/sidebar/folder-item.tsx +16 -9
  30. package/src/app/components/sidebar/icon-picker.tsx +4 -5
  31. package/src/app/components/sidebar/sidebar.tsx +87 -25
  32. package/src/app/components/slide-canvas.tsx +1 -10
  33. package/src/app/components/style-panel/design-provider.tsx +2 -6
  34. package/src/app/components/style-panel/style-panel.tsx +26 -18
  35. package/src/app/components/theme-toggle.tsx +7 -5
  36. package/src/app/components/thumbnail-rail.tsx +4 -2
  37. package/src/app/favicon.ico +0 -0
  38. package/src/app/lib/export-html.ts +1 -9
  39. package/src/app/lib/export-pdf.ts +0 -5
  40. package/src/app/lib/inspector/use-editor.ts +9 -7
  41. package/src/app/lib/print-ready.ts +0 -4
  42. package/src/app/lib/sdk.ts +1 -2
  43. package/src/app/lib/use-locale.ts +20 -0
  44. package/src/app/routes/home.tsx +90 -45
  45. package/src/app/routes/presenter.tsx +45 -25
  46. package/src/app/routes/slide.tsx +37 -24
  47. package/src/app/styles.css +28 -0
  48. package/src/app/virtual.d.ts +4 -0
  49. package/src/locale/en.ts +303 -0
  50. package/src/locale/format.ts +12 -0
  51. package/src/locale/index.ts +6 -0
  52. package/src/locale/ja.ts +307 -0
  53. package/src/locale/types.ts +323 -0
  54. package/src/locale/zh-cn.ts +303 -0
  55. package/src/locale/zh-tw.ts +303 -0
@@ -2,6 +2,7 @@ import { Palette, X } from 'lucide-react';
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Field, NumberField, Section } from '@/components/panel/panel-fields';
4
4
  import { PanelShell, usePanelMount } from '@/components/panel/panel-shell';
5
+ import { useLocale } from '@/lib/use-locale';
5
6
  import { Button } from '../ui/button';
6
7
  import { Input } from '../ui/input';
7
8
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
@@ -29,6 +30,7 @@ type DesignPanelProps = {
29
30
  export function DesignPanel({ open, onClose }: DesignPanelProps) {
30
31
  const { draft, exists, warning, loaded, dirty, update } = useDesignPanelState();
31
32
  const { mounted, animVisible } = usePanelMount(open);
33
+ const t = useLocale();
32
34
 
33
35
  if (!loaded) return null;
34
36
  if (!mounted) return null;
@@ -43,15 +45,19 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
43
45
  <div className="flex min-w-0 items-center gap-2">
44
46
  <Palette className="size-3.5 text-muted-foreground" />
45
47
  <span className="font-heading text-[12px] font-semibold tracking-tight">
46
- Design tokens
48
+ {t.stylePanel.designTokens}
47
49
  </span>
48
50
  {!exists && (
49
51
  <span className="rounded-[3px] border border-hairline bg-muted/60 px-1.5 py-px font-mono text-[9.5px] uppercase tracking-[0.08em] text-muted-foreground">
50
- draft
52
+ {t.stylePanel.draftBadge}
51
53
  </span>
52
54
  )}
53
55
  {dirty && (
54
- <span className="size-1.5 rounded-full bg-brand" title="Unsaved" aria-hidden />
56
+ <span
57
+ className="size-1.5 rounded-full bg-brand"
58
+ title={t.stylePanel.unsavedTitle}
59
+ aria-hidden
60
+ />
55
61
  )}
56
62
  </div>
57
63
  <Button
@@ -59,7 +65,7 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
59
65
  size="icon-sm"
60
66
  className="text-muted-foreground hover:text-foreground"
61
67
  onClick={onClose}
62
- aria-label="Close design panel"
68
+ aria-label={t.stylePanel.closePanelAria}
63
69
  >
64
70
  <X className="size-3.5" />
65
71
  </Button>
@@ -74,9 +80,9 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
74
80
  )
75
81
  }
76
82
  >
77
- <Section title="Colors">
83
+ <Section title={t.stylePanel.colorsSection}>
78
84
  <ColorField
79
- label="Background"
85
+ label={t.stylePanel.backgroundLabel}
80
86
  value={draft.palette.bg}
81
87
  onChange={(v) =>
82
88
  update((d) => {
@@ -85,7 +91,7 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
85
91
  }
86
92
  />
87
93
  <ColorField
88
- label="Text"
94
+ label={t.stylePanel.textLabel}
89
95
  value={draft.palette.text}
90
96
  onChange={(v) =>
91
97
  update((d) => {
@@ -94,7 +100,7 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
94
100
  }
95
101
  />
96
102
  <ColorField
97
- label="Accent"
103
+ label={t.stylePanel.accentLabel}
98
104
  value={draft.palette.accent}
99
105
  onChange={(v) =>
100
106
  update((d) => {
@@ -106,9 +112,9 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
106
112
 
107
113
  <Separator />
108
114
 
109
- <Section title="Typography">
115
+ <Section title={t.stylePanel.typographySection}>
110
116
  <FontField
111
- label="Display"
117
+ label={t.stylePanel.displayFontLabel}
112
118
  value={draft.fonts.display}
113
119
  onChange={(v) =>
114
120
  update((d) => {
@@ -117,7 +123,7 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
117
123
  }
118
124
  />
119
125
  <FontField
120
- label="Body"
126
+ label={t.stylePanel.bodyFontLabel}
121
127
  value={draft.fonts.body}
122
128
  onChange={(v) =>
123
129
  update((d) => {
@@ -126,7 +132,7 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
126
132
  }
127
133
  />
128
134
  <SliderField
129
- label="Hero"
135
+ label={t.stylePanel.heroLabel}
130
136
  value={draft.typeScale.hero}
131
137
  min={48}
132
138
  max={240}
@@ -139,7 +145,7 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
139
145
  }
140
146
  />
141
147
  <SliderField
142
- label="Body"
148
+ label={t.stylePanel.bodyLabel}
143
149
  value={draft.typeScale.body}
144
150
  min={16}
145
151
  max={72}
@@ -155,9 +161,9 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
155
161
 
156
162
  <Separator />
157
163
 
158
- <Section title="Shape">
164
+ <Section title={t.stylePanel.shapeSection}>
159
165
  <SliderField
160
- label="Radius"
166
+ label={t.stylePanel.radiusLabel}
161
167
  value={draft.radius}
162
168
  min={0}
163
169
  max={80}
@@ -181,6 +187,7 @@ export function DesignToggleButton({
181
187
  active: boolean;
182
188
  onToggle: () => void;
183
189
  }) {
190
+ const t = useLocale();
184
191
  if (import.meta.env.PROD) return null;
185
192
  return (
186
193
  <Button
@@ -188,10 +195,10 @@ export function DesignToggleButton({
188
195
  variant={active ? 'default' : 'ghost'}
189
196
  onClick={onToggle}
190
197
  data-design-ui
191
- title="Design tokens"
198
+ title={t.stylePanel.designToggleTitle}
192
199
  >
193
200
  <Palette className="size-3.5" />
194
- <span className="hidden md:inline">Design</span>
201
+ <span className="hidden md:inline">{t.stylePanel.designToggle}</span>
195
202
  </Button>
196
203
  );
197
204
  }
@@ -247,6 +254,7 @@ function FontField({
247
254
  onChange: (v: string) => void;
248
255
  }) {
249
256
  const matched = FONT_PRESETS.find((p) => p.value === value);
257
+ const tFont = useLocale();
250
258
  return (
251
259
  <Field label={label}>
252
260
  <Select
@@ -266,7 +274,7 @@ function FontField({
266
274
  ))}
267
275
  {!matched && (
268
276
  <SelectItem value="__custom__" className="text-xs">
269
- Custom…
277
+ {tFont.stylePanel.fontPresetCustom}
270
278
  </SelectItem>
271
279
  )}
272
280
  </SelectContent>
@@ -8,11 +8,13 @@ import {
8
8
  DropdownMenuItem,
9
9
  DropdownMenuTrigger,
10
10
  } from '@/components/ui/dropdown-menu';
11
+ import { useLocale } from '@/lib/use-locale';
11
12
  import { cn } from '@/lib/utils';
12
13
 
13
14
  export function ThemeToggle() {
14
15
  const { theme, setTheme } = useTheme();
15
16
  const [mounted, setMounted] = useState(false);
17
+ const t = useLocale();
16
18
 
17
19
  useEffect(() => {
18
20
  setMounted(true);
@@ -22,8 +24,8 @@ export function ThemeToggle() {
22
24
  <DropdownMenu>
23
25
  <DropdownMenuTrigger
24
26
  type="button"
25
- aria-label="Toggle theme"
26
- title="Theme"
27
+ aria-label={t.themeToggle.toggleAria}
28
+ title={t.themeToggle.title}
27
29
  className={cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), 'relative')}
28
30
  >
29
31
  <Sun className="size-3.5 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
@@ -35,21 +37,21 @@ export function ThemeToggle() {
35
37
  data-active={mounted && theme === 'light'}
36
38
  >
37
39
  <Sun />
38
- Light
40
+ {t.themeToggle.light}
39
41
  </DropdownMenuItem>
40
42
  <DropdownMenuItem
41
43
  onSelect={() => setTheme('dark')}
42
44
  data-active={mounted && theme === 'dark'}
43
45
  >
44
46
  <Moon />
45
- Dark
47
+ {t.themeToggle.dark}
46
48
  </DropdownMenuItem>
47
49
  <DropdownMenuItem
48
50
  onSelect={() => setTheme('system')}
49
51
  data-active={mounted && theme === 'system'}
50
52
  >
51
53
  <Monitor />
52
- System
54
+ {t.themeToggle.system}
53
55
  </DropdownMenuItem>
54
56
  </DropdownMenuContent>
55
57
  </DropdownMenu>
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { ScrollArea } from '@/components/ui/scroll-area';
3
+ import { format, useLocale } from '@/lib/use-locale';
3
4
  import { cn } from '@/lib/utils';
4
5
  import type { DesignSystem } from '../lib/design';
5
6
  import type { Page } from '../lib/sdk';
@@ -27,6 +28,7 @@ export function ThumbnailRail({
27
28
  orientation = 'vertical',
28
29
  }: Props) {
29
30
  const activeRef = useRef<HTMLButtonElement | null>(null);
31
+ const t = useLocale();
30
32
 
31
33
  // biome-ignore lint/correctness/useExhaustiveDependencies: `current` triggers re-scroll on selection change
32
34
  useEffect(() => {
@@ -55,7 +57,7 @@ export function ThumbnailRail({
55
57
  type="button"
56
58
  ref={active ? activeRef : undefined}
57
59
  onClick={() => onSelect(i)}
58
- aria-label={`Go to page ${i + 1}`}
60
+ aria-label={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
59
61
  aria-current={active ? 'true' : undefined}
60
62
  className={cn('group/thumb relative flex shrink-0 flex-col items-center gap-1.5')}
61
63
  >
@@ -95,7 +97,7 @@ export function ThumbnailRail({
95
97
  <ScrollArea className="h-full border-r border-hairline bg-sidebar">
96
98
  <aside className="flex flex-col gap-2 px-3 py-3">
97
99
  <div className="flex items-baseline justify-between px-1 pb-1">
98
- <span className="eyebrow">Pages</span>
100
+ <span className="eyebrow">{t.thumbnailRail.pages}</span>
99
101
  <span className="folio">{pages.length.toString().padStart(2, '0')}</span>
100
102
  </div>
101
103
  {pages.map((PageComp, i) => {
Binary file
@@ -1,8 +1,3 @@
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
1
  import { createElement } from 'react';
7
2
  import { createRoot } from 'react-dom/client';
8
3
  import { designToCssVars } from './design';
@@ -39,9 +34,7 @@ export async function exportSlideAsHtml(slide: SlideModule, slideId: string): Pr
39
34
  const buf = new Uint8Array(await res.arrayBuffer());
40
35
  const name = uniqueAssetName(absolute, usedNames);
41
36
  assets.set(url, { name, bytes: buf });
42
- } catch {
43
- // ignore unreachable assets
44
- }
37
+ } catch {}
45
38
  }
46
39
 
47
40
  const rewrittenPages = pagesHtml.map((html) => rewriteUrls(html, assets, 'html'));
@@ -175,7 +168,6 @@ function looksLikeAsset(url: string): boolean {
175
168
  if (url.startsWith('mailto:') || url.startsWith('javascript:')) return false;
176
169
  const abs = toAbsolute(url);
177
170
  if (!abs) return false;
178
- // Same-origin only: we can only fetch local assets.
179
171
  try {
180
172
  const u = new URL(abs);
181
173
  if (u.origin !== window.location.origin) return false;
@@ -1,8 +1,3 @@
1
- // Exports a slide as a PDF via the browser's native print engine.
2
- // Each page in `slide.default` becomes one PDF page at 1920×1080.
3
- // Text stays selectable and inline SVG remains vector — `window.print()`
4
- // preserves both. The user picks "Save as PDF" in the print dialog.
5
-
6
1
  import { createElement } from 'react';
7
2
  import { createRoot, type Root } from 'react-dom/client';
8
3
  import { designToCssVars } from './design';
@@ -2,12 +2,14 @@ import { useCallback } from 'react';
2
2
 
3
3
  export type EditOp =
4
4
  | { kind: 'set-style'; key: string; value: string | null }
5
- | { kind: 'set-text'; value: string }
5
+ | { kind: 'set-text'; value: string; prevText?: string }
6
6
  | { kind: 'set-attr-asset'; attr: string; assetPath: string; previewUrl: string }
7
7
  | { kind: 'replace-placeholder-with-image'; assetPath: string };
8
8
 
9
9
  export type Edit = { line: number; column: number; ops: EditOp[] };
10
10
 
11
+ export type EditResult = { ok: boolean; error?: string };
12
+
11
13
  export class NoOpEditError extends Error {
12
14
  constructor() {
13
15
  super(
@@ -37,9 +39,11 @@ export function useEditor(slideId: string) {
37
39
  );
38
40
 
39
41
  // Batch many element edits into one file write and one HMR tick.
42
+ // Returns one result per input edit so callers can keep failed
43
+ // edits buffered while clearing the ones that landed.
40
44
  const applyEdits = useCallback(
41
- async (edits: Edit[]) => {
42
- if (edits.length === 0) return;
45
+ async (edits: Edit[]): Promise<EditResult[]> => {
46
+ if (edits.length === 0) return [];
43
47
  const res = await fetch('/__edit/batch', {
44
48
  method: 'POST',
45
49
  headers: { 'content-type': 'application/json' },
@@ -47,14 +51,12 @@ export function useEditor(slideId: string) {
47
51
  });
48
52
  const body = (await res.json().catch(() => ({}))) as {
49
53
  error?: string;
50
- changed?: boolean;
51
- results?: Array<{ ok: boolean; error?: string }>;
54
+ results?: EditResult[];
52
55
  };
53
56
  if (!res.ok) {
54
57
  throw new Error(body.error ?? `POST /__edit/batch → ${res.status}`);
55
58
  }
56
- const failed = body.results?.find((r) => !r.ok);
57
- if (failed?.error) throw new Error(failed.error);
59
+ return body.results ?? [];
58
60
  },
59
61
  [slideId],
60
62
  );
@@ -1,6 +1,3 @@
1
- // Helpers used by the PDF export flow to wait for the page to settle before
2
- // invoking window.print(). Browser-only — no Node / headless dependency.
3
-
4
1
  const DEFAULT_WAITFOR_TIMEOUT_MS = 10_000;
5
2
 
6
3
  export async function waitForFonts(): Promise<void> {
@@ -38,7 +35,6 @@ export async function waitForDataWaitfor(
38
35
  );
39
36
  }
40
37
 
41
- /** Returns true if `frame` has no running finite-iteration animations. */
42
38
  export function isFrameAnimationSettled(frame: Element): boolean {
43
39
  if (typeof document.getAnimations !== 'function') return true;
44
40
  for (const anim of document.getAnimations()) {
@@ -11,8 +11,7 @@ export type SlideModule = {
11
11
  default: Page[];
12
12
  meta?: SlideMeta;
13
13
  design?: DesignSystem;
14
- // Index-aligned with `default`. Each entry is the speaker note for the
15
- // page at the same position. Used by Presenter View only.
14
+ // Index-aligned with `default`.
16
15
  notes?: (string | undefined)[];
17
16
  };
18
17
 
@@ -0,0 +1,20 @@
1
+ import config from 'virtual:open-slide/config';
2
+ import { en } from '../../locale/en';
3
+ import type { Locale, Plural } from '../../locale/types';
4
+
5
+ const resolved: Locale = (config.locale as Locale | undefined) ?? en;
6
+
7
+ export function useLocale(): Locale {
8
+ return resolved;
9
+ }
10
+
11
+ export function format(template: string, vars: Record<string, string | number>): string {
12
+ return template.replace(/\{(\w+)\}/g, (m, key) => {
13
+ const v = vars[key];
14
+ return v === undefined ? m : String(v);
15
+ });
16
+ }
17
+
18
+ export function plural(count: number, forms: Plural): string {
19
+ return count === 1 ? forms.one : forms.other;
20
+ }