@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
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* New Project modal —
|
|
4
|
-
*
|
|
3
|
+
* New Project modal — a two-step wizard. Step 1 ("Start new project from:") picks the source on one
|
|
4
|
+
* of four tabs: a built-in Template, a Starter Site, an Import of an existing site, or an Agent
|
|
5
|
+
* prompt. Step 2 ("New Project Parameters") collects the project identity plus a design quickstart
|
|
6
|
+
* (colors, fonts, logo, breakpoints — a creation-time subset of the settings modal), prefilled from
|
|
7
|
+
* the chosen source and customizable before anything is written.
|
|
8
|
+
*
|
|
9
|
+
* Import and Agent are gated behind the AI credentials form until a key is stored.
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
12
|
import { html } from "lit-html";
|
|
8
13
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
9
14
|
import { openModal } from "../ui/layers";
|
|
10
15
|
import { getPlatform } from "../platform";
|
|
16
|
+
import { hasOpenAiKey } from "../services/ai-settings";
|
|
17
|
+
import { setPendingAgentPrompt } from "../services/agent-seed";
|
|
18
|
+
import { createAiCredentialsForm } from "../ui/ai-credentials-form";
|
|
19
|
+
import { PROJECT_TEMPLATES } from "./templates";
|
|
20
|
+
import { collectDesign, renderDesignFields, resetDesignFields } from "./design-fields";
|
|
21
|
+
import {
|
|
22
|
+
cancelImport,
|
|
23
|
+
importButtonLabel,
|
|
24
|
+
isImportRunning,
|
|
25
|
+
renderImportProgress,
|
|
26
|
+
renderImportSource,
|
|
27
|
+
renderImportStatus,
|
|
28
|
+
resetImportTab,
|
|
29
|
+
startImport,
|
|
30
|
+
validateImportSource,
|
|
31
|
+
} from "./import-tab";
|
|
11
32
|
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
33
|
+
import type { StarterInfo } from "../types";
|
|
34
|
+
import type { ImportTabCtx } from "./import-tab";
|
|
35
|
+
|
|
36
|
+
type NewProjectTab = "template" | "starter" | "import" | "agent";
|
|
37
|
+
type WizardStep = "source" | "params";
|
|
12
38
|
|
|
13
39
|
let _handle: ReturnType<typeof openModal> | null = null;
|
|
14
40
|
|
|
15
|
-
/**
|
|
16
|
-
* @type {{
|
|
17
|
-
* name: string;
|
|
18
|
-
* description: string;
|
|
19
|
-
* url: string;
|
|
20
|
-
* adapter: string;
|
|
21
|
-
* directory: string;
|
|
22
|
-
* }}
|
|
23
|
-
*/
|
|
24
41
|
let _form = {
|
|
25
42
|
adapter: "static",
|
|
26
43
|
description: "",
|
|
@@ -29,13 +46,44 @@ let _form = {
|
|
|
29
46
|
url: "",
|
|
30
47
|
};
|
|
31
48
|
|
|
49
|
+
let _tab: NewProjectTab = "template";
|
|
50
|
+
let _step: WizardStep = "source";
|
|
51
|
+
let _template = "blank";
|
|
52
|
+
let _starter = "";
|
|
53
|
+
let _agentPrompt = "";
|
|
54
|
+
|
|
55
|
+
/** Which source the Parameters step was last seeded for — Back/Next keeps user edits intact. */
|
|
56
|
+
let _paramsSeededFor = "";
|
|
57
|
+
|
|
32
58
|
let _error = "";
|
|
33
59
|
|
|
34
60
|
let _creating = false;
|
|
35
61
|
|
|
36
|
-
/**
|
|
62
|
+
/** Starter templates offered in the picker (empty until loaded / on platforms without starters). */
|
|
63
|
+
let _starters: StarterInfo[] = [];
|
|
64
|
+
|
|
37
65
|
let _resolve: ((result: { root: string; config: ProjectConfig } | null) => void) | null = null;
|
|
38
66
|
|
|
67
|
+
// One credentials form shared by the Import and Agent gates; lazy so the modal module can load
|
|
68
|
+
// Before a platform is registered (the form fetches models through the platform on edit).
|
|
69
|
+
let _credsForm: ReturnType<typeof createAiCredentialsForm> | null = null;
|
|
70
|
+
|
|
71
|
+
function credsForm() {
|
|
72
|
+
_credsForm ??= createAiCredentialsForm({
|
|
73
|
+
onSaved: () => {
|
|
74
|
+
if (_handle) {
|
|
75
|
+
renderModal();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
requestRender: () => {
|
|
79
|
+
if (_handle) {
|
|
80
|
+
renderModal();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
return _credsForm;
|
|
85
|
+
}
|
|
86
|
+
|
|
39
87
|
/**
|
|
40
88
|
* Open the New Project modal. Returns a promise that resolves with the created project info (or
|
|
41
89
|
* null if cancelled).
|
|
@@ -56,8 +104,38 @@ export function openNewProjectModal(): Promise<{
|
|
|
56
104
|
name: "",
|
|
57
105
|
url: "",
|
|
58
106
|
};
|
|
107
|
+
_tab = "template";
|
|
108
|
+
_step = "source";
|
|
109
|
+
_template = "blank";
|
|
110
|
+
_starter = "";
|
|
111
|
+
_agentPrompt = "";
|
|
112
|
+
_paramsSeededFor = "";
|
|
59
113
|
_error = "";
|
|
60
114
|
_creating = false;
|
|
115
|
+
_starters = [];
|
|
116
|
+
_dirDerived = true;
|
|
117
|
+
resetImportTab();
|
|
118
|
+
resetDesignFields();
|
|
119
|
+
|
|
120
|
+
// Load starter templates in the background; re-render when they arrive. Platforms without
|
|
121
|
+
// Starters leave the Starter Site tab showing its empty note.
|
|
122
|
+
const platform = getPlatform();
|
|
123
|
+
if (platform.listStarters) {
|
|
124
|
+
void platform
|
|
125
|
+
.listStarters()
|
|
126
|
+
.then((starters) => {
|
|
127
|
+
_starters = starters;
|
|
128
|
+
if (!_starter) {
|
|
129
|
+
_starter = starters[0]?.id ?? "";
|
|
130
|
+
}
|
|
131
|
+
if (_handle) {
|
|
132
|
+
renderModal();
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.catch(() => {
|
|
136
|
+
/* Non-fatal: the Starter tab keeps its empty note. */
|
|
137
|
+
});
|
|
138
|
+
}
|
|
61
139
|
|
|
62
140
|
return new Promise((resolve) => {
|
|
63
141
|
_resolve = resolve;
|
|
@@ -66,7 +144,7 @@ export function openNewProjectModal(): Promise<{
|
|
|
66
144
|
}
|
|
67
145
|
|
|
68
146
|
export function closeNewProjectModal() {
|
|
69
|
-
if (!_handle) {
|
|
147
|
+
if (!_handle || _creating || isImportRunning()) {
|
|
70
148
|
return;
|
|
71
149
|
}
|
|
72
150
|
_handle.close();
|
|
@@ -77,7 +155,53 @@ export function closeNewProjectModal() {
|
|
|
77
155
|
}
|
|
78
156
|
}
|
|
79
157
|
|
|
158
|
+
/** Close the modal and resolve its promise with a created/imported project. */
|
|
159
|
+
function finish(result: { root: string; config: ProjectConfig }) {
|
|
160
|
+
_creating = false;
|
|
161
|
+
if (_handle) {
|
|
162
|
+
_handle.close();
|
|
163
|
+
_handle = null;
|
|
164
|
+
}
|
|
165
|
+
if (_resolve) {
|
|
166
|
+
_resolve(result);
|
|
167
|
+
_resolve = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function deriveSlug(name: string): string {
|
|
172
|
+
return name
|
|
173
|
+
.toLowerCase()
|
|
174
|
+
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
175
|
+
.replaceAll(/^-|-$/g, "");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The human label of the chosen source, shown as context on the Parameters step. */
|
|
179
|
+
function sourceLabel(): string {
|
|
180
|
+
switch (_tab) {
|
|
181
|
+
case "starter": {
|
|
182
|
+
return `Starter Site · ${_starters.find((s) => s.id === _starter)?.name ?? _starter}`;
|
|
183
|
+
}
|
|
184
|
+
case "import": {
|
|
185
|
+
return "Import";
|
|
186
|
+
}
|
|
187
|
+
case "agent": {
|
|
188
|
+
return "Agent";
|
|
189
|
+
}
|
|
190
|
+
default: {
|
|
191
|
+
return `Template · ${PROJECT_TEMPLATES.find((t) => t.id === _template)?.name ?? _template}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
80
196
|
function renderModal() {
|
|
197
|
+
const platform = getPlatform();
|
|
198
|
+
const importCtx: ImportTabCtx = {
|
|
199
|
+
credsForm: credsForm(),
|
|
200
|
+
form: _form,
|
|
201
|
+
onDone: finish,
|
|
202
|
+
rerender: renderModal,
|
|
203
|
+
};
|
|
204
|
+
|
|
81
205
|
const onInput =
|
|
82
206
|
(field: "name" | "description" | "url" | "adapter" | "directory") => (e: Event) => {
|
|
83
207
|
_form[field] = (e.target as HTMLInputElement).value;
|
|
@@ -86,10 +210,7 @@ function renderModal() {
|
|
|
86
210
|
_dirDerived = true;
|
|
87
211
|
}
|
|
88
212
|
if (_dirDerived && field === "name") {
|
|
89
|
-
_form.directory = _form.name
|
|
90
|
-
.toLowerCase()
|
|
91
|
-
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
92
|
-
.replaceAll(/^-|-$/g, "");
|
|
213
|
+
_form.directory = deriveSlug(_form.name);
|
|
93
214
|
}
|
|
94
215
|
if (field === "directory") {
|
|
95
216
|
_dirDerived = false;
|
|
@@ -102,6 +223,79 @@ function renderModal() {
|
|
|
102
223
|
renderModal();
|
|
103
224
|
};
|
|
104
225
|
|
|
226
|
+
const selectTemplate = (id: string) => {
|
|
227
|
+
_template = id;
|
|
228
|
+
renderModal();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const selectStarter = (id: string) => {
|
|
232
|
+
_starter = id;
|
|
233
|
+
renderModal();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const onTabChange = (e: Event) => {
|
|
237
|
+
if (_creating || isImportRunning()) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
_tab = (e.target as HTMLElement & { selected: string }).selected as NewProjectTab;
|
|
241
|
+
_error = "";
|
|
242
|
+
renderModal();
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// ─── Step transitions ──────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
const goNext = () => {
|
|
248
|
+
if (_tab === "starter" && !_starter) {
|
|
249
|
+
_error = "Choose a starter site";
|
|
250
|
+
renderModal();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (_tab === "import" && !validateImportSource(importCtx)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (_tab === "agent" && !_agentPrompt.trim()) {
|
|
257
|
+
_error = "Describe the site you want the agent to build";
|
|
258
|
+
renderModal();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Seed the Parameters step from the chosen source — only when the source changed, so Back +
|
|
263
|
+
// Next round-trips keep the user's edits.
|
|
264
|
+
const seedKey = `${_tab}:${_template}:${_starter}`;
|
|
265
|
+
if (_paramsSeededFor !== seedKey) {
|
|
266
|
+
_paramsSeededFor = seedKey;
|
|
267
|
+
if (_tab === "starter") {
|
|
268
|
+
const meta = _starters.find((s) => s.id === _starter);
|
|
269
|
+
if (meta && !_form.description.trim()) {
|
|
270
|
+
_form.description = meta.tagline;
|
|
271
|
+
}
|
|
272
|
+
resetDesignFields({
|
|
273
|
+
...(meta?.accent ? { accent: meta.accent } : {}),
|
|
274
|
+
mediaNote: "Leave empty to keep the starter's breakpoints.",
|
|
275
|
+
});
|
|
276
|
+
} else if (_tab === "template" || _tab === "agent") {
|
|
277
|
+
const templateId = _tab === "agent" ? "blank" : _template;
|
|
278
|
+
const meta = PROJECT_TEMPLATES.find((t) => t.id === templateId);
|
|
279
|
+
resetDesignFields(meta ? { media: meta.media } : {});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
_step = "params";
|
|
284
|
+
_error = "";
|
|
285
|
+
renderModal();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const goBack = () => {
|
|
289
|
+
if (_creating || isImportRunning()) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
_step = "source";
|
|
293
|
+
_error = "";
|
|
294
|
+
renderModal();
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// ─── Submission ────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
105
299
|
const onSubmit = async () => {
|
|
106
300
|
if (!_form.name.trim()) {
|
|
107
301
|
_error = "Project name is required";
|
|
@@ -109,10 +303,7 @@ function renderModal() {
|
|
|
109
303
|
return;
|
|
110
304
|
}
|
|
111
305
|
if (!_form.directory.trim()) {
|
|
112
|
-
_form.directory = _form.name
|
|
113
|
-
.toLowerCase()
|
|
114
|
-
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
115
|
-
.replaceAll(/^-|-$/g, "");
|
|
306
|
+
_form.directory = deriveSlug(_form.name);
|
|
116
307
|
}
|
|
117
308
|
|
|
118
309
|
_creating = true;
|
|
@@ -120,17 +311,44 @@ function renderModal() {
|
|
|
120
311
|
renderModal();
|
|
121
312
|
|
|
122
313
|
try {
|
|
123
|
-
const
|
|
124
|
-
const result = await
|
|
314
|
+
const design = collectDesign();
|
|
315
|
+
const result = await getPlatform().createProject({
|
|
316
|
+
..._form,
|
|
317
|
+
...(_tab === "starter" && _starter ? { starter: _starter } : { template: _template }),
|
|
318
|
+
...(design ? { design } : {}),
|
|
319
|
+
});
|
|
320
|
+
finish(result);
|
|
321
|
+
} catch (error) {
|
|
125
322
|
_creating = false;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
323
|
+
_error = errorMessage(error);
|
|
324
|
+
renderModal();
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const onAgentSubmit = async () => {
|
|
329
|
+
if (!_form.name.trim()) {
|
|
330
|
+
_error = "Project name is required";
|
|
331
|
+
renderModal();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (!_form.directory.trim()) {
|
|
335
|
+
_form.directory = deriveSlug(_form.name);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
_creating = true;
|
|
339
|
+
_error = "";
|
|
340
|
+
renderModal();
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const design = collectDesign();
|
|
344
|
+
const result = await getPlatform().createProject({
|
|
345
|
+
..._form,
|
|
346
|
+
template: "blank",
|
|
347
|
+
...(design ? { design } : {}),
|
|
348
|
+
});
|
|
349
|
+
// The window that opens the project consumes this and hands the prompt to the assistant.
|
|
350
|
+
setPendingAgentPrompt(result.root, _agentPrompt.trim());
|
|
351
|
+
finish(result);
|
|
134
352
|
} catch (error) {
|
|
135
353
|
_creating = false;
|
|
136
354
|
_error = errorMessage(error);
|
|
@@ -138,6 +356,213 @@ function renderModal() {
|
|
|
138
356
|
}
|
|
139
357
|
};
|
|
140
358
|
|
|
359
|
+
// ─── Step 1: source selection ──────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
const templateSourceTpl = () => html`
|
|
362
|
+
<div class="new-project-templates">
|
|
363
|
+
${PROJECT_TEMPLATES.map(
|
|
364
|
+
(t) => html`
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
class="new-project-template ${_template === t.id ? "selected" : ""}"
|
|
368
|
+
@click=${() => selectTemplate(t.id)}
|
|
369
|
+
title=${t.tagline}
|
|
370
|
+
>
|
|
371
|
+
<div class="new-project-template-blank">${t.glyph}</div>
|
|
372
|
+
<div class="new-project-template-body">
|
|
373
|
+
<div class="new-project-template-name">${t.name}</div>
|
|
374
|
+
<div class="new-project-template-tag">${t.tagline}</div>
|
|
375
|
+
</div>
|
|
376
|
+
</button>
|
|
377
|
+
`,
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
`;
|
|
381
|
+
|
|
382
|
+
const starterSourceTpl = () =>
|
|
383
|
+
_starters.length > 0
|
|
384
|
+
? html`
|
|
385
|
+
<div class="new-project-templates">
|
|
386
|
+
${_starters.map(
|
|
387
|
+
(s) => html`
|
|
388
|
+
<button
|
|
389
|
+
type="button"
|
|
390
|
+
class="new-project-template ${_starter === s.id ? "selected" : ""}"
|
|
391
|
+
@click=${() => selectStarter(s.id)}
|
|
392
|
+
title=${s.description}
|
|
393
|
+
>
|
|
394
|
+
<img class="new-project-template-thumb" src=${s.thumbnail} alt="" />
|
|
395
|
+
<div class="new-project-template-body">
|
|
396
|
+
<div class="new-project-template-name">${s.name}</div>
|
|
397
|
+
<div class="new-project-template-tag">${s.tagline}</div>
|
|
398
|
+
</div>
|
|
399
|
+
</button>
|
|
400
|
+
`,
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
`
|
|
404
|
+
: html`<div class="new-project-tab-intro">No starter sites are available.</div>`;
|
|
405
|
+
|
|
406
|
+
const agentSourceTpl = () => {
|
|
407
|
+
if (!hasOpenAiKey()) {
|
|
408
|
+
return html`
|
|
409
|
+
<div class="new-project-tab-intro">
|
|
410
|
+
The agent uses your AI provider to build the site. Add an OpenAI-compatible API key to
|
|
411
|
+
continue.
|
|
412
|
+
</div>
|
|
413
|
+
<div class="new-project-creds">${credsForm().render()}</div>
|
|
414
|
+
`;
|
|
415
|
+
}
|
|
416
|
+
return html`
|
|
417
|
+
<div class="new-project-tab-intro">
|
|
418
|
+
Describe the site you want; the assistant builds it in the editor while you watch.
|
|
419
|
+
</div>
|
|
420
|
+
<label class="new-project-field">
|
|
421
|
+
<span class="new-project-label">Prompt *</span>
|
|
422
|
+
<sp-textfield
|
|
423
|
+
multiline
|
|
424
|
+
class="new-project-agent-prompt"
|
|
425
|
+
placeholder="A landing page for a small coffee roastery with a menu and contact form…"
|
|
426
|
+
.value=${_agentPrompt}
|
|
427
|
+
@input=${(e: Event) => {
|
|
428
|
+
_agentPrompt = (e.target as HTMLInputElement).value;
|
|
429
|
+
}}
|
|
430
|
+
style="width: 100%"
|
|
431
|
+
></sp-textfield>
|
|
432
|
+
</label>
|
|
433
|
+
`;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const sourceBodyTpl = () => {
|
|
437
|
+
switch (_tab) {
|
|
438
|
+
case "starter": {
|
|
439
|
+
return starterSourceTpl();
|
|
440
|
+
}
|
|
441
|
+
case "import": {
|
|
442
|
+
return renderImportSource(importCtx);
|
|
443
|
+
}
|
|
444
|
+
case "agent": {
|
|
445
|
+
return agentSourceTpl();
|
|
446
|
+
}
|
|
447
|
+
default: {
|
|
448
|
+
return templateSourceTpl();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// ─── Step 2: parameters ────────────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
const nameDirFieldsTpl = () => html`
|
|
456
|
+
<label class="new-project-field">
|
|
457
|
+
<span class="new-project-label">Project Name *</span>
|
|
458
|
+
<sp-textfield
|
|
459
|
+
placeholder="My Site"
|
|
460
|
+
.value=${_form.name}
|
|
461
|
+
@input=${onInput("name")}
|
|
462
|
+
style="width: 100%"
|
|
463
|
+
></sp-textfield>
|
|
464
|
+
</label>
|
|
465
|
+
|
|
466
|
+
<label class="new-project-field">
|
|
467
|
+
<span class="new-project-label">Directory</span>
|
|
468
|
+
<sp-textfield
|
|
469
|
+
placeholder="my-site"
|
|
470
|
+
.value=${_form.directory}
|
|
471
|
+
@input=${onInput("directory")}
|
|
472
|
+
style="width: 100%"
|
|
473
|
+
></sp-textfield>
|
|
474
|
+
</label>
|
|
475
|
+
`;
|
|
476
|
+
|
|
477
|
+
const paramsBodyTpl = () => html`
|
|
478
|
+
<div class="new-project-step-context">${sourceLabel()}</div>
|
|
479
|
+
<div class="new-project-step-heading">New Project Parameters</div>
|
|
480
|
+
${nameDirFieldsTpl()}
|
|
481
|
+
${_tab === "import"
|
|
482
|
+
? renderImportStatus()
|
|
483
|
+
: html`
|
|
484
|
+
<label class="new-project-field">
|
|
485
|
+
<span class="new-project-label">Description</span>
|
|
486
|
+
<sp-textfield
|
|
487
|
+
placeholder="A short description of the site"
|
|
488
|
+
.value=${_form.description}
|
|
489
|
+
@input=${onInput("description")}
|
|
490
|
+
style="width: 100%"
|
|
491
|
+
></sp-textfield>
|
|
492
|
+
</label>
|
|
493
|
+
|
|
494
|
+
<label class="new-project-field">
|
|
495
|
+
<span class="new-project-label">Production URL</span>
|
|
496
|
+
<sp-textfield
|
|
497
|
+
placeholder="https://example.com"
|
|
498
|
+
.value=${_form.url}
|
|
499
|
+
@input=${onInput("url")}
|
|
500
|
+
style="width: 100%"
|
|
501
|
+
></sp-textfield>
|
|
502
|
+
</label>
|
|
503
|
+
|
|
504
|
+
<label class="new-project-field">
|
|
505
|
+
<span class="new-project-label">Deployment Adapter</span>
|
|
506
|
+
<sp-picker label="Adapter" .value=${_form.adapter} @change=${onAdapterChange}>
|
|
507
|
+
<sp-menu-item value="static">Static</sp-menu-item>
|
|
508
|
+
<sp-menu-item value="cloudflare-pages">Cloudflare Pages</sp-menu-item>
|
|
509
|
+
<sp-menu-item value="node">Node</sp-menu-item>
|
|
510
|
+
<sp-menu-item value="bun">Bun</sp-menu-item>
|
|
511
|
+
</sp-picker>
|
|
512
|
+
</label>
|
|
513
|
+
|
|
514
|
+
${renderDesignFields({ rerender: renderModal })}
|
|
515
|
+
`}
|
|
516
|
+
`;
|
|
517
|
+
|
|
518
|
+
// ─── Footer ────────────────────────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
const footerTpl = () => {
|
|
521
|
+
if (isImportRunning()) {
|
|
522
|
+
return html`
|
|
523
|
+
<sp-button variant="secondary" @click=${() => cancelImport(importCtx)}>
|
|
524
|
+
Cancel Import
|
|
525
|
+
</sp-button>
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
if (_step === "source") {
|
|
529
|
+
const gated = (_tab === "import" || _tab === "agent") && !hasOpenAiKey();
|
|
530
|
+
return html`
|
|
531
|
+
<sp-button variant="secondary" @click=${closeNewProjectModal}>Cancel</sp-button>
|
|
532
|
+
${gated ? "" : html`<sp-button variant="accent" @click=${goNext}>Next</sp-button>`}
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
const primary =
|
|
536
|
+
_tab === "import"
|
|
537
|
+
? html`
|
|
538
|
+
<sp-button variant="accent" @click=${() => startImport(importCtx)}>
|
|
539
|
+
${importButtonLabel()}
|
|
540
|
+
</sp-button>
|
|
541
|
+
`
|
|
542
|
+
: _tab === "agent"
|
|
543
|
+
? html`
|
|
544
|
+
<sp-button variant="accent" ?disabled=${_creating} @click=${onAgentSubmit}>
|
|
545
|
+
${_creating ? "Creating…" : "Create & Start Agent"}
|
|
546
|
+
</sp-button>
|
|
547
|
+
`
|
|
548
|
+
: html`
|
|
549
|
+
<sp-button variant="accent" ?disabled=${_creating} @click=${onSubmit}>
|
|
550
|
+
${_creating ? "Creating…" : "Create Project"}
|
|
551
|
+
</sp-button>
|
|
552
|
+
`;
|
|
553
|
+
return html`
|
|
554
|
+
<sp-button variant="secondary" ?disabled=${_creating} @click=${goBack}>Back</sp-button>
|
|
555
|
+
${primary}
|
|
556
|
+
`;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const bodyTpl = () => {
|
|
560
|
+
if (isImportRunning()) {
|
|
561
|
+
return renderImportProgress();
|
|
562
|
+
}
|
|
563
|
+
return _step === "source" ? sourceBodyTpl() : paramsBodyTpl();
|
|
564
|
+
};
|
|
565
|
+
|
|
141
566
|
const tpl = html`
|
|
142
567
|
<sp-underlay open @close=${closeNewProjectModal}></sp-underlay>
|
|
143
568
|
<div
|
|
@@ -149,70 +574,28 @@ function renderModal() {
|
|
|
149
574
|
}}
|
|
150
575
|
>
|
|
151
576
|
<div class="new-project-modal-header">
|
|
152
|
-
<h2 class="new-project-modal-title">
|
|
577
|
+
<h2 class="new-project-modal-title">Start new project from:</h2>
|
|
153
578
|
<sp-action-button quiet size="s" @click=${closeNewProjectModal} title="Close">
|
|
154
579
|
<sp-icon-close slot="icon"></sp-icon-close>
|
|
155
580
|
</sp-action-button>
|
|
156
581
|
</div>
|
|
582
|
+
${_step === "source" && !isImportRunning()
|
|
583
|
+
? html`
|
|
584
|
+
<div class="new-project-tabs">
|
|
585
|
+
<sp-tabs selected=${_tab} quiet @change=${onTabChange}>
|
|
586
|
+
<sp-tab value="template" label="Template"></sp-tab>
|
|
587
|
+
<sp-tab value="starter" label="Starter Site"></sp-tab>
|
|
588
|
+
${platform.importSite ? html`<sp-tab value="import" label="Import"></sp-tab>` : ""}
|
|
589
|
+
<sp-tab value="agent" label="Agent"></sp-tab>
|
|
590
|
+
</sp-tabs>
|
|
591
|
+
</div>
|
|
592
|
+
`
|
|
593
|
+
: ""}
|
|
157
594
|
<div class="new-project-modal-body">
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<sp-textfield
|
|
161
|
-
placeholder="My Site"
|
|
162
|
-
.value=${_form.name}
|
|
163
|
-
@input=${onInput("name")}
|
|
164
|
-
style="width: 100%"
|
|
165
|
-
></sp-textfield>
|
|
166
|
-
</label>
|
|
167
|
-
|
|
168
|
-
<label class="new-project-field">
|
|
169
|
-
<span class="new-project-label">Directory</span>
|
|
170
|
-
<sp-textfield
|
|
171
|
-
placeholder="my-site"
|
|
172
|
-
.value=${_form.directory}
|
|
173
|
-
@input=${onInput("directory")}
|
|
174
|
-
style="width: 100%"
|
|
175
|
-
></sp-textfield>
|
|
176
|
-
</label>
|
|
177
|
-
|
|
178
|
-
<label class="new-project-field">
|
|
179
|
-
<span class="new-project-label">Description</span>
|
|
180
|
-
<sp-textfield
|
|
181
|
-
placeholder="A short description of the site"
|
|
182
|
-
.value=${_form.description}
|
|
183
|
-
@input=${onInput("description")}
|
|
184
|
-
style="width: 100%"
|
|
185
|
-
></sp-textfield>
|
|
186
|
-
</label>
|
|
187
|
-
|
|
188
|
-
<label class="new-project-field">
|
|
189
|
-
<span class="new-project-label">Production URL</span>
|
|
190
|
-
<sp-textfield
|
|
191
|
-
placeholder="https://example.com"
|
|
192
|
-
.value=${_form.url}
|
|
193
|
-
@input=${onInput("url")}
|
|
194
|
-
style="width: 100%"
|
|
195
|
-
></sp-textfield>
|
|
196
|
-
</label>
|
|
197
|
-
|
|
198
|
-
<label class="new-project-field">
|
|
199
|
-
<span class="new-project-label">Deployment Adapter</span>
|
|
200
|
-
<sp-picker label="Adapter" .value=${_form.adapter} @change=${onAdapterChange}>
|
|
201
|
-
<sp-menu-item value="static">Static</sp-menu-item>
|
|
202
|
-
<sp-menu-item value="cloudflare-pages">Cloudflare Pages</sp-menu-item>
|
|
203
|
-
<sp-menu-item value="node">Node</sp-menu-item>
|
|
204
|
-
<sp-menu-item value="bun">Bun</sp-menu-item>
|
|
205
|
-
</sp-picker>
|
|
206
|
-
</label>
|
|
207
|
-
|
|
208
|
-
${_error ? html`<div class="new-project-error">${_error}</div>` : ""}
|
|
209
|
-
</div>
|
|
210
|
-
<div class="new-project-modal-footer">
|
|
211
|
-
<sp-button variant="secondary" @click=${closeNewProjectModal}>Cancel</sp-button>
|
|
212
|
-
<sp-button variant="accent" ?disabled=${_creating} @click=${onSubmit}>
|
|
213
|
-
${_creating ? "Creating…" : "Create Project"}
|
|
214
|
-
</sp-button>
|
|
595
|
+
${bodyTpl()}
|
|
596
|
+
${_tab !== "import" && _error ? html`<div class="new-project-error">${_error}</div>` : ""}
|
|
215
597
|
</div>
|
|
598
|
+
<div class="new-project-modal-footer">${footerTpl()}</div>
|
|
216
599
|
</div>
|
|
217
600
|
`;
|
|
218
601
|
|