Skip to content

[PROJ-1498] Add web-next/ Next.js scaffold as a parallel frontend - #275

Merged
AndrewNordstrom merged 5 commits into
mainfrom
dev/PROJ-1498-land-web-next-next-js-scaffold-parallel-dir-tooling-reconcile
Jul 3, 2026
Merged

AndrewNordstrom merged 5 commits into
mainfrom
dev/PROJ-1498-land-web-next-next-js-scaffold-parallel-dir-tooling-reconcile

Conversation

@AndrewNordstrom

Copy link
Copy Markdown
Collaborator

What

P0.1 of PROJ-1497 (productize the Corgi site). Lands the dialed Next.js design as a new top-level web-next/ dir (Next.js 15 App Router). Purely additive — nothing existing is touched.

Why this is safe (prod bit-for-bit unchanged)

  • No changes to the live Vite web/, src/feed/server.ts, root verify, or .github/workflows/deploy.yml.
  • web-next/ is not wired into the serve path or CI/deploy in this slice — it just needs to exist and build.
  • git diff origin/main -- web/ src/feed/server.ts .github/workflows/deploy.yml is empty.

Scope

  • Copy the design source into web-next/.
  • Reconcile pnpm → npm: drop the pnpm lockfile, commit package-lock.json, pin the two floating "latest" deps (framer-motion@12.42.2, @emotion/is-prop-valid@1.4.0), rename the package to corgi-web.
  • Add axios@1.17.0 (matching web/) and @tanstack/react-query@^5.101.0 for later data wiring.
  • Add eslint-config-next with eslint.ignoreDuringBuilds so the build never gates on lint; drop the codegen generator meta tag.

Verification

  • cd web-next && npm install && npm run build → green (13 routes build).
  • Prod-untouched diff empty (above).

Deferred to later slices

Static-export config (output:'export' + trailingSlash), the /post/[uri] → /post?uri= refactor, @vercel/analytics removal, the helmet CSP fix for Next inline hydration scripts, the env-gated Fastify multi-page serve, and all data/auth wiring. The static-export build was already proven end-to-end in a throwaway probe (produces out/<route>/index.html for all 12 routes + out/404.html).

Closes PROJ-1498.

Introduce the productized Corgi design as a new top-level web-next/ dir (Next.js 15 App Router). Purely additive: the live Vite web/, the Fastify serve path, and deploy.yml are untouched, so production is unchanged.

Reconcile pnpm to npm: drop the pnpm lockfile, commit an npm package-lock, pin framer-motion (12.42.2) and @emotion/is-prop-valid (1.4.0), rename the package to corgi-web. Add axios (1.17.0, matching web/) and @tanstack/react-query (^5.101.0) for later data wiring. Add eslint-config-next with ignoreDuringBuilds so the build stays green, and drop the codegen generator meta tag.

Static-export config, the /post refactor, CSP changes, and data wiring are deferred to later slices.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added a full set of new app pages, including dashboard, demo, history, vote, admin, post explanation, consent, privacy policy, terms, and sign-in screens.
    • Introduced a richer landing experience with new homepage sections, onboarding content, FAQs, changelog, and calls to action.
    • Added reusable UI building blocks for forms, dialogs, navigation, charts, tables, sliders, toasts, sidebars, and more.
  • Bug Fixes

    • Improved layout consistency, mobile navigation, loading states, and accessibility across the app.

Walkthrough

This PR lands a complete parallel web-next/ Next.js 15 App Router application: build tooling and config, global Tailwind styling, a full shadcn/ui component library, domain-specific governance widgets (score breakdowns, radars, weight bars, sliders), app shell/navigation, marketing landing sections, and all mock-data-driven product pages (dashboard, vote, history, post explanation, admin, demo, legal pages). The existing web/ Vite frontend and deploy path remain untouched.

Changes

web-next Next.js scaffold

Layer / File(s) Summary
Build tooling and project configuration
web-next/.eslintrc.json, web-next/.gitignore, web-next/components.json, web-next/next.config.mjs, web-next/package.json, web-next/postcss.config.mjs, web-next/tailwind.config.ts, web-next/tsconfig.json, web-next/lib/utils.ts
ESLint/Next/Tailwind/TypeScript config, npm dependencies, and a cn classname merge helper are added.
Global styling and theme tokens
web-next/app/globals.css, web-next/styles/globals.css
Tailwind layers and CSS custom-property theme tokens (light/dark, shimmer) are defined.
Root layout and theme provider
web-next/app/layout.tsx, web-next/components/theme-provider.tsx
Root layout wires fonts/metadata/production-only Analytics; theme provider wraps next-themes.
Basic shadcn/ui primitives
web-next/components/ui/{button,badge,input,textarea,label,card,avatar,separator,skeleton,switch,checkbox,slider,progress,spinner,kbd,toggle,toggle-group}.tsx
Foundational styled wrapper components built on Radix primitives with cva variants.
Overlay, menu, and toast primitives
web-next/components/ui/{dialog,alert-dialog,sheet,drawer,popover,hover-card,tooltip,context-menu,dropdown-menu,menubar,navigation-menu,command,sidebar,toast,sonner,toaster,use-toast}.*, web-next/hooks/use-toast.ts
Overlay/menu UI wrappers plus an in-memory toast reducer/store and consuming hook.
Form and data-display primitives
web-next/components/ui/{accordion,alert,aspect-ratio,breadcrumb,button-group,calendar,carousel,chart,collapsible,empty,field,form,input-group,input-otp,item,pagination,radio-group,resizable,scroll-area,select,table,tabs}.tsx
Additional shadcn primitives for forms, charts, and tabular/paginated data.
Domain-specific governance widgets
web-next/components/ui/{score-breakdown,score-radar,weight-bar,topic-slider,linked-slider,keyword-input,status-chip,state-kit}.tsx, web-next/components/legal-layout.tsx
Widgets for score visualization, weighted sliders, keyword chips, status chips, skeleton/empty/error states, and a legal document layout with ToC.
App shell, header, and sign-in
web-next/components/{app-shell,header,sign-in-dialog}.tsx
Sticky shell/header navigation with mobile portal menus and body-scroll locking, plus a mock sign-in form/dialog.
Landing page and marketing sections
web-next/app/page.tsx, web-next/components/{hero-section,social-proof,get-started-section,bento-section,changelog-section,faq-section,cta-section,footer-section,dashboard-preview,animated-section}.tsx
Composes the landing page from marketing sections with framer-motion entrance animations.
Product pages (dashboard, vote, history, post, demo)
web-next/app/{dashboard,vote,history,post/[uri],demo}/page.tsx
Mock-data-driven transparency dashboard, vote workbench, governance ledger, ranking explanation, and guided demo walkthrough.
Admin console
web-next/app/admin/page.tsx
Round lifecycle, weights override, content filters, topics, audit log, feed health, and announcements panels with confirmation modals.
Legal and auxiliary pages
web-next/app/{privacy,tos,research-consent,sign-in}/page.tsx
Privacy/ToS content via LegalLayout, a research-consent state machine flow, and a sign-in preview page.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related issues

  • PROJ-1498: This PR is exactly the described web-next scaffold and npm tooling reconcile (parallel web-next/ app, components, hooks, config).
  • PROJ-1497: This PR is the P0 slice of the described frontend productization epic, landing the Next.js design as the new web-next/ directory.

