@acms-subir/core 0.2.0 → 0.3.1

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 (254) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/cli/common.mjs +35 -0
  3. package/cli/legacy.mjs +317 -0
  4. package/cli/main.mjs +66 -0
  5. package/cli/palette.mjs +4 -0
  6. package/cli/preview.mjs +50 -0
  7. package/cli/remote.mjs +55 -0
  8. package/cli/themes.mjs +127 -0
  9. package/cli.mjs +2 -369
  10. package/deploy/Caddyfile +8 -0
  11. package/deploy/Dockerfile +34 -0
  12. package/deploy/compose.https.yml +21 -0
  13. package/deploy/compose.yml +46 -0
  14. package/docs/live-video.md +91 -0
  15. package/docs/theme-authoring.md +103 -0
  16. package/email-runner.mjs +32 -0
  17. package/integration.mjs +189 -2
  18. package/ops-check.mjs +27 -0
  19. package/package.json +27 -5
  20. package/src/actions/index.ts +578 -49
  21. package/src/components/AiCleaner.tsx +291 -0
  22. package/src/components/AiSettings.tsx +575 -0
  23. package/src/components/CommandPalette.tsx +200 -0
  24. package/src/components/ContentList.astro +246 -0
  25. package/src/components/DesignAccess.tsx +182 -0
  26. package/src/components/DesignManager.tsx +418 -46
  27. package/src/components/EmailSettings.tsx +521 -0
  28. package/src/components/EventPage.astro +178 -0
  29. package/src/components/LegalPage.astro +27 -0
  30. package/src/components/LinksManager.tsx +119 -58
  31. package/src/components/LiveCard.astro +174 -0
  32. package/src/components/LiveManager.tsx +910 -0
  33. package/src/components/NavManager.tsx +42 -43
  34. package/src/components/ThemeScript.astro +50 -0
  35. package/src/components/ThemeSections.astro +58 -0
  36. package/src/components/ThemeSectionsEditor.tsx +513 -0
  37. package/src/components/WidgetRenderer.astro +3 -0
  38. package/src/components/booking/BookingFlow.astro +150 -0
  39. package/src/components/booking/HoursEditor.astro +63 -0
  40. package/src/components/builder/PageBuilder.tsx +10 -2
  41. package/src/components/editor/EventPanel.tsx +401 -0
  42. package/src/components/editor/FormatDock.tsx +81 -0
  43. package/src/components/editor/LivePane.tsx +122 -0
  44. package/src/components/editor/MobileToolbar.tsx +93 -0
  45. package/src/components/editor/PostEditor.tsx +605 -223
  46. package/src/components/editor/PreviewOverlay.tsx +242 -43
  47. package/src/components/editor/extensions/calendar.tsx +51 -7
  48. package/src/components/editor/extensions/events.tsx +153 -0
  49. package/src/components/editor/extensions/slash.tsx +7 -1
  50. package/src/components/site/Pager.astro +24 -0
  51. package/src/components/site/PostCard.astro +64 -0
  52. package/src/components/site/TagLabel.astro +11 -0
  53. package/src/layouts/Admin.astro +289 -49
  54. package/src/layouts/Site.astro +352 -59
  55. package/src/lib/agent-api.ts +13 -0
  56. package/src/lib/agent-discovery.ts +40 -0
  57. package/src/lib/agent.ts +114 -0
  58. package/src/lib/ai-config.ts +58 -0
  59. package/src/lib/ai-schema.ts +22 -0
  60. package/src/lib/ai.ts +90 -0
  61. package/src/lib/api-keys.ts +65 -0
  62. package/src/lib/api.ts +56 -0
  63. package/src/lib/auth.ts +25 -5
  64. package/src/lib/backup/content.ts +36 -0
  65. package/src/lib/backup/unzip.ts +12 -1
  66. package/src/lib/backup/zip.ts +1 -1
  67. package/src/lib/booking/agent.ts +108 -0
  68. package/src/lib/booking/availability.ts +221 -0
  69. package/src/lib/booking/emails.ts +115 -0
  70. package/src/lib/booking/forms.ts +77 -0
  71. package/src/lib/booking/service.ts +277 -0
  72. package/src/lib/booking/view.ts +54 -0
  73. package/src/lib/calendar/agent-schema.ts +48 -0
  74. package/src/lib/calendar/agent.ts +136 -0
  75. package/src/lib/calendar/api.ts +81 -0
  76. package/src/lib/calendar/cards.ts +102 -0
  77. package/src/lib/calendar/config.ts +52 -0
  78. package/src/lib/calendar/data.ts +130 -0
  79. package/src/lib/calendar/events.ts +261 -0
  80. package/src/lib/calendar/feeds.ts +43 -0
  81. package/src/lib/calendar/hooks.ts +38 -0
  82. package/src/lib/calendar/http.ts +30 -0
  83. package/src/lib/calendar/ics-write.ts +120 -0
  84. package/src/lib/calendar/ics.ts +15 -5
  85. package/src/lib/calendar/import.ts +203 -0
  86. package/src/lib/calendar/render.ts +120 -37
  87. package/src/lib/calendar/rrule.ts +149 -0
  88. package/src/lib/calendar/seo.ts +48 -0
  89. package/src/lib/design-service.ts +58 -0
  90. package/src/lib/designDiff.ts +145 -0
  91. package/src/lib/designPreview.ts +110 -12
  92. package/src/lib/documents/cleanup.ts +31 -0
  93. package/src/lib/documents/extract.ts +154 -0
  94. package/src/lib/documents/vendor.d.ts +4 -0
  95. package/src/lib/drafts.ts +60 -6
  96. package/src/lib/dynamicBlocks.ts +40 -8
  97. package/src/lib/email/accounts.ts +55 -0
  98. package/src/lib/email/config.ts +86 -0
  99. package/src/lib/email/migration.ts +24 -0
  100. package/src/lib/email/providers.ts +89 -0
  101. package/src/lib/email/service.ts +78 -0
  102. package/src/lib/email/store.ts +167 -0
  103. package/src/lib/email/templates.ts +18 -0
  104. package/src/lib/email/types.ts +32 -0
  105. package/src/lib/email/webhooks.ts +87 -0
  106. package/src/lib/email.ts +15 -0
  107. package/src/lib/env.ts +1 -1
  108. package/src/lib/legal/index.ts +25 -0
  109. package/src/lib/live/client.ts +88 -0
  110. package/src/lib/live/crypto.ts +28 -0
  111. package/src/lib/live/migration.ts +15 -0
  112. package/src/lib/live/providers.ts +152 -0
  113. package/src/lib/live/service.ts +170 -0
  114. package/src/lib/live/signals.ts +89 -0
  115. package/src/lib/live/store.ts +49 -0
  116. package/src/lib/live/types.ts +52 -0
  117. package/src/lib/live/view.ts +78 -0
  118. package/src/lib/mobile/bookings.ts +73 -0
  119. package/src/lib/mobile/calendar.ts +85 -0
  120. package/src/lib/mobile/content.ts +85 -0
  121. package/src/lib/mobile/http.ts +63 -0
  122. package/src/lib/mobile/route.ts +20 -0
  123. package/src/lib/ops.ts +8 -0
  124. package/src/lib/preview.ts +21 -1
  125. package/src/lib/repo/migrations.ts +221 -0
  126. package/src/lib/repo/sql.ts +907 -68
  127. package/src/lib/repo/types.ts +125 -5
  128. package/src/lib/scheduler.ts +27 -0
  129. package/src/lib/schema.ts +316 -1
  130. package/src/lib/search-i18n.ts +3 -0
  131. package/src/lib/socialIcons.ts +32 -0
  132. package/src/lib/tagColor.ts +11 -0
  133. package/src/lib/theme.ts +59 -40
  134. package/src/lib/themeTokens.ts +183 -0
  135. package/src/lib/umami.ts +108 -0
  136. package/src/lib/webhook-sql.ts +68 -0
  137. package/src/lib/webhooks.ts +147 -0
  138. package/src/lib/widgets.ts +1 -0
  139. package/src/middleware.ts +45 -3
  140. package/src/pages/404.astro +23 -0
  141. package/src/pages/[...slug].astro +14 -5
  142. package/src/pages/account/[action].astro +53 -0
  143. package/src/pages/admin/ai.astro +12 -0
  144. package/src/pages/admin/c/[collection]/[id].astro +4 -1
  145. package/src/pages/admin/c/[collection]/index.astro +127 -92
  146. package/src/pages/admin/calendar/bookings.astro +400 -0
  147. package/src/pages/admin/calendar/calendars.astro +265 -0
  148. package/src/pages/admin/calendar/events/[id].astro +56 -0
  149. package/src/pages/admin/calendar/events/index.astro +408 -0
  150. package/src/pages/admin/calendar/index.ts +4 -0
  151. package/src/pages/admin/calendar/integrations.astro +4 -0
  152. package/src/pages/admin/calendar/resources/[id].astro +180 -0
  153. package/src/pages/admin/calendar/resources/index.astro +106 -0
  154. package/src/pages/admin/calendar/resources/link.astro +196 -0
  155. package/src/pages/admin/design.astro +10 -3
  156. package/src/pages/admin/email.astro +13 -0
  157. package/src/pages/admin/index.astro +232 -85
  158. package/src/pages/admin/integrations.astro +237 -0
  159. package/src/pages/admin/links.astro +3 -3
  160. package/src/pages/admin/live.astro +12 -0
  161. package/src/pages/admin/login.astro +14 -10
  162. package/src/pages/admin/navigation.astro +8 -2
  163. package/src/pages/admin/pages/[id].astro +4 -1
  164. package/src/pages/admin/pages/index.astro +41 -48
  165. package/src/pages/admin/settings.astro +418 -367
  166. package/src/pages/admin/tags.astro +4 -4
  167. package/src/pages/admin/users.astro +38 -28
  168. package/src/pages/api/agent/docs.ts +33 -0
  169. package/src/pages/api/agent/index.ts +21 -0
  170. package/src/pages/api/ai/clean.ts +13 -0
  171. package/src/pages/api/ai/draft.ts +14 -0
  172. package/src/pages/api/ai/settings.ts +53 -0
  173. package/src/pages/api/ai/test.ts +12 -0
  174. package/src/pages/api/backup.ts +8 -21
  175. package/src/pages/api/calendar/availability.ts +35 -0
  176. package/src/pages/api/calendar/bookings/[id].ts +49 -0
  177. package/src/pages/api/calendar/bookings/index.ts +73 -0
  178. package/src/pages/api/calendar/calendars.ts +35 -0
  179. package/src/pages/api/calendar/docs.ts +66 -0
  180. package/src/pages/api/calendar/events/[id].ts +63 -0
  181. package/src/pages/api/calendar/events/index.ts +112 -0
  182. package/src/pages/api/calendar/resources.ts +34 -0
  183. package/src/pages/api/design/capabilities.ts +10 -0
  184. package/src/pages/api/design/index.ts +58 -0
  185. package/src/pages/api/design/keys.ts +24 -0
  186. package/src/pages/api/design/media.ts +32 -0
  187. package/src/pages/api/email/run.ts +11 -0
  188. package/src/pages/api/email/settings.ts +61 -0
  189. package/src/pages/api/email/webhooks/[provider].ts +4 -0
  190. package/src/pages/api/live/oauth/youtube/callback.ts +19 -0
  191. package/src/pages/api/live/settings.ts +45 -0
  192. package/src/pages/api/live/status.ts +7 -0
  193. package/src/pages/api/live/thumb.ts +48 -0
  194. package/src/pages/api/mcp.ts +59 -0
  195. package/src/pages/api/media/upload.ts +2 -2
  196. package/src/pages/api/ops/status.ts +39 -0
  197. package/src/pages/api/restore.ts +27 -37
  198. package/src/pages/api/search.ts +18 -4
  199. package/src/pages/api/v1/availability.ts +7 -0
  200. package/src/pages/api/v1/bookings.ts +17 -0
  201. package/src/pages/api/v1/calendars.ts +7 -0
  202. package/src/pages/api/v1/content/[id].ts +7 -0
  203. package/src/pages/api/v1/content/index.ts +7 -0
  204. package/src/pages/api/v1/events.ts +7 -0
  205. package/src/pages/api/v1/resources.ts +7 -0
  206. package/src/pages/api/webhooks/docs.ts +61 -0
  207. package/src/pages/author/[slug].astro +43 -35
  208. package/src/pages/blog/[slug].astro +137 -66
  209. package/src/pages/blog/index.astro +95 -30
  210. package/src/pages/blog/tag/[slug].astro +45 -40
  211. package/src/pages/book/[resource]/[link].astro +70 -0
  212. package/src/pages/book/[resource]/index.astro +63 -0
  213. package/src/pages/book/[resource].ics.ts +38 -0
  214. package/src/pages/book/index.astro +47 -0
  215. package/src/pages/book/manage/[token].astro +108 -0
  216. package/src/pages/calendar/[slug].ics.ts +27 -0
  217. package/src/pages/calendar/feed.ics.ts +15 -0
  218. package/src/pages/calendar/index.astro +191 -0
  219. package/src/pages/embed/book/[resource]/[link].astro +33 -0
  220. package/src/pages/embed/calendar/[slug].astro +43 -0
  221. package/src/pages/events/[slug].astro +74 -0
  222. package/src/pages/events/[slug].ics.ts +25 -0
  223. package/src/pages/index.astro +12 -3
  224. package/src/pages/legal/privacy.astro +4 -0
  225. package/src/pages/legal/terms.astro +4 -0
  226. package/src/pages/links.astro +29 -3
  227. package/src/pages/llms.txt.ts +4 -0
  228. package/src/pages/preview/[type]/[id].astro +27 -6
  229. package/src/pages/search.astro +40 -20
  230. package/src/pages/sitemap.xml.ts +16 -1
  231. package/src/styles/admin.css +924 -105
  232. package/src/styles/cards.css +92 -74
  233. package/src/styles/editor.css +492 -114
  234. package/src/styles/email-settings.css +117 -0
  235. package/src/styles/live-admin.css +126 -0
  236. package/src/styles/live.css +199 -0
  237. package/src/styles/sections.css +12 -0
  238. package/src/styles/site.css +938 -281
  239. package/src/styles/theme-editor.css +135 -0
  240. package/src/theme-sdk/contract.d.mts +619 -0
  241. package/src/theme-sdk/contract.mjs +81 -0
  242. package/src/theme-sdk/index.ts +15 -0
  243. package/src/theme-sdk/tokens.d.mts +148 -0
  244. package/src/theme-sdk/tokens.mjs +55 -0
  245. package/src/themes/editorial/templates/home.astro +4 -4
  246. package/src/themes/editorial/templates/post-minimal.astro +2 -2
  247. package/src/themes/editorial/theme.css +27 -37
  248. package/src/themes/editorial/theme.json +25 -3
  249. package/src/themes/field/theme.json +27 -9
  250. package/src/themes/ink/theme.json +30 -12
  251. package/src/themes/night/theme.css +4 -7
  252. package/src/themes/night/theme.json +28 -10
  253. package/src/virtual.d.ts +14 -0
  254. package/src/worker.ts +5 -1
