Skip to content

Neobrutalist-Bento student portal + drag-to-confirm payment slider - #9

Open
thribhuvan003 wants to merge 2 commits into
mainfrom
feat/student-neobrutalist-bento
Open

Neobrutalist-Bento student portal + drag-to-confirm payment slider#9
thribhuvan003 wants to merge 2 commits into
mainfrom
feat/student-neobrutalist-bento

Conversation

@thribhuvan003

@thribhuvan003 thribhuvan003 commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Neobrutalist-Bento student redesign + drag-to-confirm payment slider

Upgrades the student ordering portal (src/components/portal-student/*) and the static demo (public/demo/student.html) to a premium Winter-2026 Neobrutalist-Bento visual system, and replaces the payment-confirm button with a custom drag-to-confirm slider.

Note: this supersedes the earlier "Editorial Minimalist" student styling on main. The Files-changed view shows only this branch's changes; merge may require choosing this branch's version on overlapping files.

Design system

  • 3px solid-black borders, flat light/medium offset shadows (soft, not pitch-black), cement-sky #4F6E8C accent, bento cards
  • Distinctive type per role — Unbounded (headings) · Bricolage Grotesque (titles) · Hanken Grotesk (body) · Space Mono (prices/timers/OTP)
  • Token-driven via [data-portal="student"] in globals.css; kitchen/admin portals untouched

Interactions

  • New DragToConfirm (framer-motion) replaces the "I paid" confirm button — drag past ~85%, spring release, reduced-motion fallback. Vanilla pointer/touch/keyboard equivalent in the demo.
  • Custom neobrutalist canteen dropdown replaces the native OS <select>

Data rule

  • No Supabase/server-action/realtime/logic changes — styling + the single confirm-button swap only.

Verification

  • tsc --noEmit clean · next lint clean (only a pre-existing admin warning)
  • demo:verify passes for student.html
  • Live walk-through (Playwright): menu, cart, payment slider (drag → confirm → tracking → OTP), canteen dropdown selection

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Drag-to-confirm slider for payment confirmation replaces the previous button interaction
    • Custom dropdown menu for canteen selection
  • Style

    • Redesigned interface to "Winter 2026" Neobrutalist-Bento theme with blue/indigo color palette
    • Updated borders, shadows, and button styling throughout the app
    • Added premium typefaces for improved typography
  • Documentation

    • New comprehensive UI specification guide published

…slider

Upgrade the student ordering portal and static demo to a Winter-2026 Neobrutalist-Bento system:

- 3px solid-black borders, soft light/medium offset shadows, cement-sky (#4F6E8C) accent, bento cards

- Distinctive type: Unbounded (headings), Bricolage (titles), Hanken Grotesk (body), Space Mono (numerics)

- New framer-motion DragToConfirm slider replaces the payment-confirm button (dynamic portal + vanilla demo)

- Custom neobrutalist canteen dropdown replaces the native OS select

- Token-driven via [data-portal=student]; kitchen/admin untouched; no data/Supabase/logic changes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tray Ready Ready Preview, Comment Jun 23, 2026 11:49am

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ff348a1-ac37-415c-81f8-cec1fe8655ad

📥 Commits

Reviewing files that changed from the base of the PR and between 9886379 and fd1e5e3.

📒 Files selected for processing (11)
  • scripts/landing-audit.mjs
  • src/app/globals.css
  • src/components/landing/landing-motion.tsx
  • src/components/landing/landing-page.tsx
  • src/components/landing/sections/CampusModelSection.tsx
  • src/components/landing/sections/ClosingSection.tsx
  • src/components/landing/sections/MetricsAndTicker.tsx
  • src/components/landing/sections/SyncSection.tsx
  • src/components/landing/sections/TrayHero.tsx
  • src/components/landing/sections/TrustSection.tsx
  • src/lib/motion/tray-framer.tsx
 ___________________________________________
< Sending Skynet back to the drawing board. >
 -------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
📝 Walkthrough

Walkthrough

The student portal receives a full "Winter 2026 Neobrutalist-Bento" retheme: CSS variables shift from red/amber to blue/indigo, three new Google fonts are wired in, a new DragToConfirm Framer Motion component replaces click-based payment confirmation, the demo page's native canteen <select> is replaced by a custom dropdown, and a ui_spec.md design reference is added.

Changes

Neobrutalist-Bento Winter 2026 Student Portal Overhaul

Layer / File(s) Summary
Design tokens, fonts, and UI spec
src/app/layout.tsx, src/app/globals.css, ui_spec.md
Three new Google fonts (Unbounded, Hanken_Grotesk, Space_Mono) wired into RootLayout. globals.css replaces the Vintage Editorial Pulp student theme with Neobrutalist-Bento --ns-* primitives, role-specific font tokens (--font-display-ns, --font-title-ns, --font-body-ns, --font-num-ns), and .ns-card/.ns-press utility classes. ui_spec.md documents the full design spec including bento grid rules, drag-to-confirm patterns, and font CDN references.
DragToConfirm React component
src/components/portal-student/drag-to-confirm.tsx
New exported component using Framer Motion useMotionValue/useTransform to animate a fill bar and label opacity as the user drags. On release, springs thumb to start or confirmed end based on threshold comparison and calls onConfirm. Renders a plain-button fallback for reduced-motion users.
Demo page palette and Neobrutalist-Bento CSS
public/demo/student.html (lines 10–332)
Replaces all red/amber accent CSS variables with blue/indigo across every component state (live pill, category button, search focus, cart bar, QR animation, tracking ring, OTP visuals, menu controls). Adds Neobrutalist-Bento override block with black borders, font overrides, custom canteen-dropdown CSS, and pay-slider CSS including reduced-motion handling.
Demo custom canteen dropdown
public/demo/student.html (lines 369–544)
Replaces <select id="canteenSelect"> with a #canteenDD div/trigger/panel structure. JavaScript adds syncCanteenDD(), closeCanteenDD(), and expands initCanteenSelect() to render panel buttons from TD.listCanteens() with click/keyboard/click-away/aria-expanded wiring.
Demo drag-to-confirm pay slider
public/demo/student.html (lines 443–727)
Replaces #btnPaid with #paySlider markup. initPaySlider() implements mouse/touch/keyboard drag tracking, done-state marking, window.__resetPaySlider(), and resize measurement. startPayment() resets the slider before starting the countdown; confirmPaid() uses a paymentConfirming guard, updates the slider label, stops the timer, generates OTP, and pushes the order.
Portal shell component restyling
src/components/portal-student/top-bar.tsx, src/components/portal-student/canteen-switcher.tsx, src/components/portal-student/cart-drawer.tsx
top-bar.tsx switches header, logo, search input, and action buttons to solid black borders and rounded-lg. canteen-switcher.tsx updates ocean-token accent, dropdown border/shadow, and active-state colors. cart-drawer.tsx uses ns-card for desktop aside, ns-press for mobile trigger, and updated totals/checkout button classes.
Menu board and item card restyling
src/components/portal-student/menu-board.tsx, src/components/portal-student/menu-item-card.tsx
menu-board.tsx updates style constants S with new font tokens, changes hero/section headings to bold fontHeading, reworks mobile category chips, and updates SpecialCard/RegularCard borders, hover shadows (--ns-shadow), price fonts (--font-num-ns), and add buttons to ns-press. menu-item-card.tsx switches the card shell to inline CSS-variable style, updates typography, quantity control border, and Add button classes.
PayPanel + TrackPanel integration
src/components/portal-student/pay-panel.tsx, src/components/portal-student/track-panel.tsx
pay-panel.tsx replaces the direct-UPI "I paid" button with DragToConfirm wired to onIvePaid/verifying; updates all heading/button font tokens; switches payment container, Razorpay button, timer card, and order summary to ns-card/ns-press/design tokens. track-panel.tsx adds getMyOrderOtpWithTimeout (10 s Promise.race), and updates pickup-code panel, OTP digit boxes, stepper, "You ordered" card, and total typography to Neobrutalist-Bento styling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hop hop, the red has fled,
Blue borders bold and bento-spread!
Drag to pay — no button tap,
The slider snaps, the OTP maps.
Neobrutalist winter's here at last,
Sharp shadows, thick lines, crisp and fast! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the main changes: the redesign of the student portal to a Neobrutalist-Bento visual system and the introduction of a drag-to-confirm payment slider component.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/student-neobrutalist-bento

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.

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

🧹 Nitpick comments (1)
src/app/layout.tsx (1)

209-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using array join for readability.

The className string is now quite long with 19 font variables. While functional, an array-based approach improves maintainability:

className={[
  inter.variable,
  fraunces.variable,
  manrope.variable,
  jetbrains.variable,
  instrumentSerif.variable,
  newsreader.variable,
  spaceGrotesk.variable,
  geist.variable,
  geistMono.variable,
  bebasNeue.variable,
  cormorant.variable,
  plusJakarta.variable,
  barlowCondensed.variable,
  dmSerif.variable,
  dmMono.variable,
  kronaOne.variable,
  bricolage.variable,
  unbounded.variable,
  hankenGrotesk.variable,
  spaceMono.variable,
].join(' ')}
🤖 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/layout.tsx` at line 209, The className attribute contains a very long
concatenated string of 19 font variables making it difficult to read and
maintain. Convert this into an array containing all the font variables
(inter.variable, fraunces.variable, manrope.variable, jetbrains.variable,
instrumentSerif.variable, newsreader.variable, spaceGrotesk.variable,
geist.variable, geistMono.variable, bebasNeue.variable, cormorant.variable,
plusJakarta.variable, barlowCondensed.variable, dmSerif.variable,
dmMono.variable, kronaOne.variable, bricolage.variable, unbounded.variable,
hankenGrotesk.variable, spaceMono.variable) and use the join method with a space
separator to create the final className string. This will improve readability
and make it easier to manage the list of fonts in the future.
🤖 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 `@public/demo/student.html`:
- Around line 528-533: The syncCanteenDD function toggles the is-active class on
dropdown options but does not update the aria-selected attribute for
accessibility. In the forEach loop that processes elements with the data-canteen
attribute, add a call to setAttribute to set aria-selected to "true" when the
element's dataset.canteen matches activeId, and "false" otherwise, following the
same pattern that syncSegs uses for keeping aria-selected in sync with the
active state.
- Around line 640-649: The confirmPaid() function sets the text content of the
paySliderLabel element to "Confirming…" but the element's opacity remains at 0
from the previous slider animation, making the text invisible. Restore the
opacity of the label element (retrieved by getElementById("paySliderLabel")) to
a visible value (such as 1) before or immediately after setting its textContent
to ensure the confirmation feedback is visible during the confirm delay.

In `@src/components/portal-student/canteen-switcher.tsx`:
- Around line 57-61: The canteen-switcher dropdown controls currently have
outline-none applied which removes keyboard focus indicators without providing a
visible focus-visible alternative, making keyboard navigation difficult. Remove
the outline-none class from the button element with onClick={() =>
setOpen(!open)} at line 57-61, and replace it with focus-visible:outline or a
similar focus-visible utility class to restore visible keyboard focus. Apply the
same fix to the other dropdown control elements mentioned at lines 101-101 and
112-116 to ensure consistent keyboard accessibility across all interactive
dropdown components.

In `@ui_spec.md`:
- Around line 111-124: The CSS Grid schema visualizer code block is missing a
language identifier for the fenced code block. Add the `text` language
identifier to the opening fence of the ASCII art grid diagram by changing the
opening ``` to ```text. This improves accessibility and enables proper syntax
highlighting for the markdown document.
- Around line 224-330: Update the DragConfirmSlider component in the spec to
match the actual implementation constants. In the handleDragEnd function, change
the drag threshold check from 0.95 to 0.85 to match the THRESHOLD constant in
the actual implementation. Additionally, update the thumbWidth constant from 52
to 56 to match THUMB, and update the padding constant from 12 to 4 to match PAD
in the actual implementation. These changes ensure the spec documentation
accurately reflects how the component actually behaves.

---

Nitpick comments:
In `@src/app/layout.tsx`:
- Line 209: The className attribute contains a very long concatenated string of
19 font variables making it difficult to read and maintain. Convert this into an
array containing all the font variables (inter.variable, fraunces.variable,
manrope.variable, jetbrains.variable, instrumentSerif.variable,
newsreader.variable, spaceGrotesk.variable, geist.variable, geistMono.variable,
bebasNeue.variable, cormorant.variable, plusJakarta.variable,
barlowCondensed.variable, dmSerif.variable, dmMono.variable, kronaOne.variable,
bricolage.variable, unbounded.variable, hankenGrotesk.variable,
spaceMono.variable) and use the join method with a space separator to create the
final className string. This will improve readability and make it easier to
manage the list of fonts in the future.
🪄 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 Plus

Run ID: e8ceea3a-75da-4d27-a809-b2d0671653a6

📥 Commits

Reviewing files that changed from the base of the PR and between f901071 and 9886379.

📒 Files selected for processing (12)
  • public/demo/student.html
  • src/app/globals.css
  • src/app/layout.tsx
  • src/components/portal-student/canteen-switcher.tsx
  • src/components/portal-student/cart-drawer.tsx
  • src/components/portal-student/drag-to-confirm.tsx
  • src/components/portal-student/menu-board.tsx
  • src/components/portal-student/menu-item-card.tsx
  • src/components/portal-student/pay-panel.tsx
  • src/components/portal-student/top-bar.tsx
  • src/components/portal-student/track-panel.tsx
  • ui_spec.md

Comment thread public/demo/student.html
Comment on lines +528 to +533
function syncCanteenDD(activeId){
var lbl=document.getElementById("canteenDDLabel");var c=TD.getCanteen(activeId);
if(lbl&&c)lbl.textContent=c.name;
var panel=document.getElementById("canteenDDPanel");
if(panel)panel.querySelectorAll("[data-canteen]").forEach(function(b){b.classList.toggle("is-active",b.dataset.canteen===activeId);});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update aria-selected on dropdown options, not just the is-active class.

syncSegs keeps aria-selected in sync (Line 526), but syncCanteenDD only toggles the is-active class. After switching canteens the panel option that was initially rendered with aria-selected="true" stays selected for assistive tech, so the listbox reports the wrong active option.

🛡️ Proposed fix
-  if(panel)panel.querySelectorAll("[data-canteen]").forEach(function(b){b.classList.toggle("is-active",b.dataset.canteen===activeId);});
+  if(panel)panel.querySelectorAll("[data-canteen]").forEach(function(b){var on=b.dataset.canteen===activeId;b.classList.toggle("is-active",on);b.setAttribute("aria-selected",on?"true":"false");});
📝 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
function syncCanteenDD(activeId){
var lbl=document.getElementById("canteenDDLabel");var c=TD.getCanteen(activeId);
if(lbl&&c)lbl.textContent=c.name;
var panel=document.getElementById("canteenDDPanel");
if(panel)panel.querySelectorAll("[data-canteen]").forEach(function(b){b.classList.toggle("is-active",b.dataset.canteen===activeId);});
}
function syncCanteenDD(activeId){
var lbl=document.getElementById("canteenDDLabel");var c=TD.getCanteen(activeId);
if(lbl&&c)lbl.textContent=c.name;
var panel=document.getElementById("canteenDDPanel");
if(panel)panel.querySelectorAll("[data-canteen]").forEach(function(b){var on=b.dataset.canteen===activeId;b.classList.toggle("is-active",on);b.setAttribute("aria-selected",on?"true":"false");});
}
🤖 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 `@public/demo/student.html` around lines 528 - 533, The syncCanteenDD function
toggles the is-active class on dropdown options but does not update the
aria-selected attribute for accessibility. In the forEach loop that processes
elements with the data-canteen attribute, add a call to setAttribute to set
aria-selected to "true" when the element's dataset.canteen matches activeId, and
"false" otherwise, following the same pattern that syncSegs uses for keeping
aria-selected in sync with the active state.

Comment thread public/demo/student.html
Comment on lines 640 to +649
function confirmPaid(){
var btn=document.getElementById("btnPaid");if(!btn||btn.classList.contains("is-confirming"))return;
btn.classList.add("is-confirming");btn.textContent="Confirming…";
if(paymentConfirming)return;paymentConfirming=true;
var lbl=document.getElementById("paySliderLabel");if(lbl)lbl.textContent="Confirming…";
setTimeout(function(){
btn.classList.remove("is-confirming");btn.textContent="I've paid — confirm my order";
if(payInterval){clearInterval(payInterval);payInterval=null;}
pickupOtp=genOtp();pushOrderToKitchen();
var now=new Date();var el=document.getElementById("placedTime");if(el)el.textContent=now.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});
var rsc=document.getElementById("readyStepCopy");if(rsc)rsc.textContent=serviceMode==="dine"?"Serving to your table":"Collect at the pickup counter";
setView("tracking");runTrackingSimulation();
paymentConfirming=false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"Confirming…" label is invisible.

When the slide completes, succeed()setX(maxX) drives the label opacity to 0 (since 1-(maxX/(maxX*0.6)) is negative and clamped). confirmPaid then sets the text to "Confirming…" but never restores opacity, so the feedback text never shows during the 700ms confirm delay.

🐛 Proposed fix
-  var lbl=document.getElementById("paySliderLabel");if(lbl)lbl.textContent="Confirming…";
+  var lbl=document.getElementById("paySliderLabel");if(lbl){lbl.textContent="Confirming…";lbl.style.opacity="1";}
📝 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
function confirmPaid(){
var btn=document.getElementById("btnPaid");if(!btn||btn.classList.contains("is-confirming"))return;
btn.classList.add("is-confirming");btn.textContent="Confirming…";
if(paymentConfirming)return;paymentConfirming=true;
var lbl=document.getElementById("paySliderLabel");if(lbl)lbl.textContent="Confirming…";
setTimeout(function(){
btn.classList.remove("is-confirming");btn.textContent="I've paid — confirm my order";
if(payInterval){clearInterval(payInterval);payInterval=null;}
pickupOtp=genOtp();pushOrderToKitchen();
var now=new Date();var el=document.getElementById("placedTime");if(el)el.textContent=now.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});
var rsc=document.getElementById("readyStepCopy");if(rsc)rsc.textContent=serviceMode==="dine"?"Serving to your table":"Collect at the pickup counter";
setView("tracking");runTrackingSimulation();
paymentConfirming=false;
function confirmPaid(){
if(paymentConfirming)return;paymentConfirming=true;
var lbl=document.getElementById("paySliderLabel");if(lbl){lbl.textContent="Confirming…";lbl.style.opacity="1";}
setTimeout(function(){
if(payInterval){clearInterval(payInterval);payInterval=null;}
pickupOtp=genOtp();pushOrderToKitchen();
var now=new Date();var el=document.getElementById("placedTime");if(el)el.textContent=now.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});
var rsc=document.getElementById("readyStepCopy");if(rsc)rsc.textContent=serviceMode==="dine"?"Serving to your table":"Collect at the pickup counter";
setView("tracking");runTrackingSimulation();
paymentConfirming=false;
🧰 Tools
🪛 ast-grep (0.44.0)

[error] 642-649: React's useState should not be directly called
Context: setTimeout(function(){
if(payInterval){clearInterval(payInterval);payInterval=null;}
pickupOtp=genOtp();pushOrderToKitchen();
var now=new Date();var el=document.getElementById("placedTime");if(el)el.textContent=now.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});
var rsc=document.getElementById("readyStepCopy");if(rsc)rsc.textContent=serviceMode==="dine"?"Serving to your table":"Collect at the pickup counter";
setView("tracking");runTrackingSimulation();
paymentConfirming=false;
},700)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)


[error] 647-647: React's useState should not be directly called
Context: setView("tracking")
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🤖 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 `@public/demo/student.html` around lines 640 - 649, The confirmPaid() function
sets the text content of the paySliderLabel element to "Confirming…" but the
element's opacity remains at 0 from the previous slider animation, making the
text invisible. Restore the opacity of the label element (retrieved by
getElementById("paySliderLabel")) to a visible value (such as 1) before or
immediately after setting its textContent to ensure the confirmation feedback is
visible during the confirm delay.

Comment on lines 57 to 61
<button
type="button"
onClick={() => setOpen(!open)}
className="flex items-center gap-1 cursor-pointer hover:opacity-85 text-left pr-2.5 outline-none select-none h-full shrink-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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore visible keyboard focus on dropdown controls.

outline-none is applied on trigger/options without a focus-visible replacement, so keyboard focus is hard to track in this core control.

Suggested fix
- className="flex items-center gap-1 cursor-pointer hover:opacity-85 text-left pr-2.5 outline-none select-none h-full shrink-0"
+ className="flex items-center gap-1 cursor-pointer hover:opacity-85 text-left pr-2.5 outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--color-ocean-500)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--color-paper)] select-none h-full shrink-0"
- className={`w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors cursor-pointer outline-none ${
+ className={`w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--color-ocean-500)] focus-visible:ring-inset ${

Also applies to: 101-101, 112-116

🤖 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/portal-student/canteen-switcher.tsx` around lines 57 - 61, The
canteen-switcher dropdown controls currently have outline-none applied which
removes keyboard focus indicators without providing a visible focus-visible
alternative, making keyboard navigation difficult. Remove the outline-none class
from the button element with onClick={() => setOpen(!open)} at line 57-61, and
replace it with focus-visible:outline or a similar focus-visible utility class
to restore visible keyboard focus. Apply the same fix to the other dropdown
control elements mentioned at lines 101-101 and 112-116 to ensure consistent
keyboard accessibility across all interactive dropdown components.

Comment thread ui_spec.md
Comment on lines +111 to +124
```
+-------------------------------------------------------+
| Card 1 (2x2) | Card 2 (2x1) |
| [col-span-2] | [col-span-2] |
| [row-span-2] | [row-span-1] |
| +-------------------------+
| | Card 3 (2x1) |
| | [col-span-2] |
| | [row-span-1] |
+-----------------------------+-------------------------+
| Card 4 (1x1) | Card 5 (2x1) |Card 6 (1x1) |
| [col-span-1] | [col-span-2] |[col-span-1] |
+-------------------------------------------------------+
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifier to fenced code block.

The CSS Grid schema visualizer is missing a language identifier. While this is ASCII art, specifying text improves accessibility and syntax highlighting:

📝 Proposed fix
 #### CSS Grid Schema Visualizer
-```
+```text
 +-------------------------------------------------------+
 |        Card 1 (2x2)         |      Card 2 (2x1)       |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 111-111: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@ui_spec.md` around lines 111 - 124, The CSS Grid schema visualizer code block
is missing a language identifier for the fenced code block. Add the `text`
language identifier to the opening fence of the ASCII art grid diagram by
changing the opening ``` to ```text. This improves accessibility and enables
proper syntax highlighting for the markdown document.

Source: Linters/SAST tools

Comment thread ui_spec.md
Comment on lines +224 to +330
"use client";

import React, { useRef, useState } from "react";
import { motion, useMotionValue, useTransform } from "framer-motion";
import { ArrowRight, Check } from "lucide-react";

interface DragConfirmSliderProps {
onConfirm: () => void;
text?: string;
successText?: string;
width?: number; // Total width of the slider container in pixels
}

export function DragConfirmSlider({
onConfirm,
text = "Slide to confirm payment",
successText = "Payment confirmed!",
width = 320,
}: DragConfirmSliderProps) {
const [isConfirmed, setIsConfirmed] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const constraintsRef = useRef<HTMLDivElement>(null);

// Track horizontal position of thumb
const x = useMotionValue(0);

// The thumb is 52px wide (w-13). Total drag distance: width - thumbWidth - padding.
const thumbWidth = 52;
const padding = 12; // p-1.5 = 6px on each side (total 12px)
const dragRange = width - thumbWidth - padding;

// Transform opacity of the background instruction label as we slide
const labelOpacity = useTransform(x, [0, dragRange / 1.5], [1, 0]);

// Transform background color saturation / fill percentage
const fillWidth = useTransform(x, (latest) => `${latest + thumbWidth / 2}px`);

const handleDragEnd = () => {
setIsDragging(false);

// Check if user reached the threshold (95% of drag range)
if (x.get() >= dragRange * 0.95) {
x.set(dragRange);
setIsConfirmed(true);
if (onConfirm) onConfirm();
} else {
// Spring back to starting position if released early
x.set(0);
}
};

return (
<div
ref={constraintsRef}
className="relative h-[68px] border-[3px] border-black bg-white rounded-2xl p-1.5 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] overflow-hidden select-none"
style={{ width: `${width}px` }}
>
{/* Slide Progress Fill (Dynamic Green Background) */}
<motion.div
className="absolute top-0 left-0 h-full bg-green-400 border-r-2 border-black"
style={{ width: fillWidth }}
/>