Suggested labels: documentation, javascript, dependencies

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Clear and specific: it names the new web-next Next.js scaffold and the parallel frontend scope.
Description check ✅ Passed The description matches the changes, covering the new web-next app, npm reconcile, and build intent.
Linked Issues check ✅ Passed The PR adds the web-next scaffold and tooling changes the issue asks for, while leaving production paths untouched.
Out of Scope Changes check ✅ Passed No clear unrelated code changes appear outside the scaffold/tooling scope; the added files stay within web-next and configs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/PROJ-1498-land-web-next-next-js-scaffold-parallel-dir-tooling-reconcile
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch dev/PROJ-1498-land-web-next-next-js-scaffold-parallel-dir-tooling-reconcile

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Notion


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels Jul 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 53

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web-next/hooks/use-mobile.ts (1)

1-20: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate hook implementation — consolidate with components/ui/use-mobile.tsx.

This file is byte-identical to web-next/components/ui/use-mobile.tsx. sidebar.tsx imports only from @/hooks/use-mobile, so the components/ui/use-mobile.tsx copy appears to be dead/orphaned code. Keeping two copies of the same logic risks future divergence (one gets bugfixed, the other doesn't) and confuses which is canonical.

AI agent prompt: "Search the web-next/ codebase for all imports of useIsMobile. Keep the canonical implementation in web-next/hooks/use-mobile.ts, delete web-next/components/ui/use-mobile.tsx, and update any remaining imports to @/hooks/use-mobile."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-next/hooks/use-mobile.ts` around lines 1 - 20, `useIsMobile` is
duplicated in two places, and `components/ui/use-mobile.tsx` appears to be an
orphaned copy. Keep `useIsMobile` in `hooks/use-mobile.ts` as the canonical
implementation, remove the duplicate `components/ui/use-mobile.tsx`, and update
any remaining imports across the codebase to point to `@/hooks/use-mobile`
(including callers like `sidebar.tsx`).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-next/app/admin/page.tsx`:
- Around line 183-188: The LIFECYCLE array in admin/page.tsx has two entries
with the same phase value, which makes isCurrentPhase match both rows at once.
Update the lifecycle definitions so each step has a unique phase or change the
“Live” row to a distinct phase, and then keep PanelCurrentRound’s active-row
check tied to the unique phase mapping. After the fix, add a test for
PanelCurrentRound with phase "running" that verifies only one lifecycle row gets
the active styling.
- Around line 109-140: Update ConfirmModal in the admin page so keyboard users
can dismiss and operate it safely: add Escape-to-close handling and move initial
focus into the dialog when it opens. Use the existing ConfirmModal component and
its onCancel/onConfirm callbacks to wire an onKeyDown Escape handler on the
dialog container, and ensure focus lands on a control like the Cancel button
when the modal mounts for all destructive flows that reuse this component.

In `@web-next/app/demo/page.tsx`:
- Around line 246-260: Update StepCounterfactual in page.tsx so the static
subtext is rendered as normal JSX text instead of using dangerouslySetInnerHTML,
since all box.sub values are plain strings and one currently contains a literal
&apos; that shows up incorrectly. Fix the Community-governed rank entry by using
a normal apostrophe in the string, and keep the rendering path for the summary
items simple and safe by removing the innerHTML usage around the items list. Add
a regression test for StepCounterfactual that checks the rendered text contains
“community's weights applied”.

In `@web-next/app/globals.css`:
- Around line 1-81: The duplicate stylesheet in the old globals file is unused
and should be removed to avoid theme-token drift. Verify that the app continues
to use the stylesheet referenced by layout.tsx and components.json, then delete
this extra globals file or move any needed tokens into app/globals.css, using
the globals.css filenames and the existing Tailwind base layer setup as the key
references.

In `@web-next/app/history/page.tsx`:
- Around line 304-324: The `AuditRow` component is blindly casting
`entry.details.handle` to a string, which can break once real API data arrives.
Replace the `as string` usage with a runtime type check in `AuditRow` so the
display falls back to `entry.actor_did` unless `entry.details.handle` is
actually a string. Keep the fix localized to the `entry.details.handle`
rendering path and avoid assuming the `details` payload shape.

In `@web-next/app/layout.tsx`:
- Around line 1-41: RootLayout is missing the app-wide ThemeProvider, so
next-themes context is never mounted and useTheme-dependent components like
Sonner cannot work correctly. Import and wrap the layout children with
ThemeProvider in RootLayout, and add suppressHydrationWarning to the html
element to avoid hydration mismatches from the theme class. Keep the change
centered around RootLayout and the existing ThemeProvider component.

In `@web-next/app/page.tsx`:
- Around line 17-36: Remove the duplicate sticky wrapper and local scroll state
from the page component so `Header` owns all sticky positioning and scroll-based
styling; in `page.tsx`, delete the extra `div` around `<Header />` plus the
`useState`/`useEffect` scroll listener logic, and rely on `Header`’s existing
`sticky top-0 z-50` behavior and internal `useScroll(10)` handling to avoid
conflicting background/blur states.

In `@web-next/app/post/`[uri]/page.tsx:
- Around line 317-323: The copyUri function currently calls
navigator.clipboard.writeText without handling rejections, so failures are
silent and can trigger unhandled promise rejections. Update copyUri in page.tsx
to handle the promise failure path explicitly by adding error handling to the
writeText call, and use the existing copied state or a new error state to avoid
showing success when the copy fails.
- Around line 595-611: The dev state switcher in Page is only visually faded, so
it remains visible and interactive in production. Update the wrapper around the
PageState buttons to actually hide it using the same pattern as the other pages
in this PR, and add the corresponding accessibility/interaction guards like
aria-hidden and pointer-events suppression. Keep the logic centered on the
existing Page component state switcher so the buttons cannot be seen or clicked
in production.

In `@web-next/app/research-consent/page.tsx`:
- Around line 67-75: In `handleAgree` and `handleDecline` in the research
consent page, the delayed `setState` calls are not cancelled on unmount, which
can trigger updates after navigation away from the page. Update this flow to
track the timeout ID and clean it up when the component unmounts, matching the
pattern used in `sign-in-dialog.tsx`. Also ensure any future real submission
logic is guarded so the delayed success state cannot fire after the page is gone
or cause duplicate submits.
- Around line 146-148: The Link label in the research-consent page has a dead
ternary because both branches of the agreed check render the same text. Update
the JSX in the research-consent page’s Link so it either uses a single plain
string if no distinction is intended, or change the false branch to the correct
alternate copy; use the agreed condition and the Link label text as the key
symbols to locate it.
- Around line 50-57: The local Spinner in research-consent/page.tsx duplicates
the shared loading component and lacks the accessible role/name provided by
Spinner from `@/components/ui/spinner.tsx`. Remove the inline Spinner helper,
import and reuse the shared Spinner component in the saving state paths, and
keep the existing loading UI behavior while ensuring the accessible loading
announcement comes from the shared component.
- Around line 123-128: The “Recorded at” value is being generated during render
in the research consent success view, so it can change on every re-render
instead of reflecting when consent was submitted. Update the component in
research-consent/page.tsx to capture a timestamp once when the consent flow
transitions to success (for example in the same place that schedules the success
state change after submission) and store it in state, then render that stored
value instead of calling new Date().toLocaleString() inline.

In `@web-next/components.json`:
- Around line 6-12: Update the shadcn setup in components.json so the tailwind
config points to the existing Tailwind v3 config via the tailwind.config field
instead of being empty, using the components.json tailwind block as the locator.
Also add a component-generation smoke test for the v3 setup to verify npx shadcn
add reads the config and applies theme updates correctly.

In `@web-next/components/AiCodeReviews.module.css`:
- Around line 1-121: The AiCodeReviews.module.css file is currently unused
because ai-code-reviews.tsx never imports or applies any of its class names, so
its theme-specific rules and styling changes have no effect. Either remove this
dead CSS module or wire it into ai-code-reviews.tsx by importing the module and
using the relevant styles.aiCodeReviews, styles.messageBox, styles.codeBlock,
and related classes on the rendered elements. If you keep it, also make sure the
component sets the expected data-theme attribute so the dark-mode selectors
actually apply.

In `@web-next/components/animated-section.tsx`:
- Around line 6-9: The AnimatedSectionProps wrapper is typed with
HTMLAttributes<HTMLDivElement>, which does not match the props accepted by
motion.div and causes conflicts for Motion event handlers. Update the
AnimatedSectionProps type in animated-section.tsx to use HTMLMotionProps<"div">
(or explicitly omit the conflicting onDrag* and onAnimation* keys) so forwarding
props stays type-safe. Add a type test around AnimatedSection/motion.div usage
to verify onDragEnd and onAnimationEnd are accepted with the correct Motion
signatures.

In `@web-next/components/app-shell.tsx`:
- Around line 186-258: The mobile menu portal in app-shell lacks keyboard
dismissal and focus containment, so fix the overlay behavior in the mobile nav
render path by adding an Escape key handler and a focus trap while
mobileMenuOpen is true. Use the existing app-shell state setters like
setMobileMenuOpen and the portal container id/markup around the createPortal
block to wire up a keydown listener and keep tab focus inside the menu until it
closes. Ensure the focus trap is released when the menu unmounts or closes, and
that Escape reliably dismisses the overlay for keyboard users.
- Around line 145-151: The sign-out buttons rendered by AppShell are currently
placeholders with no click behavior, so wire them to the existing auth sign-out
flow or mark them explicitly disabled until auth is implemented. Update the
Button instances in AppShell (including the AppUser-related sign-out control) to
use the real sign-out handler instead of rendering inert UI.
- Around line 70-78: The body scroll lock in AppShell is only tied to
mobileMenuOpen, so it can clear too early when signInOpen is opened from the
menu. Update the scroll-lock effect in AppShell to use a single combined flag
like shouldLock = mobileMenuOpen || signInOpen, and apply it consistently in the
effect and cleanup. Also add a regression test around the AppShell interaction
where opening the mobile menu, clicking Connect Bluesky, then opening and
closing the sign-in dialog keeps the body locked until both states are closed.

In `@web-next/components/bento/ai-code-reviews.tsx`:
- Around line 67-92: The duplicated JSX in ai-code-reviews.tsx has a theme color
drift bug: the foreground box icon hardcodes `#22C55E` while the background box
already uses var(--ai-primary-color). Refactor the repeated code-snippet
rendering into a shared local component or data-driven block so both boxes use
the same stroke source, and update the foreground icon in the matching success
case to reference the theme variable instead of a fixed green value.
- Around line 16-28: The AI code reviews illustration currently puts a native
button inside a `role="img"` container, which hides any interactive descendant
from the accessibility tree; update the `ai-code-reviews` component so the
`Apply changes` control is not a real button while the parent keeps
`role="img"`, or remove `role="img"` and mark decorative pieces `aria-hidden` so
the control stays accessible. Also make sure the control has an explicit
`type="button"` if it remains a `<button>`, and verify the relevant markup
around the illustration and the `Apply changes` element stays consistent with
the chosen accessibility approach.

In `@web-next/components/bento/mcp-connectivity-illustration.tsx`:
- Around line 116-121: The MCP connectivity illustration is using a raw img
element for integration.icon, which should be replaced with Next.js next/image
in the mcp-connectivity-illustration component. Update the image rendering in
the integration grid to use next/image with the existing src and alt data, and
preserve the current styling/contain behavior so the icon layout remains
unchanged.

In `@web-next/components/bento/real-time-previews.tsx`:
- Around line 247-252: The `@keyframes pulse` block inside `RealTimePreviews` is
dead because nothing in this component applies `animation: pulse` anymore.
Remove the unused `<style jsx>` keyframe definition from
`real-time-previews.tsx`, or if the live/sync indicators are intentionally being
restored later, keep it only with a clear `TODO` comment near the related
indicator markup so its purpose is explicit.

In `@web-next/components/faq-section.tsx`:
- Around line 48-84: The FAQ accordion trigger in FAQItem is currently a
clickable div, so make it a native button in the FAQItem component and keep the
existing styling/animation intact. Add aria-expanded bound to isOpen and ensure
the toggle still calls onToggle from the button click handler. Remove the
now-unneeded preventDefault-based handleClick and any React.MouseEvent-specific
logic/imports. Also add RTL coverage for keyboard interaction so Enter/Space
toggles the item and aria-expanded reflects the open state.

In `@web-next/components/footer-section.tsx`:
- Line 1: The FooterSection component is fully static and should remain a Server
Component, so remove the "use client" directive from FooterSection in
web-next/components/footer-section.tsx. Keep the component pure markup with no
client-only behavior, and verify the component still renders correctly as part
of the server-side App Router output.

In `@web-next/components/header.tsx`:
- Around line 24-50: The mobile menu and hamburger icon logic in
AnimatedMenuIcon is duplicated with AppShell, so extract the shared behavior
into reusable components/hooks under components/. Move the scroll-lock effect
and createPortal-based mobile menu markup into a shared MobileMenuPortal plus a
useScrollLock(open) hook, and reuse the same AnimatedMenuIcon implementation in
both Header and AppShell to keep behavior and accessibility changes in one
place.

In `@web-next/components/hero-section.tsx`:
- Around line 34-51: The decorative grid in hero-section’s SVG is built with
nested map loops that generate hundreds of individual rect nodes, which is
unnecessary for a static repeating background. Refactor the grid rendering in
HeroSection to use an SVG pattern definition (with a single reusable tile) and
apply it via fill/mask instead of rendering each cell separately. Keep the
visual spacing, stroke, and opacity consistent with the current grid effect
while reducing DOM weight and parse time.

In `@web-next/components/pricing-section.tsx`:
- Around line 144-147: The plan label logic in pricing-section is fragile
because it uses pricingPlans.indexOf(plan) and a hardcoded plan name check to
infer the previous tier. Update the rendering in the .map((plan, index) => ...)
block to use the existing index directly, and treat index === 0 as the “Get
started today” case instead of checking plan.name === "Member". Keep the
“Everything in X +” label derived from the previous item via the current index
so reordering or renaming plans does not break the display.

In `@web-next/components/sign-in-dialog.tsx`:
- Around line 15-19: The SignInDialog form state is persisting across
close/reopen because the component stays mounted under Radix Dialog. Update
SignInDialog to clear handle, password, and showPassword when the dialog closes
or after a completed sign-in flow, using the SignInDialog and Dialog
open/onOpenChange handling to reset state reliably.
- Around line 21-26: The handleSubmit flow in SignInDialog leaves an uncanceled
async update behind, so loading can be set after the dialog unmounts or closes.
Update handleSubmit to keep track of the pending timer or future auth request
and clean it up on unmount/close, and guard setLoading so it only runs while the
component is still mounted. Use the existing handleSubmit and setLoading logic
in sign-in-dialog.tsx as the place to wire this in, and ensure reopening the
dialog does not reuse a stale pending submission.

In `@web-next/components/testimonial-grid-section.tsx`:
- Around line 59-60: The `padding` ternary in `testimonial-grid-section.tsx` is
redundant because both branches in the `isLarge` check return the same value.
Update the `padding` assignment in the `TestimonialGridSection` logic to either
use a single constant value if large cards do not need special spacing, or
restore a distinct large-card padding value if that was the intended behavior.
Locate the fix around the `isLarge` and `padding` variables in the component.

In `@web-next/components/ui/carousel.tsx`:
- Around line 88-99: The carousel keyboard handler in handleKeyDown is
intercepting ArrowLeft/ArrowRight even when focus is inside nested interactive
content. Update the logic in carousel.tsx so it only calls preventDefault,
scrollPrev, or scrollNext when the event target is the carousel container
itself, not an input, textarea, select, or contenteditable element. Use the
existing handleKeyDown/onKeyDownCapture wiring and add a guard based on
event.target or the active element before handling arrow keys. Also add a test
that focuses an input inside CarouselItem and verifies arrow keys are not
swallowed and do not trigger scrolling.
- Around line 109-121: The Carousel effect in
`web-next/components/ui/carousel.tsx` registers both `api.on('reInit',
onSelect)` and `api.on('select', onSelect)`, but the cleanup only removes
`select`, leaving `reInit` listeners behind on reruns. Update the
`React.useEffect` cleanup in `Carousel` to unregister the `reInit` handler as
well, matching the existing `select` teardown so both subscriptions are removed
whenever `api` or `onSelect` changes.

In `@web-next/components/ui/empty.tsx`:
- Around line 71-82: EmptyDescription has a tag/type mismatch: it is typed with
React.ComponentProps<'p'> but renders a div. Update the component to keep the
contract consistent by either changing the rendered element in EmptyDescription
to a p or changing its props type to React.ComponentProps<'div'>, and make sure
the component name and data-slot stay unchanged so callers remain easy to
locate.

In `@web-next/components/ui/form.tsx`:
- Around line 44-65: `useFormField` currently performs its guard too late and
the `FormFieldContext`/`FormItemContext` defaults make `if (!fieldContext)`
unreachable, so misuse outside `<FormField>`/`<FormItem>` can produce undefined
state and ids. In `useFormField`, check the contexts before calling
`getFieldState` or reading `itemContext.id`, and update
`FormFieldContext`/`FormItemContext` to use `null` defaults so the existing
guard can actually throw the intended error. Add a test around
`FormLabel`/`FormControl` rendered outside the `FormField`/`FormItem` tree to
verify it fails clearly instead of generating `undefined-form-item`-style ids.

In `@web-next/components/ui/input-group.tsx`:
- Around line 60-80: The InputGroupAddon component currently lets a
consumer-provided onClick override the built-in focus behavior because props are
spread after the handler. Update InputGroupAddon so the internal click-to-focus
logic is composed with any external onClick passed in via
React.ComponentProps<'div'>, ensuring both handlers run. Keep the existing focus
behavior when clicking non-button content and preserve the special-case button
check while using the component’s existing symbols (InputGroupAddon,
inputGroupAddonVariants, cn) to locate the fix.

In `@web-next/components/ui/input-otp.tsx`:
- Around line 33-58: InputOTPSlot currently destructures
inputOTPContext.slots[index] without checking that the slot exists, which can
throw when index is out of range. Update the InputOTPSlot component to guard
access to OTPInputContext.slots by validating index before destructuring, and
render a safe fallback (or null) when the slot is missing. Keep the fix
localized to InputOTPSlot and its use of OTPInputContext.

In `@web-next/components/ui/kbd.tsx`:
- Around line 18-26: The KbdGroup component is typed as a div but renders a kbd
element, so update its implementation in KbdGroup to use a proper container
element and keep the props type aligned with that tag. Adjust the JSX and
React.ComponentProps type together so consumers get correct semantics and nested
Kbd usage no longer produces redundant kbd markup.

In `@web-next/components/ui/linked-slider.tsx`:
- Around line 45-51: In LinkedSlider’s rebalance logic, handle the single-signal
case explicitly before computing the even split so `others.length === 0` never
reaches `remaining / others.length`. Update the `rebalance` path in
`linked-slider.tsx` to return only the changed signal with the clamped value
when there are no other signals, and keep the existing even-distribution
behavior for `otherSum === 0` when `others.length > 0`. Also add/adjust tests
around `rebalance` to cover a one-item `signals` array and the zero-value
two-signal case to ensure no `NaN` or `Infinity` appears.

In `@web-next/components/ui/menubar.tsx`:
- Around line 203-217: The MenubarShortcut component is setting the wrong static
property name, so React never picks up the display label. Update the
MenubarShortcut assignment to use the correct case-sensitive displayName
property on the MenubarShortcut component so DevTools and tests recognize it
consistently with the other components in this file.

In `@web-next/components/ui/pagination.tsx`:
- Around line 42-59: PaginationLink currently renders a bare anchor, which will
force full page reloads once real routes are used. Update the PaginationLink
component in pagination.tsx to use next/link’s Link instead of <a>, while
preserving href passthrough, aria-current handling, and the existing
cn/buttonVariants className merging. Keep the PaginationLinkProps shape
compatible so callers can continue passing link props through unchanged.

In `@web-next/components/ui/score-breakdown.tsx`:
- Around line 30-31: The `displayTotal` logic in `score-breakdown.tsx` is
masking negative `total_score` values while the comment only mentions the zero
edge case; either update the comment and behavior in `ScoreBreakdown` to
explicitly support clamping negatives if that is intended, or remove the
`Math.max(0, total_score)` clamp so legitimate negative totals are displayed
accurately.

In `@web-next/components/ui/score-radar.tsx`:
- Around line 27-41: `GingerDot` is dropping the `r` and `fill` values passed in
by Recharts and hardcoding its own appearance. Update the `GingerDot` props
destructuring and circle rendering so the component uses the incoming `r` and
`fill` values (with the existing defaults as fallback) instead of always forcing
a fixed radius and color. Keep the change localized to `GingerDot` in
`score-radar.tsx`.
- Around line 65-71: The ScoreRadar data mapping is assuming post/governance are
already in the 0–1 range, but values can come from untrusted sources and exceed
that domain. Update the ScoreRadar component’s signals-to-data transformation to
clamp both s.post and s.governance to the documented bounds before converting to
percentages, using the ScoreRadar and RadarSignal mapping logic so the chart
never renders above 100%.

In `@web-next/components/ui/sidebar.tsx`:
- Around line 655-691: `SidebarMenuSkeleton` is generating a random
`--skeleton-width` during render, which can differ between SSR and hydration.
Update the `width` logic in `SidebarMenuSkeleton` to use a client-only
effect/state pattern or a deterministic value so it is not computed on the
server. Keep the existing `Skeleton` and `data-sidebar="menu-skeleton-text"`
structure, but ensure the style value is set only after mount to avoid hydration
mismatches.

In `@web-next/components/ui/spinner.tsx`:
- Around line 1-5: The Spinner component currently references
React.ComponentProps<'svg'> without importing React, which breaks type-checking
in this module. Update the Spinner definition to use an explicit React type
import in spinner.tsx, either by importing ComponentProps from react and using
that type in Spinner, or by importing React as a namespace; keep the fix local
to Spinner and preserve the existing props shape.

In `@web-next/components/ui/status-chip.tsx`:
- Line 5: Export the shared Phase union from StatusChip and make consumers
import it instead of redefining their own local union, so phase values stay in
sync across StatusChip and the history page. In StatusChipProps/StatusChip,
remove the unsafe optional-chaining + `as Phase` path and use a proper type
guard against PHASE_CONFIG keys before selecting the config, keeping the
fallback render for unknown values. Update the history page to reference the
shared Phase type directly so compile-time mismatches are caught.

In `@web-next/components/ui/topic-slider.tsx`:
- Around line 23-24: The Reset button visibility in TopicSlider is driven by
duplicated local state that can get out of sync with the value prop. Update the
TopicSlider component to stop using touched as separate state, remove the
useState-based initialization, and derive the Reset control solely from
isNeutral (computed from value) so external prop updates always reflect
correctly. Keep handleChange/handleReset behavior intact, and adjust any label
styling or conditional rendering that currently depends on touched.

In `@web-next/components/ui/use-toast.ts`:
- Around line 8-9: The timer cleanup in use-toast.ts is leaking state because
addToRemoveQueue leaves dismissed toasts in toastTimeouts and memoryState.toasts
for TOAST_REMOVE_DELAY, which is far too long. Update the toast removal flow so
repeated dismissals or re-triggers clear any existing timeout for the same toast
ID before scheduling a new one, and ensure the timeout/map entry is removed
promptly once cleanup runs. Use the existing symbols addToRemoveQueue,
toastTimeouts, memoryState, and TOAST_REMOVE_DELAY to keep the cleanup
idempotent and prevent stale timers from accumulating.
- Around line 171-183: The useToast hook is re-subscribing its listener on every
state update because the React.useEffect dependency array incorrectly includes
state. Update the effect in useToast so it depends only on the stable setState
callback (or uses an empty dependency array if appropriate), keeping the
listeners.push/remove logic tied to mount and unmount only.

In `@web-next/hooks/use-toast.ts`:
- Line 9: The dismissed toast cleanup delay in use-toast is far too long,
leaving open: false entries stuck in state and toastTimeouts much longer than
intended. Update TOAST_REMOVE_DELAY in use-toast.ts to a short sane value and
make sure the toast removal path in the reducer/timeout logic still clears the
toast from state and the toastTimeouts entry promptly. Add a Jest test around
the toast lifecycle (DISMISS_TOAST/REMOVE_TOAST in useToast) with fake timers to
verify a dismissed toast is removed within the expected window.
- Around line 174-182: The `useEffect` in `useToast` is re-running on every
`state` change even though it only manages the `listeners` subscription, causing
unnecessary remove/re-add churn. Update the dependency array in the
`React.useEffect` inside `use-toast.ts` to subscribe once on mount and clean up
on unmount, since the effect body does not use `state`.

In `@web-next/next.config.mjs`:
- Around line 3-8: The Next.js build config currently disables both
type-checking and lint enforcement, so `next build` can pass despite errors.
Update `typescript.ignoreBuildErrors` and `eslint.ignoreDuringBuilds` in
`next.config.mjs` to enforce build-time checks, or clearly gate this
scaffold-only exception behind a temporary tracked mechanism if it must remain
short-term.

---

Outside diff comments:
In `@web-next/hooks/use-mobile.ts`:
- Around line 1-20: `useIsMobile` is duplicated in two places, and
`components/ui/use-mobile.tsx` appears to be an orphaned copy. Keep
`useIsMobile` in `hooks/use-mobile.ts` as the canonical implementation, remove
the duplicate `components/ui/use-mobile.tsx`, and update any remaining imports
across the codebase to point to `@/hooks/use-mobile` (including callers like
`sidebar.tsx`).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4f326c03-9106-408f-a27c-9dde6be9e68b

📥 Commits

Reviewing files that changed from the base of the PR and between a084de6 and b0a739f.

⛔ Files ignored due to path filters (43)
  • web-next/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • web-next/public/apple-icon.png is excluded by !**/*.png
  • web-next/public/icon-dark-32x32.png is excluded by !**/*.png
  • web-next/public/icon-light-32x32.png is excluded by !**/*.png
  • web-next/public/icon.svg is excluded by !**/*.svg
  • web-next/public/images/ai-code-reviews.png is excluded by !**/*.png
  • web-next/public/images/avatars/albert-flores.png is excluded by !**/*.png
  • web-next/public/images/avatars/annette-black.png is excluded by !**/*.png
  • web-next/public/images/avatars/cameron-williamson.png is excluded by !**/*.png
  • web-next/public/images/avatars/cody-fisher.png is excluded by !**/*.png
  • web-next/public/images/avatars/darlene-robertson.png is excluded by !**/*.png
  • web-next/public/images/avatars/dianne-russell.png is excluded by !**/*.png
  • web-next/public/images/avatars/robert-fox.png is excluded by !**/*.png
  • web-next/public/images/corgi-icon.svg is excluded by !**/*.svg
  • web-next/public/images/dashboard-preview.png is excluded by !**/*.png
  • web-next/public/images/deployment-easy.png is excluded by !**/*.png
  • web-next/public/images/guillermo-rauch.png is excluded by !**/*.png
  • web-next/public/images/large-card-background.svg is excluded by !**/*.svg
  • web-next/public/images/mcp-connectivity.png is excluded by !**/*.png
  • web-next/public/images/mcp-integrations/figma.svg is excluded by !**/*.svg
  • web-next/public/images/mcp-integrations/nextjs.svg is excluded by !**/*.svg
  • web-next/public/images/mcp-integrations/react.svg is excluded by !**/*.svg
  • web-next/public/images/mcp-integrations/resend.svg is excluded by !**/*.svg
  • web-next/public/images/mcp-integrations/shadcn.svg is excluded by !**/*.svg
  • web-next/public/images/mcp-integrations/tailwind-css.svg is excluded by !**/*.svg
  • web-next/public/images/new-product-ui.jpeg is excluded by !**/*.jpeg
  • web-next/public/images/one-click-integrations.png is excluded by !**/*.png
  • web-next/public/images/parallel-coding-agents.png is excluded by !**/*.png
  • web-next/public/images/product-ui.jpeg is excluded by !**/*.jpeg
  • web-next/public/images/realtime-coding-previews.png is excluded by !**/*.png
  • web-next/public/logos/logo01.svg is excluded by !**/*.svg
  • web-next/public/logos/logo02.svg is excluded by !**/*.svg
  • web-next/public/logos/logo03.svg is excluded by !**/*.svg
  • web-next/public/logos/logo04.svg is excluded by !**/*.svg
  • web-next/public/logos/logo05.svg is excluded by !**/*.svg
  • web-next/public/logos/logo06.svg is excluded by !**/*.svg
  • web-next/public/logos/logo07.svg is excluded by !**/*.svg
  • web-next/public/logos/logo08.svg is excluded by !**/*.svg
  • web-next/public/placeholder-logo.png is excluded by !**/*.png
  • web-next/public/placeholder-logo.svg is excluded by !**/*.svg
  • web-next/public/placeholder-user.jpg is excluded by !**/*.jpg
  • web-next/public/placeholder.jpg is excluded by !**/*.jpg
  • web-next/public/placeholder.svg is excluded by !**/*.svg
📒 Files selected for processing (115)
  • web-next/.eslintrc.json
  • web-next/.gitignore
  • web-next/app/admin/page.tsx
  • web-next/app/dashboard/page.tsx
  • web-next/app/demo/page.tsx
  • web-next/app/globals.css
  • web-next/app/history/page.tsx
  • web-next/app/layout.tsx
  • web-next/app/page.tsx
  • web-next/app/post/[uri]/page.tsx
  • web-next/app/privacy/page.tsx
  • web-next/app/research-consent/page.tsx
  • web-next/app/sign-in/page.tsx
  • web-next/app/tos/page.tsx
  • web-next/app/vote/page.tsx
  • web-next/components.json
  • web-next/components/AiCodeReviews.module.css
  • web-next/components/animated-section.tsx
  • web-next/components/app-shell.tsx
  • web-next/components/bento-section.tsx
  • web-next/components/bento/ai-code-reviews.tsx
  • web-next/components/bento/easy-deployment.tsx
  • web-next/components/bento/mcp-connectivity-illustration.tsx
  • web-next/components/bento/one-click-integrations-illustration.tsx
  • web-next/components/bento/parallel-agents.tsx
  • web-next/components/bento/real-time-previews.tsx
  • web-next/components/changelog-section.tsx
  • web-next/components/cta-section.tsx
  • web-next/components/dashboard-preview.tsx
  • web-next/components/faq-section.tsx
  • web-next/components/footer-section.tsx
  • web-next/components/get-started-section.tsx
  • web-next/components/header.tsx
  • web-next/components/hero-section.tsx
  • web-next/components/large-testimonial.tsx
  • web-next/components/legal-layout.tsx
  • web-next/components/pricing-section.tsx
  • web-next/components/sign-in-dialog.tsx
  • web-next/components/social-proof.tsx
  • web-next/components/testimonial-grid-section.tsx
  • web-next/components/theme-provider.tsx
  • web-next/components/ui/accordion.tsx
  • web-next/components/ui/alert-dialog.tsx
  • web-next/components/ui/alert.tsx
  • web-next/components/ui/aspect-ratio.tsx
  • web-next/components/ui/avatar.tsx
  • web-next/components/ui/badge.tsx
  • web-next/components/ui/breadcrumb.tsx
  • web-next/components/ui/button-group.tsx
  • web-next/components/ui/button.tsx
  • web-next/components/ui/calendar.tsx
  • web-next/components/ui/card.tsx
  • web-next/components/ui/carousel.tsx
  • web-next/components/ui/chart.tsx
  • web-next/components/ui/checkbox.tsx
  • web-next/components/ui/collapsible.tsx
  • web-next/components/ui/command.tsx
  • web-next/components/ui/context-menu.tsx
  • web-next/components/ui/dialog.tsx
  • web-next/components/ui/drawer.tsx
  • web-next/components/ui/dropdown-menu.tsx
  • web-next/components/ui/empty.tsx
  • web-next/components/ui/field.tsx
  • web-next/components/ui/form.tsx
  • web-next/components/ui/hover-card.tsx
  • web-next/components/ui/input-group.tsx
  • web-next/components/ui/input-otp.tsx
  • web-next/components/ui/input.tsx
  • web-next/components/ui/item.tsx
  • web-next/components/ui/kbd.tsx
  • web-next/components/ui/keyword-input.tsx
  • web-next/components/ui/label.tsx
  • web-next/components/ui/linked-slider.tsx
  • web-next/components/ui/menubar.tsx
  • web-next/components/ui/navigation-menu.tsx
  • web-next/components/ui/pagination.tsx
  • web-next/components/ui/popover.tsx
  • web-next/components/ui/progress.tsx
  • web-next/components/ui/radio-group.tsx
  • web-next/components/ui/resizable.tsx
  • web-next/components/ui/score-breakdown.tsx
  • web-next/components/ui/score-radar.tsx
  • web-next/components/ui/scroll-area.tsx
  • web-next/components/ui/select.tsx
  • web-next/components/ui/separator.tsx
  • web-next/components/ui/sheet.tsx
  • web-next/components/ui/sidebar.tsx
  • web-next/components/ui/skeleton.tsx
  • web-next/components/ui/slider.tsx
  • web-next/components/ui/sonner.tsx
  • web-next/components/ui/spinner.tsx
  • web-next/components/ui/state-kit.tsx
  • web-next/components/ui/status-chip.tsx
  • web-next/components/ui/switch.tsx
  • web-next/components/ui/table.tsx
  • web-next/components/ui/tabs.tsx
  • web-next/components/ui/textarea.tsx
  • web-next/components/ui/toast.tsx
  • web-next/components/ui/toaster.tsx
  • web-next/components/ui/toggle-group.tsx
  • web-next/components/ui/toggle.tsx
  • web-next/components/ui/tooltip.tsx
  • web-next/components/ui/topic-slider.tsx
  • web-next/components/ui/use-mobile.tsx
  • web-next/components/ui/use-toast.ts
  • web-next/components/ui/weight-bar.tsx
  • web-next/hooks/use-mobile.ts
  • web-next/hooks/use-toast.ts
  • web-next/lib/utils.ts
  • web-next/next.config.mjs
  • web-next/package.json
  • web-next/postcss.config.mjs
  • web-next/styles/globals.css
  • web-next/tailwind.config.ts
  • web-next/tsconfig.json

Comment thread web-next/app/admin/page.tsx
Comment thread web-next/app/admin/page.tsx
Comment thread web-next/app/demo/page.tsx
Comment thread web-next/app/globals.css
Comment thread web-next/app/history/page.tsx
Comment thread web-next/components/ui/use-toast.ts
Comment thread web-next/components/ui/use-toast.ts
Comment thread web-next/hooks/use-toast.ts
Comment thread web-next/hooks/use-toast.ts
Comment thread web-next/next.config.mjs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment thread web-next/app/page.tsx Outdated
Comment thread web-next/app/research-consent/page.tsx
Comment thread web-next/app/research-consent/page.tsx
Comment thread web-next/app/research-consent/page.tsx
Comment thread web-next/app/research-consent/page.tsx
Comment thread web-next/components/hero-section.tsx
Comment thread web-next/components/pricing-section.tsx Outdated
Comment thread web-next/components/sign-in-dialog.tsx
Comment thread web-next/components/sign-in-dialog.tsx
Comment thread web-next/components/testimonial-grid-section.tsx Outdated
Delete verified-unused v0 template components (all bento illustrations, testimonials, pricing, orphaned CSS) — bento-section.tsx imports none of them, so they were dead generic AI-dev-tool template code that does not fit Corgi.

Fix genuine issues in shipped components: demo renders counterfactual sub-labels as text instead of dangerouslySetInnerHTML (drops a CWE-79 pattern) and fixes a literal &apos;s that rendered verbatim; app-shell derives the body scroll lock from both the mobile menu and the sign-in dialog so the dialog keeps its lock when the menu closes; the landing page drops a redundant sticky wrapper that duplicated Header's own sticky + scroll handling; the FAQ accordion trigger is now keyboard-accessible (role button, tabIndex, Enter/Space, aria-expanded).
@coderabbitai coderabbitai Bot added the documentation Improvements or additions to documentation label Jul 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
web-next/app/demo/page.tsx (2)

234-260: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"Positions gained" sub-label ignores moved direction.

moved is computed but only feeds the numeric value (line 255); sub on line 256 is hardcoded to "Moved up by community governance" regardless of direction. With the current mock (diff = 6) this happens to be correct, but if diff were ever negative (post ranked worse under governance than pure engagement), the label would still say "Moved up" while the box styling correctly switches to the negative/tongue color scheme — a self-contradicting UI. This matters here because the PR objectives call out wiring real data into this exact component in a later slice.

🔧 Proposed fix
     {
       label: "Positions gained",
       value: moved === "up" ? `+${diff}` : moved === "down" ? `${diff}` : "—",
-      sub: "Moved up by community governance",
+      sub:
+        moved === "up"
+          ? "Moved up by community governance"
+          : moved === "down"
+          ? "Moved down by community governance"
+          : "No change under community governance",
       color: diff > 0 ? "text-success" : diff < 0 ? "text-tongue-foreground" : "text-foreground/40",
       bg: diff > 0 ? "bg-success/10 border-success/20" : diff < 0 ? "bg-tongue/10 border-tongue/20" : "bg-biscuit border-border",
     },

Suggested test: render StepCounterfactual with a mock where pure_engagement_rank < community_governed_rank (negative diff) and assert the rendered text says "Moved down", not "Moved up".

🤖 Prompt for AI Agents
In web-next/app/demo/page.tsx, StepCounterfactual (around lines 234-260), the
"Positions gained" box's `sub` field is hardcoded to "Moved up by community
governance" even though the `moved` variable (up/down/same) is already
computed above it. Update the `sub` value to branch on `moved` the same way
`value` does, so it correctly reads "Moved down by community governance" or a
neutral message when diff <= 0. Add a test that renders StepCounterfactual
with mock counterfactual data where pure_engagement_rank is less than
community_governed_rank and asserts the rendered sub text reflects "Moved
down", not "Moved up".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-next/app/demo/page.tsx` around lines 234 - 260, The StepCounterfactual
component’s “Positions gained” box uses the computed moved value for the number
but hardcodes the sub-label to “Moved up by community governance,” which breaks
when the rank change is down or unchanged. Update the boxes entry in
StepCounterfactual to derive sub from moved just like value, so it renders
direction-specific copy for up/down/same, and keep the wording aligned with the
existing diff-based styling. Add or update a test around StepCounterfactual to
cover a negative diff case and verify the sub text changes accordingly.

329-355: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Step rail buttons lose accessible name on small screens.

s.label is wrapped in hidden sm:block (line 349), so it's not rendered — and thus not exposed to assistive tech — below the sm breakpoint. On mobile, the button's only accessible content is the bare digit (or a decorative checkmark SVG marked aria-hidden), so a screen reader announces something like "3" instead of "Topic weights". The pagination dots below (lines 393-401) already get this right with explicit aria-label.

🔧 Proposed fix
               <button
                 key={s.id}
                 onClick={() => setStep(i)}
                 aria-current={active ? "step" : undefined}
+                aria-label={s.label}
                 className="flex-1 flex flex-col items-center gap-2 group relative z-10"
               >

Suggested test: render the step rail and assert each button has an accessible name matching STEPS[i].label (e.g. via getByRole("button", { name: "Topic weights" })), independent of viewport width.

🤖 Prompt for AI Agents
In web-next/app/demo/page.tsx, the step rail buttons (around lines 333-354)
show the step label in a <span> hidden below the `sm` breakpoint
(`hidden sm:block`), so on mobile the button has no meaningful accessible
name. Add an `aria-label={s.label}` attribute to each step rail <button> so
screen readers always announce the step name regardless of viewport. Add a
test asserting each step rail button is queryable by accessible name matching
its STEPS label.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-next/app/demo/page.tsx` around lines 329 - 355, Add an explicit
accessible name to each step rail button in the step-mapping block that renders
STEPS so the label is available even when the visible <span> is hidden on small
screens; update the button in the STEPS.map render to use the step label
directly for screen readers, and keep the existing visual text unchanged. Also
add a test for the step rail ensuring each button is found by accessible name
from STEPS[i].label regardless of viewport width.
web-next/components/faq-section.tsx (1)

59-93: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

role="button" now wraps the entire card, including the expanded answer — not just the trigger.

Keyboard operability is fixed (tabIndex, onKeyDown for Enter/Space, aria-expanded), which resolves the prior blocking issue. But the role="button" + onClick/onKeyDown are on the outer div (line 60) that also contains the AnimatePresence answer panel (76-91), not just the header row (68-75).

Why it matters: once expanded, a screen reader announces the answer text as part of the "button" itself, and clicking/selecting text inside the answer bubbles up to onClick and toggles the item closed — surprising behavior for someone trying to read or select the answer.

Scope the interactive role to just the header row (ideally a native <button>), and keep the answer panel as a plain sibling.

🔧 Suggested restructure
-  return (
-    <div
-      role="button"
-      tabIndex={0}
-      aria-expanded={isOpen}
-      className="w-full bg-card shadow-[0_2px_6px_rgba(46,38,32,0.06)] overflow-hidden rounded-xl border border-border transition-colors duration-200 cursor-pointer hover:border-primary/30"
-      onClick={handleClick}
-      onKeyDown={handleKeyDown}
-    >
-      <div className="w-full px-6 py-5 pr-5 flex justify-between items-center gap-5 text-left">
+  return (
+    <div className="w-full bg-card shadow-[0_2px_6px_rgba(46,38,32,0.06)] overflow-hidden rounded-xl border border-border transition-colors duration-200 hover:border-primary/30">
+      <button
+        type="button"
+        aria-expanded={isOpen}
+        onClick={onToggle}
+        className="w-full px-6 py-5 pr-5 flex justify-between items-center gap-5 text-left"
+      >
         <div className="flex-1 text-foreground text-[0.9375rem] font-semibold leading-6 break-words">{question}</div>
         <div className="flex justify-center items-center flex-shrink-0">
           <ChevronDown
             className={`w-5 h-5 text-foreground/40 transition-transform duration-300 ease-out ${isOpen ? "rotate-180" : "rotate-0"}`}
           />
         </div>
-      </div>
+      </button>
       <AnimatePresence initial={false}>

Drop handleClick/handleKeyDown in favor of native button activation (Enter/Space handled automatically), and close the tag appropriately at the end of the JSX.

Test to add: RTL test asserting clicking/selecting text inside the expanded answer does not toggle the item closed, and that Tab lands only on the header button (not on the answer text).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-next/components/faq-section.tsx` around lines 59 - 93, The FAQ item’s
interactive semantics are applied to the outer container, which makes the
expanded answer part of the button and causes clicks inside the answer to toggle
the item. Move the button behavior from the outer wrapper in the FAQ item
component to just the header/trigger row (prefer a native button), keep the
expanded content as a non-interactive sibling, and remove the outer container’s
click/key handlers so text selection and reading the answer do not collapse it.
Update the FAQ section structure around the existing `AnimatePresence`,
`handleClick`, and `handleKeyDown` logic accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@web-next/app/demo/page.tsx`:
- Around line 234-260: The StepCounterfactual component’s “Positions gained” box
uses the computed moved value for the number but hardcodes the sub-label to
“Moved up by community governance,” which breaks when the rank change is down or
unchanged. Update the boxes entry in StepCounterfactual to derive sub from moved
just like value, so it renders direction-specific copy for up/down/same, and
keep the wording aligned with the existing diff-based styling. Add or update a
test around StepCounterfactual to cover a negative diff case and verify the sub
text changes accordingly.
- Around line 329-355: Add an explicit accessible name to each step rail button
in the step-mapping block that renders STEPS so the label is available even when
the visible <span> is hidden on small screens; update the button in the
STEPS.map render to use the step label directly for screen readers, and keep the
existing visual text unchanged. Also add a test for the step rail ensuring each
button is found by accessible name from STEPS[i].label regardless of viewport
width.

In `@web-next/components/faq-section.tsx`:
- Around line 59-93: The FAQ item’s interactive semantics are applied to the
outer container, which makes the expanded answer part of the button and causes
clicks inside the answer to toggle the item. Move the button behavior from the
outer wrapper in the FAQ item component to just the header/trigger row (prefer a
native button), keep the expanded content as a non-interactive sibling, and
remove the outer container’s click/key handlers so text selection and reading
the answer do not collapse it. Update the FAQ section structure around the
existing `AnimatePresence`, `handleClick`, and `handleKeyDown` logic
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3c7ad726-de5b-4b78-9c97-3ec775fb6741

📥 Commits

Reviewing files that changed from the base of the PR and between b0a739f and fbbfdc2.

📒 Files selected for processing (4)
  • web-next/app/demo/page.tsx
  • web-next/app/page.tsx
  • web-next/components/app-shell.tsx
  • web-next/components/faq-section.tsx

@AndrewNordstrom

Copy link
Copy Markdown
Collaborator Author

CodeRabbit findings — triage & disposition

Founder-authorized triage of the 53 findings. Summary of dispositions:

Fixed (in fbbfdc2):

  • demo — replaced dangerouslySetInnerHTML with plain text (drops CWE-79 pattern) + fixed a literal &apos;s.
  • app-shell — body scroll-lock now derived from both mobile-menu and sign-in-dialog state.
  • landing page.tsx — removed the redundant sticky wrapper that duplicated Header's own sticky+scroll.
  • faq-section — accordion trigger is now keyboard-accessible (role/tabIndex/Enter-Space/aria-expanded). (The re-anchored thread at the component's closing line is the same finding; addressed.)

Deleted as verified-dead code (fbbfdc2): all components/bento/* illustrations, testimonial-grid-section, large-testimonial, pricing-section, orphaned AiCodeReviews.module.css — bento-section.tsx imports none of them; they were unused generic template code. Their threads are outdated.

Deferred to their wiring slice (tracked in the epic PROJ-1497):

  • admin ConfirmModal a11y + duplicate-phase mock → P2.5 (admin wiring).
  • research-consent setTimeout cleanup + submit-time timestamp → P2.6 (consent wiring).
  • sign-in-dialog form-reset + setTimeout cleanup → P2.0 (auth wiring).
  • history unchecked mock cast → P2 (data wiring).
  • layout.tsx ThemeProvider not mounted → P0.2 (provider scaffold, validated).
  • post/[uri] dev-state-switcher visible in prod + unchecked param → P1 (/post is fully refactored to /post?uri= there; web-next is not served until the P3 cutover, so this is not a live-prod path yet).
  • duplicate styles/globals.css → P1 cleanup.

Dismissed as upstream shadcn/ui standard (unmodified from canonical shadcn, not yet consumed by any product page; a dedicated shadcn-hardening pass will cover them if/when consumed): the components/ui/* primitives (input-group, carousel, form, menubar, pagination, sidebar, input-otp, kbd, empty, spinner, use-toast, score-*, topic-slider, linked-slider, status-chip) + the mirrored hooks/use-toast.

Trivial nitpicks acknowledged/deferred: footer "use client", header↔app-shell menu duplication, animated-section, components.json, next.config.mjs.

Resolving the corresponding threads accordingly.

@AndrewNordstrom

Copy link
Copy Markdown
Collaborator Author

@coderabbitai resolve — residual threads carry the founder-authorized disposition posted above (fixed in fbbfdc2 / deleted dead files / deferred to the wiring slices tracked in PROJ-1497).

@AndrewNordstrom
AndrewNordstrom enabled auto-merge (squash) July 2, 2026 23:59
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

@AndrewNordstrom Acknowledged. Resolving all outstanding review threads on this PR per the disposition above — fixed items in fbbfdc2, deleted dead code, and items deferred to the PROJ-1497 wiring slices (admin, research-consent, sign-in-dialog, history, layout.tsx ThemeProvider mount, /post/[uri] dev-switcher, duplicate globals.css).

One flag for later: the deferred /post/[uri] dev-state-switcher and unchecked params.uri cast are fine to leave as-is only as long as web-next stays fully unserved in prod until the P3 cutover — worth double-checking that assumption still holds when P1 lands the /post?uri= refactor, since if web-next gets partially exposed earlier (e.g., a preview deploy reachable externally) the dev-switcher would leak into a real user-facing surface.


✏️ Learnings added
Learnt from: AndrewNordstrom
URL: https://github.com/andrewnordstrom-eng/bluesky-community-feed/pull/275

Timestamp: 2026-07-02T23:59:55.528Z
Learning: In the andrewnordstrom-eng/bluesky-community-feed repo, `web-next/` is a purely additive Next.js 15 App Router scaffold (added in PR `#275`, tracked by Linear issue PROJ-1498) that is not served in production; the live frontend remains `web/` (Vite), served via `src/feed/server.ts`, with `.github/workflows/deploy.yml` unchanged. Broader rollout phases (static export, `/post` route refactor to `/post?uri=`, analytics removal, CSP fixes, env-gated multi-page serving, data/auth wiring, eventual cutover) are tracked under the epic PROJ-1497 and deferred out of scaffold-slice PRs like `#275`. Review findings in `web-next/` mock-data pages (admin, research-consent, sign-in-dialog, history, post/[uri]) that relate to data wiring, timers, or auth are expected to be deferred to their respective wiring slices rather than fixed in the scaffold PR. `components/ui/*` primitives copied verbatim from shadcn/ui and not yet consumed by any product page are dismissed from hardening review until they are actually used.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Comments resolved and changes approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant