"Local-First" Philosophy
Relevant source files
The following files were used as context for generating this wiki page:
The Character Sheet Maker is designed as a "Local-First" web application. This architectural approach prioritizes storing all user data and application state directly on the user's local machine, specifically within the web browser's localStorage. This means the core functionality—designing, customizing, and saving character sheets—operates entirely client-side, without requiring a constant internet connection or communication with a remote server.
This philosophy provides several key benefits: user data remains private, the application is fast and responsive, and it can function offline. The entire state of the character sheet, including all its components, layout, and styling, is persisted locally, ensuring that a user's work is automatically saved and available across browser sessions on the same machine.
Sources: README.md:10, src/store/useSheetStore.ts:151-153
Core Implementation: State Persistence
The foundation of the local-first approach is the application's state management, which leverages Zustand with its persistence middleware. All critical data related to the character sheet is held in a central store, which is automatically synchronized with the browser's localStorage.
Sources: src/store/useSheetStore.ts:2, src/store/useSheetStore.ts:60-153
Zustand Store and persist Middleware
The main application state, including all blocks, their positions, content, and global styles, is managed by a Zustand store defined in useSheetStore.ts. The persist middleware wraps the store's definition, intercepting state changes and saving the entire state object to localStorage under a specific key.
The configuration is straightforward:
// src/store/useSheetStore.ts:60-153
export const useSheetStore = create<SheetState>()(
persist(
(set) => ({
// ... state and actions
}),
{ name: "rpg-sheet-storage" } // AutoSave in localStorage
)
);
This configuration tells Zustand to save the state into localStorage with the key rpg-sheet-storage. Any update to the store (e.g., adding a block, moving an element, changing text) triggers an automatic save.
Sources: src/store/useSheetStore.ts:60-153
The following diagram illustrates the data flow when a user interacts with the application.
This diagram shows how a user action triggers a Zustand state update, which is then automatically saved to localStorage by the persist middleware.
Sources: src/store/useSheetStore.ts
Persisted State Structure
The SheetState interface defines the data structure that is saved to localStorage. It includes the array of blocks and global style settings.
| Property | Type | Description |
|---|---|---|
blocks | Block[] | An array of all content blocks on the canvas. Each block has properties for position, size, type, and data. |
isCompact | boolean | A flag to control the grid layout's compacting behavior. |
globalStyle | SheetStyle | An object containing the currently active theme and font settings for the entire sheet. |
Sources: src/store/useSheetStore.ts:38-58
Other Examples of Local Persistence
The local-first philosophy extends beyond just the character sheet content. Other UI state, such as the selected theme, is also persisted locally.
Theme Persistence
The useTheme custom hook manages the application's light/dark mode. It reads the user's preference from localStorage on initial load and updates it whenever the theme is toggled. This ensures the theme choice is remembered across sessions.
// src/hooks/useTheme.ts:4-14
export function useTheme() {
const [theme, setTheme] = useState<"light" | "dark">(() => {
// Read system preference or localStorage at the beginning
if (typeof window !== "undefined" && window.localStorage.getItem("theme")) {
return window.localStorage.getItem("theme") as "light" | "dark";
}
// ... fallback to system preference
return "light";
});
useEffect(() => {
// ...
localStorage.setItem("theme", theme);
}, [theme]);
// ...
}
The following sequence diagram shows how the theme is initialized and updated.
This diagram shows the useTheme hook reading from and writing to localStorage to persist the UI theme.
Sources: src/hooks/useTheme.ts
Local Data Processing and Export
A key feature enabled by the local-first architecture is the ability to export the character sheet into different formats (PNG, PDF) directly in the browser, without any server-side processing.
Export Flow
The export functionality, located in src/lib/exportUtils.ts, uses the html-to-image and jspdf libraries. When a user clicks an export button, these utilities capture the sheet's HTML element, convert it to an image, and then embed that image into a PDF file if required.
The process for exporting a PDF is as follows:
- The
downloadAsPDFfunction is called from the UI. - It finds the main canvas element by its ID (
rpg-sheet-canvas). html-to-image'stoPngfunction converts this HTML element into a high-resolution PNG image represented as a data URL.- A new
jsPDFinstance is created. - The PNG data is added to the PDF document, which is scaled to fit an A4 page.
- The
pdf.save()method is called, triggering a file download in the user's browser.
Sources: src/lib/exportUtils.ts:20-41, src/components/layout/TopBar.tsx:47-50, package.json:14-15
This flowchart details the client-side process of exporting a character sheet to a PDF file.
Sources: src/lib/exportUtils.ts:20-41
Conclusion
The "Local-First" philosophy is central to the Character Sheet Maker's design. By leveraging browser localStorage via Zustand's persistence middleware and client-side libraries for data processing, the application delivers a fast, private, and offline-capable user experience. All user-generated content and settings are stored and managed directly on the user's device, eliminating the need for a backend server for core functionality and giving the user full ownership of their data.