@jxsuite/studio 0.15.0 → 0.16.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
32
32
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
33
- "@jxsuite/runtime": "^0.12.0",
33
+ "@jxsuite/runtime": "^0.15.0",
34
34
  "@spectrum-web-components/accordion": "^1.12.1",
35
35
  "@spectrum-web-components/action-bar": "1.12.1",
36
36
  "@spectrum-web-components/action-button": "^1.12.1",
@@ -390,38 +390,12 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
390
390
  canvasEl.removeAttribute("data-content-mode");
391
391
  }
392
392
 
393
- // Resolve relative asset URLs through the PAL for environments that can't
394
- // serve project files natively (e.g. ElectroBun's views:// scheme).
395
- const platform = /** @type {any} */ (globalThis).__jxPlatform;
396
- if (platform?.resolveAssetUrl && docBase) {
397
- const mediaEls = el.querySelectorAll("img[src], video[src], source[src], video[poster]");
398
- for (const node of mediaEls) {
399
- for (const attr of ["src", "poster"]) {
400
- const val = node.getAttribute(attr);
401
- if (
402
- val &&
403
- !val.startsWith("data:") &&
404
- !val.startsWith("blob:") &&
405
- !val.startsWith("http")
406
- ) {
407
- try {
408
- const resolved = new URL(val, docBase).pathname.replace(/^\//, "");
409
- const dataUrl = await platform.resolveAssetUrl(resolved);
410
- if (dataUrl) node.setAttribute(attr, dataUrl);
411
- } catch {}
412
- }
413
- }
414
- }
415
- }
416
-
417
393
  canvasEl.appendChild(el);
394
+
418
395
  if (canvasMode === "design" || canvasMode === "edit") {
419
- // Custom element connectedCallbacks render children asynchronously —
420
- // sweep again after they've had a chance to run
421
396
  requestAnimationFrame(() => {
422
397
  const editingEl = getActiveElement();
423
398
  for (const child of canvasEl.querySelectorAll("*")) {
424
- // Preserve pointer-events on the actively-edited element
425
399
  if (view.componentInlineEdit && child === view.componentInlineEdit.el) continue;
426
400
  if (editingEl && child === editingEl) continue;
427
401
  /** @type {any} */ (child).style.pointerEvents = "none";
@@ -10,6 +10,7 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
10
10
  import { canvasWrap, canvasPanels, updateCanvas } from "../store.js";
11
11
  import { activeTab } from "../workspace/workspace.js";
12
12
  import { view } from "../view.js";
13
+ import { loadMarkdown } from "../files/file-ops.js";
13
14
  import {
14
15
  canvasPanelTemplate,
15
16
  applyTransform,
@@ -144,6 +145,7 @@ export function renderCanvas() {
144
145
  // Reset inline style overrides from other modes
145
146
  canvasWrap.style.padding = "";
146
147
  canvasWrap.style.alignItems = "";
148
+ canvasWrap.style.flexDirection = "";
147
149
  canvasWrap.style.display = "";
148
150
  canvasWrap.style.overflow = "";
149
151
  canvasWrap.style.overflow = "";
@@ -247,72 +249,79 @@ export function renderCanvas() {
247
249
  return;
248
250
  }
249
251
 
250
- // Git diff mode — render original (left) and current (right) side-by-side
252
+ // Git diff mode — render original (left) and current (right) side-by-side on panzoom surface
251
253
  if (canvasMode === "git-diff") {
252
254
  if (!_ctx.gitDiffState) {
253
- // Fallback to design mode if diff state is missing
254
255
  _ctx.setCanvasMode("design");
255
256
  renderCanvas();
256
257
  return;
257
258
  }
258
259
 
259
- canvasWrap.style.padding = "0";
260
- canvasWrap.style.overflow = "hidden";
260
+ if (modeChanged) {
261
+ canvasWrap.style.padding = "0";
262
+ canvasWrap.style.overflow = "hidden";
263
+ }
261
264
 
262
265
  const gitDiffState = _ctx.gitDiffState;
263
- const { tpl: origPanelTpl, panel: origPanel } = canvasPanelTemplate(
266
+ const panelWidth = 800;
267
+
268
+ const { tpl: origTpl, panel: origPanel } = canvasPanelTemplate(
264
269
  "git-diff-original",
265
270
  "Original",
266
271
  false,
267
- "50%",
272
+ panelWidth,
268
273
  );
269
- const { tpl: currPanelTpl, panel: currPanel } = canvasPanelTemplate(
274
+ const { tpl: currTpl, panel: currPanel } = canvasPanelTemplate(
270
275
  "git-diff-current",
271
276
  "Current",
272
277
  false,
273
- "50%",
278
+ panelWidth,
274
279
  );
275
280
 
276
- const diffTpl = html`
277
- <div style="display: flex; height: 100%; gap: 0;">
278
- <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
279
- ${origPanelTpl}
280
- </div>
281
- <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
282
- ${currPanelTpl}
281
+ litRender(
282
+ html`
283
+ <div
284
+ class="panzoom-wrap"
285
+ style="transform-origin:0 0"
286
+ ${ref((el) => {
287
+ if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
288
+ })}
289
+ >
290
+ ${origTpl} ${currTpl}
283
291
  </div>
284
- </div>
285
- `;
292
+ `,
293
+ canvasWrap,
294
+ );
286
295
 
287
- litRender(diffTpl, canvasWrap);
288
296
  canvasPanels.push(/** @type {any} */ (origPanel));
289
297
  canvasPanels.push(/** @type {any} */ (currPanel));
290
298
 
291
- // Parse original document from git content
292
- let originalDoc = null;
293
- try {
299
+ /** @param {string} content */
300
+ const parseContent = (content) => {
294
301
  if (gitDiffState.isMarkdown) {
295
- // For markdown, we need to convert it to Jx format
296
- // For now, use a simple placeholder until we have markdown parsing
297
- originalDoc = {
298
- tagName: "div",
299
- children: [{ tagName: "p", textContent: "Original (markdown)" }],
300
- };
301
- } else {
302
- // Parse as JSON
303
- originalDoc = JSON.parse(gitDiffState.originalContent);
302
+ return loadMarkdown(content).then((r) => r.document);
304
303
  }
305
- } catch {
306
- originalDoc = {
307
- tagName: "div",
308
- children: [{ tagName: "p", textContent: "Failed to parse original" }],
309
- };
310
- }
311
-
312
- const currDoc = S.document;
304
+ return Promise.resolve().then(() => {
305
+ try {
306
+ return JSON.parse(content);
307
+ } catch {
308
+ return { tagName: "div", children: [{ tagName: "p", textContent: "Failed to parse" }] };
309
+ }
310
+ });
311
+ };
312
+
313
+ const featureToggles = S.ui.featureToggles;
314
+ Promise.all([
315
+ parseContent(gitDiffState.originalContent || ""),
316
+ parseContent(gitDiffState.currentContent || ""),
317
+ ]).then(([originalDoc, currentDoc]) => {
318
+ renderCanvasIntoPanel(origPanel, new Set(), featureToggles, originalDoc, gitDiffState);
319
+ renderCanvasIntoPanel(currPanel, new Set(), featureToggles, currentDoc, gitDiffState);
320
+ });
313
321
 
314
- renderCanvasIntoPanel(origPanel, new Set(), S.ui.featureToggles, originalDoc, gitDiffState);
315
- renderCanvasIntoPanel(currPanel, new Set(), S.ui.featureToggles, currDoc, gitDiffState);
322
+ applyTransform();
323
+ if (modeChanged) observeCenterUntilStable();
324
+ renderZoomIndicator();
316
325
  return;
317
326
  }
318
327
 
@@ -498,6 +507,7 @@ function renderCanvasIntoPanel(
498
507
  } else {
499
508
  // Fallback to structural preview
500
509
  updateCanvas({ status: "ready", scope: null, error: null });
510
+ panel.canvas.innerHTML = "";
501
511
  renderCanvasNode(docToRender, [], panel.canvas, activeBreakpoints, featureToggles);
502
512
  }
503
513
  registerPanelDnD(panel);
@@ -174,9 +174,11 @@ export async function exportFile() {
174
174
  * @param {import("../tabs/tab.js").Tab} tab
175
175
  * @returns {string}
176
176
  */
177
- function serializeDocument(tab) {
177
+ export function serializeDocument(tab) {
178
178
  if (tab.doc.sourceFormat === "md") {
179
- return jxDocToMd(tab.doc.document);
179
+ const fm = tab.doc.content?.frontmatter || {};
180
+ const fullDoc = { ...fm, children: tab.doc.document.children ?? [] };
181
+ return jxDocToMd(fullDoc);
180
182
  }
181
183
  if (tab.doc.mode === "content") {
182
184
  const mdast = jxToMd(tab.doc.document);
@@ -15,7 +15,7 @@ import { createState, projectState, setProjectState } from "../store.js";
15
15
  import { getPlatform } from "../platform.js";
16
16
  import { statusMessage } from "../panels/statusbar.js";
17
17
  import { loadComponentRegistry } from "./components.js";
18
- import { workspace, openTab, activateTab } from "../workspace/workspace.js";
18
+ import { workspace, openTab, activateTab, replaceAllTabs } from "../workspace/workspace.js";
19
19
  import { loadMarkdown } from "./file-ops.js";
20
20
  import { view } from "../view.js";
21
21
 
@@ -92,6 +92,8 @@ export async function openProject({ renderActivityBar, renderLeftPanel }) {
92
92
 
93
93
  const { config, handle } = result;
94
94
 
95
+ replaceAllTabs({ id: "initial", document: { tagName: "div", children: [] } });
96
+
95
97
  setProjectState({
96
98
  ...projectState,
97
99
  projectRoot: handle.root,
@@ -383,83 +385,83 @@ export function setupTreeKeyboard(/** @type {HTMLElement} */ tree) {
383
385
  // ─── Context menu ─────────────────────────────────────────────────────────────
384
386
 
385
387
  /** @type {HTMLElement | null} */
386
- let fileContextPopover = null;
388
+ let _fileCtxHost = null;
389
+
390
+ function getFileCtxHost() {
391
+ if (!_fileCtxHost) {
392
+ _fileCtxHost = document.createElement("div");
393
+ _fileCtxHost.style.display = "contents";
394
+ (document.querySelector("sp-theme") || document.body).appendChild(_fileCtxHost);
395
+ document.addEventListener("click", dismissFileContextMenu);
396
+ }
397
+ return _fileCtxHost;
398
+ }
399
+
400
+ function dismissFileContextMenu() {
401
+ const host = _fileCtxHost;
402
+ if (!host) return;
403
+ const popover = host.querySelector("sp-popover");
404
+ if (popover) popover.removeAttribute("open");
405
+ }
387
406
 
388
407
  function showFileContextMenu(
389
408
  /** @type {MouseEvent} */ e,
390
409
  /** @type {{ name: string; path: string; type: string }} */ entry,
391
410
  /** @type {{ openFileFn: (path: string) => void; renderLeftPanel: () => void }} */ ctx,
392
411
  ) {
393
- if (fileContextPopover) {
394
- fileContextPopover.remove();
395
- fileContextPopover = null;
396
- }
397
-
412
+ e.preventDefault();
413
+ const host = getFileCtxHost();
398
414
  const isDir = entry.type === "directory";
399
- fileContextPopover = document.createElement("div");
400
-
401
- const tpl = html`
402
- <sp-popover
403
- placement="right-start"
404
- open
405
- style="position:fixed;left:${e.clientX}px;top:${e.clientY}px;z-index:9999"
406
- >
407
- <sp-menu style="min-width:160px">
408
- ${!isDir
409
- ? html`<sp-menu-item
410
- @click=${() => {
411
- closeFileContextMenu();
412
- ctx.openFileFn(entry.path);
413
- }}
414
- >Open</sp-menu-item
415
- >`
416
- : nothing}
417
- ${isDir
418
- ? html`<sp-menu-item
419
- @click=${() => {
420
- closeFileContextMenu();
421
- createNewFile(entry.path, ctx.renderLeftPanel);
422
- }}
423
- >New File…</sp-menu-item
424
- >`
425
- : nothing}
426
- <sp-menu-divider></sp-menu-divider>
427
- <sp-menu-item
428
- @click=${() => {
429
- closeFileContextMenu();
430
- renameFile(entry, ctx.renderLeftPanel);
431
- }}
432
- >Rename…</sp-menu-item
433
- >
434
- <sp-menu-item
435
- style="color:var(--danger)"
436
- @click=${() => {
437
- closeFileContextMenu();
438
- deleteFile(entry, ctx.renderLeftPanel);
439
- }}
440
- >Delete</sp-menu-item
441
- >
442
- </sp-menu>
443
- </sp-popover>
444
- `;
445
-
446
- litRender(tpl, fileContextPopover);
447
- document.body.appendChild(fileContextPopover);
448
415
 
449
- const closeHandler = (/** @type {MouseEvent} */ ev) => {
450
- if (!fileContextPopover?.contains(/** @type {Node} */ (ev.target))) {
451
- closeFileContextMenu();
452
- document.removeEventListener("mousedown", closeHandler, true);
453
- }
454
- };
455
- setTimeout(() => document.addEventListener("mousedown", closeHandler, true), 0);
456
- }
416
+ /** @type {{ label: string; action?: () => void; danger?: boolean }[]} */
417
+ const items = [];
457
418
 
458
- function closeFileContextMenu() {
459
- if (fileContextPopover) {
460
- fileContextPopover.remove();
461
- fileContextPopover = null;
419
+ if (!isDir) {
420
+ items.push({ label: "Open", action: () => ctx.openFileFn(entry.path) });
421
+ }
422
+ if (isDir) {
423
+ items.push({
424
+ label: "New File\u2026",
425
+ action: () => createNewFile(entry.path, ctx.renderLeftPanel),
426
+ });
462
427
  }
428
+ items.push({ label: "\u2014" });
429
+ items.push({ label: "Rename\u2026", action: () => renameFile(entry, ctx.renderLeftPanel) });
430
+ items.push({
431
+ label: "Delete",
432
+ action: () => deleteFile(entry, ctx.renderLeftPanel),
433
+ danger: true,
434
+ });
435
+
436
+ litRender(
437
+ html`<sp-popover style="position:fixed;z-index:10000">
438
+ <sp-menu>
439
+ ${items.map((item) =>
440
+ item.label === "\u2014"
441
+ ? html`<sp-menu-divider></sp-menu-divider>`
442
+ : html`<sp-menu-item
443
+ style=${item.danger ? "color: var(--danger)" : ""}
444
+ @click=${() => {
445
+ dismissFileContextMenu();
446
+ item.action?.();
447
+ }}
448
+ >${item.label}</sp-menu-item
449
+ >`,
450
+ )}
451
+ </sp-menu>
452
+ </sp-popover>`,
453
+ host,
454
+ );
455
+
456
+ const popover = /** @type {HTMLElement} */ (host.querySelector("sp-popover"));
457
+ popover.setAttribute("open", "");
458
+ const menuRect = popover.getBoundingClientRect();
459
+ let x = e.clientX,
460
+ y = e.clientY;
461
+ if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
462
+ if (y + menuRect.height > window.innerHeight) y = window.innerHeight - menuRect.height - 4;
463
+ popover.style.left = `${x}px`;
464
+ popover.style.top = `${y}px`;
463
465
  }
464
466
 
465
467
  // ─── File CRUD ────────────────────────────────────────────────────────────────
@@ -57,6 +57,8 @@ export function renderFunctionEditor() {
57
57
 
58
58
  litRender(nothing, canvasWrap);
59
59
  canvasWrap.style.padding = "0";
60
+ canvasWrap.style.flexDirection = "column";
61
+ canvasWrap.style.alignItems = "stretch";
60
62
 
61
63
  // Toolbar breadcrumb handles context display — re-render it
62
64
  renderOnly("toolbar");
@@ -65,12 +67,14 @@ export function renderFunctionEditor() {
65
67
  /** @type {HTMLDivElement | null} */
66
68
  let editorContainer = null;
67
69
  litRender(
68
- html`<div
69
- class="source-editor"
70
- ${ref((el) => {
71
- if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
72
- })}
73
- ></div>`,
70
+ html`<div class="source-wrap">
71
+ <div
72
+ class="source-editor"
73
+ ${ref((el) => {
74
+ if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
75
+ })}
76
+ ></div>
77
+ </div>`,
74
78
  canvasWrap,
75
79
  );
76
80
 
@@ -7,6 +7,7 @@ import { html, nothing } from "lit-html";
7
7
  import { live } from "lit-html/directives/live.js";
8
8
  import { getPlatform } from "../platform.js";
9
9
  import { updateUi, renderOnly } from "../store.js";
10
+ import { activeTab } from "../workspace/workspace.js";
10
11
  import { view } from "../view.js";
11
12
 
12
13
  async function refreshGitStatus() {
@@ -70,7 +71,8 @@ export function renderGitPanel(S, ctx) {
70
71
  const totalChanges = status?.files?.length || 0;
71
72
 
72
73
  const doCommit = async () => {
73
- const msg = S.ui.gitCommitMessage?.trim();
74
+ const tab = activeTab.value;
75
+ const msg = tab?.session.ui.gitCommitMessage?.trim();
74
76
  if (!msg) return;
75
77
  updateUi("gitCommitMessage", "");
76
78
  await gitAction("gitCommit", msg);
@@ -145,11 +147,7 @@ export function renderGitPanel(S, ctx) {
145
147
  }
146
148
  }}
147
149
  ></sp-textfield>
148
- <sp-action-button
149
- class="git-commit-btn"
150
- @click=${doCommit}
151
- ?disabled=${!S.ui.gitCommitMessage?.trim() || loading}
152
- >
150
+ <sp-action-button class="git-commit-btn" @click=${doCommit} ?disabled=${loading}>
153
151
  <sp-icon-checkmark slot="icon" size="xs"></sp-icon-checkmark>
154
152
  Commit
155
153
  </sp-action-button>
@@ -162,29 +160,33 @@ export function renderGitPanel(S, ctx) {
162
160
  const dir = parts.join("/");
163
161
 
164
162
  const onFileClick = async () => {
165
- // Only support diff for modified/added files
166
163
  if (file.status !== "M" && file.status !== "A") return;
167
164
  if (!file.path.endsWith(".md") && !file.path.endsWith(".json")) return;
168
165
 
169
166
  try {
170
167
  const plat = getPlatform();
171
168
  updateUi("gitLoading", true);
172
- const originalContent = await plat.gitShow({ path: file.path, ref: "HEAD" });
173
169
 
174
- // Determine if it's markdown or JSON
175
- const isMarkdown = file.path.endsWith(".md");
170
+ const [originalContent, currentContent] = await Promise.all([
171
+ file.status === "A"
172
+ ? Promise.resolve("")
173
+ : plat.gitShow({ path: file.path, ref: "HEAD" }),
174
+ plat.readFile(file.path),
175
+ ]);
176
176
 
177
- // For now, we'll need to pass the original content to the canvas
178
- // This requires state management which we'll add to studio.js
179
- updateUi("gitDiffState", {
177
+ const isMarkdown = file.path.endsWith(".md");
178
+ const diffState = {
180
179
  filePath: file.path,
181
180
  originalContent,
181
+ currentContent,
182
182
  isMarkdown,
183
183
  fileStatus: file.status,
184
- });
184
+ };
185
+
186
+ updateUi("gitDiffState", diffState);
185
187
 
186
- // Trigger diff mode via context
187
188
  if (ctx?.setCanvasMode) {
189
+ if (ctx.setGitDiffState) ctx.setGitDiffState(diffState);
188
190
  ctx.setCanvasMode("git-diff");
189
191
  }
190
192
  } catch (/** @type {any} */ e) {
@@ -40,6 +40,7 @@ import { selectStylebookTag, stylebookMeta } from "./stylebook-panel.js";
40
40
  * registerElementsDnD: () => void;
41
41
  * registerComponentsDnD: () => void;
42
42
  * setupTreeKeyboard: (tree: HTMLElement) => void;
43
+ * setGitDiffState: (state: any) => void;
43
44
  * }} LeftPanelCtx
44
45
  */
45
46
 
@@ -69,6 +70,9 @@ export function mount(ctx) {
69
70
  void tab.doc.mode;
70
71
  void tab.session.selection;
71
72
  void tab.session.ui.settingsTab;
73
+ void tab.session.ui.gitStatus;
74
+ void tab.session.ui.gitLoading;
75
+ void tab.session.ui.gitError;
72
76
  render();
73
77
  });
74
78
  });
@@ -81,12 +81,14 @@ export function registerPanelEvents(panel) {
81
81
  document.elementsFromPoint(e.clientX, e.clientY),
82
82
  );
83
83
 
84
+ if (!tab) return;
85
+
84
86
  for (const el of elements) {
85
87
  if (canvas.contains(el) && el !== canvas) {
86
88
  // Layout element clicked — show layout info instead of selecting in page doc
87
89
  if (layoutElements.has(el)) {
88
90
  view.layoutSelection = { el, layoutPath: activeLayoutPath };
89
- activeTab.value.session.selection = null;
91
+ tab.session.selection = null;
90
92
  renderOnly("rightPanel");
91
93
  return;
92
94
  }
@@ -94,35 +96,35 @@ export function registerPanelEvents(panel) {
94
96
 
95
97
  const originalPath = elToPath.get(el);
96
98
  if (originalPath) {
97
- let path = bubbleInlinePath(tab?.doc.document, originalPath);
99
+ let path = bubbleInlinePath(tab.doc.document, originalPath);
98
100
  const newMedia = mediaName === "base" ? null : (mediaName ?? null);
99
101
 
100
102
  const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
101
103
 
102
104
  if (
103
- pathsEqual(path, tab?.session.selection) &&
105
+ pathsEqual(path, tab.session.selection) &&
104
106
  isEditableBlock(resolvedEl) &&
105
- (canvasMode === "edit" || tab?.doc.mode === "content")
107
+ (canvasMode === "edit" || tab.doc.mode === "content")
106
108
  ) {
107
- activeTab.value.session.ui.activeMedia = newMedia;
109
+ tab.session.ui.activeMedia = newMedia;
108
110
  _ctx.enterInlineEdit(resolvedEl, path);
109
111
  return;
110
112
  }
111
113
 
112
- if (canvasMode === "design" && tab?.doc.mode !== "content") {
114
+ if (canvasMode === "design" && tab.doc.mode !== "content") {
113
115
  updateUi("pendingInlineEdit", { path, mediaName });
114
- activeTab.value.session.ui.activeMedia = newMedia;
115
- activeTab.value.session.selection = path;
116
+ tab.session.ui.activeMedia = newMedia;
117
+ tab.session.selection = path;
116
118
  return;
117
119
  }
118
120
 
119
- activeTab.value.session.ui.activeMedia = newMedia;
120
- activeTab.value.session.selection = path;
121
+ tab.session.ui.activeMedia = newMedia;
122
+ tab.session.selection = path;
121
123
  return;
122
124
  }
123
125
  }
124
126
  }
125
- activeTab.value.session.selection = null;
127
+ tab.session.selection = null;
126
128
  },
127
129
  opts,
128
130
  );
@@ -149,16 +151,18 @@ export function registerPanelEvents(panel) {
149
151
  document.elementsFromPoint(e.clientX, e.clientY),
150
152
  );
151
153
 
154
+ if (!tab) return;
155
+
152
156
  for (const el of elements) {
153
157
  if (canvas.contains(el) && el !== canvas) {
154
158
  const originalPath = elToPath.get(el);
155
159
  if (originalPath) {
156
- const path = bubbleInlinePath(tab?.doc.document, originalPath);
160
+ const path = bubbleInlinePath(tab.doc.document, originalPath);
157
161
  const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
158
162
  if (isEditableBlock(resolvedEl)) {
159
163
  const newMedia = mediaName === "base" ? null : (mediaName ?? null);
160
- activeTab.value.session.ui.activeMedia = newMedia;
161
- activeTab.value.session.selection = path;
164
+ tab.session.ui.activeMedia = newMedia;
165
+ tab.session.selection = path;
162
166
  _ctx.enterInlineEdit(resolvedEl, path);
163
167
  return;
164
168
  }
@@ -217,18 +221,19 @@ export function registerPanelEvents(panel) {
217
221
  return;
218
222
  }
219
223
  const tab = activeTab.value;
224
+ if (!tab) return;
220
225
  const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
221
226
  if (el && canvas.contains(el) && el !== canvas) {
222
227
  let path = elToPath.get(el);
223
228
  if (path) {
224
- path = bubbleInlinePath(tab?.doc.document, path);
225
- if (!pathsEqual(path, tab?.session.hover)) {
226
- activeTab.value.session.hover = path;
229
+ path = bubbleInlinePath(tab.doc.document, path);
230
+ if (!pathsEqual(path, tab.session.hover)) {
231
+ tab.session.hover = path;
227
232
  renderOnly("overlays");
228
233
  }
229
234
  }
230
- } else if (tab?.session.hover) {
231
- activeTab.value.session.hover = null;
235
+ } else if (tab.session.hover) {
236
+ tab.session.hover = null;
232
237
  renderOnly("overlays");
233
238
  }
234
239
  },
@@ -222,16 +222,16 @@ function renderFrontmatterOnlyPanel() {
222
222
  fields.push({ field, entry: fieldSchema, value: fm[field] });
223
223
  }
224
224
  for (const [field, value] of Object.entries(fm)) {
225
- if (!schemaProps[field]) {
226
- fields.push({
227
- field,
228
- entry: { type: typeof value === "boolean" ? "boolean" : "string" },
229
- value,
230
- });
231
- }
225
+ if (schemaProps[field] || field.startsWith("$")) continue;
226
+ fields.push({
227
+ field,
228
+ entry: { type: typeof value === "boolean" ? "boolean" : "string" },
229
+ value,
230
+ });
232
231
  }
233
232
  } else {
234
233
  for (const [field, value] of Object.entries(fm)) {
234
+ if (field.startsWith("$")) continue;
235
235
  fields.push({
236
236
  field,
237
237
  entry: { type: typeof value === "boolean" ? "boolean" : "string" },
@@ -1656,16 +1656,16 @@ export function renderPropertiesPanelTemplate(ctx) {
1656
1656
  fields.push({ field, entry: fieldSchema, value: fm[field] });
1657
1657
  }
1658
1658
  for (const [field, value] of Object.entries(fm)) {
1659
- if (!schemaProps[field]) {
1660
- fields.push({
1661
- field,
1662
- entry: { type: typeof value === "boolean" ? "boolean" : "string" },
1663
- value,
1664
- });
1665
- }
1659
+ if (schemaProps[field] || field.startsWith("$")) continue;
1660
+ fields.push({
1661
+ field,
1662
+ entry: { type: typeof value === "boolean" ? "boolean" : "string" },
1663
+ value,
1664
+ });
1666
1665
  }
1667
1666
  } else {
1668
1667
  for (const [field, value] of Object.entries(fm)) {
1668
+ if (field.startsWith("$")) continue;
1669
1669
  fields.push({
1670
1670
  field,
1671
1671
  entry: { type: typeof value === "boolean" ? "boolean" : "string" },