From 0b6a4fb0e0cc145c00082e2fdb7cf0c4f9f3f481 Mon Sep 17 00:00:00 2001 From: Ricky Dane Perlick Date: Sat, 16 May 2026 14:11:13 +0200 Subject: [PATCH 01/46] saving --- .../specs/collapsible-sidebar-sections.md | 367 +++++++++++++ docs/design/specs/viewmode-selector.md | 389 ++++++++++++++ .../popup-glassmorphism-overhaul/decisions.md | 35 ++ .../popup-glassmorphism-overhaul/handoffs.md | 69 +++ .../implementation-tracker.md | 41 ++ .../popup-glassmorphism-overhaul/plan.md | 406 ++++++++++++++ docs/review/collapsible-sidebar/findings.md | 279 ++++++++++ docs/review/collapsible-sidebar/summary.md | 41 ++ .../context-menu-visual-overhaul/findings.md | 309 +++++++++++ .../context-menu-visual-overhaul/summary.md | 50 ++ src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 4 +- ui/contextmenu.js | 384 +++++++------ ui/index.html | 23 +- ui/main_logic.js | 185 +++++-- ui/style.css | 507 ++++++++++++++++-- 16 files changed, 2803 insertions(+), 288 deletions(-) create mode 100644 docs/design/specs/collapsible-sidebar-sections.md create mode 100644 docs/design/specs/viewmode-selector.md create mode 100644 docs/planning/popup-glassmorphism-overhaul/decisions.md create mode 100644 docs/planning/popup-glassmorphism-overhaul/handoffs.md create mode 100644 docs/planning/popup-glassmorphism-overhaul/implementation-tracker.md create mode 100644 docs/planning/popup-glassmorphism-overhaul/plan.md create mode 100644 docs/review/collapsible-sidebar/findings.md create mode 100644 docs/review/collapsible-sidebar/summary.md create mode 100644 docs/review/context-menu-visual-overhaul/findings.md create mode 100644 docs/review/context-menu-visual-overhaul/summary.md diff --git a/docs/design/specs/collapsible-sidebar-sections.md b/docs/design/specs/collapsible-sidebar-sections.md new file mode 100644 index 00000000..dd2ddee4 --- /dev/null +++ b/docs/design/specs/collapsible-sidebar-sections.md @@ -0,0 +1,367 @@ +# Design Spec: Collapsible Sidebar Sections + +## Overview + +Make the FAVORITES and Disks sections in the sidebar collapsible/expandable with smooth animation. Both sections default to expanded. State persists via localStorage. + +--- + +## 1. HTML Structure Pattern + +### Current Structure (flat) +``` +.site-nav-bar + ├── button.site-nav-bar-button (Desktop) + ├── button.site-nav-bar-button (Downloads) + ├── ... + ├── div.horizontal-seperator ← FAVORITES separator + ├── p.site-nav-bar-title ← "FAVORITES" + ├── button.site-nav-bar-button (fav item 1) + ├── button.site-nav-bar-button (fav item 2) + ├── div.horizontal-seperator ← Disks separator + ├── button.site-nav-bar-button ← "Disks" button + ├── div.horizontal-seperator + └── div.disk-container ← disk items +``` + +### New Structure (wrapped sections) +``` +.site-nav-bar + ├── button.site-nav-bar-button (Desktop) + ├── button.site-nav-bar-button (Downloads) + ├── ... + │ + ├── div.collapse-section[data-section="favorites"] + │ ├── button.collapse-header ← clickable header + │ │ ├── div.collapse-header-left + │ │ │ ├── i.fa-solid.fa-star (10px, #ffca28) + │ │ │ └── span "FAVORITES" + │ │ └── i.fa-solid.fa-chevron-down.collapse-chevron + │ └── div.collapse-content + │ ├── button.site-nav-bar-button (fav item 1) + │ ├── button.site-nav-bar-button (fav item 2) + │ └── ... + │ + └── div.collapse-section[data-section="disks"] + ├── button.collapse-header ← clickable header + │ ├── div.collapse-header-left + │ │ ├── i.fa-solid.fa-hard-drive + │ │ └── span "Disks" + │ └── i.fa-solid.fa-chevron-down.collapse-chevron + └── div.collapse-content + ├── div.disk-container + │ ├── button.site-nav-bar-button.disk-site-nav-button + │ └── ... + └── (no extra separator needed) +``` + +--- + +## 2. CSS Classes and Styles + +```css +/* ============================================= + Collapsible Sidebar Sections + ============================================= */ + +.collapse-section { + width: 100%; + display: flex; + flex-flow: column; + align-items: center; +} + +/* Header — acts as separator + title + toggle */ +.collapse-header { + width: 100%; + height: 32px; + min-height: 32px; + display: flex; + flex-flow: row; + justify-content: space-between; + align-items: center; + padding: 0 10px; + background-color: transparent; + border: none; + border-radius: 8px; + cursor: pointer; + color: var(--textColor2); + font-size: 10px; + font-weight: bold !important; + letter-spacing: 1px; + transition: background-color 0.15s ease, color 0.15s ease; + flex-shrink: 0; +} + +.collapse-header:hover { + background-color: var(--selectColor3); + color: var(--textColor); +} + +.collapse-header:focus-visible { + outline: 2px solid var(--selectColor2); + outline-offset: 2px; + background-color: var(--selectColor3); +} + +.collapse-header-left { + display: flex; + flex-flow: row; + align-items: center; + gap: 6px; + pointer-events: none; +} + +.collapse-header-left > i { + font-size: 10px; + color: var(--textColor2); +} + +/* Chevron rotation */ +.collapse-chevron { + font-size: 9px; + color: var(--textColor2); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; +} + +/* Collapsed state — chevron rotates up */ +.collapse-section.collapsed .collapse-chevron { + transform: rotate(-90deg); +} + +/* Content wrapper — animates max-height */ +.collapse-content { + width: 100%; + display: flex; + flex-flow: column; + align-items: center; + overflow: hidden; + max-height: 1000px; /* expanded default — JS sets real value */ + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.2s ease; + opacity: 1; +} + +/* Collapsed content */ +.collapse-section.collapsed .collapse-content { + max-height: 0 !important; + opacity: 0; +} + +/* Separator line inside collapse section (above header) */ +.collapse-section + .collapse-section, +.collapse-section:first-of-type { + margin-top: 4px; +} + +/* Remove top margin from first collapse section if quick-access buttons precede it */ +.collapse-section:first-of-type { + margin-top: 0; +} + +/* Horizontal separator rendered by collapse-header acts as visual divider */ +/* Add a subtle top border to the header for section separation */ +.collapse-section { + border-top: 1px solid var(--tertiaryColor); + padding-top: 4px; +} +``` + +--- + +## 3. JS Logic Approach + +### Toggle Function + +```js +function toggleCollapseSection(sectionEl) { + const sectionKey = sectionEl.dataset.section; + const content = sectionEl.querySelector(".collapse-content"); + const isCollapsed = sectionEl.classList.contains("collapsed"); + + if (isCollapsed) { + // EXPAND + sectionEl.classList.remove("collapsed"); + // Set max-height to actual scroll height for smooth animation + content.style.maxHeight = content.scrollHeight + "px"; + // After transition ends, set to none so content can grow dynamically + content.addEventListener("transitionend", function handler() { + if (!sectionEl.classList.contains("collapsed")) { + content.style.maxHeight = "none"; + } + content.removeEventListener("transitionend", handler); + }); + } else { + // COLLAPSE + // First, set explicit max-height from current height (needed for animation) + content.style.maxHeight = content.scrollHeight + "px"; + // Force reflow so browser registers the value + content.offsetHeight; // eslint-disable-line no-unused-expressions + // Then animate to 0 + sectionEl.classList.add("collapsed"); + } + + // Persist state + localStorage.setItem("sidebar-section-" + sectionKey, isCollapsed ? "expanded" : "collapsed"); +} +``` + +### Restore State on Init + +```js +function restoreCollapseState(sectionEl) { + const sectionKey = sectionEl.dataset.section; + const saved = localStorage.getItem("sidebar-section-" + sectionKey); + + if (saved === "collapsed") { + const content = sectionEl.querySelector(".collapse-content"); + sectionEl.classList.add("collapsed"); + content.style.maxHeight = "0"; + } + // Default: expanded (no action needed) +} +``` + +### Building Sections in `insertSiteNavButtons()` + +Replace the current flat favorites/disks code with wrapped sections. Key insertion pattern: + +```js +// === FAVORITES SECTION === +if (ArrFavorites.length > 0) { + const favSection = document.createElement("div"); + favSection.className = "collapse-section"; + favSection.dataset.section = "favorites"; + + const favHeader = document.createElement("button"); + favHeader.className = "collapse-header"; + favHeader.innerHTML = ` +
+ + FAVORITES +
+ + `; + favHeader.setAttribute("aria-expanded", "true"); + favHeader.setAttribute("aria-controls", "favorites-content"); + favHeader.onclick = () => toggleCollapseSection(favSection); + + const favContent = document.createElement("div"); + favContent.className = "collapse-content"; + favContent.id = "favorites-content"; + favContent.setAttribute("role", "region"); + + // ... append favorite buttons to favContent ... + + favSection.append(favHeader); + favSection.append(favContent); + document.querySelector(".site-nav-bar").append(favSection); + + // Restore persisted state + restoreCollapseState(favSection); +} +``` + +Same pattern for Disks section. + +--- + +## 4. Specific Design Values + +| Property | Value | Rationale | +|---|---|---| +| Header height | `32px` | Slightly shorter than nav buttons (35px) to differentiate | +| Header border-radius | `8px` | Subtle, matches sidebar aesthetic | +| Header font | `10px bold, letter-spacing 1px` | Matches existing `.site-nav-bar-title` exactly | +| Header color | `var(--textColor2)` default, `var(--textColor)` on hover | Consistent with existing secondary text pattern | +| Header padding | `0 10px` | Matches nav button horizontal padding | +| Chevron icon | `fa-solid fa-chevron-down`, `9px` | Small, unobtrusive | +| Chevron rotation | `-90deg` when collapsed | Points right when collapsed, down when expanded | +| Transition timing | `0.3s cubic-bezier(0.4, 0, 0.2, 1)` for max-height | Material standard easing — snappy feel | +| Opacity transition | `0.2s ease` | Slightly faster than height for layered effect | +| Hover background | `var(--selectColor3)` | Same as nav button hover | +| Section separator | `1px solid var(--tertiaryColor)` top border on `.collapse-section` | Replaces standalone `.horizontal-separator` divs | +| Star icon | `10px, #ffca28` | Same as existing favorites star | +| Hard drive icon | `10px, var(--textColor2)` | Matches existing disk icon style | +| localStorage keys | `sidebar-section-favorites`, `sidebar-section-disks` | Namespaced, descriptive | + +--- + +## 5. Accessibility Considerations + +### ARIA Attributes +- `aria-expanded="true|false"` on each `.collapse-header` button +- `aria-controls="section-content-id"` pointing to the content container +- `role="region"` on `.collapse-content` with `aria-label` + +### Keyboard Navigation +- Section headers are ` + + + +``` + +--- + +## CSS Classes + +All inline styles move to these classes. Add to `style.css`. + +```css +/* ============================================= + ViewMode Segmented Control + ============================================= */ + +.view-mode-group { + display: flex; + align-items: center; + background-color: var(--secondaryColor); + border: 1px solid var(--tertiaryColor); + border-radius: 8px; + padding: 3px; + height: 32px; + gap: 1px; +} + +.view-mode-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 26px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--textColor2); + cursor: pointer; + transition: background-color 0.1s ease-out, color 0.1s ease-out; + padding: 0; + font-size: 11px; +} + +.view-mode-btn:hover { + background-color: var(--transparentColor); + color: var(--textColor); +} + +.view-mode-btn:active { + background-color: var(--transparentColorActive); +} + +.view-mode-btn.active { + background-color: var(--transparentColorActive); + color: var(--textColor); +} + +.view-mode-btn:focus-visible { + outline: 2px solid var(--selectColor2); + outline-offset: -1px; +} + +.view-mode-btn:disabled { + cursor: not-allowed; + opacity: 0.4; +} + +.view-mode-btn:disabled:hover { + background: transparent; + color: var(--textColor2); +} + +/* Disabled state for the container (dual-pane mode) */ +.view-mode-group.disabled { + opacity: 0.5; + pointer-events: none; +} +``` + +--- + +## Interaction States + +### Default +- Container: `--secondaryColor` bg, `--tertiaryColor` border +- Active button: `--transparentColorActive` bg, `--textColor` icon (white) +- Inactive buttons: transparent bg, `--textColor2` icon (60% white) + +### Hover (on inactive button) +- Background fades to `--transparentColor` (10% black) +- Icon brightens to `--textColor` (full white) +- 100ms ease-out transition + +### Active / Selected +- Background: `--transparentColorActive` (25% black) +- Icon: `--textColor` (full white) +- Stays visually "pressed in" + +### Click (on already-active button) +- No-op — already selected, no visual change + +### Disabled (dual-pane mode) +- Entire container opacity 0.5 +- `pointer-events: none` on container (blocks all interaction) +- No hover effects + +### Focus (keyboard navigation) +- 2px solid outline in `--selectColor2` (blue accent) +- Outline offset -1px (inside the button) +- Only visible on `:focus-visible` (keyboard only, not mouse click) + +--- + +## JS Integration + +### Changes to `switchView()` in `main_logic.js` + +Replace the select/icon-span update logic with button active-state toggling: + +```js +async function switchView(newMode = null) { + if (IsDualPaneEnabled == false) { + if (newMode) { + ViewMode = newMode; + } else { + if (ViewMode == "wrap") ViewMode = "column"; + else if (ViewMode == "column") ViewMode = "miller"; + else ViewMode = "wrap"; + } + + // Update segmented control active state + document.querySelectorAll(".view-mode-btn").forEach((btn) => { + const isActive = btn.dataset.view === ViewMode; + btn.classList.toggle("active", isActive); + btn.setAttribute("aria-pressed", isActive ? "true" : "false"); + }); + + // ... rest of existing view-switching logic unchanged ... + } +} +``` + +### Changes to `switchToDualPane()` / dual-pane disable + +```js +// Disable +document.querySelector(".view-mode-group").classList.add("disabled"); +// Re-enable +document.querySelector(".view-mode-group").classList.remove("disabled"); +``` + +Remove these lines: +```js +// DELETE: document.querySelector(".view-mode-select").disabled = true; +// DELETE: document.querySelector(".view-mode-container").style.opacity = "0.5"; +// DELETE: document.querySelector(".view-mode-select").disabled = false; +// DELETE: document.querySelector(".view-mode-container").style.opacity = "1"; +``` + +### Selectors to update in `main_logic.js` + +| Old Selector | New Selector | Notes | +|---|---|---| +| `.view-mode-container` | `.view-mode-group` | Container class rename | +| `.view-mode-select` | `.view-mode-btn` | No more select element | +| `.view-mode-icon-span` | *(removed)* | No longer needed — icons live in buttons | + +### Cleanup + +Remove from JS: +- All `.view-mode-icon-span` innerHTML assignments (lines ~3492-3499) +- All `.view-mode-select` value/property access (lines ~3489-3491) +- Inline style opacity toggling on `.view-mode-container` + +--- + +## Accessibility + +### ARIA Attributes + +| Attribute | Element | Value | +|---|---|---| +| `role="group"` | `.view-mode-group` | Identifies as a button group | +| `aria-label` | `.view-mode-group` | `"View mode"` | +| `aria-pressed` | `.view-mode-btn` | `"true"` on active, `"false"` on others | +| `title` | `.view-mode-btn` | `"Grid view"`, `"List view"`, `"Miller columns view"` | + +### Keyboard Navigation + +| Key | Behavior | +|---|---| +| `Tab` | Focus enters the group on the active button | +| `Arrow Left` | Move focus to previous button in group | +| `Arrow Right` | Move focus to next button in group | +| `Enter` / `Space` | Activate the focused button | +| `Home` | Focus first button (Grid) | +| `End` | Focus last button (Miller) | + +Arrow key navigation within the group requires a small JS handler (roving tabindex pattern): + +```js +document.querySelector(".view-mode-group").addEventListener("keydown", (e) => { + const buttons = [...document.querySelectorAll(".view-mode-btn")]; + const current = document.activeElement; + const index = buttons.indexOf(current); + if (index === -1) return; + + let next; + if (e.key === "ArrowRight") next = buttons[(index + 1) % buttons.length]; + else if (e.key === "ArrowLeft") next = buttons[(index - 1 + buttons.length) % buttons.length]; + else if (e.key === "Home") next = buttons[0]; + else if (e.key === "End") next = buttons[buttons.length - 1]; + else return; + + e.preventDefault(); + next.focus(); +}); +``` + +Roving tabindex: only the active button has `tabindex="0"`, others get `tabindex="-1"`. Updated when active state changes. + +### Screen Reader + +- Button group announced as "View mode, group" +- Each button announced as "Grid view, toggle button, pressed" / "not pressed" +- Disabled state announced as "dimmed" or "unavailable" + +--- + +## Implementation Checklist + +- [ ] Add `.view-mode-group` and `.view-mode-btn` CSS to `style.css` +- [ ] Replace HTML in `index.html` (lines 79-88) +- [ ] Update `switchView()` in `main_logic.js` — button active-state logic +- [ ] Update `switchToDualPane()` — use `.disabled` class instead of inline opacity +- [ ] Update `switchFromDualPane()` (if exists) — remove `.disabled` class +- [ ] Add roving tabindex keyboard handler +- [ ] Remove dead selectors (`.view-mode-select`, `.view-mode-icon-span`, `.view-mode-container`) +- [ ] Test: click each mode, verify view switches +- [ ] Test: keyboard navigation (Tab, arrows, Enter) +- [ ] Test: dual-pane disable/enable +- [ ] Test: screen reader output +- [ ] Test: focus-visible outline appears on keyboard nav only + +--- + +## Visual Reference + +``` + HEADER BAR +┌─────────────────────────────────────────────────────────────────────┐ +│ ← | 🏠 👁 ⬜ ⚙ [🔍 Search...] [⋮⋮ ≡ \|\|] ─ □ ✕ │ +│ ↑ │ +│ view-mode-group │ +└─────────────────────────────────────────────────────────────────────┘ + + State: Grid selected State: List selected State: Miller selected +┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ +│ ▓▓▓ │ ░░ │ ░░ │ │ │ ░░ │ ▓▓▓ │ ░░ │ │ │ ░░ │ ░░ │ ▓▓▓ │ │ +└───────────────────┘ └───────────────────┘ └───────────────────┘ + ▲active ▲active ▲active + white icon white icon white icon + dark bg dark bg dark bg + + Disabled (dual-pane): + ┌───────────────────┐ + │ ░░ │ ░░ │ ░░ │ │ opacity: 0.5, no interaction + └───────────────────┘ +``` diff --git a/docs/planning/popup-glassmorphism-overhaul/decisions.md b/docs/planning/popup-glassmorphism-overhaul/decisions.md new file mode 100644 index 00000000..683b99e3 --- /dev/null +++ b/docs/planning/popup-glassmorphism-overhaul/decisions.md @@ -0,0 +1,35 @@ +# Decisions: Popup Glassmorphism Overhaul + +## Decision Log + +### D1: CSS Variables vs Inline Glass Values +**Decision:** Add reusable CSS variables for glass properties (`--glass-bg`, `--glass-border`, `--glass-blur`, `--glass-shadow`, `--glass-radius`) in `:root`. +**Rationale:** The context menu already hardcodes these values. Extracting them into variables ensures consistency and makes future tweaks (e.g., adjusting blur amount) a single-line change. All popup types reference the same tokens. + +### D2: Scope — CSS-Only, No JS/HTML Changes +**Decision:** All changes are in `ui/style.css` only. No HTML structure or JS logic changes. +**Rationale:** The existing HTML class structure (`uni-popup` + specific class, `popup-header`, `popup-body`, `popup-controls`) is consistent and sufficient. Adding glassmorphism is purely visual. This minimizes regression risk. + +### D3: Fix Existing Typo +**Decision:** Fix `var(--tr ansparentColorActive)` typo at line 2550 in `.popup-close-button:active`. +**Rationale:** This is a pre-existing bug (space in variable name). Fixing it during this overhaul is natural since we're restyling that rule anyway. + +### D4: `backdrop-filter` on Sub-Elements +**Decision:** Apply `backdrop-filter` to `.popup-header`, `.popup-controls`, and `.settings-sidebar` as well, not just the outer popup container. +**Rationale:** These sub-areas have their own background colors. Adding subtle blur creates a layered frosted-glass depth effect. The blur values are lighter (8–12px) than the main popup (24px) to avoid performance issues with nested blurs. + +### D5: Button/Input Overrides Scoped to Popups +**Decision:** Use `.uni-popup .button`, `.uni-popup .text-input` selectors (scoped) rather than changing global `.button`/`.text-input` styles. +**Rationale:** Global button/input styles are used throughout the app (sidebar, toolbar, etc.). Changing them globally would cause unintended side effects. Scoped overrides ensure only elements inside popups get the glass treatment. + +### D6: `color-mix()` for Semi-Transparency +**Decision:** Use `color-mix(in srgb, var(--primaryColor) 85%, transparent)` pattern consistently. +**Rationale:** This is the exact pattern used by the context menu. It's cleaner than manually calculating RGBA values and adapts automatically if `--primaryColor` changes via themes. + +### D7: `:root` Variable Naming Convention +**Decision:** Use `--glass-*` prefix for all new glass token variables. +**Rationale:** Clear separation from existing `--primaryColor`/`--tertiaryColor` tokens. Makes it obvious which variables are for the glass system vs. base theme colors. + +### D8: `.popup-background` Darker Overlay +**Decision:** Increase `.popup-background` from `var(--transparentColor)` (rgba(0,0,0,0.1)) to `rgba(0,0,0,0.4)` and increase blur from 2px to 8px. +**Rationale:** The current background is too subtle — popups don't feel "above" the content. A stronger dimming effect + more blur creates clear visual separation and makes the glassmorphism popups stand out. diff --git a/docs/planning/popup-glassmorphism-overhaul/handoffs.md b/docs/planning/popup-glassmorphism-overhaul/handoffs.md new file mode 100644 index 00000000..14f6b481 --- /dev/null +++ b/docs/planning/popup-glassmorphism-overhaul/handoffs.md @@ -0,0 +1,69 @@ +# Handoffs: Popup Glassmorphism Overhaul + +## software-engineer + +### Context +All popup/modal CSS lives in `ui/style.css`. The context menu (`.context-menu` at line 2075 and `.custom-context-menu` at line 3018) already has the target glassmorphism aesthetic. Goal: make every other popup match. + +### Implementation Steps +1. Add glass design token variables to `:root` (Phase 1) +2. Update base classes: `.popup-background`, `.uni-popup`, `.popup-header`, `.popup-body`, `.popup-controls`, `.popup-close-button` (Phase 2) +3. Update input/dialog popups: `.input-dialog`, `.input-popup`, `.loading-popup` (Phase 3) +4. Update content-specific popups: preview, properties, rename, duplicates, yt-download, llm, confirm, conflict, search (Phase 4) +5. Update settings panel: `.settings-ui` and children (Phase 5) +6. Update progress bar and supporting overlays (Phase 6) +7. Add scoped button/input/select overrides inside popups (Phase 7) +8. Final polish: reduced-motion, scrollbars (Phase 8) + +### Files Modified +- `ui/style.css` — only file changed + +### Acceptance Criteria +- Every popup type uses glassmorphism (semi-transparent bg, blur, subtle border, layered shadows) +- Text readable on all popup surfaces (white on dark glass) +- Hover/active/focus states work on all interactive elements inside popups +- No visual regressions (z-index, responsive, scrollbars) +- `prefers-reduced-motion` respected + +### Key Reference Lines +- `.context-menu` glassmorphism source: lines 2075–2095 +- `.custom-context-menu`: lines 3018–3031 +- `:root` variables: lines 31–52 +- `.uni-popup` base: lines 2391–2404 +- `.popup-background`: lines 3003–3016 +- `.settings-ui`: lines 685–707 +- `.progress-bar-container-popup`: lines 2790–2810 + +--- + +## code-reviewer + +### Review Focus Areas +1. Verify all 21 popup types have glassmorphism applied +2. Check text contrast/readability on glass backgrounds +3. Check for CSS specificity conflicts (existing rules overriding new glass styles) +4. Verify no broken layouts (missing `overflow`, wrong `border-radius`, broken flex) +5. Verify `prefers-reduced-motion` fallback exists +6. Check that `.popup-close-button` has a typo fix: line 2550 has `var(--tr ansparentColorActive)` — note the space in the variable name +7. Verify z-index layering still correct (background < content < popups) + +### Risks to Inspect +- `backdrop-filter` performance on many overlapping blurred elements +- Text readability if blur amount is too high or bg opacity too low +- Settings panel has complex nested layout — verify glass doesn't break scrolling + +### Expected Output +- `docs/review/popup-glassmorphism-overhaul/findings.md` +- `docs/review/popup-glassmorphism-overhaul/summary.md` + +--- + +## documentation-writer + +### User-Facing Changes +- Visual refresh: all popups, dialogs, settings, and overlays now have a modern glassmorphism look matching the context menu +- No functional changes +- No migration steps + +### Expected Output +- Brief release notes documenting the visual refresh diff --git a/docs/planning/popup-glassmorphism-overhaul/implementation-tracker.md b/docs/planning/popup-glassmorphism-overhaul/implementation-tracker.md new file mode 100644 index 00000000..9183a1e1 --- /dev/null +++ b/docs/planning/popup-glassmorphism-overhaul/implementation-tracker.md @@ -0,0 +1,41 @@ +# Implementation Tracker: Popup Glassmorphism Overhaul + +## Status Summary +**Overall Status:** Planned +**Current Phase:** Not started +**Last Updated:** 2026-05-16 + +## Task Table + +| ID | Task | Owner | Status | Depends On | Evidence | Next Step | +|----|------|-------|--------|------------|----------|-----------| +| T1 | Add glass design token variables to `:root` | software-engineer | Planned | None | — | Implement Phase 1 | +| T2 | Restyle `.popup-background` | software-engineer | Planned | T1 | — | Implement Phase 2 | +| T3 | Restyle `.uni-popup` base | software-engineer | Planned | T1 | — | Implement Phase 2 | +| T4 | Restyle `.popup-header` | software-engineer | Planned | T1 | — | Implement Phase 2 | +| T5 | Restyle `.popup-body` | software-engineer | Planned | T1 | — | Implement Phase 2 | +| T6 | Restyle `.popup-controls` | software-engineer | Planned | T1 | — | Implement Phase 2 | +| T7 | Restyle `.popup-close-button` | software-engineer | Planned | T1 | — | Implement Phase 2 | +| T8 | Restyle `.input-dialog` / `.input-popup` | software-engineer | Planned | T3 | — | Implement Phase 3 | +| T9 | Restyle `.loading-popup` | software-engineer | Planned | T3 | — | Implement Phase 3 | +| T10 | Restyle `.item-preview-popup` | software-engineer | Planned | T3 | — | Implement Phase 4 | +| T11 | Restyle `.item-properties-popup` | software-engineer | Planned | T3 | — | Implement Phase 4 | +| T12 | Restyle `.multi-rename-popup` | software-engineer | Planned | T3 | — | Implement Phase 4 | +| T13 | Restyle `.find-duplicates-popup` | software-engineer | Planned | T3 | — | Implement Phase 4 | +| T14 | Restyle `.yt-download-popup` | software-engineer | Planned | T3 | — | Implement Phase 4 | +| T15 | Restyle `.llm-prompt-input-popup` | software-engineer | Planned | T3 | — | Implement Phase 4 | +| T16 | Restyle `.confirm-popup` | software-engineer | Planned | T3 | — | Implement Phase 4 | +| T17 | Restyle `.destination-conflict-popup` + cards | software-engineer | Planned | T3 | — | Implement Phase 4 | +| T18 | Restyle `.search-full-container` header | software-engineer | Planned | T3 | — | Implement Phase 4 | +| T19 | Restyle `.settings-ui` + header/sidebar/content | software-engineer | Planned | T1 | — | Implement Phase 5 | +| T20 | Restyle `.progress-bar-container-popup` | software-engineer | Planned | T1 | — | Implement Phase 6 | +| T21 | Restyle `.search-bar-container` | software-engineer | Planned | T1 | — | Implement Phase 6 | +| T22 | Restyle buttons/inputs/selects inside popups | software-engineer | Planned | T3 | — | Implement Phase 7 | +| T23 | Final polish: animations, scrollbars, reduced-motion | software-engineer | Planned | T22 | — | Implement Phase 8 | +| T24 | Visual QA: test all popups | code-reviewer | Planned | T23 | — | Review all popups | + +## Blockers +None + +## Drift Log +No drift yet. diff --git a/docs/planning/popup-glassmorphism-overhaul/plan.md b/docs/planning/popup-glassmorphism-overhaul/plan.md new file mode 100644 index 00000000..f0fd58a9 --- /dev/null +++ b/docs/planning/popup-glassmorphism-overhaul/plan.md @@ -0,0 +1,406 @@ +# Plan: Popup Glassmorphism Visual Overhaul + +## Summary +Restyle every popup/modal/dialog in CoDriver to match the modern glassmorphism aesthetic already established by the context menu (`.context-menu` / `.custom-context-menu`). All changes are CSS-only in `ui/style.css` — no HTML structure or JS changes needed. + +## Goals +- Unified glassmorphism look across all 21 popup types +- Shared base styles to reduce duplication and ensure consistency +- All text remains readable (WCAG contrast on dark glass) +- Hover/active/focus states for interactive elements inside popups +- Preserve existing responsive behavior and functionality + +## Non-Goals +- Changing HTML structure or JS logic +- Adding new popup types +- Refactoring popup creation code +- Changing the color scheme or CSS variable values + +## Assumptions +- `backdrop-filter` is supported in Tauri's WebView (Chromium-based) — confirmed by existing usage +- CSS `color-mix()` is available — confirmed by existing context menu usage +- No CSS preprocessor — vanilla CSS only +- The `--primaryColor`, `--tertiaryColor`, `--textColor` variables remain unchanged + +## Open Questions +None — scope is clear. + +## Dependencies +None — single-file CSS change. + +## Execution Phases + +### Phase 1: Establish Glassmorphism Design Tokens +**Owner:** software-engineer +**Output:** CSS custom properties block at top of popup section +**Acceptance Criteria:** Reusable variables defined for glass background, glass border, glass shadow, glass blur. + +**New CSS variables to add under `:root`:** +```css +--glass-bg: color-mix(in srgb, var(--primaryColor) 85%, transparent); +--glass-border: 1px solid color-mix(in srgb, var(--tertiaryColor) 50%, transparent); +--glass-blur: blur(24px) saturate(1.4); +--glass-shadow: + 0 8px 32px rgba(0, 0, 0, 0.35), + 0 2px 8px rgba(0, 0, 0, 0.2), + inset 0 0.5px 0 rgba(255, 255, 255, 0.06); +--glass-radius: 12px; +--glass-header-bg: color-mix(in srgb, var(--secondaryColor) 80%, transparent); +--glass-border-subtle: 1px solid color-mix(in srgb, var(--tertiaryColor) 35%, transparent); +``` + +--- + +### Phase 2: Restyle Base Popup Classes +**Owner:** software-engineer +**Output:** Updated `.popup-background`, `.uni-popup`, `.popup-header`, `.popup-body`, `.popup-controls`, `.popup-close-button` +**Acceptance Criteria:** All base popup classes use glassmorphism; consistent with context menu aesthetic. + +**Specific changes:** + +#### `.popup-background` (line 3003) +```css +.popup-background { + /* existing: keep display/position/size */ + background-color: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(8px) saturate(1.2); + -webkit-backdrop-filter: blur(8px) saturate(1.2); + /* rest unchanged */ +} +``` + +#### `.uni-popup` (line 2391) +```css +.uni-popup { + /* keep existing positioning */ + background-color: var(--glass-bg); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border: var(--glass-border); + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow); + overflow: hidden; + z-index: 98 !important; +} +``` + +#### `.popup-header` (line 2553) +```css +.popup-header { + background-color: var(--glass-header-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: var(--glass-border-subtle); + /* keep existing flex/layout */ +} +``` + +#### `.popup-body` (line 2571) +```css +.popup-body { + padding: 10px; + border-top: none; /* remove hard line, glass handles it */ + color: var(--textColor); +} +``` + +#### `.popup-controls` (line 2577) +```css +.popup-controls { + /* keep existing layout */ + border-top: var(--glass-border-subtle); + background-color: color-mix(in srgb, var(--primaryColor) 60%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} +``` + +#### `.popup-close-button` (line 2532) +```css +.popup-close-button { + width: 32px; + height: 32px; + display: flex; + justify-content: center; + align-items: center; + background-color: color-mix(in srgb, var(--tertiaryColor) 60%, transparent); + border: none; + border-radius: 10px; + cursor: pointer; + font-size: var(--fontSize); + transition: background-color 0.15s ease; +} +.popup-close-button:hover { + background-color: var(--transparentColor); +} +.popup-close-button:active { + background-color: var(--transparentColorActive); +} +``` + +--- + +### Phase 3: Restyle Input/Dialog Popups +**Owner:** software-engineer +**Output:** Updated `.input-dialog`, `.input-popup`, `.loading-popup` +**Acceptance Criteria:** Glassmorphism applied; text inputs inside remain usable. + +#### `.input-dialog` (line 2298) +```css +.input-dialog { + /* keep positioning */ + background-color: var(--glass-bg); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border: var(--glass-border); + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow); + /* remove old box-shadow and border */ +} +``` + +#### `.input-popup` (line 2370) +Same glass treatment as `.input-dialog`. + +#### `.loading-popup` (line 2341) +```css +.loading-popup { + /* keep positioning */ + background-color: var(--glass-bg); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border: var(--glass-border); + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow); +} +``` + +--- + +### Phase 4: Restyle Content-Specific Popups +**Owner:** software-engineer +**Output:** Updated `.compression-popup`, `.item-preview-popup`, `.item-properties-popup`, `.multi-rename-popup`, `.find-duplicates-popup`, `.yt-download-popup`, `.llm-prompt-input-popup`, `.confirm-popup`, `.destination-conflict-popup` +**Acceptance Criteria:** Each popup inherits glass base + has any popup-specific overrides. + +These all extend `.uni-popup`, so most get the glass treatment from Phase 2 for free. Only popup-specific overrides need updating: + +#### `.item-preview-popup` (line 2429) +```css +.item-preview-popup { + /* keep positioning */ + background-color: var(--glass-bg); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border: var(--glass-border); + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow); + /* remove old background-color: var(--transparentColorActive) */ +} +``` + +#### `.destination-conflict-popup` (line 2884) +Cards inside need glass treatment: +```css +.destination-conflict-card { + background: color-mix(in srgb, var(--secondaryColor) 80%, transparent); + border: var(--glass-border-subtle); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} +.destination-conflict-options label { + background: color-mix(in srgb, var(--secondaryColor) 80%, transparent); + border: var(--glass-border-subtle); +} +``` + +#### `.search-full-container` (line 1857) +Already has `.uni-popup` class — inherits glass. Update header: +```css +.search-full-container-header { + background-color: var(--glass-header-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: var(--glass-border-subtle); +} +``` + +--- + +### Phase 5: Restyle Settings Panel +**Owner:** software-engineer +**Output:** Updated `.settings-ui`, `.settings-ui-header`, `.settings-sidebar`, `.settings-content` +**Acceptance Criteria:** Settings panel matches glassmorphism aesthetic. + +#### `.settings-ui` (line 685) +```css +.settings-ui { + /* keep positioning/sizing */ + background-color: var(--glass-bg); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border: var(--glass-border); + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow); +} +``` + +#### `.settings-ui-header` (line 716) +```css +.settings-ui-header { + background-color: var(--glass-header-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: var(--glass-border-subtle); +} +``` + +#### `.settings-sidebar` (line 730) +```css +.settings-sidebar { + background-color: color-mix(in srgb, var(--secondaryColor) 60%, transparent); + border-right: var(--glass-border-subtle); +} +``` + +#### `.settings-ui .popup-controls` (line 810) +```css +.settings-ui .popup-controls { + background-color: var(--glass-header-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-top: var(--glass-border-subtle); +} +``` + +--- + +### Phase 6: Restyle Progress Bar & Supporting Elements +**Owner:** software-engineer +**Output:** Updated `.progress-bar-container-popup`, `.search-bar-container`, `.ftp-connect-container` +**Acceptance Criteria:** All overlay elements match the glass aesthetic. + +#### `.progress-bar-container-popup` (line 2790) +```css +.progress-bar-container-popup { + /* keep existing layout/sizing */ + background-color: var(--glass-bg); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border: var(--glass-border); + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow); +} +``` + +#### `.ftp-connect-container` (line 991) +Already has `.uni-popup` class — inherits glass from Phase 2. + +#### `.search-bar-container` (line 1791) +```css +.search-bar-container { + background-color: var(--glass-bg); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + border: var(--glass-border); + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow); +} +``` + +--- + +### Phase 7: Buttons & Interactive Elements Inside Popups +**Owner:** software-engineer +**Output:** Updated button styles, input styles, select styles inside popups +**Acceptance Criteria:** All interactive elements inside popups have glass-consistent styling and clear hover/active/focus states. + +**Scoped overrides for popup children:** +```css +/* Buttons inside popups */ +.uni-popup .button, +.uni-popup .button-invert, +.settings-ui .button { + background-color: color-mix(in srgb, var(--tertiaryColor) 60%, transparent); + border: var(--glass-border-subtle); + backdrop-filter: blur(4px); + transition: background-color 0.15s ease, transform 0.1s ease; +} +.uni-popup .button:hover, +.uni-popup .button-invert:hover, +.settings-ui .button:hover { + background-color: var(--transparentColor); +} +.uni-popup .button:active, +.uni-popup .button-invert:active, +.settings-ui .button:active { + background-color: var(--transparentColorActive); + transform: scale(0.97); +} + +/* Text inputs inside popups */ +.uni-popup .text-input, +.uni-popup .number-input, +.settings-ui .text-input { + background-color: color-mix(in srgb, var(--secondaryColor) 70%, transparent); + border: var(--glass-border-subtle); + backdrop-filter: blur(4px); +} +.uni-popup .text-input:focus, +.uni-popup .number-input:focus, +.settings-ui .text-input:focus { + background-color: color-mix(in srgb, var(--tertiaryColor) 70%, transparent); + border-color: var(--selectColor2); +} + +/* Select dropdowns inside popups */ +.uni-popup .select, +.settings-ui .select { + background-color: color-mix(in srgb, var(--secondaryColor) 70%, transparent); + border: var(--glass-border-subtle); +} +``` + +--- + +### Phase 8: Final Polish & Edge Cases +**Owner:** software-engineer +**Output:** Animation adjustments, reduced-motion support, scrollbar styling inside popups +**Acceptance Criteria:** No visual regressions; reduced-motion respected; scrollbars styled. + +```css +/* Reduced motion fallback for popups */ +@media (prefers-reduced-motion: reduce) { + .uni-popup, + .settings-ui, + .popup-background { + transition: none !important; + animation: none !important; + } +} + +/* Scrollbar inside popups */ +.uni-popup ::-webkit-scrollbar { + width: 4px; + height: 4px; +} +.uni-popup ::-webkit-scrollbar-thumb { + background-color: color-mix(in srgb, var(--tertiaryColor) 60%, transparent); + border-radius: 4px; +} +``` + +--- + +## Validation Plan +1. Open each popup type in the running app +2. Verify glassmorphism appearance (blur visible, semi-transparent, correct border/shadow) +3. Verify text readability on all backgrounds +4. Test hover/active states on buttons, inputs, selects inside popups +5. Test keyboard focus outlines still visible +6. Test with `prefers-reduced-motion` enabled +7. Test responsive behavior (resize window, check settings panel, conflict popup) +8. Test dark theme (default) and any other themes +9. Verify no z-index regressions (popups above content, background below popups) + +## Rollback Plan +All changes are in a single file: `ui/style.css`. Git revert of the commit is the rollback path. No database, config, or JS changes to unwind. diff --git a/docs/review/collapsible-sidebar/findings.md b/docs/review/collapsible-sidebar/findings.md new file mode 100644 index 00000000..84818483 --- /dev/null +++ b/docs/review/collapsible-sidebar/findings.md @@ -0,0 +1,279 @@ +# Code Review Findings: Collapsible Sidebar Sections & Compact Favorites + +**Reviewer:** Code Reviewer Agent +**Date:** 2026-05-16 +**Files Reviewed:** `ui/style.css` (lines 1553–1677), `ui/main_logic.js` (lines 4555–4813) + +--- + +## [CS-001] XSS via unsanitized innerHTML in favorite button labels + +**Severity:** Critical +**Location:** `ui/main_logic.js:4708-4709` + +### Description +Favorite names are derived from filesystem paths and injected directly into innerHTML without sanitization. + +```js +let name = path.split(/[\\\/]/).pop() || path; +button.innerHTML = `

