diff --git a/.gitignore b/.gitignore index 45cd856..df7d56a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,8 @@ 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 6f1479c..03950c8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "@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", @@ -1308,6 +1309,16 @@ "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", @@ -1837,6 +1848,13 @@ "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 08d2563..62b0eb1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bos-settings-frontend", - "version": "0.8.0", + "version": "0.8.2", "description": "Frontend for BOS Settings", "type": "module", "scripts": { @@ -23,6 +23,7 @@ "@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 new file mode 100644 index 0000000..40090e5 --- /dev/null +++ b/frontend/src/lib/components/Embed.svelte @@ -0,0 +1,10 @@ + + +{@render children()} diff --git a/frontend/src/lib/components/EmptyState.svelte b/frontend/src/lib/components/EmptyState.svelte index 357d48f..aba4339 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: var(--space-xl, 20px) 0; - opacity: 0.7; + padding: 28px 8px; + color: var(--muted); text-align: center; } diff --git a/frontend/src/lib/components/Group.svelte b/frontend/src/lib/components/Group.svelte index 5a6caae..fd11cb0 100644 --- a/frontend/src/lib/components/Group.svelte +++ b/frontend/src/lib/components/Group.svelte @@ -22,33 +22,35 @@ 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 { - grid-column: 1 / -1; + width: 100%; } .title { font-weight: 600; - font-size: 1.05em; - margin: 0 0 var(--space-sm, 8px); + 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; } .hint { - opacity: 0.75; + color: var(--muted, color-mix(in oklab, var(--fg) 58%, transparent)); font-size: var(--font-size-secondary, 12px); line-height: 1.4; - margin: 0 0 var(--space-sm, 8px); + margin: -6px 0 12px; } .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 450e2ad..e86d531 100644 --- a/frontend/src/lib/components/Hint.svelte +++ b/frontend/src/lib/components/Hint.svelte @@ -6,13 +6,9 @@ diff --git a/frontend/src/lib/components/Hub.svelte b/frontend/src/lib/components/Hub.svelte new file mode 100644 index 0000000..dd404a0 --- /dev/null +++ b/frontend/src/lib/components/Hub.svelte @@ -0,0 +1,51 @@ + + + + {#if tabs.length > 1} + ({ id, label }))} bind:value={tab} /> + {/if} + + {#key tab} + {#if Active} + + {/if} + {/key} + + diff --git a/frontend/src/lib/components/HyprColorField.svelte b/frontend/src/lib/components/HyprColorField.svelte index 115d4d7..cc9f430 100644 --- a/frontend/src/lib/components/HyprColorField.svelte +++ b/frontend/src/lib/components/HyprColorField.svelte @@ -1,7 +1,7 @@
- {label} +
+ {label} + {#if hint}{hint}{/if} +
{@render children()}
diff --git a/frontend/src/lib/components/SaveButton.svelte b/frontend/src/lib/components/SaveButton.svelte index 5dcd916..faadab1 100644 --- a/frontend/src/lib/components/SaveButton.svelte +++ b/frontend/src/lib/components/SaveButton.svelte @@ -19,7 +19,7 @@
- + {status}
@@ -28,28 +28,11 @@ display: flex; align-items: center; gap: var(--space-md, 12px); - 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; + margin-top: 4px; } .status { - opacity: 0.6; + color: var(--muted, color-mix(in oklab, var(--fg) 58%, transparent)); font-size: var(--font-size-secondary, 12px); } diff --git a/frontend/src/lib/components/SelectField.svelte b/frontend/src/lib/components/SelectField.svelte index 2dc50c6..feba79e 100644 --- a/frontend/src/lib/components/SelectField.svelte +++ b/frontend/src/lib/components/SelectField.svelte @@ -1,13 +1,25 @@ - + @@ -18,8 +30,8 @@ background-color: var(--bg); color: var(--on-surface); border: 1px solid transparent; - border-radius: var(--radius-secondary, 6px); - padding: var(--space-xs, 4px) var(--space-sm, 8px); + border-radius: var(--radius-sm, 10px); + padding: 6px 10px; } option { diff --git a/frontend/src/lib/components/ServiceControl.svelte b/frontend/src/lib/components/ServiceControl.svelte index 1c81d29..9f3e8c0 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}? This is a core part of the desktop's event handling — stopping it may affect other bread apps until it's restarted.`)) { + if (!confirm(`Stop ${unit}? Other bread apps may stall until it is restarted.`)) { return; } } @@ -57,7 +57,7 @@ {#if hasConfig} - + {/if} @@ -74,16 +74,16 @@ } button { - background-color: var(--surface); + background-color: var(--surface-2, var(--bg)); color: var(--on-surface); - border: none; - border-radius: var(--radius-primary, 8px); + border: 1px solid var(--line, transparent); + border-radius: var(--radius-sm, 10px); padding: var(--space-sm, 8px) var(--space-lg, 16px); cursor: pointer; } button:hover { - background-color: color-mix(in srgb, var(--on-surface) 14%, transparent); + background-color: color-mix(in srgb, var(--on-surface) 10%, transparent); } button:disabled { diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte index 95fb43e..d7c67e8 100644 --- a/frontend/src/lib/components/Sidebar.svelte +++ b/frontend/src/lib/components/Sidebar.svelte @@ -1,59 +1,130 @@ diff --git a/frontend/src/lib/components/SliderField.svelte b/frontend/src/lib/components/SliderField.svelte new file mode 100644 index 0000000..e3ee9a9 --- /dev/null +++ b/frontend/src/lib/components/SliderField.svelte @@ -0,0 +1,44 @@ + + + + onChange?.()} /> + {shown}{suffix} + + + diff --git a/frontend/src/lib/components/Subnav.svelte b/frontend/src/lib/components/Subnav.svelte new file mode 100644 index 0000000..acc782b --- /dev/null +++ b/frontend/src/lib/components/Subnav.svelte @@ -0,0 +1,40 @@ + + + + + diff --git a/frontend/src/lib/components/Switch.svelte b/frontend/src/lib/components/Switch.svelte index e2504d3..c62300d 100644 --- a/frontend/src/lib/components/Switch.svelte +++ b/frontend/src/lib/components/Switch.svelte @@ -11,33 +11,40 @@ aria-label={ariaLabel} onclick={() => (value = !value)} > - + diff --git a/frontend/src/lib/components/SwitchField.svelte b/frontend/src/lib/components/SwitchField.svelte index a49caaf..a8e2b19 100644 --- a/frontend/src/lib/components/SwitchField.svelte +++ b/frontend/src/lib/components/SwitchField.svelte @@ -2,9 +2,13 @@ import Row from "./Row.svelte"; import Switch from "./Switch.svelte"; - let { label, value = $bindable() }: { label: string; value: boolean } = $props(); + let { + label, + hint, + value = $bindable(), + }: { label: string; hint?: string; value: boolean } = $props(); - + diff --git a/frontend/src/lib/components/TextField.svelte b/frontend/src/lib/components/TextField.svelte index 62fa918..1711e56 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-secondary, 6px); - padding: var(--space-xs, 4px) var(--space-sm, 8px); + border-radius: var(--radius-sm, 10px); + padding: 6px 10px; } input:focus { diff --git a/frontend/src/lib/components/Titlebar.svelte b/frontend/src/lib/components/Titlebar.svelte new file mode 100644 index 0000000..78fe93a --- /dev/null +++ b/frontend/src/lib/components/Titlebar.svelte @@ -0,0 +1,75 @@ + + +
+
+ + + +
+
Settings
+
+ Ctrl K +
+ + diff --git a/frontend/src/lib/components/ViewScaffold.svelte b/frontend/src/lib/components/ViewScaffold.svelte index bb27e82..1eef417 100644 --- a/frontend/src/lib/components/ViewScaffold.svelte +++ b/frontend/src/lib/components/ViewScaffold.svelte @@ -1,40 +1,64 @@ -
-

{title}

-
+{#if embedded} +
{@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 new file mode 100644 index 0000000..463b59b --- /dev/null +++ b/frontend/src/lib/debounce.ts @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..86ff3ab --- /dev/null +++ b/frontend/src/lib/embed.ts @@ -0,0 +1,2 @@ +/** 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 new file mode 100644 index 0000000..b7382c9 --- /dev/null +++ b/frontend/src/lib/nav.svelte.ts @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..2b5f4e6 --- /dev/null +++ b/frontend/src/lib/search.ts @@ -0,0 +1,120 @@ +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 7d98ce6..f7d9d50 100644 --- a/frontend/src/lib/sidebar.ts +++ b/frontend/src/lib/sidebar.ts @@ -1,97 +1,112 @@ -// 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 Shield from "@lucide/svelte/icons/shield"; +import Monitor from "@lucide/svelte/icons/monitor"; import Volume2 from "@lucide/svelte/icons/volume-2"; import BatteryFull from "@lucide/svelte/icons/battery-full"; -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 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"; +import Keyboard from "@lucide/svelte/icons/keyboard"; +import LayoutGrid from "@lucide/svelte/icons/layout-grid"; +import Shield from "@lucide/svelte/icons/shield"; +import Download from "@lucide/svelte/icons/download"; +import Info from "@lucide/svelte/icons/info"; 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: "System", items: SYSTEM_ITEMS }, - { title: "Personalization", items: PERSONALIZATION_ITEMS }, - { title: "Maintenance", items: MAINTENANCE_ITEMS }, - { title: null, items: ABOUT_ITEMS }, + { 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 }, + ], + }, ]; -export const DEFAULT_PAGE = "about"; +/** 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 }; +} diff --git a/frontend/src/lib/streaming.ts b/frontend/src/lib/streaming.ts index daaeee4..bf78f05 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 new file mode 100644 index 0000000..a626e8e --- /dev/null +++ b/frontend/src/lib/styles/app.css @@ -0,0 +1,121 @@ +: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 a358759..87e4b17 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 894aca2..43c0e56 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 new file mode 100644 index 0000000..b4f7a2e --- /dev/null +++ b/frontend/src/lib/views/AboutHub.svelte @@ -0,0 +1,14 @@ + + + diff --git a/frontend/src/lib/views/Accessibility.svelte b/frontend/src/lib/views/Accessibility.svelte new file mode 100644 index 0000000..ed4094b --- /dev/null +++ b/frontend/src/lib/views/Accessibility.svelte @@ -0,0 +1,197 @@ + + + + + {#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 b9d7541..2ef5fe2 100644 --- a/frontend/src/lib/views/Appearance.svelte +++ b/frontend/src/lib/views/Appearance.svelte @@ -5,13 +5,12 @@ import Group from "$lib/components/Group.svelte"; import SwitchField from "$lib/components/SwitchField.svelte"; import SelectField from "$lib/components/SelectField.svelte"; - import NumberField from "$lib/components/NumberField.svelte"; + import SliderField from "$lib/components/SliderField.svelte"; import HyprColorField from "$lib/components/HyprColorField.svelte"; - import Row from "$lib/components/Row.svelte"; import Hint from "$lib/components/Hint.svelte"; - import SaveButton from "$lib/components/SaveButton.svelte"; + import { debounce } from "$lib/debounce"; - // 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 = [ @@ -25,13 +24,51 @@ // 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: 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 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 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; @@ -53,76 +90,73 @@ } 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; }); - async function save() { - await invoke("save_appearance", { appearance: cfg }); - } + let primed = false; + $effect(() => { + if (!loaded || !cfg) return; + JSON.stringify(cfg); + void followMouse; + if (!primed) { + primed = true; + return; + } + persist(); + }); {#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 new file mode 100644 index 0000000..692f3ea --- /dev/null +++ b/frontend/src/lib/views/AppearanceHub.svelte @@ -0,0 +1,14 @@ + + + diff --git a/frontend/src/lib/views/AppsHub.svelte b/frontend/src/lib/views/AppsHub.svelte new file mode 100644 index 0000000..5b5c900 --- /dev/null +++ b/frontend/src/lib/views/AppsHub.svelte @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/lib/views/Aur.svelte b/frontend/src/lib/views/Aur.svelte index 11c035f..ecf2ae0 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 5bc5248..914bf12 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 new file mode 100644 index 0000000..32cc11b --- /dev/null +++ b/frontend/src/lib/views/Backup.svelte @@ -0,0 +1,254 @@ + + + + + {#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 49d5962..73bad27 100644 --- a/frontend/src/lib/views/Bluetooth.svelte +++ b/frontend/src/lib/views/Bluetooth.svelte @@ -75,7 +75,10 @@ } - + {#if powered === null} {dev.name}{dev.connected ? " (connected)" : ""} - +
{/each} @@ -107,7 +110,7 @@
{#if scanResults === null} @@ -144,9 +147,13 @@ display: flex; align-items: center; gap: var(--space-sm, 8px); - background-color: var(--surface); - border-radius: var(--radius-secondary, 6px); - padding: var(--space-sm, 8px) var(--space-md, 12px); + padding: 10px 2px; + border-top: 1px solid var(--line); + } + + .row:first-child { + border-top: none; + padding-top: 0; } .name { diff --git a/frontend/src/lib/views/Bread.svelte b/frontend/src/lib/views/Bread.svelte index 15fa6a9..a0de5cf 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 ee844ef..7748928 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 NumberField from "$lib/components/NumberField.svelte"; - import SaveButton from "$lib/components/SaveButton.svelte"; + import SliderField from "$lib/components/SliderField.svelte"; + import Hint from "$lib/components/Hint.svelte"; + import { debounce } from "$lib/debounce"; import ChevronDown from "@lucide/svelte/icons/chevron-down"; import ChevronRight from "@lucide/svelte/icons/chevron-right"; @@ -23,20 +23,33 @@ } 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; }); - async function saveStyle() { - await invoke("save_breadbar_style", { style }); - css = await invoke("get_breadbar_css"); - } + let primed = false; + $effect(() => { + if (!loaded || !style) return; + JSON.stringify(style); + if (!primed) { + primed = true; + return; + } + persist(); + }); async function saveCss() { cssSaving = true; @@ -55,36 +68,37 @@ {#if style} - + - + - - + + - - - -
- - {Math.round(style.workspace_inactive_opacity * 100)}% -
-
+ + + - - - - + + + + - + {/if} - + + {/each} +
+ {/if}
- - - - - - {#if rules} - - {#each rules as rule, i (i)} -
- - - - - + {#if live} + + + {#if live.available_modes.length > 0} + + {:else} + {live.mode} + {/if} + + +
+ {#each SCALES as s (s.value)} + + {/each}
- {/each} - - - +
+ +
+ {#each TURNS as t (t.value)} + + {/each} +
+
+ + {live.x}, {live.y} + + {#if message} + + {/if}
{/if} diff --git a/frontend/src/lib/views/DisplaysHub.svelte b/frontend/src/lib/views/DisplaysHub.svelte new file mode 100644 index 0000000..3e86d77 --- /dev/null +++ b/frontend/src/lib/views/DisplaysHub.svelte @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/lib/views/Firewall.svelte b/frontend/src/lib/views/Firewall.svelte index 9bbdca0..aba18bf 100644 --- a/frontend/src/lib/views/Firewall.svelte +++ b/frontend/src/lib/views/Firewall.svelte @@ -1,4 +1,5 @@ - + + {/each} +
+ {:else} +
+ + + +
+ +
+ {#each tiles as tile (tile.id + (tile.tab ?? ""))} + + {/each} +
+ {/if} +
+ + diff --git a/frontend/src/lib/views/InputHub.svelte b/frontend/src/lib/views/InputHub.svelte new file mode 100644 index 0000000..e9a7664 --- /dev/null +++ b/frontend/src/lib/views/InputHub.svelte @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/lib/views/InputMethod.svelte b/frontend/src/lib/views/InputMethod.svelte new file mode 100644 index 0000000..c485eb5 --- /dev/null +++ b/frontend/src/lib/views/InputMethod.svelte @@ -0,0 +1,166 @@ + + + + + {#if !st} + + {:else} +
+ Enable fcitx5 for this session + +
+ + + {/if} + {#if message}{/if} +
+ + + {#if st} +
    + {#each st.packages as p (p.name)} +
  • {p.name}{p.installed ? "" : " (missing)"}
  • + {/each} +
+ {/if} + {#if missing.length} + + {/if} +
+ +
+ + diff --git a/frontend/src/lib/views/Keybinds.svelte b/frontend/src/lib/views/Keybinds.svelte index dc710cf..07b7b70 100644 --- a/frontend/src/lib/views/Keybinds.svelte +++ b/frontend/src/lib/views/Keybinds.svelte @@ -9,7 +9,7 @@ import SaveButton from "$lib/components/SaveButton.svelte"; import Switch from "$lib/components/Switch.svelte"; - // Mirrors src-tauri/src/commands/keybinds.rs's `Bind` — `action`/`key`/ + // Mirrors src-tauri/src/commands/keybinds.rs's `Bind` - `action`/`key`/ // `mods` are the only fields every bind has; everything else (`command`, // `direction`, `workspace`, `options`, breadhelp's `label`/`category`/ // `demo_cmd`, ...) round-trips through serde's `#[serde(flatten)]` as @@ -35,7 +35,7 @@ } // Every action actually seen in real binds.json files (BOS's shipped - // flat schema and this app's own dev MultiLayout config) — an `action` + // flat schema and this app's own dev MultiLayout config) - an `action` // dropdown beats free-typing a dispatcher name from memory. "Custom…" // keeps anything not in this list reachable without blocking on it. const KNOWN_ACTIONS = [ @@ -56,7 +56,7 @@ ] as const; // The editable, per-row UI state a bind gets flattened into on load and - // reconstructed back into a `Bind` on save — same trade-off the GTK + // reconstructed back into a `Bind` on save - same trade-off the GTK // version made with individual `Entry` widgets per field. interface EditRow { action: string; @@ -64,7 +64,7 @@ mods: string; // Whether `mods` has been explicitly set (present in the loaded JSON, // or touched by the user since). `None`/absent means "fall back to - // default_mods"; `Some([])` — an explicitly *empty* mods list — means + // default_mods"; `Some([])` - an explicitly *empty* mods list - means // "use no modifiers at all, even though default_mods exists" (real // BOS binds rely on this for e.g. bare media keys). Collapsing both // cases to "omit when empty" would silently turn an explicit @@ -77,12 +77,123 @@ // key they don't know about (label/category/demo_cmd, or an action // shape this editor has no dedicated fields for) survives untouched. extraValue: Record; - // Raw-JSON view of `extraValue`, kept in sync both directions — + // Raw-JSON view of `extraValue`, kept in sync both directions - // only actually shown when `advancedOpen` is true, or for an action // not in `KNOWN_ACTIONS` (nothing dedicated to show instead). extraText: string; extraError: boolean; advancedOpen: boolean; + capturing: boolean; + } + + const ACTION_LABELS: Record = { + exec: "Run command", + close: "Close window", + fullscreen: "Fullscreen", + float: "Float window", + pseudo: "Fake fullscreen", + resize: "Resize", + focus: "Go to workspace", + focus_last: "Last workspace", + move: "Move window to workspace", + move_dir: "Move window", + resize_dir: "Resize window", + layout: "Layout", + drag: "Mouse drag", + exit: "Exit Hyprland", + }; + + const KEY_LABELS: Record = { + RETURN: "Enter", + SPACE: "Space", + ESCAPE: "Esc", + BACKSPACE: "Backspace", + TAB: "Tab", + SUPER: "Super", + CTRL: "Ctrl", + ALT: "Alt", + SHIFT: "Shift", + XF86AudioRaiseVolume: "Vol +", + XF86AudioLowerVolume: "Vol -", + XF86AudioMute: "Mute", + XF86AudioMicMute: "Mic mute", + XF86MonBrightnessUp: "Bright +", + XF86MonBrightnessDown: "Bright -", + XF86AudioNext: "Next", + XF86AudioPrev: "Prev", + XF86AudioPlay: "Play", + Print: "Print", + }; + + const CODE_TO_HYPR: Record = { + Space: "SPACE", + Enter: "RETURN", + Escape: "ESCAPE", + Backspace: "BACKSPACE", + Tab: "TAB", + AudioVolumeUp: "XF86AudioRaiseVolume", + AudioVolumeDown: "XF86AudioLowerVolume", + AudioVolumeMute: "XF86AudioMute", + AudioMicMute: "XF86AudioMicMute", + BrightnessUp: "XF86MonBrightnessUp", + BrightnessDown: "XF86MonBrightnessDown", + MediaTrackNext: "XF86AudioNext", + MediaTrackPrevious: "XF86AudioPrev", + MediaPlayPause: "XF86AudioPlay", + PrintScreen: "Print", + }; + + function keyLabel(k: string): string { + return KEY_LABELS[k] ?? k; + } + + function bindTitle(row: EditRow): string { + if (row.action === "exec") { + const cmd = String(row.extraValue.command ?? ""); + if (cmd.includes("set-volume") && cmd.includes("+")) return "Volume up"; + if (cmd.includes("set-volume") && cmd.includes("-")) return "Volume down"; + if (cmd.includes("set-mute") && cmd.includes("SINK")) return "Mute"; + if (cmd.includes("set-mute") && cmd.includes("SOURCE")) return "Mute mic"; + if (cmd.includes("brightnessctl") && cmd.includes("+")) return "Brightness up"; + if (cmd.includes("brightnessctl") && cmd.includes("-")) return "Brightness down"; + if (cmd.includes("playerctl next")) return "Next track"; + if (cmd.includes("playerctl previous")) return "Previous track"; + if (cmd.includes("playerctl play-pause")) return "Play/pause"; + if (cmd) return cmd.split(/\s+/)[0].split("/").pop() ?? "Command"; + } + return ACTION_LABELS[row.action] ?? row.action; + } + + function keyChips(row: EditRow): string[] { + const mods = textToMods(row.mods); + const key = row.key.trim(); + return [...mods, key].filter(Boolean).map(keyLabel); + } + + function hyprFromEvent(e: KeyboardEvent): { mods: string[]; key: string } | null { + if (["Shift", "Control", "Alt", "Meta"].includes(e.key)) return null; + const mods: string[] = []; + if (e.metaKey) mods.push("SUPER"); + if (e.ctrlKey) mods.push("CTRL"); + if (e.altKey) mods.push("ALT"); + if (e.shiftKey) mods.push("SHIFT"); + let key = CODE_TO_HYPR[e.code] ?? CODE_TO_HYPR[e.key]; + if (!key) { + if (e.code.startsWith("Key") && e.code.length === 4) key = e.code.slice(3); + else if (e.code.startsWith("Digit")) key = e.code.slice(5); + else if (e.code.startsWith("F") && /^F\d+$/.test(e.code)) key = e.code; + else key = e.key.length === 1 ? e.key.toUpperCase() : e.key; + } + return { mods, key }; + } + + function allRows(): EditRow[] { + return [...globalsRows, ...commonRows, ...bindingsRows, ...Object.values(layoutRows).flat()]; + } + + function beginCapture(row: EditRow) { + for (const r of allRows()) r.capturing = false; + row.capturing = true; } function syncExtraText(row: EditRow) { @@ -114,7 +225,7 @@ } // Hyprland workspace refs mix bare integers ("1") and relative tokens - // ("e+1", "e-1") in the same field — keep whichever shape the user typed + // ("e+1", "e-1") in the same field - keep whichever shape the user typed // instead of forcing everything through one type. function parseWorkspaceValue(s: string): string | number | undefined { const trimmed = s.trim(); @@ -143,13 +254,14 @@ extraText: "", extraError: false, advancedOpen: false, + capturing: false, }; syncExtraText(row); return row; } function rowToBind(r: EditRow): Bind { - // `drag` binds are only meaningful as a mouse bind — there's no + // `drag` binds are only meaningful as a mouse bind - there's no // dedicated field for `options.mouse` (nothing to configure, it's // always true), so pin it here rather than exposing a checkbox // whose only correct state is "on". @@ -193,6 +305,7 @@ extraText: "", extraError: false, advancedOpen: false, + capturing: false, }; } @@ -208,13 +321,32 @@ let newLayoutName = $state(""); let loadError = $state(""); - onMount(async () => { + onMount(() => { + const onKey = (e: KeyboardEvent) => { + const row = allRows().find((r) => r.capturing); + if (!row) return; + const mapped = hyprFromEvent(e); + if (!mapped) return; + e.preventDefault(); + row.mods = mapped.mods.join(", "); + row.modsTouched = true; + row.key = mapped.key; + row.capturing = false; + }; + window.addEventListener("keydown", onKey); + void loadKeybinds(); + // Register cleanup synchronously so Svelte types the onMount + // callback as returning a function (not a Promise-of-function). + return () => window.removeEventListener("keydown", onKey); + }); + + async function loadKeybinds() { try { const p = await invoke("get_keybinds"); kind = p.kind; // Rust's `#[serde(skip_serializing_if = ...)]` on every one of // these fields means an empty one is OMITTED from the JSON - // entirely, not sent as `[]`/`{}`/`""` — every field here needs + // entirely, not sent as `[]`/`{}`/`""` - every field here needs // a `??` fallback, not just the ones that are "usually" empty. activeLayout = p.file.active_layout ?? ""; defaultMods = modsToText(p.file.default_mods ?? []); @@ -229,7 +361,7 @@ } catch (e) { loadError = String(e); } - }); + } function addLayout() { const name = newLayoutName.trim(); @@ -248,7 +380,7 @@ // A row whose "extra" column currently holds text that doesn't parse as // JSON keeps its *last successfully parsed* value in `extraValue` (see // `onExtraInput`) rather than losing it on every keystroke while the - // user is mid-edit — but that means saving while such a row is still + // user is mid-edit - but that means saving while such a row is still // showing invalid/incomplete text would silently write that stale (or, // for a brand new row, empty) value instead of what's actually on // screen. Block save entirely until every row's extra JSON is valid, so @@ -349,7 +481,7 @@ oninput={(e) => setExtra(row, "layout", e.currentTarget.value)} /> {:else if row.action === "drag"} - Mouse-drag bind — nothing else to set. + Mouse-drag bind. {:else if row.action === "close" || row.action === "fullscreen" || row.action === "float" || row.action === "pseudo" || row.action === "resize" || row.action === "focus_last" || row.action === "exit"} No extra options for this action. {:else} @@ -375,30 +507,19 @@ {#each rows as row, i (i)}
- (row.modsTouched = true)} - /> - - - {#if !KNOWN_ACTIONS.includes(row.action as (typeof KNOWN_ACTIONS)[number])} - - {/if} + {bindTitle(row)} +
@@ -436,7 +557,7 @@ {:else if kind === "flat"} @@ -453,7 +574,7 @@ {:else} {#if layoutOrder.length > 0} @@ -512,9 +633,13 @@ display: flex; flex-direction: column; gap: var(--space-xs, 4px); - background-color: var(--surface); - border-radius: var(--radius-secondary, 6px); - padding: var(--space-sm, 8px) var(--space-md, 12px); + padding: 10px 2px; + border-top: 1px solid var(--line); + } + + .bind-card:first-child { + border-top: none; + padding-top: 0; } .bind-top, @@ -525,6 +650,34 @@ flex-wrap: wrap; } + .what { + flex: 1; + min-width: 12ch; + } + + .capture { + display: flex; + align-items: center; + gap: 4px; + background: color-mix(in srgb, var(--fg) 6%, transparent); + border: 1px solid var(--line); + border-radius: 10px; + padding: 6px 10px; + color: inherit; + cursor: pointer; + min-height: 32px; + } + + .capture.listening { + border-color: var(--accent); + color: var(--accent); + } + + .plus { + opacity: 0.4; + padding: 0 1px; + } + .bind-card input, .bind-card select { background-color: var(--bg); @@ -544,26 +697,6 @@ border-color: var(--red); } - .mods { - width: 14ch; - flex-shrink: 0; - } - - .key { - width: 9ch; - flex-shrink: 0; - } - - .action-select { - width: 11ch; - flex-shrink: 0; - } - - .action-custom { - width: 11ch; - flex-shrink: 0; - } - .extra-wide { flex: 1; min-width: 16ch; diff --git a/frontend/src/lib/views/Network.svelte b/frontend/src/lib/views/Network.svelte index 0a8e788..b8e2f1b 100644 --- a/frontend/src/lib/views/Network.svelte +++ b/frontend/src/lib/views/Network.svelte @@ -3,9 +3,8 @@ 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 SwitchField from "$lib/components/SwitchField.svelte"; import InfoRow from "$lib/components/InfoRow.svelte"; - import Hint from "$lib/components/Hint.svelte"; import Wifi from "@lucide/svelte/icons/wifi"; import WifiOff from "@lucide/svelte/icons/wifi-off"; import Lock from "@lucide/svelte/icons/lock"; @@ -26,12 +25,6 @@ let pendingSsid = $state(null); let password = $state(""); - // The backend reports the first `nmcli`-visible device of TYPE=ethernet, - // which on a machine running containers/VMs can be a virtual interface - // (docker/podman veth pairs, libvirt bridges, VPN tuns) rather than a - // real NIC — nmcli doesn't distinguish these from physical ethernet. - // Names like "vethYCF3ZA: unmanaged" are meaningless to a non-technical - // user, so hide the card rather than show raw interface jargon. const VIRTUAL_IFACE_PREFIXES = ["veth", "docker", "br-", "virbr", "vnet", "tun", "tap", "vmnet", "podman"]; const ETHERNET_STATE_LABELS: Record = { connected: "Connected", @@ -47,11 +40,14 @@ if (!iface || VIRTUAL_IFACE_PREFIXES.some((prefix) => iface.startsWith(prefix))) return null; return ETHERNET_STATE_LABELS[rawState ?? ""] ?? "Not connected"; }); + let activeNet = $derived(networks?.find((n) => n.active) ?? null); + let nearby = $derived(networks?.filter((n) => !n.active) ?? []); onMount(async () => { const info = await invoke<{ radio_enabled: boolean; ethernet: string | null }>("get_network_info"); radioEnabled = info.radio_enabled; ethernet = info.ethernet; + if (info.radio_enabled) await scan(); }); async function toggleRadio(enabled: boolean) { @@ -61,10 +57,12 @@ async function scan() { scanning = true; - status = "Scanning…"; - networks = await invoke("scan_wifi"); - status = `Found ${networks.length} network(s)`; - scanning = false; + try { + networks = await invoke("scan_wifi"); + } finally { + scanning = false; + status = ""; + } } async function connect(ssid: string, secured: boolean, known: boolean) { @@ -76,8 +74,9 @@ try { await invoke("connect_wifi", { ssid, password: null }); status = `Connected to ${ssid}`; + await scan(); } catch (e) { - status = `Failed to connect to ${ssid}: ${e}`; + status = `Failed: ${e}`; } } @@ -90,54 +89,76 @@ try { await invoke("connect_wifi", { ssid, password: pw }); status = `Connected to ${ssid}`; + await scan(); } catch { - status = `Failed to connect to ${ssid}: wrong password?`; + status = "Wrong password?"; } } - function signalLabel(signal: number): string { - return `${Math.min(100, Math.max(0, signal))}%`; + function bars(signal: number): number { + if (signal >= 75) return 4; + if (signal >= 50) return 3; + if (signal >= 25) return 2; + return 1; } - - - - - + + + radioEnabled, (v) => toggleRadio(v)} /> + {#if ethernetLabel} + + {/if} - {#if ethernetLabel} - - + {#if activeNet} + +
+
+ {#each [1, 2, 3, 4] as n (n)} + = n}> + {/each} +
+
+ {activeNet.ssid} + {activeNet.secured ? "Secured" : "Open"} · {activeNet.signal}% +
+ Connected +
{/if} - +
- {#if networks === null} + {#if scanning && networks === null}
- - Not scanned yet - Press Scan to see nearby networks. + + Scanning…
- {:else if networks.length === 0} + {:else if networks === null}
- + + No scan yet +
+ {:else if nearby.length === 0 && !activeNet} +
+ No networks found - Try Scan again, or check Wi-Fi radio is on.
{:else} - {#each networks as net (net.ssid)} -
- {net.ssid}{net.active ? " (connected)" : ""} + {#each nearby as net (net.ssid)} +
+
+ {#each [1, 2, 3, 4] as n (n)} + = n}> + {/each} +
+
+ {net.ssid} + {net.known ? "Saved" : net.secured ? "Secured" : "Open"} +
{#if net.secured}{/if} - {signalLabel(net.signal)} - {#if !net.active} - - {/if} +
{/each} {/if} @@ -145,52 +166,28 @@ {#if pendingSsid}
- - + +
{/if} {#if status} {status} {/if} - - + - - + + diff --git a/frontend/src/lib/views/NetworkHub.svelte b/frontend/src/lib/views/NetworkHub.svelte new file mode 100644 index 0000000..da2b0fd --- /dev/null +++ b/frontend/src/lib/views/NetworkHub.svelte @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/lib/views/NightLight.svelte b/frontend/src/lib/views/NightLight.svelte new file mode 100644 index 0000000..8f07737 --- /dev/null +++ b/frontend/src/lib/views/NightLight.svelte @@ -0,0 +1,85 @@ + + + + + {#if !st} + + {:else if !st.installed} + + + {:else} + st!.enabled, (v) => { st!.enabled = v; applyNow(); }} + /> + {#if st.enabled} + + {/if} + {/if} + {#if message}{/if} + + + diff --git a/frontend/src/lib/views/Optional.svelte b/frontend/src/lib/views/Optional.svelte new file mode 100644 index 0000000..882d537 --- /dev/null +++ b/frontend/src/lib/views/Optional.svelte @@ -0,0 +1,149 @@ + + + + + {#if !st} + + {:else} + {#each st.items as item (item.id)} +
+
+ {item.title} +

{item.detail}

+ {item.via}{item.installed ? " · installed" : ""} +
+ {#if item.installed} + Installed + {:else} + + {/if} +
+ {/each} + {/if} + {#if message}{/if} +
+ +
+ + diff --git a/frontend/src/lib/views/Packages.svelte b/frontend/src/lib/views/Packages.svelte index d28509f..ec20f80 100644 --- a/frontend/src/lib/views/Packages.svelte +++ b/frontend/src/lib/views/Packages.svelte @@ -85,7 +85,7 @@ diff --git a/frontend/src/lib/views/Power.svelte b/frontend/src/lib/views/Power.svelte index d81ea9c..dcb50dd 100644 --- a/frontend/src/lib/views/Power.svelte +++ b/frontend/src/lib/views/Power.svelte @@ -40,7 +40,10 @@ } - + {#if info} {#each info.battery as [label, value] (label)} @@ -63,20 +66,22 @@ {#if info.charge_start !== null && info.charge_end !== null} - - setChargeThreshold("start", chargeStart)} /> + + setChargeThreshold("start", chargeStart)} /> + {chargeStart}% - - setChargeThreshold("end", chargeEnd)} /> + + setChargeThreshold("end", chargeEnd)} /> + {chargeEnd}% {/if} @@ -88,15 +93,6 @@ width: 180px; } - input[type="number"] { - width: 8ch; - background-color: var(--bg); - color: var(--on-surface); - border: 1px solid transparent; - border-radius: var(--radius-secondary, 6px); - padding: var(--space-xs, 4px) var(--space-sm, 8px); - } - .pct { margin-left: var(--space-sm, 8px); font-size: var(--font-size-secondary, 12px); diff --git a/frontend/src/lib/views/Printing.svelte b/frontend/src/lib/views/Printing.svelte new file mode 100644 index 0000000..2fc3ac5 --- /dev/null +++ b/frontend/src/lib/views/Printing.svelte @@ -0,0 +1,147 @@ + + + + + {#if !status} + + {:else if status.error} + + {:else if status.printers.length === 0} + + {:else} +
+ {#each status.printers as p (p.name)} +
+
+ {p.name}{p.is_default ? " (default)" : ""} + {p.enabled ? p.status : "disabled"} +
+ +
+ {/each} +
+ {/if} +
+ + +
+ {#if message}{/if} +
+ + + + + + +
+ + diff --git a/frontend/src/lib/views/PrivacyHub.svelte b/frontend/src/lib/views/PrivacyHub.svelte new file mode 100644 index 0000000..e0f7ef1 --- /dev/null +++ b/frontend/src/lib/views/PrivacyHub.svelte @@ -0,0 +1,14 @@ + + + diff --git a/frontend/src/lib/views/Snapshots.svelte b/frontend/src/lib/views/Snapshots.svelte index 8687a00..592f82a 100644 --- a/frontend/src/lib/views/Snapshots.svelte +++ b/frontend/src/lib/views/Snapshots.svelte @@ -25,7 +25,7 @@ } catch (e) { const msg = `${e}`.toLowerCase(); if (msg.includes("no permission")) { - errorHint = "This user isn't allowed to run snapper. Check ALLOW_USERS in /etc/snapper/configs/root — it should list your username."; + errorHint = "This user is not in ALLOW_USERS for snapper."; } else if (msg.includes("unknown config") || msg.includes("no such file")) { errorHint = "No snapper config exists for root yet, so nothing is being snapshotted. This should be set up automatically at install."; } else { @@ -42,7 +42,7 @@ if (!selected) return; if ( confirm( - `Boot into snapshot #${selected}? Snapshots on BOS are booted directly from the GRUB menu (under "BOS snapshots"), not rolled back in place. Reboot now and pick this snapshot there.`, + `Reboot to pick snapshot #${selected} in GRUB? BOS boots snapshots from the GRUB “BOS snapshots” submenu. This does not run snapper rollback.`, ) ) { invoke("reboot_system"); @@ -62,9 +62,10 @@ +
@@ -75,6 +76,11 @@ {:else if snapshots.length === 0} {:else} + {#each snapshots as snap (snap.number)} - +
@@ -121,7 +127,15 @@ min-width: 0; } - .row:hover { + .row.header { + background: transparent; + cursor: default; + opacity: 0.55; + font-size: var(--font-size-secondary, 12px); + padding-top: 0; + } + + .row:hover:not(.header) { background-color: color-mix(in srgb, var(--surface), var(--on-surface) 8%); } diff --git a/frontend/src/lib/views/Sound.svelte b/frontend/src/lib/views/Sound.svelte index b863d45..9c887a0 100644 --- a/frontend/src/lib/views/Sound.svelte +++ b/frontend/src/lib/views/Sound.svelte @@ -24,107 +24,154 @@ let output = $state(null); let input = $state(null); + function pick(devices: SoundDevice[], defaultName: string | null): number { + const i = devices.findIndex((d) => d.name === defaultName); + return i >= 0 ? i : 0; + } + async function loadSection(kind: "sinks" | "sources", title: string): Promise { - const section = await invoke<{ devices: SoundDevice[]; default_name: string | null }>("get_sound_section", { kind }); - const selected = Math.max(0, section.devices.findIndex((d) => d.name === section.default_name)); - return { kind, title, devices: section.devices, selected }; + const section = await invoke<{ devices: SoundDevice[]; default_name: string | null }>("get_sound_section", { + kind, + }); + return { kind, title, devices: section.devices, selected: pick(section.devices, section.default_name) }; } onMount(async () => { - output = await loadSection("sinks", "Output"); - input = await loadSection("sources", "Input"); + try { + output = await loadSection("sinks", "Output"); + input = await loadSection("sources", "Input"); + } catch (e) { + console.error(e); + output = { kind: "sinks", title: "Output", devices: [], selected: 0 }; + input = { kind: "sources", title: "Input", devices: [], selected: 0 }; + } }); + function current(section: DeviceSection): SoundDevice | null { + return section.devices[section.selected] ?? section.devices[0] ?? null; + } + async function selectDevice(section: DeviceSection, index: number) { + const device = section.devices[index]; + if (!device) return; section.selected = index; - await invoke("set_default_sound_device", { kind: section.kind, name: section.devices[index].name }); + await invoke("set_default_sound_device", { kind: section.kind, name: device.name }); } async function setVolume(section: DeviceSection, percent: number) { - const device = section.devices[section.selected]; + const device = current(section); + if (!device) return; device.percent = percent; - await invoke("set_sound_volume", { kind: section.kind, name: device.name, percent }); + await invoke("set_sound_volume", { kind: section.kind, name: device.name, percent: Math.round(percent) }); } async function setMute(section: DeviceSection, mute: boolean) { - const device = section.devices[section.selected]; + const device = current(section); + if (!device) return; device.mute = mute; await invoke("set_sound_mute", { kind: section.kind, name: device.name, mute }); } -{#snippet deviceSection(section: DeviceSection | null)} - {#if section} - - {#if section.devices.length === 0} - - {:else} - - - +{#snippet sectionCard(section: DeviceSection)} + + {#if section.devices.length === 0} + + {:else} + {#each section.devices as d, i (d.name + i)} + + {/each} + {#if current(section)} setVolume(section, Number(e.currentTarget.value))} /> - {section.devices[section.selected].percent}% + {Math.round(current(section)!.percent)}% section.devices[section.selected].mute, - (v) => setMute(section, v) - } + bind:value={() => current(section)!.mute, (v) => setMute(section, v)} /> {/if} - - {/if} + {/if} + {/snippet} - - {@render deviceSection(output)} - {@render deviceSection(input)} + + {#if output} + {@render sectionCard(output)} + {/if} + {#if input} + {@render sectionCard(input)} + {/if} - - + + diff --git a/frontend/src/lib/views/Users.svelte b/frontend/src/lib/views/Users.svelte index 08868f5..5a74504 100644 --- a/frontend/src/lib/views/Users.svelte +++ b/frontend/src/lib/views/Users.svelte @@ -69,7 +69,7 @@ - +
{#if accounts} {#each accounts as acc (acc.username)} diff --git a/frontend/src/lib/views/Vpn.svelte b/frontend/src/lib/views/Vpn.svelte new file mode 100644 index 0000000..1973575 --- /dev/null +++ b/frontend/src/lib/views/Vpn.svelte @@ -0,0 +1,148 @@ + + + + + {#if !status} + + {:else if status.error} + + {:else if status.connections.length === 0} + + {:else} +
+ {#each status.connections as c (c.name)} +
+
+ {c.name} + {c.kind}{c.active ? " · connected" : ""}{c.autoconnect ? " · autoconnect" : ""} +
+ {#if c.active} + + {:else} + + {/if} +
+ {/each} +
+ {/if} + + {#if message}{/if} +
+ + + + + +
+ + diff --git a/frontend/src/lib/views/registry.ts b/frontend/src/lib/views/registry.ts index ee0e5e4..d36e136 100644 --- a/frontend/src/lib/views/registry.ts +++ b/frontend/src/lib/views/registry.ts @@ -1,63 +1,30 @@ -// Maps a sidebar page id to its view component. Every sidebar id has a -// real view — +page.svelte's Placeholder is only a safety net for typos. - import type { Component } from "svelte"; -import About from "./About.svelte"; -import Breadclip from "./Breadclip.svelte"; -import Bread from "./Bread.svelte"; -import Breadbar from "./Breadbar.svelte"; -import Breadbox from "./Breadbox.svelte"; -import Breadpad from "./Breadpad.svelte"; -import Breadpaper from "./Breadpaper.svelte"; -import Breadsearch from "./Breadsearch.svelte"; -import Breadcrumbs from "./Breadcrumbs.svelte"; -import Appearance from "./Appearance.svelte"; -import Autostart from "./Autostart.svelte"; -import Display from "./Display.svelte"; -import Keybinds from "./Keybinds.svelte"; -import Sound from "./Sound.svelte"; -import DateTime from "./DateTime.svelte"; -import Power from "./Power.svelte"; -import Network from "./Network.svelte"; +import AboutHub from "./AboutHub.svelte"; +import AppearanceHub from "./AppearanceHub.svelte"; +import AppsHub from "./AppsHub.svelte"; import Bluetooth from "./Bluetooth.svelte"; -import Firewall from "./Firewall.svelte"; -import Users from "./Users.svelte"; -import Packages from "./Packages.svelte"; -import Aur from "./Aur.svelte"; -import Firmware from "./Firmware.svelte"; -import Snapshots from "./Snapshots.svelte"; -import Breadlock from "./Breadlock.svelte"; -import Breadshot from "./Breadshot.svelte"; -import Breadmon from "./Breadmon.svelte"; -import Breadhelp from "./Breadhelp.svelte"; +import DesktopHub from "./DesktopHub.svelte"; +import DisplaysHub from "./DisplaysHub.svelte"; +import Home from "./Home.svelte"; +import InputHub from "./InputHub.svelte"; +import NetworkHub from "./NetworkHub.svelte"; +import Power from "./Power.svelte"; +import PrivacyHub from "./PrivacyHub.svelte"; +import Sound from "./Sound.svelte"; +import SystemHub from "./SystemHub.svelte"; export const VIEWS: Record = { - about: About, - breadclip: Breadclip, - bread: Bread, - breadbar: Breadbar, - breadbox: Breadbox, - breadpad: Breadpad, - breadpaper: Breadpaper, - breadsearch: Breadsearch, - breadcrumbs: Breadcrumbs, - appearance: Appearance, - autostart: Autostart, - hyprland: Display, - keybinds: Keybinds, - sound: Sound, - datetime: DateTime, - power: Power, - network: Network, + home: Home, + network: NetworkHub, bluetooth: Bluetooth, - firewall: Firewall, - users: Users, - packages: Packages, - aur: Aur, - firmware: Firmware, - snapshots: Snapshots, - breadlock: Breadlock, - breadshot: Breadshot, - breadmon: Breadmon, - breadhelp: Breadhelp, + displays: DisplaysHub, + sound: Sound, + power: Power, + appearance: AppearanceHub, + desktop: DesktopHub, + input: InputHub, + apps: AppsHub, + privacy: PrivacyHub, + system: SystemHub, + about: AboutHub, }; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 22b7a6c..7916d15 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,65 +1,66 @@ -
- -
- {#if ActiveView} - - {:else} - - {/if} -
+
+ +
+ +
+ {#key nav.page} + {#if ActiveView} + + {:else} + + {/if} + {/key} +
+