# Functioneel & Technisch Ontwerp: Cortex Shared Components **Projectnaam:** Cortex Intake Intent System - Shared Components **Versie:** v1.0 **Datum:** 03-02-2026 **Auteur:** Colin Lit (met AI-assistentie) --- ## 1. Doel en Context ### 1.1 Doel van dit document Dit gecombineerde FO/TO beschrijft de **shared components** die worden hergebruikt door alle Cortex Blocks. Het document combineert: - **Functioneel (FO):** Wat de gebruiker ziet en ervaart - **Technisch (TO):** Hoe de componenten worden gebouwd ### 1.2 Relatie met andere documenten | Document | Relatie | |----------|---------| | `intake-process-intents.md` | Beschrijft de intents die deze components gebruiken | | `gap-analyse-intake-cortex.md` | Identificeert welke components ontbreken | | `block-template-pattern.md` | Beschrijft het patroon waarop deze components gebaseerd zijn | ### 1.3 Scope **In scope:** - 4 nieuwe shared components voor Cortex Blocks - 2 nieuwe custom hooks voor data fetching en context **Buiten scope:** - Individuele intake blocks (apart document) - Bestaande components (`patient-list-item.tsx`, `linked-evidence.tsx`) --- ## 2. Overzicht Componenten ### 2.1 Component Hiërarchie ``` components/cortex/ ├── blocks/ │ └── [intake-block].tsx ← Gebruikt shared components │ ├── shared/ │ ├── patient-list-item.tsx ✅ Bestaat │ ├── linked-evidence.tsx ✅ Bestaat │ │ │ ├── block-states.tsx 🆕 NIEUW (dit document) │ ├── block-section.tsx 🆕 NIEUW (dit document) │ ├── block-item.tsx 🆕 NIEUW (dit document) │ └── block-footer.tsx 🆕 NIEUW (dit document) lib/cortex/hooks/ ├── use-patient-search.ts ✅ Bestaat ├── use-patient-selection.ts ✅ Bestaat │ ├── use-block-data.ts 🆕 NIEUW (dit document) └── use-intake-context.ts 🆕 NIEUW (dit document) ``` ### 2.2 Component Overzicht | Component | Type | Functie | Gebruikt door | |-----------|------|---------|---------------| | `BlockLoading` | State | Laad-indicator tijdens data fetch | Alle blocks | | `BlockError` | State | Foutmelding met retry optie | Alle blocks | | `BlockEmpty` | State | Lege staat met actie | Alle blocks | | `BlockSection` | Layout | Sectie met icon en titel | Query blocks | | `BlockItem` | Layout | Lijst-item met badge | Query blocks | | `BlockFooter` | Layout | Footer met acties | Alle blocks | --- ## 3. User Stories ### 3.1 Algemene User Stories | ID | Rol | Doel / Actie | Verwachte waarde | Prioriteit | |----|-----|--------------|------------------|------------| | US-SC-01 | Gebruiker | Zien dat data wordt geladen | Weet dat systeem bezig is | Hoog | | US-SC-02 | Gebruiker | Foutmelding zien bij problemen | Kan retry doen of hulp zoeken | Hoog | | US-SC-03 | Gebruiker | Lege staat zien met suggestie | Weet wat te doen als geen data | Middel | | US-SC-04 | Gebruiker | Snel navigeren naar EPD | Kan details bekijken in dossier | Hoog | | US-SC-05 | Gebruiker | Risico niveau direct zien | Beoordeelt urgentie in één oogopslag | Hoog | ### 3.2 Developer User Stories | ID | Rol | Doel / Actie | Verwachte waarde | Prioriteit | |----|-----|--------------|------------------|------------| | US-DEV-01 | Developer | Consistente states hergebruiken | Sneller nieuwe blocks bouwen | Hoog | | US-DEV-02 | Developer | Generieke data hook gebruiken | Minder boilerplate code | Hoog | | US-DEV-03 | Developer | Intake context automatisch hebben | Geen prop drilling nodig | Middel | --- ## 4. Functionele Specificaties per Component ### 4.1 BlockLoading **Doel:** Toon visuele feedback tijdens het laden van data. **Functioneel gedrag:** - Toont geanimeerde spinner (rotatie) - Toont optionele tekst (bijv. "Risico's laden...") - Centreert verticaal en horizontaal in block - Verdwijnt zodra data geladen is **Visueel:** ``` ┌─────────────────────────────────────┐ │ │ │ │ │ [◌ spinner] │ │ Risico's laden... │ │ │ │ │ └─────────────────────────────────────┘ ``` **Props:** | Prop | Type | Default | Beschrijving | |------|------|---------|--------------| | `message` | `string` | `"Laden..."` | Tekst onder spinner | --- ### 4.2 BlockError **Doel:** Toon foutmelding met optie om opnieuw te proberen. **Functioneel gedrag:** - Toont rood/oranje waarschuwingsicoon - Toont foutmelding (gebruikersvriendelijk, Nederlands) - Optionele "Opnieuw proberen" knop - Verdwijnt bij retry of bij sluiten block **Visueel:** ``` ┌─────────────────────────────────────┐ │ │ │ [⚠️ icon] │ │ │ │ Kon risico's niet ophalen. │ │ Controleer je internetverbinding. │ │ │ │ [🔄 Opnieuw proberen] │ │ │ └─────────────────────────────────────┘ ``` **Props:** | Prop | Type | Default | Beschrijving | |------|------|---------|--------------| | `message` | `string` | - | Foutmelding tekst | | `onRetry` | `() => void` | `undefined` | Callback voor retry (toont knop indien aanwezig) | --- ### 4.3 BlockEmpty **Doel:** Toon informatieve lege staat met optionele actie. **Functioneel gedrag:** - Toont context-relevant icoon (configureerbaar) - Toont melding waarom leeg - Optionele actie-knop (bijv. "Toevoegen in dossier") - Visueel subtiel (niet alarmerend) **Visueel:** ``` ┌─────────────────────────────────────┐ │ │ │ [📋 icon faded] │ │ │ │ Geen risico's geregistreerd │ │ │ │ [+ Toevoegen in dossier] │ │ │ └─────────────────────────────────────┘ ``` **Props:** | Prop | Type | Default | Beschrijving | |------|------|---------|--------------| | `icon` | `LucideIcon` | - | Icoon component | | `message` | `string` | - | Hoofdboodschap | | `action` | `{ label: string; onClick: () => void }` | `undefined` | Optionele actie knop | --- ### 4.4 BlockSection **Doel:** Groepeer gerelateerde content met visuele header. **Functioneel gedrag:** - Toont icoon + titel + optionele count - Bevat children content - Consistente styling (witte achtergrond, border, padding) - Optioneel inklapbaar (v2) **Visueel:** ``` ┌─────────────────────────────────────┐ │ [⚠️] Risicotaxatie (3) │ ← Header ├─────────────────────────────────────┤ │ │ │ [Content / children] │ ← Children │ │ └─────────────────────────────────────┘ ``` **Props:** | Prop | Type | Default | Beschrijving | |------|------|---------|--------------| | `icon` | `LucideIcon` | - | Icoon component | | `iconColor` | `string` | `"text-slate-600"` | Tailwind kleur class | | `title` | `string` | - | Sectie titel | | `count` | `number` | `undefined` | Optionele count badge | | `children` | `ReactNode` | - | Sectie inhoud | --- ### 4.5 BlockItem **Doel:** Toon een enkel item in een lijst met consistente styling. **Functioneel gedrag:** - Toont titel + optionele subtitle - Toont status badge (kleur-gecodeerd) - Klikbaar indien `onClick` meegegeven - Hover state bij klikbaar - Geen hover bij niet-klikbaar **Badge varianten:** | Variant | Kleur | Gebruik | |---------|-------|---------| | `default` | Grijs | Neutraal, geen status | | `success` | Groen | Laag risico, compleet | | `warning` | Oranje | Gemiddeld risico, aandacht | | `danger` | Rood | Hoog risico, urgent | **Visueel:** ``` ┌─────────────────────────────────────────────────────┐ │ [Avatar?] Suïcidaliteit [Matig] │ ← Klikbaar │ 12 jan 2026 • Dr. Jansen │ └─────────────────────────────────────────────────────┘ ``` **Props:** | Prop | Type | Default | Beschrijving | |------|------|---------|--------------| | `title` | `string` | - | Hoofdtekst | | `subtitle` | `string` | `undefined` | Subtekst (datum, auteur, etc.) | | `badge` | `{ label: string; variant: BadgeVariant }` | `undefined` | Status badge | | `onClick` | `() => void` | `undefined` | Klik handler (maakt item klikbaar) | --- ### 4.6 BlockFooter **Doel:** Consistente footer met acties onderaan een block. **Functioneel gedrag:** - Links: secundaire actie (ghost button) - Rechts: primaire actie (solid button) - Horizontale scheidingslijn boven footer - Loading state op primaire actie **Visueel:** ``` ───────────────────────────────────────── [🔗 Bekijk in dossier] [+ Toevoegen] (secundair) (primair) ``` **Props:** | Prop | Type | Default | Beschrijving | |------|------|---------|--------------| | `secondaryAction` | `{ label, icon?, onClick }` | `undefined` | Linker actie | | `primaryAction` | `{ label, icon?, onClick, loading? }` | `undefined` | Rechter actie | --- ## 5. UI Overzicht ### 5.1 Component Compositie in een Block ``` ┌─────────────────────────────────────────────────────────────┐ │ [Icon] Risicotaxatie [X Close] │ ← BlockContainer ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ [⚠️] Huidige risico's (3) │ │ ← BlockSection │ ├───────────────────────────────────────────────────────┤ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ Suïcidaliteit [Matig] │ │ │ ← BlockItem │ │ │ 12 jan 2026 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ Agressie [Laag] │ │ │ ← BlockItem │ │ │ 10 jan 2026 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ ───────────────────────────────────────────────────────── │ │ [🔗 Bekijk in dossier] [+ Toevoegen] │ ← BlockFooter │ │ └─────────────────────────────────────────────────────────────┘ ``` ### 5.2 State Flows ``` Block Lifecycle: ┌─────────┐ data? ┌─────────┐ │ Loading │───────────▶│ Data │ └─────────┘ yes └─────────┘ │ │ │ error │ empty ▼ ▼ ┌─────────┐ ┌─────────┐ │ Error │ │ Empty │ └─────────┘ └─────────┘ │ │ │ retry │ action ▼ ▼ ┌─────────┐ ┌─────────┐ │ Loading │ │ EPD │ └─────────┘ │ Navigatie│ └─────────┘ ``` --- ## 6. Technische Architectuur ### 6.1 Component Structuur ``` components/cortex/shared/ ├── block-states.tsx # BlockLoading, BlockError, BlockEmpty ├── block-section.tsx # BlockSection ├── block-item.tsx # BlockItem ├── block-footer.tsx # BlockFooter └── index.ts # Re-exports lib/cortex/hooks/ ├── use-block-data.ts # Generic data fetching hook └── use-intake-context.ts # Intake context hook ``` ### 6.2 Dependencies ``` Externe dependencies: ├── lucide-react # Icons ├── framer-motion # Animaties (optioneel) └── @/components/ui/button # shadcn Button component Interne dependencies: ├── @/lib/utils # cn() helper ├── @/lib/cortex/error-handler # safeFetch, getErrorInfo ├── @/stores/cortex-store # useCortexStore └── @/hooks/use-toast # Toast notifications ``` --- ## 7. Technische Specificaties ### 7.1 block-states.tsx ```typescript // components/cortex/shared/block-states.tsx 'use client'; import { Loader2, AlertCircle, RefreshCw } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; // ============================================================================ // BlockLoading // ============================================================================ interface BlockLoadingProps { /** Tekst onder de spinner */ message?: string; /** Extra CSS classes */ className?: string; } export function BlockLoading({ message = 'Laden...', className }: BlockLoadingProps) { return (
{message}
); } // ============================================================================ // BlockError // ============================================================================ interface BlockErrorProps { /** Foutmelding tekst */ message: string; /** Callback voor retry knop (toont knop indien aanwezig) */ onRetry?: () => void; /** Extra CSS classes */ className?: string; } export function BlockError({ message, onRetry, className }: BlockErrorProps) { return (

