caracms 0.0.0-stage → 0.1.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.
Files changed (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -2
  3. package/bin/caracms.mjs +174 -0
  4. package/bin/gate.mjs +275 -0
  5. package/bin/init.mjs +711 -0
  6. package/bin/media.mjs +662 -0
  7. package/bin/site-runner.mjs +61 -0
  8. package/bin/templates/Page.astro.txt +31 -0
  9. package/bin/templates/TextBlock.astro.txt +13 -0
  10. package/bin/templates/caracms.config.ts.txt +22 -0
  11. package/bin/templates/catch-all.astro.txt +24 -0
  12. package/bin/templates/health.ts.txt +6 -0
  13. package/bin/templates/railway.ts.txt +147 -0
  14. package/package.json +66 -4
  15. package/skills/caracms-adopt/SKILL.md +496 -0
  16. package/skills/caracms-adopt/look-same.mjs +948 -0
  17. package/skills/caracms-blocks/SKILL.md +329 -0
  18. package/skills/caracms-content/SKILL.md +200 -0
  19. package/src/cards.ts +158 -0
  20. package/src/cli/schemas.ts +122 -0
  21. package/src/cli/site.ts +66 -0
  22. package/src/cli/validate.ts +466 -0
  23. package/src/code-routes-disk.ts +67 -0
  24. package/src/code-routes.ts +134 -0
  25. package/src/columns.ts +39 -0
  26. package/src/components/Block.astro +46 -0
  27. package/src/components/Blocks.astro +120 -0
  28. package/src/components/Buttons.astro +116 -0
  29. package/src/components/Fonts.astro +14 -0
  30. package/src/components/Form.astro +155 -0
  31. package/src/components/Head.astro +61 -0
  32. package/src/components/Link.astro +68 -0
  33. package/src/components/Lockup.astro +116 -0
  34. package/src/components/Media.astro +98 -0
  35. package/src/components/MediaPlayer.astro +102 -0
  36. package/src/components/RenderDocument.astro +64 -0
  37. package/src/components/RichText.astro +51 -0
  38. package/src/components/Text.astro +58 -0
  39. package/src/components/attributes.ts +33 -0
  40. package/src/components/descriptor.ts +45 -0
  41. package/src/components/form-client.ts +179 -0
  42. package/src/components/media-player.ts +46 -0
  43. package/src/components/media-props.ts +35 -0
  44. package/src/components/sitemap.xml.ts +35 -0
  45. package/src/config.ts +794 -0
  46. package/src/content-schema.ts +163 -0
  47. package/src/content.ts +396 -0
  48. package/src/document.ts +566 -0
  49. package/src/editor/README.md +405 -0
  50. package/src/editor/addresses.ts +61 -0
  51. package/src/editor/brand.ts +118 -0
  52. package/src/editor/client/block-list/BlockList.tsx +342 -0
  53. package/src/editor/client/block-list/Picker.tsx +105 -0
  54. package/src/editor/client/block-list/UnknownBlock.tsx +119 -0
  55. package/src/editor/client/block-list/insert-points.ts +56 -0
  56. package/src/editor/client/block-list/link.ts +71 -0
  57. package/src/editor/client/block-list/reorder.ts +105 -0
  58. package/src/editor/client/block-list/summary.ts +112 -0
  59. package/src/editor/client/collections/CollectionGrid.tsx +143 -0
  60. package/src/editor/client/collections/CollectionTable.tsx +406 -0
  61. package/src/editor/client/collections/table.ts +270 -0
  62. package/src/editor/client/context.tsx +27 -0
  63. package/src/editor/client/documents/DocMenu.tsx +154 -0
  64. package/src/editor/client/documents/actions.ts +192 -0
  65. package/src/editor/client/documents/dialogs.tsx +687 -0
  66. package/src/editor/client/editor.ts +995 -0
  67. package/src/editor/client/fields/ButtonsField.tsx +87 -0
  68. package/src/editor/client/fields/DateField.tsx +200 -0
  69. package/src/editor/client/fields/FieldControl.tsx +61 -0
  70. package/src/editor/client/fields/FieldFrame.tsx +109 -0
  71. package/src/editor/client/fields/GroupField.tsx +113 -0
  72. package/src/editor/client/fields/LinkField.tsx +302 -0
  73. package/src/editor/client/fields/ListField.tsx +642 -0
  74. package/src/editor/client/fields/LockupField.tsx +365 -0
  75. package/src/editor/client/fields/MediaField.tsx +823 -0
  76. package/src/editor/client/fields/PageFields.tsx +82 -0
  77. package/src/editor/client/fields/ReferenceField.tsx +203 -0
  78. package/src/editor/client/fields/RichTextField.tsx +300 -0
  79. package/src/editor/client/fields/SelectField.tsx +63 -0
  80. package/src/editor/client/fields/SeoGroup.tsx +282 -0
  81. package/src/editor/client/fields/SimpleFields.tsx +89 -0
  82. package/src/editor/client/fields/TextField.tsx +51 -0
  83. package/src/editor/client/fields/date.ts +73 -0
  84. package/src/editor/client/fields/host.tsx +167 -0
  85. package/src/editor/client/fields/list-summary.ts +85 -0
  86. package/src/editor/client/fields/lockup-rules.ts +181 -0
  87. package/src/editor/client/fields/parts.tsx +457 -0
  88. package/src/editor/client/fields/rich-text-commands.ts +183 -0
  89. package/src/editor/client/fields/rich-text-doc.ts +130 -0
  90. package/src/editor/client/fields/rich-text-schema.ts +185 -0
  91. package/src/editor/client/fields/stale.ts +58 -0
  92. package/src/editor/client/inbox/InboxStage.tsx +272 -0
  93. package/src/editor/client/inbox/store.ts +311 -0
  94. package/src/editor/client/live/format.ts +77 -0
  95. package/src/editor/client/live/marks.tsx +90 -0
  96. package/src/editor/client/live/notices.tsx +213 -0
  97. package/src/editor/client/live/publish.tsx +617 -0
  98. package/src/editor/client/live/session.tsx +162 -0
  99. package/src/editor/client/main.tsx +12 -0
  100. package/src/editor/client/media/LibraryStage.tsx +75 -0
  101. package/src/editor/client/media/ReplaceDialog.tsx +355 -0
  102. package/src/editor/client/media/store.ts +618 -0
  103. package/src/editor/client/nav/DocRow.tsx +116 -0
  104. package/src/editor/client/nav/search.ts +71 -0
  105. package/src/editor/client/nav/tree.ts +93 -0
  106. package/src/editor/client/overlays/OverlayMount.tsx +26 -0
  107. package/src/editor/client/overlays/editor-marks.ts +159 -0
  108. package/src/editor/client/overlays/index.ts +50 -0
  109. package/src/editor/client/overlays/layer.ts +437 -0
  110. package/src/editor/client/overlays/overlay-css.ts +177 -0
  111. package/src/editor/client/overlays/rich-text-bar.ts +717 -0
  112. package/src/editor/client/pane/Pane.tsx +91 -0
  113. package/src/editor/client/pane/levels/BlockLevel.tsx +123 -0
  114. package/src/editor/client/pane/levels/CollectionLevel.tsx +115 -0
  115. package/src/editor/client/pane/levels/DocumentLevel.tsx +90 -0
  116. package/src/editor/client/pane/levels/DocumentsLevel.tsx +390 -0
  117. package/src/editor/client/pane/levels/FileLevel.tsx +132 -0
  118. package/src/editor/client/pane/levels/FormLevel.tsx +550 -0
  119. package/src/editor/client/pane/levels/MediaLevel.tsx +657 -0
  120. package/src/editor/client/pane/levels/SettingsM6.tsx +299 -0
  121. package/src/editor/client/pane/levels/SubmissionsLevel.tsx +185 -0
  122. package/src/editor/client/pane/parts.tsx +179 -0
  123. package/src/editor/client/pane/switcher.tsx +165 -0
  124. package/src/editor/client/preview/driver.ts +640 -0
  125. package/src/editor/client/preview/field-text.ts +33 -0
  126. package/src/editor/client/preview/follow-links.ts +234 -0
  127. package/src/editor/client/preview/layout.ts +37 -0
  128. package/src/editor/client/preview/link-target.ts +50 -0
  129. package/src/editor/client/preview/markers.ts +86 -0
  130. package/src/editor/client/preview/page-edit.ts +632 -0
  131. package/src/editor/client/preview/page-editor-registry.ts +62 -0
  132. package/src/editor/client/preview/page-editor.ts +235 -0
  133. package/src/editor/client/preview/render-sequencer.ts +113 -0
  134. package/src/editor/client/preview/selection.ts +116 -0
  135. package/src/editor/client/preview/warnings.ts +61 -0
  136. package/src/editor/client/shell/App.tsx +101 -0
  137. package/src/editor/client/shell/Bar.tsx +197 -0
  138. package/src/editor/client/shell/Brand.tsx +33 -0
  139. package/src/editor/client/shell/Icons.tsx +87 -0
  140. package/src/editor/client/shell/NarrowNote.tsx +47 -0
  141. package/src/editor/client/shell/Popovers.tsx +47 -0
  142. package/src/editor/client/shell/Stage.tsx +132 -0
  143. package/src/editor/client/shell/Toast.tsx +61 -0
  144. package/src/editor/client/state/document-store.ts +447 -0
  145. package/src/editor/client/state/keeper.ts +713 -0
  146. package/src/editor/client/state/live.ts +257 -0
  147. package/src/editor/client/state/schema.ts +42 -0
  148. package/src/editor/client/state/store.ts +39 -0
  149. package/src/editor/fonts/OFL-GeistMono.txt +93 -0
  150. package/src/editor/fonts/OFL-InstrumentSans.txt +93 -0
  151. package/src/editor/fonts/geist-mono-latin-ext-wght-normal.woff2 +0 -0
  152. package/src/editor/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
  153. package/src/editor/fonts/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
  154. package/src/editor/fonts/instrument-sans-latin-wght-normal.woff2 +0 -0
  155. package/src/editor/middleware.ts +62 -0
  156. package/src/editor/routes/_live.ts +26 -0
  157. package/src/editor/routes/admin.astro +80 -0
  158. package/src/editor/routes/create.ts +19 -0
  159. package/src/editor/routes/delete.ts +19 -0
  160. package/src/editor/routes/document.ts +39 -0
  161. package/src/editor/routes/documents.ts +30 -0
  162. package/src/editor/routes/github.ts +23 -0
  163. package/src/editor/routes/history.ts +9 -0
  164. package/src/editor/routes/lock.ts +9 -0
  165. package/src/editor/routes/login/assets.d.ts +10 -0
  166. package/src/editor/routes/login/login.ts +43 -0
  167. package/src/editor/routes/media.ts +16 -0
  168. package/src/editor/routes/move.ts +17 -0
  169. package/src/editor/routes/notice.ts +9 -0
  170. package/src/editor/routes/preview.astro +77 -0
  171. package/src/editor/routes/publish.ts +9 -0
  172. package/src/editor/routes/status.ts +9 -0
  173. package/src/editor/routes/submissions.ts +16 -0
  174. package/src/editor/routes/try-again.ts +9 -0
  175. package/src/editor/routes/write.ts +18 -0
  176. package/src/editor/server/admin-props.ts +151 -0
  177. package/src/editor/server/cards.ts +61 -0
  178. package/src/editor/server/create.ts +414 -0
  179. package/src/editor/server/drafts-store.ts +127 -0
  180. package/src/editor/server/media/alt.ts +107 -0
  181. package/src/editor/server/media/bucket.ts +295 -0
  182. package/src/editor/server/media/config.ts +78 -0
  183. package/src/editor/server/media/handler.ts +221 -0
  184. package/src/editor/server/media/library.ts +254 -0
  185. package/src/editor/server/media/memory.ts +184 -0
  186. package/src/editor/server/media/mux.ts +241 -0
  187. package/src/editor/server/media/sniff.ts +599 -0
  188. package/src/editor/server/media/upload.ts +175 -0
  189. package/src/editor/server/mode.ts +63 -0
  190. package/src/editor/server/move.ts +295 -0
  191. package/src/editor/server/render.tsx +9 -0
  192. package/src/editor/server/replace.ts +72 -0
  193. package/src/editor/server/settings.ts +35 -0
  194. package/src/editor/server/write.ts +366 -0
  195. package/src/editor/styles/editor.css +500 -0
  196. package/src/editor/styles/fonts.css +40 -0
  197. package/src/editor/styles/levels/README.md +4 -0
  198. package/src/editor/styles/levels/collections.css +99 -0
  199. package/src/editor/styles/levels/document-level.css +10 -0
  200. package/src/editor/styles/levels/documents.css +9 -0
  201. package/src/editor/styles/levels/fields.css +171 -0
  202. package/src/editor/styles/levels/files.css +95 -0
  203. package/src/editor/styles/levels/live.css +10 -0
  204. package/src/editor/styles/levels/m5a-documents.css +51 -0
  205. package/src/editor/styles/levels/m6.css +82 -0
  206. package/src/editor/styles/levels/media.css +175 -0
  207. package/src/editor/styles/levels/switcher.css +3 -0
  208. package/src/editor/styles/tokens.css +114 -0
  209. package/src/editor/tsconfig.json +16 -0
  210. package/src/editor/types.ts +879 -0
  211. package/src/editor/ui-state.ts +113 -0
  212. package/src/email.ts +67 -0
  213. package/src/fields.ts +1081 -0
  214. package/src/files-route.ts +201 -0
  215. package/src/files.ts +116 -0
  216. package/src/forms/email.ts +149 -0
  217. package/src/forms/endpoint.ts +347 -0
  218. package/src/forms/inbox.ts +273 -0
  219. package/src/forms/rate-limit.ts +130 -0
  220. package/src/forms/route.ts +24 -0
  221. package/src/forms/rules.ts +270 -0
  222. package/src/forms/store.ts +486 -0
  223. package/src/head.ts +121 -0
  224. package/src/index.ts +434 -0
  225. package/src/json-source.ts +116 -0
  226. package/src/links.ts +86 -0
  227. package/src/live/README.md +42 -0
  228. package/src/live/auth/allow-rules.ts +70 -0
  229. package/src/live/auth/allow.ts +128 -0
  230. package/src/live/auth/auth.ts +155 -0
  231. package/src/live/auth/email.ts +84 -0
  232. package/src/live/auth/guard.ts +111 -0
  233. package/src/live/auth/index.ts +68 -0
  234. package/src/live/auth/links.ts +101 -0
  235. package/src/live/auth/login.ts +245 -0
  236. package/src/live/auth/middleware.ts +53 -0
  237. package/src/live/auth/request.ts +56 -0
  238. package/src/live/auth/routes.ts +9 -0
  239. package/src/live/auth/session.ts +181 -0
  240. package/src/live/auth/views.ts +355 -0
  241. package/src/live/autosaves.ts +306 -0
  242. package/src/live/backend.ts +634 -0
  243. package/src/live/drafts-store.ts +591 -0
  244. package/src/live/editor.ts +465 -0
  245. package/src/live/git.ts +1021 -0
  246. package/src/live/github.ts +271 -0
  247. package/src/live/locals.ts +24 -0
  248. package/src/live/locks.ts +273 -0
  249. package/src/live/merge.ts +197 -0
  250. package/src/live/messages.ts +151 -0
  251. package/src/live/mode.ts +164 -0
  252. package/src/live/redis.ts +533 -0
  253. package/src/live/runtime.ts +161 -0
  254. package/src/live/status.ts +109 -0
  255. package/src/live/sweep.ts +228 -0
  256. package/src/live/timings.ts +47 -0
  257. package/src/live/webhook.ts +39 -0
  258. package/src/lockup-css.ts +58 -0
  259. package/src/measure.ts +145 -0
  260. package/src/media-value.ts +239 -0
  261. package/src/media.ts +247 -0
  262. package/src/read.ts +255 -0
  263. package/src/redirect-middleware.ts +49 -0
  264. package/src/redirects.ts +76 -0
  265. package/src/relink.ts +293 -0
  266. package/src/reserved.ts +24 -0
  267. package/src/rich-text.ts +665 -0
  268. package/src/schema-key.ts +63 -0
  269. package/src/seo.ts +124 -0
  270. package/src/serialize.ts +688 -0
  271. package/src/skipped.ts +35 -0
  272. package/src/slug.ts +121 -0
  273. package/src/text-styles.ts +62 -0
  274. 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
+ };