VelquView
Phase 1 accepted · Tailwind v0 profile · fully testable

The Tailwind renderer for
native applications.

VelquView lays out and rasterizes HTML + Tailwind CSS in pure Rust — taffy for layout, fontdue for glyphs, softbuffer for pixels. No browser, no webview, no DOM runtime. Same markup, measured pixel-for-pixel against Chromium.

$ git clone https://github.com/ther12k/velqu-view-starter && cd velqu-view-starter && ./dev.sh
Built on taffyfontduewinitsoftbuffer
host · Rust
let mut view = VelquView::new();
view.enable_tailwind();
view.load_html(r#"<main class="flex h-full">…"#)?;

let frame = view.render(Viewport::try_new(1280, 800, 1.0)?)?;
// frame.pixels() → RGBA, deterministic
ui · HTML + Tailwind + vx-state
<main vx-state="{ current: 'overview', }">
  <aside class="w-56 h-full bg-slate-900 p-3">
    <div @click="current = 'overview'"
      :class="current === 'overview'
        ? 'bg-teal-600 text-white …'
        : 'text-slate-400 …'">Overview</div>
  </aside>
</main>
VelquView · Telemetry Dashboard — rendered natively, not in a browser
Telemetry dashboard rendered by VelquView

A real VelquView frame — winit window, taffy layout, fontdue glyphs, softbuffer presentation.

0
browsers, webviews, DOM runtimes
≤ 4px
max layout drift vs Chromium, 4 sample sets
3
regression gates — profile, geometry, raster
29
integration tests in the starter app
WHY VELQUVIEW

A renderer you can put in a test suite.

Not a webview with an API bolted on. A deterministic engine whose outputs can be pinned, diffed and gated in CI.

Native Tailwind v0 profile

The spacing scale, 21 color families, flex, grid, borders, radius, typography — compiled by the engine, not a CDN. Every class outside the profile is a compile-time diagnostic, never a silent drop.

Reactive documents

vx-state + @click + :class + vx-show — a small state machine evaluates bindings each frame and mutates only what changed. Tabs, modals, dashboards.

Deterministic rasterizer

Same document, same viewport → same RGBA bytes, every time. Frame digests are pinned in tests; a rendering change is a build failure, not a vibe.

Diagnostics gate

Unsupported utilities report themselves: tailwind diagnostic: class "shadow-md" …. Port real-world markup and watch the compiler catch every assumption.

Differential test suite

A 3-gate external suite compares against real Chromium with real compiled Tailwind: utility contracts, section geometry vs reviewed baselines, raster digests. Regressions and unexpected improvements both fail.

Host-owned data

The UI never claims durability. The starter's ops app observes public events, validates, persists atomically and reloads — the renderer renders, the host decides.

SIDE BY SIDE

Same file. Two engines. Measured.

Four sample sets — a login card, a component stack, a chat UI, and a full admin dashboard — each rendered by VelquView and by Chromium with real Tailwind v3.4. Geometry measured, not eyeballed.

Remaining known deltas are paint-level (glyph weight, placeholder color, no emoji) — documented in FINDINGS.md, on the post-acceptance engine list.

DOCUMENTATION

Everything between clone and a native frame.

The public surface is small on purpose. This is the whole model — plus the sharp edges we documented while building four sample apps on it.

Quick start§

1

Clone the starter

shell
git clone https://github.com/ther12k/velqu-view-starter
cd velqu-view-starter
./dev.sh  # pinned toolchain, hot reload
2

Write HTML + Tailwind

app/index.html
<body class="bg-slate-950">
  <main class="flex h-full">
    …your UI, v0 utilities…
  </main>
</body>
3

Ship the release binary

shell
cargo build --release --locked
./target/release/velqu-view-starter \
  --app-dir app --size 1440x900

The starter pins two revisions separately: the runtime crate set (Cargo.lock) and the dev tool (dev.sh → velqu-lab). They move only together, deliberately.

API surface§

One type, one loop. The host owns the window (winit), calls the view, and presents the frame.

document
view.load_html(&html)?;
view.load_document(DocumentSource::File(path))?;
view.load_stylesheet(id, css)?;
view.reload_bundle(html)?;  // state-preserving
events → state → frame
let batch = view.take_events();
view.pump_reactive(&batch);
let frame = view.render(viewport)?;
// hit_test → pointer_press / release
text input
view.set_focus(node_id)?;
view.insert_text("typed");
let value = view.control_value(node_id)?;
view.wheel(dx, dy);  // scroll containers
inspection & gates
view.tailwind_diagnostics();   // Vec<String>
view.reactive_diagnostics();
view.layout_facts();  // geometry for tests
view.reactive_plan();  // binding graph

Persistence is not in the API on purpose: the host observes Click/ValueChanged events, validates, writes storage atomically, then calls reload_bundle — selection and scroll survive.

Reactive documents§

vx-state grammar — strict, one pair per line, trailing commas
<main vx-state="{
  current: 'overview',
  range: '7d',
  modal: '',
}">
  <div @click="modal = 'export'">Open</div>
  <div vx-show="modal === 'export'">…dialog…</div>
