[Prototype] first working version of SiteCompanion

This commit is contained in:
2026-01-18 07:27:16 -05:00
parent 6106adbc6f
commit 085c19a54b
6 changed files with 2516 additions and 523 deletions

View File

@@ -29,12 +29,15 @@ const DEFAULT_SETTINGS = {
systemPrompt: systemPrompt:
"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.", "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.",
tasks: DEFAULT_TASKS, tasks: DEFAULT_TASKS,
shortcuts: [],
theme: "system", theme: "system",
toolbarAutoHide: true,
workspaces: [] workspaces: []
}; };
const OUTPUT_STORAGE_KEY = "lastOutput"; const OUTPUT_STORAGE_KEY = "lastOutput";
const AUTO_RUN_KEY = "autoRunDefaultTask"; const AUTO_RUN_KEY = "autoRunDefaultTask";
const SHORTCUT_RUN_KEY = "runShortcutId";
let activeAbortController = null; let activeAbortController = null;
let keepalivePort = null; let keepalivePort = null;
const streamState = { const streamState = {
@@ -354,6 +357,16 @@ chrome.runtime.onConnect.addListener((port) => {
}); });
chrome.runtime.onMessage.addListener((message) => { chrome.runtime.onMessage.addListener((message) => {
if (message?.type === "RUN_SHORTCUT") {
const shortcutId = message.shortcutId || "";
if (shortcutId) {
void chrome.storage.local.set({ [SHORTCUT_RUN_KEY]: shortcutId });
if (chrome.action?.openPopup) {
void chrome.action.openPopup().catch(() => {});
}
}
return;
}
if (message?.type !== "RUN_DEFAULT_TASK") return; if (message?.type !== "RUN_DEFAULT_TASK") return;
void chrome.storage.local.set({ [AUTO_RUN_KEY]: Date.now() }); void chrome.storage.local.set({ [AUTO_RUN_KEY]: Date.now() });
if (chrome.action?.openPopup) { if (chrome.action?.openPopup) {

View File

@@ -22,7 +22,32 @@ function findMinimumScope(text) {
return deepest; return deepest;
} }
function createToolbar(presets, position = "bottom-right") { function normalizeName(value) {
return (value || "").trim().toLowerCase();
}
function resolveScopedItems(parentItems, localItems, disabledNames) {
const parent = Array.isArray(parentItems) ? parentItems : [];
const local = Array.isArray(localItems) ? localItems : [];
const disabledSet = new Set(
(disabledNames || []).map((name) => normalizeName(name)).filter(Boolean)
);
const localNameSet = new Set(
local.map((item) => normalizeName(item.name)).filter(Boolean)
);
const inherited = parent.filter((item) => {
if (item?.enabled === false) return false;
const key = normalizeName(item?.name);
if (!key) return false;
if (localNameSet.has(key)) return false;
if (disabledSet.has(key)) return false;
return true;
});
const localEnabled = local.filter((item) => item?.enabled !== false);
return [...inherited, ...localEnabled];
}
function createToolbar(shortcuts, position = "bottom-right") {
let toolbar = document.getElementById("sitecompanion-toolbar"); let toolbar = document.getElementById("sitecompanion-toolbar");
if (toolbar) toolbar.remove(); if (toolbar) toolbar.remove();
@@ -63,16 +88,16 @@ function createToolbar(presets, position = "bottom-right") {
font-family: system-ui, sans-serif; font-family: system-ui, sans-serif;
`; `;
if (!presets || !presets.length) { if (!shortcuts || !shortcuts.length) {
const label = document.createElement("span"); const label = document.createElement("span");
label.textContent = "SiteCompanion"; label.textContent = "SiteCompanion";
label.style.fontSize = "12px"; label.style.fontSize = "12px";
label.style.color = "#6b5f55"; label.style.color = "#6b5f55";
toolbar.appendChild(label); toolbar.appendChild(label);
} else { } else {
for (const preset of presets) { for (const shortcut of shortcuts) {
const btn = document.createElement("button"); const btn = document.createElement("button");
btn.textContent = preset.name; btn.textContent = shortcut.name;
btn.style.cssText = ` btn.style.cssText = `
padding: 6px 12px; padding: 6px 12px;
background: #b14d2b; background: #b14d2b;
@@ -83,7 +108,7 @@ function createToolbar(presets, position = "bottom-right") {
font-size: 12px; font-size: 12px;
`; `;
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
chrome.runtime.sendMessage({ type: "RUN_PRESET", presetId: preset.id }); chrome.runtime.sendMessage({ type: "RUN_SHORTCUT", shortcutId: shortcut.id });
}); });
toolbar.appendChild(btn); toolbar.appendChild(btn);
} }
@@ -95,36 +120,68 @@ function createToolbar(presets, position = "bottom-right") {
function matchUrl(url, pattern) { function matchUrl(url, pattern) {
if (!pattern) return false; if (!pattern) return false;
let regex = null;
const regex = new RegExp("^" + pattern.split("*").join(".*") + "$");
try { try {
regex = new RegExp("^" + pattern.split("*").join(".*") + "$");
const urlObj = new URL(url); } catch {
return false;
const target = urlObj.hostname + urlObj.pathname; }
try {
return regex.test(target); const urlObj = new URL(url);
const target = urlObj.hostname + urlObj.pathname;
return regex.test(target);
} catch { } catch {
return false; return false;
} }
} }
async function refreshToolbar() { async function refreshToolbar() {
const { sites = [], presets = [], toolbarPosition = "bottom-right" } = await chrome.storage.local.get(["sites", "presets", "toolbarPosition"]); let {
sites = [],
workspaces = [],
shortcuts = [],
presets = [],
toolbarPosition = "bottom-right"
} = await chrome.storage.local.get([
"sites",
"workspaces",
"shortcuts",
"presets",
"toolbarPosition"
]);
const currentUrl = window.location.href; const currentUrl = window.location.href;
const site = sites.find(s => matchUrl(currentUrl, s.urlPattern)); const site = sites.find(s => matchUrl(currentUrl, s.urlPattern));
if (site) { if (site) {
createToolbar(presets, toolbarPosition); if (!shortcuts.length && Array.isArray(presets) && presets.length) {
shortcuts = presets;
await chrome.storage.local.set({ shortcuts });
await chrome.storage.local.remove("presets");
}
const workspace =
workspaces.find((ws) => ws.id === site.workspaceId) || null;
const workspaceDisabled = workspace?.disabledInherited?.shortcuts || [];
const siteDisabled = site?.disabledInherited?.shortcuts || [];
const workspaceShortcuts = resolveScopedItems(
shortcuts,
workspace?.shortcuts || [],
workspaceDisabled
);
const siteShortcuts = resolveScopedItems(
workspaceShortcuts,
site.shortcuts || [],
siteDisabled
);
const resolvedPosition =
site.toolbarPosition && site.toolbarPosition !== "inherit"
? site.toolbarPosition
: workspace?.toolbarPosition && workspace.toolbarPosition !== "inherit"
? workspace.toolbarPosition
: toolbarPosition;
createToolbar(siteShortcuts, resolvedPosition);
} }
} }
@@ -140,4 +197,25 @@ const observer = new MutationObserver(() => {
// observer.observe(document.documentElement, { childList: true, subtree: true }); // observer.observe(document.documentElement, { childList: true, subtree: true });
refreshToolbar(); chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (!message || typeof message !== "object") return;
if (message.type === "FIND_SCOPE") {
const node = findMinimumScope(message.text || "");
if (!node) {
sendResponse({ ok: false, error: "Scope not found." });
return;
}
sendResponse({ ok: true, extracted: node.innerText || "" });
return;
}
if (message.type === "EXTRACT_FULL") {
const extracted = document.body?.innerText || "";
sendResponse({ ok: true, extracted });
}
});
try {
refreshToolbar();
} catch (error) {
console.warn("SiteCompanion toolbar failed:", error);
}

View File

@@ -14,6 +14,7 @@ const clearOutputBtn = document.getElementById("clearOutputBtn");
const OUTPUT_STORAGE_KEY = "lastOutput"; const OUTPUT_STORAGE_KEY = "lastOutput";
const AUTO_RUN_KEY = "autoRunDefaultTask"; const AUTO_RUN_KEY = "autoRunDefaultTask";
const SHORTCUT_RUN_KEY = "runShortcutId";
const LAST_TASK_KEY = "lastSelectedTaskId"; const LAST_TASK_KEY = "lastSelectedTaskId";
const LAST_ENV_KEY = "lastSelectedEnvId"; const LAST_ENV_KEY = "lastSelectedEnvId";
const LAST_PROFILE_KEY = "lastSelectedProfileId"; const LAST_PROFILE_KEY = "lastSelectedProfileId";
@@ -42,6 +43,7 @@ const state = {
isAnalyzing: false, isAnalyzing: false,
outputRaw: "", outputRaw: "",
autoRunPending: false, autoRunPending: false,
shortcutRunPending: false,
selectedTaskId: "", selectedTaskId: "",
selectedEnvId: "", selectedEnvId: "",
selectedProfileId: "" selectedProfileId: ""
@@ -64,7 +66,12 @@ async function switchState(stateName) {
function matchUrl(url, pattern) { function matchUrl(url, pattern) {
if (!pattern) return false; if (!pattern) return false;
const regex = new RegExp("^" + pattern.split("*").join(".*") + "$"); let regex = null;
try {
regex = new RegExp("^" + pattern.split("*").join(".*") + "$");
} catch {
return false;
}
try { try {
const urlObj = new URL(url); const urlObj = new URL(url);
const target = urlObj.hostname + urlObj.pathname; const target = urlObj.hostname + urlObj.pathname;
@@ -74,6 +81,62 @@ function matchUrl(url, pattern) {
} }
} }
function normalizeName(value) {
return (value || "").trim().toLowerCase();
}
function normalizeConfigList(list) {
return Array.isArray(list)
? list.map((item) => ({ ...item, enabled: item.enabled !== false }))
: [];
}
function resolveScopedItems(parentItems, localItems, disabledNames) {
const parent = Array.isArray(parentItems) ? parentItems : [];
const local = Array.isArray(localItems) ? localItems : [];
const disabledSet = new Set(
(disabledNames || []).map((name) => normalizeName(name)).filter(Boolean)
);
const localNameSet = new Set(
local.map((item) => normalizeName(item.name)).filter(Boolean)
);
const inherited = parent.filter((item) => {
if (item?.enabled === false) return false;
const key = normalizeName(item?.name);
if (!key) return false;
if (localNameSet.has(key)) return false;
if (disabledSet.has(key)) return false;
return true;
});
const localEnabled = local.filter((item) => item?.enabled !== false);
return [...inherited, ...localEnabled];
}
function resolveEffectiveList(globalItems, workspace, site, listKey, disabledKey) {
const workspaceItems = workspace?.[listKey] || [];
const workspaceDisabled = workspace?.disabledInherited?.[disabledKey] || [];
const workspaceEffective = resolveScopedItems(
globalItems,
workspaceItems,
workspaceDisabled
);
const siteItems = site?.[listKey] || [];
const siteDisabled = site?.disabledInherited?.[disabledKey] || [];
return resolveScopedItems(workspaceEffective, siteItems, siteDisabled);
}
function filterApiConfigsForScope(apiConfigs, workspace, site) {
const workspaceDisabled = workspace?.disabledInherited?.apiConfigs || [];
const siteDisabled = site?.disabledInherited?.apiConfigs || [];
const workspaceFiltered = apiConfigs.filter(
(config) =>
config?.enabled !== false && !workspaceDisabled.includes(config.id)
);
return workspaceFiltered.filter(
(config) => !siteDisabled.includes(config.id)
);
}
async function detectSite(url) { async function detectSite(url) {
const { sites = [], workspaces = [] } = await getStorage(["sites", "workspaces"]); const { sites = [], workspaces = [] } = await getStorage(["sites", "workspaces"]);
state.sites = sites; state.sites = sites;
@@ -82,7 +145,13 @@ async function detectSite(url) {
const site = sites.find(s => matchUrl(url, s.urlPattern)); const site = sites.find(s => matchUrl(url, s.urlPattern));
if (site) { if (site) {
state.currentSite = site; state.currentSite = site;
state.currentWorkspace = workspaces.find(w => w.id === site.workspaceId) || { name: "Global", id: "global" }; const workspace =
workspaces.find((entry) => entry.id === site.workspaceId) || null;
state.currentWorkspace = workspace || {
name: "Global",
id: "global",
disabledInherited: {}
};
currentWorkspaceName.textContent = state.currentWorkspace.name; currentWorkspaceName.textContent = state.currentWorkspace.name;
switchState("normal"); switchState("normal");
return true; return true;
@@ -552,40 +621,88 @@ async function loadConfig() {
"tasks", "tasks",
"envConfigs", "envConfigs",
"profiles", "profiles",
"shortcuts",
"workspaces",
"sites",
LAST_TASK_KEY, LAST_TASK_KEY,
LAST_ENV_KEY, LAST_ENV_KEY,
LAST_PROFILE_KEY LAST_PROFILE_KEY
]); ]);
const tasks = Array.isArray(stored.tasks) ? stored.tasks : []; const tasks = normalizeConfigList(stored.tasks);
const envs = Array.isArray(stored.envConfigs) ? stored.envConfigs : []; const envs = normalizeConfigList(stored.envConfigs);
const profiles = Array.isArray(stored.profiles) ? stored.profiles : []; const profiles = normalizeConfigList(stored.profiles);
renderTasks(tasks); const shortcuts = normalizeConfigList(stored.shortcuts);
renderEnvironments(envs); const sites = Array.isArray(stored.sites) ? stored.sites : state.sites;
renderProfiles(profiles); const workspaces = Array.isArray(stored.workspaces)
? stored.workspaces
: state.workspaces;
state.sites = sites;
state.workspaces = workspaces;
if (!tasks.length) { const activeSite = state.currentSite
? sites.find((entry) => entry.id === state.currentSite.id)
: null;
const activeWorkspace =
activeSite && activeSite.workspaceId
? workspaces.find((entry) => entry.id === activeSite.workspaceId)
: null;
if (activeWorkspace) {
state.currentWorkspace = activeWorkspace;
currentWorkspaceName.textContent = activeWorkspace.name || "Global";
}
const effectiveEnvs = resolveEffectiveList(
envs,
activeWorkspace,
activeSite,
"envConfigs",
"envs"
);
const effectiveProfiles = resolveEffectiveList(
profiles,
activeWorkspace,
activeSite,
"profiles",
"profiles"
);
const effectiveTasks = resolveEffectiveList(
tasks,
activeWorkspace,
activeSite,
"tasks",
"tasks"
);
renderTasks(effectiveTasks);
renderEnvironments(effectiveEnvs);
renderProfiles(effectiveProfiles);
if (!effectiveTasks.length) {
state.selectedTaskId = ""; state.selectedTaskId = "";
setEnvironmentSelection(envs[0]?.id || ""); setEnvironmentSelection(effectiveEnvs[0]?.id || "");
setProfileSelection(profiles[0]?.id || ""); setProfileSelection(effectiveProfiles[0]?.id || "");
return; return;
} }
const storedTaskId = stored[LAST_TASK_KEY]; const storedTaskId = stored[LAST_TASK_KEY];
const storedEnvId = stored[LAST_ENV_KEY]; const storedEnvId = stored[LAST_ENV_KEY];
const storedProfileId = stored[LAST_PROFILE_KEY]; const storedProfileId = stored[LAST_PROFILE_KEY];
const initialTaskId = tasks.some((task) => task.id === storedTaskId) const initialTaskId = effectiveTasks.some((task) => task.id === storedTaskId)
? storedTaskId ? storedTaskId
: tasks[0].id; : effectiveTasks[0].id;
selectTask(initialTaskId, { resetEnv: false }); selectTask(initialTaskId, { resetEnv: false });
const task = tasks.find((item) => item.id === initialTaskId); const task = effectiveTasks.find((item) => item.id === initialTaskId);
if (storedEnvId && envs.some((env) => env.id === storedEnvId)) { if (storedEnvId && effectiveEnvs.some((env) => env.id === storedEnvId)) {
setEnvironmentSelection(storedEnvId); setEnvironmentSelection(storedEnvId);
} else { } else {
setEnvironmentSelection(getTaskDefaultEnvId(task)); setEnvironmentSelection(getTaskDefaultEnvId(task));
} }
if (storedProfileId && profiles.some((profile) => profile.id === storedProfileId)) { if (
storedProfileId &&
effectiveProfiles.some((profile) => profile.id === storedProfileId)
) {
setProfileSelection(storedProfileId); setProfileSelection(storedProfileId);
} else { } else {
setProfileSelection(getTaskDefaultProfileId(task)); setProfileSelection(getTaskDefaultProfileId(task));
@@ -652,8 +769,6 @@ async function handleAnalyze() {
activeApiKeyId = "", activeApiKeyId = "",
apiConfigs = [], apiConfigs = [],
activeApiConfigId = "", activeApiConfigId = "",
envConfigs = [],
profiles = [],
apiBaseUrl, apiBaseUrl,
apiKeyHeader, apiKeyHeader,
apiKeyPrefix, apiKeyPrefix,
@@ -665,8 +780,6 @@ async function handleAnalyze() {
"activeApiKeyId", "activeApiKeyId",
"apiConfigs", "apiConfigs",
"activeApiConfigId", "activeApiConfigId",
"envConfigs",
"profiles",
"apiBaseUrl", "apiBaseUrl",
"apiKeyHeader", "apiKeyHeader",
"apiKeyPrefix", "apiKeyPrefix",
@@ -675,9 +788,13 @@ async function handleAnalyze() {
"resume" "resume"
]); ]);
const resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : []; const resolvedConfigs = filterApiConfigsForScope(
const resolvedEnvs = Array.isArray(envConfigs) ? envConfigs : []; normalizeConfigList(apiConfigs),
const resolvedProfiles = Array.isArray(profiles) ? profiles : []; state.currentWorkspace,
state.currentSite
);
const resolvedEnvs = Array.isArray(state.envs) ? state.envs : [];
const resolvedProfiles = Array.isArray(state.profiles) ? state.profiles : [];
const selectedEnvId = envSelect.value; const selectedEnvId = envSelect.value;
const activeEnv = const activeEnv =
resolvedEnvs.find((entry) => entry.id === selectedEnvId) || resolvedEnvs.find((entry) => entry.id === selectedEnvId) ||
@@ -711,7 +828,9 @@ async function handleAnalyze() {
const resolvedApiKeyPrefix = activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? ""; const resolvedApiKeyPrefix = activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? "";
const resolvedModel = activeConfig?.model || model || ""; const resolvedModel = activeConfig?.model || model || "";
const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : []; const resolvedKeys = normalizeConfigList(apiKeys).filter(
(key) => key.enabled !== false
);
const resolvedKeyId = const resolvedKeyId =
activeConfig?.apiKeyId || activeApiKeyId || resolvedKeys[0]?.id || ""; activeConfig?.apiKeyId || activeApiKeyId || resolvedKeys[0]?.id || "";
const activeKey = resolvedKeys.find((entry) => entry.id === resolvedKeyId); const activeKey = resolvedKeys.find((entry) => entry.id === resolvedKeyId);
@@ -926,8 +1045,7 @@ updateSiteTextCount();
updatePromptCount(0); updatePromptCount(0);
renderOutput(); renderOutput();
setAnalyzing(false); setAnalyzing(false);
loadConfig(); void loadTheme();
loadTheme();
async function loadSavedOutput() { async function loadSavedOutput() {
const stored = await getStorage([OUTPUT_STORAGE_KEY]); const stored = await getStorage([OUTPUT_STORAGE_KEY]);
@@ -935,6 +1053,60 @@ async function loadSavedOutput() {
renderOutput(); renderOutput();
} }
async function loadShortcutRunRequest() {
const stored = await getStorage([
SHORTCUT_RUN_KEY,
"shortcuts",
"workspaces",
"sites"
]);
const shortcutId = stored[SHORTCUT_RUN_KEY];
if (!shortcutId) return;
state.shortcutRunPending = true;
await chrome.storage.local.remove(SHORTCUT_RUN_KEY);
const globalShortcuts = normalizeConfigList(stored.shortcuts);
const sites = Array.isArray(stored.sites) ? stored.sites : state.sites;
const workspaces = Array.isArray(stored.workspaces)
? stored.workspaces
: state.workspaces;
const activeSite = state.currentSite
? sites.find((entry) => entry.id === state.currentSite.id)
: null;
const activeWorkspace =
activeSite && activeSite.workspaceId
? workspaces.find((entry) => entry.id === activeSite.workspaceId)
: null;
const effectiveShortcuts = resolveEffectiveList(
globalShortcuts,
activeWorkspace,
activeSite,
"shortcuts",
"shortcuts"
);
const shortcut = effectiveShortcuts.find((item) => item.id === shortcutId);
if (!shortcut) {
setStatus("Shortcut not found.");
state.shortcutRunPending = false;
return;
}
if (shortcut.taskId) {
selectTask(shortcut.taskId, { resetEnv: true });
}
if (shortcut.envId) {
setEnvironmentSelection(shortcut.envId);
}
if (shortcut.profileId) {
setProfileSelection(shortcut.profileId);
}
await persistSelections();
state.autoRunPending = false;
state.shortcutRunPending = false;
void handleExtractAndAnalyze();
}
async function loadAutoRunRequest() { async function loadAutoRunRequest() {
const stored = await getStorage([AUTO_RUN_KEY]); const stored = await getStorage([AUTO_RUN_KEY]);
if (stored[AUTO_RUN_KEY]) { if (stored[AUTO_RUN_KEY]) {
@@ -945,6 +1117,7 @@ async function loadAutoRunRequest() {
} }
function maybeRunDefaultTask() { function maybeRunDefaultTask() {
if (state.shortcutRunPending) return;
if (!state.autoRunPending) return; if (!state.autoRunPending) return;
if (state.isAnalyzing) return; if (state.isAnalyzing) return;
if (!state.tasks.length) return; if (!state.tasks.length) return;
@@ -954,8 +1127,14 @@ function maybeRunDefaultTask() {
void handleExtractAndAnalyze(); void handleExtractAndAnalyze();
} }
loadSavedOutput(); async function init() {
loadAutoRunRequest(); await loadConfig();
await loadShortcutRunRequest();
await loadAutoRunRequest();
}
void init();
void loadSavedOutput();
ensurePort(); ensurePort();
chrome.storage.onChanged.addListener((changes) => { chrome.storage.onChanged.addListener((changes) => {
@@ -965,6 +1144,10 @@ chrome.storage.onChanged.addListener((changes) => {
maybeRunDefaultTask(); maybeRunDefaultTask();
} }
if (changes[SHORTCUT_RUN_KEY]?.newValue) {
void loadShortcutRunRequest();
}
if (changes[OUTPUT_STORAGE_KEY]?.newValue !== undefined) { if (changes[OUTPUT_STORAGE_KEY]?.newValue !== undefined) {
if (!state.isAnalyzing || !state.port) { if (!state.isAnalyzing || !state.port) {
state.outputRaw = changes[OUTPUT_STORAGE_KEY].newValue || ""; state.outputRaw = changes[OUTPUT_STORAGE_KEY].newValue || "";

View File

@@ -199,38 +199,31 @@ body {
overflow: hidden; overflow: hidden;
} }
.panel-summary {
list-style: none;
cursor: pointer;
display: flex;
align-items: baseline;
justify-content: flex-start;
gap: 12px;
padding: 12px 16px;
margin: 0;
}
/* Restore native marker but keep list-style: none from above? No, remove list-style: none to show marker. */
/* Wait, display: flex might hide marker in some browsers. */
/* Usually marker is ::marker pseudo-element on summary. */
/* To show native marker, summary should be display: list-item or similar? */
/* Actually, standard is display: block (or list-item). Flex might kill it. */
/* If the user wants native glyphs, I should use list-item and maybe position the h2? */
/* Let's try reverting panel-summary to default display and styling h2 inline. */
.panel-summary { .panel-summary {
cursor: pointer; cursor: pointer;
padding: 12px 16px; padding: 12px 16px;
margin: 0; margin: 0;
/* display: list-item; default */ display: list-item;
list-style: revert;
list-style-position: inside;
} }
/* Need to align H2. */ .panel-summary::marker {
.panel-summary h2 { color: var(--muted);
}
.panel-summary h2,
.panel-summary h3 {
display: inline; display: inline;
} }
.panel-summary .row-title {
display: inline-flex;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
.sub-panel .panel-summary { .sub-panel .panel-summary {
padding: 10px 12px; padding: 10px 12px;
} }
@@ -312,6 +305,20 @@ label {
color: var(--muted); color: var(--muted);
} }
.toggle-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
text-transform: none;
letter-spacing: 0;
color: var(--muted);
}
.toggle-label input[type="checkbox"] {
width: auto;
}
input, input,
textarea, textarea,
select { select {
@@ -378,6 +385,109 @@ button:active {
gap: 8px; gap: 8px;
} }
.shortcuts {
display: grid;
gap: 12px;
}
.shortcut-card {
padding: 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--card-bg);
display: grid;
gap: 8px;
}
.scope-group {
margin-top: 12px;
display: grid;
gap: 8px;
}
.scope-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--muted);
}
.inherited-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.inherited-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.inherited-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--panel);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--muted);
cursor: pointer;
}
.inherited-button input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.inherited-item.is-enabled .inherited-button {
border: 1px solid var(--accent);
background: var(--accent);
color: #fff9f3;
box-shadow: 0 8px 20px rgba(177, 77, 43, 0.2);
}
.inherited-item.is-disabled .inherited-button {
background: transparent;
color: var(--muted);
}
.inherited-item.is-overridden .inherited-button {
cursor: default;
opacity: 0.65;
}
.dup-controls {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.dup-select {
min-width: 160px;
}
.sites-list {
display: grid;
gap: 6px;
font-size: 12px;
}
.sites-list a {
color: var(--muted);
text-decoration: none;
}
.sites-list a:hover {
color: var(--ink);
}
.api-keys { .api-keys {
display: grid; display: grid;
gap: 12px; gap: 12px;

View File

@@ -13,7 +13,7 @@
</header> </header>
<div class="page-bar"> <div class="page-bar">
<div id="status" class="status"></div> <div id="status" class="status"></div>
<button id="saveBtn" class="accent">Save Settings</button> <button id="saveBtn" class="accent" type="button">Save Settings</button>
</div> </div>
<div class="settings-layout"> <div class="settings-layout">
@@ -28,27 +28,28 @@
<div class="toc-item"> <div class="toc-item">
<span class="toc-caret"></span> <a href="#global-config-panel">Global Configuration</a> <span class="toc-caret"></span> <a href="#global-config-panel">Global Configuration</a>
</div> </div>
<ul class="toc-sub hidden"> <ul class="toc-sub">
<li><a href="#appearance-panel">Appearance</a></li> <li><a href="#appearance-panel">Appearance</a></li>
<li><a href="#api-keys-panel">API Keys</a></li> <li><a href="#api-keys-panel">API Keys</a></li>
<li><a href="#api-panel">API</a></li> <li><a href="#api-panel">API</a></li>
<li><a href="#environment-panel">Environments</a></li> <li><a href="#environment-panel">Environments</a></li>
<li><a href="#profiles-panel">Profiles</a></li> <li><a href="#profiles-panel">Profiles</a></li>
<li><a href="#tasks-panel">Tasks</a></li> <li><a href="#tasks-panel">Tasks</a></li>
<li><a href="#presets-panel">Presets</a></li> <li><a href="#shortcuts-panel">Toolbar Shortcuts</a></li>
<li><a href="#global-sites-panel">Sites</a></li>
</ul> </ul>
</li> </li>
<li> <li>
<div class="toc-item"> <div class="toc-item">
<span class="toc-caret"></span> <a href="#workspaces-panel">Workspaces</a> <span class="toc-caret"></span> <a href="#workspaces-panel">Workspaces</a>
</div> </div>
<ul class="toc-sub hidden" id="toc-workspaces-list"></ul> <ul class="toc-sub" id="toc-workspaces-list"></ul>
</li> </li>
<li> <li>
<div class="toc-item"> <div class="toc-item">
<span class="toc-caret"></span> <a href="#sites-panel">Sites</a> <span class="toc-caret"></span> <a href="#sites-panel">Sites</a>
</div> </div>
<ul class="toc-sub hidden" id="toc-sites-list"></ul> <ul class="toc-sub" id="toc-sites-list"></ul>
</li> </li>
</ul> </ul>
</div> </div>
@@ -84,6 +85,12 @@
<option value="bottom-center">Bottom Center</option> <option value="bottom-center">Bottom Center</option>
</select> </select>
</div> </div>
<div class="field">
<label class="toggle-label">
<input id="toolbarAutoHide" type="checkbox" />
Auto-hide toolbar on unknown sites
</label>
</div>
</div> </div>
</details> </details>
@@ -168,20 +175,33 @@
</div> </div>
</details> </details>
<!-- Presets --> <!-- Toolbar Shortcuts -->
<details class="panel sub-panel" id="presets-panel"> <details class="panel sub-panel" id="shortcuts-panel">
<summary class="panel-summary"> <summary class="panel-summary">
<div class="row-title"> <div class="row-title">
<h2>PRESETS</h2> <h2>TOOLBAR SHORTCUTS</h2>
<span class="hint hint-accent">Toolbar shortcuts</span> <span class="hint hint-accent">One-click toolbar runs</span>
</div> </div>
</summary> </summary>
<div class="panel-body"> <div class="panel-body">
<div class="row"> <div class="row">
<div></div> <div></div>
<button id="addPresetBtn" class="ghost" type="button">Add Preset</button> <button id="addShortcutBtn" class="ghost" type="button">Add Shortcut</button>
</div> </div>
<div id="presets" class="presets"></div> <div id="shortcuts" class="shortcuts"></div>
</div>
</details>
<!-- Sites -->
<details class="panel sub-panel" id="global-sites-panel">
<summary class="panel-summary">
<div class="row-title">
<h2>SITES</h2>
<span class="hint hint-accent">Inherit directly from global</span>
</div>
</summary>
<div class="panel-body">
<div id="globalSites" class="sites-list"></div>
</div> </div>
</details> </details>
</div> </div>

File diff suppressed because it is too large Load Diff