@acms-subir/core 0.3.3-next.20260929113209 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -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/sections.mjs +186 -0
- package/cli/themes.mjs +52 -8
- package/docs/mobile.md +527 -0
- package/docs/sections.md +203 -0
- package/docs/theme-authoring.md +1 -1
- package/integration.mjs +55 -1
- 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 +4 -2
- 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 +19 -171
- 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/editor/PostEditor.tsx +174 -10
- 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 +6 -1
- package/src/layouts/Site.astro +107 -19
- 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/content-service.ts +198 -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 +19 -11
- 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 +8 -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 +11 -0
- package/src/lib/repo/sql.ts +40 -7
- package/src/lib/repo/types.ts +9 -3
- package/src/lib/roles.ts +5 -0
- package/src/lib/scheduler.ts +3 -0
- package/src/lib/schema.ts +27 -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 +217 -0
- package/src/lib/theme.ts +16 -1
- package/src/lib/translate.ts +97 -0
- package/src/lib/translations.ts +39 -0
- package/src/middleware.ts +32 -0
- 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/events/[id].astro +9 -0
- 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/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 +69 -0
- package/src/styles/editor.css +21 -0
- package/src/styles/mobile-app.css +151 -0
- package/src/styles/sections.css +7 -1
- 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 +326 -0
- package/src/theme-sdk/index.ts +10 -2
- package/src/theme-sdk/tokens.d.mts +11 -0
- package/src/virtual.d.ts +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../../../lib/editor/http';
|
|
2
|
+
import { DocKindParam } from '../../../../../../../../../lib/editor/documents';
|
|
3
|
+
import { SetDocInput, changeDocTranslation, getDocTranslation } from '../../../../../../../../../lib/editor/translations';
|
|
4
|
+
import { methodNotAllowed } from '../../../../../../../../../lib/mobile/http';
|
|
5
|
+
import { readJson } from '../../../../../../../../../lib/api';
|
|
6
|
+
import { invalidateRedirectCache } from '../../../../../../../../../middleware';
|
|
7
|
+
export { preflight as OPTIONS } from '../../../../../../../../../lib/mobile/http';
|
|
8
|
+
export const GET = editorRoute(async ({ repo, context: { params } }) => getDocTranslation(repo, DocKindParam.parse(params.kind), params.id!, params.locale!));
|
|
9
|
+
// a person's words: stored as reviewed
|
|
10
|
+
export const PUT = editorRoute(async (c) => {
|
|
11
|
+
const { params } = c.context;
|
|
12
|
+
const kind = DocKindParam.parse(params.kind);
|
|
13
|
+
const body = SetDocInput.parse(await readJson(c.context.request, 2_500_000));
|
|
14
|
+
const result = await changeDocTranslation(c.repo, kind, params.id!, params.locale!, body.revision, { op: 'set', values: body.values }, { url: c.context.url, locals: c.context.locals, onRedirect: invalidateRedirectCache });
|
|
15
|
+
await logWrite(c, 'translate', `${kind}:${params.id}:${params.locale}`);
|
|
16
|
+
return result;
|
|
17
|
+
}, { write: true });
|
|
18
|
+
export const ALL = () => methodNotAllowed('GET, PUT, OPTIONS');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../lib/editor/http';
|
|
2
|
+
import { CreateInput, createDoc, docJson, listDocs, listQuery } from '../../../../../lib/editor/documents';
|
|
3
|
+
import { methodNotAllowed, queryParams } from '../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
6
|
+
export const GET = editorRoute(async ({ repo, context }) => listDocs(repo, listQuery.parse(queryParams(context.url))));
|
|
7
|
+
export const POST = editorRoute(async (c) => {
|
|
8
|
+
const input = CreateInput.parse(await readJson(c.context.request, 20_000));
|
|
9
|
+
const doc = await createDoc(c.repo, input, c.who.user.id);
|
|
10
|
+
await logWrite(c, 'create', `${input.kind}:${doc.id}`);
|
|
11
|
+
return docJson(input.kind, doc);
|
|
12
|
+
}, { write: true, status: 201 });
|
|
13
|
+
export const ALL = () => methodNotAllowed('GET, POST, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { editorRoute } from '../../../../lib/editor/http';
|
|
2
|
+
import { me } from '../../../../lib/editor/me';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const GET = editorRoute(({ repo, who, context }) => me(repo, who, context.url, context.locals));
|
|
6
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { editorRoute, logWrite } from '../../../../../lib/editor/http';
|
|
3
|
+
import { methodNotAllowed, pageQuery, queryParams } from '../../../../../lib/mobile/http';
|
|
4
|
+
import { ApiError, readResponseBytes } from '../../../../../lib/api';
|
|
5
|
+
import { MAX_UPLOAD_BODY, storeUpload } from '../../../../../lib/media/upload';
|
|
6
|
+
import type { Media } from '../../../../../lib/schema';
|
|
7
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
8
|
+
const view = (m: Media) => ({ id: m.id, url: `/media/${m.key}`, filename: m.filename, mime: m.mime, width: m.width, height: m.height, size: m.size, alt: m.alt,
|
|
9
|
+
variants: m.variants.map((v) => ({ width: v.w, url: `/media/${v.key}` })), createdAt: new Date(m.createdAt).toISOString() });
|
|
10
|
+
const ListQuery = z.object({ q: z.string().max(200).optional(), type: z.enum(['image', 'document']).optional(), ...pageQuery }).strict();
|
|
11
|
+
export const GET = editorRoute(async ({ repo, context }) => {
|
|
12
|
+
const q = ListQuery.parse(queryParams(context.url));
|
|
13
|
+
let list = await repo.listMedia();
|
|
14
|
+
if (q.type) list = list.filter((m) => (q.type === 'image') === m.mime.startsWith('image/'));
|
|
15
|
+
if (q.q) { const needle = q.q.toLowerCase(); list = list.filter((m) => `${m.filename} ${m.alt}`.toLowerCase().includes(needle)); }
|
|
16
|
+
list.sort((a, b) => b.createdAt - a.createdAt);
|
|
17
|
+
const start = (q.page - 1) * q.limit;
|
|
18
|
+
return { items: list.slice(start, start + q.limit).map(view), pagination: { page: q.page, limit: q.limit, nextPage: start + q.limit < list.length ? q.page + 1 : null } };
|
|
19
|
+
});
|
|
20
|
+
// same types, limits and storage as the admin uploader (images arrive pre-compressed)
|
|
21
|
+
export const POST = editorRoute(async (c) => {
|
|
22
|
+
const type = c.context.request.headers.get('content-type') ?? '';
|
|
23
|
+
if (!type.toLowerCase().startsWith('multipart/form-data')) throw new ApiError(415, 'Use Content-Type: multipart/form-data with a "file" field.');
|
|
24
|
+
const bytes = await readResponseBytes(c.context.request, MAX_UPLOAD_BODY);
|
|
25
|
+
let form: FormData;
|
|
26
|
+
try { form = await new Response(bytes as BodyInit, { headers: { 'content-type': type } }).formData(); }
|
|
27
|
+
catch { throw new ApiError(400, 'The upload is not valid multipart form data.'); }
|
|
28
|
+
const media = await storeUpload(form, c.context.locals, c.repo);
|
|
29
|
+
await logWrite(c, 'upload', `media:${media.id}`);
|
|
30
|
+
return view(media);
|
|
31
|
+
}, { write: true, status: 201, limit: 30 });
|
|
32
|
+
export const ALL = () => methodNotAllowed('GET, POST, OPTIONS');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { SectionPatch, patchSection } from '../../../../../../../lib/editor/sections';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
6
|
+
// stage new field values for one section (content only for editors; locks apply)
|
|
7
|
+
export const PATCH = editorRoute(async (c) => {
|
|
8
|
+
const { pageId, sectionId } = c.context.params;
|
|
9
|
+
const body = SectionPatch.parse(await readJson(c.context.request, 500_000));
|
|
10
|
+
const result = await patchSection(c.repo, c.who, pageId!, sectionId!, body, c.context.url.origin);
|
|
11
|
+
await logWrite(c, 'section', `${pageId}:${sectionId}`);
|
|
12
|
+
return result;
|
|
13
|
+
}, { write: true });
|
|
14
|
+
export const ALL = () => methodNotAllowed('PATCH, OPTIONS');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
|
|
2
|
+
import { RevisionInput, publishSection } from '../../../../../../../lib/editor/sections';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
|
|
6
|
+
// publish just this section's staged copy (same rules as the Design screen)
|
|
7
|
+
export const POST = editorRoute(async (c) => {
|
|
8
|
+
const { pageId, sectionId } = c.context.params;
|
|
9
|
+
const body = RevisionInput.parse(await readJson(c.context.request, 5_000));
|
|
10
|
+
const result = await publishSection(c.repo, c.who, pageId!, sectionId!, body.revision, c.context.url.origin);
|
|
11
|
+
await logWrite(c, 'section-publish', `${pageId}:${sectionId}`);
|
|
12
|
+
return result;
|
|
13
|
+
}, { write: true });
|
|
14
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { editorRoute, logWrite } from '../../../../../../../../../lib/editor/http';
|
|
3
|
+
import { AiSectionInput, ReviewSectionInput, changeSectionTranslation } from '../../../../../../../../../lib/editor/translations';
|
|
4
|
+
import { failure, methodNotAllowed } from '../../../../../../../../../lib/mobile/http';
|
|
5
|
+
import { ApiError, readJson } from '../../../../../../../../../lib/api';
|
|
6
|
+
export { preflight as OPTIONS } from '../../../../../../../../../lib/mobile/http';
|
|
7
|
+
const handler = (ai: boolean) => editorRoute(async (c) => {
|
|
8
|
+
const { params } = c.context;
|
|
9
|
+
const raw = await readJson(c.context.request, 10_000);
|
|
10
|
+
const mode = ai ? (() => { const b = AiSectionInput.parse(raw); return { revision: b.revision, mode: { op: 'ai' as const, paths: b.paths, includeReviewed: b.includeReviewed } }; })()
|
|
11
|
+
: (() => { const b = ReviewSectionInput.parse(raw); return { revision: b.revision, mode: { op: 'review' as const, paths: b.paths } }; })();
|
|
12
|
+
const result = await changeSectionTranslation(c.repo, c.who, params.pageId!, params.sectionId!, params.locale!, mode.revision, mode.mode, c.context.url.origin, c.context.locals);
|
|
13
|
+
await logWrite(c, ai ? 'translate-ai' : 'translate-review', `${params.pageId}:${params.sectionId}:${params.locale}`);
|
|
14
|
+
return result;
|
|
15
|
+
}, { write: true, ...(ai ? { limit: 10 } : {}) });
|
|
16
|
+
const review = handler(false), aiDraft = handler(true);
|
|
17
|
+
export const POST: APIRoute = (context) => {
|
|
18
|
+
if (context.params.action === 'review') return review(context);
|
|
19
|
+
if (context.params.action === 'ai-draft') return aiDraft(context);
|
|
20
|
+
return failure(new ApiError(404, 'Unknown action.'));
|
|
21
|
+
};
|
|
22
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { editorRoute, logWrite } from '../../../../../../../../../lib/editor/http';
|
|
2
|
+
import { SetSectionInput, changeSectionTranslation, getSectionTranslation } from '../../../../../../../../../lib/editor/translations';
|
|
3
|
+
import { methodNotAllowed } from '../../../../../../../../../lib/mobile/http';
|
|
4
|
+
import { readJson } from '../../../../../../../../../lib/api';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../../../../../../lib/mobile/http';
|
|
6
|
+
export const GET = editorRoute(async ({ repo, context: { params } }) => getSectionTranslation(repo, params.pageId!, params.sectionId!, params.locale!));
|
|
7
|
+
export const PUT = editorRoute(async (c) => {
|
|
8
|
+
const { params } = c.context;
|
|
9
|
+
const body = SetSectionInput.parse(await readJson(c.context.request, 500_000));
|
|
10
|
+
const result = await changeSectionTranslation(c.repo, c.who, params.pageId!, params.sectionId!, params.locale!, body.revision, { op: 'set', values: body.values }, c.context.url.origin, c.context.locals);
|
|
11
|
+
await logWrite(c, 'translate', `${params.pageId}:${params.sectionId}:${params.locale}`);
|
|
12
|
+
return result;
|
|
13
|
+
}, { write: true });
|
|
14
|
+
export const ALL = () => methodNotAllowed('GET, PUT, OPTIONS');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { editorRoute } from '../../../../../lib/editor/http';
|
|
2
|
+
import { SectionsQuery, listSections } from '../../../../../lib/editor/sections';
|
|
3
|
+
import { methodNotAllowed, queryParams } from '../../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
|
|
5
|
+
export const GET = editorRoute(async ({ repo, who, context }) => listSections(repo, SectionsQuery.parse(queryParams(context.url)), who));
|
|
6
|
+
export const ALL = () => methodNotAllowed('GET, OPTIONS');
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { authPreflight, revokeRoute } from '../../../../lib/editor/token-route';
|
|
3
|
+
// Editor apps: sign this device out (RFC 7009). Either token revokes the device.
|
|
4
|
+
export const POST: APIRoute = (context) => revokeRoute(context);
|
|
5
|
+
export const OPTIONS: APIRoute = () => authPreflight();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { authPreflight, tokenRoute } from '../../../../lib/editor/token-route';
|
|
3
|
+
// Editor apps: exchange a sign-in code (+ PKCE verifier) or a refresh token for device tokens.
|
|
4
|
+
export const POST: APIRoute = (context) => tokenRoute(context);
|
|
5
|
+
export const OPTIONS: APIRoute = () => authPreflight();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../../../../lib/repo';
|
|
3
|
+
import { unregisterDevice, updateDevice } from '../../../../lib/push/api';
|
|
4
|
+
import { failure, limitRequest, methodNotAllowed, response } from '../../../../lib/mobile/http';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
6
|
+
// PUT: change topics/locale. DELETE: opt out (idempotent).
|
|
7
|
+
function handler(run: (repo: Awaited<ReturnType<typeof getRepo>>, request: Request, token: string) => Promise<unknown>): APIRoute {
|
|
8
|
+
return async context => {
|
|
9
|
+
try {
|
|
10
|
+
const repo = await getRepo(context.locals);
|
|
11
|
+
let address = 'unknown';
|
|
12
|
+
try { address = context.clientAddress || address; } catch { /* shared fallback bucket */ }
|
|
13
|
+
await limitRequest(repo, address, true);
|
|
14
|
+
return response(await run(repo, context.request, context.params.token ?? ''));
|
|
15
|
+
} catch (error) { return failure(error); }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export const PUT = handler((repo, request, token) => updateDevice(repo, request, token));
|
|
19
|
+
export const DELETE = handler((repo, _request, token) => unregisterDevice(repo, token));
|
|
20
|
+
export const ALL = () => methodNotAllowed('PUT, DELETE, OPTIONS');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../../../../lib/repo';
|
|
3
|
+
import { registerDevice } from '../../../../lib/push/api';
|
|
4
|
+
import { failure, limitRequest, methodNotAllowed, response } from '../../../../lib/mobile/http';
|
|
5
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
6
|
+
// Register or refresh this device's Expo push token (only when push is enabled).
|
|
7
|
+
export const POST: APIRoute = async context => {
|
|
8
|
+
try {
|
|
9
|
+
const repo = await getRepo(context.locals);
|
|
10
|
+
let address = 'unknown';
|
|
11
|
+
try { address = context.clientAddress || address; } catch { /* shared fallback bucket */ }
|
|
12
|
+
await limitRequest(repo, address, true);
|
|
13
|
+
const result = await registerDevice(repo, context.request);
|
|
14
|
+
return response(result, result.created ? 201 : 200);
|
|
15
|
+
} catch (error) { return failure(error); }
|
|
16
|
+
};
|
|
17
|
+
export const ALL = () => methodNotAllowed('POST, OPTIONS');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getOpenApi } from '../../../lib/mobile/openapi';
|
|
2
|
+
import { readRoute } from '../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getOpenApi, true, true);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getPage } from '../../../../lib/mobile/pages';
|
|
2
|
+
import { readRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getPage);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getPage } from '../../../../lib/mobile/pages';
|
|
2
|
+
import { readRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute((repo, url) => getPage(repo, url, 'home'));
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { listPushTopics } from '../../../../lib/push/api';
|
|
2
|
+
import { readRoute } from '../../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(listPushTopics, true);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getSite } from '../../../lib/mobile/site';
|
|
2
|
+
import { readRoute } from '../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getSite);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getThemeTokens } from '../../../lib/mobile/site';
|
|
2
|
+
import { readRoute } from '../../../lib/mobile/route';
|
|
3
|
+
import { methodNotAllowed } from '../../../lib/mobile/http';
|
|
4
|
+
export { preflight as OPTIONS } from '../../../lib/mobile/http';
|
|
5
|
+
export const GET = readRoute(getThemeTokens);
|
|
6
|
+
export const HEAD = GET;
|
|
7
|
+
export const ALL = () => methodNotAllowed();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { requestLocale } from '../../lib/i18n';
|
|
3
|
+
import { localizeDoc, isExtraLocale } from '../../lib/translations';
|
|
2
4
|
import Site from '../../layouts/Site.astro';
|
|
3
5
|
import { getRepo } from '../../lib/repo';
|
|
4
6
|
import { renderMarkdown } from '../../lib/markdown';
|
|
@@ -20,7 +22,10 @@ if (!col) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
|
20
22
|
|
|
21
23
|
const { slug } = Astro.params;
|
|
22
24
|
const repo = await getRepo(Astro.locals);
|
|
23
|
-
|
|
25
|
+
// another language (/es/…): translated words over the shared post
|
|
26
|
+
const locale = requestLocale(Astro.url, Astro.locals);
|
|
27
|
+
const rawPost = slug ? await repo.getPostBySlug(slug, col.key) : null;
|
|
28
|
+
const post = rawPost ? localizeDoc(rawPost, locale) : null;
|
|
24
29
|
|
|
25
30
|
const isAdmin = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
|
|
26
31
|
if (!post || (post.status !== 'published' && !isAdmin)) {
|
|
@@ -35,7 +40,8 @@ const [tags, allUsers, allPosts, rawSettings, allMedia] = await Promise.all([
|
|
|
35
40
|
post.cover.startsWith('/media/') ? repo.listMedia() : Promise.resolve([]),
|
|
36
41
|
]);
|
|
37
42
|
// ?designPreview=1 (admin-only) renders the staged Design-screen changes
|
|
38
|
-
const
|
|
43
|
+
const baseSettings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies, Astro.locals);
|
|
44
|
+
const settings = isExtraLocale(locale) ? { ...baseSettings, locale } : baseSettings;
|
|
39
45
|
// ordered author objects; the first is the primary (gets the author card)
|
|
40
46
|
const authors = authorIds
|
|
41
47
|
.map((id) => allUsers.find((u) => u.id === id))
|
|
@@ -8,6 +8,8 @@ import { formatDate } from '../../lib/dates';
|
|
|
8
8
|
import { resolveDesign } from '../../lib/theme';
|
|
9
9
|
import { effectiveSettings } from '../../lib/designPreview';
|
|
10
10
|
import { collectionFromPath, collectionUrl, tagUrl } from '../../lib/content';
|
|
11
|
+
import { requestLocale, localePath } from '../../lib/i18n';
|
|
12
|
+
import { localizeDoc, isExtraLocale } from '../../lib/translations';
|
|
11
13
|
|
|
12
14
|
// One file serves every collection's index — injectRoute can't pass props,
|
|
13
15
|
// so the collection is resolved from the request path.
|
|
@@ -16,8 +18,13 @@ if (!col) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
|
16
18
|
|
|
17
19
|
const PAGE_SIZE = 12;
|
|
18
20
|
const repo = await getRepo(Astro.locals);
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
+
// another language (/es/blog): translated titles/excerpts, links stay in the language
|
|
22
|
+
const locale = requestLocale(Astro.url, Astro.locals);
|
|
23
|
+
const lp = (path: string) => (isExtraLocale(locale) ? localePath(path, locale) : path);
|
|
24
|
+
const [rawPosts, tags, users, rawSettings] = await Promise.all([repo.listPublishedPosts(col.key), repo.listTags(), repo.listUsers(), repo.getSettings()]);
|
|
25
|
+
const allPosts = rawPosts.map((p) => localizeDoc(p, locale));
|
|
26
|
+
const baseSettings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies, Astro.locals);
|
|
27
|
+
const settings = isExtraLocale(locale) ? { ...baseSettings, locale } : baseSettings;
|
|
21
28
|
const design = resolveDesign(settings);
|
|
22
29
|
|
|
23
30
|
// page 1 leads with the featured post (if any) as a big split card; it's
|
|
@@ -81,7 +88,7 @@ const iso = (ts: number | null) => (ts ? new Date(ts).toISOString() : undefined)
|
|
|
81
88
|
<PostCard
|
|
82
89
|
variant="feature"
|
|
83
90
|
headingLevel={2}
|
|
84
|
-
href={collectionUrl(col.key, lead.slug)}
|
|
91
|
+
href={lp(collectionUrl(col.key, lead.slug))}
|
|
85
92
|
title={lead.title}
|
|
86
93
|
excerpt={lead.excerpt}
|
|
87
94
|
cover={lead.cover}
|
|
@@ -99,7 +106,7 @@ const iso = (ts: number | null) => (ts ? new Date(ts).toISOString() : undefined)
|
|
|
99
106
|
<PostCard
|
|
100
107
|
variant={layout === 'cards' ? 'card' : 'row'}
|
|
101
108
|
headingLevel={2}
|
|
102
|
-
href={collectionUrl(col.key, p.slug)}
|
|
109
|
+
href={lp(collectionUrl(col.key, p.slug))}
|
|
103
110
|
title={p.title}
|
|
104
111
|
excerpt={p.excerpt}
|
|
105
112
|
cover={p.cover}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Site from '../../../layouts/Site.astro';
|
|
3
|
+
import ThemeSections from '../../../components/ThemeSections.astro';
|
|
4
|
+
import { getRepo } from '../../../lib/repo';
|
|
5
|
+
import { ApiError } from '../../../lib/api';
|
|
6
|
+
import { I18N } from '../../../lib/i18n';
|
|
7
|
+
import { embedSection, embedBridgeScript } from '../../../lib/mobile/embed';
|
|
8
|
+
|
|
9
|
+
// One published section, chrome-less, for mobile apps without a native
|
|
10
|
+
// component for it: GET /embed/section/<page slug|home>/<section id>
|
|
11
|
+
// ?locale=es the language to render (default: the site's)
|
|
12
|
+
// ?scheme=dark light/dark for themes that follow the device
|
|
13
|
+
// The document posts its height and link taps to the app (see
|
|
14
|
+
// lib/mobile/embed.ts); it never navigates itself.
|
|
15
|
+
const repo = await getRepo(Astro.locals);
|
|
16
|
+
let embed;
|
|
17
|
+
try {
|
|
18
|
+
embed = await embedSection(repo, Astro.url, Astro.params.path ?? '');
|
|
19
|
+
} catch (error) {
|
|
20
|
+
const status = error instanceof ApiError ? error.status : 500;
|
|
21
|
+
return new Response((status === 400 ? 'Bad request' : status === 404 ? 'Not found' : 'Error'), { status, headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store', 'X-Robots-Tag': 'noindex' } });
|
|
22
|
+
}
|
|
23
|
+
if (I18N.enabled && embed.locale && embed.locale !== I18N.defaultLocale) (Astro.locals as { acmsLocale?: string }).acmsLocale = embed.locale;
|
|
24
|
+
Astro.response.headers.set('Content-Security-Policy', 'frame-ancestors *');
|
|
25
|
+
Astro.response.headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
26
|
+
Astro.response.headers.set('X-Robots-Tag', 'noindex');
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<Site title="Section" bare noindex>
|
|
30
|
+
<div id="acms-embed" data-section={embed.sectionId} data-scheme={embed.scheme}>
|
|
31
|
+
<ThemeSections page={embed.page} settings={embed.settings} locale={embed.locale} editable={false} />
|
|
32
|
+
</div>
|
|
33
|
+
<style is:global>
|
|
34
|
+
html, body { background: var(--bg); }
|
|
35
|
+
main.site-main { max-width: none; margin: 0; padding: 0; }
|
|
36
|
+
#acms-embed { display: flow-root; }
|
|
37
|
+
#acms-embed .acms-sections { margin: 0; }
|
|
38
|
+
#acms-embed .acms-section { padding: 0; }
|
|
39
|
+
astro-dev-toolbar { display: none !important; }
|
|
40
|
+
</style>
|
|
41
|
+
<script is:inline set:html={embedBridgeScript(embed.sectionId)} />
|
|
42
|
+
</Site>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { requestLocale } from '../../lib/i18n';
|
|
3
|
+
import { localizeDoc, isExtraLocale } from '../../lib/translations';
|
|
2
4
|
import Site from '../../layouts/Site.astro';
|
|
3
5
|
import EventPage from '../../components/EventPage.astro';
|
|
4
6
|
import { getRepo } from '../../lib/repo';
|
|
@@ -21,13 +23,16 @@ import { waitUntilFrom } from '../../lib/webhooks';
|
|
|
21
23
|
// repeating event — the calendar grid links carry it.
|
|
22
24
|
if (!CALENDAR.enabled) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
23
25
|
const repo = await getRepo(Astro.locals);
|
|
24
|
-
const
|
|
26
|
+
const locale = requestLocale(Astro.url, Astro.locals);
|
|
27
|
+
const rawEvent = await repo.getEventBySlug(Astro.params.slug ?? '');
|
|
28
|
+
const event = rawEvent ? localizeDoc(rawEvent, locale) : null;
|
|
25
29
|
const [rawSettings, allCalendars] = await Promise.all([repo.getSettings(), repo.listCalendars()]);
|
|
26
30
|
const calMap = new Map(allCalendars.map((c) => [c.id, c]));
|
|
27
31
|
const isAdmin = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
|
|
28
32
|
if (!event || (!eventIsPublic(event, calMap) && !isAdmin)) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
29
33
|
|
|
30
|
-
const
|
|
34
|
+
const baseSettings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies, Astro.locals);
|
|
35
|
+
const settings = isExtraLocale(locale) ? { ...baseSettings, locale } : baseSettings;
|
|
31
36
|
const now = Date.now();
|
|
32
37
|
const on = Astro.url.searchParams.get('on') ?? undefined;
|
|
33
38
|
const occurrence = featuredOccurrence(event, settings, now, on && /^\d{4}-\d{2}-\d{2}$/.test(on) ? on : undefined);
|
package/src/pages/index.astro
CHANGED
|
@@ -14,14 +14,20 @@ const repo = await getRepo(Astro.locals);
|
|
|
14
14
|
const locale = I18N.enabled ? localeFromPath(Astro.url.pathname) : I18N.defaultLocale;
|
|
15
15
|
const wantsLocaleHome = I18N.enabled && locale !== I18N.defaultLocale;
|
|
16
16
|
let [home, rawSettings] = await Promise.all([
|
|
17
|
-
repo.getHomePage(
|
|
17
|
+
repo.getHomePage(),
|
|
18
18
|
repo.getSettings(),
|
|
19
19
|
]);
|
|
20
20
|
// ?designPreview=1 (admin-only) renders the staged Design-screen changes
|
|
21
21
|
const settings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies, Astro.locals);
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
22
|
+
// Another language: the default home page's section layout with that
|
|
23
|
+
// language's translated words, once it has any. A separately authored home
|
|
24
|
+
// page for the language (the older setup) keeps serving until then, and /es
|
|
25
|
+
// never 404s mid-setup.
|
|
26
|
+
if (wantsLocaleHome) {
|
|
27
|
+
const translated = Boolean(home && settings.sectionPages?.[home.id]?.sections.some((s) => s.translations?.[locale]));
|
|
28
|
+
const separate = translated ? null : await repo.getHomePage(locale);
|
|
29
|
+
if (separate) home = separate;
|
|
30
|
+
}
|
|
25
31
|
|
|
26
32
|
// Live state must not survive in a cached page after its snapshot expires.
|
|
27
33
|
if (settings.live.mode !== 'off' || home?.blocks.some(block => block.type === 'live') ||
|
|
@@ -46,7 +52,7 @@ const homeForTemplate = home ? { ...home, bodyHtml: homeHtml } : null;
|
|
|
46
52
|
|
|
47
53
|
<Site title={home?.seoTitle || undefined} description={home?.seoDesc || undefined}>
|
|
48
54
|
{home && settings.sectionPages?.[home.id]?.mode === 'sections' ? (
|
|
49
|
-
<ThemeSections page={home} settings={settings} />
|
|
55
|
+
<ThemeSections page={home} settings={settings} locale={locale} />
|
|
50
56
|
) : HomeTemplate ? (
|
|
51
57
|
<HomeTemplate settings={settings} page={homeForTemplate} posts={posts} tags={tags} />
|
|
52
58
|
) : home ? (
|
package/src/pages/sitemap.xml.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { siteOrigin, xmlEscape } from '../lib/seo';
|
|
|
5
5
|
import { CONTENT, collectionUrl, tagUrl } from '../lib/content';
|
|
6
6
|
import { CALENDAR, eventUrl, bookingsEnabled, resourceUrl } from '../lib/calendar/config';
|
|
7
7
|
import { eventIsPublic } from '../lib/calendar/events';
|
|
8
|
+
import { extraLocales, localePath } from '../lib/i18n';
|
|
9
|
+
import { hasTranslation } from '../lib/translations';
|
|
8
10
|
|
|
9
11
|
// DB-backed sitemap: content lives in D1/libSQL, so this is generated per
|
|
10
12
|
// request instead of at build time. Draft/scheduled content never appears.
|
|
@@ -49,6 +51,12 @@ export const GET: APIRoute = async ({ locals, url }) => {
|
|
|
49
51
|
...publicEvents.map((e) => ({ loc: eventUrl(e.slug), lastmod: e.updatedAt })),
|
|
50
52
|
]
|
|
51
53
|
: []),
|
|
54
|
+
// translated documents also live at /<locale>/… (acms({ i18n }))
|
|
55
|
+
...extraLocales().flatMap((l) => [
|
|
56
|
+
...pages.filter((p) => p.status === 'published' && !p.isHome && hasTranslation(p, l)).map((p) => ({ loc: localePath(`/${p.slug}`, l), lastmod: p.updatedAt })),
|
|
57
|
+
...posts.filter((p) => hasTranslation(p, l)).map((p) => ({ loc: localePath(collectionUrl(p.collection, p.slug), l), lastmod: p.updatedAt })),
|
|
58
|
+
...publicEvents.filter((e) => hasTranslation(e, l)).map((e) => ({ loc: localePath(eventUrl(e.slug), l), lastmod: e.updatedAt })),
|
|
59
|
+
]),
|
|
52
60
|
...(bookingsEnabled()
|
|
53
61
|
? [{ loc: CALENDAR.bookBase }, ...(await repo.listResources()).filter((r) => r.visibility === 'public').map((r) => ({ loc: resourceUrl(r.slug), lastmod: r.updatedAt }))]
|
|
54
62
|
: []),
|
package/src/styles/admin.css
CHANGED
|
@@ -756,6 +756,43 @@ dialog.modal[open] { animation: modal-in 0.16s ease-out; }
|
|
|
756
756
|
.login-wrap { background: var(--bg); }
|
|
757
757
|
.btn.block { width: 100%; justify-content: center; min-height: 38px; }
|
|
758
758
|
.login-note { margin-top: 14px; }
|
|
759
|
+
.login-for-app { text-align: center; margin: -8px 0 14px; color: var(--muted); }
|
|
760
|
+
|
|
761
|
+
/* editor-app sign-in (consent) — same paper card as login, a little wider */
|
|
762
|
+
.consent-wrap { align-items: start; padding-top: max(8vh, 20px); }
|
|
763
|
+
.consent-card { width: 100%; max-width: 440px; padding: 0; overflow: hidden; }
|
|
764
|
+
.consent-site { display: flex; align-items: center; gap: 8px; padding: 14px 22px; border-bottom: 1px solid var(--line); color: var(--text-2); font-weight: 600; font-size: 13px; min-width: 0; }
|
|
765
|
+
.consent-site span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
766
|
+
.consent-body { padding: 22px 22px 18px; }
|
|
767
|
+
.consent-body h1 { font-size: 20px; line-height: 1.3; letter-spacing: -0.02em; margin: 0 0 6px; overflow-wrap: anywhere; }
|
|
768
|
+
.consent-app { color: var(--text); }
|
|
769
|
+
.consent-lead { color: var(--muted); margin: 0 0 18px; }
|
|
770
|
+
.consent-lead b { color: var(--text-2); font-weight: 600; }
|
|
771
|
+
.consent-who { display: flex; align-items: center; gap: 12px; padding: 12px; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-md); margin-bottom: 18px; min-width: 0; }
|
|
772
|
+
.consent-initial { flex: none; width: 36px; height: 36px; border-radius: var(--r-md); display: grid; place-items: center; background: var(--accent-soft); color: var(--accent-deep); font-weight: 700; font-size: 15px; }
|
|
773
|
+
.consent-id { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
|
774
|
+
.consent-id b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
775
|
+
.consent-id span { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
776
|
+
.consent-role { flex: none; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--text-2); }
|
|
777
|
+
.consent-role i { width: 7px; height: 7px; border-radius: 2px; background: var(--blue); }
|
|
778
|
+
.consent-role i.is-owner { background: var(--accent); }
|
|
779
|
+
.consent-label { font-size: 12px; font-weight: 600; color: var(--muted); margin: 0 0 6px; }
|
|
780
|
+
.consent-can { list-style: none; margin: 0 0 18px; padding: 0; display: grid; gap: 6px; }
|
|
781
|
+
.consent-can li { position: relative; padding-left: 20px; color: var(--text-2); }
|
|
782
|
+
.consent-can li::before { content: ''; position: absolute; left: 3px; top: 0.45em; width: 8px; height: 5px; border-left: 2px solid var(--green); border-bottom: 2px solid var(--green); transform: rotate(-45deg); }
|
|
783
|
+
.consent-device { margin-bottom: 16px; }
|
|
784
|
+
.consent-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
785
|
+
.consent-actions .btn { justify-content: center; min-height: 40px; }
|
|
786
|
+
.consent-meta { margin: 18px 0 0; padding-top: 14px; border-top: 1px solid var(--line); display: grid; gap: 6px; font-size: 12px; }
|
|
787
|
+
.consent-meta div { display: flex; gap: 10px; min-width: 0; }
|
|
788
|
+
.consent-meta dt { color: var(--faint); flex: 0 0 76px; }
|
|
789
|
+
.consent-meta dd { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--text-2); }
|
|
790
|
+
.consent-meta code { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 11.5px; }
|
|
791
|
+
.consent-foot { margin: 12px 0 0; }
|
|
792
|
+
@media (max-width: 480px) {
|
|
793
|
+
.consent-wrap { padding: 0; align-items: stretch; }
|
|
794
|
+
.consent-card { max-width: none; border-radius: 0; border: 0; min-height: 100vh; box-shadow: none; }
|
|
795
|
+
}
|
|
759
796
|
|
|
760
797
|
/* recent list on dashboard */
|
|
761
798
|
.recent { list-style: none; margin: 0; padding: 0; }
|
|
@@ -1473,3 +1510,35 @@ a.bk-row:hover, .bk-row.is-sel { background: var(--hover); }
|
|
|
1473
1510
|
.dm-hist li { flex-direction: column; align-items: flex-start; }
|
|
1474
1511
|
.dm-status-actions { margin-left: 0; width: 100%; }
|
|
1475
1512
|
}
|
|
1513
|
+
|
|
1514
|
+
/* role: marker + text (no badges) */
|
|
1515
|
+
.role-mark { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 600; color: var(--text-2); }
|
|
1516
|
+
.role-mark i { width: 7px; height: 7px; border-radius: 2px; background: var(--blue); }
|
|
1517
|
+
.role-mark i.is-owner { background: var(--accent); }
|
|
1518
|
+
|
|
1519
|
+
/* Users → Signed-in apps */
|
|
1520
|
+
.devices-card { margin-top: 20px; }
|
|
1521
|
+
.devices-head { padding: 16px 20px 12px; border-bottom: 1px solid var(--line); }
|
|
1522
|
+
.devices-head h2 { font-size: 15px; margin: 0 0 2px; letter-spacing: -0.01em; }
|
|
1523
|
+
.devices-head .hint { margin: 0; }
|
|
1524
|
+
.devices-empty { color: var(--muted); padding: 18px 20px 20px; margin: 0; }
|
|
1525
|
+
.devices-group + .devices-group { border-top: 1px solid var(--line); }
|
|
1526
|
+
.devices-person { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 20px 2px; min-width: 0; }
|
|
1527
|
+
.devices-person > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1528
|
+
.devices-person .t-sub { color: var(--faint); font-size: 12px; }
|
|
1529
|
+
.devices-list { list-style: none; margin: 0; padding: 4px 8px 8px; }
|
|
1530
|
+
.device-row { display: grid; grid-template-columns: 32px minmax(0, 1.2fr) minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 10px 12px; border-radius: var(--r-md); }
|
|
1531
|
+
.device-row:hover { background: var(--hover); }
|
|
1532
|
+
.device-icon { width: 32px; height: 32px; border-radius: var(--r-sm); display: grid; place-items: center; background: var(--panel-2); border: 1px solid var(--line); color: var(--muted); }
|
|
1533
|
+
.device-main { display: flex; flex-direction: column; min-width: 0; }
|
|
1534
|
+
.device-main b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1535
|
+
.device-main .t-sub { color: var(--faint); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 11.5px; }
|
|
1536
|
+
.device-when { display: flex; flex-wrap: wrap; column-gap: 16px; row-gap: 2px; color: var(--text-2); font-size: 12.5px; font-variant-numeric: tabular-nums; }
|
|
1537
|
+
.device-when > span { white-space: nowrap; }
|
|
1538
|
+
.device-k { color: var(--faint); margin-right: 3px; }
|
|
1539
|
+
@media (max-width: 640px) {
|
|
1540
|
+
.device-row { grid-template-columns: 32px minmax(0, 1fr) auto; row-gap: 4px; }
|
|
1541
|
+
.device-when { grid-column: 2 / 4; grid-row: 2; }
|
|
1542
|
+
.device-action { grid-column: 3; grid-row: 1; }
|
|
1543
|
+
.devices-head, .devices-person { padding-left: 16px; padding-right: 16px; }
|
|
1544
|
+
}
|
package/src/styles/editor.css
CHANGED
|
@@ -1024,3 +1024,24 @@
|
|
|
1024
1024
|
.pe-evcard-one small { display: block; font-size: 11.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--accent-deep); }
|
|
1025
1025
|
.pe-evcard-one b { display: block; font-size: 15px; color: var(--text); margin-top: 2px; }
|
|
1026
1026
|
.pe-evcard-one em { display: block; font-style: normal; font-size: 12px; color: var(--amber); margin-top: 3px; }
|
|
1027
|
+
|
|
1028
|
+
/* ---- translations: language switch + panel ---- */
|
|
1029
|
+
.pe-langs button { position: relative; padding: 4px 10px; font-weight: 600; letter-spacing: .02em; }
|
|
1030
|
+
.pe-lang-dot { position: absolute; top: 3px; right: 3px; width: 6px; height: 6px; border-radius: 2px; background: var(--amber, #93660c); }
|
|
1031
|
+
.pe-trpanel { margin: 0 0 18px; padding: 14px 16px; border-radius: var(--r-md); background: var(--panel-2); font-size: 13px; color: var(--text-2); }
|
|
1032
|
+
.pe-tr-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px; }
|
|
1033
|
+
.pe-tr-head b { font-size: 14px; color: var(--text); }
|
|
1034
|
+
.pe-tr-states { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 12px; }
|
|
1035
|
+
.pe-tr-states .is-missing { color: var(--red); }
|
|
1036
|
+
.pe-tr-states .is-outdated { color: var(--amber, #93660c); }
|
|
1037
|
+
.pe-tr-states .is-ai { color: var(--blue, #2f5fb3); }
|
|
1038
|
+
.pe-tr-states .is-reviewed { color: var(--green, #127442); }
|
|
1039
|
+
.pe-tr-note { margin: 6px 0 10px; color: var(--muted); font-size: 12.5px; }
|
|
1040
|
+
.pe-tr-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 10px; }
|
|
1041
|
+
.pe-tr-fields label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 500; color: var(--muted); }
|
|
1042
|
+
.pe-tr-fields input, .pe-tr-fields textarea { font: inherit; font-size: 13.5px; color: var(--text); padding: 7px 10px; border: 1px solid var(--line-strong); border-radius: var(--r-sm); background: var(--bg); resize: vertical; }
|
|
1043
|
+
.pe-tr-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; }
|
|
1044
|
+
.pe-tr-msg { font-size: 12.5px; color: var(--muted); }
|
|
1045
|
+
@media (max-width: 767px) { .pe-langs { order: 3; } .pe-tr-fields input, .pe-tr-fields textarea { font-size: 16px; } }
|
|
1046
|
+
.pe-seg button[aria-checked='true'] { background: var(--bg); color: var(--text); box-shadow: var(--shadow-sm); }
|
|
1047
|
+
:root[data-theme='dark'] .pe-seg button[aria-checked='true'] { background: #3a3632; }
|