Fix organization setup flow: redirect to onboarding for incomplete setup

This commit is contained in:
Ra
2025-08-18 10:33:45 -07:00
commit 557b113196
60 changed files with 16246 additions and 0 deletions

361
components/UiKit.tsx Normal file
View File

@@ -0,0 +1,361 @@
import React, { useState, ReactNode } from 'react';
import { NavLink, Outlet } from 'react-router-dom';
import { useTheme } from '../contexts/ThemeContext';
import { Theme, NavItem } from '../types';
import { useOrg } from '../contexts/OrgContext';
import { useAuth } from '../contexts/AuthContext';
// ========== ICONS ==========
export const CompanyWikiIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 22h16a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1zM8 7h8v2H8V7zm0 4h8v2H8v-2zm0 4h5v2H8v-2z" fill="currentColor" /></svg>
);
export const SubmissionsIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM9 13v-2h6v2H9zm6 4H9v-2h6v2zm-6-8V4h1l5 5h-1V9H9z" fill="currentColor" /></svg>
);
export const ReportsIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16 6h3a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h3V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2zm-4-2v2h-2V4h2zm-2 9v-2h-2v2h2zm4 0v-2h-2v2h2zm-4 4v-2h-2v2h2zm4 0v-2h-2v2h2zm-6-4H8V9h2v2z" fill="currentColor" /></svg>
);
export const ChatIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M21 11.5a8.5 8.5 0 0 1-17 0 8.5 8.5 0 0 1 17 0z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /><path d="M21 11.5a8.5 8.5 0 0 0-8.5-8.5V3m0 17v-1.5a8.5 8.5 0 0 0 8.5-8.5H11" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg>
);
export const HelpIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-13a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0v-4a1 1 0 0 0-1-1zm0 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" fill="currentColor" /></svg>
);
export const SettingsIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19.14 12.94a2.001 2.001 0 0 0-2.28 0l-.71.71a2 2 0 0 0 0 2.83l.71.71a2 2 0 0 0 2.83 0l.71-.71a2 2 0 0 0 0-2.83l-.71-.71zM4.86 12.94a2 2 0 0 0 0 2.83l.71.71a2 2 0 0 0 2.83 0l.71-.71a2 2 0 0 0 0-2.83l-.71-.71a2 2 0 0 0-2.83 0zM12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7.78-1.94a2 2 0 0 0-2.83 0l-.71.71a2 2 0 0 0 0 2.83l.71.71a2 2 0 0 0 2.83 0l.71-.71a2 2 0 0 0 0-2.83l-.71-.71zM4.86 5.22a2 2 0 0 0 0 2.83l.71.71a2 2 0 0 0 2.83 0l.71-.71a2 2 0 0 0 0-2.83l-.71-.71a2 2 0 0 0-2.83 0z" fill="currentColor" /></svg>
);
export const CopyIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16 2H8a2 2 0 0 0-2 2v12h2V4h8v4h4v8h2V8l-4-4h-2z" fill="currentColor" /><path d="M4 8h12v12H4z" fill="currentColor" opacity="0.5" /></svg>
);
export const PlusIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor" /></svg>
);
export const ChevronDownIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 15.5l-6-6 1.4-1.4 4.6 4.6 4.6-4.6L18 9.5l-6 6z" fill="currentColor" /></svg>
);
export const UploadIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 16h6v-6h4l-8-8-8 8h4v6zm-4 2h14v2H5v-2z" fill="currentColor" /></svg>
);
export const CheckIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor" /></svg>
);
export const WarningIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2V7h2v7z" fill="currentColor" /></svg>
);
export const DownloadIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" fill="currentColor" /></svg>
);
export const MinusIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 13H5v-2h14v2z" fill="currentColor" /></svg>
);
export const SunIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
);
export const MoonIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
);
export const SystemIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
);
export const SendIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /></svg>
);
const NAV_ITEMS: NavItem[] = [
{ href: '/company-wiki', label: 'Company Wiki', icon: CompanyWikiIcon },
{ href: '/submissions', label: 'Submissions', icon: SubmissionsIcon },
{ href: '/reports', label: 'Reports', icon: ReportsIcon },
{ href: '/chat', label: 'Chat', icon: ChatIcon },
{ href: '/help', label: 'Help', icon: HelpIcon },
];
const BOTTOM_NAV_ITEMS: NavItem[] = [
{ href: '/settings', label: 'Settings', icon: SettingsIcon }
];
// ========== LAYOUT COMPONENTS ==========
const Sidebar = () => {
const { org, issueInviteViaApi } = useOrg();
const { signOutUser } = useAuth();
const [showInviteModal, setShowInviteModal] = useState(false);
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
const [inviteLink, setInviteLink] = useState('');
const [emailLink, setEmailLink] = useState('');
const commonLinkClasses = "flex items-center w-full px-3 py-2.5 rounded-lg text-sm font-medium transition-colors duration-200";
const activeClass = "bg-[--sidebar-active-bg] text-[--sidebar-active-text]";
const inactiveClass = "text-[--sidebar-text] hover:bg-[--sidebar-active-bg] hover:text-[--sidebar-active-text]";
const handleInvite = async () => {
try {
const result = await issueInviteViaApi({
name: inviteForm.name,
email: inviteForm.email,
role: inviteForm.role,
department: inviteForm.department
});
setInviteLink(result.inviteLink);
setEmailLink(result.emailLink);
setInviteForm({ name: '', email: '', role: '', department: '' });
} catch (error) {
console.error('Failed to invite employee:', error);
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const renderNavItems = (items: NavItem[]) => items.map(item => (
<li key={item.href}>
<NavLink
to={item.href}
className={({ isActive }) => `${commonLinkClasses} ${isActive ? activeClass : inactiveClass}`}
>
<item.icon className="w-5 h-5 mr-3" />
<span>{item.label}</span>
</NavLink>
</li>
));
return (
<aside className="w-64 flex-shrink-0 bg-[--sidebar-bg] border-r border-[--border-color] flex flex-col p-4">
<div className="flex items-center mb-8">
<div className="w-8 h-8 bg-[--accent] rounded-full flex items-center justify-center font-bold text-white text-lg mr-2">A</div>
<h1 className="text-xl font-bold text-[--text-primary]">{org?.name || 'Auditly'}</h1>
</div>
<nav className="flex-1">
<ul className="space-y-2">
{renderNavItems(NAV_ITEMS)}
</ul>
</nav>
<div className="mt-auto space-y-4">
<ul className="space-y-2 border-t border-[--border-color] pt-4">
{renderNavItems(BOTTOM_NAV_ITEMS)}
</ul>
<div className="p-4 rounded-lg bg-[--background-tertiary] text-center">
<h3 className="font-bold text-sm text-[--text-primary]">Build [Company]'s Report</h3>
<p className="text-xs text-[--text-secondary] mt-1 mb-3">Share this form with your team members to capture valuable info about your company to train Auditly.</p>
<div className="flex space-x-2">
<Button size="sm" className="w-full" onClick={() => setShowInviteModal(true)}>
<PlusIcon className="w-4 h-4 mr-1" /> Invite
</Button>
<Button size="sm" variant="secondary" className="w-full" onClick={() => (emailLink || inviteLink) && copyToClipboard(emailLink || inviteLink)}>
<CopyIcon className="w-4 h-4 mr-1" /> Copy
</Button>
</div>
<Button size="sm" variant="ghost" className="w-full mt-2" onClick={signOutUser}>Sign out</Button>
</div>
{/* Invite Modal */}
{showInviteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-[--background-secondary] p-6 rounded-lg max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Name</label>
<input
type="text"
value={inviteForm.name}
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Employee name"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Email</label>
<input
type="email"
value={inviteForm.email}
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="employee@company.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Role</label>
<input
type="text"
value={inviteForm.role}
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g. Software Engineer"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Department</label>
<input
type="text"
value={inviteForm.department}
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g. Engineering"
/>
</div>
{(inviteLink || emailLink) && (
<div className="space-y-3">
{emailLink && (
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">
Email Link (for GET requests)
</label>
<div className="flex space-x-2">
<input
type="text"
value={emailLink}
readOnly
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none text-sm"
/>
<Button size="sm" onClick={() => copyToClipboard(emailLink)}>
<CopyIcon className="w-4 h-4" />
</Button>
</div>
</div>
)}
{inviteLink && (
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">
App Link (direct)
</label>
<div className="flex space-x-2">
<input
type="text"
value={inviteLink}
readOnly
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none text-sm"
/>
<Button size="sm" onClick={() => copyToClipboard(inviteLink)}>
<CopyIcon className="w-4 h-4" />
</Button>
</div>
</div>
)}
</div>
)}
</div>
<div className="flex space-x-2 mt-6">
<Button
variant="secondary"
className="flex-1"
onClick={() => {
setShowInviteModal(false);
setInviteLink('');
setInviteForm({ name: '', email: '', role: '', department: '' });
}}
>
Cancel
</Button>
<Button
className="flex-1"
onClick={handleInvite}
disabled={!inviteForm.name || !inviteForm.email}
>
Generate Invite
</Button>
</div>
</div>
</div>
)}
</div>
</aside>
);
};
export const Layout = () => (
<div className="flex h-screen bg-[--background-primary]">
<Sidebar />
<main className="flex-1 overflow-y-auto">
<Outlet />
</main>
</div>
);
// ========== UI PRIMITIVES ==========
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
children: ReactNode;
className?: string;
padding?: 'sm' | 'md' | 'lg';
}
export const Card: React.FC<CardProps> = ({ children, className, padding = 'md', ...props }) => {
const paddingClasses = {
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
};
return (
<div className={`bg-[--background-secondary] border border-[--border-color] rounded-xl shadow-sm ${paddingClasses[padding]} ${className || ''}`} {...props}>
{children}
</div>
);
};
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
export const Button: React.FC<ButtonProps> = ({ children, variant = 'primary', size = 'md', className, ...props }) => {
const baseClasses = 'inline-flex items-center justify-center font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed';
const variantClasses = {
primary: 'bg-[--accent] text-[--accent-text] hover:bg-[--accent-hover] focus:ring-[--accent]',
secondary: 'bg-[--button-secondary-bg] text-[--text-primary] hover:bg-[--button-secondary-hover] focus:ring-[--accent] border border-[--border-color]',
danger: 'bg-[--status-red] text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'bg-transparent text-[--text-primary] hover:bg-[--background-tertiary]'
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
};
return (
<button className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className || ''}`} {...props}>
{children}
</button>
);
};
interface AccordionProps {
items: { question: string; answer: string }[];
}
export const Accordion = ({ items }: AccordionProps) => {
const [openIndex, setOpenIndex] = useState<number | null>(0);
const toggleItem = (index: number) => {
setOpenIndex(openIndex === index ? null : index);
};
return (
<div className="space-y-3">
{items.map((item, index) => (
<div key={index} className="border-b border-[--border-color]">
<button
onClick={() => toggleItem(index)}
className="w-full flex justify-between items-center py-4 text-left text-[--text-primary] font-medium"
>
<span>{item.question}</span>
{openIndex === index ? <MinusIcon className="w-5 h-5" /> : <PlusIcon className="w-5 h-5" />}
</button>
{openIndex === index && (
<div className="pb-4 text-[--text-secondary]">
{item.answer}
</div>
)}
</div>
))}
</div>
);
};

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, Tooltip, Legend } from 'recharts';
export interface RadarMetric {
label: string;
value: number; // 0-100
max?: number;
}
interface Props {
title?: string;
data: RadarMetric[];
height?: number;
color?: string;
}
const RadarPerformanceChart: React.FC<Props> = ({ title, data, height = 320, color = '#3b82f6' }) => {
const chartData = data.map(d => ({ subject: d.label, A: d.value, fullMark: d.max ?? 100 }));
return (
<div className="w-full h-full">
{title && <h4 className="text-sm font-medium text-[--text-secondary] mb-2">{title}</h4>}
<div style={{ width: '100%', height }}>
<ResponsiveContainer>
<RadarChart data={chartData} margin={{ top: 10, right: 30, bottom: 10, left: 10 }}>
<PolarGrid stroke="var(--border-color)" />
<PolarAngleAxis dataKey="subject" tick={{ fill: 'var(--text-secondary)', fontSize: 11 }} />
<PolarRadiusAxis angle={30} domain={[0, 100]} tick={{ fill: 'var(--text-secondary)', fontSize: 10 }} />
<Radar name={title || 'Score'} dataKey="A" stroke={color} fill={color} fillOpacity={0.35} />
<Tooltip wrapperStyle={{ fontSize: 12 }} contentStyle={{ background: 'var(--background-secondary)', border: '1px solid var(--border-color)' }} />
<Legend wrapperStyle={{ fontSize: 12 }} />
</RadarChart>
</ResponsiveContainer>
</div>
</div>
);
};
export default RadarPerformanceChart;

View File

@@ -0,0 +1,30 @@
import React from 'react';
interface ScoreItem { label: string; value: number; max?: number; }
interface Props { title?: string; items: ScoreItem[]; color?: string; }
const ScoreBarList: React.FC<Props> = ({ title, items, color = '#6366f1' }) => {
return (
<div className="space-y-3">
{title && <h4 className="text-sm font-medium text-[--text-secondary]">{title}</h4>}
<ul className="space-y-2">
{items.map(it => {
const pct = Math.min(100, Math.round((it.value / (it.max ?? 100)) * 100));
return (
<li key={it.label} className="space-y-1">
<div className="flex justify-between text-xs text-[--text-secondary]">
<span>{it.label}</span>
<span>{it.value}{it.max ? `/${it.max}` : ''}</span>
</div>
<div className="h-2 bg-[--background-secondary] rounded overflow-hidden">
<div className="h-full transition-all" style={{ width: pct + '%', background: color }} />
</div>
</li>
);
})}
</ul>
</div>
);
};
export default ScoreBarList;

View File

@@ -0,0 +1,108 @@
import React from 'react';
import { EmployeeQuestion } from '../../employeeQuestions';
import { QuestionInput } from '../ui/QuestionInput';
interface EnhancedFigmaQuestionProps {
questionNumber?: string | number;
question: EmployeeQuestion;
answer?: string;
onAnswerChange?: (value: string) => void;
onBack?: () => void;
onNext?: () => void;
showNavigation?: boolean;
nextLabel?: string;
backLabel?: string;
className?: string;
}
export const EnhancedFigmaQuestion: React.FC<EnhancedFigmaQuestionProps> = ({
questionNumber = 'Q',
question,
answer = '',
onAnswerChange,
onBack,
onNext,
showNavigation = true,
nextLabel = 'Next',
backLabel = 'Back',
className = ''
}) => {
return (
<div className={`w-full max-w-[600px] px-5 pt-5 pb-6 bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 inline-flex flex-col justify-end items-end gap-4 ${className}`}>
{/* Question Header */}
<div className="self-stretch inline-flex justify-start items-start gap-3">
<div className="justify-start text-gray-400 text-xl font-medium font-['Inter'] leading-loose">
{questionNumber}
</div>
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
<div className="self-stretch justify-start text-[--text-primary] text-xl font-semibold font-['Inter'] leading-loose">
{question.prompt}
</div>
<div className="self-stretch justify-start text-[--text-secondary] text-sm font-normal font-['Inter'] leading-tight">
{question.required ? 'Required' : 'Optional'} {question.category}
{question.type && `${question.type}`}
</div>
</div>
</div>
{/* Separator */}
<div className="self-stretch h-px bg-gray-200 dark:bg-gray-700"></div>
{/* Answer Section */}
<div className="self-stretch inline-flex justify-start items-start gap-3">
<div className="justify-start text-gray-400 text-xl font-medium font-['Inter'] leading-loose">A</div>
<div className="flex-1">
<QuestionInput
question={question}
value={answer}
onChange={onAnswerChange || (() => { })}
className="border-0 bg-transparent focus:ring-0 p-0"
/>
</div>
</div>
{/* Navigation */}
{showNavigation && (
<div className="inline-flex justify-start items-center gap-3">
{onBack && (
<button
onClick={onBack}
className="px-4 py-3.5 bg-gray-100 dark:bg-gray-700 rounded-full flex justify-center items-center gap-1 overflow-hidden hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
<div className="relative">
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 15L8 10L13 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-[--text-primary] text-sm font-medium font-['Inter'] leading-tight">
{backLabel}
</div>
</div>
</button>
)}
{onNext && (
<button
onClick={onNext}
className="px-4 py-3.5 bg-blue-500 rounded-full border-2 border-blue-400 flex justify-center items-center gap-1 overflow-hidden hover:bg-blue-600 transition-colors"
>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">
{nextLabel}
</div>
</div>
<div className="relative">
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 15L13 10L8 5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</button>
)}
</div>
)}
</div>
);
};
export default EnhancedFigmaQuestion;

View File

@@ -0,0 +1,50 @@
import React from 'react';
// Figma-based Alert component with proper CSS variables and styling
interface FigmaAlertProps {
title: string;
variant?: 'success' | 'error' | 'warning' | 'info';
children?: React.ReactNode;
onClose?: () => void;
className?: string;
}
export const FigmaAlert: React.FC<FigmaAlertProps> = ({
title,
variant = 'info',
children,
onClose,
className = ''
}) => {
const getBorderColor = () => {
switch (variant) {
case 'success': return 'bg-Other-Green';
case 'error': return 'bg-red-500';
case 'warning': return 'bg-yellow-500';
default: return 'bg-blue-500';
}
};
return (
<div className={`p-4 relative bg-Other-White rounded-lg shadow-[0px_2px_2px_-1px_rgba(10,13,18,0.04)] shadow-[0px_4px_6px_-2px_rgba(10,13,18,0.03)] shadow-[0px_12px_16px_-4px_rgba(10,13,18,0.08)] outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex justify-center items-center gap-2.5 overflow-hidden ${className}`}>
<div className="w-96 max-w-96 justify-start text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">
{title}
{children && <div className="mt-1 text-xs text-Neutrals-NeutralSlate600">{children}</div>}
</div>
{onClose && (
<button onClick={onClose} className="flex-shrink-0">
<div data-svg-wrapper className="relative">
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.6663 5.83325L6.33301 14.1666M6.33301 5.83325L14.6663 14.1666" stroke="var(--Neutrals-NeutralSlate600, #535862)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</button>
)}
<div className={`w-2 h-32 left-[-4px] top-[-41px] absolute ${getBorderColor()}`} />
</div>
);
};
export default FigmaAlert;

View File

@@ -0,0 +1,76 @@
import React from 'react';
interface FigmaInputProps {
label?: string;
placeholder?: string;
type?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
icon?: React.ReactNode;
buttonText?: string;
onButtonClick?: () => void;
className?: string;
required?: boolean;
}
export const FigmaInput: React.FC<FigmaInputProps> = ({
label = 'Email',
placeholder = 'Enter your email',
type = 'text',
value,
onChange,
icon,
buttonText,
onButtonClick,
className = '',
required = false
}) => {
return (
<div className={`w-[464px] inline-flex flex-col justify-start items-start gap-2 ${className}`}>
{label && (
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
<div className="justify-start text-Neutrals-NeutralSlate800 text-sm font-medium font-['Inter'] leading-tight">
{label} {required && <span className="text-red-500">*</span>}
</div>
</div>
)}
<div className="self-stretch inline-flex justify-start items-start gap-2">
<div className="flex-1 px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-start items-center gap-2 overflow-hidden">
{icon && (
<div data-svg-wrapper className="relative">
{icon}
</div>
)}
<input
type={type}
value={value}
onChange={onChange}
placeholder={placeholder}
className="flex-1 bg-transparent outline-none text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500"
/>
</div>
{buttonText && (
<button
onClick={onButtonClick}
className="w-32 max-w-32 px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200 transition-colors"
>
<div className="justify-center text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-tight">
{buttonText}
</div>
</button>
)}
</div>
</div>
);
};
// Email icon component for convenience
export const EmailIcon: React.FC = () => (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.66675 5.83325L8.47085 10.5961C9.02182 10.9818 9.29731 11.1746 9.59697 11.2493C9.86166 11.3153 10.1385 11.3153 10.4032 11.2493C10.7029 11.1746 10.9783 10.9818 11.5293 10.5961L18.3334 5.83325M5.66675 16.6666H14.3334C15.7335 16.6666 16.4336 16.6666 16.9684 16.3941C17.4388 16.1544 17.8212 15.772 18.0609 15.3016C18.3334 14.7668 18.3334 14.0667 18.3334 12.6666V7.33325C18.3334 5.93312 18.3334 5.23306 18.0609 4.69828C17.8212 4.22787 17.4388 3.84542 16.9684 3.60574C16.4336 3.33325 15.7335 3.33325 14.3334 3.33325H5.66675C4.26662 3.33325 3.56655 3.33325 3.03177 3.60574C2.56137 3.84542 2.17892 4.22787 1.93923 4.69828C1.66675 5.23306 1.66675 5.93312 1.66675 7.33325V12.6666C1.66675 14.0667 1.66675 14.7668 1.93923 15.3016C2.17892 15.772 2.56137 16.1544 3.03177 16.3941C3.56655 16.6666 4.26662 16.6666 5.66675 16.6666Z" stroke="var(--Neutrals-NeutralSlate500, #717680)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
export default FigmaInput;

View File

@@ -0,0 +1,71 @@
import React from 'react';
interface ProgressStepProps {
number: number;
title: string;
isActive?: boolean;
isCompleted?: boolean;
}
interface FigmaProgressProps {
steps: ProgressStepProps[];
currentStep?: number;
className?: string;
}
const ProgressStep: React.FC<ProgressStepProps> = ({ number, title, isActive = false, isCompleted = false }) => {
const stepClasses = isActive
? "p-2 bg-Main-BG-Gray-50 rounded-[10px] shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)]"
: "p-2 bg-Other-White rounded-[10px]";
const numberClasses = isActive || isCompleted
? "h-5 p-0.5 bg-Brand-Orange rounded-[999px]"
: "h-5 p-0.5 bg-bg-white-0 rounded-[999px] outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200";
const numberTextClasses = isActive || isCompleted
? "w-4 text-center justify-start text-Neutrals-NeutralSlate0 text-xs font-medium font-['Inter'] leading-none"
: "w-4 text-center justify-start text-Neutrals-NeutralSlate600 text-xs font-medium font-['Inter'] leading-none";
const titleClasses = isActive
? "flex-1 justify-start text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight"
: "flex-1 justify-start text-Neutrals-NeutralSlate600 text-sm font-normal font-['Inter'] leading-tight";
return (
<div className={`self-stretch inline-flex justify-start items-center gap-2.5 overflow-hidden ${stepClasses}`}>
<div className={`inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden ${numberClasses}`}>
<div className={numberTextClasses}>{number}</div>
</div>
<div className={titleClasses}>{title}</div>
</div>
);
};
export const FigmaProgress: React.FC<FigmaProgressProps> = ({ steps, currentStep = 1, className = '' }) => {
return (
<div className={`self-stretch inline-flex flex-col justify-start items-start gap-2 ${className}`}>
{steps.map((step, index) => (
<ProgressStep
key={index}
number={step.number}
title={step.title}
isActive={step.number === currentStep}
isCompleted={step.number < currentStep}
/>
))}
</div>
);
};
// Default onboarding steps as shown in Figma
export const defaultOnboardingSteps: ProgressStepProps[] = [
{ number: 1, title: "Company Overview & Vision" },
{ number: 2, title: "Leadership & Organizational Structure" },
{ number: 3, title: "Operations & Execution" },
{ number: 4, title: "Culture & Team Health" },
{ number: 5, title: "Sales, Marketing & Growth" },
{ number: 6, title: "Financial Health & Metrics" },
{ number: 7, title: "Innovation & Product/Service Strategy" },
{ number: 8, title: "Personal Leadership & Risk" }
];
export default FigmaProgress;

View File

@@ -0,0 +1,127 @@
import React from 'react';
interface FigmaQuestionProps {
questionNumber?: string | number;
title: string;
description?: string;
answer?: string;
onAnswerChange?: (value: string) => void;
onBack?: () => void;
onNext?: () => void;
showNavigation?: boolean;
nextLabel?: string;
backLabel?: string;
className?: string;
}
export const FigmaQuestion: React.FC<FigmaQuestionProps> = ({
questionNumber = 'Q',
title,
description,
answer = '',
onAnswerChange,
onBack,
onNext,
showNavigation = true,
nextLabel = 'Next',
backLabel = 'Back',
className = ''
}) => {
return (
<div className={`w-[600px] px-5 pt-5 pb-6 bg-Other-White rounded-2xl outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex flex-col justify-end items-end gap-4 ${className}`}>
{/* Question Header */}
<div className="self-stretch inline-flex justify-start items-start gap-3">
<div className="justify-start text-zinc-300 text-xl font-medium font-['Inter'] leading-loose">
{questionNumber}
</div>
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
<div className="self-stretch justify-start text-Neutrals-NeutralSlate950 text-xl font-semibold font-['Inter'] leading-loose">
{title}
</div>
{description && (
<div className="self-stretch justify-start text-Neutrals-NeutralSlate500 text-sm font-normal font-['Inter'] leading-tight">
{description}
</div>
)}
</div>
</div>
{/* Separator */}
<div data-svg-wrapper>
<svg width="563" height="5" viewBox="0 0 563 5" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_37_3168)">
<path d="M1.5 1L561.5 1" stroke="var(--Neutrals-NeutralSlate200, #E9EAEB)" />
</g>
<defs>
<filter id="filter0_d_37_3168" x="0" y="0.5" width="563" height="4" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset dy="1.5" />
<feGaussianBlur stdDeviation="0.75" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_37_3168" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_37_3168" result="shape" />
</filter>
</defs>
</svg>
</div>
{/* Answer Section */}
<div className="self-stretch inline-flex justify-start items-center gap-3">
<div className="justify-start text-zinc-300 text-xl font-medium font-['Inter'] leading-loose">A</div>
<div className="flex-1">
<textarea
value={answer}
onChange={(e) => onAnswerChange?.(e.target.value)}
placeholder="Type your answer...."
className="w-full bg-transparent outline-none resize-none text-Neutrals-NeutralSlate950 text-base font-normal font-['Inter'] leading-normal placeholder:text-Neutrals-NeutralSlate500 min-h-[100px]"
/>
</div>
</div>
{/* Navigation */}
{showNavigation && (
<div className="inline-flex justify-start items-center gap-3">
{onBack && (
<button
onClick={onBack}
className="px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200 transition-colors"
>
<div data-svg-wrapper className="relative">
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 15L8 10L13 5" stroke="var(--Neutrals-NeutralSlate950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">
{backLabel}
</div>
</div>
</button>
)}
{onNext && (
<button
onClick={onNext}
className="px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
>
<div className="px-1 flex justify-center items-center">
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">
{nextLabel}
</div>
</div>
<div data-svg-wrapper className="relative">
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 15L13 10L8 5" stroke="var(--Neutrals-NeutralSlate0, #FDFDFD)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</button>
)}
</div>
)}
</div>
);
};
export default FigmaQuestion;

35
components/ui/Alert.tsx Normal file
View File

@@ -0,0 +1,35 @@
import React from 'react';
type AlertVariant = 'info' | 'success' | 'warning' | 'error';
const variantStyles: Record<AlertVariant, string> = {
info: 'bg-blue-50 text-blue-800 border-blue-300 dark:bg-blue-900/30 dark:text-blue-200 dark:border-blue-700',
success: 'bg-green-50 text-green-800 border-green-300 dark:bg-green-900/30 dark:text-green-200 dark:border-green-700',
warning: 'bg-amber-50 text-amber-800 border-amber-300 dark:bg-amber-900/30 dark:text-amber-200 dark:border-amber-700',
error: 'bg-red-50 text-red-800 border-red-300 dark:bg-red-900/30 dark:text-red-200 dark:border-red-700'
};
export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
title?: string;
variant?: AlertVariant;
children?: React.ReactNode;
icon?: React.ReactNode;
compact?: boolean;
}
export const Alert: React.FC<AlertProps> = ({ title, variant = 'info', children, icon, compact, className, ...rest }) => {
return (
<div
className={`border rounded-md ${variantStyles[variant]} ${compact ? 'px-3 py-2 text-sm' : 'px-4 py-3'} flex items-start space-x-3 ${className || ''}`}
{...rest}
>
{icon && <div className="pt-0.5">{icon}</div>}
<div className="flex-1">
{title && <div className="font-medium mb-0.5">{title}</div>}
{children && <div className="leading-snug">{children}</div>}
</div>
</div>
);
};
export default Alert;

View File

@@ -0,0 +1,25 @@
import React from 'react';
export interface BreadcrumbItem { label: string; href?: string; }
export const Breadcrumbs: React.FC<{ items: BreadcrumbItem[]; onNavigate?: (href: string) => void; }> = ({ items, onNavigate }) => (
<nav className="text-sm text-[--text-secondary]" aria-label="Breadcrumb">
<ol className="flex flex-wrap items-center gap-1">
{items.map((item, i) => (
<li key={i} className="flex items-center">
{item.href ? (
<button
onClick={() => onNavigate?.(item.href!)}
className="text-blue-600 hover:underline dark:text-blue-400"
>{item.label}</button>
) : (
<span className="text-[--text-primary] font-medium">{item.label}</span>
)}
{i < items.length - 1 && <span className="mx-2 text-[--border-color]">/</span>}
</li>
))}
</ol>
</nav>
);
export default Breadcrumbs;

1
components/ui/Icons.tsx Normal file
View File

@@ -0,0 +1 @@
export { CompanyWikiIcon, SubmissionsIcon, ReportsIcon, ChatIcon, HelpIcon, SettingsIcon, CopyIcon, PlusIcon, ChevronDownIcon, UploadIcon, CheckIcon, WarningIcon, DownloadIcon, MinusIcon, SunIcon, MoonIcon, SystemIcon, SendIcon } from '../UiKit';

49
components/ui/Inputs.tsx Normal file
View File

@@ -0,0 +1,49 @@
import React from 'react';
interface BaseFieldProps { label?: string; description?: string; error?: string; required?: boolean; children: React.ReactNode; className?: string; }
export const Field: React.FC<BaseFieldProps> = ({ label, description, error, required, children, className }) => (
<div className={`space-y-2 ${className || ''}`}>
{label && (
<label className="block text-sm font-medium text-[--text-primary] tracking-[-0.14px]">
{label} {required && <span className="text-[--status-red]">*</span>}
</label>
)}
{children}
{description && !error && <p className="text-sm text-[--text-secondary] tracking-[-0.14px]">{description}</p>}
{error && <p className="text-sm text-[--status-red]">{error}</p>}
</div>
);
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { invalid?: boolean; }
export const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, invalid, ...rest }, ref) => (
<input
ref={ref}
className={`w-full px-4 py-3.5 rounded-full border text-sm bg-[--input-bg] text-[--text-primary] placeholder:text-[--input-placeholder] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent] border-[--input-border] transition-all duration-200 ${invalid ? 'border-red-500 focus:ring-red-500' : ''} ${className || ''}`}
{...rest}
/>
));
Input.displayName = 'Input';
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { invalid?: boolean; }
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, invalid, ...rest }, ref) => (
<textarea
ref={ref}
className={`w-full px-4 py-3.5 rounded-2xl border text-sm resize-vertical bg-[--input-bg] text-[--text-primary] placeholder:text-[--input-placeholder] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent] border-[--input-border] transition-all duration-200 ${invalid ? 'border-red-500 focus:ring-red-500' : ''} ${className || ''}`}
{...rest}
/>
));
Textarea.displayName = 'Textarea';
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> { invalid?: boolean; }
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(({ className, invalid, children, ...rest }, ref) => (
<select
ref={ref}
className={`w-full px-4 py-3.5 rounded-full border text-sm bg-[--input-bg] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent] border-[--input-border] transition-all duration-200 ${invalid ? 'border-red-500 focus:ring-red-500' : ''} ${className || ''}`}
{...rest}
>
{children}
</select>
));
Select.displayName = 'Select';
export default { Field, Input, Textarea, Select };

View File

@@ -0,0 +1,24 @@
import React from 'react';
export const LinearProgress: React.FC<{ value: number; className?: string; }> = ({ value, className }) => (
<div className={`w-full h-2 rounded-full bg-[--background-tertiary] overflow-hidden ${className || ''}`}>
<div className="h-full bg-blue-500 transition-all" style={{ width: `${Math.min(100, Math.max(0, value))}%` }} />
</div>
);
export interface StepProgressProps { current: number; total: number; labels?: string[]; }
export const StepProgress: React.FC<StepProgressProps> = ({ current, total, labels }) => (
<div>
<div className="flex justify-between mb-2">
{Array.from({ length: total }).map((_, i) => (
<div key={i} className="flex-1 flex flex-col items-center">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mb-1 ${i <= current ? 'bg-blue-500 text-white' : 'bg-[--background-tertiary] text-[--text-secondary]'}`}>{i + 1}</div>
{labels && labels[i] && <span className="text-[10px] text-center px-1 text-[--text-secondary] truncate max-w-[72px]">{labels[i]}</span>}
</div>
))}
</div>
<LinearProgress value={((current + 1) / total) * 100} />
</div>
);
export default { LinearProgress, StepProgress };

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { Field } from './Inputs';
interface QuestionProps { label: string; required?: boolean; description?: string; error?: string; children: React.ReactNode; }
export const Question: React.FC<QuestionProps> = ({ label, required, description, error, children }) => (
<Field label={label} required={required} description={description} error={error}>{children}</Field>
);
export default Question;

