← Pocket Notes history

feat(examples): implement five standalone browser games and apps

Actual project-only Git commit. Email addresses are redacted; source bytes and recorded timestamps are preserved. This is a code record, not human acceptance.

commit dc8c3a6620815897d926c4cb9bcf56be9ce49f96
Author:     0xLeif <redacted@invalid>
AuthorDate: Sun Nov 8 19:35:16 2026 -0700
Commit:     0xLeif <redacted@invalid>
CommitDate: Sun Nov 8 19:35:16 2026 -0700

    feat(examples): implement five standalone browser games and apps

diff --git a/apps/pocket-notes/LICENSE b/apps/pocket-notes/LICENSE
new file mode 100644
index 0000000..812de8f
--- /dev/null
+++ b/apps/pocket-notes/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 CorvidLabs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/apps/pocket-notes/README.md b/apps/pocket-notes/README.md
new file mode 100644
index 0000000..95ddf5e
--- /dev/null
+++ b/apps/pocket-notes/README.md
@@ -0,0 +1,31 @@
+# Pocket Notes
+
+A standalone browser-local example. No runtime dependencies, accounts, analytics, external artwork or network API.
+
+## Run
+
+Install Bun (verified during development with 1.4.0), then from this extracted project:
+
+~~~sh
+bun install --frozen-lockfile
+bun run test
+bun run typecheck
+bun run build
+bun run start
+~~~
+
+Open http://127.0.0.1:4355. PORT overrides the local server port. The only development dependency is exactly TypeScript 5.9.3. Tests use Bun's built-in runner; strict semantic typechecking covers src only with ES2022/DOM libraries. Bun compiles browser code into dist/ui.js. Deploy the contents of dist under a root or a trailing-slash directory such as /examples/pocket-notes/play/; assets are relative.
+
+## Use and data
+
+Edit a single note, including line breaks and whitespace. The limit is 20,000 UTF-16 code units; oversized input stays in the editor and must be shortened before saving/export. Clearing the editor saves an empty note. Export note (.txt) keeps text exactly.
+
+Data is saved in this origin's localStorage under corvid-examples:pocket-notes:v1. Reload restores validated version 1 data. Unsupported or malformed data warns and is not overwritten automatically. Valid new user edits or explicit Retry save may replace stored data. If storage access, reading or writing fails, current valid in-memory edits remain usable and an explicit warning is shown. Browser-local storage is not a backup; export before closing the tab if saving fails. Different hosts, ports and browser profiles have separate storage.
+
+Export downloads only when its button is activated. No import, migration, autosent files, server storage or cross-tab synchronization. Runtime storage access, actual downloads, keyboard/mobile behavior and accessibility need separate browser verification; model tests do not prove those properties.
+
+## Source and tests
+
+src/model.ts holds pure immutable state and validation. IDs/times are supplied to model operations. src/storage.ts validates real serialized data through an injected getItem/setItem adapter. src/ui.ts owns DOM, browser storage acquisition and explicit downloads. Invalid model operations return ModelResult errors; they do not throw RangeError. Tests exercise the actual helpers, including unavailable storage and retained edits.
+
+MIT license applies to newly authored code; see LICENSE. No claim of human implementation acceptance or deployment is included.
diff --git a/apps/pocket-notes/build.ts b/apps/pocket-notes/build.ts
new file mode 100644
index 0000000..41c0360
--- /dev/null
+++ b/apps/pocket-notes/build.ts
@@ -0,0 +1,17 @@
+const base = import.meta.dir;
+await Bun.$`mkdir -p ${base}/dist`;
+const result = await Bun.build({
+    entrypoints: [base + "/src/ui.ts"],
+    outdir: base + "/dist",
+    target: "browser",
+    format: "esm",
+    naming: "ui.js",
+});
+if (!result.success) {
+    for (const log of result.logs) console.error(log);
+    process.exit(1);
+}
+for (const name of ["index.html", "styles.css"]) {
+    await Bun.write(base + "/dist/" + name, Bun.file(base + "/" + name));
+}
+console.log("Pocket Notes built into dist/");
diff --git a/apps/pocket-notes/bun.lock b/apps/pocket-notes/bun.lock
new file mode 100644
index 0000000..5a85ed1
--- /dev/null
+++ b/apps/pocket-notes/bun.lock
@@ -0,0 +1,15 @@
+{
+  "lockfileVersion": 2,
+  "configVersion": 1,
+  "workspaces": {
+    "": {
+      "name": "@corvid-examples/pocket-notes",
+      "devDependencies": {
+        "typescript": "5.9.3",
+      },
+    },
+  },
+  "packages": {
+    "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+  }
+}
diff --git a/apps/pocket-notes/index.html b/apps/pocket-notes/index.html
new file mode 100644
index 0000000..0b2daa9
--- /dev/null
+++ b/apps/pocket-notes/index.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="en">
+<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="description" content="One editable browser-local note with explicit text export.">
+<title>Pocket Notes | CorvidLabs examples</title><link rel="stylesheet" href="./styles.css">
+<script type="module" src="./ui.js"></script></head>
+<body><main class="shell"><nav class="rail" aria-label="Example"><a href="../">Example details</a><span>CorvidLabs / 05</span></nav>
+<section class="surface" aria-labelledby="title"><p class="eyebrow">A little space to think</p>
+<h1 id="title">Pocket Notes</h1><p class="intro">Write one note, keep it in this browser, and take a plain-text copy whenever you choose.</p>
+<noscript>This app needs JavaScript to edit and save notes. No data is sent to a server. The downloadable source includes setup instructions.</noscript>
+<div style="margin-top:28px"><label for="note">Your note</label><textarea id="note" spellcheck="true" aria-describedby="note-help input-error note-count"></textarea><p id="note-help" class="help">Up to 20,000 characters. Whitespace is kept exactly. Emptying the editor clears the saved note.</p><p id="note-count" class="count"></p></div>
+<p id="input-error" class="error" role="alert"></p>
+<p id="storage-warning" class="warning" role="alert" hidden></p>
+<p id="save-status" class="status" role="status">New in this tab. Nothing saved yet.</p>
+
+<div class="toolbar"><button id="export" type="button">Export note (.txt)</button><button id="retry" class="secondary" type="button">Retry save</button></div>
+<p class="help">Browser-local only. No account or cloud backup. Private browsing, clearing site data or storage limits can remove saved work. Export a copy before closing if saving is unavailable. No import in this example.</p>
+</section><footer>Built as a small, inspectable example. MIT-licensed source; no external artwork.</footer></main></body></html>
diff --git a/apps/pocket-notes/serve.ts b/apps/pocket-notes/serve.ts
new file mode 100644
index 0000000..5f8c880
--- /dev/null
+++ b/apps/pocket-notes/serve.ts
@@ -0,0 +1,16 @@
+const base = import.meta.dir + "/dist/";
+const port = Number(process.env.PORT ?? 4355);
+if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PORT must be 1..65535.");
+if (!(await Bun.file(base + "index.html").exists())) throw new Error("Run bun run build before bun run start.");
+Bun.serve({
+    hostname: "127.0.0.1",
+    port,
+    fetch(request) {
+        const path = new URL(request.url).pathname;
+        const name = path.endsWith("/") || path.endsWith("/index.html") ? "index.html"
+            : path.endsWith("/ui.js") ? "ui.js" : path.endsWith("/styles.css") ? "styles.css" : null;
+        if (!name) return new Response("Not found", { status: 404 });
+        return new Response(Bun.file(base + name), { headers: { "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" } });
+    },
+});
+console.log("Pocket Notes: http://127.0.0.1:" + port);
diff --git a/apps/pocket-notes/src/model.ts b/apps/pocket-notes/src/model.ts
index b3059c2..cd780e5 100644
--- a/apps/pocket-notes/src/model.ts
+++ b/apps/pocket-notes/src/model.ts
@@ -8,15 +8,37 @@ export interface FileExport {
     readonly contents: string;
 }
 
-export interface NoteState {
-    readonly version: 1;
-    readonly text: string;
-    readonly updatedAt: number;
-}
+export interface NoteState { readonly version: 1; readonly text: string; readonly updatedAt: number; }
 export const MAX_NOTE_LENGTH = 20_000;
-export function createNoteState(): NoteState { throw new Error("Not implemented"); }
-export function validateNoteState(_value: unknown): ModelResult<NoteState> { throw new Error("Not implemented"); }
-export function editNote(_state: NoteState, _text: string, _at: number): ModelResult<NoteState> {
-    throw new Error("Not implemented");
+
+function validTime(value: unknown): value is number {
+    return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
+}
+
+export function createNoteState(): NoteState { return { version: 1, text: "", updatedAt: 0 }; }
+
+export function validateNoteState(value: unknown): ModelResult<NoteState> {
+    if (typeof value !== "object" || value === null || Array.isArray(value)) {
+        return { ok: false, error: "Note data must be a version 1 note." };
+    }
+    const raw = value as Record<string, unknown>;
+    if (Object.keys(raw).length !== 3 || !["version", "text", "updatedAt"].every(key => Object.hasOwn(raw, key))
+        || raw.version !== 1 || typeof raw.text !== "string" || raw.text.length > MAX_NOTE_LENGTH || !validTime(raw.updatedAt)) {
+        return { ok: false, error: "Stored note has an unsupported version, text or timestamp." };
+    }
+    return { ok: true, value: { version: 1, text: raw.text, updatedAt: raw.updatedAt } };
+}
+
+export function editNote(state: NoteState, text: string, at: number): ModelResult<NoteState> {
+    const checked = validateNoteState(state);
+    if (!checked.ok) return checked;
+    if (text.length > MAX_NOTE_LENGTH) return { ok: false, error: "Keep your note within 20,000 characters. Your editor text is still here." };
+    if (!validTime(at) || at < state.updatedAt) return { ok: false, error: "Note time cannot be invalid or move backwards." };
+    return { ok: true, value: { version: 1, text, updatedAt: at } };
+}
+
+export function createNoteExport(state: NoteState): FileExport {
+    const checked = validateNoteState(state);
+    if (!checked.ok) throw new Error(checked.error);
+    return { filename: "pocket-note.txt", mediaType: "text/plain", contents: checked.value.text };
 }
-export function createNoteExport(_state: NoteState): FileExport { throw new Error("Not implemented"); }
diff --git a/apps/pocket-notes/src/storage.ts b/apps/pocket-notes/src/storage.ts
index 78d8b25..1904f69 100644
--- a/apps/pocket-notes/src/storage.ts
+++ b/apps/pocket-notes/src/storage.ts
@@ -1,4 +1,4 @@
-import type { NoteState } from "./model";
+import { validateNoteState, type NoteState } from "./model";
 
 export interface StorageAdapter {
     getItem(key: string): string | null;
@@ -13,9 +13,27 @@ export type SaveResult =
     | { readonly ok: false; readonly message: string };
 
 export const NOTE_STORAGE_KEY = "corvid-examples:pocket-notes:v1";
-export function loadNote(_storage: StorageAdapter): LoadResult<NoteState> {
-    throw new Error("Not implemented");
+
+export function loadNote(storage: StorageAdapter): LoadResult<NoteState> {
+    try {
+        const raw = storage.getItem(NOTE_STORAGE_KEY);
+        if (raw === null) return { status: "empty" };
+        const parsed: unknown = JSON.parse(raw);
+        const checked = validateNoteState(parsed);
+        if (!checked.ok) return { status: "warning", message: checked.error + " Stored data was left untouched." };
+        return { status: "loaded", value: checked.value };
+    } catch {
+        return { status: "warning", message: "Saved data could not be read. Keep working in this tab and export a copy." };
+    }
 }
-export function saveNote(_storage: StorageAdapter, _state: NoteState): SaveResult {
-    throw new Error("Not implemented");
+
+export function saveNote(storage: StorageAdapter, state: NoteState): SaveResult {
+    const checked = validateNoteState(state);
+    if (!checked.ok) return { ok: false, message: checked.error };
+    try {
+        storage.setItem(NOTE_STORAGE_KEY, JSON.stringify(checked.value));
+        return { ok: true };
+    } catch {
+        return { ok: false, message: "Changes are in this tab only because browser storage could not save them. Export a copy." };
+    }
 }
diff --git a/apps/pocket-notes/src/ui.ts b/apps/pocket-notes/src/ui.ts
new file mode 100644
index 0000000..3d570e5
--- /dev/null
+++ b/apps/pocket-notes/src/ui.ts
@@ -0,0 +1,84 @@
+import { createNoteExport, createNoteState, editNote } from "./model";
+import { loadNote, saveNote } from "./storage";
+
+function element<Value extends HTMLElement>(id: string): Value {
+    const found = document.getElementById(id);
+    if (!found) throw new Error("Missing interface element: " + id);
+    return found as Value;
+}
+
+function download(file: { filename: string; mediaType: string; contents: string }): void {
+    const url = URL.createObjectURL(new Blob([file.contents], { type: file.mediaType + ";charset=utf-8" }));
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = file.filename;
+    document.body.append(link);
+    link.click();
+    link.remove();
+    window.setTimeout(() => URL.revokeObjectURL(url), 1000);
+}
+
+const editor = element<HTMLTextAreaElement>("note");
+const warning = element<HTMLParagraphElement>("storage-warning");
+const message = element<HTMLParagraphElement>("input-error");
+const status = element<HTMLParagraphElement>("save-status");
+const count = element<HTMLParagraphElement>("note-count");
+const exportButton = element<HTMLButtonElement>("export");
+let state = createNoteState();
+
+function showWarning(text: string): void {
+    warning.textContent = text;
+    warning.hidden = false;
+    status.textContent = "Not saved in browser";
+}
+
+function save(): void {
+    if (editor.value !== state.text) {
+        message.textContent = "Shorten your note to 20,000 characters before saving or exporting. Editor text is preserved.";
+        return;
+    }
+    try {
+        const result = saveNote(window.localStorage, state);
+        if (!result.ok) { showWarning(result.message); return; }
+        warning.hidden = true;
+        status.textContent = "Saved in this browser";
+    } catch {
+        showWarning("Browser storage is unavailable. Your note stays in this tab. Export a copy before closing.");
+    }
+}
+
+function updateCount(): void {
+    count.textContent = editor.value.length.toLocaleString() + " / 20,000 characters";
+}
+
+editor.addEventListener("input", () => {
+    updateCount();
+    const result = editNote(state, editor.value, Math.max(Date.now(), state.updatedAt));
+    if (!result.ok) {
+        message.textContent = result.error + " Shorten it before exporting; you can still select and copy all editor text.";
+        editor.setAttribute("aria-invalid", "true");
+        exportButton.disabled = true;
+        status.textContent = "Editor has unsaved changes";
+        return;
+    }
+    state = result.value;
+    message.textContent = "";
+    editor.removeAttribute("aria-invalid");
+    exportButton.disabled = false;
+    save();
+});
+exportButton.addEventListener("click", () => {
+    try { download(createNoteExport(state)); }
+    catch { message.textContent = "Export could not start. Your text is still here; select and copy it or try again."; }
+});
+element<HTMLButtonElement>("retry").addEventListener("click", save);
+
+try {
+    const loaded = loadNote(window.localStorage);
+    if (loaded.status === "loaded") { state = loaded.value; status.textContent = "Restored from this browser"; }
+    else if (loaded.status === "warning") showWarning(loaded.message);
+} catch {
+    showWarning("Browser storage is unavailable. Keep writing in this tab and export a copy.");
+}
+editor.value = state.text;
+updateCount();
diff --git a/apps/pocket-notes/styles.css b/apps/pocket-notes/styles.css
new file mode 100644
index 0000000..2272176
--- /dev/null
+++ b/apps/pocket-notes/styles.css
@@ -0,0 +1,21 @@
+:root{--paper:#e0b05a}
+:root{font-family:"Helvetica Neue",system-ui,sans-serif;color:#15181b;background:#131619;font-synthesis:none;line-height:1.55}
+*{box-sizing:border-box}body{margin:0;padding:32px 20px 56px}a{color:inherit;text-underline-offset:4px}
+.shell{max-width:760px;margin:auto}.rail{display:flex;justify-content:space-between;gap:16px;color:#f4f3ef;font:13px ui-monospace,monospace;margin-bottom:28px;flex-wrap:wrap}
+.surface{padding:clamp(22px,5vw,48px);background:var(--paper);border-radius:6px;border:1px solid #15181b}
+.eyebrow{font:12px ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase}
+h1{font-size:clamp(36px,7vw,56px);line-height:1.05;letter-spacing:-.05em;margin:18px 0}p{margin:12px 0}
+.intro{max-width:48ch}.toolbar{display:flex;gap:12px;flex-wrap:wrap;margin:24px 0}
+button,input,textarea{font:inherit}button{min-height:44px;padding:10px 16px;border:1px solid #15181b;border-radius:4px;background:#15181b;color:#faf9f6;cursor:pointer;font-weight:600}
+button.secondary{color:#15181b;background:transparent}button:disabled{opacity:.65;cursor:not-allowed}
+:focus-visible{outline:3px solid #087267;outline-offset:4px}input,textarea{color:#15181b;background:#fffdf9;border:1px solid #505456;border-radius:4px;padding:12px;width:100%;min-width:0;font-size:18px}
+label{display:block;font-weight:650;margin-bottom:8px}.field{flex:1;min-width:0}.add-row{display:flex;gap:10px;align-items:end;margin-top:28px}.add-row button{min-height:54px}
+.help,.status,.count{font-size:14px}.error{color:#871e12;font-weight:600}.warning{border:2px solid #713b00;background:#fff0ca;padding:14px;font-size:16px}
+.task-list{padding:0;list-style:none;margin:28px 0}.task{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start;padding:20px 0;border-top:1px solid #777c7d}
+.task-name{font-size:20px;overflow-wrap:anywhere;padding-top:8px}.complete .task-name{text-decoration:line-through;color:#4c5052}
+details{grid-column:1/-1;font-size:14px}summary{cursor:pointer;min-height:44px;padding:10px 0}details ol{padding-left:24px}
+textarea{resize:vertical;min-height:380px;line-height:1.7;background:transparent;border-color:#624311;font-size:20px}
+footer{color:#c8c6be;font-size:14px;margin-top:24px}footer a{display:inline-block;min-height:44px;padding-top:10px}
+noscript{display:block;border:2px solid #871e12;padding:16px;margin:20px 0}
+@media(max-width:430px){body{padding:20px 12px 32px}.add-row{align-items:stretch;flex-direction:column}.task{grid-template-columns:1fr}.task button{justify-self:start}.toolbar button{flex:1}textarea{min-height:330px}}
+@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;animation:none!important;transition:none!important}}