@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.
Files changed (53) hide show
  1. package/README.md +82 -0
  2. package/dist/studio.js +5114 -3424
  3. package/dist/studio.js.map +65 -49
  4. package/package.json +6 -3
  5. package/src/browse/browse.js +27 -3
  6. package/src/canvas/canvas-diff.js +184 -0
  7. package/src/canvas/canvas-helpers.js +10 -14
  8. package/src/canvas/canvas-live-render.js +136 -17
  9. package/src/canvas/canvas-render.js +154 -21
  10. package/src/canvas/canvas-utils.js +21 -23
  11. package/src/editor/component-inline-edit.js +54 -41
  12. package/src/editor/content-inline-edit.js +46 -47
  13. package/src/editor/context-menu.js +63 -39
  14. package/src/editor/convert-to-component.js +11 -14
  15. package/src/editor/insertion-helper.js +8 -10
  16. package/src/editor/shortcuts.js +69 -39
  17. package/src/files/components.js +15 -4
  18. package/src/files/files.js +72 -24
  19. package/src/panels/activity-bar.js +29 -7
  20. package/src/panels/block-action-bar.js +104 -80
  21. package/src/panels/canvas-dnd.js +132 -50
  22. package/src/panels/dnd.js +32 -28
  23. package/src/panels/editors.js +7 -14
  24. package/src/panels/elements-panel.js +9 -3
  25. package/src/panels/events-panel.js +44 -39
  26. package/src/panels/git-panel.js +45 -3
  27. package/src/panels/layers-panel.js +16 -11
  28. package/src/panels/left-panel.js +108 -43
  29. package/src/panels/overlays.js +97 -35
  30. package/src/panels/panel-events.js +18 -11
  31. package/src/panels/properties-panel.js +467 -98
  32. package/src/panels/right-panel.js +85 -37
  33. package/src/panels/shared.js +0 -22
  34. package/src/panels/signals-panel.js +125 -54
  35. package/src/panels/statusbar.js +23 -8
  36. package/src/panels/style-inputs.js +4 -2
  37. package/src/panels/style-panel.js +128 -105
  38. package/src/panels/stylebook-panel.js +42 -17
  39. package/src/panels/tab-strip.js +124 -0
  40. package/src/panels/toolbar.js +39 -9
  41. package/src/reactivity.js +28 -0
  42. package/src/settings/content-types-editor.js +78 -8
  43. package/src/settings/defs-editor.js +56 -7
  44. package/src/settings/schema-field-ui.js +99 -11
  45. package/src/site-context.js +105 -0
  46. package/src/state.js +0 -456
  47. package/src/store.js +105 -124
  48. package/src/studio.js +112 -121
  49. package/src/tabs/tab.js +153 -0
  50. package/src/tabs/transact.js +406 -0
  51. package/src/ui/spectrum.js +2 -0
  52. package/src/view.js +3 -0
  53. package/src/workspace/workspace.js +89 -0
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
  /**
@@ -335,31 +333,6 @@ export function updateFrontmatter(state, field, value) {
335
333
  };
336
334
  }
337
335
 
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
336
  // ─── Selection / hover ────────────────────────────────────────────────────────
364
337
 
365
338
  /**
@@ -380,366 +353,6 @@ export function hoverNode(state, path) {
380
353
  return { ...state, hover: path };
381
354
  }
382
355
 
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
356
  // ─── Document stack (component navigation) ──────────────────────────────────
744
357
 
745
358
  /**
@@ -785,72 +398,3 @@ export function popDocument(state) {
785
398
  ui: { ...state.ui, leftTab: "layers" },
786
399
  };
787
400
  }
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
- }