@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
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
2
2
|
import { actions } from 'astro:actions';
|
|
3
3
|
import type { Settings } from '../lib/schema';
|
|
4
|
+
import type { DesignChange } from '../lib/designDiff';
|
|
4
5
|
import { ImagePicker } from './editor/ImagePicker';
|
|
6
|
+
import ThemeSectionsEditor, { SectionsReview, useDesignApi } from './ThemeSectionsEditor';
|
|
7
|
+
import DesignAccess from './DesignAccess';
|
|
8
|
+
import '../styles/theme-editor.css';
|
|
5
9
|
|
|
6
10
|
// Design screen: pick a code-first theme (src/themes/<slug>/), layer sparse
|
|
7
11
|
// token overrides on top (saved to settings.designOverrides — the theme files
|
|
@@ -18,9 +22,12 @@ interface ThemeInfo {
|
|
|
18
22
|
slug: string;
|
|
19
23
|
name: string;
|
|
20
24
|
description: string;
|
|
21
|
-
tokens: Record<string, string | number
|
|
25
|
+
tokens: Record<string, string | number> & { dark?: Record<string, string> };
|
|
22
26
|
post: Record<string, string | boolean>;
|
|
23
27
|
hasHome: boolean;
|
|
28
|
+
version: string;
|
|
29
|
+
templateCount: number;
|
|
30
|
+
sectionCount: number;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
// post-page toggles: designOverrides key + which theme default backs it
|
|
@@ -31,6 +38,20 @@ const POST_TOGGLE_ROWS: { key: string; field: string; label: string; hint: strin
|
|
|
31
38
|
{ key: 'postAuthorCard', field: 'authorCard', label: 'Author card', hint: 'Avatar + bio block under the post' },
|
|
32
39
|
{ key: 'postShare', field: 'share', label: 'Share buttons', hint: 'X, LinkedIn and copy-link' },
|
|
33
40
|
{ key: 'postRelated', field: 'related', label: 'Related reading', hint: 'Three related posts at the bottom' },
|
|
41
|
+
{ key: 'postProgress', field: 'progress', label: 'Reading progress', hint: 'Thin accent bar across the top while reading' },
|
|
42
|
+
{ key: 'postToc', field: 'toc', label: 'On this page', hint: 'Sticky section list beside long posts (wide screens)' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// shape / layout tokens (token v2): designOverrides key = token key
|
|
46
|
+
const LOOK_ROWS: { key: string; label: string; hint: string; options: [string, string][] }[] = [
|
|
47
|
+
{ key: 'scheme', label: 'Light & dark', hint: 'Auto follows the visitor’s device and adds a toggle to the header', options: [['auto', 'Auto + toggle'], ['light', 'Light only'], ['dark', 'Dark only']] },
|
|
48
|
+
{ key: 'button', label: 'Buttons', hint: 'Fill for primary buttons', options: [['ink', 'Ink (text color)'], ['accent', 'Accent color']] },
|
|
49
|
+
{ key: 'radius', label: 'Corners', hint: 'Roundness of cards, images and controls', options: [['sharp', 'Sharp'], ['soft', 'Soft'], ['round', 'Round'], ['smooth', 'Extra round']] },
|
|
50
|
+
{ key: 'elevation', label: 'Depth', hint: 'Shadows under cards and buttons', options: [['flat', 'Flat (borders only)'], ['soft', 'Soft'], ['lifted', 'Lifted']] },
|
|
51
|
+
{ key: 'density', label: 'Spacing', hint: 'Vertical rhythm of pages and sections', options: [['compact', 'Compact'], ['cozy', 'Cozy'], ['airy', 'Airy']] },
|
|
52
|
+
{ key: 'header', label: 'Header', hint: 'Style of the site header', options: [['floating', 'Floating bar'], ['bar', 'Full-width bar'], ['minimal', 'Minimal (scrolls away)'], ['centered', 'Centered logo']] },
|
|
53
|
+
{ key: 'footer', label: 'Footer', hint: 'Columns show links, socials and a Read column', options: [['columns', 'Columns'], ['simple', 'Simple row']] },
|
|
54
|
+
{ key: 'blogLayout', label: 'Post lists', hint: 'Auto uses cards when most posts have a cover', options: [['auto', 'Auto'], ['cards', 'Cards'], ['list', 'List']] },
|
|
34
55
|
];
|
|
35
56
|
const COVER_LABEL: Record<string, string> = { normal: 'Content width', full: 'Full bleed', overlay: 'Overlay (title on image)', hidden: 'Hidden' };
|
|
36
57
|
interface Pairing { key: string; label: string; headingFont: string; bodyFont: string; googleFonts: string }
|
|
@@ -44,8 +65,14 @@ interface Props {
|
|
|
44
65
|
unsplashEnabled: boolean;
|
|
45
66
|
/** public path of the newest published post, or null when nothing is published */
|
|
46
67
|
postPreviewPath: string | null;
|
|
68
|
+
/** staged draft vs live, computed server-side at load */
|
|
69
|
+
changes: DesignChange[];
|
|
47
70
|
}
|
|
48
71
|
|
|
72
|
+
interface VersionRow { id: string; createdAt: number; author: string; summary: string; theme: string }
|
|
73
|
+
const when = (ts: number) =>
|
|
74
|
+
new Date(ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
|
75
|
+
|
|
49
76
|
const COLOR_FIELDS: { key: string; label: string; hint: string }[] = [
|
|
50
77
|
{ key: 'bg', label: 'Background', hint: 'Page background' },
|
|
51
78
|
{ key: 'ink', label: 'Text', hint: 'Body and heading text' },
|
|
@@ -53,7 +80,7 @@ const COLOR_FIELDS: { key: string; label: string; hint: string }[] = [
|
|
|
53
80
|
{ key: 'muted', label: 'Muted text', hint: 'Bylines, captions, metadata' },
|
|
54
81
|
{ key: 'line', label: 'Borders', hint: 'Dividers and card borders' },
|
|
55
82
|
{ key: 'panel', label: 'Panels', hint: 'Cards and link buttons' },
|
|
56
|
-
{ key: 'soft', label: 'Soft fill', hint: '
|
|
83
|
+
{ key: 'soft', label: 'Soft fill', hint: 'Inline code, quotes, table heads, empty images' },
|
|
57
84
|
];
|
|
58
85
|
const WIDTHS = [
|
|
59
86
|
{ label: 'Narrow', px: 640 },
|
|
@@ -86,7 +113,22 @@ const DEVICES = [
|
|
|
86
113
|
] as const;
|
|
87
114
|
type DeviceKey = (typeof DEVICES)[number]['key'];
|
|
88
115
|
|
|
89
|
-
|
|
116
|
+
// control tabs; the active one lives in the URL hash so reloads (after a
|
|
117
|
+
// section save, discard or restore) land back on the same tab
|
|
118
|
+
const TABS = [
|
|
119
|
+
{ key: 'theme', label: 'Theme' },
|
|
120
|
+
{ key: 'brand', label: 'Brand' },
|
|
121
|
+
{ key: 'pages', label: 'Pages' },
|
|
122
|
+
{ key: 'review', label: 'Review' },
|
|
123
|
+
{ key: 'access', label: 'Access' },
|
|
124
|
+
] as const;
|
|
125
|
+
type TabKey = (typeof TABS)[number]['key'];
|
|
126
|
+
const tabFromHash = (): TabKey => {
|
|
127
|
+
const h = typeof location === 'undefined' ? '' : location.hash.slice(1);
|
|
128
|
+
return (TABS.find((t) => t.key === h)?.key ?? 'theme');
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default function DesignManager({ settings, pendingDesign, themes, pairings, unsplashEnabled, postPreviewPath, changes: initialChanges }: Props) {
|
|
90
132
|
const [pending, setPending] = useState(pendingDesign);
|
|
91
133
|
const [activeTheme, setActiveTheme] = useState(settings.activeTheme || 'ink');
|
|
92
134
|
const [overrides, setOverrides] = useState<Record<string, unknown>>(
|
|
@@ -96,6 +138,7 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
96
138
|
const [siteFavicon, setSiteFavicon] = useState(settings.siteFavicon);
|
|
97
139
|
const [siteCover, setSiteCover] = useState(settings.siteCover);
|
|
98
140
|
const [saveState, setSaveState] = useState<SaveState>('saved');
|
|
141
|
+
const [saveMessage, setSaveMessage] = useState('');
|
|
99
142
|
const [logoPickerOpen, setLogoPickerOpen] = useState(false);
|
|
100
143
|
const [faviconPickerOpen, setFaviconPickerOpen] = useState(false);
|
|
101
144
|
const [coverPickerOpen, setCoverPickerOpen] = useState(false);
|
|
@@ -103,6 +146,57 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
103
146
|
const [tick, setTick] = useState(0);
|
|
104
147
|
const [previewPage, setPreviewPage] = useState<'home' | 'post'>('home');
|
|
105
148
|
const [previewDevice, setPreviewDevice] = useState<DeviceKey>('mobile');
|
|
149
|
+
const [previewScheme, setPreviewScheme] = useState<'light' | 'dark'>('light');
|
|
150
|
+
// "What changes" + preview compare / share / history
|
|
151
|
+
const [changes, setChanges] = useState<DesignChange[]>(initialChanges);
|
|
152
|
+
const [compare, setCompare] = useState<'draft' | 'live'>('draft');
|
|
153
|
+
const [previewPath, setPreviewPath] = useState('/');
|
|
154
|
+
const pathRef = useRef('/');
|
|
155
|
+
const shareRef = useRef<HTMLDialogElement>(null);
|
|
156
|
+
const histRef = useRef<HTMLDialogElement>(null);
|
|
157
|
+
const [shareDays, setShareDays] = useState(7);
|
|
158
|
+
const [shareUrl, setShareUrl] = useState('');
|
|
159
|
+
const [shareMsg, setShareMsg] = useState('');
|
|
160
|
+
const [versions, setVersions] = useState<VersionRow[] | null>(null);
|
|
161
|
+
const [tab, setTabState] = useState<TabKey>(tabFromHash);
|
|
162
|
+
const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({});
|
|
163
|
+
const setTab = (key: TabKey, focus = false) => {
|
|
164
|
+
setTabState(key);
|
|
165
|
+
history.replaceState(null, '', key === 'theme' ? location.pathname + location.search : `#${key}`);
|
|
166
|
+
if (focus) tabRefs.current[key]?.focus();
|
|
167
|
+
};
|
|
168
|
+
const onTabKey = (e: ReactKeyboardEvent) => {
|
|
169
|
+
const i = TABS.findIndex((t) => t.key === tab);
|
|
170
|
+
const to = e.key === 'ArrowRight' ? i + 1 : e.key === 'ArrowLeft' ? i - 1 : e.key === 'Home' ? 0 : e.key === 'End' ? TABS.length - 1 : null;
|
|
171
|
+
if (to === null) return;
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
setTab(TABS[(to + TABS.length) % TABS.length].key, true);
|
|
174
|
+
};
|
|
175
|
+
// keys load on first visit to Access, not on every Design page load
|
|
176
|
+
const accessSeen = useRef(false);
|
|
177
|
+
if (tab === 'access') accessSeen.current = true;
|
|
178
|
+
// shared GET /api/design for page layouts (Pages + Review)
|
|
179
|
+
const designApi = useDesignApi();
|
|
180
|
+
// staged page layouts count as a draft even if the change list omits them
|
|
181
|
+
const sectionsStaged = useMemo(() => {
|
|
182
|
+
const d = designApi.data;
|
|
183
|
+
if (!d?.draft?.sectionPages) return false;
|
|
184
|
+
return JSON.stringify(d.draft.sectionPages) !== JSON.stringify(d.live.sectionPages ?? {});
|
|
185
|
+
}, [designApi.data]);
|
|
186
|
+
const sectionsStagedRef = useRef(false);
|
|
187
|
+
sectionsStagedRef.current = sectionsStaged;
|
|
188
|
+
|
|
189
|
+
// the preview iframe reports where it navigated (preview-mode links keep the
|
|
190
|
+
// draft), so reloads after a change land on the same page
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const onMsg = (e: MessageEvent) => {
|
|
193
|
+
if (e.origin !== location.origin || e.data?.type !== 'acms-preview-nav') return;
|
|
194
|
+
pathRef.current = String(e.data.path || '/');
|
|
195
|
+
setPreviewPath(pathRef.current);
|
|
196
|
+
};
|
|
197
|
+
addEventListener('message', onMsg);
|
|
198
|
+
return () => removeEventListener('message', onMsg);
|
|
199
|
+
}, []);
|
|
106
200
|
const frameRef = useRef<HTMLDivElement>(null);
|
|
107
201
|
const [frameBox, setFrameBox] = useState({ w: 0, h: 0 });
|
|
108
202
|
useEffect(() => {
|
|
@@ -119,20 +213,25 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
119
213
|
latest.current = { activeTheme, overrides, siteLogo, siteFavicon, siteCover };
|
|
120
214
|
|
|
121
215
|
const theme = themes.find((t) => t.slug === activeTheme) ?? themes[0];
|
|
216
|
+
const latestChanges = useRef(changes);
|
|
217
|
+
latestChanges.current = changes;
|
|
122
218
|
|
|
123
219
|
// saves stage into settings.draftDesign — the live site is untouched
|
|
124
220
|
const doSave = async () => {
|
|
125
221
|
const { activeTheme, overrides, siteLogo, siteFavicon, siteCover } = latest.current;
|
|
126
|
-
const { error } = await actions.settings.saveDesignDraft({
|
|
222
|
+
const { data, error } = await actions.settings.saveDesignDraft({
|
|
127
223
|
activeTheme,
|
|
128
224
|
designOverrides: overrides,
|
|
129
225
|
siteLogo,
|
|
130
226
|
siteFavicon,
|
|
131
227
|
siteCover,
|
|
132
228
|
});
|
|
133
|
-
if (error) { setSaveState('error'); return false; }
|
|
229
|
+
if (error) { setSaveMessage(error.message); setSaveState('error'); return false; }
|
|
134
230
|
setSaveState('saved');
|
|
135
|
-
|
|
231
|
+
// edits that land back on the live design leave nothing staged
|
|
232
|
+
const next = (data?.changes ?? []) as DesignChange[];
|
|
233
|
+
setChanges(next);
|
|
234
|
+
setPending(next.length > 0);
|
|
136
235
|
setTick((t) => t + 1);
|
|
137
236
|
return true;
|
|
138
237
|
};
|
|
@@ -147,17 +246,67 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
147
246
|
const publishDesign = async () => {
|
|
148
247
|
if (timer.current) { clearTimeout(timer.current); timer.current = null; }
|
|
149
248
|
if (!(await doSave())) return;
|
|
249
|
+
const n = latestChanges.current.length;
|
|
250
|
+
if (!n && !sectionsStagedRef.current) return;
|
|
251
|
+
const what = n ? `${n} ${n === 1 ? 'change' : 'changes'}` : 'the staged page layouts';
|
|
252
|
+
if (!window.confirm(`Publish ${what}? Visitors see them right away. The current design is saved to History.`)) return;
|
|
150
253
|
const { error } = await actions.settings.applyDesign({});
|
|
151
|
-
if (error) { setSaveState('error'); return; }
|
|
254
|
+
if (error) { setSaveMessage(error.message); setSaveState('error'); return; }
|
|
152
255
|
setPending(false);
|
|
256
|
+
setChanges([]);
|
|
257
|
+
setVersions(null);
|
|
153
258
|
setTick((t) => t + 1);
|
|
259
|
+
void designApi.reload();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// undo one row of the change list: the server rewrites the draft, the
|
|
263
|
+
// controls rehydrate from it (or from the live design when nothing's left)
|
|
264
|
+
const revertChange = async (key: string) => {
|
|
265
|
+
if (timer.current) { clearTimeout(timer.current); timer.current = null; }
|
|
266
|
+
setSaveState('saving');
|
|
267
|
+
const { data, error } = await actions.settings.revertDesignChange({ key });
|
|
268
|
+
if (error) { setSaveMessage(error.message); setSaveState('error'); return; }
|
|
269
|
+
if (key === 'nav') {
|
|
270
|
+
setChanges(data.changes as DesignChange[]);
|
|
271
|
+
setPending(data.changes.length > 0);
|
|
272
|
+
setSaveState('saved');
|
|
273
|
+
setTick((t) => t + 1);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
location.reload();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const openShare = () => { setShareUrl(''); setShareMsg(''); shareRef.current?.showModal(); };
|
|
280
|
+
const createShare = async () => {
|
|
281
|
+
if (timer.current) { clearTimeout(timer.current); timer.current = null; await doSave(); }
|
|
282
|
+
const { data, error } = await actions.settings.createShareLink({ days: shareDays });
|
|
283
|
+
if (error) { setShareMsg(error.message); return; }
|
|
284
|
+
setShareUrl(data.url);
|
|
285
|
+
try { await navigator.clipboard.writeText(data.url); setShareMsg('Link copied.'); } catch { setShareMsg(''); }
|
|
286
|
+
};
|
|
287
|
+
const revokeShares = async () => {
|
|
288
|
+
const { error } = await actions.settings.revokeShareLinks({});
|
|
289
|
+
setShareUrl('');
|
|
290
|
+
setShareMsg(error ? error.message : 'Every earlier share link has stopped working.');
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const openHistory = async () => {
|
|
294
|
+
histRef.current?.showModal();
|
|
295
|
+
const { data } = await actions.settings.listDesignVersions({});
|
|
296
|
+
setVersions((data?.versions ?? []) as VersionRow[]);
|
|
297
|
+
};
|
|
298
|
+
const restoreVersion = async (id: string) => {
|
|
299
|
+
if (pending && !window.confirm('Replace your current draft with this version? Your unpublished changes will be lost.')) return;
|
|
300
|
+
const { error } = await actions.settings.restoreDesignVersion({ id });
|
|
301
|
+
if (error) { window.alert(error.message); return; }
|
|
302
|
+
location.reload();
|
|
154
303
|
};
|
|
155
304
|
|
|
156
305
|
const discardDesign = async () => {
|
|
157
306
|
if (!window.confirm('Discard the staged design changes and go back to the live design? This cannot be undone.')) return;
|
|
158
307
|
if (timer.current) { clearTimeout(timer.current); timer.current = null; }
|
|
159
308
|
const { error } = await actions.settings.discardDesign({});
|
|
160
|
-
if (error) { setSaveState('error'); return; }
|
|
309
|
+
if (error) { setSaveMessage(error.message); setSaveState('error'); return; }
|
|
161
310
|
location.reload(); // rehydrate the controls from the live design
|
|
162
311
|
};
|
|
163
312
|
|
|
@@ -179,10 +328,13 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
179
328
|
|
|
180
329
|
const copyThemeJson = async () => {
|
|
181
330
|
const tokens: Record<string, string | number> = { ...theme.tokens };
|
|
182
|
-
for (const f of [...COLOR_FIELDS.map((c) => c.key), 'width']) {
|
|
331
|
+
for (const f of [...COLOR_FIELDS.map((c) => c.key), 'width', ...LOOK_ROWS.map((r) => r.key)]) {
|
|
183
332
|
const v = overrides[f];
|
|
184
333
|
if (v !== undefined && v !== null && v !== '') tokens[f] = v as string | number;
|
|
185
334
|
}
|
|
335
|
+
if (typeof overrides.darkAccent === 'string' && overrides.darkAccent) {
|
|
336
|
+
(tokens as Record<string, unknown>).dark = { ...(theme.tokens.dark ?? {}), accent: overrides.darkAccent };
|
|
337
|
+
}
|
|
186
338
|
const pairing = pairings.find((p) => p.key === overrides.fontPairing);
|
|
187
339
|
if (pairing) {
|
|
188
340
|
tokens.headingFont = pairing.headingFont;
|
|
@@ -199,6 +351,14 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
199
351
|
setTimeout(() => setCopied(false), 1800);
|
|
200
352
|
};
|
|
201
353
|
|
|
354
|
+
// rebuilt only when something that should reload the preview changes —
|
|
355
|
+
// not when the iframe merely reports a new path
|
|
356
|
+
const iframeSrc = useMemo(() => {
|
|
357
|
+
const path = pathRef.current.split('?')[0] || '/';
|
|
358
|
+
return `${path}?designPreview=${compare === 'live' ? 'live' : '1'}&design=${tick}&scheme=${previewScheme}`;
|
|
359
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
360
|
+
}, [tick, compare, previewScheme, previewPage]);
|
|
361
|
+
|
|
202
362
|
const overrideCount = Object.keys(overrides).filter((k) => overrides[k] !== undefined && overrides[k] !== '').length;
|
|
203
363
|
|
|
204
364
|
// "you changed it · Reset to theme" hint line, per the plan mock
|
|
@@ -209,56 +369,79 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
209
369
|
</>
|
|
210
370
|
);
|
|
211
371
|
|
|
372
|
+
const hasDraft = (pending && changes.length > 0) || sectionsStaged;
|
|
373
|
+
|
|
212
374
|
return (
|
|
213
375
|
<div className="dm-layout">
|
|
214
376
|
<div className="dm-controls">
|
|
215
377
|
<div className="card card-pad">
|
|
216
378
|
<div className="dm-cardhead">
|
|
217
379
|
<p className="dm-sub">
|
|
218
|
-
Active theme: <b>{theme.name}</b> · loaded from <code>src/themes/{theme.slug}/</code
|
|
219
|
-
|
|
380
|
+
Active theme: <b>{theme.name}</b> · loaded from <code>src/themes/{theme.slug}/</code>. Changes stay in a
|
|
381
|
+
private draft until you publish; visitors keep the live design.
|
|
220
382
|
</p>
|
|
221
383
|
<div className="dm-headactions">
|
|
222
|
-
{overrideCount > 0 && (
|
|
223
|
-
<button className="btn sm ghost" type="button" onClick={() => { setOverrides({}); scheduleSave(); }}>
|
|
224
|
-
Reset all
|
|
225
|
-
</button>
|
|
226
|
-
)}
|
|
227
384
|
<span className={`lm-savestate ${saveState}`}>
|
|
228
|
-
{saveState === 'saving' ? 'Saving…' : saveState === 'error' ? 'Save failed' :
|
|
385
|
+
{saveState === 'saving' ? 'Saving…' : saveState === 'error' ? (saveMessage || 'Save failed') : null}
|
|
386
|
+
</span>
|
|
387
|
+
<button className="btn sm ghost" type="button" onClick={() => void openHistory()}>History</button>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<div className="dm-status">
|
|
392
|
+
<div className="dm-status-line">
|
|
393
|
+
{hasDraft ? (
|
|
394
|
+
<><span className="dm-st is-draft">Draft</span><span className="dm-st-sub">· {changes.length > 0 ? `${changes.length} ${changes.length === 1 ? 'change' : 'changes'}` : 'page layouts'}, not live</span></>
|
|
395
|
+
) : (
|
|
396
|
+
<><span className="dm-st is-live">Live</span><span className="dm-st-sub">· no unpublished changes</span></>
|
|
397
|
+
)}
|
|
398
|
+
<span className="dm-status-actions">
|
|
399
|
+
{overrideCount > 0 && (
|
|
400
|
+
<button className="btn sm ghost" type="button" onClick={() => { setOverrides({}); scheduleSave(); }}>Reset all</button>
|
|
401
|
+
)}
|
|
402
|
+
{hasDraft && (
|
|
403
|
+
<>
|
|
404
|
+
<button className="btn sm ghost" type="button" onClick={discardDesign}>Discard</button>
|
|
405
|
+
<button className="btn sm primary" type="button" onClick={() => void publishDesign()}>Publish…</button>
|
|
406
|
+
</>
|
|
407
|
+
)}
|
|
229
408
|
</span>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</>
|
|
409
|
+
</div>
|
|
410
|
+
<div className="dm-status-links">
|
|
411
|
+
{hasDraft && tab !== 'review' && (
|
|
412
|
+
<button className="btn sm" type="button" onClick={() => setTab('review')}>Review changes</button>
|
|
235
413
|
)}
|
|
414
|
+
<a className="btn sm" href="/?designPreview=1" target="_blank" rel="noopener">
|
|
415
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z" /><circle cx="12" cy="12" r="3" /></svg>
|
|
416
|
+
Preview site
|
|
417
|
+
</a>
|
|
418
|
+
<button className="btn sm" type="button" onClick={openShare} disabled={!hasDraft} title={hasDraft ? undefined : 'Make a change first'}>
|
|
419
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="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" /></svg>
|
|
420
|
+
Share preview
|
|
421
|
+
</button>
|
|
236
422
|
</div>
|
|
237
423
|
</div>
|
|
238
424
|
|
|
239
|
-
<div className="
|
|
240
|
-
{
|
|
425
|
+
<div className="seg te-tabs" role="tablist" aria-label="Design settings" onKeyDown={onTabKey}>
|
|
426
|
+
{TABS.map((t) => (
|
|
241
427
|
<button
|
|
242
|
-
key={t.
|
|
428
|
+
key={t.key}
|
|
429
|
+
ref={(el) => { tabRefs.current[t.key] = el; }}
|
|
243
430
|
type="button"
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
431
|
+
role="tab"
|
|
432
|
+
id={`dm-tab-${t.key}`}
|
|
433
|
+
aria-selected={tab === t.key}
|
|
434
|
+
aria-controls={`dm-panel-${t.key}`}
|
|
435
|
+
tabIndex={tab === t.key ? 0 : -1}
|
|
436
|
+
onClick={() => setTab(t.key)}
|
|
247
437
|
>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
style={{ background: String(t.tokens.bg), borderColor: String(t.tokens.line) }}
|
|
251
|
-
>
|
|
252
|
-
<span style={{ color: String(t.tokens.ink), fontFamily: String(t.tokens.headingFont) }}>Aa</span>
|
|
253
|
-
<i style={{ background: String(t.tokens.accent) }} />
|
|
254
|
-
</span>
|
|
255
|
-
<span className="dm-theme-name">{t.name}</span>
|
|
256
|
-
<span className="dm-theme-desc">{t.description.split(' — ')[0]}</span>
|
|
257
|
-
{t.hasHome && <span className="dm-chip">coded homepage</span>}
|
|
438
|
+
{t.label}
|
|
439
|
+
{t.key === 'review' && pending && changes.length > 0 && <span className="te-tab-count" aria-label={`${changes.length} staged`}>{changes.length}</span>}
|
|
258
440
|
</button>
|
|
259
441
|
))}
|
|
260
442
|
</div>
|
|
261
443
|
|
|
444
|
+
<div className="te-panel" role="tabpanel" id="dm-panel-brand" aria-labelledby="dm-tab-brand" hidden={tab !== 'brand'}>
|
|
262
445
|
<div className="dm-row">
|
|
263
446
|
<div className="dm-lab">
|
|
264
447
|
<span>Logo</span>
|
|
@@ -360,6 +543,33 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
360
543
|
</div>
|
|
361
544
|
);
|
|
362
545
|
})}
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<div className="te-panel" role="tabpanel" id="dm-panel-theme" aria-labelledby="dm-tab-theme" hidden={tab !== 'theme'}>
|
|
549
|
+
<div className="dm-themes">
|
|
550
|
+
{themes.map((t) => (
|
|
551
|
+
<button
|
|
552
|
+
key={t.slug}
|
|
553
|
+
type="button"
|
|
554
|
+
className={`dm-theme ${t.slug === activeTheme ? 'active' : ''}`}
|
|
555
|
+
title={t.description}
|
|
556
|
+
aria-pressed={t.slug === activeTheme}
|
|
557
|
+
onClick={() => pickTheme(t.slug)}
|
|
558
|
+
>
|
|
559
|
+
<span
|
|
560
|
+
className="dm-swatch"
|
|
561
|
+
style={{ background: String(t.tokens.bg), borderColor: String(t.tokens.line) }}
|
|
562
|
+
>
|
|
563
|
+
<span style={{ color: String(t.tokens.ink), fontFamily: String(t.tokens.headingFont) }}>Aa</span>
|
|
564
|
+
<i style={{ background: String(t.tokens.accent) }} />
|
|
565
|
+
</span>
|
|
566
|
+
<span className="dm-theme-name">{t.name}</span>
|
|
567
|
+
<span className="dm-theme-desc">{t.description.split(' — ')[0]}</span>
|
|
568
|
+
<span className="dm-chip">v{t.version} · {t.sectionCount} sections</span>
|
|
569
|
+
<span className="dm-theme-desc">{t.templateCount} custom templates · installed</span>
|
|
570
|
+
</button>
|
|
571
|
+
))}
|
|
572
|
+
</div>
|
|
363
573
|
|
|
364
574
|
<div className="dm-row">
|
|
365
575
|
<div className="dm-lab">
|
|
@@ -367,7 +577,7 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
367
577
|
<small>
|
|
368
578
|
{overrides.width !== undefined
|
|
369
579
|
? overriddenHint(`${Number(theme.tokens.width)}px`, 'width')
|
|
370
|
-
: `
|
|
580
|
+
: `Reading column width for posts and pages; theme default is ${Number(theme.tokens.width)}px`}
|
|
371
581
|
</small>
|
|
372
582
|
</div>
|
|
373
583
|
<div className="dm-ctl">
|
|
@@ -418,6 +628,53 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
418
628
|
</div>
|
|
419
629
|
)}
|
|
420
630
|
|
|
631
|
+
<p className="card-title dm-group">Look</p>
|
|
632
|
+
<p className="dm-grouphint">
|
|
633
|
+
Shape, depth and layout. Every theme has defaults; changes here apply on top of it.
|
|
634
|
+
</p>
|
|
635
|
+
|
|
636
|
+
{LOOK_ROWS.map((r) => (
|
|
637
|
+
<div className="dm-row" key={r.key}>
|
|
638
|
+
<div className="dm-lab">
|
|
639
|
+
<span>{r.label}{overrides[r.key] !== undefined && <em className="dm-ovr">override</em>}</span>
|
|
640
|
+
<small>{r.hint}</small>
|
|
641
|
+
</div>
|
|
642
|
+
<div className="dm-ctl">
|
|
643
|
+
<select
|
|
644
|
+
className="lm-select"
|
|
645
|
+
value={String(overrides[r.key] ?? '')}
|
|
646
|
+
onChange={(e) => (e.target.value ? setOv(r.key, e.target.value) : clearOv(r.key))}
|
|
647
|
+
>
|
|
648
|
+
<option value="">Theme default ({r.options.find(([v]) => v === String(theme.tokens[r.key]))?.[1] ?? String(theme.tokens[r.key] ?? '')})</option>
|
|
649
|
+
{r.options.map(([v, label]) => <option key={v} value={v}>{label}</option>)}
|
|
650
|
+
</select>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
))}
|
|
654
|
+
|
|
655
|
+
{String(effective('scheme')) !== 'light' && (
|
|
656
|
+
<div className="dm-row">
|
|
657
|
+
<div className="dm-lab">
|
|
658
|
+
<span>Dark accent{overrides.darkAccent !== undefined && <em className="dm-ovr">override</em>}</span>
|
|
659
|
+
<small>
|
|
660
|
+
{theme.tokens.dark && Object.keys(theme.tokens.dark).length
|
|
661
|
+
? 'Accent used in dark mode, the theme ships its own dark palette'
|
|
662
|
+
: 'This theme generates its dark palette; check it in the preview'}
|
|
663
|
+
</small>
|
|
664
|
+
</div>
|
|
665
|
+
<div className="dm-ctl">
|
|
666
|
+
<input
|
|
667
|
+
type="color"
|
|
668
|
+
value={String(overrides.darkAccent ?? theme.tokens.dark?.accent ?? '#7d93ff')}
|
|
669
|
+
onChange={(e) => setOv('darkAccent', e.target.value)}
|
|
670
|
+
/>
|
|
671
|
+
{overrides.darkAccent !== undefined && (
|
|
672
|
+
<button className="btn sm ghost" type="button" onClick={() => clearOv('darkAccent')}>Reset</button>
|
|
673
|
+
)}
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
)}
|
|
677
|
+
|
|
421
678
|
<p className="card-title dm-group">Posts</p>
|
|
422
679
|
<p className="dm-grouphint">
|
|
423
680
|
How individual posts render. The theme sets the defaults; anything you change here overrides it site-wide.
|
|
@@ -474,6 +731,49 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
474
731
|
</button>
|
|
475
732
|
</div>
|
|
476
733
|
</div>
|
|
734
|
+
</div>
|
|
735
|
+
|
|
736
|
+
<div className="te-panel" role="tabpanel" id="dm-panel-pages" aria-labelledby="dm-tab-pages" hidden={tab !== 'pages'}>
|
|
737
|
+
<ThemeSectionsEditor api={designApi} activeTheme={activeTheme} unsplashEnabled={unsplashEnabled} />
|
|
738
|
+
</div>
|
|
739
|
+
|
|
740
|
+
<div className="te-panel" role="tabpanel" id="dm-panel-review" aria-labelledby="dm-tab-review" hidden={tab !== 'review'}>
|
|
741
|
+
<p className="card-title dm-group te-first">Staged changes</p>
|
|
742
|
+
{pending && changes.length > 0 ? (
|
|
743
|
+
<ul className="dm-changes">
|
|
744
|
+
{changes.map((c) => (
|
|
745
|
+
<li key={c.key}>
|
|
746
|
+
<span className="dm-ch-label">{c.label}</span>
|
|
747
|
+
<span className="dm-ch-val">
|
|
748
|
+
{c.kind === 'color' ? (
|
|
749
|
+
<><i className="dm-ch-sw" style={{ background: c.from || 'transparent' }} title={c.from || 'Theme default'} />→<i className="dm-ch-sw" style={{ background: c.to || 'transparent' }} title={c.to || 'Theme default'} /></>
|
|
750
|
+
) : c.kind === 'image' ? (
|
|
751
|
+
<>{c.from ? 'Changed' : 'Added'}{!c.to && ' (removed)'}</>
|
|
752
|
+
) : c.kind === 'nav' ? (
|
|
753
|
+
<>{c.to}</>
|
|
754
|
+
) : (
|
|
755
|
+
<>{c.from} → <b>{c.to}</b></>
|
|
756
|
+
)}
|
|
757
|
+
</span>
|
|
758
|
+
<button type="button" className="dm-ch-undo" onClick={() => void revertChange(c.key)} aria-label={`Undo ${c.label}`}>Undo</button>
|
|
759
|
+
</li>
|
|
760
|
+
))}
|
|
761
|
+
</ul>
|
|
762
|
+
) : (
|
|
763
|
+
<p className="te-muted">
|
|
764
|
+
{hasDraft ? 'No theme or brand changes staged.' : 'Nothing staged. The live design is what visitors see.'}
|
|
765
|
+
</p>
|
|
766
|
+
)}
|
|
767
|
+
<p className="card-title dm-group">Page layouts</p>
|
|
768
|
+
<SectionsReview api={designApi} />
|
|
769
|
+
<p className="dm-grouphint te-reviewhint">
|
|
770
|
+
Publish from the bar above to make every staged change live at once, or share a preview link first.
|
|
771
|
+
</p>
|
|
772
|
+
</div>
|
|
773
|
+
|
|
774
|
+
<div className="te-panel" role="tabpanel" id="dm-panel-access" aria-labelledby="dm-tab-access" hidden={tab !== 'access'}>
|
|
775
|
+
{accessSeen.current && <DesignAccess />}
|
|
776
|
+
</div>
|
|
477
777
|
</div>
|
|
478
778
|
</div>
|
|
479
779
|
|
|
@@ -495,19 +795,29 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
495
795
|
</button>
|
|
496
796
|
))}
|
|
497
797
|
</div>
|
|
798
|
+
<div className="dm-ptabs" title="Compare with the live design (D in the full preview)">
|
|
799
|
+
<button type="button" className={compare === 'live' ? 'active' : ''} onClick={() => setCompare('live')}>Live</button>
|
|
800
|
+
<button type="button" className={compare === 'draft' ? 'active' : ''} onClick={() => setCompare('draft')}>Draft</button>
|
|
801
|
+
</div>
|
|
802
|
+
{String(effective('scheme')) === 'auto' && (
|
|
803
|
+
<div className="dm-ptabs">
|
|
804
|
+
<button type="button" className={previewScheme === 'light' ? 'active' : ''} onClick={() => setPreviewScheme('light')}>Light</button>
|
|
805
|
+
<button type="button" className={previewScheme === 'dark' ? 'active' : ''} onClick={() => setPreviewScheme('dark')}>Dark</button>
|
|
806
|
+
</div>
|
|
807
|
+
)}
|
|
498
808
|
{postPreviewPath && (
|
|
499
809
|
<div className="dm-ptabs">
|
|
500
810
|
<button
|
|
501
811
|
type="button"
|
|
502
812
|
className={previewPage === 'home' ? 'active' : ''}
|
|
503
|
-
onClick={() => setPreviewPage('home')}
|
|
813
|
+
onClick={() => { pathRef.current = '/'; setPreviewPath('/'); setPreviewPage('home'); }}
|
|
504
814
|
>
|
|
505
815
|
Home
|
|
506
816
|
</button>
|
|
507
817
|
<button
|
|
508
818
|
type="button"
|
|
509
819
|
className={previewPage === 'post' ? 'active' : ''}
|
|
510
|
-
onClick={() => setPreviewPage(
|
|
820
|
+
onClick={() => { pathRef.current = postPreviewPath ?? "/"; setPreviewPath(postPreviewPath ?? "/"); setPreviewPage("post"); }}
|
|
511
821
|
>
|
|
512
822
|
Post
|
|
513
823
|
</button>
|
|
@@ -515,11 +825,12 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
515
825
|
)}
|
|
516
826
|
<a
|
|
517
827
|
className="btn sm ghost"
|
|
518
|
-
href={
|
|
828
|
+
href={`${previewPath.split('?')[0]}?designPreview=1`}
|
|
519
829
|
target="_blank"
|
|
520
830
|
rel="noopener"
|
|
831
|
+
title={`Open ${previewPath} in preview mode`}
|
|
521
832
|
>
|
|
522
|
-
Open ↗
|
|
833
|
+
Open draft ↗
|
|
523
834
|
</a>
|
|
524
835
|
</div>
|
|
525
836
|
</div>
|
|
@@ -529,8 +840,8 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
529
840
|
return (
|
|
530
841
|
<div className="dm-frame" ref={frameRef}>
|
|
531
842
|
<iframe
|
|
532
|
-
key={
|
|
533
|
-
src={
|
|
843
|
+
key={iframeSrc}
|
|
844
|
+
src={iframeSrc}
|
|
534
845
|
title="Site preview"
|
|
535
846
|
style={{
|
|
536
847
|
width: device.width,
|
|
@@ -543,6 +854,67 @@ export default function DesignManager({ settings, pendingDesign, themes, pairing
|
|
|
543
854
|
})()}
|
|
544
855
|
</div>
|
|
545
856
|
|
|
857
|
+
<dialog className="modal dm-dialog" ref={shareRef} onClick={(e) => { if (e.target === e.currentTarget) shareRef.current?.close(); }}>
|
|
858
|
+
<div className="modal-head">
|
|
859
|
+
<span>Share a preview</span>
|
|
860
|
+
<button className="modal-x" type="button" onClick={() => shareRef.current?.close()} aria-label="Close">✕</button>
|
|
861
|
+
</div>
|
|
862
|
+
<div className="modal-body">
|
|
863
|
+
<p className="dm-dialog-sub">Anyone with the link can browse the site with your draft design. They can't edit or publish, and the link stops working when you publish or discard the draft.</p>
|
|
864
|
+
{shareUrl ? (
|
|
865
|
+
<div className="dm-share-url">
|
|
866
|
+
<input readOnly value={shareUrl} onFocus={(e) => e.currentTarget.select()} aria-label="Preview link" />
|
|
867
|
+
<button className="btn sm primary" type="button" onClick={() => void navigator.clipboard.writeText(shareUrl).then(() => setShareMsg('Link copied.'))}>Copy</button>
|
|
868
|
+
</div>
|
|
869
|
+
) : (
|
|
870
|
+
<div className="dm-share-row">
|
|
871
|
+
<label>Expires
|
|
872
|
+
<select className="lm-select" value={shareDays} onChange={(e) => setShareDays(Number(e.target.value))}>
|
|
873
|
+
<option value={1}>In 24 hours</option>
|
|
874
|
+
<option value={7}>In 7 days</option>
|
|
875
|
+
<option value={30}>In 30 days</option>
|
|
876
|
+
</select>
|
|
877
|
+
</label>
|
|
878
|
+
<button className="btn sm primary" type="button" onClick={() => void createShare()}>Create link</button>
|
|
879
|
+
</div>
|
|
880
|
+
)}
|
|
881
|
+
{shareMsg && <p className="dm-dialog-msg">{shareMsg}</p>}
|
|
882
|
+
<div className="dm-share-foot">
|
|
883
|
+
<button className="btn sm ghost dm-danger" type="button" onClick={() => void revokeShares()}>Revoke all links</button>
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
</dialog>
|
|
887
|
+
|
|
888
|
+
<dialog className="modal dm-dialog" ref={histRef} onClick={(e) => { if (e.target === e.currentTarget) histRef.current?.close(); }}>
|
|
889
|
+
<div className="modal-head">
|
|
890
|
+
<span>Design history</span>
|
|
891
|
+
<button className="modal-x" type="button" onClick={() => histRef.current?.close()} aria-label="Close">✕</button>
|
|
892
|
+
</div>
|
|
893
|
+
<div className="modal-body">
|
|
894
|
+
<p className="dm-dialog-sub">Every publish saves the design it replaced. Restoring loads a version as your draft, so you can preview it before publishing.</p>
|
|
895
|
+
{versions === null ? (
|
|
896
|
+
<p className="dm-dialog-msg">Loading…</p>
|
|
897
|
+
) : versions.length === 0 ? (
|
|
898
|
+
<p className="dm-dialog-msg">No earlier versions yet. The first publish creates one.</p>
|
|
899
|
+
) : (
|
|
900
|
+
<ul className="dm-hist">
|
|
901
|
+
{versions.map((v) => (
|
|
902
|
+
<li key={v.id}>
|
|
903
|
+
<div>
|
|
904
|
+
<b>{v.theme}{v.summary ? ` · replaced by: ${v.summary}` : ''}</b>
|
|
905
|
+
<small>Saved {when(v.createdAt)}{v.author ? ` by ${v.author}` : ''}</small>
|
|
906
|
+
</div>
|
|
907
|
+
<span className="dm-hist-actions">
|
|
908
|
+
<a className="btn sm ghost" href={`/?designVersion=${v.id}`} target="_blank" rel="noopener">Preview</a>
|
|
909
|
+
<button className="btn sm" type="button" onClick={() => void restoreVersion(v.id)}>Restore as draft</button>
|
|
910
|
+
</span>
|
|
911
|
+
</li>
|
|
912
|
+
))}
|
|
913
|
+
</ul>
|
|
914
|
+
)}
|
|
915
|
+
</div>
|
|
916
|
+
</dialog>
|
|
917
|
+
|
|
546
918
|
<ImagePicker
|
|
547
919
|
open={logoPickerOpen}
|
|
548
920
|
unsplashEnabled={unsplashEnabled}
|