style.css now has explicit breakpoints and primitives covering
the full target range:
- Global: overflow-x: hidden on body, max-width:100% on media,
fluid typography via clamp() so headings shrink on 320px.
- .wrap: 64rem cap at desktop, 72rem at 1440px, 96rem at 2560px;
generous side padding at large widths so text doesn't hug the
edge on huge monitors.
- .app-shell layout primitive (grid: sidebar + content [+ aside
on ultrawide]) ready for the eventual notes UI:
* mobile: single column, sidebar hidden behind a toggle
([data-sidebar="open"] reveals it).
* 768px+: 2-column with 16rem sidebar.
* 1024px+: 18rem sidebar.
* 1440px+: 20rem sidebar, content max-width 56rem so reading
lines don't grow unbounded.
* 2560px+: 3-column (sidebar | content | aside) so the
editor stays at reading width while the extra real estate
hosts backlinks/preview.
- .app-resize-handle with touch-action:none so pointer-event
drag handlers won't conflict with browser scrolling.
- Auth card tightens on viewports under 360px.
Result: no horizontal scroll at any width; content uses ultrawide
space effectively without sacrificing legibility.
Closes#18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- manifest.webmanifest: standalone display mode, theme #2563eb,
start_url=/app.html (so users who install land in the app
shell, not the marketing page), scope=/. Three icons: 192px
any-purpose, 512px any-purpose, 512px maskable for adaptive
icons on Android.
- icons/: PNGs generated from favicon.svg.
- sw.js: cache-first for the precached app shell, network-first
for /api/* and /auth/* (we never serve stale auth or notes).
Versioned cache name (librenotes-shell-v1) so a SW update
evicts old assets. skipWaiting + clients.claim so a new SW
takes over without a manual reload.
- pwa.js: registers the SW on every page and handles
beforeinstallprompt by showing #install-btn. Hides the button
again on appinstalled. Defer loaded so it never blocks render.
- All HTML pages link the manifest, set the theme-color meta,
and load pwa.js. Landing page exposes the install button next
to the existing CTAs.
Closes#19.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmd/librenotes/web/public/auth-client.js exposes window.authClient
with the full session API used by the rest of the frontend:
Session storage (#14):
- saveSession / loadSession / clearSession / isAuthenticated
- Backed by sessionStorage, not localStorage: tokens are isolated
per tab and cleared on tab close. localStorage would survive
tab close on a shared device, which we want to avoid.
- loadSession returns null when expires_at has passed, so callers
treat expired sessions as logged-out without a network round
trip.
API wrapper (#14):
- apiFetch(url, init) attaches Authorization: Bearer <jwt> to
every call. On 401 it clears the session and redirects to
/login.html?next=<current-path> so the user returns where they
started. Throws after the redirect so the caller's .then does
not run with stale data.
Tenant-scoped localStorage (#15):
- tenantStore() returns a get/set/remove wrapper whose keys are
prefixed "librenotes:{user_id}:". Two users on the same browser
therefore have fully independent UI state. JSON serialisation
with try/catch fallbacks for corrupted or quota-exceeded
storage so a bad blob never crashes the app.
- clearTenantStore(userID) removes every key with that prefix.
Called from clearSession() so logout wipes both the JWT and
the user's preferences.
verify.html + verify.js complete the magic-link flow: read
?token=, POST /auth/verify, hand the response to saveSession(),
strip the token from the URL via history.replaceState. Errors
route the user back to /login.html.
app.html + app.js are a minimal authenticated landing demonstrating
the full stack end-to-end: apiFetch hits /api/whoami, tenantStore
persists a theme preference, logout clears both. The full notes
UI is left to a later phase — this is the seam.
Closes#14 and #15.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmd/librenotes/web/public/login.{html,js}:
- Email input with required + autocomplete + autofocus, ARIA
attributes for screen readers (aria-describedby, aria-invalid,
role="alert" on the error container, role="status" on success).
- Client-side regex validation runs before POST to /auth/login
to avoid a network round-trip for obvious typos. Server is
still the source of truth.
- Loading state disables the button and changes its label.
- Success state replaces the form with "Check your email"
including the address, plus the 15-minute / single-use note.
- Error states map server statuses to user-friendly messages:
429 -> "too many requests", 400 -> "invalid email", anything
else -> generic server error. Network errors get their own
message so users can distinguish offline from server problems.
- No external CSS or JS dependencies; works with keyboard and
on small viewports.
Closes#13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmd/librenotes/serve.go wires the multi-tenant HTTP server:
storage + auth + httpapi packages, configurable via flags or
LIBRENOTES_* env vars. Embeds web/public/ for unauthenticated
static content. Generates an ephemeral JWT secret with a warning
when none is supplied. Adds security headers (CSP, nosniff,
DENY-frame, no-referrer) on every response. Background goroutine
purges expired magic-link tokens every 10 minutes.
cmd/librenotes/web/public/ provides the unauthenticated frontend:
- index.html: hero, features grid, fork attribution, footer.
Mobile-first, responsive from 320px up via clamp() and
auto-fit grid. SEO + Open Graph tags. No JS dependency.
- privacy.html: placeholder privacy policy (full text TBD).
- style.css: shared design tokens (light/dark via [data-theme]),
used by landing, auth pages, and the post-login app shell.
- favicon.svg: minimal mark.
The "serve" command sits alongside the original notesium CLI
verbs; main.go dispatches "serve" to the new code path and
forwards everything else to notesium.Run().
Closes#16.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial fork of github.com/alonswartz/notesium into librenotes:
- Source moved to internal/notesium/ (package notesium)
- Thin entry point at cmd/librenotes/main.go
- Module renamed to git.librete.ch/public/librenotes
- main() exposed as notesium.Run()
- LICENSE preserved (MIT), NOTICE added with attribution
- Web assets and completion.bash co-located with embedding code
to satisfy go:embed path constraints
Closes#3, #34, #35.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>