mirror of
https://github.com/avinal/sciezka.git
synced 2026-07-03 23:30:09 +05:30
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:
+142
@@ -149,6 +149,148 @@ body {
|
||||
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 {
|
||||
display: flex;
|
||||
|
||||
@@ -12,9 +12,15 @@
|
||||
</svg>
|
||||
<input id="search-input" type="text" placeholder="Search tabs, history, bookmarks..." autofocus>
|
||||
<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 id="mode-bar"></div>
|
||||
<div id="results"></div>
|
||||
<div id="config-panel" style="display:none"></div>
|
||||
<div id="footer">
|
||||
<span class="hint"><kbd>↑↓</kbd> navigate</span>
|
||||
<span class="hint"><kbd>Enter</kbd> open</span>
|
||||
|
||||
+30
-1
@@ -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[] {
|
||||
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 }));
|
||||
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
@@ -98,7 +98,7 @@ window.addEventListener("message", (event) => {
|
||||
}
|
||||
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) => {
|
||||
const msg = typeof response === "object" && response ? { ...(response as Record<string, unknown>), _nonce: messageNonce } : response;
|
||||
iframe?.contentWindow?.postMessage(msg, EXTENSION_ORIGIN);
|
||||
|
||||
+160
-10
@@ -1,7 +1,7 @@
|
||||
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> = {
|
||||
tabs: "Tabs",
|
||||
history: "History",
|
||||
@@ -9,20 +9,29 @@ const MODE_LABELS: Record<SearchMode, string> = {
|
||||
closed: "Closed",
|
||||
};
|
||||
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);
|
||||
|
||||
let modes: SearchMode[] = [...ALL_MODES];
|
||||
let currentMode: SearchMode = "tabs";
|
||||
let currentMethod: SearchMethod = "fuzzy";
|
||||
let results: SearchResult[] = [];
|
||||
let selectedIndex = 0;
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let configOpen = false;
|
||||
|
||||
const input = document.getElementById("search-input") as HTMLInputElement;
|
||||
const resultsContainer = document.getElementById("results") as HTMLDivElement;
|
||||
const modeBar = document.getElementById("mode-bar") as HTMLDivElement;
|
||||
const methodBadge = document.getElementById("method-badge") as HTMLSpanElement;
|
||||
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 {
|
||||
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 {
|
||||
modeBar.innerHTML = "";
|
||||
for (let i = 0; i < MODES.length; i++) {
|
||||
const mode = MODES[i];
|
||||
for (let i = 0; i < modes.length; i++) {
|
||||
const mode = modes[i];
|
||||
const btn = document.createElement("button");
|
||||
btn.className = `mode-btn${mode === currentMode ? " active" : ""}`;
|
||||
btn.innerHTML = `<span class="mode-label">${MODE_LABELS[mode]}</span><kbd>${i + 1}</kbd>`;
|
||||
@@ -62,6 +78,114 @@ function renderMethodBadge(): void {
|
||||
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 {
|
||||
const posSet = new Set(positions.map((p) => p - offset).filter((p) => p >= 0 && p < text.length));
|
||||
let result = "";
|
||||
@@ -188,12 +312,20 @@ input.addEventListener("input", () => {
|
||||
debounceTimer = setTimeout(doSearch, 50);
|
||||
});
|
||||
|
||||
configBtn.addEventListener("click", toggleConfig);
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
if (configOpen) {
|
||||
toggleConfig();
|
||||
return;
|
||||
}
|
||||
window.parent.postMessage({ type: "closeSaka", _nonce: MESSAGE_NONCE }, "*");
|
||||
return;
|
||||
}
|
||||
|
||||
if (configOpen) return;
|
||||
|
||||
if (e.key === "ArrowDown" || (e.ctrlKey && e.key === "j")) {
|
||||
e.preventDefault();
|
||||
selectedIndex = Math.min(selectedIndex + 1, results.length - 1);
|
||||
@@ -227,8 +359,8 @@ document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
const dir = e.shiftKey ? -1 : 1;
|
||||
const idx = MODES.indexOf(currentMode);
|
||||
currentMode = MODES[(idx + dir + MODES.length) % MODES.length];
|
||||
const idx = modes.indexOf(currentMode);
|
||||
currentMode = modes[(idx + dir + modes.length) % modes.length];
|
||||
renderModeBar();
|
||||
doSearch();
|
||||
return;
|
||||
@@ -239,6 +371,7 @@ document.addEventListener("keydown", (e) => {
|
||||
const idx = METHODS.indexOf(currentMethod);
|
||||
currentMethod = METHODS[(idx + 1) % METHODS.length];
|
||||
renderMethodBadge();
|
||||
persistSettings();
|
||||
doSearch();
|
||||
return;
|
||||
}
|
||||
@@ -255,11 +388,11 @@ document.addEventListener("keydown", (e) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.ctrlKey && e.key >= "1" && e.key <= "5") {
|
||||
if (e.ctrlKey && e.key >= "1" && e.key <= "4") {
|
||||
e.preventDefault();
|
||||
const modeIdx = parseInt(e.key, 10) - 1;
|
||||
if (modeIdx < MODES.length) {
|
||||
currentMode = MODES[modeIdx];
|
||||
if (modeIdx < modes.length) {
|
||||
currentMode = modes[modeIdx];
|
||||
renderModeBar();
|
||||
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();
|
||||
renderMethodBadge();
|
||||
input.focus();
|
||||
|
||||
+23
-1
@@ -48,10 +48,32 @@ export interface ResizeMessage {
|
||||
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 =
|
||||
| SearchRequest
|
||||
| SearchResponse
|
||||
| ActionRequest
|
||||
| ToggleMessage
|
||||
| CloseMessage
|
||||
| ResizeMessage;
|
||||
| ResizeMessage
|
||||
| GetSettingsRequest
|
||||
| SaveSettingsRequest
|
||||
| SettingsResponse;
|
||||
|
||||
Reference in New Issue
Block a user