@jxsuite/studio 0.28.2 → 0.28.4

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 (104) hide show
  1. package/dist/studio.js +86144 -83971
  2. package/dist/studio.js.map +229 -219
  3. package/package.json +5 -5
  4. package/src/browse/browse-modal.ts +9 -3
  5. package/src/browse/browse.ts +139 -82
  6. package/src/canvas/canvas-diff.ts +35 -18
  7. package/src/canvas/canvas-helpers.ts +55 -21
  8. package/src/canvas/canvas-live-render.ts +140 -89
  9. package/src/canvas/canvas-render.ts +127 -82
  10. package/src/canvas/canvas-utils.ts +68 -42
  11. package/src/canvas/nested-site-style.ts +15 -7
  12. package/src/editor/component-inline-edit.ts +90 -51
  13. package/src/editor/content-inline-edit.ts +75 -72
  14. package/src/editor/context-menu.ts +125 -71
  15. package/src/editor/convert-targets.ts +17 -13
  16. package/src/editor/convert-to-component.ts +51 -28
  17. package/src/editor/convert-to-repeater.ts +44 -19
  18. package/src/editor/inline-edit.ts +107 -52
  19. package/src/editor/inline-format.ts +128 -60
  20. package/src/editor/insertion-helper.ts +26 -14
  21. package/src/editor/shortcuts.ts +89 -43
  22. package/src/editor/slash-menu.ts +41 -26
  23. package/src/files/components.ts +9 -7
  24. package/src/files/file-ops.ts +53 -33
  25. package/src/files/files.ts +246 -143
  26. package/src/format/constraints.ts +25 -15
  27. package/src/format/format-host.ts +33 -10
  28. package/src/github/github-auth.ts +24 -14
  29. package/src/github/github-publish.ts +20 -14
  30. package/src/new-project/new-project-modal.ts +27 -18
  31. package/src/panels/activity-bar.ts +27 -26
  32. package/src/panels/ai-panel.ts +100 -44
  33. package/src/panels/block-action-bar.ts +84 -52
  34. package/src/panels/canvas-dnd.ts +47 -28
  35. package/src/panels/data-explorer.ts +24 -11
  36. package/src/panels/dnd.ts +148 -100
  37. package/src/panels/editors.ts +54 -37
  38. package/src/panels/elements-panel.ts +23 -13
  39. package/src/panels/events-panel.ts +48 -38
  40. package/src/panels/git-panel.ts +95 -60
  41. package/src/panels/head-panel.ts +138 -84
  42. package/src/panels/imports-panel.ts +66 -29
  43. package/src/panels/layers-panel.ts +80 -36
  44. package/src/panels/left-panel.ts +86 -53
  45. package/src/panels/overlays.ts +36 -19
  46. package/src/panels/panel-events.ts +48 -27
  47. package/src/panels/panel-scheduler.ts +41 -23
  48. package/src/panels/preview-render.ts +26 -26
  49. package/src/panels/properties-panel.ts +155 -118
  50. package/src/panels/pseudo-preview.ts +31 -15
  51. package/src/panels/quick-search.ts +28 -13
  52. package/src/panels/right-panel.ts +35 -24
  53. package/src/panels/shared.ts +46 -29
  54. package/src/panels/signals-panel.ts +303 -186
  55. package/src/panels/statusbar.ts +27 -12
  56. package/src/panels/style-inputs.ts +30 -24
  57. package/src/panels/style-panel.ts +130 -90
  58. package/src/panels/style-utils.ts +63 -23
  59. package/src/panels/stylebook-layers-panel.ts +27 -30
  60. package/src/panels/stylebook-panel.ts +140 -72
  61. package/src/panels/tab-strip.ts +21 -10
  62. package/src/panels/toolbar.ts +52 -40
  63. package/src/panels/welcome-screen.ts +1 -1
  64. package/src/platform.ts +2 -1
  65. package/src/platforms/devserver.ts +221 -105
  66. package/src/recent-projects.ts +15 -7
  67. package/src/resize-edges.ts +7 -5
  68. package/src/services/cem-export.ts +73 -57
  69. package/src/services/code-services.ts +52 -25
  70. package/src/services/monaco-setup.ts +5 -5
  71. package/src/settings/content-types-editor.ts +133 -62
  72. package/src/settings/css-vars-editor.ts +31 -14
  73. package/src/settings/defs-editor.ts +125 -63
  74. package/src/settings/general-settings.ts +11 -6
  75. package/src/settings/head-editor.ts +30 -15
  76. package/src/settings/schema-field-ui.ts +75 -35
  77. package/src/settings/settings-modal.ts +35 -17
  78. package/src/site-context.ts +80 -47
  79. package/src/state.ts +98 -65
  80. package/src/store.ts +42 -23
  81. package/src/studio.ts +165 -144
  82. package/src/tabs/tab.ts +51 -45
  83. package/src/tabs/transact.ts +230 -109
  84. package/src/types.ts +60 -51
  85. package/src/ui/button-group.ts +3 -2
  86. package/src/ui/color-selector.ts +33 -15
  87. package/src/ui/expression-editor.ts +139 -86
  88. package/src/ui/field-input.ts +35 -18
  89. package/src/ui/field-row.ts +1 -1
  90. package/src/ui/icons.ts +5 -6
  91. package/src/ui/layers.ts +33 -26
  92. package/src/ui/media-picker.ts +13 -7
  93. package/src/ui/panel-resize.ts +39 -20
  94. package/src/ui/spectrum.ts +4 -2
  95. package/src/ui/unit-selector.ts +20 -11
  96. package/src/ui/value-selector.ts +13 -7
  97. package/src/ui/widgets.ts +34 -18
  98. package/src/utils/canvas-media.ts +66 -33
  99. package/src/utils/edit-display.ts +43 -44
  100. package/src/utils/google-fonts.ts +10 -6
  101. package/src/utils/inherited-style.ts +22 -8
  102. package/src/utils/studio-utils.ts +88 -45
  103. package/src/view.ts +9 -5
  104. package/src/workspace/workspace.ts +49 -29
