@jxsuite/studio 0.10.2 → 0.13.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/README.md +82 -0
- package/dist/studio.js +5114 -3424
- package/dist/studio.js.map +65 -49
- package/package.json +6 -3
- package/src/browse/browse.js +27 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +136 -17
- package/src/canvas/canvas-render.js +154 -21
- package/src/canvas/canvas-utils.js +21 -23
- package/src/editor/component-inline-edit.js +54 -41
- package/src/editor/content-inline-edit.js +46 -47
- package/src/editor/context-menu.js +63 -39
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -10
- package/src/editor/shortcuts.js +69 -39
- package/src/files/components.js +15 -4
- package/src/files/files.js +72 -24
- package/src/panels/activity-bar.js +29 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/canvas-dnd.js +132 -50
- package/src/panels/dnd.js +32 -28
- package/src/panels/editors.js +7 -14
- package/src/panels/elements-panel.js +9 -3
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +45 -3
- package/src/panels/layers-panel.js +16 -11
- package/src/panels/left-panel.js +108 -43
- package/src/panels/overlays.js +97 -35
- package/src/panels/panel-events.js +18 -11
- package/src/panels/properties-panel.js +467 -98
- package/src/panels/right-panel.js +85 -37
- package/src/panels/shared.js +0 -22
- package/src/panels/signals-panel.js +125 -54
- package/src/panels/statusbar.js +23 -8
- package/src/panels/style-inputs.js +4 -2
- package/src/panels/style-panel.js +128 -105
- package/src/panels/stylebook-panel.js +42 -17
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +39 -9
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +78 -8
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +99 -11
- package/src/site-context.js +105 -0
- package/src/state.js +0 -456
- package/src/store.js +105 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/spectrum.js +2 -0
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +89 -0
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { html, nothing } from "lit-html";
|
|
7
7
|
|
|
8
|
-
export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "
|
|
8
|
+
export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "reference"];
|
|
9
|
+
|
|
10
|
+
export const FORMAT_OPTIONS = ["", "image", "date", "color"];
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* @typedef {{
|
|
@@ -14,6 +16,7 @@ export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "d
|
|
|
14
16
|
* required?: string[];
|
|
15
17
|
* items?: any;
|
|
16
18
|
* format?: string;
|
|
19
|
+
* $ref?: string;
|
|
17
20
|
* }} SchemaProperty
|
|
18
21
|
*/
|
|
19
22
|
|
|
@@ -23,6 +26,8 @@ export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "d
|
|
|
23
26
|
* onToggleRequired: (name: string) => void;
|
|
24
27
|
* onRename: (oldName: string, newName: string) => void;
|
|
25
28
|
* onChangeType: (name: string, newType: string) => void;
|
|
29
|
+
* onChangeFormat?: (name: string, format: string) => void;
|
|
30
|
+
* onChangeRefTarget?: (name: string, target: string) => void;
|
|
26
31
|
* onAddNestedField?: (
|
|
27
32
|
* parentName: string,
|
|
28
33
|
* state: { name: string; type: string; required: boolean },
|
|
@@ -31,9 +36,32 @@ export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "d
|
|
|
31
36
|
* onToggleNestedRequired?: (parentName: string, childName: string) => void;
|
|
32
37
|
* onRenameNested?: (parentName: string, oldChild: string, newChild: string) => void;
|
|
33
38
|
* onChangeNestedType?: (parentName: string, childName: string, newType: string) => void;
|
|
39
|
+
* onChangeNestedFormat?: (parentName: string, childName: string, format: string) => void;
|
|
34
40
|
* }} FieldHandlers
|
|
35
41
|
*/
|
|
36
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Detect the studio field type from a JSON Schema property definition.
|
|
45
|
+
*
|
|
46
|
+
* @param {SchemaProperty} schema
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function detectFieldType(schema) {
|
|
50
|
+
if (schema.$ref) return "reference";
|
|
51
|
+
return schema.type || "string";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detect the format from a JSON Schema property definition.
|
|
56
|
+
*
|
|
57
|
+
* @param {SchemaProperty} schema
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function detectFieldFormat(schema) {
|
|
61
|
+
if (schema.type === "array" && schema.items?.format) return schema.items.format;
|
|
62
|
+
return schema.format || "";
|
|
63
|
+
}
|
|
64
|
+
|
|
37
65
|
/**
|
|
38
66
|
* Render a single schema field as an inline-editable form row.
|
|
39
67
|
*
|
|
@@ -41,13 +69,17 @@ export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "d
|
|
|
41
69
|
* @param {SchemaProperty} fieldSchema — JSON Schema property definition
|
|
42
70
|
* @param {boolean} isRequired
|
|
43
71
|
* @param {FieldHandlers} handlers
|
|
72
|
+
* @param {string[]} [contentTypeNames] - Available content type names for reference target picker
|
|
44
73
|
* @returns {any}
|
|
45
74
|
*/
|
|
46
|
-
export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers) {
|
|
47
|
-
const type = fieldSchema
|
|
75
|
+
export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, contentTypeNames = []) {
|
|
76
|
+
const type = detectFieldType(fieldSchema);
|
|
77
|
+
const format = detectFieldFormat(fieldSchema);
|
|
48
78
|
const isNested = type === "object";
|
|
79
|
+
const isRef = type === "reference";
|
|
49
80
|
const nestedProps = fieldSchema.properties || {};
|
|
50
81
|
const nestedRequired = fieldSchema.required || [];
|
|
82
|
+
const refTarget = fieldSchema.$ref ? fieldSchema.$ref.replace("#/contentTypes/", "") : "";
|
|
51
83
|
|
|
52
84
|
return html`
|
|
53
85
|
<div class="schema-field-card">
|
|
@@ -71,6 +103,11 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers) {
|
|
|
71
103
|
}}
|
|
72
104
|
></sp-textfield>
|
|
73
105
|
${typePickerTpl(type, (newType) => handlers.onChangeType(fieldName, newType))}
|
|
106
|
+
${type === "string" || type === "array"
|
|
107
|
+
? formatPickerTpl(format, (f) => {
|
|
108
|
+
if (handlers.onChangeFormat) handlers.onChangeFormat(fieldName, f);
|
|
109
|
+
})
|
|
110
|
+
: nothing}
|
|
74
111
|
<sp-switch
|
|
75
112
|
size="s"
|
|
76
113
|
?checked=${isRequired}
|
|
@@ -87,6 +124,23 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers) {
|
|
|
87
124
|
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
88
125
|
</sp-action-button>
|
|
89
126
|
</div>
|
|
127
|
+
${isRef && contentTypeNames.length
|
|
128
|
+
? html`
|
|
129
|
+
<div class="schema-field-ref-target">
|
|
130
|
+
<sp-picker
|
|
131
|
+
size="s"
|
|
132
|
+
label="Target"
|
|
133
|
+
value=${refTarget}
|
|
134
|
+
@change=${(/** @type {any} */ e) => {
|
|
135
|
+
if (handlers.onChangeRefTarget)
|
|
136
|
+
handlers.onChangeRefTarget(fieldName, e.target.value);
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
${contentTypeNames.map((n) => html`<sp-menu-item value=${n}>${n}</sp-menu-item>`)}
|
|
140
|
+
</sp-picker>
|
|
141
|
+
</div>
|
|
142
|
+
`
|
|
143
|
+
: nothing}
|
|
90
144
|
${isNested
|
|
91
145
|
? html`
|
|
92
146
|
<div class="schema-field-nested">
|
|
@@ -119,7 +173,8 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers) {
|
|
|
119
173
|
* @returns {any}
|
|
120
174
|
*/
|
|
121
175
|
function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, handlers) {
|
|
122
|
-
const type = childSchema
|
|
176
|
+
const type = detectFieldType(childSchema);
|
|
177
|
+
const format = detectFieldFormat(childSchema);
|
|
123
178
|
|
|
124
179
|
return html`
|
|
125
180
|
<div class="schema-field-card schema-field-card--nested">
|
|
@@ -149,6 +204,12 @@ function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, hand
|
|
|
149
204
|
if (handlers.onChangeNestedType)
|
|
150
205
|
handlers.onChangeNestedType(parentName, childName, newType);
|
|
151
206
|
})}
|
|
207
|
+
${type === "string" || type === "array"
|
|
208
|
+
? formatPickerTpl(format, (f) => {
|
|
209
|
+
if (handlers.onChangeNestedFormat)
|
|
210
|
+
handlers.onChangeNestedFormat(parentName, childName, f);
|
|
211
|
+
})
|
|
212
|
+
: nothing}
|
|
152
213
|
<sp-switch
|
|
153
214
|
size="s"
|
|
154
215
|
?checked=${isRequired}
|
|
@@ -244,10 +305,30 @@ export function typePickerTpl(value, onChange) {
|
|
|
244
305
|
`;
|
|
245
306
|
}
|
|
246
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Render the format picker as an sp-picker dropdown.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} value
|
|
312
|
+
* @param {(format: string) => void} onChange
|
|
313
|
+
* @returns {any}
|
|
314
|
+
*/
|
|
315
|
+
export function formatPickerTpl(value, onChange) {
|
|
316
|
+
return html`
|
|
317
|
+
<sp-picker
|
|
318
|
+
size="s"
|
|
319
|
+
label="Format"
|
|
320
|
+
value=${value}
|
|
321
|
+
@change=${(/** @type {any} */ e) => onChange(e.target.value)}
|
|
322
|
+
>
|
|
323
|
+
${FORMAT_OPTIONS.map((f) => html`<sp-menu-item value=${f}>${f || "(none)"}</sp-menu-item>`)}
|
|
324
|
+
</sp-picker>
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
|
|
247
328
|
/**
|
|
248
329
|
* Render the add-field form (inline, not a dialog).
|
|
249
330
|
*
|
|
250
|
-
* @param {{ name: string; type: string; required: boolean }} state
|
|
331
|
+
* @param {{ name: string; type: string; format: string; required: boolean }} state
|
|
251
332
|
* @param {{
|
|
252
333
|
* onInput: (field: string, value: any) => void;
|
|
253
334
|
* onConfirm: () => void;
|
|
@@ -269,6 +350,9 @@ export function addFieldFormTpl(state, handlers) {
|
|
|
269
350
|
}}
|
|
270
351
|
></sp-textfield>
|
|
271
352
|
${typePickerTpl(state.type, (t) => handlers.onInput("type", t))}
|
|
353
|
+
${state.type === "string" || state.type === "array"
|
|
354
|
+
? formatPickerTpl(state.format || "", (f) => handlers.onInput("format", f))
|
|
355
|
+
: nothing}
|
|
272
356
|
<sp-switch
|
|
273
357
|
size="s"
|
|
274
358
|
?checked=${state.required}
|
|
@@ -283,25 +367,28 @@ export function addFieldFormTpl(state, handlers) {
|
|
|
283
367
|
}
|
|
284
368
|
|
|
285
369
|
/**
|
|
286
|
-
* Build a JSON Schema property definition from a type
|
|
370
|
+
* Build a JSON Schema property definition from a type and optional format.
|
|
287
371
|
*
|
|
288
372
|
* @param {string} type
|
|
373
|
+
* @param {string} [format]
|
|
289
374
|
* @returns {object}
|
|
290
375
|
*/
|
|
291
|
-
export function schemaForType(type) {
|
|
376
|
+
export function schemaForType(type, format) {
|
|
292
377
|
switch (type) {
|
|
293
378
|
case "number":
|
|
294
379
|
return { type: "number" };
|
|
295
380
|
case "boolean":
|
|
296
381
|
return { type: "boolean" };
|
|
297
382
|
case "array":
|
|
298
|
-
return
|
|
383
|
+
return format
|
|
384
|
+
? { type: "array", items: { type: "string", format } }
|
|
385
|
+
: { type: "array", items: { type: "string" } };
|
|
299
386
|
case "object":
|
|
300
387
|
return { type: "object", properties: {}, required: [] };
|
|
301
|
-
case "
|
|
302
|
-
return {
|
|
388
|
+
case "reference":
|
|
389
|
+
return { $ref: "#/contentTypes/" };
|
|
303
390
|
default:
|
|
304
|
-
return { type: "string" };
|
|
391
|
+
return format ? { type: "string", format } : { type: "string" };
|
|
305
392
|
}
|
|
306
393
|
}
|
|
307
394
|
|
|
@@ -314,6 +401,7 @@ export function schemaForType(type) {
|
|
|
314
401
|
*/
|
|
315
402
|
export function yamlDefault(type, format) {
|
|
316
403
|
if (format === "date") return new Date().toISOString().split("T")[0];
|
|
404
|
+
if (format === "image") return '""';
|
|
317
405
|
switch (type) {
|
|
318
406
|
case "boolean":
|
|
319
407
|
return "false";
|
package/src/site-context.js
CHANGED
|
@@ -109,6 +109,111 @@ export function getEffectiveHead(docHead) {
|
|
|
109
109
|
return merged;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// ─── Layout resolution ──────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/** @type {Map<string, any>} */
|
|
115
|
+
const layoutCache = new Map();
|
|
116
|
+
|
|
117
|
+
export function invalidateLayoutCache() {
|
|
118
|
+
layoutCache.clear();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Determine the effective layout path for a document.
|
|
123
|
+
*
|
|
124
|
+
* @param {any} docLayout - The document's $layout value (string path, false, or undefined)
|
|
125
|
+
* @returns {string | null} The layout path, or null if no layout applies
|
|
126
|
+
*/
|
|
127
|
+
export function getEffectiveLayoutPath(docLayout) {
|
|
128
|
+
if (docLayout === false) return null;
|
|
129
|
+
const defaultLayout = projectState?.projectConfig?.defaults?.layout;
|
|
130
|
+
return docLayout || defaultLayout || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a layout document by path. Fetches and caches the parsed JSON.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} layoutPath - Relative path to the layout file (e.g., "./layouts/base.json")
|
|
137
|
+
* @returns {Promise<any | null>} The parsed layout document, or null on failure
|
|
138
|
+
*/
|
|
139
|
+
export async function resolveLayoutDoc(layoutPath) {
|
|
140
|
+
const normalized = layoutPath.replace(/^\.\//, "");
|
|
141
|
+
if (layoutCache.has(normalized)) return structuredClone(layoutCache.get(normalized));
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const platform = getPlatform();
|
|
145
|
+
const content = await platform.readFile(normalized);
|
|
146
|
+
const doc = JSON.parse(content);
|
|
147
|
+
layoutCache.set(normalized, doc);
|
|
148
|
+
return structuredClone(doc);
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Distribute page children into a layout document's <slot> elements. Returns the merged document
|
|
156
|
+
* with page content injected into slots.
|
|
157
|
+
*
|
|
158
|
+
* @param {any} layoutDoc - Deep-cloned layout document
|
|
159
|
+
* @param {any} pageDoc - The page document
|
|
160
|
+
* @returns {any} The merged document
|
|
161
|
+
*/
|
|
162
|
+
export function distributePageIntoLayout(layoutDoc, pageDoc) {
|
|
163
|
+
const pageChildren = pageDoc.children ?? [];
|
|
164
|
+
const children = typeof pageChildren === "string" ? [pageChildren] : pageChildren;
|
|
165
|
+
|
|
166
|
+
const named = new Map();
|
|
167
|
+
const defaults = [];
|
|
168
|
+
|
|
169
|
+
for (const child of children) {
|
|
170
|
+
if (child && typeof child === "object" && child.attributes?.slot) {
|
|
171
|
+
const slotName = child.attributes.slot;
|
|
172
|
+
if (!named.has(slotName)) named.set(slotName, []);
|
|
173
|
+
named.get(slotName).push(child);
|
|
174
|
+
} else {
|
|
175
|
+
defaults.push(child);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fillSlots(layoutDoc, named, defaults);
|
|
180
|
+
|
|
181
|
+
if (pageDoc.state) layoutDoc.state = { ...layoutDoc.state, ...pageDoc.state };
|
|
182
|
+
if (pageDoc.$media) layoutDoc.$media = { ...layoutDoc.$media, ...pageDoc.$media };
|
|
183
|
+
if (pageDoc.style) layoutDoc.style = { ...layoutDoc.style, ...pageDoc.style };
|
|
184
|
+
if (pageDoc.attributes) layoutDoc.attributes = { ...layoutDoc.attributes, ...pageDoc.attributes };
|
|
185
|
+
|
|
186
|
+
return layoutDoc;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function fillSlots(
|
|
190
|
+
/** @type {any} */ node,
|
|
191
|
+
/** @type {Map<string, any[]>} */ named,
|
|
192
|
+
/** @type {any[]} */ defaults,
|
|
193
|
+
) {
|
|
194
|
+
if (!node || typeof node !== "object") return;
|
|
195
|
+
if (!Array.isArray(node.children)) return;
|
|
196
|
+
|
|
197
|
+
const newChildren = [];
|
|
198
|
+
for (const child of node.children) {
|
|
199
|
+
if (child && typeof child === "object" && child.tagName === "slot") {
|
|
200
|
+
const slotName = child.attributes?.name;
|
|
201
|
+
if (slotName && named.has(slotName)) {
|
|
202
|
+
newChildren.push(.../** @type {any[]} */ (named.get(slotName)));
|
|
203
|
+
named.delete(slotName);
|
|
204
|
+
} else if (!slotName && defaults.length > 0) {
|
|
205
|
+
newChildren.push(...defaults);
|
|
206
|
+
} else if (child.children) {
|
|
207
|
+
newChildren.push(...child.children);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
fillSlots(child, named, defaults);
|
|
211
|
+
newChildren.push(child);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
node.children = newChildren;
|
|
215
|
+
}
|
|
216
|
+
|
|
112
217
|
/**
|
|
113
218
|
* Update the project's project.json with a partial patch and persist to disk.
|
|
114
219
|
*
|