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