@jxsuite/studio 2.4.1 → 2.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.
Files changed (35) hide show
  1. package/dist/chunks/{content-source-cn9te96r.js → content-source-k9724vcv.js} +8 -8
  2. package/dist/chunks/{convert-to-repeater-65gjgdst.js → convert-to-repeater-k10dvcc7.js} +7 -7
  3. package/dist/chunks/{extension-sections-mdgmqrry.js → extension-sections-0ada0xcw.js} +11 -11
  4. package/dist/chunks/{files-dsh5g8j4.js → files-yew27sn9.js} +12 -12
  5. package/dist/chunks/{format-host-m9h45a79.js → format-host-wewba4nq.js} +2 -2
  6. package/dist/chunks/{git-panel-rfs8pqjj.js → git-panel-2dh1aw50.js} +7 -7
  7. package/dist/chunks/{layers-panel-6cj5nf6a.js → layers-panel-qnsghwwd.js} +10 -10
  8. package/dist/chunks/{publish-panel-b0xh7b4y.js → publish-panel-a0pdft5w.js} +5 -5
  9. package/dist/chunks/{quick-search-a83fhnsh.js → quick-search-9avyc4pc.js} +13 -13
  10. package/dist/chunks/{right-panel-hrw5kdh5.js → right-panel-kcnhtc5j.js} +9 -9
  11. package/dist/chunks/{studio-4m39t01b.js → studio-0b6fs05q.js} +5 -5
  12. package/dist/chunks/{studio-gqpbrrj5.js → studio-26sv44br.js} +8 -8
  13. package/dist/chunks/{studio-d1f2rqxh.js → studio-28xc8ws4.js} +4 -4
  14. package/dist/chunks/{studio-675v08qj.js → studio-aweyr4v1.js} +2 -2
  15. package/dist/chunks/{studio-0svvdm0a.js → studio-cz2j1c0g.js} +4 -4
  16. package/dist/chunks/{studio-qymkxbcq.js → studio-dgj7gq44.js} +3 -3
  17. package/dist/chunks/{studio-rkr6pm84.js → studio-ef8t53vc.js} +3 -3
  18. package/dist/chunks/{studio-1d6bm7eg.js → studio-f5yyvqxv.js} +7 -7
  19. package/dist/chunks/{studio-rz861p2b.js → studio-hb7c46jy.js} +5 -5
  20. package/dist/chunks/{studio-gfzyxa0m.js → studio-hyn3vhwr.js} +5 -5
  21. package/dist/chunks/{studio-9sxn0eaa.js → studio-jnakhg87.js} +4 -4
  22. package/dist/chunks/{studio-tdt7b6qb.js → studio-ke244pam.js} +6 -6
  23. package/dist/chunks/{studio-h6553a8h.js → studio-pyrw8h0w.js} +9 -7
  24. package/dist/chunks/{studio-kfq9ed73.js → studio-ra7ar50f.js} +3 -3
  25. package/dist/chunks/{studio-40fm3hyr.js → studio-rqemfhz6.js} +2 -2
  26. package/dist/chunks/{studio-91sdrn9x.js → studio-sccge2wf.js} +2 -2
  27. package/dist/chunks/{studio-n9k15qdg.js → studio-v1va0vhd.js} +8 -8
  28. package/dist/chunks/{studio-jand3kjg.js → studio-v2c5pmjj.js} +10 -10
  29. package/dist/chunks/{studio-qb6vy1z1.js → studio-ws6ef6js.js} +8 -8
  30. package/dist/iframe-entry.js +35 -3
  31. package/dist/studio.js +31 -31
  32. package/package.json +2 -2
  33. package/src/canvas/canvas-origin.ts +14 -2
  34. package/src/canvas/iframe-inline-edit.ts +79 -25
  35. package/src/types.ts +3 -1
package/dist/studio.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  seedPublishConnected
3
- } from "./chunks/studio-qymkxbcq.js";
3
+ } from "./chunks/studio-dgj7gq44.js";
4
4
  import"./chunks/studio-wdt9y92a.js";
