@sarjallab09/figma-intelligence 1.0.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/LICENSE +26 -0
- package/README.md +327 -0
- package/bin/cli.js +859 -0
- package/design-bridge/.env.example +5 -0
- package/design-bridge/bridge.js +196 -0
- package/design-bridge/lib/assets.js +367 -0
- package/design-bridge/lib/prompt.js +85 -0
- package/design-bridge/lib/server.js +66 -0
- package/design-bridge/lib/stitch.js +37 -0
- package/design-bridge/lib/tokens.js +82 -0
- package/design-bridge/package-lock.json +579 -0
- package/design-bridge/package.json +19 -0
- package/figma-bridge-plugin/README.md +97 -0
- package/figma-bridge-plugin/anthropic-chat-runner.js +192 -0
- package/figma-bridge-plugin/bridge-relay.js +2363 -0
- package/figma-bridge-plugin/chat-runner.js +459 -0
- package/figma-bridge-plugin/code.js +1528 -0
- package/figma-bridge-plugin/codex-runner.js +505 -0
- package/figma-bridge-plugin/component-schemas.js +110 -0
- package/figma-bridge-plugin/content-context.js +869 -0
- package/figma-bridge-plugin/create-button.js +216 -0
- package/figma-bridge-plugin/gemini-cli-runner.js +291 -0
- package/figma-bridge-plugin/gemini-runner.js +187 -0
- package/figma-bridge-plugin/html-to-figma.js +927 -0
- package/figma-bridge-plugin/knowledge-hub/.gitkeep +0 -0
- package/figma-bridge-plugin/knowledge-hub/uspec-references/anatomy-spec.md +159 -0
- package/figma-bridge-plugin/knowledge-hub/uspec-references/api-spec.md +162 -0
- package/figma-bridge-plugin/knowledge-hub/uspec-references/color-spec.md +148 -0
- package/figma-bridge-plugin/knowledge-hub/uspec-references/full-spec-template.md +314 -0
- package/figma-bridge-plugin/knowledge-hub/uspec-references/property-spec.md +175 -0
- package/figma-bridge-plugin/knowledge-hub/uspec-references/screen-reader-spec.md +180 -0
- package/figma-bridge-plugin/knowledge-hub/uspec-references/structure-spec.md +165 -0
- package/figma-bridge-plugin/manifest.json +21 -0
- package/figma-bridge-plugin/package-lock.json +1936 -0
- package/figma-bridge-plugin/package.json +20 -0
- package/figma-bridge-plugin/perplexity-runner.js +188 -0
- package/figma-bridge-plugin/references/SKILL.md +178 -0
- package/figma-bridge-plugin/references/anatomy-spec.md +159 -0
- package/figma-bridge-plugin/references/api-spec.md +162 -0
- package/figma-bridge-plugin/references/color-spec.md +148 -0
- package/figma-bridge-plugin/references/full-spec-template.md +314 -0
- package/figma-bridge-plugin/references/property-spec.md +175 -0
- package/figma-bridge-plugin/references/screen-reader-spec.md +180 -0
- package/figma-bridge-plugin/references/structure-spec.md +165 -0
- package/figma-bridge-plugin/shared-prompt-config.js +604 -0
- package/figma-bridge-plugin/spec-helpers/build-table.js +269 -0
- package/figma-bridge-plugin/spec-helpers/classify-elements.js +189 -0
- package/figma-bridge-plugin/spec-helpers/index.js +35 -0
- package/figma-bridge-plugin/spec-helpers/parse-figma-link.js +49 -0
- package/figma-bridge-plugin/spec-helpers/position-markers.js +158 -0
- package/figma-bridge-plugin/stitch-auth.js +322 -0
- package/figma-bridge-plugin/stitch-runner.js +1427 -0
- package/figma-bridge-plugin/token-resolver.js +107 -0
- package/figma-bridge-plugin/ui.html +4467 -0
- package/figma-intelligence-layer/.env.example +39 -0
- package/figma-intelligence-layer/docs/local-image-generation.md +60 -0
- package/figma-intelligence-layer/examples/comfyui-workflow-template.example.json +101 -0
- package/figma-intelligence-layer/jest.config.js +14 -0
- package/figma-intelligence-layer/mcp-config.json +19 -0
- package/figma-intelligence-layer/package-lock.json +5892 -0
- package/figma-intelligence-layer/package.json +48 -0
- package/figma-intelligence-layer/scripts/setup-comfyui-local.sh +67 -0
- package/figma-intelligence-layer/scripts/start-comfyui.sh +33 -0
- package/figma-intelligence-layer/src/index.ts +2233 -0
- package/figma-intelligence-layer/src/shared/auto-layout-validator.ts +404 -0
- package/figma-intelligence-layer/src/shared/cache.ts +187 -0
- package/figma-intelligence-layer/src/shared/color-operations.ts +533 -0
- package/figma-intelligence-layer/src/shared/color-utils.ts +138 -0
- package/figma-intelligence-layer/src/shared/component-script-builder.ts +413 -0
- package/figma-intelligence-layer/src/shared/component-templates.ts +2767 -0
- package/figma-intelligence-layer/src/shared/concept-taxonomy.ts +694 -0
- package/figma-intelligence-layer/src/shared/decision-log.ts +128 -0
- package/figma-intelligence-layer/src/shared/design-system-context.ts +568 -0
- package/figma-intelligence-layer/src/shared/design-system-intelligence.ts +131 -0
- package/figma-intelligence-layer/src/shared/design-system-matcher.ts +184 -0
- package/figma-intelligence-layer/src/shared/design-system-normalizers.ts +196 -0
- package/figma-intelligence-layer/src/shared/design-system-tokens.ts +295 -0
- package/figma-intelligence-layer/src/shared/dtcg-validator.ts +530 -0
- package/figma-intelligence-layer/src/shared/enrichment-pipeline.ts +671 -0
- package/figma-intelligence-layer/src/shared/figma-bridge.ts +1408 -0
- package/figma-intelligence-layer/src/shared/font-config.ts +126 -0
- package/figma-intelligence-layer/src/shared/icon-catalog.ts +360 -0
- package/figma-intelligence-layer/src/shared/icon-fetch.ts +80 -0
- package/figma-intelligence-layer/src/shared/prototype-script-builder.ts +162 -0
- package/figma-intelligence-layer/src/shared/response-compression.ts +440 -0
- package/figma-intelligence-layer/src/shared/semantic-token-catalog.ts +324 -0
- package/figma-intelligence-layer/src/shared/token-binder.ts +505 -0
- package/figma-intelligence-layer/src/shared/token-math.ts +427 -0
- package/figma-intelligence-layer/src/shared/token-naming.ts +468 -0
- package/figma-intelligence-layer/src/shared/token-utils.ts +420 -0
- package/figma-intelligence-layer/src/shared/types.ts +346 -0
- package/figma-intelligence-layer/src/shared/typography-presets.ts +94 -0
- package/figma-intelligence-layer/src/shared/unsplash.ts +165 -0
- package/figma-intelligence-layer/src/shared/vision-client.ts +607 -0
- package/figma-intelligence-layer/src/shared/vision-provider-anthropic.ts +334 -0
- package/figma-intelligence-layer/src/shared/vision-provider-openai.ts +446 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-handler.ts +782 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-renderer.ts +496 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotation-kit.ts +230 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/colorblind-sim.ts +66 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/index.ts +810 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-analyzer.ts +1191 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-figma-page.ts +1346 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-handler.ts +148 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-figma-page.ts +499 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-report.ts +910 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-checker.ts +989 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-criteria.ts +1160 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/design-from-ref/index.ts +424 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/component-recognizer.ts +38 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/ds-matcher.ts +111 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/font-matcher.ts +114 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/icon-resolver.ts +103 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/index.ts +1060 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/layout-segmenter.ts +18 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/token-inferencer.ts +39 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/vision-pipeline.ts +58 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/sketch-to-design/index.ts +298 -0
- package/figma-intelligence-layer/src/tools/phase1-vision/visual-audit/index.ts +197 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/component-audit/index.ts +494 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/intent-translator/index.ts +356 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/container-patterns.ts +123 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/index.ts +663 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/built-in-rules.yaml +56 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/index.ts +614 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/rule-engine.ts +113 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/color-theory.ts +178 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/index.ts +470 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/index.ts +429 -0
- package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/token-override-maps.ts +226 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/ai-image-insert/index.ts +535 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/index.ts +660 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/pattern-fingerprints.ts +209 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/composition-builder/index.ts +540 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/figma-animated-build.ts +391 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/index.ts +2019 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/screen-templates.ts +131 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/prototype-map/index.ts +381 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/prototype-wire/index.ts +565 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/swarm-build/index.ts +764 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/system-drift/index.ts +535 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/unsplash-search/index.ts +84 -0
- package/figma-intelligence-layer/src/tools/phase3-generation/url-to-frame/index.ts +401 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/css-animations.ts +68 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/framer-motion.ts +78 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/swift-animations.ts +93 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/index.ts +596 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/ci-check/index.ts +462 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/export-tokens/index.ts +1470 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/generate-component-code/index.ts +829 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/handoff-spec/index.ts +702 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/icon-library-sync/index.ts +483 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/index.ts +501 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/storybook-parser.ts +106 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/watch-docs/index.ts +676 -0
- package/figma-intelligence-layer/src/tools/phase4-sync/webhook-listener/index.ts +560 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/apg-doc/index.ts +1043 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/component-detection.ts +620 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/anatomy.ts +331 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/color-tokens.ts +77 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/properties.ts +54 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/snapshot.ts +287 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/spacing.ts +71 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/states.ts +43 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/typography.ts +71 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/index.ts +221 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/_default.ts +166 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/accordion.ts +232 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/alert.ts +234 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar-group.ts +270 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar.ts +249 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/badge.ts +231 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/banner.ts +293 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/breadcrumb.ts +240 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/button.ts +243 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/calendar.ts +307 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/card.ts +143 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/checkbox.ts +227 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/chip.ts +233 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/combobox.ts +282 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/datepicker.ts +276 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/divider.ts +223 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/drawer.ts +255 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/dropdown-menu.ts +289 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/empty-state.ts +261 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/file-uploader.ts +290 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/form.ts +265 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/grid.ts +238 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/icon.ts +255 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/index.ts +128 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-edit.ts +286 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-message.ts +255 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/input.ts +330 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/link.ts +247 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/list.ts +250 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/menu.ts +247 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/modal.ts +144 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navbar.ts +264 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navigation.ts +251 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/number-input.ts +261 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/pagination.ts +248 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/popover.ts +270 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/progress.ts +251 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/radio.ts +142 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/range-slider.ts +282 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/rating.ts +250 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/search.ts +258 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/segmented-control.ts +265 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/select.ts +319 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/skeleton.ts +256 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/slider.ts +232 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/spinner.ts +239 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/status-dot.ts +252 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/stepper.ts +270 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/table.ts +244 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tabs.ts +143 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tag.ts +243 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/textarea.ts +259 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/time-picker.ts +293 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toast.ts +144 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toggle.ts +289 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toolbar.ts +267 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tooltip.ts +232 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/treeview.ts +257 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/typography.ts +319 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/legacy-compat.ts +121 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/anatomy-diagram.ts +430 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/figma-page.ts +312 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/json.ts +129 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/markdown.ts +78 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/visual-doc.ts +2333 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/accessibility.ts +100 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/anatomy.ts +32 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/color-tokens.ts +59 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/content-guidance.ts +18 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/design-tokens.ts +53 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/interaction-rules.ts +19 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/overview.ts +91 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/properties-api.ts +71 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/qa-criteria.ts +19 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/related-components.ts +110 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/responsive.ts +19 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/size-specs.ts +67 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/spacing-structure.ts +58 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/state-specs.ts +79 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/states.ts +50 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/type-hierarchy.ts +33 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/typography.ts +55 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/usage-guidelines.ts +73 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/variants.ts +81 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/types.ts +409 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/index.ts +198 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/renderer.ts +701 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/types.ts +88 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/decision-log/index.ts +135 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/design-decision-log/index.ts +491 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/ds-primitives/index.ts +416 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/ds-scaffolder/index.ts +722 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/ds-variables/index.ts +449 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/health-report/index.ts +393 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/index.ts +406 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/figma-page.ts +292 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/json.ts +24 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/markdown.ts +172 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/naming-guide.ts +409 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/token-analytics/index.ts +594 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/token-docs/index.ts +710 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/token-migrate/index.ts +458 -0
- package/figma-intelligence-layer/src/tools/phase5-governance/token-naming/index.ts +134 -0
- package/figma-intelligence-layer/tests/apg-doc.test.ts +101 -0
- package/figma-intelligence-layer/tests/design-system-context.test.ts +152 -0
- package/figma-intelligence-layer/tests/design-system-matcher.test.ts +144 -0
- package/figma-intelligence-layer/tests/figma-bridge.test.ts +83 -0
- package/figma-intelligence-layer/tests/generate-image-and-insert.test.ts +56 -0
- package/figma-intelligence-layer/tests/screen-cloner-regression.test.ts +69 -0
- package/figma-intelligence-layer/tests/smoke.test.ts +174 -0
- package/figma-intelligence-layer/tests/spec-generator.test.ts +127 -0
- package/figma-intelligence-layer/tests/token-migrate.test.ts +21 -0
- package/figma-intelligence-layer/tests/token-naming.test.ts +30 -0
- package/figma-intelligence-layer/tsconfig.json +19 -0
- package/package.json +35 -0
- package/scripts/clean-existing-chunks.js +179 -0
- package/scripts/connect-ai-tool.js +490 -0
- package/scripts/convert-hub-pdfs.js +425 -0
- package/scripts/figma-mcp-status.js +349 -0
- package/scripts/register-codex-mcp.js +96 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: no-hardcoded-colors
|
|
3
|
+
severity: error
|
|
4
|
+
description: "All colors must reference a design token variable, not a hardcoded hex value"
|
|
5
|
+
applies-to: "all"
|
|
6
|
+
check: "fills.every(f => f.type === 'VARIABLE_ALIAS') && strokes.every(s => s.type === 'VARIABLE_ALIAS')"
|
|
7
|
+
autofix: "Map fill/stroke to nearest color token by hex distance"
|
|
8
|
+
|
|
9
|
+
- id: spacing-on-grid
|
|
10
|
+
severity: warning
|
|
11
|
+
description: "All spacing values (padding, gap) must be multiples of 4px"
|
|
12
|
+
applies-to: "frames and auto-layout nodes"
|
|
13
|
+
check: "(paddingLeft % 4 === 0) && (paddingRight % 4 === 0) && (paddingTop % 4 === 0) && (paddingBottom % 4 === 0) && (itemSpacing % 4 === 0)"
|
|
14
|
+
autofix: "Round each spacing value to nearest 4px multiple"
|
|
15
|
+
|
|
16
|
+
- id: button-min-touch-target
|
|
17
|
+
severity: error
|
|
18
|
+
description: "Interactive components (Buttons, Links, Icon buttons) must be at least 44x44px for WCAG 2.5.5 compliance"
|
|
19
|
+
applies-to: "components matching 'Button*' or 'IconButton*' or 'Link*'"
|
|
20
|
+
check: "node.width >= 44 && node.height >= 44"
|
|
21
|
+
autofix: false
|
|
22
|
+
|
|
23
|
+
- id: component-must-be-instance
|
|
24
|
+
severity: error
|
|
25
|
+
description: "Interactive UI elements must be DS component instances, not raw frames"
|
|
26
|
+
applies-to: "nodes matching interactive patterns (button, input, select, checkbox, toggle)"
|
|
27
|
+
check: "node.type === 'INSTANCE'"
|
|
28
|
+
autofix: false
|
|
29
|
+
|
|
30
|
+
- id: no-detached-text
|
|
31
|
+
severity: warning
|
|
32
|
+
description: "Text color fills must use variable aliases, not hardcoded values"
|
|
33
|
+
applies-to: "TEXT nodes"
|
|
34
|
+
check: "fills.every(f => f.type === 'VARIABLE_ALIAS')"
|
|
35
|
+
autofix: "Map text color to nearest semantic text token"
|
|
36
|
+
|
|
37
|
+
- id: image-needs-description
|
|
38
|
+
severity: warning
|
|
39
|
+
description: "Image layers should have an alt text annotation for accessibility documentation"
|
|
40
|
+
applies-to: "nodes with image fills"
|
|
41
|
+
check: "node.pluginData?.altText?.length > 0"
|
|
42
|
+
autofix: false
|
|
43
|
+
|
|
44
|
+
- id: no-mixed-radii
|
|
45
|
+
severity: warning
|
|
46
|
+
description: "Border radius values should use token references, not mixed hardcoded values"
|
|
47
|
+
applies-to: "frames and components with cornerRadius"
|
|
48
|
+
check: "node.boundVariables?.cornerRadius !== undefined || node.cornerRadius === 0"
|
|
49
|
+
autofix: "Map to nearest radius token"
|
|
50
|
+
|
|
51
|
+
- id: consistent-elevation
|
|
52
|
+
severity: warning
|
|
53
|
+
description: "Drop shadow effects should match a defined elevation token"
|
|
54
|
+
applies-to: "nodes with DROP_SHADOW effects"
|
|
55
|
+
check: "effects.every(e => e.type !== 'DROP_SHADOW' || node.boundVariables?.effects !== undefined)"
|
|
56
|
+
autofix: false
|
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Lint Rules
|
|
3
|
+
// Define, run, list, and delete design-system lint rules against a Figma file.
|
|
4
|
+
// Supports 5 built-in rules and user-defined rules loaded from a YAML file.
|
|
5
|
+
// Auto-fix snaps spacing to 4-px grid and maps hardcoded colors to the nearest
|
|
6
|
+
// DS token.
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
import fs from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import yaml from "js-yaml";
|
|
12
|
+
import { getBridge } from "../../../shared/figma-bridge.js";
|
|
13
|
+
import { decisionLog } from "../../../shared/decision-log.js";
|
|
14
|
+
import { snapToSpacingToken, snapToColorToken } from "../../../shared/token-utils.js";
|
|
15
|
+
import { FigmaNode, LintViolation, Paint, Token } from "../../../shared/types.js";
|
|
16
|
+
|
|
17
|
+
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export type LintAction = "define" | "run" | "list" | "delete";
|
|
20
|
+
export type OutputFormat = "inline" | "report" | "ci";
|
|
21
|
+
|
|
22
|
+
export interface LintRulesArgs {
|
|
23
|
+
action: LintAction;
|
|
24
|
+
ruleFile?: string;
|
|
25
|
+
nodeId?: string;
|
|
26
|
+
autoFix?: boolean;
|
|
27
|
+
outputFormat: OutputFormat;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LintRule {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
severity: "error" | "warning";
|
|
35
|
+
autoFixable: boolean;
|
|
36
|
+
builtIn: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LintRuleSet {
|
|
40
|
+
rules: LintRule[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LintRunResult {
|
|
44
|
+
violations: LintViolation[];
|
|
45
|
+
totalChecked: number;
|
|
46
|
+
errorCount: number;
|
|
47
|
+
warningCount: number;
|
|
48
|
+
autoFixedCount: number;
|
|
49
|
+
exitCode: number; // 0 = pass, 1 = warnings, 2 = errors (CI-ready)
|
|
50
|
+
outputText: string;
|
|
51
|
+
logEntryId: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface LintDefineResult {
|
|
55
|
+
savedPath: string;
|
|
56
|
+
ruleCount: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface LintListResult {
|
|
60
|
+
rules: LintRule[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface LintDeleteResult {
|
|
64
|
+
deletedRuleIds: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type LintRulesResult = LintRunResult | LintDefineResult | LintListResult | LintDeleteResult;
|
|
68
|
+
|
|
69
|
+
// ─── Built-in rules ───────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export const BUILT_IN_RULES: LintRule[] = [
|
|
72
|
+
{
|
|
73
|
+
id: "no-hardcoded-colors",
|
|
74
|
+
name: "No Hardcoded Colors",
|
|
75
|
+
description: "All fills must use variable aliases rather than raw color values.",
|
|
76
|
+
severity: "error",
|
|
77
|
+
autoFixable: true,
|
|
78
|
+
builtIn: true,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "spacing-on-grid",
|
|
82
|
+
name: "Spacing On 4px Grid",
|
|
83
|
+
description: "All padding and gap values must be multiples of 4px.",
|
|
84
|
+
severity: "warning",
|
|
85
|
+
autoFixable: true,
|
|
86
|
+
builtIn: true,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "button-min-touch-target",
|
|
90
|
+
name: "Button Minimum Touch Target",
|
|
91
|
+
description: "Button components must be at least 44×44px to meet touch accessibility guidelines.",
|
|
92
|
+
severity: "error",
|
|
93
|
+
autoFixable: false,
|
|
94
|
+
builtIn: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "component-must-be-instance",
|
|
98
|
+
name: "Interactive Elements Must Be DS Instances",
|
|
99
|
+
description: "Interactive elements (buttons, inputs, links) must be instances of DS components.",
|
|
100
|
+
severity: "error",
|
|
101
|
+
autoFixable: false,
|
|
102
|
+
builtIn: true,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "no-detached-text",
|
|
106
|
+
name: "No Detached Text Styles",
|
|
107
|
+
description: "Text layers must use variable aliases for their color fills.",
|
|
108
|
+
severity: "warning",
|
|
109
|
+
autoFixable: true,
|
|
110
|
+
builtIn: true,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
// ─── Persistent rule-store path ───────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
const RULES_STORE_PATH =
|
|
117
|
+
process.env.LINT_RULES_PATH ||
|
|
118
|
+
path.join(process.cwd(), ".figma-lint-rules.json");
|
|
119
|
+
|
|
120
|
+
// ─── Rule store helpers ───────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
async function loadStoredRules(): Promise<LintRule[]> {
|
|
123
|
+
try {
|
|
124
|
+
const data = await fs.readFile(RULES_STORE_PATH, "utf-8");
|
|
125
|
+
const parsed = JSON.parse(data) as { rules: LintRule[] };
|
|
126
|
+
return parsed.rules ?? [];
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function saveStoredRules(rules: LintRule[]): Promise<void> {
|
|
133
|
+
await fs.writeFile(
|
|
134
|
+
RULES_STORE_PATH,
|
|
135
|
+
JSON.stringify({ rules }, null, 2),
|
|
136
|
+
"utf-8"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function getAllRules(): Promise<LintRule[]> {
|
|
141
|
+
const stored = await loadStoredRules();
|
|
142
|
+
const storedIds = new Set(stored.map((r) => r.id));
|
|
143
|
+
// Merge: built-in rules first, then any stored custom rules
|
|
144
|
+
const customRules = stored.filter((r) => !r.builtIn);
|
|
145
|
+
const overriddenBuiltIn = BUILT_IN_RULES.map((br) => {
|
|
146
|
+
const override = stored.find((r) => r.id === br.id);
|
|
147
|
+
return override ?? br;
|
|
148
|
+
});
|
|
149
|
+
return [...overriddenBuiltIn, ...customRules];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── YAML rule loader ─────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
interface YamlRuleDef {
|
|
155
|
+
id: string;
|
|
156
|
+
name?: string;
|
|
157
|
+
description?: string;
|
|
158
|
+
severity?: "error" | "warning";
|
|
159
|
+
autoFixable?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function loadRulesFromYaml(filePath: string): Promise<LintRule[]> {
|
|
163
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
164
|
+
const parsed = yaml.load(content) as { rules?: YamlRuleDef[] };
|
|
165
|
+
|
|
166
|
+
if (!parsed?.rules || !Array.isArray(parsed.rules)) {
|
|
167
|
+
throw new Error(`lintRules: YAML file at "${filePath}" must contain a top-level "rules" array.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return parsed.rules.map((r) => ({
|
|
171
|
+
id: r.id,
|
|
172
|
+
name: r.name ?? r.id,
|
|
173
|
+
description: r.description ?? "",
|
|
174
|
+
severity: r.severity ?? "warning",
|
|
175
|
+
autoFixable: r.autoFixable ?? false,
|
|
176
|
+
builtIn: false,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Node traversal ───────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
function collectAllNodes(root: FigmaNode): FigmaNode[] {
|
|
183
|
+
const nodes: FigmaNode[] = [root];
|
|
184
|
+
if (root.children) {
|
|
185
|
+
for (const child of root.children) {
|
|
186
|
+
nodes.push(...collectAllNodes(child));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return nodes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Rule checks ──────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function hasHardcodedColor(fills: Paint[] | undefined): boolean {
|
|
195
|
+
if (!fills || fills.length === 0) return false;
|
|
196
|
+
return fills.some(
|
|
197
|
+
(f) => f.type === "SOLID" && !f.variableId
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isSpacingOffGrid(value: number | undefined): boolean {
|
|
202
|
+
if (value === undefined || value === 0) return false;
|
|
203
|
+
return value % 4 !== 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isButton(node: FigmaNode): boolean {
|
|
207
|
+
const name = node.name.toLowerCase();
|
|
208
|
+
return (
|
|
209
|
+
/\bbtn\b|\bbutton\b|\bcta\b/.test(name) ||
|
|
210
|
+
node.type === "INSTANCE" ||
|
|
211
|
+
node.type === "COMPONENT"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isInteractiveButNotInstance(node: FigmaNode): boolean {
|
|
216
|
+
const name = node.name.toLowerCase();
|
|
217
|
+
const isInteractive = /\bbtn\b|\bbutton\b|\binput\b|\blink\b|\bcheckbox\b|\bradio\b|\bswitch\b/.test(name);
|
|
218
|
+
return isInteractive && node.type !== "INSTANCE" && node.type !== "COMPONENT";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function hasDetachedTextColor(node: FigmaNode): boolean {
|
|
222
|
+
if (node.type !== "TEXT") return false;
|
|
223
|
+
return hasHardcodedColor(node.fills);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─── Auto-fix builders ────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
function buildSnapSpacingScript(nodeId: string, snappedPadding: number, snappedGap: number): string {
|
|
229
|
+
return `
|
|
230
|
+
(async () => {
|
|
231
|
+
const node = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
|
|
232
|
+
if (!node) return;
|
|
233
|
+
const fields = ['paddingLeft','paddingRight','paddingTop','paddingBottom'];
|
|
234
|
+
for (const f of fields) {
|
|
235
|
+
if (f in node && typeof node[f] === 'number' && node[f] % 4 !== 0) {
|
|
236
|
+
node[f] = ${snappedPadding};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if ('itemSpacing' in node && typeof node.itemSpacing === 'number' && node.itemSpacing % 4 !== 0) {
|
|
240
|
+
node.itemSpacing = ${snappedGap};
|
|
241
|
+
}
|
|
242
|
+
return { fixed: true };
|
|
243
|
+
})()
|
|
244
|
+
`.trim();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildMapColorToTokenScript(nodeId: string, tokenName: string): string {
|
|
248
|
+
return `
|
|
249
|
+
(async () => {
|
|
250
|
+
// Color-to-token mapping requires a pre-existing variable;
|
|
251
|
+
// we annotate the node with a plugin data note for human review.
|
|
252
|
+
const node = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
|
|
253
|
+
if (!node) return;
|
|
254
|
+
node.setPluginData('suggestedColorToken', ${JSON.stringify(tokenName)});
|
|
255
|
+
return { annotated: true, tokenName: ${JSON.stringify(tokenName)} };
|
|
256
|
+
})()
|
|
257
|
+
`.trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Violation checker ────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
async function runChecks(
|
|
263
|
+
nodes: FigmaNode[],
|
|
264
|
+
rules: LintRule[],
|
|
265
|
+
autoFix: boolean,
|
|
266
|
+
dsTokens: Token[],
|
|
267
|
+
bridge: Awaited<ReturnType<typeof getBridge>>
|
|
268
|
+
): Promise<{ violations: LintViolation[]; autoFixedCount: number }> {
|
|
269
|
+
const violations: LintViolation[] = [];
|
|
270
|
+
let autoFixedCount = 0;
|
|
271
|
+
|
|
272
|
+
const activeRuleIds = new Set(rules.map((r) => r.id));
|
|
273
|
+
|
|
274
|
+
for (const node of nodes) {
|
|
275
|
+
// ── no-hardcoded-colors ─────────────────────────────────────────────────
|
|
276
|
+
if (activeRuleIds.has("no-hardcoded-colors") && hasHardcodedColor(node.fills)) {
|
|
277
|
+
const rule = rules.find((r) => r.id === "no-hardcoded-colors")!;
|
|
278
|
+
let fixed = false;
|
|
279
|
+
|
|
280
|
+
if (autoFix && rule.autoFixable) {
|
|
281
|
+
// Find the first hardcoded fill and suggest the nearest token
|
|
282
|
+
const hardcoded = node.fills!.find((f) => f.type === "SOLID" && !f.variableId && f.color);
|
|
283
|
+
if (hardcoded?.color) {
|
|
284
|
+
const { r, g, b } = hardcoded.color;
|
|
285
|
+
const hex = `#${Math.round(r * 255).toString(16).padStart(2, "0")}${Math.round(g * 255).toString(16).padStart(2, "0")}${Math.round(b * 255).toString(16).padStart(2, "0")}`;
|
|
286
|
+
const tokenRef = snapToColorToken(hex, dsTokens);
|
|
287
|
+
const fixScript = buildMapColorToTokenScript(node.id, tokenRef.tokenName);
|
|
288
|
+
const fixResult = await bridge.execute(fixScript);
|
|
289
|
+
if (fixResult.success) {
|
|
290
|
+
fixed = true;
|
|
291
|
+
autoFixedCount++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
violations.push({
|
|
297
|
+
ruleId: "no-hardcoded-colors",
|
|
298
|
+
severity: rule.severity,
|
|
299
|
+
nodeId: node.id,
|
|
300
|
+
nodeName: node.name,
|
|
301
|
+
message: `Node "${node.name}" has hardcoded fill colors. All fills must use variable aliases.`,
|
|
302
|
+
autoFixed: fixed,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── spacing-on-grid ──────────────────────────────────────────────────────
|
|
307
|
+
if (activeRuleIds.has("spacing-on-grid")) {
|
|
308
|
+
const rule = rules.find((r) => r.id === "spacing-on-grid")!;
|
|
309
|
+
const offGridFields = [
|
|
310
|
+
{ field: "paddingLeft", value: node.paddingLeft },
|
|
311
|
+
{ field: "paddingRight", value: node.paddingRight },
|
|
312
|
+
{ field: "paddingTop", value: node.paddingTop },
|
|
313
|
+
{ field: "paddingBottom", value: node.paddingBottom },
|
|
314
|
+
{ field: "itemSpacing", value: node.itemSpacing },
|
|
315
|
+
].filter((f) => isSpacingOffGrid(f.value));
|
|
316
|
+
|
|
317
|
+
if (offGridFields.length > 0) {
|
|
318
|
+
let fixed = false;
|
|
319
|
+
|
|
320
|
+
if (autoFix && rule.autoFixable) {
|
|
321
|
+
const paddingRef = snapToSpacingToken(node.paddingLeft ?? 0);
|
|
322
|
+
const gapRef = snapToSpacingToken(node.itemSpacing ?? 0);
|
|
323
|
+
const fixScript = buildSnapSpacingScript(
|
|
324
|
+
node.id,
|
|
325
|
+
paddingRef.tokenValue as number,
|
|
326
|
+
gapRef.tokenValue as number
|
|
327
|
+
);
|
|
328
|
+
const fixResult = await bridge.execute(fixScript);
|
|
329
|
+
if (fixResult.success) {
|
|
330
|
+
fixed = true;
|
|
331
|
+
autoFixedCount++;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
violations.push({
|
|
336
|
+
ruleId: "spacing-on-grid",
|
|
337
|
+
severity: rule.severity,
|
|
338
|
+
nodeId: node.id,
|
|
339
|
+
nodeName: node.name,
|
|
340
|
+
message: `Node "${node.name}" has spacing values not on the 4px grid: ${offGridFields.map((f) => `${f.field}=${f.value}`).join(", ")}.`,
|
|
341
|
+
autoFixed: fixed,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── button-min-touch-target ──────────────────────────────────────────────
|
|
347
|
+
if (activeRuleIds.has("button-min-touch-target") && isButton(node)) {
|
|
348
|
+
const rule = rules.find((r) => r.id === "button-min-touch-target")!;
|
|
349
|
+
const w = node.absoluteBoundingBox?.width ?? node.width ?? 0;
|
|
350
|
+
const h = node.absoluteBoundingBox?.height ?? node.height ?? 0;
|
|
351
|
+
|
|
352
|
+
if (w < 44 || h < 44) {
|
|
353
|
+
violations.push({
|
|
354
|
+
ruleId: "button-min-touch-target",
|
|
355
|
+
severity: rule.severity,
|
|
356
|
+
nodeId: node.id,
|
|
357
|
+
nodeName: node.name,
|
|
358
|
+
message: `Button "${node.name}" is ${Math.round(w)}×${Math.round(h)}px — must be ≥ 44×44px for touch accessibility.`,
|
|
359
|
+
autoFixed: false,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── component-must-be-instance ───────────────────────────────────────────
|
|
365
|
+
if (activeRuleIds.has("component-must-be-instance") && isInteractiveButNotInstance(node)) {
|
|
366
|
+
const rule = rules.find((r) => r.id === "component-must-be-instance")!;
|
|
367
|
+
violations.push({
|
|
368
|
+
ruleId: "component-must-be-instance",
|
|
369
|
+
severity: rule.severity,
|
|
370
|
+
nodeId: node.id,
|
|
371
|
+
nodeName: node.name,
|
|
372
|
+
message: `Interactive element "${node.name}" (type: ${node.type}) is not a DS component instance.`,
|
|
373
|
+
autoFixed: false,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── no-detached-text ─────────────────────────────────────────────────────
|
|
378
|
+
if (activeRuleIds.has("no-detached-text") && hasDetachedTextColor(node)) {
|
|
379
|
+
const rule = rules.find((r) => r.id === "no-detached-text")!;
|
|
380
|
+
let fixed = false;
|
|
381
|
+
|
|
382
|
+
if (autoFix && rule.autoFixable) {
|
|
383
|
+
const hardcoded = node.fills?.find((f) => f.type === "SOLID" && !f.variableId && f.color);
|
|
384
|
+
if (hardcoded?.color) {
|
|
385
|
+
const { r, g, b } = hardcoded.color;
|
|
386
|
+
const hex = `#${Math.round(r * 255).toString(16).padStart(2, "0")}${Math.round(g * 255).toString(16).padStart(2, "0")}${Math.round(b * 255).toString(16).padStart(2, "0")}`;
|
|
387
|
+
const tokenRef = snapToColorToken(hex, dsTokens);
|
|
388
|
+
const fixScript = buildMapColorToTokenScript(node.id, tokenRef.tokenName);
|
|
389
|
+
const fixResult = await bridge.execute(fixScript);
|
|
390
|
+
if (fixResult.success) {
|
|
391
|
+
fixed = true;
|
|
392
|
+
autoFixedCount++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
violations.push({
|
|
398
|
+
ruleId: "no-detached-text",
|
|
399
|
+
severity: rule.severity,
|
|
400
|
+
nodeId: node.id,
|
|
401
|
+
nodeName: node.name,
|
|
402
|
+
message: `Text node "${node.name}" uses a hardcoded color fill instead of a variable alias.`,
|
|
403
|
+
autoFixed: fixed,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { violations, autoFixedCount };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ─── Output formatters ────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
function formatOutput(
|
|
414
|
+
violations: LintViolation[],
|
|
415
|
+
format: OutputFormat,
|
|
416
|
+
totalChecked: number,
|
|
417
|
+
autoFixedCount: number
|
|
418
|
+
): string {
|
|
419
|
+
if (format === "ci") {
|
|
420
|
+
const errors = violations.filter((v) => v.severity === "error" && !v.autoFixed);
|
|
421
|
+
const warnings = violations.filter((v) => v.severity === "warning" && !v.autoFixed);
|
|
422
|
+
const lines = [
|
|
423
|
+
`=== Figma Lint Report ===`,
|
|
424
|
+
`Nodes checked : ${totalChecked}`,
|
|
425
|
+
`Errors : ${errors.length}`,
|
|
426
|
+
`Warnings : ${warnings.length}`,
|
|
427
|
+
`Auto-fixed : ${autoFixedCount}`,
|
|
428
|
+
``,
|
|
429
|
+
];
|
|
430
|
+
for (const v of violations) {
|
|
431
|
+
const icon = v.autoFixed ? "✓" : v.severity === "error" ? "✗" : "⚠";
|
|
432
|
+
lines.push(`[${icon}] [${v.ruleId}] ${v.nodeName}: ${v.message}`);
|
|
433
|
+
}
|
|
434
|
+
return lines.join("\n");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (format === "report") {
|
|
438
|
+
const grouped: Record<string, LintViolation[]> = {};
|
|
439
|
+
for (const v of violations) {
|
|
440
|
+
(grouped[v.ruleId] = grouped[v.ruleId] ?? []).push(v);
|
|
441
|
+
}
|
|
442
|
+
const lines = [`# Lint Report\n`];
|
|
443
|
+
for (const [ruleId, vs] of Object.entries(grouped)) {
|
|
444
|
+
lines.push(`## ${ruleId} (${vs.length} violation${vs.length !== 1 ? "s" : ""})`);
|
|
445
|
+
for (const v of vs) {
|
|
446
|
+
const badge = v.autoFixed ? " [auto-fixed]" : "";
|
|
447
|
+
lines.push(`- **${v.severity.toUpperCase()}** \`${v.nodeId}\` — ${v.message}${badge}`);
|
|
448
|
+
}
|
|
449
|
+
lines.push("");
|
|
450
|
+
}
|
|
451
|
+
return lines.join("\n");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// inline (default)
|
|
455
|
+
return violations
|
|
456
|
+
.map((v) => {
|
|
457
|
+
const badge = v.autoFixed ? " [auto-fixed]" : "";
|
|
458
|
+
return `[${v.severity.toUpperCase()}] ${v.ruleId} @ "${v.nodeName}" (${v.nodeId}): ${v.message}${badge}`;
|
|
459
|
+
})
|
|
460
|
+
.join("\n");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ─── Action implementations ───────────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
async function actionDefine(args: LintRulesArgs): Promise<LintDefineResult> {
|
|
466
|
+
if (!args.ruleFile) {
|
|
467
|
+
throw new Error("lintRules:define requires `ruleFile` pointing to a YAML file.");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const newRules = await loadRulesFromYaml(args.ruleFile);
|
|
471
|
+
const existing = await loadStoredRules();
|
|
472
|
+
|
|
473
|
+
// Merge: new rules overwrite existing rules with the same id
|
|
474
|
+
const merged = [...existing];
|
|
475
|
+
for (const nr of newRules) {
|
|
476
|
+
const idx = merged.findIndex((r) => r.id === nr.id);
|
|
477
|
+
if (idx >= 0) {
|
|
478
|
+
merged[idx] = nr;
|
|
479
|
+
} else {
|
|
480
|
+
merged.push(nr);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
await saveStoredRules(merged);
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
savedPath: RULES_STORE_PATH,
|
|
488
|
+
ruleCount: merged.length,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function actionList(): Promise<LintListResult> {
|
|
493
|
+
const rules = await getAllRules();
|
|
494
|
+
return { rules };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function actionDelete(args: LintRulesArgs): Promise<LintDeleteResult> {
|
|
498
|
+
if (!args.ruleFile) {
|
|
499
|
+
throw new Error("lintRules:delete requires `ruleFile` containing rule IDs to delete.");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const toDelete = await loadRulesFromYaml(args.ruleFile);
|
|
503
|
+
const deleteIds = new Set(toDelete.map((r) => r.id));
|
|
504
|
+
const existing = await loadStoredRules();
|
|
505
|
+
const filtered = existing.filter((r) => !deleteIds.has(r.id));
|
|
506
|
+
await saveStoredRules(filtered);
|
|
507
|
+
|
|
508
|
+
return { deletedRuleIds: [...deleteIds] };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function actionRun(args: LintRulesArgs): Promise<LintRunResult> {
|
|
512
|
+
const bridge = await getBridge();
|
|
513
|
+
const rules = await getAllRules();
|
|
514
|
+
const dsTokens = await bridge.getTokens();
|
|
515
|
+
|
|
516
|
+
// Fetch the node tree (or root if nodeId is not specified)
|
|
517
|
+
let rootNode: FigmaNode;
|
|
518
|
+
|
|
519
|
+
if (args.nodeId) {
|
|
520
|
+
rootNode = await bridge.getNode(args.nodeId);
|
|
521
|
+
} else {
|
|
522
|
+
// Traverse all pages
|
|
523
|
+
const execResult = await bridge.execute(`
|
|
524
|
+
var allNodes = [];
|
|
525
|
+
var page = figma.currentPage;
|
|
526
|
+
var nodes = page.findAll(function() { return true; });
|
|
527
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
528
|
+
var n = nodes[i];
|
|
529
|
+
allNodes.push({
|
|
530
|
+
id: n.id,
|
|
531
|
+
name: n.name,
|
|
532
|
+
type: n.type,
|
|
533
|
+
fills: n.fills || [],
|
|
534
|
+
absoluteBoundingBox: n.absoluteBoundingBox,
|
|
535
|
+
paddingLeft: n.paddingLeft,
|
|
536
|
+
paddingRight: n.paddingRight,
|
|
537
|
+
paddingTop: n.paddingTop,
|
|
538
|
+
paddingBottom: n.paddingBottom,
|
|
539
|
+
itemSpacing: n.itemSpacing,
|
|
540
|
+
layoutMode: n.layoutMode,
|
|
541
|
+
opacity: n.opacity,
|
|
542
|
+
width: n.width,
|
|
543
|
+
height: n.height,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return { id: 'root', name: 'Root', type: 'DOCUMENT', children: allNodes };
|
|
547
|
+
`);
|
|
548
|
+
|
|
549
|
+
if (!execResult.success) {
|
|
550
|
+
throw new Error(`lintRules: Failed to fetch node tree: ${execResult.error}`);
|
|
551
|
+
}
|
|
552
|
+
rootNode = execResult.result as FigmaNode;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const nodes = collectAllNodes(rootNode);
|
|
556
|
+
|
|
557
|
+
const { violations, autoFixedCount } = await runChecks(
|
|
558
|
+
nodes,
|
|
559
|
+
rules,
|
|
560
|
+
args.autoFix ?? false,
|
|
561
|
+
dsTokens,
|
|
562
|
+
bridge
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const unfixedErrors = violations.filter((v) => v.severity === "error" && !v.autoFixed);
|
|
566
|
+
const unfixedWarnings = violations.filter((v) => v.severity === "warning" && !v.autoFixed);
|
|
567
|
+
const exitCode = unfixedErrors.length > 0 ? 2 : unfixedWarnings.length > 0 ? 1 : 0;
|
|
568
|
+
|
|
569
|
+
const outputText = formatOutput(violations, args.outputFormat, nodes.length, autoFixedCount);
|
|
570
|
+
|
|
571
|
+
const logEntry = await decisionLog.log({
|
|
572
|
+
tool: "lint-rules",
|
|
573
|
+
nodeIds: violations.map((v) => v.nodeId).slice(0, 20),
|
|
574
|
+
rationale: `Lint run on ${nodes.length} node(s). Violations: ${violations.length} (${unfixedErrors.length} errors, ${unfixedWarnings.length} warnings). Auto-fixed: ${autoFixedCount}. Exit code: ${exitCode}.`,
|
|
575
|
+
tokens: [],
|
|
576
|
+
reversible: false,
|
|
577
|
+
metadata: {
|
|
578
|
+
ruleCount: rules.length,
|
|
579
|
+
nodeCount: nodes.length,
|
|
580
|
+
violationCount: violations.length,
|
|
581
|
+
autoFixedCount,
|
|
582
|
+
exitCode,
|
|
583
|
+
autoFix: args.autoFix,
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
violations,
|
|
589
|
+
totalChecked: nodes.length,
|
|
590
|
+
errorCount: unfixedErrors.length,
|
|
591
|
+
warningCount: unfixedWarnings.length,
|
|
592
|
+
autoFixedCount,
|
|
593
|
+
exitCode,
|
|
594
|
+
outputText,
|
|
595
|
+
logEntryId: logEntry.id,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ─── Main handler ─────────────────────────────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
export async function lintRulesHandler(args: LintRulesArgs): Promise<LintRulesResult> {
|
|
602
|
+
switch (args.action) {
|
|
603
|
+
case "define":
|
|
604
|
+
return actionDefine(args);
|
|
605
|
+
case "list":
|
|
606
|
+
return actionList();
|
|
607
|
+
case "delete":
|
|
608
|
+
return actionDelete(args);
|
|
609
|
+
case "run":
|
|
610
|
+
return actionRun(args);
|
|
611
|
+
default:
|
|
612
|
+
throw new Error(`lintRules: Unknown action "${(args as LintRulesArgs).action}". Valid actions: define | run | list | delete.`);
|
|
613
|
+
}
|
|
614
|
+
}
|