From 1bcbe64106ff4229de1af2a94babe4c444e900e5 Mon Sep 17 00:00:00 2001 From: emy Date: Sun, 22 Feb 2026 20:05:56 +0100 Subject: [PATCH 1/7] =?UTF-8?q?refactor(ui):=20remove=20compact=20mode=20p?= =?UTF-8?q?aths=20and=20legacy=20styles=20=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 14 +- src/components/explorer/FileGrid.tsx | 4 - src/components/explorer/FileList.tsx | 5 - src/components/explorer/FileView.tsx | 4 - .../explorer/fileGrid/useGridSelection.ts | 6 +- .../explorer/fileGrid/useGridVirtual.ts | 9 +- .../explorer/fileList/useListLayout.ts | 13 +- .../explorer/fileList/useListSelection.ts | 6 +- .../explorer/fileList/useListVirtual.ts | 9 +- src/components/explorer/fileView/constants.ts | 2 +- src/components/overlay/SettingsOverlay.tsx | 2 - .../settings/SettingsGeneralSection.tsx | 18 --- .../settings/SettingsViewSection.tsx | 130 ------------------ src/constants/settings.ts | 2 +- src/hooks/ui/app/useAppAppearance.ts | 51 ++++--- src/hooks/ui/app/useAppEffects.ts | 3 - src/hooks/ui/app/useAppFileViewProps.tsx | 1 - src/modules/settings.defaults.ts | 1 - src/modules/settings.normalize.ts | 4 - src/modules/settings.store.ts | 1 - src/modules/settings.types.ts | 1 - src/styles/base/_scrollbars.scss | 5 - src/styles/components/_file-view.scss | 90 +++--------- src/styles/components/_sidebar.scss | 6 +- src/styles/layout/_layout.scss | 10 +- src/styles/tokens/_theme.scss | 21 +-- 26 files changed, 74 insertions(+), 344 deletions(-) delete mode 100644 src/components/settings/SettingsViewSection.tsx diff --git a/src/App.tsx b/src/App.tsx index a08925e..639fc8c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -435,13 +435,12 @@ const App = () => { }, settings: { confirmClose: settings.confirmClose, - accentTheme: settings.accentTheme, - ambientBackground: settings.ambientBackground, - blurOverlays: settings.blurOverlays, - gridRounded: settings.gridRounded, - gridCentered: settings.gridCentered, - compactMode: settings.compactMode, - }, + accentTheme: settings.accentTheme, + ambientBackground: settings.ambientBackground, + blurOverlays: settings.blurOverlays, + gridRounded: settings.gridRounded, + gridCentered: settings.gridCentered, + }, view: { activeTabId, activeTabPath, @@ -698,7 +697,6 @@ const App = () => { scrollRestoreTop, scrollRequest, smoothScroll: settings.smoothScroll, - compactMode: settings.compactMode, sortState, canGoUp, }, diff --git a/src/components/explorer/FileGrid.tsx b/src/components/explorer/FileGrid.tsx index 3ab6276..5154a2d 100644 --- a/src/components/explorer/FileGrid.tsx +++ b/src/components/explorer/FileGrid.tsx @@ -35,7 +35,6 @@ type FileGridProps = { scrollRestoreTop: number; scrollRequest?: { index: number; nonce: number } | null; smoothScroll: boolean; - compactMode: boolean; selectedPaths: Set; onSetSelection: (paths: string[], anchor?: string) => void; onOpenDir: (path: string) => void; @@ -107,7 +106,6 @@ const FileGrid = ({ scrollRestoreTop, scrollRequest, smoothScroll, - compactMode, selectedPaths, onSetSelection, onOpenDir, @@ -225,7 +223,6 @@ const FileGrid = ({ columnCount, rowCount, rowHeight, - compactMode, viewItems, }); @@ -269,7 +266,6 @@ const FileGrid = ({ columnWidth, rowHeight, gridGap, - compactMode, onSetSelection, onClearSelection, onStartDragOut, diff --git a/src/components/explorer/FileList.tsx b/src/components/explorer/FileList.tsx index a372024..1a41829 100644 --- a/src/components/explorer/FileList.tsx +++ b/src/components/explorer/FileList.tsx @@ -34,7 +34,6 @@ type FileListProps = { scrollRestoreTop: number; scrollRequest?: { index: number; nonce: number } | null; smoothScroll: boolean; - compactMode: boolean; sortState: SortState; onSortChange: (next: SortState) => void; categoryTinting: boolean; @@ -88,7 +87,6 @@ const FileList = ({ scrollRestoreTop, scrollRequest, smoothScroll, - compactMode, sortState, onSortChange, categoryTinting, @@ -148,7 +146,6 @@ const FileList = ({ }, [indexMap, items]); const { listRef, itemHeight, rowHeight, listVars } = useListLayout({ - compactMode, smoothScroll, scrollRestoreKey, scrollRestoreTop, @@ -159,7 +156,6 @@ const FileList = ({ listRef, viewKey, itemHeight, - compactMode, rows, }); @@ -185,7 +181,6 @@ const FileList = ({ selectionItems, itemHeight, rowHeight, - compactMode, onSetSelection, onClearSelection, onStartDragOut, diff --git a/src/components/explorer/FileView.tsx b/src/components/explorer/FileView.tsx index 4847c75..3018492 100644 --- a/src/components/explorer/FileView.tsx +++ b/src/components/explorer/FileView.tsx @@ -51,7 +51,6 @@ type FileViewProps = { scrollRestoreTop: number; scrollRequest?: { index: number; nonce: number } | null; smoothScroll: boolean; - compactMode: boolean; sortState: SortState; onSortChange: (next: SortState) => void; selectedPaths: Set; @@ -154,7 +153,6 @@ export const FileView = ({ onEntryPreviewPress, onEntryPreviewRelease, smoothScroll, - compactMode, sortState, onSortChange, onContextMenuDown, @@ -206,7 +204,6 @@ export const FileView = ({ {...viewProps} currentPath={currentPath} smoothScroll={smoothScroll} - compactMode={compactMode} canGoUp={canGoUp} onGoUp={onGoUp} thumbnailsEnabled={thumbnailsEnabled} @@ -251,7 +248,6 @@ export const FileView = ({ {...viewProps} currentPath={currentPath} smoothScroll={smoothScroll} - compactMode={compactMode} sortState={sortState} onSortChange={onSortChange} categoryTinting={categoryTinting} diff --git a/src/components/explorer/fileGrid/useGridSelection.ts b/src/components/explorer/fileGrid/useGridSelection.ts index 58fd9b8..fdc2ec4 100644 --- a/src/components/explorer/fileGrid/useGridSelection.ts +++ b/src/components/explorer/fileGrid/useGridSelection.ts @@ -2,7 +2,7 @@ import type { RefObject } from "react"; import { useEntryDragOut, useSelectionDrag } from "@/hooks"; import type { DropTarget } from "@/lib"; -import { COMPACT_VIEW_INSET } from "../fileView/constants"; +import { VIEW_INSET } from "../fileView/constants"; const noop = () => {}; @@ -14,7 +14,6 @@ type UseGridSelectionOptions = { columnWidth: number; rowHeight: number; gridGap: number; - compactMode: boolean; onSetSelection: (paths: string[], anchor?: string) => void; onClearSelection: () => void; onStartDragOut?: (paths: string[]) => void; @@ -31,7 +30,6 @@ export const useGridSelection = ({ columnWidth, rowHeight, gridGap, - compactMode, onSetSelection, onClearSelection, onStartDragOut, @@ -51,7 +49,7 @@ export const useGridSelection = ({ columnWidth, rowHeight, gap: gridGap, - insetTop: compactMode ? COMPACT_VIEW_INSET : 0, + insetTop: VIEW_INSET, }, }); diff --git a/src/components/explorer/fileGrid/useGridVirtual.ts b/src/components/explorer/fileGrid/useGridVirtual.ts index 3f16dd1..3cd37d0 100644 --- a/src/components/explorer/fileGrid/useGridVirtual.ts +++ b/src/components/explorer/fileGrid/useGridVirtual.ts @@ -3,7 +3,7 @@ import type { RefObject } from "react"; import { useEffect, useMemo, useRef, useState } from "react"; import { useDynamicOverscan, useVirtualRange } from "@/hooks"; import type { EntryItem } from "@/lib"; -import { COMPACT_VIEW_INSET } from "../fileView/constants"; +import { VIEW_INSET } from "../fileView/constants"; const GRID_OVERSCAN = 3; const GRID_OVERSCAN_MIN = 1; @@ -19,7 +19,6 @@ type UseGridVirtualOptions = { columnCount: number; rowCount: number; rowHeight: number; - compactMode: boolean; viewItems: EntryItem[]; }; @@ -36,7 +35,6 @@ export const useGridVirtual = ({ columnCount, rowCount, rowHeight, - compactMode, viewItems, }: UseGridVirtualOptions): GridVirtualState => { const baseOverscan = useDynamicOverscan({ @@ -142,14 +140,13 @@ export const useGridVirtual = ({ GRID_OVERSCAN_BURST_MAX, Math.max(GRID_OVERSCAN_MIN, baseOverscan + wheelBurstOverscan), ); - const viewInset = compactMode ? COMPACT_VIEW_INSET : 0; const virtual = useVirtualRange( viewportRef, rowCount, rowHeight, overscan, - viewInset, - viewInset, + VIEW_INSET, + VIEW_INSET, ); const startIndex = virtual.startIndex * columnCount; const endIndex = Math.min(viewItems.length, virtual.endIndex * columnCount); diff --git a/src/components/explorer/fileList/useListLayout.ts b/src/components/explorer/fileList/useListLayout.ts index da7d303..3460c4d 100644 --- a/src/components/explorer/fileList/useListLayout.ts +++ b/src/components/explorer/fileList/useListLayout.ts @@ -3,13 +3,10 @@ import type { CSSProperties, RefObject } from "react"; import { useMemo, useRef } from "react"; import { useScrollRestore, useWheelSnap } from "@/hooks"; -const ROW_HEIGHT = 48; -const ROW_GAP = 8; -const COMPACT_ROW_HEIGHT = 36; -const COMPACT_ROW_GAP = 6; +const ROW_HEIGHT = 36; +const ROW_GAP = 6; type UseListLayoutOptions = { - compactMode: boolean; smoothScroll: boolean; scrollRestoreKey: string; scrollRestoreTop: number; @@ -25,16 +22,14 @@ type ListLayoutState = { }; export const useListLayout = ({ - compactMode, smoothScroll, scrollRestoreKey, scrollRestoreTop, loading, }: UseListLayoutOptions): ListLayoutState => { const listRef = useRef(null); - // Match virtualization height with compact spacing when enabled. - const rowHeight = compactMode ? COMPACT_ROW_HEIGHT : ROW_HEIGHT; - const rowGap = compactMode ? COMPACT_ROW_GAP : ROW_GAP; + const rowHeight = ROW_HEIGHT; + const rowGap = ROW_GAP; const itemHeight = rowHeight + rowGap; const listVars = useMemo( diff --git a/src/components/explorer/fileList/useListSelection.ts b/src/components/explorer/fileList/useListSelection.ts index 6350582..bc5e5e9 100644 --- a/src/components/explorer/fileList/useListSelection.ts +++ b/src/components/explorer/fileList/useListSelection.ts @@ -2,7 +2,7 @@ import type { RefObject } from "react"; import { useEntryDragOut, useSelectionDrag } from "@/hooks"; import type { DropTarget } from "@/lib"; -import { COMPACT_VIEW_INSET } from "../fileView/constants"; +import { VIEW_INSET } from "../fileView/constants"; const noop = () => {}; @@ -12,7 +12,6 @@ type UseListSelectionOptions = { selectionItems: { path: string; selectable: boolean }[]; itemHeight: number; rowHeight: number; - compactMode: boolean; onSetSelection: (paths: string[], anchor?: string) => void; onClearSelection: () => void; onStartDragOut?: (paths: string[]) => void; @@ -27,7 +26,6 @@ export const useListSelection = ({ selectionItems, itemHeight, rowHeight, - compactMode, onSetSelection, onClearSelection, onStartDragOut, @@ -45,7 +43,7 @@ export const useListSelection = ({ items: selectionItems, itemHeight, rowHeight, - insetTop: compactMode ? COMPACT_VIEW_INSET : 0, + insetTop: VIEW_INSET, }, }); diff --git a/src/components/explorer/fileList/useListVirtual.ts b/src/components/explorer/fileList/useListVirtual.ts index 771d3c7..a2eb0dc 100644 --- a/src/components/explorer/fileList/useListVirtual.ts +++ b/src/components/explorer/fileList/useListVirtual.ts @@ -3,7 +3,7 @@ import type { RefObject } from "react"; import { useMemo } from "react"; import { useDynamicOverscan, useVirtualRange } from "@/hooks"; import type { EntryItem } from "@/lib"; -import { COMPACT_VIEW_INSET } from "../fileView/constants"; +import { VIEW_INSET } from "../fileView/constants"; const OVERSCAN = 10; const OVERSCAN_MIN = 2; @@ -13,7 +13,6 @@ type UseListVirtualOptions = { listRef: RefObject; viewKey: string; itemHeight: number; - compactMode: boolean; rows: EntryItem[]; }; @@ -26,7 +25,6 @@ export const useListVirtual = ({ listRef, viewKey, itemHeight, - compactMode, rows, }: UseListVirtualOptions): ListVirtualState => { const overscan = useDynamicOverscan({ @@ -35,14 +33,13 @@ export const useListVirtual = ({ min: OVERSCAN_MIN, warmupMs: OVERSCAN_WARMUP_MS, }); - const viewInset = compactMode ? COMPACT_VIEW_INSET : 0; const virtual = useVirtualRange( listRef, rows.length, itemHeight, overscan, - viewInset, - viewInset, + VIEW_INSET, + VIEW_INSET, ); // Memoize the visible slice so selection drags don't rebuild row metadata. const visibleRows = useMemo( diff --git a/src/components/explorer/fileView/constants.ts b/src/components/explorer/fileView/constants.ts index 631bfaf..1b8e36b 100644 --- a/src/components/explorer/fileView/constants.ts +++ b/src/components/explorer/fileView/constants.ts @@ -1,2 +1,2 @@ // Shared layout constants for list/grid virtualization and selection. -export const COMPACT_VIEW_INSET = 10; +export const VIEW_INSET = 10; diff --git a/src/components/overlay/SettingsOverlay.tsx b/src/components/overlay/SettingsOverlay.tsx index f396650..2bcbf60 100644 --- a/src/components/overlay/SettingsOverlay.tsx +++ b/src/components/overlay/SettingsOverlay.tsx @@ -43,7 +43,6 @@ export const SettingsOverlay = ({ fixedWidthTabs, smoothScroll, smartTabJump, - compactMode, accentTheme, categoryTinting, showParentEntry, @@ -253,7 +252,6 @@ export const SettingsOverlay = ({ defaultViewMode={defaultViewMode} smoothScroll={smoothScroll} gridCentered={gridCentered} - compactMode={compactMode} showParentEntry={showParentEntry} confirmDelete={confirmDelete} confirmClose={confirmClose} diff --git a/src/components/settings/SettingsGeneralSection.tsx b/src/components/settings/SettingsGeneralSection.tsx index bbe0335..895c783 100644 --- a/src/components/settings/SettingsGeneralSection.tsx +++ b/src/components/settings/SettingsGeneralSection.tsx @@ -8,7 +8,6 @@ type SettingsGeneralSectionProps = { defaultViewMode: ViewMode; smoothScroll: boolean; gridCentered: boolean; - compactMode: boolean; showParentEntry: boolean; confirmDelete: boolean; confirmClose: boolean; @@ -20,7 +19,6 @@ export const SettingsGeneralSection = ({ defaultViewMode, smoothScroll, gridCentered, - compactMode, showParentEntry, confirmDelete, confirmClose, @@ -81,22 +79,6 @@ export const SettingsGeneralSection = ({ -
-
-
Compact mode
-
- Edge-to-edge layout with a flush sidebar and a simplified content frame. -
-
- -
Parent directory entry
diff --git a/src/components/settings/SettingsViewSection.tsx b/src/components/settings/SettingsViewSection.tsx deleted file mode 100644 index c559cb2..0000000 --- a/src/components/settings/SettingsViewSection.tsx +++ /dev/null @@ -1,130 +0,0 @@ -// Deprecated view settings section; superseded by SettingsGeneralSection. -import type { ViewMode } from "@/types"; -import type { SettingsUpdateHandler } from "./types"; -import { PressButton } from "@/components/primitives/PressButton"; - -type SettingsViewSectionProps = { - sectionId: string; - defaultViewMode: ViewMode; - smoothScroll: boolean; - gridCentered: boolean; - compactMode: boolean; - showParentEntry: boolean; - confirmDelete: boolean; - onUpdate: SettingsUpdateHandler; -}; - -export const SettingsViewSection = ({ - sectionId, - defaultViewMode, - smoothScroll, - gridCentered, - compactMode, - showParentEntry, - confirmDelete, - onUpdate, -}: SettingsViewSectionProps) => { - return ( -
-
View
-
-
-
Default view
-
Used for new tabs and new sessions.
-
-
- onUpdate({ defaultViewMode: "thumbs" })} - > - Grid - - onUpdate({ defaultViewMode: "list" })} - > - List - -
-
-
-
-
Smooth scrolling
-
- Keep the original smooth wheel scroll instead of snapping by row. -
-
- -
-
-
-
Center grid items
-
Center the grid within the viewport.
-
- -
-
-
-
Compact mode
-
- Edge-to-edge layout with a flush sidebar and a simplified content frame. -
-
- -
-
-
-
Parent directory entry
-
Show a pseudo entry for moving up one level.
-
- -
-
-
-
Confirm deletes
-
- Ask before sending items to the trash. -
-
- -
-
- ); -}; diff --git a/src/constants/settings.ts b/src/constants/settings.ts index b904503..cf31ca2 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -10,4 +10,4 @@ export const GRID_GAP_MAX = 24; export const GRID_GAP_DEFAULT = 8; export const SETTINGS_STORAGE_KEY = "stratum.settings"; -export const SETTINGS_STORAGE_VERSION = 18; +export const SETTINGS_STORAGE_VERSION = 19; diff --git a/src/hooks/ui/app/useAppAppearance.ts b/src/hooks/ui/app/useAppAppearance.ts index f4bf2b8..27d3458 100644 --- a/src/hooks/ui/app/useAppAppearance.ts +++ b/src/hooks/ui/app/useAppAppearance.ts @@ -2,30 +2,27 @@ import { useEffect } from "react"; import type { AccentTheme } from "@/modules"; -type AppAppearanceOptions = { - accentTheme: AccentTheme; - ambientBackground: boolean; - blurOverlays: boolean; - gridRounded: boolean; - gridCentered: boolean; - compactMode: boolean; -}; - -export const useAppAppearance = ({ - accentTheme, - ambientBackground, - blurOverlays, - gridRounded, - gridCentered, - compactMode, -}: AppAppearanceOptions) => { - useEffect(() => { - const root = document.documentElement; - root.dataset.accent = accentTheme; - root.dataset.ambient = ambientBackground ? "true" : "false"; - root.dataset.blurOverlays = blurOverlays ? "true" : "false"; - root.dataset.gridCorners = gridRounded ? "rounded" : "straight"; - root.dataset.gridCenter = gridCentered ? "true" : "false"; - root.dataset.compact = compactMode ? "true" : "false"; - }, [accentTheme, ambientBackground, blurOverlays, gridRounded, gridCentered, compactMode]); -}; +type AppAppearanceOptions = { + accentTheme: AccentTheme; + ambientBackground: boolean; + blurOverlays: boolean; + gridRounded: boolean; + gridCentered: boolean; +}; + +export const useAppAppearance = ({ + accentTheme, + ambientBackground, + blurOverlays, + gridRounded, + gridCentered, +}: AppAppearanceOptions) => { + useEffect(() => { + const root = document.documentElement; + root.dataset.accent = accentTheme; + root.dataset.ambient = ambientBackground ? "true" : "false"; + root.dataset.blurOverlays = blurOverlays ? "true" : "false"; + root.dataset.gridCorners = gridRounded ? "rounded" : "straight"; + root.dataset.gridCenter = gridCentered ? "true" : "false"; + }, [accentTheme, ambientBackground, blurOverlays, gridRounded, gridCentered]); +}; diff --git a/src/hooks/ui/app/useAppEffects.ts b/src/hooks/ui/app/useAppEffects.ts index 1bc053f..0d3f5b0 100644 --- a/src/hooks/ui/app/useAppEffects.ts +++ b/src/hooks/ui/app/useAppEffects.ts @@ -29,7 +29,6 @@ type AppEffectSettings = { blurOverlays: boolean; gridRounded: boolean; gridCentered: boolean; - compactMode: boolean; }; type AppEffectView = { @@ -101,7 +100,6 @@ export const useAppEffects = ({ blurOverlays, gridRounded, gridCentered, - compactMode, } = settings; const { activeTabId, @@ -172,7 +170,6 @@ export const useAppEffects = ({ blurOverlays, gridRounded, gridCentered, - compactMode, }); useEffect(() => { diff --git a/src/hooks/ui/app/useAppFileViewProps.tsx b/src/hooks/ui/app/useAppFileViewProps.tsx index d9a7624..cfb5981 100644 --- a/src/hooks/ui/app/useAppFileViewProps.tsx +++ b/src/hooks/ui/app/useAppFileViewProps.tsx @@ -20,7 +20,6 @@ type UseAppFileViewPropsOptions = { | "scrollRestoreTop" | "scrollRequest" | "smoothScroll" - | "compactMode" | "sortState" | "canGoUp" >; diff --git a/src/modules/settings.defaults.ts b/src/modules/settings.defaults.ts index 484d913..100ece7 100644 --- a/src/modules/settings.defaults.ts +++ b/src/modules/settings.defaults.ts @@ -27,7 +27,6 @@ export const DEFAULT_SETTINGS: Settings = { fixedWidthTabs: false, smoothScroll: false, smartTabJump: true, - compactMode: true, accentTheme: "red", categoryTinting: false, showParentEntry: true, diff --git a/src/modules/settings.normalize.ts b/src/modules/settings.normalize.ts index b44bc39..eee7bbd 100644 --- a/src/modules/settings.normalize.ts +++ b/src/modules/settings.normalize.ts @@ -150,10 +150,6 @@ export const coerceSettings = (value: Partial | null | undefined): Set typeof value?.smartTabJump === "boolean" ? value.smartTabJump : DEFAULT_SETTINGS.smartTabJump, - compactMode: - typeof value?.compactMode === "boolean" - ? value.compactMode - : DEFAULT_SETTINGS.compactMode, accentTheme: coerceAccentTheme(value?.accentTheme), categoryTinting: typeof value?.categoryTinting === "boolean" diff --git a/src/modules/settings.store.ts b/src/modules/settings.store.ts index 5bc52d3..cf5bc8d 100644 --- a/src/modules/settings.store.ts +++ b/src/modules/settings.store.ts @@ -76,7 +76,6 @@ useSettingsStore.subscribe((state) => { fixedWidthTabs: state.fixedWidthTabs, smoothScroll: state.smoothScroll, smartTabJump: state.smartTabJump, - compactMode: state.compactMode, accentTheme: state.accentTheme, categoryTinting: state.categoryTinting, showParentEntry: state.showParentEntry, diff --git a/src/modules/settings.types.ts b/src/modules/settings.types.ts index 0445734..9b6e241 100644 --- a/src/modules/settings.types.ts +++ b/src/modules/settings.types.ts @@ -26,7 +26,6 @@ export type Settings = { fixedWidthTabs: boolean; smoothScroll: boolean; smartTabJump: boolean; - compactMode: boolean; accentTheme: AccentTheme; categoryTinting: boolean; showParentEntry: boolean; diff --git a/src/styles/base/_scrollbars.scss b/src/styles/base/_scrollbars.scss index 6362dbc..cc61db5 100644 --- a/src/styles/base/_scrollbars.scss +++ b/src/styles/base/_scrollbars.scss @@ -1,10 +1,5 @@ // Shared scrollbar styling. :root { - --scrollbar-thumb: #20283a; -} - -:root[data-compact="true"] { - // Lighter thumb helps the scrollbar stay visible in dense layouts. --scrollbar-thumb: #d9deea; } .list-body::-webkit-scrollbar { diff --git a/src/styles/components/_file-view.scss b/src/styles/components/_file-view.scss index 4152afc..5730771 100644 --- a/src/styles/components/_file-view.scss +++ b/src/styles/components/_file-view.scss @@ -5,13 +5,8 @@ gap: 12px; min-height: 0; flex: 1; - padding-top: 10px; - --list-columns: 1fr 110px 180px; -} - -:root[data-compact="true"] :where(.file-list) { + padding: var(--content-edge-gap); --list-columns: 1fr 90px 150px; - padding: var(--compact-edge-gap); } // Scope list internals to the list container. @@ -56,12 +51,8 @@ overflow-y: auto; min-height: 0; flex: 1; - padding: var(--thumb-padding, 6px); - // Compensate for the reserved scrollbar gutter so side + top gaps stay even. - padding-right: max( - calc(var(--thumb-padding, 6px) - var(--thumb-scrollbar-size, 10px)), - 0px - ); + padding: 0; + padding-right: 0; scroll-behavior: auto; scrollbar-gutter: stable; scrollbar-width: auto; @@ -104,10 +95,6 @@ } } -:root[data-compact="true"] :where(.thumb-shell) .thumb-viewport { - padding: 0; -} - :root[data-grid-center="false"] :where(.thumb-shell) .thumb-grid { align-content: start; justify-content: start; @@ -164,10 +151,7 @@ display: flex; flex-direction: column; overflow: hidden; -} - -:root[data-compact="true"] :where(.thumb-shell) { - padding: var(--compact-edge-gap); + padding: var(--content-edge-gap); } :where(.thumb-shell) { @@ -414,7 +398,7 @@ color: rgba(220, 227, 238, 0.82); } -:root[data-ambient="true"][data-compact="true"] :where(.thumb-shell) .thumb-card.is-selected { +:root[data-ambient="true"] :where(.thumb-shell) .thumb-card.is-selected { border-color: rgba(var(--accent-rgb), 0.7); background: rgba(var(--accent-rgb), 0.12); } @@ -426,9 +410,7 @@ background: rgba(var(--accent-rgb), 0.18); } -:root[data-ambient="true"][data-compact="true"] - :where(.thumb-shell) - .thumb-card[data-drop-target="true"] { +:root[data-ambient="true"] :where(.thumb-shell) .thumb-card[data-drop-target="true"] { border-color: rgba(var(--accent-rgb), 0.9); background: rgba(var(--accent-rgb), 0.18); } @@ -527,7 +509,7 @@ display: grid; grid-template-columns: var(--list-columns); align-items: center; - padding: 6px 10px; + padding: 4px 8px; border-bottom: 1px solid var(--stroke); background: rgba(12, 16, 24, 0.6); } @@ -538,14 +520,14 @@ justify-content: flex-start; gap: 6px; width: 100%; - padding: 4px 6px; + padding: 3px 4px; border-radius: 6px; border: 1px solid transparent; background: transparent; - font-size: 11px; + font-size: 10px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.16em; + letter-spacing: 0.12em; color: var(--muted); text-align: left; transition: none; @@ -585,22 +567,12 @@ } } -:root[data-compact="true"] :where(.file-list) .list-header { - padding: 4px 8px; -} - -:root[data-compact="true"] :where(.file-list) .list-header-button { - padding: 3px 4px; - font-size: 10px; - letter-spacing: 0.12em; -} - :where(.file-list) { .list-body { position: relative; overflow-x: hidden; overflow-y: auto; - padding-right: 8px; + padding-right: 0; min-width: 0; flex: 1; scroll-behavior: auto; @@ -675,10 +647,6 @@ } } -:root[data-compact="true"] :where(.file-list) .list-body { - padding-right: 0; -} - .selection-rect { position: fixed; z-index: 3; @@ -718,10 +686,10 @@ display: grid; grid-template-columns: var(--list-columns); align-items: center; - gap: 12px; - padding: 10px 14px; + gap: 8px; + padding: 6px 10px; height: var(--list-row-height, auto); - border-radius: 8px; + border-radius: 6px; background: var(--surface-control-muted); --list-dot-color: var(--accent); border: 1px solid transparent; @@ -736,7 +704,7 @@ .row.is-renaming .name { overflow: visible; - gap: 8px; + gap: 6px; } .row.is-parent { @@ -784,7 +752,8 @@ .row .name { display: flex; align-items: center; - gap: 10px; + gap: 8px; + font-size: 13px; font-weight: 600; min-width: 0; overflow: hidden; @@ -794,8 +763,8 @@ .row .name::before { content: ""; - width: 10px; - height: 10px; + width: 8px; + height: 8px; border-radius: 999px; background: var(--list-dot-color); } @@ -809,27 +778,6 @@ } } -:root[data-compact="true"] :where(.file-list) .row { - gap: 8px; - padding: 6px 10px; - border-radius: 6px; -} - - -:root[data-compact="true"] :where(.file-list) .row .name { - gap: 8px; - font-size: 13px; -} - -:root[data-compact="true"] :where(.file-list) .row.is-renaming .name { - gap: 6px; -} - -:root[data-compact="true"] :where(.file-list) .row .name::before { - width: 8px; - height: 8px; -} - :root[data-ambient="true"] :where(.file-list) .row { background: rgba(18, 22, 33, 0.82); } diff --git a/src/styles/components/_sidebar.scss b/src/styles/components/_sidebar.scss index b911da6..b00f328 100644 --- a/src/styles/components/_sidebar.scss +++ b/src/styles/components/_sidebar.scss @@ -6,6 +6,7 @@ --scrollbar-thumb: #1f2736; background: #0d0f14; border: 1px solid #1a2233; + border-left: none; border-radius: var(--panel-radius); padding: var(--sidebar-padding); display: flex; @@ -254,8 +255,3 @@ color: rgba(var(--accent-rgb), 0.95); } -// Compact mode removes the outer container frame while keeping the content intact. -:root[data-compact="true"] .sidebar { - border-left: none; -} - diff --git a/src/styles/layout/_layout.scss b/src/styles/layout/_layout.scss index d064420..11c9f31 100644 --- a/src/styles/layout/_layout.scss +++ b/src/styles/layout/_layout.scss @@ -14,8 +14,8 @@ } .main { - background: var(--panel); - border: 1px solid var(--stroke); + background: transparent; + border: none; border-radius: var(--panel-radius); padding: var(--main-padding); min-height: 0; @@ -23,9 +23,3 @@ flex-direction: column; min-width: 0; } - -// Compact mode removes the outer container frame while keeping the content intact. -:root[data-compact="true"] .main { - background: transparent; - border: none; -} diff --git a/src/styles/tokens/_theme.scss b/src/styles/tokens/_theme.scss index 116801a..499e41b 100644 --- a/src/styles/tokens/_theme.scss +++ b/src/styles/tokens/_theme.scss @@ -50,13 +50,14 @@ $accent-themes: ( --accent-rgb: 225, 76, 76; --topstack-height: 120px; --statusbar-height: 30px; - // Layout sizing tokens to keep density changes centralized. - --layout-gap: 16px; - --layout-padding: 12px 16px 16px; - --panel-radius: 10px; + // Layout sizing tokens for the default edge-to-edge shell. + --layout-gap: 0; + --layout-padding: 0; + --panel-radius: 0; --sidebar-padding: 16px; - --main-padding: 12px; + --main-padding: 0; --thumb-grid-margin: 0; + --content-edge-gap: 8px; } @each $name, $theme in $accent-themes { @@ -74,13 +75,3 @@ $accent-themes: ( --ambient-glow-mid: rgba(var(--accent-rgb), 0.32); --ambient-glow-soft: rgba(var(--accent-rgb), 0.18); } - -// Compact mode trims layout chrome for an edge-to-edge workspace. -:root[data-compact="true"] { - --layout-gap: 0; - --layout-padding: 0; - --panel-radius: 0; - --main-padding: 0; - --thumb-grid-margin: 0; - --compact-edge-gap: 8px; -} From 43f0f2fe5be3bddec35d13d8f6f02d1741fda4d1 Mon Sep 17 00:00:00 2001 From: emy Date: Sun, 22 Feb 2026 20:06:44 +0100 Subject: [PATCH 2/7] =?UTF-8?q?chore(dev):=20update=20frontend=20dev=20con?= =?UTF-8?q?fig=20and=20scripts=20=E2=9A=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- vite.config.ts | 42 +++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index c99b6fc..3dd7237 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stratum", "private": true, - "version": "0.1.7", + "version": "0.1.8", "type": "module", "scripts": { "dev": "vite", diff --git a/vite.config.ts b/vite.config.ts index 9881a82..6212e01 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,8 +6,8 @@ import { fileURLToPath } from "node:url"; // @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; const rootDir = fileURLToPath(new URL(".", import.meta.url)); - -// https://vite.dev/config/ + +// https://vite.dev/config/ export default defineConfig(async () => ({ plugins: [react()], resolve: { @@ -64,22 +64,22 @@ export default defineConfig(async () => ({ // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors - clearScreen: false, - // 2. tauri expects a fixed port, fail if that port is not available - server: { - port: 1421, - strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: 1421, - } - : undefined, - watch: { - // 3. tell Vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"], - }, - }, -})); + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 5173, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 5173, + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, +})); From 33a455f6fb319eb748f0e79bd37a9c9ab11a0fcb Mon Sep 17 00:00:00 2001 From: emy Date: Sun, 22 Feb 2026 20:06:50 +0100 Subject: [PATCH 3/7] =?UTF-8?q?chore(tauri):=20sync=20app=20config=20with?= =?UTF-8?q?=20Rust=20dependency=20updates=20=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1a151b5..182f592 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3632,7 +3632,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stratum" -version = "0.1.7" +version = "0.1.8" dependencies = [ "flate2", "image", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6e76862..f1d9b72 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stratum" -version = "0.1.7" +version = "0.1.8" description = "Stratum File Manager" authors = ["emy"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b692c33..9f0fa04 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,11 +1,11 @@ -{ - "$schema": "https://schema.tauri.app/config/2", - "productName": "Stratum", - "version": "0.1.7", - "identifier": "com.emy.stratum", - "build": { - "beforeDevCommand": "pnpm dev", - "devUrl": "http://localhost:1421", +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Stratum", + "version": "0.1.8", + "identifier": "com.emy.stratum", + "build": { + "beforeDevCommand": "pnpm dev", + "devUrl": "http://localhost:5173", "beforeBuildCommand": "pnpm build", "frontendDist": "../dist" }, From f77ad909d4a7bcf71991160978040ae53c5ac8c4 Mon Sep 17 00:00:00 2001 From: emy Date: Sun, 1 Mar 2026 07:22:53 +0100 Subject: [PATCH 4/7] build(config): align Vite and Tauri dev host settings on Windows, add Node typings --- package.json | 1 + pnpm-lock.yaml | 26 +++++++++++++++++++++----- src-tauri/tauri.conf.json | 2 +- tsconfig.node.json | 15 ++++++++------- vite.config.ts | 10 ++++++---- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 3dd7237..9edd949 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "tinykeys": "^3.0.0" }, "devDependencies": { + "@types/node": "^24.3.0", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/debug": "^4.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92ee99f..e064976 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: '@types/debug': specifier: ^4.1.12 version: 4.1.12 + '@types/node': + specifier: ^24.3.0 + version: 24.11.0 '@types/react': specifier: ^19.1.8 version: 19.2.9 @@ -47,7 +50,7 @@ importers: version: 19.2.3(@types/react@19.2.9) '@vitejs/plugin-react': specifier: ^4.6.0 - version: 4.7.0(vite@7.3.1(sass@1.97.2)) + version: 4.7.0(vite@7.3.1(@types/node@24.11.0)(sass@1.97.2)) sass: specifier: ^1.80.7 version: 1.97.2 @@ -56,7 +59,7 @@ importers: version: 5.8.3 vite: specifier: ^7.0.4 - version: 7.3.1(sass@1.97.2) + version: 7.3.1(@types/node@24.11.0)(sass@1.97.2) packages: @@ -626,6 +629,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@24.11.0': + resolution: {integrity: sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -807,6 +813,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1311,6 +1320,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@24.11.0': + dependencies: + undici-types: 7.16.0 + '@types/react-dom@19.2.3(@types/react@19.2.9)': dependencies: '@types/react': 19.2.9 @@ -1319,7 +1332,7 @@ snapshots: dependencies: csstype: 3.2.3 - '@vitejs/plugin-react@4.7.0(vite@7.3.1(sass@1.97.2))': + '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@24.11.0)(sass@1.97.2))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -1327,7 +1340,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.3.1(sass@1.97.2) + vite: 7.3.1(@types/node@24.11.0)(sass@1.97.2) transitivePeerDependencies: - supports-color @@ -1504,6 +1517,8 @@ snapshots: typescript@5.8.3: {} + undici-types@7.16.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -1514,7 +1529,7 @@ snapshots: dependencies: react: 19.2.3 - vite@7.3.1(sass@1.97.2): + vite@7.3.1(@types/node@24.11.0)(sass@1.97.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -1523,6 +1538,7 @@ snapshots: rollup: 4.55.2 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 24.11.0 fsevents: 2.3.3 sass: 1.97.2 diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9f0fa04..2538433 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -5,7 +5,7 @@ "identifier": "com.emy.stratum", "build": { "beforeDevCommand": "pnpm dev", - "devUrl": "http://localhost:5173", + "devUrl": "http://127.0.0.1:1420", "beforeBuildCommand": "pnpm build", "frontendDist": "../dist" }, diff --git a/tsconfig.node.json b/tsconfig.node.json index 165a9ba..6edb6eb 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,10 +1,11 @@ { - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "types": ["node"] + }, "include": ["vite.config.ts"] } diff --git a/vite.config.ts b/vite.config.ts index 6212e01..f853005 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,8 +3,9 @@ import react from "@vitejs/plugin-react"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; -// @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; +const localDevHost = "127.0.0.1"; +const devPort = 1420; const rootDir = fileURLToPath(new URL(".", import.meta.url)); // https://vite.dev/config/ @@ -67,14 +68,15 @@ export default defineConfig(async () => ({ clearScreen: false, // 2. tauri expects a fixed port, fail if that port is not available server: { - port: 5173, + port: devPort, strictPort: true, - host: host || false, + // Use an explicit IPv4 loopback address so Windows does not resolve `localhost` to `::1`. + host: host || localDevHost, hmr: host ? { protocol: "ws", host, - port: 5173, + port: devPort, } : undefined, watch: { From c42f57e0ae531548d017edddb5616c2d5dbeb4fb Mon Sep 17 00:00:00 2001 From: emy Date: Sun, 1 Mar 2026 07:23:17 +0100 Subject: [PATCH 5/7] =?UTF-8?q?feat(explorer):=20=E2=9C=A8=20show=20pendin?= =?UTF-8?q?g=20delete=20state=20in=20grid=20and=20list=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 1 + src/components/explorer/FileGrid.tsx | 19 +++- src/components/explorer/FileList.tsx | 18 +++- src/components/explorer/FileView.tsx | 4 + .../explorer/fileGrid/EntryCard.tsx | 21 ++-- .../explorer/fileGrid/gridCard.types.ts | 1 + src/components/explorer/fileList/EntryRow.tsx | 12 ++- src/hooks/domain/filesystem/useFileManager.ts | 1 + .../domain/filesystem/useFileMutations.ts | 44 ++++++++- src/hooks/ui/app/useAppFileViewProps.tsx | 1 + src/styles/components/_file-view.scss | 96 +++++++++++++++++++ 11 files changed, 198 insertions(+), 20 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 639fc8c..9ce8267 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -697,6 +697,7 @@ const App = () => { scrollRestoreTop, scrollRequest, smoothScroll: settings.smoothScroll, + pendingDeletePaths: fileManager.pendingDeletePaths, sortState, canGoUp, }, diff --git a/src/components/explorer/FileGrid.tsx b/src/components/explorer/FileGrid.tsx index 5154a2d..959580d 100644 --- a/src/components/explorer/FileGrid.tsx +++ b/src/components/explorer/FileGrid.tsx @@ -9,7 +9,7 @@ import { useEntryPresence, useScrollToIndex, } from "@/hooks"; -import { getEmptyMessage, handleMiddleClick } from "@/lib"; +import { getEmptyMessage, handleMiddleClick, normalizePath } from "@/lib"; import type { DropTarget, EntryItem } from "@/lib"; import type { GridNameEllipsis, GridSize, ThumbnailFit } from "@/modules"; import type { EntryMeta, FileEntry, RenameCommitReason, ThumbnailRequest } from "@/types"; @@ -35,6 +35,7 @@ type FileGridProps = { scrollRestoreTop: number; scrollRequest?: { index: number; nonce: number } | null; smoothScroll: boolean; + pendingDeletePaths: Set; selectedPaths: Set; onSetSelection: (paths: string[], anchor?: string) => void; onOpenDir: (path: string) => void; @@ -106,6 +107,7 @@ const FileGrid = ({ scrollRestoreTop, scrollRequest, smoothScroll, + pendingDeletePaths, selectedPaths, onSetSelection, onOpenDir, @@ -158,6 +160,13 @@ const FileGrid = ({ canGoUp, onGoUp, }: FileGridProps) => { + const isDeletePending = useCallback( + (path: string) => { + const key = normalizePath(path) ?? path.trim(); + return key ? pendingDeletePaths.has(key) : false; + }, + [pendingDeletePaths], + ); const emptyMessage = useMemo(() => getEmptyMessage(searchQuery), [searchQuery]); const { items: viewItems } = useEntryPresence({ items, @@ -168,9 +177,12 @@ const FileGrid = ({ () => viewItems.map((item) => ({ path: item.type === "parent" ? item.path : item.entry.path, - selectable: item.type !== "parent" && item.presence !== "removed", + selectable: + item.type !== "parent" && + item.presence !== "removed" && + !isDeletePending(item.entry.path), })), - [viewItems], + [isDeletePending, viewItems], ); const resolvedIndexMap = useMemo(() => { if (indexMap) return indexMap; @@ -432,6 +444,7 @@ const FileGrid = ({ nameEllipsis={gridNameEllipsis} hideExtension={gridNameHideExtension} selected={selectedPaths.has(item.entry.path)} + isDeleting={isDeletePending(item.entry.path)} dropTarget={isDropTarget} isRenaming={renameTargetPath === item.entry.path} renameValue={renameValue} diff --git a/src/components/explorer/FileList.tsx b/src/components/explorer/FileList.tsx index 1a41829..9255dca 100644 --- a/src/components/explorer/FileList.tsx +++ b/src/components/explorer/FileList.tsx @@ -9,7 +9,7 @@ import { useEntryPresence, useScrollToIndex, } from "@/hooks"; -import { getEmptyMessage, handleMiddleClick } from "@/lib"; +import { getEmptyMessage, handleMiddleClick, normalizePath } from "@/lib"; import type { DropTarget, EntryItem } from "@/lib"; import type { EntryMeta, FileEntry, RenameCommitReason, SortState } from "@/types"; import { EmptyState } from "@/components/primitives/EmptyState"; @@ -34,6 +34,7 @@ type FileListProps = { scrollRestoreTop: number; scrollRequest?: { index: number; nonce: number } | null; smoothScroll: boolean; + pendingDeletePaths: Set; sortState: SortState; onSortChange: (next: SortState) => void; categoryTinting: boolean; @@ -87,6 +88,7 @@ const FileList = ({ scrollRestoreTop, scrollRequest, smoothScroll, + pendingDeletePaths, sortState, onSortChange, categoryTinting, @@ -121,6 +123,13 @@ const FileList = ({ canGoUp, onGoUp, }: FileListProps) => { + const isDeletePending = useCallback( + (path: string) => { + const key = normalizePath(path) ?? path.trim(); + return key ? pendingDeletePaths.has(key) : false; + }, + [pendingDeletePaths], + ); const emptyMessage = useMemo(() => getEmptyMessage(searchQuery), [searchQuery]); const { items: rows } = useEntryPresence({ items, @@ -131,9 +140,11 @@ const FileList = ({ () => rows.map((row) => ({ path: row.type === "parent" ? row.path : row.entry.path, - selectable: row.presence !== "removed", + selectable: + row.presence !== "removed" && + (row.type === "parent" || !isDeletePending(row.entry.path)), })), - [rows], + [isDeletePending, rows], ); const resolvedIndexMap = useMemo(() => { if (indexMap) return indexMap; @@ -347,6 +358,7 @@ const FileList = ({ sizeLabel={rowMeta?.sizeLabel ?? ""} modifiedLabel={rowMeta?.modifiedLabel ?? ""} selected={selectedPaths.has(row.entry.path)} + isDeleting={isDeletePending(row.entry.path)} dropTarget={isDropTarget} isRenaming={renameTargetPath === row.entry.path} renameValue={renameValue} diff --git a/src/components/explorer/FileView.tsx b/src/components/explorer/FileView.tsx index 3018492..2544a97 100644 --- a/src/components/explorer/FileView.tsx +++ b/src/components/explorer/FileView.tsx @@ -51,6 +51,7 @@ type FileViewProps = { scrollRestoreTop: number; scrollRequest?: { index: number; nonce: number } | null; smoothScroll: boolean; + pendingDeletePaths: Set; sortState: SortState; onSortChange: (next: SortState) => void; selectedPaths: Set; @@ -153,6 +154,7 @@ export const FileView = ({ onEntryPreviewPress, onEntryPreviewRelease, smoothScroll, + pendingDeletePaths, sortState, onSortChange, onContextMenuDown, @@ -204,6 +206,7 @@ export const FileView = ({ {...viewProps} currentPath={currentPath} smoothScroll={smoothScroll} + pendingDeletePaths={pendingDeletePaths} canGoUp={canGoUp} onGoUp={onGoUp} thumbnailsEnabled={thumbnailsEnabled} @@ -248,6 +251,7 @@ export const FileView = ({ {...viewProps} currentPath={currentPath} smoothScroll={smoothScroll} + pendingDeletePaths={pendingDeletePaths} sortState={sortState} onSortChange={onSortChange} categoryTinting={categoryTinting} diff --git a/src/components/explorer/fileGrid/EntryCard.tsx b/src/components/explorer/fileGrid/EntryCard.tsx index 0ec2137..2dc4e0a 100644 --- a/src/components/explorer/fileGrid/EntryCard.tsx +++ b/src/components/explorer/fileGrid/EntryCard.tsx @@ -40,10 +40,11 @@ export const EntryCard = memo(({ disableTooltip = false, showSize, showExtension, - nameEllipsis, - hideExtension, - selected, - dropTarget, + nameEllipsis, + hideExtension, + selected, + isDeleting, + dropTarget, isRenaming, renameValue, onRenameChange, @@ -108,13 +109,16 @@ export const EntryCard = memo(({ ] : displayName; const nameEllipsisMode = useMiddleEllipsis ? "middle" : "end"; - // Keep the card element stable so thumbnail previews do not remount on rename. + // Keep the card element stable so thumbnail previews do not remount on rename. const isRemoved = presence === "removed"; - const isInteractive = !isRenaming && !isRemoved; - const tooltipDisabled = isRenaming || entry.isDir || disableTooltip || !isInteractive; + const isInteractive = !isRenaming && !isRemoved && !isDeleting; + const tooltipDisabled = + isRenaming || isDeleting || entry.isDir || disableTooltip || !isInteractive; const cardClass = `thumb-card${isRenaming ? " is-renaming" : ""}${ entry.isDir ? " is-dir" : "" - }${selected ? " is-selected" : ""}${isRemoved ? " is-removed" : ""}`; + }${selected ? " is-selected" : ""}${isRemoved ? " is-removed" : ""}${ + isDeleting ? " is-deleting" : "" + }`; const handleMouseDown = (event: ReactMouseEvent) => { if (!isInteractive) return; if (event.button === 1) { @@ -185,6 +189,7 @@ export const EntryCard = memo(({ data-is-dir={entry.isDir ? "true" : "false"} data-kind={fileKind} data-drop-target={dropTarget ? "true" : "false"} + data-delete-pending={isDeleting ? "true" : "false"} data-presence={presence} aria-selected={selected} aria-hidden={isRemoved ? "true" : "false"} diff --git a/src/components/explorer/fileGrid/gridCard.types.ts b/src/components/explorer/fileGrid/gridCard.types.ts index aeb560c..34bbeb8 100644 --- a/src/components/explorer/fileGrid/gridCard.types.ts +++ b/src/components/explorer/fileGrid/gridCard.types.ts @@ -48,6 +48,7 @@ export type EntryCardProps = { nameEllipsis: GridNameEllipsis; hideExtension: boolean; selected: boolean; + isDeleting: boolean; dropTarget: boolean; isRenaming: boolean; renameValue: string; diff --git a/src/components/explorer/fileList/EntryRow.tsx b/src/components/explorer/fileList/EntryRow.tsx index fd79a49..e97f1b8 100644 --- a/src/components/explorer/fileList/EntryRow.tsx +++ b/src/components/explorer/fileList/EntryRow.tsx @@ -18,6 +18,7 @@ type EntryRowProps = { sizeLabel: string; modifiedLabel: string; selected: boolean; + isDeleting: boolean; dropTarget: boolean; isRenaming: boolean; renameValue: string; @@ -42,6 +43,7 @@ export const EntryRow = memo(({ sizeLabel, modifiedLabel, selected, + isDeleting, dropTarget, isRenaming, renameValue, @@ -58,8 +60,8 @@ export const EntryRow = memo(({ presence = "stable", }: EntryRowProps) => { const isRemoved = presence === "removed"; - const isInteractive = !isRenaming && !isRemoved; - const isSelectable = !isRemoved; + const isInteractive = !isRenaming && !isRemoved && !isDeleting; + const isSelectable = !isRemoved && !isDeleting; const handleMouseDown = (event: ReactMouseEvent) => { if (!isInteractive) return; if (event.button === 1) { @@ -120,7 +122,7 @@ export const EntryRow = memo(({
(null); // Suppress add/remove presence animation while undoing rename so entries stay in-place. const [suppressUndoPresence, setSuppressUndoPresence] = useState(false); + // Tracks entries currently being deleted so views can show immediate progress feedback. + const [pendingDeletePaths, setPendingDeletePaths] = useState>(new Set()); + + const toPathKey = useCallback((path: string) => { + const trimmed = path.trim(); + if (!trimmed) return ""; + return normalizePath(trimmed) ?? trimmed; + }, []); const pushUndo = useCallback((action: UndoAction) => { const stack = undoStackRef.current; @@ -64,7 +72,7 @@ export const useFileMutations = ({ log, }); - const { deleteEntriesInView } = useFileManagerDelete({ + const { deleteEntriesInView: runDeleteEntriesInView } = useFileManagerDelete({ deleteInFlightRef, trashRootRef, homePathRef, @@ -74,6 +82,37 @@ export const useFileMutations = ({ log, }); + const deleteEntriesInView = useCallback( + async (paths: string[]) => { + if (deleteInFlightRef.current) return null; + const nextPending = new Set( + paths + .map((path) => toPathKey(path)) + .filter(Boolean), + ); + if (nextPending.size === 0) return null; + + setPendingDeletePaths((previous) => { + if (previous.size === 0) return new Set(nextPending); + const merged = new Set(previous); + nextPending.forEach((path) => merged.add(path)); + return merged; + }); + + try { + return await runDeleteEntriesInView(paths); + } finally { + setPendingDeletePaths((previous) => { + if (previous.size === 0) return previous; + const next = new Set(previous); + nextPending.forEach((path) => next.delete(path)); + return next; + }); + } + }, + [runDeleteEntriesInView, toPathKey], + ); + const performRenameRequests = useCallback( async ( renames: RenameRequest[], @@ -172,6 +211,7 @@ export const useFileMutations = ({ return { suppressUndoPresence, + pendingDeletePaths, deleteEntriesInView, duplicateEntriesInView, pasteEntriesInView, diff --git a/src/hooks/ui/app/useAppFileViewProps.tsx b/src/hooks/ui/app/useAppFileViewProps.tsx index cfb5981..1c2ca32 100644 --- a/src/hooks/ui/app/useAppFileViewProps.tsx +++ b/src/hooks/ui/app/useAppFileViewProps.tsx @@ -20,6 +20,7 @@ type UseAppFileViewPropsOptions = { | "scrollRestoreTop" | "scrollRequest" | "smoothScroll" + | "pendingDeletePaths" | "sortState" | "canGoUp" >; diff --git a/src/styles/components/_file-view.scss b/src/styles/components/_file-view.scss index 5730771..4b05758 100644 --- a/src/styles/components/_file-view.scss +++ b/src/styles/components/_file-view.scss @@ -144,6 +144,12 @@ place-items: center; } +@keyframes delete-pending-spin { + to { + transform: rotate(360deg); + } +} + .thumb-shell { position: relative; min-height: 0; @@ -156,6 +162,7 @@ :where(.thumb-shell) { .thumb-card { + position: relative; border: 1px solid var(--stroke); border-radius: 6px; padding: 6px; @@ -176,6 +183,50 @@ cursor: default; } + // Show immediate in-card delete progress without shifting layout. + .thumb-card.is-deleting { + cursor: progress; + } + + .thumb-card.is-deleting::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: rgba(7, 10, 16, 0.28); + pointer-events: none; + z-index: 3; + } + + .thumb-card.is-deleting::after { + content: ""; + position: absolute; + right: 8px; + top: 8px; + width: 12px; + height: 12px; + border: 2px solid rgba(255, 255, 255, 0.34); + border-top-color: rgba(255, 255, 255, 0.95); + border-radius: 999px; + pointer-events: none; + z-index: 4; + animation: delete-pending-spin 0.8s linear infinite; + } + + .thumb-card.is-deleting .thumb-icon { + filter: saturate(0.72) brightness(0.82); + } + + .thumb-card.is-deleting .thumb-preview, + .thumb-card.is-deleting .thumb-app-icon, + .thumb-card.is-deleting .thumb-svg { + filter: blur(0.9px); + } + + .thumb-card.is-deleting .thumb-meta { + opacity: 0.74; + } + // Give folders a subtle cool tint so they are easier to distinguish from files. .thumb-card.is-dir:not(.is-selected):not([data-drop-target="true"]) { background: rgba(92, 126, 196, 0.12); @@ -679,10 +730,16 @@ transition: none; opacity: 0.4; } + + :where(.file-list) .row.is-deleting::after, + :where(.thumb-shell) .thumb-card.is-deleting::after { + animation: none; + } } :where(.file-list) { .row { + position: relative; display: grid; grid-template-columns: var(--list-columns); align-items: center; @@ -702,6 +759,45 @@ cursor: default; } + // Keep rows visible while delete work is in-flight, but mark them as pending. + .row.is-deleting { + cursor: progress; + } + + .row.is-deleting::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: rgba(7, 10, 16, 0.26); + pointer-events: none; + z-index: 1; + } + + .row.is-deleting::after { + content: ""; + position: absolute; + right: 10px; + top: 50%; + width: 11px; + height: 11px; + margin-top: -5.5px; + border: 2px solid rgba(255, 255, 255, 0.34); + border-top-color: rgba(255, 255, 255, 0.95); + border-radius: 999px; + pointer-events: none; + z-index: 2; + animation: delete-pending-spin 0.8s linear infinite; + } + + .row.is-deleting .name, + .row.is-deleting .size, + .row.is-deleting .modified { + opacity: 0.72; + position: relative; + z-index: 2; + } + .row.is-renaming .name { overflow: visible; gap: 6px; From 4f092827b3050fe6bdffd71b5c5ea006417ccff5 Mon Sep 17 00:00:00 2001 From: emy Date: Mon, 2 Mar 2026 00:17:53 +0100 Subject: [PATCH 6/7] feat(tauri): enable custom window chrome config and permissions --- src-tauri/capabilities/default.json | 4 ++++ src-tauri/tauri.conf.json | 14 ++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 76e9053..5ae82fb 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,8 +5,12 @@ "windows": ["main"], "permissions": [ "core:default", + "core:window:allow-close", + "core:window:allow-minimize", "core:window:allow-set-size", + "core:window:allow-start-dragging", "core:window:allow-set-title", + "core:window:allow-toggle-maximize", "core:window:allow-destroy", "dialog:default", { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2538433..f591ba8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -14,12 +14,14 @@ { "title": "Stratum", "width": 1200, - "height": 760, - "minWidth": 900, - "minHeight": 600, - "dragDropEnabled": false, - "backgroundColor": "#0a0b10" - } + "height": 760, + "minWidth": 900, + "minHeight": 600, + "dragDropEnabled": false, + "decorations": false, + "shadow": true, + "backgroundColor": "#0a0b10" + } ], "security": { "csp": null, From 50f08dd84bcf045743411244ac4d1ccd58e68c04 Mon Sep 17 00:00:00 2001 From: emy Date: Mon, 2 Mar 2026 00:18:18 +0100 Subject: [PATCH 7/7] feat(shell): add frameless window chrome, preview topbar + UI polish --- src/App.tsx | 9 +- src/components/app/AppOverlays.tsx | 46 +++- src/components/app/AppWindowFrame.tsx | 4 + src/components/app/index.ts | 3 +- src/components/explorer/PathBar.tsx | 39 ++- src/components/explorer/PathBarActions.tsx | 33 +-- src/components/icons/uiIcons.tsx | 61 ++++- src/components/navigation/WindowControls.tsx | 58 +++++ src/components/navigation/index.ts | 3 +- src/components/overlay/AboutModal.tsx | 13 +- .../preview/QuickPreviewOverlay.tsx | 43 +++- src/components/preview/QuickPreviewTopBar.tsx | 72 ++++++ src/components/preview/index.ts | 1 + .../primitives/ToolbarIconButton.tsx | 4 +- src/components/primitives/WindowChromeBar.tsx | 42 ++++ .../primitives/WindowDragRegion.tsx | 18 ++ src/components/primitives/index.ts | 2 + src/constants/index.ts | 1 + src/constants/tooltip.ts | 1 + src/hooks/ui/app/useAppEffects.ts | 16 ++ src/hooks/ui/app/useAppTopstackProps.tsx | 12 +- src/styles/app.scss | 2 + src/styles/components/_modals.scss | 38 +-- src/styles/components/_pathbar.scss | 235 ++++++++++++------ src/styles/components/_quick-preview.scss | 118 ++++++--- src/styles/components/_sidebar.scss | 4 +- src/styles/components/_tabs.scss | 7 +- src/styles/components/_topstack.scss | 4 +- src/styles/components/_window-chrome.scss | 75 ++++++ src/styles/components/_window-frame.scss | 11 + src/styles/layout/_app-shell.scss | 8 +- src/styles/tokens/_theme.scss | 2 + 32 files changed, 781 insertions(+), 204 deletions(-) create mode 100644 src/components/app/AppWindowFrame.tsx create mode 100644 src/components/navigation/WindowControls.tsx create mode 100644 src/components/preview/QuickPreviewTopBar.tsx create mode 100644 src/components/primitives/WindowChromeBar.tsx create mode 100644 src/components/primitives/WindowDragRegion.tsx create mode 100644 src/styles/components/_window-chrome.scss create mode 100644 src/styles/components/_window-frame.scss diff --git a/src/App.tsx b/src/App.tsx index 9ce8267..e5e9b69 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ // App shell wiring: composes state hooks, layout blocks, and overlays. import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { AppOverlays, AppShellLayout } from "@/components"; +import { AppOverlays, AppShellLayout, AppWindowFrame } from "@/components"; import { useAppCommands, useAppContextMenuSection, @@ -836,9 +836,9 @@ const App = () => { return (
- { }} /> +
); }; diff --git a/src/components/app/AppOverlays.tsx b/src/components/app/AppOverlays.tsx index 3e82c98..f3f10d5 100644 --- a/src/components/app/AppOverlays.tsx +++ b/src/components/app/AppOverlays.tsx @@ -24,7 +24,7 @@ const LazyQuickPreviewOverlay = lazy(async () => { return { default: module.QuickPreviewOverlay }; }); -const useDeferredMount = (open: boolean) => { +const useDeferredMount = (open: boolean, prewarmOnIdle = false) => { const [mounted, setMounted] = useState(open); useEffect(() => { @@ -33,6 +33,46 @@ const useDeferredMount = (open: boolean) => { } }, [open]); + useEffect(() => { + if (mounted || open || !prewarmOnIdle) return; + if (typeof window === "undefined") return; + + const idleWindow = window as Window & { + requestIdleCallback?: ( + callback: IdleRequestCallback, + options?: IdleRequestOptions, + ) => number; + cancelIdleCallback?: (handle: number) => void; + }; + let timeoutId: number | null = null; + let idleId: number | null = null; + + const prewarm = () => { + setMounted(true); + }; + + // Warm hidden heavyweight overlays after the first paint so the first open feels instant. + if (typeof idleWindow.requestIdleCallback === "function") { + idleId = idleWindow.requestIdleCallback(() => { + prewarm(); + }, { timeout: 1200 }); + } else { + timeoutId = window.setTimeout(prewarm, 280); + } + + return () => { + if (timeoutId != null) { + window.clearTimeout(timeoutId); + } + if ( + idleId != null && + typeof idleWindow.cancelIdleCallback === "function" + ) { + idleWindow.cancelIdleCallback(idleId); + } + }; + }, [mounted, open, prewarmOnIdle]); + return mounted; }; @@ -52,8 +92,8 @@ export const AppOverlays = ({ settings, }: AppOverlaysProps) => { // Load heavyweight overlays on first use, then keep them mounted. - const conversionMounted = useDeferredMount(conversion.open); - const settingsMounted = useDeferredMount(settings.open); + const conversionMounted = useDeferredMount(conversion.open, true); + const settingsMounted = useDeferredMount(settings.open, true); const quickPreviewMounted = useDeferredMount(quickPreview.open); return ( diff --git a/src/components/app/AppWindowFrame.tsx b/src/components/app/AppWindowFrame.tsx new file mode 100644 index 0000000..0d0bc96 --- /dev/null +++ b/src/components/app/AppWindowFrame.tsx @@ -0,0 +1,4 @@ +// Global window chrome that stays visible above fullscreen overlay backdrops. +export const AppWindowFrame = () => { + return