@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.
- package/dist/studio.js +1229 -477
- package/dist/studio.js.map +41 -39
- package/package.json +6 -5
- package/src/browse/browse-modal.js +16 -13
- package/src/browse/browse.js +19 -16
- package/src/canvas/canvas-live-render.js +19 -0
- package/src/canvas/canvas-utils.js +13 -6
- package/src/editor/context-menu.js +27 -13
- package/src/editor/convert-targets.js +60 -0
- package/src/editor/convert-to-component.js +9 -10
- package/src/editor/convert-to-repeater.js +226 -0
- package/src/editor/inline-edit.js +3 -0
- package/src/editor/shortcuts.js +10 -2
- package/src/editor/slash-menu.js +36 -17
- package/src/files/files.js +244 -16
- package/src/github/github-publish.js +26 -15
- package/src/panels/activity-bar.js +5 -5
- package/src/panels/ai-panel.js +10 -3
- package/src/panels/block-action-bar.js +73 -24
- package/src/panels/git-panel.js +7 -2
- package/src/panels/imports-panel.js +6 -1
- package/src/panels/left-panel.js +20 -1
- package/src/panels/properties-panel.js +16 -11
- package/src/panels/quick-search.js +4 -4
- package/src/panels/right-panel.js +16 -14
- package/src/panels/signals-panel.js +120 -0
- package/src/panels/statusbar.js +6 -2
- package/src/panels/style-panel.js +6 -2
- package/src/panels/tab-strip.js +5 -2
- package/src/settings/content-types-editor.js +1 -1
- package/src/settings/settings-modal.js +12 -9
- package/src/store.js +15 -9
- package/src/studio.js +17 -9
- package/src/ui/layers.js +31 -1
- package/src/utils/edit-display.js +1 -6
- package/src/utils/studio-utils.js +11 -11
- package/src/workspace/workspace.js +19 -0
package/src/panels/tab-strip.js
CHANGED
|
@@ -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 =
|
|
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}
|
|
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
|
-
|
|
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(
|
|
104
|
+
renderGeneralSettings(_contentEl);
|
|
102
105
|
break;
|
|
103
106
|
case "head":
|
|
104
|
-
renderHeadEditor(
|
|
107
|
+
renderHeadEditor(_contentEl);
|
|
105
108
|
break;
|
|
106
109
|
case "cssVars":
|
|
107
|
-
renderCssVarsEditor(
|
|
110
|
+
renderCssVarsEditor(_contentEl);
|
|
108
111
|
break;
|
|
109
112
|
case "definitions":
|
|
110
|
-
renderDefsEditor(
|
|
113
|
+
renderDefsEditor(_contentEl);
|
|
111
114
|
break;
|
|
112
115
|
case "contentTypes":
|
|
113
|
-
renderContentTypesEditor(
|
|
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
|
-
// ───
|
|
29
|
+
// ─── Shell element refs (populated by initShellRefs) ─────────────────────────
|
|
30
30
|
|
|
31
|
-
export
|
|
32
|
-
export
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
458
|
+
const _urlParams = new URLSearchParams(location.search);
|
|
459
|
+
const _projectParam = _urlParams.get("project") || _urlParams.get("open");
|
|
454
460
|
|
|
455
|
-
if (
|
|
456
|
-
// ?
|
|
461
|
+
if (_projectParam) {
|
|
462
|
+
// ?project= mode: skip normal loadProject, set up site context from the path
|
|
457
463
|
const isAbsPath =
|
|
458
|
-
|
|
464
|
+
_projectParam.startsWith("/") ||
|
|
465
|
+
_projectParam.startsWith("~") ||
|
|
466
|
+
/^[A-Za-z]:[/\\]/.test(_projectParam);
|
|
459
467
|
if (!isAbsPath) {
|
|
460
|
-
statusMessage(`Error: ?
|
|
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(
|
|
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 =
|
|
520
|
-
let fileRelPath = _fileParam || siteCtx.fileRelPath ||
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
}
|