Web lint warnings reduced from 20 → 15 by fixing the categories that
flag real architectural smells rather than the canonical
fetch-on-mount setState pattern.
Real fixes:
1. web/src/lib/use-theme.ts — replace useEffect + setState mount-sync
pattern with React.useSyncExternalStore. The hook now subscribes to
browser storage events, returns a stable snapshot for SSR, and uses
a manual storage-event dispatch so same-document setters refresh
correctly. Eliminates the cascading-render advisory and gains free
cross-tab theme sync.
2. web/src/lib/use-keyboard-shortcuts.ts — move ref assignment from
render time into a useEffect. Fixes the 'Cannot access refs during
render' advisory without behavior change.
3. web/src/components/NoteEditor.tsx — move onSaveRef.current = onSave
from render time into a useEffect for the same reason.
4. web/src/app/(app)/reviews/page.tsx — wrap handleDecision and
handleBatchDecision in useCallback so the useEffect that depends
on them no longer re-subscribes the keydown listener on every
render. Fixes both react-hooks/exhaustive-deps warnings and the
underlying perf bug they pointed at.
5. web/src/app/(app)/prompts/page.tsx — wrap loadTemplates in
useCallback declared before the useEffect that calls it. Fixes
the 'Cannot access variable before it is declared' advisory.
Remaining 15 warnings are React-compiler runtime hints about
fetchData().then(setData) patterns inside useEffect, which is the
canonical fetch-on-mount pattern shown in React's own docs. Resolving
them properly requires Suspense + use() or risky startTransition
wraps; both are out of scope and tracked under future tech debt.
Verified:
- pnpm --filter @notelett/web run typecheck: passes
- pnpm --filter @notelett/web run lint: 0 errors, 15 warnings (down 5)
- pnpm run verify: backend 380/380, web 96/96, mobile 97/97
Completes the high-leverage half of UI5 by migrating the most form-heavy
authenticated screens off the legacy 'input-shell' / inline-style pattern
onto Input, Textarea, Select, and AlertBanner primitives.
Migrated:
- web/src/app/(app)/settings/page.tsx — change-password form, feedback
form, MCP/API-tokens/offline-queue cards. Replaces 'surface-card'
sections with Card components, 'input-shell' inputs/selects/textareas
with Input/Select/Textarea, and inline error/success divs with
AlertBanner.
- web/src/components/CreateNoteModal.tsx — template/workspace/title/body/tags
fields. Select primitive uses options=[{value,label}].
- web/src/components/LinkNoteModal.tsx — search input + relationship-type
select + alert banner for errors.
- web/src/components/ShareDialog.tsx — user-id input, permission select,
collaborator/public-link rows now use AlertBanner (tone='neutral') for
the muted-surface look. Web Share API unsupported message is now a
proper tone='warning' banner.
- web/src/components/PromptTemplateEditor.tsx — full form (name, slug,
description, 3 selects, 2 textareas) migrated.
All existing tests continue to pass without modification because
@testing-library queries (getByLabel, getByPlaceholder, getByText) are
robust against the underlying HTML structure changes.
Verified:
- pnpm --filter @notelett/web run typecheck: passes
- pnpm --filter @notelett/web run test: 96/96 (existing CreateNoteModal,
LinkNoteModal, ShareDialog suites all green)
- pnpm run verify: end-to-end (backend 380/380, web 96/96, mobile 97/97)
- Legacy class matches in web/src dropped from 89 to 69 over the UI5
slice; remaining matches are in UI6/UI7 territory (dashboard, search,
workspaces list, notes detail, chat, palace, NoteEditor).
Sprint C / UI5 — migrate the highest-leverage user-facing forms off the
legacy 'input-shell' / inline-style pattern onto the @bytelyst/ui Input,
Textarea, and AlertBanner primitives via the local Primitives.tsx adapter.
Adapter additions (web/src/components/ui/Primitives.tsx):
- Re-export AlertBanner, FormSection, and FieldGrid from @bytelyst/ui so
product code never imports from the underlying package directly.
Migrated screens:
- web/src/app/(auth)/login/page.tsx
- web/src/app/(auth)/register/page.tsx
- web/src/app/(auth)/forgot-password/page.tsx
- web/src/components/CreateWorkspaceModal.tsx
Each migration replaces the ad-hoc 'input-shell' inputs and manual
label/error/success divs with the Input (label + hint props), Textarea,
and AlertBanner (tone='error'|'success') primitives. Inline style blocks
are replaced with Tailwind utility classes that read from the existing
--nl-* CSS custom properties so the visual tokens remain unchanged.
The 3 auth pages alone remove 9 input-shell call sites; the
CreateWorkspaceModal removes 2 more.
Verified:
- pnpm --filter @notelett/web run typecheck: passes
- pnpm --filter @notelett/web run test: 96/96 pass
- pnpm run verify: end-to-end green (backend 380/380, web 96/96, mobile 97/97)
BroadcastBanner: use {messages} destructure from listMessages(),
replace broken pollMessages callback with manual setInterval refetch,
use real InAppMessage type from @bytelyst/broadcast-client.
SurveyBanner: destructure {survey} from getActiveSurvey(), replace
broken pollSurveys callback with manual setInterval, pass typed
QuestionAnswer objects to submitAnswer() instead of raw strings,
align question type rendering with actual API types (single_choice,
rating, nps, text_short, text_long, etc).
Made-with: Cursor
Phase 3 (Web UX Polish) of the execution roadmap:
- Add reusable Pagination component for list views
- Fix getNoteDetail to avoid fetching all notes when workspaceId is unknown
- Add file upload button to ArtifactPanel using uploadArtifact() from blob-client
- Remove unused zustand and zod from web dependencies
- SSR safety already addressed via existing lazy-init patterns
Made-with: Cursor