@jxsuite/studio 3.0.0 → 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 (173) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
  3. package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
  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-6vvx96n2.js → convert-to-repeater-dhw1nrz9.js} +27 -19
  7. package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
  8. package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-3ydgjbdy.js} +32 -24
  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-8fr3545e.js → format-host-tgcmfs4p.js} +3 -3
  12. package/dist/chunks/fs-events-hrrg328g.js +21 -0
  13. package/dist/chunks/{git-panel-g01d93vc.js → git-panel-0yywyk76.js} +21 -16
  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-8ezdegcx.js → studio-1hbqnt19.js} +36 -32
  33. package/dist/chunks/{studio-r5xbtctf.js → studio-2zkz8evn.js} +446 -2451
  34. package/dist/chunks/{studio-pkp61mh8.js → studio-4pqqxbrt.js} +23 -15
  35. package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
  36. package/dist/chunks/{studio-3dkcd4c5.js → studio-5q69gfag.js} +60 -56
  37. package/dist/chunks/{studio-rh1cnamv.js → studio-5rywrkxz.js} +563 -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-xntfdy1x.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-ry1vz5sv.js → studio-9c4k4vhv.js} +15 -13
  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-2x9zbxmx.js → studio-ahjfdy6e.js} +9 -7
  47. package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
  48. package/dist/chunks/{studio-dc8s8heb.js → studio-cb12yz9w.js} +8528 -8780
  49. package/dist/chunks/studio-cft9r4r7.js +119 -0
  50. package/dist/chunks/{studio-hcxzyk3x.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-7p35254k.js → studio-ex8zyp98.js} +30 -26
  54. package/dist/chunks/studio-gv8vvshc.js +118 -0
  55. package/dist/chunks/studio-hbtp0eh1.js +1315 -0
  56. package/dist/chunks/{studio-a0yp7syn.js → studio-htsf0w6m.js} +29 -114
  57. package/dist/chunks/{studio-j595e748.js → studio-kjjsjak4.js} +139 -63
  58. package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
  59. package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
  60. package/dist/chunks/{studio-g5gdqnyd.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-p5dja96s.js → studio-p8j86wag.js} +2 -2
  66. package/dist/chunks/{studio-6z5eyjwn.js → studio-pdfjntbk.js} +66 -129
  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-k4jvjcfx.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-5efrtn1m.js → studio-wx5609xd.js} +21 -17
  81. package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
  82. package/dist/chunks/{studio-hj2se583.js → studio-yxrkyv8a.js} +41 -31
  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 +108 -24
  86. package/dist/studio.js +2362 -1695
  87. package/package.json +16 -15
  88. package/src/browse/library-pane.ts +57 -3
  89. package/src/canvas/canvas-render.ts +23 -1
  90. package/src/canvas/iframe-patch.ts +47 -21
  91. package/src/canvas/iframe-render.ts +22 -6
  92. package/src/commands/app-commands.ts +4 -0
  93. package/src/commands/command-args.ts +29 -0
  94. package/src/commands/context.ts +15 -1
  95. package/src/commands/defaults.ts +29 -1
  96. package/src/commands/levels.ts +16 -0
  97. package/src/commands/live-context.ts +3 -0
  98. package/src/content/collection-match.ts +267 -0
  99. package/src/content/entry-commands.ts +38 -29
  100. package/src/content/entry-fields.ts +6 -0
  101. package/src/editor/shortcuts.ts +5 -0
  102. package/src/files/file-ops.ts +10 -42
  103. package/src/files/files.ts +485 -59
  104. package/src/files/fs-events.ts +9 -0
  105. package/src/files/gitignore.ts +432 -0
  106. package/src/files/serialize-document.ts +60 -0
  107. package/src/format/convert-file.ts +511 -0
  108. package/src/format/format-choices.ts +199 -0
  109. package/src/grid/grid-view.ts +16 -2
  110. package/src/media/media-open.ts +58 -0
  111. package/src/media/media-pane.ts +310 -0
  112. package/src/new-project/import-tab.ts +332 -160
  113. package/src/new-project/new-project-modal.ts +27 -34
  114. package/src/packages/ensure-deps.ts +14 -7
  115. package/src/packages/jxsuite-update.ts +11 -11
  116. package/src/page-params.ts +10 -46
  117. package/src/panels/activity-bar.ts +45 -16
  118. package/src/panels/ai-chat/attached-context.ts +6 -1
  119. package/src/panels/ai-chat/chat-view.ts +255 -13
  120. package/src/panels/ai-chat/composer.ts +21 -97
  121. package/src/panels/ai-panel.ts +116 -2
  122. package/src/panels/navigator-panels.ts +7 -6
  123. package/src/panels/settings-menu.ts +799 -0
  124. package/src/panels/toolbar.ts +135 -15
  125. package/src/platforms/cloud.ts +20 -0
  126. package/src/platforms/devserver.ts +67 -33
  127. package/src/preview/preview-overlay.ts +226 -0
  128. package/src/services/ai-ask.ts +247 -0
  129. package/src/services/ai-import-tools.ts +494 -0
  130. package/src/services/ai-project-tools.ts +20 -26
  131. package/src/services/ai-system-prompt.ts +88 -2
  132. package/src/services/ai-turn-signal.ts +70 -0
  133. package/src/services/connection.ts +74 -0
  134. package/src/services/context-manager.ts +89 -0
  135. package/src/services/document-assistant.ts +45 -4
  136. package/src/services/import-client.ts +33 -6
  137. package/src/services/import-run.ts +158 -0
  138. package/src/services/import-seed.ts +120 -0
  139. package/src/services/jx-validate.ts +58 -6
  140. package/src/services/project-adoption.ts +71 -0
  141. package/src/services/references.ts +0 -0
  142. package/src/services/settings/definitions.ts +17 -0
  143. package/src/services/tool-executor.ts +34 -2
  144. package/src/settings/contexts-section.ts +45 -3
  145. package/src/settings/dependencies-editor.ts +43 -23
  146. package/src/settings/general-settings.ts +30 -12
  147. package/src/settings/preferences-dialog.ts +18 -41
  148. package/src/settings/preferences-sections.ts +57 -0
  149. package/src/settings/settings-document.ts +65 -9
  150. package/src/studio.ts +33 -6
  151. package/src/tabs/transact.ts +42 -19
  152. package/src/types.ts +103 -5
  153. package/src/ui/ai-model-picker.ts +162 -0
  154. package/src/ui/choice-field.ts +72 -0
  155. package/src/ui/layers.ts +83 -5
  156. package/src/utils/studio-utils.ts +22 -52
  157. package/styles/forced-colors.css +7 -0
  158. package/styles/overlays.css +256 -0
  159. package/styles/panels.css +13 -1
  160. package/styles/shell.css +111 -0
  161. package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
  162. package/dist/chunks/content-source-7fy9qrm0.js +0 -40
  163. package/dist/chunks/files-xcybanxe.js +0 -70
  164. package/dist/chunks/layers-panel-27essgkc.js +0 -60
  165. package/dist/chunks/publish-panel-09sy2bab.js +0 -22
  166. package/dist/chunks/quick-search-sefb61v8.js +0 -55
  167. package/dist/chunks/right-panel-9r32jd5j.js +0 -39
  168. package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
  169. package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
  170. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
  171. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
  172. package/dist/chunks/studio-c31yb4t6.js +0 -55
  173. package/src/canvas/site-style-css.ts +0 -117
