sketchmark 2.1.7 → 2.1.9

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/bin/editor-ui.cjs CHANGED
@@ -8,33 +8,28 @@ function editorHtml(title, options = {}) {
8
8
  const mp4MuxerUrl = options.mp4MuxerUrl || "";
9
9
  const mp4MuxerSource = mp4MuxerUrl ? "" : resolveMp4MuxerSource(options.mp4MuxerSource);
10
10
  const serverExportFallback = options.serverExportFallback !== false;
11
- const canvasStageRender = options.canvasStageRender === true;
12
11
  const localDocumentControls = options.localDocumentControls === true;
13
12
  const bootstrapScript = typeof options.bootstrapScript === "string" ? options.bootstrapScript : "";
14
13
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sketchmark Editor - ${escapeHtml(title)}</title><style>
15
14
  @font-face{font-family:'Roboto';src:url('/fonts/Roboto-Light.ttf') format('truetype');font-weight:300;font-style:normal;font-display:swap}
16
15
  @font-face{font-family:'Roboto';src:url('/fonts/Roboto-Regular.ttf') format('truetype');font-weight:400;font-style:normal;font-display:swap}
17
16
  @font-face{font-family:'Roboto';src:url('/fonts/Roboto-Bold.ttf') format('truetype');font-weight:700;font-style:normal;font-display:swap}
18
- html,body{margin:0;width:100%;height:100%;font:13px Roboto,Arial,sans-serif;background:#c0c0c0;color:#000;overflow:hidden}
19
- body{display:grid;grid-template-columns:240px 1fr 300px;grid-template-rows:1fr 165px;min-width:900px;overflow:hidden}
17
+ html,body{margin:0;width:100%;height:100%;font:13px Roboto,Arial,sans-serif;background:#c0c0c0;color:#000;overflow:hidden}
18
+ body{--tree-width:240px;display:grid;grid-template-columns:var(--tree-width) 6px minmax(0,1fr) 300px;grid-template-rows:1fr 165px;min-width:900px;overflow:hidden}
19
+ body.resizingSidebar{cursor:col-resize;user-select:none}
20
20
  button,input,select,textarea{font:13px Roboto,Arial,sans-serif}
21
21
  button{padding:3px 8px}
22
22
  input,select,textarea{box-sizing:border-box;width:100%}
23
23
  textarea{min-height:88px;padding:4px;resize:vertical}
24
- #tree,#inspector,#timeline{background:#f3f4f6;border:1px solid #c6ccd6;overflow:auto;padding:6px;scrollbar-width:none;-ms-overflow-style:none}
25
- #tree{grid-row:1/3}
26
- #stageWrap{position:relative;display:grid;place-items:center;min-width:0;min-height:0;padding:0;background:#fff;overflow:hidden;cursor:default}
27
- #stageWrap.panning{cursor:grabbing}
24
+ #tree,#inspector,#timeline{background:#f3f4f6;border:1px solid #c6ccd6;overflow:auto;padding:6px;scrollbar-width:none;-ms-overflow-style:none}
25
+ #tree{grid-column:1;grid-row:1/3;display:flex;flex-direction:column;min-height:0;overflow:hidden}
26
+ #treeResizeHandle{grid-column:2;grid-row:1/3;background:#d1d5db;border-left:1px solid #aab2bf;border-right:1px solid #aab2bf;cursor:col-resize;z-index:5}
27
+ #treeResizeHandle:hover,body.resizingSidebar #treeResizeHandle{background:#9ca3af}
28
+ #stageWrap{position:relative;display:grid;place-items:center;min-width:0;min-height:0;padding:0;background:#fff;overflow:hidden;cursor:default}
29
+ #stageWrap{grid-column:3;grid-row:1}
30
+ #stageWrap.panning{cursor:grabbing}
28
31
  #stage{display:grid;place-items:center;min-width:0;min-height:0;position:relative}
29
32
  #stage svg{max-width:100%;max-height:calc(100vh - 190px);background:white;border:1px solid #333;overflow:visible}
30
- ${canvasStageRender ? `#stage.sketchmarkCanvasStage{display:block;position:relative;line-height:0;background:white;border:1px solid #333;box-sizing:content-box}
31
- #stageCanvas{display:block;background:white}
32
- #stage.sketchmarkCanvasStage>svg{position:absolute;left:0;top:0;width:100%;height:100%;max-width:none;max-height:none;background:transparent;border:0;box-sizing:border-box;z-index:2;pointer-events:auto}
33
- #stage.sketchmarkCanvasStage>svg:not(#stageLiveSvg)>*:not(defs):not(#__sketchmark_handles):not(#__sketchmark_drag_preview){opacity:0}
34
- #stage.sketchmarkCanvasStage>svg:not(#stageLiveSvg) #__sketchmark_handles,#stage.sketchmarkCanvasStage>svg:not(#stageLiveSvg) #__sketchmark_drag_preview{opacity:1}
35
- #stage.sketchmarkCanvasStage>#stageLiveSvg{display:none;position:absolute;left:0;top:0;width:100%;height:100%;max-width:none;max-height:none;background:transparent;border:0;box-sizing:border-box;z-index:1;pointer-events:none;overflow:visible}
36
- #stage.sketchmarkCanvasStage.liveSvgDrag>#stageCanvas{visibility:hidden}
37
- #stage.sketchmarkCanvasStage.liveSvgDrag>#stageLiveSvg{display:block}` : ""}
38
33
  ${localDocumentControls ? `.browserFileGrid{display:grid;gap:6px}
39
34
  .browserFileActions{display:grid;grid-template-columns:1fr 1fr;gap:4px}
40
35
  .browserFileInput{font-size:12px}
@@ -42,25 +37,46 @@ ${localDocumentControls ? `.browserFileGrid{display:grid;gap:6px}
42
37
  #viewportHud{position:absolute;right:12px;bottom:12px;display:flex;flex-direction:column;gap:6px;align-items:stretch;padding:6px;background:rgba(238,238,238,.95);border:1px solid #8f96a3;z-index:3}
43
38
  #viewportHud button{padding:1px 8px;min-width:42px}
44
39
  #zoomLabel{min-width:42px;text-align:center;font-weight:bold;color:#111827}
45
- #timeline{grid-column:2/4}
40
+ #inspector{grid-column:4;grid-row:1}
41
+ #timeline{grid-column:3/5;grid-row:2}
46
42
  .row{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin:4px 0}
47
43
  .row label{display:block}
48
44
  .colorField{display:grid;grid-template-columns:34px 1fr;gap:6px;align-items:center}
49
45
  .colorField input[type=color]{width:34px;height:28px;padding:0;border:1px solid #8f96a3;background:#fff;cursor:pointer}
50
46
  .stack{display:grid;gap:5px}.section{margin:0 0 10px}.label{display:block;font-weight:bold;margin:0 0 3px}
51
- .panelGroup{margin:0 0 8px;border:1px solid #c9cfdb;background:#f8fafc}
52
- .panelGroup > summary{list-style:none;cursor:pointer;padding:6px 8px;font-weight:bold;display:flex;justify-content:space-between;align-items:center;gap:8px}
53
- .panelGroup > summary::-webkit-details-marker{display:none}
54
- .panelGroup > summary::after{content:"+";font-weight:bold;color:#4b5563}
55
- .panelGroup[open] > summary::after{content:"-"}
56
- .panelGroup > summary .summaryMeta{font-size:11px;color:#4b5563;font-weight:normal;margin-left:auto}
57
- .panelBody{padding:6px 8px;border-top:1px solid #d8dee8}
58
- .subhead{display:block;font-size:11px;font-weight:bold;color:#374151;margin:6px 0 2px}
59
- .treeRow{display:grid;grid-template-columns:16px 20px 20px 1fr;gap:3px;align-items:center;margin:1px 0}
60
- .treePad{display:block;width:16px;height:20px}
61
- .treeCtl{height:20px;padding:0;border:1px solid #9aa1ad;background:#f8fafc;cursor:pointer;line-height:18px;font-size:11px}
62
- .treeCtl.active{background:#003399;color:#fff;border-color:#003399}
63
- .canvasError{color:#900;font-size:11px;min-height:14px}
47
+ .panelGroup{margin:0 0 8px;border:1px solid #c9cfdb;background:#f8fafc}
48
+ .panelGroup > summary{list-style:none;cursor:pointer;padding:6px 8px;font-weight:bold;display:flex;justify-content:space-between;align-items:center;gap:8px}
49
+ .panelGroup > summary::-webkit-details-marker{display:none}
50
+ .panelGroup > summary::after{content:"+";font-weight:bold;color:#4b5563}
51
+ .panelGroup[open] > summary::after{content:"-"}
52
+ .panelGroup > summary .summaryMeta{font-size:11px;color:#4b5563;font-weight:normal;margin-left:auto}
53
+ .panelBody{padding:6px 8px;border-top:1px solid #d8dee8}
54
+ #tree > .panelGroup{flex:0 0 auto}
55
+ #tree > .panelGroup[data-panel='tree-elements']{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;margin-bottom:0}
56
+ #tree > .panelGroup[data-panel='tree-elements']:not([open]){flex:0 0 auto}
57
+ #tree > .panelGroup[data-panel='tree-elements'] > summary{flex:0 0 auto}
58
+ #tree > .panelGroup[data-panel='tree-elements'] > .panelBody{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;overflow:hidden}
59
+ .elementsFixed{flex:0 0 auto}
60
+ .elementsTreeList{flex:1 1 auto;min-height:0;overflow:auto;padding-right:2px;scrollbar-width:none;-ms-overflow-style:none}
61
+ .subhead{display:block;font-size:11px;font-weight:bold;color:#374151;margin:6px 0 2px}
62
+ .treeRow{display:grid;grid-template-columns:18px 20px 20px 20px 1fr;gap:3px;align-items:center;margin:1px 0}
63
+ .treePad{display:block;width:18px;height:20px}
64
+ .treeCtl{height:20px;padding:0;border:1px solid #9aa1ad;background:#f8fafc;cursor:pointer;line-height:18px;font-size:11px}
65
+ .treeCtl.active{background:#003399;color:#fff;border-color:#003399}
66
+ .treeCtl:disabled{opacity:.35;cursor:default}
67
+ .treeFold{position:relative;border-color:transparent;background:transparent}
68
+ .treeFold:hover{background:#e5e7eb;border-color:#cbd5e1}
69
+ .treeFold::before{content:"";position:absolute;left:6px;top:5px;border-style:solid;border-width:5px 0 5px 7px;border-color:transparent transparent transparent #374151}
70
+ .treeFold.expanded::before{left:4px;top:7px;border-width:7px 5px 0 5px;border-color:#374151 transparent transparent transparent}
71
+ .layerBar{display:grid;gap:4px;margin:0 0 6px;padding:5px;border:1px solid #cbd5e1;background:#fff}
72
+ .layerMeta{font-size:11px;color:#374151;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
73
+ .layerActions{display:grid;grid-template-columns:repeat(5,1fr);gap:4px}
74
+ .layerActions button{padding:2px 0;min-width:0}
75
+ .treeDanger{color:#9f1239}
76
+ .treeDanger:hover:not(:disabled){background:#fee2e2;border-color:#f43f5e}
77
+ .insertBar{display:grid;grid-template-columns:1fr 34px;gap:4px;margin-bottom:6px}
78
+ .treeEmpty{font-size:11px;color:#555;padding:4px 6px}
79
+ .canvasError{color:#900;font-size:11px;min-height:14px}
64
80
  .treeBtn{display:block;width:100%;text-align:left;margin:0;border:1px solid transparent;background:#f8fafc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:2px 6px}
65
81
  .treeBtn.dim{opacity:0.6}
66
82
  .treeBtn.selected{background:#003399;color:white}.muted{color:#555}.track{border:1px solid #888;background:#ddd;padding:5px;margin:4px 0}
@@ -87,14 +103,15 @@ ${localDocumentControls ? `.browserFileGrid{display:grid;gap:6px}
87
103
  .curveModal{width:min(760px,calc(100vw - 32px));max-height:calc(100vh - 32px);overflow:auto;border:2px outset #ddd;background:#ececec;padding:8px;scrollbar-width:none;-ms-overflow-style:none}
88
104
  .curveModalBar{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}
89
105
  .curveModalContent .curvePanel{margin-top:0}
90
- #tree::-webkit-scrollbar,#inspector::-webkit-scrollbar,#timeline::-webkit-scrollbar,.curveModal::-webkit-scrollbar{width:0;height:0}
106
+ #tree::-webkit-scrollbar,#elementsTree::-webkit-scrollbar,#inspector::-webkit-scrollbar,#timeline::-webkit-scrollbar,.curveModal::-webkit-scrollbar{width:0;height:0}
91
107
  #error{color:#900;min-height:18px;margin-top:6px}.tiny{font-size:11px;color:#444}.toolbar{display:grid;grid-template-columns:auto 1fr auto auto;gap:6px;align-items:center}
92
108
  .exportButtons{display:grid;grid-template-columns:1fr 1fr;gap:4px}
93
109
  .exportButtons button{width:100%;text-align:left;padding:4px 6px}
94
110
  .exportButtons button.exportWide{grid-column:1/3}
95
- </style></head><body><aside id="tree"></aside><main id="stageWrap"><div id="stage"></div><div id="viewportHud"><button id="zoomOut" type="button" title="Zoom out">-</button><button id="zoomIn" type="button" title="Zoom in">+</button><button id="zoomFit" type="button" title="Reset zoom and pan">Fit</button><span id="zoomLabel">100%</span></div></main><aside id="inspector"></aside><section id="timeline"></section><div id="curveModalBackdrop" class="modalBackdrop hidden"><div id="curveModal" class="curveModal" role="dialog" aria-modal="true" aria-label="Interpolation Graph"><div class="curveModalBar"><strong>Interpolation Graph</strong><button id="curveModalClose" type="button">Close</button></div><div id="curveModalContent" class="curveModalContent"></div></div></div>${bootstrapScript ? `<script>\n${bootstrapScript}\n</script>` : ""}<script>
96
- const tree = document.getElementById("tree");
97
- const stageWrap = document.getElementById("stageWrap");
111
+ </style></head><body><aside id="tree"></aside><div id="treeResizeHandle" title="Resize elements panel"></div><main id="stageWrap"><div id="stage"></div><div id="viewportHud"><button id="zoomOut" type="button" title="Zoom out">-</button><button id="zoomIn" type="button" title="Zoom in">+</button><button id="zoomFit" type="button" title="Reset zoom and pan">Fit</button><span id="zoomLabel">100%</span></div></main><aside id="inspector"></aside><section id="timeline"></section><div id="curveModalBackdrop" class="modalBackdrop hidden"><div id="curveModal" class="curveModal" role="dialog" aria-modal="true" aria-label="Interpolation Graph"><div class="curveModalBar"><strong>Interpolation Graph</strong><button id="curveModalClose" type="button">Close</button></div><div id="curveModalContent" class="curveModalContent"></div></div></div>${bootstrapScript ? `<script>\n${bootstrapScript}\n</script>` : ""}<script>
112
+ const tree = document.getElementById("tree");
113
+ const treeResizeHandle = document.getElementById("treeResizeHandle");
114
+ const stageWrap = document.getElementById("stageWrap");
98
115
  const stage = document.getElementById("stage");
99
116
  const zoomOut = document.getElementById("zoomOut");
100
117
  const zoomIn = document.getElementById("zoomIn");
@@ -113,32 +130,44 @@ let currentTime = 0;
113
130
  let playing = false;
114
131
  let lastTick = 0;
115
132
  let playHandle = 0;
116
- let resolvedDoc = null;
117
- let drawScheduled = false;
118
- let drawInFlight = false;
119
- let drawQueued = false;
120
- let drag = null;
121
- let suppressClick = false;
133
+ let resolvedDoc = null;
134
+ let drawScheduled = false;
135
+ let drawInFlight = false;
136
+ let drawQueued = false;
137
+ let drag = null;
138
+ let suppressClick = false;
122
139
  const FONT_FAMILY_OPTIONS = [
123
140
  { label: "Roboto (Local)", value: "Roboto, Arial, sans-serif" }
124
141
  ];
125
- const FONT_WEIGHT_OPTIONS = [
126
- { label: "300", value: "300" },
127
- { label: "400", value: "400" },
128
- { label: "500", value: "500" },
129
- { label: "600", value: "600" },
130
- { label: "700", value: "700" }
131
- ];
132
- let collapsedGroups = new Set();
142
+ const FONT_WEIGHT_OPTIONS = [
143
+ { label: "300", value: "300" },
144
+ { label: "400", value: "400" },
145
+ { label: "500", value: "500" },
146
+ { label: "600", value: "600" },
147
+ { label: "700", value: "700" }
148
+ ];
149
+ const ELEMENT_PRESET_OPTIONS = [
150
+ { label: "Text", value: "text" },
151
+ { label: "Rectangle", value: "rectangle" },
152
+ { label: "Circle", value: "circle" },
153
+ { label: "Line", value: "line" },
154
+ { label: "Path", value: "path" },
155
+ { label: "Point", value: "point" },
156
+ { label: "Group", value: "group" }
157
+ ];
158
+ let collapsedGroups = new Set();
133
159
  let hiddenIds = new Set();
134
- let lockedIds = new Set();
135
- let parentById = Object.create(null);
136
- let childIdsById = Object.create(null);
137
- let typeById = Object.create(null);
138
- let sidebarCommitTimers = Object.create(null);
139
- let selectedSegment = null;
140
- let panelOpenState = Object.create(null);
141
- let viewport = { initialized: false, baseWidth: 1, baseHeight: 1, x: 0, y: 0, width: 1, height: 1 };
160
+ let lockedIds = new Set();
161
+ let parentById = Object.create(null);
162
+ let childIdsById = Object.create(null);
163
+ let layerIndexById = Object.create(null);
164
+ let layerCountById = Object.create(null);
165
+ let typeById = Object.create(null);
166
+ let sidebarCommitTimers = Object.create(null);
167
+ let selectedSegment = null;
168
+ let panelOpenState = Object.create(null);
169
+ let insertPresetKind = "rectangle";
170
+ let viewport = { initialized: false, baseWidth: 1, baseHeight: 1, x: 0, y: 0, width: 1, height: 1 };
142
171
  let viewportPan = null;
143
172
  let spacePanActive = false;
144
173
  const API_BASE = ${scriptJson(apiBase)};
@@ -146,171 +175,13 @@ const EDITOR_TITLE = ${scriptJson(title || "sketchmark")};
146
175
  const MP4_MUXER_URL = ${scriptJson(mp4MuxerUrl)};
147
176
  const MP4_MUXER_SOURCE = ${scriptJson(mp4MuxerSource)};
148
177
  const SERVER_EXPORT_FALLBACK = ${scriptJson(serverExportFallback)};
149
- const CANVAS_STAGE_RENDER = ${scriptJson(canvasStageRender)};
150
178
  const LOCAL_DOCUMENT_CONTROLS = ${scriptJson(localDocumentControls)};
179
+ const TREE_WIDTH_KEY = "sketchmark.editor.treeWidth";
180
+ const DEFAULT_TREE_WIDTH = 240;
181
+ const MIN_TREE_WIDTH = 180;
182
+ const MAX_TREE_WIDTH = 620;
151
183
  let mp4MuxerObjectUrl = "";
152
-
153
- let canvasStageRenderToken = 0;
154
- let canvasStageRenderScheduled = false;
155
- let pendingCanvasStageCanvas = null;
156
- let liveDragPreviewPendingClear = false;
157
-
158
- function requestVisibleCanvasStageRender(canvas) {
159
- if (!CANVAS_STAGE_RENDER || !canvas) return;
160
- pendingCanvasStageCanvas = canvas;
161
- syncCanvasStageLayout(canvas);
162
- if (canvasStageRenderScheduled) return;
163
- canvasStageRenderScheduled = true;
164
- requestAnimationFrame(() => {
165
- canvasStageRenderScheduled = false;
166
- renderVisibleCanvasStage(pendingCanvasStageCanvas);
167
- });
168
- }
169
-
170
- function setStageOverlaySvg(svgMarkup) {
171
- const template = document.createElement("template");
172
- template.innerHTML = String(svgMarkup || "").trim();
173
- const nextSvg = template.content.querySelector("svg");
174
- if (!nextSvg) {
175
- stage.innerHTML = String(svgMarkup || "");
176
- return;
177
- }
178
- const previousSvg = stage.querySelector("svg:not(#stageLiveSvg)");
179
- if (previousSvg) {
180
- previousSvg.replaceWith(nextSvg);
181
- return;
182
- }
183
- const stageCanvas = document.getElementById("stageCanvas");
184
- if (stageCanvas && stageCanvas.parentElement === stage) stageCanvas.after(nextSvg);
185
- else stage.appendChild(nextSvg);
186
- }
187
-
188
- function syncCanvasStageLayout(canvas) {
189
- const svg = currentSvg();
190
- if (!CANVAS_STAGE_RENDER || !svg || !canvas) return null;
191
- const size = canvasSize(canvas);
192
- let stageCanvas = document.getElementById("stageCanvas");
193
- if (!stageCanvas || stageCanvas.parentElement !== stage) {
194
- stageCanvas = document.createElement("canvas");
195
- stageCanvas.id = "stageCanvas";
196
- stage.insertBefore(stageCanvas, svg);
197
- } else if (stageCanvas.nextSibling !== svg) {
198
- stage.insertBefore(stageCanvas, svg);
199
- }
200
- stage.classList.add("sketchmarkCanvasStage");
201
- const display = canvasStageDisplaySize(size);
202
- stage.style.width = display.width + "px";
203
- stage.style.height = display.height + "px";
204
- stageCanvas.style.width = display.width + "px";
205
- stageCanvas.style.height = display.height + "px";
206
- svg.style.width = display.width + "px";
207
- svg.style.height = display.height + "px";
208
- svg.style.maxWidth = "none";
209
- svg.style.maxHeight = "none";
210
- const liveSvg = document.getElementById("stageLiveSvg");
211
- if (liveSvg && liveSvg.parentElement === stage) {
212
- liveSvg.style.width = display.width + "px";
213
- liveSvg.style.height = display.height + "px";
214
- liveSvg.style.maxWidth = "none";
215
- liveSvg.style.maxHeight = "none";
216
- }
217
- const pixelRatio = Math.max(1, Math.min(3, Number(window.devicePixelRatio || 1)));
218
- const pixelWidth = Math.max(1, Math.round(display.width * pixelRatio));
219
- const pixelHeight = Math.max(1, Math.round(display.height * pixelRatio));
220
- return { svg, stageCanvas, display, pixelRatio, pixelWidth, pixelHeight };
221
- }
222
-
223
- function renderVisibleCanvasStage(canvas) {
224
- const layout = syncCanvasStageLayout(canvas);
225
- if (!layout) return;
226
- const { svg, stageCanvas, display, pixelRatio, pixelWidth, pixelHeight } = layout;
227
- const context = stageCanvas.getContext("2d");
228
- if (!context) return;
229
- const serialized = serializeStageSvgForCanvas(svg);
230
- const token = ++canvasStageRenderToken;
231
- const image = new Image();
232
- const url = URL.createObjectURL(new Blob([serialized], { type: "image/svg+xml;charset=utf-8" }));
233
- image.onload = () => {
234
- URL.revokeObjectURL(url);
235
- if (token !== canvasStageRenderToken || !stage.contains(stageCanvas)) return;
236
- if (stageCanvas.width !== pixelWidth) stageCanvas.width = pixelWidth;
237
- if (stageCanvas.height !== pixelHeight) stageCanvas.height = pixelHeight;
238
- context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
239
- context.clearRect(0, 0, display.width, display.height);
240
- context.drawImage(image, 0, 0, display.width, display.height);
241
- if (liveDragPreviewPendingClear && !drag) clearLiveDragPreview();
242
- };
243
- image.onerror = () => {
244
- URL.revokeObjectURL(url);
245
- };
246
- image.src = url;
247
- }
248
-
249
- function canvasStageDisplaySize(size) {
250
- const availableWidth = Math.max(1, Number(stageWrap && stageWrap.clientWidth || size.width) - 2);
251
- const availableHeight = Math.max(1, Number(stageWrap && stageWrap.clientHeight || size.height) - 2);
252
- const scale = Math.min(1, availableWidth / size.width, availableHeight / size.height);
253
- return {
254
- width: Math.max(1, Math.round(size.width * scale)),
255
- height: Math.max(1, Math.round(size.height * scale))
256
- };
257
- }
258
-
259
- function serializeStageSvgForCanvas(svg) {
260
- const clone = svg.cloneNode(true);
261
- const handles = clone.querySelector("#__sketchmark_handles");
262
- if (handles) handles.remove();
263
- const preview = clone.querySelector("#__sketchmark_drag_preview");
264
- if (preview) preview.remove();
265
- if (drag && drag.id) {
266
- const dragged = clone.querySelector("#" + cssId(drag.id));
267
- if (dragged) dragged.remove();
268
- }
269
- clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
270
- return new XMLSerializer().serializeToString(clone);
271
- }
272
-
273
- function showLiveDragPreview(target) {
274
- if (!CANVAS_STAGE_RENDER || !target) return;
275
- const svg = target.ownerSVGElement;
276
- if (!svg) return;
277
- syncLiveDragSvg(svg);
278
- stage.classList.add("liveSvgDrag");
279
- liveDragPreviewPendingClear = false;
280
- }
281
-
282
- function syncLiveDragSvg(svg) {
283
- const clone = svg.cloneNode(true);
284
- const handles = clone.querySelector("#__sketchmark_handles");
285
- if (handles) handles.remove();
286
- const preview = clone.querySelector("#__sketchmark_drag_preview");
287
- if (preview) preview.remove();
288
- clone.setAttribute("id", "stageLiveSvg");
289
- clone.setAttribute("aria-hidden", "true");
290
- let liveSvg = document.getElementById("stageLiveSvg");
291
- if (liveSvg) liveSvg.replaceWith(clone);
292
- else svg.after(clone);
293
- clone.style.width = svg.style.width;
294
- clone.style.height = svg.style.height;
295
- clone.style.maxWidth = "none";
296
- clone.style.maxHeight = "none";
297
- }
298
-
299
- function releaseLiveDragPreview(waitForDraw) {
300
- if (!CANVAS_STAGE_RENDER || !stage.classList.contains("liveSvgDrag")) return;
301
- liveDragPreviewPendingClear = true;
302
- if (waitForDraw) requestDraw();
303
- else requestVisibleCanvasStageRender(doc && doc.canvas);
304
- }
305
-
306
- function clearLiveDragPreview() {
307
- const preview = stage.querySelector("#__sketchmark_drag_preview");
308
- if (preview) preview.remove();
309
- const liveSvg = document.getElementById("stageLiveSvg");
310
- if (liveSvg) liveSvg.remove();
311
- stage.classList.remove("liveSvgDrag");
312
- liveDragPreviewPendingClear = false;
313
- }
184
+ let sidebarResize = null;
314
185
 
315
186
  function browserStoragePanel() {
316
187
  if (!LOCAL_DOCUMENT_CONTROLS) return "";
@@ -377,9 +248,66 @@ function showBrowserStorageStatus() {
377
248
  if (box && api && api.storageStatus) box.textContent = api.storageStatus();
378
249
  }
379
250
 
251
+ function initTreeResize() {
252
+ setTreeWidth(loadTreeWidth(), false);
253
+ if (!treeResizeHandle) return;
254
+ treeResizeHandle.addEventListener("pointerdown", beginTreeResize);
255
+ treeResizeHandle.addEventListener("dblclick", () => setTreeWidth(DEFAULT_TREE_WIDTH, true));
256
+ }
257
+
258
+ function beginTreeResize(event) {
259
+ sidebarResize = {
260
+ pointerId: event.pointerId,
261
+ startX: event.clientX,
262
+ startWidth: tree.getBoundingClientRect().width
263
+ };
264
+ document.body.classList.add("resizingSidebar");
265
+ treeResizeHandle.setPointerCapture?.(event.pointerId);
266
+ event.preventDefault();
267
+ }
268
+
269
+ function updateTreeResize(event) {
270
+ if (!sidebarResize) return;
271
+ setTreeWidth(sidebarResize.startWidth + event.clientX - sidebarResize.startX, false);
272
+ }
273
+
274
+ function endTreeResize() {
275
+ if (!sidebarResize) return;
276
+ sidebarResize = null;
277
+ document.body.classList.remove("resizingSidebar");
278
+ setTreeWidth(tree.getBoundingClientRect().width, true);
279
+ }
280
+
281
+ function loadTreeWidth() {
282
+ try {
283
+ const stored = Number(window.localStorage.getItem(TREE_WIDTH_KEY));
284
+ return Number.isFinite(stored) && stored > 0 ? stored : DEFAULT_TREE_WIDTH;
285
+ } catch {
286
+ return DEFAULT_TREE_WIDTH;
287
+ }
288
+ }
289
+
290
+ function setTreeWidth(width, persist) {
291
+ const next = clampTreeWidth(width);
292
+ document.body.style.setProperty("--tree-width", next + "px");
293
+ if (persist) {
294
+ try {
295
+ window.localStorage.setItem(TREE_WIDTH_KEY, String(Math.round(next)));
296
+ } catch {}
297
+ }
298
+ resizeElementsTree();
299
+ }
300
+
301
+ function clampTreeWidth(width) {
302
+ const value = Number(width);
303
+ const availableMax = Math.max(MIN_TREE_WIDTH, window.innerWidth - 520);
304
+ const max = Math.min(MAX_TREE_WIDTH, availableMax);
305
+ return Math.max(MIN_TREE_WIDTH, Math.min(max, Number.isFinite(value) ? value : DEFAULT_TREE_WIDTH));
306
+ }
307
+
380
308
  function apiPath(path) {
381
- return API_BASE + path;
382
- }
309
+ return API_BASE + path;
310
+ }
383
311
 
384
312
  curveModalClose.onclick = closeCurveModal;
385
313
  curveModalBackdrop.onclick = (event) => {
@@ -410,18 +338,17 @@ async function load() {
410
338
  requestDraw();
411
339
  }
412
340
 
413
- async function draw() {
414
- if (drawInFlight) {
415
- drawQueued = true;
416
- return;
417
- }
418
- drawInFlight = true;
419
- const time = currentTime;
420
- try {
421
- const data = await api(apiPath("/frame") + "?time=" + encodeURIComponent(time));
422
- resolvedDoc = data.resolved || null;
423
- if (CANVAS_STAGE_RENDER) setStageOverlaySvg(data.svg);
424
- else stage.innerHTML = data.svg;
341
+ async function draw() {
342
+ if (drawInFlight) {
343
+ drawQueued = true;
344
+ return;
345
+ }
346
+ drawInFlight = true;
347
+ const time = currentTime;
348
+ try {
349
+ const data = await api(apiPath("/frame") + "?time=" + encodeURIComponent(time));
350
+ resolvedDoc = data.resolved || null;
351
+ stage.innerHTML = data.svg;
425
352
  const svg = currentSvg();
426
353
  if (svg) {
427
354
  svg.style.overflow = "visible";
@@ -446,18 +373,18 @@ async function draw() {
446
373
  }
447
374
  }
448
375
 
449
- function requestDraw() {
450
- if (drawInFlight) {
451
- drawQueued = true;
452
- return;
376
+ function requestDraw() {
377
+ if (drawInFlight) {
378
+ drawQueued = true;
379
+ return;
453
380
  }
454
381
  if (drawScheduled) return;
455
382
  drawScheduled = true;
456
- requestAnimationFrame(() => {
457
- drawScheduled = false;
458
- draw().catch(showError);
459
- });
460
- }
383
+ requestAnimationFrame(() => {
384
+ drawScheduled = false;
385
+ draw().catch(showError);
386
+ });
387
+ }
461
388
 
462
389
  function canvasSize(canvas) {
463
390
  const width = Math.max(1, Number(canvas && canvas.width || 1));
@@ -482,13 +409,12 @@ function ensureViewportState(canvas, forceReset) {
482
409
  clampViewport();
483
410
  }
484
411
 
485
- function applyViewportToSvg(svg, canvas) {
486
- if (!svg) return;
487
- ensureViewportState(canvas, false);
412
+ function applyViewportToSvg(svg, canvas) {
413
+ if (!svg) return;
414
+ ensureViewportState(canvas, false);
488
415
  svg.setAttribute("viewBox", viewport.x.toFixed(3) + " " + viewport.y.toFixed(3) + " " + viewport.width.toFixed(3) + " " + viewport.height.toFixed(3));
489
416
  svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
490
417
  updateZoomLabel();
491
- requestVisibleCanvasStageRender(canvas);
492
418
  }
493
419
 
494
420
  function updateZoomLabel() {
@@ -529,7 +455,7 @@ function zoomFactorFromWheel(event) {
529
455
  }
530
456
 
531
457
  function currentSvg() {
532
- return stage.querySelector("svg:not(#stageLiveSvg)") || stage.querySelector("svg");
458
+ return stage.querySelector("svg");
533
459
  }
534
460
 
535
461
  function svgPointFromClient(svg, clientX, clientY) {
@@ -662,21 +588,118 @@ function panelDetails(panelId, title, body, options) {
662
588
  "</details>";
663
589
  }
664
590
 
665
- function bindPanelStates(scope) {
666
- const root = scope || document;
667
- const panels = root.querySelectorAll("details[data-panel]");
668
- for (const panel of panels) {
669
- const panelId = panel.getAttribute("data-panel");
670
- if (!panelId) continue;
671
- panelOpenState[panelId] = panel.open;
672
- panel.ontoggle = () => {
673
- panelOpenState[panelId] = panel.open;
674
- };
675
- }
676
- }
677
-
678
- function renderTree() {
679
- const canvas = doc && doc.canvas ? doc.canvas : {};
591
+ function bindPanelStates(scope) {
592
+ const root = scope || document;
593
+ const panels = root.querySelectorAll("details[data-panel]");
594
+ for (const panel of panels) {
595
+ const panelId = panel.getAttribute("data-panel");
596
+ if (!panelId) continue;
597
+ panelOpenState[panelId] = panel.open;
598
+ panel.ontoggle = () => {
599
+ panelOpenState[panelId] = panel.open;
600
+ resizeElementsTree();
601
+ };
602
+ }
603
+ }
604
+
605
+ function elementInsertPanel() {
606
+ return "<div class='insertBar'><select id='insertPreset'>" + ELEMENT_PRESET_OPTIONS.map((item) =>
607
+ "<option value='" + escapeAttr(item.value) + "'" + (item.value === insertPresetKind ? " selected" : "") + ">" + escapeText(item.label) + "</option>"
608
+ ).join("") + "</select><button id='insertTopElement' type='button' title='Add element'>+</button></div>" + selectedLayerPanel();
609
+ }
610
+
611
+ function bindElementInsertControls() {
612
+ const preset = document.getElementById("insertPreset");
613
+ if (preset) {
614
+ preset.onchange = () => {
615
+ insertPresetKind = String(preset.value || "rectangle");
616
+ };
617
+ }
618
+ const add = document.getElementById("insertTopElement");
619
+ if (add) add.onclick = () => insertElement("");
620
+ bindLayerButton("layerBack", "back");
621
+ bindLayerButton("layerBackward", "backward");
622
+ bindLayerButton("layerForward", "forward");
623
+ bindLayerButton("layerFront", "front");
624
+ const remove = document.getElementById("layerDelete");
625
+ if (remove) remove.onclick = () => {
626
+ if (selectedId) deleteTreeElement(selectedId);
627
+ };
628
+ }
629
+
630
+ async function insertElement(parentId) {
631
+ try {
632
+ const data = await api(apiPath("/element"), {
633
+ method: "POST",
634
+ headers: { "content-type": "application/json" },
635
+ body: JSON.stringify({ preset: insertPresetKind, parentId: parentId || null })
636
+ });
637
+ doc = data.document;
638
+ refs = data.elements;
639
+ rebuildElementIndex();
640
+ if (parentId) collapsedGroups.delete(parentId);
641
+ selectedId = typeof data.insertedId === "string" ? data.insertedId : selectedId;
642
+ renderTree();
643
+ renderInspector();
644
+ renderTimeline();
645
+ requestDraw();
646
+ } catch (error) {
647
+ showError(error);
648
+ }
649
+ }
650
+
651
+ function selectedLayerPanel() {
652
+ const id = selectedId && findElement(selectedId) ? selectedId : "";
653
+ const index = id ? Number(layerIndexById[id]) : -1;
654
+ const count = id ? Number(layerCountById[id]) : 0;
655
+ const locked = id ? isElementLocked(id) : true;
656
+ const canMove = Boolean(id) && !locked && Number.isFinite(index) && Number.isFinite(count) && count > 1;
657
+ const label = id ? "Layer: " + id : "Layer";
658
+ return "<div class='layerBar'>" +
659
+ "<div class='layerMeta' title='" + escapeAttr(label) + "'>" + escapeText(label) + "</div>" +
660
+ "<div class='layerActions'>" +
661
+ layerActionButton("layerBack", "<<", "Send to back", !canMove || index <= 0) +
662
+ layerActionButton("layerBackward", "<", "Send backward", !canMove || index <= 0) +
663
+ layerActionButton("layerForward", ">", "Bring forward", !canMove || index >= count - 1) +
664
+ layerActionButton("layerFront", ">>", "Bring to front", !canMove || index >= count - 1) +
665
+ layerActionButton("layerDelete", "X", "Delete element/group", !id || locked, " treeDanger") +
666
+ "</div></div>";
667
+ }
668
+
669
+ function layerActionButton(id, label, title, disabled, className) {
670
+ return "<button id='" + escapeAttr(id) + "' class='treeCtl" + (className || "") + "' type='button' title='" + escapeAttr(title) + "'" + (disabled ? " disabled" : "") + ">" + escapeText(label) + "</button>";
671
+ }
672
+
673
+ function bindLayerButton(id, direction) {
674
+ const button = document.getElementById(id);
675
+ if (button) button.onclick = () => {
676
+ if (selectedId) reorderLayer(selectedId, direction);
677
+ };
678
+ }
679
+
680
+ async function reorderLayer(id, direction) {
681
+ try {
682
+ if (!ensureElementEditable(id)) return;
683
+ await mutate(apiPath("/reorder"), { id, direction }, { refreshTimeline: false });
684
+ } catch (error) {
685
+ showError(error);
686
+ }
687
+ }
688
+
689
+ async function deleteTreeElement(id) {
690
+ try {
691
+ if (!ensureElementEditable(id)) return;
692
+ const isGroup = typeById[id] === "group";
693
+ const message = "Delete '" + id + "'" + (isGroup ? " and its children" : "") + "?";
694
+ if (!window.confirm(message)) return;
695
+ await mutate(apiPath("/delete-element"), { id });
696
+ } catch (error) {
697
+ showError(error);
698
+ }
699
+ }
700
+
701
+ function renderTree() {
702
+ const canvas = doc && doc.canvas ? doc.canvas : {};
680
703
  const canvasSummary = Math.round(valueOr(canvas.width, 1)) + "x" + Math.round(valueOr(canvas.height, 1));
681
704
  const canvasBody =
682
705
  "<div class='row'><label>Width<input id='canvasWidth' type='number' step='1' min='1' value='" + escapeAttr(valueOr(canvas.width, 1)) + "'></label><label>Height<input id='canvasHeight' type='number' step='1' min='1' value='" + escapeAttr(valueOr(canvas.height, 1)) + "'></label></div>" +
@@ -686,27 +709,35 @@ function renderTree() {
686
709
  tree.innerHTML =
687
710
  browserStoragePanel() +
688
711
  panelDetails("tree-canvas", "Canvas", canvasBody, { defaultOpen: false, meta: canvasSummary }) +
689
- panelDetails("tree-elements", "Elements", "<div id='elementsTree'></div>", { defaultOpen: false, meta: refs.length + " items" });
712
+ panelDetails("tree-elements", "Elements", "<div class='elementsFixed'>" + elementInsertPanel() + "</div><div id='elementsTree' class='elementsTreeList'></div>", { defaultOpen: false, meta: refs.length + " items" });
690
713
  bindPanelStates(tree);
691
714
  bindCanvasInputs();
692
715
  bindBrowserStoragePanel();
693
- const treeRoot = document.getElementById("elementsTree");
694
- if (!treeRoot) return;
695
- for (const ref of refs) {
696
- if (isInCollapsedBranch(ref.id)) continue;
697
- const row = document.createElement("div");
716
+ bindElementInsertControls();
717
+ const treeRoot = document.getElementById("elementsTree");
718
+ if (!treeRoot) return;
719
+ if (!refs.length) {
720
+ const empty = document.createElement("div");
721
+ empty.className = "treeEmpty";
722
+ empty.textContent = "No elements yet.";
723
+ treeRoot.appendChild(empty);
724
+ }
725
+ for (const ref of refs) {
726
+ if (isInCollapsedBranch(ref.id)) continue;
727
+ const row = document.createElement("div");
698
728
  row.className = "treeRow";
699
729
  row.style.paddingLeft = 8 + ref.depth * 14 + "px";
700
- const hasChildren = hasTreeChildren(ref.id) && typeById[ref.id] === "group";
701
- if (hasChildren) {
702
- const fold = document.createElement("button");
703
- fold.className = "treeCtl";
704
- fold.textContent = collapsedGroups.has(ref.id) ? "+" : "-";
705
- fold.title = collapsedGroups.has(ref.id) ? "Expand group" : "Collapse group";
706
- fold.onclick = (event) => {
707
- event.stopPropagation();
708
- toggleCollapse(ref.id);
709
- };
730
+ const hasChildren = hasTreeChildren(ref.id) && typeById[ref.id] === "group";
731
+ if (hasChildren) {
732
+ const isCollapsed = collapsedGroups.has(ref.id);
733
+ const fold = document.createElement("button");
734
+ fold.className = "treeCtl treeFold" + (isCollapsed ? " collapsed" : " expanded");
735
+ fold.title = isCollapsed ? "Expand group" : "Collapse group";
736
+ fold.setAttribute("aria-label", fold.title);
737
+ fold.onclick = (event) => {
738
+ event.stopPropagation();
739
+ toggleCollapse(ref.id);
740
+ };
710
741
  row.appendChild(fold);
711
742
  } else {
712
743
  const pad = document.createElement("span");
@@ -729,16 +760,48 @@ function renderTree() {
729
760
  lock.onclick = (event) => {
730
761
  event.stopPropagation();
731
762
  toggleLocked(ref.id);
732
- };
733
- row.appendChild(lock);
734
- const button = document.createElement("button");
735
- button.className = "treeBtn" + (ref.id === selectedId ? " selected" : "") + (isElementHidden(ref.id) ? " dim" : "");
763
+ };
764
+ row.appendChild(lock);
765
+ if (ref.type === "group") {
766
+ const add = document.createElement("button");
767
+ add.className = "treeCtl";
768
+ add.textContent = "+";
769
+ add.title = "Add element to group";
770
+ add.onclick = (event) => {
771
+ event.stopPropagation();
772
+ insertElement(ref.id);
773
+ };
774
+ row.appendChild(add);
775
+ } else {
776
+ const pad = document.createElement("span");
777
+ pad.className = "treePad";
778
+ row.appendChild(pad);
779
+ }
780
+ const button = document.createElement("button");
781
+ button.className = "treeBtn" + (ref.id === selectedId ? " selected" : "") + (isElementHidden(ref.id) ? " dim" : "");
736
782
  button.textContent = ref.id + " " + ref.type;
737
783
  button.onclick = () => select(ref.id);
738
- row.appendChild(button);
739
- treeRoot.appendChild(row);
740
- }
741
- }
784
+ row.appendChild(button);
785
+ treeRoot.appendChild(row);
786
+ }
787
+ resizeElementsTree();
788
+ }
789
+
790
+ function resizeElementsTree() {
791
+ const treeRoot = document.getElementById("elementsTree");
792
+ const panel = document.querySelector("[data-panel='tree-elements']");
793
+ if (!treeRoot || !panel || !panel.open) return;
794
+ const body = panel.querySelector(".panelBody");
795
+ const fixed = panel.querySelector(".elementsFixed");
796
+ if (!body || !fixed) return;
797
+ const treeRect = tree.getBoundingClientRect();
798
+ const bodyRect = body.getBoundingClientRect();
799
+ const fixedRect = fixed.getBoundingClientRect();
800
+ const styles = window.getComputedStyle(body);
801
+ const paddingBottom = Number.parseFloat(styles.paddingBottom || "0") || 0;
802
+ const available = treeRect.bottom - bodyRect.top - fixedRect.height - paddingBottom - 6;
803
+ treeRoot.style.maxHeight = Math.max(72, Math.floor(available)) + "px";
804
+ }
742
805
 
743
806
  function bindCanvasInputs() {
744
807
  const bind = (id, callback) => {
@@ -802,22 +865,28 @@ function showCanvasError(message) {
802
865
  if (box) box.textContent = message || "";
803
866
  }
804
867
 
805
- function rebuildElementIndex() {
806
- parentById = Object.create(null);
807
- childIdsById = Object.create(null);
808
- typeById = Object.create(null);
809
- const visit = (elements, parentId) => {
810
- for (const element of elements || []) {
811
- const id = element && element.id;
812
- const nextParent = id || parentId;
813
- if (id) {
814
- typeById[id] = element.type;
815
- parentById[id] = parentId || "";
816
- childIdsById[id] = childIdsById[id] || [];
817
- if (parentId) {
818
- childIdsById[parentId] = childIdsById[parentId] || [];
819
- childIdsById[parentId].push(id);
820
- }
868
+ function rebuildElementIndex() {
869
+ parentById = Object.create(null);
870
+ childIdsById = Object.create(null);
871
+ layerIndexById = Object.create(null);
872
+ layerCountById = Object.create(null);
873
+ typeById = Object.create(null);
874
+ const visit = (elements, parentId) => {
875
+ const list = elements || [];
876
+ for (let index = 0; index < list.length; index += 1) {
877
+ const element = list[index];
878
+ const id = element && element.id;
879
+ const nextParent = id || parentId;
880
+ if (id) {
881
+ typeById[id] = element.type;
882
+ parentById[id] = parentId || "";
883
+ layerIndexById[id] = index;
884
+ layerCountById[id] = list.length;
885
+ childIdsById[id] = childIdsById[id] || [];
886
+ if (parentId) {
887
+ childIdsById[parentId] = childIdsById[parentId] || [];
888
+ childIdsById[parentId].push(id);
889
+ }
821
890
  }
822
891
  if (element && element.type === "group") visit(element.children || [], nextParent);
823
892
  }
@@ -1324,12 +1393,13 @@ function renderTimeline() {
1324
1393
  const element = findElement(selectedId);
1325
1394
  const tracks = element && element.timeline && element.timeline.tracks ? element.timeline.tracks : {};
1326
1395
  timeline.innerHTML = "<div class='toolbar'><button id='play'>" + (playing ? "Pause" : "Play") + "</button><input id='scrub' type='range' min='0' max='" + Math.max(Number(doc.canvas.duration || 0), 0.01) + "' step='0.005' value='" + currentTime + "'><strong id='timeLabel'>" + currentTime.toFixed(2) + "s</strong><button id='refresh'>Refresh</button></div>";
1327
- document.getElementById("play").onclick = togglePlay;
1328
- document.getElementById("refresh").onclick = load;
1329
- document.getElementById("scrub").oninput = (event) => {
1330
- setCurrentTime(event.target.value);
1331
- };
1332
- const properties = Object.keys(tracks);
1396
+ document.getElementById("play").onclick = togglePlay;
1397
+ document.getElementById("refresh").onclick = load;
1398
+ const scrub = document.getElementById("scrub");
1399
+ scrub.oninput = (event) => {
1400
+ setCurrentTime(event.target.value);
1401
+ };
1402
+ const properties = Object.keys(tracks);
1333
1403
  if (!properties.length) {
1334
1404
  selectedSegment = null;
1335
1405
  closeCurveModal();
@@ -1974,18 +2044,18 @@ async function applySegmentCurve(property, segmentIndex, curve) {
1974
2044
  );
1975
2045
  }
1976
2046
 
1977
- function setCurrentTime(time) {
1978
- const duration = Math.max(Number(doc && doc.canvas && doc.canvas.duration || 0), 0.01);
1979
- const next = Math.max(0, Math.min(Number(time || 0), duration));
1980
- currentTime = next;
2047
+ function setCurrentTime(time) {
2048
+ const duration = Math.max(Number(doc && doc.canvas && doc.canvas.duration || 0), 0.01);
2049
+ const next = Math.max(0, Math.min(Number(time || 0), duration));
2050
+ currentTime = next;
1981
2051
  const scrub = document.getElementById("scrub");
1982
2052
  if (scrub) scrub.value = String(next);
1983
2053
  const label = document.getElementById("timeLabel");
1984
2054
  if (label) label.textContent = next.toFixed(2) + "s";
1985
- const kfTime = document.getElementById("kfTime");
1986
- if (kfTime) kfTime.value = next.toFixed(2);
1987
- requestDraw();
1988
- }
2055
+ const kfTime = document.getElementById("kfTime");
2056
+ if (kfTime) kfTime.value = next.toFixed(2);
2057
+ requestDraw();
2058
+ }
1989
2059
 
1990
2060
  function clearSidebarCommitTimers() {
1991
2061
  for (const key of Object.keys(sidebarCommitTimers)) {
@@ -2236,12 +2306,20 @@ document.addEventListener("keyup", (event) => {
2236
2306
  if (event.code === "Space") spacePanActive = false;
2237
2307
  });
2238
2308
 
2239
- window.addEventListener("blur", () => {
2240
- spacePanActive = false;
2241
- endViewportPan();
2242
- });
2243
-
2244
- stage.addEventListener("pointerdown", (event) => {
2309
+ window.addEventListener("blur", () => {
2310
+ spacePanActive = false;
2311
+ endViewportPan();
2312
+ });
2313
+
2314
+ window.addEventListener("resize", () => {
2315
+ setTreeWidth(tree.getBoundingClientRect().width || loadTreeWidth(), false);
2316
+ resizeElementsTree();
2317
+ });
2318
+ window.addEventListener("pointermove", updateTreeResize);
2319
+ window.addEventListener("pointerup", endTreeResize);
2320
+ window.addEventListener("pointercancel", endTreeResize);
2321
+
2322
+ stage.addEventListener("pointerdown", (event) => {
2245
2323
  const handle = event.target.closest("[data-handle]");
2246
2324
  if (handle && selectedId) {
2247
2325
  if (isElementLocked(selectedId) || isElementHidden(selectedId)) return;
@@ -2317,7 +2395,6 @@ async function finishDrag() {
2317
2395
  if (snapshot.transform) snapshot.target.setAttribute("transform", snapshot.transform);
2318
2396
  else snapshot.target.removeAttribute("transform");
2319
2397
  }
2320
- releaseLiveDragPreview(committed);
2321
2398
  }
2322
2399
  }
2323
2400
 
@@ -2343,8 +2420,6 @@ function startDrag(event, target, mode) {
2343
2420
  event.preventDefault();
2344
2421
  event.stopPropagation();
2345
2422
  stage.setPointerCapture?.(event.pointerId);
2346
- showLiveDragPreview(target);
2347
- requestVisibleCanvasStageRender(doc && doc.canvas);
2348
2423
  }
2349
2424
 
2350
2425
  async function commitDrag(snapshot) {
@@ -2444,7 +2519,6 @@ function parentPoint(event, target) {
2444
2519
  function previewDraggedTransform(prefix) {
2445
2520
  if (!drag || !drag.target) return;
2446
2521
  drag.target.setAttribute("transform", prefix + (drag.transform ? " " + drag.transform : ""));
2447
- showLiveDragPreview(drag.target);
2448
2522
  }
2449
2523
  function selectedTarget() {
2450
2524
  if (!selectedId || isElementHidden(selectedId) || isElementLocked(selectedId)) return null;
@@ -2745,10 +2819,11 @@ function cap(value) { value = String(value); return value.charAt(0).toUpperCase(
2745
2819
  function cssId(id) { return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\\\\]^\\\`{|}~])/g, "\\\\$1"); }
2746
2820
  function escapeText(value) { return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
2747
2821
  function escapeAttr(value) { return escapeText(value).replace(/"/g, "&quot;").replace(/'/g, "&#39;"); }
2748
- function showError(error) { const box = document.getElementById("error"); if (box) box.textContent = error.message || String(error); }
2749
- load().catch(showError);
2750
- </script></body></html>`;
2751
- }
2822
+ function showError(error) { const box = document.getElementById("error"); if (box) box.textContent = error.message || String(error); }
2823
+ initTreeResize();
2824
+ load().catch(showError);
2825
+ </script></body></html>`;
2826
+ }
2752
2827
 
2753
2828
 
2754
2829
  function escapeHtml(value) {