@jxsuite/studio 0.20.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/studio.js +1229 -477
  2. package/dist/studio.js.map +41 -39
  3. package/package.json +6 -5
  4. package/src/browse/browse-modal.js +16 -13
  5. package/src/browse/browse.js +19 -16
  6. package/src/canvas/canvas-live-render.js +19 -0
  7. package/src/canvas/canvas-utils.js +13 -6
  8. package/src/editor/context-menu.js +27 -13
  9. package/src/editor/convert-targets.js +60 -0
  10. package/src/editor/convert-to-component.js +9 -10
  11. package/src/editor/convert-to-repeater.js +226 -0
  12. package/src/editor/inline-edit.js +3 -0
  13. package/src/editor/shortcuts.js +10 -2
  14. package/src/editor/slash-menu.js +36 -17
  15. package/src/files/files.js +244 -16
  16. package/src/github/github-publish.js +26 -15
  17. package/src/panels/activity-bar.js +5 -5
  18. package/src/panels/ai-panel.js +10 -3
  19. package/src/panels/block-action-bar.js +73 -24
  20. package/src/panels/git-panel.js +7 -2
  21. package/src/panels/imports-panel.js +6 -1
  22. package/src/panels/left-panel.js +20 -1
  23. package/src/panels/properties-panel.js +16 -11
  24. package/src/panels/quick-search.js +4 -4
  25. package/src/panels/right-panel.js +16 -14
  26. package/src/panels/signals-panel.js +120 -0
  27. package/src/panels/statusbar.js +6 -2
  28. package/src/panels/style-panel.js +6 -2
  29. package/src/panels/tab-strip.js +5 -2
  30. package/src/settings/content-types-editor.js +1 -1
  31. package/src/settings/settings-modal.js +12 -9
  32. package/src/store.js +15 -9
  33. package/src/studio.js +17 -9
  34. package/src/ui/layers.js +31 -1
  35. package/src/utils/edit-display.js +1 -6
  36. package/src/utils/studio-utils.js +11 -11
  37. package/src/workspace/workspace.js +19 -0
@@ -10,6 +10,7 @@ import { classMap } from "lit-html/directives/class-map.js";
10
10
  import { repeat } from "lit-html/directives/repeat.js";
11
11
  import { effect, effectScope } from "../reactivity.js";
12
12
  import { workspace, activateTab, closeTab } from "../workspace/workspace.js";
13
+ import { showConfirmDialog } from "../ui/layers.js";
13
14
 
14
15
  /** @typedef {import("../tabs/tab.js").Tab} Tab */
15
16
 
