diff --git a/app/globals.css b/app/globals.css index 01d8300..a057498 100644 --- a/app/globals.css +++ b/app/globals.css @@ -81,3 +81,26 @@ @apply bg-background text-foreground; } } + +@layer utilities { + .scrollbar-thin { + scrollbar-width: thin; + } + + .scrollbar-thin::-webkit-scrollbar { + width: 8px; + } + + .scrollbar-thin::-webkit-scrollbar-track { + background: transparent; + } + + .scrollbar-thin::-webkit-scrollbar-thumb { + background-color: rgb(203 213 225); + border-radius: 4px; + } + + .scrollbar-thin::-webkit-scrollbar-thumb:hover { + background-color: rgb(148 163 184); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 013d117..7de6703 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -37,10 +37,10 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > -
+
- {children} +
{children}
diff --git a/app/page.tsx b/app/page.tsx index 11fa4a2..796013b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -46,7 +46,7 @@ export default function Page() { transparent competency benchmark - . Let’s build it together. + . Let's build it together.

Memo turns collaborative competency mapping into an open @@ -92,15 +92,7 @@ export default function Page() {
Competency-Based Recommenders -

+

Competency-based recommenders use{' '} competency networks diff --git a/app/session/page.tsx b/app/session/page.tsx index 2d417a0..2f819a0 100644 --- a/app/session/page.tsx +++ b/app/session/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState, useCallback, Suspense } from 'react'; +import { useEffect, useState, useCallback, useRef, Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import { getRandomCompetenciesAction } from '@/app/actions/competencies'; import { @@ -18,15 +18,22 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { Kbd } from '@/components/ui/kbd'; -import { ArrowRight } from 'lucide-react'; +import { + ArrowRight, + Info, + ArrowLeftRight, + Check, + Layers, + TrendingUp, + Equal, + Unlink, +} from 'lucide-react'; import { Card, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; -import { Label } from '@/components/ui/label'; -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; type SessionStats = { completed: number; @@ -46,6 +53,74 @@ const RELATIONSHIP_TYPES: RelationshipTypeOption[] = ( label: type.charAt(0) + type.slice(1).toLowerCase(), })); +// Blue-Purple theme colors (hardcoded after user selection) +const THEME_COLORS = { + source: { primary: '#0a4da2', secondary: '#4263eb' }, + destination: { primary: '#7c3aed', secondary: '#9775fa' }, +}; + +// Helper to get dynamic description for relationship types +// Used in the live preview banner +const getRelationshipDescription = ( + type: RelationshipType, + titleA: string, + titleB: string +) => { + switch (type) { + case 'ASSUMES': + return ( + <> + {titleA} requires{' '} + {titleB} as + prerequisite + + ); + case 'EXTENDS': + return ( + <> + {titleA} builds on + / is a subset or advanced form of{' '} + {titleB} + + ); + case 'MATCHES': + return ( + <> + {titleA} is + equivalent / strongly overlaps with{' '} + {titleB} + + ); + case 'UNRELATED': + return ( + <> + {titleA} and{' '} + {titleB} have no + meaningful relationship + + ); + default: + return ''; + } +}; + +// Text colors for relationship types (used in preview sentence) +const RELATIONSHIP_TYPE_TEXT_COLORS: Record = { + ASSUMES: 'text-blue-600', + EXTENDS: 'text-purple-600', + MATCHES: 'text-emerald-600', + UNRELATED: 'text-slate-600', +}; + +// Shared scrollbar styles for competency description containers +const SCROLLBAR_STYLES: { + scrollbarWidth: 'thin'; + scrollbarColor: string; +} = { + scrollbarWidth: 'thin', + scrollbarColor: 'rgb(203 213 225) transparent', +}; + function SessionPageContent() { const searchParams = useSearchParams(); const countParam = searchParams.get('count'); @@ -53,9 +128,7 @@ function SessionPageContent() { const count = Number.isFinite(parsedCount) && parsedCount > 0 ? parsedCount : 2; - const [relation, setRelation] = useState( - RELATIONSHIP_TYPES[0]!.value - ); + const [relation, setRelation] = useState(null); const [stats, setStats] = useState({ completed: 0, skipped: 0, @@ -71,6 +144,31 @@ function SessionPageContent() { const [error, setError] = useState(null); const [userId, setUserId] = useState(null); const [isCreating, setIsCreating] = useState(false); + const [isTransitioning, setIsTransitioning] = useState(false); + const [swapRotation, setSwapRotation] = useState(0); + const [hoveredRelation, setHoveredRelation] = + useState(null); + const hoverTimeoutRef = useRef(null); + + const handleMouseEnter = useCallback((val: RelationshipType) => { + if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = setTimeout(() => setHoveredRelation(val), 120); + }, []); + + const handleMouseLeave = useCallback(() => { + if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = setTimeout(() => setHoveredRelation(null), 70); + }, []); + + // Cleanup hover timeout on unmount to prevent memory leaks + useEffect(() => { + return () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + }; + }, []); const [relationshipToDelete, setRelationshipToDelete] = useState< string | null >(null); @@ -99,30 +197,43 @@ function SessionPageContent() { void loadDemoUser(); }, []); - const loadCompetencies = useCallback(async () => { - setError(null); - setCompetencies(null); + const loadCompetencies = useCallback( + async (isInitialLoad = false) => { + setError(null); - const result = await getRandomCompetenciesAction(count); - if (!result.success) { - setError( - result.error ?? - 'An unexpected error occurred while fetching competencies.' - ); - setCompetencies([]); - return; - } + // Only set competencies to null on initial load, not during transitions + if (isInitialLoad) { + setCompetencies(null); + } else { + setIsTransitioning(true); + } - if (!result.competencies || result.competencies.length === 0) { - setCompetencies([]); - return; - } + const result = await getRandomCompetenciesAction(count); - setCompetencies(result.competencies); - }, [count]); + if (!result.success) { + setError( + result.error ?? + 'An unexpected error occurred while fetching competencies.' + ); + setCompetencies([]); + setIsTransitioning(false); + return; + } + + if (!result.competencies || result.competencies.length === 0) { + setCompetencies([]); + setIsTransitioning(false); + return; + } + + setCompetencies(result.competencies); + setIsTransitioning(false); + }, + [count] + ); useEffect(() => { - void loadCompetencies(); + loadCompetencies(true); }, [loadCompetencies]); const handleAction = useCallback( @@ -162,6 +273,7 @@ function SessionPageContent() { competencies: competencies ? [...competencies] : undefined, }, ]); + setRelation(null); // Reset selection after adding relation await loadCompetencies(); } catch (err) { setError( @@ -180,6 +292,7 @@ function SessionPageContent() { competencies: competencies ? [...competencies] : undefined, }, ]); + setRelation(null); // Reset relationship type selection try { await loadCompetencies(); } catch (err) { @@ -256,12 +369,14 @@ function SessionPageContent() { return; } - // ⌘+Shift+Z or Ctrl+Shift+Z for Undo (only if there's history to undo) - // Using Shift+Z to avoid browser's default undo behavior + // Shift+Z for Undo (only if there's history to undo, and NOT Cmd/Ctrl+Shift+Z) + // Check this early to prevent conflicts with other handlers if ( - (event.metaKey || event.ctrlKey) && - event.key === 'z' && + event.key.toLowerCase() === 'z' && event.shiftKey && + !event.metaKey && + !event.ctrlKey && + !event.altKey && history.length > 0 ) { event.preventDefault(); @@ -270,6 +385,18 @@ function SessionPageContent() { return; } + // If Enter is pressed on a relationship button, prevent default button behavior + // and let the global handler manage it + if ( + event.key === 'Enter' && + target.tagName === 'BUTTON' && + target.closest('[data-relationship-button]') + ) { + event.preventDefault(); + event.stopPropagation(); + // Continue to global Enter handler below + } + // Space for Skip (only if not loading and has competencies) if ( event.key === ' ' && @@ -283,6 +410,24 @@ function SessionPageContent() { return; } + // 1, 2, 3, 4 for relationship types (only if relationship types are loaded) + // Check this BEFORE Enter to prevent conflicts + if ( + ['1', '2', '3', '4'].includes(event.key) && + RELATIONSHIP_TYPES.length > 0 && + !isLoading && + !isCreating + ) { + event.preventDefault(); + event.stopPropagation(); + const index = parseInt(event.key) - 1; + if (RELATIONSHIP_TYPES[index]) { + const selectedValue = RELATIONSHIP_TYPES[index]!.value; + setRelation(prev => (prev === selectedValue ? null : selectedValue)); + } + return; + } + // Enter for Add Relation (only if not loading, not creating, has userId, relation, and competencies) if ( event.key === 'Enter' && @@ -294,6 +439,7 @@ function SessionPageContent() { competencies.length >= 2 ) { event.preventDefault(); + event.stopPropagation(); handleAction('completed'); return; } @@ -315,22 +461,33 @@ function SessionPageContent() { ]); return ( -

+
-
-
-
- Mapping Session - - Completed: {stats.completed} +
+
+ {/* Header Section */} +
+
+ + Mapping Session + +
+ + + + Completed:{' '} + + {stats.completed} + + + +
+
{error && ( @@ -378,36 +535,103 @@ function SessionPageContent() { {!error && !noCompetencies && !notEnough && ( <> -

- {isLoading || !competencies || competencies.length < 2 - ? 'Loading competencies for this mapping session...' - : `How does "${competencies[0]!.title}" relate to "${ - competencies[1]!.title - }"?`} -

- -
-
+ {/* Main Question */} +
+ {isLoading || !competencies || competencies.length < 2 ? ( +
+ + How does + +
+ + relate to + +
+ + ? + +
+ ) : ( +

+ How does{' '} + + {competencies[0]!.title} + {' '} + relate to{' '} + + {competencies[1]!.title} + + ? +

+ )} +
+ + {/* Competencies Side by Side for Comparison */} +
+
+ {/* Origin Competency */} {isLoading || !competencies || !competencies[0] ? ( - - - - Loading first competency... - + +
+ +
+
+
+
+
+
+
+
+
+
+
) : ( - - -
+ +
+ +
+ + Competency + +
+
{/* Placeholder badges; replace with competency metadata once available */} - + Apply + @@ -425,80 +649,134 @@ function SessionPageContent() {

- + Control Flow
-
- +
+ {competencies[0]!.title} - - {competencies[0]!.description} - +
+ + {competencies[0]!.description} + +
)} -
-
-
-
-