5
5
  import {
6
6
  ElementResolutionController,
@@ -67,7 +67,7 @@ import {
67
67
  registerStyleCommands,
68
68
  render as render3,
69
69
  setInspectorTab
70
- } from "./chunks/studio-1d6bm7eg.js";
70
+ } from "./chunks/studio-f5yyvqxv.js";
71
71
  import {
72
72
  CommandUnavailableError,
73
73
  chordFromEvent,
@@ -99,7 +99,7 @@ import {
99
99
  showSlashMenu,
100
100
  showSlashMenuAtRect,
101
101
  suppressBlockActionBar
102
- } from "./chunks/studio-qb6vy1z1.js";
102
+ } from "./chunks/studio-ws6ef6js.js";
103
103
  import {
104
104
  addableContexts,
105
105
  defaultCommands,
@@ -111,7 +111,7 @@ import {
111
111
  tokenLabel,
112
112
  transposeStylebookStyle,
113
113
  writeTokenOverride
114
- } from "./chunks/studio-gfzyxa0m.js";
114
+ } from "./chunks/studio-hyn3vhwr.js";
115
115
  import {
116
116
  DERIVE_PRESETS,
117
117
  JxColorPopover,
@@ -144,7 +144,7 @@ import {
144
144
  spNumberField,
145
145
  spTextArea,
146
146
  spTextField
147
- } from "./chunks/studio-n9k15qdg.js";
147
+ } from "./chunks/studio-v1va0vhd.js";
148
148
  import {
149
149
  LitElement,
150
150
  css
@@ -160,7 +160,7 @@ import {
160
160
  import {
161
161
  initQuickSearch,
162
162
  openQuickSearch
163
- } from "./chunks/studio-9sxn0eaa.js";
163
+ } from "./chunks/studio-jnakhg87.js";
164
164
  import {
165
165
  addRecentProject,
166
166
  clearRecentProjects,
@@ -187,10 +187,10 @@ import {
187
187
  removeRecentProject,
188
188
  renderFilesTemplate,
189
189
  seedEntry
190
- } from "./chunks/studio-jand3kjg.js";
190
+ } from "./chunks/studio-v2c5pmjj.js";
191
191
  import {
192
192
  computeWindow
193
- } from "./chunks/studio-91sdrn9x.js";
193
+ } from "./chunks/studio-sccge2wf.js";
194
194
  import {
195
195
  autoSyncProjectOnOpen,
196
196
  cleanupGitPanel,
@@ -211,13 +211,13 @@ import {
211
211
  registerSourceControlCommands,
212
212
  renderGitPanel,
213
213
  stripRange
214
- } from "./chunks/studio-tdt7b6qb.js";
214
+ } from "./chunks/studio-ke244pam.js";
215
215
  import {
216
216
  createPanelScheduler
217
217
  } from "./chunks/studio-8jpzqk5s.js";
218
218
  import {
219
219
  selectContributedEntry
220
- } from "./chunks/studio-rz861p2b.js";
220
+ } from "./chunks/studio-hb7c46jy.js";
221
221
  import {
222
222
  notifySettingsDocument,
223
223
  onSettingsDocumentChanged,
@@ -238,7 +238,7 @@ import {
238
238
  renderForm,
239
239
  renderProvenanceChip,
240
240
  resolveContextPointer
241
- } from "./chunks/studio-rkr6pm84.js";
241
+ } from "./chunks/studio-ef8t53vc.js";
242
242
  import {
243
243
  validateDoc,
244
244
  validateProjectConfig
@@ -263,7 +263,7 @@ import {
263
263
  registerGridCommands,
264
264
  scanLibrary,
265
265
  uploadDirForCategory
266
- } from "./chunks/studio-4m39t01b.js";
266
+ } from "./chunks/studio-0b6fs05q.js";
267
267
  import {
268
268
  live
269
269
  } from "./chunks/studio-c31yb4t6.js";
@@ -307,7 +307,7 @@ import {
307
307
  setIncludeDrafts,
308
308
  startFsSync,
309
309
  writesForTurn
310
- } from "./chunks/studio-gqpbrrj5.js";
310
+ } from "./chunks/studio-26sv44br.js";
311
311
  import"./chunks/studio-2w11grav.js";
312
312
  import"./chunks/studio-bw5dz6km.js";