${name}

`; +``` + +A path like `/Users/test/` injects executable HTML. Same pattern exists for disk names at lines 4780 and 5062 (`mount.name`), though `addNewMount` partially mitigates with `.replaceAll('"', "")` — which strips quotes but not HTML tags. + +### Impact +In a Tauri desktop app, the blast radius is limited to the user's own filesystem (self-XSS). However, a crafted `.zip` with a malicious folder name extracted to the filesystem, then favorited, could execute arbitrary JS in the app context. This breaks the Tauri security model where the webview should never execute untrusted code. + +### Recommendation +Use `textContent` instead of `innerHTML` for the name portion: + +```js +const p = document.createElement("p"); +p.textContent = name; +// Or: escape HTML entities before interpolation +``` + +Apply the same fix to disk name injection at lines 4780, 5062. + +--- + +## [CS-002] `restoreCollapseState` sets inline `max-height: 0` — redundant but creates stale inline style + +**Severity:** Major +**Location:** `ui/main_logic.js:4584-4594` + +### Description +```js +function restoreCollapseState(sectionEl) { + // ... + sectionEl.classList.add("collapsed"); + content.style.maxHeight = "0"; // ← inline style + header.setAttribute("aria-expanded", "false"); +} +``` + +The CSS `!important` rule (`.collapse-section.collapsed .collapse-content { max-height: 0 !important }`) already enforces `max-height: 0` when the `.collapsed` class is present. The inline `max-height: "0"` is redundant in the collapsed state but **persists as stale state** when expanding. + +When `toggleCollapseSection` later expands: +1. Removes `.collapsed` → CSS `!important` no longer applies +2. Inline `max-height: "0"` **is still there** from `restoreCollapseState` +3. `content.style.maxHeight = content.scrollHeight + "px"` overwrites it + +Step 3 saves it, but if any code path removes `.collapsed` without going through `toggleCollapseSection`, the inline `0` would clip content. + +### Impact +Currently benign because step 3 always runs. But it's a latent bug — fragile coupling between `restoreCollapseState` and `toggleCollapseState`. + +### Recommendation +Remove the `content.style.maxHeight = "0"` line from `restoreCollapseState`. The CSS `!important` rule handles collapsed state. If the inline style is needed for the expand animation's starting point, set it at expand-time (which `toggleCollapseSection` already does via `content.scrollHeight`). + +--- + +## [CS-003] `role="region"` without `aria-label` or `aria-labelledby` + +**Severity:** Major +**Location:** `ui/main_logic.js:4701-4702`, `ui/main_logic.js:4766-4767` + +### Description +```js +favContent.setAttribute("role", "region"); +// No aria-label or aria-labelledby +``` + +Per WAI-ARIA spec, `role="region"` **must** have an accessible name. Without it, screen readers treat it as a generic group and may skip or misannounce it. The `aria-controls` on the header correctly points to the content ID, but the region itself is unlabeled. + +### Impact +Screen reader users cannot identify the purpose of the region landmarks. The `aria-expanded`/`aria-controls` pattern works for the button, but the region itself is invisible as a navigable landmark. + +### Recommendation +Add `aria-labelledby` pointing to the header's ID. Generate unique IDs for headers: + +```js +favHeader.id = "favorites-header"; +favContent.setAttribute("aria-labelledby", "favorites-header"); +``` + +--- + +## [CS-004] `max-height: 1000px` hardcoded limit clips content on initial load + +**Severity:** Major +**Location:** `ui/style.css:1625` + +### Description +```css +.collapse-content { + max-height: 1000px; /* ← hard limit */ + transition: max-height 0.3s ..., opacity 0.2s ease; + opacity: 1; +} +``` + +On initial page load (no localStorage state), sections are expanded via CSS with `max-height: 1000px`. The `transitionend` handler that sets `max-height: none` only fires after a user-initiated toggle. If content exceeds 1000px (many favorites + many disks), it clips silently. + +The JS correctly resolves this after the first toggle-to-expand (sets `max-height: none` on transitionend), but the initial render is CSS-only. + +### Impact +Low probability in practice (sidebar rarely exceeds 1000px), but the limit is arbitrary and will silently break for power users with many favorites or mounted volumes. + +### Recommendation +After `restoreCollapseState` runs (or after building the DOM), measure `scrollHeight` and set `content.style.maxHeight = content.scrollHeight + "px"`. Or add a one-time post-render adjustment: + +```js +// After building collapse sections, ensure expanded sections aren't clipped +sectionEl.querySelectorAll(".collapse-content").forEach(c => { + if (!sectionEl.classList.contains("collapsed")) { + c.style.maxHeight = "none"; + } +}); +``` + +--- + +## [CS-005] `addNewMount` bypasses collapse state awareness + +**Severity:** Minor +**Location:** `ui/main_logic.js:5086` + +### Description +```js +document.querySelector(".disk-container").append(diskButton); +``` + +`addNewMount` appends a new disk button to `.disk-container` (now nested inside `.collapse-content`) without considering: +1. Whether the disk section is collapsed (button added but invisible) +2. Whether the inline `max-height` needs updating to accommodate the new button +3. Whether `scrollHeight` changed and the transition state needs refreshing + +### Impact +- If collapsed: new drive appears invisible until user manually expands — **unexpected** for a hot-plug event +- If expanded with `max-height: none`: works fine +- If expanded with `max-height: 1000px` CSS default: new button may be clipped if near the limit + +### Recommendation +After appending, if the disk section is expanded, update `max-height`: + +```js +document.querySelector(".disk-container").append(diskButton); +const diskSection = document.querySelector('[data-section="disks"]'); +if (diskSection && !diskSection.classList.contains("collapsed")) { + const content = diskSection.querySelector(".collapse-content"); + content.style.maxHeight = content.scrollHeight + "px"; +} +``` + +Or auto-expand the disk section when a new mount is detected (UX decision for Navigator). + +--- + +## [CS-006] Dead cleanup selectors in `insertSiteNavButtons` + +**Severity:** Minor +**Location:** `ui/main_logic.js:4600-4601, 4603` + +### Description +```js +$(".site-nav-bar-title").remove(); // ← no longer created +$(".site-nav-bar > .horizontal-seperator").remove(); // ← no longer created +$(".site-nav-bar > .disk-container").remove(); // ← now nested inside .collapse-section +``` + +These selectors no longer match any elements created by the new code. Titles and separators are replaced by `.collapse-section` borders. The `.disk-container` is now a descendant of `.collapse-section`, not a direct child of `.site-nav-bar`. + +Line 4602 (`$(".collapse-section").remove()`) already handles cleanup of all nested content. + +### Impact +No functional impact — jQuery silently ignores non-matching selectors. But dead code misleads future maintainers into thinking these elements still exist. + +### Recommendation +Remove the three dead selector lines. Keep only: +```js +$(".site-nav-bar-button").remove(); +$(".site-nav-bar-button-fav").remove(); +$(".collapse-section").remove(); +``` + +--- + +## [CS-007] `transitionend` handler guard is correct but doesn't handle rapid clicks cleanly + +**Severity:** Minor +**Location:** `ui/main_logic.js:4565-4570` + +### Description +```js +content.addEventListener("transitionend", function handler() { + if (!sectionEl.classList.contains("collapsed")) { + content.style.maxHeight = "none"; + } + content.removeEventListener("transitionend", handler); +}); +``` + +If the user rapidly clicks expand → collapse → expand before transitions complete: +1. Expand: handler H1 added +2. Quick collapse: H1 fires on collapse's `transitionend`, guard fails (section is collapsed), H1 self-removes ✓ +3. Expand: handler H2 added, fires on expand's `transitionend`, sets `max-height: none` ✓ + +This is **correct** — the guard prevents stale handlers from causing damage. However, `transitionend` fires for each animated property. The CSS transitions both `max-height` and `opacity`, so `transitionend` fires **twice** per toggle. The handler removes itself on first fire, leaving the second `transitionend` unhandled (benign, but could confuse debugging). + +### Impact +No functional impact. The `max-height: none` is set on the first `transitionend` (for `max-height`), which is correct. The second `transitionend` (for `opacity`) is ignored. + +### Recommendation +Filter for the specific property in the handler: + +```js +content.addEventListener("transitionend", function handler(e) { + if (e.propertyName !== "max-height") return; + // ... existing logic +}); +``` + +--- + +## [CS-008] `restoreCollapseState` doesn't restore `aria-expanded` on initially expanded (default) state + +**Severity:** Minor +**Location:** `ui/main_logic.js:4695-4696`, `ui/main_logic.js:4760-4761` + +### Description +Headers are created with `aria-expanded="true"` (the default expanded state). `restoreCollapseState` only sets `aria-expanded="false"` when restoring a collapsed state. For expanded state, the initial `"true"` persists — which is correct. + +However, there's no code path that validates `aria-expanded` matches the actual visual state if the DOM is manipulated externally. This is a minor robustness concern. + +### Impact +None in normal usage. Aria state stays synchronized because all toggle operations go through `toggleCollapseSection`. + +### Recommendation +No change needed. Current approach is correct. + +--- + +## Positive Findings + +1. **Clean collapse animation pattern**: The `scrollHeight` → reflow → class toggle → `transitionend` → `max-height: none` pattern is the correct approach for animating dynamic-height content with CSS transitions. Well implemented. + +2. **Good keyboard accessibility**: Using ` -
- - - - +
+ + +
diff --git a/ui/main_logic.js b/ui/main_logic.js index caef0def..d7a40848 100644 --- a/ui/main_logic.js +++ b/ui/main_logic.js @@ -190,13 +190,13 @@ let CurrentTheme = "0"; /* Upper right search bar logic */ document.querySelector(".search-bar-input").addEventListener("focusin", (e) => { - $(".file-searchbar").css("width", "300px"); + $(".file-searchbar").css("width", "320px"); IsInputFocused = true; }); document .querySelector(".search-bar-input") .addEventListener("focusout", (e) => { - $(".file-searchbar").css("width", "200px"); + $(".file-searchbar").css("width", "220px"); IsInputFocused = false; }); document.querySelector(".search-bar-input").addEventListener("keyup", (e) => { @@ -3485,19 +3485,12 @@ async function switchView(newMode = null) { else ViewMode = "wrap"; } - // Update dropdown if it exists - const select = document.querySelector(".view-mode-select"); - const iconSpan = document.querySelector(".view-mode-icon-span"); - if (select) select.value = ViewMode; - if (iconSpan) { - if (ViewMode === "wrap") { - iconSpan.innerHTML = ''; - } else if (ViewMode === "column") { - iconSpan.innerHTML = ''; - } else if (ViewMode === "miller") { - iconSpan.innerHTML = ''; - } - } + // Update segmented control active state + document.querySelectorAll(".view-mode-btn").forEach((btn) => { + const isActive = btn.dataset.view === ViewMode; + btn.classList.toggle("active", isActive); + btn.setAttribute("aria-pressed", isActive ? "true" : "false"); + }); if (ViewMode == "column") { document.querySelectorAll(".directory-list").forEach((list) => { @@ -3557,13 +3550,30 @@ async function switchView(newMode = null) { } } +// Roving tabindex for view mode group +document.querySelector(".view-mode-group").addEventListener("keydown", (e) => { + const buttons = [...document.querySelectorAll(".view-mode-btn")]; + const current = document.activeElement; + const index = buttons.indexOf(current); + if (index === -1) return; + + let next; + if (e.key === "ArrowRight") next = buttons[(index + 1) % buttons.length]; + else if (e.key === "ArrowLeft") next = buttons[(index - 1 + buttons.length) % buttons.length]; + else if (e.key === "Home") next = buttons[0]; + else if (e.key === "End") next = buttons[buttons.length - 1]; + else return; + + e.preventDefault(); + next.focus(); +}); + async function switchToDualPane() { if (IsDualPaneEnabled == false) { OrgViewMode = ViewMode; IsDualPaneEnabled = true; ViewMode = "column"; - document.querySelector(".view-mode-select").disabled = true; - document.querySelector(".view-mode-container").style.opacity = "0.5"; + document.querySelector(".view-mode-group").classList.add("disabled"); document.querySelector(".miller-container").style.display = "none"; if (Platform == "darwin") { $(".header-nav").css("padding-left", "85px"); @@ -3609,8 +3619,7 @@ async function switchToDualPane() { }); } else { IsDualPaneEnabled = false; - document.querySelector(".view-mode-select").disabled = false; - document.querySelector(".view-mode-container").style.opacity = "1"; + document.querySelector(".view-mode-group").classList.remove("disabled"); $(".non-dual-pane-container")?.css("width", "calc(100vw - 150px)"); $(".non-dual-pane-container")?.css("opacity", "1"); $(".non-dual-pane-container")?.css("height", "100%"); @@ -4541,11 +4550,54 @@ async function getDir(number) { return dirPath; } +function toggleCollapseSection(sectionEl) { + const sectionKey = sectionEl.dataset.section; + const content = sectionEl.querySelector(".collapse-content"); + const header = sectionEl.querySelector(".collapse-header"); + const isCollapsed = sectionEl.classList.contains("collapsed"); + + if (isCollapsed) { + sectionEl.classList.remove("collapsed"); + content.style.maxHeight = content.scrollHeight + "px"; + header.setAttribute("aria-expanded", "true"); + content.addEventListener("transitionend", function handler() { + if (!sectionEl.classList.contains("collapsed")) { + content.style.maxHeight = "none"; + } + content.removeEventListener("transitionend", handler); + }); + } else { + content.style.maxHeight = content.scrollHeight + "px"; + content.offsetHeight; + sectionEl.classList.add("collapsed"); + header.setAttribute("aria-expanded", "false"); + } + + localStorage.setItem( + "sidebar-section-" + sectionKey, + isCollapsed ? "expanded" : "collapsed", + ); +} + +function restoreCollapseState(sectionEl) { + const sectionKey = sectionEl.dataset.section; + const saved = localStorage.getItem("sidebar-section-" + sectionKey); + if (saved === "collapsed") { + const content = sectionEl.querySelector(".collapse-content"); + const header = sectionEl.querySelector(".collapse-header"); + sectionEl.classList.add("collapsed"); + content.style.maxHeight = "0"; + header.setAttribute("aria-expanded", "false"); + } +} + async function insertSiteNavButtons() { // Clear current stack of dynamic elements in sidebar $(".site-nav-bar-button").remove(); + $(".site-nav-bar-button-fav").remove(); $(".site-nav-bar-title").remove(); $(".site-nav-bar > .horizontal-seperator").remove(); + $(".collapse-section").remove(); $(".site-nav-bar > .disk-container").remove(); let disks = await invoke("list_disks"); @@ -4606,10 +4658,10 @@ async function insertSiteNavButtons() { button.className = "site-nav-bar-button"; button.innerHTML = ` ${siteNavButtons[i][0]}`; button.setAttribute("itempath", siteNavButtons[i][1]); - button.onclick = siteNavButtons[i][3]; // Support for dragging files to the directory + button.onclick = siteNavButtons[i][3]; button.ondragover = (e) => { - button.style.border = "1px solid var(--selectColor2)"; - button.style.backgroundColor = "var(--selectColor3)"; + button.style.border = "1px solid var(--tertiaryColor)"; + button.style.backgroundColor = "var(--sidebarHover)"; button.style.scale = "1.05"; DraggedOverElement = button; MousePos = [e.clientX, e.clientY]; @@ -4622,22 +4674,35 @@ async function insertSiteNavButtons() { document.querySelector(".site-nav-bar").append(button); } - // Favorites + // Favorites collapsible section if (ArrFavorites.length > 0) { - let seperator = document.createElement("div"); - seperator.className = "horizontal-seperator"; - document.querySelector(".site-nav-bar").append(seperator); + const favSection = document.createElement("div"); + favSection.className = "collapse-section"; + favSection.dataset.section = "favorites"; + + const favHeader = document.createElement("button"); + favHeader.className = "collapse-header"; + favHeader.innerHTML = ` +
+ + FAVORITES +
+ + `; + favHeader.setAttribute("aria-expanded", "true"); + favHeader.setAttribute("aria-controls", "favorites-content"); + favHeader.onclick = () => toggleCollapseSection(favSection); - let favTitle = document.createElement("p"); - favTitle.className = "site-nav-bar-title"; - favTitle.innerHTML = "FAVORITES"; - document.querySelector(".site-nav-bar").append(favTitle); + const favContent = document.createElement("div"); + favContent.className = "collapse-content"; + favContent.id = "favorites-content"; + favContent.setAttribute("role", "region"); ArrFavorites.forEach((path) => { let button = document.createElement("button"); - button.className = "site-nav-bar-button"; + button.className = "site-nav-bar-button-fav"; let name = path.split(/[\\\/]/).pop() || path; - button.innerHTML = `

${name}

`; + button.innerHTML = `

${name}

`; button.setAttribute("itempath", path); button.title = path; button.onclick = async () => { @@ -4655,40 +4720,51 @@ async function insertSiteNavButtons() { ]); }; button.ondragover = (e) => { - button.style.border = "1px solid var(--selectColor2)"; - button.style.backgroundColor = "var(--selectColor3)"; - button.style.scale = "1.05"; + button.style.border = "1px solid var(--tertiaryColor)"; + button.style.backgroundColor = "var(--sidebarHover)"; DraggedOverElement = button; MousePos = [e.clientX, e.clientY]; }; button.ondragleave = () => { button.style.border = "1px solid transparent"; button.style.backgroundColor = "transparent"; - button.style.scale = "1"; }; - document.querySelector(".site-nav-bar").append(button); + favContent.append(button); }); + + favSection.append(favHeader); + favSection.append(favContent); + document.querySelector(".site-nav-bar").append(favSection); + restoreCollapseState(favSection); } - let seperator = document.createElement("div"); - seperator.className = "horizontal-seperator"; - document.querySelector(".site-nav-bar").append(seperator); + // Disks collapsible section + const diskSection = document.createElement("div"); + diskSection.className = "collapse-section"; + diskSection.dataset.section = "disks"; + + const diskHeader = document.createElement("button"); + diskHeader.className = "collapse-header"; + diskHeader.innerHTML = ` +
+ + DISKS +
+ + `; + diskHeader.setAttribute("aria-expanded", "true"); + diskHeader.setAttribute("aria-controls", "disks-content"); + diskHeader.onclick = () => toggleCollapseSection(diskSection); + + const diskContent = document.createElement("div"); + diskContent.className = "collapse-content"; + diskContent.id = "disks-content"; + diskContent.setAttribute("role", "region"); let diskContainer = document.createElement("div"); diskContainer.className = "disk-container"; - // Available disks as site nav buttons - let diskButton = document.createElement("button"); - diskButton.className = "site-nav-bar-button"; - diskButton.onclick = () => listDisks(); - diskButton.innerHTML = ` Disks`; - document.querySelector(".site-nav-bar").append(diskButton); - if (disks.length > 0) { - let seperator2 = document.createElement("div"); - seperator2.className = "horizontal-seperator"; - document.querySelector(".site-nav-bar").append(seperator2); - disks.forEach((mount) => { let diskButton = document.createElement("button"); diskButton.dataset.itempath = mount.path; @@ -4703,7 +4779,6 @@ async function insertSiteNavButtons() { await openDirAndSwitch(mount.path); await listDirectories(); }; - // Show space left with gradient diskButton.style.background = `linear-gradient(to right, var(--selectColor3) ${(100 - (100 / mount.capacity) * mount.avail).toFixed(2)}%, var(--transparentColor), transparent)`; diskButton.style.backgroundRepeat = "no-repeat"; if (mount.format.includes("SSHFS") || mount.is_removable == true) { @@ -4725,7 +4800,11 @@ async function insertSiteNavButtons() { }); } - document.querySelector(".site-nav-bar").append(diskContainer); + diskContent.append(diskContainer); + diskSection.append(diskHeader); + diskSection.append(diskContent); + document.querySelector(".site-nav-bar").append(diskSection); + restoreCollapseState(diskSection); } /* File operation context menu */ diff --git a/ui/style.css b/ui/style.css index b867a47c..b82d9199 100644 --- a/ui/style.css +++ b/ui/style.css @@ -47,6 +47,8 @@ --selectColor: rgba(69, 69, 69, 0.144); --selectColor2: rgba(11, 100, 253, 0.75); --selectColor3: rgba(11, 100, 253, 0.25); + --sidebarHover: rgba(255, 255, 255, 0.08); + --sidebarFocus: rgba(255, 255, 255, 0.12); } html { @@ -248,9 +250,39 @@ body { border-right: 1px solid var(--tertiaryColor); border-radius: 0px; transition: width 0.4s cubic-bezier(0.165, 0.84, 0.44, 1), padding 0.4s ease, min-width 0.4s ease; - overflow: hidden; - position: relative; + overflow-x: hidden; overflow-y: auto; + min-height: 0; +} + +.site-nav-bar-blur-top { + position: fixed; + top: 0; + left: 0; + width: 150px; + height: 70px; + z-index: 10; + pointer-events: none; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + -webkit-mask-image: linear-gradient(to bottom, black 0%, black 40%, transparent 100%); + mask-image: linear-gradient(to bottom, black 0%, black 40%, transparent 100%); +} + +.site-nav-bar-gradient-top { + position: fixed; + top: 0; + left: 0; + width: 150px; + height: 70px; + z-index: 11; + pointer-events: none; + background: linear-gradient( + to bottom, + rgba(36, 36, 44, 1) 0%, + rgba(36, 36, 44, 0.7) 30%, + transparent 100% + ); } .site-nav-bar-button { @@ -282,15 +314,14 @@ body { } .site-nav-bar-button:hover { - background-color: var(--selectColor3) !important; + background-color: var(--sidebarHover) !important; color: var(--textColor); - scale: 1.025; } .site-nav-bar-button:focus-visible { outline: 2px solid var(--selectColor2); outline-offset: 2px; - background-color: var(--selectColor3) !important; + background-color: var(--sidebarFocus) !important; } .disk-site-nav-button { @@ -303,7 +334,7 @@ body { background-color: var(--secondaryColor); } .disk-site-nav-button:hover { - background-color: var(--selectColor3) !important; + background-color: var(--sidebarHover) !important; } .disk-container { @@ -315,6 +346,7 @@ body { gap: 2px; overflow-y: auto; overflow-x: hidden; + flex-shrink: 0; box-shadow: inset 0px 0px 10px 5px rgba(100, 100, 100, 0.1), 0px 0px 5px 2px rgba(0, 0, 0, 0.1); border-radius: 10px; } @@ -866,12 +898,44 @@ input:checked + .slider:before { /* Context Menu Animation */ .context-menu { transform-origin: top left; - animation: fadeIn 0.15s ease-out, scaleUp 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); + animation: contextMenuIn 0.18s cubic-bezier(0.16, 1, 0.3, 1); +} + +.context-menu.context-menu--closing { + animation: contextMenuOut 0.12s ease-in forwards; +} + +@keyframes contextMenuIn { + from { + opacity: 0; + transform: scale(0.92) translateY(-4px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } } -@keyframes scaleUp { - from { transform: scale(0.85); } - to { transform: scale(1); } +@keyframes contextMenuOut { + from { + opacity: 1; + transform: scale(1) translateY(0); + } + to { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } +} + +@keyframes contextItemIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } } /* Toast Animation */ @@ -886,7 +950,7 @@ input:checked + .slider:before { .site-nav-bar-button:hover { padding-left: 15px; - background-color: var(--selectColor3) !important; + background-color: var(--sidebarHover) !important; } /* Settings UI Transition */ @@ -1442,19 +1506,208 @@ button > svg { width: 40px; } +.view-mode-group { + display: flex; + align-items: center; + background-color: var(--secondaryColor); + border: 1px solid var(--tertiaryColor); + border-radius: 8px; + padding: 3px; + height: 32px; + gap: 1px; +} + +.view-mode-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 26px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--textColor2); + cursor: pointer; + transition: background-color 0.1s ease-out, color 0.1s ease-out; + padding: 0; + font-size: 11px; +} + +.view-mode-btn:hover { + background-color: var(--transparentColor); + color: var(--textColor); +} + +.view-mode-btn:active { + background-color: var(--transparentColorActive); +} + +.view-mode-btn.active { + background-color: var(--transparentColorActive); + color: var(--textColor); +} + +.view-mode-btn:focus-visible { + outline: 2px solid var(--selectColor2); + outline-offset: -1px; +} + +.view-mode-btn:disabled { + cursor: not-allowed; + opacity: 0.4; +} + +.view-mode-btn:disabled:hover { + background: transparent; + color: var(--textColor2); +} + +.view-mode-group.disabled { + opacity: 0.5; + pointer-events: none; +} + .site-nav-bar-title { font-size: 10px; color: var(--textColor2); width: 100%; padding-left: 10px; margin: 5px 0; + flex-shrink: 0; font-weight: bold !important; letter-spacing: 1px; } +/* Collapsible Sidebar Sections */ +.collapse-section { + width: 100%; + display: flex; + flex-flow: column; + align-items: center; + border-top: 1px solid var(--tertiaryColor); + padding-top: 4px; +} + +.collapse-header { + width: 100%; + height: 32px; + min-height: 32px; + display: flex; + flex-flow: row; + justify-content: space-between; + align-items: center; + padding: 0 10px; + background-color: transparent; + border: none; + border-radius: 8px; + cursor: pointer; + color: var(--textColor2); + font-size: 10px; + font-weight: bold !important; + letter-spacing: 1px; + transition: background-color 0.15s ease, color 0.15s ease; + flex-shrink: 0; +} + +.collapse-header:hover { + background-color: var(--sidebarHover); + color: var(--textColor); +} + +.collapse-header:focus-visible { + outline: 2px solid var(--selectColor2); + outline-offset: 2px; + background-color: var(--sidebarFocus); +} + +.collapse-header-left { + display: flex; + flex-flow: row; + align-items: center; + gap: 6px; + pointer-events: none; +} + +.collapse-header-left > i { + font-size: 10px; + color: var(--textColor2); +} + +.collapse-chevron { + font-size: 9px; + color: var(--textColor2); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; +} + +.collapse-section.collapsed .collapse-chevron { + transform: rotate(-90deg); +} + +.collapse-content { + width: 100%; + display: flex; + flex-flow: column; + align-items: center; + overflow: hidden; + max-height: 1000px; + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.2s ease; + opacity: 1; +} + +.collapse-section.collapsed .collapse-content { + max-height: 0 !important; + opacity: 0; +} + +/* Compact Favorites Button */ +.site-nav-bar-button-fav { + background-color: transparent; + border: 1px solid transparent; + width: 100%; + height: 28px; + min-height: 28px; + text-align: left; + border-radius: 6px; + padding: 4px 10px; + cursor: pointer; + display: flex; + flex-flow: row; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--textColor); + transition: 0.2s ease; +} + +.site-nav-bar-button-fav > p { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; +} + +.site-nav-bar-button-fav > i { + font-size: 9px; +} + +.site-nav-bar-button-fav:hover { + background-color: var(--sidebarHover) !important; + color: var(--textColor); +} + +.site-nav-bar-button-fav:focus-visible { + outline: 2px solid var(--selectColor2); + outline-offset: 2px; + background-color: var(--sidebarFocus) !important; +} + .horizontal-seperator { width: 90%; height: 1px; + min-height: 1px; + flex-shrink: 0; background-color: var(--tertiaryColor); margin: 2px 0px; } @@ -1471,37 +1724,53 @@ button > svg { display: flex; flex-flow: row; align-items: center; + background-color: var(--primaryColor); border: 1px solid var(--tertiaryColor); - width: 200px; - height: 80%; - border-radius: 10px; + width: 220px; + height: 34px; + border-radius: 6px; overflow: hidden; margin: 0 auto; - transition: 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.2s ease; +} + +.file-searchbar:focus-within { + background-color: var(--secondaryColor); } .file-searchbar > input { width: 100%; - padding: 10px 15px; - border-radius: 5px 0px 0px 5px; + padding: 0 12px; + height: 100%; border: none; - z-index: 3; + background: transparent; outline: none; color: var(--textColor); + font-size: 12px; + letter-spacing: 0.2px; + border-radius: 0; } -.file-searchbar > input:hover { - background-color: var(--tertiaryColor); +.file-searchbar > input::placeholder { + color: var(--textColor2); + font-size: 11px; } .search-button, .cancel-search-button { - padding: 10px 15px 10px 12px; + display: flex; + align-items: center; + justify-content: center; + width: 34px; + min-width: 34px; + height: 100%; border: none; - background-color: var(--secondaryColor); + background: transparent; cursor: pointer; - z-index: 2; - transition: 0.3s ease; + color: var(--textColor2); + font-size: 12px; + transition: color 0.15s ease, background-color 0.15s ease; + flex-shrink: 0; } .cancel-search-button { @@ -1510,7 +1779,8 @@ button > svg { .search-button:hover, .cancel-search-button:hover { - background-color: var(--transparentColorActive); + color: var(--textColor); + background-color: var(--sidebarHover); } .search-bar-container { @@ -1798,61 +2068,75 @@ button > svg { } .context-menu { - width: 180px; + width: 190px; height: fit-content; - padding: 5px; + padding: 6px; display: flex; flex-flow: column; - justify-content: center; - align-items: center; + align-items: stretch; position: fixed; top: 0; left: 0; - margin: auto; - background-color: var(--primaryColor); - border-radius: 10px; - box-shadow: 0px 0px 10px 0px var(--transparentColorActive); + background-color: color-mix(in srgb, var(--primaryColor) 85%, transparent); + backdrop-filter: blur(24px) saturate(1.4); + -webkit-backdrop-filter: blur(24px) saturate(1.4); + border-radius: 12px; + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.35), + 0 2px 8px rgba(0, 0, 0, 0.2), + inset 0 0.5px 0 rgba(255, 255, 255, 0.06); z-index: 999; - border: 1px solid var(--tertiaryColor); + border: 1px solid color-mix(in srgb, var(--tertiaryColor) 50%, transparent); } .context-item { color: var(--textColor); width: 100%; height: fit-content; + min-height: 32px; background-color: transparent; - border-radius: 10px; - margin: 2px 0; - padding: 7px 10px; - gap: 10px; + border-radius: 8px; + margin: 1px 0; + padding: 0 10px; + gap: 0; cursor: pointer; text-align: left; display: flex; flex-flow: row; - justify-content: space-between; align-items: center; - transition: 0.1s ease-out; + transition: + background-color 0.12s ease, + color 0.12s ease, + transform 0.1s ease; font-size: var(--fontSize); border: none; + opacity: 0; + animation: contextItemIn 0.15s ease forwards; + position: relative; +} + +.context-item:active { + transform: scale(0.97); + background-color: var(--transparentColorActive); } .context-item:hover { - background-color: var(--transparentColor); + background-color: var(--sidebarHover); color: var(--textColor); } .c-item-disabled { - font-style: italic; - color: rgba(200, 200, 200, 0.3) !important; + opacity: 0.35; + cursor: default; + pointer-events: none; } -.c-item-disabled > svg { - font-style: italic; - color: rgba(200, 200, 200, 0.3) !important; +.c-item-disabled:hover { + background-color: transparent; } -.c-item-disabled:hover { - color: rgba(200, 200, 200, 0.3); +.c-item-disabled:active { + transform: none; } .context-with-dropdown { @@ -1883,6 +2167,106 @@ button > svg { border: 1px solid var(--tertiaryColor); } +/* Context menu icon + label group */ +.context-item-group { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; +} + +/* Context menu icon (left-aligned) */ +.context-item-icon { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + flex-shrink: 0; + font-size: 13px; + opacity: 0.7; + transition: opacity 0.12s ease; +} + +.context-item:hover .context-item-icon { + opacity: 1; +} + +/* Context menu label */ +.context-item .context-label { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 400; + letter-spacing: 0.01em; +} + +/* Chevron for submenu items */ +.context-item-chevron { + display: flex; + align-items: center; + justify-content: center; + width: 14px; + flex-shrink: 0; + margin-left: 8px; + opacity: 0.4; + transition: opacity 0.12s ease, transform 0.12s ease; +} + +.context-item:hover .context-item-chevron { + opacity: 0.7; + transform: translateX(1px); +} + +/* Divider / separator between groups */ +.context-divider { + height: 1px; + margin: 4px 8px; + background-color: var(--tertiaryColor); + border: none; + flex-shrink: 0; +} + +/* Staggered animation for each item */ +.context-item:nth-child(1) { animation-delay: 0.00s; } +.context-item:nth-child(2) { animation-delay: 0.01s; } +.context-item:nth-child(3) { animation-delay: 0.02s; } +.context-item:nth-child(4) { animation-delay: 0.03s; } +.context-item:nth-child(5) { animation-delay: 0.04s; } +.context-item:nth-child(6) { animation-delay: 0.05s; } +.context-item:nth-child(7) { animation-delay: 0.06s; } +.context-item:nth-child(8) { animation-delay: 0.07s; } +.context-item:nth-child(9) { animation-delay: 0.08s; } +.context-item:nth-child(10) { animation-delay: 0.09s; } +.context-item:nth-child(11) { animation-delay: 0.10s; } +.context-item:nth-child(12) { animation-delay: 0.11s; } +.context-item:nth-child(13) { animation-delay: 0.12s; } +.context-item:nth-child(14) { animation-delay: 0.13s; } +.context-item:nth-child(15) { animation-delay: 0.14s; } + +/* Submenu scrollbar */ +.context-submenu::-webkit-scrollbar { + width: 4px; +} + +.context-submenu::-webkit-scrollbar-thumb { + background-color: var(--transparentColorActive); + border-radius: 4px; +} + +/* Reduced motion fallback */ +@media (prefers-reduced-motion: reduce) { + .context-menu, + .context-item, + .context-menu.context-menu--closing { + animation: none !important; + transition: none !important; + opacity: 1 !important; + transform: none !important; + } +} + .open-with-item:hover { background-color: var(--transparentColorActive); color: var(--textColor); @@ -2627,25 +3011,34 @@ button > svg { } .custom-context-menu { - background-color: var(--primaryColor); - border: 1px solid var(--tertiaryColor); - border-radius: 10px; - padding: 5px; - box-shadow: 0px 0px 10px 1px var(--transparentColorActive); + background-color: color-mix(in srgb, var(--primaryColor) 85%, transparent); + backdrop-filter: blur(24px) saturate(1.4); + -webkit-backdrop-filter: blur(24px) saturate(1.4); + border: 1px solid color-mix(in srgb, var(--tertiaryColor) 50%, transparent); + border-radius: 12px; + padding: 6px; + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.35), + 0 2px 8px rgba(0, 0, 0, 0.2), + inset 0 0.5px 0 rgba(255, 255, 255, 0.06); color: var(--textColor); z-index: 3; } .c-item-custom { - padding: 5px 10px; + padding: 7px 10px; border: none; - border-radius: 5px; + border-radius: 8px; cursor: pointer; - transition: 0.2s ease; - background-color: var(--transparentColor); + transition: background-color 0.12s ease, transform 0.1s ease; + background-color: transparent; color: var(--textColor); font-size: var(--fontSize); } .c-item-custom:hover { + background-color: var(--sidebarHover); +} +.c-item-custom:active { + transform: scale(0.97); background-color: var(--transparentColorActive); } From 3c363e7dc63e6fb71527fc999185da6e2d4e1f2b Mon Sep 17 00:00:00 2001 From: Ricky Dane Perlick Date: Sat, 16 May 2026 20:49:26 +0200 Subject: [PATCH 02/46] docs: add design spec for soft & elevated popup overhaul --- .../specs/2026-05-16-popup-overhaul-design.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-16-popup-overhaul-design.md diff --git a/docs/superpowers/specs/2026-05-16-popup-overhaul-design.md b/docs/superpowers/specs/2026-05-16-popup-overhaul-design.md new file mode 100644 index 00000000..93e506ed --- /dev/null +++ b/docs/superpowers/specs/2026-05-16-popup-overhaul-design.md @@ -0,0 +1,54 @@ +# Design: Popup System & Settings UI Overhaul (Soft & Elevated) + +## Purpose +Overhaul the CoDriver popup system and Settings UI to use a unified architecture and a modern, "Soft & Elevated" aesthetic inspired by macOS/iOS native applications. + +## Architecture & Consolidation +1. **Unified Manager:** All existing popups (loading, confirm, input, previews, conflicts) and the Settings UI will be migrated to use the `PopupManager` class (currently defined in `ui/popup-system.js` but seemingly underutilized). +2. **Deprecation:** The legacy `.uni-popup` structure and ad-hoc creation functions (e.g., `showPopup`, `showInputPopup`, `toggleSettings`) will be refactored to wrap or utilize `PopupManager.open()`. +3. **Settings UI:** The Settings panel, currently a toggled side/overlay element, will become a standard managed popup, ensuring consistent behavior for focus trapping, backdrop blurring, and Escape-to-close. + +## Aesthetic: Soft & Elevated +The visual design moves away from sharp borders and basic backgrounds toward an airy, friendly, and structured look. + +### Key Visual Characteristics +- **Generous Padding:** Increased breathing room inside popups (e.g., `32px` outer padding, `16px` inner element padding). +- **Large Border Radii:** + - Outer popup container: `24px` + - Inner elements (buttons, setting cards): `16px` or fully rounded (`9999px`) for buttons/toggles. +- **Soft Shadows:** Dropping harsh drop-shadows for a diffuse, soft shadow (`0 20px 25px -5px rgba(0, 0, 0, 0.05)`). +- **Icon-led Headers:** Centered or prominent headers featuring a circular, soft-background icon wrapper. +- **Card-based Content:** Individual settings or list items wrapped in subtle, interactive cards (`.setting-card`) that elevate slightly on hover. + +### CSS Variables & Theme Integration +Since CoDriver supports multiple themes (Dark, Default, Hacker, Light), the "Soft & Elevated" structural properties (padding, radius, animation, shadows) will be defined as new base tokens, while colors will map to existing theme variables (`var(--primaryColor)`, `var(--secondaryColor)`, `var(--textColor)`). + +**New Structural Tokens (to be added to `:root`):** +```css +--popup-radius-xl: 24px; +--popup-radius-lg: 16px; +--popup-radius-full: 9999px; +--popup-shadow-soft: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.05); +``` + +### Components +1. **Container:** `.soft-popup` with entry animation (`scaleUp`), large radius, and soft shadow. +2. **Header:** `.soft-header` containing an `.icon-wrapper` and text. +3. **Body/Cards:** `.soft-body` containing `.setting-card` elements for lists or settings. +4. **Controls:** `.soft-toggle` for boolean settings, replacing basic checkboxes. +5. **Buttons:** `.btn-soft` (primary and cancel variants) with full rounding and hover elevation. + +## Data Flow & Lifecycle +1. Caller invokes `PopupManager.open(options)`. +2. Manager constructs the DOM elements using the "Soft & Elevated" CSS classes. +3. Backdrop is shown with a soft blur. +4. Popup animates in (`scaleUp`). +5. Upon user action or Escape, the exit animation plays. +6. Popup is removed from DOM and Promise/callback resolves. + +## Execution Strategy +1. Introduce the new CSS architecture in `ui/style.css`. +2. Update `ui/popup-system.js` to construct popups using the new HTML/CSS structure. +3. Migrate existing `showPopup`, `showLoadingPopup`, `showConfirmPopup` etc., in `ui/main_logic.js` to use `PopupManager`. +4. Migrate Settings UI to use `PopupManager` and the `.setting-card` layout. +5. Verify cross-theme compatibility and remove legacy `.uni-popup` CSS. \ No newline at end of file From 57874afcd670d753a1ead8239129a6db20a88e10 Mon Sep 17 00:00:00 2001 From: Ricky Dane Perlick Date: Sat, 16 May 2026 21:11:52 +0200 Subject: [PATCH 03/46] feat(ui): complete overhaul of popup system and settings UI to Soft & Elevated design --- .../popup-visual-overhaul/decisions.md | 77 + .../popup-visual-overhaul/handoffs.md | 92 ++ .../implementation-tracker.md | 56 + docs/planning/popup-visual-overhaul/plan.md | 356 +++++ docs/review/popup-visual-overhaul/findings.md | 330 ++++ docs/review/popup-visual-overhaul/summary.md | 54 + .../plans/2026-05-16-popup-overhaul.md | 560 +++++++ ui/index.html | 1 + ui/main_logic.js | 1357 +++++++++-------- ui/popup-system.js | 342 +++++ ui/style.css | 179 ++- 11 files changed, 2728 insertions(+), 676 deletions(-) create mode 100644 docs/planning/popup-visual-overhaul/decisions.md create mode 100644 docs/planning/popup-visual-overhaul/handoffs.md create mode 100644 docs/planning/popup-visual-overhaul/implementation-tracker.md create mode 100644 docs/planning/popup-visual-overhaul/plan.md create mode 100644 docs/review/popup-visual-overhaul/findings.md create mode 100644 docs/review/popup-visual-overhaul/summary.md create mode 100644 docs/superpowers/plans/2026-05-16-popup-overhaul.md create mode 100644 ui/popup-system.js diff --git a/docs/planning/popup-visual-overhaul/decisions.md b/docs/planning/popup-visual-overhaul/decisions.md new file mode 100644 index 00000000..d59cb257 --- /dev/null +++ b/docs/planning/popup-visual-overhaul/decisions.md @@ -0,0 +1,77 @@ +# Decisions: Complete Visual Overhaul of All Popups + +## D1: Exit Animation Pattern for Promise-Based Popups + +**Context:** `showPopup()` and `showDestinationConflictPopup()` return Promises that resolve when the popup closes. The `closeConfirmPopup()` function directly removes the DOM element. + +**Decision:** Resolve the Promise immediately, then animate out. The animation is purely cosmetic (150ms). Waiting for `animationend` before resolving would add unnecessary latency to the calling code. + +**Pattern:** +```js +// Resolve immediately +resolve(value); +// Animate out +popup.classList.add("popup-exit"); +popup.addEventListener("animationend", () => popup.remove(), { once: true }); +``` + +For `closeConfirmPopup()` specifically: +```js +function closeConfirmPopup() { + const popup = document.querySelector(".confirm-popup"); + if (popup) { + popup.classList.add("popup-exit"); + popup.addEventListener("animationend", () => popup.remove(), { once: true }); + } + $(".popup-background").css("display", "none"); + $(".popup-background").css("opacity", "0"); + IsPopUpOpen = false; +} +``` + +**Rationale:** The 150ms exit animation is too short for users to interact with the popup after resolve. No functional difference. Avoids complicating the Promise chain. + +--- + +## D2: Keep `.item-preview-popup` jQuery fadeOut + +**Context:** `.item-preview-popup` already uses `$(popup).fadeIn(200)` on open and `$(popup).fadeOut(200, callback)` on close via `closeItemPreview()`. + +**Decision:** Leave as-is. jQuery fadeOut already provides a smooth exit. Adding CSS animation on top would conflict. + +**Exception:** Still add `.popup-enter` class for the entrance animation (CSS `popupIn` is smoother than jQuery fadeIn due to scale transform). But the exit stays jQuery-managed. + +--- + +## D3: Keep `.settings-ui` CSS Class Toggle + +**Context:** `.settings-ui` uses `.active` class toggle for show/hide with CSS transitions (`opacity` + `transform`). + +**Decision:** Leave as-is. Already has entrance/exit transitions built in. Apply glass tokens to the base styles only. + +--- + +## D4: Border-Radius Consistency at 12px + +**Context:** Context menu uses `12px`. Current popups use mix of `10px` and `15px`. Settings panel uses `15px`. + +**Decision:** Standardize all popups to `12px` via `--glass-radius`. Settings panel header `border-radius` adjusts to `12px 12px 0 0`. + +--- + +## D5: `popup-background` Blur Strength + +**Context:** Current backdrop blur is `2px`. Context menu doesn't use backdrop (it's inline). Task says "stronger blur." + +**Decision:** Use `blur(12px) saturate(1.2)` with `background-color: rgba(0, 0, 0, 0.45)`. This creates a strong frosted glass effect behind popups without being too dark. 12px is enough to noticeably blur content behind the overlay while keeping it recognizable. + +--- + +## D6: Typography in Headers + +**Context:** Current `.popup-header h3` uses `font-weight: bolder`. Task wants "better typography hierarchy." + +**Decision:** +- `.popup-header h3`: `font-weight: 700`, `font-size: 0.95em`, `letter-spacing: -0.01em` +- Keep existing `gap: 10px` between icon and text +- No changes to body text (already uses `--textColor` / `--textColor2` hierarchy) diff --git a/docs/planning/popup-visual-overhaul/handoffs.md b/docs/planning/popup-visual-overhaul/handoffs.md new file mode 100644 index 00000000..2c249714 --- /dev/null +++ b/docs/planning/popup-visual-overhaul/handoffs.md @@ -0,0 +1,92 @@ +# Handoffs: Complete Visual Overhaul of All Popups + +## software-engineer + +### Context +CoDriver is a Tauri desktop file explorer. Frontend is vanilla HTML/CSS/JS in `ui/`. The context menu (`.custom-context-menu` at line 3013 of `style.css`) already has a modern glassmorphism style. All popups need to match this aesthetic. + +### Files to Modify +1. **`ui/style.css`** (primary) — lines 2293-3011 for popup styles, lines 1-50 for `:root` variables, lines 650-680 for keyframes +2. **`ui/main_logic.js`** (secondary) — animation class toggling in ~15 functions + +### Implementation Steps + +**CSS (`ui/style.css`):** +1. Add glass token variables under `:root` (after line 50) +2. Add `@keyframes popupIn` and `@keyframes popupOut` (near line 650) +3. Add `.popup-enter` and `.popup-exit` utility classes +4. Restyle `.popup-background` (line 2998) — stronger blur +5. Restyle `.uni-popup` (line 2386) — glass base +6. Restyle `.popup-header` (line 2548) — glass bg + typography +7. Restyle `.popup-body` (line 2566) — remove hard border +8. Restyle `.popup-controls` (line 2572) — glass footer +9. Restyle `.popup-close-button` (line 2527) — glass +10. Restyle `.input-dialog` (line 2293), `.input-popup` (line 2365), `.loading-popup` (line 2336) +11. Restyle `.item-preview-popup` (line 2424), `.multi-rename-popup-header` (line 2407) +12. Restyle `.destination-conflict-card` (line 2914) and options +13. Restyle `.settings-ui` (line 684), `.settings-ui-header` (line 715), `.settings-sidebar` (line 729) +14. Restyle `.progress-bar-container-popup` (line 2785) +15. Add scoped button/input overrides inside popups +16. Add reduced-motion media query and scrollbar styling + +**JS (`ui/main_logic.js`):** +17. In each `show*` function: add `popup.classList.add("popup-enter")` after `appendChild`/`append` +18. In each `close*` function: replace direct `.remove()` with exit animation pattern + +### Acceptance Criteria +- All popups visually match `.custom-context-menu` glassmorphism +- Entrance animation: scale(0.95) + fade-in, ~220ms +- Exit animation: scale(0.97) + fade-out, ~150ms, then DOM removal +- All text readable on glass backgrounds +- All existing functionality preserved +- Works with all CSS variable themes +- `prefers-reduced-motion` respected + +### Key Reference +Context menu style (line 3013): +```css +.custom-context-menu { + background-color: color-mix(in srgb, var(--primaryColor) 85%, transparent); + backdrop-filter: blur(24px) saturate(1.4); + -webkit-backdrop-filter: blur(24px) saturate(1.4); + border: 1px solid color-mix(in srgb, var(--tertiaryColor) 50%, transparent); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35), 0 2px 8px rgba(0, 0, 0, 0.2), inset 0 0.5px 0 rgba(255, 255, 255, 0.06); +} +``` + +### Risks +- **Double-close race condition:** If user rapidly clicks close button, multiple exit animations could fire. Mitigate with a `isClosing` guard flag. +- **jQuery `.remove()` vs exit animation:** Some close functions use `$(selector).remove()` (jQuery). Need to get the DOM element reference first, then apply exit animation. +- **`closeConfirmPopup()` Promise pattern:** Resolve Promise immediately, animate out after. See decisions.md D1. + +--- + +## code-reviewer + +### Review Focus Areas +1. **Animation timing:** Verify entrance/exit durations feel natural (not too fast/slow) +2. **Glass rendering:** Check that `backdrop-filter` renders correctly in Tauri WebView +3. **Z-index stacking:** Ensure popups still layer correctly above content +4. **Theme compatibility:** Verify glass tokens work with non-default themes +5. **Reduced-motion:** Confirm `prefers-reduced-motion` disables all animations +6. **No regressions:** Every popup type still opens, functions, and closes correctly +7. **Edge cases:** Rapid open/close, Escape key during animation, multiple popups + +### Expected Output +- `docs/review/popup-visual-overhaul/findings.md` +- `docs/review/popup-visual-overhaul/summary.md` + +--- + +## documentation-writer + +### Changes to Document +- Visual overhaul of all popup dialogs (glassmorphism + animations) +- No user-facing functional changes +- No migration or breaking changes +- No new features — purely cosmetic + +### Expected Output +- Release notes summarizing the visual refresh +- Commit message following project conventions diff --git a/docs/planning/popup-visual-overhaul/implementation-tracker.md b/docs/planning/popup-visual-overhaul/implementation-tracker.md new file mode 100644 index 00000000..6b1f1c17 --- /dev/null +++ b/docs/planning/popup-visual-overhaul/implementation-tracker.md @@ -0,0 +1,56 @@ +# Implementation Tracker: Complete Visual Overhaul of All Popups + +## Status Summary +**Overall Status:** Completed +**Current Phase:** All phases complete +**Last Updated:** 2026-05-16 + +## Task Table + +| ID | Task | Owner | Status | Depends On | Evidence | Next Step | +|----|------|-------|--------|------------|----------|-----------| +| T1 | Add glass CSS variables to `:root` | software-engineer | Completed | None | `ui/style.css:53-61` | — | +| T2 | Add `@keyframes popupIn` and `popupOut` | software-engineer | Completed | None | `ui/style.css:957-977` | — | +| T3 | Add `.popup-enter` / `.popup-exit` utility classes | software-engineer | Completed | T2 | `ui/style.css:3071-3078` | — | +| T4 | Restyle `.popup-background` (stronger blur) | software-engineer | Completed | T1 | `ui/style.css:3056-3069` | — | +| T5 | Restyle `.uni-popup` (glass base) | software-engineer | Completed | T1 | `ui/style.css:2443-2457` | — | +| T6 | Restyle `.popup-header` (glass + typography) | software-engineer | Completed | T1 | `ui/style.css:2608-2628` | — | +| T7 | Restyle `.popup-body` (remove hard border) | software-engineer | Completed | T1 | `ui/style.css:2630-2633` | — | +| T8 | Restyle `.popup-controls` (glass footer) | software-engineer | Completed | T1 | `ui/style.css:2635-2645` | — | +| T9 | Restyle `.popup-close-button` (glass) | software-engineer | Completed | T1 | `ui/style.css:2587-2600` | — | +| T10 | Restyle `.input-dialog` | software-engineer | Completed | T5 | `ui/style.css:2350-2370` | — | +| T11 | Restyle `.input-popup` | software-engineer | Completed | T5 | `ui/style.css:2422-2440` | — | +| T12 | Restyle `.loading-popup` | software-engineer | Completed | T5 | `ui/style.css:2393-2420` | — | +| T13 | Restyle `.item-preview-popup` | software-engineer | Completed | T5 | `ui/style.css:2484-2498` | — | +| T14 | Restyle `.multi-rename-popup-header` | software-engineer | Completed | T5 | `ui/style.css:2465-2474` | — | +| T15 | Restyle `.destination-conflict-card` + options | software-engineer | Completed | T5 | `ui/style.css:2972-2978`, `ui/style.css:2998-3006` | — | +| T16 | Restyle `.settings-ui` + header/sidebar | software-engineer | Completed | T1 | `ui/style.css:684-706`, `ui/style.css:715-727`, `ui/style.css:729-737`, `ui/style.css:809-814` | — | +| T17 | Restyle `.progress-bar-container-popup` | software-engineer | Completed | T1 | `ui/style.css:2845-2862` | — | +| T18 | Add scoped button/input overrides inside popups | software-engineer | Completed | T5 | `ui/style.css:3080-3103` | — | +| T19 | JS: Add `.popup-enter` to all popup creation functions | software-engineer | Completed | T3 | `ui/main_logic.js:4925`, `ui/main_logic.js:1805`, `ui/main_logic.js:1825`, `ui/main_logic.js:2025`, `ui/main_logic.js:4075`, `ui/main_logic.js:3869`, `ui/main_logic.js:1658` | — | +| T20 | JS: Add `.popup-exit` to all popup close functions | software-engineer | Completed | T3 | `ui/main_logic.js:4978-4984`, `ui/main_logic.js:1810-1815`, `ui/main_logic.js:1840-1846`, `ui/main_logic.js:2035-2041`, `ui/main_logic.js:4141-4148`, `ui/main_logic.js:3886-3892`, `ui/main_logic.js:1786-1794` | — | +| T21 | Final polish: reduced-motion, scrollbars, edge cases | software-engineer | Completed | T18 | `ui/style.css:3189-3209` | — | +| T22 | Visual QA: test all popup types | code-reviewer | Planned | T21 | — | Review all popups | + +## Blockers +None + +## Drift Log +No drift. + +## Implementation Summary + +### CSS Changes (`ui/style.css`) +- **Phase 1**: Added 8 glass CSS variables under `:root` (lines 53-61). Added `@keyframes popupIn` and `popupOut` after existing keyframes (lines 957-977). +- **Phase 2**: Updated `.popup-background` with stronger overlay + blur (lines 3056-3069). Updated `.uni-popup` with full glass treatment (lines 2443-2457). Updated `.popup-header` with glass bg, backdrop blur, border-bottom, improved typography (lines 2608-2628). Removed `border-top` from `.popup-body` (line 2633). Updated `.popup-controls` with glass footer (lines 2635-2645). Updated `.popup-close-button` with glass bg + transition (lines 2587-2600). Added `.popup-enter` and `.popup-exit` utility classes (lines 3071-3078). +- **Phase 3**: Updated `.input-dialog` with glass treatment (lines 2350-2370). Updated `.input-popup` with glass treatment (lines 2422-2440). Updated `.loading-popup` with glass tokens (lines 2393-2420). +- **Phase 4**: Updated `.item-preview-popup` with glass tokens (lines 2484-2498). Updated `.multi-rename-popup-header` with glass header bg (lines 2465-2474). Updated `.destination-conflict-card` with glass bg (lines 2972-2978). Updated `.destination-conflict-options label` with glass-consistent bg (lines 2998-3006). +- **Phase 5**: Updated `.settings-ui` with glass treatment (lines 684-706). Updated `.settings-ui-header` with glass header bg (lines 715-727). Updated `.settings-sidebar` with glass bg (lines 729-737). Updated `.settings-ui .popup-controls` with glass treatment (lines 809-814). Updated `.progress-bar-container-popup` with glass tokens (lines 2845-2862). +- **Phase 6**: Added scoped overrides for `.uni-popup .icon-button` (glass bg, hover, active with scale) and `.uni-popup .text-input`/`.uni-popup .number-input` (glass bg, focus border) (lines 3080-3103). +- **Phase 8**: Added `@media (prefers-reduced-motion: reduce)` block for popup elements (lines 3189-3199). Added scrollbar styling inside popups (lines 3201-3209). + +### JS Changes (`ui/main_logic.js`) +- **Phase 7**: Added `popup.classList.add("popup-enter")` after appendChild in: `showPopup()` (line 4925), `showLoadingPopup()` (line 1805), `showInputPopup()` (line 1825), `showDestinationConflictPopup()` (line 2025), `showMultiRenamePopup()` (line 4075), `showProperties()` (line 3869), compression popup (line 1658). +- Replaced direct `.remove()` with exit animation pattern in: `closeConfirmPopup()` (lines 4978-4984), `closeLoadingPopup()` (lines 1810-1815), `closeInputPopup()` (lines 1840-1846), conflict popup close callback (lines 2035-2041), `closeMultiRenamePopup()` (lines 4141-4148), `closeInfoProperties()` (lines 3886-3892), `closeCompressPopup()` (lines 1786-1794). +- Left `closeItemPreview()` unchanged (uses jQuery fadeOut). +- Left settings UI unchanged (uses CSS class toggle). diff --git a/docs/planning/popup-visual-overhaul/plan.md b/docs/planning/popup-visual-overhaul/plan.md new file mode 100644 index 00000000..fad1a956 --- /dev/null +++ b/docs/planning/popup-visual-overhaul/plan.md @@ -0,0 +1,356 @@ +# Plan: Complete Visual Overhaul of All Popups + +## Summary +Restyle every popup/modal/dialog in CoDriver to match the modern glassmorphism aesthetic established by `.custom-context-menu`. Add entrance/exit animations, layered shadows, improved typography, and better button styling. Changes touch `ui/style.css` (primary) and `ui/main_logic.js` (minimal — animation class toggling only). + +## Goals +- All popups match context menu's glassmorphism: semi-transparent glass bg, blur, layered shadows, subtle borders +- Entrance animations: scale(0.95) + fade-in, ~200ms, smooth easing +- Exit animations: scale(0.97) + fade-out, ~150ms, then DOM removal +- Stronger backdrop blur on `.popup-background` (8px → 12px) +- Typography hierarchy: headers use `font-weight: 700`, proper sizing, subtle letter-spacing +- Better button styling in `.popup-controls`: glass-consistent, clear hover/active states +- Border-radius consistency: 12px everywhere (matching context menu) +- All CSS variables respected — works with every theme +- Preserve all existing functionality + +## Non-Goals +- Changing HTML structure of existing popups +- Adding new popup types +- Refactoring popup creation logic +- Changing CSS variable values or color scheme +- Touching popup z-index hierarchy + +## Assumptions +- `backdrop-filter` works in Tauri WebView (Chromium) — confirmed by existing usage +- `color-mix()` available — confirmed by context menu usage +- No CSS preprocessor — vanilla CSS only +- `--primaryColor`, `--tertiaryColor`, `--textColor` etc. remain unchanged +- jQuery available for `fadeOut()` (already used by `.item-preview-popup`) + +## Open Questions +None — scope is clear and self-contained. + +## Dependencies +- Single file for CSS: `ui/style.css` +- Single file for JS: `ui/main_logic.js` (animation class toggling only) +- No backend/Rust changes needed + +## Execution Phases + +### Phase 1: CSS Design Tokens & Keyframes +**Owner:** software-engineer +**Files:** `ui/style.css` +**Output:** New CSS variables under `:root`, new `@keyframes` for popup animations +**Acceptance Criteria:** Glass tokens defined, animation keyframes exist, no visual change yet. + +**Add under `:root` (after line 50):** +```css +/* Popup glassmorphism tokens */ +--glass-bg: color-mix(in srgb, var(--primaryColor) 85%, transparent); +--glass-border: 1px solid color-mix(in srgb, var(--tertiaryColor) 50%, transparent); +--glass-blur: blur(24px) saturate(1.4); +--glass-shadow: + 0 8px 32px rgba(0, 0, 0, 0.35), + 0 2px 8px rgba(0, 0, 0, 0.2), + inset 0 0.5px 0 rgba(255, 255, 255, 0.06); +--glass-radius: 12px; +--glass-header-bg: color-mix(in srgb, var(--secondaryColor) 80%, transparent); +--glass-border-subtle: 1px solid color-mix(in srgb, var(--tertiaryColor) 35%, transparent); +``` + +**Add keyframes (near existing `@keyframes` block ~line 650):** +```css +@keyframes popupIn { + from { + opacity: 0; + transform: scale(0.95) translateY(8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +@keyframes popupOut { + from { + opacity: 1; + transform: scale(1) translateY(0); + } + to { + opacity: 0; + transform: scale(0.97) translateY(-4px); + } +} +``` + +--- + +### Phase 2: Base Popup Classes (Glass + Animation) +**Owner:** software-engineer +**Files:** `ui/style.css` +**Output:** Updated `.popup-background`, `.uni-popup`, `.popup-header`, `.popup-body`, `.popup-controls`, `.popup-close-button` +**Acceptance Criteria:** All base classes use glass tokens, entrance animation applied via `.popup-enter`, exit via `.popup-exit`. + +**`.popup-background` (line 2998):** +- `background-color: rgba(0, 0, 0, 0.45)` (stronger overlay) +- `backdrop-filter: blur(12px) saturate(1.2)` (up from 2px) + +**`.uni-popup` (line 2386):** +- `background-color: var(--glass-bg)` +- `backdrop-filter: var(--glass-blur)` +- `border: var(--glass-border)` +- `border-radius: var(--glass-radius)` +- `box-shadow: var(--glass-shadow)` + +**`.popup-header` (line 2548):** +- `background-color: var(--glass-header-bg)` +- `backdrop-filter: blur(12px)` +- `border-bottom: var(--glass-border-subtle)` +- `padding: 12px 16px` (slightly more breathing room) +- `h3` → `font-weight: 700`, `font-size: 0.95em`, `letter-spacing: -0.01em` + +**`.popup-body` (line 2566):** +- Remove `border-top: 1px solid var(--tertiaryColor)` (glass handles separation) + +**`.popup-controls` (line 2572):** +- `border-top: var(--glass-border-subtle)` +- `background-color: color-mix(in srgb, var(--primaryColor) 60%, transparent)` +- `backdrop-filter: blur(8px)` +- `padding: 10px 14px` + +**`.popup-close-button` (line 2527):** +- `background-color: color-mix(in srgb, var(--tertiaryColor) 60%, transparent)` +- `transition: background-color 0.15s ease` + +**New animation utility classes:** +```css +.popup-enter { + animation: popupIn 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} +.popup-exit { + animation: popupOut 0.15s ease-in forwards; + pointer-events: none; +} +``` + +--- + +### Phase 3: Input/Dialog Popups +**Owner:** software-engineer +**Files:** `ui/style.css` +**Output:** Updated `.input-dialog`, `.input-popup`, `.loading-popup` +**Acceptance Criteria:** Glass treatment applied, text inputs remain usable and readable. + +**`.input-dialog` (line 2293):** +- Apply glass: bg, blur, border, radius, shadow +- Remove old `box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.2)` + +**`.input-popup` (line 2365):** +- Same glass treatment as `.input-dialog` + +**`.loading-popup` (line 2336):** +- Apply glass tokens +- Inner `h4` background: `color-mix(in srgb, var(--secondaryColor) 80%, transparent)` + +--- + +### Phase 4: Content-Specific Popups +**Owner:** software-engineer +**Files:** `ui/style.css` +**Output:** Updated all content-specific popup classes +**Acceptance Criteria:** Each popup inherits glass base + has popup-specific overrides consistent with glass aesthetic. + +All these extend `.uni-popup`, so they inherit glass from Phase 2. Only specific overrides: + +**`.item-preview-popup` (line 2424):** +- Apply glass tokens (currently uses `var(--transparentColorActive)` as bg) +- Keep `backdrop-filter: blur(5px)` → upgrade to `var(--glass-blur)` + +**`.item-properties-popup` (line 2480):** +- Inherits from `.uni-popup` — no extra glass needed + +**`.multi-rename-popup` (line 2401):** +- Inherits from `.uni-popup` — no extra glass needed +- `.multi-rename-popup-header` → glass header bg + +**`.confirm-popup` (line 2864):** +- Inherits from `.uni-popup` — no extra glass needed + +**`.destination-conflict-popup` (line 2879):** +- Inherits from `.uni-popup` — no extra glass needed +- `.destination-conflict-card` → `background: color-mix(in srgb, var(--secondaryColor) 80%, transparent)` +- `.destination-conflict-options label` → glass-consistent bg + +**`.find-duplicates-popup` (line 2608):** +- Inherits from `.uni-popup` — no extra glass needed + +**`.yt-download-popup` (line 2638):** +- Inherits from `.uni-popup` — no extra glass needed + +**`.llm-prompt-input-popup` (line 2644):** +- Inherits from `.uni-popup` — no extra glass needed + +**`.compression-popup` (line 2417):** +- Inherits from `.uni-popup` — no extra glass needed + +--- + +### Phase 5: Settings Panel & Progress Bar +**Owner:** software-engineer +**Files:** `ui/style.css` +**Output:** Updated `.settings-ui`, `.settings-ui-header`, `.settings-sidebar`, `.progress-bar-container-popup` +**Acceptance Criteria:** Settings panel and progress bar match glass aesthetic. + +**`.settings-ui` (line 684):** +- `background-color: var(--glass-bg)` +- `backdrop-filter: var(--glass-blur)` +- `border: var(--glass-border)` +- `border-radius: var(--glass-radius)` (12px, was 15px) +- `box-shadow: var(--glass-shadow)` +- Keep existing transition for `.active` toggle + +**`.settings-ui-header` (line 715):** +- `background-color: var(--glass-header-bg)` +- `backdrop-filter: blur(12px)` +- `border-bottom: var(--glass-border-subtle)` +- `border-radius: 12px 12px 0 0` + +**`.settings-sidebar` (line 729):** +- `background-color: color-mix(in srgb, var(--secondaryColor) 60%, transparent)` +- `border-right: var(--glass-border-subtle)` + +**`.progress-bar-container-popup` (line 2785):** +- `background-color: var(--glass-bg)` +- `backdrop-filter: var(--glass-blur)` +- `border: var(--glass-border)` +- `border-radius: var(--glass-radius)` +- `box-shadow: var(--glass-shadow)` + +--- + +### Phase 6: Buttons & Interactive Elements Inside Popups +**Owner:** software-engineer +**Files:** `ui/style.css` +**Output:** Scoped overrides for buttons, inputs, selects inside popups +**Acceptance Criteria:** All interactive elements inside popups have glass-consistent styling. + +```css +/* Buttons inside popups */ +.uni-popup .icon-button { + background-color: color-mix(in srgb, var(--secondaryColor) 80%, transparent); + border: var(--glass-border-subtle); + transition: background-color 0.15s ease, transform 0.1s ease; +} +.uni-popup .icon-button:hover { + background-color: var(--transparentColor); +} +.uni-popup .icon-button:active { + background-color: var(--transparentColorActive); + transform: scale(0.97); +} + +/* Text inputs inside popups */ +.uni-popup .text-input, +.uni-popup .number-input { + background-color: color-mix(in srgb, var(--secondaryColor) 70%, transparent); + border: var(--glass-border-subtle); +} +.uni-popup .text-input:focus, +.uni-popup .number-input:focus { + border-color: var(--selectColor2); +} +``` + +--- + +### Phase 7: JS Animation Integration +**Owner:** software-engineer +**Files:** `ui/main_logic.js` +**Output:** Add `.popup-enter` on creation, `.popup-exit` before removal +**Acceptance Criteria:** Popups animate in/out; no visual glitching; exit animation completes before DOM removal. + +**Pattern for entrance (add after `appendChild`):** +```js +popup.classList.add("popup-enter"); +``` + +**Pattern for exit (replace direct `.remove()`):** +```js +popup.classList.add("popup-exit"); +popup.addEventListener("animationend", () => popup.remove(), { once: true }); +``` + +**Functions to modify:** + +| Function | Line | Change | +|----------|------|--------| +| `showPopup()` | 4862 | Add `.popup-enter` after appendChild | +| `closeConfirmPopup()` | 4968 | Replace `.remove()` with exit animation | +| `showLoadingPopup()` | 1791 | Add `.popup-enter` after append | +| `closeLoadingPopup()` | 1802 | Replace `.remove()` with exit animation | +| `showInputPopup()` | 1807 | Add `.popup-enter` after append | +| `closeInputPopup()` | 1832 | Replace `.remove()` with exit animation | +| `showDestinationConflictPopup()` | 1973 | Add `.popup-enter` after appendChild | +| `close` callback in conflict popup | 2030 | Replace `.remove()` with exit animation | +| `showMultiRenamePopup()` | 3998 | Add `.popup-enter` after append | +| `closeMultiRenamePopup()` | 4118 | Replace `.remove()` with exit animation | +| `showProperties()` | 3826 | Add `.popup-enter` after append | +| `closeInfoProperties()` | 3874 | Replace `.remove()` with exit animation | +| `showItemPreview()` | 3891 | Add `.popup-enter` after append | +| `closeItemPreview()` | 4123 | Already uses fadeOut — keep as-is | +| compression popup | 1616 | Add `.popup-enter` after append | +| `closeCompressPopup()` | 1785 | Replace `.remove()` with exit animation | + +**Note:** `.item-preview-popup` already uses jQuery `fadeOut()` — leave unchanged. `.settings-ui` uses CSS class toggle (`.active`) — leave unchanged. + +--- + +### Phase 8: Final Polish +**Owner:** software-engineer +**Files:** `ui/style.css` +**Output:** Reduced-motion support, scrollbar styling inside popups, edge case fixes +**Acceptance Criteria:** No visual regressions; reduced-motion respected; tested across all popup types. + +```css +/* Reduced motion fallback */ +@media (prefers-reduced-motion: reduce) { + .uni-popup, + .settings-ui, + .popup-background, + .popup-enter, + .popup-exit { + animation: none !important; + transition: none !important; + } +} + +/* Scrollbar inside popups */ +.uni-popup ::-webkit-scrollbar { + width: 4px; + height: 4px; +} +.uni-popup ::-webkit-scrollbar-thumb { + background-color: color-mix(in srgb, var(--tertiaryColor) 60%, transparent); + border-radius: 4px; +} +``` + +--- + +## Validation Plan +1. Open each popup type in running app +2. Verify glassmorphism: blur visible, semi-transparent, correct border/shadow +3. Verify entrance animation plays on open (scale + fade) +4. Verify exit animation plays on close (scale + fade, then removal) +5. Verify text readability on all backgrounds +6. Test hover/active states on buttons inside popups +7. Test keyboard focus outlines still visible +8. Test with `prefers-reduced-motion` enabled +9. Test responsive behavior (resize window) +10. Test dark theme (default) and any other themes +11. Verify no z-index regressions +12. Verify no timing issues (double-click close, rapid open/close) + +## Rollback Plan +All changes in two files: `ui/style.css` and `ui/main_logic.js`. Git revert of the commit is the rollback path. No database, config, or backend changes to unwind. diff --git a/docs/review/popup-visual-overhaul/findings.md b/docs/review/popup-visual-overhaul/findings.md new file mode 100644 index 00000000..61e53b6b --- /dev/null +++ b/docs/review/popup-visual-overhaul/findings.md @@ -0,0 +1,330 @@ +# Code Review Findings: Popup Visual Overhaul + +**Reviewer:** Code Reviewer Agent +**Date:** 2026-05-16 +**Files Reviewed:** `ui/style.css` (3209 lines), `ui/main_logic.js` (5128 lines) +**Plan Reference:** `docs/planning/popup-visual-overhaul/plan.md` + +--- + +## Major Issues (5) + +### [M-001] CSS Typo: Broken Variable Reference in `.popup-close-button:active` + +**Severity:** Major +**Location:** `ui/style.css:2596` + +### Description +The `:active` state of `.popup-close-button` references `var(--tr ansparentColorActive)` — there's a space splitting the variable name. CSS will silently fail to resolve this token, leaving the active state with no background color. + +### Evidence +```css +.popup-close-button:active { + background-color: var(--tr ansparentColorActive); /* ← space in name */ +} +``` + +### Impact +When users click (mousedown) on any popup close button, no background highlight appears. Visual feedback is broken for this interaction state. + +### Recommendation +Fix the variable name: +```css +background-color: var(--transparentColorActive); +``` + +--- + +### [M-002] `showItemPreview()` Uses jQuery fadeIn Instead of `.popup-enter` + +**Severity:** Major +**Location:** `ui/main_logic.js:4007-4008` +**Plan Reference:** Phase 7 — "Add `.popup-enter` after append" + +### Description +The plan explicitly states to add `.popup-enter` class after `appendChild` in `showItemPreview()`. The implementation instead uses jQuery `$(popup).fadeIn(fadeTime)`. This means the item preview popup gets a simple opacity fade rather than the coordinated scale(0.95) + translateY(8px) entrance animation used by every other popup. + +### Evidence +```js +document.querySelector("body").append(popup); +$(popup).fadeIn(fadeTime); // ← plan says .popup-enter, not jQuery fadeIn +``` + +### Impact +Item preview is the only popup that doesn't animate with the glassmorphism entrance pattern. Visual inconsistency — every other popup scales in, but item preview just fades in. + +### Recommendation +Replace the jQuery fadeIn with the standard entrance pattern: +```js +document.querySelector("body").append(popup); +popup.classList.add("popup-enter"); +``` +Note: `closeItemPreview()` already uses jQuery fadeOut and should remain unchanged per plan. + +--- + +### [M-003] `.input-dialog` Popups Missing Entrance/Exit Animations + +**Severity:** Major +**Location:** `ui/main_logic.js:2176-2235, 2237-2242, 2244-2280` + +### Description +Four functions that create or destroy `.input-dialog` popups don't use the animation pattern: + +| Function | Issue | +|----------|-------| +| `createFolderInputPrompt()` | No `.popup-enter` on creation, direct `.remove()` on Enter | +| `createFileInputPrompt()` | No `.popup-enter` on creation, direct `.remove()` on Enter | +| `renameElementInputPrompt()` | No `.popup-enter` on creation, direct `.remove()` on Enter | +| `closeInputDialogs()` | Direct `$(".input-dialog").remove()` — no exit animation | + +### Evidence +```js +// createFolderInputPrompt — no popup-enter +document.querySelector("body").append(nameInput); +// ... direct remove on Enter: +nameInput.remove(); + +// closeInputDialogs — direct remove +function closeInputDialogs() { + $(".input-dialog").remove(); // ← no exit animation +} +``` + +### Impact +Rename, new folder, and new file dialogs appear/disappear abruptly instead of with the smooth scale+fade animation. These are high-frequency popups users encounter regularly. + +### Recommendation +Add `.popup-enter` after append in each creation function. Replace direct `.remove()` with exit animation pattern in each close path. For `closeInputDialogs()`, iterate and animate each: +```js +function closeInputDialogs() { + document.querySelectorAll(".input-dialog").forEach(popup => { + popup.classList.add("popup-exit"); + popup.addEventListener("animationend", () => popup.remove(), { once: true }); + }); + // ... state resets +} +``` + +--- + +### [M-004] `showFindDuplicates()` / `closeFindDuplicatesPopup()` Missing Animations + +**Severity:** Major +**Location:** `ui/main_logic.js:4354-4408, 4410-4414` + +### Description +The find duplicates popup is created without `.popup-enter` and destroyed with direct `.remove()`. The plan's Phase 7 function table doesn't include these functions — this appears to be an oversight. + +### Evidence +```js +// showFindDuplicates — no popup-enter +document.querySelector("body").append(popup); +// (no popup.classList.add("popup-enter")) + +// closeFindDuplicatesPopup — direct remove +function closeFindDuplicatesPopup() { + IsPopUpOpen = false; + cancelOperation(); + document.querySelector(".find-duplicates-popup")?.remove(); // ← no exit animation +} +``` + +### Impact +Duplicates popup appears/disappears without animation. + +### Recommendation +Add `.popup-enter` after append in `showFindDuplicates()`. Use exit animation pattern in `closeFindDuplicatesPopup()`. + +--- + +### [M-005] `showYtDownload()` / `closeYtDownloadPopup()` Missing Animations + +**Severity:** Major +**Location:** `ui/main_logic.js:4428-4481, 4493-4497` + +### Description +Same pattern as M-004. The YouTube download popup is created without `.popup-enter` and destroyed with direct `.remove()`. Also missing from the plan's Phase 7 function table. + +### Evidence +```js +// showYtDownload — no popup-enter +document.querySelector("body").append(popup); +// (no popup.classList.add("popup-enter")) + +// closeYtDownloadPopup — direct remove +async function closeYtDownloadPopup() { + IsPopUpOpen = false; + cancelOperation(); + document.querySelector(".yt-download-popup")?.remove(); // ← no exit animation +} +``` + +### Impact +YouTube download popup appears/disappears without animation. + +### Recommendation +Add `.popup-enter` after append in `showYtDownload()`. Use exit animation pattern in `closeYtDownloadPopup()`. + +--- + +## Minor Issues (5) + +### [m-001] `.destination-conflict-header` Overrides Glass Border-Bottom + +**Severity:** Minor +**Location:** `ui/style.css:2944-2947` + +### Description +The `.destination-conflict-header` class sets `border-bottom: 1px solid var(--tertiaryColor)`, overriding the `.popup-header`'s glass-consistent `border-bottom: var(--glass-border-subtle)`. The glass token uses `color-mix(in srgb, var(--tertiaryColor) 35%, transparent)` for a subtle semi-transparent border, but this override makes it fully opaque. + +### Evidence +```css +.destination-conflict-header { + justify-content: space-between; + border-bottom: 1px solid var(--tertiaryColor); /* ← overrides glass */ +} +``` + +### Impact +Slight visual inconsistency — conflict popup header border is more prominent than other popup headers. + +### Recommendation +Replace with the glass token: +```css +border-bottom: var(--glass-border-subtle); +``` + +--- + +### [m-002] Inline Styles Use Solid Borders Instead of Glass Tokens + +**Severity:** Minor +**Location:** `ui/main_logic.js:1623, 4031, 4035` + +### Description +Several inline `border-bottom` styles in popup HTML templates use `1px solid var(--tertiaryColor)` instead of the glass-subtle equivalent. While these are inside popup bodies (not headers), they create visual inconsistency with the glass aesthetic. + +### Evidence +```js +// Compression popup (line 1623) +`
` + +// Multi-rename popup (lines 4031, 4035) +`
` +`
` +``` + +### Impact +Minor visual inconsistency — these borders are fully opaque while the glass aesthetic favors semi-transparent borders. + +### Recommendation +Either use CSS classes with glass tokens, or update inline styles to use the glass-subtle equivalent. Low priority since these are internal dividers. + +--- + +### [m-003] `createFileInputPrompt()` Missing `.uni-popup` Class + +**Severity:** Minor +**Location:** `ui/main_logic.js:2211` + +### Description +`createFolderInputPrompt()` (line 2181) sets `nameInput.className = "input-dialog uni-popup"`, but `createFileInputPrompt()` (line 2211) sets `nameInput.className = "input-dialog"` without `uni-popup`. This means the scoped `.uni-popup .text-input` overrides (glass input styling) don't apply to the file creation dialog. + +### Evidence +```js +// createFolderInputPrompt — has uni-popup +nameInput.className = "input-dialog uni-popup"; + +// createFileInputPrompt — missing uni-popup +nameInput.className = "input-dialog"; +``` + +### Impact +Text input inside the file creation dialog uses the default `.text-input` styling instead of the glass-consistent `.uni-popup .text-input` override. + +### Recommendation +Add `uni-popup` to the file creation dialog: +```js +nameInput.className = "input-dialog uni-popup"; +``` + +--- + +### [m-004] `resetEverything()` Hides Background During Exit Animations + +**Severity:** Minor +**Location:** `ui/main_logic.js:343` + +### Description +`resetEverything()` calls `$(".popup-background").css("display", "none")` immediately, while the exit animations triggered by the close functions (0.15s each) are still playing. This means the backdrop blur disappears before the popups finish fading out. + +### Evidence +```js +async function resetEverything() { + closeLoadingPopup(); // starts 0.15s exit animation + // ... other close calls ... + closeConfirmPopup(); // starts 0.15s exit animation + $(".popup-background").css("display", "none"); // ← immediate + // ... +} +``` + +### Impact +During the 0.15s exit animation window, popups are fading out against a clear background instead of the blurred backdrop. Users may briefly see the underlying content through the fading popup. + +### Recommendation +Delay the background removal until exit animations complete, or accept the brief visual gap as acceptable for a "reset everything" escape-hatch function. + +--- + +### [m-005] Duplicate `animationend` Listeners on Repeated Close Calls + +**Severity:** Minor +**Location:** Various close functions + +### Description +If a close function is called twice before the exit animation completes (e.g., `resetEverything()` while a close is already in progress), the `animationend` listener is registered twice. Both fire and both call `.remove()`, but the second `.remove()` is a no-op on an already-removed element. + +### Impact +Harmless but inefficient. No functional breakage. + +### Recommendation +Low priority. Could add a guard (e.g., check for `.popup-exit` class before adding) but the current behavior is safe. + +--- + +## Plan Alignment + +### Plan Phases Covered + +| Phase | Status | Notes | +|-------|--------|-------| +| Phase 1: CSS Design Tokens & Keyframes | ✅ Complete | All tokens and keyframes match plan exactly | +| Phase 2: Base Popup Classes | ✅ Complete | All base classes use glass tokens | +| Phase 3: Input/Dialog Popups | ✅ Complete | `.input-dialog`, `.input-popup`, `.loading-popup` all styled | +| Phase 4: Content-Specific Popups | ✅ Complete | All popup types inherit glass from `.uni-popup` | +| Phase 5: Settings Panel & Progress Bar | ✅ Complete | Glass treatment applied | +| Phase 6: Buttons & Interactive Elements | ✅ Complete | Scoped overrides present | +| Phase 7: JS Animation Integration | ⚠️ Partial | 12/16 functions done, 4 missing (see M-003, M-004, M-005), 1 deviates (see M-002) | +| Phase 8: Final Polish | ✅ Complete | Reduced-motion fallback, scrollbar styling present | + +### Plan Deviations + +1. **M-002**: `showItemPreview()` uses jQuery fadeIn instead of `.popup-enter` — direct plan deviation +2. **M-004, M-005**: `showFindDuplicates`, `closeFindDuplicatesPopup`, `showYtDownload`, `closeYtDownloadPopup` were omitted from the plan's Phase 7 function table — plan gap +3. **M-003**: `.input-dialog` creation/close functions were omitted from the plan's Phase 7 function table — plan gap + +### Acceptance Criteria + +| Criteria | Status | +|----------|--------| +| All popups match context menu glassmorphism | ✅ | +| Entrance animations: scale(0.95) + fade-in, ~200ms | ✅ (where implemented) | +| Exit animations: scale(0.97) + fade-out, ~150ms | ✅ (where implemented) | +| Stronger backdrop blur on `.popup-background` | ✅ | +| Typography hierarchy in headers | ✅ | +| Better button styling in `.popup-controls` | ✅ | +| Border-radius consistency: 12px everywhere | ✅ | +| All CSS variables respected | ✅ | +| Preserve all existing functionality | ✅ | diff --git a/docs/review/popup-visual-overhaul/summary.md b/docs/review/popup-visual-overhaul/summary.md new file mode 100644 index 00000000..6cb8dc70 --- /dev/null +++ b/docs/review/popup-visual-overhaul/summary.md @@ -0,0 +1,54 @@ +# Code Review Summary: Popup Visual Overhaul + +**Reviewer:** Code Reviewer Agent +**Date:** 2026-05-16 +**Status:** Needs Changes + +## Overall Assessment + +The CSS implementation is solid — all glass tokens, keyframes, and popup class styling match the plan exactly. The glassmorphism treatment is consistently applied across all popup types. The main gaps are in the JS animation integration: 5 popup creation/close functions are missing entrance or exit animations, and 1 function deviates from the plan by using jQuery fadeIn instead of the `.popup-enter` class. + +## Critical Issues (0) + +None. + +## Major Issues (5) + +| ID | Issue | Location | +|----|-------|----------| +| M-001 | CSS typo: `var(--tr ansparentColorActive)` — broken variable in `.popup-close-button:active` | `style.css:2596` | +| M-002 | `showItemPreview()` uses jQuery fadeIn instead of `.popup-enter` (plan deviation) | `main_logic.js:4008` | +| M-003 | `.input-dialog` popups (rename, new folder, new file) missing entrance/exit animations | `main_logic.js:2176-2280` | +| M-004 | `showFindDuplicates()` / `closeFindDuplicatesPopup()` missing animations | `main_logic.js:4354-4414` | +| M-005 | `showYtDownload()` / `closeYtDownloadPopup()` missing animations | `main_logic.js:4428-4497` | + +## Minor Issues (5) + +| ID | Issue | Location | +|----|-------|----------| +| m-001 | `.destination-conflict-header` overrides glass border-bottom with solid border | `style.css:2944` | +| m-002 | Inline styles in compression/multi-rename popups use solid borders | `main_logic.js:1623,4031,4035` | +| m-003 | `createFileInputPrompt()` missing `.uni-popup` class (inconsistent with folder variant) | `main_logic.js:2211` | +| m-004 | `resetEverything()` hides backdrop immediately while exit animations still playing | `main_logic.js:343` | +| m-005 | Duplicate `animationend` listeners possible on repeated close calls | Various | + +## Positive Findings + +- All 8 glass tokens correctly defined in `:root` — exact match with plan +- `@keyframes popupIn` / `popupOut` syntactically correct with proper easing +- `.popup-enter` / `.popup-exit` utility classes correctly implemented +- Consistent 12px border-radius across all popups via `--glass-radius` +- Proper `prefers-reduced-motion` fallback covering all popup elements +- All `animationend` listeners use `{ once: true }` — no memory leaks +- Promise-based popups (confirm, delete, extract, prompt, destination conflict) resolve correctly — resolve() fires before exit animation starts +- Settings UI correctly left as CSS toggle (`.active` class) +- `closeItemPreview()` correctly left as jQuery fadeOut +- Typography hierarchy applied in all popup headers +- Scoped button/input overrides inside `.uni-popup` for consistent interactive element styling +- Scrollbar styling inside popups with glass-consistent thumb + +## Recommendation + +Fix M-001 (CSS typo) immediately — it's a one-character fix with visible impact. Address M-003, M-004, M-005 by adding the `.popup-enter` / exit animation pattern to the remaining popup functions. Resolve M-002 by switching `showItemPreview()` to use `.popup-enter` instead of jQuery fadeIn. + +After fixes, all popups will have consistent glassmorphism styling and entrance/exit animations. diff --git a/docs/superpowers/plans/2026-05-16-popup-overhaul.md b/docs/superpowers/plans/2026-05-16-popup-overhaul.md new file mode 100644 index 00000000..af48092c --- /dev/null +++ b/docs/superpowers/plans/2026-05-16-popup-overhaul.md @@ -0,0 +1,560 @@ +# Popup Overhaul Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Refactor all popups and the Settings UI to use the `PopupManager` with the modern "Soft & Elevated" design. + +**Architecture:** We will add new structural CSS variables and classes to `ui/style.css`, update `PopupManager` in `ui/popup-system.js` to generate the new DOM structure (`.soft-popup`, `.soft-header`, etc.), and migrate `ui/main_logic.js` functions to use `PopupManager.open()`. The Settings UI will be updated to use the `.setting-card` layout and managed by `PopupManager`. + +**Tech Stack:** Vanilla JS, Vanilla CSS, HTML. + +--- + +### Task 1: Add "Soft & Elevated" CSS tokens and base classes + +**Files:** +- Modify: `ui/style.css` + +- [ ] **Step 1: Write UI test script** + +Create `test_popup.html` to visually verify the classes. + +```html + + + + + + +
+

Test

+
Body
+
+ + +``` + +- [ ] **Step 2: Run test to verify it fails (looks unstyled)** + +Run: `open test_popup.html` (or open in a browser) +Expected: The popup should look unstyled and basic, without soft shadows or rounded corners. + +- [ ] **Step 3: Write minimal implementation** + +Append these tokens to the `:root` block in `ui/style.css`: + +```css + --popup-radius-xl: 24px; + --popup-radius-lg: 16px; + --popup-radius-full: 9999px; + --popup-shadow-soft: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.05); +``` + +Append these classes to the end of `ui/style.css`: + +```css +.soft-popup { + background: var(--primaryColor); + border-radius: var(--popup-radius-xl); + box-shadow: var(--popup-shadow-soft); + width: min(440px, 90vw); + overflow: hidden; + animation: scaleUp 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); + border: 1px solid rgba(255,255,255,0.05); + color: var(--textColor); +} + +.soft-header { + padding: 32px 32px 16px; + text-align: center; + position: relative; +} + +.soft-header h3 { + margin: 0; + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.03em; +} + +.soft-body { + padding: 0 32px 32px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.soft-footer { + padding: 24px 32px; + background: var(--secondaryColor); + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.btn-soft { + padding: 12px 24px; + border-radius: var(--popup-radius-full); + font-weight: 600; + font-size: 0.95rem; + cursor: pointer; + border: none; + transition: all 0.2s ease; + text-align: center; +} + +.btn-primary { + background: var(--selectColor); + color: white; +} + +.btn-cancel { + background: var(--tertiaryColor); + color: var(--textColor); +} + +@keyframes scaleUp { + from { opacity: 0; transform: scale(0.9); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes scaleDown { + from { opacity: 1; transform: scale(1); } + to { opacity: 0; transform: scale(0.95); } +} + +.soft-popup.is-closing { + animation: scaleDown 0.2s ease forwards; +} + +/* Settings Cards */ +.setting-card { + background: var(--secondaryColor); + border-radius: var(--popup-radius-lg); + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid transparent; +} +.setting-name { font-weight: 600; } +.setting-desc { font-size: 0.85rem; color: var(--textColor2); } +``` + +- [ ] **Step 4: Run test to verify it passes** + +Refresh `test_popup.html` in browser. +Expected: The popup should now have large rounded corners, a soft shadow, and proper styling. + +- [ ] **Step 5: Commit** + +```bash +rm test_popup.html +git add ui/style.css +git commit -m "feat(ui): add soft & elevated popup tokens and classes" +``` + +--- + +### Task 2: Refactor PopupManager + +**Files:** +- Modify: `ui/popup-system.js` + +- [ ] **Step 1: Write test script** + +Create `test_manager.html`. + +```html + + + + + + + + +``` + +- [ ] **Step 2: Run test to verify it fails (uses old classes)** + +Run: `open test_manager.html` +Expected: The popup uses `cd-popup` classes, not the new `soft-popup` styling. + +- [ ] **Step 3: Write minimal implementation** + +In `ui/popup-system.js`, update `PopupManager.open()` to use the new DOM structure. + +Find: +```javascript + // Create popup wrapper + const popup = document.createElement("div"); + popup.className = `cd-popup ${className}`; + + // Create container + const container = document.createElement("div"); + container.className = "cd-popup__container"; +``` + +Replace with: +```javascript + // Create popup wrapper + const popup = document.createElement("div"); + popup.className = `soft-popup ${className}`; +``` +*(Remove `container` creation and appending entirely. Append directly to `popup`)* + +Find: +```javascript + // Header + if (!noHeader) { + const header = document.createElement("div"); + header.className = "cd-popup__header"; +``` + +Replace the entire Header block with: +```javascript + if (!noHeader) { + const header = document.createElement("div"); + header.className = "soft-header"; + + const heading = document.createElement("h3"); + heading.textContent = title; + header.appendChild(heading); + + popup.appendChild(header); + } +``` + +Find the Body block and replace with: +```javascript + const body = document.createElement("div"); + body.className = "soft-body"; + body.innerHTML = content; + popup.appendChild(body); +``` + +Find the Controls block and replace with: +```javascript + if (buttons.length > 0) { + const controls = document.createElement("div"); + controls.className = "soft-footer"; + + buttons.forEach((btn) => { + const button = document.createElement("button"); + button.className = `btn-soft ${btn.primary ? 'btn-primary' : 'btn-cancel'} ${btn.className || ""}`; + button.innerHTML = `${btn.label || "Button"}`; + button.onclick = () => { + if (typeof btn.action === "function") { + const result = btn.action(); + if (result !== false) this.close(popup); + } else { + this.close(popup); + } + }; + controls.appendChild(button); + }); + popup.appendChild(controls); + } + + appendTo.appendChild(popup); +``` + +Also, update `PopupManager.close()` to use `.is-closing`: +Find `popupElement.classList.remove("is-open");` and replace with `popupElement.classList.add("is-closing");` + +- [ ] **Step 4: Run test to verify it passes** + +Refresh `test_manager.html`. +Expected: Popup looks soft and elevated, buttons styled correctly. + +- [ ] **Step 5: Commit** + +```bash +rm test_manager.html +git add ui/popup-system.js +git commit -m "refactor(ui): update PopupManager to generate soft & elevated DOM" +``` + +--- + +### Task 3: Migrate Loading & Input Popups + +**Files:** +- Modify: `ui/main_logic.js` + +- [ ] **Step 1: Write test script** + +Create `test_logic.html`. + +```html + + + + + + + + + + + +``` + +- [ ] **Step 2: Run test to verify it fails (uses legacy DOM)** + +Inspect DOM while open, it uses `
`. + +- [ ] **Step 3: Write minimal implementation** + +In `ui/main_logic.js`: + +Replace `showLoadingPopup` implementation: +```javascript +let currentLoadingPopup = null; +function showLoadingPopup(msg) { + closeLoadingPopup(); // Ensure no duplicates + currentLoadingPopup = PopupManager.open({ + noHeader: true, + content: `
+ +

${msg}

+
`, + allowBackdropClose: false, + allowEscapeClose: false + }); +} +``` + +Replace `closeLoadingPopup` implementation: +```javascript +function closeLoadingPopup() { + if (currentLoadingPopup) { + PopupManager.close(currentLoadingPopup); + currentLoadingPopup = null; + } +} +``` + +Replace `showInputPopup` implementation: +```javascript +let currentInputPopup = null; +function showInputPopup(msg) { + closeInputPopup(); + return new Promise((resolve) => { + currentInputPopup = PopupManager.open({ + title: "Input Required", + content: `

${msg}

`, + allowBackdropClose: false, + buttons: [ + { label: "Cancel", action: () => resolve(null) }, + { label: "Confirm", primary: true, action: () => resolve(document.getElementById("popup-generic-input").value) } + ], + onClose: () => resolve(null) + }); + setTimeout(() => document.getElementById("popup-generic-input")?.focus(), 100); + }); +} +``` + +Replace `closeInputPopup` implementation: +```javascript +function closeInputPopup() { + if (currentInputPopup) { + PopupManager.close(currentInputPopup); + currentInputPopup = null; + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run `npm run tauri dev` (or manually trigger these in the console). +Expected: Loading and input popups use the new aesthetic. + +- [ ] **Step 5: Commit** + +```bash +rm -f test_logic.html +git add ui/main_logic.js +git commit -m "refactor(ui): migrate loading and input popups to PopupManager" +``` + +--- + +### Task 4: Migrate Confirm Popup + +**Files:** +- Modify: `ui/main_logic.js` + +- [ ] **Step 1: Write test script** + +(Manual test within application via developer console: `showPopup("Confirm action?", 0)`) + +- [ ] **Step 2: Run test to verify it fails (legacy DOM)** + +Call `showPopup("Confirm", 0)` in console. Expected: uses legacy `.confirm-popup` structure. + +- [ ] **Step 3: Write minimal implementation** + +In `ui/main_logic.js`, replace the entire `showPopup` function: + +```javascript +let currentConfirmPopup = null; +async function showPopup(message = "Nothing to see here!", type = 0, subtitle = "") { + closeConfirmPopup(); + + let primaryLabel = "Confirm"; + let isDanger = false; + if (type === 2) { primaryLabel = "Delete"; isDanger = true; } // PopupType.DELETE + if (type === 1) { primaryLabel = "Extract"; } // PopupType.EXTRACT + + return new Promise((resolve) => { + let contentHtml = `

${message}

`; + if (subtitle) contentHtml += `

${subtitle}

`; + if (type === 3) { // PopupType.PROMPT + contentHtml += ``; + } + + currentConfirmPopup = PopupManager.open({ + title: "Confirmation", + content: contentHtml, + allowBackdropClose: false, + buttons: [ + { label: "Cancel", action: () => { resolve(type === 3 ? null : false); } }, + { + label: primaryLabel, + primary: !isDanger, + danger: isDanger, + action: () => { + if (type === 3) resolve(document.getElementById("popup-prompt-input").value); + else resolve(true); + } + } + ], + onClose: () => resolve(type === 3 ? null : false) + }); + + if (type === 3) { + setTimeout(() => { + const inp = document.getElementById("popup-prompt-input"); + if(inp) { + inp.focus(); + inp.onkeyup = (e) => { if(e.key === "Enter") { resolve(inp.value); PopupManager.close(currentConfirmPopup); } }; + } + }, 100); + } + }); +} +``` + +Replace `closeConfirmPopup`: +```javascript +function closeConfirmPopup() { + if (currentConfirmPopup) { + PopupManager.close(currentConfirmPopup); + currentConfirmPopup = null; + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Call `showPopup("Test", 0)` in app console. +Expected: It renders as a `.soft-popup` with correct buttons and styling. + +- [ ] **Step 5: Commit** + +```bash +git add ui/main_logic.js +git commit -m "refactor(ui): migrate confirm popup to PopupManager" +``` + +--- + +### Task 5: Migrate Settings UI to PopupManager Layout + +**Files:** +- Modify: `ui/main_logic.js` +- Modify: `ui/style.css` + +- [ ] **Step 1: Manual UI Test Setup** + +Run app. Open Settings. +Expected: Slides in from side or uses `.active` class with legacy flat styling. + +- [ ] **Step 2: Check current toggle logic** + +Currently `$(".settings-ui").addClass("active");` handles display. + +- [ ] **Step 3: Write minimal implementation** + +In `ui/main_logic.js`, find where `$(".settings-ui").addClass("active")` is called (likely in an event listener like `$("#settings-btn").click()`). + +Change it to extract the HTML of `.settings-ui` and wrap it: + +```javascript +// Find the function or click handler that opens settings. For example: +// If it's a dedicated function: +let settingsPopupRef = null; +function toggleSettings() { + if (settingsPopupRef) return; // Already open + + const settingsEl = document.querySelector(".settings-ui"); + // Ensure we display it properly by removing 'display: none' if it had it + settingsEl.style.display = 'block'; + + // Wrap it in the new PopupManager + settingsPopupRef = PopupManager.wrapLegacy(settingsEl, { + allowBackdropClose: true, + allowEscapeClose: true, + onClose: () => { + // Re-hide or clean up if needed + settingsPopupRef = null; + } + }); +} + +// In ui/main_logic.js, search for `$(".settings-ui").addClass("active");` +// Replace it with `toggleSettings();` +``` +*(Note: As the exact caller for Settings isn't fully mapped, adapt the above to replace the specific click handler in `main_logic.js` that adds `.active` to `.settings-ui`)* + +In `ui/style.css`, remove the `.settings-ui.active` styles that handle side-panel sliding: +```css +/* Remove or comment out these if they exist to prevent layout conflicts */ +/* .settings-ui { position: fixed; right: -400px; ... } */ +/* .settings-ui.active { right: 0; } */ +``` +Add to `ui/style.css` to ensure internal cards use the new look: +```css +.settings-control-group { + background: var(--secondaryColor); + border-radius: var(--popup-radius-lg); + padding: 16px; + margin-bottom: 12px; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Open Settings in the app. +Expected: Settings appears centered in a `.soft-popup` envelope with blurred backdrop and Escape-to-close behavior, instead of a slide-out panel. + +- [ ] **Step 5: Commit** + +```bash +git add ui/main_logic.js ui/style.css +git commit -m "refactor(ui): migrate Settings panel to PopupManager soft styling" +``` diff --git a/ui/index.html b/ui/index.html index fe15b064..ad986086 100644 --- a/ui/index.html +++ b/ui/index.html @@ -9,6 +9,7 @@ + diff --git a/ui/main_logic.js b/ui/main_logic.js index d7a40848..5b0b4ca4 100644 --- a/ui/main_logic.js +++ b/ui/main_logic.js @@ -376,6 +376,10 @@ document.addEventListener("mousedown", (e) => { !e.target.classList.contains("input-dialog") && !e.target.classList.contains("confirm-popup") && !e.target.classList.contains("uni-popup") && + !e.target.classList.contains("soft-popup") && + !e.target.closest(".soft-popup") && + !e.target.classList.contains("custom-context-menu") && + !e.target.closest(".custom-context-menu") && !e.target.classList.contains("popup-body") && !e.target.classList.contains("popup-body-content") && !e.target.classList.contains("directory-entry") && @@ -835,7 +839,11 @@ document.onkeydown = async (e) => { if (((e.ctrlKey && Platform != "darwin") || e.metaKey) && e.key == "g") { e.preventDefault(); e.stopPropagation(); - showInputPopup("Input path to jump to"); + const path = await showInputPopup("Input path to jump to"); + if (path) { + await openDirAndSwitch(path); + await listDirectories(); + } } // New folder input prompt when f7 is pressed @@ -1594,33 +1602,31 @@ async function extractItem(item) { } async function showCompressPopup(item) { - IsPopUpOpen = true; let arrCompressItems = ArrSelectedItems; if (ArrSelectedItems.length > 1) { arrCompressItems = ArrSelectedItems; } else { arrCompressItems = [item]; } + let compressFileNames = ""; if (arrCompressItems.length > 1) { for (let i = 0; i < arrCompressItems.length; i++) { compressFileNames += - "
" + - arrCompressItems[i].getAttribute("itemname") + - "
"; + `
${escapeHtml(arrCompressItems[i].getAttribute("itemname"))}
`; } } else { - compressFileNames = item.getAttribute("itemname"); - } - if (compressFileNames != "") { - let popup = document.createElement("div"); - popup.className = "uni-popup compression-popup"; - popup.innerHTML = ` - -
+ compressFileNames = escapeHtml(item.getAttribute("itemname")); + } + + if (compressFileNames === "") return; + + PopupManager.open({ + className: "cd-popup--compression", + title: "Compression options", + icon: "fa-solid fa-compress", + content: ` +

Selected item(s)


@@ -1628,59 +1634,53 @@ async function showCompressPopup(item) {

-