@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.
- package/dist/iframe-entry.js +6238 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +30972 -17222
- package/dist/studio.js.map +328 -83
- package/package.json +9 -9
- 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 +5 -1
- package/src/new-project/design-fields.ts +260 -0
- package/src/new-project/import-tab.ts +322 -0
- package/src/new-project/new-project-modal.ts +472 -89
- package/src/new-project/templates.ts +61 -0
- package/src/packages/ensure-deps.ts +6 -0
- package/src/packages/pull-package-sync.ts +339 -0
- package/src/page-params.ts +383 -0
- package/src/panels/ai-chat/attached-context.ts +49 -0
- package/src/panels/ai-chat/chat-markdown.ts +38 -0
- package/src/panels/ai-chat/chat-view.ts +250 -0
- package/src/panels/ai-chat/composer.ts +315 -0
- package/src/panels/ai-chat/sessions-view.ts +98 -0
- package/src/panels/ai-panel.ts +189 -457
- 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/git-panel.ts +16 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +21 -7
- 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 +45 -1
- package/src/services/agent-seed.ts +91 -0
- package/src/services/ai-models.ts +64 -0
- package/src/services/ai-session-store.ts +250 -0
- package/src/services/ai-settings.ts +5 -0
- package/src/services/automation.ts +140 -0
- package/src/services/document-assistant.ts +98 -38
- package/src/services/import-client.ts +134 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +9 -9
- package/src/services/settings-store.ts +93 -0
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/store.ts +4 -62
- package/src/studio.ts +115 -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 +98 -18
- package/src/ui/ai-credentials-form.ts +205 -0
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +13 -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
package/src/types.ts
CHANGED
|
@@ -123,9 +123,57 @@ export interface RenameResult {
|
|
|
123
123
|
error?: string;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** A starter template surfaced in the New Project picker (mirrors @jxsuite/starters StarterMeta). */
|
|
127
|
+
export interface StarterInfo {
|
|
128
|
+
id: string;
|
|
129
|
+
name: string;
|
|
130
|
+
industry: string;
|
|
131
|
+
tagline: string;
|
|
132
|
+
description: string;
|
|
133
|
+
features: string[];
|
|
134
|
+
accent: string;
|
|
135
|
+
/** Preview image as a self-contained `data:` URI. */
|
|
136
|
+
thumbnail: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A progress line from the AI-guided site import (mirrors @jxsuite/import ImportProgressEvent). */
|
|
140
|
+
export interface ImportProgressEvent {
|
|
141
|
+
phase: string;
|
|
142
|
+
message: string;
|
|
143
|
+
current?: number;
|
|
144
|
+
total?: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Options for {@link StudioPlatform.importSite}. */
|
|
148
|
+
export interface ImportSiteOptions {
|
|
149
|
+
/** The live site to clone; must be http(s). */
|
|
150
|
+
url: string;
|
|
151
|
+
/** Display name for the new project. */
|
|
152
|
+
name: string;
|
|
153
|
+
/** Destination directory (platform-interpreted: project-relative on the dev server). */
|
|
154
|
+
directory: string;
|
|
155
|
+
/** Max crawl depth; 0 = single page. */
|
|
156
|
+
depth: number;
|
|
157
|
+
/** Max pages to capture. */
|
|
158
|
+
maxPages: number;
|
|
159
|
+
/** Refine component/prop names with the LLM (requires a key). */
|
|
160
|
+
aiComponents: boolean;
|
|
161
|
+
/** OpenAI-compatible credentials, from the user's AI settings. */
|
|
162
|
+
apiKey?: string;
|
|
163
|
+
baseUrl?: string;
|
|
164
|
+
model?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
126
167
|
export interface StudioPlatform {
|
|
127
168
|
id: string;
|
|
128
169
|
projectRoot: string;
|
|
170
|
+
/**
|
|
171
|
+
* URL of the canvas iframe document. Optional: when set (chromium serves it from the project
|
|
172
|
+
* server under the studio namespace), the iframe host uses it; otherwise the host falls back to
|
|
173
|
+
* the default dev-server path. Not keyed on `id` — chromium and electrobun both report `id:
|
|
174
|
+
* "desktop"`, but only chromium sets this.
|
|
175
|
+
*/
|
|
176
|
+
canvasUrl?: string;
|
|
129
177
|
activate: (root?: string) => Promise<void>;
|
|
130
178
|
openProject: () => Promise<{
|
|
131
179
|
config: ProjectConfig;
|
|
@@ -190,6 +238,13 @@ export interface StudioPlatform {
|
|
|
190
238
|
/** Invoke a format capability (parse/serialize) — { format, action, source?, doc?, options? }. */
|
|
191
239
|
formatAction?: (payload: Record<string, unknown>) => Promise<unknown>;
|
|
192
240
|
fetchPluginSchema: (src: string, prototype?: string, base?: string) => Promise<unknown>;
|
|
241
|
+
/**
|
|
242
|
+
* Resolve a class-prototype config through the backend's `/__jx_resolve__` pipeline (with the
|
|
243
|
+
* project's content types loaded) and return the parsed result. Used by the tab-bar's dynamic
|
|
244
|
+
* route-param picker to enumerate ContentCollection entries. Optional: platforms without a
|
|
245
|
+
* resolve backend omit it and the picker falls back to a plain fetch.
|
|
246
|
+
*/
|
|
247
|
+
resolveClass?: (body: Record<string, unknown>) => Promise<unknown>;
|
|
193
248
|
gitStatus: () => Promise<GitStatusResult>;
|
|
194
249
|
gitBranches: () => Promise<GitBranchesResult>;
|
|
195
250
|
gitLog: (limit?: number) => Promise<GitLogEntry[]>;
|
|
@@ -213,7 +268,41 @@ export interface StudioPlatform {
|
|
|
213
268
|
url?: string;
|
|
214
269
|
adapter?: string;
|
|
215
270
|
directory: string;
|
|
271
|
+
/** Id of a starter template to clone, or "blank"/undefined for the minimal template. */
|
|
272
|
+
starter?: string;
|
|
273
|
+
/** Id of a built-in template variant (from `@jxsuite/create/templates`); undefined = blank. */
|
|
274
|
+
template?: string;
|
|
275
|
+
/** Design quickstart (colors, fonts, logo, breakpoints) applied on top of the scaffold. */
|
|
276
|
+
design?: {
|
|
277
|
+
accent?: string;
|
|
278
|
+
background?: string;
|
|
279
|
+
text?: string;
|
|
280
|
+
bodyFont?: string;
|
|
281
|
+
headingFont?: string;
|
|
282
|
+
media?: Record<string, string>;
|
|
283
|
+
logo?: { name: string; base64: string };
|
|
284
|
+
};
|
|
216
285
|
}) => Promise<{ root: string; config: ProjectConfig }>;
|
|
286
|
+
/**
|
|
287
|
+
* List the starter templates available in the New Project picker. Absent on platforms that don't
|
|
288
|
+
* ship starters (the picker then offers only a blank project).
|
|
289
|
+
*/
|
|
290
|
+
listStarters?: () => Promise<StarterInfo[]>;
|
|
291
|
+
/**
|
|
292
|
+
* AI-guided import of an existing site into a new project. Runs in the backend (headless Chrome +
|
|
293
|
+
* fs), streaming progress until the project is written. Absent on platforms without a backend
|
|
294
|
+
* import pipeline — the New Project modal hides its Import tab then.
|
|
295
|
+
*/
|
|
296
|
+
importSite?: (
|
|
297
|
+
opts: ImportSiteOptions,
|
|
298
|
+
onProgress: (evt: ImportProgressEvent) => void,
|
|
299
|
+
signal?: AbortSignal,
|
|
300
|
+
) => Promise<{ root: string; config: ProjectConfig }>;
|
|
301
|
+
/**
|
|
302
|
+
* Open a native directory picker and return the chosen absolute path (null when cancelled).
|
|
303
|
+
* Desktop only; the dev server interprets directories relative to its root instead.
|
|
304
|
+
*/
|
|
305
|
+
pickDirectory?: () => Promise<string | null>;
|
|
217
306
|
/** Stack B AI assistant: URL of the OpenAI-compatible SSE chat proxy (`/__studio/ai/chat`). */
|
|
218
307
|
aiChatUrl: () => string | Promise<string>;
|
|
219
308
|
// ─── Multi-window (desktop only; undefined on dev-server) ───────────────────
|
|
@@ -237,6 +326,15 @@ export interface StudioPlatform {
|
|
|
237
326
|
getRecentProjects?: () => Promise<RecentProjectEntry[]>;
|
|
238
327
|
/** Persist the full recent-projects list to the backend store. */
|
|
239
328
|
saveRecentProjects?: (projects: RecentProjectEntry[]) => Promise<void>;
|
|
329
|
+
// ─── User settings (backend-persisted; undefined on dev-server) ─────────────
|
|
330
|
+
/**
|
|
331
|
+
* Read the user-level settings map (e.g. the AI connection parameters) from a backend store
|
|
332
|
+
* shared across all projects/windows. Platforms without a native backend (dev server) omit it and
|
|
333
|
+
* settings live in localStorage only.
|
|
334
|
+
*/
|
|
335
|
+
getSettings?: () => Promise<Record<string, string>>;
|
|
336
|
+
/** Persist the full user-level settings map to the backend store. */
|
|
337
|
+
saveSettings?: (settings: Record<string, string>) => Promise<void>;
|
|
240
338
|
}
|
|
241
339
|
|
|
242
340
|
/** A recently-opened project, keyed by its re-openable `root` (platform-specific). */
|
|
@@ -252,18 +350,11 @@ export interface CanvasPanel {
|
|
|
252
350
|
mediaName: string;
|
|
253
351
|
element: HTMLElement;
|
|
254
352
|
canvas: HTMLElement;
|
|
255
|
-
overlay: HTMLElement;
|
|
256
|
-
overlayClk: HTMLElement;
|
|
257
353
|
viewport: HTMLElement;
|
|
258
354
|
scrollContainer: HTMLElement;
|
|
259
|
-
dropLine: HTMLElement;
|
|
260
355
|
_width: number | null;
|
|
261
356
|
/** True when the panel's DOM reflects the current document via a successful live render. */
|
|
262
357
|
ready: boolean;
|
|
263
|
-
/** Breakpoints active for this panel's width (persisted for surgical patch re-application). */
|
|
264
|
-
activeBreakpoints: Set<string> | null;
|
|
265
|
-
/** Render context captured from the last successful live render (null until then). */
|
|
266
|
-
liveCtx: PanelLiveCtx | null;
|
|
267
358
|
/**
|
|
268
359
|
* Effect scope owning the reactive effects created while rendering this panel's content
|
|
269
360
|
* (including child scopes from surgical subtree renders). Stopped when panels are rebuilt.
|
|
@@ -271,17 +362,6 @@ export interface CanvasPanel {
|
|
|
271
362
|
renderScope: { stop: () => void; run: <T>(fn: () => T) => T | undefined } | null;
|
|
272
363
|
}
|
|
273
364
|
|
|
274
|
-
/** Per-panel context persisted by a successful live render so patches can re-render subtrees. */
|
|
275
|
-
export interface PanelLiveCtx {
|
|
276
|
-
scope: Record<string, unknown>;
|
|
277
|
-
canvasMode: string;
|
|
278
|
-
layoutWrapped: boolean;
|
|
279
|
-
pageContentPrefix: (string | number)[] | null;
|
|
280
|
-
pageContentOffset: number;
|
|
281
|
-
arrayPaths: Set<string>;
|
|
282
|
-
pathMapper: (created: Node, path: (string | number)[], def: unknown) => void;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
365
|
export interface DocumentStackEntry {
|
|
286
366
|
document: JxMutableNode;
|
|
287
367
|
documentPath: string | null;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Ai-credentials-form.ts — reusable AI provider credentials form (key / model / endpoint).
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the ai-panel key gate so other hosts (e.g. the New Project modal) can embed the
|
|
6
|
+
* same form. All draft/model-list state lives inside the closure, so multiple instances never
|
|
7
|
+
* share state. Persists via src/services/ai-settings.ts on Save.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { html, nothing } from "lit-html";
|
|
13
|
+
import type { TemplateResult } from "lit-html";
|
|
14
|
+
import { fetchAvailableModels, invalidateModelCache } from "../services/ai-models";
|
|
15
|
+
import {
|
|
16
|
+
getBaseUrl,
|
|
17
|
+
getModel,
|
|
18
|
+
getOpenAiKey,
|
|
19
|
+
hasOpenAiKey,
|
|
20
|
+
setBaseUrl,
|
|
21
|
+
setModel,
|
|
22
|
+
setOpenAiKey,
|
|
23
|
+
} from "../services/ai-settings";
|
|
24
|
+
|
|
25
|
+
export interface AiCredentialsFormOptions {
|
|
26
|
+
/** Host re-render scheduler — called whenever the form's internal state changes. */
|
|
27
|
+
requestRender: () => void;
|
|
28
|
+
/** Called after Save persists the credentials. */
|
|
29
|
+
onSaved?: () => void;
|
|
30
|
+
/** Called when Cancel dismisses the form (Cancel is only offered when a key already exists). */
|
|
31
|
+
onCancel?: () => void;
|
|
32
|
+
/** Optional context line (TemplateResult | string) replacing the default blurb. */
|
|
33
|
+
intro?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AiCredentialsForm {
|
|
37
|
+
render: () => TemplateResult;
|
|
38
|
+
/** Preload drafts from the stored ai-settings and auto-fetch the model list. */
|
|
39
|
+
startEdit: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create an AI credentials form instance bound to a host's render scheduler.
|
|
44
|
+
*
|
|
45
|
+
* @param {AiCredentialsFormOptions} opts
|
|
46
|
+
* @returns {AiCredentialsForm}
|
|
47
|
+
*/
|
|
48
|
+
export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCredentialsForm {
|
|
49
|
+
let keyDraft = "";
|
|
50
|
+
let baseUrlDraft = "";
|
|
51
|
+
let modelDraft = "";
|
|
52
|
+
|
|
53
|
+
/** Fetched from the proxy's /models endpoint (sibling of the chat endpoint). */
|
|
54
|
+
let availableModels: { id: string; name: string }[] = [];
|
|
55
|
+
let modelsLoading = false;
|
|
56
|
+
let modelsError = "";
|
|
57
|
+
|
|
58
|
+
/** Open the form pre-filled with the current settings, and load the model list. */
|
|
59
|
+
function startEdit() {
|
|
60
|
+
keyDraft = getOpenAiKey();
|
|
61
|
+
baseUrlDraft = getBaseUrl();
|
|
62
|
+
modelDraft = getModel();
|
|
63
|
+
opts.requestRender();
|
|
64
|
+
// Auto-fetch available models if not already loaded.
|
|
65
|
+
if (availableModels.length === 0 && !modelsLoading) {
|
|
66
|
+
void fetchModels();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Persist the drafted key + endpoint + model and notify the host. */
|
|
71
|
+
function save() {
|
|
72
|
+
setOpenAiKey(keyDraft);
|
|
73
|
+
setBaseUrl(baseUrlDraft);
|
|
74
|
+
setModel(modelDraft);
|
|
75
|
+
keyDraft = "";
|
|
76
|
+
baseUrlDraft = "";
|
|
77
|
+
modelDraft = "";
|
|
78
|
+
// Clear fetched models so they're re-fetched with the new credentials next time.
|
|
79
|
+
availableModels = [];
|
|
80
|
+
invalidateModelCache();
|
|
81
|
+
opts.onSaved?.();
|
|
82
|
+
opts.requestRender();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Dismiss the form without saving (only offered when a key already exists). */
|
|
86
|
+
function cancel() {
|
|
87
|
+
keyDraft = "";
|
|
88
|
+
baseUrlDraft = "";
|
|
89
|
+
modelDraft = "";
|
|
90
|
+
opts.onCancel?.();
|
|
91
|
+
opts.requestRender();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fetch available models via src/services/ai-models.ts, preferring the in-form drafts over the
|
|
96
|
+
* stored settings so the list reflects the credentials being edited. Always forces past the
|
|
97
|
+
* module cache — this runs on explicit user action (or first open) with possibly-new drafts.
|
|
98
|
+
*/
|
|
99
|
+
async function fetchModels() {
|
|
100
|
+
modelsLoading = true;
|
|
101
|
+
modelsError = "";
|
|
102
|
+
opts.requestRender();
|
|
103
|
+
try {
|
|
104
|
+
availableModels = await fetchAvailableModels({
|
|
105
|
+
apiKey: getOpenAiKey() || keyDraft,
|
|
106
|
+
baseUrl: baseUrlDraft || getBaseUrl(),
|
|
107
|
+
force: true,
|
|
108
|
+
});
|
|
109
|
+
} catch (error: unknown) {
|
|
110
|
+
modelsError = (error as Error).message || "Failed to fetch models";
|
|
111
|
+
} finally {
|
|
112
|
+
modelsLoading = false;
|
|
113
|
+
opts.requestRender();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The key + model + endpoint form column. */
|
|
118
|
+
function render(): TemplateResult {
|
|
119
|
+
const haveKey = hasOpenAiKey();
|
|
120
|
+
return html`
|
|
121
|
+
<div
|
|
122
|
+
class="ai-creds-form"
|
|
123
|
+
style="display:flex;flex-direction:column;gap:10px;max-width:320px;text-align:left"
|
|
124
|
+
>
|
|
125
|
+
<div style="font-weight:600;align-self:center">AI provider key</div>
|
|
126
|
+
${opts.intro === undefined
|
|
127
|
+
? html`
|
|
128
|
+
<div style="font-size:11px;color:var(--fg-dim)">
|
|
129
|
+
Any OpenAI-compatible key works. Stored locally in this browser; sent only to the
|
|
130
|
+
Studio proxy (never to a third party except your chosen endpoint).
|
|
131
|
+
</div>
|
|
132
|
+
`
|
|
133
|
+
: html`<div style="font-size:11px;color:var(--fg-dim)">${opts.intro}</div>`}
|
|
134
|
+
<input
|
|
135
|
+
type="password"
|
|
136
|
+
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"
|
|
137
|
+
placeholder="sk-… or any compatible key"
|
|
138
|
+
.value=${keyDraft}
|
|
139
|
+
@input=${(e: Event) => {
|
|
140
|
+
keyDraft = (e.target as HTMLInputElement).value;
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
<div style="font-weight:500;font-size:11px;margin-top:4px">Model</div>
|
|
144
|
+
${availableModels.length > 0
|
|
145
|
+
? html`
|
|
146
|
+
<sp-combobox
|
|
147
|
+
size="s"
|
|
148
|
+
allows-custom-value
|
|
149
|
+
.value=${modelDraft}
|
|
150
|
+
@change=${(e: Event) => {
|
|
151
|
+
modelDraft = (e.target as HTMLInputElement).value;
|
|
152
|
+
}}
|
|
153
|
+
@input=${(e: Event) => {
|
|
154
|
+
modelDraft = (e.target as HTMLInputElement).value;
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
${availableModels.map(
|
|
158
|
+
(m) => html`<sp-menu-item value=${m.id}>${m.name}</sp-menu-item>`,
|
|
159
|
+
)}
|
|
160
|
+
</sp-combobox>
|
|
161
|
+
`
|
|
162
|
+
: html`
|
|
163
|
+
<input
|
|
164
|
+
type="text"
|
|
165
|
+
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"
|
|
166
|
+
placeholder="Model ID (e.g. gpt-4o, claude-sonnet-4-20250514, etc.)"
|
|
167
|
+
.value=${modelDraft}
|
|
168
|
+
@input=${(e: Event) => {
|
|
169
|
+
modelDraft = (e.target as HTMLInputElement).value;
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
`}
|
|
173
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
174
|
+
<sp-button size="s" variant="secondary" ?disabled=${modelsLoading} @click=${fetchModels}>
|
|
175
|
+
${modelsLoading
|
|
176
|
+
? "Fetching…"
|
|
177
|
+
: availableModels.length > 0
|
|
178
|
+
? "Refresh models"
|
|
179
|
+
: "Fetch models"}
|
|
180
|
+
</sp-button>
|
|
181
|
+
${modelsError
|
|
182
|
+
? html`<span style="font-size:10px;color:var(--danger)">${modelsError}</span>`
|
|
183
|
+
: nothing}
|
|
184
|
+
</div>
|
|
185
|
+
<input
|
|
186
|
+
type="text"
|
|
187
|
+
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"
|
|
188
|
+
placeholder="Endpoint (optional, e.g. http://localhost:11434/v1)"
|
|
189
|
+
.value=${baseUrlDraft}
|
|
190
|
+
@input=${(e: Event) => {
|
|
191
|
+
baseUrlDraft = (e.target as HTMLInputElement).value;
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
<div style="display:flex;gap:8px;align-self:flex-end">
|
|
195
|
+
${haveKey
|
|
196
|
+
? html`<sp-button size="s" variant="secondary" @click=${cancel}>Cancel</sp-button>`
|
|
197
|
+
: nothing}
|
|
198
|
+
<sp-button size="s" variant="primary" @click=${save}>Save</sp-button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { render, startEdit };
|
|
205
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jx brand theme fragment.
|
|
3
|
+
*
|
|
4
|
+
* Registered as the Spectrum 'app' fragment (see spectrum.ts), so it is adopted into every
|
|
5
|
+
* <sp-theme> shadow root _after_ the system/color/scale fragments and wins the :host cascade. It
|
|
6
|
+
* rebrands the stock Spectrum dark theme by overriding only the palette `-rgb` triplets: every
|
|
7
|
+
* derived token in the published theme CSS (accent, focus ring, background layers, alpha-composed
|
|
8
|
+
* tints) resolves through `rgba(var(--spectrum-*-rgb))`, so the whole theme follows coherently.
|
|
9
|
+
*
|
|
10
|
+
* Canonical brand values: sites/jxsuite.com/project.json (accent #3b82f6, accent-hover #60a5fa,
|
|
11
|
+
* neutral surfaces #0a0a0a / #111111 / #1a1a1a / #222222). Intermediate stops interpolate along the
|
|
12
|
+
* Tailwind blue/zinc ramps the brand palette derives from.
|
|
13
|
+
*
|
|
14
|
+
* Dark-theme stop order: gray 50 (darkest) -> 900 (lightest); blue 100 (darkest) -> 1400
|
|
15
|
+
* (lightest). Semantic anchors under this ramp:
|
|
16
|
+
*
|
|
17
|
+
* - Accent button fill = accent-500 (white text ~5.2:1)
|
|
18
|
+
* - Focus ring = blue-800 (brand accent-hover)
|
|
19
|
+
* - Studio --accent = accent-700 (exact brand blue)
|
|
20
|
+
* - Accent content/visual = accent-900 (soft on-dark tint)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { css } from "@spectrum-web-components/base";
|
|
24
|
+
|
|
25
|
+
export const jxTheme = css`
|
|
26
|
+
:host {
|
|
27
|
+
/* Neutral (gray) ramp — Jx near-black surfaces and zinc text tones */
|
|
28
|
+
--spectrum-gray-50-rgb: 10, 10, 10; /* #0a0a0a bg-primary / base */
|
|
29
|
+
--spectrum-gray-75-rgb: 17, 17, 17; /* #111111 bg-secondary / layer-1 */
|
|
30
|
+
--spectrum-gray-100-rgb: 26, 26, 26; /* #1a1a1a surface / layer-2 */
|
|
31
|
+
--spectrum-gray-200-rgb: 34, 34, 34; /* #222222 border / surface-hover */
|
|
32
|
+
--spectrum-gray-300-rgb: 63, 63, 70; /* #3f3f46 strong border */
|
|
33
|
+
--spectrum-gray-400-rgb: 82, 82, 91; /* #52525b component border */
|
|
34
|
+
--spectrum-gray-500-rgb: 113, 113, 122; /* #71717a text-muted */
|
|
35
|
+
--spectrum-gray-600-rgb: 161, 161, 170; /* #a1a1aa text-secondary */
|
|
36
|
+
--spectrum-gray-700-rgb: 212, 212, 216; /* #d4d4d8 subtle text */
|
|
37
|
+
--spectrum-gray-800-rgb: 228, 228, 231; /* #e4e4e7 primary text */
|
|
38
|
+
--spectrum-gray-900-rgb: 250, 250, 250; /* #fafafa ink / headings */
|
|
39
|
+
|
|
40
|
+
/* Blue ramp — drives accent, informative, and the focus indicator */
|
|
41
|
+
--spectrum-blue-100-rgb: 23, 37, 84; /* #172554 */
|
|
42
|
+
--spectrum-blue-200-rgb: 30, 58, 138; /* #1e3a8a */
|
|
43
|
+
--spectrum-blue-300-rgb: 30, 64, 175; /* #1e40af */
|
|
44
|
+
--spectrum-blue-400-rgb: 29, 78, 216; /* #1d4ed8 */
|
|
45
|
+
--spectrum-blue-500-rgb: 37, 99, 235; /* #2563eb accent button fill */
|
|
46
|
+
--spectrum-blue-600-rgb: 48, 112, 241; /* #3070f1 */
|
|
47
|
+
--spectrum-blue-700-rgb: 59, 130, 246; /* #3b82f6 brand accent */
|
|
48
|
+
--spectrum-blue-800-rgb: 96, 165, 250; /* #60a5fa accent-hover / focus */
|
|
49
|
+
--spectrum-blue-900-rgb: 147, 197, 253; /* #93c5fd soft on-dark accent */
|
|
50
|
+
--spectrum-blue-1000-rgb: 191, 219, 254; /* #bfdbfe */
|
|
51
|
+
--spectrum-blue-1100-rgb: 219, 234, 254; /* #dbeafe */
|
|
52
|
+
--spectrum-blue-1200-rgb: 239, 246, 255; /* #eff6ff */
|
|
53
|
+
--spectrum-blue-1300-rgb: 248, 250, 252; /* #f8fafc */
|
|
54
|
+
--spectrum-blue-1400-rgb: 255, 255, 255; /* #ffffff */
|
|
55
|
+
|
|
56
|
+
/* Brand font stacks. Sans drops adobe-clean so Adobe Clean never renders
|
|
57
|
+
on machines that have it installed; mono leads with the vendored
|
|
58
|
+
JetBrains Mono (see index.html @font-face). */
|
|
59
|
+
--spectrum-sans-font-family-stack: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
60
|
+
--spectrum-code-font-family-stack:
|
|
61
|
+
"JetBrains Mono", "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
|
62
|
+
}
|
|
63
|
+
`;
|
package/src/ui/media-picker.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { ref } from "lit-html/directives/ref.js";
|
|
|
12
12
|
import { getPlatform } from "../platform";
|
|
13
13
|
import { debouncedStyleCommit, renderOnly } from "../store";
|
|
14
14
|
import { getLayerSlot } from "./layers";
|
|
15
|
+
import { rectOf } from "../utils/geometry";
|
|
16
|
+
import { loopbackAssetSrc } from "../canvas/canvas-origin";
|
|
15
17
|
|
|
16
18
|
// ─── Media file cache ────────────────────────────────────────────────────────
|
|
17
19
|
|
|
@@ -142,7 +144,7 @@ function onPopoverOutsideClick(e: MouseEvent) {
|
|
|
142
144
|
|
|
143
145
|
function renderMediaPickerPopover() {
|
|
144
146
|
const host = getLayerSlot("popover", "media-picker");
|
|
145
|
-
const rect = _popoverAnchorEl
|
|
147
|
+
const rect = _popoverAnchorEl ? rectOf(_popoverAnchorEl) : undefined;
|
|
146
148
|
if (!rect) {
|
|
147
149
|
return;
|
|
148
150
|
}
|
|
@@ -210,7 +212,7 @@ function renderMediaPickerPopover() {
|
|
|
210
212
|
${m.isImage
|
|
211
213
|
? html`<img
|
|
212
214
|
slot="icon"
|
|
213
|
-
src=${m.path}
|
|
215
|
+
src=${loopbackAssetSrc(m.path)}
|
|
214
216
|
alt=""
|
|
215
217
|
style="width:24px;height:24px;object-fit:cover;border-radius:var(--spectrum-corner-radius-75, 2px)"
|
|
216
218
|
/>`
|
|
@@ -232,7 +234,7 @@ function renderMediaPickerPopover() {
|
|
|
232
234
|
// Fine-tune position after render using actual measured dimensions
|
|
233
235
|
requestAnimationFrame(() => {
|
|
234
236
|
if (_popoverEl) {
|
|
235
|
-
const popoverRect = _popoverEl
|
|
237
|
+
const popoverRect = rectOf(_popoverEl);
|
|
236
238
|
let adjLeft = popoverRect.left;
|
|
237
239
|
let adjTop = popoverRect.top;
|
|
238
240
|
let needsAdjust = false;
|
|
@@ -304,7 +306,7 @@ export function renderMediaPicker(prop: string, value: string, onCommit: (val: s
|
|
|
304
306
|
return html`
|
|
305
307
|
<div class="media-picker">
|
|
306
308
|
${isImage && currentValue
|
|
307
|
-
? html`<img class="media-picker-thumb" src=${currentValue} alt="" />`
|
|
309
|
+
? html`<img class="media-picker-thumb" src=${loopbackAssetSrc(currentValue)} alt="" />`
|
|
308
310
|
: nothing}
|
|
309
311
|
<sp-textfield
|
|
310
312
|
size="s"
|
package/src/ui/spectrum.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { Theme } from "@spectrum-web-components/theme/src/Theme.js";
|
|
|
11
11
|
import themeSpectrumCSS from "@spectrum-web-components/theme/src/theme.css.js";
|
|
12
12
|
import themeDarkCSS from "@spectrum-web-components/theme/src/theme-dark.css.js";
|
|
13
13
|
import scaleMediumCSS from "@spectrum-web-components/theme/src/scale-medium.css.js";
|
|
14
|
+
import { jxTheme } from "./jx-theme";
|
|
14
15
|
import { Tabs } from "@spectrum-web-components/tabs/src/Tabs.js";
|
|
15
16
|
import { Tab } from "@spectrum-web-components/tabs/src/Tab.js";
|
|
16
17
|
import { TabPanel } from "@spectrum-web-components/tabs/src/TabPanel.js";
|
|
@@ -126,6 +127,10 @@ import { IconVisibility } from "@spectrum-web-components/icons-workflow/src/elem
|
|
|
126
127
|
import { IconVisibilityOff } from "@spectrum-web-components/icons-workflow/src/elements/IconVisibilityOff.js";
|
|
127
128
|
import { IconArtboard } from "@spectrum-web-components/icons-workflow/src/elements/IconArtboard.js";
|
|
128
129
|
import { IconChat } from "@spectrum-web-components/icons-workflow/src/elements/IconChat.js";
|
|
130
|
+
import { IconSend } from "@spectrum-web-components/icons-workflow/src/elements/IconSend.js";
|
|
131
|
+
import { IconStop } from "@spectrum-web-components/icons-workflow/src/elements/IconStop.js";
|
|
132
|
+
import { IconHistory } from "@spectrum-web-components/icons-workflow/src/elements/IconHistory.js";
|
|
133
|
+
import { IconAttach } from "@spectrum-web-components/icons-workflow/src/elements/IconAttach.js";
|
|
129
134
|
import { IconViewList } from "@spectrum-web-components/icons-workflow/src/elements/IconViewList.js";
|
|
130
135
|
import { IconRailRightClose } from "@spectrum-web-components/icons-workflow/src/elements/IconRailRightClose.js";
|
|
131
136
|
import { IconRailRightOpen } from "@spectrum-web-components/icons-workflow/src/elements/IconRailRightOpen.js";
|
|
@@ -271,6 +276,10 @@ const components = [
|
|
|
271
276
|
["sp-icon-visibility-off", IconVisibilityOff],
|
|
272
277
|
["sp-icon-artboard", IconArtboard],
|
|
273
278
|
["sp-icon-chat", IconChat],
|
|
279
|
+
["sp-icon-send", IconSend],
|
|
280
|
+
["sp-icon-stop", IconStop],
|
|
281
|
+
["sp-icon-history", IconHistory],
|
|
282
|
+
["sp-icon-attach", IconAttach],
|
|
274
283
|
["sp-icon-view-list", IconViewList],
|
|
275
284
|
["sp-icon-text-bold", IconTextBold],
|
|
276
285
|
["sp-icon-text-italic", IconTextItalic],
|
|
@@ -296,5 +305,9 @@ for (const [tag, ctor] of components as [string, CustomElementConstructor][]) {
|
|
|
296
305
|
Theme.registerThemeFragment("spectrum", "system", themeSpectrumCSS);
|
|
297
306
|
Theme.registerThemeFragment("dark", "color", themeDarkCSS);
|
|
298
307
|
Theme.registerThemeFragment("medium", "scale", scaleMediumCSS);
|
|
308
|
+
/* Jx brand overrides. The 'app' kind must be registered under the literal
|
|
309
|
+
name "app" and, registered last, is adopted after the fragments above so
|
|
310
|
+
its :host declarations win the cascade (see src/ui/jx-theme.ts). */
|
|
311
|
+
Theme.registerThemeFragment("app", "app", jxTheme);
|
|
299
312
|
|
|
300
313
|
export { components };
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
/// <reference lib="dom.iterable" />
|
|
3
3
|
/** Canvas media/breakpoint utilities — pure functions extracted for testability. */
|
|
4
4
|
|
|
5
|
-
import type { JxStyle } from "@jxsuite/schema/types";
|
|
6
|
-
|
|
7
5
|
/**
|
|
8
6
|
* Classify $media entries into size breakpoints (get a canvas each) and feature queries (rendered
|
|
9
7
|
* as toolbar toggles).
|
|
@@ -63,138 +61,3 @@ export function activeBreakpointsForWidth(
|
|
|
63
61
|
}
|
|
64
62
|
return active;
|
|
65
63
|
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Apply styles to a canvas element, including active media overrides. Base (flat) styles applied
|
|
69
|
-
* first, then matching media overrides in source order.
|
|
70
|
-
*
|
|
71
|
-
* @param {HTMLElement} el
|
|
72
|
-
* @param {JxStyle | null | undefined} styleDef
|
|
73
|
-
* @param {Set<string>} activeBreakpoints
|
|
74
|
-
* @param {Record<string, boolean>} featureToggles
|
|
75
|
-
*/
|
|
76
|
-
export function applyCanvasStyle(
|
|
77
|
-
el: HTMLElement,
|
|
78
|
-
styleDef: JxStyle | null | undefined,
|
|
79
|
-
activeBreakpoints: Set<string>,
|
|
80
|
-
featureToggles: Record<string, boolean>,
|
|
81
|
-
) {
|
|
82
|
-
if (!styleDef || typeof styleDef !== "object") {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
for (const [prop, val] of Object.entries(styleDef)) {
|
|
86
|
-
if (typeof val === "string" || typeof val === "number") {
|
|
87
|
-
try {
|
|
88
|
-
if (prop.startsWith("--")) {
|
|
89
|
-
el.style.setProperty(prop, String(val));
|
|
90
|
-
} else {
|
|
91
|
-
(el.style as unknown as Record<string, unknown>)[prop] = val;
|
|
92
|
-
}
|
|
93
|
-
} catch {}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
for (const [key, val] of Object.entries(styleDef)) {
|
|
97
|
-
if (!key.startsWith("@") || typeof val !== "object" || val === null) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const mediaName = key.slice(1);
|
|
101
|
-
if (mediaName === "--") {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (activeBreakpoints.has(mediaName) || featureToggles[mediaName]) {
|
|
105
|
-
for (const [prop, v] of Object.entries(/** @type {Record<string, unknown>} */ val)) {
|
|
106
|
-
if (typeof v === "string" || typeof v === "number") {
|
|
107
|
-
try {
|
|
108
|
-
if (prop.startsWith("--")) {
|
|
109
|
-
el.style.setProperty(prop, String(v));
|
|
110
|
-
} else {
|
|
111
|
-
(el.style as unknown as Record<string, unknown>)[prop] = v;
|
|
112
|
-
}
|
|
113
|
-
} catch {}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Scan stylesheets for @media rules matching active breakpoints, collecting the CSS declarations
|
|
122
|
-
* that should be applied as inline overrides per data-jx element.
|
|
123
|
-
*
|
|
124
|
-
* Returns a Map of data-jx uid → Map of CSS property → value.
|
|
125
|
-
*
|
|
126
|
-
* @param {Iterable<CSSStyleSheet>} styleSheets
|
|
127
|
-
* @param {Set<string>} activeBreakpoints
|
|
128
|
-
* @returns {Map<string, Map<string, string>>}
|
|
129
|
-
*/
|
|
130
|
-
export function collectMediaOverrides(
|
|
131
|
-
styleSheets: Iterable<CSSStyleSheet>,
|
|
132
|
-
activeBreakpoints: Set<string>,
|
|
133
|
-
) {
|
|
134
|
-
const overrides = new Map<string, Map<string, string>>();
|
|
135
|
-
if (activeBreakpoints.size === 0) {
|
|
136
|
-
return overrides;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
for (const sheet of styleSheets) {
|
|
140
|
-
/** @type {CSSRuleList | null} */
|
|
141
|
-
let rules;
|
|
142
|
-
try {
|
|
143
|
-
rules = sheet.cssRules;
|
|
144
|
-
} catch {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (!rules) {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
for (const rule of rules) {
|
|
151
|
-
if (!(rule instanceof CSSMediaRule)) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (!activeBreakpoints.has(rule.conditionText)) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
for (const mediaRule of rule.cssRules) {
|
|
158
|
-
if (!(mediaRule instanceof CSSStyleRule)) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
const selector = mediaRule.selectorText;
|
|
162
|
-
const jxMatch = selector.match(/\[data-jx="([^"]+)"\]/);
|
|
163
|
-
if (!jxMatch) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
const [, uid] = jxMatch;
|
|
167
|
-
if (!overrides.has(uid!)) {
|
|
168
|
-
overrides.set(uid!, new Map());
|
|
169
|
-
}
|
|
170
|
-
const props = overrides.get(uid!) as Map<string, string>;
|
|
171
|
-
for (const prop of mediaRule.style) {
|
|
172
|
-
props.set(prop, mediaRule.style.getPropertyValue(prop));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return overrides;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Apply collected media overrides to elements within a canvas.
|
|
182
|
-
*
|
|
183
|
-
* @param {Element} canvasEl
|
|
184
|
-
* @param {Map<string, Map<string, string>>} overrides
|
|
185
|
-
*/
|
|
186
|
-
export function applyOverridesToCanvas(
|
|
187
|
-
canvasEl: Element,
|
|
188
|
-
overrides: Map<string, Map<string, string>>,
|
|
189
|
-
) {
|
|
190
|
-
for (const [uid, props] of overrides) {
|
|
191
|
-
const els = canvasEl.querySelectorAll(`[data-jx="${uid}"]`);
|
|
192
|
-
for (const el of els) {
|
|
193
|
-
for (const [prop, val] of props) {
|
|
194
|
-
try {
|
|
195
|
-
(el as HTMLElement).style.setProperty(prop, val);
|
|
196
|
-
} catch {}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|