Stores

Using Svelte stores for global state management in DesQTA

Stores Overview

DesQTA uses Svelte stores for managing global application state. Stores provide reactive state that can be accessed from any component in the application.

Store Architecture

Stores are located in src/lib/stores/ and follow a consistent pattern:

src/lib/stores/
├── theme.ts              # Theme and accent color
├── themeBuilderSidebar.ts # Theme builder UI state
└── toast.ts              # Toast notifications

Store Types

Writable Stores

Writable stores allow reading and writing values:

import { writable } from 'svelte/store';

export const count = writable(0);

// In component
import { count } from '$lib/stores/count';
count.set(10);           // Set value
count.update(n => n + 1); // Update value
$count;                  // Read value (auto-subscribe)

Readable Stores

Readable stores are read-only:

import { readable } from 'svelte/store';

export const time = readable(new Date(), (set) => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);
  
  return () => clearInterval(interval);
});

Derived Stores

Derived stores compute values from other stores:

import { derived } from 'svelte/store';
import { count } from './count';

export const doubled = derived(count, $count => $count * 2);

Theme Store

The theme store manages application theming and accent colors.

Location

src/lib/stores/theme.ts

Stores

// Basic theme mode (light/dark/system)
export const theme = writable<'light' | 'dark' | 'system'>('system');

// Accent color
export const accentColor = writable('#3b82f6');

// Current theme pack name
export const currentTheme = writable('default');

// Theme manifest (theme configuration)
export const themeManifest = writable<ThemeManifest | null>(null);

// Custom CSS
export const customCSS = writable('');

// Previewing theme (not yet applied)
export const previewingTheme = writable<string | null>(null);

Derived Stores

// Theme properties from manifest
export const themeProperties = derived(
  [currentTheme, themeManifest],
  ([$currentTheme, $manifest]) => {
    if (!$manifest) return {};
    return $manifest.customProperties;
  }
);

Functions

Load Accent Color

import { loadAccentColor } from '$lib/stores/theme';

await loadAccentColor();
// Loads accent color from settings and updates store

Load Theme

import { loadTheme } from '$lib/stores/theme';

await loadTheme();
// Loads theme preference from settings and applies it

Update Accent Color

import { updateAccentColor } from '$lib/stores/theme';

await updateAccentColor('#6366f1');
// Updates accent color and saves to settings

Update Theme

import { updateTheme } from '$lib/stores/theme';

await updateTheme('dark');
// Updates theme mode and saves to settings

Load and Apply Theme Pack

import { loadAndApplyTheme } from '$lib/stores/theme';

await loadAndApplyTheme('midnight');
// Loads theme pack, applies it, and updates all related stores

Theme Preview

import { startThemePreview, cancelThemePreview, applyPreviewTheme } from '$lib/stores/theme';

// Start preview (doesn't save)
await startThemePreview('sunset');

// Cancel preview (restore previous)
await cancelThemePreview();

// Apply preview as active theme
await applyPreviewTheme();

Usage Example

<script lang="ts">
  import { theme, accentColor, loadTheme, updateTheme } from '$lib/stores/theme';
  import { onMount } from 'svelte';
  
  onMount(async () => {
    await loadTheme();
  });
  
  function toggleTheme() {
    const current = $theme;
    const next = current === 'dark' ? 'light' : 'dark';
    updateTheme(next);
  }
</script>

<div class="bg-white dark:bg-zinc-900" style="--accent-color: {$accentColor}">
  <button onclick={toggleTheme}>
    Current theme: {$theme}
  </button>
</div>

Toast Store

The toast store manages notification toasts.

Location

src/lib/stores/toast.ts

Usage

import { toast } from '$lib/stores/toast';

// Show success toast
toast.success('Operation completed successfully!');

// Show error toast
toast.error('Something went wrong');

// Show info toast
toast.info('Here is some information');

// Show warning toast
toast.warning('Please review your changes');

// Dismiss specific toast
toast.dismiss(toastId);

// Clear all toasts
toast.clear();

Toast Methods

interface ToastStore {
  success(message: string, duration?: number): string;
  error(message: string, duration?: number): string;
  info(message: string, duration?: number): string;
  warning(message: string, duration?: number): string;
  dismiss(id: string): void;
  clear(): void;
}

Usage Example

<script lang="ts">
  import { toast } from '$lib/stores/toast';
  
  async function saveData() {
    try {
      await save();
      toast.success('Saved successfully!');
    } catch (error) {
      toast.error('Failed to save');
    }
  }
</script>

<button onclick={saveData}>Save</button>

Theme Builder Sidebar Store

Manages the theme builder sidebar state.

Location

src/lib/stores/themeBuilderSidebar.ts

Usage

import { themeBuilderSidebarOpen } from '$lib/stores/themeBuilderSidebar';

// Toggle sidebar
themeBuilderSidebarOpen.update(open => !open);

