@jxsuite/studio 0.28.4 → 0.29.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 (81) hide show
  1. package/dist/studio.js +6769 -5399
  2. package/dist/studio.js.map +86 -79
  3. package/package.json +5 -4
  4. package/src/browse/browse-modal.ts +2 -2
  5. package/src/browse/browse.ts +20 -19
  6. package/src/canvas/canvas-diff.ts +3 -3
  7. package/src/canvas/canvas-helpers.ts +15 -2
  8. package/src/canvas/canvas-live-render.ts +168 -86
  9. package/src/canvas/canvas-patcher.ts +656 -0
  10. package/src/canvas/canvas-perf.ts +68 -0
  11. package/src/canvas/canvas-render.ts +135 -23
  12. package/src/canvas/canvas-subtree-render.ts +113 -0
  13. package/src/canvas/canvas-utils.ts +4 -0
  14. package/src/editor/component-inline-edit.ts +4 -35
  15. package/src/editor/context-menu.ts +92 -74
  16. package/src/editor/convert-to-component.ts +11 -2
  17. package/src/editor/convert-to-repeater.ts +4 -5
  18. package/src/editor/inline-edit.ts +9 -9
  19. package/src/editor/inline-format.ts +11 -11
  20. package/src/editor/merge-tags.ts +120 -0
  21. package/src/editor/shortcuts.ts +4 -4
  22. package/src/files/components.ts +37 -0
  23. package/src/files/file-ops.ts +28 -7
  24. package/src/files/files.ts +25 -21
  25. package/src/github/github-auth.ts +14 -2
  26. package/src/github/github-publish.ts +12 -2
  27. package/src/panels/activity-bar.ts +1 -1
  28. package/src/panels/ai-panel.ts +67 -39
  29. package/src/panels/block-action-bar.ts +72 -1
  30. package/src/panels/canvas-dnd.ts +55 -26
  31. package/src/panels/data-explorer.ts +5 -3
  32. package/src/panels/dnd.ts +12 -15
  33. package/src/panels/editors.ts +5 -23
  34. package/src/panels/elements-panel.ts +2 -12
  35. package/src/panels/events-panel.ts +1 -1
  36. package/src/panels/git-panel.ts +6 -6
  37. package/src/panels/head-panel.ts +1 -1
  38. package/src/panels/layers-panel.ts +177 -147
  39. package/src/panels/preview-render.ts +15 -0
  40. package/src/panels/properties-panel.ts +16 -27
  41. package/src/panels/quick-search.ts +2 -2
  42. package/src/panels/right-panel.ts +2 -6
  43. package/src/panels/shared.ts +3 -0
  44. package/src/panels/signals-panel.ts +24 -22
  45. package/src/panels/statusbar.ts +15 -6
  46. package/src/panels/style-panel.ts +3 -3
  47. package/src/panels/style-utils.ts +3 -3
  48. package/src/panels/stylebook-panel.ts +4 -4
  49. package/src/panels/tab-bar.ts +198 -0
  50. package/src/panels/tab-strip.ts +2 -2
  51. package/src/panels/toolbar.ts +6 -75
  52. package/src/platforms/devserver.ts +60 -34
  53. package/src/reactivity.ts +2 -0
  54. package/src/services/cem-export.ts +23 -2
  55. package/src/services/code-services.ts +16 -2
  56. package/src/services/monaco-setup.ts +2 -1
  57. package/src/settings/content-types-editor.ts +16 -16
  58. package/src/settings/css-vars-editor.ts +2 -2
  59. package/src/settings/defs-editor.ts +14 -14
  60. package/src/settings/general-settings.ts +6 -6
  61. package/src/settings/head-editor.ts +2 -2
  62. package/src/site-context.ts +1 -1
  63. package/src/state.ts +66 -12
  64. package/src/store.ts +15 -3
  65. package/src/studio.ts +73 -33
  66. package/src/tabs/patch-ops.ts +143 -0
  67. package/src/tabs/tab.ts +15 -1
  68. package/src/tabs/transact.ts +454 -27
  69. package/src/types.ts +41 -0
  70. package/src/ui/color-selector.ts +7 -7
  71. package/src/ui/icons.ts +0 -30
  72. package/src/ui/media-picker.ts +2 -2
  73. package/src/ui/panel-resize.ts +6 -1
  74. package/src/ui/unit-selector.ts +2 -2
  75. package/src/ui/value-selector.ts +3 -3
  76. package/src/utils/canvas-media.ts +6 -6
  77. package/src/utils/edit-display.ts +125 -83
  78. package/src/utils/google-fonts.ts +1 -1
  79. package/src/utils/inherited-style.ts +3 -2
  80. package/src/utils/studio-utils.ts +1 -1
  81. package/src/view.ts +4 -1
