Files
website/docs
mroxso 50795167f5 feat: highlight text in the Reader (NIP-84) (#31)
* 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>
2026-09-06 18:47:47 +02:00
..
2026-09-06 11:37:43 +02:00
2026-09-06 11:37:43 +02:00
2026-09-06 11:37:43 +02:00

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.