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