@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
@@ -7,25 +7,25 @@ export interface SlashCommand {
7
7
  }
8
8
 
9
9
  const TAG_LABELS = {
10
- p: "Paragraph",
10
+ article: "Article",
11
+ aside: "Aside",
12
+ blockquote: "Blockquote",
13
+ div: "Div",
14
+ footer: "Footer",
11
15
  h1: "Heading 1",
12
16
  h2: "Heading 2",
13
17
  h3: "Heading 3",
14
18
  h4: "Heading 4",
15
19
  h5: "Heading 5",
16
20
  h6: "Heading 6",
17
- blockquote: "Blockquote",
18
- div: "Div",
19
- section: "Section",
20
- article: "Article",
21
- aside: "Aside",
22
- main: "Main",
23
21
  header: "Header",
24
- footer: "Footer",
22
+ main: "Main",
25
23
  nav: "Nav",
24
+ ol: "Numbered List",
25
+ p: "Paragraph",
26
26
  search: "Search",
27
+ section: "Section",
27
28
  ul: "Bulleted List",
28
- ol: "Numbered List",
29
29
  } as Record<string, string>;
30
30
 
31
31
  const groups = (elementsMeta.$convertGroups || {}) as Record<string, string[]>;
@@ -41,23 +41,27 @@ export function getConvertTargets(currentTag: string, isEmpty: boolean) {
41
41
  const def = (elementsMeta.$defs as Record<string, Record<string, unknown>>)?.[currentTag] as
42
42
  | Record<string, unknown>
43
43
  | undefined;
44
- if (!def?.$convertTo) return [];
44
+ if (!def?.$convertTo) {
45
+ return [];
46
+ }
45
47
 
46
48
  const groupNames =
47
49
  isEmpty && def.$convertToWhenEmpty
48
50
  ? (def.$convertToWhenEmpty as string[])
49
51
  : [def.$convertTo as string];
50
52
 
51
- const tags: Set<string> = new Set();
53
+ const tags = new Set<string>();
52
54
  for (const name of groupNames) {
53
55
  for (const tag of groups[name] || []) {
54
- if (tag !== currentTag) tags.add(tag);
56
+ if (tag !== currentTag) {
57
+ tags.add(tag);
58
+ }
55
59
  }
56
60
  }
57
61
 
58
62
  return [...tags].map((tag) => ({
63
+ description: "",
59
64
  label: TAG_LABELS[tag] || tag.charAt(0).toUpperCase() + tag.slice(1),
60
65
  tag,
61
- description: "",
62
66
  }));
63
67
  }
@@ -1,11 +1,12 @@
1
1
  /// <reference lib="dom" />
2
2
  // ─── Convert to Component ─────────────────────────────────────────────────────
3
3
  import { html, render as litRender } from "lit-html";
4
+ import { errorMessage } from "@jxsuite/schema/parse";
4
5
  import { ref } from "lit-html/directives/ref.js";
5
- import { getNodeAtPath, parentElementPath, childIndex } from "../store";
6
+ import { childIndex, getNodeAtPath, parentElementPath } from "../store";
6
7
  import { activeTab } from "../workspace/workspace";
7
8
  import { transact } from "../tabs/transact";
8
- import { computeRelativePath, loadComponentRegistry, componentRegistry } from "../files/components";
9
+ import { componentRegistry, computeRelativePath, loadComponentRegistry } from "../files/components";
9
10
  import { getPlatform } from "../platform";
10
11
  import { statusMessage } from "../panels/statusbar";
11
12
  import { showDialog } from "../ui/layers";
@@ -17,21 +18,27 @@ const VALID_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
17
18
  /** Convert the currently selected element into a reusable component. */
