efficient-easel 18.2.0 → 19.0.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 (114) hide show
  1. package/README.md +26 -1
  2. package/dist/{CanvasDragLayer-dZCPbQCP.d.ts → CanvasDragLayer-ARfaivCu.d.ts} +21 -3
  3. package/dist/{host-epHo7r8P.d.ts → ScreenDocumentContext-D55vIrG3.d.ts} +208 -208
  4. package/dist/autosave/index.d.ts +4 -4
  5. package/dist/{canvas-typing-BsNigaCa.d.ts → canvas-typing-Be4FO4q7.d.ts} +5 -5
  6. package/dist/chrome/chrome.css +2 -18
  7. package/dist/chrome/index.d.ts +5 -39
  8. package/dist/chrome/index.js +1 -1
  9. package/dist/{chunk-C2RHV42O.js → chunk-23DYNW6S.js} +1 -1
  10. package/dist/chunk-324PHMLC.js +8 -0
  11. package/dist/{chunk-HGV3FWOB.js → chunk-5DPIGWUL.js} +1 -1
  12. package/dist/{chunk-YLQYEDUQ.js → chunk-6DNDTEF6.js} +1 -1
  13. package/dist/chunk-AJSDWWJW.js +1 -0
  14. package/dist/chunk-C56MPJBH.js +1 -0
  15. package/dist/{chunk-N5AN2RF2.js → chunk-CM732ENS.js} +1 -1
  16. package/dist/{chunk-QEAUF632.js → chunk-CPFDJMFD.js} +12 -12
  17. package/dist/{chunk-S4AWJX4Q.js → chunk-EZG3TGQ7.js} +1 -1
  18. package/dist/chunk-EZKUTVQP.js +62 -0
  19. package/dist/{chunk-XHHVMAGM.js → chunk-FGYJ5DJY.js} +1 -1
  20. package/dist/chunk-J4MHNAED.js +5 -0
  21. package/dist/{chunk-VEZPDGDQ.js → chunk-JBOSEHIX.js} +1 -1
  22. package/dist/chunk-JC34NH4G.js +1 -0
  23. package/dist/chunk-KRL3IUV3.js +1 -0
  24. package/dist/chunk-LNHT2KPX.js +2 -0
  25. package/dist/{chunk-SNB4LQMK.js → chunk-LUBJKUVV.js} +1 -1
  26. package/dist/chunk-M62P4IQK.js +2 -0
  27. package/dist/chunk-MS2U6MV5.js +1 -0
  28. package/dist/{chunk-7TOQCTHV.js → chunk-PY7OOQEZ.js} +3 -3
  29. package/dist/{chunk-KG7RJWOK.js → chunk-RYNBSKE7.js} +1 -1
  30. package/dist/{chunk-XDZHKYQX.js → chunk-SINAM2IT.js} +1 -1
  31. package/dist/{chunk-DXROGLB5.js → chunk-SP3TRDKA.js} +1 -1
  32. package/dist/{chunk-X52NFKUS.js → chunk-T5WDR5IE.js} +1 -1
  33. package/dist/chunk-WMQKV3DK.js +7 -0
  34. package/dist/chunk-Z2V3TTAR.js +18 -0
  35. package/dist/chunk-ZG6PVMC3.js +19 -0
  36. package/dist/copy/index.d.ts +7 -7
  37. package/dist/copy/index.js +29 -29
  38. package/dist/drag/drag.css +23 -0
  39. package/dist/drag/index.d.ts +1 -1
  40. package/dist/drag/index.js +1 -1
  41. package/dist/easel/index.d.ts +2 -2
  42. package/dist/easel/index.js +1 -1
  43. package/dist/fields/fields.css +3 -3
  44. package/dist/fields/index.d.ts +1 -1
  45. package/dist/panel/index.d.ts +17 -84
  46. package/dist/panel/index.js +13 -19
  47. package/dist/panel/panel.css +41 -137
  48. package/dist/{panelWidths-D9hTLR25.d.ts → panelWidths-DgQXiPXU.d.ts} +34 -6
  49. package/dist/screen/components/AddBlockRow.js +1 -1
  50. package/dist/screen/components/AddSidePanel.js +1 -1
  51. package/dist/screen/components/MediaPopup.d.ts +21 -0
  52. package/dist/screen/components/MediaPopup.js +1 -0
  53. package/dist/screen/components/NodeContextMenu.d.ts +4 -4
  54. package/dist/screen/components/NodeContextMenu.js +1 -1
  55. package/dist/screen/components/PageEditGuardNotices.js +1 -1
  56. package/dist/screen/components/SaveControls.d.ts +6 -6
  57. package/dist/screen/components/SaveControls.js +1 -1
  58. package/dist/screen/components/ScreenChrome.js +1 -1
  59. package/dist/screen/components/ScreenDocumentContext.d.ts +1 -1
  60. package/dist/screen/components/ScreenDocumentContext.js +1 -1
  61. package/dist/screen/components/ScreenMount.d.ts +8 -8
  62. package/dist/screen/components/ScreenMount.js +1 -1
  63. package/dist/screen/components/ScreenStoreProvider.d.ts +7 -7
  64. package/dist/screen/components/ScreenStoreProvider.js +1 -1
  65. package/dist/screen/components/SectionScreenShell.js +17 -16
  66. package/dist/screen/components/SidePanelHead.js +1 -1
  67. package/dist/screen/components/panel-widths.d.ts +7 -6
  68. package/dist/screen/components/panel-widths.js +1 -1
  69. package/dist/screen/components/usePartBridge.d.ts +7 -7
  70. package/dist/screen/components/usePartBridge.js +1 -1
  71. package/dist/screen/index.d.ts +5 -5
  72. package/dist/screen/index.js +1 -1
  73. package/dist/screen/lib/document-addressing.d.ts +1 -1
  74. package/dist/screen/lib/host-inbound.js +1 -1
  75. package/dist/screen/lib/host.d.ts +5 -5
  76. package/dist/screen/lib/key-scope.js +1 -1
  77. package/dist/screen/lib/media-panel.d.ts +74 -37
  78. package/dist/screen/lib/media-panel.js +1 -1
  79. package/dist/screen/lib/media-picker.d.ts +5 -5
  80. package/dist/screen/lib/media-picker.js +1 -1
  81. package/dist/screen/lib/puck/device-fields.js +1 -1
  82. package/dist/screen/lib/puck/style-clipboard.d.ts +7 -7
  83. package/dist/screen/lib/puck/style-clipboard.js +1 -1
  84. package/dist/screen/lib/puck/use-section-ops.js +1 -1
  85. package/dist/screen/lib/save-status.d.ts +2 -2
  86. package/dist/screen/lib/screen-parts.d.ts +52 -11
  87. package/dist/screen/lib/screen-parts.js +1 -1
  88. package/dist/screen/screen.css +5 -4
  89. package/dist/screen/words.d.ts +3 -0
  90. package/dist/screen/words.js +1 -1
  91. package/dist/strings/index.d.ts +6 -6
  92. package/dist/strings/index.js +2 -2
  93. package/dist/{types-DT8ywrFQ.d.ts → types-B3iSHX-w.d.ts} +2 -22
  94. package/dist/{words-zH1H0WFw.d.ts → words-BeX5eGq7.d.ts} +2 -2
  95. package/dist/words-BpTjCfqO.d.ts +89 -0
  96. package/dist/{words-DkWcK1rh.d.ts → words-Cm8PMYn0.d.ts} +1 -1
  97. package/dist/{words-BpiVrsW5.d.ts → words-DYu4iBrR.d.ts} +1 -21
  98. package/package.json +8 -8
  99. package/dist/chunk-2ZWA6ARJ.js +0 -8
  100. package/dist/chunk-5EZ5Z3K7.js +0 -7
  101. package/dist/chunk-B4JN227A.js +0 -1
  102. package/dist/chunk-GWIVPLOX.js +0 -23
  103. package/dist/chunk-LIRNQXAY.js +0 -1
  104. package/dist/chunk-LLDHIBYG.js +0 -18
  105. package/dist/chunk-OBIL7BZO.js +0 -4
  106. package/dist/chunk-R6WTQWAC.js +0 -2
  107. package/dist/chunk-SEVGZP36.js +0 -62
  108. package/dist/chunk-STWSCR25.js +0 -2
  109. package/dist/chunk-U277UX5I.js +0 -1
  110. package/dist/chunk-W6SDWCOL.js +0 -5
  111. package/dist/chunk-Z6PVRXVY.js +0 -1
  112. package/dist/screen/components/MediaSidePanel.d.ts +0 -49
  113. package/dist/screen/components/MediaSidePanel.js +0 -1
  114. package/dist/words-D-yhjdOg.d.ts +0 -183
