@fragments-sdk/cli 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/bin.js +996 -79
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
  4. package/dist/chunk-6JBGU74P.js.map +1 -0
  5. package/dist/chunk-7OPWMLOE.js +1625 -0
  6. package/dist/chunk-7OPWMLOE.js.map +1 -0
  7. package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
  8. package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
  9. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
  12. package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
  13. package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
  14. package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
  15. package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +15 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
  20. package/dist/mcp-bin.js +8 -220
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-WY23TJCP.js +12 -0
  23. package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
  24. package/dist/static-viewer-GBR7YNF3.js +12 -0
  25. package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
  26. package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
  27. package/dist/viewer-SUFOISZM.js +1822 -0
  28. package/dist/viewer-SUFOISZM.js.map +1 -0
  29. package/package.json +6 -5
  30. package/src/bin.ts +31 -0
  31. package/src/build.ts +147 -13
  32. package/src/cli-commands.ts +18 -0
  33. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  34. package/src/commands/a11y-report.ts +625 -0
  35. package/src/commands/a11y.ts +168 -14
  36. package/src/commands/build.ts +16 -0
  37. package/src/commands/graph.ts +274 -0
  38. package/src/core/auto-props.ts +464 -0
  39. package/src/core/composition.ts +64 -1
  40. package/src/core/graph-extractor.test.ts +542 -0
  41. package/src/core/graph-extractor.ts +601 -0
  42. package/src/core/importAnalyzer.ts +5 -0
  43. package/src/core/schema.ts +2 -0
  44. package/src/core/types.ts +3 -1
  45. package/src/index.ts +4 -0
  46. package/src/mcp/server.ts +13 -220
  47. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  48. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  49. package/src/theme/contrast.test.ts +331 -0
  50. package/src/theme/contrast.ts +246 -0
  51. package/src/theme/generator.ts +213 -1
  52. package/src/theme/index.ts +16 -0
  53. package/src/theme/types.ts +51 -0
  54. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  55. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  56. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  57. package/src/viewer/components/ActionCapture.tsx +1 -1
  58. package/src/viewer/components/ActionsPanel.tsx +142 -183
  59. package/src/viewer/components/App.tsx +276 -183
  60. package/src/viewer/components/BottomPanel.tsx +40 -80
  61. package/src/viewer/components/CodePanel.tsx +9 -87
  62. package/src/viewer/components/CommandPalette.tsx +117 -74
  63. package/src/viewer/components/ComponentGraph.tsx +143 -126
  64. package/src/viewer/components/ComponentHeader.tsx +46 -43
  65. package/src/viewer/components/ContractPanel.tsx +124 -117
  66. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  67. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  68. package/src/viewer/components/FragmentEditor.tsx +126 -63
  69. package/src/viewer/components/HealthDashboard.tsx +146 -171
  70. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  71. package/src/viewer/components/Icons.tsx +151 -98
  72. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  73. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  74. package/src/viewer/components/IsolatedRender.tsx +12 -6
  75. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  76. package/src/viewer/components/LandingPage.tsx +285 -305
  77. package/src/viewer/components/Layout.tsx +12 -10
  78. package/src/viewer/components/LeftSidebar.tsx +103 -155
  79. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  80. package/src/viewer/components/PreviewArea.tsx +113 -44
  81. package/src/viewer/components/PreviewFrameHost.tsx +36 -6
  82. package/src/viewer/components/PreviewPane.tsx +2 -3
  83. package/src/viewer/components/PreviewToolbar.tsx +109 -105
  84. package/src/viewer/components/PropsEditor.tsx +154 -74
  85. package/src/viewer/components/PropsTable.tsx +95 -82
  86. package/src/viewer/components/RelationsSection.tsx +71 -40
  87. package/src/viewer/components/ResizablePanel.tsx +158 -55
  88. package/src/viewer/components/RightSidebar.tsx +46 -56
  89. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  90. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  91. package/src/viewer/components/StoryRenderer.tsx +4 -11
  92. package/src/viewer/components/Toast.tsx +3 -67
  93. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  94. package/src/viewer/components/UsageSection.tsx +26 -26
  95. package/src/viewer/components/VariantMatrix.tsx +140 -47
  96. package/src/viewer/components/VariantTabs.tsx +24 -68
  97. package/src/viewer/components/ViewportSelector.tsx +121 -114
  98. package/src/viewer/constants/ui.ts +23 -22
  99. package/src/viewer/entry.tsx +8 -3
  100. package/src/viewer/index.ts +3 -6
  101. package/src/viewer/preview-frame.html +43 -18
  102. package/src/viewer/server.ts +7 -16
  103. package/src/viewer/styles/globals.css +46 -85
  104. package/src/viewer/utils/a11y-fixes.ts +53 -30
  105. package/dist/chunk-ICAIQ57V.js.map +0 -1
  106. package/dist/chunk-U4GQ2JTD.js +0 -832
  107. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  108. package/dist/scan-ESEXV7LF.js +0 -12
  109. package/dist/static-viewer-O37MJ5B6.js +0 -12
  110. package/dist/viewer-YDGFDTK5.js +0 -11104
  111. package/dist/viewer-YDGFDTK5.js.map +0 -1
  112. package/src/viewer/postcss.config.js +0 -6
  113. package/src/viewer/tailwind.config.js +0 -37
  114. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  115. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  116. /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
  117. /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
  118. /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
  119. /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
  120. /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
  121. /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
  122. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  123. /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
  124. /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { useState, useEffect, useCallback, useMemo, useRef } from "react";
13
13
  import type { Result, NodeResult, ImpactValue, RunOptions } from "axe-core";
14
- import clsx from "clsx";
14
+ import { Badge, Tabs, Dialog, Card, Alert, Text, Stack, Box, Button, Chip, EmptyState } from "@fragments/ui";
15
15
  import { BRAND } from "../../core/index.js";
