|
| 1 | +# Contact Book Manager |
| 2 | + |
| 3 | +A full-featured contact book manager built with **WebUI SSR** and **FAST-Element** client hydration. Demonstrates Atomic Design component architecture, IndexedDB offline storage, client-side routing, and responsive layout — all rendered server-side with the `--plugin=fast` pipeline. |
| 4 | + |
| 5 | +## Views |
| 6 | + |
| 7 | +The app implements 7 views, routed via the `page` attribute on the root `<cb-app>` element: |
| 8 | + |
| 9 | +| View | Description | |
| 10 | +|------|-------------| |
| 11 | +| **Dashboard** | Stats row (total contacts, favorites, groups) + 5 most recent contacts | |
| 12 | +| **All Contacts** | Searchable list of all contacts | |
| 13 | +| **Favorites** | Filtered view of starred contacts | |
| 14 | +| **Group View** | Contacts filtered by group (Work, Family, Friends, Other) | |
| 15 | +| **Contact Detail** | Full contact profile with edit, favorite, and delete actions | |
| 16 | +| **Add Contact** | Empty form to create a new contact | |
| 17 | +| **Edit Contact** | Pre-filled form to modify an existing contact | |
| 18 | + |
| 19 | +The app ships with **15 sample contacts** across **4 groups** (Work, Family, Friends, Other). |
| 20 | + |
| 21 | +## Architecture |
| 22 | + |
| 23 | +### Atomic Design |
| 24 | + |
| 25 | +Components follow [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/) principles: |
| 26 | + |
| 27 | +``` |
| 28 | +src/ |
| 29 | +├── cb-app/ # Root shell — state, routing, event delegation |
| 30 | +├── atoms/ # 6 stateless presentational primitives |
| 31 | +├── molecules/ # 4 composite atom groupings |
| 32 | +└── organisms/ # 6 full feature sections |
| 33 | +``` |
| 34 | + |
| 35 | +### SSR + Hydration |
| 36 | + |
| 37 | +1. **Server** — The Rust `webui-cli` pre-renders HTML using `--plugin=fast`. Templates use `{{mustache}}` interpolation, `<if condition="...">`, and `<for each="...">` directives, evaluated against `data/state.json`. |
| 38 | +2. **Client** — `index.ts` registers all 17 components with `templateOptions: 'defer-and-hydrate'`. Templates are NOT bundled into JS — they already exist in the DOM from SSR. |
| 39 | +3. **Hydration** — Components with a `prepare()` method read initial state from server-rendered DOM (e.g., contacts from hidden `data-*` spans), then FAST's observation system takes over for reactivity. |
| 40 | + |
| 41 | +### State Management |
| 42 | + |
| 43 | +All state lives in the root `<cb-app>` component: |
| 44 | + |
| 45 | +- **`@attr`** fields for HTML-reflected attributes (`page`, `searchQuery`, `activeGroup`, counts) |
| 46 | +- **`@observable`** fields for internal arrays (`contacts`, `filteredContacts`, `favoriteContacts`, `selectedContact`, `groups`) |
| 47 | +- **IndexedDB** (`ContactBookDB`) persists contacts client-side. On init, `prepare()` seeds IndexedDB from server data if needed; every mutation calls `saveToDB()` immediately. |
| 48 | + |
| 49 | +### Event Handling |
| 50 | + |
| 51 | +Child components communicate upward via **bubbling `CustomEvent`s** (`bubbles: true, composed: true`). The root `<cb-app>` listens on its `shadowRoot` in `connectedCallback()` — a delegation pattern. |
| 52 | + |
| 53 | +``` |
| 54 | +cb-header → 'search', 'add-contact' |
| 55 | +cb-sidebar → 'navigate' |
| 56 | +cb-contact-card → 'select-contact' |
| 57 | +cb-contact-detail → 'edit-contact', 'toggle-favorite', 'delete-contact', 'back' |
| 58 | +cb-contact-form → 'form-save', 'form-cancel' |
| 59 | +``` |
| 60 | + |
| 61 | +## Prerequisites |
| 62 | + |
| 63 | +- **Rust toolchain** — see `rust-toolchain.toml` at the repo root |
| 64 | +- **Node.js** (≥18) + **pnpm** |
| 65 | + |
| 66 | +## Quick Start |
| 67 | + |
| 68 | +```bash |
| 69 | +# From the repository root: |
| 70 | + |
| 71 | +# Install dependencies |
| 72 | +pnpm install |
| 73 | + |
| 74 | +# Build the SSR protocol binary |
| 75 | +cargo run -p webui-cli -- build ./examples/app/contact-book-manager/src \ |
| 76 | + --out ./examples/app/contact-book-manager/dist \ |
| 77 | + --css external \ |
| 78 | + --plugin=fast |
| 79 | + |
| 80 | +# Bundle client JS |
| 81 | +npx esbuild examples/app/contact-book-manager/src/index.ts \ |
| 82 | + --bundle --outfile=examples/app/contact-book-manager/dist/index.js \ |
| 83 | + --format=esm --sourcemap |
| 84 | + |
| 85 | +# Start dev server |
| 86 | +cargo run -p webui-cli -- start ./examples/app/contact-book-manager/src \ |
| 87 | + --state ./examples/app/contact-book-manager/data/state.json \ |
| 88 | + --plugin=fast \ |
| 89 | + --servedir ./examples/app/contact-book-manager/dist \ |
| 90 | + --port 3001 |
| 91 | +``` |
| 92 | + |
| 93 | +Or use the xtask shortcut (runs server + client watcher concurrently): |
| 94 | + |
| 95 | +```bash |
| 96 | +cargo xtask dev contact-book-manager |
| 97 | +``` |
| 98 | + |
| 99 | +Then open [http://localhost:3001](http://localhost:3001). |
| 100 | + |
| 101 | +## Project Structure |
| 102 | + |
| 103 | +``` |
| 104 | +contact-book-manager/ |
| 105 | +├── package.json |
| 106 | +├── tsconfig.json |
| 107 | +├── data/ |
| 108 | +│ └── state.json # Pre-seeded state (15 contacts, 4 groups) |
| 109 | +├── dist/ # Build output |
| 110 | +│ ├── protocol.bin # SSR binary |
| 111 | +│ ├── index.js # Bundled client JS |
| 112 | +│ └── cb-*.css # Per-component stylesheets |
| 113 | +└── src/ |
| 114 | + ├── index.html # Root HTML shell |
| 115 | + ├── index.ts # Entry — registers all 17 components |
| 116 | + ├── cb-app/ # Root app component |
| 117 | + │ ├── cb-app.ts |
| 118 | + │ ├── cb-app.html |
| 119 | + │ └── cb-app.css |
| 120 | + ├── atoms/ |
| 121 | + │ ├── cb-avatar/ # Circular initials avatar |
| 122 | + │ ├── cb-badge/ # Colored group pill |
| 123 | + │ ├── cb-button/ # Multi-variant button |
| 124 | + │ ├── cb-empty-state/ # Placeholder for empty lists |
| 125 | + │ ├── cb-icon-button/ # Square icon-only button |
| 126 | + │ └── cb-input/ # Styled text input |
| 127 | + ├── molecules/ |
| 128 | + │ ├── cb-form-field/ # Label + input + error message |
| 129 | + │ ├── cb-nav-item/ # Sidebar navigation row |
| 130 | + │ ├── cb-search-bar/ # Search input with clear button |
| 131 | + │ └── cb-stat-card/ # Dashboard KPI card |
| 132 | + ├── organisms/ |
| 133 | + │ ├── cb-contact-card/ # Compact contact row |
| 134 | + │ ├── cb-contact-detail/ # Full contact profile view |
| 135 | + │ ├── cb-contact-form/ # Add/edit contact form |
| 136 | + │ ├── cb-contact-list/ # Scrollable contact list |
| 137 | + │ ├── cb-header/ # Sticky top bar |
| 138 | + │ └── cb-sidebar/ # Left navigation panel |
| 139 | +``` |
| 140 | + |
| 141 | +## Component Catalog |
| 142 | + |
| 143 | +| Layer | Tag | Purpose | |
| 144 | +|-------|-----|---------| |
| 145 | +| **Root** | `<cb-app>` | Application shell — state, routing, event delegation, IndexedDB | |
| 146 | +| **Atom** | `<cb-avatar>` | Circular avatar with initials and colored background (sm/md/lg) | |
| 147 | +| **Atom** | `<cb-badge>` | Pill label with group color variants (work/family/friends/other) | |
| 148 | +| **Atom** | `<cb-button>` | Button with variant (primary/secondary/danger/ghost) and size | |
| 149 | +| **Atom** | `<cb-empty-state>` | Centered icon + message for empty content areas | |
| 150 | +| **Atom** | `<cb-icon-button>` | Square icon-only button with optional danger hover | |
| 151 | +| **Atom** | `<cb-input>` | Styled text input with placeholder, type, and name | |
| 152 | +| **Molecule** | `<cb-form-field>` | Label + `<cb-input>` + optional error message | |
| 153 | +| **Molecule** | `<cb-nav-item>` | Sidebar row with icon, label, count badge, and active state | |
| 154 | +| **Molecule** | `<cb-search-bar>` | Search input with icon and conditional clear button | |
| 155 | +| **Molecule** | `<cb-stat-card>` | Dashboard card with emoji icon, numeric value, and label | |
| 156 | +| **Organism** | `<cb-contact-card>` | Compact contact row: avatar, name, star, email, phone, badge | |
| 157 | +| **Organism** | `<cb-contact-detail>` | Full-page contact view with edit/favorite/delete actions | |
| 158 | +| **Organism** | `<cb-contact-form>` | Two-column add/edit form with group selector and notes | |
| 159 | +| **Organism** | `<cb-contact-list>` | Renders contact cards via `<for>` loop with empty state fallback | |
| 160 | +| **Organism** | `<cb-header>` | Sticky top bar with title, search, and "Add Contact" button | |
| 161 | +| **Organism** | `<cb-sidebar>` | Fixed nav panel with static items + dynamic group list | |
| 162 | + |
| 163 | +## Key Design Decisions |
| 164 | + |
| 165 | +### Use `connectedCallback` listeners, not template `@event` bindings |
| 166 | + |
| 167 | +FAST-HTML hydration processes templates declaratively. Event bindings like `@click` are not supported in the SSR template syntax. Instead, components attach listeners manually in `connectedCallback()` and use a `listenersAttached` guard to prevent duplicates on reconnection. |
| 168 | + |
| 169 | +### Use `dispatchEvent` instead of `$emit` |
| 170 | + |
| 171 | +FAST-Element's `$emit` helper is not available during the hydration stage. Components use `this.dispatchEvent(new CustomEvent(...))` directly, with `bubbles: true` and `composed: true` to cross shadow DOM boundaries. |
| 172 | + |
| 173 | +### Use `field!: type` for fields set in `prepare()` |
| 174 | + |
| 175 | +Fields initialized by the `prepare()` lifecycle hook use TypeScript's definite assignment assertion (`!`) rather than default values. This avoids overwriting server-hydrated state with empty defaults. |
| 176 | + |
| 177 | +### No nested custom elements in templates |
| 178 | + |
| 179 | +Server-rendered templates avoid nesting custom elements inside other custom element templates to prevent hydration mismatches between the SSR output and the client's shadow DOM expectations. |
| 180 | + |
| 181 | +### Use CSS `::before` for emoji, not inline emoji |
| 182 | + |
| 183 | +Emoji characters in SSR templates can cause double-encoding issues during the server render pass. Components use CSS `::before` pseudo-elements with `content` properties for display emoji instead. |
| 184 | + |
| 185 | +### `data-action` delegation for multi-button components |
| 186 | + |
| 187 | +Components with multiple action buttons (e.g., `<cb-contact-detail>`) use a single `click` listener and identify the action via `data-action` attributes, dispatching the appropriate event in a switch/case block. |
| 188 | + |
| 189 | +## Responsive Layout |
| 190 | + |
| 191 | +- **Desktop (≥768px):** Fixed sidebar (260px) + scrollable main content area |
| 192 | +- **Mobile (<768px):** Sidebar hidden, single-column layout, "Add Contact" button label hidden (icon only) |
0 commit comments