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,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The block list (the mockup's #blocks, #removed, #drag, #unknown): a section-builder
|
|
3
|
+
* document's blocks, one row each.
|
|
4
|
+
*
|
|
5
|
+
* - **Hover** sets `blockHover(editor)` (link.ts), and a row is drawn hovered while
|
|
6
|
+
* that store holds its key, so the preview's overlays can follow it both ways.
|
|
7
|
+
* **Selected** (`.is-current`) is `editor.view.block`.
|
|
8
|
+
* - **Reorder by drag** (the grip; the drop line is full width) **and by keys**
|
|
9
|
+
* (M2's keys rule, on a focused row): ⌥↑/⌥↓ move it, ⌫ removes it (with Undo:
|
|
10
|
+
* the toast and ⌘Z, Decision 18), Enter opens it, ↑/↓ move between rows. The row
|
|
11
|
+
* menu has the same moves with their keys.
|
|
12
|
+
* - **An unknown block** (Decisions 7 and 21) is marked Skipped with its kept keys
|
|
13
|
+
* summarised; it moves and is removed like any block, and opens to a panel, not
|
|
14
|
+
* fields. The serializer writes it back as read (by its `source`).
|
|
15
|
+
*
|
|
16
|
+
* Everything that changes the document goes through `editor.doc` (document-store.ts).
|
|
17
|
+
*/
|
|
18
|
+
import { own } from '../../../document.ts';
|
|
19
|
+
import { fieldHoldsMedia, fieldWarnings, type Field } from '../../../fields.ts';
|
|
20
|
+
import { useEffect, useRef, useState, type KeyboardEvent } from 'react';
|
|
21
|
+
import { useEditor, useStore } from '../context.tsx';
|
|
22
|
+
import { Icon } from '../shell/Icons.tsx';
|
|
23
|
+
import { blockLabel } from '../state/document-store.ts';
|
|
24
|
+
import { offersBlock } from '../state/schema.ts';
|
|
25
|
+
import { sameDoc, type EditorBlock, type EditorSite } from '../../types.ts';
|
|
26
|
+
import { blockHover, openPicker, setBlockHover } from './link.ts';
|
|
27
|
+
import { rowKeyAction, useRowDrag } from './reorder.ts';
|
|
28
|
+
import { blockIcon, blockSummary, sharedOf, usedOnLine } from './summary.ts';
|
|
29
|
+
|
|
30
|
+
export function BlockList() {
|
|
31
|
+
const editor = useEditor();
|
|
32
|
+
const blocks = useStore(editor.doc.state, (s) => s.document.blocks);
|
|
33
|
+
const title = useStore(editor.doc.state, (s) => s.document.title || s.entry.label);
|
|
34
|
+
const current = useStore(editor.view, (v) => v.block);
|
|
35
|
+
const hovered = useStore(blockHover(editor));
|
|
36
|
+
// The row's marks beside the name (the mockup's browHTML): too long for mobile, and has its own phone lines.
|
|
37
|
+
const showing = useStore(editor.previewStatus, (s) => s.showing);
|
|
38
|
+
const warnings = useStore(editor.previewStatus, (s) => s.warnings);
|
|
39
|
+
const ref = useStore(editor.doc.state, (s) => s.ref);
|
|
40
|
+
const tooLong = new Set(sameDoc(showing, ref) ? warnings.filter((w) => w.tooLong).map((w) => w.block) : []);
|
|
41
|
+
// Read-only (M4: a lock, held edits, signed out): rows still open their blocks, nothing moves or goes.
|
|
42
|
+
const readOnly = useStore(editor.access, (a) => a.readOnly);
|
|
43
|
+
const documents = useStore(editor.documents);
|
|
44
|
+
const [menu, setMenu] = useState<string | null>(null);
|
|
45
|
+
const list = useRef<HTMLOListElement>(null);
|
|
46
|
+
/** A row (by key) to focus once the list has re-rendered: after a keyboard move or remove. */
|
|
47
|
+
const focusNext = useRef<string | 'add' | null>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const want = focusNext.current;
|
|
51
|
+
if (!want) return;
|
|
52
|
+
focusNext.current = null;
|
|
53
|
+
const el =
|
|
54
|
+
want === 'add'
|
|
55
|
+
? list.current?.parentElement?.querySelector<HTMLElement>('.add-row')
|
|
56
|
+
: list.current?.querySelector<HTMLElement>(`.brow[data-block-key="${want}"] .brow-main`);
|
|
57
|
+
el?.focus();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// A row hovered or focused when the list goes (the picker opens, a block opens) isn't hovered any more.
|
|
61
|
+
useEffect(() => () => setBlockHover(editor, null), [editor]);
|
|
62
|
+
|
|
63
|
+
// The row menu closes on a click outside it.
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!menu) return;
|
|
66
|
+
const onDown = (event: MouseEvent) => {
|
|
67
|
+
const target = event.target as Element | null;
|
|
68
|
+
if (target?.closest?.('.menu') || target?.closest?.('.brow-more')) return;
|
|
69
|
+
setMenu(null);
|
|
70
|
+
};
|
|
71
|
+
document.addEventListener('mousedown', onDown);
|
|
72
|
+
return () => document.removeEventListener('mousedown', onDown);
|
|
73
|
+
}, [menu]);
|
|
74
|
+
|
|
75
|
+
// A shared entry that renders (M5a Decision 8) is named by its shared block; one that's skipped stays "Unknown block".
|
|
76
|
+
const label = (b: EditorBlock) => sharedOf(editor.site, documents, b)?.label ?? blockLabel(editor.site, b);
|
|
77
|
+
|
|
78
|
+
const move = (key: string, to: number, viaKeys: boolean) => {
|
|
79
|
+
const from = editor.doc.indexOf(key);
|
|
80
|
+
const block = editor.doc.block(key);
|
|
81
|
+
if (!block || from < 0 || to < 0 || to >= blocks.length || to === from) return;
|
|
82
|
+
if (viaKeys) focusNext.current = key;
|
|
83
|
+
editor.doc.moveBlock(key, to);
|
|
84
|
+
editor.announce(`Moved ${label(block)} to ${to + 1} of ${blocks.length}`);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const remove = (key: string, viaKeys: boolean) => {
|
|
88
|
+
const index = editor.doc.indexOf(key);
|
|
89
|
+
const block = editor.doc.block(key);
|
|
90
|
+
if (!block) return;
|
|
91
|
+
if (viaKeys) focusNext.current = blocks[index + 1]?.key ?? blocks[index - 1]?.key ?? 'add';
|
|
92
|
+
editor.doc.removeBlock(key);
|
|
93
|
+
editor.announce(`Removed ${label(block)}. Undo with Command Z.`);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const onRowKey = (event: KeyboardEvent<HTMLButtonElement>, block: EditorBlock, index: number) => {
|
|
97
|
+
if (readOnly && (event.altKey || event.key === 'Backspace' || event.key === 'Delete')) return;
|
|
98
|
+
const action = rowKeyAction(event, index, blocks.length);
|
|
99
|
+
if (action?.kind === 'move') move(block.key, action.to, true);
|
|
100
|
+
else if (action?.kind === 'remove') remove(block.key, true);
|
|
101
|
+
else if (action?.kind === 'focus') list.current?.querySelector<HTMLElement>(`.brow[data-block-key="${blocks[action.to]!.key}"] .brow-main`)?.focus();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Drag (HTML drag and drop on the rows; the drop line is the mockup's): reorder.ts, shared with M5a's lists.
|
|
105
|
+
const rows = useRowDrag({
|
|
106
|
+
rowOf: (target) => target.closest?.('.brow[data-block-key]') as HTMLElement | null,
|
|
107
|
+
keyOf: (row) => row.dataset.blockKey!,
|
|
108
|
+
indexOf: (key) => editor.doc.indexOf(key),
|
|
109
|
+
onMove: (key, to) => move(key, to, false),
|
|
110
|
+
onStart: () => setMenu(null),
|
|
111
|
+
});
|
|
112
|
+
const drag = rows.drag;
|
|
113
|
+
|
|
114
|
+
const dragged = drag ? blocks.find((b) => b.key === drag.key) : undefined;
|
|
115
|
+
const target = drag?.drop ? blocks.find((b) => b.key === drag.drop!.key) : undefined;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<>
|
|
119
|
+
<ol
|
|
120
|
+
ref={list}
|
|
121
|
+
className="blist"
|
|
122
|
+
aria-label={`Blocks on ${title}`}
|
|
123
|
+
onMouseLeave={() => setBlockHover(editor, null)}
|
|
124
|
+
onDragOver={rows.over}
|
|
125
|
+
onDrop={rows.drop}
|
|
126
|
+
>
|
|
127
|
+
{blocks.map((block, i) => {
|
|
128
|
+
const name = label(block);
|
|
129
|
+
const shared = sharedOf(editor.site, documents, block);
|
|
130
|
+
const skipped = !block.known && !shared;
|
|
131
|
+
const type = shared?.blockType ?? (block.known ? block.type : null);
|
|
132
|
+
const notOffered = !!type && !offersBlock(editor.site, ref.collection, type);
|
|
133
|
+
const cls = [
|
|
134
|
+
'brow',
|
|
135
|
+
current === block.key && !drag ? 'is-current' : '',
|
|
136
|
+
skipped ? 'is-unknown' : '',
|
|
137
|
+
readOnly ? 'is-disabled' : '',
|
|
138
|
+
hovered === block.key && !drag ? 'hov' : '',
|
|
139
|
+
rows.classes(block.key),
|
|
140
|
+
]
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.join(' ');
|
|
143
|
+
return (
|
|
144
|
+
<li
|
|
145
|
+
key={block.key}
|
|
146
|
+
className={cls}
|
|
147
|
+
data-block-key={block.key}
|
|
148
|
+
data-block-type={shared?.blockType ?? block.type}
|
|
149
|
+
data-shared={shared ? shared.path : undefined}
|
|
150
|
+
draggable={!readOnly}
|
|
151
|
+
onMouseEnter={() => setBlockHover(editor, block.key)}
|
|
152
|
+
onDragStart={(e) => rows.start(e, block.key)}
|
|
153
|
+
onDragEnd={rows.cancel}
|
|
154
|
+
>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
className="brow-main"
|
|
158
|
+
aria-current={current === block.key ? 'true' : undefined}
|
|
159
|
+
aria-label={`${name}${shared ? ', shared' : ''}, ${i + 1} of ${blocks.length}${skipped ? ', skipped' : ''}. Alt and arrow keys move it`}
|
|
160
|
+
onClick={() => editor.openBlock(block.key)}
|
|
161
|
+
onKeyDown={(e) => onRowKey(e, block, i)}
|
|
162
|
+
onFocus={() => setBlockHover(editor, block.key)}
|
|
163
|
+
onBlur={() => {
|
|
164
|
+
if (blockHover(editor).get() === block.key) setBlockHover(editor, null);
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<span className="grip" aria-hidden="true">
|
|
168
|
+
{readOnly ? null : <Icon name="grip" />}
|
|
169
|
+
</span>
|
|
170
|
+
<span className="bi">
|
|
171
|
+
<Icon name={blockIcon(shared ? { type: shared.blockType!, known: true } : block)} />
|
|
172
|
+
</span>
|
|
173
|
+
<span className="bt">
|
|
174
|
+
<span className="nm">
|
|
175
|
+
{name}
|
|
176
|
+
{shared ? (
|
|
177
|
+
<span className="chip shared" data-chip="shared">
|
|
178
|
+
<Icon name="shared" size="xs" />
|
|
179
|
+
Shared
|
|
180
|
+
</span>
|
|
181
|
+
) : null}
|
|
182
|
+
{!skipped ? null : (
|
|
183
|
+
<span className="chip warn" title="Its type isn't in the site's code: kept in the file, skipped on the page">
|
|
184
|
+
<Icon name="warn" size="xs" />
|
|
185
|
+
Skipped
|
|
186
|
+
</span>
|
|
187
|
+
)}
|
|
188
|
+
{notOffered ? (
|
|
189
|
+
<span className="chip warn" title="Not offered by this collection" data-chip="not-offered">
|
|
190
|
+
<Icon name="warn" size="xs" />
|
|
191
|
+
Not offered here
|
|
192
|
+
</span>
|
|
193
|
+
) : null}
|
|
194
|
+
{tooLong.has(i) ? (
|
|
195
|
+
<span className="chip warn" title="Too long for mobile" data-chip="too-long">
|
|
196
|
+
<Icon name="phone" size="xs" />
|
|
197
|
+
Long
|
|
198
|
+
</span>
|
|
199
|
+
) : null}
|
|
200
|
+
{altGaps(editor.site, block) ? (
|
|
201
|
+
<span className="chip warn" title="An image has no alt text" data-chip="alt">
|
|
202
|
+
<Icon name="image" size="xs" />
|
|
203
|
+
Alt
|
|
204
|
+
</span>
|
|
205
|
+
) : null}
|
|
206
|
+
{hasPhoneLines(editor.site, block) ? (
|
|
207
|
+
<span className="chip phone" title="Has its own lines on Phone" data-chip="phone-lines">
|
|
208
|
+
<Icon name="phone" size="xs" />
|
|
209
|
+
</span>
|
|
210
|
+
) : null}
|
|
211
|
+
</span>
|
|
212
|
+
<span className="sm">{shared ? usedOnLine(shared) : blockSummary(editor.site, block, editor.forms)}</span>
|
|
213
|
+
</span>
|
|
214
|
+
</button>
|
|
215
|
+
{readOnly ? null : (
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
className="ib brow-more"
|
|
219
|
+
aria-label={`Actions for ${name}`}
|
|
220
|
+
aria-haspopup="menu"
|
|
221
|
+
aria-expanded={menu === block.key}
|
|
222
|
+
onClick={() => setMenu(menu === block.key ? null : block.key)}
|
|
223
|
+
>
|
|
224
|
+
<Icon name="more" />
|
|
225
|
+
</button>
|
|
226
|
+
)}
|
|
227
|
+
{menu === block.key && !readOnly ? (
|
|
228
|
+
<RowMenu
|
|
229
|
+
first={i === 0}
|
|
230
|
+
last={i === blocks.length - 1}
|
|
231
|
+
onClose={() => {
|
|
232
|
+
setMenu(null);
|
|
233
|
+
list.current?.querySelector<HTMLElement>(`.brow[data-block-key="${block.key}"] .brow-more`)?.focus();
|
|
234
|
+
}}
|
|
235
|
+
onMove={(dir) => {
|
|
236
|
+
setMenu(null);
|
|
237
|
+
move(block.key, i + dir, true);
|
|
238
|
+
}}
|
|
239
|
+
onRemove={() => {
|
|
240
|
+
setMenu(null);
|
|
241
|
+
remove(block.key, true);
|
|
242
|
+
}}
|
|
243
|
+
/>
|
|
244
|
+
) : null}
|
|
245
|
+
</li>
|
|
246
|
+
);
|
|
247
|
+
})}
|
|
248
|
+
</ol>
|
|
249
|
+
<button type="button" className="add-row" disabled={readOnly} onClick={() => openPicker(editor, blocks.length)}>
|
|
250
|
+
<Icon name="plus" />
|
|
251
|
+
Add block
|
|
252
|
+
</button>
|
|
253
|
+
{!readOnly && drag && dragged && target && target.key !== dragged.key ? (
|
|
254
|
+
<p className="kb-hint" role="status" style={{ color: 'var(--select-strong)' }}>
|
|
255
|
+
Drop to put {label(dragged)} {drag.drop!.after ? 'below' : 'above'} {label(target)}.
|
|
256
|
+
</p>
|
|
257
|
+
) : null}
|
|
258
|
+
</>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function RowMenu(props: { first: boolean; last: boolean; onClose: () => void; onMove: (dir: -1 | 1) => void; onRemove: () => void }) {
|
|
263
|
+
const menu = useRef<HTMLDivElement>(null);
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
menu.current?.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus();
|
|
266
|
+
}, []);
|
|
267
|
+
const onKey = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
268
|
+
if (event.key === 'Escape') {
|
|
269
|
+
// Closes the menu only; App.tsx would otherwise go up a level.
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
props.onClose();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
const items = [...(menu.current?.querySelectorAll<HTMLButtonElement>('button:not(:disabled)') ?? [])];
|
|
277
|
+
const at = items.indexOf(document.activeElement as HTMLButtonElement);
|
|
278
|
+
items[(at + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length]?.focus();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
return (
|
|
282
|
+
<div ref={menu} className="menu" role="menu" style={{ right: 4, top: 'calc(100% + 2px)' }} onKeyDown={onKey}>
|
|
283
|
+
<button type="button" role="menuitem" disabled={props.first} onClick={() => props.onMove(-1)}>
|
|
284
|
+
<Icon name="up" />
|
|
285
|
+
Move up
|
|
286
|
+
<span className="k">
|
|
287
|
+
<kbd>⌥</kbd>
|
|
288
|
+
<kbd>↑</kbd>
|
|
289
|
+
</span>
|
|
290
|
+
</button>
|
|
291
|
+
<button type="button" role="menuitem" disabled={props.last} onClick={() => props.onMove(1)}>
|
|
292
|
+
<Icon name="down" />
|
|
293
|
+
Move down
|
|
294
|
+
<span className="k">
|
|
295
|
+
<kbd>⌥</kbd>
|
|
296
|
+
<kbd>↓</kbd>
|
|
297
|
+
</span>
|
|
298
|
+
</button>
|
|
299
|
+
<hr />
|
|
300
|
+
<button type="button" role="menuitem" className="danger" onClick={props.onRemove}>
|
|
301
|
+
<Icon name="trash" />
|
|
302
|
+
Remove
|
|
303
|
+
<span className="k">
|
|
304
|
+
<kbd>⌫</kbd>
|
|
305
|
+
</span>
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** A block with a lockup that has its own phone lines (Decision 10's object form), at the top level or inside a group. */
|
|
312
|
+
function hasPhoneLines(site: EditorSite, block: EditorBlock): boolean {
|
|
313
|
+
if (!block.known) return false;
|
|
314
|
+
const fields = own(site.blocks, block.type)?.fields ?? {};
|
|
315
|
+
const phoned = (field: Field, value: unknown) => {
|
|
316
|
+
const phone = field.kind === 'lockup' && !!value && typeof value === 'object' ? (value as { phone?: unknown }).phone : undefined;
|
|
317
|
+
return Array.isArray(phone) && phone.length > 0;
|
|
318
|
+
};
|
|
319
|
+
return Object.entries(fields).some(([name, field]) => {
|
|
320
|
+
const value = block.data[name];
|
|
321
|
+
if (field.kind !== 'group') return phoned(field, value);
|
|
322
|
+
const inner = value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
|
|
323
|
+
return Object.entries(field.fields).some(([key, f]) => phoned(f, inner[key]));
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* How many images in a block's rendered slots have no alt text (M5b Decision 9: the field's
|
|
329
|
+
* own `missing-alt` warning, so a decorative image never counts). The mockup's browHTML
|
|
330
|
+
* marks such a block (its "Alt" chip); it blocks nothing. An
|
|
331
|
+
* unknown block isn't rendered, and a shared entry's images are its shared block's.
|
|
332
|
+
*/
|
|
333
|
+
export function altGaps(site: EditorSite, block: EditorBlock): number {
|
|
334
|
+
if (!block.known || block.shared) return 0;
|
|
335
|
+
const fields = own(site.blocks, block.type)?.fields ?? {};
|
|
336
|
+
let n = 0;
|
|
337
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
338
|
+
if (!fieldHoldsMedia(field)) continue;
|
|
339
|
+
n += fieldWarnings(field, block.data[name]).filter((w) => w.code === 'missing-alt').length;
|
|
340
|
+
}
|
|
341
|
+
return n;
|
|
342
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The add-block picker (the mockup's #picker): on the document level, in place of
|
|
3
|
+
* the block list, while `pickerAt(editor)` holds an index (link.ts → openPicker).
|
|
4
|
+
* The site's block types, each with its fields; picking one adds an empty block of
|
|
5
|
+
* that type at the index and opens it. Back, or Esc, returns to the list.
|
|
6
|
+
*
|
|
7
|
+
* M5a: the Shared blocks section inserts a shared block's entry (`{ "shared": name }`),
|
|
8
|
+
* not a copy (Decision 8).
|
|
9
|
+
*/
|
|
10
|
+
import { own } from '../../../document.ts';
|
|
11
|
+
import { useEffect, useRef } from 'react';
|
|
12
|
+
import { useEditor, useStore } from '../context.tsx';
|
|
13
|
+
import { Icon } from '../shell/Icons.tsx';
|
|
14
|
+
import { blockLabel } from '../state/document-store.ts';
|
|
15
|
+
import { offeredBlocks, offersBlock } from '../state/schema.ts';
|
|
16
|
+
import { CurrentHead, DocumentsTab, GlobalNotes, LevelTab, RecentStrip, Trail } from '../pane/parts.tsx';
|
|
17
|
+
import { closePicker } from './link.ts';
|
|
18
|
+
import { blockIcon, fieldsSummary, sharedOf, usedOnLine } from './summary.ts';
|
|
19
|
+
|
|
20
|
+
export function Picker({ at }: { at: number }) {
|
|
21
|
+
const editor = useEditor();
|
|
22
|
+
const blocks = useStore(editor.doc.state, (s) => s.document.blocks);
|
|
23
|
+
const title = useStore(editor.doc.state, (s) => s.document.title || s.entry.label);
|
|
24
|
+
const documents = useStore(editor.documents);
|
|
25
|
+
const collection = useStore(editor.doc.state, (s) => s.ref.collection);
|
|
26
|
+
const body = useRef<HTMLDivElement>(null);
|
|
27
|
+
const before = blocks[at - 1];
|
|
28
|
+
const n = blocks.length;
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
body.current?.querySelector<HTMLElement>('.ptype')?.focus();
|
|
32
|
+
// Esc closes the picker, before App.tsx's Esc would go up a level.
|
|
33
|
+
const onKey = (event: KeyboardEvent) => {
|
|
34
|
+
if (event.key !== 'Escape' || event.defaultPrevented || editor.view.get().pop) return;
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
closePicker(editor);
|
|
37
|
+
};
|
|
38
|
+
window.addEventListener('keydown', onKey, true);
|
|
39
|
+
return () => window.removeEventListener('keydown', onKey, true);
|
|
40
|
+
}, [editor]);
|
|
41
|
+
|
|
42
|
+
// The collection's offered blocks (config.ts `blocks`), else every block.
|
|
43
|
+
const offered = offeredBlocks(editor.site, collection);
|
|
44
|
+
const types = offered ? offered.map((type) => own(editor.site.blocks, type)).filter((b) => !!b) : Object.values(editor.site.blocks);
|
|
45
|
+
// M5a Decision 8: the shared blocks that render (their type is one the site knows), of a type the collection offers.
|
|
46
|
+
const shared = documents.filter((d) => d.collection === 'shared' && !d.warning && !!d.blockType && !!own(editor.site.blocks, d.blockType) && offersBlock(editor.site, collection, d.blockType));
|
|
47
|
+
const pickShared = (name: string, label: string) => {
|
|
48
|
+
const key = editor.doc.addShared(name, at);
|
|
49
|
+
closePicker(editor);
|
|
50
|
+
editor.openBlock(key);
|
|
51
|
+
editor.announce(`Added ${label}, a shared block`);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const pick = (type: string) => {
|
|
55
|
+
const key = editor.doc.addBlock(type, at);
|
|
56
|
+
closePicker(editor);
|
|
57
|
+
editor.openBlock(key);
|
|
58
|
+
editor.announce(`Added ${own(editor.site.blocks, type)?.label ?? type}`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<Trail>
|
|
64
|
+
<DocumentsTab />
|
|
65
|
+
<LevelTab name={title} kind="Blocks" count={`${n} block${n === 1 ? '' : 's'}`} onClick={() => closePicker(editor)} />
|
|
66
|
+
<CurrentHead
|
|
67
|
+
back={{ label: `Back to ${title}`, onClick: () => closePicker(editor) }}
|
|
68
|
+
crumb={title}
|
|
69
|
+
title="Add a block"
|
|
70
|
+
sub={before ? `After ${sharedOf(editor.site, documents, before)?.label ?? blockLabel(editor.site, before)}` : n ? 'At the top of the page' : 'The first block on the page'}
|
|
71
|
+
/>
|
|
72
|
+
</Trail>
|
|
73
|
+
<div className="side-body" ref={body} data-picker-at={at}>
|
|
74
|
+
<GlobalNotes />
|
|
75
|
+
<p className="sec-h">Blocks</p>
|
|
76
|
+
<div className="picker-grid">
|
|
77
|
+
{types.map((schema) => (
|
|
78
|
+
<button key={schema.type} type="button" className="ptype" data-type={schema.type} onClick={() => pick(schema.type)}>
|
|
79
|
+
<span className="sk">
|
|
80
|
+
<Icon name={blockIcon({ type: schema.type, known: true })} />
|
|
81
|
+
</span>
|
|
82
|
+
<b>{schema.label}</b>
|
|
83
|
+
<small>{fieldsSummary(schema)}</small>
|
|
84
|
+
</button>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
{shared.length ? (
|
|
88
|
+
<>
|
|
89
|
+
<p className="sec-h" data-section="shared-blocks">
|
|
90
|
+
Shared blocks
|
|
91
|
+
</p>
|
|
92
|
+
{shared.map((d) => (
|
|
93
|
+
<button key={d.path} type="button" className="drow" data-shared={d.path} onClick={() => pickShared(d.path, d.label)}>
|
|
94
|
+
<Icon name="shared" />
|
|
95
|
+
<span className="nm">{d.label}</span>
|
|
96
|
+
<span className="meta">{usedOnLine(d)}</span>
|
|
97
|
+
</button>
|
|
98
|
+
))}
|
|
99
|
+
</>
|
|
100
|
+
) : null}
|
|
101
|
+
</div>
|
|
102
|
+
<RecentStrip />
|
|
103
|
+
</>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An unknown block's panel (the mockup's #unknown, its unknownPanel; Decisions 7
|
|
3
|
+
* and 21), for the block level to show in place of fields when the open block's
|
|
4
|
+
* type isn't in the site's config:
|
|
5
|
+
*
|
|
6
|
+
* {block.known ? <fields…/> : <UnknownBlockPanel block={block} />}
|
|
7
|
+
*
|
|
8
|
+
* It says the block is kept in the file and skipped on the page, lists what's kept
|
|
9
|
+
* (its top-level keys, each with a short preview of its value), and offers Remove,
|
|
10
|
+
* with Undo. It can't be edited.
|
|
11
|
+
*/
|
|
12
|
+
import { useEditor, useStore } from '../context.tsx';
|
|
13
|
+
import { Icon } from '../shell/Icons.tsx';
|
|
14
|
+
import type { EditorBlock } from '../../types.ts';
|
|
15
|
+
import { keptSummary, sharedOf } from './summary.ts';
|
|
16
|
+
import { sharedEntryOf } from '../../../document.ts';
|
|
17
|
+
|
|
18
|
+
export function UnknownBlockPanel({ block }: { block: EditorBlock }) {
|
|
19
|
+
const editor = useEditor();
|
|
20
|
+
const readOnly = useStore(editor.access, (a) => a.readOnly);
|
|
21
|
+
const documents = useStore(editor.documents);
|
|
22
|
+
const kept = keptSummary(block.data);
|
|
23
|
+
const remove = () => {
|
|
24
|
+
editor.doc.removeBlock(block.key);
|
|
25
|
+
editor.goTo('document');
|
|
26
|
+
editor.announce('Removed Unknown block. Undo with Command Z.');
|
|
27
|
+
};
|
|
28
|
+
const removeEntry = () => {
|
|
29
|
+
editor.doc.removeBlock(block.key, { label: `Removed ${sharedOf(editor.site, documents, block)?.label ?? 'shared block'}` });
|
|
30
|
+
editor.goTo('document');
|
|
31
|
+
editor.announce('Removed the shared block from this page. Undo with Command Z.');
|
|
32
|
+
};
|
|
33
|
+
// A shared-block entry (M5a Decision 8): selected, moved and removed like a block, never
|
|
34
|
+
// edited here. One that renders offers its own document (the mockup's #shared panel).
|
|
35
|
+
const entry = sharedEntryOf(block);
|
|
36
|
+
const shared = entry !== undefined ? sharedOf(editor.site, documents, block) : null;
|
|
37
|
+
if (shared) {
|
|
38
|
+
const used = (shared.usedOn ?? []).map((u) => documents.find((d) => d.collection === u.collection && d.path === u.path)?.label ?? u.path);
|
|
39
|
+
const n = used.length;
|
|
40
|
+
return (
|
|
41
|
+
<div className="panel" data-shared-entry={entry} data-shared-panel="">
|
|
42
|
+
<h3>{shared.label} is a shared block</h3>
|
|
43
|
+
<p>Edit it in Shared blocks; it's on every page that uses it.</p>
|
|
44
|
+
<div className="used">
|
|
45
|
+
{used.map((t, i) => (
|
|
46
|
+
<span key={`${i}:${t}`} className="chip shared">
|
|
47
|
+
{t}
|
|
48
|
+
</span>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
<div className="acts">
|
|
52
|
+
<button type="button" className="btn ink" data-act="edit-shared" onClick={() => void editor.openDocument({ collection: 'shared', path: shared.path })}>
|
|
53
|
+
<Icon name="shared" size="sm" />
|
|
54
|
+
Edit {shared.label} (used on {n} page{n === 1 ? '' : 's'})
|
|
55
|
+
</button>
|
|
56
|
+
<button type="button" className="btn" disabled={readOnly} onClick={removeEntry}>
|
|
57
|
+
Remove from this page
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (entry !== undefined) {
|
|
64
|
+
// Skipped on the page (Phase 2's warning): no such shared block, or a type the code doesn't know.
|
|
65
|
+
const listedShared = documents.find((d) => d.collection === 'shared' && d.path === entry && !d.warning);
|
|
66
|
+
const name = listedShared?.label ?? entry;
|
|
67
|
+
return (
|
|
68
|
+
<div data-unknown-block={block.key} data-shared-entry={entry}>
|
|
69
|
+
<div className="note warn">
|
|
70
|
+
<Icon name="warn" />
|
|
71
|
+
<div>
|
|
72
|
+
<b>{!listedShared ? `There's no shared block "${entry}"` : `${name}'s type isn't in the site's code any more`}</b>
|
|
73
|
+
<p>Skipped on the page; kept in the file.</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="panel">
|
|
77
|
+
<div className="acts">
|
|
78
|
+
<button type="button" className="btn" disabled={readOnly} onClick={remove}>
|
|
79
|
+
Remove from this page
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return (
|
|
87
|
+
<div data-unknown-block={block.key}>
|
|
88
|
+
<div className="note warn">
|
|
89
|
+
<Icon name="warn" />
|
|
90
|
+
<div>
|
|
91
|
+
<b>This block's type isn't in the site's code any more</b>
|
|
92
|
+
<p>
|
|
93
|
+
<span className="mono">{block.type || 'No type'}</span> is skipped on the page; kept in the file.
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="panel">
|
|
98
|
+
<h3>What's kept</h3>
|
|
99
|
+
{kept.length ? (
|
|
100
|
+
<dl className="kept">
|
|
101
|
+
{kept.map((k) => (
|
|
102
|
+
<div key={k.key}>
|
|
103
|
+
<dt>{k.label}</dt>
|
|
104
|
+
<dd>{k.preview}</dd>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
</dl>
|
|
108
|
+
) : (
|
|
109
|
+
<p>Nothing but its type.</p>
|
|
110
|
+
)}
|
|
111
|
+
<div className="acts">
|
|
112
|
+
<button type="button" className="btn" disabled={readOnly} onClick={remove}>
|
|
113
|
+
Remove block
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The empty page's invitation in the preview (the mockup's renderPreview:
|
|
3
|
+
* `.cms-empty .cms-add`), painted into Phase 3's overlay layer (client/overlays): a
|
|
4
|
+
* dashed area where its blocks would go (Blocks.astro's preview-only
|
|
5
|
+
* `data-cms-empty`) with "+ Add the first block", in every frame. A click opens the
|
|
6
|
+
* add-block picker at 0 (link.ts → openPicker).
|
|
7
|
+
*
|
|
8
|
+
* (The "+ Add block" points on a selected block's edges went in the pre-M7 polish:
|
|
9
|
+
* the sidebar's Add block row is the one way to add after the first.) Nothing is
|
|
10
|
+
* drawn while the preview shows another document than the sidebar (one is opening).
|
|
11
|
+
*
|
|
12
|
+
* startEmptyPageAdd(editor) // a stop; DocumentsLevel mounts it once (it's always mounted)
|
|
13
|
+
*/
|
|
14
|
+
import type { Editor } from '../editor.ts';
|
|
15
|
+
import { overlaysFor } from '../overlays/index.ts';
|
|
16
|
+
import { sameDoc } from '../../types.ts';
|
|
17
|
+
import { openPicker } from './link.ts';
|
|
18
|
+
|
|
19
|
+
/** Whether the preview offers "+ Add the first block" now (pure apart from reading stores). */
|
|
20
|
+
export function offersFirstBlock(editor: Pick<Editor, 'doc' | 'previewStatus'>): boolean {
|
|
21
|
+
const status = editor.previewStatus.get();
|
|
22
|
+
const session = editor.doc.get();
|
|
23
|
+
if (status.opening || !sameDoc(status.showing, session.ref)) return false;
|
|
24
|
+
// Only a section builder has blocks (M5a: not a form or a shared block).
|
|
25
|
+
return session.document.kind === 'sections' && session.document.blocks.length === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function startEmptyPageAdd(editor: Editor): () => void {
|
|
29
|
+
const overlays = overlaysFor(editor);
|
|
30
|
+
if (!overlays) return () => {};
|
|
31
|
+
|
|
32
|
+
const remove = overlays.addPainter((ctx) => {
|
|
33
|
+
if (!offersFirstBlock(editor)) return null;
|
|
34
|
+
const area = ctx.doc.querySelector('[data-cms-empty]');
|
|
35
|
+
if (!area) return null;
|
|
36
|
+
const box = ctx.el('div', 'cms-empty');
|
|
37
|
+
box.setAttribute('style', ctx.place(ctx.rect(area, -24)));
|
|
38
|
+
const b = ctx.el('button', 'cms-add');
|
|
39
|
+
b.type = 'button';
|
|
40
|
+
b.dataset.ins = '0';
|
|
41
|
+
b.textContent = '+ Add the first block';
|
|
42
|
+
// The page's own handlers (click to edit) ignore the layer; keep the press here anyway.
|
|
43
|
+
b.addEventListener('mousedown', (event) => event.stopPropagation());
|
|
44
|
+
b.addEventListener('click', (event) => {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
openPicker(editor, 0);
|
|
48
|
+
});
|
|
49
|
+
box.appendChild(b);
|
|
50
|
+
return box;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const redraw = () => overlays.redraw();
|
|
54
|
+
const stops = [remove, editor.doc.state.subscribe(redraw), editor.previewStatus.subscribe(redraw)];
|
|
55
|
+
return () => stops.forEach((stop) => stop());
|
|
56
|
+
}
|