@@ -20,7 +20,7 @@ import type { CanvasPanel } from "../types";
20
20
 
21
21
  import type { JxPath } from "../state";
22
22
 
23
- export type { CanvasPanel };
23
+ export type { CanvasPanel } from "../types";
24
24
 
25
25
  interface DropInstruction {
26
26
  type: "reorder-above" | "reorder-below" | "make-child";
@@ -125,34 +125,63 @@ export function registerPanelDnD(panel: CanvasPanel) {
125
125
  });
126
126
  view.canvasDndCleanups.push(monitorCleanup);
127
127
 
128
- const document = activeTab.value?.doc.document;
129
128
  for (const el of allEls) {
130
- const elPath = elToPath.get(el);
131
- if (!elPath) {
129
+ if (!elToPath.get(el)) {
132
130
  continue;
133
131
  }
132
+ registerElementDropTarget(el);
133
+ }
134
+ }
134
135
 
135
- const node = getNodeAtPath(document!, elPath);
136
- const tag = (node?.tagName || "div").toLowerCase();
137
- const hasElementChildren =
138
- Array.isArray(node?.children) &&
139
- node.children.some((c: unknown) => c != null && typeof c === "object");
140
- const isLeaf = VOID_ELEMENTS.has(tag) || !hasElementChildren;
136
+ /**
137
+ * Register one canvas element as a drop target. Path and leaf-ness are read live from elToPath and
138
+ * the current document at drag time, so surgical patches that remap sibling paths never leave stale
139
+ * closures behind.
140
+ *
141
+ * @param {Element} el
142
+ */
143
+ function registerElementDropTarget(el: Element) {
144
+ const cleanup = dropTargetForElements({
145
+ canDrop({ source }) {
146
+ const elPath = elToPath.get(el);
147
+ if (!elPath) {
148
+ return false;
149
+ }
150
+ const srcPath = source.data.path as JxPath | undefined;
151
+ if (srcPath && isAncestor(srcPath, elPath)) {
152
+ return false;
153
+ }
154
+ return true;
155
+ },
156
+ element: /** @type {HTMLElement} */ el,
157
+ getData() {
158
+ const elPath = elToPath.get(el) ?? [];
159
+ const document = activeTab.value?.doc.document;
160
+ const node = document ? getNodeAtPath(document, elPath) : undefined;
161
+ const tag = (node?.tagName || "div").toLowerCase();
162
+ const hasElementChildren =
163
+ Array.isArray(node?.children) &&
164
+ node.children.some((c: unknown) => c != null && typeof c === "object");
165
+ return { _isVoid: VOID_ELEMENTS.has(tag) || !hasElementChildren, path: elPath };
166
+ },
167
+ });
168
+ view.canvasDndCleanups.push(cleanup);
169
+ }
141
170
 
142
- const cleanup = dropTargetForElements({
143
- canDrop({ source }) {
144
- const srcPath = source.data.path as JxPath | undefined;
145
- if (srcPath && isAncestor(srcPath, elPath)) {
146
- return false;
147
- }
148
- return true;
149
- },
150
- element: /** @type {HTMLElement} */ el,
151
- getData() {
152
- return { _isVoid: isLeaf, path: elPath };
153
- },
154
- });
155
- view.canvasDndCleanups.push(cleanup);
171
+ /**
172
+ * Register drop targets for a freshly patched-in subtree (root plus descendants). Cleanups join
173
+ * view.canvasDndCleanups and are released by the next full render like all canvas DnD handlers.
174
+ *
175
+ * @param {HTMLElement} rootEl
176
+ */
177
+ export function registerSubtreeDnD(rootEl: HTMLElement) {
178
+ if (elToPath.get(rootEl)) {
179
+ registerElementDropTarget(rootEl);
180
+ }
181
+ for (const el of rootEl.querySelectorAll("*")) {
182
+ if (elToPath.get(el)) {
183
+ registerElementDropTarget(el);
184
+ }
156
185
  }
157
186
  }