@@ -11,8 +11,10 @@ import type { NavItem, NavArea } from '../lib/schema';
11
11
  // area is empty); footer renders above the footer text; social feeds the
12
12
  // footer social row and share UIs.
13
13
  //
14
- // Edits stay local until "Save changes" commits them all at once — the live
15
- // nav never sees a half-rearranged state. Discard restores the saved state.
14
+ // Edits stay local until "Save changes", which STAGES the whole list in the
15
+ // design draft: menus go live together with the design on "Publish design"
16
+ // (here or on the Design screen), and preview mode shows them first.
17
+ // Discard restores the last saved state.
16
18
 
17
19
  type SaveState = 'saved' | 'saving' | 'error';
18
20
 
@@ -22,8 +24,7 @@ const AREAS: { key: NavArea; title: string; hint: string }[] = [
22
24
  { key: 'social', title: 'Social', hint: 'Footer social row — also used for share links on posts.' },
23
25
  ];
24
26
 
25
- // unsaved additions carry a temp id until nav.create returns the real one
26
- const isTemp = (id: string) => id.startsWith('tmp-');
27
+ // new items carry a temp id; Publish design gives them real ids
27
28
 
28
29
  function Row({
29
30
  item, onEdit, onArea, onDelete,
@@ -67,8 +68,10 @@ function Row({
67
68
  );
68
69
  }
69
70
 
70
- export default function NavManager({ items: initial }: { items: NavItem[] }) {
71
+ export default function NavManager({ items: initial, staged: stagedInitially }: { items: NavItem[]; staged: boolean }) {
71
72
  const [items, setItems] = useState<NavItem[]>(initial);
73
+ // the saved list is staged (differs from the live menus) until Publish
74
+ const [staged, setStaged] = useState(stagedInitially);
72
75
  // last state the server has — dirty compares against this
73
76
  const [saved, setSaved] = useState<NavItem[]>(initial);
74
77
  const [saveState, setSaveState] = useState<SaveState>('saved');
@@ -122,45 +125,34 @@ export default function NavManager({ items: initial }: { items: NavItem[] }) {
122
125
  });
123
126
  }
124
127
 
125
- // one commit for everything: deletes, creates (temp ids become real),
126
- // updates, then the full display order
128
+ // one commit for everything: the full ordered list stages in the draft
127
129
  async function saveAll() {
128
130
  setSaveState('saving');
129
- try {
130
- const savedById = new Map(saved.map((n) => [n.id, n]));
131
- const keptIds = new Set(items.map((n) => n.id));
132
- for (const n of saved) {
133
- if (!keptIds.has(n.id)) {
134
- const r = await actions.nav.delete({ id: n.id });
135
- if (r.error) throw r.error;
136
- }
137
- }
138
- const next: NavItem[] = [];
139
- for (const n of items) {
140
- if (isTemp(n.id)) {
141
- const r = await actions.nav.create({ label: n.label, url: n.url, area: n.area });
142
- if (r.error) throw r.error;
143
- next.push({ ...n, id: r.data.item.id });
144
- } else {
145
- const prev = savedById.get(n.id);
146
- if (prev && (prev.label !== n.label || prev.url !== n.url || prev.area !== n.area)) {
147
- const r = await actions.nav.update({ id: n.id, label: n.label, url: n.url, area: n.area });
148
- if (r.error) throw r.error;
149
- }
150
- next.push(n);
151
- }
152
- }
153
- const ordered = AREAS.flatMap((a) => next.filter((n) => n.area === a.key)).map((n) => n.id);
154
- if (ordered.length) {
155
- const r = await actions.nav.reorder({ ids: ordered });
156
- if (r.error) throw r.error;
157
- }
158
- setItems(next);
159
- setSaved(next);
160
- setSaveState('saved');
161
- } catch {
162
- setSaveState('error');
163
- }
131
+ const ordered = AREAS.flatMap((a) => items.filter((n) => n.area === a.key));
132
+ const r = await actions.nav.saveDraft({
133
+ items: ordered.map(({ id, label, url, area }) => ({ id, label, url, area })),
134
+ });
135
+ if (r.error) { setSaveState('error'); return; }
136
+ setItems(ordered);
137
+ setSaved(ordered);
138
+ setStaged(r.data.changes.some((c) => c.key === 'nav'));
139
+ setSaveState('saved');
140
+ }
141
+
142
+ async function publish() {
143
+ if (!window.confirm('Publish now? Staged menus and any staged design changes go live together. The current design is saved to History.')) return;
144
+ setSaveState('saving');
145
+ const r = await actions.settings.applyDesign({});
146
+ if (r.error) { setSaveState('error'); return; }
147
+ setStaged(false);
148
+ setSaveState('saved');
149
+ }
150
+
151
+ async function unstage() {
152
+ setSaveState('saving');
153
+ const r = await actions.settings.revertDesignChange({ key: 'nav' });
154
+ if (r.error) { setSaveState('error'); return; }
155
+ location.reload(); // back to the live menus
164
156
  }
165
157
 
166
158
  function discard() {
@@ -172,7 +164,7 @@ export default function NavManager({ items: initial }: { items: NavItem[] }) {
172
164
  <div className="nm">
173
165
  <div className="nm-head">
174
166
  <span className={`lm-savestate ${saveState === 'saved' && dirty ? 'dirty' : saveState}`}>
175
- {saveState === 'saving' ? 'Saving…' : saveState === 'error' ? 'Save failed' : dirty ? 'Unsaved changes — not live yet' : 'Live'}
167
+ {saveState === 'saving' ? 'Saving…' : saveState === 'error' ? 'Save failed' : dirty ? 'Unsaved changes' : staged ? 'Staged, not live yet' : 'Live'}
176
168
  </span>
177
169
  {dirty && (
178
170
  <>
@@ -180,6 +172,13 @@ export default function NavManager({ items: initial }: { items: NavItem[] }) {
180
172
  <button className="btn sm primary" type="button" onClick={() => void saveAll()} disabled={saveState === 'saving'}>Save changes</button>
181
173
  </>
182
174
  )}
175
+ {!dirty && staged && (
176
+ <>
177
+ <button className="btn sm ghost" type="button" onClick={() => void unstage()} disabled={saveState === 'saving'}>Discard staged menus</button>
178
+ <a className="btn sm" href="/?designPreview=1" target="_blank" rel="noopener">Preview site ↗</a>
179
+ <button className="btn sm primary" type="button" onClick={() => void publish()} disabled={saveState === 'saving'}>Publish…</button>
180
+ </>
181
+ )}
183
182
  </div>
184
183
  {AREAS.map((a) => {
185
184
  const group = items.filter((n) => n.area === a.key);
@@ -0,0 +1,50 @@
1
+ ---
2
+ // Admin theme (Light / Dark / System). Inlined in <head> of every admin
3
+ // document so data-theme is set before first paint — no light flash on a
4
+ // dark-mode machine. The preference lives in localStorage per browser;
5
+ // "system" follows the OS live. Public-site previews (iframes) never read it.
6
+ ---
7
+
8
+ <meta name="color-scheme" content="light dark" />
9
+ <meta name="theme-color" content="#e8e6e1" />
10
+ <script is:inline>
11
+ (() => {
12
+ const KEY = 'acms-theme';
13
+ const COLORS = { light: '#e8e6e1', dark: '#141311' };
14
+ const mq = matchMedia('(prefers-color-scheme: dark)');
15
+ const root = document.documentElement;
16
+ const pref = () => {
17
+ try { return localStorage.getItem(KEY) || 'system'; } catch { return 'system'; }
18
+ };
19
+ const apply = (p = pref()) => {
20
+ const t = p === 'dark' || (p === 'system' && mq.matches) ? 'dark' : 'light';
21
+ root.dataset.theme = t;
22
+ root.dataset.themePref = p;
23
+ document.querySelector('meta[name="theme-color"]')?.setAttribute('content', COLORS[t]);
24
+ document.querySelectorAll('[data-set-theme]').forEach((el) => {
25
+ el.setAttribute('aria-pressed', String(el.getAttribute('data-set-theme') === p));
26
+ });
27
+ };
28
+ const set = (p) => {
29
+ try { localStorage.setItem(KEY, p); } catch {}
30
+ apply(p);
31
+ window.dispatchEvent(new CustomEvent('acms:theme', { detail: root.dataset.theme }));
32
+ };
33
+ window.acmsTheme = { set, pref };
34
+ apply();
35
+ mq.addEventListener('change', () => pref() === 'system' && apply());
36
+ document.addEventListener('DOMContentLoaded', () => apply());
37
+ document.addEventListener('click', (e) => {
38
+ const el = e.target instanceof Element ? e.target.closest('[data-set-theme]') : null;
39
+ if (el) set(el.getAttribute('data-set-theme'));
40
+ });
41
+ // ⌘⇧L / Ctrl+Shift+L cycles light → dark → system
42
+ document.addEventListener('keydown', (e) => {
43
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'l') {
44
+ e.preventDefault();
45
+ const order = ['light', 'dark', 'system'];
46
+ set(order[(order.indexOf(pref()) + 1) % order.length]);
47
+ }
48
+ });
49
+ })();
50
+ </script>
@@ -0,0 +1,58 @@
1
+ ---
2
+ import LiveCard from './LiveCard.astro';
3
+ import type { Page, Settings } from '../lib/schema';
4
+ import { getTheme } from '../lib/theme';
5
+ import { getRepo } from '../lib/repo';
6
+ import { CONTENT, collectionUrl } from '../lib/content';
7
+ import { sectionIssues } from '../theme-sdk/contract.mjs';
8
+ import WidgetRenderer from './WidgetRenderer.astro';
9
+ import { expandDynamicBlocks } from '../lib/dynamicBlocks';
10
+ import { highlightCodeHtml } from '../lib/highlightCode';
11
+ import PostCard from './site/PostCard.astro';
12
+ import '../styles/sections.css';
13
+ interface Props { page: Page; settings: Settings }
14
+ const { page, settings } = Astro.props;
15
+ const theme = getTheme(settings.activeTheme);
16
+ const layout = settings.sectionPages[page.id];
17
+ const repo = await getRepo(Astro.locals);
18
+ const bodyHtml = page.bodyHtml ? highlightCodeHtml(await expandDynamicBlocks(page.bodyHtml, repo)) : '';
19
+ const safeUrl = (value: unknown) => {
20
+ const s = String(value ?? '').trim();
21
+ return /^(https?:\/\/|mailto:|tel:|\/(?!\/)|#)/i.test(s) ? s : '';
22
+ };
23
+ const sections = await Promise.all(layout.sections.map(async (section) => {
24
+ const definition = theme.sections[section.type];
25
+ const invalid = sectionIssues({ mode: 'sections', sections: [section] }, theme.sections).length > 0;
26
+ const data = Object.fromEntries(Object.entries(definition?.fields ?? {}).map(([key, field]) => [key, section.data[key] ?? (field as {default?: unknown}).default ?? '']));
27
+ const collection = String(data.collection || 'posts');
28
+ const posts = section.type === 'posts' && !section.hidden && !invalid && CONTENT.collections[collection]
29
+ ? (await repo.listPublishedPosts(collection)).slice(0, Math.max(1, Math.min(12, Number(data.limit) || 3))) : [];
30
+ return { ...section, data, invalid, posts, Template: theme.sectionTemplates[section.type] as any };
31
+ }));
32
+ ---
33
+ <div class="acms-sections">
34
+ {sections.filter(s => !s.hidden && !s.invalid).map(({ id, type, variant, data, Template, posts }) => (
35
+ <section id={`section-${id}`} class:list={['acms-section', `section-${type}`, `variant-${variant}`]}>
36
+ {type === 'live' && !Template ? <LiveCard display={variant} /> : type === 'page-content' ? (page.bodyHtml ? <article class="page-body" set:html={bodyHtml} /> : <WidgetRenderer pageId={page.id} blocks={page.blocks} />) : Template ? <Template data={data} variant={variant} settings={settings} page={page} /> : (
37
+ <>
38
+ <div class="section-copy">
39
+ {data.heading && <h2>{String(data.heading)}</h2>}
40
+ {data.text && <p class="section-text">{String(data.text)}</p>}
41
+ {data.label && safeUrl(data.href) && <a class="w-btn primary" href={safeUrl(data.href)}>{String(data.label)}</a>}
42
+ </div>
43
+ {type === 'hero' && safeUrl(data.image) && <img class="section-image" src={safeUrl(data.image)} alt={String(data.alt ?? '')} loading="eager" />}
44
+ {['features', 'testimonials', 'faq'].includes(type) && (
45
+ <div class="section-items">
46
+ {String(data.items ?? '').split('\n').filter(Boolean).map((row) => {
47
+ const [title, ...body] = row.split('|');
48
+ return type === 'faq' ? <details><summary>{title}</summary><p>{body.join('|')}</p></details>
49
+ : <article><h3>{title}</h3><p>{body.join('|')}</p></article>;
50
+ })}
51
+ </div>
52
+ )}
53
+ {type === 'posts' && <div class="section-items">{posts.map(post => <PostCard variant={variant === 'list' ? 'row' : 'card'} href={collectionUrl(post.collection, post.slug)} title={post.title} excerpt={post.excerpt} cover={post.cover} coverAlt={post.coverAlt} />)}</div>}
54
+ </>
55
+ )}
56
+ </section>
57
+ ))}
58
+ </div>