Skip to content

feat: add responsive#11

Merged
ha-phuongvu merged 15 commits into
mainfrom
2nd-june
Jun 9, 2026
Merged

feat: add responsive#11
ha-phuongvu merged 15 commits into
mainfrom
2nd-june

Conversation

@ha-phuongvu

@ha-phuongvu ha-phuongvu commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Mobile nav drawer and improved header interactions
    • Image slider for activity galleries
    • Bottom-sheet map interface with regions selector and zoom feedback
  • Style

    • Broad mobile responsiveness and spacing refinements across pages (headers, heroes, content blocks)
    • Redesigned footer and updated contact/presence layout
    • Consistent image presentation tweaks (rounded corners, responsive sizing)
    • Updated global CSS tokens and mobile padding
  • Bug Fixes

    • Header auto-hide now respects open navigation state

Sang-mac and others added 11 commits June 2, 2026 08:28
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign footer motto block and contact details (work/personal email,
phone, WeChat, website), and tighten responsive alignment across the
footer, header, globe section, hero, quote, and subpage hero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ha-phuongvu, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 56 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be88b417-9eb3-4c8f-816e-40d9c04f5a56

📥 Commits

Reviewing files that changed from the base of the PR and between 3ca4611 and 43d6110.

📒 Files selected for processing (13)
  • src/app/about/TravelRoutes.tsx
  • src/app/contact/ContactContent.tsx
  • src/app/globals.css
  • src/components/MottoQuote.tsx
  • src/components/SubpageHero.tsx
  • src/components/hero/HeroSection.tsx
  • src/components/home/AboutSection.tsx
  • src/components/home/globe/GlobeSection.tsx
  • src/components/home/globe/globe-section.css
  • src/components/home/globe/utils/globe-helpers.ts
  • src/components/layout/Footer.tsx
  • src/components/layout/Header.tsx
  • src/components/quote/QuoteSection.tsx
📝 Walkthrough

Walkthrough

This PR applies mobile-first responsive updates across global styles and many components, refactors header/footer navigation and language/select styling, standardizes portrait image layout, converts the activities gallery to a client-side image slider, and refactors the GlobeSection with region presets and a mobile bottom-sheet drag interaction.

Changes

Mobile Responsive Site Redesign

Layer / File(s) Summary
Global CSS foundations and responsive tokens
src/app/globals.css
Extends theme with --radius-6, adds responsive h1/h2 and marquee rules, updates mobile hamburger/drawer styling, reworks .feathered-image overlay and adds .feathered-image-gray, and reduces --side-padding to 16px at small screens.
Header & Lang/Footer navigation
src/components/Header.tsx, src/components/LangSwitcher.tsx, src/components/ui/Select.tsx, src/components/Footer.tsx
Header: scroll auto-hide respects mobile drawer and adds mobile toggle with ARIA. LangSwitcher/Select: button and listbox styling adjusted to white text and whitespace-preserving option labels. Footer: refactored to render office-driven presence (work/personal email, phone, wechat, address lines, website) and a revised quote/motto area; navigation links updated.
About page responsive layouts
src/app/about/ClosingQuote.tsx, src/app/about/ContentBlocks.tsx, src/app/about/TravelRoutes.tsx
ClosingQuote adds relative z-10 and responsive side-padding. ContentBlocks collapses two_images and horizontal grids to single column on mobile and adjusts gaps. TravelRoutes adds explicit full-bleed padding, adjusts tab pill/button padding, and renders selected-place image as lg+ contained 2:1 crop and <lg full-bleed centered image.
Portrait image layout refactor
src/app/about/HeroHeader.tsx, src/components/SubpageHero.tsx, src/components/hero/HeroSection.tsx, src/components/quote/QuoteSection.tsx, src/app/contact/ContactContent.tsx, src/components/MottoQuote.tsx, src/components/activity/AboutSection.tsx
Standardizes portraits from fill + aspect-square + object-cover to w-full h-auto non-fill images, adjusts wrapper rounding to rounded-6, and adds responsive container constraints for consistent mobile presentation.
Activities page image slider
src/app/activities/ActivityContent.tsx
Converts to a client component using useState to manage a single-image-per-view slider; adds prev/next chevrons (conditional) and dot controls; updates "city not found" layout/typography.
Globe interactive map with regions and mobile bottom-sheet
src/components/activity/GlobeSection.tsx, src/components/activity/globe-helpers.ts, src/components/activity/globe-section.css
Large refactor: adds RegionPreset and REGIONS, increases MAX_ZOOM, switches map style to mapbox/satellite-streets-v12, clamps panning with maxBounds, manages terrain per projection, introduces region toolbar and exit control, centralizes zoom-edge feedback, and implements a mobile bottom-sheet with ResizeObserver-measured collapsed height and pointer drag snapping; CSS adds sheet handle, regions toolbar, zoom toast, and marker label pill styling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • 8seneca-hub/tabernam-site#9: Modifies shared About page components (ClosingQuote, ContentBlocks, TravelRoutes) that this PR's UI/layout refinements build upon.
  • 8seneca-hub/tabernam-site#7: Updates global styling patterns in src/app/globals.css including feathered-image and marquee-cell shadow vignettes that this PR extends further.

