@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.
- package/CHANGELOG.md +105 -0
- package/cli/common.mjs +2 -2
- package/cli/legacy.mjs +1 -1
- package/cli/main.mjs +82 -8
- package/cli/mobile-doctor.mjs +430 -0
- package/cli/mobile.mjs +315 -0
- package/cli/native-sections.mjs +168 -0
- package/cli/preview.mjs +8 -4
- package/cli/sections.mjs +188 -0
- package/cli/themes.mjs +52 -8
- package/docs/mobile.md +527 -0
- package/docs/sections.md +203 -0
- package/docs/theme-authoring.md +19 -3
- package/integration.mjs +76 -5
- package/mobile-template/client/AGENTS.md +97 -0
- package/mobile-template/client/README.md +65 -0
- package/mobile-template/client/app/_layout.tsx +56 -0
- package/mobile-template/client/app/index.tsx +21 -0
- package/mobile-template/client/app.json +21 -0
- package/mobile-template/client/components/LanguageSwitch.tsx +31 -0
- package/mobile-template/client/eas.json +21 -0
- package/mobile-template/client/env.example +3 -0
- package/mobile-template/client/gitignore +42 -0
- package/mobile-template/client/package.json +38 -0
- package/mobile-template/client/sections/index.ts +11 -0
- package/mobile-template/client/tsconfig.json +7 -0
- package/mobile-template/editor/AGENTS.md +66 -0
- package/mobile-template/editor/README.md +73 -0
- package/mobile-template/editor/app/_layout.tsx +76 -0
- package/mobile-template/editor/app/auth.tsx +13 -0
- package/mobile-template/editor/app/index.tsx +55 -0
- package/mobile-template/editor/app.json +21 -0
- package/mobile-template/editor/components/Screen.tsx +51 -0
- package/mobile-template/editor/components/SignIn.tsx +36 -0
- package/mobile-template/editor/components/TextTabs.tsx +27 -0
- package/mobile-template/editor/eas.json +21 -0
- package/mobile-template/editor/env.example +5 -0
- package/mobile-template/editor/gitignore +42 -0
- package/mobile-template/editor/lib/appearance.tsx +12 -0
- package/mobile-template/editor/lib/config.ts +17 -0
- package/mobile-template/editor/lib/format.ts +24 -0
- package/mobile-template/editor/lib/tools.ts +16 -0
- package/mobile-template/editor/package.json +40 -0
- package/mobile-template/editor/tools/README.md +2 -0
- package/mobile-template/editor/tsconfig.json +7 -0
- package/mobile-template/recipes/booking/files/app/book/[resource].tsx +57 -0
- package/mobile-template/recipes/booking/files/app/book/details.tsx +105 -0
- package/mobile-template/recipes/booking/files/app/book/index.tsx +38 -0
- package/mobile-template/recipes/booking/recipe.json +17 -0
- package/mobile-template/recipes/editor-bookings/files/app/bookings/index.tsx +113 -0
- package/mobile-template/recipes/editor-bookings/files/tools/bookings.ts +1 -0
- package/mobile-template/recipes/editor-bookings/recipe.json +8 -0
- package/mobile-template/recipes/editor-content/files/app/content/[kind]/[id].tsx +13 -0
- package/mobile-template/recipes/editor-content/files/app/content/index.tsx +76 -0
- package/mobile-template/recipes/editor-content/files/components/DocumentDetail.tsx +55 -0
- package/mobile-template/recipes/editor-content/files/tools/content.ts +1 -0
- package/mobile-template/recipes/editor-content/recipe.json +8 -0
- package/mobile-template/recipes/editor-media/files/app/media/index.tsx +73 -0
- package/mobile-template/recipes/editor-media/files/tools/media.ts +1 -0
- package/mobile-template/recipes/editor-media/recipe.json +8 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/[pageId]/[sectionId].tsx +12 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/index.tsx +48 -0
- package/mobile-template/recipes/editor-sections/files/components/SectionEditorPane.tsx +77 -0
- package/mobile-template/recipes/editor-sections/files/tools/sections.ts +1 -0
- package/mobile-template/recipes/editor-sections/recipe.json +8 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/index.tsx +63 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/review.tsx +14 -0
- package/mobile-template/recipes/editor-translations/files/components/ReviewPane.tsx +72 -0
- package/mobile-template/recipes/editor-translations/files/tools/translations.ts +1 -0
- package/mobile-template/recipes/editor-translations/recipe.json +8 -0
- package/mobile-template/recipes/editor-writer/files/app/write/[kind]/[id].tsx +132 -0
- package/mobile-template/recipes/editor-writer/files/app/write/new.tsx +61 -0
- package/mobile-template/recipes/editor-writer/files/tools/writer.ts +1 -0
- package/mobile-template/recipes/editor-writer/recipe.json +8 -0
- package/mobile-template/recipes/events/files/app/events/[slug].tsx +41 -0
- package/mobile-template/recipes/events/files/app/events/index.tsx +50 -0
- package/mobile-template/recipes/events/files/lib/addToCalendar.ts +27 -0
- package/mobile-template/recipes/events/recipe.json +15 -0
- package/mobile-template/recipes/news/files/app/news/[slug].tsx +25 -0
- package/mobile-template/recipes/news/files/app/news/index.tsx +50 -0
- package/mobile-template/recipes/news/recipe.json +13 -0
- package/mobile-template/recipes/push/files/app/notifications.tsx +64 -0
- package/mobile-template/recipes/push/files/components/PushSetup.tsx +15 -0
- package/mobile-template/recipes/push/files/lib/push.ts +15 -0
- package/mobile-template/recipes/push/recipe.json +20 -0
- package/package.json +17 -13
- package/section-library/README.md +16 -0
- package/section-library/contact/Contact.astro +49 -0
- package/section-library/contact/section.json +106 -0
- package/section-library/cta-band/CtaBand.astro +28 -0
- package/section-library/cta-band/section.json +51 -0
- package/section-library/feature-grid/FeatureGrid.astro +54 -0
- package/section-library/feature-grid/section.json +107 -0
- package/section-library/gallery/Gallery.astro +29 -0
- package/section-library/gallery/section.json +51 -0
- package/section-library/hero-banner/HeroBanner.astro +42 -0
- package/section-library/hero-banner/section.json +81 -0
- package/section-library/latest-posts/LatestPosts.astro +22 -0
- package/section-library/latest-posts/section.json +37 -0
- package/section-library/pricing/Pricing.astro +40 -0
- package/section-library/pricing/section.json +117 -0
- package/section-library/questions/Questions.astro +38 -0
- package/section-library/questions/section.json +69 -0
- package/section-library/service-times/ServiceTimes.astro +42 -0
- package/section-library/service-times/section.json +127 -0
- package/section-library/team/Team.astro +45 -0
- package/section-library/team/section.json +88 -0
- package/section-library/upcoming-events/UpcomingEvents.astro +54 -0
- package/section-library/upcoming-events/section.json +51 -0
- package/src/actions/index.ts +205 -174
- package/src/components/AiSettings.tsx +45 -5
- package/src/components/DesignManager.tsx +112 -16
- package/src/components/MobileApp.tsx +392 -0
- package/src/components/ThemeSections.astro +40 -21
- package/src/components/ThemeSectionsEditor.tsx +757 -229
- package/src/components/booking/BookingFlow.astro +6 -3
- package/src/components/calendar/BookingsHead.astro +51 -0
- package/src/components/calendar/CalendarBoard.tsx +618 -0
- package/src/components/calendar/MiniMonth.tsx +54 -0
- package/src/components/calendar/NewEventDialog.tsx +44 -0
- package/src/components/calendar/QuickAdd.tsx +263 -0
- package/src/components/calendar/RoomHolds.tsx +203 -0
- package/src/components/calendar/WhereItLands.tsx +51 -0
- package/src/components/editor/EventPanel.tsx +23 -2
- package/src/components/editor/PostEditor.tsx +206 -14
- package/src/components/import/ImportExport.tsx +4 -1
- package/src/components/import/runner.ts +1 -0
- package/src/components/sections/SectionFields.tsx +560 -0
- package/src/components/sections/SectionPicker.tsx +120 -0
- package/src/components/sections/TranslationPanel.tsx +143 -0
- package/src/components/sections/checks.ts +64 -0
- package/src/layouts/Admin.astro +11 -7
- package/src/layouts/Site.astro +136 -21
- package/src/lib/agent-discovery.ts +11 -7
- package/src/lib/agent.ts +35 -2
- package/src/lib/ai-schema.ts +5 -0
- package/src/lib/ai.ts +5 -0
- package/src/lib/api.ts +12 -0
- package/src/lib/backup/content.ts +6 -1
- package/src/lib/booking/agent.ts +3 -2
- package/src/lib/booking/approval.ts +70 -0
- package/src/lib/booking/holds.ts +120 -0
- package/src/lib/booking/service.ts +37 -3
- package/src/lib/calendar/agent-schema.ts +9 -1
- package/src/lib/calendar/agent.ts +16 -1
- package/src/lib/calendar/board.ts +171 -0
- package/src/lib/calendar/config.ts +3 -0
- package/src/lib/calendar/grid.ts +169 -0
- package/src/lib/calendar/migration.ts +16 -0
- package/src/lib/calendar/render.ts +9 -7
- package/src/lib/calendar/schedule.ts +166 -0
- package/src/lib/content-service.ts +203 -0
- package/src/lib/design-service.ts +153 -5
- package/src/lib/drafts.ts +3 -3
- package/src/lib/editor/auth.ts +264 -0
- package/src/lib/editor/bookings.ts +60 -0
- package/src/lib/editor/config.ts +25 -0
- package/src/lib/editor/documents.ts +227 -0
- package/src/lib/editor/http.ts +48 -0
- package/src/lib/editor/me.ts +30 -0
- package/src/lib/editor/migration.ts +57 -0
- package/src/lib/editor/next.ts +11 -0
- package/src/lib/editor/openapi.ts +124 -0
- package/src/lib/editor/schemas.ts +91 -0
- package/src/lib/editor/sections.ts +96 -0
- package/src/lib/editor/store.ts +184 -0
- package/src/lib/editor/token-route.ts +74 -0
- package/src/lib/editor/translations.ts +163 -0
- package/src/lib/email/accounts.ts +2 -1
- package/src/lib/email/types.ts +2 -1
- package/src/lib/home-source.ts +34 -0
- package/src/lib/i18n.ts +34 -0
- package/src/lib/import/apply.ts +31 -2
- package/src/lib/import/bundle.ts +4 -0
- package/src/lib/import/export.ts +1 -0
- package/src/lib/import/ghost.ts +1 -0
- package/src/lib/media/upload.ts +83 -0
- package/src/lib/mobile/associations.ts +25 -0
- package/src/lib/mobile/calendar.ts +26 -14
- package/src/lib/mobile/config.mjs +59 -0
- package/src/lib/mobile/content.ts +18 -13
- package/src/lib/mobile/embed.ts +83 -0
- package/src/lib/mobile/http.ts +14 -6
- package/src/lib/mobile/locale.ts +18 -0
- package/src/lib/mobile/openapi.ts +111 -0
- package/src/lib/mobile/pages.ts +71 -0
- package/src/lib/mobile/route.ts +3 -3
- package/src/lib/mobile/schemas.ts +60 -0
- package/src/lib/mobile/site.ts +41 -0
- package/src/lib/push/api.ts +158 -0
- package/src/lib/push/config.ts +20 -0
- package/src/lib/push/expo.ts +76 -0
- package/src/lib/push/migration.ts +60 -0
- package/src/lib/push/service.ts +302 -0
- package/src/lib/push/store.ts +144 -0
- package/src/lib/push/types.ts +43 -0
- package/src/lib/repo/migrations.ts +14 -0
- package/src/lib/repo/sql.ts +65 -20
- package/src/lib/repo/types.ts +10 -4
- package/src/lib/roles.ts +5 -0
- package/src/lib/scheduler.ts +6 -1
- package/src/lib/schema.ts +76 -1
- package/src/lib/section-ai.ts +55 -0
- package/src/lib/section-targets.ts +62 -0
- package/src/lib/sections-agent.ts +146 -0
- package/src/lib/sections.ts +242 -0
- package/src/lib/site-chrome.ts +34 -0
- package/src/lib/theme-assets.ts +16 -0
- package/src/lib/theme.ts +33 -4
- package/src/lib/translate.ts +97 -0
- package/src/lib/translations.ts +39 -0
- package/src/middleware.ts +38 -2
- package/src/pages/.well-known/apple-app-site-association.ts +6 -0
- package/src/pages/.well-known/assetlinks.json.ts +6 -0
- package/src/pages/[...slug].astro +8 -3
- package/src/pages/admin/app-auth.astro +159 -0
- package/src/pages/admin/c/[collection]/[id].astro +9 -0
- package/src/pages/admin/calendar/bookings.astro +32 -26
- package/src/pages/admin/calendar/calendars.astro +4 -263
- package/src/pages/admin/calendar/events/[id].astro +9 -0
- package/src/pages/admin/calendar/events/index.astro +41 -83
- package/src/pages/admin/calendar/index.astro +43 -0
- package/src/pages/admin/calendar/resources/[id].astro +27 -5
- package/src/pages/admin/calendar/resources/index.astro +16 -17
- package/src/pages/admin/calendar/resources/link.astro +52 -12
- package/src/pages/admin/calendar/settings.astro +458 -0
- package/src/pages/admin/integrations.astro +5 -11
- package/src/pages/admin/login.astro +6 -2
- package/src/pages/admin/logout.ts +5 -2
- package/src/pages/admin/mobile.astro +18 -0
- package/src/pages/admin/pages/[id].astro +62 -0
- package/src/pages/admin/section-preview.astro +61 -0
- package/src/pages/admin/users.astro +107 -11
- package/src/pages/api/agent/docs.ts +6 -2
- package/src/pages/api/agent/index.ts +1 -1
- package/src/pages/api/ai/settings.ts +7 -4
- package/src/pages/api/design/ai.ts +15 -0
- package/src/pages/api/design/index.ts +12 -42
- package/src/pages/api/design/translate.ts +15 -0
- package/src/pages/api/live/thumb.ts +1 -1
- package/src/pages/api/mcp.ts +2 -2
- package/src/pages/api/media/upload.ts +10 -78
- package/src/pages/api/push/admin.ts +27 -0
- package/src/pages/api/v1/admin/bookings/[id]/[action].ts +14 -0
- package/src/pages/api/v1/admin/bookings/index.ts +6 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/[action].ts +17 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/index.ts +20 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts +26 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts +18 -0
- package/src/pages/api/v1/admin/documents/index.ts +13 -0
- package/src/pages/api/v1/admin/me.ts +6 -0
- package/src/pages/api/v1/admin/media/index.ts +32 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/publish.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts +22 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/index.ts +6 -0
- package/src/pages/api/v1/auth/revoke.ts +5 -0
- package/src/pages/api/v1/auth/token.ts +5 -0
- package/src/pages/api/v1/devices/[token].ts +20 -0
- package/src/pages/api/v1/devices/index.ts +17 -0
- package/src/pages/api/v1/openapi.json.ts +7 -0
- package/src/pages/api/v1/pages/[...slug].ts +7 -0
- package/src/pages/api/v1/pages/home.ts +7 -0
- package/src/pages/api/v1/push/topics.ts +7 -0
- package/src/pages/api/v1/site.ts +7 -0
- package/src/pages/api/v1/theme.ts +7 -0
- package/src/pages/blog/[slug].astro +8 -2
- package/src/pages/blog/index.astro +11 -4
- package/src/pages/book/[resource].ics.ts +7 -1
- package/src/pages/embed/section/[...path].astro +42 -0
- package/src/pages/events/[slug].astro +7 -2
- package/src/pages/index.astro +11 -5
- package/src/pages/sitemap.xml.ts +8 -0
- package/src/styles/admin.css +117 -0
- package/src/styles/cards.css +5 -1
- package/src/styles/editor.css +57 -0
- package/src/styles/mobile-app.css +151 -0
- package/src/styles/schedule.css +325 -0
- package/src/styles/sections.css +21 -12
- package/src/styles/theme-editor.css +255 -0
- package/src/theme-sdk/contract.d.mts +2193 -89
- package/src/theme-sdk/contract.mjs +77 -20
- package/src/theme-sdk/fields.d.mts +88 -0
- package/src/theme-sdk/fields.mjs +330 -0
- package/src/theme-sdk/index.ts +14 -2
- package/src/theme-sdk/tokens.d.mts +11 -0
- package/src/virtual.d.ts +4 -0
- package/src/pages/admin/calendar/index.ts +0 -4
package/docs/sections.md
ADDED
|
@@ -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.
|
package/docs/theme-authoring.md
CHANGED
|
@@ -32,9 +32,25 @@ const { page, posts } = Astro.props;
|
|
|
32
32
|
{posts.map(post => <PostCard href={collectionUrl(post.collection, post.slug)} title={post.title} excerpt={post.excerpt} />)}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Public types: HomeProps (settings, page, posts, tags), PageProps (settings, page, html), PostProps (settings, post, html, authors, related), EventProps (settings, event, occurrence, calendars, html, upcoming, origin, preview, rsvp), SectionProps (settings, page, variant, data). Public components: PostCard with card/row/feature variants, Pager with base/page/count, TagLabel with name/href/accent. Helpers include collectionUrl, postUrl, postsIndexUrl, tagUrl, formatDate, slugify, authorSlug, localeFromPath, localeHomeUrl and CONTENT. Use collectionUrl for non-default collections; postUrl is the built-in posts alias.
|
|
35
|
+
Public types: ChromeProps (site), HomeProps (settings, page, posts, tags), PageProps (settings, page, html), PostProps (settings, post, html, authors, related), EventProps (settings, event, occurrence, calendars, html, upcoming, origin, preview, rsvp), SectionProps (settings, page, variant, data). Public components: PostCard with card/row/feature variants, Pager with base/page/count, TagLabel with name/href/accent. Helpers include collectionUrl, postUrl, postsIndexUrl, tagUrl, formatDate, slugify, authorSlug, localeFromPath, localeHomeUrl and CONTENT. Use collectionUrl for non-default collections; postUrl is the built-in posts alias.
|
|
36
36
|
|
|
37
|
-
Template filenames: `home.astro`, `event.astro`, `post-<name>.astro`, `page-<name>.astro`.
|
|
37
|
+
Template filenames: `home.astro`, `event.astro`, `header.astro`, `footer.astro`, `post-<name>.astro`, `page-<name>.astro`.
|
|
38
|
+
|
|
39
|
+
`header.astro` and `footer.astro` replace the site header and footer on every public page. They receive `ChromeProps` — one `site` prop (`SiteChrome`) with everything already resolved for the request: `title`, `tagline`, `logo`, `homeUrl`, `links` (header nav, each `{ label, url, current, external }`, already localized), `footerLinks`, `social` (with inline SVG `icon`), `readLinks`, `footerHtml`, `languages` (`{ code, label, url, current }`, where `label` is the language's own name, e.g. "Español"), `search` (`{ url, label }` or null), `schemeToggle`, `liveHeader`, `headerStyle`, `footerStyle`. Core behaviors attach through data hooks, so keep them on your own elements: `data-search-open` (opens search), `data-scheme-toggle` (light/dark), `data-menu-open` (phone menu sheet, built from `site.links`) and a hidden `data-live-header` link (shown during a live stream).
|
|
40
|
+
|
|
41
|
+
```astro
|
|
42
|
+
---
|
|
43
|
+
import type { ChromeProps } from '@acms-subir/core/theme';
|
|
44
|
+
const { site } = Astro.props as ChromeProps;
|
|
45
|
+
---
|
|
46
|
+
<header class="gh-head">
|
|
47
|
+
<a href={site.homeUrl}>{site.logo ? <img src={site.logo} alt={site.title} /> : site.title}</a>
|
|
48
|
+
<nav>{site.links.map((l) => <a href={l.url} aria-current={l.current ? 'page' : undefined}>{l.label}</a>)}</nav>
|
|
49
|
+
{site.languages.map((l) => <a href={l.url} lang={l.code} aria-current={l.current ? 'true' : undefined}>{l.label}</a>)}
|
|
50
|
+
{site.search && <a href={site.search.url} data-search-open>{site.search.label}</a>}
|
|
51
|
+
<button type="button" data-menu-open aria-label="Open menu">Menu</button>
|
|
52
|
+
</header>
|
|
53
|
+
``` Post/page variants are selected in the content editor. Core owns routes, auth, SEO and published-content queries. Templates only replace their documented content area. Never query private data from a public section or insert untrusted strings with set:html. The core's rendered `html`/`bodyHtml` is the intended rich-content handoff.
|
|
38
54
|
|
|
39
55
|
## Editable sections
|
|
40
56
|
|
|
@@ -56,7 +72,7 @@ Custom definitions live in `theme.json`:
|
|
|
56
72
|
}
|
|
57
73
|
```
|
|
58
74
|
|
|
59
|
-
The component receives SectionProps.
|
|
75
|
+
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). An image field (or its default) may name a file the theme ships, like `assets/hero.jpg`; it resolves to that file's built URL on the website and in the mobile API. Post sources give each item `excerpt` (the post's own, else the first ~160 characters of its text) and `authors` (`{ name, url, avatar }`, public byline fields only). 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
76
|
|
|
61
77
|
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
78
|
|
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'],
|
|
@@ -251,7 +257,7 @@ function resolveSearch(search) {
|
|
|
251
257
|
const EVENT_FIELD_TYPES = ['text', 'textarea', 'number', 'url', 'select'];
|
|
252
258
|
function resolveCalendar(calendar, contentBases, locales) {
|
|
253
259
|
const defaults = {
|
|
254
|
-
enabled: true, base: '/calendar', eventsBase: '/events', bookings: true, bookBase: '/book',
|
|
260
|
+
enabled: true, base: '/calendar', eventsBase: '/events', bookings: true, bookBase: '/book', bookingApproval: 'auto',
|
|
255
261
|
resourceKinds: ['Room', 'Person', 'Vehicle', 'Equipment'], defaultView: 'month', eventFields: [],
|
|
256
262
|
};
|
|
257
263
|
if (calendar === false) return { ...defaults, enabled: false };
|
|
@@ -269,17 +275,31 @@ function resolveCalendar(calendar, contentBases, locales) {
|
|
|
269
275
|
}
|
|
270
276
|
const enabled = calendar.enabled ?? true;
|
|
271
277
|
if (typeof enabled !== 'boolean') throw new Error('acms: calendar.enabled must be true or false');
|
|
278
|
+
// bookings: true | false | { approval: 'auto' | 'rules' | 'manual' } — the
|
|
279
|
+
// object form turns bookings on and sets the starting approval default
|
|
280
|
+
// (Calendar settings → Bookings can change it later)
|
|
281
|
+
const bookingsOpt = calendar.bookings ?? true;
|
|
282
|
+
let bookingApproval = defaults.bookingApproval;
|
|
283
|
+
if (bookingsOpt && typeof bookingsOpt === 'object' && !Array.isArray(bookingsOpt)) {
|
|
284
|
+
const extra = Object.keys(bookingsOpt).filter((k) => k !== 'approval');
|
|
285
|
+
if (extra.length) throw new Error(`acms: unknown calendar.bookings key "${extra[0]}" — the only option is calendar.bookings.approval`);
|
|
286
|
+
bookingApproval = bookingsOpt.approval ?? defaults.bookingApproval;
|
|
287
|
+
if (!['auto', 'rules', 'manual'].includes(bookingApproval)) {
|
|
288
|
+
throw new Error(`acms: calendar.bookings.approval must be "auto", "rules", or "manual" — got "${bookingApproval}"`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
272
291
|
const resolved = {
|
|
273
292
|
enabled,
|
|
274
293
|
base: calendar.base ?? defaults.base,
|
|
275
294
|
eventsBase: calendar.eventsBase ?? defaults.eventsBase,
|
|
276
|
-
bookings:
|
|
295
|
+
bookings: typeof bookingsOpt === 'object' && bookingsOpt !== null ? true : bookingsOpt,
|
|
296
|
+
bookingApproval,
|
|
277
297
|
bookBase: calendar.bookBase ?? defaults.bookBase,
|
|
278
298
|
resourceKinds: calendar.resourceKinds ?? defaults.resourceKinds,
|
|
279
299
|
defaultView: calendar.defaultView ?? defaults.defaultView,
|
|
280
300
|
eventFields: [],
|
|
281
301
|
};
|
|
282
|
-
if (typeof resolved.bookings !== 'boolean') throw new Error('acms: calendar.bookings must be true or
|
|
302
|
+
if (typeof resolved.bookings !== 'boolean') throw new Error('acms: calendar.bookings must be true, false, or { approval }');
|
|
283
303
|
if (!Array.isArray(resolved.resourceKinds) || !resolved.resourceKinds.length || resolved.resourceKinds.some((k) => typeof k !== 'string' || !k.trim())) {
|
|
284
304
|
throw new Error('acms: calendar.resourceKinds must be a non-empty array of labels, e.g. ["Room", "Person"]');
|
|
285
305
|
}
|
|
@@ -379,6 +399,9 @@ export const homes = import.meta.glob('/src/themes/*/templates/home.astro', { ea
|
|
|
379
399
|
export const postTpls = import.meta.glob('/src/themes/*/templates/post-*.astro', { eager: true });
|
|
380
400
|
export const pageTpls = import.meta.glob('/src/themes/*/templates/page-*.astro', { eager: true });
|
|
381
401
|
export const eventTpls = import.meta.glob('/src/themes/*/templates/event.astro', { eager: true });
|
|
402
|
+
export const headerTpls = import.meta.glob('/src/themes/*/templates/header.astro', { eager: true });
|
|
403
|
+
export const footerTpls = import.meta.glob('/src/themes/*/templates/footer.astro', { eager: true });
|
|
404
|
+
export const migrations = import.meta.glob('/src/themes/*/migrations/*.mjs', { eager: true });
|
|
382
405
|
`;
|
|
383
406
|
|
|
384
407
|
function acmsVitePlugin(userConfig) {
|
|
@@ -418,6 +441,13 @@ function acmsVitePlugin(userConfig) {
|
|
|
418
441
|
* Home page content per language is authored in /admin as a normal page
|
|
419
442
|
* marked "home" with that language selected. Omit for monolingual sites —
|
|
420
443
|
* nothing changes.
|
|
444
|
+
* @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]
|
|
445
|
+
* Optional iOS Universal Links and Android App Links associations, and push
|
|
446
|
+
* notifications for the site's own Expo apps (`push: true`; off by default):
|
|
447
|
+
* /api/v1/devices, /api/v1/push/topics and Admin → Mobile app. `editor:
|
|
448
|
+
* { redirectUris, name? }` turns on editor apps: staff sign in through the
|
|
449
|
+
* browser (/admin/app-auth, OAuth 2.0 + PKCE; only the listed redirect URIs
|
|
450
|
+
* receive codes) and edit through the bearer-token /api/v1/admin/* API.
|
|
421
451
|
* @param {{enabled?: boolean}} [options.search]
|
|
422
452
|
* Public site search: the /search page, the /api/search JSON endpoint, and
|
|
423
453
|
* the header search modal. On by default — set `enabled: false` to remove
|
|
@@ -441,6 +471,7 @@ export default function acms(options = {}) {
|
|
|
441
471
|
]);
|
|
442
472
|
const i18n = resolveI18n(options.i18n, contentBases);
|
|
443
473
|
const search = resolveSearch(options.search);
|
|
474
|
+
const mobile = resolveMobile(options.mobile);
|
|
444
475
|
const calendar = resolveCalendar(options.calendar, contentBases, i18n.locales);
|
|
445
476
|
const legal = { enabled: true, operatorName: '', contactEmail: '', updatedAt: '', ...(options.legal === false ? { enabled: false } : options.legal ?? {}) };
|
|
446
477
|
if (typeof legal.enabled !== 'boolean' || ['operatorName','contactEmail','updatedAt'].some(k => typeof legal[k] !== 'string')) throw new Error('acms: invalid legal configuration');
|
|
@@ -453,13 +484,52 @@ export default function acms(options = {}) {
|
|
|
453
484
|
content,
|
|
454
485
|
i18n,
|
|
455
486
|
search,
|
|
487
|
+
mobile,
|
|
456
488
|
calendar,
|
|
457
489
|
legal,
|
|
458
490
|
};
|
|
459
491
|
const routes = [...ROUTES,
|
|
492
|
+
['/api/v1/site', 'api/v1/site.ts'],
|
|
493
|
+
['/api/v1/theme', 'api/v1/theme.ts'],
|
|
494
|
+
['/api/v1/pages/home', 'api/v1/pages/home.ts'],
|
|
495
|
+
['/api/v1/pages/[...slug]', 'api/v1/pages/[...slug].ts'],
|
|
496
|
+
['/api/v1/openapi.json', 'api/v1/openapi.json.ts'],
|
|
460
497
|
['/api/v1/content', 'api/v1/content/index.ts'],
|
|
461
498
|
['/api/v1/content/[id]', 'api/v1/content/[id].ts'],
|
|
462
499
|
];
|
|
500
|
+
if (mobile.ios) routes.push(['/.well-known/apple-app-site-association', '.well-known/apple-app-site-association.ts']);
|
|
501
|
+
if (mobile.android) routes.push(['/.well-known/assetlinks.json', '.well-known/assetlinks.json.ts']);
|
|
502
|
+
if (mobile.push.enabled) {
|
|
503
|
+
routes.push(
|
|
504
|
+
['/api/v1/devices', 'api/v1/devices/index.ts'],
|
|
505
|
+
['/api/v1/devices/[token]', 'api/v1/devices/[token].ts'],
|
|
506
|
+
['/api/v1/push/topics', 'api/v1/push/topics.ts'],
|
|
507
|
+
['/api/push/admin', 'api/push/admin.ts'],
|
|
508
|
+
['/admin/mobile', 'admin/mobile.astro'],
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
if (mobile.editor.enabled) {
|
|
512
|
+
// editor apps: browser sign-in (OAuth 2.0 + PKCE) and the bearer-only admin API
|
|
513
|
+
routes.push(
|
|
514
|
+
['/admin/app-auth', 'admin/app-auth.astro'],
|
|
515
|
+
['/api/v1/auth/token', 'api/v1/auth/token.ts'],
|
|
516
|
+
['/api/v1/auth/revoke', 'api/v1/auth/revoke.ts'],
|
|
517
|
+
['/api/v1/admin/me', 'api/v1/admin/me.ts'],
|
|
518
|
+
['/api/v1/admin/documents', 'api/v1/admin/documents/index.ts'],
|
|
519
|
+
['/api/v1/admin/documents/[kind]/[id]', 'api/v1/admin/documents/[kind]/[id]/index.ts'],
|
|
520
|
+
['/api/v1/admin/documents/[kind]/[id]/[action]', 'api/v1/admin/documents/[kind]/[id]/[action].ts'],
|
|
521
|
+
['/api/v1/admin/documents/[kind]/[id]/translations/[locale]', 'api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts'],
|
|
522
|
+
['/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action]', 'api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts'],
|
|
523
|
+
['/api/v1/admin/sections', 'api/v1/admin/sections/index.ts'],
|
|
524
|
+
['/api/v1/admin/sections/[pageId]/[sectionId]', 'api/v1/admin/sections/[pageId]/[sectionId]/index.ts'],
|
|
525
|
+
['/api/v1/admin/sections/[pageId]/[sectionId]/publish', 'api/v1/admin/sections/[pageId]/[sectionId]/publish.ts'],
|
|
526
|
+
['/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]', 'api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts'],
|
|
527
|
+
['/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action]', 'api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts'],
|
|
528
|
+
['/api/v1/admin/media', 'api/v1/admin/media/index.ts'],
|
|
529
|
+
['/api/v1/admin/bookings', 'api/v1/admin/bookings/index.ts'],
|
|
530
|
+
['/api/v1/admin/bookings/[id]/[action]', 'api/v1/admin/bookings/[id]/[action].ts'],
|
|
531
|
+
);
|
|
532
|
+
}
|
|
463
533
|
if (legal.enabled) routes.push(['/privacy', 'legal/privacy.astro'], ['/terms', 'legal/terms.astro']);
|
|
464
534
|
for (const c of Object.values(content.collections)) {
|
|
465
535
|
routes.push([c.base, 'blog/index.astro'], [`${c.base}/[slug]`, 'blog/[slug].astro']);
|
|
@@ -490,7 +560,8 @@ export default function acms(options = {}) {
|
|
|
490
560
|
['/api/v1/calendars', 'api/v1/calendars.ts'],
|
|
491
561
|
['/api/calendar/events', 'api/calendar/events/index.ts'],
|
|
492
562
|
['/api/calendar/calendars', 'api/calendar/calendars.ts'],
|
|
493
|
-
['/admin/calendar', 'admin/calendar/index.
|
|
563
|
+
['/admin/calendar', 'admin/calendar/index.astro'],
|
|
564
|
+
['/admin/calendar/settings', 'admin/calendar/settings.astro'],
|
|
494
565
|
['/admin/calendar/events', 'admin/calendar/events/index.astro'],
|
|
495
566
|
['/admin/calendar/events/[id]', 'admin/calendar/events/[id].astro'],
|
|
496
567
|
['/admin/calendar/calendars', 'admin/calendar/calendars.astro'],
|
|
@@ -529,7 +600,7 @@ export default function acms(options = {}) {
|
|
|
529
600
|
if (config.root) {
|
|
530
601
|
const discoveryDir = fileURLToPath(new URL('.astro/', config.root));
|
|
531
602
|
mkdirSync(discoveryDir, {recursive:true});
|
|
532
|
-
writeFileSync(join(discoveryDir,'acms-project.json'), JSON.stringify({ themeApiVersion:1, collections:userConfig.content.collections, calendar:userConfig.calendar }));
|
|
603
|
+
writeFileSync(join(discoveryDir,'acms-project.json'), JSON.stringify({ themeApiVersion:1, collections:userConfig.content.collections, calendar:userConfig.calendar, mobile:userConfig.mobile ?? {} }));
|
|
533
604
|
}
|
|
534
605
|
updateConfig({ vite: { plugins: [acmsVitePlugin(userConfig)] } });
|
|
535
606
|
|
|
@@ -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
|
+
}
|