18
19
  export async function convertToComponent() {
19
20
  const tab = activeTab.value;
20
- if (!tab?.session.selection || tab.session.selection.length < 2) return;
21
+ if (!tab?.session.selection || tab.session.selection.length < 2) {
22
+ return;
23
+ }
21
24
 
22
25
  const node = getNodeAtPath(tab.doc.document, tab.session.selection);
23
- if (!node || !node.tagName) return;
26
+ if (!node || !node.tagName) {
27
+ return;
28
+ }
24
29
 
25
30
  const defaultName = deriveDefaultName(node);
26
31
  const name = await promptComponentName(defaultName);
27
- if (!name) return;
32
+ if (!name) {
33
+ return;
34
+ }
28
35
 
29
36
  // Extract component definition
30
37
  const componentDef = extractComponentDef(node);
31
38
  componentDef.tagName = name;
32
39
 
33
40
  // Compute paths
34
- const componentFile = "components/" + name + ".json";
41
+ const componentFile = `components/${name}.json`;
35
42
  const refPath = computeRelativePath(tab.documentPath, componentFile);
36
43
 
37
44
  // Single atomic mutation: replace node + add $elements ref
@@ -41,12 +48,19 @@ export async function convertToComponent() {
41
48
  const pp = parentElementPath(selectionPath) ?? [];
42
49
  const idx = childIndex(selectionPath) as number;
43
50
  let parent = doc;
44
- for (const seg of pp) parent = parent[seg];
45
- if (!parent.children) parent.children = [];
51
+ // Paths address nodes by construction (same contract as getNodeAtPath).
52
+ for (const seg of pp) {
53
+ parent = parent[seg] as JxMutableNode;
54
+ }
55
+ if (!Array.isArray(parent.children)) {
56
+ parent.children = [];
57
+ }
46
58
  parent.children[idx] = { tagName: name };
47
59
 
48
60
  // Ensure $elements exists and add the $ref
49
- if (!doc.$elements) doc.$elements = [];
61
+ if (!doc.$elements) {
62
+ doc.$elements = [];
63
+ }
50
64
  const alreadyReferenced = doc.$elements.some(
51
65
  (/** @type {JxMutableNode | string | { $ref: string }} */ el) =>
52
66
  el && typeof el === "object" && "$ref" in el && el.$ref === refPath,
@@ -62,8 +76,8 @@ export async function convertToComponent() {
62
76
  await platform.writeFile(componentFile, JSON.stringify(componentDef, null, 2));
63
77
  await loadComponentRegistry();
64
78
  statusMessage(`Converted to <${name}>`);
65
- } catch (err) {
66
- statusMessage(`Error saving component: ${(err as Error).message}`);
79
+ } catch (error) {
80
+ statusMessage(`Error saving component: ${errorMessage(error)}`);
67
81
  }
68
82
  }
69
83
 
@@ -74,9 +88,11 @@ export async function convertToComponent() {
74
88
  * @returns {string}
75
89
  */
76
90
  function deriveDefaultName(node: JxMutableNode) {
77
- if (node.$id && node.$id.includes("-")) return node.$id.toLowerCase();
91
+ if (node.$id && node.$id.includes("-")) {
92
+ return node.$id.toLowerCase();
93
+ }
78
94
  const tag = (node.tagName ?? "div").toLowerCase();
79
- return tag.includes("-") ? tag : "jx-" + tag;
95
+ return tag.includes("-") ? tag : `jx-${tag}`;
80
96
  }
81
97
 
82
98
  /**
@@ -100,24 +116,24 @@ function extractComponentDef(node: JxMutableNode) {
100
116
  * @returns {{ valid: boolean; error: string }}
101
117
  */
102
118
  function validateName(val: string) {
103
- val = val.trim().toLowerCase();
104
- if (!val.includes("-")) {
119
+ const name = val.trim().toLowerCase();
120
+ if (!name.includes("-")) {
105
121
  return {
106
- valid: false,
107
122
  error: "Name must contain a hyphen (e.g. my-component)",
123
+ valid: false,
108
124
  };
109
125
  }
110
- if (!VALID_NAME.test(val)) {
126
+ if (!VALID_NAME.test(name)) {
111
127
  return {
112
- valid: false,
113
128
  error: "Lowercase letters, digits, and hyphens only",
129
+ valid: false,
114
130
  };
115
131
  }
116
- const exists = componentRegistry.some((c: JxMutableNode) => c.tagName === val);
132
+ const exists = componentRegistry.some((c) => c.tagName === name);
117
133
  if (exists) {
118
- return { valid: false, error: `Component <${val}> already exists` };
134
+ return { error: `Component <${name}> already exists`, valid: false };
119
135
  }
120
- return { valid: true, error: "" };
136
+ return { error: "", valid: true };
121
137
  }
122
138
 
123
139
  /**
@@ -134,7 +150,7 @@ function promptComponentName(defaultName: string) {
134
150
  function confirm() {
135
151
  const result = validateName(value);
136
152
  if (!result.valid) {
137
- error = result.error;
153
+ ({ error } = result);
138
154
  rerender();
139
155
  return;
140
156
  }
@@ -149,13 +165,17 @@ function promptComponentName(defaultName: string) {
149
165
  }
150
166
 
151
167
  function onKeydown(e: KeyboardEvent) {
152
- if (e.key === "Enter") confirm();
168
+ if (e.key === "Enter") {
169
+ confirm();
170
+ }
153
171
  }
154
172
 
155
173
  function rerender() {
156
- const layer = document.getElementById("layer-dialog");
174
+ const layer = document.querySelector("#layer-dialog");
157
175
  const slot = layer?.lastElementChild;
158
- if (slot) litRender(buildTpl(), slot as HTMLElement);
176
+ if (slot) {
177
+ litRender(buildTpl(), slot as HTMLElement);
178
+ }
159
179
  }
160
180
 
161
181
  function buildTpl() {
@@ -175,16 +195,19 @@ function promptComponentName(defaultName: string) {
175
195
  <sp-textfield
176
196
  placeholder="my-component"
177
197
  value=${value}
178
- ?negative=${!!error}
198
+ ?negative=${Boolean(error)}
179
199
  @input=${onInput}
180
200
  @keydown=${onKeydown}
181
201
  ${ref((el) => {
182
- if (el)
202
+ if (el) {
183
203
  requestAnimationFrame(() => {
184
204
  (el as HTMLElement).focus();
185
205
  const input = (el as HTMLElement).shadowRoot?.querySelector("input");
186
- if (input) input.select();
206
+ if (input) {
207
+ input.select();
208
+ }
187
209
  });
210
+ }
188
211
  })}
189
212
  >
190
213
  <sp-help-text slot="negative-help-text">${error}</sp-help-text>
@@ -2,7 +2,7 @@
2
2
  // ─── Convert to Repeater ──────────────────────────────────────────────────────
3
3
  import { html, render as litRender, nothing } from "lit-html";
4
4
  import { ref } from "lit-html/directives/ref.js";
5
- import { getNodeAtPath, parentElementPath, childIndex } from "../store";
5
+ import { childIndex, childList, getNodeAtPath, parentElementPath } from "../store";
6
6
  import { activeTab } from "../workspace/workspace";
7
7
  import { transactDoc } from "../tabs/transact";
8
8
  import { showDialog } from "../ui/layers";
@@ -21,40 +21,56 @@ interface RepeaterConfig {
21
21
  /** Convert the currently selected element into a repeater template. */
22
22
  export async function convertToRepeater() {
23
23
  const tab = activeTab.value;
24
- if (!tab?.session.selection || tab.session.selection.length < 2) return;
24
+ if (!tab?.session.selection || tab.session.selection.length < 2) {
25
+ return;
26
+ }
25
27
 
26
28
  const path = tab.session.selection;
27
29
  const node = getNodeAtPath(tab.doc.document, path);
28
- if (!node) return;
30
+ if (!node) {
31
+ return;
32
+ }
29
33
 
30
34
  const defs = tab.doc.document.state || {};
31
35
  const config = await promptRepeaterConfig(defs);
32
- if (!config) return;
36
+ if (!config) {
37
+ return;
38
+ }
33
39
 
34
40
  transactDoc(tab, (t) => {
35
41
  const doc = t.doc.document;
36
42
  if (config.newDef) {
37
- if (!doc.state) doc.state = {};
38
- doc.state[config.newDef.name] = { type: "array", default: [] };
43
+ if (!doc.state) {
44
+ doc.state = {};
45
+ }
46
+ doc.state[config.newDef.name] = { default: [], type: "array" };
39
47
  }
40
48
  const pp = parentElementPath(path);
41
- if (!pp) return;
49
+ if (!pp) {
50
+ return;
51
+ }
42
52
  const idx = childIndex(path) as number;
43
53
  const parent = getNodeAtPath(doc, pp);
44
- if (!parent?.children) return;
45
- const element = parent.children[idx];
54
+ if (!parent?.children) {
55
+ return;
56
+ }
57
+ const element = childList(parent)[idx];
46
58
 
47
59
  const repeater: Record<string, unknown> = {
48
60
  $prototype: "Array",
49
61
  items: config.items,
50
62
  map: element,
51
63
  };
52
- if (config.filter) repeater.filter = config.filter;
53
- if (config.sort) repeater.sort = config.sort;
64
+ if (config.filter) {
65
+ repeater.filter = config.filter;
66
+ }
67
+ if (config.sort) {
68
+ repeater.sort = config.sort;
69
+ }
54
70
 
55
71
  (parent.children as (string | JxMutableNode)[])[idx] = {
56
- tagName: "div",
57
72
  children: repeater as unknown as (string | JxMutableNode)[],
73
+ tagName: "div",
58
74
  };
59
75
  });
60
76
  }
@@ -75,8 +91,12 @@ async function promptRepeaterConfig(defs: Record<string, unknown>) {
75
91
  const docPath = tab?.documentPath;
76
92
  for (const [name, d] of Object.entries(defs)) {
77
93
  const def = d as Record<string, unknown> | null;
78
- if (!def?.$prototype || def.$prototype === "Function" || def.$prototype === "Array") continue;
79
- if (arrayDefs.some(([n]) => n === name)) continue;
94
+ if (!def?.$prototype || def.$prototype === "Function" || def.$prototype === "Array") {
95
+ continue;
96
+ }
97
+ if (arrayDefs.some(([n]) => n === name)) {
98
+ continue;
99
+ }
80
100
  const schema = await fetchPluginSchema(
81
101
  {
82
102
  ...(def.$src != null && { $src: def.$src as string }),
@@ -132,9 +152,11 @@ async function promptRepeaterConfig(defs: Record<string, unknown>) {
132
152
  }
133
153
 
134
154
  function rerender() {
135
- const layer = document.getElementById("layer-dialog");
155
+ const layer = document.querySelector("#layer-dialog");
136
156
  const slot = layer?.lastElementChild;
137
- if (slot) litRender(buildTpl(), slot as HTMLElement);
157
+ if (slot) {
158
+ litRender(buildTpl(), slot as HTMLElement);
159
+ }
138
160
  }
139
161
 
140
162
  function buildTpl() {
@@ -179,18 +201,21 @@ async function promptRepeaterConfig(defs: Record<string, unknown>) {
179
201
  size="s"
180
202
  placeholder="myItems"
181
203
  .value=${newDefName}
182
- ?negative=${!!error}
204
+ ?negative=${Boolean(error)}
183
205
  @input=${(e: Event) => {
184
206
  newDefName = (e.target as HTMLInputElement).value || "";
185
207
  error = "";
186
208
  rerender();
187
209
  }}
188
210
  @keydown=${(e: KeyboardEvent) => {
189
- if (e.key === "Enter") confirm();
211
+ if (e.key === "Enter") {
212
+ confirm();
213
+ }
190
214
  }}
191
215
  ${ref((el) => {
192
- if (el && source === "__new__")
216
+ if (el && source === "__new__") {
193
217
  requestAnimationFrame(() => (el as HTMLElement).focus());
218
+ }
194
219
  })}
195
220
  >
196
221
  <sp-help-text slot="negative-help-text">${error}</sp-help-text>