efficient-easel 0.99.2 → 0.101.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 +233 -17
- package/dist/autosave/index.d.ts +11 -5
- package/dist/blocks/index.d.ts +1 -1
- package/dist/canvas/index.d.ts +2 -2
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/chrome.css +96 -29
- package/dist/chrome/index.d.ts +67 -18
- package/dist/chrome/index.js +25 -21
- package/dist/{chunk-JYPOOHII.js → chunk-2FZJWH24.js} +3 -3
- package/dist/chunk-3EDSDPIP.js +1 -0
- package/dist/chunk-546BBOPE.js +1 -0
- package/dist/chunk-67GNT6PF.js +1 -0
- package/dist/chunk-EOJNJCMJ.js +16 -0
- package/dist/chunk-EWVJSRRY.js +6 -0
- package/dist/chunk-FPOKPNO4.js +1 -0
- package/dist/chunk-H24N5TMZ.js +11 -0
- package/dist/chunk-IG3U5NWJ.js +1 -0
- package/dist/chunk-JUQ6YJCG.js +1 -0
- package/dist/chunk-KTVH2ZRL.js +15 -0
- package/dist/{chunk-PI26YXYD.js → chunk-L5KW4HG5.js} +16 -8
- package/dist/chunk-M6F6LS3H.js +1 -0
- package/dist/chunk-P6O363EE.js +6 -0
- package/dist/chunk-PAELDYYS.js +1 -0
- package/dist/chunk-QTLAVDJE.js +1 -0
- package/dist/chunk-UH7IGZKF.js +1 -0
- package/dist/chunk-WZ7TGAAT.js +3 -0
- package/dist/chunk-ZFDCDJNZ.js +16 -0
- package/dist/chunk-ZJEBF4NB.js +1 -0
- package/dist/chunk-ZNS7727Y.js +12 -0
- package/dist/copy/copy.css +442 -5
- package/dist/copy/index.d.ts +302 -5
- package/dist/copy/index.js +32 -10
- package/dist/drag/drag.css +10 -1
- package/dist/drag/index.d.ts +99 -8
- package/dist/drag/index.js +18 -17
- package/dist/easel/easel.css +15 -0
- package/dist/easel/index.d.ts +104 -33
- package/dist/easel/index.js +9 -1
- package/dist/edit-lock/index.d.ts +1 -3
- package/dist/edit-lock/index.js +1 -1
- package/dist/fields/fields.css +270 -38
- package/dist/fields/index.d.ts +209 -26
- package/dist/fields/index.js +22 -53
- package/dist/gold/index.js +1 -1
- package/dist/icons/index.d.ts +13 -1
- package/dist/icons/index.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/markup/index.d.ts +111 -10
- package/dist/markup/index.js +1 -1
- package/dist/menu/index.d.ts +58 -5
- package/dist/menu/index.js +1 -15
- package/dist/menu/menu.css +17 -6
- package/dist/node-ops/index.d.ts +1 -1
- package/dist/node-ops/index.js +1 -1
- package/dist/panel/index.d.ts +83 -4
- package/dist/panel/index.js +18 -16
- package/dist/panel/panel.css +135 -62
- package/dist/pick/index.d.ts +78 -2
- package/dist/pick/index.js +7 -14
- package/dist/pick/pick.css +197 -10
- package/dist/rail/index.d.ts +3 -7
- package/dist/rail/index.js +1 -1
- package/dist/strings/catalogue.d.ts +13 -35
- package/dist/strings/catalogue.js +1 -1
- package/dist/strings/index.d.ts +12 -18
- package/dist/strings/index.js +2 -2
- package/dist/style-clipboard/index.d.ts +1 -1
- 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 +43 -2
- package/dist/tree/index.js +1 -1
- package/dist/tree/tree.css +71 -0
- package/dist/{types-p9j2Fac7.d.ts → types-DGcCsxs8.d.ts} +72 -7
- package/dist/{types-Bs_gVkVI.d.ts → types-DWS-s9z-.d.ts} +1 -9
- package/dist/{types-DO-b0LNj.d.ts → types-Djdq6Xkd.d.ts} +9 -8
- package/dist/{types-BOSGNvil.d.ts → types-E3HrTPg2.d.ts} +7 -13
- package/dist/{types-C7HFvdtk.d.ts → types-VqL6t5HH.d.ts} +82 -5
- package/dist/{watch-B5RTR-WX.d.ts → watch-BTYOJpQU.d.ts} +18 -1
- package/dist/{words-DagJdwIA.d.ts → words-AHHfiaRY.d.ts} +31 -2
- package/dist/words-BGrHZAJJ.d.ts +46 -0
- package/dist/{words-c-3h9xQQ.d.ts → words-BMzV04b1.d.ts} +1 -1
- package/dist/words-CRYvAiHX.d.ts +105 -0
- package/dist/words-D8kf071T.d.ts +93 -0
- package/dist/{words-N4ebT88_.d.ts → words-DgHcY5qB.d.ts} +9 -3
- package/dist/{words-C74EUpFr.d.ts → words-XH0TvZPg.d.ts} +60 -4
- package/package.json +8 -8
- package/dist/chunk-66CKSTXP.js +0 -14
- package/dist/chunk-672FAEKU.js +0 -1
- package/dist/chunk-67VZRQLG.js +0 -13
- package/dist/chunk-6PS6STGA.js +0 -1
- package/dist/chunk-7A7O2JWQ.js +0 -1
- package/dist/chunk-7JTL3P6P.js +0 -1
- package/dist/chunk-F2PIG3AA.js +0 -10
- package/dist/chunk-H4B72XCJ.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-XUE5O62R.js +0 -3
- package/dist/chunk-ZT7M24XD.js +0 -1
- package/dist/editor/index.d.ts +0 -10
- package/dist/editor/index.js +0 -1
- package/dist/words-CD3WADyb.d.ts +0 -51
- /package/dist/{chunk-2PHD4L6A.js → chunk-OWBO4WKA.js} +0 -0
package/README.md
CHANGED
|
@@ -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
|
|
@@ -297,7 +297,7 @@ the editor owns the device, the save status, the widths and which panels are ope
|
|
|
297
297
|
```tsx
|
|
298
298
|
import {
|
|
299
299
|
EaselTopBar, TopBarCluster, TopBarDivider, EaselIconButton, DeviceSwitch,
|
|
300
|
-
SaveState, PanelResizeHandle, InspectorHead, DEFAULT_PANEL_W, panelWidthVars,
|
|
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
|
|
|
@@ -318,13 +318,24 @@ import 'efficient-easel/chrome.css'; // once, after shell.css
|
|
|
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
|
|
@@ -812,10 +1020,11 @@ block's name, a field's label, a tile's caption, the document's `noun`.
|
|
|
812
1020
|
|
|
813
1021
|
## The names before the Easel rename
|
|
814
1022
|
|
|
815
|
-
The package took the name `efficient-easel` at 0.96.0. Its exports followed
|
|
816
|
-
every `Editor…` name is an `Easel…` one, and the store's subpath is
|
|
817
|
-
`efficient-easel/easel`.
|
|
818
|
-
|
|
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.
|
|
819
1028
|
|
|
820
1029
|
| Subpath | Was | Is |
|
|
821
1030
|
| --- | --- | --- |
|
|
@@ -831,9 +1040,6 @@ same value as its new name, so an app moves file by file.
|
|
|
831
1040
|
| `/strings` | `EditorStrings`, `DEFAULT_EDITOR_STRINGS`, `mergeEditorStrings`, `EditorStringsProvider`, `EditorStringsProviderProps`, `useEditorStrings`, `useEditorLocale` | `EaselStrings`, `DEFAULT_EASEL_STRINGS`, `mergeEaselStrings`, `EaselStringsProvider`, `EaselStringsProviderProps`, `useEaselStrings`, `useEaselLocale` |
|
|
832
1041
|
| `/edit-lock` | `newEditorClientId` | `newEaselClientId` |
|
|
833
1042
|
|
|
834
|
-
`efficient-easel/editor` and `efficient-easel/easel` are built on one chunk, so
|
|
835
|
-
a provider imported from one is read by a hook imported from the other.
|
|
836
|
-
|
|
837
1043
|
Not renamed, because other code holds them: the `.es-editor` class and every
|
|
838
1044
|
other `es-` class, the `--es-*` variables, the `editor-shell:style-clipboard`
|
|
839
1045
|
storage key, and the rail's default label, the word "Editor", which a screen
|
|
@@ -855,3 +1061,13 @@ docker run --rm -v "$PWD":/w -w /w node:26-slim \
|
|
|
855
1061
|
jsdom window in `tests/_dom-env.ts` and runs in its own process.
|
|
856
1062
|
- `npm run build` — `tsup` → `dist/` (root + the `rail`, `shell` and `gold`
|
|
857
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,7 +1,7 @@
|
|
|
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-
|
|
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
5
|
import '../wordFit-D4S36PvB.js';
|
|
6
6
|
import 'react';
|
|
7
7
|
import '../edit-lock/index.js';
|
|
@@ -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';
|
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
|
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";
|
package/dist/chrome/chrome.css
CHANGED
|
@@ -9,7 +9,16 @@
|
|
|
9
9
|
* colours are the same colours, not copies of them.
|
|
10
10
|
*
|
|
11
11
|
* Needs `efficient-easel/shell.css` for the `--es-*` tokens. Widths come in as
|
|
12
|
-
* `--es-left-w` / `--es-right-w` from `panelWidthVars()
|
|
12
|
+
* `--es-left-w` / `--es-right-w` from `panelWidthVars()` or `setPanelWidthVars()`,
|
|
13
|
+
* set on the editor ROOT.
|
|
14
|
+
*
|
|
15
|
+
* EVERY READ OF A WIDTH CARRIES ITS DEFAULT (`DEFAULT_PANEL_W`: 288 / 320;
|
|
16
|
+
* `tests/csp-server-render` holds the two together). A server-rendered editor
|
|
17
|
+
* has no width in scope until its script runs — and under a
|
|
18
|
+
* Content-Security-Policy that refuses `style` attributes, none from the HTML
|
|
19
|
+
* at all — and without a fallback the rail's column was 1px wide. A fallback
|
|
20
|
+
* at the read, not a declaration on a root: a host may set the variables on an
|
|
21
|
+
* ancestor of its own, and a default declared lower down would shadow them.
|
|
13
22
|
*
|
|
14
23
|
* Light only (owner, 2026-08-06: no dark mode in the editor or the designer).
|
|
15
24
|
*/
|
|
@@ -47,7 +56,7 @@
|
|
|
47
56
|
want more room than the window has — the column collapses to its buttons
|
|
48
57
|
with a correct `width` sitting right here (found twice on :5173, 2026-09-04). */
|
|
49
58
|
flex: none;
|
|
50
|
-
width: var(--es-left-w);
|
|
59
|
+
width: var(--es-left-w, 288px);
|
|
51
60
|
box-sizing: border-box;
|
|
52
61
|
height: 100%;
|
|
53
62
|
gap: 0;
|
|
@@ -320,11 +329,11 @@
|
|
|
320
329
|
container-type: inline-size;
|
|
321
330
|
}
|
|
322
331
|
.es-sidepanel--left {
|
|
323
|
-
width: var(--es-left-w);
|
|
332
|
+
width: var(--es-left-w, 288px);
|
|
324
333
|
border-right: 1px solid var(--es-line);
|
|
325
334
|
}
|
|
326
335
|
.es-sidepanel--right {
|
|
327
|
-
width: var(--es-right-w);
|
|
336
|
+
width: var(--es-right-w, 320px);
|
|
328
337
|
border-left: 1px solid var(--es-line);
|
|
329
338
|
}
|
|
330
339
|
.es-panel-head {
|
|
@@ -363,11 +372,11 @@
|
|
|
363
372
|
touch-action: none;
|
|
364
373
|
}
|
|
365
374
|
.es-resizer--left {
|
|
366
|
-
left: var(--es-left-w);
|
|
375
|
+
left: var(--es-left-w, 288px);
|
|
367
376
|
justify-content: flex-start;
|
|
368
377
|
}
|
|
369
378
|
.es-resizer--right {
|
|
370
|
-
right: var(--es-right-w);
|
|
379
|
+
right: var(--es-right-w, 320px);
|
|
371
380
|
justify-content: flex-end;
|
|
372
381
|
}
|
|
373
382
|
.es-resizer-bar {
|
|
@@ -393,45 +402,42 @@ body.es-col-resizing iframe {
|
|
|
393
402
|
}
|
|
394
403
|
|
|
395
404
|
/* ── Inspector head ──────────────────────────────────────────────────────── */
|
|
396
|
-
/*
|
|
397
|
-
|
|
405
|
+
/* ← Go up one level: the blue text link (owner ruling 2026-10-06, the website
|
|
406
|
+
Locations panel's look, one look in both editors). The arrow and the parent's
|
|
407
|
+
name in the accent colour, 12px; no box, no fill, no divider, and NO hover
|
|
408
|
+
change, as on the link it was taken from. Only the focus ring shows. At the
|
|
409
|
+
panel top it sits in the panel's own inset, 16 and 12 under 260px (one
|
|
410
|
+
spacing scale, owner ruling 2026-10-02); `inline` drops the wrapper. */
|
|
398
411
|
.es-insp-up {
|
|
399
412
|
display: flex;
|
|
400
|
-
align-items: center;
|
|
401
413
|
flex: none;
|
|
402
|
-
|
|
403
|
-
|
|
414
|
+
min-width: 0;
|
|
415
|
+
padding: 12px 16px 0;
|
|
404
416
|
}
|
|
405
417
|
.es-insp-up-btn {
|
|
406
|
-
display: flex;
|
|
407
|
-
align-items:
|
|
408
|
-
gap:
|
|
409
|
-
|
|
418
|
+
display: inline-flex;
|
|
419
|
+
align-items: baseline;
|
|
420
|
+
gap: 4px;
|
|
421
|
+
max-width: 100%;
|
|
410
422
|
min-width: 0;
|
|
411
|
-
padding:
|
|
423
|
+
padding: 0;
|
|
412
424
|
border: 0;
|
|
413
|
-
|
|
414
|
-
background: transparent;
|
|
415
|
-
cursor: pointer;
|
|
425
|
+
background: none;
|
|
416
426
|
font: inherit;
|
|
417
427
|
font-size: 12px;
|
|
418
|
-
color: var(--es-
|
|
428
|
+
color: var(--es-accent);
|
|
419
429
|
text-align: left;
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
.es-insp-up-btn:hover {
|
|
423
|
-
background: var(--es-hover);
|
|
424
|
-
color: var(--es-text);
|
|
430
|
+
cursor: pointer;
|
|
425
431
|
}
|
|
426
432
|
.es-insp-up-btn:focus-visible {
|
|
427
433
|
outline: 2px solid var(--es-accent);
|
|
428
|
-
outline-offset:
|
|
434
|
+
outline-offset: 2px;
|
|
429
435
|
}
|
|
430
|
-
.es-insp-up-
|
|
431
|
-
display: inline-flex;
|
|
436
|
+
.es-insp-up-arrow {
|
|
432
437
|
flex: none;
|
|
433
438
|
}
|
|
434
439
|
.es-insp-up-name {
|
|
440
|
+
min-width: 0;
|
|
435
441
|
overflow: hidden;
|
|
436
442
|
text-overflow: ellipsis;
|
|
437
443
|
white-space: nowrap;
|
|
@@ -450,7 +456,46 @@ body.es-col-resizing iframe {
|
|
|
450
456
|
is the host's. */
|
|
451
457
|
@container (width < 260px) {
|
|
452
458
|
.es-panel-head,
|
|
453
|
-
.es-insp-head
|
|
459
|
+
.es-insp-head,
|
|
460
|
+
.es-insp-up { padding-inline: 12px; }
|
|
461
|
+
}
|
|
462
|
+
/* A HEAD WITH A WAY-OUT WORD, UNDER 260px (card [MERGE] One Media Panel Head
|
|
463
|
+
For Both Editors, GG 2026-10-06). At a 220px panel the chip, Cancel, the
|
|
464
|
+
hairline and Hide left the titles about 38px: "Choose image" drew "Ch…" in
|
|
465
|
+
both editors. Nothing is cut (owner: change the layout, never the feature):
|
|
466
|
+
the word takes its own row, under the titles, and the hairline goes because
|
|
467
|
+
nothing stands beside Hide to part it from.
|
|
468
|
+
row 1 [chip] Choose image [hide]
|
|
469
|
+
For Background image
|
|
470
|
+
row 2 [Cancel]
|
|
471
|
+
The actions box steps aside (`display: contents`) so its buttons can sit in
|
|
472
|
+
different rows; every glyph button is locked to row 1 and takes the next
|
|
473
|
+
free column, so a head with several tools keeps them side by side. */
|
|
474
|
+
@container (width < 260px) {
|
|
475
|
+
.es-insp-head:has(.es-insp-textbtn) {
|
|
476
|
+
display: grid;
|
|
477
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
478
|
+
grid-auto-columns: auto;
|
|
479
|
+
align-items: center;
|
|
480
|
+
column-gap: 12px;
|
|
481
|
+
row-gap: 8px;
|
|
482
|
+
}
|
|
483
|
+
.es-insp-head:has(.es-insp-textbtn) > .es-insp-chip,
|
|
484
|
+
.es-insp-head:has(.es-insp-textbtn) > .es-insp-titles,
|
|
485
|
+
.es-insp-head:has(.es-insp-textbtn) .es-insp-iconbtn {
|
|
486
|
+
grid-row: 1;
|
|
487
|
+
}
|
|
488
|
+
.es-insp-head:has(.es-insp-textbtn) > .es-insp-actions {
|
|
489
|
+
display: contents;
|
|
490
|
+
}
|
|
491
|
+
.es-insp-head:has(.es-insp-textbtn) .es-insp-textbtn {
|
|
492
|
+
grid-row: 2;
|
|
493
|
+
grid-column: 2;
|
|
494
|
+
justify-self: start;
|
|
495
|
+
}
|
|
496
|
+
.es-insp-head:has(.es-insp-textbtn) .es-insp-divider {
|
|
497
|
+
display: none;
|
|
498
|
+
}
|
|
454
499
|
}
|
|
455
500
|
.es-insp-chip {
|
|
456
501
|
width: 26px;
|
|
@@ -519,6 +564,28 @@ body.es-col-resizing iframe {
|
|
|
519
564
|
color: var(--es-accent);
|
|
520
565
|
}
|
|
521
566
|
|
|
567
|
+
/* A side panel's head has two pieces besides its glyph buttons (MediaPanelHead,
|
|
568
|
+
card [MERGE] One Media Panel Head, 2026-10-06; until then each editor's own
|
|
569
|
+
sheet drew them, as `.sf-head-*` and `.cd-head-*`). A MODE's way out is a
|
|
570
|
+
word ("Cancel", "Done") on the ghost button, drawn to the 28px of the head's
|
|
571
|
+
icon buttons so the row keeps one height (its sides are the scale's 12; both
|
|
572
|
+
editors drew 10, off the 2/4/8/12/16/32 scale); the hairline parts the
|
|
573
|
+
panel's own tools from the Hide panel button that ends every head. */
|
|
574
|
+
.es-btn-ghost.es-insp-textbtn {
|
|
575
|
+
height: 28px;
|
|
576
|
+
padding: 0 12px;
|
|
577
|
+
font-size: 12.5px;
|
|
578
|
+
border-radius: 7px;
|
|
579
|
+
white-space: nowrap;
|
|
580
|
+
}
|
|
581
|
+
.es-insp-divider {
|
|
582
|
+
width: 1px;
|
|
583
|
+
height: 16px;
|
|
584
|
+
margin: 0 4px;
|
|
585
|
+
background: var(--es-line);
|
|
586
|
+
flex: none;
|
|
587
|
+
}
|
|
588
|
+
|
|
522
589
|
/* ── Edit notice — somebody else is in here, or a save was refused ───────── */
|
|
523
590
|
/* A bar under the top bar, never a dialog (blueprint 2.5): the operator has to
|
|
524
591
|
be able to read and copy from the screen it warns them about. No token of its
|