diff --git a/.gitignore b/.gitignore
index df7d56a..45cd856 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,8 +32,3 @@ logs/
# Claude Code local agent state
.claude/
-
-# .freebuff local tool state (not for commit)
-.freebuff/
-# graphify knowledge-graph output (local tool cache, not for commit)
-graphify-out/
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 03950c8..6f1479c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -19,7 +19,6 @@
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tauri-apps/cli": "^2",
- "@types/node": "^26.4.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
@@ -1309,16 +1308,6 @@
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"license": "MIT"
},
- "node_modules/@types/node": {
- "version": "26.4.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz",
- "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~8.3.0"
- }
- },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -1848,13 +1837,6 @@
"node": ">=14.17"
}
},
- "node_modules/undici-types": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
- "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/vite": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 62b0eb1..08d2563 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "bos-settings-frontend",
- "version": "0.8.2",
+ "version": "0.8.0",
"description": "Frontend for BOS Settings",
"type": "module",
"scripts": {
@@ -23,7 +23,6 @@
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tauri-apps/cli": "^2",
- "@types/node": "^26.4.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
diff --git a/frontend/src/lib/components/Embed.svelte b/frontend/src/lib/components/Embed.svelte
deleted file mode 100644
index 40090e5..0000000
--- a/frontend/src/lib/components/Embed.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-{@render children()}
diff --git a/frontend/src/lib/components/EmptyState.svelte b/frontend/src/lib/components/EmptyState.svelte
index aba4339..357d48f 100644
--- a/frontend/src/lib/components/EmptyState.svelte
+++ b/frontend/src/lib/components/EmptyState.svelte
@@ -16,8 +16,8 @@
flex-direction: column;
align-items: center;
gap: 6px;
- padding: 28px 8px;
- color: var(--muted);
+ padding: var(--space-xl, 20px) 0;
+ opacity: 0.7;
text-align: center;
}
diff --git a/frontend/src/lib/components/Group.svelte b/frontend/src/lib/components/Group.svelte
index fd11cb0..5a6caae 100644
--- a/frontend/src/lib/components/Group.svelte
+++ b/frontend/src/lib/components/Group.svelte
@@ -22,35 +22,33 @@
display: flex;
flex-direction: column;
min-width: 0;
- background: var(--surface);
- border: 1px solid var(--line, color-mix(in srgb, var(--fg) 8%, transparent));
- border-radius: var(--radius, 14px);
- padding: 16px 18px;
}
.group.wide {
- width: 100%;
+ grid-column: 1 / -1;
}
.title {
font-weight: 600;
- font-size: 13px;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- color: var(--muted, color-mix(in oklab, var(--fg) 58%, transparent));
- margin: 0 0 12px;
+ font-size: 1.05em;
+ margin: 0 0 var(--space-sm, 8px);
}
.hint {
- color: var(--muted, color-mix(in oklab, var(--fg) 58%, transparent));
+ opacity: 0.75;
font-size: var(--font-size-secondary, 12px);
line-height: 1.4;
- margin: -6px 0 12px;
+ margin: 0 0 var(--space-sm, 8px);
}
.rows {
display: flex;
flex-direction: column;
+ /* Flex children default to refusing to shrink below their content's
+ natural width (min-width: auto) — without this, a button/input
+ with enough text overflows past the grid column's actual pixel
+ width instead of wrapping, visually spilling into the next
+ column. */
min-width: 0;
}
diff --git a/frontend/src/lib/components/Hint.svelte b/frontend/src/lib/components/Hint.svelte
index e86d531..450e2ad 100644
--- a/frontend/src/lib/components/Hint.svelte
+++ b/frontend/src/lib/components/Hint.svelte
@@ -6,9 +6,13 @@
diff --git a/frontend/src/lib/components/Hub.svelte b/frontend/src/lib/components/Hub.svelte
deleted file mode 100644
index dd404a0..0000000
--- a/frontend/src/lib/components/Hub.svelte
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
- {#if tabs.length > 1}
- ({ id, label }))} bind:value={tab} />
- {/if}
-
-
diff --git a/frontend/src/lib/components/HyprColorField.svelte b/frontend/src/lib/components/HyprColorField.svelte
index cc9f430..115d4d7 100644
--- a/frontend/src/lib/components/HyprColorField.svelte
+++ b/frontend/src/lib/components/HyprColorField.svelte
@@ -1,7 +1,7 @@
-
- {label}
- {#if hint}{hint}{/if}
-
+
{label}
{@render children()}
diff --git a/frontend/src/lib/components/SaveButton.svelte b/frontend/src/lib/components/SaveButton.svelte
index faadab1..5dcd916 100644
--- a/frontend/src/lib/components/SaveButton.svelte
+++ b/frontend/src/lib/components/SaveButton.svelte
@@ -19,7 +19,7 @@
-
+
{status}
@@ -28,11 +28,28 @@
display: flex;
align-items: center;
gap: var(--space-md, 12px);
- margin-top: 4px;
+ margin-top: var(--space-lg, 16px);
+ /* Always spans the full grid width, regardless of how many
+ Group columns the rest of the page laid out above it. */
+ grid-column: 1 / -1;
+ }
+
+ button {
+ background-color: var(--accent);
+ color: var(--on-accent);
+ border: none;
+ border-radius: var(--radius-primary, 8px);
+ padding: var(--space-sm, 8px) var(--space-lg, 16px);
+ cursor: pointer;
+ }
+
+ button:disabled {
+ opacity: 0.5;
+ cursor: default;
}
.status {
- color: var(--muted, color-mix(in oklab, var(--fg) 58%, transparent));
+ opacity: 0.6;
font-size: var(--font-size-secondary, 12px);
}
diff --git a/frontend/src/lib/components/SelectField.svelte b/frontend/src/lib/components/SelectField.svelte
index feba79e..2dc50c6 100644
--- a/frontend/src/lib/components/SelectField.svelte
+++ b/frontend/src/lib/components/SelectField.svelte
@@ -1,25 +1,13 @@
-
+
@@ -30,8 +18,8 @@
background-color: var(--bg);
color: var(--on-surface);
border: 1px solid transparent;
- border-radius: var(--radius-sm, 10px);
- padding: 6px 10px;
+ border-radius: var(--radius-secondary, 6px);
+ padding: var(--space-xs, 4px) var(--space-sm, 8px);
}
option {
diff --git a/frontend/src/lib/components/ServiceControl.svelte b/frontend/src/lib/components/ServiceControl.svelte
index 9f3e8c0..1c81d29 100644
--- a/frontend/src/lib/components/ServiceControl.svelte
+++ b/frontend/src/lib/components/ServiceControl.svelte
@@ -30,7 +30,7 @@
function toggle() {
if (active && critical) {
- if (!confirm(`Stop ${unit}? Other bread apps may stall until it is restarted.`)) {
+ if (!confirm(`Stop ${unit}? This is a core part of the desktop's event handling — stopping it may affect other bread apps until it's restarted.`)) {
return;
}
}
@@ -57,7 +57,7 @@
{#if hasConfig}
-
+
{/if}
@@ -74,16 +74,16 @@
}
button {
- background-color: var(--surface-2, var(--bg));
+ background-color: var(--surface);
color: var(--on-surface);
- border: 1px solid var(--line, transparent);
- border-radius: var(--radius-sm, 10px);
+ border: none;
+ border-radius: var(--radius-primary, 8px);
padding: var(--space-sm, 8px) var(--space-lg, 16px);
cursor: pointer;
}
button:hover {
- background-color: color-mix(in srgb, var(--on-surface) 10%, transparent);
+ background-color: color-mix(in srgb, var(--on-surface) 14%, transparent);
}
button:disabled {
diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte
index d7c67e8..95fb43e 100644
--- a/frontend/src/lib/components/Sidebar.svelte
+++ b/frontend/src/lib/components/Sidebar.svelte
@@ -1,130 +1,59 @@
diff --git a/frontend/src/lib/components/SliderField.svelte b/frontend/src/lib/components/SliderField.svelte
deleted file mode 100644
index e3ee9a9..0000000
--- a/frontend/src/lib/components/SliderField.svelte
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
- onChange?.()} />
- {shown}{suffix}
-
-
-
diff --git a/frontend/src/lib/components/Subnav.svelte b/frontend/src/lib/components/Subnav.svelte
deleted file mode 100644
index acc782b..0000000
--- a/frontend/src/lib/components/Subnav.svelte
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
- {#each items as item (item.id)}
-
- {/each}
-
-
-
diff --git a/frontend/src/lib/components/Switch.svelte b/frontend/src/lib/components/Switch.svelte
index c62300d..e2504d3 100644
--- a/frontend/src/lib/components/Switch.svelte
+++ b/frontend/src/lib/components/Switch.svelte
@@ -11,40 +11,33 @@
aria-label={ariaLabel}
onclick={() => (value = !value)}
>
-
+
diff --git a/frontend/src/lib/components/SwitchField.svelte b/frontend/src/lib/components/SwitchField.svelte
index a8e2b19..a49caaf 100644
--- a/frontend/src/lib/components/SwitchField.svelte
+++ b/frontend/src/lib/components/SwitchField.svelte
@@ -2,13 +2,9 @@
import Row from "./Row.svelte";
import Switch from "./Switch.svelte";
- let {
- label,
- hint,
- value = $bindable(),
- }: { label: string; hint?: string; value: boolean } = $props();
+ let { label, value = $bindable() }: { label: string; value: boolean } = $props();
-
+
diff --git a/frontend/src/lib/components/TextField.svelte b/frontend/src/lib/components/TextField.svelte
index 1711e56..62fa918 100644
--- a/frontend/src/lib/components/TextField.svelte
+++ b/frontend/src/lib/components/TextField.svelte
@@ -18,8 +18,8 @@
background-color: var(--bg);
color: var(--on-surface);
border: 1px solid transparent;
- border-radius: var(--radius-sm, 10px);
- padding: 6px 10px;
+ border-radius: var(--radius-secondary, 6px);
+ padding: var(--space-xs, 4px) var(--space-sm, 8px);
}
input:focus {
diff --git a/frontend/src/lib/components/Titlebar.svelte b/frontend/src/lib/components/Titlebar.svelte
deleted file mode 100644
index 78fe93a..0000000
--- a/frontend/src/lib/components/Titlebar.svelte
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
-
-
-
-
-
Settings
-
-
Ctrl K
-
-
-
diff --git a/frontend/src/lib/components/ViewScaffold.svelte b/frontend/src/lib/components/ViewScaffold.svelte
index 1eef417..bb27e82 100644
--- a/frontend/src/lib/components/ViewScaffold.svelte
+++ b/frontend/src/lib/components/ViewScaffold.svelte
@@ -1,64 +1,40 @@
-{#if embedded}
-
+
+
{title}
+
{@render children()}
-{:else}
-
-
{title}
- {#if lede}
-
{lede}
- {/if}
-
- {@render children()}
-
-
-{/if}
+
diff --git a/frontend/src/lib/debounce.ts b/frontend/src/lib/debounce.ts
deleted file mode 100644
index 463b59b..0000000
--- a/frontend/src/lib/debounce.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export function debounce(fn: () => void, ms = 280): () => void {
- let t: ReturnType
| undefined;
- return () => {
- clearTimeout(t);
- t = setTimeout(fn, ms);
- };
-}
diff --git a/frontend/src/lib/embed.ts b/frontend/src/lib/embed.ts
deleted file mode 100644
index 86ff3ab..0000000
--- a/frontend/src/lib/embed.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-/** When set, ViewScaffold renders inner groups only - used by hub tab panes. */
-export const EMBEDDED_KEY = "bos-settings-embedded";
diff --git a/frontend/src/lib/nav.svelte.ts b/frontend/src/lib/nav.svelte.ts
deleted file mode 100644
index b7382c9..0000000
--- a/frontend/src/lib/nav.svelte.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { DEFAULT_PAGE, resolvePage } from "$lib/sidebar";
-
-/** Shared navigation - module `$state` so sidebar/search/hubs all see the same page. */
-export const nav = $state({
- page: DEFAULT_PAGE,
- tab: null as string | null,
- searchNonce: 0,
-});
-
-export function go(page: string, tab?: string) {
- const resolved = resolvePage(page);
- nav.tab = tab ?? resolved.tab ?? "";
- nav.page = resolved.page;
-}
-
-export type Navigate = (page: string, tab?: string) => void;
-export const NAVIGATE_KEY = "bos-settings-navigate";
diff --git a/frontend/src/lib/search.ts b/frontend/src/lib/search.ts
deleted file mode 100644
index 2b5f4e6..0000000
--- a/frontend/src/lib/search.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-export interface SearchHit {
- page: string;
- tab?: string;
- label: string;
- keywords: string;
-}
-
-export const SEARCH_INDEX: SearchHit[] = [
- { page: "home", label: "Home", keywords: "search overview status" },
- {
- page: "network",
- label: "Wi-Fi",
- keywords: "wifi ssid password scan radio ethernet hotspot dns",
- },
- {
- page: "network",
- tab: "vpn",
- label: "VPN / WireGuard",
- keywords: "vpn wireguard tailscale tunnel import conf",
- },
- {
- page: "network",
- tab: "breadcrumbs",
- label: "Saved networks",
- keywords: "wifi profiles breadcrumbs known ssid tailscale",
- },
- { page: "bluetooth", label: "Bluetooth", keywords: "bluetooth headphones pair scan mouse keyboard" },
- {
- page: "displays",
- label: "Displays",
- keywords: "monitor scale resolution arrange rotate vrr hdr hyprland",
- },
- {
- page: "displays",
- tab: "nightlight",
- label: "Night light",
- keywords: "night light hyprsunset temperature warmth sunset",
- },
- {
- page: "displays",
- tab: "breadmon",
- label: "Live arrange",
- keywords: "breadmon mirror profile arrange",
- },
- {
- page: "appearance",
- label: "Wallpaper",
- keywords: "wallpaper theme palette pywal breadpaper accent",
- },
- {
- page: "appearance",
- tab: "appearance",
- label: "Windows",
- keywords: "gaps blur rounding border shadow tiling dwindle",
- },
- { page: "sound", label: "Sound", keywords: "volume mute microphone output input speaker pavucontrol" },
- {
- page: "power",
- label: "Power & battery",
- keywords: "battery brightness charge tlp suspend idle lid",
- },
- {
- page: "input",
- label: "Keyboard & mouse",
- keywords: "keybind shortcut layout touchpad tap natural scroll follow mouse",
- },
- { page: "input", tab: "ime", label: "Input method", keywords: "fcitx5 ime cjk chinese japanese korean" },
- {
- page: "input",
- tab: "accessibility",
- label: "Accessibility",
- keywords: "orca screen reader zoom magnifier sticky keys",
- },
- {
- page: "desktop",
- label: "Bar",
- keywords: "breadbar modules clock tray workspaces",
- },
- { page: "desktop", tab: "breadbox", label: "Launcher", keywords: "breadbox launcher apps" },
- { page: "desktop", tab: "breadlock", label: "Lock screen", keywords: "lock greet breadlock idle" },
- { page: "desktop", tab: "breadshot", label: "Screenshots", keywords: "screenshot grim slurp breadshot" },
- { page: "desktop", tab: "autostart", label: "Startup apps", keywords: "autostart login startup" },
- { page: "desktop", tab: "breadclip", label: "Clipboard", keywords: "clipboard history breadclip" },
- { page: "desktop", tab: "breadpad", label: "Notes", keywords: "breadpad notes calendar caldav" },
- { page: "desktop", tab: "breadsearch", label: "File search", keywords: "breadsearch breadmill index" },
- { page: "desktop", tab: "bread", label: "Daemon", keywords: "breadd daemon adapters lua" },
- { page: "apps", label: "Default apps", keywords: "browser terminal pdf mime default" },
- { page: "apps", tab: "optional", label: "Optional software", keywords: "steam flatpak libreoffice" },
- { page: "apps", tab: "printing", label: "Printing", keywords: "cups printer" },
- { page: "privacy", label: "Firewall", keywords: "firewall ufw port allow" },
- { page: "privacy", tab: "users", label: "Users", keywords: "users password account" },
- { page: "system", label: "Updates", keywords: "update bakery pacman firmware nvidia" },
- { page: "system", tab: "packages", label: "Packages", keywords: "packages bakery install" },
- { page: "system", tab: "aur", label: "AUR", keywords: "aur yay" },
- { page: "system", tab: "snapshots", label: "Snapshots", keywords: "snapper snapshot rollback grub" },
- { page: "system", tab: "backup", label: "Backup", keywords: "restic backup restore" },
- { page: "system", tab: "channel", label: "Bakery channel", keywords: "track stable beta dev bakery" },
- { page: "about", label: "About", keywords: "hostname kernel cpu gpu about machine" },
- { page: "about", tab: "datetime", label: "Date & time", keywords: "timezone ntp clock date time" },
- { page: "desktop", tab: "breadhelp", label: "Help", keywords: "help breadhelp onboarding" },
-];
-
-export function searchSettings(query: string, limit = 8): SearchHit[] {
- const q = query.trim().toLowerCase();
- if (q.length < 1) return [];
- const scored = SEARCH_INDEX.map((hit) => {
- const hay = `${hit.label} ${hit.keywords} ${hit.page}`.toLowerCase();
- let score = 0;
- if (hit.label.toLowerCase().startsWith(q)) score += 8;
- if (hit.label.toLowerCase().includes(q)) score += 4;
- if (hay.includes(q)) score += 2;
- for (const part of q.split(/\s+/)) {
- if (hay.includes(part)) score += 1;
- }
- return { hit, score };
- })
- .filter((x) => x.score > 0)
- .sort((a, b) => b.score - a.score);
- return scored.slice(0, limit).map((x) => x.hit);
-}
diff --git a/frontend/src/lib/sidebar.ts b/frontend/src/lib/sidebar.ts
index f7d9d50..7d98ce6 100644
--- a/frontend/src/lib/sidebar.ts
+++ b/frontend/src/lib/sidebar.ts
@@ -1,112 +1,97 @@
+// Ports src/ui/sidebar.rs's declarative item lists verbatim. Grouped by task,
+// not "app vs system internals" — a user thinks "I want to change my Wi-Fi",
+// not "which of these is a bread-ecosystem app" (why breadcrumbs/Wi-Fi
+// Profiles lives in System, not Personalization).
+
import type { Component } from "svelte";
-import House from "@lucide/svelte/icons/house";
import Wifi from "@lucide/svelte/icons/wifi";
+import Network from "@lucide/svelte/icons/network";
import Bluetooth from "@lucide/svelte/icons/bluetooth";
-import Monitor from "@lucide/svelte/icons/monitor";
+import Shield from "@lucide/svelte/icons/shield";
import Volume2 from "@lucide/svelte/icons/volume-2";
import BatteryFull from "@lucide/svelte/icons/battery-full";
-import Palette from "@lucide/svelte/icons/palette";
-import AppWindow from "@lucide/svelte/icons/app-window";
+import Clock from "@lucide/svelte/icons/clock";
+import Monitor from "@lucide/svelte/icons/monitor";
import Keyboard from "@lucide/svelte/icons/keyboard";
+import Rocket from "@lucide/svelte/icons/rocket";
+import Users from "@lucide/svelte/icons/users";
+import Palette from "@lucide/svelte/icons/palette";
+import Image from "@lucide/svelte/icons/image";
import LayoutGrid from "@lucide/svelte/icons/layout-grid";
-import Shield from "@lucide/svelte/icons/shield";
-import Download from "@lucide/svelte/icons/download";
+import Grid3x3 from "@lucide/svelte/icons/grid-3x3";
+import Clipboard from "@lucide/svelte/icons/clipboard";
+import NotebookPen from "@lucide/svelte/icons/notebook-pen";
+import Search from "@lucide/svelte/icons/search";
+import Cog from "@lucide/svelte/icons/cog";
+import Package from "@lucide/svelte/icons/package";
+import RefreshCw from "@lucide/svelte/icons/refresh-cw";
+import History from "@lucide/svelte/icons/history";
import Info from "@lucide/svelte/icons/info";
+import Lock from "@lucide/svelte/icons/lock";
+import Camera from "@lucide/svelte/icons/camera";
+import AppWindow from "@lucide/svelte/icons/app-window";
+import CircleHelp from "@lucide/svelte/icons/circle-help";
export interface SidebarItem {
+ /** Must match a key in the view component map (see routing in +page.svelte). */
id: string;
label: string;
+ /** Dim second line — the underlying binary/config name, for items whose
+ * human label doesn't already make that obvious. */
+ sublabel?: string;
icon: Component;
}
+export const SYSTEM_ITEMS: SidebarItem[] = [
+ { id: "network", label: "Network", icon: Wifi },
+ { id: "breadcrumbs", label: "Wi-Fi Profiles", sublabel: "breadcrumbs", icon: Network },
+ { id: "bluetooth", label: "Bluetooth", icon: Bluetooth },
+ { id: "firewall", label: "Firewall", icon: Shield },
+ { id: "sound", label: "Sound", icon: Volume2 },
+ { id: "power", label: "Power", icon: BatteryFull },
+ { id: "datetime", label: "Date & Time", icon: Clock },
+ { id: "hyprland", label: "Display", sublabel: "monitors.json", icon: Monitor },
+ { id: "breadmon", label: "Monitors", sublabel: "breadmon", icon: AppWindow },
+ { id: "breadlock", label: "Lock & greet", sublabel: "breadlock", icon: Lock },
+ { id: "keybinds", label: "Keybinds", sublabel: "binds.json", icon: Keyboard },
+ { id: "breadshot", label: "Screenshots", sublabel: "breadshot", icon: Camera },
+ { id: "autostart", label: "Startup Apps", sublabel: "autostart.json", icon: Rocket },
+ { id: "users", label: "Users", icon: Users },
+];
+
+export const PERSONALIZATION_ITEMS: SidebarItem[] = [
+ { id: "appearance", label: "Appearance", sublabel: "settings.json", icon: Palette },
+ { id: "breadpaper", label: "Wallpaper", sublabel: "breadpaper", icon: Image },
+ { id: "breadbar", label: "Bar", sublabel: "breadbar", icon: LayoutGrid },
+ { id: "breadbox", label: "Launcher", sublabel: "breadbox", icon: Grid3x3 },
+ { id: "breadclip", label: "Clipboard", sublabel: "breadclipd", icon: Clipboard },
+ { id: "breadpad", label: "Notes", sublabel: "breadpad", icon: NotebookPen },
+ { id: "breadsearch", label: "File Search", sublabel: "breadsearch", icon: Search },
+ { id: "bread", label: "Daemon", sublabel: "breadd", icon: Cog },
+];
+
+export const MAINTENANCE_ITEMS: SidebarItem[] = [
+ { id: "packages", label: "Packages", icon: Package },
+ { id: "aur", label: "AUR", icon: Search },
+ { id: "firmware", label: "Firmware", icon: RefreshCw },
+ { id: "snapshots", label: "Snapshots", icon: History },
+];
+
+export const ABOUT_ITEMS: SidebarItem[] = [
+ { id: "breadhelp", label: "Help", sublabel: "breadhelp", icon: CircleHelp },
+ { id: "about", label: "About", icon: Info },
+];
+
export interface SidebarSection {
title: string | null;
items: SidebarItem[];
}
export const SIDEBAR_SECTIONS: SidebarSection[] = [
- { title: "Overview", items: [{ id: "home", label: "Home", icon: House }] },
- {
- title: "Devices",
- items: [
- { id: "network", label: "Wi-Fi & internet", icon: Wifi },
- { id: "bluetooth", label: "Bluetooth", icon: Bluetooth },
- { id: "displays", label: "Displays", icon: Monitor },
- { id: "sound", label: "Sound", icon: Volume2 },
- { id: "power", label: "Power & battery", icon: BatteryFull },
- ],
- },
- {
- title: "Desktop",
- items: [
- { id: "appearance", label: "Appearance", icon: Palette },
- { id: "desktop", label: "Bar & apps", icon: AppWindow },
- { id: "input", label: "Keyboard & mouse", icon: Keyboard },
- { id: "apps", label: "Default apps", icon: LayoutGrid },
- ],
- },
- {
- title: "System",
- items: [
- { id: "privacy", label: "Privacy & users", icon: Shield },
- { id: "system", label: "Updates & backup", icon: Download },
- { id: "about", label: "About", icon: Info },
- ],
- },
+ { title: "System", items: SYSTEM_ITEMS },
+ { title: "Personalization", items: PERSONALIZATION_ITEMS },
+ { title: "Maintenance", items: MAINTENANCE_ITEMS },
+ { title: null, items: ABOUT_ITEMS },
];
-/** Old sidebar ids (screenshot CLI, in-app jumps) → hub page + optional tab. */
-export const PAGE_ALIASES: Record = {
- home: { page: "home" },
- network: { page: "network" },
- vpn: { page: "network", tab: "vpn" },
- breadcrumbs: { page: "network", tab: "breadcrumbs" },
- bluetooth: { page: "bluetooth" },
- displays: { page: "displays" },
- hyprland: { page: "displays", tab: "hyprland" },
- nightlight: { page: "displays", tab: "nightlight" },
- breadmon: { page: "displays", tab: "breadmon" },
- sound: { page: "sound" },
- power: { page: "power" },
- appearance: { page: "appearance" },
- breadpaper: { page: "appearance", tab: "breadpaper" },
- desktop: { page: "desktop" },
- breadbar: { page: "desktop", tab: "breadbar" },
- breadbox: { page: "desktop", tab: "breadbox" },
- breadlock: { page: "desktop", tab: "breadlock" },
- breadshot: { page: "desktop", tab: "breadshot" },
- autostart: { page: "desktop", tab: "autostart" },
- breadclip: { page: "desktop", tab: "more" },
- breadpad: { page: "desktop", tab: "more" },
- breadsearch: { page: "desktop", tab: "more" },
- bread: { page: "desktop", tab: "more" },
- breadhelp: { page: "desktop", tab: "more" },
- more: { page: "desktop", tab: "more" },
- input: { page: "input" },
- keybinds: { page: "input", tab: "keybinds" },
- ime: { page: "input", tab: "ime" },
- accessibility: { page: "input", tab: "accessibility" },
- apps: { page: "apps" },
- defaults: { page: "apps" },
- optional: { page: "apps", tab: "optional" },
- printing: { page: "apps", tab: "printing" },
- privacy: { page: "privacy" },
- firewall: { page: "privacy" },
- users: { page: "privacy", tab: "users" },
- system: { page: "system" },
- updates: { page: "system" },
- packages: { page: "system", tab: "packages" },
- aur: { page: "system", tab: "aur" },
- firmware: { page: "system", tab: "firmware" },
- snapshots: { page: "system", tab: "snapshots" },
- backup: { page: "system", tab: "backup" },
- channel: { page: "system", tab: "channel" },
- about: { page: "about" },
- datetime: { page: "about", tab: "datetime" },
-};
-
-export const DEFAULT_PAGE = "home";
-
-export function resolvePage(id: string): { page: string; tab?: string } {
- return PAGE_ALIASES[id] ?? { page: id };
-}
+export const DEFAULT_PAGE = "about";
diff --git a/frontend/src/lib/streaming.ts b/frontend/src/lib/streaming.ts
index bf78f05..daaeee4 100644
--- a/frontend/src/lib/streaming.ts
+++ b/frontend/src/lib/streaming.ts
@@ -1,5 +1,5 @@
// Frontend half of the event-streaming command pattern (see
-// src/src/commands/streaming.rs) - listens for `cmd-output` lines from a
+// src/src/commands/streaming.rs) — listens for `cmd-output` lines from a
// typed Tauri command that runs a hardcoded program, then resolves once
// the process exits.
diff --git a/frontend/src/lib/styles/app.css b/frontend/src/lib/styles/app.css
deleted file mode 100644
index a626e8e..0000000
--- a/frontend/src/lib/styles/app.css
+++ /dev/null
@@ -1,121 +0,0 @@
-:root {
- --bg-2: color-mix(in oklab, var(--bg) 82%, var(--surface));
- --surface-2: color-mix(in oklab, var(--surface) 72%, var(--overlay));
- --muted: color-mix(in oklab, var(--fg) 58%, transparent);
- --faint: color-mix(in oklab, var(--fg) 38%, transparent);
- --line: color-mix(in srgb, var(--fg) 8%, transparent);
- --accent-soft: color-mix(in oklab, var(--accent) 18%, transparent);
- --radius: 14px;
- --radius-sm: 10px;
-}
-
-* {
- box-sizing: border-box;
-}
-
-html,
-body {
- margin: 0;
- border: none;
- outline: none;
- height: 100%;
- color-scheme: dark;
- background-color: var(--bg, #12161c);
- color: var(--fg);
- font-family: var(--font-family, sans-serif);
- font-size: var(--font-size-base, 14px);
-}
-
-#svelte {
- height: 100%;
-}
-
-button,
-input,
-select,
-textarea {
- font: inherit;
- color: inherit;
-}
-
-:focus {
- outline: none;
-}
-
-:focus-visible {
- outline: 2px solid color-mix(in oklab, var(--accent) 70%, white);
- outline-offset: 2px;
-}
-
-input[type="range"] {
- accent-color: var(--accent);
-}
-
-.btn {
- background: var(--surface-2, var(--surface));
- border: 1px solid var(--line);
- padding: 8px 14px;
- border-radius: var(--radius-sm, 10px);
- font-size: 13px;
- cursor: pointer;
- color: var(--fg);
-}
-
-.btn:hover {
- background: color-mix(in oklab, var(--surface-2, var(--surface)) 80%, var(--fg));
-}
-
-.btn.primary {
- background: var(--accent);
- color: var(--on-accent);
- border-color: transparent;
- font-weight: 600;
-}
-
-.btn.primary:hover {
- filter: brightness(1.06);
-}
-
-.btn.danger {
- color: var(--red);
-}
-
-.btn:disabled {
- opacity: 0.5;
- cursor: default;
-}
-
-.kbd {
- font-size: 11px;
- letter-spacing: 0.04em;
- background: color-mix(in srgb, var(--fg) 6%, transparent);
- border: 1px solid var(--line);
- padding: 2px 7px;
- border-radius: 6px;
- color: var(--muted);
-}
-
-.pills {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
-}
-
-.pill {
- display: inline-flex;
- align-items: center;
- padding: 6px 11px;
- border-radius: 999px;
- font-size: 12px;
- background: color-mix(in srgb, var(--fg) 6%, transparent);
- border: 1px solid var(--line);
- color: var(--fg);
- cursor: pointer;
-}
-
-.pill.on {
- background: var(--accent);
- color: var(--on-accent);
- border-color: transparent;
- font-weight: 600;
-}
diff --git a/frontend/src/lib/theme/index.ts b/frontend/src/lib/theme/index.ts
index 87e4b17..a358759 100644
--- a/frontend/src/lib/theme/index.ts
+++ b/frontend/src/lib/theme/index.ts
@@ -1,6 +1,6 @@
// Bridges bread-theme's pywal-derived palette into the webview. Mirrors
// bread_theme::gtk::apply_shared()'s two-phase pattern: fetch once at
-// startup, then keep it live via a backend-pushed event - see
+// startup, then keep it live via a backend-pushed event — see
// src-tauri/src/commands/theme.rs for the file-watch side of this.
import { invoke } from "@tauri-apps/api/core";
diff --git a/frontend/src/lib/views/About.svelte b/frontend/src/lib/views/About.svelte
index 43c0e56..894aca2 100644
--- a/frontend/src/lib/views/About.svelte
+++ b/frontend/src/lib/views/About.svelte
@@ -24,7 +24,7 @@
// The backend reports the GPU as a raw lspci device string, e.g.
// "Advanced Micro Devices, Inc. [AMD/ATI] Krackan [Radeon 840M / 860M Graphics] (rev c2)".
// That's too technical for a general settings page, so extract just the
- // marketing model name. Heuristic (not exhaustive - just needs to read
+ // marketing model name. Heuristic (not exhaustive — just needs to read
// well for common vendors): drop the trailing "(rev ..)", prefer the
// text inside the last [...] bracket group (usually the model name),
// collapse "840M / 860M" style multi-model lists to the last variant,
diff --git a/frontend/src/lib/views/AboutHub.svelte b/frontend/src/lib/views/AboutHub.svelte
deleted file mode 100644
index b4f7a2e..0000000
--- a/frontend/src/lib/views/AboutHub.svelte
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
diff --git a/frontend/src/lib/views/Accessibility.svelte b/frontend/src/lib/views/Accessibility.svelte
deleted file mode 100644
index ed4094b..0000000
--- a/frontend/src/lib/views/Accessibility.svelte
+++ /dev/null
@@ -1,197 +0,0 @@
-
-
-
-
- {#if !st}
-
- {:else if !st.orca_installed}
-
-
- {:else}
-
- Orca
-
-
- {/if}
-
-
-
- {#if st}
-
-
-
- {#if !st.kmag_installed}
-
-
- {:else}
-
- {/if}
- {/if}
-
-
-
- {#if st}
-
- Sticky keys
-
-
-
- Slow keys
-
-
-
- {/if}
-
- {#if message}{/if}
-
-
-
-
diff --git a/frontend/src/lib/views/Appearance.svelte b/frontend/src/lib/views/Appearance.svelte
index 2ef5fe2..b9d7541 100644
--- a/frontend/src/lib/views/Appearance.svelte
+++ b/frontend/src/lib/views/Appearance.svelte
@@ -5,12 +5,13 @@
import Group from "$lib/components/Group.svelte";
import SwitchField from "$lib/components/SwitchField.svelte";
import SelectField from "$lib/components/SelectField.svelte";
- import SliderField from "$lib/components/SliderField.svelte";
+ import NumberField from "$lib/components/NumberField.svelte";
import HyprColorField from "$lib/components/HyprColorField.svelte";
+ import Row from "$lib/components/Row.svelte";
import Hint from "$lib/components/Hint.svelte";
- import { debounce } from "$lib/debounce";
+ import SaveButton from "$lib/components/SaveButton.svelte";
- // Common XKB layout codes. Not exhaustive - if the config holds
+ // Common XKB layout codes. Not exhaustive — if the config holds
// something else, it's merged in below so it's never dropped from the
// dropdown.
const COMMON_KB_LAYOUTS = [
@@ -24,51 +25,13 @@
// window under the cursor; 2 = focus follows the cursor, but clicking a
// window keeps keyboard focus there until the mouse moves again; 3 =
// cursor and keyboard focus are fully independent.
- const FOLLOW_MOUSE_MODES: { value: string; label: string }[] = [
- { value: "0", label: "Click to focus" },
- { value: "1", label: "Follow mouse" },
- { value: "2", label: "Follow, keep focus on click" },
- { value: "3", label: "Mouse and keys independent" },
+ const FOLLOW_MOUSE_MODES: { value: number; label: string }[] = [
+ { value: 0, label: "Off — click to focus" },
+ { value: 1, label: "Follow mouse" },
+ { value: 2, label: "Follow mouse, detach on click" },
+ { value: 3, label: "Fully detached from keyboard focus" },
];
- const KB_LABELS: Record = {
- us: "English (US)",
- gb: "English (UK)",
- de: "German",
- fr: "French",
- es: "Spanish",
- it: "Italian",
- pt: "Portuguese",
- nl: "Dutch",
- se: "Swedish",
- no: "Norwegian",
- dk: "Danish",
- fi: "Finnish",
- pl: "Polish",
- cz: "Czech",
- sk: "Slovak",
- hu: "Hungarian",
- ro: "Romanian",
- gr: "Greek",
- tr: "Turkish",
- ru: "Russian",
- ua: "Ukrainian",
- jp: "Japanese",
- kr: "Korean",
- cn: "Chinese",
- br: "Portuguese (Brazil)",
- ca: "English (Canada)",
- ch: "German (Switzerland)",
- be: "Belgian",
- at: "German (Austria)",
- ie: "English (Ireland)",
- };
-
- const LAYOUT_LABELS: Record = {
- dwindle: "Tiling",
- master: "Master stack",
- };
-
interface Appearance {
gaps_in: number;
gaps_out: number;
@@ -90,73 +53,76 @@
}
let cfg = $state(null);
- let loaded = $state(false);
- let followMouse = $state("1");
let kbLayoutOptions = $derived(
cfg && !COMMON_KB_LAYOUTS.includes(cfg.kb_layout) ? [...COMMON_KB_LAYOUTS, cfg.kb_layout] : COMMON_KB_LAYOUTS,
);
- const persist = debounce(() => {
- if (!cfg) return;
- invoke("save_appearance", { appearance: { ...cfg, follow_mouse: Number(followMouse) } });
- }, 450);
-
onMount(async () => {
cfg = await invoke("get_appearance");
- followMouse = String(cfg.follow_mouse);
- loaded = true;
});
- let primed = false;
- $effect(() => {
- if (!loaded || !cfg) return;
- JSON.stringify(cfg);
- void followMouse;
- if (!primed) {
- primed = true;
- return;
- }
- persist();
- });
+ async function save() {
+ await invoke("save_appearance", { appearance: cfg });
+ }
{#if cfg}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
- {#if cfg.blur_enabled}
-
-
- {/if}
-
- {#if cfg.shadow_enabled}
-
- {/if}
+
+
+
+
+
-
-
- m.value)}
- labels={Object.fromEntries(FOLLOW_MOUSE_MODES.map((m) => [m.value, m.label]))}
- />
-
+
+
+
+
+
+
-
+
+
{/if}
+
+
diff --git a/frontend/src/lib/views/AppearanceHub.svelte b/frontend/src/lib/views/AppearanceHub.svelte
deleted file mode 100644
index 692f3ea..0000000
--- a/frontend/src/lib/views/AppearanceHub.svelte
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
diff --git a/frontend/src/lib/views/AppsHub.svelte b/frontend/src/lib/views/AppsHub.svelte
deleted file mode 100644
index 5b5c900..0000000
--- a/frontend/src/lib/views/AppsHub.svelte
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
diff --git a/frontend/src/lib/views/Aur.svelte b/frontend/src/lib/views/Aur.svelte
index ecf2ae0..11c035f 100644
--- a/frontend/src/lib/views/Aur.svelte
+++ b/frontend/src/lib/views/Aur.svelte
@@ -34,7 +34,7 @@
diff --git a/frontend/src/lib/views/Autostart.svelte b/frontend/src/lib/views/Autostart.svelte
index 914bf12..5bc5248 100644
--- a/frontend/src/lib/views/Autostart.svelte
+++ b/frontend/src/lib/views/Autostart.svelte
@@ -41,7 +41,7 @@
{#if entries}
{#each entries as entry, i (i)}
diff --git a/frontend/src/lib/views/Backup.svelte b/frontend/src/lib/views/Backup.svelte
deleted file mode 100644
index 32cc11b..0000000
--- a/frontend/src/lib/views/Backup.svelte
+++ /dev/null
@@ -1,254 +0,0 @@
-
-
-
-
- {#if !st}
-
- {:else if !st.restic_installed}
-
-
- {:else}
-
-
-
-
- {/if}
-
-
-
-
-
-
-
-
- {#if message}{/if}
-
-
-
-
-
-
-
-
-
-
-
- {#if snapshots.length === 0}
-
- {:else}
-
- {#each snapshots as s (s.id)}
-
- {/each}
-
- {/if}
-
-
-
-
-
diff --git a/frontend/src/lib/views/Bluetooth.svelte b/frontend/src/lib/views/Bluetooth.svelte
index 73bad27..49d5962 100644
--- a/frontend/src/lib/views/Bluetooth.svelte
+++ b/frontend/src/lib/views/Bluetooth.svelte
@@ -75,10 +75,7 @@
}
-
+
{#if powered === null}
{dev.name}{dev.connected ? " (connected)" : ""}
-
+
{/each}
@@ -110,7 +107,7 @@
{#if scanResults === null}
@@ -147,13 +144,9 @@
display: flex;
align-items: center;
gap: var(--space-sm, 8px);
- padding: 10px 2px;
- border-top: 1px solid var(--line);
- }
-
- .row:first-child {
- border-top: none;
- padding-top: 0;
+ background-color: var(--surface);
+ border-radius: var(--radius-secondary, 6px);
+ padding: var(--space-sm, 8px) var(--space-md, 12px);
}
.name {
diff --git a/frontend/src/lib/views/Bread.svelte b/frontend/src/lib/views/Bread.svelte
index a0de5cf..15fa6a9 100644
--- a/frontend/src/lib/views/Bread.svelte
+++ b/frontend/src/lib/views/Bread.svelte
@@ -34,7 +34,7 @@
let cfg = $state
(null);
// The daemon's 4 compiled-in modules plus every *.lua file actually
- // sitting in the configured module directory - real installed modules,
+ // sitting in the configured module directory — real installed modules,
// not a guess, so picking one to disable is a click.
let knownModules = $state([]);
diff --git a/frontend/src/lib/views/Breadbar.svelte b/frontend/src/lib/views/Breadbar.svelte
index 7748928..ee844ef 100644
--- a/frontend/src/lib/views/Breadbar.svelte
+++ b/frontend/src/lib/views/Breadbar.svelte
@@ -3,10 +3,10 @@
import { invoke } from "@tauri-apps/api/core";
import ViewScaffold from "$lib/components/ViewScaffold.svelte";
import Group from "$lib/components/Group.svelte";
+ import Row from "$lib/components/Row.svelte";
import TextField from "$lib/components/TextField.svelte";
- import SliderField from "$lib/components/SliderField.svelte";
- import Hint from "$lib/components/Hint.svelte";
- import { debounce } from "$lib/debounce";
+ import NumberField from "$lib/components/NumberField.svelte";
+ import SaveButton from "$lib/components/SaveButton.svelte";
import ChevronDown from "@lucide/svelte/icons/chevron-down";
import ChevronRight from "@lucide/svelte/icons/chevron-right";
@@ -23,33 +23,20 @@
}
let style = $state(null);
- let loaded = $state(false);
let advancedOpen = $state(false);
let css = $state("");
let cssStatus = $state("");
let cssSaving = $state(false);
- const persist = debounce(() => {
- if (!style) return;
- invoke("save_breadbar_style", { style }).then(() => invoke("get_breadbar_css").then((c) => (css = c)));
- }, 320);
-
onMount(async () => {
style = await invoke("get_breadbar_style");
css = await invoke("get_breadbar_css");
- loaded = true;
});
- let primed = false;
- $effect(() => {
- if (!loaded || !style) return;
- JSON.stringify(style);
- if (!primed) {
- primed = true;
- return;
- }
- persist();
- });
+ async function saveStyle() {
+ await invoke("save_breadbar_style", { style });
+ css = await invoke("get_breadbar_css");
+ }
async function saveCss() {
cssSaving = true;
@@ -68,37 +55,36 @@
{#if style}
-
+
-
+
-
-
+
+
-
-
-
+
+
+
+
+
+ {Math.round(style.workspace_inactive_opacity * 100)}%
+
+
-
-
-
-
+
+
+
+
-
+
{/if}
-
+
diff --git a/frontend/src/lib/views/Breadsearch.svelte b/frontend/src/lib/views/Breadsearch.svelte
index 1ac6880..c46fec9 100644
--- a/frontend/src/lib/views/Breadsearch.svelte
+++ b/frontend/src/lib/views/Breadsearch.svelte
@@ -48,7 +48,7 @@
{#if cfg}
@@ -56,7 +56,7 @@