@jxsuite/studio 2.4.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
  3. package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
  4. package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
  5. package/dist/chunks/content-source-t88vb2dr.js +49 -0
  6. package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
  7. package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
  8. package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
  9. package/dist/chunks/file-ops-rskmhxyq.js +48 -0
  10. package/dist/chunks/files-0cratsr0.js +92 -0
  11. package/dist/chunks/{format-host-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
  12. package/dist/chunks/fs-events-hrrg328g.js +21 -0
  13. package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
  14. package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
  15. package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
  16. package/dist/chunks/layers-q75tj2mf.js +45 -0
  17. package/dist/chunks/lit-html-j055mkh8.js +22 -0
  18. package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
  19. package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
  20. package/dist/chunks/notify-yjm79n4s.js +30 -0
  21. package/dist/chunks/parse-24d0gay3.js +19 -0
  22. package/dist/chunks/platform-ba1j1gbh.js +16 -0
  23. package/dist/chunks/publish-panel-54jvhskq.js +28 -0
  24. package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
  25. package/dist/chunks/references-3qx8fkyp.js +29 -0
  26. package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
  27. package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
  28. package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
  29. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
  30. package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
  31. package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
  32. package/dist/chunks/{studio-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
  33. package/dist/chunks/studio-2zkz8evn.js +3163 -0
  34. package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
  35. package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
  36. package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
  37. package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -133
  38. package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
  39. package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
  40. package/dist/chunks/{studio-hyn3vhwr.js → studio-8swrjbny.js} +31 -18
  41. package/dist/chunks/studio-92ab2jqe.js +282 -0
  42. package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
  43. package/dist/chunks/{studio-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
  44. package/dist/chunks/studio-9n4mrmnb.js +184 -0
  45. package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
  46. package/dist/chunks/{studio-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
  47. package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
  48. package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
  49. package/dist/chunks/studio-cft9r4r7.js +119 -0
  50. package/dist/chunks/{studio-rqemfhz6.js → studio-eenfkt4e.js} +6 -6
  51. package/dist/chunks/studio-eesa1myv.js +498 -0
  52. package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
  53. package/dist/chunks/{studio-ke244pam.js → studio-ex8zyp98.js} +51 -25
  54. package/dist/chunks/studio-gv8vvshc.js +118 -0
  55. package/dist/chunks/studio-hbtp0eh1.js +1315 -0
  56. package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
  57. package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
  58. package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
  59. package/dist/chunks/studio-kyh9avvv.js +28 -0
  60. package/dist/chunks/{studio-jnakhg87.js → studio-kyk2v92a.js} +14 -14
  61. package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
  62. package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
  63. package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
  64. package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
  65. package/dist/chunks/{studio-sccge2wf.js → studio-p8j86wag.js} +2 -2
  66. package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
  67. package/dist/chunks/studio-q43a5y9h.js +394 -0
  68. package/dist/chunks/studio-q7v10cg2.js +106 -0
  69. package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
  70. package/dist/chunks/studio-r1r7afxx.js +1066 -0
  71. package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
  72. package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
  73. package/dist/chunks/studio-sf7zjgv1.js +91 -0
  74. package/dist/chunks/studio-t7qb9db8.js +665 -0
  75. package/dist/chunks/studio-tmc5emrw.js +8 -0
  76. package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
  77. package/dist/chunks/{studio-dgj7gq44.js → studio-v38hmvx1.js} +7 -7
  78. package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
  79. package/dist/chunks/studio-wcy8trfx.js +263 -0
  80. package/dist/chunks/{studio-hb7c46jy.js → studio-wx5609xd.js} +21 -15
  81. package/dist/chunks/studio-xrapxwwc.js +1539 -0
  82. package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
  83. package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
  84. package/dist/chunks/workspace-xpres40v.js +89 -0
  85. package/dist/iframe-entry.js +450 -48
  86. package/dist/studio.js +2455 -1761
  87. package/package.json +16 -15
  88. package/src/browse/library-layouts.ts +3 -3
  89. package/src/browse/library-pane.ts +59 -5
  90. package/src/browse/library-preview.ts +19 -3
  91. package/src/canvas/asset-refs.ts +119 -0
  92. package/src/canvas/asset-resolve.ts +256 -0
  93. package/src/canvas/canvas-live-render.ts +23 -15
  94. package/src/canvas/canvas-render.ts +23 -1
  95. package/src/canvas/iframe-entry.ts +1 -0
  96. package/src/canvas/iframe-host.ts +4 -0
  97. package/src/canvas/iframe-patch.ts +47 -21
  98. package/src/canvas/iframe-protocol.ts +9 -0
  99. package/src/canvas/iframe-render.ts +50 -16
  100. package/src/commands/app-commands.ts +4 -0
  101. package/src/commands/command-args.ts +29 -0
  102. package/src/commands/context.ts +15 -1
  103. package/src/commands/defaults.ts +29 -1
  104. package/src/commands/levels.ts +16 -0
  105. package/src/commands/live-context.ts +3 -0
  106. package/src/content/collection-match.ts +267 -0
  107. package/src/content/entry-commands.ts +38 -29
  108. package/src/content/entry-fields.ts +6 -0
  109. package/src/editor/file-drop-action.ts +2 -1
  110. package/src/editor/shortcuts.ts +5 -0
  111. package/src/files/file-ops.ts +10 -42
  112. package/src/files/files.ts +487 -61
  113. package/src/files/fs-events.ts +9 -0
  114. package/src/files/gitignore.ts +432 -0
  115. package/src/files/media-paths.ts +37 -14
  116. package/src/files/media-upload.ts +47 -6
  117. package/src/files/serialize-document.ts +60 -0
  118. package/src/format/convert-file.ts +511 -0
  119. package/src/format/format-choices.ts +199 -0
  120. package/src/grid/grid-idle.ts +45 -0
  121. package/src/grid/grid-view.ts +35 -0
  122. package/src/grid/schema-columns.ts +2 -1
  123. package/src/grid/sources/content-source.ts +24 -3
  124. package/src/media/media-open.ts +58 -0
  125. package/src/media/media-pane.ts +310 -0
  126. package/src/new-project/import-tab.ts +333 -160
  127. package/src/new-project/new-project-modal.ts +27 -34
  128. package/src/packages/ensure-deps.ts +14 -7
  129. package/src/packages/jxsuite-update.ts +11 -11
  130. package/src/page-params.ts +10 -46
  131. package/src/panels/activity-bar.ts +45 -16
  132. package/src/panels/ai-chat/attached-context.ts +6 -1
  133. package/src/panels/ai-chat/chat-view.ts +255 -13
  134. package/src/panels/ai-chat/composer.ts +21 -80
  135. package/src/panels/ai-panel.ts +116 -2
  136. package/src/panels/frontmatter-fields.ts +6 -2
  137. package/src/panels/navigator-panels.ts +7 -6
  138. package/src/panels/properties-panel.ts +8 -2
  139. package/src/panels/seo-modal.ts +5 -6
  140. package/src/panels/settings-menu.ts +799 -0
  141. package/src/panels/signals-panel.ts +2 -1
  142. package/src/panels/toolbar.ts +135 -15
  143. package/src/platforms/cloud.ts +34 -3
  144. package/src/platforms/devserver.ts +72 -34
  145. package/src/preview/preview-overlay.ts +226 -0
  146. package/src/services/ai-ask.ts +247 -0
  147. package/src/services/ai-import-tools.ts +494 -0
  148. package/src/services/ai-models.ts +101 -11
  149. package/src/services/ai-project-tools.ts +20 -26
  150. package/src/services/ai-settings.ts +69 -71
  151. package/src/services/ai-system-prompt.ts +88 -2
  152. package/src/services/ai-turn-signal.ts +70 -0
  153. package/src/services/cf-settings.ts +19 -37
  154. package/src/services/connection.ts +74 -0
  155. package/src/services/context-manager.ts +89 -0
  156. package/src/services/document-assistant.ts +49 -7
  157. package/src/services/idle.ts +6 -1
  158. package/src/services/import-client.ts +33 -6
  159. package/src/services/import-run.ts +158 -0
  160. package/src/services/import-seed.ts +120 -0
  161. package/src/services/jx-validate.ts +58 -6
  162. package/src/services/project-adoption.ts +71 -0
  163. package/src/services/references.ts +0 -0
  164. package/src/services/settings/definitions.ts +181 -0
  165. package/src/services/settings/kernel.ts +335 -0
  166. package/src/services/settings/write-queue.ts +116 -0
  167. package/src/services/tool-executor.ts +34 -2
  168. package/src/settings/contexts-section.ts +45 -3
  169. package/src/settings/dependencies-editor.ts +43 -23
  170. package/src/settings/general-settings.ts +30 -12
  171. package/src/settings/preferences-accounts.ts +6 -19
  172. package/src/settings/preferences-dialog.ts +18 -41
  173. package/src/settings/preferences-keymap.ts +8 -18
  174. package/src/settings/preferences-sections.ts +57 -0
  175. package/src/settings/schema-field-ui.ts +2 -2
  176. package/src/settings/settings-document.ts +65 -9
  177. package/src/shell.ts +6 -14
  178. package/src/studio.ts +65 -7
  179. package/src/tabs/transact.ts +42 -19
  180. package/src/types.ts +164 -8
  181. package/src/ui/ai-credentials-form.ts +52 -27
  182. package/src/ui/ai-model-picker.ts +162 -0
  183. package/src/ui/choice-field.ts +72 -0
  184. package/src/ui/layers.ts +83 -5
  185. package/src/ui/media-picker.ts +6 -11
  186. package/src/utils/studio-utils.ts +43 -31
  187. package/styles/forced-colors.css +7 -0
  188. package/styles/overlays.css +256 -0
  189. package/styles/panels.css +13 -1
  190. package/styles/shell.css +111 -0
  191. package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
  192. package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
  193. package/dist/chunks/content-source-k9724vcv.js +0 -39
  194. package/dist/chunks/files-yew27sn9.js +0 -69
  195. package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
  196. package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
  197. package/dist/chunks/quick-search-9avyc4pc.js +0 -54
  198. package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
  199. package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
  200. package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
  201. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
  202. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
  203. package/dist/chunks/studio-a7pzveq1.js +0 -110
  204. package/dist/chunks/studio-c31yb4t6.js +0 -55
  205. package/dist/chunks/studio-ra7ar50f.js +0 -6682
  206. package/src/canvas/content-assets.ts +0 -263
  207. package/src/canvas/site-style-css.ts +0 -121
  208. package/src/services/settings-store.ts +0 -99
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "2.4.3",
3
+ "version": "4.0.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,19 +52,20 @@
52
52
  "lint:styles": "bun run scripts/check-styles.ts",
53
53
  "test": "bun run lint:styles && bun test --isolate",
54
54
  "test:coverage": "bun test --isolate --coverage",
55
- "upgrade": "bunx npm-check-updates -u && bun install"
55
+ "upgrade": "bun update --latest && bun install"
56
56
  },
57
57
  "dependencies": {
58
58
  "@atlaskit/pragmatic-drag-and-drop": "^3.0.0",
59
59
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.1.0",
60
- "@jxsuite/ai": "^0.36.3",
61
- "@jxsuite/collab": "^0.8.2",
62
- "@jxsuite/create": "^1.3.6",
63
- "@jxsuite/formulas": "^0.0.13",
64
- "@jxsuite/markup": "^0.4.6",
65
- "@jxsuite/protocol": "^1.2.1",
66
- "@jxsuite/runtime": "^2.0.5",
67
- "@jxsuite/schema": "^1.8.1",
60
+ "@jxsuite/ai": "^0.36.5",
61
+ "@jxsuite/collab": "^0.8.4",
62
+ "@jxsuite/create": "^1.3.8",
63
+ "@jxsuite/formulas": "^0.0.15",
64
+ "@jxsuite/markup": "^0.4.8",
65
+ "@jxsuite/protocol": "^2.1.0",
66
+ "@jxsuite/runtime": "^3.0.0",
67
+ "@jxsuite/schema": "^2.0.0",
68
+ "@jxsuite/site": "^1.0.0",
68
69
  "@spectrum-web-components/accordion": "^1.12.2",
69
70
  "@spectrum-web-components/action-bar": "1.12.2",
70
71
  "@spectrum-web-components/action-button": "^1.12.2",
@@ -107,15 +108,15 @@
107
108
  "lit": "^3.3.3",
108
109
  "lit-html": "^3.3.3",
109
110
  "monaco-editor": "^0.56.0",
110
- "tabulator-tables": "^6.5",
111
+ "tabulator-tables": "^6.5.2",
111
112
  "yaml": "^2.9.0"
112
113
  },
113
114
  "devDependencies": {
114
- "@happy-dom/global-registrator": "^20.11.6",
115
+ "@happy-dom/global-registrator": "^20.11.8",
115
116
  "@types/mdast": "^4.0.4",
116
- "@webref/css": "^8.7.2",
117
- "@webref/elements": "^2.7.1",
118
- "@webref/idl": "^3.82.2"
117
+ "@webref/css": "^8.7.3",
118
+ "@webref/elements": "^2.8.0",
119
+ "@webref/idl": "^3.83.0"
119
120
  },
120
121
  "//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
121
122
  }
@@ -19,7 +19,7 @@ import { html, nothing } from "lit-html";
19
19
  import { repeat } from "lit-html/directives/repeat.js";
20
20
  import { ref } from "lit-html/directives/ref.js";
21
21
  import { isImage } from "../files/media-upload";
22
- import { loopbackAssetSrc } from "../canvas/canvas-origin";
22
+ import { previewFileSrc } from "../files/media-paths";
23
23
  import { localeLabel } from "@jxsuite/schema/locale";
24
24
  import { groupByCategory, groupByDate } from "./library-model";
25
25
  import type { LibraryFile, LibraryLayout } from "./library-model";
@@ -154,7 +154,7 @@ function previewTpl(file: LibraryFile, ctx: LayoutContext): TemplateResult {
154
154
  return html`<img
155
155
  class="library-thumb"
156
156
  loading="lazy"
157
- src=${loopbackAssetSrc(`/${file.path}`)}
157
+ src=${previewFileSrc(file.path)}
158
158
  alt=""
159
159
  />`;
160
160
  }
@@ -268,7 +268,7 @@ export function mediaTpl(files: readonly LibraryFile[], ctx: LayoutContext): Tem
268
268
  ? html`<img
269
269
  class="library-thumb"
270
270
  loading="lazy"
271
- src=${loopbackAssetSrc(`/${file.path}`)}
271
+ src=${previewFileSrc(file.path)}
272
272
  alt=""
273
273
  />`
274
274
  : html`<sp-icon-document size="l"></sp-icon-document>`
@@ -42,8 +42,9 @@ import { invalidateUsages } from "../services/references";
42
42
  import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
43
43
  import { createFileIn, openFileInTab } from "../files/files";
44
44
  import { createEntry } from "../content/entry-commands";
45
+ import { formatByExtension, formatSerialize } from "../format/format-host";
45
46
  import { entryCollections } from "../content/entry-model";
46
- import { UPLOAD_ACCEPT, uploadAssets } from "../files/media-upload";
47
+ import { uploadAccept, uploadAssets } from "../files/media-upload";
47
48
  import { localeLabel } from "@jxsuite/schema/locale";
48
49
  import {
49
50
  LIBRARY_CATEGORIES,
@@ -70,6 +71,7 @@ import {
70
71
  tableRowsTpl,
71
72
  } from "./library-layouts";
72
73
  import type { ActivityHandle } from "../panels/activity-panel";
74
+ import type { FormatChoice } from "../files/files";
73
75
  import type { EffectScope } from "@vue/reactivity";
74
76
  import type { LibraryFile, LibraryLayout } from "./library-model";
75
77
  import type { LibrarySource } from "./library-source";
@@ -602,10 +604,60 @@ export function libraryNewEntries(): LibraryNewEntry[] {
602
604
  * `createEntry` is called rather than reimplemented; a second seeder here would drift from the one
603
605
  * the palette's `content.newEntry` uses within a release.
604
606
  *
605
- * A **document kind** keeps `files.ts`'s verbatim name field, deliberately: a page may be a `.md`
606
- * as easily as a `.json`, so the extension the author types is a real choice and forcing one here
607
- * would remove it. A collection's extension is not a choice — it is the collection's.
607
+ * A **document kind** gets the format PICKER, which is the same argument the verbatim name field
608
+ * used to make: a page may be a `.md` as easily as a `.json`, so the extension is a real choice —
609
+ * now offered rather than left to be typed from memory. A collection's extension is not a choice;
610
+ * it is the collection's, and `createEntry` supplies it.
611
+ *
612
+ * **A layout is not a choice either, and that is not a policy.** Both readers of a layout parse it
613
+ * as JSON and neither dispatches through the format registry — `site/layout-resolver.ts` throws and
614
+ * `site-context.ts` returns null — so a `.md` layout is a file the build cannot load. There is no
615
+ * `"layout"` document kind for a format to declare, so this is stated here rather than derived.
616
+ *
617
+ * A **component** keeps the picker: a format whose `$studio.documentMode.componentWhen` promotes a
618
+ * frontmatter `tagName` (Markdown's does) can express one perfectly well. What it cannot do is
619
+ * express one from a `newFileTemplate` that carries no `tagName`, so the seed below supplies it —
620
+ * through the format's own serializer, because this module knows no format's syntax.
621
+ */
622
+ /** How each document kind's extension is settled. Anything else falls back to the full picker. */
623
+ const FORMAT_CHOICE_BY_KIND: Record<string, FormatChoice> = {
624
+ component: { defaultExt: ".json", docKind: "component", kind: "choose" },
625
+ layout: { ext: ".json", kind: "fixed" },
626
+ page: { defaultExt: ".json", docKind: "page", kind: "choose" },
627
+ };
628
+
629
+ /**
630
+ * The body a new component starts as, when the reader picked a format rather than `.json`.
631
+ *
632
+ * `.json` answers `undefined` and takes the shared blank document. A format answers with its OWN
633
+ * serialization of `{ tagName }` — never a hand-written `---\ntagName: …\n---`, which would put one
634
+ * format's syntax in the Library. Without the `tagName` the file satisfies neither
635
+ * `$studio.documentMode.componentWhen` nor the build's tag registration, and is a component nothing
636
+ * loads. A stem that is not a valid custom-element name (no hyphen) cannot carry one, so it falls
637
+ * back to the format's own template and the author names the tag themselves.
608
638
  */
639
+ async function componentSeed(ext: string, fileName: string): Promise<string | undefined> {
640
+ const format = formatByExtension(ext);
641
+ if (!format?.capabilities.serialize) {
642
+ return undefined;
643
+ }
644
+ const tagName = fileName.slice(0, fileName.length - ext.length);
645
+ if (!/^[a-z][\d.a-z]*-[\d.a-z-]*$/.test(tagName)) {
646
+ return undefined;
647
+ }
648
+ try {
649
+ return await formatSerialize(
650
+ format.name,
651
+ { children: [], tagName },
652
+ { frontmatter: true, mode: "roundtrip" },
653
+ );
654
+ } catch {
655
+ // The serializer is the project's, so its failure is not a reason to refuse the creation —
656
+ // The format's own template still produces a file the author can finish by hand.
657
+ return undefined;
658
+ }
659
+ }
660
+
609
661
  export async function createLibraryEntry(key: string): Promise<string | null> {
610
662
  const entry = libraryNewEntries().find((row) => row.key === key);
611
663
  if (!entry) {
@@ -614,7 +666,9 @@ export async function createLibraryEntry(key: string): Promise<string | null> {
614
666
  const created =
615
667
  entry.collection === undefined
616
668
  ? await createFileIn({
669
+ ...(entry.key === "component" ? { content: componentSeed } : {}),
617
670
  dir: entry.dir,
671
+ format: FORMAT_CHOICE_BY_KIND[entry.key] ?? { defaultExt: ".json", kind: "choose" },
618
672
  source: "Library",
619
673
  suggestedName: "untitled",
620
674
  title: `New ${entry.label}`,
@@ -760,7 +814,7 @@ function toolbarTpl(panel: ActiveLibraryPane, files: readonly LibraryFile[]) {
760
814
  <input
761
815
  type="file"
762
816
  multiple
763
- accept=${UPLOAD_ACCEPT}
817
+ accept=${uploadAccept()}
764
818
  class="library-upload-input"
765
819
  @change=${(e: Event) => {
766
820
  const input = e.target as HTMLInputElement;
@@ -19,7 +19,13 @@
19
19
  * already knows a preview is in flight without this module declaring an idle source of its own.
20
20
  */
21
21
 
22
- import { buildScope, renderNode, setSkipServerFunctions } from "@jxsuite/runtime";
22
+ import {
23
+ buildScope,
24
+ renderNode,
25
+ setCanvasAssetResolver,
26
+ setSkipServerFunctions,
27
+ } from "@jxsuite/runtime";
28
+ import { assetContextFor, hostAssetDeclarations, resolveAssetRef } from "../canvas/asset-refs";
23
29
  import { componentRegistry } from "../files/components";
24
30
  import { getPlatform } from "../platform";
25
31
  import { loadFormats, formatForPath } from "../format/format-host";
@@ -115,6 +121,7 @@ export async function renderDocPreview(filePath: string): Promise<HTMLElement |
115
121
  try {
116
122
  const content = await getPlatform().readFile(filePath);
117
123
  setSkipServerFunctions(true);
124
+ const assets = assetContextFor(filePath, hostAssetDeclarations());
118
125
  await loadFormats();
119
126
  let document_: JxDocument;
120
127
  if (formatForPath(filePath)) {
@@ -124,8 +131,17 @@ export async function renderDocPreview(filePath: string): Promise<HTMLElement |
124
131
  document_ = JSON.parse(content) as JxDocument;
125
132
  }
126
133
  const scope = await buildScope(document_, {}, location.href);
127
- const element = renderNode(document_, scope);
128
- return element instanceof HTMLElement ? element : null;
134
+ /* This render lands in the PARENT document, so an `<img src="/hero.jpg">` inside it resolves
135
+ against `index.html` — which is the shell, not the site. Same hook the canvas installs, same
136
+ context, restored afterwards because the next preview is a different document and a stale
137
+ `documentDir` would resolve its relative refs against the wrong entry. */
138
+ setCanvasAssetResolver((value) => resolveAssetRef(value, assets));
139
+ try {
140
+ const element = renderNode(document_, scope);
141
+ return element instanceof HTMLElement ? element : null;
142
+ } finally {
143
+ setCanvasAssetResolver(null);
144
+ }
129
145
  } catch {
130
146
  return null;
131
147
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Asset references, resolved for the canvas — the half that asks the running editor.
3
+ *
4
+ * The resolution math itself is {@link file://./asset-resolve.ts}, which is pure and which the
5
+ * canvas iframe imports. This module is everything that has to look something up: the open
6
+ * project's content sections and locales, the active tab, the registered platform's declarations.
7
+ *
8
+ * The split is by REALM. The iframe bundle is deliberately dependency-light, and reaching
9
+ * `projectState` or `getPlatform()` from it drags the whole editor shell in behind it — which is
10
+ * what `check-studio-dist` caught the first time these lived in one file.
11
+ *
12
+ * @docs studio/projects/media
13
+ */
14
+
15
+ import { resolveI18n } from "@jxsuite/schema/locale";
16
+ import { BUILD_LANES, dirOfPath } from "@jxsuite/schema/asset-paths";
17
+ import { contentMountFor, resolveAssetRef } from "./asset-resolve";
18
+ import { documentBase, loopbackAssetSrc } from "./canvas-origin";
19
+ import { getPlatform, hasPlatform } from "../platform";
20
+ import { projectState } from "../store";
21
+ import { activeTab } from "../workspace/workspace";
22
+ import type { AssetContext } from "./asset-resolve";
23
+ import type { ContentSectionEntry } from "../types";
24
+
25
+ /* Re-exported so callers that want both halves import one module. The pure ones are defined next
26
+ door precisely so the iframe can reach them WITHOUT reaching any of the above. */
27
+ export { contentMountFor, mountedRefFor, resolveAssetRef } from "./asset-resolve";
28
+ export type { AssetContext } from "./asset-resolve";
29
+
30
+ /**
31
+ * The open project's canonical locale tags, or `[]` when it declares none.
32
+ *
33
+ * Through `resolveI18n` rather than off the raw config, so `EN-us` means the same thing here as it
34
+ * does in the compiler and the content loader — the whole reason that resolver is in
35
+ * `@jxsuite/schema`.
36
+ */
37
+ export function projectLocales(): string[] {
38
+ return resolveI18n(projectState?.projectConfig ?? {}).i18n?.locales ?? [];
39
+ }
40
+
41
+ /**
42
+ * The context for a document in the project currently open, or null when nothing applies.
43
+ *
44
+ * @param {string | null | undefined} documentPath - Project-relative path of the open document
45
+ * @param {object} [over] - Host declarations: the asset space and the project-file base URL
46
+ * @returns {AssetContext | null} A context, or null when no resolution is needed
47
+ */
48
+ export function assetContextFor(
49
+ documentPath: string | null | undefined,
50
+ over?: { space?: "site" | "repo" | undefined; fileBaseUrl?: string | undefined },
51
+ ): AssetContext | null {
52
+ const space = over?.space ?? "site";
53
+ const mount = contentMountFor(
54
+ documentPath,
55
+ projectState?.projectConfig?.content as Record<string, ContentSectionEntry> | undefined,
56
+ projectLocales(),
57
+ );
58
+ /* In site space the mount is the ONLY thing that needs doing, so no mount means no work. In repo
59
+ space every reference needs rebasing, mount or not. */
60
+ if (space === "site" && !mount) {
61
+ return null;
62
+ }
63
+ return {
64
+ documentDir: dirOfPath(documentPath ?? ""),
65
+ ...(over?.fileBaseUrl === undefined ? {} : { fileBaseUrl: over.fileBaseUrl }),
66
+ lanes: BUILD_LANES,
67
+ mounts: mount ? [mount] : [],
68
+ space,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * The src a PARENT-REALM preview should load for an AUTHORED value — a media-picker thumbnail, the
74
+ * social card in the SEO modal.
75
+ *
76
+ * The canvas resolves its own references; panel chrome renders in the parent document, where a
77
+ * content-relative value would resolve against `index.html` and 404 exactly as it did in the
78
+ * canvas. Same mapping, same rule — resolved against the ACTIVE tab, which is the entry whose field
79
+ * is being edited.
80
+ *
81
+ * Takes what the author WROTE. For a project FILE PATH — a library thumbnail, a media-browser tile
82
+ * — use `previewFileSrc` in `files/media-paths`; the two are separate because a file path is not an
83
+ * authored reference and confusing them is silent.
84
+ *
85
+ * The loopback absolutization is folded in rather than left to the caller: every one of the four
86
+ * call sites needs both steps, and one that composed them by hand would work everywhere except the
87
+ * desktop shell, where a relative src resolves against `views://`.
88
+ *
89
+ * @param {string} value - The reference as the author wrote it
90
+ * @returns {string} A src the parent realm can load
91
+ */
92
+ export function previewAssetSrc(value: string): string {
93
+ if (!value) {
94
+ return value;
95
+ }
96
+ const ctx = assetContextFor(activeTab.value?.documentPath, hostAssetDeclarations());
97
+ return loopbackAssetSrc(resolveAssetRef(value, ctx) ?? value);
98
+ }
99
+
100
+ /**
101
+ * What the registered host says about media — the platform's `assetSpace`, and the base project
102
+ * files are served under.
103
+ *
104
+ * Read through the platform rather than passed in, because every caller is panel chrome with no
105
+ * access to the render pipeline and because the answer is a property of the HOST, not of the call.
106
+ * The canvas pipeline builds the same two values from the same two places; the difference is only
107
+ * that it already has the document in hand.
108
+ *
109
+ * @returns {{ space?: "site" | "repo"; fileBaseUrl: string }} The host's declarations
110
+ */
111
+ export function hostAssetDeclarations(): {
112
+ space?: "site" | "repo" | undefined;
113
+ fileBaseUrl: string;
114
+ } {
115
+ return {
116
+ fileBaseUrl: documentBase(projectState?.projectRoot),
117
+ ...(hasPlatform() ? { space: getPlatform().assetSpace } : {}),
118
+ };
119
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Asset references, resolved for the canvas — the PURE half.
3
+ *
4
+ * Separate from `asset-refs.ts` for one reason, and it is a hard one: this module runs **inside the
5
+ * canvas iframe**, which is a deliberately dependency-light bundle. `asset-refs.ts` reaches the
6
+ * open project, the active tab and the registered platform, and importing any of that from the
7
+ * iframe drags the entire editor shell in behind it. The build gate caught exactly that.
8
+ *
9
+ * So the split is by REALM, not by taste: everything here is a pure function of its arguments, and
10
+ * everything that has to ask the running editor a question lives next door.
11
+ *
12
+ * A document names media in one of two shapes, and neither is a URL the canvas can use as written.
13
+ *
14
+ * A content entry references media relative to ITSELF — `![](./images/hero.png)` — so the file
15
+ * reads correctly in a plain markdown editor and so collections sourced outside the project root
16
+ * still reach their media (specs/site-architecture.md §9.3). On the built site the collection
17
+ * loader rewrites those refs onto the content type's asset mount,
18
+ * `/content/<type>/images/hero.png`. Studio opens an entry as a STANDALONE document, so that loader
19
+ * never runs.
20
+ *
21
+ * A page references media by SITE URL — `/hero.jpg` — which is a file the project tree calls
22
+ * `public/hero.jpg`. That works wherever the canvas is hosted by something that serves the site's
23
+ * own URL space, which is what the dev server and the desktop loopback both are.
24
+ *
25
+ * {@link AssetContext} is the host's answer to "which of those two do you serve", and
26
+ * {@link resolveAssetRef} is the whole of the resolution:
27
+ *
28
+ * - `"site"` — the origin already answers site URLs, so only the content-mount mapping is missing.
29
+ * This is what desktop and `jx dev` have always done, and the default when a host says nothing.
30
+ * - `"repo"` — nothing answers a site URL, and the host serves PROJECT PATHS under a base instead.
31
+ * Every reference resolves to the file it names and is rebased onto that base. The mount detour
32
+ * disappears: a content entry's `./images/hero.png` is already `content/posts/images/hero.png`, a
33
+ * real path the host can serve.
34
+ *
35
+ * The SOURCE document is never touched in either case: the authored ref is what gets serialized
36
+ * back to disk, and the properties panel shows the author what they wrote.
37
+ *
38
+ * One deliberate divergence from the content loader: it declines to rewrite a ref whose target does
39
+ * not exist (`existsSync`), leaving an unresolvable path alone rather than changing its meaning.
40
+ * The browser has no filesystem, so this resolves optimistically. A missing file is broken either
41
+ * way; pointing at the URL the built site would have attempted is the more informative failure.
42
+ *
43
+ * @docs studio/projects/media
44
+ */
45
+
46
+ import {
47
+ assetUrlFor,
48
+ encodeProjectPath,
49
+ isNonRelativeRef,
50
+ joinProjectPath,
51
+ projectPathForRef,
52
+ splitRefSuffix,
53
+ } from "@jxsuite/schema/asset-paths";
54
+ import type { AssetLane, AssetMount } from "@jxsuite/schema/asset-paths";
55
+ import type { ContentSectionEntry } from "../types";
56
+
57
+ /** Content type names safe in a URL path segment. Mirrors `SAFE_TYPE_NAME` in the content loader. */
58
+ const SAFE_TYPE_NAME = /^[\w.~-]+$/;
59
+
60
+ /** The project.json section key content types live under; also their URL namespace. */
61
+ const SECTION_KEY = "content";
62
+
63
+ /**
64
+ * The placeholder a content type's `source` may carry to say "one directory per locale".
65
+ *
66
+ * Held here rather than imported out of the parser extension's content loader ({@link
67
+ * file://../../../../extensions/parser/src/content-loader.ts}): that module reads the filesystem
68
+ * and this one runs in a browser. It is also an extension, and core may not import one.
69
+ */
70
+ const LOCALE_PLACEHOLDER = "{locale}";
71
+
72
+ /**
73
+ * Everything the canvas needs to resolve one reference, as PLAIN DATA.
74
+ *
75
+ * Plain data because it has to cross a realm. The resolver itself is a function and cannot be
76
+ * posted into the canvas iframe, so the parent posts this beside the render document and the iframe
77
+ * builds the resolver from it locally.
78
+ */
79
+ export interface AssetContext {
80
+ /** What the canvas ORIGIN answers for a site URL — the platform's `assetSpace`, defaulted. */
81
+ space: "site" | "repo";
82
+ /**
83
+ * Base URL under which the host serves PROJECT PATHS. Required by `"repo"` and inert without it:
84
+ * a host that declares repo space and no base has told us its site URLs are wrong without telling
85
+ * us what is right, and inventing an answer there would be worse than leaving the ref alone.
86
+ */
87
+ fileBaseUrl?: string | undefined;
88
+ /** Project-relative directory of the document being rendered (`""` at the project root). */
89
+ documentDir: string;
90
+ /** Project-relative asset mounts that apply to this document. */
91
+ mounts: AssetMount[];
92
+ /**
93
+ * The resolution order for a site-absolute URL.
94
+ *
95
+ * `BUILD_LANES` by default, and that is the deliberate answer rather than an omission: with no
96
+ * filesystem to probe, a lane list has to collapse to ONE candidate, and the one that makes the
97
+ * preview agree with the deployed site is the build's. See `DEV_SERVER_LANES` for the order the
98
+ * editing servers actually use, and why the two differ.
99
+ */
100
+ lanes: readonly AssetLane[];
101
+ }
102
+
103
+ /**
104
+ * The asset mount for the content type that owns `documentPath`, or null when the document is not a
105
+ * directory-backed content entry.
106
+ *
107
+ * Only in-project sources can match: an entry Studio can address project-relatively is by
108
+ * definition inside the project, and a source pointing elsewhere (`../../docs`) names files the
109
+ * file tree cannot reach in the first place.
110
+ *
111
+ * @param {string | null | undefined} documentPath - Project-relative path of the open document
112
+ * @param {Record<string, ContentSectionEntry> | null | undefined} content - Project.json `content`
113
+ * @param {readonly string[]} [locales] - Canonical locale tags, for `{locale}` sources
114
+ * @returns {AssetMount | null} The mount, project-relative
115
+ */
116
+ export function contentMountFor(
117
+ documentPath: string | null | undefined,
118
+ content: Record<string, ContentSectionEntry> | null | undefined,
119
+ locales: readonly string[] = [],
120
+ ): AssetMount | null {
121
+ if (!documentPath || !content) {
122
+ return null;
123
+ }
124
+ const path = documentPath.replaceAll("\\", "/").replace(/^\.\//, "");
125
+ let best: AssetMount | null = null;
126
+ const consider = (dir: string, urlPrefix: string): void => {
127
+ if (path.startsWith(`${dir}/`) && (!best || dir.length > best.dir.length)) {
128
+ best = { dir, urlPrefix };
129
+ }
130
+ };
131
+ for (const [name, def] of Object.entries(content)) {
132
+ if (!def?.source || !SAFE_TYPE_NAME.test(name)) {
133
+ continue;
134
+ }
135
+ const dir = normalizeSource(def.source);
136
+ // A single-file source is not a collection directory — its siblings are not its assets.
137
+ if (dir === "" || dir.startsWith("..")) {
138
+ continue;
139
+ }
140
+ if (dir.includes(LOCALE_PLACEHOLDER)) {
141
+ const mount = localeMountFor(path, dir, name, locales);
142
+ if (mount) {
143
+ consider(mount.dir, mount.urlPrefix);
144
+ }
145
+ continue;
146
+ }
147
+ if (/\.[a-z0-9]+$/i.test(dir)) {
148
+ continue;
149
+ }
150
+ consider(dir, `/${SECTION_KEY}/${name}`);
151
+ }
152
+ return best;
153
+ }
154
+
155
+ /**
156
+ * The mount for ONE locale of a `{locale}` source — the locale being whichever one `path` is in.
157
+ *
158
+ * A localized content type is N directories, not N content types, and each publishes separately at
159
+ * `/content/<type>/<locale>` so a French post's `./hero.png` and its English translation's cannot
160
+ * collide at one URL. The content loader picks the directory by probing the disk (`localeSource`);
161
+ * Studio has no disk, so it reads the locale out of the document path it was given and checks that
162
+ * against the project's declared tags. That is not a weaker test — the path IS the file, so the
163
+ * only question left is whether its directory is a locale the project declares.
164
+ *
165
+ * Matched case-insensitively, for the same reason the loader does: two writers name that directory
166
+ * and they differ. A project that declared `fr-CA` most likely typed `fr-CA/`, while Studio creates
167
+ * `fr-ca/` because a page directory becomes a URL and the site's URLs are lowercase. The mount's
168
+ * `dir` is the spelling on disk and its `urlPrefix` is the CANONICAL tag, exactly as the loader
169
+ * publishes it.
170
+ */
171
+ function localeMountFor(
172
+ path: string,
173
+ source: string,
174
+ name: string,
175
+ locales: readonly string[],
176
+ ): AssetMount | null {
177
+ const cut = source.indexOf(LOCALE_PLACEHOLDER);
178
+ const head = source.slice(0, cut);
179
+ const tail = source.slice(cut + LOCALE_PLACEHOLDER.length);
180
+ if (!path.startsWith(head)) {
181
+ return null;
182
+ }
183
+ const segment = path.slice(head.length).split("/")[0] ?? "";
184
+ if (segment === "" || !SAFE_TYPE_NAME.test(segment)) {
185
+ return null;
186
+ }
187
+ const canonical = locales.find((tag) => tag.toLowerCase() === segment.toLowerCase());
188
+ if (canonical === undefined || !SAFE_TYPE_NAME.test(canonical)) {
189
+ return null;
190
+ }
191
+ const dir = `${head}${segment}${tail}`.replace(/\/+$/, "");
192
+ return { dir, urlPrefix: `/${SECTION_KEY}/${name}/${canonical}` };
193
+ }
194
+
195
+ /** Strip `./` and any trailing slash from a configured `source`, the way collectionDirs does. */
196
+ function normalizeSource(source: string): string {
197
+ return source.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/+$/, "");
198
+ }
199
+
200
+ /**
201
+ * The URL the canvas should load for one authored reference, or null to leave it as written.
202
+ *
203
+ * Null is not a failure — it is most of the answers. An absolute URL, a `data:` URI, a `${…}`
204
+ * template that did not resolve to a project file, a `../` escape out of the collection: each one
205
+ * already means what it says, and changing it would be the bug.
206
+ *
207
+ * @param {string} value - The reference as the author wrote it
208
+ * @param {AssetContext | null} ctx - The host's asset context, or null for no resolution at all
209
+ * @returns {string | null} The URL to load, or null to leave `value` untouched
210
+ */
211
+ export function resolveAssetRef(value: string, ctx: AssetContext | null): string | null {
212
+ if (!ctx || value === "") {
213
+ return null;
214
+ }
215
+ if (ctx.space === "repo") {
216
+ if (!ctx.fileBaseUrl) {
217
+ return null;
218
+ }
219
+ const path = projectPathForRef(value, ctx.documentDir, ctx.mounts, ctx.lanes);
220
+ if (path === null) {
221
+ return null;
222
+ }
223
+ const { suffix } = splitRefSuffix(value);
224
+ return `${ctx.fileBaseUrl}${encodeProjectPath(path)}${suffix}`;
225
+ }
226
+ const [mount] = ctx.mounts;
227
+ return mount ? mountedRefFor(value, ctx.documentDir, mount) : null;
228
+ }
229
+
230
+ /**
231
+ * The mounted URL for one content-relative reference, or null to leave it as written. A ref is
232
+ * rewritten only when it is relative and lands inside the collection directory — anything else (an
233
+ * absolute URL, a project-root path, a bound template, a `../` escape out of the collection) keeps
234
+ * the meaning it already had.
235
+ */
236
+ export function mountedRefFor(value: string, entryDir: string, mount: AssetMount): string | null {
237
+ if (isNonRelativeRef(value)) {
238
+ return null;
239
+ }
240
+ const { path: bare, suffix } = splitRefSuffix(value);
241
+ let decoded: string;
242
+ try {
243
+ decoded = decodeURIComponent(bare);
244
+ } catch {
245
+ return null;
246
+ }
247
+ const target = joinProjectPath(entryDir, decoded);
248
+ if (target === null) {
249
+ return null;
250
+ }
251
+ const url = assetUrlFor([mount], target);
252
+ if (!url) {
253
+ return null; // Outside the collection — not the mount's to publish.
254
+ }
255
+ return `${url}${suffix}`;
256
+ }