@open-slide/core 1.0.4 → 1.0.5

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 (46) hide show
  1. package/dist/{build-DqfKmw9h.js → build-CoON6kTb.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-CN7J0RDO.js → config-Bxtztw-H.js} +373 -221
  4. package/dist/{config-DweCbRkQ.d.ts → config-D2y1AXaN.d.ts} +3 -0
  5. package/dist/{dev-jWxtWHAG.js → dev-IezNC17X.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-BwYjtENY.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/inspector-panel.tsx +68 -39
  19. package/src/app/components/inspector/inspector-provider.tsx +185 -58
  20. package/src/app/components/inspector/save-bar.tsx +6 -2
  21. package/src/app/components/panel/save-card.tsx +12 -9
  22. package/src/app/components/pdf-progress-toast.tsx +11 -4
  23. package/src/app/components/present/control-bar.tsx +17 -10
  24. package/src/app/components/present/help-overlay.tsx +18 -17
  25. package/src/app/components/present/overview-grid.tsx +6 -4
  26. package/src/app/components/sidebar/folder-item.tsx +16 -7
  27. package/src/app/components/sidebar/icon-picker.tsx +4 -2
  28. package/src/app/components/sidebar/sidebar.tsx +87 -25
  29. package/src/app/components/style-panel/style-panel.tsx +26 -18
  30. package/src/app/components/theme-toggle.tsx +7 -5
  31. package/src/app/components/thumbnail-rail.tsx +4 -2
  32. package/src/app/favicon.ico +0 -0
  33. package/src/app/lib/inspector/use-editor.ts +9 -7
  34. package/src/app/lib/use-locale.ts +20 -0
  35. package/src/app/routes/home.tsx +90 -45
  36. package/src/app/routes/presenter.tsx +45 -25
  37. package/src/app/routes/slide.tsx +37 -24
  38. package/src/app/styles.css +28 -0
  39. package/src/app/virtual.d.ts +4 -0
  40. package/src/locale/en.ts +303 -0
  41. package/src/locale/format.ts +12 -0
  42. package/src/locale/index.ts +6 -0
  43. package/src/locale/ja.ts +307 -0
  44. package/src/locale/types.ts +323 -0
  45. package/src/locale/zh-cn.ts +303 -0
  46. package/src/locale/zh-tw.ts +303 -0
@@ -35,7 +35,9 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
35
35
  import { type AssetEntry, useAssets } from '@/lib/assets';
36
36
  import { findSlideSource } from '@/lib/inspector/fiber';
37
37
  import type { EditOp } from '@/lib/inspector/use-editor';
38
+ import { useLocale } from '@/lib/use-locale';
38
39
  import { cn } from '@/lib/utils';
40
+ import type { Locale } from '../../../locale/types';
39
41
  import { type SelectedTarget, useInspector } from './inspector-provider';
40
42
 
41
43
  type ElementSnapshot = {
@@ -57,6 +59,7 @@ export function InspectorPanel() {
57
59
  useInspector();
58
60
  const [snapshot, setSnapshot] = useState<ElementSnapshot | null>(null);
59
61
  const reloadCounter = useReloadCounter();
62
+ const t = useLocale();
60
63
 
61
64
  useEffect(() => {
62
65
  void reloadCounter;
@@ -138,7 +141,9 @@ export function InspectorPanel() {
138
141
  header={
139
142
  <>
140
143
  <div className="flex min-w-0 items-center gap-2">
141
- <span className="font-heading text-[12px] font-semibold tracking-tight">Inspect</span>
144
+ <span className="font-heading text-[12px] font-semibold tracking-tight">
145
+ {t.inspector.inspect}
146
+ </span>
142
147
  <span aria-hidden className="h-3 w-px bg-hairline" />
143
148
  <span className="rounded-[3px] border border-hairline bg-card px-1.5 py-px font-mono text-[10.5px] text-foreground/85">
144
149
  &lt;{pinSelected.anchor.tagName.toLowerCase()}&gt;
@@ -149,7 +154,7 @@ export function InspectorPanel() {
149
154
  size="icon-sm"
150
155
  className="text-muted-foreground hover:text-foreground"
151
156
  onClick={() => setSelected(null)}
152
- aria-label="Deselect"
157
+ aria-label={t.inspector.deselect}
153
158
  >
154
159
  <X className="size-3.5" />
155
160
  </Button>
@@ -158,14 +163,14 @@ export function InspectorPanel() {
158
163
  footer={<CommentsSection selected={pinSelected} onAdd={add} />}
159
164
  >
160
165
  {pinSnapshot.text !== null && (
161
- <Section title="Content">
166
+ <Section title={t.inspector.contentSection}>
162
167
  <ContentField snapshot={pinSnapshot} apply={apply} />
163
168
  </Section>
164
169
  )}
165
170
 
166
171
  <Separator />
167
172
 
168
- <Section title="Typography">
173
+ <Section title={t.inspector.typographySection}>
169
174
  <FontSizeField snapshot={pinSnapshot} apply={apply} />
170
175
  <FontWeightField snapshot={pinSnapshot} apply={apply} />
171
176
  <StyleToggles snapshot={pinSnapshot} apply={apply} />
@@ -176,15 +181,15 @@ export function InspectorPanel() {
176
181
 
177
182
  <Separator />
178
183
 
179
- <Section title="Color">
184
+ <Section title={t.inspector.colorSection}>
180
185
  <ColorField
181
- label="Text"
186
+ label={t.inspector.textColor}
182
187
  value={pinSnapshot.color}
183
188
  onChange={(v) => apply([{ kind: 'set-style', key: 'color', value: v }])}
184
189
  clearable={false}
185
190
  />
186
191
  <ColorField
187
- label="Background"
192
+ label={t.inspector.backgroundColor}
188
193
  value={pinSnapshot.backgroundColor ?? '#ffffff'}
189
194
  dim={!pinSnapshot.backgroundColor}
190
195
  onChange={(v) => apply([{ kind: 'set-style', key: 'backgroundColor', value: v }])}
@@ -196,7 +201,7 @@ export function InspectorPanel() {
196
201
  {pinSnapshot.imageSrc !== null && (
197
202
  <>
198
203
  <Separator />
199
- <Section title="Image">
204
+ <Section title={t.inspector.imageSection}>
200
205
  <ImageField slideId={slideId} src={pinSnapshot.imageSrc} apply={apply} />
201
206
  </Section>
202
207
  </>
@@ -205,7 +210,7 @@ export function InspectorPanel() {
205
210
  {pinSnapshot.placeholder && (
206
211
  <>
207
212
  <Separator />
208
- <Section title="Image placeholder">
213
+ <Section title={t.inspector.imagePlaceholderSection}>
209
214
  <PlaceholderField
210
215
  slideId={slideId}
211
216
  hint={pinSnapshot.placeholder.hint}
@@ -246,6 +251,7 @@ function ContentField({
246
251
  // candidates (Bopomofo/Pinyin only commit on candidate selection).
247
252
  const [local, setLocal] = useState(snapshot.text ?? '');
248
253
  const composingRef = useRef(false);
254
+ const t = useLocale();
249
255
 
250
256
  useEffect(() => {
251
257
  if (!composingRef.current) setLocal(snapshot.text ?? '');
@@ -272,7 +278,7 @@ function ContentField({
272
278
  }}
273
279
  rows={3}
274
280
  className="min-h-16 resize-none text-xs"
275
- placeholder="Element text"
281
+ placeholder={t.inspector.elementTextPlaceholder}
276
282
  />
277
283
  );
278
284
  }
@@ -287,8 +293,9 @@ function FontSizeField({
287
293
  const set = (px: number) => {
288
294
  apply([{ kind: 'set-style', key: 'fontSize', value: `${Math.round(px)}px` }]);
289
295
  };
296
+ const t = useLocale();
290
297
  return (
291
- <Field label="Size">
298
+ <Field label={t.inspector.sizeLabel}>
292
299
  <Slider
293
300
  min={8}
294
301
  max={200}
@@ -308,14 +315,16 @@ function FontSizeField({
308
315
  );
309
316
  }
310
317
 
311
- const WEIGHT_OPTIONS: { value: string; label: string }[] = [
312
- { value: '300', label: 'Light · 300' },
313
- { value: '400', label: 'Regular · 400' },
314
- { value: '500', label: 'Medium · 500' },
315
- { value: '600', label: 'Semibold · 600' },
316
- { value: '700', label: 'Bold · 700' },
317
- { value: '800', label: 'Extrabold · 800' },
318
- ];
318
+ function getWeightOptions(t: Locale): { value: string; label: string }[] {
319
+ return [
320
+ { value: '300', label: t.inspector.weightLight },
321
+ { value: '400', label: t.inspector.weightRegular },
322
+ { value: '500', label: t.inspector.weightMedium },
323
+ { value: '600', label: t.inspector.weightSemibold },
324
+ { value: '700', label: t.inspector.weightBold },
325
+ { value: '800', label: t.inspector.weightExtrabold },
326
+ ];
327
+ }
319
328
 
320
329
  function FontWeightField({
321
330
  snapshot,
@@ -324,8 +333,10 @@ function FontWeightField({
324
333
  snapshot: ElementSnapshot;
325
334
  apply: (ops: EditOp[]) => void;
326
335
  }) {
336
+ const t = useLocale();
337
+ const weightOptions = getWeightOptions(t);
327
338
  return (
328
- <Field label="Weight">
339
+ <Field label={t.inspector.weightLabel}>
329
340
  <Select
330
341
  value={String(snapshot.fontWeight)}
331
342
  onValueChange={(value) => {
@@ -343,7 +354,7 @@ function FontWeightField({
343
354
  <SelectValue />
344
355
  </SelectTrigger>
345
356
  <SelectContent>
346
- {WEIGHT_OPTIONS.map((opt) => (
357
+ {weightOptions.map((opt) => (
347
358
  <SelectItem key={opt.value} value={opt.value} className="text-xs">
348
359
  {opt.label}
349
360
  </SelectItem>
@@ -361,8 +372,9 @@ function StyleToggles({
361
372
  snapshot: ElementSnapshot;
362
373
  apply: (ops: EditOp[]) => void;
363
374
  }) {
375
+ const t = useLocale();
364
376
  return (
365
- <Field label="Style">
377
+ <Field label={t.inspector.styleLabel}>
366
378
  <Toggle
367
379
  size="sm"
368
380
  variant="outline"
@@ -370,7 +382,7 @@ function StyleToggles({
370
382
  onPressedChange={(v) =>
371
383
  apply([{ kind: 'set-style', key: 'fontWeight', value: v ? '700' : null }])
372
384
  }
373
- aria-label="Bold"
385
+ aria-label={t.inspector.boldAria}
374
386
  >
375
387
  <Bold className="size-3.5" />
376
388
  </Toggle>
@@ -381,7 +393,7 @@ function StyleToggles({
381
393
  onPressedChange={(v) =>
382
394
  apply([{ kind: 'set-style', key: 'fontStyle', value: v ? 'italic' : null }])
383
395
  }
384
- aria-label="Italic"
396
+ aria-label={t.inspector.italicAria}
385
397
  >
386
398
  <Italic className="size-3.5" />
387
399
  </Toggle>
@@ -400,8 +412,9 @@ function LineHeightField({
400
412
  const set = (n: number) => {
401
413
  apply([{ kind: 'set-style', key: 'lineHeight', value: String(round2(n)) }]);
402
414
  };
415
+ const t = useLocale();
403
416
  return (
404
- <Field label="Line height">
417
+ <Field label={t.inspector.lineHeightLabel}>
405
418
  <Slider
406
419
  min={0.8}
407
420
  max={3}
@@ -431,8 +444,9 @@ function LetterSpacingField({
431
444
  },
432
445
  ]);
433
446
  };
447
+ const t = useLocale();
434
448
  return (
435
- <Field label="Tracking">
449
+ <Field label={t.inspector.trackingLabel}>
436
450
  <Slider
437
451
  min={-5}
438
452
  max={20}
@@ -467,8 +481,9 @@ function TextAlignField({
467
481
  snapshot: ElementSnapshot;
468
482
  apply: (ops: EditOp[]) => void;
469
483
  }) {
484
+ const t = useLocale();
470
485
  return (
471
- <Field label="Align">
486
+ <Field label={t.inspector.alignLabel}>
472
487
  <ToggleGroup
473
488
  type="single"
474
489
  size="sm"
@@ -513,6 +528,7 @@ function ColorField({
513
528
  // Buffer the text input so intermediate hex like "#a" doesn't
514
529
  // commit until it parses as a full color.
515
530
  const [draft, setDraft] = useState(value);
531
+ const tColor = useLocale();
516
532
  useEffect(() => setDraft(value), [value]);
517
533
 
518
534
  const commitHex = (hex: string) => {
@@ -559,7 +575,7 @@ function ColorField({
559
575
  size="icon"
560
576
  className="size-8 text-muted-foreground hover:text-foreground"
561
577
  onClick={onClear}
562
- aria-label="Clear"
578
+ aria-label={tColor.inspector.clearAria}
563
579
  >
564
580
  <X className="size-3.5" />
565
581
  </Button>
@@ -578,6 +594,7 @@ function ImageField({
578
594
  apply: (ops: EditOp[]) => void;
579
595
  }) {
580
596
  const [open, setOpen] = useState(false);
597
+ const t = useLocale();
581
598
  return (
582
599
  <div className="space-y-2">
583
600
  <div className="flex items-center gap-3">
@@ -600,7 +617,7 @@ function ImageField({
600
617
  onClick={() => setOpen(true)}
601
618
  >
602
619
  <ImageIcon className="size-3.5" />
603
- Replace…
620
+ {t.inspector.replace}
604
621
  </Button>
605
622
  </div>
606
623
  {open && (
@@ -639,10 +656,12 @@ function PlaceholderField({
639
656
  }) {
640
657
  const [open, setOpen] = useState(false);
641
658
  const [submitting, setSubmitting] = useState(false);
659
+ const t = useLocale();
642
660
  return (
643
661
  <div className="space-y-2">
644
662
  <p className="text-[11px] leading-relaxed text-muted-foreground">
645
- Hint: <span className="font-medium text-foreground">{hint}</span>
663
+ {t.inspector.placeholderHintLabel}{' '}
664
+ <span className="font-medium text-foreground">{hint}</span>
646
665
  </p>
647
666
  <Button
648
667
  type="button"
@@ -653,7 +672,7 @@ function PlaceholderField({
653
672
  onClick={() => setOpen(true)}
654
673
  >
655
674
  <ImageIcon className="size-3.5" />
656
- Replace…
675
+ {t.inspector.replace}
657
676
  </Button>
658
677
  {open && (
659
678
  <AssetPickerDialog
@@ -690,21 +709,28 @@ function AssetPickerDialog({
690
709
  }) {
691
710
  const { assets, loading } = useAssets(slideId);
692
711
  const images = assets.filter((a) => a.mime.startsWith('image/'));
712
+ const t = useLocale();
713
+ const path = `slides/${slideId}/assets/`;
714
+ const [descPrefix, descSuffix] = t.inspector.replaceImageDescription.split('{path}');
693
715
  return (
694
716
  <Dialog open onOpenChange={(o) => !o && onClose()}>
695
717
  <DialogContent className="sm:max-w-xl">
696
718
  <DialogHeader>
697
- <DialogTitle>Replace image</DialogTitle>
719
+ <DialogTitle>{t.inspector.replaceImageDialogTitle}</DialogTitle>
698
720
  <DialogDescription>
699
- Pick an asset from <span className="font-mono">slides/{slideId}/assets/</span>.
721
+ {descPrefix}
722
+ <span className="font-mono">{path}</span>
723
+ {descSuffix}
700
724
  </DialogDescription>
701
725
  </DialogHeader>
702
726
  <div className="max-h-[60vh] overflow-y-auto">
703
727
  {loading ? (
704
- <p className="px-1 py-6 text-center text-xs text-muted-foreground">Loading…</p>
728
+ <p className="px-1 py-6 text-center text-xs text-muted-foreground">
729
+ {t.inspector.pickerLoading}
730
+ </p>
705
731
  ) : images.length === 0 ? (
706
732
  <p className="px-1 py-6 text-center text-xs text-muted-foreground">
707
- No images in this slide's assets folder yet. Add some from the Assets tab.
733
+ {t.inspector.pickerEmpty}
708
734
  </p>
709
735
  ) : (
710
736
  <div className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-3">
@@ -750,6 +776,7 @@ function CommentsSection({
750
776
  }) {
751
777
  const [draft, setDraft] = useState('');
752
778
  const [submitting, setSubmitting] = useState(false);
779
+ const t = useLocale();
753
780
 
754
781
  const submit = async () => {
755
782
  const trimmed = draft.trim();
@@ -764,7 +791,7 @@ function CommentsSection({
764
791
  };
765
792
 
766
793
  return (
767
- <Section title="Note for the agent">
794
+ <Section title={t.inspector.noteForAgent}>
768
795
  <div className="flex flex-col gap-2">
769
796
  <div className="comment-cue rounded-[6px]">
770
797
  <Textarea
@@ -776,14 +803,16 @@ function CommentsSection({
776
803
  submit();
777
804
  }
778
805
  }}
779
- placeholder="Describe a change for the agent…"
806
+ placeholder={t.inspector.noteAgentPlaceholder}
780
807
  className="min-h-16 resize-none text-[12px]"
781
808
  />
782
809
  </div>
783
810
  <div className="flex items-center justify-between gap-2">
784
- <span className="font-mono text-[10.5px] text-muted-foreground/70">⌘↵ to send</span>
811
+ <span className="font-mono text-[10.5px] text-muted-foreground/70">
812
+ {t.inspector.noteShortcutHint}
813
+ </span>
785
814
  <Button size="sm" variant="brand" disabled={submitting || !draft.trim()} onClick={submit}>
786
- Add note
815
+ {t.inspector.addNote}
787
816
  </Button>
788
817
  </div>
789
818
  </div>