@acms-subir/core 0.3.3-next.20260929113209 → 0.3.3
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/CHANGELOG.md +69 -0
- package/cli/common.mjs +2 -2
- package/cli/legacy.mjs +1 -1
- package/cli/main.mjs +82 -8
- package/cli/mobile-doctor.mjs +430 -0
- package/cli/mobile.mjs +315 -0
- package/cli/native-sections.mjs +168 -0
- package/cli/sections.mjs +186 -0
- package/cli/themes.mjs +52 -8
- package/docs/mobile.md +527 -0
- package/docs/sections.md +203 -0
- package/docs/theme-authoring.md +1 -1
- package/integration.mjs +55 -1
- package/mobile-template/client/AGENTS.md +97 -0
- package/mobile-template/client/README.md +65 -0
- package/mobile-template/client/app/_layout.tsx +56 -0
- package/mobile-template/client/app/index.tsx +21 -0
- package/mobile-template/client/app.json +21 -0
- package/mobile-template/client/components/LanguageSwitch.tsx +31 -0
- package/mobile-template/client/eas.json +21 -0
- package/mobile-template/client/env.example +3 -0
- package/mobile-template/client/gitignore +42 -0
- package/mobile-template/client/package.json +38 -0
- package/mobile-template/client/sections/index.ts +11 -0
- package/mobile-template/client/tsconfig.json +7 -0
- package/mobile-template/editor/AGENTS.md +66 -0
- package/mobile-template/editor/README.md +73 -0
- package/mobile-template/editor/app/_layout.tsx +76 -0
- package/mobile-template/editor/app/auth.tsx +13 -0
- package/mobile-template/editor/app/index.tsx +55 -0
- package/mobile-template/editor/app.json +21 -0
- package/mobile-template/editor/components/Screen.tsx +51 -0
- package/mobile-template/editor/components/SignIn.tsx +36 -0
- package/mobile-template/editor/components/TextTabs.tsx +27 -0
- package/mobile-template/editor/eas.json +21 -0
- package/mobile-template/editor/env.example +5 -0
- package/mobile-template/editor/gitignore +42 -0
- package/mobile-template/editor/lib/appearance.tsx +12 -0
- package/mobile-template/editor/lib/config.ts +17 -0
- package/mobile-template/editor/lib/format.ts +24 -0
- package/mobile-template/editor/lib/tools.ts +16 -0
- package/mobile-template/editor/package.json +40 -0
- package/mobile-template/editor/tools/README.md +2 -0
- package/mobile-template/editor/tsconfig.json +7 -0
- package/mobile-template/recipes/booking/files/app/book/[resource].tsx +57 -0
- package/mobile-template/recipes/booking/files/app/book/details.tsx +105 -0
- package/mobile-template/recipes/booking/files/app/book/index.tsx +38 -0
- package/mobile-template/recipes/booking/recipe.json +17 -0
- package/mobile-template/recipes/editor-bookings/files/app/bookings/index.tsx +113 -0
- package/mobile-template/recipes/editor-bookings/files/tools/bookings.ts +1 -0
- package/mobile-template/recipes/editor-bookings/recipe.json +8 -0
- package/mobile-template/recipes/editor-content/files/app/content/[kind]/[id].tsx +13 -0
- package/mobile-template/recipes/editor-content/files/app/content/index.tsx +76 -0
- package/mobile-template/recipes/editor-content/files/components/DocumentDetail.tsx +55 -0
- package/mobile-template/recipes/editor-content/files/tools/content.ts +1 -0
- package/mobile-template/recipes/editor-content/recipe.json +8 -0
- package/mobile-template/recipes/editor-media/files/app/media/index.tsx +73 -0
- package/mobile-template/recipes/editor-media/files/tools/media.ts +1 -0
- package/mobile-template/recipes/editor-media/recipe.json +8 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/[pageId]/[sectionId].tsx +12 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/index.tsx +48 -0
- package/mobile-template/recipes/editor-sections/files/components/SectionEditorPane.tsx +77 -0
- package/mobile-template/recipes/editor-sections/files/tools/sections.ts +1 -0
- package/mobile-template/recipes/editor-sections/recipe.json +8 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/index.tsx +63 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/review.tsx +14 -0
- package/mobile-template/recipes/editor-translations/files/components/ReviewPane.tsx +72 -0
- package/mobile-template/recipes/editor-translations/files/tools/translations.ts +1 -0
- package/mobile-template/recipes/editor-translations/recipe.json +8 -0
- package/mobile-template/recipes/editor-writer/files/app/write/[kind]/[id].tsx +132 -0
- package/mobile-template/recipes/editor-writer/files/app/write/new.tsx +61 -0
- package/mobile-template/recipes/editor-writer/files/tools/writer.ts +1 -0
- package/mobile-template/recipes/editor-writer/recipe.json +8 -0
- package/mobile-template/recipes/events/files/app/events/[slug].tsx +41 -0
- package/mobile-template/recipes/events/files/app/events/index.tsx +50 -0
- package/mobile-template/recipes/events/files/lib/addToCalendar.ts +27 -0
- package/mobile-template/recipes/events/recipe.json +15 -0
- package/mobile-template/recipes/news/files/app/news/[slug].tsx +25 -0
- package/mobile-template/recipes/news/files/app/news/index.tsx +50 -0
- package/mobile-template/recipes/news/recipe.json +13 -0
- package/mobile-template/recipes/push/files/app/notifications.tsx +64 -0
- package/mobile-template/recipes/push/files/components/PushSetup.tsx +15 -0
- package/mobile-template/recipes/push/files/lib/push.ts +15 -0
- package/mobile-template/recipes/push/recipe.json +20 -0
- package/package.json +4 -2
- package/section-library/README.md +16 -0
- package/section-library/contact/Contact.astro +49 -0
- package/section-library/contact/section.json +106 -0
- package/section-library/cta-band/CtaBand.astro +28 -0
- package/section-library/cta-band/section.json +51 -0
- package/section-library/feature-grid/FeatureGrid.astro +54 -0
- package/section-library/feature-grid/section.json +107 -0
- package/section-library/gallery/Gallery.astro +29 -0
- package/section-library/gallery/section.json +51 -0
- package/section-library/hero-banner/HeroBanner.astro +42 -0
- package/section-library/hero-banner/section.json +81 -0
- package/section-library/latest-posts/LatestPosts.astro +22 -0
- package/section-library/latest-posts/section.json +37 -0
- package/section-library/pricing/Pricing.astro +40 -0
- package/section-library/pricing/section.json +117 -0
- package/section-library/questions/Questions.astro +38 -0
- package/section-library/questions/section.json +69 -0
- package/section-library/service-times/ServiceTimes.astro +42 -0
- package/section-library/service-times/section.json +127 -0
- package/section-library/team/Team.astro +45 -0
- package/section-library/team/section.json +88 -0
- package/section-library/upcoming-events/UpcomingEvents.astro +54 -0
- package/section-library/upcoming-events/section.json +51 -0
- package/src/actions/index.ts +19 -171
- package/src/components/AiSettings.tsx +45 -5
- package/src/components/DesignManager.tsx +112 -16
- package/src/components/MobileApp.tsx +392 -0
- package/src/components/ThemeSections.astro +40 -21
- package/src/components/ThemeSectionsEditor.tsx +757 -229
- package/src/components/editor/PostEditor.tsx +174 -10
- package/src/components/import/ImportExport.tsx +4 -1
- package/src/components/import/runner.ts +1 -0
- package/src/components/sections/SectionFields.tsx +560 -0
- package/src/components/sections/SectionPicker.tsx +120 -0
- package/src/components/sections/TranslationPanel.tsx +143 -0
- package/src/components/sections/checks.ts +64 -0
- package/src/layouts/Admin.astro +6 -1
- package/src/layouts/Site.astro +107 -19
- package/src/lib/agent-discovery.ts +11 -7
- package/src/lib/agent.ts +35 -2
- package/src/lib/ai-schema.ts +5 -0
- package/src/lib/ai.ts +5 -0
- package/src/lib/api.ts +12 -0
- package/src/lib/content-service.ts +198 -0
- package/src/lib/design-service.ts +153 -5
- package/src/lib/drafts.ts +3 -3
- package/src/lib/editor/auth.ts +264 -0
- package/src/lib/editor/bookings.ts +60 -0
- package/src/lib/editor/config.ts +25 -0
- package/src/lib/editor/documents.ts +227 -0
- package/src/lib/editor/http.ts +48 -0
- package/src/lib/editor/me.ts +30 -0
- package/src/lib/editor/migration.ts +57 -0
- package/src/lib/editor/next.ts +11 -0
- package/src/lib/editor/openapi.ts +124 -0
- package/src/lib/editor/schemas.ts +91 -0
- package/src/lib/editor/sections.ts +96 -0
- package/src/lib/editor/store.ts +184 -0
- package/src/lib/editor/token-route.ts +74 -0
- package/src/lib/editor/translations.ts +163 -0
- package/src/lib/email/accounts.ts +2 -1
- package/src/lib/email/types.ts +2 -1
- package/src/lib/home-source.ts +34 -0
- package/src/lib/i18n.ts +34 -0
- package/src/lib/import/apply.ts +31 -2
- package/src/lib/import/bundle.ts +4 -0
- package/src/lib/import/export.ts +1 -0
- package/src/lib/import/ghost.ts +1 -0
- package/src/lib/media/upload.ts +83 -0
- package/src/lib/mobile/associations.ts +25 -0
- package/src/lib/mobile/calendar.ts +19 -11
- package/src/lib/mobile/config.mjs +59 -0
- package/src/lib/mobile/content.ts +18 -13
- package/src/lib/mobile/embed.ts +83 -0
- package/src/lib/mobile/http.ts +8 -6
- package/src/lib/mobile/locale.ts +18 -0
- package/src/lib/mobile/openapi.ts +111 -0
- package/src/lib/mobile/pages.ts +71 -0
- package/src/lib/mobile/route.ts +3 -3
- package/src/lib/mobile/schemas.ts +60 -0
- package/src/lib/mobile/site.ts +41 -0
- package/src/lib/push/api.ts +158 -0
- package/src/lib/push/config.ts +20 -0
- package/src/lib/push/expo.ts +76 -0
- package/src/lib/push/migration.ts +60 -0
- package/src/lib/push/service.ts +302 -0
- package/src/lib/push/store.ts +144 -0
- package/src/lib/push/types.ts +43 -0
- package/src/lib/repo/migrations.ts +11 -0
- package/src/lib/repo/sql.ts +40 -7
- package/src/lib/repo/types.ts +9 -3
- package/src/lib/roles.ts +5 -0
- package/src/lib/scheduler.ts +3 -0
- package/src/lib/schema.ts +27 -1
- package/src/lib/section-ai.ts +55 -0
- package/src/lib/section-targets.ts +62 -0
- package/src/lib/sections-agent.ts +146 -0
- package/src/lib/sections.ts +217 -0
- package/src/lib/theme.ts +16 -1
- package/src/lib/translate.ts +97 -0
- package/src/lib/translations.ts +39 -0
- package/src/middleware.ts +32 -0
- package/src/pages/.well-known/apple-app-site-association.ts +6 -0
- package/src/pages/.well-known/assetlinks.json.ts +6 -0
- package/src/pages/[...slug].astro +8 -3
- package/src/pages/admin/app-auth.astro +159 -0
- package/src/pages/admin/c/[collection]/[id].astro +9 -0
- package/src/pages/admin/calendar/events/[id].astro +9 -0
- package/src/pages/admin/login.astro +6 -2
- package/src/pages/admin/logout.ts +5 -2
- package/src/pages/admin/mobile.astro +18 -0
- package/src/pages/admin/pages/[id].astro +62 -0
- package/src/pages/admin/section-preview.astro +61 -0
- package/src/pages/admin/users.astro +107 -11
- package/src/pages/api/agent/docs.ts +6 -2
- package/src/pages/api/agent/index.ts +1 -1
- package/src/pages/api/ai/settings.ts +7 -4
- package/src/pages/api/design/ai.ts +15 -0
- package/src/pages/api/design/index.ts +12 -42
- package/src/pages/api/design/translate.ts +15 -0
- package/src/pages/api/live/thumb.ts +1 -1
- package/src/pages/api/mcp.ts +2 -2
- package/src/pages/api/media/upload.ts +10 -78
- package/src/pages/api/push/admin.ts +27 -0
- package/src/pages/api/v1/admin/bookings/[id]/[action].ts +14 -0
- package/src/pages/api/v1/admin/bookings/index.ts +6 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/[action].ts +17 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/index.ts +20 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts +26 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts +18 -0
- package/src/pages/api/v1/admin/documents/index.ts +13 -0
- package/src/pages/api/v1/admin/me.ts +6 -0
- package/src/pages/api/v1/admin/media/index.ts +32 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/publish.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts +22 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/index.ts +6 -0
- package/src/pages/api/v1/auth/revoke.ts +5 -0
- package/src/pages/api/v1/auth/token.ts +5 -0
- package/src/pages/api/v1/devices/[token].ts +20 -0
- package/src/pages/api/v1/devices/index.ts +17 -0
- package/src/pages/api/v1/openapi.json.ts +7 -0
- package/src/pages/api/v1/pages/[...slug].ts +7 -0
- package/src/pages/api/v1/pages/home.ts +7 -0
- package/src/pages/api/v1/push/topics.ts +7 -0
- package/src/pages/api/v1/site.ts +7 -0
- package/src/pages/api/v1/theme.ts +7 -0
- package/src/pages/blog/[slug].astro +8 -2
- package/src/pages/blog/index.astro +11 -4
- package/src/pages/embed/section/[...path].astro +42 -0
- package/src/pages/events/[slug].astro +7 -2
- package/src/pages/index.astro +11 -5
- package/src/pages/sitemap.xml.ts +8 -0
- package/src/styles/admin.css +69 -0
- package/src/styles/editor.css +21 -0
- package/src/styles/mobile-app.css +151 -0
- package/src/styles/sections.css +7 -1
- package/src/styles/theme-editor.css +255 -0
- package/src/theme-sdk/contract.d.mts +2193 -89
- package/src/theme-sdk/contract.mjs +77 -20
- package/src/theme-sdk/fields.d.mts +88 -0
- package/src/theme-sdk/fields.mjs +326 -0
- package/src/theme-sdk/index.ts +10 -2
- package/src/theme-sdk/tokens.d.mts +11 -0
- package/src/virtual.d.ts +1 -0
|
@@ -7,7 +7,8 @@ import Highlight from '@tiptap/extension-highlight';
|
|
|
7
7
|
import { TableKit } from '@tiptap/extension-table';
|
|
8
8
|
import { Placeholder, CharacterCount } from '@tiptap/extensions';
|
|
9
9
|
import { actions } from 'astro:actions';
|
|
10
|
-
import type { CalEvent, Page, Post, Status } from '../../lib/schema';
|
|
10
|
+
import type { CalEvent, DocTranslations, Page, Post, Status } from '../../lib/schema';
|
|
11
|
+
import { textHash } from '../../theme-sdk/fields.mjs';
|
|
11
12
|
import { ImageCard, EmbedCard, ButtonCard, Callout, HtmlCard, detectEmbed } from './extensions/cards';
|
|
12
13
|
import { FileCard, GalleryCard, BookmarkCard } from './extensions/mediaCards';
|
|
13
14
|
import { CodeBlock } from './extensions/codeBlock';
|
|
@@ -62,6 +63,10 @@ interface Props {
|
|
|
62
63
|
templates?: string[];
|
|
63
64
|
// public identity for the Search / Social preview tabs
|
|
64
65
|
site?: { title: string; url: string };
|
|
66
|
+
// extra site languages (acms({ i18n })) and whether AI drafts are available
|
|
67
|
+
languages?: { code: string; name: string }[];
|
|
68
|
+
mainLanguage?: { code: string; name: string };
|
|
69
|
+
translationDrafts?: boolean;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
interface Meta extends EventMeta {
|
|
@@ -136,13 +141,17 @@ function joinBrokenTableRows(text: string): string {
|
|
|
136
141
|
return out.join('\n');
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
type TrField = 'title' | 'excerpt' | 'bodyHtml' | 'seoTitle' | 'seoDesc' | 'locationName' | 'ctaLabel';
|
|
145
|
+
const emptyTr = () => ({ title: '', excerpt: '', bodyHtml: '', seoTitle: '', seoDesc: '', slug: '', locationName: '', ctaLabel: '', meta: {} as Record<string, { state: 'ai' | 'reviewed'; src: string }> });
|
|
146
|
+
const TR_STATE: Record<string, string> = { missing: 'not translated', ai: 'AI draft', reviewed: 'reviewed', outdated: 'English changed' };
|
|
147
|
+
|
|
139
148
|
const NO_EVENT: EventMeta = {
|
|
140
149
|
start: '', end: '', allDay: false, timezone: '', rrule: '', exdates: [], cancelled: false,
|
|
141
150
|
location: { name: '', address: '', url: '' }, cta: { label: '', href: '', style: 'primary' }, calendarIds: [], fields: {},
|
|
142
151
|
rsvp: { enabled: false, capacity: 0, autoConfirm: true, maxSeats: 4 },
|
|
143
152
|
};
|
|
144
153
|
|
|
145
|
-
export default function PostEditor({ kind = 'post', post, page, event, calendars = [], eventFields = [], siteTimezone = '', initialHTML, unsplashEnabled, previewPath, sharePath, livePath: livePathProp, templates = [], site = { title: '', url: '' } }: Props) {
|
|
154
|
+
export default function PostEditor({ kind = 'post', post, page, event, calendars = [], eventFields = [], siteTimezone = '', initialHTML, unsplashEnabled, previewPath, sharePath, livePath: livePathProp, templates = [], site = { title: '', url: '' }, languages = [], mainLanguage, translationDrafts = false }: Props) {
|
|
146
155
|
const doc = (kind === 'page' ? page : kind === 'event' ? event : post)!;
|
|
147
156
|
// posts and events share the feature image, excerpt, tags and "featured"
|
|
148
157
|
const postLike = kind !== 'page';
|
|
@@ -230,6 +239,21 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
230
239
|
};
|
|
231
240
|
// excerpt line under the title: shown when it has text or was asked for
|
|
232
241
|
const [excerptOpen, setExcerptOpen] = useState(false);
|
|
242
|
+
|
|
243
|
+
// ---- translations: the same document's words in another language ----
|
|
244
|
+
// Spanish (etc.) lives in `translations[lang]`; switching language swaps
|
|
245
|
+
// the editor body, the rest (dates, cover, tags…) stays shared.
|
|
246
|
+
const [trs, setTrs] = useState<DocTranslations>(() => ((doc as { translations?: DocTranslations }).translations ?? {}) as DocTranslations);
|
|
247
|
+
const trsRef = useRef(trs);
|
|
248
|
+
trsRef.current = trs;
|
|
249
|
+
const [lang, setLangState] = useState('');
|
|
250
|
+
const langRef = useRef('');
|
|
251
|
+
const mainBody = useRef<{ html: string; json: unknown } | null>(null);
|
|
252
|
+
const trBodyDirty = useRef(false);
|
|
253
|
+
const [trBusy, setTrBusy] = useState(false);
|
|
254
|
+
const [trMsg, setTrMsg] = useState('');
|
|
255
|
+
const mainLangName = mainLanguage?.name || 'English';
|
|
256
|
+
const mainLangShort = (mainLanguage?.code || 'en').toUpperCase();
|
|
233
257
|
// sidebar: reveal the date input for drafts that have no date yet
|
|
234
258
|
const [dateOpen, setDateOpen] = useState(false);
|
|
235
259
|
// which Preview tab opens (the sidebar's search card jumps to Search)
|
|
@@ -322,7 +346,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
322
346
|
return false;
|
|
323
347
|
},
|
|
324
348
|
},
|
|
325
|
-
onUpdate: () => queueSave(),
|
|
349
|
+
onUpdate: () => { if (langRef.current) trBodyDirty.current = true; queueSave(); },
|
|
326
350
|
onSelectionUpdate: ({ editor: ed }) => {
|
|
327
351
|
// the top-level block holding the caret; the live pane scrolls to it.
|
|
328
352
|
// Debounced and split-only so typing doesn't re-render the editor.
|
|
@@ -344,8 +368,18 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
344
368
|
if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
|
|
345
369
|
const m = { ...metaRef.current, ...overrides };
|
|
346
370
|
setSave('saving');
|
|
347
|
-
|
|
348
|
-
|
|
371
|
+
// in another language the editor holds that language's body: store it as
|
|
372
|
+
// the translation and keep sending the main-language body unchanged
|
|
373
|
+
if (langRef.current && trBodyDirty.current) {
|
|
374
|
+
trBodyDirty.current = false;
|
|
375
|
+
const l = langRef.current;
|
|
376
|
+
const cur = trsRef.current[l] ?? emptyTr();
|
|
377
|
+
trsRef.current = { ...trsRef.current, [l]: { ...cur, bodyHtml: ed.isEmpty ? '' : ed.getHTML(), meta: { ...cur.meta, bodyHtml: { state: 'reviewed', src: textHash(mainBody.current?.html ?? '') } } } };
|
|
378
|
+
setTrs(trsRef.current);
|
|
379
|
+
}
|
|
380
|
+
const bodyJson = langRef.current && mainBody.current ? JSON.stringify(mainBody.current.json) : JSON.stringify(ed.getJSON());
|
|
381
|
+
const bodyHtml = langRef.current && mainBody.current ? mainBody.current.html : ed.getHTML();
|
|
382
|
+
const translations = languages.length ? trsRef.current : undefined;
|
|
349
383
|
if (kind === 'event') {
|
|
350
384
|
const r = await actions.events.save({
|
|
351
385
|
id: docId,
|
|
@@ -375,6 +409,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
375
409
|
fields: m.fields,
|
|
376
410
|
rsvp: m.rsvp,
|
|
377
411
|
scheduledAt: m.scheduledAt,
|
|
412
|
+
translations,
|
|
378
413
|
});
|
|
379
414
|
if (r.error) {
|
|
380
415
|
setSave('error');
|
|
@@ -403,6 +438,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
403
438
|
status: m.status,
|
|
404
439
|
isHome: m.isHome,
|
|
405
440
|
locale: m.locale,
|
|
441
|
+
translations,
|
|
406
442
|
});
|
|
407
443
|
if (r.error) {
|
|
408
444
|
setSave('error');
|
|
@@ -437,6 +473,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
437
473
|
template: m.template,
|
|
438
474
|
publishedAt: m.publishedAt,
|
|
439
475
|
scheduledAt: m.scheduledAt,
|
|
476
|
+
translations,
|
|
440
477
|
});
|
|
441
478
|
if (r.error) {
|
|
442
479
|
setSave('error');
|
|
@@ -463,6 +500,92 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
463
500
|
saveTimer.current = setTimeout(() => save(), 1200);
|
|
464
501
|
}, [save]);
|
|
465
502
|
|
|
503
|
+
// the main-language text a translated field is made from
|
|
504
|
+
const mainValue = (f: TrField): string => {
|
|
505
|
+
const m = metaRef.current;
|
|
506
|
+
switch (f) {
|
|
507
|
+
case 'title': return m.title;
|
|
508
|
+
case 'excerpt': return postLike ? m.excerpt : '';
|
|
509
|
+
case 'seoTitle': return m.seoTitle;
|
|
510
|
+
case 'seoDesc': return m.seoDesc;
|
|
511
|
+
case 'locationName': return kind === 'event' ? m.location.name : '';
|
|
512
|
+
case 'ctaLabel': return kind === 'event' ? m.cta.label : '';
|
|
513
|
+
default: return langRef.current && mainBody.current ? mainBody.current.html : (editorRef.current?.isEmpty ? '' : editorRef.current?.getHTML() ?? '');
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const trFields = (): TrField[] => ['title', ...(postLike ? ['excerpt' as const] : []), 'bodyHtml', 'seoTitle', 'seoDesc', ...(kind === 'event' ? ['locationName' as const, 'ctaLabel' as const] : [])];
|
|
517
|
+
const trStatus = (l: string, f: TrField) => {
|
|
518
|
+
const src = mainValue(f);
|
|
519
|
+
if (!src.trim()) return 'n/a';
|
|
520
|
+
const t = trsRef.current[l];
|
|
521
|
+
const v = t?.[f] ?? '';
|
|
522
|
+
if (!String(v).trim()) return 'missing';
|
|
523
|
+
const meta = t?.meta?.[f];
|
|
524
|
+
if (meta && meta.src !== textHash(src)) return 'outdated';
|
|
525
|
+
return meta?.state === 'reviewed' ? 'reviewed' : 'ai';
|
|
526
|
+
};
|
|
527
|
+
const setTr = (f: TrField, value: string, state: 'ai' | 'reviewed' = 'reviewed') => {
|
|
528
|
+
const l = langRef.current;
|
|
529
|
+
if (!l) return;
|
|
530
|
+
const cur = trsRef.current[l] ?? emptyTr();
|
|
531
|
+
trsRef.current = { ...trsRef.current, [l]: { ...cur, [f]: value, meta: { ...cur.meta, [f]: { state, src: textHash(mainValue(f)) } } } };
|
|
532
|
+
setTrs(trsRef.current);
|
|
533
|
+
queueSave();
|
|
534
|
+
};
|
|
535
|
+
// switch the editor between the main language and a translation
|
|
536
|
+
const switchLang = async (next: string) => {
|
|
537
|
+
const ed = editorRef.current;
|
|
538
|
+
if (!ed || next === langRef.current) return;
|
|
539
|
+
await save();
|
|
540
|
+
if (!langRef.current) mainBody.current = { html: ed.getHTML(), json: ed.getJSON() };
|
|
541
|
+
const html = next ? (trsRef.current[next]?.bodyHtml ?? '') : (mainBody.current?.html ?? '');
|
|
542
|
+
langRef.current = next;
|
|
543
|
+
setLangState(next);
|
|
544
|
+
trBodyDirty.current = false;
|
|
545
|
+
ed.commands.setContent(html, { emitUpdate: false });
|
|
546
|
+
if (!next) mainBody.current = null;
|
|
547
|
+
setTrMsg('');
|
|
548
|
+
};
|
|
549
|
+
// machine drafts for every field still missing or with changed English
|
|
550
|
+
const translateAll = async (includeReviewed = false) => {
|
|
551
|
+
const l = langRef.current;
|
|
552
|
+
if (!l) return;
|
|
553
|
+
const fields = trFields().filter((f) => { const st = trStatus(l, f); return st === 'missing' || (st === 'outdated' && (includeReviewed || trsRef.current[l]?.meta?.[f]?.state !== 'reviewed')) || (includeReviewed && st === 'ai'); });
|
|
554
|
+
if (!fields.length) { setTrMsg('Everything is already translated.'); return; }
|
|
555
|
+
setTrBusy(true); setTrMsg('');
|
|
556
|
+
try {
|
|
557
|
+
const res = await fetch('/api/design/translate', {
|
|
558
|
+
method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
559
|
+
body: JSON.stringify({ locale: l, units: fields.map((f) => ({ id: f, text: mainValue(f), kind: f === 'bodyHtml' ? 'richtext' : f === 'excerpt' || f === 'seoDesc' ? 'textarea' : 'text', label: f })) }),
|
|
560
|
+
});
|
|
561
|
+
const body = await res.json().catch(() => ({})) as { results?: Record<string, string>; error?: string };
|
|
562
|
+
if (!res.ok) { setTrMsg(body.error || `Translation failed (${res.status}).`); return; }
|
|
563
|
+
const results = body.results ?? {};
|
|
564
|
+
const cur = trsRef.current[l] ?? emptyTr();
|
|
565
|
+
const next = { ...cur, meta: { ...cur.meta } } as typeof cur;
|
|
566
|
+
for (const f of fields) {
|
|
567
|
+
if (results[f] === undefined) continue;
|
|
568
|
+
(next as Record<string, unknown>)[f] = results[f];
|
|
569
|
+
next.meta[f] = { state: 'ai', src: textHash(mainValue(f)) };
|
|
570
|
+
}
|
|
571
|
+
trsRef.current = { ...trsRef.current, [l]: next };
|
|
572
|
+
setTrs(trsRef.current);
|
|
573
|
+
if (results.bodyHtml !== undefined && langRef.current === l) { editorRef.current?.commands.setContent(results.bodyHtml, { emitUpdate: false }); trBodyDirty.current = false; }
|
|
574
|
+
setTrMsg(`Translated ${Object.keys(results).length} ${Object.keys(results).length === 1 ? 'part' : 'parts'} as AI drafts. Check them, then mark them right.`);
|
|
575
|
+
queueSave();
|
|
576
|
+
} catch { setTrMsg('Couldn’t reach the server.'); } finally { setTrBusy(false); }
|
|
577
|
+
};
|
|
578
|
+
const confirmAll = () => {
|
|
579
|
+
const l = langRef.current;
|
|
580
|
+
if (!l) return;
|
|
581
|
+
const cur = trsRef.current[l] ?? emptyTr();
|
|
582
|
+
const meta = { ...cur.meta };
|
|
583
|
+
for (const f of trFields()) if (String(cur[f] ?? '').trim() && mainValue(f).trim()) meta[f] = { state: 'reviewed', src: textHash(mainValue(f)) };
|
|
584
|
+
trsRef.current = { ...trsRef.current, [l]: { ...cur, meta } };
|
|
585
|
+
setTrs(trsRef.current);
|
|
586
|
+
queueSave();
|
|
587
|
+
};
|
|
588
|
+
|
|
466
589
|
const setMeta = useCallback((patch: Partial<Meta>) => {
|
|
467
590
|
setMetaState((prev) => {
|
|
468
591
|
const next = { ...prev, ...patch };
|
|
@@ -808,6 +931,17 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
808
931
|
Preview
|
|
809
932
|
</button>
|
|
810
933
|
</div>
|
|
934
|
+
{languages.length > 0 && (
|
|
935
|
+
<div className="pe-seg pe-langs" role="radiogroup" aria-label="Language">
|
|
936
|
+
<button type="button" role="radio" aria-checked={!lang} onClick={() => void switchLang('')}>{mainLangShort}</button>
|
|
937
|
+
{languages.map((l) => (
|
|
938
|
+
<button key={l.code} type="button" role="radio" aria-checked={lang === l.code} title={`${l.name} version`} onClick={() => void switchLang(l.code)}>
|
|
939
|
+
{l.code.toUpperCase()}
|
|
940
|
+
{trFields().some((f) => ['missing', 'outdated'].includes(trStatus(l.code, f))) && <i className="pe-lang-dot" aria-label="needs translation" />}
|
|
941
|
+
</button>
|
|
942
|
+
))}
|
|
943
|
+
</div>
|
|
944
|
+
)}
|
|
811
945
|
<div className="pe-topbar-right">
|
|
812
946
|
<div className="pe-publish-wrap">
|
|
813
947
|
<button type="button" className="pe-btn primary sm" onClick={() => setPublishOpen((v) => !v)}>
|
|
@@ -973,7 +1107,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
973
1107
|
Feature image
|
|
974
1108
|
</button>
|
|
975
1109
|
)}
|
|
976
|
-
{!meta.excerpt && !excerptOpen && (
|
|
1110
|
+
{!meta.excerpt && !excerptOpen && !lang && (
|
|
977
1111
|
<button type="button" title="A short teaser under the title, on post cards and in link previews" onClick={() => setExcerptOpen(true)}>
|
|
978
1112
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M4 7h16M4 12h10M4 17h7" /></svg>
|
|
979
1113
|
Excerpt
|
|
@@ -982,6 +1116,36 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
982
1116
|
</div>
|
|
983
1117
|
)}
|
|
984
1118
|
|
|
1119
|
+
{lang && (() => {
|
|
1120
|
+
const name = languages.find((l) => l.code === lang)?.name ?? lang;
|
|
1121
|
+
const statuses = trFields().map((f) => [f, trStatus(lang, f)] as const).filter(([, st]) => st !== 'n/a');
|
|
1122
|
+
const label: Record<TrField, string> = { title: 'Title', excerpt: 'Excerpt', bodyHtml: 'Body', seoTitle: 'Search title', seoDesc: 'Search description', locationName: 'Place name', ctaLabel: 'Button text' };
|
|
1123
|
+
const t = trs[lang] ?? emptyTr();
|
|
1124
|
+
return (
|
|
1125
|
+
<section className="pe-trpanel" aria-label={`${name} version`}>
|
|
1126
|
+
<div className="pe-tr-head">
|
|
1127
|
+
<b>{name} version</b>
|
|
1128
|
+
<span className="pe-tr-states">{statuses.map(([f, st]) => <span key={f} className={`is-${st}`}>{label[f]}: {TR_STATE[st]}</span>)}</span>
|
|
1129
|
+
</div>
|
|
1130
|
+
<p className="pe-tr-note">You're editing the {name} words. Dates, photos, tags and links are shared with {mainLangName}. Anything left empty shows in {mainLangName}.</p>
|
|
1131
|
+
<div className="pe-tr-fields">
|
|
1132
|
+
{postLike && <label>Excerpt<textarea rows={2} value={t.excerpt} placeholder={meta.excerpt} onChange={(e) => setTr('excerpt', e.target.value)} /></label>}
|
|
1133
|
+
<label>Search title<input value={t.seoTitle} placeholder={meta.seoTitle || meta.title} onChange={(e) => setTr('seoTitle', e.target.value)} /></label>
|
|
1134
|
+
<label>Search description<textarea rows={2} value={t.seoDesc} placeholder={meta.seoDesc} onChange={(e) => setTr('seoDesc', e.target.value)} /></label>
|
|
1135
|
+
{kind === 'event' && <label>Place name<input value={t.locationName} placeholder={meta.location.name} onChange={(e) => setTr('locationName', e.target.value)} /></label>}
|
|
1136
|
+
{kind === 'event' && <label>Button text<input value={t.ctaLabel} placeholder={meta.cta.label} onChange={(e) => setTr('ctaLabel', e.target.value)} /></label>}
|
|
1137
|
+
</div>
|
|
1138
|
+
<div className="pe-tr-actions">
|
|
1139
|
+
{translationDrafts && <button type="button" className="pe-btn sm" disabled={trBusy} onClick={() => void translateAll()}>{trBusy ? 'Translating…' : `Translate the rest with AI`}</button>}
|
|
1140
|
+
{statuses.some(([, st]) => st === 'ai' || st === 'outdated') && <button type="button" className="pe-btn ghost sm" onClick={confirmAll}>✓ Looks right</button>}
|
|
1141
|
+
{!translationDrafts && <span className="pe-tr-msg">Turn on AI in Settings → AI & agents and the {name} can draft itself.</span>}
|
|
1142
|
+
<button type="button" className="pe-btn ghost sm" onClick={() => void switchLang('')}>Back to {mainLangName}</button>
|
|
1143
|
+
{trMsg && <span className="pe-tr-msg" role="status">{trMsg}</span>}
|
|
1144
|
+
</div>
|
|
1145
|
+
</section>
|
|
1146
|
+
);
|
|
1147
|
+
})()}
|
|
1148
|
+
|
|
985
1149
|
<textarea
|
|
986
1150
|
className="pe-title"
|
|
987
1151
|
rows={1}
|
|
@@ -989,19 +1153,19 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
|
|
|
989
1153
|
// grow to fit on mount and on narrow screens, not just while typing
|
|
990
1154
|
if (el) { el.style.height = 'auto'; el.style.height = `${el.scrollHeight}px`; }
|
|
991
1155
|
}}
|
|
992
|
-
placeholder={kind === 'page' ? 'Page title' : `${col.singular} title`}
|
|
993
|
-
value={meta.title}
|
|
1156
|
+
placeholder={lang ? meta.title || 'Title' : kind === 'page' ? 'Page title' : `${col.singular} title`}
|
|
1157
|
+
value={lang ? (trs[lang]?.title ?? '') : meta.title}
|
|
994
1158
|
onChange={(e) => {
|
|
995
1159
|
e.target.style.height = 'auto';
|
|
996
1160
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
|
997
|
-
setMeta({ title: e.target.value });
|
|
1161
|
+
if (langRef.current) setTr('title', e.target.value); else setMeta({ title: e.target.value });
|
|
998
1162
|
}}
|
|
999
1163
|
onKeyDown={(e) => {
|
|
1000
1164
|
if (e.key === 'Enter') { e.preventDefault(); editor?.commands.focus('start'); }
|
|
1001
1165
|
}}
|
|
1002
1166
|
/>
|
|
1003
1167
|
|
|
1004
|
-
{postLike && (meta.excerpt || excerptOpen) && (
|
|
1168
|
+
{postLike && !lang && (meta.excerpt || excerptOpen) && (
|
|
1005
1169
|
<textarea
|
|
1006
1170
|
className="pe-excerpt"
|
|
1007
1171
|
rows={1}
|
|
@@ -13,7 +13,7 @@ interface Plan {
|
|
|
13
13
|
}
|
|
14
14
|
interface Run { id: string; source: string; label: string; status: 'running' | 'done' | 'undone'; report: Report; createdAt: number }
|
|
15
15
|
interface Report {
|
|
16
|
-
counts?: { posts: { created: number; updated: number }; pages: { created: number; updated: number }; users: number; tags: number; media: number; redirects: number; nav: number };
|
|
16
|
+
counts?: { posts: { created: number; updated: number }; pages: { created: number; updated: number }; users: number; tags: number; media: number; redirects: number; nav: number; layouts?: number };
|
|
17
17
|
settings?: string[];
|
|
18
18
|
warnings?: { item: string; code: string; message: string }[];
|
|
19
19
|
failedAssets?: { url: string; error: string }[];
|
|
@@ -336,6 +336,9 @@ function Done({ report, reset, openHistory }: { report: Report; reset: () => voi
|
|
|
336
336
|
{report.settings?.length ? ' · site identity updated' : ''}
|
|
337
337
|
</p>
|
|
338
338
|
)}
|
|
339
|
+
{!!c?.layouts && (
|
|
340
|
+
<p className="imp-mark ok">{plural(c.layouts, 'page layout')} staged in the design draft · <a href="/admin/design#review">Review and publish</a></p>
|
|
341
|
+
)}
|
|
339
342
|
{!!report.failedAssets?.length && (
|
|
340
343
|
<>
|
|
341
344
|
<p className="imp-mark warn">{plural(report.failedAssets.length, 'file')} couldn’t be copied and still point at the old site</p>
|
|
@@ -268,6 +268,7 @@ export async function runImport(
|
|
|
268
268
|
nav: bundle.nav.map((n) => ({ ...n, url: fix(n.url) })),
|
|
269
269
|
legacy: bundle.items.filter((i) => i.legacyPath).map((i) => ({ sourceId: i.sourceId, type: i.type, path: i.legacyPath })),
|
|
270
270
|
redirects: bundle.redirects,
|
|
271
|
+
layouts: bundle.layouts ?? [],
|
|
271
272
|
warnings: bundle.warnings,
|
|
272
273
|
failedAssets,
|
|
273
274
|
});
|