diff --git a/app/figma-plugin/src/ui.template.html b/app/figma-plugin/src/ui.template.html index 8fe745ab..838cbcb7 100644 --- a/app/figma-plugin/src/ui.template.html +++ b/app/figma-plugin/src/ui.template.html @@ -146,6 +146,7 @@

Ready to analyze

maxDepth: result.maxDepth, }); el.className = 'visible'; + CanICode.initReportInteractions(el); document.getElementById('footer').style.display = ''; } diff --git a/app/shared/styles.css b/app/shared/styles.css index 1e8ea33f..ae6c5a7b 100644 --- a/app/shared/styles.css +++ b/app/shared/styles.css @@ -68,11 +68,11 @@ body { ================================================================ */ /* ---- Details/summary reset ---- */ -.rpt-cat > summary::-webkit-details-marker, +.rpt-rule > summary::-webkit-details-marker, .rpt-issue > summary::-webkit-details-marker { display: none; } -.rpt-cat > summary::marker, +.rpt-rule > summary::marker, .rpt-issue > summary::marker { content: ""; } -.rpt-cat > summary, +.rpt-rule > summary, .rpt-issue > summary { list-style: none; } /* ---- Overall Score ---- */ @@ -115,10 +115,12 @@ body { display: flex; flex-direction: column; align-items: center; - position: relative; cursor: pointer; text-decoration: none; color: var(--fg); + background: none; + border: none; + padding: 4px; transition: opacity 0.15s; } .rpt-gauge-item:hover { opacity: 0.8; } @@ -270,25 +272,80 @@ body { } .rpt-opps-link:hover { color: var(--fg); } -/* ---- Categories ---- */ -.rpt-cats > * + * { - margin-top: 12px; +/* ---- Category Tabs (shadcn pill style) ---- */ +.rpt-tabs { + margin-bottom: 24px; + position: relative; } -.rpt-cat { - overflow: hidden; +@media (max-width: 600px) { + .rpt-tab-list::after { + content: ""; + position: sticky; + right: 0; + width: 40px; + min-height: 100%; + background: linear-gradient(to right, transparent, var(--bg)); + pointer-events: none; + flex-shrink: 0; + margin-left: -40px; + } } -.rpt-cat-header { - padding: 14px 20px; +.rpt-tab-list { display: flex; - align-items: center; - gap: 12px; + gap: 4px; + padding: 4px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow-x: auto; + white-space: nowrap; + -webkit-overflow-scrolling: touch; +} +.rpt-tab-list::-webkit-scrollbar { display: none; } +.rpt-tab-list { scrollbar-width: none; } +.rpt-tab { + padding: 6px 12px; + font-size: 13px; + font-weight: 500; + color: var(--fg-muted); + background: transparent; + border: none; + border-radius: 6px; cursor: pointer; - transition: background 0.15s; - user-select: none; + transition: all 0.15s; + white-space: nowrap; + flex-shrink: 0; +} +.rpt-tab:hover { color: var(--fg); background: rgba(0,0,0,0.04); } +.rpt-tab.active { + color: var(--fg); + background: var(--card); + box-shadow: 0 1px 2px rgba(0,0,0,0.06); +} +.rpt-tab-count { + font-size: 11px; + color: var(--fg-muted); + margin-left: 2px; +} +.rpt-tab.active .rpt-tab-count { color: var(--fg-muted); } +.rpt-tab-panel { display: none; padding-top: 16px; } +.rpt-tab-panel.active { display: block; } +.rpt-cat-empty { + padding: 24px 20px; + font-size: 14px; + color: var(--green); + font-weight: 500; + text-align: center; +} + +/* ---- Gauge item active highlight ---- */ +.rpt-gauge-item.active { + opacity: 1; + background: rgba(0,0,0,0.04); + border-radius: var(--radius); } -.rpt-cat-header:hover { background: rgba(0,0,0,0.02); } -/* Score badge */ +/* ---- Score badge ---- */ .rpt-badge { display: inline-flex; align-items: center; @@ -306,65 +363,76 @@ body { .rpt-badge.score-amber { background: var(--amber-bg); color: #b45309; border-color: rgba(245,158,11,0.2); } .rpt-badge.score-red { background: var(--red-bg); color: #b91c1c; border-color: rgba(239,68,68,0.2); } -.rpt-cat-info { - flex: 1; - min-width: 0; -} -.rpt-cat-name { - font-size: 14px; - font-weight: 600; +/* ---- Rule Section ---- */ +.rpt-tab-panel > .rpt-rule + .rpt-rule { + margin-top: 12px; } -.rpt-cat-desc { - font-size: 12px; - color: var(--fg-muted); +.rpt-rule { + overflow: hidden; } -.rpt-cat-count { - font-size: 12px; - color: var(--fg-muted); - white-space: nowrap; +.rpt-rule-header { + padding: 14px 20px; + display: flex; + align-items: flex-start; + gap: 12px; + cursor: pointer; + user-select: none; + transition: background 0.15s; } -.rpt-cat-chevron { +.rpt-rule-header:hover { background: rgba(0,0,0,0.02); } +.rpt-rule-chevron { width: 16px; height: 16px; color: var(--fg-muted); transition: transform 0.2s; flex-shrink: 0; + margin-top: 2px; + margin-left: auto; } -.rpt-cat[open] > .rpt-cat-header .rpt-cat-chevron { +.rpt-rule[open] > .rpt-rule-header .rpt-rule-chevron { transform: rotate(180deg); } -.rpt-cat-body { - border-top: 1px solid var(--border); -} -.rpt-cat-empty { - padding: 16px 20px; +.rpt-rule-name { font-size: 14px; - color: var(--green); - font-weight: 500; -} - -/* ---- Severity Group ---- */ -.rpt-sev-group { - padding: 12px 20px; + font-weight: 600; + display: block; } -.rpt-sev-header { +.rpt-rule-meta { + font-size: 12px; + color: var(--fg-muted); display: flex; align-items: center; - gap: 8px; - margin-bottom: 8px; + gap: 6px; + margin-top: 2px; } -.rpt-sev-label { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; +.rpt-rule-title { + display: block; + flex: 1; + min-width: 0; } -.rpt-sev-count { +.rpt-rule-info { + display: block; + padding: 8px 0 4px; font-size: 12px; color: var(--fg-muted); - margin-left: auto; + line-height: 1.6; + margin-top: 8px; +} +.rpt-rule-info-line { + display: block; +} +.rpt-rule-info-line + .rpt-rule-info-line { + margin-top: 4px; +} +.rpt-rule-info strong { + color: var(--fg); + font-weight: 500; } -.rpt-sev-issues > * + * { +.rpt-rule-issues { + padding: 8px 12px; + border-top: 1px solid var(--border); +} +.rpt-rule-issues > * + * { margin-top: 4px; } @@ -379,16 +447,11 @@ body { align-items: center; gap: 10px; padding: 8px 12px; - font-size: 14px; + font-size: 13px; cursor: pointer; transition: background 0.15s; } .rpt-issue-header:hover { background: rgba(0,0,0,0.02); } -.rpt-issue-name { - font-weight: 500; - white-space: nowrap; - flex-shrink: 0; -} .rpt-issue-msg { color: var(--fg-muted); font-size: 12px; @@ -415,10 +478,25 @@ body { padding: 12px; background: #fafafa; border-top: 1px solid var(--border); - font-size: 14px; + font-size: 13px; } .rpt-issue-body > * + * { - margin-top: 8px; + margin-top: 6px; +} +.rpt-issue-suggestion { + font-weight: 500; + color: var(--fg); +} +.rpt-issue-guide { + font-size: 12px; + color: var(--fg-muted); + padding: 4px 8px; + background: var(--blue-bg); + border-radius: 4px; + display: inline-block; +} +.rpt-issue-guide::before { + content: "ℹ️ "; } .rpt-issue-path { font-family: var(--font-mono); @@ -426,17 +504,6 @@ body { color: var(--fg-muted); word-break: break-all; } -.rpt-issue-info { - color: var(--fg-muted); - line-height: 1.6; -} -.rpt-issue-info > * + * { - margin-top: 4px; -} -.rpt-issue-info strong { - color: var(--fg); - font-weight: 500; -} .rpt-issue-actions { display: flex; align-items: center; @@ -494,11 +561,12 @@ body { .rpt-gauges { padding: 16px; } .rpt-summary-inner { gap: 12px; } .rpt-summary-total { border-left: none; padding-left: 0; } + .rpt-tab-list { gap: 8px; padding: 4px 12px; } + .rpt-tab { padding: 6px 14px; } .rpt-opps-item { padding: 10px 16px; gap: 10px; } .rpt-opps-bar-wrap { width: 80px; } .rpt-opps-link { font-size: 11px; } - .rpt-cat-header { padding: 10px 14px; gap: 8px; } - .rpt-sev-group { padding: 10px 14px; } + .rpt-rule-header { padding: 10px 14px; gap: 8px; } .rpt-issue-header { padding: 6px 10px; gap: 6px; font-size: 12px; } .rpt-issue-body { padding: 10px; font-size: 12px; } } diff --git a/app/web/src/index.html b/app/web/src/index.html index 494b730e..dd877dce 100644 --- a/app/web/src/index.html +++ b/app/web/src/index.html @@ -364,6 +364,7 @@

Analyze your Figma designs

figmaToken: getToken() || undefined, }); el.classList.add('visible'); + CanICode.initReportInteractions(el); el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } diff --git a/src/browser.ts b/src/browser.ts index 2e88c06e..2fcec2cd 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -34,7 +34,7 @@ export { } from "./core/ui-helpers.js"; // Report rendering (shared with web/plugin) -export { renderReportBody } from "./core/report-html/render.js"; +export { renderReportBody, initReportInteractions } from "./core/report-html/render.js"; export type { ReportData } from "./core/report-html/render.js"; // Import rules to register them with the global registry diff --git a/src/core/contracts/category.ts b/src/core/contracts/category.ts index 2bdfcbb8..62121fcc 100644 --- a/src/core/contracts/category.ts +++ b/src/core/contracts/category.ts @@ -5,8 +5,8 @@ export const CategorySchema = z.enum([ "responsive-critical", "code-quality", "token-management", - "interaction", "minor", + "interaction", ]); export type Category = z.infer; @@ -18,6 +18,6 @@ export const CATEGORY_LABELS: Record = { "responsive-critical": "Responsive Critical", "code-quality": "Code Quality", "token-management": "Token Management", - "interaction": "Interaction", "minor": "Minor", + "interaction": "Interaction", }; diff --git a/src/core/contracts/rule.ts b/src/core/contracts/rule.ts index 5cb211b2..786cb7e5 100644 --- a/src/core/contracts/rule.ts +++ b/src/core/contracts/rule.ts @@ -68,6 +68,8 @@ export interface RuleViolation { nodeId: string; nodePath: string; message: string; + suggestion: string; + guide?: string; } /** diff --git a/src/core/report-html/index.ts b/src/core/report-html/index.ts index 5630cbaa..7bc22d25 100644 --- a/src/core/report-html/index.ts +++ b/src/core/report-html/index.ts @@ -5,14 +5,14 @@ import type { AnalysisFile } from "../contracts/figma-node.js"; import type { AnalysisResult } from "../engine/rule-engine.js"; import type { ScoreReport } from "../engine/scoring.js"; import { escapeHtml } from "../ui-helpers.js"; -import { renderReportBody } from "./render.js"; +import { renderReportBody, initReportInteractions } from "./render.js"; import type { ReportData } from "./render.js"; declare const __REPORT_CSS__: string; const reportCss: string = __REPORT_CSS__; export type { ReportData } from "./render.js"; -export { renderReportBody } from "./render.js"; +export { renderReportBody, initReportInteractions } from "./render.js"; export interface NodeScreenshot { nodeId: string; @@ -63,7 +63,6 @@ export function generateHtmlReport(