@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli": {
|
|
3
|
+
"version": ">= 16.0.0",
|
|
4
|
+
"appVersionSource": "remote"
|
|
5
|
+
},
|
|
6
|
+
"build": {
|
|
7
|
+
"development": {
|
|
8
|
+
"developmentClient": true,
|
|
9
|
+
"distribution": "internal"
|
|
10
|
+
},
|
|
11
|
+
"preview": {
|
|
12
|
+
"distribution": "internal"
|
|
13
|
+
},
|
|
14
|
+
"production": {
|
|
15
|
+
"autoIncrement": true
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"submit": {
|
|
19
|
+
"production": {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
node_modules/
|
|
5
|
+
|
|
6
|
+
# Expo
|
|
7
|
+
.expo/
|
|
8
|
+
dist/
|
|
9
|
+
web-build/
|
|
10
|
+
expo-env.d.ts
|
|
11
|
+
|
|
12
|
+
# Native
|
|
13
|
+
.kotlin/
|
|
14
|
+
*.orig.*
|
|
15
|
+
*.jks
|
|
16
|
+
*.p8
|
|
17
|
+
*.p12
|
|
18
|
+
*.key
|
|
19
|
+
*.mobileprovision
|
|
20
|
+
|
|
21
|
+
# Metro
|
|
22
|
+
.metro-health-check*
|
|
23
|
+
|
|
24
|
+
# debug
|
|
25
|
+
npm-debug.*
|
|
26
|
+
yarn-debug.*
|
|
27
|
+
yarn-error.*
|
|
28
|
+
|
|
29
|
+
# macOS
|
|
30
|
+
.DS_Store
|
|
31
|
+
*.pem
|
|
32
|
+
|
|
33
|
+
# local env files
|
|
34
|
+
.env*.local
|
|
35
|
+
|
|
36
|
+
# typescript
|
|
37
|
+
*.tsbuildinfo
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# generated native folders
|
|
41
|
+
/ios
|
|
42
|
+
/android
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__ACMS_APP_SLUG__",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "expo-router/entry",
|
|
6
|
+
"acms": { "app": "client", "template": 1 },
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "expo start",
|
|
9
|
+
"android": "expo start --android",
|
|
10
|
+
"ios": "expo start --ios",
|
|
11
|
+
"web": "expo start --web",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"doctor": "npx expo-doctor"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@acms-subir/client": "^0.1.0",
|
|
17
|
+
"@acms-subir/native": "^0.1.0",
|
|
18
|
+
"@tanstack/react-query": "^5.104.0",
|
|
19
|
+
"expo": "~57.0.26",
|
|
20
|
+
"expo-constants": "~57.0.20",
|
|
21
|
+
"expo-linking": "~57.0.11",
|
|
22
|
+
"expo-router": "~57.0.24",
|
|
23
|
+
"expo-status-bar": "~57.0.1",
|
|
24
|
+
"expo-system-ui": "~57.0.4",
|
|
25
|
+
"react": "19.2.3",
|
|
26
|
+
"react-dom": "19.2.3",
|
|
27
|
+
"react-native": "0.86.3",
|
|
28
|
+
"react-native-safe-area-context": "~5.7.0",
|
|
29
|
+
"react-native-screens": "~4.26.0",
|
|
30
|
+
"react-native-svg": "15.15.4",
|
|
31
|
+
"react-native-web": "~0.21.0",
|
|
32
|
+
"react-native-webview": "13.16.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "~19.2.2",
|
|
36
|
+
"typescript": "~6.0.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineSectionRegistry } from '@acms-subir/native';
|
|
2
|
+
// acms:imports (npx acms section add|new adds imports above this line)
|
|
3
|
+
|
|
4
|
+
// Native section components for this app, keyed by section type or
|
|
5
|
+
// "type:variant". The site's library sections (hero-banner, team, pricing, …)
|
|
6
|
+
// already render natively from @acms-subir/native/sections; add an entry here
|
|
7
|
+
// to replace one, or to give the site's own coded sections a native look.
|
|
8
|
+
// Sections without a native component show as the website renders them.
|
|
9
|
+
export const sections = defineSectionRegistry({
|
|
10
|
+
// acms:sections
|
|
11
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
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 **editor app** for the ACMS site at `__ACMS_SITE_URL__`: an Expo
|
|
8
|
+
app for the site's staff (owners and editors) to write, edit sections,
|
|
9
|
+
review translations, manage media and answer bookings. It signs in through
|
|
10
|
+
the site (OAuth + PKCE in the system browser) and calls the bearer-only
|
|
11
|
+
`/api/v1/admin/*` API. It lives beside the website as a separate project.
|
|
12
|
+
|
|
13
|
+
## Security rules (non-negotiable)
|
|
14
|
+
|
|
15
|
+
1. **Never handle passwords.** No login form, no password field, no
|
|
16
|
+
"remember me". Sign-in is `useAuth().signIn()`, which opens the site.
|
|
17
|
+
2. **Tokens only in secure storage.** `<EditorProvider>` keeps them in
|
|
18
|
+
`expo-secure-store` (sessionStorage on the web). Never read them into
|
|
19
|
+
state, never put them in AsyncStorage, files, URLs, analytics or logs.
|
|
20
|
+
Never `console.log` a token, a code, an API response with a token, or
|
|
21
|
+
a request's headers.
|
|
22
|
+
3. **Use the hooks, never raw fetch.** `@acms-subir/native/editor` (`useAuth`,
|
|
23
|
+
`useMe`, `useDocuments`, `useDocument`, `usePublish`, `useSectionEditor`,
|
|
24
|
+
`useTranslations`, `useTranslationQueue`, `useMedia`, `useBookingsInbox`) or
|
|
25
|
+
`useEditorClient()` inside a TanStack Query hook for anything else. They
|
|
26
|
+
refresh tokens and sign out when the site revokes the device.
|
|
27
|
+
4. **Respect roles.** The server enforces every rule, but the UI must hide
|
|
28
|
+
what the person can't do: read `useMe().data.permissions`, and in section
|
|
29
|
+
forms `canEdit(fieldKey)` / `permissions.canEditRows(key)`. Editors can't
|
|
30
|
+
edit locked sections or fields, or change layout, style or visibility.
|
|
31
|
+
5. **Handle conflicts.** Saves send a revision. A 409 means someone saved
|
|
32
|
+
first: show `conflict` from `useDocument` / `useSectionEditor` with
|
|
33
|
+
"Keep mine" and "Use theirs"; never retry blindly over someone's work.
|
|
34
|
+
6. **No secrets in the app.** `EXPO_PUBLIC_*` values are bundled and readable.
|
|
35
|
+
No API keys, no agent tokens, no admin routes other than `/api/v1/admin/*`.
|
|
36
|
+
|
|
37
|
+
## Design rules
|
|
38
|
+
|
|
39
|
+
- Default look is the admin's "Inset Paper" (`useEditorTheme()`: `colors`,
|
|
40
|
+
`text`, `radius`), light and dark. Use the building blocks (`EditorButton`,
|
|
41
|
+
`EditorInput`, `Segmented`, `Notice`, `StatusText`, `ListRow`, `Card`,
|
|
42
|
+
`Sheet`, `FieldInput`, `SectionFields`).
|
|
43
|
+
- No pill-shaped chips, badges or filters (8–12px rectangles, text tabs,
|
|
44
|
+
marker + text). Status is colored text. No colored left-edge stripes for
|
|
45
|
+
selection or callouts (tinted fill instead). No gradient-squircle logos.
|
|
46
|
+
- Phones and tablets: `useWide()` from `components/Screen.tsx`; on ≥768
|
|
47
|
+
use the width (`SplitView`: list + detail), never a stretched phone column.
|
|
48
|
+
Touch targets at least 44pt.
|
|
49
|
+
|
|
50
|
+
## Where things are
|
|
51
|
+
|
|
52
|
+
- `app/_layout.tsx`: `<EditorProvider>` (site URL, client id, redirect URI) and
|
|
53
|
+
the signed-out / signed-in switch.
|
|
54
|
+
- `app/index.tsx`: Home (who is signed in, tools, appearance, sign out).
|
|
55
|
+
- `app/auth.tsx`: the sign-in return address route.
|
|
56
|
+
- `lib/config.ts`: site URL, scheme, redirect URI, client id.
|
|
57
|
+
- `tools/*.ts`: one file per Home entry (`{ title, description, href, order }`).
|
|
58
|
+
- `components/Screen.tsx`: `Screen`, `SplitView`, `useWide`.
|
|
59
|
+
|
|
60
|
+
## Before you finish
|
|
61
|
+
|
|
62
|
+
- `npm run typecheck` passes; `npx expo export --platform web` bundles.
|
|
63
|
+
- Try it signed in as an **editor** and as an **owner** (locked sections,
|
|
64
|
+
publish rights), in light and dark, on a phone and a tablet width.
|
|
65
|
+
- `npx acms mobile doctor` (from the site folder) passes: redirect URI listed
|
|
66
|
+
on the site, secure storage installed, no tokens in AsyncStorage or logs.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# __ACMS_APP_NAME__
|
|
2
|
+
|
|
3
|
+
A blank Expo app for the staff of your ACMS site (`__ACMS_SITE_URL__`). People
|
|
4
|
+
sign in with their site account through the website itself, and the app
|
|
5
|
+
already knows who they are and what their role allows. What it does after
|
|
6
|
+
that is yours to build: add ready-made screens with recipes, or write your own
|
|
7
|
+
with the hooks from `@acms-subir/native/editor`.
|
|
8
|
+
|
|
9
|
+
It is separate from the client app (the one your visitors use). Both can live
|
|
10
|
+
beside the site.
|
|
11
|
+
|
|
12
|
+
## Let the site accept this app
|
|
13
|
+
|
|
14
|
+
The site only signs in apps whose return address it knows. Add this app's
|
|
15
|
+
address to the site's `astro.config.mjs` and restart (or deploy) the site:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
acms({ mobile: { editor: { name: '__ACMS_APP_NAME__', redirectUris: ['__ACMS_REDIRECT_URI__'] } } })
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For the web version, also list its address, e.g. `https://staff.example.com/auth`
|
|
22
|
+
(`http://localhost/auth` covers local development on any port).
|
|
23
|
+
|
|
24
|
+
## Run it
|
|
25
|
+
|
|
26
|
+
From your **site** folder (starts the site too, and points the app at an
|
|
27
|
+
address your phone can reach):
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
cd __ACMS_APP_DIR__ && npm install && cd ..
|
|
31
|
+
npx acms mobile dev --dir __ACMS_APP_DIR__
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Scan the QR code with Expo Go, or press `i` / `a` / `w`. Tap **Sign in**: the
|
|
35
|
+
site opens in the browser, you approve this device, and you're back in the app.
|
|
36
|
+
|
|
37
|
+
## Add screens
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx acms mobile recipe list
|
|
41
|
+
npx acms mobile recipe add editor-content --dir __ACMS_APP_DIR__ # posts, pages, events: list, filters, search
|
|
42
|
+
npx acms mobile recipe add editor-writer --dir __ACMS_APP_DIR__ # write, save drafts, publish, conflicts
|
|
43
|
+
npx acms mobile recipe add editor-sections --dir __ACMS_APP_DIR__ # edit a page's sections, publish one
|
|
44
|
+
npx acms mobile recipe add editor-translations --dir __ACMS_APP_DIR__ # review another language
|
|
45
|
+
npx acms mobile recipe add editor-media --dir __ACMS_APP_DIR__ # media grid + photo upload
|
|
46
|
+
npx acms mobile recipe add editor-bookings --dir __ACMS_APP_DIR__ # approve / decline bookings
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Each shows up on Home automatically (a file in `tools/`).
|
|
50
|
+
|
|
51
|
+
## Security
|
|
52
|
+
|
|
53
|
+
- Nobody types a password into this app. Sign-in happens on the website.
|
|
54
|
+
- Tokens live only in the device's secure storage (Keychain / Keystore via
|
|
55
|
+
`expo-secure-store`); on the web, in the tab's sessionStorage.
|
|
56
|
+
- The site decides what each person may do on every request. The app hides
|
|
57
|
+
what a role can't do, but never relies on that.
|
|
58
|
+
- Staff can sign a device out from the site (Users page), and changing a
|
|
59
|
+
password signs every device out.
|
|
60
|
+
|
|
61
|
+
## Check it
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npx acms mobile doctor --dir __ACMS_APP_DIR__
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Ship it
|
|
68
|
+
|
|
69
|
+
Builds and store submission use [EAS](https://docs.expo.dev/build/introduction/).
|
|
70
|
+
Staff apps are often distributed internally (TestFlight, internal testing
|
|
71
|
+
track, or `eas build --profile preview`). Check `app.json` first:
|
|
72
|
+
`ios.bundleIdentifier` / `android.package` (`__ACMS_BUNDLE_ID__`) and `scheme`
|
|
73
|
+
can't change without updating the site's `redirectUris`.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { ActivityIndicator, Text, View } from 'react-native';
|
|
3
|
+
import { DarkTheme, DefaultTheme, Stack, ThemeProvider } from 'expo-router';
|
|
4
|
+
import { StatusBar } from 'expo-status-bar';
|
|
5
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
6
|
+
import { EditorProvider, completeAuthRedirect, useAuth, useEditorTheme } from '@acms-subir/native/editor';
|
|
7
|
+
import { siteUrl, clientId, redirectUri } from '../lib/config';
|
|
8
|
+
import { AppearanceProvider, useAppearance } from '../lib/appearance';
|
|
9
|
+
import { SignIn } from '../components/SignIn';
|
|
10
|
+
|
|
11
|
+
// Web only: the sign-in popup hands its result back to the app and closes.
|
|
12
|
+
completeAuthRedirect();
|
|
13
|
+
|
|
14
|
+
export default function RootLayout() {
|
|
15
|
+
if (!siteUrl) return <MissingUrl />;
|
|
16
|
+
return (
|
|
17
|
+
<SafeAreaProvider>
|
|
18
|
+
<AppearanceProvider>
|
|
19
|
+
<Provider />
|
|
20
|
+
</AppearanceProvider>
|
|
21
|
+
</SafeAreaProvider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Provider() {
|
|
26
|
+
const { appearance } = useAppearance();
|
|
27
|
+
return (
|
|
28
|
+
<EditorProvider baseUrl={siteUrl!} clientId={clientId} redirectUri={redirectUri} colorScheme={appearance === 'auto' ? undefined : appearance}>
|
|
29
|
+
<Gate />
|
|
30
|
+
</EditorProvider>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Signed out → the sign-in screen; signed in → the app. The server checks
|
|
35
|
+
// every request anyway; this only decides what to show.
|
|
36
|
+
function Gate() {
|
|
37
|
+
const t = useEditorTheme();
|
|
38
|
+
const auth = useAuth();
|
|
39
|
+
const navTheme = useMemo(() => {
|
|
40
|
+
const base = t.scheme === 'dark' ? DarkTheme : DefaultTheme;
|
|
41
|
+
return { ...base, colors: { ...base.colors, primary: t.colors.accent, background: t.colors.panel, card: t.colors.panel, text: t.colors.text, border: t.colors.line, notification: t.colors.accent } };
|
|
42
|
+
}, [t]);
|
|
43
|
+
return (
|
|
44
|
+
<ThemeProvider value={navTheme}>
|
|
45
|
+
<StatusBar style={t.scheme === 'dark' ? 'light' : 'dark'} />
|
|
46
|
+
{auth.status === 'loading' ? (
|
|
47
|
+
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: t.colors.panel }}><ActivityIndicator color={t.colors.muted} /></View>
|
|
48
|
+
) : !auth.signedIn ? <SignIn /> : (
|
|
49
|
+
<Stack
|
|
50
|
+
screenOptions={{
|
|
51
|
+
headerStyle: { backgroundColor: t.colors.panel },
|
|
52
|
+
headerTintColor: t.colors.text,
|
|
53
|
+
headerTitleStyle: { fontWeight: '600' },
|
|
54
|
+
headerShadowVisible: false,
|
|
55
|
+
headerBackTitle: 'Back',
|
|
56
|
+
contentStyle: { backgroundColor: t.colors.panel },
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<Stack.Screen name="index" options={{ title: 'Home' }} />
|
|
60
|
+
<Stack.Screen name="auth" options={{ headerShown: false }} />
|
|
61
|
+
</Stack>
|
|
62
|
+
)}
|
|
63
|
+
</ThemeProvider>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function MissingUrl() {
|
|
68
|
+
return (
|
|
69
|
+
<View style={{ flex: 1, justifyContent: 'center', padding: 24, gap: 8 }}>
|
|
70
|
+
<Text style={{ fontSize: 18, fontWeight: '600' }}>Connect your site</Text>
|
|
71
|
+
<Text style={{ fontSize: 15, lineHeight: 22 }}>
|
|
72
|
+
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.
|
|
73
|
+
</Text>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
3
|
+
import { router } from 'expo-router';
|
|
4
|
+
import { useEditorTheme } from '@acms-subir/native/editor';
|
|
5
|
+
|
|
6
|
+
// The sign-in return address (<scheme>://auth, or /auth on the web). The
|
|
7
|
+
// browser session normally catches it before this screen shows; if the link
|
|
8
|
+
// opens the app directly, just go Home.
|
|
9
|
+
export default function AuthReturn() {
|
|
10
|
+
const t = useEditorTheme();
|
|
11
|
+
useEffect(() => { const id = setTimeout(() => router.replace('/'), 300); return () => clearTimeout(id); }, []);
|
|
12
|
+
return <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: t.colors.panel }}><ActivityIndicator color={t.colors.muted} /></View>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Text, View } from 'react-native';
|
|
2
|
+
import { router } from 'expo-router';
|
|
3
|
+
import { Card, EditorButton, ListRow, SectionLabel, Segmented, errorMessage, useAuth, useEditorTheme, useMe } from '@acms-subir/native/editor';
|
|
4
|
+
import { Screen, useWide } from '../components/Screen';
|
|
5
|
+
import { tools } from '../lib/tools';
|
|
6
|
+
import { useAppearance, type Appearance } from '../lib/appearance';
|
|
7
|
+
|
|
8
|
+
// Home: who is signed in, and the screens this app offers (one per file in
|
|
9
|
+
// tools/). Blank on purpose: add screens with recipes or build your own.
|
|
10
|
+
export default function Home() {
|
|
11
|
+
const t = useEditorTheme();
|
|
12
|
+
const auth = useAuth();
|
|
13
|
+
const me = useMe();
|
|
14
|
+
const { wide } = useWide();
|
|
15
|
+
const { appearance, setAppearance } = useAppearance();
|
|
16
|
+
const staffRole = me.data?.user.role ?? auth.role;
|
|
17
|
+
const role = staffRole ? staffRole[0].toUpperCase() + staffRole.slice(1) : 'Staff';
|
|
18
|
+
return (
|
|
19
|
+
<Screen refreshing={me.isRefetching} onRefresh={() => me.refetch()}>
|
|
20
|
+
<View style={{ gap: 4 }}>
|
|
21
|
+
<Text style={t.text.meta}>{me.data?.site.name ?? ' '}</Text>
|
|
22
|
+
<Text accessibilityRole="header" style={t.text.screenTitle}>Signed in as {auth.user?.name}</Text>
|
|
23
|
+
<Text style={t.text.body}>{role} · {auth.user?.email}</Text>
|
|
24
|
+
</View>
|
|
25
|
+
{me.isError && <Text style={[t.text.meta, { color: t.colors.red }]}>{errorMessage(me.error)}</Text>}
|
|
26
|
+
|
|
27
|
+
<SectionLabel>Tools</SectionLabel>
|
|
28
|
+
{tools.length ? (
|
|
29
|
+
<Card style={wide ? { flexDirection: 'row', flexWrap: 'wrap' } : undefined}>
|
|
30
|
+
{tools.map((tool) => (
|
|
31
|
+
<View key={tool.name} style={wide ? { width: '50%' } : undefined}>
|
|
32
|
+
<ListRow title={tool.title} meta={tool.description} onPress={() => router.push(tool.href as never)} right={<Text style={t.text.meta}>›</Text>} />
|
|
33
|
+
</View>
|
|
34
|
+
))}
|
|
35
|
+
</Card>
|
|
36
|
+
) : (
|
|
37
|
+
<View style={{ backgroundColor: t.colors.sel, borderRadius: t.radius.card, padding: 14, gap: 6 }}>
|
|
38
|
+
<Text style={t.text.row}>Nothing here yet</Text>
|
|
39
|
+
<Text style={t.text.body}>This app is connected and signed in. Add screens from the site folder, for example:</Text>
|
|
40
|
+
<Text style={[t.text.meta, { fontFamily: 'Menlo' }]}>npx acms mobile recipe add editor-content</Text>
|
|
41
|
+
</View>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
<SectionLabel>Appearance</SectionLabel>
|
|
45
|
+
<Segmented<Appearance> accessibilityLabel="Appearance" value={appearance} onChange={setAppearance}
|
|
46
|
+
options={[{ value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }, { value: 'auto', label: 'Auto' }]} />
|
|
47
|
+
|
|
48
|
+
<SectionLabel>This device</SectionLabel>
|
|
49
|
+
<Card>
|
|
50
|
+
<ListRow title={auth.device?.name ?? 'This device'} meta="Signed in through the website. Sign out here or from the site’s Users page." />
|
|
51
|
+
</Card>
|
|
52
|
+
<EditorButton variant="danger" label="Sign out" onPress={() => { void auth.signOut(); }} />
|
|
53
|
+
</Screen>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -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", "expo-secure-store"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Platform, RefreshControl, ScrollView, View, useWindowDimensions, type ScrollViewProps } from 'react-native';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
import { useEditorTheme } from '@acms-subir/native/editor';
|
|
5
|
+
|
|
6
|
+
/** ≥ 768pt: tablets and desktop web use the width (list + detail), phones stack */
|
|
7
|
+
export function useWide() {
|
|
8
|
+
const { width } = useWindowDimensions();
|
|
9
|
+
return { wide: width >= 768, width, gutter: width >= 768 ? 24 : 16 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** a scrolling screen on the Inset Paper panel, readable width on big screens */
|
|
13
|
+
export function Screen({ children, refreshing, onRefresh, maxWidth = 760, ...rest }: ScrollViewProps & { children: ReactNode; refreshing?: boolean; onRefresh?: () => void; maxWidth?: number }) {
|
|
14
|
+
const t = useEditorTheme();
|
|
15
|
+
const { gutter } = useWide();
|
|
16
|
+
const insets = useSafeAreaInsets();
|
|
17
|
+
return (
|
|
18
|
+
<ScrollView
|
|
19
|
+
style={{ flex: 1, backgroundColor: t.colors.panel }}
|
|
20
|
+
contentContainerStyle={{ padding: gutter, paddingBottom: gutter + insets.bottom, gap: 16, width: '100%', maxWidth, alignSelf: 'center' }}
|
|
21
|
+
keyboardShouldPersistTaps="handled"
|
|
22
|
+
automaticallyAdjustKeyboardInsets
|
|
23
|
+
refreshControl={onRefresh ? <RefreshControl refreshing={!!refreshing} onRefresh={onRefresh} tintColor={t.colors.muted} /> : undefined}
|
|
24
|
+
{...rest}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</ScrollView>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* List + detail. On tablets both show side by side (the list keeps a
|
|
33
|
+
* comfortable width, the detail uses the rest); on phones only the list shows
|
|
34
|
+
* and rows open their own screen.
|
|
35
|
+
*/
|
|
36
|
+
export function SplitView({ list, detail, listWidth = 380 }: { list: ReactNode; detail: ReactNode; listWidth?: number }) {
|
|
37
|
+
const t = useEditorTheme();
|
|
38
|
+
const { wide } = useWide();
|
|
39
|
+
if (!wide) return <View style={{ flex: 1, backgroundColor: t.colors.panel }}>{list}</View>;
|
|
40
|
+
return (
|
|
41
|
+
<View style={{ flex: 1, flexDirection: 'row', backgroundColor: t.colors.shell }}>
|
|
42
|
+
<View style={{ width: listWidth, backgroundColor: t.colors.panel, borderRightWidth: 1, borderRightColor: t.colors.line }}>{list}</View>
|
|
43
|
+
<View style={{ flex: 1, backgroundColor: t.colors.panel }}>{detail}</View>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** buttons in a screen header (the web header has no right inset of its own) */
|
|
49
|
+
export function HeaderActions({ children }: { children: ReactNode }) {
|
|
50
|
+
return <View style={{ flexDirection: 'row', gap: 8, marginRight: Platform.OS === 'web' ? 16 : 0 }}>{children}</View>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ActivityIndicator, Image, Text, View } from 'react-native';
|
|
2
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
3
|
+
import { EditorButton, Notice, resolveUrl, useAuth, useEditorTheme, useSiteInfo } from '@acms-subir/native/editor';
|
|
4
|
+
import { siteUrl } from '../lib/config';
|
|
5
|
+
import { useWide } from './Screen';
|
|
6
|
+
|
|
7
|
+
// Staff sign in on the website itself (system browser): the app never sees a
|
|
8
|
+
// password. The site shows an approve screen, then returns here with a code.
|
|
9
|
+
export function SignIn() {
|
|
10
|
+
const t = useEditorTheme();
|
|
11
|
+
const auth = useAuth();
|
|
12
|
+
const site = useSiteInfo();
|
|
13
|
+
const insets = useSafeAreaInsets();
|
|
14
|
+
const { wide } = useWide();
|
|
15
|
+
const name = site.data?.name ?? '';
|
|
16
|
+
const logo = resolveUrl(site.data?.logo || site.data?.icon, siteUrl!);
|
|
17
|
+
const busy = auth.status === 'signingIn';
|
|
18
|
+
return (
|
|
19
|
+
<View style={{ flex: 1, backgroundColor: wide ? t.colors.shell : t.colors.panel, alignItems: 'center', justifyContent: 'center', padding: 24, paddingTop: 24 + insets.top }}>
|
|
20
|
+
<View style={{ width: '100%', maxWidth: 420, gap: 20, ...(wide ? { backgroundColor: t.colors.panel, borderRadius: t.radius.sheet, padding: 32, borderWidth: 1, borderColor: t.colors.line } : {}) }}>
|
|
21
|
+
<View style={{ gap: 12 }}>
|
|
22
|
+
{logo ? <Image source={{ uri: logo }} accessibilityLabel={name} style={{ height: 48, width: 160 }} resizeMode="contain" /> : null}
|
|
23
|
+
{site.isPending ? <ActivityIndicator color={t.colors.muted} style={{ alignSelf: 'flex-start' }} /> : (
|
|
24
|
+
<Text accessibilityRole="header" style={t.text.screenTitle}>{name || 'Your site'}</Text>
|
|
25
|
+
)}
|
|
26
|
+
<Text style={t.text.body}>Sign in with your {name || 'site'} account to write, edit sections and answer bookings from this device.</Text>
|
|
27
|
+
</View>
|
|
28
|
+
{!!auth.signedOutReason && <Notice tone="warn" title="Signed out">{auth.signedOutReason}</Notice>}
|
|
29
|
+
{!!auth.error && <Notice tone="error" title="Couldn’t sign in">{auth.error}</Notice>}
|
|
30
|
+
{site.isError && <Notice tone="warn">Can’t reach {siteUrl}. Check the address in .env and your connection.</Notice>}
|
|
31
|
+
<EditorButton variant="primary" label={busy ? 'Waiting for the browser…' : `Sign in with ${name || 'the site'}`} busy={busy} disabled={busy} onPress={() => { void auth.signIn(); }} />
|
|
32
|
+
<Text style={t.text.meta}>You’ll approve this device on the website. The app never sees your password, and you can sign it out any time from the site’s Users page.</Text>
|
|
33
|
+
</View>
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Pressable, ScrollView, Text, View } from 'react-native';
|
|
2
|
+
import { useEditorTheme } from '@acms-subir/native/editor';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Filters as text tabs (never pills): the active one is stronger text with a
|
|
6
|
+
* short underline. Scrolls sideways when there are many.
|
|
7
|
+
*/
|
|
8
|
+
export function TextTabs<T extends string>({ value, options, onChange, accessibilityLabel }: {
|
|
9
|
+
value: T; options: { value: T; label: string; count?: number }[]; onChange: (v: T) => void; accessibilityLabel?: string;
|
|
10
|
+
}) {
|
|
11
|
+
const t = useEditorTheme();
|
|
12
|
+
return (
|
|
13
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false} accessibilityRole="tablist" accessibilityLabel={accessibilityLabel} contentContainerStyle={{ gap: 18, paddingRight: 8 }}>
|
|
14
|
+
{options.map((o) => {
|
|
15
|
+
const on = o.value === value;
|
|
16
|
+
return (
|
|
17
|
+
<Pressable key={o.value} accessibilityRole="tab" accessibilityState={{ selected: on }} onPress={() => onChange(o.value)} style={{ minHeight: 40, justifyContent: 'center' }}>
|
|
18
|
+
<Text style={{ color: on ? t.colors.text : t.colors.muted, fontWeight: on ? '700' : '500', fontSize: 15 }}>
|
|
19
|
+
{o.label}{o.count !== undefined ? <Text style={{ color: t.colors.muted, fontWeight: '500' }}>{` ${o.count}`}</Text> : null}
|
|
20
|
+
</Text>
|
|
21
|
+
<View style={{ height: 2, borderRadius: 1, marginTop: 4, backgroundColor: on ? t.colors.accent : 'transparent' }} />
|
|
22
|
+
</Pressable>
|
|
23
|
+
);
|
|
24
|
+
})}
|
|
25
|
+
</ScrollView>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli": {
|
|
3
|
+
"version": ">= 16.0.0",
|
|
4
|
+
"appVersionSource": "remote"
|
|
5
|
+
},
|
|
6
|
+
"build": {
|
|
7
|
+
"development": {
|
|
8
|
+
"developmentClient": true,
|
|
9
|
+
"distribution": "internal"
|
|
10
|
+
},
|
|
11
|
+
"preview": {
|
|
12
|
+
"distribution": "internal"
|
|
13
|
+
},
|
|
14
|
+
"production": {
|
|
15
|
+
"autoIncrement": true
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"submit": {
|
|
19
|
+
"production": {}
|
|
20
|
+
}
|
|
21
|
+
}
|