@jxsuite/studio 0.0.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.css +3676 -0
- package/dist/studio.js +188743 -0
- package/dist/studio.js.map +1448 -0
- package/package.json +67 -0
- package/src/editor/context-menu.js +144 -0
- package/src/editor/inline-edit.js +597 -0
- package/src/editor/inline-format.js +572 -0
- package/src/editor/shortcuts.js +275 -0
- package/src/editor/slash-menu.js +167 -0
- package/src/files/components.js +40 -0
- package/src/files/file-ops.js +195 -0
- package/src/files/files.js +569 -0
- package/src/markdown/md-allowlist.js +101 -0
- package/src/markdown/md-convert.js +491 -0
- package/src/panels/activity-bar.js +69 -0
- package/src/panels/data-explorer.js +181 -0
- package/src/panels/events-panel.js +235 -0
- package/src/panels/imports-panel.js +427 -0
- package/src/panels/signals-panel.js +1093 -0
- package/src/panels/statusbar.js +56 -0
- package/src/platform.js +31 -0
- package/src/platforms/devserver.js +293 -0
- package/src/services/cem-export.js +130 -0
- package/src/services/code-services.js +98 -0
- package/src/site-context.js +122 -0
- package/src/state.js +744 -0
- package/src/store.js +332 -0
- package/src/studio.js +7692 -0
- package/src/ui/icons.js +83 -0
- package/src/ui/jx-styled-combobox.js +142 -0
- package/src/ui/spectrum.js +238 -0
- package/src/utils/studio-utils.js +185 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imports panel — context-aware import manager with cherry-pick component selection.
|
|
3
|
+
*
|
|
4
|
+
* When editing project.json: shows Class Imports, Dependencies (add/remove packages), and
|
|
5
|
+
* per-package component toggles for cherry-picking individual elements. When editing a
|
|
6
|
+
* page/layout/component/collection: shows Component Imports ($ref picker) and per-package component
|
|
7
|
+
* toggles.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { html, nothing } from "lit-html";
|
|
11
|
+
import { componentRegistry, computeRelativePath } from "../files/components.js";
|
|
12
|
+
import { projectState } from "../store.js";
|
|
13
|
+
import { updateSiteConfig } from "../site-context.js";
|
|
14
|
+
import { getPlatform } from "../platform.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the subpath specifier for a component: `<package>/<modulePath>`
|
|
18
|
+
*
|
|
19
|
+
* @param {any} comp
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function componentSpecifier(comp) {
|
|
23
|
+
return `${comp.package}/${comp.modulePath}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if a component is enabled (present in $elements array). Supports both cherry-picked subpath
|
|
28
|
+
* specifiers and legacy full-package imports.
|
|
29
|
+
*
|
|
30
|
+
* @param {any} comp
|
|
31
|
+
* @param {any[]} elements
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function isComponentEnabled(comp, elements) {
|
|
35
|
+
if (!elements?.length) return false;
|
|
36
|
+
const specifier = componentSpecifier(comp);
|
|
37
|
+
for (const entry of elements) {
|
|
38
|
+
if (typeof entry !== "string") continue;
|
|
39
|
+
// Cherry-picked subpath match
|
|
40
|
+
if (entry === specifier) return true;
|
|
41
|
+
// Legacy full-package match
|
|
42
|
+
if (entry === comp.package) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Group npm components by package name.
|
|
49
|
+
*
|
|
50
|
+
* @returns {Map<string, any[]>}
|
|
51
|
+
*/
|
|
52
|
+
function groupByPackage() {
|
|
53
|
+
/** @type {Map<string, any[]>} */
|
|
54
|
+
const groups = new Map();
|
|
55
|
+
for (const comp of componentRegistry) {
|
|
56
|
+
if (comp.source !== "npm" || !comp.package || !comp.modulePath) continue;
|
|
57
|
+
if (!groups.has(comp.package)) groups.set(comp.package, []);
|
|
58
|
+
groups.get(comp.package)?.push(comp);
|
|
59
|
+
}
|
|
60
|
+
return groups;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {{
|
|
65
|
+
* renderLeftPanel: () => void;
|
|
66
|
+
* documentPath: string | null;
|
|
67
|
+
* documentElements: any[];
|
|
68
|
+
* applyMutation: (fn: (doc: any) => void) => void;
|
|
69
|
+
* }} ctx
|
|
70
|
+
* @returns {any}
|
|
71
|
+
*/
|
|
72
|
+
export function renderImportsTemplate({
|
|
73
|
+
renderLeftPanel,
|
|
74
|
+
documentPath,
|
|
75
|
+
documentElements,
|
|
76
|
+
applyMutation,
|
|
77
|
+
}) {
|
|
78
|
+
const isSiteLevel = documentPath?.endsWith("project.json");
|
|
79
|
+
|
|
80
|
+
if (isSiteLevel) {
|
|
81
|
+
return renderSiteLevelImports(renderLeftPanel);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return renderDocumentLevelImports({
|
|
85
|
+
renderLeftPanel,
|
|
86
|
+
documentPath,
|
|
87
|
+
documentElements,
|
|
88
|
+
applyMutation,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Site-level: Class Imports + Dependencies + Component Cherry-pick ─────────
|
|
93
|
+
|
|
94
|
+
/** @param {() => void} renderLeftPanel */
|
|
95
|
+
function renderSiteLevelImports(renderLeftPanel) {
|
|
96
|
+
const siteImports = projectState?.projectConfig?.imports || {};
|
|
97
|
+
const entries = Object.entries(siteImports);
|
|
98
|
+
const siteElements = projectState?.projectConfig?.$elements || [];
|
|
99
|
+
|
|
100
|
+
const packageGroups = groupByPackage();
|
|
101
|
+
|
|
102
|
+
return html`
|
|
103
|
+
<div class="imports-panel">
|
|
104
|
+
<!-- Class Imports -->
|
|
105
|
+
<div class="imports-section">
|
|
106
|
+
<div class="imports-section-header">
|
|
107
|
+
<span class="imports-section-title">Class Imports</span>
|
|
108
|
+
<span class="imports-count">${entries.length}</span>
|
|
109
|
+
</div>
|
|
110
|
+
${entries.length > 0
|
|
111
|
+
? html`
|
|
112
|
+
<div class="imports-list">
|
|
113
|
+
${entries.map(
|
|
114
|
+
([name, path]) => html`
|
|
115
|
+
<div class="import-row">
|
|
116
|
+
<span class="import-name" title=${/** @type {string} */ (path)}>${name}</span>
|
|
117
|
+
<span class="import-path">${path}</span>
|
|
118
|
+
<sp-action-button
|
|
119
|
+
quiet
|
|
120
|
+
size="xs"
|
|
121
|
+
title="Remove"
|
|
122
|
+
@click=${async () => {
|
|
123
|
+
const updated = { ...siteImports };
|
|
124
|
+
delete updated[name];
|
|
125
|
+
await updateSiteConfig({ imports: updated });
|
|
126
|
+
renderLeftPanel();
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<sp-icon-close slot="icon" size="xs"></sp-icon-close>
|
|
130
|
+
</sp-action-button>
|
|
131
|
+
</div>
|
|
132
|
+
`,
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
`
|
|
136
|
+
: html`<div class="imports-empty">No class imports</div>`}
|
|
137
|
+
<div class="import-add-form">
|
|
138
|
+
<sp-textfield placeholder="Name" size="s" class="import-add-name"></sp-textfield>
|
|
139
|
+
<sp-textfield placeholder="Path" size="s" class="import-add-path"></sp-textfield>
|
|
140
|
+
<sp-action-button
|
|
141
|
+
quiet
|
|
142
|
+
size="xs"
|
|
143
|
+
title="Add import"
|
|
144
|
+
@click=${async (/** @type {any} */ e) => {
|
|
145
|
+
const form = e.target.closest(".import-add-form");
|
|
146
|
+
const nameField = form?.querySelector(".import-add-name");
|
|
147
|
+
const pathField = form?.querySelector(".import-add-path");
|
|
148
|
+
const name = nameField?.value?.trim();
|
|
149
|
+
const path = pathField?.value?.trim();
|
|
150
|
+
if (!name || !path) return;
|
|
151
|
+
nameField.value = "";
|
|
152
|
+
pathField.value = "";
|
|
153
|
+
const updated = { ...siteImports, [name]: path };
|
|
154
|
+
await updateSiteConfig({ imports: updated });
|
|
155
|
+
renderLeftPanel();
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
<sp-icon-add slot="icon" size="xs"></sp-icon-add>
|
|
159
|
+
</sp-action-button>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- npm Dependencies with per-component toggles -->
|
|
164
|
+
${[...packageGroups.entries()].map(
|
|
165
|
+
([pkg, comps]) => html`
|
|
166
|
+
<div class="imports-section">
|
|
167
|
+
<div class="imports-section-header">
|
|
168
|
+
<span class="imports-section-title import-mono">${pkg}</span>
|
|
169
|
+
<sp-action-button
|
|
170
|
+
quiet
|
|
171
|
+
size="xs"
|
|
172
|
+
title="Remove package"
|
|
173
|
+
@click=${async () => {
|
|
174
|
+
if (!confirm("Remove " + pkg + "?")) return;
|
|
175
|
+
try {
|
|
176
|
+
const platform = getPlatform();
|
|
177
|
+
await platform.removePackage(pkg);
|
|
178
|
+
// Also remove all cherry-picked elements for this package
|
|
179
|
+
const updatedElements = siteElements.filter(
|
|
180
|
+
(/** @type {any} */ e) => typeof e !== "string" || !e.startsWith(pkg + "/"),
|
|
181
|
+
);
|
|
182
|
+
const { loadComponentRegistry } = await import("../files/components.js");
|
|
183
|
+
await loadComponentRegistry();
|
|
184
|
+
await updateSiteConfig({ $elements: updatedElements });
|
|
185
|
+
renderLeftPanel();
|
|
186
|
+
} catch (/** @type {any} */ e) {
|
|
187
|
+
console.error("Failed to remove package:", e);
|
|
188
|
+
}
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
<sp-icon-close slot="icon" size="xs"></sp-icon-close>
|
|
192
|
+
</sp-action-button>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="imports-list imports-component-list">
|
|
195
|
+
${comps.map((/** @type {any} */ comp) => {
|
|
196
|
+
const enabled = isComponentEnabled(comp, siteElements);
|
|
197
|
+
const specifier = componentSpecifier(comp);
|
|
198
|
+
return html`
|
|
199
|
+
<div class="import-row import-component-row">
|
|
200
|
+
<sp-checkbox
|
|
201
|
+
size="s"
|
|
202
|
+
.checked=${enabled}
|
|
203
|
+
@change=${async (/** @type {any} */ e) => {
|
|
204
|
+
let updated = [...siteElements];
|
|
205
|
+
// Remove legacy full-package import if present
|
|
206
|
+
updated = updated.filter((/** @type {any} */ el) => el !== pkg);
|
|
207
|
+
if (e.target.checked) {
|
|
208
|
+
if (!updated.includes(specifier)) updated.push(specifier);
|
|
209
|
+
} else {
|
|
210
|
+
updated = updated.filter((/** @type {any} */ el) => el !== specifier);
|
|
211
|
+
}
|
|
212
|
+
await updateSiteConfig({ $elements: updated });
|
|
213
|
+
renderLeftPanel();
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<span class="import-component-label"><${comp.tagName}></span>
|
|
217
|
+
</sp-checkbox>
|
|
218
|
+
</div>
|
|
219
|
+
`;
|
|
220
|
+
})}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
`,
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
<!-- Add package -->
|
|
227
|
+
<div class="imports-section">
|
|
228
|
+
<div class="imports-section-header">
|
|
229
|
+
<span class="imports-section-title">Add Dependency</span>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="import-add-form">
|
|
232
|
+
<sp-textfield
|
|
233
|
+
placeholder="Package name…"
|
|
234
|
+
size="s"
|
|
235
|
+
style="flex:1"
|
|
236
|
+
@keydown=${async (/** @type {any} */ e) => {
|
|
237
|
+
if (e.key !== "Enter") return;
|
|
238
|
+
const name = e.target.value?.trim();
|
|
239
|
+
if (!name) return;
|
|
240
|
+
e.target.value = "";
|
|
241
|
+
try {
|
|
242
|
+
const platform = getPlatform();
|
|
243
|
+
await platform.addPackage(name);
|
|
244
|
+
const { loadComponentRegistry } = await import("../files/components.js");
|
|
245
|
+
await loadComponentRegistry();
|
|
246
|
+
renderLeftPanel();
|
|
247
|
+
} catch (/** @type {any} */ err) {
|
|
248
|
+
console.error("Failed to add package:", err);
|
|
249
|
+
}
|
|
250
|
+
}}
|
|
251
|
+
></sp-textfield>
|
|
252
|
+
<sp-action-button
|
|
253
|
+
quiet
|
|
254
|
+
size="xs"
|
|
255
|
+
title="Add package"
|
|
256
|
+
@click=${async (/** @type {any} */ e) => {
|
|
257
|
+
const input = e.target.closest(".import-add-form")?.querySelector("sp-textfield");
|
|
258
|
+
const name = input?.value?.trim();
|
|
259
|
+
if (!name) return;
|
|
260
|
+
input.value = "";
|
|
261
|
+
try {
|
|
262
|
+
const platform = getPlatform();
|
|
263
|
+
await platform.addPackage(name);
|
|
264
|
+
const { loadComponentRegistry } = await import("../files/components.js");
|
|
265
|
+
await loadComponentRegistry();
|
|
266
|
+
renderLeftPanel();
|
|
267
|
+
} catch (/** @type {any} */ err) {
|
|
268
|
+
console.error("Failed to add package:", err);
|
|
269
|
+
}
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<sp-icon-add slot="icon" size="xs"></sp-icon-add>
|
|
273
|
+
</sp-action-button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Document-level: Component Imports + npm Component Cherry-pick ───────────
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @param {{
|
|
284
|
+
* renderLeftPanel: () => void;
|
|
285
|
+
* documentPath: string | null;
|
|
286
|
+
* documentElements: any[];
|
|
287
|
+
* applyMutation: (fn: (doc: any) => void) => void;
|
|
288
|
+
* }} ctx
|
|
289
|
+
*/
|
|
290
|
+
function renderDocumentLevelImports({
|
|
291
|
+
renderLeftPanel,
|
|
292
|
+
documentPath,
|
|
293
|
+
documentElements,
|
|
294
|
+
applyMutation,
|
|
295
|
+
}) {
|
|
296
|
+
const refEntries = documentElements.filter(
|
|
297
|
+
(/** @type {any} */ e) => e && typeof e === "object" && e.$ref,
|
|
298
|
+
);
|
|
299
|
+
const npmEntries = documentElements.filter((/** @type {any} */ e) => typeof e === "string");
|
|
300
|
+
|
|
301
|
+
// Available JX components not yet imported
|
|
302
|
+
const importedRefs = new Set(refEntries.map((/** @type {any} */ e) => e.$ref));
|
|
303
|
+
const availableComponents = componentRegistry.filter(
|
|
304
|
+
(/** @type {any} */ c) =>
|
|
305
|
+
c.source !== "npm" && !importedRefs.has(`./${c.path}`) && !importedRefs.has(c.path),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const packageGroups = groupByPackage();
|
|
309
|
+
|
|
310
|
+
/** @param {string} ref */
|
|
311
|
+
const removeRef = (ref) => {
|
|
312
|
+
applyMutation((/** @type {any} */ doc) => {
|
|
313
|
+
doc.$elements = (doc.$elements || []).filter(
|
|
314
|
+
(/** @type {any} */ e) => !(e && typeof e === "object" && e.$ref === ref),
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
renderLeftPanel();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return html`
|
|
321
|
+
<div class="imports-panel">
|
|
322
|
+
<!-- Component Imports ($ref) -->
|
|
323
|
+
<div class="imports-section">
|
|
324
|
+
<div class="imports-section-header">
|
|
325
|
+
<span class="imports-section-title">Components</span>
|
|
326
|
+
<span class="imports-count">${refEntries.length}</span>
|
|
327
|
+
</div>
|
|
328
|
+
${refEntries.length > 0
|
|
329
|
+
? html`
|
|
330
|
+
<div class="imports-list">
|
|
331
|
+
${refEntries.map(
|
|
332
|
+
(/** @type {any} */ entry) => html`
|
|
333
|
+
<div class="import-row">
|
|
334
|
+
<span class="import-path" title=${entry.$ref}>${entry.$ref}</span>
|
|
335
|
+
<sp-action-button
|
|
336
|
+
quiet
|
|
337
|
+
size="xs"
|
|
338
|
+
title="Remove"
|
|
339
|
+
@click=${() => removeRef(entry.$ref)}
|
|
340
|
+
>
|
|
341
|
+
<sp-icon-close slot="icon" size="xs"></sp-icon-close>
|
|
342
|
+
</sp-action-button>
|
|
343
|
+
</div>
|
|
344
|
+
`,
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
`
|
|
348
|
+
: nothing}
|
|
349
|
+
${availableComponents.length > 0
|
|
350
|
+
? html`
|
|
351
|
+
<div class="import-add-form">
|
|
352
|
+
<sp-picker
|
|
353
|
+
size="s"
|
|
354
|
+
label="Add component…"
|
|
355
|
+
class="import-picker"
|
|
356
|
+
@change=${(/** @type {any} */ e) => {
|
|
357
|
+
const tag = e.target.value;
|
|
358
|
+
if (!tag) return;
|
|
359
|
+
e.target.value = "";
|
|
360
|
+
const comp = componentRegistry.find(
|
|
361
|
+
(/** @type {any} */ c) => c.tagName === tag,
|
|
362
|
+
);
|
|
363
|
+
if (!comp) return;
|
|
364
|
+
const relPath = computeRelativePath(documentPath, comp.path);
|
|
365
|
+
applyMutation((/** @type {any} */ doc) => {
|
|
366
|
+
if (!doc.$elements) doc.$elements = [];
|
|
367
|
+
doc.$elements.push({ $ref: relPath });
|
|
368
|
+
});
|
|
369
|
+
renderLeftPanel();
|
|
370
|
+
}}
|
|
371
|
+
>
|
|
372
|
+
${availableComponents.map(
|
|
373
|
+
(/** @type {any} */ c) =>
|
|
374
|
+
html`<sp-menu-item value=${c.tagName}><${c.tagName}></sp-menu-item>`,
|
|
375
|
+
)}
|
|
376
|
+
</sp-picker>
|
|
377
|
+
</div>
|
|
378
|
+
`
|
|
379
|
+
: nothing}
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<!-- npm Package Components (cherry-pick toggles) -->
|
|
383
|
+
${[...packageGroups.entries()].map(
|
|
384
|
+
([pkg, comps]) => html`
|
|
385
|
+
<div class="imports-section">
|
|
386
|
+
<div class="imports-section-header">
|
|
387
|
+
<span class="imports-section-title import-mono">${pkg}</span>
|
|
388
|
+
</div>
|
|
389
|
+
<div class="imports-list imports-component-list">
|
|
390
|
+
${comps.map((/** @type {any} */ comp) => {
|
|
391
|
+
const enabled = isComponentEnabled(comp, npmEntries);
|
|
392
|
+
const specifier = componentSpecifier(comp);
|
|
393
|
+
return html`
|
|
394
|
+
<div class="import-row import-component-row">
|
|
395
|
+
<sp-checkbox
|
|
396
|
+
size="s"
|
|
397
|
+
.checked=${enabled}
|
|
398
|
+
@change=${(/** @type {any} */ e) => {
|
|
399
|
+
applyMutation((/** @type {any} */ doc) => {
|
|
400
|
+
if (!doc.$elements) doc.$elements = [];
|
|
401
|
+
// Remove legacy full-package import if present
|
|
402
|
+
doc.$elements = doc.$elements.filter(
|
|
403
|
+
(/** @type {any} */ el) => el !== pkg,
|
|
404
|
+
);
|
|
405
|
+
if (e.target.checked) {
|
|
406
|
+
if (!doc.$elements.includes(specifier)) doc.$elements.push(specifier);
|
|
407
|
+
} else {
|
|
408
|
+
doc.$elements = doc.$elements.filter(
|
|
409
|
+
(/** @type {any} */ el) => el !== specifier,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
renderLeftPanel();
|
|
414
|
+
}}
|
|
415
|
+
>
|
|
416
|
+
<span class="import-component-label"><${comp.tagName}></span>
|
|
417
|
+
</sp-checkbox>
|
|
418
|
+
</div>
|
|
419
|
+
`;
|
|
420
|
+
})}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
`,
|
|
424
|
+
)}
|
|
425
|
+
</div>
|
|
426
|
+
`;
|
|
427
|
+
}
|