diff --git a/frontend/app/components/SanctityScore.tsx b/frontend/app/components/SanctityScore.tsx index f0a28d2..537bd67 100644 --- a/frontend/app/components/SanctityScore.tsx +++ b/frontend/app/components/SanctityScore.tsx @@ -2,45 +2,22 @@ import { useMemo } from "react"; import type { Finding } from "../types"; +import { + calculateScore, + getScoreGrade, + getScoreNarrative, + getSeverityColor, +} from "../lib/report-export"; interface SanctityScoreProps { findings: Finding[]; } -const SEVERITY_WEIGHTS: Record = { - critical: 15, - high: 10, - medium: 5, - low: 2, -}; - -function calculateScore(findings: Finding[]): number { - let score = 100; - for (const f of findings) { - score -= SEVERITY_WEIGHTS[f.severity] ?? 0; - } - return Math.max(0, Math.min(100, score)); -} - -function getGrade(score: number): string { - if (score >= 90) return "A"; - if (score >= 80) return "B"; - if (score >= 65) return "C"; - if (score >= 50) return "D"; - return "F"; -} - -function getColor(score: number): string { - if (score >= 76) return "#22c55e"; - if (score >= 61) return "#f59e0b"; - if (score >= 41) return "#f97316"; - return "#ef4444"; -} - export function SanctityScore({ findings }: SanctityScoreProps) { const score = useMemo(() => calculateScore(findings), [findings]); - const grade = getGrade(score); - const color = getColor(score); + const grade = getScoreGrade(score); + const color = getSeverityColor(score); + const narrative = getScoreNarrative(score); const radius = 70; const strokeWidth = 12; @@ -53,67 +30,53 @@ export function SanctityScore({ findings }: SanctityScoreProps) { Sanctity Score
-= 76 - ? "Good security posture" - : score >= 50 - ? "Moderate risk — review findings" - : "High risk — immediate attention needed" - }`} - > - Sanctity Score: {score}/100, Grade {grade} - {/* Background arc */} - - {/* Progress arc */} - - {/* Score text */} - - {score} - - {/* Grade label */} - - Grade: {grade} - - + Sanctity Score: {score}/100, Grade {grade} + + + + {score} + + + Grade: {grade} + +

- {score >= 76 - ? "Good security posture" - : score >= 50 - ? "Moderate risk — review findings" - : "High risk — immediate attention needed"} + {narrative}

); diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index 9d47bcd..79e2652 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -1,10 +1,17 @@ "use client"; -import { useState, useCallback, useTransition, useMemo } from "react"; +import { useState, useCallback, useTransition } from "react"; import dynamic from "next/dynamic"; -import type { CallGraphNode, CallGraphEdge, Finding, Severity } from "../types"; +import type { + AnalysisReport, + CallGraphNode, + CallGraphEdge, + Finding, + Severity, +} from "../types"; import { transformReport, extractCallGraph, normalizeReport } from "../lib/transform"; import { exportToPdf } from "../lib/export-pdf"; +import { exportToCsv } from "../lib/export-csv"; import { SeverityFilter } from "../components/SeverityFilter"; import { FindingsList } from "../components/FindingsList"; import { SummaryChart } from "../components/SummaryChart"; @@ -49,6 +56,7 @@ function extractErrorMessage(payload: unknown, fallback: string): string { export default function DashboardPage() { const [findings, setFindings] = useState([]); + const [report, setReport] = useState(null); const [callGraphNodes, setCallGraphNodes] = useState([]); const [callGraphEdges, setCallGraphEdges] = useState([]); const [severityFilter, setSeverityFilter] = useState("all"); @@ -65,6 +73,7 @@ export default function DashboardPage() { const transformed = transformReport(report); const graph = extractCallGraph(report); + setReport(report); setFindings(transformed); setCallGraphNodes(graph.nodes); setCallGraphEdges(graph.edges); @@ -78,6 +87,7 @@ export default function DashboardPage() { applyReport(JSON.parse(text || SAMPLE_JSON)); } catch (e) { setError(e instanceof Error ? e.message : "Invalid JSON"); + setReport(null); setFindings([]); setCallGraphNodes([]); setCallGraphEdges([]); @@ -162,6 +172,9 @@ export default function DashboardPage() {

Paste JSON from sanctifier analyze --format json, upload an existing report, or analyze a Rust contract source file.

+
+ Audit exports include a compliance-style PDF with Sanctity Score and methodology, plus a CSV findings register for remediation and governance workflows. +
{uploadStatus && ( diff --git a/frontend/app/lib/export-csv.ts b/frontend/app/lib/export-csv.ts new file mode 100644 index 0000000..920511c --- /dev/null +++ b/frontend/app/lib/export-csv.ts @@ -0,0 +1,19 @@ +import type { AnalysisReport, Finding } from "../types"; +import { buildCsvContent } from "./report-export"; + +export function exportToCsv( + findings: Finding[], + report: AnalysisReport | null, + title = "Sanctifier Compliance Audit Report" +): void { + const csv = buildCsvContent(findings, report, title); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + + link.href = url; + link.download = "sanctifier-report.csv"; + link.click(); + + URL.revokeObjectURL(url); +} diff --git a/frontend/app/lib/export-pdf.ts b/frontend/app/lib/export-pdf.ts index f0e6391..5cece92 100644 --- a/frontend/app/lib/export-pdf.ts +++ b/frontend/app/lib/export-pdf.ts @@ -1,136 +1,163 @@ -import type { Finding, Severity } from "../types"; - -const SEVERITY_WEIGHTS: Record = { - critical: 15, - high: 10, - medium: 5, - low: 2, -}; - -function calculateScore(findings: Finding[]): number { - let score = 100; - for (const f of findings) { - score -= SEVERITY_WEIGHTS[f.severity] ?? 0; - } - return Math.max(0, Math.min(100, score)); -} +import type { AnalysisReport, Finding } from "../types"; +import { + buildAuditExportModel, + getSeverityColor, + orderedSeverities, +} from "./report-export"; export async function exportToPdf( findings: Finding[], - title = "Sanctifier Security Report" + report: AnalysisReport | null, + title = "Sanctifier Compliance Audit Report" ): Promise { try { const { jsPDF } = await import("jspdf"); const doc = new jsPDF(); + const model = buildAuditExportModel(findings, report, title); + const accent = getSeverityColor(model.score); + const pageWidth = doc.internal.pageSize.getWidth(); + const pageHeight = doc.internal.pageSize.getHeight(); let pageNum = 1; const addFooter = () => { doc.setFontSize(8); doc.setFont("helvetica", "normal"); doc.setTextColor(150); - doc.text( - `Sanctifier Security Report - Page ${pageNum}`, - 105, - 290, - { align: "center" } - ); - doc.setTextColor(0); + doc.text(`${model.title} - Page ${pageNum}`, pageWidth / 2, pageHeight - 8, { + align: "center", + }); + doc.setTextColor(24, 24, 27); }; - // Header + doc.setFillColor(15, 23, 42); + doc.rect(0, 0, pageWidth, 42, "F"); + + doc.setTextColor(255, 255, 255); doc.setFontSize(20); doc.setFont("helvetica", "bold"); - doc.text(title, 14, 22); + doc.text(model.title, 14, 18); doc.setFontSize(10); doc.setFont("helvetica", "normal"); - doc.text(`Generated: ${new Date().toLocaleString()}`, 14, 30); - doc.text(`Total findings: ${findings.length}`, 14, 36); + doc.text(`Generated: ${model.generatedAt}`, 14, 26); + doc.text("Prepared for compliance and audit review", 14, 32); + + doc.setFillColor(accent); + doc.roundedRect(pageWidth - 58, 10, 42, 20, 4, 4, "F"); + doc.setFontSize(18); + doc.setFont("helvetica", "bold"); + doc.text(String(model.score), pageWidth - 37, 20, { align: "center" }); + doc.setFontSize(8); + doc.text(`Grade ${model.grade}`, pageWidth - 37, 26, { align: "center" }); - // Sanctity Score - const score = calculateScore(findings); - doc.setFontSize(14); + doc.setTextColor(24, 24, 27); + let y = 54; + + doc.setFontSize(12); doc.setFont("helvetica", "bold"); - doc.text(`Sanctity Score: ${score}/100`, 14, 48); + doc.text("Executive Summary", 14, y); + y += 8; - // Severity summary table - const severities: Severity[] = ["critical", "high", "medium", "low"]; - const counts: Record = { critical: 0, high: 0, medium: 0, low: 0 }; - findings.forEach((f) => { counts[f.severity]++; }); + doc.setFontSize(10); + doc.setFont("helvetica", "normal"); + const summary = doc.splitTextToSize( + `${model.narrative}. Sanctifier identified ${model.totalFindings} total findings in this report. ` + + "Use the severity summary and detailed findings below to support remediation planning and audit evidence collection.", + 182 + ); + doc.text(summary, 14, y); + y += summary.length * 5 + 6; - let y = 58; doc.setFontSize(12); doc.setFont("helvetica", "bold"); - doc.text("Summary", 14, y); + doc.text("Severity Summary", 14, y); y += 8; doc.setFontSize(10); doc.setFont("helvetica", "normal"); - severities.forEach((sev) => { - doc.text(`${sev.charAt(0).toUpperCase() + sev.slice(1)}: ${counts[sev]}`, 14, y); - y += 6; + orderedSeverities().forEach((severity) => { + const label = severity.charAt(0).toUpperCase() + severity.slice(1); + doc.text(`${label}: ${model.severityCounts[severity]}`, 14, y); + y += 5; }); y += 6; - // Separator line doc.setDrawColor(200); - doc.line(14, y, 196, y); + doc.line(14, y, pageWidth - 14, y); y += 10; - // Findings grouped by severity + doc.setFontSize(12); + doc.setFont("helvetica", "bold"); + doc.text("Methodology", 14, y); + y += 8; + + doc.setFontSize(10); + doc.setFont("helvetica", "normal"); + model.methodology.forEach((step) => { + const lines = doc.splitTextToSize(`- ${step}`, 182); + doc.text(lines, 14, y); + y += lines.length * 5 + 2; + }); + y += 4; + addFooter(); - severities.forEach((sev) => { - const sevFindings = findings.filter((f) => f.severity === sev); - if (sevFindings.length === 0) return; + orderedSeverities().forEach((severity) => { + const groupedFindings = model.findingsBySeverity[severity]; + if (groupedFindings.length === 0) { + return; + } if (y > 250) { doc.addPage(); - pageNum++; + pageNum += 1; y = 20; addFooter(); } - // Section header + const label = severity.charAt(0).toUpperCase() + severity.slice(1); doc.setFontSize(13); doc.setFont("helvetica", "bold"); - doc.text( - `${sev.charAt(0).toUpperCase() + sev.slice(1)} (${sevFindings.length})`, - 14, - y - ); + doc.text(`${label} Findings (${groupedFindings.length})`, 14, y); y += 8; - sevFindings.forEach((f, i) => { + groupedFindings.forEach((finding, index) => { if (y > 260) { doc.addPage(); - pageNum++; + pageNum += 1; y = 20; addFooter(); } doc.setFontSize(11); doc.setFont("helvetica", "bold"); - doc.text(`${i + 1}. ${f.title}`, 14, y); + doc.text(`${index + 1}. ${finding.title}`, 14, y); y += 6; doc.setFont("helvetica", "normal"); doc.setFontSize(9); - doc.text(`Category: ${f.category}`, 20, y); + doc.text(`Category: ${finding.category}`, 20, y); + y += 5; + doc.text(`Code: ${finding.code}`, 20, y); y += 5; - doc.text(`Location: ${f.location}`, 20, y); + doc.text(`Location: ${finding.location}`, 20, y); y += 5; - if (f.snippet) { - const snippetLines = doc.splitTextToSize(`Code: ${f.snippet}`, 170); + if (finding.snippet) { + const snippetLines = doc.splitTextToSize(`Code: ${finding.snippet}`, 170); doc.text(snippetLines, 20, y); y += snippetLines.length * 4; } - if (f.suggestion) { - const suggLines = doc.splitTextToSize(`Suggestion: ${f.suggestion}`, 170); - doc.text(suggLines, 20, y); - y += suggLines.length * 4; + + if (finding.suggestion) { + const suggestionLines = doc.splitTextToSize( + `Suggestion: ${finding.suggestion}`, + 170 + ); + doc.text(suggestionLines, 20, y); + y += suggestionLines.length * 4; } + y += 6; }); diff --git a/frontend/app/lib/report-export.test.ts b/frontend/app/lib/report-export.test.ts new file mode 100644 index 0000000..383c8d8 --- /dev/null +++ b/frontend/app/lib/report-export.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import type { Finding } from "../types"; +import { + buildAuditExportModel, + buildCsvContent, + calculateScore, + getScoreGrade, +} from "./report-export"; + +const findings: Finding[] = [ + { + id: "1", + code: "S001", + severity: "critical", + category: "Auth Gap", + title: "Missing require_auth()", + location: "src/lib.rs:10", + suggestion: "Add require_auth().", + raw: null, + }, + { + id: "2", + code: "S008", + severity: "medium", + category: "Event Issue", + title: "Inconsistent topics", + location: "src/lib.rs:24", + snippet: "publish((symbol_short!(\"evt\"),), data);", + raw: null, + }, +]; + +describe("report export helpers", () => { + it("builds an audit model with score and severity buckets", () => { + const model = buildAuditExportModel(findings, null); + + expect(model.score).toBe(80); + expect(model.grade).toBe("B"); + expect(model.severityCounts.critical).toBe(1); + expect(model.severityCounts.medium).toBe(1); + expect(model.findingsBySeverity.critical).toHaveLength(1); + }); + + it("builds CSV content with quoted cells", () => { + const csv = buildCsvContent(findings, null); + + expect(csv).toContain('"report_title","generated_at","sanctity_score"'); + expect(csv).toContain('"Missing require_auth()"'); + expect(csv).toContain('"Add require_auth()."'); + }); + + it("shares score helpers with the dashboard", () => { + expect(calculateScore(findings)).toBe(80); + expect(getScoreGrade(80)).toBe("B"); + }); +}); diff --git a/frontend/app/lib/report-export.ts b/frontend/app/lib/report-export.ts new file mode 100644 index 0000000..a3b11d0 --- /dev/null +++ b/frontend/app/lib/report-export.ts @@ -0,0 +1,153 @@ +import type { AnalysisReport, Finding, Severity } from "../types"; + +const SEVERITY_WEIGHTS: Record = { + critical: 15, + high: 10, + medium: 5, + low: 2, +}; + +const SEVERITY_ORDER: Severity[] = ["critical", "high", "medium", "low"]; + +export interface AuditExportModel { + title: string; + generatedAt: string; + score: number; + grade: string; + narrative: string; + severityCounts: Record; + totalFindings: number; + methodology: string[]; + findingsBySeverity: Record; + findings: Finding[]; + report: AnalysisReport | null; +} + +export function calculateScore(findings: Finding[]): number { + let score = 100; + for (const finding of findings) { + score -= SEVERITY_WEIGHTS[finding.severity] ?? 0; + } + return Math.max(0, Math.min(100, score)); +} + +export function getScoreGrade(score: number): string { + if (score >= 90) return "A"; + if (score >= 80) return "B"; + if (score >= 65) return "C"; + if (score >= 50) return "D"; + return "F"; +} + +export function getScoreNarrative(score: number): string { + if (score >= 76) return "Good security posture"; + if (score >= 50) return "Moderate risk - review findings"; + return "High risk - immediate attention needed"; +} + +export function getSeverityColor(score: number): string { + if (score >= 76) return "#22c55e"; + if (score >= 61) return "#f59e0b"; + if (score >= 41) return "#f97316"; + return "#ef4444"; +} + +export function buildAuditExportModel( + findings: Finding[], + report: AnalysisReport | null, + title = "Sanctifier Compliance Audit Report" +): AuditExportModel { + const score = calculateScore(findings); + const severityCounts = createSeverityCounts(findings); + const findingsBySeverity = createSeverityBuckets(findings); + + return { + title, + generatedAt: new Date().toLocaleString(), + score, + grade: getScoreGrade(score), + narrative: getScoreNarrative(score), + severityCounts, + totalFindings: findings.length, + methodology: [ + "Static analysis findings are normalized from Sanctifier JSON output before export.", + "Sanctity Score starts at 100 and is reduced by weighted severity deductions.", + "Critical and high findings should be reviewed before production deployment.", + "This document is intended for audit, compliance, and remediation tracking workflows.", + ], + findingsBySeverity, + findings, + report, + }; +} + +export function buildCsvContent( + findings: Finding[], + report: AnalysisReport | null, + title = "Sanctifier Compliance Audit Report" +): string { + const model = buildAuditExportModel(findings, report, title); + const rows = [ + [ + "report_title", + "generated_at", + "sanctity_score", + "grade", + "severity", + "code", + "category", + "title", + "location", + "suggestion", + "snippet", + ], + ...model.findings.map((finding) => [ + model.title, + model.generatedAt, + String(model.score), + model.grade, + finding.severity, + finding.code, + finding.category, + finding.title, + finding.location, + finding.suggestion ?? "", + finding.snippet ?? "", + ]), + ]; + + return rows.map((row) => row.map(escapeCsvCell).join(",")).join("\n"); +} + +export function orderedSeverities(): Severity[] { + return [...SEVERITY_ORDER]; +} + +function createSeverityCounts(findings: Finding[]): Record { + const counts: Record = { + critical: 0, + high: 0, + medium: 0, + low: 0, + }; + + findings.forEach((finding) => { + counts[finding.severity] += 1; + }); + + return counts; +} + +function createSeverityBuckets(findings: Finding[]): Record { + return { + critical: findings.filter((finding) => finding.severity === "critical"), + high: findings.filter((finding) => finding.severity === "high"), + medium: findings.filter((finding) => finding.severity === "medium"), + low: findings.filter((finding) => finding.severity === "low"), + }; +} + +function escapeCsvCell(value: string): string { + const normalized = value.replace(/\r?\n/g, " ").replace(/"/g, '""'); + return `"${normalized}"`; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ba9e0d8..c882de6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -105,6 +105,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -210,7 +211,7 @@ "version": "7.27.1", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -220,7 +221,7 @@ "version": "7.28.5", "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -313,7 +314,7 @@ "version": "7.29.0", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1800,8 +1801,9 @@ "version": "1.58.2", "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.58.2.tgz", "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.58.2" }, @@ -3118,6 +3120,7 @@ "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3323,6 +3326,7 @@ "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3346,6 +3350,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3356,6 +3361,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3426,6 +3432,7 @@ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -4198,6 +4205,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4515,8 +4523,9 @@ "version": "1.0.0", "resolved": "https://registry.npmmirror.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -4613,6 +4622,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5306,6 +5316,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -5383,6 +5394,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5568,6 +5580,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6329,6 +6342,7 @@ "integrity": "sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" @@ -8172,7 +8186,7 @@ "version": "1.58.2", "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.58.2.tgz", "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.58.2" @@ -8191,8 +8205,9 @@ "version": "1.58.2", "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.58.2.tgz", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -8365,6 +8380,7 @@ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8419,6 +8435,7 @@ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8596,6 +8613,7 @@ "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9026,6 +9044,7 @@ "integrity": "sha512-p8seiSI6FiVY6P3V0pG+5v7c8pDMehMAFRWEhG5XqIBSQszzOjDnW2rNvm3odoLKfo3V3P6Cs6Hv9ILzymULyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/core": "8.6.18" }, @@ -9625,6 +9644,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9817,6 +9837,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -10367,6 +10388,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }