@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.
Files changed (251) hide show
  1. package/CHANGELOG.md +69 -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/sections.mjs +186 -0
  9. package/cli/themes.mjs +52 -8
  10. package/docs/mobile.md +527 -0
  11. package/docs/sections.md +203 -0
  12. package/docs/theme-authoring.md +1 -1
  13. package/integration.mjs +55 -1
  14. package/mobile-template/client/AGENTS.md +97 -0
  15. package/mobile-template/client/README.md +65 -0
  16. package/mobile-template/client/app/_layout.tsx +56 -0
  17. package/mobile-template/client/app/index.tsx +21 -0
  18. package/mobile-template/client/app.json +21 -0
  19. package/mobile-template/client/components/LanguageSwitch.tsx +31 -0
  20. package/mobile-template/client/eas.json +21 -0
  21. package/mobile-template/client/env.example +3 -0
  22. package/mobile-template/client/gitignore +42 -0
  23. package/mobile-template/client/package.json +38 -0
  24. package/mobile-template/client/sections/index.ts +11 -0
  25. package/mobile-template/client/tsconfig.json +7 -0
  26. package/mobile-template/editor/AGENTS.md +66 -0
  27. package/mobile-template/editor/README.md +73 -0
  28. package/mobile-template/editor/app/_layout.tsx +76 -0
  29. package/mobile-template/editor/app/auth.tsx +13 -0
  30. package/mobile-template/editor/app/index.tsx +55 -0
  31. package/mobile-template/editor/app.json +21 -0
  32. package/mobile-template/editor/components/Screen.tsx +51 -0
  33. package/mobile-template/editor/components/SignIn.tsx +36 -0
  34. package/mobile-template/editor/components/TextTabs.tsx +27 -0
  35. package/mobile-template/editor/eas.json +21 -0
  36. package/mobile-template/editor/env.example +5 -0
  37. package/mobile-template/editor/gitignore +42 -0
  38. package/mobile-template/editor/lib/appearance.tsx +12 -0
  39. package/mobile-template/editor/lib/config.ts +17 -0
  40. package/mobile-template/editor/lib/format.ts +24 -0
  41. package/mobile-template/editor/lib/tools.ts +16 -0
  42. package/mobile-template/editor/package.json +40 -0
  43. package/mobile-template/editor/tools/README.md +2 -0
  44. package/mobile-template/editor/tsconfig.json +7 -0
  45. package/mobile-template/recipes/booking/files/app/book/[resource].tsx +57 -0
  46. package/mobile-template/recipes/booking/files/app/book/details.tsx +105 -0
  47. package/mobile-template/recipes/booking/files/app/book/index.tsx +38 -0
  48. package/mobile-template/recipes/booking/recipe.json +17 -0
  49. package/mobile-template/recipes/editor-bookings/files/app/bookings/index.tsx +113 -0
  50. package/mobile-template/recipes/editor-bookings/files/tools/bookings.ts +1 -0
  51. package/mobile-template/recipes/editor-bookings/recipe.json +8 -0
  52. package/mobile-template/recipes/editor-content/files/app/content/[kind]/[id].tsx +13 -0
  53. package/mobile-template/recipes/editor-content/files/app/content/index.tsx +76 -0
  54. package/mobile-template/recipes/editor-content/files/components/DocumentDetail.tsx +55 -0
  55. package/mobile-template/recipes/editor-content/files/tools/content.ts +1 -0
  56. package/mobile-template/recipes/editor-content/recipe.json +8 -0
  57. package/mobile-template/recipes/editor-media/files/app/media/index.tsx +73 -0
  58. package/mobile-template/recipes/editor-media/files/tools/media.ts +1 -0
  59. package/mobile-template/recipes/editor-media/recipe.json +8 -0
  60. package/mobile-template/recipes/editor-sections/files/app/sections/[pageId]/[sectionId].tsx +12 -0
  61. package/mobile-template/recipes/editor-sections/files/app/sections/index.tsx +48 -0
  62. package/mobile-template/recipes/editor-sections/files/components/SectionEditorPane.tsx +77 -0
  63. package/mobile-template/recipes/editor-sections/files/tools/sections.ts +1 -0
  64. package/mobile-template/recipes/editor-sections/recipe.json +8 -0
  65. package/mobile-template/recipes/editor-translations/files/app/translations/index.tsx +63 -0
  66. package/mobile-template/recipes/editor-translations/files/app/translations/review.tsx +14 -0
  67. package/mobile-template/recipes/editor-translations/files/components/ReviewPane.tsx +72 -0
  68. package/mobile-template/recipes/editor-translations/files/tools/translations.ts +1 -0
  69. package/mobile-template/recipes/editor-translations/recipe.json +8 -0
  70. package/mobile-template/recipes/editor-writer/files/app/write/[kind]/[id].tsx +132 -0
  71. package/mobile-template/recipes/editor-writer/files/app/write/new.tsx +61 -0
  72. package/mobile-template/recipes/editor-writer/files/tools/writer.ts +1 -0
  73. package/mobile-template/recipes/editor-writer/recipe.json +8 -0
  74. package/mobile-template/recipes/events/files/app/events/[slug].tsx +41 -0
  75. package/mobile-template/recipes/events/files/app/events/index.tsx +50 -0
  76. package/mobile-template/recipes/events/files/lib/addToCalendar.ts +27 -0
  77. package/mobile-template/recipes/events/recipe.json +15 -0
  78. package/mobile-template/recipes/news/files/app/news/[slug].tsx +25 -0
  79. package/mobile-template/recipes/news/files/app/news/index.tsx +50 -0
  80. package/mobile-template/recipes/news/recipe.json +13 -0
  81. package/mobile-template/recipes/push/files/app/notifications.tsx +64 -0
  82. package/mobile-template/recipes/push/files/components/PushSetup.tsx +15 -0
  83. package/mobile-template/recipes/push/files/lib/push.ts +15 -0
  84. package/mobile-template/recipes/push/recipe.json +20 -0
  85. package/package.json +4 -2
  86. package/section-library/README.md +16 -0
  87. package/section-library/contact/Contact.astro +49 -0
  88. package/section-library/contact/section.json +106 -0
  89. package/section-library/cta-band/CtaBand.astro +28 -0
  90. package/section-library/cta-band/section.json +51 -0
  91. package/section-library/feature-grid/FeatureGrid.astro +54 -0
  92. package/section-library/feature-grid/section.json +107 -0
  93. package/section-library/gallery/Gallery.astro +29 -0
  94. package/section-library/gallery/section.json +51 -0
  95. package/section-library/hero-banner/HeroBanner.astro +42 -0
  96. package/section-library/hero-banner/section.json +81 -0
  97. package/section-library/latest-posts/LatestPosts.astro +22 -0
  98. package/section-library/latest-posts/section.json +37 -0
  99. package/section-library/pricing/Pricing.astro +40 -0
  100. package/section-library/pricing/section.json +117 -0
  101. package/section-library/questions/Questions.astro +38 -0
  102. package/section-library/questions/section.json +69 -0
  103. package/section-library/service-times/ServiceTimes.astro +42 -0
  104. package/section-library/service-times/section.json +127 -0
  105. package/section-library/team/Team.astro +45 -0
  106. package/section-library/team/section.json +88 -0
  107. package/section-library/upcoming-events/UpcomingEvents.astro +54 -0
  108. package/section-library/upcoming-events/section.json +51 -0
  109. package/src/actions/index.ts +19 -171
  110. package/src/components/AiSettings.tsx +45 -5
  111. package/src/components/DesignManager.tsx +112 -16
  112. package/src/components/MobileApp.tsx +392 -0
  113. package/src/components/ThemeSections.astro +40 -21
  114. package/src/components/ThemeSectionsEditor.tsx +757 -229
  115. package/src/components/editor/PostEditor.tsx +174 -10
  116. package/src/components/import/ImportExport.tsx +4 -1
  117. package/src/components/import/runner.ts +1 -0
  118. package/src/components/sections/SectionFields.tsx +560 -0
  119. package/src/components/sections/SectionPicker.tsx +120 -0
  120. package/src/components/sections/TranslationPanel.tsx +143 -0
  121. package/src/components/sections/checks.ts +64 -0
  122. package/src/layouts/Admin.astro +6 -1
  123. package/src/layouts/Site.astro +107 -19
  124. package/src/lib/agent-discovery.ts +11 -7
  125. package/src/lib/agent.ts +35 -2
  126. package/src/lib/ai-schema.ts +5 -0
  127. package/src/lib/ai.ts +5 -0
  128. package/src/lib/api.ts +12 -0
  129. package/src/lib/content-service.ts +198 -0
  130. package/src/lib/design-service.ts +153 -5
  131. package/src/lib/drafts.ts +3 -3
  132. package/src/lib/editor/auth.ts +264 -0
  133. package/src/lib/editor/bookings.ts +60 -0
  134. package/src/lib/editor/config.ts +25 -0
  135. package/src/lib/editor/documents.ts +227 -0
  136. package/src/lib/editor/http.ts +48 -0
  137. package/src/lib/editor/me.ts +30 -0
  138. package/src/lib/editor/migration.ts +57 -0
  139. package/src/lib/editor/next.ts +11 -0
  140. package/src/lib/editor/openapi.ts +124 -0
  141. package/src/lib/editor/schemas.ts +91 -0
  142. package/src/lib/editor/sections.ts +96 -0
  143. package/src/lib/editor/store.ts +184 -0
  144. package/src/lib/editor/token-route.ts +74 -0
  145. package/src/lib/editor/translations.ts +163 -0
  146. package/src/lib/email/accounts.ts +2 -1
  147. package/src/lib/email/types.ts +2 -1
  148. package/src/lib/home-source.ts +34 -0
  149. package/src/lib/i18n.ts +34 -0
  150. package/src/lib/import/apply.ts +31 -2
  151. package/src/lib/import/bundle.ts +4 -0
  152. package/src/lib/import/export.ts +1 -0
  153. package/src/lib/import/ghost.ts +1 -0
  154. package/src/lib/media/upload.ts +83 -0
  155. package/src/lib/mobile/associations.ts +25 -0
  156. package/src/lib/mobile/calendar.ts +19 -11
  157. package/src/lib/mobile/config.mjs +59 -0
  158. package/src/lib/mobile/content.ts +18 -13
  159. package/src/lib/mobile/embed.ts +83 -0
  160. package/src/lib/mobile/http.ts +8 -6
  161. package/src/lib/mobile/locale.ts +18 -0
  162. package/src/lib/mobile/openapi.ts +111 -0
  163. package/src/lib/mobile/pages.ts +71 -0
  164. package/src/lib/mobile/route.ts +3 -3
  165. package/src/lib/mobile/schemas.ts +60 -0
  166. package/src/lib/mobile/site.ts +41 -0
  167. package/src/lib/push/api.ts +158 -0
  168. package/src/lib/push/config.ts +20 -0
  169. package/src/lib/push/expo.ts +76 -0
  170. package/src/lib/push/migration.ts +60 -0
  171. package/src/lib/push/service.ts +302 -0
  172. package/src/lib/push/store.ts +144 -0
  173. package/src/lib/push/types.ts +43 -0
  174. package/src/lib/repo/migrations.ts +11 -0
  175. package/src/lib/repo/sql.ts +40 -7
  176. package/src/lib/repo/types.ts +9 -3
  177. package/src/lib/roles.ts +5 -0
  178. package/src/lib/scheduler.ts +3 -0
  179. package/src/lib/schema.ts +27 -1
  180. package/src/lib/section-ai.ts +55 -0
  181. package/src/lib/section-targets.ts +62 -0
  182. package/src/lib/sections-agent.ts +146 -0
  183. package/src/lib/sections.ts +217 -0
  184. package/src/lib/theme.ts +16 -1
  185. package/src/lib/translate.ts +97 -0
  186. package/src/lib/translations.ts +39 -0
  187. package/src/middleware.ts +32 -0
  188. package/src/pages/.well-known/apple-app-site-association.ts +6 -0
  189. package/src/pages/.well-known/assetlinks.json.ts +6 -0
  190. package/src/pages/[...slug].astro +8 -3
  191. package/src/pages/admin/app-auth.astro +159 -0
  192. package/src/pages/admin/c/[collection]/[id].astro +9 -0
  193. package/src/pages/admin/calendar/events/[id].astro +9 -0
  194. package/src/pages/admin/login.astro +6 -2
  195. package/src/pages/admin/logout.ts +5 -2
  196. package/src/pages/admin/mobile.astro +18 -0
  197. package/src/pages/admin/pages/[id].astro +62 -0
  198. package/src/pages/admin/section-preview.astro +61 -0
  199. package/src/pages/admin/users.astro +107 -11
  200. package/src/pages/api/agent/docs.ts +6 -2
  201. package/src/pages/api/agent/index.ts +1 -1
  202. package/src/pages/api/ai/settings.ts +7 -4
  203. package/src/pages/api/design/ai.ts +15 -0
  204. package/src/pages/api/design/index.ts +12 -42
  205. package/src/pages/api/design/translate.ts +15 -0
  206. package/src/pages/api/live/thumb.ts +1 -1
  207. package/src/pages/api/mcp.ts +2 -2
  208. package/src/pages/api/media/upload.ts +10 -78
  209. package/src/pages/api/push/admin.ts +27 -0
  210. package/src/pages/api/v1/admin/bookings/[id]/[action].ts +14 -0
  211. package/src/pages/api/v1/admin/bookings/index.ts +6 -0
  212. package/src/pages/api/v1/admin/documents/[kind]/[id]/[action].ts +17 -0
  213. package/src/pages/api/v1/admin/documents/[kind]/[id]/index.ts +20 -0
  214. package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts +26 -0
  215. package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts +18 -0
  216. package/src/pages/api/v1/admin/documents/index.ts +13 -0
  217. package/src/pages/api/v1/admin/me.ts +6 -0
  218. package/src/pages/api/v1/admin/media/index.ts +32 -0
  219. package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/index.ts +14 -0
  220. package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/publish.ts +14 -0
  221. package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts +22 -0
  222. package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts +14 -0
  223. package/src/pages/api/v1/admin/sections/index.ts +6 -0
  224. package/src/pages/api/v1/auth/revoke.ts +5 -0
  225. package/src/pages/api/v1/auth/token.ts +5 -0
  226. package/src/pages/api/v1/devices/[token].ts +20 -0
  227. package/src/pages/api/v1/devices/index.ts +17 -0
  228. package/src/pages/api/v1/openapi.json.ts +7 -0
  229. package/src/pages/api/v1/pages/[...slug].ts +7 -0
  230. package/src/pages/api/v1/pages/home.ts +7 -0
  231. package/src/pages/api/v1/push/topics.ts +7 -0
  232. package/src/pages/api/v1/site.ts +7 -0
  233. package/src/pages/api/v1/theme.ts +7 -0
  234. package/src/pages/blog/[slug].astro +8 -2
  235. package/src/pages/blog/index.astro +11 -4
  236. package/src/pages/embed/section/[...path].astro +42 -0
  237. package/src/pages/events/[slug].astro +7 -2
  238. package/src/pages/index.astro +11 -5
  239. package/src/pages/sitemap.xml.ts +8 -0
  240. package/src/styles/admin.css +69 -0
  241. package/src/styles/editor.css +21 -0
  242. package/src/styles/mobile-app.css +151 -0
  243. package/src/styles/sections.css +7 -1
  244. package/src/styles/theme-editor.css +255 -0
  245. package/src/theme-sdk/contract.d.mts +2193 -89
  246. package/src/theme-sdk/contract.mjs +77 -20
  247. package/src/theme-sdk/fields.d.mts +88 -0
  248. package/src/theme-sdk/fields.mjs +326 -0
  249. package/src/theme-sdk/index.ts +10 -2
  250. package/src/theme-sdk/tokens.d.mts +11 -0
  251. package/src/virtual.d.ts +1 -0