@@ -117,12 +118,14 @@ function tabLabel(tab) {
117
118
  *
118
119
  * @param {string} id
119
120
  */
120
- function requestClose(id) {
121
+ async function requestClose(id) {
121
122
  const tab = workspace.tabs.get(id);
122
123
  if (!tab) return;
123
124
  if (tab.doc.dirty) {
124
- const confirmed = window.confirm(
125
+ const confirmed = await showConfirmDialog(
126
+ "Unsaved Changes",
125
127
  `"${tabLabel(tab)}" has unsaved changes. Close without saving?`,
128
+ { confirmLabel: "Close", destructive: true },
126
129
  );
127
130
  if (!confirmed) return;
128
131
  }
@@ -57,7 +57,7 @@ function handleNewContentType(rerender) {
57
57
  if (config.contentTypes[slug]) return; // already exists
58
58
 
59
59
  config.contentTypes[slug] = {
60
- source: `./content/${slug}/**/*.md`,
60
+ source: `./content/${slug}/`,
61
61
  schema: { type: "object", properties: {}, required: [] },
62
62
  };
63
63
 
@@ -20,6 +20,9 @@ let _handle = null;
20
20
  /** @type {string} */
21
21
  let _activeSection = "general";
22
22
 
23
+ /** @type {HTMLElement | null} */
24
+ let _contentEl = null;
25
+
23
26
  const sections = [
24
27
  { key: "general", label: "General", icon: "sp-icon-properties" },
25
28
  { key: "head", label: "Head", icon: "sp-icon-file-single-web-page" },
@@ -38,6 +41,7 @@ export function closeSettingsModal() {
38
41
  if (!_handle) return;
39
42
  _handle.close();
40
43
  _handle = null;
44
+ _contentEl = null;
41
45
  }
42
46
 
43
47
  function renderModal() {
@@ -77,7 +81,8 @@ function renderModal() {
77
81
  <div
78
82
  class="settings-modal-content"
79
83
  ${ref((el) => {
80
- if (el) requestAnimationFrame(() => renderActiveSection());
84
+ _contentEl = /** @type {HTMLElement | null} */ (el || null);
85
+ if (_contentEl) requestAnimationFrame(() => renderActiveSection());
81
86
  })}
82
87
  ></div>
83
88
  </div>
@@ -92,25 +97,23 @@ function renderModal() {
92
97
  }
93
98
 
94
99
  function renderActiveSection() {
95
- if (!_handle) return;
96
- const container = _handle.host.querySelector(".settings-modal-content");
97
- if (!container) return;
100
+ if (!_handle || !_contentEl) return;
98
101
 
99
102
  switch (_activeSection) {
100
103
  case "general":
101
- renderGeneralSettings(/** @type {HTMLElement} */ (container));
104
+ renderGeneralSettings(_contentEl);
102
105
  break;
103
106
  case "head":
104
- renderHeadEditor(/** @type {HTMLElement} */ (container));
107
+ renderHeadEditor(_contentEl);
105
108
  break;
106
109
  case "cssVars":
107
- renderCssVarsEditor(/** @type {HTMLElement} */ (container));
110
+ renderCssVarsEditor(_contentEl);
108
111
  break;
109
112
  case "definitions":
110
- renderDefsEditor(/** @type {HTMLElement} */ (container));
113
+ renderDefsEditor(_contentEl);
111
114
  break;
112
115
  case "contentTypes":
113
- renderContentTypesEditor(/** @type {HTMLElement} */ (container));
116
+ renderContentTypesEditor(_contentEl);
114
117
  break;
115
118
  }
116
119
  }
package/src/store.js CHANGED
@@ -26,17 +26,23 @@ export {
26
26
  updateFrontmatter,
27
27
  } from "./state.js";
28
28
 
29
- // ─── DOM shortcuts & element refs ────────────────────────────────────────────
29
+ // ─── Shell element refs (populated by initShellRefs) ─────────────────────────
30
30
 
31
- export const $ = (/** @type {string} */ sel) => document.querySelector(sel);
32
- export const _$$ = (/** @type {string} */ sel) => document.querySelectorAll(sel);
31
+ export let canvasWrap = /** @type {HTMLElement} */ (/** @type {unknown} */ (null));
32
+ export let activityBar = /** @type {HTMLElement} */ (/** @type {unknown} */ (null));
33
+ export let leftPanel = /** @type {HTMLElement} */ (/** @type {unknown} */ (null));
34
+ export let rightPanel = /** @type {HTMLElement} */ (/** @type {unknown} */ (null));
35
+ export let toolbarEl = /** @type {HTMLElement} */ (/** @type {unknown} */ (null));
36
+ export let statusbarEl = /** @type {HTMLElement} */ (/** @type {unknown} */ (null));
33
37
 
34
- export const canvasWrap = /** @type {HTMLElement} */ (document.querySelector("#canvas-wrap"));
35
- export const activityBar = /** @type {HTMLElement} */ (document.querySelector("#activity-bar"));
36
- export const leftPanel = /** @type {HTMLElement} */ (document.querySelector("#left-panel"));
37
- export const rightPanel = /** @type {HTMLElement} */ (document.querySelector("#right-panel"));
38
- export const toolbarEl = /** @type {HTMLElement} */ (document.querySelector("#toolbar"));
39
- export const statusbarEl = /** @type {HTMLElement} */ (document.querySelector("#statusbar"));
38
+ export function initShellRefs() {
39
+ canvasWrap = /** @type {HTMLElement} */ (document.querySelector("#canvas-wrap"));
40
+ activityBar = /** @type {HTMLElement} */ (document.querySelector("#activity-bar"));
41
+ leftPanel = /** @type {HTMLElement} */ (document.querySelector("#left-panel"));
42
+ rightPanel = /** @type {HTMLElement} */ (document.querySelector("#right-panel"));
43
+ toolbarEl = /** @type {HTMLElement} */ (document.querySelector("#toolbar"));
44
+ statusbarEl = /** @type {HTMLElement} */ (document.querySelector("#statusbar"));
45
+ }
40
46
 
41
47
  // ─── Shared containers (mutated in place by owner modules) ───────────────────
42
48
 
package/src/studio.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  setProjectState,
18
18
  requireProjectState,
19
19
  updateUi,
20
+ initShellRefs,
20
21
  } from "./store.js";
21
22
 
22
23
  import { activeTab, openTab, closeAllTabs } from "./workspace/workspace.js";
@@ -46,6 +47,7 @@ import {
46
47
  openFileInTab,
47
48
  openHomePage,
48
49
  setupTreeKeyboard,
50
+ registerFileTreeDnD,
49
51
  loadDirectory,
50
52
  } from "./files/files.js";
51
53
  import { renderImportsTemplate } from "./panels/imports-panel.js";
@@ -286,6 +288,8 @@ mountResizeEdges();
286
288
 
287
289
  // ─── Render loop ──────────────────────────────────────────────────────────────
288
290
 
291
+ initShellRefs();
292
+
289
293
  // Mount extracted panel modules
290
294
  toolbarPanel.mount(toolbarEl, {
291
295
  navigateBack: () => navigateBack(),
@@ -418,6 +422,7 @@ leftPanelMod.mount({
418
422
  registerElementsDnD,
419
423
  registerComponentsDnD,
420
424
  setupTreeKeyboard,
425
+ registerFileTreeDnD,
421
426
  cloneRepository: () => cloneRepository({ openRecentProject }),
422
427
  setGitDiffState: (
423
428
  /** @type {import("./canvas/canvas-render.js").GitDiffState | null} */ state,
@@ -450,14 +455,17 @@ function safeRenderRightPanel() {
450
455
  // Now that renderers are registered, bootstrap
451
456
  registerFunctionCompletions();
452
457
 
453
- const _openParam = new URLSearchParams(location.search).get("open");
458
+ const _urlParams = new URLSearchParams(location.search);
459
+ const _projectParam = _urlParams.get("project") || _urlParams.get("open");
454
460
 
455
- if (_openParam) {
456
- // ?open= mode: skip normal loadProject, set up site context from the path
461
+ if (_projectParam) {
462
+ // ?project= mode: skip normal loadProject, set up site context from the path
457
463
  const isAbsPath =
458
- _openParam.startsWith("/") || _openParam.startsWith("~") || /^[A-Za-z]:[/\\]/.test(_openParam);
464
+ _projectParam.startsWith("/") ||
465
+ _projectParam.startsWith("~") ||
466
+ /^[A-Za-z]:[/\\]/.test(_projectParam);
459
467
  if (!isAbsPath) {
460
- statusMessage(`Error: ?open= requires an absolute path (got "${_openParam}")`);
468
+ statusMessage(`Error: ?project= requires an absolute path (got "${_projectParam}")`);
461
469
  render();
462
470
  } else {
463
471
  render();
@@ -465,7 +473,7 @@ if (_openParam) {
465
473
  (async () => {
466
474
  try {
467
475
  const siteCtx = platform.resolveSiteContext
468
- ? await platform.resolveSiteContext(_openParam)
476
+ ? await platform.resolveSiteContext(_projectParam)
469
477
  : { sitePath: null };
470
478
 
471
479
  if (siteCtx.sitePath) {
@@ -516,8 +524,8 @@ if (_openParam) {
516
524
  }
517
525
 
518
526
  // Read and open the file
519
- const _fileParam = new URLSearchParams(location.search).get("file");
520
- let fileRelPath = _fileParam || siteCtx.fileRelPath || _openParam;
527
+ const _fileParam = _urlParams.get("file");
528
+ let fileRelPath = _fileParam || siteCtx.fileRelPath || _projectParam;
521
529
 
522
530
  // When opening project.json, default to home page instead
523
531
  if (fileRelPath === "project.json" || fileRelPath.endsWith("/project.json")) {
@@ -560,7 +568,7 @@ if (_openParam) {
560
568
  }
561
569
 
562
570
  render();
563
- statusMessage(`Opened ${_openParam}`);
571
+ statusMessage(`Opened ${fileRelPath}`);
564
572
  }
565
573
  } catch (/** @type {unknown} */ e) {
566
574
  statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
package/src/ui/layers.js CHANGED
@@ -1,4 +1,4 @@
1
- import { render as litRender, nothing } from "lit-html";
1
+ import { render as litRender, nothing, html } from "lit-html";
2
2
 
3
3
  /** @type {HTMLElement} */
4
4
  let _popoverLayer;
@@ -37,6 +37,36 @@ export function showDialog(templateFn) {
37
37
  });
38
38
  }
39
39
 
40
+ /**
41
+ * Show a confirm/cancel dialog. Returns true if confirmed, false otherwise.
42
+ *
43
+ * @param {string} headline
44
+ * @param {string | import("lit-html").TemplateResult} message
45
+ * @param {{ confirmLabel?: string; cancelLabel?: string; destructive?: boolean }} [opts]
46
+ * @returns {Promise<boolean>}
47
+ */
48
+ export function showConfirmDialog(headline, message, opts = {}) {
49
+ const { confirmLabel = "Confirm", cancelLabel = "Cancel", destructive = false } = opts;
50
+ return showDialog(
51
+ (done) => html`
52
+ <sp-dialog-wrapper
53
+ open
54
+ underlay
55
+ headline=${headline}
56
+ confirm-label=${confirmLabel}
57
+ cancel-label=${cancelLabel}
58
+ size="s"
59
+ @confirm=${() => done(true)}
60
+ @cancel=${() => done(false)}
61
+ @close=${() => done(false)}
62
+ class=${destructive ? "dialog-destructive" : ""}
63
+ >
64
+ <p>${message}</p>
65
+ </sp-dialog-wrapper>
66
+ `,
67
+ );
68
+ }
69
+
40
70
  /**
41
71
  * Open a persistent modal. Returns a handle with update() and close() methods.
42
72
  *
@@ -65,12 +65,7 @@ export function prepareForEditMode(node) {
65
65
  {
66
66
  tagName: "div",
67
67
  className: "repeater-perimeter",
68
- state: {
69
- $map: { item: {}, index: 0 },
70
- "$map/item": {},
71
- "$map/index": 0,
72
- },
73
- children: [prepareForEditMode(template)],
68
+ children: [prepareForEditMode(/** @type {JxMutableNode} */ (template))],
74
69
  },
75
70
  ];
76
71
  } else {
@@ -136,17 +136,17 @@ export function findContentTypeSchema(documentPath, projectConfig) {
136
136
  /** @type {Record<string, ContentTypeDef>} */ (projectConfig.contentTypes),
137
137
  )) {
138
138
  if (!def.source || !def.schema) continue;
139
- const src = def.source.replace(/^\.\//, "");
140
- const dir = src.split("/")[0];
141
- const ext = src.includes("*.md")
142
- ? ".md"
143
- : src.includes("*.json")
144
- ? ".json"
145
- : src.includes("*.csv")
146
- ? ".csv"
147
- : null;
148
- if (documentPath.startsWith(dir + "/") && (!ext || documentPath.endsWith(ext))) {
149
- return { name, schema: def.schema };
139
+ const src = def.source.replace(/^\.\//, "").replace(/\/$/, "");
140
+ const hasExt = src.includes(".") && !src.endsWith("/");
141
+ if (hasExt) {
142
+ if (documentPath === src || documentPath.endsWith("/" + src)) {
143
+ return { name, schema: def.schema };
144
+ }
145
+ } else {
146
+ const ext = `.${def.format || "md"}`;
147
+ if (documentPath.startsWith(src + "/") && documentPath.endsWith(ext)) {
148
+ return { name, schema: def.schema };
149
+ }
150
150
  }
151
151
  }
152
152
  return null;
@@ -139,3 +139,22 @@ export function replaceAllTabs(newTabOpts) {
139
139
  export function activateTab(tabId) {
140
140
  if (workspace.tabs.has(tabId)) workspace.activeTabId = tabId;
141
141
  }
142
+
143
+ /**
144
+ * Re-key a tab after its backing file has been renamed. Preserves all tab state including unsaved
145
+ * changes.
146
+ *
147
+ * @param {string} oldId
148
+ * @param {string} newId
149
+ * @param {string} newDocumentPath
150
+ */
151
+ export function renameTab(oldId, newId, newDocumentPath) {
152
+ const tab = workspace.tabs.get(oldId);
153
+ if (!tab) return;
154
+ tab.id = newId;
155
+ tab.documentPath = newDocumentPath;
156
+ workspace.tabs.delete(oldId);
157
+ workspace.tabs.set(newId, tab);
158
+ workspace.tabOrder = workspace.tabOrder.map((id) => (id === oldId ? newId : id));
159
+ if (workspace.activeTabId === oldId) workspace.activeTabId = newId;
160
+ }