Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/skills/pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ Closes #43
```

> **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.

## PR description
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.
14 changes: 2 additions & 12 deletions crates/webui-parser/src/condition_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,6 @@ impl ConditionParser {
_ => unreachable!(),
};

// Clean up the right side (if it's a string literal)
let right = if (right.starts_with('"') && right.ends_with('"'))
|| (right.starts_with('\'') && right.ends_with('\''))
{
// Remove quotes
&right[1..right.len() - 1]
} else {
right
};

let predicate = ConditionExpr::predicate(left, operator, right);

return Ok(predicate);
Expand Down Expand Up @@ -318,7 +308,7 @@ mod tests {
assert!(matches!(&result.expr, Some(Expr::Predicate(pred)) if
pred.left == "name" &&
ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
pred.right == "John"
pred.right == "\"John\""
));
}

Expand Down Expand Up @@ -424,7 +414,7 @@ mod tests {
matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Predicate(pred)) if
pred.left == "appearance" &&
ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
pred.right == "hub"
pred.right == "\"hub\""
) &&
LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "actions.trailing")
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export default {
{ text: 'Overview', link: '/guide/concepts/handlers/' },
{ text: 'Rust', link: '/guide/concepts/handlers/rust' },
{ text: 'Node.js', link: '/guide/concepts/handlers/node' },
{ text: 'Electron', link: '/guide/concepts/handlers/electron' },
{ text: 'WebAssembly', link: '/guide/concepts/handlers/wasm' },
{ text: 'FFI (C API)', link: '/guide/concepts/handlers/ffi' }
]
Expand Down
60 changes: 60 additions & 0 deletions docs/guide/concepts/handlers/electron.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# WebUI Electron Handler

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.

## How it Works

1. **Build phase** — `webui build --plugin=fast` compiles templates into `protocol.bin`. esbuild bundles the client JS.
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.
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.
4. **Hydration** — The client JS bundle hydrates the SSR output, attaching event listeners and enabling interactivity — same as in a browser.

## Usage

```bash
# 1. Build the native addon
cargo build -p webui-node --release

# 2. Build a WebUI app (e.g., contact-book-manager)
cd examples/app/contact-book-manager
npm run build

# 3. Run it in Electron
cd examples/integration/electron
npm run build
npx electron dist/main.js ../../app/contact-book-manager/dist ../../app/contact-book-manager/data/state.json --plugin=fast
```

## CLI Arguments

| Argument | Description | Default |
|----------|-------------|---------|
| `dist-dir` | Path to app's `dist/` directory with `protocol.bin` and assets | `../../app/hello-world/dist` |
| `state.json` | Path to state JSON file | `../../app/hello-world/data/state.json` |
| `--plugin=fast` | Enable FAST hydration plugin | None |

## Custom Titlebar

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.

## The `webui://` Protocol

Electron's custom protocol handler maps routes to content:

- `webui://app/` → SSR-rendered HTML
- `webui://app/*.css` → CSS files from the dist directory
- `webui://app/*.js` → JS bundles from the dist directory

This avoids the need for a local HTTP server and provides a clean, secure origin for the app.

## Example

