@jxsuite/studio 0.11.0 → 0.14.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/dist/studio.js +6517 -5870
- package/dist/studio.js.map +57 -51
- package/package.json +5 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +28 -2
- package/src/canvas/canvas-render.js +170 -20
- package/src/canvas/canvas-utils.js +22 -25
- package/src/editor/component-inline-edit.js +55 -44
- package/src/editor/content-inline-edit.js +47 -50
- package/src/editor/context-menu.js +78 -53
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -11
- package/src/editor/shortcuts.js +88 -64
- package/src/files/components.js +15 -4
- package/src/files/file-ops.js +57 -108
- package/src/files/files.js +81 -28
- package/src/panels/activity-bar.js +31 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/canvas-dnd.js +4 -10
- package/src/panels/dnd.js +77 -35
- package/src/panels/editors.js +17 -26
- package/src/panels/elements-panel.js +17 -11
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +47 -4
- package/src/panels/layers-panel.js +25 -21
- package/src/panels/left-panel.js +109 -44
- package/src/panels/overlays.js +91 -41
- package/src/panels/panel-events.js +28 -37
- package/src/panels/preview-render.js +7 -3
- package/src/panels/properties-panel.js +179 -104
- package/src/panels/pseudo-preview.js +9 -8
- 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 +26 -19
- package/src/panels/style-inputs.js +5 -4
- package/src/panels/style-panel.js +128 -105
- package/src/panels/style-utils.js +8 -6
- package/src/panels/stylebook-layers-panel.js +5 -5
- package/src/panels/stylebook-panel.js +27 -31
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +40 -10
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +56 -7
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +60 -25
- package/src/state.js +2 -459
- package/src/store.js +61 -219
- package/src/studio.js +163 -300
- package/src/tabs/tab.js +168 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/color-selector.js +4 -5
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +90 -0
|
@@ -5,17 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { html, nothing } from "lit-html";
|
|
7
7
|
|
|
8
|
-
export const FIELD_TYPES = [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"boolean",
|
|
12
|
-
"array",
|
|
13
|
-
"object",
|
|
14
|
-
"date",
|
|
15
|
-
"image",
|
|
16
|
-
"gallery",
|
|
17
|
-
"reference",
|
|
18
|
-
];
|
|
8
|
+
export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "reference"];
|
|
9
|
+
|
|
10
|
+
export const FORMAT_OPTIONS = ["", "image", "date", "color"];
|
|
19
11
|
|
|
20
12
|
/**
|
|
21
13
|
* @typedef {{
|
|
@@ -34,6 +26,7 @@ export const FIELD_TYPES = [
|
|
|
34
26
|
* onToggleRequired: (name: string) => void;
|
|
35
27
|
* onRename: (oldName: string, newName: string) => void;
|
|
36
28
|
* onChangeType: (name: string, newType: string) => void;
|
|
29
|
+
* onChangeFormat?: (name: string, format: string) => void;
|
|
37
30
|
* onChangeRefTarget?: (name: string, target: string) => void;
|
|
38
31
|
* onAddNestedField?: (
|
|
39
32
|
* parentName: string,
|
|
@@ -43,6 +36,7 @@ export const FIELD_TYPES = [
|
|
|
43
36
|
* onToggleNestedRequired?: (parentName: string, childName: string) => void;
|
|
44
37
|
* onRenameNested?: (parentName: string, oldChild: string, newChild: string) => void;
|
|
45
38
|
* onChangeNestedType?: (parentName: string, childName: string, newType: string) => void;
|
|
39
|
+
* onChangeNestedFormat?: (parentName: string, childName: string, format: string) => void;
|
|
46
40
|
* }} FieldHandlers
|
|
47
41
|
*/
|
|
48
42
|
|
|
@@ -54,12 +48,20 @@ export const FIELD_TYPES = [
|
|
|
54
48
|
*/
|
|
55
49
|
export function detectFieldType(schema) {
|
|
56
50
|
if (schema.$ref) return "reference";
|
|
57
|
-
if (schema.format === "image") return "image";
|
|
58
|
-
if (schema.format === "date") return "date";
|
|
59
|
-
if (schema.type === "array" && schema.items?.format === "image") return "gallery";
|
|
60
51
|
return schema.type || "string";
|
|
61
52
|
}
|
|
62
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
|
+
|
|
63
65
|
/**
|
|
64
66
|
* Render a single schema field as an inline-editable form row.
|
|
65
67
|
*
|
|
@@ -72,6 +74,7 @@ export function detectFieldType(schema) {
|
|
|
72
74
|
*/
|
|
73
75
|
export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, contentTypeNames = []) {
|
|
74
76
|
const type = detectFieldType(fieldSchema);
|
|
77
|
+
const format = detectFieldFormat(fieldSchema);
|
|
75
78
|
const isNested = type === "object";
|
|
76
79
|
const isRef = type === "reference";
|
|
77
80
|
const nestedProps = fieldSchema.properties || {};
|
|
@@ -100,6 +103,11 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, conte
|
|
|
100
103
|
}}
|
|
101
104
|
></sp-textfield>
|
|
102
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}
|
|
103
111
|
<sp-switch
|
|
104
112
|
size="s"
|
|
105
113
|
?checked=${isRequired}
|
|
@@ -166,6 +174,7 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, conte
|
|
|
166
174
|
*/
|
|
167
175
|
function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, handlers) {
|
|
168
176
|
const type = detectFieldType(childSchema);
|
|
177
|
+
const format = detectFieldFormat(childSchema);
|
|
169
178
|
|
|
170
179
|
return html`
|
|
171
180
|
<div class="schema-field-card schema-field-card--nested">
|
|
@@ -195,6 +204,12 @@ function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, hand
|
|
|
195
204
|
if (handlers.onChangeNestedType)
|
|
196
205
|
handlers.onChangeNestedType(parentName, childName, newType);
|
|
197
206
|
})}
|
|
207
|
+
${type === "string" || type === "array"
|
|
208
|
+
? formatPickerTpl(format, (f) => {
|
|
209
|
+
if (handlers.onChangeNestedFormat)
|
|
210
|
+
handlers.onChangeNestedFormat(parentName, childName, f);
|
|
211
|
+
})
|
|
212
|
+
: nothing}
|
|
198
213
|
<sp-switch
|
|
199
214
|
size="s"
|
|
200
215
|
?checked=${isRequired}
|
|
@@ -290,10 +305,30 @@ export function typePickerTpl(value, onChange) {
|
|
|
290
305
|
`;
|
|
291
306
|
}
|
|
292
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
|
+
|
|
293
328
|
/**
|
|
294
329
|
* Render the add-field form (inline, not a dialog).
|
|
295
330
|
*
|
|
296
|
-
* @param {{ name: string; type: string; required: boolean }} state
|
|
331
|
+
* @param {{ name: string; type: string; format: string; required: boolean }} state
|
|
297
332
|
* @param {{
|
|
298
333
|
* onInput: (field: string, value: any) => void;
|
|
299
334
|
* onConfirm: () => void;
|
|
@@ -315,6 +350,9 @@ export function addFieldFormTpl(state, handlers) {
|
|
|
315
350
|
}}
|
|
316
351
|
></sp-textfield>
|
|
317
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}
|
|
318
356
|
<sp-switch
|
|
319
357
|
size="s"
|
|
320
358
|
?checked=${state.required}
|
|
@@ -329,31 +367,28 @@ export function addFieldFormTpl(state, handlers) {
|
|
|
329
367
|
}
|
|
330
368
|
|
|
331
369
|
/**
|
|
332
|
-
* Build a JSON Schema property definition from a type
|
|
370
|
+
* Build a JSON Schema property definition from a type and optional format.
|
|
333
371
|
*
|
|
334
372
|
* @param {string} type
|
|
373
|
+
* @param {string} [format]
|
|
335
374
|
* @returns {object}
|
|
336
375
|
*/
|
|
337
|
-
export function schemaForType(type) {
|
|
376
|
+
export function schemaForType(type, format) {
|
|
338
377
|
switch (type) {
|
|
339
378
|
case "number":
|
|
340
379
|
return { type: "number" };
|
|
341
380
|
case "boolean":
|
|
342
381
|
return { type: "boolean" };
|
|
343
382
|
case "array":
|
|
344
|
-
return
|
|
383
|
+
return format
|
|
384
|
+
? { type: "array", items: { type: "string", format } }
|
|
385
|
+
: { type: "array", items: { type: "string" } };
|
|
345
386
|
case "object":
|
|
346
387
|
return { type: "object", properties: {}, required: [] };
|
|
347
|
-
case "date":
|
|
348
|
-
return { type: "string", format: "date" };
|
|
349
|
-
case "image":
|
|
350
|
-
return { type: "string", format: "image" };
|
|
351
|
-
case "gallery":
|
|
352
|
-
return { type: "array", items: { type: "string", format: "image" } };
|
|
353
388
|
case "reference":
|
|
354
389
|
return { $ref: "#/contentTypes/" };
|
|
355
390
|
default:
|
|
356
|
-
return { type: "string" };
|
|
391
|
+
return format ? { type: "string", format } : { type: "string" };
|
|
357
392
|
}
|
|
358
393
|
}
|
|
359
394
|
|
package/src/state.js
CHANGED
|
@@ -33,8 +33,6 @@
|
|
|
33
33
|
* }} StudioState
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
const HISTORY_LIMIT = 100;
|
|
37
|
-
|
|
38
36
|
// ─── Path utilities ───────────────────────────────────────────────────────────
|
|
39
37
|
|
|
40
38
|
/**
|
|
@@ -213,7 +211,6 @@ export function createState(doc) {
|
|
|
213
211
|
mode: "component", // 'component' | 'content'
|
|
214
212
|
content: { frontmatter: {} }, // frontmatter metadata for .md files
|
|
215
213
|
ui: {
|
|
216
|
-
leftTab: "layers", // 'files' | 'layers' | 'blocks' | 'state' | 'data'
|
|
217
214
|
rightTab: "properties", // 'properties' | 'events' | 'style'
|
|
218
215
|
zoom: 1,
|
|
219
216
|
activeMedia: null, // '--md' | null (base) — focused canvas/breakpoint
|
|
@@ -335,31 +332,6 @@ export function updateFrontmatter(state, field, value) {
|
|
|
335
332
|
};
|
|
336
333
|
}
|
|
337
334
|
|
|
338
|
-
// ─── Core mutation ────────────────────────────────────────────────────────────
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Apply a mutation to the document. Clones the document immutably, applies the mutation function to
|
|
342
|
-
* the clone, and pushes to history.
|
|
343
|
-
*
|
|
344
|
-
* @param {StudioState} state
|
|
345
|
-
* @param {(doc: any) => void} mutationFn
|
|
346
|
-
* @returns {StudioState}
|
|
347
|
-
*/
|
|
348
|
-
export function applyMutation(state, mutationFn) {
|
|
349
|
-
const newDoc = structuredClone(state.document);
|
|
350
|
-
mutationFn(newDoc);
|
|
351
|
-
const truncated = state.history.slice(0, state.historyIndex + 1);
|
|
352
|
-
truncated.push({ document: newDoc, selection: state.selection });
|
|
353
|
-
if (truncated.length > HISTORY_LIMIT) truncated.shift();
|
|
354
|
-
return {
|
|
355
|
-
...state,
|
|
356
|
-
document: newDoc,
|
|
357
|
-
history: truncated,
|
|
358
|
-
historyIndex: truncated.length - 1,
|
|
359
|
-
dirty: true,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
335
|
// ─── Selection / hover ────────────────────────────────────────────────────────
|
|
364
336
|
|
|
365
337
|
/**
|
|
@@ -380,366 +352,6 @@ export function hoverNode(state, path) {
|
|
|
380
352
|
return { ...state, hover: path };
|
|
381
353
|
}
|
|
382
354
|
|
|
383
|
-
// ─── Undo / redo ──────────────────────────────────────────────────────────────
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* @param {StudioState} state
|
|
387
|
-
* @returns {StudioState}
|
|
388
|
-
*/
|
|
389
|
-
export function undo(state) {
|
|
390
|
-
if (state.historyIndex <= 0) return state;
|
|
391
|
-
const idx = state.historyIndex - 1;
|
|
392
|
-
const snap = state.history[idx];
|
|
393
|
-
return {
|
|
394
|
-
...state,
|
|
395
|
-
document: snap.document,
|
|
396
|
-
selection: snap.selection,
|
|
397
|
-
historyIndex: idx,
|
|
398
|
-
dirty: true,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* @param {StudioState} state
|
|
404
|
-
* @returns {StudioState}
|
|
405
|
-
*/
|
|
406
|
-
export function redo(state) {
|
|
407
|
-
if (state.historyIndex >= state.history.length - 1) return state;
|
|
408
|
-
const idx = state.historyIndex + 1;
|
|
409
|
-
const snap = state.history[idx];
|
|
410
|
-
return {
|
|
411
|
-
...state,
|
|
412
|
-
document: snap.document,
|
|
413
|
-
selection: snap.selection,
|
|
414
|
-
historyIndex: idx,
|
|
415
|
-
dirty: true,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// ─── Document mutations ───────────────────────────────────────────────────────
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* @param {StudioState} state
|
|
423
|
-
* @param {JxPath} parentPath
|
|
424
|
-
* @param {number} index
|
|
425
|
-
* @param {any} nodeDef
|
|
426
|
-
* @returns {StudioState}
|
|
427
|
-
*/
|
|
428
|
-
export function insertNode(state, parentPath, index, nodeDef) {
|
|
429
|
-
return applyMutation(state, (doc) => {
|
|
430
|
-
const parent = getNodeAtPath(doc, parentPath);
|
|
431
|
-
if (!parent.children) parent.children = [];
|
|
432
|
-
parent.children.splice(index, 0, nodeDef);
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* @param {StudioState} state
|
|
438
|
-
* @param {JxPath} path
|
|
439
|
-
* @returns {StudioState}
|
|
440
|
-
*/
|
|
441
|
-
export function removeNode(state, path) {
|
|
442
|
-
if (!path || path.length < 2) return state; // can't remove root
|
|
443
|
-
const elemPath = parentElementPath(path);
|
|
444
|
-
const idx = childIndex(path);
|
|
445
|
-
const newState = applyMutation(state, (doc) => {
|
|
446
|
-
getNodeAtPath(doc, /** @type {JxPath} */ (elemPath)).children.splice(idx, 1);
|
|
447
|
-
});
|
|
448
|
-
// Clear selection if we removed the selected node
|
|
449
|
-
if (state.selection && isAncestor(path, state.selection)) {
|
|
450
|
-
return { ...newState, selection: null };
|
|
451
|
-
}
|
|
452
|
-
return newState;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* @param {StudioState} state
|
|
457
|
-
* @param {JxPath} path
|
|
458
|
-
* @returns {StudioState}
|
|
459
|
-
*/
|
|
460
|
-
export function duplicateNode(state, path) {
|
|
461
|
-
if (!path || path.length < 2) return state;
|
|
462
|
-
const node = getNodeAtPath(state.document, path);
|
|
463
|
-
if (!node) return state;
|
|
464
|
-
const elemPath = /** @type {JxPath} */ (parentElementPath(path));
|
|
465
|
-
const idx = /** @type {number} */ (childIndex(path));
|
|
466
|
-
const newState = insertNode(state, elemPath, idx + 1, structuredClone(node));
|
|
467
|
-
return selectNode(newState, [...elemPath, "children", idx + 1]);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Wrap the node at `path` in a new wrapper element (e.g. a div).
|
|
472
|
-
*
|
|
473
|
-
* @param {StudioState} state
|
|
474
|
-
* @param {JxPath} path
|
|
475
|
-
* @param {string} wrapperTag
|
|
476
|
-
* @returns {StudioState}
|
|
477
|
-
*/
|
|
478
|
-
export function wrapNode(state, path, wrapperTag = "div") {
|
|
479
|
-
if (!path || path.length < 2) return state;
|
|
480
|
-
const node = getNodeAtPath(state.document, path);
|
|
481
|
-
if (!node) return state;
|
|
482
|
-
const elemPath = /** @type {JxPath} */ (parentElementPath(path));
|
|
483
|
-
const idx = /** @type {number} */ (childIndex(path));
|
|
484
|
-
const wrapper = { tagName: wrapperTag, children: [structuredClone(node)] };
|
|
485
|
-
const newState = applyMutation(state, (doc) => {
|
|
486
|
-
const parent = getNodeAtPath(doc, elemPath);
|
|
487
|
-
parent.children.splice(idx, 1, wrapper);
|
|
488
|
-
});
|
|
489
|
-
return selectNode(newState, [...elemPath, "children", idx]);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* @param {StudioState} state
|
|
494
|
-
* @param {JxPath} fromPath
|
|
495
|
-
* @param {JxPath} toParentPath
|
|
496
|
-
* @param {number} toIndex
|
|
497
|
-
* @returns {StudioState}
|
|
498
|
-
*/
|
|
499
|
-
export function moveNode(state, fromPath, toParentPath, toIndex) {
|
|
500
|
-
const newState = applyMutation(state, (doc) => {
|
|
501
|
-
const fromParentPath = /** @type {JxPath} */ (parentElementPath(fromPath));
|
|
502
|
-
const fromParent = getNodeAtPath(doc, fromParentPath);
|
|
503
|
-
const fromIdx = childIndex(fromPath);
|
|
504
|
-
const [node] = fromParent.children.splice(fromIdx, 1);
|
|
505
|
-
const toParent = getNodeAtPath(doc, toParentPath);
|
|
506
|
-
if (!toParent.children) toParent.children = [];
|
|
507
|
-
// Adjust target index if moving within the same parent and source was before target
|
|
508
|
-
let adjustedIndex = toIndex;
|
|
509
|
-
if (fromParent === toParent && /** @type {number} */ (fromIdx) < toIndex) {
|
|
510
|
-
adjustedIndex--;
|
|
511
|
-
}
|
|
512
|
-
toParent.children.splice(adjustedIndex, 0, node);
|
|
513
|
-
});
|
|
514
|
-
// Update selection to follow the moved node
|
|
515
|
-
if (pathsEqual(newState.selection, fromPath)) {
|
|
516
|
-
let adjustedIdx = toIndex;
|
|
517
|
-
// Adjust if same parent and source was before target
|
|
518
|
-
const fromParentPath = /** @type {JxPath} */ (parentElementPath(fromPath));
|
|
519
|
-
const fromIdx = childIndex(fromPath);
|
|
520
|
-
if (
|
|
521
|
-
fromParentPath.length === toParentPath.length &&
|
|
522
|
-
fromParentPath.every((v, i) => v === toParentPath[i]) &&
|
|
523
|
-
/** @type {number} */ (fromIdx) < toIndex
|
|
524
|
-
) {
|
|
525
|
-
adjustedIdx = toIndex - 1;
|
|
526
|
-
}
|
|
527
|
-
newState.selection = [...toParentPath, "children", adjustedIdx];
|
|
528
|
-
}
|
|
529
|
-
return newState;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* @param {StudioState} state
|
|
534
|
-
* @param {JxPath} path
|
|
535
|
-
* @param {string} key
|
|
536
|
-
* @param {any} value
|
|
537
|
-
* @returns {StudioState}
|
|
538
|
-
*/
|
|
539
|
-
export function updateProperty(state, path, key, value) {
|
|
540
|
-
return applyMutation(state, (doc) => {
|
|
541
|
-
const node = getNodeAtPath(doc, path);
|
|
542
|
-
if (value === undefined || value === null || value === "") delete node[key];
|
|
543
|
-
else node[key] = value;
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* @param {StudioState} state
|
|
549
|
-
* @param {JxPath} path
|
|
550
|
-
* @param {string} prop
|
|
551
|
-
* @param {any} value
|
|
552
|
-
* @returns {StudioState}
|
|
553
|
-
*/
|
|
554
|
-
export function updateStyle(state, path, prop, value) {
|
|
555
|
-
return applyMutation(state, (doc) => {
|
|
556
|
-
const node = getNodeAtPath(doc, path);
|
|
557
|
-
if (!node.style) node.style = {};
|
|
558
|
-
if (value === undefined || value === "") delete node.style[prop];
|
|
559
|
-
else node.style[prop] = value;
|
|
560
|
-
if (Object.keys(node.style).length === 0) delete node.style;
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* @param {StudioState} state
|
|
566
|
-
* @param {JxPath} path
|
|
567
|
-
* @param {string} attr
|
|
568
|
-
* @param {any} value
|
|
569
|
-
* @returns {StudioState}
|
|
570
|
-
*/
|
|
571
|
-
export function updateAttribute(state, path, attr, value) {
|
|
572
|
-
return applyMutation(state, (doc) => {
|
|
573
|
-
const node = getNodeAtPath(doc, path);
|
|
574
|
-
if (!node.attributes) node.attributes = {};
|
|
575
|
-
if (value === undefined || value === "") delete node.attributes[attr];
|
|
576
|
-
else node.attributes[attr] = value;
|
|
577
|
-
if (Object.keys(node.attributes).length === 0) delete node.attributes;
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* @param {StudioState} state
|
|
583
|
-
* @param {string} name
|
|
584
|
-
* @param {any} def
|
|
585
|
-
* @returns {StudioState}
|
|
586
|
-
*/
|
|
587
|
-
export function addDef(state, name, def) {
|
|
588
|
-
return applyMutation(state, (doc) => {
|
|
589
|
-
if (!doc.state) doc.state = {};
|
|
590
|
-
doc.state[name] = def;
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* @param {StudioState} state
|
|
596
|
-
* @param {string} name
|
|
597
|
-
* @returns {StudioState}
|
|
598
|
-
*/
|
|
599
|
-
export function removeDef(state, name) {
|
|
600
|
-
return applyMutation(state, (doc) => {
|
|
601
|
-
if (doc.state) {
|
|
602
|
-
delete doc.state[name];
|
|
603
|
-
if (Object.keys(doc.state).length === 0) delete doc.state;
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* @param {StudioState} state
|
|
610
|
-
* @param {string} name
|
|
611
|
-
* @param {Record<string, any>} updates
|
|
612
|
-
* @returns {StudioState}
|
|
613
|
-
*/
|
|
614
|
-
export function updateDef(state, name, updates) {
|
|
615
|
-
return applyMutation(state, (doc) => {
|
|
616
|
-
if (!doc.state) doc.state = {};
|
|
617
|
-
if (!doc.state[name]) doc.state[name] = {};
|
|
618
|
-
Object.assign(doc.state[name], updates);
|
|
619
|
-
for (const k of Object.keys(doc.state[name])) {
|
|
620
|
-
if (doc.state[name][k] === undefined || doc.state[name][k] === null) {
|
|
621
|
-
delete doc.state[name][k];
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* @param {StudioState} state
|
|
629
|
-
* @param {string} oldName
|
|
630
|
-
* @param {string} newName
|
|
631
|
-
* @returns {StudioState}
|
|
632
|
-
*/
|
|
633
|
-
export function renameDef(state, oldName, newName) {
|
|
634
|
-
return applyMutation(state, (doc) => {
|
|
635
|
-
if (!doc.state || !doc.state[oldName]) return;
|
|
636
|
-
doc.state[newName] = doc.state[oldName];
|
|
637
|
-
delete doc.state[oldName];
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// ─── Media mutations ─────────────────────────────────────────────────────────
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Update a style property inside a media override block (e.g., `@--md`).
|
|
645
|
-
*
|
|
646
|
-
* @param {StudioState} state
|
|
647
|
-
* @param {JxPath} path
|
|
648
|
-
* @param {string} mediaName
|
|
649
|
-
* @param {string} prop
|
|
650
|
-
* @param {any} value
|
|
651
|
-
* @returns {StudioState}
|
|
652
|
-
*/
|
|
653
|
-
export function updateMediaStyle(state, path, mediaName, prop, value) {
|
|
654
|
-
return applyMutation(state, (doc) => {
|
|
655
|
-
const node = getNodeAtPath(doc, path);
|
|
656
|
-
if (!node.style) node.style = {};
|
|
657
|
-
const key = `@${mediaName}`;
|
|
658
|
-
if (!node.style[key]) node.style[key] = {};
|
|
659
|
-
if (value === undefined || value === "") {
|
|
660
|
-
delete node.style[key][prop];
|
|
661
|
-
if (Object.keys(node.style[key]).length === 0) delete node.style[key];
|
|
662
|
-
} else {
|
|
663
|
-
node.style[key][prop] = value;
|
|
664
|
-
}
|
|
665
|
-
if (Object.keys(node.style).length === 0) delete node.style;
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* Update a style property inside a nested selector block (e.g., :hover).
|
|
671
|
-
*
|
|
672
|
-
* @param {StudioState} state
|
|
673
|
-
* @param {JxPath} path
|
|
674
|
-
* @param {string} selector
|
|
675
|
-
* @param {string} prop
|
|
676
|
-
* @param {any} value
|
|
677
|
-
* @returns {StudioState}
|
|
678
|
-
*/
|
|
679
|
-
export function updateNestedStyle(state, path, selector, prop, value) {
|
|
680
|
-
return applyMutation(state, (doc) => {
|
|
681
|
-
const node = getNodeAtPath(doc, path);
|
|
682
|
-
if (!node.style) node.style = {};
|
|
683
|
-
if (!node.style[selector]) node.style[selector] = {};
|
|
684
|
-
if (value === undefined || value === "") {
|
|
685
|
-
delete node.style[selector][prop];
|
|
686
|
-
if (Object.keys(node.style[selector]).length === 0) delete node.style[selector];
|
|
687
|
-
} else {
|
|
688
|
-
node.style[selector][prop] = value;
|
|
689
|
-
}
|
|
690
|
-
if (Object.keys(node.style).length === 0) delete node.style;
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* Update a style property inside a nested selector within a media block (e.g., `@--md` > `:hover`).
|
|
696
|
-
*
|
|
697
|
-
* @param {StudioState} state
|
|
698
|
-
* @param {JxPath} path
|
|
699
|
-
* @param {string} mediaName
|
|
700
|
-
* @param {string} selector
|
|
701
|
-
* @param {string} prop
|
|
702
|
-
* @param {any} value
|
|
703
|
-
* @returns {StudioState}
|
|
704
|
-
*/
|
|
705
|
-
export function updateMediaNestedStyle(state, path, mediaName, selector, prop, value) {
|
|
706
|
-
return applyMutation(state, (doc) => {
|
|
707
|
-
const node = getNodeAtPath(doc, path);
|
|
708
|
-
if (!node.style) node.style = {};
|
|
709
|
-
const key = `@${mediaName}`;
|
|
710
|
-
if (!node.style[key]) node.style[key] = {};
|
|
711
|
-
if (!node.style[key][selector]) node.style[key][selector] = {};
|
|
712
|
-
if (value === undefined || value === "") {
|
|
713
|
-
delete node.style[key][selector][prop];
|
|
714
|
-
if (Object.keys(node.style[key][selector]).length === 0) delete node.style[key][selector];
|
|
715
|
-
if (Object.keys(node.style[key]).length === 0) delete node.style[key];
|
|
716
|
-
} else {
|
|
717
|
-
node.style[key][selector][prop] = value;
|
|
718
|
-
}
|
|
719
|
-
if (Object.keys(node.style).length === 0) delete node.style;
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* Add or update a named media entry at the document root.
|
|
725
|
-
*
|
|
726
|
-
* @param {StudioState} state
|
|
727
|
-
* @param {string} name
|
|
728
|
-
* @param {any} query
|
|
729
|
-
* @returns {StudioState}
|
|
730
|
-
*/
|
|
731
|
-
export function updateMedia(state, name, query) {
|
|
732
|
-
return applyMutation(state, (doc) => {
|
|
733
|
-
if (!doc.$media) doc.$media = {};
|
|
734
|
-
if (query === undefined || query === "") {
|
|
735
|
-
delete doc.$media[name];
|
|
736
|
-
if (Object.keys(doc.$media).length === 0) delete doc.$media;
|
|
737
|
-
} else {
|
|
738
|
-
doc.$media[name] = query;
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
|
|
743
355
|
// ─── Document stack (component navigation) ──────────────────────────────────
|
|
744
356
|
|
|
745
357
|
/**
|
|
@@ -764,7 +376,7 @@ export function pushDocument(state, doc, documentPath) {
|
|
|
764
376
|
const newState = createState(doc);
|
|
765
377
|
newState.documentStack = [...(state.documentStack || []), frame];
|
|
766
378
|
newState.documentPath = documentPath;
|
|
767
|
-
newState.ui = { ...state.ui,
|
|
379
|
+
newState.ui = { ...state.ui, activeMedia: null, activeSelector: null };
|
|
768
380
|
return newState;
|
|
769
381
|
}
|
|
770
382
|
|
|
@@ -782,75 +394,6 @@ export function popDocument(state) {
|
|
|
782
394
|
...state,
|
|
783
395
|
...frame,
|
|
784
396
|
documentStack: stack,
|
|
785
|
-
ui: { ...state.ui
|
|
397
|
+
ui: { ...state.ui },
|
|
786
398
|
};
|
|
787
399
|
}
|
|
788
|
-
|
|
789
|
-
// ─── $props mutations ────────────────────────────────────────────────────────
|
|
790
|
-
|
|
791
|
-
/**
|
|
792
|
-
* Update a $prop on a component instance.
|
|
793
|
-
*
|
|
794
|
-
* @param {StudioState} state
|
|
795
|
-
* @param {JxPath} path
|
|
796
|
-
* @param {string} propName
|
|
797
|
-
* @param {any} value
|
|
798
|
-
* @returns {StudioState}
|
|
799
|
-
*/
|
|
800
|
-
export function updateProp(state, path, propName, value) {
|
|
801
|
-
return applyMutation(state, (doc) => {
|
|
802
|
-
const node = getNodeAtPath(doc, path);
|
|
803
|
-
if (!node.$props) node.$props = {};
|
|
804
|
-
if (value === undefined || value === null || value === "") delete node.$props[propName];
|
|
805
|
-
else node.$props[propName] = value;
|
|
806
|
-
if (Object.keys(node.$props).length === 0) delete node.$props;
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// ─── $switch case mutations ──────────────────────────────────────────────────
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* @param {StudioState} state
|
|
814
|
-
* @param {JxPath} path
|
|
815
|
-
* @param {string} caseName
|
|
816
|
-
* @param {any} [caseDef]
|
|
817
|
-
* @returns {StudioState}
|
|
818
|
-
*/
|
|
819
|
-
export function addSwitchCase(state, path, caseName, caseDef) {
|
|
820
|
-
return applyMutation(state, (doc) => {
|
|
821
|
-
const node = getNodeAtPath(doc, path);
|
|
822
|
-
if (!node.cases) node.cases = {};
|
|
823
|
-
node.cases[caseName] = caseDef || { tagName: "div", textContent: caseName };
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* @param {StudioState} state
|
|
829
|
-
* @param {JxPath} path
|
|
830
|
-
* @param {string} caseName
|
|
831
|
-
* @returns {StudioState}
|
|
832
|
-
*/
|
|
833
|
-
export function removeSwitchCase(state, path, caseName) {
|
|
834
|
-
return applyMutation(state, (doc) => {
|
|
835
|
-
const node = getNodeAtPath(doc, path);
|
|
836
|
-
if (node.cases) {
|
|
837
|
-
delete node.cases[caseName];
|
|
838
|
-
}
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* @param {StudioState} state
|
|
844
|
-
* @param {JxPath} path
|
|
845
|
-
* @param {string} oldName
|
|
846
|
-
* @param {string} newName
|
|
847
|
-
* @returns {StudioState}
|
|
848
|
-
*/
|
|
849
|
-
export function renameSwitchCase(state, path, oldName, newName) {
|
|
850
|
-
return applyMutation(state, (doc) => {
|
|
851
|
-
const node = getNodeAtPath(doc, path);
|
|
852
|
-
if (!node.cases || !node.cases[oldName]) return;
|
|
853
|
-
node.cases[newName] = node.cases[oldName];
|
|
854
|
-
delete node.cases[oldName];
|
|
855
|
-
});
|
|
856
|
-
}
|