mirror of
https://github.com/layer-systems/website.git
synced 2026-09-12 05:33:12 +02:00
* feat: Spells app for saved, shareable Nostr queries (grimoire kind 777)
The original feedback mentioned "grimoire spells" — traced to
github.com/purrgrammer/grimoire, a third-party Nostr client with its
own draft NIP for kind 777 "Spell" events: a REQ filter (kinds,
authors, one tag filter, limit, time window) encoded as portable,
shareable tags, with $me/$contacts runtime variables and relative
timestamps ("7d", "now").
- src/hooks/useSpells.ts implements that draft NIP as-is (same tags,
same variables, same relative-time grammar) rather than a
reinterpretation, so a spell saved here round-trips with Grimoire.
- src/apps/spells: browse "My Spells" / "Discover", build one with
NewSpellForm, and Run it on demand against the resolved filter,
rendering kind-1 results with NoteCard.
- Only the "Spell" half is implemented; "Spellbook" (kind 30777,
saved window layouts) is left as a documented follow-up.
Closes #24
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
* fix: address review feedback on the Spells app
Per review:
- Scope is now derived (forced to "discover" when signed out) rather
than stored as the requested value directly, matching the Feed
app's pattern — signing out mid-session can no longer leave "My
Spells" selected.
- resolveSpellFilter() now validates a spell's tag-filter letter
(single a-zA-Z char) before using it as a "#<letter>" filter key,
and clamps limit to [1, 500] instead of trusting a relay-sourced
spell's number outright — a malformed or hostile spell can no
longer produce a "#undefined" filter key or an enormous/NaN/zero
limit. Added regression tests for all of these.
- NewSpellForm's Field now renders a real <label htmlFor> connected
to each input's id (via useId()), and the Authors button group
moved to a <fieldset>/<legend> instead of a label sitting over
unrelated buttons — screen readers can now name every control.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
* fix: validate spell tag filter/limit at write time, and hide malformed badges
Per review:
- encodeSpellTags() now validates the tag filter's letter and the
limit before writing them, instead of only resolveSpellFilter()
catching bad values on Run — a spell authored through this app can
no longer save a filter it will silently fail to apply later.
Exported isValidTagLetter() so both sides share one definition of
"valid."
- The spell detail view's tag-filter badge now hides itself for a
malformed tag filter (e.g. from a relay-sourced spell this app
didn't author) instead of rendering "#undefined:" or similar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
* fix: reject non-integer/negative spell kinds, de-flake a timing test
Per remaining "previously missed" findings:
- parseSpell() now requires k tags to be non-negative integers
(Number.isInteger && >= 0), not just finite — a relay-sourced spell
claiming kind "1.5" or "-1" no longer passes through into a
malformed filter.
- The resolveTimestamp wall-clock tests asserted toBeCloseTo a single
captured `now`, which a slow runner or timing skew between the two
Date.now() calls could flake. Replaced with a before/after range
assertion.
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>
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.