Skip to content

Commit c09710d

Browse files
feat: add contact book manager app and Electron integration (#68)
* feat: add contact book manager app and Electron integration Add a sophisticated contact book manager example application and a generic Electron integration for running any WebUI app as a desktop application. Contact Book Manager (examples/app/contact-book-manager/) - 17 Atomic Design components (6 atoms, 4 molecules, 6 organisms, 1 root) - 7 views: Dashboard, All Contacts, Favorites, Groups, Detail, Add, Edit - Full CRUD operations with IndexedDB offline persistence - 15 sample contacts across 4 groups (Family, Work, Friends, Other) - Responsive layout with sidebar (desktop) and single column (mobile) - FAST-HTML hydration with connectedCallback event listeners - CSS ::before pseudo-elements for emoji icons (avoids protocol encoding) Electron Integration (examples/integration/electron/) - Generic Electron wrapper accepting CLI args: dist-dir, state.json, --plugin - Custom webui:// protocol scheme serves SSR HTML + static assets - Frameless window with titleBarOverlay for native window controls - Header component acts as drag handle via -webkit-app-region: drag - Uses process.dlopen for cross-platform native addon loading (.dll/.dylib/.so) - ESM throughout with import.meta.dirname for path resolution Condition Parser Bug Fix (crates/webui-parser/src/condition_parser.rs) - Fix string literal quote-stripping in parse_predicate() that caused expression evaluator MissingValue errors for conditions like page == 'dashboard'. Quotes are now preserved for is_literal() checks. Documentation - Add Electron handler page (docs/guide/concepts/handlers/electron.md) - Add Electron to handlers index and VitePress sidebar config - Add Examples section to Rust handler docs pointing to integration examples - Add electron to workspace catalog (pnpm-workspace.yaml)
1 parent 0ea1b96 commit c09710d

80 files changed

Lines changed: 4522 additions & 193 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/skills/pr/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ Closes #43
3939
```
4040

4141
> **Note:** Issue-linking keywords only work when the PR targets the repository's default branch. See [GitHub docs: linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for the full reference.
42+
43+
## PR description
44+
Remove the Co-author-by line from the PR description. If you want to credit a co-author, add them as a reviewer instead. And check all the changes from its merge-base to get a detailed summary for the commit.

.github/skills/quality-gate/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: quality-gate
3-
description: Required verification workflow for formatting, linting, tests, dependency audits, and builds.
3+
description: Run before every commit or push: formatting, linting, tests, dependency audits, and builds.
44
---
55

66
# Quality Gate Workflow

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
[workspace]
22
members = [
3-
"crates/webui-cli",
4-
"crates/webui-expressions",
5-
"crates/webui-ffi",
6-
"crates/webui-handler",
7-
"crates/webui-node",
8-
"crates/webui-parser",
9-
"crates/webui-protocol",
10-
"crates/webui-state",
11-
"crates/webui-wasm",
3+
"crates/*",
124
"xtask",
5+
"examples/integration/rust",
6+
"examples/integration/ssr-performance-showdown",
137
]
148
resolver = "2"
159

crates/webui-parser/src/condition_parser.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,6 @@ impl ConditionParser {
137137
_ => unreachable!(),
138138
};
139139

140-
// Clean up the right side (if it's a string literal)
141-
let right = if (right.starts_with('"') && right.ends_with('"'))
142-
|| (right.starts_with('\'') && right.ends_with('\''))
143-
{
144-
// Remove quotes
145-
&right[1..right.len() - 1]
146-
} else {
147-
right
148-
};
149-
150140
let predicate = ConditionExpr::predicate(left, operator, right);
151141

152142
return Ok(predicate);
@@ -318,7 +308,7 @@ mod tests {
318308
assert!(matches!(&result.expr, Some(Expr::Predicate(pred)) if
319309
pred.left == "name" &&
320310
ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
321-
pred.right == "John"
311+
pred.right == "\"John\""
322312
));
323313
}
324314

@@ -424,7 +414,7 @@ mod tests {
424414
matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Predicate(pred)) if
425415
pred.left == "appearance" &&
426416
ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
427-
pred.right == "hub"
417+
pred.right == "\"hub\""
428418
) &&
429419
LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
430420
matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "actions.trailing")

docs/.vitepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export default {
6363
{ text: 'Overview', link: '/guide/concepts/handlers/' },
6464
{ text: 'Rust', link: '/guide/concepts/handlers/rust' },
6565
{ text: 'Node.js', link: '/guide/concepts/handlers/node' },
66+
{ text: 'Electron', link: '/guide/concepts/handlers/electron' },
6667
{ text: 'WebAssembly', link: '/guide/concepts/handlers/wasm' },
6768
{ text: 'FFI (C API)', link: '/guide/concepts/handlers/ffi' }
6869
]
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# WebUI Electron Handler
2+
3+
WebUI apps can run as native desktop applications using Electron. The Electron integration uses the `webui-node` native addon to render pre-built protocols at startup, then serves the rendered HTML and static assets through a custom `webui://` protocol scheme — no HTTP server needed.
4+
5+
## How it Works
6+
7+
1. **Build phase**`webui build --plugin=fast` compiles templates into `protocol.bin`. esbuild bundles the client JS.
8+
2. **Startup** — Electron's main process loads the native addon (`webui-node`), reads `protocol.bin` + `state.json`, and calls `addon.render()` to produce the full SSR HTML.
9+
3. **Custom protocol** — A `webui://` protocol scheme is registered. When Electron loads `webui://app/`, it serves the rendered HTML. CSS and JS assets are served from the app's `dist/` directory.
10+
4. **Hydration** — The client JS bundle hydrates the SSR output, attaching event listeners and enabling interactivity — same as in a browser.
11+
12+
## Usage
13+
14+
```bash
15+
# 1. Build the native addon
16+
cargo build -p webui-node --release
17+
18+
# 2. Build a WebUI app (e.g., contact-book-manager)
19+
cd examples/app/contact-book-manager
20+
npm run build
21+
22+
# 3. Run it in Electron
23+
cd examples/integration/electron
24+
npm run build
25+
npx electron dist/main.js ../../app/contact-book-manager/dist ../../app/contact-book-manager/data/state.json --plugin=fast
26+
```
27+
28+
## CLI Arguments
29+
30+
| Argument | Description | Default |
31+
|----------|-------------|---------|
32+
| `dist-dir` | Path to app's `dist/` directory with `protocol.bin` and assets | `../../app/hello-world/dist` |
33+
| `state.json` | Path to state JSON file | `../../app/hello-world/data/state.json` |
34+
| `--plugin=fast` | Enable FAST hydration plugin | None |
35+
36+
## Custom Titlebar
37+
38+
The integration uses `titleBarStyle: 'hidden'` with `titleBarOverlay` for a frameless native look. The app's header component uses `-webkit-app-region: drag` to act as the drag handle. Interactive elements within the header use `-webkit-app-region: no-drag` to remain clickable.
39+
40+
## The `webui://` Protocol
41+
42+
Electron's custom protocol handler maps routes to content:
43+
44+
- `webui://app/` → SSR-rendered HTML
45+
- `webui://app/*.css` → CSS files from the dist directory
46+
- `webui://app/*.js` → JS bundles from the dist directory
47+
48+
This avoids the need for a local HTTP server and provides a clean, secure origin for the app.
49+
50+
## Example
51+
52+
A complete working example is available at [`examples/integration/electron/`](https://github.com/user/webui/tree/main/examples/integration/electron).
53+
54+
The [Contact Book Manager](https://github.com/user/webui/tree/main/examples/app/contact-book-manager) app demonstrates a full-featured WebUI application that works both in the browser (via `webui start`) and as an Electron desktop app.
55+
56+
## Performance Notes
57+
58+
- The native addon renders the entire page synchronously at startup — no per-request overhead.
59+
- Protocol data is loaded once and rendered once. The custom protocol handler serves pre-rendered HTML from memory.
60+
- Client-side hydration runs identically to the browser — same JS bundle, same FAST-HTML components.

docs/guide/concepts/handlers/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ WebUI provides official handlers for several popular programming languages (othe
88

99
- [**Rust**](./rust) - High-performance native rendering with the Rust programming language
1010
- [**Node.js**](./node) - Streaming SSR via a native addon built with napi-rs
11+
- [**Electron**](./electron) - Desktop apps via Electron with custom `webui://` protocol
1112
- [**WebAssembly**](./wasm) - In-browser rendering for playgrounds and client-side use
1213
- [**FFI (C API)**](./ffi) - Shared library for Go, C#, Python, and any language with C interop
1314

docs/guide/concepts/handlers/rust.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,9 @@ pub enum HandlerError {
186186
```
187187

188188
You can handle these specific error cases to provide better error messages for different failure scenarios.
189+
190+
## Examples
191+
192+
Working integration examples are available in the repository:
193+
194+
- [`examples/integration/rust/`](https://github.com/user/webui/tree/main/examples/integration/rust) — Rust HTTP server integrations (hyper, tiny_http)
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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

Comments
 (0)