@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,12 +1,20 @@
|
|
|
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
|
|
8
12
|
// are never written to), set the site logo, and export the effective design
|
|
9
13
|
// as a committable theme.json.
|
|
14
|
+
//
|
|
15
|
+
// Every change stages into settings.draftDesign — the preview iframe renders
|
|
16
|
+
// the staged design (?designPreview=1, admin-only) while visitors keep the
|
|
17
|
+
// live one until "Publish design" applies it.
|
|
10
18
|
|
|
11
19
|
type SaveState = 'saved' | 'saving' | 'error';
|
|
12
20
|
|
|
@@ -14,9 +22,12 @@ interface ThemeInfo {
|
|
|
14
22
|
slug: string;
|
|
15
23
|
name: string;
|
|
16
24
|
description: string;
|
|
17
|
-
tokens: Record<string, string | number
|
|
25
|
+
tokens: Record<string, string | number> & { dark?: Record<string, string> };
|
|
18
26
|
post: Record<string, string | boolean>;
|
|
19
27
|
hasHome: boolean;
|
|
28
|
+
version: string;
|
|
29
|
+
templateCount: number;
|
|
30
|
+
sectionCount: number;
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
// post-page toggles: designOverrides key + which theme default backs it
|
|
@@ -27,19 +38,41 @@ const POST_TOGGLE_ROWS: { key: string; field: string; label: string; hint: strin
|
|
|
27
38
|
{ key: 'postAuthorCard', field: 'authorCard', label: 'Author card', hint: 'Avatar + bio block under the post' },
|
|
28
39
|
{ key: 'postShare', field: 'share', label: 'Share buttons', hint: 'X, LinkedIn and copy-link' },
|
|
29
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']] },
|
|
30
55
|
];
|
|
31
56
|
const COVER_LABEL: Record<string, string> = { normal: 'Content width', full: 'Full bleed', overlay: 'Overlay (title on image)', hidden: 'Hidden' };
|
|
32
57
|
interface Pairing { key: string; label: string; headingFont: string; bodyFont: string; googleFonts: string }
|
|
33
58
|
|
|
34
59
|
interface Props {
|
|
35
60
|
settings: Settings;
|
|
61
|
+
// true when settings.draftDesign held staged changes at load time
|
|
62
|
+
pendingDesign: boolean;
|
|
36
63
|
themes: ThemeInfo[];
|
|
37
64
|
pairings: Pairing[];
|
|
38
65
|
unsplashEnabled: boolean;
|
|
39
66
|
/** public path of the newest published post, or null when nothing is published */
|
|
40
67
|
postPreviewPath: string | null;
|
|
68
|
+
/** staged draft vs live, computed server-side at load */
|
|
69
|
+
changes: DesignChange[];
|
|
41
70
|
}
|
|
42
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
|
+
|
|
43
76
|
const COLOR_FIELDS: { key: string; label: string; hint: string }[] = [
|
|
44
77
|
{ key: 'bg', label: 'Background', hint: 'Page background' },
|
|
45
78
|
{ key: 'ink', label: 'Text', hint: 'Body and heading text' },
|
|
@@ -47,7 +80,7 @@ const COLOR_FIELDS: { key: string; label: string; hint: string }[] = [
|
|
|
47
80
|
{ key: 'muted', label: 'Muted text', hint: 'Bylines, captions, metadata' },
|
|
48
81
|
{ key: 'line', label: 'Borders', hint: 'Dividers and card borders' },
|
|
49
82
|
{ key: 'panel', label: 'Panels', hint: 'Cards and link buttons' },
|
|
50
|
-
{ key: 'soft', label: 'Soft fill', hint: '
|
|
83
|
+
{ key: 'soft', label: 'Soft fill', hint: 'Inline code, quotes, table heads, empty images' },
|
|
51
84
|
];
|
|
52
85
|
const WIDTHS = [
|
|
53
86
|
{ label: 'Narrow', px: 640 },
|
|
@@ -80,7 +113,23 @@ const DEVICES = [
|
|
|
80
113
|
] as const;
|
|
81
114
|
type DeviceKey = (typeof DEVICES)[number]['key'];
|
|
82
115
|
|
|
83
|
-
|
|
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) {
|
|
132
|
+
const [pending, setPending] = useState(pendingDesign);
|
|
84
133
|
const [activeTheme, setActiveTheme] = useState(settings.activeTheme || 'ink');
|
|
85
134
|
const [overrides, setOverrides] = useState<Record<string, unknown>>(
|
|
86
135
|
(settings.designOverrides as Record<string, unknown>) ?? {}
|
|
@@ -89,6 +138,7 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
89
138
|
const [siteFavicon, setSiteFavicon] = useState(settings.siteFavicon);
|
|
90
139
|
const [siteCover, setSiteCover] = useState(settings.siteCover);
|
|
91
140
|
const [saveState, setSaveState] = useState<SaveState>('saved');
|
|
141
|
+
const [saveMessage, setSaveMessage] = useState('');
|
|
92
142
|
const [logoPickerOpen, setLogoPickerOpen] = useState(false);
|
|
93
143
|
const [faviconPickerOpen, setFaviconPickerOpen] = useState(false);
|
|
94
144
|
const [coverPickerOpen, setCoverPickerOpen] = useState(false);
|
|
@@ -96,6 +146,57 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
96
146
|
const [tick, setTick] = useState(0);
|
|
97
147
|
const [previewPage, setPreviewPage] = useState<'home' | 'post'>('home');
|
|
98
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
|
+
}, []);
|
|
99
200
|
const frameRef = useRef<HTMLDivElement>(null);
|
|
100
201
|
const [frameBox, setFrameBox] = useState({ w: 0, h: 0 });
|
|
101
202
|
useEffect(() => {
|
|
@@ -112,23 +213,101 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
112
213
|
latest.current = { activeTheme, overrides, siteLogo, siteFavicon, siteCover };
|
|
113
214
|
|
|
114
215
|
const theme = themes.find((t) => t.slug === activeTheme) ?? themes[0];
|
|
216
|
+
const latestChanges = useRef(changes);
|
|
217
|
+
latestChanges.current = changes;
|
|
218
|
+
|
|
219
|
+
// saves stage into settings.draftDesign — the live site is untouched
|
|
220
|
+
const doSave = async () => {
|
|
221
|
+
const { activeTheme, overrides, siteLogo, siteFavicon, siteCover } = latest.current;
|
|
222
|
+
const { data, error } = await actions.settings.saveDesignDraft({
|
|
223
|
+
activeTheme,
|
|
224
|
+
designOverrides: overrides,
|
|
225
|
+
siteLogo,
|
|
226
|
+
siteFavicon,
|
|
227
|
+
siteCover,
|
|
228
|
+
});
|
|
229
|
+
if (error) { setSaveMessage(error.message); setSaveState('error'); return false; }
|
|
230
|
+
setSaveState('saved');
|
|
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);
|
|
235
|
+
setTick((t) => t + 1);
|
|
236
|
+
return true;
|
|
237
|
+
};
|
|
115
238
|
|
|
116
239
|
const scheduleSave = () => {
|
|
117
240
|
setSaveState('saving');
|
|
118
241
|
if (timer.current) clearTimeout(timer.current);
|
|
119
|
-
timer.current = setTimeout(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
242
|
+
timer.current = setTimeout(doSave, 500);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Publish design: flush any queued save into the staged blob, then apply it
|
|
246
|
+
const publishDesign = async () => {
|
|
247
|
+
if (timer.current) { clearTimeout(timer.current); timer.current = null; }
|
|
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;
|
|
253
|
+
const { error } = await actions.settings.applyDesign({});
|
|
254
|
+
if (error) { setSaveMessage(error.message); setSaveState('error'); return; }
|
|
255
|
+
setPending(false);
|
|
256
|
+
setChanges([]);
|
|
257
|
+
setVersions(null);
|
|
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);
|
|
129
272
|
setSaveState('saved');
|
|
130
273
|
setTick((t) => t + 1);
|
|
131
|
-
|
|
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();
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const discardDesign = async () => {
|
|
306
|
+
if (!window.confirm('Discard the staged design changes and go back to the live design? This cannot be undone.')) return;
|
|
307
|
+
if (timer.current) { clearTimeout(timer.current); timer.current = null; }
|
|
308
|
+
const { error } = await actions.settings.discardDesign({});
|
|
309
|
+
if (error) { setSaveMessage(error.message); setSaveState('error'); return; }
|
|
310
|
+
location.reload(); // rehydrate the controls from the live design
|
|
132
311
|
};
|
|
133
312
|
|
|
134
313
|
const setOv = (key: string, value: unknown) => {
|
|
@@ -149,10 +328,13 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
149
328
|
|
|
150
329
|
const copyThemeJson = async () => {
|
|
151
330
|
const tokens: Record<string, string | number> = { ...theme.tokens };
|
|
152
|
-
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)]) {
|
|
153
332
|
const v = overrides[f];
|
|
154
333
|
if (v !== undefined && v !== null && v !== '') tokens[f] = v as string | number;
|
|
155
334
|
}
|
|
335
|
+
if (typeof overrides.darkAccent === 'string' && overrides.darkAccent) {
|
|
336
|
+
(tokens as Record<string, unknown>).dark = { ...(theme.tokens.dark ?? {}), accent: overrides.darkAccent };
|
|
337
|
+
}
|
|
156
338
|
const pairing = pairings.find((p) => p.key === overrides.fontPairing);
|
|
157
339
|
if (pairing) {
|
|
158
340
|
tokens.headingFont = pairing.headingFont;
|
|
@@ -169,6 +351,14 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
169
351
|
setTimeout(() => setCopied(false), 1800);
|
|
170
352
|
};
|
|
171
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
|
+
|
|
172
362
|
const overrideCount = Object.keys(overrides).filter((k) => overrides[k] !== undefined && overrides[k] !== '').length;
|
|
173
363
|
|
|
174
364
|
// "you changed it · Reset to theme" hint line, per the plan mock
|
|
@@ -179,50 +369,79 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
179
369
|
</>
|
|
180
370
|
);
|
|
181
371
|
|
|
372
|
+
const hasDraft = (pending && changes.length > 0) || sectionsStaged;
|
|
373
|
+
|
|
182
374
|
return (
|
|
183
375
|
<div className="dm-layout">
|
|
184
376
|
<div className="dm-controls">
|
|
185
377
|
<div className="card card-pad">
|
|
186
378
|
<div className="dm-cardhead">
|
|
187
379
|
<p className="dm-sub">
|
|
188
|
-
Active theme: <b>{theme.name}</b> · loaded from <code>src/themes/{theme.slug}/</code
|
|
189
|
-
|
|
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.
|
|
190
382
|
</p>
|
|
191
383
|
<div className="dm-headactions">
|
|
192
|
-
{overrideCount > 0 && (
|
|
193
|
-
<button className="btn sm ghost" type="button" onClick={() => { setOverrides({}); scheduleSave(); }}>
|
|
194
|
-
Reset all
|
|
195
|
-
</button>
|
|
196
|
-
)}
|
|
197
384
|
<span className={`lm-savestate ${saveState}`}>
|
|
198
|
-
{saveState === 'saving' ? 'Saving…' : saveState === 'error' ? 'Save failed' :
|
|
385
|
+
{saveState === 'saving' ? 'Saving…' : saveState === 'error' ? (saveMessage || 'Save failed') : null}
|
|
199
386
|
</span>
|
|
387
|
+
<button className="btn sm ghost" type="button" onClick={() => void openHistory()}>History</button>
|
|
200
388
|
</div>
|
|
201
389
|
</div>
|
|
202
390
|
|
|
203
|
-
<div className="dm-
|
|
204
|
-
|
|
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
|
+
)}
|
|
408
|
+
</span>
|
|
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>
|
|
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>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<div className="seg te-tabs" role="tablist" aria-label="Design settings" onKeyDown={onTabKey}>
|
|
426
|
+
{TABS.map((t) => (
|
|
205
427
|
<button
|
|
206
|
-
key={t.
|
|
428
|
+
key={t.key}
|
|
429
|
+
ref={(el) => { tabRefs.current[t.key] = el; }}
|
|
207
430
|
type="button"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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)}
|
|
211
437
|
>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
style={{ background: String(t.tokens.bg), borderColor: String(t.tokens.line) }}
|
|
215
|
-
>
|
|
216
|
-
<span style={{ color: String(t.tokens.ink), fontFamily: String(t.tokens.headingFont) }}>Aa</span>
|
|
217
|
-
<i style={{ background: String(t.tokens.accent) }} />
|
|
218
|
-
</span>
|
|
219
|
-
<span className="dm-theme-name">{t.name}</span>
|
|
220
|
-
<span className="dm-theme-desc">{t.description.split(' — ')[0]}</span>
|
|
221
|
-
{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>}
|
|
222
440
|
</button>
|
|
223
441
|
))}
|
|
224
442
|
</div>
|
|
225
443
|
|
|
444
|
+
<div className="te-panel" role="tabpanel" id="dm-panel-brand" aria-labelledby="dm-tab-brand" hidden={tab !== 'brand'}>
|
|
226
445
|
<div className="dm-row">
|
|
227
446
|
<div className="dm-lab">
|
|
228
447
|
<span>Logo</span>
|
|
@@ -324,6 +543,33 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
324
543
|
</div>
|
|
325
544
|
);
|
|
326
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>
|
|
327
573
|
|
|
328
574
|
<div className="dm-row">
|
|
329
575
|
<div className="dm-lab">
|
|
@@ -331,7 +577,7 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
331
577
|
<small>
|
|
332
578
|
{overrides.width !== undefined
|
|
333
579
|
? overriddenHint(`${Number(theme.tokens.width)}px`, 'width')
|
|
334
|
-
: `
|
|
580
|
+
: `Reading column width for posts and pages; theme default is ${Number(theme.tokens.width)}px`}
|
|
335
581
|
</small>
|
|
336
582
|
</div>
|
|
337
583
|
<div className="dm-ctl">
|
|
@@ -382,6 +628,53 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
382
628
|
</div>
|
|
383
629
|
)}
|
|
384
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
|
+
|
|
385
678
|
<p className="card-title dm-group">Posts</p>
|
|
386
679
|
<p className="dm-grouphint">
|
|
387
680
|
How individual posts render. The theme sets the defaults; anything you change here overrides it site-wide.
|
|
@@ -438,6 +731,49 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
438
731
|
</button>
|
|
439
732
|
</div>
|
|
440
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>
|
|
441
777
|
</div>
|
|
442
778
|
</div>
|
|
443
779
|
|
|
@@ -459,19 +795,29 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
459
795
|
</button>
|
|
460
796
|
))}
|
|
461
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
|
+
)}
|
|
462
808
|
{postPreviewPath && (
|
|
463
809
|
<div className="dm-ptabs">
|
|
464
810
|
<button
|
|
465
811
|
type="button"
|
|
466
812
|
className={previewPage === 'home' ? 'active' : ''}
|
|
467
|
-
onClick={() => setPreviewPage('home')}
|
|
813
|
+
onClick={() => { pathRef.current = '/'; setPreviewPath('/'); setPreviewPage('home'); }}
|
|
468
814
|
>
|
|
469
815
|
Home
|
|
470
816
|
</button>
|
|
471
817
|
<button
|
|
472
818
|
type="button"
|
|
473
819
|
className={previewPage === 'post' ? 'active' : ''}
|
|
474
|
-
onClick={() => setPreviewPage(
|
|
820
|
+
onClick={() => { pathRef.current = postPreviewPath ?? "/"; setPreviewPath(postPreviewPath ?? "/"); setPreviewPage("post"); }}
|
|
475
821
|
>
|
|
476
822
|
Post
|
|
477
823
|
</button>
|
|
@@ -479,11 +825,12 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
479
825
|
)}
|
|
480
826
|
<a
|
|
481
827
|
className="btn sm ghost"
|
|
482
|
-
href={
|
|
828
|
+
href={`${previewPath.split('?')[0]}?designPreview=1`}
|
|
483
829
|
target="_blank"
|
|
484
830
|
rel="noopener"
|
|
831
|
+
title={`Open ${previewPath} in preview mode`}
|
|
485
832
|
>
|
|
486
|
-
Open ↗
|
|
833
|
+
Open draft ↗
|
|
487
834
|
</a>
|
|
488
835
|
</div>
|
|
489
836
|
</div>
|
|
@@ -493,8 +840,8 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
493
840
|
return (
|
|
494
841
|
<div className="dm-frame" ref={frameRef}>
|
|
495
842
|
<iframe
|
|
496
|
-
key={
|
|
497
|
-
src={
|
|
843
|
+
key={iframeSrc}
|
|
844
|
+
src={iframeSrc}
|
|
498
845
|
title="Site preview"
|
|
499
846
|
style={{
|
|
500
847
|
width: device.width,
|
|
@@ -507,6 +854,67 @@ export default function DesignManager({ settings, themes, pairings, unsplashEnab
|
|
|
507
854
|
})()}
|
|
508
855
|
</div>
|
|
509
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
|
+
|
|
510
918
|
<ImagePicker
|
|
511
919
|
open={logoPickerOpen}
|
|
512
920
|
unsplashEnabled={unsplashEnabled}
|