Skip to content

Latest commit

 

History

History
48 lines (31 loc) · 4.14 KB

File metadata and controls

48 lines (31 loc) · 4.14 KB

Frontend

No framework, no build step, no bundler. Server-rendered MiniJinja templates plus one vanilla-JS file per page. This page covers the conventions that keep that maintainable — and the two hard-won rules (XSS discipline, animation fill-modes) that exist because breaking them has bitten before.

Layout

Piece Files Notes
Templates templates/*.html All extend base.html (head, CSRF <meta>, auth-redirect script, base.js). Rendered by MiniJinja; url_for('static', filename=…) resolves static assets.
Page JS static/js/{index,settings,view,images,explore}.js One file per page, loaded by that page's template. No modules, no globals shared across pages.
Shared JS static/js/base.js authFetch (adds the X-CSRFToken header from the meta tag) and formatFileSize. Every mutating call goes through authFetch.
CSS static/css/base.css + one file per page GitHub-dark palette in raw hex (#0d1117 bg, #161b22 panels, #30363d borders, #58a6ff accent). No variables layer — match the existing literals.

Search itself bypasses the API: the server injects MEILI_HOST and MEILI_API_KEY into the page (see templates/index.html scripts block) and index.js queries Meilisearch directly with the official JS client.

XSS discipline (non-negotiable)

Filenames and file contents are attacker-influenced (a downloaded file, a torrent, a ZIM archive). v0.3.18 fixed a stored XSS where these were interpolated into innerHTML unescaped — don't reintroduce it:

  • Every untrusted string that meets innerHTML goes through the local escapeHtml helper first — in both text and attribute positions.
  • Meilisearch match highlights come back as literal <em> tags. The pattern is: escape everything, then restore only the highlight tags (escapeHtml(s).replace(/&lt;em&gt;/g,'<em>')…) — see renderResults in index.js.
  • New render paths should prefer textContent/createElement where practical.

Motion & animation rules

The sbx- keyframes in base.css (sbxFadeUp, sbxFloat, sbxSheen) power the entrance choreography, and per-page polish blocks apply them. Two rules:

  1. Entrance animations use animation-fill-mode: backwards, never both/forwards. A filled final keyframe keeps applying via the animation cascade origin, which outranks all normal declarations — it permanently kills :hover/:active transforms on the same element. This has bitten this codebase twice; backwards keeps items hidden through their stagger delay and releases everything when the animation ends.
  2. All decorative motion must die under prefers-reduced-motion. base.css has a global kill switch; keep new looping/entrance animations inside @media (prefers-reduced-motion: no-preference) anyway so intent is explicit.

The landing-page demo replica (keep it in sync!)

docs/index.html is the public landing page and it contains a working, exact replica of the app UI — every sb--prefixed class mirrors a real selector from static/css/*, and its JS mirrors the real render templates (result items, image rail, AI summary with citations, vault PIN gate, Enter-to-search with the syntax-error modal).

Two things follow:

  • When you change the app's UI, update the replica — it's advertised as "the real interface," and it also ships inside the binary: src/assets.rs embeds it and the app serves it at /demo-site for the in-app demo page (a sandboxed allow-scripts iframe).
  • The page must stay fully self-contained: no CDN scripts, fonts, or external resource loads (outbound href links are fine). It runs with an opaque origin in-app, so no localStorage, no history.pushState, and clipboard calls need promise .catch handling.

Adding a page (checklist)

  1. templates/foo.html extending base.html; register the route in src/routes/pages.rs.
  2. static/css/foo.css + static/js/foo.js, linked with a ?v=1 cache-buster.
  3. Escape everything untrusted; use authFetch for mutations.
  4. In debug builds: restart the app to pick up template changes (MiniJinja caches at startup); CSS/JS only need a refresh.

← Back to the dev docs