@@ -0,0 +1,203 @@
1
+ # Editable sections: code the look, leave the content editable
2
+
3
+ This is the guide for agents (and people) building pages for an ACMS site. The rule is simple:
4
+
5
+ > **Code decides how things look. Section fields decide what the owner can change.**
6
+
7
+ The owner will update service times, events, prices, people and text long after you're gone, in Design → Pages, without touching code. So anything that might change must be a **field**, never hardcoded in the component.
8
+
9
+ ## The three layers
10
+
11
+ | Layer | Lives in | Owned by |
12
+ | --- | --- | --- |
13
+ | Code | `src/themes/<theme>/sections/*.astro` | you (the agent). Rewrite freely |
14
+ | Schema | `theme.json → sections.<type>.fields` | the contract between both |
15
+ | Data | the site database (Design → Pages) | the owner. Never overwrite it |
16
+
17
+ Changing code needs a build and deploy. Changing data never does.
18
+
19
+ ## Workflow
20
+
21
+ ```sh
22
+ acms inspect --json # project, themes, capabilities
23
+ acms docs sections # this guide
24
+ acms section library # ready-made sections to start from
25
+ acms section add service-times --theme church
26
+ acms section add upcoming-events --theme church
27
+ acms section new visit-banner --theme church # blank scaffold
28
+ # edit theme.json fields + the .astro markup
29
+ acms check --build # validate, refresh section-types.d.ts, compile
30
+ ```
31
+
32
+ With a mobile app beside the site (`acms add mobile`), library sections render
33
+ natively in the app out of the box; `acms section add <name> --theme <t> --native`
34
+ copies an editable native version into `mobile/sections/`, `acms section new`
35
+ also writes a typed native stub there, and `acms check` lists sections the app
36
+ can only show as a web view. Keep a section's native version in step with its
37
+ fields and variants.
38
+
39
+ Then give the page starting content in one of two ways:
40
+
41
+ 1. **A preset** in `theme.json` (preferred for code-first work). The owner sees "Start from Church home" in Design → Pages and gets a copy they can edit:
42
+
43
+ ```json
44
+ "presets": {
45
+ "church-home": {
46
+ "name": "Church home",
47
+ "description": "Hero, service times, next events, visit info",
48
+ "sections": [
49
+ { "type": "hero-banner", "variant": "cover", "data": { "heading": "A church for the whole city." } },
50
+ { "type": "service-times", "data": { "services": [ { "title": "Sunday Worship", "day": "Sunday", "time": "10:00" } ] } },
51
+ { "type": "upcoming-events", "data": { "events": { "calendar": "youth", "limit": 3 } } }
52
+ ]
53
+ }
54
+ }
55
+ ```
56
+
57
+ 2. **MCP / agent API** when you're connected to a live site: `list_section_types`, `get_page_layout`, `set_page_layout`, `update_section_data`. Everything stages into the design draft. The owner publishes.
58
+
59
+ ## Defining a section
60
+
61
+ ```json
62
+ "sections": {
63
+ "service-times": {
64
+ "name": "Service times",
65
+ "description": "Weekly gatherings with day, time and a short note.",
66
+ "version": 1,
67
+ "component": "sections/ServiceTimes.astro",
68
+ "variants": ["cards", "strip"],
69
+ "fields": {
70
+ "heading": { "label": "Heading", "type": "text", "default": "Join us this week" },
71
+ "services": { "label": "Services", "type": "list", "itemLabel": "title", "max": 12, "fields": {
72
+ "title": { "label": "Name", "type": "text" },
73
+ "day": { "label": "Day", "type": "select", "options": ["Sunday", "Wednesday"], "default": "Sunday" },
74
+ "time": { "label": "Time", "type": "time", "default": "10:00" },
75
+ "streamed": { "label": "Streamed online", "type": "toggle" }
76
+ } }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ Field keys are camelCase. Every field takes `label`, optional `help` (one line shown under the input) and `default`.
83
+
84
+ ### Field types
85
+
86
+ | Type | Stored value | Component receives | Use for |
87
+ | --- | --- | --- | --- |
88
+ | `text` | string | string | headings, names, short lines |
89
+ | `textarea` | string | string (keep newlines with `white-space: pre-line`) | paragraphs, addresses |
90
+ | `richtext` | HTML | **sanitized** HTML: render with `set:html` | answers, notes with links/lists |
91
+ | `image` | URL | safe URL or `''` | photos (owner picks from the media library) |
92
+ | `url` | URL | safe URL or `''` | bare links |
93
+ | `link` | `{ label, href, ref? }` | `LinkValue { label, href, external }` | buttons. `ref` follows a page/post/event/booking if its address changes |
94
+ | `select` | string | one of `options` | small fixed choices |
95
+ | `number` | number | number | counts |
96
+ | `toggle` | boolean | boolean | show/hide flags |
97
+ | `date` | `YYYY-MM-DD` | string. Format with `formatDay()` | one-off dates |
98
+ | `time` | `HH:MM` | string. Format with `formatTime()` | times of day |
99
+ | `list` | array of rows | array of rows (sub-fields resolved the same way) | repeating things: services, people, plans, photos |
100
+ | `source` | `{ calendar?, collection?, tag?, resource?, limit }` | fresh `EventItem[]` / `PostItem[]` / `BookingItem[]` | content that must stay current on its own |
101
+
102
+ `list` options: `fields` (scalar sub-fields only; a list can't contain a list or source), `itemLabel` (the sub-field shown on collapsed rows), `min`, `max` (default 50).
103
+
104
+ `source` options: `source: "events" | "posts" | "bookings"`. The owner picks which calendar / collection + tag / resource, and how many (1–24).
105
+
106
+ **When to use `source` instead of `list`:** if the thing already exists elsewhere in ACMS (calendar events, blog posts, booking links), link to it with `source`. Then the homepage updates itself when the calendar changes. Use a `list` only for content that lives nowhere else.
107
+
108
+ ### More schema options
109
+
110
+ | Where | Option | What it does |
111
+ | --- | --- | --- |
112
+ | section | `category` | Groups it in the "Add a section" picker ("Intro", "Events & times", "People"…) |
113
+ | section | `example` | Sample data: renders the picker thumbnail and fills **empty** fields in the admin preview (never saved, never shown to visitors). Always provide one |
114
+ | section | `limit` | At most this many per page (e.g. `1` for a hero) |
115
+ | section | `presets` | Extra ready-filled picker entries: `[{ "name": "Service times: holiday", "variant": "strip", "data": {…} }]` |
116
+ | field | `visibleIf` | Show only when a sibling matches: `{ "field": "style", "equals": "image" }` (also `notEquals`, `in`, or no operator = "is filled in"). Works inside list rows against the row |
117
+ | field | `required`, `maxLength` | Enforced in the editor (live counter), before publishing, and by `acms check` for examples/presets |
118
+ | field | `aiHint` | Guidance for "Help me write" on that field ("friendly, under 12 words") |
119
+ | field | `locked` | Only owners can change it; editors see it read-only |
120
+
121
+ Owners can also lock a whole section, and schedule any section with **Show from / until** (site timezone). Image fields get a **focal point**: the component receives `<key>Focus` (e.g. `data.imageFocus`, or `row.photoFocus` in lists) ready for `style={`object-position:${data.imageFocus}`}`, or for `background-position`. Use it on every cropped image.
122
+
123
+ ## Writing the component
124
+
125
+ ```astro
126
+ ---
127
+ import type { SectionProps } from '@acms-subir/core/theme';
128
+ import { formatTime } from '@acms-subir/core/theme';
129
+ import type { ServiceTimesData } from '../section-types'; // generated by acms check / acms theme types
130
+ const { data, variant, settings } = Astro.props as SectionProps<ServiceTimesData>;
131
+ ---
132
+ <div class:list={['times', `is-${variant}`]}>
133
+ <h2>{data.heading}</h2>
134
+ <ul>
135
+ {data.services.filter((s) => s.title).map((s) => (
136
+ <li><b>{s.title}</b> {s.day} · {formatTime(s.time, settings.locale)}</li>
137
+ ))}
138
+ </ul>
139
+ </div>
140
+ <style>/* scoped; anything goes */</style>
141
+ ```
142
+
143
+ The component receives `data` (resolved values, defaults filled in), `variant`, `settings` and `page`. The core wraps it in `<section class="acms-section section-<type> variant-<variant>">`.
144
+
145
+ Rules:
146
+
147
+ - **Render empty values gracefully.** Every field can be empty; lists can have zero rows; sources can return nothing. Skip empty rows (`filter((s) => s.title)`), and show an "empty" line for sources.
148
+ - **Only `richtext` is HTML.** Never `set:html` any other field.
149
+ - Style with theme tokens (`--accent`, `--ink`, `--muted`, `--panel`, `--soft`, `--line`, `--bg`, `--r`, `--r-sm`, `--r-lg`, `--font-heading`, `--font-body`) so the section follows the theme and dark mode. Reuse `.w-btn primary|outline` for buttons.
150
+ - No data fetching in components. Anything dynamic comes through a `source` field.
151
+ - Text the owner might reword (headings, button labels, "See all" links, empty-state lines) is a field with a sensible `default`.
152
+
153
+ ## Responsive: phone *and* desktop, every time
154
+
155
+ A section that only works at one width is broken. Design for both ends, not "mobile, stretched":
156
+
157
+ - **The frame is wide.** Section pages break out of the article column to the theme's `--wide-width` (about 1080px, the same frame as the header). Use that room on desktop: multi-column grids, side-by-side copy and media, split layouts. A single narrow column floating on a wide screen is a bug. Cap only *prose* line length (`max-width: 60ch` on paragraphs), never the whole section.
158
+ - **Grids adapt.** Prefer `repeat(auto-fit, minmax(min(100%, 15rem), 1fr))`, which fills desktop and stacks on phones with no media query. Fixed column counts (`repeat(3, …)`) need `@media` steps down to 2 and then 1.
159
+ - **Split layouts stack.** Two-column (text | image, info | hours) collapses to one column below roughly 700px; decide what comes first on phones.
160
+ - **No fixed widths.** No `width: 600px`. Use `%`, `fr`, `min()`, `clamp()` and `max-width`. Nothing may cause sideways scrolling at 360px.
161
+ - **Type and spacing scale.** Headings and padding use `clamp()` so they're big on desktop and sensible on phones.
162
+ - **Touch targets** are at least 44px tall on phones.
163
+
164
+ Check every section at **390px (phone), 768px (tablet) and 1280–1440px (desktop)**. The Design screen's preview has all three (Mobile · Tablet · Desktop), and `acms theme dev` does too. `acms check` flags common desktop and phone traps (fixed pixel widths, fixed column counts without a breakpoint), but only looking at it proves it.
165
+
166
+ ## Other languages
167
+
168
+ With `acms({ i18n: { locales: ['en', 'es'] } })`, a page keeps **one** layout. Each section stores its facts once (times, dates, photos, links, linked events) and, per extra language, only its **words**. `/es` renders the same sections with the Spanish words, falling back to English field by field. Owners translate in Design → Pages → Español, and AI drafts the Spanish automatically whenever the English changes (marked "AI draft" until someone reviews it).
169
+
170
+ What translates by default: `text`, `textarea`, `richtext`, and the label of a `link`. Everything else is shared. Adjust per field:
171
+
172
+ - `"translatable": false` on a text field that isn't words: phone numbers, emails, prices, street addresses, names of people.
173
+ - `"translatable": true` on a `select` whose options are words (e.g. weekday names).
174
+
175
+ Components need nothing special: they receive the data already in the visitor's language, and `settings.locale` is set to it, so format dates and times with it (`formatTime(s.time, settings.locale)`) and they come out right (`19:30`, `sáb, 3 oct`). Fixed words written into a component (a "See all" label) stay in the main language. Make them fields if they must translate.
176
+
177
+ List rows get a hidden `_id` so their translations follow them when reordered. Keep `_id` when you rewrite rows in migrations.
178
+
179
+ ## Changing a section later without losing content
180
+
181
+ Saved data is keyed by field key. So:
182
+
183
+ - **Adding a field** is always safe. Give it a `default`.
184
+ - **Removing a field** is safe. Its saved value is kept (the owner sees "kept in case the theme brings it back").
185
+ - **Changing a textarea of `a | b` lines into a list** is automatic: stored strings become rows, cells map to sub-fields in order.
186
+ - **Renaming or reshaping a field:** bump the section's `version` and add `migrations/<type>.mjs`:
187
+
188
+ ```js
189
+ // src/themes/church/migrations/service-times.mjs
190
+ export default function migrate(data, fromVersion) {
191
+ if (fromVersion < 2) data.services = (data.services ?? []).map(({ name, ...row }) => ({ ...row, title: name }));
192
+ return data;
193
+ }
194
+ ```
195
+
196
+ The site renders old data through the migration immediately. The owner's "Upgrade sections" button saves the upgraded copy. Without a migration, a version bump hides the section until one exists.
197
+
198
+ ## Check before you finish
199
+
200
+ - `acms check --build` passes. It validates fields, list labels, select defaults, presets and migrations, and compiles.
201
+ - Every piece of changeable content is a field; nothing the owner would edit is hardcoded.
202
+ - Each section looks right with empty fields, one row and many rows, in light and dark, at **phone (390px), tablet (768px) and desktop (1280px+)** widths. On desktop it uses the wide frame (no lonely narrow column); on phones nothing scrolls sideways.
203
+ - In Design → Pages (or the preview's **Edit** chips), every section opens a form that makes sense to a non-developer: clear labels, `help` where needed, `itemLabel` set on lists.
@@ -56,7 +56,7 @@ Custom definitions live in `theme.json`:
56
56
  }
57
57
  ```
