mirror of
https://github.com/layer-systems/website.git
synced 2026-09-12 13:43:01 +02:00
* feat: highlight text in the Reader (NIP-84) Adds NIP-84 highlights (kind 9802) to the article reader: - Selecting text in an article shows a floating "Highlight" button (src/apps/articles/HighlightLayer.tsx), publishing the selected plain text tagged to the article (`a`) and its author (`p`, role "author"). - Existing highlights for the article are listed underneath it, with the highlighter's identity and timestamp. Closes #23 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * fix: harden selection handling and clamp the highlight button Per review: - Guard sel.rangeCount === 0 before calling getRangeAt(0), which throws otherwise. - Scope containment by the range's commonAncestorContainer instead of just anchorNode, so a selection that starts inside the article but is dragged out past its boundary is correctly rejected. - Clamp the floating button's top so a selection near the top of the viewport doesn't push it off-screen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto * fix: don't highlight against a malformed address, gate the listener Per a "needs a closer look" review pass: - articles/index.tsx now passes an empty string, not a malformed "kind:pubkey:" address, when an article has no d tag. HighlightLayer treats a falsy address as "highlighting isn't available here." - The selectionchange listener is only registered when both user and address are present (in the effect's deps), instead of always running selection tracking regardless of whether a highlight could ever be published. - handleHighlight and the floating button both guard on address too, not just selection, so stale selection state from before a prop change went missing can't still trigger a publish. - docs/apps.md corrected: the saved text comes from Selection.toString() (window.getSelection()), not Range.toString(). 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.