feat(ui): redesign landing page sections - #1791
Conversation
Elevate visual design across all landing page sections below the hero with better layout variety, atmospheric backgrounds, improved typography hierarchy, and scroll-triggered animations. - Add Plus Jakarta Sans display font and CSS utilities (noise texture, surface-elevated token, font-display utility) - Features: two-tier card grid with blur-slide animations - Graph: left-aligned heading, stats row, gradient background - Code example: split layout with sticky code block, macOS-style dots - Testimonials: dual marquee rows with edge fade masks - FAQ: two-column layout with sticky heading - CTA: atmospheric background with ShimmerButton - Footer: gradient separator, reorganized bottom bar - Hero: add 6 missing provider icons (23 total) - Enterprise: update stats, add SOC2 Type II card - Pricing: accurate model counts (210+), free models (3) - Fix BYOK pricing across comparison and changelog (free, not 1% fee) - Add discount form, public discounts API, and global discount banner Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WalkthroughAdds discount expiry support end-to-end (form, server actions, API routes, UI lists/banners, countdown timers), new organization discounts pages/components, and multiple landing UI/design updates (fonts, tokens, marquee, testimonials, hero, footer, features, and copy). Changes
Sequence Diagram(s)sequenceDiagram
participant AdminUI as Admin Form (browser)
participant ServerAction as Next.js Server Action
participant API as API Route (/orgs/{id}/discounts / public)
participant DB as Database
AdminUI->>ServerAction: submit create discount (provider, model, percent, reason, expiresAt)
ServerAction->>API: call createOrganizationDiscount (payload incl. expiresAt)
API->>DB: INSERT discount row (expiresAt)
DB-->>API: created discount
API-->>ServerAction: success
ServerAction-->>AdminUI: render/redirect to discounts list
Note over AdminUI,API: Listing discounts (org or public)
AdminUI->>API: GET /orgs/{id}/discounts or /public/discounts?modelId=...
API->>DB: SELECT discounts WHERE organizationId = ? OR NULL AND (expiresAt IS NULL OR expiresAt >= now)
DB-->>API: discounts[]
API-->>AdminUI: discounts[] (used by DiscountsClient / GlobalDiscountBanner)
AdminUI->>AdminUI: Countdown component renders live remaining time for each expiresAt
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/ui/src/components/pricing/pricing-table.tsx (1)
216-219: 🛠️ Refactor suggestion | 🟠 MajorRemove unnecessary
as anycast onLinkhref.Line 218 weakens type safety unnecessarily. The conditional guard at line 216 (
feature.learnMoreLink &&) already narrows the type tostring, which is a valid type for the Next.jsLinkcomponent'shrefprop. Pass it directly without the cast.Proposed fix
- <Link - href={feature.learnMoreLink as any} - className="text-xs text-blue-600 hover:underline" - > + <Link + href={feature.learnMoreLink} + className="text-xs text-blue-600 hover:underline" + >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/pricing/pricing-table.tsx` around lines 216 - 219, Remove the unnecessary type cast on the Next.js Link href: the conditional guard around feature.learnMoreLink already narrows it to a valid string, so change the Link usage in the pricing table (the JSX using feature.learnMoreLink and Link) to pass href={feature.learnMoreLink} directly instead of href={feature.learnMoreLink as any}; this restores type safety without other logic changes.
🧹 Nitpick comments (6)
apps/ui/src/components/landing/faq.tsx (2)
94-101: Extract duplicated trigger className to a constant.The same ~280-character className string is repeated across all 4
AccordionPrimitive.Triggerelements (lines 94, 132, 157, 177). Extract to a constant for maintainability.♻️ Proposed refactor
Add a constant before the component:
const triggerClassName = "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-center justify-between gap-4 rounded-md py-2 text-left font-display text-lg md:text-xl font-medium leading-7 transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&>svg>path:last-child]:origin-center [&>svg>path:last-child]:transition-all [&>svg>path:last-child]:duration-200 [&[data-state=open]>svg]:rotate-180 [&[data-state=open]>svg>path:last-child]:rotate-90 [&[data-state=open]>svg>path:last-child]:opacity-0 text-foreground";Then use it in each trigger:
-<AccordionPrimitive.Trigger className="focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-center justify-between gap-4 rounded-md py-2 text-left font-display text-lg md:text-xl font-medium leading-7 transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&>svg>path:last-child]:origin-center [&>svg>path:last-child]:transition-all [&>svg>path:last-child]:duration-200 [&[data-state=open]>svg]:rotate-180 [&[data-state=open]>svg>path:last-child]:rotate-90 [&[data-state=open]>svg>path:last-child]:opacity-0 text-foreground"> +<AccordionPrimitive.Trigger className={triggerClassName}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/landing/faq.tsx` around lines 94 - 101, Extract the long duplicated className string into a single constant (e.g., triggerClassName) and replace each AccordionPrimitive.Trigger's inline className with that constant; locate all uses of AccordionPrimitive.Trigger in this file and update their className prop to use triggerClassName to remove duplication and improve maintainability.
60-61: Consider removing structural comments.Comments like
{/* Left column: sticky heading */},{/* Item 1 */}, etc. describe structure that's already evident from the code (grid classes,value="item-1"). As per coding guidelines, avoid unnecessary code comments.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/landing/faq.tsx` around lines 60 - 61, Remove unnecessary structural comments in the FAQ component (e.g., the JSX comments like {/* Left column: sticky heading */} and {/* Item 1 */}) that duplicate information already expressed by markup and classNames (see the div with className "lg:col-span-2 lg:sticky lg:top-24 lg:self-start" and the elements using value="item-1"); delete these redundant comments and keep only comments that add non-obvious, actionable context.apps/ui/src/components/landing/cta.tsx (1)
13-20: Remove decorative inline comments in JSX.Lines 13, 16, and 19 add comments that restate obvious markup intent and create noise in this component.
As per coding guidelines, "No unnecessary code comments".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/landing/cta.tsx` around lines 13 - 20, In the CTA component remove the three decorative inline JSX comments that restate obvious markup (the comment nodes immediately preceding the three absolute divs for the gradient separator, atmospheric background, and soft radial glow) — delete the comment lines like {/* Gradient separator at top */}, {/* Atmospheric background */}, and {/* Soft radial glow */} so only the div elements remain; do not alter the div attributes or layout logic in this file.apps/ui/src/components/landing/features.tsx (1)
382-385: Use stable keys instead of index keys in both card maps.
key={i}is fragile if ordering changes;feature.slugis a stable, unique key here. All slugs in the file are unique, making this refactor safe.Proposed refactor
- {tier1Features.map((feature, i) => ( + {tier1Features.map((feature) => ( <FeaturedCard - key={i} + key={feature.slug} icon={feature.icon} title={feature.title} description={feature.description}- {tier2Features.map((feature, i) => ( + {tier2Features.map((feature) => ( <CompactCard - key={i} + key={feature.slug} icon={feature.icon} title={feature.title} description={feature.description}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/landing/features.tsx` around lines 382 - 385, Replace unstable index keys used in the FeaturedCard maps with the stable unique slug: in the tier1Features.map where FeaturedCard currently uses key={i}, change it to use key={feature.slug}; do the same in the other feature card map (the second map rendering FeaturedCard) so both use feature.slug as the key to ensure stable identity when ordering changes.apps/ui/src/components/landing/testimonials.tsx (1)
22-23: Remove purely descriptive JSX comments in this section.These comments restate obvious structure and can be dropped to match repo style.
As per coding guidelines, "
**/*.{ts,tsx,js,jsx}: No unnecessary code comments".Also applies to: 36-37, 48-49
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/landing/testimonials.tsx` around lines 22 - 23, Remove the purely descriptive JSX comment nodes that restate structure: delete the {/* Noise texture */} comment immediately preceding the div with className "absolute inset-0 bg-noise" and the similar descriptive comments around the other elements referenced in the review (the comment blocks around lines 36-37 and 48-49). Keep the JSX elements (e.g., the div with className "absolute inset-0 bg-noise") intact and only remove the unnecessary {/* ... */} comment nodes to comply with the repo guideline about unnecessary comments.apps/ui/src/components/landing/graph.tsx (1)
78-85: Drop section-label JSX comments for consistency with repo style.These comments are structural labels and can be removed without losing clarity.
As per coding guidelines, "
**/*.{ts,tsx,js,jsx}: No unnecessary code comments".Also applies to: 115-118
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/landing/graph.tsx` around lines 78 - 85, Remove the unnecessary JSX comments that label sections in apps/ui/src/components/landing/graph.tsx (e.g., the "Gradient background", "Noise texture", "Header" comments adjacent to the <div className="absolute inset-0 bg-gradient-to-b from-background via-surface-elevated to-background" />, <div className="absolute inset-0 bg-noise" />, and the <div className="relative"> container) and also drop the similar comments around the elements at the 115–118 region; leave the JSX elements and className attributes intact so only the comment nodes are deleted for consistency with the repo style rule forbidding unnecessary comments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/src/index.ts`:
- Around line 224-225: The /public/discounts route is currently mounted directly
with app.route("/public/discounts", publicDiscounts); ensure this is intentional
and not duplicated in the authenticated routes aggregator by confirming
publicDiscounts is not exported or included in apps/api/src/routes/index.ts; if
it should be public keep only this mount, otherwise move the mounting into the
routes index and remove this line. After making the change, run pnpm build to
verify the API routing and ensure no duplicate registrations or build-time route
errors occur.
In `@apps/api/src/routes/organization.ts`:
- Around line 801-811: Replace the direct SQL-style reads using
db.select().from(tables.discount) with the repository-standard
db.query.discount.findMany() calls: for the org-specific query (currently using
and(eq(tables.discount.organizationId, id), notExpired)) call
db.query.discount.findMany() with a where that composes the same conditions
(organizationId equals id plus the notExpired predicate) and orderBy createdAt
descending; for the global query (currently using
and(isNull(tables.discount.organizationId), notExpired)) call
db.query.discount.findMany() with a where that composes organizationId is null
plus the notExpired predicate and orderBy createdAt descending. Ensure you reuse
the existing notExpired expression and the same ordering (desc on createdAt) so
behavior stays identical.
In `@apps/api/src/routes/public-discounts.ts`:
- Around line 51-61: Replace the raw query using
db.select().from(tables.discount) with the canonical query API
db().query.discount.findMany(); pass the same filters by converting the where
clause into the findMany's where argument: require organizationId to be null,
model to be null or equal to modelId, and include the existing notExpired
condition, and apply the same ordering by createdAt descending (use the
findMany/orderBy option). Update the call site that currently references
tables.discount and notExpired so the conditions map to the findMany where
object and orderBy: { createdAt: 'desc' }.
- Around line 57-61: The query currently only orders by createdAt, causing newer
global discounts to appear before older model-specific ones; change the .orderBy
call to first sort by specificity (model-specific discounts matching modelId
before all-model/null ones) and then by createdAt DESC, e.g. add a primary order
key like desc(eq(tables.discount.model, modelId)) (or
desc(isNotNull(tables.discount.model) && eq(...))) ahead of
desc(tables.discount.createdAt) so model-specific discounts are prioritized.
In `@apps/ui/src/app/dashboard/`[orgId]/org/discounts/page.tsx:
- Line 25: Remove the unnecessary "as any" cast on the "/orgs/{id}/discounts"
endpoint string: locate the code using the literal "/orgs/{id}/discounts" (same
pattern used for "/orgs/{id}/transactions" and "/orgs/{id}/projects") and delete
the "as any" so the call uses the generated api/v1 types directly; ensure the
call signature compiles against the existing GET type for that path and adjust
any surrounding generics if TypeScript still needs explicit typing.
In `@apps/ui/src/app/globals.css`:
- Line 11: Remove the self-referential CSS custom property assignment for
--font-display (the line "--font-display: var(--font-display);"); it's invalid
and unnecessary because the display font variable is already injected elsewhere,
so delete that declaration and leave the other aliasing lines (e.g.,
--font-geist-mono → --font-mono, --font-inter → --font-sans) unchanged.
In `@apps/ui/src/components/countdown.tsx`:
- Around line 9-16: The getTimeRemaining function and the countdown component
must immediately react to changes and guard against invalid dates: update
getTimeRemaining (used by the Countdown component) to check new
Date(expiresAt).getTime() for NaN and return expired: true with zeroed fields
when invalid or when end <= now; compute diff and integer
days/hours/minutes/seconds only for valid numbers. In the Countdown component
(useEffect that sets the interval), call getTimeRemaining(expiresAt) and set
state immediately when expiresAt changes (before creating the interval) and then
start the interval for subsequent ticks; also clear the timer on unmount or when
expiresAt changes to avoid stale timers.
In `@apps/ui/src/components/landing/code-example.tsx`:
- Around line 246-247: Remove the non-essential JSX inline comments inside the
component markup (e.g., the comment before the AnimatedGroup JSX and other
similar comments around lines referenced like the ones near AnimatedGroup) so
the UI component contains only necessary markup and developer-facing comments
are eliminated; locate occurrences within the component (search for
AnimatedGroup and surrounding JSX blocks and any JSX comments like {/* ... */})
and delete those comment nodes (also remove the other noted JSX comments around
the other column/tabs/bullets sections) leaving the JSX elements and their props
intact.
- Around line 370-375: Replace the inline explicit types (className: string;
style: CSSProperties; tokens: any[]; getLineProps: (props: any) => any;
getTokenProps: (props: any) => any) used in the Highlight render-prop callback
with the exported RenderProps type from prism-react-renderer: import RenderProps
from 'prism-react-renderer' (or named import if applicable) and change the
callback signature to accept a single parameter typed as RenderProps; do the
same replacement for the other occurrence referenced (lines ~388–394) so both
render-prop callbacks use RenderProps instead of any.
In `@apps/ui/src/components/landing/cta.tsx`:
- Line 53: The anchor currently uses href={config.githubUrl ?? ""} with
target="_blank" which can cause same-page navigation when config.githubUrl is
missing and lacks rel attributes; update the CTA anchor (the <a href={...}
target="_blank"> in cta.tsx) to only render when config.githubUrl is a non-empty
string (or disable the link) and add rel="noopener noreferrer" to the anchor
when using target="_blank"; e.g., guard the link by conditionally rendering the
<a> around the clickable content using config.githubUrl truthiness or use a safe
fallback that does not open a new tab, and include rel="noopener noreferrer"
whenever target="_blank" is present.
In `@apps/ui/src/components/landing/features.tsx`:
- Line 354: In the Features component JSX
(apps/ui/src/components/landing/features.tsx) remove non-essential inline JSX
comments such as the section-label comments (e.g. {/* Dot grid background */}
and the other similar comments around the block rendered at the same area),
leaving the markup clean; locate the JSX inside the Features functional
component (or the component returned markup) and delete those comment nodes
(including the ones referenced near the other two occurrences) so only
meaningful comments or explanatory code remain.
In `@apps/ui/src/components/landing/footer.tsx`:
- Around line 176-180: The anchor elements in footer.tsx that use
target="_blank" (the <a> tags inside the Footer component around config.docsUrl
and the other anchor at lines ~220-223) lack rel attributes; update those
anchors to include rel="noopener noreferrer" to restore proper tab isolation and
security when opening external links in a new tab—locate the <a
href={config.docsUrl ?? ""} target="_blank"> and the corresponding anchor
further down and add rel="noopener noreferrer" to each.
- Around line 185-188: In the Footer component replace all occurrences of Link
hrefs that use "as any" (e.g., href={"/integrations" as any}, href={"/guides" as
any}, href={"/brand" as any}, href={"/compare/litellm" as any}) by passing the
string literals directly (e.g., href="/integrations") so you remove the
unnecessary type casts; update each Link usage in this file to use plain string
href props and remove the "as any" casts to preserve Next.js route type safety.
In `@apps/ui/src/components/landing/graph.tsx`:
- Around line 136-149: The rightRefs array contains a dangling element at index
5 that is never attached to a Circle (you render logos for indices 0–4 and the
Help Circle at index 6), causing null-targets in AnimatedBeam; fix by removing
the unused slot from rightRefs initialization so the array only contains refs
for mounted targets (or alternately mount a hidden Circle for that ref), and
ensure any iteration in AnimatedBeam over rightRefs (beam generation loop) uses
the updated array length or guards against nulls so no null ref is processed.
In `@apps/ui/src/components/models/global-discount-banner.tsx`:
- Around line 35-44: The component's fetch logic leaves stale discounts set when
a fetch fails or returns no active discount; update the effect/fetch in
global-discount-banner (the code that uses setDiscount and computes withExpiry)
to explicitly clear the discount state: call setDiscount(undefined|null)
immediately when starting a new fetch for a different modelId, call
setDiscount(undefined|null) when res.ok is false, call
setDiscount(undefined|null) when no withExpiry is found, and also in the catch
block so errors clear the banner; keep the existing setDiscount(withExpiry) path
for the success case.
In `@apps/ui/src/content/changelog/2026-01-21-pro-features-now-free.md`:
- Line 22: The copy "Use your own API keys from OpenAI, Anthropic, Google, and
25+ other providers." is inconsistent with the PR's "25 providers" messaging;
update the sentence (the exact string containing "25+ other providers") to read
something like "Use your own API keys from OpenAI, Anthropic, Google — 25
providers total." to align counts across the changelog.
---
Outside diff comments:
In `@apps/ui/src/components/pricing/pricing-table.tsx`:
- Around line 216-219: Remove the unnecessary type cast on the Next.js Link
href: the conditional guard around feature.learnMoreLink already narrows it to a
valid string, so change the Link usage in the pricing table (the JSX using
feature.learnMoreLink and Link) to pass href={feature.learnMoreLink} directly
instead of href={feature.learnMoreLink as any}; this restores type safety
without other logic changes.
---
Nitpick comments:
In `@apps/ui/src/components/landing/cta.tsx`:
- Around line 13-20: In the CTA component remove the three decorative inline JSX
comments that restate obvious markup (the comment nodes immediately preceding
the three absolute divs for the gradient separator, atmospheric background, and
soft radial glow) — delete the comment lines like {/* Gradient separator at top
*/}, {/* Atmospheric background */}, and {/* Soft radial glow */} so only the
div elements remain; do not alter the div attributes or layout logic in this
file.
In `@apps/ui/src/components/landing/faq.tsx`:
- Around line 94-101: Extract the long duplicated className string into a single
constant (e.g., triggerClassName) and replace each AccordionPrimitive.Trigger's
inline className with that constant; locate all uses of
AccordionPrimitive.Trigger in this file and update their className prop to use
triggerClassName to remove duplication and improve maintainability.
- Around line 60-61: Remove unnecessary structural comments in the FAQ component
(e.g., the JSX comments like {/* Left column: sticky heading */} and {/* Item 1
*/}) that duplicate information already expressed by markup and classNames (see
the div with className "lg:col-span-2 lg:sticky lg:top-24 lg:self-start" and the
elements using value="item-1"); delete these redundant comments and keep only
comments that add non-obvious, actionable context.
In `@apps/ui/src/components/landing/features.tsx`:
- Around line 382-385: Replace unstable index keys used in the FeaturedCard maps
with the stable unique slug: in the tier1Features.map where FeaturedCard
currently uses key={i}, change it to use key={feature.slug}; do the same in the
other feature card map (the second map rendering FeaturedCard) so both use
feature.slug as the key to ensure stable identity when ordering changes.
In `@apps/ui/src/components/landing/graph.tsx`:
- Around line 78-85: Remove the unnecessary JSX comments that label sections in
apps/ui/src/components/landing/graph.tsx (e.g., the "Gradient background",
"Noise texture", "Header" comments adjacent to the <div className="absolute
inset-0 bg-gradient-to-b from-background via-surface-elevated to-background" />,
<div className="absolute inset-0 bg-noise" />, and the <div
className="relative"> container) and also drop the similar comments around the
elements at the 115–118 region; leave the JSX elements and className attributes
intact so only the comment nodes are deleted for consistency with the repo style
rule forbidding unnecessary comments.
In `@apps/ui/src/components/landing/testimonials.tsx`:
- Around line 22-23: Remove the purely descriptive JSX comment nodes that
restate structure: delete the {/* Noise texture */} comment immediately
preceding the div with className "absolute inset-0 bg-noise" and the similar
descriptive comments around the other elements referenced in the review (the
comment blocks around lines 36-37 and 48-49). Keep the JSX elements (e.g., the
div with className "absolute inset-0 bg-noise") intact and only remove the
unnecessary {/* ... */} comment nodes to comply with the repo guideline about
unnecessary comments.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a51e82c2-a193-4a42-986b-e1ed9a44c686
⛔ Files ignored due to path filters (4)
apps/admin/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/code/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/playground/src/lib/api/v1.d.tsis excluded by!**/v1.d.tsapps/ui/src/lib/api/v1.d.tsis excluded by!**/v1.d.ts
📒 Files selected for processing (29)
apps/admin/src/app/discounts/page.tsxapps/admin/src/app/organizations/[orgId]/discounts/page.tsxapps/admin/src/components/discount-form.tsxapps/api/src/index.tsapps/api/src/routes/organization.tsapps/api/src/routes/public-discounts.tsapps/ui/src/app/dashboard/[orgId]/org/discounts/discounts-client.tsxapps/ui/src/app/dashboard/[orgId]/org/discounts/page.tsxapps/ui/src/app/globals.cssapps/ui/src/app/layout.tsxapps/ui/src/app/models/[name]/page.tsxapps/ui/src/components/countdown.tsxapps/ui/src/components/dashboard/animated-nav-icons.tsxapps/ui/src/components/dashboard/dashboard-sidebar.tsxapps/ui/src/components/enterprise/features.tsxapps/ui/src/components/enterprise/hero.tsxapps/ui/src/components/landing/code-example.tsxapps/ui/src/components/landing/comparison.tsxapps/ui/src/components/landing/cta.tsxapps/ui/src/components/landing/faq.tsxapps/ui/src/components/landing/features.tsxapps/ui/src/components/landing/footer.tsxapps/ui/src/components/landing/graph.tsxapps/ui/src/components/landing/hero.tsxapps/ui/src/components/landing/marquee-container.tsxapps/ui/src/components/landing/testimonials.tsxapps/ui/src/components/models/global-discount-banner.tsxapps/ui/src/components/pricing/pricing-table.tsxapps/ui/src/content/changelog/2026-01-21-pro-features-now-free.md
- Remove self-referential --font-display CSS variable assignment - Add NaN guard and immediate state sync in Countdown component - Remove inline type annotations from Highlight render prop callback - Remove unused CSSProperties import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/ui/src/components/landing/code-example.tsx (1)
278-289: Expose selected state on language toggle buttons.Add pressed/selected semantics so assistive tech can announce which snippet is active.
♿ Suggested accessibility tweak
<button key={key} onClick={() => setActiveTab(key as keyof typeof codeExamples)} + aria-pressed={activeTab === key} + aria-label={`Show ${example.label} code example`} className={cn( @@ <button key={key} onClick={() => setActiveTab(key as keyof typeof codeExamples) } + aria-pressed={activeTab === key} + aria-label={`Show ${example.label} code example`} className={cn(Also applies to: 303-316
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/landing/code-example.tsx` around lines 278 - 289, The language toggle buttons lack pressed/selected semantics; update the button rendered inside the map (the one using key, example.label, setActiveTab and comparing activeTab === key) to include an ARIA state such as aria-pressed={activeTab === key} (or aria-current="true"/"false") so assistive tech can announce the active snippet, and mirror the same change for the second similar block (the other buttons around the 303-316 region) so both toggle sets expose selection state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/ui/src/app/globals.css`:
- Around line 213-232: Stylelint flags Tailwind v4 at-rules like the `@utility`
blocks (e.g., `@utility` font-display, `@utility` bg-noise, `@utility`
bg-surface-elevated) as unknown at-rules; fix by either updating your stylelint
config to ignore Tailwind directives (add ignoreAtRules:
["utility","theme","plugin","custom-variant"] to your .stylelintrc.json rules)
or by converting these `@utility` blocks into standard Tailwind v3+ compatible
syntax (use `@layer` utilities and regular CSS class selectors instead of
`@utility/`@theme) so the linter and IDE stop reporting errors.
In `@apps/ui/src/components/landing/code-example.tsx`:
- Around line 221-223: The copy button handler currently calls setCopied(true)
and starts a setTimeout without clearing prior timers, which stacks timers and
can update state after unmount; modify the component in code-example.tsx to
store the timeout id in a ref (e.g., copyTimeoutRef via useRef<number | null>),
clear any existing timeout with clearTimeout(copyTimeoutRef.current) before
creating a new setTimeout, assign the new id to copyTimeoutRef.current, and add
a useEffect cleanup that clears the timeout on unmount to avoid stale state
updates from setCopied.
---
Nitpick comments:
In `@apps/ui/src/components/landing/code-example.tsx`:
- Around line 278-289: The language toggle buttons lack pressed/selected
semantics; update the button rendered inside the map (the one using key,
example.label, setActiveTab and comparing activeTab === key) to include an ARIA
state such as aria-pressed={activeTab === key} (or aria-current="true"/"false")
so assistive tech can announce the active snippet, and mirror the same change
for the second similar block (the other buttons around the 303-316 region) so
both toggle sets expose selection state.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 8fcb8251-bb24-48d4-9173-9ae07f793d77
📒 Files selected for processing (3)
apps/ui/src/app/globals.cssapps/ui/src/components/countdown.tsxapps/ui/src/components/landing/code-example.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/ui/src/components/countdown.tsx
| @utility font-display { | ||
| font-family: var(--font-display), var(--font-sans), system-ui, sans-serif; | ||
| } | ||
|
|
||
| @utility bg-noise { | ||
| &::before { | ||
| content: ""; | ||
| position: absolute; | ||
| inset: 0; | ||
| opacity: 0.03; | ||
| background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); | ||
| background-repeat: repeat; | ||
| background-size: 256px 256px; | ||
| pointer-events: none; | ||
| } | ||
| } | ||
|
|
||
| @utility bg-surface-elevated { | ||
| background-color: var(--surface-elevated); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Tailwind directives in globals.css =="
rg -n --fixed-strings '@utility' apps/ui/src/app/globals.css
rg -n --fixed-strings '@theme inline' apps/ui/src/app/globals.css
echo
echo "== Stylelint config support for Tailwind directives =="
fd -HI 'stylelint*' -E node_modules -E .git | while read -r f; do
echo "--- $f"
rg -n -C2 'at-rule-no-unknown|ignoreAtRules|tailwind|customSyntax|scss' "$f" || true
done
echo
echo "== Biome CSS parser/linter config =="
fd -HI 'biome*.json*' -E node_modules -E .git | while read -r f; do
echo "--- $f"
rg -n -C2 'css|parser|linter|tailwind' "$f" || true
done
echo
echo "== Relevant package versions =="
fd -HI 'package.json' -E node_modules -E .git | while read -r f; do
echo "--- $f"
jq -r '
(.dependencies // {}) + (.devDependencies // {})
| to_entries[]
| select(.key | test("tailwind|stylelint|biome"; "i"))
| "\(.key)=\(.value)"
' "$f" || true
doneRepository: theopenco/llmgateway
Length of output: 2093
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Complete .stylelintrc.json =="
cat .stylelintrc.json
echo
echo "== PostCSS config for Tailwind v4 =="
fd -HI 'postcss.config.*' -E node_modules -E .git | while read -r f; do
echo "--- $f"
cat "$f"
done
echo
echo "== Check if `@tailwindcss/postcss` is a plugin =="
rg -n 'tailwindcss/postcss|@tailwindcss/postcss' postcss.config.* || true
echo
echo "== Stylelint at-rule-no-unknown rule =="
cat .stylelintrc.json | jq -r '.rules["at-rule-no-unknown"]' 2>/dev/null || echo "Rule not explicitly defined"
echo
echo "== Check tailwindcss v4 postcss plugin documentation =="
rg -n 'postcss\|@tailwindcss' apps/ui/package.json || trueRepository: theopenco/llmgateway
Length of output: 1931
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== CI workflows checking for lint =="
fd -HI 'workflow\|\.yml\|\.yaml' .github/workflows 2>/dev/null | while read -r f; do
echo "--- $f"
rg -n 'stylelint|lint.*css|prettier' "$f" || echo "(no lint checks found)"
done
echo
echo "== Pre-commit or husky lint config =="
fd -HI '\.husky|pre-commit' -E node_modules -E .git | while read -r f; do
echo "--- $f"
head -20 "$f" 2>/dev/null || cat "$f" 2>/dev/null
done
echo
echo "== Package.json lint scripts =="
rg -n '"lint"|"style"' apps/ui/package.json -A1 -B1 || true
echo
echo "== Check if tailwind-language-server or official plugin =="
rg -n 'stylelint-tailwind|tailwindcss.*stylelint|postcss-tailwind' . -E node_modules -E .git || echo "No Tailwind-specific stylelint plugin found"
echo
echo "== Stylelint plugin or custom syntax for Tailwind =="
cat apps/ui/package.json | jq -r '.devDependencies | keys[] | select(. | test("stylelint|postcss"; "i"))' || trueRepository: theopenco/llmgateway
Length of output: 151
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Husky hooks =="
ls -la .husky/ 2>/dev/null || echo "No .husky directory"
echo
echo "== Hook contents (pre-commit, lint, etc) =="
find .husky -type f ! -name '.' -exec echo "--- {} ---" \; -exec cat {} \; 2>/dev/null || true
echo
echo "== Biome CSS linter rules =="
cat biome.json | jq '.css // empty'
echo
echo "== Try running stylelint on globals.css =="
cd apps/ui && npx stylelint src/app/globals.css 2>&1 | head -50 || echo "Stylelint command failed or not available"Repository: theopenco/llmgateway
Length of output: 1693
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Lint-staged configuration =="
if [ -f .lintstagedrc.json ]; then
cat .lintstagedrc.json
elif [ -f .lintstagedrc.mjs ]; then
cat .lintstagedrc.mjs
elif [ -f .lintstagedrc.js ]; then
cat .lintstagedrc.js
else
echo "Checking package.json for lint-staged config..."
cat package.json | jq '.["lint-staged"] // empty' || echo "No lint-staged in root package.json"
fi
echo
echo "== Check apps/ui package.json lint-staged =="
cat apps/ui/package.json | jq '.["lint-staged"] // empty' || echo "No lint-staged config in apps/ui"Repository: theopenco/llmgateway
Length of output: 189
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Is lint-staged installed? =="
rg -n '"lint-staged"' package.json apps/*/package.json
echo
echo "== Full package.json lint-staged and related config =="
cat package.json | jq '.. | select(type=="object") | select(has("lint-staged") or has("husky"))' 2>/dev/null || cat package.json | jq 'keys[]' | head -20
echo
echo "== Check for all lint-staged configs =="
fd -HI 'lintstagedrc|lint-staged' -E node_modules -E .git
echo
echo "== Default lint-staged behavior (check docs) =="
echo "lint-staged by default uses linter configs to determine which files to check."
echo "With stylelint configured for SCSS/CSS, it should check *.css, *.scss files."
echo "Since pre-commit runs 'pnpm lint-staged' without args, globals.css will be included."Repository: theopenco/llmgateway
Length of output: 2120
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== apps/ui/.lintstagedrc.json =="
cat apps/ui/.lintstagedrc.json
echo
echo "== Summary: Lint enforcement in commit flow =="
echo "1. Pre-commit hook runs: pnpm lint-staged"
echo "2. apps/ui/.lintstagedrc.json defines which files are checked"
echo "3. stylelint errors will block commit if CSS files are included"Repository: theopenco/llmgateway
Length of output: 490
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check for CI workflows (GitHub Actions, etc) =="
find .github -type f -name '*.yml' -o -name '*.yaml' 2>/dev/null | head -20
echo
echo "== Search for any stylelint or CSS linting in CI =="
find .github -type f \( -name '*.yml' -o -name '*.yaml' \) -exec grep -l 'stylelint\|css.*lint\|lint.*css' {} \; 2>/dev/null || echo "No CSS linting found in CI"
echo
echo "== Check turbo tasks for lint =="
cat turbo.json 2>/dev/null | jq '.tasks[] | select(.cache == false or .outputs == [])' || echo "No turbo.json or no relevant tasks"
echo
echo "== Summary of findings =="
echo "- stylelint errors: 11 errors confirmed"
echo "- Tailwind v4 `@utility/`@theme directives: NOT recognized by stylelint-config-standard-scss"
echo "- Pre-commit lint-staged: DOES NOT check *.css files in apps/ui"
echo "- Immediate merge blocker: NO (won't fail pre-commit)"
echo "- CI blocker risk: UNKNOWN (need to check CI config)"Repository: theopenco/llmgateway
Length of output: 844
Stylelint reports errors on Tailwind v4 @utility and @theme directives, but they're not currently enforced on CSS files.
Lines 213, 217, and 230 contain @utility blocks that stylelint flags as unknown at-rules (11 errors total across the file). The repo uses Tailwind v4 with proper PostCSS configuration, but stylelint-config-standard-scss doesn't recognize Tailwind's v4 directives. This won't block commits since lint-staged only checks JS/TS/JSON/Markdown files and CI doesn't run stylelint. However, these errors will appear in IDEs and should be resolved to avoid technical debt. Either update .stylelintrc.json to ignore Tailwind directives (add ignoreAtRules: ["utility", "theme", "plugin", "custom-variant"] to rules) or rewrite as @layer utilities with standard CSS class selectors.
🧰 Tools
🪛 Biome (2.4.4)
[error] 213-215: Tailwind-specific syntax is disabled.
(parse)
[error] 217-228: Tailwind-specific syntax is disabled.
(parse)
[error] 230-232: Tailwind-specific syntax is disabled.
(parse)
🪛 Stylelint (17.3.0)
[error] 218-218: Unexpected missing scoping root (nesting-selector-no-missing-scoping-root)
(nesting-selector-no-missing-scoping-root)
[error] 213-213: Unexpected unknown at-rule "@Utility" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
[error] 217-217: Unexpected unknown at-rule "@Utility" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
[error] 230-230: Unexpected unknown at-rule "@Utility" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/ui/src/app/globals.css` around lines 213 - 232, Stylelint flags Tailwind
v4 at-rules like the `@utility` blocks (e.g., `@utility` font-display, `@utility`
bg-noise, `@utility` bg-surface-elevated) as unknown at-rules; fix by either
updating your stylelint config to ignore Tailwind directives (add ignoreAtRules:
["utility","theme","plugin","custom-variant"] to your .stylelintrc.json rules)
or by converting these `@utility` blocks into standard Tailwind v3+ compatible
syntax (use `@layer` utilities and regular CSS class selectors instead of
`@utility/`@theme) so the linter and IDE stop reporting errors.
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); | ||
| toast({ |
There was a problem hiding this comment.
Clear/reset the copy timeout to avoid stale state updates after unmount.
Repeated clicks stack timers, and unmount can leave a pending timeout that still tries to update state.
🛠️ Proposed fix
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
@@
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const [copied, setCopied] = useState(false);
+ const copyResetTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
@@
useEffect(() => {
setMounted(true);
+ return () => {
+ if (copyResetTimeout.current) {
+ clearTimeout(copyResetTimeout.current);
+ }
+ };
}, []);
@@
await navigator.clipboard.writeText(text);
setCopied(true);
- setTimeout(() => setCopied(false), 2000);
+ if (copyResetTimeout.current) {
+ clearTimeout(copyResetTimeout.current);
+ }
+ copyResetTimeout.current = setTimeout(() => setCopied(false), 2000);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| toast({ | |
| import { useState, useEffect, useRef } from "react"; | |
| // ... other imports ... | |
| export default function CodeExample() { | |
| const { resolvedTheme } = useTheme(); | |
| const [mounted, setMounted] = useState(false); | |
| const [copied, setCopied] = useState(false); | |
| const copyResetTimeout = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| useEffect(() => { | |
| setMounted(true); | |
| return () => { | |
| if (copyResetTimeout.current) { | |
| clearTimeout(copyResetTimeout.current); | |
| } | |
| }; | |
| }, []); | |
| // ... other code ... | |
| // Around lines 221-223: | |
| const handleCopy = async (text: string) => { | |
| await navigator.clipboard.writeText(text); | |
| setCopied(true); | |
| if (copyResetTimeout.current) { | |
| clearTimeout(copyResetTimeout.current); | |
| } | |
| copyResetTimeout.current = setTimeout(() => setCopied(false), 2000); | |
| toast({ | |
| // ... rest of toast code ... | |
| }); | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/ui/src/components/landing/code-example.tsx` around lines 221 - 223, The
copy button handler currently calls setCopied(true) and starts a setTimeout
without clearing prior timers, which stacks timers and can update state after
unmount; modify the component in code-example.tsx to store the timeout id in a
ref (e.g., copyTimeoutRef via useRef<number | null>), clear any existing timeout
with clearTimeout(copyTimeoutRef.current) before creating a new setTimeout,
assign the new id to copyTimeoutRef.current, and add a useEffect cleanup that
clears the timeout on unmount to avoid stale state updates from setCopied.
Summary
Test plan
pnpm buildto confirm no build errors🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Updates
Other