@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.
- package/dist/bin.js +996 -79
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
- package/dist/chunk-6JBGU74P.js.map +1 -0
- package/dist/chunk-7OPWMLOE.js +1625 -0
- package/dist/chunk-7OPWMLOE.js.map +1 -0
- package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
- package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
- package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
- package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
- package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
- package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
- package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
- package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
- package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
- package/dist/mcp-bin.js +8 -220
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-WY23TJCP.js +12 -0
- package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
- package/dist/static-viewer-GBR7YNF3.js +12 -0
- package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
- package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
- package/dist/viewer-SUFOISZM.js +1822 -0
- package/dist/viewer-SUFOISZM.js.map +1 -0
- package/package.json +6 -5
- package/src/bin.ts +31 -0
- package/src/build.ts +147 -13
- package/src/cli-commands.ts +18 -0
- package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
- package/src/commands/a11y-report.ts +625 -0
- package/src/commands/a11y.ts +168 -14
- package/src/commands/build.ts +16 -0
- package/src/commands/graph.ts +274 -0
- package/src/core/auto-props.ts +464 -0
- package/src/core/composition.ts +64 -1
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +5 -0
- package/src/core/schema.ts +2 -0
- package/src/core/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/mcp/server.ts +13 -220
- package/src/theme/__tests__/component-contrast.test.ts +338 -0
- package/src/theme/__tests__/contrast-validation.test.ts +326 -0
- package/src/theme/contrast.test.ts +331 -0
- package/src/theme/contrast.ts +246 -0
- package/src/theme/generator.ts +213 -1
- package/src/theme/index.ts +16 -0
- package/src/theme/types.ts +51 -0
- package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
- package/src/viewer/components/AccessibilityPanel.tsx +493 -433
- package/src/viewer/components/ActionCapture.tsx +1 -1
- package/src/viewer/components/ActionsPanel.tsx +142 -183
- package/src/viewer/components/App.tsx +276 -183
- package/src/viewer/components/BottomPanel.tsx +40 -80
- package/src/viewer/components/CodePanel.tsx +9 -87
- package/src/viewer/components/CommandPalette.tsx +117 -74
- package/src/viewer/components/ComponentGraph.tsx +143 -126
- package/src/viewer/components/ComponentHeader.tsx +46 -43
- package/src/viewer/components/ContractPanel.tsx +124 -117
- package/src/viewer/components/ErrorBoundary.tsx +47 -35
- package/src/viewer/components/FigmaEmbed.tsx +18 -13
- package/src/viewer/components/FragmentEditor.tsx +126 -63
- package/src/viewer/components/HealthDashboard.tsx +146 -171
- package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
- package/src/viewer/components/Icons.tsx +151 -98
- package/src/viewer/components/InteractionsPanel.tsx +317 -264
- package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
- package/src/viewer/components/IsolatedRender.tsx +12 -6
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
- package/src/viewer/components/LandingPage.tsx +285 -305
- package/src/viewer/components/Layout.tsx +12 -10
- package/src/viewer/components/LeftSidebar.tsx +103 -155
- package/src/viewer/components/MultiViewportPreview.tsx +254 -63
- package/src/viewer/components/PreviewArea.tsx +113 -44
- package/src/viewer/components/PreviewFrameHost.tsx +36 -6
- package/src/viewer/components/PreviewPane.tsx +2 -3
- package/src/viewer/components/PreviewToolbar.tsx +109 -105
- package/src/viewer/components/PropsEditor.tsx +154 -74
- package/src/viewer/components/PropsTable.tsx +95 -82
- package/src/viewer/components/RelationsSection.tsx +71 -40
- package/src/viewer/components/ResizablePanel.tsx +158 -55
- package/src/viewer/components/RightSidebar.tsx +46 -56
- package/src/viewer/components/ScreenshotButton.tsx +12 -12
- package/src/viewer/components/SkeletonLoader.tsx +99 -83
- package/src/viewer/components/StoryRenderer.tsx +4 -11
- package/src/viewer/components/Toast.tsx +3 -67
- package/src/viewer/components/TokenStylePanel.tsx +136 -118
- package/src/viewer/components/UsageSection.tsx +26 -26
- package/src/viewer/components/VariantMatrix.tsx +140 -47
- package/src/viewer/components/VariantTabs.tsx +24 -68
- package/src/viewer/components/ViewportSelector.tsx +121 -114
- package/src/viewer/constants/ui.ts +23 -22
- package/src/viewer/entry.tsx +8 -3
- package/src/viewer/index.ts +3 -6
- package/src/viewer/preview-frame.html +43 -18
- package/src/viewer/server.ts +7 -16
- package/src/viewer/styles/globals.css +46 -85
- package/src/viewer/utils/a11y-fixes.ts +53 -30
- package/dist/chunk-ICAIQ57V.js.map +0 -1
- package/dist/chunk-U4GQ2JTD.js +0 -832
- package/dist/chunk-U4GQ2JTD.js.map +0 -1
- package/dist/scan-ESEXV7LF.js +0 -12
- package/dist/static-viewer-O37MJ5B6.js +0 -12
- package/dist/viewer-YDGFDTK5.js +0 -11104
- package/dist/viewer-YDGFDTK5.js.map +0 -1
- package/src/viewer/postcss.config.js +0 -6
- package/src/viewer/tailwind.config.js +0 -37
- /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
- /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
- /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
- /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
- /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
- /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
- /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
- /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
- /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
- /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
- /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
|
|
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
|
-
<
|
|
578
|
-
<LoadingIcon
|
|
579
|
-
<
|
|
580
|
-
</
|
|
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
|
-
<
|
|
588
|
-
<XIcon
|
|
589
|
-
<
|
|
590
|
-
<
|
|
591
|
-
|
|
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
|
-
<
|
|
604
|
-
<AccessibilityIcon
|
|
605
|
-
<
|
|
606
|
-
<
|
|
607
|
-
|
|
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
|
|
618
|
-
{/* Tabs */}
|
|
619
|
-
<
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
<
|
|
647
|
-
<
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
657
|
+
style={{
|
|
658
|
+
width: 12,
|
|
659
|
+
height: 12,
|
|
660
|
+
...(isScanning ? { animation: 'spin 1s linear infinite' } : {}),
|
|
661
|
+
}}
|
|
661
662
|
/>
|
|
662
663
|
{isScanning ? "Scanning..." : "Rescan"}
|
|
663
|
-
</
|
|
664
|
+
</Button>
|
|
664
665
|
|
|
665
666
|
{/* AI Setup button */}
|
|
666
|
-
<
|
|
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
|
|
673
|
+
<WandIcon style={{ width: 12, height: 12 }} />
|
|
677
674
|
{aiConfig ? "AI Ready" : "Setup AI"}
|
|
678
|
-
</
|
|
679
|
-
</
|
|
675
|
+
</Button>
|
|
676
|
+
</Stack>
|
|
680
677
|
|
|
681
678
|
{/* Content */}
|
|
682
|
-
<div
|
|
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
|
|
742
|
-
{showAISetup && (
|
|
743
|
-
<
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
|
809
|
+
<ChevronDownIcon style={{ width: 16, height: 16, color: 'var(--text-tertiary)', flexShrink: 0 }} />
|
|
837
810
|
) : (
|
|
838
|
-
<ChevronRightIcon
|
|
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
|
-
<
|
|
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
|
-
</
|
|
818
|
+
</Badge>
|
|
852
819
|
)}
|
|
853
820
|
|
|
854
821
|
{/* Rule ID */}
|
|
855
|
-
<
|
|
856
|
-
{rule.id}
|
|
857
|
-
</span>
|
|
822
|
+
<Text font="mono" size="xs" color="tertiary">{rule.id}</Text>
|
|
858
823
|
|
|
859
824
|
{/* Rule description */}
|
|
860
|
-
<
|
|
825
|
+
<Text size="xs" style={{
|
|
826
|
+
flex: 1,
|
|
827
|
+
overflow: 'hidden',
|
|
828
|
+
textOverflow: 'ellipsis',
|
|
829
|
+
whiteSpace: 'nowrap',
|
|
830
|
+
}}>
|
|
861
831
|
{rule.description}
|
|
862
|
-
</
|
|
832
|
+
</Text>
|
|
863
833
|
|
|
864
834
|
{/* Element count */}
|
|
865
|
-
<
|
|
835
|
+
<Badge size="sm">
|
|
866
836
|
{rule.nodes.length} element{rule.nodes.length !== 1 ? "s" : ""}
|
|
867
|
-
</
|
|
837
|
+
</Badge>
|
|
868
838
|
</button>
|
|
869
839
|
|
|
870
840
|
{/* Expanded content */}
|
|
871
841
|
{isExpanded && (
|
|
872
|
-
<div
|
|
842
|
+
<div style={{ borderTop: '1px solid var(--border)', background: 'var(--bg-secondary)' }}>
|
|
873
843
|
{/* Why it matters + WCAG reference */}
|
|
874
844
|
{staticFix && (
|
|
875
|
-
<
|
|
876
|
-
<
|
|
877
|
-
<
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
<
|
|
889
|
-
"
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
<
|
|
941
|
-
<
|
|
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
|
-
|
|
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
|
-
</
|
|
950
|
+
</Box>
|
|
953
951
|
)}
|
|
954
952
|
|
|
955
953
|
{/* Affected elements */}
|
|
956
|
-
<div
|
|
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
|
-
<
|
|
962
|
+
<Box
|
|
965
963
|
key={i}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
<
|
|
973
|
-
<
|
|
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
|
-
</
|
|
991
|
+
</Button>
|
|
989
992
|
|
|
990
993
|
{aiEnabled && onGenerateFix && (
|
|
991
|
-
<
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
</
|
|
1009
|
+
</Button>
|
|
1010
1010
|
)}
|
|
1011
|
-
</
|
|
1011
|
+
</Stack>
|
|
1012
1012
|
|
|
1013
1013
|
{/* HTML snippet */}
|
|
1014
|
-
<pre
|
|
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
|
-
<
|
|
1031
|
+
<Text size="xs" color="secondary" style={{ marginTop: 8, lineHeight: 1.6 }}>
|
|
1021
1032
|
{node.failureSummary}
|
|
1022
|
-
</
|
|
1033
|
+
</Text>
|
|
1023
1034
|
)}
|
|
1024
1035
|
|
|
1025
1036
|
{/* Static element fix */}
|
|
1026
1037
|
{elementFix && (
|
|
1027
|
-
<
|
|
1028
|
-
<
|
|
1029
|
-
<
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
</
|
|
1056
|
-
</
|
|
1057
|
-
<pre
|
|
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
|
-
</
|
|
1101
|
+
</Box>
|
|
1063
1102
|
);
|
|
1064
1103
|
})}
|
|
1065
1104
|
</div>
|
|
1066
1105
|
|
|
1067
1106
|
{/* Tags */}
|
|
1068
1107
|
{rule.tags && rule.tags.length > 0 && (
|
|
1069
|
-
<
|
|
1070
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
1114
|
+
<Text size="xs" color="tertiary">
|
|
1081
1115
|
+{rule.tags.length - 8} more
|
|
1082
|
-
</
|
|
1116
|
+
</Text>
|
|
1083
1117
|
)}
|
|
1084
|
-
</
|
|
1085
|
-
</
|
|
1118
|
+
</Stack>
|
|
1119
|
+
</Box>
|
|
1086
1120
|
)}
|
|
1087
1121
|
</div>
|
|
1088
1122
|
)}
|
|
1089
|
-
</
|
|
1123
|
+
</Card>
|
|
1090
1124
|
);
|
|
1091
1125
|
})}
|
|
1092
|
-
</
|
|
1126
|
+
</Stack>
|
|
1093
1127
|
);
|
|
1094
1128
|
}
|
|
1095
1129
|
|
|
@@ -1111,140 +1145,177 @@ function RuleList({
|
|
|
1111
1145
|
type,
|
|
1112
1146
|
}: RuleListProps) {
|
|
1113
1147
|
return (
|
|
1114
|
-
<
|
|
1148
|
+
<Stack gap="sm">
|
|
1115
1149
|
{rules.map((rule) => {
|
|
1116
1150
|
const isExpanded = expandedRules.has(rule.id);
|
|
1117
1151
|
|
|
1118
1152
|
return (
|
|
1119
|
-
<
|
|
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
|
-
|
|
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
|
|
1171
|
+
<ChevronDownIcon style={{ width: 16, height: 16, color: 'var(--text-tertiary)', flexShrink: 0 }} />
|
|
1130
1172
|
) : (
|
|
1131
|
-
<ChevronRightIcon
|
|
1173
|
+
<ChevronRightIcon style={{ width: 16, height: 16, color: 'var(--text-tertiary)', flexShrink: 0 }} />
|
|
1132
1174
|
)}
|
|
1133
1175
|
|
|
1134
1176
|
{type === "pass" && (
|
|
1135
|
-
<CheckIcon
|
|
1177
|
+
<CheckIcon style={{ width: 16, height: 16, color: 'var(--color-success)', flexShrink: 0 }} />
|
|
1136
1178
|
)}
|
|
1137
1179
|
|
|
1138
1180
|
{type === "incomplete" && (
|
|
1139
|
-
<
|
|
1140
|
-
Review
|
|
1141
|
-
</span>
|
|
1181
|
+
<Badge variant="warning" size="sm">Review</Badge>
|
|
1142
1182
|
)}
|
|
1143
1183
|
|
|
1144
|
-
<
|
|
1184
|
+
<Text size="xs" style={{
|
|
1185
|
+
flex: 1,
|
|
1186
|
+
overflow: 'hidden',
|
|
1187
|
+
textOverflow: 'ellipsis',
|
|
1188
|
+
whiteSpace: 'nowrap',
|
|
1189
|
+
}}>
|
|
1145
1190
|
{rule.description}
|
|
1146
|
-
</
|
|
1191
|
+
</Text>
|
|
1147
1192
|
|
|
1148
|
-
<
|
|
1193
|
+
<Text size="xs" color="tertiary" style={{ flexShrink: 0 }}>
|
|
1149
1194
|
{rule.nodes.length} element{rule.nodes.length !== 1 ? "s" : ""}
|
|
1150
|
-
</
|
|
1195
|
+
</Text>
|
|
1151
1196
|
</button>
|
|
1152
1197
|
|
|
1153
1198
|
{/* Expanded content */}
|
|
1154
1199
|
{isExpanded && (
|
|
1155
|
-
<div
|
|
1156
|
-
<
|
|
1157
|
-
<
|
|
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
|
-
|
|
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
|
-
</
|
|
1213
|
+
</Box>
|
|
1169
1214
|
|
|
1170
|
-
<div
|
|
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
|
-
<
|
|
1221
|
+
<Box
|
|
1177
1222
|
key={i}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
1199
|
-
|
|
1200
|
-
<pre
|
|
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
|
-
</
|
|
1264
|
+
</Box>
|
|
1204
1265
|
);
|
|
1205
1266
|
})}
|
|
1206
1267
|
</div>
|
|
1207
1268
|
</div>
|
|
1208
1269
|
)}
|
|
1209
|
-
</
|
|
1270
|
+
</Card>
|
|
1210
1271
|
);
|
|
1211
1272
|
})}
|
|
1212
|
-
</
|
|
1273
|
+
</Stack>
|
|
1213
1274
|
);
|
|
1214
1275
|
}
|
|
1215
1276
|
|
|
1216
1277
|
function SuccessMessage({ message }: { message: string }) {
|
|
1217
1278
|
return (
|
|
1218
|
-
<
|
|
1219
|
-
<
|
|
1220
|
-
<
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
</
|
|
1228
|
-
</
|
|
1296
|
+
</EmptyState.Description>
|
|
1297
|
+
</EmptyState>
|
|
1229
1298
|
);
|
|
1230
1299
|
}
|
|
1231
1300
|
|
|
1232
1301
|
function EmptyMessage({ message }: { message: string }) {
|
|
1233
1302
|
return (
|
|
1234
|
-
<
|
|
1235
|
-
<
|
|
1236
|
-
|
|
1237
|
-
|
|
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
|
|
1312
|
+
interface AISetupModalContentProps {
|
|
1242
1313
|
onSave: (config: AIProviderConfig) => void;
|
|
1243
1314
|
onClose: () => void;
|
|
1244
1315
|
currentConfig: AIProviderConfig | null;
|
|
1245
1316
|
}
|
|
1246
1317
|
|
|
1247
|
-
function
|
|
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
|
-
|
|
1262
|
-
<
|
|
1263
|
-
<
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
-
? "
|
|
1294
|
-
: "
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
</
|
|
1371
|
-
|
|
1430
|
+
</Dialog.Body>
|
|
1431
|
+
</>
|
|
1372
1432
|
);
|
|
1373
1433
|
}
|
|
1374
1434
|
|