// Set state
themeBuilderSidebarOpen.set(true);

Creating Custom Stores

Basic Store Pattern

// src/lib/stores/myStore.ts
import { writable, derived } from 'svelte/store';

// Writable store
export const myValue = writable<string>('initial');

// Derived store
export const myComputed = derived(myValue, $value => {
  return $value.toUpperCase();
});

// Store with actions
export const myStore = {
  subscribe: myValue.subscribe,
  set: myValue.set,
  update: myValue.update,
  reset: () => myValue.set('initial'),
  customAction: (value: string) => {
    // Custom logic
    myValue.set(value);
  }
};

Store with Async Loading

// src/lib/stores/dataStore.ts
import { writable } from 'svelte/store';
import { invoke } from '@tauri-apps/api/core';

export const data = writable<any[]>([]);
export const loading = writable(false);
export const error = writable<string | null>(null);

export const dataStore = {
  subscribe: data.subscribe,
  loading: {
    subscribe: loading.subscribe
  },
  error: {
    subscribe: error.subscribe
  },
  async load() {
    loading.set(true);
    error.set(null);
    try {
      const result = await invoke('get_data');
      data.set(result);
    } catch (e) {
      error.set(e instanceof Error ? e.message : 'Failed to load');
    } finally {
      loading.set(false);
    }
  },
  reset() {
    data.set([]);
    error.set(null);
  }
};

Store with Persistence

// src/lib/stores/persistentStore.ts
import { writable } from 'svelte/store';
import { invoke } from '@tauri-apps/api/core';

function createPersistentStore<T>(key: string, defaultValue: T) {
  const store = writable<T>(defaultValue);
  
  // Load from settings
  async function load() {
    try {
      const subset = await invoke<any>('get_settings_subset', { keys: [key] });
      if (subset?.[key] !== undefined) {
        store.set(subset[key]);
      }
    } catch (e) {
      console.error(`Failed to load ${key}:`, e);
    }
  }
  
  // Save to settings
  async function save(value: T) {
    try {
      const { saveSettingsWithQueue } = await import('../services/settingsSync');
      await saveSettingsWithQueue({ [key]: value });
      store.set(value);
    } catch (e) {
      console.error(`Failed to save ${key}:`, e);
    }
  }
  
  return {
    subscribe: store.subscribe,
    set: (value: T) => save(value),
    update: (updater: (value: T) => T) => {
      store.update(current => {
        const newValue = updater(current);
        save(newValue);
        return newValue;
      });
    },
    load
  };
}

export const myPersistentValue = createPersistentStore('my_key', 'default');

Store Best Practices

1. Keep Stores Focused

Each store should manage a single concern:

// ✅ Good - Focused store
export const userPreferences = writable({ theme: 'dark' });

// ❌ Avoid - Too broad
export const appState = writable({ user: {}, theme: {}, settings: {} });

2. Use Derived Stores for Computations

// ✅ Good - Derived store
export const fullName = derived(
  [firstName, lastName],
  ([$first, $last]) => `${$first} ${$last}`
);

// ❌ Avoid - Computing in component
let fullName = $derived(`${$firstName} ${$lastName}`);

3. Provide Store Actions

// ✅ Good - Actions included
export const counter = {
  subscribe: count.subscribe,
  increment: () => count.update(n => n + 1),
  decrement: () => count.update(n => n - 1),
  reset: () => count.set(0)
};

// ❌ Avoid - Direct store manipulation
count.update(n => n + 1);

4. Handle Async Operations

// ✅ Good - Loading and error states
export const dataStore = {
  data: writable([]),
  loading: writable(false),
  error: writable(null),
  async load() {
    loading.set(true);
    try {
      const result = await fetchData();
      data.set(result);
    } catch (e) {
      error.set(e.message);
    } finally {
      loading.set(false);
    }
  }
};

5. Use TypeScript

// ✅ Good - Typed store
interface User {
  id: number;
  name: string;
}

export const user = writable<User | null>(null);

// ❌ Avoid - Untyped store
export const user = writable(null);

Store Composition

Combining Multiple Stores

import { derived } from 'svelte/store';
import { theme } from './theme';
import { accentColor } from './theme';
import { user } from './user';

// Combine multiple stores
export const appState = derived(
  [theme, accentColor, user],
  ([$theme, $accent, $user]) => ({
    theme: $theme,
    accentColor: $accent,
    user: $user,
    isAuthenticated: $user !== null
  })
);

Store Dependencies

// Store A depends on Store B
export const storeA = writable(0);
export const storeB = derived(storeA, $a => $a * 2);
export const storeC = derived(storeB, $b => $b + 10);

Store Testing

Testing Stores

import { get } from 'svelte/store';
import { count, increment } from './counter';

test('increment increases count', () => {
  count.set(0);
  increment();
  expect(get(count)).toBe(1);
});

Next Steps