@jxsuite/studio 0.37.1 → 1.0.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 +77654 -76269
  2. package/dist/studio.js.map +128 -119
  3. package/package.json +44 -44
  4. package/src/account-status.ts +39 -0
  5. package/src/browse/browse.ts +10 -14
  6. package/src/canvas/iframe-host.ts +1 -1
  7. package/src/editor/context-menu.ts +1 -1
  8. package/src/editor/repeater-scope.ts +8 -13
  9. package/src/files/files.ts +3 -0
  10. package/src/format/format-host.ts +63 -1
  11. package/src/new-project/add-repo-modal.ts +183 -0
  12. package/src/new-project/new-project-modal.ts +22 -3
  13. package/src/page-params.ts +34 -8
  14. package/src/panels/ai-chat/chat-markdown.ts +2 -2
  15. package/src/panels/ai-panel.ts +61 -4
  16. package/src/panels/data-grid.ts +619 -0
  17. package/src/panels/signals-panel.ts +102 -437
  18. package/src/panels/statusbar.ts +1 -1
  19. package/src/panels/welcome-screen.ts +50 -0
  20. package/src/platform-errors.ts +30 -0
  21. package/src/platforms/cloud.ts +172 -89
  22. package/src/platforms/devserver.ts +172 -0
  23. package/src/services/context-resolver.ts +73 -0
  24. package/src/services/data-service.ts +155 -0
  25. package/src/services/monaco-setup.ts +75 -26
  26. package/src/settings/contributed-section.ts +406 -0
  27. package/src/settings/extension-sections.ts +145 -0
  28. package/src/settings/schema-field-ui.ts +4 -2
  29. package/src/settings/settings-modal.ts +101 -42
  30. package/src/site-context.ts +10 -1
  31. package/src/studio.ts +24 -0
  32. package/src/tabs/transact.ts +1 -1
  33. package/src/types.ts +120 -1
  34. package/src/ui/form-controls.ts +322 -0
  35. package/src/ui/progress-modal.ts +2 -2
  36. package/src/ui/schema-form.ts +524 -0
  37. package/src/utils/studio-utils.ts +4 -3
  38. package/src/settings/content-types-editor.ts +0 -599
