Skip to content

Latest commit

 

History

History
363 lines (269 loc) · 8.79 KB

File metadata and controls

363 lines (269 loc) · 8.79 KB

UI Patterns

Overview

This app uses a modern CSS stack optimized for Tauri desktop applications:

  • Tailwind CSS v4 with CSS-based configuration
  • shadcn/ui v4 component library
  • OKLCH color space for perceptually uniform colors
  • Desktop-specific defaults for native app feel

Tailwind v4 Configuration

Tailwind v4 uses CSS-based configuration instead of tailwind.config.js.

File Structure

src/
├── App.css              # Main window styles + Tailwind imports
├── quick-pane.css       # Quick pane window styles
└── theme-variables.css  # Shared theme variables (colors, radii)

Multi-window theming: theme-variables.css is imported by both App.css and quick-pane.css so all windows share the same theme tokens. When adding new color variables, add them to theme-variables.css.

Structure

@import 'tailwindcss'; /* Core Tailwind */
@import 'tw-animate-css'; /* Animation utilities */

@custom-variant dark (&:is(.dark *)); /* Dark mode variant */

@theme inline {
  /* Map CSS variables to Tailwind tokens */
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  /* ... */
}

:root {
  /* Light mode values */
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
}

.dark {
  /* Dark mode overrides */
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
}

@layer base {
  /* Global base styles */
}

Key Concepts

Directive Purpose
@theme inline Maps CSS variables to Tailwind's design token system
@custom-variant dark Enables dark: prefix based on .dark class
@layer base Base styles that apply globally

Adding Custom Colors

To add a new semantic color:

@theme inline {
  --color-success: var(--success);
  --color-success-foreground: var(--success-foreground);
}

:root {
  --success: oklch(0.7 0.15 145);
  --success-foreground: oklch(1 0 0);
}

.dark {
  --success: oklch(0.6 0.15 145);
  --success-foreground: oklch(1 0 0);
}

Then use with Tailwind: bg-success text-success-foreground

Dark Mode

How It Works

  1. ThemeProvider (src/components/ThemeProvider.tsx) manages theme state
  2. Adds .dark class to <html> element when dark mode is active
  3. CSS variables in .dark override :root values
  4. Tailwind's dark: variant applies styles conditionally

Theme Options

  • light - Force light mode
  • dark - Force dark mode
  • system - Follow OS preference (default)

Using in Components

// Access theme in components
import { useTheme } from '@/hooks/use-theme'

function MyComponent() {
  const { theme, setTheme } = useTheme()

  return <button onClick={() => setTheme('dark')}>Current: {theme}</button>
}

Why .dark Class (Not light-dark())

This app uses the .dark class approach rather than CSS light-dark() because:

  • Standard pattern for shadcn/ui ecosystem
  • JavaScript control over theme switching
  • Supports "system" preference detection
  • Compatible with all shadcn components

OKLCH Colors

All colors use the OKLCH color space for perceptual uniformity.

Format

oklch(lightness chroma hue)
oklch(0.7 0.15 250)  /* L: 0-1, C: 0-0.4, H: 0-360 */

Why OKLCH

  • Perceptually uniform - Equal steps in values = equal perceived change
  • Wide gamut - Access to P3 display colors
  • Intuitive - Lightness is predictable (unlike HSL)

Color Palette Structure

Token Purpose
--background / --foreground Page background and text
--card / --card-foreground Card surfaces
--primary / --primary-foreground Primary actions
--secondary / --secondary-foreground Secondary actions
--muted / --muted-foreground Subdued elements
--accent / --accent-foreground Highlights
--destructive Destructive actions (red)
--border / --input / --ring Borders and focus rings

Desktop-Specific Styles

The @layer base section includes styles that make the app feel native on desktop.

Text Selection

body {
  user-select: none; /* Disable by default */
}

input,
textarea,
[contenteditable='true'] {
  user-select: text !important; /* Enable in editable areas */
}