</main>
Sharp edges, learned the hard way:
  • :class replaces the entire class attribute — every branch must carry the full class list, or your layout classes vanish when the binding applies.
  • No {{ }} text interpolation — bake values per state and switch with vx-show, or duplicate the node.
  • Inline elements (span) inside flex containers flow as text — use block leaves (div) for flex items.
  • Declaration order follows class order: write m-0 mt-6, never the reverse, or the shorthand silently wins.

Diagnostics§

Every class outside the profile reports itself at load — port real-world markup and the compiler lists every assumption you made:

stdout
tailwind diagnostic: class "shadow-md": utility is outside the renderer profile v0
tailwind diagnostic: class "ml-auto": margin: auto is deferred in profile v0
tailwind diagnostic: class "border-b": unknown border utility "border-b"
reactive diagnostic: node 7 vx-show: expression has an unterminated string literal

In the comparison suite this is a gate: sample pages must compile with zero diagnostics before any render is measured.

Profile v0 reference§

In the v0 profile

  • ✓Flow & grid layout, gap, justify, items, self
  • ✓4px spacing scale · w/h · min/max sizes · max-w breakpoints
  • ✓21 color families × 11 shades, text/bg/border
  • ✓Border, radius scale, overflow & container scroll
  • ✓Typography scale + weights, text alignment, whitespace
  • ✓Text inputs & textareas, focus, IME, hit-testing

Outside v0 (diagnosed, not silent)

  • ×Shadows, gradients, opacity, transforms
  • ×Responsive variants & media queries
  • ×Positioning (absolute/fixed/sticky) · z-index
  • ×Per-side borders, tracking, decorations
  • ×Transitions & animations
  • →Author CSS covers interaction paint (:hover, cursor)

Sample apps§

Six runnable apps live in the velqu-showcase repo — one command each:

run any sample
./target/release/velqu-view-starter --app-dir <sample> --data-dir /tmp/vv --size 1440x900
# <sample> = live/        17-section component showcase (HyperUI ports)
#          dash/        telemetry dashboard — tabs, KPI ranges, takeover modal
#          login/ ab/ chat/ windmill/   the four A/B comparison sets

The window title comes from the document's <title>; each sample is a plain directory — index.html plus any *.css, loaded in sorted cascade order.

Runner CLI§

FlagWhat it does
--app-dir DIRLoad a directory app (index.html + sorted *.css). Title comes from the document.
--data-dir DIRWhere the host persists its data (ops app: records.json under flock).
--size WxH · --scale FLogical window size and DPI scale for the first presentation.
--headless --out PRender offscreen and write a PNG; no window. The CI path.
--frames NHeadless: render N frames and verify byte determinism.
--exit-after-ms NWindow mode: auto-close (smoke tests, automation).
--measurePrint launch→first-frame and uptime stats.

Known limits§

Stated plainly, because the measurements say so — these are engine-level items on the post-acceptance list, frozen until the supervised operator session:

Glyph weight

fontdue rasterizes plain coverage; Chromium's FreeType stem-darkens. Same font renders lighter in velqu — measured, zoomed, documented.

One typeface

A single bundled DejaVu face. No webfonts, no italic/mono variants, no emoji coverage — missing glyphs simply don't render.

Flat by profile

No shadows, gradients or opacity in v0 — depth comes from tonal steps and borders, which dark UIs do beautifully.