Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.
2 changes: 1 addition & 1 deletion .github/skills/quality-gate/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: quality-gate
description: Required verification workflow for formatting, linting, tests, dependency audits, and builds.
description: Run before every commit or push: formatting, linting, tests, dependency audits, and builds.
---

# Quality Gate Workflow
Expand Down
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 3 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
[workspace]
members = [
"crates/webui-cli",
"crates/webui-expressions",
"crates/webui-ffi",
"crates/webui-handler",
"crates/webui-node",
"crates/webui-parser",
"crates/webui-protocol",
"crates/webui-state",
"crates/webui-wasm",
"crates/*",
"xtask",
"examples/integration/rust",
"examples/integration/ssr-performance-showdown",
]
resolver = "2"

Expand Down
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