@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,2019 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Page Architect
|
|
3
|
+
// Parses a natural-language flow description into a sequence of screens and
|
|
4
|
+
// builds them in Figma using Auto Layout frames populated with DS components.
|
|
5
|
+
// Optionally wires prototype connections and generates a flow-map page.
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
import Fuse from "fuse.js";
|
|
9
|
+
import { getBridge } from "../../../shared/figma-bridge.js";
|
|
10
|
+
import { decisionLog } from "../../../shared/decision-log.js";
|
|
11
|
+
import { ComponentSet, Token } from "../../../shared/types.js";
|
|
12
|
+
import { buildWireScript } from "../../../shared/prototype-script-builder.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveDesignPalette,
|
|
15
|
+
resolveFloatToken,
|
|
16
|
+
ResolvedPalette,
|
|
17
|
+
} from "../../../shared/token-binder.js";
|
|
18
|
+
import {
|
|
19
|
+
fetchRemoteImageAsDataUri,
|
|
20
|
+
searchUnsplashPhotos,
|
|
21
|
+
trackUnsplashDownload,
|
|
22
|
+
UnsplashOrientation,
|
|
23
|
+
} from "../../../shared/unsplash.js";
|
|
24
|
+
import { FontConfig, resolveFontConfig, generateFontLoadScript, fontNameLiteral } from "../../../shared/font-config.js";
|
|
25
|
+
import { generateValidatorScript, generateValidatorCall, generateDocumentRepairScript, generateDocumentRepairCall } from "../../../shared/auto-layout-validator.js";
|
|
26
|
+
|
|
27
|
+
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface PageArchitectArgs {
|
|
30
|
+
productContext: string;
|
|
31
|
+
flow: string;
|
|
32
|
+
platform: "web" | "mobile" | "both";
|
|
33
|
+
width?: number;
|
|
34
|
+
wireframeMode?: boolean;
|
|
35
|
+
includeFlowMap?: boolean;
|
|
36
|
+
contentMode: "placeholder" | "realistic";
|
|
37
|
+
useStockImages?: boolean;
|
|
38
|
+
imageQuery?: string;
|
|
39
|
+
fonts?: Partial<FontConfig>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ScreenSpec {
|
|
43
|
+
name: string;
|
|
44
|
+
purpose: string;
|
|
45
|
+
template: ScreenTemplate;
|
|
46
|
+
requiredComponents: string[];
|
|
47
|
+
layoutPattern: string;
|
|
48
|
+
realisticContent?: ScreenContent;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ScreenContent {
|
|
52
|
+
heading?: string;
|
|
53
|
+
subheading?: string;
|
|
54
|
+
bodyText?: string;
|
|
55
|
+
ctaLabel?: string;
|
|
56
|
+
listItems?: string[];
|
|
57
|
+
sectionTitle?: string;
|
|
58
|
+
summaryItems?: string[];
|
|
59
|
+
helperText?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CreatedScreen {
|
|
63
|
+
frameId: string;
|
|
64
|
+
name: string;
|
|
65
|
+
template: ScreenTemplate;
|
|
66
|
+
instantiatedComponents: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PrototypeConnection {
|
|
70
|
+
fromFrameId: string;
|
|
71
|
+
toFrameId: string;
|
|
72
|
+
fromName: string;
|
|
73
|
+
toName: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PageArchitectResult {
|
|
77
|
+
screens: CreatedScreen[];
|
|
78
|
+
prototypeConnections: PrototypeConnection[];
|
|
79
|
+
flowMapPageId: string | null;
|
|
80
|
+
frameIds: string[];
|
|
81
|
+
prototypeConnectionCount: number;
|
|
82
|
+
logEntryId: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Screen templates ─────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
type ScreenTemplate =
|
|
88
|
+
| "auth"
|
|
89
|
+
| "dashboard"
|
|
90
|
+
| "list"
|
|
91
|
+
| "detail"
|
|
92
|
+
| "settings"
|
|
93
|
+
| "onboarding"
|
|
94
|
+
| "checkout-cart"
|
|
95
|
+
| "checkout-address"
|
|
96
|
+
| "checkout-shipping"
|
|
97
|
+
| "checkout-payment"
|
|
98
|
+
| "checkout-review"
|
|
99
|
+
| "checkout-success"
|
|
100
|
+
| "document"
|
|
101
|
+
| "generic";
|
|
102
|
+
|
|
103
|
+
interface TemplateDefinition {
|
|
104
|
+
template: ScreenTemplate;
|
|
105
|
+
keywords: string[];
|
|
106
|
+
components: string[];
|
|
107
|
+
layoutPattern: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const SCREEN_TEMPLATES: TemplateDefinition[] = [
|
|
111
|
+
{
|
|
112
|
+
template: "auth",
|
|
113
|
+
keywords: ["login", "sign in", "signup", "register", "auth", "password", "email", "forgot"],
|
|
114
|
+
components: ["TextInput", "Button", "Heading", "Link", "Divider", "SocialButton", "Checkbox", "Logo", "Avatar"],
|
|
115
|
+
layoutPattern: "Centered single-column with email + password inputs and primary CTA",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
template: "dashboard",
|
|
119
|
+
keywords: ["dashboard", "home", "overview", "main", "hub"],
|
|
120
|
+
components: ["Navigation", "Header", "Card", "Chart", "Badge", "Avatar", "Table"],
|
|
121
|
+
layoutPattern: "Sidebar nav + top header + main content grid",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
template: "list",
|
|
125
|
+
keywords: ["list", "feed", "search", "browse", "explore", "results", "index", "directory"],
|
|
126
|
+
components: ["SearchBar", "Filter", "ListItem", "Pagination", "FilterChip", "Thumbnail"],
|
|
127
|
+
layoutPattern: "Search/filter header + scrollable list of items",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
template: "detail",
|
|
131
|
+
keywords: ["detail", "view", "show", "profile", "product", "article", "post", "item"],
|
|
132
|
+
components: ["Image", "Heading", "Body", "Button", "ActionBar", "BackButton", "ShareButton", "Rating", "Tag"],
|
|
133
|
+
layoutPattern: "Hero image + content area + sticky action bar",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
template: "settings",
|
|
137
|
+
keywords: ["settings", "preferences", "account", "profile edit", "configuration", "options"],
|
|
138
|
+
components: ["SectionHeader", "FormField", "Toggle", "Button"],
|
|
139
|
+
layoutPattern: "Grouped form sections with labels and inputs",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
template: "onboarding",
|
|
143
|
+
keywords: ["onboarding", "welcome", "intro", "get started", "tutorial", "step", "walkthrough"],
|
|
144
|
+
components: ["Illustration", "Heading", "Body", "Button"],
|
|
145
|
+
layoutPattern: "Full-bleed illustration + centered heading + body + next button",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
template: "checkout-cart",
|
|
149
|
+
keywords: ["cart", "bag", "basket"],
|
|
150
|
+
components: ["Navigation", "ProductCard", "QuantityStepper", "PromoCode", "OrderSummary", "Button"],
|
|
151
|
+
layoutPattern: "Product list with summary card and bottom CTA",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
template: "checkout-address",
|
|
155
|
+
keywords: ["address", "delivery address", "shipping address"],
|
|
156
|
+
components: ["Navigation", "AddressCard", "TextInput", "Button"],
|
|
157
|
+
layoutPattern: "Saved address selection with editable delivery form",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
template: "checkout-shipping",
|
|
161
|
+
keywords: ["shipping", "delivery", "method"],
|
|
162
|
+
components: ["Navigation", "RadioOption", "OrderSummary", "Button"],
|
|
163
|
+
layoutPattern: "Shipping method options with delivery ETA and summary",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
template: "checkout-payment",
|
|
167
|
+
keywords: ["payment", "card", "billing", "wallet"],
|
|
168
|
+
components: ["Navigation", "PaymentMethodRow", "TextInput", "Button"],
|
|
169
|
+
layoutPattern: "Saved payment methods, billing fields, and trust messaging",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
template: "checkout-review",
|
|
173
|
+
keywords: ["review", "confirm", "place order", "order review"],
|
|
174
|
+
components: ["Navigation", "OrderSummary", "AddressCard", "PaymentMethodRow", "Button"],
|
|
175
|
+
layoutPattern: "Sectioned review screen with final totals and place-order CTA",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
template: "checkout-success",
|
|
179
|
+
keywords: ["success", "confirmation", "complete", "thank you"],
|
|
180
|
+
components: ["Illustration", "Heading", "OrderSummary", "Button"],
|
|
181
|
+
layoutPattern: "Confirmation state with receipt summary and next steps",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
template: "document",
|
|
185
|
+
keywords: ["document", "documentation", "spec", "specification", "guidelines", "style guide", "reference", "wiki", "readme", "changelog", "api doc", "design doc"],
|
|
186
|
+
components: ["Heading", "Body", "Divider", "Table", "Badge"],
|
|
187
|
+
layoutPattern: "Full-width document page with header, TOC, and content sections",
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
// ─── Platform widths ──────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
function platformWidth(platform: "web" | "mobile" | "both", userWidth?: number): number {
|
|
194
|
+
if (userWidth) return userWidth;
|
|
195
|
+
switch (platform) {
|
|
196
|
+
case "mobile": return 390;
|
|
197
|
+
case "web": return 1440;
|
|
198
|
+
case "both": return 1440;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function mobileWidth(_platform: "web" | "mobile" | "both"): number | null {
|
|
203
|
+
return null; // single-frame output — no duplicate (Mobile) frames
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Parse flow into screen specs (template matching) ────────────────────────
|
|
207
|
+
|
|
208
|
+
function parseFlowToScreens(
|
|
209
|
+
productContext: string,
|
|
210
|
+
flow: string,
|
|
211
|
+
contentMode: "placeholder" | "realistic"
|
|
212
|
+
): ScreenSpec[] {
|
|
213
|
+
const rawParts = flow.split(/\s*(?:→|->|>>|,|\n)\s*/);
|
|
214
|
+
let screenNames = rawParts
|
|
215
|
+
.map((s) => s.trim())
|
|
216
|
+
.filter((s) => s.length > 2 && s.length < 60)
|
|
217
|
+
.filter((s) => {
|
|
218
|
+
const lower = s.toLowerCase();
|
|
219
|
+
// Keep if it matches any template keyword
|
|
220
|
+
const hasKeyword = SCREEN_TEMPLATES.some((t) =>
|
|
221
|
+
t.keywords.some((kw) => lower.includes(kw))
|
|
222
|
+
);
|
|
223
|
+
// Keep if it looks like a proper name (capital letter or common UI term)
|
|
224
|
+
const looksLikeName =
|
|
225
|
+
/[A-Z]/.test(s) ||
|
|
226
|
+
/^(home|profile|search|feed|inbox|cart|map)$/i.test(lower);
|
|
227
|
+
return hasKeyword || looksLikeName || s.split(/\s+/).length <= 3;
|
|
228
|
+
})
|
|
229
|
+
.slice(0, 8);
|
|
230
|
+
|
|
231
|
+
// If we filtered out most fragments, treat the whole input as one screen
|
|
232
|
+
if (
|
|
233
|
+
screenNames.length === 0 ||
|
|
234
|
+
(rawParts.length > 3 && screenNames.length <= 1)
|
|
235
|
+
) {
|
|
236
|
+
screenNames = [flow.trim().slice(0, 80)];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const parsed = screenNames.map((name) => ({
|
|
240
|
+
name: name.replace(/\s+/g, " ").trim(),
|
|
241
|
+
purpose: `User navigates to ${name}`,
|
|
242
|
+
templateHint: "generic" as string | undefined,
|
|
243
|
+
requiredComponents: ["Heading", "Button"] as string[],
|
|
244
|
+
layoutPattern: "Generic content frame",
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
return parsed.map((item, index) => {
|
|
248
|
+
const templateDef =
|
|
249
|
+
SCREEN_TEMPLATES.find(
|
|
250
|
+
(t) =>
|
|
251
|
+
t.template === item.templateHint ||
|
|
252
|
+
t.keywords.some((kw) => item.name.toLowerCase().includes(kw))
|
|
253
|
+
) ?? null;
|
|
254
|
+
|
|
255
|
+
const resolvedTemplate =
|
|
256
|
+
(item.templateHint as ScreenTemplate) ?? templateDef?.template ?? "generic";
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
name: item.name,
|
|
260
|
+
purpose: item.purpose,
|
|
261
|
+
template: resolvedTemplate,
|
|
262
|
+
requiredComponents:
|
|
263
|
+
item.requiredComponents ??
|
|
264
|
+
templateDef?.components ??
|
|
265
|
+
["Heading", "Button"],
|
|
266
|
+
layoutPattern:
|
|
267
|
+
item.layoutPattern ??
|
|
268
|
+
templateDef?.layoutPattern ??
|
|
269
|
+
"Vertical stack",
|
|
270
|
+
realisticContent:
|
|
271
|
+
contentMode === "realistic"
|
|
272
|
+
? buildRealisticContent(resolvedTemplate, item.name, productContext, index)
|
|
273
|
+
: undefined,
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildRealisticContent(
|
|
279
|
+
template: ScreenTemplate,
|
|
280
|
+
name: string,
|
|
281
|
+
productContext: string,
|
|
282
|
+
index: number
|
|
283
|
+
): ScreenContent {
|
|
284
|
+
const brandLabel = /skincare|beauty|cosmetic/.test(productContext.toLowerCase())
|
|
285
|
+
? "Lumin Daily"
|
|
286
|
+
: /fashion|apparel/.test(productContext.toLowerCase())
|
|
287
|
+
? "Studio North"
|
|
288
|
+
: "Northstar";
|
|
289
|
+
|
|
290
|
+
switch (template) {
|
|
291
|
+
case "checkout-cart":
|
|
292
|
+
return {
|
|
293
|
+
heading: "Your bag",
|
|
294
|
+
subheading: "Review items, delivery timing, and savings before checkout.",
|
|
295
|
+
ctaLabel: "Continue to address",
|
|
296
|
+
sectionTitle: `${brandLabel} favorites`,
|
|
297
|
+
listItems: ["Vitamin C Serum x1 $48", "Barrier Repair Cream x1 $36", "Free sample kit included"],
|
|
298
|
+
summaryItems: ["Subtotal $84", "Shipping Free", "Tax $6", "Total $90"],
|
|
299
|
+
helperText: "Promo code SAVE10 applied",
|
|
300
|
+
};
|
|
301
|
+
case "checkout-address":
|
|
302
|
+
return {
|
|
303
|
+
heading: "Delivery address",
|
|
304
|
+
subheading: "Choose where your order should arrive.",
|
|
305
|
+
ctaLabel: "Continue to shipping",
|
|
306
|
+
sectionTitle: "Saved addresses",
|
|
307
|
+
listItems: ["Home 21 Lake Shore Drive, Apt 6B", "Office 101 Market Street, Floor 9"],
|
|
308
|
+
helperText: "Add delivery instructions for the courier",
|
|
309
|
+
};
|
|
310
|
+
case "checkout-shipping":
|
|
311
|
+
return {
|
|
312
|
+
heading: "Shipping method",
|
|
313
|
+
subheading: "Select the delivery speed that works best for you.",
|
|
314
|
+
ctaLabel: "Continue to payment",
|
|
315
|
+
sectionTitle: "Delivery options",
|
|
316
|
+
listItems: ["Standard Free Arrives Tue, Mar 17", "Express $12 Arrives Mon, Mar 16", "Same day $18 Arrives today by 9 PM"],
|
|
317
|
+
summaryItems: ["Items 2", "Estimated delivery 2-3 business days"],
|
|
318
|
+
};
|
|
319
|
+
case "checkout-payment":
|
|
320
|
+
return {
|
|
321
|
+
heading: "Payment method",
|
|
322
|
+
subheading: "Your card details are encrypted and secure.",
|
|
323
|
+
ctaLabel: "Review order",
|
|
324
|
+
sectionTitle: "Saved methods",
|
|
325
|
+
listItems: ["Visa ending in 4242", "Apple Pay", "Add new card"],
|
|
326
|
+
helperText: "Billing address same as delivery",
|
|
327
|
+
};
|
|
328
|
+
case "checkout-review":
|
|
329
|
+
return {
|
|
330
|
+
heading: "Review your order",
|
|
331
|
+
subheading: "Double-check delivery, payment, and totals before placing the order.",
|
|
332
|
+
ctaLabel: "Place order",
|
|
333
|
+
sectionTitle: "Order summary",
|
|
334
|
+
listItems: ["Delivery to Maya Patel", "Payment Visa 4242", "Standard shipping Free"],
|
|
335
|
+
summaryItems: ["Subtotal $84", "Discount -$8", "Tax $6", "Total $82"],
|
|
336
|
+
helperText: "By placing this order, you agree to the terms and refund policy.",
|
|
337
|
+
};
|
|
338
|
+
case "checkout-success":
|
|
339
|
+
return {
|
|
340
|
+
heading: "Order confirmed",
|
|
341
|
+
subheading: "Thanks for shopping with us. A receipt has been sent to your email.",
|
|
342
|
+
ctaLabel: "Track shipment",
|
|
343
|
+
sectionTitle: "What happens next",
|
|
344
|
+
listItems: ["Order #NS-2048", "Estimated arrival Tue, Mar 17", "Receipt sent to maya@example.com"],
|
|
345
|
+
summaryItems: ["Need help? Contact support 24/7"],
|
|
346
|
+
};
|
|
347
|
+
case "detail":
|
|
348
|
+
return {
|
|
349
|
+
heading: name,
|
|
350
|
+
subheading: "Thoughtful product details, benefits, and a clear path to continue.",
|
|
351
|
+
ctaLabel: "Continue",
|
|
352
|
+
};
|
|
353
|
+
case "document":
|
|
354
|
+
return {
|
|
355
|
+
heading: name,
|
|
356
|
+
subheading: `Comprehensive documentation for ${productContext}.`,
|
|
357
|
+
sectionTitle: "Table of Contents",
|
|
358
|
+
listItems: ["1. Overview & Purpose", "2. Usage Guidelines", "3. Properties & API", "4. Accessibility", "5. Examples"],
|
|
359
|
+
bodyText: "This document provides detailed specifications, usage patterns, and implementation guidance.",
|
|
360
|
+
};
|
|
361
|
+
default:
|
|
362
|
+
return {
|
|
363
|
+
heading: name,
|
|
364
|
+
subheading: `Designed for ${productContext}.`,
|
|
365
|
+
ctaLabel: index === 0 ? "Get started" : "Continue",
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ─── AI Content Bundle ────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
interface ContentBundle {
|
|
373
|
+
brand: { name: string; tagline: string };
|
|
374
|
+
user: { name: string; email: string };
|
|
375
|
+
order: {
|
|
376
|
+
number: string;
|
|
377
|
+
items: Array<{ name: string; price: string; qty: number }>;
|
|
378
|
+
subtotal: string;
|
|
379
|
+
shipping: string;
|
|
380
|
+
tax: string;
|
|
381
|
+
total: string;
|
|
382
|
+
discount?: string;
|
|
383
|
+
estimatedDelivery: string;
|
|
384
|
+
};
|
|
385
|
+
address: { home: string; work: string };
|
|
386
|
+
imageQuery: string;
|
|
387
|
+
screens: Array<{
|
|
388
|
+
screenName: string;
|
|
389
|
+
heading: string;
|
|
390
|
+
subheading: string;
|
|
391
|
+
ctaLabel: string;
|
|
392
|
+
listItems?: string[];
|
|
393
|
+
summaryItems?: string[];
|
|
394
|
+
sectionTitle?: string;
|
|
395
|
+
helperText?: string;
|
|
396
|
+
bodyText?: string;
|
|
397
|
+
}>;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function generateContentBundle(
|
|
401
|
+
productContext: string,
|
|
402
|
+
screenNames: string[]
|
|
403
|
+
): Promise<ContentBundle | null> {
|
|
404
|
+
const apiKey = process.env.ANTHROPIC_API_KEY?.trim();
|
|
405
|
+
if (!apiKey) return null;
|
|
406
|
+
|
|
407
|
+
const prompt = `You are a UI content writer. Generate a realistic content bundle for a "${productContext}" app.
|
|
408
|
+
|
|
409
|
+
Screens: ${screenNames.join(", ")}
|
|
410
|
+
|
|
411
|
+
Return ONLY valid JSON (no markdown, no extra text):
|
|
412
|
+
{
|
|
413
|
+
"brand": { "name": "string", "tagline": "string" },
|
|
414
|
+
"user": { "name": "string", "email": "string" },
|
|
415
|
+
"order": {
|
|
416
|
+
"number": "string",
|
|
417
|
+
"items": [{ "name": "string", "price": "string", "qty": 1 }],
|
|
418
|
+
"subtotal": "string", "shipping": "string", "tax": "string", "total": "string",
|
|
419
|
+
"discount": "string", "estimatedDelivery": "string"
|
|
420
|
+
},
|
|
421
|
+
"address": { "home": "string", "work": "string" },
|
|
422
|
+
"imageQuery": "string",
|
|
423
|
+
"screens": [
|
|
424
|
+
{
|
|
425
|
+
"screenName": "string",
|
|
426
|
+
"heading": "string",
|
|
427
|
+
"subheading": "string",
|
|
428
|
+
"ctaLabel": "string",
|
|
429
|
+
"listItems": ["string"],
|
|
430
|
+
"summaryItems": ["string"],
|
|
431
|
+
"sectionTitle": "string",
|
|
432
|
+
"helperText": "string",
|
|
433
|
+
"bodyText": "string"
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
Rules:
|
|
439
|
+
- Brand name, user name, products, and prices must fit the product context
|
|
440
|
+
- imageQuery: a specific Unsplash-friendly photographic term (e.g. "artisan coffee overhead shot")
|
|
441
|
+
- listItems and summaryItems: real relevant data, not generic placeholder text
|
|
442
|
+
- Keep text concise like a real production app (subheading max 120 chars, bodyText max 150 chars)
|
|
443
|
+
- screens array must have one entry per screen in the same order provided`;
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers: {
|
|
449
|
+
"Content-Type": "application/json",
|
|
450
|
+
"x-api-key": apiKey,
|
|
451
|
+
"anthropic-version": "2023-06-01",
|
|
452
|
+
},
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
model: "claude-haiku-4-5-20251001",
|
|
455
|
+
max_tokens: 2048,
|
|
456
|
+
messages: [{ role: "user", content: prompt }],
|
|
457
|
+
}),
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (!response.ok) {
|
|
461
|
+
console.warn(`generateContentBundle: API error ${response.status}`);
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const data = await response.json() as { content: Array<{ type: string; text: string }> };
|
|
466
|
+
const text = data.content?.[0]?.text ?? "";
|
|
467
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
468
|
+
if (!jsonMatch) return null;
|
|
469
|
+
|
|
470
|
+
return JSON.parse(jsonMatch[0]) as ContentBundle;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.warn(`generateContentBundle: ${error instanceof Error ? error.message : String(error)}`);
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function applyBundleToScreen(
|
|
478
|
+
template: ScreenTemplate,
|
|
479
|
+
screenName: string,
|
|
480
|
+
bundle: ContentBundle,
|
|
481
|
+
index: number
|
|
482
|
+
): ScreenContent {
|
|
483
|
+
const screenData =
|
|
484
|
+
bundle.screens.find((s) => s.screenName.toLowerCase() === screenName.toLowerCase()) ??
|
|
485
|
+
bundle.screens[index] ??
|
|
486
|
+
bundle.screens[0];
|
|
487
|
+
|
|
488
|
+
if (!screenData) return buildRealisticContent(template, screenName, "", index);
|
|
489
|
+
|
|
490
|
+
switch (template) {
|
|
491
|
+
case "checkout-cart":
|
|
492
|
+
return {
|
|
493
|
+
heading: screenData.heading || "Your bag",
|
|
494
|
+
subheading: screenData.subheading,
|
|
495
|
+
ctaLabel: screenData.ctaLabel || "Continue to address",
|
|
496
|
+
sectionTitle: screenData.sectionTitle || bundle.brand.name,
|
|
497
|
+
listItems: bundle.order.items.map((i) => `${i.name} x${i.qty} ${i.price}`),
|
|
498
|
+
summaryItems: [
|
|
499
|
+
`Subtotal ${bundle.order.subtotal}`,
|
|
500
|
+
`Shipping ${bundle.order.shipping}`,
|
|
501
|
+
`Tax ${bundle.order.tax}`,
|
|
502
|
+
`Total ${bundle.order.total}`,
|
|
503
|
+
],
|
|
504
|
+
helperText: screenData.helperText,
|
|
505
|
+
};
|
|
506
|
+
case "checkout-address":
|
|
507
|
+
return {
|
|
508
|
+
heading: screenData.heading || "Delivery address",
|
|
509
|
+
subheading: screenData.subheading,
|
|
510
|
+
ctaLabel: screenData.ctaLabel || "Continue to shipping",
|
|
511
|
+
sectionTitle: screenData.sectionTitle || "Saved addresses",
|
|
512
|
+
listItems: [`Home ${bundle.address.home}`, `Work ${bundle.address.work}`],
|
|
513
|
+
helperText: screenData.helperText,
|
|
514
|
+
};
|
|
515
|
+
case "checkout-shipping":
|
|
516
|
+
return {
|
|
517
|
+
heading: screenData.heading || "Shipping method",
|
|
518
|
+
subheading: screenData.subheading,
|
|
519
|
+
ctaLabel: screenData.ctaLabel || "Continue to payment",
|
|
520
|
+
sectionTitle: screenData.sectionTitle || "Delivery options",
|
|
521
|
+
listItems: screenData.listItems ?? [
|
|
522
|
+
`Standard Free Arrives ${bundle.order.estimatedDelivery}`,
|
|
523
|
+
"Express $12 Arrives tomorrow",
|
|
524
|
+
"Same day $18 Arrives today by 9 PM",
|
|
525
|
+
],
|
|
526
|
+
summaryItems: screenData.summaryItems ?? [
|
|
527
|
+
`Items ${bundle.order.items.length}`,
|
|
528
|
+
`Estimated delivery ${bundle.order.estimatedDelivery}`,
|
|
529
|
+
],
|
|
530
|
+
};
|
|
531
|
+
case "checkout-payment":
|
|
532
|
+
return {
|
|
533
|
+
heading: screenData.heading || "Payment method",
|
|
534
|
+
subheading: screenData.subheading,
|
|
535
|
+
ctaLabel: screenData.ctaLabel || "Review order",
|
|
536
|
+
sectionTitle: screenData.sectionTitle || "Saved methods",
|
|
537
|
+
listItems: screenData.listItems ?? ["Visa ending in 4242", "Apple Pay", "Add new card"],
|
|
538
|
+
helperText: screenData.helperText,
|
|
539
|
+
};
|
|
540
|
+
case "checkout-review":
|
|
541
|
+
return {
|
|
542
|
+
heading: screenData.heading || "Review your order",
|
|
543
|
+
subheading: screenData.subheading,
|
|
544
|
+
ctaLabel: screenData.ctaLabel || "Place order",
|
|
545
|
+
sectionTitle: screenData.sectionTitle || "Order summary",
|
|
546
|
+
listItems: [
|
|
547
|
+
`Delivery to ${bundle.user.name}`,
|
|
548
|
+
"Payment Visa 4242",
|
|
549
|
+
bundle.order.shipping === "Free" ? "Standard shipping Free" : `Shipping ${bundle.order.shipping}`,
|
|
550
|
+
],
|
|
551
|
+
summaryItems: [
|
|
552
|
+
`Subtotal ${bundle.order.subtotal}`,
|
|
553
|
+
...(bundle.order.discount ? [`Discount -${bundle.order.discount}`] : []),
|
|
554
|
+
`Tax ${bundle.order.tax}`,
|
|
555
|
+
`Total ${bundle.order.total}`,
|
|
556
|
+
],
|
|
557
|
+
helperText: screenData.helperText,
|
|
558
|
+
};
|
|
559
|
+
case "checkout-success":
|
|
560
|
+
return {
|
|
561
|
+
heading: screenData.heading || "Order confirmed",
|
|
562
|
+
subheading: screenData.subheading || `Thanks for shopping with ${bundle.brand.name}. A receipt has been sent to your email.`,
|
|
563
|
+
ctaLabel: screenData.ctaLabel || "Track shipment",
|
|
564
|
+
sectionTitle: screenData.sectionTitle || "What happens next",
|
|
565
|
+
listItems: [
|
|
566
|
+
`Order ${bundle.order.number}`,
|
|
567
|
+
`Estimated arrival ${bundle.order.estimatedDelivery}`,
|
|
568
|
+
`Receipt sent to ${bundle.user.email}`,
|
|
569
|
+
],
|
|
570
|
+
summaryItems: ["Need help? Contact support 24/7"],
|
|
571
|
+
};
|
|
572
|
+
default:
|
|
573
|
+
return {
|
|
574
|
+
heading: screenData.heading,
|
|
575
|
+
subheading: screenData.subheading,
|
|
576
|
+
ctaLabel: screenData.ctaLabel,
|
|
577
|
+
sectionTitle: screenData.sectionTitle,
|
|
578
|
+
listItems: screenData.listItems,
|
|
579
|
+
summaryItems: screenData.summaryItems,
|
|
580
|
+
helperText: screenData.helperText,
|
|
581
|
+
bodyText: screenData.bodyText,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ─── DS component matching ────────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
function buildFuse(sets: ComponentSet[]): Fuse<ComponentSet> {
|
|
589
|
+
return new Fuse(sets, {
|
|
590
|
+
keys: ["name", "description"],
|
|
591
|
+
threshold: 0.45,
|
|
592
|
+
includeScore: true,
|
|
593
|
+
minMatchCharLength: 2,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function resolveComponents(
|
|
598
|
+
requiredNames: string[],
|
|
599
|
+
fuse: Fuse<ComponentSet>
|
|
600
|
+
): Array<{ name: string; nodeId: string | null }> {
|
|
601
|
+
return requiredNames.map((name) => {
|
|
602
|
+
const results = fuse.search(name);
|
|
603
|
+
if (results.length === 0) return { name, nodeId: null };
|
|
604
|
+
const best = results[0];
|
|
605
|
+
const firstChild = best.item.children[0];
|
|
606
|
+
return {
|
|
607
|
+
name: best.item.name,
|
|
608
|
+
nodeId: firstChild?.id ?? null,
|
|
609
|
+
};
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ─── Figma script: build screen frame ────────────────────────────────────────
|
|
614
|
+
|
|
615
|
+
interface FrameSpec {
|
|
616
|
+
name: string;
|
|
617
|
+
purpose: string;
|
|
618
|
+
width: number;
|
|
619
|
+
template: ScreenTemplate;
|
|
620
|
+
wireframeMode: boolean;
|
|
621
|
+
contentMode: "placeholder" | "realistic";
|
|
622
|
+
content?: ScreenContent;
|
|
623
|
+
resolvedComponents: Array<{ name: string; nodeId: string | null }>;
|
|
624
|
+
xOffset: number;
|
|
625
|
+
imageHash?: string | null;
|
|
626
|
+
resolvedPalette?: ResolvedPalette;
|
|
627
|
+
fontConfig: FontConfig;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
interface ScreenImagery {
|
|
631
|
+
imageHash: string;
|
|
632
|
+
sourceUrl: string;
|
|
633
|
+
photographerName: string;
|
|
634
|
+
photographerProfileUrl: string;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function isImageHeavyContext(productContext: string, flow: string): boolean {
|
|
638
|
+
const text = `${productContext} ${flow}`.toLowerCase();
|
|
639
|
+
return /(ecommerce|shop|store|retail|product|travel|hotel|hospitality|restaurant|food|beauty|fashion|wellness|fitness|interior|furniture|lifestyle|editorial|marketplace|booking)/.test(text);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function guessImageQuery(productContext: string, flow: string): string {
|
|
643
|
+
const text = `${productContext} ${flow}`.toLowerCase();
|
|
644
|
+
if (/(skincare|beauty|cosmetic|serum|makeup)/.test(text)) return "premium skincare product";
|
|
645
|
+
if (/(fashion|clothing|apparel|shoe|jewelry)/.test(text)) return "premium fashion product";
|
|
646
|
+
if (/(travel|hotel|hospitality|booking|resort)/.test(text)) return "boutique hotel travel";
|
|
647
|
+
if (/(food|restaurant|meal|grocery)/.test(text)) return "premium food photography";
|
|
648
|
+
if (/(fitness|wellness|health|yoga)/.test(text)) return "wellness lifestyle";
|
|
649
|
+
if (/(interior|furniture|home decor|real estate)/.test(text)) return "modern interior design";
|
|
650
|
+
return productContext.trim();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function loadStockImagery(
|
|
654
|
+
query: string,
|
|
655
|
+
flow: string,
|
|
656
|
+
count: number,
|
|
657
|
+
orientation: UnsplashOrientation
|
|
658
|
+
): Promise<ScreenImagery[]> {
|
|
659
|
+
const search = await searchUnsplashPhotos({
|
|
660
|
+
query,
|
|
661
|
+
perPage: Math.min(Math.max(count, 1), 6),
|
|
662
|
+
orientation,
|
|
663
|
+
contentFilter: "high",
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const imagery: ScreenImagery[] = [];
|
|
667
|
+
const bridge = await getBridge();
|
|
668
|
+
|
|
669
|
+
for (const photo of search.results) {
|
|
670
|
+
const dataUri = await fetchRemoteImageAsDataUri(photo.urls.regular || photo.urls.small);
|
|
671
|
+
const imported = await bridge.importImage(dataUri);
|
|
672
|
+
await trackUnsplashDownload(photo.links.downloadLocation);
|
|
673
|
+
imagery.push({
|
|
674
|
+
imageHash: imported.imageHash,
|
|
675
|
+
sourceUrl: photo.links.html,
|
|
676
|
+
photographerName: photo.photographer.name,
|
|
677
|
+
photographerProfileUrl: photo.photographer.profileUrl,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return imagery;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ─── Shimmer skeleton script ──────────────────────────────────────────────────
|
|
685
|
+
// Creates the outer frame immediately with loading-skeleton rectangles so the
|
|
686
|
+
// user sees the frame appear in the viewport before content is injected.
|
|
687
|
+
function buildShimmerScript(spec: FrameSpec, isFirst: boolean): string {
|
|
688
|
+
const { name, width, xOffset, wireframeMode } = spec;
|
|
689
|
+
const bgColor = wireframeMode ? "{ r: 0.97, g: 0.97, b: 0.97 }" : "{ r: 1, g: 1, b: 1 }";
|
|
690
|
+
|
|
691
|
+
const navigateSnippet = isFirst
|
|
692
|
+
? `figma.viewport.scrollAndZoomIntoView([frame]);`
|
|
693
|
+
: "";
|
|
694
|
+
|
|
695
|
+
return `
|
|
696
|
+
(async () => {
|
|
697
|
+
const frame = figma.createFrame();
|
|
698
|
+
frame.name = ${JSON.stringify(name)};
|
|
699
|
+
frame.resize(${width}, 800);
|
|
700
|
+
frame.layoutMode = 'VERTICAL';
|
|
701
|
+
frame.primaryAxisSizingMode = 'AUTO';
|
|
702
|
+
frame.counterAxisSizingMode = 'FIXED';
|
|
703
|
+
frame.itemSpacing = 16;
|
|
704
|
+
frame.paddingLeft = 24;
|
|
705
|
+
frame.paddingRight = 24;
|
|
706
|
+
frame.paddingTop = 24;
|
|
707
|
+
frame.paddingBottom = 32;
|
|
708
|
+
frame.fills = [{ type: 'SOLID', color: ${bgColor} }];
|
|
709
|
+
frame.x = ${xOffset};
|
|
710
|
+
frame.y = 0;
|
|
711
|
+
figma.currentPage.appendChild(frame);
|
|
712
|
+
|
|
713
|
+
const skeletonFill = [{ type: 'SOLID', color: { r: 0.91, g: 0.92, b: 0.95 } }];
|
|
714
|
+
const lightFill = [{ type: 'SOLID', color: { r: 0.95, g: 0.96, b: 0.98 } }];
|
|
715
|
+
const shimmerItems = [
|
|
716
|
+
{ h: 32, radius: 6, fill: skeletonFill },
|
|
717
|
+
{ h: 18, radius: 4, fill: lightFill, w: 0.6 },
|
|
718
|
+
{ h: 120, radius: 12, fill: lightFill },
|
|
719
|
+
{ h: 72, radius: 8, fill: skeletonFill },
|
|
720
|
+
{ h: 72, radius: 8, fill: skeletonFill },
|
|
721
|
+
{ h: 48, radius: 24, fill: [{ type: 'SOLID', color: { r: 0.55, g: 0.40, b: 0.95 }, opacity: 0.18 }] },
|
|
722
|
+
];
|
|
723
|
+
for (const item of shimmerItems) {
|
|
724
|
+
const r = figma.createRectangle();
|
|
725
|
+
r.name = '__shimmer__';
|
|
726
|
+
const rw = item.w ? Math.round((${width} - 48) * item.w) : ${width} - 48;
|
|
727
|
+
r.resize(rw, item.h);
|
|
728
|
+
r.cornerRadius = item.radius;
|
|
729
|
+
r.fills = item.fill;
|
|
730
|
+
frame.appendChild(r);
|
|
731
|
+
if (!item.w && 'layoutSizingHorizontal' in r) r.layoutSizingHorizontal = 'FILL';
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
${navigateSnippet}
|
|
735
|
+
return { frameId: frame.id };
|
|
736
|
+
})();
|
|
737
|
+
`.trim();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function buildScreenScript(spec: FrameSpec): string {
|
|
741
|
+
const {
|
|
742
|
+
name,
|
|
743
|
+
width,
|
|
744
|
+
template,
|
|
745
|
+
wireframeMode,
|
|
746
|
+
contentMode,
|
|
747
|
+
content,
|
|
748
|
+
resolvedComponents,
|
|
749
|
+
xOffset,
|
|
750
|
+
imageHash,
|
|
751
|
+
resolvedPalette,
|
|
752
|
+
fontConfig: fc,
|
|
753
|
+
} = spec;
|
|
754
|
+
|
|
755
|
+
// Font literals for template string interpolation
|
|
756
|
+
const fontLoadBlock = generateFontLoadScript(fc);
|
|
757
|
+
const headingBoldFont = fontNameLiteral("heading", "Bold", fc);
|
|
758
|
+
const bodyRegularFont = fontNameLiteral("body", "Regular", fc);
|
|
759
|
+
const uiMediumFont = fontNameLiteral("ui", "Medium", fc);
|
|
760
|
+
|
|
761
|
+
const bgColor = wireframeMode
|
|
762
|
+
? "{ r: 0.97, g: 0.97, b: 0.97 }"
|
|
763
|
+
: "{ r: 1, g: 1, b: 1 }";
|
|
764
|
+
|
|
765
|
+
const headerText =
|
|
766
|
+
contentMode === "realistic" && content?.heading
|
|
767
|
+
? content.heading
|
|
768
|
+
: name;
|
|
769
|
+
|
|
770
|
+
const subText =
|
|
771
|
+
contentMode === "realistic" && content?.subheading
|
|
772
|
+
? content.subheading
|
|
773
|
+
: spec.purpose ?? "";
|
|
774
|
+
|
|
775
|
+
const ctaText =
|
|
776
|
+
contentMode === "realistic" && content?.ctaLabel
|
|
777
|
+
? content.ctaLabel
|
|
778
|
+
: "Continue";
|
|
779
|
+
const bodyText =
|
|
780
|
+
contentMode === "realistic" && content?.bodyText
|
|
781
|
+
? content.bodyText
|
|
782
|
+
: "";
|
|
783
|
+
const sectionTitle =
|
|
784
|
+
contentMode === "realistic" && content?.sectionTitle
|
|
785
|
+
? content.sectionTitle
|
|
786
|
+
: "";
|
|
787
|
+
const listItems = contentMode === "realistic" ? (content?.listItems ?? []) : [];
|
|
788
|
+
const summaryItems = contentMode === "realistic" ? (content?.summaryItems ?? []) : [];
|
|
789
|
+
const helperText =
|
|
790
|
+
contentMode === "realistic" && content?.helperText
|
|
791
|
+
? content.helperText
|
|
792
|
+
: "";
|
|
793
|
+
|
|
794
|
+
// Template-specific skeleton builders
|
|
795
|
+
const templateBody = buildTemplateBody(
|
|
796
|
+
template,
|
|
797
|
+
headerText,
|
|
798
|
+
subText,
|
|
799
|
+
ctaText,
|
|
800
|
+
wireframeMode,
|
|
801
|
+
imageHash,
|
|
802
|
+
bodyText,
|
|
803
|
+
sectionTitle,
|
|
804
|
+
listItems,
|
|
805
|
+
summaryItems,
|
|
806
|
+
helperText,
|
|
807
|
+
resolvedComponents,
|
|
808
|
+
resolvedPalette,
|
|
809
|
+
fc
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
// Build variable binding script for CTA button and other key elements
|
|
813
|
+
const bindingScript = buildPostCreationBindings(resolvedPalette);
|
|
814
|
+
|
|
815
|
+
// Document template uses wider frame, larger padding/gap, semantic naming
|
|
816
|
+
const isDocTemplate = template === "document";
|
|
817
|
+
const frameWidth = isDocTemplate ? 1200 : width;
|
|
818
|
+
const framePadding = isDocTemplate ? 56 : 24;
|
|
819
|
+
const frameGap = isDocTemplate ? 48 : 16;
|
|
820
|
+
const frameName = isDocTemplate ? `Document Page - ${name}` : name;
|
|
821
|
+
|
|
822
|
+
return `
|
|
823
|
+
(async () => {
|
|
824
|
+
${fontLoadBlock}
|
|
825
|
+
|
|
826
|
+
// Create new frame
|
|
827
|
+
const frame = figma.createFrame();
|
|
828
|
+
frame.name = ${JSON.stringify(frameName)};
|
|
829
|
+
frame.resize(${frameWidth}, 900);
|
|
830
|
+
frame.layoutMode = 'VERTICAL';
|
|
831
|
+
frame.primaryAxisSizingMode = 'AUTO';
|
|
832
|
+
frame.counterAxisSizingMode = 'FIXED';
|
|
833
|
+
frame.itemSpacing = ${frameGap};
|
|
834
|
+
frame.paddingLeft = ${framePadding};
|
|
835
|
+
frame.paddingRight = ${framePadding};
|
|
836
|
+
frame.paddingTop = ${framePadding};
|
|
837
|
+
frame.paddingBottom = ${isDocTemplate ? 72 : 32};
|
|
838
|
+
frame.fills = [{ type: 'SOLID', color: ${bgColor} }];
|
|
839
|
+
frame.x = ${xOffset};
|
|
840
|
+
frame.y = 0;
|
|
841
|
+
figma.currentPage.appendChild(frame);
|
|
842
|
+
|
|
843
|
+
${templateBody}
|
|
844
|
+
|
|
845
|
+
${bindingScript}
|
|
846
|
+
|
|
847
|
+
// ── Auto-Layout Safety Validator (inline, zero extra bridge calls) ──
|
|
848
|
+
${generateValidatorScript()}
|
|
849
|
+
${generateValidatorCall('frame')}
|
|
850
|
+
|
|
851
|
+
// ── Document Repair Pass (runs only on document-type pages) ──
|
|
852
|
+
${generateDocumentRepairScript()}
|
|
853
|
+
${generateDocumentRepairCall('frame')}
|
|
854
|
+
|
|
855
|
+
return { frameId: frame.id };
|
|
856
|
+
})();
|
|
857
|
+
`.trim();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Generate variable binding code that runs after all elements are created.
|
|
862
|
+
* Walks the frame's children and binds matching variables to fills/strokes.
|
|
863
|
+
*/
|
|
864
|
+
function buildPostCreationBindings(resolvedPalette?: ResolvedPalette): string {
|
|
865
|
+
if (!resolvedPalette) return "";
|
|
866
|
+
|
|
867
|
+
// Collect all variable IDs we want to bind
|
|
868
|
+
const varBindings: Array<{
|
|
869
|
+
namePattern: string;
|
|
870
|
+
field: "fills" | "strokes";
|
|
871
|
+
variableId: string;
|
|
872
|
+
}> = [];
|
|
873
|
+
|
|
874
|
+
if (resolvedPalette.primary.variableId) {
|
|
875
|
+
varBindings.push({ namePattern: "CTA", field: "fills", variableId: resolvedPalette.primary.variableId });
|
|
876
|
+
}
|
|
877
|
+
if (resolvedPalette.border.variableId) {
|
|
878
|
+
varBindings.push({ namePattern: "InputField|SearchBar|SettingsGroup|AddressForm|CardDetails|ListItem", field: "strokes", variableId: resolvedPalette.border.variableId });
|
|
879
|
+
}
|
|
880
|
+
if (resolvedPalette.surface.variableId) {
|
|
881
|
+
varBindings.push({ namePattern: "InputField|SettingsGroup|AddressForm|CardDetails", field: "fills", variableId: resolvedPalette.surface.variableId });
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (varBindings.length === 0) return "";
|
|
885
|
+
|
|
886
|
+
return `
|
|
887
|
+
// ── Bind design-system variables to generated elements ──
|
|
888
|
+
try {
|
|
889
|
+
const __bindings = ${JSON.stringify(varBindings)};
|
|
890
|
+
const __bindChildren = async (parent) => {
|
|
891
|
+
if (!parent.children) return;
|
|
892
|
+
for (const child of parent.children) {
|
|
893
|
+
for (const b of __bindings) {
|
|
894
|
+
const patterns = b.namePattern.split('|');
|
|
895
|
+
if (patterns.some(p => child.name === p || child.name.startsWith(p))) {
|
|
896
|
+
const v = await figma.variables.getVariableByIdAsync(b.variableId);
|
|
897
|
+
if (v && child[b.field] && child[b.field].length > 0) {
|
|
898
|
+
const paints = [...child[b.field]];
|
|
899
|
+
if (paints[0].type === 'SOLID') {
|
|
900
|
+
paints[0] = figma.variables.setBoundVariableForPaint(paints[0], 'color', v);
|
|
901
|
+
child[b.field] = paints;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
await __bindChildren(child);
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
await __bindChildren(frame);
|
|
910
|
+
|
|
911
|
+
// Bind CTA button label text color
|
|
912
|
+
${resolvedPalette.primaryText.variableId ? `
|
|
913
|
+
const __ctaNode = frame.findOne(n => n.name === 'CTA');
|
|
914
|
+
if (__ctaNode && __ctaNode.children) {
|
|
915
|
+
for (const c of __ctaNode.children) {
|
|
916
|
+
if (c.type === 'TEXT' && c.fills && c.fills.length > 0) {
|
|
917
|
+
const v = await figma.variables.getVariableByIdAsync(${JSON.stringify(resolvedPalette.primaryText.variableId)});
|
|
918
|
+
if (v) {
|
|
919
|
+
const paints = [...c.fills];
|
|
920
|
+
paints[0] = figma.variables.setBoundVariableForPaint(paints[0], 'color', v);
|
|
921
|
+
c.fills = paints;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}` : ""}
|
|
926
|
+
} catch (e) {
|
|
927
|
+
// Token binding is best-effort — frame still renders with hardcoded values
|
|
928
|
+
}`;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function buildTemplateBody(
|
|
932
|
+
template: ScreenTemplate,
|
|
933
|
+
headerText: string,
|
|
934
|
+
subText: string,
|
|
935
|
+
ctaText: string,
|
|
936
|
+
wireframeMode: boolean,
|
|
937
|
+
imageHash?: string | null,
|
|
938
|
+
bodyText?: string,
|
|
939
|
+
sectionTitle?: string,
|
|
940
|
+
listItems: string[] = [],
|
|
941
|
+
summaryItems: string[] = [],
|
|
942
|
+
helperText?: string,
|
|
943
|
+
resolvedComponents: Array<{ name: string; nodeId: string | null }> = [],
|
|
944
|
+
resolvedPalette?: ResolvedPalette,
|
|
945
|
+
fc?: FontConfig
|
|
946
|
+
): string {
|
|
947
|
+
// Font literals for template string interpolation
|
|
948
|
+
const _fc = fc ?? resolveFontConfig();
|
|
949
|
+
const headingBoldFont = fontNameLiteral("heading", "Bold", _fc);
|
|
950
|
+
const bodyRegularFont = fontNameLiteral("body", "Regular", _fc);
|
|
951
|
+
const uiMediumFont = fontNameLiteral("ui", "Medium", _fc);
|
|
952
|
+
// ── Centralized color palette — resolved from DS tokens when available ──
|
|
953
|
+
const palette = {
|
|
954
|
+
primary: resolvedPalette?.primary.rgb ?? "{ r: 0.09, g: 0.09, b: 0.09 }",
|
|
955
|
+
primaryText: resolvedPalette?.primaryText.rgb ?? "{ r: 1, g: 1, b: 1 }",
|
|
956
|
+
surface: resolvedPalette?.surface.rgb ?? "{ r: 0.98, g: 0.98, b: 0.99 }",
|
|
957
|
+
border: resolvedPalette?.border.rgb ?? "{ r: 0.90, g: 0.91, b: 0.93 }",
|
|
958
|
+
muted: resolvedPalette?.muted.rgb ?? "{ r: 0.45, g: 0.45, b: 0.50 }",
|
|
959
|
+
accent: resolvedPalette?.accent.rgb ?? "{ r: 0.22, g: 0.35, b: 0.96 }",
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// Collect variable IDs for post-creation binding
|
|
963
|
+
const bindings: Array<{ elementName: string; field: string; variableId: string }> = [];
|
|
964
|
+
if (resolvedPalette) {
|
|
965
|
+
if (resolvedPalette.primary.variableId) {
|
|
966
|
+
bindings.push({ elementName: "CTA", field: "fills", variableId: resolvedPalette.primary.variableId });
|
|
967
|
+
}
|
|
968
|
+
if (resolvedPalette.primaryText.variableId) {
|
|
969
|
+
bindings.push({ elementName: "CTA__label", field: "fills", variableId: resolvedPalette.primaryText.variableId });
|
|
970
|
+
}
|
|
971
|
+
if (resolvedPalette.surface.variableId) {
|
|
972
|
+
bindings.push({ elementName: "__surface__", field: "fills", variableId: resolvedPalette.surface.variableId });
|
|
973
|
+
}
|
|
974
|
+
if (resolvedPalette.border.variableId) {
|
|
975
|
+
bindings.push({ elementName: "__border__", field: "strokes", variableId: resolvedPalette.border.variableId });
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const rectColor = wireframeMode
|
|
980
|
+
? "{ r: 0.88, g: 0.88, b: 0.88 }"
|
|
981
|
+
: "{ r: 0.94, g: 0.95, b: 1 }";
|
|
982
|
+
|
|
983
|
+
// Helper: try to instantiate a DS component, falling back to manual code
|
|
984
|
+
const tryInstantiate = (componentName: string, fallbackCode: string): string => {
|
|
985
|
+
const match = resolvedComponents.find(
|
|
986
|
+
(c) => c.nodeId && c.name.toLowerCase().includes(componentName.toLowerCase())
|
|
987
|
+
);
|
|
988
|
+
if (match) {
|
|
989
|
+
return `{
|
|
990
|
+
const comp = await figma.getNodeByIdAsync(${JSON.stringify(match.nodeId)});
|
|
991
|
+
if (comp && 'createInstance' in comp) {
|
|
992
|
+
const inst = comp.createInstance();
|
|
993
|
+
frame.appendChild(inst);
|
|
994
|
+
if ('layoutSizingHorizontal' in inst) inst.layoutSizingHorizontal = 'FILL';
|
|
995
|
+
} else { ${fallbackCode} }
|
|
996
|
+
}`;
|
|
997
|
+
}
|
|
998
|
+
return fallbackCode;
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
const addHeading = `
|
|
1002
|
+
const heading = figma.createText();
|
|
1003
|
+
heading.characters = ${JSON.stringify(headerText)};
|
|
1004
|
+
heading.fontSize = 28;
|
|
1005
|
+
heading.fontName = ${headingBoldFont};
|
|
1006
|
+
frame.appendChild(heading);
|
|
1007
|
+
if ('layoutSizingHorizontal' in heading) heading.layoutSizingHorizontal = 'FILL';`;
|
|
1008
|
+
|
|
1009
|
+
const addSubheading = subText
|
|
1010
|
+
? `
|
|
1011
|
+
const subheading = figma.createText();
|
|
1012
|
+
subheading.characters = ${JSON.stringify(subText.slice(0, 140))};
|
|
1013
|
+
subheading.fontSize = 16;
|
|
1014
|
+
subheading.fontName = ${bodyRegularFont};
|
|
1015
|
+
subheading.opacity = 0.65;
|
|
1016
|
+
frame.appendChild(subheading);
|
|
1017
|
+
if ('layoutSizingHorizontal' in subheading) subheading.layoutSizingHorizontal = 'FILL';`
|
|
1018
|
+
: "";
|
|
1019
|
+
|
|
1020
|
+
const addSectionTitle = sectionTitle
|
|
1021
|
+
? `
|
|
1022
|
+
const sectionTitleNode = figma.createText();
|
|
1023
|
+
sectionTitleNode.characters = ${JSON.stringify(sectionTitle)};
|
|
1024
|
+
sectionTitleNode.fontSize = 14;
|
|
1025
|
+
sectionTitleNode.fontName = ${headingBoldFont};
|
|
1026
|
+
sectionTitleNode.opacity = 0.8;
|
|
1027
|
+
frame.appendChild(sectionTitleNode);
|
|
1028
|
+
if ('layoutSizingHorizontal' in sectionTitleNode) sectionTitleNode.layoutSizingHorizontal = 'FILL';`
|
|
1029
|
+
: "";
|
|
1030
|
+
|
|
1031
|
+
const addBodyText = bodyText
|
|
1032
|
+
? `
|
|
1033
|
+
const bodyNode = figma.createText();
|
|
1034
|
+
bodyNode.characters = ${JSON.stringify(bodyText.slice(0, 180))};
|
|
1035
|
+
bodyNode.fontSize = 15;
|
|
1036
|
+
bodyNode.fontName = ${bodyRegularFont};
|
|
1037
|
+
bodyNode.opacity = 0.75;
|
|
1038
|
+
frame.appendChild(bodyNode);
|
|
1039
|
+
if ('layoutSizingHorizontal' in bodyNode) bodyNode.layoutSizingHorizontal = 'FILL';`
|
|
1040
|
+
: "";
|
|
1041
|
+
|
|
1042
|
+
const addTextRows = (rows: string[], title: string) => {
|
|
1043
|
+
if (rows.length === 0) return "";
|
|
1044
|
+
return `
|
|
1045
|
+
{
|
|
1046
|
+
const group = figma.createFrame();
|
|
1047
|
+
group.name = ${JSON.stringify(title)};
|
|
1048
|
+
group.layoutMode = 'VERTICAL';
|
|
1049
|
+
group.primaryAxisSizingMode = 'AUTO';
|
|
1050
|
+
group.counterAxisSizingMode = 'AUTO';
|
|
1051
|
+
group.itemSpacing = 12;
|
|
1052
|
+
group.paddingLeft = 16;
|
|
1053
|
+
group.paddingRight = 16;
|
|
1054
|
+
group.paddingTop = 16;
|
|
1055
|
+
group.paddingBottom = 16;
|
|
1056
|
+
group.cornerRadius = 12;
|
|
1057
|
+
group.fills = [{ type: 'SOLID', color: { r: 0.98, g: 0.98, b: 0.99 } }];
|
|
1058
|
+
${rows
|
|
1059
|
+
.map(
|
|
1060
|
+
(row) => `
|
|
1061
|
+
{
|
|
1062
|
+
const t = figma.createText();
|
|
1063
|
+
t.characters = ${JSON.stringify(row)};
|
|
1064
|
+
t.fontSize = 15;
|
|
1065
|
+
t.fontName = ${bodyRegularFont};
|
|
1066
|
+
group.appendChild(t);
|
|
1067
|
+
if ('layoutSizingHorizontal' in t) t.layoutSizingHorizontal = 'FILL';
|
|
1068
|
+
}`
|
|
1069
|
+
)
|
|
1070
|
+
.join("\n")}
|
|
1071
|
+
frame.appendChild(group);
|
|
1072
|
+
if ('layoutSizingHorizontal' in group) group.layoutSizingHorizontal = 'FILL';
|
|
1073
|
+
}`;
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
const addHelperText = helperText
|
|
1077
|
+
? `
|
|
1078
|
+
const helperNode = figma.createText();
|
|
1079
|
+
helperNode.characters = ${JSON.stringify(helperText.slice(0, 180))};
|
|
1080
|
+
helperNode.fontSize = 13;
|
|
1081
|
+
helperNode.fontName = ${bodyRegularFont};
|
|
1082
|
+
helperNode.opacity = 0.6;
|
|
1083
|
+
frame.appendChild(helperNode);
|
|
1084
|
+
if ('layoutSizingHorizontal' in helperNode) helperNode.layoutSizingHorizontal = 'FILL';`
|
|
1085
|
+
: "";
|
|
1086
|
+
|
|
1087
|
+
// Wireframe-only grey block — only used in wireframeMode
|
|
1088
|
+
const addPlaceholderRect = (label: string, height: number) => `
|
|
1089
|
+
{
|
|
1090
|
+
const r = figma.createRectangle();
|
|
1091
|
+
r.name = ${JSON.stringify(label)};
|
|
1092
|
+
r.resize(frame.width - 48, ${height});
|
|
1093
|
+
r.fills = [{ type: 'SOLID', color: ${rectColor} }];
|
|
1094
|
+
r.cornerRadius = 8;
|
|
1095
|
+
frame.appendChild(r);
|
|
1096
|
+
if ('layoutSizingHorizontal' in r) r.layoutSizingHorizontal = 'FILL';
|
|
1097
|
+
}`;
|
|
1098
|
+
|
|
1099
|
+
const addImageRect = (label: string, height: number) => `
|
|
1100
|
+
{
|
|
1101
|
+
const r = figma.createRectangle();
|
|
1102
|
+
r.name = ${JSON.stringify(label)};
|
|
1103
|
+
r.resize(frame.width - 48, ${height});
|
|
1104
|
+
${imageHash
|
|
1105
|
+
? `r.fills = [{ type: 'IMAGE', imageHash: ${JSON.stringify(imageHash)}, scaleMode: 'FILL' }];`
|
|
1106
|
+
: `r.fills = [{ type: 'SOLID', color: ${rectColor} }];`}
|
|
1107
|
+
r.cornerRadius = 12;
|
|
1108
|
+
frame.appendChild(r);
|
|
1109
|
+
if ('layoutSizingHorizontal' in r) r.layoutSizingHorizontal = 'FILL';
|
|
1110
|
+
}`;
|
|
1111
|
+
|
|
1112
|
+
const ctaFallback = `
|
|
1113
|
+
const btn = figma.createFrame();
|
|
1114
|
+
btn.name = 'CTA';
|
|
1115
|
+
btn.resize(frame.width - 48, 52);
|
|
1116
|
+
btn.layoutMode = 'HORIZONTAL';
|
|
1117
|
+
btn.primaryAxisAlignItems = 'CENTER';
|
|
1118
|
+
btn.counterAxisAlignItems = 'CENTER';
|
|
1119
|
+
btn.fills = [{ type: 'SOLID', color: ${palette.primary} }];
|
|
1120
|
+
btn.cornerRadius = 12;
|
|
1121
|
+
const btnLabel = figma.createText();
|
|
1122
|
+
btnLabel.characters = ${JSON.stringify(ctaText)};
|
|
1123
|
+
btnLabel.fontSize = 16;
|
|
1124
|
+
btnLabel.fontName = ${uiMediumFont};
|
|
1125
|
+
btnLabel.fills = [{ type: 'SOLID', color: ${palette.primaryText} }];
|
|
1126
|
+
btn.appendChild(btnLabel);
|
|
1127
|
+
frame.appendChild(btn);
|
|
1128
|
+
if ('layoutSizingHorizontal' in btn) btn.layoutSizingHorizontal = 'FILL';
|
|
1129
|
+
`;
|
|
1130
|
+
const addCTA = tryInstantiate("Button", ctaFallback);
|
|
1131
|
+
|
|
1132
|
+
// ── Styled elements (replace grey rects in realistic / non-wireframe mode) ──
|
|
1133
|
+
|
|
1134
|
+
const addInputField = (label: string, placeholder: string) => wireframeMode
|
|
1135
|
+
? addPlaceholderRect(label, 52)
|
|
1136
|
+
: `
|
|
1137
|
+
{
|
|
1138
|
+
const inp = figma.createFrame();
|
|
1139
|
+
inp.name = ${JSON.stringify(label)};
|
|
1140
|
+
inp.layoutMode = 'HORIZONTAL';
|
|
1141
|
+
inp.counterAxisAlignItems = 'CENTER';
|
|
1142
|
+
inp.paddingLeft = 16; inp.paddingRight = 16;
|
|
1143
|
+
inp.primaryAxisSizingMode = 'FIXED'; inp.counterAxisSizingMode = 'FIXED';
|
|
1144
|
+
inp.resize(frame.width - 48, 52);
|
|
1145
|
+
inp.fills = [{ type: 'SOLID', color: { r: 0.99, g: 0.99, b: 1 } }];
|
|
1146
|
+
inp.cornerRadius = 10;
|
|
1147
|
+
inp.strokes = [{ type: 'SOLID', color: { r: 0.87, g: 0.88, b: 0.91 } }];
|
|
1148
|
+
inp.strokeWeight = 1.5;
|
|
1149
|
+
const ph = figma.createText();
|
|
1150
|
+
ph.characters = ${JSON.stringify(placeholder)};
|
|
1151
|
+
ph.fontSize = 15;
|
|
1152
|
+
ph.fontName = ${bodyRegularFont};
|
|
1153
|
+
ph.fills = [{ type: 'SOLID', color: { r: 0.63, g: 0.64, b: 0.68 } }];
|
|
1154
|
+
inp.appendChild(ph);
|
|
1155
|
+
if ('layoutSizingHorizontal' in ph) ph.layoutSizingHorizontal = 'FILL';
|
|
1156
|
+
frame.appendChild(inp);
|
|
1157
|
+
if ('layoutSizingHorizontal' in inp) inp.layoutSizingHorizontal = 'FILL';
|
|
1158
|
+
}`;
|
|
1159
|
+
|
|
1160
|
+
const addNavBar = (brandText: string) => wireframeMode
|
|
1161
|
+
? addPlaceholderRect("Navigation Bar", 56)
|
|
1162
|
+
: `
|
|
1163
|
+
{
|
|
1164
|
+
const nav = figma.createFrame();
|
|
1165
|
+
nav.name = 'NavBar';
|
|
1166
|
+
nav.layoutMode = 'HORIZONTAL';
|
|
1167
|
+
nav.counterAxisAlignItems = 'CENTER';
|
|
1168
|
+
nav.primaryAxisAlignItems = 'SPACE_BETWEEN';
|
|
1169
|
+
nav.paddingLeft = 0; nav.paddingRight = 0;
|
|
1170
|
+
nav.primaryAxisSizingMode = 'FIXED'; nav.counterAxisSizingMode = 'FIXED';
|
|
1171
|
+
nav.resize(frame.width - 48, 56);
|
|
1172
|
+
nav.fills = [];
|
|
1173
|
+
const bLabel = figma.createText();
|
|
1174
|
+
bLabel.characters = ${JSON.stringify(brandText)};
|
|
1175
|
+
bLabel.fontSize = 17;
|
|
1176
|
+
bLabel.fontName = ${headingBoldFont};
|
|
1177
|
+
nav.appendChild(bLabel);
|
|
1178
|
+
const mIcon = figma.createText();
|
|
1179
|
+
mIcon.characters = '\u22EF';
|
|
1180
|
+
mIcon.fontSize = 20;
|
|
1181
|
+
mIcon.fontName = ${headingBoldFont};
|
|
1182
|
+
mIcon.opacity = 0.4;
|
|
1183
|
+
nav.appendChild(mIcon);
|
|
1184
|
+
frame.appendChild(nav);
|
|
1185
|
+
if ('layoutSizingHorizontal' in nav) nav.layoutSizingHorizontal = 'FILL';
|
|
1186
|
+
}`;
|
|
1187
|
+
|
|
1188
|
+
const addStatRow = () => wireframeMode
|
|
1189
|
+
? addPlaceholderRect("Stats Row", 96)
|
|
1190
|
+
: `
|
|
1191
|
+
{
|
|
1192
|
+
const row = figma.createFrame();
|
|
1193
|
+
row.name = 'StatsRow';
|
|
1194
|
+
row.layoutMode = 'HORIZONTAL';
|
|
1195
|
+
row.itemSpacing = 12;
|
|
1196
|
+
row.primaryAxisSizingMode = 'FIXED'; row.counterAxisSizingMode = 'AUTO';
|
|
1197
|
+
row.resize(frame.width - 48, 1);
|
|
1198
|
+
row.fills = [];
|
|
1199
|
+
${[["24", "Active"], ["8", "Pending"], ["142", "Total"]]
|
|
1200
|
+
.map(
|
|
1201
|
+
([val, lbl]) => `
|
|
1202
|
+
{
|
|
1203
|
+
const sc = figma.createFrame();
|
|
1204
|
+
sc.name = ${JSON.stringify(lbl)};
|
|
1205
|
+
sc.layoutMode = 'VERTICAL';
|
|
1206
|
+
sc.primaryAxisSizingMode = 'AUTO'; sc.counterAxisSizingMode = 'AUTO';
|
|
1207
|
+
sc.paddingLeft = 16; sc.paddingRight = 16;
|
|
1208
|
+
sc.paddingTop = 14; sc.paddingBottom = 14;
|
|
1209
|
+
sc.itemSpacing = 2;
|
|
1210
|
+
sc.cornerRadius = 12;
|
|
1211
|
+
sc.fills = [{ type: 'SOLID', color: { r: 0.96, g: 0.97, b: 1 } }];
|
|
1212
|
+
const sv = figma.createText(); sv.characters = ${JSON.stringify(val)};
|
|
1213
|
+
sv.fontSize = 24; sv.fontName = ${headingBoldFont};
|
|
1214
|
+
sc.appendChild(sv);
|
|
1215
|
+
const sl = figma.createText(); sl.characters = ${JSON.stringify(lbl)};
|
|
1216
|
+
sl.fontSize = 12; sl.fontName = ${bodyRegularFont}; sl.opacity = 0.55;
|
|
1217
|
+
sc.appendChild(sl);
|
|
1218
|
+
row.appendChild(sc);
|
|
1219
|
+
if ('layoutSizingHorizontal' in sc) sc.layoutSizingHorizontal = 'FILL';
|
|
1220
|
+
}`
|
|
1221
|
+
)
|
|
1222
|
+
.join("\n")}
|
|
1223
|
+
frame.appendChild(row);
|
|
1224
|
+
if ('layoutSizingHorizontal' in row) row.layoutSizingHorizontal = 'FILL';
|
|
1225
|
+
}`;
|
|
1226
|
+
|
|
1227
|
+
const addSearchBar = () => wireframeMode
|
|
1228
|
+
? addPlaceholderRect("Search Bar", 48)
|
|
1229
|
+
: `
|
|
1230
|
+
{
|
|
1231
|
+
const sb = figma.createFrame();
|
|
1232
|
+
sb.name = 'SearchBar';
|
|
1233
|
+
sb.layoutMode = 'HORIZONTAL';
|
|
1234
|
+
sb.counterAxisAlignItems = 'CENTER';
|
|
1235
|
+
sb.paddingLeft = 16; sb.paddingRight = 16;
|
|
1236
|
+
sb.itemSpacing = 8;
|
|
1237
|
+
sb.primaryAxisSizingMode = 'FIXED'; sb.counterAxisSizingMode = 'FIXED';
|
|
1238
|
+
sb.resize(frame.width - 48, 48);
|
|
1239
|
+
sb.fills = [{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.97 } }];
|
|
1240
|
+
sb.cornerRadius = 24;
|
|
1241
|
+
const sIcon = figma.createText();
|
|
1242
|
+
sIcon.characters = '\uD83D\uDD0D';
|
|
1243
|
+
sIcon.fontSize = 14; sIcon.fontName = ${bodyRegularFont};
|
|
1244
|
+
sIcon.opacity = 0.4;
|
|
1245
|
+
sb.appendChild(sIcon);
|
|
1246
|
+
const sph = figma.createText();
|
|
1247
|
+
sph.characters = 'Search\u2026';
|
|
1248
|
+
sph.fontSize = 15; sph.fontName = ${bodyRegularFont};
|
|
1249
|
+
sph.opacity = 0.45;
|
|
1250
|
+
sb.appendChild(sph);
|
|
1251
|
+
if ('layoutSizingHorizontal' in sph) sph.layoutSizingHorizontal = 'FILL';
|
|
1252
|
+
frame.appendChild(sb);
|
|
1253
|
+
if ('layoutSizingHorizontal' in sb) sb.layoutSizingHorizontal = 'FILL';
|
|
1254
|
+
}`;
|
|
1255
|
+
|
|
1256
|
+
const addListItemRows = (items: string[]) => wireframeMode
|
|
1257
|
+
? items.map(() => addPlaceholderRect("List Item", 72)).join("\n")
|
|
1258
|
+
: items
|
|
1259
|
+
.map(
|
|
1260
|
+
(item) => `
|
|
1261
|
+
{
|
|
1262
|
+
const row = figma.createFrame();
|
|
1263
|
+
row.name = 'ListItem';
|
|
1264
|
+
row.layoutMode = 'HORIZONTAL';
|
|
1265
|
+
row.counterAxisAlignItems = 'CENTER';
|
|
1266
|
+
row.paddingLeft = 16; row.paddingRight = 16;
|
|
1267
|
+
row.itemSpacing = 12;
|
|
1268
|
+
row.primaryAxisSizingMode = 'FIXED'; row.counterAxisSizingMode = 'FIXED';
|
|
1269
|
+
row.resize(frame.width - 48, 68);
|
|
1270
|
+
row.cornerRadius = 10;
|
|
1271
|
+
row.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
|
1272
|
+
row.strokes = [{ type: 'SOLID', color: { r: 0.92, g: 0.93, b: 0.96 } }];
|
|
1273
|
+
row.strokeWeight = 1;
|
|
1274
|
+
const avatar = figma.createEllipse();
|
|
1275
|
+
avatar.resize(40, 40);
|
|
1276
|
+
avatar.fills = [{ type: 'SOLID', color: { r: 0.91, g: 0.92, b: 0.97 } }];
|
|
1277
|
+
row.appendChild(avatar);
|
|
1278
|
+
const textCol = figma.createFrame();
|
|
1279
|
+
textCol.name = 'Labels';
|
|
1280
|
+
textCol.layoutMode = 'VERTICAL';
|
|
1281
|
+
textCol.primaryAxisSizingMode = 'AUTO'; textCol.counterAxisSizingMode = 'AUTO';
|
|
1282
|
+
textCol.itemSpacing = 3; textCol.fills = [];
|
|
1283
|
+
const titleT = figma.createText();
|
|
1284
|
+
titleT.characters = ${JSON.stringify(item)};
|
|
1285
|
+
titleT.fontSize = 15; titleT.fontName = ${uiMediumFont};
|
|
1286
|
+
textCol.appendChild(titleT);
|
|
1287
|
+
const subT = figma.createText();
|
|
1288
|
+
subT.characters = 'Tap to view details';
|
|
1289
|
+
subT.fontSize = 13; subT.fontName = ${bodyRegularFont};
|
|
1290
|
+
subT.opacity = 0.5;
|
|
1291
|
+
textCol.appendChild(subT);
|
|
1292
|
+
row.appendChild(textCol);
|
|
1293
|
+
if ('layoutSizingHorizontal' in textCol) textCol.layoutSizingHorizontal = 'FILL';
|
|
1294
|
+
const arrowT = figma.createText();
|
|
1295
|
+
arrowT.characters = '\u203A';
|
|
1296
|
+
arrowT.fontSize = 18; arrowT.fontName = ${bodyRegularFont};
|
|
1297
|
+
arrowT.opacity = 0.35;
|
|
1298
|
+
row.appendChild(arrowT);
|
|
1299
|
+
frame.appendChild(row);
|
|
1300
|
+
if ('layoutSizingHorizontal' in row) row.layoutSizingHorizontal = 'FILL';
|
|
1301
|
+
}`
|
|
1302
|
+
)
|
|
1303
|
+
.join("\n");
|
|
1304
|
+
|
|
1305
|
+
const addSettingsGroup = (items: string[]) => wireframeMode
|
|
1306
|
+
? addPlaceholderRect("Settings Group", items.length * 52)
|
|
1307
|
+
: `
|
|
1308
|
+
{
|
|
1309
|
+
const grp = figma.createFrame();
|
|
1310
|
+
grp.name = 'SettingsGroup';
|
|
1311
|
+
grp.layoutMode = 'VERTICAL';
|
|
1312
|
+
grp.primaryAxisSizingMode = 'AUTO'; grp.counterAxisSizingMode = 'FIXED';
|
|
1313
|
+
grp.resize(frame.width - 48, 1);
|
|
1314
|
+
grp.cornerRadius = 12;
|
|
1315
|
+
grp.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
|
1316
|
+
grp.strokes = [{ type: 'SOLID', color: { r: 0.92, g: 0.93, b: 0.96 } }];
|
|
1317
|
+
grp.strokeWeight = 1;
|
|
1318
|
+
${items
|
|
1319
|
+
.map(
|
|
1320
|
+
(item, i) => `
|
|
1321
|
+
{
|
|
1322
|
+
const ri = figma.createFrame();
|
|
1323
|
+
ri.name = 'SettingsRow';
|
|
1324
|
+
ri.layoutMode = 'HORIZONTAL';
|
|
1325
|
+
ri.counterAxisAlignItems = 'CENTER';
|
|
1326
|
+
ri.primaryAxisAlignItems = 'SPACE_BETWEEN';
|
|
1327
|
+
ri.paddingLeft = 16; ri.paddingRight = 16;
|
|
1328
|
+
ri.primaryAxisSizingMode = 'FIXED'; ri.counterAxisSizingMode = 'FIXED';
|
|
1329
|
+
ri.resize(frame.width - 48, 52);
|
|
1330
|
+
ri.fills = [];
|
|
1331
|
+
${i > 0 ? `ri.strokes = [{ type: 'SOLID', color: { r: 0.93, g: 0.94, b: 0.96 } }]; ri.strokeWeight = 1; ri.strokeAlign = 'INSIDE';` : ""}
|
|
1332
|
+
const rl = figma.createText();
|
|
1333
|
+
rl.characters = ${JSON.stringify(item)};
|
|
1334
|
+
rl.fontSize = 15; rl.fontName = ${bodyRegularFont};
|
|
1335
|
+
ri.appendChild(rl);
|
|
1336
|
+
const ra = figma.createText();
|
|
1337
|
+
ra.characters = '\u203A';
|
|
1338
|
+
ra.fontSize = 18; ra.fontName = ${bodyRegularFont};
|
|
1339
|
+
ra.opacity = 0.35;
|
|
1340
|
+
ri.appendChild(ra);
|
|
1341
|
+
grp.appendChild(ri);
|
|
1342
|
+
if ('layoutSizingHorizontal' in ri) ri.layoutSizingHorizontal = 'FILL';
|
|
1343
|
+
}`
|
|
1344
|
+
)
|
|
1345
|
+
.join("\n")}
|
|
1346
|
+
frame.appendChild(grp);
|
|
1347
|
+
if ('layoutSizingHorizontal' in grp) grp.layoutSizingHorizontal = 'FILL';
|
|
1348
|
+
}`;
|
|
1349
|
+
|
|
1350
|
+
const addAddressForm = () => wireframeMode
|
|
1351
|
+
? addPlaceholderRect("Address Form", 180)
|
|
1352
|
+
: `
|
|
1353
|
+
{
|
|
1354
|
+
const form = figma.createFrame();
|
|
1355
|
+
form.name = 'AddressForm';
|
|
1356
|
+
form.layoutMode = 'VERTICAL';
|
|
1357
|
+
form.primaryAxisSizingMode = 'AUTO'; form.counterAxisSizingMode = 'FIXED';
|
|
1358
|
+
form.resize(frame.width - 48, 1);
|
|
1359
|
+
form.itemSpacing = 12;
|
|
1360
|
+
form.paddingLeft = 16; form.paddingRight = 16;
|
|
1361
|
+
form.paddingTop = 16; form.paddingBottom = 16;
|
|
1362
|
+
form.cornerRadius = 12;
|
|
1363
|
+
form.fills = [{ type: 'SOLID', color: { r: 0.99, g: 0.99, b: 1 } }];
|
|
1364
|
+
form.strokes = [{ type: 'SOLID', color: { r: 0.9, g: 0.91, b: 0.94 } }];
|
|
1365
|
+
form.strokeWeight = 1;
|
|
1366
|
+
['Street address', 'City', 'Postcode'].forEach(function(lbl) {
|
|
1367
|
+
const fi = figma.createFrame();
|
|
1368
|
+
fi.layoutMode = 'HORIZONTAL'; fi.counterAxisAlignItems = 'CENTER';
|
|
1369
|
+
fi.paddingLeft = 12; fi.paddingRight = 12;
|
|
1370
|
+
fi.primaryAxisSizingMode = 'FIXED'; fi.counterAxisSizingMode = 'FIXED';
|
|
1371
|
+
fi.resize(form.width - 32, 46);
|
|
1372
|
+
fi.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
|
1373
|
+
fi.cornerRadius = 8;
|
|
1374
|
+
fi.strokes = [{ type: 'SOLID', color: { r: 0.87, g: 0.88, b: 0.91 } }];
|
|
1375
|
+
fi.strokeWeight = 1;
|
|
1376
|
+
const ft = figma.createText();
|
|
1377
|
+
ft.characters = lbl;
|
|
1378
|
+
ft.fontSize = 14; ft.fontName = ${bodyRegularFont};
|
|
1379
|
+
ft.opacity = 0.5;
|
|
1380
|
+
fi.appendChild(ft);
|
|
1381
|
+
form.appendChild(fi);
|
|
1382
|
+
if ('layoutSizingHorizontal' in fi) fi.layoutSizingHorizontal = 'FILL';
|
|
1383
|
+
});
|
|
1384
|
+
frame.appendChild(form);
|
|
1385
|
+
if ('layoutSizingHorizontal' in form) form.layoutSizingHorizontal = 'FILL';
|
|
1386
|
+
}`;
|
|
1387
|
+
|
|
1388
|
+
const addCardDetailsForm = () => wireframeMode
|
|
1389
|
+
? addPlaceholderRect("Card Details", 140)
|
|
1390
|
+
: `
|
|
1391
|
+
{
|
|
1392
|
+
const cdf = figma.createFrame();
|
|
1393
|
+
cdf.name = 'CardDetails';
|
|
1394
|
+
cdf.layoutMode = 'VERTICAL';
|
|
1395
|
+
cdf.primaryAxisSizingMode = 'AUTO'; cdf.counterAxisSizingMode = 'FIXED';
|
|
1396
|
+
cdf.resize(frame.width - 48, 1);
|
|
1397
|
+
cdf.itemSpacing = 12;
|
|
1398
|
+
cdf.paddingLeft = 16; cdf.paddingRight = 16;
|
|
1399
|
+
cdf.paddingTop = 16; cdf.paddingBottom = 16;
|
|
1400
|
+
cdf.cornerRadius = 12;
|
|
1401
|
+
cdf.fills = [{ type: 'SOLID', color: { r: 0.99, g: 0.99, b: 1 } }];
|
|
1402
|
+
cdf.strokes = [{ type: 'SOLID', color: { r: 0.9, g: 0.91, b: 0.94 } }];
|
|
1403
|
+
cdf.strokeWeight = 1;
|
|
1404
|
+
['Card number', 'MM / YY', 'CVV'].forEach(function(lbl) {
|
|
1405
|
+
const fi = figma.createFrame();
|
|
1406
|
+
fi.layoutMode = 'HORIZONTAL'; fi.counterAxisAlignItems = 'CENTER';
|
|
1407
|
+
fi.paddingLeft = 12; fi.paddingRight = 12;
|
|
1408
|
+
fi.primaryAxisSizingMode = 'FIXED'; fi.counterAxisSizingMode = 'FIXED';
|
|
1409
|
+
fi.resize(cdf.width - 32, 46);
|
|
1410
|
+
fi.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
|
1411
|
+
fi.cornerRadius = 8;
|
|
1412
|
+
fi.strokes = [{ type: 'SOLID', color: { r: 0.87, g: 0.88, b: 0.91 } }];
|
|
1413
|
+
fi.strokeWeight = 1;
|
|
1414
|
+
const ft = figma.createText();
|
|
1415
|
+
ft.characters = lbl;
|
|
1416
|
+
ft.fontSize = 14; ft.fontName = ${bodyRegularFont};
|
|
1417
|
+
ft.opacity = 0.5;
|
|
1418
|
+
fi.appendChild(ft);
|
|
1419
|
+
cdf.appendChild(fi);
|
|
1420
|
+
if ('layoutSizingHorizontal' in fi) fi.layoutSizingHorizontal = 'FILL';
|
|
1421
|
+
});
|
|
1422
|
+
frame.appendChild(cdf);
|
|
1423
|
+
if ('layoutSizingHorizontal' in cdf) cdf.layoutSizingHorizontal = 'FILL';
|
|
1424
|
+
}`;
|
|
1425
|
+
|
|
1426
|
+
switch (template) {
|
|
1427
|
+
case "auth":
|
|
1428
|
+
return `
|
|
1429
|
+
${addHeading}
|
|
1430
|
+
${addSubheading}
|
|
1431
|
+
${addInputField("Email", "Email address")}
|
|
1432
|
+
${addInputField("Password", "Password")}
|
|
1433
|
+
${addCTA}
|
|
1434
|
+
`;
|
|
1435
|
+
|
|
1436
|
+
case "dashboard":
|
|
1437
|
+
return `
|
|
1438
|
+
${addNavBar(sectionTitle || headerText)}
|
|
1439
|
+
${addHeading}
|
|
1440
|
+
${addSubheading}
|
|
1441
|
+
${addStatRow()}
|
|
1442
|
+
${addImageRect("Main Content", 300)}
|
|
1443
|
+
`;
|
|
1444
|
+
|
|
1445
|
+
case "list": {
|
|
1446
|
+
const displayItems = listItems.length > 0 ? listItems.slice(0, 3) : ["Item One", "Item Two", "Item Three"];
|
|
1447
|
+
return `
|
|
1448
|
+
${addHeading}
|
|
1449
|
+
${addSearchBar()}
|
|
1450
|
+
${addImageRect("Featured Item", 180)}
|
|
1451
|
+
${addListItemRows(displayItems)}
|
|
1452
|
+
`;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
case "detail":
|
|
1456
|
+
return `
|
|
1457
|
+
${addImageRect("Hero Image", 240)}
|
|
1458
|
+
${addHeading}
|
|
1459
|
+
${addSubheading}
|
|
1460
|
+
${addBodyText}
|
|
1461
|
+
${addTextRows(listItems.length > 0 ? listItems : ["Key feature or detail", "Another relevant point", "Why users should care"], "Details")}
|
|
1462
|
+
${addCTA}
|
|
1463
|
+
`;
|
|
1464
|
+
|
|
1465
|
+
case "settings": {
|
|
1466
|
+
const settingsItems = listItems.length > 0 ? listItems : ["Notifications", "Privacy & Security", "Language", "Help & Support", "Sign out"];
|
|
1467
|
+
const half = Math.ceil(settingsItems.length / 2);
|
|
1468
|
+
return `
|
|
1469
|
+
${addHeading}
|
|
1470
|
+
${addSubheading}
|
|
1471
|
+
${addSettingsGroup(settingsItems.slice(0, half))}
|
|
1472
|
+
${addSettingsGroup(settingsItems.slice(half))}
|
|
1473
|
+
${addCTA}
|
|
1474
|
+
`;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
case "onboarding":
|
|
1478
|
+
return `
|
|
1479
|
+
${addImageRect("Illustration", 280)}
|
|
1480
|
+
${addHeading}
|
|
1481
|
+
${addSubheading}
|
|
1482
|
+
${addCTA}
|
|
1483
|
+
`;
|
|
1484
|
+
|
|
1485
|
+
case "checkout-cart":
|
|
1486
|
+
return `
|
|
1487
|
+
${addHeading}
|
|
1488
|
+
${addSubheading}
|
|
1489
|
+
${addSectionTitle}
|
|
1490
|
+
${addImageRect("Product Hero", 160)}
|
|
1491
|
+
${addTextRows(listItems, "Cart Items")}
|
|
1492
|
+
${addHelperText}
|
|
1493
|
+
${addTextRows(summaryItems, "Order Summary")}
|
|
1494
|
+
${addCTA}
|
|
1495
|
+
`;
|
|
1496
|
+
|
|
1497
|
+
case "checkout-address":
|
|
1498
|
+
return `
|
|
1499
|
+
${addHeading}
|
|
1500
|
+
${addSubheading}
|
|
1501
|
+
${addSectionTitle}
|
|
1502
|
+
${addTextRows(listItems, "Saved Addresses")}
|
|
1503
|
+
${addAddressForm()}
|
|
1504
|
+
${addHelperText}
|
|
1505
|
+
${addCTA}
|
|
1506
|
+
`;
|
|
1507
|
+
|
|
1508
|
+
case "checkout-shipping":
|
|
1509
|
+
return `
|
|
1510
|
+
${addHeading}
|
|
1511
|
+
${addSubheading}
|
|
1512
|
+
${addSectionTitle}
|
|
1513
|
+
${addTextRows(listItems, "Shipping Options")}
|
|
1514
|
+
${addTextRows(summaryItems, "Delivery Summary")}
|
|
1515
|
+
${addCTA}
|
|
1516
|
+
`;
|
|
1517
|
+
|
|
1518
|
+
case "checkout-payment":
|
|
1519
|
+
return `
|
|
1520
|
+
${addHeading}
|
|
1521
|
+
${addSubheading}
|
|
1522
|
+
${addSectionTitle}
|
|
1523
|
+
${addTextRows(listItems, "Payment Methods")}
|
|
1524
|
+
${addCardDetailsForm()}
|
|
1525
|
+
${addHelperText}
|
|
1526
|
+
${addCTA}
|
|
1527
|
+
`;
|
|
1528
|
+
|
|
1529
|
+
case "checkout-review":
|
|
1530
|
+
return `
|
|
1531
|
+
${addHeading}
|
|
1532
|
+
${addSubheading}
|
|
1533
|
+
${addSectionTitle}
|
|
1534
|
+
${addTextRows(listItems, "Review Sections")}
|
|
1535
|
+
${addTextRows(summaryItems, "Final Totals")}
|
|
1536
|
+
${addHelperText}
|
|
1537
|
+
${addCTA}
|
|
1538
|
+
`;
|
|
1539
|
+
|
|
1540
|
+
case "checkout-success":
|
|
1541
|
+
return `
|
|
1542
|
+
${addImageRect("Success Illustration", 220)}
|
|
1543
|
+
${addHeading}
|
|
1544
|
+
${addSubheading}
|
|
1545
|
+
${addTextRows(listItems, "Confirmation Details")}
|
|
1546
|
+
${addTextRows(summaryItems, "Support")}
|
|
1547
|
+
${addCTA}
|
|
1548
|
+
`;
|
|
1549
|
+
|
|
1550
|
+
case "document": {
|
|
1551
|
+
// Document-style page with proper auto-layout structure
|
|
1552
|
+
const tocItems = listItems.length > 0 ? listItems : ["1. Overview", "2. Usage", "3. API", "4. Accessibility"];
|
|
1553
|
+
|
|
1554
|
+
const addTocSection = `
|
|
1555
|
+
{
|
|
1556
|
+
// ── TOC Section ──
|
|
1557
|
+
const tocSection = figma.createFrame();
|
|
1558
|
+
tocSection.name = 'Section Block - Table of Contents';
|
|
1559
|
+
tocSection.layoutMode = 'VERTICAL';
|
|
1560
|
+
tocSection.primaryAxisSizingMode = 'AUTO';
|
|
1561
|
+
tocSection.counterAxisSizingMode = 'AUTO';
|
|
1562
|
+
tocSection.itemSpacing = 20;
|
|
1563
|
+
tocSection.fills = [];
|
|
1564
|
+
frame.appendChild(tocSection);
|
|
1565
|
+
if ('layoutSizingHorizontal' in tocSection) tocSection.layoutSizingHorizontal = 'FILL';
|
|
1566
|
+
if ('layoutAlign' in tocSection) tocSection.layoutAlign = 'STRETCH';
|
|
1567
|
+
|
|
1568
|
+
${sectionTitle ? `
|
|
1569
|
+
const tocTitle = figma.createText();
|
|
1570
|
+
tocTitle.characters = ${JSON.stringify(sectionTitle)};
|
|
1571
|
+
tocTitle.fontSize = 20;
|
|
1572
|
+
tocTitle.fontName = ${headingBoldFont};
|
|
1573
|
+
tocSection.appendChild(tocTitle);
|
|
1574
|
+
if ('layoutSizingHorizontal' in tocTitle) tocTitle.layoutSizingHorizontal = 'FILL';
|
|
1575
|
+
` : ''}
|
|
1576
|
+
|
|
1577
|
+
${tocItems.map((item, idx) => `
|
|
1578
|
+
{
|
|
1579
|
+
const tocRow = figma.createFrame();
|
|
1580
|
+
tocRow.name = 'TOC Row';
|
|
1581
|
+
tocRow.layoutMode = 'HORIZONTAL';
|
|
1582
|
+
tocRow.primaryAxisSizingMode = 'AUTO';
|
|
1583
|
+
tocRow.counterAxisSizingMode = 'AUTO';
|
|
1584
|
+
tocRow.itemSpacing = 12;
|
|
1585
|
+
tocRow.fills = [];
|
|
1586
|
+
frame.children.length; // force layout
|
|
1587
|
+
tocSection.appendChild(tocRow);
|
|
1588
|
+
if ('layoutSizingHorizontal' in tocRow) tocRow.layoutSizingHorizontal = 'FILL';
|
|
1589
|
+
|
|
1590
|
+
const tocLabel = figma.createText();
|
|
1591
|
+
tocLabel.characters = ${JSON.stringify(item)};
|
|
1592
|
+
tocLabel.fontSize = 16;
|
|
1593
|
+
tocLabel.fontName = ${bodyRegularFont};
|
|
1594
|
+
tocRow.appendChild(tocLabel);
|
|
1595
|
+
if ('layoutSizingHorizontal' in tocLabel) tocLabel.layoutSizingHorizontal = 'FILL';
|
|
1596
|
+
}`).join('\n')}
|
|
1597
|
+
}`;
|
|
1598
|
+
|
|
1599
|
+
const addDivider = `
|
|
1600
|
+
{
|
|
1601
|
+
const div = figma.createFrame();
|
|
1602
|
+
div.name = 'Divider';
|
|
1603
|
+
div.resize(frame.width - 112, 1);
|
|
1604
|
+
div.fills = [{ type: 'SOLID', color: { r: 0.88, g: 0.88, b: 0.9 } }];
|
|
1605
|
+
frame.appendChild(div);
|
|
1606
|
+
if ('layoutSizingHorizontal' in div) div.layoutSizingHorizontal = 'FILL';
|
|
1607
|
+
if ('layoutAlign' in div) div.layoutAlign = 'STRETCH';
|
|
1608
|
+
}`;
|
|
1609
|
+
|
|
1610
|
+
const addDocBody = bodyText ? `
|
|
1611
|
+
{
|
|
1612
|
+
const bodySection = figma.createFrame();
|
|
1613
|
+
bodySection.name = 'Section Block - Overview';
|
|
1614
|
+
bodySection.layoutMode = 'VERTICAL';
|
|
1615
|
+
bodySection.primaryAxisSizingMode = 'AUTO';
|
|
1616
|
+
bodySection.counterAxisSizingMode = 'AUTO';
|
|
1617
|
+
bodySection.itemSpacing = 16;
|
|
1618
|
+
bodySection.fills = [];
|
|
1619
|
+
frame.appendChild(bodySection);
|
|
1620
|
+
if ('layoutSizingHorizontal' in bodySection) bodySection.layoutSizingHorizontal = 'FILL';
|
|
1621
|
+
if ('layoutAlign' in bodySection) bodySection.layoutAlign = 'STRETCH';
|
|
1622
|
+
|
|
1623
|
+
const overviewTitle = figma.createText();
|
|
1624
|
+
overviewTitle.characters = 'Overview';
|
|
1625
|
+
overviewTitle.fontSize = 24;
|
|
1626
|
+
overviewTitle.fontName = ${headingBoldFont};
|
|
1627
|
+
bodySection.appendChild(overviewTitle);
|
|
1628
|
+
if ('layoutSizingHorizontal' in overviewTitle) overviewTitle.layoutSizingHorizontal = 'FILL';
|
|
1629
|
+
|
|
1630
|
+
const bodyNode = figma.createText();
|
|
1631
|
+
bodyNode.characters = ${JSON.stringify(bodyText.slice(0, 300))};
|
|
1632
|
+
bodyNode.fontSize = 15;
|
|
1633
|
+
bodyNode.fontName = ${bodyRegularFont};
|
|
1634
|
+
bodyNode.opacity = 0.75;
|
|
1635
|
+
bodySection.appendChild(bodyNode);
|
|
1636
|
+
if ('layoutSizingHorizontal' in bodyNode) bodyNode.layoutSizingHorizontal = 'FILL';
|
|
1637
|
+
}` : '';
|
|
1638
|
+
|
|
1639
|
+
return `
|
|
1640
|
+
// ── Document Header Block ──
|
|
1641
|
+
{
|
|
1642
|
+
const headerBlock = figma.createFrame();
|
|
1643
|
+
headerBlock.name = 'Header Block';
|
|
1644
|
+
headerBlock.layoutMode = 'VERTICAL';
|
|
1645
|
+
headerBlock.primaryAxisSizingMode = 'AUTO';
|
|
1646
|
+
headerBlock.counterAxisSizingMode = 'AUTO';
|
|
1647
|
+
headerBlock.itemSpacing = 8;
|
|
1648
|
+
headerBlock.fills = [];
|
|
1649
|
+
frame.appendChild(headerBlock);
|
|
1650
|
+
if ('layoutSizingHorizontal' in headerBlock) headerBlock.layoutSizingHorizontal = 'FILL';
|
|
1651
|
+
if ('layoutAlign' in headerBlock) headerBlock.layoutAlign = 'STRETCH';
|
|
1652
|
+
|
|
1653
|
+
const hTitle = figma.createText();
|
|
1654
|
+
hTitle.characters = ${JSON.stringify(headerText)};
|
|
1655
|
+
hTitle.fontSize = 40;
|
|
1656
|
+
hTitle.fontName = ${headingBoldFont};
|
|
1657
|
+
headerBlock.appendChild(hTitle);
|
|
1658
|
+
if ('layoutSizingHorizontal' in hTitle) hTitle.layoutSizingHorizontal = 'FILL';
|
|
1659
|
+
|
|
1660
|
+
${subText ? `
|
|
1661
|
+
const hSub = figma.createText();
|
|
1662
|
+
hSub.characters = ${JSON.stringify(subText.slice(0, 140))};
|
|
1663
|
+
hSub.fontSize = 16;
|
|
1664
|
+
hSub.fontName = ${bodyRegularFont};
|
|
1665
|
+
hSub.opacity = 0.6;
|
|
1666
|
+
headerBlock.appendChild(hSub);
|
|
1667
|
+
if ('layoutSizingHorizontal' in hSub) hSub.layoutSizingHorizontal = 'FILL';
|
|
1668
|
+
` : ''}
|
|
1669
|
+
}
|
|
1670
|
+
${addDivider}
|
|
1671
|
+
${addTocSection}
|
|
1672
|
+
${addDivider}
|
|
1673
|
+
${addDocBody}
|
|
1674
|
+
`;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
default:
|
|
1678
|
+
return `
|
|
1679
|
+
${addHeading}
|
|
1680
|
+
${addSubheading}
|
|
1681
|
+
${addImageRect("Content Visual", 220)}
|
|
1682
|
+
${addTextRows(listItems.length > 0 ? listItems : ["Primary content area"], "Content")}
|
|
1683
|
+
${addCTA}
|
|
1684
|
+
`;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// ─── Prototype connection script (uses shared builder) ──────────────────────
|
|
1689
|
+
|
|
1690
|
+
function buildPrototypeScript(fromId: string, toId: string): string {
|
|
1691
|
+
return buildWireScript([{
|
|
1692
|
+
fromNodeId: fromId,
|
|
1693
|
+
toNodeId: toId,
|
|
1694
|
+
trigger: { type: "ON_CLICK" },
|
|
1695
|
+
animation: { type: "SMART_ANIMATE", duration: 0.3, easing: "EASE_IN_AND_OUT" },
|
|
1696
|
+
}]);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// ─── Flow map page ────────────────────────────────────────────────────────────
|
|
1700
|
+
|
|
1701
|
+
function buildFlowMapScript(screens: CreatedScreen[], fc: FontConfig): string {
|
|
1702
|
+
const fontLoadBlock = generateFontLoadScript(fc);
|
|
1703
|
+
const headingBoldFont = fontNameLiteral("heading", "Bold", fc);
|
|
1704
|
+
|
|
1705
|
+
const screenData = screens.map((s, i) => ({
|
|
1706
|
+
id: s.frameId,
|
|
1707
|
+
name: s.name,
|
|
1708
|
+
x: i * 280,
|
|
1709
|
+
y: 0,
|
|
1710
|
+
}));
|
|
1711
|
+
|
|
1712
|
+
return `
|
|
1713
|
+
(async () => {
|
|
1714
|
+
${fontLoadBlock}
|
|
1715
|
+
|
|
1716
|
+
// Create or find flow map page
|
|
1717
|
+
let flowPage = figma.root.children.find(p => p.name === '[Flow Map]');
|
|
1718
|
+
if (!flowPage) {
|
|
1719
|
+
flowPage = figma.createPage();
|
|
1720
|
+
flowPage.name = '[Flow Map]';
|
|
1721
|
+
}
|
|
1722
|
+
await figma.setCurrentPageAsync(flowPage);
|
|
1723
|
+
|
|
1724
|
+
const screenData = ${JSON.stringify(screenData)};
|
|
1725
|
+
const createdBoxes = [];
|
|
1726
|
+
|
|
1727
|
+
for (const s of screenData) {
|
|
1728
|
+
const box = figma.createFrame();
|
|
1729
|
+
box.name = s.name;
|
|
1730
|
+
box.resize(240, 140);
|
|
1731
|
+
box.x = s.x;
|
|
1732
|
+
box.y = 0;
|
|
1733
|
+
box.fills = [{ type: 'SOLID', color: { r: 0.93, g: 0.96, b: 1 } }];
|
|
1734
|
+
box.cornerRadius = 12;
|
|
1735
|
+
box.strokeWeight = 2;
|
|
1736
|
+
box.strokes = [{ type: 'SOLID', color: { r: 0.24, g: 0.37, b: 1 } }];
|
|
1737
|
+
|
|
1738
|
+
const label = figma.createText();
|
|
1739
|
+
label.characters = s.name;
|
|
1740
|
+
label.fontSize = 14;
|
|
1741
|
+
label.fontName = ${headingBoldFont};
|
|
1742
|
+
label.x = 16;
|
|
1743
|
+
label.y = 16;
|
|
1744
|
+
box.appendChild(label);
|
|
1745
|
+
|
|
1746
|
+
flowPage.appendChild(box);
|
|
1747
|
+
createdBoxes.push(box);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// Draw connector lines between sequential screens
|
|
1751
|
+
for (let i = 0; i < createdBoxes.length - 1; i++) {
|
|
1752
|
+
const from = createdBoxes[i];
|
|
1753
|
+
const to = createdBoxes[i + 1];
|
|
1754
|
+
const line = figma.createLine();
|
|
1755
|
+
line.x = from.x + from.width;
|
|
1756
|
+
line.y = from.y + from.height / 2;
|
|
1757
|
+
line.resize(to.x - (from.x + from.width), 0);
|
|
1758
|
+
line.strokes = [{ type: 'SOLID', color: { r: 0.24, g: 0.37, b: 1 } }];
|
|
1759
|
+
line.strokeWeight = 2;
|
|
1760
|
+
flowPage.appendChild(line);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
figma.viewport.scrollAndZoomIntoView(createdBoxes);
|
|
1764
|
+
return { pageId: flowPage.id };
|
|
1765
|
+
})();
|
|
1766
|
+
`.trim();
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// ─── Main handler ─────────────────────────────────────────────────────────────
|
|
1770
|
+
|
|
1771
|
+
export async function pageArchitectHandler(
|
|
1772
|
+
args: PageArchitectArgs
|
|
1773
|
+
): Promise<PageArchitectResult> {
|
|
1774
|
+
const {
|
|
1775
|
+
productContext,
|
|
1776
|
+
flow,
|
|
1777
|
+
platform,
|
|
1778
|
+
width: userWidth,
|
|
1779
|
+
wireframeMode = false,
|
|
1780
|
+
includeFlowMap = false,
|
|
1781
|
+
contentMode,
|
|
1782
|
+
useStockImages = true,
|
|
1783
|
+
imageQuery,
|
|
1784
|
+
} = args;
|
|
1785
|
+
|
|
1786
|
+
if (!flow) throw new Error("pageArchitect: `flow` is required.");
|
|
1787
|
+
if (!productContext) throw new Error("pageArchitect: `productContext` is required.");
|
|
1788
|
+
|
|
1789
|
+
const bridge = await getBridge();
|
|
1790
|
+
const fontConfig = resolveFontConfig(args.fonts, bridge.getActiveDesignSystemId());
|
|
1791
|
+
|
|
1792
|
+
// 1. Parse flow into screen specs
|
|
1793
|
+
const screenSpecs = parseFlowToScreens(productContext, flow, contentMode);
|
|
1794
|
+
|
|
1795
|
+
if (screenSpecs.length === 0) {
|
|
1796
|
+
throw new Error("pageArchitect: Could not parse any screens from the flow description.");
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// 1.5 Generate AI content bundle for realistic mode
|
|
1800
|
+
let contentBundle: ContentBundle | null = null;
|
|
1801
|
+
if (contentMode === "realistic") {
|
|
1802
|
+
try {
|
|
1803
|
+
contentBundle = await generateContentBundle(productContext, screenSpecs.map((s) => s.name));
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
console.warn(`pageArchitect: content bundle generation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1806
|
+
}
|
|
1807
|
+
if (contentBundle) {
|
|
1808
|
+
for (const [index, spec] of screenSpecs.entries()) {
|
|
1809
|
+
spec.realisticContent = applyBundleToScreen(spec.template, spec.name, contentBundle, index);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// 2. Load DS components for matching + tokens for palette resolution
|
|
1815
|
+
const componentSets = await bridge.getComponentSets();
|
|
1816
|
+
const fuse = buildFuse(componentSets);
|
|
1817
|
+
|
|
1818
|
+
// Fetch design tokens to resolve palette colors and bind variables
|
|
1819
|
+
let dsTokens: Token[] = [];
|
|
1820
|
+
let palette: ResolvedPalette | undefined;
|
|
1821
|
+
const dsId = bridge.getActiveDesignSystemId();
|
|
1822
|
+
try {
|
|
1823
|
+
dsTokens = await bridge.getTokens();
|
|
1824
|
+
// When a DS is selected, its tokens are authoritative (no fallback to file tokens)
|
|
1825
|
+
if (dsId || dsTokens.length > 0) {
|
|
1826
|
+
palette = resolveDesignPalette(dsTokens, dsId);
|
|
1827
|
+
}
|
|
1828
|
+
} catch {
|
|
1829
|
+
// If DS is selected, still resolve palette from DS tokens even if getTokens fails
|
|
1830
|
+
if (dsId) {
|
|
1831
|
+
palette = resolveDesignPalette([], dsId);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// 2.5 Optionally prepare imagery for image-heavy flows
|
|
1836
|
+
const shouldUseStockImages =
|
|
1837
|
+
useStockImages &&
|
|
1838
|
+
!wireframeMode;
|
|
1839
|
+
|
|
1840
|
+
let stockImagery: ScreenImagery[] = [];
|
|
1841
|
+
if (shouldUseStockImages) {
|
|
1842
|
+
try {
|
|
1843
|
+
const querySource = imageQuery?.trim()
|
|
1844
|
+
? imageQuery
|
|
1845
|
+
: contentBundle?.imageQuery
|
|
1846
|
+
? contentBundle.imageQuery
|
|
1847
|
+
: guessImageQuery(productContext, flow);
|
|
1848
|
+
stockImagery = await loadStockImagery(querySource, flow, screenSpecs.length, platform === "mobile" ? "portrait" : "landscape");
|
|
1849
|
+
} catch (error) {
|
|
1850
|
+
console.warn(`pageArchitect: stock imagery lookup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// 3. Build each screen frame
|
|
1855
|
+
const createdScreens: CreatedScreen[] = [];
|
|
1856
|
+
const frameWidth = platformWidth(platform, userWidth);
|
|
1857
|
+
const FRAME_GAP = 80;
|
|
1858
|
+
|
|
1859
|
+
// Find rightmost existing frame so new screens don't overlap prior work
|
|
1860
|
+
let xOffset = 0;
|
|
1861
|
+
try {
|
|
1862
|
+
const posScript = `(async () => {
|
|
1863
|
+
const frames = figma.currentPage.children.filter(n =>
|
|
1864
|
+
n.type === 'FRAME' && !n.name.startsWith('__agent_')
|
|
1865
|
+
);
|
|
1866
|
+
const maxX = frames.reduce((max, f) => Math.max(max, f.x + f.width), 0);
|
|
1867
|
+
return { maxX };
|
|
1868
|
+
})();`;
|
|
1869
|
+
const posResult = await bridge.execute(posScript);
|
|
1870
|
+
const posMax = (posResult.result as { maxX?: number })?.maxX ?? 0;
|
|
1871
|
+
if (posResult.success && posMax > 0) {
|
|
1872
|
+
xOffset = posMax + FRAME_GAP;
|
|
1873
|
+
}
|
|
1874
|
+
} catch {
|
|
1875
|
+
// ignore — start at 0
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
for (const [index, spec] of screenSpecs.entries()) {
|
|
1879
|
+
const resolved = resolveComponents(spec.requiredComponents, fuse);
|
|
1880
|
+
const assignedImage = stockImagery[index % Math.max(stockImagery.length, 1)];
|
|
1881
|
+
|
|
1882
|
+
const frameSpec: FrameSpec = {
|
|
1883
|
+
name: spec.name,
|
|
1884
|
+
width: frameWidth,
|
|
1885
|
+
template: spec.template,
|
|
1886
|
+
wireframeMode,
|
|
1887
|
+
contentMode,
|
|
1888
|
+
content: spec.realisticContent,
|
|
1889
|
+
resolvedComponents: resolved,
|
|
1890
|
+
xOffset,
|
|
1891
|
+
purpose: spec.purpose,
|
|
1892
|
+
imageHash: assignedImage?.imageHash ?? null,
|
|
1893
|
+
resolvedPalette: palette,
|
|
1894
|
+
fontConfig,
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1897
|
+
// Build final frame directly in a single execute call (no shimmer phase —
|
|
1898
|
+
// eliminates one WebSocket round-trip and prevents duplicate/overlapping frames).
|
|
1899
|
+
const script = buildScreenScript(frameSpec);
|
|
1900
|
+
const execResult = await bridge.execute(script);
|
|
1901
|
+
|
|
1902
|
+
if (execResult.success && execResult.result) {
|
|
1903
|
+
const res = execResult.result as { frameId: string };
|
|
1904
|
+
if (res.frameId) {
|
|
1905
|
+
createdScreens.push({
|
|
1906
|
+
frameId: res.frameId,
|
|
1907
|
+
name: spec.name,
|
|
1908
|
+
template: spec.template,
|
|
1909
|
+
instantiatedComponents: resolved
|
|
1910
|
+
.filter((c) => c.nodeId !== null)
|
|
1911
|
+
.map((c) => c.name),
|
|
1912
|
+
});
|
|
1913
|
+
xOffset += frameWidth + FRAME_GAP;
|
|
1914
|
+
}
|
|
1915
|
+
} else {
|
|
1916
|
+
console.error(
|
|
1917
|
+
`pageArchitect: Failed to create frame "${spec.name}": ${execResult.error}`
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// If "both" platforms, also create mobile frame
|
|
1922
|
+
const mw = mobileWidth(platform);
|
|
1923
|
+
if (mw && mw !== frameWidth) {
|
|
1924
|
+
const mobileSpec: FrameSpec = {
|
|
1925
|
+
...frameSpec,
|
|
1926
|
+
width: mw,
|
|
1927
|
+
name: `${spec.name} (Mobile)`,
|
|
1928
|
+
xOffset,
|
|
1929
|
+
};
|
|
1930
|
+
const mobileScript = buildScreenScript(mobileSpec);
|
|
1931
|
+
const mobileResult = await bridge.execute(mobileScript);
|
|
1932
|
+
if (mobileResult.success && mobileResult.result) {
|
|
1933
|
+
const res = mobileResult.result as { frameId: string };
|
|
1934
|
+
if (res.frameId) {
|
|
1935
|
+
createdScreens.push({
|
|
1936
|
+
frameId: res.frameId,
|
|
1937
|
+
name: `${spec.name} (Mobile)`,
|
|
1938
|
+
template: spec.template,
|
|
1939
|
+
instantiatedComponents: resolved
|
|
1940
|
+
.filter((c) => c.nodeId !== null)
|
|
1941
|
+
.map((c) => c.name),
|
|
1942
|
+
});
|
|
1943
|
+
xOffset += mw + FRAME_GAP;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// 4. Wire prototype connections between consecutive screens
|
|
1950
|
+
const prototypeConnections: PrototypeConnection[] = [];
|
|
1951
|
+
const primaryScreens = createdScreens.filter((s) => !s.name.includes("(Mobile)"));
|
|
1952
|
+
|
|
1953
|
+
for (let i = 0; i < primaryScreens.length - 1; i++) {
|
|
1954
|
+
const from = primaryScreens[i];
|
|
1955
|
+
const to = primaryScreens[i + 1];
|
|
1956
|
+
const script = buildPrototypeScript(from.frameId, to.frameId);
|
|
1957
|
+
const result = await bridge.execute(script);
|
|
1958
|
+
if (result.success) {
|
|
1959
|
+
prototypeConnections.push({
|
|
1960
|
+
fromFrameId: from.frameId,
|
|
1961
|
+
toFrameId: to.frameId,
|
|
1962
|
+
fromName: from.name,
|
|
1963
|
+
toName: to.name,
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// 5. Optionally create flow map page
|
|
1969
|
+
let flowMapPageId: string | null = null;
|
|
1970
|
+
if (includeFlowMap && createdScreens.length > 0) {
|
|
1971
|
+
const flowScript = buildFlowMapScript(primaryScreens, fontConfig);
|
|
1972
|
+
const flowResult = await bridge.execute(flowScript);
|
|
1973
|
+
if (flowResult.success && flowResult.result) {
|
|
1974
|
+
const res = flowResult.result as { pageId: string };
|
|
1975
|
+
flowMapPageId = res.pageId;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// 6. Log the decision
|
|
1980
|
+
const logEntry = await decisionLog.log({
|
|
1981
|
+
tool: "page-architect",
|
|
1982
|
+
nodeIds: createdScreens.map((s) => s.frameId),
|
|
1983
|
+
rationale: `Built ${createdScreens.length} screen frame(s) for flow: "${flow.slice(0, 100)}". Platform: ${platform}. Content: ${contentMode}. AI content bundle: ${!!contentBundle}. Wireframe: ${wireframeMode}. Prototype connections: ${prototypeConnections.length}. Flow map: ${!!flowMapPageId}. Token binding: ${palette ? "active" : "none"} (${dsTokens.length} tokens resolved).`,
|
|
1984
|
+
tokens: dsTokens.slice(0, 20).map((t) => t.name),
|
|
1985
|
+
reversible: true,
|
|
1986
|
+
metadata: {
|
|
1987
|
+
productContext,
|
|
1988
|
+
platform,
|
|
1989
|
+
wireframeMode,
|
|
1990
|
+
contentMode,
|
|
1991
|
+
useStockImages: shouldUseStockImages,
|
|
1992
|
+
stockImageCount: stockImagery.length,
|
|
1993
|
+
stockImageSources: stockImagery.map((item) => item.sourceUrl),
|
|
1994
|
+
includeFlowMap,
|
|
1995
|
+
screenCount: createdScreens.length,
|
|
1996
|
+
prototypeConnectionCount: prototypeConnections.length,
|
|
1997
|
+
flowMapPageId,
|
|
1998
|
+
},
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
// 7. Navigate viewport to the first created screen so the user lands there
|
|
2002
|
+
const primaryScreens2 = createdScreens.filter((s) => !s.name.includes("(Mobile)"));
|
|
2003
|
+
if (primaryScreens2.length > 0) {
|
|
2004
|
+
try {
|
|
2005
|
+
await bridge.navigate(primaryScreens2[0].frameId);
|
|
2006
|
+
} catch {
|
|
2007
|
+
// Non-critical navigation error — ignore
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
return {
|
|
2012
|
+
screens: createdScreens,
|
|
2013
|
+
prototypeConnections,
|
|
2014
|
+
flowMapPageId,
|
|
2015
|
+
frameIds: createdScreens.map((s) => s.frameId),
|
|
2016
|
+
prototypeConnectionCount: prototypeConnections.length,
|
|
2017
|
+
logEntryId: logEntry.id,
|
|
2018
|
+
};
|
|
2019
|
+
}
|