Merge pull request 'Add theme switcher, tool search, and vendored Bootstrap Icons (v0.6.4)' (#1) from theme-switcher into main
Reviewed-on: https://codeberg.org/listyantidewi/your-everyday-tools/pulls/1
@@ -2,6 +2,19 @@
|
||||
|
||||
All notable changes to **Your Everyday Tools** are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project loosely follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.6.4] — 2026-06-07
|
||||
|
||||
### Added — UI navigation and theming
|
||||
|
||||
- Added a theme switcher (system / light / dark) in the top bar. Preference is persisted in `localStorage` and applied before first paint to avoid flash.
|
||||
- Added tool search on the home page — filter tool cards by name or description.
|
||||
- Added global search in the top bar — available on every page with a dropdown of matching tools.
|
||||
|
||||
### Changed — Icons and theme UX
|
||||
|
||||
- Replaced the Bootstrap Icons shim with fully vendored font files (`woff` / `woff2`) for offline use.
|
||||
- Theme transitions animate smoothly; the active theme button shows a brief spin on click.
|
||||
|
||||
## [0.6.3] — 2026-06-06
|
||||
|
||||
### Added — Local conversion fidelity layer
|
||||
|
||||
@@ -365,8 +365,8 @@ your-everyday-tools/
|
||||
│ ├── media_tools.py # FFmpeg-powered audio & video tools
|
||||
│ └── capabilities.py # /capabilities endpoint
|
||||
├── templates/
|
||||
│ ├── base.html # Main layout (sidebar + content area)
|
||||
│ ├── index.html # Home page with tool cards
|
||||
│ ├── base.html # Main layout (sidebar, global search, theme switcher)
|
||||
│ ├── index.html # Home page with tool cards and search
|
||||
│ ├── upload_tool.html # Universal template for all file-based tools
|
||||
│ └── tools/ # Individual client-side tool templates
|
||||
│ ├── calculator.html
|
||||
@@ -393,8 +393,9 @@ your-everyday-tools/
|
||||
│ └── hash_generator.html
|
||||
└── static/
|
||||
├── css/style.css # All styles, no framework
|
||||
├── css/icons.css # Local icon shim; no CDN required
|
||||
└── js/main.js # File upload, AJAX, sidebar, shared logic
|
||||
├── css/icons.css # Vendored Bootstrap Icons; no CDN required
|
||||
├── fonts/bootstrap-icons.woff2 # Bootstrap Icons font files
|
||||
└── js/main.js # Sidebar, theme, search, file upload, shared logic
|
||||
```
|
||||
|
||||
### Architecture Notes
|
||||
@@ -402,7 +403,7 @@ your-everyday-tools/
|
||||
- **One universal template** — `upload_tool.html` powers all 25+ server-side tools. Each route passes title, description, accepted file types, and form options as template variables. No per-tool template duplication.
|
||||
- **Client-side tools** (text utilities, calculators, security tools) run entirely in the browser with vanilla JavaScript — zero server round-trips.
|
||||
- **Local-first processing** — pure browser tools never leave the page; server routes process files locally. Some engines such as LibreOffice, FFmpeg, ODA, and pdf2docx use isolated temporary directories when their CLI/library workflow requires files.
|
||||
- **No CSS framework or CDN dependency** — custom CSS with CSS Grid, Flexbox, CSS custom properties, and a local icon shim.
|
||||
- **No CSS framework or CDN dependency** — custom CSS with CSS Grid, Flexbox, CSS custom properties, and vendored Bootstrap Icons.
|
||||
- **Graceful degradation** — optional packages and external binaries (`LibreOffice`, `FFmpeg`, `ffprobe`, `Tesseract`, ODA File Converter, `rembg`, `pyzbar`, `pdf2docx`, `pdfplumber`, `pytesseract`, `pillow-heif`, Whisper, etc.) are reported through `/capabilities` and tool-page status banners. Missing high-fidelity engines either show a clear unavailable state or require explicit basic fallback consent.
|
||||
|
||||
---
|
||||
|
||||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 461 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 491 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 230 KiB |
@@ -7,17 +7,57 @@
|
||||
--success: #2ec4b6;
|
||||
--danger: #e63946;
|
||||
--warning: #f4a261;
|
||||
/* surface palette */
|
||||
--bg: #f5f6fa;
|
||||
--surface: #ffffff;
|
||||
--text: #2d3436;
|
||||
--text-light: #636e72;
|
||||
--muted: var(--text-light);
|
||||
--border: #dfe6e9;
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
/* semantic accent */
|
||||
--accent-soft: #eef1ff;
|
||||
--accent-soft-hover: #dde3ff;
|
||||
/* notes/warning block */
|
||||
--note-bg: #fff8e1;
|
||||
--note-border: #f5b700;
|
||||
--note-text: #5a4200;
|
||||
--note-summary: #8a6300;
|
||||
--note-link: #b26f00;
|
||||
--note-code-bg: rgba(0,0,0,.06);
|
||||
/* upload hover */
|
||||
--upload-hover-bg: #f0f3ff;
|
||||
/* result states */
|
||||
--result-success-bg: #e8faf8;
|
||||
--result-success-text: #1a7a6d;
|
||||
--result-error-bg: #fdeaea;
|
||||
/* layout */
|
||||
--sidebar-w: 260px;
|
||||
--radius: 8px;
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg: #1a1d23;
|
||||
--surface: #22262e;
|
||||
--text: #e4e6eb;
|
||||
--text-light: #9aa0ac;
|
||||
--border: #3a3f4b;
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.35);
|
||||
--accent-soft: rgba(67,97,238,.2);
|
||||
--accent-soft-hover: rgba(67,97,238,.35);
|
||||
--note-bg: #2a2000;
|
||||
--note-border: #c49a00;
|
||||
--note-text: #f0c040;
|
||||
--note-summary: #d4a800;
|
||||
--note-link: #e8c000;
|
||||
--note-code-bg: rgba(255,255,255,.08);
|
||||
--upload-hover-bg: rgba(67,97,238,.12);
|
||||
--result-success-bg: #082e2a;
|
||||
--result-success-text: #4ecdc4;
|
||||
--result-error-bg: #2d0a0a;
|
||||
}
|
||||
|
||||
html { font-size: 15px; }
|
||||
body {
|
||||
font-family: var(--font);
|
||||
@@ -98,7 +138,7 @@ a { color: var(--primary); text-decoration: none; }
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
.nav-item:hover, .nav-item.active {
|
||||
background: #eef1ff;
|
||||
background: var(--accent-soft);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -123,6 +163,87 @@ a { color: var(--primary); text-decoration: none; }
|
||||
z-index: 50;
|
||||
}
|
||||
.top-title { font-weight: 600; font-size: 1.05rem; }
|
||||
|
||||
.global-search {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
max-width: 360px;
|
||||
}
|
||||
.global-search input {
|
||||
width: 100%;
|
||||
padding: .4rem .8rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: .9rem;
|
||||
font-family: var(--font);
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.global-search input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(67,97,238,.12);
|
||||
}
|
||||
.global-search-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + .4rem);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
z-index: 200;
|
||||
}
|
||||
.global-search-dropdown.open { display: block; }
|
||||
.global-search-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
padding: .55rem .85rem;
|
||||
color: var(--text);
|
||||
font-size: .875rem;
|
||||
text-decoration: none;
|
||||
transition: background .1s;
|
||||
}
|
||||
.global-search-result:hover,
|
||||
.global-search-result.active { background: var(--bg); color: var(--text); }
|
||||
.global-search-result .result-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.global-search-result .result-name { font-weight: 500; }
|
||||
.global-search-result .result-desc {
|
||||
font-size: .75rem;
|
||||
color: var(--text-light);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.global-search-result .result-cat {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
font-size: .72rem;
|
||||
color: var(--text-light);
|
||||
white-space: nowrap;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: .1rem .4rem;
|
||||
}
|
||||
.global-search-empty {
|
||||
padding: .8rem;
|
||||
color: var(--text-light);
|
||||
font-size: .85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
display: none;
|
||||
border: none;
|
||||
@@ -132,6 +253,58 @@ a { color: var(--primary); text-decoration: none; }
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Theme Switcher ───────────────────────────── */
|
||||
.theme-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 3px;
|
||||
margin-left: auto;
|
||||
transition: background .3s ease, border-color .3s ease;
|
||||
}
|
||||
.theme-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-light);
|
||||
border-radius: 16px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: .9rem;
|
||||
transition: background .2s ease, color .2s ease, transform .2s ease, box-shadow .2s ease;
|
||||
}
|
||||
.theme-btn:hover { background: var(--border); color: var(--text); transform: scale(1.1); }
|
||||
.theme-btn:active { transform: scale(.92); }
|
||||
.theme-btn.active {
|
||||
background: var(--surface);
|
||||
color: var(--primary);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.18);
|
||||
}
|
||||
.theme-btn i {
|
||||
transition: transform .35s ease;
|
||||
}
|
||||
.theme-btn.spinning i {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
/* ── Theme transition (only when user switches, not on page load) ── */
|
||||
html.theme-animate,
|
||||
html.theme-animate *,
|
||||
html.theme-animate *::before,
|
||||
html.theme-animate *::after {
|
||||
transition:
|
||||
background-color .35s ease,
|
||||
color .35s ease,
|
||||
border-color .35s ease,
|
||||
box-shadow .35s ease !important;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
@@ -180,6 +353,34 @@ a { color: var(--primary); text-decoration: none; }
|
||||
.home-hero h1 { font-size: 2rem; margin-bottom: .4rem; }
|
||||
.home-hero p { color: var(--text-light); font-size: 1.05rem; }
|
||||
|
||||
.home-search {
|
||||
margin: 1.25rem auto 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
.home-search input {
|
||||
width: 100%;
|
||||
padding: .55rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-size: .95rem;
|
||||
font-family: var(--font);
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.home-search input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(67,97,238,.12);
|
||||
}
|
||||
|
||||
.search-empty {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-light);
|
||||
font-size: .95rem;
|
||||
}
|
||||
|
||||
.category-section { margin-bottom: 2rem; }
|
||||
.category-title {
|
||||
font-size: 1.1rem;
|
||||
@@ -227,19 +428,19 @@ a { color: var(--primary); text-decoration: none; }
|
||||
.tool-header p { color: var(--text-light); }
|
||||
|
||||
.tool-notes {
|
||||
background: #fff8e1;
|
||||
border-left: 3px solid #f5b700;
|
||||
background: var(--note-bg);
|
||||
border-left: 3px solid var(--note-border);
|
||||
border-radius: 4px;
|
||||
padding: .75rem 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: .88rem;
|
||||
line-height: 1.5;
|
||||
color: #5a4200;
|
||||
color: var(--note-text);
|
||||
}
|
||||
.tool-notes p { margin: 0 0 .5rem; }
|
||||
.tool-notes p:last-child { margin-bottom: 0; }
|
||||
.tool-notes code {
|
||||
background: rgba(0,0,0,.06);
|
||||
background: var(--note-code-bg);
|
||||
padding: .1rem .35rem;
|
||||
border-radius: 3px;
|
||||
font-size: .82rem;
|
||||
@@ -248,12 +449,12 @@ a { color: var(--primary); text-decoration: none; }
|
||||
.tool-notes details > summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #8a6300;
|
||||
color: var(--note-summary);
|
||||
}
|
||||
.tool-notes details[open] > summary { margin-bottom: .5rem; }
|
||||
.tool-notes ol, .tool-notes ul { margin: .25rem 0 .5rem 1.2rem; padding: 0; }
|
||||
.tool-notes li { margin-bottom: .2rem; }
|
||||
.tool-notes a { color: #b26f00; }
|
||||
.tool-notes a { color: var(--note-link); }
|
||||
|
||||
.capability-status {
|
||||
border: 1px solid var(--border);
|
||||
@@ -308,7 +509,7 @@ a { color: var(--primary); text-decoration: none; }
|
||||
}
|
||||
.upload-zone:hover, .upload-zone.dragover {
|
||||
border-color: var(--primary);
|
||||
background: #f0f3ff;
|
||||
background: var(--upload-hover-bg);
|
||||
}
|
||||
.upload-zone input[type="file"] {
|
||||
position: absolute;
|
||||
@@ -362,6 +563,7 @@ textarea {
|
||||
font: inherit;
|
||||
font-size: .9rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
transition: border-color .15s;
|
||||
}
|
||||
input:focus, select:focus, textarea:focus {
|
||||
@@ -397,12 +599,12 @@ textarea { resize: vertical; }
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.result-success {
|
||||
background: #e8faf8;
|
||||
color: #1a7a6d;
|
||||
background: var(--result-success-bg);
|
||||
color: var(--result-success-text);
|
||||
}
|
||||
.result-success .btn { margin-left: auto; }
|
||||
.result-error {
|
||||
background: #fdeaea;
|
||||
background: var(--result-error-bg);
|
||||
color: var(--danger);
|
||||
}
|
||||
.result-text-box {
|
||||
@@ -474,6 +676,8 @@ textarea { resize: vertical; }
|
||||
font-family: "Consolas", "Monaco", monospace;
|
||||
font-size: .85rem;
|
||||
outline: none;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
.pane-body pre {
|
||||
min-height: 250px;
|
||||
@@ -511,8 +715,8 @@ textarea { resize: vertical; }
|
||||
transition: background .1s;
|
||||
}
|
||||
.calc-btn:hover { background: var(--bg); }
|
||||
.calc-btn.op { background: #eef1ff; color: var(--primary); font-weight: 600; }
|
||||
.calc-btn.op:hover { background: #dde3ff; }
|
||||
.calc-btn.op { background: var(--accent-soft); color: var(--primary); font-weight: 600; }
|
||||
.calc-btn.op:hover { background: var(--accent-soft-hover); }
|
||||
.calc-btn.eq { background: var(--primary); color: #fff; }
|
||||
.calc-btn.eq:hover { background: var(--primary-dark); }
|
||||
.calc-btn.span2 { grid-column: span 2; }
|
||||
|
||||
@@ -1,3 +1,61 @@
|
||||
/* ── Theme ────────────────────────────────────── */
|
||||
const THEME_KEY = "theme";
|
||||
|
||||
function getStoredTheme() {
|
||||
return localStorage.getItem(THEME_KEY) || "system";
|
||||
}
|
||||
|
||||
function resolveTheme(mode) {
|
||||
if (mode === "dark") return "dark";
|
||||
if (mode === "light") return "light";
|
||||
return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light";
|
||||
}
|
||||
|
||||
function applyTheme(mode, animate) {
|
||||
localStorage.setItem(THEME_KEY, mode);
|
||||
const html = document.documentElement;
|
||||
|
||||
if (animate) {
|
||||
html.classList.add("theme-animate");
|
||||
}
|
||||
|
||||
html.dataset.theme = resolveTheme(mode);
|
||||
|
||||
document.querySelectorAll(".theme-btn").forEach(btn => {
|
||||
btn.classList.toggle("active", btn.dataset.themeMode === mode);
|
||||
});
|
||||
|
||||
if (animate) {
|
||||
clearTimeout(applyTheme._timer);
|
||||
applyTheme._timer = setTimeout(() => html.classList.remove("theme-animate"), 400);
|
||||
}
|
||||
}
|
||||
|
||||
function initTheme() {
|
||||
const mode = getStoredTheme();
|
||||
applyTheme(mode, false);
|
||||
document.querySelectorAll(".theme-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
// Spin the icon
|
||||
const icon = btn.querySelector("i");
|
||||
if (icon) {
|
||||
btn.classList.remove("spinning");
|
||||
void btn.offsetWidth; // force reflow to restart animation
|
||||
btn.classList.add("spinning");
|
||||
setTimeout(() => btn.classList.remove("spinning"), 380);
|
||||
}
|
||||
applyTheme(btn.dataset.themeMode, true);
|
||||
});
|
||||
});
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
||||
if (getStoredTheme() === "system") {
|
||||
document.documentElement.dataset.theme = resolveTheme("system");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Sidebar ──────────────────────────────────── */
|
||||
function toggleCategory(btn) {
|
||||
btn.classList.toggle("open");
|
||||
@@ -30,12 +88,152 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
});
|
||||
|
||||
initTheme();
|
||||
initToolSearch();
|
||||
initGlobalSearch();
|
||||
initUploadZone();
|
||||
initToolForm();
|
||||
initDependentOptions();
|
||||
initCapabilityStatus();
|
||||
});
|
||||
|
||||
/* ── Global Toolbar Search ────────────────────── */
|
||||
function initGlobalSearch() {
|
||||
const wrapper = document.getElementById("global-search");
|
||||
const input = document.getElementById("global-search-input");
|
||||
const dropdown = document.getElementById("global-search-dropdown");
|
||||
if (!wrapper || !input || !dropdown) return;
|
||||
|
||||
// Hide on home page; show everywhere else
|
||||
if (window.location.pathname === "/") {
|
||||
wrapper.style.display = "none";
|
||||
return;
|
||||
}
|
||||
wrapper.style.display = "";
|
||||
|
||||
// Build tool list from sidebar nav items (already in DOM)
|
||||
const tools = [];
|
||||
document.querySelectorAll(".nav-category").forEach(cat => {
|
||||
const catBtn = cat.querySelector(".nav-category-btn");
|
||||
const catName = catBtn ? catBtn.textContent.trim() : "";
|
||||
cat.querySelectorAll(".nav-item").forEach(a => {
|
||||
tools.push({
|
||||
name: a.textContent.trim(),
|
||||
href: a.getAttribute("href"),
|
||||
cat: catName,
|
||||
desc: a.dataset.desc || "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let activeIdx = -1;
|
||||
|
||||
function openDropdown() {
|
||||
dropdown.classList.add("open");
|
||||
input.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
dropdown.classList.remove("open");
|
||||
input.setAttribute("aria-expanded", "false");
|
||||
activeIdx = -1;
|
||||
}
|
||||
|
||||
function renderResults(query) {
|
||||
if (!query) { closeDropdown(); return; }
|
||||
|
||||
const matches = tools.filter(t =>
|
||||
t.name.toLowerCase().includes(query) ||
|
||||
t.cat.toLowerCase().includes(query) ||
|
||||
t.desc.includes(query)
|
||||
).slice(0, 8);
|
||||
|
||||
if (matches.length === 0) {
|
||||
dropdown.innerHTML = `<div class="global-search-empty">No tools found.</div>`;
|
||||
openDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.innerHTML = matches.map((t, i) => `
|
||||
<a href="${t.href}" class="global-search-result" role="option" data-idx="${i}">
|
||||
<span class="result-info">
|
||||
<span class="result-name">${escapeHtml(t.name)}</span>
|
||||
<span class="result-desc">${escapeHtml(t.desc)}</span>
|
||||
</span>
|
||||
<span class="result-cat">${escapeHtml(t.cat)}</span>
|
||||
</a>
|
||||
`).join("");
|
||||
activeIdx = -1;
|
||||
openDropdown();
|
||||
}
|
||||
|
||||
function setActive(idx) {
|
||||
const items = dropdown.querySelectorAll(".global-search-result");
|
||||
items.forEach(el => el.classList.remove("active"));
|
||||
if (idx >= 0 && idx < items.length) {
|
||||
items[idx].classList.add("active");
|
||||
items[idx].scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
activeIdx = idx;
|
||||
}
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
renderResults(input.value.trim().toLowerCase());
|
||||
});
|
||||
|
||||
input.addEventListener("keydown", e => {
|
||||
const items = dropdown.querySelectorAll(".global-search-result");
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setActive(Math.min(activeIdx + 1, items.length - 1));
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setActive(Math.max(activeIdx - 1, 0));
|
||||
} else if (e.key === "Enter") {
|
||||
if (activeIdx >= 0 && items[activeIdx]) {
|
||||
window.location.href = items[activeIdx].getAttribute("href");
|
||||
}
|
||||
} else if (e.key === "Escape") {
|
||||
input.blur();
|
||||
closeDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", e => {
|
||||
if (!wrapper.contains(e.target)) closeDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Home Page Tool Search ────────────────────── */
|
||||
function initToolSearch() {
|
||||
const input = document.getElementById("tool-search");
|
||||
if (!input) return;
|
||||
|
||||
const cards = Array.from(document.querySelectorAll(".tool-card"));
|
||||
const sections = Array.from(document.querySelectorAll(".category-section"));
|
||||
const empty = document.getElementById("search-empty");
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const query = input.value.trim().toLowerCase();
|
||||
|
||||
cards.forEach(card => {
|
||||
const match = !query || card.dataset.search.includes(query);
|
||||
card.style.display = match ? "" : "none";
|
||||
});
|
||||
|
||||
sections.forEach(section => {
|
||||
const hasVisible = Array.from(section.querySelectorAll(".tool-card"))
|
||||
.some(c => c.style.display !== "none");
|
||||
section.style.display = hasVisible ? "" : "none";
|
||||
});
|
||||
|
||||
if (empty) {
|
||||
const anyVisible = cards.some(c => c.style.display !== "none");
|
||||
empty.style.display = anyVisible ? "none" : "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function initCapabilityStatus() {
|
||||
const box = document.getElementById("capability-status");
|
||||
if (!box) return;
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Your Everyday Tools{% endblock %}</title>
|
||||
<script>
|
||||
(function () {
|
||||
var stored = localStorage.getItem('theme') || 'system';
|
||||
var resolved = stored === 'dark' ? 'dark'
|
||||
: stored === 'light' ? 'light'
|
||||
: (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/icons.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
@@ -21,7 +30,7 @@
|
||||
</button>
|
||||
<div class="nav-items">
|
||||
{% for tool in cat.tools %}
|
||||
<a href="/{{ cat.id }}/{{ tool.id }}" class="nav-item">
|
||||
<a href="/{{ cat.id }}/{{ tool.id }}" class="nav-item" data-desc="{{ tool.desc | lower }}">
|
||||
<i class="bi {{ tool.icon }}"></i> {{ tool.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -37,6 +46,15 @@
|
||||
<header class="top-bar">
|
||||
<button class="menu-btn" onclick="openSidebar()"><i class="bi bi-list"></i></button>
|
||||
<span class="top-title">{% block top_title %}Your Everyday Tools{% endblock %}</span>
|
||||
<div class="global-search" id="global-search">
|
||||
<input type="text" id="global-search-input" placeholder="Search tools..." autocomplete="off" aria-label="Search tools" aria-expanded="false" aria-controls="global-search-dropdown">
|
||||
<div class="global-search-dropdown" id="global-search-dropdown" role="listbox"></div>
|
||||
</div>
|
||||
<div class="theme-switcher" id="theme-switcher" role="group" aria-label="Color theme">
|
||||
<button class="theme-btn" data-theme-mode="system" title="System theme"><i class="bi bi-circle-half"></i></button>
|
||||
<button class="theme-btn" data-theme-mode="light" title="Light theme"><i class="bi bi-sun-fill"></i></button>
|
||||
<button class="theme-btn" data-theme-mode="dark" title="Dark theme"><i class="bi bi-moon-fill"></i></button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content-area">
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<div class="home-hero">
|
||||
<h1>Your Everyday Tools</h1>
|
||||
<p>All the utilities you need, in one place.</p>
|
||||
<div class="home-search">
|
||||
<input type="text" id="tool-search" placeholder="Search tools..." autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for cat in tool_categories %}
|
||||
@@ -14,7 +17,7 @@
|
||||
</div>
|
||||
<div class="tools-grid">
|
||||
{% for tool in cat.tools %}
|
||||
<a href="/{{ cat.id }}/{{ tool.id }}" class="tool-card">
|
||||
<a href="/{{ cat.id }}/{{ tool.id }}" class="tool-card" data-search="{{ (tool.name ~ ' ' ~ tool.desc) | lower }}">
|
||||
<div class="card-icon"><i class="bi {{ tool.icon }}"></i></div>
|
||||
<div class="card-body">
|
||||
<h3>{{ tool.name }}</h3>
|
||||
@@ -25,4 +28,5 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p id="search-empty" class="search-empty" style="display:none;">No tools match your search.</p>
|
||||
{% endblock %}
|
||||
|
||||
@@ -67,7 +67,7 @@ function showStrength(pw, poolSize) {
|
||||
let label, color, pct;
|
||||
if (entropy < 40) { label = "Weak"; color = "var(--danger)"; pct = 25; }
|
||||
else if (entropy < 60) { label = "Fair"; color = "var(--warning)"; pct = 50; }
|
||||
else if (entropy < 80) { label = "Strong"; color = "#2ec4b6"; pct = 75; }
|
||||
else if (entropy < 80) { label = "Strong"; color = "var(--success)"; pct = 75; }
|
||||
else { label = "Very Strong"; color = "var(--primary)"; pct = 100; }
|
||||
|
||||
document.getElementById("pw-strength").innerHTML =
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
.match-idx { color: var(--text-light); min-width: 2rem; }
|
||||
.match-val { font-family: Consolas,Monaco,monospace; }
|
||||
.match-pos { color: var(--text-light); font-size: .8rem; }
|
||||
[data-theme="dark"] .regex-hl { background: rgba(244,162,97,.22); }
|
||||
[data-theme="dark"] .regex-hl:nth-of-type(even) { background: rgba(23,162,184,.22); border-bottom-color: #17a2b8; }
|
||||
</style>
|
||||
<script>
|
||||
function testRegex() {
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
.diff-stats { font-size: .85rem; margin-bottom: .5rem; }
|
||||
.diff-stats .add { color: #28a745; font-weight: 600; }
|
||||
.diff-stats .del { color: var(--danger); font-weight: 600; }
|
||||
[data-theme="dark"] .diff-add { background: #0d2e14; }
|
||||
[data-theme="dark"] .diff-del { background: #2e0d0d; }
|
||||
[data-theme="dark"] .diff-stats .add { color: #4ecdc4; }
|
||||
</style>
|
||||
<script>
|
||||
function computeDiff() {
|
||||
|
||||