@jxsuite/studio 0.32.0 → 0.34.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 (94) hide show
  1. package/dist/iframe-entry.js +6230 -0
  2. package/dist/iframe-entry.js.map +35 -0
  3. package/dist/studio.js +22791 -13709
  4. package/dist/studio.js.map +181 -74
  5. package/package.json +12 -7
  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 +2 -1
  41. package/src/page-params.ts +383 -0
  42. package/src/panels/ai-panel.ts +386 -328
  43. package/src/panels/block-action-bar.ts +296 -138
  44. package/src/panels/canvas-dnd-bridge.ts +397 -0
  45. package/src/panels/component-preview.ts +56 -0
  46. package/src/panels/dnd.ts +41 -17
  47. package/src/panels/drag-ghost.ts +62 -0
  48. package/src/panels/editors.ts +1 -1
  49. package/src/panels/overlays.ts +10 -125
  50. package/src/panels/properties-panel.ts +210 -0
  51. package/src/panels/right-panel.ts +0 -2
  52. package/src/panels/signals-panel.ts +136 -22
  53. package/src/panels/stylebook-doc.ts +373 -0
  54. package/src/panels/stylebook-panel.ts +46 -689
  55. package/src/panels/tab-bar.ts +159 -13
  56. package/src/panels/toolbar.ts +3 -2
  57. package/src/platforms/devserver.ts +18 -47
  58. package/src/services/ai-settings.ts +107 -0
  59. package/src/services/ai-system-prompt.ts +617 -0
  60. package/src/services/ai-tools.ts +854 -0
  61. package/src/services/context-manager.ts +200 -0
  62. package/src/services/document-assistant.ts +183 -0
  63. package/src/services/jx-validate.ts +65 -0
  64. package/src/services/monaco-setup.ts +12 -0
  65. package/src/services/render-critic.ts +75 -0
  66. package/src/services/token-lint.ts +140 -0
  67. package/src/services/tool-executor.ts +156 -0
  68. package/src/settings/css-vars-editor.ts +2 -2
  69. package/src/state.ts +29 -0
  70. package/src/store.ts +4 -62
  71. package/src/studio.ts +90 -40
  72. package/src/tabs/doc-op-apply.ts +89 -0
  73. package/src/tabs/patch-ops.ts +6 -2
  74. package/src/tabs/tab.ts +23 -4
  75. package/src/tabs/transact.ts +33 -69
  76. package/src/types.ts +16 -24
  77. package/src/ui/jx-theme.ts +63 -0
  78. package/src/ui/media-picker.ts +6 -4
  79. package/src/ui/spectrum.ts +5 -0
  80. package/src/utils/canvas-media.ts +0 -137
  81. package/src/utils/edit-display.ts +23 -3
  82. package/src/utils/geometry.ts +43 -0
  83. package/src/utils/link-target.ts +93 -0
  84. package/src/utils/strip-events.ts +54 -0
  85. package/src/view.ts +0 -23
  86. package/src/workspace/workspace.ts +14 -1
  87. package/src/canvas/canvas-subtree-render.ts +0 -113
  88. package/src/editor/component-inline-edit.ts +0 -349
  89. package/src/editor/content-inline-edit.ts +0 -207
  90. package/src/editor/insertion-helper.ts +0 -308
  91. package/src/panels/canvas-dnd.ts +0 -329
  92. package/src/panels/panel-events.ts +0 -306
  93. package/src/panels/preview-render.ts +0 -132
  94. package/src/panels/pseudo-preview.ts +0 -75
@@ -1,20 +1,23 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Tab bar — a per-tab contextual action bar rendered between the tab strip and the edit content.
4
- * Standardizes Back/breadcrumb navigation, media feature toggles, and mode actions (Code-mode
5
- * Export) into a single bar shared identically by every edit mode.
3
+ * Tab bar — a persistent per-tab settings bar rendered between the tab strip and the edit content.
4
+ * Standardizes Back/breadcrumb navigation, the view settings cluster (preview toggle, layout
5
+ * visibility, dynamic route-param pickers shown in the edit/design base modes), media feature
6
+ * toggles, and mode actions (Code-mode Export) into a single bar shared by every edit mode.
6
7
  *
7
8
  * Follows the same module shape as tab-strip.ts: mount(host, ctx) → effectScope/effect → render().
