caracms 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +57 -2
- package/bin/caracms.mjs +174 -0
- package/bin/gate.mjs +275 -0
- package/bin/init.mjs +712 -0
- package/bin/media.mjs +662 -0
- package/bin/site-runner.mjs +61 -0
- package/bin/templates/Page.astro.txt +31 -0
- package/bin/templates/TextBlock.astro.txt +13 -0
- package/bin/templates/caracms.config.ts.txt +22 -0
- package/bin/templates/catch-all.astro.txt +25 -0
- package/bin/templates/health.ts.txt +6 -0
- package/bin/templates/railway.ts.txt +147 -0
- package/package.json +66 -4
- package/skills/caracms-adopt/SKILL.md +508 -0
- package/skills/caracms-adopt/look-same.mjs +957 -0
- package/skills/caracms-blocks/SKILL.md +376 -0
- package/skills/caracms-content/SKILL.md +201 -0
- package/skills/caracms-tina/SKILL.md +427 -0
- package/skills/caracms-tina/field-kinds.md +88 -0
- package/skills/caracms-tina/tina-to-caracms.mjs +1011 -0
- package/src/block-runs.ts +84 -0
- package/src/cards.ts +158 -0
- package/src/cli/schemas.ts +122 -0
- package/src/cli/site.ts +66 -0
- package/src/cli/validate.ts +471 -0
- package/src/code-routes-disk.ts +67 -0
- package/src/code-routes.ts +134 -0
- package/src/columns.ts +39 -0
- package/src/components/Block.astro +46 -0
- package/src/components/BlockRunTree.astro +45 -0
- package/src/components/Blocks.astro +139 -0
- package/src/components/Buttons.astro +116 -0
- package/src/components/Fonts.astro +14 -0
- package/src/components/Form.astro +221 -0
- package/src/components/Head.astro +62 -0
- package/src/components/Link.astro +68 -0
- package/src/components/Lockup.astro +116 -0
- package/src/components/Media.astro +124 -0
- package/src/components/MediaPlayer.astro +102 -0
- package/src/components/RenderDocument.astro +64 -0
- package/src/components/RichText.astro +59 -0
- package/src/components/Text.astro +81 -0
- package/src/components/attributes.ts +33 -0
- package/src/components/descriptor.ts +45 -0
- package/src/components/form-client.ts +182 -0
- package/src/components/media-player.ts +46 -0
- package/src/components/media-props.ts +35 -0
- package/src/components/sitemap.xml.ts +36 -0
- package/src/config.ts +825 -0
- package/src/content-schema.ts +166 -0
- package/src/content.ts +422 -0
- package/src/document.ts +579 -0
- package/src/editor/README.md +405 -0
- package/src/editor/addresses.ts +61 -0
- package/src/editor/brand.ts +118 -0
- package/src/editor/client/block-list/BlockList.tsx +342 -0
- package/src/editor/client/block-list/Picker.tsx +105 -0
- package/src/editor/client/block-list/UnknownBlock.tsx +119 -0
- package/src/editor/client/block-list/insert-points.ts +56 -0
- package/src/editor/client/block-list/link.ts +71 -0
- package/src/editor/client/block-list/reorder.ts +105 -0
- package/src/editor/client/block-list/summary.ts +118 -0
- package/src/editor/client/collections/CollectionGrid.tsx +143 -0
- package/src/editor/client/collections/CollectionTable.tsx +406 -0
- package/src/editor/client/collections/table.ts +270 -0
- package/src/editor/client/context.tsx +27 -0
- package/src/editor/client/documents/DocMenu.tsx +154 -0
- package/src/editor/client/documents/actions.ts +192 -0
- package/src/editor/client/documents/dialogs.tsx +687 -0
- package/src/editor/client/editor.ts +995 -0
- package/src/editor/client/fields/ButtonsField.tsx +87 -0
- package/src/editor/client/fields/DateField.tsx +200 -0
- package/src/editor/client/fields/FieldControl.tsx +61 -0
- package/src/editor/client/fields/FieldFrame.tsx +109 -0
- package/src/editor/client/fields/GroupField.tsx +113 -0
- package/src/editor/client/fields/HiddenToggle.tsx +51 -0
- package/src/editor/client/fields/LinkField.tsx +307 -0
- package/src/editor/client/fields/ListField.tsx +642 -0
- package/src/editor/client/fields/LockupField.tsx +365 -0
- package/src/editor/client/fields/MediaField.tsx +823 -0
- package/src/editor/client/fields/PageFields.tsx +82 -0
- package/src/editor/client/fields/ReferenceField.tsx +209 -0
- package/src/editor/client/fields/RichTextField.tsx +306 -0
- package/src/editor/client/fields/SelectField.tsx +63 -0
- package/src/editor/client/fields/SeoGroup.tsx +282 -0
- package/src/editor/client/fields/SimpleFields.tsx +89 -0
- package/src/editor/client/fields/TextField.tsx +51 -0
- package/src/editor/client/fields/date.ts +73 -0
- package/src/editor/client/fields/host.tsx +172 -0
- package/src/editor/client/fields/list-summary.ts +85 -0
- package/src/editor/client/fields/lockup-rules.ts +181 -0
- package/src/editor/client/fields/parts.tsx +458 -0
- package/src/editor/client/fields/rich-text-commands.ts +183 -0
- package/src/editor/client/fields/rich-text-doc.ts +156 -0
- package/src/editor/client/fields/rich-text-schema.ts +224 -0
- package/src/editor/client/fields/stale.ts +58 -0
- package/src/editor/client/inbox/InboxStage.tsx +272 -0
- package/src/editor/client/inbox/store.ts +311 -0
- package/src/editor/client/live/format.ts +77 -0
- package/src/editor/client/live/marks.tsx +90 -0
- package/src/editor/client/live/notices.tsx +213 -0
- package/src/editor/client/live/publish.tsx +617 -0
- package/src/editor/client/live/session.tsx +162 -0
- package/src/editor/client/main.tsx +12 -0
- package/src/editor/client/media/LibraryStage.tsx +75 -0
- package/src/editor/client/media/ReplaceDialog.tsx +355 -0
- package/src/editor/client/media/store.ts +618 -0
- package/src/editor/client/nav/DocRow.tsx +122 -0
- package/src/editor/client/nav/search.ts +71 -0
- package/src/editor/client/nav/tree.ts +93 -0
- package/src/editor/client/overlays/OverlayMount.tsx +26 -0
- package/src/editor/client/overlays/editor-marks.ts +159 -0
- package/src/editor/client/overlays/index.ts +50 -0
- package/src/editor/client/overlays/layer.ts +437 -0
- package/src/editor/client/overlays/overlay-css.ts +177 -0
- package/src/editor/client/overlays/rich-text-bar.ts +717 -0
- package/src/editor/client/pane/Pane.tsx +91 -0
- package/src/editor/client/pane/levels/BlockLevel.tsx +123 -0
- package/src/editor/client/pane/levels/CollectionLevel.tsx +115 -0
- package/src/editor/client/pane/levels/DocumentLevel.tsx +93 -0
- package/src/editor/client/pane/levels/DocumentsLevel.tsx +390 -0
- package/src/editor/client/pane/levels/FileLevel.tsx +132 -0
- package/src/editor/client/pane/levels/FormLevel.tsx +550 -0
- package/src/editor/client/pane/levels/MediaLevel.tsx +657 -0
- package/src/editor/client/pane/levels/SettingsM6.tsx +299 -0
- package/src/editor/client/pane/levels/SubmissionsLevel.tsx +185 -0
- package/src/editor/client/pane/parts.tsx +179 -0
- package/src/editor/client/pane/switcher.tsx +165 -0
- package/src/editor/client/preview/driver.ts +640 -0
- package/src/editor/client/preview/field-text.ts +71 -0
- package/src/editor/client/preview/follow-links.ts +234 -0
- package/src/editor/client/preview/layout.ts +37 -0
- package/src/editor/client/preview/link-target.ts +50 -0
- package/src/editor/client/preview/markers.ts +86 -0
- package/src/editor/client/preview/page-edit.ts +632 -0
- package/src/editor/client/preview/page-editor-registry.ts +63 -0
- package/src/editor/client/preview/page-editor.ts +235 -0
- package/src/editor/client/preview/render-sequencer.ts +113 -0
- package/src/editor/client/preview/selection.ts +116 -0
- package/src/editor/client/preview/warnings.ts +61 -0
- package/src/editor/client/shell/App.tsx +101 -0
- package/src/editor/client/shell/Bar.tsx +197 -0
- package/src/editor/client/shell/Brand.tsx +33 -0
- package/src/editor/client/shell/Icons.tsx +87 -0
- package/src/editor/client/shell/NarrowNote.tsx +47 -0
- package/src/editor/client/shell/Popovers.tsx +47 -0
- package/src/editor/client/shell/Stage.tsx +132 -0
- package/src/editor/client/shell/Toast.tsx +61 -0
- package/src/editor/client/state/document-store.ts +465 -0
- package/src/editor/client/state/keeper.ts +713 -0
- package/src/editor/client/state/live.ts +257 -0
- package/src/editor/client/state/schema.ts +42 -0
- package/src/editor/client/state/store.ts +39 -0
- package/src/editor/fonts/OFL-GeistMono.txt +93 -0
- package/src/editor/fonts/OFL-InstrumentSans.txt +93 -0
- package/src/editor/fonts/geist-mono-latin-ext-wght-normal.woff2 +0 -0
- package/src/editor/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
- package/src/editor/fonts/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
- package/src/editor/fonts/instrument-sans-latin-wght-normal.woff2 +0 -0
- package/src/editor/middleware.ts +62 -0
- package/src/editor/routes/_live.ts +26 -0
- package/src/editor/routes/admin.astro +80 -0
- package/src/editor/routes/create.ts +19 -0
- package/src/editor/routes/delete.ts +19 -0
- package/src/editor/routes/document.ts +39 -0
- package/src/editor/routes/documents.ts +30 -0
- package/src/editor/routes/github.ts +23 -0
- package/src/editor/routes/history.ts +9 -0
- package/src/editor/routes/lock.ts +9 -0
- package/src/editor/routes/login/assets.d.ts +10 -0
- package/src/editor/routes/login/login.ts +43 -0
- package/src/editor/routes/media.ts +16 -0
- package/src/editor/routes/move.ts +17 -0
- package/src/editor/routes/notice.ts +9 -0
- package/src/editor/routes/preview.astro +77 -0
- package/src/editor/routes/publish.ts +9 -0
- package/src/editor/routes/status.ts +9 -0
- package/src/editor/routes/submissions.ts +16 -0
- package/src/editor/routes/try-again.ts +9 -0
- package/src/editor/routes/write.ts +18 -0
- package/src/editor/server/admin-props.ts +151 -0
- package/src/editor/server/cards.ts +61 -0
- package/src/editor/server/create.ts +414 -0
- package/src/editor/server/drafts-store.ts +127 -0
- package/src/editor/server/media/alt.ts +107 -0
- package/src/editor/server/media/bucket.ts +295 -0
- package/src/editor/server/media/config.ts +78 -0
- package/src/editor/server/media/handler.ts +221 -0
- package/src/editor/server/media/library.ts +254 -0
- package/src/editor/server/media/memory.ts +184 -0
- package/src/editor/server/media/mux.ts +241 -0
- package/src/editor/server/media/sniff.ts +599 -0
- package/src/editor/server/media/upload.ts +175 -0
- package/src/editor/server/mode.ts +63 -0
- package/src/editor/server/move.ts +295 -0
- package/src/editor/server/render.tsx +9 -0
- package/src/editor/server/replace.ts +72 -0
- package/src/editor/server/settings.ts +35 -0
- package/src/editor/server/write.ts +366 -0
- package/src/editor/styles/editor.css +502 -0
- package/src/editor/styles/fonts.css +40 -0
- package/src/editor/styles/levels/README.md +4 -0
- package/src/editor/styles/levels/collections.css +99 -0
- package/src/editor/styles/levels/document-level.css +10 -0
- package/src/editor/styles/levels/documents.css +9 -0
- package/src/editor/styles/levels/fields.css +171 -0
- package/src/editor/styles/levels/files.css +95 -0
- package/src/editor/styles/levels/live.css +10 -0
- package/src/editor/styles/levels/m5a-documents.css +51 -0
- package/src/editor/styles/levels/m6.css +82 -0
- package/src/editor/styles/levels/media.css +175 -0
- package/src/editor/styles/levels/switcher.css +3 -0
- package/src/editor/styles/tokens.css +114 -0
- package/src/editor/tsconfig.json +16 -0
- package/src/editor/types.ts +881 -0
- package/src/editor/ui-state.ts +113 -0
- package/src/email.ts +67 -0
- package/src/fields.ts +1129 -0
- package/src/files-route.ts +201 -0
- package/src/files.ts +116 -0
- package/src/forms/email.ts +149 -0
- package/src/forms/endpoint.ts +347 -0
- package/src/forms/inbox.ts +273 -0
- package/src/forms/rate-limit.ts +130 -0
- package/src/forms/route.ts +24 -0
- package/src/forms/rules.ts +278 -0
- package/src/forms/store.ts +486 -0
- package/src/head.ts +125 -0
- package/src/index.ts +434 -0
- package/src/json-source.ts +116 -0
- package/src/links.ts +121 -0
- package/src/live/README.md +43 -0
- package/src/live/auth/allow-rules.ts +70 -0
- package/src/live/auth/allow.ts +128 -0
- package/src/live/auth/auth.ts +155 -0
- package/src/live/auth/email.ts +84 -0
- package/src/live/auth/guard.ts +111 -0
- package/src/live/auth/index.ts +68 -0
- package/src/live/auth/links.ts +101 -0
- package/src/live/auth/login.ts +245 -0
- package/src/live/auth/middleware.ts +53 -0
- package/src/live/auth/request.ts +56 -0
- package/src/live/auth/routes.ts +9 -0
- package/src/live/auth/session.ts +181 -0
- package/src/live/auth/views.ts +355 -0
- package/src/live/autosaves.ts +306 -0
- package/src/live/backend.ts +652 -0
- package/src/live/branches.ts +19 -0
- package/src/live/drafts-store.ts +591 -0
- package/src/live/editor.ts +465 -0
- package/src/live/git.ts +1021 -0
- package/src/live/github.ts +271 -0
- package/src/live/locals.ts +24 -0
- package/src/live/locks.ts +273 -0
- package/src/live/merge.ts +197 -0
- package/src/live/messages.ts +151 -0
- package/src/live/mode.ts +210 -0
- package/src/live/redis.ts +533 -0
- package/src/live/runtime.ts +164 -0
- package/src/live/status.ts +109 -0
- package/src/live/sweep.ts +228 -0
- package/src/live/timings.ts +47 -0
- package/src/live/webhook.ts +39 -0
- package/src/lockup-css.ts +58 -0
- package/src/measure.ts +145 -0
- package/src/media-value.ts +239 -0
- package/src/media.ts +255 -0
- package/src/read.ts +256 -0
- package/src/redirect-middleware.ts +49 -0
- package/src/redirects.ts +76 -0
- package/src/relink.ts +293 -0
- package/src/reserved.ts +24 -0
- package/src/rich-text.ts +690 -0
- package/src/schema-key.ts +63 -0
- package/src/seo.ts +124 -0
- package/src/serialize.ts +697 -0
- package/src/skipped.ts +35 -0
- package/src/slug.ts +121 -0
- package/src/text-styles.ts +62 -0
- package/src/virtual.d.ts +28 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor shell (Decision 2; M2's 4d layout): one left pane, the stage with the
|
|
3
|
+
* preview, one dark bar along the bottom, server-rendered so the chrome is there at
|
|
4
|
+
* first paint and hydrated in the browser.
|
|
5
|
+
*
|
|
6
|
+
* Keys (M2 → Rules): ⌘K the document switcher; Esc closes a popover, leaves a field,
|
|
7
|
+
* then goes up a level; ⌘Z (outside a field) undoes (Decision 18).
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect } from 'react';
|
|
10
|
+
import { EditorContext, useStore } from '../context.tsx';
|
|
11
|
+
import type { Editor } from '../editor.ts';
|
|
12
|
+
import { OverlayMount } from '../overlays/OverlayMount.tsx';
|
|
13
|
+
import { Pane } from '../pane/Pane.tsx';
|
|
14
|
+
import { SWITCHER_READY } from '../pane/switcher.tsx';
|
|
15
|
+
import { Bar } from './Bar.tsx';
|
|
16
|
+
import { IconSprite } from './Icons.tsx';
|
|
17
|
+
import { NarrowNote } from './NarrowNote.tsx';
|
|
18
|
+
import { Popovers } from './Popovers.tsx';
|
|
19
|
+
import { SignedOutModal } from '../live/session.tsx';
|
|
20
|
+
import { Stage } from './Stage.tsx';
|
|
21
|
+
import { DocumentDialogs } from '../documents/dialogs.tsx';
|
|
22
|
+
import { Toast } from './Toast.tsx';
|
|
23
|
+
|
|
24
|
+
const IN_FIELD = 'input,textarea,select,[contenteditable="true"],[contenteditable="plaintext-only"]';
|
|
25
|
+
|
|
26
|
+
export function EditorApp({ editor }: { editor: Editor }) {
|
|
27
|
+
useEffect(() => editor.start(), [editor]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const onKey = (event: KeyboardEvent) => {
|
|
30
|
+
const target = event.target as Element | null;
|
|
31
|
+
const inField = !!target?.closest?.(IN_FIELD);
|
|
32
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
33
|
+
if (mod && event.key.toLowerCase() === 'k') {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
const { pop } = editor.view.get();
|
|
36
|
+
if (SWITCHER_READY) editor.setPop(pop === 'switcher' ? null : 'switcher');
|
|
37
|
+
else editor.goTo('documents');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// ⌘\ hides or shows the sidebar (pre-M7 polish), from anywhere, a field included.
|
|
41
|
+
if (mod && !event.shiftKey && !event.altKey && (event.key === '\\' || event.code === 'Backslash')) {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
editor.setSideHidden(editor.ui.get().side !== 'hidden');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (mod && !event.shiftKey && event.key.toLowerCase() === 'z' && !inField && editor.doc.get().undo.length > 0) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
editor.doc.undo();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (event.key === 'Escape') {
|
|
52
|
+
if (event.defaultPrevented) return;
|
|
53
|
+
if (editor.view.get().pop) {
|
|
54
|
+
editor.setPop(null);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (inField) {
|
|
58
|
+
(target as HTMLElement).blur();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
editor.goUp();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
window.addEventListener('keydown', onKey);
|
|
65
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
66
|
+
}, [editor]);
|
|
67
|
+
|
|
68
|
+
const sideHidden = useStore(editor.ui, (u) => u.side === 'hidden');
|
|
69
|
+
return (
|
|
70
|
+
<EditorContext.Provider value={editor}>
|
|
71
|
+
<IconSprite />
|
|
72
|
+
<div id="app" data-screen="editor">
|
|
73
|
+
<div className="editor" id="editor" data-side={sideHidden ? 'hidden' : undefined}>
|
|
74
|
+
<Pane />
|
|
75
|
+
<Stage />
|
|
76
|
+
<Bar />
|
|
77
|
+
<Popovers />
|
|
78
|
+
<div id="toast-root">
|
|
79
|
+
<Toast />
|
|
80
|
+
</div>
|
|
81
|
+
<div id="modal-root">
|
|
82
|
+
{editor.mode === 'live' ? <SignedOutModal /> : null}
|
|
83
|
+
<DocumentDialogs />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<NarrowNote />
|
|
87
|
+
</div>
|
|
88
|
+
<Live editor={editor} />
|
|
89
|
+
<OverlayMount />
|
|
90
|
+
</EditorContext.Provider>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Live({ editor }: { editor: Editor }) {
|
|
95
|
+
const text = useStore(editor.live);
|
|
96
|
+
return (
|
|
97
|
+
<div className="sr" id="live" aria-live="polite">
|
|
98
|
+
{text}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one bar, dark, along the bottom (the mockup's renderBar): the brand slots and
|
|
3
|
+
* the document switcher's trigger on the left; the width toggle with its scale
|
|
4
|
+
* under the preview; the keep state on the right. In live mode (M4) the waiting
|
|
5
|
+
* count with Publish (live/publish.tsx) and the signed-in avatar (live/session.tsx)
|
|
6
|
+
* follow it, and a locked document's trigger has a lock; in local mode their places
|
|
7
|
+
* stay empty, as in M3.
|
|
8
|
+
*/
|
|
9
|
+
import { SWITCHER_READY } from '../pane/switcher.tsx';
|
|
10
|
+
import { useEditor, useStore } from '../context.tsx';
|
|
11
|
+
import { TRUE_WIDTH, WIDTHS, WIDTH_LABEL, docId } from '../../types.ts';
|
|
12
|
+
import { PublishButton } from '../live/publish.tsx';
|
|
13
|
+
import { AccountButton } from '../live/session.tsx';
|
|
14
|
+
import { BrandMark, useBrand } from './Brand.tsx';
|
|
15
|
+
import { Icon } from './Icons.tsx';
|
|
16
|
+
import { useFrameScale } from './Stage.tsx';
|
|
17
|
+
|
|
18
|
+
export function Bar() {
|
|
19
|
+
const editor = useEditor();
|
|
20
|
+
const brand = useBrand();
|
|
21
|
+
const docTitle = useStore(editor.doc.state, (s) => s.document.title || s.entry.label);
|
|
22
|
+
// M6: the inbox names itself (it has no document of its own).
|
|
23
|
+
const inInbox = useStore(editor.view, (v) => v.level === 'submissions');
|
|
24
|
+
const title = inInbox ? 'Submissions' : docTitle;
|
|
25
|
+
const width = useStore(editor.view, (v) => v.width);
|
|
26
|
+
const pop = useStore(editor.view, (v) => v.pop);
|
|
27
|
+
const viewOpening = useStore(editor.view, (v) => v.opening);
|
|
28
|
+
const renderOpening = useStore(editor.previewStatus, (s) => s.opening);
|
|
29
|
+
const failed = useStore(editor.previewStatus, (s) => s.failed);
|
|
30
|
+
const warnings = useStore(editor.previewStatus, (s) => s.warnings);
|
|
31
|
+
const documents = useStore(editor.documents);
|
|
32
|
+
const scale = useFrameScale(width);
|
|
33
|
+
const veil = useStore(editor.access, (a) => a.veil);
|
|
34
|
+
const sideHidden = useStore(editor.ui, (u) => u.side === 'hidden');
|
|
35
|
+
const tooLong = warnings.filter((w) => w.tooLong).length;
|
|
36
|
+
const opening = viewOpening ?? renderOpening;
|
|
37
|
+
const openingLabel = opening ? (documents.find((d) => docId(d) === docId(opening))?.label ?? opening.path) : null;
|
|
38
|
+
|
|
39
|
+
const openSwitcher = () => {
|
|
40
|
+
if (SWITCHER_READY) editor.setPop(pop === 'switcher' ? null : 'switcher');
|
|
41
|
+
else editor.goTo('documents');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let note = null;
|
|
45
|
+
if (openingLabel) note = <span className="bar-note">Opening {openingLabel}…</span>;
|
|
46
|
+
else if (failed)
|
|
47
|
+
note = (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
className="bar-warn"
|
|
51
|
+
aria-haspopup="dialog"
|
|
52
|
+
aria-expanded={pop === 'preview-trouble'}
|
|
53
|
+
onClick={() => editor.setPop(pop === 'preview-trouble' ? null : 'preview-trouble')}
|
|
54
|
+
>
|
|
55
|
+
<Icon name="warn" size="sm" />
|
|
56
|
+
Preview not updated
|
|
57
|
+
</button>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<footer className="bar" id="bar" aria-label="Editor bar">
|
|
62
|
+
<div className="bar-l">
|
|
63
|
+
<span className="logo-slot" title={brand.name}>
|
|
64
|
+
<BrandMark />
|
|
65
|
+
</span>
|
|
66
|
+
<span className="bar-name">{brand.name}</span>
|
|
67
|
+
<span className="bar-sep" aria-hidden="true" />
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
className="docbtn"
|
|
71
|
+
aria-haspopup="dialog"
|
|
72
|
+
aria-expanded={pop === 'switcher'}
|
|
73
|
+
aria-label={`${title}: go to another document`}
|
|
74
|
+
onClick={openSwitcher}
|
|
75
|
+
>
|
|
76
|
+
{veil ? <Icon name="lock" size="sm" /> : null}
|
|
77
|
+
<span className="t">{title}</span>
|
|
78
|
+
<Icon name="chev-u" size="xs" />
|
|
79
|
+
<kbd>⌘K</kbd>
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className="ib side-toggle"
|
|
84
|
+
aria-controls="side"
|
|
85
|
+
aria-expanded={!sideHidden}
|
|
86
|
+
aria-keyshortcuts="Meta+Backslash Control+Backslash"
|
|
87
|
+
aria-label={sideHidden ? 'Show the sidebar' : 'Hide the sidebar'}
|
|
88
|
+
title={sideHidden ? 'Show the sidebar (⌘\\)' : 'Hide the sidebar (⌘\\)'}
|
|
89
|
+
onClick={() => editor.setSideHidden(!sideHidden)}
|
|
90
|
+
>
|
|
91
|
+
<Icon name="sidebar" />
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
<div className="bar-r">
|
|
95
|
+
<div className="bar-a">{note}</div>
|
|
96
|
+
<div className="bar-c">
|
|
97
|
+
<div className="seg" role="radiogroup" aria-label="Preview width">
|
|
98
|
+
{WIDTHS.map((w) => (
|
|
99
|
+
<button key={w} type="button" role="radio" aria-checked={width === w} data-width={w} onClick={() => editor.setWidth(w)}>
|
|
100
|
+
{WIDTH_LABEL[w]}
|
|
101
|
+
<span className="w">{TRUE_WIDTH[w]}</span>
|
|
102
|
+
{w === 'phone' && tooLong > 0 ? (
|
|
103
|
+
<>
|
|
104
|
+
<span className="wdot" aria-hidden="true" />
|
|
105
|
+
<span className="tip" role="tooltip">
|
|
106
|
+
<b>
|
|
107
|
+
{tooLong} thing{tooLong > 1 ? 's' : ''}
|
|
108
|
+
</b>{' '}
|
|
109
|
+
too long for mobile
|
|
110
|
+
</span>
|
|
111
|
+
<span className="sr">
|
|
112
|
+
, {tooLong} thing{tooLong > 1 ? 's' : ''} too long for mobile
|
|
113
|
+
</span>
|
|
114
|
+
</>
|
|
115
|
+
) : null}
|
|
116
|
+
</button>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
<span className="scale" title={scale ? `Shown at ${Math.round(scale * 100)}% of its real size` : undefined}>
|
|
120
|
+
{scale === null ? '' : scale >= 0.995 ? '1×' : `${scale.toFixed(2)}×`}
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="bar-e">
|
|
124
|
+
<Keep />
|
|
125
|
+
{editor.mode === 'live' ? (
|
|
126
|
+
<>
|
|
127
|
+
<PublishButton />
|
|
128
|
+
<AccountButton />
|
|
129
|
+
</>
|
|
130
|
+
) : (
|
|
131
|
+
<>
|
|
132
|
+
{/* Local mode (M3): the waiting count, Publish and the avatar are live mode's. */}
|
|
133
|
+
<span data-slot="publish" hidden />
|
|
134
|
+
<span data-slot="account" hidden />
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</footer>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function Keep() {
|
|
144
|
+
const editor = useEditor();
|
|
145
|
+
const state = useStore(editor.keep, (k) => k.state);
|
|
146
|
+
const error = useStore(editor.keep, (k) => k.error);
|
|
147
|
+
const unsent = useStore(editor.keep, (k) => k.unsent ?? 0);
|
|
148
|
+
const signedOut = useStore(editor.liveState, (s) => !!s.signedOut);
|
|
149
|
+
if (state === 'signedout' || (signedOut && state !== 'offline')) {
|
|
150
|
+
return (
|
|
151
|
+
<span className="keep loud" role="status" data-keep="signedout">
|
|
152
|
+
<Icon name="signout" size="sm" />
|
|
153
|
+
{unsent > 0 ? 'Not kept: signed out' : 'Signed out'}
|
|
154
|
+
</span>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (state === 'held') {
|
|
158
|
+
// Live mode: a 409, or another editor's change since. The document's note says why, with Use mine.
|
|
159
|
+
return (
|
|
160
|
+
<span className="keep loud" role="status" data-keep="held" title="Your latest changes are held in this browser">
|
|
161
|
+
<Icon name="warn" size="sm" />
|
|
162
|
+
Not kept
|
|
163
|
+
</span>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (state === 'keeping') {
|
|
167
|
+
return (
|
|
168
|
+
<span className="keep" role="status" data-keep="keeping">
|
|
169
|
+
<span className="pulse" aria-hidden="true" />
|
|
170
|
+
Keeping…
|
|
171
|
+
</span>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (state === 'offline') {
|
|
175
|
+
return (
|
|
176
|
+
<span className="keep loud" role="status" data-keep="offline" title={error ?? undefined}>
|
|
177
|
+
<Icon name="offline" size="sm" />
|
|
178
|
+
Can't reach the server
|
|
179
|
+
</span>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (state === 'refused') {
|
|
183
|
+
// The server answered and said no (a 4xx): not "can't reach". Its message is in the pane's note.
|
|
184
|
+
return (
|
|
185
|
+
<span className="keep loud" role="status" data-keep="refused" title={error ?? undefined}>
|
|
186
|
+
<Icon name="warn" size="sm" />
|
|
187
|
+
Not kept
|
|
188
|
+
</span>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return (
|
|
192
|
+
<span className="keep" data-keep="kept">
|
|
193
|
+
<Icon name="check" size="sm" />
|
|
194
|
+
Kept
|
|
195
|
+
</span>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The brand's logo slot (M2 → Brand slots), wherever the chrome shows the site's
|
|
3
|
+
* mark: the bar, the documents level's header and the narrow note.
|
|
4
|
+
*
|
|
5
|
+
* A site's logo (Site Settings' `brand.logo`, a signed URL from the server: M5b
|
|
6
|
+
* Decision 11) fills the slot. A site with
|
|
7
|
+
* no logo gets its initial on its accent: the accent fills the slot's tile, the
|
|
8
|
+
* letter is light or dark by contrast and a pale accent gets a darker edge (M2
|
|
9
|
+
* Decision 5; brand.ts computes both), so any accent stays legible.
|
|
10
|
+
*
|
|
11
|
+
* All four slots are Site Settings' (M5a Decision 10): `editor.brand`, which the
|
|
12
|
+
* live poll keeps current, so a change shows without a reload. `useBrand()` is the
|
|
13
|
+
* chrome's one way to read them.
|
|
14
|
+
*/
|
|
15
|
+
import { useEditor, useStore } from '../context.tsx';
|
|
16
|
+
import type { EditorBrand } from '../../types.ts';
|
|
17
|
+
|
|
18
|
+
/** The brand as it applies now (Site Settings' name, accent, logo and icon). */
|
|
19
|
+
export function useBrand(): EditorBrand {
|
|
20
|
+
const editor = useEditor();
|
|
21
|
+
return useStore(editor.brand);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function BrandMark() {
|
|
25
|
+
const brand = useBrand();
|
|
26
|
+
if (brand.logo) return <img src={brand.logo} alt="" />;
|
|
27
|
+
const initial = [...brand.name.trim()][0]?.toLocaleUpperCase() ?? '';
|
|
28
|
+
return (
|
|
29
|
+
<span className="logo-mono" aria-hidden="true">
|
|
30
|
+
{initial}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chrome's icons: the mockup's SVG sprite, lifted as is (notes/mockups/m2-editor.html).
|
|
3
|
+
* The sprite renders once in the shell; <Icon name="grip" /> uses a symbol from it.
|
|
4
|
+
* `i-*` are interface icons; `b-*` are block-type icons.
|
|
5
|
+
*/
|
|
6
|
+
const SYMBOLS = [
|
|
7
|
+
'<symbol id="i-chev-l" viewBox="0 0 16 16"><path d="M10 3 5 8l5 5"/></symbol>',
|
|
8
|
+
'<symbol id="i-chev-r" viewBox="0 0 16 16"><path d="m6 3 5 5-5 5"/></symbol>',
|
|
9
|
+
'<symbol id="i-chev-d" viewBox="0 0 16 16"><path d="m3.5 6 4.5 4.5L12.5 6"/></symbol>',
|
|
10
|
+
'<symbol id="i-chev-u" viewBox="0 0 16 16"><path d="m3.5 10 4.5-4.5 4.5 4.5"/></symbol>',
|
|
11
|
+
'<symbol id="i-sidebar" viewBox="0 0 16 16"><rect x="2" y="3" width="12" height="10" rx="2"/><path d="M6.5 3v10"/></symbol>',
|
|
12
|
+
'<symbol id="i-up" viewBox="0 0 16 16"><path d="M8 13V3M4 7l4-4 4 4"/></symbol>',
|
|
13
|
+
'<symbol id="i-down" viewBox="0 0 16 16"><path d="M8 3v10M4 9l4 4 4-4"/></symbol>',
|
|
14
|
+
'<symbol id="i-page" viewBox="0 0 16 16"><path d="M4 1.5h5.5L12.5 4.5v10H4z"/><path d="M9.5 1.5v3h3"/></symbol>',
|
|
15
|
+
'<symbol id="i-folder" viewBox="0 0 16 16"><path d="M1.5 4.2a1 1 0 0 1 1-1h3.4l1.6 1.8h6a1 1 0 0 1 1 1v6.8a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1z"/></symbol>',
|
|
16
|
+
'<symbol id="i-case" viewBox="0 0 16 16"><rect x="1.5" y="4.5" width="13" height="9" rx="1.5"/><path d="M5.5 4.5v-2h5v2"/></symbol>',
|
|
17
|
+
'<symbol id="i-journal" viewBox="0 0 16 16"><path d="M3.5 1.5h9v13h-9z"/><path d="M6 5h4.5M6 7.5h4.5M6 10h3"/></symbol>',
|
|
18
|
+
// M6c (G5): the List / Grid switch, and a clip card's play mark
|
|
19
|
+
'<symbol id="i-list" viewBox="0 0 16 16"><path d="M5.5 4h8.5M5.5 8h8.5M5.5 12h8.5"/><path d="M2 4h1M2 8h1M2 12h1"/></symbol>',
|
|
20
|
+
'<symbol id="i-grid" viewBox="0 0 16 16"><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/></symbol>',
|
|
21
|
+
'<symbol id="i-play" viewBox="0 0 10 10"><path d="M2 1.2v7.6L8.6 5z"/></symbol>',
|
|
22
|
+
'<symbol id="i-shared" viewBox="0 0 16 16"><path d="M6.6 9.4a2.9 2.9 0 0 0 4.1 0l2.2-2.2a2.9 2.9 0 0 0-4.1-4.1l-.7.7"/><path d="M9.4 6.6a2.9 2.9 0 0 0-4.1 0L3.1 8.8a2.9 2.9 0 0 0 4.1 4.1l.7-.7"/></symbol>',
|
|
23
|
+
'<symbol id="i-bolt" viewBox="0 0 16 16"><path d="M9 1.5 3.5 9H8l-1 5.5L12.5 7H8z"/></symbol>',
|
|
24
|
+
'<symbol id="i-gear" viewBox="0 0 16 16"><circle cx="8" cy="8" r="2.3"/><path d="M8 1.5v1.8M8 12.7v1.8M1.5 8h1.8M12.7 8h1.8M3.4 3.4l1.3 1.3M11.3 11.3l1.3 1.3M3.4 12.6l1.3-1.3M11.3 4.7l1.3-1.3"/></symbol>',
|
|
25
|
+
'<symbol id="i-lock" viewBox="0 0 16 16"><rect x="3" y="7" width="10" height="7.5" rx="1.5"/><path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2"/></symbol>',
|
|
26
|
+
'<symbol id="i-check" viewBox="0 0 16 16"><path d="m3 8.5 3.2 3L13 4.5"/></symbol>',
|
|
27
|
+
'<symbol id="i-warn" viewBox="0 0 16 16"><path d="M8 2 14.8 13.6H1.2z"/><path d="M8 6.3v3.4M8 11.6v.1"/></symbol>',
|
|
28
|
+
'<symbol id="i-x" viewBox="0 0 16 16"><path d="m4 4 8 8M12 4l-8 8"/></symbol>',
|
|
29
|
+
'<symbol id="i-plus" viewBox="0 0 16 16"><path d="M8 3v10M3 8h10"/></symbol>',
|
|
30
|
+
'<symbol id="i-grip" viewBox="0 0 16 16"><g fill="currentColor" stroke="none"><circle cx="6" cy="4" r="1.1"/><circle cx="10" cy="4" r="1.1"/><circle cx="6" cy="8" r="1.1"/><circle cx="10" cy="8" r="1.1"/><circle cx="6" cy="12" r="1.1"/><circle cx="10" cy="12" r="1.1"/></g></symbol>',
|
|
31
|
+
'<symbol id="i-more" viewBox="0 0 16 16"><g fill="currentColor" stroke="none"><circle cx="3.5" cy="8" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="12.5" cy="8" r="1.2"/></g></symbol>',
|
|
32
|
+
'<symbol id="i-search" viewBox="0 0 16 16"><circle cx="7" cy="7" r="4.5"/><path d="m10.4 10.4 3.6 3.6"/></symbol>',
|
|
33
|
+
'<symbol id="i-phone" viewBox="0 0 16 16"><rect x="4.5" y="1.5" width="7" height="13" rx="1.6"/><path d="M7 12.3h2"/></symbol>',
|
|
34
|
+
'<symbol id="i-image" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5"/><circle cx="5.5" cy="6" r="1.2"/><path d="m2 12 3.8-3.8 3 3 2-2 3.6 3.6"/></symbol>',
|
|
35
|
+
'<symbol id="i-clock" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.3"/><path d="M8 4.6V8l2.4 1.5"/></symbol>',
|
|
36
|
+
'<symbol id="i-undo" viewBox="0 0 16 16"><path d="M5 3 2 6l3 3"/><path d="M2 6h7.5a4 4 0 0 1 0 8H7"/></symbol>',
|
|
37
|
+
'<symbol id="i-ext" viewBox="0 0 16 16"><path d="M9 2.5h4.5V7M13.5 2.5 7.5 8.5M11.5 9.5v4h-9v-9h4"/></symbol>',
|
|
38
|
+
'<symbol id="i-trash" viewBox="0 0 16 16"><path d="M2.5 4h11M6 4V2.5h4V4M4 4l.7 10h6.6L12 4"/></symbol>',
|
|
39
|
+
'<symbol id="i-offline" viewBox="0 0 16 16"><path d="M4.4 12.5h7.1a3 3 0 0 0 .5-6 4.4 4.4 0 0 0-8.1-.9A3.4 3.4 0 0 0 4.4 12.5z"/><path d="m2 2 12 12"/></symbol>',
|
|
40
|
+
'<symbol id="i-target" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5.5"/><circle cx="8" cy="8" r="1.6" fill="currentColor"/></symbol>',
|
|
41
|
+
'<symbol id="i-history" viewBox="0 0 16 16"><path d="M2.5 8a5.5 5.5 0 1 0 1.6-3.9L2.5 5.7"/><path d="M2.5 2.5v3.2h3.2M8 5v3l2 1.3"/></symbol>',
|
|
42
|
+
'<symbol id="i-signout" viewBox="0 0 16 16"><path d="M6 2.5H3v11h3M10 5l3 3-3 3M13 8H6"/></symbol>',
|
|
43
|
+
// Pre-M7 item 8: Change address…, and a redirect's "from → to".
|
|
44
|
+
'<symbol id="i-arrow-r" viewBox="0 0 16 16"><path d="M2.5 8h11M9.5 4l4 4-4 4"/></symbol>',
|
|
45
|
+
'<symbol id="i-copy" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2"/></symbol>',
|
|
46
|
+
'<symbol id="i-upload" viewBox="0 0 16 16"><path d="M8 10.5V2.5M4.8 5.7 8 2.5l3.2 3.2"/><path d="M2.5 10v2.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V10"/></symbol>',
|
|
47
|
+
'<symbol id="i-film" viewBox="0 0 16 16"><rect x="1.5" y="3" width="13" height="10" rx="1.5"/><path d="M6.5 6v4l3.5-2z"/></symbol>',
|
|
48
|
+
'<symbol id="i-media" viewBox="0 0 16 16"><rect x="4.5" y="1.5" width="10" height="9" rx="1.2"/><path d="M11.5 13.5h-9a1 1 0 0 1-1-1v-7"/><path d="m5 10 3-3 2.2 2.2 1.3-1.3 3 3"/></symbol>',
|
|
49
|
+
'<symbol id="i-mail" viewBox="0 0 16 16"><rect x="1.5" y="3.5" width="13" height="9" rx="1.5"/><path d="m2 4.5 6 4.5 6-4.5"/></symbol>',
|
|
50
|
+
// M6: the inbox
|
|
51
|
+
'<symbol id="i-inbox" viewBox="0 0 16 16"><path d="M1.5 9.5 3.6 3a1 1 0 0 1 .95-.7h6.9a1 1 0 0 1 .95.7l2.1 6.5v3.2a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1z"/><path d="M1.5 9.5h3.7l1 1.7h3.6l1-1.7h3.7"/></symbol>',
|
|
52
|
+
'<symbol id="i-download" viewBox="0 0 16 16"><path d="M8 2.5v8M4.8 7.3 8 10.5l3.2-3.2"/><path d="M2.5 10v2.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V10"/></symbol>',
|
|
53
|
+
'<symbol id="i-reply" viewBox="0 0 16 16"><path d="M6.5 3.5 2.5 7.5l4 4"/><path d="M2.5 7.5h6.8a4.2 4.2 0 0 1 4.2 4.2v.8"/></symbol>',
|
|
54
|
+
'<symbol id="i-attach" viewBox="0 0 16 16"><path d="m13.2 7.6-5.5 5.5a3.2 3.2 0 0 1-4.6-4.6l5.8-5.8a2.1 2.1 0 0 1 3 3l-5.6 5.6a1 1 0 0 1-1.5-1.5l5.1-5.1"/></symbol>',
|
|
55
|
+
'<symbol id="i-unread" viewBox="0 0 16 16"><path d="M14.5 8.5v3.5a1.5 1.5 0 0 1-1.5 1.5H3A1.5 1.5 0 0 1 1.5 12V5A1.5 1.5 0 0 1 3 3.5h6"/><path d="m2 4.5 6 4.5 2.4-1.8"/><circle cx="13" cy="4" r="1.8" fill="currentColor" stroke="none"/></symbol>',
|
|
56
|
+
'<symbol id="b-hero" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="11" rx="1"/><path d="M4 6.5h6.5M4 9h4"/></symbol>',
|
|
57
|
+
'<symbol id="b-head" viewBox="0 0 16 16"><path d="M2.5 4h9M2.5 7h7M2.5 11h11M2.5 13.5h8"/></symbol>',
|
|
58
|
+
'<symbol id="b-tm" viewBox="0 0 16 16"><rect x="1.5" y="3.5" width="6" height="9" rx="1"/><path d="M9.5 5.5h5M9.5 8h5M9.5 10.5h3"/></symbol>',
|
|
59
|
+
'<symbol id="b-cases" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="5.5" height="5" rx="1"/><rect x="9" y="2.5" width="5.5" height="5" rx="1"/><rect x="1.5" y="9.5" width="5.5" height="4" rx="1"/><rect x="9" y="9.5" width="5.5" height="4" rx="1"/></symbol>',
|
|
60
|
+
'<symbol id="b-cta" viewBox="0 0 16 16"><rect x="1.5" y="3" width="13" height="10" rx="1"/><rect x="5" y="8.8" width="6" height="2.2" rx="1.1"/><path d="M4.5 6h7"/></symbol>',
|
|
61
|
+
'<symbol id="b-footer" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="11" rx="1"/><path d="M1.5 9.5h13M4 11.5h3"/></symbol>',
|
|
62
|
+
'<symbol id="b-team" viewBox="0 0 16 16"><circle cx="5" cy="6" r="2"/><circle cx="11" cy="6" r="2"/><path d="M1.5 13a3.5 3.5 0 0 1 7 0M7.5 13a3.5 3.5 0 0 1 7 0"/></symbol>',
|
|
63
|
+
'<symbol id="b-quote" viewBox="0 0 16 16"><path d="M3 10.5c0-3 1-5 3.5-6M9 10.5c0-3 1-5 3.5-6"/><path d="M3 10.5h2.5v2.5H3zM9 10.5h2.5v2.5H9z"/></symbol>',
|
|
64
|
+
'<symbol id="b-gallery" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="8" height="11" rx="1"/><rect x="11" y="2.5" width="3.5" height="5" rx="1"/><rect x="11" y="8.5" width="3.5" height="5" rx="1"/></symbol>',
|
|
65
|
+
'<symbol id="b-video" viewBox="0 0 16 16"><rect x="1.5" y="3" width="13" height="10" rx="1.5"/><path d="M6.5 6v4l3.5-2z"/></symbol>',
|
|
66
|
+
'<symbol id="b-form" viewBox="0 0 16 16"><rect x="2" y="2.5" width="12" height="3" rx="1"/><rect x="2" y="7" width="12" height="3" rx="1"/><rect x="2" y="11.5" width="5" height="2.5" rx="1"/></symbol>',
|
|
67
|
+
'<symbol id="b-faq" viewBox="0 0 16 16"><path d="M2.5 4h11M2.5 8h11M2.5 12h11"/><path d="m11 3 1.2 1.1L11 5.2"/></symbol>',
|
|
68
|
+
'<symbol id="b-unknown" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="11" rx="1" stroke-dasharray="2 1.8"/><path d="M6.6 6.7a1.5 1.5 0 1 1 2 1.4c-.4.2-.6.5-.6.9v.2M8 10.9v.1"/></symbol>',
|
|
69
|
+
].join('');
|
|
70
|
+
|
|
71
|
+
export function IconSprite() {
|
|
72
|
+
return (
|
|
73
|
+
<svg width="0" height="0" style={{ position: 'absolute' }} aria-hidden="true">
|
|
74
|
+
<defs dangerouslySetInnerHTML={{ __html: SYMBOLS }} />
|
|
75
|
+
</svg>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `name` without the `i-` prefix for interface icons (`grip`, `chev-r`), with `b-` for block icons. */
|
|
80
|
+
export function Icon({ name, size }: { name: string; size?: 'sm' | 'xs' }) {
|
|
81
|
+
const href = `#${name.startsWith('b-') ? name : `i-${name}`}`;
|
|
82
|
+
return (
|
|
83
|
+
<svg className={size ? `i i-${size}` : 'i'} aria-hidden="true">
|
|
84
|
+
<use href={href} />
|
|
85
|
+
</svg>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Below 1024 wide, the editor asks to be opened on a bigger screen (M2 Decision 8;
|
|
3
|
+
* the mockup's #narrow). Shown by a container query (editor.css), so it's right at
|
|
4
|
+
* first paint.
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { useEditor } from '../context.tsx';
|
|
8
|
+
import { BrandMark, useBrand } from './Brand.tsx';
|
|
9
|
+
import { Icon } from './Icons.tsx';
|
|
10
|
+
|
|
11
|
+
export function NarrowNote() {
|
|
12
|
+
const editor = useEditor();
|
|
13
|
+
const { host } = editor.site;
|
|
14
|
+
const brand = useBrand();
|
|
15
|
+
const address = `${host}/admin`;
|
|
16
|
+
const [copied, setCopied] = useState(false);
|
|
17
|
+
return (
|
|
18
|
+
<div className="screen narrow" id="narrow" aria-label="Open the editor on a larger screen">
|
|
19
|
+
<div className="nw">
|
|
20
|
+
<div className="nw-in">
|
|
21
|
+
<div className="nw-brand">
|
|
22
|
+
<span className="si-logo">
|
|
23
|
+
<BrandMark />
|
|
24
|
+
</span>
|
|
25
|
+
{brand.name}
|
|
26
|
+
</div>
|
|
27
|
+
<h1>Open the editor on a bigger screen</h1>
|
|
28
|
+
<p>The editor needs a window at least 1,024 pixels wide.</p>
|
|
29
|
+
<p>On a laptop or desktop, go to:</p>
|
|
30
|
+
<div className="addr">
|
|
31
|
+
<span>{address}</span>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className="btn sm"
|
|
35
|
+
onClick={() => {
|
|
36
|
+
void navigator.clipboard?.writeText(`${location.protocol}//${address}`).then(() => setCopied(true));
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<Icon name={copied ? 'check' : 'copy'} size="sm" />
|
|
40
|
+
{copied ? 'Copied' : 'Copy'}
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Popovers that open upward from the bar. Phase 2's: preview trouble (M2: "a render
|
|
3
|
+
* that failed keeps the last good preview and says the latest change couldn't be
|
|
4
|
+
* shown"). The switcher is Phase 4's (pane/switcher.tsx).
|
|
5
|
+
*/
|
|
6
|
+
import { useEditor, useStore } from '../context.tsx';
|
|
7
|
+
import { Switcher } from '../pane/switcher.tsx';
|
|
8
|
+
import { HistoryPop, WaitingPop } from '../live/publish.tsx';
|
|
9
|
+
import { AccountPop } from '../live/session.tsx';
|
|
10
|
+
|
|
11
|
+
export function Popovers() {
|
|
12
|
+
const editor = useEditor();
|
|
13
|
+
const pop = useStore(editor.view, (v) => v.pop);
|
|
14
|
+
const failed = useStore(editor.previewStatus, (s) => s.failed);
|
|
15
|
+
return (
|
|
16
|
+
<div id="pops">
|
|
17
|
+
{pop === 'switcher' ? <Switcher /> : null}
|
|
18
|
+
{/* Live mode (M4): Publish's summary, its history, the account. */}
|
|
19
|
+
{editor.mode === 'live' && pop === 'waiting' ? <WaitingPop /> : null}
|
|
20
|
+
{editor.mode === 'live' && pop === 'history' ? <HistoryPop /> : null}
|
|
21
|
+
{editor.mode === 'live' && pop === 'account' ? <AccountPop /> : null}
|
|
22
|
+
{pop === 'preview-trouble' && failed ? (
|
|
23
|
+
<div className="pop" role="dialog" aria-label="Preview not updated" style={{ left: 'calc(var(--side-col) + 12px)', width: 360 }}>
|
|
24
|
+
<div className="pop-h">
|
|
25
|
+
<div>
|
|
26
|
+
<h2>The latest change couldn't be shown</h2>
|
|
27
|
+
<p className="sub">Your change is kept.</p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="pop-f">
|
|
31
|
+
<span className="sp" />
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className="btn ink"
|
|
35
|
+
onClick={() => {
|
|
36
|
+
editor.setPop(null);
|
|
37
|
+
editor.preview?.retry();
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
Try again
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
) : null}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stage: three iframes of the preview route at 1440, 768 and 375, one shown,
|
|
3
|
+
* the other two laid out behind it (`inert`, opacity 0; never display: none,
|
|
4
|
+
* Decision 13 and M2 Decision 4). Each at its true width, scaled to fit (layout.ts).
|
|
5
|
+
*
|
|
6
|
+
* No flicker (M2 Decision 9): the server renders the iframes, so they start
|
|
7
|
+
* loading before the editor hydrates; until the first preview has painted the stage
|
|
8
|
+
* is a still surface (`unpainted`), then it fades in once (`painting`). The width
|
|
9
|
+
* switch crossfades. Another document's preview swaps in by morph (driver.ts), so
|
|
10
|
+
* the old one stays on screen until the new one is there.
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useRef, useState } from 'react';
|
|
13
|
+
import { useEditor, useStore } from '../context.tsx';
|
|
14
|
+
import { previewSrc } from '../preview/driver.ts';
|
|
15
|
+
import { stageLayout } from '../preview/layout.ts';
|
|
16
|
+
import { TRUE_WIDTH, WIDTHS, WIDTH_LABEL, type Width } from '../../types.ts';
|
|
17
|
+
import { Icon } from './Icons.tsx';
|
|
18
|
+
import { LibraryStage } from '../media/LibraryStage.tsx';
|
|
19
|
+
import { InboxStage } from '../inbox/InboxStage.tsx';
|
|
20
|
+
import { CollectionTable, useTableCollection } from '../collections/CollectionTable.tsx';
|
|
21
|
+
|
|
22
|
+
export function useFrameScale(width: Width): number | null {
|
|
23
|
+
const editor = useEditor();
|
|
24
|
+
return useStore(editor.stageLayout, (layout) => (layout ? layout[width].scale : null));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Stage() {
|
|
28
|
+
const editor = useEditor();
|
|
29
|
+
const width = useStore(editor.view, (v) => v.width);
|
|
30
|
+
const painted = useStore(editor.previewStatus, (s) => s.painted);
|
|
31
|
+
const layout = useStore(editor.stageLayout);
|
|
32
|
+
// The iframes' first load renders the file of the document opened with the page; later
|
|
33
|
+
// documents are morphed in, so the src never changes (a change would reload the frame).
|
|
34
|
+
// Site Settings has no page of its own (M5a): opened first, the stage shows Home's.
|
|
35
|
+
const [src] = useState(() => previewSrc(editor.previewStatus.get().showing));
|
|
36
|
+
// While Site Settings is open the last page stays, dimmed, with a way back (the mockup's #settings).
|
|
37
|
+
const settingsAside = useStore(editor.doc.state, (s) => s.document.kind === 'settings');
|
|
38
|
+
// M5b: the library (browsing, not a slot's picker) has no page of its own either: the last page, dimmed, is its drop target.
|
|
39
|
+
const library = useStore(editor.view, (v) => v.level === 'media' && !v.media?.pick);
|
|
40
|
+
// M6: the inbox has no page either: the last page, dimmed, with a submission's sheet over it.
|
|
41
|
+
const inbox = useStore(editor.view, (v) => v.level === 'submissions');
|
|
42
|
+
// Pre-M7 polish: a flat collection's level shows its documents as a table, full width, in place of the preview.
|
|
43
|
+
const tableOf = useTableCollection();
|
|
44
|
+
const table = !!tableOf;
|
|
45
|
+
const aside = !table && (settingsAside || library || inbox);
|
|
46
|
+
const showing = useStore(editor.previewStatus, (s) => s.showing);
|
|
47
|
+
const documents = useStore(editor.documents);
|
|
48
|
+
const back = documents.find((d) => d.collection === showing.collection && d.path === showing.path);
|
|
49
|
+
const backLabel = back?.label ?? 'Home';
|
|
50
|
+
const stage = useRef<HTMLElement>(null);
|
|
51
|
+
const iframes = useRef<Partial<Record<Width, HTMLIFrameElement | null>>>({});
|
|
52
|
+
const [fading, setFading] = useState(false);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const el = stage.current;
|
|
56
|
+
if (!el) return;
|
|
57
|
+
const measure = () => {
|
|
58
|
+
if (el.clientWidth > 0 && el.clientHeight > 0) editor.stageLayout.set(stageLayout(el.clientWidth, el.clientHeight));
|
|
59
|
+
};
|
|
60
|
+
measure();
|
|
61
|
+
const observer = new ResizeObserver(measure);
|
|
62
|
+
observer.observe(el);
|
|
63
|
+
return () => observer.disconnect();
|
|
64
|
+
}, [editor]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const { desktop, tablet, phone } = iframes.current;
|
|
68
|
+
if (!editor.preview || !desktop || !tablet || !phone) return;
|
|
69
|
+
return editor.preview.attach({ desktop, tablet, phone });
|
|
70
|
+
}, [editor]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!painted) return;
|
|
74
|
+
setFading(true);
|
|
75
|
+
const timer = setTimeout(() => setFading(false), 400);
|
|
76
|
+
return () => clearTimeout(timer);
|
|
77
|
+
}, [painted]);
|
|
78
|
+
|
|
79
|
+
const className = ['stage', !painted && 'unpainted', fading && 'painting', aside && 'is-aside', table && 'is-table'].filter(Boolean).join(' ');
|
|
80
|
+
return (
|
|
81
|
+
<main ref={stage} className={className} id="stage" aria-label="Preview of the page" data-painted={painted ? 'true' : 'false'}>
|
|
82
|
+
{/* Before the frames, as the mockup's: over the held page while a document has none of its own (Site Settings). */}
|
|
83
|
+
<div className="stage-aside" id="stage-aside" inert={!aside} data-stage-aside={aside ? '' : undefined}>
|
|
84
|
+
{inbox ? (
|
|
85
|
+
<InboxStage back={backLabel} onBack={() => void editor.openDocument(showing)} />
|
|
86
|
+
) : library ? (
|
|
87
|
+
<LibraryStage back={backLabel} onBack={() => void editor.openDocument(showing)} />
|
|
88
|
+
) : aside ? (
|
|
89
|
+
<div className="in">
|
|
90
|
+
<Icon name="gear" />
|
|
91
|
+
<h2>Site Settings has no page of its own</h2>
|
|
92
|
+
<button type="button" className="btn" data-act="back-to-page" onClick={() => void editor.openDocument(showing)}>
|
|
93
|
+
<Icon name="chev-l" size="sm" />
|
|
94
|
+
Back to {backLabel}
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
) : null}
|
|
98
|
+
</div>
|
|
99
|
+
<div className="stage-table" inert={!table} data-stage-table={table ? '' : undefined}>
|
|
100
|
+
{tableOf ? <CollectionTable collection={tableOf} /> : null}
|
|
101
|
+
</div>
|
|
102
|
+
{WIDTHS.map((w) => {
|
|
103
|
+
const box = layout?.[w];
|
|
104
|
+
const on = w === width;
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
key={w}
|
|
108
|
+
className={`frame${on ? ' on' : ''}${w !== 'desktop' ? ' boxed' : ''}`}
|
|
109
|
+
data-w={w}
|
|
110
|
+
inert={!on}
|
|
111
|
+
style={box ? { left: box.left, top: box.top, width: box.width, height: box.height } : undefined}
|
|
112
|
+
>
|
|
113
|
+
<iframe
|
|
114
|
+
inert={aside || table || undefined}
|
|
115
|
+
ref={(el) => {
|
|
116
|
+
iframes.current[w] = el;
|
|
117
|
+
}}
|
|
118
|
+
src={src}
|
|
119
|
+
title={`Preview at ${WIDTH_LABEL[w]} width, ${TRUE_WIDTH[w]} pixels`}
|
|
120
|
+
data-width={TRUE_WIDTH[w]}
|
|
121
|
+
style={{
|
|
122
|
+
width: `${TRUE_WIDTH[w]}px`,
|
|
123
|
+
height: `${box ? box.iframeHeight : 900}px`,
|
|
124
|
+
transform: box ? `scale(${box.scale})` : undefined,
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
</main>
|
|
131
|
+
);
|
|
132
|
+
}
|