@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.
Files changed (38) hide show
  1. package/dist/studio.js +47638 -33445
  2. package/dist/studio.js.map +449 -344
  3. package/package.json +45 -34
  4. package/src/browse/browse.js +414 -0
  5. package/src/editor/context-menu.js +48 -1
  6. package/src/editor/convert-to-component.js +208 -0
  7. package/src/editor/inline-edit.js +33 -6
  8. package/src/editor/shortcuts.js +6 -1
  9. package/src/files/components.js +4 -2
  10. package/src/files/file-ops.js +102 -54
  11. package/src/files/files.js +22 -8
  12. package/src/markdown/md-convert.js +309 -11
  13. package/src/panels/activity-bar.js +3 -0
  14. package/src/panels/head-panel.js +576 -0
  15. package/src/panels/overlays.js +125 -0
  16. package/src/panels/right-panel.js +104 -0
  17. package/src/panels/shared.js +41 -0
  18. package/src/panels/signals-panel.js +95 -94
  19. package/src/panels/toolbar.js +217 -0
  20. package/src/platforms/devserver.js +58 -16
  21. package/src/settings/collections-editor.js +428 -0
  22. package/src/settings/defs-editor.js +418 -0
  23. package/src/settings/schema-field-ui.js +329 -0
  24. package/src/state.js +99 -2
  25. package/src/store.js +77 -41
  26. package/src/studio.js +1523 -1375
  27. package/src/ui/button-group.js +91 -0
  28. package/src/ui/color-selector.js +299 -0
  29. package/src/ui/field-row.js +47 -0
  30. package/src/ui/media-picker.js +172 -0
  31. package/src/ui/panel-resize.js +96 -0
  32. package/src/ui/spectrum.js +36 -2
  33. package/src/ui/unit-selector.js +106 -0
  34. package/src/ui/{jx-styled-combobox.js → value-selector.js} +7 -7
  35. package/src/ui/widgets.js +106 -0
  36. package/src/utils/inherited-style.js +54 -0
  37. package/src/utils/studio-utils.js +32 -0
  38. 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
+ }