Why: Desktop apps typically don't allow selecting UI text, only content.

Cursor

* {
  cursor: default; /* Arrow cursor everywhere */
}

input,
textarea {
  cursor: text !important;
}

.cursor-pointer {
  cursor: pointer !important;
}

Why: Native apps use arrow cursor, not text cursor on labels.

Scroll Behavior

body {
  overscroll-behavior: none; /* Prevent bounce/refresh */
  overflow: hidden; /* Prevent body scroll */
}

Why: Prevents pull-to-refresh and elastic scrolling that feels wrong in desktop apps.

Drag Regions

*[data-tauri-drag-region] {
  -webkit-app-region: drag;
  app-region: drag;
}

Apply data-tauri-drag-region to elements that should drag the window (like title bars).

Component Organization

src/components/
├── layout/           # App structure
│   ├── MainWindow.tsx
│   ├── LeftSideBar.tsx
│   ├── RightSideBar.tsx
│   └── MainWindowContent.tsx
├── titlebar/         # Window chrome
│   ├── TitleBar.tsx
│   ├── MacOSWindowControls.tsx
│   └── WindowsWindowControls.tsx
├── ui/               # shadcn primitives
│   ├── button.tsx
│   ├── dialog.tsx
│   └── ...
├── command-palette/  # Command palette feature
├── preferences/      # Preferences dialog
├── ThemeProvider.tsx
└── ErrorBoundary.tsx

Conventions

  • layout/ - Structural components that define app regions
  • titlebar/ - Platform-specific window controls
  • ui/ - shadcn/ui primitives (don't modify directly)
  • Feature folders - Group related components together

shadcn/ui Usage

Adding Components

npx shadcn@latest add button
npx shadcn@latest add dialog

Components are copied to src/components/ui/ and can be customized.

Customizing Components

shadcn components are yours to modify. Common customizations:

// src/components/ui/button.tsx
const buttonVariants = cva('...', {
  variants: {
    variant: {
      default: 'bg-primary text-primary-foreground',
      // Add custom variant
      success: 'bg-success text-success-foreground',
    },
  },
})

Available Components

This app includes commonly needed components. Run npx shadcn@latest add [component] to add more from ui.shadcn.com.

The cn() Utility

All components use the cn() utility for conditional classes:

import { cn } from '@/lib/utils'

function MyComponent({ className, disabled }) {
  return (
    <div
      className={cn(
        'base-styles here',
        disabled && 'opacity-50',
        className // Allow overrides
      )}
    >
      ...
    </div>
  )
}

Pattern: Always accept className prop and merge with cn() for flexibility.

Component Patterns

Layout Components

Layout components should:

  • Accept children and className props
  • Use flexbox with overflow-hidden to prevent content bleed
  • Not set external margins (let parent control spacing)
interface SideBarProps {
  children?: React.ReactNode
  className?: string
}

export function LeftSideBar({ children, className }: SideBarProps) {
  return (
    <div className={cn('flex flex-col h-full overflow-hidden', className)}>
      {children}
    </div>
  )
}

Visibility with CSS

For panels that toggle visibility, prefer CSS over conditional rendering:

// Good: Preserves component state
;<ResizablePanel className={cn(!visible && 'hidden')}>
  <SideBar />
</ResizablePanel>

// Avoid: Loses component state on hide/show
{
  visible && <SideBar />
}

This preserves scroll position, form state, and resize dimensions.

Best Practices

Do

  • Use semantic color tokens (bg-background, text-foreground)
  • Accept className prop on components
  • Use cn() for conditional classes
  • Keep desktop UX conventions (cursor, selection, scroll)
  • Follow existing patterns in codebase

Don't

  • Use raw color values (bg-white, text-gray-900)
  • Hardcode light/dark specific values
  • Override shadcn components in place (copy and modify instead)
  • Add cursor-pointer everywhere (only for actual clickable elements)
  • Use viewport-based responsive design (this is a fixed-size desktop app)