@acms-subir/core 0.3.3-next.20260929113209 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/cli/common.mjs +2 -2
  3. package/cli/legacy.mjs +1 -1
  4. package/cli/main.mjs +82 -8
  5. package/cli/mobile-doctor.mjs +430 -0
  6. package/cli/mobile.mjs +315 -0
  7. package/cli/native-sections.mjs +168 -0
  8. package/cli/preview.mjs +8 -4
  9. package/cli/sections.mjs +188 -0
  10. package/cli/themes.mjs +52 -8
  11. package/docs/mobile.md +527 -0
  12. package/docs/sections.md +203 -0
  13. package/docs/theme-authoring.md +19 -3
  14. package/integration.mjs +76 -5
  15. package/mobile-template/client/AGENTS.md +97 -0
  16. package/mobile-template/client/README.md +65 -0
  17. package/mobile-template/client/app/_layout.tsx +56 -0
  18. package/mobile-template/client/app/index.tsx +21 -0
  19. package/mobile-template/client/app.json +21 -0
  20. package/mobile-template/client/components/LanguageSwitch.tsx +31 -0
  21. package/mobile-template/client/eas.json +21 -0
  22. package/mobile-template/client/env.example +3 -0
  23. package/mobile-template/client/gitignore +42 -0
  24. package/mobile-template/client/package.json +38 -0
  25. package/mobile-template/client/sections/index.ts +11 -0
  26. package/mobile-template/client/tsconfig.json +7 -0
  27. package/mobile-template/editor/AGENTS.md +66 -0
  28. package/mobile-template/editor/README.md +73 -0
  29. package/mobile-template/editor/app/_layout.tsx +76 -0
  30. package/mobile-template/editor/app/auth.tsx +13 -0
  31. package/mobile-template/editor/app/index.tsx +55 -0
  32. package/mobile-template/editor/app.json +21 -0
  33. package/mobile-template/editor/components/Screen.tsx +51 -0
  34. package/mobile-template/editor/components/SignIn.tsx +36 -0
  35. package/mobile-template/editor/components/TextTabs.tsx +27 -0
  36. package/mobile-template/editor/eas.json +21 -0
  37. package/mobile-template/editor/env.example +5 -0
  38. package/mobile-template/editor/gitignore +42 -0
  39. package/mobile-template/editor/lib/appearance.tsx +12 -0
  40. package/mobile-template/editor/lib/config.ts +17 -0
  41. package/mobile-template/editor/lib/format.ts +24 -0
  42. package/mobile-template/editor/lib/tools.ts +16 -0
  43. package/mobile-template/editor/package.json +40 -0
  44. package/mobile-template/editor/tools/README.md +2 -0
  45. package/mobile-template/editor/tsconfig.json +7 -0
  46. package/mobile-template/recipes/booking/files/app/book/[resource].tsx +57 -0
  47. package/mobile-template/recipes/booking/files/app/book/details.tsx +105 -0
  48. package/mobile-template/recipes/booking/files/app/book/index.tsx +38 -0
  49. package/mobile-template/recipes/booking/recipe.json +17 -0
  50. package/mobile-template/recipes/editor-bookings/files/app/bookings/index.tsx +113 -0
  51. package/mobile-template/recipes/editor-bookings/files/tools/bookings.ts +1 -0
  52. package/mobile-template/recipes/editor-bookings/recipe.json +8 -0
  53. package/mobile-template/recipes/editor-content/files/app/content/[kind]/[id].tsx +13 -0
  54. package/mobile-template/recipes/editor-content/files/app/content/index.tsx +76 -0
  55. package/mobile-template/recipes/editor-content/files/components/DocumentDetail.tsx +55 -0
  56. package/mobile-template/recipes/editor-content/files/tools/content.ts +1 -0
  57. package/mobile-template/recipes/editor-content/recipe.json +8 -0
  58. package/mobile-template/recipes/editor-media/files/app/media/index.tsx +73 -0
  59. package/mobile-template/recipes/editor-media/files/tools/media.ts +1 -0
  60. package/mobile-template/recipes/editor-media/recipe.json +8 -0
  61. package/mobile-template/recipes/editor-sections/files/app/sections/[pageId]/[sectionId].tsx +12 -0
  62. package/mobile-template/recipes/editor-sections/files/app/sections/index.tsx +48 -0
  63. package/mobile-template/recipes/editor-sections/files/components/SectionEditorPane.tsx +77 -0
  64. package/mobile-template/recipes/editor-sections/files/tools/sections.ts +1 -0
  65. package/mobile-template/recipes/editor-sections/recipe.json +8 -0
  66. package/mobile-template/recipes/editor-translations/files/app/translations/index.tsx +63 -0
  67. package/mobile-template/recipes/editor-translations/files/app/translations/review.tsx +14 -0
  68. package/mobile-template/recipes/editor-translations/files/components/ReviewPane.tsx +72 -0
  69. package/mobile-template/recipes/editor-translations/files/tools/translations.ts +1 -0
  70. package/mobile-template/recipes/editor-translations/recipe.json +8 -0
  71. package/mobile-template/recipes/editor-writer/files/app/write/[kind]/[id].tsx +132 -0
  72. package/mobile-template/recipes/editor-writer/files/app/write/new.tsx +61 -0
  73. package/mobile-template/recipes/editor-writer/files/tools/writer.ts +1 -0
  74. package/mobile-template/recipes/editor-writer/recipe.json +8 -0
  75. package/mobile-template/recipes/events/files/app/events/[slug].tsx +41 -0
  76. package/mobile-template/recipes/events/files/app/events/index.tsx +50 -0
  77. package/mobile-template/recipes/events/files/lib/addToCalendar.ts +27 -0
  78. package/mobile-template/recipes/events/recipe.json +15 -0
  79. package/mobile-template/recipes/news/files/app/news/[slug].tsx +25 -0
  80. package/mobile-template/recipes/news/files/app/news/index.tsx +50 -0
  81. package/mobile-template/recipes/news/recipe.json +13 -0
  82. package/mobile-template/recipes/push/files/app/notifications.tsx +64 -0
  83. package/mobile-template/recipes/push/files/components/PushSetup.tsx +15 -0
  84. package/mobile-template/recipes/push/files/lib/push.ts +15 -0
  85. package/mobile-template/recipes/push/recipe.json +20 -0
  86. package/package.json +17 -13
  87. package/section-library/README.md +16 -0
  88. package/section-library/contact/Contact.astro +49 -0
  89. package/section-library/contact/section.json +106 -0
  90. package/section-library/cta-band/CtaBand.astro +28 -0
  91. package/section-library/cta-band/section.json +51 -0
  92. package/section-library/feature-grid/FeatureGrid.astro +54 -0
  93. package/section-library/feature-grid/section.json +107 -0
  94. package/section-library/gallery/Gallery.astro +29 -0
  95. package/section-library/gallery/section.json +51 -0
  96. package/section-library/hero-banner/HeroBanner.astro +42 -0
  97. package/section-library/hero-banner/section.json +81 -0
  98. package/section-library/latest-posts/LatestPosts.astro +22 -0
  99. package/section-library/latest-posts/section.json +37 -0
  100. package/section-library/pricing/Pricing.astro +40 -0
  101. package/section-library/pricing/section.json +117 -0
  102. package/section-library/questions/Questions.astro +38 -0
  103. package/section-library/questions/section.json +69 -0
  104. package/section-library/service-times/ServiceTimes.astro +42 -0
  105. package/section-library/service-times/section.json +127 -0
  106. package/section-library/team/Team.astro +45 -0
  107. package/section-library/team/section.json +88 -0
  108. package/section-library/upcoming-events/UpcomingEvents.astro +54 -0
  109. package/section-library/upcoming-events/section.json +51 -0
  110. package/src/actions/index.ts +205 -174
  111. package/src/components/AiSettings.tsx +45 -5
  112. package/src/components/DesignManager.tsx +112 -16
  113. package/src/components/MobileApp.tsx +392 -0
  114. package/src/components/ThemeSections.astro +40 -21
  115. package/src/components/ThemeSectionsEditor.tsx +757 -229
  116. package/src/components/booking/BookingFlow.astro +6 -3
  117. package/src/components/calendar/BookingsHead.astro +51 -0
  118. package/src/components/calendar/CalendarBoard.tsx +618 -0
  119. package/src/components/calendar/MiniMonth.tsx +54 -0
  120. package/src/components/calendar/NewEventDialog.tsx +44 -0
  121. package/src/components/calendar/QuickAdd.tsx +263 -0
  122. package/src/components/calendar/RoomHolds.tsx +203 -0
  123. package/src/components/calendar/WhereItLands.tsx +51 -0
  124. package/src/components/editor/EventPanel.tsx +23 -2
  125. package/src/components/editor/PostEditor.tsx +206 -14
  126. package/src/components/import/ImportExport.tsx +4 -1
  127. package/src/components/import/runner.ts +1 -0
  128. package/src/components/sections/SectionFields.tsx +560 -0
  129. package/src/components/sections/SectionPicker.tsx +120 -0
  130. package/src/components/sections/TranslationPanel.tsx +143 -0
  131. package/src/components/sections/checks.ts +64 -0
  132. package/src/layouts/Admin.astro +11 -7
  133. package/src/layouts/Site.astro +136 -21
  134. package/src/lib/agent-discovery.ts +11 -7
  135. package/src/lib/agent.ts +35 -2
  136. package/src/lib/ai-schema.ts +5 -0
  137. package/src/lib/ai.ts +5 -0
  138. package/src/lib/api.ts +12 -0
  139. package/src/lib/backup/content.ts +6 -1
  140. package/src/lib/booking/agent.ts +3 -2
  141. package/src/lib/booking/approval.ts +70 -0
  142. package/src/lib/booking/holds.ts +120 -0
  143. package/src/lib/booking/service.ts +37 -3
  144. package/src/lib/calendar/agent-schema.ts +9 -1
  145. package/src/lib/calendar/agent.ts +16 -1
  146. package/src/lib/calendar/board.ts +171 -0
  147. package/src/lib/calendar/config.ts +3 -0
  148. package/src/lib/calendar/grid.ts +169 -0
  149. package/src/lib/calendar/migration.ts +16 -0
  150. package/src/lib/calendar/render.ts +9 -7
  151. package/src/lib/calendar/schedule.ts +166 -0
  152. package/src/lib/content-service.ts +203 -0
  153. package/src/lib/design-service.ts +153 -5
  154. package/src/lib/drafts.ts +3 -3
  155. package/src/lib/editor/auth.ts +264 -0
  156. package/src/lib/editor/bookings.ts +60 -0
  157. package/src/lib/editor/config.ts +25 -0
  158. package/src/lib/editor/documents.ts +227 -0
  159. package/src/lib/editor/http.ts +48 -0
  160. package/src/lib/editor/me.ts +30 -0
  161. package/src/lib/editor/migration.ts +57 -0
  162. package/src/lib/editor/next.ts +11 -0
  163. package/src/lib/editor/openapi.ts +124 -0
  164. package/src/lib/editor/schemas.ts +91 -0
  165. package/src/lib/editor/sections.ts +96 -0
  166. package/src/lib/editor/store.ts +184 -0
  167. package/src/lib/editor/token-route.ts +74 -0
  168. package/src/lib/editor/translations.ts +163 -0
  169. package/src/lib/email/accounts.ts +2 -1
  170. package/src/lib/email/types.ts +2 -1
  171. package/src/lib/home-source.ts +34 -0
  172. package/src/lib/i18n.ts +34 -0
  173. package/src/lib/import/apply.ts +31 -2
  174. package/src/lib/import/bundle.ts +4 -0
  175. package/src/lib/import/export.ts +1 -0
  176. package/src/lib/import/ghost.ts +1 -0
  177. package/src/lib/media/upload.ts +83 -0
  178. package/src/lib/mobile/associations.ts +25 -0
  179. package/src/lib/mobile/calendar.ts +26 -14
  180. package/src/lib/mobile/config.mjs +59 -0
  181. package/src/lib/mobile/content.ts +18 -13
  182. package/src/lib/mobile/embed.ts +83 -0
  183. package/src/lib/mobile/http.ts +14 -6
  184. package/src/lib/mobile/locale.ts +18 -0
  185. package/src/lib/mobile/openapi.ts +111 -0
  186. package/src/lib/mobile/pages.ts +71 -0
  187. package/src/lib/mobile/route.ts +3 -3
  188. package/src/lib/mobile/schemas.ts +60 -0
  189. package/src/lib/mobile/site.ts +41 -0
  190. package/src/lib/push/api.ts +158 -0
  191. package/src/lib/push/config.ts +20 -0
  192. package/src/lib/push/expo.ts +76 -0
  193. package/src/lib/push/migration.ts +60 -0
  194. package/src/lib/push/service.ts +302 -0
  195. package/src/lib/push/store.ts +144 -0
  196. package/src/lib/push/types.ts +43 -0
  197. package/src/lib/repo/migrations.ts +14 -0
  198. package/src/lib/repo/sql.ts +65 -20
  199. package/src/lib/repo/types.ts +10 -4
  200. package/src/lib/roles.ts +5 -0
  201. package/src/lib/scheduler.ts +6 -1
  202. package/src/lib/schema.ts +76 -1
  203. package/src/lib/section-ai.ts +55 -0
  204. package/src/lib/section-targets.ts +62 -0
  205. package/src/lib/sections-agent.ts +146 -0
  206. package/src/lib/sections.ts +242 -0
  207. package/src/lib/site-chrome.ts +34 -0
  208. package/src/lib/theme-assets.ts +16 -0
  209. package/src/lib/theme.ts +33 -4
  210. package/src/lib/translate.ts +97 -0
  211. package/src/lib/translations.ts +39 -0
  212. package/src/middleware.ts +38 -2
  213. package/src/pages/.well-known/apple-app-site-association.ts +6 -0
  214. package/src/pages/.well-known/assetlinks.json.ts +6 -0
  215. package/src/pages/[...slug].astro +8 -3
  216. package/src/pages/admin/app-auth.astro +159 -0
  217. package/src/pages/admin/c/[collection]/[id].astro +9 -0
  218. package/src/pages/admin/calendar/bookings.astro +32 -26
  219. package/src/pages/admin/calendar/calendars.astro +4 -263
  220. package/src/pages/admin/calendar/events/[id].astro +9 -0
  221. package/src/pages/admin/calendar/events/index.astro +41 -83
  222. package/src/pages/admin/calendar/index.astro +43 -0
  223. package/src/pages/admin/calendar/resources/[id].astro +27 -5
  224. package/src/pages/admin/calendar/resources/index.astro +16 -17
  225. package/src/pages/admin/calendar/resources/link.astro +52 -12
  226. package/src/pages/admin/calendar/settings.astro +458 -0
  227. package/src/pages/admin/integrations.astro +5 -11
  228. package/src/pages/admin/login.astro +6 -2
  229. package/src/pages/admin/logout.ts +5 -2
  230. package/src/pages/admin/mobile.astro +18 -0
  231. package/src/pages/admin/pages/[id].astro +62 -0
  232. package/src/pages/admin/section-preview.astro +61 -0
  233. package/src/pages/admin/users.astro +107 -11
  234. package/src/pages/api/agent/docs.ts +6 -2
  235. package/src/pages/api/agent/index.ts +1 -1
  236. package/src/pages/api/ai/settings.ts +7 -4
  237. package/src/pages/api/design/ai.ts +15 -0
  238. package/src/pages/api/design/index.ts +12 -42
  239. package/src/pages/api/design/translate.ts +15 -0
  240. package/src/pages/api/live/thumb.ts +1 -1
  241. package/src/pages/api/mcp.ts +2 -2
  242. package/src/pages/api/media/upload.ts +10 -78
  243. package/src/pages/api/push/admin.ts +27 -0
  244. package/src/pages/api/v1/admin/bookings/[id]/[action].ts +14 -0
  245. package/src/pages/api/v1/admin/bookings/index.ts +6 -0
  246. package/src/pages/api/v1/admin/documents/[kind]/[id]/[action].ts +17 -0
  247. package/src/pages/api/v1/admin/documents/[kind]/[id]/index.ts +20 -0
  248. package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts +26 -0
  249. package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts +18 -0
  250. package/src/pages/api/v1/admin/documents/index.ts +13 -0
  251. package/src/pages/api/v1/admin/me.ts +6 -0
  252. package/src/pages/api/v1/admin/media/index.ts +32 -0
  253. package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/index.ts +14 -0
  254. package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/publish.ts +14 -0
  255. package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts +22 -0
  256. package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts +14 -0
  257. package/src/pages/api/v1/admin/sections/index.ts +6 -0
  258. package/src/pages/api/v1/auth/revoke.ts +5 -0
  259. package/src/pages/api/v1/auth/token.ts +5 -0
  260. package/src/pages/api/v1/devices/[token].ts +20 -0
  261. package/src/pages/api/v1/devices/index.ts +17 -0
  262. package/src/pages/api/v1/openapi.json.ts +7 -0
  263. package/src/pages/api/v1/pages/[...slug].ts +7 -0
  264. package/src/pages/api/v1/pages/home.ts +7 -0
  265. package/src/pages/api/v1/push/topics.ts +7 -0
  266. package/src/pages/api/v1/site.ts +7 -0
  267. package/src/pages/api/v1/theme.ts +7 -0
  268. package/src/pages/blog/[slug].astro +8 -2
  269. package/src/pages/blog/index.astro +11 -4
  270. package/src/pages/book/[resource].ics.ts +7 -1
  271. package/src/pages/embed/section/[...path].astro +42 -0
  272. package/src/pages/events/[slug].astro +7 -2
  273. package/src/pages/index.astro +11 -5
  274. package/src/pages/sitemap.xml.ts +8 -0
  275. package/src/styles/admin.css +117 -0
  276. package/src/styles/cards.css +5 -1
  277. package/src/styles/editor.css +57 -0
  278. package/src/styles/mobile-app.css +151 -0
  279. package/src/styles/schedule.css +325 -0
  280. package/src/styles/sections.css +21 -12
  281. package/src/styles/theme-editor.css +255 -0
  282. package/src/theme-sdk/contract.d.mts +2193 -89
  283. package/src/theme-sdk/contract.mjs +77 -20
  284. package/src/theme-sdk/fields.d.mts +88 -0
  285. package/src/theme-sdk/fields.mjs +330 -0
  286. package/src/theme-sdk/index.ts +14 -2
  287. package/src/theme-sdk/tokens.d.mts +11 -0
  288. package/src/virtual.d.ts +4 -0
  289. package/src/pages/admin/calendar/index.ts +0 -4