{message}

{onRetry && ( )}
); } // ============================================================================ // BlockEmpty // ============================================================================ interface BlockEmptyProps { /** Icoon component */ icon: LucideIcon; /** Hoofdboodschap */ message: string; /** Optionele actie knop */ action?: { label: string; onClick: () => void; }; /** Extra CSS classes */ className?: string; } export function BlockEmpty({ icon: Icon, message, action, className }: BlockEmptyProps) { return (

{message}

{action && ( )}
); } ``` ### 7.2 block-section.tsx ```typescript // components/cortex/shared/block-section.tsx 'use client'; import type { ReactNode } from 'react'; import type { LucideIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; interface BlockSectionProps { /** Icoon component */ icon: LucideIcon; /** Tailwind kleur class voor icoon */ iconColor?: string; /** Sectie titel */ title: string; /** Optionele count badge */ count?: number; /** Sectie inhoud */ children: ReactNode; /** Extra CSS classes */ className?: string; } export function BlockSection({ icon: Icon, iconColor = 'text-slate-600', title, count, children, className, }: BlockSectionProps) { return (
{/* Header */}

{title}

{count !== undefined && ( ({count}) )}
{/* Content */} {children}
); } ``` ### 7.3 block-item.tsx ```typescript // components/cortex/shared/block-item.tsx 'use client'; import { cn } from '@/lib/utils'; // ============================================================================ // Types // ============================================================================ export type BadgeVariant = 'default' | 'success' | 'warning' | 'danger'; interface BlockItemProps { /** Hoofdtekst */ title: string; /** Subtekst (datum, auteur, etc.) */ subtitle?: string; /** Status badge */ badge?: { label: string; variant: BadgeVariant; }; /** Klik handler (maakt item klikbaar) */ onClick?: () => void; /** Extra CSS classes */ className?: string; } // ============================================================================ // Styling // ============================================================================ const BADGE_STYLES: Record = { default: 'bg-slate-100 text-slate-700 border-slate-200', success: 'bg-green-50 text-green-700 border-green-200', warning: 'bg-amber-50 text-amber-700 border-amber-200', danger: 'bg-red-50 text-red-700 border-red-200', }; // ============================================================================ // Component // ============================================================================ export function BlockItem({ title, subtitle, badge, onClick, className }: BlockItemProps) { const isClickable = Boolean(onClick); const Component = isClickable ? 'button' : 'div'; return ( {/* Content */}

