mirror of
https://github.com/layer-systems/website.git
synced 2026-09-12 05:33:12 +02:00
* feat: local draft notes with a blank "new note" entry point Writing was tied to publishing: the Feed composer either sits empty or fires a note straight to relays, with nowhere to keep something you're not ready to publish yet. The Note app now supports a draft mode when opened without an id: a blank note kept in localStorage until you publish it or discard it, reachable via a new "New note" button in the Feed toolbar, the Go menu, or the command palette (all already open the Note app with no params). Closes #19 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * fix: guard against double-publish and dropped relay params Per review: - handlePublish now also checks publish.isPending itself, not just the button's disabled state — a second click landing before React re-renders could otherwise fire mutateAsync twice. - Publishing a draft now merges into the existing params instead of replacing them outright, so relay hints (or anything else already in params) survive the id being added. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * docs: mark notes app's id param as optional Per review — the draft mode added by this PR means id is no longer required to open the Note app. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * fix: sync useLocalStorage across same-tab consumers of one key Per review: the Notes app is explicitly non-singleton, so opening two "New Note" windows meant two DraftNote instances writing the same localStorage key independently — the native `storage` event only fires in *other* tabs/documents, never the one that wrote, so the two windows would silently diverge (discard/publish in one wouldn't update the other). useLocalStorage now also dispatches a same-document custom event on every write, and every instance sharing that key listens for it — verified live with two open draft windows staying in sync as one is typed into. Also dropped a redundant `{}` params argument on an openApp() call that every other call site omits when opening with no parameters. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto --------- Co-authored-by: highperfocused <highperfocused@pm.me> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Documentation
This project is a Nostr client built as a desktop operating system: there are no
pages, only apps that open as windows you can move, resize, stack and keep side by
side. PLAN.md in the repository root records why it was built this way and which
decisions were taken; these documents describe how it actually works.
| Document | What it covers |
|---|---|
window-manager.md |
The OS core: state, geometry, gestures, persistence, routing |
apps.md |
The app registry, the contract every app implements, and how to add one |
nostr.md |
Data access, relay hints, and the rules for rendering untrusted content |
styleguide.md |
Design tokens, materials, typography, motion, and the layout rules for app content |
Layout of the custom code
src/
os/ # Window manager — no UI, no Nostr
types.ts # AppDefinition, WindowState, AppProps
registry.ts # The catalogue of apps
windowReducer.ts # All state transitions (pure)
WindowManagerProvider.tsx
WindowManagerContext.ts
useWindowManager.ts
layout.ts # Viewport maths: clamping, cascade, snap targets
useDrag.ts # Pointer-driven moving
useResize.ts # Pointer-driven resizing
useOsKeyboard.ts # System-wide shortcuts
persistence.ts # Session save / restore
components/os/ # The shell
OsShell.tsx # Entry point: picks desktop or mobile, syncs the URL
MenuBar.tsx # macOS-style bar (app menu, Go, Window, relays, theme, login)
MenuBarClock.tsx
Desktop.tsx # Wallpaper, icon grid, desktop context menu
DesktopIcon.tsx
WindowLayer.tsx # Renders every window, owns the snap preview
WindowFrame.tsx # Window chrome: title bar, traffic lights, resize handles
TrafficLights.tsx
CommandPalette.tsx # ⌘K
MobileAppShell.tsx # Home screen + full-screen app, under 768px
AppChrome.tsx # Layout primitives every app builds on
components/nostr/ # Shared Nostr UI
NoteCard.tsx, NoteContent.tsx, AuthorLine.tsx, LoginRequired.tsx
apps/<id>/index.tsx # One default-exported component per app
hooks/ # useRelayStatus, useRelayHints, useFollows (+ template hooks)
lib/nostrUtils.ts # sanitizeUrl, relay hints, tag helpers, time formatting
The three layers do not reach into each other: src/os/ knows nothing about Nostr or
about any particular app, components/os/ knows about windows but not about note
kinds, and an app knows about its own data but never about window geometry.
Running it
npm run dev # Vite dev server (port 8080 by default — see vite.config.ts)
npm run test # tsc --noEmit + eslint + vitest + production build
npm run test is the gate: it type-checks, lints, runs the unit tests and builds.
Nothing is finished until it passes.