@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.
Files changed (55) hide show
  1. package/dist/studio.js +6517 -5870
  2. package/dist/studio.js.map +57 -51
  3. package/package.json +5 -3
  4. package/src/canvas/canvas-diff.js +184 -0
  5. package/src/canvas/canvas-helpers.js +10 -14
  6. package/src/canvas/canvas-live-render.js +28 -2
  7. package/src/canvas/canvas-render.js +170 -20
  8. package/src/canvas/canvas-utils.js +22 -25
  9. package/src/editor/component-inline-edit.js +55 -44
  10. package/src/editor/content-inline-edit.js +47 -50
  11. package/src/editor/context-menu.js +78 -53
  12. package/src/editor/convert-to-component.js +11 -14
  13. package/src/editor/insertion-helper.js +8 -11
  14. package/src/editor/shortcuts.js +88 -64
  15. package/src/files/components.js +15 -4
  16. package/src/files/file-ops.js +57 -108
  17. package/src/files/files.js +81 -28
  18. package/src/panels/activity-bar.js +31 -7
  19. package/src/panels/block-action-bar.js +104 -80
  20. package/src/panels/canvas-dnd.js +4 -10
  21. package/src/panels/dnd.js +77 -35
  22. package/src/panels/editors.js +17 -26
  23. package/src/panels/elements-panel.js +17 -11
  24. package/src/panels/events-panel.js +44 -39
  25. package/src/panels/git-panel.js +47 -4
  26. package/src/panels/layers-panel.js +25 -21
  27. package/src/panels/left-panel.js +109 -44
  28. package/src/panels/overlays.js +91 -41
  29. package/src/panels/panel-events.js +28 -37
  30. package/src/panels/preview-render.js +7 -3
  31. package/src/panels/properties-panel.js +179 -104
  32. package/src/panels/pseudo-preview.js +9 -8
  33. package/src/panels/right-panel.js +85 -37
  34. package/src/panels/shared.js +0 -22
  35. package/src/panels/signals-panel.js +125 -54
  36. package/src/panels/statusbar.js +26 -19
  37. package/src/panels/style-inputs.js +5 -4
  38. package/src/panels/style-panel.js +128 -105
  39. package/src/panels/style-utils.js +8 -6
  40. package/src/panels/stylebook-layers-panel.js +5 -5
  41. package/src/panels/stylebook-panel.js +27 -31
  42. package/src/panels/tab-strip.js +124 -0
  43. package/src/panels/toolbar.js +40 -10
  44. package/src/reactivity.js +28 -0
  45. package/src/settings/content-types-editor.js +56 -7
  46. package/src/settings/defs-editor.js +56 -7
  47. package/src/settings/schema-field-ui.js +60 -25
  48. package/src/state.js +2 -459
  49. package/src/store.js +61 -219
  50. package/src/studio.js +163 -300
  51. package/src/tabs/tab.js +168 -0
  52. package/src/tabs/transact.js +406 -0
  53. package/src/ui/color-selector.js +4 -5
  54. package/src/view.js +3 -0
  55. 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
- "string",
10
- "number",
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 string.
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 { type: "array", items: { type: "string" } };
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, leftTab: "layers", activeMedia: null, activeSelector: null };
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, leftTab: "layers" },
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
- }