{/* Slide Instructions Label */}
<motion.div
className="absolute inset-0 flex items-center justify-center text-sm font-extrabold text-black pointer-events-none z-10"
style={{ opacity: isConfirmed ? 0 : labelOpacity }}
>
{text}
<ArrowRight className="w-4 h-4 ml-2 animate-pulse" />
</motion.div>

{/* Success State Indicator */}
{isConfirmed && (
<div className="absolute inset-0 flex items-center justify-center text-sm font-extrabold text-black z-10 bg-green-400">
{successText}
<Check className="w-5 h-5 ml-2 border-2 border-black rounded-full p-0.5 bg-white" />
</div>
)}

{/* Draggable Thumb (Slider Handle) */}
{!isConfirmed && (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: dragRange }}
dragElastic={0.05}
dragMomentum={false}
style={{ x }}
onDragStart={() => setIsDragging(true)}
onDragEnd={handleDragEnd}
className={`
relative z-20 w-[52px] h-[50px]
border-[3px] border-black bg-yellow-300 rounded-xl
flex items-center justify-center cursor-grab
shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]
transition-colors duration-150
active:cursor-grabbing active:bg-yellow-400
\${isDragging ? 'shadow-none' : ''}
`}
>
<ArrowRight className="w-5 h-5 text-black" />
</motion.div>
)}
</div>
);
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile drag threshold with actual implementation.