8
- * The bar collapses (renders `nothing`, so `#tab-bar:empty` hides the row) when there is nothing
9
- * contextual to show.
9
+ * The bar renders `nothing` (so `#tab-bar:empty` hides the row) only when no tab is active.
10
10
  */
11
11
 
12
12
  import { html, render as litRender, nothing } from "lit-html";
13
- import { updateUi } from "../store";
13
+ import { projectState, updateUi } from "../store";
14
14
  import { effect, effectScope } from "../reactivity";
15
15
  import { activeTab } from "../workspace/workspace";
16
- import { getEffectiveMedia } from "../site-context";
16
+ import { getEffectiveLayoutPath, getEffectiveMedia } from "../site-context";
17
+ import { dynamicRouteParams, loadParamValues, pagePathsDef } from "../page-params";
17
18
  import { mediaDisplayName } from "./shared";
19
+ import type { ParamValues } from "../page-params";
20
+ import type { Tab } from "../tabs/tab";
18
21
  import type { DocumentStackEntry, FunctionEditDef } from "../types";
19
22
  import type { EffectScope } from "@vue/reactivity";
20
23
  import type { TemplateResult } from "lit-html";
@@ -59,12 +62,16 @@ export function mount(host: HTMLElement, ctx: TabBarCtx) {
59
62
  if (tab) {
60
63
  // Read reactive properties to establish tracking — mirrors the toolbar's subset
61
64
  void tab.doc.document;
65
+ void tab.doc.document?.$layout;
62
66
  void tab.doc.mode;
63
67
  void tab.documentPath;
64
68
  void tab.session.documentStack.length;
65
69
  void tab.session.ui.canvasMode;
66
70
  void tab.session.ui.editingFunction;
67
71
  void tab.session.ui.featureToggles;
72
+ void tab.session.ui.preview;
73
+ void tab.session.ui.previewParams;
74
+ void tab.session.ui.showLayout;
68
75
  }
69
76
  render();
70
77
  });
@@ -143,6 +150,55 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
143
150
  `;
144
151
  }
145
152
 
153
+ // ── Right region: view settings cluster (edit/design base modes only) ──
154
+ // Gates on the BASE mode (not the effective mode): the cluster stays visible while the preview
155
+ // Toggle is on so it can be toggled back off.
156
+ const baseMode = S.ui.canvasMode;
157
+ const isPage = Boolean(
158
+ S.documentPath &&
159
+ projectState?.isSiteProject &&
160
+ (S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/")),
161
+ );
162
+ let settingsTpl: TemplateResult | typeof nothing = nothing;
163
+ if (!editing && (baseMode === "edit" || baseMode === "design")) {
164
+ const canPreview = tab.capabilities.modes.includes("preview");
165
+ const hasLayout = isPage && Boolean(getEffectiveLayoutPath(S.document?.$layout));
166
+ const pickersTpl = isPage ? paramPickersTpl(tab) : nothing;
167
+ const previewTpl = canPreview
168
+ ? html`
169
+ <sp-action-button
170
+ toggles
171
+ size="s"
172
+ title="Preview resolved values"
173
+ ?selected=${Boolean(S.ui.preview)}
174
+ @click=${() => updateUi("preview", !S.ui.preview)}
175
+ >
176
+ <sp-icon-preview slot="icon"></sp-icon-preview>
177
+ Preview
178
+ </sp-action-button>
179
+ `
180
+ : nothing;
181
+ const layoutTpl = hasLayout
182
+ ? html`
183
+ <sp-action-button
184
+ toggles
185
+ size="s"
186
+ title="Show layout elements"
187
+ ?selected=${S.ui.showLayout !== false}
188
+ @click=${() => updateUi("showLayout", S.ui.showLayout === false)}
189
+ >
190
+ Layout
191
+ </sp-action-button>
192
+ `
193
+ : nothing;
194
+ if (pickersTpl !== nothing || previewTpl !== nothing || layoutTpl !== nothing) {
195
+ settingsTpl = html`
196
+ ${pickersTpl}
197
+ <sp-action-group compact size="s">${layoutTpl} ${previewTpl}</sp-action-group>
198
+ `;
199
+ }
200
+ }
201
+
146
202
  // ── Right region: media feature toggles ──
147
203
  const { featureQueries } = ctx.parseMediaEntries(getEffectiveMedia(S.document?.$media));
148
204
  const togglesTpl =
@@ -183,16 +239,106 @@ function tabBarTemplate(ctx: TabBarCtx): TemplateResult | typeof nothing {
183
239
  `