View File

@@ -0,0 +1,182 @@
import React from 'react';
import { EmployeeQuestion, EMPLOYEE_QUESTIONS } from '../../employeeQuestions';
import { Input, Textarea } from './Inputs';
interface QuestionInputProps {
question: EmployeeQuestion;
value: string;
onChange: (value: string) => void;
className?: string;
// For yes/no questions with follow-ups, we need access to all answers and ability to set follow-up
allAnswers?: Record<string, string>;
onFollowupChange?: (questionId: string, value: string) => void;
}
// Helper function to find follow-up question for a given question
const findFollowupQuestion = (questionId: string): EmployeeQuestion | null => {
return EMPLOYEE_QUESTIONS.find(q => q.followupTo === questionId) || null;
};
export const QuestionInput: React.FC<QuestionInputProps> = ({
question,
value,
onChange,
className = '',
allAnswers = {},
onFollowupChange
}) => {
const baseInputClasses = "w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500";
switch (question.type) {
case 'text':
return (
<Input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={question.placeholder}
className={className}
/>
);
case 'textarea':
return (
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={question.placeholder}
className={className}
rows={4}
/>
);
case 'yesno':
const followupQuestion = findFollowupQuestion(question.id);
const followupValue = followupQuestion ? allAnswers[followupQuestion.id] || '' : '';
return (
<div className={`space-y-4 ${className}`}>
{/* Yes/No Radio Buttons */}
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name={question.id}
value="Yes"
checked={value === 'Yes'}
onChange={(e) => onChange(e.target.value)}
className="w-4 h-4 text-[--accent] border-[--border-color] focus:ring-[--accent]"
/>
<span className="text-[--text-primary]">Yes</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name={question.id}
value="No"
checked={value === 'No'}
onChange={(e) => onChange(e.target.value)}
className="w-4 h-4 text-[--accent] border-[--border-color] focus:ring-[--accent]"
/>
<span className="text-[--text-primary]">No</span>
</label>
</div>
{/* Conditional Follow-up Textarea */}
{followupQuestion && value === 'Yes' && (
<div className="space-y-2">
<label className="block text-sm font-medium text-[--text-primary] tracking-[-0.14px]">
{followupQuestion.prompt}
</label>
<Textarea
value={followupValue}
onChange={(e) => onFollowupChange?.(followupQuestion.id, e.target.value)}
placeholder={followupQuestion.placeholder}
rows={3}
className="w-full"
/>
</div>
)}
</div>
);
case 'scale':
const scaleMin = question.scaleMin || 1;
const scaleMax = question.scaleMax || 10;
const currentValue = parseInt(value) || scaleMin;
return (
<div className={`space-y-3 ${className}`}>
<div className="flex w-full justify-between text-sm text-[--text-secondary]">
<span>{question.scaleLabels?.min || `${scaleMin}`}</span>
<span>{question.scaleLabels?.max || `${scaleMax}`}</span>
</div>
{/* Grid container that aligns slider and numbers */}
<div className="grid grid-cols-[auto_1fr_auto] gap-4 items-center">
{/* Left label space - dynamically sized */}
<div className="text-xs -me-12 text-transparent select-none">
{question.scaleLabels?.min || `${scaleMin}`}
</div>
{/* Slider container */}
<div className="relative">
<input
type="range"
min={scaleMin}
max={scaleMax}
value={currentValue}
onChange={(e) => onChange(e.target.value)}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
/>
{/* Numbers positioned absolutely under the slider */}
<div className="flex justify-between absolute -bottom-5 left-0 right-0 text-xs text-[--text-secondary]">
{Array.from({ length: scaleMax - scaleMin + 1 }, (_, i) => (
<span key={i} className="w-4 text-center">
{scaleMin + i}
</span>
))}
</div>
</div>
{/* Current value badge */}
<div className="w-12 h-8 bg-[--accent] text-white rounded flex items-center justify-center text-sm font-medium">
{currentValue}
</div>
</div>
{/* Add some bottom padding to account for the absolute positioned numbers */}
<div className="h-4"></div>
</div>
);
case 'select':
return (
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className={`${baseInputClasses} ${className}`}
>
<option value="">Select an option...</option>
{question.options?.map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
);
default:
return (
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={question.placeholder || "Type your answer here..."}
className={`${baseInputClasses} min-h-[100px] resize-vertical ${className}`}
rows={4}
/>
);
}
};
export default QuestionInput;

11
components/ui/Table.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
export const Table: React.FC<React.TableHTMLAttributes<HTMLTableElement>> = ({ className, children, ...rest }) => (
<table className={`w-full text-sm border-separate border-spacing-0 ${className || ''}`} {...rest}>{children}</table>
);
export const THead: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({ className, children, ...rest }) => (<thead className={`${className || ''}`} {...rest}>{children}</thead>);
export const TBody: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({ className, children, ...rest }) => (<tbody className={`${className || ''}`} {...rest}>{children}</tbody>);
export const TR: React.FC<React.HTMLAttributes<HTMLTableRowElement>> = ({ className, children, ...rest }) => (<tr className={`hover:bg-[--background-tertiary] transition-colors ${className || ''}`} {...rest}>{children}</tr>);
export const TH: React.FC<React.ThHTMLAttributes<HTMLTableHeaderCellElement>> = ({ className, children, ...rest }) => (<th className={`text-left font-medium px-4 py-2 text-[--text-secondary] border-b border-[--border-color] bg-[--background-secondary] first:rounded-tl-md last:rounded-tr-md ${className || ''}`} {...rest}>{children}</th>);
export const TD: React.FC<React.TdHTMLAttributes<HTMLTableCellElement>> = ({ className, children, ...rest }) => (<td className={`px-4 py-2 border-b border-[--border-color] text-[--text-primary] ${className || ''}`} {...rest}>{children}</td>);
export default { Table, THead, TBody, TR, TH, TD };

8
components/ui/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export * from './Alert';
export * from './Breadcrumb';
export * from './Icons';
export * from './Inputs';
export * from './Progress';
export * from './Question';
export * from './QuestionInput';
export * from './Table';