@jxsuite/studio 0.33.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.
- package/dist/iframe-entry.js +6230 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +10754 -11060
- package/dist/studio.js.map +83 -72
- package/package.json +7 -7
- package/src/browse/browse.ts +11 -4
- package/src/canvas/canvas-helpers.ts +2 -56
- package/src/canvas/canvas-live-render.ts +102 -435
- package/src/canvas/canvas-origin.ts +66 -0
- package/src/canvas/canvas-patcher.ts +63 -403
- package/src/canvas/canvas-render.ts +70 -212
- package/src/canvas/canvas-utils.ts +37 -65
- package/src/canvas/iframe-channel.ts +154 -0
- package/src/canvas/iframe-drop.ts +484 -0
- package/src/canvas/iframe-entry.ts +600 -0
- package/src/canvas/iframe-host.ts +1373 -0
- package/src/canvas/iframe-inline-edit.ts +367 -0
- package/src/canvas/iframe-insert.ts +164 -0
- package/src/canvas/iframe-interaction.ts +176 -0
- package/src/canvas/iframe-keys.ts +85 -0
- package/src/canvas/iframe-overlay.ts +218 -0
- package/src/canvas/iframe-patch.ts +363 -0
- package/src/canvas/iframe-protocol.ts +361 -0
- package/src/canvas/iframe-render.ts +458 -0
- package/src/canvas/iframe-slash.ts +114 -0
- package/src/canvas/iframe-subtree.ts +113 -0
- package/src/canvas/path-mapping.ts +86 -0
- package/src/canvas/serialize-scope.ts +65 -0
- package/src/editor/canvas-context-menu.ts +40 -0
- package/src/editor/canvas-slash-bridge.ts +21 -0
- package/src/editor/context-menu.ts +2 -1
- package/src/editor/inline-edit-apply.ts +183 -0
- package/src/editor/inline-edit.ts +99 -21
- package/src/editor/inline-link.ts +89 -0
- package/src/editor/insert-zone-action.ts +35 -0
- package/src/editor/merge-tags.ts +26 -2
- package/src/editor/repeater-scope.ts +144 -0
- package/src/editor/shortcuts.ts +14 -28
- package/src/editor/slash-menu.ts +73 -42
- package/src/files/files.ts +2 -1
- package/src/page-params.ts +383 -0
- package/src/panels/ai-panel.ts +5 -7
- package/src/panels/block-action-bar.ts +296 -138
- package/src/panels/canvas-dnd-bridge.ts +397 -0
- package/src/panels/component-preview.ts +56 -0
- package/src/panels/dnd.ts +41 -17
- package/src/panels/drag-ghost.ts +62 -0
- package/src/panels/editors.ts +1 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +0 -2
- package/src/panels/signals-panel.ts +136 -22
- package/src/panels/stylebook-doc.ts +373 -0
- package/src/panels/stylebook-panel.ts +46 -689
- package/src/panels/tab-bar.ts +159 -13
- package/src/panels/toolbar.ts +3 -2
- package/src/platforms/devserver.ts +15 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +9 -9
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/store.ts +4 -62
- package/src/studio.ts +90 -40
- package/src/tabs/doc-op-apply.ts +89 -0
- package/src/tabs/patch-ops.ts +6 -2
- package/src/tabs/tab.ts +23 -4
- package/src/tabs/transact.ts +2 -74
- package/src/types.ts +14 -18
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +5 -0
- package/src/utils/canvas-media.ts +0 -137
- package/src/utils/edit-display.ts +23 -3
- package/src/utils/geometry.ts +43 -0
- package/src/utils/link-target.ts +93 -0
- package/src/utils/strip-events.ts +54 -0
- package/src/view.ts +0 -23
- package/src/workspace/workspace.ts +14 -1
- package/src/canvas/canvas-subtree-render.ts +0 -113
- package/src/editor/component-inline-edit.ts +0 -349
- package/src/editor/content-inline-edit.ts +0 -207
- package/src/editor/insertion-helper.ts +0 -308
- package/src/panels/canvas-dnd.ts +0 -329
- package/src/panels/panel-events.ts +0 -306
- package/src/panels/preview-render.ts +0 -132
- package/src/panels/pseudo-preview.ts +0 -75
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Dynamic route-param helpers for the tab-bar's $params picker.
|
|
4
|
+
*
|
|
5
|
+
* A page like `pages/products/[sku].json` declares its candidate values in a `$paths` block (spec
|
|
6
|
+
* §4.3). At build time the compiler expands those into concrete routes (`resolvePathEntries` in
|
|
7
|
+
* `@jxsuite/compiler`); in the studio the same shapes are resolved live so the author can pick one
|
|
8
|
+
* value to render. The chosen values are substituted into the canvas render document
|
|
9
|
+
* (`substitutePreviewParams`), which mirrors what the compiler's context injection produces for the
|
|
10
|
+
* matching built page.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getPlatform } from "./platform";
|
|
14
|
+
import type { JxMutableNode, JxPathsDef } from "@jxsuite/schema/types";
|
|
15
|
+
|
|
16
|
+
/** Param name → candidate values, in `$paths` declaration order. */
|
|
17
|
+
export type ParamValues = Record<string, string[]>;
|
|
18
|
+
|
|
19
|
+
/** Bracket route segments: `[param]` (named) and `[...param]` (catch-all). */
|
|
20
|
+
const PARAM_SEGMENT = /\[\.\.\.(\w+)\]|\[(\w+)\]/g;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Param names declared by a document path's bracket segments, e.g. `pages/products/[sku].json` →
|
|
24
|
+
* `["sku"]`. Mirrors the compiler's `fileToRoute` parsing.
|
|
25
|
+
*
|
|
26
|
+
* @param {string | null | undefined} documentPath
|
|
27
|
+
* @returns {string[]}
|
|
28
|
+
*/
|
|
29
|
+
export function dynamicRouteParams(documentPath: string | null | undefined) {
|
|
30
|
+
if (!documentPath) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const params: string[] = [];
|
|
34
|
+
for (const match of documentPath.matchAll(PARAM_SEGMENT)) {
|
|
35
|
+
params.push((match[1] ?? match[2])!);
|
|
36
|
+
}
|
|
37
|
+
return params;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* URL pattern for a page document path, mirroring the compiler's `fileToRoute`:
|
|
42
|
+
* `pages/products/[sku].json` → `/products/:sku`, `pages/index.json` → `/`.
|
|
43
|
+
*
|
|
44
|
+
* @param {string | null | undefined} documentPath
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function documentUrlPattern(documentPath: string | null | undefined) {
|
|
48
|
+
if (!documentPath) {
|
|
49
|
+
return "/";
|
|
50
|
+
}
|
|
51
|
+
let urlPath = documentPath
|
|
52
|
+
.replace(/^\.\//, "")
|
|
53
|
+
.replace(/^pages\//, "")
|
|
54
|
+
.replace(/\.[^/.]+$/, "");
|
|
55
|
+
if (urlPath.endsWith("/index")) {
|
|
56
|
+
urlPath = urlPath.slice(0, -6) || "/";
|
|
57
|
+
} else if (urlPath === "index") {
|
|
58
|
+
urlPath = "/";
|
|
59
|
+
}
|
|
60
|
+
if (!urlPath.startsWith("/")) {
|
|
61
|
+
urlPath = `/${urlPath}`;
|
|
62
|
+
}
|
|
63
|
+
return urlPath.replaceAll(PARAM_SEGMENT, (_match, spread: string, named: string) =>
|
|
64
|
+
spread ? "*" : `:${named}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The `$paths` declaration of an open document. JSON pages carry it on the document root;
|
|
70
|
+
* format-class pages (markdown) keep it in the frontmatter.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ document?: unknown; frontmatter?: Record<string, unknown> }} doc
|
|
73
|
+
* @returns {JxPathsDef | null}
|
|
74
|
+
*/
|
|
75
|
+
export function pagePathsDef(doc: {
|
|
76
|
+
document?: JxMutableNode | null;
|
|
77
|
+
frontmatter?: Record<string, unknown> | null;
|
|
78
|
+
}) {
|
|
79
|
+
const fromDoc = (doc.document as { $paths?: JxPathsDef } | null | undefined)?.$paths;
|
|
80
|
+
const fromFrontmatter = doc.frontmatter?.$paths as JxPathsDef | undefined;
|
|
81
|
+
return fromDoc ?? fromFrontmatter ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const valueCache = new Map<string, Promise<ParamValues>>();
|
|
85
|
+
|
|
86
|
+
/** Drop all cached enumerations (e.g. after content files change). */
|
|
87
|
+
export function invalidateParamValues() {
|
|
88
|
+
valueCache.clear();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Enumerate candidate values for each param declared by a `$paths` block. Results are cached per
|
|
93
|
+
* (documentPath, $paths) pair; failures are not cached so a transient backend error retries on the
|
|
94
|
+
* next call.
|
|
95
|
+
*
|
|
96
|
+
* @param {string | null | undefined} documentPath - Cache key component
|
|
97
|
+
* @param {JxPathsDef | null} pathsDef
|
|
98
|
+
* @returns {Promise<ParamValues>}
|
|
99
|
+
*/
|
|
100
|
+
export function loadParamValues(
|
|
101
|
+
documentPath: string | null | undefined,
|
|
102
|
+
pathsDef: JxPathsDef | null,
|
|
103
|
+
) {
|
|
104
|
+
if (!pathsDef) {
|
|
105
|
+
return Promise.resolve({} as ParamValues);
|
|
106
|
+
}
|
|
107
|
+
const key = `${documentPath ?? ""}::${JSON.stringify(pathsDef)}`;
|
|
108
|
+
let pending = valueCache.get(key);
|
|
109
|
+
if (!pending) {
|
|
110
|
+
pending = resolveParamValues(pathsDef).catch((error: unknown) => {
|
|
111
|
+
valueCache.delete(key);
|
|
112
|
+
console.warn("page-params: failed to enumerate $paths values:", error);
|
|
113
|
+
return {} as ParamValues;
|
|
114
|
+
});
|
|
115
|
+
valueCache.set(key, pending);
|
|
116
|
+
}
|
|
117
|
+
return pending;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve one `$paths` shape to values, mirroring the compiler's `resolvePathEntries`.
|
|
122
|
+
*
|
|
123
|
+
* @param {JxPathsDef} pathsDef
|
|
124
|
+
* @returns {Promise<ParamValues>}
|
|
125
|
+
*/
|
|
126
|
+
async function resolveParamValues(pathsDef: JxPathsDef): Promise<ParamValues> {
|
|
127
|
+
// Legacy: array of param objects — group values per param key
|
|
128
|
+
if (Array.isArray(pathsDef)) {
|
|
129
|
+
const out: ParamValues = {};
|
|
130
|
+
for (const entry of pathsDef) {
|
|
131
|
+
for (const [param, value] of Object.entries(entry)) {
|
|
132
|
+
pushValue(out, param, value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Content type-based: { contentType: "blog", param: "slug", field: "id" } — resolved through the
|
|
139
|
+
// Backend's ContentCollection pipeline (the same one the canvas's ContentEntry uses), so every
|
|
140
|
+
// Offered value is guaranteed to resolve in preview.
|
|
141
|
+
if ("contentType" in pathsDef && pathsDef.contentType) {
|
|
142
|
+
const param = pathsDef.param ?? "slug";
|
|
143
|
+
const field = pathsDef.field ?? "id";
|
|
144
|
+
const entries = (await resolveContentCollection(pathsDef.contentType)) as {
|
|
145
|
+
id?: unknown;
|
|
146
|
+
data?: Record<string, unknown>;
|
|
147
|
+
}[];
|
|
148
|
+
const out: ParamValues = { [param]: [] };
|
|
149
|
+
if (!Array.isArray(entries)) {
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
pushValue(out, param, field === "id" ? entry.id : (entry.data?.[field] ?? entry.id));
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Explicit values: { values: ["en", "fr"], param: "lang" }
|
|
159
|
+
if ("values" in pathsDef && Array.isArray(pathsDef.values)) {
|
|
160
|
+
const param = pathsDef.param ?? "value";
|
|
161
|
+
const out: ParamValues = { [param]: [] };
|
|
162
|
+
for (const value of pathsDef.values) {
|
|
163
|
+
pushValue(out, param, value);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Data file ref: { "$ref": "./data/products.json", param: "id", field: "sku" }
|
|
169
|
+
if ("$ref" in pathsDef && pathsDef.$ref) {
|
|
170
|
+
const param = pathsDef.param ?? "id";
|
|
171
|
+
const field = pathsDef.field ?? "id";
|
|
172
|
+
const content = await getPlatform().readFile(pathsDef.$ref.replace(/^\.\//, ""));
|
|
173
|
+
const data = JSON.parse(content) as unknown;
|
|
174
|
+
const out: ParamValues = { [param]: [] };
|
|
175
|
+
if (!Array.isArray(data)) {
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
for (const item of data as Record<string, unknown>[]) {
|
|
179
|
+
pushValue(out, param, item?.[field] ?? item?.id ?? item);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve a ContentCollection for a content type via the PAL, falling back to a plain dev-proxy
|
|
189
|
+
* fetch on platforms that predate `resolveClass`.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} contentType
|
|
192
|
+
* @returns {Promise<unknown>}
|
|
193
|
+
*/
|
|
194
|
+
function resolveContentCollection(contentType: string) {
|
|
195
|
+
const body = {
|
|
196
|
+
$prototype: "ContentCollection",
|
|
197
|
+
$src: "@jxsuite/parser/ContentCollection.class.json",
|
|
198
|
+
contentType,
|
|
199
|
+
};
|
|
200
|
+
const platform = getPlatform();
|
|
201
|
+
if (platform.resolveClass) {
|
|
202
|
+
return platform.resolveClass(body);
|
|
203
|
+
}
|
|
204
|
+
return fetch("/__jx_resolve__", {
|
|
205
|
+
body: JSON.stringify(body),
|
|
206
|
+
headers: { "content-type": "application/json" },
|
|
207
|
+
method: "POST",
|
|
208
|
+
}).then((res) => {
|
|
209
|
+
if (!res.ok) {
|
|
210
|
+
throw new Error(`Class resolution failed: ${res.status}`);
|
|
211
|
+
}
|
|
212
|
+
return res.json() as Promise<unknown>;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {ParamValues} out
|
|
218
|
+
* @param {string} param
|
|
219
|
+
* @param {unknown} value
|
|
220
|
+
*/
|
|
221
|
+
function pushValue(out: ParamValues, param: string, value: unknown) {
|
|
222
|
+
if (value === null || value === undefined || value === "") {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const str = String(value);
|
|
226
|
+
const list = (out[param] ??= []);
|
|
227
|
+
if (!list.includes(str)) {
|
|
228
|
+
list.push(str);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Substitute chosen param values into a canvas render document:
|
|
234
|
+
*
|
|
235
|
+
* - Every `{ "$ref": "#/$params/<name>" }` value with a chosen `<name>` becomes the literal string
|
|
236
|
+
* (the exact dereference `ContentEntry.resolve()` performs against `route._pathParams`), and
|
|
237
|
+
* - `state.$page` is injected mirroring the compiler's `injectContext`, so `${state.$page.params.x}`
|
|
238
|
+
* template expressions render.
|
|
239
|
+
*
|
|
240
|
+
* Pure: returns a rebuilt tree. The design/edit render doc shares node references with the tab's
|
|
241
|
+
* source document (stripEventHandlers/prepareForEditMode shallow-rebuild), so in-place mutation
|
|
242
|
+
* here would corrupt the document being edited and saved.
|
|
243
|
+
*
|
|
244
|
+
* @param {JxMutableNode} renderDoc
|
|
245
|
+
* @param {Record<string, string>} params
|
|
246
|
+
* @param {string | null | undefined} documentPath
|
|
247
|
+
* @returns {JxMutableNode}
|
|
248
|
+
*/
|
|
249
|
+
export function substitutePreviewParams(
|
|
250
|
+
renderDoc: JxMutableNode,
|
|
251
|
+
params: Record<string, string>,
|
|
252
|
+
documentPath: string | null | undefined,
|
|
253
|
+
): JxMutableNode {
|
|
254
|
+
if (Object.keys(params).length === 0) {
|
|
255
|
+
return renderDoc;
|
|
256
|
+
}
|
|
257
|
+
const doc = substituteParamRefs(renderDoc, params) as JxMutableNode & {
|
|
258
|
+
state?: Record<string, unknown>;
|
|
259
|
+
title?: unknown;
|
|
260
|
+
};
|
|
261
|
+
doc.state ??= {};
|
|
262
|
+
doc.state.$page = {
|
|
263
|
+
params: { ...params },
|
|
264
|
+
title: typeof doc.title === "string" ? doc.title : "",
|
|
265
|
+
url: documentUrlPattern(documentPath),
|
|
266
|
+
};
|
|
267
|
+
return doc;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* State keys whose entries are class prototypes ($prototype + $src) referencing a route param.
|
|
272
|
+
* These are the entries `substitutePreviewParams` rewrites, and the ones `resolveParamBoundState`
|
|
273
|
+
* bakes.
|
|
274
|
+
*
|
|
275
|
+
* @param {Record<string, unknown> | null | undefined} state - The ORIGINAL document state (with
|
|
276
|
+
* `$ref`s intact — substitution removes the marker)
|
|
277
|
+
* @returns {string[]}
|
|
278
|
+
*/
|
|
279
|
+
export function paramBoundStateKeys(state: Record<string, unknown> | null | undefined) {
|
|
280
|
+
if (!state) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
return Object.keys(state).filter((key) => {
|
|
284
|
+
const entry = state[key] as Record<string, unknown> | null;
|
|
285
|
+
return (
|
|
286
|
+
entry &&
|
|
287
|
+
typeof entry === "object" &&
|
|
288
|
+
typeof entry.$prototype === "string" &&
|
|
289
|
+
typeof entry.$src === "string" &&
|
|
290
|
+
JSON.stringify(entry).includes('"#/$params/')
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolve substituted class-prototype state entries through the backend and bake the values into
|
|
297
|
+
* `renderDoc.state` — compiler parity: the built site resolves ContentEntry/ContentCollection
|
|
298
|
+
* BEFORE templates run. Without baking, the iframe runtime resolves these entries asynchronously
|
|
299
|
+
* (null on the first render pass), so preview templates like `${state.product.data.title}`
|
|
300
|
+
* dereference null and abort the whole render.
|
|
301
|
+
*
|
|
302
|
+
* Failures leave the entry in place (the runtime's own async resolution remains the fallback).
|
|
303
|
+
*
|
|
304
|
+
* @param {JxMutableNode} renderDoc - The substituted render doc (mutated in place — it is the fresh
|
|
305
|
+
* rebuild from substitutePreviewParams, never the source doc)
|
|
306
|
+
* @param {string[]} keys - From paramBoundStateKeys(originalDoc.state)
|
|
307
|
+
* @param {string | undefined} docBase - Document base URL for relative-$src rebasing
|
|
308
|
+
*/
|
|
309
|
+
export async function resolveParamBoundState(
|
|
310
|
+
renderDoc: JxMutableNode,
|
|
311
|
+
keys: string[],
|
|
312
|
+
docBase?: string,
|
|
313
|
+
) {
|
|
314
|
+
if (keys.length === 0) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const platform = getPlatform();
|
|
318
|
+
if (!platform.resolveClass) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const { state } = renderDoc as { state?: Record<string, unknown> };
|
|
322
|
+
if (!state) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await Promise.all(
|
|
326
|
+
keys.map(async (key) => {
|
|
327
|
+
const entry = state[key];
|
|
328
|
+
if (!entry || typeof entry !== "object") {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
state[key] = await platform.resolveClass!({
|
|
333
|
+
...(entry as Record<string, unknown>),
|
|
334
|
+
...(docBase ? { $base: docBase } : {}),
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.warn(`page-params: failed to resolve state.${key} for preview:`, error);
|
|
338
|
+
}
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {unknown} node
|
|
345
|
+
* @param {Record<string, string>} params
|
|
346
|
+
* @returns {unknown} — a rebuilt copy with matched param refs replaced by literals
|
|
347
|
+
*/
|
|
348
|
+
function substituteParamRefs(node: unknown, params: Record<string, string>): unknown {
|
|
349
|
+
const replaced = paramRefValue(node, params);
|
|
350
|
+
if (replaced !== null) {
|
|
351
|
+
return replaced;
|
|
352
|
+
}
|
|
353
|
+
if (!node || typeof node !== "object") {
|
|
354
|
+
return node;
|
|
355
|
+
}
|
|
356
|
+
if (Array.isArray(node)) {
|
|
357
|
+
return node.map((item) => substituteParamRefs(item, params));
|
|
358
|
+
}
|
|
359
|
+
const out: Record<string, unknown> = {};
|
|
360
|
+
for (const [key, value] of Object.entries(node)) {
|
|
361
|
+
out[key] = substituteParamRefs(value, params);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The chosen literal for a `{ "$ref": "#/$params/<name>" }` value, or null when the value is not a
|
|
368
|
+
* param ref (or no value was chosen for it).
|
|
369
|
+
*
|
|
370
|
+
* @param {unknown} value
|
|
371
|
+
* @param {Record<string, string>} params
|
|
372
|
+
* @returns {string | null}
|
|
373
|
+
*/
|
|
374
|
+
function paramRefValue(value: unknown, params: Record<string, string>) {
|
|
375
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
const ref = (value as { $ref?: unknown }).$ref;
|
|
379
|
+
if (typeof ref !== "string" || !ref.startsWith("#/$params/")) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return params[ref.slice("#/$params/".length)] ?? null;
|
|
383
|
+
}
|
package/src/panels/ai-panel.ts
CHANGED
|
@@ -218,13 +218,13 @@ function renderKeyGate() {
|
|
|
218
218
|
<div class="ai-tab-body">
|
|
219
219
|
<div class="ai-status-center" style="gap:10px;max-width:320px;text-align:left">
|
|
220
220
|
<div style="font-weight:600;align-self:center">AI provider key</div>
|
|
221
|
-
<div style="font-size:11px;color:var(--
|
|
221
|
+
<div style="font-size:11px;color:var(--fg-dim)">
|
|
222
222
|
Any OpenAI-compatible key works. Stored locally in this browser; sent only to the Studio
|
|
223
223
|
proxy (never to a third party except your chosen endpoint).
|
|
224
224
|
</div>
|
|
225
225
|
<input
|
|
226
226
|
type="password"
|
|
227
|
-
style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--
|
|
227
|
+
style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-input);color:var(--fg);font-size:12px"
|
|
228
228
|
placeholder="sk-… or any compatible key"
|
|
229
229
|
.value=${keyDraft}
|
|
230
230
|
@input=${(e: Event) => {
|
|
@@ -253,7 +253,7 @@ function renderKeyGate() {
|
|
|
253
253
|
: html`
|
|
254
254
|
<input
|
|
255
255
|
type="text"
|
|
256
|
-
style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--
|
|
256
|
+
style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-input);color:var(--fg);font-size:12px"
|
|
257
257
|
placeholder="Model ID (e.g. gpt-4o, claude-sonnet-4-20250514, etc.)"
|
|
258
258
|
.value=${modelDraft}
|
|
259
259
|
@input=${(e: Event) => {
|
|
@@ -270,14 +270,12 @@ function renderKeyGate() {
|
|
|
270
270
|
: "Fetch models"}
|
|
271
271
|
</sp-button>
|
|
272
272
|
${modelsError
|
|
273
|
-
? html`<span style="font-size:10px;color:var(--
|
|
274
|
-
>${modelsError}</span
|
|
275
|
-
>`
|
|
273
|
+
? html`<span style="font-size:10px;color:var(--danger)">${modelsError}</span>`
|
|
276
274
|
: nothing}
|
|
277
275
|
</div>
|
|
278
276
|
<input
|
|
279
277
|
type="text"
|
|
280
|
-
style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--
|
|
278
|
+
style="width:100%;box-sizing:border-box;padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-input);color:var(--fg);font-size:12px"
|
|
281
279
|
placeholder="Endpoint (optional, e.g. http://localhost:11434/v1)"
|
|
282
280
|
.value=${baseUrlDraft}
|
|
283
281
|
@input=${(e: Event) => {
|