@@ -1,6 +1,8 @@
1
1
  import { useMemo, useState } from 'react';
2
2
  import type { EventCta, EventLocation } from '../../lib/schema';
3
3
  import type { EventFieldConfig } from '../../lib/calendar/config';
4
+ import WhereItLands from '../calendar/WhereItLands';
5
+ import RoomHolds, { type RoomHoldValue } from '../calendar/RoomHolds';
4
6
  import { buildRepeat, describeRepeat, parseRepeat, WEEKDAY_CODES, weekdayOf, type RepeatParts } from '../../lib/calendar/rrule';
5
7
 
6
8
  // The event half of the editor sidebar: When · Repeats · Where · Button ·
@@ -21,6 +23,7 @@ export interface EventMeta {
21
23
  calendarIds: string[];
22
24
  fields: Record<string, string>;
23
25
  rsvp: { enabled: boolean; capacity: number; autoConfirm: boolean; maxSeats: number };
26
+ roomHold: RoomHoldValue;
24
27
  }
25
28
 
26
29
  export interface CalendarOption {
@@ -35,6 +38,11 @@ interface Props {
35
38
  calendars: CalendarOption[];
36
39
  fields: EventFieldConfig[];
37
40
  siteTimezone: string;
41
+ // shows the "where it lands" mini month (Schedule data, minus this event)
42
+ eventId?: string;
43
+ // for the Rooms section (conflict wording, "Hold now" on drafts)
44
+ title?: string;
45
+ status?: string;
38
46
  }
39
47
 
40
48
  const DAY_LETTERS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
@@ -64,7 +72,7 @@ function zoneList(): string[] {
64
72
  }
65
73
  }
