Frontend

Routing

All three page routes are lazy-loaded via React.lazy() in main.jsx, wrapped in <Suspense> with a .loader spinner fallback:

const Dashboard = React.lazy(() => import("./pages/Dashboard/Dashboard"));
const CreateProfile = React.lazy(() => import("./pages/CreateProfile/CreateProfile"));
const Settings = React.lazy(() => import("./pages/Settings/Settings"));
<BrowserRouter>
  <Routes>
    <Route path="/" element={<AppLayout />}>
      <Route index element={<Suspense fallback={<div className="loader" />}><Dashboard /></Suspense>} />
      <Route path="profile" element={<Suspense fallback={<div className="loader" />}><CreateProfile /></Suspense>} />
      <Route path="setting" element={<Suspense fallback={<div className="loader" />}><Settings /></Suspense>} />
    </Route>
  </Routes>
</BrowserRouter>

AppLayout is the persistent shell. Child routes swap via <Outlet />.

Layout System

┌─────────────────────────────────────┐
│  Titlebar (40px, sticky, z-index 10)│
├────────┬────────────────────────────┤
│ Nav    │  PageContainer (flex: 1)   │
│ 200px  │  ┌──────────────────────┐  │
│ (56px  │  │  <AppPg>             │  │
│ narrow)│  │  padding: 32px 40px  │  │
│        │  │  24px 20px @800px    │  │
│        │  │  16px 12px @500px    │  │
│        │  └──────────────────────┘  │
├────────┴────────────────────────────┤
│  height: calc(100vh - 40px)         │
└─────────────────────────────────────┘
  • Nav collapses from 200px to 56px at 800px window width — text labels hide via max-width: 0; opacity: 0 (no layout reflow)
  • Anti-jitter rule: No CSS transitions on layout properties (width, padding). Only visual-only properties (opacity, max-width) transition.
  • Content padding steps down: 32px 40px24px 20px @800px → 16px 12px @500px

Vite Build Config

rollupOptions.output.manualChunks splits dependencies into separate chunks:

  • vendorreact + react-dom
  • routerreact-router-dom

CSS Architecture

Shared form/page styles live in app/src/styles/forms.css (imported by CreateProfile and Settings). Dashboard.css only contains Dashboard-specific styles (header, buttons). Six duplicate @import "../../index.css" lines were removed from component CSS files — Vite handles CSS injection automatically.

Components

SelectMenu

Custom dropdown replacing native <select>, used for the Rainmeter layout picker on CreateProfile. Features animated open/close (120ms), click-outside dismiss, and full theme support via CSS variables.

ResizableTextarea

Custom resizable textarea that replaces the native Windows resize handle (an unstylable OS-drawn widget). The native handle is disabled with resize: none and a themed drag handle is rendered at the bottom-right corner. Height is set directly on the DOM ref during drag — zero React re-renders. Minimum height: 80px.

ImportExportModal

Full-screen modal overlay with backdrop blur for importing/exporting profiles. Supports radio toggle between single/all profiles for export. Auto-detects single vs wrapped JSON format on import.

UpdateBanner

Floating toast at bottom-right driven by useUpdateChecker hook. Three states: available (version + release notes), downloading (percentage + progress bar), downloaded (Restart & Install button). Dismiss animation slides right 30px + fades over 250ms using only GPU-composited properties.

ProfileCard

Displays profile name and active tool badges (emoji icons). Three action buttons: Run (calls sidecar), Edit (navigates to /profile), Remove.

Form Layout

CreateProfile and Settings forms use app/src/styles/forms.css:

  • .profileCont / .settingCont: grid-template-columns: 200px 1fr with align-items: start
  • .form-group: display: grid; grid-template-columns: subgrid — prevents textarea resize from affecting adjacent rows
  • At 800px: collapses to single-column flex layout

Tauri Window Config

PropertyValue
Default size1000x800
Minimum size700x500
Decorationsfalse (custom titlebar)