58
58
 
59
- The component receives SectionProps. Supported field kinds: text, textarea, image, url, select (options required), number. Instances have stable id/type/version/variant/hidden/data fields. A page layout has mode and sections, persisted by page ID in `settings.sectionPages`. Section values and order have a design draft lifecycle distinct from rich-text/widget page drafts; publishing design does not publish pending page text. Theme switches preserve the map. To migrate a custom schema, transform a pulled draft to the new version, review it, and push it with its revision; incompatible versions never auto-convert silently. Existing widget pages continue using their original renderer.
59
+ The component receives SectionProps<Data>; `acms theme types <slug>` (also run by `acms check`) writes `section-types.d.ts` with one Data interface per section. Field kinds: text, textarea, richtext, image, url, link, select (options required), number, toggle, date, time, list (a repeater of those) and source (live events, posts or booking links). The full guide, including presets, migrations and the starter library (`acms section library` / `acms section add`), is `acms docs sections`. Instances have stable id/type/version/variant/hidden/data fields. A page layout has mode and sections, persisted by page ID in `settings.sectionPages`. Section values and order have a design draft lifecycle distinct from rich-text/widget page drafts; publishing design does not publish pending page text. Theme switches preserve the map. To migrate a custom schema, transform a pulled draft to the new version, review it, and push it with its revision; incompatible versions never auto-convert silently. Existing widget pages continue using their original renderer.
60
60
 
