Skip to content

Qirava DMS docs

Studio UI architecture: SSR, components, shell

Studio's UI is server-side rendered with Quill — the same view layer, theming, and design components used by the public website and docs. There is no client-side framework, no bundler, and no static asset server: every page is a complete HTML document assembled in Rust from view nodes and shipped whole, with critical CSS inlined into the <head>. The StudioShell builds that document — a sticky header (brand, theme toggle, user@role, logout), a left sidebar nav filtered to your role, and a main content column — and page handlers fill the content with qquill-design components (Stat, Alert) plus small zero-dependency table/card/form helpers. A no-flicker theme island sets dark/light before first paint. This page explains the SSR pipeline, the shell anatomy, the component and CSS strategy, and the theme island.

Server-side rendering with Quill#

Studio is pure SSR. A page handler builds a tree of view Nodes with qquill-view (el for elements, text for escaped text, raw for trusted markup, fragment to group), renders it to bytes with render_bytes, and returns it as an HTML response. All Studio routes are declared OutputKind::Html. There is no hydration of page content — only one small theme island runs on the client (see below).

qquill-view
The node builder. el("div").class(..).attr(..).child(..); text(..) escapes; raw(..) injects trusted strings verbatim; fragment([..]) groups siblings (used to prepend <!doctype html>).
render_bytes / render
Serialize a Node tree to HTML bytes. The renderer escapes all text and attribute values — every dynamic value (usernames, flash messages) goes through text(..) and is escaped.
OutputKind::Html
Every Studio route is served as Html; the handlers frame headers (Set-Cookie / Location / X-Status) via the __headers seam.

The StudioShell#

StudioShell builds the full <html> chrome for every authenticated page via document(user, current, content, extra_css). It owns three regions: a sticky header, a left sidebar nav, and the main content column. The unauthenticated login page uses auth_document(title, content, extra_css) instead, which has the brand and centered card but NO session chrome.

┌──────────────────────────────────────────────────────────┐
│ .qs-header (sticky)                                        │
│  [brand lockup]            [Theme] [ada@custodian][Log out]│
├───────────────┬──────────────────────────────────────────┤
│ .qs-sidebar   │ <main id="main"> .qs-content              │
│  Dashboard    │   <h1>…</h1>                              │
│  Users & RBAC │   (optional flash/error alert)           │
│  Databases    │   stat grid / cards / tables / forms     │
│  Workers&Apps │                                          │
│  Governance   │                                          │
│ (RBAC-filtered)│                                          │
└───────────────┴──────────────────────────────────────────┘
grid-template-columns: 220px 1fr
StudioShell::document layout
  • Header brand: the full qbrand wordmark lockup, recolored to currentColor so it adapts to the theme with no second asset (ink on light, light on dark) — the same single brand source as the website.
  • Header right: the theme toggle island, the user@role badge, and a Logout <form> POST carrying the session-bound CSRF token.
  • Sidebar: the five Screen entries in nav order, FILTERED by screen.can_access(role); the current screen is marked aria-current="page".
  • Main: <main id="main"> with the page's content node.

The StudioUser passed to the shell carries user, role, and the raw session_token. The token is used only to derive the per-request CSRF token for embedded forms (user.csrf()) — it is NEVER rendered into the page.

The <head>: theme boot, then CSS#

The shared <head> is assembled in a specific order that matters for correctness. It carries charset and viewport meta, a robots=noindex meta (the admin app must not be indexed), the title, the favicon, the no-flicker theme boot script, and then a single inlined <style> block. The CSS order is: theme variable blocks (default_theme_set().to_css()), then base_css(), then — for the authenticated shell only — chrome_css(), then the page's component CSS (extra_css).

text
<head>
  <meta charset=utf-8> <meta name=viewport …> <meta name=robots content=noindex>
  <title>… — Qirava Studio</title>
  <link rel=icon type=image/svg+xml href="data:image/svg+xml,…">  (inlined qbrand icon)
  <script>…theme boot…</script>   ← runs BEFORE the stylesheet (no flicker)
  <style>theme-vars + base_css [+ chrome_css] + page CSS</style>
</head>
base_css()
The shared, chrome-free layout: body defaults plus the login card and the form/input/button primitives reused on BOTH the authenticated shell and the login document. Carries NONE of the session-chrome rules.
chrome_css()
The authenticated-only session chrome: sticky header, sidebar nav, content grid, plus the dashboard stat grid and the table/card primitives. Inlined ONLY by document(); the login document omits it entirely.
Favicon
The qbrand ICON inlined as an image/svg+xml data URI (percent-encoded for an HTML attribute). Studio has no static file server, so the mark is embedded directly — same single source as the website's /favicon.svg.

Design components and the per-page CSS pattern#

Studio renders structured UI with qquill-design components and a handful of small zero-dependency helpers. The components follow a render-then-attach-CSS pattern: you build the component, call .render() to get a rendered handle, append its companion CSS (handle.style().to_css()) to the page's CSS string, and append its node (handle.into_node()) to the content. The shell then inlines that accumulated page CSS after its own.

rust
// Dashboard stat cards: render each Stat, collect its CSS, then its node.
use qquill_design::Stat;
let stat = Stat::new("stat-users", "Users", counts.users.to_string()).render();
css.push_str(&stat.style().to_css()); // companion CSS → page CSS
grid = grid.child(stat.into_node());  // node → content
// Finally: page(&shell, &user, screen, content, &css) inlines `css` last.
  • Stat — the dashboard stat cards (Databases, Tables, Users, Workers, Active sessions).
  • Alert (Severity: Info, Success, Danger) — flash/error banners, the 403 message, and the login form's messages.
  • table(headers, rows) — a zero-dep <table class="qs-table"> helper; every cell is text(..) so row data (usernames, grants) is escaped.
  • card(title, body) — a <section class="qs-card"> with an <h2> title.
  • field / select_field / action_form — the form primitives; action_form always embeds the session CSRF hidden input and POSTs to a studio.action.* route.

The flash banner pattern threads through every page: page_head reads ?error= (rendered as a Danger alert) or ?flash= (rendered as a Success alert) from the query string and prepends it under the <h1>. This is what surfaces the Post/Redirect/Get outcome of a mutation.

The no-flicker theme island#

Dark/light theming is var-based: every color in base_css and chrome_css is a CSS custom property (e.g. var(--qq-color-bg), var(--qq-color-fg), var(--qq-color-brand)) defined by the qquill-theme variable blocks. The theme is chosen before first paint by a boot script emitted in the <head> ahead of the stylesheet, so there is no flash of the wrong theme. The boot is configured with BootConfig::new(ThemeMode::Light) as the default.

The header's Theme button is the one hydratable island on the page. SSR renders it as an accessible static <button>; at runtime the island (trigger "load") flips data-q-theme on <html>. The button carries the island attributes that wire it up.

html
<button type="button" class="qs-themetoggle"
        data-q-island="studio-theme" data-q-kind="theme"
        data-q-trigger="load" data-q-theme-toggle="true"
        aria-label="Toggle dark / light theme">Theme</button>
Pre-paint boot
qquill_theme::boot_script_tag(&boot) runs before the <style>, setting data-q-theme on <html> so the correct theme is applied on the first frame.
Theme island
The only client-side hydration in Studio. SSR is accessible without it; the island just enables the toggle to flip the theme at runtime.
Reference shell
The whole approach mirrors qquill-docs::DocShell (the dogfood reference): theme boot before critical CSS, all colors theme-var based.