@@ -8,9 +8,16 @@
8
8
  */
9
9
 
10
10
  import elementsMeta from "../../data/elements-meta.json";
11
- import { toggleInlineFormat, normalizeInlineContent } from "./inline-format";
11
+ import { normalizeInlineContent, toggleInlineFormat } from "./inline-format";
12
12
  import type { JxPath } from "../state";
13
13
  import type { JxMutableNode } from "@jxsuite/schema/types";
14
+
15
+ import {
16
+ showSlashMenu as sharedShowSlashMenu,
17
+ dismissSlashMenu as sharedDismissSlashMenu,
18
+ isSlashMenuOpen,
19
+ } from "./slash-menu";
20
+
14
21
  export interface InlineAction {
15
22
  tag: string;
16
23
  label: string;
@@ -35,12 +42,6 @@ export interface JxContentResult {
35
42
  children?: (JxMutableNode | string)[];
36
43
  }
37
44
 
38
- import {
39
- showSlashMenu as sharedShowSlashMenu,
40
- dismissSlashMenu as sharedDismissSlashMenu,
41
- isSlashMenuOpen,
42
- } from "./slash-menu";
43
-
44
45
  // ─── Inline tag set (tags that represent rich text formatting) ─────────────
45
46
 
46
47
  /** Fallback set — used when parent context is unknown */
@@ -90,9 +91,13 @@ const EDITABLE_BLOCKS = new Set([
90
91
  * @returns {boolean}
91
92
  */
92
93
  export function isInlineInContext(childTag: string, parentTag: string) {
93
- if (!parentTag) return INLINE_TAGS.has(childTag);
94
+ if (!parentTag) {
95
+ return INLINE_TAGS.has(childTag);
96
+ }
94
97
  const parentDef = (elementsMeta.$defs as Record<string, ElementDef>)[parentTag];
95
- if (!parentDef || !parentDef.$inlineChildren) return false;
98
+ if (!parentDef || !parentDef.$inlineChildren) {
99
+ return false;
100
+ }
96
101
  return parentDef.$inlineChildren.includes(childTag);
97
102
  }
98
103
 
@@ -105,19 +110,23 @@ export function isInlineInContext(childTag: string, parentTag: string) {
105
110
  */
106
111
  export function getInlineActions(tag: string) {
107
112
  const def = (elementsMeta.$defs as Record<string, ElementDef>)[tag];
108
- if (!def) return null;
113
+ if (!def) {
114
+ return null;
115
+ }
109
116
  let actions = def.$inlineActions;
110
117
  if (typeof actions === "string") {
111
118
  const refDef = (elementsMeta.$defs as Record<string, ElementDef>)[actions];
112
119
  actions = refDef?.$inlineActions ?? undefined;
113
120
  }
114
- if (!Array.isArray(actions)) return null;
121
+ if (!Array.isArray(actions)) {
122
+ return null;
123
+ }
115
124
  return actions;
116
125
  }
117
126
 
118
127
  // ─── Editing state ─────────────────────────────────────────────────────────
119
128
 
120
- let activeEl: HTMLElement | null = null; // currently contenteditable element
129
+ let activeEl: HTMLElement | null = null; // Currently contenteditable element
121
130
  let activePath: JxPath | null = null; // JSON path to the active element
122
131
  /**
123
132
  * @type {((
@@ -133,22 +142,22 @@ let commitFn:
133
142
  children: (JxMutableNode | string)[] | null,
134
143
  textContent: string | null,
135
144
  ) => void)
136
- | null = null; // function(path, newChildren, newTextContent) to commit changes
145
+ | null = null; // Function(path, newChildren, newTextContent) to commit changes
137
146
  /**
138
147
  * @type {((path: JxPath, beforeChildren: JxContentResult, afterChildren: JxContentResult) => void)
139
148
  * | null}
140
149
  */
141
150
  let splitFn:
142
151
  | ((path: JxPath, beforeChildren: JxContentResult, afterChildren: JxContentResult) => void)
143
- | null = null; // function(path, beforeChildren, afterChildren) to split paragraph
152
+ | null = null; // Function(path, beforeChildren, afterChildren) to split paragraph
144
153
  /**
145
154
  * @type {((path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void)
146
155
  * | null}
147
156
  */
148
157
  let insertFn:
149
158
  | ((path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void)
150
- | null = null; // function(path, elementDef, commitData?) to insert after current block
151
- let endFn: (() => void) | null = null; // function() called when editing stops
159
+ | null = null; // Function(path, elementDef, commitData?) to insert after current block
160
+ let endFn: (() => void) | null = null; // Function() called when editing stops
152
161
 
153
162
  /**
154
163
  * Check if an element is a text-bearing editable block.
@@ -169,7 +178,9 @@ export function isEditableBlock(el: HTMLElement) {
169
178
  * @returns {boolean}
170
179
  */
171
180
  export function isInlineElement(node: JxMutableNode, parentNode?: JxMutableNode) {
172
- if (!node || typeof node !== "object") return false;
181
+ if (!node || typeof node !== "object") {
182
+ return false;
183
+ }
173
184
  const childTag = (node.tagName ?? "div").toLowerCase();
174
185
  if (parentNode) {
175
186
  const parentTag = (parentNode.tagName ?? "div").toLowerCase();
@@ -216,7 +227,9 @@ export function startEditing(
216
227
  onEnd: () => void;
217
228
  },
218
229
  ) {
219
- if (activeEl) stopEditing();
230
+ if (activeEl) {
231
+ stopEditing();
232
+ }
220
233
 
221
234
  activeEl = el;
222
235
  activePath = path;
@@ -251,7 +264,9 @@ export function startEditing(
251
264
 
252
265
  /** Stop editing and commit changes. */
253
266
  export function stopEditing() {
254
- if (!activeEl) return;
267
+ if (!activeEl) {
268
+ return;
269
+ }
255
270
 
256
271
  commitChanges();
257
272
  sharedDismissSlashMenu();
@@ -310,7 +325,9 @@ function handleKeydown(e: KeyboardEvent) {
310
325
  }
311
326
 
312
327
  if (e.key === "Enter" && !e.shiftKey) {
313
- if (isSlashMenuOpen()) return; // shared slash menu captures Enter
328
+ if (isSlashMenuOpen()) {
329
+ return;
330
+ } // Shared slash menu captures Enter
314
331
  e.preventDefault();
315
332
  e.stopPropagation();
316
333
  handleEnterKey();
@@ -335,18 +352,24 @@ function handleKeydown(e: KeyboardEvent) {
335
352
  // Rich text shortcuts
336
353
  if (e.ctrlKey || e.metaKey) {
337
354
  switch (e.key) {
338
- case "b":
355
+ case "b": {
339
356
  e.preventDefault();
340
357
  toggleInlineFormat("strong", activeEl);
341
358
  break;
342
- case "i":
359
+ }
360
+ case "i": {
343
361
  e.preventDefault();
344
362
  toggleInlineFormat("em", activeEl);
345
363
  break;
346
- case "`":
364
+ }
365
+ case "`": {
347
366
  e.preventDefault();
348
367
  toggleInlineFormat("code", activeEl);
349
368
  break;
369
+ }
370
+ default: {
371
+ break;
372
+ }
350
373
  }
351
374
  }
352
375
 
@@ -369,7 +392,9 @@ function handleInput() {
369
392
  /** @param {FocusEvent} _e */
370
393
  function handleBlur(_e: FocusEvent) {
371
394
  // Don't close if focus moved to slash menu
372
- if (isSlashMenuOpen()) return;
395
+ if (isSlashMenuOpen()) {
396
+ return;
397
+ }
373
398
 
374
399
  // Delay to allow click events to fire
375
400
  setTimeout(() => {
@@ -390,10 +415,14 @@ function handlePaste(e: ClipboardEvent) {
390
415
  // ─── Enter key: split paragraph ────────────────────────────────────────────
391
416
 
392
417
  function handleEnterKey() {
393
- if (!splitFn || !activeEl || !activePath) return;
418
+ if (!splitFn || !activeEl || !activePath) {
419
+ return;
420
+ }
394
421
 
395
422
  const sel = window.getSelection();
396
- if (!sel || !sel.rangeCount) return;
423
+ if (!sel || !sel.rangeCount) {
424
+ return;
425
+ }
397
426
 
398
427
  const range = sel.getRangeAt(0);
399
428
 
@@ -440,7 +469,9 @@ function handleEnterKey() {
440
469
  // ─── Content sync: DOM → Jx ────────────────────────────────────────────
441
470
 
442
471
  function commitChanges() {
443
- if (!commitFn || !activeEl || !activePath) return;
472
+ if (!commitFn || !activeEl || !activePath) {
473
+ return;
474
+ }
444
475
 
445
476
  normalizeInlineContent(activeEl);
446
477
  const result = elementToJx(activeEl);
@@ -455,16 +486,14 @@ function commitChanges() {
455
486
  * @returns {JxContentResult}
456
487
  */
457
488
  export function normalizeChildren(node: { children?: (JxMutableNode | string)[] }) {
458
- if (!Array.isArray(node.children) || node.children.length === 0) return { textContent: "" };
489
+ if (!Array.isArray(node.children) || node.children.length === 0) {
490
+ return { textContent: "" };
491
+ }
459
492
 
460
493
  // Step 1: Merge adjacent text nodes
461
494
  const merged = [];
462
495
  for (const child of node.children) {
463
- if (
464
- typeof child === "string" &&
465
- merged.length > 0 &&
466
- typeof merged[merged.length - 1] === "string"
467
- ) {
496
+ if (typeof child === "string" && merged.length > 0 && typeof merged.at(-1) === "string") {
468
497
  merged[merged.length - 1] += child;
469
498
  } else {
470
499
  merged.push(child);
@@ -490,7 +519,9 @@ function elementToJx(el: HTMLElement): JxContentResult {
490
519
  const nodes = el.childNodes;
491
520
 
492
521
  // If just a single text node, use textContent
493
- if (nodes.length === 0) return { textContent: "" };
522
+ if (nodes.length === 0) {
523
+ return { textContent: "" };
524
+ }
494
525
  if (nodes.length === 1 && nodes[0].nodeType === Node.TEXT_NODE) {
495
526
  return { textContent: nodes[0].textContent };
496
527
  }
@@ -499,7 +530,9 @@ function elementToJx(el: HTMLElement): JxContentResult {
499
530
  const children: (JxMutableNode | string)[] = [];
500
531
  for (const child of nodes) {
501
532
  const jsx = domNodeToJx(child);
502
- if (jsx !== null && jsx !== undefined) children.push(jsx);
533
+ if (jsx !== null && jsx !== undefined) {
534
+ children.push(jsx);
535
+ }
503
536
  }
504
537
 
505
538
  // Normalize: merge adjacent text nodes + fold all-text to textContent
@@ -515,11 +548,15 @@ function elementToJx(el: HTMLElement): JxContentResult {
515
548
  function domNodeToJx(node: Node) {
516
549
  if (node.nodeType === Node.TEXT_NODE) {
517
550
  const text = node.textContent;
518
- if (!text) return null;
551
+ if (!text) {
552
+ return null;
553
+ }
519
554
  return text; // Bare string — text node child
520
555
  }
521
556
 
522
- if (node.nodeType !== Node.ELEMENT_NODE) return null;
557
+ if (node.nodeType !== Node.ELEMENT_NODE) {
558
+ return null;
559
+ }
523
560
 
524
561
  const el = node as HTMLElement;
525
562
  const tag = el.tagName.toLowerCase();
@@ -532,12 +569,16 @@ function domNodeToJx(node: Node) {
532
569
  s: "del",
533
570
  strike: "del",
534
571
  };
535
- if (tagMap[tag]) result.tagName = tagMap[tag];
572
+ if (tagMap[tag]) {
573
+ result.tagName = tagMap[tag];
574
+ }
536
575
 
537
576
  // Attributes
538
577
  if (tag === "a" && (el as HTMLAnchorElement).href) {
539
- result.attributes = { href: el.getAttribute("href") };
540
- if ((el as HTMLAnchorElement).title) result.attributes.title = (el as HTMLAnchorElement).title;
578
+ result.attributes = { href: el.getAttribute("href") ?? "" };
579
+ if ((el as HTMLAnchorElement).title) {
580
+ result.attributes.title = (el as HTMLAnchorElement).title;
581
+ }
541
582
  }
542
583
  if (tag === "code") {
543
584
  result.textContent = el.textContent;
@@ -545,7 +586,7 @@ function domNodeToJx(node: Node) {
545
586
  }
546
587
 
547
588
  // Recurse children
548
- const childNodes = el.childNodes;
589
+ const { childNodes } = el;
549
590
  if (childNodes.length === 0) {
550
591
  result.textContent = "";
551
592
  } else if (childNodes.length === 1 && childNodes[0].nodeType === Node.TEXT_NODE) {
@@ -554,7 +595,9 @@ function domNodeToJx(node: Node) {
554
595
  result.children = [];
555
596
  for (const child of childNodes) {
556
597
  const jsx = domNodeToJx(child);
557
- if (jsx) result.children.push(/** @type {JxMutableNode} */ jsx);
598
+ if (jsx) {
599
+ result.children.push(/** @type {JxMutableNode} */ jsx);
600
+ }
558
601
  }
559
602
  }
560
603
 
@@ -569,7 +612,9 @@ function domNodeToJx(node: Node) {
569
612
  */
570
613
  function fragmentToJx(frag: DocumentFragment) {
571
614
  const nodes = frag.childNodes;
572
- if (nodes.length === 0) return { textContent: "" };
615
+ if (nodes.length === 0) {
616
+ return { textContent: "" };
617
+ }
573
618
  if (nodes.length === 1 && nodes[0].nodeType === Node.TEXT_NODE) {
574
619
  return { textContent: nodes[0].textContent };
575
620
  }
@@ -577,14 +622,16 @@ function fragmentToJx(frag: DocumentFragment) {
577
622
  const children: (JxMutableNode | string)[] = [];
578
623
  for (const child of nodes) {
579
624
  const jsx = domNodeToJx(child);
580
- if (jsx) children.push(jsx);
625
+ if (jsx) {
626
+ children.push(jsx);
627
+ }
581
628
  }
582
629
 
583
630
  if (
584
631
  children.length === 1 &&
585
632
  typeof children[0] !== "string" &&
586
633
  children[0].tagName === "span" &&
587
- children[0].textContent != null
634
+ typeof children[0].textContent === "string"
588
635
  ) {
589
636
  return { textContent: children[0].textContent };
590
637
  }
@@ -611,10 +658,14 @@ function getTextBeforeCursor(range: Range) {
611
658
  let _slashFilterStart = 0;
612
659
 
613
660
  function openSlashMenu() {
614
- if (!activeEl || !insertFn || !activePath) return;
661
+ if (!activeEl || !insertFn || !activePath) {
662
+ return;
663
+ }
615
664
 
616
665
  const sel = window.getSelection();
617
- if (!sel || !sel.rangeCount) return;
666
+ if (!sel || !sel.rangeCount) {
667
+ return;
668
+ }
618
669
  const range = sel.getRangeAt(0);
619
670
  _slashFilterStart = getTextBeforeCursor(range).length;
620
671
 
@@ -622,7 +673,9 @@ function openSlashMenu() {
622
673
  }
623
674
 
624
675
  function updateSlashMenu() {
625
- if (!activeEl) return;
676
+ if (!activeEl) {
677
+ return;
678
+ }
626
679
 
627
680
  const sel = window.getSelection();
628
681
  if (!sel || !sel.rangeCount) {
@@ -634,7 +687,7 @@ function updateSlashMenu() {
634
687
  const fullText = getTextBeforeCursor(range);
635
688
  const slashIdx = fullText.lastIndexOf("/");
636
689
 
637
- if (slashIdx < 0 || fullText.length < _slashFilterStart - 1) {
690
+ if (slashIdx === -1 || fullText.length < _slashFilterStart - 1) {
638
691
  sharedDismissSlashMenu();
639
692
  return;
640
693
  }
@@ -645,7 +698,9 @@ function updateSlashMenu() {
645
698
 
646
699
  /** @param {SlashCommand} cmd */
647
700
  function handleSlashSelect(cmd: SlashCommand) {
648
- if (!activeEl || !insertFn || !activePath) return;
701
+ if (!activeEl || !insertFn || !activePath) {
702
+ return;
703
+ }
649
704
 
650
705
  // Remove the /command text from the element
651
706
  const sel = window.getSelection();
@@ -653,7 +708,7 @@ function handleSlashSelect(cmd: SlashCommand) {
653
708
  const range = sel.getRangeAt(0);
654
709
  const fullText = getTextBeforeCursor(range);
655
710
  const slashIdx = fullText.lastIndexOf("/");
656
- if (slashIdx >= 0) {
711
+ if (slashIdx !== -1) {
657
712
  const walker = document.createTreeWalker(activeEl, NodeFilter.SHOW_TEXT);
658
713
  let charCount = 0;
659
714
  let slashNode: Text | null = null;
@@ -677,7 +732,7 @@ function handleSlashSelect(cmd: SlashCommand) {
677
732
  }
678
733
 
679
734
  // Compute commit data inline instead of calling commitChanges() — avoids a separate
680
- // update() call that would race with the insertFn update() (two concurrent async renders).
735
+ // Update() call that would race with the insertFn update() (two concurrent async renders).
681
736
  normalizeInlineContent(activeEl);
682
737
  const commitResult = elementToJx(activeEl);
683
738