@jxsuite/studio 0.33.0 → 0.35.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 (106) hide show
  1. package/dist/iframe-entry.js +6238 -0
  2. package/dist/iframe-entry.js.map +35 -0
  3. package/dist/studio.js +30972 -17222
  4. package/dist/studio.js.map +328 -83
  5. package/package.json +9 -9
  6. package/src/browse/browse.ts +11 -4
  7. package/src/canvas/canvas-helpers.ts +2 -56
  8. package/src/canvas/canvas-live-render.ts +102 -435
  9. package/src/canvas/canvas-origin.ts +66 -0
  10. package/src/canvas/canvas-patcher.ts +63 -403
  11. package/src/canvas/canvas-render.ts +70 -212
  12. package/src/canvas/canvas-utils.ts +37 -65
  13. package/src/canvas/iframe-channel.ts +154 -0
  14. package/src/canvas/iframe-drop.ts +484 -0
  15. package/src/canvas/iframe-entry.ts +600 -0
  16. package/src/canvas/iframe-host.ts +1373 -0
  17. package/src/canvas/iframe-inline-edit.ts +367 -0
  18. package/src/canvas/iframe-insert.ts +164 -0
  19. package/src/canvas/iframe-interaction.ts +176 -0
  20. package/src/canvas/iframe-keys.ts +85 -0
  21. package/src/canvas/iframe-overlay.ts +218 -0
  22. package/src/canvas/iframe-patch.ts +363 -0
  23. package/src/canvas/iframe-protocol.ts +361 -0
  24. package/src/canvas/iframe-render.ts +458 -0
  25. package/src/canvas/iframe-slash.ts +114 -0
  26. package/src/canvas/iframe-subtree.ts +113 -0
  27. package/src/canvas/path-mapping.ts +86 -0
  28. package/src/canvas/serialize-scope.ts +65 -0
  29. package/src/editor/canvas-context-menu.ts +40 -0
  30. package/src/editor/canvas-slash-bridge.ts +21 -0
  31. package/src/editor/context-menu.ts +2 -1
  32. package/src/editor/inline-edit-apply.ts +183 -0
  33. package/src/editor/inline-edit.ts +99 -21
  34. package/src/editor/inline-link.ts +89 -0
  35. package/src/editor/insert-zone-action.ts +35 -0
  36. package/src/editor/merge-tags.ts +26 -2
  37. package/src/editor/repeater-scope.ts +144 -0
  38. package/src/editor/shortcuts.ts +14 -28
  39. package/src/editor/slash-menu.ts +73 -42
  40. package/src/files/files.ts +5 -1
  41. package/src/new-project/design-fields.ts +260 -0
  42. package/src/new-project/import-tab.ts +322 -0
  43. package/src/new-project/new-project-modal.ts +472 -89
  44. package/src/new-project/templates.ts +61 -0
  45. package/src/packages/ensure-deps.ts +6 -0
  46. package/src/packages/pull-package-sync.ts +339 -0
  47. package/src/page-params.ts +383 -0
  48. package/src/panels/ai-chat/attached-context.ts +49 -0
  49. package/src/panels/ai-chat/chat-markdown.ts +38 -0
  50. package/src/panels/ai-chat/chat-view.ts +250 -0
  51. package/src/panels/ai-chat/composer.ts +315 -0
  52. package/src/panels/ai-chat/sessions-view.ts +98 -0
  53. package/src/panels/ai-panel.ts +189 -457
  54. package/src/panels/block-action-bar.ts +296 -138
  55. package/src/panels/canvas-dnd-bridge.ts +397 -0
  56. package/src/panels/component-preview.ts +56 -0
  57. package/src/panels/dnd.ts +41 -17
  58. package/src/panels/drag-ghost.ts +62 -0
  59. package/src/panels/editors.ts +1 -1
  60. package/src/panels/git-panel.ts +16 -1
  61. package/src/panels/overlays.ts +10 -125
  62. package/src/panels/properties-panel.ts +210 -0
  63. package/src/panels/right-panel.ts +21 -7
  64. package/src/panels/signals-panel.ts +136 -22
  65. package/src/panels/stylebook-doc.ts +373 -0
  66. package/src/panels/stylebook-panel.ts +46 -689
  67. package/src/panels/tab-bar.ts +159 -13
  68. package/src/panels/toolbar.ts +3 -2
  69. package/src/platforms/devserver.ts +45 -1
  70. package/src/services/agent-seed.ts +91 -0
  71. package/src/services/ai-models.ts +64 -0
  72. package/src/services/ai-session-store.ts +250 -0
  73. package/src/services/ai-settings.ts +5 -0
  74. package/src/services/automation.ts +140 -0
  75. package/src/services/document-assistant.ts +98 -38
  76. package/src/services/import-client.ts +134 -0
  77. package/src/services/monaco-setup.ts +12 -0
  78. package/src/services/render-critic.ts +9 -9
  79. package/src/services/settings-store.ts +93 -0
  80. package/src/settings/css-vars-editor.ts +2 -2
  81. package/src/store.ts +4 -62
  82. package/src/studio.ts +115 -40
  83. package/src/tabs/doc-op-apply.ts +89 -0
  84. package/src/tabs/patch-ops.ts +6 -2
  85. package/src/tabs/tab.ts +23 -4
  86. package/src/tabs/transact.ts +2 -74
  87. package/src/types.ts +98 -18
  88. package/src/ui/ai-credentials-form.ts +205 -0
  89. package/src/ui/jx-theme.ts +63 -0
  90. package/src/ui/media-picker.ts +6 -4
  91. package/src/ui/spectrum.ts +13 -0
  92. package/src/utils/canvas-media.ts +0 -137
  93. package/src/utils/edit-display.ts +23 -3
  94. package/src/utils/geometry.ts +43 -0
  95. package/src/utils/link-target.ts +93 -0
  96. package/src/utils/strip-events.ts +54 -0
  97. package/src/view.ts +0 -23
  98. package/src/workspace/workspace.ts +14 -1
  99. package/src/canvas/canvas-subtree-render.ts +0 -113
  100. package/src/editor/component-inline-edit.ts +0 -349
  101. package/src/editor/content-inline-edit.ts +0 -207
  102. package/src/editor/insertion-helper.ts +0 -308
  103. package/src/panels/canvas-dnd.ts +0 -329
  104. package/src/panels/panel-events.ts +0 -306
  105. package/src/panels/preview-render.ts +0 -132
  106. package/src/panels/pseudo-preview.ts +0 -75
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Shared client for the /__studio/import-site NDJSON stream. Every platform's importSite is "a
3
+ * streaming fetch to some URL" — the dev server posts to its own origin, the desktop platforms post
4
+ * to their token-gated loopback servers — so the request/parse/settle logic lives here once.
5
+ *
6
+ * The endpoint emits one JSON object per line: `progress` lines (forwarded to onProgress),
7
+ * `heartbeat` keep-alives (ignored), and a terminal `done` ({root, config}) or `error` line.
8
+ */
9
+
10
+ import type { ImportProgressEvent, ImportSiteOptions } from "../types";
11
+ import type { ProjectConfig } from "@jxsuite/schema/types";
12
+
13
+ interface StreamLine {
14
+ type: string;
15
+ phase?: string;
16
+ message?: string;
17
+ current?: number;
18
+ total?: number;
19
+ root?: string;
20
+ config?: ProjectConfig;
21
+ error?: string;
22
+ }
23
+
24
+ /**
25
+ * POST an import request and consume its NDJSON progress stream.
26
+ *
27
+ * @param {string} endpoint — the platform's import-site URL (may carry an auth token)
28
+ * @param {ImportSiteOptions} opts — the import request; `apiKey`/`baseUrl` travel as headers
29
+ * @param {(evt: ImportProgressEvent) => void} onProgress
30
+ * @param {AbortSignal} [signal]
31
+ */
32
+ export async function streamImport(
33
+ endpoint: string,
34
+ opts: ImportSiteOptions,
35
+ onProgress: (evt: ImportProgressEvent) => void,
36
+ signal?: AbortSignal,
37
+ ): Promise<{ root: string; config: ProjectConfig }> {
38
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
39
+ if (opts.apiKey) {
40
+ headers["X-Api-Key"] = opts.apiKey;
41
+ }
42
+ if (opts.baseUrl) {
43
+ headers["X-Api-Base-URL"] = opts.baseUrl;
44
+ }
45
+
46
+ const res = await fetch(endpoint, {
47
+ body: JSON.stringify({
48
+ url: opts.url,
49
+ directory: opts.directory,
50
+ depth: opts.depth,
51
+ maxPages: opts.maxPages,
52
+ aiComponents: opts.aiComponents,
53
+ ...(opts.model === undefined ? {} : { aiModel: opts.model }),
54
+ }),
55
+ headers,
56
+ method: "POST",
57
+ ...(signal === undefined ? {} : { signal }),
58
+ });
59
+
60
+ if (!res.ok) {
61
+ let message = `Import failed (${res.status})`;
62
+ try {
63
+ const body = (await res.json()) as { error?: string };
64
+ if (body.error) {
65
+ message = body.error;
66
+ }
67
+ } catch {
68
+ /* Non-JSON error body — keep the status message. */
69
+ }
70
+ throw new Error(message);
71
+ }
72
+ if (!res.body) {
73
+ throw new Error("Import stream had no response body");
74
+ }
75
+
76
+ const reader = res.body.getReader();
77
+ const decoder = new TextDecoder();
78
+ let buffer = "";
79
+ let result: { root: string; config: ProjectConfig } | null = null;
80
+
81
+ const handleLine = (line: string) => {
82
+ const trimmed = line.trim();
83
+ if (!trimmed) {
84
+ return;
85
+ }
86
+ let parsed: StreamLine;
87
+ try {
88
+ parsed = JSON.parse(trimmed) as StreamLine;
89
+ } catch {
90
+ return; // Tolerate a mangled line rather than killing the whole import.
91
+ }
92
+ if (parsed.type === "progress") {
93
+ onProgress({
94
+ phase: parsed.phase ?? "",
95
+ message: parsed.message ?? "",
96
+ ...(parsed.current === undefined ? {} : { current: parsed.current }),
97
+ ...(parsed.total === undefined ? {} : { total: parsed.total }),
98
+ });
99
+ } else if (parsed.type === "done" && parsed.root !== undefined && parsed.config) {
100
+ result = { root: parsed.root, config: parsed.config };
101
+ } else if (parsed.type === "error") {
102
+ throw new Error(parsed.error || "Import failed");
103
+ }
104
+ // Heartbeats and unknown line types are ignored.
105
+ };
106
+
107
+ try {
108
+ for (;;) {
109
+ const { done, value } = await reader.read();
110
+ if (done) {
111
+ break;
112
+ }
113
+ buffer += decoder.decode(value, { stream: true });
114
+ // Lines can split across chunks: keep the trailing partial in the buffer.
115
+ const lines = buffer.split("\n");
116
+ buffer = lines.pop() ?? "";
117
+ for (const line of lines) {
118
+ handleLine(line);
119
+ }
120
+ }
121
+ handleLine(buffer);
122
+ } finally {
123
+ try {
124
+ await reader.cancel();
125
+ } catch {
126
+ /* The stream is already closed (e.g. after an abort). */
127
+ }
128
+ }
129
+
130
+ if (!result) {
131
+ throw new Error("Import stream ended without a result");
132
+ }
133
+ return result;
134
+ }
@@ -23,6 +23,18 @@ self.MonacoEnvironment = {
23
23
  },
