Skip to content

feat(share-web): /me + /@handle polish — modals, SWR header, row redesign#375

Merged
graydawnc merged 1 commit into
mainfrom
feat/share-web-design-recovery
Jun 8, 2026
Merged

feat(share-web): /me + /@handle polish — modals, SWR header, row redesign#375
graydawnc merged 1 commit into
mainfrom
feat/share-web-design-recovery

Conversation

@graydawnc

@graydawnc graydawnc commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Polish pass on the share-web /me and /@handle surfaces. Scoped to packages/share-web/ only; no backend or desktop changes.

Changes

Header SWR + cache

  • lib/me-cache.ts — versioned localStorage cache of {name, avatar_url} so first paint shows the real avatar instead of a 100ms blank while /api/me round-trips
  • lib/auth-cache.ts — module-level memo so cross-page Header mounts share one fetchMe call; invalidated on signOut and on a 401 response
  • Header default changes from auth='out' to auth='auto' — paints from cache, revalidates in the background; Reader.tsx, Profile.tsx, Tombstone.tsx, and the Me loading / error states drop the hardcoded auth="out" prop
  • Wordmark href /https://spool.pro/ so logo-click lands on the landing site in both dev and prod

Account surface

  • DeleteAccountModal replaces the inline idle / confirming / scheduled / cancelling state machine; opened from a small "Delete account" link in the "Signed in as …" footer row
  • onScheduled(at) lifts state to the parent so the deletion-pending banner appears immediately after the user confirms — previously the banner waited for the next reload
  • Banner copy carries an inline "Cancel deletion" button that re-opens the same modal in cancel mode
  • useRef re-entrancy lock on confirmUnpublish so a double-tap on a slow device cannot fire two parallel revoke calls

ShareRow + Unpublish flow

  • Single-line title (ellipsis + title= hover tooltip), meta line with a visibility icon (globe for profile-listed, link-2 for unlisted; title= tooltip, cursor: help) + published date + optional expires + revoked = small red Unpublished pill
  • Whole-row click navigates to the reader: title + meta wrap in one <a class="sw-share-link"> so hovering the visibility icon shows its title= naturally without an overlay blocking events
  • Hover: row bg → --card-2, title → accent
  • Actions live outside the anchor as siblings (copy + eye-off only — the external-link Open icon is removed because the title is now the open affordance)
  • UnpublishConfirmModal uses the same .sw-modal-* shell; renders the target row plus a 410-Gone warning before committing

Profile (public /@handle) rows

  • Single-line title + mono date, same anchor wrap, no actions

Utilities

  • relativeDate(ts, now) + Number.isFinite guards on humanDate / humanDateTime / relativeDate so NaN does not surface as Invalid Date
  • .sw-section-label drops text-transform: uppercase, keeps mono + tracking
  • .reader-paper gets shadow tokens (light + dark)
  • ModalShell shared by both confirm modals — Esc and backdrop click close, gated by busy

Test plan

  • pnpm --filter @spool/share-web typecheck — clean
  • pnpm --filter @spool/share-web exec vitest run — 70 / 70
    • dates.test.ts (8): humanDate / humanDateTime guards, relativeDate buckets (Today / Yesterday / Nd ago / 7+ days fallback / future / NaN)
    • me-cache.test.ts (7): round-trip, version-mismatch invalidation, malformed-JSON tolerance, clearCachedMe, localStorage-unavailable fallback
    • api-cache.test.ts (5): fetchMe 200 writes cache, 401 clears cache + drops the auth memo, 403 leaves cache intact, signOut clears both layers even when the network call fails
  • Manual dogfooding in worktree: sign-in / cross-page header sync, hover tooltips on visibility icons, row click → reader, copy + eye-off actions, Unpublish modal Esc + backdrop close + double-tap block, Delete account modal from both healthy and pending-deletion states, sign-out clears cache + memo

Follow-ups

  • /terms and /privacy are placeholder pages — real content needed
  • Desktop Spool app Shares → Published tab rows want the same polish

Submitter: @graydawnc

🤖 Generated with Claude Code

…sign

Recovers polish work designed + implemented on 2026-06-04 that never
landed on main. Ten items, scoped to share-web only (no backend or
desktop changes).

Header SWR + cache:
- New `lib/me-cache.ts` — versioned localStorage cache of {name, avatar_url}
- New `lib/auth-cache.ts` — module-level memo so cross-page Header mounts
  share one fetchMe; invalidated on signout + 401
- `Header` default `auth='auto'` paints from cache first, revalidates in
  background; pages drop their hardcoded `auth="out"` props
- Wordmark href "/" → "https://spool.pro/" so the logo click goes to the
  landing site in both dev and prod

Account surface:
- `DeleteAccountModal` replaces the inline confirm/scheduled state
  machine; opened from a quiet "Delete account" link on the right
  edge of the "Signed in as …" row
- `onScheduled(at)` callback lifts state to the parent so the deletion-
  pending banner appears immediately after Yes, not on the next refresh
- Banner copy now carries an inline "Cancel deletion" button that opens
  the same modal in cancel mode
- Re-entrancy lock (useRef) on confirm-unpublish so a double-tap on a
  slow device can't fire two parallel revokes

ShareRow + Unpublish flow:
- New row layout: bold single-line title (ellipsis + hover tooltip),
  meta line with a visibility ICON (`globe` for profile-listed, `link-2`
  for unlisted; tooltip on hover, `cursor: help`) + published date +
  optional expires + revoked = small red UNPUBLISHED pill
- Whole-row click navigates to the reader — title + meta wrap in a
  single `<a>` so hovering the visibility icon shows its `title=`
  naturally (no `::after` overlay to block tooltips)
- Hover state: row bg → `--card-2`, title → accent
- Actions sit outside the anchor as siblings (copy + eye-off only —
  the external-link Open icon dropped; title is the open affordance)
- `UnpublishConfirmModal` reuses the same `.sw-modal-*` shell — shows
  the target row + 410-Gone warning before committing

Profile (public /@handle) rows:
- Simple title + mono date layout — same anchor wrap, no actions

Utilities:
- `relativeDate(ts, now)` + `Number.isFinite` guards on humanDate /
  humanDateTime / relativeDate so NaN doesn't surface as "Invalid Date"
- `.sw-section-label` drops `text-transform: uppercase` per the
  sentence-case rule; mono + tracking kept
- Reader paper shadow tokens (light + dark variants)
- `ModalShell` shared by Delete + Unpublish modals (ESC + backdrop
  close, gated by busy)

Tests:
- `dates.test.ts` (8) — humanDate guards, relativeDate buckets (Today /
  Yesterday / Nd ago / >7 days fallback / future / NaN)
- `me-cache.test.ts` (7) — round-trip, version mismatch invalidates,
  malformed JSON survives, clear works, localStorage-unavailable
  fallback doesn't throw
- `api-cache.test.ts` (5) — fetchMe 200 writes cache + 401 clears it
  AND drops the auth memo, 403 leaves cache alone (deletion-pending
  surface still needs identity), signOut clears both layers even on
  network failure

44 → 70 tests; tsc clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@graydawnc
graydawnc added this pull request to the merge queue Jun 8, 2026
Merged via the queue into main with commit 1fcef46 Jun 8, 2026
6 checks passed
@graydawnc
graydawnc deleted the feat/share-web-design-recovery branch June 8, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant