PR #144 (`31da384`, 2026-06-13) shipped a `ReferenceError: useFocusTrap
is not defined` to production because the flat ESLint config did NOT
enable the core `no-undef` rule — only `react/jsx-no-undef` (which
catches undefined JSX components, not plain JS identifier references).
This PR closes that gap, narrowly.
## What changes
- `eslint.config.mjs`: enable `no-undef: 'error'` for source files +
define the ~40 browser / Node / Vitest globals the rule needs.
Hand-curated globals list (rejected pulling in the `globals` npm
package for one config block).
- 3 latent bugs surfaced + fixed (NOT silenced with disables):
| Site | Bug | Fix |
|------|-----|-----|
| `components/CollectionPageView.js:238` | `onClick={toggleFavorite}` — fn defined in `lib/use-collection-view.js:269` (collection-level favorite) but missing from the hook's `return {}` | Added to hook return + component destructure |
| `components/CollectionPageView.js:532` | `onTogglePublic={togglePublic}` — same pattern, fn at line 315 of the hook | Same shape: hook return + destructure |
| `components/ShareModal.js:99` | `fetchInvitedUsers()` scoped inside the useEffect body but called from `handleInvite` outside | Extracted to component scope via `useCallback`; effect dep array updated |
Bugs 1 + 2 broke the "Favorite collection" button and the public-toggle
in the Share modal on the collection-detail page. Bug 3 broke the
"refresh invitee list" path after a successful invite. None had been
flagged because the operator hadn't exercised those exact flows since
the relevant hooks were last refactored.
- `components/ShareModal.js`: also adds an eslint-disable for
`react-hooks/set-state-in-effect` on the moved `fetchInvitedUsers()`
call. Matches the canonical pattern in `pages/profile.js:90` —
async fetch; setState fires post-resolve, not synchronously to the
effect body.
## Why not pull in @eslint/js/recommended wholesale?
The recommended bundle also enables `no-unused-vars`,
`no-prototype-builtins`, `no-empty`, `no-cond-assign`, and ~10 others
— each would generate dozens of pre-existing violations on this
codebase. The right rule-by-rule sweep is the deferred
`adopt-eslint-recommended-set` convoy. This PR is scoped to the one
rule that would have caught PR #144's bug class.
## Test plan
- [x] `npm run lint` — clean (1 pre-existing unrelated warning on
`CollectionsPageView.js`'s `eslint-disable` directive — out of
scope)
- [x] `npm run test:run` — 25 files / 123 tests pass
- [ ] CI on this PR
- [ ] Post-merge: exercise the three formerly-broken paths (favorite a
collection from its detail page; toggle a collection public via
Share modal; invite a user and confirm the invitee list refreshes)
## Convoy doc
`.convoys/enable-no-undef-eslint-rule.md` documents the surfaced bugs,
D1 (no-undef only vs recommended bundle), D2 (hand-curated globals vs
`globals` package), risks, and acceptance.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sub-convoy #3 from .convoys/redesign-v2-from-mockups.md (umbrella
§ 7.3 — locked: sweep to ALL authenticated pages this convoy).
What ships:
- components/ui/TopSearchBar.js — the top horizontal chrome strip
from the mockup. Layout: prominent search input on left (with
magnifier icon + Cmd+K/Ctrl+K hint pill that adapts to platform)
+ notification bell with red badge (hidden when count=0) + mail
icon + compact user-menu chip (gradient-tile avatar + display name
+ chevron). Avatar reads user.username with a fallback initial.
Renders null for unauthenticated visitors (public marketing pages
use their own header).
- components/ui/CommandPaletteModal.js — the surface that opens on
⌘K / Ctrl+K. Single search input, auto-focused. Enter submits to
/cards?q=<query>. 3 quick-action buttons (Dashboard / Cards /
Scanner) below the input. Eschews live-result preview, recent-
search storage, and federated-search ranking; those are deferred
to a follow-up convoy per umbrella § 7.2.
- components/Layout.js: TopSearchBar mounted in the main-content
column ABOVE <main> for authenticated users (drops the legacy
showSearch prop dependency — the prop stays for back-compat but
no longer drives the header's visibility). Global keydown listener
attached at Layout scope, toggles the CommandPaletteModal on
⌘K/Ctrl+K (preventDefault on the shortcut so the browser's native
bookmark/search shortcut doesn't fire). The legacy <header>
block that rendered an inline search input is removed; that
surface is replaced by TopSearchBar + CommandPaletteModal.
- page-header-glass call-site sweep (umbrella § 7.3 contract:
"no call site references it after this convoy"):
- pages/dashboard.js
- pages/my-cards.js
- pages/community/collections.js
- components/CollectionsPageView.js
- components/CollectionPageView.js
- components/CardsPageView.js
Each `page-header-glass p-4 sm:p-6` is replaced with plain content
padding (`px-4 sm:px-6 pt-6 pb-2`). Page titles + actions stay
exactly where they were inside the content area; the glass chrome
that previously framed them is now provided by TopSearchBar above.
The .page-header-glass utility class stays in styles/globals.css
(a downstream sweep convoy can remove it once the unused-CSS lint
catches it).
- components/ui/index.js: barrel export updated with TopSearchBar +
CommandPaletteModal.
Lint fix:
- CommandPaletteModal initially used useEffect(setQuery(''), [open])
to reset the input on open; that hits the react-hooks/set-state-
in-effect rule (we added the rule in fix-auth-bypass Brief 5). Use
the "during render with previous-state tracking" pattern that
NavigationContent uses (lines 168-178 of components/Layout.js)
for the same purpose. No useEffect required.
Tests:
- npm run test:run: 113/113 (was 110; +3 new — implicit Layout
tree-render coverage of the new TopSearchBar mount paths).
- npm run lint: clean (1 pre-existing unused-disable warning).
- npm run build: green.
Next: sub-convoy #6 (card-grid outer-glow), #7 (dashboard layout
rebuild), #8 (right-rail Card Spotlight).
Co-authored-by: Cursor <cursoragent@cursor.com>
Three prior PRs (#97 panels, #98 backdrop-filter, #99 box-shadow
composition) all landed correct CSS but the user reported the design
"still looks the same" on /my-cards and /dashboard. CDP diagnostic on
both production and a local dev build found the actual root causes:
1) The body background is a flat warm-white (#fefcf8). Glass surfaces
sitting on a same-hue solid have nothing to blur — the backdrop-
filter renders but produces no visible distortion. The "glass" reads
as a flat 68%-alpha rectangle over an identical 100% color, which
is visually indistinguishable from a solid card.
2) Pages with populated data (the user's /my-cards with 2 cards) have
ZERO .glass-panel elements after PR #97's sweep. The 1 panel I
migrated on my-cards was the empty-state CTA — which never renders
when the user has cards. Dashboard had 5, but most of the visible
chrome (header strip, recent-lists card) was left flat by my prior
"page headers stay solid" call. That call was wrong: in this layout
the sidebar is the only persistent chrome, so the page header strip
has no glass-topbar to compete with.
Fix — two changes that compound:
(A) Hearth gradient on body. Soft warm radial-glow biased to the
bottom-left (the seat of a fire) + a second softer warm glow at
the top-right + a subtle vertical wash from cooler-top to warmer-
bottom. background-attachment: fixed so scrolling content slides
OVER the gradient (which is what creates the parallax-blur
behavior glass needs). Dark theme version replaces the secondary
radial with the purple accent. All values intentionally low-alpha
(8% ember, 6% gold in light; 16%/8% in dark) — readable, not
garish.
(B) New .page-header-glass utility for the recurring page-header-
strip pattern. tint=high (it spans full content width and needs
more visual weight than .glass-panel) + blur=20px + the rim-light
inset highlight + a 1px shadow as the bottom separator. Applied
via mechanical sweep to the 6 page header strips on:
- /my-cards
- /dashboard
- /community/collections
- components/CardsPageView (used by /cards)
- components/CollectionsPageView (used by /collections)
- components/CollectionPageView (used by /collection/[id])
After this PR, /my-cards (and every other authenticated page) will
show a distinctly translucent floating header strip with a soft top
highlight against the warm hearth gradient — i.e. an actual visible
design shift, even on data-grid pages with no inner cards.
Verified locally — npm run build, lint clean, vitest 104/104.
Co-authored-by: Cursor <cursoragent@cursor.com>