@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.
- package/dist/{build-DqfKmw9h.js → build-4wOJF1l4.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-DweCbRkQ.d.ts → config-D2y1AXaN.d.ts} +3 -0
- package/dist/{config-CN7J0RDO.js → config-evLWCV1-.js} +378 -222
- package/dist/{dev-jWxtWHAG.js → dev-BUr0S-Ij.js} +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +1189 -0
- package/dist/{preview-CSA05Gfm.js → preview-DP_gIphz.js} +1 -1
- package/dist/types-BVvl_xup.d.ts +314 -0
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +1 -1
- package/package.json +7 -1
- package/src/app/app.tsx +6 -2
- package/src/app/components/asset-view.tsx +87 -64
- package/src/app/components/click-nav-zones.tsx +4 -2
- package/src/app/components/inspector/comment-widget.tsx +9 -7
- package/src/app/components/inspector/inspect-overlay.tsx +79 -17
- package/src/app/components/inspector/inspector-panel.tsx +68 -39
- package/src/app/components/inspector/inspector-provider.tsx +185 -58
- package/src/app/components/inspector/save-bar.tsx +6 -5
- package/src/app/components/panel/save-card.tsx +12 -9
- package/src/app/components/pdf-progress-toast.tsx +11 -4
- package/src/app/components/player.tsx +7 -25
- package/src/app/components/present/control-bar.tsx +17 -10
- package/src/app/components/present/help-overlay.tsx +18 -17
- package/src/app/components/present/overview-grid.tsx +6 -9
- package/src/app/components/present/use-presenter-channel.ts +3 -10
- package/src/app/components/sidebar/folder-item.tsx +16 -9
- package/src/app/components/sidebar/icon-picker.tsx +4 -5
- package/src/app/components/sidebar/sidebar.tsx +87 -25
- package/src/app/components/slide-canvas.tsx +1 -10
- package/src/app/components/style-panel/design-provider.tsx +2 -6
- package/src/app/components/style-panel/style-panel.tsx +26 -18
- package/src/app/components/theme-toggle.tsx +7 -5
- package/src/app/components/thumbnail-rail.tsx +4 -2
- package/src/app/favicon.ico +0 -0
- package/src/app/lib/export-html.ts +1 -9
- package/src/app/lib/export-pdf.ts +0 -5
- package/src/app/lib/inspector/use-editor.ts +9 -7
- package/src/app/lib/print-ready.ts +0 -4
- package/src/app/lib/sdk.ts +1 -2
- package/src/app/lib/use-locale.ts +20 -0
- package/src/app/routes/home.tsx +90 -45
- package/src/app/routes/presenter.tsx +45 -25
- package/src/app/routes/slide.tsx +37 -24
- package/src/app/styles.css +28 -0
- package/src/app/virtual.d.ts +4 -0
- package/src/locale/en.ts +303 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +307 -0
- package/src/locale/types.ts +323 -0
- package/src/locale/zh-cn.ts +303 -0
- package/src/locale/zh-tw.ts +303 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { PANEL_TRANSITION_MS } from '@/components/panel/panel-shell';
|
|
2
3
|
import { findSlideSource, type SlideSourceHit } from '@/lib/inspector/fiber';
|
|
3
4
|
import { useInspector } from './inspector-provider';
|
|
4
5
|
|
|
5
|
-
type Highlight = {
|
|
6
|
+
type Highlight = { hit: SlideSourceHit };
|
|
6
7
|
|
|
7
8
|
type RelRect = { left: number; top: number; width: number; height: number };
|
|
8
9
|
|
|
9
10
|
const FRAME_FADE_MS = 150;
|
|
10
11
|
const FRAME_MORPH_MS = 180;
|
|
12
|
+
const LAYOUT_TRACK_MS = PANEL_TRANSITION_MS + FRAME_MORPH_MS;
|
|
11
13
|
|
|
12
14
|
export function InspectOverlay() {
|
|
13
15
|
const { active, slideId, selected, setSelected, cancel } = useInspector();
|
|
@@ -33,7 +35,7 @@ export function InspectOverlay() {
|
|
|
33
35
|
if (!el) return setHover(null);
|
|
34
36
|
const hit = findSlideSource(el, slideId, { hostOnly: true });
|
|
35
37
|
if (!hit) return setHover(null);
|
|
36
|
-
setHover({
|
|
38
|
+
setHover({ hit });
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
const onClick = (e: MouseEvent) => {
|
|
@@ -45,7 +47,7 @@ export function InspectOverlay() {
|
|
|
45
47
|
e.preventDefault();
|
|
46
48
|
e.stopPropagation();
|
|
47
49
|
setSelected({ line: hit.line, column: hit.column, anchor: hit.anchor });
|
|
48
|
-
setHover({
|
|
50
|
+
setHover({ hit });
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
window.addEventListener('pointermove', onMove, true);
|
|
@@ -64,7 +66,7 @@ export function InspectOverlay() {
|
|
|
64
66
|
overlayRef={overlayRef}
|
|
65
67
|
// Pin to the selection so the highlight tracks what the panel
|
|
66
68
|
// is editing even after the cursor moves away.
|
|
67
|
-
|
|
69
|
+
targetAnchor={selected?.anchor ?? hover?.hit.anchor ?? null}
|
|
68
70
|
/>
|
|
69
71
|
);
|
|
70
72
|
}
|
|
@@ -72,26 +74,77 @@ export function InspectOverlay() {
|
|
|
72
74
|
function FrameOverlay({
|
|
73
75
|
active,
|
|
74
76
|
overlayRef,
|
|
75
|
-
|
|
77
|
+
targetAnchor,
|
|
76
78
|
}: {
|
|
77
79
|
active: boolean;
|
|
78
80
|
overlayRef: React.RefObject<HTMLDivElement>;
|
|
79
|
-
|
|
81
|
+
targetAnchor: HTMLElement | null;
|
|
80
82
|
}) {
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
const [rect, setRect] = useState<RelRect | null>(null);
|
|
84
|
+
const [hasTarget, setHasTarget] = useState(false);
|
|
85
|
+
|
|
86
|
+
const measure = useCallback(() => {
|
|
87
|
+
const overlay = overlayRef.current;
|
|
88
|
+
if (!active || !targetAnchor?.isConnected || !overlay) {
|
|
89
|
+
setHasTarget(false);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const targetRect = targetAnchor.getBoundingClientRect();
|
|
94
|
+
const overlayRect = overlay.getBoundingClientRect();
|
|
95
|
+
const next = {
|
|
89
96
|
left: targetRect.left - overlayRect.left,
|
|
90
97
|
top: targetRect.top - overlayRect.top,
|
|
91
98
|
width: targetRect.width,
|
|
92
99
|
height: targetRect.height,
|
|
93
100
|
};
|
|
94
|
-
|
|
101
|
+
|
|
102
|
+
setHasTarget(true);
|
|
103
|
+
setRect((prev) => (sameRect(prev, next) ? prev : next));
|
|
104
|
+
}, [active, overlayRef, targetAnchor]);
|
|
105
|
+
|
|
106
|
+
useLayoutEffect(() => {
|
|
107
|
+
measure();
|
|
108
|
+
}, [measure]);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!active) {
|
|
112
|
+
setHasTarget(false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let scheduled = 0;
|
|
117
|
+
let tracking = 0;
|
|
118
|
+
const scheduleMeasure = () => {
|
|
119
|
+
cancelAnimationFrame(scheduled);
|
|
120
|
+
scheduled = requestAnimationFrame(measure);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const resizeObserver = new ResizeObserver(scheduleMeasure);
|
|
124
|
+
const root = document.querySelector<HTMLElement>('[data-inspector-root]');
|
|
125
|
+
if (root) resizeObserver.observe(root);
|
|
126
|
+
if (overlayRef.current) resizeObserver.observe(overlayRef.current);
|
|
127
|
+
if (targetAnchor) resizeObserver.observe(targetAnchor);
|
|
128
|
+
|
|
129
|
+
const stopAt = performance.now() + LAYOUT_TRACK_MS;
|
|
130
|
+
const trackPanelTransition = () => {
|
|
131
|
+
measure();
|
|
132
|
+
if (performance.now() < stopAt) tracking = requestAnimationFrame(trackPanelTransition);
|
|
133
|
+
};
|
|
134
|
+
tracking = requestAnimationFrame(trackPanelTransition);
|
|
135
|
+
|
|
136
|
+
window.addEventListener('resize', scheduleMeasure, true);
|
|
137
|
+
window.addEventListener('scroll', scheduleMeasure, true);
|
|
138
|
+
return () => {
|
|
139
|
+
resizeObserver.disconnect();
|
|
140
|
+
cancelAnimationFrame(scheduled);
|
|
141
|
+
cancelAnimationFrame(tracking);
|
|
142
|
+
window.removeEventListener('resize', scheduleMeasure, true);
|
|
143
|
+
window.removeEventListener('scroll', scheduleMeasure, true);
|
|
144
|
+
};
|
|
145
|
+
}, [active, measure, overlayRef, targetAnchor]);
|
|
146
|
+
|
|
147
|
+
const visible = !!(active && hasTarget && rect);
|
|
95
148
|
|
|
96
149
|
// First render after appearing: snap to the new rect (no transition).
|
|
97
150
|
// Subsequent rect changes in the same visible session: animate.
|
|
@@ -106,7 +159,6 @@ function FrameOverlay({
|
|
|
106
159
|
}, [visible]);
|
|
107
160
|
|
|
108
161
|
if (!active) return null;
|
|
109
|
-
const rect = lastRectRef.current;
|
|
110
162
|
const transition = morph
|
|
111
163
|
? `left ${FRAME_MORPH_MS}ms ease-out, top ${FRAME_MORPH_MS}ms ease-out, ` +
|
|
112
164
|
`width ${FRAME_MORPH_MS}ms ease-out, height ${FRAME_MORPH_MS}ms ease-out, ` +
|
|
@@ -134,6 +186,16 @@ function FrameOverlay({
|
|
|
134
186
|
);
|
|
135
187
|
}
|
|
136
188
|
|
|
189
|
+
function sameRect(a: RelRect | null, b: RelRect): boolean {
|
|
190
|
+
return (
|
|
191
|
+
!!a &&
|
|
192
|
+
Math.abs(a.left - b.left) < 0.5 &&
|
|
193
|
+
Math.abs(a.top - b.top) < 0.5 &&
|
|
194
|
+
Math.abs(a.width - b.width) < 0.5 &&
|
|
195
|
+
Math.abs(a.height - b.height) < 0.5
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
137
199
|
function pickElement(x: number, y: number): HTMLElement | null {
|
|
138
200
|
const stack = document.elementsFromPoint(x, y);
|
|
139
201
|
for (const el of stack) {
|
|
@@ -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">
|
|
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
|
<{pinSelected.anchor.tagName.toLowerCase()}>
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
184
|
+
<Section title={t.inspector.colorSection}>
|
|
180
185
|
<ColorField
|
|
181
|
-
label=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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=
|
|
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
|
-
{
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
|
719
|
+
<DialogTitle>{t.inspector.replaceImageDialogTitle}</DialogTitle>
|
|
698
720
|
<DialogDescription>
|
|
699
|
-
|
|
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">
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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"
|
|
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
|
-
|
|
815
|
+
{t.inspector.addNote}
|
|
787
816
|
</Button>
|
|
788
817
|
</div>
|
|
789
818
|
</div>
|