@the-portland-company/canvas-editor 0.4.0 → 0.4.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2 — tray fits the canvas column
4
+
5
+ - The tile strip shrinks and scrolls horizontally so the Side | Center toggle,
6
+ labels, undo/redo and × never clip outside the canvas column.
7
+
8
+ ## 0.4.1 — tray always on screen
9
+
10
+ - The block tray is fixed to the viewport over the canvas column (live-measured
11
+ left/width, 1rem above the bottom) instead of sticking to the canvas scroll
12
+ box, which could sit below the fold inside a host page. `BlockToolbar` takes
13
+ an `anchor` ref to the canvas element.
14
+
3
15
  ## 0.4.0 — V1-inspired chrome: floating block tray, dashed block outlines, settings on the left
4
16
 
5
17
  Per Spencer's 2026-09-11 direction that V2 (email.politogyvrm.com) take its
package/dist/canvas.css CHANGED
@@ -174,19 +174,22 @@
174
174
 
175
175
  /* ---- block tray (floating palette) --------------------------------------- */
176
176
 
177
+ /* Fixed to the viewport, spanning the canvas column (left/width set inline
178
+ from a live measurement) so it is always on screen; once dragged it keeps
179
+ the pointer-derived left/top instead. */
177
180
  .ce-tray-dock {
178
- position: sticky; bottom: 0.25rem; z-index: 10;
181
+ position: fixed; bottom: 1rem; z-index: 40;
179
182
  display: flex; justify-content: center;
180
- margin-top: auto;
183
+ padding: 0 0.5rem;
181
184
  pointer-events: none;
182
185
  }
183
186
 
184
- .ce-tray-dock.is-floating { position: fixed; bottom: auto; margin: 0; }
187
+ .ce-tray-dock.is-floating { bottom: auto; width: auto; padding: 0; }
185
188
 
186
189
  .ce-tray {
187
190
  pointer-events: auto;
188
191
  display: flex; align-items: center; gap: 1rem;
189
- width: fit-content; max-width: 100%;
192
+ width: fit-content; max-width: 100%; min-width: 0;
190
193
  padding: 9px 10px 4px;
191
194
  background: var(--color-surface);
192
195
  border: 1px solid var(--color-border);
@@ -196,12 +199,17 @@
196
199
 
197
200
  .dark .ce-tray { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.5); }
198
201
 
202
+ /* min-width:0 lets the strip shrink and scroll so the controls after it
203
+ (Side | Center, labels, undo/redo, ×) stay inside the canvas column. */
199
204
  .ce-tray-tiles {
200
205
  display: flex; align-items: stretch; gap: 0.5rem;
206
+ flex: 0 1 auto; min-width: 0;
201
207
  overflow-x: auto; padding-bottom: 0.25rem;
202
208
  scrollbar-width: thin;
203
209
  }
204
210
 
