diff --git a/sitecompanion/content.js b/sitecompanion/content.js index f9d2e2b..1a7643f 100644 --- a/sitecompanion/content.js +++ b/sitecompanion/content.js @@ -47,12 +47,55 @@ function resolveScopedItems(parentItems, localItems, disabledNames) { return [...inherited, ...localEnabled]; } -function createToolbar(shortcuts, position = "bottom-right") { +function resolveThemeValue(globalTheme, workspace, site) { + const siteTheme = site?.theme; + if (siteTheme && siteTheme !== "inherit") return siteTheme; + const workspaceTheme = workspace?.theme; + if (workspaceTheme && workspaceTheme !== "inherit") return workspaceTheme; + return globalTheme || "system"; +} + +function resolveThemeMode(theme) { + if (theme === "dark" || theme === "light") return theme; + if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { + return "dark"; + } + return "light"; +} + +function getToolbarThemeTokens(mode) { + if (mode === "dark") { + return { + ink: "#abb2bf", + muted: "#8b93a5", + accent: "#61afef", + accentDeep: "#56b6c2", + panel: "#2f343f", + border: "#3e4451", + glow: "rgba(97, 175, 239, 0.2)", + shadow: "rgba(0, 0, 0, 0.35)" + }; + } + return { + ink: "#1f1a17", + muted: "#6b5f55", + accent: "#b14d2b", + accentDeep: "#7d321b", + panel: "#fff7ec", + border: "#e4d6c5", + glow: "rgba(177, 77, 43, 0.18)", + shadow: "rgba(122, 80, 47, 0.12)" + }; +} + +function createToolbar(shortcuts, position = "bottom-right", themeMode = "light") { let toolbar = document.getElementById("sitecompanion-toolbar"); if (toolbar) toolbar.remove(); toolbar = document.createElement("div"); toolbar.id = "sitecompanion-toolbar"; + + const tokens = getToolbarThemeTokens(themeMode); let posStyle = ""; switch (position) { @@ -77,35 +120,38 @@ function createToolbar(shortcuts, position = "bottom-right") { toolbar.style.cssText = ` position: fixed; ${posStyle} - background: #fff7ec; - border: 1px solid #e4d6c5; + background: ${tokens.panel}; + border: 1px solid ${tokens.border}; border-radius: 12px; padding: 8px; - box-shadow: 0 8px 24px rgba(0,0,0,0.15); - z-index: 999999; + box-shadow: 0 12px 30px ${tokens.shadow}; + z-index: 2147483647; display: flex; gap: 8px; - font-family: system-ui, sans-serif; + font-family: system-ui, -apple-system, "Segoe UI", sans-serif; + color: ${tokens.ink}; `; if (!shortcuts || !shortcuts.length) { const label = document.createElement("span"); label.textContent = "SiteCompanion"; label.style.fontSize = "12px"; - label.style.color = "#6b5f55"; + label.style.color = tokens.muted; toolbar.appendChild(label); } else { for (const shortcut of shortcuts) { const btn = document.createElement("button"); + btn.type = "button"; btn.textContent = shortcut.name; btn.style.cssText = ` padding: 6px 12px; - background: #b14d2b; - color: white; - border: none; - border-radius: 8px; + background: ${tokens.accent}; + color: #fff9f3; + border: 1px solid ${tokens.accent}; + border-radius: 10px; cursor: pointer; font-size: 12px; + box-shadow: 0 8px 20px ${tokens.glow}; `; btn.addEventListener("click", () => { chrome.runtime.sendMessage({ type: "RUN_SHORTCUT", shortcutId: shortcut.id }); @@ -138,24 +184,35 @@ function matchUrl(url, pattern) { +let suppressObserver = false; + async function refreshToolbar() { + suppressObserver = true; let { sites = [], workspaces = [], shortcuts = [], presets = [], - toolbarPosition = "bottom-right" + toolbarPosition = "bottom-right", + theme = "system" } = await chrome.storage.local.get([ "sites", "workspaces", "shortcuts", "presets", - "toolbarPosition" + "toolbarPosition", + "theme" ]); const currentUrl = window.location.href; const site = sites.find(s => matchUrl(currentUrl, s.urlPattern)); - if (site) { + try { + if (!site) { + const toolbar = document.getElementById("sitecompanion-toolbar"); + if (toolbar) toolbar.remove(); + return; + } + if (!shortcuts.length && Array.isArray(presets) && presets.length) { shortcuts = presets; await chrome.storage.local.set({ shortcuts }); @@ -181,21 +238,33 @@ async function refreshToolbar() { : workspace?.toolbarPosition && workspace.toolbarPosition !== "inherit" ? workspace.toolbarPosition : toolbarPosition; - createToolbar(siteShortcuts, resolvedPosition); + const resolvedTheme = resolveThemeValue(theme, workspace, site); + const themeMode = resolveThemeMode(resolvedTheme); + createToolbar(siteShortcuts, resolvedPosition, themeMode); + } finally { + window.setTimeout(() => { + suppressObserver = false; + }, 0); } } +let refreshTimer = null; +function scheduleToolbarRefresh() { + if (refreshTimer) return; + refreshTimer = window.setTimeout(() => { + refreshTimer = null; + void refreshToolbar(); + }, 200); +} + const observer = new MutationObserver(() => { - - // refreshToolbar(); // Debounce this? - + if (suppressObserver) return; + scheduleToolbarRefresh(); }); - - -// observer.observe(document.documentElement, { childList: true, subtree: true }); +observer.observe(document.documentElement, { childList: true, subtree: true }); chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (!message || typeof message !== "object") return; diff --git a/sitecompanion/popup.html b/sitecompanion/popup.html index c57e419..4502634 100644 --- a/sitecompanion/popup.html +++ b/sitecompanion/popup.html @@ -28,6 +28,10 @@

Review extracted text:

+
+ + +
diff --git a/sitecompanion/popup.js b/sitecompanion/popup.js index f6b89e8..ed8420d 100644 --- a/sitecompanion/popup.js +++ b/sitecompanion/popup.js @@ -18,6 +18,7 @@ const SHORTCUT_RUN_KEY = "runShortcutId"; const LAST_TASK_KEY = "lastSelectedTaskId"; const LAST_ENV_KEY = "lastSelectedEnvId"; const LAST_PROFILE_KEY = "lastSelectedProfileId"; +const POPUP_DRAFT_KEY = "popupDraft"; const unknownSiteState = document.getElementById("unknownSiteState"); const extractionReviewState = document.getElementById("extractionReviewState"); @@ -25,6 +26,7 @@ const normalExecutionState = document.getElementById("normalExecutionState"); const partialTextPaste = document.getElementById("partialTextPaste"); const extractFullBtn = document.getElementById("extractFullBtn"); const extractedPreview = document.getElementById("extractedPreview"); +const siteNameInput = document.getElementById("siteNameInput"); const urlPatternInput = document.getElementById("urlPatternInput"); const retryExtractBtn = document.getElementById("retryExtractBtn"); const confirmSiteBtn = document.getElementById("confirmSiteBtn"); @@ -44,6 +46,9 @@ const state = { outputRaw: "", autoRunPending: false, shortcutRunPending: false, + currentPopupState: "unknown", + globalTheme: "system", + forcedTask: null, selectedTaskId: "", selectedEnvId: "", selectedProfileId: "" @@ -53,6 +58,7 @@ async function switchState(stateName) { unknownSiteState.classList.add("hidden"); extractionReviewState.classList.add("hidden"); normalExecutionState.classList.add("hidden"); + state.currentPopupState = stateName; if (stateName === "unknown") { unknownSiteState.classList.remove("hidden"); @@ -64,6 +70,37 @@ async function switchState(stateName) { await chrome.storage.local.set({ lastPopupState: stateName }); } +function buildPopupDraft() { + return { + state: state.currentPopupState, + siteText: state.siteText || "", + urlPattern: urlPatternInput?.value?.trim() || "", + siteName: siteNameInput?.value?.trim() || "" + }; +} + +async function persistPopupDraft() { + await chrome.storage.local.set({ [POPUP_DRAFT_KEY]: buildPopupDraft() }); +} + +async function clearPopupDraft() { + await chrome.storage.local.remove(POPUP_DRAFT_KEY); +} + +function applyPopupDraft(draft) { + if (!draft || typeof draft !== "object") return; + if (draft.siteText) { + state.siteText = draft.siteText; + extractedPreview.textContent = state.siteText; + } + if (typeof draft.urlPattern === "string") { + urlPatternInput.value = draft.urlPattern; + } + if (typeof draft.siteName === "string") { + siteNameInput.value = draft.siteName; + } +} + function matchUrl(url, pattern) { if (!pattern) return false; let regex = null; @@ -112,6 +149,20 @@ function resolveScopedItems(parentItems, localItems, disabledNames) { return [...inherited, ...localEnabled]; } +function findTaskById(taskId, globalTasks, workspace, site) { + if (!taskId) return null; + const lists = [ + Array.isArray(site?.tasks) ? site.tasks : [], + Array.isArray(workspace?.tasks) ? workspace.tasks : [], + Array.isArray(globalTasks) ? globalTasks : [] + ]; + for (const list of lists) { + const found = list.find((item) => item?.id === taskId); + if (found) return found; + } + return null; +} + function resolveEffectiveList(globalItems, workspace, site, listKey, disabledKey) { const workspaceItems = workspace?.[listKey] || []; const workspaceDisabled = workspace?.disabledInherited?.[disabledKey] || []; @@ -378,6 +429,14 @@ function applyTheme(theme) { document.documentElement.dataset.theme = value; } +function resolveThemeForPopup(baseTheme) { + const siteTheme = state.currentSite?.theme; + if (siteTheme && siteTheme !== "inherit") return siteTheme; + const workspaceTheme = state.currentWorkspace?.theme; + if (workspaceTheme && workspaceTheme !== "inherit") return workspaceTheme; + return baseTheme || "system"; +} + function setAnalyzing(isAnalyzing) { state.isAnalyzing = isAnalyzing; runBtn.disabled = isAnalyzing; @@ -604,17 +663,20 @@ async function loadConfig() { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const currentUrl = tabs[0]?.url || ""; - const { lastPopupState } = await getStorage(["lastPopupState"]); + const { lastPopupState, [POPUP_DRAFT_KEY]: popupDraft } = await getStorage([ + "lastPopupState", + POPUP_DRAFT_KEY + ]); await detectSite(currentUrl); - if (lastPopupState && lastPopupState !== "unknown") { - // If we had a state like 'review', we might want to stay there, - // but detectSite might have switched to 'normal' if it matched. - // AGENTS.md says popup state must be persisted. - if (state.currentSite && lastPopupState === "normal") { - await switchState("normal"); - } else if (!state.currentSite && (lastPopupState === "unknown" || lastPopupState === "review")) { - await switchState(lastPopupState); + if (state.currentSite) { + if (lastPopupState === "normal") { + await switchState("normal"); } + } else if (popupDraft?.state === "review") { + applyPopupDraft(popupDraft); + await switchState("review"); + } else if (lastPopupState === "unknown") { + await switchState("unknown"); } const stored = await getStorage([ @@ -624,6 +686,7 @@ async function loadConfig() { "shortcuts", "workspaces", "sites", + "theme", LAST_TASK_KEY, LAST_ENV_KEY, LAST_PROFILE_KEY @@ -650,6 +713,10 @@ async function loadConfig() { state.currentWorkspace = activeWorkspace; currentWorkspaceName.textContent = activeWorkspace.name || "Global"; } + if (stored.theme) { + state.globalTheme = stored.theme; + } + applyTheme(resolveThemeForPopup(state.globalTheme)); const effectiveEnvs = resolveEffectiveList( envs, @@ -721,7 +788,8 @@ async function loadConfig() { async function loadTheme() { const { theme = "system" } = await getStorage(["theme"]); - applyTheme(theme); + state.globalTheme = theme; + applyTheme(resolveThemeForPopup(theme)); } async function handleExtract() { @@ -758,7 +826,11 @@ async function handleAnalyze() { } const taskId = taskSelect.value; - const task = state.tasks.find((item) => item.id === taskId); + const forcedTask = state.forcedTask; + const task = forcedTask || state.tasks.find((item) => item.id === taskId); + if (forcedTask) { + state.forcedTask = null; + } if (!task) { setStatus("Select a task."); return; @@ -947,6 +1019,16 @@ function handleCopyRaw() { void copyTextToClipboard(text, "Markdown"); } +async function fillSiteDefaultsFromTab() { + const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!tabs[0]?.url) return; + const url = new URL(tabs[0].url); + urlPatternInput.value = url.hostname + url.pathname + "*"; + if (!siteNameInput.value.trim()) { + siteNameInput.value = url.hostname; + } +} + partialTextPaste.addEventListener("input", async () => { const text = partialTextPaste.value.trim(); if (text.length < 5) return; @@ -957,10 +1039,9 @@ partialTextPaste.addEventListener("input", async () => { if (response?.ok) { state.siteText = response.extracted; extractedPreview.textContent = state.siteText; - const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); - const url = new URL(tabs[0].url); - urlPatternInput.value = url.hostname + url.pathname + "*"; + await fillSiteDefaultsFromTab(); switchState("review"); + await persistPopupDraft(); setStatus("Review extraction."); } } catch (error) { @@ -975,10 +1056,9 @@ extractFullBtn.addEventListener("click", async () => { if (response?.ok) { state.siteText = response.extracted; extractedPreview.textContent = state.siteText; - const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); - const url = new URL(tabs[0].url); - urlPatternInput.value = url.hostname + url.pathname + "*"; + await fillSiteDefaultsFromTab(); switchState("review"); + await persistPopupDraft(); setStatus("Review extraction."); } } catch (error) { @@ -986,14 +1066,34 @@ extractFullBtn.addEventListener("click", async () => { } }); +siteNameInput.addEventListener("input", () => { + if (state.currentPopupState !== "review") return; + void persistPopupDraft(); +}); + +urlPatternInput.addEventListener("input", () => { + if (state.currentPopupState !== "review") return; + void persistPopupDraft(); +}); + retryExtractBtn.addEventListener("click", () => { switchState("unknown"); partialTextPaste.value = ""; + extractedPreview.textContent = ""; + urlPatternInput.value = ""; + siteNameInput.value = ""; + state.siteText = ""; + void clearPopupDraft(); setStatus("Ready."); }); confirmSiteBtn.addEventListener("click", async () => { + const name = siteNameInput.value.trim(); const pattern = urlPatternInput.value.trim(); + if (!name) { + setStatus("Enter a site name."); + return; + } if (!pattern) { setStatus("Enter a URL pattern."); return; @@ -1008,12 +1108,14 @@ confirmSiteBtn.addEventListener("click", async () => { const newSite = { id: `site-${Date.now()}`, + name, urlPattern: pattern, workspaceId: "global" // Default to global for now }; state.sites.push(newSite); await chrome.storage.local.set({ sites: state.sites }); + await clearPopupDraft(); state.currentSite = newSite; state.currentWorkspace = { name: "Global", id: "global" }; currentWorkspaceName.textContent = "Global"; @@ -1058,7 +1160,8 @@ async function loadShortcutRunRequest() { SHORTCUT_RUN_KEY, "shortcuts", "workspaces", - "sites" + "sites", + "tasks" ]); const shortcutId = stored[SHORTCUT_RUN_KEY]; if (!shortcutId) return; @@ -1066,7 +1169,12 @@ async function loadShortcutRunRequest() { state.shortcutRunPending = true; await chrome.storage.local.remove(SHORTCUT_RUN_KEY); + if (!state.tasks.length) { + await loadConfig(); + } + const globalShortcuts = normalizeConfigList(stored.shortcuts); + const globalTasks = normalizeConfigList(stored.tasks); const sites = Array.isArray(stored.sites) ? stored.sites : state.sites; const workspaces = Array.isArray(stored.workspaces) ? stored.workspaces @@ -1092,9 +1200,28 @@ async function loadShortcutRunRequest() { return; } - if (shortcut.taskId) { - selectTask(shortcut.taskId, { resetEnv: true }); + const shortcutTaskId = shortcut.taskId || ""; + const scopedTask = findTaskById( + shortcutTaskId, + globalTasks, + activeWorkspace, + activeSite + ); + if (shortcutTaskId && !scopedTask) { + setStatus("Shortcut task is unavailable."); + state.shortcutRunPending = false; + return; } + + if (scopedTask && state.tasks.some((item) => item.id === scopedTask.id)) { + selectTask(scopedTask.id, { resetEnv: true }); + } else if (!state.tasks.length && !scopedTask) { + setStatus("No tasks configured."); + state.shortcutRunPending = false; + return; + } + + state.forcedTask = scopedTask || null; if (shortcut.envId) { setEnvironmentSelection(shortcut.envId); } @@ -1156,6 +1283,7 @@ chrome.storage.onChanged.addListener((changes) => { } if (changes.theme) { - applyTheme(changes.theme.newValue || "system"); + state.globalTheme = changes.theme.newValue || "system"; + applyTheme(resolveThemeForPopup(state.globalTheme)); } }); diff --git a/sitecompanion/settings.css b/sitecompanion/settings.css index 8cd78ea..927ebfd 100644 --- a/sitecompanion/settings.css +++ b/sitecompanion/settings.css @@ -49,6 +49,7 @@ body { .toc { flex: 0 0 160px; + width: 160px; display: flex; flex-direction: column; gap: 8px; @@ -56,6 +57,9 @@ body { position: sticky; top: 16px; padding: 12px; + max-height: calc(100vh - 32px); + min-height: 0; + overflow: hidden; border-radius: 12px; border: 1px solid var(--border); background: var(--panel); @@ -77,6 +81,24 @@ body { .toc-links { display: block; + overflow-y: auto; + flex: 1 1 auto; + min-height: 0; +} + +.toc-resizer { + position: absolute; + top: 0; + right: 0; + width: 6px; + height: 100%; + cursor: col-resize; + background: transparent; +} + +body.is-resizing { + cursor: col-resize; + user-select: none; } .toc ul { @@ -148,7 +170,7 @@ body { } .sidebar-errors { - margin-top: auto; + margin-top: 8px; border-radius: 10px; border: 1px solid #c0392b; background: rgba(192, 57, 43, 0.08); @@ -172,14 +194,6 @@ body { margin-bottom: 16px; } -.page-bar { - display: flex; - justify-content: flex-end; - align-items: center; - gap: 12px; - margin-bottom: 16px; -} - .title { font-size: 26px; font-weight: 700; @@ -376,6 +390,30 @@ button:active { gap: 12px; } +.workspaces, +.sites { + display: grid; + gap: 12px; +} + +.workspace-card, +.site-card { + padding: 12px; + display: grid; + gap: 12px; + overflow: visible; +} + +.workspace-header, +.site-header { + align-items: flex-end; +} + +.workspace-header .field, +.site-header .field { + margin-bottom: 0; +} + .task-card { padding: 12px; border-radius: 12px; @@ -511,12 +549,22 @@ button:active { .api-key-actions .delete, .api-config-actions .delete, .env-config-actions .delete, -.task-actions .delete { +.task-actions .delete, +.shortcut-actions .delete { background: #c0392b; border-color: #c0392b; color: #fff6f2; } +.workspace-header .delete, +.site-header .delete { + background: #c0392b; + border-color: #c0392b; + color: #fff6f2; + padding: 10px 12px; + font-size: 13px; +} + .api-configs { display: grid; gap: 12px; @@ -622,6 +670,12 @@ button:active { justify-content: flex-end; } +.shortcut-actions { + display: flex; + gap: 6px; + justify-content: flex-end; +} + @media (max-width: 720px) { .settings-layout { flex-direction: column; diff --git a/sitecompanion/settings.html b/sitecompanion/settings.html index 0408bc4..4965fa9 100644 --- a/sitecompanion/settings.html +++ b/sitecompanion/settings.html @@ -11,16 +11,12 @@
SiteCompanion Settings
Configure workspaces, tasks, and API access
-
-
- -
-
diff --git a/sitecompanion/settings.js b/sitecompanion/settings.js index f6ce166..3f501a4 100644 --- a/sitecompanion/settings.js +++ b/sitecompanion/settings.js @@ -1,4 +1,3 @@ -const saveBtn = document.getElementById("saveBtn"); const saveBtnSidebar = document.getElementById("saveBtnSidebar"); const addApiConfigBtn = document.getElementById("addApiConfigBtn"); const apiConfigsContainer = document.getElementById("apiConfigs"); @@ -16,13 +15,14 @@ const addSiteBtn = document.getElementById("addSiteBtn"); const sitesContainer = document.getElementById("sites"); const addShortcutBtn = document.getElementById("addShortcutBtn"); const shortcutsContainer = document.getElementById("shortcuts"); -const statusEl = document.getElementById("status"); const statusSidebarEl = document.getElementById("statusSidebar"); const sidebarErrorsEl = document.getElementById("sidebarErrors"); const themeSelect = document.getElementById("themeSelect"); const toolbarPositionSelect = document.getElementById("toolbarPositionSelect"); const toolbarAutoHide = document.getElementById("toolbarAutoHide"); const globalSitesContainer = document.getElementById("globalSites"); +const toc = document.querySelector(".toc"); +const tocResizer = document.getElementById("tocResizer"); const OPENAI_DEFAULTS = { apiBaseUrl: "https://api.openai.com/v1", @@ -32,17 +32,63 @@ const OPENAI_DEFAULTS = { const DEFAULT_MODEL = "gpt-4o-mini"; const DEFAULT_SYSTEM_PROMPT = "You are a precise, honest assistant. Be concise and avoid inventing details, be critical about evaluations. You should put in a small summary of all the sections at the end. You should answer in no longer than 3 sections including the summary. And remember to bold or italicize key points."; +const SIDEBAR_WIDTH_KEY = "sidebarWidth"; + +function getSidebarWidthLimits() { + const min = 160; + const max = Math.max(min, Math.min(360, window.innerWidth - 240)); + return { min, max }; +} + +function applySidebarWidth(width) { + if (!toc) return; + const { min, max } = getSidebarWidthLimits(); + if (!Number.isFinite(width)) { + toc.style.width = ""; + toc.style.flex = ""; + return; + } + const clamped = Math.min(Math.max(width, min), max); + toc.style.width = `${clamped}px`; + toc.style.flex = `0 0 ${clamped}px`; +} + +function initSidebarResize() { + if (!toc || !tocResizer) return; + let startX = 0; + let startWidth = 0; + + const onMouseMove = (event) => { + const delta = event.clientX - startX; + applySidebarWidth(startWidth + delta); + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + document.body.classList.remove("is-resizing"); + const width = toc.getBoundingClientRect().width; + void chrome.storage.local.set({ [SIDEBAR_WIDTH_KEY]: Math.round(width) }); + }; + + tocResizer.addEventListener("mousedown", (event) => { + event.preventDefault(); + startX = event.clientX; + startWidth = toc.getBoundingClientRect().width; + document.body.classList.add("is-resizing"); + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }); +} function getStorage(keys) { return new Promise((resolve) => chrome.storage.local.get(keys, resolve)); } function setStatus(message) { - statusEl.textContent = message; if (statusSidebarEl) statusSidebarEl.textContent = message; if (!message) return; setTimeout(() => { - if (statusEl.textContent === message) statusEl.textContent = ""; if (statusSidebarEl?.textContent === message) statusSidebarEl.textContent = ""; }, 2000); } @@ -73,7 +119,7 @@ function renderGlobalSitesList(sites) { for (const site of globalSites) { const link = document.createElement("a"); link.href = "#"; - link.textContent = site.urlPattern || "Untitled Site"; + link.textContent = site.name || site.urlPattern || "Untitled Site"; link.addEventListener("click", (e) => { e.preventDefault(); const card = document.querySelector(`.site-card[data-id="${site.id}"]`); @@ -1863,7 +1909,13 @@ function buildScopedModuleSection({ const newItem = newItemFactory(localContainer); const options = typeof cardOptions === "function" ? cardOptions() : cardOptions; - localContainer.appendChild(buildCard(newItem, localContainer, options)); + const newCard = buildCard(newItem, localContainer, options); + const first = localContainer.firstElementChild; + if (first) { + localContainer.insertBefore(newCard, first); + } else { + localContainer.appendChild(newCard); + } if (module === "envs") { updateEnvApiOptions(); } else if (module === "profiles") { @@ -1933,7 +1985,10 @@ function listWorkspaceTargets() { function listSiteTargets() { return [...sitesContainer.querySelectorAll(".site-card")].map((card) => ({ id: card.dataset.id || "", - name: card.querySelector(".site-pattern")?.value || "Untitled Site" + name: + card.querySelector(".site-name")?.value || + card.querySelector(".site-pattern")?.value || + "Untitled Site" })); } @@ -2214,6 +2269,7 @@ function buildWorkspaceCard(ws, allWorkspaces = [], allSites = []) { const header = document.createElement("div"); header.className = "row workspace-header"; + header.style.alignItems = "flex-end"; const nameField = document.createElement("div"); nameField.className = "field"; @@ -2387,7 +2443,7 @@ function buildWorkspaceCard(ws, allWorkspaces = [], allSites = []) { for (const site of ownedSites) { const link = document.createElement("a"); link.href = "#"; - link.textContent = site.urlPattern || "Untitled Site"; + link.textContent = site.name || site.urlPattern || "Untitled Site"; link.addEventListener("click", (e) => { e.preventDefault(); const siteCard = document.querySelector( @@ -2411,6 +2467,7 @@ function buildWorkspaceCard(ws, allWorkspaces = [], allSites = []) { function collectSites() { const cards = [...sitesContainer.querySelectorAll(".site-card")]; return cards.map((card) => { + const nameInput = card.querySelector(".site-name"); const patternInput = card.querySelector(".site-pattern"); const workspaceSelect = card.querySelector(".site-workspace"); const themeSelect = card.querySelector(".appearance-theme"); @@ -2426,6 +2483,7 @@ function collectSites() { const apiConfigsInherited = card.querySelector('.inherited-list[data-module="apiConfigs"]'); return { id: card.dataset.id || newSiteId(), + name: (nameInput?.value || "").trim(), urlPattern: (patternInput?.value || "").trim(), workspaceId: workspaceSelect?.value || "global", theme: themeSelect?.value || "inherit", @@ -2451,9 +2509,26 @@ function buildSiteCard(site, allWorkspaces = []) { card.dataset.id = site.id || newSiteId(); const row = document.createElement("div"); - row.className = "row"; + row.className = "row site-header"; row.style.alignItems = "flex-end"; + const nameField = document.createElement("div"); + nameField.className = "field"; + nameField.style.flex = "0.6"; + const nameLabel = document.createElement("label"); + nameLabel.textContent = "Site name"; + const nameInput = document.createElement("input"); + nameInput.type = "text"; + nameInput.value = site.name || ""; + nameInput.className = "site-name"; + nameInput.placeholder = "Site Name"; + nameInput.addEventListener("input", () => { + updateToc(collectWorkspaces(), collectSites()); + scheduleSidebarErrors(); + }); + nameField.appendChild(nameLabel); + nameField.appendChild(nameInput); + const patternField = document.createElement("div"); patternField.className = "field"; patternField.style.flex = "1"; @@ -2519,6 +2594,7 @@ function buildSiteCard(site, allWorkspaces = []) { scheduleSidebarErrors(); }); + row.appendChild(nameField); row.appendChild(patternField); row.appendChild(wsField); row.appendChild(deleteBtn); @@ -2924,11 +3000,13 @@ function buildShortcutCard(shortcut, _container, options = {}) { taskField.appendChild(taskLabel); taskField.appendChild(taskSelect); + const actions = document.createElement("div"); + actions.className = "shortcut-actions"; + const deleteBtn = document.createElement("button"); deleteBtn.type = "button"; deleteBtn.className = "ghost delete"; deleteBtn.textContent = "Delete"; - deleteBtn.style.marginTop = "8px"; deleteBtn.addEventListener("click", () => { card.remove(); scheduleSidebarErrors(); @@ -2939,7 +3017,7 @@ function buildShortcutCard(shortcut, _container, options = {}) { card.appendChild(envField); card.appendChild(profileField); card.appendChild(taskField); - card.appendChild( + actions.appendChild( buildDuplicateControls("shortcuts", () => ({ id: card.dataset.id, name: nameInput.value || "Untitled Shortcut", @@ -2949,7 +3027,8 @@ function buildShortcutCard(shortcut, _container, options = {}) { enabled: enabledInput.checked })) ); - card.appendChild(deleteBtn); + actions.appendChild(deleteBtn); + card.appendChild(actions); return card; } @@ -3071,7 +3150,10 @@ function updateSidebarErrors() { const siteCards = [...sitesContainer.querySelectorAll(".site-card")]; siteCards.forEach((card) => { - const label = card.querySelector(".site-pattern")?.value || "Site"; + const label = + card.querySelector(".site-name")?.value || + card.querySelector(".site-pattern")?.value || + "Site"; checkNameInputs( card.querySelector(".site-envs"), ".env-config-name", @@ -3094,6 +3176,8 @@ function updateSidebarErrors() { ); }); + checkNameInputs(sitesContainer, ".site-name", "Sites"); + if (!enabledTasks.length) errors.push("No tasks enabled."); if (!enabledEnvs.length) errors.push("No environments enabled."); if (!enabledProfiles.length) errors.push("No profiles enabled."); @@ -3206,7 +3290,8 @@ async function loadSettings() { workspaces = [], sites = [], toolbarPosition = "bottom-right", - toolbarAutoHide: storedToolbarAutoHide = true + toolbarAutoHide: storedToolbarAutoHide = true, + sidebarWidth } = await getStorage([ "apiKey", "apiKeys", @@ -3229,7 +3314,8 @@ async function loadSettings() { "workspaces", "sites", "toolbarPosition", - "toolbarAutoHide" + "toolbarAutoHide", + SIDEBAR_WIDTH_KEY ]); themeSelect.value = theme; @@ -3241,6 +3327,9 @@ async function loadSettings() { if (toolbarAutoHide) { toolbarAutoHide.checked = Boolean(storedToolbarAutoHide); } + if (Number.isFinite(sidebarWidth)) { + applySidebarWidth(sidebarWidth); + } if (!shortcuts.length && Array.isArray(legacyPresets) && legacyPresets.length) { shortcuts = legacyPresets; @@ -3287,6 +3376,7 @@ async function loadSettings() { if (!site || typeof site !== "object") return site; return { ...site, + name: site.name || site.urlPattern || "", workspaceId: site.workspaceId || "global", theme: site.theme || "inherit", toolbarPosition: site.toolbarPosition || "inherit", @@ -3638,7 +3728,6 @@ async function saveSettings() { } } -saveBtn.addEventListener("click", () => void saveSettings()); if (saveBtnSidebar) { saveBtnSidebar.addEventListener("click", () => void saveSettings()); } @@ -3759,7 +3848,12 @@ addWorkspaceBtn.addEventListener("click", () => { shortcuts: [], disabledInherited: normalizeDisabledInherited() }, collectWorkspaces(), collectSites()); - workspacesContainer.appendChild(newCard); + const first = workspacesContainer.firstElementChild; + if (first) { + workspacesContainer.insertBefore(newCard, first); + } else { + workspacesContainer.appendChild(newCard); + } refreshWorkspaceInheritedLists(); scheduleSidebarErrors(); updateToc(collectWorkspaces(), collectSites()); @@ -3768,6 +3862,7 @@ addWorkspaceBtn.addEventListener("click", () => { addSiteBtn.addEventListener("click", () => { const newCard = buildSiteCard({ id: newSiteId(), + name: "", urlPattern: "", workspaceId: "global", theme: "inherit", @@ -3778,7 +3873,12 @@ addSiteBtn.addEventListener("click", () => { shortcuts: [], disabledInherited: normalizeDisabledInherited() }, collectWorkspaces()); - sitesContainer.appendChild(newCard); + const first = sitesContainer.firstElementChild; + if (first) { + sitesContainer.insertBefore(newCard, first); + } else { + sitesContainer.appendChild(newCard); + } refreshSiteInheritedLists(); scheduleSidebarErrors(); updateToc(collectWorkspaces(), collectSites()); @@ -3792,11 +3892,17 @@ addShortcutBtn.addEventListener("click", () => { profileId: "", taskId: "" }); - shortcutsContainer.appendChild(newCard); + const first = shortcutsContainer.firstElementChild; + if (first) { + shortcutsContainer.insertBefore(newCard, first); + } else { + shortcutsContainer.appendChild(newCard); + } scheduleSidebarErrors(); }); themeSelect.addEventListener("change", () => applyTheme(themeSelect.value)); +initSidebarResize(); loadSettings(); @@ -3908,7 +4014,7 @@ function updateToc(workspaces, sites) { for (const site of sites) { const li = document.createElement("li"); const a = document.createElement("a"); - a.textContent = site.urlPattern || "Untitled Site"; + a.textContent = site.name || site.urlPattern || "Untitled Site"; a.href = "#"; a.addEventListener("click", (e) => { e.preventDefault();