efficient-easel 0.99.1 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -36
- package/dist/autosave/index.d.ts +12 -6
- package/dist/blocks/index.d.ts +2 -2
- package/dist/canvas/index.d.ts +3 -3
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/chrome.css +96 -29
- package/dist/chrome/index.d.ts +72 -19
- package/dist/chrome/index.js +17 -21
- package/dist/chunk-3EDSDPIP.js +1 -0
- package/dist/chunk-67GNT6PF.js +1 -0
- package/dist/{chunk-PI26YXYD.js → chunk-6EIVTKZZ.js} +21 -14
- package/dist/chunk-A5UK76FC.js +6 -0
- package/dist/chunk-AZT7V4UM.js +10 -0
- package/dist/chunk-DICUMYMG.js +12 -0
- package/dist/chunk-HDVD27VB.js +1 -0
- package/dist/chunk-JQM3V7XQ.js +1 -0
- package/dist/{chunk-TDA4HH54.js → chunk-JUQ6YJCG.js} +1 -1
- package/dist/chunk-LA54QLP5.js +1 -0
- package/dist/chunk-LGKQXSKO.js +16 -0
- package/dist/chunk-M6F6LS3H.js +1 -0
- package/dist/chunk-MH5GUZFQ.js +9 -0
- package/dist/chunk-P6O363EE.js +6 -0
- package/dist/chunk-PAELDYYS.js +1 -0
- package/dist/{chunk-JYPOOHII.js → chunk-QKVR3K73.js} +3 -3
- package/dist/chunk-SAJUCHOX.js +1 -0
- package/dist/chunk-TRVZ7C23.js +1 -0
- package/dist/chunk-V7GIIXFB.js +16 -0
- package/dist/{chunk-IBOFSS56.js → chunk-WZ7TGAAT.js} +1 -1
- package/dist/chunk-ZJEBF4NB.js +1 -0
- package/dist/copy/copy.css +388 -5
- package/dist/copy/index.d.ts +271 -8
- package/dist/copy/index.js +31 -10
- package/dist/data/index.d.ts +4 -4
- package/dist/drag/drag.css +10 -1
- package/dist/drag/index.d.ts +102 -11
- package/dist/drag/index.js +18 -17
- package/dist/{editor/editor.css → easel/easel.css} +16 -1
- package/dist/{editor → easel}/index.d.ts +146 -51
- package/dist/easel/index.js +9 -0
- package/dist/edit-lock/index.d.ts +3 -3
- package/dist/edit-lock/index.js +1 -1
- package/dist/fields/fields.css +103 -26
- package/dist/fields/index.d.ts +51 -13
- package/dist/fields/index.js +30 -46
- package/dist/gold/index.js +1 -1
- package/dist/icons/index.d.ts +10 -1
- package/dist/icons/index.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/markup/index.d.ts +94 -10
- package/dist/markup/index.js +1 -1
- package/dist/menu/index.d.ts +61 -6
- package/dist/menu/index.js +15 -15
- package/dist/menu/menu.css +17 -6
- package/dist/node-ops/index.d.ts +9 -9
- package/dist/node-ops/index.js +1 -1
- package/dist/panel/index.d.ts +73 -5
- package/dist/panel/index.js +18 -16
- package/dist/panel/panel.css +56 -1
- package/dist/pick/index.d.ts +42 -4
- package/dist/pick/index.js +7 -14
- package/dist/pick/pick.css +14 -0
- package/dist/rail/index.d.ts +6 -6
- package/dist/rail/index.js +1 -1
- package/dist/rail/rail.css +1 -1
- package/dist/strings/catalogue.d.ts +24 -40
- package/dist/strings/catalogue.js +1 -1
- package/dist/strings/index.d.ts +25 -23
- package/dist/strings/index.js +2 -2
- package/dist/style-clipboard/index.d.ts +5 -5
- package/dist/style-clipboard/index.js +1 -1
- package/dist/text-styles/index.d.ts +328 -0
- package/dist/text-styles/index.js +38 -0
- package/dist/text-styles/text-styles.css +309 -0
- package/dist/tree/index.d.ts +44 -3
- package/dist/tree/index.js +1 -1
- package/dist/tree/tree.css +71 -0
- package/dist/{types-CnRzp9oc.d.ts → types-DGcCsxs8.d.ts} +79 -12
- package/dist/{types-DNrn_w4V.d.ts → types-DWS-s9z-.d.ts} +10 -10
- package/dist/{types-pGBXevBU.d.ts → types-Djdq6Xkd.d.ts} +15 -8
- package/dist/{types-Dy6oR6Z2.d.ts → types-E3HrTPg2.d.ts} +20 -14
- package/dist/{types-C7HFvdtk.d.ts → types-VqL6t5HH.d.ts} +82 -5
- package/dist/{watch-DmasQA61.d.ts → watch-BTYOJpQU.d.ts} +19 -2
- package/dist/{wordFit-9B9nqyf3.d.ts → wordFit-D4S36PvB.d.ts} +1 -1
- package/dist/{words-DagJdwIA.d.ts → words-AHHfiaRY.d.ts} +31 -2
- package/dist/{words-D9ChvbNC.d.ts → words-BDignMl_.d.ts} +1 -1
- package/dist/{words-Cqmyey0s.d.ts → words-BMzV04b1.d.ts} +2 -2
- package/dist/words-CJjUKcV5.d.ts +37 -0
- package/dist/words-CRYvAiHX.d.ts +105 -0
- package/dist/{words-B2YLaKEr.d.ts → words-CnnHb9jo.d.ts} +1 -1
- package/dist/{words-CD3WADyb.d.ts → words-CzoyDNHm.d.ts} +4 -0
- package/dist/{words-bOMsUJk9.d.ts → words-DBq7vfHy.d.ts} +59 -6
- package/dist/{words-DOhF-4D4.d.ts → words-DMG4dYq2.d.ts} +1 -1
- package/dist/{words-BK-npta-.d.ts → words-DgHcY5qB.d.ts} +10 -4
- package/dist/{words-B87GIFWH.d.ts → words-is_Vy6hn.d.ts} +1 -1
- package/package.json +14 -8
- package/dist/chunk-66CKSTXP.js +0 -14
- package/dist/chunk-67VZRQLG.js +0 -13
- package/dist/chunk-6PS6STGA.js +0 -1
- package/dist/chunk-7A7O2JWQ.js +0 -1
- package/dist/chunk-H4B72XCJ.js +0 -1
- package/dist/chunk-JE63JHKO.js +0 -1
- package/dist/chunk-KDIVA6WD.js +0 -1
- package/dist/chunk-MCLBHENY.js +0 -1
- package/dist/chunk-QB3BZIRA.js +0 -5
- package/dist/chunk-SJQG5ENJ.js +0 -1
- package/dist/chunk-SUO5627P.js +0 -1
- package/dist/chunk-ZT7M24XD.js +0 -1
- package/dist/editor/index.js +0 -9
- /package/dist/{chunk-GGTPNGFJ.js → chunk-AELNNAFM.js} +0 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ it since 0.93.0.
|
|
|
21
21
|
The storefront editor (`efficient-storefront`, Next 16 + Puck) and the campaign
|
|
22
22
|
designer (`efficient-admin-portal`, Vite/React) each hand-rolled their own narrow
|
|
23
23
|
left icon-rail. They had already drifted — button 34px vs 36px, icon 18px vs
|
|
24
|
-
20px, radius 9 vs 8, a CSS-var accent vs a Tailwind one. `
|
|
24
|
+
20px, radius 9 vs 8, a CSS-var accent vs a Tailwind one. `EaselRail` is the one
|
|
25
25
|
rail they converge on.
|
|
26
26
|
|
|
27
27
|
## The leaf
|
|
@@ -36,14 +36,14 @@ it.
|
|
|
36
36
|
// A client component in any editor:
|
|
37
37
|
'use client';
|
|
38
38
|
import {
|
|
39
|
-
|
|
39
|
+
EaselRail,
|
|
40
40
|
PagesIcon, LayersIcon, StylesIcon, MediaIcon, SitemapIcon, AddIcon,
|
|
41
41
|
} from 'efficient-easel/rail';
|
|
42
42
|
import 'efficient-easel/rail.css'; // once, from a client/global entry
|
|
43
43
|
|
|
44
44
|
export function LeftRail({ view, onView, onAdd }) {
|
|
45
45
|
return (
|
|
46
|
-
<
|
|
46
|
+
<EaselRail
|
|
47
47
|
ariaLabel="Left panel views"
|
|
48
48
|
items={[
|
|
49
49
|
{ id: 'pages', label: 'Pages', icon: <PagesIcon />, active: view === 'pages', onSelect: () => onView('pages') },
|
|
@@ -59,14 +59,14 @@ export function LeftRail({ view, onView, onAdd }) {
|
|
|
59
59
|
|
|
60
60
|
## API
|
|
61
61
|
|
|
62
|
-
### `<
|
|
62
|
+
### `<EaselRail items ariaLabel? className? orientation? />`
|
|
63
63
|
|
|
64
64
|
A **pure presentational** toolbar. It owns the look, never the app state — you
|
|
65
65
|
decide which item is `active` and what each `onSelect` does.
|
|
66
66
|
|
|
67
67
|
| prop | type | default | notes |
|
|
68
68
|
|---|---|---|---|
|
|
69
|
-
| `items` | `
|
|
69
|
+
| `items` | `EaselRailItem[]` | — | buttons, in render order |
|
|
70
70
|
| `ariaLabel` | `string` | `"Editor"` | names the toolbar for assistive tech |
|
|
71
71
|
| `className` | `string` | — | merged onto `.es-rail` |
|
|
72
72
|
| `orientation` | `'column' \| 'row'` | `'column'` | which way the buttons run |
|
|
@@ -85,17 +85,17 @@ changes. A row is 34px tall, so a 52px bar centres it with 9px to spare.
|
|
|
85
85
|
|
|
86
86
|
```tsx
|
|
87
87
|
// The same rail, in the top bar's empty middle.
|
|
88
|
-
<
|
|
88
|
+
<EaselRail orientation="row" ariaLabel="Editor views" items={items} />
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
`aria-orientation` follows the layout (`vertical` / `horizontal`); the `toolbar`
|
|
92
92
|
role does not change, because a rail that mixes view toggles with plain actions
|
|
93
93
|
is a toolbar whichever way it runs.
|
|
94
94
|
|
|
95
|
-
### `
|
|
95
|
+
### `EaselRailItem`
|
|
96
96
|
|
|
97
97
|
```ts
|
|
98
|
-
interface
|
|
98
|
+
interface EaselRailItem {
|
|
99
99
|
id: string; // React key + selection correlation
|
|
100
100
|
label: string; // aria-label + hover title
|
|
101
101
|
icon: React.ReactNode; // an 18px currentColor glyph — use the shipped icons
|
|
@@ -109,7 +109,7 @@ A **view item** passes `active: true | false` and renders as a toggle
|
|
|
109
109
|
(`aria-pressed`, `.is-active` highlight). An **action item** (the quick-add "+")
|
|
110
110
|
omits `active` and renders as a plain button.
|
|
111
111
|
|
|
112
|
-
`
|
|
112
|
+
`EaselRailButton` — the single-button building block `EaselRail` maps to — is
|
|
113
113
|
exported too, for bespoke layouts.
|
|
114
114
|
|
|
115
115
|
## Tokens
|
|
@@ -216,7 +216,7 @@ email. Framework-free: no React, no DOM, no imports.
|
|
|
216
216
|
| `- ` at the start of a line | a bullet line, one level | yes |
|
|
217
217
|
| `1. ` at the start of a line | a numbered line, one level: 1-3 digits, `. `, words; the number is kept as typed | yes |
|
|
218
218
|
| `[x]{underline}`, `[x]{sup}`, `[x]{sub}` | underline, superscript, subscript (tree only) | not yet — `applyMark` writes them; the buttons come with the final text box |
|
|
219
|
-
| `[x]{color=name}` | a theme colour
|
|
219
|
+
| `[x]{color=name}` / `[x]{color=#rrggbb}` | a theme colour by name (`isColorName`), or any colour as exactly six hex digits (`isColorValue`; saved lowercase) (tree only) | not yet — `applyColor` writes it |
|
|
220
220
|
| `@@x@@`, `++x++` | Emphasis, Strong (the storefront's old word styles) | no — read only |
|
|
221
221
|
| `*x*`, `#x#`, `[x]` | legacy accent, bold, italic (per product set) | no — read only |
|
|
222
222
|
|
|
@@ -235,7 +235,7 @@ not learned them prints the delimiters.
|
|
|
235
235
|
| `isSafeLinkHref(href)` | The link rule. Call it before writing any `href`. |
|
|
236
236
|
| `tokenizeInline(value, rules)`, `stripInline` | The flat reading older renderers use: runs only, no links, no nesting. |
|
|
237
237
|
| `applyMark(text, start, end, style)`, `MARKUP_TOOLS`, `markupTools`, `COPY_FIELD_TOOLS` | The writer behind a toolbar button, and the button descriptors. |
|
|
238
|
-
| `applyColor(text, start, end, name)`, `isColorName`, `spanFor` | A colour carries a
|
|
238
|
+
| `applyColor(text, start, end, name)`, `isColorName`, `isColorValue`, `spanFor` | A colour carries a value, so it has its own writer: it writes, changes and (`null`) takes off `[x]{color=name}` — a theme name or a 6-digit hex, saved lowercase — and changes nothing for a value `isColorValue` refuses (`#f00`, `rgb()`, a typo). |
|
|
239
239
|
| `STANDARD_MARKUP`, `STOREFRONT_MARKUP`, `CAMPAIGN_MARKUP` | The rule sets. The last two add each product's legacy runs. |
|
|
240
240
|
|
|
241
241
|
## The edit lock — two operators, one document
|
|
@@ -263,7 +263,7 @@ export const pageEditLock = createEditLock({
|
|
|
263
263
|
|
|
264
264
|
The plain half needs no binding: `canWriteDocument`, `holderOf`,
|
|
265
265
|
`readLockHolder`, `accessForNewDocument`, `documentPermissions`, `describeAge`,
|
|
266
|
-
`nameOrSomeone`, `
|
|
266
|
+
`nameOrSomeone`, `newEaselClientId`, `shouldReleaseHeldWrite`,
|
|
267
267
|
`shouldReloadAfterTakeover`, `documentVersionMoved`, `reloadIfVersionMoved`.
|
|
268
268
|
The bound half —
|
|
269
269
|
`accessAfterClaimFailure`, `accessAfterTakeoverFailure`,
|
|
@@ -296,16 +296,16 @@ the editor owns the device, the save status, the widths and which panels are ope
|
|
|
296
296
|
|
|
297
297
|
```tsx
|
|
298
298
|
import {
|
|
299
|
-
|
|
300
|
-
SaveState, PanelResizeHandle, InspectorHead, DEFAULT_PANEL_W, panelWidthVars,
|
|
299
|
+
EaselTopBar, TopBarCluster, TopBarDivider, EaselIconButton, DeviceSwitch,
|
|
300
|
+
SaveState, PanelResizeHandle, InspectorHead, DEFAULT_PANEL_W, panelWidthVars, setPanelWidthVars,
|
|
301
301
|
} from 'efficient-easel/chrome';
|
|
302
302
|
import 'efficient-easel/chrome.css'; // once, after shell.css
|
|
303
303
|
|
|
304
304
|
<div className="es-editor" style={panelWidthVars(widths)}> {/* the vars go on the ROOT */}
|
|
305
|
-
<
|
|
306
|
-
<TopBarCluster kind="rail"><
|
|
305
|
+
<EaselTopBar>
|
|
306
|
+
<TopBarCluster kind="rail"><EaselRail orientation="row" items={items} /></TopBarCluster>
|
|
307
307
|
<TopBarCluster>
|
|
308
|
-
<
|
|
308
|
+
<EaselIconButton label="Undo" onClick={undo}><UndoIcon /></EaselIconButton>
|
|
309
309
|
<TopBarDivider />
|
|
310
310
|
<DeviceSwitch devices={devices} value={device} onChange={setDevice} />
|
|
311
311
|
</TopBarCluster>
|
|
@@ -313,18 +313,29 @@ import 'efficient-easel/chrome.css'; // once, after shell.css
|
|
|
313
313
|
<SaveState tone="saved" label="" />
|
|
314
314
|
<button className="es-btn-primary">Done</button>
|
|
315
315
|
</TopBarCluster>
|
|
316
|
-
</
|
|
316
|
+
</EaselTopBar>
|
|
317
317
|
…
|
|
318
318
|
</div>
|
|
319
319
|
```
|
|
320
320
|
|
|
321
|
+
A server renders that `style` prop as a `style=""` attribute. Under a
|
|
322
|
+
Content-Security-Policy with no `'unsafe-inline'` for styles the browser refuses
|
|
323
|
+
it, so a **server-rendered** editor writes the widths through the CSSOM instead:
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
<div className="es-editor" ref={(el) => { if (el) setPanelWidthVars(el, widths); }}>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Until that runs, chrome.css draws the panels at `DEFAULT_PANEL_W`. See
|
|
330
|
+
[Server rendering and a strict Content-Security-Policy](#server-rendering-and-a-strict-content-security-policy).
|
|
331
|
+
|
|
321
332
|
## The Layers tree — one row at every depth
|
|
322
333
|
|
|
323
334
|
`efficient-easel/tree` (added in 0.42.0) is the Layers tree both editors draw. Every
|
|
324
335
|
row has the same shape; only `Subtree` says depth.
|
|
325
336
|
|
|
326
337
|
```tsx
|
|
327
|
-
import { LayersTree, TreeGroupLabel, Subtree, TreeRow, TreeRowAction } from 'efficient-easel/tree';
|
|
338
|
+
import { LayersTree, TreeGroupLabel, Subtree, TreeRow, TreeRowAction, TreeEmptySlot } from 'efficient-easel/tree';
|
|
328
339
|
import 'efficient-easel/tree.css';
|
|
329
340
|
|
|
330
341
|
<LayersTree>
|
|
@@ -335,9 +346,17 @@ import 'efficient-easel/tree.css';
|
|
|
335
346
|
<Subtree>
|
|
336
347
|
<TreeRow icon={<TextLinesIcon />} name="Text" preview="BACK IN STOCK NOW" />
|
|
337
348
|
</Subtree>
|
|
349
|
+
<Subtree>
|
|
350
|
+
<TreeEmptySlot onAdd={addToColumn} /> {/* a list with nothing in it */}
|
|
351
|
+
</Subtree>
|
|
338
352
|
</LayersTree>
|
|
339
353
|
```
|
|
340
354
|
|
|
355
|
+
`TreeEmptySlot` is an **empty list as a place**: a dashed box the size of a row
|
|
356
|
+
that says "Add a block", takes a drop and says "Drop here" while one may land.
|
|
357
|
+
With no `onAdd` (a read-only tree) it says "No blocks yet". The three words are
|
|
358
|
+
`tree.addBlock`, `tree.dropHere` and `tree.emptySlot` in the strings catalogue.
|
|
359
|
+
|
|
341
360
|
## Copy style, paste style — one rule for both editors
|
|
342
361
|
|
|
343
362
|
`efficient-easel/style-clipboard` (added in 0.50.0) is the right-click menu's
|
|
@@ -566,9 +585,9 @@ editors' own polling loops, host-head style injection and hand-written
|
|
|
566
585
|
coordinate maths.
|
|
567
586
|
|
|
568
587
|
```tsx
|
|
569
|
-
import { useCanvasFrame, useCanvasEvent, useCanvasStyle, useCanvasLayout, frameRectToHost, isTypingTarget, nodeIdAt,
|
|
588
|
+
import { useCanvasFrame, useCanvasEvent, useCanvasStyle, useCanvasLayout, frameRectToHost, isTypingTarget, nodeIdAt, LIST_SELECTOR } from 'efficient-easel/canvas';
|
|
570
589
|
|
|
571
|
-
const canvas = useCanvasFrame(editorRootRef, { readySelector:
|
|
590
|
+
const canvas = useCanvasFrame(editorRootRef, { readySelector: LIST_SELECTOR }); // see the note below
|
|
572
591
|
useCanvasStyle(canvas, 'hidden-blocks', hiddenBlockCss(ids)); // canvas document only
|
|
573
592
|
useCanvasEvent(canvas, 'contextmenu', (e) => open(nodeIdAt(e.target))); // follows a document swap
|
|
574
593
|
const tick = useCanvasLayout(canvas); // re-measure when this changes
|
|
@@ -576,7 +595,7 @@ const tick = useCanvasLayout(canvas); // re-m
|
|
|
576
595
|
|
|
577
596
|
| Export | What it answers |
|
|
578
597
|
|---|---|
|
|
579
|
-
| `useCanvasFrame(rootRef, { selector?, readySelector? })` | `{ frame, document, window }` or `null`. Found by a MutationObserver and the frame's `load` event — never a timer. `readySelector` waits for the
|
|
598
|
+
| `useCanvasFrame(rootRef, { selector?, readySelector? })` | `{ frame, document, window }` or `null`. Found by a MutationObserver and the frame's `load` event — never a timer. `readySelector` waits for the canvas to be drawn in a loaded-but-empty document. |
|
|
580
599
|
| `useCanvasEvent(canvas, type, handler, { on?, capture? })` | A listener on the canvas document (or window) that moves to a replacement document and always calls the latest handler, without re-binding on every render. |
|
|
581
600
|
| `useCanvasStyle(canvas, key, css)` | A `<style>` in the canvas document's head only — re-injected into a replacement, put back if something empties the head (Puck's AutoFrame does, once), removed on unmount, never written to the host page. |
|
|
582
601
|
| `useCanvasLayout(canvas)` | A number that changes when the canvas scrolls, resizes, is replaced, or the host scrolls or resizes (plus two settle passes). |
|
|
@@ -587,6 +606,13 @@ const tick = useCanvasLayout(canvas); // re-m
|
|
|
587
606
|
| `watchCanvasFrame(root, onChange, options)` | The watcher the hook wraps, for code outside React. |
|
|
588
607
|
| `setCanvasNonce(document, nonce)` | The CSP nonce every `<style>` created in that canvas document carries (0.95.0). `CanvasFrame`'s `nonce` prop sets it; a host that builds its own frame calls it. |
|
|
589
608
|
|
|
609
|
+
⚠️ **Wait for a list, not for a block.** `LIST_SELECTOR` matches every list
|
|
610
|
+
the canvas draws, and the page's own is there with nothing in it.
|
|
611
|
+
`readySelector: BLOCK_SELECTOR` means "ready once the canvas holds a block", so
|
|
612
|
+
the canvas of a page with **no blocks** is never published: the drag layer,
|
|
613
|
+
canvas stylesheets and context menus do nothing on the one page a first block
|
|
614
|
+
has to be dropped on. `BLOCK_SELECTOR` is for a canvas that is never empty.
|
|
615
|
+
|
|
590
616
|
## Node operations — `efficient-easel/node-ops`
|
|
591
617
|
|
|
592
618
|
`efficient-easel/node-ops` (added in 0.71.0, blueprint step 2.2) is what can be
|
|
@@ -612,7 +638,7 @@ const items = nodeMenuItems({
|
|
|
612
638
|
});
|
|
613
639
|
|
|
614
640
|
const up = ops.parentOf(selection); // null → render nothing
|
|
615
|
-
{up && <InspectorUpLevel name={nameOf(up)} onSelect={() => select(up)}
|
|
641
|
+
{up && <InspectorUpLevel name={nameOf(up)} onSelect={() => select(up)} />} // ← Section
|
|
616
642
|
```
|
|
617
643
|
|
|
618
644
|
- **One action, one commit.** Every operation (remove, duplicate, move, step,
|
|
@@ -677,6 +703,14 @@ onChange={(doc) => canWrite && autosave.push(doc)}
|
|
|
677
703
|
(after saving or discarding it); a save still out for the old document can
|
|
678
704
|
no longer change the new one's state.
|
|
679
705
|
|
|
706
|
+
- **Feed it from `createEasel({ onChange })`, not from `easel.subscribe`.**
|
|
707
|
+
`onChange` is called for a change to the document and never for `load()`. A
|
|
708
|
+
subscriber is also told of a load, a selection and a permission, and pushing
|
|
709
|
+
each of those saves a document nobody edited.
|
|
710
|
+
- **`autosave.indicator` is always English.** Autosave does not read the
|
|
711
|
+
strings provider. In another language, build the pill from the state:
|
|
712
|
+
`<SaveState {...saveIndicator(autosave.state, useEaselStrings().save)} />`.
|
|
713
|
+
|
|
680
714
|
`createAutosave` is the same engine without React, with the clock passed in —
|
|
681
715
|
the specs run minutes of debounce and backoff on a fake one.
|
|
682
716
|
|
|
@@ -691,7 +725,7 @@ land (`canDrop`) and what it writes (`onDrop`, `onStep`).
|
|
|
691
725
|
|
|
692
726
|
```tsx
|
|
693
727
|
import { useTreeDrag, gapToMoveIndex } from 'efficient-easel/drag';
|
|
694
|
-
import { LayersTree, TreeRow, TreeSeam } from 'efficient-easel/tree';
|
|
728
|
+
import { LayersTree, Subtree, TreeEmptySlot, TreeRow, TreeSeam } from 'efficient-easel/tree';
|
|
695
729
|
|
|
696
730
|
const drag = useTreeDrag<Item>({
|
|
697
731
|
keyOf: (item) => item.id,
|
|
@@ -704,6 +738,9 @@ const drag = useTreeDrag<Item>({
|
|
|
704
738
|
<LayersTree rootProps={drag.rootProps()}>
|
|
705
739
|
<TreeSeam {...drag.seam({ place: { ...list, index: 0 } })} onAdd={() => openAdd(list, 0)} label="Add block here" />
|
|
706
740
|
<TreeRow {...drag.row({ item, place, into: containerPlace, pinned })} icon={…} name={…} onSelect={…} />
|
|
741
|
+
<Subtree> {/* a list with nothing in it: not a seam */}
|
|
742
|
+
<TreeEmptySlot {...drag.seam({ place: { ...column, index: 0 } })} onAdd={() => openAdd(column, 0)} />
|
|
743
|
+
</Subtree>
|
|
707
744
|
```
|
|
708
745
|
|
|
709
746
|
- **The whole row is picked up**, and the drag picture is the row, raised.
|
|
@@ -718,6 +755,17 @@ const drag = useTreeDrag<Item>({
|
|
|
718
755
|
- **⌥↑ / ⌥↓** step a row. The list **scrolls at its edges**.
|
|
719
756
|
- **A seam is the gap:** seams in every gap of a list, or in none. The list's
|
|
720
757
|
spacing does not change either way.
|
|
758
|
+
- **An empty list is not a seam.** A seam hands its height back to the rows
|
|
759
|
+
either side of it; alone in a list it is a strip a few pixels tall, and a
|
|
760
|
+
drop aimed at "into this empty column" lands on whatever is really under the
|
|
761
|
+
pointer. Draw `TreeEmptySlot` (`efficient-easel/tree`) there, with the same
|
|
762
|
+
`drag.seam()`.
|
|
763
|
+
- **A target names an effect its source allowed.** A row or a block picked up
|
|
764
|
+
is a move; a tile from the Add list is a copy (`effectAllowed = 'copy'`), and
|
|
765
|
+
a browser refuses a drop whose `dropEffect` the source did not allow: the
|
|
766
|
+
line is drawn and `drop` never fires. The tree and the canvas both answer
|
|
767
|
+
with `dropEffectFor(e.dataTransfer.effectAllowed)`; a drop target of a
|
|
768
|
+
host's own in the same session should too.
|
|
721
769
|
|
|
722
770
|
**The canvas and the Add list** (0.75.0, step 3.2) join the same drag through a
|
|
723
771
|
shared session, so a thing picked up on one surface can land on another:
|
|
@@ -740,12 +788,172 @@ const drag = useTreeDrag({ …, session }); // the tree
|
|
|
740
788
|
blocks. Your `model` says what may be picked up (leave out what cannot move)
|
|
741
789
|
and where "+ Add section" goes (`seamsBetween`).
|
|
742
790
|
- **One handle,** on the smallest box under the pointer, never unmounted
|
|
743
|
-
mid-drag.
|
|
791
|
+
mid-drag. It follows the CONTENT, not the last mouse move: it is picked
|
|
792
|
+
again whenever the canvas is measured again (a wheel in a frame that scrolls
|
|
793
|
+
itself, a block added above) and when a drag ends, so a host has no reason
|
|
794
|
+
to nudge it with a mouse move of its own.
|
|
744
795
|
- **The line** runs across a list, or down between columns (a slot laid out
|
|
745
796
|
across).
|
|
746
797
|
- **An empty slot** is a dashed "+ Add block" cell that also takes drops.
|
|
798
|
+
- **A drop the innermost list refuses falls through to the list outside it.**
|
|
799
|
+
A section held over a column lands between sections, and the line is drawn
|
|
800
|
+
where it will land before it is let go. Without that a page covered by its
|
|
801
|
+
inner lists could take a section only on the few pixels between sections.
|
|
802
|
+
A host for whom a refusal is final says so per drop:
|
|
803
|
+
`fallThrough={(item, refused, from) => …}` is asked only about a place
|
|
804
|
+
`canDrop` has just refused, and `false` ends it there: no line, nothing
|
|
805
|
+
lands. For example, a Columns block over a column, or a block about to
|
|
806
|
+
leave the container it was picked up in (`from`). Left out, every refusal
|
|
807
|
+
falls through. A drop back where the thing already is never falls through.
|
|
808
|
+
- **The canvas scrolls at its edges,** like the tree: the host's pane around a
|
|
809
|
+
frame as tall as its content, and the frame's own document when the frame
|
|
810
|
+
fills its pane and scrolls itself (`EaselCanvas height="fill"`).
|
|
811
|
+
- **One tile can be off:** `AddTile.disabled`, with `disabledReason` as its
|
|
812
|
+
tooltip ("A page has one header"). The tile stays in the list, dimmed, is
|
|
813
|
+
still found by search and counted; a click adds nothing and a drag does not
|
|
814
|
+
start. It is `aria-disabled`, so it keeps the focus and its reason can be
|
|
815
|
+
reached. `readOnly` is still the whole panel's.
|
|
747
816
|
- Styles: `import 'efficient-easel/drag.css'`.
|
|
748
817
|
|
|
818
|
+
## The store, its keys and its permissions — `efficient-easel/easel`
|
|
819
|
+
|
|
820
|
+
```tsx
|
|
821
|
+
import { createEasel, EaselProvider, EaselCanvas, useSelectedFields } from 'efficient-easel/easel';
|
|
822
|
+
import 'efficient-easel/easel.css';
|
|
823
|
+
|
|
824
|
+
const easel = createEasel<EaselConfig>({ config, data, onChange: (doc) => autosave.push(doc) });
|
|
825
|
+
|
|
826
|
+
<EaselProvider easel={easel} hotkeys={ownsThePage}>
|
|
827
|
+
<EaselCanvas height="fill" />
|
|
828
|
+
</EaselProvider>
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
- **Say the type: `createEasel<EaselConfig>(…)`.** `createEasel` infers its
|
|
832
|
+
type from the config it is handed, and a config typed more narrowly (a
|
|
833
|
+
`Config<…>` from `efficient-easel/blocks`) gives an `Easel<ThatConfig>`,
|
|
834
|
+
which `EaselProvider` does not accept.
|
|
835
|
+
- **The store enforces no permission.** `dispatch`, `run`, `undo` and `redo`
|
|
836
|
+
write whatever they are asked to. Permissions are what the PARTS read: the
|
|
837
|
+
settings panel (`useSelectedFields().readOnly`), the menus, the undo keys. A
|
|
838
|
+
host that must not write, because somebody else holds the document, gates
|
|
839
|
+
its own calls.
|
|
840
|
+
- **`setPermissions` replaces, and tells its readers.** It is not merged over
|
|
841
|
+
the last value (name every key: `documentPermissions` in
|
|
842
|
+
`efficient-easel/edit-lock` does). A panel reading
|
|
843
|
+
`useSelectedFields().readOnly` or `useEasel((s) => s.getPermissions)`
|
|
844
|
+
re-renders when the permissions change, and only then; the same permissions
|
|
845
|
+
again tell nobody, so it is safe to call from an effect.
|
|
846
|
+
- **Undo and redo on the keyboard.** `EaselProvider` binds ⌘Z / Ctrl+Z and
|
|
847
|
+
the redo keys to the page, and `EaselCanvas` binds them in the frame's own
|
|
848
|
+
document, because a key pressed in the frame never reaches the page.
|
|
849
|
+
- `hotkeys={false}` on the provider turns **both** off, for a host that binds
|
|
850
|
+
its own.
|
|
851
|
+
- `<EaselCanvas hotkeys>` says otherwise for the frame alone: a host that
|
|
852
|
+
binds the page's keys itself (to its own root, because the page is another
|
|
853
|
+
app's) and still wants the frame's passes `hotkeys` there.
|
|
854
|
+
- Whatever those say, the keys do nothing while the easel-wide `edit`
|
|
855
|
+
permission is off. An undo is a write. `bindUndoKeys(target, editor)`, for
|
|
856
|
+
a host's own target, is held the same way.
|
|
857
|
+
- **`planFields().rows` holds `slot` rows.** They draw nothing and are there so
|
|
858
|
+
the slot's name has a bucket. Filter `row.field.type !== 'slot'` before
|
|
859
|
+
drawing or counting rows of your own.
|
|
860
|
+
- **Inserting a block runs no resolver; `easel.resolveNode` does.** The
|
|
861
|
+
`insert` action puts the block in with its `defaultProps`, as Puck's reducer
|
|
862
|
+
did. What ran a new block's `resolveData` in Puck was its own drop from the
|
|
863
|
+
component list, and the engine has no drop of its own: the Add list's click
|
|
864
|
+
and the drag's drop are the host's. So follow an insert with it:
|
|
865
|
+
|
|
866
|
+
```ts
|
|
867
|
+
easel.dispatch({ type: 'insert', componentType, destinationZone, destinationIndex, id });
|
|
868
|
+
void easel.resolveNode(id, { trigger: 'insert' }); // or after `easel.run(…)`
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
The block is told `trigger: 'insert'`, what it holds is resolved with it,
|
|
872
|
+
and what changed is written into the insert's own undo step. A block
|
|
873
|
+
changed by hand, or taken out, while its resolver ran is left as it is.
|
|
874
|
+
- **`InspectorUpLevel` draws its own `←`** (the one up-level look, Lewis
|
|
875
|
+
2026-10-06): a blue text link, the arrow and the parent's name, no box and
|
|
876
|
+
no hover change. `icon` is ignored. Inside a panel body, pass `inline`. The
|
|
877
|
+
icon set has no back arrow, on purpose: nothing needs one now, and a glyph
|
|
878
|
+
enters the set by the owner's icon standard, not for a host.
|
|
879
|
+
|
|
880
|
+
### What an accessibility audit of a second host found
|
|
881
|
+
|
|
882
|
+
A host built only from the parts, audited with axe-core in a real Chromium.
|
|
883
|
+
What follows is how the parts answer it; `scripts/second-host-clickthrough.mjs`
|
|
884
|
+
runs the same rules (`--axe <axe.min.js>`).
|
|
885
|
+
|
|
886
|
+
- **The right-click menu lives on `<body>`, and a host may move it into its
|
|
887
|
+
own landmarks.** On the body the box is page content outside every landmark
|
|
888
|
+
(axe `region`, a best-practice rule). It is not wrapped in a region or a
|
|
889
|
+
dialog to satisfy that: a menu is neither. A host that wants it inside its
|
|
890
|
+
landmarks portals it into its editor's root, through the check:
|
|
891
|
+
|
|
892
|
+
```tsx
|
|
893
|
+
import { EaselContextMenu, menuPortalTarget } from 'efficient-easel/menu';
|
|
894
|
+
|
|
895
|
+
<EaselContextMenu portalTo={menuPortalTarget(rootRef.current)} … />
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
The box is `position: fixed` at the pointer, and an ancestor that is
|
|
899
|
+
transformed, filtered or contained places and clips a fixed box inside it.
|
|
900
|
+
`menuPortalTarget` gives the root back only when none does, and `null` (the
|
|
901
|
+
body) otherwise, so a root that is scaled on one screen and not on another
|
|
902
|
+
is right on both. The body stays the default because it is the one place
|
|
903
|
+
that cannot go wrong. The root's stacking is the host's: the box is
|
|
904
|
+
`z-index: 9999` inside it.
|
|
905
|
+
- **A dropdown's open list has a name.** In a settings row
|
|
906
|
+
(`InspectorField`) the list is `aria-labelledby` the row's own label. A
|
|
907
|
+
`FieldSelect` on its own is named by `ariaLabelledBy` or `ariaLabel`, and
|
|
908
|
+
with neither the list takes its button's name, so it is never nameless.
|
|
909
|
+
- **Small grey words can be read.** The Add panel's group count, a menu
|
|
910
|
+
row's shortcut, a read-only or disabled field's value and an empty slot's
|
|
911
|
+
note are at least 4.5:1 on their ground (WCAG AA), held by ratio in
|
|
912
|
+
`tests/contrast-small-text.test.ts`. A host that overrode
|
|
913
|
+
`.es-add-group-count` or `--es-menu-shortcut-fg` can drop the override.
|
|
914
|
+
- **The rail says which words it draws.** The width decides in CSS between
|
|
915
|
+
every word, the open panel's word, and glyphs only, so a longer language
|
|
916
|
+
folds sooner (German "Zurück", "Ebenen"). `<TopBarRail onWords={…}>` is told
|
|
917
|
+
`'all' | 'chosen' | 'none'`, read back from what the stylesheet drew, when
|
|
918
|
+
it changes. Every button keeps its name as `title` and `aria-label`.
|
|
919
|
+
- **A list's drag hint is drawn whole under its last row.** A list in the
|
|
920
|
+
settings panel (`ListField`) is not a scroll box of its own; the panel
|
|
921
|
+
around it scrolls.
|
|
922
|
+
|
|
923
|
+
### Server rendering and a strict Content-Security-Policy
|
|
924
|
+
|
|
925
|
+
A policy with no `'unsafe-inline'` for styles (`style-src 'self'`, or by nonce)
|
|
926
|
+
refuses every `style=""` **attribute** in the HTML a server sends. It does not
|
|
927
|
+
govern the CSSOM, which is how React writes a `style` prop in the browser, so a
|
|
928
|
+
client-rendered editor is not affected. And React does not repair it when it
|
|
929
|
+
hydrates: an attribute it finds matching is left alone, so a refused style stays
|
|
930
|
+
refused until its value changes.
|
|
931
|
+
|
|
932
|
+
So the frame's parts write no `style` of their own into a server's HTML:
|
|
933
|
+
|
|
934
|
+
- **The canvas frame's look is a class** (`.es-canvas-frame`, `.is-fill`, in
|
|
935
|
+
`efficient-easel/easel.css`): no border, the pane's width, and the pane's
|
|
936
|
+
height when it fills.
|
|
937
|
+
- **The panel widths have their default in the stylesheet** wherever chrome.css
|
|
938
|
+
reads them, and a host writes its own with `setPanelWidthVars(el, widths)`
|
|
939
|
+
from a ref rather than `style={panelWidthVars(widths)}`.
|
|
940
|
+
- **Stylesheets in the frame** take the page's nonce: `<EaselCanvas nonce>`.
|
|
941
|
+
|
|
942
|
+
What still writes a `style`, and where it matters:
|
|
943
|
+
|
|
944
|
+
| Part | When | Under such a policy |
|
|
945
|
+
|---|---|---|
|
|
946
|
+
| The drag layer's handle, line and cells; context menus; the rail's measured words | In the browser only, from state or a measurement | Nothing to do: never in a server's HTML |
|
|
947
|
+
| Everything inside the canvas frame (lists, block wrappers, outlines) | In the browser only: the frame is filled once it mounts | Nothing to do |
|
|
948
|
+
| `EaselCanvas frame={false}` | Server-rendered with the page | Mount it on the client |
|
|
949
|
+
| `ColourPicker`'s swatch, when it has a value | Server-rendered with a settings panel that holds a colour | Mount that panel on the client |
|
|
950
|
+
| `SaveState` with `counting` and `waitMs` | Only while a save waits, which a server render is not | Nothing to do |
|
|
951
|
+
| A `style` or a slot's `style` you pass (`CanvasFrame`, `TopBarCluster`, `Render`) | As you wrote it | Yours to move to a class |
|
|
952
|
+
|
|
953
|
+
`tests/csp-server-render.test.tsx` server-renders a frame built from the parts
|
|
954
|
+
and fails on any `style` attribute; `scripts/second-host-clickthrough.mjs`
|
|
955
|
+
serves one under a real policy in Chrome.
|
|
956
|
+
|
|
749
957
|
## The engine's words — `efficient-easel/strings`
|
|
750
958
|
|
|
751
959
|
`efficient-easel/strings` (added in 0.95.0) is every word the engine itself
|
|
@@ -754,24 +962,24 @@ typed catalogue, so a host in another language can translate it. A host that
|
|
|
754
962
|
does nothing sees the English it always saw.
|
|
755
963
|
|
|
756
964
|
```tsx
|
|
757
|
-
import {
|
|
965
|
+
import { EaselStringsProvider, useEaselStrings, type DeepPartial, type EaselStrings } from 'efficient-easel/strings';
|
|
758
966
|
|
|
759
|
-
const german: DeepPartial<
|
|
967
|
+
const german: DeepPartial<EaselStrings> = {
|
|
760
968
|
save: { failed: 'Speichern fehlgeschlagen' },
|
|
761
969
|
lock: { minutesAgo: ({ count }) => `vor ${count} Min.` }, // a value or a plural: a function
|
|
762
970
|
};
|
|
763
971
|
|
|
764
|
-
<
|
|
972
|
+
<EaselStringsProvider value={german} locale="de">
|
|
765
973
|
<Editor />
|
|
766
|
-
</
|
|
974
|
+
</EaselStringsProvider>;
|
|
767
975
|
```
|
|
768
976
|
|
|
769
|
-
- **`
|
|
977
|
+
- **`EaselStrings`** is grouped by the module that says the word (`notice`,
|
|
770
978
|
`lock`, `save`, `chrome`, `rail`, `tree`, `panel`, `fields`, `pick`, `drag`,
|
|
771
979
|
`menu`, `styleClipboard`, `copy`, `canvas`). A plain word is a `string`;
|
|
772
980
|
anything with a value in it is a function, so the host places the value and
|
|
773
|
-
does its own plural. `
|
|
774
|
-
- **What is left out falls back** (`
|
|
981
|
+
does its own plural. `DEFAULT_EASEL_STRINGS` is the English.
|
|
982
|
+
- **What is left out falls back** (`mergeEaselStrings`), and a provider inside
|
|
775
983
|
another lays its words over the outer one's.
|
|
776
984
|
- **The provider reaches the parts that have hooks:** the edit notice, the device
|
|
777
985
|
switch, "up one level", the panel grip and the top-bar rail; the copy box, its
|
|
@@ -784,9 +992,9 @@ const german: DeepPartial<EditorStrings> = {
|
|
|
784
992
|
argument:
|
|
785
993
|
|
|
786
994
|
```tsx
|
|
787
|
-
const strings =
|
|
995
|
+
const strings = useEaselStrings();
|
|
788
996
|
|
|
789
|
-
<
|
|
997
|
+
<EaselRail items={items} ariaLabel={strings.rail.label} />
|
|
790
998
|
<TreeRow … words={strings.tree} />
|
|
791
999
|
<ColourPicker … words={strings.pick.colour} />
|
|
792
1000
|
<ImageBox … words={strings.pick.image} />
|
|
@@ -799,17 +1007,44 @@ const german: DeepPartial<EditorStrings> = {
|
|
|
799
1007
|
styleMenuItems({ …, words: strings.styleClipboard });
|
|
800
1008
|
pasteMessage(plan, source, target, strings.styleClipboard);
|
|
801
1009
|
```
|
|
802
|
-
- **`
|
|
1010
|
+
- **`useEaselLocale()`** is the provider's `locale` (`'en'` with none), for a
|
|
803
1011
|
host's own blocks and fields. The engine does not read it.
|
|
804
1012
|
- **A server file may import it.** Importing the entry runs nothing — the React
|
|
805
1013
|
context is made on first use. `efficient-easel/strings/catalogue` is the
|
|
806
|
-
catalogue, its types and `
|
|
1014
|
+
catalogue, its types and `mergeEaselStrings` with no React in its graph at
|
|
807
1015
|
all: what a host's own test imports to check its languages against the
|
|
808
1016
|
engine's keys.
|
|
809
1017
|
|
|
810
1018
|
What a host supplies was never the engine's and is not in the catalogue: a
|
|
811
1019
|
block's name, a field's label, a tile's caption, the document's `noun`.
|
|
812
1020
|
|
|
1021
|
+
## The names before the Easel rename
|
|
1022
|
+
|
|
1023
|
+
The package took the name `efficient-easel` at 0.96.0. Its exports followed at
|
|
1024
|
+
0.99.2: every `Editor…` name is an `Easel…` one, and the store's subpath is
|
|
1025
|
+
`efficient-easel/easel`. From 0.99.2 to 0.99.x the old names were kept as
|
|
1026
|
+
deprecated aliases. **They are removed in 0.100.0**: a host must be on the new
|
|
1027
|
+
names, and on `efficient-web-builder` 0.7.1 or later, before it takes 0.100.0.
|
|
1028
|
+
|
|
1029
|
+
| Subpath | Was | Is |
|
|
1030
|
+
| --- | --- | --- |
|
|
1031
|
+
| `efficient-easel/editor` | the subpath itself, and `editor.css` | `efficient-easel/easel`, `easel.css` |
|
|
1032
|
+
| `/easel` | `createEditor`, `Editor`, `EditorOptions` | `createEasel`, `Easel`, `EaselOptions` |
|
|
1033
|
+
| `/easel` | `EditorProvider` (prop `editor`) | `EaselProvider` (prop `easel`) |
|
|
1034
|
+
| `/easel` | `useEditor`, `useEditorApi`, `createUseEditor`, `useEditorNodeOps` | `useEasel`, `useEaselApi`, `createUseEasel`, `useEaselNodeOps` |
|
|
1035
|
+
| `/easel` | `EditorCanvas`, `EditorCanvasProps` | `EaselCanvas`, `EaselCanvasProps` |
|
|
1036
|
+
| `/easel` | `EditorConfig`, `EditorView`, `EditorUi`, `EditorAction`, `EditorCore`, `EditorHistory` | `EaselConfig`, `EaselView`, `EaselUi`, `EaselAction`, `EaselCore`, `EaselHistory` |
|
|
1037
|
+
| `/rail` | `EditorRail`, `EditorRailButton`, `EditorRailItem`, `EditorRailProps`, `EditorRailButtonProps`, `EditorRailOrientation` | `EaselRail`, `EaselRailButton`, `EaselRailItem`, `EaselRailProps`, `EaselRailButtonProps`, `EaselRailOrientation` |
|
|
1038
|
+
| `/menu` | `EditorContextMenu`, `EditorContextMenuItem`, `EditorContextMenuProps`, `EditorContextMenuSkin` | `EaselContextMenu`, `EaselContextMenuItem`, `EaselContextMenuProps`, `EaselContextMenuSkin` |
|
|
1039
|
+
| `/chrome` | `EditorTopBar`, `EditorIconButton`, `EditorIconButtonProps` | `EaselTopBar`, `EaselIconButton`, `EaselIconButtonProps` |
|
|
1040
|
+
| `/strings` | `EditorStrings`, `DEFAULT_EDITOR_STRINGS`, `mergeEditorStrings`, `EditorStringsProvider`, `EditorStringsProviderProps`, `useEditorStrings`, `useEditorLocale` | `EaselStrings`, `DEFAULT_EASEL_STRINGS`, `mergeEaselStrings`, `EaselStringsProvider`, `EaselStringsProviderProps`, `useEaselStrings`, `useEaselLocale` |
|
|
1041
|
+
| `/edit-lock` | `newEditorClientId` | `newEaselClientId` |
|
|
1042
|
+
|
|
1043
|
+
Not renamed, because other code holds them: the `.es-editor` class and every
|
|
1044
|
+
other `es-` class, the `--es-*` variables, the `editor-shell:style-clipboard`
|
|
1045
|
+
storage key, and the rail's default label, the word "Editor", which a screen
|
|
1046
|
+
reader says to the person using a host's editor.
|
|
1047
|
+
|
|
813
1048
|
## Develop
|
|
814
1049
|
|
|
815
1050
|
Node is native on the dev Mac (v26.8.1) — run these in your own worktree; a
|
|
@@ -826,3 +1061,13 @@ docker run --rm -v "$PWD":/w -w /w node:26-slim \
|
|
|
826
1061
|
jsdom window in `tests/_dom-env.ts` and runs in its own process.
|
|
827
1062
|
- `npm run build` — `tsup` → `dist/` (root + the `rail`, `shell` and `gold`
|
|
828
1063
|
subpaths) + the copied CSS.
|
|
1064
|
+
- `node scripts/second-host-clickthrough.mjs [out-dir]` — what jsdom cannot
|
|
1065
|
+
hold. It server-renders a lean editor built only from the parts
|
|
1066
|
+
(`tests/fixtures/second-host-page.tsx`), serves it under a
|
|
1067
|
+
Content-Security-Policy with no `'unsafe-inline'`, and drives it in headless
|
|
1068
|
+
Chrome with real drags and keys: a tile dropped on the tree, the canvas
|
|
1069
|
+
scrolling under a drag, undo keys with editing held, a permission reaching
|
|
1070
|
+
the settings panel, no refused `style` attribute, an empty slot taking a
|
|
1071
|
+
drop. One PASS or FAIL per step and a screenshot of each. Not part of
|
|
1072
|
+
`npm test`: CI has no Chrome. Run it when you touch `drag`, `editor`,
|
|
1073
|
+
`chrome` or `tree`.
|
package/dist/autosave/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { A as Autosave, a as AutosaveSnapshot, b as SaveIndicator, c as AutosaveOptions } from '../words-
|
|
2
|
-
export { d as AutosaveClock, S as SaveWords, e as createAutosave, s as saveIndicator } from '../words-
|
|
3
|
-
import '../types-
|
|
4
|
-
import '../types-
|
|
5
|
-
import '../wordFit-
|
|
1
|
+
import { A as Autosave, a as AutosaveSnapshot, b as SaveIndicator, c as AutosaveOptions } from '../words-BMzV04b1.js';
|
|
2
|
+
export { d as AutosaveClock, S as SaveWords, e as createAutosave, s as saveIndicator } from '../words-BMzV04b1.js';
|
|
3
|
+
import '../types-DGcCsxs8.js';
|
|
4
|
+
import '../types-DWS-s9z-.js';
|
|
5
|
+
import '../wordFit-D4S36PvB.js';
|
|
6
6
|
import 'react';
|
|
7
7
|
import '../edit-lock/index.js';
|
|
8
8
|
|
|
@@ -16,7 +16,13 @@ type UseAutosaveOptions<T> = Omit<AutosaveOptions<T>, 'save' | 'onFailure'> & {
|
|
|
16
16
|
};
|
|
17
17
|
type UseAutosave<T> = Omit<Autosave<T>, 'setSave' | 'getSnapshot' | 'subscribe' | 'dispose'> & {
|
|
18
18
|
state: AutosaveSnapshot;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The pill's tone and words — `<SaveState {...indicator} />`.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ ALWAYS ENGLISH. Autosave is held to its own lane and does not read the
|
|
23
|
+
* strings provider. A host in another language builds the pill from the
|
|
24
|
+
* state instead: `saveIndicator(autosave.state, useEaselStrings().save)`.
|
|
25
|
+
*/
|
|
20
26
|
indicator: SaveIndicator;
|
|
21
27
|
};
|
|
22
28
|
declare function useAutosave<T>(options: UseAutosaveOptions<T>): UseAutosave<T>;
|
package/dist/blocks/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReactElement, FC, CSSProperties, ReactNode, JSX } from 'react';
|
|
2
|
-
import { P as Permissions, A as AppState } from '../types-
|
|
2
|
+
import { P as Permissions, A as AppState } from '../types-E3HrTPg2.js';
|
|
3
3
|
import '../data/index.js';
|
|
4
4
|
import '../types-41704JE4.js';
|
|
5
5
|
import '../resolve-D68pb3Bx.js';
|
|
@@ -21,7 +21,7 @@ import '../plan-DChbUXdS.js';
|
|
|
21
21
|
* - the `ConfigParams` form of `Config<{ components, root, fields }>` and the
|
|
22
22
|
* user field-type extension it carries (neither app uses it);
|
|
23
23
|
* - everything about Puck's own editor — its store, overrides, plugins and
|
|
24
|
-
* history. Those are the engine's now (`efficient-easel/
|
|
24
|
+
* history. Those are the engine's now (`efficient-easel/easel`).
|
|
25
25
|
*
|
|
26
26
|
* The `puck` prop keeps its name: it is what every block's `render` destructures
|
|
27
27
|
* (`puck.isEditing`, `puck.renderDropZone`), and renaming it is the block rewrite
|
package/dist/canvas/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RefObject } from 'react';
|
|
2
|
-
import { C as CanvasTarget, W as WatchOptions } from '../watch-
|
|
3
|
-
export { B as BLOCK_SELECTOR, a as CANVAS_SELECTOR, i as isFrameReady, w as watchCanvasFrame } from '../watch-
|
|
2
|
+
import { C as CanvasTarget, W as WatchOptions } from '../watch-BTYOJpQU.js';
|
|
3
|
+
export { B as BLOCK_SELECTOR, a as CANVAS_SELECTOR, L as LIST_SELECTOR, i as isFrameReady, w as watchCanvasFrame } from '../watch-BTYOJpQU.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The small facts every canvas surface needs, written once — pure DOM, no
|
|
@@ -103,7 +103,7 @@ declare function fitCanvas({ available, width, maxScale, }: {
|
|
|
103
103
|
* 'nonce-…'`), where a `<style>` without it is refused. Every sheet
|
|
104
104
|
* `upsertCanvasStyle` / `useCanvasStyle` creates in `doc` afterwards wears it.
|
|
105
105
|
*
|
|
106
|
-
* `CanvasFrame` (`efficient-easel/
|
|
106
|
+
* `CanvasFrame` (`efficient-easel/easel`) calls this with its `nonce` prop as
|
|
107
107
|
* it builds the frame, so a host says it once; a host that builds its own
|
|
108
108
|
* frame calls it itself. `undefined` (or `''`) forgets it.
|
|
109
109
|
*/
|
package/dist/canvas/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{BLOCK_SELECTOR,CANVAS_SELECTOR,isFrameReady,useCanvasEvent,useCanvasFrame,useCanvasLayout,useCanvasStyle,watchCanvasFrame}from"../chunk-
|
|
1
|
+
export{BLOCK_SELECTOR,CANVAS_SELECTOR,LIST_SELECTOR,isFrameReady,useCanvasEvent,useCanvasFrame,useCanvasLayout,useCanvasStyle,watchCanvasFrame}from"../chunk-PAELDYYS.js";export{BLOCK_ATTRIBUTE,clientPointToLayer,elementScale,fitCanvas,framePointToHost,frameRectToHost,frameRectToLayer,frameScale,isTypingTarget,nodeIdAt,removeCanvasStyle,setCanvasNonce,upsertCanvasStyle}from"../chunk-ZNCEOSXO.js";
|