61
61
  Images in template files should be imported using relative paths so Astro bundles them. Theme CSS can reference `url(assets/name.webp)`; ACMS resolves it through the bundled asset inventory. Brand images and editable section images use the media library. Packaged assets must be local files under assets; declare them in the manifest. Use `theme pack --optimize` to run current img-subir and rewrite references before packing. Original-image backup directories are excluded.
62
62
 
package/integration.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolveMobile } from './src/lib/mobile/config.mjs';
1
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
3
  import { dirname, join } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
@@ -32,6 +33,8 @@ const ROUTES = [
32
33
  ['/go/[id]', 'go/[id].ts'],
33
34
  ['/media/[...key]', 'media/[...key].ts'],
34
35
  ['/embed/[pageId]/[blockId]', 'embed/[pageId]/[blockId].ts'],
36
+ // one published section for mobile apps without a native component for it
37
+ ['/embed/section/[...path]', 'embed/section/[...path].astro'],
35
38
  ['/preview/[type]/[id]', 'preview/[type]/[id].astro'],
36
39
  ['/rss.xml', 'rss.xml.ts'],
37
40
  ['/sitemap.xml', 'sitemap.xml.ts'],
@@ -40,6 +43,7 @@ const ROUTES = [
40
43
  // admin
41
44
  ['/admin', 'admin/index.astro'],
42
45
  ['/admin/design', 'admin/design.astro'],
46
+ ['/admin/section-preview', 'admin/section-preview.astro'],
43
47
  ['/admin/links', 'admin/links.astro'],
44
48
  ['/admin/login', 'admin/login.astro'],
45
49
  ['/admin/logout', 'admin/logout.ts'],
@@ -82,6 +86,8 @@ const ROUTES = [
82
86
  ['/api/design/keys', 'api/design/keys.ts'],
83
87
  ['/api/design/capabilities', 'api/design/capabilities.ts'],
84
88
  ['/api/design/media', 'api/design/media.ts'],
89
+ ['/api/design/ai', 'api/design/ai.ts'],
90
+ ['/api/design/translate', 'api/design/translate.ts'],
85
91
  ['/api/agent', 'api/agent/index.ts'],
86
92
  ['/api/agent/docs', 'api/agent/docs.ts'],
87
93
  ['/api/mcp', 'api/mcp.ts'],
@@ -379,6 +385,7 @@ export const homes = import.meta.glob('/src/themes/*/templates/home.astro', { ea
379
385
  export const postTpls = import.meta.glob('/src/themes/*/templates/post-*.astro', { eager: true });
380
386
  export const pageTpls = import.meta.glob('/src/themes/*/templates/page-*.astro', { eager: true });
381
387
  export const eventTpls = import.meta.glob('/src/themes/*/templates/event.astro', { eager: true });
388
+ export const migrations = import.meta.glob('/src/themes/*/migrations/*.mjs', { eager: true });
382
389
  `;
383
390
 
384
391
  function acmsVitePlugin(userConfig) {
@@ -418,6 +425,13 @@ function acmsVitePlugin(userConfig) {
418
425
  * Home page content per language is authored in /admin as a normal page
419
426
  * marked "home" with that language selected. Omit for monolingual sites —
420
427
  * nothing changes.
428
+ * @param {{ios?: {teamId: string, bundleId: string, paths?: string[]}, android?: {package: string, sha256CertFingerprints: string[]}, push?: boolean | {enabled?: boolean, retentionDays?: number}, editor?: {redirectUris: string[], name?: string, accessTokenMinutes?: number, refreshTokenDays?: number}}} [options.mobile]
429
+ * Optional iOS Universal Links and Android App Links associations, and push
430
+ * notifications for the site's own Expo apps (`push: true`; off by default):
431
+ * /api/v1/devices, /api/v1/push/topics and Admin → Mobile app. `editor:
432
+ * { redirectUris, name? }` turns on editor apps: staff sign in through the
433
+ * browser (/admin/app-auth, OAuth 2.0 + PKCE; only the listed redirect URIs
434
+ * receive codes) and edit through the bearer-token /api/v1/admin/* API.
421
435
  * @param {{enabled?: boolean}} [options.search]
422
436
  * Public site search: the /search page, the /api/search JSON endpoint, and
423
437
  * the header search modal. On by default — set `enabled: false` to remove
@@ -441,6 +455,7 @@ export default function acms(options = {}) {
441
455
  ]);
442
456
  const i18n = resolveI18n(options.i18n, contentBases);
443
457
  const search = resolveSearch(options.search);
458
+ const mobile = resolveMobile(options.mobile);
444
459
  const calendar = resolveCalendar(options.calendar, contentBases, i18n.locales);
445
460
  const legal = { enabled: true, operatorName: '', contactEmail: '', updatedAt: '', ...(options.legal === false ? { enabled: false } : options.legal ?? {}) };
446
461
  if (typeof legal.enabled !== 'boolean' || ['operatorName','contactEmail','updatedAt'].some(k => typeof legal[k] !== 'string')) throw new Error('acms: invalid legal configuration');
@@ -453,13 +468,52 @@ export default function acms(options = {}) {
453
468
  content,
454
469
  i18n,
455
470
  search,
471
+ mobile,
456
472
  calendar,
457
473
  legal,
458
474
  };
459
475
  const routes = [...ROUTES,
476
+ ['/api/v1/site', 'api/v1/site.ts'],
477
+ ['/api/v1/theme', 'api/v1/theme.ts'],
478
+ ['/api/v1/pages/home', 'api/v1/pages/home.ts'],
479
+ ['/api/v1/pages/[...slug]', 'api/v1/pages/[...slug].ts'],
480
+ ['/api/v1/openapi.json', 'api/v1/openapi.json.ts'],
460
481
  ['/api/v1/content', 'api/v1/content/index.ts'],
461
482
  ['/api/v1/content/[id]', 'api/v1/content/[id].ts'],
462
483
  ];
484
+ if (mobile.ios) routes.push(['/.well-known/apple-app-site-association', '.well-known/apple-app-site-association.ts']);
485
+ if (mobile.android) routes.push(['/.well-known/assetlinks.json', '.well-known/assetlinks.json.ts']);
486
+ if (mobile.push.enabled) {
487
+ routes.push(
488
+ ['/api/v1/devices', 'api/v1/devices/index.ts'],
489
+ ['/api/v1/devices/[token]', 'api/v1/devices/[token].ts'],
490
+ ['/api/v1/push/topics', 'api/v1/push/topics.ts'],
491
+ ['/api/push/admin', 'api/push/admin.ts'],
492
+ ['/admin/mobile', 'admin/mobile.astro'],
493
+ );
494
+ }
495
+ if (mobile.editor.enabled) {
496
+ // editor apps: browser sign-in (OAuth 2.0 + PKCE) and the bearer-only admin API
497
+ routes.push(
498
+ ['/admin/app-auth', 'admin/app-auth.astro'],
499
+ ['/api/v1/auth/token', 'api/v1/auth/token.ts'],
500
+ ['/api/v1/auth/revoke', 'api/v1/auth/revoke.ts'],
501
+ ['/api/v1/admin/me', 'api/v1/admin/me.ts'],
502
+ ['/api/v1/admin/documents', 'api/v1/admin/documents/index.ts'],
503
+ ['/api/v1/admin/documents/[kind]/[id]', 'api/v1/admin/documents/[kind]/[id]/index.ts'],
504
+ ['/api/v1/admin/documents/[kind]/[id]/[action]', 'api/v1/admin/documents/[kind]/[id]/[action].ts'],
505
+ ['/api/v1/admin/documents/[kind]/[id]/translations/[locale]', 'api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts'],
506
+ ['/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action]', 'api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts'],
507
+ ['/api/v1/admin/sections', 'api/v1/admin/sections/index.ts'],
508
+ ['/api/v1/admin/sections/[pageId]/[sectionId]', 'api/v1/admin/sections/[pageId]/[sectionId]/index.ts'],
509
+ ['/api/v1/admin/sections/[pageId]/[sectionId]/publish', 'api/v1/admin/sections/[pageId]/[sectionId]/publish.ts'],
510
+ ['/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]', 'api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts'],
511
+ ['/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action]', 'api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts'],
512
+ ['/api/v1/admin/media', 'api/v1/admin/media/index.ts'],
513
+ ['/api/v1/admin/bookings', 'api/v1/admin/bookings/index.ts'],
514
+ ['/api/v1/admin/bookings/[id]/[action]', 'api/v1/admin/bookings/[id]/[action].ts'],
515
+ );
516
+ }
463
517
  if (legal.enabled) routes.push(['/privacy', 'legal/privacy.astro'], ['/terms', 'legal/terms.astro']);
464
518
  for (const c of Object.values(content.collections)) {
465
519
  routes.push([c.base, 'blog/index.astro'], [`${c.base}/[slug]`, 'blog/[slug].astro']);
@@ -529,7 +583,7 @@ export default function acms(options = {}) {
529
583
  if (config.root) {
530
584
  const discoveryDir = fileURLToPath(new URL('.astro/', config.root));
531
585
  mkdirSync(discoveryDir, {recursive:true});
532
- writeFileSync(join(discoveryDir,'acms-project.json'), JSON.stringify({ themeApiVersion:1, collections:userConfig.content.collections, calendar:userConfig.calendar }));
586
+ writeFileSync(join(discoveryDir,'acms-project.json'), JSON.stringify({ themeApiVersion:1, collections:userConfig.content.collections, calendar:userConfig.calendar, mobile:userConfig.mobile ?? {} }));
533
587
  }
534
588
  updateConfig({ vite: { plugins: [acmsVitePlugin(userConfig)] } });
535
589
 
@@ -0,0 +1,97 @@
1
+ # Agent rules for this app
2
+
3
+ Full guide: run `npx acms docs mobile` from the **site** folder (the folder
4
+ that contains this app; all `acms` commands run there, not in this folder). Finish
5
+ every change with `npx acms mobile doctor` (no failures).
6
+
7
+ This is the **client app** for the ACMS site at `__ACMS_SITE_URL__`: an Expo
8
+ (React Native) app for iOS, Android and web that shows the site's content. The
9
+ site owner edits everything in the site admin; the app only reads the public
10
+ API. It lives beside the website but is a separate project (its own
11
+ `package.json` and `node_modules`), not part of the site build.
12
+
13
+ ## Rules
14
+
15
+ 1. **Use the ACMS hooks, never raw fetch.** Import from `@acms-subir/native`
16
+ (`useSite`, `useTheme`, `useLocale`, `usePage`, `usePosts`, `usePost`,
17
+ `useEvents`, `useEvent`, `useResources`, `useAvailability`, `useBooking`,
18
+ `<Sections>`, `<RichText>`, `<EventDate>`). For anything else use the typed
19
+ client from `useAcms()` (`@acms-subir/client`) inside a TanStack Query hook.
20
+ 2. **Never hardcode content.** Names, times, text, events, prices, people and
21
+ images come from the site. If the app needs something the owner should be
22
+ able to change, it must be a section field or content in the admin, not a
23
+ string in this code. Only UI chrome ("Try again", "Book") belongs here.
24
+ 3. **Public API only.** No admin routes, no database, no agent or integration
25
+ keys, no admin password. There are no secrets in a mobile app:
26
+ `EXPO_PUBLIC_*` values are bundled and readable by anyone.
27
+ 4. **Style from the site's theme.** Colors, fonts and radius come from
28
+ `useTheme()` (`t.colors`, `t.styles`). Support light and dark mode. No
29
+ pill-shaped chips or badges (use 10–12px radius rectangles or plain text),
30
+ no colored left-edge accent stripes, no gradient-squircle logos.
31
+ 5. **Phones and tablets.** Use `useLayout()` / `useWindowDimensions` for
32
+ columns and gutters; never fixed widths. Content should use the width on a
33
+ tablet, not a stretched phone column.
34
+ 6. **Bookings are sensitive.** `useBooking()` handles idempotency. Never put a
35
+ booking's manage token, email or idempotency key in a URL, log or analytics
36
+ event; store the manage token with `expo-secure-store` if you keep it.
37
+ 7. **Links:** section links may be root-relative. Open them with
38
+ `useOpenLink()` (site pages go through `onNavigate`), or resolve with
39
+ `resolveUrl(href, baseUrl)`. To open your own screens for site pages, pass
40
+ `onNavigate={(path, url) => …}` to `<AcmsProvider>` in `app/_layout.tsx`
41
+ (e.g. `router.push` for `/events/<slug>` when the events recipe is in).
42
+ 8. **Sections mirror the website.** A native section uses the same fields and
43
+ variants as the site's `src/themes/<theme>/sections/*.astro` component and
44
+ renders empty fields the same way.
45
+
46
+ ## Where things are
47
+
48
+ - `app/_layout.tsx` wires `<AcmsProvider>` (site URL from `EXPO_PUBLIC_ACMS_URL`),
49
+ theme colors and the language switch.
50
+ - `app/index.tsx` renders the homepage sections with pull to refresh.
51
+ - `sections/index.ts` is this app's section registry, passed to
52
+ `<Sections registry={sections}>`. Keys are a section type or `type:variant`.
53
+ - Each section renders with the first match of: `sections/index.ts` → the
54
+ packaged library components (`@acms-subir/native/sections`: hero-banner,
55
+ service-times, upcoming-events, feature-grid, team, pricing, gallery,
56
+ questions, cta-band, contact, latest-posts) → the website's rendering in a
57
+ web view (react-native-webview; an iframe on web) → a generic view.
58
+ - To change a library section, get an editable copy (from the site folder):
59
+ `npx acms section add team --theme <slug> --native` → `sections/Team.tsx`.
60
+ - New coded sections: `npx acms section new <type> --theme <slug>` writes the
61
+ web component and a typed native stub here (`sections/<Type>.tsx`, types in
62
+ `sections/types.ts`). Finish the stub so it matches the web version.
63
+ - `npx acms check` lists sections that only show as a web view; give the ones
64
+ people use most a native component. Build yours from the exported pieces
65
+ (`Grid`, `FocalImage`, `SectionHeader`, `ActionButton`, `TextLink`,
66
+ `useSectionMetrics`) so they stay responsive and themed.
67
+ - Images: pass `<key>Focus` to `FocalImage` so crops keep the owner's focal point.
68
+
69
+ ## Recipes
70
+
71
+ Add ready-made screens from the site folder instead of writing them from scratch:
72
+
73
+ ```sh
74
+ npx acms mobile recipe list
75
+ npx acms mobile recipe add events # events list, detail, add to calendar
76
+ npx acms mobile recipe add news # posts list and article
77
+ npx acms mobile recipe add booking # resources → slots → form → confirmation
78
+ npx acms mobile recipe add push # notifications: turn on, topics, taps (site needs push on)
79
+ ```
80
+
81
+ Each recipe prints the extra Expo packages to install (`npx expo install ...`).
82
+
83
+ ## Before you finish
84
+
85
+ - `npm run typecheck` passes.
86
+ - Check iOS and Android (and `npm run web`), in both light and dark mode, on a
87
+ phone-sized and a tablet-sized screen. Compare the home screen with the
88
+ website: same sections, same order, same words.
89
+ - `npx expo export --platform web` bundles without errors.
90
+ - `npx acms mobile dev` (from the site folder) runs the site and this app so a
91
+ real phone on the same network or Tailscale can reach it.
92
+ - `npx acms mobile doctor` (from the site folder) passes: packages match the
93
+ site, the site address works, no secrets, sections covered, recipe
94
+ packages installed. Read its warnings too.
95
+ - If the task needs content the site doesn't have yet (a page, a calendar),
96
+ read it by slug anyway, show an empty state, and tell the owner what to
97
+ create in the admin. Don't hardcode it.
@@ -0,0 +1,65 @@
1
+ # __ACMS_APP_NAME__
2
+
3
+ A blank Expo app connected to your ACMS site (`__ACMS_SITE_URL__`). It already
4
+ shows your homepage sections, follows your site's Design colors in light and
5
+ dark mode, and switches language when your site has more than one. Everything
6
+ else is yours to build.
7
+
8
+ ## Run it
9
+
10
+ From your **site** folder (starts the site too, and points the app at an
11
+ address your phone can reach):
12
+
13
+ ```sh
14
+ cd __ACMS_APP_DIR__ && npm install && cd ..
15
+ npx acms mobile dev
16
+ ```
17
+
18
+ Scan the QR code with Expo Go, or press `i` / `a` / `w` for the iOS simulator,
19
+ Android emulator or a browser. Or run the app on its own:
20
+
21
+ ```sh
22
+ npm start
23
+ ```
24
+
25
+ The site address comes from `EXPO_PUBLIC_ACMS_URL` in `.env` (your production
26
+ site) and `.env.local` (written by `acms mobile dev`, not committed).
27
+
28
+ ## Add screens
29
+
30
+ ```sh
31
+ npx acms mobile recipe add events # also: news, booking, push
32
+ ```
33
+
34
+ Build your own with the hooks from `@acms-subir/native`. `AGENTS.md` has the
35
+ rules coding agents follow here; `npx acms docs mobile` (from the site folder)
36
+ is the full guide with a cookbook (tabs, events, native sections, booking).
37
+
38
+ ## Check it
39
+
40
+ From the site folder:
41
+
42
+ ```sh
43
+ npx acms mobile doctor # --offline skips network checks, --json for tools
44
+ ```
45
+
46
+ It checks the ACMS packages match your site's version, the site address works
47
+ (and isn't localhost for a phone), app ids and deep links, sections the app
48
+ can't show natively, secrets in the code, and recipe packages. It exits 1 when
49
+ something fails.
50
+
51
+ ## Ship it
52
+
53
+ Builds and store submission use [EAS](https://docs.expo.dev/build/introduction/):
54
+
55
+ ```sh
56
+ npx eas-cli build --profile preview --platform all # installable test builds
57
+ npx eas-cli build --profile production --platform all
58
+ npx eas-cli submit --platform ios
59
+ ```
60
+
61
+ Before your first store build, run `npx acms mobile doctor` and check
62
+ `app.json`: app name, `ios.bundleIdentifier`
63
+ (`__ACMS_BUNDLE_ID__`) and `android.package` can't change after release. To open
64
+ site links in the app, set `acms({ mobile: { ios, android } })` in the site's
65
+ `astro.config.mjs` (see the site's mobile API docs).
@@ -0,0 +1,56 @@
1
+ import { useMemo } from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import { DarkTheme, DefaultTheme, Stack, ThemeProvider } from 'expo-router';
4
+ import { StatusBar } from 'expo-status-bar';
5
+ import { AcmsProvider, useSite, useTheme } from '@acms-subir/native';
6
+ import { LanguageSwitch } from '../components/LanguageSwitch';
7
+
8
+ // Set in .env (production site) or .env.local (written by `npx acms mobile dev`).
9
+ const baseUrl = process.env.EXPO_PUBLIC_ACMS_URL;
10
+
11
+ export default function RootLayout() {
12
+ if (!baseUrl) return <MissingUrl />;
13
+ return (
14
+ <AcmsProvider baseUrl={baseUrl}>
15
+ <ThemedStack />
16
+ </AcmsProvider>
17
+ );
18
+ }
19
+
20
+ // Navigation chrome follows the site's Design tokens in light and dark mode.
21
+ function ThemedStack() {
22
+ const t = useTheme();
23
+ const { data: site } = useSite();
24
+ const navTheme = useMemo(() => {
25
+ const base = t.scheme === 'dark' ? DarkTheme : DefaultTheme;
26
+ return { ...base, colors: { ...base.colors, primary: t.colors.accent, background: t.colors.bg, card: t.colors.bg, text: t.colors.ink, border: t.colors.line, notification: t.colors.accent } };
27
+ }, [t]);
28
+ return (
29
+ <ThemeProvider value={navTheme}>
30
+ <StatusBar style={t.scheme === 'dark' ? 'light' : 'dark'} />
31
+ <Stack
32
+ screenOptions={{
33
+ headerStyle: { backgroundColor: t.colors.bg },
34
+ headerTintColor: t.colors.ink,
35
+ headerTitleStyle: { fontFamily: t.styles.fonts.heading },
36
+ headerShadowVisible: false,
37
+ contentStyle: { backgroundColor: t.colors.bg },
38
+ headerRight: () => <LanguageSwitch />,
39
+ }}
40
+ >
41
+ <Stack.Screen name="index" options={{ title: site?.name ?? '' }} />
42
+ </Stack>
43
+ </ThemeProvider>
44
+ );
45
+ }
46
+
47
+ function MissingUrl() {
48
+ return (
49
+ <View style={{ flex: 1, justifyContent: 'center', padding: 24, gap: 8 }}>
50
+ <Text style={{ fontSize: 18, fontWeight: '600' }}>Connect your site</Text>
51
+ <Text style={{ fontSize: 15, lineHeight: 22 }}>
52
+ Set EXPO_PUBLIC_ACMS_URL in .env (for example https://example.com), or run `npx acms mobile dev` from your site folder, then restart Expo.
53
+ </Text>
54
+ </View>
55
+ );
56
+ }
@@ -0,0 +1,21 @@
1
+ import { RefreshControl, ScrollView } from 'react-native';
2
+ import { Sections, useRefresh, useTheme } from '@acms-subir/native';
3
+ import { sections } from '../sections';
4
+
5
+ // The home screen shows the site's published homepage sections. Owners edit
6
+ // them in the site admin (Design → Pages); the app picks changes up on refresh.
7
+ // Each section renders with: your components in sections/ → the packaged
8
+ // library components → the website's rendering (web view) → a generic view.
9
+ export default function Home() {
10
+ const t = useTheme();
11
+ const { refreshing, refresh } = useRefresh();
12
+ return (
13
+ <ScrollView
14
+ style={{ flex: 1, backgroundColor: t.colors.bg }}
15
+ contentInsetAdjustmentBehavior="automatic"
16
+ refreshControl={<RefreshControl refreshing={refreshing} onRefresh={refresh} tintColor={t.colors.muted} />}
17
+ >
18
+ <Sections page="home" registry={sections} />
19
+ </ScrollView>
20
+ );
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "expo": {
3
+ "name": "__ACMS_APP_NAME__",
4
+ "slug": "__ACMS_APP_SLUG__",
5
+ "version": "1.0.0",
6
+ "scheme": "__ACMS_APP_SCHEME__",
7
+ "orientation": "default",
8
+ "userInterfaceStyle": "automatic",
9
+ "ios": {
10
+ "bundleIdentifier": "__ACMS_BUNDLE_ID__",
11
+ "supportsTablet": true
12
+ },
13
+ "android": {
14
+ "package": "__ACMS_BUNDLE_ID__"
15
+ },
16
+ "web": {
17
+ "output": "single"
18
+ },
19
+ "plugins": ["expo-router"]
20
+ }
21
+ }
@@ -0,0 +1,31 @@
1
+ import { Pressable, Text, View } from 'react-native';
2
+ import { useLocale, useTheme } from '@acms-subir/native';
3
+
4
+ // Text buttons ("EN ES") for sites with more than one language.
5
+ export function LanguageSwitch() {
6
+ const t = useTheme();
7
+ const { locale, languages, setLocale } = useLocale();
8
+ if (languages.length < 2) return null;
9
+ return (
10
+ <View style={{ flexDirection: 'row', gap: 4 }}>
11
+ {languages.map((l) => {
12
+ const active = l.code === locale;
13
+ return (
14
+ <Pressable
15
+ key={l.code}
16
+ onPress={() => setLocale(l.code)}
17
+ accessibilityRole="button"
18
+ accessibilityState={{ selected: active }}
19
+ accessibilityLabel={l.name}
20
+ hitSlop={8}
21
+ style={{ paddingHorizontal: 6, paddingVertical: 4 }}
22
+ >
23
+ <Text style={{ color: active ? t.colors.ink : t.colors.muted, fontWeight: active ? '700' : '500', fontSize: 14 }}>
24
+ {l.code.toUpperCase()}
25
+ </Text>
26
+ </Pressable>
27
+ );
28
+ })}
29
+ </View>
30
+ );
31
+ }