Navet is a smart home dashboard PWA built on React 19 + TypeScript 6 + Zustand 5 + Tailwind CSS 4.3. Connects to Home Assistant over WebSocket.
- Use Conventional Commits format:
type(scope): summary - Valid types:
feat,fix,refactor,docs,test,chore,build,ci,perf,style - Summary must be lowercase, imperative mood, no period at the end
- Do not use generic or free-form commit messages
- Include a concise bullet-point body for meaningful changes so the project can track what changed between releases
- Keep bullet lists contiguous in the commit body. Do not create each bullet as a separate
git commit -mparagraph, because that inserts blank lines between items. - Keep commit bodies focused on user-visible behavior, architecture, tests, docs, or operational impact
- Trivial commits such as formatting-only or metadata-only changes may omit the body
- Do not use
git commit --no-verifyunless the user explicitly approves skipping hooks for that commit
pnpm typecheck # type-check without emitting
pnpm check # Biome lint + format check
pnpm format # Biome format (auto-fix)
pnpm check:stories # validate Storybook title, coverage, and ownership rules
pnpm check:ui-kit # validate UI-kit import and boundary rules
pnpm test # unit tests
pnpm storybook:build # static Storybook build for GitHub PagesDo not run pnpm build unless explicitly asked.
Do not run pnpm typecheck or pnpm check yourself — ask the user to run them and report back.
For release work, do not run any pnpm command yourself. List the required pnpm commands for the user, wait for the reported results, and continue from there.
If a commit or hook is blocked by TypeScript errors, fix the type errors instead of updating or relying on a typecheck baseline.
Storybook builds for GitHub Pages at /navet/storybook/ via STORYBOOK_BASE_PATH=/navet/storybook/.
Keep manager and preview asset paths relative or base-aware; do not add root-relative /logo.svg
style paths in Storybook configuration.
| Layer | Tool |
|---|---|
| Framework | React 19, TypeScript 6 (strict) |
| Build | Vite 8, pnpm |
| Styling | Tailwind CSS 4.3, Radix UI |
| State | Zustand 5 (all shared state) |
| HA Integration | home-assistant-js-websocket |
| Linting / Format | Biome 2 |
src/app/
features/ # 16 domain modules — each owns its hooks, stores, components
components/
ui/ # Radix UI wrappers (buttons, dialogs, selects …)
layout/ # Header, sidebar, navigation
primitives/ # Low-level reusable UI building blocks
patterns/ # Composed shared UI structures
system/ # Curated public surface for Storybook and cross-app discovery
shared/ # App-specific shared UI + compatibility shims
figma/ # Design integration components
config/ # App-level configuration helpers
constants/ # Shared constants
stores/ # All Zustand stores (auth, config, HA, settings, theme, navigation …)
pwa/ # PWA update state and install/update support
services/ # HomeAssistantService — WebSocket + HA API
hooks/ # Shared hook modules (useHomeAssistant, useDeviceMap, useCardState …)
device-mappers/ # HA domain-specific entity-to-device mappers
entity-utils/ # Shared HA entity parsing and formatting helpers
theme-generators/
session/ # Config serialization helpers
utils/ # Pure helpers (storage, effects-quality, colors, dashboard-config)
i18n/ # Translation files (en, sv, de, fr, es)
marketing/ # Marketing/public-site support modules
navigation/ # Section type and helpers
storybook/ # Shared Storybook utilities (story-frames, story-docs)
ui-kit/ # Storybook-facing inventory, overview, and recipe stories
test-utils/ # Shared test helpers
types/ # Shared app-level TypeScript types
demo/ # GitHub Pages demo app and demo data
These rules apply to all code written for this project. Follow them before writing, and verify against them before finishing.
- Prefer reusable, composable, detachable components over large monolithic ones.
- Keep components focused on a single responsibility.
- Extract repeated UI, logic, and utility patterns instead of duplicating them.
- Keep code scalable and easy to extend without rewriting existing functionality.
- Use clear separation of concerns: UI, state, business logic, and utilities belong in separate layers.
- New shared cross-feature UI belongs in
src/app/components/primitives/orsrc/app/components/patterns/. Feature-specific logic stays in the feature module. src/app/components/system/is the curated public export surface, not the authoring location for new components.src/app/components/shared/is for app-specific shared UI and compatibility shims; do not default new primitives there.- Do not add new feature-specific hooks, stores, or utilities to global folders unless they are genuinely shared across multiple features.
- Use modern React best practices throughout.
- Avoid unnecessary prop drilling — prefer better composition or lifting state only when needed.
- Keep state as local as possible; only lift it when multiple components need it.
- Avoid over-engineering, but do not allow quick hacks that reduce maintainability.
- Prefer predictable and consistent patterns across the codebase over one-off solutions.
- Optimize for both high-end devices and low-power devices (tablets, Raspberry Pi, dashboards).
- Avoid unnecessary re-renders — use minimal Zustand selectors and memoize only where it provides real value.
- Avoid heavy computations during render; move them to
useMemoor outside the component if they are expensive. - Lazy load expensive features where appropriate (see existing
lazy()usage in the dashboard). - Keep DOM structure lean and avoid deep nesting.
- Be careful with animations, shadows, blur, and heavy CSS effects — they must remain smooth on weaker hardware. Flag any tradeoff where visual richness may hurt performance on low-power devices.
- Follow DRY, but do not abstract prematurely — three similar lines are better than a premature abstraction.
- Prefer readability over cleverness.
- Reuse existing components, hooks, and utilities before creating new ones.
- Do not introduce duplicate utilities, hooks, or component variants unless clearly justified.
- Keep naming consistent and descriptive.
- Check whether an existing component, hook, utility, or pattern should be reused.
- Before building any new UI element, scan
src/app/components/primitives/first. If a primitive already covers the use case (button, slider, dialog shell, round control, card header, etc.), use it — do not re-implement it inline or in a feature folder. - Before adding a new Storybook story file, check whether a story for that component already exists — run
glob src/app/**/*.stories.*or search for the component name. Add to an existing story file rather than creating a duplicate. - Before writing new UI logic, check if unit tests already exist — look for
__tests__/directories next to the source. Extend existing tests before adding duplicate coverage. - If creating something new, make it reusable if that is realistically beneficial.
- Explain any architectural decision that affects maintainability or performance.
- Flag any tradeoff where visual richness may hurt performance on low-power devices.
- Do not produce shortcut code that solves the immediate task but worsens the codebase.
- Is this reusable?
- Is this consistent with existing patterns?
- Is this the simplest maintainable solution?
- Will this perform well on weaker devices?
- Does this avoid duplication?
- All shared state is Zustand. Do not introduce React Context for reactive state.
- Context is only for infrastructure without reactive state (e.g.
I18nProviderinsrc/app/i18n/). - Use selectors from
src/app/stores/selectors.tsto subscribe to the minimum slice needed. - Persisted stores use
persistmiddleware withcreateJSONStorage(() => localStorage)and amergefunction that validates before rehydrating. Never use rawwindow.localStoragein stores. - See
docs/technical/REACT_ZUSTAND.mdfor the full state management guide.
| Store | Responsibility |
|---|---|
auth-store |
isAuthenticated, config, login, logout |
config-store |
HA connection config, testConnection, saveConfig |
home-assistant-store |
WebSocket connection state, entities, registries |
settings-store |
User preferences (persisted) |
theme-store |
Theme mode, accent color, wallpaper (persisted) |
navigation-store |
Active section, current room (persisted) |
edit-mode-store |
Dashboard edit mode toggle |
search-store |
Search query and filtered device ids |
error-store |
Global app error overlay (ErrorDisplay): error, setError, clearError |
// Good — minimal subscription
const connected = useHomeAssistant(homeAssistantSelectors.connected);
// Good — one subscription for a related group
const { disableAnimations, effectsQuality, weatherForecastMode } = useSettingsStore(
settingsSelectors.displaySettings
);
// Avoid — re-renders on every store change
const store = useHomeAssistant();HomeAssistantService emits typed events: 'entities' | 'config' | 'registries' | 'connection'.
The store subscribes and updates only the affected slice. Do not add catch-all listeners that
copy all service state on every event.
- Import from a feature's root
index.tswhen crossing feature boundaries — never reach into itscomponents/,hooks/, orstores/subdirectories from outside. - Use
@/app/...for shared app modules and cross-feature imports. - Keep short relative imports for files inside the same small feature/module subtree.
- Split large feature files into: entry component, controller hook, presentational views, local types/data/constants.
- Card-style features use folder modules with
index.tsxentries:light-card/ hvac-card/ media-card/ vacuum-card/ weather-card/ - A controller hook should not exceed ~150 lines. Split a coherent group of state and handlers
into its own named hook (e.g.
useOnboardingController,useDashboardDialogs). - Never call
storeInstance.setState()from a component — use the store's own action methods.
src/app/features/dashboard/utils/card-renderer.tsxis the dashboard card registry. Do not move card rendering registration logic back into genericsrc/app/utils/.- Dashboard entity visibility, custom card state, card ordering, and room ordering must stay
colocated with the dashboard feature (
src/app/features/dashboard/stores/).
- Use
src/app/components/shared/theme/theme-surface-tokens.tsfor shared surface decisions. - Do not add
theme === 'light' ? ... : ...branches inline when a shared surface token covers it. - Only keep local theme branches for truly feature-specific styling (domain accents, status colors).
- Tailwind CSS 4 only — no inline style objects except for dynamic numeric values
(e.g.
style={{ width:${pct}%}}). - Glass aesthetic:
backdrop-blur-xl,bg-white/5–10,border border-white/10–20. - 4 themes:
glass(default),dark,light,black— applied viadata-themeon<html>. Persisted legacycontrastvalues are normalized toblack.
- When moving or renaming files referenced by docs, update the active docs in the same change.
- Keep these current:
README.md,docs/README.md,design-system/README.md,design-system/FEATURES.md. - Treat
docs/archive/status/*as historical snapshots — do not rewrite them. - If you add or reorganize stories, run
pnpm check:storiesand keep Storybook titles, coverage, and colocated story ownership valid.
- CI checks run on branch pushes and pull requests.
- GitHub Pages deploys the demo at
/navet/demo/and Storybook at/navet/storybook/. - Pushes to
mainpublish only developer app images:ghcr.io/awesomestvi/navet:devandsha-*. - Manual Publish workflow runs are for developer hardware testing and default to the
devapp image tag. - Public beta app images publish only from
v*-alpha.*,v*-beta.*, andv*-rc.*tags. They update the exact tag,beta,latest, andsha-*. - Home Assistant add-on images publish
devandsha-*onmainpushes or manual workflow runs, and add-on version,beta,latest, andsha-*on public beta tags. - There is no stable channel yet. Treat
latestas the current public beta compatibility tag because existing users already consume it.
| File | Purpose |
|---|---|
| src/app/App.tsx | Root — provider tree, connection effect, global DOM attributes |
| src/app/stores/ | All Zustand stores + selectors |
| src/app/services/home-assistant.service.ts | WebSocket connection, typed event emitter |
| src/app/stores/home-assistant-store.ts | HA state — subscribes to typed service events |
| src/app/features/dashboard/hooks/use-dashboard-controller.ts | Dashboard coordinator hook |
| src/app/features/dashboard/utils/card-renderer.tsx | Dashboard card registry |
| src/app/components/shared/theme/theme-surface-tokens.ts | Shared surface theme tokens |
| src/app/hooks/use-ha-devices.ts | HA entity to device type mapping |
| src/app/hooks/device-mappers/ | Domain-specific HA device mapper modules |
| src/app/hooks/entity-utils/ | Shared HA entity parsing and formatting helpers |
| src/app/hooks/ha-entity-utils.ts | Compatibility barrel for entity transformation utilities |
| src/app/hooks/ha-device-mappers.ts | Compatibility barrel for the device mapper registry |
| src/app/stores/selectors.ts | Typed selectors for all stores |
| src/app/storybook/story-frames.tsx | Shared Storybook frame utilities — EntityCardStoryFrame, SettingsDialogStoryFrame, noopCardSizeChange |
| src/app/storybook/story-docs.ts | Story-specific documentation strings |
| src/app/ui-kit/ | Storybook UI-kit discovery pages |
| .storybook/main.ts | Storybook Vite integration, static assets, and base path support |
| docs/technical/REACT_ZUSTAND.md | State management decision guide |
| design-system/UI-GUIDELINES.md | Color system, typography, card sizes, glass effects |
| design-system/FEATURES.md | Feature map with test locations |
- Create
src/app/features/<name>/withindex.ts,components/, and optionallyhooks/andstores/. - If the feature has persisted state, create a Zustand store with
persistmiddleware. - If the feature reads HA entities, use
useHomeAssistant(homeAssistantSelectors.entities)— do not subscribe to the service directly. - Expose a single controller hook for the feature's root component to consume.
- Register the feature in
src/app/features/dashboard/components/dashboard-section-router.tsxusinglazy()if it needs a top-level section.
- Add the card in
src/app/features/<domain>/components/<name>-card/as a folder module. - Implement
use-<name>-card-controller.tsthat reads from the HA store and calls service methods. - Register in
src/app/hooks/use-ha-devices.tsunder the appropriate device type. - Register in
src/app/features/dashboard/utils/card-renderer.tsx. - Add to the "Add card" dialog in
src/app/features/dashboard/components/add-card-dialog/.
- Do not use
window.localStoragedirectly — usesrc/app/utils/storage. - Do not call
storeInstance.setState()from outside the store file — use its action methods. - Do not create React Context for shared reactive state — use Zustand.
- Do not add a catch-all HA service listener that syncs all fields on every event.
- Do not make multiple
useXyzStore(state => state.field)calls when a combined selector exists. - Do not make a controller hook longer than ~150 lines — split it.
- Do not import from inside a feature's subdirectories across feature boundaries — use the feature root
index.ts. - Do not duplicate a component, hook, or utility — check if something reusable already exists first.
- Do not solve a task with shortcut code that makes the codebase harder to maintain.
- Do not add new low-level shared UI to
src/app/components/shared/when it should live inprimitives/orpatterns/. - Do not re-implement a UI element that already exists in
src/app/components/primitives/— always check primitives before writing new UI. - Do not create a new
*.stories.*file for a component that already has one — extend the existing story file instead. - Do not place Storybook frame or utility components (e.g.
EntityCardStoryFrame,SettingsDialogStoryFrame) inside feature or dashboard subdirectories — they belong insrc/app/storybook/story-frames.tsx. - Do not skip unit tests for shared utilities, entity mappers, or controller logic — check for existing
__tests__/directories first.