@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,260 @@
1
+ /**
2
+ * The design-quickstart section of the New Project Parameters step: colors, fonts, logo, and
3
+ * breakpoints. A trimmed-down, creation-time version of the settings modal — every field is
4
+ * optional, and empty fields leave the template/starter defaults untouched (the backend applies
5
+ * overrides best-effort against the starters' shared token conventions).
6
+ */
7
+
8
+ import { html } from "lit-html";
9
+ import type { TemplateResult } from "lit-html";
10
+
11
+ /** Mirrors @jxsuite/create's DesignOptions (threaded through platform.createProject). */
12
+ export interface DesignSelection {
13
+ accent?: string;
14
+ background?: string;
15
+ text?: string;
16
+ bodyFont?: string;
17
+ headingFont?: string;
18
+ media?: Record<string, string>;
19
+ logo?: { name: string; base64: string };
20
+ }
21
+
22
+ interface MediaRow {
23
+ name: string;
24
+ value: string;
25
+ }
26
+
27
+ let _accent = "";
28
+ let _background = "";
29
+ let _text = "";
30
+ let _bodyFont = "";
31
+ let _headingFont = "";
32
+ let _mediaRows: MediaRow[] = [];
33
+ let _mediaNote = "";
34
+ let _logo: { name: string; base64: string } | null = null;
35
+ let _logoError = "";
36
+
37
+ // Prefills are display defaults, not overrides: a value identical to its prefill is NOT sent, so
38
+ // An untouched form leaves the template/starter exactly as authored (a starter's registry accent
39
+ // Need not equal its --color-primary, and re-sending a template's own $media is just noise).
40
+ let _prefillAccent = "";
41
+ let _prefillMediaJson = "";
42
+
43
+ function mediaRowsJson(rows: MediaRow[]): string {
44
+ return JSON.stringify(rows.map((r) => [r.name.trim(), r.value.trim()]));
45
+ }
46
+
47
+ export interface DesignPrefill {
48
+ /** Prefilled accent color (a starter's registry accent). */
49
+ accent?: string;
50
+ /** Prefilled breakpoint rows (a template's $media preset). */
51
+ media?: Record<string, string>;
52
+ /** Shown under the breakpoints editor when it starts empty (starter sites). */
53
+ mediaNote?: string;
54
+ }
55
+
56
+ /** Reset the section for a fresh modal pass, seeding prefills from the chosen source. */
57
+ export function resetDesignFields(prefill: DesignPrefill = {}) {
58
+ _accent = prefill.accent ?? "";
59
+ _background = "";
60
+ _text = "";
61
+ _bodyFont = "";
62
+ _headingFont = "";
63
+ _mediaRows = Object.entries(prefill.media ?? {}).map(([name, value]) => ({ name, value }));
64
+ _mediaNote = prefill.mediaNote ?? "";
65
+ _logo = null;
66
+ _logoError = "";
67
+ _prefillAccent = _accent;
68
+ _prefillMediaJson = mediaRowsJson(_mediaRows);
69
+ }
70
+
71
+ /**
72
+ * The design overrides to send with createProject, or undefined when everything was left at its
73
+ * defaults (the payload then stays byte-identical to a plain create).
74
+ */
75
+ export function collectDesign(): DesignSelection | undefined {
76
+ const media: Record<string, string> = {};
77
+ for (const row of _mediaRows) {
78
+ if (row.name.trim() && row.value.trim()) {
79
+ media[row.name.trim()] = row.value.trim();
80
+ }
81
+ }
82
+ const accentChanged = _accent.trim() && _accent.trim() !== _prefillAccent;
83
+ const mediaChanged =
84
+ Object.keys(media).length > 0 && mediaRowsJson(_mediaRows) !== _prefillMediaJson;
85
+ const design: DesignSelection = {
86
+ ...(accentChanged ? { accent: _accent.trim() } : {}),
87
+ ...(_background.trim() ? { background: _background.trim() } : {}),
88
+ ...(_text.trim() ? { text: _text.trim() } : {}),
89
+ ...(_bodyFont.trim() ? { bodyFont: _bodyFont.trim() } : {}),
90
+ ...(_headingFont.trim() ? { headingFont: _headingFont.trim() } : {}),
91
+ ...(mediaChanged ? { media } : {}),
92
+ ...(_logo ? { logo: _logo } : {}),
93
+ };
94
+ return Object.keys(design).length > 0 ? design : undefined;
95
+ }
96
+
97
+ /** Base64-encode a small file (logos) without blowing the call stack on spread. */
98
+ async function fileToBase64(file: File): Promise<string> {
99
+ const bytes = new Uint8Array(await file.arrayBuffer());
100
+ let binary = "";
101
+ const CHUNK = 32_768;
102
+ for (let i = 0; i < bytes.length; i += CHUNK) {
103
+ binary += String.fromCodePoint(...bytes.subarray(i, i + CHUNK));
104
+ }
105
+ return btoa(binary);
106
+ }
107
+
108
+ function colorRow(
109
+ label: string,
110
+ value: string,
111
+ set: (v: string) => void,
112
+ rerender: () => void,
113
+ ): TemplateResult {
114
+ const onText = (e: Event) => {
115
+ set((e.target as HTMLInputElement).value);
116
+ rerender();
117
+ };
118
+ return html`
119
+ <div class="new-project-color-row">
120
+ <span class="new-project-label">${label}</span>
121
+ <div class="css-var-swatch" style="background:${value || "transparent"}">
122
+ <input type="color" .value=${value || "#888888"} @input=${onText} />
123
+ </div>
124
+ <sp-textfield placeholder="Keep default" .value=${value} @input=${onText}></sp-textfield>
125
+ </div>
126
+ `;
127
+ }
128
+
129
+ export function renderDesignFields(ctx: { rerender: () => void }): TemplateResult {
130
+ const { rerender } = ctx;
131
+
132
+ const onLogoChange = async (e: Event) => {
133
+ const input = e.target as HTMLInputElement;
134
+ const file = input.files?.[0];
135
+ if (!file) {
136
+ return;
137
+ }
138
+ if (!/\.(svg|png|jpe?g|webp|gif|ico)$/i.test(file.name)) {
139
+ _logoError = "Use an SVG, PNG, JPEG, WebP, GIF, or ICO image";
140
+ _logo = null;
141
+ rerender();
142
+ return;
143
+ }
144
+ _logoError = "";
145
+ _logo = { base64: await fileToBase64(file), name: file.name };
146
+ rerender();
147
+ };
148
+
149
+ const setRow = (index: number, field: "name" | "value") => (e: Event) => {
150
+ const row = _mediaRows[index];
151
+ if (row) {
152
+ row[field] = (e.target as HTMLInputElement).value;
153
+ }
154
+ };
155
+
156
+ return html`
157
+ <div class="new-project-design-section">
158
+ <div class="new-project-design-heading">Colors</div>
159
+ ${colorRow("Accent", _accent, (v) => (_accent = v), rerender)}
160
+ ${colorRow("Background", _background, (v) => (_background = v), rerender)}
161
+ ${colorRow("Text", _text, (v) => (_text = v), rerender)}
162
+ </div>
163
+
164
+ <div class="new-project-design-section">
165
+ <div class="new-project-design-heading">Fonts</div>
166
+ <label class="new-project-field">
167
+ <span class="new-project-label">Body Font</span>
168
+ <sp-textfield
169
+ placeholder="Keep default (e.g. 'Inter', system-ui, sans-serif)"
170
+ .value=${_bodyFont}
171
+ @input=${(e: Event) => (_bodyFont = (e.target as HTMLInputElement).value)}
172
+ style="width: 100%"
173
+ ></sp-textfield>
174
+ </label>
175
+ <label class="new-project-field">
176
+ <span class="new-project-label">Heading Font</span>
177
+ <sp-textfield
178
+ placeholder="Keep default"
179
+ .value=${_headingFont}
180
+ @input=${(e: Event) => (_headingFont = (e.target as HTMLInputElement).value)}
181
+ style="width: 100%"
182
+ ></sp-textfield>
183
+ </label>
184
+ </div>
185
+
186
+ <div class="new-project-design-section">
187
+ <div class="new-project-design-heading">Logo</div>
188
+ <div class="new-project-logo-row">
189
+ <input type="file" accept=".svg,.png,.jpg,.jpeg,.webp,.gif,.ico" @change=${onLogoChange} />
190
+ ${_logo
191
+ ? html`
192
+ <span class="new-project-logo-name">
193
+ ${_logo.name} → public/${_logo.name}
194
+ <sp-action-button
195
+ quiet
196
+ size="s"
197
+ title="Remove logo"
198
+ @click=${() => {
199
+ _logo = null;
200
+ rerender();
201
+ }}
202
+ >
203
+
204
+ </sp-action-button>
205
+ </span>
206
+ `
207
+ : html`<span class="new-project-tab-intro"
208
+ >Copied into the project's public/ folder.</span
209
+ >`}
210
+ </div>
211
+ ${_logoError ? html`<div class="new-project-error">${_logoError}</div>` : ""}
212
+ </div>
213
+
214
+ <div class="new-project-design-section">
215
+ <div class="new-project-design-heading">Breakpoints</div>
216
+ ${_mediaRows.length === 0 && _mediaNote
217
+ ? html`<div class="new-project-tab-intro">${_mediaNote}</div>`
218
+ : ""}
219
+ ${_mediaRows.map(
220
+ (row, index) => html`
221
+ <div class="new-project-media-row">
222
+ <sp-textfield
223
+ class="new-project-media-name"
224
+ placeholder="--md"
225
+ .value=${row.name}
226
+ @input=${setRow(index, "name")}
227
+ ></sp-textfield>
228
+ <sp-textfield
229
+ class="new-project-media-value"
230
+ placeholder="(max-width: 768px)"
231
+ .value=${row.value}
232
+ @input=${setRow(index, "value")}
233
+ ></sp-textfield>
234
+ <sp-action-button
235
+ quiet
236
+ size="s"
237
+ title="Remove breakpoint"
238
+ @click=${() => {
239
+ _mediaRows.splice(index, 1);
240
+ rerender();
241
+ }}
242
+ >
243
+
244
+ </sp-action-button>
245
+ </div>
246
+ `,
247
+ )}
248
+ <sp-button
249
+ variant="secondary"
250
+ size="s"
251
+ @click=${() => {
252
+ _mediaRows.push({ name: "", value: "" });
253
+ rerender();
254
+ }}
255
+ >
256
+ Add Breakpoint
257
+ </sp-button>
258
+ </div>
259
+ `;
260
+ }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * The New Project modal's Import source step: an AI-guided clone of an existing site. Gated behind
3
+ * the AI credentials form when no key is stored; otherwise a URL + crawl-options form. The
4
+ * name/directory parameters live on the modal's shared Parameters step; the running pipeline
5
+ * streams its progress into a log here and resolves the modal with the imported project.
6
+ */
7
+
8
+ import { html } from "lit-html";
9
+ import { getPlatform } from "../platform";
10
+ import { getBaseUrl, getModel, getOpenAiKey, hasOpenAiKey } from "../services/ai-settings";
11
+ import { errorMessage } from "@jxsuite/schema/parse";
12
+ import type { TemplateResult } from "lit-html";
13
+ import type { ProjectConfig } from "@jxsuite/schema/types";
14
+ import type { ImportProgressEvent } from "../types";
15
+
16
+ /** Structural view of src/ui/ai-credentials-form's controller (what this tab renders when gated). */
17
+ export interface CredsFormLike {
18
+ render: () => TemplateResult;
19
+ startEdit: () => void;
20
+ }
21
+
22
+ export interface ImportTabCtx {
23
+ /** The modal's shared name/directory state (the import destination slug). */
24
+ form: { name: string; directory: string };
25
+ rerender: () => void;
26
+ /** Called with the imported project; the modal resolves and closes. */
27
+ onDone: (result: { root: string; config: ProjectConfig }) => void;
28
+ /** The modal's shared credentials-form instance, rendered while no key is stored. */
29
+ credsForm: CredsFormLike;
30
+ }
31
+
32
+ let _url = "";
33
+ let _depth = 1;
34
+ let _maxPages = 20;
35
+ let _aiNaming = true;
36
+ let _status: "idle" | "running" | "error" = "idle";
37
+ let _log: ImportProgressEvent[] = [];
38
+ let _errorMsg = "";
39
+ let _abort: AbortController | null = null;
40
+ let _dirManual = false;
41
+
42
+ /** Reset the tab's state when the modal opens. */
43
+ export function resetImportTab() {
44
+ _url = "";
45
+ _depth = 1;
46
+ _maxPages = 20;
47
+ _aiNaming = true;
48
+ _status = "idle";
49
+ _log = [];
50
+ _errorMsg = "";
51
+ _abort = null;
52
+ _dirManual = false;
53
+ }
54
+
55
+ function slugOf(name: string): string {
56
+ return name
57
+ .toLowerCase()
58
+ .replaceAll(/[^a-z0-9]+/g, "-")
59
+ .replaceAll(/^-|-$/g, "");
60
+ }
61
+
62
+ /** True while an import stream is active (the modal disables tab switching and close). */
63
+ export function isImportRunning(): boolean {
64
+ return _status === "running";
65
+ }
66
+
67
+ /** Abort the running import (the footer's Cancel Import button). */
68
+ export function cancelImport(ctx: ImportTabCtx) {
69
+ _abort?.abort();
70
+ _abort = null;
71
+ _status = "idle";
72
+ ctx.rerender();
73
+ }
74
+
75
+ /**
76
+ * Validate the source step (the URL) before advancing to the Parameters step. Sets the inline error
77
+ * and returns false when invalid.
78
+ */
79
+ export function validateImportSource(ctx: ImportTabCtx): boolean {
80
+ let parsed: URL | null = null;
81
+ try {
82
+ parsed = new URL(_url.trim());
83
+ } catch {
84
+ parsed = null;
85
+ }
86
+ if (!parsed || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
87
+ _errorMsg = "Enter a valid URL (e.g. https://example.com)";
88
+ ctx.rerender();
89
+ return false;
90
+ }
91
+ _errorMsg = "";
92
+ return true;
93
+ }
94
+
95
+ /** Validate and kick off the import. Wired to the modal footer's primary button. */
96
+ export async function startImport(ctx: ImportTabCtx) {
97
+ const platform = getPlatform();
98
+ if (_status === "running" || !platform.importSite) {
99
+ return;
100
+ }
101
+ let parsed: URL;
102
+ try {
103
+ parsed = new URL(_url.trim());
104
+ } catch {
105
+ _errorMsg = "Enter a valid URL (e.g. https://example.com)";
106
+ ctx.rerender();
107
+ return;
108
+ }
109
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
110
+ _errorMsg = "The URL must start with http:// or https://";
111
+ ctx.rerender();
112
+ return;
113
+ }
114
+ if (!ctx.form.name.trim()) {
115
+ _errorMsg = "Project name is required";
116
+ ctx.rerender();
117
+ return;
118
+ }
119
+ if (!ctx.form.directory.trim()) {
120
+ ctx.form.directory = slugOf(ctx.form.name);
121
+ }
122
+
123
+ _status = "running";
124
+ _errorMsg = "";
125
+ _log = [];
126
+ _abort = new AbortController();
127
+ ctx.rerender();
128
+
129
+ try {
130
+ const key = getOpenAiKey();
131
+ const baseUrl = getBaseUrl();
132
+ const model = getModel();
133
+ const result = await platform.importSite(
134
+ {
135
+ url: parsed.href,
136
+ name: ctx.form.name.trim(),
137
+ directory: ctx.form.directory.trim(),
138
+ depth: _depth,
139
+ maxPages: _maxPages,
140
+ aiComponents: _aiNaming,
141
+ ...(key ? { apiKey: key } : {}),
142
+ ...(baseUrl ? { baseUrl } : {}),
143
+ ...(model ? { model } : {}),
144
+ },
145
+ (evt) => {
146
+ _log = [..._log, evt];
147
+ ctx.rerender();
148
+ requestAnimationFrame(() => {
149
+ const el = document.querySelector(".new-project-import-log");
150
+ if (el) {
151
+ el.scrollTop = el.scrollHeight;
152
+ }
153
+ });
154
+ },
155
+ _abort.signal,
156
+ );
157
+ _status = "idle";
158
+ _abort = null;
159
+ ctx.onDone(result);
160
+ } catch (error) {
161
+ if (_status !== "running") {
162
+ return; // Cancelled by the user — cancelImport already reset the tab.
163
+ }
164
+ _abort = null;
165
+ _status = "error";
166
+ _errorMsg = errorMessage(error);
167
+ ctx.rerender();
168
+ }
169
+ }
170
+
171
+ /** The footer's primary-button label for the Import tab's current state. */
172
+ export function importButtonLabel(): string {
173
+ if (_status === "running") {
174
+ return "Importing…";
175
+ }
176
+ return _status === "error" ? "Retry Import" : "Import Site";
177
+ }
178
+
179
+ function onUrlInput(ctx: ImportTabCtx) {
180
+ return (e: Event) => {
181
+ _url = (e.target as HTMLInputElement).value;
182
+ // Prefill the project name from the hostname while the user hasn't typed one.
183
+ if (!ctx.form.name.trim()) {
184
+ try {
185
+ const host = new URL(_url).hostname.replace(/^www\./, "");
186
+ if (host) {
187
+ ctx.form.name = host;
188
+ if (!_dirManual) {
189
+ ctx.form.directory = slugOf(host);
190
+ }
191
+ }
192
+ } catch {
193
+ /* Partial URL while typing — leave the name alone. */
194
+ }
195
+ }
196
+ ctx.rerender();
197
+ };
198
+ }
199
+
200
+ function renderProgress(): TemplateResult {
201
+ const latest = _log.at(-1);
202
+ return html`
203
+ <div class="new-project-import-progress">
204
+ <sp-progress-circle indeterminate size="s"></sp-progress-circle>
205
+ <span class="new-project-import-phase">${latest ? latest.phase : "starting"}</span>
206
+ <span>${latest ? latest.message : "Starting import…"}</span>
207
+ </div>
208
+ <div class="new-project-import-log">
209
+ ${_log.map(
210
+ (evt) => html`
211
+ <div class="new-project-import-log-line">
212
+ <span class="new-project-import-phase">${evt.phase}</span>
213
+ ${evt.message}
214
+ </div>
215
+ `,
216
+ )}
217
+ </div>
218
+ `;
219
+ }
220
+
221
+ /** The streaming progress view shown (in place of the Parameters step) while a run is active. */
222
+ export function renderImportProgress(): TemplateResult {
223
+ return renderProgress();
224
+ }
225
+
226
+ /** Inline error + retained log, rendered on the Parameters step after a failed run. */
227
+ export function renderImportStatus(): TemplateResult {
228
+ if (_status !== "error") {
229
+ return html``;
230
+ }
231
+ return html`
232
+ ${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
233
+ ${_log.length > 0
234
+ ? html`
235
+ <div class="new-project-import-log">
236
+ ${_log.map(
237
+ (evt) => html`
238
+ <div class="new-project-import-log-line">
239
+ <span class="new-project-import-phase">${evt.phase}</span>
240
+ ${evt.message}
241
+ </div>
242
+ `,
243
+ )}
244
+ </div>
245
+ `
246
+ : ""}
247
+ `;
248
+ }
249
+
250
+ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
251
+ if (!hasOpenAiKey()) {
252
+ return html`
253
+ <div class="new-project-tab-intro">
254
+ Importing uses an AI-guided pipeline to clone an existing site. Add an OpenAI-compatible API
255
+ key to continue.
256
+ </div>
257
+ <div class="new-project-creds">${ctx.credsForm.render()}</div>
258
+ `;
259
+ }
260
+
261
+ return html`
262
+ <div class="new-project-tab-intro">
263
+ Clone an existing site into an editable Jx project: pages, styles, assets, and components.
264
+ </div>
265
+ <label class="new-project-field">
266
+ <span class="new-project-label">Site URL *</span>
267
+ <sp-textfield
268
+ placeholder="https://example.com"
269
+ .value=${_url}
270
+ @input=${onUrlInput(ctx)}
271
+ style="width: 100%"
272
+ ></sp-textfield>
273
+ </label>
274
+ <div class="new-project-import-grid">
275
+ <label class="new-project-field">
276
+ <span class="new-project-label">Crawl Depth</span>
277
+ <sp-number-field
278
+ min="0"
279
+ max="2"
280
+ .value=${_depth}
281
+ @change=${(e: Event) => {
282
+ _depth = Math.trunc(Number((e.target as HTMLInputElement).value)) || 0;
283
+ }}
284
+ ></sp-number-field>
285
+ </label>
286
+ <label class="new-project-field">
287
+ <span class="new-project-label">Max Pages</span>
288
+ <sp-number-field
289
+ min="1"
290
+ max="100"
291
+ .value=${_maxPages}
292
+ @change=${(e: Event) => {
293
+ _maxPages = Math.trunc(Number((e.target as HTMLInputElement).value)) || 1;
294
+ }}
295
+ ></sp-number-field>
296
+ </label>
297
+ </div>
298
+ <sp-switch
299
+ ?checked=${_aiNaming}
300
+ @change=${(e: Event) => {
301
+ _aiNaming = (e.target as HTMLInputElement).checked;
302
+ }}
303
+ >
304
+ AI component naming
305
+ </sp-switch>
306
+ ${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
307
+ ${_status === "error" && _log.length > 0
308
+ ? html`
309
+ <div class="new-project-import-log">
310
+ ${_log.map(
311
+ (evt) => html`
312
+ <div class="new-project-import-log-line">
313
+ <span class="new-project-import-phase">${evt.phase}</span>
314
+ ${evt.message}
315
+ </div>
316
+ `,
317
+ )}
318
+ </div>
319
+ `
320
+ : ""}
321
+ `;
322
+ }