@jxsuite/studio 0.22.1 → 0.23.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,9 +31,9 @@
31
31
  "dependencies": {
32
32
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
33
33
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
34
- "@jxsuite/parser": "^0.22.1",
35
- "@jxsuite/runtime": "^0.22.1",
36
- "@jxsuite/schema": "^0.22.1",
34
+ "@jxsuite/parser": "^0.23.0",
35
+ "@jxsuite/runtime": "^0.23.0",
36
+ "@jxsuite/schema": "^0.23.0",
37
37
  "@spectrum-web-components/accordion": "^1.12.1",
38
38
  "@spectrum-web-components/action-bar": "1.12.1",
39
39
  "@spectrum-web-components/action-button": "^1.12.1",
@@ -3,6 +3,7 @@ import { html, nothing } from "lit-html";
3
3
  import { live } from "lit-html/directives/live.js";
4
4
  import { activeTab } from "../workspace/workspace.js";
5
5
  import { transactDoc, mutateUpdateProperty } from "../tabs/transact.js";
6
+ import { renderExpressionEditor } from "../ui/expression-editor.js";
6
7
 
7
8
  export const EVENT_NAMES = [
8
9
  "onclick",
@@ -66,7 +67,7 @@ export function eventsSidebarTemplate(helpers) {
66
67
  if (!k.startsWith("on")) return false;
67
68
  const v = node[k];
68
69
  if (!v || typeof v !== "object") return false;
69
- return v.$ref || v.$prototype === "Function";
70
+ return v.$ref || v.$prototype === "Function" || v.$expression;
70
71
  });
71
72
 
72
73
  return html`
@@ -79,6 +80,8 @@ export function eventsSidebarTemplate(helpers) {
79
80
  ${eventKeys.map((evKey) => {
80
81
  const evVal = node[evKey];
81
82
  const isInline = evVal.$prototype === "Function";
83
+ const isExpression = evVal.$expression != null;
84
+ const currentMode = isInline ? "inline" : isExpression ? "$expression" : "ref";
82
85
  return html`
83
86
  <div class="event-binding">
84
87
  <div class="event-row">
@@ -103,9 +106,10 @@ export function eventsSidebarTemplate(helpers) {
103
106
  <sp-picker
104
107
  size="s"
105
108
  class="event-mode"
106
- .value=${live(isInline ? "inline" : "ref")}
109
+ .value=${live(currentMode)}
107
110
  @change=${(/** @type {Event} */ e) => {
108
- if (/** @type {HTMLInputElement} */ (e.target).value === "inline") {
111
+ const newMode = /** @type {HTMLInputElement} */ (e.target).value;
112
+ if (newMode === "inline") {
109
113
  transactDoc(activeTab.value, (t) =>
110
114
  mutateUpdateProperty(t, selection, evKey, {
111
115
  $prototype: "Function",
@@ -113,6 +117,12 @@ export function eventsSidebarTemplate(helpers) {
113
117
  parameters: [],
114
118
  }),
115
119
  );
120
+ } else if (newMode === "$expression") {
121
+ transactDoc(activeTab.value, (t) =>
122
+ mutateUpdateProperty(t, selection, evKey, {
123
+ $expression: { operator: "=", target: null },
124
+ }),
125
+ );
116
126
  } else {
117
127
  const firstFn = functionDefs[0];
118
128
  transactDoc(activeTab.value, (t) =>
@@ -127,6 +137,7 @@ export function eventsSidebarTemplate(helpers) {
127
137
  }}
128
138
  >
129
139
  <sp-menu-item value="inline">inline</sp-menu-item>
140
+ <sp-menu-item value="$expression">$expression</sp-menu-item>
130
141
  <sp-menu-item value="ref">$ref</sp-menu-item>
131
142
  </sp-picker>
132
143
  <sp-action-button
@@ -176,35 +187,50 @@ export function eventsSidebarTemplate(helpers) {
176
187
  </sp-action-button>
177
188
  </div>
178
189
  `
179
- : html`
180
- <sp-picker
181
- size="s"
182
- class="event-handler"
183
- .value=${live(evVal.$ref || "__none__")}
184
- @change=${(/** @type {Event} */ e) => {
185
- if (
186
- /** @type {HTMLInputElement} */ (e.target).value &&
187
- /** @type {HTMLInputElement} */ (e.target).value !== "__none__"
188
- ) {
189
- transactDoc(activeTab.value, (t) =>
190
- mutateUpdateProperty(t, selection, evKey, {
191
- $ref: /** @type {HTMLInputElement} */ (e.target).value,
192
- }),
193
- );
194
- } else {
195
- transactDoc(activeTab.value, (t) =>
196
- mutateUpdateProperty(t, selection, evKey, undefined),
197
- );
198
- }
199
- }}
200
- >
201
- <sp-menu-item value="__none__">— none —</sp-menu-item>
202
- ${functionDefs.map(
203
- ([fName]) =>
204
- html`<sp-menu-item value=${`#/state/${fName}`}>${fName}</sp-menu-item>`,
205
- )}
206
- </sp-picker>
207
- `}
190
+ : isExpression
191
+ ? html`
192
+ <div class="event-body-row">
193
+ ${renderExpressionEditor(
194
+ evVal.$expression,
195
+ (/** @type {any} */ newNode) =>
196
+ transactDoc(activeTab.value, (t) =>
197
+ mutateUpdateProperty(t, selection, evKey, {
198
+ $expression: newNode,
199
+ }),
200
+ ),
201
+ { stateDefs: Object.keys(defs), allowEventRef: true },
202
+ )}
203
+ </div>
204
+ `
205
+ : html`
206
+ <sp-picker
207
+ size="s"
208
+ class="event-handler"
209
+ .value=${live(evVal.$ref || "__none__")}
210
+ @change=${(/** @type {Event} */ e) => {
211
+ if (
212
+ /** @type {HTMLInputElement} */ (e.target).value &&
213
+ /** @type {HTMLInputElement} */ (e.target).value !== "__none__"
214
+ ) {
215
+ transactDoc(activeTab.value, (t) =>
216
+ mutateUpdateProperty(t, selection, evKey, {
217
+ $ref: /** @type {HTMLInputElement} */ (e.target).value,
218
+ }),
219
+ );
220
+ } else {
221
+ transactDoc(activeTab.value, (t) =>
222
+ mutateUpdateProperty(t, selection, evKey, undefined),
223
+ );
224
+ }
225
+ }}
226
+ >
227
+ <sp-menu-item value="__none__">— none —</sp-menu-item>
228
+ ${functionDefs.map(
229
+ ([fName]) =>
230
+ html`<sp-menu-item value=${`#/state/${fName}`}>${fName}</sp-menu-item>`,
231
+ )}
232
+ </sp-picker>
233
+ `}
208
234
  </div>
209
235
  `;
210
236
  })}
@@ -108,12 +108,14 @@ export function unmount() {
108
108
  _lastTab = null;
109
109
  }
110
110
 
111
+ let _rafId = 0;
112
+
111
113
  export function render() {
112
114
  if (!_ctx) return;
113
115
  if (_rendering) return;
114
116
  if (!_scheduled) {
115
117
  _scheduled = true;
116
- queueMicrotask(_flush);
118
+ _rafId = requestAnimationFrame(_flush);
117
119
  }
118
120
  }
119
121
 
@@ -143,8 +145,10 @@ function _ensureContainers() {
143
145
 
144
146
  function _flush() {
145
147
  _scheduled = false;
148
+ _rafId = 0;
146
149
  if (!_ctx) return;
147
150
  if (_rendering) return;
151
+ if (_hasFocus || isColorPopoverOpen()) return;
148
152
  _rendering = true;
149
153
  try {
150
154
  const ctx = /** @type {RightPanelCtx} */ (_ctx);
@@ -18,6 +18,7 @@ import {
18
18
  mutateRenameDef,
19
19
  } from "../tabs/transact.js";
20
20
  import { renderFieldRow } from "../ui/field-row.js";
21
+ import { renderExpressionEditor, expressionHint } from "../ui/expression-editor.js";
21
22
  import { renderMediaPicker } from "../ui/media-picker.js";
22
23
  import { fetchPluginSchema, pluginSchemaCache } from "../services/code-services.js";
23
24
 
@@ -118,6 +119,7 @@ const DEF_TEMPLATES = /** @type {Record<string, SignalDef>} */ ({
118
119
  map: { $prototype: "Map", default: {} },
119
120
  formData: { $prototype: "FormData", fields: {} },
120
121
  function: { $prototype: "Function", body: "", parameters: [] },
122
+ expression: { $expression: { operator: "=", target: null } },
121
123
  external: { $prototype: "", $src: "" },
122
124
  });
123
125
 
@@ -148,6 +150,7 @@ const STUDIO_RESERVED_KEYS = new Set([
148
150
  export function defCategory(def) {
149
151
  if (!def) return "state";
150
152
  const d = /** @type {SignalDef} */ (def);
153
+ if (d.$expression) return "expression";
151
154
  if (d.$handler || d.$prototype === "Function") return "function";
152
155
  if (d.$compute) return "computed";
153
156
  if (d.$prototype) return "data";
@@ -162,6 +165,7 @@ export function defCategory(def) {
162
165
  export function defBadgeLabel(def) {
163
166
  if (!def) return "S";
164
167
  const d = /** @type {SignalDef} */ (def);
168
+ if (d.$expression) return "E";
165
169
  if (d.$handler || d.$prototype === "Function") return "F";
166
170
  if (d.$compute) return "C";
167
171
  if (d.$prototype) return d.$prototype.charAt(0);
@@ -176,6 +180,7 @@ export function defBadgeLabel(def) {
176
180
  */
177
181
  export function defHint(name, def) {
178
182
  if (!def) return "";
183
+ if (def.$expression) return expressionHint(def.$expression);
179
184
  if (def.$prototype === "Function") {
180
185
  if (def.body) return def.body.length > 20 ? def.body.slice(0, 20) + "..." : def.body;
181
186
  if (def.$src) return def.$src;
@@ -314,6 +319,7 @@ export function renderSignalsTemplate(S, ctx) {
314
319
  state: [],
315
320
  computed: [],
316
321
  data: [],
322
+ expression: [],
317
323
  function: [],
318
324
  });
319
325
  for (const [name, def] of entries) {
@@ -324,6 +330,7 @@ export function renderSignalsTemplate(S, ctx) {
324
330
  { key: "state", label: "State", items: groups.state },
325
331
  { key: "computed", label: "Computed", items: groups.computed },
326
332
  { key: "data", label: "Data", items: groups.data },
333
+ { key: "expression", label: "Expressions", items: groups.expression },
327
334
  { key: "function", label: "Functions", items: groups.function },
328
335
  ];
329
336
 
@@ -462,6 +469,7 @@ export function renderSignalsTemplate(S, ctx) {
462
469
  )}`
463
470
  : nothing}
464
471
  <sp-menu-divider></sp-menu-divider>
472
+ <sp-menu-item value="expression">Expression</sp-menu-item>
465
473
  <sp-menu-item value="function">Function</sp-menu-item>
466
474
  </sp-picker>
467
475
  </div>
@@ -697,6 +705,16 @@ function renderSignalEditorTemplate(
697
705
  fields = renderDataSourceFields(S, name, def, textareaRow, pickerRow, ctx);
698
706
  } else if (cat === "function") {
699
707
  fields = renderFunctionFields(S, name, def, textareaRow, ctx);
708
+ } else if (cat === "expression") {
709
+ const exprNode = def.$expression || { operator: "=", target: null };
710
+ fields = html`
711
+ ${renderExpressionEditor(
712
+ exprNode,
713
+ (/** @type {any} */ newNode) =>
714
+ transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { $expression: newNode })),
715
+ { stateDefs: Object.keys(S.document.state || {}), allowEventRef: false },
716
+ )}
717
+ `;
700
718
  }
701
719
 
702
720
  return html`${nameField}${fields}`;
package/src/store.js CHANGED
@@ -131,7 +131,11 @@ export function stripEventHandlers(node) {
131
131
  /** @type {Record<string, unknown>} */
132
132
  const out = {};
133
133
  for (const [k, v] of Object.entries(node)) {
134
- if (k.startsWith("on") && typeof v === "object" && (v?.$ref || v?.$prototype === "Function"))
134
+ if (
135
+ k.startsWith("on") &&
136
+ typeof v === "object" &&
137
+ (v?.$ref || v?.$prototype === "Function" || v?.$expression)
138
+ )
135
139
  continue;
136
140
  if (k === "children") {
137
141
  out.children = Array.isArray(v) ? v.map(stripEventHandlers) : stripEventHandlers(v);
package/src/studio.js CHANGED
@@ -368,8 +368,9 @@ initWelcome({
368
368
  cloneRepository: () => cloneRepository({ openRecentProject }),
369
369
  });
370
370
 
371
- // Effect-driven canvas rendering: auto-triggers renderCanvas when reactive deps change
372
- let _canvasRenderScheduled = false;
371
+ // Effect-driven canvas rendering: auto-triggers renderCanvas when reactive deps change.
372
+ // Uses double-RAF so the canvas render yields to higher-priority panel paints first.
373
+ let _canvasRafId = 0;
373
374
  effect(() => {
374
375
  const tab = activeTab.value;
375
376
  if (tab) {
@@ -383,15 +384,16 @@ effect(() => {
383
384
  void tab.session.ui.stylebookFilter;
384
385
  void tab.session.ui.stylebookCustomizedOnly;
385
386
  }
386
- if (!_canvasRenderScheduled) {
387
- _canvasRenderScheduled = true;
388
- queueMicrotask(() => {
389
- _canvasRenderScheduled = false;
390
- try {
391
- renderCanvas();
392
- } catch (e) {
393
- console.error("renderCanvas error:", e);
394
- }
387
+ if (!_canvasRafId) {
388
+ _canvasRafId = requestAnimationFrame(() => {
389
+ _canvasRafId = requestAnimationFrame(() => {
390
+ _canvasRafId = 0;
391
+ try {
392
+ renderCanvas();
393
+ } catch (e) {
394
+ console.error("renderCanvas error:", e);
395
+ }
396
+ });
395
397
  });
396
398
  }
397
399
  });