@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.
- package/dist/{build-DqfKmw9h.js → build-CoON6kTb.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-CN7J0RDO.js → config-Bxtztw-H.js} +373 -221
- package/dist/{config-DweCbRkQ.d.ts → config-D2y1AXaN.d.ts} +3 -0
- package/dist/{dev-jWxtWHAG.js → dev-IezNC17X.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-BwYjtENY.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/inspector-panel.tsx +68 -39
- package/src/app/components/inspector/inspector-provider.tsx +185 -58
- package/src/app/components/inspector/save-bar.tsx +6 -2
- package/src/app/components/panel/save-card.tsx +12 -9
- package/src/app/components/pdf-progress-toast.tsx +11 -4
- 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 -4
- package/src/app/components/sidebar/folder-item.tsx +16 -7
- package/src/app/components/sidebar/icon-picker.tsx +4 -2
- package/src/app/components/sidebar/sidebar.tsx +87 -25
- 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/inspector/use-editor.ts +9 -7
- 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
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
searchSvgl,
|
|
38
38
|
useAssets,
|
|
39
39
|
} from '@/lib/assets';
|
|
40
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
40
41
|
import { cn } from '@/lib/utils';
|
|
41
42
|
|
|
42
43
|
type Props = { slideId: string };
|
|
@@ -56,6 +57,7 @@ export function AssetView({ slideId }: Props) {
|
|
|
56
57
|
const [logoSearchOpen, setLogoSearchOpen] = useState(false);
|
|
57
58
|
const dragDepth = useRef(0);
|
|
58
59
|
const inputId = useId();
|
|
60
|
+
const t = useLocale();
|
|
59
61
|
|
|
60
62
|
const existingNames = new Set(assets.map((a) => a.name));
|
|
61
63
|
|
|
@@ -68,19 +70,19 @@ export function AssetView({ slideId }: Props) {
|
|
|
68
70
|
if (decision === 'cancel') return;
|
|
69
71
|
if (decision === 'replace') {
|
|
70
72
|
const res = await upload(file, { overwrite: true });
|
|
71
|
-
if (!res.ok) toast.error(
|
|
72
|
-
else toast.success(
|
|
73
|
+
if (!res.ok) toast.error(format(t.asset.toastUploadFailed, { status: res.status }));
|
|
74
|
+
else toast.success(format(t.asset.toastReplaced, { name: file.name }));
|
|
73
75
|
return;
|
|
74
76
|
}
|
|
75
77
|
const next = renamedCopy(file, existingNames);
|
|
76
78
|
const res = await upload(next, { overwrite: false });
|
|
77
|
-
if (!res.ok) toast.error(
|
|
78
|
-
else toast.success(
|
|
79
|
+
if (!res.ok) toast.error(format(t.asset.toastUploadFailed, { status: res.status }));
|
|
80
|
+
else toast.success(format(t.asset.toastUploadedAs, { name: next.name }));
|
|
79
81
|
return;
|
|
80
82
|
}
|
|
81
83
|
const res = await upload(file);
|
|
82
|
-
if (!res.ok) toast.error(
|
|
83
|
-
else toast.success(
|
|
84
|
+
if (!res.ok) toast.error(format(t.asset.toastUploadFailed, { status: res.status }));
|
|
85
|
+
else toast.success(format(t.asset.toastUploaded, { name: file.name }));
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
async function handleFiles(files: FileList | File[]) {
|
|
@@ -95,14 +97,14 @@ export function AssetView({ slideId }: Props) {
|
|
|
95
97
|
if (!available) {
|
|
96
98
|
return (
|
|
97
99
|
<div className="flex h-full items-center justify-center px-4 text-center text-sm text-muted-foreground">
|
|
98
|
-
|
|
100
|
+
{t.asset.devOnlyMessage}
|
|
99
101
|
</div>
|
|
100
102
|
);
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
return (
|
|
104
106
|
<section
|
|
105
|
-
aria-label=
|
|
107
|
+
aria-label={t.asset.sectionAria}
|
|
106
108
|
className={cn('relative flex h-full flex-col bg-background')}
|
|
107
109
|
onDragEnter={(e) => {
|
|
108
110
|
if (!hasFiles(e)) return;
|
|
@@ -131,16 +133,16 @@ export function AssetView({ slideId }: Props) {
|
|
|
131
133
|
>
|
|
132
134
|
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-hairline bg-sidebar px-6 py-3">
|
|
133
135
|
<div className="min-w-0">
|
|
134
|
-
<span className="eyebrow">
|
|
136
|
+
<span className="eyebrow">{t.asset.eyebrow}</span>
|
|
135
137
|
<p className="mt-0.5 truncate text-[12px] text-muted-foreground">
|
|
136
138
|
<span className="font-mono text-[11.5px]">slides/{slideId}/assets/</span>
|
|
137
139
|
{!loading && (
|
|
138
140
|
<>
|
|
139
141
|
<span className="mx-2 opacity-50">·</span>
|
|
140
142
|
<span className="folio">
|
|
141
|
-
{assets.length.
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
{format(assets.length === 1 ? t.asset.fileCount.one : t.asset.fileCount.other, {
|
|
144
|
+
count: assets.length.toString().padStart(2, '0'),
|
|
145
|
+
})}
|
|
144
146
|
</span>
|
|
145
147
|
</>
|
|
146
148
|
)}
|
|
@@ -156,7 +158,7 @@ export function AssetView({ slideId }: Props) {
|
|
|
156
158
|
)}
|
|
157
159
|
>
|
|
158
160
|
<Search className="size-3.5" />
|
|
159
|
-
<span>
|
|
161
|
+
<span>{t.asset.searchLogos}</span>
|
|
160
162
|
</button>
|
|
161
163
|
<label
|
|
162
164
|
htmlFor={inputId}
|
|
@@ -167,7 +169,7 @@ export function AssetView({ slideId }: Props) {
|
|
|
167
169
|
)}
|
|
168
170
|
>
|
|
169
171
|
<Upload className="size-3.5" />
|
|
170
|
-
<span>
|
|
172
|
+
<span>{t.asset.upload}</span>
|
|
171
173
|
</label>
|
|
172
174
|
<input
|
|
173
175
|
id={inputId}
|
|
@@ -187,7 +189,7 @@ export function AssetView({ slideId }: Props) {
|
|
|
187
189
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
188
190
|
{loading ? (
|
|
189
191
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
|
190
|
-
|
|
192
|
+
{t.asset.loading}
|
|
191
193
|
</div>
|
|
192
194
|
) : assets.length === 0 ? (
|
|
193
195
|
<EmptyState />
|
|
@@ -205,15 +207,15 @@ export function AssetView({ slideId }: Props) {
|
|
|
205
207
|
return;
|
|
206
208
|
}
|
|
207
209
|
if (existingNames.has(next)) {
|
|
208
|
-
toast.error(
|
|
210
|
+
toast.error(t.asset.nameAlreadyExists);
|
|
209
211
|
return;
|
|
210
212
|
}
|
|
211
213
|
const res = await rename(asset.name, next);
|
|
212
214
|
if (!res.ok) {
|
|
213
|
-
toast.error(
|
|
215
|
+
toast.error(format(t.asset.toastRenameFailed, { status: res.status }));
|
|
214
216
|
return;
|
|
215
217
|
}
|
|
216
|
-
toast.success(
|
|
218
|
+
toast.success(format(t.asset.toastRenamed, { name: next }));
|
|
217
219
|
setRenaming(null);
|
|
218
220
|
}}
|
|
219
221
|
/>
|
|
@@ -241,7 +243,7 @@ export function AssetView({ slideId }: Props) {
|
|
|
241
243
|
<div className="absolute inset-x-0 bottom-8 flex justify-center">
|
|
242
244
|
<div className="flex animate-in items-center gap-2 rounded-[6px] border border-border bg-card px-3 py-1.5 text-[12px] font-medium shadow-floating fade-in-0 slide-in-from-bottom-1 duration-300">
|
|
243
245
|
<ArrowDownToLine className="size-3.5 text-brand" />
|
|
244
|
-
<span>
|
|
246
|
+
<span>{t.asset.dropToUpload}</span>
|
|
245
247
|
</div>
|
|
246
248
|
</div>
|
|
247
249
|
</div>
|
|
@@ -265,8 +267,8 @@ export function AssetView({ slideId }: Props) {
|
|
|
265
267
|
const target = confirmDelete;
|
|
266
268
|
setConfirmDelete(null);
|
|
267
269
|
const res = await remove(target.name);
|
|
268
|
-
if (!res.ok) toast.error(
|
|
269
|
-
else toast.success(
|
|
270
|
+
if (!res.ok) toast.error(format(t.asset.toastDeleteFailed, { status: res.status }));
|
|
271
|
+
else toast.success(format(t.asset.toastDeleted, { name: target.name }));
|
|
270
272
|
}}
|
|
271
273
|
/>
|
|
272
274
|
)}
|
|
@@ -284,16 +286,20 @@ export function AssetView({ slideId }: Props) {
|
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
function EmptyState() {
|
|
289
|
+
const t = useLocale();
|
|
287
290
|
return (
|
|
288
291
|
<div className="flex h-full flex-col items-center justify-center gap-4 px-6 py-16 text-center">
|
|
289
292
|
<div className="flex size-12 items-center justify-center rounded-full border border-hairline bg-card text-muted-foreground">
|
|
290
293
|
<ImageIcon className="size-5" />
|
|
291
294
|
</div>
|
|
292
295
|
<div>
|
|
293
|
-
<p className="font-heading text-[14px] font-semibold tracking-tight">
|
|
296
|
+
<p className="font-heading text-[14px] font-semibold tracking-tight">
|
|
297
|
+
{t.asset.noAssetsYet}
|
|
298
|
+
</p>
|
|
294
299
|
<p className="mt-1 max-w-xs text-[12.5px] leading-relaxed text-muted-foreground">
|
|
295
|
-
|
|
296
|
-
.
|
|
300
|
+
{t.asset.noAssetsHintPrefix}
|
|
301
|
+
<span className="font-mono text-foreground">{t.asset.upload}</span>
|
|
302
|
+
{t.asset.noAssetsHintSuffix}
|
|
297
303
|
</p>
|
|
298
304
|
</div>
|
|
299
305
|
</div>
|
|
@@ -334,12 +340,13 @@ function AssetCard({
|
|
|
334
340
|
onDelete: () => void;
|
|
335
341
|
}) {
|
|
336
342
|
const isImage = asset.mime.startsWith('image/');
|
|
343
|
+
const t = useLocale();
|
|
337
344
|
return (
|
|
338
345
|
<div className="group relative flex flex-col overflow-hidden rounded-[6px] border border-border bg-card shadow-edge transition-shadow hover:shadow-floating focus-within:ring-2 focus-within:ring-ring/30">
|
|
339
346
|
<button
|
|
340
347
|
type="button"
|
|
341
348
|
onClick={onPreview}
|
|
342
|
-
aria-label={
|
|
349
|
+
aria-label={format(t.asset.previewAria, { name: asset.name })}
|
|
343
350
|
className="relative flex aspect-square w-full items-center justify-center overflow-hidden border-b border-hairline bg-[repeating-conic-gradient(theme(colors.muted)_0_25%,transparent_0_50%)] bg-[length:14px_14px]"
|
|
344
351
|
>
|
|
345
352
|
{isImage ? (
|
|
@@ -367,7 +374,7 @@ function AssetCard({
|
|
|
367
374
|
<DropdownMenu>
|
|
368
375
|
<DropdownMenuTrigger
|
|
369
376
|
type="button"
|
|
370
|
-
aria-label={
|
|
377
|
+
aria-label={format(t.asset.actionsAria, { name: asset.name })}
|
|
371
378
|
className={cn(
|
|
372
379
|
buttonVariants({ variant: 'ghost', size: 'icon-xs' }),
|
|
373
380
|
'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 aria-expanded:opacity-100',
|
|
@@ -378,15 +385,15 @@ function AssetCard({
|
|
|
378
385
|
<DropdownMenuContent align="end" className="min-w-[160px]">
|
|
379
386
|
<DropdownMenuItem onSelect={onPreview}>
|
|
380
387
|
<ImageIcon />
|
|
381
|
-
|
|
388
|
+
{t.asset.previewMenuItem}
|
|
382
389
|
</DropdownMenuItem>
|
|
383
390
|
<DropdownMenuItem onSelect={onRename}>
|
|
384
391
|
<Pencil />
|
|
385
|
-
|
|
392
|
+
{t.asset.renameMenuItem}
|
|
386
393
|
</DropdownMenuItem>
|
|
387
394
|
<DropdownMenuItem onSelect={onDelete}>
|
|
388
395
|
<Trash2 />
|
|
389
|
-
|
|
396
|
+
{t.asset.deleteMenuItem}
|
|
390
397
|
</DropdownMenuItem>
|
|
391
398
|
</DropdownMenuContent>
|
|
392
399
|
</DropdownMenu>
|
|
@@ -474,23 +481,27 @@ function ConflictDialog({
|
|
|
474
481
|
file: File;
|
|
475
482
|
onChoose: (decision: 'replace' | 'rename' | 'cancel') => void;
|
|
476
483
|
}) {
|
|
484
|
+
const t = useLocale();
|
|
485
|
+
const [descPrefix, descSuffix] = t.asset.conflictDescription.split('{name}');
|
|
477
486
|
return (
|
|
478
487
|
<Dialog open onOpenChange={(open) => !open && onChoose('cancel')}>
|
|
479
488
|
<DialogContent>
|
|
480
489
|
<DialogHeader>
|
|
481
|
-
<DialogTitle>
|
|
490
|
+
<DialogTitle>{t.asset.conflictTitle}</DialogTitle>
|
|
482
491
|
<DialogDescription>
|
|
483
|
-
|
|
492
|
+
{descPrefix}
|
|
493
|
+
<span className="font-mono">{file.name}</span>
|
|
494
|
+
{descSuffix}
|
|
484
495
|
</DialogDescription>
|
|
485
496
|
</DialogHeader>
|
|
486
497
|
<DialogFooter>
|
|
487
498
|
<Button variant="outline" onClick={() => onChoose('cancel')}>
|
|
488
|
-
|
|
499
|
+
{t.common.cancel}
|
|
489
500
|
</Button>
|
|
490
501
|
<Button variant="outline" onClick={() => onChoose('rename')}>
|
|
491
|
-
|
|
502
|
+
{t.asset.conflictRenameCopy}
|
|
492
503
|
</Button>
|
|
493
|
-
<Button onClick={() => onChoose('replace')}>
|
|
504
|
+
<Button onClick={() => onChoose('replace')}>{t.asset.conflictReplace}</Button>
|
|
494
505
|
</DialogFooter>
|
|
495
506
|
</DialogContent>
|
|
496
507
|
</Dialog>
|
|
@@ -506,22 +517,25 @@ function DeleteDialog({
|
|
|
506
517
|
onCancel: () => void;
|
|
507
518
|
onConfirm: () => void;
|
|
508
519
|
}) {
|
|
520
|
+
const t = useLocale();
|
|
521
|
+
const [descPrefix, descSuffix] = t.asset.deleteAssetDescription.split('{name}');
|
|
509
522
|
return (
|
|
510
523
|
<Dialog open onOpenChange={(open) => !open && onCancel()}>
|
|
511
524
|
<DialogContent>
|
|
512
525
|
<DialogHeader>
|
|
513
|
-
<DialogTitle>
|
|
526
|
+
<DialogTitle>{t.asset.deleteAssetTitle}</DialogTitle>
|
|
514
527
|
<DialogDescription>
|
|
515
|
-
|
|
516
|
-
|
|
528
|
+
{descPrefix}
|
|
529
|
+
<span className="font-mono">{asset.name}</span>
|
|
530
|
+
{descSuffix}
|
|
517
531
|
</DialogDescription>
|
|
518
532
|
</DialogHeader>
|
|
519
533
|
<DialogFooter>
|
|
520
534
|
<Button variant="outline" onClick={onCancel}>
|
|
521
|
-
|
|
535
|
+
{t.common.cancel}
|
|
522
536
|
</Button>
|
|
523
537
|
<Button variant="destructive" onClick={onConfirm}>
|
|
524
|
-
|
|
538
|
+
{t.common.delete}
|
|
525
539
|
</Button>
|
|
526
540
|
</DialogFooter>
|
|
527
541
|
</DialogContent>
|
|
@@ -529,9 +543,21 @@ function DeleteDialog({
|
|
|
529
543
|
);
|
|
530
544
|
}
|
|
531
545
|
|
|
546
|
+
function NoResultsMessage({ query, t }: { query: string; t: ReturnType<typeof useLocale> }) {
|
|
547
|
+
const [prefix, suffix] = t.asset.logoSearchNoResults.split('{query}');
|
|
548
|
+
return (
|
|
549
|
+
<>
|
|
550
|
+
{prefix}
|
|
551
|
+
<span className="font-mono text-foreground">{query}</span>
|
|
552
|
+
{suffix}
|
|
553
|
+
</>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
532
557
|
function PreviewDialog({ asset, onClose }: { asset: AssetEntry; onClose: () => void }) {
|
|
533
558
|
const isImage = asset.mime.startsWith('image/');
|
|
534
559
|
const importPath = `./assets/${asset.name}`;
|
|
560
|
+
const t = useLocale();
|
|
535
561
|
return (
|
|
536
562
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
537
563
|
<DialogContent className="sm:max-w-2xl">
|
|
@@ -552,13 +578,13 @@ function PreviewDialog({ asset, onClose }: { asset: AssetEntry; onClose: () => v
|
|
|
552
578
|
) : (
|
|
553
579
|
<div className="flex items-center justify-center rounded-md border bg-muted/40 py-12 text-muted-foreground">
|
|
554
580
|
<FileImage className="mr-2 size-5" />
|
|
555
|
-
<span className="text-sm">
|
|
581
|
+
<span className="text-sm">{t.asset.noPreview}</span>
|
|
556
582
|
</div>
|
|
557
583
|
)}
|
|
558
584
|
<div className="rounded-[5px] border border-hairline bg-muted/50 px-3 py-2 font-mono text-[11.5px] leading-relaxed">
|
|
559
|
-
<span className="text-muted-foreground">
|
|
585
|
+
<span className="text-muted-foreground">{t.asset.importHintComment}</span>
|
|
560
586
|
<span className="text-brand">'{importPath}'</span>
|
|
561
|
-
<span className="text-muted-foreground"
|
|
587
|
+
<span className="text-muted-foreground">{t.asset.importHintSemi}</span>
|
|
562
588
|
</div>
|
|
563
589
|
</DialogContent>
|
|
564
590
|
</Dialog>
|
|
@@ -581,6 +607,7 @@ function LogoSearchDialog({
|
|
|
581
607
|
const [pending, setPending] = useState<Set<number>>(() => new Set());
|
|
582
608
|
const [retryToken, setRetryToken] = useState(0);
|
|
583
609
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
610
|
+
const t = useLocale();
|
|
584
611
|
|
|
585
612
|
useEffect(() => {
|
|
586
613
|
queueMicrotask(() => inputRef.current?.focus());
|
|
@@ -599,7 +626,7 @@ function LogoSearchDialog({
|
|
|
599
626
|
})
|
|
600
627
|
.catch((err: unknown) => {
|
|
601
628
|
if (ctrl.signal.aborted) return;
|
|
602
|
-
setError(err instanceof Error ? err.message :
|
|
629
|
+
setError(err instanceof Error ? err.message : t.asset.toastSearchFailed);
|
|
603
630
|
setLoading(false);
|
|
604
631
|
});
|
|
605
632
|
}, 200);
|
|
@@ -613,9 +640,9 @@ function LogoSearchDialog({
|
|
|
613
640
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
614
641
|
<DialogContent className="sm:max-w-2xl">
|
|
615
642
|
<DialogHeader>
|
|
616
|
-
<DialogTitle>
|
|
643
|
+
<DialogTitle>{t.asset.logoSearchTitle}</DialogTitle>
|
|
617
644
|
<DialogDescription>
|
|
618
|
-
|
|
645
|
+
{t.asset.logoSearchPoweredByPrefix}
|
|
619
646
|
<a
|
|
620
647
|
href="https://svgl.app"
|
|
621
648
|
target="_blank"
|
|
@@ -634,7 +661,7 @@ function LogoSearchDialog({
|
|
|
634
661
|
ref={inputRef}
|
|
635
662
|
value={query}
|
|
636
663
|
onChange={(e) => setQuery(e.target.value)}
|
|
637
|
-
placeholder=
|
|
664
|
+
placeholder={t.asset.logoSearchPlaceholder}
|
|
638
665
|
className="h-9 w-full rounded-[6px] border border-border bg-background py-2 pl-8 pr-3 text-[13px] outline-none focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
639
666
|
/>
|
|
640
667
|
</div>
|
|
@@ -646,10 +673,8 @@ function LogoSearchDialog({
|
|
|
646
673
|
<CloudOff className="size-5 text-muted-foreground" />
|
|
647
674
|
</div>
|
|
648
675
|
<div>
|
|
649
|
-
<p className="text-sm font-medium">
|
|
650
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
651
|
-
Check your connection and try again.
|
|
652
|
-
</p>
|
|
676
|
+
<p className="text-sm font-medium">{t.asset.logoSearchErrorTitle}</p>
|
|
677
|
+
<p className="mt-1 text-xs text-muted-foreground">{t.asset.logoSearchErrorBody}</p>
|
|
653
678
|
</div>
|
|
654
679
|
<Button
|
|
655
680
|
variant="outline"
|
|
@@ -658,7 +683,7 @@ function LogoSearchDialog({
|
|
|
658
683
|
className="gap-1.5"
|
|
659
684
|
>
|
|
660
685
|
<RotateCw className="size-3.5" />
|
|
661
|
-
|
|
686
|
+
{t.common.tryAgain}
|
|
662
687
|
</Button>
|
|
663
688
|
</div>
|
|
664
689
|
) : loading && !results ? (
|
|
@@ -678,16 +703,13 @@ function LogoSearchDialog({
|
|
|
678
703
|
<div>
|
|
679
704
|
<p className="text-sm font-medium">
|
|
680
705
|
{query.trim() ? (
|
|
681
|
-
|
|
682
|
-
No logos for{' '}
|
|
683
|
-
<span className="font-mono text-foreground">"{query.trim()}"</span>
|
|
684
|
-
</>
|
|
706
|
+
<NoResultsMessage query={query.trim()} t={t} />
|
|
685
707
|
) : (
|
|
686
|
-
|
|
708
|
+
t.asset.logoSearchEmpty
|
|
687
709
|
)}
|
|
688
710
|
</p>
|
|
689
711
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
690
|
-
|
|
712
|
+
{t.asset.logoSearchEmptyHintPrefix}
|
|
691
713
|
<a
|
|
692
714
|
href="https://svgl.app"
|
|
693
715
|
target="_blank"
|
|
@@ -696,7 +718,7 @@ function LogoSearchDialog({
|
|
|
696
718
|
>
|
|
697
719
|
svgl.app
|
|
698
720
|
</a>
|
|
699
|
-
.
|
|
721
|
+
{t.asset.logoSearchEmptyHintSuffix}
|
|
700
722
|
</p>
|
|
701
723
|
</div>
|
|
702
724
|
</div>
|
|
@@ -716,7 +738,7 @@ function LogoSearchDialog({
|
|
|
716
738
|
try {
|
|
717
739
|
await onPick(file);
|
|
718
740
|
} catch (err) {
|
|
719
|
-
toast.error(err instanceof Error ? err.message :
|
|
741
|
+
toast.error(err instanceof Error ? err.message : t.asset.toastDownloadFailed);
|
|
720
742
|
} finally {
|
|
721
743
|
setPending((prev) => {
|
|
722
744
|
const next = new Set(prev);
|
|
@@ -733,7 +755,7 @@ function LogoSearchDialog({
|
|
|
733
755
|
|
|
734
756
|
<DialogFooter>
|
|
735
757
|
<Button variant="outline" onClick={onClose}>
|
|
736
|
-
|
|
758
|
+
{t.common.done}
|
|
737
759
|
</Button>
|
|
738
760
|
</DialogFooter>
|
|
739
761
|
</DialogContent>
|
|
@@ -752,6 +774,7 @@ function LogoResultCard({
|
|
|
752
774
|
}) {
|
|
753
775
|
const hasVariants = typeof item.route === 'object' && item.route !== null;
|
|
754
776
|
const [variant, setVariant] = useState<'light' | 'dark'>('light');
|
|
777
|
+
const t = useLocale();
|
|
755
778
|
|
|
756
779
|
const previewUrl = useMemo(() => {
|
|
757
780
|
if (typeof item.route === 'string') return item.route;
|
|
@@ -796,7 +819,7 @@ function LogoResultCard({
|
|
|
796
819
|
variant === 'light' ? 'bg-foreground text-background' : 'hover:bg-muted',
|
|
797
820
|
)}
|
|
798
821
|
>
|
|
799
|
-
|
|
822
|
+
{t.asset.logoVariantLight}
|
|
800
823
|
</button>
|
|
801
824
|
<button
|
|
802
825
|
type="button"
|
|
@@ -806,7 +829,7 @@ function LogoResultCard({
|
|
|
806
829
|
variant === 'dark' ? 'bg-foreground text-background' : 'hover:bg-muted',
|
|
807
830
|
)}
|
|
808
831
|
>
|
|
809
|
-
|
|
832
|
+
{t.asset.logoVariantDark}
|
|
810
833
|
</button>
|
|
811
834
|
</div>
|
|
812
835
|
) : null}
|
|
@@ -819,12 +842,12 @@ function LogoResultCard({
|
|
|
819
842
|
const file = await fetchSvgAsFile(previewUrl, filename);
|
|
820
843
|
await onAdd(file);
|
|
821
844
|
} catch (err) {
|
|
822
|
-
toast.error(err instanceof Error ? err.message :
|
|
845
|
+
toast.error(err instanceof Error ? err.message : t.asset.toastDownloadFailed);
|
|
823
846
|
}
|
|
824
847
|
}}
|
|
825
848
|
className="ml-auto h-6 px-2 text-[11px]"
|
|
826
849
|
>
|
|
827
|
-
{pending ? <Loader2 className="size-3 animate-spin" /> :
|
|
850
|
+
{pending ? <Loader2 className="size-3 animate-spin" /> : t.common.add}
|
|
828
851
|
</Button>
|
|
829
852
|
</div>
|
|
830
853
|
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLocale } from '@/lib/use-locale';
|
|
1
2
|
import { useInspector } from './inspector/inspector-provider';
|
|
2
3
|
|
|
3
4
|
type Props = {
|
|
@@ -9,13 +10,14 @@ type Props = {
|
|
|
9
10
|
|
|
10
11
|
export function ClickNavZones({ onPrev, onNext, canPrev, canNext }: Props) {
|
|
11
12
|
const { active } = useInspector();
|
|
13
|
+
const t = useLocale();
|
|
12
14
|
if (active) return null;
|
|
13
15
|
|
|
14
16
|
return (
|
|
15
17
|
<>
|
|
16
18
|
<button
|
|
17
19
|
type="button"
|
|
18
|
-
aria-label=
|
|
20
|
+
aria-label={t.clickNav.prevAria}
|
|
19
21
|
onClick={onPrev}
|
|
20
22
|
disabled={!canPrev}
|
|
21
23
|
data-inspector-ui
|
|
@@ -23,7 +25,7 @@ export function ClickNavZones({ onPrev, onNext, canPrev, canNext }: Props) {
|
|
|
23
25
|
/>
|
|
24
26
|
<button
|
|
25
27
|
type="button"
|
|
26
|
-
aria-label=
|
|
28
|
+
aria-label={t.clickNav.nextAria}
|
|
27
29
|
onClick={onNext}
|
|
28
30
|
disabled={!canNext}
|
|
29
31
|
data-inspector-ui
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { MessageSquare, Trash2, X } from 'lucide-react';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
+
import { format, plural, useLocale } from '@/lib/use-locale';
|
|
3
4
|
import { useInspector } from './inspector-provider';
|
|
4
5
|
|
|
5
6
|
export function CommentWidget() {
|
|
7
|
+
const t = useLocale();
|
|
6
8
|
const { comments, remove, error } = useInspector();
|
|
7
9
|
const [open, setOpen] = useState(false);
|
|
8
10
|
const count = comments.length;
|
|
@@ -13,7 +15,7 @@ export function CommentWidget() {
|
|
|
13
15
|
<div className="w-80 rounded-md border bg-card shadow-xl animate-in fade-in-0 slide-in-from-bottom-2 duration-200">
|
|
14
16
|
<div className="flex items-center justify-between border-b px-3 py-2">
|
|
15
17
|
<span className="text-xs font-semibold">
|
|
16
|
-
{count
|
|
18
|
+
{format(plural(count, t.inspector.commentsCount), { count })}
|
|
17
19
|
</span>
|
|
18
20
|
<button
|
|
19
21
|
type="button"
|
|
@@ -26,7 +28,7 @@ export function CommentWidget() {
|
|
|
26
28
|
{error && <p className="px-3 py-2 text-xs text-red-600">{error}</p>}
|
|
27
29
|
{count === 0 ? (
|
|
28
30
|
<p className="px-3 py-6 text-center text-xs text-muted-foreground">
|
|
29
|
-
|
|
31
|
+
{t.inspector.commentsEmpty}
|
|
30
32
|
</p>
|
|
31
33
|
) : (
|
|
32
34
|
<>
|
|
@@ -38,7 +40,7 @@ export function CommentWidget() {
|
|
|
38
40
|
>
|
|
39
41
|
<div className="min-w-0 flex-1">
|
|
40
42
|
<div className="text-[10px] font-mono text-muted-foreground">
|
|
41
|
-
|
|
43
|
+
{format(t.inspector.commentLineLabel, { n: c.line })}
|
|
42
44
|
</div>
|
|
43
45
|
<div className="mt-0.5 text-xs break-words">{c.note}</div>
|
|
44
46
|
</div>
|
|
@@ -46,7 +48,7 @@ export function CommentWidget() {
|
|
|
46
48
|
type="button"
|
|
47
49
|
onClick={() => remove(c.id)}
|
|
48
50
|
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-muted hover:text-red-600"
|
|
49
|
-
title=
|
|
51
|
+
title={t.inspector.commentDeleteAria}
|
|
50
52
|
>
|
|
51
53
|
<Trash2 className="size-3.5" />
|
|
52
54
|
</button>
|
|
@@ -54,11 +56,11 @@ export function CommentWidget() {
|
|
|
54
56
|
))}
|
|
55
57
|
</ul>
|
|
56
58
|
<div className="border-t px-3 py-2 text-[11px] text-muted-foreground">
|
|
57
|
-
|
|
59
|
+
{t.inspector.commentsApplyHintPrefix}
|
|
58
60
|
<code className="rounded bg-muted px-1 py-0.5 font-mono text-foreground">
|
|
59
61
|
/apply-comments
|
|
60
|
-
</code>
|
|
61
|
-
|
|
62
|
+
</code>
|
|
63
|
+
{t.inspector.commentsApplyHintSuffix}
|
|
62
64
|
</div>
|
|
63
65
|
</>
|
|
64
66
|
)}
|