211
+ .ce-tray-tiles > .ce-tile { flex: 0 0 auto; }
212
+
205
213
  .ce-tile {
206
214
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.25rem;
207
215
  min-width: 2.75rem; height: 2.75rem; padding: 0.5rem;
package/dist/editor.cjs CHANGED
@@ -3937,6 +3937,7 @@ function trayItems() {
3937
3937
  return [...visible].sort((a, b) => rank(a) - rank(b));
3938
3938
  }
3939
3939
  function BlockToolbar({
3940
+ anchor,
3940
3941
  insertMode,
3941
3942
  onInsertModeChange,
3942
3943
  showLabels,
@@ -3950,6 +3951,25 @@ function BlockToolbar({
3950
3951
  const [pos, setPos] = react.useState(null);
3951
3952
  const offset = react.useRef(null);
3952
3953
  const pillRef = react.useRef(null);
3954
+ const [bounds, setBounds] = react.useState(null);
3955
+ react.useEffect(() => {
3956
+ const el = anchor.current;
3957
+ if (!el) return;
3958
+ const measure = () => {
3959
+ const r = el.getBoundingClientRect();
3960
+ setBounds({ left: Math.round(r.left), width: Math.round(r.width) });
3961
+ };
3962
+ measure();
3963
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;
3964
+ ro?.observe(el);
3965
+ window.addEventListener("resize", measure);
3966
+ window.addEventListener("scroll", measure, true);
3967
+ return () => {
3968
+ ro?.disconnect();
3969
+ window.removeEventListener("resize", measure);
3970
+ window.removeEventListener("scroll", measure, true);
3971
+ };
3972
+ }, [anchor]);
3953
3973
  const onHandlePointerDown = react.useCallback((e) => {
3954
3974
  const rect = pillRef.current?.getBoundingClientRect();
3955
3975
  if (!rect) return;
@@ -3972,29 +3992,36 @@ function BlockToolbar({
3972
3992
  window.addEventListener("pointerup", up);
3973
3993
  return up;
3974
3994
  }, [pos !== null]);
3975
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `ce-tray-dock${pos ? " is-floating" : ""}`, style: pos ? { left: pos.x, top: pos.y } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: pillRef, className: "ce-tray", role: "toolbar", "aria-label": "Blocks", children: [
3976
- /* @__PURE__ */ jsxRuntime.jsx(
3977
- "button",
3978
- {
3979
- type: "button",
3980
- className: "ce-tray-btn is-grab",
3981
- onPointerDown: onHandlePointerDown,
3982
- onDoubleClick: () => setPos(null),
3983
- title: "Move toolbar (double-click to dock)",
3984
- "aria-label": "Move toolbar",
3985
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "move", size: 18 })
3986
- }
3987
- ),
3988
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ce-tray-tiles", children: trayItems().map((def) => /* @__PURE__ */ jsxRuntime.jsx(TrayTile, { def, showLabel: showLabels }, def.type)) }),
3989
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ce-seg", role: "radiogroup", "aria-label": "Insert control placement", children: [
3990
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", role: "radio", "aria-checked": insertMode === "side", className: `ce-seg-btn${insertMode === "side" ? " is-active" : ""}`, onClick: () => onInsertModeChange("side"), children: "Side" }),
3991
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", role: "radio", "aria-checked": insertMode === "center", className: `ce-seg-btn${insertMode === "center" ? " is-active" : ""}`, onClick: () => onInsertModeChange("center"), children: "Center" })
3992
- ] }),
3993
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: `ce-tray-btn${showLabels ? " is-active" : ""}`, onClick: onToggleLabels, title: showLabels ? "Hide labels" : "Show labels", "aria-pressed": showLabels, "aria-label": "Toggle tile labels", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "type", size: 18 }) }),
3994
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ce-tray-btn", onClick: onUndo, disabled: !canUndo, title: "Undo (\u2318Z)", "aria-label": "Undo", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "rotate-ccw", size: 18 }) }),
3995
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ce-tray-btn", onClick: onRedo, disabled: !canRedo, title: "Redo (\u2318\u21E7Z)", "aria-label": "Redo", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "rotate-cw", size: 18 }) }),
3996
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ce-tray-btn", onClick: onClose, title: "Close blocks toolbar", "aria-label": "Close blocks toolbar", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "x", size: 18 }) })
3997
- ] }) });
3995
+ return /* @__PURE__ */ jsxRuntime.jsx(
3996
+ "div",
3997
+ {
3998
+ className: `ce-tray-dock${pos ? " is-floating" : ""}`,
3999
+ style: pos ? { left: pos.x, top: pos.y } : bounds ? { left: bounds.left, width: bounds.width } : void 0,
4000
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: pillRef, className: "ce-tray", role: "toolbar", "aria-label": "Blocks", children: [
4001
+ /* @__PURE__ */ jsxRuntime.jsx(
4002
+ "button",
4003
+ {
4004
+ type: "button",
4005
+ className: "ce-tray-btn is-grab",
4006
+ onPointerDown: onHandlePointerDown,
4007
+ onDoubleClick: () => setPos(null),
4008
+ title: "Move toolbar (double-click to dock)",
4009
+ "aria-label": "Move toolbar",
4010
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "move", size: 18 })
4011
+ }
4012
+ ),
4013
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ce-tray-tiles", children: trayItems().map((def) => /* @__PURE__ */ jsxRuntime.jsx(TrayTile, { def, showLabel: showLabels }, def.type)) }),
4014
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ce-seg", role: "radiogroup", "aria-label": "Insert control placement", children: [
4015
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", role: "radio", "aria-checked": insertMode === "side", className: `ce-seg-btn${insertMode === "side" ? " is-active" : ""}`, onClick: () => onInsertModeChange("side"), children: "Side" }),
4016
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", role: "radio", "aria-checked": insertMode === "center", className: `ce-seg-btn${insertMode === "center" ? " is-active" : ""}`, onClick: () => onInsertModeChange("center"), children: "Center" })
4017
+ ] }),
4018
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: `ce-tray-btn${showLabels ? " is-active" : ""}`, onClick: onToggleLabels, title: showLabels ? "Hide labels" : "Show labels", "aria-pressed": showLabels, "aria-label": "Toggle tile labels", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "type", size: 18 }) }),
4019
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ce-tray-btn", onClick: onUndo, disabled: !canUndo, title: "Undo (\u2318Z)", "aria-label": "Undo", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "rotate-ccw", size: 18 }) }),
4020
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ce-tray-btn", onClick: onRedo, disabled: !canRedo, title: "Redo (\u2318\u21E7Z)", "aria-label": "Redo", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "rotate-cw", size: 18 }) }),
4021
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ce-tray-btn", onClick: onClose, title: "Close blocks toolbar", "aria-label": "Close blocks toolbar", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "x", size: 18 }) })
4022
+ ] })
4023
+ }
4024
+ );
3998
4025
  }
