@jxsuite/studio 0.1.0 → 0.5.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.
- package/dist/studio.js +47638 -33445
- package/dist/studio.js.map +449 -344
- package/package.json +45 -34
- package/src/browse/browse.js +414 -0
- package/src/editor/context-menu.js +48 -1
- package/src/editor/convert-to-component.js +208 -0
- package/src/editor/inline-edit.js +33 -6
- package/src/editor/shortcuts.js +6 -1
- package/src/files/components.js +4 -2
- package/src/files/file-ops.js +102 -54
- package/src/files/files.js +22 -8
- package/src/markdown/md-convert.js +309 -11
- package/src/panels/activity-bar.js +3 -0
- package/src/panels/head-panel.js +576 -0
- package/src/panels/overlays.js +125 -0
- package/src/panels/right-panel.js +104 -0
- package/src/panels/shared.js +41 -0
- package/src/panels/signals-panel.js +95 -94
- package/src/panels/toolbar.js +217 -0
- package/src/platforms/devserver.js +58 -16
- package/src/settings/collections-editor.js +428 -0
- package/src/settings/defs-editor.js +418 -0
- package/src/settings/schema-field-ui.js +329 -0
- package/src/state.js +99 -2
- package/src/store.js +77 -41
- package/src/studio.js +1523 -1375
- package/src/ui/button-group.js +91 -0
- package/src/ui/color-selector.js +299 -0
- package/src/ui/field-row.js +47 -0
- package/src/ui/media-picker.js +172 -0
- package/src/ui/panel-resize.js +96 -0
- package/src/ui/spectrum.js +36 -2
- package/src/ui/unit-selector.js +106 -0
- package/src/ui/{jx-styled-combobox.js → value-selector.js} +7 -7
- package/src/ui/widgets.js +106 -0
- package/src/utils/inherited-style.js +54 -0
- package/src/utils/studio-utils.js +32 -0
- package/src/view.js +45 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definitions Editor — visual editor for project-level $defs (JSON Schema type definitions).
|
|
3
|
+
*
|
|
4
|
+
* Manages entries in project.json `$defs` — reusable type schemas for external datasets, API
|
|
5
|
+
* responses, CMS payloads, etc. Same concept as component-level $defs but scoped to the entire
|
|
6
|
+
* project.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { html, render as litRender } from "lit-html";
|
|
10
|
+
import { getPlatform } from "../platform.js";
|
|
11
|
+
import { projectState } from "../store.js";
|
|
12
|
+
import { fieldCardTpl, addFieldFormTpl, schemaForType } from "./schema-field-ui.js";
|
|
13
|
+
|
|
14
|
+
// ─── Module state ─────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** @type {string | null} */
|
|
17
|
+
let selectedDef = null;
|
|
18
|
+
let showAddField = false;
|
|
19
|
+
let newFieldState = { name: "", type: "string", required: false };
|
|
20
|
+
let showNewDef = false;
|
|
21
|
+
let newDefName = "";
|
|
22
|
+
|
|
23
|
+
// ─── Persistence ──────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
async function saveProjectConfig() {
|
|
26
|
+
const platform = getPlatform();
|
|
27
|
+
const config = /** @type {any} */ (projectState).projectConfig;
|
|
28
|
+
await platform.writeFile("project.json", JSON.stringify(config, null, "\t"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Get the selected $def schema object. */
|
|
34
|
+
function getSelectedDef() {
|
|
35
|
+
const config = projectState?.projectConfig;
|
|
36
|
+
return config?.$defs?.[/** @type {string} */ (selectedDef)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Handlers ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/** @param {() => void} rerender */
|
|
42
|
+
function handleNewDef(rerender) {
|
|
43
|
+
const name = newDefName.trim();
|
|
44
|
+
if (!name) return;
|
|
45
|
+
|
|
46
|
+
const config = projectState?.projectConfig;
|
|
47
|
+
if (!config) return;
|
|
48
|
+
if (!config.$defs) config.$defs = {};
|
|
49
|
+
if (config.$defs[name]) return; // already exists
|
|
50
|
+
|
|
51
|
+
config.$defs[name] = {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {},
|
|
54
|
+
required: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
selectedDef = name;
|
|
58
|
+
showNewDef = false;
|
|
59
|
+
newDefName = "";
|
|
60
|
+
rerender();
|
|
61
|
+
saveProjectConfig();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {() => void} rerender */
|
|
65
|
+
function handleAddField(rerender) {
|
|
66
|
+
const name = newFieldState.name.trim();
|
|
67
|
+
if (!name || !selectedDef) return;
|
|
68
|
+
|
|
69
|
+
const def = getSelectedDef();
|
|
70
|
+
if (!def) return;
|
|
71
|
+
|
|
72
|
+
if (!def.properties) def.properties = {};
|
|
73
|
+
def.properties[name] = schemaForType(newFieldState.type);
|
|
74
|
+
|
|
75
|
+
if (newFieldState.required) {
|
|
76
|
+
if (!def.required) def.required = [];
|
|
77
|
+
if (!def.required.includes(name)) def.required.push(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
showAddField = false;
|
|
81
|
+
newFieldState = { name: "", type: "string", required: false };
|
|
82
|
+
rerender();
|
|
83
|
+
saveProjectConfig();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} fieldName
|
|
88
|
+
* @param {() => void} rerender
|
|
89
|
+
*/
|
|
90
|
+
function handleDeleteField(fieldName, rerender) {
|
|
91
|
+
const def = getSelectedDef();
|
|
92
|
+
if (!def?.properties) return;
|
|
93
|
+
|
|
94
|
+
delete def.properties[fieldName];
|
|
95
|
+
if (def.required) {
|
|
96
|
+
def.required = def.required.filter((/** @type {string} */ r) => r !== fieldName);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
rerender();
|
|
100
|
+
saveProjectConfig();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} fieldName
|
|
105
|
+
* @param {() => void} rerender
|
|
106
|
+
*/
|
|
107
|
+
function handleToggleRequired(fieldName, rerender) {
|
|
108
|
+
const def = getSelectedDef();
|
|
109
|
+
if (!def) return;
|
|
110
|
+
if (!def.required) def.required = [];
|
|
111
|
+
|
|
112
|
+
const idx = def.required.indexOf(fieldName);
|
|
113
|
+
if (idx >= 0) def.required.splice(idx, 1);
|
|
114
|
+
else def.required.push(fieldName);
|
|
115
|
+
|
|
116
|
+
rerender();
|
|
117
|
+
saveProjectConfig();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} oldName
|
|
122
|
+
* @param {string} newName
|
|
123
|
+
* @param {() => void} rerender
|
|
124
|
+
*/
|
|
125
|
+
function handleRenameField(oldName, newName, rerender) {
|
|
126
|
+
const def = getSelectedDef();
|
|
127
|
+
if (!def?.properties || !newName || def.properties[newName]) return;
|
|
128
|
+
|
|
129
|
+
/** @type {Record<string, any>} */
|
|
130
|
+
const newProps = {};
|
|
131
|
+
for (const [key, val] of Object.entries(def.properties)) {
|
|
132
|
+
newProps[key === oldName ? newName : key] = val;
|
|
133
|
+
}
|
|
134
|
+
def.properties = newProps;
|
|
135
|
+
|
|
136
|
+
if (def.required) {
|
|
137
|
+
def.required = def.required.map((/** @type {string} */ r) => (r === oldName ? newName : r));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
rerender();
|
|
141
|
+
saveProjectConfig();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {string} fieldName
|
|
146
|
+
* @param {string} newType
|
|
147
|
+
* @param {() => void} rerender
|
|
148
|
+
*/
|
|
149
|
+
function handleChangeType(fieldName, newType, rerender) {
|
|
150
|
+
const def = getSelectedDef();
|
|
151
|
+
if (!def?.properties?.[fieldName]) return;
|
|
152
|
+
|
|
153
|
+
def.properties[fieldName] = schemaForType(newType);
|
|
154
|
+
rerender();
|
|
155
|
+
saveProjectConfig();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── Nested field handlers ───────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} parentName
|
|
162
|
+
* @param {{ name: string; type: string; required: boolean }} fieldState
|
|
163
|
+
* @param {() => void} rerender
|
|
164
|
+
*/
|
|
165
|
+
function handleAddNestedField(parentName, fieldState, rerender) {
|
|
166
|
+
const def = getSelectedDef();
|
|
167
|
+
const parent = def?.properties?.[parentName];
|
|
168
|
+
if (!parent) return;
|
|
169
|
+
|
|
170
|
+
if (!parent.properties) parent.properties = {};
|
|
171
|
+
parent.properties[fieldState.name] = schemaForType(fieldState.type);
|
|
172
|
+
|
|
173
|
+
if (fieldState.required) {
|
|
174
|
+
if (!parent.required) parent.required = [];
|
|
175
|
+
if (!parent.required.includes(fieldState.name)) parent.required.push(fieldState.name);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
rerender();
|
|
179
|
+
saveProjectConfig();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} parentName
|
|
184
|
+
* @param {string} childName
|
|
185
|
+
* @param {() => void} rerender
|
|
186
|
+
*/
|
|
187
|
+
function handleDeleteNested(parentName, childName, rerender) {
|
|
188
|
+
const def = getSelectedDef();
|
|
189
|
+
const parent = def?.properties?.[parentName];
|
|
190
|
+
if (!parent?.properties) return;
|
|
191
|
+
|
|
192
|
+
delete parent.properties[childName];
|
|
193
|
+
if (parent.required) {
|
|
194
|
+
parent.required = parent.required.filter((/** @type {string} */ r) => r !== childName);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rerender();
|
|
198
|
+
saveProjectConfig();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {string} parentName
|
|
203
|
+
* @param {string} childName
|
|
204
|
+
* @param {() => void} rerender
|
|
205
|
+
*/
|
|
206
|
+
function handleToggleNestedRequired(parentName, childName, rerender) {
|
|
207
|
+
const def = getSelectedDef();
|
|
208
|
+
const parent = def?.properties?.[parentName];
|
|
209
|
+
if (!parent) return;
|
|
210
|
+
if (!parent.required) parent.required = [];
|
|
211
|
+
|
|
212
|
+
const idx = parent.required.indexOf(childName);
|
|
213
|
+
if (idx >= 0) parent.required.splice(idx, 1);
|
|
214
|
+
else parent.required.push(childName);
|
|
215
|
+
|
|
216
|
+
rerender();
|
|
217
|
+
saveProjectConfig();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {string} parentName
|
|
222
|
+
* @param {string} oldChild
|
|
223
|
+
* @param {string} newChild
|
|
224
|
+
* @param {() => void} rerender
|
|
225
|
+
*/
|
|
226
|
+
function handleRenameNested(parentName, oldChild, newChild, rerender) {
|
|
227
|
+
const def = getSelectedDef();
|
|
228
|
+
const parent = def?.properties?.[parentName];
|
|
229
|
+
if (!parent?.properties || !newChild || parent.properties[newChild]) return;
|
|
230
|
+
|
|
231
|
+
/** @type {Record<string, any>} */
|
|
232
|
+
const newProps = {};
|
|
233
|
+
for (const [key, val] of Object.entries(parent.properties)) {
|
|
234
|
+
newProps[key === oldChild ? newChild : key] = val;
|
|
235
|
+
}
|
|
236
|
+
parent.properties = newProps;
|
|
237
|
+
|
|
238
|
+
if (parent.required) {
|
|
239
|
+
parent.required = parent.required.map((/** @type {string} */ r) =>
|
|
240
|
+
r === oldChild ? newChild : r,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
rerender();
|
|
245
|
+
saveProjectConfig();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {string} parentName
|
|
250
|
+
* @param {string} childName
|
|
251
|
+
* @param {string} newType
|
|
252
|
+
* @param {() => void} rerender
|
|
253
|
+
*/
|
|
254
|
+
function handleChangeNestedType(parentName, childName, newType, rerender) {
|
|
255
|
+
const def = getSelectedDef();
|
|
256
|
+
const parent = def?.properties?.[parentName];
|
|
257
|
+
if (!parent?.properties?.[childName]) return;
|
|
258
|
+
|
|
259
|
+
parent.properties[childName] = schemaForType(newType);
|
|
260
|
+
rerender();
|
|
261
|
+
saveProjectConfig();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** @param {() => void} rerender */
|
|
265
|
+
function handleDeleteDef(rerender) {
|
|
266
|
+
if (!selectedDef) return;
|
|
267
|
+
const config = projectState?.projectConfig;
|
|
268
|
+
if (!config?.$defs?.[selectedDef]) return;
|
|
269
|
+
|
|
270
|
+
delete config.$defs[selectedDef];
|
|
271
|
+
selectedDef = null;
|
|
272
|
+
|
|
273
|
+
rerender();
|
|
274
|
+
saveProjectConfig();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Render the definitions editor.
|
|
281
|
+
*
|
|
282
|
+
* @param {HTMLElement} container
|
|
283
|
+
*/
|
|
284
|
+
export function renderDefsEditor(container) {
|
|
285
|
+
const rerender = () => renderDefsEditor(container);
|
|
286
|
+
const config = projectState?.projectConfig;
|
|
287
|
+
const defs = config?.$defs || {};
|
|
288
|
+
const defNames = Object.keys(defs);
|
|
289
|
+
|
|
290
|
+
// Left column — def list
|
|
291
|
+
const listTpl = html`
|
|
292
|
+
<div class="settings-list-panel">
|
|
293
|
+
${defNames.map(
|
|
294
|
+
(name) => html`
|
|
295
|
+
<sp-action-button
|
|
296
|
+
size="s"
|
|
297
|
+
?selected=${selectedDef === name}
|
|
298
|
+
@click=${() => {
|
|
299
|
+
selectedDef = name;
|
|
300
|
+
showAddField = false;
|
|
301
|
+
rerender();
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
${name}
|
|
305
|
+
</sp-action-button>
|
|
306
|
+
`,
|
|
307
|
+
)}
|
|
308
|
+
${showNewDef
|
|
309
|
+
? html`
|
|
310
|
+
<div class="settings-inline-form">
|
|
311
|
+
<sp-textfield
|
|
312
|
+
size="s"
|
|
313
|
+
placeholder="TypeName"
|
|
314
|
+
.value=${newDefName}
|
|
315
|
+
@input=${(/** @type {any} */ e) => {
|
|
316
|
+
newDefName = e.target.value;
|
|
317
|
+
}}
|
|
318
|
+
@keydown=${(/** @type {any} */ e) => {
|
|
319
|
+
if (e.key === "Enter") handleNewDef(rerender);
|
|
320
|
+
if (e.key === "Escape") {
|
|
321
|
+
showNewDef = false;
|
|
322
|
+
rerender();
|
|
323
|
+
}
|
|
324
|
+
}}
|
|
325
|
+
></sp-textfield>
|
|
326
|
+
<sp-action-button size="s" @click=${() => handleNewDef(rerender)}>
|
|
327
|
+
Create
|
|
328
|
+
</sp-action-button>
|
|
329
|
+
</div>
|
|
330
|
+
`
|
|
331
|
+
: html`
|
|
332
|
+
<sp-action-button
|
|
333
|
+
size="s"
|
|
334
|
+
quiet
|
|
335
|
+
@click=${() => {
|
|
336
|
+
showNewDef = true;
|
|
337
|
+
rerender();
|
|
338
|
+
}}
|
|
339
|
+
>
|
|
340
|
+
<sp-icon-add slot="icon"></sp-icon-add> New Definition
|
|
341
|
+
</sp-action-button>
|
|
342
|
+
`}
|
|
343
|
+
</div>
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
// Right column — schema editor
|
|
347
|
+
let editorTpl;
|
|
348
|
+
if (!selectedDef || !defs[selectedDef]) {
|
|
349
|
+
editorTpl = html`<div class="settings-empty-state">Select or create a type definition</div>`;
|
|
350
|
+
} else {
|
|
351
|
+
const def = defs[selectedDef];
|
|
352
|
+
const properties = def.properties || {};
|
|
353
|
+
const required = def.required || [];
|
|
354
|
+
|
|
355
|
+
/** @type {import("./schema-field-ui.js").FieldHandlers} */
|
|
356
|
+
const handlers = {
|
|
357
|
+
onDelete: (n) => handleDeleteField(n, rerender),
|
|
358
|
+
onToggleRequired: (n) => handleToggleRequired(n, rerender),
|
|
359
|
+
onRename: (oldN, newN) => handleRenameField(oldN, newN, rerender),
|
|
360
|
+
onChangeType: (n, t) => handleChangeType(n, t, rerender),
|
|
361
|
+
onAddNestedField: (p, s) => handleAddNestedField(p, s, rerender),
|
|
362
|
+
onDeleteNested: (p, c) => handleDeleteNested(p, c, rerender),
|
|
363
|
+
onToggleNestedRequired: (p, c) => handleToggleNestedRequired(p, c, rerender),
|
|
364
|
+
onRenameNested: (p, o, n) => handleRenameNested(p, o, n, rerender),
|
|
365
|
+
onChangeNestedType: (p, c, t) => handleChangeNestedType(p, c, t, rerender),
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const fieldCards = Object.entries(properties).map(([name, fieldDef]) =>
|
|
369
|
+
fieldCardTpl(name, /** @type {any} */ (fieldDef), required.includes(name), handlers),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
editorTpl = html`
|
|
373
|
+
<div class="settings-editor-panel">
|
|
374
|
+
<div class="settings-editor-header">
|
|
375
|
+
<h3>${selectedDef}</h3>
|
|
376
|
+
<sp-action-button
|
|
377
|
+
size="xs"
|
|
378
|
+
quiet
|
|
379
|
+
title="Delete definition"
|
|
380
|
+
@click=${() => handleDeleteDef(rerender)}
|
|
381
|
+
>
|
|
382
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
383
|
+
</sp-action-button>
|
|
384
|
+
</div>
|
|
385
|
+
<div class="schema-field-list">${fieldCards}</div>
|
|
386
|
+
${showAddField
|
|
387
|
+
? addFieldFormTpl(newFieldState, {
|
|
388
|
+
onInput: (field, value) => {
|
|
389
|
+
newFieldState = { ...newFieldState, [field]: value };
|
|
390
|
+
rerender();
|
|
391
|
+
},
|
|
392
|
+
onConfirm: () => handleAddField(rerender),
|
|
393
|
+
onCancel: () => {
|
|
394
|
+
showAddField = false;
|
|
395
|
+
newFieldState = { name: "", type: "string", required: false };
|
|
396
|
+
rerender();
|
|
397
|
+
},
|
|
398
|
+
})
|
|
399
|
+
: html`
|
|
400
|
+
<sp-action-button
|
|
401
|
+
size="s"
|
|
402
|
+
quiet
|
|
403
|
+
@click=${() => {
|
|
404
|
+
showAddField = true;
|
|
405
|
+
rerender();
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
<sp-icon-add slot="icon"></sp-icon-add> Add Field
|
|
409
|
+
</sp-action-button>
|
|
410
|
+
`}
|
|
411
|
+
</div>
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const tpl = html` <div class="settings-two-col">${listTpl} ${editorTpl}</div> `;
|
|
416
|
+
|
|
417
|
+
litRender(tpl, container);
|
|
418
|
+
}
|