16
16
  import {
17
17
  updateComponentA11yResult,
@@ -84,6 +84,13 @@ const IMPACT_ORDER: Record<string, number> = {
84
84
  minor: 3,
85
85
  };
86
86
 
87
+ // WCAG level color mapping
88
+ const WCAG_LEVEL_COLORS: Record<string, { bg: string; color: string }> = {
89
+ A: { bg: 'var(--color-success-bg)', color: 'var(--color-success)' },
90
+ AA: { bg: 'var(--color-info-bg)', color: 'var(--color-info)' },
91
+ AAA: { bg: 'var(--color-accent-subtle)', color: 'var(--color-accent)' },
92
+ };
93
+
87
94
  // Cache the axe-core module to avoid repeated dynamic imports
88
95
  let axeModule: typeof import("axe-core") | null = null;
89
96
 
@@ -574,112 +581,102 @@ export function AccessibilityPanel({
574
581
  // Render loading state
575
582
  if (isScanning && !results) {
576
583
  return (
577
- <div className="flex flex-col items-center justify-center p-8 text-tertiary">
578
- <LoadingIcon className="w-6 h-6 animate-spin mb-2" />
579
- <span className="text-xs">Running accessibility checks...</span>
580
- </div>
584
+ <Stack align="center" justify="center" style={{ padding: 32 }}>
585
+ <LoadingIcon style={{ width: 24, height: 24, animation: 'spin 1s linear infinite', marginBottom: 8 }} />
586
+ <Text size="xs" color="tertiary">Running accessibility checks...</Text>
587
+ </Stack>
581
588
  );
582
589
  }
583
590
 
584
591
  // Render error state
585
592
  if (error) {
586
593
  return (
587
- <div className="flex flex-col items-center justify-center p-8">
588
- <XIcon className="w-6 h-6 text-red-500 mb-2" />
589
- <span className="text-xs text-red-600 mb-2">{error}</span>
590
- <button
591
- onClick={runScan}
592
- className="text-xs text-[--color-accent] hover:underline"
593
- >
594
- Retry Scan
595
- </button>
596
- </div>
594
+ <Stack align="center" justify="center" style={{ padding: 32 }}>
595
+ <XIcon style={{ width: 24, height: 24, color: 'var(--color-danger)', marginBottom: 8 }} />
596
+ <Text size="xs" color="secondary" style={{ color: 'var(--color-danger)', marginBottom: 8 }}>{error}</Text>
597
+ <Button variant="ghost" size="sm" onClick={runScan}>Retry Scan</Button>
598
+ </Stack>
597
599
  );
598
600
  }
599
601
 
600
602
  // Render no results state
601
603
  if (!results) {
602
604
  return (
603
- <div className="flex flex-col items-center justify-center p-8 text-tertiary">
604
- <AccessibilityIcon className="w-8 h-8 mb-2 opacity-50" />
605
- <span className="text-xs mb-2">No accessibility scan results</span>
606
- <button
607
- onClick={runScan}
608
- className="text-xs px-3 py-1 rounded bg-[--color-accent] text-white hover:opacity-90"
609
- >
610
- Run Scan
611
- </button>
612
- </div>
605
+ <Stack align="center" justify="center" style={{ padding: 32 }}>
606
+ <AccessibilityIcon style={{ width: 32, height: 32, marginBottom: 8, opacity: 0.5, color: 'var(--text-tertiary)' }} />
607
+ <Text size="xs" color="tertiary" style={{ marginBottom: 8 }}>No accessibility scan results</Text>
608
+ <Button size="sm" onClick={runScan}>Run Scan</Button>
609
+ </Stack>
613
610
  );
614
611
  }
615
612
 
616
613
  return (
617
- <div className="flex flex-col h-full overflow-hidden">
618
- {/* Tabs */}
619
- <div className="flex items-center gap-1 px-4 py-2 border-b border-[--border] bg-[--bg-secondary] flex-shrink-0">
620
- <TabButton
621
- active={activeTab === "violations"}
622
- onClick={() => setActiveTab("violations")}
623
- count={counts.violations}
624
- label="Violations"
625
- variant="error"
626
- />
627
- <TabButton
628
- active={activeTab === "passes"}
629
- onClick={() => setActiveTab("passes")}
630
- count={counts.passes}
631
- label="Passes"
632
- variant="success"
633
- />
634
- <TabButton
635
- active={activeTab === "incomplete"}
636
- onClick={() => setActiveTab("incomplete")}
637
- count={counts.incomplete}
638
- label="Incomplete"
639
- variant="warning"
640
- />
641
-
642
- <div className="flex-1" />
614
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
615
+ {/* Tabs header with actions */}
616
+ <Stack direction="row" align="center" gap="xs" style={{
617
+ padding: '8px 16px',
618
+ borderBottom: '1px solid var(--border)',
619
+ background: 'var(--bg-secondary)',
620
+ flexShrink: 0,
621
+ }}>
622
+ <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)}>
623
+ <Tabs.List>
624
+ <Tabs.Tab value="violations">
625
+ <span style={{ color: 'var(--color-danger)' }}>{counts.violations}</span> Violations
626
+ </Tabs.Tab>
627
+ <Tabs.Tab value="passes">
628
+ <span style={{ color: 'var(--color-success)' }}>{counts.passes}</span> Passes
629
+ </Tabs.Tab>
630
+ <Tabs.Tab value="incomplete">
631
+ <span style={{ color: 'var(--color-warning)' }}>{counts.incomplete}</span> Incomplete
632
+ </Tabs.Tab>
633
+ </Tabs.List>
634
+ </Tabs>
635
+
636
+ <div style={{ flex: 1 }} />
643
637
 
644
638
  {/* Re-scanning indicator */}
645
639
  {isReScanning && (
646
- <span className="flex items-center gap-1 px-2 py-1 text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 rounded animate-pulse">
647
- <LoadingIcon className="w-3 h-3 animate-spin" />
648
- Re-scanning...
649
- </span>
640
+ <Badge variant="warning" size="sm">
641
+ <Stack direction="row" align="center" gap="xs">
642
+ <LoadingIcon style={{ width: 12, height: 12, animation: 'spin 1s linear infinite' }} />
643
+ Re-scanning...
644
+ </Stack>
645
+ </Badge>
650
646
  )}
651
647
 
652
648
  {/* Rescan button */}
653
- <button
649
+ <Button
650
+ variant="ghost"
651
+ size="sm"
654
652
  onClick={runScan}
655
653
  disabled={isScanning}
656
- className="flex items-center gap-1 px-2 py-1 text-xs text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded disabled:opacity-50"
657
654
  title="Re-run accessibility scan"
658
655
  >
659
656
  <LoadingIcon
660
- className={clsx("w-3 h-3", isScanning && "animate-spin")}
657
+ style={{
658
+ width: 12,
659
+ height: 12,
660
+ ...(isScanning ? { animation: 'spin 1s linear infinite' } : {}),
661
+ }}
661
662
  />
662
663
  {isScanning ? "Scanning..." : "Rescan"}
663
- </button>
664
+ </Button>
664
665
 
665
666
  {/* AI Setup button */}
666
- <button
667
+ <Button
668
+ variant={aiConfig ? "secondary" : "ghost"}
669
+ size="sm"
667
670
  onClick={() => setShowAISetup(true)}
668
- className={clsx(
669
- "flex items-center gap-1 px-2 py-1 text-xs rounded",
670
- aiConfig
671
- ? "text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-950/30"
672
- : "text-tertiary hover:text-primary hover:bg-[--bg-hover]"
673
- )}
674
671
  title={aiConfig ? "AI fixes enabled" : "Configure AI for fix suggestions"}
675
672
  >
676
- <WandIcon className="w-3 h-3" />
673
+ <WandIcon style={{ width: 12, height: 12 }} />
677
674
  {aiConfig ? "AI Ready" : "Setup AI"}
678
- </button>
679
- </div>
675
+ </Button>
676
+ </Stack>
680
677
 
681
678
  {/* Content */}
682
- <div className="flex-1 overflow-y-auto p-4">
679
+ <div style={{ flex: 1, overflowY: 'auto', padding: 16 }}>
683
680
  {activeTab === "violations" && (
684
681
  <>
685
682
  {counts.violations === 0 ? (
@@ -738,51 +735,22 @@ export function AccessibilityPanel({
738
735
  )}
739
736
  </div>
740
737
 
741
- {/* AI Setup Modal */}
742
- {showAISetup && (
743
- <AISetupModal
744
- onSave={saveAIConfig}
745
- onClose={() => setShowAISetup(false)}
746
- currentConfig={aiConfig}
747
- />
748
- )}
738
+ {/* AI Setup Dialog */}
739
+ <Dialog open={showAISetup} onOpenChange={(open) => !open && setShowAISetup(false)}>
740
+ <Dialog.Content>
741
+ <AISetupModalContent
742
+ onSave={saveAIConfig}
743
+ onClose={() => setShowAISetup(false)}
744
+ currentConfig={aiConfig}
745
+ />
746
+ </Dialog.Content>
747
+ </Dialog>
749
748
  </div>
750
749
  );
751
750
  }
752
751
 
753
752
  // ----- Sub-components -----
754
753
 
755
- interface TabButtonProps {
756
- active: boolean;
757
- onClick: () => void;
758
- count: number;
759
- label: string;
760
- variant: "error" | "success" | "warning";
761
- }
762
-
763
- function TabButton({ active, onClick, count, label, variant }: TabButtonProps) {
764
- const variantStyles = {
765
- error: "text-red-600 dark:text-red-400",
766
- success: "text-green-600 dark:text-green-400",
767
- warning: "text-amber-600 dark:text-amber-400",
768
- };
769
-
770
- return (
771
- <button
772
- onClick={onClick}
773
- className={clsx(
774
- "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded transition-colors",
775
- active
776
- ? "text-primary bg-[--bg-hover]"
777
- : "text-tertiary hover:text-secondary"
778
- )}
779
- >
780
- <span className={variantStyles[variant]}>{count}</span>
781
- <span>{label}</span>
782
- </button>
783
- );
784
- }
785
-
786
754
  interface EnhancedRuleListProps {
787
755
  rules: Result[];
788
756
  expandedRules: Set<string>;
@@ -812,7 +780,7 @@ function EnhancedRuleList({
812
780
  copiedFix,
813
781
  }: EnhancedRuleListProps) {
814
782
  return (
815
- <div className="space-y-3">
783
+ <Stack gap="md">
816
784
  {rules.map((rule) => {
817
785
  const isExpanded = expandedRules.has(rule.id);
818
786
  const impactColors = getImpactColorClass(rule.impact || null);
@@ -820,140 +788,170 @@ function EnhancedRuleList({
820
788
  const wcagTags = extractWcagTags(rule.tags);
821
789
 
822
790
  return (
823
- <div
824
- key={rule.id}
825
- className={clsx(
826
- "rounded-lg border border-[--border] overflow-hidden border-l-4",
827
- impactColors.borderLeft
828
- )}
829
- >
791
+ <Card key={rule.id} style={{ borderLeft: `4px solid ${impactColors.borderLeft}` }}>
830
792
  {/* Rule Header */}
831
793
  <button
832
794
  onClick={() => onToggleRule(rule.id)}
833
- className="w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-[--bg-hover] transition-colors"
795
+ style={{
796
+ width: '100%',
797
+ display: 'flex',
798
+ alignItems: 'center',
799
+ gap: 8,
800
+ padding: '10px 12px',
801
+ textAlign: 'left',
802
+ background: 'none',
803
+ border: 'none',
804
+ cursor: 'pointer',
805
+ transition: 'background var(--transition-fast)',
806
+ }}
834
807
  >
835
808
  {isExpanded ? (
836
- <ChevronDownIcon className="w-4 h-4 text-tertiary flex-shrink-0" />
809
+ <ChevronDownIcon style={{ width: 16, height: 16, color: 'var(--text-tertiary)', flexShrink: 0 }} />
837
810
  ) : (
838
- <ChevronRightIcon className="w-4 h-4 text-tertiary flex-shrink-0" />
811
+ <ChevronRightIcon style={{ width: 16, height: 16, color: 'var(--text-tertiary)', flexShrink: 0 }} />
839
812
  )}
840
813
 
841
814
  {/* Impact badge */}
842
815
  {rule.impact && (
843
- <span
844
- className={clsx(
845
- "text-[10px] font-semibold px-1.5 py-0.5 rounded uppercase tracking-wide",
846
- impactColors.bg,
847
- impactColors.text
848
- )}
849
- >
816
+ <Badge variant={impactColors.variant} size="sm">
850
817
  {rule.impact}
851
- </span>
818
+ </Badge>
852
819
  )}
853
820
 
854
821
  {/* Rule ID */}
855
- <span className="text-xs font-mono text-tertiary">
856
- {rule.id}
857
- </span>
822
+ <Text font="mono" size="xs" color="tertiary">{rule.id}</Text>
858
823
 
859
824
  {/* Rule description */}
860
- <span className="text-xs text-primary flex-1 truncate">
825
+ <Text size="xs" style={{
826
+ flex: 1,
827
+ overflow: 'hidden',
828
+ textOverflow: 'ellipsis',
829
+ whiteSpace: 'nowrap',
830
+ }}>
861
831
  {rule.description}
862
- </span>
832
+ </Text>
863
833
 
864
834
  {/* Element count */}
865
- <span className="text-[10px] text-tertiary flex-shrink-0 bg-[--bg-hover] px-1.5 py-0.5 rounded">
835
+ <Badge size="sm">
866
836
  {rule.nodes.length} element{rule.nodes.length !== 1 ? "s" : ""}
867
- </span>
837
+ </Badge>
868
838
  </button>
869
839
 
870
840
  {/* Expanded content */}
871
841
  {isExpanded && (
872
- <div className="border-t border-[--border] bg-[--bg-secondary]">
842
+ <div style={{ borderTop: '1px solid var(--border)', background: 'var(--bg-secondary)' }}>
873
843
  {/* Why it matters + WCAG reference */}
874
844
  {staticFix && (
875
- <div className="px-3 py-3 border-b border-[--border-subtle] space-y-2">
876
- <div>
877
- <p className="text-[10px] font-semibold text-tertiary uppercase tracking-wide mb-1">
878
- Why it matters
879
- </p>
880
- <p className="text-xs text-secondary leading-relaxed">
881
- {staticFix.whyItMatters}
882
- </p>
883
- </div>
884
-
885
- {/* WCAG criterion */}
886
- {staticFix.wcagCriterion && (
887
- <div className="flex items-center gap-2">
888
- <span className={clsx(
889
- "text-[10px] px-1.5 py-0.5 rounded font-medium",
890
- staticFix.wcagCriterion.level === 'A' && "bg-green-100 dark:bg-green-950/50 text-green-700 dark:text-green-300",
891
- staticFix.wcagCriterion.level === 'AA' && "bg-blue-100 dark:bg-blue-950/50 text-blue-700 dark:text-blue-300",
892
- staticFix.wcagCriterion.level === 'AAA' && "bg-purple-100 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300",
893
- )}>
894
- WCAG {staticFix.wcagCriterion.id} Level {staticFix.wcagCriterion.level}
895
- </span>
896
- <span className="text-xs text-secondary">
897
- {staticFix.wcagCriterion.name}
898
- </span>
899
- <a
900
- href={staticFix.wcagCriterion.url}
901
- target="_blank"
902
- rel="noopener noreferrer"
903
- className="text-xs text-[--color-accent] hover:underline ml-auto"
904
- >
905
- Learn more
906
- </a>
907
- </div>
908
- )}
845
+ <Box padding="sm" style={{ borderBottom: '1px solid var(--border-subtle)' }}>
846
+ <Stack gap="sm">
847
+ <Alert severity="info">
848
+ <Alert.Body>
849
+ <Alert.Title>Why it matters</Alert.Title>
850
+ <Alert.Content>
851
+ <Text size="xs">{staticFix.whyItMatters}</Text>
852
+ </Alert.Content>
853
+ </Alert.Body>
854
+ </Alert>
855
+
856
+ {/* WCAG criterion */}
857
+ {staticFix.wcagCriterion && (
858
+ <Stack direction="row" align="center" gap="sm">
859
+ <Badge size="sm" style={{
860
+ background: WCAG_LEVEL_COLORS[staticFix.wcagCriterion.level]?.bg || 'var(--bg-hover)',
861
+ color: WCAG_LEVEL_COLORS[staticFix.wcagCriterion.level]?.color || 'var(--text-secondary)',
862
+ }}>
863
+ WCAG {staticFix.wcagCriterion.id} Level {staticFix.wcagCriterion.level}
864
+ </Badge>
865
+ <Text size="xs" color="secondary">{staticFix.wcagCriterion.name}</Text>
866
+ <a
867
+ href={staticFix.wcagCriterion.url}
868
+ target="_blank"
869
+ rel="noopener noreferrer"
870
+ style={{
871
+ fontSize: 12,
872
+ color: 'var(--color-accent)',
873
+ marginLeft: 'auto',
874
+ textDecoration: 'none',
875
+ }}
876
+ >
877
+ Learn more
878
+ </a>
879
+ </Stack>
880
+ )}
909
881
 
910
- {/* Before/After examples */}
911
- {(staticFix.badExample || staticFix.goodExample) && (
912
- <div className="grid grid-cols-2 gap-2 mt-2">
913
- {staticFix.badExample && (
914
- <div>
915
- <p className="text-[10px] font-medium text-red-600 dark:text-red-400 mb-1">
916
- Before
917
- </p>
918
- <pre className="text-[10px] font-mono p-2 rounded bg-red-50 dark:bg-red-950/30 text-red-800 dark:text-red-200 overflow-x-auto whitespace-pre-wrap">
919
- {staticFix.badExample}
920
- </pre>
921
- </div>
922
- )}
923
- {staticFix.goodExample && (
924
- <div>
925
- <p className="text-[10px] font-medium text-green-600 dark:text-green-400 mb-1">
926
- After
927
- </p>
928
- <pre className="text-[10px] font-mono p-2 rounded bg-green-50 dark:bg-green-950/30 text-green-800 dark:text-green-200 overflow-x-auto whitespace-pre-wrap">
929
- {staticFix.goodExample}
930
- </pre>
931
- </div>
932
- )}
933
- </div>
934
- )}
935
- </div>
882
+ {/* Before/After examples */}
883
+ {(staticFix.badExample || staticFix.goodExample) && (
884
+ <div style={{
885
+ display: 'grid',
886
+ gridTemplateColumns: '1fr 1fr',
887
+ gap: 8,
888
+ marginTop: 8,
889
+ }}>
890
+ {staticFix.badExample && (
891
+ <div>
892
+ <Text size="xs" weight="medium" style={{ color: 'var(--color-danger)', marginBottom: 4 }}>
893
+ Before
894
+ </Text>
895
+ <pre style={{
896
+ fontSize: 10,
897
+ fontFamily: 'monospace',
898
+ padding: 8,
899
+ borderRadius: 'var(--radius-sm)',
900
+ background: 'var(--color-danger-bg)',
901
+ color: 'var(--color-danger)',
902
+ overflowX: 'auto',
903
+ whiteSpace: 'pre-wrap',
904
+ margin: 0,
905
+ }}>
906
+ {staticFix.badExample}
907
+ </pre>
908
+ </div>
909
+ )}
910
+ {staticFix.goodExample && (
911
+ <div>
912
+ <Text size="xs" weight="medium" style={{ color: 'var(--color-success)', marginBottom: 4 }}>
913
+ After
914
+ </Text>
915
+ <pre style={{
916
+ fontSize: 10,
917
+ fontFamily: 'monospace',
918
+ padding: 8,
919
+ borderRadius: 'var(--radius-sm)',
920
+ background: 'var(--color-success-bg)',
921
+ color: 'var(--color-success)',
922
+ overflowX: 'auto',
923
+ whiteSpace: 'pre-wrap',
924
+ margin: 0,
925
+ }}>
926
+ {staticFix.goodExample}
927
+ </pre>
928
+ </div>
929
+ )}
930
+ </div>
931
+ )}
932
+ </Stack>
933
+ </Box>
936
934
  )}
937
935
 
938
936
  {/* Help info (fallback if no static fix) */}
939
937
  {!staticFix && (
940
- <div className="px-3 py-2 border-b border-[--border-subtle]">
941
- <p className="text-xs text-secondary mb-1">{rule.help}</p>
938
+ <Box padding="sm" style={{ borderBottom: '1px solid var(--border-subtle)' }}>
939
+ <Text size="xs" color="secondary" style={{ marginBottom: 4 }}>{rule.help}</Text>
942
940
  {rule.helpUrl && (
943
941
  <a
944
942
  href={rule.helpUrl}
945
943
  target="_blank"
946
944
  rel="noopener noreferrer"
947
- className="text-xs text-[--color-accent] hover:underline"
945
+ style={{ fontSize: 12, color: 'var(--color-accent)', textDecoration: 'none' }}
948
946
  >
949
947
  Learn more about {rule.id}
950
948
  </a>
951
949
  )}
952
- </div>
950
+ </Box>
953
951
  )}
954
952
 
955
953
  {/* Affected elements */}
956
- <div className="divide-y divide-[--border-subtle]">
954
+ <div>
957
955
  {rule.nodes.map((node, i) => {
958
956
  const fixKey = `${rule.id}-${node.html}`;
959
957
  const isHighlighted =
@@ -961,135 +959,171 @@ function EnhancedRuleList({
961
959
  const elementFix = generateElementFix(rule.id, node as unknown as SerializedNode);
962
960
 
963
961
  return (
964
- <div
962
+ <Box
965
963
  key={i}
966
- className={clsx(
967
- "px-3 py-3",
968
- isHighlighted && "bg-red-50 dark:bg-red-950/30"
969
- )}
964
+ padding="sm"
965
+ style={{
966
+ borderTop: i > 0 ? '1px solid var(--border-subtle)' : undefined,
967
+ background: isHighlighted ? 'var(--color-danger-bg)' : undefined,
968
+ }}
970
969
  >
971
970
  {/* Element selector + actions */}
972
- <div className="flex items-center gap-2 mb-2">
973
- <button
971
+ <Stack direction="row" align="center" gap="sm" style={{ marginBottom: 8 }}>
972
+ <Button
973
+ variant={isHighlighted ? "primary" : "ghost"}
974
+ size="sm"
974
975
  onClick={() =>
975
976
  onHighlight(
976
977
  isHighlighted ? null : node.target.join(", ")
977
978
  )
978
979
  }
979
- className={clsx(
980
- "text-[10px] font-mono px-2 py-1 rounded truncate max-w-[200px] transition-colors",
981
- isHighlighted
982
- ? "bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200"
983
- : "bg-[--bg-hover] text-secondary hover:text-primary hover:bg-[--bg-tertiary]"
984
- )}
985
980
  title={node.target.join(" > ")}
981
+ style={{
982
+ fontFamily: 'monospace',
983
+ fontSize: 10,
984
+ maxWidth: 200,
985
+ overflow: 'hidden',
986
+ textOverflow: 'ellipsis',
987
+ whiteSpace: 'nowrap',
988
+ }}
986
989
  >
987
990
  {isHighlighted ? "Hide" : "Highlight"}: {node.target[node.target.length - 1]}
988
- </button>
991
+ </Button>
989
992
 
990
993
  {aiEnabled && onGenerateFix && (
991
- <button
994
+ <Button
995
+ variant="ghost"
996
+ size="sm"
992
997
  onClick={() => onGenerateFix(rule, node)}
993
998
  disabled={generatingFix === fixKey}
994
- className={clsx(
995
- "flex items-center gap-1 px-2 py-1 text-[10px] rounded transition-colors",
996
- generatingFix === fixKey
997
- ? "bg-purple-100 dark:bg-purple-950/50 text-purple-600 dark:text-purple-400"
998
- : "bg-[--bg-hover] text-tertiary hover:text-primary"
999
- )}
1000
999
  title="Generate AI fix suggestion"
1001
1000
  >
1002
1001
  <WandIcon
1003
- className={clsx(
1004
- "w-3 h-3",
1005
- generatingFix === fixKey && "animate-pulse"
1006
- )}
1002
+ style={{
1003
+ width: 12,
1004
+ height: 12,
1005
+ ...(generatingFix === fixKey ? { animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite' } : {}),
1006
+ }}
1007
1007
  />
1008
1008
  {generatingFix === fixKey ? "Generating..." : "AI Fix"}
1009
- </button>
1009
+ </Button>
1010
1010
  )}
1011
- </div>
1011
+ </Stack>
1012
1012
 
1013
1013
  {/* HTML snippet */}
1014
- <pre className="text-[10px] font-mono text-tertiary bg-[--bg-primary] rounded p-2 overflow-x-auto whitespace-pre-wrap border border-[--border-subtle]">
1014
+ <pre style={{
1015
+ fontSize: 10,
1016
+ fontFamily: 'monospace',
1017
+ color: 'var(--text-tertiary)',
1018
+ background: 'var(--bg-primary)',
1019
+ borderRadius: 'var(--radius-sm)',
1020
+ padding: 8,
1021
+ overflowX: 'auto',
1022
+ whiteSpace: 'pre-wrap',
1023
+ border: '1px solid var(--border-subtle)',
1024
+ margin: 0,
1025
+ }}>
1015
1026
  {node.html}
1016
1027
  </pre>
1017
1028
 
1018
1029
  {/* Failure summary */}
1019
1030
  {node.failureSummary && (
1020
- <p className="text-xs text-secondary mt-2 leading-relaxed">
1031
+ <Text size="xs" color="secondary" style={{ marginTop: 8, lineHeight: 1.6 }}>
1021
1032
  {node.failureSummary}
1022
- </p>
1033
+ </Text>
1023
1034
  )}
1024
1035
 
1025
1036
  {/* Static element fix */}
1026
1037
  {elementFix && (
1027
- <div className="mt-2 p-2 rounded bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800">
1028
- <div className="flex items-center justify-between mb-1">
1029
- <span className="text-[10px] font-medium text-blue-700 dark:text-blue-300">
1030
- Suggested Fix
1031
- </span>
1032
- <button
1033
- onClick={() => onCopyFix(elementFix.fixedHtml, fixKey)}
1034
- className="text-[10px] text-blue-600 dark:text-blue-400 hover:underline"
1035
- >
1036
- {copiedFix === fixKey ? "Copied!" : "Copy"}
1037
- </button>
1038
- </div>
1039
- <p className="text-[10px] text-blue-800 dark:text-blue-200 mb-2">
1040
- {elementFix.explanation}
1041
- </p>
1042
- <pre className="text-[10px] font-mono text-blue-800 dark:text-blue-200 bg-blue-100 dark:bg-blue-900/50 rounded p-2 overflow-x-auto whitespace-pre-wrap">
1043
- {elementFix.fixedHtml}
1044
- </pre>
1045
- </div>
1038
+ <Alert severity="info" style={{ marginTop: 8 }}>
1039
+ <Alert.Body>
1040
+ <Stack direction="row" align="center" justify="between" style={{ marginBottom: 4 }}>
1041
+ <Text size="xs" weight="medium" style={{ color: 'var(--color-info)' }}>
1042
+ Suggested Fix
1043
+ </Text>
1044
+ <Button
1045
+ variant="ghost"
1046
+ size="sm"
1047
+ onClick={() => onCopyFix(elementFix.fixedHtml, fixKey)}
1048
+ style={{ fontSize: 10, color: 'var(--color-info)' }}
1049
+ >
1050
+ {copiedFix === fixKey ? "Copied!" : "Copy"}
1051
+ </Button>
1052
+ </Stack>
1053
+ <Alert.Content>
1054
+ <Text size="xs" style={{ color: 'var(--color-info)', marginBottom: 8 }}>
1055
+ {elementFix.explanation}
1056
+ </Text>
1057
+ <pre style={{
1058
+ fontSize: 10,
1059
+ fontFamily: 'monospace',
1060
+ color: 'var(--color-info)',
1061
+ background: 'var(--bg-secondary)',
1062
+ borderRadius: 'var(--radius-sm)',
1063
+ padding: 8,
1064
+ overflowX: 'auto',
1065
+ whiteSpace: 'pre-wrap',
1066
+ margin: 0,
1067
+ }}>
1068
+ {elementFix.fixedHtml}
1069
+ </pre>
1070
+ </Alert.Content>
1071
+ </Alert.Body>
1072
+ </Alert>
1046
1073
  )}
1047
1074
 
1048
1075
  {/* AI fix suggestion */}
1049
1076
  {aiFixes?.[fixKey] && (
1050
- <div className="mt-2 p-2 rounded bg-purple-50 dark:bg-purple-950/30 border border-purple-200 dark:border-purple-800">
1051
- <div className="flex items-center gap-1 mb-1">
1052
- <WandIcon className="w-3 h-3 text-purple-600 dark:text-purple-400" />
1053
- <span className="text-[10px] font-medium text-purple-700 dark:text-purple-300">
1077
+ <div style={{
1078
+ marginTop: 8,
1079
+ padding: 8,
1080
+ borderRadius: 'var(--radius-sm)',
1081
+ background: 'var(--color-accent-subtle)',
1082
+ border: '1px solid var(--color-accent)',
1083
+ }}>
1084
+ <Stack direction="row" align="center" gap="xs" style={{ marginBottom: 4 }}>
1085
+ <WandIcon style={{ width: 12, height: 12, color: 'var(--color-accent)' }} />
1086
+ <Text size="xs" weight="medium" style={{ color: 'var(--color-accent)' }}>
1054
1087
  AI Fix Suggestion
1055
- </span>
1056
- </div>
1057
- <pre className="text-[10px] font-mono text-purple-800 dark:text-purple-200 whitespace-pre-wrap">
1088
+ </Text>
1089
+ </Stack>
1090
+ <pre style={{
1091
+ fontSize: 10,
1092
+ fontFamily: 'monospace',
1093
+ color: 'var(--color-accent)',
1094
+ whiteSpace: 'pre-wrap',
1095
+ margin: 0,
1096
+ }}>
1058
1097
  {aiFixes[fixKey]}
1059
1098
  </pre>
1060
1099
  </div>
1061
1100
  )}
1062
- </div>
1101
+ </Box>
1063
1102
  );
1064
1103
  })}
1065
1104
  </div>
1066
1105
 
1067
1106
  {/* Tags */}
1068
1107
  {rule.tags && rule.tags.length > 0 && (
1069
- <div className="px-3 py-2 border-t border-[--border-subtle]">
1070
- <div className="flex flex-wrap gap-1">
1108
+ <Box padding="sm" style={{ borderTop: '1px solid var(--border-subtle)' }}>
1109
+ <Stack direction="row" gap="xs" style={{ flexWrap: 'wrap' }}>
1071
1110
  {rule.tags.slice(0, 8).map((tag) => (
1072
- <span
1073
- key={tag}
1074
- className="text-[10px] px-1.5 py-0.5 rounded bg-[--bg-hover] text-tertiary"
1075
- >
1076
- {tag}
1077
- </span>
1111
+ <Chip key={tag} size="sm">{tag}</Chip>
1078
1112
  ))}
1079
1113
  {rule.tags.length > 8 && (
1080
- <span className="text-[10px] text-tertiary">
1114
+ <Text size="xs" color="tertiary">
1081
1115
  +{rule.tags.length - 8} more
1082
- </span>
1116
+ </Text>
1083
1117
  )}
1084
- </div>
1085
- </div>
1118
+ </Stack>
1119
+ </Box>
1086
1120
  )}
1087
1121
  </div>
1088
1122
  )}
1089
- </div>
1123
+ </Card>
1090
1124
  );
1091
1125
  })}
1092
- </div>
1126
+ </Stack>
1093
1127
  );
1094
1128
  }
1095
1129
 
@@ -1111,140 +1145,177 @@ function RuleList({
1111
1145
  type,
1112
1146
  }: RuleListProps) {
1113
1147
  return (
1114
- <div className="space-y-2">
1148
+ <Stack gap="sm">
1115
1149
  {rules.map((rule) => {
1116
1150
  const isExpanded = expandedRules.has(rule.id);
1117
1151
 
1118
1152
  return (
1119
- <div
1120
- key={rule.id}
1121
- className="rounded-lg border border-[--border] overflow-hidden"
1122
- >
1153
+ <Card key={rule.id}>
1123
1154
  {/* Rule Header */}
1124
1155
  <button
1125
1156
  onClick={() => onToggleRule(rule.id)}
1126
- className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-[--bg-hover] transition-colors"
1157
+ style={{
1158
+ width: '100%',
1159
+ display: 'flex',
1160
+ alignItems: 'center',
1161
+ gap: 8,
1162
+ padding: '8px 12px',
1163
+ textAlign: 'left',
1164
+ background: 'none',
1165
+ border: 'none',
1166
+ cursor: 'pointer',
1167
+ transition: 'background var(--transition-fast)',
1168
+ }}
1127
1169
  >
1128
1170
  {isExpanded ? (
1129
- <ChevronDownIcon className="w-4 h-4 text-tertiary flex-shrink-0" />
1171
+ <ChevronDownIcon style={{ width: 16, height: 16, color: 'var(--text-tertiary)', flexShrink: 0 }} />
1130
1172
  ) : (
1131
- <ChevronRightIcon className="w-4 h-4 text-tertiary flex-shrink-0" />
1173
+ <ChevronRightIcon style={{ width: 16, height: 16, color: 'var(--text-tertiary)', flexShrink: 0 }} />
1132
1174
  )}
1133
1175
 
1134
1176
  {type === "pass" && (
1135
- <CheckIcon className="w-4 h-4 text-green-600 flex-shrink-0" />
1177
+ <CheckIcon style={{ width: 16, height: 16, color: 'var(--color-success)', flexShrink: 0 }} />
1136
1178
  )}
1137
1179
 
1138
1180
  {type === "incomplete" && (
1139
- <span className="text-[10px] font-medium px-1.5 py-0.5 rounded uppercase bg-amber-100 dark:bg-amber-950/50 text-amber-700 dark:text-amber-300">
1140
- Review
1141
- </span>
1181
+ <Badge variant="warning" size="sm">Review</Badge>
1142
1182
  )}
1143
1183
 
1144
- <span className="text-xs text-primary flex-1 truncate">
1184
+ <Text size="xs" style={{
1185
+ flex: 1,
1186
+ overflow: 'hidden',
1187
+ textOverflow: 'ellipsis',
1188
+ whiteSpace: 'nowrap',
1189
+ }}>
1145
1190
  {rule.description}
1146
- </span>
1191
+ </Text>
1147
1192
 
1148
- <span className="text-[10px] text-tertiary flex-shrink-0">
1193
+ <Text size="xs" color="tertiary" style={{ flexShrink: 0 }}>
1149
1194
  {rule.nodes.length} element{rule.nodes.length !== 1 ? "s" : ""}
1150
- </span>
1195
+ </Text>
1151
1196
  </button>
1152
1197
 
1153
1198
  {/* Expanded content */}
1154
1199
  {isExpanded && (
1155
- <div className="border-t border-[--border] bg-[--bg-secondary]">
1156
- <div className="px-3 py-2 border-b border-[--border-subtle]">
1157
- <p className="text-xs text-secondary mb-1">{rule.help}</p>
1200
+ <div style={{ borderTop: '1px solid var(--border)', background: 'var(--bg-secondary)' }}>
1201
+ <Box padding="sm" style={{ borderBottom: '1px solid var(--border-subtle)' }}>
1202
+ <Text size="xs" color="secondary" style={{ marginBottom: 4 }}>{rule.help}</Text>
1158
1203
  {rule.helpUrl && (
1159
1204
  <a
1160
1205
  href={rule.helpUrl}
1161
1206
  target="_blank"
1162
1207
  rel="noopener noreferrer"
1163
- className="text-xs text-[--color-accent] hover:underline"
1208
+ style={{ fontSize: 12, color: 'var(--color-accent)', textDecoration: 'none' }}
1164
1209
  >
1165
1210
  Learn more about {rule.id}
1166
1211
  </a>
1167
1212
  )}
1168
- </div>
1213
+ </Box>
1169
1214
 
1170
- <div className="divide-y divide-[--border-subtle]">
1215
+ <div>
1171
1216
  {rule.nodes.map((node, i) => {
1172
1217
  const isHighlighted =
1173
1218
  highlightedElement === node.target.join(", ");
1174
1219
 
1175
1220
  return (
1176
- <div
1221
+ <Box
1177
1222
  key={i}
1178
- className={clsx(
1179
- "px-3 py-2",
1180
- isHighlighted && "bg-green-50 dark:bg-green-950/30"
1181
- )}
1223
+ padding="sm"
1224
+ style={{
1225
+ borderTop: i > 0 ? '1px solid var(--border-subtle)' : undefined,
1226
+ background: isHighlighted ? 'var(--color-success-bg)' : undefined,
1227
+ }}
1182
1228
  >
1183
- <button
1229
+ <Button
1230
+ variant={isHighlighted ? "primary" : "ghost"}
1231
+ size="sm"
1184
1232
  onClick={() =>
1185
1233
  onHighlight(
1186
1234
  isHighlighted ? null : node.target.join(", ")
1187
1235
  )
1188
1236
  }
1189
- className={clsx(
1190
- "text-[10px] font-mono px-1.5 py-0.5 rounded truncate max-w-[200px] mb-1",
1191
- isHighlighted
1192
- ? "bg-green-200 dark:bg-green-800 text-green-800 dark:text-green-200"
1193
- : "bg-[--bg-hover] text-secondary hover:text-primary"
1194
- )}
1195
1237
  title={node.target.join(" > ")}
1238
+ style={{
1239
+ fontFamily: 'monospace',
1240
+ fontSize: 10,
1241
+ maxWidth: 200,
1242
+ overflow: 'hidden',
1243
+ textOverflow: 'ellipsis',
1244
+ whiteSpace: 'nowrap',
1245
+ marginBottom: 4,
1246
+ }}
1196
1247
  >
1197
1248
  {node.target[node.target.length - 1]}
1198
- </button>
1199
-
1200
- <pre className="text-[10px] font-mono text-tertiary bg-[--bg-primary] rounded p-2 overflow-x-auto whitespace-pre-wrap">
1249
+ </Button>
1250
+
1251
+ <pre style={{
1252
+ fontSize: 10,
1253
+ fontFamily: 'monospace',
1254
+ color: 'var(--text-tertiary)',
1255
+ background: 'var(--bg-primary)',
1256
+ borderRadius: 'var(--radius-sm)',
1257
+ padding: 8,
1258
+ overflowX: 'auto',
1259
+ whiteSpace: 'pre-wrap',
1260
+ margin: 0,
1261
+ }}>
1201
1262
  {node.html}
1202
1263
  </pre>
1203
- </div>
1264
+ </Box>
1204
1265
  );
1205
1266
  })}
1206
1267
  </div>
1207
1268
  </div>
1208
1269
  )}
1209
- </div>
1270
+ </Card>
1210
1271
  );
1211
1272
  })}
1212
- </div>
1273
+ </Stack>
1213
1274
  );
1214
1275
  }
1215
1276
 
1216
1277
  function SuccessMessage({ message }: { message: string }) {
1217
1278
  return (
1218
- <div className="flex flex-col items-center justify-center py-8 text-center">
1219
- <div className="w-12 h-12 rounded-full bg-green-100 dark:bg-green-950/50 flex items-center justify-center mb-3">
1220
- <CheckIcon className="w-6 h-6 text-green-600 dark:text-green-400" />
1221
- </div>
1222
- <p className="text-sm font-medium text-green-700 dark:text-green-300">
1223
- {message}
1224
- </p>
1225
- <p className="text-xs text-tertiary mt-1">
1279
+ <EmptyState>
1280
+ <EmptyState.Icon>
1281
+ <div style={{
1282
+ width: 48,
1283
+ height: 48,
1284
+ borderRadius: '50%',
1285
+ background: 'var(--color-success-bg)',
1286
+ display: 'flex',
1287
+ alignItems: 'center',
1288
+ justifyContent: 'center',
1289
+ }}>
1290
+ <CheckIcon style={{ width: 24, height: 24, color: 'var(--color-success)' }} />
1291
+ </div>
1292
+ </EmptyState.Icon>
1293
+ <EmptyState.Title style={{ color: 'var(--color-success)' }}>{message}</EmptyState.Title>
1294
+ <EmptyState.Description>
1226
1295
  This component passed all automated accessibility checks.
1227
- </p>
1228
- </div>
1296
+ </EmptyState.Description>
1297
+ </EmptyState>
1229
1298
  );
1230
1299
  }
1231
1300
 
1232
1301
  function EmptyMessage({ message }: { message: string }) {
1233
1302
  return (
1234
- <div className="flex flex-col items-center justify-center py-8 text-center">
1235
- <AccessibilityIcon className="w-8 h-8 text-tertiary mb-2 opacity-50" />
1236
- <p className="text-xs text-tertiary">{message}</p>
1237
- </div>
1303
+ <EmptyState>
1304
+ <EmptyState.Icon>
1305
+ <AccessibilityIcon style={{ width: 32, height: 32, opacity: 0.5 }} />
1306
+ </EmptyState.Icon>
1307
+ <EmptyState.Description>{message}</EmptyState.Description>
1308
+ </EmptyState>
1238
1309
  );
1239
1310
  }
1240
1311
 
1241
- interface AISetupModalProps {
1312
+ interface AISetupModalContentProps {
1242
1313
  onSave: (config: AIProviderConfig) => void;
1243
1314
  onClose: () => void;
1244
1315
  currentConfig: AIProviderConfig | null;
1245
1316
  }
1246
1317
 
1247
- function AISetupModal({ onSave, onClose, currentConfig }: AISetupModalProps) {
1318
+ function AISetupModalContent({ onSave, onClose, currentConfig }: AISetupModalContentProps) {
1248
1319
  const [provider, setProvider] = useState<"anthropic" | "openai">(
1249
1320
  currentConfig?.provider || "anthropic"
1250
1321
  );
@@ -1258,117 +1329,106 @@ function AISetupModal({ onSave, onClose, currentConfig }: AISetupModalProps) {
1258
1329
  };
1259
1330
 
1260
1331
  return (
1261
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
1262
- <div className="bg-[--bg-primary] rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">
1263
- <div className="flex items-center justify-between px-4 py-3 border-b border-[--border]">
1264
- <h3 className="text-sm font-medium text-primary">
1265
- Configure AI Fix Suggestions
1266
- </h3>
1267
- <button
1268
- onClick={onClose}
1269
- className="p-1 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded"
1270
- >
1271
- <XIcon className="w-4 h-4" />
1272
- </button>
1273
- </div>
1332
+ <>
1333
+ <Dialog.Header>
1334
+ <Dialog.Title>Configure AI Fix Suggestions</Dialog.Title>
1335
+ <Dialog.Description>
1336
+ Enter your API key to enable AI-powered fix suggestions for
1337
+ accessibility violations. Your key is stored locally and never sent
1338
+ to our servers.
1339
+ </Dialog.Description>
1340
+ </Dialog.Header>
1341
+
1342
+ <Dialog.Body>
1343
+ <form onSubmit={handleSubmit}>
1344
+ <Stack gap="md">
1345
+ <div>
1346
+ <Text size="xs" weight="medium" style={{ marginBottom: 4 }}>Provider</Text>
1347
+ <Stack direction="row" gap="sm">
1348
+ <Button
1349
+ variant={provider === "anthropic" ? "primary" : "secondary"}
1350
+ size="sm"
1351
+ onClick={() => setProvider("anthropic")}
1352
+ type="button"
1353
+ style={{ flex: 1 }}
1354
+ >
1355
+ Anthropic (Claude)
1356
+ </Button>
1357
+ <Button
1358
+ variant={provider === "openai" ? "primary" : "secondary"}
1359
+ size="sm"
1360
+ onClick={() => setProvider("openai")}
1361
+ type="button"
1362
+ style={{ flex: 1 }}
1363
+ >
1364
+ OpenAI (GPT-4)
1365
+ </Button>
1366
+ </Stack>
1367
+ </div>
1274
1368
 
1275
- <form onSubmit={handleSubmit} className="p-4 space-y-4">
1276
- <p className="text-xs text-secondary">
1277
- Enter your API key to enable AI-powered fix suggestions for
1278
- accessibility violations. Your key is stored locally and never sent
1279
- to our servers.
1280
- </p>
1281
-
1282
- <div>
1283
- <label className="block text-xs font-medium text-primary mb-1">
1284
- Provider
1285
- </label>
1286
- <div className="flex gap-2">
1287
- <button
1288
- type="button"
1289
- onClick={() => setProvider("anthropic")}
1290
- className={clsx(
1291
- "flex-1 px-3 py-2 text-xs rounded border transition-colors",
1369
+ <div>
1370
+ <Text size="xs" weight="medium" style={{ marginBottom: 4 }}>API Key</Text>
1371
+ <input
1372
+ type="password"
1373
+ value={apiKey}
1374
+ onChange={(e) => setApiKey(e.target.value)}
1375
+ placeholder={
1292
1376
  provider === "anthropic"
1293
- ? "bg-[--color-accent] text-white border-[--color-accent]"
1294
- : "bg-[--bg-secondary] text-secondary border-[--border] hover:border-[--color-accent]"
1295
- )}
1296
- >
1297
- Anthropic (Claude)
1298
- </button>
1299
- <button
1300
- type="button"
1301
- onClick={() => setProvider("openai")}
1302
- className={clsx(
1303
- "flex-1 px-3 py-2 text-xs rounded border transition-colors",
1304
- provider === "openai"
1305
- ? "bg-[--color-accent] text-white border-[--color-accent]"
1306
- : "bg-[--bg-secondary] text-secondary border-[--border] hover:border-[--color-accent]"
1377
+ ? "sk-ant-api03-..."
1378
+ : "sk-..."
1379
+ }
1380
+ style={{
1381
+ width: '100%',
1382
+ padding: '8px 12px',
1383
+ fontSize: 12,
1384
+ borderRadius: 'var(--radius-sm)',
1385
+ border: '1px solid var(--border)',
1386
+ background: 'var(--bg-secondary)',
1387
+ color: 'var(--text-primary)',
1388
+ outline: 'none',
1389
+ boxSizing: 'border-box',
1390
+ }}
1391
+ />
1392
+ <Text size="xs" color="tertiary" style={{ marginTop: 4 }}>
1393
+ Get your API key from{" "}
1394
+ {provider === "anthropic" ? (
1395
+ <a
1396
+ href="https://console.anthropic.com/settings/keys"
1397
+ target="_blank"
1398
+ rel="noopener noreferrer"
1399
+ style={{ color: 'var(--color-accent)', textDecoration: 'none' }}
1400
+ >
1401
+ console.anthropic.com
1402
+ </a>
1403
+ ) : (
1404
+ <a
1405
+ href="https://platform.openai.com/api-keys"
1406
+ target="_blank"
1407
+ rel="noopener noreferrer"
1408
+ style={{ color: 'var(--color-accent)', textDecoration: 'none' }}
1409
+ >
1410
+ platform.openai.com
1411
+ </a>
1307
1412
  )}
1308
- >
1309
- OpenAI (GPT-4)
1310
- </button>
1413
+ </Text>
1311
1414
  </div>
1312
- </div>
1313
-
1314
- <div>
1315
- <label className="block text-xs font-medium text-primary mb-1">
1316
- API Key
1317
- </label>
1318
- <input
1319
- type="password"
1320
- value={apiKey}
1321
- onChange={(e) => setApiKey(e.target.value)}
1322
- placeholder={
1323
- provider === "anthropic"
1324
- ? "sk-ant-api03-..."
1325
- : "sk-..."
1326
- }
1327
- className="w-full px-3 py-2 text-xs rounded border border-[--border] bg-[--bg-secondary] text-primary placeholder:text-tertiary focus:outline-none focus:border-[--color-accent]"
1328
- />
1329
- <p className="text-[10px] text-tertiary mt-1">
1330
- Get your API key from{" "}
1331
- {provider === "anthropic" ? (
1332
- <a
1333
- href="https://console.anthropic.com/settings/keys"
1334
- target="_blank"
1335
- rel="noopener noreferrer"
1336
- className="text-[--color-accent] hover:underline"
1337
- >
1338
- console.anthropic.com
1339
- </a>
1340
- ) : (
1341
- <a
1342
- href="https://platform.openai.com/api-keys"
1343
- target="_blank"
1344
- rel="noopener noreferrer"
1345
- className="text-[--color-accent] hover:underline"
1346
- >
1347
- platform.openai.com
1348
- </a>
1349
- )}
1350
- </p>
1351
- </div>
1352
1415
 
1353
- <div className="flex justify-end gap-2 pt-2">
1354
- <button
1355
- type="button"
1356
- onClick={onClose}
1357
- className="px-3 py-1.5 text-xs rounded border border-[--border] text-secondary hover:bg-[--bg-hover]"
1358
- >
1359
- Cancel
1360
- </button>
1361
- <button
1362
- type="submit"
1363
- disabled={!apiKey.trim()}
1364
- className="px-3 py-1.5 text-xs rounded bg-[--color-accent] text-white hover:opacity-90 disabled:opacity-50"
1365
- >
1366
- Save Configuration
1367
- </button>
1368
- </div>
1416
+ <Dialog.Footer>
1417
+ <Button variant="secondary" size="sm" onClick={onClose} type="button">
1418
+ Cancel
1419
+ </Button>
1420
+ <Button
1421
+ size="sm"
1422
+ type="submit"
1423
+ disabled={!apiKey.trim()}
1424
+ >
1425
+ Save Configuration
1426
+ </Button>
1427
+ </Dialog.Footer>
1428
+ </Stack>
1369
1429
  </form>
1370
- </div>
1371
- </div>
1430
+ </Dialog.Body>
1431
+ </>
1372
1432
  );
1373
1433
  }
1374
1434