A complete working example is available at [`examples/integration/electron/`](https://github.com/user/webui/tree/main/examples/integration/electron).

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.

## Performance Notes

- The native addon renders the entire page synchronously at startup — no per-request overhead.
- Protocol data is loaded once and rendered once. The custom protocol handler serves pre-rendered HTML from memory.
- Client-side hydration runs identically to the browser — same JS bundle, same FAST-HTML components.
1 change: 1 addition & 0 deletions docs/guide/concepts/handlers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ WebUI provides official handlers for several popular programming languages (othe

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

Expand Down
6 changes: 6 additions & 0 deletions docs/guide/concepts/handlers/rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,9 @@ pub enum HandlerError {
```

You can handle these specific error cases to provide better error messages for different failure scenarios.

## Examples

Working integration examples are available in the repository:

- [`examples/integration/rust/`](https://github.com/user/webui/tree/main/examples/integration/rust) — Rust HTTP server integrations (hyper, tiny_http)
192 changes: 192 additions & 0 deletions examples/app/contact-book-manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Contact Book Manager

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.

## Views

The app implements 7 views, routed via the `page` attribute on the root `<cb-app>` element:

| View | Description |
|------|-------------|
| **Dashboard** | Stats row (total contacts, favorites, groups) + 5 most recent contacts |
| **All Contacts** | Searchable list of all contacts |
| **Favorites** | Filtered view of starred contacts |
| **Group View** | Contacts filtered by group (Work, Family, Friends, Other) |
| **Contact Detail** | Full contact profile with edit, favorite, and delete actions |
| **Add Contact** | Empty form to create a new contact |
| **Edit Contact** | Pre-filled form to modify an existing contact |

The app ships with **15 sample contacts** across **4 groups** (Work, Family, Friends, Other).

## Architecture

### Atomic Design

Components follow [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/) principles:

```
src/
├── cb-app/ # Root shell — state, routing, event delegation
├── atoms/ # 6 stateless presentational primitives
├── molecules/ # 4 composite atom groupings
└── organisms/ # 6 full feature sections
```

### SSR + Hydration

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`.
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.
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.

### State Management

All state lives in the root `<cb-app>` component:

- **`@attr`** fields for HTML-reflected attributes (`page`, `searchQuery`, `activeGroup`, counts)
- **`@observable`** fields for internal arrays (`contacts`, `filteredContacts`, `favoriteContacts`, `selectedContact`, `groups`)
- **IndexedDB** (`ContactBookDB`) persists contacts client-side. On init, `prepare()` seeds IndexedDB from server data if needed; every mutation calls `saveToDB()` immediately.

### Event Handling

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.

```
cb-header → 'search', 'add-contact'
cb-sidebar → 'navigate'
cb-contact-card → 'select-contact'
cb-contact-detail → 'edit-contact', 'toggle-favorite', 'delete-contact', 'back'
cb-contact-form → 'form-save', 'form-cancel'
```

## Prerequisites

- **Rust toolchain** — see `rust-toolchain.toml` at the repo root
- **Node.js** (≥18) + **pnpm**

## Quick Start

```bash
# From the repository root:

# Install dependencies
pnpm install

# Build the SSR protocol binary
cargo run -p webui-cli -- build ./examples/app/contact-book-manager/src \
--out ./examples/app/contact-book-manager/dist \
--css external \
--plugin=fast

# Bundle client JS
npx esbuild examples/app/contact-book-manager/src/index.ts \
--bundle --outfile=examples/app/contact-book-manager/dist/index.js \
--format=esm --sourcemap

# Start dev server
cargo run -p webui-cli -- start ./examples/app/contact-book-manager/src \
--state ./examples/app/contact-book-manager/data/state.json \
--plugin=fast \
--servedir ./examples/app/contact-book-manager/dist \
--port 3001
```

Or use the xtask shortcut (runs server + client watcher concurrently):

```bash
cargo xtask dev contact-book-manager
```

Then open [http://localhost:3001](http://localhost:3001).

## Project Structure

```
contact-book-manager/
├── package.json
├── tsconfig.json
├── data/
│ └── state.json # Pre-seeded state (15 contacts, 4 groups)
├── dist/ # Build output
│ ├── protocol.bin # SSR binary
│ ├── index.js # Bundled client JS
│ └── cb-*.css # Per-component stylesheets
└── src/
├── index.html # Root HTML shell
├── index.ts # Entry — registers all 17 components
├── cb-app/ # Root app component
│ ├── cb-app.ts
│ ├── cb-app.html
│ └── cb-app.css
├── atoms/
│ ├── cb-avatar/ # Circular initials avatar
│ ├── cb-badge/ # Colored group pill
│ ├── cb-button/ # Multi-variant button
│ ├── cb-empty-state/ # Placeholder for empty lists
│ ├── cb-icon-button/ # Square icon-only button
│ └── cb-input/ # Styled text input
├── molecules/
│ ├── cb-form-field/ # Label + input + error message
│ ├── cb-nav-item/ # Sidebar navigation row
│ ├── cb-search-bar/ # Search input with clear button
│ └── cb-stat-card/ # Dashboard KPI card
├── organisms/
│ ├── cb-contact-card/ # Compact contact row
│ ├── cb-contact-detail/ # Full contact profile view
│ ├── cb-contact-form/ # Add/edit contact form
│ ├── cb-contact-list/ # Scrollable contact list
│ ├── cb-header/ # Sticky top bar
│ └── cb-sidebar/ # Left navigation panel
```

## Component Catalog

| Layer | Tag | Purpose |
|-------|-----|---------|
| **Root** | `<cb-app>` | Application shell — state, routing, event delegation, IndexedDB |
| **Atom** | `<cb-avatar>` | Circular avatar with initials and colored background (sm/md/lg) |
| **Atom** | `<cb-badge>` | Pill label with group color variants (work/family/friends/other) |
| **Atom** | `<cb-button>` | Button with variant (primary/secondary/danger/ghost) and size |
| **Atom** | `<cb-empty-state>` | Centered icon + message for empty content areas |
| **Atom** | `<cb-icon-button>` | Square icon-only button with optional danger hover |
| **Atom** | `<cb-input>` | Styled text input with placeholder, type, and name |
| **Molecule** | `<cb-form-field>` | Label + `<cb-input>` + optional error message |
| **Molecule** | `<cb-nav-item>` | Sidebar row with icon, label, count badge, and active state |
| **Molecule** | `<cb-search-bar>` | Search input with icon and conditional clear button |
| **Molecule** | `<cb-stat-card>` | Dashboard card with emoji icon, numeric value, and label |
| **Organism** | `<cb-contact-card>` | Compact contact row: avatar, name, star, email, phone, badge |
| **Organism** | `<cb-contact-detail>` | Full-page contact view with edit/favorite/delete actions |
| **Organism** | `<cb-contact-form>` | Two-column add/edit form with group selector and notes |
| **Organism** | `<cb-contact-list>` | Renders contact cards via `<for>` loop with empty state fallback |
| **Organism** | `<cb-header>` | Sticky top bar with title, search, and "Add Contact" button |
| **Organism** | `<cb-sidebar>` | Fixed nav panel with static items + dynamic group list |

## Key Design Decisions

### Use `connectedCallback` listeners, not template `@event` bindings

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.

### Use `dispatchEvent` instead of `$emit`

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.

### Use `field!: type` for fields set in `prepare()`

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.

### No nested custom elements in templates

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.

### Use CSS `::before` for emoji, not inline emoji

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.

### `data-action` delegation for multi-button components

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.

## Responsive Layout

- **Desktop (≥768px):** Fixed sidebar (260px) + scrollable main content area
- **Mobile (<768px):** Sidebar hidden, single-column layout, "Add Contact" button label hidden (icon only)
Loading
Loading