@starlove/orbit 1.3.5 → 1.4.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/Dockerfile +2 -1
- package/README.md +10 -9
- package/SKILL-ORBIT.md +41 -17
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/OFL.txt +93 -0
- package/dist/full/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/OFL.txt +93 -0
- package/dist/full/fonts/Inter/README.txt +118 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/OFL.txt +93 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +79 -0
- package/dist/full/fonts/OpenRing-Regular.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OFL.txt +93 -0
- package/dist/full/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/README.txt +100 -0
- package/dist/full/fonts/Orbitron/OFL.txt +93 -0
- package/dist/full/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/Orbitron/README.txt +68 -0
- package/dist/full/fonts/Rajdhani/OFL.txt +93 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/dist/full/fonts/runescape-uf.ttf +0 -0
- package/dist/full/index.html +135 -25
- package/dist/full/js/app.62905fcb.js +952 -0
- package/dist/full/js/app.62905fcb.js.map +7 -0
- package/dist/full/styles.4e60d824.css +1 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +20 -13
- package/docs/ORBIT_DISPATCH.md +73 -38
- package/docs/UPDATING.md +114 -0
- package/package.json +5 -7
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +93 -0
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +93 -0
- package/public/fonts/Inter/README.txt +118 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/OFL.txt +93 -0
- package/public/fonts/JetBrains_Mono/README.txt +79 -0
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OFL.txt +93 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +100 -0
- package/public/fonts/Orbitron/OFL.txt +93 -0
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +68 -0
- package/public/fonts/Rajdhani/OFL.txt +93 -0
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +133 -23
- package/public/js/artifact-drawer.js +79 -0
- package/public/js/board-export.js +161 -0
- package/public/js/board-menu.js +219 -17
- package/public/js/card-actions.js +1 -1
- package/public/js/create-card.js +60 -23
- package/public/js/drawer.js +164 -4
- package/public/js/font-preference.js +229 -0
- package/public/js/format.js +0 -10
- package/public/js/icons.js +16 -0
- package/public/js/local-backend.js +31 -4
- package/public/js/main.js +7 -1
- package/public/js/modal.js +3 -1
- package/public/js/panel.js +58 -0
- package/public/js/settings-tabs.js +1 -1
- package/public/js/settings.js +606 -275
- package/public/js/sse.js +3 -0
- package/public/js/state.js +1 -1
- package/public/js/text-editing.js +128 -0
- package/public/js/ticket-detail.js +377 -148
- package/public/js/ticket-typeahead.js +49 -0
- package/public/js/toast.js +19 -4
- package/public/js/topbar-notes.js +3 -3
- package/public/js/typeahead.js +206 -0
- package/public/js/update-check.js +58 -0
- package/public/styles.css +5447 -3572
- package/src/cli/orbit.js +9 -5
- package/src/core/agent.js +12 -4
- package/src/core/agents-md.js +3 -3
- package/src/core/artifacts.js +149 -0
- package/src/core/board.js +3 -2
- package/src/core/boards.js +0 -1
- package/src/core/css-bundle.js +1 -0
- package/src/core/db.js +6 -0
- package/src/core/dispatch.js +344 -34
- package/src/core/http.js +9 -2
- package/src/core/paths.js +26 -2
- package/src/core/router.js +33 -0
- package/src/core/search.js +3 -2
- package/src/core/snapshots.js +10 -5
- package/src/core/tickets.js +34 -3
- package/src/core/util.js +3 -2
- package/src/core/version-check.js +154 -0
- package/src/mcp/orbit-client.js +18 -2
- package/src/mcp-server.js +19 -14
- package/src/server.js +28 -3
- package/dist/full/js/app.4ae80764.js +0 -742
- package/dist/full/js/app.4ae80764.js.map +0 -7
- package/dist/full/styles.b9e0c130.css +0 -1
package/public/js/settings.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { state, getSettingsTab, statesForProject, ticketsForProject, currentBoard } from "./state.js";
|
|
6
6
|
import { drawerInner, $ } from "./dom.js";
|
|
7
|
-
import { escapeHtml, formatDate,
|
|
7
|
+
import { escapeHtml, formatDate, renderPreservedText, cleanText } from "./format.js";
|
|
8
8
|
import { api, withBoardQuery } from "./api.js";
|
|
9
9
|
import { toast, downloadJson } from "./toast.js";
|
|
10
10
|
import { renderDrawerShell } from "./drawer.js";
|
|
@@ -20,7 +20,22 @@ import {
|
|
|
20
20
|
setReducedMotionPreference,
|
|
21
21
|
storedReducedMotionPreference
|
|
22
22
|
} from "./motion-preference.js";
|
|
23
|
-
import { currentTheme,
|
|
23
|
+
import { currentTheme, toggleThemePreference } from "./theme-preference.js";
|
|
24
|
+
import {
|
|
25
|
+
FONT_OPTIONS,
|
|
26
|
+
FONT_TARGETS,
|
|
27
|
+
SITE_FONT_OPTIONS,
|
|
28
|
+
USER_SCALE_MIN,
|
|
29
|
+
USER_SCALE_MAX,
|
|
30
|
+
USER_SCALE_STEP,
|
|
31
|
+
storedFontPreferences,
|
|
32
|
+
setSiteFont,
|
|
33
|
+
setUserScale,
|
|
34
|
+
setFontPreference
|
|
35
|
+
} from "./font-preference.js";
|
|
36
|
+
import { closeIconSvg } from "./icons.js";
|
|
37
|
+
import { exportAllBoards } from "./board-export.js";
|
|
38
|
+
import { getUpdateInfo, fetchVersionInfo, syncUpdateBadge } from "./update-check.js";
|
|
24
39
|
|
|
25
40
|
const repositoryDelete = {
|
|
26
41
|
boardId: "",
|
|
@@ -28,9 +43,35 @@ const repositoryDelete = {
|
|
|
28
43
|
exported: false
|
|
29
44
|
};
|
|
30
45
|
|
|
46
|
+
/** @starlove/ui slider: WebKit fill reads --value (0–100), set from min/max/value. */
|
|
47
|
+
function syncSliderValue(input) {
|
|
48
|
+
const min = Number(input.min) || 0;
|
|
49
|
+
const max = Number(input.max) || 100;
|
|
50
|
+
const value = Number(input.value);
|
|
51
|
+
const span = max - min;
|
|
52
|
+
const pct = span <= 0 ? 0 : ((value - min) / span) * 100;
|
|
53
|
+
input.style.setProperty("--value", String(pct));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bindSliders(root) {
|
|
57
|
+
root.querySelectorAll('input[type="range"].slider').forEach((slider) => {
|
|
58
|
+
syncSliderValue(slider);
|
|
59
|
+
slider.addEventListener("input", () => syncSliderValue(slider));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
export async function renderProjectDetail() {
|
|
32
64
|
const context = await api(`/api/boards/${state.boardId}/context?include_struck=true`);
|
|
33
|
-
const
|
|
65
|
+
const bootstrapBoard = currentBoard();
|
|
66
|
+
const project = {
|
|
67
|
+
...context.board,
|
|
68
|
+
// `/api/boards/:id/context` is agent-safe and intentionally omits
|
|
69
|
+
// human-only project notes. The Settings UI gets those notes from the
|
|
70
|
+
// human bootstrap payload instead, so Notes can reload without turning
|
|
71
|
+
// board_context/MCP into a notes surface.
|
|
72
|
+
...(bootstrapBoard?.id === context.board?.id ? { project_notes: bootstrapBoard.project_notes || "" } : {})
|
|
73
|
+
};
|
|
74
|
+
context.board = project;
|
|
34
75
|
const activeTab = getSettingsTab();
|
|
35
76
|
|
|
36
77
|
const tabs = [
|
|
@@ -40,7 +81,7 @@ export async function renderProjectDetail() {
|
|
|
40
81
|
{ id: "notes", label: "Notes" },
|
|
41
82
|
{ id: "journal", label: "Journal" },
|
|
42
83
|
...(features.ai ? [{ id: "ai", label: "AI" }] : []),
|
|
43
|
-
{ id: "
|
|
84
|
+
{ id: "data", label: "Data" }
|
|
44
85
|
];
|
|
45
86
|
|
|
46
87
|
const resolvedTab = activeTab === "ai" && !features.ai ? "lanes" : activeTab;
|
|
@@ -48,7 +89,6 @@ export async function renderProjectDetail() {
|
|
|
48
89
|
renderDrawerShell({
|
|
49
90
|
eyebrow: "SETTINGS",
|
|
50
91
|
mode: "settings",
|
|
51
|
-
title: project.name,
|
|
52
92
|
tabs,
|
|
53
93
|
activeTab: resolvedTab,
|
|
54
94
|
onTabSelect: async (id) => {
|
|
@@ -78,9 +118,9 @@ function renderProjectTabBody(context, tab) {
|
|
|
78
118
|
return renderProjectJournalTab(context);
|
|
79
119
|
case "archive":
|
|
80
120
|
return renderProjectArchiveTab();
|
|
81
|
-
case "
|
|
121
|
+
case "data":
|
|
82
122
|
default:
|
|
83
|
-
return
|
|
123
|
+
return renderProjectDataTab(context);
|
|
84
124
|
}
|
|
85
125
|
}
|
|
86
126
|
|
|
@@ -143,31 +183,37 @@ const MCP_CLIENTS = {
|
|
|
143
183
|
function renderProjectAiTab(context) {
|
|
144
184
|
const project = context.board;
|
|
145
185
|
const aiEnabled = project.ai_enabled !== 0;
|
|
146
|
-
const aiDivider = aiEnabled ? `<div class="separator" aria-hidden="true"></div>` : "";
|
|
147
186
|
const agentContextSetup = aiEnabled ? renderAgentContextSection(project) : "";
|
|
148
187
|
const helperFilesSetup = aiEnabled && features.multiBoard ? renderHelperFilesSection(project) : "";
|
|
149
188
|
const mcpSetup = aiEnabled ? renderMcpSetupSection(context) : "";
|
|
150
189
|
|
|
151
190
|
return `
|
|
152
|
-
<div class="
|
|
153
|
-
<
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
191
|
+
<div class="settings-stack">
|
|
192
|
+
<section class="settings-card">
|
|
193
|
+
<header class="settings-card-head">
|
|
194
|
+
<h3 class="settings-card-title">AI</h3>
|
|
195
|
+
</header>
|
|
196
|
+
<div class="settings-card-body">
|
|
197
|
+
<label class="orbit-check settings-toggle" for="aiEnabledToggle">
|
|
198
|
+
<input
|
|
199
|
+
type="checkbox"
|
|
200
|
+
id="aiEnabledToggle"
|
|
201
|
+
${aiEnabled ? "checked" : ""}
|
|
202
|
+
/>
|
|
203
|
+
<span class="orbit-check-box" aria-hidden="true">
|
|
204
|
+
<svg viewBox="0 0 16 16" class="orbit-check-tick"><path d="M3.5 8.5l3 3 6-7" /></svg>
|
|
205
|
+
</span>
|
|
206
|
+
<span class="settings-toggle-text">
|
|
207
|
+
<span class="settings-row-title">Enable AI</span>
|
|
208
|
+
<span class="description">Provisions the <strong>AI Ready</strong>, <strong>In Progress</strong>, and <strong>Review</strong> lanes if missing, surfaces agent context fields, and exposes the MCP setup snippet. Disabling AI hides the AI Ready column but keeps its cards in place.</span>
|
|
209
|
+
</span>
|
|
210
|
+
</label>
|
|
211
|
+
</div>
|
|
212
|
+
</section>
|
|
213
|
+
${agentContextSetup}
|
|
214
|
+
${helperFilesSetup}
|
|
215
|
+
${mcpSetup}
|
|
165
216
|
</div>
|
|
166
|
-
${aiDivider}
|
|
167
|
-
${agentContextSetup}
|
|
168
|
-
${helperFilesSetup ? `${aiDivider}${helperFilesSetup}` : ""}
|
|
169
|
-
${aiDivider}
|
|
170
|
-
${mcpSetup}
|
|
171
217
|
`;
|
|
172
218
|
}
|
|
173
219
|
|
|
@@ -179,23 +225,26 @@ function renderAgentContextSection(project) {
|
|
|
179
225
|
const placeholderClass = hasInstructions ? "" : "is-placeholder";
|
|
180
226
|
const inner = hasInstructions ? renderPreservedText(instructions) : escapeHtml(instructionsPlaceholder);
|
|
181
227
|
return `
|
|
182
|
-
<
|
|
183
|
-
<
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
<section class="settings-card ai-context-setup">
|
|
229
|
+
<header class="settings-card-head">
|
|
230
|
+
<h3 class="settings-card-title">Agent Instructions</h3>
|
|
231
|
+
</header>
|
|
232
|
+
<div class="settings-card-body">
|
|
233
|
+
<div class="settings-row">
|
|
234
|
+
<p class="description">This project-level context is provided to agents when they work on tickets.</p>
|
|
235
|
+
<div class="inline-md-field">
|
|
236
|
+
<div
|
|
237
|
+
class="inline-md-field-body preserved-text-body editable-field settings-agent-instructions-body ${placeholderClass}"
|
|
238
|
+
data-edit-field="agent_instructions"
|
|
239
|
+
tabindex="0"
|
|
240
|
+
title="Click to edit"
|
|
241
|
+
role="button"
|
|
242
|
+
aria-label="Edit Agent Instructions"
|
|
243
|
+
>${inner}</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
197
246
|
</div>
|
|
198
|
-
</
|
|
247
|
+
</section>
|
|
199
248
|
`;
|
|
200
249
|
}
|
|
201
250
|
|
|
@@ -207,69 +256,71 @@ function renderMcpSetupSection(context) {
|
|
|
207
256
|
const commandText = mcpRunCommand(osId, paths);
|
|
208
257
|
const client = MCP_CLIENTS[clientId];
|
|
209
258
|
return `
|
|
210
|
-
<details class="
|
|
211
|
-
<summary>
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
<h3>Connect Your Agent to MCP</h3>
|
|
215
|
-
</div>
|
|
259
|
+
<details class="settings-card settings-card--collapsible mcp-setup" data-mcp-setup>
|
|
260
|
+
<summary class="settings-card-head">
|
|
261
|
+
<h3 class="settings-card-title">Connect Your Agent to MCP</h3>
|
|
262
|
+
<svg class="settings-card-chevron" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>
|
|
216
263
|
</summary>
|
|
217
|
-
<div>
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
<
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<div class="mcp-path-grid">
|
|
238
|
-
<label>
|
|
239
|
-
<span>Orbit MCP server file</span>
|
|
240
|
-
<input id="mcpServerPathInput" value="${escapeHtml(paths.serverPath)}" />
|
|
241
|
-
</label>
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
<div class="mcp-options-stack">
|
|
246
|
-
<div class="mcp-option-card">
|
|
247
|
-
<div class="mcp-option-card-head">
|
|
248
|
-
<div>
|
|
249
|
-
<strong data-mcp-output-title>${escapeHtml(client.label)} setup</strong>
|
|
250
|
-
<p class="description" data-mcp-helper>${escapeHtml(client.helper)}</p>
|
|
251
|
-
</div>
|
|
252
|
-
<button type="button" data-variant="secondary" data-copy-mcp="config">Copy setup</button>
|
|
264
|
+
<div class="settings-card-body">
|
|
265
|
+
<div class="settings-row">
|
|
266
|
+
<p class="description">A tiny service running on your computer alongside your AI agent, providing it with tools and context to collaborate with you more effectively. Tell your AI to use the snippet below, it knows what MCP is.</p>
|
|
267
|
+
<div class="control-grid">
|
|
268
|
+
<label class="control-field">
|
|
269
|
+
<span class="control-field-label">Operating system</span>
|
|
270
|
+
<select id="mcpOsSelect" class="select-chevron-field">
|
|
271
|
+
${Object.entries(MCP_OS_OPTIONS)
|
|
272
|
+
.map(([id, option]) => `<option value="${id}" ${id === osId ? "selected" : ""}>${escapeHtml(option.label)}</option>`)
|
|
273
|
+
.join("")}
|
|
274
|
+
</select>
|
|
275
|
+
</label>
|
|
276
|
+
<label class="control-field">
|
|
277
|
+
<span class="control-field-label">Agent app</span>
|
|
278
|
+
<select id="mcpClientSelect" class="select-chevron-field">
|
|
279
|
+
${Object.entries(MCP_CLIENTS)
|
|
280
|
+
.map(([id, option]) => `<option value="${id}" ${id === clientId ? "selected" : ""}>${escapeHtml(option.label)}</option>`)
|
|
281
|
+
.join("")}
|
|
282
|
+
</select>
|
|
283
|
+
</label>
|
|
253
284
|
</div>
|
|
254
|
-
<div class="mcp-
|
|
255
|
-
<
|
|
256
|
-
|
|
285
|
+
<div class="control-grid control-grid--single mcp-path-grid">
|
|
286
|
+
<label class="control-field">
|
|
287
|
+
<span class="control-field-label">Orbit MCP server file</span>
|
|
288
|
+
<input id="mcpServerPathInput" value="${escapeHtml(paths.serverPath)}" />
|
|
289
|
+
</label>
|
|
257
290
|
</div>
|
|
258
291
|
</div>
|
|
259
292
|
|
|
260
|
-
<div class="
|
|
293
|
+
<div class="settings-row">
|
|
294
|
+
<div class="mcp-options-stack">
|
|
295
|
+
<div class="mcp-option-card">
|
|
296
|
+
<div class="mcp-option-card-head">
|
|
297
|
+
<div>
|
|
298
|
+
<strong data-mcp-output-title>${escapeHtml(client.label)} setup</strong>
|
|
299
|
+
<p class="description" data-mcp-helper>${escapeHtml(client.helper)}</p>
|
|
300
|
+
</div>
|
|
301
|
+
<button type="button" data-variant="secondary" data-copy-mcp="config">Copy setup</button>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="mcp-terminal">
|
|
304
|
+
<div class="mcp-terminal-chrome" aria-hidden="true"><span></span><span></span><span></span></div>
|
|
305
|
+
<pre class="mcp-snippet"><code data-mcp-config>${escapeHtml(configText)}</code></pre>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
261
308
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<div>
|
|
265
|
-
<
|
|
266
|
-
|
|
309
|
+
<div class="mcp-or-divider" aria-hidden="true"><span>OR</span></div>
|
|
310
|
+
|
|
311
|
+
<div class="mcp-option-card">
|
|
312
|
+
<div class="mcp-option-card-head">
|
|
313
|
+
<div>
|
|
314
|
+
<strong>Manual run command</strong>
|
|
315
|
+
<p class="description">Use this only when an app asks you to start the MCP server yourself.</p>
|
|
316
|
+
</div>
|
|
317
|
+
<button type="button" data-variant="secondary" data-copy-mcp="command">Copy command</button>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="mcp-terminal">
|
|
320
|
+
<div class="mcp-terminal-chrome" aria-hidden="true"><span></span><span></span><span></span></div>
|
|
321
|
+
<pre class="mcp-snippet mcp-snippet--compact"><code data-mcp-command>${escapeHtml(commandText)}</code></pre>
|
|
322
|
+
</div>
|
|
267
323
|
</div>
|
|
268
|
-
<button type="button" data-variant="secondary" data-copy-mcp="command">Copy command</button>
|
|
269
|
-
</div>
|
|
270
|
-
<div class="mcp-terminal">
|
|
271
|
-
<div class="mcp-terminal-chrome" aria-hidden="true"><span></span><span></span><span></span></div>
|
|
272
|
-
<pre class="mcp-snippet mcp-snippet--compact"><code data-mcp-command>${escapeHtml(commandText)}</code></pre>
|
|
273
324
|
</div>
|
|
274
325
|
</div>
|
|
275
326
|
</div>
|
|
@@ -387,7 +438,7 @@ function renderProjectArchiveTab() {
|
|
|
387
438
|
`;
|
|
388
439
|
}
|
|
389
440
|
|
|
390
|
-
function
|
|
441
|
+
function renderProjectDataTab(context) {
|
|
391
442
|
const project = context.board;
|
|
392
443
|
if (repositoryDelete.boardId !== project.id) {
|
|
393
444
|
repositoryDelete.boardId = project.id;
|
|
@@ -395,58 +446,74 @@ function renderProjectRepositoryTab(context) {
|
|
|
395
446
|
repositoryDelete.exported = false;
|
|
396
447
|
}
|
|
397
448
|
const repoMeta = features.multiBoard
|
|
398
|
-
?
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
449
|
+
? ""
|
|
450
|
+
: `
|
|
451
|
+
<section class="settings-card">
|
|
452
|
+
<header class="settings-card-head">
|
|
453
|
+
<h3 class="settings-card-title">Data</h3>
|
|
454
|
+
</header>
|
|
455
|
+
<div class="settings-card-body">
|
|
456
|
+
<div class="settings-row">
|
|
457
|
+
<p class="description">This board lives in your browser only. Export a snapshot if you want to move it to a real Orbit install, or import one to seed this preview.</p>
|
|
406
458
|
</div>
|
|
407
459
|
</div>
|
|
408
|
-
</
|
|
409
|
-
: `
|
|
410
|
-
<p class="description">This board lives in your browser only. Export a snapshot if you want to move it to a real Orbit install, or import one to seed this preview.</p>`;
|
|
460
|
+
</section>`;
|
|
411
461
|
return `
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
<
|
|
421
|
-
<
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
462
|
+
<div class="settings-stack">
|
|
463
|
+
${renderUpdateCard()}
|
|
464
|
+
|
|
465
|
+
${repoMeta}
|
|
466
|
+
|
|
467
|
+
${features.multiBoard ? renderBoardRenameSection(project) : ""}
|
|
468
|
+
|
|
469
|
+
<section class="settings-card">
|
|
470
|
+
<header class="settings-card-head">
|
|
471
|
+
<h3 class="settings-card-title">Board Snapshot</h3>
|
|
472
|
+
</header>
|
|
473
|
+
<div class="settings-card-body">
|
|
474
|
+
<div class="settings-row">
|
|
475
|
+
<p class="description">Import accepts Orbit snapshots or Trello board JSON exports. The default import creates a separate board and leaves this board untouched.</p>
|
|
476
|
+
${features.attachments ? `
|
|
477
|
+
<label class="orbit-check settings-toggle">
|
|
478
|
+
<input type="checkbox" id="exportProjectImages" />
|
|
479
|
+
<span class="orbit-check-box" aria-hidden="true">
|
|
480
|
+
<svg viewBox="0 0 16 16" class="orbit-check-tick"><path d="M3.5 8.5l3 3 6-7" /></svg>
|
|
481
|
+
</span>
|
|
482
|
+
<span class="settings-toggle-text">
|
|
483
|
+
<span class="settings-row-title">Include attached images</span>
|
|
484
|
+
</span>
|
|
485
|
+
</label>
|
|
486
|
+
` : ""}
|
|
487
|
+
<div class="deployment-actions">
|
|
488
|
+
<button type="button" data-variant="primary" id="exportProject">Export</button>
|
|
489
|
+
<input type="file" id="importSnapshotAsNewFile" class="import-file-input" accept=".orbit.json,.json,application/json" />
|
|
490
|
+
<button type="button" data-variant="primary" id="importSnapshotAsNewButton">Import as New Board</button>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
</section>
|
|
495
|
+
|
|
496
|
+
<section class="settings-card settings-card--danger">
|
|
497
|
+
<header class="settings-card-head">
|
|
498
|
+
<h3 class="settings-card-title">Replace Current Board</h3>
|
|
499
|
+
</header>
|
|
500
|
+
<div class="settings-card-body">
|
|
501
|
+
<div class="settings-row">
|
|
502
|
+
<p class="description">Advanced: replace this board from an Orbit snapshot or Trello export after making the normal pre-import backup. Type <strong>${escapeHtml(project.slug || "")}</strong> to enable replacement.</p>
|
|
503
|
+
<label class="control-field repository-confirm-field">
|
|
504
|
+
<span class="control-field-label">Board slug</span>
|
|
505
|
+
<input id="replaceImportConfirmInput" autocomplete="off" spellcheck="false" placeholder="${escapeHtml(project.slug || "")}" />
|
|
506
|
+
</label>
|
|
507
|
+
<div class="deployment-actions">
|
|
508
|
+
<input type="file" id="replaceCurrentBoardImportFile" class="import-file-input" accept=".orbit.json,.json,application/json" disabled />
|
|
509
|
+
<button type="button" data-variant="primary" data-arc="danger" id="replaceCurrentBoardImportButton" disabled>Replace Current Board</button>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
</section>
|
|
435
514
|
|
|
436
|
-
|
|
437
|
-
<h3>Replace Current Board</h3>
|
|
438
|
-
<p class="description">Advanced: replace this board from an Orbit snapshot or Trello export after making the normal pre-import backup. Type <strong>${escapeHtml(project.slug || "")}</strong> to enable replacement.</p>
|
|
439
|
-
<label class="repository-confirm-field">
|
|
440
|
-
<span>Board slug</span>
|
|
441
|
-
<input id="replaceImportConfirmInput" autocomplete="off" spellcheck="false" placeholder="${escapeHtml(project.slug || "")}" />
|
|
442
|
-
</label>
|
|
443
|
-
<div class="deployment-actions">
|
|
444
|
-
<input type="file" id="replaceCurrentBoardImportFile" class="import-file-input" accept=".orbit.json,.json,application/json" disabled />
|
|
445
|
-
<button type="button" data-variant="secondary" id="replaceCurrentBoardImportButton" disabled>Replace Current Board</button>
|
|
446
|
-
</div>
|
|
515
|
+
${features.multiBoard ? renderDeleteBoardSection(project) : ""}
|
|
447
516
|
</div>
|
|
448
|
-
|
|
449
|
-
${features.multiBoard ? renderDeleteBoardSection(project) : ""}
|
|
450
517
|
`;
|
|
451
518
|
}
|
|
452
519
|
|
|
@@ -454,40 +521,51 @@ function renderHelperFilesSection(project) {
|
|
|
454
521
|
const hasFolder = Boolean(project.system_path);
|
|
455
522
|
const managed = Boolean(project.manage_helper_files);
|
|
456
523
|
return `
|
|
457
|
-
<
|
|
458
|
-
<
|
|
459
|
-
<
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
524
|
+
<section class="settings-card helper-files-section">
|
|
525
|
+
<header class="settings-card-head">
|
|
526
|
+
<h3 class="settings-card-title">AI Helper Files</h3>
|
|
527
|
+
</header>
|
|
528
|
+
<div class="settings-card-body">
|
|
529
|
+
<div class="settings-row">
|
|
530
|
+
<p class="description">Orbit can keep <code>SKILL-ORBIT.md</code> and <code>AGENTS.md</code> in this board's coding project folder so your AI agent knows how to use Orbit.${hasFolder ? "" : " Link a folder to enable this."}</p>
|
|
531
|
+
<div class="folder-picker-field helper-files-folder">
|
|
532
|
+
<input id="helperFolderInput" type="text" readonly placeholder="No folder linked" value="${escapeHtml(project.system_path || "")}" />
|
|
533
|
+
<button type="button" id="helperPickFolderBtn">${hasFolder ? "Change" : "Browse"}</button>
|
|
534
|
+
</div>
|
|
535
|
+
<label class="orbit-check settings-toggle">
|
|
536
|
+
<input type="checkbox" id="manageHelperFilesToggle" ${managed ? "checked" : ""} ${hasFolder ? "" : "disabled"} />
|
|
537
|
+
<span class="orbit-check-box" aria-hidden="true">
|
|
538
|
+
<svg viewBox="0 0 16 16" class="orbit-check-tick"><path d="M3.5 8.5l3 3 6-7" /></svg>
|
|
539
|
+
</span>
|
|
540
|
+
<span class="settings-toggle-text">
|
|
541
|
+
<span class="settings-row-title">Generate & maintain helper files in this folder</span>
|
|
542
|
+
</span>
|
|
543
|
+
</label>
|
|
544
|
+
</div>
|
|
466
545
|
</div>
|
|
467
|
-
|
|
468
|
-
<input type="checkbox" id="manageHelperFilesToggle" ${managed ? "checked" : ""} ${hasFolder ? "" : "disabled"} />
|
|
469
|
-
<span class="orbit-check-box" aria-hidden="true">
|
|
470
|
-
<svg viewBox="0 0 16 16" class="orbit-check-tick"><path d="M3.5 8.5l3 3 6-7" /></svg>
|
|
471
|
-
</span>
|
|
472
|
-
<span>Generate & maintain helper files in this folder</span>
|
|
473
|
-
</label>
|
|
474
|
-
</div>
|
|
546
|
+
</section>
|
|
475
547
|
`;
|
|
476
548
|
}
|
|
477
549
|
|
|
478
550
|
function renderBoardRenameSection(project) {
|
|
479
551
|
return `
|
|
480
|
-
<
|
|
481
|
-
<
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
552
|
+
<section class="settings-card board-rename-section">
|
|
553
|
+
<header class="settings-card-head">
|
|
554
|
+
<h3 class="settings-card-title">Board Name</h3>
|
|
555
|
+
</header>
|
|
556
|
+
<div class="settings-card-body">
|
|
557
|
+
<div class="settings-row">
|
|
558
|
+
<p class="description">Rename the board display name only. The canonical URL slug stays <strong>${escapeHtml(project.slug || "")}</strong>, so existing board links keep working.</p>
|
|
559
|
+
<form id="boardRenameForm" class="field-form">
|
|
560
|
+
<label class="control-field">
|
|
561
|
+
<span class="control-field-label">Display name</span>
|
|
562
|
+
<input name="name" value="${escapeHtml(project.name || "")}" maxlength="120" required />
|
|
563
|
+
</label>
|
|
564
|
+
<button type="submit" data-variant="primary">Rename Board</button>
|
|
565
|
+
</form>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
</section>
|
|
491
569
|
`;
|
|
492
570
|
}
|
|
493
571
|
|
|
@@ -495,15 +573,21 @@ function renderDeleteBoardSection(project) {
|
|
|
495
573
|
const slug = escapeHtml(project.slug || "");
|
|
496
574
|
if (repositoryDelete.step === "choice") {
|
|
497
575
|
return `
|
|
498
|
-
<
|
|
499
|
-
<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
<
|
|
504
|
-
|
|
576
|
+
<section class="settings-card settings-card--danger">
|
|
577
|
+
<header class="settings-card-head">
|
|
578
|
+
<h3 class="settings-card-title">Delete Board</h3>
|
|
579
|
+
</header>
|
|
580
|
+
<div class="settings-card-body">
|
|
581
|
+
<div class="settings-row">
|
|
582
|
+
<p class="description">Delete this board from Orbit and remove its repo-local Orbit files. Export a snapshot first if you may need this board again.</p>
|
|
583
|
+
<div class="repository-delete-actions">
|
|
584
|
+
<button type="button" data-variant="primary" id="deleteBoardExportFirst">Export first</button>
|
|
585
|
+
<button type="button" data-variant="primary" data-arc="danger" id="deleteBoardSkipExport">Delete without exporting</button>
|
|
586
|
+
<button type="button" data-variant="ghost" id="deleteBoardCancel">Cancel</button>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
505
589
|
</div>
|
|
506
|
-
</
|
|
590
|
+
</section>
|
|
507
591
|
`;
|
|
508
592
|
}
|
|
509
593
|
|
|
@@ -512,28 +596,40 @@ function renderDeleteBoardSection(project) {
|
|
|
512
596
|
? "Snapshot exported. Type the board slug to confirm permanent deletion."
|
|
513
597
|
: "Type the board slug to confirm permanent deletion.";
|
|
514
598
|
return `
|
|
515
|
-
<
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
<
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
599
|
+
<section class="settings-card settings-card--danger is-confirming">
|
|
600
|
+
<header class="settings-card-head">
|
|
601
|
+
<h3 class="settings-card-title">Delete Board</h3>
|
|
602
|
+
</header>
|
|
603
|
+
<div class="settings-card-body">
|
|
604
|
+
<div class="settings-row">
|
|
605
|
+
<p class="description">${escapeHtml(exportedCopy)}</p>
|
|
606
|
+
<label class="control-field repository-confirm-field">
|
|
607
|
+
<span class="control-field-label">Board slug</span>
|
|
608
|
+
<strong>${slug}</strong>
|
|
609
|
+
<input id="deleteBoardConfirmInput" autocomplete="off" spellcheck="false" placeholder="${slug}" autofocus />
|
|
610
|
+
</label>
|
|
611
|
+
<div class="repository-delete-actions">
|
|
612
|
+
<button type="button" data-variant="primary" data-arc="danger" id="deleteBoardConfirm" disabled>Delete board permanently</button>
|
|
613
|
+
<button type="button" data-variant="ghost" id="deleteBoardCancel">Cancel</button>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
526
616
|
</div>
|
|
527
|
-
</
|
|
617
|
+
</section>
|
|
528
618
|
`;
|
|
529
619
|
}
|
|
530
620
|
|
|
531
621
|
return `
|
|
532
|
-
<
|
|
533
|
-
<
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
622
|
+
<section class="settings-card settings-card--danger">
|
|
623
|
+
<header class="settings-card-head">
|
|
624
|
+
<h3 class="settings-card-title">Delete Board</h3>
|
|
625
|
+
</header>
|
|
626
|
+
<div class="settings-card-body">
|
|
627
|
+
<div class="settings-row">
|
|
628
|
+
<p class="description">Remove this board from Orbit and delete its repo-local Orbit files. This cannot be undone without an exported snapshot.</p>
|
|
629
|
+
<button type="button" data-variant="primary" data-arc="danger" id="deleteBoardStart">Delete Board</button>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
</section>
|
|
537
633
|
`;
|
|
538
634
|
}
|
|
539
635
|
|
|
@@ -546,9 +642,9 @@ function renderNotesSettingsTab(context) {
|
|
|
546
642
|
const placeholderClass = hasNotes ? "" : "is-placeholder";
|
|
547
643
|
const inner = hasNotes ? renderPreservedText(notes) : escapeHtml(notesPlaceholder);
|
|
548
644
|
return `
|
|
549
|
-
<div class="section
|
|
645
|
+
<div class="section settings-notes-section">
|
|
550
646
|
<div class="inline-md-field">
|
|
551
|
-
<span class="inline-md-field-label">Notes For You</span>
|
|
647
|
+
<span class="inline-md-field-label font-orbitron">Notes For You</span>
|
|
552
648
|
<div
|
|
553
649
|
class="inline-md-field-body preserved-text-body editable-field settings-notes-body ${placeholderClass}"
|
|
554
650
|
data-edit-field="project_notes"
|
|
@@ -566,7 +662,7 @@ function renderProjectLanesTab() {
|
|
|
566
662
|
const showPriority = state.showPriority;
|
|
567
663
|
return `
|
|
568
664
|
<div class="section">
|
|
569
|
-
<label class="orbit-check" for="showPriorityToggle">
|
|
665
|
+
<label class="orbit-check settings-toggle" for="showPriorityToggle">
|
|
570
666
|
<input
|
|
571
667
|
type="checkbox"
|
|
572
668
|
id="showPriorityToggle"
|
|
@@ -575,7 +671,9 @@ function renderProjectLanesTab() {
|
|
|
575
671
|
<span class="orbit-check-box" aria-hidden="true">
|
|
576
672
|
<svg viewBox="0 0 16 16" class="orbit-check-tick"><path d="M3.5 8.5l3 3 6-7" /></svg>
|
|
577
673
|
</span>
|
|
578
|
-
<span class="
|
|
674
|
+
<span class="settings-toggle-text">
|
|
675
|
+
<span class="settings-row-title">Show priority label on cards</span>
|
|
676
|
+
</span>
|
|
579
677
|
</label>
|
|
580
678
|
<p class="description lanes-description">Rename, reorder, add, or remove lanes.</p>
|
|
581
679
|
${renderLaneManager()}
|
|
@@ -587,6 +685,87 @@ function renderProjectLanesTab() {
|
|
|
587
685
|
`;
|
|
588
686
|
}
|
|
589
687
|
|
|
688
|
+
const UPDATE_INSTRUCTIONS = {
|
|
689
|
+
source: "git pull\npnpm install\npnpm run build:bundle\n# then restart Orbit",
|
|
690
|
+
docker: "git pull\ndocker compose build\ndocker compose up -d",
|
|
691
|
+
dist: "npm install -g @starlove/orbit@latest\n# then restart Orbit"
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const UPDATE_MODE_LABEL = {
|
|
695
|
+
source: "local / server",
|
|
696
|
+
docker: "Docker",
|
|
697
|
+
dist: "npm"
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const updateCheckGlyph = `<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0L4.3 10.7a1 1 0 1 1 1.4-1.4l2.8 2.8 6.8-6.8a1 1 0 0 1 1.4 0z" clip-rule="evenodd"/></svg>`;
|
|
701
|
+
|
|
702
|
+
// "Version & Updates" card (top of the Data tab). Reads the cached /api/version
|
|
703
|
+
// payload (populated by the boot-time check in update-check.js); renders
|
|
704
|
+
// neutrally when it's missing (preview edition / check disabled). The whole
|
|
705
|
+
// card gains an accent when an update is available so it reads at a glance.
|
|
706
|
+
function renderUpdateCard() {
|
|
707
|
+
const info = getUpdateInfo();
|
|
708
|
+
const mode = info?.deployMode || "source";
|
|
709
|
+
const available = Boolean(info?.updateAvailable);
|
|
710
|
+
const current = info?.current ? `v${escapeHtml(info.current)}` : "—";
|
|
711
|
+
const showSelfUpdate = Boolean(info?.selfUpdate && available);
|
|
712
|
+
|
|
713
|
+
const chip = !info
|
|
714
|
+
? `<span class="update-status-chip update-status-chip--idle">Not checked</span>`
|
|
715
|
+
: info.error
|
|
716
|
+
? `<span class="update-status-chip update-status-chip--warn">Couldn't check</span>`
|
|
717
|
+
: available
|
|
718
|
+
? `<span class="update-status-chip update-status-chip--available"><span class="update-status-dot" aria-hidden="true"></span>Update available</span>`
|
|
719
|
+
: `<span class="update-status-chip update-status-chip--current">${updateCheckGlyph} Up to date</span>`;
|
|
720
|
+
|
|
721
|
+
const statusLine = !info
|
|
722
|
+
? "Check whether a newer version of Orbit has shipped."
|
|
723
|
+
: info.error
|
|
724
|
+
? `Couldn't reach GitHub (${escapeHtml(info.error)}). Try again in a moment.`
|
|
725
|
+
: available
|
|
726
|
+
? "Back up your boards, then update with the steps below."
|
|
727
|
+
: "You have the latest version of Orbit.";
|
|
728
|
+
|
|
729
|
+
const versionBlock = available
|
|
730
|
+
? `<span class="update-version-now">${current}</span>
|
|
731
|
+
<span class="update-version-arrow" aria-hidden="true">→</span>
|
|
732
|
+
<span class="update-version-next">v${escapeHtml(info.latest)}</span>`
|
|
733
|
+
: `<span class="update-version-now">${current}</span>`;
|
|
734
|
+
|
|
735
|
+
const howto = available
|
|
736
|
+
? `
|
|
737
|
+
<details class="update-howto" open>
|
|
738
|
+
<summary>How to update <span class="update-howto-mode">${escapeHtml(UPDATE_MODE_LABEL[mode] || mode)}</span></summary>
|
|
739
|
+
<pre class="update-commands"><code id="updateCommands">${escapeHtml(UPDATE_INSTRUCTIONS[mode] || UPDATE_INSTRUCTIONS.source)}</code></pre>
|
|
740
|
+
<div class="update-howto-foot">
|
|
741
|
+
<button type="button" data-variant="ghost" id="updateCopyCommands">Copy commands</button>
|
|
742
|
+
${info.changesUrl ? `<a class="update-changes-link" href="${escapeHtml(info.changesUrl)}" target="_blank" rel="noopener">View changes ↗</a>` : ""}
|
|
743
|
+
</div>
|
|
744
|
+
</details>`
|
|
745
|
+
: "";
|
|
746
|
+
|
|
747
|
+
return `
|
|
748
|
+
<section class="settings-card update-card${available ? " update-card--available" : ""}">
|
|
749
|
+
<header class="settings-card-head">
|
|
750
|
+
<h3 class="settings-card-title">Version & Updates</h3>
|
|
751
|
+
${chip}
|
|
752
|
+
</header>
|
|
753
|
+
<div class="settings-card-body">
|
|
754
|
+
<div class="settings-row update-row">
|
|
755
|
+
<div class="update-version-line">${versionBlock}</div>
|
|
756
|
+
<p class="description" id="updateStatusLine">${statusLine}</p>
|
|
757
|
+
<div class="deployment-actions update-actions">
|
|
758
|
+
<button type="button" data-variant="${available ? "secondary" : "primary"}" id="updateCheckNow">Check now</button>
|
|
759
|
+
<button type="button" data-variant="secondary" id="updateBackupBoards">Back up all boards</button>
|
|
760
|
+
${showSelfUpdate ? `<button type="button" data-variant="primary" id="updateRunNow">Update now</button>` : ""}
|
|
761
|
+
</div>
|
|
762
|
+
${howto}
|
|
763
|
+
<p class="description update-docs-hint">Full guide: <a href="https://github.com/brucestarlove/ss-orbit/blob/master/docs/UPDATING.md" target="_blank" rel="noopener">docs/UPDATING.md</a></p>
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
</section>`;
|
|
767
|
+
}
|
|
768
|
+
|
|
590
769
|
function renderProjectAppearanceTab() {
|
|
591
770
|
const theme = currentTheme();
|
|
592
771
|
const reduceMotion = effectiveReducedMotionPreference();
|
|
@@ -597,58 +776,114 @@ function renderProjectAppearanceTab() {
|
|
|
597
776
|
: "Using your saved preference.";
|
|
598
777
|
|
|
599
778
|
return `
|
|
600
|
-
<div class="
|
|
601
|
-
<
|
|
602
|
-
<
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
779
|
+
<div class="settings-stack appearance-settings">
|
|
780
|
+
<section class="settings-card">
|
|
781
|
+
<header class="settings-card-head">
|
|
782
|
+
<h3 class="settings-card-title">Theme</h3>
|
|
783
|
+
</header>
|
|
784
|
+
<div class="settings-card-body">
|
|
785
|
+
<div class="settings-toggle">
|
|
786
|
+
<button
|
|
787
|
+
type="button"
|
|
788
|
+
class="topbar-btn topbar-ctl-shrink"
|
|
789
|
+
data-variant="theme"
|
|
790
|
+
id="appearanceThemeToggle"
|
|
791
|
+
aria-label="Toggle theme"
|
|
792
|
+
>
|
|
793
|
+
<svg class="topbar-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">${themeIconMarkup(theme)}</svg>
|
|
794
|
+
</button>
|
|
795
|
+
<span class="settings-toggle-text">
|
|
796
|
+
<span class="settings-row-title" id="appearanceThemeLabel">${theme === "dark" ? "Dark" : "Light"}</span>
|
|
797
|
+
<span class="description">Tap to switch between the light and dark theme used across the app.</span>
|
|
798
|
+
</span>
|
|
799
|
+
</div>
|
|
800
|
+
<div class="settings-toggle">
|
|
801
|
+
<button
|
|
802
|
+
type="button"
|
|
803
|
+
class="topbar-btn topbar-ctl-shrink motion-toggle"
|
|
804
|
+
data-variant="theme"
|
|
805
|
+
id="appearanceMotionToggle"
|
|
806
|
+
aria-pressed="${reduceMotion ? "true" : "false"}"
|
|
807
|
+
aria-label="Toggle reduced theme motion"
|
|
808
|
+
>
|
|
809
|
+
${motionIconMarkup()}
|
|
810
|
+
</button>
|
|
811
|
+
<span class="settings-toggle-text">
|
|
812
|
+
<span class="settings-row-title">Reduce theme motion</span>
|
|
813
|
+
<span class="description">Turns off the animated starfield, meteors, and theme animations. ${escapeHtml(sourceLabel)}</span>
|
|
814
|
+
</span>
|
|
815
|
+
</div>
|
|
606
816
|
</div>
|
|
607
|
-
|
|
608
|
-
|
|
817
|
+
</section>
|
|
818
|
+
${renderTypographyControls()}
|
|
609
819
|
</div>
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
820
|
+
`;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function renderTypographyControls() {
|
|
824
|
+
const prefs = storedFontPreferences();
|
|
825
|
+
const siteOptions = SITE_FONT_OPTIONS
|
|
826
|
+
.map((id) => `<option value="${id}"${id === prefs.site ? " selected" : ""}>${escapeHtml(FONT_OPTIONS[id]?.label || id)}</option>`)
|
|
827
|
+
.join("");
|
|
828
|
+
const overrideSelects = Object.entries(FONT_TARGETS).map(([target, config]) => {
|
|
829
|
+
const current = prefs[target];
|
|
830
|
+
const options = Object.entries(FONT_OPTIONS)
|
|
831
|
+
.map(([id, font]) => `<option value="${id}"${id === current ? " selected" : ""}>${escapeHtml(font.label)}</option>`)
|
|
832
|
+
.join("");
|
|
833
|
+
return `
|
|
834
|
+
<label class="control-field">
|
|
835
|
+
<span class="control-field-label">${escapeHtml(config.label)}</span>
|
|
836
|
+
<select class="select-chevron-field" data-font-target="${target}">${options}</select>
|
|
621
837
|
</label>
|
|
622
|
-
|
|
623
|
-
|
|
838
|
+
`;
|
|
839
|
+
}).join("");
|
|
840
|
+
|
|
841
|
+
return `
|
|
842
|
+
<section class="settings-card appearance-typography-card">
|
|
843
|
+
<header class="settings-card-head">
|
|
844
|
+
<h3 class="settings-card-title">Typography</h3>
|
|
845
|
+
</header>
|
|
846
|
+
<div class="settings-card-body">
|
|
847
|
+
<div class="settings-row settings-slider-row">
|
|
848
|
+
<span class="control-field-label">Text size</span>
|
|
849
|
+
<input type="range" class="slider" id="fontScaleSlider" min="${USER_SCALE_MIN}" max="${USER_SCALE_MAX}" step="${USER_SCALE_STEP}" value="${prefs.userScale}" aria-label="Text size" />
|
|
850
|
+
<output id="fontScaleValue" class="control-value">${Math.round(prefs.userScale * 100)}%</output>
|
|
851
|
+
</div>
|
|
852
|
+
<div class="settings-row">
|
|
853
|
+
<label class="control-field">
|
|
854
|
+
<span class="control-field-label">Primary Font</span>
|
|
855
|
+
<select class="select-chevron-field" id="siteFontSelect">${siteOptions}</select>
|
|
856
|
+
</label>
|
|
857
|
+
</div>
|
|
858
|
+
<div class="settings-row">
|
|
859
|
+
<div class="control-grid typography-overrides">${overrideSelects}</div>
|
|
860
|
+
</div>
|
|
861
|
+
</div>
|
|
862
|
+
</section>
|
|
624
863
|
`;
|
|
625
864
|
}
|
|
626
865
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
866
|
+
// Mirrors the topbar theme toggle: moon when dark is active, sun when light.
|
|
867
|
+
function themeIconMarkup(theme) {
|
|
868
|
+
return theme === "dark"
|
|
630
869
|
? '<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />'
|
|
631
870
|
: '<circle cx="12" cy="12" r="5" /><path d="M12 1v2m0 18v2M4.2 4.2l1.4 1.4m12.8 12.8l1.4 1.4M1 12h2m18 0h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />';
|
|
871
|
+
}
|
|
632
872
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
>${icon}</svg>
|
|
648
|
-
</span>
|
|
649
|
-
<span class="appearance-theme-option-label">${label}</span>
|
|
650
|
-
</label>
|
|
651
|
-
`;
|
|
873
|
+
// Twinkling starfield for the motion toggle. Stars shimmer (CSS) while motion
|
|
874
|
+
// is allowed and freeze when the button is pressed (reduced). Each star's
|
|
875
|
+
// --d staggers its animation so the field reads as alive, not pulsing in unison.
|
|
876
|
+
function motionIconMarkup() {
|
|
877
|
+
const stars = [
|
|
878
|
+
{ cx: 6, cy: 7, r: 1.1, d: "0s" },
|
|
879
|
+
{ cx: 17.5, cy: 6, r: 1.5, d: "0.5s" },
|
|
880
|
+
{ cx: 12, cy: 12, r: 1, d: "0.9s" },
|
|
881
|
+
{ cx: 7.5, cy: 17, r: 1.4, d: "0.25s" },
|
|
882
|
+
{ cx: 18, cy: 16.5, r: 1.1, d: "0.7s" }
|
|
883
|
+
]
|
|
884
|
+
.map((s) => `<circle class="motion-star" cx="${s.cx}" cy="${s.cy}" r="${s.r}" style="animation-delay:${s.d}" />`)
|
|
885
|
+
.join("");
|
|
886
|
+
return `<svg class="topbar-icon motion-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true">${stars}</svg>`;
|
|
652
887
|
}
|
|
653
888
|
|
|
654
889
|
function renderProjectJournalTab(context) {
|
|
@@ -656,8 +891,9 @@ function renderProjectJournalTab(context) {
|
|
|
656
891
|
return `
|
|
657
892
|
${aiEnabled ? `<p class="description">Agent-facing project memory: decisions to follow and lessons to apply. Struck entries stay visible here but are excluded from agent context.</p>` : ""}
|
|
658
893
|
<div class="section">
|
|
659
|
-
${renderProjectEntries(context.entries)}
|
|
894
|
+
${renderProjectEntries(context.entries, aiEnabled)}
|
|
660
895
|
<form id="projectEntryForm" class="comment-form project-entry-form">
|
|
896
|
+
<div class="project-entry-form-header">Add entry</div>
|
|
661
897
|
<select name="type" class="select-chevron-field" aria-label="Entry type">
|
|
662
898
|
<option value="decision">Decision</option>
|
|
663
899
|
<option value="lesson">Lesson</option>
|
|
@@ -685,7 +921,7 @@ function parseImportSnapshotText(text) {
|
|
|
685
921
|
function bindProjectTabHandlers(context, tab) {
|
|
686
922
|
const project = context.board;
|
|
687
923
|
|
|
688
|
-
if (tab === "
|
|
924
|
+
if (tab === "data") {
|
|
689
925
|
const exportSnapshot = async (includeAttachments = Boolean($("#exportProjectImages")?.checked)) => {
|
|
690
926
|
const suffix = includeAttachments ? "?include_attachments=true" : "";
|
|
691
927
|
const snapshot = await api(`/api/boards/${encodeURIComponent(project.id)}/export${suffix}`);
|
|
@@ -824,6 +1060,60 @@ function bindProjectTabHandlers(context, tab) {
|
|
|
824
1060
|
toast.success(`Deleted board: ${project.name}`);
|
|
825
1061
|
navigate({ boardId: state.boardId, view: "board" }, { replace: true });
|
|
826
1062
|
});
|
|
1063
|
+
|
|
1064
|
+
// Version & Updates card (top of the Data tab).
|
|
1065
|
+
$("#updateCheckNow")?.addEventListener("click", async () => {
|
|
1066
|
+
const btn = $("#updateCheckNow");
|
|
1067
|
+
btn.disabled = true;
|
|
1068
|
+
btn.textContent = "Checking…";
|
|
1069
|
+
await fetchVersionInfo({ force: true });
|
|
1070
|
+
syncUpdateBadge();
|
|
1071
|
+
state.detailMode = "settings";
|
|
1072
|
+
await renderProjectDetail();
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
$("#updateBackupBoards")?.addEventListener("click", async () => {
|
|
1076
|
+
const btn = $("#updateBackupBoards");
|
|
1077
|
+
const orig = btn.textContent;
|
|
1078
|
+
btn.disabled = true;
|
|
1079
|
+
btn.textContent = "Backing up…";
|
|
1080
|
+
try {
|
|
1081
|
+
await exportAllBoards();
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
toast.error(`Backup failed: ${error?.message || "request failed"}`);
|
|
1084
|
+
} finally {
|
|
1085
|
+
btn.disabled = false;
|
|
1086
|
+
btn.textContent = orig;
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
$("#updateCopyCommands")?.addEventListener("click", async () => {
|
|
1091
|
+
const text = $("#updateCommands")?.textContent || "";
|
|
1092
|
+
try {
|
|
1093
|
+
await navigator.clipboard.writeText(text);
|
|
1094
|
+
toast.success("Update commands copied");
|
|
1095
|
+
} catch {
|
|
1096
|
+
toast.error("Couldn't copy — select the commands manually");
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
$("#updateRunNow")?.addEventListener("click", async () => {
|
|
1101
|
+
const ok = window.confirm(
|
|
1102
|
+
"Update Orbit now? It will pull the latest code, rebuild, and restart. Back up your boards first."
|
|
1103
|
+
);
|
|
1104
|
+
if (!ok) return;
|
|
1105
|
+
const btn = $("#updateRunNow");
|
|
1106
|
+
btn.disabled = true;
|
|
1107
|
+
btn.textContent = "Updating…";
|
|
1108
|
+
try {
|
|
1109
|
+
await api("/api/update", { method: "POST" });
|
|
1110
|
+
toast.success("Update started — Orbit will restart shortly. Reload in a minute.", 9000);
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
toast.error(`Update failed to start: ${error?.message || "request failed"}`);
|
|
1113
|
+
btn.disabled = false;
|
|
1114
|
+
btn.textContent = "Update now";
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
827
1117
|
}
|
|
828
1118
|
|
|
829
1119
|
if (tab === "notes") {
|
|
@@ -895,21 +1185,54 @@ function bindProjectTabHandlers(context, tab) {
|
|
|
895
1185
|
}
|
|
896
1186
|
|
|
897
1187
|
if (tab === "appearance") {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1188
|
+
const themeToggleBtn = $("#appearanceThemeToggle");
|
|
1189
|
+
if (themeToggleBtn) {
|
|
1190
|
+
const themeLabel = $("#appearanceThemeLabel");
|
|
1191
|
+
const themeIcon = themeToggleBtn.querySelector(".topbar-icon");
|
|
1192
|
+
themeToggleBtn.addEventListener("click", () => {
|
|
1193
|
+
const theme = toggleThemePreference();
|
|
1194
|
+
if (themeIcon) themeIcon.innerHTML = themeIconMarkup(theme);
|
|
1195
|
+
if (themeLabel) themeLabel.textContent = theme === "dark" ? "Dark" : "Light";
|
|
902
1196
|
toast.success(theme === "dark" ? "Dark theme enabled" : "Light theme enabled");
|
|
903
1197
|
});
|
|
904
|
-
}
|
|
1198
|
+
}
|
|
905
1199
|
|
|
906
|
-
$("#
|
|
907
|
-
|
|
1200
|
+
const motionToggleBtn = $("#appearanceMotionToggle");
|
|
1201
|
+
motionToggleBtn?.addEventListener("click", () => {
|
|
1202
|
+
const reduce = motionToggleBtn.getAttribute("aria-pressed") !== "true";
|
|
1203
|
+
motionToggleBtn.setAttribute("aria-pressed", reduce ? "true" : "false");
|
|
908
1204
|
setReducedMotionPreference(reduce);
|
|
909
1205
|
applyReducedMotionPreference();
|
|
910
1206
|
window.dispatchEvent(new CustomEvent("orbit:motion-preference-change", { detail: { reduce } }));
|
|
911
1207
|
toast.success(reduce ? "Theme motion reduced" : "Theme motion enabled");
|
|
912
1208
|
});
|
|
1209
|
+
|
|
1210
|
+
// Primary font swap resets every override, so reflect that in the category
|
|
1211
|
+
// selects without re-fetching/re-rendering the whole drawer.
|
|
1212
|
+
$("#siteFontSelect")?.addEventListener("change", (event) => {
|
|
1213
|
+
const next = setSiteFont(event.currentTarget.value);
|
|
1214
|
+
drawerInner.querySelectorAll("select[data-font-target]").forEach((select) => {
|
|
1215
|
+
select.value = next[select.dataset.fontTarget];
|
|
1216
|
+
});
|
|
1217
|
+
toast.success(`Primary font: ${FONT_OPTIONS[next.site]?.label || "updated"}`);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
const fontScaleSlider = $("#fontScaleSlider");
|
|
1221
|
+
if (fontScaleSlider) {
|
|
1222
|
+
const scaleValue = $("#fontScaleValue");
|
|
1223
|
+
fontScaleSlider.addEventListener("input", (event) => {
|
|
1224
|
+
const next = setUserScale(event.currentTarget.value);
|
|
1225
|
+
if (scaleValue) scaleValue.textContent = `${Math.round(next.userScale * 100)}%`;
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
bindSliders(drawerInner);
|
|
1230
|
+
|
|
1231
|
+
drawerInner.querySelectorAll("select[data-font-target]").forEach((select) => {
|
|
1232
|
+
select.addEventListener("change", (event) => {
|
|
1233
|
+
setFontPreference(event.currentTarget.dataset.fontTarget, event.currentTarget.value);
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
913
1236
|
}
|
|
914
1237
|
|
|
915
1238
|
if (tab === "ai") {
|
|
@@ -1087,7 +1410,7 @@ function bindProjectTabHandlers(context, tab) {
|
|
|
1087
1410
|
const id = deleteBtn.dataset.archiveDelete;
|
|
1088
1411
|
if (!confirm(`Permanently delete "${cardTitle}"? This cannot be undone.`)) return;
|
|
1089
1412
|
await api(withBoardQuery(`/api/tickets/${encodeURIComponent(id)}`), { method: "DELETE" });
|
|
1090
|
-
toast.
|
|
1413
|
+
toast.removed(`Deleted: ${cardTitle}`);
|
|
1091
1414
|
await refresh();
|
|
1092
1415
|
}
|
|
1093
1416
|
});
|
|
@@ -1138,33 +1461,41 @@ async function copyText(value) {
|
|
|
1138
1461
|
textArea.remove();
|
|
1139
1462
|
}
|
|
1140
1463
|
|
|
1141
|
-
function renderProjectEntries(entries = []) {
|
|
1464
|
+
function renderProjectEntries(entries = [], aiEnabled = false) {
|
|
1142
1465
|
if (!entries.length) return `<p class="description">No project entries yet.</p>`;
|
|
1143
1466
|
return entries
|
|
1144
1467
|
.map(
|
|
1145
1468
|
(entry) => {
|
|
1146
1469
|
const isStruck = Boolean(entry.struck_at);
|
|
1470
|
+
const showStruckState = aiEnabled && isStruck;
|
|
1147
1471
|
return `
|
|
1148
|
-
<div class="comment project-entry ${escapeHtml(entry.type)} ${
|
|
1149
|
-
<div class="comment-
|
|
1150
|
-
<div>
|
|
1151
|
-
<
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
<button
|
|
1157
|
-
type="button"
|
|
1158
|
-
data-variant="ghost"
|
|
1159
|
-
class="project-entry-strike"
|
|
1160
|
-
data-entry-action="toggle-struck"
|
|
1161
|
-
data-entry-id="${escapeHtml(entry.id)}"
|
|
1162
|
-
data-struck="${isStruck ? "true" : "false"}"
|
|
1163
|
-
aria-pressed="${isStruck ? "true" : "false"}"
|
|
1164
|
-
>${isStruck ? "Restore" : "Strike"}</button>
|
|
1472
|
+
<div class="comment project-entry ${escapeHtml(entry.type)} ${showStruckState ? "is-struck" : ""}">
|
|
1473
|
+
<div class="comment-head">
|
|
1474
|
+
<div class="comment-head-main">
|
|
1475
|
+
<div class="comment-head-title">
|
|
1476
|
+
<span class="entry-type-pill ${escapeHtml(entry.type)}">${escapeHtml(entry.type)}</span>
|
|
1477
|
+
<strong class="entry-title">${escapeHtml(entry.title)}</strong>
|
|
1478
|
+
</div>
|
|
1479
|
+
${showStruckState ? `<span class="project-entry-status">Excluded from agent context</span>` : ""}
|
|
1165
1480
|
</div>
|
|
1166
1481
|
</div>
|
|
1167
1482
|
<div class="comment-body">${escapeHtml(entry.body)}</div>
|
|
1483
|
+
<div class="comment-footer">
|
|
1484
|
+
<span class="comment-footer-meta">${escapeHtml(entry.created_by)} · ${formatDate(entry.created_at)}</span>
|
|
1485
|
+
${
|
|
1486
|
+
aiEnabled
|
|
1487
|
+
? `<button
|
|
1488
|
+
type="button"
|
|
1489
|
+
data-variant="ghost"
|
|
1490
|
+
class="project-entry-strike detail-action-btn"
|
|
1491
|
+
data-entry-action="toggle-struck"
|
|
1492
|
+
data-entry-id="${escapeHtml(entry.id)}"
|
|
1493
|
+
data-struck="${isStruck ? "true" : "false"}"
|
|
1494
|
+
aria-pressed="${isStruck ? "true" : "false"}"
|
|
1495
|
+
>${isStruck ? "Restore" : "Strike"}</button>`
|
|
1496
|
+
: ""
|
|
1497
|
+
}
|
|
1498
|
+
</div>
|
|
1168
1499
|
</div>
|
|
1169
1500
|
`;
|
|
1170
1501
|
}
|
|
@@ -1231,7 +1562,7 @@ function renderLaneManager() {
|
|
|
1231
1562
|
<span class="lane-new-sun" aria-hidden="true"></span>
|
|
1232
1563
|
<span class="lane-new-label">New</span>
|
|
1233
1564
|
</button>
|
|
1234
|
-
<button type="button" data-variant="ghost" class="settings-lane-delete-btn danger-button" data-lane-action="delete" ${deleteDisabled(lane, counts, aiEnabled) ? "disabled" : ""} title="${deleteTitle(lane, counts, aiEnabled)}" aria-label="Delete lane"
|
|
1565
|
+
<button type="button" data-variant="ghost" class="settings-lane-delete-btn danger-button" data-lane-action="delete" ${deleteDisabled(lane, counts, aiEnabled) ? "disabled" : ""} title="${deleteTitle(lane, counts, aiEnabled)}" aria-label="Delete lane">${closeIconSvg}</button>
|
|
1235
1566
|
</div>
|
|
1236
1567
|
`;
|
|
1237
1568
|
})
|