158
187
 
@@ -229,7 +258,7 @@ function nearestChildEdge(children: HTMLElement[], cursorY: number, parentPath:
229
258
  let closestIdx = children.length - 1;
230
259
 
231
260
  for (let i = 0; i < children.length; i++) {
232
- const rect = children[i].getBoundingClientRect();
261
+ const rect = children[i]!.getBoundingClientRect();
233
262
  const topDist = Math.abs(cursorY - rect.top);
234
263
  const bottomDist = Math.abs(cursorY - rect.bottom);
235
264
 
@@ -248,7 +277,7 @@ function nearestChildEdge(children: HTMLElement[], cursorY: number, parentPath:
248
277
  const childPath = [...parentPath, "children", closestIdx];
249
278
  return {
250
279
  instruction,
251
- referenceEl: children[closestIdx],
280
+ referenceEl: children[closestIdx]!,
252
281
  targetPath: childPath,
253
282
  };
254
283
  }
@@ -9,7 +9,7 @@ import type { TemplateResult } from "lit-html";
9
9
  const expandedDataKeys = new Set();
10
10
 
11
11
  /** Unwrap a Vue ref (has .value and .__v_isRef) to get the underlying value. */
12
- function unwrapSignal(value: unknown) {
12
+ export function unwrapSignal(value: unknown) {
13
13
  if (value && typeof value === "object" && (value as Record<string, unknown>).__v_isRef) {
14
14
  return (value as Record<string, unknown>).value;
15
15
  }
@@ -17,7 +17,7 @@ function unwrapSignal(value: unknown) {
17
17
  }
18
18
 
19
19
  /** Type label for a signal value in the data explorer. */
20
- function dataTypeLabel(value: unknown) {
20
+ export function dataTypeLabel(value: unknown) {
21
21
  const v = unwrapSignal(value);
22
22
  if (v === null) {
23
23
  return "null";
@@ -164,7 +164,9 @@ export function renderDataTreeTemplate(
164
164
  ><span class="data-value data-${item === null ? "null" : typeof item}">${valText}</span>
165
165
  </div>`;
166
166
  }
167
- const label = Array.isArray(item) ? `Array(${item.length})` : `{${Object.keys(item).length}}`;
167
+ const label = Array.isArray(item)
168
+ ? `Array(${item.length})`
169
+ : `{${Object.keys(item as object).length}}`;
168
170
  return html`
169
171
  <div class="data-branch" style="padding-left:${indent}">
170
172
  <span class="data-key">[${i}] </span
package/src/panels/dnd.ts CHANGED
@@ -27,7 +27,11 @@ import {
27
27
  import { mutateInsertNode, mutateMoveNode, transact, transactDoc } from "../tabs/transact";
28
28
  import { activeTab } from "../workspace/workspace";
29
29
  import { view } from "../view";
30
- import { componentRegistry, computeRelativePath } from "../files/components";
30
+ import {
31
+ buildComponentInstance,
32
+ componentRegistry,
33
+ computeRelativePath,
34
+ } from "../files/components";
31
35
  import { renderComponentPreview } from "./stylebook-panel";
32
36
  import { defaultDef, unsafeTags } from "./shared";
33
37
  import type { JxPath } from "../state";
@@ -205,25 +209,13 @@ export function registerComponentsDnD() {
205
209
  // Fill preview with live rendered component
206
210
  const preview = row.querySelector(".element-card-preview");
207
211
  if (preview && !preview.querySelector(tagName)) {
208
- renderComponentPreview(comp).then((el: HTMLElement) => {
212
+ void renderComponentPreview(comp).then((el: HTMLElement) => {
209
213
  preview.textContent = "";
210
214
  preview.append(el);
211
215
  });
212
216
  }
213
217
 
214
- const instanceDef = {
215
- $props: comp.props
216
- ? Object.fromEntries(
217
- comp.props.map(
218
- (/** @type {{ name: string; default?: unknown; [k: string]: unknown }} */ p) => [
219
- p.name,
220
- p.default !== undefined ? p.default : "",
221
- ],
222
- ),
223
- )
224
- : {},
225
- tagName: comp.tagName,
226
- };
218
+ const instanceDef = buildComponentInstance(comp);
227
219
  const cleanup = draggable({
228
220
  element: row,
229
221
  getInitialData() {
@@ -336,6 +328,11 @@ export function clearLayerDropGap(container: HTMLElement) {
336
328
  const rows = container.querySelectorAll(".layers-tree .layer-row");
337
329
  for (const r of rows) {
338
330
  (r as HTMLElement).style.transform = "";
331
+ // Also clear `display:none` left by hideDescendantRows. The `.layer-row` div has no `style`
332
+ // Lit binding and rows aren't keyed, so lit reuses these DOM nodes positionally on the
333
+ // Post-drop re-render — a stale `display:none` would otherwise hide whichever row lands on the
334
+ // Reused node (e.g. a sibling of the moved subtree).
335
+ (r as HTMLElement).style.display = "";
339
336
  }
340
337
  }
341
338
 
@@ -38,8 +38,7 @@ function getFunctionBody(editing: EditingTarget | null | undefined) {
38
38
  return "";
39
39
  }
40
40
 
41
- /** @param {() => void} closeFunctionEditor */
42
- export function renderFunctionEditor(closeFunctionEditor: () => void) {
41
+ export function renderFunctionEditor() {
43
42
  const editing = activeTab.value?.session.ui.editingFunction as EditingTarget | null | undefined;
44
43
 
45
44
  // If editor already exists and matches current target, just sync value
@@ -79,27 +78,10 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
79
78
  canvasWrap.style.flexDirection = "column";
80
79
  canvasWrap.style.alignItems = "stretch";
81
80
 
82
- // Toolbar breadcrumb handles context display re-render it
83
- renderOnly("toolbar");
84
-
85
- const tab = activeTab.value;
86
- const docName = tab?.documentPath?.split("/").pop() || tab?.doc.document?.tagName || "document";
87
- const ed = editing as EditingTarget;
88
- const funcLabel = ed.type === "def" ? `ƒ ${ed.defName}` : `ƒ ${ed.eventKey}`;
89
-
90
- // Editor container
81
+ // The tab bar renders the Back button + breadcrumb context for the function editor.
91
82
  let editorContainer: HTMLDivElement | null = null;
92
83
  litRender(
93
84
  html`<div class="source-wrap">
94
- <div class="source-toolbar">
95
- <sp-action-button size="s" @click=${closeFunctionEditor}>
96
- <sp-icon-back slot="icon"></sp-icon-back>
97
- Back
98
- </sp-action-button>
99
- <span class="breadcrumb-item">${docName}</span>
100
- <span class="breadcrumb-sep"> › </span>
101
- <span class="breadcrumb-item current">${funcLabel}</span>
102
- </div>
103
85
  <div
104
86
  class="source-editor"
105
87
  ${ref((el) => {
@@ -135,13 +117,13 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
135
117
  const editor = view.functionEditor;
136
118
 
137
119
  // Format on open — show pretty-printed code, then run initial lint
138
- codeService("format", { args, code: body }).then((result) => {
120
+ void codeService("format", { args, code: body }).then((result) => {
139
121
  if (result?.code != null && view.functionEditor) {
140
122
  view.functionEditor._ignoreNextChange = true;
141
123
  view.functionEditor.setValue(result.code);
142
124
  }
143
125
  });
144
- codeService("lint", { args, code: body }).then((result) => {
126
+ void codeService("lint", { args, code: body }).then((result) => {
145
127
  if (result?.diagnostics && view.functionEditor) {
146
128
  setLintMarkers(view.functionEditor, result.diagnostics as OxLintDiagnostic[]);
147
129
  }
@@ -183,7 +165,7 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
183
165
  lintDebounce = setTimeout(() => {
184
166
  const gen = (lintGen += 1);
185
167
  const currentCode = editor.getValue();
186
- codeService("lint", { args, code: currentCode }).then((result) => {
168
+ void codeService("lint", { args, code: currentCode }).then((result) => {
187
169
  if (gen !== lintGen) {
188
170
  return;
189
171
  }
@@ -7,7 +7,7 @@ import { activeTab } from "../workspace/workspace";
7
7
  import { mutateInsertNode, transactDoc } from "../tabs/transact";
8
8
  import { view } from "../view";
9
9
  import { getEffectiveElements } from "../site-context";
10
- import { componentRegistry } from "../files/components";
10
+ import { buildComponentInstance, componentRegistry } from "../files/components";
11
11
 
12
12
  import type { ComponentEntry } from "../files/components";
13
13
  import type { JxElement, JxMutableNode } from "@jxsuite/schema/types";
@@ -134,17 +134,7 @@ export function renderElementsTemplate(ctx: {
134
134
  const parentPath = t?.session.selection || [];
135
135
  const parent = getNodeAtPath(t!.doc.document, parentPath);
136
136
  const idx = childList(parent).length;
137
- const instanceDef = {
138
- $props: Object.fromEntries(
139
- (comp.props || []).map(
140
- (/** @type {{ name: string; default?: unknown }} */ p) => [
141
- p.name,
142
- p.default !== undefined ? p.default : "",
143
- ],
144
- ),
145
- ),
146
- tagName: comp.tagName,
147
- };
137
+ const instanceDef = buildComponentInstance(comp);
148
138
  transactDoc(t!, (tr) =>
149
139
  mutateInsertNode(tr, parentPath, idx, structuredClone(instanceDef)),
150
140
  );
@@ -266,7 +266,7 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
266
266
  if (functionDefs.length > 0) {
267
267
  transactDoc(activeTab.value, (t) =>
268
268
  mutateUpdateProperty(t, selection, evName, {
269
- $ref: `#/state/${functionDefs[0][0]}`,
269
+ $ref: `#/state/${functionDefs[0]![0]}`,
270
270
  }),
271
271
  );
272
272
  } else {
@@ -132,7 +132,7 @@ async function gitAction(action: string, body?: unknown) {
132
132
  updateUi("gitLoading", true);
133
133
  updateUi("gitError", null);
134
134
  try {
135
- await plat[action](body);
135
+ await plat[action]!(body);
136
136
  await refreshGitStatus();
137
137
  } catch (error) {
138
138
  updateUi("gitError", errorMessage(error));
@@ -191,7 +191,7 @@ export function renderGitPanel(
191
191
  const loading = S.ui.gitLoading;
192
192
 
193
193
  if (!status && !loading) {
194
- refreshGitStatus();
194
+ void refreshGitStatus();
195
195
  return html`<div class="git-panel">
196
196
  <div class="git-loading">Loading...</div>
197
197
  </div>`;
@@ -232,7 +232,7 @@ export function renderGitPanel(
232
232
  if (!_pollTimer) {
233
233
  _pollTimer = setInterval(() => {
234
234
  if (view.leftTab === "git" && !S.ui.gitLoading) {
235
- refreshGitStatus();
235
+ void refreshGitStatus();
236
236
  }
237
237
  }, 30_000);
238
238
  }
@@ -417,7 +417,7 @@ export function renderGitPanel(
417
417
  const switchTab = (tab: string) => {
418
418
  _gitSubTab = tab;
419
419
  if (tab === "history" && !S.ui.gitLogEntries) {
420
- fetchGitLog();
420
+ void fetchGitLog();
421
421
  }
422
422
  renderOnly("leftPanel");
423
423
  };
@@ -453,7 +453,7 @@ export function renderGitPanel(
453
453
  @keydown=${(e: KeyboardEvent) => {
454
454
  if (e.ctrlKey && e.key === "Enter") {
455
455
  e.preventDefault();
456
- doCommit();
456
+ void doCommit();
457
457
  }
458
458
  }}
459
459
  ></sp-textfield>
@@ -489,7 +489,7 @@ export function renderGitPanel(
489
489
  "hidden",
490
490
  "",
491
491
  );
492
- doCommit();
492
+ void doCommit();
493
493
  }}
494
494
  >
495
495
  Commit (don't sync)
@@ -334,7 +334,7 @@ export function renderHeadTemplate({
334
334
  let layoutSection: TemplateResult | symbol = nothing;
335
335
  if (isPage) {
336
336
  if (layoutEntries === null) {
337
- loadLayoutEntries();
337
+ void loadLayoutEntries();
338
338
  } else {
339
339
  const currentLayout = doc.$layout;
340
340
  const defaultLayout = projectState?.projectConfig?.defaults?.layout;