313
313
  import {
@@ -316,18 +316,18 @@ import {
316
316
  isImage,
317
317
  setMediaChangedHandler,
318
318
  uploadAssets
319
- } from "./chunks/studio-0svvdm0a.js";
319
+ } from "./chunks/studio-cz2j1c0g.js";
320
320
  import {
321
321
  beginActivity,
322
322
  registerActivityPanel,
323
323
  showProgressModal
324
- } from "./chunks/studio-d1f2rqxh.js";
324
+ } from "./chunks/studio-28xc8ws4.js";
325
325
  import {
326
326
  currentDeploy,
327
327
  latestDeployment,
328
328
  noteDeployment,
329
329
  platformSupportsPublish
330
- } from "./chunks/studio-675v08qj.js";
330
+ } from "./chunks/studio-aweyr4v1.js";
331
331
  import {
332
332
  CATEGORIES,
333
333
  SPAN_FULL_RENDER,
@@ -439,7 +439,7 @@ import {
439
439
  timeSpan,
440
440
  timeSpanAsync,
441
441
  updateActivePanelHeaders
442
- } from "./chunks/studio-h6553a8h.js";
442
+ } from "./chunks/studio-pyrw8h0w.js";
443
443
  import {
444
444
  BOTTOM_TAB_IDS,
445
445
  BUFFER_COMMIT,
@@ -606,7 +606,7 @@ import {
606
606
  updateSiteConfig,
607
607
  view,
608
608
  workspace
609
- } from "./chunks/studio-kfq9ed73.js";
609
+ } from "./chunks/studio-ra7ar50f.js";
610
610
  import {
611
611
  classMap,
612
612
  ifDefined,
@@ -669,7 +669,7 @@ import {
669
669
  mountStillWanted,
670
670
  refreshExtensionUi,
671
671
  refreshFormats
672
- } from "./chunks/studio-40fm3hyr.js";
672
+ } from "./chunks/studio-rqemfhz6.js";
673
673
  import {
674
674
  getPlatform,
675
675
  hasPlatform,
@@ -16807,7 +16807,7 @@ function entryIdsFor(collection) {
16807
16807
  if (known) {
16808
16808
  return known;
16809
16809
  }
16810
- const pending = import("./chunks/content-source-cn9te96r.js").then((m) => m.listCollectionEntryIds(collection));
16810
+ const pending = import("./chunks/content-source-k9724vcv.js").then((m) => m.listCollectionEntryIds(collection));
16811
16811
  entryIdCache.set(collection, pending);
16812
16812
  return pending;
16813
16813
  }
@@ -17068,7 +17068,7 @@ function renderEntryMode(surface, tab) {
17068
17068
  });
17069
17069
  }