The spec example uses a 95% threshold (line 265: x.get() >= dragRange * 0.95), but the actual DragToConfirm component in src/components/portal-student/drag-to-confirm.tsx uses 85% (THRESHOLD = 0.85). Additionally, the spec uses thumbWidth = 52 and padding = 12, while the implementation uses THUMB = 56 and PAD = 4.

Consider updating this example to match the implementation:

- const thumbWidth = 52;
- const padding = 12; // p-1.5 = 6px on each side (total 12px)
+ const thumbWidth = 56;
+ const padding = 4; // px — track inner padding
  const dragRange = width - thumbWidth - padding;
  
  const handleDragEnd = () => {
    setIsDragging(false);
    
-   // Check if user reached the threshold (95% of drag range)
-   if (x.get() >= dragRange * 0.95) {
+   // Check if user reached the threshold (85% of drag range)
+   if (x.get() >= dragRange * 0.85) {
📝 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
"use client";
import React, { useRef, useState } from "react";
import { motion, useMotionValue, useTransform } from "framer-motion";
import { ArrowRight, Check } from "lucide-react";
interface DragConfirmSliderProps {
onConfirm: () => void;
text?: string;
successText?: string;
width?: number; // Total width of the slider container in pixels
}
export function DragConfirmSlider({
onConfirm,
text = "Slide to confirm payment",
successText = "Payment confirmed!",
width = 320,
}: DragConfirmSliderProps) {
const [isConfirmed, setIsConfirmed] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const constraintsRef = useRef<HTMLDivElement>(null);
// Track horizontal position of thumb
const x = useMotionValue(0);
// The thumb is 52px wide (w-13). Total drag distance: width - thumbWidth - padding.
const thumbWidth = 52;
const padding = 12; // p-1.5 = 6px on each side (total 12px)
const dragRange = width - thumbWidth - padding;
// Transform opacity of the background instruction label as we slide
const labelOpacity = useTransform(x, [0, dragRange / 1.5], [1, 0]);
// Transform background color saturation / fill percentage
const fillWidth = useTransform(x, (latest) => `${latest + thumbWidth / 2}px`);
const handleDragEnd = () => {
setIsDragging(false);
// Check if user reached the threshold (95% of drag range)
if (x.get() >= dragRange * 0.95) {
x.set(dragRange);
setIsConfirmed(true);
if (onConfirm) onConfirm();
} else {
// Spring back to starting position if released early
x.set(0);
}
};
return (
<div
ref={constraintsRef}
className="relative h-[68px] border-[3px] border-black bg-white rounded-2xl p-1.5 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] overflow-hidden select-none"
style={{ width: `${width}px` }}
>
{/* Slide Progress Fill (Dynamic Green Background) */}
<motion.div
className="absolute top-0 left-0 h-full bg-green-400 border-r-2 border-black"
style={{ width: fillWidth }}
/>
{/* Slide Instructions Label */}
<motion.div
className="absolute inset-0 flex items-center justify-center text-sm font-extrabold text-black pointer-events-none z-10"
style={{ opacity: isConfirmed ? 0 : labelOpacity }}
>
{text}
<ArrowRight className="w-4 h-4 ml-2 animate-pulse" />
</motion.div>
{/* Success State Indicator */}
{isConfirmed && (
<div className="absolute inset-0 flex items-center justify-center text-sm font-extrabold text-black z-10 bg-green-400">
{successText}
<Check className="w-5 h-5 ml-2 border-2 border-black rounded-full p-0.5 bg-white" />
</div>
)}
{/* Draggable Thumb (Slider Handle) */}
{!isConfirmed && (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: dragRange }}
dragElastic={0.05}
dragMomentum={false}
style={{ x }}
onDragStart={() => setIsDragging(true)}
onDragEnd={handleDragEnd}
className={`
relative z-20 w-[52px] h-[50px]
border-[3px] border-black bg-yellow-300 rounded-xl
flex items-center justify-center cursor-grab
shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]
transition-colors duration-150
active:cursor-grabbing active:bg-yellow-400
\${isDragging ? 'shadow-none' : ''}
`}
>
<ArrowRight className="w-5 h-5 text-black" />
</motion.div>
)}
</div>
);
}
```
"use client";
import React, { useRef, useState } from "react";
import { motion, useMotionValue, useTransform } from "framer-motion";
import { ArrowRight, Check } from "lucide-react";
interface DragConfirmSliderProps {
onConfirm: () => void;
text?: string;
successText?: string;
width?: number; // Total width of the slider container in pixels
}
export function DragConfirmSlider({
onConfirm,
text = "Slide to confirm payment",
successText = "Payment confirmed!",
width = 320,
}: DragConfirmSliderProps) {
const [isConfirmed, setIsConfirmed] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const constraintsRef = useRef<HTMLDivElement>(null);
// Track horizontal position of thumb
const x = useMotionValue(0);
// The thumb is 56px wide (w-14). Total drag distance: width - thumbWidth - padding.
const thumbWidth = 56;
const padding = 4; // px — track inner padding
const dragRange = width - thumbWidth - padding;
// Transform opacity of the background instruction label as we slide
const labelOpacity = useTransform(x, [0, dragRange / 1.5], [1, 0]);
// Transform background color saturation / fill percentage
const fillWidth = useTransform(x, (latest) => `${latest + thumbWidth / 2}px`);
const handleDragEnd = () => {
setIsDragging(false);
// Check if user reached the threshold (85% of drag range)
if (x.get() >= dragRange * 0.85) {
x.set(dragRange);
setIsConfirmed(true);
if (onConfirm) onConfirm();
} else {
// Spring back to starting position if released early
x.set(0);
}
};
return (
<div
ref={constraintsRef}
className="relative h-[68px] border-[3px] border-black bg-white rounded-2xl p-1.5 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] overflow-hidden select-none"
style={{ width: `${width}px` }}
>
{/* Slide Progress Fill (Dynamic Green Background) */}
<motion.div
className="absolute top-0 left-0 h-full bg-green-400 border-r-2 border-black"
style={{ width: fillWidth }}
/>
{/* Slide Instructions Label */}
<motion.div
className="absolute inset-0 flex items-center justify-center text-sm font-extrabold text-black pointer-events-none z-10"
style={{ opacity: isConfirmed ? 0 : labelOpacity }}
>
{text}
<ArrowRight className="w-4 h-4 ml-2 animate-pulse" />
</motion.div>
{/* Success State Indicator */}
{isConfirmed && (
<div className="absolute inset-0 flex items-center justify-center text-sm font-extrabold text-black z-10 bg-green-400">
{successText}
<Check className="w-5 h-5 ml-2 border-2 border-black rounded-full p-0.5 bg-white" />
</div>
)}
{/* Draggable Thumb (Slider Handle) */}
{!isConfirmed && (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: dragRange }}
dragElastic={0.05}
dragMomentum={false}
style={{ x }}
onDragStart={() => setIsDragging(true)}
onDragEnd={handleDragEnd}
className={`
relative z-20 w-[52px] h-[50px]
border-[3px] border-black bg-yellow-300 rounded-xl
flex items-center justify-center cursor-grab
shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]
transition-colors duration-150
active:cursor-grabbing active:bg-yellow-400
\${isDragging ? 'shadow-none' : ''}
`}
>
<ArrowRight className="w-5 h-5 text-black" />
</motion.div>
)}
</div>
);
}
🤖 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 `@ui_spec.md` around lines 224 - 330, Update the DragConfirmSlider component in
the spec to match the actual implementation constants. In the handleDragEnd
function, change the drag threshold check from 0.95 to 0.85 to match the
THRESHOLD constant in the actual implementation. Additionally, update the
thumbWidth constant from 52 to 56 to match THUMB, and update the padding
constant from 12 to 4 to match PAD in the actual implementation. These changes
ensure the spec documentation accurately reflects how the component actually
behaves.

Replace generic AI landing patterns with campus-gazette typography, anti-template motion, and section polish. Adds Sync pipeline scroll scrub, ClosingSection, nav anchor fixes, and Playwright landing audit script.
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

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