24
24
  };
25
25
 
26
+ /* Monaco caches glyph widths when an editor is created. JetBrains Mono is a
27
+ vendored webfont (index.html @font-face) and may finish loading after the
28
+ first editor mounts, so remeasure once all document fonts are ready to keep
29
+ cursor and selection alignment correct. */
30
+ if (typeof document !== "undefined" && document.fonts?.ready) {
31
+ // oxlint-disable-next-line unicorn/prefer-top-level-await -- fire-and-forget: awaiting fonts.ready at top level would block module evaluation (and studio startup) until webfonts load
32
+ void document.fonts.ready.then(async () => {
33
+ const monaco = await import("monaco-editor/esm/vs/editor/editor.api.js");
34
+ monaco.editor.remeasureFonts();
35
+ });
36
+ }
37
+
26
38
  // oxlint-disable-next-line typescript/no-unsafe-call, typescript/no-unsafe-member-access -- jsonDefaults is imported from Monaco's untyped ESM contribution (see @ts-expect-error above); no type declarations exist for this named export
27
39
  jsonDefaults.setDiagnosticsOptions({
28
40
  allowComments: false,
@@ -13,9 +13,8 @@
13
13
  * @license MIT
14
14
  */
15
15
 
16
- import { buildScope, renderNode, setSkipServerFunctions } from "@jxsuite/runtime";
16
+ import { buildScope, renderNode, runScoped, setSkipServerFunctions } from "@jxsuite/runtime";
17
17
  import type { JxDocument } from "@jxsuite/schema/types";
18
- import { effectScope } from "../reactivity";
19
18
 
20
19
  /**
21
20
  * Translate a render-time error into an actionable message the model can fix.
@@ -51,25 +50,26 @@ function translateRenderError(err: Error): string {
51
50
  export async function renderCheck(
52
51
  doc: JxDocument,
53
52
  ): Promise<{ ok: true } | { ok: false; error: string }> {
54
- const scope = effectScope();
53
+ let stopRender: (() => void) | null = null;
55
54
  try {
56
55
  setSkipServerFunctions(true);
57
56
 
58
57
  const state = await buildScope(doc, {});
59
58
 
60
59
  const container = document.createElement("div");
61
- scope.run(() => {
60
+ const { stop } = runScoped(() => {
62
61
  container.append(renderNode(doc, state));
63
62
  });
63
+ stopRender = stop;
64
64
 
65
65
  return { ok: true };
66
66
  } catch (error) {
67
67
  return { ok: false, error: translateRenderError(error as Error) };
68
68
  } finally {
69
- // Dispose all effects created synchronously by renderNode. Effects from the async
70
- // BuildScope (prototype/computed setup) escape the scope because Vue tracks the active
71
- // Scope synchronously they're GC-eligible once the throwaway state/container are
72
- // Unreferenced. This bounded leak is acceptable for v1.
73
- scope.stop();
69
+ // Dispose all effects created synchronously by renderNode via the RUNTIME's runScoped — scope
70
+ // Collection is per @vue/reactivity module instance, so a studio effectScope here would collect
71
+ // Nothing. Effects from the async buildScope (prototype/computed setup) still escape the scope;
72
+ // They're GC-eligible once the throwaway state/container are unreferenced. Bounded, acceptable.
73
+ stopRender?.();
74
74
  }
75
75
  }
@@ -0,0 +1,93 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Settings-store.js — sync of user settings between localStorage and the platform backend.
4
+ *
5
+ * LocalStorage stays the synchronous source of truth for every consumer (ai-settings getters read
6
+ * it directly). On platforms with a backend user-settings store (desktop/chromium — where
7
+ * localStorage does not roam across browser profiles), the persisted keys are hydrated from the
8
+ * backend at boot and written through on every change. The dev server omits the platform methods
9
+ * and settings remain localStorage-only.
10
+ *
11
+ * @license MIT
12
+ */
13
+
14
+ import { getPlatform, hasPlatform } from "../platform";
15
+
16
+ /** The localStorage keys mirrored into the platform's user-settings store. */
17
+ export const PERSISTED_SETTINGS_KEYS = ["jx.ai.openaiKey", "jx.ai.baseUrl", "jx.ai.model"] as const;
18
+
19
+ /** Read a localStorage value defensively, treating unavailable/throwing storage as empty. */
20
+ function readLocal(key: string): string {
21
+ try {
22
+ return globalThis.localStorage?.getItem(key) ?? "";
23
+ } catch {
24
+ return "";
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Pull the persisted settings from the backend store into localStorage. Call once after the
30
+ * platform is registered; a no-op on platforms without a settings store (dev server).
31
+ *
32
+ * Migration: keys the backend does not know yet but localStorage does (an existing user upgrading
33
+ * from localStorage-only builds) are pushed back to the backend once, fire-and-forget.
34
+ */
35
+ export async function hydrateSettings(): Promise<void> {
36
+ if (!hasPlatform()) {
37
+ return;
38
+ }
39
+ const platform = getPlatform();
40
+ if (!platform.getSettings) {
41
+ return;
42
+ }
43
+ let stored: Record<string, string>;
44
+ try {
45
+ stored = await platform.getSettings();
46
+ } catch {
47
+ return;
48
+ }
49
+ const merged: Record<string, string> = { ...stored };
50
+ let needsMigration = false;
51
+ for (const key of PERSISTED_SETTINGS_KEYS) {
52
+ const backendValue = stored[key];
53
+ if (backendValue) {
54
+ try {
55
+ globalThis.localStorage?.setItem(key, backendValue);
56
+ } catch {
57
+ /* Storage unavailable — consumers fall back to defaults. */
58
+ }
59
+ } else {
60
+ const localValue = readLocal(key);
61
+ if (localValue) {
62
+ merged[key] = localValue;
63
+ needsMigration = true;
64
+ }
65
+ }
66
+ }
67
+ if (needsMigration && platform.saveSettings) {
68
+ void platform.saveSettings(merged).catch(() => {});
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Write the current values of the persisted keys through to the backend store, fire-and-forget.
74
+ * Call after mutating any of {@link PERSISTED_SETTINGS_KEYS} in localStorage; a no-op on platforms
75
+ * without a settings store (dev server).
76
+ */
77
+ export function persistSettings(): void {
78
+ if (!hasPlatform()) {
79
+ return;
80
+ }
81
+ const platform = getPlatform();
82
+ if (!platform.saveSettings) {
83
+ return;
84
+ }
85
+ const settings: Record<string, string> = {};
86
+ for (const key of PERSISTED_SETTINGS_KEYS) {
87
+ const value = readLocal(key);
88
+ if (value) {
89
+ settings[key] = value;
90
+ }
91
+ }
92
+ void platform.saveSettings(settings).catch(() => {});
93
+ }
@@ -114,7 +114,7 @@ function renderColorSection(
114
114
  <div class="css-var-swatch" style="background:${val}">
115
115
  <input
116
116
  type="color"
117
- .value=${val && String(val).startsWith("#") ? val : "#007acc"}
117
+ .value=${val && String(val).startsWith("#") ? val : "#3b82f6"}
118
118
  @input=${(e: Event) => updateVar(name, (e.target as HTMLInputElement).value)}
119
119
  />
120
120
  </div>
@@ -131,7 +131,7 @@ function renderColorSection(
131
131
  </div>
132
132
  `,
133
133
  )}
134
- ${renderAddRow("--color-", "Primary Blue", "#007acc", addVar)}
134
+ ${renderAddRow("--color-", "Primary Blue", "#3b82f6", addVar)}
135
135
  </div>
136
136
  `;
137
137
  }
package/src/store.ts CHANGED
@@ -10,10 +10,9 @@
10
10
  // ─── Re-exports from state.js ────────────────────────────────────────────────
11
11
 
12
12
  import { activeTab } from "./workspace/workspace";
13
- import { isEventBinding } from "@jxsuite/schema/guards";
14
13
  import type { JxPath } from "./state";
15
14
  import type { JxMutableNode } from "@jxsuite/schema/types";
16
- import type { CanvasPanel } from "./panels/canvas-dnd.js";
15
+ import type { CanvasPanel } from "./types";
17
16
 
18
17
  export {
19
18
  createState,
@@ -52,20 +51,6 @@ export function initShellRefs() {
52
51
 
53
52
  // ─── Shared containers (mutated in place by owner modules) ───────────────────
54
53
 
55
- export const elToPath = new WeakMap<Element, JxPath>();
56
-
57
- /**
58
- * Canvas element → the runtime scope its children render with. Captured during live renders so the
59
- * canvas patcher can re-render a subtree in isolation with the same prototype-chained scope.
60
- */
61
- export const elToScope = new WeakMap<Element, Record<string, unknown>>();
62
-
63
- /**
64
- * Subtree root element → the effect scope created for its surgical render. Stopped when the subtree
65
- * is replaced or removed so render-time reactive effects don't accumulate.
66
- */
67
- export const elToRenderScope = new WeakMap<Element, { stop: () => void }>();
68
-
69
54
  export const canvasPanels: CanvasPanel[] = [];
70
55
 
71
56
  // ─── Shared constants ────────────────────────────────────────────────────────
@@ -139,52 +124,9 @@ export function cancelStyleDebounce(prop: string) {
139
124
  _styleDebounceTimers.delete(prop);
140
125
  }
141
126
 
142
- /**
143
- * Strip all on* event handler properties from a Jx document tree (deep clone).
144
- *
145
- * @param {JxMutableNode} node
146
- * @returns {JxMutableNode}
147
- */
148
- export function stripEventHandlers(node: JxMutableNode): JxMutableNode {
149
- if (!node || typeof node !== "object") {
150
- return node;
151
- }
152
- if (Array.isArray(node)) {
153
- // Arrays of nodes round-trip element-wise; the array itself is not a node.
154
- return node.map((n: JxMutableNode) => stripEventHandlers(n)) as unknown as JxMutableNode;
155
- }
156
- const out: Record<string, unknown> = {};
157
- for (const [k, v] of Object.entries(node)) {
158
- if (k.startsWith("on") && isEventBinding(v)) {
159
- continue;
160
- }
161
- if (k === "children") {
162
- out.children = Array.isArray(v)
163
- ? v.map((c: JxMutableNode) => stripEventHandlers(c))
164
- : stripEventHandlers(v as JxMutableNode);
165
- } else if (k === "cases" && typeof v === "object") {
166
- const cases: Record<string, unknown> = {};
167
- for (const [ck, cv] of Object.entries(v as Record<string, unknown>)) {
168
- cases[ck] = stripEventHandlers(cv as JxMutableNode);
169
- }
170
- out.cases = cases;
171
- } else if (k === "state" && typeof v === "object" && v !== null) {
172
- const state: Record<string, unknown> = {};
173
- for (const [sk, sv] of Object.entries(v as Record<string, unknown>)) {
174
- if (sv && typeof sv === "object" && (sv as Record<string, unknown>).timing === "server") {
175
- continue;
176
- }
177
- state[sk] = sv;
178
- }
179
- out.state = state;
180
- } else if (k === "style" || k === "attributes" || k === "$media") {
181
- out[k] = v;
182
- } else {
183
- out[k] = v;
184
- }
185
- }
186
- return out;
187
- }
127
+ // `stripEventHandlers` moved to ./utils/strip-events (dependency-light, shared with the iframe
128
+ // Subtree renderer); re-exported here so existing `from "../store"` imports keep working.
129
+ export { stripEventHandlers } from "./utils/strip-events";
188
130
 
189
131
  // ─── Render orchestration ────────────────────────────────────────────────────
190
132