Neobrutalist-Bento student portal + drag-to-confirm payment slider - #9
Neobrutalist-Bento student portal + drag-to-confirm payment slider#9thribhuvan003 wants to merge 2 commits into
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughThe 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 ChangesNeobrutalist-Bento Winter 2026 Student Portal Overhaul
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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: 5
🧹 Nitpick comments (1)
src/app/layout.tsx (1)
209-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider using array join for readability.
The
classNamestring 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
📒 Files selected for processing (12)
public/demo/student.htmlsrc/app/globals.csssrc/app/layout.tsxsrc/components/portal-student/canteen-switcher.tsxsrc/components/portal-student/cart-drawer.tsxsrc/components/portal-student/drag-to-confirm.tsxsrc/components/portal-student/menu-board.tsxsrc/components/portal-student/menu-item-card.tsxsrc/components/portal-student/pay-panel.tsxsrc/components/portal-student/top-bar.tsxsrc/components/portal-student/track-panel.tsxui_spec.md
| 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);}); | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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; |
There was a problem hiding this comment.
🎯 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.
| 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.
| <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" | ||
| > |
There was a problem hiding this comment.
🎯 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.
| ``` | ||
| +-------------------------------------------------------+ | ||
| | 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] | | ||
| +-------------------------------------------------------+ | ||
| ``` |
There was a problem hiding this comment.
📐 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
| "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> | ||
| ); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
📐 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.
| "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.
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
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.Design system
#4F6E8Caccent, bento cards[data-portal="student"]inglobals.css; kitchen/admin portals untouchedInteractions
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.<select>Data rule
Verification
tsc --noEmitclean ·next lintclean (only a pre-existing admin warning)demo:verifypasses for student.html🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Style
Documentation