feat: add inline config panel for search method and mode order

Assisted-by: Claude Code
Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
2026-04-28 00:13:06 +05:30
parent 8bd3577be2
commit de0b245a55
6 changed files with 362 additions and 13 deletions
+142
View File
@@ -149,6 +149,148 @@ body {
cursor: default; cursor: default;
} }
/* Config button */
#config-btn {
flex-shrink: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 0;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition: color 0.12s;
}
#config-btn:hover {
color: var(--text);
}
#config-btn.active {
color: var(--accent);
}
/* Config panel */
#config-panel {
padding: 12px 16px;
}
.config-section {
margin-bottom: 14px;
}
.config-section:last-child {
margin-bottom: 0;
}
.config-label {
font-size: 11px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.config-hint {
font-weight: 400;
text-transform: none;
letter-spacing: 0;
color: var(--text-secondary);
}
.config-method-row {
display: flex;
gap: 4px;
}
.config-method-btn {
flex: 1;
padding: 6px 0;
border: 1px solid var(--border);
border-radius: 0;
background: transparent;
color: var(--text-dim);
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: background 0.12s, color 0.12s;
}
.config-method-btn:hover {
background: var(--hover);
color: var(--text);
}
.config-method-btn.active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.config-order-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.config-order-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: var(--bg-alt);
border: 1px solid var(--border);
cursor: grab;
transition: background 0.08s;
}
.config-order-row:active {
cursor: grabbing;
}
.config-order-row.dragging {
opacity: 0.5;
}
.config-order-label {
font-size: 13px;
color: var(--text);
}
.config-arrows {
display: flex;
gap: 2px;
}
.config-arrow-btn {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: 0;
background: transparent;
color: var(--text-secondary);
font-size: 10px;
cursor: pointer;
transition: background 0.08s;
}
.config-arrow-btn:hover:not(:disabled) {
background: var(--hover);
color: var(--text);
}
.config-arrow-btn:disabled {
opacity: 0.3;
cursor: default;
}
/* Mode bar */ /* Mode bar */
#mode-bar { #mode-bar {
display: flex; display: flex;
+6
View File
@@ -12,9 +12,15 @@
</svg> </svg>
<input id="search-input" type="text" placeholder="Search tabs, history, bookmarks..." autofocus> <input id="search-input" type="text" placeholder="Search tabs, history, bookmarks..." autofocus>
<span id="method-badge">fuzzy</span> <span id="method-badge">fuzzy</span>
<button id="config-btn" title="Settings">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</button>
</div> </div>
<div id="mode-bar"></div> <div id="mode-bar"></div>
<div id="results"></div> <div id="results"></div>
<div id="config-panel" style="display:none"></div>
<div id="footer"> <div id="footer">
<span class="hint"><kbd>&uarr;&darr;</kbd> navigate</span> <span class="hint"><kbd>&uarr;&darr;</kbd> navigate</span>
<span class="hint"><kbd>Enter</kbd> open</span> <span class="hint"><kbd>Enter</kbd> open</span>
+30 -1
View File
@@ -1,4 +1,21 @@
import type { Message, SearchRequest, ActionRequest, SearchItem } from "./types"; import type { Message, SearchRequest, ActionRequest, SearchItem, Settings } from "./types";
const DEFAULT_SETTINGS: Settings = {
defaultMethod: "fuzzy",
modeOrder: ["tabs", "history", "bookmarks", "closed"],
};
async function getSettings(): Promise<Settings> {
const data = await chrome.storage.sync.get(["defaultMethod", "modeOrder"]);
return { ...DEFAULT_SETTINGS, ...data } as Settings;
}
async function saveSettings(partial: Partial<Settings>): Promise<Settings> {
const current = await getSettings();
const updated = { ...current, ...partial };
await chrome.storage.sync.set(updated);
return updated;
}
function sortByRecent(items: SearchItem[]): SearchItem[] { function sortByRecent(items: SearchItem[]): SearchItem[] {
return items.sort((a, b) => (b.lastAccessed ?? 0) - (a.lastAccessed ?? 0)); return items.sort((a, b) => (b.lastAccessed ?? 0) - (a.lastAccessed ?? 0));
@@ -135,5 +152,17 @@ chrome.runtime.onMessage.addListener(
handleAction(message).then(() => sendResponse({ ok: true })); handleAction(message).then(() => sendResponse({ ok: true }));
return true; return true;
} }
if (message.type === "getSettings") {
getSettings().then((settings) => {
sendResponse({ type: "settingsResponse", settings });
});
return true;
}
if (message.type === "saveSettings") {
saveSettings(message.settings).then((settings) => {
sendResponse({ type: "settingsResponse", settings });
});
return true;
}
} }
); );
+1 -1
View File
@@ -98,7 +98,7 @@ window.addEventListener("message", (event) => {
} }
return; return;
} }
if (data.type === "search" || data.type === "action") { if (data.type === "search" || data.type === "action" || data.type === "getSettings" || data.type === "saveSettings") {
chrome.runtime.sendMessage(data, (response: unknown) => { chrome.runtime.sendMessage(data, (response: unknown) => {
const msg = typeof response === "object" && response ? { ...(response as Record<string, unknown>), _nonce: messageNonce } : response; const msg = typeof response === "object" && response ? { ...(response as Record<string, unknown>), _nonce: messageNonce } : response;
iframe?.contentWindow?.postMessage(msg, EXTENSION_ORIGIN); iframe?.contentWindow?.postMessage(msg, EXTENSION_ORIGIN);
+160 -10
View File
@@ -1,7 +1,7 @@
import { search } from "./search"; import { search } from "./search";
import type { SearchItem, SearchMode, SearchMethod, SearchResult, Message } from "./types"; import type { SearchItem, SearchMode, SearchMethod, SearchResult, Message, Settings } from "./types";
const MODES: SearchMode[] = ["tabs", "history", "bookmarks", "closed"]; const ALL_MODES: SearchMode[] = ["tabs", "history", "bookmarks", "closed"];
const MODE_LABELS: Record<SearchMode, string> = { const MODE_LABELS: Record<SearchMode, string> = {
tabs: "Tabs", tabs: "Tabs",
history: "History", history: "History",
@@ -9,20 +9,29 @@ const MODE_LABELS: Record<SearchMode, string> = {
closed: "Closed", closed: "Closed",
}; };
const METHODS: SearchMethod[] = ["fuzzy", "fulltext", "prefix"]; const METHODS: SearchMethod[] = ["fuzzy", "fulltext", "prefix"];
const METHOD_LABELS: Record<SearchMethod, string> = {
fuzzy: "Fuzzy",
fulltext: "Full Text",
prefix: "Prefix",
};
const MESSAGE_NONCE = location.hash.slice(1); const MESSAGE_NONCE = location.hash.slice(1);
let modes: SearchMode[] = [...ALL_MODES];
let currentMode: SearchMode = "tabs"; let currentMode: SearchMode = "tabs";
let currentMethod: SearchMethod = "fuzzy"; let currentMethod: SearchMethod = "fuzzy";
let results: SearchResult[] = []; let results: SearchResult[] = [];
let selectedIndex = 0; let selectedIndex = 0;
let debounceTimer: ReturnType<typeof setTimeout> | null = null; let debounceTimer: ReturnType<typeof setTimeout> | null = null;
let configOpen = false;
const input = document.getElementById("search-input") as HTMLInputElement; const input = document.getElementById("search-input") as HTMLInputElement;
const resultsContainer = document.getElementById("results") as HTMLDivElement; const resultsContainer = document.getElementById("results") as HTMLDivElement;
const modeBar = document.getElementById("mode-bar") as HTMLDivElement; const modeBar = document.getElementById("mode-bar") as HTMLDivElement;
const methodBadge = document.getElementById("method-badge") as HTMLSpanElement; const methodBadge = document.getElementById("method-badge") as HTMLSpanElement;
const root = document.getElementById("sciezka-root") as HTMLDivElement; const root = document.getElementById("sciezka-root") as HTMLDivElement;
const configPanel = document.getElementById("config-panel") as HTMLDivElement;
const configBtn = document.getElementById("config-btn") as HTMLButtonElement;
function notifyResize(): void { function notifyResize(): void {
const height = Math.min(root.scrollHeight, 520); const height = Math.min(root.scrollHeight, 520);
@@ -42,10 +51,17 @@ function sendMessage(msg: Message): Promise<unknown> {
}); });
} }
function persistSettings(): void {
sendMessage({
type: "saveSettings",
settings: { defaultMethod: currentMethod, modeOrder: modes },
} as Message);
}
function renderModeBar(): void { function renderModeBar(): void {
modeBar.innerHTML = ""; modeBar.innerHTML = "";
for (let i = 0; i < MODES.length; i++) { for (let i = 0; i < modes.length; i++) {
const mode = MODES[i]; const mode = modes[i];
const btn = document.createElement("button"); const btn = document.createElement("button");
btn.className = `mode-btn${mode === currentMode ? " active" : ""}`; btn.className = `mode-btn${mode === currentMode ? " active" : ""}`;
btn.innerHTML = `<span class="mode-label">${MODE_LABELS[mode]}</span><kbd>${i + 1}</kbd>`; btn.innerHTML = `<span class="mode-label">${MODE_LABELS[mode]}</span><kbd>${i + 1}</kbd>`;
@@ -62,6 +78,114 @@ function renderMethodBadge(): void {
methodBadge.textContent = currentMethod; methodBadge.textContent = currentMethod;
} }
function toggleConfig(): void {
configOpen = !configOpen;
configBtn.classList.toggle("active", configOpen);
if (configOpen) {
resultsContainer.style.display = "none";
configPanel.style.display = "block";
renderConfigPanel();
} else {
configPanel.style.display = "none";
resultsContainer.style.display = "";
}
notifyResize();
}
function renderConfigPanel(): void {
configPanel.innerHTML = "";
const methodSection = document.createElement("div");
methodSection.className = "config-section";
methodSection.innerHTML = `<div class="config-label">Search Method</div>`;
const methodRow = document.createElement("div");
methodRow.className = "config-method-row";
for (const m of METHODS) {
const btn = document.createElement("button");
btn.className = `config-method-btn${m === currentMethod ? " active" : ""}`;
btn.textContent = METHOD_LABELS[m];
btn.addEventListener("click", () => {
currentMethod = m;
renderMethodBadge();
renderConfigPanel();
persistSettings();
doSearch();
});
methodRow.appendChild(btn);
}
methodSection.appendChild(methodRow);
configPanel.appendChild(methodSection);
const orderSection = document.createElement("div");
orderSection.className = "config-section";
orderSection.innerHTML = `<div class="config-label">Tab Order <span class="config-hint">drag or use arrows</span></div>`;
const orderList = document.createElement("div");
orderList.className = "config-order-list";
for (let i = 0; i < modes.length; i++) {
const mode = modes[i];
const row = document.createElement("div");
row.className = "config-order-row";
row.draggable = true;
row.dataset.index = String(i);
const label = document.createElement("span");
label.className = "config-order-label";
label.textContent = MODE_LABELS[mode];
const arrows = document.createElement("span");
arrows.className = "config-arrows";
const upBtn = document.createElement("button");
upBtn.className = "config-arrow-btn";
upBtn.textContent = "\u25B2";
upBtn.disabled = i === 0;
upBtn.addEventListener("click", () => { swapModes(i, i - 1); });
const downBtn = document.createElement("button");
downBtn.className = "config-arrow-btn";
downBtn.textContent = "\u25BC";
downBtn.disabled = i === modes.length - 1;
downBtn.addEventListener("click", () => { swapModes(i, i + 1); });
arrows.appendChild(upBtn);
arrows.appendChild(downBtn);
row.appendChild(label);
row.appendChild(arrows);
row.addEventListener("dragstart", (e) => {
e.dataTransfer?.setData("text/plain", String(i));
row.classList.add("dragging");
});
row.addEventListener("dragend", () => { row.classList.remove("dragging"); });
row.addEventListener("dragover", (e) => { e.preventDefault(); });
row.addEventListener("drop", (e) => {
e.preventDefault();
const from = parseInt(e.dataTransfer?.getData("text/plain") ?? "", 10);
if (!isNaN(from) && from !== i) {
const moved = modes.splice(from, 1)[0];
modes.splice(i, 0, moved);
if (currentMode === modes[0]) currentMode = modes[0];
renderModeBar();
renderConfigPanel();
persistSettings();
}
});
orderList.appendChild(row);
}
orderSection.appendChild(orderList);
configPanel.appendChild(orderSection);
notifyResize();
}
function swapModes(a: number, b: number): void {
[modes[a], modes[b]] = [modes[b], modes[a]];
renderModeBar();
renderConfigPanel();
persistSettings();
}
function highlightText(text: string, positions: number[], offset: number): string { function highlightText(text: string, positions: number[], offset: number): string {
const posSet = new Set(positions.map((p) => p - offset).filter((p) => p >= 0 && p < text.length)); const posSet = new Set(positions.map((p) => p - offset).filter((p) => p >= 0 && p < text.length));
let result = ""; let result = "";
@@ -188,12 +312,20 @@ input.addEventListener("input", () => {
debounceTimer = setTimeout(doSearch, 50); debounceTimer = setTimeout(doSearch, 50);
}); });
configBtn.addEventListener("click", toggleConfig);
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {
if (e.key === "Escape") { if (e.key === "Escape") {
if (configOpen) {
toggleConfig();
return;
}
window.parent.postMessage({ type: "closeSaka", _nonce: MESSAGE_NONCE }, "*"); window.parent.postMessage({ type: "closeSaka", _nonce: MESSAGE_NONCE }, "*");
return; return;
} }
if (configOpen) return;
if (e.key === "ArrowDown" || (e.ctrlKey && e.key === "j")) { if (e.key === "ArrowDown" || (e.ctrlKey && e.key === "j")) {
e.preventDefault(); e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, results.length - 1); selectedIndex = Math.min(selectedIndex + 1, results.length - 1);
@@ -227,8 +359,8 @@ document.addEventListener("keydown", (e) => {
if (e.key === "Tab") { if (e.key === "Tab") {
e.preventDefault(); e.preventDefault();
const dir = e.shiftKey ? -1 : 1; const dir = e.shiftKey ? -1 : 1;
const idx = MODES.indexOf(currentMode); const idx = modes.indexOf(currentMode);
currentMode = MODES[(idx + dir + MODES.length) % MODES.length]; currentMode = modes[(idx + dir + modes.length) % modes.length];
renderModeBar(); renderModeBar();
doSearch(); doSearch();
return; return;
@@ -239,6 +371,7 @@ document.addEventListener("keydown", (e) => {
const idx = METHODS.indexOf(currentMethod); const idx = METHODS.indexOf(currentMethod);
currentMethod = METHODS[(idx + 1) % METHODS.length]; currentMethod = METHODS[(idx + 1) % METHODS.length];
renderMethodBadge(); renderMethodBadge();
persistSettings();
doSearch(); doSearch();
return; return;
} }
@@ -255,11 +388,11 @@ document.addEventListener("keydown", (e) => {
return; return;
} }
if (e.ctrlKey && e.key >= "1" && e.key <= "5") { if (e.ctrlKey && e.key >= "1" && e.key <= "4") {
e.preventDefault(); e.preventDefault();
const modeIdx = parseInt(e.key, 10) - 1; const modeIdx = parseInt(e.key, 10) - 1;
if (modeIdx < MODES.length) { if (modeIdx < modes.length) {
currentMode = MODES[modeIdx]; currentMode = modes[modeIdx];
renderModeBar(); renderModeBar();
doSearch(); doSearch();
} }
@@ -267,7 +400,24 @@ document.addEventListener("keydown", (e) => {
} }
}); });
document.addEventListener("DOMContentLoaded", () => { async function loadSettings(): Promise<void> {
try {
const response = await sendMessage({ type: "getSettings" } as Message);
const data = response as { type: string; settings: Settings };
if (data?.settings) {
currentMethod = data.settings.defaultMethod;
if (data.settings.modeOrder?.length) {
modes = data.settings.modeOrder;
currentMode = modes[0];
}
}
} catch {
// defaults
}
}
document.addEventListener("DOMContentLoaded", async () => {
await loadSettings();
renderModeBar(); renderModeBar();
renderMethodBadge(); renderMethodBadge();
input.focus(); input.focus();
+23 -1
View File
@@ -48,10 +48,32 @@ export interface ResizeMessage {
height: number; height: number;
} }
export interface Settings {
defaultMethod: SearchMethod;
modeOrder: SearchMode[];
}
export interface GetSettingsRequest {
type: "getSettings";
}
export interface SaveSettingsRequest {
type: "saveSettings";
settings: Partial<Settings>;
}
export interface SettingsResponse {
type: "settingsResponse";
settings: Settings;
}
export type Message = export type Message =
| SearchRequest | SearchRequest
| SearchResponse | SearchResponse
| ActionRequest | ActionRequest
| ToggleMessage | ToggleMessage
| CloseMessage | CloseMessage
| ResizeMessage; | ResizeMessage
| GetSettingsRequest
| SaveSettingsRequest
| SettingsResponse;