clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/prf-provision-custody
lofi / website / src / pages / index.tsx
index.tsx 18.0K
1import type { ReactNode } from "react";2import { useState } from "react";3import Link from "@docusaurus/Link";4import useDocusaurusContext from "@docusaurus/useDocusaurusContext";5import useBaseUrl from "@docusaurus/useBaseUrl";6import Layout from "@theme/Layout";7import SyncDemo from "../components/SyncDemo";8import Leaf from "../components/Leaf";910import "../css/landing.css";1112const CREATE_CMD = "deno run -A jsr:@nzip/lofi/create my-app";1314function CopyCmd(): ReactNode {15 const [label, setLabel] = useState("Copy");16 const copy = async () => {17 try {18 await navigator.clipboard.writeText(CREATE_CMD);19 setLabel("Copied");20 setTimeout(() => setLabel("Copy"), 1600);21 } catch {22 setLabel("Ctrl+C");23 }24 };25 return (26 <div className="cmd">27 <code>28 <b>$&nbsp;</b>29 {CREATE_CMD}30 </code>31 <button type="button" onClick={copy}>{label}</button>32 </div>33 );34}3536const FEATURES: Array<{ title: string; body: ReactNode }> = [37 {38 title: "Local-first data",39 body:40 "The UI reads from durable local storage instead of waiting on the network. Storage failures are surfaced, never silently swapped for memory.",41 },42 {43 title: "Identity from first launch",44 body:45 "Each user begins with a private on-device account, even when the app has no sync service configured.",46 },47 {48 title: "Optional sync and recovery",49 body:50 "Users make that same account portable when you connect managed Jazz sync: a passkey where available, a recovery phrase as the portable fallback.",51 },52 {53 title: "Narrow collaboration templates",54 body: (55 <>56 Private resources, direct shares, and fixed-role groups sit behind a57 small <code className="inline-mono">@nzip/lofi/access</code>{" "}58 API. Raw Jazz permissions stay available as an escape hatch.59 </>60 ),61 },62 {63 title: "Local-first test helpers",64 body:65 "Playwright-backed fixtures cover offline writes, multiple clients, convergence, and readiness — without hand-timed sleeps.",66 },67 {68 title: "Explicit mobile support",69 body:70 "Unsupported browsers get a clear explanation, not a half-working app that risks the user's data.",71 },72];7374export default function Home(): ReactNode {75 const { siteConfig } = useDocusaurusContext();76 const version = siteConfig.customFields?.lofiVersion as string;77 const heroArt = useBaseUrl("/img/lofi-hero-art.webp");7879 return (80 <Layout81 title="lofi"82 description="lofi generates installable Deno web apps that open immediately, keep working offline, and sync when users choose. Astro shell, Preact islands, Jazz 2 CRDT sync."83 >84 <main className="landing">85 {/* ===================== HERO: MARK + CLAIM ===================== */}86 <div className="wrap">87 <header>88 <div className="hero-top">89 <h1 className="nmark" aria-label="lofi">90 <span className="lo">lo</span>91 <span className="fi">fi</span>92 </h1>93 <div className="hero-side">94 <p className="eyebrow">95 <span>local-first meta-framework</span>96 <span className="pill">v{version} · alpha · MIT</span>97 </p>98 </div>99 </div>100 <div className="hero-main">101 <div>102 <p className="hero-claim">103 Installable web apps where the network is optional and the104 user holds the keys.105 </p>106 <CopyCmd />107 <p className="cmd-note">108 Requires Deno 2.9+. Astro + Preact + Jazz 2, generated.109 </p>110 <div className="cta-row">111 <Link className="btn btn--primary" to="/docs/getting-started">112 Read the guide113 </Link>114 <Link115 className="btn"116 href="https://github.com/FelineStateMachine/lofi"117 >118 source119 </Link>120 </div>121 </div>122 <div className="hero-art">123 <img124 src={heroArt}125 width="900"126 height="900"127 fetchPriority="high"128 alt="A phone containing a growing plant. Its roots run below the soil line inside the device; a dotted line arcs to a second, smaller phone."129 />130 </div>131 </div>132 </header>133 </div>134135 {/* ===================== LIFECYCLE ===================== */}136 <div className="wrap">137 <section className="band band--rule">138 <div className="band-head">139 <h2>Open now. Back up later.</h2>140 </div>141 <div className="life">142 <div className="life-step">143 <span className="life-tag">First launch</span>144 <h3>A private account, on the device</h3>145 <p>146 Every user gets an on-device account at first open, even when147 the app has no sync service configured at all. Nothing to sign148 up or waiting.149 </p>150 </div>151 <div className="life-step">152 <span className="life-tag">Every launch after</span>153 <h3>Reads from local storage, not the network</h3>154 <p>155 The UI reads durable local data instead of waiting on a round156 trip. When storage fails, lofi surfaces it rather than quietly157 falling back to memory and risking the data.158 </p>159 </div>160 <div className="life-step">161 <span className="life-tag">If the user wants it</span>162 <h3>The same account, made portable</h3>163 <p>164 Connect managed Jazz sync and a user can back up the account165 they already have — recovering it with a passkey where166 supported, or a 24-word phrase. Data made offline comes along.167 </p>168 <p className="life-cmd">169 <code className="inline-mono">deno task jazz:provision</code>170 <br />171 <span>172 or scaffold with <code className="inline-mono">--sync</code>173 {" "}174 from the start.175 </span>176 </p>177 </div>178 </div>179 </section>180 </div>181182 {/* ===================== DEMO ===================== */}183 <div className="wrap">184 <section className="band band--rule">185 <div className="band-head">186 <p className="eyebrow">at the core of the idea</p>187 <h2>Pull the cable.</h2>188 <p className="lede band-lede">189 This is the starter task app the generator gives you. Turn the190 network off, keep typing on both devices, then turn it back on.191 Nothing is lost and nothing conflicts. That is what the CRDT192 buys you.193 </p>194 </div>195 <SyncDemo />196 </section>197 </div>198199 {/* ===================== FEATURES ===================== */}200 <div className="wrap">201 <section className="band band--rule">202 <div className="band-head">203 <h2>The parts you would otherwise write yourself.</h2>204 </div>205 <div className="grid-3">206 {FEATURES.map((feature) => (207 <div className="cell" key={feature.title}>208 <Leaf />209 <h3>{feature.title}</h3>210 <p>{feature.body}</p>211 </div>212 ))}213 </div>214 </section>215 </div>216217 {/* ===================== ANATOMY ===================== */}218 <div className="wrap">219 <section className="band band--rule">220 <div className="band-head">221 <h2>222 You own the product.223 <br />224 lofi owns the plumbing.225 </h2>226 </div>227 <div className="anat">228 <pre229 className="tree"230 aria-label="Generated project layout, coloured by ownership"231 ><code>{"my-app/\n├── "}<span className="yours">deno.json</span>{" "}<span className="note"># tasks + one pinned version</span>{"\n├── "}<span className="yours">public/</span>{" "}<span className="note"># manifest, product icons</span>{"\n├── "}<span className="yours">src/</span>{"\n│ ├── "}<span className="yours"><b>app.ts</b></span>{" "}<span className="note"># storage, sync, passkey config</span>{"\n│ ├── "}<span className="yours"><b>schema.ts</b></span>{" "}<span className="note"># persisted data model</span>{"\n│ ├── "}<span className="yours"><b>permissions.ts</b></span>{" "}<span className="note"># private, shared, or group</span>{"\n│ ├── "}<span className="yours">islands/</span>{" "}<span className="note"># interactive Preact UI</span>{"\n│ ├── "}<span className="yours">layouts/</span>{" "}<span className="note"># document shell</span>{"\n│ ├── "}<span className="yours">pages/</span>{" "}<span className="note"># Astro routes</span>{"\n│ └── "}<span className="yours">styles/</span>{" "}<span className="note"># product styling</span>{"\n├── "}<span className="yours">tests/</span>{" "}<span className="note"># local-first journeys</span>{"\n├── "}<span className="note">.lofi/</span>{" "}<span className="note"># generated tooling (ignored)</span>{"\n└── "}<span className="note">dist/</span>{" "}<span className="note"># production PWA (ignored)</span>{"\n\n"}<span className="theirs">@nzip/lofi</span>{" "}<span className="note"># storage, identity, sync, PWA</span></code></pre>232 <div>233 <p>234 A generated project keeps your source separate from the235 versioned framework package. Product work stays in the files236 above; framework behaviour is imported, never copied into your237 tree. Upgrading the package updates the runtime without a238 migration through your own source.239 </p>240 <ul className="legend">241 <li>242 <span className="swatch swatch--yours" />243 <span>244 <b>Yours.</b>{" "}245 Schema, permissions, config, routes, islands, styles,246 tests.247 </span>248 </li>249 <li>250 <span className="swatch swatch--theirs" />251 <span>252 <b>lofi's.</b>{" "}253 Storage, identity, sync, lifecycle, and PWA plumbing —254 imported from one pinned version.255 </span>256 </li>257 <li>258 <span className="swatch swatch--gen" />259 <span>260 <b>Generated.</b>{" "}261 Regenerated on demand and git-ignored. Never edit by hand.262 </span>263 </li>264 </ul>265 <p className="anat-more">266 <Link to="/docs/reference/project-layout">267 See the exact generated-project map →268 </Link>269 </p>270 </div>271 </div>272 </section>273 </div>274275 {/* ===================== STACK + LIMITS ===================== */}276 <div className="wrap" id="stack">277 <section className="band band--rule">278 <div className="two-up">279 <div>280 <div className="band-head band-head--tight">281 <p className="eyebrow">stack and version policy</p>282 <h2>Pinned on purpose.</h2>283 </div>284 <p className="dim intro-note">285 The data layer is an alpha, so every upgrade is reviewed and286 validated before it reaches a generated project.287 </p>288 <table className="tbl">289 <thead>290 <tr>291 <th>Layer</th>292 <th>Choice</th>293 <th>Pinned at</th>294 </tr>295 </thead>296 <tbody>297 <tr>298 <td>Data / sync</td>299 <td>Jazz 2, CRDTs, OPFS</td>300 <td className="pin">2.0.0-alpha.53</td>301 </tr>302 <tr>303 <td>UI runtime</td>304 <td>Preact islands, thin adapter</td>305 <td className="pin">Preact 10 / Astro 7</td>306 </tr>307 <tr>308 <td>Shell</td>309 <td>Prerendered Astro, static host</td>310 <td className="pin">Astro 7</td>311 </tr>312 <tr>313 <td>Toolchain</td>314 <td>Deno tasks, npm compat</td>315 <td className="pin">Deno 2.9</td>316 </tr>317 </tbody>318 </table>319 </div>320 <div>321 <div className="band-head band-head--tight">322 <h2>What it isn't, yet.</h2>323 </div>324 <p className="dim intro-note">325 Built for mobile web apps where offline is a requirement, not326 a best-effort enhancement. Before you commit:327 </p>328 <ul className="limits">329 <li>330 <b>It is an early alpha release.</b>{" "}331 Expect the surface to move.332 </li>333 <li>334 <b>The data layer is Jazz 2 alpha</b>, deliberately pinned335 to a reviewed version.336 </li>337 <li>338 <b>Browser floors are real.</b>{" "}339 Android Chrome 148+ and iOS Safari 16.4+.340 </li>341 <li>342 <b>Recovery is user-controlled.</b>{" "}343 lofi keeps no recoverable account material on the server, so344 the phrase has to be kept safe.345 </li>346 </ul>347 </div>348 </div>349 </section>350 </div>351352 {/* ===================== COMMANDS ===================== */}353 <div className="wrap">354 <section className="band band--rule">355 <div className="band-head">356 <h2>Every project, a unified toolset.</h2>357 <p className="dim intro-note">358 Every generated project exposes these as{" "}359 <code className="inline-mono">deno task &lt;name&gt;</code>.360 </p>361 </div>362 <div className="two-up">363 <table className="tbl">364 <caption>Everyday development</caption>365 <tbody>366 <tr>367 <td>368 <code>dev</code>369 </td>370 <td>Runs the Astro dev server and prints runtime state.</td>371 </tr>372 <tr>373 <td>374 <code>doctor</code>375 </td>376 <td>377 Checks readiness without printing config or secrets.378 </td>379 </tr>380 <tr>381 <td>382 <code>test</code>383 </td>384 <td>Runs the deterministic local-first suite.</td>385 </tr>386 <tr>387 <td>388 <code>build</code>389 </td>390 <td>391 Creates a static production build in <code>dist/</code>.392 </td>393 </tr>394 <tr>395 <td>396 <code>preview</code>397 </td>398 <td>Serves the production build locally.</td>399 </tr>400 </tbody>401 </table>402 <table className="tbl">403 <caption>Sync, schema, deployment</caption>404 <tbody>405 <tr>406 <td>407 <code>jazz:provision</code>408 </td>409 <td>410 Creates a managed Jazz app and configures{" "}411 <code>.env</code>.412 </td>413 </tr>414 <tr>415 <td>416 <code>schema:validate</code>417 <br />418 <code>schema:deploy</code>419 </td>420 <td>Validates and publishes the Jazz schema.</td>421 </tr>422 <tr>423 <td>424 <code>migrations:create</code>425 <br />426 <code>migrations:push</code>427 </td>428 <td>Authors and pushes schema migrations.</td>429 </tr>430 <tr>431 <td>432 <code>deploy</code>433 <br />434 <code>deploy:create</code>435 </td>436 <td>Hosts the static build on Deno Deploy.</td>437 </tr>438 </tbody>439 </table>440 </div>441 </section>442 </div>443 </main>444 </Layout>445 );446}