package/README.md CHANGED
@@ -48,7 +48,7 @@ efficient-shell's components (`efficient-shell/ui`): they draw in the shell's
48
48
  Tailwind classes, which **the host's Tailwind compiles from the shell's dist**
49
49
  (the `@source` lines in its `ui.css`). Easel itself emits no Tailwind class.
50
50
  The exported names hosts import (`EaselIconButton`, `SettingsSwitch`,
51
- `SettingsSegmented`, `SettingsTabs`, `FieldText`, `FieldNumber`, …) stay, as
51
+ `SettingsSegmented`, `FieldText`, `FieldNumber`, …) stay, as
52
52
  thin wrappers with the props they had.
53
53
 
54
54
  `tests/own-ui-ratchet.test.ts` holds the named primitives: since 1.0.0 none
@@ -307,6 +307,12 @@ it, so a **server-rendered** editor writes the widths through the CSSOM instead:
307
307
  Until that runs, chrome.css draws the panels at `DEFAULT_PANEL_W`. See
308
308
  [Server rendering and a strict Content-Security-Policy](#server-rendering-and-a-strict-content-security-policy).
309
309
 
310
+ **Each side panel has its own floor (ruling A4g).** `MIN_PANEL_W_BY_SIDE`
311
+ is `{ left: 240, right: 220 }`; the ceiling is `MAX_PANEL_W` (560) on both.
312
+ Clamp with the side, `clampPanelWidth(width, 'left')`, and hand each
313
+ `PanelResizeHandle` its side's `min`. `MIN_PANEL_W` (220) is `@deprecated`: it
314
+ is only the right floor now.
315
+
310
316
  **The inspector head's words, and the body's inset (9.0.0).** `headWords()`
311
317
  gives the head's two lines the same way in both editors: line 1 is the thing's
312
318
  own name (its Layers row's word), line 2 its level.
@@ -1125,6 +1131,25 @@ backend (`defaultBackend`) and its own reading of its client's errors
1125
1131
  by the host a field asks through; the Add panel lists its own session's saved
1126
1132
  sections.
1127
1133
 
1134
+ The media library is the host's, shown as ONE popup (harness UI-20, owner
1135
+ decision 2026-10-23; `screen/lib/media-panel`, `components/MediaPopup`). The
1136
+ host hands its component to `MediaPanelProvider` (`panel`); the screen draws it
1137
+ once at its root with `{ pick, browsing, onPick, onClose }` and the host draws
1138
+ it as its popup. A field's `requestMediaPick` opens it for that field; the
1139
+ rail's Media opens it to browse (`useOpenMediaBrowse`); `onClose` leaves the
1140
+ field as it was. There is no media view in the left panel. A host with no
1141
+ popup still gets `storefront-editor:open-media-picker`.
1142
+
1143
+ A file dragged out of that popup (the shell's `MEDIA_FILE_DRAG_TYPE`) is a
1144
+ drag in the screen's own session, like an Add tile, when the layer says what a
1145
+ picture is (`ScreenCanvasDrag.media`, a `ScreenMediaDrop`): between things it
1146
+ becomes `create(file, maker)` at the places Add offers `adds`; held over a
1147
+ node the layer `takes`, that node is outlined ("Replace image") and the drop
1148
+ writes `replace(node, file)`. Before either, the file goes through the host's
1149
+ pick path when the host gives one (`MediaPanelProvider` `place`): a website
1150
+ takes a file another website holds by copying it in, as a pick of it does.
1151
+ `CanvasDragLayer` takes the drop on a node through `dropOn`.
1152
+
1128
1153
  Its Layers tree is the layer's to frame (0.103.0). `pinned.isFirst` and
1129
1154
  `isLast` say which node is pinned where the type alone does not (the campaign
1130
1155
  designer's Header and Footer are sections, told apart by `_region`);
@@ -122,9 +122,13 @@ declare function canvasTarget(zones: readonly CanvasZone[], x: number, y: number
122
122
  * two, and after its last (owner 2026-10-14) — for a list that HAS children
123
123
  * (an empty one draws its cell instead). Across a 'y' list, down an 'x' one.
124
124
  * Between two children the band is the room they leave. At an end it is the
125
- * room between the list's edge and its child, centred there but never closer
126
- * than `inset` to the edge: a ➕ half outside its list is cut by the canvas, and
127
- * focusing it scrolls the canvas to show it.
125
+ * room between the list's edge and its child, centred there and pushed toward
126
+ * `inset` from the edge (a ➕ half outside its list can be cut by the canvas) —
127
+ * but never past the child's own edge. A list only as tall as its blocks (an
128
+ * email column, a centred row) has no room at its ends: the ➕ sits ON the
129
+ * block's edge, never across the block (Lewis 2026-10-23: the line drawn
130
+ * through a button's words). Across the list, the band spans the block or
131
+ * blocks it sits next to, never the empty room beside them.
128
132
  */
129
133
  declare function insertSeams(zone: CanvasZone, label?: string, inset?: number): CanvasSeam[];
130
134
  declare function seamsBetween(zone: CanvasZone, keyPrefix?: string): CanvasSeam[];
@@ -178,6 +182,20 @@ type CanvasDragLayerProps<I> = {
178
182
  * its answer may have changed: the handle is picked again with it.
179
183
  */
180
184
  choose?: (under: readonly CanvasSource<I>[]) => CanvasSource<I> | null;
185
+ /**
186
+ * A drop ON a thing rather than between things (the editors' media popup,
187
+ * harness UI-20, 2026-10-23: a picture dropped on an image replaces it).
188
+ * While a drag is over a thing under the pointer that `accepts` the item —
189
+ * the innermost one that does — its box is outlined with `label` and the
190
+ * drop goes to `onDrop` with it; anywhere else the drop is the ordinary one
191
+ * between things. Absent: every drop is between things, as before.
192
+ */
193
+ dropOn?: {
194
+ accepts: (item: I, source: CanvasSource<I>) => boolean;
195
+ onDrop: (item: I, source: CanvasSource<I>) => void;
196
+ /** What the outline says the drop does ("Replace image"). */
197
+ label?: (item: I, source: CanvasSource<I>) => string;
198
+ };
181
199
  /** The handle CLICKED, not dragged: select what it names. Absent: a click does nothing. */
182
200
  onPick?: (item: I) => void;
183
201
  /** "+ Add section" in a seam: open the Add panel at that place. */
@@ -1,12 +1,12 @@
1
1
  import * as react from 'react';
2
- import { RefObject, ReactNode } from 'react';
2
+ import { ReactNode, RefObject } from 'react';
3
3
  import { Data } from './blocks/index.js';
4
4
  import { EaselDocumentAdapter, EaselCapabilities } from './screen/lib/document-contract.js';
5
- import { SessionBackend, SessionBlocks } from './screen/lib/session.js';
6
5
  import { SaveStatus } from './screen/lib/document-switch.js';
7
6
  import { BridgedCapabilities } from './screen/lib/admin-bridge.js';
8
7
  import { EditAccess, SaveConflict } from './edit-lock/index.js';
9
8
  import { A as AutosaveSnapshot } from './engine-CvXpFE86.js';
9
+ import { SessionBackend, SessionBlocks } from './screen/lib/session.js';
10
10
 
11
11
  /**
12
12
  * lib/document-addressing.ts — HOW A DOCUMENT IS ADDRESSED: what the
@@ -138,212 +138,6 @@ type AnyDocumentAddressing = DocumentAddressing<ScreenTarget> | DocumentAddressi
138
138
  */
139
139
  declare const NO_DOCUMENT_ADDRESSING: DocumentAddressing<AnyScreenTarget>;
140
140
 
141
- type ScreenTarget = {
142
- kind: "page";
143
- slug: string;
144
- siteId?: string;
145
- } | {
146
- kind: "template";
147
- id: string;
148
- pageSlug?: string;
149
- };
150
- /**
151
- * A DOCUMENT OF A HOST'S OWN — not a website's page or template: what the
152
- * campaign designer opens (0.103.0). `record` is the record that stores it (a
153
- * campaign's id), and `slug` the document within that record (one of its
154
- * variants), the empty slug when it names none.
155
- *
156
- * The provider asks no more of it than that. One record is one adapter (it
157
- * re-keys on `record`, as a page target re-keys on its site); two of these are
158
- * the same document when record and slug match; and a document that has gone
159
- * is reported, never replaced by a Home, which is a website's.
160
- *
161
- * Beside `ScreenTarget` and not in it: that union is the website's two kinds,
162
- * as its hosts name and build them (and as the storefront publishes them), and
163
- * its host messages carry one. A host's own document reaches the host through
164
- * its address (`EaselHostAddress.show`), and never as `navigated`.
165
- */
166
- type DocumentTarget = {
167
- kind: "document";
168
- record: string;
169
- slug?: string;
170
- };
171
- /** Every target the document's provider opens: a website's page or template
172
- * (`ScreenTarget`), or a document of a host's own (`DocumentTarget`). */
173
- type AnyScreenTarget = ScreenTarget | DocumentTarget;
174
-
175
- type ScreenDocumentValue = {
176
- /** The document the editor is currently pointed at (runtime, retargetable). */
177
- target: AnyScreenTarget;
178
- /** Record-type plumbing (load/save/list/create/delete) for the pointed-at
179
- * document. Chrome and panels consult `capabilities`, never the record type. */
180
- adapter: EaselDocumentAdapter;
181
- /** What this document type supports — the ONLY sanctioned mode switch. */
182
- capabilities: EaselCapabilities;
183
- /** Id of the website template being edited (template mode only). */
184
- templateId: string | null;
185
- /** Slug of the page being edited — the template page's slug in template mode,
186
- * and a host's own document's `slug` (or the empty slug) for one of those. */
187
- currentSlug: string;
188
- /** Kind of the loaded page (`dynamic` drives the product-binding controls), or
189
- * null while loading / in template mode where the record carries no kind. */
190
- currentKind: string | null;
191
- /** Id of the website this page belongs to (page mode only; null in template
192
- * mode or when the editor was opened without a site). The list/save paths and
193
- * the page switcher scope to this so they act on one site's pages, not the
194
- * whole tenant's. */
195
- siteId: string | null;
196
- /** Whether the editor's backend may be called yet. Under the EFFICIENT backend
197
- * that is "the console's token has arrived"; an embedder's own backend
198
- * authorises its calls itself and is ready from the first render. THE gate
199
- * for anything that goes through the backend — never `token`, which an
200
- * embedder's backend does not have. */
201
- ready: boolean;
202
- /** The bridged admin JWT once it arrives (null while waiting, and always null
203
- * under an embedder's own backend). Only for the callers that still pass it
204
- * to the EFFICIENT client themselves. */
205
- token: string | null;
206
- /** Latest token for imperative reads (custom fields, pickers, panels). */
207
- tokenRef: RefObject<string | null>;
208
- /** Bridge editor events to the embedding admin portal. */
209
- notifyParent: SendToHost;
210
- /** Operator capabilities the ADMIN CONSOLE granted over the origin-gated
211
- * bridge. Distinct from `capabilities` above: those describe the DOCUMENT
212
- * (can it publish, has it site settings), these describe the PERSON, and the
213
- * editor is never allowed to infer them for itself. */
214
- bridgeCapabilities: BridgedCapabilities;
215
- /** Run an authed op; on 401 ask the parent for a fresh token and retry once. */
216
- runAuthed: (op: (token: string) => Promise<unknown>) => Promise<boolean>;
217
- /** Backend id of the loaded document — the identity of what the editor is
218
- * editing right now. Null while loading and between retargets, so it also
219
- * keys any once-per-document work (e.g. the pinned-chrome seed). */
220
- docId: string | null;
221
- /** The document exactly as it was LOADED from the backend: Puck's initial
222
- * state, and the persisted snapshot a load-time seed must measure itself
223
- * against. Null while loading. Edits live in Puck's own store, so this does
224
- * NOT track them — it stays the last-loaded layout for the whole session. */
225
- data: Data | null;
226
- /** The freshest layout the merchant is actually viewing — the latest edit made
227
- * since load (tracked on every onChange, even after the autosave settles), else
228
- * the last-loaded `data`. Lets a feature snapshot the live view (e.g. the
229
- * read-only origin backdrop in footer focus) without reaching into Puck's store.
230
- * Lean accessor (ISP) — never exposes the ref. */
231
- getLiveData: () => Data | null;
232
- error: string | null;
233
- /** True while the template-entry bounce is redirecting to the Home page. */
234
- redirecting: boolean;
235
- /** The pinned-seed gate's four words for where the autosave stands. */
236
- saveStatus: SaveStatus;
237
- /** The autosave's state (efficient-easel/autosave) — what the toolbar pill
238
- * draws, through `saveIndicator`. */
239
- saveState: AutosaveSnapshot;
240
- /** Whether autosave is running. The toolbar pill is a switch: clicking it
241
- * flips this. OFF = edits are HELD (kept in memory, still flushable by Publish
242
- * / navigation / re-enabling) but never written on a timer. Session-scoped —
243
- * survives in-place page switches. */
244
- autoSaveEnabled: boolean;
245
- /** Auto-save stopped because a save was REFUSED, not because the operator
246
- * paused it — so a stopped auto-save can never read as a switch somebody
247
- * flicked themselves. */
248
- autoSaveStoppedByConflict: boolean;
249
- /** True when an edit hasn't been persisted yet — including one held while the
250
- * claim is in flight. Drives the guard that prompts before an in-place
251
- * navigation would abandon held-but-unsaved edits. */
252
- hasUnsavedEdits: boolean;
253
- /** Flip autosave on/off. Turning it back ON writes any held edits at once, so
254
- * "on" always means caught up. */
255
- toggleAutoSave: () => void;
256
- /** "Retry now": a failed save goes again at once instead of waiting out its
257
- * backoff. */
258
- retrySave: () => void;
259
- /** Autosave — hand every Puck onChange to this. */
260
- onChange: (next: Data) => void;
261
- /** Persist the pending edit immediately; resolves to whether nothing is left
262
- * unsaved. */
263
- flushSave: () => Promise<boolean>;
264
- /** Persist + publish the current document (Save for templates). */
265
- onPublish: (next: Data) => Promise<void>;
266
- /** Whether this editor may write the open page, and why not when it may not.
267
- * `editing` is the only writable state; the gate is on the save path itself,
268
- * so hiding or showing chrome from this can't be the thing that protects the
269
- * document. Always `editing` in template mode (no lock there). */
270
- access: EditAccess;
271
- /** SOMEBODY ELSE holds this page, so this editor is read-only: it may look and
272
- * select, but not move, add, duplicate or delete anything — until they take
273
- * control themselves. The ONE answer the chrome asks; no panel re-derives it,
274
- * so none of them can drift.
275
- *
276
- * NOT what protects the document — `persistLayout` refuses the write, and it
277
- * refuses it in more cases than this (while the claim is still `checking`, and
278
- * after a refused save). This is the narrower question the CHROME asks, and it
279
- * is narrower on purpose: during `checking` nobody is named yet, and the
280
- * pinned Header/Footer seed runs in the chrome's mount commit and must still
281
- * land. Its write is held by `onChange` and released by the claim if it is
282
- * granted, or never released if it is not — card 1096.
283
- *
284
- * …or the document itself is FROZEN (`readOnlyReason`): nobody holds it, and
285
- * nobody may write it, ever.
286
- *
287
- * …or a save was REFUSED (`saveConflict`, 0.106.0): the stored document moved
288
- * on under somebody else's save, the autosave has stopped, and nothing this
289
- * editor does is written again until the operator reloads. Until 0.106.0 the
290
- * canvas, the tree and the inspector went on taking edits nothing would save,
291
- * and Undo / Redo went on rewinding them; the old campaign designer locked
292
- * the canvas there, and owner ruling R5 (Victor Mau, 2026-10-09) says the
293
- * screen's Undo / Redo are DISABLED, not hidden, while this is true. The
294
- * operator still reads, selects and copies from the screen the notice warns
295
- * them about. */
296
- readOnly: boolean;
297
- /** Why the open document may never be written, as the backend said it
298
- * (`EaselDocument.readOnlyReason`: a campaign that has been sent), or null
299
- * for every document that may. Such a document is read-only for good: its
300
- * saves are refused, its lock is never claimed, and the save pill's place
301
- * says this instead. Optional for a context built by hand, where it is
302
- * absent and means null. */
303
- readOnlyReason?: string | null;
304
- /** Take the page: from the "someone else is editing" choice, or back after
305
- * somebody took it from you. */
306
- takeOverEditing: () => void;
307
- /** Choose to look without touching (keeps the holder for the ribbon). */
308
- viewReadOnly: () => void;
309
- /** A save the backend REFUSED because the stored page moved on. Non-null means
310
- * autosave has stopped and the operator's work is still only on their screen.
311
- * Told in a bar under the top bar (efficient-easel/chrome `EditNotice`),
312
- * never a dialog, so there is nothing to dismiss: the operator reads and
313
- * copies from the screen it is warning them about. */
314
- saveConflict: SaveConflict | null;
315
- /** The last save failed because the server could not be REACHED (no answer
316
- * at all), not because it answered no (8.0.0). With a failed save, the bar
317
- * says "You're offline" where it would say "Not saved". Optional for a
318
- * context built by hand, where it is absent and means false. */
319
- saveUnreachable?: boolean;
320
- /** Abandon this editor's version and reload what is stored. The ONLY path that
321
- * throws the operator's work away, and only they can ask for it. */
322
- reloadFromServer: () => void;
323
- /** Retarget the editor in place (guards unsaved changes before it swaps). */
324
- navigate: (target: AnyScreenTarget) => void;
325
- /** A retarget held behind the unsaved-changes prompt (null = no prompt). */
326
- pendingNav: AnyScreenTarget | null;
327
- cancelPendingNav: () => void;
328
- discardAndNavigate: () => void;
329
- saveAndNavigate: () => Promise<void>;
330
- };
331
- declare const ScreenDocumentContext: react.Context<ScreenDocumentValue | null>;
332
- declare function useScreenDocument(): ScreenDocumentValue;
333
- declare function ScreenDocumentProvider({ target, addressing: handed, onHostMessage, children, }: {
334
- target: AnyScreenTarget;
335
- /** How this editor's documents are ADDRESSED — the layer's rules, handed in
336
- * by whoever mounts the provider (the website's entry hands the website's:
337
- * lib/website-document-addressing). Without them there is one document that
338
- * nothing stores. ONE object for the life of the mount, like the session. */
339
- addressing?: AnyDocumentAddressing;
340
- /** The layer's own host messages, asked before the ones every editor answers
341
- * (lib/host-inbound): the website's are lib/website-host-inbound.
342
- * Returns true when it handled one. */
343
- onHostMessage?: (raw: unknown) => boolean;
344
- children: ReactNode;
345
- }): react.JSX.Element;
346
-
347
141
  /**
348
142
  * The editor's HOST — whatever embeds the storefront editor, and the one way the
349
143
  * editor talks to it.
@@ -615,4 +409,210 @@ declare function createEaselHost(options: {
615
409
  deliver: (message: HostToEaselMessage) => void;
616
410
  };
617
411
 
412
+ type ScreenTarget = {
413
+ kind: "page";
414
+ slug: string;
415
+ siteId?: string;
416
+ } | {
417
+ kind: "template";
418
+ id: string;
419
+ pageSlug?: string;
420
+ };
421
+ /**
422
+ * A DOCUMENT OF A HOST'S OWN — not a website's page or template: what the
423
+ * campaign designer opens (0.103.0). `record` is the record that stores it (a
424
+ * campaign's id), and `slug` the document within that record (one of its
425
+ * variants), the empty slug when it names none.
426
+ *
427
+ * The provider asks no more of it than that. One record is one adapter (it
428
+ * re-keys on `record`, as a page target re-keys on its site); two of these are
429
+ * the same document when record and slug match; and a document that has gone
430
+ * is reported, never replaced by a Home, which is a website's.
431
+ *
432
+ * Beside `ScreenTarget` and not in it: that union is the website's two kinds,
433
+ * as its hosts name and build them (and as the storefront publishes them), and
434
+ * its host messages carry one. A host's own document reaches the host through
435
+ * its address (`EaselHostAddress.show`), and never as `navigated`.
436
+ */
437
+ type DocumentTarget = {
438
+ kind: "document";
439
+ record: string;
440
+ slug?: string;
441
+ };
442
+ /** Every target the document's provider opens: a website's page or template
443
+ * (`ScreenTarget`), or a document of a host's own (`DocumentTarget`). */
444
+ type AnyScreenTarget = ScreenTarget | DocumentTarget;
445
+
446
+ type ScreenDocumentValue = {
447
+ /** The document the editor is currently pointed at (runtime, retargetable). */
448
+ target: AnyScreenTarget;
449
+ /** Record-type plumbing (load/save/list/create/delete) for the pointed-at
450
+ * document. Chrome and panels consult `capabilities`, never the record type. */
451
+ adapter: EaselDocumentAdapter;
452
+ /** What this document type supports — the ONLY sanctioned mode switch. */
453
+ capabilities: EaselCapabilities;
454
+ /** Id of the website template being edited (template mode only). */
455
+ templateId: string | null;
456
+ /** Slug of the page being edited — the template page's slug in template mode,
457
+ * and a host's own document's `slug` (or the empty slug) for one of those. */
458
+ currentSlug: string;
459
+ /** Kind of the loaded page (`dynamic` drives the product-binding controls), or
460
+ * null while loading / in template mode where the record carries no kind. */
461
+ currentKind: string | null;
462
+ /** Id of the website this page belongs to (page mode only; null in template
463
+ * mode or when the editor was opened without a site). The list/save paths and
464
+ * the page switcher scope to this so they act on one site's pages, not the
465
+ * whole tenant's. */
466
+ siteId: string | null;
467
+ /** Whether the editor's backend may be called yet. Under the EFFICIENT backend
468
+ * that is "the console's token has arrived"; an embedder's own backend
469
+ * authorises its calls itself and is ready from the first render. THE gate
470
+ * for anything that goes through the backend — never `token`, which an
471
+ * embedder's backend does not have. */
472
+ ready: boolean;
473
+ /** The bridged admin JWT once it arrives (null while waiting, and always null
474
+ * under an embedder's own backend). Only for the callers that still pass it
475
+ * to the EFFICIENT client themselves. */
476
+ token: string | null;
477
+ /** Latest token for imperative reads (custom fields, pickers, panels). */
478
+ tokenRef: RefObject<string | null>;
479
+ /** Bridge editor events to the embedding admin portal. */
480
+ notifyParent: SendToHost;
481
+ /** Operator capabilities the ADMIN CONSOLE granted over the origin-gated
482
+ * bridge. Distinct from `capabilities` above: those describe the DOCUMENT
483
+ * (can it publish, has it site settings), these describe the PERSON, and the
484
+ * editor is never allowed to infer them for itself. */
485
+ bridgeCapabilities: BridgedCapabilities;
486
+ /** Run an authed op; on 401 ask the parent for a fresh token and retry once. */
487
+ runAuthed: (op: (token: string) => Promise<unknown>) => Promise<boolean>;
488
+ /** Backend id of the loaded document — the identity of what the editor is
489
+ * editing right now. Null while loading and between retargets, so it also
490
+ * keys any once-per-document work (e.g. the pinned-chrome seed). */
491
+ docId: string | null;
492
+ /** The document exactly as it was LOADED from the backend: Puck's initial
493
+ * state, and the persisted snapshot a load-time seed must measure itself
494
+ * against. Null while loading. Edits live in Puck's own store, so this does
495
+ * NOT track them — it stays the last-loaded layout for the whole session. */
496
+ data: Data | null;
497
+ /** The freshest layout the merchant is actually viewing — the latest edit made
498
+ * since load (tracked on every onChange, even after the autosave settles), else
499
+ * the last-loaded `data`. Lets a feature snapshot the live view (e.g. the
500
+ * read-only origin backdrop in footer focus) without reaching into Puck's store.
501
+ * Lean accessor (ISP) — never exposes the ref. */
502
+ getLiveData: () => Data | null;
503
+ error: string | null;
504
+ /** True while the template-entry bounce is redirecting to the Home page. */
505
+ redirecting: boolean;
506
+ /** The pinned-seed gate's four words for where the autosave stands. */
507
+ saveStatus: SaveStatus;
508
+ /** The autosave's state (efficient-easel/autosave) — what the toolbar pill
509
+ * draws, through `saveIndicator`. */
510
+ saveState: AutosaveSnapshot;
511
+ /** Whether autosave is running. The toolbar pill is a switch: clicking it
512
+ * flips this. OFF = edits are HELD (kept in memory, still flushable by Publish
513
+ * / navigation / re-enabling) but never written on a timer. Session-scoped —
514
+ * survives in-place page switches. */
515
+ autoSaveEnabled: boolean;
516
+ /** Auto-save stopped because a save was REFUSED, not because the operator
517
+ * paused it — so a stopped auto-save can never read as a switch somebody
518
+ * flicked themselves. */
519
+ autoSaveStoppedByConflict: boolean;
520
+ /** True when an edit hasn't been persisted yet — including one held while the
521
+ * claim is in flight. Drives the guard that prompts before an in-place
522
+ * navigation would abandon held-but-unsaved edits. */
523
+ hasUnsavedEdits: boolean;
524
+ /** Flip autosave on/off. Turning it back ON writes any held edits at once, so
525
+ * "on" always means caught up. */
526
+ toggleAutoSave: () => void;
527
+ /** "Retry now": a failed save goes again at once instead of waiting out its
528
+ * backoff. */
529
+ retrySave: () => void;
530
+ /** Autosave — hand every Puck onChange to this. */
531
+ onChange: (next: Data) => void;
532
+ /** Persist the pending edit immediately; resolves to whether nothing is left
533
+ * unsaved. */
534
+ flushSave: () => Promise<boolean>;
535
+ /** Persist + publish the current document (Save for templates). */
536
+ onPublish: (next: Data) => Promise<void>;
537
+ /** Whether this editor may write the open page, and why not when it may not.
538
+ * `editing` is the only writable state; the gate is on the save path itself,
539
+ * so hiding or showing chrome from this can't be the thing that protects the
540
+ * document. Always `editing` in template mode (no lock there). */
541
+ access: EditAccess;
542
+ /** SOMEBODY ELSE holds this page, so this editor is read-only: it may look and
543
+ * select, but not move, add, duplicate or delete anything — until they take
544
+ * control themselves. The ONE answer the chrome asks; no panel re-derives it,
545
+ * so none of them can drift.
546
+ *
547
+ * NOT what protects the document — `persistLayout` refuses the write, and it
548
+ * refuses it in more cases than this (while the claim is still `checking`, and
549
+ * after a refused save). This is the narrower question the CHROME asks, and it
550
+ * is narrower on purpose: during `checking` nobody is named yet, and the
551
+ * pinned Header/Footer seed runs in the chrome's mount commit and must still
552
+ * land. Its write is held by `onChange` and released by the claim if it is
553
+ * granted, or never released if it is not — card 1096.
554
+ *
555
+ * …or the document itself is FROZEN (`readOnlyReason`): nobody holds it, and
556
+ * nobody may write it, ever.
557
+ *
558
+ * …or a save was REFUSED (`saveConflict`, 0.106.0): the stored document moved
559
+ * on under somebody else's save, the autosave has stopped, and nothing this
560
+ * editor does is written again until the operator reloads. Until 0.106.0 the
561
+ * canvas, the tree and the inspector went on taking edits nothing would save,
562
+ * and Undo / Redo went on rewinding them; the old campaign designer locked
563
+ * the canvas there, and owner ruling R5 (Victor Mau, 2026-10-09) says the
564
+ * screen's Undo / Redo are DISABLED, not hidden, while this is true. The
565
+ * operator still reads, selects and copies from the screen the notice warns
566
+ * them about. */
567
+ readOnly: boolean;
568
+ /** Why the open document may never be written, as the backend said it
569
+ * (`EaselDocument.readOnlyReason`: a campaign that has been sent), or null
570
+ * for every document that may. Such a document is read-only for good: its
571
+ * saves are refused, its lock is never claimed, and the save pill's place
572
+ * says this instead. Optional for a context built by hand, where it is
573
+ * absent and means null. */
574
+ readOnlyReason?: string | null;
575
+ /** Take the page: from the "someone else is editing" choice, or back after
576
+ * somebody took it from you. */
577
+ takeOverEditing: () => void;
578
+ /** Choose to look without touching (keeps the holder for the ribbon). */
579
+ viewReadOnly: () => void;
580
+ /** A save the backend REFUSED because the stored page moved on. Non-null means
581
+ * autosave has stopped and the operator's work is still only on their screen.
582
+ * Told in a bar under the top bar (efficient-easel/chrome `EditNotice`),
583
+ * never a dialog, so there is nothing to dismiss: the operator reads and
584
+ * copies from the screen it is warning them about. */
585
+ saveConflict: SaveConflict | null;
586
+ /** The last save failed because the server could not be REACHED (no answer
587
+ * at all), not because it answered no (8.0.0). With a failed save, the bar
588
+ * says "You're offline" where it would say "Not saved". Optional for a
589
+ * context built by hand, where it is absent and means false. */
590
+ saveUnreachable?: boolean;
591
+ /** Abandon this editor's version and reload what is stored. The ONLY path that
592
+ * throws the operator's work away, and only they can ask for it. */
593
+ reloadFromServer: () => void;
594
+ /** Retarget the editor in place (guards unsaved changes before it swaps). */
595
+ navigate: (target: AnyScreenTarget) => void;
596
+ /** A retarget held behind the unsaved-changes prompt (null = no prompt). */
597
+ pendingNav: AnyScreenTarget | null;
598
+ cancelPendingNav: () => void;
599
+ discardAndNavigate: () => void;
600
+ saveAndNavigate: () => Promise<void>;
601
+ };
602
+ declare const ScreenDocumentContext: react.Context<ScreenDocumentValue | null>;
603
+ declare function useScreenDocument(): ScreenDocumentValue;
604
+ declare function ScreenDocumentProvider({ target, addressing: handed, onHostMessage, children, }: {
605
+ target: AnyScreenTarget;
606
+ /** How this editor's documents are ADDRESSED — the layer's rules, handed in
607
+ * by whoever mounts the provider (the website's entry hands the website's:
608
+ * lib/website-document-addressing). Without them there is one document that
609
+ * nothing stores. ONE object for the life of the mount, like the session. */
610
+ addressing?: AnyDocumentAddressing;
611
+ /** The layer's own host messages, asked before the ones every editor answers
612
+ * (lib/host-inbound): the website's are lib/website-host-inbound.
613
+ * Returns true when it handled one. */
614
+ onHostMessage?: (raw: unknown) => boolean;
615
+ children: ReactNode;
616
+ }): react.JSX.Element;
617
+
618
618
  export { type AnyDocumentAddressing as A, DETACHED_EASEL_HOST as D, type EaselHost as E, type HostPickedMedia as H, type InboundHostMessage as I, NO_DOCUMENT_ADDRESSING as N, type PickedMediaSource as P, type SectionScreenTarget as S, type DocumentAddressing as a, type EaselHostAddress as b, EaselHostContext as c, EaselHostProvider as d, type EaselMessageType as e, type EaselToHostMessage as f, type HostToEaselMessage as g, type SendToHost as h, createEaselHost as i, useHostEscape as j, type AnyScreenTarget as k, type DocumentTarget as l, ScreenDocumentContext as m, ScreenDocumentProvider as n, type ScreenDocumentValue as o, type ScreenTarget as p, useScreenDocument as q, useEaselHost as u };
@@ -1,13 +1,13 @@
1
1
  import { a as Autosave, A as AutosaveSnapshot, b as AutosaveOptions } from '../engine-CvXpFE86.js';
2
2
  export { c as AutosaveClock, d as createAutosave } from '../engine-CvXpFE86.js';
3
- import { S as SaveIndicator } from '../words-DkWcK1rh.js';
4
- export { a as SaveWords, s as saveIndicator } from '../words-DkWcK1rh.js';
5
- import '../types-DT8ywrFQ.js';
3
+ import { S as SaveIndicator } from '../words-Cm8PMYn0.js';
4
+ export { a as SaveWords, s as saveIndicator } from '../words-Cm8PMYn0.js';
5
+ import '../types-B3iSHX-w.js';
6
6
  import '../types-Cy7rdmXt.js';
7
7
  import '../wordFit-7XpmhcaW.js';
8
8
  import 'react';
9
9
  import '../edit-lock/index.js';
10
- import '../panelWidths-D9hTLR25.js';
10
+ import '../panelWidths-DgQXiPXU.js';
11
11
 
12
12
  type UseAutosaveOptions<T> = Omit<AutosaveOptions<T>, 'save' | 'onFailure'> & {
13
13
  /** Write one document. Read at call time, so it may close over current props. */
@@ -1,8 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { MarkStyle, WrapResult, InlineRule, MarkupTool } from 'efficient-shell/markup';
4
- import { C as CopyWords, a as CopyAlign } from './words-D-yhjdOg.js';
5
- import { TextToolbarCase, TextToolbarMark } from 'efficient-shell/ui';
4
+ import { TextAlign, TextToolbarCase, TextToolbarMark } from 'efficient-shell/ui';
5
+ import { C as CopyWords } from './words-BpTjCfqO.js';
6
6
 
7
7
  type CellWords = NonNullable<CopyWords['cells']>;
8
8
  interface CopyCell {
@@ -177,13 +177,13 @@ interface CopyBoxLastTool {
177
177
  * `A` is inferred from `allow`, so nothing has to be spelled at the call site.
178
178
  *
179
179
  * ⚠️ IT DOES NOT FORBID A WIDER HANDLER, and cannot: a function taking the
180
- * whole `CopyAlign` is assignable where one taking three is wanted, because
180
+ * whole `TextAlign` is assignable where one taking three is wanted, because
181
181
  * parameters are contravariant — and that direction is SAFE, since only the
182
182
  * three can ever arrive. What this buys is that the narrow handler now
183
183
  * type-checks without a cast, which is what makes writing the safe one the
184
184
  * path of least resistance.
185
185
  */
186
- interface CopyBoxAlign<A extends CopyAlign = CopyAlign> {
186
+ interface CopyBoxAlign<A extends TextAlign = TextAlign> {
187
187
  value: A | '';
188
188
  onChange: (next: A) => void;
189
189
  /**
@@ -241,7 +241,7 @@ interface CopyBoxProps {
241
241
  */
242
242
  actions?: ReactNode;
243
243
  /** The alignment strip. Omitted = no strip. */
244
- align?: CopyBoxAlign<CopyAlign>;
244
+ align?: CopyBoxAlign<TextAlign>;
245
245
  /** @deprecated Not drawn: the toolbar's drawings are the shell set's.
246
246
  * Removed in 12.0.0. */
247
247
  glyphs?: Partial<Record<MarkStyle, ReactNode>>;
@@ -248,8 +248,8 @@
248
248
  background: var(--es-panel);
249
249
  overflow: hidden;
250
250
  position: relative;
251
- /* A size container: a panel is DRAGGED between 220 and 560, so its contents
252
- ask the panel's width, never the screen's. */
251
+ /* A size container: a panel is DRAGGED between its floor (left 240, right
252
+ 220) and 560, so its contents ask the panel's width, never the screen's. */
253
253
  container-type: inline-size;
254
254
  }
255
255
  /* THE PANEL'S SIDE INSET, ONE HOME: 16, AND 12 UNDER A 260px PANEL (one
@@ -403,9 +403,6 @@ span:has(> .es-insp-up-btn) {
403
403
  grid-column: 2;
404
404
  justify-self: start;
405
405
  }
406
- .es-insp-head:has(.es-insp-textbtn) .es-insp-divider {
407
- display: none;
408
- }
409
406
  }
410
407
  .es-insp-chip {
411
408
  width: 26px;
@@ -474,19 +471,6 @@ span:has(> .es-insp-up-btn) {
474
471
  flex: none;
475
472
  margin-left: auto;
476
473
  }
477
- /* A side panel's head has two pieces besides its glyph buttons (MediaPanelHead,
478
- card [MERGE] One Media Panel Head, 2026-10-06; until then each editor's own
479
- sheet drew them, as `.sf-head-*` and `.cd-head-*`). A MODE's way out is a
480
- word ("Cancel", "Done") on the shell's small Button (`.es-insp-textbtn`
481
- names it for the narrow layout above); the hairline parts the
482
- panel's own tools from the Hide panel button that ends every head. */
483
- .es-insp-divider {
484
- width: 1px;
485
- height: 16px;
486
- margin: 0 4px;
487
- background: var(--es-line);
488
- flex: none;
489
- }
490
474
 
491
475
  /* ── Edit notice — somebody else is in here, or a save was refused ───────── */
492
476
  /* A bar under the top bar, never a dialog (blueprint 2.5): the operator has to