{title}

{subtitle && (

{subtitle}

)}
{/* Badge */} {badge && ( {badge.label} )}
); } // ============================================================================ // Helper: Get variant from risk level // ============================================================================ export function getRiskBadgeVariant(level: string): BadgeVariant { switch (level.toLowerCase()) { case 'laag': return 'success'; case 'gemiddeld': return 'warning'; case 'hoog': case 'zeer_hoog': return 'danger'; default: return 'default'; } } ``` ### 7.4 block-footer.tsx ```typescript // components/cortex/shared/block-footer.tsx 'use client'; import type { LucideIcon } from 'lucide-react'; import { Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; interface ActionConfig { /** Knop label */ label: string; /** Optioneel icoon */ icon?: LucideIcon; /** Click handler */ onClick: () => void; /** Loading state (alleen voor primary) */ loading?: boolean; /** Disabled state */ disabled?: boolean; } interface BlockFooterProps { /** Linker actie (ghost button) */ secondaryAction?: ActionConfig; /** Rechter actie (solid button) */ primaryAction?: ActionConfig; /** Extra CSS classes */ className?: string; } export function BlockFooter({ secondaryAction, primaryAction, className }: BlockFooterProps) { // Don't render if no actions if (!secondaryAction && !primaryAction) { return null; } return (
{/* Secondary Action (left) */} {secondaryAction ? ( ) : (
// Spacer )} {/* Primary Action (right) */} {primaryAction && ( )}
); } ``` ### 7.5 use-block-data.ts ```typescript // lib/cortex/hooks/use-block-data.ts 'use client'; import { useState, useEffect, useCallback } from 'react'; import { useToast } from '@/hooks/use-toast'; import { safeFetch, getErrorInfo } from '@/lib/cortex/error-handler'; // ============================================================================ // Types // ============================================================================ interface UseBlockDataOptions { /** API endpoint (relatief pad) */ endpoint: string; /** Query parameters */ params?: Record; /** Of data moet worden opgehaald */ enabled?: boolean; /** Callback bij error */ onError?: (error: Error) => void; /** Operatie naam voor error messages */ operationName?: string; } interface UseBlockDataResult { /** Opgehaalde data */ data: T | null; /** Loading state */ isLoading: boolean; /** Error message */ error: string | null; /** Refetch functie */ refetch: () => Promise; } // ============================================================================ // Hook // ============================================================================ export function useBlockData({ endpoint, params, enabled = true, onError, operationName = 'Data laden', }: UseBlockDataOptions): UseBlockDataResult { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(enabled); const [error, setError] = useState(null); const { toast } = useToast(); const fetchData = useCallback(async () => { if (!enabled) return; setIsLoading(true); setError(null); try { // Build URL with params const url = new URL(endpoint, window.location.origin); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.set(key, value); } }); } const response = await safeFetch( url.toString(), undefined, { operation: operationName } ); const result = await response.json(); setData(result); } catch (err) { const errorInfo = getErrorInfo(err, { operation: operationName }); setError(errorInfo.description); onError?.(err as Error); toast({ variant: 'destructive', title: errorInfo.title, description: errorInfo.description, }); } finally { setIsLoading(false); } }, [endpoint, JSON.stringify(params), enabled, onError, operationName, toast]); useEffect(() => { if (enabled) { fetchData(); } }, [fetchData, enabled]); return { data, isLoading, error, refetch: fetchData }; } ``` ### 7.6 use-intake-context.ts ```typescript // lib/cortex/hooks/use-intake-context.ts 'use client'; import { useMemo } from 'react'; import { useCortexStore } from '@/stores/cortex-store'; import type { BlockPrefillData } from '@/stores/cortex-store'; import { formatPatientName } from '@/lib/fhir/patient-mapper'; // ============================================================================ // Types // ============================================================================ interface UseIntakeContextResult { /** Patient ID (van prefill of activePatient) */ patientId: string | null; /** Intake ID (van prefill) */ intakeId: string | null; /** Patient naam (voor display) */ patientName: string | null; /** Of er voldoende context is */ hasPatientContext: boolean; /** Of er intake context is */ hasIntakeContext: boolean; } // ============================================================================ // Hook // ============================================================================ export function useIntakeContext( prefill?: BlockPrefillData ): UseIntakeContextResult { const { activePatient } = useCortexStore(); return useMemo(() => { // Patient context: van prefill of activePatient const patientId = prefill?.patientId || activePatient?.id || null; const patientName = prefill?.patientName || (activePatient ? formatPatientName(activePatient) : null); // Intake context: alleen van prefill (TODO: activeIntake in store) const intakeId = prefill?.intakeId || null; return { patientId, intakeId, patientName, hasPatientContext: Boolean(patientId), hasIntakeContext: Boolean(intakeId), }; }, [prefill, activePatient]); } ``` ### 7.7 Index Export ```typescript // components/cortex/shared/index.ts // Block States export { BlockLoading, BlockError, BlockEmpty } from './block-states'; // Block Layout export { BlockSection } from './block-section'; export { BlockItem, getRiskBadgeVariant, type BadgeVariant } from './block-item'; export { BlockFooter } from './block-footer'; // Existing export { PatientListItem, PatientListEmpty, PatientListLoading } from './patient-list-item'; export { LinkedEvidence } from './linked-evidence'; ``` --- ## 8. API Ontwerp ### 8.1 Geen nieuwe API endpoints De shared components maken gebruik van bestaande endpoints en de nieuwe intake endpoints (beschreven in apart document). De hooks abstraheren de API calls. ### 8.2 Error Handling Alle API errors worden afgehandeld via de bestaande `error-handler.ts`: ```typescript // Voorbeeld error flow try { const response = await safeFetch('/api/cortex/intake/123/risks'); // ... } catch (err) { const errorInfo = getErrorInfo(err, { operation: 'Risico's laden' }); // errorInfo.title = "Laden mislukt" // errorInfo.description = "Kon risico's niet ophalen. Probeer opnieuw." } ``` --- ## 9. Gebruikersrollen en Rechten De shared components zijn UI-only en hebben geen eigen rechtenmodel. Rechten worden afgedwongen door: 1. **API routes** - RLS policies in Supabase 2. **Store** - `activePatient` alleen beschikbaar na selectie 3. **Blocks** - Tonen alleen data waartoe gebruiker toegang heeft --- ## 10. Performance & Accessibility ### 10.1 Performance | Aspect | Target | Implementatie | |--------|--------|---------------| | Render tijd | < 16ms | Geen zware berekeningen in render | | Bundle size | < 5KB per component | Tree-shakeable exports | | Re-renders | Minimaal | `useMemo` voor derived state | ### 10.2 Accessibility | Aspect | Implementatie | |--------|---------------| | Screen readers | `aria-label` op interactieve elementen | | Keyboard nav | `button` elements met `onClick` | | Focus visible | Tailwind `focus-visible:ring` | | Color contrast | WCAG AA compliant kleuren | | Loading state | `aria-busy="true"` | --- ## 11. Testing Strategie ### 11.1 Unit Tests ```typescript // __tests__/components/cortex/shared/block-item.test.tsx describe('BlockItem', () => { it('renders title and subtitle', () => { render(); expect(screen.getByText('Test')).toBeInTheDocument(); expect(screen.getByText('Sub')).toBeInTheDocument(); }); it('renders badge with correct variant', () => { render(); const badge = screen.getByText('Hoog'); expect(badge).toHaveClass('bg-red-50'); }); it('is clickable when onClick provided', () => { const onClick = jest.fn(); render(); fireEvent.click(screen.getByRole('button')); expect(onClick).toHaveBeenCalled(); }); it('is not clickable when onClick not provided', () => { render(); expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); }); ``` ### 11.2 Visual Tests Storybook stories voor elke component variant: ```typescript // stories/BlockItem.stories.tsx export default { title: 'Cortex/Shared/BlockItem', component: BlockItem, }; export const Default = () => ; export const WithSubtitle = () => ; export const WithBadge = () => ; export const Clickable = () => alert('Clicked!')} />; ``` --- ## 12. Implementatie Volgorde | # | Component | Effort | Dependencies | |---|-----------|--------|--------------| | 1 | `block-states.tsx` | S | Geen | | 2 | `block-section.tsx` | S | Geen | | 3 | `block-item.tsx` | S | Geen | | 4 | `block-footer.tsx` | S | `@/components/ui/button` | | 5 | `use-block-data.ts` | M | `error-handler.ts` | | 6 | `use-intake-context.ts` | S | `cortex-store` | | 7 | `index.ts` exports | S | 1-6 | **Totaal geschat:** ~1 dag --- ## 13. Risico's en Mitigatie | Risico | Impact | Mitigatie | |--------|--------|-----------| | Inconsistente styling | Middel | Design tokens in Tailwind config | | Over-engineering | Laag | YAGNI - alleen bouwen wat nodig is | | Bundle bloat | Laag | Tree-shakeable exports | | Breaking changes | Middel | Semantic versioning, deprecation warnings | --- ## 14. Bijlagen & Referenties **Projectdocumenten:** - `docs/intent/intake-intent-proces/intake-process-intents.md` - Intent specificaties - `docs/intent/intake-intent-proces/gap-analyse-intake-cortex.md` - Gap analyse - `docs/intent/intake-intent-proces/block-template-pattern.md` - Template patterns **Bestaande code:** - `components/cortex/shared/patient-list-item.tsx` - Referentie voor patterns - `components/cortex/blocks/block-container.tsx` - Parent container - `lib/cortex/error-handler.ts` - Error handling utilities **Tech documentatie:** - Lucide Icons: https://lucide.dev/icons - Tailwind CSS: https://tailwindcss.com/docs - shadcn/ui Button: https://ui.shadcn.com/docs/components/button