From 27686a2186816f9a9fdb70b3a871ee2f20b6dfda Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Thu, 5 Mar 2026 08:58:39 -0800 Subject: [PATCH 1/8] 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) --- .github/skills/pr/SKILL.md | 3 + crates/webui-parser/src/condition_parser.rs | 14 +- docs/.vitepress/config.js | 1 + docs/guide/concepts/handlers/electron.md | 60 ++ docs/guide/concepts/handlers/index.md | 1 + docs/guide/concepts/handlers/rust.md | 6 + examples/app/contact-book-manager/README.md | 207 ++++++ .../app/contact-book-manager/data/state.json | 578 +++++++++++++++++ .../app/contact-book-manager/package.json | 21 + .../src/atoms/cb-avatar/cb-avatar.css | 33 + .../src/atoms/cb-avatar/cb-avatar.html | 5 + .../src/atoms/cb-avatar/cb-avatar.ts | 13 + .../src/atoms/cb-badge/cb-badge.css | 34 + .../src/atoms/cb-badge/cb-badge.html | 3 + .../src/atoms/cb-badge/cb-badge.ts | 12 + .../src/atoms/cb-button/cb-button.css | 73 +++ .../src/atoms/cb-button/cb-button.html | 5 + .../src/atoms/cb-button/cb-button.ts | 13 + .../atoms/cb-empty-state/cb-empty-state.css | 32 + .../atoms/cb-empty-state/cb-empty-state.html | 7 + .../atoms/cb-empty-state/cb-empty-state.ts | 13 + .../atoms/cb-icon-button/cb-icon-button.css | 33 + .../atoms/cb-icon-button/cb-icon-button.html | 5 + .../atoms/cb-icon-button/cb-icon-button.ts | 13 + .../src/atoms/cb-input/cb-input.css | 27 + .../src/atoms/cb-input/cb-input.html | 3 + .../src/atoms/cb-input/cb-input.ts | 14 + .../src/cb-app/cb-app.css | 93 +++ .../src/cb-app/cb-app.html | 193 ++++++ .../contact-book-manager/src/cb-app/cb-app.ts | 350 ++++++++++ .../app/contact-book-manager/src/index.html | 39 ++ .../app/contact-book-manager/src/index.ts | 68 ++ .../molecules/cb-form-field/cb-form-field.css | 37 ++ .../cb-form-field/cb-form-field.html | 9 + .../molecules/cb-form-field/cb-form-field.ts | 16 + .../src/molecules/cb-nav-item/cb-nav-item.css | 49 ++ .../molecules/cb-nav-item/cb-nav-item.html | 9 + .../src/molecules/cb-nav-item/cb-nav-item.ts | 26 + .../molecules/cb-search-bar/cb-search-bar.css | 54 ++ .../cb-search-bar/cb-search-bar.html | 9 + .../molecules/cb-search-bar/cb-search-bar.ts | 43 ++ .../molecules/cb-stat-card/cb-stat-card.css | 34 + .../molecules/cb-stat-card/cb-stat-card.html | 9 + .../molecules/cb-stat-card/cb-stat-card.ts | 13 + .../cb-contact-card/cb-contact-card.css | 88 +++ .../cb-contact-card/cb-contact-card.html | 18 + .../cb-contact-card/cb-contact-card.ts | 37 ++ .../cb-contact-detail/cb-contact-detail.css | 173 +++++ .../cb-contact-detail/cb-contact-detail.html | 53 ++ .../cb-contact-detail/cb-contact-detail.ts | 60 ++ .../cb-contact-form/cb-contact-form.css | 149 +++++ .../cb-contact-form/cb-contact-form.html | 47 ++ .../cb-contact-form/cb-contact-form.ts | 82 +++ .../cb-contact-list/cb-contact-list.css | 14 + .../cb-contact-list/cb-contact-list.html | 31 + .../cb-contact-list/cb-contact-list.ts | 53 ++ .../src/organisms/cb-header/cb-header.css | 105 +++ .../src/organisms/cb-header/cb-header.html | 19 + .../src/organisms/cb-header/cb-header.ts | 45 ++ .../src/organisms/cb-sidebar/cb-sidebar.css | 92 +++ .../src/organisms/cb-sidebar/cb-sidebar.html | 30 + .../src/organisms/cb-sidebar/cb-sidebar.ts | 77 +++ .../app/contact-book-manager/tsconfig.json | 19 + examples/integration/electron/README.md | 44 ++ examples/integration/electron/main.ts | 137 ++++ examples/integration/electron/package.json | 15 + examples/integration/electron/preload.ts | 7 + examples/integration/electron/tsconfig.json | 14 + pnpm-lock.yaml | 597 +++++++++++++++++- pnpm-workspace.yaml | 8 +- 70 files changed, 4273 insertions(+), 21 deletions(-) create mode 100644 docs/guide/concepts/handlers/electron.md create mode 100644 examples/app/contact-book-manager/README.md create mode 100644 examples/app/contact-book-manager/data/state.json create mode 100644 examples/app/contact-book-manager/package.json create mode 100644 examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.css create mode 100644 examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.html create mode 100644 examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.ts create mode 100644 examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.css create mode 100644 examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.html create mode 100644 examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.ts create mode 100644 examples/app/contact-book-manager/src/atoms/cb-button/cb-button.css create mode 100644 examples/app/contact-book-manager/src/atoms/cb-button/cb-button.html create mode 100644 examples/app/contact-book-manager/src/atoms/cb-button/cb-button.ts create mode 100644 examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.css create mode 100644 examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.html create mode 100644 examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.ts create mode 100644 examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.css create mode 100644 examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.html create mode 100644 examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.ts create mode 100644 examples/app/contact-book-manager/src/atoms/cb-input/cb-input.css create mode 100644 examples/app/contact-book-manager/src/atoms/cb-input/cb-input.html create mode 100644 examples/app/contact-book-manager/src/atoms/cb-input/cb-input.ts create mode 100644 examples/app/contact-book-manager/src/cb-app/cb-app.css create mode 100644 examples/app/contact-book-manager/src/cb-app/cb-app.html create mode 100644 examples/app/contact-book-manager/src/cb-app/cb-app.ts create mode 100644 examples/app/contact-book-manager/src/index.html create mode 100644 examples/app/contact-book-manager/src/index.ts create mode 100644 examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.css create mode 100644 examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.html create mode 100644 examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.ts create mode 100644 examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.css create mode 100644 examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.html create mode 100644 examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.ts create mode 100644 examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.css create mode 100644 examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.html create mode 100644 examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.ts create mode 100644 examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.css create mode 100644 examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.html create mode 100644 examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.ts create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.css create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.html create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.ts create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.css create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.html create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.ts create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.css create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.html create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.ts create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.css create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.html create mode 100644 examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.ts create mode 100644 examples/app/contact-book-manager/src/organisms/cb-header/cb-header.css create mode 100644 examples/app/contact-book-manager/src/organisms/cb-header/cb-header.html create mode 100644 examples/app/contact-book-manager/src/organisms/cb-header/cb-header.ts create mode 100644 examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.css create mode 100644 examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.html create mode 100644 examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.ts create mode 100644 examples/app/contact-book-manager/tsconfig.json create mode 100644 examples/integration/electron/README.md create mode 100644 examples/integration/electron/main.ts create mode 100644 examples/integration/electron/package.json create mode 100644 examples/integration/electron/preload.ts create mode 100644 examples/integration/electron/tsconfig.json diff --git a/.github/skills/pr/SKILL.md b/.github/skills/pr/SKILL.md index c9c55ee3..1d6c1875 100644 --- a/.github/skills/pr/SKILL.md +++ b/.github/skills/pr/SKILL.md @@ -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. \ No newline at end of file diff --git a/crates/webui-parser/src/condition_parser.rs b/crates/webui-parser/src/condition_parser.rs index 05c0c216..37a98c54 100644 --- a/crates/webui-parser/src/condition_parser.rs +++ b/crates/webui-parser/src/condition_parser.rs @@ -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); @@ -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\"" )); } @@ -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") diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index c2231c7d..8a831d6e 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -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' } ] diff --git a/docs/guide/concepts/handlers/electron.md b/docs/guide/concepts/handlers/electron.md new file mode 100644 index 00000000..8aed60f3 --- /dev/null +++ b/docs/guide/concepts/handlers/electron.md @@ -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. diff --git a/docs/guide/concepts/handlers/index.md b/docs/guide/concepts/handlers/index.md index 29052490..3b1bf9f9 100644 --- a/docs/guide/concepts/handlers/index.md +++ b/docs/guide/concepts/handlers/index.md @@ -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 diff --git a/docs/guide/concepts/handlers/rust.md b/docs/guide/concepts/handlers/rust.md index 97d3bc18..590d9374 100644 --- a/docs/guide/concepts/handlers/rust.md +++ b/docs/guide/concepts/handlers/rust.md @@ -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) diff --git a/examples/app/contact-book-manager/README.md b/examples/app/contact-book-manager/README.md new file mode 100644 index 00000000..dfe83913 --- /dev/null +++ b/examples/app/contact-book-manager/README.md @@ -0,0 +1,207 @@ +# 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 `` 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, ``, and `` 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 `` 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 `` 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 + └── electron/ # Optional Electron wrapper + ├── main.ts + └── preload.ts +``` + +## Component Catalog + +| Layer | Tag | Purpose | +|-------|-----|---------| +| **Root** | `` | Application shell — state, routing, event delegation, IndexedDB | +| **Atom** | `` | Circular avatar with initials and colored background (sm/md/lg) | +| **Atom** | `` | Pill label with group color variants (work/family/friends/other) | +| **Atom** | `` | Button with variant (primary/secondary/danger/ghost) and size | +| **Atom** | `` | Centered icon + message for empty content areas | +| **Atom** | `` | Square icon-only button with optional danger hover | +| **Atom** | `` | Styled text input with placeholder, type, and name | +| **Molecule** | `` | Label + `` + optional error message | +| **Molecule** | `` | Sidebar row with icon, label, count badge, and active state | +| **Molecule** | `` | Search input with icon and conditional clear button | +| **Molecule** | `` | Dashboard card with emoji icon, numeric value, and label | +| **Organism** | `` | Compact contact row: avatar, name, star, email, phone, badge | +| **Organism** | `` | Full-page contact view with edit/favorite/delete actions | +| **Organism** | `` | Two-column add/edit form with group selector and notes | +| **Organism** | `` | Renders contact cards via `` loop with empty state fallback | +| **Organism** | `` | Sticky top bar with title, search, and "Add Contact" button | +| **Organism** | `` | 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., ``) 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) + +## Electron (Optional) + +The `src/electron/` directory contains an optional desktop wrapper. It uses the `webui-node` native addon to render `protocol.bin` + `state.json` into HTML, then serves it via a custom `webui://` protocol scheme inside an Electron `BrowserWindow`. + +To use it, build the native addon first: + +```bash +cargo build -p webui-node --release +``` + +The Electron entry point is not part of the standard web build — it is an alternative deployment target that reuses the same SSR binary and client JS bundle. diff --git a/examples/app/contact-book-manager/data/state.json b/examples/app/contact-book-manager/data/state.json new file mode 100644 index 00000000..e63dfe9f --- /dev/null +++ b/examples/app/contact-book-manager/data/state.json @@ -0,0 +1,578 @@ +{ + "page": "dashboard", + "searchQuery": "", + "activeGroup": "all", + "groups": ["Family", "Work", "Friends", "Other"], + "totalContacts": 15, + "totalFavorites": 5, + "totalGroups": 4, + "contacts": [ + { + "id": "1", + "firstName": "Sarah", + "lastName": "Chen", + "email": "sarah.chen@example.com", + "phone": "+1 (555) 123-4567", + "company": "Contoso Ltd", + "group": "Work", + "favorite": true, + "initials": "SC", + "avatarColor": "#4A90D9", + "notes": "Met at the tech conference in Seattle", + "address": "123 Innovation Dr, Seattle, WA 98101" + }, + { + "id": "2", + "firstName": "Marcus", + "lastName": "Johnson", + "email": "marcus.johnson@example.com", + "phone": "+1 (555) 234-5678", + "company": "", + "group": "Family", + "favorite": false, + "initials": "MJ", + "avatarColor": "#E67E22", + "notes": "", + "address": "456 Oak Avenue, Portland, OR 97201" + }, + { + "id": "3", + "firstName": "Yuki", + "lastName": "Tanaka", + "email": "yuki.tanaka@example.com", + "phone": "+1 (555) 345-6789", + "company": "Fabrikam Inc", + "group": "Work", + "favorite": true, + "initials": "YT", + "avatarColor": "#2ECC71", + "notes": "Collaborating on the API redesign project", + "address": "789 Market St, San Francisco, CA 94103" + }, + { + "id": "4", + "firstName": "Priya", + "lastName": "Sharma", + "email": "priya.sharma@example.com", + "phone": "+1 (555) 456-7890", + "company": "", + "group": "Friends", + "favorite": false, + "initials": "PS", + "avatarColor": "#9B59B6", + "notes": "Birthday: March 15", + "address": "321 Elm Street, Austin, TX 78701" + }, + { + "id": "5", + "firstName": "James", + "lastName": "O'Brien", + "email": "james.obrien@example.com", + "phone": "+1 (555) 567-8901", + "company": "Northwind Traders", + "group": "Work", + "favorite": false, + "initials": "JO", + "avatarColor": "#3498DB", + "notes": "", + "address": "654 Pine Road, Boston, MA 02108" + }, + { + "id": "6", + "firstName": "Amara", + "lastName": "Okafor", + "email": "amara.okafor@example.com", + "phone": "+1 (555) 678-9012", + "company": "", + "group": "Family", + "favorite": true, + "initials": "AO", + "avatarColor": "#E74C3C", + "notes": "Visiting in December for the holidays", + "address": "987 Maple Lane, Atlanta, GA 30301" + }, + { + "id": "7", + "firstName": "Luis", + "lastName": "Ramirez", + "email": "luis.ramirez@example.com", + "phone": "+1 (555) 789-0123", + "company": "Ramirez Photography", + "group": "Friends", + "favorite": false, + "initials": "LR", + "avatarColor": "#1ABC9C", + "notes": "", + "address": "147 Sunset Blvd, Miami, FL 33101" + }, + { + "id": "8", + "firstName": "Emma", + "lastName": "Lindström", + "email": "emma.lindstrom@example.com", + "phone": "+1 (555) 890-1234", + "company": "Adventure Works", + "group": "Work", + "favorite": false, + "initials": "EL", + "avatarColor": "#F39C12", + "notes": "Handles procurement and vendor relations", + "address": "258 Broadway, New York, NY 10007" + }, + { + "id": "9", + "firstName": "David", + "lastName": "Kim", + "email": "david.kim@example.com", + "phone": "+1 (555) 901-2345", + "company": "", + "group": "Family", + "favorite": false, + "initials": "DK", + "avatarColor": "#8E44AD", + "notes": "", + "address": "369 Cherry Blossom Way, Chicago, IL 60601" + }, + { + "id": "10", + "firstName": "Fatima", + "lastName": "Al-Hassan", + "email": "fatima.alhassan@example.com", + "phone": "+1 (555) 012-3456", + "company": "Contoso Ltd", + "group": "Work", + "favorite": true, + "initials": "FA", + "avatarColor": "#16A085", + "notes": "Leading the new design system initiative", + "address": "480 University Ave, Palo Alto, CA 94301" + }, + { + "id": "11", + "firstName": "Carlos", + "lastName": "Mendoza", + "email": "carlos.mendoza@example.com", + "phone": "+1 (555) 111-2233", + "company": "", + "group": "Friends", + "favorite": false, + "initials": "CM", + "avatarColor": "#D35400", + "notes": "Runs the weekend hiking group", + "address": "512 Mountain View Rd, Denver, CO 80201" + }, + { + "id": "12", + "firstName": "Aisha", + "lastName": "Patel", + "email": "aisha.patel@example.com", + "phone": "+1 (555) 222-3344", + "company": "Patel & Associates", + "group": "Other", + "favorite": false, + "initials": "AP", + "avatarColor": "#2980B9", + "notes": "Financial advisor — annual review in January", + "address": "630 Commerce St, Dallas, TX 75201" + }, + { + "id": "13", + "firstName": "Ryan", + "lastName": "Mitchell", + "email": "ryan.mitchell@example.com", + "phone": "+1 (555) 333-4455", + "company": "", + "group": "Family", + "favorite": false, + "initials": "RM", + "avatarColor": "#27AE60", + "notes": "", + "address": "741 Lakeshore Dr, Minneapolis, MN 55401" + }, + { + "id": "14", + "firstName": "Sofia", + "lastName": "Andersson", + "email": "sofia.andersson@example.com", + "phone": "+1 (555) 444-5566", + "company": "", + "group": "Friends", + "favorite": true, + "initials": "SA", + "avatarColor": "#C0392B", + "notes": "Planning a trip to Stockholm together next summer", + "address": "852 Birch Street, Nashville, TN 37201" + }, + { + "id": "15", + "firstName": "Kenji", + "lastName": "Watanabe", + "email": "kenji.watanabe@example.com", + "phone": "+1 (555) 555-6677", + "company": "Watanabe Martial Arts", + "group": "Other", + "favorite": false, + "initials": "KW", + "avatarColor": "#7F8C8D", + "notes": "Teaches Saturday morning classes", + "address": "963 Dojo Lane, San Diego, CA 92101" + } + ], + "filteredContacts": [ + { + "id": "1", + "firstName": "Sarah", + "lastName": "Chen", + "email": "sarah.chen@example.com", + "phone": "+1 (555) 123-4567", + "company": "Contoso Ltd", + "group": "Work", + "favorite": true, + "initials": "SC", + "avatarColor": "#4A90D9", + "notes": "Met at the tech conference in Seattle", + "address": "123 Innovation Dr, Seattle, WA 98101" + }, + { + "id": "2", + "firstName": "Marcus", + "lastName": "Johnson", + "email": "marcus.johnson@example.com", + "phone": "+1 (555) 234-5678", + "company": "", + "group": "Family", + "favorite": false, + "initials": "MJ", + "avatarColor": "#E67E22", + "notes": "", + "address": "456 Oak Avenue, Portland, OR 97201" + }, + { + "id": "3", + "firstName": "Yuki", + "lastName": "Tanaka", + "email": "yuki.tanaka@example.com", + "phone": "+1 (555) 345-6789", + "company": "Fabrikam Inc", + "group": "Work", + "favorite": true, + "initials": "YT", + "avatarColor": "#2ECC71", + "notes": "Collaborating on the API redesign project", + "address": "789 Market St, San Francisco, CA 94103" + }, + { + "id": "4", + "firstName": "Priya", + "lastName": "Sharma", + "email": "priya.sharma@example.com", + "phone": "+1 (555) 456-7890", + "company": "", + "group": "Friends", + "favorite": false, + "initials": "PS", + "avatarColor": "#9B59B6", + "notes": "Birthday: March 15", + "address": "321 Elm Street, Austin, TX 78701" + }, + { + "id": "5", + "firstName": "James", + "lastName": "O'Brien", + "email": "james.obrien@example.com", + "phone": "+1 (555) 567-8901", + "company": "Northwind Traders", + "group": "Work", + "favorite": false, + "initials": "JO", + "avatarColor": "#3498DB", + "notes": "", + "address": "654 Pine Road, Boston, MA 02108" + }, + { + "id": "6", + "firstName": "Amara", + "lastName": "Okafor", + "email": "amara.okafor@example.com", + "phone": "+1 (555) 678-9012", + "company": "", + "group": "Family", + "favorite": true, + "initials": "AO", + "avatarColor": "#E74C3C", + "notes": "Visiting in December for the holidays", + "address": "987 Maple Lane, Atlanta, GA 30301" + }, + { + "id": "7", + "firstName": "Luis", + "lastName": "Ramirez", + "email": "luis.ramirez@example.com", + "phone": "+1 (555) 789-0123", + "company": "Ramirez Photography", + "group": "Friends", + "favorite": false, + "initials": "LR", + "avatarColor": "#1ABC9C", + "notes": "", + "address": "147 Sunset Blvd, Miami, FL 33101" + }, + { + "id": "8", + "firstName": "Emma", + "lastName": "Lindström", + "email": "emma.lindstrom@example.com", + "phone": "+1 (555) 890-1234", + "company": "Adventure Works", + "group": "Work", + "favorite": false, + "initials": "EL", + "avatarColor": "#F39C12", + "notes": "Handles procurement and vendor relations", + "address": "258 Broadway, New York, NY 10007" + }, + { + "id": "9", + "firstName": "David", + "lastName": "Kim", + "email": "david.kim@example.com", + "phone": "+1 (555) 901-2345", + "company": "", + "group": "Family", + "favorite": false, + "initials": "DK", + "avatarColor": "#8E44AD", + "notes": "", + "address": "369 Cherry Blossom Way, Chicago, IL 60601" + }, + { + "id": "10", + "firstName": "Fatima", + "lastName": "Al-Hassan", + "email": "fatima.alhassan@example.com", + "phone": "+1 (555) 012-3456", + "company": "Contoso Ltd", + "group": "Work", + "favorite": true, + "initials": "FA", + "avatarColor": "#16A085", + "notes": "Leading the new design system initiative", + "address": "480 University Ave, Palo Alto, CA 94301" + }, + { + "id": "11", + "firstName": "Carlos", + "lastName": "Mendoza", + "email": "carlos.mendoza@example.com", + "phone": "+1 (555) 111-2233", + "company": "", + "group": "Friends", + "favorite": false, + "initials": "CM", + "avatarColor": "#D35400", + "notes": "Runs the weekend hiking group", + "address": "512 Mountain View Rd, Denver, CO 80201" + }, + { + "id": "12", + "firstName": "Aisha", + "lastName": "Patel", + "email": "aisha.patel@example.com", + "phone": "+1 (555) 222-3344", + "company": "Patel & Associates", + "group": "Other", + "favorite": false, + "initials": "AP", + "avatarColor": "#2980B9", + "notes": "Financial advisor — annual review in January", + "address": "630 Commerce St, Dallas, TX 75201" + }, + { + "id": "13", + "firstName": "Ryan", + "lastName": "Mitchell", + "email": "ryan.mitchell@example.com", + "phone": "+1 (555) 333-4455", + "company": "", + "group": "Family", + "favorite": false, + "initials": "RM", + "avatarColor": "#27AE60", + "notes": "", + "address": "741 Lakeshore Dr, Minneapolis, MN 55401" + }, + { + "id": "14", + "firstName": "Sofia", + "lastName": "Andersson", + "email": "sofia.andersson@example.com", + "phone": "+1 (555) 444-5566", + "company": "", + "group": "Friends", + "favorite": true, + "initials": "SA", + "avatarColor": "#C0392B", + "notes": "Planning a trip to Stockholm together next summer", + "address": "852 Birch Street, Nashville, TN 37201" + }, + { + "id": "15", + "firstName": "Kenji", + "lastName": "Watanabe", + "email": "kenji.watanabe@example.com", + "phone": "+1 (555) 555-6677", + "company": "Watanabe Martial Arts", + "group": "Other", + "favorite": false, + "initials": "KW", + "avatarColor": "#7F8C8D", + "notes": "Teaches Saturday morning classes", + "address": "963 Dojo Lane, San Diego, CA 92101" + } + ], + "recentContacts": [ + { + "id": "11", + "firstName": "Carlos", + "lastName": "Mendoza", + "email": "carlos.mendoza@example.com", + "phone": "+1 (555) 111-2233", + "company": "", + "group": "Friends", + "favorite": false, + "initials": "CM", + "avatarColor": "#D35400", + "notes": "Runs the weekend hiking group", + "address": "512 Mountain View Rd, Denver, CO 80201" + }, + { + "id": "12", + "firstName": "Aisha", + "lastName": "Patel", + "email": "aisha.patel@example.com", + "phone": "+1 (555) 222-3344", + "company": "Patel & Associates", + "group": "Other", + "favorite": false, + "initials": "AP", + "avatarColor": "#2980B9", + "notes": "Financial advisor — annual review in January", + "address": "630 Commerce St, Dallas, TX 75201" + }, + { + "id": "13", + "firstName": "Ryan", + "lastName": "Mitchell", + "email": "ryan.mitchell@example.com", + "phone": "+1 (555) 333-4455", + "company": "", + "group": "Family", + "favorite": false, + "initials": "RM", + "avatarColor": "#27AE60", + "notes": "", + "address": "741 Lakeshore Dr, Minneapolis, MN 55401" + }, + { + "id": "14", + "firstName": "Sofia", + "lastName": "Andersson", + "email": "sofia.andersson@example.com", + "phone": "+1 (555) 444-5566", + "company": "", + "group": "Friends", + "favorite": true, + "initials": "SA", + "avatarColor": "#C0392B", + "notes": "Planning a trip to Stockholm together next summer", + "address": "852 Birch Street, Nashville, TN 37201" + }, + { + "id": "15", + "firstName": "Kenji", + "lastName": "Watanabe", + "email": "kenji.watanabe@example.com", + "phone": "+1 (555) 555-6677", + "company": "Watanabe Martial Arts", + "group": "Other", + "favorite": false, + "initials": "KW", + "avatarColor": "#7F8C8D", + "notes": "Teaches Saturday morning classes", + "address": "963 Dojo Lane, San Diego, CA 92101" + } + ], + "favoriteContacts": [ + { + "id": "1", + "firstName": "Sarah", + "lastName": "Chen", + "email": "sarah.chen@example.com", + "phone": "+1 (555) 123-4567", + "company": "Contoso Ltd", + "group": "Work", + "favorite": true, + "initials": "SC", + "avatarColor": "#4A90D9", + "notes": "Met at the tech conference in Seattle", + "address": "123 Innovation Dr, Seattle, WA 98101" + }, + { + "id": "3", + "firstName": "Yuki", + "lastName": "Tanaka", + "email": "yuki.tanaka@example.com", + "phone": "+1 (555) 345-6789", + "company": "Fabrikam Inc", + "group": "Work", + "favorite": true, + "initials": "YT", + "avatarColor": "#2ECC71", + "notes": "Collaborating on the API redesign project", + "address": "789 Market St, San Francisco, CA 94103" + }, + { + "id": "6", + "firstName": "Amara", + "lastName": "Okafor", + "email": "amara.okafor@example.com", + "phone": "+1 (555) 678-9012", + "company": "", + "group": "Family", + "favorite": true, + "initials": "AO", + "avatarColor": "#E74C3C", + "notes": "Visiting in December for the holidays", + "address": "987 Maple Lane, Atlanta, GA 30301" + }, + { + "id": "10", + "firstName": "Fatima", + "lastName": "Al-Hassan", + "email": "fatima.alhassan@example.com", + "phone": "+1 (555) 012-3456", + "company": "Contoso Ltd", + "group": "Work", + "favorite": true, + "initials": "FA", + "avatarColor": "#16A085", + "notes": "Leading the new design system initiative", + "address": "480 University Ave, Palo Alto, CA 94301" + }, + { + "id": "14", + "firstName": "Sofia", + "lastName": "Andersson", + "email": "sofia.andersson@example.com", + "phone": "+1 (555) 444-5566", + "company": "", + "group": "Friends", + "favorite": true, + "initials": "SA", + "avatarColor": "#C0392B", + "notes": "Planning a trip to Stockholm together next summer", + "address": "852 Birch Street, Nashville, TN 37201" + } + ], + "selectedContact": null +} diff --git a/examples/app/contact-book-manager/package.json b/examples/app/contact-book-manager/package.json new file mode 100644 index 00000000..6ad2bbe5 --- /dev/null +++ b/examples/app/contact-book-manager/package.json @@ -0,0 +1,21 @@ +{ + "name": "contact-book-manager", + "version": "1.0.0", + "type": "module", + "private": true, + "scripts": { + "build:protocol": "cargo run -p webui-cli -- build ./src --out ./dist --css external --plugin=fast", + "build:client": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --sourcemap", + "build": "npm run build:protocol && npm run build:client", + "start:client": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --sourcemap --watch", + "start:server": "cargo run -p webui-cli -- start ./src --state ./data/state.json --plugin=fast --servedir ./dist --port 3001", + "start": "cargo xtask dev contact-book-manager" + }, + "devDependencies": { + "@microsoft/fast-element": "catalog:", + "@microsoft/fast-html": "catalog:", + "esbuild": "catalog:", + "tslib": "catalog:", + "typescript": "catalog:" + } +} diff --git a/examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.css b/examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.css new file mode 100644 index 00000000..23136a6b --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.css @@ -0,0 +1,33 @@ +:host { + display: inline-flex; +} + +.avatar { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + color: #fff; + font-weight: 600; + font-size: 16px; + user-select: none; +} + +.initials { + line-height: 1; + text-transform: uppercase; +} + +:host([size="sm"]) .avatar { + width: 32px; + height: 32px; + font-size: 13px; +} + +:host([size="lg"]) .avatar { + width: 64px; + height: 64px; + font-size: 24px; +} diff --git a/examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.html b/examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.html new file mode 100644 index 00000000..14946997 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.html @@ -0,0 +1,5 @@ + diff --git a/examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.ts b/examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.ts new file mode 100644 index 00000000..b105cc04 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-avatar/cb-avatar.ts @@ -0,0 +1,13 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbAvatar extends RenderableFASTElement(FASTElement) { + @attr initials = ''; + @attr color = '#6B7280'; + @attr size = 'md'; +} + +CbAvatar.defineAsync({ + name: 'cb-avatar', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.css b/examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.css new file mode 100644 index 00000000..cf9da1b6 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.css @@ -0,0 +1,34 @@ +:host { + display: inline-flex; +} + +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + line-height: 1.5; + background-color: #e5e7eb; + color: #374151; +} + +:host([variant="work"]) .badge { + background-color: #dbeafe; + color: #1d4ed8; +} + +:host([variant="family"]) .badge { + background-color: #dcfce7; + color: #15803d; +} + +:host([variant="friends"]) .badge { + background-color: #f3e8ff; + color: #7e22ce; +} + +:host([variant="other"]) .badge { + background-color: #e5e7eb; + color: #374151; +} diff --git a/examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.html b/examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.html new file mode 100644 index 00000000..f21dfad3 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.html @@ -0,0 +1,3 @@ + diff --git a/examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.ts b/examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.ts new file mode 100644 index 00000000..7e7e10ce --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-badge/cb-badge.ts @@ -0,0 +1,12 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbBadge extends RenderableFASTElement(FASTElement) { + @attr label = ''; + @attr variant = 'default'; +} + +CbBadge.defineAsync({ + name: 'cb-badge', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/atoms/cb-button/cb-button.css b/examples/app/contact-book-manager/src/atoms/cb-button/cb-button.css new file mode 100644 index 00000000..49c2b4ed --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-button/cb-button.css @@ -0,0 +1,73 @@ +:host { + display: inline-flex; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + border: 1px solid transparent; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s; + line-height: 1.4; + background-color: #0078d4; + color: #fff; +} + +.btn:hover { + background-color: #106ebe; +} + +.btn:active { + background-color: #005a9e; +} + +.btn:focus-visible { + outline: 2px solid #0078d4; + outline-offset: 2px; +} + +:host([variant="secondary"]) .btn { + background-color: #fff; + color: #374151; + border-color: #d1d5db; +} + +:host([variant="secondary"]) .btn:hover { + background-color: #f9fafb; + border-color: #9ca3af; +} + +:host([variant="danger"]) .btn { + background-color: #dc2626; + color: #fff; +} + +:host([variant="danger"]) .btn:hover { + background-color: #b91c1c; +} + +:host([variant="ghost"]) .btn { + background-color: transparent; + color: #374151; +} + +:host([variant="ghost"]) .btn:hover { + background-color: #f3f4f6; +} + +:host([size="sm"]) .btn { + padding: 4px 10px; + font-size: 12px; +} + +:host([size="lg"]) .btn { + padding: 12px 24px; + font-size: 16px; +} diff --git a/examples/app/contact-book-manager/src/atoms/cb-button/cb-button.html b/examples/app/contact-book-manager/src/atoms/cb-button/cb-button.html new file mode 100644 index 00000000..85b93bc1 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-button/cb-button.html @@ -0,0 +1,5 @@ + diff --git a/examples/app/contact-book-manager/src/atoms/cb-button/cb-button.ts b/examples/app/contact-book-manager/src/atoms/cb-button/cb-button.ts new file mode 100644 index 00000000..b50039ad --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-button/cb-button.ts @@ -0,0 +1,13 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbButton extends RenderableFASTElement(FASTElement) { + @attr label = ''; + @attr variant = 'primary'; + @attr size = 'md'; +} + +CbButton.defineAsync({ + name: 'cb-button', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.css b/examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.css new file mode 100644 index 00000000..e0fa141d --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.css @@ -0,0 +1,32 @@ +:host { + display: block; +} + +.empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 48px; + text-align: center; +} + +.empty-icon { + font-size: 48px; + margin-bottom: 16px; + line-height: 1; +} + +.empty-title { + font-size: 18px; + font-weight: 600; + color: #374151; + margin: 0 0 8px; +} + +.empty-message { + font-size: 14px; + color: #6b7280; + margin: 0; +} + +.empty-icon::before { content: '\1F4ED'; } diff --git a/examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.html b/examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.html new file mode 100644 index 00000000..ed685003 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.html @@ -0,0 +1,7 @@ + diff --git a/examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.ts b/examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.ts new file mode 100644 index 00000000..ed095ce2 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-empty-state/cb-empty-state.ts @@ -0,0 +1,13 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbEmptyState extends RenderableFASTElement(FASTElement) { + @attr icon = '📭'; + @attr title = ''; + @attr message = ''; +} + +CbEmptyState.defineAsync({ + name: 'cb-empty-state', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.css b/examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.css new file mode 100644 index 00000000..6fc9acd4 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.css @@ -0,0 +1,33 @@ +:host { + display: inline-flex; +} + +.icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border: none; + border-radius: 8px; + background-color: transparent; + cursor: pointer; + font-size: 18px; + padding: 0; + color: #374151; + transition: background-color 0.15s; +} + +.icon-btn:hover { + background-color: #f3f4f6; +} + +.icon-btn:focus-visible { + outline: 2px solid #0078d4; + outline-offset: 2px; +} + +:host([variant="danger"]) .icon-btn:hover { + background-color: #fee2e2; + color: #dc2626; +} diff --git a/examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.html b/examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.html new file mode 100644 index 00000000..a2e58cce --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.html @@ -0,0 +1,5 @@ + diff --git a/examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.ts b/examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.ts new file mode 100644 index 00000000..03c7771c --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.ts @@ -0,0 +1,13 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbIconButton extends RenderableFASTElement(FASTElement) { + @attr icon = ''; + @attr title = ''; + @attr variant = 'default'; +} + +CbIconButton.defineAsync({ + name: 'cb-icon-button', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/atoms/cb-input/cb-input.css b/examples/app/contact-book-manager/src/atoms/cb-input/cb-input.css new file mode 100644 index 00000000..7f5594ba --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-input/cb-input.css @@ -0,0 +1,27 @@ +:host { + display: block; +} + +.input { + width: 100%; + padding: 10px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + font-family: inherit; + line-height: 1.4; + color: #111827; + background-color: #fff; + box-sizing: border-box; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.input::placeholder { + color: #9ca3af; +} + +.input:focus { + outline: none; + border-color: #0078d4; + box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.15); +} diff --git a/examples/app/contact-book-manager/src/atoms/cb-input/cb-input.html b/examples/app/contact-book-manager/src/atoms/cb-input/cb-input.html new file mode 100644 index 00000000..48f80ed1 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-input/cb-input.html @@ -0,0 +1,3 @@ + diff --git a/examples/app/contact-book-manager/src/atoms/cb-input/cb-input.ts b/examples/app/contact-book-manager/src/atoms/cb-input/cb-input.ts new file mode 100644 index 00000000..16614d00 --- /dev/null +++ b/examples/app/contact-book-manager/src/atoms/cb-input/cb-input.ts @@ -0,0 +1,14 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbInput extends RenderableFASTElement(FASTElement) { + @attr placeholder = ''; + @attr value = ''; + @attr type = 'text'; + @attr name = ''; +} + +CbInput.defineAsync({ + name: 'cb-input', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/cb-app/cb-app.css b/examples/app/contact-book-manager/src/cb-app/cb-app.css new file mode 100644 index 00000000..5a012b36 --- /dev/null +++ b/examples/app/contact-book-manager/src/cb-app/cb-app.css @@ -0,0 +1,93 @@ +:host { + display: block; + min-height: 100vh; + background: #f0f2f5; +} + +.layout { + display: flex; + min-height: calc(100vh - 65px); +} + +.content { + flex: 1; + padding: 24px 32px; + overflow-y: auto; +} + +.page-title { + font-size: 24px; + font-weight: 700; + color: #111827; + margin: 0 0 20px; +} + +.section-title { + font-size: 16px; + font-weight: 600; + color: #374151; + margin: 28px 0 14px; +} + +.stats-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; +} + +.contact-list-container { + background: #fff; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + overflow: hidden; +} + +/* Inlined stat-card styles */ +.stat-card { + display: flex; + align-items: center; + gap: 14px; + background: white; + border-radius: 12px; + padding: 20px 24px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.stat-icon { + font-size: 28px; +} + +.stat-content { + display: flex; + flex-direction: column; +} + +.stat-value { + font-size: 28px; + font-weight: 700; + color: #111827; +} + +.stat-label { + font-size: 13px; + color: #6b7280; + margin-top: 2px; +} + +/* Responsive */ +@media (max-width: 768px) { + .layout { + flex-direction: column; + } + .content { + padding: 16px; + } + .stats-row { + grid-template-columns: 1fr; + } +} + +/* Icon classes using CSS content to avoid emoji encoding issues */ +.stat-icon-contacts::before { content: '\1F465'; } +.stat-icon-favorites::before { content: '\2B50'; } +.stat-icon-groups::before { content: '\1F4C1'; } diff --git a/examples/app/contact-book-manager/src/cb-app/cb-app.html b/examples/app/contact-book-manager/src/cb-app/cb-app.html new file mode 100644 index 00000000..62cac25c --- /dev/null +++ b/examples/app/contact-book-manager/src/cb-app/cb-app.html @@ -0,0 +1,193 @@ + diff --git a/examples/app/contact-book-manager/src/cb-app/cb-app.ts b/examples/app/contact-book-manager/src/cb-app/cb-app.ts new file mode 100644 index 00000000..661c71bb --- /dev/null +++ b/examples/app/contact-book-manager/src/cb-app/cb-app.ts @@ -0,0 +1,350 @@ +import { FASTElement, attr, observable } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +interface Contact { + id: string; + firstName: string; + lastName: string; + email: string; + phone: string; + company: string; + group: string; + favorite: boolean; + initials: string; + avatarColor: string; + notes: string; + address: string; +} + +const AVATAR_COLORS = [ + '#4A90D9', '#E74C3C', '#2ECC71', '#F39C12', '#9B59B6', + '#1ABC9C', '#E67E22', '#3498DB', '#E91E63', '#00BCD4', +]; + +const groupsStore = new WeakMap(); + +export class CbApp extends RenderableFASTElement(FASTElement) { + @attr page = 'dashboard'; + @attr({ attribute: 'search-query' }) searchQuery = ''; + @attr({ attribute: 'active-group' }) activeGroup = 'all'; + @attr({ attribute: 'total-contacts' }) totalContacts = '0'; + @attr({ attribute: 'total-favorites' }) totalFavorites = '0'; + @attr({ attribute: 'total-groups' }) totalGroups = '0'; + + @observable contacts!: Contact[]; + @observable filteredContacts!: Contact[]; + @observable favoriteContacts!: Contact[]; + @observable recentContacts!: Contact[]; + @observable selectedContact: Contact | null = null; + + private nextId = 100; + private db: IDBDatabase | null = null; + private listenersAttached!: boolean; + + connectedCallback(): void { + super.connectedCallback(); + if (this.listenersAttached) return; + this.listenersAttached = true; + const root = this.shadowRoot; + if (!root) return; + root.addEventListener('navigate', (e: Event) => { + e.stopPropagation(); + this.onNavigate(e as CustomEvent); + }); + root.addEventListener('search', (e: Event) => { + e.stopPropagation(); + this.onSearch(e as CustomEvent); + }); + root.addEventListener('add-contact', (e: Event) => { + e.stopPropagation(); + this.onAddContact(); + }); + root.addEventListener('select-contact', (e: Event) => { + e.stopPropagation(); + this.onSelectContact(e as CustomEvent); + }); + root.addEventListener('edit-contact', (e: Event) => { + e.stopPropagation(); + this.onEditContact(e as CustomEvent); + }); + root.addEventListener('delete-contact', (e: Event) => { + e.stopPropagation(); + this.onDeleteContact(e as CustomEvent); + }); + root.addEventListener('toggle-favorite', (e: Event) => { + e.stopPropagation(); + this.onToggleFavorite(e as CustomEvent); + }); + root.addEventListener('back', (e: Event) => { + e.stopPropagation(); + this.onBack(); + }); + root.addEventListener('form-save', (e: Event) => { + e.stopPropagation(); + this.onFormSave(e as CustomEvent); + }); + root.addEventListener('form-cancel', (e: Event) => { + e.stopPropagation(); + this.onFormCancel(); + }); + } + + async prepare(): Promise { + if (!this.shadowRoot) return; + + // Read ALL contacts from the hidden data store + const dataEls = this.shadowRoot.querySelectorAll('.contact-data'); + const contacts: Contact[] = []; + const seen = new Set(); + for (const el of dataEls) { + const id = (el as HTMLElement).dataset.id || ''; + if (!id || seen.has(id)) continue; + seen.add(id); + const ds = (el as HTMLElement).dataset; + contacts.push({ + id, + firstName: ds.fn || '', + lastName: ds.ln || '', + email: ds.email || '', + phone: ds.phone || '', + company: ds.company || '', + group: ds.group || '', + favorite: ds.favorite === 'true', + initials: ds.initials || '', + avatarColor: ds.color || '#6B7280', + notes: ds.notes || '', + address: ds.address || '', + }); + } + this.contacts = contacts; + + // Read groups from sidebar nav items + const sidebar = this.shadowRoot.querySelector('cb-sidebar'); + const navItems = sidebar?.shadowRoot?.querySelectorAll('.nav-item') || []; + const groups: string[] = []; + for (const el of navItems) { + const label = (el as HTMLElement).getAttribute('data-nav') || ''; + if (!['Dashboard', 'All Contacts', 'Favorites'].includes(label) && label) { + groups.push(label); + } + } + groupsStore.set(this, groups); + + if (this.contacts.length > 0) { + this.nextId = Math.max(...this.contacts.map(c => Number(c.id) || 0)) + 1; + } + + this.updateDerivedState(); + await this.initDB(); + } + + // --- IndexedDB --- + private async initDB(): Promise { + return new Promise((resolve) => { + const request = indexedDB.open('ContactBookDB', 1); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains('contacts')) { + db.createObjectStore('contacts', { keyPath: 'id' }); + } + }; + request.onsuccess = () => { + this.db = request.result; + this.loadFromDB().then(resolve); + }; + request.onerror = () => resolve(); + }); + } + + private async loadFromDB(): Promise { + if (!this.db) return; + return new Promise((resolve) => { + const tx = this.db!.transaction('contacts', 'readonly'); + const store = tx.objectStore('contacts'); + const req = store.getAll(); + req.onsuccess = () => { + const stored = req.result as Contact[]; + if (stored.length >= this.contacts.length && stored.length > 0) { + this.contacts = stored; + this.updateDerivedState(); + } else { + this.saveToDB(); + } + resolve(); + }; + req.onerror = () => resolve(); + }); + } + + private saveToDB(): void { + if (!this.db) return; + const tx = this.db.transaction('contacts', 'readwrite'); + const store = tx.objectStore('contacts'); + store.clear(); + for (const c of this.contacts) { + store.put(c); + } + } + + // --- Derived state --- + private updateDerivedState(): void { + this.totalContacts = String(this.contacts.length); + this.favoriteContacts = this.contacts.filter(c => c.favorite); + this.totalFavorites = String(this.favoriteContacts.length); + this.recentContacts = this.contacts.slice(-5).reverse(); + + const uniqueGroups = [...new Set(this.contacts.map(c => c.group).filter(Boolean))]; + const groups = groupsStore.get(this) || []; + if (groups.length === 0) { + groupsStore.set(this, uniqueGroups); + } + this.totalGroups = String((groupsStore.get(this) || uniqueGroups).length); + + this.applyFilter(); + } + + private applyFilter(): void { + let filtered = this.contacts; + if (this.page === 'favorites') { + filtered = this.favoriteContacts; + } else if (this.page === 'group' && this.activeGroup !== 'all') { + filtered = this.contacts.filter(c => c.group === this.activeGroup); + } else if (this.page === 'dashboard') { + filtered = this.recentContacts; + } + + if (this.searchQuery) { + const q = this.searchQuery.toLowerCase(); + filtered = filtered.filter(c => + c.firstName.toLowerCase().includes(q) || + c.lastName.toLowerCase().includes(q) || + c.email.toLowerCase().includes(q) || + c.company.toLowerCase().includes(q) + ); + } + this.filteredContacts = filtered; + + // Push filtered contacts to the visible cb-contact-list + const contactList = this.shadowRoot!.querySelector( + `.content > [class$="-page"]:not([hidden]) cb-contact-list, .dashboard cb-contact-list` + ) as any; + if (contactList && contactList.contacts !== undefined) { + contactList.contacts = this.filteredContacts; + } + } + + // --- Event handlers --- + onNavigate(e: CustomEvent<{ page: string; group?: string }>): void { + this.page = e.detail.page; + if (e.detail.group) this.activeGroup = e.detail.group; + this.selectedContact = null; + this.updateDerivedState(); + } + + onSearch(e: CustomEvent<{ value: string }>): void { + this.searchQuery = e.detail.value; + this.applyFilter(); + } + + onAddContact(): void { + this.page = 'add'; + this.selectedContact = null; + } + + onSelectContact(e: CustomEvent<{ id: string }>): void { + this.selectedContact = this.contacts.find(c => c.id === e.detail.id) || null; + if (this.selectedContact) this.page = 'detail'; + } + + onEditContact(e: CustomEvent<{ id: string }>): void { + this.selectedContact = this.contacts.find(c => c.id === e.detail.id) || null; + if (this.selectedContact) this.page = 'edit'; + } + + onDeleteContact(e: CustomEvent<{ id: string }>): void { + this.contacts = this.contacts.filter(c => c.id !== e.detail.id); + this.selectedContact = null; + this.page = 'contacts'; + this.updateDerivedState(); + this.saveToDB(); + } + + onToggleFavorite(e: CustomEvent<{ id: string }>): void { + const contact = this.contacts.find(c => c.id === e.detail.id); + if (contact) { + contact.favorite = !contact.favorite; + this.contacts = [...this.contacts]; + if (this.selectedContact?.id === e.detail.id) { + this.selectedContact = { ...contact }; + } + this.updateDerivedState(); + this.saveToDB(); + } + } + + onBack(): void { + this.page = 'contacts'; + this.selectedContact = null; + this.updateDerivedState(); + } + + onFormSave(e: CustomEvent>): void { + const data = e.detail; + if (data.id) { + // Edit existing + const idx = this.contacts.findIndex(c => c.id === data.id); + if (idx >= 0) { + const existing = this.contacts[idx]; + const updated: Contact = { + ...existing, + firstName: data.firstName || existing.firstName, + lastName: data.lastName || existing.lastName, + email: data.email || existing.email, + phone: data.phone || existing.phone, + company: data.company || '', + address: data.address || '', + group: data.group || existing.group, + notes: data.notes || '', + initials: (data.firstName?.[0] || existing.firstName[0] || '').toUpperCase() + + (data.lastName?.[0] || existing.lastName[0] || '').toUpperCase(), + }; + this.contacts = [...this.contacts.slice(0, idx), updated, ...this.contacts.slice(idx + 1)]; + this.selectedContact = updated; + } + } else { + // Add new + const initials = ((data.firstName?.[0] || '') + (data.lastName?.[0] || '')).toUpperCase(); + const newContact: Contact = { + id: String(this.nextId++), + firstName: data.firstName || '', + lastName: data.lastName || '', + email: data.email || '', + phone: data.phone || '', + company: data.company || '', + address: data.address || '', + group: data.group || 'Other', + notes: data.notes || '', + favorite: false, + initials, + avatarColor: AVATAR_COLORS[this.contacts.length % AVATAR_COLORS.length], + }; + this.contacts = [...this.contacts, newContact]; + } + this.page = 'contacts'; + this.updateDerivedState(); + this.saveToDB(); + } + + onFormCancel(): void { + if (this.selectedContact) { + this.page = 'detail'; + } else { + this.page = 'contacts'; + } + } +} + +CbApp.defineAsync({ + name: 'cb-app', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/index.html b/examples/app/contact-book-manager/src/index.html new file mode 100644 index 00000000..329db2a1 --- /dev/null +++ b/examples/app/contact-book-manager/src/index.html @@ -0,0 +1,39 @@ + + + + + + + Contact Book + + + + + + + + + + diff --git a/examples/app/contact-book-manager/src/index.ts b/examples/app/contact-book-manager/src/index.ts new file mode 100644 index 00000000..d087823b --- /dev/null +++ b/examples/app/contact-book-manager/src/index.ts @@ -0,0 +1,68 @@ +/** + * Contact-book-manager entry point — bootstraps FAST-HTML hydration. + * + * The server pre-renders HTML with hydration markers via `webui build --plugin=fast`. + * This script: + * 1. Registers custom elements (17 components) via defineAsync + * 2. Configures FAST-HTML observation maps for reactive attribute tracking + * 3. Defines , triggering hydration of pre-rendered shadow DOM + */ + +performance.mark('contact-book-hydration-started'); + +import { TemplateElement } from '@microsoft/fast-html'; + +// Side-effect imports — register custom elements via defineAsync + +// Root +import './cb-app/cb-app.js'; + +// Atoms +import './atoms/cb-avatar/cb-avatar.js'; +import './atoms/cb-badge/cb-badge.js'; +import './atoms/cb-button/cb-button.js'; +import './atoms/cb-input/cb-input.js'; +import './atoms/cb-icon-button/cb-icon-button.js'; +import './atoms/cb-empty-state/cb-empty-state.js'; + +// Molecules +import './molecules/cb-search-bar/cb-search-bar.js'; +import './molecules/cb-form-field/cb-form-field.js'; +import './molecules/cb-stat-card/cb-stat-card.js'; +import './molecules/cb-nav-item/cb-nav-item.js'; + +// Organisms +import './organisms/cb-header/cb-header.js'; +import './organisms/cb-sidebar/cb-sidebar.js'; +import './organisms/cb-contact-card/cb-contact-card.js'; +import './organisms/cb-contact-list/cb-contact-list.js'; +import './organisms/cb-contact-detail/cb-contact-detail.js'; +import './organisms/cb-contact-form/cb-contact-form.js'; + +// Configure and start hydration +TemplateElement.options({ + 'cb-app': { observerMap: 'all' }, + 'cb-avatar': { observerMap: 'all' }, + 'cb-badge': { observerMap: 'all' }, + 'cb-button': { observerMap: 'all' }, + 'cb-input': { observerMap: 'all' }, + 'cb-icon-button': { observerMap: 'all' }, + 'cb-empty-state': { observerMap: 'all' }, + 'cb-search-bar': { observerMap: 'all' }, + 'cb-form-field': { observerMap: 'all' }, + 'cb-stat-card': { observerMap: 'all' }, + 'cb-nav-item': { observerMap: 'all' }, + 'cb-header': { observerMap: 'all' }, + 'cb-sidebar': {}, + 'cb-contact-card': { observerMap: 'all' }, + 'cb-contact-list': { observerMap: 'all' }, + 'cb-contact-detail': { observerMap: 'all' }, + 'cb-contact-form': { observerMap: 'all' }, +}).config({ + hydrationComplete() { + performance.measure('contact-book-hydration-completed', 'contact-book-hydration-started'); + console.log('Hydration complete!'); + }, +}).define({ + name: 'f-template', +}); diff --git a/examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.css b/examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.css new file mode 100644 index 00000000..5ccd93d5 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.css @@ -0,0 +1,37 @@ +:host { + display: block; +} + +.field { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 16px; +} + +.label { + font-size: 13px; + font-weight: 600; + color: #374151; +} + +.input { + width: 100%; + padding: 10px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + font-family: inherit; + box-sizing: border-box; +} + +.input:focus { + outline: none; + border-color: #0078d4; + box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.15); +} + +.error { + font-size: 12px; + color: #dc2626; +} diff --git a/examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.html b/examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.html new file mode 100644 index 00000000..96d78a15 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.html @@ -0,0 +1,9 @@ + diff --git a/examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.ts b/examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.ts new file mode 100644 index 00000000..8ab66964 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-form-field/cb-form-field.ts @@ -0,0 +1,16 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbFormField extends RenderableFASTElement(FASTElement) { + @attr label = ''; + @attr name = ''; + @attr value = ''; + @attr placeholder = ''; + @attr type = 'text'; + @attr error = ''; +} + +CbFormField.defineAsync({ + name: 'cb-form-field', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.css b/examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.css new file mode 100644 index 00000000..0bc45a90 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.css @@ -0,0 +1,49 @@ +:host { + display: block; +} + +.nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s; +} + +.nav-item:hover { + background: #f3f4f6; +} + +:host([active]) .nav-item { + background: #e0f2fe; + color: #0078d4; +} + +.nav-icon { + font-size: 18px; + width: 24px; + text-align: center; +} + +.nav-label { + flex: 1; + font-size: 14px; + font-weight: 500; +} + +.nav-count { + font-size: 12px; + background: #e5e7eb; + color: #6b7280; + border-radius: 10px; + padding: 1px 8px; + min-width: 22px; + text-align: center; +} + +:host([active]) .nav-count { + background: #bfdbfe; + color: #0078d4; +} diff --git a/examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.html b/examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.html new file mode 100644 index 00000000..2c3bdf5c --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.html @@ -0,0 +1,9 @@ + diff --git a/examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.ts b/examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.ts new file mode 100644 index 00000000..52fb0da3 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-nav-item/cb-nav-item.ts @@ -0,0 +1,26 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbNavItem extends RenderableFASTElement(FASTElement) { + @attr icon = ''; + @attr label = ''; + @attr count = ''; + @attr({ mode: 'boolean' }) active = false; + private listenersAttached!: boolean; + + connectedCallback(): void { + super.connectedCallback(); + if (this.listenersAttached) return; + this.listenersAttached = true; + this.addEventListener('click', () => this.onClick()); + } + + onClick(): void { + this.dispatchEvent(new CustomEvent('nav-select', { bubbles: true, composed: true, detail: { label: this.label } })); + } +} + +CbNavItem.defineAsync({ + name: 'cb-nav-item', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.css b/examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.css new file mode 100644 index 00000000..1ed173a6 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.css @@ -0,0 +1,54 @@ +:host { + display: block; +} + +.search-container { + display: flex; + align-items: center; + background: white; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 0 12px; + gap: 8px; +} + +.search-container:focus-within { + border-color: #0078d4; + box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.15); +} + +.search-icon { + font-size: 16px; + color: #9ca3af; +} + +.search-input { + flex: 1; + border: none; + outline: none; + padding: 10px 0; + font-size: 14px; + font-family: inherit; + background: transparent; +} + +.clear-btn { + width: 28px; + height: 28px; + border: none; + background: transparent; + cursor: pointer; + font-size: 18px; + color: #9ca3af; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.clear-btn:hover { + color: #374151; + background: #f3f4f6; +} + +.search-icon::before { content: '\1F50D'; } diff --git a/examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.html b/examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.html new file mode 100644 index 00000000..704b42ee --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.html @@ -0,0 +1,9 @@ + diff --git a/examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.ts b/examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.ts new file mode 100644 index 00000000..a02366a2 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-search-bar/cb-search-bar.ts @@ -0,0 +1,43 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbSearchBar extends RenderableFASTElement(FASTElement) { + @attr placeholder = 'Search contacts...'; + @attr value = ''; + private listenersAttached!: boolean; + + connectedCallback(): void { + super.connectedCallback(); + if (this.listenersAttached) return; + this.listenersAttached = true; + this.addEventListener('input', (e: Event) => this.onInput(e)); + this.addEventListener('click', (e: Event) => this.onClick(e as MouseEvent)); + } + + private emit(type: string, detail?: unknown): void { + this.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail })); + } + + onInput(e: Event): void { + const input = e.composedPath().find(el => (el as HTMLElement).tagName === 'INPUT') as HTMLInputElement; + if (input) { + this.value = input.value; + this.emit('search', { value: this.value }); + } + } + + onClick(e: MouseEvent): void { + const target = (e.composedPath()[0] as HTMLElement); + if (target.closest('[data-action="clear"]')) { + this.value = ''; + const input = this.shadowRoot?.querySelector('input'); + if (input) input.value = ''; + this.emit('search', { value: '' }); + } + } +} + +CbSearchBar.defineAsync({ + name: 'cb-search-bar', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.css b/examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.css new file mode 100644 index 00000000..3af42ef8 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.css @@ -0,0 +1,34 @@ +:host { + display: block; +} + +.stat-card { + display: flex; + align-items: center; + gap: 14px; + background: white; + border-radius: 12px; + padding: 20px 24px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.stat-icon { + font-size: 28px; +} + +.stat-content { + display: flex; + flex-direction: column; +} + +.stat-value { + font-size: 28px; + font-weight: 700; + color: #111827; +} + +.stat-label { + font-size: 13px; + color: #6b7280; + margin-top: 2px; +} diff --git a/examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.html b/examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.html new file mode 100644 index 00000000..39b5a762 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.html @@ -0,0 +1,9 @@ + diff --git a/examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.ts b/examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.ts new file mode 100644 index 00000000..1e8380a5 --- /dev/null +++ b/examples/app/contact-book-manager/src/molecules/cb-stat-card/cb-stat-card.ts @@ -0,0 +1,13 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbStatCard extends RenderableFASTElement(FASTElement) { + @attr icon = ''; + @attr value = ''; + @attr label = ''; +} + +CbStatCard.defineAsync({ + name: 'cb-stat-card', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.css b/examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.css new file mode 100644 index 00000000..489c8d73 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.css @@ -0,0 +1,88 @@ +:host { + display: block; +} + +.card { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 18px; + border-bottom: 1px solid #f3f4f6; + cursor: pointer; + transition: background 0.15s; +} + +.card:hover { + background: #f9fafb; +} + +.card-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.card-top { + display: flex; + align-items: center; + gap: 6px; +} + +.name { + font-size: 15px; + font-weight: 600; + color: #111827; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.fav-star { + font-size: 12px; +} + +.fav-star::before { + content: '\2B50'; +} + +.email { + font-size: 13px; + color: #6b7280; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.phone { + font-size: 13px; + color: #9ca3af; +} + +.avatar { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.avatar-initials { + color: #fff; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.5px; +} + +.badge { + font-size: 12px; + padding: 2px 10px; + border-radius: 12px; + background: #e5e7eb; + color: #374151; + white-space: nowrap; + flex-shrink: 0; +} diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.html b/examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.html new file mode 100644 index 00000000..7dbdc75f --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.html @@ -0,0 +1,18 @@ + diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.ts b/examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.ts new file mode 100644 index 00000000..3dfa553a --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-card/cb-contact-card.ts @@ -0,0 +1,37 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbContactCard extends RenderableFASTElement(FASTElement) { + @attr id = ''; + @attr({ attribute: 'first-name' }) firstName = ''; + @attr({ attribute: 'last-name' }) lastName = ''; + @attr email = ''; + @attr phone = ''; + @attr company = ''; + @attr group = ''; + @attr favorite = 'false'; + @attr initials = ''; + @attr({ attribute: 'avatar-color' }) avatarColor = ''; + @attr notes = ''; + @attr address = ''; + + private listenersAttached!: boolean; + + connectedCallback(): void { + super.connectedCallback(); + if (this.listenersAttached) return; + this.listenersAttached = true; + this.addEventListener('click', () => { + this.onClick(); + }); + } + + onClick(): void { + this.dispatchEvent(new CustomEvent('select-contact', { bubbles: true, composed: true, detail: { id: this.id } })); + } +} + +CbContactCard.defineAsync({ + name: 'cb-contact-card', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.css b/examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.css new file mode 100644 index 00000000..47369afc --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.css @@ -0,0 +1,173 @@ +:host { + display: block; +} + +.detail { + background: #fff; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + overflow: hidden; +} + +.detail-header { + display: flex; + align-items: center; + gap: 20px; + padding: 24px; + border-bottom: 1px solid #f3f4f6; +} + +.detail-name h2 { + font-size: 22px; + font-weight: 700; + margin: 0; +} + +.company { + font-size: 14px; + color: #6b7280; +} + +.detail-actions { + display: flex; + gap: 4px; + margin-left: auto; +} + +.detail-body { + padding: 24px; +} + +.detail-section { + margin-bottom: 24px; +} + +.detail-section h3 { + font-size: 14px; + font-weight: 600; + color: #6b7280; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 12px; + margin-top: 0; +} + +.detail-field { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 8px 0; +} + +.field-label { + font-size: 13px; + color: #6b7280; + min-width: 100px; + flex-shrink: 0; +} + +.field-value { + font-size: 14px; + color: #111827; +} + +.notes { + white-space: pre-wrap; +} + +.detail-footer { + padding: 16px 24px; + border-top: 1px solid #f3f4f6; +} + +.back-btn { + background: none; + border: none; + cursor: pointer; + font-size: 14px; + color: #0078d4; + padding: 8px 0; + font-family: inherit; +} + +.back-btn:hover { + text-decoration: underline; +} + +/* Inlined avatar styles */ +.avatar { + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: #fff; + font-weight: 600; + user-select: none; +} + +.avatar-lg { + width: 64px; + height: 64px; + font-size: 24px; +} + +.avatar-initials { + line-height: 1; + text-transform: uppercase; + color: #fff; + font-size: 24px; + font-weight: 700; +} + +/* Inlined badge styles */ +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + line-height: 1.5; + background-color: #e5e7eb; + color: #374151; +} + +/* Inlined icon-button styles */ +.icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border: none; + border-radius: 8px; + background-color: transparent; + cursor: pointer; + font-size: 18px; + padding: 0; + color: #374151; + transition: background-color 0.15s; +} + +.icon-btn:hover { + background-color: #f3f4f6; +} + +.icon-btn:focus-visible { + outline: 2px solid #0078d4; + outline-offset: 2px; +} + +.icon-btn-danger:hover { + background-color: #fee2e2; + color: #dc2626; +} + +/* Icon classes using CSS content to avoid emoji encoding issues */ +.icon-edit::before { content: '\270F\FE0F'; } +.icon-star::before { content: '\2B50'; } +.icon-trash::before { content: '\1F5D1\FE0F'; } +.label-email::before { content: '\1F4E7 '; } +.label-phone::before { content: '\1F4F1 '; } +.label-address::before { content: '\1F4CD '; } +.label-group::before { content: '\1F465 '; } +.label-notes::before { content: '\1F4DD '; } diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.html b/examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.html new file mode 100644 index 00000000..ff0dcce6 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.html @@ -0,0 +1,53 @@ + diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.ts b/examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.ts new file mode 100644 index 00000000..7d914428 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-detail/cb-contact-detail.ts @@ -0,0 +1,60 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbContactDetail extends RenderableFASTElement(FASTElement) { + @attr id = ''; + @attr({ attribute: 'first-name' }) firstName = ''; + @attr({ attribute: 'last-name' }) lastName = ''; + @attr email = ''; + @attr phone = ''; + @attr company = ''; + @attr group = ''; + @attr favorite = ''; + @attr initials = ''; + @attr({ attribute: 'avatar-color' }) avatarColor = ''; + @attr notes = ''; + @attr address = ''; + + private listenersAttached!: boolean; + + connectedCallback(): void { + super.connectedCallback(); + if (this.listenersAttached) return; + this.listenersAttached = true; + this.addEventListener('click', (e: Event) => { + this.onClick(e as MouseEvent); + }); + } + + private emit(type: string, detail?: unknown): void { + this.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail })); + } + + onClick(e: MouseEvent): void { + const target = e.composedPath()[0] as HTMLElement; + const actionEl = target.closest('[data-action]'); + if (!actionEl) return; + + const action = actionEl.getAttribute('data-action'); + + switch (action) { + case 'edit': + this.emit('edit-contact', { id: this.id }); + break; + case 'toggle-favorite': + this.emit('toggle-favorite', { id: this.id }); + break; + case 'delete': + this.emit('delete-contact', { id: this.id }); + break; + case 'back': + this.emit('back'); + break; + } + } +} + +CbContactDetail.defineAsync({ + name: 'cb-contact-detail', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.css b/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.css new file mode 100644 index 00000000..140cbebc --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.css @@ -0,0 +1,149 @@ +:host { + display: block; +} + +.form-container { + background: #fff; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + padding: 32px; +} + +.form-title { + font-size: 22px; + font-weight: 700; + margin: 0 0 24px; +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0 20px; +} + +@media (max-width: 640px) { + .form-grid { + grid-template-columns: 1fr; + } +} + +/* Inlined form-field styles */ +.form-field { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 16px; +} + +.field-input { + width: 100%; + padding: 10px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + font-family: inherit; + box-sizing: border-box; +} + +.field-input:focus { + outline: none; + border-color: #0078d4; + box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.15); +} + +.form-field-full { + margin-bottom: 16px; +} + +.field-label { + font-size: 13px; + font-weight: 600; + color: #374151; + display: block; + margin-bottom: 4px; +} + +.group-selector { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 4px; +} + +.group-option { + padding: 8px 16px; + border: 1px solid #d1d5db; + border-radius: 20px; + background: #fff; + cursor: pointer; + font-size: 13px; + font-family: inherit; + transition: all 0.15s; +} + +.group-option:hover { + border-color: #0078d4; + color: #0078d4; +} + +.group-option.active { + background: #0078d4; + color: #fff; + border-color: #0078d4; +} + +.notes-input { + width: 100%; + padding: 10px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + font-family: inherit; + resize: vertical; + box-sizing: border-box; +} + +.notes-input:focus { + border-color: #0078d4; + outline: none; + box-shadow: 0 0 0 2px rgba(0, 120, 212, 0.2); +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid #f3f4f6; +} + +.cancel-btn { + padding: 10px 24px; + border: 1px solid #d1d5db; + border-radius: 8px; + background: #fff; + cursor: pointer; + font-size: 14px; + font-family: inherit; +} + +.cancel-btn:hover { + background: #f9fafb; +} + +.save-btn { + padding: 10px 24px; + border: none; + border-radius: 8px; + background: #0078d4; + color: #fff; + cursor: pointer; + font-size: 14px; + font-weight: 600; + font-family: inherit; +} + +.save-btn:hover { + background: #106ebe; +} diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.html b/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.html new file mode 100644 index 00000000..67e122b7 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.html @@ -0,0 +1,47 @@ + diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.ts b/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.ts new file mode 100644 index 00000000..3c78ecc3 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.ts @@ -0,0 +1,82 @@ +import { FASTElement, attr, observable } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +const formGroupsStore = new WeakMap(); + +export class CbContactForm extends RenderableFASTElement(FASTElement) { + @attr({ attribute: 'form-title' }) formTitle = 'Add Contact'; + @attr({ attribute: 'edit-id' }) editId = ''; + @attr({ attribute: 'first-name' }) firstName = ''; + @attr({ attribute: 'last-name' }) lastName = ''; + @attr email = ''; + @attr phone = ''; + @attr company = ''; + @attr address = ''; + @attr group = ''; + @attr notes = ''; + @observable selectedGroup = ''; + + private listenersAttached!: boolean; + + connectedCallback(): void { + super.connectedCallback(); + if (this.listenersAttached) return; + this.listenersAttached = true; + this.addEventListener('click', (e: Event) => { + this.onClick(e as MouseEvent); + }); + } + + async prepare(): Promise { + const groups: string[] = []; + for (const el of this.shadowRoot!.querySelectorAll('.group-option')) { + const g = el.getAttribute('data-group') || el.textContent || ''; + if (g) groups.push(g); + } + formGroupsStore.set(this, groups); + this.selectedGroup = this.group || (groups.length > 0 ? groups[0] : ''); + } + + onClick(e: MouseEvent): void { + const target = e.composedPath()[0] as HTMLElement; + const action = target.closest('[data-action]')?.getAttribute('data-action'); + const groupBtn = target.closest('[data-group]'); + + if (groupBtn) { + this.selectedGroup = groupBtn.getAttribute('data-group') || ''; + // Update visual active state + for (const btn of this.shadowRoot!.querySelectorAll('.group-option')) { + btn.classList.toggle('active', btn.getAttribute('data-group') === this.selectedGroup); + } + return; + } + + if (action === 'cancel') { + this.dispatchEvent(new CustomEvent('form-cancel', { bubbles: true, composed: true })); + } else if (action === 'save') { + const formData = this.collectFormData(); + if (formData) { + this.dispatchEvent(new CustomEvent('form-save', { bubbles: true, composed: true, detail: formData })); + } + } + } + + private collectFormData(): Record | null { + const inputs = this.shadowRoot!.querySelectorAll('input.field-input'); + const data: Record = {}; + for (const input of inputs) { + const name = (input as HTMLInputElement).name || ''; + data[name] = (input as HTMLInputElement).value || ''; + } + data.group = this.selectedGroup; + const textarea = this.shadowRoot!.querySelector('.notes-input') as HTMLTextAreaElement; + data.notes = textarea?.value || ''; + if (this.editId) data.id = this.editId; + return data; + } +} + +CbContactForm.defineAsync({ + name: 'cb-contact-form', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.css b/examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.css new file mode 100644 index 00000000..0fa47d64 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.css @@ -0,0 +1,14 @@ +:host { + display: block; +} + +.list-container { + min-height: 200px; +} + +.list { + background: #fff; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + overflow: hidden; +} diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.html b/examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.html new file mode 100644 index 00000000..d457bc25 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.html @@ -0,0 +1,31 @@ + diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.ts b/examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.ts new file mode 100644 index 00000000..8da6526b --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-list/cb-contact-list.ts @@ -0,0 +1,53 @@ +import { FASTElement, observable } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +interface Contact { + id: string; + firstName: string; + lastName: string; + email: string; + phone: string; + company: string; + group: string; + favorite: boolean; + initials: string; + avatarColor: string; + notes: string; + address: string; +} + +export class CbContactList extends RenderableFASTElement(FASTElement) { + @observable contacts!: Contact[]; + @observable hasContacts = true; + + async prepare(): Promise { + const contacts: Contact[] = []; + for (const el of this.shadowRoot!.querySelectorAll('cb-contact-card')) { + contacts.push({ + id: el.getAttribute('id') || '', + firstName: el.getAttribute('first-name') || '', + lastName: el.getAttribute('last-name') || '', + email: el.getAttribute('email') || '', + phone: el.getAttribute('phone') || '', + company: el.getAttribute('company') || '', + group: el.getAttribute('group') || '', + favorite: el.getAttribute('favorite') === 'true', + initials: el.getAttribute('initials') || '', + avatarColor: el.getAttribute('avatar-color') || '#6B7280', + notes: el.getAttribute('notes') || '', + address: el.getAttribute('address') || '', + }); + } + this.contacts = contacts; + this.hasContacts = contacts.length > 0; + } + + contactsChanged(): void { + this.hasContacts = this.contacts && this.contacts.length > 0; + } +} + +CbContactList.defineAsync({ + name: 'cb-contact-list', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/organisms/cb-header/cb-header.css b/examples/app/contact-book-manager/src/organisms/cb-header/cb-header.css new file mode 100644 index 00000000..48f1178f --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-header/cb-header.css @@ -0,0 +1,105 @@ +:host { + display: block; +} + +.header { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 24px; + background: #fff; + border-bottom: 1px solid #e5e7eb; + position: sticky; + top: 0; + z-index: 10; + -webkit-app-region: drag; +} + +.header-left { + flex-shrink: 0; +} + +.title { + font-size: 20px; + font-weight: 700; + color: #111827; + margin: 0; +} + +.header-center { + flex: 1; + max-width: 480px; +} + +.search-container { + display: flex; + align-items: center; + background: #fff; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 0 12px; + gap: 8px; + -webkit-app-region: no-drag; +} + +.search-container:focus-within { + border-color: #0078d4; + box-shadow: 0 0 0 2px rgba(0, 120, 212, 0.15); +} + +.search-icon { + font-size: 16px; + color: #9ca3af; +} + +.search-input { + flex: 1; + border: none; + outline: none; + padding: 10px 0; + font-size: 14px; + font-family: inherit; + background: transparent; +} + +.header-right { + flex-shrink: 0; +} + +.add-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + background: #0078d4; + color: #fff; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + -webkit-app-region: no-drag; +} + +.add-btn:hover { + background: #106ebe; +} + +.add-icon { + font-size: 18px; + font-weight: bold; +} + +@media (max-width: 768px) { + .add-label { + display: none; + } + + .header-center { + max-width: none; + } +} + +.title::before { content: '\1F4C7 '; } +.search-icon::before { content: '\1F50D'; } diff --git a/examples/app/contact-book-manager/src/organisms/cb-header/cb-header.html b/examples/app/contact-book-manager/src/organisms/cb-header/cb-header.html new file mode 100644 index 00000000..e9588a63 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-header/cb-header.html @@ -0,0 +1,19 @@ + diff --git a/examples/app/contact-book-manager/src/organisms/cb-header/cb-header.ts b/examples/app/contact-book-manager/src/organisms/cb-header/cb-header.ts new file mode 100644 index 00000000..cb79d187 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-header/cb-header.ts @@ -0,0 +1,45 @@ +import { FASTElement, attr } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbHeader extends RenderableFASTElement(FASTElement) { + @attr({ attribute: 'search-query' }) searchQuery = ''; + + private listenersAttached!: boolean; + + connectedCallback(): void { + super.connectedCallback(); + if (this.listenersAttached) return; + this.listenersAttached = true; + this.addEventListener('click', (e: Event) => { + this.onClick(e as MouseEvent); + }); + this.addEventListener('input', (e: Event) => { + this.onInput(e); + }); + } + + private emit(type: string, detail?: unknown): void { + this.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail })); + } + + onInput(e: Event): void { + const input = e.composedPath().find(el => (el as HTMLElement).tagName === 'INPUT') as HTMLInputElement; + if (input) { + this.searchQuery = input.value; + this.emit('search', { value: input.value }); + } + } + + onClick(e: MouseEvent): void { + const target = e.composedPath()[0] as HTMLElement; + const action = target.closest('[data-action]')?.getAttribute('data-action'); + if (action === 'add-contact') { + this.emit('add-contact'); + } + } +} + +CbHeader.defineAsync({ + name: 'cb-header', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.css b/examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.css new file mode 100644 index 00000000..35f80bb9 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.css @@ -0,0 +1,92 @@ +:host { + display: block; +} + +.sidebar { + width: 260px; + min-height: calc(100vh - 65px); + background: #fff; + border-right: 1px solid #e5e7eb; + padding: 16px 12px; + overflow-y: auto; +} + +.nav-section { + display: flex; + flex-direction: column; + gap: 2px; +} + +.nav-divider { + height: 1px; + background: #e5e7eb; + margin: 12px 0; +} + +.nav-heading { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #9ca3af; + padding: 8px 14px 4px; + font-weight: 600; + margin: 0; +} + +/* Inlined nav-item styles */ +.nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s; +} + +.nav-item:hover { + background: #f3f4f6; +} + +.nav-item.active { + background: #e0f2fe; + color: #0078d4; +} + +.nav-icon { + font-size: 18px; + width: 24px; + text-align: center; +} + +.nav-label { + flex: 1; + font-size: 14px; + font-weight: 500; +} + +.nav-count { + font-size: 12px; + background: #e5e7eb; + color: #6b7280; + border-radius: 10px; + padding: 1px 8px; + min-width: 22px; + text-align: center; +} + +.nav-item.active .nav-count { + background: #bfdbfe; + color: #0078d4; +} + +.nav-icon-dashboard::before { content: '\1F4CA'; } +.nav-icon-contacts::before { content: '\1F465'; } +.nav-icon-favorites::before { content: '\2B50'; } +.nav-icon-folder::before { content: '\1F4C1'; } + +@media (max-width: 768px) { + :host { + display: none; + } +} diff --git a/examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.html b/examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.html new file mode 100644 index 00000000..923f42bb --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.html @@ -0,0 +1,30 @@ + diff --git a/examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.ts b/examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.ts new file mode 100644 index 00000000..5026c593 --- /dev/null +++ b/examples/app/contact-book-manager/src/organisms/cb-sidebar/cb-sidebar.ts @@ -0,0 +1,77 @@ +import { FASTElement, attr, observable } from '@microsoft/fast-element'; +import { RenderableFASTElement } from '@microsoft/fast-html'; + +export class CbSidebar extends RenderableFASTElement(FASTElement) { + @attr page = 'dashboard'; + @attr({ attribute: 'active-group' }) activeGroup = 'all'; + @attr({ attribute: 'total-contacts' }) totalContacts = '0'; + @attr({ attribute: 'total-favorites' }) totalFavorites = '0'; + groups!: string[]; + @observable dashboardActive!: boolean; + @observable contactsActive!: boolean; + @observable favoritesActive!: boolean; + + private listenersAttached!: boolean; + + connectedCallback(): void { + super.connectedCallback(); + if (this.listenersAttached) return; + this.listenersAttached = true; + this.addEventListener('click', (e: Event) => { + this.onNavClick(e as MouseEvent); + }); + } + + async prepare(): Promise { + if (!this.shadowRoot) return; + + const groups: string[] = []; + for (const el of this.shadowRoot.querySelectorAll('.nav-item')) { + const label = el.getAttribute('data-nav') || ''; + if (['Dashboard', 'All Contacts', 'Favorites'].includes(label)) continue; + if (label) groups.push(label); + } + this.groups = groups; + this.updateActiveState(); + } + + pageChanged(): void { + this.updateActiveState(); + } + + private updateActiveState(): void { + this.dashboardActive = this.page === 'dashboard'; + this.contactsActive = this.page === 'contacts'; + this.favoritesActive = this.page === 'favorites'; + + // Update active class on nav items + for (const el of this.shadowRoot?.querySelectorAll('.nav-item') || []) { + const nav = el.getAttribute('data-nav') || ''; + let isActive = false; + if (nav === 'Dashboard') isActive = this.dashboardActive; + else if (nav === 'All Contacts') isActive = this.contactsActive; + else if (nav === 'Favorites') isActive = this.favoritesActive; + else if (this.page === 'group') isActive = nav === this.activeGroup; + el.classList.toggle('active', isActive); + } + } + + private emit(type: string, detail?: unknown): void { + this.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail })); + } + + onNavClick(e: MouseEvent): void { + const target = (e.composedPath()[0] as HTMLElement).closest('.nav-item') as HTMLElement | null; + if (!target) return; + const label = target.getAttribute('data-nav') || ''; + if (label === 'Dashboard') this.emit('navigate', { page: 'dashboard' }); + else if (label === 'All Contacts') this.emit('navigate', { page: 'contacts' }); + else if (label === 'Favorites') this.emit('navigate', { page: 'favorites' }); + else this.emit('navigate', { page: 'group', group: label }); + } +} + +CbSidebar.defineAsync({ + name: 'cb-sidebar', + templateOptions: 'defer-and-hydrate', +}); diff --git a/examples/app/contact-book-manager/tsconfig.json b/examples/app/contact-book-manager/tsconfig.json new file mode 100644 index 00000000..efdf4bb9 --- /dev/null +++ b/examples/app/contact-book-manager/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "declaration": false, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/integration/electron/README.md b/examples/integration/electron/README.md new file mode 100644 index 00000000..b7c66da8 --- /dev/null +++ b/examples/integration/electron/README.md @@ -0,0 +1,44 @@ +# WebUI Electron Integration + +Wraps any pre-built WebUI app in a frameless Electron desktop window using the `webui-node` native addon. + +## Prerequisites + +1. Build the native addon: + +```bash +cargo build -p webui-node --release +``` + +2. Build a WebUI app (e.g. hello-world): + +```bash +cargo run -p webui-cli -- build ../../app/hello-world/templates --out ../../app/hello-world/dist --css external --plugin=fast +esbuild ../../app/hello-world/src/index.ts --bundle --outfile=../../app/hello-world/dist/index.js --format=esm +``` + +## Usage + +Run with the default hello-world app: + +```bash +pnpm start +``` + +Or point to any other WebUI app: + +```bash +# hello-world +electron dist/main.js ../../app/hello-world/dist ../../app/hello-world/data/state.json --plugin=fast + +# contact-book-manager +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 the app's `dist/` directory containing `protocol.bin` and CSS/JS assets | `../../app/hello-world/dist` | +| `state.json` | Path to the state JSON file | `../../app/hello-world/data/state.json` | +| `--plugin=fast` | Enable FAST hydration plugin | _(disabled)_ | diff --git a/examples/integration/electron/main.ts b/examples/integration/electron/main.ts new file mode 100644 index 00000000..9f1e0c20 --- /dev/null +++ b/examples/integration/electron/main.ts @@ -0,0 +1,137 @@ +// Electron integration for WebUI — renders any pre-built WebUI app in a +// frameless desktop window using the webui-node native addon. +// +// Prerequisites: +// 1. Build the native addon: cargo build -p webui-node --release +// 2. Build a WebUI app: +// cargo run -p webui-cli -- build /src --out /dist --css external --plugin=fast +// esbuild /src/index.ts --bundle --outfile=/dist/index.js --format=esm +// +// Usage: +// electron dist/main.js [dist-dir] [state.json] [--plugin=fast] + +import { app, BrowserWindow, Menu, net, protocol } from 'electron'; +import { existsSync, readFileSync } from 'fs'; +import { platform } from 'os'; +import { resolve, join, basename } from 'path'; + +// --------------------------------------------------------------------------- +// CLI arguments +// --------------------------------------------------------------------------- + +// Filter out Electron's own args (everything before --) +const args = process.argv.slice(2).filter(a => !a.startsWith('--inspect')); + +const positional = args.filter(a => !a.startsWith('--')); +const flags = args.filter(a => a.startsWith('--')); + +const distDir = resolve(positional[0] || join(import.meta.dirname, '..', '..', 'app', 'hello-world', 'dist')); +const statePath = resolve(positional[1] || join(import.meta.dirname, '..', '..', 'app', 'hello-world', 'data', 'state.json')); + +const pluginArg = flags.find(a => a.startsWith('--plugin=')); +const pluginName = pluginArg ? pluginArg.split('=')[1] : undefined; + +// --------------------------------------------------------------------------- +// Native addon +// --------------------------------------------------------------------------- + +function loadAddon() { + const root = resolve(import.meta.dirname, '..', '..', '..'); + const ext = platform() === 'darwin' ? 'dylib' : platform() === 'win32' ? 'dll' : 'so'; + const prefix = platform() === 'win32' ? '' : 'lib'; + const filename = `${prefix}webui_node.${ext}`; + + const mod = { exports: {} } as { exports: Record }; + for (const profile of ['release', 'debug']) { + try { + process.dlopen(mod, join(root, 'target', profile, filename)); + return mod.exports as { render(protocol: Buffer, state: string, onChunk: (chunk: string) => void, plugin?: string): void }; + } catch { + // try next profile + } + } + throw new Error( + `Could not find ${filename} in target/release or target/debug.\nRun: cargo build -p webui-node --release`, + ); +} + +const addon = loadAddon(); + +// --------------------------------------------------------------------------- +// Custom protocol +// --------------------------------------------------------------------------- + +protocol.registerSchemesAsPrivileged([ + { + scheme: 'webui', + privileges: { standard: true, secure: true, supportFetchAPI: true }, + }, +]); + +// --------------------------------------------------------------------------- +// App lifecycle +// --------------------------------------------------------------------------- + +app.whenReady().then(() => { + Menu.setApplicationMenu(null); + + if (!existsSync(join(distDir, 'protocol.bin'))) { + console.error(`protocol.bin not found in ${distDir}`); + console.error('Build the app first: cargo run -p webui-cli -- build /src --out /dist'); + app.quit(); + return; + } + + const protocolBin = readFileSync(join(distDir, 'protocol.bin')); + const stateJson = existsSync(statePath) ? readFileSync(statePath, 'utf-8') : '{}'; + + // Render SSR HTML + const chunks: string[] = []; + addon.render(protocolBin, stateJson, (chunk: string) => { + chunks.push(chunk); + }, pluginName); + const html = chunks.join(''); + + // Protocol handler — serves rendered HTML + static assets + protocol.handle('webui', (request) => { + const url = new URL(request.url); + + if (url.pathname === '/' || url.pathname === '') { + return new Response(html, { + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }); + } + + // Serve static assets (CSS, JS, maps) from dist dir + const filePath = join(distDir, basename(url.pathname)); + if (existsSync(filePath)) { + return net.fetch(`file://${filePath}`); + } + + return new Response('Not Found', { status: 404 }); + }); + + // Main window + const win = new BrowserWindow({ + width: 1200, + height: 800, + titleBarStyle: 'hidden', + titleBarOverlay: { + color: '#ffffff', + symbolColor: '#374151', + height: 40, + }, + webPreferences: { + preload: join(import.meta.dirname, 'preload.js'), + contextIsolation: true, + }, + }); + + win.loadURL('webui://app/'); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); diff --git a/examples/integration/electron/package.json b/examples/integration/electron/package.json new file mode 100644 index 00000000..42ce8d4d --- /dev/null +++ b/examples/integration/electron/package.json @@ -0,0 +1,15 @@ +{ + "name": "webui-electron-example", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "esbuild main.ts preload.ts --outdir=dist --platform=node --format=esm", + "start": "npm run build && electron dist/main.js" + }, + "devDependencies": { + "electron": "catalog:", + "esbuild": "catalog:", + "typescript": "catalog:" + } +} diff --git a/examples/integration/electron/preload.ts b/examples/integration/electron/preload.ts new file mode 100644 index 00000000..3c01bbf0 --- /dev/null +++ b/examples/integration/electron/preload.ts @@ -0,0 +1,7 @@ +import { contextBridge } from 'electron'; + +contextBridge.exposeInMainWorld('versions', { + electron: process.versions.electron, + node: process.versions.node, + chrome: process.versions.chrome, +}); diff --git a/examples/integration/electron/tsconfig.json b/examples/integration/electron/tsconfig.json new file mode 100644 index 00000000..56caed33 --- /dev/null +++ b/examples/integration/electron/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": false, + "outDir": "dist" + }, + "include": ["*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb0541e2..31b3830f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ catalogs: '@microsoft/fast-html': specifier: 1.0.0-alpha.38 version: 1.0.0-alpha.38 + electron: + specifier: ^36.3.1 + version: 36.9.5 esbuild: specifier: ^0.25.0 version: 0.25.12 @@ -84,11 +87,29 @@ importers: version: 6.39.15 vitepress: specifier: 'catalog:' - version: 1.6.4(@algolia/client-search@5.21.0)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3) + version: 1.6.4(@algolia/client-search@5.21.0)(@types/node@22.19.13)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3) vue: specifier: 'catalog:' version: 3.5.29(typescript@5.9.3) + examples/app/contact-book-manager: + devDependencies: + '@microsoft/fast-element': + specifier: 'catalog:' + version: 2.10.1 + '@microsoft/fast-html': + specifier: 'catalog:' + version: 1.0.0-alpha.38(@microsoft/fast-element@2.10.1) + esbuild: + specifier: 'catalog:' + version: 0.25.12 + tslib: + specifier: 'catalog:' + version: 2.8.1 + typescript: + specifier: 'catalog:' + version: 5.9.3 + examples/app/hello-world: {} examples/app/todo-fast: @@ -109,6 +130,18 @@ importers: specifier: 'catalog:' version: 5.9.3 + examples/integration/electron: + devDependencies: + electron: + specifier: 'catalog:' + version: 36.9.5 + esbuild: + specifier: 'catalog:' + version: 0.25.12 + typescript: + specifier: 'catalog:' + version: 5.9.3 + examples/integration/node: {} packages: @@ -258,6 +291,10 @@ packages: search-insights: optional: true + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -722,12 +759,29 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} @@ -740,12 +794,21 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@22.19.13': + resolution: {integrity: sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -851,6 +914,21 @@ packages: birpc@0.2.19: resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -860,6 +938,9 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -873,20 +954,71 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + electron@36.9.5: + resolution: {integrity: sha512-1UCss2IqxqujSzg/2jkRjuiT3G+EEXgd6UKB5kUekwQW1LJ6d4QCr8YItfC3Rr9VIGRDJ29eOERmnRNO1Eh+NA==} + engines: {node: '>= 12.20.55'} + hasBin: true + emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -897,17 +1029,59 @@ packages: engines: {node: '>=18'} hasBin: true + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + focus-trap@7.6.4: resolution: {integrity: sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==} + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -920,16 +1094,43 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + mdast-util-to-hast@13.2.0: resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} @@ -948,20 +1149,49 @@ packages: micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minisearch@7.1.2: resolution: {integrity: sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==} mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -975,9 +1205,20 @@ packages: preact@10.26.4: resolution: {integrity: sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + property-information@7.0.0: resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -987,9 +1228,19 @@ packages: regex@6.0.1: resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + rollup@4.36.0: resolution: {integrity: sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -998,6 +1249,22 @@ packages: search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + shiki@2.5.0: resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} @@ -1012,12 +1279,19 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + superjson@2.2.2: resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} engines: {node: '>=16'} @@ -1031,11 +1305,18 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -1051,6 +1332,10 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -1111,6 +1396,12 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -1340,6 +1631,20 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' + '@electron/get@2.0.3': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -1634,12 +1939,31 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@4.6.0': {} + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 22.19.13 + '@types/responselike': 1.0.3 + '@types/estree@1.0.6': {} '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 + '@types/http-cache-semantics@4.2.0': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.19.13 + '@types/linkify-it@5.0.0': {} '@types/markdown-it@14.1.2': @@ -1653,15 +1977,28 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/node@22.19.13': + dependencies: + undici-types: 6.21.0 + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.19.13 + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.21': {} + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.19.13 + optional: true + '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-vue@5.2.3(vite@5.4.14)(vue@3.5.29(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.3(vite@5.4.14(@types/node@22.19.13))(vue@3.5.29(typescript@5.9.3))': dependencies: - vite: 5.4.14 + vite: 5.4.14(@types/node@22.19.13) vue: 3.5.29(typescript@5.9.3) '@vue/compiler-core@3.5.29': @@ -1781,12 +2118,33 @@ snapshots: birpc@0.2.19: {} + boolean@3.2.0: + optional: true + + buffer-crc32@0.2.13: {} + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + ccount@2.0.1: {} character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + comma-separated-tokens@2.0.3: {} copy-anything@3.0.5: @@ -1797,16 +2155,66 @@ snapshots: csstype@3.2.3: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + dequal@2.0.3: {} + detect-node@2.1.0: + optional: true + devlop@1.1.0: dependencies: dequal: 2.0.3 + electron@36.9.5: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 22.19.13 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + emoji-regex-xs@1.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + entities@7.0.1: {} + env-paths@2.2.1: {} + + es-define-property@1.0.1: + optional: true + + es-errors@1.3.0: + optional: true + + es6-error@4.1.1: + optional: true + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -1862,15 +2270,82 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + escape-string-regexp@4.0.0: + optional: true + estree-walker@2.0.2: {} + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + focus-trap@7.6.4: dependencies: tabbable: 6.2.0 + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fsevents@2.3.3: optional: true + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.4 + serialize-error: 7.0.1 + optional: true + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + + gopd@1.2.0: + optional: true + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -1893,14 +2368,41 @@ snapshots: html-void-elements@3.0.0: {} + http-cache-semantics@4.2.0: {} + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + is-what@4.1.16: {} + json-buffer@3.0.1: {} + + json-stringify-safe@5.0.1: + optional: true + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + lowercase-keys@2.0.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 mark.js@8.11.1: {} + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + mdast-util-to-hast@13.2.0: dependencies: '@types/hast': 3.0.4 @@ -1930,18 +2432,37 @@ snapshots: micromark-util-types@2.0.2: {} + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + minisearch@7.1.2: {} mitt@3.0.1: {} + ms@2.1.3: {} + nanoid@3.3.11: {} + normalize-url@6.1.0: {} + + object-keys@1.1.1: + optional: true + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + oniguruma-to-es@3.1.1: dependencies: emoji-regex-xs: 1.0.0 regex: 6.0.1 regex-recursion: 6.0.2 + p-cancelable@2.1.1: {} + + pend@1.2.0: {} + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} @@ -1954,8 +2475,17 @@ snapshots: preact@10.26.4: {} + progress@2.0.3: {} + property-information@7.0.0: {} + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + quick-lru@5.1.1: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -1966,8 +2496,24 @@ snapshots: dependencies: regex-utilities: 2.3.0 + resolve-alpn@1.2.1: {} + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + rfdc@1.4.1: {} + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + rollup@4.36.0: dependencies: '@types/estree': 1.0.6 @@ -1995,6 +2541,19 @@ snapshots: search-insights@2.17.3: {} + semver-compare@1.0.0: + optional: true + + semver@6.3.1: {} + + semver@7.7.4: + optional: true + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + shiki@2.5.0: dependencies: '@shikijs/core': 2.5.0 @@ -2012,6 +2571,9 @@ snapshots: speakingurl@14.0.1: {} + sprintf-js@1.1.3: + optional: true + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -2019,6 +2581,12 @@ snapshots: style-mod@4.1.3: {} + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + superjson@2.2.2: dependencies: copy-anything: 3.0.5 @@ -2029,8 +2597,13 @@ snapshots: tslib@2.8.1: {} + type-fest@0.13.1: + optional: true + typescript@5.9.3: {} + undici-types@6.21.0: {} + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 @@ -2054,6 +2627,8 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universalify@0.1.2: {} + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -2064,15 +2639,16 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite@5.4.14: + vite@5.4.14(@types/node@22.19.13): dependencies: esbuild: 0.21.5 postcss: 8.5.6 rollup: 4.36.0 optionalDependencies: + '@types/node': 22.19.13 fsevents: 2.3.3 - vitepress@1.6.4(@algolia/client-search@5.21.0)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3): + vitepress@1.6.4(@algolia/client-search@5.21.0)(@types/node@22.19.13)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3): dependencies: '@docsearch/css': 3.8.2 '@docsearch/js': 3.8.2(@algolia/client-search@5.21.0)(search-insights@2.17.3) @@ -2081,7 +2657,7 @@ snapshots: '@shikijs/transformers': 2.5.0 '@shikijs/types': 2.5.0 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.2.3(vite@5.4.14)(vue@3.5.29(typescript@5.9.3)) + '@vitejs/plugin-vue': 5.2.3(vite@5.4.14(@types/node@22.19.13))(vue@3.5.29(typescript@5.9.3)) '@vue/devtools-api': 7.7.2 '@vue/shared': 3.5.29 '@vueuse/core': 12.8.2(typescript@5.9.3) @@ -2090,7 +2666,7 @@ snapshots: mark.js: 8.11.1 minisearch: 7.1.2 shiki: 2.5.0 - vite: 5.4.14 + vite: 5.4.14(@types/node@22.19.13) vue: 3.5.29(typescript@5.9.3) optionalDependencies: postcss: 8.5.6 @@ -2133,4 +2709,11 @@ snapshots: w3c-keyname@2.2.8: {} + wrappy@1.0.2: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4bed85cb..457ac3bc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,8 +2,7 @@ packages: - docs - examples/** - '!**/test/**' -onlyBuiltDependencies: - - esbuild + catalog: '@codemirror/commands': ^6.10.2 '@codemirror/lang-css': ^6.3.1 @@ -15,8 +14,13 @@ catalog: '@codemirror/view': ^6.39.15 '@microsoft/fast-element': ^2.0.1 '@microsoft/fast-html': 1.0.0-alpha.38 + electron: ^36.3.1 esbuild: ^0.25.0 tslib: ^2.8.0 typescript: ^5.9.3 vitepress: ^1.6.4 vue: ^3.5.29 + +onlyBuiltDependencies: + - electron + - esbuild From d3ad826c7529757b45e320caa158aa4fc93537bc Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Thu, 5 Mar 2026 11:17:46 -0800 Subject: [PATCH 2/8] fix(examples): fix missing group buttons in contact form and require explicit CLI args - Add ensureGroupButtons() to cb-contact-form that reads groups from the sidebar when the form is dynamically created (not SSR'd), fixing the empty group selector in Add/Edit Contact views - Remove hello-world fallback defaults from electron and node integrations; both now require explicit dist-dir and state path args, printing usage on missing args - Remove electron dependency from contact-book-manager; Electron is launched from integration/electron by passing app dist/state paths - Update READMEs to reflect required args and removed electron section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- examples/app/contact-book-manager/README.md | 15 ------- .../app/contact-book-manager/package.json | 2 +- .../cb-contact-form/cb-contact-form.ts | 45 +++++++++++++++++++ examples/integration/electron/README.md | 22 +++------ examples/integration/electron/main.ts | 13 ++++-- examples/integration/electron/package.json | 2 +- examples/integration/node/index.js | 12 +++-- pnpm-lock.yaml | 3 ++ 8 files changed, 76 insertions(+), 38 deletions(-) diff --git a/examples/app/contact-book-manager/README.md b/examples/app/contact-book-manager/README.md index dfe83913..1bf24098 100644 --- a/examples/app/contact-book-manager/README.md +++ b/examples/app/contact-book-manager/README.md @@ -136,9 +136,6 @@ contact-book-manager/ │ ├── cb-contact-list/ # Scrollable contact list │ ├── cb-header/ # Sticky top bar │ └── cb-sidebar/ # Left navigation panel - └── electron/ # Optional Electron wrapper - ├── main.ts - └── preload.ts ``` ## Component Catalog @@ -193,15 +190,3 @@ Components with multiple action buttons (e.g., ``) use a sing - **Desktop (≥768px):** Fixed sidebar (260px) + scrollable main content area - **Mobile (<768px):** Sidebar hidden, single-column layout, "Add Contact" button label hidden (icon only) - -## Electron (Optional) - -The `src/electron/` directory contains an optional desktop wrapper. It uses the `webui-node` native addon to render `protocol.bin` + `state.json` into HTML, then serves it via a custom `webui://` protocol scheme inside an Electron `BrowserWindow`. - -To use it, build the native addon first: - -```bash -cargo build -p webui-node --release -``` - -The Electron entry point is not part of the standard web build — it is an alternative deployment target that reuses the same SSR binary and client JS bundle. diff --git a/examples/app/contact-book-manager/package.json b/examples/app/contact-book-manager/package.json index 6ad2bbe5..32dcaee2 100644 --- a/examples/app/contact-book-manager/package.json +++ b/examples/app/contact-book-manager/package.json @@ -10,7 +10,7 @@ "start:client": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --sourcemap --watch", "start:server": "cargo run -p webui-cli -- start ./src --state ./data/state.json --plugin=fast --servedir ./dist --port 3001", "start": "cargo xtask dev contact-book-manager" - }, + }, "devDependencies": { "@microsoft/fast-element": "catalog:", "@microsoft/fast-html": "catalog:", diff --git a/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.ts b/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.ts index 3c78ecc3..7b5d7346 100644 --- a/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.ts +++ b/examples/app/contact-book-manager/src/organisms/cb-contact-form/cb-contact-form.ts @@ -25,6 +25,10 @@ export class CbContactForm extends RenderableFASTElement(FASTElement) { this.addEventListener('click', (e: Event) => { this.onClick(e as MouseEvent); }); + + // When the form is dynamically created (not SSR'd), the loop + // for groups produces no buttons. Ensure they exist after first render. + requestAnimationFrame(() => this.ensureGroupButtons()); } async prepare(): Promise { @@ -37,6 +41,47 @@ export class CbContactForm extends RenderableFASTElement(FASTElement) { this.selectedGroup = this.group || (groups.length > 0 ? groups[0] : ''); } + private ensureGroupButtons(): void { + const sr = this.shadowRoot; + if (!sr) return; + + const existing = sr.querySelectorAll('.group-option'); + if (existing.length > 0) return; + + // Read groups from the sidebar (always SSR'd with group nav items) + const hostRoot = this.getRootNode() as ShadowRoot; + const app = hostRoot?.host; + const sidebar = app?.shadowRoot?.querySelector('cb-sidebar'); + const navItems = sidebar?.shadowRoot?.querySelectorAll('.nav-item[data-nav]') || []; + const groups: string[] = []; + for (const el of navItems) { + const label = (el as HTMLElement).getAttribute('data-nav') || ''; + if (!['Dashboard', 'All Contacts', 'Favorites'].includes(label) && label) { + groups.push(label); + } + } + + if (groups.length === 0) return; + + const selector = sr.querySelector('.group-selector'); + if (!selector) return; + + for (const g of groups) { + const btn = document.createElement('button'); + btn.className = 'group-option'; + btn.setAttribute('data-group', g); + btn.textContent = g; + selector.appendChild(btn); + } + + formGroupsStore.set(this, groups); + this.selectedGroup = this.group || groups[0] || ''; + + for (const btn of selector.querySelectorAll('.group-option')) { + btn.classList.toggle('active', btn.getAttribute('data-group') === this.selectedGroup); + } + } + onClick(e: MouseEvent): void { const target = e.composedPath()[0] as HTMLElement; const action = target.closest('[data-action]')?.getAttribute('data-action'); diff --git a/examples/integration/electron/README.md b/examples/integration/electron/README.md index b7c66da8..ac1762e5 100644 --- a/examples/integration/electron/README.md +++ b/examples/integration/electron/README.md @@ -19,26 +19,18 @@ esbuild ../../app/hello-world/src/index.ts --bundle --outfile=../../app/hello-wo ## Usage -Run with the default hello-world app: - -```bash -pnpm start -``` - -Or point to any other WebUI app: - ```bash # hello-world -electron dist/main.js ../../app/hello-world/dist ../../app/hello-world/data/state.json --plugin=fast +pnpm start ../../app/hello-world/dist ../../app/hello-world/data/state.json --plugin=fast # contact-book-manager -electron dist/main.js ../../app/contact-book-manager/dist ../../app/contact-book-manager/data/state.json --plugin=fast +pnpm start ../../app/contact-book-manager/dist ../../app/contact-book-manager/data/state.json --plugin=fast ``` ## CLI Arguments -| Argument | Description | Default | -|---|---|---| -| `dist-dir` | Path to the app's `dist/` directory containing `protocol.bin` and CSS/JS assets | `../../app/hello-world/dist` | -| `state.json` | Path to the state JSON file | `../../app/hello-world/data/state.json` | -| `--plugin=fast` | Enable FAST hydration plugin | _(disabled)_ | +| Argument | Description | +|---|---| +| `dist-dir` | **(required)** Path to the app's `dist/` directory containing `protocol.bin` and CSS/JS assets | +| `state.json` | **(required)** Path to the state JSON file | +| `--plugin=fast` | Enable FAST hydration plugin _(optional)_ | diff --git a/examples/integration/electron/main.ts b/examples/integration/electron/main.ts index 9f1e0c20..ee266a04 100644 --- a/examples/integration/electron/main.ts +++ b/examples/integration/electron/main.ts @@ -25,8 +25,15 @@ const args = process.argv.slice(2).filter(a => !a.startsWith('--inspect')); const positional = args.filter(a => !a.startsWith('--')); const flags = args.filter(a => a.startsWith('--')); -const distDir = resolve(positional[0] || join(import.meta.dirname, '..', '..', 'app', 'hello-world', 'dist')); -const statePath = resolve(positional[1] || join(import.meta.dirname, '..', '..', 'app', 'hello-world', 'data', 'state.json')); +if (positional.length < 2) { + console.error('Usage: electron dist/main.js [--plugin=fast]'); + console.error(' dist-dir Path to the app dist/ directory containing protocol.bin'); + console.error(' state.json Path to the JSON state file'); + process.exit(1); +} + +const distDir = resolve(positional[0]); +const statePath = resolve(positional[1]); const pluginArg = flags.find(a => a.startsWith('--plugin=')); const pluginName = pluginArg ? pluginArg.split('=')[1] : undefined; @@ -36,7 +43,7 @@ const pluginName = pluginArg ? pluginArg.split('=')[1] : undefined; // --------------------------------------------------------------------------- function loadAddon() { - const root = resolve(import.meta.dirname, '..', '..', '..'); + const root = resolve(import.meta.dirname, '..', '..', '..', '..'); const ext = platform() === 'darwin' ? 'dylib' : platform() === 'win32' ? 'dll' : 'so'; const prefix = platform() === 'win32' ? '' : 'lib'; const filename = `${prefix}webui_node.${ext}`; diff --git a/examples/integration/electron/package.json b/examples/integration/electron/package.json index 42ce8d4d..905ed244 100644 --- a/examples/integration/electron/package.json +++ b/examples/integration/electron/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "esbuild main.ts preload.ts --outdir=dist --platform=node --format=esm", - "start": "npm run build && electron dist/main.js" + "start": "npm run build && electron dist/main.js --" }, "devDependencies": { "electron": "catalog:", diff --git a/examples/integration/node/index.js b/examples/integration/node/index.js index d938df3c..fd3c8717 100644 --- a/examples/integration/node/index.js +++ b/examples/integration/node/index.js @@ -40,9 +40,15 @@ function loadAddon() { ); } -const protocolPath = - process.argv[2] || "../../app/hello-world/dist/protocol.bin"; -const statePath = process.argv[3] || "../../app/hello-world/data/state.json"; +if (!process.argv[2] || !process.argv[3]) { + console.error("Usage: node index.js [--plugin=fast]"); + console.error(" protocol.bin Path to the compiled protocol binary"); + console.error(" state.json Path to the JSON state file"); + process.exit(1); +} + +const protocolPath = process.argv[2]; +const statePath = process.argv[3]; // Check for --plugin=fast flag const pluginArg = process.argv.find((a) => a.startsWith("--plugin=")); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31b3830f..04cbf977 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,9 @@ importers: '@microsoft/fast-html': specifier: 'catalog:' version: 1.0.0-alpha.38(@microsoft/fast-element@2.10.1) + electron: + specifier: 'catalog:' + version: 36.9.5 esbuild: specifier: 'catalog:' version: 0.25.12 From 63d95ab85294f003845640af6db3053e53f3013f Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Thu, 5 Mar 2026 11:46:38 -0800 Subject: [PATCH 3/8] build(xtask): add electron integration to build-examples step Register electron as an integration build that runs 'pnpm run build' in examples/integration/electron, compiling main.ts and preload.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- xtask/src/build_examples.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/xtask/src/build_examples.rs b/xtask/src/build_examples.rs index b90a0e88..6a6ce175 100644 --- a/xtask/src/build_examples.rs +++ b/xtask/src/build_examples.rs @@ -28,6 +28,15 @@ pub const INTEGRATION_BUILDS: &[IntegrationBuild] = &[ }], run_commands: &[], }, + IntegrationBuild { + name: "electron", + commands: &[BuildCommand { + cmd: "pnpm", + args: &["run", "build"], + cwd: Some("examples/integration/electron"), + }], + run_commands: &[], + }, IntegrationBuild { name: "rust", commands: &[BuildCommand { From fdb6ce312630ec4fc79c1d89307e69167b9a5532 Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Thu, 5 Mar 2026 12:05:49 -0800 Subject: [PATCH 4/8] chore: fix package lock --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04cbf977..31b3830f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,9 +100,6 @@ importers: '@microsoft/fast-html': specifier: 'catalog:' version: 1.0.0-alpha.38(@microsoft/fast-element@2.10.1) - electron: - specifier: 'catalog:' - version: 36.9.5 esbuild: specifier: 'catalog:' version: 0.25.12 From c36fa4b8de9e58ca8c177fdfd81cdf3c1dd1fbc0 Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Thu, 5 Mar 2026 12:04:40 -0800 Subject: [PATCH 5/8] feat: add SSR performance showdown integration example (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Rust/actix-web server under examples/integration/ssr-performance-showdown that replicates the spiral-tile benchmark from the ssr-performance-showdown project (https://github.com/nicolo-ribaudo/ssr-performance-showdown). The template (app/index.html) uses a loop over a tiles array and is compiled ahead of time into dist/protocol.bin via webui build. At startup the server loads the pre-built protocol binary. On every request it computes ~2400 spiral tile positions (matching the showdown parameters: 960x720 canvas, 10px cells, 0.2 angle step) and passes them as JSON state to WebUIHandler, which renders the final HTML. This keeps the runtime dependency surface minimal — only webui-protocol and webui-handler are needed; the parser is not linked into the server binary. --- .../ssr-performance-showdown/Cargo.lock | 1997 +++++++++++++++++ .../ssr-performance-showdown/Cargo.toml | 18 + .../ssr-performance-showdown/README.md | 72 + .../ssr-performance-showdown/app/index.html | 37 + .../ssr-performance-showdown/src/main.rs | 140 ++ 5 files changed, 2264 insertions(+) create mode 100644 examples/integration/ssr-performance-showdown/Cargo.lock create mode 100644 examples/integration/ssr-performance-showdown/Cargo.toml create mode 100644 examples/integration/ssr-performance-showdown/README.md create mode 100644 examples/integration/ssr-performance-showdown/app/index.html create mode 100644 examples/integration/ssr-performance-showdown/src/main.rs diff --git a/examples/integration/ssr-performance-showdown/Cargo.lock b/examples/integration/ssr-performance-showdown/Cargo.lock new file mode 100644 index 00000000..03a22c5e --- /dev/null +++ b/examples/integration/ssr-performance-showdown/Cargo.lock @@ -0,0 +1,1997 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" +dependencies = [ + "bytestring", + "cfg-if", + "http", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.6.2", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "webui-expressions" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "webui-protocol", + "webui-state", +] + +[[package]] +name = "webui-handler" +version = "0.1.0" +dependencies = [ + "html-escape", + "serde", + "serde_json", + "thiserror", + "webui-expressions", + "webui-protocol", + "webui-state", +] + +[[package]] +name = "webui-protocol" +version = "0.1.0" +dependencies = [ + "anyhow", + "prost", + "prost-build", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "webui-ssr-test" +version = "0.1.0" +dependencies = [ + "actix-web", + "anyhow", + "serde_json", + "webui-handler", + "webui-protocol", +] + +[[package]] +name = "webui-state" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/examples/integration/ssr-performance-showdown/Cargo.toml b/examples/integration/ssr-performance-showdown/Cargo.toml new file mode 100644 index 00000000..9f1e7fbf --- /dev/null +++ b/examples/integration/ssr-performance-showdown/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ssr-performance-showdown" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "webui-ssr-test" +path = "src/main.rs" + +[dependencies] +serde_json = { workspace = true } +webui-protocol = { path = "../../../crates/webui-protocol" } +webui-handler = { path = "../../../crates/webui-handler" } +actix-web = { workspace = true } +anyhow = { workspace = true } + +[workspace] diff --git a/examples/integration/ssr-performance-showdown/README.md b/examples/integration/ssr-performance-showdown/README.md new file mode 100644 index 00000000..1426daaa --- /dev/null +++ b/examples/integration/ssr-performance-showdown/README.md @@ -0,0 +1,72 @@ +# WebUI SSR Performance Test + +Rust server that renders a spiral pattern of ~2400 tiles entirely on the +server, comparable to the `fastify-html` entry in the +[ssr-performance-showdown](https://github.com/nicolo-ribaudo/ssr-performance-showdown) benchmark. + +The template is compiled once with `webui build` into a `protocol.bin`. +On every request the server computes the tile positions and passes them +as state to `WebUIHandler`, which streams the final HTML. + +## Usage + +```bash +# From the repository root — build the protocol first +cargo run -p webui-cli --release -- build examples/integration/ssr-performance-showdown/app --out examples/integration/ssr-performance-showdown/dist + +# Run the server frrm this location +cargo run -p ssr-performance-showdown --release # listen on :3000 +cargo run -p ssr-performance-showdown --release -- --port 3001 # custom port +``` + + +## Results +Using `autocannon` we can run with warmup `npx autocannon -c 100 -d 10 -w 2 http://localhost:3000`: + +**webui-rust** +``` +┌─────────┬───────┬───────┬───────┬───────┬──────────┬─────────┬───────┐ +│ Stat    │ 2.5%  │ 50%   │ 97.5% │ 99%   │ Avg      │ Stdev   │ Max   │ +├─────────┼───────┼───────┼───────┼───────┼──────────┼─────────┼───────┤ +│ Latency │ 11 ms │ 18 ms │ 44 ms │ 52 ms │ 21.71 ms │ 9.57 ms │ 87 ms │ +└─────────┴───────┴───────┴───────┴───────┴──────────┴─────────┴───────┘ +┌───────────┬────────┬────────┬────────┬────────┬─────────┬────────┬────────┐ +│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg     │ Stdev  │ Min    │ +├───────────┼────────┼────────┼────────┼────────┼─────────┼────────┼────────┤ +│ Req/Sec   │ 2,987  │ 2,987  │ 4,623  │ 4,875  │ 4,511.3 │ 520.24 │ 2,986  │ +├───────────┼────────┼────────┼────────┼────────┼─────────┼────────┼────────┤ +│ Bytes/Sec │ 453 MB │ 453 MB │ 702 MB │ 740 MB │ 684 MB  │ 79 MB  │ 453 MB │ +└───────────┴────────┴────────┴────────┴────────┴─────────┴────────┴────────┘ +``` + +**fastify-html** +``` +┌─────────┬───────┬───────┬────────┬────────┬──────────┬────────┬────────┐ +│ Stat    │ 2.5%  │ 50%   │ 97.5%  │ 99%    │ Avg      │ Stdev  │ Max    │ +├─────────┼───────┼───────┼────────┼────────┼──────────┼────────┼────────┤ +│ Latency │ 87 ms │ 92 ms │ 107 ms │ 118 ms │ 93.42 ms │ 6.5 ms │ 135 ms │ +└─────────┴───────┴───────┴────────┴────────┴──────────┴────────┴────────┘ +┌───────────┬────────┬────────┬────────┬────────┬─────────┬───────┬────────┐ +│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg     │ Stdev │ Min    │ +├───────────┼────────┼────────┼────────┼────────┼─────────┼───────┼────────┤ +│ Req/Sec   │ 915    │ 915    │ 1,075  │ 1,105  │ 1,060.7 │ 55.63 │ 915    │ +├───────────┼────────┼────────┼────────┼────────┼─────────┼───────┼────────┤ +│ Bytes/Sec │ 181 MB │ 181 MB │ 212 MB │ 218 MB │ 209 MB  │ 11 MB │ 181 MB │ +└───────────┴────────┴────────┴────────┴────────┴─────────┴───────┴────────┘ +``` + +**react** +``` +┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ +│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev    │ Max    │ +├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ +│ Latency │ 168 ms │ 180 ms │ 203 ms │ 210 ms │ 179.24 ms │ 16.77 ms │ 262 ms │ +└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ +┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────┬─────────┐ +│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev  │ Min     │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤ +│ Req/Sec   │ 479     │ 479     │ 554     │ 573     │ 552     │ 25.95  │ 479     │ +├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────┼─────────┤ +│ Bytes/Sec │ 68.2 MB │ 68.2 MB │ 78.8 MB │ 81.5 MB │ 78.5 MB │ 3.7 MB │ 68.1 MB │ +└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────┴─────────┘ +``` \ No newline at end of file diff --git a/examples/integration/ssr-performance-showdown/app/index.html b/examples/integration/ssr-performance-showdown/app/index.html new file mode 100644 index 00000000..827197f5 --- /dev/null +++ b/examples/integration/ssr-performance-showdown/app/index.html @@ -0,0 +1,37 @@ + + + + + + Spiral Pattern + + + +
+ +
+
+
+ + diff --git a/examples/integration/ssr-performance-showdown/src/main.rs b/examples/integration/ssr-performance-showdown/src/main.rs new file mode 100644 index 00000000..28e5043a --- /dev/null +++ b/examples/integration/ssr-performance-showdown/src/main.rs @@ -0,0 +1,140 @@ +//! SSR Performance Test — WebUI Framework +//! +//! Rust server that loads a pre-built protocol.bin, computes a spiral +//! pattern of tiles per-request, and renders them through the WebUI +//! handler — comparable to the `fastify-html` entry in the +//! ssr-performance-showdown benchmark. +//! +//! Prerequisites: +//! cargo run -p webui-cli -- build app --out dist +//! +//! Usage: +//! cargo run # listen on :3000 +//! cargo run -- --port 3001 # custom port + +use actix_web::{web, App, HttpResponse, HttpServer}; +use anyhow::{Context, Result}; +use std::path::Path; +use webui_handler::{ResponseWriter, WebUIHandler}; +use webui_protocol::WebUIProtocol; + +// ── Spiral parameters (match ssr-performance-showdown) ────────────────── + +const WRAPPER_WIDTH: f64 = 960.0; +const WRAPPER_HEIGHT: f64 = 720.0; +const CELL_SIZE: f64 = 10.0; +const ANGLE_STEP: f64 = 0.2; +const RADIUS_STEP: f64 = CELL_SIZE * 0.015; + +// ── In-memory response writer ─────────────────────────────────────────── + +struct MemoryWriter { + buf: String, +} + +impl MemoryWriter { + fn with_capacity(cap: usize) -> Self { + Self { + buf: String::with_capacity(cap), + } + } +} + +impl ResponseWriter for MemoryWriter { + fn write(&mut self, content: &str) -> webui_handler::Result<()> { + self.buf.push_str(content); + Ok(()) + } + + fn end(&mut self) -> webui_handler::Result<()> { + Ok(()) + } +} + +// ── Spiral tile computation (runs per-request) ────────────────────────── + +fn compute_tiles() -> Vec { + let center_x = WRAPPER_WIDTH / 2.0; + let center_y = WRAPPER_HEIGHT / 2.0; + let max_radius = WRAPPER_WIDTH.min(WRAPPER_HEIGHT) / 2.0; + + let mut angle: f64 = 0.0; + let mut radius: f64 = 0.0; + let mut tiles = Vec::with_capacity(2400); + + while radius < max_radius { + let x = center_x + angle.cos() * radius; + let y = center_y + angle.sin() * radius; + + if x >= 0.0 + && x <= WRAPPER_WIDTH - CELL_SIZE + && y >= 0.0 + && y <= WRAPPER_HEIGHT - CELL_SIZE + { + tiles.push(serde_json::json!({ + "left": format!("{:.2}px", x), + "top": format!("{:.2}px", y), + })); + } + + angle += ANGLE_STEP; + radius += RADIUS_STEP; + } + + tiles +} + +// ── Route handler ─────────────────────────────────────────────────────── + +async fn handle_index(protocol: web::Data) -> HttpResponse { + let state = serde_json::json!({ "tiles": compute_tiles() }); + + let mut writer = MemoryWriter::with_capacity(256 * 1024); + let mut handler = WebUIHandler::new(); + + if let Err(e) = handler.handle(&protocol, &state, &mut writer) { + return HttpResponse::InternalServerError().body(format!("Render error: {e}")); + } + + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(writer.buf) +} + +// ── Startup ───────────────────────────────────────────────────────────── + +fn main() -> Result<()> { + let port: u16 = std::env::args() + .position(|a| a == "--port") + .and_then(|i| std::env::args().nth(i + 1)) + .and_then(|v| v.parse().ok()) + .unwrap_or(3000); + + let protocol_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("dist/protocol.bin"); + let protocol = WebUIProtocol::from_protobuf_file(&protocol_path) + .with_context(|| format!("Failed to load {}", protocol_path.display()))?; + let protocol_data = web::Data::new(protocol); + + println!("Listening on http://localhost:{port}"); + + actix_web::rt::System::new() + .block_on(async move { + HttpServer::new(move || { + App::new() + .app_data(protocol_data.clone()) + .route("/", web::get().to(handle_index)) + .route("/index.html", web::get().to(handle_index)) + .default_service(web::route().to(|| async { + HttpResponse::NotFound().body("Not Found") + })) + }) + .bind(format!("0.0.0.0:{port}")) + .with_context(|| format!("Failed to bind to port {port}"))? + .run() + .await + .map_err(|e| anyhow::anyhow!("{e}")) + }) + .context("Server error")?; + + Ok(()) +} From ed5c9262469c6c83f2c122be9b99bb7a4b3fcbd3 Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Thu, 5 Mar 2026 12:23:45 -0800 Subject: [PATCH 6/8] chore: add integration examples to root workspace - Use glob pattern (crates/*) for crate members - Add rust and ssr-performance-showdown as workspace members - Inherit shared dependencies (serde_json, anyhow, actix-web) via workspace = true - Remove standalone [workspace] declarations from example Cargo.tomls Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 21 ++++++++++++++++++ Cargo.toml | 12 +++------- examples/integration/rust/Cargo.toml | 6 ++--- .../ssr-performance-showdown/Cargo.lock | 22 +++++++++---------- .../ssr-performance-showdown/Cargo.toml | 2 -- .../ssr-performance-showdown/README.md | 4 ++-- 6 files changed, 39 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffefc831..83263395 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1960,6 +1960,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ssr-performance-showdown" +version = "0.1.0" +dependencies = [ + "actix-web", + "anyhow", + "serde_json", + "webui-handler", + "webui-protocol", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2536,6 +2547,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "webui-rust-example" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde_json", + "webui-handler", + "webui-protocol", +] + [[package]] name = "webui-state" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index fedc4c3c..e914e0ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/integration/rust/Cargo.toml b/examples/integration/rust/Cargo.toml index 440d7c7c..662a3f1a 100644 --- a/examples/integration/rust/Cargo.toml +++ b/examples/integration/rust/Cargo.toml @@ -9,9 +9,7 @@ name = "webui-rust-example" path = "src/main.rs" [dependencies] -serde_json = "1.0" +serde_json = { workspace = true } webui-protocol = { path = "../../../crates/webui-protocol" } webui-handler = { path = "../../../crates/webui-handler" } -anyhow = "1.0" - -[workspace] +anyhow = { workspace = true } diff --git a/examples/integration/ssr-performance-showdown/Cargo.lock b/examples/integration/ssr-performance-showdown/Cargo.lock index 03a22c5e..3d6cc985 100644 --- a/examples/integration/ssr-performance-showdown/Cargo.lock +++ b/examples/integration/ssr-performance-showdown/Cargo.lock @@ -1279,6 +1279,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ssr-performance-showdown" +version = "0.1.0" +dependencies = [ + "actix-web", + "anyhow", + "serde_json", + "webui-handler", + "webui-protocol", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1590,17 +1601,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "webui-ssr-test" -version = "0.1.0" -dependencies = [ - "actix-web", - "anyhow", - "serde_json", - "webui-handler", - "webui-protocol", -] - [[package]] name = "webui-state" version = "0.1.0" diff --git a/examples/integration/ssr-performance-showdown/Cargo.toml b/examples/integration/ssr-performance-showdown/Cargo.toml index 9f1e7fbf..7d9afd37 100644 --- a/examples/integration/ssr-performance-showdown/Cargo.toml +++ b/examples/integration/ssr-performance-showdown/Cargo.toml @@ -14,5 +14,3 @@ webui-protocol = { path = "../../../crates/webui-protocol" } webui-handler = { path = "../../../crates/webui-handler" } actix-web = { workspace = true } anyhow = { workspace = true } - -[workspace] diff --git a/examples/integration/ssr-performance-showdown/README.md b/examples/integration/ssr-performance-showdown/README.md index 1426daaa..0f2d7640 100644 --- a/examples/integration/ssr-performance-showdown/README.md +++ b/examples/integration/ssr-performance-showdown/README.md @@ -11,8 +11,8 @@ as state to `WebUIHandler`, which streams the final HTML. ## Usage ```bash -# From the repository root — build the protocol first -cargo run -p webui-cli --release -- build examples/integration/ssr-performance-showdown/app --out examples/integration/ssr-performance-showdown/dist +# From this folder +cargo run -p webui-cli --release -- build app --out dist # Run the server frrm this location cargo run -p ssr-performance-showdown --release # listen on :3000 From 7bab0ec7ff89ff11ddb551525fbd6d64908855bb Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Thu, 5 Mar 2026 12:29:36 -0800 Subject: [PATCH 7/8] chore: add ssr-performance-showdown to xtask integration builds - Add entry in INTEGRATION_BUILDS for ssr-performance-showdown - Fix clippy warnings: use RangeInclusive::contains, avoid json! macro unwrap Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ssr-performance-showdown/src/main.rs | 289 +++++++++--------- xtask/src/build_examples.rs | 13 + 2 files changed, 162 insertions(+), 140 deletions(-) diff --git a/examples/integration/ssr-performance-showdown/src/main.rs b/examples/integration/ssr-performance-showdown/src/main.rs index 28e5043a..580541ed 100644 --- a/examples/integration/ssr-performance-showdown/src/main.rs +++ b/examples/integration/ssr-performance-showdown/src/main.rs @@ -1,140 +1,149 @@ -//! SSR Performance Test — WebUI Framework -//! -//! Rust server that loads a pre-built protocol.bin, computes a spiral -//! pattern of tiles per-request, and renders them through the WebUI -//! handler — comparable to the `fastify-html` entry in the -//! ssr-performance-showdown benchmark. -//! -//! Prerequisites: -//! cargo run -p webui-cli -- build app --out dist -//! -//! Usage: -//! cargo run # listen on :3000 -//! cargo run -- --port 3001 # custom port - -use actix_web::{web, App, HttpResponse, HttpServer}; -use anyhow::{Context, Result}; -use std::path::Path; -use webui_handler::{ResponseWriter, WebUIHandler}; -use webui_protocol::WebUIProtocol; - -// ── Spiral parameters (match ssr-performance-showdown) ────────────────── - -const WRAPPER_WIDTH: f64 = 960.0; -const WRAPPER_HEIGHT: f64 = 720.0; -const CELL_SIZE: f64 = 10.0; -const ANGLE_STEP: f64 = 0.2; -const RADIUS_STEP: f64 = CELL_SIZE * 0.015; - -// ── In-memory response writer ─────────────────────────────────────────── - -struct MemoryWriter { - buf: String, -} - -impl MemoryWriter { - fn with_capacity(cap: usize) -> Self { - Self { - buf: String::with_capacity(cap), - } - } -} - -impl ResponseWriter for MemoryWriter { - fn write(&mut self, content: &str) -> webui_handler::Result<()> { - self.buf.push_str(content); - Ok(()) - } - - fn end(&mut self) -> webui_handler::Result<()> { - Ok(()) - } -} - -// ── Spiral tile computation (runs per-request) ────────────────────────── - -fn compute_tiles() -> Vec { - let center_x = WRAPPER_WIDTH / 2.0; - let center_y = WRAPPER_HEIGHT / 2.0; - let max_radius = WRAPPER_WIDTH.min(WRAPPER_HEIGHT) / 2.0; - - let mut angle: f64 = 0.0; - let mut radius: f64 = 0.0; - let mut tiles = Vec::with_capacity(2400); - - while radius < max_radius { - let x = center_x + angle.cos() * radius; - let y = center_y + angle.sin() * radius; - - if x >= 0.0 - && x <= WRAPPER_WIDTH - CELL_SIZE - && y >= 0.0 - && y <= WRAPPER_HEIGHT - CELL_SIZE - { - tiles.push(serde_json::json!({ - "left": format!("{:.2}px", x), - "top": format!("{:.2}px", y), - })); - } - - angle += ANGLE_STEP; - radius += RADIUS_STEP; - } - - tiles -} - -// ── Route handler ─────────────────────────────────────────────────────── - -async fn handle_index(protocol: web::Data) -> HttpResponse { - let state = serde_json::json!({ "tiles": compute_tiles() }); - - let mut writer = MemoryWriter::with_capacity(256 * 1024); - let mut handler = WebUIHandler::new(); - - if let Err(e) = handler.handle(&protocol, &state, &mut writer) { - return HttpResponse::InternalServerError().body(format!("Render error: {e}")); - } - - HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(writer.buf) -} - -// ── Startup ───────────────────────────────────────────────────────────── - -fn main() -> Result<()> { - let port: u16 = std::env::args() - .position(|a| a == "--port") - .and_then(|i| std::env::args().nth(i + 1)) - .and_then(|v| v.parse().ok()) - .unwrap_or(3000); - - let protocol_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("dist/protocol.bin"); - let protocol = WebUIProtocol::from_protobuf_file(&protocol_path) - .with_context(|| format!("Failed to load {}", protocol_path.display()))?; - let protocol_data = web::Data::new(protocol); - - println!("Listening on http://localhost:{port}"); - - actix_web::rt::System::new() - .block_on(async move { - HttpServer::new(move || { - App::new() - .app_data(protocol_data.clone()) - .route("/", web::get().to(handle_index)) - .route("/index.html", web::get().to(handle_index)) - .default_service(web::route().to(|| async { - HttpResponse::NotFound().body("Not Found") - })) - }) - .bind(format!("0.0.0.0:{port}")) - .with_context(|| format!("Failed to bind to port {port}"))? - .run() - .await - .map_err(|e| anyhow::anyhow!("{e}")) - }) - .context("Server error")?; - - Ok(()) -} +//! SSR Performance Test — WebUI Framework +//! +//! Rust server that loads a pre-built protocol.bin, computes a spiral +//! pattern of tiles per-request, and renders them through the WebUI +//! handler — comparable to the `fastify-html` entry in the +//! ssr-performance-showdown benchmark. +//! +//! Prerequisites: +//! cargo run -p webui-cli -- build app --out dist +//! +//! Usage: +//! cargo run # listen on :3000 +//! cargo run -- --port 3001 # custom port + +use actix_web::{web, App, HttpResponse, HttpServer}; +use anyhow::{Context, Result}; +use std::path::Path; +use webui_handler::{ResponseWriter, WebUIHandler}; +use webui_protocol::WebUIProtocol; + +// ── Spiral parameters (match ssr-performance-showdown) ────────────────── + +const WRAPPER_WIDTH: f64 = 960.0; +const WRAPPER_HEIGHT: f64 = 720.0; +const CELL_SIZE: f64 = 10.0; +const ANGLE_STEP: f64 = 0.2; +const RADIUS_STEP: f64 = CELL_SIZE * 0.015; + +// ── In-memory response writer ─────────────────────────────────────────── + +struct MemoryWriter { + buf: String, +} + +impl MemoryWriter { + fn with_capacity(cap: usize) -> Self { + Self { + buf: String::with_capacity(cap), + } + } +} + +impl ResponseWriter for MemoryWriter { + fn write(&mut self, content: &str) -> webui_handler::Result<()> { + self.buf.push_str(content); + Ok(()) + } + + fn end(&mut self) -> webui_handler::Result<()> { + Ok(()) + } +} + +// ── Spiral tile computation (runs per-request) ────────────────────────── + +fn compute_tiles() -> Vec { + let center_x = WRAPPER_WIDTH / 2.0; + let center_y = WRAPPER_HEIGHT / 2.0; + let max_radius = WRAPPER_WIDTH.min(WRAPPER_HEIGHT) / 2.0; + + let mut angle: f64 = 0.0; + let mut radius: f64 = 0.0; + let mut tiles = Vec::with_capacity(2400); + + while radius < max_radius { + let x = center_x + angle.cos() * radius; + let y = center_y + angle.sin() * radius; + + if (0.0..=WRAPPER_WIDTH - CELL_SIZE).contains(&x) + && (0.0..=WRAPPER_HEIGHT - CELL_SIZE).contains(&y) + { + tiles.push(serde_json::Value::Object({ + let mut m = serde_json::Map::with_capacity(2); + m.insert( + "left".to_owned(), + serde_json::Value::String(format!("{x:.2}px")), + ); + m.insert( + "top".to_owned(), + serde_json::Value::String(format!("{y:.2}px")), + ); + m + })); + } + + angle += ANGLE_STEP; + radius += RADIUS_STEP; + } + + tiles +} + +// ── Route handler ─────────────────────────────────────────────────────── + +async fn handle_index(protocol: web::Data) -> HttpResponse { + let tiles_value = serde_json::Value::Array(compute_tiles()); + let mut state_map = serde_json::Map::with_capacity(1); + state_map.insert("tiles".to_owned(), tiles_value); + let state = serde_json::Value::Object(state_map); + + let mut writer = MemoryWriter::with_capacity(256 * 1024); + let mut handler = WebUIHandler::new(); + + if let Err(e) = handler.handle(&protocol, &state, &mut writer) { + return HttpResponse::InternalServerError().body(format!("Render error: {e}")); + } + + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(writer.buf) +} + +// ── Startup ───────────────────────────────────────────────────────────── + +fn main() -> Result<()> { + let port: u16 = std::env::args() + .position(|a| a == "--port") + .and_then(|i| std::env::args().nth(i + 1)) + .and_then(|v| v.parse().ok()) + .unwrap_or(3000); + + let protocol_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("dist/protocol.bin"); + let protocol = WebUIProtocol::from_protobuf_file(&protocol_path) + .with_context(|| format!("Failed to load {}", protocol_path.display()))?; + let protocol_data = web::Data::new(protocol); + + println!("Listening on http://localhost:{port}"); + + actix_web::rt::System::new() + .block_on(async move { + HttpServer::new(move || { + App::new() + .app_data(protocol_data.clone()) + .route("/", web::get().to(handle_index)) + .route("/index.html", web::get().to(handle_index)) + .default_service( + web::route().to(|| async { HttpResponse::NotFound().body("Not Found") }), + ) + }) + .bind(format!("0.0.0.0:{port}")) + .with_context(|| format!("Failed to bind to port {port}"))? + .run() + .await + .map_err(|e| anyhow::anyhow!("{e}")) + }) + .context("Server error")?; + + Ok(()) +} diff --git a/xtask/src/build_examples.rs b/xtask/src/build_examples.rs index 6a6ce175..7c0207d5 100644 --- a/xtask/src/build_examples.rs +++ b/xtask/src/build_examples.rs @@ -50,6 +50,19 @@ pub const INTEGRATION_BUILDS: &[IntegrationBuild] = &[ }], run_commands: &[], }, + IntegrationBuild { + name: "ssr-performance-showdown", + commands: &[BuildCommand { + cmd: "cargo", + args: &[ + "check", + "--manifest-path", + "examples/integration/ssr-performance-showdown/Cargo.toml", + ], + cwd: None, + }], + run_commands: &[], + }, ]; pub fn run_integration_builds() -> Result<(), String> { From c58c716232042cd6d24486def89f99065b3a3aa6 Mon Sep 17 00:00:00 2001 From: Mohamed Mansour Date: Thu, 5 Mar 2026 12:34:53 -0800 Subject: [PATCH 8/8] chore: clarify quality-gate skill triggers before commit/push Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/quality-gate/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/skills/quality-gate/SKILL.md b/.github/skills/quality-gate/SKILL.md index d03d8adc..a3538fc2 100644 --- a/.github/skills/quality-gate/SKILL.md +++ b/.github/skills/quality-gate/SKILL.md @@ -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