@acms-subir/core 0.2.0 → 0.3.1
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 +87 -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 -369
- 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/live-video.md +91 -0
- package/docs/theme-authoring.md +103 -0
- package/email-runner.mjs +32 -0
- package/integration.mjs +189 -2
- package/ops-check.mjs +27 -0
- package/package.json +27 -5
- package/src/actions/index.ts +578 -49
- 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 +418 -46
- 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 +119 -58
- package/src/components/LiveCard.astro +174 -0
- package/src/components/LiveManager.tsx +910 -0
- package/src/components/NavManager.tsx +42 -43
- package/src/components/ThemeScript.astro +50 -0
- package/src/components/ThemeSections.astro +58 -0
- package/src/components/ThemeSectionsEditor.tsx +513 -0
- package/src/components/WidgetRenderer.astro +3 -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 +605 -223
- package/src/components/editor/PreviewOverlay.tsx +242 -43
- package/src/components/editor/extensions/calendar.tsx +51 -7
- package/src/components/editor/extensions/events.tsx +153 -0
- package/src/components/editor/extensions/slash.tsx +7 -1
- 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 +352 -59
- 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/api-keys.ts +65 -0
- package/src/lib/api.ts +56 -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 +110 -12
- 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 +60 -6
- package/src/lib/dynamicBlocks.ts +40 -8
- 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/live/client.ts +88 -0
- package/src/lib/live/crypto.ts +28 -0
- package/src/lib/live/migration.ts +15 -0
- package/src/lib/live/providers.ts +152 -0
- package/src/lib/live/service.ts +170 -0
- package/src/lib/live/signals.ts +89 -0
- package/src/lib/live/store.ts +49 -0
- package/src/lib/live/types.ts +52 -0
- package/src/lib/live/view.ts +78 -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 +221 -0
- package/src/lib/repo/sql.ts +907 -68
- package/src/lib/repo/types.ts +125 -5
- package/src/lib/scheduler.ts +27 -0
- package/src/lib/schema.ts +316 -1
- 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 +183 -0
- package/src/lib/umami.ts +108 -0
- package/src/lib/webhook-sql.ts +68 -0
- package/src/lib/webhooks.ts +147 -0
- package/src/lib/widgets.ts +1 -0
- package/src/middleware.ts +45 -3
- package/src/pages/404.astro +23 -0
- package/src/pages/[...slug].astro +14 -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 +4 -1
- package/src/pages/admin/c/[collection]/index.astro +127 -92
- 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 +4 -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 +10 -3
- package/src/pages/admin/email.astro +13 -0
- package/src/pages/admin/index.astro +232 -85
- package/src/pages/admin/integrations.astro +237 -0
- package/src/pages/admin/links.astro +3 -3
- package/src/pages/admin/live.astro +12 -0
- package/src/pages/admin/login.astro +14 -10
- package/src/pages/admin/navigation.astro +8 -2
- package/src/pages/admin/pages/[id].astro +4 -1
- package/src/pages/admin/pages/index.astro +41 -48
- package/src/pages/admin/settings.astro +418 -367
- 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 +66 -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/live/oauth/youtube/callback.ts +19 -0
- package/src/pages/api/live/settings.ts +45 -0
- package/src/pages/api/live/status.ts +7 -0
- package/src/pages/api/live/thumb.ts +48 -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/api/webhooks/docs.ts +61 -0
- package/src/pages/author/[slug].astro +43 -35
- package/src/pages/blog/[slug].astro +137 -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 +12 -3
- package/src/pages/legal/privacy.astro +4 -0
- package/src/pages/legal/terms.astro +4 -0
- package/src/pages/links.astro +29 -3
- package/src/pages/llms.txt.ts +4 -0
- package/src/pages/preview/[type]/[id].astro +27 -6
- package/src/pages/search.astro +40 -20
- package/src/pages/sitemap.xml.ts +16 -1
- package/src/styles/admin.css +924 -105
- package/src/styles/cards.css +92 -74
- package/src/styles/editor.css +492 -114
- package/src/styles/email-settings.css +117 -0
- package/src/styles/live-admin.css +126 -0
- package/src/styles/live.css +199 -0
- package/src/styles/sections.css +12 -0
- package/src/styles/site.css +938 -281
- package/src/styles/theme-editor.css +135 -0
- package/src/theme-sdk/contract.d.mts +619 -0
- package/src/theme-sdk/contract.mjs +81 -0
- package/src/theme-sdk/index.ts +15 -0
- package/src/theme-sdk/tokens.d.mts +148 -0
- package/src/theme-sdk/tokens.mjs +55 -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 +27 -37
- package/src/themes/editorial/theme.json +25 -3
- package/src/themes/field/theme.json +27 -9
- package/src/themes/ink/theme.json +30 -12
- package/src/themes/night/theme.css +4 -7
- package/src/themes/night/theme.json +28 -10
- package/src/virtual.d.ts +14 -0
- package/src/worker.ts +5 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
// One post teaser in three shapes, shared by the blog index, tag and author
|
|
3
|
+
// archives and "Keep reading":
|
|
4
|
+
// card — 16:10 cover (or a generated tile), meta line, title, excerpt
|
|
5
|
+
// row — text-first list row with a small thumbnail on the right
|
|
6
|
+
// feature — the big split card for the featured post
|
|
7
|
+
// Posts without a cover get a quiet generated tile (soft fill + first tag),
|
|
8
|
+
// so a grid never goes ragged.
|
|
9
|
+
import TagLabel from './TagLabel.astro';
|
|
10
|
+
import { tagColor } from '../../lib/tagColor';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
variant?: 'card' | 'row' | 'feature';
|
|
14
|
+
href: string;
|
|
15
|
+
title: string;
|
|
16
|
+
excerpt?: string;
|
|
17
|
+
cover?: string;
|
|
18
|
+
coverAlt?: string;
|
|
19
|
+
date?: string;
|
|
20
|
+
datetime?: string;
|
|
21
|
+
readMins?: number;
|
|
22
|
+
tags?: { name: string; href: string }[];
|
|
23
|
+
featured?: boolean;
|
|
24
|
+
author?: { name: string; avatar?: string; href: string } | null;
|
|
25
|
+
headingLevel?: 2 | 3;
|
|
26
|
+
}
|
|
27
|
+
const {
|
|
28
|
+
variant = 'card', href, title, excerpt, cover, coverAlt = '', date, datetime, readMins,
|
|
29
|
+
tags = [], featured = false, author = null, headingLevel = 3,
|
|
30
|
+
} = Astro.props;
|
|
31
|
+
const H = `h${headingLevel}` as 'h2' | 'h3';
|
|
32
|
+
const firstTag = tags[0]?.name;
|
|
33
|
+
const shownTags = tags.slice(0, variant === 'feature' ? 2 : 1);
|
|
34
|
+
---
|
|
35
|
+
<article class:list={['post-card', `is-${variant}`]}>
|
|
36
|
+
{variant !== 'row' && (
|
|
37
|
+
<a class="post-card-media" href={href} tabindex="-1" aria-hidden="true">
|
|
38
|
+
{cover
|
|
39
|
+
? <img src={cover} alt={coverAlt} loading={variant === 'feature' ? 'eager' : 'lazy'} />
|
|
40
|
+
: <span class="post-card-tile" style={firstTag ? `--tc:${tagColor(firstTag)}` : undefined}><span>{firstTag ?? title}</span></span>}
|
|
41
|
+
</a>
|
|
42
|
+
)}
|
|
43
|
+
<div class="post-card-body">
|
|
44
|
+
<div class="post-card-meta">
|
|
45
|
+
{featured && <TagLabel name="Featured" accent />}
|
|
46
|
+
{shownTags.map((t) => <TagLabel name={t.name} href={t.href} />)}
|
|
47
|
+
{date && <time datetime={datetime}>{date}</time>}
|
|
48
|
+
{readMins && <span class="post-card-read">{readMins} min read</span>}
|
|
49
|
+
</div>
|
|
50
|
+
<H class="post-card-title"><a href={href}>{title}</a></H>
|
|
51
|
+
{excerpt && <p class="post-card-excerpt">{excerpt}</p>}
|
|
52
|
+
{variant === 'feature' && author && (
|
|
53
|
+
<a class="post-card-author" href={author.href}>
|
|
54
|
+
{author.avatar
|
|
55
|
+
? <img class="avatar" src={author.avatar} alt="" />
|
|
56
|
+
: <span class="avatar avatar-empty" aria-hidden="true"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="8.5" r="3.5" /><path d="M5 20a7 7 0 0 1 14 0" /></svg></span>}
|
|
57
|
+
<span>{author.name}</span>
|
|
58
|
+
</a>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
{variant === 'row' && cover && (
|
|
62
|
+
<a class="post-card-thumb" href={href} tabindex="-1" aria-hidden="true"><img src={cover} alt="" loading="lazy" /></a>
|
|
63
|
+
)}
|
|
64
|
+
</article>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A tag as marker + text. `href` makes it a link; `accent` swaps the hashed
|
|
3
|
+
// color for the theme accent (used for "Featured").
|
|
4
|
+
import { tagColor } from '../../lib/tagColor';
|
|
5
|
+
interface Props { name: string; href?: string; accent?: boolean }
|
|
6
|
+
const { name, href, accent = false } = Astro.props;
|
|
7
|
+
const style = accent ? undefined : `--tc:${tagColor(name)}`;
|
|
8
|
+
---
|
|
9
|
+
{href
|
|
10
|
+
? <a class:list={['tag', { 'is-accent': accent }]} href={href} style={style}>{name}</a>
|
|
11
|
+
: <span class:list={['tag', { 'is-accent': accent }]} style={style}>{name}</span>}
|
package/src/layouts/Admin.astro
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { getRepo } from '../lib/repo';
|
|
3
3
|
import { BRAND } from '../lib/brand';
|
|
4
4
|
import { CONTENT } from '../lib/content';
|
|
5
|
+
import { CALENDAR, bookingsEnabled } from '../lib/calendar/config';
|
|
6
|
+
import { validateSession, SESSION_COOKIE, OWNER_ENV } from '../lib/auth';
|
|
7
|
+
import ThemeScript from '../components/ThemeScript.astro';
|
|
8
|
+
import CommandPalette from '../components/CommandPalette';
|
|
5
9
|
import '../styles/admin.css';
|
|
6
10
|
|
|
7
11
|
interface Props {
|
|
@@ -14,12 +18,24 @@ interface Props {
|
|
|
14
18
|
const { title, section, wide = false } = Astro.props;
|
|
15
19
|
|
|
16
20
|
const repo = await getRepo(Astro.locals);
|
|
17
|
-
const
|
|
21
|
+
const sessionUserId = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
|
|
22
|
+
const [pages, posts, links, home, settings, sessionUser] = await Promise.all([
|
|
18
23
|
repo.listPages(),
|
|
19
24
|
repo.listPosts(),
|
|
20
25
|
repo.listLinks(),
|
|
21
26
|
repo.getHomePage(),
|
|
27
|
+
repo.getSettings(),
|
|
28
|
+
sessionUserId && sessionUserId !== OWNER_ENV ? repo.getUser(sessionUserId) : Promise.resolve(null),
|
|
22
29
|
]);
|
|
30
|
+
// pending booking requests, shown as a count on the Bookings nav item
|
|
31
|
+
const pendingBookings = bookingsEnabled() ? (await repo.listBookings({ from: Date.now(), statuses: ['pending'] })).length : 0;
|
|
32
|
+
const me = {
|
|
33
|
+
name: sessionUser?.name || 'Admin',
|
|
34
|
+
role: sessionUserId === OWNER_ENV ? 'Owner' : sessionUser?.role === 'owner' ? 'Owner' : 'Editor',
|
|
35
|
+
avatar: sessionUser?.avatar || '',
|
|
36
|
+
};
|
|
37
|
+
const initials = me.name.split(/\s+/).map((w) => w[0] ?? '').join('').slice(0, 2).toUpperCase();
|
|
38
|
+
const siteName = settings.siteTitle || BRAND.name;
|
|
23
39
|
|
|
24
40
|
const icons: Record<string, string> = {
|
|
25
41
|
dashboard: 'M3 3h7v9H3zM14 3h7v5h-7zM14 12h7v9h-7zM3 16h7v5H3z',
|
|
@@ -30,34 +46,107 @@ const icons: Record<string, string> = {
|
|
|
30
46
|
links: 'M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1.5 1.5M14 10a4 4 0 0 0-6 0l-3 3a4 4 0 0 0 6 6l1.5-1.5',
|
|
31
47
|
tags: 'M3 12V3h9l9 9-9 9zM7.5 7.5h0',
|
|
32
48
|
users: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8M22 21v-2a4 4 0 0 0-3-3.87M15 3.13a4 4 0 0 1 0 7.75',
|
|
33
|
-
settings: '
|
|
49
|
+
settings: 'M4 7h10M18 7h2M4 17h4M12 17h8M14 5v4M8 15v4',
|
|
34
50
|
design: 'M12 3c3.2 3.7 6 7 6 10.2a6 6 0 1 1-12 0C6 10 8.8 6.7 12 3zM9.5 14.5a2.5 2.5 0 0 0 5 0',
|
|
35
51
|
navigation: 'M4 6h16M4 12h10M4 18h13M19 11l3 3-3 3',
|
|
52
|
+
search: 'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4',
|
|
53
|
+
write: 'M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4',
|
|
54
|
+
chev: 'M6 9l6 6 6-6',
|
|
55
|
+
chevr: 'M9 6l6 6-6 6',
|
|
56
|
+
updown: 'M8 9l4-4 4 4M8 15l4 4 4-4',
|
|
57
|
+
side: 'M4 4h16v16H4zM9 4v16',
|
|
58
|
+
sun: 'M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4',
|
|
59
|
+
moon: 'M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5z',
|
|
60
|
+
monitor: 'M3 4h18v12H3zM8 20h8M12 16v4',
|
|
61
|
+
globe: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.4 3.8 5.6 3.8 9S14.5 18.6 12 21c-2.5-2.4-3.8-5.6-3.8-9S9.5 5.4 12 3z',
|
|
62
|
+
out: 'M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3',
|
|
63
|
+
more: 'M5 12h.01M12 12h.01M19 12h.01',
|
|
64
|
+
user: 'M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21a8 8 0 0 1 16 0',
|
|
65
|
+
calendar: 'M3 5.5h18v15H3zM3 10h18M8 3v4M16 3v4',
|
|
66
|
+
calendars: 'M4 6h.01M4 12h.01M4 18h.01M8 6h12M8 12h12M8 18h12',
|
|
67
|
+
bookings: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2',
|
|
68
|
+
resources: 'M3 21V9l9-5 9 5v12M9 21v-6h6v6',
|
|
69
|
+
plug: 'M9 7V3M15 7V3M6 7h12v4a6 6 0 0 1-12 0zM12 17v4',
|
|
36
70
|
};
|
|
37
71
|
|
|
38
72
|
// one sidebar entry per configured collection, in config order
|
|
39
73
|
const byCollection = new Map<string, number>();
|
|
40
74
|
for (const p of posts) byCollection.set(p.collection, (byCollection.get(p.collection) ?? 0) + 1);
|
|
41
|
-
const
|
|
75
|
+
const collections = Object.entries(CONTENT.collections);
|
|
76
|
+
const firstCollection = collections[0]?.[0] ?? 'posts';
|
|
77
|
+
const collectionItems = collections.map(([key, c]) => ({
|
|
42
78
|
key: `c:${key}`,
|
|
43
79
|
label: c.plural,
|
|
44
80
|
href: `/admin/c/${key}`,
|
|
45
81
|
icon: icons.posts,
|
|
46
|
-
count: byCollection.get(key) ?? 0,
|
|
82
|
+
count: byCollection.get(key) ?? 0 as number | null,
|
|
47
83
|
}));
|
|
48
84
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
|
|
85
|
+
type Item = { key: string; label: string; href: string; icon: string; count: number | null; attention?: boolean };
|
|
86
|
+
const groups: { id: string; label: string; items: Item[] }[] = [
|
|
87
|
+
{
|
|
88
|
+
id: 'main',
|
|
89
|
+
label: '',
|
|
90
|
+
items: [
|
|
91
|
+
{ key: 'dashboard', label: 'Overview', href: '/admin', icon: icons.dashboard, count: null },
|
|
92
|
+
{ key: 'home', label: 'Home page', href: home ? `/admin/pages/${home.id}` : '/admin/pages', icon: icons.home, count: null },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'content',
|
|
97
|
+
label: 'Content',
|
|
98
|
+
items: [
|
|
99
|
+
...collectionItems,
|
|
100
|
+
{ key: 'pages', label: 'Pages', href: '/admin/pages', icon: icons.pages, count: pages.length },
|
|
101
|
+
{ key: 'tags', label: 'Tags', href: '/admin/tags', icon: icons.tags, count: null },
|
|
102
|
+
{ key: 'media', label: 'Media', href: '/admin/media', icon: icons.media, count: null },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
...(CALENDAR.enabled
|
|
106
|
+
? [{
|
|
107
|
+
id: 'calendar',
|
|
108
|
+
label: 'Calendar',
|
|
109
|
+
items: [
|
|
110
|
+
{ key: 'calendar:events', label: 'Events', href: '/admin/calendar/events', icon: icons.calendar, count: null },
|
|
111
|
+
...(bookingsEnabled()
|
|
112
|
+
? [
|
|
113
|
+
{ key: 'calendar:bookings', label: 'Bookings', href: '/admin/calendar/bookings', icon: icons.bookings, count: pendingBookings || null, attention: pendingBookings > 0 },
|
|
114
|
+
{ key: 'calendar:resources', label: 'Resources', href: '/admin/calendar/resources', icon: icons.resources, count: null },
|
|
115
|
+
]
|
|
116
|
+
: []),
|
|
117
|
+
{ key: 'calendar:calendars', label: 'Calendars', href: '/admin/calendar/calendars', icon: icons.calendars, count: null },
|
|
118
|
+
],
|
|
119
|
+
}]
|
|
120
|
+
: []),
|
|
121
|
+
{
|
|
122
|
+
id: 'site',
|
|
123
|
+
label: 'Site',
|
|
124
|
+
items: [
|
|
125
|
+
{ key: 'design', label: 'Design', href: '/admin/design', icon: icons.design, count: null },
|
|
126
|
+
{ key: 'navigation', label: 'Navigation', href: '/admin/navigation', icon: icons.navigation, count: null },
|
|
127
|
+
{ key: 'links', label: 'Links page', href: '/admin/links', icon: icons.links, count: links.length },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'team',
|
|
132
|
+
label: 'Team',
|
|
133
|
+
items: [
|
|
134
|
+
{ key: 'users', label: 'Users', href: '/admin/users', icon: icons.users, count: null },
|
|
135
|
+
{ key: 'ai', label: 'Document import', href: '/admin/ai', icon: icons.posts, count: null },
|
|
136
|
+
{ key: 'integrations', label: 'API & webhooks', href: '/admin/integrations', icon: icons.plug, count: null },
|
|
137
|
+
{ key: 'settings', label: 'Settings', href: '/admin/settings', icon: icons.settings, count: null },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
const activeGroup = groups.find((g) => g.items.some((i) => i.key === section));
|
|
142
|
+
const crumbGroup = activeGroup?.label ?? '';
|
|
143
|
+
|
|
144
|
+
// bottom tab bar (phones): Home · Content · Links · Search · More
|
|
145
|
+
const tabActive = section === 'dashboard' ? 'home' : section.startsWith('c:') || section === 'pages' ? 'content' : section === 'links' ? 'links' : 'more';
|
|
146
|
+
const themeOpts = [
|
|
147
|
+
{ key: 'light', label: 'Light', icon: icons.sun },
|
|
148
|
+
{ key: 'dark', label: 'Dark', icon: icons.moon },
|
|
149
|
+
{ key: 'system', label: 'Auto', icon: icons.monitor },
|
|
61
150
|
];
|
|
62
151
|
---
|
|
63
152
|
|
|
@@ -65,55 +154,206 @@ const items = [
|
|
|
65
154
|
<html lang="en">
|
|
66
155
|
<head>
|
|
67
156
|
<meta charset="utf-8" />
|
|
68
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
157
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
69
158
|
<meta name="robots" content="noindex" />
|
|
70
159
|
<title>{title} · {BRAND.name} admin</title>
|
|
160
|
+
<ThemeScript />
|
|
161
|
+
<script is:inline>
|
|
162
|
+
// restore the collapsed rail before paint
|
|
163
|
+
try { if (localStorage.getItem('acms-rail') === '1') document.documentElement.classList.add('rail-pending'); } catch {}
|
|
164
|
+
</script>
|
|
71
165
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
72
166
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
73
167
|
</head>
|
|
74
168
|
<body>
|
|
75
|
-
<div class="shell">
|
|
76
|
-
<aside class="sidebar">
|
|
169
|
+
<div class="shell" id="acms-shell">
|
|
170
|
+
<aside class="sidebar" aria-label="Admin navigation">
|
|
77
171
|
<div class="brand">
|
|
78
|
-
<svg width="
|
|
79
|
-
<rect x="1" y="3" width="22" height="4.4" rx="1.9" fill="
|
|
80
|
-
<rect x="1" y="10" width="12.5" height="4.4" rx="1.9" fill="
|
|
81
|
-
<rect x="1" y="17" width="17.5" height="4.4" rx="1.9" fill="
|
|
172
|
+
<svg width="20" height="20" viewBox="0 0 24 24" role="img" aria-label={BRAND.name}>
|
|
173
|
+
<rect x="1" y="3" width="22" height="4.4" rx="1.9" fill="currentColor" opacity="0.85" />
|
|
174
|
+
<rect x="1" y="10" width="12.5" height="4.4" rx="1.9" fill="var(--accent)" />
|
|
175
|
+
<rect x="1" y="17" width="17.5" height="4.4" rx="1.9" fill="currentColor" opacity="0.35" />
|
|
82
176
|
</svg>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
</
|
|
177
|
+
<span class="brand-name" title={`v${BRAND.version} · ${BRAND.stack}`}>{siteName}</span>
|
|
178
|
+
<button class="icon-btn" type="button" id="acms-rail-toggle" aria-label="Collapse sidebar" title="Collapse sidebar">
|
|
179
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.side} /></svg>
|
|
180
|
+
</button>
|
|
87
181
|
</div>
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</a>
|
|
96
|
-
))}
|
|
97
|
-
<div class="nav-sep" />
|
|
98
|
-
<a class="nav-item" href="/" target="_blank" rel="noopener">
|
|
99
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
100
|
-
<path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.4 3.8 5.6 3.8 9S14.5 18.6 12 21c-2.5-2.4-3.8-5.6-3.8-9S9.5 5.4 12 3z" />
|
|
101
|
-
</svg>
|
|
102
|
-
View site
|
|
182
|
+
<button class="side-search" type="button" data-cmdk title="Search or jump to (⌘K)">
|
|
183
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d={icons.search} /></svg>
|
|
184
|
+
<span>Search</span><kbd>⌘K</kbd>
|
|
185
|
+
</button>
|
|
186
|
+
<a class="nav-item" href={`/admin/c/${firstCollection}?new=1`} title={`Write a ${(CONTENT.collections[firstCollection] ?? CONTENT.posts).singular.toLowerCase()}`}>
|
|
187
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.write} /></svg>
|
|
188
|
+
<span>Write a {(CONTENT.collections[firstCollection] ?? CONTENT.posts).singular.toLowerCase()}</span>
|
|
103
189
|
</a>
|
|
190
|
+
<nav class="nav-scroll">
|
|
191
|
+
{groups.map((g) => {
|
|
192
|
+
const links = g.items.map((it) => (
|
|
193
|
+
<a class={`nav-item ${section === it.key ? 'active' : ''}`} href={it.href} title={it.label} aria-current={section === it.key ? 'page' : undefined}>
|
|
194
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={it.icon} /></svg>
|
|
195
|
+
<span>{it.label}</span>
|
|
196
|
+
{it.count !== null && <span class={`count${it.attention ? ' is-attention' : ''}`}>{it.count}</span>}
|
|
197
|
+
</a>
|
|
198
|
+
));
|
|
199
|
+
return g.label ? (
|
|
200
|
+
<details class="nav-group" data-group={g.id} open>
|
|
201
|
+
<summary>
|
|
202
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={icons.chev} /></svg>
|
|
203
|
+
{g.label}
|
|
204
|
+
</summary>
|
|
205
|
+
{links}
|
|
206
|
+
</details>
|
|
207
|
+
) : <div>{links}</div>;
|
|
208
|
+
})}
|
|
209
|
+
</nav>
|
|
104
210
|
<div class="sidebar-foot">
|
|
105
|
-
|
|
211
|
+
<button class="account" type="button" id="acms-account" aria-haspopup="menu" aria-expanded="false">
|
|
212
|
+
<span class="avatar">{me.avatar ? <img src={me.avatar} alt="" /> : initials}</span>
|
|
213
|
+
<span class="who"><b>{me.name}</b><small>{me.role}</small></span>
|
|
214
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.updown} /></svg>
|
|
215
|
+
</button>
|
|
216
|
+
<div class="menu-pop" id="acms-account-menu" role="menu" hidden>
|
|
217
|
+
<div class="menu-label">Appearance</div>
|
|
218
|
+
<div class="seg block" role="group" aria-label="Theme">
|
|
219
|
+
{themeOpts.map((t) => (
|
|
220
|
+
<button type="button" data-set-theme={t.key}>
|
|
221
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={t.icon} /></svg>{t.label}
|
|
222
|
+
</button>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
<div class="menu-sep" />
|
|
226
|
+
<a class="menu-item" href="/" target="_blank" rel="noopener" role="menuitem">
|
|
227
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.globe} /></svg>View site
|
|
228
|
+
</a>
|
|
229
|
+
<a class="menu-item" href="/admin/logout" role="menuitem">
|
|
230
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.out} /></svg>Sign out
|
|
231
|
+
</a>
|
|
232
|
+
</div>
|
|
106
233
|
</div>
|
|
107
234
|
</aside>
|
|
235
|
+
|
|
108
236
|
<div class="main">
|
|
109
|
-
<
|
|
110
|
-
<div class="
|
|
111
|
-
|
|
237
|
+
<main class="panel" id="acms-panel">
|
|
238
|
+
<div class="topbar">
|
|
239
|
+
<div class="crumb">
|
|
240
|
+
{crumbGroup && <span>{crumbGroup}</span>}
|
|
241
|
+
{crumbGroup && <span class="sl">/</span>}
|
|
242
|
+
<b>{title}</b>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="topbar-actions">
|
|
245
|
+
<slot name="topbar" />
|
|
246
|
+
<a class="btn sm ghost" href="/" target="_blank" rel="noopener">View site ↗</a>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
<div class={`content ${wide ? 'content-wide' : ''}`}>
|
|
250
|
+
<slot />
|
|
251
|
+
</div>
|
|
252
|
+
</main>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<nav class="tabbar" aria-label="Sections">
|
|
257
|
+
<a href="/admin" class={tabActive === 'home' ? 'active' : ''}>
|
|
258
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.home} /></svg>Home
|
|
259
|
+
</a>
|
|
260
|
+
<a href={`/admin/c/${firstCollection}`} class={tabActive === 'content' ? 'active' : ''}>
|
|
261
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.posts} /></svg>Content
|
|
262
|
+
</a>
|
|
263
|
+
<a href="/admin/links" class={tabActive === 'links' ? 'active' : ''}>
|
|
264
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.links} /></svg>Links
|
|
265
|
+
</a>
|
|
266
|
+
<button type="button" data-cmdk>
|
|
267
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.search} /></svg>Search
|
|
268
|
+
</button>
|
|
269
|
+
<button type="button" class={tabActive === 'more' ? 'active' : ''} id="acms-more-open">
|
|
270
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d={icons.more} /></svg>More
|
|
271
|
+
</button>
|
|
272
|
+
</nav>
|
|
273
|
+
|
|
274
|
+
<dialog class="sheet" id="acms-more" aria-label="More">
|
|
275
|
+
<div class="sheet-grab" />
|
|
276
|
+
<div class="sheet-body">
|
|
277
|
+
<div class="sheet-title">More</div>
|
|
278
|
+
<div class="glist">
|
|
279
|
+
<div class="glist-row">
|
|
280
|
+
<span class="avatar" style="width:34px;height:34px;font-size:12px">{me.avatar ? <img src={me.avatar} alt="" /> : initials}</span>
|
|
281
|
+
<span class="grow"><b>{me.name}</b><br /><small class="hint">{me.role} · {siteName}</small></span>
|
|
282
|
+
</div>
|
|
112
283
|
</div>
|
|
113
|
-
<div class=
|
|
114
|
-
|
|
284
|
+
<div class="sheet-label">Appearance</div>
|
|
285
|
+
<div class="seg" role="group" aria-label="Theme" style="display:flex;margin-bottom:12px">
|
|
286
|
+
{themeOpts.map((t) => (
|
|
287
|
+
<button type="button" data-set-theme={t.key} style="flex:1;padding:8px 4px">
|
|
288
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={t.icon} /></svg>{t.label}
|
|
289
|
+
</button>
|
|
290
|
+
))}
|
|
291
|
+
</div>
|
|
292
|
+
{groups.map((g) => (
|
|
293
|
+
<div class="glist">
|
|
294
|
+
{g.items
|
|
295
|
+
.filter((it) => !['dashboard', `c:${firstCollection}`, 'links'].includes(it.key))
|
|
296
|
+
.map((it) => (
|
|
297
|
+
<a href={it.href} aria-current={section === it.key ? 'page' : undefined}>
|
|
298
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={it.icon} /></svg>
|
|
299
|
+
<span class="grow">{it.label}</span>
|
|
300
|
+
{it.count !== null && <span class="count">{it.count}</span>}
|
|
301
|
+
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={icons.chevr} /></svg>
|
|
302
|
+
</a>
|
|
303
|
+
))}
|
|
304
|
+
</div>
|
|
305
|
+
))}
|
|
306
|
+
<div class="glist">
|
|
307
|
+
<a href="/" target="_blank" rel="noopener">
|
|
308
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.globe} /></svg>
|
|
309
|
+
<span class="grow">View site</span>
|
|
310
|
+
</a>
|
|
311
|
+
<a href="/admin/logout" class="danger">
|
|
312
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.out} /></svg>
|
|
313
|
+
<span class="grow">Sign out</span>
|
|
314
|
+
</a>
|
|
115
315
|
</div>
|
|
116
316
|
</div>
|
|
117
|
-
</
|
|
317
|
+
</dialog>
|
|
318
|
+
|
|
319
|
+
<CommandPalette client:idle collections={collections.map(([key, c]) => ({ key, singular: c.singular, plural: c.plural }))} />
|
|
320
|
+
|
|
321
|
+
<script>
|
|
322
|
+
const shell = document.getElementById('acms-shell');
|
|
323
|
+
// collapsed rail, remembered per browser
|
|
324
|
+
if (document.documentElement.classList.contains('rail-pending')) shell?.classList.add('is-rail');
|
|
325
|
+
document.getElementById('acms-rail-toggle')?.addEventListener('click', () => {
|
|
326
|
+
const on = !shell?.classList.contains('is-rail');
|
|
327
|
+
shell?.classList.toggle('is-rail', on);
|
|
328
|
+
try { localStorage.setItem('acms-rail', on ? '1' : '0'); } catch {}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// nav groups remember open/closed
|
|
332
|
+
document.querySelectorAll<HTMLDetailsElement>('details.nav-group').forEach((d) => {
|
|
333
|
+
const k = `acms-nav-${d.dataset.group}`;
|
|
334
|
+
try { if (localStorage.getItem(k) === '0' && !d.querySelector('.active')) d.open = false; } catch {}
|
|
335
|
+
d.addEventListener('toggle', () => { try { localStorage.setItem(k, d.open ? '1' : '0'); } catch {} });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// account menu
|
|
339
|
+
const acct = document.getElementById('acms-account');
|
|
340
|
+
const menu = document.getElementById('acms-account-menu');
|
|
341
|
+
const close = () => { if (menu) menu.hidden = true; acct?.setAttribute('aria-expanded', 'false'); };
|
|
342
|
+
acct?.addEventListener('click', (e) => {
|
|
343
|
+
e.stopPropagation();
|
|
344
|
+
if (!menu) return;
|
|
345
|
+
menu.hidden = !menu.hidden;
|
|
346
|
+
acct.setAttribute('aria-expanded', String(!menu.hidden));
|
|
347
|
+
});
|
|
348
|
+
document.addEventListener('click', (e) => {
|
|
349
|
+
if (menu && !menu.hidden && !(e.target instanceof Node && menu.contains(e.target))) close();
|
|
350
|
+
});
|
|
351
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
|
|
352
|
+
|
|
353
|
+
// More sheet (phones)
|
|
354
|
+
const sheet = document.getElementById('acms-more') as HTMLDialogElement | null;
|
|
355
|
+
document.getElementById('acms-more-open')?.addEventListener('click', () => sheet?.showModal());
|
|
356
|
+
sheet?.addEventListener('click', (e) => { if (e.target === sheet) sheet.close(); });
|
|
357
|
+
</script>
|
|
118
358
|
</body>
|
|
119
359
|
</html>
|