@@ -30,6 +30,7 @@ import { installUrlOf } from "../platform-errors";
30
30
  import { hasAiCredentials } from "../services/ai-models";
31
31
  import { setPendingAgentPrompt } from "../services/agent-seed";
32
32
  import { initProjectRepo } from "../files/files";
33
+ import { runImportHandoff } from "../services/import-seed";
33
34
  import { createAiCredentialsForm } from "../ui/ai-credentials-form";
34
35
  import { createManagedConnect } from "../ui/ai-managed-connect";
35
36
  import {
@@ -39,14 +40,10 @@ import {
39
40
  resetLocationFields,
40
41
  } from "./location-fields";
41
42
  import {
42
- cancelImport,
43
+ handoffImport,
43
44
  importButtonLabel,
44
- isImportRunning,
45
- renderImportProgress,
46
45
  renderImportSource,
47
- renderImportStatus,
48
46
  resetImportTab,
49
- startImport,
50
47
  validateImportSource,
51
48
  } from "./import-tab";
52
49
  import type { ProjectConfig } from "@jxsuite/schema/types";
@@ -211,9 +208,6 @@ export function closeNewProjectModal() {
211
208
  if (!_handle || _creating) {
212
209
  return;
213
210
  }
214
- if (isImportRunning()) {
215
- cancelImport(importCtxFor(renderModal));
216
- }
217
211
  _handle.close();
218
212
  _handle = null;
219
213
  if (_resolve) {
@@ -223,7 +217,7 @@ export function closeNewProjectModal() {
223
217
  }
224
218
 
225
219
  /** Close the modal and resolve its promise with a created/imported project. */
226
- function finish(result: { root: string; config: ProjectConfig }) {
220
+ function finish(result: { root: string; config: ProjectConfig } | null) {
227
221
  _creating = false;
228
222
  if (_handle) {
229
223
  _handle.close();
@@ -236,8 +230,12 @@ function finish(result: { root: string; config: ProjectConfig }) {
236
230
  }
237
231
 
238
232
  /**
239
- * Every path out of the wizard that produced a project: initialise version control for it, then
240
- * hand it to the caller. A scaffold is not a repository, and Delete and Rename are one click away.
233
+ * Every path out of the wizard that produced a project HERE: initialise version control for it,
234
+ * then hand it to the caller. A scaffold is not a repository, and Delete and Rename are one click
235
+ * away.
236
+ *
237
+ * The Import path does not come through here any more — `import_site` creates the project, so the
238
+ * obligation moved with it into `services/project-adoption.ts`, which both bootstrap tools share.
241
239
  */
242
240
  async function finishCreated(result: { root: string; config: ProjectConfig }) {
243
241
  await initProjectRepo(result.root);
@@ -275,9 +273,13 @@ function importCtxFor(rerender: () => void): ImportTabCtx {
275
273
  credsForm: credsForm(),
276
274
  form: _form,
277
275
  managedConnect: managedConnect(),
278
- onDone: (result) => {
279
- // See above: the discard is deliberate, and `void` as a statement is what says so.
280
- void finishCreated(result);
276
+ /* The wizard creates nothing on this path. `import_site` is `no-project` tiered and does the
277
+ creating — including the git init every create path owes (`specs/desktop.md` §4.5), which
278
+ moved into `adoptCreatedProject` with it. The modal resolves `null`, the same as a dismissal,
279
+ because nothing was created HERE. */
280
+ onHandoff: (brief) => {
281
+ finish(null);
282
+ void runImportHandoff(brief);
281
283
  },
282
284
  rerender,
283
285
  resolveDestination: () => validateParams(),
@@ -338,7 +340,7 @@ function renderModal() {
338
340
  };
339
341
 
340
342
  const onTabChange = (e: Event) => {
341
- if (_creating || isImportRunning()) {
343
+ if (_creating) {
342
344
  return;
343
345
  }
344
346
  _tab = (e.target as HTMLElement & { selected: string }).selected as NewProjectTab;
@@ -363,7 +365,7 @@ function renderModal() {
363
365
  };
364
366
 
365
367
  const goBack = () => {
366
- if (_creating || isImportRunning()) {
368
+ if (_creating) {
367
369
  return;
368
370
  }
369
371
  _step = "source";
@@ -524,7 +526,12 @@ function renderModal() {
524
526
  })}
525
527
  ${
526
528
  _tab === "import"
527
- ? renderImportStatus()
529
+ ? html`
530
+ <div class="new-project-tab-intro">
531
+ The assistant runs the import and reports as it goes — this dialog closes as soon as
532
+ it starts.
533
+ </div>
534
+ `
528
535
  : html`
529
536
  <div class="new-project-tab-intro">
530
537
  The site's address, deployment target and design tokens are project settings — set
@@ -542,15 +549,6 @@ function renderModal() {
542
549
  Cancel
543
550
  </sp-button>
544
551
  `;
545
- if (isImportRunning()) {
546
- // One cancel, not two: the run's own Cancel is the step's Cancel, and dismissing the modal
547
- // (Escape, the underlay, the header ✕) aborts the run on the way out.
548
- return html`
549
- <sp-button variant="secondary" @click=${() => cancelImport(importCtx)}>
550
- Cancel Import
551
- </sp-button>
552
- `;
553
- }
554
552
  if (_step === "source") {
555
553
  const gated = (_tab === "import" || _tab === "agent") && !aiGateOpen();
556
554
  return html`
@@ -561,7 +559,7 @@ function renderModal() {
561
559
  const primary =
562
560
  _tab === "import"
563
561
  ? html`
564
- <sp-button variant="accent" @click=${() => startImport(importCtx)}>
562
+ <sp-button variant="accent" @click=${() => handoffImport(importCtx)}>
565
563
  ${importButtonLabel()}
566
564
  </sp-button>
567
565
  `
@@ -583,12 +581,7 @@ function renderModal() {
583
581
  `;
584
582
  };
585
583
 
586
- const bodyTpl = () => {
587
- if (isImportRunning()) {
588
- return renderImportProgress();
589
- }
590
- return _step === "source" ? sourceBodyTpl() : paramsBodyTpl();
591
- };
584
+ const bodyTpl = () => (_step === "source" ? sourceBodyTpl() : paramsBodyTpl());
592
585
 
593
586
  const tpl = html`
594
587
  <sp-underlay open @close=${closeNewProjectModal}></sp-underlay>
@@ -602,7 +595,7 @@ function renderModal() {
602
595
  </sp-action-button>
603
596
  </div>
604
597
  ${
605
- _step === "source" && !isImportRunning()
598
+ _step === "source"
606
599
  ? html`
607
600
  <div class="new-project-tabs">
608
601
  <sp-tabs selected=${_tab} quiet @change=${onTabChange}>
@@ -3,30 +3,36 @@
3
3
  * Ensure the active project's dependencies are installed before the editor loads. When the backend
4
4
  * reports node_modules is missing, run `bun install` behind a blocking progress modal so component
5
5
  * and format resolution see the real dependency tree. No-op on platforms without package support.
6
+ *
7
+ * It REPORTS whether an install actually ran, because "format resolution sees the real dependency
8
+ * tree" was only half true: project activation fetches the format registry before calling this, and
9
+ * `loadFormats` memoises its answer — so on a fresh clone the empty registry it got survived the
10
+ * whole session, and the New File picker offered nothing but JSON in a project whose `project.json`
11
+ * enables the markdown extension. The caller re-fetches on a true.
6
12
  */
7
13
 
8
14
  import { getPlatform } from "../platform";
9
15
  import { showProgressModal } from "../ui/progress-modal";
10
16
  import { shouldInstallAutomation } from "../services/automation";
11
17
 
12
- export async function ensureDependenciesInstalled(): Promise<void> {
18
+ export async function ensureDependenciesInstalled(): Promise<boolean> {
13
19
  // Automation/screenshot runs open projects read-only to drive the canvas — they must never mutate
14
20
  // The project on disk (a `bun install` would litter node_modules/bun.lock into starter templates).
15
21
  if (shouldInstallAutomation(location.search)) {
16
- return;
22
+ return false;
17
23
  }
18
24
  const platform = getPlatform();
19
25
  if (!platform.dependenciesNeedInstall || !platform.installDependencies) {
20
- return;
26
+ return false;
21
27
  }
22
28
  let needs = false;
23
29
  try {
24
30
  needs = await platform.dependenciesNeedInstall();
25
31
  } catch {
26
- return;
32
+ return false;
27
33
  }
28
34
  if (!needs) {
29
- return;
35
+ return false;
30
36
  }
31
37
  const progress = showProgressModal({
32
38
  status: "Running bun install…",
@@ -36,10 +42,11 @@ export async function ensureDependenciesInstalled(): Promise<void> {
36
42
  const result = await platform.installDependencies();
37
43
  if (result.ok) {
38
44
  progress.done();
39
- } else {
40
- progress.fail(result.log ?? "bun install failed");
45
+ return true;
41
46
  }
47
+ progress.fail(result.log ?? "bun install failed");
42
48
  } catch (error) {
43
49
  progress.fail(error instanceof Error ? error.message : String(error));
44
50
  }
51
+ return false;
45
52
  }
@@ -16,7 +16,7 @@
16
16
  * build` and the project's types — not the running app. There is no reason for them to match
17
17
  * Studio, and pinning them to it was the residue of an assumption that is no longer true.
18
18
  *
19
- * The registry lookup is `platform.outdatedPackages()`, the same seam the dependencies editor uses.
19
+ * The registry lookup is `platform.packageVersions()`, the same seam the dependencies editor uses.
20
20
  * A host that does not offer it (the cloud session, which manages dependencies server-side) simply
21
21
  * gets no prompt: without the registry there is no honest target, and guessing is what this
22
22
  * replaced.
@@ -50,21 +50,22 @@ export interface JxsuiteUpdate {
50
50
  */
51
51
  export async function checkJxsuiteUpdate(): Promise<JxsuiteUpdate[]> {
52
52
  const platform = getPlatform();
53
- if (!platform.outdatedPackages) {
53
+ if (!platform.packageVersions) {
54
54
  return [];
55
55
  }
56
56
  let reported;
57
57
  try {
58
- reported = await platform.outdatedPackages();
58
+ reported = await platform.packageVersions();
59
59
  } catch {
60
60
  return [];
61
61
  }
62
62
  const outdated: JxsuiteUpdate[] = [];
63
63
  for (const p of reported) {
64
64
  /*
65
- * `isUpgrade`, not merely "differs from latest". `outdatedPackages` reports any difference, and
66
- * a project deliberately pinned AHEAD of the registry — a prerelease, or a range bumped before
67
- * the publish landed — would otherwise be offered a downgrade described as an update.
65
+ * `isUpgrade`, not merely "differs from latest". `packageVersions` reports the registry's answer
66
+ * for every dependency, current ones included, and a project deliberately pinned AHEAD of the
67
+ * registry — a prerelease, or a range bumped before the publish landed — would otherwise be
68
+ * offered a downgrade described as an update.
68
69
  */
69
70
  if (p.name.startsWith(JXSUITE_PREFIX) && isUpgrade(p.current, p.latest)) {
70
71
  outdated.push({ current: p.current, dev: Boolean(p.dev), latest: p.latest, name: p.name });
@@ -146,11 +147,10 @@ export async function maybePromptJxsuiteUpdate(projectRoot: string): Promise<voi
146
147
  // That is not hypothetical: it put this dialog into the middle of 33 committed screenshots,
147
148
  // Including docs/images/hero.png, which is the jxsuite.com marketing hero.
148
149
  //
149
- // Correct pins are NOT sufficient on their own. `outdatedPackages` compares the range's base
150
- // Version against the registry's `latest` (packages/server/src/packages.ts: `latest ===
151
- // StripRange(p.version)`), not whether the range resolves it — so a project pinned `^1.4.1` is
152
- // "outdated" the moment 1.4.2 publishes, and every starter shot would be scrimmed again by the
153
- // Next patch release of any @jxsuite package.
150
+ // Correct pins are NOT sufficient on their own. This compares the range's BASE version against
151
+ // The registry's `latest`, not whether the range resolves it — so a project pinned `^1.4.1` is
152
+ // Behind the moment 1.4.2 publishes, and every starter shot would be scrimmed again by the next
153
+ // Patch release of any @jxsuite package.
154
154
  if (shouldInstallAutomation(location.search)) {
155
155
  return;
156
156
  }
@@ -10,6 +10,7 @@
10
10
  * matching built page.
11
11
  */
12
12
 
13
+ import { documentUrlPattern as routePattern } from "@jxsuite/site/routes";
13
14
  import { getPlatform } from "./platform";
14
15
  import { loadExtensions } from "./format/format-host";
15
16
  import type { JxMutableNode, JxPathsDef } from "@jxsuite/schema/types";
@@ -17,54 +18,17 @@ import type { JxMutableNode, JxPathsDef } from "@jxsuite/schema/types";
17
18
  /** Param name → candidate values, in `$paths` declaration order. */
18
19
  export type ParamValues = Record<string, string[]>;
19
20
 
20
- /** Bracket route segments: `[param]` (named) and `[...param]` (catch-all). */
21
- const PARAM_SEGMENT = /\[\.\.\.(\w+)\]|\[(\w+)\]/g;
22
-
23
21
  /**
24
- * Param names declared by a document path's bracket segments, e.g. `pages/products/[sku].json` →
25
- * `["sku"]`. Mirrors the compiler's `fileToRoute` parsing.
22
+ * Route derivation is `@jxsuite/site/routes`, re-exported so this module stays the studio's one
23
+ * import for everything `$params`.
26
24
  *
27
- * @param {string | null | undefined} documentPath
28
- * @returns {string[]}
25
+ * These two used to be written out here, under a docblock admitting they mirrored the compiler's
26
+ * `fileToRoute`. A mirror is a copy that nothing checks, and a route is the worst place to keep
27
+ * one: a disagreement is a page that renders on the canvas and 404s in a browser. The studio cannot
28
+ * import `@jxsuite/compiler` (its graph carries `sharp` and `esbuild`, and this bundles for a
29
+ * browser), so the rules moved down to the package both CAN import.
29
30
  */
30
- export function dynamicRouteParams(documentPath: string | null | undefined) {
31
- if (!documentPath) {
32
- return [];
33
- }
34
- const params: string[] = [];
35
- for (const match of documentPath.matchAll(PARAM_SEGMENT)) {
36
- params.push((match[1] ?? match[2])!);
37
- }
38
- return params;
39
- }
40
-
41
- /**
42
- * URL pattern for a page document path, mirroring the compiler's `fileToRoute`:
43
- * `pages/products/[sku].json` → `/products/:sku`, `pages/index.json` → `/`.
44
- *
45
- * @param {string | null | undefined} documentPath
46
- * @returns {string}
47
- */
48
- export function documentUrlPattern(documentPath: string | null | undefined) {
49
- if (!documentPath) {
50
- return "/";
51
- }
52
- let urlPath = documentPath
53
- .replace(/^\.\//, "")
54
- .replace(/^pages\//, "")
55
- .replace(/\.[^/.]+$/, "");
56
- if (urlPath.endsWith("/index")) {
57
- urlPath = urlPath.slice(0, -6) || "/";
58
- } else if (urlPath === "index") {
59
- urlPath = "/";
60
- }
61
- if (!urlPath.startsWith("/")) {
62
- urlPath = `/${urlPath}`;
63
- }
64
- return urlPath.replaceAll(PARAM_SEGMENT, (_match, spread: string, named: string) =>
65
- spread ? "*" : `:${named}`,
66
- );
67
- }
31
+ export { documentUrlPattern, dynamicRouteParams } from "@jxsuite/site/routes";
68
32
 
69
33
  /**
70
34
  * The `$paths` declaration of an open document. JSON pages carry it on the document root;
@@ -289,7 +253,7 @@ export function substitutePreviewParams(
289
253
  doc.state.$page = {
290
254
  params: { ...params },
291
255
  title: typeof doc.title === "string" ? doc.title : "",
292
- url: documentUrlPattern(documentPath),
256
+ url: routePattern(documentPath),
293
257
  };
294
258
  return doc;
295
259
  }
@@ -30,6 +30,12 @@ import type { PanelRecord } from "./panel-registry";
30
30
  import type { CommandContext } from "../commands/context";
31
31
  import type { EffectScope } from "@vue/reactivity";
32
32
  import { activeRegistry } from "../commands/active-registry";
33
+ import {
34
+ dismissSettingsMenu,
35
+ isSettingsMenuOpen,
36
+ openSettingsMenu,
37
+ SETTINGS_MENU_PLACEMENT,
38
+ } from "./settings-menu";
33
39
  import type { TemplateResult } from "lit-html";
34
40
 
35
41
  let _scope: EffectScope | null = null;
@@ -55,6 +61,9 @@ export function mount() {
55
61
  }
56
62
 
57
63
  export function unmount() {
64
+ // Before the scope stops: an unmounted rail must not leave the menu's document-level capture
65
+ // Keydown listener behind, answering for a gear that is no longer on screen.
66
+ dismissSettingsMenu();
58
67
  _scope?.stop();
59
68
  _scope = null;
60
69
  }
@@ -168,36 +177,56 @@ function railButton(panel: PanelRecord, ctx: CommandContext): TemplateResult {
168
177
  }
169
178
 
170
179
  /**
171
- * The rail foot: **Preferences**, and only Preferences (plan §3.2 ②).
180
+ * The rail foot: the ⚙ **Settings** menu.
172
181
  *
173
182
  * It was two hand-authored buttons calling two module functions directly — the exact shape §2
174
- * principle 1 bans, and the reason the gear opened PROJECT configuration from an application-level
175
- * slot. Preferences is the application's own settings and follows the author between projects, so
176
- * it is the one that earns the pinned position; project configuration is `settings.open`, which
177
- * lives in the ⬢ menu and the palette because it is scoped to a project; and About is `help.about`,
178
- * reachable by name.
183
+ * principle 1 bans — and then, for a release, one button that ran `app.preferences` and nothing
184
+ * else. That was the right shape for a pinned SLOT: a slot holds one thing, so it held the
185
+ * application's settings, and project configuration went to the ⬢ menu and the palette because a
186
+ * slot at application level could not honestly offer it.
179
187
  *
180
- * Rendered FROM the record, so the label and the chord in its tooltip cannot drift from ⌘,.
188
+ * **This reverses that, and the reason it is not a regression is that the gear is no longer a
189
+ * slot.** A menu may host two levels — `commandbar/overflow` has admitted application, project and
190
+ * document since the matrix was written — because a menu prints each row's own name, chord and gate
191
+ * beside it, so nothing about a row's level has to be inferred from where the control sits. The
192
+ * levels are still separated: `settings/menu` is a matrix row admitting exactly application and
193
+ * project, and the menu draws a divider where the level changes. Its one-click meaning is gone
194
+ * deliberately: ⌘, still opens Preferences from anywhere, and the menu's first row prints that
195
+ * chord.
196
+ *
197
+ * Rendered FROM the placement, so a record joins the gear by declaring `settings/menu` and there is
198
+ * nothing here to update in step. With no project open the two project rows hide themselves and the
199
+ * menu is Preferences alone — which is the correct answer on the welcome screen, and the reason the
200
+ * button's own visibility asks the placement rather than one id.
181
201
  */
182
202
  function railFooterTpl(): TemplateResult {
183
203
  const registry = activeRegistry();
184
- const command = registry?.get("app.preferences");
185
- if (!registry || !command || !registry.isVisible("app.preferences")) {
204
+ if (!registry || registry.forPlacement(SETTINGS_MENU_PLACEMENT).length === 0) {
186
205
  return html`${nothing}`;
187
206
  }
188
- const chord = registry.keymap.formatBinding("app.preferences");
207
+ const open = isSettingsMenuOpen();
189
208
  return html`
190
209
  <button
191
210
  type="button"
192
- class="rail-item"
193
- data-command="app.preferences"
194
- title=${chord ? `${command.title} (${chord})` : command.title}
195
- @click=${() => {
196
- void registry.run("app.preferences");
211
+ class="rail-item${open ? " rail-item--menu-open" : ""}"
212
+ data-rail-menu="settings"
213
+ aria-haspopup="menu"
214
+ aria-expanded=${open ? "true" : "false"}
215
+ title="Settings"
216
+ @click=${(event: MouseEvent) => {
217
+ openSettingsMenu(event.currentTarget as HTMLElement, { rerender: renderActivityBar });
218
+ }}
219
+ @keydown=${(event: KeyboardEvent) => {
220
+ // The menu-button convention: either arrow opens, and the menu takes the keyboard from
221
+ // There. Enter and Space are the button's own activation and already reach `@click`.
222
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
223
+ event.preventDefault();
224
+ openSettingsMenu(event.currentTarget as HTMLElement, { rerender: renderActivityBar });
225
+ }
197
226
  }}
198
227
  >
199
228
  <span class="rail-icon"><sp-icon-settings size="m"></sp-icon-settings></span>
200
- <span class="rail-label">Preferences</span>
229
+ <span class="rail-label">Settings</span>
201
230
  </button>
202
231
  `;
203
232
  }
@@ -15,7 +15,12 @@
15
15
  export const ATTACHED_CONTEXT_DELIMITER = "---- attached context ----";
16
16
 
17
17
  export interface ContextChip {
18
- kind: "page" | "selection";
18
+ /**
19
+ * `"import"` is not attachable from the composer's menu — it is stamped by the New Project Import
20
+ * hand-off, which needs the same channel for a different reason: the run's parameters must reach
21
+ * the model, and message content is the only thing the streaming payload carries.
22
+ */
23
+ kind: "page" | "selection" | "import";
19
24
  /** Short chip label, e.g. the page path or `<h1>`. */
20
25
  label: string;
21
26
  /** The context line embedded into the message content. */