diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0418a3ba..f9c03831 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,7 +35,7 @@ jobs: if: matrix.settings.platform == 'ubuntu-22.04' # This must match the platform value defined above. run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsoup-3.0-dev # webkitgtk 4.0 for Tauri v1 - webkitgtk 4.1 for Tauri v2. - name: install Rust stable diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 147b85a8..b22da124 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -32,7 +32,7 @@ jobs: if: matrix.settings.platform == 'ubuntu-22.04' # This must match the platform value defined above. run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsoup-3.0-dev # webkitgtk 4.0 for Tauri v1 - webkitgtk 4.1 for Tauri v2. - name: install Rust stable diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..b85999e1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,179 @@ +# AI Developer & Agent Onboarding Guide (AGENTS.md) + +Welcome, agent! This document serves as your technical manual and architectural reference for developing, debugging, and extending **CoDriver**. Refer to this guide to align with existing design patterns, technology choices, and structural constraints. + +--- + +## ๐Ÿš€ Repository Identity & Mission + +**CoDriver** is a high-performance, cross-platform desktop file explorer built with **Tauri v2** and **Rust**. +- **No path caching**: Directory exploration is done in real-time, relying on the raw speed of Rust, concurrent disk access (`rayon` & `jwalk`), and CPU power. +- **Cross-platform**: Native feel on Windows, macOS, and Linux. +- **Rich features**: Includes dual-pane layout, miller columns, quick file preview (images, PDFs, video, code), drag-and-drop, multi-format archive compression/extraction, SSHFS mount integration, and Gemini AI-powered features (like image background removal). + +--- + +## ๐Ÿ“‚ Directory Structure + +Here is a map of the repository's major files and folders: + +``` +CoDriver/ +โ”œโ”€โ”€ .vscode/ # Workspace settings for VS Code +โ”œโ”€โ”€ .zed/ # Workspace settings for Zed +โ”œโ”€โ”€ arch/ # Architecture-specific scripts/files +โ”œโ”€โ”€ docs/ # Specifications, plans, release notes, and documentation +โ”‚ โ”œโ”€โ”€ design/ # UI/UX mockups and feature specs +โ”‚ โ””โ”€โ”€ superpowers/ # Active sprint specifications and plans +โ”œโ”€โ”€ memories/ # AI-agent working state and project context +โ”‚ โ””โ”€โ”€ project_context.md # High-level summary of active sprints and tech stacks +โ”œโ”€โ”€ snap/ # Snap packaging configurations (Linux) +โ”œโ”€โ”€ src-tauri/ # Rust backend code (Tauri Core) +โ”‚ โ”œโ”€โ”€ src/ +โ”‚ โ”‚ โ”œโ”€โ”€ applications.rs # Desktop application integration helpers +โ”‚ โ”‚ โ”œโ”€โ”€ main.rs # Entry point, state, and Tauri command declarations (heavy) +โ”‚ โ”‚ โ”œโ”€โ”€ utils.rs # Heavy duty FS operations, watchers, compression +โ”‚ โ”‚ โ””โ”€โ”€ window_tauri_ext.rs # Platform window custom styling integrations +โ”‚ โ”œโ”€โ”€ Cargo.toml # Rust crate dependency map +โ”‚ โ””โ”€โ”€ tauri.conf.json # Tauri configuration (window sizes, allowlists, assets) +โ”œโ”€โ”€ ui/ # Frontend assets loaded by Tauri (Vanilla JS + jQuery) +โ”‚ โ”œโ”€โ”€ index.html # Main app shell & modal containers +โ”‚ โ”œโ”€โ”€ main_logic.js # Core UI state manager and operation orchestration (heavy) +โ”‚ โ”œโ”€โ”€ events.js # Global listeners for Tauri-emitted backend events +โ”‚ โ”œโ”€โ”€ contextmenu.js # Custom right-click menu management +โ”‚ โ”œโ”€โ”€ utils.js # Shared DOM and IPC helpers +โ”‚ โ”œโ”€โ”€ models.js # Core JS model definitions (e.g. ActiveAction, Popups) +โ”‚ โ””โ”€โ”€ style.css # Premium glassmorphism design system & styles +โ””โ”€โ”€ README.md # Public orientation document +``` + +--- + +## ๐Ÿ’ป Tech Stack & Architecture + +### 1. The Frontend (ui/) +- **Core UI Structure**: Single-page application built on pure HTML5 and vanilla CSS. +- **DOM Orchestration**: Uses **jQuery** (`$`) for event handling, animations, and DOM manipulation. Avoid introducing complex front-end frameworks (React, Vue, etc.) as the project structure relies on global namespace orchestration. +- **Visual Design**: Sleek glassmorphism theme defined in `ui/style.css` leveraging modern CSS variables (e.g., `--primaryColor`, `--textColor`, `--glass-blur`). Features responsive grid/list/miller layouts. +- **Third-Party Libraries**: + - `DragSelect` (`ds.min.js`) for click-and-drag file selections. + - `Font Awesome` (`font-awesome/`) for clean iconography. + +### 2. The Backend (src-tauri/) +- **Core**: **Tauri v2** (Tauri v2 has been adopted for the codebase). +- **Disk Walking**: **jwalk** and **walkdir** for highly parallel directory traversals. +- **Concurrency**: **rayon** for multi-threaded operation mapping. +- **FS Watching**: **notify** crate registers platform-specific filesystem watchers to push live updates to the UI. +- **Archive Integrations**: `sevenz-rust`, `zip`, `tar`, `flate2`, `zstd`, `brotlic`, `density-rs`. + +--- + +## ๐Ÿ”„ IPC & Tauri Command Integration + +Communication between the frontend and the backend is done via Tauri's IPC bridge. + +```mermaid +sequenceDiagram + participant UI as ui/main_logic.js + participant IPC as Tauri Invoke Bridge + participant Rust as src-tauri/src/main.rs + participant FS as Local Filesystem + + UI->>IPC: invoke("open_dir", { path: "/Users/..." }) + IPC->>Rust: open_dir(path) + Rust->>FS: Read directory entries + FS-->>Rust: Entries vector + Rust-->>IPC: FDir Array + IPC-->>UI: Array of JS objects +``` + +### IPC Convention +1. **Rust Command Declaration** (`src-tauri/src/main.rs`): + ```rust + #[tauri::command] + fn your_command_name(custom_argument: String) -> Result { + // Backend logic + Ok("Success".into()) + } + ``` +2. **Registration**: Ensure your command is registered inside the `.invoke_handler` list in `main.rs`: + ```rust + .invoke_handler(tauri::generate_handler![ + list_dirs, + // ... + your_command_name + ]) + ``` +3. **JS Invocation** (`ui/main_logic.js`): + Tauri bridges snake_case Rust arguments to camelCase JS properties automatically. + ```javascript + const result = await invoke("your_command_name", { customArgument: "value" }); + ``` + +--- + +## ๐ŸŽจ UI & Styling Guidelines + +To maintain visual excellence, follow these rules: +1. **Glassmorphism Theme**: Always utilize the design system's variables from `style.css` rather than static colors. + ```css + background: var(--glass-bg); + border: var(--glass-border); + backdrop-filter: var(--glass-blur); + ``` +2. **Unified Modals & Popups (`.props-card`)**: Refrain from using crude custom layouts or old `.uni-popup` overlays. All interactive modal dialogs (such as Properties, Compress, Extract, Delete Confirm, and FTP Connection) must inherit from the `.props-card` design pattern: + - **Hero Header (`.props-card__hero`)**: Houses a `.props-card__thumb` holding a dedicated Font Awesome icon (or a circular progress loader styled with `.preloader-small-invert` for ongoing processes), and a `.props-card__heading` containing the `.props-card__name` (title) and `.props-card__meta` (subtext or `.props-card__chip` labels). + - **Form Grid Items (`.props-card__list`)**: Wrapped in a `
` list using `
` grid containers (`88px 1fr` layout split). Labels (`
`) should feature small, clean icons. Value fields (`
`) house high-contrast text inputs, number fields, or dropdown select boxes styled with the `.props-card__input` class. + - **Form Layout**: Prefer simple vertical stacking for multiple inputs over dynamic horizontal row division to avoid alignment splits and wrap breaks. + - **Structured Footer (`.props-card__footer`)**: Placed at the bottom holding primary and secondary buttons styled with `.props-card__btn` and `.props-card__btn--primary`. + - **Display Activation (JS)**: When showing the modal, always invoke `.style.display = "flex"` (never `"block"`) to allow the card's native flexbox and stretch behaviors to function. +3. **Prevent Placeholder Assets**: When displaying visual helpers, do not use dead layout placeholders. Generate or use proper icon sheets and local resources found in `ui/resources/` or `ui/font-awesome`. +4. **Asynchronous Previews & Circular Loaders**: When displaying large or heavy preview elements (such as PDF files) in modals, avoid setting the resource source immediately inside the HTML template to prevent the UI from freezing. Instead: + - Immediately display the modal container with a centered circular progress loader (using the style of the indicators from the action items, e.g. `.preloader-invert` or `.preloader-small-invert`). + - Load the resource asynchronously in the background by dynamically setting the target element's `src` attribute. + - Listen to the `"load"` event of the element, and once fired, hide the loading indicator, make the preview visible, and transition the background properties (e.g. to a solid white background for high PDF legibility). + +--- + +## ๐Ÿ› ๏ธ Key Developer Workflows & Recipes + +### 1. Working with Paths +> [!CAUTION] +> **Path Separator Trap**: Paths are highly platform-dependent. Always normalize paths by replacing double-backslashes `\\` with forward slashes `/` where appropriate on the frontend, and verify that your path adjustments are Windows-safe. + +### 2. State & Focus Flags +When implementing new keyboard shortcuts or popups, protect the global UI namespace by checking the state flags in `main_logic.js`: +- `IsPopUpOpen`: Set to `true` when a modal/popup is visible. Prevents general explorer keyboard events (like navigating or deleting) from triggering. +- `IsInputFocused`: Set to `true` when an `` is active. Prevents search shortcuts or navigation commands from intercepting typing. +- `IsDisableShortcuts`: Use to globally silence key interception. + +### 3. File Operations & Progress Bars +File operations are handled asynchronously to keep the UI smooth: +- Selecting items buffers them in `ArrSelectedItems` or `ArrCopyItems` (for copy/cut). +- Invoking `arr_copy_paste` runs chunked operations in the Rust backend. +- Rust emits events like `"update-progress-bar"` and `"finish-progress-bar"` which are picked up in `ui/events.js` to update the `.active-actions-container` layout. +- Dismissing the detailed progress modal via **Run in Background** tracks progress silently. Users can click on a progress action item within the active actions popup to invoke `reopenProgressModal()`, which resets the dismissal state, closes the active actions popup, and opens the detailed progress modal populated with live data. + +### 4. Running a Local Build +Ensure you have the tauri-cli installed: +```bash +cargo install tauri-cli +``` +Run the development environment locally: +```bash +cargo tauri dev +``` + +--- + +## ๐Ÿค– AI Guidelines & Prompt Engineering + +When developing or debugging this repository, you should prioritize: +1. **Vanilla Style Preservation**: Do not attempt to refactor the jQuery layout into modern JS frameworks (React, Vue, Svelte) unless explicitly requested. +2. **Keep Calculations in Rust**: Heavy calculations, recursive walks, filtering, and heavy text parsing should always reside on the Rust backend (`utils.rs` or `main.rs`) to maintain high desktop performance. +3. **Graceful IPC Failures**: Always wrap Tauri commands in JS `try/catch` and display human-readable notifications using the global toast notification functions: + ```javascript + showToast("Action failed: " + error, ToastType.ERROR); + ``` +4. **Respect AI Provider Selection**: Do not hardcode a specific AI provider (such as Gemini or OpenAI). Always check the active `ai_provider` configuration (e.g. the global `AiProvider` on the frontend, and the settings map in `app_config.json` on the backend). AI commands must query the selected provider and model to ensure user settings and keychain API keys are fully respected. + 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/disk-sidebar-fixes/decisions.md b/docs/planning/disk-sidebar-fixes/decisions.md new file mode 100644 index 00000000..4d03a27e --- /dev/null +++ b/docs/planning/disk-sidebar-fixes/decisions.md @@ -0,0 +1,17 @@ +# Decisions: Disk Sidebar Display and Usage Refresh Fixes + +## D1: Treat quote cleanup as display-only +**Decision:** Strip only one matching outer quote pair from disk display labels. Preserve raw values for paths, dataset attributes, and backend calls. +**Rationale:** Prior fix intentionally preserved raw disk path/name for behavior. The current bug is visual; path mutation risks breaking navigation/unmount. + +## D2: Use event-driven/debounced refresh, not polling +**Decision:** Refresh disk usage after operation completion and coalesce calls with a short debounce. +**Rationale:** Disk usage should become current after mutations, but continuous polling wastes work and can cause UI churn during long operations. + +## D3: Prefer updating existing disk row DOM +**Decision:** Update `.disk-nav-usage`, `.disk-nav-progress-fill`, and `title` in place from fresh `list_disks` data. Fall back to `insertSiteNavButtons()` only if row mapping is unreliable. +**Rationale:** In-place update avoids flicker, preserves collapse state/selection better, and minimizes re-render cost. + +## D4: Preserve safe DOM construction for disk rows +**Decision:** Do not reintroduce string-built disk rows or unsafe `innerHTML` for mount-provided values. +**Rationale:** Recent work added safe DOM creation and progress bars; this fix must not regress XSS/path safety. diff --git a/docs/planning/disk-sidebar-fixes/handoffs.md b/docs/planning/disk-sidebar-fixes/handoffs.md new file mode 100644 index 00000000..46c5b65b --- /dev/null +++ b/docs/planning/disk-sidebar-fixes/handoffs.md @@ -0,0 +1,41 @@ +# Handoffs: Disk Sidebar Display and Usage Refresh Fixes + +## reverse-engineer +- Inspect `ui/events.js` `fs-mount-changed` and `ui/main_logic.js` `addNewMount`. +- Confirm all places disk sidebar rows are created or rebuilt. +- Confirm file operation completion points likely to affect disk capacity. +- Expected output: concise map of raw mount path/name flow and mutation refresh touchpoints. + +## ui-ux-designer +- Define display behavior for quoted disk names: + - Sidebar name should strip one matching outer quote pair. + - Tooltip should match display name. + - Internal quotes should remain visible. +- Confirm whether disk dropdown labels should share same cleanup. +- Expected output: final display rule and any edge cases. + +## software-engineer +- Implement display-only cleanup in `ui/main_logic.js`; do not mutate `mount.path`, `payload.paths`, `itempath`, or backend command args. +- Prefer helper names such as `displayDiskName`, `calculateDiskUsedPercentage`, `refreshDiskSidebarUsage`, `scheduleDiskUsageRefresh`. +- Update existing disk row DOM using `textContent` and `style.width`; avoid adding unsafe disk-row `innerHTML`. +- Wire debounced refresh after successful copy/move/delete/create/rename/compress/extract and optional `finish-progress-bar` event. +- Acceptance criteria: + - Event-added `"Data"` displays as `Data`. + - Raw path behavior unchanged. + - Sidebar usage updates after file operations without continuous polling. + +## code-reviewer +- Focus areas: + - Display cleanup applied only to names/labels, never paths. + - No broad `.replaceAll('"', '')` on paths or mount objects. + - No unsafe `innerHTML` reintroduced for disk row data. + - Refresh helper is debounced and does not run on every progress tick. + - Full sidebar rebuild, if used, preserves collapse/selection enough for current UX. +- Expected output: findings in `docs/review/disk-sidebar-fixes/`. + +## documentation-writer +- Document two user-visible fixes: + - Event-added drive names no longer show wrapping quotes. + - Disk usage indicators refresh after file operations. +- Mention no migration or breaking change. +- Expected output: release notes and commit message under `docs/disk-sidebar-fixes/` or release-note location chosen by Navigator. diff --git a/docs/planning/disk-sidebar-fixes/implementation-tracker.md b/docs/planning/disk-sidebar-fixes/implementation-tracker.md new file mode 100644 index 00000000..2dedb67a --- /dev/null +++ b/docs/planning/disk-sidebar-fixes/implementation-tracker.md @@ -0,0 +1,25 @@ +# Implementation Tracker: Disk Sidebar Display and Usage Refresh Fixes + +## Status Summary +**Overall Status:** Planned +**Current Phase:** Phase 1: Context and Architecture +**Last Updated:** 2026-05-18 + +## Task Table + +| ID | Task | Owner | Status | Depends On | Evidence | Next Step | +|----|------|-------|--------|------------|----------|-----------| +| T1 | Confirm disk sidebar and mount event flow | reverse-engineer | Done | None | `ui/events.js:87`, `ui/main_logic.js:4961`, `ui/main_logic.js:5467` | Navigator approve plan | +| T2 | Define display-only quote cleanup behavior | ui-ux-designer | Planned | T1 | `docs/planning/disk-sidebar-fixes/plan.md` | Confirm sidebar/dropdown label scope | +| T3 | Implement display helper and sidebar label usage | software-engineer | Planned | T2 | TBD | Add helper without mutating paths | +| T4 | Implement debounced disk usage refresh helper | software-engineer | Planned | T1 | TBD | Update existing row DOM or rebuild sidebar safely | +| T5 | Wire refresh after successful file operations | software-engineer | Planned | T4 | TBD | Add calls at mutation completion points | +| T6 | Add optional finish-progress-bar safety refresh | software-engineer | Planned | T4 | TBD | Debounced call in `ui/events.js` | +| T7 | Review DOM safety, raw path preservation, refresh frequency | code-reviewer | Planned | T3,T4,T5,T6 | TBD | Review diff against plan | +| T8 | Write release notes and commit message | documentation-writer | Planned | T7 | TBD | Summarize user-visible fixes | + +## Blockers +- Need Navigator decision on whether disk dropdown labels are included in display-only quote cleanup. + +## Drift Log +- None. diff --git a/docs/planning/disk-sidebar-fixes/plan.md b/docs/planning/disk-sidebar-fixes/plan.md new file mode 100644 index 00000000..b6a3cc92 --- /dev/null +++ b/docs/planning/disk-sidebar-fixes/plan.md @@ -0,0 +1,122 @@ +# Plan: Disk Sidebar Display and Usage Refresh Fixes + +## Summary +Fix two related sidebar disk issues without changing filesystem behavior: remove only presentation-only wrapping quotes from event-added disk names, and refresh sidebar disk usage after file operations that can change capacity usage. + +## Goals +- Display event-added mount names like `Data` instead of `"Data"`. +- Preserve raw disk paths/names used for navigation, unmount, selection, and backend calls. +- Refresh sidebar disk percentage/progress after copy, move, delete, create, rename, compress, extract, and similar filesystem mutations. +- Keep recent safe DOM creation for disk rows intact. + +## Non-Goals +- Do not alter backend disk discovery, mount paths, or unmount path handling. +- Do not add continuous polling for disk usage. +- Do not rewrite existing sidebar/dropdown architecture beyond small helper/touchpoint changes. +- Do not convert unrelated `innerHTML` usage outside this fix. + +## Assumptions +- `list_disks` and `get_disk_info` return current `capacity` and `avail` values quickly enough for user-triggered refreshes. +- Window event-added mounts enter through `ui/events.js` `fs-mount-changed` and `ui/main_logic.js` `addNewMount`. +- Disk sidebar rows are created by `createSidebarDiskButton(mount, pathOverride = "")`. +- Existing operation completion points already await backend filesystem commands, so a post-operation disk refresh can be scheduled there. + +## Open Questions +- Should disk dropdown labels also strip display-only wrapping quotes, or only sidebar disk rows? Recommended: use same display helper for dropdown labels for consistency, while leaving option values raw. +- Which operations should trigger sidebar usage refresh in v1? Recommended: every completed mutation path already refreshing views, plus progress finish event as safety. +- Should failed operations refresh usage? Recommended: refresh only after successful mutation, except `finish-progress-bar` can schedule a best-effort debounced refresh. + +## Dependencies +- Architecture context: static Tauri UI in `ui/`, global JS in `ui/main_logic.js`, Tauri event listeners in `ui/events.js`. +- Existing backend commands: `list_disks`, `get_disk_info`, file mutation commands. +- Existing disk UI styles in `ui/style.css`; no CSS change expected unless updated progress state needs minor styling. + +## Likely Files and Functions +- `ui/main_logic.js` + - `createSidebarDiskButton(mount, pathOverride = "")`: compute sanitized display name only; keep `path` raw. + - `addNewMount(payload)`: event-added mount path/name flow; append disk row using raw `pathOverride`. + - `insertSiteNavButtons()`: full sidebar rebuild; candidate for reuse by refresh helper if acceptable. + - `setDiskDropdowns()`: optional display-label sanitization for dropdown options; option `value` must stay raw. + - File operation completion points: `deleteItems`, `runResolvedCopyMove`, `pasteItem`, `compressItem`, `extractItem`, `createFolder`, `createFile`, `renameElement`, `renameElementsWithFormat`, drag/drop cleanup paths invoking `arr_delete_items`. + - New helper candidate: `displayDiskName(rawName)`, `scheduleDiskUsageRefresh()`, `refreshDiskSidebarUsage()`. +- `ui/events.js` + - `fs-mount-changed`: existing add/remove mount event flow. + - `finish-progress-bar`: optional completion-triggered debounced sidebar usage refresh. +- `ui/style.css` + - No expected change; verify progress classes `.disk-nav-usage`, `.disk-nav-progress-fill` already support updates. + +## Execution Phases + +### Phase 1: Context and Architecture +**Owner:** reverse-engineer +**Output:** Confirm disk sidebar data flow and operation completion touchpoints. +**Acceptance Criteria:** `createSidebarDiskButton`, `addNewMount`, refresh helpers, and mutation functions are mapped with raw/display data boundaries. + +### Phase 2: Design +**Owner:** ui-ux-designer +**Output:** Minimal display behavior spec. +**Acceptance Criteria:** Sidebar name, tooltip, and optional disk dropdown labels display unquoted names; progress update is visually unchanged except current percentage/bar. + +### Phase 3: Implementation +**Owner:** software-engineer +**Output:** JS-only changes in `ui/main_logic.js` and possibly `ui/events.js`. +**Acceptance Criteria:** +- Names wrapped with one matching quote pair display without that pair. +- Raw `mount.path`, `pathOverride`, DOM `itempath`, `dataset.itempath`, navigation, and unmount calls remain unchanged. +- Disk usage text and progress fill update after successful filesystem mutations. +- Refresh is debounced/event-driven; no interval polling is introduced. +- Disk rows continue using `createElement`, `textContent`, attributes/dataset, and no unsafe `innerHTML` for disk row data. + +### Phase 4: Review +**Owner:** code-reviewer +**Output:** Findings against raw/display separation, refresh frequency, and DOM safety. +**Acceptance Criteria:** No critical/major issues; no path mutation or unsafe disk row HTML regression. + +### Phase 5: Documentation +**Owner:** documentation-writer +**Output:** Short release note and commit message. +**Acceptance Criteria:** Notes mention display-only quote cleanup and sidebar usage refresh after operations. + +## Suggested Implementation Steps +1. Add a small helper in `ui/main_logic.js`, near disk helpers: + - `displayDiskName(rawName)` returns `/` for empty names. + - Strip only one matching wrapping pair of `"..."` or `'...'` from the display value. + - Do not call it on paths. +2. Update `createSidebarDiskButton`: + - Keep `const path = pathOverride || mount.path || ""` as raw behavior path. + - Keep raw mount object for unmount logic. + - Use `const displayName = displayDiskName(mount.name)` for `title` and `.disk-nav-name.textContent` only. +3. Optionally update `setDiskDropdowns` labels: + - Use `displayDiskName(disks[i].name)` for `