@elytracms/studio 0.0.102 → 0.0.104
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/convex/ingest.test.ts +251 -23
- package/convex/ingest.ts +77 -19
- package/dist/components/ai/translate-document-dialog.d.ts +43 -0
- package/dist/components/ai/translate-document-dialog.js +104 -36
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -3
- package/dist/components/ai/translate-skipped.js +33 -7
- package/dist/components/ai/translate-skipped.js.map +1 -1
- package/dist/components/builder/canvas-theme-notice.js +4 -1
- package/dist/components/builder/canvas-theme-notice.js.map +1 -1
- package/dist/components/components/components-index.d.ts +0 -7
- package/dist/components/components/components-index.js +9 -2
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/lib/ai/translate-apply.d.ts +69 -0
- package/dist/lib/ai/translate-apply.js +36 -0
- package/dist/lib/ai/translate-apply.js.map +1 -0
- package/dist/lib/ai/translate-document.d.ts +95 -7
- package/dist/lib/ai/translate-document.js +195 -57
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +33 -2
- package/dist/lib/ai/writer-form.js +188 -27
- package/dist/lib/ai/writer-form.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +56 -10
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +41 -1
- package/dist/lib/i18n/messages.en.js +61 -10
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/package.json +3 -3
|
@@ -2,17 +2,19 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { createCollectionRegistry } from '@elytracms/core/cms-core';
|
|
4
4
|
import { Check, Languages, Loader2, X } from 'lucide-react';
|
|
5
|
-
import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Row, Stack, Text, } from '@elytracms/studio/ui';
|
|
5
|
+
import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Row, SegmentedControl, Stack, Text, } from '@elytracms/studio/ui';
|
|
6
6
|
import { translate } from '../../lib/i18n/index.js';
|
|
7
7
|
import { localeChipLabel } from '../../lib/locale/index.js';
|
|
8
8
|
import { getStudioOperations } from '../../lib/operations/index.js';
|
|
9
9
|
import { resolveDeploymentAi } from '../../lib/workspace/index.js';
|
|
10
|
-
import { resolveAiConfig, resolveKnowledge, translateDocument } from '../../lib/ai/index.js';
|
|
10
|
+
import { planTranslation, resolveAiConfig, resolveKnowledge, translateDocument } from '../../lib/ai/index.js';
|
|
11
|
+
import { applyTranslation } from '../../lib/ai/translate-apply.js';
|
|
11
12
|
import { cmsValidationIssueMessage } from '../../lib/persona/labels.js';
|
|
12
13
|
import { useCms } from '../cms/cms-store.js';
|
|
13
|
-
import { TranslateSkipped } from './translate-skipped.js';
|
|
14
|
+
import { TranslateBucketList, TranslateSkipped } from './translate-skipped.js';
|
|
14
15
|
export function TranslateDocumentDialog({ projectId, collection, docRef, document, revision, localeConfig, flushDraft, open, onOpenChange, }) {
|
|
15
16
|
const [phase, setPhase] = useState('confirm');
|
|
17
|
+
const [mode, setMode] = useState('fill-empty');
|
|
16
18
|
const [progress, setProgress] = useState(null);
|
|
17
19
|
const [summary, setSummary] = useState(null);
|
|
18
20
|
/**
|
|
@@ -20,7 +22,7 @@ export function TranslateDocumentDialog({ projectId, collection, docRef, documen
|
|
|
20
22
|
* EC-473 `ai.error.applyInvalid` copy ("the proposal no longer fits the current
|
|
21
23
|
* document: …"), which is exactly this situation: no new catalog string, and the
|
|
22
24
|
* editor is told WHICH rules the translation would have broken rather than a bare
|
|
23
|
-
* "failed".
|
|
25
|
+
* "failed". EC-752: also why a re-translation wrote nothing (no restore point).
|
|
24
26
|
*/
|
|
25
27
|
const [refusal, setRefusal] = useState(null);
|
|
26
28
|
const abortRef = useRef(null);
|
|
@@ -32,6 +34,10 @@ export function TranslateDocumentDialog({ projectId, collection, docRef, documen
|
|
|
32
34
|
useEffect(() => () => abortRef.current?.abort(), []);
|
|
33
35
|
const base = localeConfig.default;
|
|
34
36
|
const targets = localeConfig.locales.filter((l) => l !== base);
|
|
37
|
+
// EC-752 rule 4: the confirm lists the run's OWN plan — the same judge, on the same input.
|
|
38
|
+
const plan = useMemo(() => mode === 'retranslate-all'
|
|
39
|
+
? planTranslation({ collection, document, localeConfig, mode, validateOptions: cms.validateOptions })
|
|
40
|
+
: null, [mode, collection, document, localeConfig, cms.validateOptions]);
|
|
35
41
|
const run = useCallback(async () => {
|
|
36
42
|
if (projectId === undefined) {
|
|
37
43
|
setPhase('error');
|
|
@@ -56,6 +62,7 @@ export function TranslateDocumentDialog({ projectId, collection, docRef, documen
|
|
|
56
62
|
collection,
|
|
57
63
|
document,
|
|
58
64
|
localeConfig,
|
|
65
|
+
mode,
|
|
59
66
|
knowledge,
|
|
60
67
|
signal: controller.signal,
|
|
61
68
|
onProgress: setProgress,
|
|
@@ -78,36 +85,44 @@ export function TranslateDocumentDialog({ projectId, collection, docRef, documen
|
|
|
78
85
|
setPhase('error');
|
|
79
86
|
return;
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const { operations } = await getStudioOperations();
|
|
92
|
-
// Provenance: an AI-tagged version snapshot (best-effort — the draft already landed).
|
|
93
|
-
try {
|
|
94
|
-
await operations.documents.snapshot({
|
|
88
|
+
const { operations } = await getStudioOperations();
|
|
89
|
+
// EC-752 rule 3 (in `applyTranslation`): a re-translation records a labelled restore
|
|
90
|
+
// point of the stored draft BEFORE it writes; then the patches go through the session's
|
|
91
|
+
// queue at the revision the draft was authored on (EC-595 A2 / EC-647 — a bucket a
|
|
92
|
+
// colleague changed meanwhile is refused on that field and shown there); then the
|
|
93
|
+
// AI-provenance snapshot.
|
|
94
|
+
const applied = await applyTranslation(result, {
|
|
95
|
+
commit: (patches) => cms.commitPatches(docRef, patches, patches.map(() => revision)),
|
|
96
|
+
snapshot: async ({ label, origin }) => {
|
|
97
|
+
const recorded = await operations.documents.snapshot({
|
|
95
98
|
projectId,
|
|
96
99
|
ref: docRef,
|
|
97
|
-
label
|
|
98
|
-
origin:
|
|
100
|
+
label,
|
|
101
|
+
...(origin ? { origin } : {}),
|
|
99
102
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
103
|
+
if (!recorded.ok)
|
|
104
|
+
throw new Error(recorded.error.message);
|
|
105
|
+
return recorded.value;
|
|
106
|
+
},
|
|
107
|
+
}, { restorePoint: translate('ai.translate.snapshotLabel'), provenance: translate('cms.version.aiAuthoredTitle') });
|
|
108
|
+
if (applied.status === 'snapshot-failed') {
|
|
109
|
+
setRefusal(translate('ai.translate.snapshotFailed'));
|
|
110
|
+
setPhase('error');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (applied.status === 'failed') {
|
|
114
|
+
setPhase('error');
|
|
115
|
+
return;
|
|
104
116
|
}
|
|
105
117
|
setSummary({
|
|
118
|
+
mode,
|
|
119
|
+
plan: result.plan,
|
|
106
120
|
translated: result.translated,
|
|
107
|
-
slugs: result.slugsDerived,
|
|
108
121
|
locales: Object.keys(result.perLocale).length,
|
|
109
122
|
cancelled: result.cancelled,
|
|
110
|
-
|
|
123
|
+
writes: applied.writes,
|
|
124
|
+
refused: applied.refused,
|
|
125
|
+
...(applied.restorePoint !== undefined ? { restorePoint: applied.restorePoint } : {}),
|
|
111
126
|
skipped: result.skipped,
|
|
112
127
|
});
|
|
113
128
|
setPhase('done');
|
|
@@ -118,21 +133,74 @@ export function TranslateDocumentDialog({ projectId, collection, docRef, documen
|
|
|
118
133
|
finally {
|
|
119
134
|
abortRef.current = null;
|
|
120
135
|
}
|
|
121
|
-
}, [projectId, collection, document, revision, localeConfig, docRef, flushDraft, registry, cms]);
|
|
136
|
+
}, [projectId, collection, document, revision, localeConfig, mode, docRef, flushDraft, registry, cms]);
|
|
122
137
|
const close = () => {
|
|
123
138
|
if (phase === 'running')
|
|
124
139
|
return; // never close mid-run — Cancel aborts instead
|
|
125
140
|
onOpenChange(false);
|
|
126
141
|
};
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
return translate('ai.translate.nothing');
|
|
130
|
-
if (s.cancelled)
|
|
131
|
-
return translate('ai.translate.cancelled');
|
|
132
|
-
return translate('ai.translate.applied', { count: s.translated, locales: s.locales });
|
|
133
|
-
};
|
|
134
|
-
return (_jsx(Dialog, { open: open, onOpenChange: (next) => (!next ? close() : undefined), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Languages, { className: "size-4" }), " ", translate('ai.translate.dialogTitle')] }) }), _jsx(DialogDescription, { children: translate('ai.translate.dialogDescription', { base: localeChipLabel(base) }) })] }), phase === 'confirm' ? (_jsxs(Stack, { gap: "4", className: "py-2", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "medium", tone: "muted", children: translate('ai.translate.targetsLabel') }), _jsx(Row, { gap: "1", wrap: true, children: targets.map((l) => (_jsx(Badge, { variant: "outline", children: localeChipLabel(l) }, l))) })] }), _jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "medium", tone: "muted", children: translate('ai.translate.scopeLabel') }), _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Badge, { variant: "default", children: translate('ai.translate.fillMissing') }), _jsx(Button, { size: "sm", variant: "ghost", disabled: true, title: translate('ai.translate.retranslateSoon'), children: translate('ai.translate.retranslateAll') }), _jsx(Text, { size: "xs", tone: "muted", children: translate('ai.translate.retranslateSoon') })] })] })] })) : null, phase === 'running' ? (_jsxs(Stack, { gap: "3", align: "center", className: "py-6", children: [_jsx(Loader2, { className: "size-6 animate-spin text-primary" }), _jsx(Text, { size: "sm", tone: "muted", "data-testid": "translate-progress", children: progress && progress.total > 0
|
|
142
|
+
const replaces = (plan?.buckets ?? []).some((bucket) => bucket.kind === 'replace');
|
|
143
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (next) => (!next ? close() : undefined), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Languages, { className: "size-4" }), " ", translate('ai.translate.dialogTitle')] }) }), _jsx(DialogDescription, { children: translate('ai.translate.dialogDescription', { base: localeChipLabel(base) }) })] }), phase === 'confirm' ? (_jsx(TranslateConfirm, { mode: mode, onModeChange: setMode, base: base, targets: targets, plan: plan, collection: collection })) : null, phase === 'running' ? (_jsxs(Stack, { gap: "3", align: "center", className: "py-6", children: [_jsx(Loader2, { className: "size-6 animate-spin text-primary" }), _jsx(Text, { size: "sm", tone: "muted", "data-testid": "translate-progress", children: progress && progress.total > 0
|
|
135
144
|
? translate('ai.translate.progress', { completed: progress.completed, total: progress.total })
|
|
136
|
-
: translate('ai.translate.preparing') }), progress && progress.total > 0 ? (_jsx("div", { className: "h-1.5 w-56 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-primary transition-all", style: { width: `${Math.round((progress.completed / progress.total) * 100)}%` } }) })) : null] })) : null, phase === 'done' && summary ? (_jsxs(Stack, { gap: "2", align: "center", className: "py-6", children: [_jsx(Check, { className: "size-6 text-success" }), _jsx(
|
|
145
|
+
: translate('ai.translate.preparing') }), progress && progress.total > 0 ? (_jsx("div", { className: "h-1.5 w-56 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-primary transition-all", style: { width: `${Math.round((progress.completed / progress.total) * 100)}%` } }) })) : null] })) : null, phase === 'done' && summary ? (_jsxs(Stack, { gap: "2", align: "center", className: "py-6", children: [_jsx(Check, { className: "size-6 text-success" }), _jsx(TranslateDoneSummary, { summary: summary, base: base, collection: collection })] })) : null, phase === 'error' ? (_jsxs(Stack, { gap: "2", align: "center", className: "py-6", children: [_jsx(X, { className: "size-6 text-destructive" }), _jsx(Text, { size: "sm", tone: "muted", className: "text-center", "data-testid": "translate-error", children: refusal ?? translate('ai.translate.failed') })] })) : null, _jsx(DialogFooter, { className: "mt-2", children: phase === 'confirm' ? (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: close, children: translate('common.cancel') }), mode === 'retranslate-all' && replaces ? (_jsxs(Button, { size: "sm", variant: "destructive", onClick: () => void run(), "data-testid": "translate-confirm", children: [_jsx(Languages, {}), " ", translate('ai.translate.confirmReplace')] })) : (_jsxs(Button, { size: "sm", onClick: () => void run(), "data-testid": "translate-confirm", children: [_jsx(Languages, {}), " ", translate('ai.translate.confirm')] }))] })) : phase === 'running' ? (_jsx(Button, { variant: "outline", size: "sm", onClick: () => abortRef.current?.abort(), "data-testid": "translate-cancel", children: translate('common.cancel') })) : (_jsx(Button, { size: "sm", onClick: close, "data-testid": "translate-close", children: translate('ai.translate.close') })) })] }) }));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* EC-752 — the confirm phase: the target languages, the scope choice and, for a
|
|
149
|
+
* re-translation, its plan (what it replaces, fills and leaves as it is — "up to").
|
|
150
|
+
*/
|
|
151
|
+
export function TranslateConfirm({ mode, onModeChange, base, targets, plan, collection, }) {
|
|
152
|
+
const baseLabel = localeChipLabel(base);
|
|
153
|
+
return (_jsxs(Stack, { gap: "4", className: "py-2", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "medium", tone: "muted", children: translate('ai.translate.targetsLabel') }), _jsx(Row, { gap: "1", wrap: true, children: targets.map((l) => (_jsx(Badge, { variant: "outline", children: localeChipLabel(l) }, l))) })] }), _jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "medium", tone: "muted", children: translate('ai.translate.scopeLabel') }), _jsx(SegmentedControl, { ariaLabel: translate('ai.translate.scopeLabel'), testId: "translate-scope", items: [
|
|
154
|
+
{
|
|
155
|
+
key: 'fill-empty',
|
|
156
|
+
label: translate('ai.translate.fillMissing'),
|
|
157
|
+
active: mode === 'fill-empty',
|
|
158
|
+
onClick: () => onModeChange('fill-empty'),
|
|
159
|
+
testId: 'translate-scope-fill',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
key: 'retranslate-all',
|
|
163
|
+
label: translate('ai.translate.retranslateAll'),
|
|
164
|
+
active: mode === 'retranslate-all',
|
|
165
|
+
onClick: () => onModeChange('retranslate-all'),
|
|
166
|
+
testId: 'translate-scope-retranslate',
|
|
167
|
+
},
|
|
168
|
+
] }), _jsx(Text, { size: "xs", tone: "muted", children: translate(mode === 'retranslate-all' ? 'ai.translate.retranslateHint' : 'ai.translate.fillMissingHint', {
|
|
169
|
+
base: baseLabel,
|
|
170
|
+
}) })] }), mode === 'retranslate-all' && plan ? _jsx(RetranslatePlan, { plan: plan, base: base, collection: collection }) : null] }));
|
|
171
|
+
}
|
|
172
|
+
/** EC-752 rule 4 — what a re-translation will do, from its own pre-provider plan. */
|
|
173
|
+
function RetranslatePlan({ plan, base, collection }) {
|
|
174
|
+
const baseLabel = localeChipLabel(base);
|
|
175
|
+
const replace = plan.buckets.filter((bucket) => bucket.kind === 'replace');
|
|
176
|
+
const fill = plan.buckets.filter((bucket) => bucket.kind === 'fill');
|
|
177
|
+
return (_jsxs(Stack, { gap: "2", "data-testid": "translate-plan", children: [replace.length > 0 ? (_jsx(TranslateBucketList, { title: translate('ai.translate.planReplaceTitle', { base: baseLabel, count: replace.length }), buckets: replace, collection: collection, testId: "translate-plan-replace" })) : (_jsx(Text, { size: "xs", tone: "muted", children: translate('ai.translate.planReplaceNone') })), _jsx(TranslateBucketList, { title: translate('ai.translate.planFillTitle', { base: baseLabel, count: fill.length }), buckets: fill, collection: collection, testId: "translate-plan-fill" }), _jsx(TranslateSkipped, { skipped: plan.skipped, collection: collection, mode: plan.mode, title: translate('ai.translate.planKeptTitle', { count: plan.skipped.length }) }), _jsxs(Stack, { gap: "px", children: [_jsx(Text, { size: "xs", tone: "muted", children: translate('ai.translate.planWhole') }), _jsx(Text, { size: "xs", tone: "muted", children: translate('ai.translate.planSlugsKept') }), replace.length > 0 ? (_jsx(Text, { size: "xs", tone: "muted", children: translate('ai.translate.planRestorePoint') })) : null] })] }));
|
|
178
|
+
}
|
|
179
|
+
/** The headline of the done phase (EC-752: mode-aware; slugs and replacements are listed below it). */
|
|
180
|
+
export function translateDoneMessage(summary, base) {
|
|
181
|
+
const retranslate = summary.mode === 'retranslate-all';
|
|
182
|
+
const nothing = summary.plan.total === 0 && summary.skipped.length === 0 && summary.writes.length === 0 && summary.refused.length === 0;
|
|
183
|
+
if (nothing) {
|
|
184
|
+
return retranslate
|
|
185
|
+
? translate('ai.translate.retranslateNothing', { base: localeChipLabel(base) })
|
|
186
|
+
: translate('ai.translate.nothing');
|
|
187
|
+
}
|
|
188
|
+
if (summary.cancelled)
|
|
189
|
+
return translate(retranslate ? 'ai.translate.retranslateCancelled' : 'ai.translate.cancelled');
|
|
190
|
+
if (retranslate)
|
|
191
|
+
return translate('ai.translate.retranslateDone');
|
|
192
|
+
return translate('ai.translate.applied', { count: summary.translated, locales: summary.locales });
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* EC-752 — the done phase: what was actually written (a re-translation lists what it
|
|
196
|
+
* replaced and filled), the slugs it set, what was refused at its revision, what a
|
|
197
|
+
* re-translation planned but left as it was, its restore point, and what was not translated.
|
|
198
|
+
*/
|
|
199
|
+
export function TranslateDoneSummary({ summary, base, collection, }) {
|
|
200
|
+
const retranslate = summary.mode === 'retranslate-all';
|
|
201
|
+
const of = (kind) => summary.writes.filter((write) => write.kind === kind);
|
|
202
|
+
const done = (bucket) => [...summary.writes, ...summary.refused].some((write) => write.field === bucket.field && write.locale === bucket.locale);
|
|
203
|
+
const kept = retranslate ? summary.plan.buckets.filter((bucket) => !done(bucket)) : [];
|
|
204
|
+
return (_jsxs(Stack, { gap: "2", className: "w-full", children: [_jsx(Text, { size: "sm", tone: "muted", className: "text-center", "data-testid": "translate-summary", children: translateDoneMessage(summary, base) }), retranslate ? (_jsxs(_Fragment, { children: [_jsx(TranslateBucketList, { title: translate('ai.translate.replacedTitle', { count: of('replace').length }), buckets: of('replace'), collection: collection, testId: "translate-replaced" }), _jsx(TranslateBucketList, { title: translate('ai.translate.filledTitle', { count: of('fill').length }), buckets: of('fill'), collection: collection, testId: "translate-filled" })] })) : null, _jsx(TranslateBucketList, { title: translate('ai.translate.slugsTitle', { count: of('slug').length }), buckets: of('slug'), collection: collection, testId: "translate-slugs" }), _jsx(TranslateBucketList, { title: translate('ai.translate.refusedTitle', { count: summary.refused.length }), buckets: summary.refused, collection: collection, testId: "translate-refused" }), _jsx(TranslateBucketList, { title: translate('ai.translate.keptTitle', { count: kept.length }), buckets: kept, collection: collection, testId: "translate-kept" }), summary.restorePoint !== undefined ? (_jsx(Text, { size: "xs", tone: "muted", "data-testid": "translate-restore-point", children: translate('ai.translate.restorePoint', { version: summary.restorePoint }) })) : null, _jsx(TranslateSkipped, { skipped: summary.skipped, collection: collection, mode: summary.mode })] }));
|
|
137
205
|
}
|
|
138
206
|
//# sourceMappingURL=translate-document-dialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate-document-dialog.js","sourceRoot":"","sources":["../../../src/components/ai/translate-document-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { createCollectionRegistry } from '@elytracms/core/cms-core'\nimport { Check, Languages, Loader2, X } from 'lucide-react'\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { localeChipLabel } from '../../lib/locale'\nimport { getStudioOperations } from '../../lib/operations'\nimport { resolveDeploymentAi } from '../../lib/workspace'\nimport { resolveAiConfig, resolveKnowledge, translateDocument } from '../../lib/ai'\nimport type { TranslateProgress, TranslateSkip } from '../../lib/ai'\nimport { cmsValidationIssueMessage } from '../../lib/persona/labels'\nimport { useCms } from '../cms/cms-store'\nimport type { TranslateDocumentActionProps } from './translate-document-action'\nimport { TranslateSkipped } from './translate-skipped'\n\n/**\n * The \"Translate document\" confirm + progress dialog (EC-332), heavy side — loaded lazily\n * by the gate so the AI engine stays off the critical path (EC-331 bundle discipline). A\n * modal is allowed here (a picker/confirmation, per the house rule — property forms are\n * not). It resolves the EC-331 provider + knowledge, runs the ONE `translateDocument`\n * engine over the live draft (fill-empty-only on EC-318 connection state), streams\n * progress, then writes the result to the DRAFT via the operations layer with a\n * provenance-tagged snapshot (mirroring the EC-199 apply) and reflects it into the open\n * editor. Cancel aborts cleanly — any already-translated units stay on the draft, visible\n * in Compare (EC-317) and rejectable via Discard (EC-316).\n */\ntype Phase = 'confirm' | 'running' | 'done' | 'error'\n\ninterface RunSummary {\n translated: number\n slugs: number\n locales: number\n cancelled: boolean\n nothing: boolean\n /** EC-593: every text position the run could not translate — listed, never silent. */\n skipped: readonly TranslateSkip[]\n}\n\nexport function TranslateDocumentDialog({\n projectId,\n collection,\n docRef,\n document,\n revision,\n localeConfig,\n flushDraft,\n open,\n onOpenChange,\n}: TranslateDocumentActionProps & { open: boolean; onOpenChange: (open: boolean) => void }) {\n const [phase, setPhase] = useState<Phase>('confirm')\n const [progress, setProgress] = useState<TranslateProgress | null>(null)\n const [summary, setSummary] = useState<RunSummary | null>(null)\n /**\n * EC-497 — why the refusal was blocked, when the gate refused. Reuses the\n * EC-473 `ai.error.applyInvalid` copy (\"the proposal no longer fits the current\n * document: …\"), which is exactly this situation: no new catalog string, and the\n * editor is told WHICH rules the translation would have broken rather than a bare\n * \"failed\".\n */\n const [refusal, setRefusal] = useState<string | null>(null)\n const abortRef = useRef<AbortController | null>(null)\n const cms = useCms()\n // The session registry the produced draft is judged against. Built here rather\n // than added to the CMS store's public surface for one consumer; the memo means\n // once per open, not once per render.\n const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections])\n\n useEffect(() => () => abortRef.current?.abort(), [])\n\n const base = localeConfig.default\n const targets = localeConfig.locales.filter((l) => l !== base)\n\n const run = useCallback(async () => {\n if (projectId === undefined) {\n setPhase('error')\n return\n }\n const resolved = resolveAiConfig(resolveDeploymentAi())\n const provider = resolved.resolved?.provider\n if (!provider || !resolved.ok) {\n setPhase('error')\n return\n }\n const controller = new AbortController()\n abortRef.current = controller\n setRefusal(null)\n setPhase('running')\n await flushDraft()\n setProgress({ completed: 0, total: 0 })\n try {\n const knowledge = await resolveKnowledge(resolved.resolved?.knowledge ?? [])\n const result = await translateDocument({\n provider,\n collection,\n document,\n localeConfig,\n knowledge,\n signal: controller.signal,\n onProgress: setProgress,\n // EC-497 — this action used to hand `translateDocument`'s output STRAIGHT to\n // `documents.upsert` with nothing in between: no gate, no proposal card, no\n // re-validation, so model-generated content reached the document completely\n // unvalidated while the chat path validated twice. It now validates with the\n // session's canonical bag (`cms.validateOptions`) — the editor's own rules —\n // and refuses below on what the translation INTRODUCED.\n registry,\n validateOptions: cms.validateOptions,\n })\n if (result.introducedErrors.length > 0) {\n setRefusal(\n translate('ai.error.applyInvalid', {\n issues: result.introducedErrors\n .slice(0, 3)\n .map((issue) => cmsValidationIssueMessage(issue))\n .join('; '),\n }),\n )\n setPhase('error')\n return\n }\n if (result.translated > 0 || result.slugsDerived > 0) {\n // EC-595 A2: the translation persists as the patches of the buckets it filled\n // (never the whole document) — through the session's queue, at the revision\n // the draft it was authored on is at (EC-647); a bucket a colleague changed\n // meanwhile is refused on that field and shown there.\n const outcome = await cms.commitPatches(\n docRef,\n result.patches,\n result.patches.map(() => revision),\n )\n if (outcome?.status === 'failed') {\n setPhase('error')\n return\n }\n const { operations } = await getStudioOperations()\n // Provenance: an AI-tagged version snapshot (best-effort — the draft already landed).\n try {\n await operations.documents.snapshot({\n projectId,\n ref: docRef,\n label: translate('cms.version.aiAuthoredTitle'),\n origin: 'assistant',\n })\n } catch {\n // ignore — provenance is additive\n }\n }\n setSummary({\n translated: result.translated,\n slugs: result.slugsDerived,\n locales: Object.keys(result.perLocale).length,\n cancelled: result.cancelled,\n nothing: result.total === 0 && result.skipped.length === 0,\n skipped: result.skipped,\n })\n setPhase('done')\n } catch {\n setPhase('error')\n } finally {\n abortRef.current = null\n }\n }, [projectId, collection, document, revision, localeConfig, docRef, flushDraft, registry, cms])\n\n const close = () => {\n if (phase === 'running') return // never close mid-run — Cancel aborts instead\n onOpenChange(false)\n }\n\n const doneMessage = (s: RunSummary): string => {\n if (s.nothing) return translate('ai.translate.nothing')\n if (s.cancelled) return translate('ai.translate.cancelled')\n return translate('ai.translate.applied', { count: s.translated, locales: s.locales })\n }\n\n return (\n <Dialog open={open} onOpenChange={(next) => (!next ? close() : undefined)}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n <Row gap=\"2\" align=\"center\">\n <Languages className=\"size-4\" /> {translate('ai.translate.dialogTitle')}\n </Row>\n </DialogTitle>\n <DialogDescription>\n {translate('ai.translate.dialogDescription', { base: localeChipLabel(base) })}\n </DialogDescription>\n </DialogHeader>\n\n {phase === 'confirm' ? (\n <Stack gap=\"4\" className=\"py-2\">\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.targetsLabel')}\n </Text>\n <Row gap=\"1\" wrap>\n {targets.map((l) => (\n <Badge key={l} variant=\"outline\">\n {localeChipLabel(l)}\n </Badge>\n ))}\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.scopeLabel')}\n </Text>\n <Row gap=\"2\" align=\"center\">\n <Badge variant=\"default\">{translate('ai.translate.fillMissing')}</Badge>\n <Button size=\"sm\" variant=\"ghost\" disabled title={translate('ai.translate.retranslateSoon')}>\n {translate('ai.translate.retranslateAll')}\n </Button>\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.translate.retranslateSoon')}\n </Text>\n </Row>\n </Stack>\n </Stack>\n ) : null}\n\n {phase === 'running' ? (\n <Stack gap=\"3\" align=\"center\" className=\"py-6\">\n <Loader2 className=\"size-6 animate-spin text-primary\" />\n <Text size=\"sm\" tone=\"muted\" data-testid=\"translate-progress\">\n {progress && progress.total > 0\n ? translate('ai.translate.progress', { completed: progress.completed, total: progress.total })\n : translate('ai.translate.preparing')}\n </Text>\n {progress && progress.total > 0 ? (\n <div className=\"h-1.5 w-56 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-primary transition-all\"\n style={{ width: `${Math.round((progress.completed / progress.total) * 100)}%` }}\n />\n </div>\n ) : null}\n </Stack>\n ) : null}\n\n {phase === 'done' && summary ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <Check className=\"size-6 text-success\" />\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\" data-testid=\"translate-summary\">\n {doneMessage(summary)}\n </Text>\n <TranslateSkipped skipped={summary.skipped} collection={collection} />\n </Stack>\n ) : null}\n\n {phase === 'error' ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <X className=\"size-6 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\" data-testid=\"translate-error\">\n {refusal ?? translate('ai.translate.failed')}\n </Text>\n </Stack>\n ) : null}\n\n <DialogFooter className=\"mt-2\">\n {phase === 'confirm' ? (\n <>\n <Button variant=\"ghost\" size=\"sm\" onClick={close}>\n {translate('common.cancel')}\n </Button>\n <Button size=\"sm\" onClick={() => void run()} data-testid=\"translate-confirm\">\n <Languages /> {translate('ai.translate.confirm')}\n </Button>\n </>\n ) : phase === 'running' ? (\n <Button variant=\"outline\" size=\"sm\" onClick={() => abortRef.current?.abort()} data-testid=\"translate-cancel\">\n {translate('common.cancel')}\n </Button>\n ) : (\n <Button size=\"sm\" onClick={close} data-testid=\"translate-close\">\n {translate('ai.translate.close')}\n </Button>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"translate-document-dialog.js","sourceRoot":"","sources":["../../../src/components/ai/translate-document-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { createCollectionRegistry } from '@elytracms/core/cms-core'\nimport type { CollectionDef, Locale } from '@elytracms/core/cms-core'\nimport { Check, Languages, Loader2, X } from 'lucide-react'\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Row,\n SegmentedControl,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { localeChipLabel } from '../../lib/locale'\nimport { getStudioOperations } from '../../lib/operations'\nimport { resolveDeploymentAi } from '../../lib/workspace'\nimport { planTranslation, resolveAiConfig, resolveKnowledge, translateDocument } from '../../lib/ai'\nimport type { TranslateMode, TranslatePlan, TranslateProgress, TranslateSkip, TranslateWrite } from '../../lib/ai'\nimport { applyTranslation } from '../../lib/ai/translate-apply'\nimport { cmsValidationIssueMessage } from '../../lib/persona/labels'\nimport { useCms } from '../cms/cms-store'\nimport type { TranslateDocumentActionProps } from './translate-document-action'\nimport { TranslateBucketList, TranslateSkipped } from './translate-skipped'\n\n/**\n * The \"Translate document\" confirm + progress dialog (EC-332), heavy side — loaded lazily\n * by the gate so the AI engine stays off the critical path (EC-331 bundle discipline). A\n * modal is allowed here (a picker/confirmation, per the house rule — property forms are\n * not). It resolves the EC-331 provider + knowledge, runs the ONE `translateDocument`\n * engine over the live draft, streams progress, then writes the result to the DRAFT via\n * the session (`applyTranslation`) and reflects it into the open editor.\n *\n * The scope is a choice (EC-752): **Fill missing** (the default — only values still\n * connected to the base, EC-318; nothing is overwritten) or **Re-translate everything**,\n * which also REPLACES values with their own translation. Choosing it shows the run's own\n * pre-provider plan (`planTranslation` — what it replaces, fills and leaves as it is), and\n * running it takes the deliberate \"Re-translate and replace\". It snapshots the draft as a\n * labelled version BEFORE writing, so Versions brings any replaced value back; it never\n * touches a slug the editor set and writes a value only whole. The done summary reports\n * what was actually written.\n *\n * Cancel aborts cleanly: fill-empty keeps the already-translated units on the draft;\n * re-translate keeps only values whose every unit landed. Either way the result is visible\n * in Compare (EC-317).\n */\ntype Phase = 'confirm' | 'running' | 'done' | 'error'\n\n/** What the done phase reports — what the run actually wrote, against what it planned. */\nexport interface TranslateRunSummary {\n mode: TranslateMode\n plan: TranslatePlan\n /** Units translated (the fill message's count). */\n translated: number\n locales: number\n cancelled: boolean\n /** EC-752: the writes that landed (a write refused at its revision is in {@link refused}). */\n writes: readonly TranslateWrite[]\n refused: readonly TranslateWrite[]\n /** EC-752: the version saved before a re-translation wrote. */\n restorePoint?: number\n /** EC-593: every text position the run could not translate — listed, never silent. */\n skipped: readonly TranslateSkip[]\n}\n\nexport function TranslateDocumentDialog({\n projectId,\n collection,\n docRef,\n document,\n revision,\n localeConfig,\n flushDraft,\n open,\n onOpenChange,\n}: TranslateDocumentActionProps & { open: boolean; onOpenChange: (open: boolean) => void }) {\n const [phase, setPhase] = useState<Phase>('confirm')\n const [mode, setMode] = useState<TranslateMode>('fill-empty')\n const [progress, setProgress] = useState<TranslateProgress | null>(null)\n const [summary, setSummary] = useState<TranslateRunSummary | null>(null)\n /**\n * EC-497 — why the refusal was blocked, when the gate refused. Reuses the\n * EC-473 `ai.error.applyInvalid` copy (\"the proposal no longer fits the current\n * document: …\"), which is exactly this situation: no new catalog string, and the\n * editor is told WHICH rules the translation would have broken rather than a bare\n * \"failed\". EC-752: also why a re-translation wrote nothing (no restore point).\n */\n const [refusal, setRefusal] = useState<string | null>(null)\n const abortRef = useRef<AbortController | null>(null)\n const cms = useCms()\n // The session registry the produced draft is judged against. Built here rather\n // than added to the CMS store's public surface for one consumer; the memo means\n // once per open, not once per render.\n const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections])\n\n useEffect(() => () => abortRef.current?.abort(), [])\n\n const base = localeConfig.default\n const targets = localeConfig.locales.filter((l) => l !== base)\n // EC-752 rule 4: the confirm lists the run's OWN plan — the same judge, on the same input.\n const plan = useMemo(\n () =>\n mode === 'retranslate-all'\n ? planTranslation({ collection, document, localeConfig, mode, validateOptions: cms.validateOptions })\n : null,\n [mode, collection, document, localeConfig, cms.validateOptions],\n )\n\n const run = useCallback(async () => {\n if (projectId === undefined) {\n setPhase('error')\n return\n }\n const resolved = resolveAiConfig(resolveDeploymentAi())\n const provider = resolved.resolved?.provider\n if (!provider || !resolved.ok) {\n setPhase('error')\n return\n }\n const controller = new AbortController()\n abortRef.current = controller\n setRefusal(null)\n setPhase('running')\n await flushDraft()\n setProgress({ completed: 0, total: 0 })\n try {\n const knowledge = await resolveKnowledge(resolved.resolved?.knowledge ?? [])\n const result = await translateDocument({\n provider,\n collection,\n document,\n localeConfig,\n mode,\n knowledge,\n signal: controller.signal,\n onProgress: setProgress,\n // EC-497 — this action used to hand `translateDocument`'s output STRAIGHT to\n // `documents.upsert` with nothing in between: no gate, no proposal card, no\n // re-validation, so model-generated content reached the document completely\n // unvalidated while the chat path validated twice. It now validates with the\n // session's canonical bag (`cms.validateOptions`) — the editor's own rules —\n // and refuses below on what the translation INTRODUCED.\n registry,\n validateOptions: cms.validateOptions,\n })\n if (result.introducedErrors.length > 0) {\n setRefusal(\n translate('ai.error.applyInvalid', {\n issues: result.introducedErrors\n .slice(0, 3)\n .map((issue) => cmsValidationIssueMessage(issue))\n .join('; '),\n }),\n )\n setPhase('error')\n return\n }\n const { operations } = await getStudioOperations()\n // EC-752 rule 3 (in `applyTranslation`): a re-translation records a labelled restore\n // point of the stored draft BEFORE it writes; then the patches go through the session's\n // queue at the revision the draft was authored on (EC-595 A2 / EC-647 — a bucket a\n // colleague changed meanwhile is refused on that field and shown there); then the\n // AI-provenance snapshot.\n const applied = await applyTranslation(\n result,\n {\n commit: (patches) => cms.commitPatches(docRef, patches, patches.map(() => revision)),\n snapshot: async ({ label, origin }) => {\n const recorded = await operations.documents.snapshot({\n projectId,\n ref: docRef,\n label,\n ...(origin ? { origin } : {}),\n })\n if (!recorded.ok) throw new Error(recorded.error.message)\n return recorded.value\n },\n },\n { restorePoint: translate('ai.translate.snapshotLabel'), provenance: translate('cms.version.aiAuthoredTitle') },\n )\n if (applied.status === 'snapshot-failed') {\n setRefusal(translate('ai.translate.snapshotFailed'))\n setPhase('error')\n return\n }\n if (applied.status === 'failed') {\n setPhase('error')\n return\n }\n setSummary({\n mode,\n plan: result.plan,\n translated: result.translated,\n locales: Object.keys(result.perLocale).length,\n cancelled: result.cancelled,\n writes: applied.writes,\n refused: applied.refused,\n ...(applied.restorePoint !== undefined ? { restorePoint: applied.restorePoint } : {}),\n skipped: result.skipped,\n })\n setPhase('done')\n } catch {\n setPhase('error')\n } finally {\n abortRef.current = null\n }\n }, [projectId, collection, document, revision, localeConfig, mode, docRef, flushDraft, registry, cms])\n\n const close = () => {\n if (phase === 'running') return // never close mid-run — Cancel aborts instead\n onOpenChange(false)\n }\n\n const replaces = (plan?.buckets ?? []).some((bucket) => bucket.kind === 'replace')\n\n return (\n <Dialog open={open} onOpenChange={(next) => (!next ? close() : undefined)}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n <Row gap=\"2\" align=\"center\">\n <Languages className=\"size-4\" /> {translate('ai.translate.dialogTitle')}\n </Row>\n </DialogTitle>\n <DialogDescription>\n {translate('ai.translate.dialogDescription', { base: localeChipLabel(base) })}\n </DialogDescription>\n </DialogHeader>\n\n {phase === 'confirm' ? (\n <TranslateConfirm\n mode={mode}\n onModeChange={setMode}\n base={base}\n targets={targets}\n plan={plan}\n collection={collection}\n />\n ) : null}\n\n {phase === 'running' ? (\n <Stack gap=\"3\" align=\"center\" className=\"py-6\">\n <Loader2 className=\"size-6 animate-spin text-primary\" />\n <Text size=\"sm\" tone=\"muted\" data-testid=\"translate-progress\">\n {progress && progress.total > 0\n ? translate('ai.translate.progress', { completed: progress.completed, total: progress.total })\n : translate('ai.translate.preparing')}\n </Text>\n {progress && progress.total > 0 ? (\n <div className=\"h-1.5 w-56 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-primary transition-all\"\n style={{ width: `${Math.round((progress.completed / progress.total) * 100)}%` }}\n />\n </div>\n ) : null}\n </Stack>\n ) : null}\n\n {phase === 'done' && summary ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <Check className=\"size-6 text-success\" />\n <TranslateDoneSummary summary={summary} base={base} collection={collection} />\n </Stack>\n ) : null}\n\n {phase === 'error' ? (\n <Stack gap=\"2\" align=\"center\" className=\"py-6\">\n <X className=\"size-6 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\" data-testid=\"translate-error\">\n {refusal ?? translate('ai.translate.failed')}\n </Text>\n </Stack>\n ) : null}\n\n <DialogFooter className=\"mt-2\">\n {phase === 'confirm' ? (\n <>\n <Button variant=\"ghost\" size=\"sm\" onClick={close}>\n {translate('common.cancel')}\n </Button>\n {mode === 'retranslate-all' && replaces ? (\n // EC-752: the deliberate confirm — the button says what it does.\n <Button size=\"sm\" variant=\"destructive\" onClick={() => void run()} data-testid=\"translate-confirm\">\n <Languages /> {translate('ai.translate.confirmReplace')}\n </Button>\n ) : (\n <Button size=\"sm\" onClick={() => void run()} data-testid=\"translate-confirm\">\n <Languages /> {translate('ai.translate.confirm')}\n </Button>\n )}\n </>\n ) : phase === 'running' ? (\n <Button variant=\"outline\" size=\"sm\" onClick={() => abortRef.current?.abort()} data-testid=\"translate-cancel\">\n {translate('common.cancel')}\n </Button>\n ) : (\n <Button size=\"sm\" onClick={close} data-testid=\"translate-close\">\n {translate('ai.translate.close')}\n </Button>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\n/**\n * EC-752 — the confirm phase: the target languages, the scope choice and, for a\n * re-translation, its plan (what it replaces, fills and leaves as it is — \"up to\").\n */\nexport function TranslateConfirm({\n mode,\n onModeChange,\n base,\n targets,\n plan,\n collection,\n}: {\n mode: TranslateMode\n onModeChange: (mode: TranslateMode) => void\n base: Locale\n targets: readonly Locale[]\n /** The run's plan — present in `retranslate-all`. */\n plan: TranslatePlan | null\n collection: CollectionDef\n}) {\n const baseLabel = localeChipLabel(base)\n return (\n <Stack gap=\"4\" className=\"py-2\">\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.targetsLabel')}\n </Text>\n <Row gap=\"1\" wrap>\n {targets.map((l) => (\n <Badge key={l} variant=\"outline\">\n {localeChipLabel(l)}\n </Badge>\n ))}\n </Row>\n </Stack>\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"medium\" tone=\"muted\">\n {translate('ai.translate.scopeLabel')}\n </Text>\n <SegmentedControl\n ariaLabel={translate('ai.translate.scopeLabel')}\n testId=\"translate-scope\"\n items={[\n {\n key: 'fill-empty',\n label: translate('ai.translate.fillMissing'),\n active: mode === 'fill-empty',\n onClick: () => onModeChange('fill-empty'),\n testId: 'translate-scope-fill',\n },\n {\n key: 'retranslate-all',\n label: translate('ai.translate.retranslateAll'),\n active: mode === 'retranslate-all',\n onClick: () => onModeChange('retranslate-all'),\n testId: 'translate-scope-retranslate',\n },\n ]}\n />\n <Text size=\"xs\" tone=\"muted\">\n {translate(mode === 'retranslate-all' ? 'ai.translate.retranslateHint' : 'ai.translate.fillMissingHint', {\n base: baseLabel,\n })}\n </Text>\n </Stack>\n {mode === 'retranslate-all' && plan ? <RetranslatePlan plan={plan} base={base} collection={collection} /> : null}\n </Stack>\n )\n}\n\n/** EC-752 rule 4 — what a re-translation will do, from its own pre-provider plan. */\nfunction RetranslatePlan({ plan, base, collection }: { plan: TranslatePlan; base: Locale; collection: CollectionDef }) {\n const baseLabel = localeChipLabel(base)\n const replace = plan.buckets.filter((bucket) => bucket.kind === 'replace')\n const fill = plan.buckets.filter((bucket) => bucket.kind === 'fill')\n return (\n <Stack gap=\"2\" data-testid=\"translate-plan\">\n {replace.length > 0 ? (\n <TranslateBucketList\n title={translate('ai.translate.planReplaceTitle', { base: baseLabel, count: replace.length })}\n buckets={replace}\n collection={collection}\n testId=\"translate-plan-replace\"\n />\n ) : (\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.translate.planReplaceNone')}\n </Text>\n )}\n <TranslateBucketList\n title={translate('ai.translate.planFillTitle', { base: baseLabel, count: fill.length })}\n buckets={fill}\n collection={collection}\n testId=\"translate-plan-fill\"\n />\n <TranslateSkipped\n skipped={plan.skipped}\n collection={collection}\n mode={plan.mode}\n title={translate('ai.translate.planKeptTitle', { count: plan.skipped.length })}\n />\n <Stack gap=\"px\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.translate.planWhole')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.translate.planSlugsKept')}\n </Text>\n {replace.length > 0 ? (\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.translate.planRestorePoint')}\n </Text>\n ) : null}\n </Stack>\n </Stack>\n )\n}\n\n/** The headline of the done phase (EC-752: mode-aware; slugs and replacements are listed below it). */\nexport function translateDoneMessage(summary: TranslateRunSummary, base: Locale): string {\n const retranslate = summary.mode === 'retranslate-all'\n const nothing =\n summary.plan.total === 0 && summary.skipped.length === 0 && summary.writes.length === 0 && summary.refused.length === 0\n if (nothing) {\n return retranslate\n ? translate('ai.translate.retranslateNothing', { base: localeChipLabel(base) })\n : translate('ai.translate.nothing')\n }\n if (summary.cancelled) return translate(retranslate ? 'ai.translate.retranslateCancelled' : 'ai.translate.cancelled')\n if (retranslate) return translate('ai.translate.retranslateDone')\n return translate('ai.translate.applied', { count: summary.translated, locales: summary.locales })\n}\n\n/**\n * EC-752 — the done phase: what was actually written (a re-translation lists what it\n * replaced and filled), the slugs it set, what was refused at its revision, what a\n * re-translation planned but left as it was, its restore point, and what was not translated.\n */\nexport function TranslateDoneSummary({\n summary,\n base,\n collection,\n}: {\n summary: TranslateRunSummary\n base: Locale\n collection: CollectionDef\n}) {\n const retranslate = summary.mode === 'retranslate-all'\n const of = (kind: TranslateWrite['kind']) => summary.writes.filter((write) => write.kind === kind)\n const done = (bucket: { field: string; locale: string }) =>\n [...summary.writes, ...summary.refused].some((write) => write.field === bucket.field && write.locale === bucket.locale)\n const kept = retranslate ? summary.plan.buckets.filter((bucket) => !done(bucket)) : []\n return (\n <Stack gap=\"2\" className=\"w-full\">\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\" data-testid=\"translate-summary\">\n {translateDoneMessage(summary, base)}\n </Text>\n {retranslate ? (\n <>\n <TranslateBucketList\n title={translate('ai.translate.replacedTitle', { count: of('replace').length })}\n buckets={of('replace')}\n collection={collection}\n testId=\"translate-replaced\"\n />\n <TranslateBucketList\n title={translate('ai.translate.filledTitle', { count: of('fill').length })}\n buckets={of('fill')}\n collection={collection}\n testId=\"translate-filled\"\n />\n </>\n ) : null}\n <TranslateBucketList\n title={translate('ai.translate.slugsTitle', { count: of('slug').length })}\n buckets={of('slug')}\n collection={collection}\n testId=\"translate-slugs\"\n />\n <TranslateBucketList\n title={translate('ai.translate.refusedTitle', { count: summary.refused.length })}\n buckets={summary.refused}\n collection={collection}\n testId=\"translate-refused\"\n />\n <TranslateBucketList\n title={translate('ai.translate.keptTitle', { count: kept.length })}\n buckets={kept}\n collection={collection}\n testId=\"translate-kept\"\n />\n {summary.restorePoint !== undefined ? (\n <Text size=\"xs\" tone=\"muted\" data-testid=\"translate-restore-point\">\n {translate('ai.translate.restorePoint', { version: summary.restorePoint })}\n </Text>\n ) : null}\n <TranslateSkipped skipped={summary.skipped} collection={collection} mode={summary.mode} />\n </Stack>\n )\n}\n"]}
|
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
2
|
-
import type { TranslateSkip } from '../../lib/ai/index.js';
|
|
2
|
+
import type { TranslateBucket, TranslateMode, TranslateSkip } from '../../lib/ai/index.js';
|
|
3
3
|
/** One skip as the editor reads it: where (field label › component › position), which language, why. */
|
|
4
4
|
export interface TranslateSkipLine {
|
|
5
5
|
where: string;
|
|
6
6
|
locale: string;
|
|
7
7
|
reason: string;
|
|
8
8
|
}
|
|
9
|
+
/** EC-752 — a field × locale value as the editor reads it: `Title (DE)`. */
|
|
10
|
+
export declare function translateBucketLabel(bucket: TranslateBucket, collection: CollectionDef | undefined): string;
|
|
9
11
|
/**
|
|
10
12
|
* The where-label of a skipped position: the field's label, then — inside a component —
|
|
11
13
|
* the component and its prop path, else the position below the field (an item counted
|
|
12
14
|
* from 1). Index-bearing segments the editor does not see (`content`, `props`) are left out.
|
|
13
15
|
*/
|
|
14
|
-
export declare function translateSkipLines(skipped: readonly TranslateSkip[], collection: CollectionDef | undefined): TranslateSkipLine[];
|
|
16
|
+
export declare function translateSkipLines(skipped: readonly TranslateSkip[], collection: CollectionDef | undefined, mode?: TranslateMode): TranslateSkipLine[];
|
|
15
17
|
/** The list; renders nothing when everything was translated. */
|
|
16
|
-
export declare function TranslateSkipped({ skipped, collection, }: {
|
|
18
|
+
export declare function TranslateSkipped({ skipped, collection, mode, title, }: {
|
|
17
19
|
skipped: readonly TranslateSkip[];
|
|
18
20
|
collection: CollectionDef | undefined;
|
|
21
|
+
/** EC-752: the run's mode — a re-translation's reasons say the whole value was left as it was. */
|
|
22
|
+
mode?: TranslateMode;
|
|
23
|
+
/** EC-752: the heading (default "Not translated (n)"); the confirm names what it will leave as it is. */
|
|
24
|
+
title?: string;
|
|
25
|
+
}): import("react").JSX.Element | null;
|
|
26
|
+
/**
|
|
27
|
+
* EC-752 — a list of field × locale values under a heading (what a re-translation replaces,
|
|
28
|
+
* fills, set as slugs or left as it was); renders nothing for an empty list.
|
|
29
|
+
*/
|
|
30
|
+
export declare function TranslateBucketList({ title, buckets, collection, testId, }: {
|
|
31
|
+
title: string;
|
|
32
|
+
buckets: readonly TranslateBucket[];
|
|
33
|
+
collection: CollectionDef | undefined;
|
|
34
|
+
testId: string;
|
|
19
35
|
}): import("react").JSX.Element | null;
|
|
@@ -9,6 +9,9 @@ import { localeChipLabel } from '../../lib/locale/index.js';
|
|
|
9
9
|
* editor reads the outcome: the "Translate document" dialog and the chat's translation
|
|
10
10
|
* proposal card. Before, a localized object or composition was skipped with no word,
|
|
11
11
|
* and a garbled rich-text block silently kept its original.
|
|
12
|
+
*
|
|
13
|
+
* EC-752 — the reason is MODE-aware: a re-translation writes a value whole or not at all,
|
|
14
|
+
* so its reasons say the whole value was left as it was (fill-empty fills the rest).
|
|
12
15
|
*/
|
|
13
16
|
const REASON_KEY = {
|
|
14
17
|
unreadable: 'ai.translate.skipped.unreadable',
|
|
@@ -16,15 +19,29 @@ const REASON_KEY = {
|
|
|
16
19
|
'structure-changed': 'ai.translate.skipped.structureChanged',
|
|
17
20
|
locked: 'ai.translate.skipped.locked',
|
|
18
21
|
};
|
|
22
|
+
const RETRANSLATE_REASON_KEY = {
|
|
23
|
+
unreadable: 'ai.translate.skipped.retranslate.unreadable',
|
|
24
|
+
'not-returned': 'ai.translate.skipped.retranslate.notReturned',
|
|
25
|
+
'structure-changed': 'ai.translate.skipped.retranslate.structureChanged',
|
|
26
|
+
locked: 'ai.translate.skipped.locked',
|
|
27
|
+
};
|
|
28
|
+
/** A top-level field's label as the editor reads it (its form label, else its name). */
|
|
29
|
+
function fieldLabel(fieldName, collection) {
|
|
30
|
+
const field = collection?.fields.find((f) => f.name === fieldName);
|
|
31
|
+
return resolveLabel(field?.form?.label) ?? fieldName;
|
|
32
|
+
}
|
|
33
|
+
/** EC-752 — a field × locale value as the editor reads it: `Title (DE)`. */
|
|
34
|
+
export function translateBucketLabel(bucket, collection) {
|
|
35
|
+
return `${fieldLabel(bucket.field, collection)} (${localeChipLabel(bucket.locale)})`;
|
|
36
|
+
}
|
|
19
37
|
/**
|
|
20
38
|
* The where-label of a skipped position: the field's label, then — inside a component —
|
|
21
39
|
* the component and its prop path, else the position below the field (an item counted
|
|
22
40
|
* from 1). Index-bearing segments the editor does not see (`content`, `props`) are left out.
|
|
23
41
|
*/
|
|
24
|
-
export function translateSkipLines(skipped, collection) {
|
|
42
|
+
export function translateSkipLines(skipped, collection, mode = 'fill-empty') {
|
|
43
|
+
const reasons = mode === 'retranslate-all' ? RETRANSLATE_REASON_KEY : REASON_KEY;
|
|
25
44
|
return skipped.map((skip) => {
|
|
26
|
-
const field = collection?.fields.find((f) => f.name === skip.field);
|
|
27
|
-
const label = resolveLabel(field?.form?.label) ?? skip.field;
|
|
28
45
|
const below = skip.component
|
|
29
46
|
? [skip.component.componentId, ...skip.at.slice(skip.at.lastIndexOf('props') + 1)]
|
|
30
47
|
: skip.at;
|
|
@@ -32,16 +49,25 @@ export function translateSkipLines(skipped, collection) {
|
|
|
32
49
|
.filter((segment) => segment !== 'content' && segment !== 'props')
|
|
33
50
|
.map((segment) => (typeof segment === 'number' ? `#${segment + 1}` : segment));
|
|
34
51
|
return {
|
|
35
|
-
where: [
|
|
52
|
+
where: [fieldLabel(skip.field, collection), ...segments].join(' › '),
|
|
36
53
|
locale: localeChipLabel(skip.locale),
|
|
37
|
-
reason: translate(
|
|
54
|
+
reason: translate(reasons[skip.reason]),
|
|
38
55
|
};
|
|
39
56
|
});
|
|
40
57
|
}
|
|
41
58
|
/** The list; renders nothing when everything was translated. */
|
|
42
|
-
export function TranslateSkipped({ skipped, collection, }) {
|
|
59
|
+
export function TranslateSkipped({ skipped, collection, mode = 'fill-empty', title, }) {
|
|
43
60
|
if (skipped.length === 0)
|
|
44
61
|
return null;
|
|
45
|
-
return (_jsxs(Stack, { gap: "1", "data-testid": "translate-skipped", children: [_jsxs(Row, { gap: "1", align: "center", children: [_jsx(AlertTriangle, { className: "size-3.5 text-warning" }), _jsx(Text, { size: "xs", weight: "medium", children: translate('ai.translate.skippedTitle', { count: skipped.length }) })] }), translateSkipLines(skipped, collection).map((line, index) => (_jsxs(Text, { size: "xs", tone: "muted", "data-translate-skip": true, children: [line.where, " (", line.locale, ") \u2014 ", line.reason] }, index)))] }));
|
|
62
|
+
return (_jsxs(Stack, { gap: "1", "data-testid": "translate-skipped", children: [_jsxs(Row, { gap: "1", align: "center", children: [_jsx(AlertTriangle, { className: "size-3.5 text-warning" }), _jsx(Text, { size: "xs", weight: "medium", children: title ?? translate('ai.translate.skippedTitle', { count: skipped.length }) })] }), translateSkipLines(skipped, collection, mode).map((line, index) => (_jsxs(Text, { size: "xs", tone: "muted", "data-translate-skip": true, children: [line.where, " (", line.locale, ") \u2014 ", line.reason] }, index)))] }));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* EC-752 — a list of field × locale values under a heading (what a re-translation replaces,
|
|
66
|
+
* fills, set as slugs or left as it was); renders nothing for an empty list.
|
|
67
|
+
*/
|
|
68
|
+
export function TranslateBucketList({ title, buckets, collection, testId, }) {
|
|
69
|
+
if (buckets.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
return (_jsxs(Stack, { gap: "1", "data-testid": testId, children: [_jsx(Text, { size: "xs", weight: "medium", children: title }), _jsx(Text, { size: "xs", tone: "muted", "data-translate-buckets": true, children: buckets.map((bucket) => translateBucketLabel(bucket, collection)).join(', ') })] }));
|
|
46
72
|
}
|
|
47
73
|
//# sourceMappingURL=translate-skipped.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate-skipped.js","sourceRoot":"","sources":["../../../src/components/ai/translate-skipped.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEvD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAGlD
|
|
1
|
+
{"version":3,"file":"translate-skipped.js","sourceRoot":"","sources":["../../../src/components/ai/translate-skipped.tsx"],"names":[],"mappings":"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","sourcesContent":["import { resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { AlertTriangle } from 'lucide-react'\nimport { Row, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { localeChipLabel } from '../../lib/locale'\nimport type { TranslateBucket, TranslateMode, TranslateSkip, TranslateSkipReason } from '../../lib/ai'\n\n/**\n * EC-593 (ratified 4e) — what a translation run could NOT translate, shown where the\n * editor reads the outcome: the \"Translate document\" dialog and the chat's translation\n * proposal card. Before, a localized object or composition was skipped with no word,\n * and a garbled rich-text block silently kept its original.\n *\n * EC-752 — the reason is MODE-aware: a re-translation writes a value whole or not at all,\n * so its reasons say the whole value was left as it was (fill-empty fills the rest).\n */\n\nconst REASON_KEY: Record<TranslateSkipReason, string> = {\n unreadable: 'ai.translate.skipped.unreadable',\n 'not-returned': 'ai.translate.skipped.notReturned',\n 'structure-changed': 'ai.translate.skipped.structureChanged',\n locked: 'ai.translate.skipped.locked',\n}\n\nconst RETRANSLATE_REASON_KEY: Record<TranslateSkipReason, string> = {\n unreadable: 'ai.translate.skipped.retranslate.unreadable',\n 'not-returned': 'ai.translate.skipped.retranslate.notReturned',\n 'structure-changed': 'ai.translate.skipped.retranslate.structureChanged',\n locked: 'ai.translate.skipped.locked',\n}\n\n/** One skip as the editor reads it: where (field label › component › position), which language, why. */\nexport interface TranslateSkipLine {\n where: string\n locale: string\n reason: string\n}\n\n/** A top-level field's label as the editor reads it (its form label, else its name). */\nfunction fieldLabel(fieldName: string, collection: CollectionDef | undefined): string {\n const field = collection?.fields.find((f) => f.name === fieldName)\n return resolveLabel(field?.form?.label) ?? fieldName\n}\n\n/** EC-752 — a field × locale value as the editor reads it: `Title (DE)`. */\nexport function translateBucketLabel(bucket: TranslateBucket, collection: CollectionDef | undefined): string {\n return `${fieldLabel(bucket.field, collection)} (${localeChipLabel(bucket.locale)})`\n}\n\n/**\n * The where-label of a skipped position: the field's label, then — inside a component —\n * the component and its prop path, else the position below the field (an item counted\n * from 1). Index-bearing segments the editor does not see (`content`, `props`) are left out.\n */\nexport function translateSkipLines(\n skipped: readonly TranslateSkip[],\n collection: CollectionDef | undefined,\n mode: TranslateMode = 'fill-empty',\n): TranslateSkipLine[] {\n const reasons = mode === 'retranslate-all' ? RETRANSLATE_REASON_KEY : REASON_KEY\n return skipped.map((skip) => {\n const below = skip.component\n ? [skip.component.componentId, ...skip.at.slice(skip.at.lastIndexOf('props') + 1)]\n : skip.at\n const segments = below\n .filter((segment) => segment !== 'content' && segment !== 'props')\n .map((segment) => (typeof segment === 'number' ? `#${segment + 1}` : segment))\n return {\n where: [fieldLabel(skip.field, collection), ...segments].join(' › '),\n locale: localeChipLabel(skip.locale),\n reason: translate(reasons[skip.reason]),\n }\n })\n}\n\n/** The list; renders nothing when everything was translated. */\nexport function TranslateSkipped({\n skipped,\n collection,\n mode = 'fill-empty',\n title,\n}: {\n skipped: readonly TranslateSkip[]\n collection: CollectionDef | undefined\n /** EC-752: the run's mode — a re-translation's reasons say the whole value was left as it was. */\n mode?: TranslateMode\n /** EC-752: the heading (default \"Not translated (n)\"); the confirm names what it will leave as it is. */\n title?: string\n}) {\n if (skipped.length === 0) return null\n return (\n <Stack gap=\"1\" data-testid=\"translate-skipped\">\n <Row gap=\"1\" align=\"center\">\n <AlertTriangle className=\"size-3.5 text-warning\" />\n <Text size=\"xs\" weight=\"medium\">\n {title ?? translate('ai.translate.skippedTitle', { count: skipped.length })}\n </Text>\n </Row>\n {translateSkipLines(skipped, collection, mode).map((line, index) => (\n <Text key={index} size=\"xs\" tone=\"muted\" data-translate-skip>\n {line.where} ({line.locale}) — {line.reason}\n </Text>\n ))}\n </Stack>\n )\n}\n\n/**\n * EC-752 — a list of field × locale values under a heading (what a re-translation replaces,\n * fills, set as slugs or left as it was); renders nothing for an empty list.\n */\nexport function TranslateBucketList({\n title,\n buckets,\n collection,\n testId,\n}: {\n title: string\n buckets: readonly TranslateBucket[]\n collection: CollectionDef | undefined\n testId: string\n}) {\n if (buckets.length === 0) return null\n return (\n <Stack gap=\"1\" data-testid={testId}>\n <Text size=\"xs\" weight=\"medium\">\n {title}\n </Text>\n <Text size=\"xs\" tone=\"muted\" data-translate-buckets>\n {buckets.map((bucket) => translateBucketLabel(bucket, collection)).join(', ')}\n </Text>\n </Stack>\n )\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { AlertTriangle, X } from 'lucide-react';
|
|
4
|
+
import { elytraGuidePath } from '@elytracms/core/studio-core';
|
|
4
5
|
import { translate } from '../../lib/i18n/index.js';
|
|
5
6
|
import { isDevRuntime } from '../../lib/persistence/index.js';
|
|
6
7
|
import { Row, Text } from '@elytracms/studio/ui';
|
|
@@ -30,6 +31,8 @@ const NOTICE_KEYS = {
|
|
|
30
31
|
'no-theme-tokens': 'builder.canvasTheme.noThemeTokensNotice',
|
|
31
32
|
'no-stylesheet': 'builder.canvasTheme.noStylesheetNotice',
|
|
32
33
|
};
|
|
34
|
+
/** EC-765: the notices point at the guide's theming chapter where the project has it installed. */
|
|
35
|
+
const THEMING_CHAPTER = elytraGuidePath('13-theming.md');
|
|
33
36
|
export function CanvasThemeNotice({ status }) {
|
|
34
37
|
// Local state only forces the re-render on dismiss; the truth is module-level.
|
|
35
38
|
const [, setDismissedAt] = useState(0);
|
|
@@ -38,7 +41,7 @@ export function CanvasThemeNotice({ status }) {
|
|
|
38
41
|
const messageKey = NOTICE_KEYS[status.kind];
|
|
39
42
|
if (!messageKey || dismissedKinds.has(status.kind))
|
|
40
43
|
return null;
|
|
41
|
-
return (_jsx("div", { "data-testid": "canvas-theme-notice", "data-notice-kind": status.kind, className: "absolute top-4 left-1/2 z-20 w-max max-w-[min(460px,90%)] -translate-x-1/2", children: _jsx("div", { className: "rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg", children: _jsxs(Row, { gap: "2", align: "start", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3.5 shrink-0 text-warning", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "min-w-0 flex-1", children: translate(messageKey) }), _jsx("button", { type: "button", "aria-label": translate('common.dismiss'), onClick: () => {
|
|
44
|
+
return (_jsx("div", { "data-testid": "canvas-theme-notice", "data-notice-kind": status.kind, className: "absolute top-4 left-1/2 z-20 w-max max-w-[min(460px,90%)] -translate-x-1/2", children: _jsx("div", { className: "rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg", children: _jsxs(Row, { gap: "2", align: "start", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3.5 shrink-0 text-warning", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "min-w-0 flex-1", children: translate(messageKey, { guide: THEMING_CHAPTER }) }), _jsx("button", { type: "button", "aria-label": translate('common.dismiss'), onClick: () => {
|
|
42
45
|
dismissedKinds.add(status.kind);
|
|
43
46
|
setDismissedAt(Date.now());
|
|
44
47
|
}, className: "shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground", children: _jsx(X, { className: "size-3.5" }) })] }) }) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-theme-notice.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-theme-notice.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAGhD;;;;;;;;;;;;;;GAcG;AAEH,MAAM,cAAc,GAAG,IAAI,GAAG,EAA6B,CAAA;AAE3D,sDAAsD;AACtD,MAAM,UAAU,8BAA8B;IAC5C,cAAc,CAAC,KAAK,EAAE,CAAA;AACxB,CAAC;AAED,MAAM,WAAW,GAAuD;IACtE,MAAM,EAAE,kCAAkC;IAC1C,kBAAkB,EAAE,2CAA2C;IAC/D,iBAAiB,EAAE,yCAAyC;IAC5D,eAAe,EAAE,wCAAwC;CAC1D,CAAA;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAE,MAAM,EAAwC;IAChF,+EAA+E;IAC/E,MAAM,CAAC,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IACtC,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,EAAE;QAAE,OAAO,IAAI,CAAA;IAC3C,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3C,IAAI,CAAC,UAAU,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAE/D,OAAO,CACL,6BACc,qBAAqB,sBACf,MAAM,CAAC,IAAI,EAC7B,SAAS,EAAC,4EAA4E,YAEtF,cAAK,SAAS,EAAC,kFAAkF,YAC/F,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,aACxB,KAAC,aAAa,IAAC,SAAS,EAAC,uCAAuC,wBAAe,EAC/E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,gBAAgB,YACvC,SAAS,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"canvas-theme-notice.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-theme-notice.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAGhD;;;;;;;;;;;;;;GAcG;AAEH,MAAM,cAAc,GAAG,IAAI,GAAG,EAA6B,CAAA;AAE3D,sDAAsD;AACtD,MAAM,UAAU,8BAA8B;IAC5C,cAAc,CAAC,KAAK,EAAE,CAAA;AACxB,CAAC;AAED,MAAM,WAAW,GAAuD;IACtE,MAAM,EAAE,kCAAkC;IAC1C,kBAAkB,EAAE,2CAA2C;IAC/D,iBAAiB,EAAE,yCAAyC;IAC5D,eAAe,EAAE,wCAAwC;CAC1D,CAAA;AAED,mGAAmG;AACnG,MAAM,eAAe,GAAG,eAAe,CAAC,eAAe,CAAC,CAAA;AAExD,MAAM,UAAU,iBAAiB,CAAC,EAAE,MAAM,EAAwC;IAChF,+EAA+E;IAC/E,MAAM,CAAC,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IACtC,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,EAAE;QAAE,OAAO,IAAI,CAAA;IAC3C,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3C,IAAI,CAAC,UAAU,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAE/D,OAAO,CACL,6BACc,qBAAqB,sBACf,MAAM,CAAC,IAAI,EAC7B,SAAS,EAAC,4EAA4E,YAEtF,cAAK,SAAS,EAAC,kFAAkF,YAC/F,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,aACxB,KAAC,aAAa,IAAC,SAAS,EAAC,uCAAuC,wBAAe,EAC/E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,gBAAgB,YACvC,SAAS,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,GAC7C,EACP,iBACE,IAAI,EAAC,QAAQ,gBACD,SAAS,CAAC,gBAAgB,CAAC,EACvC,OAAO,EAAE,GAAG,EAAE;4BACZ,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;4BAC/B,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;wBAC5B,CAAC,EACD,SAAS,EAAC,mFAAmF,YAE7F,KAAC,CAAC,IAAC,SAAS,EAAC,UAAU,GAAG,GACnB,IACL,GACF,GACF,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useState } from 'react'\nimport { AlertTriangle, X } from 'lucide-react'\nimport { elytraGuidePath } from '@elytracms/core/studio-core'\nimport { translate } from '../../lib/i18n'\nimport { isDevRuntime } from '../../lib/persistence'\nimport { Row, Text } from '@elytracms/studio/ui'\nimport type { CanvasThemeStatus } from './canvas-chrome'\n\n/**\n * EC-414 AD-D item 2 — the canvas-theme runtime notice, in the established\n * validation-surface style (the `ClipboardNoticeOverlay` shape: a dismissible,\n * NEVER-blocking overlay above the canvas, in the STUDIO document — studio UI\n * primitives are fine here, this is not in-frame chrome). Dev builds only:\n *\n * - `legacy`: the probe found the merged pre-EC-414 canvas.css — a one-time\n * notice naming the migration (the canvas keeps working exactly as before).\n * - `stylesheet-error` / `no-theme-tokens` / `no-stylesheet`: the canvas\n * stylesheet is broken or missing — the invalid state is named explicitly\n * instead of silently rendering unstyled blocks.\n *\n * \"One-time\": dismissing a kind suppresses it for the rest of the studio\n * session (module state survives canvas remounts/navigation).\n */\n\nconst dismissedKinds = new Set<CanvasThemeStatus['kind']>()\n\n/** Test hook: reset the session-sticky dismissals. */\nexport function resetCanvasThemeNoticeForTests(): void {\n dismissedKinds.clear()\n}\n\nconst NOTICE_KEYS: Partial<Record<CanvasThemeStatus['kind'], string>> = {\n legacy: 'builder.canvasTheme.legacyNotice',\n 'stylesheet-error': 'builder.canvasTheme.stylesheetErrorNotice',\n 'no-theme-tokens': 'builder.canvasTheme.noThemeTokensNotice',\n 'no-stylesheet': 'builder.canvasTheme.noStylesheetNotice',\n}\n\n/** EC-765: the notices point at the guide's theming chapter where the project has it installed. */\nconst THEMING_CHAPTER = elytraGuidePath('13-theming.md')\n\nexport function CanvasThemeNotice({ status }: { status: CanvasThemeStatus | null }) {\n // Local state only forces the re-render on dismiss; the truth is module-level.\n const [, setDismissedAt] = useState(0)\n if (!status || !isDevRuntime()) return null\n const messageKey = NOTICE_KEYS[status.kind]\n if (!messageKey || dismissedKinds.has(status.kind)) return null\n\n return (\n <div\n data-testid=\"canvas-theme-notice\"\n data-notice-kind={status.kind}\n className=\"absolute top-4 left-1/2 z-20 w-max max-w-[min(460px,90%)] -translate-x-1/2\"\n >\n <div className=\"rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg\">\n <Row gap=\"2\" align=\"start\">\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n <Text size=\"xs\" className=\"min-w-0 flex-1\">\n {translate(messageKey, { guide: THEMING_CHAPTER })}\n </Text>\n <button\n type=\"button\"\n aria-label={translate('common.dismiss')}\n onClick={() => {\n dismissedKinds.add(status.kind)\n setDismissedAt(Date.now())\n }}\n className=\"shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n <X className=\"size-3.5\" />\n </button>\n </Row>\n </div>\n </div>\n )\n}\n"]}
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Components section landing view (EC-050, overhauled EC-320). Serves BOTH
|
|
3
|
-
* audiences: editors get a live census of what components exist (totals,
|
|
4
|
-
* per-category breakdown, health) and a "pick one" nudge;
|
|
5
|
-
* devs get pointers to the real in-repo docs for registering and extending
|
|
6
|
-
* components — no code sample to drift, no invented docs.
|
|
7
|
-
*/
|
|
8
1
|
export declare function ComponentsIndex({ projectId }: {
|
|
9
2
|
projectId: string;
|
|
10
3
|
}): import("react").JSX.Element;
|