17070
17070
  async function openEntryEditor(path) {
17071
- const { openFileInTab: openFileInTab2 } = await import("./chunks/files-dsh5g8j4.js");
17071
+ const { openFileInTab: openFileInTab2 } = await import("./chunks/files-yew27sn9.js");
17072
17072
  await openFileInTab2(path);
17073
17073
  for (const [id, tab] of workspace.tabs.entries()) {
17074
17074
  if (tab.documentPath !== path) {
@@ -17129,7 +17129,7 @@ async function createEntry(collectionName) {
17129
17129
  });
17130
17130
  return null;
17131
17131
  }
17132
- const { createFileIn: createFileIn2 } = await import("./chunks/files-dsh5g8j4.js");
17132
+ const { createFileIn: createFileIn2 } = await import("./chunks/files-yew27sn9.js");
17133
17133
  const created = await createFileIn2({
17134
17134
  content,
17135
17135
  dir: collection.dir,
@@ -22349,7 +22349,7 @@ function groupProblems(records = problems) {
22349
22349
  return groups;
22350
22350
  }
22351
22351
  function openProblemPath(path) {
22352
- import("./chunks/files-dsh5g8j4.js").then((m) => m.openFileInTab(path));
22352
+ import("./chunks/files-yew27sn9.js").then((m) => m.openFileInTab(path));
22353
22353
  }
22354
22354
  function actionTpl(record) {
22355
22355
  const id = record.action;
@@ -58591,9 +58591,9 @@ function renderDefsEditor(container) {
58591
58591
 
58592
58592
  // src/version.ts
58593
58593
  var APP_NAME = "Jx Studio";
58594
- var VERSION = "2.4.1";
58595
- var BUILD_DATE = "2026-08-24T11:52:26.010Z";
58596
- var GIT_COMMIT = "eb4fe5d";
58594
+ var VERSION = "2.4.2";
58595
+ var BUILD_DATE = "2026-08-24T19:48:51.173Z";
58596
+ var GIT_COMMIT = "7859073";
58597
58597
  var LINKS = {
58598
58598
  github: "https://github.com/jxsuite/jx",
58599
58599
  docs: "https://github.com/jxsuite/jx#readme",
@@ -59670,7 +59670,7 @@ async function openProjectSettings(section) {
59670
59670
  showSettingsDocument();
59671
59671
  notifySettingsDocument();
59672
59672
  try {
59673
- const { syncExtensionSettingsSections } = await import("./chunks/extension-sections-mdgmqrry.js");
59673
+ const { syncExtensionSettingsSections } = await import("./chunks/extension-sections-0ada0xcw.js");
59674
59674
  await syncExtensionSettingsSections();
59675
59675
  } catch (error) {
59676
59676
  notify.warn(`Could not load extension settings sections — ${errorMessage(error)}`, {
@@ -60294,7 +60294,7 @@ function publishCommands() {
60294
60294
  name: "set_up_publishing"
60295
60295
  },
60296
60296
  run: async () => {
60297
- const { openPublishPanel } = await import("./chunks/publish-panel-b0xh7b4y.js");
60297
+ const { openPublishPanel } = await import("./chunks/publish-panel-a0pdft5w.js");
60298
60298
  openPublishPanel();
60299
60299
  },
60300
60300
  title: "Set Up Publishing"
@@ -61016,7 +61016,7 @@ function i18nCommands() {
61016
61016
  });
61017
61017
  return;
61018
61018
  }
61019
- const { openFileInTab: openFileInTab2 } = await import("./chunks/files-dsh5g8j4.js");
61019
+ const { openFileInTab: openFileInTab2 } = await import("./chunks/files-yew27sn9.js");
61020
61020
  await openFileInTab2(path);
61021
61021
  },
61022
61022
  title: "Open Translation"
@@ -61063,7 +61063,7 @@ function i18nCommands() {
61063
61063
  return;
61064
61064
  }
61065
61065
  const cut = path.lastIndexOf("/");
61066
- const { createFileIn: createFileIn2, openFileInTab: openFileInTab2 } = await import("./chunks/files-dsh5g8j4.js");
61066
+ const { createFileIn: createFileIn2, openFileInTab: openFileInTab2 } = await import("./chunks/files-yew27sn9.js");
61067
61067
  const created = await createFileIn2({
61068
61068
  content,
61069
61069
  dir: cut === -1 ? "." : path.slice(0, cut),
@@ -61876,5 +61876,5 @@ export {
61876
61876
  hasUnsavedTabs
61877
61877
  };
61878
61878
 
61879
- //# debugId=C20E21297FE0976064756E2164756E21
61879
+ //# debugId=31462A53C2ABCD7964756E2164756E21
61880
61880
  //# sourceMappingURL=studio.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "2.4.1",
3
+ "version": "2.4.2",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,7 +60,7 @@
60
60
  "@jxsuite/ai": "^0.36.2",
61
61
  "@jxsuite/collab": "^0.8.1",
62
62
  "@jxsuite/create": "^1.3.5",
63
- "@jxsuite/formulas": "^0.0.11",
63
+ "@jxsuite/formulas": "^0.0.12",
64
64
  "@jxsuite/markup": "^0.4.5",
65
65
  "@jxsuite/protocol": "^1.2.0",
66
66
  "@jxsuite/runtime": "^2.0.4",
@@ -42,12 +42,24 @@ export function canvasBaseOrigin(): string {
42
42
  * @returns {string} A base URL ending in `/`, safe to pass to `new URL(relativePath, base)`.
43
43
  */
44
44
  export function documentBase(projectRoot?: string): string {
45
+ const origin = canvasBaseOrigin();
45
46
  const declared = hasPlatform() ? getPlatform().documentBaseUrl : undefined;
46
47
  if (declared) {
47
- return declared.endsWith("/") ? declared : `${declared}/`;
48
+ const withSlash = declared.endsWith("/") ? declared : `${declared}/`;
49
+ /*
50
+ * ABSOLUTE, ALWAYS. The caller uses this as `new URL(documentPath, base)`, and a `base` that is
51
+ * itself relative throws `Failed to construct 'URL': Invalid base URL` — which is not a failed
52
+ * fetch but a failed MOUNT: the canvas renders nothing at all.
53
+ *
54
+ * Jx Cloud declares a root-relative base (`/api/v1/p/:owner/:repo/:branch/studio/raw/`), which
55
+ * is the natural thing to declare and must keep working. Resolving it against the canvas origin
56
+ * here means a platform may declare either shape. An already-absolute URL passes through
57
+ * unchanged, because that is what `new URL` does with one.
58
+ */
59
+ return new URL(withSlash, `${origin}/`).href;
48
60
  }
49
61
  const root = projectRoot || "";
50
- return `${canvasBaseOrigin()}/${root ? `${root}/` : ""}`;
62
+ return `${origin}/${root ? `${root}/` : ""}`;
51
63
  }
52
64
 
53
65
  /**
@@ -38,6 +38,7 @@ import { adjacentBlock, blocksInOrder } from "./iframe-position";
38
38
  import { isTagActiveInSelection, toggleInlineFormat } from "../editor/inline-format";
39
39
  import { applyLink, insertTemplateToken, linkStateForSelection } from "../editor/inline-link";
40
40
  import { restoreTemplateExpressions } from "../utils/edit-display";
41
+ import { RESERVED_KEYS, camelToKebab } from "@jxsuite/runtime";
41
42
  import { rectOfRange } from "../utils/geometry";
42
43
  import { getNodeAtPath } from "../state";
43
44
  import { parseJxPath, serializeJxPath } from "./path-mapping";
@@ -86,6 +87,77 @@ function ownerInstanceOf(el: HTMLElement): HTMLElement | null {
86
87
  return null;
87
88
  }
88
89
 
90
+ /**
91
+ * Whether the instance node delivers `prop` by some route OTHER than `$props` — in which case
92
+ * "absent from `$props`" does not mean "unset", and a `$props` write would add a SECOND source for
93
+ * one rendered value.
94
+ *
95
+ * The routes are the property bridge's own, in the order `connectedCallback` reads them
96
+ * (`compiler.md` §4.4) — a `data-jx-props` payload, literal `props.*` attributes, then JS
97
+ * properties set before connection — plus the one that reaches that third source without going
98
+ * through `$props`. Each was reproduced against the real runtime:
99
+ *
100
+ * 1. `attributes: { "data-jx-props": "{…}" }` — the compiler's pre-rendered payload, parsed into state
101
+ * before anything else. Studio edits source documents rather than build output, so this is the
102
+ * rare one; it is here because the bridge reads it.
103
+ * 2. The JSON shorthand `attributes: { "props.text": "…" }`. `connectedCallback` collects `props.*`
104
+ * attributes into state, so the marker renders that text while `$props` is empty. This repo's
105
+ * own site carries 95 of them.
106
+ * 3. A prop whose name collides with a REFLECTED HTMLElement property — `title`, `role`, `id`, `lang`,
107
+ * `dir`, `slot`, `hidden`. `setAttribute("title", …)` writes the JS property as a side effect,
108
+ * and the merge `key in this && this[key] !== undefined` picks it up. 41 shipped starter
109
+ * components declare `title` or `role` as state. Matched the way `instanceSupplies` matches:
110
+ * attribute names are case-insensitive, and an ARIA-style reflection carries a kebab attribute
111
+ * for a camelCase property (`ariaLabel` → `aria-label`).
112
+ * 4. A TOP-LEVEL key on the instance node — `{ "tagName": "x-card", "heading": "Local" }`. This is the
113
+ * property-first interface written straight onto the node: `applyProperties` sets every
114
+ * non-reserved key as a JS property, which is exactly what the merge then reads back off `this`.
115
+ * Reserved keys (`name`, `items`, `children`, …) are skipped there and so are skipped here —
116
+ * refusing on one would block a prop that the key never delivered.
117
+ *
118
+ * Editing any of them writes `$props[prop]` and leaves the other value standing. For case 3 the
119
+ * attribute WINS (applyAttributes runs after the $props assignment) so the edit is simply
120
+ * invisible; for the rest `$props` wins and the stale value waits — clearing the prop later
121
+ * resurrects it. Refusing keeps the properties panel, which edits the real source, as the one way
122
+ * in.
123
+ */
124
+ function deliveredOutsideProps(node: JxMutableNode | undefined, prop: string): boolean {
125
+ if (!node) {
126
+ return false;
127
+ }
128
+ const attrs = node.attributes as Record<string, unknown> | undefined;
129
+ if (attrs) {
130
+ if (attrs[`props.${prop}`] !== undefined) {
131
+ return true;
132
+ }
133
+ const reflected = new Set([prop.toLowerCase(), camelToKebab(prop).toLowerCase()]);
134
+ for (const [name, val] of Object.entries(attrs)) {
135
+ if (val !== undefined && reflected.has(name.toLowerCase())) {
136
+ return true;
137
+ }
138
+ }
139
+ const payload = attrs["data-jx-props"];
140
+ if (typeof payload === "string" && payloadNames(payload, prop)) {
141
+ return true;
142
+ }
143
+ }
144
+ // `$`-prefixed keys are scope bindings, never DOM properties — `applyProperties` skips them too.
145
+ if (RESERVED_KEYS.has(prop) || prop.startsWith("$")) {
146
+ return false;
147
+ }
148
+ return (node as Record<string, unknown>)[prop] !== undefined;
149
+ }
150
+
151
+ /** Whether a `data-jx-props` JSON payload carries `prop`. Unparseable payloads deliver nothing. */
152
+ function payloadNames(payload: string, prop: string): boolean {
153
+ try {
154
+ const parsed: unknown = JSON.parse(payload);
155
+ return typeof parsed === "object" && parsed !== null && prop in parsed;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
89
161
  /** Locate the rendered element for a document path via its stamped `data-jx-path`. */
90
162
  function elementForPath(container: HTMLElement, path: JxPath): HTMLElement | null {
91
163
  const serialized = serializeJxPath(path);
@@ -267,10 +339,12 @@ export function startIframeInlineEdit(
267
339
  };
268
340
 
269
341
  /**
270
- * Whether the instance's RAW `$props[prop]` value (from the pre-edit-display shadow doc) is
271
- * plain-text editable. Template-valued (`"${...}"`) and `$ref`-valued props render display sugar,
272
- * not the value — committing typed text would silently destroy the binding, so they're blocked.
273
- * An absent getShadowDoc is permissive (tests, same contract as getMode).
342
+ * Whether the instance's prop (read from the pre-edit-display shadow doc) is plain-text editable
343
+ * AND `$props` is the place it lives — a value the instance delivers by another route belongs to
344
+ * that route, and `deliveredOutsideProps` refuses it. Template-valued (`"${...}"`) and
345
+ * `$ref`-valued props render display sugar, not the value — committing typed text would silently
346
+ * destroy the binding, so they're blocked. An absent getShadowDoc is permissive (tests, same
347
+ * contract as getMode).
274
348
  */
275
349
  const propEditableAt = (hostPath: JxPath, prop: string): boolean => {
276
350
  const shadowDoc = opts?.getShadowDoc?.();
@@ -278,27 +352,7 @@ export function startIframeInlineEdit(
278
352
  return true;
279
353
  }
280
354
  const node = getNodeAtPath(shadowDoc, hostPath) as JxMutableNode | undefined;
281
- /*
282
- * `$props` IS NOT THE ONLY PLACE A PROP VALUE LIVES, and "absent from $props" is therefore not
283
- * the same question as "unset". Two shapes deliver one through `attributes` instead, and both
284
- * were reproduced against the real runtime:
285
- *
286
- * 1. The JSON shorthand `attributes: { "props.text": "…" }`. `connectedCallback` collects
287
- * `props.*` attributes into state, so the marker renders that text while `$props` is
288
- * empty. This repo's own site carries 95 of them.
289
- * 2. A prop whose name collides with a REFLECTED HTMLElement property — `title`, `role`,
290
- * `id`, `lang`, `dir`, `slot`, `hidden`. `setAttribute("title", …)` writes the JS property
291
- * as a side effect, and the merge `key in this && this[key] !== undefined` picks it up.
292
- * 41 shipped starter components declare `title` or `role` as state.
293
- *
294
- * Editing either would write `$props[prop]` and leave the attribute standing — two sources for
295
- * one rendered value. In case 2 the attribute WINS (applyAttributes runs after the $props
296
- * assignment), so the edit is simply invisible; in case 1 clearing the prop later resurrects
297
- * the old text. Refusing keeps the properties panel, which edits the attribute itself, as the
298
- * one way in.
299
- */
300
- const attrs = node?.attributes as Record<string, unknown> | undefined;
301
- if (attrs?.[`props.${prop}`] !== undefined || attrs?.[prop] !== undefined) {
355
+ if (deliveredOutsideProps(node, prop)) {
302
356
  return false;
303
357
  }
304
358
  const raw = (node?.$props as Record<string, unknown> | undefined)?.[prop];
package/src/types.ts CHANGED
@@ -195,7 +195,9 @@ export interface StudioPlatform {
195
195
  * every `$ref` fetch landed on the SPA fallback — which answers HTML at **200**, so `res.ok`
196
196
  * passed and the parse died on `Unexpected token '<'`. Images failed the same way in silence.
197
197
  *
198
- * Must end in `/`; a project-relative path is appended to it verbatim.
198
+ * May be absolute or root-relative — a root-relative value is resolved against the canvas origin,
199
+ * because the canvas uses this as `new URL(path, base)` and a relative BASE throws. A missing
200
+ * trailing `/` is added: without it `new URL` drops the last segment.
199
201
  */
200
202
  documentBaseUrl?: string;
201
203
  activate: (root?: string) => Promise<void>;