Poem

🐰 Hopping through responsive breakpoints with glee,
Mobile drawers open wide for me,
Portraits tidy, sliders glide along,
Globe and sheets snap to the beat of a song — ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "feat: add responsive" is vague and does not convey meaningful information about the extensive responsive design improvements made across multiple components. Replace with a more specific title that captures the main change, such as 'feat: improve responsive design across page layouts' or 'feat: add mobile-first responsive styling updates'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2nd-june

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.

❤️ Share

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

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
src/components/Header.tsx (1)

74-76: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Cancel pending RAF in effect cleanup.

The scroll effect removes the event listener but leaves a queued requestAnimationFrame callback alive. Add cancelAnimationFrame(raf) in cleanup to avoid post-unmount callback execution.

Suggested fix
   window.addEventListener('scroll', onScroll, { passive: true });
-  return () => window.removeEventListener('scroll', onScroll);
+  return () => {
+    window.removeEventListener('scroll', onScroll);
+    if (raf) cancelAnimationFrame(raf);
+  };
 }, []);
🤖 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 `@src/components/Header.tsx` around lines 74 - 76, The effect in Header
component adds a scroll listener via window.addEventListener('scroll', onScroll)
but does not cancel the queued requestAnimationFrame; capture the RAF id (e.g.,
raf) when calling requestAnimationFrame inside onScroll (or the effect) and in
the useEffect cleanup ensure you call cancelAnimationFrame(raf) in addition to
window.removeEventListener('scroll', onScroll) so no RAF callback runs after
unmount; update the cleanup to reference the same raf variable (useRef or scoped
let) used when scheduling the frame.
🤖 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 `@src/app/activities/ActivityContent.tsx`:
- Line 19: When the active city/id changes the local slider state "index" can
become stale and cause an out-of-range translation; add a useEffect that watches
the city identifier (id) and/or the images array length and calls setIndex to
clamp the current index into [0, images.length-1] (e.g. setIndex(prev =>
Math.max(0, Math.min(prev, images.length - 1))) or setIndex(0) when you prefer
resetting to the first slide). Place this effect near the existing useState
declaration for index so the component (and the translation logic that uses
index) always gets a valid value after id or images change.

In `@src/components/Footer.tsx`:
- Line 32: The websiteUrl value (const websiteUrl = office?.websiteUrl || '')
can be scheme-less and becomes a relative link; normalize it before binding to
href by trimming whitespace, returning an empty string when absent, and
prefixing "https://" when the value does not start with "http://" or "https://"
(keep existing mailto: or other schemes intact). Update the code that computes
websiteUrl and any places that bind it to href (the websiteUrl variable used in
the Footer component and the other href bindings referenced) so the rendered
href is always an absolute URL or empty.

In `@src/components/Header.tsx`:
- Around line 128-129: The header uses a custom breakpoint `max-[769px]` which
conflicts with the drawer/toggle CSS that targets `max-width: 768px`, causing a
broken edge state; update all occurrences of `max-[769px]` in the Header
component (e.g., the element with className "site-header" and other responsive
className usages inside Header.tsx) to `max-[768px]` so the header layout and
drawer/toggle CSS share the exact same breakpoint.

---

Outside diff comments:
In `@src/components/Header.tsx`:
- Around line 74-76: The effect in Header component adds a scroll listener via
window.addEventListener('scroll', onScroll) but does not cancel the queued
requestAnimationFrame; capture the RAF id (e.g., raf) when calling
requestAnimationFrame inside onScroll (or the effect) and in the useEffect
cleanup ensure you call cancelAnimationFrame(raf) in addition to
window.removeEventListener('scroll', onScroll) so no RAF callback runs after
unmount; update the cleanup to reference the same raf variable (useRef or scoped
let) used when scheduling the frame.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2b754130-0b65-448d-968a-cd329a3fe362

📥 Commits

Reviewing files that changed from the base of the PR and between 23cdefe and 8e8f207.

