diff --git a/frontend/src/app/playground/page.tsx b/frontend/src/app/playground/page.tsx index 187ba7cf..708532a5 100644 --- a/frontend/src/app/playground/page.tsx +++ b/frontend/src/app/playground/page.tsx @@ -1,25 +1,25 @@ 'use client'; import { VirtualizedFileTree, type FileTreeNode } from '@/components/explorer/VirtualizedFileTree'; -import dynamic from 'next/dynamic'; -const CodeEditor = dynamic(() => import('@/components/playground/CodeEditor').then((mod) => mod.CodeEditor), { - ssr: false, -}); import { OfflineIndicator } from '@/components/storage/OfflineIndicator'; import { - CompileOutputTerminal, - type CompileLogEntry, + CompileOutputTerminal, + type CompileLogEntry, } from '@/components/terminal/CompileOutputTerminal'; import { TerminalPanel } from '@/components/terminal/TerminalPanel'; import { WithSkeleton } from '@/components/ui/WithSkeleton'; import { EditorSkeleton } from '@/components/ui/skeletons/EditorSkeleton'; import { useTutorial } from '@/contexts/TutorialContext'; -import { useState, useEffect, useMemo, useCallback } from 'react'; import { CollaborationProvider } from '@/lib/collaboration/YjsProvider'; +import { FilePresenceManager } from '@/lib/explorer/FilePresence'; import { DatabaseManager } from '@/lib/storage/DatabaseManager'; import { SyncManager } from '@/lib/storage/SyncManager'; -import { FilePresenceManager } from '@/lib/explorer/FilePresence'; import { Settings, X } from 'lucide-react'; +import dynamic from 'next/dynamic'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +const CodeEditor = dynamic(() => import('@/components/playground/CodeEditor').then((mod) => mod.CodeEditor), { + ssr: false, +}); const INITIAL_TREE: FileTreeNode[] = [ { @@ -113,6 +113,12 @@ export default function PlaygroundPage() { const [pendingCount, setPendingCount] = useState(0); const [activeTab, setActiveTab] = useState<'editor' | 'output'>('editor'); + // Track the live editor code so the audit panel can analyse it + const [editorCode, setEditorCode] = useState(''); + const { result: auditResult, isPending: auditPending, runAudit } = useAccessibilityAudit(editorCode, { + debounceMs: 500, + }); + useEffect(() => { const timer = setTimeout(() => setIsInitializing(false), 1500); return () => clearTimeout(timer); @@ -355,6 +361,7 @@ export default function PlaygroundPage() { roomName="main-lab-session" collaborationProvider={provider} settings={editorSettings} + onCodeChange={setEditorCode} /> @@ -379,6 +386,12 @@ export default function PlaygroundPage() { + +

Laboratory Notes diff --git a/frontend/src/components/playground/AccessibilityAuditPanel.tsx b/frontend/src/components/playground/AccessibilityAuditPanel.tsx new file mode 100644 index 00000000..ec474e86 --- /dev/null +++ b/frontend/src/components/playground/AccessibilityAuditPanel.tsx @@ -0,0 +1,369 @@ +'use client'; + +import type { AuditIssue, AuditResult, AuditSeverity } from '@/lib/editor/SorobanAccessibilityAuditor'; +import { AlertCircle, AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Info, RefreshCw } from 'lucide-react'; +import React, { useId, useState } from 'react'; + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +interface SeverityBadgeProps { + severity: AuditSeverity; + count: number; +} + +function SeverityBadge({ severity, count }: SeverityBadgeProps) { + const styles: Record = { + error: 'bg-red-900/40 text-red-400 border-red-700/40', + warning: 'bg-amber-900/40 text-amber-400 border-amber-700/40', + info: 'bg-blue-900/40 text-blue-400 border-blue-700/40', + }; + + const label: Record = { + error: 'ERR', + warning: 'WARN', + info: 'INFO', + }; + + return ( + + {label[severity]} {count} + + ); +} + +interface IssueRowProps { + issue: AuditIssue; + defaultExpanded?: boolean; +} + +function IssueRow({ issue, defaultExpanded = false }: IssueRowProps) { + const [expanded, setExpanded] = useState(defaultExpanded); + const detailId = useId(); + + const iconMap: Record = { + error: