@sarjallab09/figma-intelligence 1.1.0 → 1.2.1
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/README.md +69 -36
- package/dist/bin/cli.js +2 -0
- package/dist/design-bridge/bridge.js +2 -0
- package/dist/figma-bridge-plugin/bridge-relay.js +2 -0
- package/dist/figma-bridge-plugin/code.js +1 -0
- package/{figma-bridge-plugin → dist/figma-bridge-plugin}/package-lock.json +0 -3
- package/dist/figma-bridge-plugin/ui.html +4970 -0
- package/dist/figma-intelligence-layer/dist/index.js +2 -0
- package/dist/scripts/clean-existing-chunks.js +2 -0
- package/dist/scripts/connect-ai-tool.js +2 -0
- package/dist/scripts/convert-hub-pdfs.js +2 -0
- package/dist/scripts/figma-mcp-status.js +2 -0
- package/dist/scripts/register-codex-mcp.js +2 -0
- package/dist/scripts/test-copilot-chat.js +2 -0
- package/package.json +11 -8
- package/bin/cli.js +0 -859
- package/design-bridge/bridge.js +0 -196
- package/design-bridge/lib/assets.js +0 -367
- package/design-bridge/lib/prompt.js +0 -85
- package/design-bridge/lib/server.js +0 -66
- package/design-bridge/lib/stitch.js +0 -37
- package/design-bridge/lib/tokens.js +0 -82
- package/design-bridge/package-lock.json +0 -579
- package/figma-bridge-plugin/README.md +0 -97
- package/figma-bridge-plugin/anthropic-chat-runner.js +0 -192
- package/figma-bridge-plugin/bridge-relay.js +0 -2505
- package/figma-bridge-plugin/chat-runner.js +0 -485
- package/figma-bridge-plugin/code.js +0 -1534
- package/figma-bridge-plugin/codex-runner.js +0 -505
- package/figma-bridge-plugin/component-schemas.js +0 -110
- package/figma-bridge-plugin/content-context.js +0 -869
- package/figma-bridge-plugin/create-button.js +0 -216
- package/figma-bridge-plugin/gemini-cli-runner.js +0 -291
- package/figma-bridge-plugin/gemini-runner.js +0 -187
- package/figma-bridge-plugin/html-to-figma.js +0 -927
- package/figma-bridge-plugin/knowledge-hub/.gitkeep +0 -0
- package/figma-bridge-plugin/knowledge-hub/uspec-references/anatomy-spec.md +0 -159
- package/figma-bridge-plugin/knowledge-hub/uspec-references/api-spec.md +0 -162
- package/figma-bridge-plugin/knowledge-hub/uspec-references/color-spec.md +0 -148
- package/figma-bridge-plugin/knowledge-hub/uspec-references/full-spec-template.md +0 -314
- package/figma-bridge-plugin/knowledge-hub/uspec-references/property-spec.md +0 -175
- package/figma-bridge-plugin/knowledge-hub/uspec-references/screen-reader-spec.md +0 -180
- package/figma-bridge-plugin/knowledge-hub/uspec-references/structure-spec.md +0 -165
- package/figma-bridge-plugin/perplexity-runner.js +0 -188
- package/figma-bridge-plugin/references/SKILL.md +0 -178
- package/figma-bridge-plugin/references/anatomy-spec.md +0 -159
- package/figma-bridge-plugin/references/api-spec.md +0 -162
- package/figma-bridge-plugin/references/color-spec.md +0 -148
- package/figma-bridge-plugin/references/full-spec-template.md +0 -314
- package/figma-bridge-plugin/references/property-spec.md +0 -175
- package/figma-bridge-plugin/references/screen-reader-spec.md +0 -180
- package/figma-bridge-plugin/references/structure-spec.md +0 -165
- package/figma-bridge-plugin/shared-prompt-config.js +0 -645
- package/figma-bridge-plugin/spec-helpers/build-table.js +0 -269
- package/figma-bridge-plugin/spec-helpers/classify-elements.js +0 -189
- package/figma-bridge-plugin/spec-helpers/index.js +0 -35
- package/figma-bridge-plugin/spec-helpers/parse-figma-link.js +0 -49
- package/figma-bridge-plugin/spec-helpers/position-markers.js +0 -158
- package/figma-bridge-plugin/stitch-auth.js +0 -322
- package/figma-bridge-plugin/stitch-runner.js +0 -1427
- package/figma-bridge-plugin/token-resolver.js +0 -107
- package/figma-bridge-plugin/ui.html +0 -4542
- package/figma-intelligence-layer/.env.example +0 -39
- package/figma-intelligence-layer/docs/local-image-generation.md +0 -60
- package/figma-intelligence-layer/examples/comfyui-workflow-template.example.json +0 -101
- package/figma-intelligence-layer/jest.config.js +0 -14
- package/figma-intelligence-layer/mcp-config.json +0 -19
- package/figma-intelligence-layer/package-lock.json +0 -5892
- package/figma-intelligence-layer/scripts/setup-comfyui-local.sh +0 -67
- package/figma-intelligence-layer/scripts/start-comfyui.sh +0 -33
- package/figma-intelligence-layer/src/index.ts +0 -2233
- package/figma-intelligence-layer/src/shared/auto-layout-validator.ts +0 -404
- package/figma-intelligence-layer/src/shared/cache.ts +0 -187
- package/figma-intelligence-layer/src/shared/color-operations.ts +0 -533
- package/figma-intelligence-layer/src/shared/color-utils.ts +0 -138
- package/figma-intelligence-layer/src/shared/component-script-builder.ts +0 -413
- package/figma-intelligence-layer/src/shared/component-templates.ts +0 -2767
- package/figma-intelligence-layer/src/shared/concept-taxonomy.ts +0 -694
- package/figma-intelligence-layer/src/shared/decision-log.ts +0 -128
- package/figma-intelligence-layer/src/shared/design-system-context.ts +0 -568
- package/figma-intelligence-layer/src/shared/design-system-intelligence.ts +0 -131
- package/figma-intelligence-layer/src/shared/design-system-matcher.ts +0 -184
- package/figma-intelligence-layer/src/shared/design-system-normalizers.ts +0 -196
- package/figma-intelligence-layer/src/shared/design-system-tokens.ts +0 -295
- package/figma-intelligence-layer/src/shared/dtcg-validator.ts +0 -530
- package/figma-intelligence-layer/src/shared/enrichment-pipeline.ts +0 -671
- package/figma-intelligence-layer/src/shared/figma-bridge.ts +0 -1418
- package/figma-intelligence-layer/src/shared/font-config.ts +0 -126
- package/figma-intelligence-layer/src/shared/icon-catalog.ts +0 -360
- package/figma-intelligence-layer/src/shared/icon-fetch.ts +0 -80
- package/figma-intelligence-layer/src/shared/prototype-script-builder.ts +0 -162
- package/figma-intelligence-layer/src/shared/response-compression.ts +0 -440
- package/figma-intelligence-layer/src/shared/semantic-token-catalog.ts +0 -324
- package/figma-intelligence-layer/src/shared/token-binder.ts +0 -505
- package/figma-intelligence-layer/src/shared/token-math.ts +0 -427
- package/figma-intelligence-layer/src/shared/token-naming.ts +0 -468
- package/figma-intelligence-layer/src/shared/token-utils.ts +0 -420
- package/figma-intelligence-layer/src/shared/types.ts +0 -346
- package/figma-intelligence-layer/src/shared/typography-presets.ts +0 -94
- package/figma-intelligence-layer/src/shared/unsplash.ts +0 -165
- package/figma-intelligence-layer/src/shared/vision-client.ts +0 -607
- package/figma-intelligence-layer/src/shared/vision-provider-anthropic.ts +0 -334
- package/figma-intelligence-layer/src/shared/vision-provider-openai.ts +0 -446
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-handler.ts +0 -782
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-renderer.ts +0 -496
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotation-kit.ts +0 -230
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/colorblind-sim.ts +0 -66
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/index.ts +0 -810
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-analyzer.ts +0 -1191
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-figma-page.ts +0 -1346
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-handler.ts +0 -148
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-figma-page.ts +0 -499
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-report.ts +0 -910
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-checker.ts +0 -989
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-criteria.ts +0 -1160
- package/figma-intelligence-layer/src/tools/phase1-vision/design-from-ref/index.ts +0 -424
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/component-recognizer.ts +0 -38
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/ds-matcher.ts +0 -111
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/font-matcher.ts +0 -114
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/icon-resolver.ts +0 -103
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/index.ts +0 -1060
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/layout-segmenter.ts +0 -18
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/token-inferencer.ts +0 -39
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/vision-pipeline.ts +0 -58
- package/figma-intelligence-layer/src/tools/phase1-vision/sketch-to-design/index.ts +0 -298
- package/figma-intelligence-layer/src/tools/phase1-vision/visual-audit/index.ts +0 -197
- package/figma-intelligence-layer/src/tools/phase2-accuracy/component-audit/index.ts +0 -494
- package/figma-intelligence-layer/src/tools/phase2-accuracy/intent-translator/index.ts +0 -356
- package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/container-patterns.ts +0 -123
- package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/index.ts +0 -663
- package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/built-in-rules.yaml +0 -56
- package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/index.ts +0 -614
- package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/rule-engine.ts +0 -113
- package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/color-theory.ts +0 -178
- package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/index.ts +0 -470
- package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/index.ts +0 -429
- package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/token-override-maps.ts +0 -226
- package/figma-intelligence-layer/src/tools/phase3-generation/ai-image-insert/index.ts +0 -535
- package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/index.ts +0 -660
- package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/pattern-fingerprints.ts +0 -209
- package/figma-intelligence-layer/src/tools/phase3-generation/composition-builder/index.ts +0 -540
- package/figma-intelligence-layer/src/tools/phase3-generation/figma-animated-build.ts +0 -391
- package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/index.ts +0 -2019
- package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/screen-templates.ts +0 -131
- package/figma-intelligence-layer/src/tools/phase3-generation/prototype-map/index.ts +0 -381
- package/figma-intelligence-layer/src/tools/phase3-generation/prototype-wire/index.ts +0 -565
- package/figma-intelligence-layer/src/tools/phase3-generation/swarm-build/index.ts +0 -764
- package/figma-intelligence-layer/src/tools/phase3-generation/system-drift/index.ts +0 -535
- package/figma-intelligence-layer/src/tools/phase3-generation/unsplash-search/index.ts +0 -84
- package/figma-intelligence-layer/src/tools/phase3-generation/url-to-frame/index.ts +0 -401
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/css-animations.ts +0 -68
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/framer-motion.ts +0 -78
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/swift-animations.ts +0 -93
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/index.ts +0 -596
- package/figma-intelligence-layer/src/tools/phase4-sync/ci-check/index.ts +0 -462
- package/figma-intelligence-layer/src/tools/phase4-sync/export-tokens/index.ts +0 -1470
- package/figma-intelligence-layer/src/tools/phase4-sync/generate-component-code/index.ts +0 -829
- package/figma-intelligence-layer/src/tools/phase4-sync/handoff-spec/index.ts +0 -702
- package/figma-intelligence-layer/src/tools/phase4-sync/icon-library-sync/index.ts +0 -483
- package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/index.ts +0 -501
- package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/storybook-parser.ts +0 -106
- package/figma-intelligence-layer/src/tools/phase4-sync/watch-docs/index.ts +0 -676
- package/figma-intelligence-layer/src/tools/phase4-sync/webhook-listener/index.ts +0 -560
- package/figma-intelligence-layer/src/tools/phase5-governance/apg-doc/index.ts +0 -1043
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/component-detection.ts +0 -620
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/anatomy.ts +0 -331
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/color-tokens.ts +0 -77
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/properties.ts +0 -54
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/snapshot.ts +0 -287
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/spacing.ts +0 -71
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/states.ts +0 -43
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/typography.ts +0 -71
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/index.ts +0 -221
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/_default.ts +0 -166
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/accordion.ts +0 -232
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/alert.ts +0 -234
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar-group.ts +0 -270
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar.ts +0 -249
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/badge.ts +0 -231
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/banner.ts +0 -293
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/breadcrumb.ts +0 -240
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/button.ts +0 -243
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/calendar.ts +0 -307
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/card.ts +0 -143
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/checkbox.ts +0 -227
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/chip.ts +0 -233
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/combobox.ts +0 -282
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/datepicker.ts +0 -276
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/divider.ts +0 -223
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/drawer.ts +0 -255
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/dropdown-menu.ts +0 -289
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/empty-state.ts +0 -261
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/file-uploader.ts +0 -290
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/form.ts +0 -265
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/grid.ts +0 -238
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/icon.ts +0 -255
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/index.ts +0 -128
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-edit.ts +0 -286
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-message.ts +0 -255
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/input.ts +0 -330
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/link.ts +0 -247
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/list.ts +0 -250
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/menu.ts +0 -247
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/modal.ts +0 -144
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navbar.ts +0 -264
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navigation.ts +0 -251
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/number-input.ts +0 -261
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/pagination.ts +0 -248
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/popover.ts +0 -270
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/progress.ts +0 -251
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/radio.ts +0 -142
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/range-slider.ts +0 -282
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/rating.ts +0 -250
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/search.ts +0 -258
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/segmented-control.ts +0 -265
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/select.ts +0 -319
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/skeleton.ts +0 -256
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/slider.ts +0 -232
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/spinner.ts +0 -239
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/status-dot.ts +0 -252
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/stepper.ts +0 -270
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/table.ts +0 -244
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tabs.ts +0 -143
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tag.ts +0 -243
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/textarea.ts +0 -259
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/time-picker.ts +0 -293
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toast.ts +0 -144
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toggle.ts +0 -289
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toolbar.ts +0 -267
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tooltip.ts +0 -232
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/treeview.ts +0 -257
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/typography.ts +0 -319
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/legacy-compat.ts +0 -121
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/anatomy-diagram.ts +0 -430
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/figma-page.ts +0 -312
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/json.ts +0 -129
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/markdown.ts +0 -78
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/visual-doc.ts +0 -2333
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/accessibility.ts +0 -100
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/anatomy.ts +0 -32
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/color-tokens.ts +0 -59
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/content-guidance.ts +0 -18
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/design-tokens.ts +0 -53
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/interaction-rules.ts +0 -19
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/overview.ts +0 -91
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/properties-api.ts +0 -71
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/qa-criteria.ts +0 -19
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/related-components.ts +0 -110
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/responsive.ts +0 -19
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/size-specs.ts +0 -67
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/spacing-structure.ts +0 -58
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/state-specs.ts +0 -79
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/states.ts +0 -50
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/type-hierarchy.ts +0 -33
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/typography.ts +0 -55
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/usage-guidelines.ts +0 -73
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/variants.ts +0 -81
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/types.ts +0 -409
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/index.ts +0 -198
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/renderer.ts +0 -701
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/types.ts +0 -88
- package/figma-intelligence-layer/src/tools/phase5-governance/decision-log/index.ts +0 -135
- package/figma-intelligence-layer/src/tools/phase5-governance/design-decision-log/index.ts +0 -491
- package/figma-intelligence-layer/src/tools/phase5-governance/ds-primitives/index.ts +0 -416
- package/figma-intelligence-layer/src/tools/phase5-governance/ds-scaffolder/index.ts +0 -722
- package/figma-intelligence-layer/src/tools/phase5-governance/ds-variables/index.ts +0 -449
- package/figma-intelligence-layer/src/tools/phase5-governance/health-report/index.ts +0 -393
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/index.ts +0 -406
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/figma-page.ts +0 -292
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/json.ts +0 -24
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/markdown.ts +0 -172
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/naming-guide.ts +0 -409
- package/figma-intelligence-layer/src/tools/phase5-governance/token-analytics/index.ts +0 -594
- package/figma-intelligence-layer/src/tools/phase5-governance/token-docs/index.ts +0 -710
- package/figma-intelligence-layer/src/tools/phase5-governance/token-migrate/index.ts +0 -458
- package/figma-intelligence-layer/src/tools/phase5-governance/token-naming/index.ts +0 -134
- package/figma-intelligence-layer/tests/apg-doc.test.ts +0 -101
- package/figma-intelligence-layer/tests/design-system-context.test.ts +0 -152
- package/figma-intelligence-layer/tests/design-system-matcher.test.ts +0 -144
- package/figma-intelligence-layer/tests/figma-bridge.test.ts +0 -83
- package/figma-intelligence-layer/tests/generate-image-and-insert.test.ts +0 -56
- package/figma-intelligence-layer/tests/screen-cloner-regression.test.ts +0 -69
- package/figma-intelligence-layer/tests/smoke.test.ts +0 -174
- package/figma-intelligence-layer/tests/spec-generator.test.ts +0 -127
- package/figma-intelligence-layer/tests/token-migrate.test.ts +0 -21
- package/figma-intelligence-layer/tests/token-naming.test.ts +0 -30
- package/figma-intelligence-layer/tsconfig.json +0 -19
- package/scripts/clean-existing-chunks.js +0 -179
- package/scripts/connect-ai-tool.js +0 -490
- package/scripts/convert-hub-pdfs.js +0 -425
- package/scripts/figma-mcp-status.js +0 -349
- package/scripts/register-codex-mcp.js +0 -96
- /package/{design-bridge → dist/design-bridge}/.env.example +0 -0
- /package/{design-bridge → dist/design-bridge}/package.json +0 -0
- /package/{figma-bridge-plugin → dist/figma-bridge-plugin}/manifest.json +0 -0
- /package/{figma-bridge-plugin → dist/figma-bridge-plugin}/package.json +0 -0
- /package/{figma-intelligence-layer → dist/figma-intelligence-layer}/package.json +0 -0
package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-analyzer.ts
DELETED
|
@@ -1,1191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keyboard & Screen Reader Order Analyzer
|
|
3
|
-
* Pure analysis engine that extracts interactive elements from a Figma node tree,
|
|
4
|
-
* infers ARIA roles/labels, computes keyboard tab order and screen reader reading
|
|
5
|
-
* order, and produces all 10 annotation sections as structured data.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { FigmaNode, WCAGIssue } from "../../../shared/types.js";
|
|
9
|
-
import {
|
|
10
|
-
checkMeaningfulSequence,
|
|
11
|
-
checkFocusOrder,
|
|
12
|
-
checkNameRoleValue,
|
|
13
|
-
checkLabelsOrInstructions,
|
|
14
|
-
extractSolidColor,
|
|
15
|
-
findParentBgColor,
|
|
16
|
-
checkTextContrast,
|
|
17
|
-
checkTouchTarget,
|
|
18
|
-
checkTargetSizeMinimum,
|
|
19
|
-
} from "./wcag-checker.js";
|
|
20
|
-
|
|
21
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
export interface TabOrderEntry {
|
|
24
|
-
order: number;
|
|
25
|
-
elementDescription: string;
|
|
26
|
-
role: string;
|
|
27
|
-
label: string;
|
|
28
|
-
notes: string;
|
|
29
|
-
nodeId: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ReadingOrderNode {
|
|
33
|
-
landmarkRole?: string;
|
|
34
|
-
landmarkLabel?: string;
|
|
35
|
-
content: string;
|
|
36
|
-
role?: string;
|
|
37
|
-
depth: number;
|
|
38
|
-
children?: ReadingOrderNode[];
|
|
39
|
-
nodeId: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface FocusAnnouncement {
|
|
43
|
-
element: string;
|
|
44
|
-
announcement: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface StateChangeRule {
|
|
48
|
-
trigger: string;
|
|
49
|
-
behavior: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface FocusRule {
|
|
53
|
-
scenario: string;
|
|
54
|
-
rule: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface AriaReq {
|
|
58
|
-
element: string;
|
|
59
|
-
attribute: string;
|
|
60
|
-
value: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface KBRule {
|
|
64
|
-
component: string;
|
|
65
|
-
key: string;
|
|
66
|
-
action: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface Warning {
|
|
70
|
-
id: number;
|
|
71
|
-
severity: "Critical" | "Warning" | "Info";
|
|
72
|
-
title: string;
|
|
73
|
-
description: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface AuditSummaryRow {
|
|
77
|
-
severity: string;
|
|
78
|
-
count: number;
|
|
79
|
-
category: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface A11yOrderAnalysis {
|
|
83
|
-
header: { frameName: string; nodeId: string; date: string; standard: string };
|
|
84
|
-
scope: string;
|
|
85
|
-
assumptions: string[];
|
|
86
|
-
keyboardTabOrder: TabOrderEntry[];
|
|
87
|
-
screenReaderReadingOrder: ReadingOrderNode[];
|
|
88
|
-
interactionAnnouncements: {
|
|
89
|
-
onFocus: FocusAnnouncement[];
|
|
90
|
-
stateChanges: StateChangeRule[];
|
|
91
|
-
};
|
|
92
|
-
focusManagement: FocusRule[];
|
|
93
|
-
implementationNotes: {
|
|
94
|
-
ariaTable: AriaReq[];
|
|
95
|
-
keyboardBehavior: KBRule[];
|
|
96
|
-
dosDonts: string[];
|
|
97
|
-
};
|
|
98
|
-
warnings: Warning[];
|
|
99
|
-
auditSummary: AuditSummaryRow[];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ─── Role Inference ─────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
const ROLE_PATTERNS: [RegExp, string][] = [
|
|
105
|
-
[/\b(button|btn|cta)\b/i, "button"],
|
|
106
|
-
[/\b(link|anchor|href)\b/i, "link"],
|
|
107
|
-
[/\b(input|field|textbox|text[-_ ]?field)\b/i, "textbox"],
|
|
108
|
-
[/\b(checkbox|check[-_ ]?box)\b/i, "checkbox"],
|
|
109
|
-
[/\b(radio)\b/i, "radio"],
|
|
110
|
-
[/\b(toggle|switch)\b/i, "switch"],
|
|
111
|
-
[/\b(select|dropdown|combo[-_ ]?box)\b/i, "listbox"],
|
|
112
|
-
[/\b(slider|range)\b/i, "slider"],
|
|
113
|
-
[/\b(spinner|stepper|quantity)\b/i, "spinbutton"],
|
|
114
|
-
[/\b(tab)\b/i, "tab"],
|
|
115
|
-
[/\b(search)\b/i, "searchbox"],
|
|
116
|
-
[/\b(menu[-_ ]?item)\b/i, "menuitem"],
|
|
117
|
-
[/\b(menu)\b/i, "menu"],
|
|
118
|
-
[/\b(modal|dialog)\b/i, "dialog"],
|
|
119
|
-
[/\b(image|photo|avatar|thumbnail)\b/i, "img"],
|
|
120
|
-
];
|
|
121
|
-
|
|
122
|
-
const LANDMARK_PATTERNS: [RegExp, string, string][] = [
|
|
123
|
-
[/\b(nav|navigation)\b/i, "navigation", "Navigation"],
|
|
124
|
-
[/\b(header|top[-_ ]?bar|app[-_ ]?bar|navbar)\b/i, "banner", "Site header"],
|
|
125
|
-
[/\b(footer|bottom[-_ ]?bar)\b/i, "contentinfo", "Site footer"],
|
|
126
|
-
[/\b(sidebar|side[-_ ]?nav|drawer)\b/i, "complementary", "Sidebar"],
|
|
127
|
-
[/\b(main|content|body)\b/i, "main", "Main content"],
|
|
128
|
-
[/\b(banner|promo|offer|announcement)\b/i, "status", "Announcement"],
|
|
129
|
-
[/\b(form)\b/i, "form", "Form"],
|
|
130
|
-
[/\b(search)\b/i, "search", "Search"],
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
function inferRole(node: FigmaNode): string | null {
|
|
134
|
-
const nameLower = node.name.toLowerCase();
|
|
135
|
-
|
|
136
|
-
// Check name-based patterns
|
|
137
|
-
for (const [pattern, role] of ROLE_PATTERNS) {
|
|
138
|
-
if (pattern.test(nameLower)) return role;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// INSTANCE/COMPONENT nodes: only assign a role if the name strongly
|
|
142
|
-
// indicates interactivity. Decorative icons (heart, percent, trailing
|
|
143
|
-
// icon, etc.) must NOT be promoted to "button" just because they are
|
|
144
|
-
// component instances.
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function inferLandmark(node: FigmaNode): { role: string; label: string } | null {
|
|
149
|
-
const nameLower = node.name.toLowerCase();
|
|
150
|
-
for (const [pattern, role, label] of LANDMARK_PATTERNS) {
|
|
151
|
-
if (pattern.test(nameLower)) return { role, label };
|
|
152
|
-
}
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ─── Label Inference ────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
function hasTextChild(node: FigmaNode): boolean {
|
|
159
|
-
if (node.type === "TEXT" && node.characters) return true;
|
|
160
|
-
return node.children?.some((c) => hasTextChild(c)) ?? false;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function findTextContent(node: FigmaNode): string {
|
|
164
|
-
if (node.type === "TEXT" && node.characters) return node.characters.trim();
|
|
165
|
-
if (!node.children) return "";
|
|
166
|
-
const texts: string[] = [];
|
|
167
|
-
for (const child of node.children) {
|
|
168
|
-
const t = findTextContent(child);
|
|
169
|
-
if (t) texts.push(t);
|
|
170
|
-
}
|
|
171
|
-
return texts.join(" ");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function cleanNodeName(name: string): string {
|
|
175
|
-
return name
|
|
176
|
-
.replace(/^(icon|icn|ic)[-_ /]*/i, "")
|
|
177
|
-
.replace(/[-_/]/g, " ")
|
|
178
|
-
.replace(/\s+/g, " ")
|
|
179
|
-
.trim();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function findParentContext(
|
|
183
|
-
nodeId: string,
|
|
184
|
-
allNodes: FigmaNode[],
|
|
185
|
-
depth: number = 0
|
|
186
|
-
): string | null {
|
|
187
|
-
if (depth > 6) return null;
|
|
188
|
-
for (const n of allNodes) {
|
|
189
|
-
if (n.children?.some((c) => c.id === nodeId)) {
|
|
190
|
-
// Look for a text child that could be a product/item name
|
|
191
|
-
const nameText = n.children?.find(
|
|
192
|
-
(c) =>
|
|
193
|
-
c.type === "TEXT" &&
|
|
194
|
-
c.characters &&
|
|
195
|
-
c.characters.length > 2 &&
|
|
196
|
-
c.characters.length < 80
|
|
197
|
-
);
|
|
198
|
-
if (nameText?.characters) return nameText.characters.trim();
|
|
199
|
-
// Walk further up
|
|
200
|
-
return findParentContext(n.id, allNodes, depth + 1);
|
|
201
|
-
}
|
|
202
|
-
if (n.children) {
|
|
203
|
-
const found = findParentContext(nodeId, n.children, depth);
|
|
204
|
-
if (found) return found;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function inferLabel(
|
|
211
|
-
node: FigmaNode,
|
|
212
|
-
role: string,
|
|
213
|
-
allNodes: FigmaNode[]
|
|
214
|
-
): string {
|
|
215
|
-
// Text content inside the element
|
|
216
|
-
const textContent = findTextContent(node);
|
|
217
|
-
|
|
218
|
-
// For elements with text, use it
|
|
219
|
-
if (textContent && textContent.length < 80) {
|
|
220
|
-
// Enhance with context for actions
|
|
221
|
-
if (
|
|
222
|
-
role === "button" &&
|
|
223
|
-
/^(remove|delete|close|add|minus|plus|decrease|increase)$/i.test(
|
|
224
|
-
textContent
|
|
225
|
-
)
|
|
226
|
-
) {
|
|
227
|
-
const context = findParentContext(node.id, allNodes);
|
|
228
|
-
if (context) {
|
|
229
|
-
const action = textContent.toLowerCase();
|
|
230
|
-
if (/remove|delete/i.test(action)) return `Remove ${context} from cart`;
|
|
231
|
-
if (/decrease|minus/i.test(action))
|
|
232
|
-
return `Decrease quantity for ${context}`;
|
|
233
|
-
if (/increase|plus|add/i.test(action))
|
|
234
|
-
return `Increase quantity for ${context}`;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return textContent;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Icon-only elements: clean up name
|
|
241
|
-
if (role === "button" || role === "link") {
|
|
242
|
-
const cleaned = cleanNodeName(node.name);
|
|
243
|
-
if (cleaned) {
|
|
244
|
-
// Capitalize first letter
|
|
245
|
-
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Use node name as fallback
|
|
250
|
-
return cleanNodeName(node.name) || node.name;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ─── Node Collection Helpers ────────────────────────────────────────────────
|
|
254
|
-
|
|
255
|
-
function collectAll(node: FigmaNode, acc: FigmaNode[] = []): FigmaNode[] {
|
|
256
|
-
acc.push(node);
|
|
257
|
-
if (node.children) {
|
|
258
|
-
for (const child of node.children) collectAll(child, acc);
|
|
259
|
-
}
|
|
260
|
-
return acc;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function isInsideNavigation(node: FigmaNode, rootNode: FigmaNode): boolean {
|
|
264
|
-
// Walk up parent chain to check if this node is inside a navigation landmark
|
|
265
|
-
function findParent(current: FigmaNode, targetId: string): FigmaNode | null {
|
|
266
|
-
if (current.children) {
|
|
267
|
-
for (const child of current.children) {
|
|
268
|
-
if (child.id === targetId) return current;
|
|
269
|
-
const found = findParent(child, targetId);
|
|
270
|
-
if (found) return found;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
let parentNode = findParent(rootNode, node.id);
|
|
277
|
-
let depth = 0;
|
|
278
|
-
while (parentNode && depth < 8) {
|
|
279
|
-
if (inferLandmark(parentNode)?.role === "navigation" ||
|
|
280
|
-
inferLandmark(parentNode)?.role === "banner") {
|
|
281
|
-
return true;
|
|
282
|
-
}
|
|
283
|
-
const nextParent = findParent(rootNode, parentNode.id);
|
|
284
|
-
if (!nextParent || nextParent.id === parentNode.id) break;
|
|
285
|
-
parentNode = nextParent;
|
|
286
|
-
depth++;
|
|
287
|
-
}
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Cache root node for navigation lookups in isInteractive
|
|
292
|
-
let _analysisRootNode: FigmaNode | null = null;
|
|
293
|
-
|
|
294
|
-
function isInteractive(node: FigmaNode): boolean {
|
|
295
|
-
const role = inferRole(node);
|
|
296
|
-
if (role && !["img", "dialog", "menu"].includes(role)) return true;
|
|
297
|
-
|
|
298
|
-
// INSTANCE/COMPONENT with explicitly interactive names only.
|
|
299
|
-
// DO NOT treat all leaf instances as interactive — many are decorative
|
|
300
|
-
// icons (heart, star, percent, trailing icon, etc.).
|
|
301
|
-
if (
|
|
302
|
-
(node.type === "INSTANCE" || node.type === "COMPONENT") &&
|
|
303
|
-
/button|btn|link|cta|click/i.test(node.name)
|
|
304
|
-
)
|
|
305
|
-
return true;
|
|
306
|
-
|
|
307
|
-
// TEXT nodes inside navigation/banner landmarks are likely links
|
|
308
|
-
if (
|
|
309
|
-
node.type === "TEXT" &&
|
|
310
|
-
node.characters &&
|
|
311
|
-
node.characters.trim().length > 0 &&
|
|
312
|
-
node.characters.trim().length < 40 &&
|
|
313
|
-
_analysisRootNode &&
|
|
314
|
-
isInsideNavigation(node, _analysisRootNode)
|
|
315
|
-
) {
|
|
316
|
-
return true;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function hasInteractiveDescendant(node: FigmaNode): boolean {
|
|
323
|
-
if (!node.children) return false;
|
|
324
|
-
for (const child of node.children) {
|
|
325
|
-
const role = inferRole(child);
|
|
326
|
-
if (role && !["img", "dialog", "menu"].includes(role)) return true;
|
|
327
|
-
if (
|
|
328
|
-
(child.type === "INSTANCE" || child.type === "COMPONENT") &&
|
|
329
|
-
/button|btn|link|cta|click/i.test(child.name)
|
|
330
|
-
)
|
|
331
|
-
return true;
|
|
332
|
-
if (hasInteractiveDescendant(child)) return true;
|
|
333
|
-
}
|
|
334
|
-
return false;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function isHeading(node: FigmaNode): boolean {
|
|
338
|
-
if (node.type !== "TEXT") return false;
|
|
339
|
-
const fontSize = node.style?.fontSize ?? 16;
|
|
340
|
-
const fontWeight = node.style?.fontWeight ?? 400;
|
|
341
|
-
return fontSize >= 20 || fontWeight >= 700;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ─── Tab Order Computation ──────────────────────────────────────────────────
|
|
345
|
-
|
|
346
|
-
const ROW_THRESHOLD = 10; // px tolerance for same-row grouping
|
|
347
|
-
|
|
348
|
-
function computeTabOrder(
|
|
349
|
-
interactiveNodes: FigmaNode[],
|
|
350
|
-
allNodes: FigmaNode[]
|
|
351
|
-
): TabOrderEntry[] {
|
|
352
|
-
// Filter nodes with bounding boxes
|
|
353
|
-
const withBounds = interactiveNodes.filter((n) => n.absoluteBoundingBox);
|
|
354
|
-
|
|
355
|
-
// Sort by visual position: row-group by Y, then left-to-right within row
|
|
356
|
-
withBounds.sort((a, b) => {
|
|
357
|
-
const ay = a.absoluteBoundingBox!.y;
|
|
358
|
-
const by = b.absoluteBoundingBox!.y;
|
|
359
|
-
if (Math.abs(ay - by) <= ROW_THRESHOLD) {
|
|
360
|
-
return a.absoluteBoundingBox!.x - b.absoluteBoundingBox!.x;
|
|
361
|
-
}
|
|
362
|
-
return ay - by;
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
return withBounds.map((node, idx) => {
|
|
366
|
-
let role = inferRole(node) || (node.type === "TEXT" ? "link" : "button");
|
|
367
|
-
const label = inferLabel(node, role, allNodes);
|
|
368
|
-
const notes = generateNotes(node, role);
|
|
369
|
-
|
|
370
|
-
return {
|
|
371
|
-
order: idx + 1,
|
|
372
|
-
elementDescription: generateElementDescription(node),
|
|
373
|
-
role,
|
|
374
|
-
label,
|
|
375
|
-
notes,
|
|
376
|
-
nodeId: node.id,
|
|
377
|
-
};
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function generateElementDescription(node: FigmaNode): string {
|
|
382
|
-
let name = node.name;
|
|
383
|
-
// Clean up Figma-style naming
|
|
384
|
-
name = name.replace(/^(component|instance|frame)[-_ /]*/i, "");
|
|
385
|
-
name = name.replace(/[-_/]/g, " ").replace(/\s+/g, " ").trim();
|
|
386
|
-
return name || node.name;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function generateNotes(node: FigmaNode, role: string): string {
|
|
390
|
-
const notes: string[] = [];
|
|
391
|
-
|
|
392
|
-
if (role === "spinbutton") {
|
|
393
|
-
notes.push('aria-valuenow required; keyboard: Arrow Up/Down to adjust');
|
|
394
|
-
}
|
|
395
|
-
if (role === "textbox") {
|
|
396
|
-
notes.push("Pair with visible label or aria-label");
|
|
397
|
-
}
|
|
398
|
-
if (role === "link" && node.name.toLowerCase().includes("logo")) {
|
|
399
|
-
notes.push("Navigates to homepage");
|
|
400
|
-
}
|
|
401
|
-
if (role === "button" && /search/i.test(node.name)) {
|
|
402
|
-
notes.push("Opens search overlay; announce expanded/collapsed state");
|
|
403
|
-
}
|
|
404
|
-
if (role === "button" && /menu|hamburger|nav/i.test(node.name)) {
|
|
405
|
-
notes.push("Opens navigation menu; see Focus Management for trap behavior");
|
|
406
|
-
}
|
|
407
|
-
if (role === "button" && /remove|delete/i.test(node.name)) {
|
|
408
|
-
notes.push("See Focus Management for post-removal focus");
|
|
409
|
-
}
|
|
410
|
-
if (role === "button" && /decrease|minus/i.test(node.name)) {
|
|
411
|
-
notes.push("Disabled when qty = 1; announce current qty on activation");
|
|
412
|
-
}
|
|
413
|
-
if (role === "button" && /increase|plus/i.test(node.name)) {
|
|
414
|
-
notes.push("Announce new qty on activation");
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Touch target warning
|
|
418
|
-
if (node.width && node.height && (node.width < 44 || node.height < 44)) {
|
|
419
|
-
notes.push(`Touch target ${node.width}x${node.height}px below 44x44px minimum`);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return notes.join("; ") || "\u2014";
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// ─── Screen Reader Reading Order ────────────────────────────────────────────
|
|
426
|
-
|
|
427
|
-
function buildReadingOrder(
|
|
428
|
-
node: FigmaNode,
|
|
429
|
-
depth: number = 0
|
|
430
|
-
): ReadingOrderNode[] {
|
|
431
|
-
const results: ReadingOrderNode[] = [];
|
|
432
|
-
|
|
433
|
-
// Check if this node is a landmark
|
|
434
|
-
const landmark = inferLandmark(node);
|
|
435
|
-
|
|
436
|
-
if (node.type === "TEXT" && node.characters) {
|
|
437
|
-
results.push({
|
|
438
|
-
content: node.characters.trim(),
|
|
439
|
-
role: isHeading(node)
|
|
440
|
-
? `heading (h${estimateHeadingLevel(node)})`
|
|
441
|
-
: "text",
|
|
442
|
-
depth,
|
|
443
|
-
nodeId: node.id,
|
|
444
|
-
});
|
|
445
|
-
return results;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Interactive leaf nodes
|
|
449
|
-
const role = inferRole(node);
|
|
450
|
-
if (role && !node.children?.length) {
|
|
451
|
-
const label = inferLabel(node, role, []);
|
|
452
|
-
results.push({
|
|
453
|
-
content: `${label} (${role})`,
|
|
454
|
-
role,
|
|
455
|
-
depth,
|
|
456
|
-
nodeId: node.id,
|
|
457
|
-
});
|
|
458
|
-
return results;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Container/group nodes
|
|
462
|
-
if (node.children && node.children.length > 0) {
|
|
463
|
-
// Sort children by visual position (top-to-bottom, left-to-right)
|
|
464
|
-
// instead of relying on Figma's layer order which may be inverted
|
|
465
|
-
const sortedChildren = [...node.children].sort((a, b) => {
|
|
466
|
-
const ay = a.absoluteBoundingBox?.y ?? 0;
|
|
467
|
-
const by = b.absoluteBoundingBox?.y ?? 0;
|
|
468
|
-
if (Math.abs(ay - by) <= ROW_THRESHOLD) {
|
|
469
|
-
return (a.absoluteBoundingBox?.x ?? 0) - (b.absoluteBoundingBox?.x ?? 0);
|
|
470
|
-
}
|
|
471
|
-
return ay - by;
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
const childResults: ReadingOrderNode[] = [];
|
|
475
|
-
for (const child of sortedChildren) {
|
|
476
|
-
childResults.push(...buildReadingOrder(child, depth + 1));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (landmark) {
|
|
480
|
-
// Wrap children in landmark
|
|
481
|
-
results.push({
|
|
482
|
-
landmarkRole: landmark.role,
|
|
483
|
-
landmarkLabel: landmark.label,
|
|
484
|
-
content: node.name,
|
|
485
|
-
depth,
|
|
486
|
-
children: childResults,
|
|
487
|
-
nodeId: node.id,
|
|
488
|
-
});
|
|
489
|
-
} else if (
|
|
490
|
-
role === "dialog" ||
|
|
491
|
-
(node.name.toLowerCase().includes("product") &&
|
|
492
|
-
childResults.length > 0)
|
|
493
|
-
) {
|
|
494
|
-
// Group container
|
|
495
|
-
results.push({
|
|
496
|
-
content: node.name,
|
|
497
|
-
role: role || "group",
|
|
498
|
-
depth,
|
|
499
|
-
children: childResults,
|
|
500
|
-
nodeId: node.id,
|
|
501
|
-
});
|
|
502
|
-
} else {
|
|
503
|
-
// Flatten into parent
|
|
504
|
-
results.push(...childResults);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return results;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function estimateHeadingLevel(node: FigmaNode): number {
|
|
512
|
-
const fontSize = node.style?.fontSize ?? 16;
|
|
513
|
-
if (fontSize >= 32) return 1;
|
|
514
|
-
if (fontSize >= 24) return 2;
|
|
515
|
-
if (fontSize >= 20) return 3;
|
|
516
|
-
if (fontSize >= 18) return 4;
|
|
517
|
-
if (fontSize >= 16) return 5;
|
|
518
|
-
return 6;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// ─── Interaction Announcements ──────────────────────────────────────────────
|
|
522
|
-
|
|
523
|
-
function generateFocusAnnouncements(
|
|
524
|
-
tabOrder: TabOrderEntry[]
|
|
525
|
-
): FocusAnnouncement[] {
|
|
526
|
-
return tabOrder.map((entry) => {
|
|
527
|
-
let announcement = `${entry.label}, ${entry.role}`;
|
|
528
|
-
|
|
529
|
-
// Add state info
|
|
530
|
-
if (entry.notes.includes("Disabled")) {
|
|
531
|
-
announcement += ", disabled";
|
|
532
|
-
}
|
|
533
|
-
if (entry.notes.includes("current page")) {
|
|
534
|
-
announcement += ", current page";
|
|
535
|
-
}
|
|
536
|
-
if (entry.role === "textbox") {
|
|
537
|
-
announcement += ", edit text, blank";
|
|
538
|
-
}
|
|
539
|
-
if (entry.role === "spinbutton") {
|
|
540
|
-
announcement += ", spin button, 1";
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
return { element: entry.elementDescription, announcement };
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function generateStateChanges(
|
|
548
|
-
tabOrder: TabOrderEntry[]
|
|
549
|
-
): StateChangeRule[] {
|
|
550
|
-
const rules: StateChangeRule[] = [];
|
|
551
|
-
|
|
552
|
-
// Quantity controls
|
|
553
|
-
const hasSpinbutton = tabOrder.some((e) => e.role === "spinbutton");
|
|
554
|
-
if (hasSpinbutton) {
|
|
555
|
-
rules.push({
|
|
556
|
-
trigger: "Quantity change (spinbutton / button activation)",
|
|
557
|
-
behavior:
|
|
558
|
-
'Announce via aria-live="assertive": "Quantity updated to [N] for [Product Name]". Update order summary total via aria-live="polite".',
|
|
559
|
-
});
|
|
560
|
-
rules.push({
|
|
561
|
-
trigger: "Decrease at minimum quantity",
|
|
562
|
-
behavior:
|
|
563
|
-
'"Minimum quantity reached" — button becomes aria-disabled="true"',
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Remove actions
|
|
568
|
-
const hasRemove = tabOrder.some((e) =>
|
|
569
|
-
/remove|delete/i.test(e.elementDescription)
|
|
570
|
-
);
|
|
571
|
-
if (hasRemove) {
|
|
572
|
-
rules.push({
|
|
573
|
-
trigger: "Item removed",
|
|
574
|
-
behavior:
|
|
575
|
-
'Announce "[Item Name] removed from cart". Update item count via aria-live="polite". Update order summary.',
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Form submission
|
|
580
|
-
const hasTextbox = tabOrder.some((e) => e.role === "textbox");
|
|
581
|
-
if (hasTextbox) {
|
|
582
|
-
rules.push({
|
|
583
|
-
trigger: "Form validation error",
|
|
584
|
-
behavior:
|
|
585
|
-
'Announce error via role="alert" or aria-live="assertive". Input receives aria-invalid="true" and aria-describedby pointing to error.',
|
|
586
|
-
});
|
|
587
|
-
rules.push({
|
|
588
|
-
trigger: "Form submission success",
|
|
589
|
-
behavior:
|
|
590
|
-
'Announce success via aria-live="polite". Focus remains on input or moves to success message.',
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Navigation
|
|
595
|
-
const hasPrimaryCTA = tabOrder.some(
|
|
596
|
-
(e) =>
|
|
597
|
-
e.role === "button" &&
|
|
598
|
-
/proceed|checkout|submit|continue|next/i.test(e.elementDescription)
|
|
599
|
-
);
|
|
600
|
-
if (hasPrimaryCTA) {
|
|
601
|
-
rules.push({
|
|
602
|
-
trigger: "Primary CTA activation",
|
|
603
|
-
behavior:
|
|
604
|
-
'Announce "Navigating to [destination]". Standard page navigation.',
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Toggle/switch
|
|
609
|
-
const hasSwitch = tabOrder.some((e) => e.role === "switch");
|
|
610
|
-
if (hasSwitch) {
|
|
611
|
-
rules.push({
|
|
612
|
-
trigger: "Toggle state change",
|
|
613
|
-
behavior: 'Announce new state: "[Label], switch, on/off"',
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return rules;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// ─── Focus Management ───────────────────────────────────────────────────────
|
|
621
|
-
|
|
622
|
-
function generateFocusRules(tabOrder: TabOrderEntry[]): FocusRule[] {
|
|
623
|
-
const rules: FocusRule[] = [];
|
|
624
|
-
|
|
625
|
-
rules.push({
|
|
626
|
-
scenario: "Initial page load",
|
|
627
|
-
rule: "Focus on the <main> landmark or the primary heading (<h1>).",
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
const hasSpinbutton = tabOrder.some((e) => e.role === "spinbutton");
|
|
631
|
-
if (hasSpinbutton) {
|
|
632
|
-
rules.push({
|
|
633
|
-
scenario: "After quantity change",
|
|
634
|
-
rule: "Focus stays on the activated button (plus or minus). Do NOT move focus.",
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const hasRemove = tabOrder.some((e) =>
|
|
639
|
-
/remove|delete/i.test(e.elementDescription)
|
|
640
|
-
);
|
|
641
|
-
if (hasRemove) {
|
|
642
|
-
rules.push({
|
|
643
|
-
scenario: "After item removal",
|
|
644
|
-
rule: "If items remain: move focus to the next item's name. If removed item was last: move to previous item's name. If list empty: move to empty state message or \"Continue Shopping\" link.",
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const hasMenu = tabOrder.some(
|
|
649
|
-
(e) => e.role === "button" && /menu|hamburger|nav/i.test(e.elementDescription)
|
|
650
|
-
);
|
|
651
|
-
if (hasMenu) {
|
|
652
|
-
rules.push({
|
|
653
|
-
scenario: "Menu/drawer activation",
|
|
654
|
-
rule: 'Focus traps inside the drawer. Escape closes and returns focus to the menu button. Drawer has role="dialog", aria-modal="true".',
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const hasSearch = tabOrder.some(
|
|
659
|
-
(e) => e.role === "button" && /search/i.test(e.elementDescription)
|
|
660
|
-
);
|
|
661
|
-
if (hasSearch) {
|
|
662
|
-
rules.push({
|
|
663
|
-
scenario: "Search overlay activation",
|
|
664
|
-
rule: "Focus moves to the search input. Escape closes and returns focus to the search button.",
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const hasTextbox = tabOrder.some((e) => e.role === "textbox");
|
|
669
|
-
if (hasTextbox) {
|
|
670
|
-
rules.push({
|
|
671
|
-
scenario: "Form validation failure",
|
|
672
|
-
rule: 'Focus moves to the input. Input marked aria-invalid="true". Error associated via aria-describedby.',
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
return rules;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// ─── Implementation Notes ───────────────────────────────────────────────────
|
|
680
|
-
|
|
681
|
-
const APG_KEYBOARD_MAP: Record<string, KBRule[]> = {
|
|
682
|
-
button: [
|
|
683
|
-
{ component: "Button", key: "Enter / Space", action: "Activate" },
|
|
684
|
-
],
|
|
685
|
-
link: [{ component: "Link", key: "Enter", action: "Navigate" }],
|
|
686
|
-
textbox: [
|
|
687
|
-
{ component: "Text input", key: "Enter", action: "Submit (if applicable)" },
|
|
688
|
-
],
|
|
689
|
-
spinbutton: [
|
|
690
|
-
{ component: "Spinbutton", key: "Arrow Up", action: "Increment value" },
|
|
691
|
-
{
|
|
692
|
-
component: "Spinbutton",
|
|
693
|
-
key: "Arrow Down",
|
|
694
|
-
action: "Decrement value (min bound)",
|
|
695
|
-
},
|
|
696
|
-
{ component: "Spinbutton", key: "Home", action: "Set to minimum" },
|
|
697
|
-
{ component: "Spinbutton", key: "End", action: "Set to maximum" },
|
|
698
|
-
],
|
|
699
|
-
checkbox: [
|
|
700
|
-
{ component: "Checkbox", key: "Space", action: "Toggle checked state" },
|
|
701
|
-
],
|
|
702
|
-
radio: [
|
|
703
|
-
{
|
|
704
|
-
component: "Radio",
|
|
705
|
-
key: "Arrow Up/Down",
|
|
706
|
-
action: "Move within radio group",
|
|
707
|
-
},
|
|
708
|
-
{ component: "Radio", key: "Space", action: "Select focused option" },
|
|
709
|
-
],
|
|
710
|
-
switch: [
|
|
711
|
-
{ component: "Switch", key: "Space", action: "Toggle on/off" },
|
|
712
|
-
],
|
|
713
|
-
listbox: [
|
|
714
|
-
{
|
|
715
|
-
component: "Listbox",
|
|
716
|
-
key: "Arrow Up/Down",
|
|
717
|
-
action: "Navigate options",
|
|
718
|
-
},
|
|
719
|
-
{ component: "Listbox", key: "Enter", action: "Select option" },
|
|
720
|
-
{ component: "Listbox", key: "Escape", action: "Close dropdown" },
|
|
721
|
-
],
|
|
722
|
-
slider: [
|
|
723
|
-
{
|
|
724
|
-
component: "Slider",
|
|
725
|
-
key: "Arrow Left/Right",
|
|
726
|
-
action: "Adjust value",
|
|
727
|
-
},
|
|
728
|
-
{ component: "Slider", key: "Home", action: "Set to minimum" },
|
|
729
|
-
{ component: "Slider", key: "End", action: "Set to maximum" },
|
|
730
|
-
],
|
|
731
|
-
tab: [
|
|
732
|
-
{
|
|
733
|
-
component: "Tab",
|
|
734
|
-
key: "Arrow Left/Right",
|
|
735
|
-
action: "Move between tabs",
|
|
736
|
-
},
|
|
737
|
-
],
|
|
738
|
-
dialog: [
|
|
739
|
-
{
|
|
740
|
-
component: "Dialog",
|
|
741
|
-
key: "Escape",
|
|
742
|
-
action: "Close dialog, return focus to trigger",
|
|
743
|
-
},
|
|
744
|
-
],
|
|
745
|
-
menuitem: [
|
|
746
|
-
{
|
|
747
|
-
component: "Menu item",
|
|
748
|
-
key: "Arrow Up/Down",
|
|
749
|
-
action: "Navigate items",
|
|
750
|
-
},
|
|
751
|
-
{ component: "Menu item", key: "Enter", action: "Activate item" },
|
|
752
|
-
{ component: "Menu item", key: "Escape", action: "Close menu" },
|
|
753
|
-
],
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
function generateAriaRequirements(
|
|
757
|
-
tabOrder: TabOrderEntry[],
|
|
758
|
-
readingOrder: ReadingOrderNode[]
|
|
759
|
-
): AriaReq[] {
|
|
760
|
-
const reqs: AriaReq[] = [];
|
|
761
|
-
const seenElements = new Set<string>();
|
|
762
|
-
|
|
763
|
-
for (const entry of tabOrder) {
|
|
764
|
-
const key = `${entry.role}-${entry.elementDescription}`;
|
|
765
|
-
if (seenElements.has(key)) continue;
|
|
766
|
-
seenElements.add(key);
|
|
767
|
-
|
|
768
|
-
// Icon-only buttons need aria-label
|
|
769
|
-
if (
|
|
770
|
-
(entry.role === "button" || entry.role === "link") &&
|
|
771
|
-
/icon|search|cart|menu|hamburger|close|minus|plus|heart/i.test(
|
|
772
|
-
entry.elementDescription
|
|
773
|
-
)
|
|
774
|
-
) {
|
|
775
|
-
reqs.push({
|
|
776
|
-
element: entry.elementDescription,
|
|
777
|
-
attribute: "aria-label",
|
|
778
|
-
value: entry.label,
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// Spinbutton requirements
|
|
783
|
-
if (entry.role === "spinbutton") {
|
|
784
|
-
reqs.push({
|
|
785
|
-
element: entry.elementDescription,
|
|
786
|
-
attribute: "role",
|
|
787
|
-
value: "spinbutton",
|
|
788
|
-
});
|
|
789
|
-
reqs.push({
|
|
790
|
-
element: entry.elementDescription,
|
|
791
|
-
attribute: "aria-valuenow",
|
|
792
|
-
value: "current quantity",
|
|
793
|
-
});
|
|
794
|
-
reqs.push({
|
|
795
|
-
element: entry.elementDescription,
|
|
796
|
-
attribute: "aria-valuemin",
|
|
797
|
-
value: '"1"',
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Current page indicator
|
|
802
|
-
if (/cart/i.test(entry.elementDescription) && entry.role === "link") {
|
|
803
|
-
reqs.push({
|
|
804
|
-
element: entry.elementDescription,
|
|
805
|
-
attribute: "aria-current",
|
|
806
|
-
value: '"page"',
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Landmark regions
|
|
812
|
-
for (const rNode of readingOrder) {
|
|
813
|
-
if (rNode.landmarkRole) {
|
|
814
|
-
reqs.push({
|
|
815
|
-
element: rNode.content || rNode.landmarkLabel || "",
|
|
816
|
-
attribute: "role",
|
|
817
|
-
value: rNode.landmarkRole,
|
|
818
|
-
});
|
|
819
|
-
if (rNode.landmarkLabel) {
|
|
820
|
-
reqs.push({
|
|
821
|
-
element: rNode.content || rNode.landmarkLabel,
|
|
822
|
-
attribute: "aria-label",
|
|
823
|
-
value: rNode.landmarkLabel,
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Live regions
|
|
830
|
-
const hasQuantity = tabOrder.some((e) => e.role === "spinbutton");
|
|
831
|
-
if (hasQuantity) {
|
|
832
|
-
reqs.push({
|
|
833
|
-
element: "Item count text",
|
|
834
|
-
attribute: "aria-live",
|
|
835
|
-
value: '"polite"',
|
|
836
|
-
});
|
|
837
|
-
reqs.push({
|
|
838
|
-
element: "Total amount",
|
|
839
|
-
attribute: "aria-live",
|
|
840
|
-
value: '"polite"',
|
|
841
|
-
});
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// Decorative dividers
|
|
845
|
-
reqs.push({
|
|
846
|
-
element: "Decorative dividers",
|
|
847
|
-
attribute: "aria-hidden",
|
|
848
|
-
value: '"true"',
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
return reqs;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function generateKeyboardBehavior(tabOrder: TabOrderEntry[]): KBRule[] {
|
|
855
|
-
const rules: KBRule[] = [];
|
|
856
|
-
const seenRoles = new Set<string>();
|
|
857
|
-
|
|
858
|
-
// Global navigation
|
|
859
|
-
rules.push({
|
|
860
|
-
component: "Entire page",
|
|
861
|
-
key: "Tab",
|
|
862
|
-
action: "Move to next focusable element",
|
|
863
|
-
});
|
|
864
|
-
rules.push({
|
|
865
|
-
component: "Entire page",
|
|
866
|
-
key: "Shift+Tab",
|
|
867
|
-
action: "Move to previous focusable element",
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
for (const entry of tabOrder) {
|
|
871
|
-
if (seenRoles.has(entry.role)) continue;
|
|
872
|
-
seenRoles.add(entry.role);
|
|
873
|
-
|
|
874
|
-
const apgRules = APG_KEYBOARD_MAP[entry.role];
|
|
875
|
-
if (apgRules) {
|
|
876
|
-
rules.push(...apgRules);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
return rules;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
function generateDosDonts(tabOrder: TabOrderEntry[]): string[] {
|
|
884
|
-
const rules: string[] = [];
|
|
885
|
-
|
|
886
|
-
// Dos
|
|
887
|
-
rules.push(
|
|
888
|
-
'Do: Use <button> for all action elements (submit, remove, toggle, increase/decrease)'
|
|
889
|
-
);
|
|
890
|
-
rules.push(
|
|
891
|
-
'Do: Use <a> for elements that navigate to other pages (product links, logo)'
|
|
892
|
-
);
|
|
893
|
-
|
|
894
|
-
const hasTextbox = tabOrder.some((e) => e.role === "textbox");
|
|
895
|
-
if (hasTextbox) {
|
|
896
|
-
rules.push(
|
|
897
|
-
'Do: Use <input type="text"> with a visible <label> for text inputs'
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
const hasSpinbutton = tabOrder.some((e) => e.role === "spinbutton");
|
|
902
|
-
if (hasSpinbutton) {
|
|
903
|
-
rules.push(
|
|
904
|
-
'Do: Group quantity controls (minus, value, plus) in a <div role="group" aria-label="Quantity for [Item]">'
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
rules.push(
|
|
909
|
-
'Do: Use aria-live="polite" on elements that update dynamically (counts, totals)'
|
|
910
|
-
);
|
|
911
|
-
rules.push(
|
|
912
|
-
"Do: Provide aria-label on icon-only buttons and links"
|
|
913
|
-
);
|
|
914
|
-
|
|
915
|
-
// Don'ts
|
|
916
|
-
rules.push(
|
|
917
|
-
"Don't: Use <div> or <span> for interactive elements -- always use semantic HTML"
|
|
918
|
-
);
|
|
919
|
-
rules.push(
|
|
920
|
-
"Don't: Rely on color alone for conveying status or state changes"
|
|
921
|
-
);
|
|
922
|
-
if (hasTextbox) {
|
|
923
|
-
rules.push(
|
|
924
|
-
"Don't: Use placeholder as the only label for inputs"
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
rules.push(
|
|
928
|
-
"Don't: Remove focus outline on any interactive element"
|
|
929
|
-
);
|
|
930
|
-
rules.push(
|
|
931
|
-
"Don't: Use tabindex values greater than 0 -- let DOM order control tab sequence"
|
|
932
|
-
);
|
|
933
|
-
rules.push(
|
|
934
|
-
"Don't: Make decorative dividers or purely decorative images focusable"
|
|
935
|
-
);
|
|
936
|
-
|
|
937
|
-
return rules;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// ─── Warnings & Audit Summary ───────────────────────────────────────────────
|
|
941
|
-
|
|
942
|
-
function generateWarnings(
|
|
943
|
-
allNodes: FigmaNode[],
|
|
944
|
-
interactiveNodes: FigmaNode[],
|
|
945
|
-
textNodes: FigmaNode[]
|
|
946
|
-
): { warnings: Warning[]; summary: AuditSummaryRow[] } {
|
|
947
|
-
const warnings: Warning[] = [];
|
|
948
|
-
const issues: WCAGIssue[] = [];
|
|
949
|
-
let warnId = 1;
|
|
950
|
-
|
|
951
|
-
// Contrast checks
|
|
952
|
-
for (const node of textNodes) {
|
|
953
|
-
const issue = checkTextContrast(node, allNodes, "AA");
|
|
954
|
-
if (issue) issues.push(issue);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Touch target checks
|
|
958
|
-
for (const node of interactiveNodes) {
|
|
959
|
-
const ttIssue = checkTouchTarget(node);
|
|
960
|
-
if (ttIssue) issues.push(ttIssue);
|
|
961
|
-
const tsIssue = checkTargetSizeMinimum(node);
|
|
962
|
-
if (tsIssue) issues.push(tsIssue);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// Reading/focus order checks
|
|
966
|
-
issues.push(...checkMeaningfulSequence(allNodes));
|
|
967
|
-
issues.push(...checkFocusOrder(interactiveNodes));
|
|
968
|
-
issues.push(...checkNameRoleValue(interactiveNodes));
|
|
969
|
-
issues.push(...checkLabelsOrInstructions(interactiveNodes, allNodes));
|
|
970
|
-
|
|
971
|
-
// Group issues into warnings
|
|
972
|
-
const contrastErrors = issues.filter(
|
|
973
|
-
(i) => i.criterion.includes("1.4.3") && i.severity === "error"
|
|
974
|
-
);
|
|
975
|
-
const contrastWarnings = issues.filter(
|
|
976
|
-
(i) => i.criterion.includes("1.4.3") && i.severity === "warning"
|
|
977
|
-
);
|
|
978
|
-
const touchTargetIssues = issues.filter(
|
|
979
|
-
(i) =>
|
|
980
|
-
i.criterion.includes("2.5.5") || i.criterion.includes("2.5.8")
|
|
981
|
-
);
|
|
982
|
-
const nameRoleIssues = issues.filter((i) =>
|
|
983
|
-
i.criterion.includes("4.1.2")
|
|
984
|
-
);
|
|
985
|
-
const focusOrderIssues = issues.filter((i) =>
|
|
986
|
-
i.criterion.includes("2.4.3")
|
|
987
|
-
);
|
|
988
|
-
|
|
989
|
-
if (contrastErrors.length > 0) {
|
|
990
|
-
warnings.push({
|
|
991
|
-
id: warnId++,
|
|
992
|
-
severity: "Critical",
|
|
993
|
-
title: `Contrast failures (${contrastErrors.length} elements)`,
|
|
994
|
-
description: `${contrastErrors.length} text elements fail WCAG AA contrast minimum. Fix by darkening text or lightening background to achieve at least 4.5:1 ratio.`,
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
if (touchTargetIssues.length > 0) {
|
|
999
|
-
warnings.push({
|
|
1000
|
-
id: warnId++,
|
|
1001
|
-
severity: "Critical",
|
|
1002
|
-
title: `Touch target violations (${touchTargetIssues.length} elements)`,
|
|
1003
|
-
description: `${touchTargetIssues.length} interactive elements are below the 44x44px minimum. Apply invisible padding to achieve compliant tap zones.`,
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
if (nameRoleIssues.length > 0) {
|
|
1008
|
-
warnings.push({
|
|
1009
|
-
id: warnId++,
|
|
1010
|
-
severity: "Critical",
|
|
1011
|
-
title: `Missing accessible names (${nameRoleIssues.length} elements)`,
|
|
1012
|
-
description: `${nameRoleIssues.length} interactive elements lack unique accessible names. Screen readers cannot distinguish them. Add aria-label with contextual information.`,
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
if (contrastWarnings.length > 0) {
|
|
1017
|
-
warnings.push({
|
|
1018
|
-
id: warnId++,
|
|
1019
|
-
severity: "Warning",
|
|
1020
|
-
title: `Contrast near-misses (${contrastWarnings.length} elements)`,
|
|
1021
|
-
description: `${contrastWarnings.length} text elements have contrast between 3:1 and 4.5:1. While they pass for large text, they fail for normal text.`,
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (focusOrderIssues.length > 0) {
|
|
1026
|
-
warnings.push({
|
|
1027
|
-
id: warnId++,
|
|
1028
|
-
severity: "Warning",
|
|
1029
|
-
title: `Focus order mismatches (${focusOrderIssues.length})`,
|
|
1030
|
-
description: `${focusOrderIssues.length} interactive elements have a tab order that does not match visual layout. Verify DOM order matches intended reading order.`,
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// Build summary
|
|
1035
|
-
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
1036
|
-
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
1037
|
-
const suggestionCount = issues.filter(
|
|
1038
|
-
(i) => i.severity === "suggestion"
|
|
1039
|
-
).length;
|
|
1040
|
-
|
|
1041
|
-
const summary: AuditSummaryRow[] = [];
|
|
1042
|
-
if (errorCount > 0) {
|
|
1043
|
-
summary.push({
|
|
1044
|
-
severity: "Error",
|
|
1045
|
-
count: errorCount,
|
|
1046
|
-
category: `Critical issues (contrast, touch targets, missing names)`,
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
if (warningCount > 0) {
|
|
1050
|
-
summary.push({
|
|
1051
|
-
severity: "Warning",
|
|
1052
|
-
count: warningCount,
|
|
1053
|
-
category: `Potential issues (near-miss contrast, focus order, fixed containers)`,
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
if (suggestionCount > 0) {
|
|
1057
|
-
summary.push({
|
|
1058
|
-
severity: "Suggestion",
|
|
1059
|
-
count: suggestionCount,
|
|
1060
|
-
category: `Improvement opportunities`,
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
const totalIssues = errorCount + warningCount + suggestionCount;
|
|
1065
|
-
const totalChecks = allNodes.length;
|
|
1066
|
-
const passRate =
|
|
1067
|
-
totalChecks > 0
|
|
1068
|
-
? Math.round(
|
|
1069
|
-
((totalChecks - totalIssues) / totalChecks) * 100
|
|
1070
|
-
)
|
|
1071
|
-
: 100;
|
|
1072
|
-
summary.push({
|
|
1073
|
-
severity: "Total",
|
|
1074
|
-
count: totalIssues,
|
|
1075
|
-
category: `issues across ${totalChecks} checks (${passRate}% pass rate)`,
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
return { warnings, summary };
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// ─── Scope & Assumptions Generation ─────────────────────────────────────────
|
|
1082
|
-
|
|
1083
|
-
function generateScope(
|
|
1084
|
-
node: FigmaNode,
|
|
1085
|
-
interactiveNodes: FigmaNode[],
|
|
1086
|
-
landmarks: ReadingOrderNode[]
|
|
1087
|
-
): string {
|
|
1088
|
-
const parts: string[] = [];
|
|
1089
|
-
parts.push(`Complete ${node.name} page including:`);
|
|
1090
|
-
|
|
1091
|
-
const landmarkNames = landmarks
|
|
1092
|
-
.filter((l) => l.landmarkRole)
|
|
1093
|
-
.map((l) => l.landmarkLabel || l.content);
|
|
1094
|
-
if (landmarkNames.length > 0) {
|
|
1095
|
-
parts.push(landmarkNames.join(", "));
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
const roleGroups = new Map<string, number>();
|
|
1099
|
-
for (const n of interactiveNodes) {
|
|
1100
|
-
const role = inferRole(n) || "interactive element";
|
|
1101
|
-
roleGroups.set(role, (roleGroups.get(role) || 0) + 1);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
const roleSummary = Array.from(roleGroups.entries())
|
|
1105
|
-
.map(([role, count]) => `${count} ${role}${count > 1 ? "s" : ""}`)
|
|
1106
|
-
.join(", ");
|
|
1107
|
-
if (roleSummary) parts.push(roleSummary);
|
|
1108
|
-
|
|
1109
|
-
return parts.join(" ");
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function generateAssumptions(node: FigmaNode): string[] {
|
|
1113
|
-
return [
|
|
1114
|
-
`Page is in default/resting state`,
|
|
1115
|
-
`No validation errors are visible`,
|
|
1116
|
-
`No modal dialogs are open`,
|
|
1117
|
-
`All interactive elements are enabled unless noted`,
|
|
1118
|
-
`Content matches the current Figma frame "${node.name}"`,
|
|
1119
|
-
];
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
// ─── Main Analysis Function ─────────────────────────────────────────────────
|
|
1123
|
-
|
|
1124
|
-
export function analyzeKeyboardAndScreenReaderOrder(
|
|
1125
|
-
rootNode: FigmaNode,
|
|
1126
|
-
nodeId: string
|
|
1127
|
-
): A11yOrderAnalysis {
|
|
1128
|
-
// Set root node for navigation context lookups
|
|
1129
|
-
_analysisRootNode = rootNode;
|
|
1130
|
-
|
|
1131
|
-
// Collect all nodes
|
|
1132
|
-
const allNodes = collectAll(rootNode);
|
|
1133
|
-
const textNodes = allNodes.filter((n) => n.type === "TEXT");
|
|
1134
|
-
const interactiveNodes = allNodes.filter((n) => isInteractive(n));
|
|
1135
|
-
|
|
1136
|
-
// Compute tab order
|
|
1137
|
-
const keyboardTabOrder = computeTabOrder(interactiveNodes, allNodes);
|
|
1138
|
-
|
|
1139
|
-
// Build reading order
|
|
1140
|
-
const screenReaderReadingOrder = buildReadingOrder(rootNode);
|
|
1141
|
-
|
|
1142
|
-
// Generate interaction announcements
|
|
1143
|
-
const onFocus = generateFocusAnnouncements(keyboardTabOrder);
|
|
1144
|
-
const stateChanges = generateStateChanges(keyboardTabOrder);
|
|
1145
|
-
|
|
1146
|
-
// Focus management rules
|
|
1147
|
-
const focusManagement = generateFocusRules(keyboardTabOrder);
|
|
1148
|
-
|
|
1149
|
-
// Implementation notes
|
|
1150
|
-
const ariaTable = generateAriaRequirements(
|
|
1151
|
-
keyboardTabOrder,
|
|
1152
|
-
screenReaderReadingOrder
|
|
1153
|
-
);
|
|
1154
|
-
const keyboardBehavior = generateKeyboardBehavior(keyboardTabOrder);
|
|
1155
|
-
const dosDonts = generateDosDonts(keyboardTabOrder);
|
|
1156
|
-
|
|
1157
|
-
// Warnings & audit summary
|
|
1158
|
-
const { warnings, summary } = generateWarnings(
|
|
1159
|
-
allNodes,
|
|
1160
|
-
interactiveNodes,
|
|
1161
|
-
textNodes
|
|
1162
|
-
);
|
|
1163
|
-
|
|
1164
|
-
// Scope & assumptions
|
|
1165
|
-
const scope = generateScope(
|
|
1166
|
-
rootNode,
|
|
1167
|
-
interactiveNodes,
|
|
1168
|
-
screenReaderReadingOrder
|
|
1169
|
-
);
|
|
1170
|
-
const assumptions = generateAssumptions(rootNode);
|
|
1171
|
-
|
|
1172
|
-
const today = new Date().toISOString().split("T")[0];
|
|
1173
|
-
|
|
1174
|
-
return {
|
|
1175
|
-
header: {
|
|
1176
|
-
frameName: rootNode.name,
|
|
1177
|
-
nodeId,
|
|
1178
|
-
date: today,
|
|
1179
|
-
standard: "WAI-ARIA APG + WCAG 2.1 AA",
|
|
1180
|
-
},
|
|
1181
|
-
scope,
|
|
1182
|
-
assumptions,
|
|
1183
|
-
keyboardTabOrder,
|
|
1184
|
-
screenReaderReadingOrder,
|
|
1185
|
-
interactionAnnouncements: { onFocus, stateChanges },
|
|
1186
|
-
focusManagement,
|
|
1187
|
-
implementationNotes: { ariaTable, keyboardBehavior, dosDonts },
|
|
1188
|
-
warnings,
|
|
1189
|
-
auditSummary: summary,
|
|
1190
|
-
};
|
|
1191
|
-
}
|