📒 Files selected for processing (16)
  • src/app/about/ClosingQuote.tsx
  • src/app/about/ContentBlocks.tsx
  • src/app/about/TravelRoutes.tsx
  • src/app/activities/ActivityContent.tsx
  • src/app/contact/ContactContent.tsx
  • src/app/globals.css
  • src/components/Footer.tsx
  • src/components/Header.tsx
  • src/components/MottoQuote.tsx
  • src/components/SubpageHero.tsx
  • src/components/activity/AboutSection.tsx
  • src/components/activity/GlobeSection.tsx
  • src/components/activity/globe-helpers.ts
  • src/components/activity/globe-section.css
  • src/components/hero/HeroSection.tsx
  • src/components/quote/QuoteSection.tsx

const params = useSearchParams();
const id = params?.get('id') ?? '';
const city = cities.find((c) => c.slug === id);
const [index, setIndex] = useState(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset/clamp slider index when city changes.

Line 19 keeps stale index across id changes; when the next city has fewer images, Line 61 can translate past the last slide and show an empty frame.

Suggested fix
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
@@
   const photos = city.photos ?? [];
   const last = photos.length - 1;
+
+  useEffect(() => {
+    setIndex(0);
+  }, [city.slug]);

Also applies to: 37-39, 61-61

🤖 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 `@src/app/activities/ActivityContent.tsx` at line 19, When the active city/id
changes the local slider state "index" can become stale and cause an
out-of-range translation; add a useEffect that watches the city identifier (id)
and/or the images array length and calls setIndex to clamp the current index
into [0, images.length-1] (e.g. setIndex(prev => Math.max(0, Math.min(prev,
images.length - 1))) or setIndex(0) when you prefer resetting to the first
slide). Place this effect near the existing useState declaration for index so
the component (and the translation logic that uses index) always gets a valid
value after id or images change.

const addressLines = office?.addressLines && office.addressLines.length > 0
? office.addressLines
: [t('footer.location')];
const websiteUrl = office?.websiteUrl || '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize websiteUrl before binding to href.

Right now a scheme-less value (e.g., tabernam.com) becomes a relative link. Normalize to absolute https://... before rendering.

Suggested fix
-  const websiteUrl = office?.websiteUrl || '';
+  const websiteUrlRaw = (office?.websiteUrl || '').trim();
+  const websiteUrl = websiteUrlRaw
+    ? (/^https?:\/\//i.test(websiteUrlRaw) ? websiteUrlRaw : `https://${websiteUrlRaw}`)
+    : '';

Also applies to: 109-113

🤖 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 `@src/components/Footer.tsx` at line 32, The websiteUrl value (const websiteUrl
= office?.websiteUrl || '') can be scheme-less and becomes a relative link;
normalize it before binding to href by trimming whitespace, returning an empty
string when absent, and prefixing "https://" when the value does not start with
"http://" or "https://" (keep existing mailto: or other schemes intact). Update
the code that computes websiteUrl and any places that bind it to href (the
websiteUrl variable used in the Footer component and the other href bindings
referenced) so the rendered href is always an absolute URL or empty.

Comment thread src/components/Header.tsx Outdated
Comment on lines 128 to 129
className="site-header fixed top-0 left-0 right-0 z-100 bg-gray-20 grid grid-cols-[auto_1fr_auto] max-[769px]:grid-cols-[1fr_auto] items-center gap-6 px-[var(--side-padding)] max-md:px-[16px] py-2.5"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unify mobile breakpoint contracts (769 vs 768) to avoid a broken edge state.

Line 128/138/146/152/158 use max-[769px], while the drawer/toggle CSS in src/app/globals.css only activates at max-width: 768px. At exactly 769px, header layout switches to mobile classes but drawer mechanics/toggle CSS don’t, causing inconsistent nav behavior.

Suggested fix
- className="... max-[769px]:grid-cols-[1fr_auto] ..."
+ className="... max-[768px]:grid-cols-[1fr_auto] ..."

- className="nav ... max-[769px]:gap-0"
+ className="nav ... max-[768px]:gap-0"

- <div className="hidden max-[769px]:block">
+ <div className="hidden max-[768px]:block">

- <div className="max-[769px]:hidden">
+ <div className="max-[768px]:hidden">

- className="nav-toggle ... hidden max-[769px]:block"
+ className="nav-toggle ... hidden max-[768px]:block"

Also applies to: 138-138, 146-158

🤖 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 `@src/components/Header.tsx` around lines 128 - 129, The header uses a custom
breakpoint `max-[769px]` which conflicts with the drawer/toggle CSS that targets
`max-width: 768px`, causing a broken edge state; update all occurrences of
`max-[769px]` in the Header component (e.g., the element with className
"site-header" and other responsive className usages inside Header.tsx) to
`max-[768px]` so the header layout and drawer/toggle CSS share the exact same
breakpoint.

Add responsive idle zoom (idleZoomFor) and restore-on-close camera
handling for city cards in the globe section, plus assorted responsive
adjustments across header, hero, quote, activities, about, and UI
components.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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: 1

Caution

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

⚠️ Outside diff range comments (2)
src/components/activity/GlobeSection.tsx (2)

289-318: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restored cards can close back to the wrong viewport.

When activeIdx is rehydrated from the saved globe state, prevActiveIdxRef.current is still null, so this branch snapshots the idle camera before any regionKey framing has happened. The close-button path then reuses that stale camera and the map comes back at the idle/world view while the restored region tab stays selected. Persist the pre-city camera in the restore payload, or bypass restoreOnCloseRef for restored sessions so close falls through to the region fitBounds.

🤖 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 `@src/components/activity/GlobeSection.tsx` around lines 289 - 318, The restore
path is capturing a stale preCityCamera because prevActiveIdxRef.current is null
during rehydration; update the logic around prevActiveIdxRef, preCityCameraRef,
and restoreOnCloseRef so restored sessions don't snapshot or use that stale
camera: either persist preCityCameraRef into the saved restore payload when
serializing the globe state, or (simpler) skip setting preCityCameraRef and set
restoreOnCloseRef.current = false when activeIdx was rehydrated so the
close-button branch falls through to the region fitBounds logic; specifically
change the block that currently checks prevActiveIdxRef.current === null (and
the subsequent map.flyTo) to only set preCityCameraRef when
prevActiveIdxRef.current !== null or when a new non‑rehydrated activation
occurs, and ensure restoreOnCloseRef is cleared for rehydrated activeIdx cases.

122-129: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The min-zoom toast misses the real floor after maxBounds.

maxBounds makes the effective minimum zoom viewport-dependent, but this listener still only compares against MIN_ZOOM. On larger viewports the map clamps above 1.2, so wheel/pinch users can hit the real floor without ever seeing the “minimum zoom” feedback. Reuse the same floor calculation from zoomBy() here so gesture and button zoom paths stay consistent.

Also applies to: 151-160

🤖 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 `@src/components/activity/GlobeSection.tsx` around lines 122 - 129, The
min-zoom toast is checking only MIN_ZOOM but the effective minimum zoom is
clamped by maxBounds (viewport-dependent), so update the zoom-change/gesture
listener(s) that currently compare against MIN_ZOOM (the block around
renderWorldCopies/maxBounds and the similar block later) to reuse the same floor
calculation used in zoomBy(): compute the effective minimum zoom the same way
zoomBy() does and compare against that value, then show the “minimum zoom” toast
when the gesture hits that computed floor; reference MIN_ZOOM, maxBounds, and
the zoomBy() helper so you mirror its exact logic rather than using a hardcoded
MIN_ZOOM check.
♻️ Duplicate comments (1)
src/app/activities/ActivityContent.tsx (1)

3-3: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset slider index when city changes.

The past review comment remains valid. When the user navigates to a different city (via query param id), the index state is not reset. If the previous city had index=3 and the new city has only 2 photos, line 61 will compute translateX(-300%) on a 2-slide track, displaying a blank frame.

🔄 Proposed fix
-import { useState } from 'react';
+import { useEffect, useState } from 'react';

Then add an effect after line 19:

 const [index, setIndex] = useState(0);
+
+useEffect(() => {
+  setIndex(0);
+}, [id]);

Also applies to: 19-19

🤖 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 `@src/app/activities/ActivityContent.tsx` at line 3, The slider index isn't
reset when the route/query `id` (city) changes, so add a useEffect in the
ActivityContent component that watches the city identifier (the `id` query param
or prop used to fetch `photos`) and calls setIndex(0) when it changes; ensure
the effect depends on that id (and optionally on `photos.length`) to reset
`index` to 0 to avoid computing an out-of-range translateX for the new photo
set.
🧹 Nitpick comments (3)
src/app/activities/ActivityContent.tsx (3)

68-69: ⚖️ Poor tradeoff

Consider using next/image for optimization.

The slider uses a regular <img> tag, which bypasses Next.js automatic image optimization, lazy loading, and responsive srcsets. While Directus CMS URLs may require additional configuration, integrating next/image would improve performance and user experience.

Example migration
-{/* eslint-disable-next-line `@next/next/no-img-element` */}
-<img className="w-full h-full object-cover" src={src} alt={`${name} — ${i + 1}`} />
+<Image
+  className="object-cover"
+  src={src}
+  alt={`${name} — ${i + 1}`}
+  fill
+  sizes="(max-width: 768px) 100vw, 1320px"
+/>

You'll need to import Image from next/image and configure remotePatterns in next.config.js for your Directus domain.

🤖 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 `@src/app/activities/ActivityContent.tsx` around lines 68 - 69, Replace the
plain <img> in ActivityContent (the element rendering src/alt using name and i)
with Next.js Image: import Image from "next/image", remove the eslint-disable
comment, and render Image with appropriate props (either width/height or fill
plus object-fit via className) and use alt={`${name} — ${i + 1}`} and src={src};
additionally, add your Directus domain to next.config.js remotePatterns so
external Directus URLs are allowed by next/image. Ensure you update any
surrounding markup/CSS to support Image's layout (e.g., position: relative when
using fill).

97-107: 💤 Low value

Optionally add aria-live region for screen reader announcements.

The dot navigation allows users to jump between slides, but screen reader users won't receive automatic announcements of slide changes. Consider adding an aria-live="polite" region to announce the current slide.

Example implementation
 <div className="mt-6 flex justify-center gap-2">
+  <div className="sr-only" aria-live="polite" aria-atomic="true">
+    Image {index + 1} of {photos.length}
+  </div>
   {photos.map((_, i) => (

Add this CSS if sr-only is not defined:

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
🤖 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 `@src/app/activities/ActivityContent.tsx` around lines 97 - 107, Add an
aria-live region next to the dot navigation so screen readers announce slide
changes: create a visually-hidden element (e.g., class "sr-only") within the
ActivityContent component near the div that renders photos.map(...) and the
buttons, and render text like `Current slide ${index + 1} of ${photos.length}`
inside it; update this element by referencing the existing index state (used by
setIndex) so it changes whenever a user clicks a dot. Ensure the element uses
aria-live="polite" and is styled with an sr-only utility (or add the provided
sr-only CSS) so it is announced but not visible.

104-104: ActivityContent.tsx: bg-gray-60 is defined in the theme

  • src/app/globals.css defines the theme token --color-gray-60 (backed by --gray-60: #C2CBD7``), so bg-gray-60 is a valid palette utility.
  • Optional: consider using next/image for the slider images (if it fits your Directus setup) and add an aria-live announcement when the slide changes.
🤖 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 `@src/app/activities/ActivityContent.tsx` at line 104, The class bg-gray-60
used in ActivityContent.tsx is valid because globals.css defines
--color-gray-60, so either leave the current class on the element (the template
string containing i === index ? 'w-6 bg-brand' : 'w-2 bg-gray-60') or, to avoid
Tailwind purge/utility-missing risks, replace the utility with an explicit
CSS-variable utility such as bg-[color:var(--color-gray-60)] or ensure your
Tailwind config safelists 'bg-gray-60'; verify globals.css contains
--color-gray-60 and that your build config will preserve that utility.
🤖 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 `@src/components/LangSwitcher.tsx`:
- Line 16: The Tailwind v3-style leading "!" inside arbitrary variants in the
LangSwitcher className (e.g., "[&>button]:!text-white",
"[&>button]:hover:!bg-transparent", "[&>button>svg]:!text-white") must be
changed to the v4 trailing suffix form (e.g., "[&>button]:text-white!",
"[&>button]:hover:bg-transparent!", "[&>button>svg]:text-white!") so the
important overrides apply; update the className string in the LangSwitcher
component accordingly and then re-check text contrast at both header breakpoints
where LangSwitcher is rendered to ensure accessibility.

---

Outside diff comments:
In `@src/components/activity/GlobeSection.tsx`:
- Around line 289-318: The restore path is capturing a stale preCityCamera
because prevActiveIdxRef.current is null during rehydration; update the logic
around prevActiveIdxRef, preCityCameraRef, and restoreOnCloseRef so restored
sessions don't snapshot or use that stale camera: either persist
preCityCameraRef into the saved restore payload when serializing the globe
state, or (simpler) skip setting preCityCameraRef and set
restoreOnCloseRef.current = false when activeIdx was rehydrated so the
close-button branch falls through to the region fitBounds logic; specifically
change the block that currently checks prevActiveIdxRef.current === null (and
the subsequent map.flyTo) to only set preCityCameraRef when
prevActiveIdxRef.current !== null or when a new non‑rehydrated activation
occurs, and ensure restoreOnCloseRef is cleared for rehydrated activeIdx cases.
- Around line 122-129: The min-zoom toast is checking only MIN_ZOOM but the
effective minimum zoom is clamped by maxBounds (viewport-dependent), so update
the zoom-change/gesture listener(s) that currently compare against MIN_ZOOM (the
block around renderWorldCopies/maxBounds and the similar block later) to reuse
the same floor calculation used in zoomBy(): compute the effective minimum zoom
the same way zoomBy() does and compare against that value, then show the
“minimum zoom” toast when the gesture hits that computed floor; reference
MIN_ZOOM, maxBounds, and the zoomBy() helper so you mirror its exact logic
rather than using a hardcoded MIN_ZOOM check.

---

Duplicate comments:
In `@src/app/activities/ActivityContent.tsx`:
- Line 3: The slider index isn't reset when the route/query `id` (city) changes,
so add a useEffect in the ActivityContent component that watches the city
identifier (the `id` query param or prop used to fetch `photos`) and calls
setIndex(0) when it changes; ensure the effect depends on that id (and
optionally on `photos.length`) to reset `index` to 0 to avoid computing an
out-of-range translateX for the new photo set.

---

Nitpick comments:
In `@src/app/activities/ActivityContent.tsx`:
- Around line 68-69: Replace the plain <img> in ActivityContent (the element
rendering src/alt using name and i) with Next.js Image: import Image from
"next/image", remove the eslint-disable comment, and render Image with
appropriate props (either width/height or fill plus object-fit via className)
and use alt={`${name} — ${i + 1}`} and src={src}; additionally, add your
Directus domain to next.config.js remotePatterns so external Directus URLs are
allowed by next/image. Ensure you update any surrounding markup/CSS to support
Image's layout (e.g., position: relative when using fill).
- Around line 97-107: Add an aria-live region next to the dot navigation so
screen readers announce slide changes: create a visually-hidden element (e.g.,
class "sr-only") within the ActivityContent component near the div that renders
photos.map(...) and the buttons, and render text like `Current slide ${index +
1} of ${photos.length}` inside it; update this element by referencing the
existing index state (used by setIndex) so it changes whenever a user clicks a
dot. Ensure the element uses aria-live="polite" and is styled with an sr-only
utility (or add the provided sr-only CSS) so it is announced but not visible.
- Line 104: The class bg-gray-60 used in ActivityContent.tsx is valid because
globals.css defines --color-gray-60, so either leave the current class on the
element (the template string containing i === index ? 'w-6 bg-brand' : 'w-2
bg-gray-60') or, to avoid Tailwind purge/utility-missing risks, replace the
utility with an explicit CSS-variable utility such as
bg-[color:var(--color-gray-60)] or ensure your Tailwind config safelists
'bg-gray-60'; verify globals.css contains --color-gray-60 and that your build
config will preserve that utility.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5088d387-5f2d-4eb8-a243-271ef6aaecac

📥 Commits

Reviewing files that changed from the base of the PR and between 8e8f207 and 3ca4611.

📒 Files selected for processing (15)
  • src/app/about/HeroHeader.tsx
  • src/app/about/TravelRoutes.tsx
  • src/app/activities/ActivityContent.tsx
  • src/app/globals.css
  • src/components/Header.tsx
  • src/components/HomeMarquee.tsx
  • src/components/LangSwitcher.tsx
  • src/components/SubpageHero.tsx
  • src/components/activity/AboutSection.tsx
  • src/components/activity/GlobeSection.tsx
  • src/components/activity/globe-helpers.ts
  • src/components/activity/globe-section.css
  • src/components/hero/HeroSection.tsx
  • src/components/quote/QuoteSection.tsx
  • src/components/ui/Select.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/HomeMarquee.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/components/SubpageHero.tsx
  • src/components/quote/QuoteSection.tsx
  • src/components/activity/globe-helpers.ts
  • src/components/Header.tsx
  • src/app/globals.css
  • src/components/activity/globe-section.css

onChange={switchLang}
ariaLabel="Language"
className="lang-switcher ml-1 [&>button]:!text-dark [&>button]:!text-[18px] [&>button]:!font-normal [&>button]:!normal-case [&>button]:!tracking-[-0.007em] [&>button]:hover:!bg-transparent [&>button>svg]:!text-dark"
className="lang-switcher ml-1 [&>button]:!text-white [&>button]:!text-[18px] [&>button]:!font-normal [&>button]:!normal-case [&>button]:!tracking-[-0.007em] [&>button]:hover:!bg-transparent [&>button>svg]:!text-white"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all Tailwind important modifiers using old v3 syntax.
# Expect: Instances of `:!` or `!text-` or other `!utility-` patterns.

rg -n ':\!' --type=tsx --type=ts --type=jsx --type=js -C2

Repository: 8seneca-hub/tabernam-site

Length of output: 140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect Tailwind version
echo "== package.json (tailwind-related) =="
if [ -f package.json ]; then
  cat package.json | sed -n '1,200p' | rg -n '"tailwindcss"|tailwindcss|tailwind' || true
else
  echo "package.json not found"
fi

# 2) Inspect LangSwitcher className line(s)
echo
echo "== src/components/LangSwitcher.tsx (relevant lines) =="
if [ -f src/components/LangSwitcher.tsx ]; then
  nl -ba src/components/LangSwitcher.tsx | sed -n '1,120p' | rg -n 'LangSwitcher|lang-switcher|className' -n || true
  # Print around line ~16 regardless
  nl -ba src/components/LangSwitcher.tsx | sed -n '10,40p'
else
  echo "src/components/LangSwitcher.tsx not found"
fi

# 3) Find Tailwind v3-style important-in-arbitrary-variant syntax: :!...
echo
echo "== Occurrences of ':!' in TS/TSX/JS/JSX (arbitrary variants) =="
rg -n ':!' --type=tsx --type=ts --type=jsx --type=js -C1 || true

# Also catch plain '!text-' patterns in class strings
echo
echo "== Occurrences of '!text-' / '!bg-' / '!font-' in TS/TSX/JS/JSX =="
rg -n "![a-zA-Z]+-(?:\\[.*?\\]|[-_a-zA-Z0-9]+)" --type=tsx --type=ts --type=jsx --type=js -C0 || true

# 4) Find where LangSwitcher is rendered so we can reason about backgrounds/contrast
echo
echo "== Usages of <LangSwitcher .../> =="
rg -n '<\s*LangSwitcher\b' -S src --glob='*.tsx' --glob='*.ts' || true

echo
echo "== Usages of LangSwitcher symbol =="
rg -n 'LangSwitcher' -S src --glob='*.tsx' --glob='*.ts' -C1 || true

Repository: 8seneca-hub/tabernam-site

Length of output: 339


🌐 Web query:

Tailwind CSS v4 important modifier arbitrary variants syntax order ! must come after utility name text-white! :!text-white breaking change

💡 Result:

In Tailwind CSS v4, the important modifier syntax has been updated to improve consistency and readability. The recommended way to mark a utility as important is to append the exclamation mark ! to the very end of the utility class (e.g., text-white!) [1][2][3]. While the previous v3 syntax (placing ! at the beginning of the utility name, such as !text-white) is still supported for backwards compatibility, it is considered deprecated [4][3]. Key details regarding this change: 1. Consistency: Moving the ! to the end makes the syntax easier to reason about, particularly when using multiple variants (e.g., hover:text-white!) [5]. 2. Arbitrary Variants: The rule applies consistently to standard utilities and arbitrary variants [1]. When using arbitrary variants, the important modifier should still be applied to the specific utility class being modified [3]. 3. Migration: The Tailwind CSS upgrade tool (npx @tailwindcss/upgrade``) is designed to handle this migration automatically [6]. 4. @apply usage: Inside `@apply` directives, the `!important` suffix is the standard approach in v4; placing `!` at the start of utilities within `@apply` is deprecated [5]. For projects that require all utility classes to be marked as important, you can use the `important` flag during import, which replaces the `important: true` configuration in the older `tailwind.config.js` format [7][8]: `@import` "tailwindcss" important; Top results for this research: [4], [5], [7], [1], [2], [3], [8], [6]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show LangSwitcher.tsx with line numbers around the target area
echo "== File: src/components/LangSwitcher.tsx =="
if [ -f src/components/LangSwitcher.tsx ]; then
  awk 'NR>=1 && NR<=120 {printf "%5d:%s\n", NR, $0}' src/components/LangSwitcher.tsx
else
  echo "Missing: src/components/LangSwitcher.tsx"
fi

# 2) Find usages of LangSwitcher
echo
echo "== Usages of <LangSwitcher .../> =="
rg -n "<\\s*LangSwitcher\\b" src --glob='*.tsx' --glob='*.ts' || true

echo
echo "== Usages of LangSwitcher identifier =="
rg -n "\\bLangSwitcher\\b" src --glob='*.tsx' --glob='*.ts' -C1 || true

# 3) Find old arbitrary-variant important form: :!
echo
echo "== Occurrences of ':!' in class strings / TS/TSX/JS/JSX =="
rg -n ":\!" src --type=tsx --type=ts --type=jsx --type=js -C1 || true

# 4) Find v4-style important suffix in class strings
echo
echo "== Occurrences of '*!' important suffix in class strings (common ones) =="
rg -n "text-white!" src --type=tsx --type=ts --type=jsx --type=js -C0 || true
rg -n "\\!bg-" src --type=tsx --type=ts --type=jsx --type=js -C0 || true
rg -n "[a-zA-Z0-9_\\-\\[\\]]+!" src --type=tsx --type=ts --type=jsx --type=js -C0 || true

Repository: 8seneca-hub/tabernam-site

Length of output: 2225


🌐 Web query:

Tailwind CSS v4 arbitrary variants important modifier syntax example "[&>button]:text-white!" and whether "[&>button]:!text-white" works

💡 Result:

In Tailwind CSS v4, the syntax for the important modifier has been standardized to be used as a suffix at the end of a utility class [1][2]. For your example, the correct syntax is: [&>button]:text-white! This applies the!important modifier to the text-white utility within the arbitrary variant [3]. Regarding your question about [&>button]:!text-white: This syntax is generally not supported in Tailwind CSS v4 [1][2]. Placing the! modifier at the beginning of the utility class (inside the arbitrary variant or otherwise) was the convention in v3, but v4 strictly requires the modifier to be placed as a suffix [1][2]. While some build configurations or legacy setups might occasionally process the prefix correctly depending on how the utility is parsed, the official and recommended practice for v4 is to use the trailing! suffix for all utility classes [1][2].

Citations:


🌐 Web query:

Does Tailwind CSS v4 support the old important prefix syntax inside arbitrary variants like "[&>button]:!text-white" or is it invalid (must be "[&>button]:text-white!")?

💡 Result:

In Tailwind CSS v4, the correct syntax for making a utility important is to place the! modifier at the end of the class name (e.g., text-white!) [1]. While Tailwind CSS v4 generally maintains some backwards compatibility for v3 features, the established and recommended syntax for marking utilities as important in v4 is exclusively the trailing! modifier [1]. Regarding arbitrary variants like [&>button]:!text-white, this syntax is effectively invalid in the context of v4's preferred styling patterns. You should use the trailing modifier syntax within the arbitrary variant: [&>button]:text-white! [1]. To summarize the relevant changes in v4: - The! modifier must be placed at the end of the class name, whereas in v3 it was placed at the beginning [1]. - This trailing! syntax applies to all utilities, including those used within arbitrary variants [1]. - While some older patterns may be supported for transitionary compatibility, the v4 documentation explicitly instructs developers to adopt the trailing! modifier [1].

Citations:


Fix Tailwind v4 !important syntax in LangSwitcher className.

src/components/LangSwitcher.tsx uses v3-style leading ! inside arbitrary variants (e.g., [&>button]:!text-white, [&>button]:hover:!bg-transparent, [&>button>svg]:!text-white). Tailwind v4 expects the trailing suffix form (text-white!, bg-transparent!), otherwise these important overrides may not apply.

🐛 Proposed fix
-      className="lang-switcher ml-1 [&>button]:!text-white [&>button]:!text-[18px] [&>button]:!font-normal [&>button]:!normal-case [&>button]:!tracking-[-0.007em] [&>button]:hover:!bg-transparent [&>button>svg]:!text-white"
+      className="lang-switcher ml-1 [&>button]:text-white! [&>button]:text-[18px]! [&>button]:font-normal! [&>button]:normal-case! [&>button]:tracking-[-0.007em]! [&>button]:hover:bg-transparent! [&>button>svg]:text-white!"

After updating, re-check text contrast in both header breakpoints where LangSwitcher is rendered.

📝 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.

Suggested change
className="lang-switcher ml-1 [&>button]:!text-white [&>button]:!text-[18px] [&>button]:!font-normal [&>button]:!normal-case [&>button]:!tracking-[-0.007em] [&>button]:hover:!bg-transparent [&>button>svg]:!text-white"
className="lang-switcher ml-1 [&>button]:text-white! [&>button]:text-[18px]! [&>button]:font-normal! [&>button]:normal-case! [&>button]:tracking-[-0.007em]! [&>button]:hover:bg-transparent! [&>button>svg]:text-white!"
🤖 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 `@src/components/LangSwitcher.tsx` at line 16, The Tailwind v3-style leading
"!" inside arbitrary variants in the LangSwitcher className (e.g.,
"[&>button]:!text-white", "[&>button]:hover:!bg-transparent",
"[&>button>svg]:!text-white") must be changed to the v4 trailing suffix form
(e.g., "[&>button]:text-white!", "[&>button]:hover:bg-transparent!",
"[&>button>svg]:text-white!") so the important overrides apply; update the
className string in the LangSwitcher component accordingly and then re-check
text contrast at both header breakpoints where LangSwitcher is rendered to
ensure accessibility.

@ha-phuongvu ha-phuongvu merged commit d1135b4 into main Jun 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant