vibespot 0.7.1 → 0.9.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/index.js +160 -99
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/ui/chat.js +175 -7
- package/ui/index.html +58 -31
- package/ui/settings.js +456 -238
- package/ui/setup.js +205 -18
- package/ui/styles.css +343 -0
- package/ui/upload-panel.js +99 -35
package/ui/settings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Settings panel —
|
|
2
|
+
* Settings panel — tabbed layout with AI, HubSpot, GitHub, vibeSpot tabs.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
|
|
9
9
|
let settingsData = null;
|
|
10
|
+
let activeTab = "ai";
|
|
10
11
|
const activePolls = {};
|
|
11
12
|
|
|
12
13
|
const ENGINE_LABELS = {
|
|
@@ -23,7 +24,6 @@ const ENGINE_LABELS = {
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
|
|
25
26
|
function openSettings() {
|
|
26
|
-
// Close menu if open
|
|
27
27
|
if (typeof closeMenu === "function") closeMenu();
|
|
28
28
|
document.getElementById("settings-overlay").classList.remove("hidden");
|
|
29
29
|
refreshSettings();
|
|
@@ -37,6 +37,22 @@ function closeSettings() {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Tab switching
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function initTabs() {
|
|
45
|
+
const tabs = document.querySelectorAll("#settings-tabs .settings__tab");
|
|
46
|
+
tabs.forEach((tab) => {
|
|
47
|
+
tab.addEventListener("click", () => {
|
|
48
|
+
tabs.forEach((t) => t.classList.remove("active"));
|
|
49
|
+
tab.classList.add("active");
|
|
50
|
+
activeTab = tab.dataset.tab;
|
|
51
|
+
if (settingsData) renderSettings(settingsData);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
// ---------------------------------------------------------------------------
|
|
41
57
|
// Fetch and render
|
|
42
58
|
// ---------------------------------------------------------------------------
|
|
@@ -56,16 +72,29 @@ async function refreshSettings() {
|
|
|
56
72
|
|
|
57
73
|
function renderSettings(data) {
|
|
58
74
|
const body = document.getElementById("settings-body");
|
|
75
|
+
body.innerHTML = "";
|
|
76
|
+
|
|
77
|
+
switch (activeTab) {
|
|
78
|
+
case "ai": renderAITab(body, data); break;
|
|
79
|
+
case "hubspot": renderHubSpotTab(body, data); break;
|
|
80
|
+
case "github": renderGitHubTab(body, data); break;
|
|
81
|
+
case "vibespot": renderVibeSpotTab(body, data); break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// AI Tab
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
function renderAITab(body, data) {
|
|
59
90
|
const env = data.environment;
|
|
60
91
|
const config = data.config;
|
|
61
92
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
aiSection.appendChild(sectionTitle("AI Engine"));
|
|
93
|
+
// Engine selector
|
|
94
|
+
const section = el("section", "settings__section");
|
|
95
|
+
section.appendChild(sectionTitle("Engine"));
|
|
96
|
+
section.appendChild(desc("Choose which AI engine generates your HubSpot modules. API engines need an API key. CLI engines need the tool installed on your system."));
|
|
67
97
|
|
|
68
|
-
// Engine selector pills
|
|
69
98
|
const selectEl = el("div", "settings__engine-select");
|
|
70
99
|
const allEngines = [
|
|
71
100
|
{ id: "claude-code", label: "Claude Code" },
|
|
@@ -82,15 +111,12 @@ function renderSettings(data) {
|
|
|
82
111
|
btn.textContent = eng.label;
|
|
83
112
|
btn.disabled = !available;
|
|
84
113
|
if (config.aiEngine === eng.id) btn.classList.add("active");
|
|
85
|
-
if (!config.aiEngine && available && env.availableEngines[0] === eng.id) {
|
|
86
|
-
// Highlight first available if none selected
|
|
87
|
-
}
|
|
88
114
|
btn.addEventListener("click", () => setEngine(eng.id));
|
|
89
115
|
selectEl.appendChild(btn);
|
|
90
116
|
}
|
|
91
|
-
|
|
117
|
+
section.appendChild(selectEl);
|
|
92
118
|
|
|
93
|
-
// Model selector
|
|
119
|
+
// Model selector
|
|
94
120
|
const activeEngine = config.aiEngine || (env.availableEngines.length > 0 ? env.availableEngines[0] : null);
|
|
95
121
|
if (activeEngine) {
|
|
96
122
|
const modelRow = el("div", "settings__model-row");
|
|
@@ -110,7 +136,6 @@ function renderSettings(data) {
|
|
|
110
136
|
modelSelect.appendChild(opt);
|
|
111
137
|
}
|
|
112
138
|
|
|
113
|
-
// Custom model option
|
|
114
139
|
const customOpt = document.createElement("option");
|
|
115
140
|
customOpt.value = "__custom__";
|
|
116
141
|
customOpt.textContent = "Custom...";
|
|
@@ -130,24 +155,16 @@ function renderSettings(data) {
|
|
|
130
155
|
});
|
|
131
156
|
|
|
132
157
|
modelRow.appendChild(modelSelect);
|
|
133
|
-
|
|
158
|
+
section.appendChild(modelRow);
|
|
134
159
|
}
|
|
135
160
|
|
|
136
|
-
|
|
137
|
-
aiSection.appendChild(subsectionTitle("CLI Tools"));
|
|
138
|
-
const cliTools = [
|
|
139
|
-
{ key: "claudeCode", name: "Claude Code", installId: "claude", url: "https://claude.ai/code" },
|
|
140
|
-
{ key: "geminiCli", name: "Gemini CLI", installId: "gemini", url: "https://github.com/google-gemini/gemini-cli" },
|
|
141
|
-
{ key: "codexCli", name: "Codex CLI", installId: "codex", url: "https://github.com/openai/codex" },
|
|
142
|
-
];
|
|
161
|
+
body.appendChild(section);
|
|
143
162
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
163
|
+
// API Keys section
|
|
164
|
+
const keysSection = el("section", "settings__section");
|
|
165
|
+
keysSection.appendChild(sectionTitle("API Keys"));
|
|
166
|
+
keysSection.appendChild(desc("API keys are stored locally in ~/.vibespot/config.json and never sent anywhere except the provider\u2019s API."));
|
|
148
167
|
|
|
149
|
-
// API keys subsection
|
|
150
|
-
aiSection.appendChild(subsectionTitle("API Keys"));
|
|
151
168
|
const providers = [
|
|
152
169
|
{ key: "anthropic", name: "Anthropic", placeholder: "sk-ant-api03-..." },
|
|
153
170
|
{ key: "openai", name: "OpenAI", placeholder: "sk-..." },
|
|
@@ -156,253 +173,321 @@ function renderSettings(data) {
|
|
|
156
173
|
|
|
157
174
|
for (const prov of providers) {
|
|
158
175
|
const keyInfo = env.apiKeys[prov.key];
|
|
159
|
-
|
|
176
|
+
keysSection.appendChild(createApiKeyCard(prov.key, prov.name, prov.placeholder, keyInfo));
|
|
160
177
|
}
|
|
178
|
+
body.appendChild(keysSection);
|
|
161
179
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
hsSection.appendChild(sectionTitle("HubSpot"));
|
|
167
|
-
hsSection.appendChild(createHubSpotCard(env.tools.hubspot));
|
|
168
|
-
body.appendChild(hsSection);
|
|
169
|
-
|
|
170
|
-
// --- GitHub section ---
|
|
171
|
-
const ghSection = el("section", "settings__section");
|
|
172
|
-
ghSection.appendChild(sectionTitle("GitHub"));
|
|
173
|
-
ghSection.appendChild(createGitHubCard(env.tools.github));
|
|
174
|
-
body.appendChild(ghSection);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
// Tool card
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
|
|
181
|
-
function createToolCard(name, info, installId, url) {
|
|
182
|
-
const card = el("div", "settings__card");
|
|
180
|
+
// CLI Tools section with toggles
|
|
181
|
+
const cliSection = el("section", "settings__section");
|
|
182
|
+
cliSection.appendChild(sectionTitle("CLI Tools"));
|
|
183
|
+
cliSection.appendChild(desc("Enable CLI tools you have installed. Install status is only checked when you toggle a tool on, so disabled tools add zero overhead."));
|
|
183
184
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
row.appendChild(dot("muted"));
|
|
190
|
-
}
|
|
185
|
+
const cliTools = [
|
|
186
|
+
{ key: "claudeCode", id: "claude-code", name: "Claude Code", installId: "claude", url: "https://claude.ai/code" },
|
|
187
|
+
{ key: "geminiCli", id: "gemini-cli", name: "Gemini CLI", installId: "gemini", url: "https://github.com/google-gemini/gemini-cli" },
|
|
188
|
+
{ key: "codexCli", id: "codex-cli", name: "Codex CLI", installId: "codex", url: "https://github.com/openai/codex" },
|
|
189
|
+
];
|
|
191
190
|
|
|
192
|
-
const
|
|
193
|
-
label.textContent = name;
|
|
194
|
-
row.appendChild(label);
|
|
191
|
+
const enabledTools = config.enabledCLITools || [];
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
row
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
193
|
+
for (const tool of cliTools) {
|
|
194
|
+
const enabled = enabledTools.includes(tool.id);
|
|
195
|
+
const info = env.tools[tool.key];
|
|
196
|
+
const row = el("div", "settings__card");
|
|
197
|
+
|
|
198
|
+
const toggleRow = el("div", "settings__toggle-row");
|
|
199
|
+
const labelWrap = el("div", "");
|
|
200
|
+
const label = el("div", "settings__toggle-label");
|
|
201
|
+
label.textContent = tool.name;
|
|
202
|
+
labelWrap.appendChild(label);
|
|
203
|
+
|
|
204
|
+
if (enabled && info.found) {
|
|
205
|
+
const sub = el("div", "settings__toggle-label-sub");
|
|
206
|
+
sub.textContent = `v${info.version}` + (info.authenticated ? " \u2014 authenticated" : " \u2014 not authenticated");
|
|
207
|
+
sub.style.color = info.authenticated ? "var(--success)" : "var(--warning)";
|
|
208
|
+
labelWrap.appendChild(sub);
|
|
209
|
+
} else if (enabled && !info.found) {
|
|
210
|
+
const sub = el("div", "settings__toggle-label-sub");
|
|
211
|
+
sub.textContent = "Not installed";
|
|
212
|
+
sub.style.color = "var(--text-muted)";
|
|
213
|
+
labelWrap.appendChild(sub);
|
|
213
214
|
}
|
|
214
|
-
}
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
toggleRow.appendChild(labelWrap);
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
218
|
+
const toggle = el("button", "settings__toggle" + (enabled ? " active" : ""));
|
|
219
|
+
toggle.addEventListener("click", async () => {
|
|
220
|
+
const newVal = !enabled;
|
|
221
|
+
await fetch("/api/settings/cli-toggle", {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: { "Content-Type": "application/json" },
|
|
224
|
+
body: JSON.stringify({ toolId: tool.id, enabled: newVal }),
|
|
225
|
+
});
|
|
226
|
+
refreshSettings();
|
|
227
|
+
});
|
|
228
|
+
toggleRow.appendChild(toggle);
|
|
229
|
+
|
|
230
|
+
row.appendChild(toggleRow);
|
|
231
|
+
|
|
232
|
+
// If enabled but not installed, show install button
|
|
233
|
+
if (enabled && !info.found) {
|
|
234
|
+
const installRow = el("div", "settings__card-row");
|
|
235
|
+
const installBtn = el("button", "settings__btn settings__btn--primary");
|
|
236
|
+
installBtn.textContent = "Install";
|
|
237
|
+
installBtn.addEventListener("click", () => installTool(tool.installId, installBtn));
|
|
238
|
+
installRow.appendChild(installBtn);
|
|
239
|
+
if (tool.url) {
|
|
240
|
+
const link = el("a", "settings__btn");
|
|
241
|
+
link.textContent = "Docs";
|
|
242
|
+
link.href = tool.url;
|
|
243
|
+
link.target = "_blank";
|
|
244
|
+
link.style.textDecoration = "none";
|
|
245
|
+
installRow.appendChild(link);
|
|
246
|
+
}
|
|
247
|
+
row.appendChild(installRow);
|
|
248
|
+
}
|
|
228
249
|
|
|
250
|
+
// If enabled, installed, but not authenticated — show sign in
|
|
251
|
+
if (enabled && info.found && !info.authenticated) {
|
|
252
|
+
const authRow = el("div", "settings__card-row");
|
|
229
253
|
const authBtn = el("button", "settings__btn settings__btn--primary");
|
|
230
254
|
authBtn.textContent = "Sign in";
|
|
231
|
-
authBtn.addEventListener("click", () => authCLI(installId, authBtn));
|
|
255
|
+
authBtn.addEventListener("click", () => authCLI(tool.installId, authBtn));
|
|
232
256
|
authRow.appendChild(authBtn);
|
|
233
|
-
|
|
234
|
-
card.appendChild(authRow);
|
|
257
|
+
row.appendChild(authRow);
|
|
235
258
|
}
|
|
236
|
-
} else if (info.found && info.authenticated) {
|
|
237
|
-
const authRow = el("div", "settings__card-row settings__card-row--sub");
|
|
238
|
-
const authLabel = el("span", "settings__card-meta");
|
|
239
|
-
authLabel.textContent = "Authenticated";
|
|
240
|
-
authLabel.style.color = "var(--success, #22c55e)";
|
|
241
|
-
authRow.appendChild(authLabel);
|
|
242
|
-
card.appendChild(authRow);
|
|
243
|
-
}
|
|
244
259
|
|
|
245
|
-
|
|
260
|
+
cliSection.appendChild(row);
|
|
261
|
+
}
|
|
262
|
+
body.appendChild(cliSection);
|
|
246
263
|
}
|
|
247
264
|
|
|
248
265
|
// ---------------------------------------------------------------------------
|
|
249
|
-
//
|
|
266
|
+
// HubSpot Tab
|
|
250
267
|
// ---------------------------------------------------------------------------
|
|
251
268
|
|
|
252
|
-
function
|
|
253
|
-
const
|
|
269
|
+
function renderHubSpotTab(body, data) {
|
|
270
|
+
const env = data.environment;
|
|
271
|
+
const config = data.config;
|
|
272
|
+
const hs = env.tools.hubspot;
|
|
254
273
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
274
|
+
// Upload mode toggle
|
|
275
|
+
const modeSection = el("section", "settings__section");
|
|
276
|
+
modeSection.appendChild(sectionTitle("Upload Mode"));
|
|
258
277
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const value = el("span", "settings__apikey-value");
|
|
262
|
-
value.textContent = keyInfo.masked;
|
|
263
|
-
card.appendChild(value);
|
|
278
|
+
const currentMode = config.hubspotUploadMode || "api";
|
|
279
|
+
const pills = el("div", "settings__mode-pills");
|
|
264
280
|
|
|
265
|
-
|
|
281
|
+
const apiPill = el("button", "settings__mode-pill" + (currentMode === "api" ? " active" : ""));
|
|
282
|
+
apiPill.textContent = "API (recommended)";
|
|
283
|
+
apiPill.addEventListener("click", () => setHsMode("api"));
|
|
284
|
+
pills.appendChild(apiPill);
|
|
266
285
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
showApiKeyInput(card, provider, name, placeholder);
|
|
272
|
-
});
|
|
273
|
-
actions.appendChild(editBtn);
|
|
286
|
+
const cliPill = el("button", "settings__mode-pill" + (currentMode === "cli" ? " active" : ""));
|
|
287
|
+
cliPill.textContent = "CLI (legacy)";
|
|
288
|
+
cliPill.addEventListener("click", () => setHsMode("cli"));
|
|
289
|
+
pills.appendChild(cliPill);
|
|
274
290
|
|
|
275
|
-
|
|
276
|
-
clearBtn.textContent = "Clear";
|
|
277
|
-
clearBtn.addEventListener("click", async () => {
|
|
278
|
-
await fetch("/api/settings/apikey", {
|
|
279
|
-
method: "POST",
|
|
280
|
-
headers: { "Content-Type": "application/json" },
|
|
281
|
-
body: JSON.stringify({ provider, apiKey: null }),
|
|
282
|
-
});
|
|
283
|
-
refreshSettings();
|
|
284
|
-
});
|
|
285
|
-
actions.appendChild(clearBtn);
|
|
291
|
+
modeSection.appendChild(pills);
|
|
286
292
|
|
|
287
|
-
|
|
293
|
+
if (currentMode === "api") {
|
|
294
|
+
modeSection.appendChild(desc("Uploads directly to HubSpot via API. No CLI installation needed. Supports parallel file uploads for faster deployments."));
|
|
288
295
|
} else {
|
|
289
|
-
|
|
296
|
+
modeSection.appendChild(desc("Uses the HubSpot CLI (hs command). Requires @hubspot/cli installed globally. Slower sequential uploads."));
|
|
290
297
|
}
|
|
291
298
|
|
|
292
|
-
|
|
293
|
-
}
|
|
299
|
+
body.appendChild(modeSection);
|
|
294
300
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
container.innerHTML = "";
|
|
299
|
-
if (label) container.appendChild(label);
|
|
300
|
-
else {
|
|
301
|
-
const lbl = el("span", "settings__apikey-label");
|
|
302
|
-
lbl.textContent = name;
|
|
303
|
-
container.appendChild(lbl);
|
|
304
|
-
}
|
|
301
|
+
// Accounts section
|
|
302
|
+
const acctSection = el("section", "settings__section");
|
|
303
|
+
acctSection.appendChild(sectionTitle("Accounts"));
|
|
305
304
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
input.placeholder = placeholder;
|
|
309
|
-
container.appendChild(input);
|
|
305
|
+
if (currentMode === "api") {
|
|
306
|
+
acctSection.appendChild(desc("Connect HubSpot accounts with a Personal Access Key. Keys are stored locally and used to authenticate API requests."));
|
|
310
307
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// ---------------------------------------------------------------------------
|
|
324
|
-
// HubSpot card
|
|
325
|
-
// ---------------------------------------------------------------------------
|
|
326
|
-
|
|
327
|
-
function createHubSpotCard(hs) {
|
|
328
|
-
const card = el("div", "settings__card");
|
|
308
|
+
const accounts = config.hubspotAccounts || [];
|
|
309
|
+
if (accounts.length > 0) {
|
|
310
|
+
for (const acct of accounts) {
|
|
311
|
+
const isActive = acct.portalId === config.activeHubSpotAccount;
|
|
312
|
+
const card = el("div", "settings__card");
|
|
313
|
+
const row = el("div", "settings__card-row");
|
|
314
|
+
row.appendChild(dot(isActive ? "success" : "muted"));
|
|
315
|
+
const label = el("span", "settings__card-label");
|
|
316
|
+
label.textContent = `${acct.portalName} (${acct.portalId})`;
|
|
317
|
+
if (isActive) label.textContent += " \u2014 active";
|
|
318
|
+
row.appendChild(label);
|
|
329
319
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
320
|
+
const actions = el("span", "settings__card-actions");
|
|
321
|
+
if (!isActive) {
|
|
322
|
+
const useBtn = el("button", "settings__btn settings__btn--small");
|
|
323
|
+
useBtn.textContent = "Use";
|
|
324
|
+
useBtn.addEventListener("click", () => switchHsAccount(acct.portalId, useBtn));
|
|
325
|
+
actions.appendChild(useBtn);
|
|
326
|
+
}
|
|
327
|
+
const removeBtn = el("button", "settings__btn settings__btn--small settings__btn--danger");
|
|
328
|
+
removeBtn.textContent = "Remove";
|
|
329
|
+
removeBtn.addEventListener("click", () => removeHsAccount(acct.portalId, removeBtn));
|
|
330
|
+
actions.appendChild(removeBtn);
|
|
331
|
+
row.appendChild(actions);
|
|
332
|
+
card.appendChild(row);
|
|
333
|
+
acctSection.appendChild(card);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
const empty = el("div", "settings__card");
|
|
337
|
+
const emptyRow = el("div", "settings__card-row");
|
|
338
|
+
emptyRow.appendChild(dot("muted"));
|
|
339
|
+
const emptyLabel = el("span", "settings__card-label");
|
|
340
|
+
emptyLabel.textContent = "No accounts connected";
|
|
341
|
+
emptyRow.appendChild(emptyLabel);
|
|
342
|
+
empty.appendChild(emptyRow);
|
|
343
|
+
acctSection.appendChild(empty);
|
|
344
|
+
}
|
|
336
345
|
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
346
|
+
// Add account button + PAK input
|
|
347
|
+
const addCard = el("div", "settings__card");
|
|
348
|
+
const addRow = el("div", "settings__card-row");
|
|
349
|
+
const addBtn = el("button", "settings__btn settings__btn--primary");
|
|
350
|
+
addBtn.textContent = "Add Account";
|
|
351
|
+
addBtn.addEventListener("click", () => showPakInput(addCard, addBtn));
|
|
352
|
+
addRow.appendChild(addBtn);
|
|
353
|
+
addCard.appendChild(addRow);
|
|
354
|
+
acctSection.appendChild(addCard);
|
|
341
355
|
} else {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
356
|
+
// CLI mode — show CLI status and accounts from hs accounts list
|
|
357
|
+
acctSection.appendChild(desc("HubSpot CLI accounts are managed by the hs command. Use \u201chs auth\u201d to add accounts."));
|
|
358
|
+
|
|
359
|
+
const cliCard = el("div", "settings__card");
|
|
360
|
+
const cliRow = el("div", "settings__card-row");
|
|
361
|
+
cliRow.appendChild(dot(hs.found ? "success" : "warn"));
|
|
362
|
+
const cliLabel = el("span", "settings__card-label");
|
|
363
|
+
cliLabel.textContent = "HubSpot CLI";
|
|
364
|
+
cliRow.appendChild(cliLabel);
|
|
365
|
+
|
|
366
|
+
if (hs.found) {
|
|
367
|
+
const meta = el("span", "settings__card-meta");
|
|
368
|
+
meta.textContent = `v${hs.version}`;
|
|
369
|
+
cliRow.appendChild(meta);
|
|
370
|
+
} else {
|
|
371
|
+
const installBtn = el("button", "settings__btn");
|
|
372
|
+
installBtn.textContent = "Install";
|
|
373
|
+
installBtn.addEventListener("click", () => installTool("hubspot", installBtn));
|
|
374
|
+
cliRow.appendChild(installBtn);
|
|
375
|
+
}
|
|
376
|
+
cliCard.appendChild(cliRow);
|
|
377
|
+
acctSection.appendChild(cliCard);
|
|
352
378
|
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
379
|
+
if (hs.found) {
|
|
380
|
+
const accounts = hs.accounts || [];
|
|
381
|
+
for (const acct of accounts) {
|
|
382
|
+
const card = el("div", "settings__card");
|
|
356
383
|
const row = el("div", "settings__card-row");
|
|
357
384
|
row.appendChild(dot(acct.isDefault ? "success" : "muted"));
|
|
358
385
|
const label = el("span", "settings__card-label");
|
|
359
386
|
label.textContent = `${acct.name} (${acct.portalId})`;
|
|
360
|
-
if (acct.isDefault) label.textContent += "
|
|
387
|
+
if (acct.isDefault) label.textContent += " \u2014 active";
|
|
361
388
|
row.appendChild(label);
|
|
362
389
|
|
|
363
390
|
const actions = el("span", "settings__card-actions");
|
|
364
|
-
|
|
365
391
|
if (!acct.isDefault) {
|
|
366
392
|
const useBtn = el("button", "settings__btn settings__btn--small");
|
|
367
393
|
useBtn.textContent = "Use";
|
|
368
394
|
useBtn.addEventListener("click", () => switchHsAccount(acct.portalId, useBtn));
|
|
369
395
|
actions.appendChild(useBtn);
|
|
370
396
|
}
|
|
371
|
-
|
|
372
397
|
const removeBtn = el("button", "settings__btn settings__btn--small settings__btn--danger");
|
|
373
398
|
removeBtn.textContent = "Remove";
|
|
374
399
|
removeBtn.addEventListener("click", () => removeHsAccount(acct.portalId, removeBtn));
|
|
375
400
|
actions.appendChild(removeBtn);
|
|
376
|
-
|
|
377
401
|
row.appendChild(actions);
|
|
378
402
|
card.appendChild(row);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const authRow = el("div", "settings__card-row");
|
|
382
|
-
authRow.appendChild(dot("warn"));
|
|
383
|
-
const authLabel = el("span", "settings__card-label");
|
|
384
|
-
authLabel.textContent = "Not authenticated";
|
|
385
|
-
authRow.appendChild(authLabel);
|
|
386
|
-
card.appendChild(authRow);
|
|
387
|
-
}
|
|
403
|
+
acctSection.appendChild(card);
|
|
404
|
+
}
|
|
388
405
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
406
|
+
const addRow2 = el("div", "settings__card");
|
|
407
|
+
const addBtnRow = el("div", "settings__card-row");
|
|
408
|
+
const addBtn2 = el("button", "settings__btn settings__btn--primary");
|
|
409
|
+
addBtn2.textContent = "Add Account";
|
|
410
|
+
addBtn2.addEventListener("click", () => startHsAuth(addBtn2, addRow2));
|
|
411
|
+
addBtnRow.appendChild(addBtn2);
|
|
412
|
+
addRow2.appendChild(addBtnRow);
|
|
413
|
+
acctSection.appendChild(addRow2);
|
|
414
|
+
}
|
|
396
415
|
}
|
|
397
416
|
|
|
398
|
-
|
|
417
|
+
body.appendChild(acctSection);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function showPakInput(container, addBtn) {
|
|
421
|
+
// Hide the add button, show PAK input
|
|
422
|
+
addBtn.style.display = "none";
|
|
423
|
+
|
|
424
|
+
const instructions = el("div", "settings__instructions");
|
|
425
|
+
instructions.innerHTML = `
|
|
426
|
+
<strong>Connect your HubSpot account:</strong>
|
|
427
|
+
<ol>
|
|
428
|
+
<li>Open <a href="https://app.hubspot.com/l/personal-access-key" target="_blank" rel="noopener">HubSpot Personal Access Key</a></li>
|
|
429
|
+
<li>Create a key with the <strong>Content</strong> scope enabled</li>
|
|
430
|
+
<li>Copy the key and paste it below</li>
|
|
431
|
+
</ol>
|
|
432
|
+
<div class="settings__pak-row">
|
|
433
|
+
<input type="password" class="settings__apikey-input" placeholder="Personal access key (pat-na1-...)" />
|
|
434
|
+
<button class="settings__btn settings__btn--primary">Connect</button>
|
|
435
|
+
</div>
|
|
436
|
+
`;
|
|
437
|
+
container.appendChild(instructions);
|
|
438
|
+
|
|
439
|
+
const input = instructions.querySelector("input");
|
|
440
|
+
const saveBtn = instructions.querySelector("button");
|
|
441
|
+
input.focus();
|
|
442
|
+
|
|
443
|
+
const doSave = async () => {
|
|
444
|
+
const key = input.value.trim();
|
|
445
|
+
if (!key) return;
|
|
446
|
+
saveBtn.disabled = true;
|
|
447
|
+
saveBtn.innerHTML = '<span class="settings__spinner"></span> Validating...';
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const res = await fetch("/api/settings/hs-auth", {
|
|
451
|
+
method: "POST",
|
|
452
|
+
headers: { "Content-Type": "application/json" },
|
|
453
|
+
body: JSON.stringify({ personalAccessKey: key }),
|
|
454
|
+
});
|
|
455
|
+
const data = await res.json();
|
|
456
|
+
if (data.ok) {
|
|
457
|
+
refreshSettings();
|
|
458
|
+
} else if (data.jobId) {
|
|
459
|
+
pollJob(data.jobId, () => refreshSettings(), () => {
|
|
460
|
+
saveBtn.textContent = "Failed";
|
|
461
|
+
saveBtn.disabled = false;
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
saveBtn.textContent = data.error || "Failed";
|
|
465
|
+
saveBtn.disabled = false;
|
|
466
|
+
setTimeout(() => { saveBtn.textContent = "Connect"; }, 2000);
|
|
467
|
+
}
|
|
468
|
+
} catch {
|
|
469
|
+
saveBtn.textContent = "Failed";
|
|
470
|
+
setTimeout(() => { saveBtn.textContent = "Connect"; saveBtn.disabled = false; }, 2000);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
saveBtn.addEventListener("click", doSave);
|
|
475
|
+
input.addEventListener("keydown", (e) => {
|
|
476
|
+
if (e.key === "Enter") { e.preventDefault(); doSave(); }
|
|
477
|
+
});
|
|
399
478
|
}
|
|
400
479
|
|
|
401
480
|
// ---------------------------------------------------------------------------
|
|
402
|
-
// GitHub
|
|
481
|
+
// GitHub Tab
|
|
403
482
|
// ---------------------------------------------------------------------------
|
|
404
483
|
|
|
405
|
-
function
|
|
484
|
+
function renderGitHubTab(body, data) {
|
|
485
|
+
const gh = data.environment.tools.github;
|
|
486
|
+
|
|
487
|
+
const section = el("section", "settings__section");
|
|
488
|
+
section.appendChild(sectionTitle("GitHub CLI"));
|
|
489
|
+
section.appendChild(desc("GitHub CLI enables pushing your theme to a repository. Optional \u2014 not needed for HubSpot deployment."));
|
|
490
|
+
|
|
406
491
|
const card = el("div", "settings__card");
|
|
407
492
|
|
|
408
493
|
// CLI status
|
|
@@ -435,9 +520,7 @@ function createGitHubCard(gh) {
|
|
|
435
520
|
authRow.appendChild(authLabel);
|
|
436
521
|
|
|
437
522
|
const actions = el("span", "settings__card-actions");
|
|
438
|
-
|
|
439
523
|
if (gh.authenticated) {
|
|
440
|
-
// Switch account = logout + login
|
|
441
524
|
const switchBtn = el("button", "settings__btn settings__btn--small");
|
|
442
525
|
switchBtn.textContent = "Switch";
|
|
443
526
|
switchBtn.addEventListener("click", () => switchGhAccount(switchBtn));
|
|
@@ -458,7 +541,69 @@ function createGitHubCard(gh) {
|
|
|
458
541
|
card.appendChild(authRow);
|
|
459
542
|
}
|
|
460
543
|
|
|
461
|
-
|
|
544
|
+
section.appendChild(card);
|
|
545
|
+
body.appendChild(section);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
// vibeSpot Tab
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
|
|
552
|
+
function renderVibeSpotTab(body, data) {
|
|
553
|
+
const section = el("section", "settings__section");
|
|
554
|
+
section.appendChild(sectionTitle("About"));
|
|
555
|
+
section.appendChild(desc("General vibeSpot configuration and information."));
|
|
556
|
+
|
|
557
|
+
const card = el("div", "settings__card");
|
|
558
|
+
|
|
559
|
+
// Version
|
|
560
|
+
const versionRow = el("div", "settings__card-row");
|
|
561
|
+
const versionLabel = el("span", "settings__card-label");
|
|
562
|
+
versionLabel.textContent = "Version";
|
|
563
|
+
versionRow.appendChild(versionLabel);
|
|
564
|
+
const versionVal = el("span", "settings__card-meta");
|
|
565
|
+
versionVal.textContent = data.version || "dev";
|
|
566
|
+
versionRow.appendChild(versionVal);
|
|
567
|
+
card.appendChild(versionRow);
|
|
568
|
+
|
|
569
|
+
// Workspace
|
|
570
|
+
const wsRow = el("div", "settings__card-row");
|
|
571
|
+
const wsLabel = el("span", "settings__card-label");
|
|
572
|
+
wsLabel.textContent = "Workspace";
|
|
573
|
+
wsRow.appendChild(wsLabel);
|
|
574
|
+
const wsVal = el("span", "settings__card-meta");
|
|
575
|
+
wsVal.textContent = "~/vibespot-themes";
|
|
576
|
+
wsVal.style.fontFamily = "var(--font-mono, monospace)";
|
|
577
|
+
wsVal.style.fontSize = "11px";
|
|
578
|
+
wsRow.appendChild(wsVal);
|
|
579
|
+
card.appendChild(wsRow);
|
|
580
|
+
|
|
581
|
+
// Session count
|
|
582
|
+
if (data.sessionCount !== undefined) {
|
|
583
|
+
const sessRow = el("div", "settings__card-row");
|
|
584
|
+
const sessLabel = el("span", "settings__card-label");
|
|
585
|
+
sessLabel.textContent = "Saved sessions";
|
|
586
|
+
sessRow.appendChild(sessLabel);
|
|
587
|
+
const sessVal = el("span", "settings__card-meta");
|
|
588
|
+
sessVal.textContent = String(data.sessionCount);
|
|
589
|
+
sessRow.appendChild(sessVal);
|
|
590
|
+
card.appendChild(sessRow);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Local themes count
|
|
594
|
+
if (data.localThemeCount !== undefined) {
|
|
595
|
+
const themeRow = el("div", "settings__card-row");
|
|
596
|
+
const themeLabel = el("span", "settings__card-label");
|
|
597
|
+
themeLabel.textContent = "Local themes";
|
|
598
|
+
themeRow.appendChild(themeLabel);
|
|
599
|
+
const themeVal = el("span", "settings__card-meta");
|
|
600
|
+
themeVal.textContent = String(data.localThemeCount);
|
|
601
|
+
themeRow.appendChild(themeVal);
|
|
602
|
+
card.appendChild(themeRow);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
section.appendChild(card);
|
|
606
|
+
body.appendChild(section);
|
|
462
607
|
}
|
|
463
608
|
|
|
464
609
|
// ---------------------------------------------------------------------------
|
|
@@ -472,7 +617,6 @@ async function setEngine(engineId) {
|
|
|
472
617
|
body: JSON.stringify({ engine: engineId }),
|
|
473
618
|
});
|
|
474
619
|
|
|
475
|
-
// Update statusbar engine label
|
|
476
620
|
const statusEngine = document.getElementById("status-engine");
|
|
477
621
|
if (statusEngine) {
|
|
478
622
|
statusEngine.textContent = ENGINE_LABELS[engineId] || engineId;
|
|
@@ -481,6 +625,15 @@ async function setEngine(engineId) {
|
|
|
481
625
|
refreshSettings();
|
|
482
626
|
}
|
|
483
627
|
|
|
628
|
+
async function setHsMode(mode) {
|
|
629
|
+
await fetch("/api/settings/hs-mode", {
|
|
630
|
+
method: "POST",
|
|
631
|
+
headers: { "Content-Type": "application/json" },
|
|
632
|
+
body: JSON.stringify({ mode }),
|
|
633
|
+
});
|
|
634
|
+
refreshSettings();
|
|
635
|
+
}
|
|
636
|
+
|
|
484
637
|
async function saveApiKey(provider, key, btn) {
|
|
485
638
|
if (!key || !key.trim()) return;
|
|
486
639
|
btn.disabled = true;
|
|
@@ -518,9 +671,7 @@ async function installTool(toolId, btn) {
|
|
|
518
671
|
const data = await res.json();
|
|
519
672
|
|
|
520
673
|
if (data.jobId) {
|
|
521
|
-
pollJob(data.jobId, () => {
|
|
522
|
-
refreshSettings();
|
|
523
|
-
}, (err) => {
|
|
674
|
+
pollJob(data.jobId, () => refreshSettings(), () => {
|
|
524
675
|
btn.textContent = "Failed";
|
|
525
676
|
btn.disabled = false;
|
|
526
677
|
});
|
|
@@ -611,7 +762,6 @@ async function switchGhAccount(btn) {
|
|
|
611
762
|
btn.innerHTML = '<span class="settings__spinner"></span>';
|
|
612
763
|
|
|
613
764
|
try {
|
|
614
|
-
// Logout first, then trigger login
|
|
615
765
|
const res = await fetch("/api/settings/gh-logout", {
|
|
616
766
|
method: "POST",
|
|
617
767
|
headers: { "Content-Type": "application/json" },
|
|
@@ -620,7 +770,6 @@ async function switchGhAccount(btn) {
|
|
|
620
770
|
const data = await res.json();
|
|
621
771
|
if (data.jobId) {
|
|
622
772
|
pollJob(data.jobId, () => {
|
|
623
|
-
// After logout completes, start login flow
|
|
624
773
|
startGhAuth(btn);
|
|
625
774
|
}, () => {
|
|
626
775
|
btn.textContent = "Failed";
|
|
@@ -646,7 +795,6 @@ async function startHsAuth(btn, card) {
|
|
|
646
795
|
const data = await res.json();
|
|
647
796
|
|
|
648
797
|
if (data.needsKey) {
|
|
649
|
-
// Show instructions to get a personal access key
|
|
650
798
|
btn.textContent = "Connect";
|
|
651
799
|
btn.disabled = false;
|
|
652
800
|
|
|
@@ -764,18 +912,15 @@ function pollJob(jobId, onComplete, onError) {
|
|
|
764
912
|
// CLI auth
|
|
765
913
|
// ---------------------------------------------------------------------------
|
|
766
914
|
|
|
767
|
-
async function authCLI(cli, btn
|
|
915
|
+
async function authCLI(cli, btn) {
|
|
768
916
|
btn.disabled = true;
|
|
769
917
|
btn.innerHTML = '<span class="settings__spinner"></span>';
|
|
770
918
|
|
|
771
919
|
try {
|
|
772
|
-
const payload = { cli };
|
|
773
|
-
if (apiKey) payload.apiKey = apiKey;
|
|
774
|
-
|
|
775
920
|
const res = await fetch("/api/settings/cli-auth", {
|
|
776
921
|
method: "POST",
|
|
777
922
|
headers: { "Content-Type": "application/json" },
|
|
778
|
-
body: JSON.stringify(
|
|
923
|
+
body: JSON.stringify({ cli }),
|
|
779
924
|
});
|
|
780
925
|
const data = await res.json();
|
|
781
926
|
|
|
@@ -786,17 +931,15 @@ async function authCLI(cli, btn, apiKey) {
|
|
|
786
931
|
}
|
|
787
932
|
|
|
788
933
|
if (data.hint) {
|
|
789
|
-
// Show hint to user (e.g., check browser)
|
|
790
934
|
btn.innerHTML = '<span class="settings__spinner"></span> Check browser...';
|
|
791
935
|
}
|
|
792
936
|
|
|
793
937
|
if (data.jobId) {
|
|
794
938
|
pollJob(data.jobId, () => refreshSettings(), () => {
|
|
795
|
-
btn.textContent = "Failed
|
|
939
|
+
btn.textContent = "Failed \u2014 try again";
|
|
796
940
|
btn.disabled = false;
|
|
797
941
|
});
|
|
798
942
|
} else {
|
|
799
|
-
// Immediate success (e.g., Codex API key saved)
|
|
800
943
|
refreshSettings();
|
|
801
944
|
}
|
|
802
945
|
} catch {
|
|
@@ -805,16 +948,86 @@ async function authCLI(cli, btn, apiKey) {
|
|
|
805
948
|
}
|
|
806
949
|
}
|
|
807
950
|
|
|
951
|
+
// ---------------------------------------------------------------------------
|
|
952
|
+
// API key card
|
|
953
|
+
// ---------------------------------------------------------------------------
|
|
954
|
+
|
|
955
|
+
function createApiKeyCard(provider, name, placeholder, keyInfo) {
|
|
956
|
+
const card = el("div", "settings__apikey-row");
|
|
957
|
+
|
|
958
|
+
const label = el("span", "settings__apikey-label");
|
|
959
|
+
label.textContent = name;
|
|
960
|
+
card.appendChild(label);
|
|
961
|
+
|
|
962
|
+
if (keyInfo.configured) {
|
|
963
|
+
const value = el("span", "settings__apikey-value");
|
|
964
|
+
value.textContent = keyInfo.masked;
|
|
965
|
+
card.appendChild(value);
|
|
966
|
+
|
|
967
|
+
const actions = el("div", "settings__apikey-actions");
|
|
968
|
+
|
|
969
|
+
const editBtn = el("button", "settings__btn");
|
|
970
|
+
editBtn.textContent = "Edit";
|
|
971
|
+
editBtn.addEventListener("click", () => {
|
|
972
|
+
showApiKeyInput(card, provider, name, placeholder);
|
|
973
|
+
});
|
|
974
|
+
actions.appendChild(editBtn);
|
|
975
|
+
|
|
976
|
+
const clearBtn = el("button", "settings__btn");
|
|
977
|
+
clearBtn.textContent = "Clear";
|
|
978
|
+
clearBtn.addEventListener("click", async () => {
|
|
979
|
+
await fetch("/api/settings/apikey", {
|
|
980
|
+
method: "POST",
|
|
981
|
+
headers: { "Content-Type": "application/json" },
|
|
982
|
+
body: JSON.stringify({ provider, apiKey: null }),
|
|
983
|
+
});
|
|
984
|
+
refreshSettings();
|
|
985
|
+
});
|
|
986
|
+
actions.appendChild(clearBtn);
|
|
987
|
+
|
|
988
|
+
card.appendChild(actions);
|
|
989
|
+
} else {
|
|
990
|
+
showApiKeyInput(card, provider, name, placeholder);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return card;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function showApiKeyInput(container, provider, name, placeholder) {
|
|
997
|
+
const label = container.querySelector(".settings__apikey-label");
|
|
998
|
+
container.innerHTML = "";
|
|
999
|
+
if (label) container.appendChild(label);
|
|
1000
|
+
else {
|
|
1001
|
+
const lbl = el("span", "settings__apikey-label");
|
|
1002
|
+
lbl.textContent = name;
|
|
1003
|
+
container.appendChild(lbl);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const input = el("input", "settings__apikey-input");
|
|
1007
|
+
input.type = "password";
|
|
1008
|
+
input.placeholder = placeholder;
|
|
1009
|
+
container.appendChild(input);
|
|
1010
|
+
|
|
1011
|
+
const saveBtn = el("button", "settings__btn settings__btn--primary");
|
|
1012
|
+
saveBtn.textContent = "Save";
|
|
1013
|
+
saveBtn.addEventListener("click", () => saveApiKey(provider, input.value, saveBtn));
|
|
1014
|
+
container.appendChild(saveBtn);
|
|
1015
|
+
|
|
1016
|
+
input.addEventListener("keydown", (e) => {
|
|
1017
|
+
if (e.key === "Enter") { e.preventDefault(); saveBtn.click(); }
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
input.focus();
|
|
1021
|
+
}
|
|
1022
|
+
|
|
808
1023
|
// ---------------------------------------------------------------------------
|
|
809
1024
|
// Model selection helpers
|
|
810
1025
|
// ---------------------------------------------------------------------------
|
|
811
1026
|
|
|
812
1027
|
function getModelsForEngine(engine) {
|
|
813
|
-
// Use server-provided model catalog if available
|
|
814
1028
|
if (settingsData && settingsData.models && settingsData.models[engine]) {
|
|
815
1029
|
return settingsData.models[engine];
|
|
816
1030
|
}
|
|
817
|
-
// Fallback to hardcoded defaults
|
|
818
1031
|
switch (engine) {
|
|
819
1032
|
case "claude-code":
|
|
820
1033
|
return [
|
|
@@ -869,7 +1082,6 @@ async function setEngineModel(engine, model) {
|
|
|
869
1082
|
body: JSON.stringify({ engine, model }),
|
|
870
1083
|
});
|
|
871
1084
|
|
|
872
|
-
// Update statusbar
|
|
873
1085
|
const statusEngine = document.getElementById("status-engine");
|
|
874
1086
|
if (statusEngine) {
|
|
875
1087
|
const label = ENGINE_LABELS[engine] || engine;
|
|
@@ -890,8 +1102,7 @@ function el(tag, className) {
|
|
|
890
1102
|
}
|
|
891
1103
|
|
|
892
1104
|
function dot(variant) {
|
|
893
|
-
|
|
894
|
-
return d;
|
|
1105
|
+
return el("span", `settings__dot settings__dot--${variant}`);
|
|
895
1106
|
}
|
|
896
1107
|
|
|
897
1108
|
function sectionTitle(text) {
|
|
@@ -906,6 +1117,12 @@ function subsectionTitle(text) {
|
|
|
906
1117
|
return h;
|
|
907
1118
|
}
|
|
908
1119
|
|
|
1120
|
+
function desc(text) {
|
|
1121
|
+
const p = el("p", "settings__description");
|
|
1122
|
+
p.textContent = text;
|
|
1123
|
+
return p;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
909
1126
|
function escSettings(str) {
|
|
910
1127
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
911
1128
|
}
|
|
@@ -919,12 +1136,13 @@ document.getElementById("settings-overlay").addEventListener("click", (e) => {
|
|
|
919
1136
|
if (e.target.id === "settings-overlay") closeSettings();
|
|
920
1137
|
});
|
|
921
1138
|
|
|
922
|
-
// Setup screen settings button
|
|
923
1139
|
document.getElementById("btn-setup-settings").addEventListener("click", openSettings);
|
|
924
1140
|
|
|
925
|
-
// Escape key
|
|
926
1141
|
document.addEventListener("keydown", (e) => {
|
|
927
1142
|
if (e.key === "Escape" && !document.getElementById("settings-overlay").classList.contains("hidden")) {
|
|
928
1143
|
closeSettings();
|
|
929
1144
|
}
|
|
930
1145
|
});
|
|
1146
|
+
|
|
1147
|
+
// Initialize tabs
|
|
1148
|
+
initTabs();
|