3999
4026
  function TrayTile({ def, showLabel }) {
4000
4027
  const { attributes, listeners, setNodeRef, isDragging } = core$1.useDraggable({
@@ -4196,6 +4223,7 @@ function CanvasEditor({
4196
4223
  writePref(TRAY_OPEN_KEY, open ? "1" : "0");
4197
4224
  };
4198
4225
  const [trayLabels, setTrayLabelsState] = react.useState(() => readPref(TRAY_LABELS_KEY) === "1");
4226
+ const canvasRef = react.useRef(null);
4199
4227
  const toggleTrayLabels = () => setTrayLabelsState((v) => {
4200
4228
  writePref(TRAY_LABELS_KEY, v ? "0" : "1");
4201
4229
  return !v;
@@ -4581,7 +4609,7 @@ function CanvasEditor({
4581
4609
  metaPanel && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t pt-4", style: { borderColor: "var(--color-border)" }, children: metaPanel({ doc: currentDoc(), setMeta }) })
4582
4610
  ] })
4583
4611
  ] }),
4584
- /* @__PURE__ */ jsxRuntime.jsxs("main", { className: "ce-canvas", style: { background: settings?.outerBackground || void 0 }, children: [
4612
+ /* @__PURE__ */ jsxRuntime.jsxs("main", { ref: canvasRef, className: "ce-canvas", style: { background: settings?.outerBackground || void 0 }, children: [
4585
4613
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ce-canvas-body", children: /* @__PURE__ */ jsxRuntime.jsx(
4586
4614
  CanvasArea,
4587
4615
  {
@@ -4601,6 +4629,7 @@ function CanvasEditor({
4601
4629
  trayOpen && /* @__PURE__ */ jsxRuntime.jsx(
4602
4630
  BlockToolbar,
4603
4631
  {
4632
+ anchor: canvasRef,
4604
4633
  insertMode,
4605
4634
  onInsertModeChange: setInsertMode,
4606
4635
  showLabels: trayLabels,