184
240
  : nothing;
185
241
 
186
- // Collapse the bar entirely when there is nothing contextual to show.
187
- if (navTpl === nothing && togglesTpl === nothing && exportTpl === nothing) {
188
- return nothing;
189
- }
190
-
191
242
  return html`
192
243
  <div class="tab-bar">
193
244
  ${navTpl}
194
245
  <div class="tb-spacer"></div>
195
- ${togglesTpl} ${exportTpl}
246
+ ${settingsTpl} ${togglesTpl} ${exportTpl}
196
247
  </div>
197
248
  `;
198
249
  }
250
+
251
+ // ── Dynamic route-param pickers ──────────────────────────────────────────────
252
+ // Candidate values load asynchronously (ContentCollection resolution / data-file read); the module
253
+ // Caches the last result per (documentPath, $paths) and re-renders when it lands — the same lazy
254
+ // Fill pattern as head-panel's loadLayoutEntries. When values arrive, any param without a chosen
255
+ // Value auto-selects the first candidate (matching the compiler, whose first expanded route is the
256
+ // First path entry).
257
+
258
+ let _paramValues: ParamValues | null = null;
259
+
260
+ let _paramValuesKey: string | null = null;
261
+
262
+ /**
263
+ * @param {Tab} tab
264
+ * @returns {ParamValues | null} — null while loading (or when the doc declares no params)
265
+ */
266
+ function paramValuesFor(tab: Tab): ParamValues | null {
267
+ const pathsDef = pagePathsDef({
268
+ document: tab.doc.document,
269
+ frontmatter: tab.doc.content.frontmatter,
270
+ });
271
+ if (!pathsDef && dynamicRouteParams(tab.documentPath).length === 0) {
272
+ return null;
273
+ }
274
+ const key = `${tab.documentPath ?? ""}::${JSON.stringify(pathsDef)}`;
275
+ if (_paramValuesKey === key) {
276
+ return _paramValues;
277
+ }
278
+ _paramValuesKey = key;
279
+ _paramValues = null;
280
+ void loadParamValues(tab.documentPath, pathsDef).then((values) => {
281
+ if (_paramValuesKey !== key || activeTab.value !== tab) {
282
+ return;
283
+ }
284
+ _paramValues = values;
285
+ autoSelectParams(tab, values);
286
+ render();
287
+ });
288
+ return _paramValues;
289
+ }
290
+
291
+ /**
292
+ * @param {Tab} tab
293
+ * @param {ParamValues} values
294
+ */
295
+ function autoSelectParams(tab: Tab, values: ParamValues) {
296
+ const current = tab.session.ui.previewParams ?? {};
297
+ const additions: Record<string, string> = {};
298
+ for (const [name, list] of Object.entries(values)) {
299
+ if (!current[name] && list.length > 0) {
300
+ additions[name] = list[0]!;
301
+ }
302
+ }
303
+ if (Object.keys(additions).length > 0) {
304
+ updateUi("previewParams", { ...current, ...additions });
305
+ }
306
+ }
307
+
308
+ /**
309
+ * @param {Tab} tab
310
+ * @returns {TemplateResult | typeof nothing}
311
+ */
312
+ function paramPickersTpl(tab: Tab): TemplateResult | typeof nothing {
313
+ const values = paramValuesFor(tab);
314
+ const names = new Set(dynamicRouteParams(tab.documentPath));
315
+ for (const name of Object.keys(values ?? {})) {
316
+ names.add(name);
317
+ }
318
+ if (names.size === 0) {
319
+ return nothing;
320
+ }
321
+ const { previewParams } = tab.session.ui;
322
+ return html`
323
+ ${[...names].map(
324
+ (name) => html`
325
+ <sp-picker
326
+ size="s"
327
+ quiet
328
+ class="tab-bar-param"
329
+ label=${name}
330
+ title=${`Preview value for [${name}]`}
331
+ value=${previewParams?.[name] ?? ""}
332
+ @change=${(e: Event) => {
333
+ const { value } = e.target as HTMLInputElement;
334
+ updateUi("previewParams", { ...tab.session.ui.previewParams, [name]: value });
335
+ }}
336
+ >
337
+ ${(values?.[name] ?? []).map(
338
+ (v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`,
339
+ )}
340
+ </sp-picker>
341
+ `,
342
+ )}
343
+ `;
344
+ }
@@ -305,7 +305,6 @@ function minimalToolbarTemplate(ctx: ToolbarCtx) {
305
305
  const modes = [
306
306
  { iconTag: "sp-icon-edit", key: "edit", label: "Edit" },
307
307
  { iconTag: "sp-icon-artboard", key: "design", label: "Design" },
308
- { iconTag: "sp-icon-preview", key: "preview", label: "Preview" },
309
308
  { iconTag: "sp-icon-code", key: "source", label: "Code" },
310
309
  { iconTag: "sp-icon-brush", key: "stylebook", label: "Stylebook" },
311
310
  ];
@@ -333,7 +332,9 @@ function toolbarTemplate() {
333
332
  selection: tab.session.selection,
334
333
  ui: tab.session.ui,
335
334
  };
336
- const canvasMode = ctx.getCanvasMode();
335
+ // Base mode, not the effective mode: the switcher keeps Edit/Design highlighted while the
336
+ // Tab-bar preview toggle is on (preview is no longer a switchable mode).
337
+ const { canvasMode } = tab.session.ui;
337
338
 
338
339
  const modeSwitcherTpl = html`
339
340
  <sp-action-group selects="single" size="s" compact>
@@ -588,6 +588,21 @@ export function createDevServerPlatform() {
588
588
  return schema;
589
589
  },
590
590
 
591
+ // ─── Class resolution (dev-proxy) ─────────────────────────────────────
592
+
593
+ /** @param {Record<string, unknown>} body */
594
+ async resolveClass(body: Record<string, unknown>) {
595
+ const res = await fetch("/__jx_resolve__", {
596
+ body: JSON.stringify(body),
597
+ headers: { "content-type": "application/json" },
598
+ method: "POST",
599
+ });
600
+ if (!res.ok) {
601
+ throw new Error(`Class resolution failed: ${res.status}`);
602
+ }
603
+ return await readJson<unknown>(res);
604
+ },
605
+
591
606
  // ─── Git operations ──────────────────────────────────────────────────
592
607
 
593
608
  async gitStatus() {
@@ -793,54 +808,10 @@ export function createDevServerPlatform() {
793
808
  }
794
809
  },
795
810
 
796
- // ─── AI Assistant ───────────────────────────────────
797
-
798
- async aiAuthStatus() {
799
- const res = await fetch("/__studio/ai/auth-status");
800
- return await res.json();
801
- },
802
-
803
- /** @param {{ message: string; systemPrompt?: string }} opts */
804
- async aiCreateSession(opts: { message: string; systemPrompt?: string }) {
805
- const res = await fetch("/__studio/ai/session", {
806
- body: JSON.stringify(opts),
807
- headers: { "Content-Type": "application/json" },
808
- method: "POST",
809
- });
810
- if (!res.ok) {
811
- const body = await readJson<ErrorBody>(res);
812
- throw new Error(body.error);
813
- }
814
- return await res.json();
815
- },
816
-
817
- /** @param {string} id @param {string} message */
818
- async aiSendMessage(id: string, message: string) {
819
- const res = await fetch(`/__studio/ai/session/${id}/message`, {
820
- body: JSON.stringify({ message }),
821
- headers: { "Content-Type": "application/json" },
822
- method: "POST",
823
- });
824
- if (!res.ok) {
825
- const body = await readJson<ErrorBody>(res);
826
- throw new Error(body.error);
827
- }
828
- return await res.json();
829
- },
830
-
831
- /** @param {string} id */
832
- aiStreamUrl(id: string) {
833
- return `/__studio/ai/session/${id}/stream`;
834
- },
835
-
836
- /** @param {string} id */
837
- async aiStopSession(id: string) {
838
- await fetch(`/__studio/ai/session/${id}/stop`, { method: "POST" });
839
- },
811
+ // ─── AI Assistant (Stack B: OpenAI-compatible SSE proxy) ───────────────────
840
812
 
841
- /** @param {string} id */
842
- async aiDeleteSession(id: string) {
843
- await fetch(`/__studio/ai/session/${id}`, { method: "DELETE" });
813
+ aiChatUrl() {
814
+ return "/__studio/ai/chat";
844
815
  },
845
816
  };
846
817
  }
@@ -0,0 +1,107 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Ai-settings.js — local persistence for the AI assistant's provider settings.
4
+ *
5
+ * The Stack B proxy reads the OpenAI key from the `X-Api-Key` header (falling back to the server's
6
+ * `OPENAI_API_KEY` env var). Studio stores a user-supplied key in localStorage so the browser/dev
7
+ * build works without an env var. The key never leaves the machine except to the same-origin proxy.
8
+ *
9
+ * @license MIT
10
+ */
11
+
12
+ const KEY_STORAGE = "jx.ai.openaiKey";
13
+ const BASE_URL_STORAGE = "jx.ai.baseUrl";
14
+ const MODEL_STORAGE = "jx.ai.model";
15
+ const DEFAULT_MODEL = "gpt-4o";
16
+
17
+ /** @returns {string} The stored OpenAI key, or "" if none/unavailable. */
18
+ export function getOpenAiKey() {
19
+ try {
20
+ return globalThis.localStorage?.getItem(KEY_STORAGE) ?? "";
21
+ } catch {
22
+ return "";
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Persist (or clear) the OpenAI key.
28
+ *
29
+ * @param {string} key - The key to store; an empty/blank value clears it.
30
+ */
31
+ export function setOpenAiKey(key: string) {
32
+ try {
33
+ const trimmed = (key || "").trim();
34
+ if (trimmed) {
35
+ globalThis.localStorage?.setItem(KEY_STORAGE, trimmed);
36
+ } else {
37
+ globalThis.localStorage?.removeItem(KEY_STORAGE);
38
+ }
39
+ } catch {
40
+ /* LocalStorage unavailable — settings are not persisted. */
41
+ }
42
+ }
43
+
44
+ /** @returns {boolean} Whether a non-empty key is stored. */
45
+ export function hasOpenAiKey() {
46
+ return getOpenAiKey().length > 0;
47
+ }
48
+
49
+ /**
50
+ * @returns {string} The OpenAI-compatible base URL override (e.g. a local LLM, OpenRouter, Azure),
51
+ * or "" to use the proxy's default (`https://api.openai.com/v1` / server `OPENAI_BASE_URL`).
52
+ */
53
+ export function getBaseUrl() {
54
+ try {
55
+ return globalThis.localStorage?.getItem(BASE_URL_STORAGE) ?? "";
56
+ } catch {
57
+ return "";
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Persist (or clear) the base URL override.
63
+ *
64
+ * @param {string} url - The base URL; an empty/blank value clears the override.
65
+ */
66
+ export function setBaseUrl(url: string) {
67
+ try {
68
+ const trimmed = (url || "").trim().replace(/\/+$/, "");
69
+ if (trimmed) {
70
+ globalThis.localStorage?.setItem(BASE_URL_STORAGE, trimmed);
71
+ } else {
72
+ globalThis.localStorage?.removeItem(BASE_URL_STORAGE);
73
+ }
74
+ } catch {
75
+ /* LocalStorage unavailable — settings are not persisted. */
76
+ }
77
+ }
78
+
79
+ // ─── Model selection ────────────────────────────────────────────────────────
80
+
81
+ /** @returns {string} The stored model ID, or the default ("gpt-4o"). */
82
+ export function getModel() {
83
+ try {
84
+ return globalThis.localStorage?.getItem(MODEL_STORAGE) || DEFAULT_MODEL;
85
+ } catch {
86
+ return DEFAULT_MODEL;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Persist the selected model ID.
92
+ *
93
+ * @param {string} modelId - The model ID to store; an empty/blank value clears it, reverting to
94
+ * default.
95
+ */
96
+ export function setModel(modelId: string) {
97
+ try {
98
+ const trimmed = (modelId || "").trim();
99
+ if (trimmed) {
100
+ globalThis.localStorage?.setItem(MODEL_STORAGE, trimmed);
101
+ } else {
102
+ globalThis.localStorage?.removeItem(MODEL_STORAGE);
103
+ }
104
+ } catch {
105
+ /* LocalStorage unavailable — settings are not persisted. */
106
+ }
107
+ }