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,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overlay layer: what the editor draws over the preview, inside each iframe
|
|
3
|
+
* (M2 Decision 6; Phase 3). One shared layer, which the editor's own marks
|
|
4
|
+
* (editor-marks.ts) and other modules (Phase 4: the block list's hover, insert
|
|
5
|
+
* points) draw into through a small API. See editor/README.md → Overlays.
|
|
6
|
+
*
|
|
7
|
+
* overlays.set(channel, marks) show these marks, replacing the channel's last ones
|
|
8
|
+
* overlays.clear(channel) show none of the channel's marks
|
|
9
|
+
* overlays.addPainter(painter) draw your own elements, positioned against a block, on every redraw
|
|
10
|
+
* overlays.redraw() draw again soon (a painter's own state changed)
|
|
11
|
+
*
|
|
12
|
+
* A child of the layer marked `data-cms-keep` (KEEP) is left where it is by every
|
|
13
|
+
* redraw, on top: the formatting bar of rich text on the page (rich-text-bar.ts).
|
|
14
|
+
*
|
|
15
|
+
* **Preview renders only.** The layer is created at run time by /admin in its own
|
|
16
|
+
* iframes; nothing of it is in any render, so the public build can't have it
|
|
17
|
+
* (Decision 17). In each frame it's one element, `#cms-overlays`, appended to
|
|
18
|
+
* `<html>` (outside `<body>`, so the driver's body morph never touches it), plus one
|
|
19
|
+
* `<style>` in `<head>` (never morphed either). It's absolutely positioned in
|
|
20
|
+
* document coordinates, so it scrolls with the page; `pointer-events: none`, except
|
|
21
|
+
* on the buttons marks and painters put there.
|
|
22
|
+
*
|
|
23
|
+
* **Constant size on screen** (M2 → Overlays): each frame is shown scaled, so the
|
|
24
|
+
* layer sets `--k` to 1 / that scale and every size in the CSS is `calc(var(--k) * Npx)`.
|
|
25
|
+
*/
|
|
26
|
+
import type { PreviewDriver, PreviewFrame } from '../preview/driver.ts';
|
|
27
|
+
import type { StageLayout } from '../preview/layout.ts';
|
|
28
|
+
import type { Store } from '../state/store.ts';
|
|
29
|
+
import type { Width } from '../../types.ts';
|
|
30
|
+
import { OVERLAY_CSS, OVERLAY_TOKENS } from './overlay-css.ts';
|
|
31
|
+
import { blockElement, blockElements, fieldElement, skippedAnchor, skippedTop } from '../preview/markers.ts';
|
|
32
|
+
|
|
33
|
+
export type MarkKind =
|
|
34
|
+
/** A block under the pointer (or its row in the block list): thin outline, a wash, a light tag. */
|
|
35
|
+
| 'hover'
|
|
36
|
+
/** The open block: 2px outline, a filled tag. */
|
|
37
|
+
| 'selected'
|
|
38
|
+
/** A field under the pointer inside the open block: a thin dashed ring. */
|
|
39
|
+
| 'field-hover'
|
|
40
|
+
/** The picked field: a 2px ring with the field's name above it. */
|
|
41
|
+
| 'picked'
|
|
42
|
+
/** The field being typed on the page: a soft ring, "Typing on the page". */
|
|
43
|
+
| 'typing'
|
|
44
|
+
/** Amber dashes with an amber chip: "Too long for mobile". */
|
|
45
|
+
| 'warn'
|
|
46
|
+
/** An unknown block, skipped on the page: an amber dashed line across the page where it would be, with a pill. */
|
|
47
|
+
| 'skipped';
|
|
48
|
+
|
|
49
|
+
export interface Mark {
|
|
50
|
+
kind: MarkKind;
|
|
51
|
+
/** The block's index in the rendered document (`data-cms-block`). */
|
|
52
|
+
block: number;
|
|
53
|
+
/** A field in it (`data-cms-field`); the whole block when absent. */
|
|
54
|
+
field?: string | null;
|
|
55
|
+
/** The tag's, chip's or pill's text. None: no tag. */
|
|
56
|
+
label?: string;
|
|
57
|
+
/** `phone`: teal, for phone lines. */
|
|
58
|
+
tone?: 'phone';
|
|
59
|
+
/** The frames to draw it in. Default: all three. */
|
|
60
|
+
frames?: readonly Width[];
|
|
61
|
+
/** Makes the mark's label a button (the skipped pill opens its block). */
|
|
62
|
+
onClick?: (event: MouseEvent) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface Rect {
|
|
66
|
+
left: number;
|
|
67
|
+
top: number;
|
|
68
|
+
width: number;
|
|
69
|
+
height: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What a painter gets, once per frame per redraw. */
|
|
73
|
+
export interface OverlayContext {
|
|
74
|
+
frame: PreviewFrame;
|
|
75
|
+
width: Width;
|
|
76
|
+
/** The frame on screen (the others are drawn too, behind it). */
|
|
77
|
+
visible: boolean;
|
|
78
|
+
/** The frame's document. Create elements with `el()`, not the editor's `document`. */
|
|
79
|
+
doc: Document;
|
|
80
|
+
/** 1 / the frame's scale: multiply a size you want on screen by this. The CSS has `--k`. */
|
|
81
|
+
k: number;
|
|
82
|
+
/** A block's element (`[data-cms-block="i"]`), or null. A skipped block has none (see `skippedTop`). */
|
|
83
|
+
block(index: number): HTMLElement | null;
|
|
84
|
+
/** Where skipped block `index` would be, in the layer's coordinates (its comment, preview/markers.ts), or null. */
|
|
85
|
+
skippedTop(index: number): number | null;
|
|
86
|
+
/** A field's element inside a block, or null. */
|
|
87
|
+
field(index: number, field: string): HTMLElement | null;
|
|
88
|
+
/** Every block element on the page, in order. */
|
|
89
|
+
blocks(): HTMLElement[];
|
|
90
|
+
/** An element's box in the layer's coordinates (the document's), grown by `pad` px on screen. */
|
|
91
|
+
rect(el: Element, pad?: number): Rect;
|
|
92
|
+
/** `left/top/width/height` as a style string for a Rect. */
|
|
93
|
+
place(rect: Rect): string;
|
|
94
|
+
/** An element in the frame's document: `el('div', 'cms-ins', { left: '0px' })`. */
|
|
95
|
+
el<K extends keyof HTMLElementTagNameMap>(tag: K, className?: string, style?: Partial<Record<string, string>>): HTMLElementTagNameMap[K];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Returns the elements to put in the layer for this frame (or nothing). Called on every redraw. */
|
|
99
|
+
export type OverlayPainter = (ctx: OverlayContext) => Node | readonly Node[] | null | undefined | void;
|
|
100
|
+
|
|
101
|
+
export interface Overlays {
|
|
102
|
+
set(channel: string, marks: readonly Mark[]): void;
|
|
103
|
+
clear(channel: string): void;
|
|
104
|
+
/** Returns a remove. Painters run after the marks, in the order added. */
|
|
105
|
+
addPainter(painter: OverlayPainter): () => void;
|
|
106
|
+
/** Redraw on the next animation frame (coalesced). */
|
|
107
|
+
redraw(): void;
|
|
108
|
+
/** Redraw now (tests). */
|
|
109
|
+
flush(): void;
|
|
110
|
+
/** The layer element in a frame, if it's mounted. */
|
|
111
|
+
layerOf(frame: PreviewFrame): HTMLElement | null;
|
|
112
|
+
/** Unmount from every frame and stop. */
|
|
113
|
+
destroy(): void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const LAYER_ID = 'cms-overlays';
|
|
117
|
+
/** A child of the layer with this attribute is left alone by every redraw (it draws itself). */
|
|
118
|
+
export const KEEP = 'data-cms-keep';
|
|
119
|
+
const STYLE_ID = 'cms-overlays-css';
|
|
120
|
+
const FONT_FAMILY = 'caracms UI';
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Mark kinds in drawing order (later ones on top), as the mockup draws them: the
|
|
124
|
+
* painters' elements (insert points, the empty page's invitation) go between the
|
|
125
|
+
* block marks (up to `selected`) and the field marks, so a picked field's ring and
|
|
126
|
+
* a warning sit over an insert point.
|
|
127
|
+
*/
|
|
128
|
+
const ORDER: MarkKind[] = ['skipped', 'hover', 'selected', 'field-hover', 'picked', 'typing', 'warn'];
|
|
129
|
+
const UNDER_PAINTERS = new Set<MarkKind>(['skipped', 'hover', 'selected']);
|
|
130
|
+
|
|
131
|
+
export function createOverlays(options: {
|
|
132
|
+
driver: PreviewDriver;
|
|
133
|
+
/** The stage's layout, for each frame's scale. */
|
|
134
|
+
layout: Store<StageLayout | null>;
|
|
135
|
+
/** The width on screen. */
|
|
136
|
+
visibleWidth: () => Width;
|
|
137
|
+
/** The editor's document, to read the overlay tokens from (default: the global document). */
|
|
138
|
+
tokensFrom?: Document;
|
|
139
|
+
/**
|
|
140
|
+
* M4 (Decision 9): `quiet` draws nothing (a read-only document: no overlays);
|
|
141
|
+
* `veil` draws only the lock's veil, a two-tone hatch over the whole page that
|
|
142
|
+
* takes the pointer, so nothing on the page can be clicked or typed. Default normal.
|
|
143
|
+
*/
|
|
144
|
+
mode?: () => { kind: 'normal' } | { kind: 'quiet' } | { kind: 'veil'; title: string };
|
|
145
|
+
}): Overlays {
|
|
146
|
+
const { driver, layout } = options;
|
|
147
|
+
const channels = new Map<string, readonly Mark[]>();
|
|
148
|
+
const painters: OverlayPainter[] = [];
|
|
149
|
+
const stops: (() => void)[] = [];
|
|
150
|
+
const tokensFrom = options.tokensFrom ?? document;
|
|
151
|
+
let raf = 0;
|
|
152
|
+
let destroyed = false;
|
|
153
|
+
/** Each frame's veil, while it shows or lifts. */
|
|
154
|
+
const veils = new WeakMap<Document, HTMLElement>();
|
|
155
|
+
|
|
156
|
+
const tokens = (() => {
|
|
157
|
+
const style = getComputedStyle(tokensFrom.documentElement);
|
|
158
|
+
const out: string[] = [];
|
|
159
|
+
for (const [name, token] of Object.entries(OVERLAY_TOKENS)) {
|
|
160
|
+
const value = style.getPropertyValue(token).trim();
|
|
161
|
+
if (value) out.push(`${name}:${value}`);
|
|
162
|
+
}
|
|
163
|
+
return out.join(';');
|
|
164
|
+
})();
|
|
165
|
+
// The editor's UI font (the first preloaded font on /admin is Instrument Sans), for the tags.
|
|
166
|
+
const fontUrl = (() => {
|
|
167
|
+
const link = tokensFrom.querySelector<HTMLLinkElement>('link[rel="preload"][as="font"][href*="instrument-sans"]');
|
|
168
|
+
return link ? new URL(link.getAttribute('href')!, tokensFrom.baseURI).href : null;
|
|
169
|
+
})();
|
|
170
|
+
|
|
171
|
+
function mount(frame: PreviewFrame): HTMLElement | null {
|
|
172
|
+
const doc = frame.document;
|
|
173
|
+
if (!doc?.documentElement) return null;
|
|
174
|
+
if (!doc.getElementById(STYLE_ID)) {
|
|
175
|
+
const style = doc.createElement('style');
|
|
176
|
+
style.id = STYLE_ID;
|
|
177
|
+
style.textContent =
|
|
178
|
+
(fontUrl ? `@font-face{font-family:"${FONT_FAMILY}";src:url("${fontUrl}") format("woff2");font-weight:100 900;font-display:swap}\n` : '') + OVERLAY_CSS;
|
|
179
|
+
(doc.head ?? doc.documentElement).appendChild(style);
|
|
180
|
+
}
|
|
181
|
+
let layer = doc.getElementById(LAYER_ID);
|
|
182
|
+
if (!layer || layer.parentElement !== doc.documentElement) {
|
|
183
|
+
layer?.remove();
|
|
184
|
+
layer = doc.createElement('div');
|
|
185
|
+
layer.id = LAYER_ID;
|
|
186
|
+
layer.setAttribute('aria-hidden', 'true');
|
|
187
|
+
layer.setAttribute('style', tokens);
|
|
188
|
+
doc.documentElement.appendChild(layer);
|
|
189
|
+
}
|
|
190
|
+
return layer;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function context(frame: PreviewFrame, doc: Document, win: Window): OverlayContext {
|
|
194
|
+
const scale = layout.get()?.[frame.width].scale ?? 1;
|
|
195
|
+
const k = 1 / (scale > 0 ? scale : 1);
|
|
196
|
+
return {
|
|
197
|
+
frame,
|
|
198
|
+
width: frame.width,
|
|
199
|
+
visible: options.visibleWidth() === frame.width,
|
|
200
|
+
doc,
|
|
201
|
+
k,
|
|
202
|
+
// A fields document's root (`data-cms-document`) is block 0 (preview/markers.ts).
|
|
203
|
+
block: (i) => blockElement(doc, i),
|
|
204
|
+
field: (i, field) => fieldElement(doc, i, field),
|
|
205
|
+
blocks: () => blockElements(doc),
|
|
206
|
+
skippedTop(i) {
|
|
207
|
+
const anchor = skippedAnchor(doc, i);
|
|
208
|
+
return anchor ? skippedTop(anchor) + win.scrollY : null;
|
|
209
|
+
},
|
|
210
|
+
rect(el, pad = 0) {
|
|
211
|
+
const r = el.getBoundingClientRect();
|
|
212
|
+
const p = pad * k;
|
|
213
|
+
return { left: r.left + win.scrollX - p, top: r.top + win.scrollY - p, width: r.width + 2 * p, height: r.height + 2 * p };
|
|
214
|
+
},
|
|
215
|
+
place: (r) => `left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px`,
|
|
216
|
+
el(tag, className, style) {
|
|
217
|
+
const node = doc.createElement(tag);
|
|
218
|
+
if (className) node.className = className;
|
|
219
|
+
if (style) for (const [key, value] of Object.entries(style)) if (value !== undefined) node.style.setProperty(key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`), value);
|
|
220
|
+
return node;
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function drawMark(ctx: OverlayContext, mark: Mark): Node | null {
|
|
226
|
+
// A skipped block has no element: its mark goes where its comment is.
|
|
227
|
+
const skippedAt = mark.kind === 'skipped' ? ctx.skippedTop(mark.block) : null;
|
|
228
|
+
const target = mark.kind === 'skipped' ? null : mark.field ? ctx.field(mark.block, mark.field) : ctx.block(mark.block);
|
|
229
|
+
if (!target && skippedAt === null) return null;
|
|
230
|
+
const tone = mark.tone === 'phone' ? ' phone' : '';
|
|
231
|
+
const label = (cls: string) => {
|
|
232
|
+
if (!mark.label) return null;
|
|
233
|
+
const tag = ctx.el(mark.onClick ? 'button' : 'span', cls);
|
|
234
|
+
if (mark.onClick) {
|
|
235
|
+
(tag as HTMLButtonElement).type = 'button';
|
|
236
|
+
tag.addEventListener('click', (event) => {
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
event.stopPropagation();
|
|
239
|
+
mark.onClick!(event as MouseEvent);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
tag.textContent = mark.label;
|
|
243
|
+
return tag;
|
|
244
|
+
};
|
|
245
|
+
const box = (cls: string, pad: number, labelClass: string | null) => {
|
|
246
|
+
const el = ctx.el('div', `cms-box ${cls}${tone}`);
|
|
247
|
+
el.setAttribute('style', ctx.place(ctx.rect(target!, pad)));
|
|
248
|
+
el.dataset.mark = mark.kind;
|
|
249
|
+
el.dataset.block = String(mark.block);
|
|
250
|
+
if (mark.field) el.dataset.field = mark.field;
|
|
251
|
+
const tag = labelClass ? label(labelClass) : null;
|
|
252
|
+
if (tag) el.appendChild(tag);
|
|
253
|
+
return el;
|
|
254
|
+
};
|
|
255
|
+
switch (mark.kind) {
|
|
256
|
+
case 'hover':
|
|
257
|
+
return box('cms-hov', 0, 'cms-tag');
|
|
258
|
+
case 'selected':
|
|
259
|
+
return box('cms-sel', 0, 'cms-tag');
|
|
260
|
+
case 'field-hover':
|
|
261
|
+
return box('cms-fhov', 4, null);
|
|
262
|
+
case 'picked':
|
|
263
|
+
return box('cms-pick', 6, 'cms-chip');
|
|
264
|
+
case 'typing':
|
|
265
|
+
return box('cms-type', 6, 'cms-chip');
|
|
266
|
+
case 'warn':
|
|
267
|
+
return box('cms-warn', 4, 'cms-chip warn');
|
|
268
|
+
case 'skipped': {
|
|
269
|
+
const el = ctx.el('div', 'cms-skip');
|
|
270
|
+
el.style.top = `${skippedAt}px`;
|
|
271
|
+
el.dataset.mark = 'skipped';
|
|
272
|
+
el.dataset.block = String(mark.block);
|
|
273
|
+
const pill = label('');
|
|
274
|
+
if (pill) el.appendChild(pill);
|
|
275
|
+
return el;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function drawFrame(frame: PreviewFrame) {
|
|
281
|
+
const doc = frame.document;
|
|
282
|
+
const win = frame.window;
|
|
283
|
+
if (!doc || !win) return;
|
|
284
|
+
const layer = mount(frame);
|
|
285
|
+
if (!layer) return;
|
|
286
|
+
const ctx = context(frame, doc, win);
|
|
287
|
+
layer.style.setProperty('--k', ctx.k.toFixed(4));
|
|
288
|
+
const mode = options.mode?.() ?? { kind: 'normal' };
|
|
289
|
+
const height = () => `${Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0)}px`;
|
|
290
|
+
// The veil is one node per frame, kept across redraws, so it can lift: when the lock
|
|
291
|
+
// goes it fades out (`.out`, over --dur-slow) above whatever is drawn now, then goes.
|
|
292
|
+
let veil = veils.get(doc) ?? null;
|
|
293
|
+
if (mode.kind === 'veil') {
|
|
294
|
+
if (!veil || veil.ownerDocument !== doc) {
|
|
295
|
+
veil = ctx.el('div', 'cms-veil');
|
|
296
|
+
veil.dataset.mark = 'veil';
|
|
297
|
+
veils.set(doc, veil);
|
|
298
|
+
}
|
|
299
|
+
veil.classList.remove('out');
|
|
300
|
+
veil.title = mode.title;
|
|
301
|
+
veil.style.height = height();
|
|
302
|
+
} else if (veil) {
|
|
303
|
+
if (!veil.classList.contains('out')) {
|
|
304
|
+
const ms = parseFloat(getComputedStyle(veil).transitionDuration) * 1000 || 0;
|
|
305
|
+
if (ms > 0 && veil.isConnected) {
|
|
306
|
+
veil.classList.add('out');
|
|
307
|
+
const lifting = veil;
|
|
308
|
+
win.setTimeout(() => {
|
|
309
|
+
if (veils.get(doc) !== lifting || !lifting.classList.contains('out')) return;
|
|
310
|
+
veils.delete(doc);
|
|
311
|
+
lifting.remove();
|
|
312
|
+
}, ms + 40);
|
|
313
|
+
} else {
|
|
314
|
+
veils.delete(doc);
|
|
315
|
+
veil = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (mode.kind !== 'normal') {
|
|
320
|
+
replaceMarks(layer, veil ? [veil] : []);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const out: Node[] = [];
|
|
324
|
+
const marks = [...channels.values()].flat().filter((m) => !m.frames || m.frames.includes(frame.width));
|
|
325
|
+
marks.sort((a, b) => ORDER.indexOf(a.kind) - ORDER.indexOf(b.kind));
|
|
326
|
+
const draw = (mark: Mark) => {
|
|
327
|
+
try {
|
|
328
|
+
const node = drawMark(ctx, mark);
|
|
329
|
+
if (node) out.push(node);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('[caracms] an overlay mark failed:', error);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
marks.filter((m) => UNDER_PAINTERS.has(m.kind)).forEach(draw);
|
|
335
|
+
for (const painter of painters) {
|
|
336
|
+
try {
|
|
337
|
+
const result = painter(ctx);
|
|
338
|
+
if (result) out.push(...(Array.isArray(result) ? result : [result as Node]));
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.error('[caracms] an overlay painter failed:', error);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
marks.filter((m) => !UNDER_PAINTERS.has(m.kind)).forEach(draw);
|
|
344
|
+
if (veil) out.push(veil);
|
|
345
|
+
replaceMarks(layer, out);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* The layer's children become `nodes`, except the ones marked `data-cms-keep` (the
|
|
350
|
+
* formatting bar of on-page rich text, rich-text-bar.ts), which stay where they are,
|
|
351
|
+
* on top: never taken out, since taking out a node that holds focus (the link
|
|
352
|
+
* popover's search) blurs it.
|
|
353
|
+
*/
|
|
354
|
+
function replaceMarks(layer: HTMLElement, nodes: readonly Node[]) {
|
|
355
|
+
let kept: Element | null = null;
|
|
356
|
+
for (const child of [...layer.childNodes]) {
|
|
357
|
+
if (child.nodeType === 1 && (child as Element).hasAttribute(KEEP)) kept ??= child as Element;
|
|
358
|
+
else child.remove();
|
|
359
|
+
}
|
|
360
|
+
const doc = layer.ownerDocument;
|
|
361
|
+
const frag = doc.createDocumentFragment();
|
|
362
|
+
frag.append(...nodes);
|
|
363
|
+
layer.insertBefore(frag, kept);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function flush() {
|
|
367
|
+
cancelAnimationFrame(raf);
|
|
368
|
+
raf = 0;
|
|
369
|
+
if (destroyed) return;
|
|
370
|
+
for (const frame of driver.frames()) drawFrame(frame);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function redraw() {
|
|
374
|
+
if (destroyed || raf) return;
|
|
375
|
+
raf = requestAnimationFrame(() => {
|
|
376
|
+
raf = 0;
|
|
377
|
+
flush();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Each frame: the layer on load, again after every morph; a redraw when its page changes size.
|
|
382
|
+
stops.push(
|
|
383
|
+
driver.registerFrameHook((frame) => {
|
|
384
|
+
const doc = frame.document;
|
|
385
|
+
const win = frame.window as (Window & typeof globalThis) | null;
|
|
386
|
+
if (!doc || !win) return;
|
|
387
|
+
mount(frame);
|
|
388
|
+
redraw();
|
|
389
|
+
const observer = new win.ResizeObserver(() => redraw());
|
|
390
|
+
observer.observe(doc.documentElement);
|
|
391
|
+
if (doc.body) observer.observe(doc.body);
|
|
392
|
+
const onFonts = () => redraw();
|
|
393
|
+
doc.fonts?.addEventListener?.('loadingdone', onFonts);
|
|
394
|
+
doc.addEventListener('input', onFonts, true);
|
|
395
|
+
return () => {
|
|
396
|
+
observer.disconnect();
|
|
397
|
+
doc.fonts?.removeEventListener?.('loadingdone', onFonts);
|
|
398
|
+
doc.removeEventListener('input', onFonts, true);
|
|
399
|
+
};
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
stops.push(layout.subscribe(() => redraw()));
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
set(channel, marks) {
|
|
406
|
+
channels.set(channel, marks);
|
|
407
|
+
redraw();
|
|
408
|
+
},
|
|
409
|
+
clear(channel) {
|
|
410
|
+
if (channels.delete(channel)) redraw();
|
|
411
|
+
},
|
|
412
|
+
addPainter(painter) {
|
|
413
|
+
painters.push(painter);
|
|
414
|
+
redraw();
|
|
415
|
+
return () => {
|
|
416
|
+
const i = painters.indexOf(painter);
|
|
417
|
+
if (i >= 0) painters.splice(i, 1);
|
|
418
|
+
redraw();
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
redraw,
|
|
422
|
+
flush,
|
|
423
|
+
layerOf(frame) {
|
|
424
|
+
const layer = frame.document?.getElementById(LAYER_ID);
|
|
425
|
+
return layer ?? null;
|
|
426
|
+
},
|
|
427
|
+
destroy() {
|
|
428
|
+
destroyed = true;
|
|
429
|
+
cancelAnimationFrame(raf);
|
|
430
|
+
for (const stop of stops) stop();
|
|
431
|
+
for (const frame of driver.frames()) {
|
|
432
|
+
frame.document?.getElementById(LAYER_ID)?.remove();
|
|
433
|
+
frame.document?.getElementById(STYLE_ID)?.remove();
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overlays' CSS, injected into each preview iframe's <head> (never into the
|
|
3
|
+
* public page; Decision 17). Lifted from the mockup's overlayCSS() (notes/mockups/
|
|
4
|
+
* m2-editor.html), M2 → Rules → Overlays: a blue core with a white line both sides,
|
|
5
|
+
* the same size on screen at any scale (`--k` = 1 / the frame's scale), hover thin
|
|
6
|
+
* with a wash, selected 2px with a filled tag, a picked field ringed with its name,
|
|
7
|
+
* teal for phone lines, amber dashes for too long and skipped, and the lock's veil,
|
|
8
|
+
* a two-tone hatch that lifts (fades out over --dur-slow) when the lock goes.
|
|
9
|
+
*
|
|
10
|
+
* Colours come from the editor's tokens (`--ov*` in styles/tokens.css): the layer
|
|
11
|
+
* copies them onto itself as `--cms-*` when it mounts (layer.ts), so there are no
|
|
12
|
+
* raw colours here (M2 Decision 7). Everything is scoped under `#cms-overlays`, the
|
|
13
|
+
* layer, so it can't touch the site; the rules outside it only set cursors, hide
|
|
14
|
+
* the browser's own outline on the fields the layer rings, and give a mounted page
|
|
15
|
+
* editor (on-page rich text) the one style ProseMirror needs, `white-space:
|
|
16
|
+
* pre-wrap`, which applies only while it's mounted (Decision 3).
|
|
17
|
+
*
|
|
18
|
+
* A string, not a .css file: it goes into another document, so the editor's own
|
|
19
|
+
* stylesheet pipeline doesn't apply.
|
|
20
|
+
*/
|
|
21
|
+
/** A size that stays the same on screen at any preview scale. */
|
|
22
|
+
const K = (n: number) => `calc(var(--k)*${n}px)`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The formatting bar of rich text typed on the page and what opens from it (on-page
|
|
26
|
+
* rich text, Decision 9; rich-text-bar.ts), lifted from the mockup's RT CSS: the
|
|
27
|
+
* sidebar toolbar's look at a constant size, the style menu, the link popover, and
|
|
28
|
+
* the linked words' wash. A reset first, so a site's own rules for `button`, `input`
|
|
29
|
+
* or `p` can't reach in.
|
|
30
|
+
*/
|
|
31
|
+
const RICH_TEXT_BAR_CSS = `#cms-overlays .cms-rt{position:absolute;left:0;top:0;width:100%;height:0;pointer-events:none;font:400 ${K(13)}/1.45 "caracms UI",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:var(--cms-t);letter-spacing:0;text-transform:none;text-align:left;white-space:normal}
|
|
32
|
+
#cms-overlays .cms-rt :where(div,p,span,b,i,button,input){margin:0;padding:0;border:0;background:none;box-shadow:none;letter-spacing:0;text-transform:none;text-shadow:none;text-decoration:none;text-indent:0;float:none;min-width:0;min-height:0;max-width:none;font-style:normal;color:inherit}
|
|
33
|
+
#cms-overlays .cms-rt button{font-family:"caracms UI",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-appearance:none;appearance:none}
|
|
34
|
+
#cms-overlays .cms-rt :focus-visible{outline:${K(2)} solid var(--cms-focus);outline-offset:${K(1)}}
|
|
35
|
+
#cms-overlays .cms-rt .cms-i{width:${K(14)};height:${K(14)};flex:none;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
36
|
+
#cms-overlays .cms-hold{position:absolute;background:var(--cms-sel-halo);border-radius:${K(2)}}
|
|
37
|
+
#cms-overlays .cms-rtw{position:absolute;pointer-events:auto;transition:top var(--cms-dur-fast) cubic-bezier(.2,.8,.2,1),left var(--cms-dur-fast) cubic-bezier(.2,.8,.2,1);animation:cms-rise var(--cms-dur-base) cubic-bezier(.2,.8,.2,1)}
|
|
38
|
+
#cms-overlays .cms-rtw[data-place=below]{animation-name:cms-drop}
|
|
39
|
+
@keyframes cms-rise{from{opacity:0;translate:0 ${K(6)}}}
|
|
40
|
+
@keyframes cms-drop{from{opacity:0;translate:0 ${K(-6)}}}
|
|
41
|
+
#cms-overlays .cms-rtbar,#cms-overlays .cms-rtmenu,#cms-overlays .cms-lpop{background:var(--cms-surface);box-shadow:0 ${K(14)} ${K(36)} var(--cms-pop),0 0 0 ${K(1)} var(--cms-pop-line)}
|
|
42
|
+
#cms-overlays .cms-rtbar{display:flex;align-items:center;gap:${K(2)};padding:${K(3)};border-radius:${K(8)};white-space:nowrap}
|
|
43
|
+
#cms-overlays .cms-rtbar button{display:inline-grid;place-items:center;width:${K(28)};height:${K(26)};padding:0;border:0;border-radius:${K(5)};background:none;color:var(--cms-muted);font-size:${K(13)};font-weight:600;line-height:1;cursor:pointer;transition:background var(--cms-dur-fast),color var(--cms-dur-fast)}
|
|
44
|
+
#cms-overlays .cms-rtbar button:hover{background:var(--cms-hover);color:var(--cms-t)}
|
|
45
|
+
#cms-overlays .cms-rtbar button[aria-pressed=true],#cms-overlays .cms-rtbar button[aria-expanded=true]{background:var(--cms-pressed);color:var(--cms-t)}
|
|
46
|
+
#cms-overlays .cms-rtbar button b{font-weight:700}
|
|
47
|
+
#cms-overlays .cms-rtbar button i{font:italic 600 ${K(14)}/1 Georgia,serif}
|
|
48
|
+
#cms-overlays .cms-rtbar .cms-st{display:inline-flex;align-items:center;justify-content:space-between;gap:${K(4)};width:auto;min-width:${K(100)};padding:0 ${K(6)} 0 ${K(8)};color:var(--cms-t);font-weight:500}
|
|
49
|
+
#cms-overlays .cms-rtbar .cms-st .cms-i{width:${K(12)};height:${K(12)};color:var(--cms-icon)}
|
|
50
|
+
#cms-overlays .cms-rtsep{flex:none;width:${K(1)};height:${K(16)};margin:0 ${K(3)};background:var(--cms-line)}
|
|
51
|
+
#cms-overlays .cms-rtmenu{position:absolute;left:0;top:100%;min-width:${K(188)};padding:${K(4)};border-radius:${K(10)}}
|
|
52
|
+
#cms-overlays .cms-rtmenu button{display:flex;align-items:center;gap:${K(8)};width:100%;height:${K(30)};padding:0 ${K(8)};border:0;border-radius:${K(5)};background:none;color:var(--cms-t);font-size:${K(13)};line-height:1;text-align:left;cursor:pointer}
|
|
53
|
+
#cms-overlays .cms-rtmenu button:hover{background:var(--cms-hover)}
|
|
54
|
+
#cms-overlays .cms-rtmenu button[aria-checked=true]{background:var(--cms-sel-wash);color:var(--cms-sel-strong)}
|
|
55
|
+
#cms-overlays .cms-rtmenu .k{margin-left:auto;display:inline-flex}
|
|
56
|
+
#cms-overlays .cms-rtmenu .st-h2{font-size:${K(16)};font-weight:600;letter-spacing:-.01em}
|
|
57
|
+
#cms-overlays .cms-rtmenu .st-h3{font-size:${K(14)};font-weight:600}
|
|
58
|
+
#cms-overlays .cms-lpop{position:absolute;left:0;top:100%;padding:${K(4)};border-radius:${K(10)}}
|
|
59
|
+
#cms-overlays .cms-lpop .find{position:relative;margin:0 0 ${K(4)}}
|
|
60
|
+
#cms-overlays .cms-lpop .find .cms-i{position:absolute;left:${K(10)};top:50%;translate:0 -50%;width:${K(16)};height:${K(16)};color:var(--cms-icon)}
|
|
61
|
+
#cms-overlays .cms-lpop input{box-sizing:border-box;width:100%;height:${K(32)};padding:0 ${K(10)} 0 ${K(34)};border:${K(1)} solid var(--cms-line-strong);border-radius:${K(7)};background:var(--cms-surface);color:var(--cms-t);font:400 ${K(13)}/1 "caracms UI",system-ui,sans-serif;outline:0;-webkit-appearance:none;appearance:none}
|
|
62
|
+
#cms-overlays .cms-lpop input:focus{border-color:var(--cms-sel);box-shadow:0 0 0 ${K(3)} var(--cms-sel-halo)}
|
|
63
|
+
#cms-overlays .cms-lpop input[aria-invalid=true]{border-color:var(--cms-wdot)}
|
|
64
|
+
#cms-overlays .cms-msg{display:flex;align-items:flex-start;gap:${K(6)};margin:${K(2)} ${K(6)} ${K(6)};font-size:${K(12)};font-weight:500;line-height:1.4;color:var(--cms-wtext)}
|
|
65
|
+
#cms-overlays .cms-msg .cms-i{margin-top:${K(1)};color:var(--cms-wdot)}
|
|
66
|
+
#cms-overlays .cms-lpop .sw-h{padding:${K(6)} ${K(8)} ${K(4)};font-size:${K(11)};font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--cms-muted)}
|
|
67
|
+
#cms-overlays .cms-lpop .drow{display:flex;align-items:center;gap:${K(8)};width:100%;min-height:${K(32)};padding:0 ${K(8)};border:0;border-radius:${K(7)};background:none;color:var(--cms-t);font-size:${K(13)};line-height:1.3;text-align:left;cursor:pointer}
|
|
68
|
+
#cms-overlays .cms-lpop .drow:hover,#cms-overlays .cms-lpop .drow.is-active{background:var(--cms-hover)}
|
|
69
|
+
#cms-overlays .cms-lpop .drow .cms-i{width:${K(16)};height:${K(16)};color:var(--cms-icon)}
|
|
70
|
+
#cms-overlays .cms-lpop .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
71
|
+
#cms-overlays .cms-lpop .crumb{color:var(--cms-subtle);font-weight:500}
|
|
72
|
+
#cms-overlays .cms-lpop .meta{flex:none;color:var(--cms-subtle)}
|
|
73
|
+
#cms-overlays .cms-lpop .mono{font-family:"Geist Mono",ui-monospace,SFMono-Regular,Menlo,monospace;font-size:${K(10.5)}}
|
|
74
|
+
#cms-overlays .cms-lpop .sw-more{padding:${K(6)} ${K(8)};font-size:${K(12)};color:var(--cms-subtle)}
|
|
75
|
+
#cms-overlays .cms-lpop-foot{margin-top:${K(4)};padding:${K(8)} ${K(8)} ${K(4)};border-top:${K(1)} solid var(--cms-line);font-size:${K(11)};line-height:1.5;color:var(--cms-subtle)}
|
|
76
|
+
#cms-overlays .cms-lpop .cms-top{display:flex;gap:${K(4)};margin:0 0 ${K(4)}}
|
|
77
|
+
#cms-overlays .cms-lpop .cms-top .find{flex:1;min-width:0;margin:0}
|
|
78
|
+
#cms-overlays .cms-lpop .cms-btn{display:inline-flex;align-items:center;gap:${K(6)};flex:none;height:${K(32)};padding:0 ${K(10)};border:${K(1)} solid var(--cms-line-strong);border-radius:${K(7)};background:var(--cms-surface);color:var(--cms-t);font-size:${K(13)};font-weight:500;cursor:pointer}
|
|
79
|
+
#cms-overlays .cms-lpop .cms-btn:hover{background:var(--cms-hover)}
|
|
80
|
+
#cms-overlays .cms-lpop .cms-btn .cms-i{color:var(--cms-icon)}
|
|
81
|
+
#cms-overlays .cms-ftype{position:relative;display:block;width:${K(18)};height:${K(22)};flex:none;border:${K(1)} solid var(--cms-line-strong);border-radius:${K(2)};background:linear-gradient(var(--cms-line-strong),var(--cms-line-strong)) ${K(4)} ${K(8)}/calc(100% - ${K(8)}) ${K(1)} no-repeat,linear-gradient(var(--cms-line-strong),var(--cms-line-strong)) ${K(4)} ${K(11)}/calc(100% - ${K(8)}) ${K(1)} no-repeat,var(--cms-surface);clip-path:polygon(0 0,calc(100% - ${K(6)}) 0,100% ${K(6)},100% 100%,0 100%)}
|
|
82
|
+
#cms-overlays .cms-picked{display:flex;align-items:center;gap:${K(8)};padding:${K(6)} ${K(4)} ${K(6)} ${K(8)};border-radius:${K(7)};background:var(--cms-wash)}
|
|
83
|
+
#cms-overlays .cms-picked .ft{flex:1;min-width:0;display:flex;flex-direction:column;line-height:1.35}
|
|
84
|
+
#cms-overlays .cms-picked b{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
85
|
+
#cms-overlays .cms-picked small{font-size:${K(11.5)};color:var(--cms-subtle)}
|
|
86
|
+
#cms-overlays .cms-picked>.cms-i{color:var(--cms-wdot)}
|
|
87
|
+
#cms-overlays .cms-ib{display:grid;place-items:center;width:${K(26)};height:${K(26)};flex:none;border-radius:${K(6)};color:var(--cms-icon);cursor:pointer}
|
|
88
|
+
#cms-overlays .cms-ib:hover{background:var(--cms-hover);color:var(--cms-t)}
|
|
89
|
+
#cms-overlays .cms-opens{display:flex;align-items:center;gap:${K(8)};margin-top:${K(6)};padding:0 ${K(4)}}
|
|
90
|
+
#cms-overlays .cms-opens .lbl{flex:none;font-size:${K(11.5)};font-weight:600;color:var(--cms-muted)}
|
|
91
|
+
#cms-overlays .cms-seg{display:flex;flex:1;padding:${K(2)};border-radius:${K(7)};background:var(--cms-pressed)}
|
|
92
|
+
#cms-overlays .cms-seg button{display:inline-flex;flex:1;align-items:center;justify-content:center;gap:${K(5)};height:${K(26)};border-radius:${K(5)};color:var(--cms-muted);font-size:${K(12.5)};font-weight:500;cursor:pointer}
|
|
93
|
+
#cms-overlays .cms-seg button[aria-checked=true]{background:var(--cms-surface);color:var(--cms-t);box-shadow:0 ${K(1)} ${K(2)} var(--cms-pop)}
|
|
94
|
+
#cms-overlays .cms-seg .cms-i{width:${K(13)};height:${K(13)}}
|
|
95
|
+
@media (prefers-reduced-motion:reduce){#cms-overlays .cms-rtw,#cms-overlays .cms-rtbar button{transition:none;animation:none}}
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
export const OVERLAY_CSS = `
|
|
99
|
+
html [data-cms-block]{cursor:default}
|
|
100
|
+
html [data-cms-kind="text"]{cursor:text}
|
|
101
|
+
html [data-cms-kind="lockup"]{cursor:pointer}
|
|
102
|
+
html [data-cms-kind="text"]:focus{outline:none}
|
|
103
|
+
html [data-cms-kind="richText"][tabindex]{cursor:text}
|
|
104
|
+
html [data-cms-kind="richText"][tabindex]:focus{outline:none}
|
|
105
|
+
html [data-cms-kind="richText"].ProseMirror{white-space:pre-wrap}
|
|
106
|
+
#cms-overlays{all:initial;position:absolute;left:0;top:0;width:100%;height:0;overflow:visible;pointer-events:none;z-index:2147483000;font-family:"caracms UI",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased;line-height:1;letter-spacing:0;text-transform:none}
|
|
107
|
+
#cms-overlays *{box-sizing:border-box}
|
|
108
|
+
#cms-overlays .cms-box{position:absolute;border-radius:calc(var(--k)*3px)}
|
|
109
|
+
#cms-overlays .cms-hov{border:calc(var(--k)*1.5px) solid var(--cms-ov);background:var(--cms-wash);box-shadow:0 0 0 calc(var(--k)*1px) var(--cms-halo),inset 0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
110
|
+
#cms-overlays .cms-sel{border:calc(var(--k)*2px) solid var(--cms-ov);box-shadow:0 0 0 calc(var(--k)*1px) var(--cms-halo),inset 0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
111
|
+
#cms-overlays .cms-fhov{border:calc(var(--k)*1px) dashed var(--cms-ov);box-shadow:0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
112
|
+
#cms-overlays .cms-pick{border:calc(var(--k)*2px) solid var(--cms-ov);border-radius:calc(var(--k)*4px);box-shadow:0 0 0 calc(var(--k)*1.5px) var(--cms-halo),inset 0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
113
|
+
#cms-overlays .phone{--cms-ov:var(--cms-phone)}
|
|
114
|
+
#cms-overlays .cms-type{border:calc(var(--k)*1.5px) solid var(--cms-ov);border-radius:calc(var(--k)*4px);box-shadow:0 0 0 calc(var(--k)*1px) var(--cms-halo),0 0 0 calc(var(--k)*5px) var(--cms-ring)}
|
|
115
|
+
#cms-overlays .cms-warn{border:calc(var(--k)*2px) dashed var(--cms-warn);border-radius:calc(var(--k)*4px);box-shadow:0 0 0 calc(var(--k)*1px) var(--cms-halo),inset 0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
116
|
+
#cms-overlays .cms-tag,#cms-overlays .cms-chip{position:absolute;display:inline-flex;align-items:center;gap:calc(var(--k)*5px);height:calc(var(--k)*22px);padding:0 calc(var(--k)*8px);background:var(--cms-ov);color:var(--cms-on);font-size:calc(var(--k)*11.5px);font-weight:600;line-height:1;white-space:nowrap;box-shadow:0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
117
|
+
#cms-overlays .cms-tag{left:calc(var(--k)*-2px);top:calc(var(--k)*-2px);border-radius:calc(var(--k)*3px) 0 calc(var(--k)*6px) 0}
|
|
118
|
+
#cms-overlays .cms-hov .cms-tag{background:var(--cms-surface);color:var(--cms-ov-text);left:calc(var(--k)*-1.5px);top:calc(var(--k)*-1.5px);box-shadow:inset 0 0 0 calc(var(--k)*1.5px) var(--cms-ov)}
|
|
119
|
+
#cms-overlays .cms-chip{left:calc(var(--k)*-2px);bottom:100%;margin-bottom:calc(var(--k)*5px);border-radius:calc(var(--k)*5px)}
|
|
120
|
+
#cms-overlays .cms-chip.in{bottom:auto;top:calc(var(--k)*4px);left:calc(var(--k)*4px);margin:0}
|
|
121
|
+
#cms-overlays .cms-chip.warn{background:var(--cms-warn-wash);color:var(--cms-warn-text);box-shadow:inset 0 0 0 calc(var(--k)*1.5px) var(--cms-warn),0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
122
|
+
#cms-overlays .cms-go{pointer-events:auto;position:absolute;display:inline-flex;align-items:center;height:calc(var(--k)*22px);padding:0 calc(var(--k)*9px);border:0;border-radius:999px;background:var(--cms-surface);color:var(--cms-ov-text);font:600 calc(var(--k)*11.5px)/1 "caracms UI",system-ui,sans-serif;box-shadow:0 0 0 calc(var(--k)*1.5px) var(--cms-ov),0 calc(var(--k)*3px) calc(var(--k)*10px) var(--cms-lift);cursor:pointer;white-space:nowrap}
|
|
123
|
+
#cms-overlays .cms-go.off{pointer-events:none;cursor:default;box-shadow:0 0 0 calc(var(--k)*1px) var(--cms-halo),0 calc(var(--k)*3px) calc(var(--k)*10px) var(--cms-lift)}
|
|
124
|
+
#cms-overlays .cms-go:focus-visible{outline:calc(var(--k)*2px) solid var(--cms-ov);outline-offset:calc(var(--k)*2px)}
|
|
125
|
+
#cms-overlays .cms-add{pointer-events:auto;position:absolute;left:50%;top:0;transform:translate(-50%,-50%);display:inline-flex;align-items:center;gap:calc(var(--k)*4px);height:calc(var(--k)*24px);padding:0 calc(var(--k)*11px);border:0;border-radius:999px;background:var(--cms-surface);color:var(--cms-ov-text);font:600 calc(var(--k)*11.5px)/1 "caracms UI",system-ui,sans-serif;box-shadow:0 0 0 calc(var(--k)*1.5px) var(--cms-ov),0 calc(var(--k)*3px) calc(var(--k)*10px) var(--cms-lift);cursor:pointer;white-space:nowrap}
|
|
126
|
+
#cms-overlays .cms-add:hover{background:var(--cms-ov);color:var(--cms-on)}
|
|
127
|
+
#cms-overlays .cms-add:focus-visible,#cms-overlays .cms-skip button:focus-visible{outline:calc(var(--k)*2px) solid var(--cms-ov);outline-offset:calc(var(--k)*2px)}
|
|
128
|
+
#cms-overlays .cms-empty{position:absolute;border:calc(var(--k)*1.5px) dashed var(--cms-ov);border-radius:calc(var(--k)*6px);box-shadow:0 0 0 calc(var(--k)*1px) var(--cms-halo),inset 0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
129
|
+
#cms-overlays .cms-empty .cms-add{top:50%;height:calc(var(--k)*34px);font-size:calc(var(--k)*13px);padding:0 calc(var(--k)*16px)}
|
|
130
|
+
#cms-overlays .cms-skip{position:absolute;left:0;right:0;height:0}
|
|
131
|
+
#cms-overlays .cms-skip::before{content:"";position:absolute;left:0;right:0;top:calc(var(--k)*-1px);border-top:calc(var(--k)*2px) dashed var(--cms-warn);box-shadow:0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
132
|
+
#cms-overlays .cms-skip button{pointer-events:auto;cursor:pointer;position:absolute;left:50%;top:0;transform:translate(-50%,-50%);height:calc(var(--k)*22px);padding:0 calc(var(--k)*9px);display:inline-flex;align-items:center;border:0;border-radius:999px;background:var(--cms-warn-wash);color:var(--cms-warn-text);font:600 calc(var(--k)*11.5px)/1 "caracms UI",system-ui,sans-serif;white-space:nowrap;box-shadow:inset 0 0 0 calc(var(--k)*1.5px) var(--cms-warn),0 0 0 calc(var(--k)*1px) var(--cms-halo)}
|
|
133
|
+
#cms-overlays .cms-skip.on button{box-shadow:inset 0 0 0 calc(var(--k)*2px) var(--cms-warn),0 0 0 calc(var(--k)*1px) var(--cms-halo),0 0 0 calc(var(--k)*4px) var(--cms-ring)}
|
|
134
|
+
#cms-overlays .cms-veil{position:absolute;left:0;top:0;width:100%;pointer-events:auto;cursor:not-allowed;background:repeating-linear-gradient(135deg,var(--cms-veil-a) 0 calc(var(--k)*7px),var(--cms-veil-b) calc(var(--k)*7px) calc(var(--k)*14px));transition:opacity var(--cms-dur,0ms) ease}
|
|
135
|
+
#cms-overlays .cms-veil.out{opacity:0;pointer-events:none}
|
|
136
|
+
@media (prefers-reduced-motion:reduce){#cms-overlays .cms-veil{transition:none}}
|
|
137
|
+
${RICH_TEXT_BAR_CSS}`;
|
|
138
|
+
|
|
139
|
+
/** The editor's overlay tokens, copied onto the layer as `--cms-*` (overlay-css.ts uses only these). */
|
|
140
|
+
export const OVERLAY_TOKENS: Record<string, string> = {
|
|
141
|
+
'--cms-ov': '--ov',
|
|
142
|
+
'--cms-ov-text': '--ov-text',
|
|
143
|
+
'--cms-halo': '--ov-halo',
|
|
144
|
+
'--cms-on': '--ov-on',
|
|
145
|
+
'--cms-surface': '--ov-surface',
|
|
146
|
+
'--cms-wash': '--ov-wash',
|
|
147
|
+
'--cms-phone': '--ov-phone',
|
|
148
|
+
'--cms-warn': '--ov-warn',
|
|
149
|
+
'--cms-warn-text': '--ov-warn-text',
|
|
150
|
+
'--cms-warn-wash': '--ov-warn-wash',
|
|
151
|
+
'--cms-ring': '--ov-ring',
|
|
152
|
+
'--cms-lift': '--ov-lift',
|
|
153
|
+
'--cms-veil-a': '--ov-veil-a',
|
|
154
|
+
'--cms-veil-b': '--ov-veil-b',
|
|
155
|
+
// The veil lifts over --dur-slow (M2 → Motion); 0ms under reduced motion, as the token is.
|
|
156
|
+
'--cms-dur': '--dur-slow',
|
|
157
|
+
// The formatting bar of rich text on the page (RICH_TEXT_BAR_CSS): the chrome's own tokens, as the sidebar's toolbar has them.
|
|
158
|
+
'--cms-t': '--text',
|
|
159
|
+
'--cms-muted': '--text-muted',
|
|
160
|
+
'--cms-subtle': '--text-subtle',
|
|
161
|
+
'--cms-icon': '--icon',
|
|
162
|
+
'--cms-hover': '--surface-hover',
|
|
163
|
+
'--cms-pressed': '--surface-pressed',
|
|
164
|
+
'--cms-line': '--border',
|
|
165
|
+
'--cms-line-strong': '--border-strong',
|
|
166
|
+
'--cms-sel': '--select',
|
|
167
|
+
'--cms-sel-strong': '--select-strong',
|
|
168
|
+
'--cms-sel-wash': '--select-wash',
|
|
169
|
+
'--cms-sel-halo': '--select-halo',
|
|
170
|
+
'--cms-focus': '--focus',
|
|
171
|
+
'--cms-wtext': '--status-warning',
|
|
172
|
+
'--cms-wdot': '--status-warning-dot',
|
|
173
|
+
'--cms-pop': '--ov-pop',
|
|
174
|
+
'--cms-pop-line': '--ov-pop-line',
|
|
175
|
+
'--cms-dur-fast': '--dur-fast',
|
|
176
|
+
'--cms-dur-base': '--dur-base',
|
|
177
|
+
};
|