chore: Tailwind CSS v3 → v4 migration (#99)
* chore: run Tailwind v4 upgrade tool (Phase 1) - Upgraded tailwindcss v3 → v4.2.1, postcss plugin to @tailwindcss/postcss - Deleted tailwind.config.js, migrated theme to CSS @theme block in index.css - Replaced @tailwind directives with @import 'tailwindcss' - Added @custom-variant dark, @utility blocks for custom utilities - Updated class names across 128 files (shadow-sm → shadow-xs, etc.) - Removed autoprefixer (built into v4) - Added migration plan doc Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: switch from @tailwindcss/postcss to @tailwindcss/vite (Phase 2) - Replaced @tailwindcss/postcss with @tailwindcss/vite plugin - Deleted postcss.config.js (no longer needed) - Tailwind now runs as a native Vite plugin for faster HMR Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: convert to OKLCH colors, move keyframes into @theme (Phase 3-4) - Replaced all HSL color indirection with direct OKLCH values in @theme - Moved all keyframes inside @theme block (v4 pattern) - Eliminated hsl(var(--x)) double-indirection across 17 component files - Replaced hsl() inline styles with var(--color-*) theme references - Cleaned up redundant rdp-* utility blocks - Fixed @custom-variant dark syntax to use :where() - Added sidebar/glass/shadow vars as OKLCH in :root Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #99.
This commit is contained in:
@@ -329,7 +329,7 @@ export default function SurveyPage() {
|
||||
<span className="hidden sm:inline">Resolution<span className="text-gradient-brand">Flow</span></span>
|
||||
</a>
|
||||
<div className="flex flex-1 items-center gap-2 sm:gap-2.5" style={{ maxWidth: '280px' }}>
|
||||
<div className="flex-1 h-[3px] rounded-full overflow-hidden" style={{ background: 'hsl(var(--border))' }}>
|
||||
<div className="flex-1 h-[3px] rounded-full overflow-hidden" style={{ background: 'var(--color-border)' }}>
|
||||
<div className="h-full rounded-full bg-gradient-brand transition-[width] duration-500" style={{ width: `${progressPct}%` }} />
|
||||
</div>
|
||||
<span className="text-[11px] font-label text-muted-foreground whitespace-nowrap tabular-nums">{answeredCount}/{TOTAL_QUESTIONS}</span>
|
||||
@@ -376,7 +376,7 @@ export default function SurveyPage() {
|
||||
key={i}
|
||||
className="flex-1 h-1 sm:h-[3px] rounded-full transition-colors duration-300"
|
||||
style={{
|
||||
background: i < currentSlide ? '#34d399' : i === currentSlide ? 'linear-gradient(90deg, #06b6d4, #22d3ee)' : 'hsl(var(--border))',
|
||||
background: i < currentSlide ? '#34d399' : i === currentSlide ? 'linear-gradient(90deg, #06b6d4, #22d3ee)' : 'var(--color-border)',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -399,12 +399,12 @@ export default function SurveyPage() {
|
||||
</button>
|
||||
) : <div />}
|
||||
{si < SLIDES.length - 1 ? (
|
||||
<button onClick={() => goSlide(si + 1)} className="inline-flex items-center gap-2 px-5 py-2.5 sm:px-6 sm:py-3 rounded-[10px] text-sm font-semibold bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20 transition-all duration-150 hover:opacity-90 active:scale-[0.97]">
|
||||
<button onClick={() => goSlide(si + 1)} className="inline-flex items-center gap-2 px-5 py-2.5 sm:px-6 sm:py-3 rounded-[10px] text-sm font-semibold bg-gradient-brand text-brand-dark shadow-lg shadow-primary/20 transition-all duration-150 hover:opacity-90 active:scale-[0.97]">
|
||||
Next
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={handleSubmit} disabled={isSubmitting} className="inline-flex items-center gap-2 px-5 py-2.5 sm:px-6 sm:py-3 rounded-[10px] text-sm font-semibold bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20 transition-all duration-150 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 disabled:cursor-not-allowed">
|
||||
<button onClick={handleSubmit} disabled={isSubmitting} className="inline-flex items-center gap-2 px-5 py-2.5 sm:px-6 sm:py-3 rounded-[10px] text-sm font-semibold bg-gradient-brand text-brand-dark shadow-lg shadow-primary/20 transition-all duration-150 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 disabled:cursor-not-allowed">
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
{!isSubmitting && <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M20 6L9 17l-5-5"/></svg>}
|
||||
</button>
|
||||
@@ -433,7 +433,7 @@ export default function SurveyPage() {
|
||||
|
||||
{/* Email a copy */}
|
||||
<div className="glass-card-static p-4 sm:p-6 max-w-[420px] mx-auto mb-5">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-3">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-3">
|
||||
Email a copy to yourself
|
||||
</p>
|
||||
{!emailSent ? (
|
||||
@@ -443,7 +443,7 @@ export default function SurveyPage() {
|
||||
value={emailInput}
|
||||
onChange={e => setEmailInput(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
className="flex-1 rounded-[9px] px-3.5 py-2.5 text-sm text-foreground placeholder:text-[#5a6170] focus:outline-none"
|
||||
className="flex-1 rounded-[9px] px-3.5 py-2.5 text-sm text-foreground placeholder:text-brand-text-muted focus:outline-hidden"
|
||||
style={{ background: 'rgba(16, 17, 20, 0.6)', border: '1px solid var(--glass-border)' }}
|
||||
onFocus={e => { e.currentTarget.style.borderColor = 'rgba(6, 182, 212, 0.4)' }}
|
||||
onBlur={e => { e.currentTarget.style.borderColor = 'var(--glass-border)' }}
|
||||
@@ -473,7 +473,7 @@ export default function SurveyPage() {
|
||||
}
|
||||
}}
|
||||
disabled={!emailInput.trim() || emailSending}
|
||||
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-[9px] text-sm font-semibold bg-gradient-brand text-[#101114] transition-all duration-150 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-[9px] text-sm font-semibold bg-gradient-brand text-brand-dark transition-all duration-150 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
>
|
||||
{emailSending ? (
|
||||
<>
|
||||
@@ -502,7 +502,7 @@ export default function SurveyPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/survey/thank-you')}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 sm:px-6 rounded-[10px] text-sm font-semibold bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20 transition-all duration-150 hover:opacity-90 active:scale-[0.97]"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 sm:px-6 rounded-[10px] text-sm font-semibold bg-gradient-brand text-brand-dark shadow-lg shadow-primary/20 transition-all duration-150 hover:opacity-90 active:scale-[0.97]"
|
||||
>
|
||||
Finish
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
|
||||
@@ -535,7 +535,7 @@ function ScenarioBox({ scenario }: { scenario: { title: string; symptom: string;
|
||||
|
||||
function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQuestion; answer?: string | string[]; setAnswer: (id: string, val: string | string[]) => void }) {
|
||||
return (
|
||||
<div className="glass-card-static p-4 sm:p-7 mb-3 sm:mb-4 transition-[border-color] duration-200 focus-within:!border-[rgba(6,182,212,0.25)]">
|
||||
<div className="glass-card-static p-4 sm:p-7 mb-3 sm:mb-4 transition-[border-color] duration-200 focus-within:border-[rgba(6,182,212,0.25)]!">
|
||||
<div className="font-label text-[11px] mb-1.5 font-medium" style={{ color: '#06b6d4' }}>Q{q.num}</div>
|
||||
<div className="font-heading text-[14px] sm:text-[15px] font-semibold text-foreground leading-snug mb-1">{q.text}</div>
|
||||
{q.hint && <div className="text-[12px] text-muted-foreground mb-3 sm:mb-4 leading-snug">{q.hint}</div>}
|
||||
@@ -551,10 +551,10 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
|
||||
style={{
|
||||
background: answer === opt ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)',
|
||||
border: `1px solid ${answer === opt ? '#06b6d4' : 'var(--glass-border)'}`,
|
||||
color: answer === opt ? 'hsl(var(--foreground))' : 'hsl(var(--muted-foreground))',
|
||||
color: answer === opt ? 'var(--color-foreground)' : 'var(--color-muted-foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="w-[18px] h-[18px] rounded-full flex-shrink-0 flex items-center justify-center transition-all duration-150 mt-0.5" style={{ border: `2px solid ${answer === opt ? '#06b6d4' : 'var(--glass-border)'}` }}>
|
||||
<div className="w-[18px] h-[18px] rounded-full shrink-0 flex items-center justify-center transition-all duration-150 mt-0.5" style={{ border: `2px solid ${answer === opt ? '#06b6d4' : 'var(--glass-border)'}` }}>
|
||||
{answer === opt && <div className="w-2 h-2 rounded-full" style={{ background: '#06b6d4' }} />}
|
||||
</div>
|
||||
<span className="leading-snug">{opt}</span>
|
||||
@@ -578,10 +578,10 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
|
||||
style={{
|
||||
background: selected ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)',
|
||||
border: `1px solid ${selected ? '#06b6d4' : 'var(--glass-border)'}`,
|
||||
color: selected ? 'hsl(var(--foreground))' : 'hsl(var(--muted-foreground))',
|
||||
color: selected ? 'var(--color-foreground)' : 'var(--color-muted-foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="w-[18px] h-[18px] rounded-[5px] flex-shrink-0 flex items-center justify-center text-[11px] transition-all duration-150 mt-0.5" style={{ border: `2px solid ${selected ? '#06b6d4' : 'var(--glass-border)'}`, background: selected ? '#06b6d4' : 'transparent', color: selected ? 'white' : 'transparent' }}>
|
||||
<div className="w-[18px] h-[18px] rounded-[5px] shrink-0 flex items-center justify-center text-[11px] transition-all duration-150 mt-0.5" style={{ border: `2px solid ${selected ? '#06b6d4' : 'var(--glass-border)'}`, background: selected ? '#06b6d4' : 'transparent', color: selected ? 'white' : 'transparent' }}>
|
||||
{'\u2713'}
|
||||
</div>
|
||||
<span className="leading-snug">{opt}</span>
|
||||
@@ -600,7 +600,7 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
|
||||
value={(answer as string) || ''}
|
||||
onChange={e => setAnswer(q.id, e.target.value)}
|
||||
placeholder="Type your answer here..."
|
||||
className="w-full min-h-[100px] rounded-[9px] p-3 sm:p-3.5 text-[13px] sm:text-sm text-foreground leading-relaxed resize-y transition-all duration-200 placeholder:text-[#5a6170] focus:outline-none"
|
||||
className="w-full min-h-[100px] rounded-[9px] p-3 sm:p-3.5 text-[13px] sm:text-sm text-foreground leading-relaxed resize-y transition-all duration-200 placeholder:text-brand-text-muted focus:outline-hidden"
|
||||
style={{
|
||||
background: 'rgba(16, 17, 20, 0.6)',
|
||||
border: '1px solid var(--glass-border)',
|
||||
@@ -633,7 +633,7 @@ function RangeInput({ question: q, value, onChange }: { question: SurveyQuestion
|
||||
onChange={e => onChange(e.target.value + (q.suffix || ''))}
|
||||
className="w-full h-2 sm:h-1 rounded-full appearance-none cursor-pointer touch-none"
|
||||
style={{
|
||||
background: `linear-gradient(to right, #06b6d4 0%, #06b6d4 ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, hsl(var(--border)) ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, hsl(var(--border)) 100%)`,
|
||||
background: `linear-gradient(to right, #06b6d4 0%, #06b6d4 ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) 100%)`,
|
||||
}}
|
||||
/>
|
||||
<div className="flex justify-between text-[11px] text-muted-foreground mt-2.5">
|
||||
@@ -733,13 +733,13 @@ function DragRank({ items, onChange }: { items: string[]; onChange: (items: stri
|
||||
border: `1px solid ${overIdx === idx || draggingIdx === idx ? '#06b6d4' : 'var(--glass-border)'}`,
|
||||
opacity: draggingIdx === idx ? 0.5 : 1,
|
||||
cursor: 'grab',
|
||||
color: 'hsl(var(--muted-foreground))',
|
||||
color: 'var(--color-muted-foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0 text-[#5a6170]">
|
||||
<div className="shrink-0 text-brand-text-muted">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>
|
||||
</div>
|
||||
<div className="font-label text-[11px] font-semibold w-5 text-center flex-shrink-0" style={{ color: '#06b6d4' }}>{idx + 1}</div>
|
||||
<div className="font-label text-[11px] font-semibold w-5 text-center shrink-0" style={{ color: '#06b6d4' }}>{idx + 1}</div>
|
||||
<div className="flex-1 leading-snug">{item}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user