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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drill-down pane (M2 → the drill-down sidebar): documents → a document → a
|
|
3
|
+
* block, with a collection's own level beside documents (Decision 9). The levels you
|
|
4
|
+
* came from stack above as tabs; back and Esc go up a level (App.tsx handles Esc).
|
|
5
|
+
*
|
|
6
|
+
* Each level is its own file under ./levels, rendered inside a `.side-level` (a
|
|
7
|
+
* flex column, so a level's .trail / .side-body / .side-foot sit as in the mockup), so Phase 3 (the block level) and
|
|
8
|
+
* Phase 4 (the documents, collection and document levels) can each replace theirs
|
|
9
|
+
* without touching this frame. LEVELS maps a level to its component.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useRef, type ComponentType } from 'react';
|
|
12
|
+
import { useEditor, useStore } from '../context.tsx';
|
|
13
|
+
import type { PaneLevel } from '../editor.ts';
|
|
14
|
+
import { BlockLevel } from './levels/BlockLevel.tsx';
|
|
15
|
+
import { CollectionLevel } from './levels/CollectionLevel.tsx';
|
|
16
|
+
import { DocumentLevel } from './levels/DocumentLevel.tsx';
|
|
17
|
+
import { DocumentsLevel } from './levels/DocumentsLevel.tsx';
|
|
18
|
+
import { MediaLevel } from './levels/MediaLevel.tsx';
|
|
19
|
+
import { SubmissionsLevel } from './levels/SubmissionsLevel.tsx';
|
|
20
|
+
|
|
21
|
+
export const LEVELS: Record<PaneLevel, ComponentType> = {
|
|
22
|
+
documents: DocumentsLevel,
|
|
23
|
+
collection: CollectionLevel,
|
|
24
|
+
document: DocumentLevel,
|
|
25
|
+
block: BlockLevel,
|
|
26
|
+
// M5b: the library (Site-wide → Media), and a slot's picker in place of the block's fields.
|
|
27
|
+
media: MediaLevel,
|
|
28
|
+
// M6: the inbox (Site-wide → Submissions).
|
|
29
|
+
submissions: SubmissionsLevel,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Marks a `.side-body` as scrolling for a moment after each scroll, so its thin
|
|
34
|
+
* scrollbar shows while it moves as well as on hover (editor.css). Scroll events
|
|
35
|
+
* don't bubble, so the pane listens in the capture phase.
|
|
36
|
+
*/
|
|
37
|
+
function useScrollingMark(ref: { current: HTMLElement | null }) {
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const root = ref.current;
|
|
40
|
+
if (!root) return;
|
|
41
|
+
const timers = new Map<Element, ReturnType<typeof setTimeout>>();
|
|
42
|
+
const onScroll = (event: Event) => {
|
|
43
|
+
const el = event.target;
|
|
44
|
+
if (!(el instanceof HTMLElement) || !el.classList.contains('side-body')) return;
|
|
45
|
+
el.dataset.scrolling = '';
|
|
46
|
+
clearTimeout(timers.get(el));
|
|
47
|
+
timers.set(
|
|
48
|
+
el,
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
delete el.dataset.scrolling;
|
|
51
|
+
timers.delete(el);
|
|
52
|
+
}, 800),
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
root.addEventListener('scroll', onScroll, { capture: true, passive: true });
|
|
56
|
+
return () => {
|
|
57
|
+
root.removeEventListener('scroll', onScroll, { capture: true });
|
|
58
|
+
timers.forEach((t) => clearTimeout(t));
|
|
59
|
+
};
|
|
60
|
+
}, [ref]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function Pane() {
|
|
64
|
+
const editor = useEditor();
|
|
65
|
+
const side = useRef<HTMLElement>(null);
|
|
66
|
+
useScrollingMark(side);
|
|
67
|
+
const level = useStore(editor.view, (v) => v.level);
|
|
68
|
+
const blockKey = useStore(editor.view, (v) => v.block);
|
|
69
|
+
const hasBlock = useStore(editor.doc.state, (s) => (blockKey ? s.document.blocks.some((b) => b.key === blockKey) : false));
|
|
70
|
+
// A form (M5a) has no block level: its fields are on its document level.
|
|
71
|
+
const form = useStore(editor.doc.state, (s) => s.document.kind !== 'sections');
|
|
72
|
+
// A block that's gone (removed, or another document opened) falls back to its document.
|
|
73
|
+
const shown: PaneLevel = level === 'block' && (!hasBlock || form) ? 'document' : level;
|
|
74
|
+
const Level = LEVELS[shown];
|
|
75
|
+
const Documents = LEVELS.documents;
|
|
76
|
+
// The documents level stays mounted (hidden when it isn't shown): going up is instant,
|
|
77
|
+
// it keeps its own state (a filter, scroll), and its Pages groups are in the server's
|
|
78
|
+
// HTML as the cookie says (Decision 20; Verify 1).
|
|
79
|
+
return (
|
|
80
|
+
<aside ref={side} className="side" id="side" aria-label="Documents, blocks and fields" data-level={shown}>
|
|
81
|
+
<div className="side-level" data-pane-level="documents" hidden={shown !== 'documents'}>
|
|
82
|
+
<Documents />
|
|
83
|
+
</div>
|
|
84
|
+
{shown !== 'documents' ? (
|
|
85
|
+
<div className="side-level" data-pane-level={shown}>
|
|
86
|
+
<Level />
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
89
|
+
</aside>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The block level (Phase 3; the mockup's blockView): one block's fields, with the
|
|
3
|
+
* levels you came from stacked above, and back. (The "1 of 5" stepper went in the
|
|
4
|
+
* pre-M7 polish: the block list and the preview are how you move between blocks.)
|
|
5
|
+
*
|
|
6
|
+
* The fields (Decision 12), each in a field frame (fields/FieldFrame.tsx): text,
|
|
7
|
+
* one line or several (also typed on the page), the lockup (main lines, phone
|
|
8
|
+
* lines, the warnings; edited here only) and select; and M5a's (Decision 2): rich
|
|
9
|
+
* text, link, buttons, toggle, number, date, list and reference, all edited here
|
|
10
|
+
* (M5a Decision 13). They're mounted through a field host (fields/host.tsx), the
|
|
11
|
+
* same way a fields document's form mounts them. On Phone, a block with a lockup
|
|
12
|
+
* says that changes show at every width, except a heading's phone lines.
|
|
13
|
+
*
|
|
14
|
+
* An unknown block (Decision 21) shows Phase 4's panel: what's kept, and Remove.
|
|
15
|
+
*
|
|
16
|
+
* A field is changed with `editor.doc.setField(key, field, value)`; the preview
|
|
17
|
+
* re-renders and the keeper writes it (document-store.ts).
|
|
18
|
+
*/
|
|
19
|
+
import { useMemo, type ReactNode } from 'react';
|
|
20
|
+
import { own } from '../../../../document.ts';
|
|
21
|
+
import { titleCase } from '../../../../fields.ts';
|
|
22
|
+
import { UnknownBlockPanel } from '../../block-list/UnknownBlock.tsx';
|
|
23
|
+
import { sharedOf } from '../../block-list/summary.ts';
|
|
24
|
+
import { useEditor, useStore } from '../../context.tsx';
|
|
25
|
+
import { FieldControl } from '../../fields/FieldControl.tsx';
|
|
26
|
+
import { blockHost, FieldHostProvider, rawBlock } from '../../fields/host.tsx';
|
|
27
|
+
import { Icon } from '../../shell/Icons.tsx';
|
|
28
|
+
import { blockLabel } from '../../state/document-store.ts';
|
|
29
|
+
import { CurrentHead, DocumentsTab, GlobalNotes, LevelTab, RecentStrip, Trail } from '../parts.tsx';
|
|
30
|
+
import { DocNotices, ReadOnlyChip } from '../../live/notices.tsx';
|
|
31
|
+
|
|
32
|
+
export function BlockLevel() {
|
|
33
|
+
const editor = useEditor();
|
|
34
|
+
const session = useStore(editor.doc.state);
|
|
35
|
+
const key = useStore(editor.view, (v) => v.block);
|
|
36
|
+
const width = useStore(editor.view, (v) => v.width);
|
|
37
|
+
// Under a lock, with held edits, or signed out: every field disabled (M4 Decision 9; the mockup's #locked-block).
|
|
38
|
+
const readOnly = useStore(editor.access, (a) => a.readOnly);
|
|
39
|
+
const documents = useStore(editor.documents);
|
|
40
|
+
const { document, entry } = session;
|
|
41
|
+
const index = document.blocks.findIndex((b) => b.key === key);
|
|
42
|
+
const block = document.blocks[index];
|
|
43
|
+
const raw = block ? rawBlock(session.base, block.source) : undefined;
|
|
44
|
+
const host = useMemo(() => (key ? blockHost(editor, key, raw, readOnly) : null), [editor, key, raw, readOnly]);
|
|
45
|
+
if (!block || !host) return null;
|
|
46
|
+
const n = document.blocks.length;
|
|
47
|
+
const title = document.title || entry.label;
|
|
48
|
+
// A shared entry that renders (M5a Decision 8) is named by its shared block, and marked Shared.
|
|
49
|
+
const shared = sharedOf(editor.site, documents, block);
|
|
50
|
+
const nameOf = (b: typeof block) => sharedOf(editor.site, documents, b)?.label ?? blockLabel(editor.site, b);
|
|
51
|
+
const fields = block.known ? Object.entries(own(editor.site.blocks, block.type)?.fields ?? {}) : [];
|
|
52
|
+
// A lockup at the top level or inside a group (`left.heading`).
|
|
53
|
+
const hasLockup = fields.some(([, f]) => f.kind === 'lockup' || (f.kind === 'group' && Object.values(f.fields).some((g) => g.kind === 'lockup')));
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<Trail>
|
|
57
|
+
<DocumentsTab />
|
|
58
|
+
<LevelTab name={title} kind="Blocks" count={`${n} block${n === 1 ? '' : 's'}`} onClick={() => editor.goTo('document')} />
|
|
59
|
+
<CurrentHead
|
|
60
|
+
back={{ label: `Back to ${title}`, onClick: () => editor.goTo('document') }}
|
|
61
|
+
crumb={title}
|
|
62
|
+
title={nameOf(block)}
|
|
63
|
+
chips={
|
|
64
|
+
<>
|
|
65
|
+
{shared ? <span className="chip shared">Shared</span> : null}
|
|
66
|
+
<ReadOnlyChip />
|
|
67
|
+
</>
|
|
68
|
+
}
|
|
69
|
+
/>
|
|
70
|
+
</Trail>
|
|
71
|
+
<div className="side-body block-level" data-block-key={block.key}>
|
|
72
|
+
<GlobalNotes />
|
|
73
|
+
<DocNotices />
|
|
74
|
+
{width === 'phone' && hasLockup && !readOnly ? (
|
|
75
|
+
<div className="note phone">
|
|
76
|
+
<Icon name="phone" />
|
|
77
|
+
<div>
|
|
78
|
+
<p>Changes show at every width, except a heading's phone lines.</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
) : null}
|
|
82
|
+
{!block.known ? (
|
|
83
|
+
<UnknownBlockPanel block={block} />
|
|
84
|
+
) : (
|
|
85
|
+
<Fields live={editor.mode === 'live'} readOnly={readOnly}>
|
|
86
|
+
{/* M6c (G8): the block's own help, at the top of its level. */}
|
|
87
|
+
<BlockHelp help={own(editor.site.blocks, block.type)?.help} />
|
|
88
|
+
{fields.length === 0 ? <p className="field-help">This block has nothing to set.</p> : null}
|
|
89
|
+
<FieldHostProvider host={host}>
|
|
90
|
+
{fields.map(([name, field]) => (
|
|
91
|
+
<FieldControl key={`${block.key}:${name}`} name={name} label={field.label ?? titleCase(name)} field={field} value={block.data[name]} lockupIndex={index} />
|
|
92
|
+
))}
|
|
93
|
+
</FieldHostProvider>
|
|
94
|
+
</Fields>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
<RecentStrip />
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** M6c (G8): a block's help (its config's `help`), plain text at the top of its sidebar level. */
|
|
103
|
+
export function BlockHelp({ help }: { help: string | undefined }) {
|
|
104
|
+
return help ? (
|
|
105
|
+
<p className="field-help block-help" data-block-help="">
|
|
106
|
+
{help}
|
|
107
|
+
</p>
|
|
108
|
+
) : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The fields' wrapper. Live mode (M4 Decision 9): a fieldset with no box of its own,
|
|
113
|
+
* disabled while the document is read-only, so every control in it is. Local mode: no
|
|
114
|
+
* wrapper, as in M3.
|
|
115
|
+
*/
|
|
116
|
+
function Fields({ live, readOnly, children }: { live: boolean; readOnly: boolean; children: ReactNode }) {
|
|
117
|
+
if (!live) return <>{children}</>;
|
|
118
|
+
return (
|
|
119
|
+
<fieldset className="ro-fields" disabled={readOnly} data-read-only={readOnly ? '' : undefined}>
|
|
120
|
+
{children}
|
|
121
|
+
</fieldset>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A collection's own level (Decision 9; the mockup's #collection and
|
|
3
|
+
* #collection-empty): the documents level stacked above, a header with the
|
|
4
|
+
* collection's name, count and **+ New**, its own filter, and the list. An empty
|
|
5
|
+
* collection's empty state lives here.
|
|
6
|
+
*
|
|
7
|
+
* Nesting is Pages only: a collection here is a flat list, alphabetical by title
|
|
8
|
+
* (the reader's order). **+ New** (M5a Decision 11) opens the New dialog
|
|
9
|
+
* (documents/dialogs.tsx). Shared blocks (M5a; the mockup's #shared-list) have their
|
|
10
|
+
* own level here too: each row says where it's used, and + New asks for a block type
|
|
11
|
+
* and a name.
|
|
12
|
+
*
|
|
13
|
+
* Pre-M7 polish: the stage shows the same collection as a table (collections/
|
|
14
|
+
* CollectionTable.tsx). The list here is one view with it: the same find (either box),
|
|
15
|
+
* the table's filters and its order.
|
|
16
|
+
*/
|
|
17
|
+
import { useEditor, useStore } from '../../context.tsx';
|
|
18
|
+
import { Icon } from '../../shell/Icons.tsx';
|
|
19
|
+
import { DocRow } from '../../nav/DocRow.tsx';
|
|
20
|
+
import { tableColumns, viewRows } from '../../collections/table.ts';
|
|
21
|
+
import { useRefIndex, useTableView } from '../../collections/CollectionTable.tsx';
|
|
22
|
+
import { CurrentHead, DocumentsTab, GlobalNotes, RecentStrip, Trail } from '../parts.tsx';
|
|
23
|
+
import { usedOnLine } from '../../block-list/summary.ts';
|
|
24
|
+
import { PAGELESS_SUB, singular } from '../../documents/actions.ts';
|
|
25
|
+
import { openDialog } from '../../documents/dialogs.tsx';
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export function CollectionLevel() {
|
|
29
|
+
const editor = useEditor();
|
|
30
|
+
const name = useStore(editor.view, (v) => v.collection);
|
|
31
|
+
const documents = useStore(editor.documents);
|
|
32
|
+
// One view with the stage's table (pre-M7 polish): the same find, filters and order.
|
|
33
|
+
const [view, update] = useTableView(name ?? '');
|
|
34
|
+
const refs = useRefIndex();
|
|
35
|
+
const query = view.query;
|
|
36
|
+
const setQuery = (q: string) => update({ query: q });
|
|
37
|
+
const signedOut = useStore(editor.access, (a) => a.signedOut);
|
|
38
|
+
const collection = editor.site.collections.find((c) => c.name === name);
|
|
39
|
+
if (!collection) return null;
|
|
40
|
+
const docs = documents.filter((d) => d.collection === collection.name);
|
|
41
|
+
const shown = viewRows(docs, tableColumns(collection), view, refs);
|
|
42
|
+
const what = collection.label;
|
|
43
|
+
const shared = collection.kind === 'shared';
|
|
44
|
+
const one = singular(collection);
|
|
45
|
+
const readOnly = !!signedOut;
|
|
46
|
+
const create = () => openDialog(editor, shared ? { kind: 'new-shared' } : { kind: 'new', collection: collection.name });
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<Trail>
|
|
50
|
+
<DocumentsTab />
|
|
51
|
+
<CurrentHead
|
|
52
|
+
back={{ label: 'Back to documents', onClick: () => editor.goTo('documents') }}
|
|
53
|
+
title={collection.label}
|
|
54
|
+
chips={<span className="n-chip">{docs.length}</span>}
|
|
55
|
+
sub={shared ? undefined : collection.urlBase === null ? PAGELESS_SUB : <span className="mono">{`${editor.site.host}${collection.urlBase}…`}</span>}
|
|
56
|
+
menu={
|
|
57
|
+
<button type="button" className="btn sm" data-act="new-document" disabled={readOnly} onClick={create}>
|
|
58
|
+
<Icon name="plus" size="sm" />
|
|
59
|
+
New
|
|
60
|
+
</button>
|
|
61
|
+
}
|
|
62
|
+
/>
|
|
63
|
+
</Trail>
|
|
64
|
+
<div className="side-body" data-collection={collection.name}>
|
|
65
|
+
<GlobalNotes />
|
|
66
|
+
{docs.length === 0 ? (
|
|
67
|
+
<div className="empty" data-empty="collection">
|
|
68
|
+
<b>{shared ? 'No shared blocks yet' : `Nothing in ${what} yet`}</b>
|
|
69
|
+
{!shared && collection.urlBase === null ? <span>No pages of their own.</span> : null}
|
|
70
|
+
<button type="button" className="btn sm" disabled={readOnly} onClick={create}>
|
|
71
|
+
<Icon name="plus" size="sm" />
|
|
72
|
+
{shared ? 'New shared block' : `New ${one}`}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
) : (
|
|
76
|
+
<>
|
|
77
|
+
<div className="find" style={{ marginTop: 0 }}>
|
|
78
|
+
<Icon name="search" />
|
|
79
|
+
<input
|
|
80
|
+
className="input"
|
|
81
|
+
type="search"
|
|
82
|
+
placeholder={shared ? 'Find a shared block' : `Find in ${what}`}
|
|
83
|
+
aria-label={shared ? 'Find a shared block' : `Find in ${what}`}
|
|
84
|
+
data-filter="collection"
|
|
85
|
+
autoComplete="off"
|
|
86
|
+
spellCheck={false}
|
|
87
|
+
value={query}
|
|
88
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
89
|
+
onKeyDown={(e) => {
|
|
90
|
+
if (e.key === 'Enter') {
|
|
91
|
+
const first = shown.find((d) => !d.warning);
|
|
92
|
+
if (first) void editor.openDocument({ collection: first.collection, path: first.path });
|
|
93
|
+
} else if (e.key === 'Escape' && query) {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
setQuery('');
|
|
96
|
+
}
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
<div id="coll-list">
|
|
101
|
+
{shown.length ? (
|
|
102
|
+
shown.map((d) => <DocRow key={d.path} doc={d} extra={shared ? <span>{usedOnLine(d)}</span> : null} />)
|
|
103
|
+
) : (
|
|
104
|
+
<p className="no-hits" role="status">
|
|
105
|
+
<b>{Object.keys(view.filters).length > 0 ? `Nothing in ${what} matches the table's filters` : `Nothing in ${what} called “${query.trim()}”`}</b>
|
|
106
|
+
</p>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
<RecentStrip />
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The document level: the open document's blocks (the mockup's #blocks, #picker,
|
|
3
|
+
* #removed, #drag, #unknown, #empty-page). The block list is client/block-list/
|
|
4
|
+
* BlockList.tsx; the add-block picker (Picker.tsx) takes this level's place while
|
|
5
|
+
* it's open; an empty page invites adding the first block.
|
|
6
|
+
*
|
|
7
|
+
* A nested page shows its parent in front of its title ("Services / Branding").
|
|
8
|
+
* The page menu: View on the live site, Delete page… (documents/DocMenu.tsx; M5a).
|
|
9
|
+
*
|
|
10
|
+
* M5a: a list-of-fields document, a shared block and Site Settings are forms, each
|
|
11
|
+
* with its own level (FormLevel.tsx); this picks by the document's kind.
|
|
12
|
+
*/
|
|
13
|
+
import { useEditor, useStore } from '../../context.tsx';
|
|
14
|
+
import { Icon } from '../../shell/Icons.tsx';
|
|
15
|
+
import { BlockList } from '../../block-list/BlockList.tsx';
|
|
16
|
+
import { openPicker, pickerAt } from '../../block-list/link.ts';
|
|
17
|
+
import { Picker } from '../../block-list/Picker.tsx';
|
|
18
|
+
import { CurrentHead, DocumentsTab, GlobalNotes, parentLabel, RecentStrip, Trail } from '../parts.tsx';
|
|
19
|
+
import { DocNotices, ReadOnlyChip } from '../../live/notices.tsx';
|
|
20
|
+
import { DocMenu } from '../../documents/DocMenu.tsx';
|
|
21
|
+
import { FieldsDocumentLevel, SettingsLevel, SharedDocumentLevel } from './FormLevel.tsx';
|
|
22
|
+
import { SeoGroup } from '../../fields/SeoGroup.tsx';
|
|
23
|
+
import { HiddenToggle } from '../../fields/HiddenToggle.tsx';
|
|
24
|
+
import { PageFields } from '../../fields/PageFields.tsx';
|
|
25
|
+
|
|
26
|
+
/** The open document's level, by its kind (M5a): a section builder's blocks, or a form. */
|
|
27
|
+
export function DocumentLevel() {
|
|
28
|
+
const editor = useEditor();
|
|
29
|
+
const kind = useStore(editor.doc.state, (s) => s.document.kind);
|
|
30
|
+
if (kind === 'fields') return <FieldsDocumentLevel />;
|
|
31
|
+
if (kind === 'shared') return <SharedDocumentLevel />;
|
|
32
|
+
if (kind === 'settings') return <SettingsLevel />;
|
|
33
|
+
return <SectionsLevel />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function SectionsLevel() {
|
|
37
|
+
const editor = useEditor();
|
|
38
|
+
const ref = useStore(editor.doc.state, (s) => s.ref);
|
|
39
|
+
const entry = useStore(editor.doc.state, (s) => s.entry);
|
|
40
|
+
const title = useStore(editor.doc.state, (s) => s.document.title || s.entry.label);
|
|
41
|
+
const empty = useStore(editor.doc.state, (s) => s.document.blocks.length === 0);
|
|
42
|
+
const documents = useStore(editor.documents);
|
|
43
|
+
const picking = useStore(pickerAt(editor));
|
|
44
|
+
const readOnly = useStore(editor.access, (a) => a.readOnly);
|
|
45
|
+
const waiting = useStore(editor.liveState, (s) => s.status?.waiting);
|
|
46
|
+
if (picking !== null) return <Picker at={picking} />;
|
|
47
|
+
const collection = editor.site.collections.find((c) => c.name === ref.collection);
|
|
48
|
+
const url = entry.url ?? '';
|
|
49
|
+
const isNew = (waiting ?? []).some((w) => w.collection === ref.collection && w.path === ref.path && w.change === 'new' && !w.movedFrom);
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<Trail>
|
|
53
|
+
<DocumentsTab />
|
|
54
|
+
<CurrentHead
|
|
55
|
+
back={{ label: 'Back to documents', onClick: () => editor.goTo('documents') }}
|
|
56
|
+
crumb={collection?.nested ? parentLabel(ref, documents) : collection?.label}
|
|
57
|
+
title={title}
|
|
58
|
+
chips={
|
|
59
|
+
<>
|
|
60
|
+
<span className="chip kind">{collection?.nested ? 'Page' : (collection?.label ?? ref.collection)}</span>
|
|
61
|
+
<ReadOnlyChip />
|
|
62
|
+
{isNew ? <span className="chip waiting">New</span> : null}
|
|
63
|
+
</>
|
|
64
|
+
}
|
|
65
|
+
sub={<span className="mono">{`${editor.site.host}${url === '/' ? '' : url}`}</span>}
|
|
66
|
+
menu={<DocMenu label="Page actions" />}
|
|
67
|
+
/>
|
|
68
|
+
</Trail>
|
|
69
|
+
<div className="side-body" data-document={`${ref.collection}/${ref.path}`}>
|
|
70
|
+
<GlobalNotes />
|
|
71
|
+
<DocNotices />
|
|
72
|
+
{empty ? (
|
|
73
|
+
<div className="empty" data-empty="page" style={{ padding: 'var(--space-4)' }}>
|
|
74
|
+
<b>No blocks yet</b>
|
|
75
|
+
<button type="button" className="btn ink" style={{ marginTop: 'var(--space-2)' }} disabled={readOnly} onClick={() => openPicker(editor, 0)}>
|
|
76
|
+
<Icon name="plus" size="sm" />
|
|
77
|
+
Add the first block
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
) : (
|
|
81
|
+
<BlockList />
|
|
82
|
+
)}
|
|
83
|
+
{/* M6c (G2): the page's own fields, when its collection has some, under its blocks. */}
|
|
84
|
+
<PageFields />
|
|
85
|
+
{/* M9 Decision 7: hidden from the site, or not. */}
|
|
86
|
+
<HiddenToggle />
|
|
87
|
+
{/* M6 Decision 13: the page's own Search and sharing, under its blocks. */}
|
|
88
|
+
<SeoGroup />
|
|
89
|
+
</div>
|
|
90
|
+
<RecentStrip />
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
}
|