Gunnar

nordisk/facett

.nornir/app-guide.md

# facett-app — the application-framework — guide

> **Class:** `guide` · audience: a techie composing an app from facett components.
> The *what* is in [`app-design.md`](app-design.md); the *why* in [`app-reasoning.md`](app-reasoning.md).
> This file is the workflow.

## f0 — the end-to-end workflow: model → router → shell → demo

Building a navigable facett app is four moves, in order:

1. **Declare a `NavModel`** — the tree every surface reads from.
2. **Pick a shell** — `NavShell` (body text per route) or `AppShell` (real `Facet` panes per
   route, plus menubar/palette).
3. **Drive it headlessly** — `navigate`/`select`/`invoke` calls, asserted via `state_json()`,
   with zero window needed.
4. **Wire it into `facett-demo`** — a deck tab so it renders in the live app and the web demo,
   and shows up in the mega-matrix + robot-UI coverage.

```rust
use facett_app::{NavItem, NavModel, NavShell};

let model = NavModel::new([
    NavItem::new("home", "Home").icon("🏠"),
    NavItem::new("data", "Data").icon("🗃").children([
        NavItem::new("data.grid", "Grid").route("grid").badge("12k"),
        NavItem::new("data.table", "Table").route("table"),
    ]),
    NavItem::new("settings", "Settings").icon("⚙"),
    NavItem::new("admin", "Admin (locked)").disabled(),
]);

let mut shell = NavShell::new("nav", model)
    .with_body("home", "Welcome — pick something on the left.")
    .with_body("grid", "The Arrow grid would render here.")
    .with_body("table", "The data table would render here.");

// Headless drive, no ui() call needed:
shell.navigate("table");
assert_eq!(shell.current_route(), "table");
assert_eq!(shell.breadcrumb(), vec!["Data", "Table"]);
```

## f1 — `NavModel` + `Router`: building and querying the tree

Build the tree once with `NavModel::new(items)`; `NavItem::new(id, label)` plus the builders
`.icon(...)`, `.route(...)`, `.badge(...)`, `.disabled()`, `.child(item)` / `.children(items)`
compose it. A leaf with no `.route(...)` navigates to its own `id`.

Query it directly when you need the tree without a shell wrapping it:

```rust
model.first_route();               // Some("home") — router start
model.find_route("data.table");    // the NavItem targeting that route
model.breadcrumb_items("table");   // [("Data", "data"), ("Table", "table")] — clickable trail
model.routes();                    // every enabled navigable route id, in order
model.state_json();                // the whole tree as json (golden-file / robot-UI friendly)
```

`Router` is usable standalone if you don't want a shell at all — e.g. driving a bare
`FacetDeck::set_active_by_title` by hand:

```rust
use facett_app::Router;
let mut r = Router::new("home");
r.navigate("data");
r.navigate("table");
r.back();                 // → "data"
assert!(r.can_forward());
r.forward();              // → "table"
r.to_hash();              // "#table" — write to window.location.hash on wasm
Router::route_from_hash("#data%20table"); // Some("data table") — parse it back
```

## f2 — choosing a shell, and wiring the extra surfaces

| you need… | use |
|---|---|
| routed body **text** only, a rail + breadcrumb | `NavShell` |
| routed **real components** (grids, charts, 3D views) per route | `AppShell::with_pane(route, Box::new(component))` |
| a menubar over the same nav tree | `AppShell::with_menubar()` (or `facett_menu::menu_bar(ui, &nav_commands(&model))` directly) |
| a Ctrl-K fuzzy "go to…" palette | `AppShell::with_command_palette()` (or a standalone `NavPalette`) |
| two components side by side under one route | `SplitPane::new(title, Box::new(a), Box::new(b))`, mounted as one `AppShell` pane |
| a per-route control (e.g. a settings dropdown) | `NavShell::with_route_dropdown(route, label, Dropdown::new(...).options([...]))` |

`AppShell` composition, mirroring `facett-demo`'s `shell` tab
(`facett-demo/src/lib.rs:395-421`):

```rust
use facett_app::{AppShell, NavItem, NavModel, SplitPane};

let model = NavModel::new([
    NavItem::new("home", "Home").icon("🏠"),
    NavItem::new("models", "3D Models").icon("🧊").children([
        NavItem::new("m.a", "Avocado").route("avocado"),
        NavItem::new("m.b", "Skull").route("skull"),
        NavItem::new("m.c", "Compare").route("compare"),
    ]),
]);

let compare = SplitPane::new("compare", Box::new(view_a), Box::new(view_b));

let mut shell = AppShell::new("shell", model)
    .with_body("home", "Pick a model on the left.")
    .with_pane("avocado", Box::new(view_a_owned))
    .with_pane("skull", Box::new(view_b_owned))
    .with_pane("compare", Box::new(compare))
    .with_command_palette()
    .with_menubar();

shell.navigate("compare");
assert!(shell.active_pane().is_some());
```

Overlays (`Modal`, `Toasts`, `Popover`) and input widgets (`Tree`, `TabView`, `ColorPicker`,
`DatePicker`, `Accordion`, `Stepper`, `Pagination`) are drawn inside whatever `Facet::ui` hosts
them — they are not shells themselves. The pattern is always: keep the widget as a struct
field, call its `.show(ui)` (or `.ui(ui)` for `Facet`s) each frame, and read the `Option<T>`
it returns for "did something change this frame":

```rust
// Inside your Facet::ui:
if let Some(action) = self.modal.show(ui) {           // "ok" / "cancel" / "dismiss"
    if action == "ok" { self.toasts.push(Level::Success, "Saved."); }
}
self.toasts.show(ui);   // stacked top-right, self-expiring
```

## f3 — running the demo, native and web

```sh
cargo run -p facett-demo                 # the live native window (the nav/shell/overlay/widgets
                                          # tabs sit alongside every other component)
cargo test -p facett-app                 # NavModel/Router/shell/widget unit tests (facett-app/src/tests.rs)
cargo test -p facett-demo                # self-check + snapshot + mega matrix + robot drive
cargo test -p facett-demo --test robot_demo -- --nocapture   # drive the whole app headlessly
```

Building and serving the web (WASM) build:

```sh
cargo xtask demo-wasm                    # → ./dist (index.html + JS glue + .wasm)
(cd dist && python3 -m http.server 8080) # → http://localhost:8080
cargo xtask deploy-pages                 # build, then publish dist/ to the `pages` branch
```

`#TabName` in the URL deep-links to a tab on load (`facett-demo/src/main.rs:73` reads
`window.location().hash()`); from inside the running app, `DemoState::set_active_tab(title)`
does the same thing programmatically. The wasm build drops `region-clip`/`map-wgpu`/
`map3d-wgpu` (native-only), so those surfaces degrade gracefully rather than panicking — see
`app-for-idiots.md` if you're checking the deployed site behaves the same as native.

To read what actually ran in a **shipped** wasm bundle (no filesystem to dump to), open the
browser devtools console on the deployed page and call:

```js
JSON.parse(window.__facett_state())
```

This returns the same `DemoState::state_json()` a native test asserts on — every tab, every
control, the `nav`/`shell`/`overlay`/`widgets` state included.