@jxsuite/studio 0.37.0 → 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
@@ -0,0 +1,322 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Built-in schema-form controls (specs/extensions.md §9.1): "schema-builder" (visual JSON-Schema
4
+ * field editor wrapping settings/schema-field-ui) and "secret" (value committed via the host's
5
+ * secret store, never project.json). The third built-in, "binding", registers from
6
+ * panels/signals-panel.ts because it owns panel-local ephemeral UI state (custom-ref mode) and the
7
+ * route-param picker semantics.
8
+ *
9
+ * Imported once for side effects from studio startup.
10
+ */
11
+
12
+ import { html } from "lit-html";
13
+ import { repeat } from "lit-html/directives/repeat.js";
14
+ import { registerFormControl } from "./schema-form";
15
+ import {
16
+ addFieldFormTpl,
17
+ detectFieldFormat,
18
+ fieldCardTpl,
19
+ schemaForType,
20
+ } from "../settings/schema-field-ui";
21
+ import { toCamelCase } from "../utils/studio-utils";
22
+
23
+ import type { FieldHandlers, SchemaProperty } from "../settings/schema-field-ui";
24
+ import type { SchemaFormControlArgs } from "./schema-form";
25
+
26
+ // ─── Schema-builder control ──────────────────────────────────────────────────
27
+
28
+ /** The JSON-Schema object shape the schema-builder edits. */
29
+ interface BuilderSchema {
30
+ type?: string;
31
+ properties?: Record<string, SchemaProperty>;
32
+ required?: string[];
33
+ }
34
+
35
+ interface AddFieldState {
36
+ format: string;
37
+ name: string;
38
+ required: boolean;
39
+ type: string;
40
+ }
41
+
42
+ const blankAddFieldState = (): AddFieldState => ({
43
+ format: "",
44
+ name: "",
45
+ required: false,
46
+ type: "string",
47
+ });
48
+
49
+ /** Per-field add-field form state, keyed by `${fieldKeyPrefix}.${key}`. */
50
+ const addFieldOpen = new Set<string>();
51
+ const addFieldStates = new Map<string, AddFieldState>();
52
+
53
+ /** Reset schema-builder ephemeral UI state (test hook). */
54
+ export function resetFormControlUiState(): void {
55
+ addFieldOpen.clear();
56
+ addFieldStates.clear();
57
+ }
58
+
59
+ /** Working clone of the schema value — JSON round-trip, as values may be reactive proxies. */
60
+ function cloneSchema(value: unknown): BuilderSchema {
61
+ const base =
62
+ value && typeof value === "object" && !Array.isArray(value)
63
+ ? value
64
+ : { properties: {}, required: [], type: "object" };
65
+ // oxlint-disable-next-line unicorn/prefer-structured-clone
66
+ return JSON.parse(JSON.stringify(base)) as BuilderSchema;
67
+ }
68
+
69
+ /** Visual JSON-Schema field editor wrapping the shared schema-field-ui templates. */
70
+ function schemaBuilderControl({ key, value, onChange, ctx, rerender }: SchemaFormControlArgs) {
71
+ const schema =
72
+ value && typeof value === "object" && !Array.isArray(value)
73
+ ? (value as BuilderSchema)
74
+ : ({ properties: {}, required: [], type: "object" } as BuilderSchema);
75
+ const properties = schema.properties ?? {};
76
+ const required = schema.required ?? [];
77
+ const stateKey = `${ctx.fieldKeyPrefix ?? ""}.${key}`;
78
+
79
+ const update = (mutate: (draft: BuilderSchema) => void) => {
80
+ const draft = cloneSchema(value);
81
+ draft.type ??= "object";
82
+ draft.properties ??= {};
83
+ draft.required ??= [];
84
+ mutate(draft);
85
+ onChange(draft);
86
+ rerender?.();
87
+ };
88
+
89
+ // Content types available as reference targets, resolved through the host context
90
+ const contentTypesValue = ctx.resolvePointer("#/$context/content");
91
+ const contentTypeNames =
92
+ contentTypesValue && typeof contentTypesValue === "object"
93
+ ? Object.keys(contentTypesValue)
94
+ : [];
95
+
96
+ const handlers: FieldHandlers = {
97
+ onAddNestedField: (parentName, fieldState) =>
98
+ update((draft) => {
99
+ const parent = draft.properties?.[parentName];
100
+ const name = toCamelCase(fieldState.name);
101
+ if (!parent || !name) {
102
+ return;
103
+ }
104
+ parent.properties ??= {};
105
+ parent.properties[name] = schemaForType(fieldState.type);
106
+ if (fieldState.required) {
107
+ parent.required ??= [];
108
+ if (!parent.required.includes(name)) {
109
+ parent.required.push(name);
110
+ }
111
+ }
112
+ }),
113
+ onChangeFormat: (name, format) =>
114
+ update((draft) => {
115
+ const prop = draft.properties?.[name];
116
+ if (!prop) {
117
+ return;
118
+ }
119
+ draft.properties![name] = schemaForType(prop.type || "string", format || undefined);
120
+ }),
121
+ onChangeNestedFormat: (parentName, childName, format) =>
122
+ update((draft) => {
123
+ const parent = draft.properties?.[parentName];
124
+ const prop = parent?.properties?.[childName];
125
+ if (!prop) {
126
+ return;
127
+ }
128
+ parent!.properties![childName] = schemaForType(prop.type || "string", format || undefined);
129
+ }),
130
+ onChangeNestedType: (parentName, childName, newType) =>
131
+ update((draft) => {
132
+ const parent = draft.properties?.[parentName];
133
+ const prop = parent?.properties?.[childName];
134
+ if (!prop) {
135
+ return;
136
+ }
137
+ const oldFormat =
138
+ newType === "string" || newType === "array" ? detectFieldFormat(prop) : undefined;
139
+ parent!.properties![childName] = schemaForType(newType, oldFormat || undefined);
140
+ }),
141
+ onChangeRefTarget: (name, target) =>
142
+ update((draft) => {
143
+ if (!draft.properties?.[name]) {
144
+ return;
145
+ }
146
+ draft.properties[name] = { $ref: `#/content/${target}` };
147
+ }),
148
+ onChangeType: (name, newType) =>
149
+ update((draft) => {
150
+ const prop = draft.properties?.[name];
151
+ if (!prop) {
152
+ return;
153
+ }
154
+ const oldFormat =
155
+ newType === "string" || newType === "array" ? detectFieldFormat(prop) : undefined;
156
+ draft.properties![name] = schemaForType(newType, oldFormat || undefined);
157
+ }),
158
+ onDelete: (name) =>
159
+ update((draft) => {
160
+ delete draft.properties?.[name];
161
+ draft.required = (draft.required ?? []).filter((r) => r !== name);
162
+ }),
163
+ onDeleteNested: (parentName, childName) =>
164
+ update((draft) => {
165
+ const parent = draft.properties?.[parentName];
166
+ if (!parent?.properties) {
167
+ return;
168
+ }
169
+ delete parent.properties[childName];
170
+ if (parent.required) {
171
+ parent.required = parent.required.filter((r) => r !== childName);
172
+ }
173
+ }),
174
+ onRename: (oldName, newName) =>
175
+ update((draft) => {
176
+ const normalized = toCamelCase(newName);
177
+ if (!draft.properties || !normalized || draft.properties[normalized]) {
178
+ return;
179
+ }
180
+ const next: Record<string, SchemaProperty> = {};
181
+ for (const [k, v] of Object.entries(draft.properties)) {
182
+ next[k === oldName ? normalized : k] = v;
183
+ }
184
+ draft.properties = next;
185
+ draft.required = (draft.required ?? []).map((r) => (r === oldName ? normalized : r));
186
+ }),
187
+ onRenameNested: (parentName, oldChild, newChild) =>
188
+ update((draft) => {
189
+ const parent = draft.properties?.[parentName];
190
+ const normalized = toCamelCase(newChild);
191
+ if (!parent?.properties || !normalized || parent.properties[normalized]) {
192
+ return;
193
+ }
194
+ const next: Record<string, SchemaProperty> = {};
195
+ for (const [k, v] of Object.entries(parent.properties)) {
196
+ next[k === oldChild ? normalized : k] = v;
197
+ }
198
+ parent.properties = next;
199
+ if (parent.required) {
200
+ parent.required = parent.required.map((r) => (r === oldChild ? normalized : r));
201
+ }
202
+ }),
203
+ onToggleNestedRequired: (parentName, childName) =>
204
+ update((draft) => {
205
+ const parent = draft.properties?.[parentName];
206
+ if (!parent) {
207
+ return;
208
+ }
209
+ parent.required ??= [];
210
+ const idx = parent.required.indexOf(childName);
211
+ if (idx === -1) {
212
+ parent.required.push(childName);
213
+ } else {
214
+ parent.required.splice(idx, 1);
215
+ }
216
+ }),
217
+ onToggleRequired: (name) =>
218
+ update((draft) => {
219
+ draft.required ??= [];
220
+ const idx = draft.required.indexOf(name);
221
+ if (idx === -1) {
222
+ draft.required.push(name);
223
+ } else {
224
+ draft.required.splice(idx, 1);
225
+ }
226
+ }),
227
+ };
228
+
229
+ const fieldCards = repeat(
230
+ Object.entries(properties),
231
+ ([name]) => name,
232
+ ([name, def]) => fieldCardTpl(name, def, required.includes(name), handlers, contentTypeNames),
233
+ );
234
+
235
+ const addFieldState = addFieldStates.get(stateKey) ?? blankAddFieldState();
236
+
237
+ return html`
238
+ <div class="schema-builder">
239
+ <div class="schema-field-list">${fieldCards}</div>
240
+ ${addFieldOpen.has(stateKey)
241
+ ? addFieldFormTpl(addFieldState, {
242
+ onCancel: () => {
243
+ addFieldOpen.delete(stateKey);
244
+ addFieldStates.delete(stateKey);
245
+ rerender?.();
246
+ },
247
+ onConfirm: () => {
248
+ const raw = addFieldState.name.trim();
249
+ const name = toCamelCase(raw);
250
+ if (!name) {
251
+ return;
252
+ }
253
+ // Close the form before update() rerenders with the committed value
254
+ addFieldOpen.delete(stateKey);
255
+ addFieldStates.delete(stateKey);
256
+ update((draft) => {
257
+ draft.properties![name] = schemaForType(
258
+ addFieldState.type,
259
+ addFieldState.format || undefined,
260
+ );
261
+ if (addFieldState.required && !draft.required!.includes(name)) {
262
+ draft.required!.push(name);
263
+ }
264
+ });
265
+ },
266
+ onInput: (field, val) => {
267
+ addFieldStates.set(stateKey, { ...addFieldState, [field]: val });
268
+ rerender?.();
269
+ },
270
+ })
271
+ : html`
272
+ <sp-action-button
273
+ size="s"
274
+ quiet
275
+ @click=${() => {
276
+ addFieldOpen.add(stateKey);
277
+ addFieldStates.set(stateKey, blankAddFieldState());
278
+ rerender?.();
279
+ }}
280
+ >
281
+ <sp-icon-add slot="icon"></sp-icon-add> Add Field
282
+ </sp-action-button>
283
+ `}
284
+ </div>
285
+ `;
286
+ }
287
+
288
+ registerFormControl("schema-builder", schemaBuilderControl);
289
+
290
+ // ─── Secret control ──────────────────────────────────────────────────────────
291
+
292
+ // Secret VALUES commit through the host's secret store (ctx.commitSecret → platform setSecrets),
293
+ // Never project.json; the field itself persists only the derived env-var NAME commitSecret
294
+ // Returns. Hosts without a secrets surface leave the control disabled.
295
+ registerFormControl("secret", ({ key, value, onChange, ctx, rerender }) => {
296
+ const { commitSecret } = ctx;
297
+ const envName = typeof value === "string" && value ? value : null;
298
+ return html`<sp-textfield
299
+ size="s"
300
+ type="password"
301
+ class="secret-field"
302
+ placeholder=${envName ? `Stored as ${envName}` : "Not set"}
303
+ ?disabled=${!commitSecret}
304
+ @change=${async (e: Event) => {
305
+ const target = e.target as HTMLInputElement;
306
+ const secretValue = target.value;
307
+ if (!commitSecret || !secretValue) {
308
+ return;
309
+ }
310
+ const name = await commitSecret(key, secretValue);
311
+ target.value = "";
312
+ if (typeof name === "string" && name && name !== envName) {
313
+ onChange(name);
314
+ } else {
315
+ rerender?.();
316
+ }
317
+ }}
318
+ ></sp-textfield>`;
319
+ });
320
+
321
+ // Keep an explicit export so hosts can assert the built-ins module loaded
322
+ export const builtinFormControls = ["schema-builder", "secret"] as const;
@@ -57,8 +57,8 @@ function errorView(title: string, message: string, onClose: () => void): Templat
57
57
  <pre
58
58
  style="font-size:var(--spectrum-font-size-50, 11px);white-space:pre-wrap;overflow:auto;max-height:240px;margin:0;color:var(--fg-dim,#aaa)"
59
59
  >
60
- ${message}</pre
61
- >
60
+ ${message}
61
+ </pre>
62
62
  </div>
63
63
  <div style="display:flex;justify-content:flex-end">
64
64
  <sp-button size="s" @click=${onClose}>Close</sp-button>