66
74
 
67
- export function EventPanel({ meta, onChange, calendars, fields, siteTimezone }: Props) {
75
+ export function EventPanel({ meta, onChange, calendars, fields, siteTimezone, eventId, title = '', status = 'draft' }: Props) {
68
76
  const repeat = useMemo(() => parseRepeat(meta.rrule, meta.start), [meta.rrule, meta.start]);
69
77
  const [skipDraft, setSkipDraft] = useState('');
70
78
  const zones = useMemo(zoneList, []);
@@ -136,6 +144,10 @@ export function EventPanel({ meta, onChange, calendars, fields, siteTimezone }:
136
144
  {!meta.allDay && !showEndDate && (
137
145
  <button type="button" className="pe-ev-link" onClick={() => setShowEndDate(true)}>Ends on another day</button>
138
146
  )}
147
+ {eventId && datePart(meta.start) && (
148
+ <WhereItLands date={datePart(meta.start)} eventId={eventId}
149
+ onPick={(d) => setStart(meta.allDay ? d : `${d}T${timePart(meta.start) || '10:00'}`)} />
150
+ )}
139
151
  <label className="pe-switch-row">
140
152
  <span><b>All day</b><small>No start or end time</small></span>
141
153
  <input type="checkbox" role="switch" className="pe-switch" checked={meta.allDay} onChange={(e) => toggleAllDay(e.target.checked)} />
@@ -277,6 +289,15 @@ export function EventPanel({ meta, onChange, calendars, fields, siteTimezone }:
277
289
  </label>
278
290
  </section>
279
291
 
292
+ <RoomHolds
293
+ times={{ eventId: eventId ?? '', title, start: meta.start, end: meta.end, allDay: meta.allDay, timezone: meta.timezone, rrule: meta.rrule, exdates: meta.exdates }}
294
+ hold={meta.roomHold}
295
+ status={status}
296
+ locationName={meta.location.name}
297
+ onChange={(roomHold) => onChange({ roomHold })}
298
+ onLocation={(name) => onChange({ location: { ...meta.location, name } })}
299
+ />
300
+
280
301
  <section className="pe-sb-section pe-ev">
281
302
  <h3 className="pe-sb-title">Button</h3>
282
303
  <div className="pe-ev-row">
@@ -327,7 +348,7 @@ export function EventPanel({ meta, onChange, calendars, fields, siteTimezone }:
327
348
  <section className="pe-sb-section pe-ev">
328
349
  <h3 className="pe-sb-title">Calendars</h3>
329
350
  {calendars.length === 0 ? (
330
- <p className="pe-ev-note">No calendars yet — the event shows on the main calendar. <a href="/admin/calendar/calendars" target="_blank" rel="noopener">Create one</a> to group events.</p>
351
+ <p className="pe-ev-note">No calendars yet — the event shows on the main calendar. <a href="/admin/calendar/settings?new=1" target="_blank" rel="noopener">Create one</a> to group events.</p>
331
352
  ) : (
332
353
  <div className="pe-ev-cals">
333
354
  {calendars.map((c) => {
@@ -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';
@@ -30,6 +31,7 @@ import { I18N, localeHomeUrl } from '../../lib/i18n';
30
31
  import { mergeEventDraft, mergePageDraft, mergePostDraft } from '../../lib/drafts';
31
32
  import { CALENDAR, eventUrl, type EventFieldConfig } from '../../lib/calendar/config';
32
33
  import { EventPanel, eventWhenLabel, type CalendarOption, type EventMeta } from './EventPanel';
34
+ import { fetchRoomStatus, NO_HOLD, RoomConflictDialog } from '../calendar/RoomHolds';
33
35
 
34
36
  // The Ghost-style writing surface, shared by posts AND pages: distraction-free
35
37
  // column, cards via "/", autosave, a collapsible settings sidebar, and an
@@ -62,6 +64,10 @@ interface Props {
62
64
  templates?: string[];
63
65
  // public identity for the Search / Social preview tabs
64
66
  site?: { title: string; url: string };
67
+ // extra site languages (acms({ i18n })) and whether AI drafts are available
68
+ languages?: { code: string; name: string }[];
69
+ mainLanguage?: { code: string; name: string };
70
+ translationDrafts?: boolean;
65
71
  }
66
72
 
67
73
  interface Meta extends EventMeta {
@@ -136,13 +142,18 @@ function joinBrokenTableRows(text: string): string {
136
142
  return out.join('\n');
137
143
  }
138
144
 
145
+ type TrField = 'title' | 'excerpt' | 'bodyHtml' | 'seoTitle' | 'seoDesc' | 'locationName' | 'ctaLabel';
146
+ const emptyTr = () => ({ title: '', excerpt: '', bodyHtml: '', seoTitle: '', seoDesc: '', slug: '', locationName: '', ctaLabel: '', meta: {} as Record<string, { state: 'ai' | 'reviewed'; src: string }> });
147
+ const TR_STATE: Record<string, string> = { missing: 'not translated', ai: 'AI draft', reviewed: 'reviewed', outdated: 'English changed' };
148
+
139
149
  const NO_EVENT: EventMeta = {
140
150
  start: '', end: '', allDay: false, timezone: '', rrule: '', exdates: [], cancelled: false,
141
151
  location: { name: '', address: '', url: '' }, cta: { label: '', href: '', style: 'primary' }, calendarIds: [], fields: {},
142
152
  rsvp: { enabled: false, capacity: 0, autoConfirm: true, maxSeats: 4 },
153
+ roomHold: NO_HOLD,
143
154
  };
144
155
 
145
- export default function PostEditor({ kind = 'post', post, page, event, calendars = [], eventFields = [], siteTimezone = '', initialHTML, unsplashEnabled, previewPath, sharePath, livePath: livePathProp, templates = [], site = { title: '', url: '' } }: Props) {
156
+ 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
157
  const doc = (kind === 'page' ? page : kind === 'event' ? event : post)!;
147
158
  // posts and events share the feature image, excerpt, tags and "featured"
148
159
  const postLike = kind !== 'page';
@@ -167,7 +178,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
167
178
  template: event!.template, publishedAt: event!.publishedAt, scheduledAt: event!.scheduledAt,
168
179
  start: event!.start, end: event!.end, allDay: event!.allDay, timezone: event!.timezone, rrule: event!.rrule,
169
180
  exdates: event!.exdates, cancelled: event!.cancelled, location: event!.location, cta: event!.cta,
170
- calendarIds: event!.calendarIds, fields: event!.fields, rsvp: event!.rsvp,
181
+ calendarIds: event!.calendarIds, fields: event!.fields, rsvp: event!.rsvp, roomHold: event!.roomHold,
171
182
  }
172
183
  : {
173
184
  ...NO_EVENT,
@@ -183,7 +194,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
183
194
  const col = kind === 'event'
184
195
  ? { singular: 'Event', plural: 'Events', base: CALENDAR.eventsBase, redirectFrom: [] }
185
196
  : kind === 'page' ? CONTENT.posts : (CONTENT.collections[meta.collection] ?? CONTENT.posts);
186
- const listHref = kind === 'page' ? '/admin/pages' : kind === 'event' ? '/admin/calendar/events' : `/admin/c/${meta.collection}`;
197
+ const listHref = kind === 'page' ? '/admin/pages' : kind === 'event' ? '/admin/calendar' : `/admin/c/${meta.collection}`;
187
198
  const noun = kind === 'page' ? 'page' : col.singular.toLowerCase();
188
199
 
189
200
  const [authors, setAuthors] = useState<AuthorOption[]>([]);
@@ -230,6 +241,21 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
230
241
  };
231
242
  // excerpt line under the title: shown when it has text or was asked for
232
243
  const [excerptOpen, setExcerptOpen] = useState(false);
244
+
245
+ // ---- translations: the same document's words in another language ----
246
+ // Spanish (etc.) lives in `translations[lang]`; switching language swaps
247
+ // the editor body, the rest (dates, cover, tags…) stays shared.
248
+ const [trs, setTrs] = useState<DocTranslations>(() => ((doc as { translations?: DocTranslations }).translations ?? {}) as DocTranslations);
249
+ const trsRef = useRef(trs);
250
+ trsRef.current = trs;
251
+ const [lang, setLangState] = useState('');
252
+ const langRef = useRef('');
253
+ const mainBody = useRef<{ html: string; json: unknown } | null>(null);
254
+ const trBodyDirty = useRef(false);
255
+ const [trBusy, setTrBusy] = useState(false);
256
+ const [trMsg, setTrMsg] = useState('');
257
+ const mainLangName = mainLanguage?.name || 'English';
258
+ const mainLangShort = (mainLanguage?.code || 'en').toUpperCase();
233
259
  // sidebar: reveal the date input for drafts that have no date yet
234
260
  const [dateOpen, setDateOpen] = useState(false);
235
261
  // which Preview tab opens (the sidebar's search card jumps to Search)
@@ -322,7 +348,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
322
348
  return false;
323
349
  },
324
350
  },
325
- onUpdate: () => queueSave(),
351
+ onUpdate: () => { if (langRef.current) trBodyDirty.current = true; queueSave(); },
326
352
  onSelectionUpdate: ({ editor: ed }) => {
327
353
  // the top-level block holding the caret; the live pane scrolls to it.
328
354
  // Debounced and split-only so typing doesn't re-render the editor.
@@ -344,8 +370,18 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
344
370
  if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
345
371
  const m = { ...metaRef.current, ...overrides };
346
372
  setSave('saving');
347
- const bodyJson = JSON.stringify(ed.getJSON());
348
- const bodyHtml = ed.getHTML();
373
+ // in another language the editor holds that language's body: store it as
374
+ // the translation and keep sending the main-language body unchanged
375
+ if (langRef.current && trBodyDirty.current) {
376
+ trBodyDirty.current = false;
377
+ const l = langRef.current;
378
+ const cur = trsRef.current[l] ?? emptyTr();
379
+ trsRef.current = { ...trsRef.current, [l]: { ...cur, bodyHtml: ed.isEmpty ? '' : ed.getHTML(), meta: { ...cur.meta, bodyHtml: { state: 'reviewed', src: textHash(mainBody.current?.html ?? '') } } } };
380
+ setTrs(trsRef.current);
381
+ }
382
+ const bodyJson = langRef.current && mainBody.current ? JSON.stringify(mainBody.current.json) : JSON.stringify(ed.getJSON());
383
+ const bodyHtml = langRef.current && mainBody.current ? mainBody.current.html : ed.getHTML();
384
+ const translations = languages.length ? trsRef.current : undefined;
349
385
  if (kind === 'event') {
350
386
  const r = await actions.events.save({
351
387
  id: docId,
@@ -374,7 +410,9 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
374
410
  calendarIds: m.calendarIds,
375
411
  fields: m.fields,
376
412
  rsvp: m.rsvp,
413
+ roomHold: m.roomHold,
377
414
  scheduledAt: m.scheduledAt,
415
+ translations,
378
416
  });
379
417
  if (r.error) {
380
418
  setSave('error');
@@ -403,6 +441,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
403
441
  status: m.status,
404
442
  isHome: m.isHome,
405
443
  locale: m.locale,
444
+ translations,
406
445
  });
407
446
  if (r.error) {
408
447
  setSave('error');
@@ -437,6 +476,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
437
476
  template: m.template,
438
477
  publishedAt: m.publishedAt,
439
478
  scheduledAt: m.scheduledAt,
479
+ translations,
440
480
  });
441
481
  if (r.error) {
442
482
  setSave('error');
@@ -463,6 +503,92 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
463
503
  saveTimer.current = setTimeout(() => save(), 1200);
464
504
  }, [save]);
465
505
 
506
+ // the main-language text a translated field is made from
507
+ const mainValue = (f: TrField): string => {
508
+ const m = metaRef.current;
509
+ switch (f) {
510
+ case 'title': return m.title;
511
+ case 'excerpt': return postLike ? m.excerpt : '';
512
+ case 'seoTitle': return m.seoTitle;
513
+ case 'seoDesc': return m.seoDesc;
514
+ case 'locationName': return kind === 'event' ? m.location.name : '';
515
+ case 'ctaLabel': return kind === 'event' ? m.cta.label : '';
516
+ default: return langRef.current && mainBody.current ? mainBody.current.html : (editorRef.current?.isEmpty ? '' : editorRef.current?.getHTML() ?? '');
517
+ }
518
+ };
519
+ const trFields = (): TrField[] => ['title', ...(postLike ? ['excerpt' as const] : []), 'bodyHtml', 'seoTitle', 'seoDesc', ...(kind === 'event' ? ['locationName' as const, 'ctaLabel' as const] : [])];
520
+ const trStatus = (l: string, f: TrField) => {
521
+ const src = mainValue(f);
522
+ if (!src.trim()) return 'n/a';
523
+ const t = trsRef.current[l];
524
+ const v = t?.[f] ?? '';
525
+ if (!String(v).trim()) return 'missing';
526
+ const meta = t?.meta?.[f];
527
+ if (meta && meta.src !== textHash(src)) return 'outdated';
528
+ return meta?.state === 'reviewed' ? 'reviewed' : 'ai';
529
+ };
530
+ const setTr = (f: TrField, value: string, state: 'ai' | 'reviewed' = 'reviewed') => {
531
+ const l = langRef.current;
532
+ if (!l) return;
533
+ const cur = trsRef.current[l] ?? emptyTr();
534
+ trsRef.current = { ...trsRef.current, [l]: { ...cur, [f]: value, meta: { ...cur.meta, [f]: { state, src: textHash(mainValue(f)) } } } };
535
+ setTrs(trsRef.current);
536
+ queueSave();
537
+ };
538
+ // switch the editor between the main language and a translation
539
+ const switchLang = async (next: string) => {
540
+ const ed = editorRef.current;
541
+ if (!ed || next === langRef.current) return;
542
+ await save();
543
+ if (!langRef.current) mainBody.current = { html: ed.getHTML(), json: ed.getJSON() };
544
+ const html = next ? (trsRef.current[next]?.bodyHtml ?? '') : (mainBody.current?.html ?? '');
545
+ langRef.current = next;
546
+ setLangState(next);
547
+ trBodyDirty.current = false;
548
+ ed.commands.setContent(html, { emitUpdate: false });
549
+ if (!next) mainBody.current = null;
550
+ setTrMsg('');
551
+ };
552
+ // machine drafts for every field still missing or with changed English
553
+ const translateAll = async (includeReviewed = false) => {
554
+ const l = langRef.current;
555
+ if (!l) return;
556
+ 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'); });
557
+ if (!fields.length) { setTrMsg('Everything is already translated.'); return; }
558
+ setTrBusy(true); setTrMsg('');
559
+ try {
560
+ const res = await fetch('/api/design/translate', {
561
+ method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' },
562
+ 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 })) }),
563
+ });
564
+ const body = await res.json().catch(() => ({})) as { results?: Record<string, string>; error?: string };
565
+ if (!res.ok) { setTrMsg(body.error || `Translation failed (${res.status}).`); return; }
566
+ const results = body.results ?? {};
567
+ const cur = trsRef.current[l] ?? emptyTr();
568
+ const next = { ...cur, meta: { ...cur.meta } } as typeof cur;
569
+ for (const f of fields) {
570
+ if (results[f] === undefined) continue;
571
+ (next as Record<string, unknown>)[f] = results[f];
572
+ next.meta[f] = { state: 'ai', src: textHash(mainValue(f)) };
573
+ }
574
+ trsRef.current = { ...trsRef.current, [l]: next };
575
+ setTrs(trsRef.current);
576
+ if (results.bodyHtml !== undefined && langRef.current === l) { editorRef.current?.commands.setContent(results.bodyHtml, { emitUpdate: false }); trBodyDirty.current = false; }
577
+ setTrMsg(`Translated ${Object.keys(results).length} ${Object.keys(results).length === 1 ? 'part' : 'parts'} as AI drafts. Check them, then mark them right.`);
578
+ queueSave();
579
+ } catch { setTrMsg('Couldn’t reach the server.'); } finally { setTrBusy(false); }
580
+ };
581
+ const confirmAll = () => {
582
+ const l = langRef.current;
583
+ if (!l) return;
584
+ const cur = trsRef.current[l] ?? emptyTr();
585
+ const meta = { ...cur.meta };
586
+ for (const f of trFields()) if (String(cur[f] ?? '').trim() && mainValue(f).trim()) meta[f] = { state: 'reviewed', src: textHash(mainValue(f)) };
587
+ trsRef.current = { ...trsRef.current, [l]: { ...cur, meta } };
588
+ setTrs(trsRef.current);
589
+ queueSave();
590
+ };
591
+
466
592
  const setMeta = useCallback((patch: Partial<Meta>) => {
467
593
  setMetaState((prev) => {
468
594
  const next = { ...prev, ...patch };
@@ -588,8 +714,17 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
588
714
  toastTimer.current = setTimeout(() => setToast(null), 4500);
589
715
  }
590
716
 
591
- async function transition(patch: Partial<Meta>) {
717
+ // publishing or scheduling an event that holds rooms: check for bookings
718
+ // that overlap first, and let the author keep or cancel each one
719
+ const [roomGate, setRoomGate] = useState<{ patch: Partial<Meta>; rooms: Parameters<typeof RoomConflictDialog>[0]['rooms'] } | null>(null);
720
+ async function transition(patch: Partial<Meta>, roomsChecked = false) {
592
721
  setPublishOpen(false);
722
+ const m0 = metaRef.current;
723
+ if (kind === 'event' && !roomsChecked && (patch.status === 'published' || patch.status === 'scheduled') && m0.roomHold.resourceIds.length) {
724
+ const st = await fetchRoomStatus({ eventId: docId, title: m0.title, start: m0.start, end: m0.end, allDay: m0.allDay, timezone: m0.timezone, rrule: m0.rrule, exdates: m0.exdates }, m0.roomHold).catch(() => null);
725
+ const rooms = (st?.rooms ?? []).filter((r) => m0.roomHold.resourceIds.includes(r.id) && r.conflicts.length);
726
+ if (rooms.length) { setRoomGate({ patch, rooms }); return; }
727
+ }
593
728
  const next = { ...metaRef.current, ...patch };
594
729
  metaRef.current = next;
595
730
  setMetaState(next);
@@ -779,6 +914,19 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
779
914
 
780
915
  return (
781
916
  <div className={`pe-shell ${sidebarOpen ? 'has-sidebar' : ''} ${split ? 'has-split' : ''}`}>
917
+ {roomGate && (
918
+ <RoomConflictDialog
919
+ rooms={roomGate.rooms}
920
+ onCancel={() => setRoomGate(null)}
921
+ onConfirm={async ({ keep, cancel, message }) => {
922
+ if (cancel.length) await actions.events.cancelBookingsForEvent({ ids: cancel, message });
923
+ const hold = metaRef.current.roomHold;
924
+ const patch = { ...roomGate.patch, roomHold: { ...hold, keep: [...new Set([...hold.keep, ...keep])] } };
925
+ setRoomGate(null);
926
+ await transition(patch, true);
927
+ }}
928
+ />
929
+ )}
782
930
  {toast && (
783
931
  <div className="pe-toast" role="status">
784
932
  <span className="pe-toast-check">✓</span>
@@ -788,7 +936,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
788
936
  )}
789
937
  <header className="pe-topbar">
790
938
  <div className="pe-topbar-left">
791
- <a className="pe-btn ghost sm" href={listHref} onClick={(e) => { if (e.metaKey || e.ctrlKey || e.shiftKey) return; e.preventDefault(); requestLeave('list'); }} aria-label={kind === 'page' ? 'Back to pages' : `Back to ${col.plural.toLowerCase()}`}>‹<span className="pe-hide-sm">{kind === 'page' ? 'Pages' : col.plural}</span></a>
939
+ <a className="pe-btn ghost sm" href={listHref} onClick={(e) => { if (e.metaKey || e.ctrlKey || e.shiftKey) return; e.preventDefault(); requestLeave('list'); }} aria-label={kind === 'page' ? 'Back to pages' : kind === 'event' ? 'Back to the schedule' : `Back to ${col.plural.toLowerCase()}`}>‹<span className="pe-hide-sm">{kind === 'page' ? 'Pages' : kind === 'event' ? 'Schedule' : col.plural}</span></a>
792
940
  <span className={`pe-savestate ${saveState}`}>
793
941
  <i className="pe-save-dot" aria-hidden="true" />
794
942
  {STATUS_LABEL[meta.status]}{pending && <em className="pe-edited-dot"> · edited</em>}<span className="pe-hide-sm"> · {saveLabel}</span>
@@ -808,6 +956,17 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
808
956
  Preview
809
957
  </button>
810
958
  </div>
959
+ {languages.length > 0 && (
960
+ <div className="pe-seg pe-langs" role="radiogroup" aria-label="Language">
961
+ <button type="button" role="radio" aria-checked={!lang} onClick={() => void switchLang('')}>{mainLangShort}</button>
962
+ {languages.map((l) => (
963
+ <button key={l.code} type="button" role="radio" aria-checked={lang === l.code} title={`${l.name} version`} onClick={() => void switchLang(l.code)}>
964
+ {l.code.toUpperCase()}
965
+ {trFields().some((f) => ['missing', 'outdated'].includes(trStatus(l.code, f))) && <i className="pe-lang-dot" aria-label="needs translation" />}
966
+ </button>
967
+ ))}
968
+ </div>
969
+ )}
811
970
  <div className="pe-topbar-right">
812
971
  <div className="pe-publish-wrap">
813
972
  <button type="button" className="pe-btn primary sm" onClick={() => setPublishOpen((v) => !v)}>
@@ -973,7 +1132,7 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
973
1132
  Feature image
974
1133
  </button>
975
1134
  )}
976
- {!meta.excerpt && !excerptOpen && (
1135
+ {!meta.excerpt && !excerptOpen && !lang && (
977
1136
  <button type="button" title="A short teaser under the title, on post cards and in link previews" onClick={() => setExcerptOpen(true)}>
978
1137
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M4 7h16M4 12h10M4 17h7" /></svg>
979
1138
  Excerpt
@@ -982,6 +1141,36 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
982
1141
  </div>
983
1142
  )}
984
1143
 
1144
+ {lang && (() => {
1145
+ const name = languages.find((l) => l.code === lang)?.name ?? lang;
1146
+ const statuses = trFields().map((f) => [f, trStatus(lang, f)] as const).filter(([, st]) => st !== 'n/a');
1147
+ const label: Record<TrField, string> = { title: 'Title', excerpt: 'Excerpt', bodyHtml: 'Body', seoTitle: 'Search title', seoDesc: 'Search description', locationName: 'Place name', ctaLabel: 'Button text' };
1148
+ const t = trs[lang] ?? emptyTr();
1149
+ return (
1150
+ <section className="pe-trpanel" aria-label={`${name} version`}>
1151
+ <div className="pe-tr-head">
1152
+ <b>{name} version</b>
1153
+ <span className="pe-tr-states">{statuses.map(([f, st]) => <span key={f} className={`is-${st}`}>{label[f]}: {TR_STATE[st]}</span>)}</span>
1154
+ </div>
1155
+ <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>
1156
+ <div className="pe-tr-fields">
1157
+ {postLike && <label>Excerpt<textarea rows={2} value={t.excerpt} placeholder={meta.excerpt} onChange={(e) => setTr('excerpt', e.target.value)} /></label>}
1158
+ <label>Search title<input value={t.seoTitle} placeholder={meta.seoTitle || meta.title} onChange={(e) => setTr('seoTitle', e.target.value)} /></label>
1159
+ <label>Search description<textarea rows={2} value={t.seoDesc} placeholder={meta.seoDesc} onChange={(e) => setTr('seoDesc', e.target.value)} /></label>
1160
+ {kind === 'event' && <label>Place name<input value={t.locationName} placeholder={meta.location.name} onChange={(e) => setTr('locationName', e.target.value)} /></label>}
1161
+ {kind === 'event' && <label>Button text<input value={t.ctaLabel} placeholder={meta.cta.label} onChange={(e) => setTr('ctaLabel', e.target.value)} /></label>}
1162
+ </div>
1163
+ <div className="pe-tr-actions">
1164
+ {translationDrafts && <button type="button" className="pe-btn sm" disabled={trBusy} onClick={() => void translateAll()}>{trBusy ? 'Translating…' : `Translate the rest with AI`}</button>}
1165
+ {statuses.some(([, st]) => st === 'ai' || st === 'outdated') && <button type="button" className="pe-btn ghost sm" onClick={confirmAll}>✓ Looks right</button>}
1166
+ {!translationDrafts && <span className="pe-tr-msg">Turn on AI in Settings → AI &amp; agents and the {name} can draft itself.</span>}
1167
+ <button type="button" className="pe-btn ghost sm" onClick={() => void switchLang('')}>Back to {mainLangName}</button>
1168
+ {trMsg && <span className="pe-tr-msg" role="status">{trMsg}</span>}
1169
+ </div>
1170
+ </section>
1171
+ );
1172
+ })()}
1173
+
985
1174
  <textarea
986
1175
  className="pe-title"
987
1176
  rows={1}
@@ -989,19 +1178,19 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
989
1178
  // grow to fit on mount and on narrow screens, not just while typing
990
1179
  if (el) { el.style.height = 'auto'; el.style.height = `${el.scrollHeight}px`; }
991
1180
  }}
992
- placeholder={kind === 'page' ? 'Page title' : `${col.singular} title`}
993
- value={meta.title}
1181
+ placeholder={lang ? meta.title || 'Title' : kind === 'page' ? 'Page title' : `${col.singular} title`}
1182
+ value={lang ? (trs[lang]?.title ?? '') : meta.title}
994
1183
  onChange={(e) => {
995
1184
  e.target.style.height = 'auto';
996
1185
  e.target.style.height = `${e.target.scrollHeight}px`;
997
- setMeta({ title: e.target.value });
1186
+ if (langRef.current) setTr('title', e.target.value); else setMeta({ title: e.target.value });
998
1187
  }}
999
1188
  onKeyDown={(e) => {
1000
1189
  if (e.key === 'Enter') { e.preventDefault(); editor?.commands.focus('start'); }
1001
1190
  }}
1002
1191
  />
1003
1192
 
1004
- {postLike && (meta.excerpt || excerptOpen) && (
1193
+ {postLike && !lang && (meta.excerpt || excerptOpen) && (
1005
1194
  <textarea
1006
1195
  className="pe-excerpt"
1007
1196
  rows={1}
@@ -1374,11 +1563,14 @@ export default function PostEditor({ kind = 'post', post, page, event, calendars
1374
1563
 
1375
1564
  {kind === 'event' && (
1376
1565
  <EventPanel
1566
+ eventId={docId}
1377
1567
  meta={meta}
1378
1568
  onChange={(patch) => setMeta(patch)}
1379
1569
  calendars={calendars}
1380
1570
  fields={eventFields}
1381
1571
  siteTimezone={siteTimezone}
1572
+ title={meta.title}
1573
+ status={meta.status}
1382
1574
  />
1383
1575
  )}
1384
1576
 
@@ -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
  });