@@ -1,599 +0,0 @@
1
- /// <reference lib="dom" />
2
- /**
3
- * Content Types Editor — visual schema builder for project content types.
4
- *
5
- * Renders inside the Settings view "Content Types" tab. Two-column layout: left column lists
6
- * content type names, right column edits the selected content type's schema.
7
- */
8
-
9
- import { html, render as litRender } from "lit-html";
10
- import { repeat } from "lit-html/directives/repeat.js";
11
- import { getPlatform } from "../platform";
12
- import { projectState } from "../store";
13
- import { addFieldFormTpl, detectFieldFormat, fieldCardTpl, schemaForType } from "./schema-field-ui";
14
- import { toCamelCase } from "../utils/studio-utils";
15
-
16
- import type { FieldHandlers } from "./schema-field-ui.js";
17
- import type {
18
- ContentTypeSchema,
19
- ContentTypeSchemaField,
20
- ProjectConfig,
21
- } from "@jxsuite/schema/types";
22
-
23
- // ─── Module state ─────────────────────────────────────────────────────────────
24
-
25
- let selectedContentType: string | null = null;
26
- let showAddField = false;
27
- let newFieldState = { format: "", name: "", required: false, type: "string" };
28
- let showNewContentType = false;
29
- let newContentTypeName = "";
30
-
31
- // ─── Persistence ──────────────────────────────────────────────────────────────
32
-
33
- async function saveProjectConfig() {
34
- const platform = getPlatform();
35
- const config = (projectState as { projectConfig: ProjectConfig }).projectConfig;
36
- await platform.writeFile("project.json", JSON.stringify(config, null, "\t"));
37
- }
38
-
39
- // ─── Helpers ─────────────────────────────────────────────────────────────────
40
-
41
- /** Get the schema object for the selected content type. */
42
- function getSelectedSchema(): ContentTypeSchema | undefined {
43
- const config = projectState?.projectConfig;
44
- return config?.contentTypes?.[selectedContentType as string]?.schema as
45
- | ContentTypeSchema
46
- | undefined;
47
- }
48
-
49
- // ─── Handlers ─────────────────────────────────────────────────────────────────
50
-
51
- /** @param {() => void} rerender */
52
- function handleNewContentType(rerender: () => void) {
53
- const slug = newContentTypeName
54
- .toLowerCase()
55
- .replaceAll(/\s+/g, "-")
56
- .replaceAll(/[^a-z0-9-]/g, "");
57
- if (!slug) {
58
- return;
59
- }
60
-
61
- const config = projectState?.projectConfig;
62
- if (!config) {
63
- return;
64
- }
65
- if (!config.contentTypes) {
66
- config.contentTypes = {};
67
- }
68
- if (config.contentTypes[slug]) {
69
- return;
70
- } // Already exists
71
-
72
- config.contentTypes[slug] = {
73
- schema: { properties: {}, required: [], type: "object" },
74
- source: `./content/${slug}/`,
75
- };
76
-
77
- selectedContentType = slug;
78
- showNewContentType = false;
79
- newContentTypeName = "";
80
- rerender();
81
-
82
- // Persist in background
83
- void saveProjectConfig().then(async () => {
84
- const platform = getPlatform();
85
- await platform.writeFile(`content/${slug}/.gitkeep`, "");
86
- });
87
- }
88
-
89
- /** @param {() => void} rerender */
90
- function handleAddField(rerender: () => void) {
91
- const raw = newFieldState.name.trim();
92
- if (!raw || !selectedContentType) {
93
- return;
94
- }
95
- const name = toCamelCase(raw);
96
-
97
- const schema = getSelectedSchema();
98
- if (!schema) {
99
- return;
100
- }
101
-
102
- if (!schema.properties) {
103
- schema.properties = {};
104
- }
105
- schema.properties[name] = schemaForType(newFieldState.type, newFieldState.format || undefined);
106
-
107
- if (newFieldState.required) {
108
- if (!schema.required) {
109
- schema.required = [];
110
- }
111
- if (!schema.required.includes(name)) {
112
- schema.required.push(name);
113
- }
114
- }
115
-
116
- showAddField = false;
117
- newFieldState = { format: "", name: "", required: false, type: "string" };
118
- rerender();
119
- void saveProjectConfig();
120
- }
121
-
122
- /**
123
- * @param {string} fieldName
124
- * @param {() => void} rerender
125
- */
126
- function handleDeleteField(fieldName: string, rerender: () => void) {
127
- const schema = getSelectedSchema();
128
- if (!schema?.properties) {
129
- return;
130
- }
131
-
132
- delete schema.properties[fieldName];
133
- if (schema.required) {
134
- schema.required = schema.required.filter((r: string) => r !== fieldName);
135
- }
136
-
137
- rerender();
138
- void saveProjectConfig();
139
- }
140
-
141
- /**
142
- * @param {string} fieldName
143
- * @param {() => void} rerender
144
- */
145
- function handleToggleRequired(fieldName: string, rerender: () => void) {
146
- const schema = getSelectedSchema();
147
- if (!schema) {
148
- return;
149
- }
150
- if (!schema.required) {
151
- schema.required = [];
152
- }
153
-
154
- const idx = schema.required.indexOf(fieldName);
155
- if (idx !== -1) {
156
- schema.required.splice(idx, 1);
157
- } else {
158
- schema.required.push(fieldName);
159
- }
160
-
161
- rerender();
162
- void saveProjectConfig();
163
- }
164
-
165
- /**
166
- * @param {string} oldName
167
- * @param {string} newName
168
- * @param {() => void} rerender
169
- */
170
- function handleRenameField(oldName: string, newName: string, rerender: () => void) {
171
- const schema = getSelectedSchema();
172
- const normalized = toCamelCase(newName);
173
- if (!schema?.properties || !normalized || schema.properties[normalized]) {
174
- return;
175
- }
176
-
177
- const newProps: Record<string, ContentTypeSchemaField> = {};
178
- for (const [key, val] of Object.entries(schema.properties)) {
179
- newProps[key === oldName ? normalized : key] = val;
180
- }
181
- schema.properties = newProps;
182
-
183
- if (schema.required) {
184
- schema.required = schema.required.map((r: string) => (r === oldName ? normalized : r));
185
- }
186
-
187
- rerender();
188
- void saveProjectConfig();
189
- }
190
-
191
- /**
192
- * @param {string} fieldName
193
- * @param {string} newType
194
- * @param {() => void} rerender
195
- */
196
- function handleChangeType(fieldName: string, newType: string, rerender: () => void) {
197
- const schema = getSelectedSchema();
198
- if (!schema?.properties?.[fieldName]) {
199
- return;
200
- }
201
-
202
- const oldFormat =
203
- newType === "string" || newType === "array"
204
- ? detectFieldFormat(schema.properties[fieldName])
205
- : undefined;
206
- schema.properties[fieldName] = schemaForType(newType, oldFormat || undefined);
207
- rerender();
208
- void saveProjectConfig();
209
- }
210
-
211
- /**
212
- * @param {string} fieldName
213
- * @param {string} format
214
- * @param {() => void} rerender
215
- */
216
- function handleChangeFormat(fieldName: string, format: string, rerender: () => void) {
217
- const schema = getSelectedSchema();
218
- if (!schema?.properties?.[fieldName]) {
219
- return;
220
- }
221
-
222
- const prop = schema.properties[fieldName];
223
- const type = prop.type || "string";
224
- schema.properties[fieldName] = schemaForType(type, format || undefined);
225
- rerender();
226
- void saveProjectConfig();
227
- }
228
-
229
- /**
230
- * @param {string} fieldName
231
- * @param {string} target
232
- * @param {() => void} rerender
233
- */
234
- function handleChangeRefTarget(fieldName: string, target: string, rerender: () => void) {
235
- const schema = getSelectedSchema();
236
- if (!schema?.properties) {
237
- return;
238
- }
239
-
240
- schema.properties[fieldName] = { $ref: `#/contentTypes/${target}` };
241
- rerender();
242
- void saveProjectConfig();
243
- }
244
-
245
- // ─── Nested field handlers ───────────────────────────────────────────────────
246
-
247
- /**
248
- * @param {string} parentName
249
- * @param {{ name: string; type: string; required: boolean }} fieldState
250
- * @param {() => void} rerender
251
- */
252
- function handleAddNestedField(
253
- parentName: string,
254
- fieldState: { name: string; type: string; required: boolean },
255
- rerender: () => void,
256
- ) {
257
- const schema = getSelectedSchema();
258
- const parent = schema?.properties?.[parentName];
259
- if (!parent) {
260
- return;
261
- }
262
-
263
- const name = toCamelCase(fieldState.name);
264
- if (!name) {
265
- return;
266
- }
267
-
268
- if (!parent.properties) {
269
- parent.properties = {};
270
- }
271
- parent.properties[name] = schemaForType(fieldState.type);
272
-
273
- if (fieldState.required) {
274
- if (!parent.required) {
275
- parent.required = [];
276
- }
277
- if (!parent.required.includes(name)) {
278
- parent.required.push(name);
279
- }
280
- }
281
-
282
- rerender();
283
- void saveProjectConfig();
284
- }
285
-
286
- /**
287
- * @param {string} parentName
288
- * @param {string} childName
289
- * @param {() => void} rerender
290
- */
291
- function handleDeleteNested(parentName: string, childName: string, rerender: () => void) {
292
- const schema = getSelectedSchema();
293
- const parent = schema?.properties?.[parentName];
294
- if (!parent?.properties) {
295
- return;
296
- }
297
-
298
- delete parent.properties[childName];
299
- if (parent.required) {
300
- parent.required = parent.required.filter((r: string) => r !== childName);
301
- }
302
-
303
- rerender();
304
- void saveProjectConfig();
305
- }
306
-
307
- /**
308
- * @param {string} parentName
309
- * @param {string} childName
310
- * @param {() => void} rerender
311
- */
312
- function handleToggleNestedRequired(parentName: string, childName: string, rerender: () => void) {
313
- const schema = getSelectedSchema();
314
- const parent = schema?.properties?.[parentName];
315
- if (!parent) {
316
- return;
317
- }
318
- if (!parent.required) {
319
- parent.required = [];
320
- }
321
-
322
- const idx = parent.required.indexOf(childName);
323
- if (idx !== -1) {
324
- parent.required.splice(idx, 1);
325
- } else {
326
- parent.required.push(childName);
327
- }
328
-
329
- rerender();
330
- void saveProjectConfig();
331
- }
332
-
333
- /**
334
- * @param {string} parentName
335
- * @param {string} oldChild
336
- * @param {string} newChild
337
- * @param {() => void} rerender
338
- */
339
- function handleRenameNested(
340
- parentName: string,
341
- oldChild: string,
342
- newChild: string,
343
- rerender: () => void,
344
- ) {
345
- const schema = getSelectedSchema();
346
- const parent = schema?.properties?.[parentName];
347
- const normalized = toCamelCase(newChild);
348
- if (!parent?.properties || !normalized || parent.properties[normalized]) {
349
- return;
350
- }
351
-
352
- const newProps: Record<string, ContentTypeSchemaField> = {};
353
- for (const [key, val] of Object.entries(parent.properties)) {
354
- newProps[key === oldChild ? normalized : key] = val;
355
- }
356
- parent.properties = newProps;
357
-
358
- if (parent.required) {
359
- parent.required = parent.required.map((r: string) => (r === oldChild ? normalized : r));
360
- }
361
-
362
- rerender();
363
- void saveProjectConfig();
364
- }
365
-
366
- /**
367
- * @param {string} parentName
368
- * @param {string} childName
369
- * @param {string} newType
370
- * @param {() => void} rerender
371
- */
372
- function handleChangeNestedType(
373
- parentName: string,
374
- childName: string,
375
- newType: string,
376
- rerender: () => void,
377
- ) {
378
- const schema = getSelectedSchema();
379
- const parent = schema?.properties?.[parentName];
380
- if (!parent?.properties?.[childName]) {
381
- return;
382
- }
383
-
384
- const oldFormat =
385
- newType === "string" || newType === "array"
386
- ? detectFieldFormat(parent.properties[childName])
387
- : undefined;
388
- parent.properties[childName] = schemaForType(newType, oldFormat || undefined);
389
- rerender();
390
- void saveProjectConfig();
391
- }
392
-
393
- /**
394
- * @param {string} parentName
395
- * @param {string} childName
396
- * @param {string} format
397
- * @param {() => void} rerender
398
- */
399
- function handleChangeNestedFormat(
400
- parentName: string,
401
- childName: string,
402
- format: string,
403
- rerender: () => void,
404
- ) {
405
- const schema = getSelectedSchema();
406
- const parent = schema?.properties?.[parentName];
407
- if (!parent?.properties?.[childName]) {
408
- return;
409
- }
410
-
411
- const prop = parent.properties[childName];
412
- const type = prop.type || "string";
413
- parent.properties[childName] = schemaForType(type, format || undefined);
414
- rerender();
415
- void saveProjectConfig();
416
- }
417
-
418
- /** @param {() => void} rerender */
419
- function handleDeleteContentType(rerender: () => void) {
420
- if (!selectedContentType) {
421
- return;
422
- }
423
- const config = projectState?.projectConfig;
424
- if (!config?.contentTypes?.[selectedContentType]) {
425
- return;
426
- }
427
-
428
- delete config.contentTypes[selectedContentType];
429
- selectedContentType = null;
430
-
431
- rerender();
432
- void saveProjectConfig();
433
- }
434
-
435
- // ─── Render ───────────────────────────────────────────────────────────────────
436
-
437
- /**
438
- * Render the content types editor.
439
- *
440
- * @param {HTMLElement} container
441
- */
442
- export function renderContentTypesEditor(container: HTMLElement) {
443
- const rerender = () => renderContentTypesEditor(container);
444
- const config = projectState?.projectConfig;
445
- const contentTypes = config?.contentTypes || {};
446
- const contentTypeNames = Object.keys(contentTypes);
447
-
448
- // Left column — content type list
449
- const listTpl = html`
450
- <div class="settings-list-panel">
451
- ${contentTypeNames.map(
452
- (name) => html`
453
- <sp-action-button
454
- size="s"
455
- ?selected=${selectedContentType === name}
456
- @click=${() => {
457
- selectedContentType = name;
458
- showAddField = false;
459
- rerender();
460
- }}
461
- >
462
- ${name}
463
- </sp-action-button>
464
- `,
465
- )}
466
- ${showNewContentType
467
- ? html`
468
- <div class="settings-inline-form">
469
- <sp-textfield
470
- size="s"
471
- placeholder="content-type-name"
472
- .value=${newContentTypeName}
473
- @input=${(e: Event) => {
474
- newContentTypeName = (e.target as HTMLInputElement).value;
475
- }}
476
- @keydown=${(e: KeyboardEvent) => {
477
- if (e.key === "Enter") {
478
- handleNewContentType(rerender);
479
- }
480
- if (e.key === "Escape") {
481
- showNewContentType = false;
482
- rerender();
483
- }
484
- }}
485
- ></sp-textfield>
486
- <sp-action-button size="s" @click=${() => handleNewContentType(rerender)}>
487
- Create
488
- </sp-action-button>
489
- </div>
490
- `
491
- : html`
492
- <sp-action-button
493
- size="s"
494
- quiet
495
- @click=${() => {
496
- showNewContentType = true;
497
- rerender();
498
- }}
499
- >
500
- <sp-icon-add slot="icon"></sp-icon-add> New Content Type
501
- </sp-action-button>
502
- `}
503
- </div>
504
- `;
505
-
506
- // Right column — schema editor
507
- let editorTpl;
508
- if (!selectedContentType || !contentTypes[selectedContentType]) {
509
- editorTpl = html`<div class="settings-empty-state">Select or create a content type</div>`;
510
- } else {
511
- const col = contentTypes[selectedContentType]!;
512
- const schema = (col.schema || {}) as ContentTypeSchema;
513
- const properties = schema.properties || {};
514
- const required = schema.required || [];
515
-
516
- const handlers: FieldHandlers = {
517
- onAddNestedField: (p: string, s: { name: string; type: string; required: boolean }) =>
518
- handleAddNestedField(p, s, rerender),
519
- onChangeFormat: (n: string, f: string) => handleChangeFormat(n, f, rerender),
520
- onChangeNestedFormat: (p: string, c: string, f: string) =>
521
- handleChangeNestedFormat(p, c, f, rerender),
522
- onChangeNestedType: (p: string, c: string, t: string) =>
523
- handleChangeNestedType(p, c, t, rerender),
524
- onChangeRefTarget: (n: string, target: string) => handleChangeRefTarget(n, target, rerender),
525
- onChangeType: (n: string, t: string) => handleChangeType(n, t, rerender),
526
- onDelete: (n: string) => handleDeleteField(n, rerender),
527
- onDeleteNested: (p: string, c: string) => handleDeleteNested(p, c, rerender),
528
- onRename: (oldN: string, newN: string) => handleRenameField(oldN, newN, rerender),
529
- onRenameNested: (p: string, o: string, n: string) => handleRenameNested(p, o, n, rerender),
530
- onToggleNestedRequired: (p: string, c: string) => handleToggleNestedRequired(p, c, rerender),
531
- onToggleRequired: (n: string) => handleToggleRequired(n, rerender),
532
- };
533
-
534
- const fieldCards = repeat(
535
- Object.entries(properties),
536
- ([name]) => name,
537
- ([name, def]) =>
538
- fieldCardTpl(
539
- name,
540
- /** @type {import("./schema-field-ui.js").SchemaProperty} */ def,
541
- required.includes(name),
542
- handlers,
543
- contentTypeNames,
544
- ),
545
- );
546
-
547
- editorTpl = html`
548
- <div class="settings-editor-panel">
549
- <div class="settings-editor-header">
550
- <h3>${selectedContentType}</h3>
551
- <sp-field-label size="s">Source: ${col.source || "—"}</sp-field-label>
552
- <sp-action-button
553
- size="xs"
554
- quiet
555
- title="Delete content type"
556
- @click=${() => handleDeleteContentType(rerender)}
557
- >
558
- <sp-icon-delete slot="icon"></sp-icon-delete>
559
- </sp-action-button>
560
- </div>
561
- <div class="schema-field-list">${fieldCards}</div>
562
- ${showAddField
563
- ? addFieldFormTpl(newFieldState, {
564
- onCancel: () => {
565
- showAddField = false;
566
- newFieldState = {
567
- format: "",
568
- name: "",
569
- required: false,
570
- type: "string",
571
- };
572
- rerender();
573
- },
574
- onConfirm: () => handleAddField(rerender),
575
- onInput: (field, value) => {
576
- newFieldState = { ...newFieldState, [field]: value };
577
- rerender();
578
- },
579
- })
580
- : html`
581
- <sp-action-button
582
- size="s"
583
- quiet
584
- @click=${() => {
585
- showAddField = true;
586
- rerender();
587
- }}
588
- >
589
- <sp-icon-add slot="icon"></sp-icon-add> Add Field
590
- </sp-action-button>
591
- `}
592
- </div>
593
- `;
594
- }
595
-
596
- const tpl = html` <div class="settings-two-col">${listTpl} ${editorTpl}</div> `;
597
-
598
- litRender(tpl, container);
599
- }