@jxsuite/studio 0.34.0 → 0.36.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 (41) hide show
  1. package/dist/iframe-entry.js +9 -1
  2. package/dist/iframe-entry.js.map +3 -3
  3. package/dist/studio.js +18382 -3686
  4. package/dist/studio.js.map +265 -26
  5. package/package.json +9 -6
  6. package/src/files/files.ts +3 -0
  7. package/src/new-project/design-fields.ts +260 -0
  8. package/src/new-project/import-tab.ts +322 -0
  9. package/src/new-project/new-project-modal.ts +472 -89
  10. package/src/new-project/templates.ts +61 -0
  11. package/src/packages/ensure-deps.ts +6 -0
  12. package/src/packages/pull-package-sync.ts +339 -0
  13. package/src/panels/ai-chat/attached-context.ts +49 -0
  14. package/src/panels/ai-chat/chat-markdown.ts +38 -0
  15. package/src/panels/ai-chat/chat-view.ts +250 -0
  16. package/src/panels/ai-chat/composer.ts +315 -0
  17. package/src/panels/ai-chat/sessions-view.ts +98 -0
  18. package/src/panels/ai-panel.ts +214 -458
  19. package/src/panels/drag-ghost.ts +1 -1
  20. package/src/panels/git-panel.ts +16 -1
  21. package/src/panels/right-panel.ts +21 -5
  22. package/src/panels/toolbar.ts +2 -0
  23. package/src/panels/welcome-screen.ts +26 -0
  24. package/src/platforms/cloud.ts +774 -0
  25. package/src/platforms/devserver.ts +100 -1
  26. package/src/project-list.ts +38 -0
  27. package/src/publish/pages-service.ts +186 -0
  28. package/src/publish/publish-panel.ts +360 -0
  29. package/src/services/agent-seed.ts +91 -0
  30. package/src/services/ai-models.ts +93 -0
  31. package/src/services/ai-session-store.ts +250 -0
  32. package/src/services/ai-settings.ts +5 -0
  33. package/src/services/automation.ts +140 -0
  34. package/src/services/cf-settings.ts +58 -0
  35. package/src/services/document-assistant.ts +98 -38
  36. package/src/services/import-client.ts +134 -0
  37. package/src/services/settings-store.ts +99 -0
  38. package/src/studio.ts +33 -0
  39. package/src/types.ts +130 -133
  40. package/src/ui/ai-credentials-form.ts +205 -0
  41. package/src/ui/spectrum.ts +8 -0
@@ -1,26 +1,43 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * New Project modal — guides the user through creating a new Jx project. Fields mirror the CLI
4
- * scaffolder: name, description, url, adapter.
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
- /** @type {((result: { root: string; config: object } | null) => void) | null} */
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 platform = getPlatform();
124
- const result = await platform.createProject(_form);
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
- if (_handle) {
127
- _handle.close();
128
- _handle = null;
129
- }
130
- if (_resolve) {
131
- _resolve(result);
132
- _resolve = null;
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">New Project</h2>
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
- <label class="new-project-field">
159
- <span class="new-project-label">Project Name *</span>
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