Settings: db-viewer redesign, theme/accent wiring, macOS overlay titlebar (#6)

* feat: settings back button returns to origin view (push/pop nav)

* feat: redesign settings screen with db-viewer border styling

* fix: settings sidebar — icon+text tabs, back at top, top border (PR feedback)

* fix: tighten settings sidebar — back and tabs in one group

* fix: settings header shows active tab name instead of duplicated title

* fix: settings — accent back button, header shows active tab; fix App test

* fix: tags settings — no create label, no card chrome (bg/border/padding)

* feat: drag-and-drop tag reordering in settings tags tab

* fix: general settings — remove section card chrome (bg/border/padding)

* fix: merge Appearance and Interface into one settings section

* fix: remove row separators between settings items within sections

* fix: settings rows use gap spacing instead of per-row padding

* feat: strip section cards from all settings tabs; delete SettingsSection

* feat: apply theme and font size settings (appearance wiring)

* feat: honor default folder setting on startup

* feat: confirm_before_delete setting now skips delete confirmations

* feat: default_ports setting prefills new connection forms

* fix: light theme — text colors, native window chrome, ThemePicker preview

* fix: sync window background color with theme so title bar follows light mode

* fix: window-sync test uses microtask flush instead of vi.waitFor

* fix: app root carries canvas bg so home-screen parent matches theme

* fix: grant window set-theme/set-background-color permissions so title bar follows theme

* fix: overlay title bar — webview paints under traffic lights, flat canvas color in both themes

* fix: compact 28px overlay titlebar with visible bottom border

* fix: in-flow titlebar strip (draggable via allowed permission); screens fill remaining height, no bottom clipping

* fix: top border only on settings + db viewer pages, not the titlebar strip

* fix: db viewer sidebar h-screen -> h-full (28px overflow clipped bottom icons)

* fix: system theme resets window to OS before reading matchMedia (was polluted by forced window theme)

* fix: theme switcher labels moved below previews for readability

* feat: configurable accent color setting (circle palette in Appearance)

* docs: update AGENTS.md + README for settings redesign, wired settings, accent color
This commit is contained in:
2026-08-01 19:24:01 +08:00
committed by GitHub
parent 7db66f1160
commit e32fe7967c
40 changed files with 1359 additions and 463 deletions
+2 -1
View File
@@ -17,6 +17,7 @@ interface ActionRowProps {
export function ActionRow({ onImport, onExport, onNewFolder, onFilters: _onFilters, onDeleteSelected, visibleItemIds = [] }: ActionRowProps) {
const setActiveView = useUiStore((s) => s.setActiveView);
const openSettings = useUiStore((s) => s.openSettings);
const selectedItemIds = useUiStore((s) => s.selectedItemIds);
const selectAllItems = useUiStore((s) => s.selectAllItems);
const clearSelection = useUiStore((s) => s.clearSelection);
@@ -84,7 +85,7 @@ export function ActionRow({ onImport, onExport, onNewFolder, onFilters: _onFilte
</div>
<div className="flex items-center gap-2 ml-auto">
<ImportExportMenu onImport={onImport ?? (() => {})} onExport={onExport ?? (() => {})} />
<Button variant="ghost" className="text-xs" onClick={() => setActiveView("settings")}>
<Button variant="ghost" className="text-xs" onClick={openSettings}>
<SettingsIcon size={14} /> Settings
</Button>
</div>
+124 -2
View File
@@ -4,12 +4,17 @@ import userEvent from "@testing-library/user-event";
import { HomeScreen } from "./HomeScreen";
import { useConnectionStore } from "../../stores/connectionStore";
import { useUiStore } from "../../stores/uiStore";
import { useSettingsStore } from "../../stores/settingsStore";
import type { Connection, Folder } from "../../lib/types";
vi.mock("../../lib/commands", () => ({
getConnections: vi.fn().mockResolvedValue([]),
getFolders: vi.fn().mockResolvedValue([]),
getTags: vi.fn().mockResolvedValue([]),
getSettings: vi.fn().mockResolvedValue({}),
deleteConnection: vi.fn().mockResolvedValue(undefined),
deleteConnectionPassword: vi.fn().mockResolvedValue(undefined),
deleteFolder: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@tauri-apps/plugin-dialog", () => ({ open: vi.fn(), save: vi.fn() }));
@@ -21,7 +26,11 @@ vi.mock("@tauri-apps/plugin-fs", () => ({
describe("HomeScreen", () => {
beforeEach(() => {
useConnectionStore.setState({ connections: [], folders: [], tags: [], loading: false, error: null });
useUiStore.setState({ searchQuery: "", activeFolderId: null, activeTagIds: [], activeDbTypes: [], activeView: "home" });
useUiStore.setState({ searchQuery: "", activeFolderId: null, activeTagIds: [], activeDbTypes: [], activeView: "home", selectedItemIds: [] });
useSettingsStore.setState({ settings: null, loading: false, error: null });
// SearchBar stores its debounce timer on window.__sb; clear any timer leaked
// by a previous test (e.g. typing a URL) so it can't fire mid-test.
window.clearTimeout((window as unknown as { __sb?: number }).__sb);
});
it("renders SearchBar and ActionRow", () => {
@@ -45,4 +54,117 @@ describe("HomeScreen", () => {
expect(useUiStore.getState().prefilledConnectionString).toBe("postgresql://user:pass@localhost:5432/mydb");
expect(useUiStore.getState().searchQuery).toBe("");
});
});
it("shows the confirmation dialog before bulk delete by default", async () => {
const user = userEvent.setup();
const conn = makeConnection("conn-1");
useConnectionStore.setState({ connections: [conn] });
useUiStore.setState({ selectedItemIds: ["conn-1"] });
render(<HomeScreen />);
await user.click(screen.getByRole("button", { name: /1 selected/i }));
await user.click(screen.getByRole("button", { name: /delete \(1\)/i }));
const { deleteConnection } = await import("../../lib/commands");
expect(
await screen.findByText(/are you sure you want to delete 1 item/i),
).toBeInTheDocument();
expect(deleteConnection).not.toHaveBeenCalled();
});
it("skips the confirmation and deletes selected connections when confirm_before_delete is false", async () => {
const user = userEvent.setup();
useSettingsStore.setState({
settings: {
...baseSettings(),
confirm_before_delete: false,
},
});
useConnectionStore.setState({ connections: [makeConnection("conn-1")] });
useUiStore.setState({ selectedItemIds: ["conn-1"] });
render(<HomeScreen />);
await user.click(screen.getByRole("button", { name: /1 selected/i }));
await user.click(screen.getByRole("button", { name: /delete \(1\)/i }));
const { deleteConnection } = await import("../../lib/commands");
expect(deleteConnection).toHaveBeenCalledWith("conn-1");
expect(screen.queryByText(/are you sure you want to delete/i)).not.toBeInTheDocument();
expect(screen.queryByTestId("animated-backdrop")).not.toBeInTheDocument();
});
it("shows the confirmation dialog before deleting a folder by default", async () => {
const user = userEvent.setup();
useConnectionStore.setState({ folders: [makeFolder("folder-1")] });
useUiStore.setState({ activeFolderId: "folder-1" });
render(<HomeScreen />);
await user.click(screen.getByRole("button", { name: /^delete$/i }));
expect(
await screen.findByText(/are you sure you want to delete \"projects\"/i),
).toBeInTheDocument();
});
it("skips the confirmation and deletes the folder when confirm_before_delete is false", async () => {
const user = userEvent.setup();
useSettingsStore.setState({
settings: {
...baseSettings(),
confirm_before_delete: false,
},
});
useConnectionStore.setState({ folders: [makeFolder("folder-1")] });
useUiStore.setState({ activeFolderId: "folder-1" });
render(<HomeScreen />);
await user.click(screen.getByRole("button", { name: /^delete$/i }));
const { deleteFolder } = await import("../../lib/commands");
expect(deleteFolder).toHaveBeenCalledWith("folder-1");
expect(screen.queryByText(/are you sure you want to delete/i)).not.toBeInTheDocument();
expect(screen.queryByTestId("animated-backdrop")).not.toBeInTheDocument();
});
});
function makeConnection(id: string): Connection {
return {
id,
name: "Local DB",
db_type: "postgresql",
host: "localhost",
port: 5432,
username: null,
folder_id: null,
keychain_ref: null,
tag_ids: [],
created_at: "",
updated_at: "",
};
}
function makeFolder(id: string): Folder {
return {
id,
name: "Projects",
parent_id: null,
tag_ids: [],
created_at: "",
updated_at: "",
};
}
function baseSettings() {
return {
confirm_before_delete: true,
default_folder_id: null,
theme: "dark" as const,
font_size: "medium" as const,
default_ports: { postgresql: 5432, mysql: 3306, redis: 6379, sqlite: null },
tag_order: null,
table_refresh_rate: 30,
table_page_size: 50,
shortcuts: {},
accent_color: "#2563EB",
};
}
+10 -3
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { DndContext, DragOverlay, closestCenter, type DragEndEvent } from "@dnd-kit/core";
import { useConnectionStore } from "../../stores/connectionStore";
import { useUiStore } from "../../stores/uiStore";
import { useSettingsStore } from "../../stores/settingsStore";
import { useFilteredConnections } from "../../hooks/useConnections";
import { useSortedTags } from "../../hooks/useSortedTags";
import { SearchBar } from "../search/SearchBar";
@@ -32,6 +33,8 @@ export function HomeScreen() {
const loadAll = useConnectionStore((s) => s.loadAll);
const selectedItemIds = useUiStore((s) => s.selectedItemIds);
const clearSelection = useUiStore((s) => s.clearSelection);
const confirmBeforeDelete =
useSettingsStore((s) => s.settings?.confirm_before_delete ?? true);
const [folderDialogOpen, setFolderDialogOpen] = useState(false);
const [editFolder, setEditFolder] = useState<Folder | null>(null);
const [confirmDelete, setConfirmDelete] = useState<{
@@ -146,7 +149,7 @@ export function HomeScreen() {
};
return (
<main className="min-h-screen p-6 bg-canvas select-none max-w-7xl mx-auto">
<main className="min-h-full p-6 bg-canvas select-none max-w-7xl mx-auto">
<div className="mb-6">
<SearchBar ref={searchRef} onDetectUrl={handleSearchUrl} />
</div>
@@ -161,7 +164,9 @@ export function HomeScreen() {
await handleExport();
}}
onDeleteSelected={() =>
setConfirmDelete({ type: "selected" })
confirmBeforeDelete
? setConfirmDelete({ type: "selected" })
: executeDeleteSelected()
}
visibleItemIds={visibleItemIds}
/>
@@ -185,7 +190,9 @@ export function HomeScreen() {
onOpenDbViewer={handleOpenDbViewer}
onEditFolder={(f) => setEditFolder(f)}
onDeleteFolder={(f) =>
setConfirmDelete({ type: "folder", folder: f })
confirmBeforeDelete
? setConfirmDelete({ type: "folder", folder: f })
: executeDeleteFolder(f)
}
/>
<DragOverlay dropAnimation={null}>