Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

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; the why in 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.
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:

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:

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):

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 Facets) each frame, and read the Option<T> it returns for "did something change this frame":

// 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

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:

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:

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.