@jxsuite/studio 0.16.1 → 0.18.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.
@@ -11,6 +11,8 @@ import { activeTab } from "../workspace/workspace.js";
11
11
  import { getEffectiveMedia } from "../site-context.js";
12
12
  import { mediaDisplayName } from "./shared.js";
13
13
  import { view } from "../view.js";
14
+ import { getRecentProjects } from "../recent-projects.js";
15
+ import { openQuickSearch } from "./quick-search.js";
14
16
 
15
17
  /** @type {HTMLElement | null} */
16
18
  let _rootEl = null;
@@ -62,6 +64,9 @@ function tbBtnTpl(label, onClick, iconTag) {
62
64
  export function mount(rootEl, ctx) {
63
65
  _rootEl = rootEl;
64
66
  _ctx = ctx;
67
+ if (/** @type {any} */ (globalThis).__jxPlatform?.windowControls) {
68
+ rootEl.classList.add("electrobun-webkit-app-region-drag");
69
+ }
65
70
  _scope = effectScope();
66
71
  _scope.run(() => {
67
72
  effect(() => {
@@ -189,9 +194,12 @@ function toolbarTemplate() {
189
194
  { key: "design", label: "Design", iconTag: "sp-icon-artboard" },
190
195
  { key: "preview", label: "Preview", iconTag: "sp-icon-preview" },
191
196
  { key: "source", label: "Code", iconTag: "sp-icon-code" },
192
- { key: "settings", label: "Settings", iconTag: "sp-icon-gears" },
197
+ { key: "stylebook", label: "Stylebook", iconTag: "sp-icon-brush" },
193
198
  ];
194
199
 
200
+ const isProjectFile = S.documentPath === "project.json";
201
+ const allowedModes = isProjectFile ? new Set(["stylebook", "source"]) : null;
202
+
195
203
  const modeSwitcherTpl = html`
196
204
  <sp-action-group selects="single" size="s" compact>
197
205
  ${modes.map(
@@ -199,8 +207,10 @@ function toolbarTemplate() {
199
207
  <sp-action-button
200
208
  size="s"
201
209
  ?selected=${canvasMode === m.key}
210
+ ?disabled=${allowedModes && !allowedModes.has(m.key)}
202
211
  @click=${() => {
203
212
  if (canvasMode === m.key) return;
213
+ if (allowedModes && !allowedModes.has(m.key)) return;
204
214
  if (S.ui.editingFunction) {
205
215
  if (view.functionEditor) {
206
216
  view.functionEditor.dispose();
@@ -212,7 +222,7 @@ function toolbarTemplate() {
212
222
  view.panY = 0;
213
223
  /** @type {Record<string, any>} */
214
224
  const uiPatch = { editingFunction: null };
215
- if (m.key === "settings") uiPatch.rightTab = "style";
225
+ if (m.key === "stylebook") uiPatch.rightTab = "style";
216
226
  if (m.key === "manage") view.leftTab = "files";
217
227
  updateSession({ ui: uiPatch });
218
228
  _ctx.renderCanvas();
@@ -226,10 +236,60 @@ function toolbarTemplate() {
226
236
  </sp-action-group>
227
237
  `;
228
238
 
239
+ const windowControls = /** @type {any} */ (globalThis).__jxPlatform?.windowControls;
240
+ const csdTpl = windowControls
241
+ ? html`
242
+ <sp-action-group class="window-controls" size="s">
243
+ <sp-action-button
244
+ quiet
245
+ size="s"
246
+ title="Minimize"
247
+ @click=${() => windowControls.minimize()}
248
+ >
249
+ <sp-icon-remove slot="icon"></sp-icon-remove>
250
+ </sp-action-button>
251
+ <sp-action-button
252
+ quiet
253
+ size="s"
254
+ title="Maximize"
255
+ @click=${() => windowControls.maximize()}
256
+ >
257
+ <sp-icon-full-screen slot="icon"></sp-icon-full-screen>
258
+ </sp-action-button>
259
+ <sp-action-button
260
+ quiet
261
+ size="s"
262
+ title="Close"
263
+ class="csd-close"
264
+ @click=${() => windowControls.close()}
265
+ >
266
+ <sp-icon-close slot="icon"></sp-icon-close>
267
+ </sp-action-button>
268
+ </sp-action-group>
269
+ `
270
+ : nothing;
271
+
272
+ const recentProjects = getRecentProjects();
273
+ const recentProjectsTpl = recentProjects.length
274
+ ? html`
275
+ <overlay-trigger placement="bottom-start">
276
+ <sp-action-button size="s" slot="trigger" title="Recent projects">
277
+ <sp-icon-chevron-down slot="icon"></sp-icon-chevron-down>
278
+ </sp-action-button>
279
+ <sp-popover slot="click-content" tip>
280
+ <sp-menu @change=${(/** @type {any} */ e) => _ctx.openRecentProject(e.target.value)}>
281
+ ${recentProjects.map(
282
+ (p) => html`<sp-menu-item value=${p.root}>${p.name}</sp-menu-item>`,
283
+ )}
284
+ </sp-menu>
285
+ </sp-popover>
286
+ </overlay-trigger>
287
+ `
288
+ : nothing;
289
+
229
290
  return html`
230
291
  <sp-action-group compact size="s">
231
- ${tbBtnTpl("Open Project", _ctx.openProject, "sp-icon-folder-open")}
232
- ${tbBtnTpl("Open File", _ctx.openFile, "sp-icon-document")}
292
+ ${tbBtnTpl("Open Project", _ctx.openProject, "sp-icon-folder-open")} ${recentProjectsTpl}
233
293
  ${tbBtnTpl("Save", _ctx.saveFile, "sp-icon-save-floppy")}
234
294
  </sp-action-group>
235
295
  <sp-action-group compact size="s">
@@ -237,17 +297,11 @@ function toolbarTemplate() {
237
297
  ${tbBtnTpl("Redo", () => tabRedo(activeTab.value), "sp-icon-redo")}
238
298
  </sp-action-group>
239
299
  <div class="tb-spacer"></div>
240
- ${S.documentPath
241
- ? html`<span class="tb-file-title" title=${S.documentPath}
242
- >${S.documentPath}${S.dirty ? html`<span class="tb-dirty">●</span>` : nothing}</span
243
- >`
244
- : S.fileHandle
245
- ? html`<span class="tb-file-title"
246
- >${S.fileHandle.name}${S.dirty ? html`<span class="tb-dirty">●</span>` : nothing}</span
247
- >`
248
- : nothing}
249
- ${breadcrumbTpl}
300
+ <sp-action-button class="tb-search-trigger" size="s" quiet @click=${openQuickSearch}>
301
+ <sp-icon-search slot="icon"></sp-icon-search>
302
+ <span class="tb-search-label">Search files… <kbd>⌘P</kbd></span>
303
+ </sp-action-button>
250
304
  <div class="tb-spacer"></div>
251
- ${togglesTpl} ${modeSwitcherTpl}
305
+ ${breadcrumbTpl} ${togglesTpl} ${modeSwitcherTpl} ${csdTpl}
252
306
  `;
253
307
  }
@@ -315,6 +315,18 @@ export function createDevServerPlatform() {
315
315
  return null;
316
316
  },
317
317
 
318
+ /** @param {string} query */
319
+ async searchFiles(query) {
320
+ const glob = `**/*${query}*.{json,md}`;
321
+ const res = await fetch(
322
+ `/__studio/files?dir=${encodeURIComponent(serverPath("."))}&glob=${encodeURIComponent(glob)}`,
323
+ );
324
+ if (!res.ok) return [];
325
+ const entries = await res.json();
326
+ for (const e of entries) e.path = stripRoot(e.path);
327
+ return entries;
328
+ },
329
+
318
330
  // ─── Plugin schema ────────────────────────────────────────────────────
319
331
 
320
332
  /**
@@ -0,0 +1,57 @@
1
+ const STORAGE_KEY = "jx-studio-recent-projects";
2
+ const FILES_STORAGE_KEY = "jx-studio-recent-files";
3
+ const MAX_RECENT = 8;
4
+ const MAX_RECENT_FILES = 10;
5
+
6
+ /**
7
+ * @typedef {{ name: string; root: string; timestamp: number }} RecentProject
8
+ *
9
+ * @typedef {{ path: string; name: string; timestamp: number }} RecentFile
10
+ */
11
+
12
+ /** @returns {RecentProject[]} */
13
+ export function getRecentProjects() {
14
+ try {
15
+ const raw = localStorage.getItem(STORAGE_KEY);
16
+ if (!raw) return [];
17
+ return /** @type {RecentProject[]} */ (JSON.parse(raw)).sort(
18
+ (a, b) => b.timestamp - a.timestamp,
19
+ );
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ /**
26
+ * @param {string} name
27
+ * @param {string} root
28
+ */
29
+ export function addRecentProject(name, root) {
30
+ const projects = getRecentProjects().filter((p) => p.root !== root);
31
+ projects.unshift({ name, root, timestamp: Date.now() });
32
+ if (projects.length > MAX_RECENT) projects.length = MAX_RECENT;
33
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(projects));
34
+ }
35
+
36
+ export function clearRecentProjects() {
37
+ localStorage.removeItem(STORAGE_KEY);
38
+ }
39
+
40
+ /** @returns {RecentFile[]} */
41
+ export function getRecentFiles() {
42
+ try {
43
+ const raw = localStorage.getItem(FILES_STORAGE_KEY);
44
+ if (!raw) return [];
45
+ return /** @type {RecentFile[]} */ (JSON.parse(raw)).sort((a, b) => b.timestamp - a.timestamp);
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ /** @param {{ path: string; name: string }} file */
52
+ export function trackRecentFile(file) {
53
+ const recent = getRecentFiles().filter((f) => f.path !== file.path);
54
+ recent.unshift({ path: file.path, name: file.name, timestamp: Date.now() });
55
+ if (recent.length > MAX_RECENT_FILES) recent.length = MAX_RECENT_FILES;
56
+ localStorage.setItem(FILES_STORAGE_KEY, JSON.stringify(recent));
57
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * CSS Variables editor — form-based editor for managing design tokens. Colors and fonts are NOT
3
+ * media-aware; sizes/spacing are media-aware.
4
+ */
5
+
6
+ import { html, render as litRender, nothing } from "lit-html";
7
+ import { ref } from "lit-html/directives/ref.js";
8
+ import { projectState } from "../store.js";
9
+ import { updateSiteConfig } from "../site-context.js";
10
+ import { getEffectiveMedia } from "../site-context.js";
11
+ import { friendlyNameToVar, varDisplayName } from "../utils/studio-utils.js";
12
+
13
+ /** @param {HTMLElement} container */
14
+ export function renderCssVarsEditor(container) {
15
+ const config = projectState.projectConfig || {};
16
+ const rootStyle = config.style || {};
17
+ const media = getEffectiveMedia(config.$media);
18
+
19
+ /**
20
+ * @type {{
21
+ * color: [string, any][];
22
+ * font: [string, any][];
23
+ * size: [string, any][];
24
+ * other: [string, any][];
25
+ * }}
26
+ */
27
+ const groups = { color: [], font: [], size: [], other: [] };
28
+ for (const [k, v] of Object.entries(rootStyle)) {
29
+ if (!k.startsWith("--")) continue;
30
+ if (typeof v !== "string" && typeof v !== "number") continue;
31
+ if (k.startsWith("--color")) groups.color.push([k, v]);
32
+ else if (k.startsWith("--font")) groups.font.push([k, v]);
33
+ else if (k.startsWith("--size") || k.startsWith("--spacing") || k.startsWith("--radius"))
34
+ groups.size.push([k, v]);
35
+ else groups.other.push([k, v]);
36
+ }
37
+
38
+ const mediaNames = media ? Object.keys(media).filter((m) => m !== "--") : [];
39
+
40
+ const save = () => {
41
+ updateSiteConfig({ style: { ...rootStyle } });
42
+ };
43
+
44
+ const updateVar = (/** @type {string} */ name, /** @type {string} */ val) => {
45
+ rootStyle[name] = val;
46
+ save();
47
+ };
48
+
49
+ const deleteVar = (/** @type {string} */ name) => {
50
+ delete rootStyle[name];
51
+ save();
52
+ renderCssVarsEditor(container);
53
+ };
54
+
55
+ const addVar = (
56
+ /** @type {string} */ prefix,
57
+ /** @type {string} */ friendlyName,
58
+ /** @type {string} */ val,
59
+ ) => {
60
+ const varName = friendlyNameToVar(friendlyName, prefix);
61
+ if (!varName || !val) return;
62
+ rootStyle[varName] = val;
63
+ save();
64
+ renderCssVarsEditor(container);
65
+ };
66
+
67
+ const tpl = html`
68
+ <div class="settings-section">
69
+ <h3 class="settings-section-title">CSS Variables</h3>
70
+
71
+ ${renderColorSection(groups.color, updateVar, deleteVar, addVar)}
72
+ ${renderFontSection(groups.font, updateVar, deleteVar, addVar)}
73
+ ${renderSizeSection(groups.size, updateVar, deleteVar, addVar, rootStyle, mediaNames)}
74
+ ${groups.other.length > 0
75
+ ? renderOtherSection(groups.other, updateVar, deleteVar, addVar, rootStyle, mediaNames)
76
+ : nothing}
77
+ </div>
78
+ `;
79
+
80
+ litRender(tpl, container);
81
+ }
82
+
83
+ /**
84
+ * @param {[string, any][]} vars
85
+ * @param {Function} updateVar
86
+ * @param {Function} deleteVar
87
+ * @param {Function} addVar
88
+ */
89
+ function renderColorSection(vars, updateVar, deleteVar, addVar) {
90
+ return html`
91
+ <div class="css-vars-group">
92
+ <h4 class="css-vars-group-title">Colors</h4>
93
+ ${vars.map(
94
+ ([name, val]) => html`
95
+ <div class="css-var-row">
96
+ <div class="css-var-swatch" style="background:${val}">
97
+ <input
98
+ type="color"
99
+ .value=${val && val.startsWith("#") ? val : "#007acc"}
100
+ @input=${(/** @type {any} */ e) => updateVar(name, e.target.value)}
101
+ />
102
+ </div>
103
+ <span class="css-var-name">${varDisplayName(name, "--color-")}</span>
104
+ <sp-textfield
105
+ size="s"
106
+ .value=${String(val)}
107
+ @change=${(/** @type {any} */ e) => updateVar(name, e.target.value)}
108
+ style="flex:1;max-width:160px"
109
+ ></sp-textfield>
110
+ <sp-action-button quiet size="s" @click=${() => deleteVar(name)}>
111
+ <sp-icon-delete slot="icon"></sp-icon-delete>
112
+ </sp-action-button>
113
+ </div>
114
+ `,
115
+ )}
116
+ ${renderAddRow("--color-", "Primary Blue", "#007acc", addVar)}
117
+ </div>
118
+ `;
119
+ }
120
+
121
+ /**
122
+ * @param {[string, any][]} vars
123
+ * @param {Function} updateVar
124
+ * @param {Function} deleteVar
125
+ * @param {Function} addVar
126
+ */
127
+ function renderFontSection(vars, updateVar, deleteVar, addVar) {
128
+ return html`
129
+ <div class="css-vars-group">
130
+ <h4 class="css-vars-group-title">Fonts</h4>
131
+ ${vars.map(
132
+ ([name, val]) => html`
133
+ <div class="css-var-row">
134
+ <span class="css-var-name">${varDisplayName(name, "--font-")}</span>
135
+ <sp-textfield
136
+ size="s"
137
+ .value=${String(val)}
138
+ @change=${(/** @type {any} */ e) => updateVar(name, e.target.value)}
139
+ style="flex:1"
140
+ ></sp-textfield>
141
+ <sp-action-button quiet size="s" @click=${() => deleteVar(name)}>
142
+ <sp-icon-delete slot="icon"></sp-icon-delete>
143
+ </sp-action-button>
144
+ </div>
145
+ <div class="css-var-font-preview" style="font-family:${val}">
146
+ The quick brown fox jumps over the lazy dog
147
+ </div>
148
+ `,
149
+ )}
150
+ ${renderAddRow("--font-", "Body Serif", "'Georgia', serif", addVar)}
151
+ </div>
152
+ `;
153
+ }
154
+
155
+ /**
156
+ * @param {[string, any][]} vars
157
+ * @param {Function} updateVar
158
+ * @param {Function} deleteVar
159
+ * @param {Function} addVar
160
+ * @param {any} rootStyle
161
+ * @param {string[]} mediaNames
162
+ */
163
+ function renderSizeSection(vars, updateVar, deleteVar, addVar, rootStyle, mediaNames) {
164
+ return html`
165
+ <div class="css-vars-group">
166
+ <h4 class="css-vars-group-title">Sizes &amp; Spacing</h4>
167
+ ${vars.map(
168
+ ([name, val]) => html`
169
+ <div class="css-var-row">
170
+ <span class="css-var-name"
171
+ >${varDisplayName(name, "--size-") ||
172
+ varDisplayName(name, "--spacing-") ||
173
+ varDisplayName(name, "--radius-") ||
174
+ name}</span
175
+ >
176
+ <sp-textfield
177
+ size="s"
178
+ .value=${String(val)}
179
+ @change=${(/** @type {any} */ e) => updateVar(name, e.target.value)}
180
+ style="max-width:120px"
181
+ ></sp-textfield>
182
+ <sp-action-button quiet size="s" @click=${() => deleteVar(name)}>
183
+ <sp-icon-delete slot="icon"></sp-icon-delete>
184
+ </sp-action-button>
185
+ </div>
186
+ ${mediaNames.length > 0 ? renderMediaOverrides(name, rootStyle, mediaNames) : nothing}
187
+ `,
188
+ )}
189
+ ${renderAddRow("--size-", "Spacing Large", "32px", addVar)}
190
+ </div>
191
+ `;
192
+ }
193
+
194
+ /**
195
+ * @param {[string, any][]} vars
196
+ * @param {Function} updateVar
197
+ * @param {Function} deleteVar
198
+ * @param {Function} addVar
199
+ * @param {any} rootStyle
200
+ * @param {string[]} mediaNames
201
+ */
202
+ function renderOtherSection(vars, updateVar, deleteVar, addVar, rootStyle, mediaNames) {
203
+ return html`
204
+ <div class="css-vars-group">
205
+ <h4 class="css-vars-group-title">Other</h4>
206
+ ${vars.map(
207
+ ([name, val]) => html`
208
+ <div class="css-var-row">
209
+ <span class="css-var-name">${name}</span>
210
+ <sp-textfield
211
+ size="s"
212
+ .value=${String(val)}
213
+ @change=${(/** @type {any} */ e) => updateVar(name, e.target.value)}
214
+ style="flex:1"
215
+ ></sp-textfield>
216
+ <sp-action-button quiet size="s" @click=${() => deleteVar(name)}>
217
+ <sp-icon-delete slot="icon"></sp-icon-delete>
218
+ </sp-action-button>
219
+ </div>
220
+ ${mediaNames.length > 0 ? renderMediaOverrides(name, rootStyle, mediaNames) : nothing}
221
+ `,
222
+ )}
223
+ ${renderAddRow("--", "Custom Var", "value", addVar)}
224
+ </div>
225
+ `;
226
+ }
227
+
228
+ /**
229
+ * @param {string} varName
230
+ * @param {any} rootStyle
231
+ * @param {string[]} mediaNames
232
+ */
233
+ function renderMediaOverrides(varName, rootStyle, mediaNames) {
234
+ const overrides = [];
235
+ for (const mediaName of mediaNames) {
236
+ const mediaKey = `@${mediaName}`;
237
+ const mediaBlock = rootStyle[mediaKey];
238
+ if (mediaBlock && typeof mediaBlock === "object" && mediaBlock[varName]) {
239
+ overrides.push({ mediaName, value: mediaBlock[varName] });
240
+ }
241
+ }
242
+
243
+ if (overrides.length === 0) return nothing;
244
+
245
+ return html`
246
+ <div class="css-var-media-overrides">
247
+ ${overrides.map(
248
+ (o) => html`
249
+ <div class="css-var-media-row">
250
+ <span class="css-var-media-label">@${o.mediaName}</span>
251
+ <sp-textfield
252
+ size="s"
253
+ .value=${String(o.value)}
254
+ @change=${(/** @type {any} */ e) => {
255
+ if (!rootStyle[`@${o.mediaName}`]) rootStyle[`@${o.mediaName}`] = {};
256
+ rootStyle[`@${o.mediaName}`][varName] = e.target.value;
257
+ updateSiteConfig({ style: { ...rootStyle } });
258
+ }}
259
+ style="max-width:120px"
260
+ ></sp-textfield>
261
+ </div>
262
+ `,
263
+ )}
264
+ </div>
265
+ `;
266
+ }
267
+
268
+ /**
269
+ * @param {string} prefix
270
+ * @param {string} placeholder
271
+ * @param {string} valuePlaceholder
272
+ * @param {Function} addVar
273
+ */
274
+ function renderAddRow(prefix, placeholder, valuePlaceholder, addVar) {
275
+ /** @type {any} */
276
+ let nameEl = null;
277
+ /** @type {any} */
278
+ let valEl = null;
279
+
280
+ return html`
281
+ <div class="css-var-add-row">
282
+ <sp-textfield
283
+ size="s"
284
+ placeholder=${placeholder}
285
+ ${ref((el) => {
286
+ if (el) nameEl = el;
287
+ })}
288
+ ></sp-textfield>
289
+ <sp-textfield
290
+ size="s"
291
+ placeholder=${valuePlaceholder}
292
+ ${ref((el) => {
293
+ if (el) valEl = el;
294
+ })}
295
+ ></sp-textfield>
296
+ <sp-action-button
297
+ size="s"
298
+ @click=${() => {
299
+ if (nameEl && valEl) {
300
+ addVar(prefix, nameEl.value, valEl.value);
301
+ }
302
+ }}
303
+ >Add</sp-action-button
304
+ >
305
+ </div>
306
+ `;
307
+ }
@@ -0,0 +1,94 @@
1
+ /** General settings section — favicon, platform adapter, and other site-wide config. */
2
+
3
+ import { html, render as litRender, nothing } from "lit-html";
4
+ import { projectState } from "../store.js";
5
+ import { updateSiteConfig } from "../site-context.js";
6
+ import { getPlatform } from "../platform.js";
7
+ import { openFileInTab } from "../files/files.js";
8
+ import { closeSettingsModal } from "./settings-modal.js";
9
+
10
+ /** @param {HTMLElement} container */
11
+ export function renderGeneralSettings(container) {
12
+ const config = projectState.projectConfig || {};
13
+
14
+ const onFaviconUpload = async () => {
15
+ const input = document.createElement("input");
16
+ input.type = "file";
17
+ input.accept = "image/*,.ico,.svg";
18
+ input.onchange = async () => {
19
+ const file = input.files?.[0];
20
+ if (!file) return;
21
+ const platform = getPlatform();
22
+ await platform.uploadFile("public/favicon.ico", file);
23
+ await updateSiteConfig({ favicon: "/favicon.ico" });
24
+ renderGeneralSettings(container);
25
+ };
26
+ input.click();
27
+ };
28
+
29
+ const onAdapterChange = (/** @type {any} */ e) => {
30
+ updateSiteConfig({ adapter: e.target.value });
31
+ };
32
+
33
+ const onEditGlobalStyles = () => {
34
+ closeSettingsModal();
35
+ openFileInTab("project.json");
36
+ };
37
+
38
+ const currentFavicon = config.favicon;
39
+
40
+ const tpl = html`
41
+ <div class="settings-section">
42
+ <h3 class="settings-section-title">General</h3>
43
+
44
+ <div class="settings-field">
45
+ <label class="settings-field-label">Favicon</label>
46
+ <p class="settings-field-desc">Upload an image to use as the site favicon.</p>
47
+ <div style="display:flex;align-items:center;gap:12px">
48
+ ${currentFavicon
49
+ ? html`<img
50
+ src=${currentFavicon}
51
+ alt="Current favicon"
52
+ style="width:32px;height:32px;object-fit:contain;border:1px solid var(--border);border-radius:4px;padding:2px"
53
+ />`
54
+ : html`<div
55
+ style="width:32px;height:32px;border:1px dashed var(--border);border-radius:4px;display:flex;align-items:center;justify-content:center;color:var(--fg-dim);font-size:11px"
56
+ >
57
+
58
+ </div>`}
59
+ <sp-action-button size="s" @click=${onFaviconUpload}> Upload Favicon </sp-action-button>
60
+ ${currentFavicon
61
+ ? html`<span style="font-size:11px;color:var(--fg-dim)">${currentFavicon}</span>`
62
+ : nothing}
63
+ </div>
64
+ </div>
65
+
66
+ <div class="settings-field">
67
+ <label class="settings-field-label">Platform Adapter</label>
68
+ <p class="settings-field-desc">Build adapter for deployment target.</p>
69
+ <sp-picker
70
+ size="s"
71
+ label="Platform Adapter"
72
+ .value=${config.adapter || "static"}
73
+ @change=${onAdapterChange}
74
+ >
75
+ <sp-menu-item value="static">Static</sp-menu-item>
76
+ <sp-menu-item value="bun">Bun</sp-menu-item>
77
+ <sp-menu-item value="node">Node</sp-menu-item>
78
+ <sp-menu-item value="cloudflare-workers">Cloudflare Workers</sp-menu-item>
79
+ <sp-menu-item value="cloudflare-pages">Cloudflare Pages</sp-menu-item>
80
+ </sp-picker>
81
+ </div>
82
+
83
+ <div class="settings-field">
84
+ <label class="settings-field-label">Global Styles</label>
85
+ <p class="settings-field-desc">Edit default element styles that apply across all pages.</p>
86
+ <sp-action-button size="s" @click=${onEditGlobalStyles}>
87
+ Edit Global Styles
88
+ </sp-action-button>
89
+ </div>
90
+ </div>
91
+ `;
92
+
93
+ litRender(tpl, container);
94
+ }