@obra-studio/figma-console-mcp 1.32.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 +21 -0
- package/README.md +879 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js +278 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts +29 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js +358 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js +342 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js +231 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts +27 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js +93 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js +309 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js +350 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts +89 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.js +41 -0
- package/dist/apps/design-system-dashboard/scoring/types.js.map +1 -0
- package/dist/apps/design-system-dashboard/server.d.ts +24 -0
- package/dist/apps/design-system-dashboard/server.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/server.js +160 -0
- package/dist/apps/design-system-dashboard/server.js.map +1 -0
- package/dist/apps/token-browser/server.d.ts +26 -0
- package/dist/apps/token-browser/server.d.ts.map +1 -0
- package/dist/apps/token-browser/server.js +137 -0
- package/dist/apps/token-browser/server.js.map +1 -0
- package/dist/browser/base.d.ts +58 -0
- package/dist/browser/base.d.ts.map +1 -0
- package/dist/browser/base.js +6 -0
- package/dist/browser/base.js.map +1 -0
- package/dist/browser/local.d.ts +87 -0
- package/dist/browser/local.d.ts.map +1 -0
- package/dist/browser/local.js +318 -0
- package/dist/browser/local.js.map +1 -0
- package/dist/core/accessibility-tools.d.ts +21 -0
- package/dist/core/accessibility-tools.d.ts.map +1 -0
- package/dist/core/accessibility-tools.js +307 -0
- package/dist/core/accessibility-tools.js.map +1 -0
- package/dist/core/annotation-tools.d.ts +14 -0
- package/dist/core/annotation-tools.d.ts.map +1 -0
- package/dist/core/annotation-tools.js +231 -0
- package/dist/core/annotation-tools.js.map +1 -0
- package/dist/core/autodocs-tools.d.ts +7 -0
- package/dist/core/autodocs-tools.d.ts.map +1 -0
- package/dist/core/autodocs-tools.js +195 -0
- package/dist/core/autodocs-tools.js.map +1 -0
- package/dist/core/comment-tools.d.ts +11 -0
- package/dist/core/comment-tools.d.ts.map +1 -0
- package/dist/core/comment-tools.js +293 -0
- package/dist/core/comment-tools.js.map +1 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +154 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/console-monitor.d.ts +82 -0
- package/dist/core/console-monitor.d.ts.map +1 -0
- package/dist/core/console-monitor.js +428 -0
- package/dist/core/console-monitor.js.map +1 -0
- package/dist/core/deep-component-tools.d.ts +14 -0
- package/dist/core/deep-component-tools.d.ts.map +1 -0
- package/dist/core/deep-component-tools.js +129 -0
- package/dist/core/deep-component-tools.js.map +1 -0
- package/dist/core/design-code-tools.d.ts +116 -0
- package/dist/core/design-code-tools.d.ts.map +1 -0
- package/dist/core/design-code-tools.js +2751 -0
- package/dist/core/design-code-tools.js.map +1 -0
- package/dist/core/design-system-manifest.d.ts +272 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -0
- package/dist/core/design-system-manifest.js +261 -0
- package/dist/core/design-system-manifest.js.map +1 -0
- package/dist/core/design-system-tools.d.ts +67 -0
- package/dist/core/design-system-tools.d.ts.map +1 -0
- package/dist/core/design-system-tools.js +874 -0
- package/dist/core/design-system-tools.js.map +1 -0
- package/dist/core/diagnose-tool.d.ts +33 -0
- package/dist/core/diagnose-tool.d.ts.map +1 -0
- package/dist/core/diagnose-tool.js +97 -0
- package/dist/core/diagnose-tool.js.map +1 -0
- package/dist/core/diff/changelog-formatter.d.ts +35 -0
- package/dist/core/diff/changelog-formatter.d.ts.map +1 -0
- package/dist/core/diff/changelog-formatter.js +276 -0
- package/dist/core/diff/changelog-formatter.js.map +1 -0
- package/dist/core/diff/diff-engine.d.ts +127 -0
- package/dist/core/diff/diff-engine.d.ts.map +1 -0
- package/dist/core/diff/diff-engine.js +335 -0
- package/dist/core/diff/diff-engine.js.map +1 -0
- package/dist/core/diff/property-compare.d.ts +19 -0
- package/dist/core/diff/property-compare.d.ts.map +1 -0
- package/dist/core/diff/property-compare.js +37 -0
- package/dist/core/diff/property-compare.js.map +1 -0
- package/dist/core/diff/version-cache.d.ts +40 -0
- package/dist/core/diff/version-cache.d.ts.map +1 -0
- package/dist/core/diff/version-cache.js +75 -0
- package/dist/core/diff/version-cache.js.map +1 -0
- package/dist/core/enrichment/enrichment-service.d.ts +52 -0
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
- package/dist/core/enrichment/enrichment-service.js +369 -0
- package/dist/core/enrichment/enrichment-service.js.map +1 -0
- package/dist/core/enrichment/index.d.ts +8 -0
- package/dist/core/enrichment/index.d.ts.map +1 -0
- package/dist/core/enrichment/index.js +8 -0
- package/dist/core/enrichment/index.js.map +1 -0
- package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
- package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
- package/dist/core/enrichment/relationship-mapper.js +352 -0
- package/dist/core/enrichment/relationship-mapper.js.map +1 -0
- package/dist/core/enrichment/style-resolver.d.ts +80 -0
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
- package/dist/core/enrichment/style-resolver.js +327 -0
- package/dist/core/enrichment/style-resolver.js.map +1 -0
- package/dist/core/figjam-tools.d.ts +8 -0
- package/dist/core/figjam-tools.d.ts.map +1 -0
- package/dist/core/figjam-tools.js +548 -0
- package/dist/core/figjam-tools.js.map +1 -0
- package/dist/core/figma-api.d.ts +245 -0
- package/dist/core/figma-api.d.ts.map +1 -0
- package/dist/core/figma-api.js +446 -0
- package/dist/core/figma-api.js.map +1 -0
- package/dist/core/figma-connector.d.ts +180 -0
- package/dist/core/figma-connector.d.ts.map +1 -0
- package/dist/core/figma-connector.js +8 -0
- package/dist/core/figma-connector.js.map +1 -0
- package/dist/core/figma-desktop-connector.d.ts +312 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -0
- package/dist/core/figma-desktop-connector.js +1298 -0
- package/dist/core/figma-desktop-connector.js.map +1 -0
- package/dist/core/figma-reconstruction-spec.d.ts +166 -0
- package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
- package/dist/core/figma-reconstruction-spec.js +403 -0
- package/dist/core/figma-reconstruction-spec.js.map +1 -0
- package/dist/core/figma-style-extractor.d.ts +76 -0
- package/dist/core/figma-style-extractor.d.ts.map +1 -0
- package/dist/core/figma-style-extractor.js +312 -0
- package/dist/core/figma-style-extractor.js.map +1 -0
- package/dist/core/figma-tools.d.ts +22 -0
- package/dist/core/figma-tools.d.ts.map +1 -0
- package/dist/core/figma-tools.js +3187 -0
- package/dist/core/figma-tools.js.map +1 -0
- package/dist/core/identity.d.ts +41 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +97 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/library-tools.d.ts +17 -0
- package/dist/core/library-tools.d.ts.map +1 -0
- package/dist/core/library-tools.js +581 -0
- package/dist/core/library-tools.js.map +1 -0
- package/dist/core/logger.d.ts +22 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +54 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/port-discovery.d.ts +171 -0
- package/dist/core/port-discovery.d.ts.map +1 -0
- package/dist/core/port-discovery.js +563 -0
- package/dist/core/port-discovery.js.map +1 -0
- package/dist/core/resolve-package-root.d.ts +2 -0
- package/dist/core/resolve-package-root.d.ts.map +1 -0
- package/dist/core/resolve-package-root.js +12 -0
- package/dist/core/resolve-package-root.js.map +1 -0
- package/dist/core/slides-tools.d.ts +8 -0
- package/dist/core/slides-tools.d.ts.map +1 -0
- package/dist/core/slides-tools.js +715 -0
- package/dist/core/slides-tools.js.map +1 -0
- package/dist/core/snippet-injector.d.ts +24 -0
- package/dist/core/snippet-injector.d.ts.map +1 -0
- package/dist/core/snippet-injector.js +97 -0
- package/dist/core/snippet-injector.js.map +1 -0
- package/dist/core/tokens/alias-resolver.d.ts +55 -0
- package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
- package/dist/core/tokens/alias-resolver.js +136 -0
- package/dist/core/tokens/alias-resolver.js.map +1 -0
- package/dist/core/tokens/config.d.ts +87 -0
- package/dist/core/tokens/config.d.ts.map +1 -0
- package/dist/core/tokens/config.js +285 -0
- package/dist/core/tokens/config.js.map +1 -0
- package/dist/core/tokens/figma-converter.d.ts +81 -0
- package/dist/core/tokens/figma-converter.d.ts.map +1 -0
- package/dist/core/tokens/figma-converter.js +196 -0
- package/dist/core/tokens/figma-converter.js.map +1 -0
- package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
- package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
- package/dist/core/tokens/formatters/css-vars.js +330 -0
- package/dist/core/tokens/formatters/css-vars.js.map +1 -0
- package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
- package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
- package/dist/core/tokens/formatters/dtcg.js +301 -0
- package/dist/core/tokens/formatters/dtcg.js.map +1 -0
- package/dist/core/tokens/formatters/index.d.ts +30 -0
- package/dist/core/tokens/formatters/index.d.ts.map +1 -0
- package/dist/core/tokens/formatters/index.js +46 -0
- package/dist/core/tokens/formatters/index.js.map +1 -0
- package/dist/core/tokens/formatters/json.d.ts +37 -0
- package/dist/core/tokens/formatters/json.d.ts.map +1 -0
- package/dist/core/tokens/formatters/json.js +188 -0
- package/dist/core/tokens/formatters/json.js.map +1 -0
- package/dist/core/tokens/formatters/less.d.ts +4 -0
- package/dist/core/tokens/formatters/less.d.ts.map +1 -0
- package/dist/core/tokens/formatters/less.js +5 -0
- package/dist/core/tokens/formatters/less.js.map +1 -0
- package/dist/core/tokens/formatters/scss.d.ts +26 -0
- package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
- package/dist/core/tokens/formatters/scss.js +253 -0
- package/dist/core/tokens/formatters/scss.js.map +1 -0
- package/dist/core/tokens/formatters/stubs.d.ts +9 -0
- package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
- package/dist/core/tokens/formatters/stubs.js +14 -0
- package/dist/core/tokens/formatters/stubs.js.map +1 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +45 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.js +208 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v3.d.ts +37 -0
- package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v3.js +238 -0
- package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v4.d.ts +41 -0
- package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v4.js +331 -0
- package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
- package/dist/core/tokens/formatters/tokens-studio.d.ts +44 -0
- package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
- package/dist/core/tokens/formatters/tokens-studio.js +251 -0
- package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
- package/dist/core/tokens/formatters/ts-module.d.ts +35 -0
- package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
- package/dist/core/tokens/formatters/ts-module.js +199 -0
- package/dist/core/tokens/formatters/ts-module.js.map +1 -0
- package/dist/core/tokens/index.d.ts +17 -0
- package/dist/core/tokens/index.d.ts.map +1 -0
- package/dist/core/tokens/index.js +16 -0
- package/dist/core/tokens/index.js.map +1 -0
- package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
- package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
- package/dist/core/tokens/parsers/css-vars.js +5 -0
- package/dist/core/tokens/parsers/css-vars.js.map +1 -0
- package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
- package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
- package/dist/core/tokens/parsers/dtcg.js +254 -0
- package/dist/core/tokens/parsers/dtcg.js.map +1 -0
- package/dist/core/tokens/parsers/index.d.ts +37 -0
- package/dist/core/tokens/parsers/index.d.ts.map +1 -0
- package/dist/core/tokens/parsers/index.js +139 -0
- package/dist/core/tokens/parsers/index.js.map +1 -0
- package/dist/core/tokens/parsers/json.d.ts +4 -0
- package/dist/core/tokens/parsers/json.d.ts.map +1 -0
- package/dist/core/tokens/parsers/json.js +8 -0
- package/dist/core/tokens/parsers/json.js.map +1 -0
- package/dist/core/tokens/parsers/scss.d.ts +3 -0
- package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
- package/dist/core/tokens/parsers/scss.js +5 -0
- package/dist/core/tokens/parsers/scss.js.map +1 -0
- package/dist/core/tokens/parsers/stubs.d.ts +15 -0
- package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
- package/dist/core/tokens/parsers/stubs.js +21 -0
- package/dist/core/tokens/parsers/stubs.js.map +1 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
- package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
- package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
- package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
- package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
- package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
- package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
- package/dist/core/tokens/parsers/tokens-studio.js +5 -0
- package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
- package/dist/core/tokens/schemas.d.ts +31 -0
- package/dist/core/tokens/schemas.d.ts.map +1 -0
- package/dist/core/tokens/schemas.js +149 -0
- package/dist/core/tokens/schemas.js.map +1 -0
- package/dist/core/tokens/transforms/color.d.ts +9 -0
- package/dist/core/tokens/transforms/color.d.ts.map +1 -0
- package/dist/core/tokens/transforms/color.js +13 -0
- package/dist/core/tokens/transforms/color.js.map +1 -0
- package/dist/core/tokens/transforms/index.d.ts +36 -0
- package/dist/core/tokens/transforms/index.d.ts.map +1 -0
- package/dist/core/tokens/transforms/index.js +30 -0
- package/dist/core/tokens/transforms/index.js.map +1 -0
- package/dist/core/tokens/transforms/size.d.ts +7 -0
- package/dist/core/tokens/transforms/size.d.ts.map +1 -0
- package/dist/core/tokens/transforms/size.js +8 -0
- package/dist/core/tokens/transforms/size.js.map +1 -0
- package/dist/core/tokens/types.d.ts +228 -0
- package/dist/core/tokens/types.d.ts.map +1 -0
- package/dist/core/tokens/types.js +19 -0
- package/dist/core/tokens/types.js.map +1 -0
- package/dist/core/tokens-tools.d.ts +42 -0
- package/dist/core/tokens-tools.d.ts.map +1 -0
- package/dist/core/tokens-tools.js +860 -0
- package/dist/core/tokens-tools.js.map +1 -0
- package/dist/core/types/design-code.d.ts +271 -0
- package/dist/core/types/design-code.d.ts.map +1 -0
- package/dist/core/types/design-code.js +5 -0
- package/dist/core/types/design-code.js.map +1 -0
- package/dist/core/types/enriched.d.ts +213 -0
- package/dist/core/types/enriched.d.ts.map +1 -0
- package/dist/core/types/enriched.js +6 -0
- package/dist/core/types/enriched.js.map +1 -0
- package/dist/core/types/index.d.ts +104 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +5 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/core/variable-resolver.d.ts +45 -0
- package/dist/core/variable-resolver.d.ts.map +1 -0
- package/dist/core/variable-resolver.js +86 -0
- package/dist/core/variable-resolver.js.map +1 -0
- package/dist/core/version-tools.d.ts +59 -0
- package/dist/core/version-tools.d.ts.map +1 -0
- package/dist/core/version-tools.js +1159 -0
- package/dist/core/version-tools.js.map +1 -0
- package/dist/core/websocket-connector.d.ts +187 -0
- package/dist/core/websocket-connector.d.ts.map +1 -0
- package/dist/core/websocket-connector.js +378 -0
- package/dist/core/websocket-connector.js.map +1 -0
- package/dist/core/websocket-server.js +866 -0
- package/dist/core/websocket-server.js.map +1 -0
- package/dist/core/write-tools.d.ts +7 -0
- package/dist/core/write-tools.d.ts.map +1 -0
- package/dist/core/write-tools.js +2172 -0
- package/dist/core/write-tools.js.map +1 -0
- package/dist/local.d.ts +95 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +3036 -0
- package/dist/local.js.map +1 -0
- package/dist/vendor/obra-autodocs/autodocs-body.generated.d.ts +2 -0
- package/dist/vendor/obra-autodocs/autodocs-body.generated.d.ts.map +1 -0
- package/dist/vendor/obra-autodocs/autodocs-body.generated.js +6 -0
- package/dist/vendor/obra-autodocs/autodocs-body.generated.js.map +1 -0
- package/figma-desktop-bridge/README.md +365 -0
- package/figma-desktop-bridge/code.js +6504 -0
- package/figma-desktop-bridge/icon.png +0 -0
- package/figma-desktop-bridge/manifest.json +67 -0
- package/figma-desktop-bridge/ui.html +2441 -0
- package/package.json +98 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// GENERATED by scripts/build-autodocs-runtime.mjs — DO NOT EDIT.
|
|
2
|
+
// Embeds the stripped Obra Autodocs plugin body (vendored code.js) as a string
|
|
3
|
+
// so it compiles into dist/ and ships with the package.
|
|
4
|
+
/* eslint-disable */
|
|
5
|
+
export const AUTODOCS_BODY = "// GENERATED by scripts/build-autodocs-runtime.mjs — DO NOT EDIT.\n// Source: src/vendor/obra-autodocs/code.js (Obra Autodocs plugin).\n// Three top-level UI-init statements stripped; hoisted declarations retained.\n// The MCP tool appends a font-load + generate()/remove entry at call time.\n// Obra Autodocs — Figma Plugin\n// Generates labeled grid documentation for component variant sets\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\nconst BRACKET_THICKNESS = 14;\nconst BRACKET_CAP_LENGTH = 7;\nconst BRACKET_LABEL_ROW_HEIGHT = 42;\nconst SIMPLE_LABEL_ROW_HEIGHT = 25;\nconst LABEL_PADDING = 10;\nconst LABEL_FONT_SIZE = 11;\nconst DEFAULT_COLOR = { r: 0.592, g: 0.278, b: 1.0 }; // #9747FF\nconst BRACKET_STROKE_WEIGHT = 1;\nconst CLUSTER_TOLERANCE = 5;\nconst LABEL_GAP = 8;\nconst GRID_STROKE_WEIGHT = 1;\nconst GRID_DASH_PATTERN = [4, 4];\nconst BOOL_ACCURACY_MAX_COMBINATIONS = 1000;\nconst ITEM_LABEL_GAP = 6; // gap between variant bottom and per-item label\nconst ITEM_LABEL_HEIGHT = 14; // reserved height for per-item label text inside cell\nconst TITLE_FONT_SIZE = 18;\nconst TITLE_GAP = 16; // gap between title and documentation frame\nconst DESC_GAP = 16;\nconst DOC_LINK_FONT_SIZE = 11;\nconst DOC_LINK_GAP = 8;\nconst DESC_PADDING = 12;\nconst DESC_FONT_SIZE = 12;\nconst DESC_LINE_HEIGHT = 1.5;\n\nvar DEBUG = false;\n\n// Mutable doc color — set per generation\nvar DOC_COLOR = { r: DEFAULT_COLOR.r, g: DEFAULT_COLOR.g, b: DEFAULT_COLOR.b };\n\n// Font cache — load eagerly at startup to avoid blocking generation\nvar _fontLoaded = false;\nvar _fontLoadPromise = null;\n\n// Reusable text measurement node + cache\nvar _measureNode = null;\nvar _measureCache = {};\n\n// Per-variant visual size overrides (for absolute-positioned overflow accommodation)\n// When set, effectiveSize() returns the visual size (frame + overflow) instead of frame size.\nvar _visualSizeOverrides = null; // { [variantId]: { width, height } }\nvar _selectionGeneration = 0; // guards against stale async sendSelectionInfo results\nvar _selectionDebounceTimer = null;\nvar _hidePropertyNames = false; // When true, labels show only values (no \"PropName: \" prefix)\nvar _autoLayoutExtras = false; // When true, Property Combinations uses auto layout instead of manual positioning\n\n// ─── Per-Component Settings Persistence ─────────────────────────────────────\n\nfunction saveComponentSettings(cs, options) {\n if (!cs) return;\n try {\n var settings = {\n showGrid: options.showGrid || false,\n color: options.color || '#9747FF',\n showBooleanVisibility: options.showBooleanVisibility || false,\n booleanCombination: options.booleanCombination || 'individual',\n booleanDisplayMode: options.booleanDisplayMode || 'list',\n enabledBooleanProps: options.enabledBooleanProps || null,\n booleanScope: options.booleanScope || null,\n showNestedInstances: options.showNestedInstances || false,\n nestedInstancesMode: options.nestedInstancesMode || 'representative',\n enabledNestedInstances: options.enabledNestedInstances || null,\n standaloneDoc: options.standaloneDoc || false,\n hidePropertyNames: options.hidePropertyNames || false,\n showTitle: options.showTitle || false,\n showDescription: options.showDescription || false,\n showDocLink: options.showDocLink || false,\n allowSpanning: options.allowSpanning || false,\n autoLayoutExtras: options.autoLayoutExtras || false,\n variableModes: options.variableModes || null,\n docMode: options.docMode || 'base',\n };\n cs.setPluginData('lastSettings', JSON.stringify(settings));\n if (DEBUG) console.log('[Settings] Saved for', cs.name, JSON.stringify(settings));\n } catch (e) {\n console.warn('[Settings] Failed to save:', e.message);\n }\n}\n\nfunction getComponentSettings(cs) {\n if (!cs) return null;\n try {\n var data = cs.getPluginData('lastSettings');\n if (!data) return null;\n return JSON.parse(data);\n } catch (e) {\n console.warn('[Settings] Failed to read:', e.message);\n return null;\n }\n}\n\n// ─── Shadcn Style Filter ────────────────────────────────────────────────────\n// Hide or delete variants by a \"Shadcn style\" variant property value. Hiding\n// sets visible=false on matching variants, stashes them below the active grid,\n// and regenerates docs; the filtered variants are skipped by analyzeLayout and\n// all doc builders.\n\nvar SHADCN_STYLE_PROP_NAMES = ['shadcn style'];\n\nfunction findShadcnStylePropKey(cs) {\n if (!cs) return null;\n try {\n var groupProps = cs.variantGroupProperties;\n if (!groupProps) return null;\n for (var key in groupProps) {\n if (!groupProps.hasOwnProperty(key)) continue;\n if (SHADCN_STYLE_PROP_NAMES.indexOf(key.toLowerCase()) !== -1) return key;\n }\n } catch (e) {}\n return null;\n}\n\nfunction getHiddenStyles(cs) {\n if (!cs) return [];\n try {\n var raw = cs.getPluginData('hiddenStyles');\n if (!raw) return [];\n var parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed;\n } catch (e) { return []; }\n}\n\nfunction setHiddenStyles(cs, arr) {\n if (!cs) return;\n try { cs.setPluginData('hiddenStyles', JSON.stringify(arr || [])); } catch (e) {}\n}\n\nfunction getShadcnStyleInfo(cs) {\n var propKey = findShadcnStylePropKey(cs);\n if (!propKey) return null;\n var variants = cs.children.filter(function(c) { return c.type === 'COMPONENT'; });\n var counts = {};\n var order = [];\n for (var i = 0; i < variants.length; i++) {\n var parsed = dvParseVariantName(variants[i].name);\n var val = parsed[propKey];\n if (val === undefined) continue;\n if (counts[val] === undefined) { counts[val] = 0; order.push(val); }\n counts[val]++;\n }\n if (order.length === 0) return null;\n var hiddenList = getHiddenStyles(cs);\n // Prune stale hidden entries (values that no longer exist)\n var pruned = hiddenList.filter(function(v) { return counts[v] !== undefined; });\n if (pruned.length !== hiddenList.length) {\n setHiddenStyles(cs, pruned);\n hiddenList = pruned;\n }\n return {\n propName: propKey,\n values: order.map(function(v) {\n return { name: v, count: counts[v], hidden: hiddenList.indexOf(v) !== -1 };\n }),\n };\n}\n\n// Active variants = COMPONENT children that are visible. Hidden-by-style\n// variants have visible=false so they're skipped during layout + generation\n// but remain as members of the CS (so Figma keeps the variant property alive).\nfunction getActiveVariants(cs) {\n return cs.children.filter(function(c) {\n return c.type === 'COMPONENT' && c.visible !== false;\n });\n}\n\nasync function applyHiddenStyles(hiddenValues, regenOptions) {\n var cs = getComponentSet();\n if (!cs) { figma.notify('Select a component set first'); return; }\n var propKey = findShadcnStylePropKey(cs);\n if (!propKey) { figma.notify('No \"Shadcn style\" property on this component set'); return; }\n\n var hiddenSet = {};\n for (var i = 0; i < hiddenValues.length; i++) hiddenSet[hiddenValues[i]] = true;\n\n var allVariants = cs.children.filter(function(c) { return c.type === 'COMPONENT'; });\n var wouldHide = 0;\n for (var v = 0; v < allVariants.length; v++) {\n var parsed = dvParseVariantName(allVariants[v].name);\n if (parsed[propKey] !== undefined && hiddenSet[parsed[propKey]]) wouldHide++;\n }\n if (wouldHide === allVariants.length) {\n figma.notify('Cannot hide every style — at least one must stay visible');\n return;\n }\n\n // Apply visibility\n for (var j = 0; j < allVariants.length; j++) {\n var p = dvParseVariantName(allVariants[j].name);\n var styleVal = p[propKey];\n var shouldHide = styleVal !== undefined && hiddenSet[styleVal];\n if (allVariants[j].visible !== !shouldHide) {\n allVariants[j].visible = !shouldHide;\n }\n }\n\n setHiddenStyles(cs, hiddenValues);\n\n // Regenerate docs if there's an existing wrapper\n var wrapper = findExistingWrapper(cs);\n if (wrapper && regenOptions) {\n await generate(regenOptions);\n } else {\n sendSelectionInfo();\n }\n}\n\nasync function deleteStyleVariants(styleValue, regenOptions) {\n var cs = getComponentSet();\n if (!cs) { figma.notify('Select a component set first'); return; }\n var propKey = findShadcnStylePropKey(cs);\n if (!propKey) { figma.notify('No \"Shadcn style\" property on this component set'); return; }\n\n var allVariants = cs.children.filter(function(c) { return c.type === 'COMPONENT'; });\n var toDelete = [];\n var wouldKeep = 0;\n for (var i = 0; i < allVariants.length; i++) {\n var parsed = dvParseVariantName(allVariants[i].name);\n if (parsed[propKey] === styleValue) toDelete.push(allVariants[i]);\n else wouldKeep++;\n }\n if (toDelete.length === 0) { figma.notify('No variants match style \"' + styleValue + '\"'); return; }\n if (wouldKeep === 0) { figma.notify('Cannot delete — this would empty the component set'); return; }\n\n for (var d = 0; d < toDelete.length; d++) {\n try { toDelete[d].remove(); } catch (e) { console.warn('[Delete] Failed on', toDelete[d].name, e.message); }\n }\n\n // Prune from hidden list\n var hiddenList = getHiddenStyles(cs).filter(function(v) { return v !== styleValue; });\n setHiddenStyles(cs, hiddenList);\n\n figma.notify('Deleted ' + toDelete.length + ' variant' + (toDelete.length === 1 ? '' : 's'));\n\n var wrapper = findExistingWrapper(cs);\n if (wrapper && regenOptions) {\n await generate(regenOptions);\n } else {\n sendSelectionInfo();\n }\n}\n\nfunction hexToRgb(hex) {\n const r = parseInt(hex.slice(1, 3), 16) / 255;\n const g = parseInt(hex.slice(3, 5), 16) / 255;\n const b = parseInt(hex.slice(5, 7), 16) / 255;\n return { r, g, b };\n}\n\nfunction formatLabel(propName, value) {\n return _hidePropertyNames ? value : propName + ': ' + value;\n}\n\nfunction createTitleText(name) {\n var titleText = figma.createText();\n titleText.name = 'Title';\n titleText.characters = name;\n titleText.fontSize = TITLE_FONT_SIZE;\n titleText.fontName = { family: 'Inter', style: 'Semi Bold' };\n titleText.fills = [{ type: 'SOLID', color: DOC_COLOR }];\n titleText.constraints = { horizontal: 'MIN', vertical: 'MIN' };\n return titleText;\n}\n\n// ─── Markdown-to-Figma rich text ─────────────────────────────────────────────\n\n/**\n * Parse a Figma component description (markdown-like) into plain text + style ranges.\n * Supports: **bold**, *italic*, ***bold italic***, `inline code`, [link](url),\n * - bullet lists, 1. numbered lists\n * Returns { text: string, ranges: [{ start, end, bold, italic, code, link }] }\n */\nfunction parseDescriptionMarkdown(raw) {\n var lines = raw.replace(/\\r\\n/g, '\\n').split('\\n');\n var plainParts = [];\n var ranges = [];\n var pos = 0;\n\n for (var li = 0; li < lines.length; li++) {\n var line = lines[li];\n if (li > 0) { plainParts.push('\\n'); pos += 1; }\n\n // Bullet list: \"- text\" or \"• text\" or \"* text\" (but not bold **)\n var bulletMatch = line.match(/^(\\s*)[-•*](?!\\*)\\s+(.*)/);\n if (bulletMatch) {\n var indent = bulletMatch[1] || '';\n plainParts.push(indent + '• ');\n pos += indent.length + 2;\n line = bulletMatch[2];\n }\n\n // Numbered list: \"1. text\"\n var numMatch = line.match(/^(\\s*)(\\d+)\\.\\s+(.*)/);\n if (!bulletMatch && numMatch) {\n var nIndent = numMatch[1] || '';\n var num = numMatch[2];\n plainParts.push(nIndent + num + '. ');\n pos += nIndent.length + num.length + 2;\n line = numMatch[3];\n }\n\n // Parse inline styles within the line\n // Order matters: bold italic (***), bold (**), italic (*), code (`), links\n var i = 0;\n while (i < line.length) {\n // Bold italic: ***text***\n if (line[i] === '*' && line[i+1] === '*' && line[i+2] === '*') {\n var closeBI = line.indexOf('***', i + 3);\n if (closeBI !== -1) {\n var biText = line.substring(i + 3, closeBI);\n ranges.push({ start: pos, end: pos + biText.length, bold: true, italic: true });\n plainParts.push(biText);\n pos += biText.length;\n i = closeBI + 3;\n continue;\n }\n }\n // Bold: **text**\n if (line[i] === '*' && line[i+1] === '*') {\n var closeB = line.indexOf('**', i + 2);\n if (closeB !== -1) {\n var bText = line.substring(i + 2, closeB);\n ranges.push({ start: pos, end: pos + bText.length, bold: true });\n plainParts.push(bText);\n pos += bText.length;\n i = closeB + 2;\n continue;\n }\n }\n // Italic: *text*\n if (line[i] === '*' && line[i+1] !== '*') {\n var closeI = line.indexOf('*', i + 1);\n if (closeI !== -1) {\n var iText = line.substring(i + 1, closeI);\n ranges.push({ start: pos, end: pos + iText.length, italic: true });\n plainParts.push(iText);\n pos += iText.length;\n i = closeI + 1;\n continue;\n }\n }\n // Inline code: `text`\n if (line[i] === '`') {\n var closeC = line.indexOf('`', i + 1);\n if (closeC !== -1) {\n var cText = line.substring(i + 1, closeC);\n ranges.push({ start: pos, end: pos + cText.length, code: true });\n plainParts.push(cText);\n pos += cText.length;\n i = closeC + 1;\n continue;\n }\n }\n // Link: [text](url)\n if (line[i] === '[') {\n var closeBracket = line.indexOf(']', i + 1);\n if (closeBracket !== -1 && line[closeBracket + 1] === '(') {\n var closeParen = line.indexOf(')', closeBracket + 2);\n if (closeParen !== -1) {\n var linkLabel = line.substring(i + 1, closeBracket);\n var linkUrl = line.substring(closeBracket + 2, closeParen);\n ranges.push({ start: pos, end: pos + linkLabel.length, link: linkUrl });\n plainParts.push(linkLabel);\n pos += linkLabel.length;\n i = closeParen + 1;\n continue;\n }\n }\n }\n // Plain character\n plainParts.push(line[i]);\n pos += 1;\n i += 1;\n }\n }\n\n return { text: plainParts.join(''), ranges: ranges };\n}\n\n/**\n * Apply parsed style ranges to a Figma text node.\n * Must be called after setting .characters and base font/size.\n */\nfunction applyRichTextStyles(textNode, ranges) {\n for (var ri = 0; ri < ranges.length; ri++) {\n var r = ranges[ri];\n if (r.start >= r.end) continue;\n\n if (r.bold && r.italic) {\n textNode.setRangeFontName(r.start, r.end, { family: 'Inter', style: 'Bold Italic' });\n } else if (r.bold) {\n textNode.setRangeFontName(r.start, r.end, { family: 'Inter', style: 'Bold' });\n } else if (r.italic) {\n textNode.setRangeFontName(r.start, r.end, { family: 'Inter', style: 'Italic' });\n }\n\n if (r.code) {\n textNode.setRangeFontName(r.start, r.end, { family: 'Inter', style: 'Regular' });\n textNode.setRangeFills(r.start, r.end, [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.8 }]);\n }\n\n if (r.link) {\n textNode.setRangeHyperlink(r.start, r.end, { type: 'URL', value: r.link });\n textNode.setRangeTextDecoration(r.start, r.end, 'UNDERLINE');\n }\n }\n}\n\nfunction createDescriptionFrame(description, maxWidth) {\n var descFrame = figma.createFrame();\n descFrame.name = 'Description';\n descFrame.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.04 }];\n descFrame.cornerRadius = 4;\n descFrame.clipsContent = false;\n descFrame.layoutMode = 'VERTICAL';\n descFrame.paddingTop = DESC_PADDING;\n descFrame.paddingBottom = DESC_PADDING;\n descFrame.paddingLeft = DESC_PADDING;\n descFrame.paddingRight = DESC_PADDING;\n descFrame.primaryAxisSizingMode = 'AUTO';\n descFrame.counterAxisSizingMode = 'FIXED';\n\n var parsed = parseDescriptionMarkdown(description.trim());\n\n var descText = figma.createText();\n descText.characters = parsed.text;\n descText.fontSize = DESC_FONT_SIZE;\n descText.lineHeight = { value: DESC_FONT_SIZE * DESC_LINE_HEIGHT, unit: 'PIXELS' };\n descText.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.6 }];\n\n applyRichTextStyles(descText, parsed.ranges);\n\n descFrame.appendChild(descText);\n descText.layoutSizingHorizontal = 'FILL';\n\n // Set width after children are added so auto-layout height can settle\n descFrame.resize(Math.max(maxWidth, 240), descFrame.height);\n\n descFrame.constraints = { horizontal: 'MIN', vertical: 'MIN' };\n return descFrame;\n}\n\nfunction getDocLink(node) {\n if (node.documentationLinks && node.documentationLinks.length > 0 && node.documentationLinks[0].uri) {\n return node.documentationLinks[0].uri;\n }\n return null;\n}\n\nfunction createDocLinkText(uri) {\n var linkText = figma.createText();\n linkText.name = 'Documentation Link';\n linkText.characters = uri;\n linkText.fontSize = DOC_LINK_FONT_SIZE;\n linkText.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.6 }];\n linkText.hyperlink = { type: 'URL', value: uri };\n linkText.textDecoration = 'UNDERLINE';\n linkText.constraints = { horizontal: 'MIN', vertical: 'MIN' };\n return linkText;\n}\n\n// ─── Plugin Init ─────────────────────────────────────────────────────────────\n\nvar _command = figma.command || 'open';\nvar _labelFontFamily = 'Inter';\n\n// Eagerly load Inter font — always needed as fallback\n/* [autodocs-runtime] init statement removed */\n\nfunction loadLabelFont(family) {\n if (!family || family === 'Inter') {\n _labelFontFamily = 'Inter';\n return Promise.resolve();\n }\n return figma.loadFontAsync({ family: family, style: 'Regular' }).then(function() {\n _labelFontFamily = family;\n console.log('[Fonts] Custom font loaded:', family);\n }).catch(function(e) {\n console.warn('[Fonts] Failed to load \"' + family + '\", falling back to Inter:', e.message);\n _labelFontFamily = 'Inter';\n figma.notify('Font \"' + family + '\" not available — using Inter instead.', { timeout: 4000 });\n });\n}\n\n/* [autodocs-runtime] init statement removed */\n\n/* [autodocs-runtime] init statement removed */\n\n// ─── Deep Variant Selector ────────────────────────────────────────────────────\n\nvar _dvComponentSet = null;\nvar _dvAllVariants = [];\n\nfunction dvParseVariantName(name) {\n var props = {};\n var parts = name.split(',');\n for (var i = 0; i < parts.length; i++) {\n var trimmed = parts[i].trim();\n var equalIndex = trimmed.indexOf('=');\n if (equalIndex > -1) {\n var propName = trimmed.substring(0, equalIndex).trim();\n var propValue = trimmed.substring(equalIndex + 1).trim();\n props[propName] = propValue;\n }\n }\n return props;\n}\n\nfunction dvFindComponentSet(node) {\n if (!node) return null;\n if (node.type === 'COMPONENT_SET') return node;\n if (node.type === 'COMPONENT' && node.parent && node.parent.type === 'COMPONENT_SET') return node.parent;\n var current = node;\n while (current && current.parent) {\n if (current.type === 'COMPONENT' && current.parent.type === 'COMPONENT_SET') return current.parent;\n current = current.parent;\n }\n return null;\n}\n\nfunction dvFindSelectedVariant(node, componentSet) {\n if (!node || !componentSet) return null;\n if (node.type === 'COMPONENT' && node.parent === componentSet) return node;\n var current = node;\n while (current && current.parent) {\n if (current.type === 'COMPONENT' && current.parent === componentSet) return current;\n current = current.parent;\n }\n return null;\n}\n\nfunction dvGetPathFromVariant(node, variant) {\n var path = [];\n var current = node;\n while (current && current !== variant) {\n var parent = current.parent;\n if (!parent || !('children' in parent)) return null;\n var idx = parent.children.indexOf(current);\n if (idx === -1) return null;\n path.unshift(idx);\n current = parent;\n }\n if (current !== variant) return null;\n return path;\n}\n\nfunction dvResolvePathInVariant(variant, path) {\n var node = variant;\n for (var i = 0; i < path.length; i++) {\n if (!node || !('children' in node)) return null;\n var child = node.children[path[i]];\n if (!child) return null;\n node = child;\n }\n return node;\n}\n\nfunction dvAnalyzeSelection(currentSelection, componentSet) {\n var paths = [];\n var seen = {};\n var hasVariantLevel = false;\n for (var i = 0; i < currentSelection.length; i++) {\n var node = currentSelection[i];\n var variant = dvFindSelectedVariant(node, componentSet);\n if (!variant) continue;\n if (node === variant) { hasVariantLevel = true; continue; }\n var path = dvGetPathFromVariant(node, variant);\n if (!path) continue;\n var key = path.join('.');\n if (!seen[key]) { seen[key] = true; paths.push(path); }\n }\n return { paths: paths, hasVariantLevel: hasVariantLevel };\n}\n\nfunction sendDeepVariantInfo() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) {\n // Don't clear cached CS — selection may be empty because UI iframe has focus\n figma.ui.postMessage({ type: 'deep-variant-info', selectedVariantProps: null });\n return;\n }\n\n var cs = dvFindComponentSet(selection[0]);\n if (!cs) {\n _dvComponentSet = null;\n _dvAllVariants = [];\n figma.ui.postMessage({ type: 'deep-variant-info', selectedVariantProps: null });\n return;\n }\n\n _dvComponentSet = cs;\n _dvAllVariants = cs.children.filter(function(child) { return child.type === 'COMPONENT'; });\n\n var selectedVariant = dvFindSelectedVariant(selection[0], cs);\n var selectedProps = selectedVariant ? dvParseVariantName(selectedVariant.name) : null;\n\n // Collect all possible values per property\n var allPropertyValues = {};\n for (var i = 0; i < _dvAllVariants.length; i++) {\n var vProps = dvParseVariantName(_dvAllVariants[i].name);\n var vPropNames = Object.keys(vProps);\n for (var j = 0; j < vPropNames.length; j++) {\n var pName = vPropNames[j];\n if (!allPropertyValues[pName]) allPropertyValues[pName] = [];\n if (allPropertyValues[pName].indexOf(vProps[pName]) === -1) {\n allPropertyValues[pName].push(vProps[pName]);\n }\n }\n }\n\n figma.ui.postMessage({\n type: 'deep-variant-info',\n selectedVariantProps: selectedProps,\n allPropertyValues: allPropertyValues\n });\n}\n\nfunction dvEnsureComponentSet() {\n if (_dvComponentSet && _dvAllVariants.length > 0) return true;\n var selection = figma.currentPage.selection;\n for (var i = 0; i < selection.length; i++) {\n var cs = dvFindComponentSet(selection[i]);\n if (cs) {\n _dvComponentSet = cs;\n _dvAllVariants = cs.children.filter(function(child) { return child.type === 'COMPONENT'; });\n return true;\n }\n }\n return false;\n}\n\nfunction dvVariantMatchesFilters(variant, filters) {\n var props = dvParseVariantName(variant.name);\n var propertyNames = Object.keys(filters);\n for (var i = 0; i < propertyNames.length; i++) {\n var propName = propertyNames[i];\n var allowedValues = filters[propName];\n if (allowedValues.length === 0) continue;\n if (allowedValues.indexOf(props[propName]) === -1) return false;\n }\n return true;\n}\n\nfunction dvSelectVariants(filters, extend) {\n if (!dvEnsureComponentSet()) {\n figma.notify('No component set selected');\n return;\n }\n\n var matchingVariants = _dvAllVariants.filter(function(v) {\n return dvVariantMatchesFilters(v, filters);\n });\n\n if (matchingVariants.length === 0) {\n figma.notify('No variants match the criteria');\n return;\n }\n\n if (!extend) {\n figma.currentPage.selection = matchingVariants;\n figma.notify('Selected ' + matchingVariants.length + ' variant' + (matchingVariants.length === 1 ? '' : 's'));\n return;\n }\n\n var currentSelection = figma.currentPage.selection;\n var analysis = dvAnalyzeSelection(currentSelection, _dvComponentSet);\n var existingIds = {};\n for (var i = 0; i < currentSelection.length; i++) existingIds[currentSelection[i].id] = true;\n\n var newNodes = [];\n\n if (analysis.paths.length > 0) {\n // Sublayer-level selection: pull the same paths from each matching variant.\n for (var j = 0; j < matchingVariants.length; j++) {\n var variant = matchingVariants[j];\n for (var k = 0; k < analysis.paths.length; k++) {\n var resolved = dvResolvePathInVariant(variant, analysis.paths[k]);\n if (resolved && !existingIds[resolved.id]) {\n existingIds[resolved.id] = true;\n newNodes.push(resolved);\n }\n }\n if (analysis.hasVariantLevel && !existingIds[variant.id]) {\n existingIds[variant.id] = true;\n newNodes.push(variant);\n }\n }\n figma.currentPage.selection = currentSelection.concat(newNodes);\n figma.notify('Added ' + newNodes.length + ' layer' + (newNodes.length === 1 ? '' : 's'));\n } else {\n // Variant-level (or empty) selection.\n for (var m = 0; m < matchingVariants.length; m++) {\n if (!existingIds[matchingVariants[m].id]) newNodes.push(matchingVariants[m]);\n }\n figma.currentPage.selection = currentSelection.concat(newNodes);\n figma.notify('Added ' + newNodes.length + ' variant' + (newNodes.length === 1 ? '' : 's'));\n }\n}\n\nfunction dvRemoveFromSelection(filters) {\n var currentSelection = figma.currentPage.selection;\n if (currentSelection.length === 0) {\n figma.notify('Nothing selected');\n return;\n }\n\n dvEnsureComponentSet();\n if (!_dvComponentSet) {\n figma.notify('No component set selected');\n return;\n }\n\n var matchingVariantIds = {};\n _dvAllVariants.forEach(function(variant) {\n if (dvVariantMatchesFilters(variant, filters)) matchingVariantIds[variant.id] = true;\n });\n\n var sawNonVariant = false;\n var newSelection = currentSelection.filter(function(node) {\n var variant = dvFindSelectedVariant(node, _dvComponentSet);\n if (!variant) return true;\n if (node !== variant) sawNonVariant = true;\n return !matchingVariantIds[variant.id];\n });\n\n var removedCount = currentSelection.length - newSelection.length;\n figma.currentPage.selection = newSelection;\n\n if (removedCount > 0) {\n var unit = sawNonVariant ? 'layer' : 'variant';\n figma.notify('Removed ' + removedCount + ' ' + unit + (removedCount === 1 ? '' : 's'));\n } else {\n figma.notify('No matching variants in selection');\n }\n}\n\n// ─── Grid Navigation ─────────────────────────────────────────────────────────\n\nvar NAV_TOLERANCE = 5;\n\nfunction navMoveUp() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select an element first'); return; }\n var parent = selection[0].parent;\n if (!parent || !('children' in parent)) { figma.notify('Selected element has no siblings'); return; }\n\n var centerYPositions = [];\n for (var i = 0; i < selection.length; i++) {\n var cy = selection[i].y + selection[i].height / 2;\n if (centerYPositions.indexOf(cy) === -1) centerYPositions.push(cy);\n }\n var isRow = centerYPositions.length === 1 && selection.length > 1;\n var minCenterY = Infinity;\n for (var i = 0; i < selection.length; i++) {\n var cy = selection[i].y + selection[i].height / 2;\n if (cy < minCenterY) minCenterY = cy;\n }\n\n var aboveYs = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'TEXT') continue;\n var childCY = child.y + child.height / 2;\n if (childCY < minCenterY - NAV_TOLERANCE && aboveYs.indexOf(childCY) === -1) aboveYs.push(childCY);\n }\n aboveYs.sort(function(a, b) { return b - a; });\n if (aboveYs.length === 0) { figma.notify('No elements above'); return; }\n var nextCY = aboveYs[0];\n\n if (isRow) {\n var rowNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.y + child.height / 2 - nextCY) <= NAV_TOLERANCE) rowNodes.push(child);\n }\n figma.currentPage.selection = rowNodes;\n figma.notify('Selected ' + rowNodes.length + ' items in row above');\n } else {\n var targetCX = selection[0].x + selection[0].width / 2;\n var candidates = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.y + child.height / 2 - nextCY) <= NAV_TOLERANCE) candidates.push(child);\n }\n var bestMatch = candidates[0], bestDist = Math.abs(candidates[0].x + candidates[0].width / 2 - targetCX);\n for (var i = 1; i < candidates.length; i++) {\n var dist = Math.abs(candidates[i].x + candidates[i].width / 2 - targetCX);\n if (dist < bestDist) { bestDist = dist; bestMatch = candidates[i]; }\n }\n figma.currentPage.selection = [bestMatch];\n }\n}\n\nfunction navMoveDown() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select an element first'); return; }\n var parent = selection[0].parent;\n if (!parent || !('children' in parent)) { figma.notify('Selected element has no siblings'); return; }\n\n var centerYPositions = [];\n for (var i = 0; i < selection.length; i++) {\n var cy = selection[i].y + selection[i].height / 2;\n if (centerYPositions.indexOf(cy) === -1) centerYPositions.push(cy);\n }\n var isRow = centerYPositions.length === 1 && selection.length > 1;\n var maxCenterY = -Infinity;\n for (var i = 0; i < selection.length; i++) {\n var cy = selection[i].y + selection[i].height / 2;\n if (cy > maxCenterY) maxCenterY = cy;\n }\n\n var belowYs = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'TEXT') continue;\n var childCY = child.y + child.height / 2;\n if (childCY > maxCenterY + NAV_TOLERANCE && belowYs.indexOf(childCY) === -1) belowYs.push(childCY);\n }\n belowYs.sort(function(a, b) { return a - b; });\n if (belowYs.length === 0) { figma.notify('No elements below'); return; }\n var nextCY = belowYs[0];\n\n if (isRow) {\n var rowNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.y + child.height / 2 - nextCY) <= NAV_TOLERANCE) rowNodes.push(child);\n }\n figma.currentPage.selection = rowNodes;\n figma.notify('Selected ' + rowNodes.length + ' items in row below');\n } else {\n var targetCX = selection[0].x + selection[0].width / 2;\n var candidates = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.y + child.height / 2 - nextCY) <= NAV_TOLERANCE) candidates.push(child);\n }\n var bestMatch = candidates[0], bestDist = Math.abs(candidates[0].x + candidates[0].width / 2 - targetCX);\n for (var i = 1; i < candidates.length; i++) {\n var dist = Math.abs(candidates[i].x + candidates[i].width / 2 - targetCX);\n if (dist < bestDist) { bestDist = dist; bestMatch = candidates[i]; }\n }\n figma.currentPage.selection = [bestMatch];\n }\n}\n\nfunction navMoveLeft() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select an element first'); return; }\n var parent = selection[0].parent;\n if (!parent || !('children' in parent)) { figma.notify('Selected element has no siblings'); return; }\n\n var centerXPositions = [];\n for (var i = 0; i < selection.length; i++) {\n var cx = selection[i].x + selection[i].width / 2;\n if (centerXPositions.indexOf(cx) === -1) centerXPositions.push(cx);\n }\n var isColumn = centerXPositions.length === 1 && selection.length > 1;\n var minCenterX = Infinity;\n for (var i = 0; i < selection.length; i++) {\n var cx = selection[i].x + selection[i].width / 2;\n if (cx < minCenterX) minCenterX = cx;\n }\n\n var leftXs = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'TEXT') continue;\n var childCX = child.x + child.width / 2;\n if (childCX < minCenterX - NAV_TOLERANCE && leftXs.indexOf(childCX) === -1) leftXs.push(childCX);\n }\n leftXs.sort(function(a, b) { return b - a; });\n if (leftXs.length === 0) { figma.notify('No elements to the left'); return; }\n var nextCX = leftXs[0];\n\n if (isColumn) {\n var colNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.x + child.width / 2 - nextCX) <= NAV_TOLERANCE) colNodes.push(child);\n }\n figma.currentPage.selection = colNodes;\n figma.notify('Selected ' + colNodes.length + ' items in column to the left');\n } else {\n var targetCY = selection[0].y + selection[0].height / 2;\n var candidates = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.x + child.width / 2 - nextCX) <= NAV_TOLERANCE) candidates.push(child);\n }\n var bestMatch = candidates[0], bestDist = Math.abs(candidates[0].y + candidates[0].height / 2 - targetCY);\n for (var i = 1; i < candidates.length; i++) {\n var dist = Math.abs(candidates[i].y + candidates[i].height / 2 - targetCY);\n if (dist < bestDist) { bestDist = dist; bestMatch = candidates[i]; }\n }\n figma.currentPage.selection = [bestMatch];\n }\n}\n\nfunction navMoveRight() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select an element first'); return; }\n var parent = selection[0].parent;\n if (!parent || !('children' in parent)) { figma.notify('Selected element has no siblings'); return; }\n\n var centerXPositions = [];\n for (var i = 0; i < selection.length; i++) {\n var cx = selection[i].x + selection[i].width / 2;\n if (centerXPositions.indexOf(cx) === -1) centerXPositions.push(cx);\n }\n var isColumn = centerXPositions.length === 1 && selection.length > 1;\n var maxCenterX = -Infinity;\n for (var i = 0; i < selection.length; i++) {\n var cx = selection[i].x + selection[i].width / 2;\n if (cx > maxCenterX) maxCenterX = cx;\n }\n\n var rightXs = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'TEXT') continue;\n var childCX = child.x + child.width / 2;\n if (childCX > maxCenterX + NAV_TOLERANCE && rightXs.indexOf(childCX) === -1) rightXs.push(childCX);\n }\n rightXs.sort(function(a, b) { return a - b; });\n if (rightXs.length === 0) { figma.notify('No elements to the right'); return; }\n var nextCX = rightXs[0];\n\n if (isColumn) {\n var colNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.x + child.width / 2 - nextCX) <= NAV_TOLERANCE) colNodes.push(child);\n }\n figma.currentPage.selection = colNodes;\n figma.notify('Selected ' + colNodes.length + ' items in column to the right');\n } else {\n var targetCY = selection[0].y + selection[0].height / 2;\n var candidates = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.x + child.width / 2 - nextCX) <= NAV_TOLERANCE) candidates.push(child);\n }\n var bestMatch = candidates[0], bestDist = Math.abs(candidates[0].y + candidates[0].height / 2 - targetCY);\n for (var i = 1; i < candidates.length; i++) {\n var dist = Math.abs(candidates[i].y + candidates[i].height / 2 - targetCY);\n if (dist < bestDist) { bestDist = dist; bestMatch = candidates[i]; }\n }\n figma.currentPage.selection = [bestMatch];\n }\n}\n\nfunction navSelectRow() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select an element first'); return; }\n var selected = selection[0];\n var parent = selected.parent;\n if (!parent || !('children' in parent)) { figma.notify('Selected element has no siblings'); return; }\n var targetCY = selected.y + selected.height / 2;\n var rowNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.y + child.height / 2 - targetCY) <= NAV_TOLERANCE) rowNodes.push(child);\n }\n figma.currentPage.selection = rowNodes;\n figma.notify('Selected ' + rowNodes.length + ' items in row');\n}\n\nfunction navSelectColumn() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select an element first'); return; }\n var selected = selection[0];\n var parent = selected.parent;\n if (!parent || !('children' in parent)) { figma.notify('Selected element has no siblings'); return; }\n var targetCX = selected.x + selected.width / 2;\n var colNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.x + child.width / 2 - targetCX) <= NAV_TOLERANCE) colNodes.push(child);\n }\n figma.currentPage.selection = colNodes;\n figma.notify('Selected ' + colNodes.length + ' items in column');\n}\n\nfunction navAddNextRow() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select elements first'); return; }\n var parent = selection[0].parent;\n if (!parent || !('children' in parent)) { figma.notify('Cannot find parent container'); return; }\n var maxCenterY = -Infinity;\n for (var i = 0; i < selection.length; i++) {\n var cy = selection[i].y + selection[i].height / 2;\n if (cy > maxCenterY) maxCenterY = cy;\n }\n var allYs = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'TEXT') continue;\n var cy = child.y + child.height / 2;\n if (allYs.indexOf(cy) === -1) allYs.push(cy);\n }\n allYs.sort(function(a, b) { return a - b; });\n var nextCY = null;\n for (var i = 0; i < allYs.length; i++) {\n if (allYs[i] > maxCenterY + NAV_TOLERANCE) { nextCY = allYs[i]; break; }\n }\n if (nextCY === null) { figma.notify('No more rows below'); return; }\n var nextRowNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.y + child.height / 2 - nextCY) <= NAV_TOLERANCE) nextRowNodes.push(child);\n }\n var newSelection = selection.slice();\n for (var i = 0; i < nextRowNodes.length; i++) newSelection.push(nextRowNodes[i]);\n figma.currentPage.selection = newSelection;\n figma.notify('Added ' + nextRowNodes.length + ' items from next row');\n}\n\nfunction navAddNextColumn() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select elements first'); return; }\n var parent = selection[0].parent;\n if (!parent || !('children' in parent)) { figma.notify('Cannot find parent container'); return; }\n var maxCenterX = -Infinity;\n for (var i = 0; i < selection.length; i++) {\n var cx = selection[i].x + selection[i].width / 2;\n if (cx > maxCenterX) maxCenterX = cx;\n }\n var allXs = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'TEXT') continue;\n var cx = child.x + child.width / 2;\n if (allXs.indexOf(cx) === -1) allXs.push(cx);\n }\n allXs.sort(function(a, b) { return a - b; });\n var nextCX = null;\n for (var i = 0; i < allXs.length; i++) {\n if (allXs[i] > maxCenterX + NAV_TOLERANCE) { nextCX = allXs[i]; break; }\n }\n if (nextCX === null) { figma.notify('No more columns to the right'); return; }\n var nextColNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.x + child.width / 2 - nextCX) <= NAV_TOLERANCE) nextColNodes.push(child);\n }\n var newSelection = selection.slice();\n for (var i = 0; i < nextColNodes.length; i++) newSelection.push(nextColNodes[i]);\n figma.currentPage.selection = newSelection;\n figma.notify('Added ' + nextColNodes.length + ' items from next column');\n}\n\nfunction navAddPrevRow() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select elements first'); return; }\n var parent = selection[0].parent;\n if (!parent || !('children' in parent)) { figma.notify('Cannot find parent container'); return; }\n var minCenterY = Infinity;\n for (var i = 0; i < selection.length; i++) {\n var cy = selection[i].y + selection[i].height / 2;\n if (cy < minCenterY) minCenterY = cy;\n }\n var allYs = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'TEXT') continue;\n var cy = child.y + child.height / 2;\n if (allYs.indexOf(cy) === -1) allYs.push(cy);\n }\n allYs.sort(function(a, b) { return b - a; });\n var prevCY = null;\n for (var i = 0; i < allYs.length; i++) {\n if (allYs[i] < minCenterY - NAV_TOLERANCE) { prevCY = allYs[i]; break; }\n }\n if (prevCY === null) { figma.notify('No more rows above'); return; }\n var prevRowNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.y + child.height / 2 - prevCY) <= NAV_TOLERANCE) prevRowNodes.push(child);\n }\n var newSelection = selection.slice();\n for (var i = 0; i < prevRowNodes.length; i++) newSelection.push(prevRowNodes[i]);\n figma.currentPage.selection = newSelection;\n figma.notify('Added ' + prevRowNodes.length + ' items from previous row');\n}\n\nfunction navAddPrevColumn() {\n var selection = figma.currentPage.selection;\n if (selection.length === 0) { figma.notify('Please select elements first'); return; }\n var parent = selection[0].parent;\n if (!parent || !('children' in parent)) { figma.notify('Cannot find parent container'); return; }\n var minCenterX = Infinity;\n for (var i = 0; i < selection.length; i++) {\n var cx = selection[i].x + selection[i].width / 2;\n if (cx < minCenterX) minCenterX = cx;\n }\n var allXs = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'TEXT') continue;\n var cx = child.x + child.width / 2;\n if (allXs.indexOf(cx) === -1) allXs.push(cx);\n }\n allXs.sort(function(a, b) { return b - a; });\n var prevCX = null;\n for (var i = 0; i < allXs.length; i++) {\n if (allXs[i] < minCenterX - NAV_TOLERANCE) { prevCX = allXs[i]; break; }\n }\n if (prevCX === null) { figma.notify('No more columns to the left'); return; }\n var prevColNodes = [];\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type !== 'TEXT' && Math.abs(child.x + child.width / 2 - prevCX) <= NAV_TOLERANCE) prevColNodes.push(child);\n }\n var newSelection = selection.slice();\n for (var i = 0; i < prevColNodes.length; i++) newSelection.push(prevColNodes[i]);\n figma.currentPage.selection = newSelection;\n figma.notify('Added ' + prevColNodes.length + ' items from previous column');\n}\n\n// ─── Selection Detection (Step 2) ────────────────────────────────────────────\n\nfunction getComponentSet() {\n const sel = figma.currentPage.selection;\n if (sel.length !== 1) {\n return null;\n }\n const node = sel[0];\n if (node.type === 'COMPONENT_SET') {\n return node;\n }\n if (node.type === 'COMPONENT' && node.parent && node.parent.type === 'COMPONENT_SET') {\n return node.parent;\n }\n // Walk up the tree to find a wrapper frame, then find the component set inside it\n let current = node;\n while (current) {\n if (current.type === 'FRAME') {\n // Check for ❖ wrapper or GusPropstar wrapper (has \"labels\" GROUP or \"instances\" FRAME)\n var isWrapper = current.name.startsWith('❖');\n if (!isWrapper) {\n for (const ch of current.children) {\n if (ch.type === 'GROUP' && ch.name.toLowerCase() === 'labels') { isWrapper = true; break; }\n if (ch.type === 'FRAME' && ch.name.toLowerCase() === 'instances') { isWrapper = true; break; }\n }\n }\n if (isWrapper) {\n // Found a wrapper — look for the component set child\n for (const child of current.children) {\n if (child.type === 'COMPONENT_SET') {\n return child;\n }\n }\n }\n }\n current = current.parent;\n }\n return null;\n}\n\nfunction isStandaloneComponent(node) {\n return node.type === 'COMPONENT' && (!node.parent || node.parent.type !== 'COMPONENT_SET');\n}\n\nfunction getStandaloneComponent() {\n var sel = figma.currentPage.selection;\n if (sel.length !== 1) return null;\n var node = sel[0];\n\n // Direct standalone component selection\n if (isStandaloneComponent(node)) {\n return node;\n }\n\n // Walk up to find a ❖ wrapper containing a standalone component\n var current = node;\n while (current) {\n if (current.type === 'FRAME' && current.name.startsWith('❖')) {\n for (var i = 0; i < current.children.length; i++) {\n if (isStandaloneComponent(current.children[i])) {\n return current.children[i];\n }\n }\n }\n current = current.parent;\n }\n return null;\n}\n\nasync function sendSelectionInfo() {\n var gen = ++_selectionGeneration;\n var _t0 = Date.now();\n try {\n const cs = getComponentSet();\n if (!cs) {\n // Try standalone component\n var standalone = getStandaloneComponent();\n if (!standalone) {\n figma.ui.postMessage({ type: 'selection', componentSet: null });\n return;\n }\n\n var saWrapper = findExistingWrapper(standalone);\n var saBoolProps = getBooleanComponentProperties(standalone);\n var saNestedSets = await findNestedComponentSetsForComponent(standalone);\n if (gen !== _selectionGeneration) return; // stale, newer selection in progress\n var saNestedInfo = saNestedSets.map(function(ns) {\n var propValues = [];\n if (ns.variantGroupProperties) {\n for (var pk in ns.variantGroupProperties) {\n if (ns.variantGroupProperties.hasOwnProperty(pk)) {\n propValues.push(ns.variantGroupProperties[pk].length);\n }\n }\n }\n return { name: ns.componentSetName, propertyValues: propValues };\n });\n\n figma.ui.postMessage({\n type: 'selection',\n componentSet: standalone.name,\n isStandalone: true,\n properties: [],\n hasWrapper: !!saWrapper,\n hasGrid: false,\n hasGusPropstar: false,\n hasBooleanProps: saBoolProps.length > 0,\n booleanPropNames: saBoolProps.map(function(p) { return p.name; }),\n variantCount: 1,\n hasNestedInstances: saNestedSets.length > 0,\n nestedInstancesInfo: saNestedInfo,\n gridWarning: null,\n });\n return;\n }\n const enumProps = getEnumProperties(cs);\n var wrapper = findExistingWrapper(cs);\n var standaloneWrapper = findStandaloneWrapper(cs);\n var variableModesWrapper = findVariableModesWrapper(cs);\n var effectiveWrapper = wrapper || standaloneWrapper;\n\n // Check grid uniformity — only warn for very sparse grids (>50% empty cells).\n // Many component sets intentionally have empty cells for variant combinations\n // that don't exist (e.g. icon-only buttons don't have text style variants).\n var gridWarning = null;\n var variants = cs.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n if (variants.length > 1) {\n var colAxis = clusterVariantAxis(variants, 'x', 'width');\n var rowAxis = clusterVariantAxis(variants, 'y', 'height');\n var cols = colAxis.clusters.length;\n var rows = rowAxis.clusters.length;\n var expected = cols * rows;\n var fillRate = variants.length / expected;\n if (variants.length !== expected && fillRate < 0.5) {\n gridWarning = variants.length + ' variants in a ' + cols + '\\u00d7' + rows + ' grid (' + (expected - variants.length) + ' empty cells)';\n }\n }\n\n // Detect single-type grid (one property spread across multi-row × multi-col grid)\n // Use raw position clustering (clusterValues) instead of clusterVariantAxis, because\n // clusterVariantAxis merges overlapping bounding boxes — a spanning variant that covers\n // 2 columns would collapse them into 1, preventing detection.\n var isSingleTypeGrid = false;\n if (enumProps.length === 1 && variants.length > 1) {\n var stCols = clusterValues(variants.map(function(v) { return v.x; })).clusters.length;\n var stRows = clusterValues(variants.map(function(v) { return v.y; })).clusters.length;\n if (stCols > 1 && stRows > 1) {\n isSingleTypeGrid = true;\n }\n }\n\n // Check for absolute-positioned overflow (e.g. dropdown menus)\n var overflowInfo = detectComponentSetOverflow(cs);\n\n var boolProps = getBooleanComponentProperties(cs);\n\n // Check for grid inside whichever wrapper exists\n var hasGrid = false;\n if (effectiveWrapper) {\n // Grid may be inside a Documentation frame or directly in wrapper\n hasGrid = effectiveWrapper.children.some(function(c) {\n if (c.type === 'FRAME' && c.name === 'Grid') return true;\n if (c.type === 'FRAME' && c.name === 'Documentation') {\n return c.children.some(function(dc) { return dc.type === 'FRAME' && dc.name === 'Grid'; });\n }\n return false;\n });\n }\n\n // Derive existing doc options from layer names in the wrapper\n var existingDocMeta = null;\n if (effectiveWrapper) {\n existingDocMeta = deriveDocMeta(effectiveWrapper);\n }\n\n // Read saved per-component settings (if any)\n var savedSettings = getComponentSettings(cs);\n\n // Detect Shadcn style property (for the hide/delete-by-style panel)\n var shadcnStyleInfo = getShadcnStyleInfo(cs);\n\n // Detect variable collections and modes\n var variableCollections = [];\n try {\n var allCollections = await figma.variables.getLocalVariableCollectionsAsync();\n variableCollections = allCollections\n .filter(function(c) { return c.modes.length > 1; })\n .map(function(c) { return { id: c.id, name: c.name, modes: c.modes.map(function(m) { return { modeId: m.modeId, name: m.name }; }) }; });\n } catch(e) {\n console.warn('[Variables] Could not read variable collections:', e.message);\n }\n\n // Send selection info immediately (without nested instance data)\n figma.ui.postMessage({\n type: 'selection',\n componentSet: cs.name,\n properties: enumProps.map(p => ({ name: p.name, values: p.values })),\n hasWrapper: !!wrapper,\n hasStandaloneWrapper: !!standaloneWrapper,\n hasVariableModesWrapper: !!variableModesWrapper,\n hasGrid: hasGrid,\n hasGusPropstar: !!detectGusPropstar(cs),\n hasBooleanProps: boolProps.length > 0,\n booleanPropNames: boolProps.map(function(p) { return p.name; }),\n variantCount: variants.length,\n hasNestedInstances: false,\n nestedInstancesInfo: [],\n gridWarning: gridWarning,\n hasOverflow: overflowInfo.hasOverflow,\n isSingleTypeGrid: isSingleTypeGrid,\n variableCollections: variableCollections,\n existingDocMeta: existingDocMeta,\n savedSettings: savedSettings,\n shadcnStyleInfo: shadcnStyleInfo,\n });\n\n // Async: check for nested component instances, then update UI if still current\n var nestedSets = await findNestedComponentSets(cs);\n if (gen !== _selectionGeneration) return; // stale, newer selection in progress\n\n if (nestedSets.length > 0) {\n var nestedInfo = nestedSets.map(function(ns) {\n var propValues = [];\n if (ns.variantGroupProperties) {\n for (var pk in ns.variantGroupProperties) {\n if (ns.variantGroupProperties.hasOwnProperty(pk)) {\n propValues.push(ns.variantGroupProperties[pk].length);\n }\n }\n }\n var parentVariants = null;\n if (ns.parentVariantIndices && ns.parentVariantIndices.length > 0) {\n parentVariants = ns.parentVariantIndices.map(function(idx) {\n return { index: idx, name: variants[idx] ? variants[idx].name : 'Variant ' + idx };\n });\n }\n return { name: ns.componentSetName, propertyValues: propValues, parentVariants: parentVariants };\n });\n\n // Update with nested instance data\n figma.ui.postMessage({\n type: 'selection',\n componentSet: cs.name,\n properties: enumProps.map(p => ({ name: p.name, values: p.values })),\n hasWrapper: !!wrapper,\n hasStandaloneWrapper: !!standaloneWrapper,\n hasGrid: hasGrid,\n hasGusPropstar: !!detectGusPropstar(cs),\n hasBooleanProps: boolProps.length > 0,\n booleanPropNames: boolProps.map(function(p) { return p.name; }),\n variantCount: variants.length,\n hasNestedInstances: true,\n nestedInstancesInfo: nestedInfo,\n gridWarning: gridWarning,\n hasOverflow: overflowInfo.hasOverflow,\n isSingleTypeGrid: isSingleTypeGrid,\n variableCollections: variableCollections,\n existingDocMeta: existingDocMeta,\n savedSettings: savedSettings,\n shadcnStyleInfo: shadcnStyleInfo,\n });\n }\n\n if (DEBUG) console.log('[Perf] sendSelectionInfo: ' + (Date.now() - _t0) + 'ms (' + variants.length + ' variants)');\n } catch (e) {\n console.error('[sendSelectionInfo] Error:', e.message, e.stack);\n figma.ui.postMessage({ type: 'selection', componentSet: null });\n }\n}\n\n// ─── Property Extraction & Filtering (Step 3) ────────────────────────────────\n\nfunction isBooleanProperty(values) {\n if (values.length !== 2) return false;\n const sorted = values.map(v => v.toLowerCase()).sort();\n const boolPairs = [['false', 'true'], ['no', 'yes'], ['off', 'on']];\n return boolPairs.some(pair => sorted[0] === pair[0] && sorted[1] === pair[1]);\n}\n\nfunction isNestedProperty(name) {\n return name.includes('/');\n}\n\nfunction getEnumProperties(componentSet, options = {}) {\n const groupProps = componentSet.variantGroupProperties;\n const result = [];\n\n // Values that still have at least one visible variant. Used to drop values\n // whose variants are all hidden via the Shadcn style filter.\n let visibleValuesByProp = null;\n const visibleVariants = componentSet.children.filter(function(c) {\n return c.type === 'COMPONENT' && c.visible !== false;\n });\n const allVariants = componentSet.children.filter(function(c) { return c.type === 'COMPONENT'; });\n if (visibleVariants.length !== allVariants.length) {\n visibleValuesByProp = {};\n for (let i = 0; i < visibleVariants.length; i++) {\n const parsed = dvParseVariantName(visibleVariants[i].name);\n for (const pname in parsed) {\n if (!visibleValuesByProp[pname]) visibleValuesByProp[pname] = {};\n visibleValuesByProp[pname][parsed[pname]] = true;\n }\n }\n }\n\n for (const [name, info] of Object.entries(groupProps)) {\n if (isNestedProperty(name)) continue;\n let values = info.values;\n if (visibleValuesByProp) {\n const seen = visibleValuesByProp[name] || {};\n values = values.filter(function(v) { return seen[v]; });\n if (values.length === 0) continue;\n }\n result.push({ name, values });\n }\n return result;\n}\n\n// ─── Boolean Component Properties ────────────────────────────────────────────\n\nfunction getBooleanComponentProperties(componentSet) {\n var propDefs = componentSet.componentPropertyDefinitions;\n if (!propDefs) return [];\n var result = [];\n for (var key in propDefs) {\n if (propDefs.hasOwnProperty(key) && propDefs[key].type === 'BOOLEAN') {\n result.push({ key: key, name: key.replace(/#.*$/, ''), defaultValue: propDefs[key].defaultValue });\n }\n }\n return result;\n}\n\nfunction determineBooleanAxis(csWidth, csHeight) {\n return csWidth >= csHeight ? 'row' : 'col';\n}\n\nfunction createBackgroundTint(x, y, width, height) {\n var rect = figma.createRectangle();\n rect.name = 'Boolean Tint';\n rect.x = x;\n rect.y = y;\n rect.resize(width, height);\n rect.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.03 }];\n rect.locked = true;\n return rect;\n}\n\nfunction createBooleanInstanceGroup(sourceNode, propsToSet, offsetX, offsetY) {\n var standalone = isStandaloneComponent(sourceNode);\n var variants = standalone ? [sourceNode] : sourceNode.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n var instances = [];\n var maxExtentX = 0;\n var maxExtentY = 0;\n for (var i = 0; i < variants.length; i++) {\n try {\n var variant = variants[i];\n var instance = variant.createInstance();\n instance.setProperties(propsToSet);\n var relX = standalone ? 0 : variant.x;\n var relY = standalone ? 0 : variant.y;\n instance.x = offsetX + relX;\n instance.y = offsetY + relY;\n var extX = relX + instance.width;\n var extY = relY + instance.height;\n if (extX > maxExtentX) maxExtentX = extX;\n if (extY > maxExtentY) maxExtentY = extY;\n instances.push(instance);\n } catch (e) {\n console.log('[Boolean Grid] Error creating instance:', e.message);\n }\n }\n return { instances: instances, width: maxExtentX, height: maxExtentY };\n}\n\n// ─── Nested Instance Detection ───────────────────────────────────────────────\n\n// Recursively find all INSTANCE nodes inside a node\nfunction findInstanceNodes(node) {\n var result = [];\n if (!('children' in node)) return result;\n for (var i = 0; i < node.children.length; i++) {\n var child = node.children[i];\n if (child.type === 'INSTANCE') {\n result.push(child);\n }\n result = result.concat(findInstanceNodes(child));\n }\n return result;\n}\n\n// Shared helper: resolves exposed instance nodes into nested component set results.\n// Used by both findNestedComponentSets and findNestedComponentSetsForComponent.\nasync function resolveNestedComponentSets(exposed, debugLabel, variantIndicesByName) {\n var seen = {};\n var result = [];\n\n for (var i = 0; i < exposed.length; i++) {\n var inst = exposed[i];\n // Skip instances whose layer name starts with '.' (local/private to the component)\n if (inst.name.charAt(0) === '.') continue;\n var mc;\n try {\n mc = await Promise.race([\n inst.getMainComponentAsync(),\n new Promise(function(_, reject) { setTimeout(function() { reject(new Error('timeout')); }, 2000); })\n ]);\n } catch (e) {\n console.log('[NestedInstances] getMainComponentAsync timed out for \"' + inst.name + '\"');\n continue;\n }\n if (!mc) continue;\n if (mc.parent && mc.parent.type === 'COMPONENT_SET') {\n var nestedCS = mc.parent;\n if (seen[nestedCS.id]) continue;\n // Skip component sets whose name starts with '.' (local/private)\n if (nestedCS.name.charAt(0) === '.') { seen[nestedCS.id] = true; continue; }\n seen[nestedCS.id] = true;\n\n var nestedVariants = nestedCS.children.filter(function(c) { return c.type === 'COMPONENT'; });\n if (nestedVariants.length <= 1) continue;\n\n var nestedGroupProps = nestedCS.variantGroupProperties;\n var groupPropData = {};\n for (var propName in nestedGroupProps) {\n if (nestedGroupProps.hasOwnProperty(propName)) {\n groupPropData[propName] = nestedGroupProps[propName].values;\n }\n }\n\n var defaultVariant = nestedVariants.filter(function(v) { return v.id === mc.id; })[0];\n var defaultProps = defaultVariant ? defaultVariant.variantProperties : (nestedVariants[0] ? nestedVariants[0].variantProperties : {});\n\n result.push({\n componentSetId: nestedCS.id,\n componentSetName: nestedCS.name,\n instanceName: inst.name,\n currentVariantId: mc.id,\n variants: nestedVariants.map(function(v) { return { id: v.id, name: v.name }; }),\n variantGroupProperties: groupPropData,\n defaultVariantProperties: defaultProps,\n parentVariantIndices: (variantIndicesByName && variantIndicesByName[inst.name]) || null\n });\n }\n }\n\n if (DEBUG) {\n console.log('[NestedInstances] Found', result.length, 'nested component sets in', debugLabel);\n for (var j = 0; j < result.length; j++) {\n console.log('[NestedInstances] \"' + result[j].componentSetName + '\" (' + result[j].variants.length + ' variants), instance layer: \"' + result[j].instanceName + '\", parent variant indices:', result[j].parentVariantIndices);\n }\n }\n\n return result;\n}\n\n// Filter exposed instances to only top-level ones (not nested inside other instances)\nfunction filterTopLevelExposed(exposed, rootNode) {\n return exposed.filter(function(inst) {\n var node = inst.parent;\n while (node && node !== rootNode) {\n if (node.type === 'INSTANCE') {\n return false;\n }\n node = node.parent;\n }\n return true;\n });\n}\n\n// Find nested component sets by scanning for instances with isExposedInstance === true.\n// Only includes instances the designer explicitly exposed in the component set.\nasync function findNestedComponentSets(componentSet) {\n var variants = componentSet.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n if (variants.length === 0) return [];\n\n // Scan all variants for exposed instances (some may only exist in certain variants)\n // Track which parent variant indices contain each exposed instance\n var exposedByComponent = {};\n var variantIndicesByName = {};\n for (var vi = 0; vi < variants.length; vi++) {\n var allInstances = findInstanceNodes(variants[vi]);\n var variantExposed = allInstances.filter(function(inst) { return inst.isExposedInstance === true; });\n variantExposed = filterTopLevelExposed(variantExposed, variants[vi]);\n // Skip instances whose layer name starts with '.' (local/private to the component)\n variantExposed = variantExposed.filter(function(inst) { return inst.name.charAt(0) !== '.'; });\n\n var seenInVariant = {};\n for (var ei = 0; ei < variantExposed.length; ei++) {\n var key = variantExposed[ei].name;\n if (!exposedByComponent[key]) {\n exposedByComponent[key] = variantExposed[ei];\n variantIndicesByName[key] = [];\n }\n if (!seenInVariant[key]) {\n variantIndicesByName[key].push(vi);\n seenInVariant[key] = true;\n }\n }\n }\n var exposed = [];\n for (var ek in exposedByComponent) {\n if (exposedByComponent.hasOwnProperty(ek)) {\n exposed.push(exposedByComponent[ek]);\n }\n }\n if (DEBUG) { console.log('[NestedInstances] top-level exposed across all variants:', exposed.length); }\n\n return resolveNestedComponentSets(exposed, componentSet.name, variantIndicesByName);\n}\n\n// Variant of findNestedComponentSets for a standalone component (not a component set)\nasync function findNestedComponentSetsForComponent(component) {\n var allInstances = findInstanceNodes(component);\n var exposed = allInstances.filter(function(inst) { return inst.isExposedInstance === true; });\n exposed = filterTopLevelExposed(exposed, component);\n // Skip instances whose layer name starts with '.' (local/private to the component)\n exposed = exposed.filter(function(inst) { return inst.name.charAt(0) !== '.'; });\n\n if (DEBUG) { console.log('[NestedInstances] top-level exposed in standalone:', exposed.length); }\n\n return resolveNestedComponentSets(exposed, 'standalone ' + component.name);\n}\n\n// Find a variant in a list by matching target property values against the variant name\nfunction findVariantByProperties(variants, targetProps) {\n for (var i = 0; i < variants.length; i++) {\n var v = variants[i];\n var props = {};\n var parts = v.name.split(',');\n for (var j = 0; j < parts.length; j++) {\n var eq = parts[j].indexOf('=');\n if (eq > 0) {\n props[parts[j].substring(0, eq).trim()] = parts[j].substring(eq + 1).trim();\n }\n }\n var match = true;\n for (var key in targetProps) {\n if (targetProps.hasOwnProperty(key) && props[key] !== targetProps[key]) {\n match = false;\n break;\n }\n }\n if (match) return v;\n }\n return null;\n}\n\n// ─── Layout Analysis (Step 4) ────────────────────────────────────────────────\n\nfunction clusterValues(positions) {\n var sorted = Array.from(new Set(positions)).sort(function(a, b) { return a - b; });\n var clusters = [];\n var highs = []; // track highest value in each cluster for chain comparison\n for (var i = 0; i < sorted.length; i++) {\n // Chain comparison: compare against the LAST (highest) value in the current\n // cluster, not the first. This prevents splitting when values gradually drift\n // across a range wider than CLUSTER_TOLERANCE (e.g. mixed-size variant centers).\n if (clusters.length === 0 || sorted[i] - highs[highs.length - 1] > CLUSTER_TOLERANCE) {\n clusters.push(sorted[i]);\n highs.push(sorted[i]);\n } else {\n highs[highs.length - 1] = sorted[i];\n }\n }\n return { clusters: clusters, highs: highs };\n}\n\n// Cluster variants into columns/rows using smart alignment detection.\n// Detect variants with absolutely positioned children that overflow the frame.\n// Returns { hasOverflow, overflowBottom, overflowRight } for a single variant.\nfunction detectVariantOverflow(variant) {\n var overflowBottom = 0;\n var overflowRight = 0;\n function scanChildren(node, depth) {\n if (depth > 3) return; // limit recursion depth\n if (!node.children) return;\n for (var i = 0; i < node.children.length; i++) {\n var child = node.children[i];\n if (child.layoutPositioning === 'ABSOLUTE') {\n var childBottom = child.y + child.height - variant.height;\n var childRight = child.x + child.width - variant.width;\n if (childBottom > overflowBottom) overflowBottom = childBottom;\n if (childRight > overflowRight) overflowRight = childRight;\n }\n scanChildren(child, depth + 1);\n }\n }\n scanChildren(variant, 0);\n return {\n hasOverflow: overflowBottom > 20 || overflowRight > 20,\n overflowBottom: overflowBottom,\n overflowRight: overflowRight\n };\n}\n\n// Detect overflow across all variants in a component set.\n// Returns { hasOverflow, maxOverflowBottom, maxOverflowRight, affectedVariants[] }\nfunction detectComponentSetOverflow(componentSet) {\n var variants = componentSet.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n var maxBottom = 0, maxRight = 0;\n var affected = [];\n for (var i = 0; i < variants.length; i++) {\n var ov = detectVariantOverflow(variants[i]);\n if (ov.hasOverflow) {\n affected.push({ name: variants[i].name, overflowBottom: ov.overflowBottom, overflowRight: ov.overflowRight });\n if (ov.overflowBottom > maxBottom) maxBottom = ov.overflowBottom;\n if (ov.overflowRight > maxRight) maxRight = ov.overflowRight;\n }\n }\n if (affected.length > 0 && DEBUG) {\n console.log('[Overflow] Detected ' + affected.length + ' variant(s) with absolutely positioned overflow:');\n for (var j = 0; j < affected.length; j++) {\n console.log('[Overflow] \"' + affected[j].name + '\" → bottom:+' + Math.round(affected[j].overflowBottom) + 'px right:+' + Math.round(affected[j].overflowRight) + 'px');\n }\n }\n return {\n hasOverflow: affected.length > 0,\n maxOverflowBottom: maxBottom,\n maxOverflowRight: maxRight,\n affectedVariants: affected\n };\n}\n\n// Tries left-edge, center, and right-edge clustering; picks the one that\n// produces fewest clusters (= the common edge variants share).\n// Returns { clusters: [refValue, ...], mins: [minEdge, ...], maxes: [maxEdge, ...], mode: 'left'|'center'|'right' }\nfunction effectiveSize(v, sizeKey) {\n if (_visualSizeOverrides && _visualSizeOverrides[v.id]) {\n return Math.max(_visualSizeOverrides[v.id][sizeKey], 1);\n }\n return Math.max(v[sizeKey], 1);\n}\n\nfunction clusterVariantAxis(variants, posKey, sizeKey) {\n var lefts = variants.map(function(v) { return v[posKey]; });\n var centers = variants.map(function(v) { return v[posKey] + effectiveSize(v, sizeKey) / 2; });\n var rights = variants.map(function(v) { return v[posKey] + effectiveSize(v, sizeKey); });\n\n var leftResult = clusterValues(lefts);\n var centerResult = clusterValues(centers);\n var rightResult = clusterValues(rights);\n\n // Pick alignment with fewest clusters (= best common edge).\n // On tie, prefer left (most common Figma default) > center > right.\n var best = 'left', bestClusters = leftResult.clusters, bestHighs = leftResult.highs, bestRefs = lefts;\n if (centerResult.clusters.length < bestClusters.length) {\n best = 'center'; bestClusters = centerResult.clusters; bestHighs = centerResult.highs; bestRefs = centers;\n }\n if (rightResult.clusters.length < bestClusters.length) {\n best = 'right'; bestClusters = rightResult.clusters; bestHighs = rightResult.highs; bestRefs = rights;\n }\n\n if (DEBUG) {\n console.log('[Cluster] ' + posKey + ' axis: left=' + leftResult.clusters.length + ' center=' + centerResult.clusters.length + ' right=' + rightResult.clusters.length + ' → using ' + best);\n }\n\n // For each cluster, compute the bounding box from actual variant edges.\n // Use the full chain range [cluster, high] to capture all chained members.\n var mins = [];\n var maxes = [];\n for (var i = 0; i < bestClusters.length; i++) {\n var minEdge = Infinity;\n var maxEdge = -Infinity;\n for (var j = 0; j < variants.length; j++) {\n if (bestRefs[j] >= bestClusters[i] - CLUSTER_TOLERANCE && bestRefs[j] <= bestHighs[i] + CLUSTER_TOLERANCE) {\n var left = variants[j][posKey];\n var right = left + effectiveSize(variants[j], sizeKey);\n if (left < minEdge) minEdge = left;\n if (right > maxEdge) maxEdge = right;\n }\n }\n mins.push(minEdge);\n maxes.push(maxEdge);\n }\n\n // Merge adjacent clusters whose bounding boxes overlap. This handles mixed-size\n // variants (e.g. 40px icons + 120px buttons in the same column) where even\n // chain-based clustering can't bridge the center gap but the variants clearly\n // occupy the same physical column.\n var didMerge = true;\n while (didMerge) {\n didMerge = false;\n for (var mi = 0; mi < bestClusters.length - 1; mi++) {\n if (maxes[mi] >= mins[mi + 1]) {\n if (DEBUG) {\n console.log('[Cluster] Merging overlapping clusters ' + mi + ' (ref=' + bestClusters[mi] +\n ', bounds=[' + mins[mi] + ',' + maxes[mi] + ']) and ' + (mi + 1) + ' (ref=' + bestClusters[mi + 1] +\n ', bounds=[' + mins[mi + 1] + ',' + maxes[mi + 1] + '])');\n }\n mins[mi] = Math.min(mins[mi], mins[mi + 1]);\n maxes[mi] = Math.max(maxes[mi], maxes[mi + 1]);\n bestClusters.splice(mi + 1, 1);\n mins.splice(mi + 1, 1);\n maxes.splice(mi + 1, 1);\n didMerge = true;\n break;\n }\n }\n }\n\n // After merging, recompute bounding boxes from scratch and compute per-cluster\n // tolerances. Merged clusters may span wider than CLUSTER_TOLERANCE, so\n // findClusterIndex needs per-cluster tolerances to match all member variants.\n var tols = [];\n for (var ti = 0; ti < bestClusters.length; ti++) {\n var tMinEdge = Infinity;\n var tMaxEdge = -Infinity;\n var maxDist = 0;\n for (var tj = 0; tj < variants.length; tj++) {\n var vLeft = variants[tj][posKey];\n var vRight = vLeft + effectiveSize(variants[tj], sizeKey);\n // Variant belongs to this cluster if its position range overlaps the bounding box\n if (vLeft < maxes[ti] + CLUSTER_TOLERANCE && vRight > mins[ti] - CLUSTER_TOLERANCE) {\n if (vLeft < tMinEdge) tMinEdge = vLeft;\n if (vRight > tMaxEdge) tMaxEdge = vRight;\n var dist = Math.abs(bestRefs[tj] - bestClusters[ti]);\n if (dist > maxDist) maxDist = dist;\n }\n }\n if (tMinEdge !== Infinity) { mins[ti] = tMinEdge; maxes[ti] = tMaxEdge; }\n tols.push(Math.max(CLUSTER_TOLERANCE, maxDist));\n }\n\n return { clusters: bestClusters, mins: mins, maxes: maxes, mode: best, tols: tols };\n}\n\nfunction findClusterIndex(clusters, value, tols) {\n for (var i = 0; i < clusters.length; i++) {\n var tol = tols ? tols[i] : CLUSTER_TOLERANCE;\n if (Math.abs(clusters[i] - value) <= tol) return i;\n }\n return -1;\n}\n\n// Get the reference value for a variant based on the axis alignment mode\nfunction getVariantRef(v, posKey, sizeKey, mode) {\n if (mode === 'center') return v[posKey] + effectiveSize(v, sizeKey) / 2;\n if (mode === 'right') return v[posKey] + effectiveSize(v, sizeKey);\n return v[posKey]; // 'left' default\n}\n\nfunction analyzeLayout(componentSet, enumProps, gridAlignment, allowSpanning) {\n const variants = componentSet.children.filter(c => c.type === 'COMPONENT' && c.visible !== false);\n if (DEBUG) {\n console.log('[Step 2] Variants found:', variants.length);\n console.log('[Step 2] Variant positions:', variants.map(v => ({ name: v.name, x: v.x, y: v.y, w: v.width, h: v.height })));\n }\n\n // Transpose variant grid if the forced alignment doesn't match the physical layout\n if (gridAlignment !== 'auto' && variants.length > 1) {\n const preColCount = clusterVariantAxis(variants, 'x', 'width').clusters.length;\n const preRowCount = clusterVariantAxis(variants, 'y', 'height').clusters.length;\n\n const needsTranspose =\n (gridAlignment === 'x' && preColCount < preRowCount) ||\n (gridAlignment === 'y' && preRowCount < preColCount);\n\n if (needsTranspose) {\n if (DEBUG) {\n console.log('[Step 2] Transposing variant grid for alignment:', gridAlignment,\n '(was', preColCount, 'cols ×', preRowCount, 'rows)',\n '| layoutMode:', componentSet.layoutMode);\n }\n\n if (componentSet.layoutMode === 'VERTICAL') {\n componentSet.layoutMode = 'HORIZONTAL';\n } else if (componentSet.layoutMode === 'HORIZONTAL') {\n componentSet.layoutMode = 'VERTICAL';\n } else {\n // No auto-layout (NONE) — reposition variants into transposed grid\n const preColAxis = clusterVariantAxis(variants, 'x', 'width');\n const preRowAxis = clusterVariantAxis(variants, 'y', 'height');\n\n // Map each variant to its grid position\n const preGrid = variants.map(v => ({\n node: v,\n col: findClusterIndex(preColAxis.clusters, getVariantRef(v, 'x', 'width', preColAxis.mode), preColAxis.tols),\n row: findClusterIndex(preRowAxis.clusters, getVariantRef(v, 'y', 'height', preRowAxis.mode), preRowAxis.tols),\n }));\n\n // Determine gap from the axis with multiple items\n let gap = 48;\n if (preRowCount > 1) {\n gap = preRowAxis.mins[1] - preRowAxis.maxes[0];\n } else if (preColCount > 1) {\n gap = preColAxis.mins[1] - preColAxis.maxes[0];\n }\n if (gap < 0) gap = 48;\n\n const padding = Math.min(preColAxis.mins[0], preRowAxis.mins[0]);\n const newColCount = preRowCount;\n const newRowCount = preColCount;\n\n // Max variant width per new column (= variants from old row)\n const newColWidths = [];\n for (let nc = 0; nc < newColCount; nc++) {\n const inOldRow = preGrid.filter(g => g.row === nc);\n newColWidths.push(Math.max(...inOldRow.map(g => Math.max(g.node.width, 1))));\n }\n\n // Max variant height per new row (= variants from old column)\n const newRowHeights = [];\n for (let nr = 0; nr < newRowCount; nr++) {\n const inOldCol = preGrid.filter(g => g.col === nr);\n newRowHeights.push(Math.max(...inOldCol.map(g => Math.max(g.node.height, 1))));\n }\n\n // Compute new column x-positions (accumulated widths + gap)\n const newColX = [padding];\n for (let nc = 1; nc < newColCount; nc++) {\n newColX.push(newColX[nc - 1] + newColWidths[nc - 1] + gap);\n }\n\n // Compute new row y-positions\n const newRowY = [padding];\n for (let nr = 1; nr < newRowCount; nr++) {\n newRowY.push(newRowY[nr - 1] + newRowHeights[nr - 1] + gap);\n }\n\n // Move each variant: old row → new column, old column → new row\n for (const g of preGrid) {\n g.node.x = newColX[g.row];\n g.node.y = newRowY[g.col];\n }\n\n // Resize component set\n const lastCol = newColCount - 1;\n const lastRow = newRowCount - 1;\n componentSet.resize(\n newColX[lastCol] + newColWidths[lastCol] + padding,\n newRowY[lastRow] + newRowHeights[lastRow] + padding\n );\n\n if (DEBUG) {\n console.log('[Step 2] Transposed to', newColCount, 'cols ×', newRowCount, 'rows',\n '| gap:', gap, '| padding:', padding, '| CS:', componentSet.width, '×', componentSet.height);\n }\n }\n }\n }\n\n // Cluster by detected alignment (left, center, or right — whichever gives fewest clusters)\n let colAxis = clusterVariantAxis(variants, 'x', 'width');\n let rowAxis = clusterVariantAxis(variants, 'y', 'height');\n let colClusters = colAxis.mins; // left edge of each column\n let rowClusters = rowAxis.mins; // top edge of each row\n let colRefs = colAxis.clusters; // reference values for column matching\n let rowRefs = rowAxis.clusters; // reference values for row matching\n if (DEBUG) {\n console.log('[Step 2] Column clusters (' + colClusters.length + ' cols, mode=' + colAxis.mode + '):', colClusters);\n console.log('[Step 2] Row clusters (' + rowClusters.length + ' rows, mode=' + rowAxis.mode + '):', rowClusters);\n console.log('[Step 2] Grid dimensions:', colClusters.length, 'cols ×', rowClusters.length, 'rows');\n }\n\n // Build grid: map each variant to its (col, row) using detected alignment\n let grid = variants.map(v => ({\n node: v,\n col: findClusterIndex(colRefs, getVariantRef(v, 'x', 'width', colAxis.mode), colAxis.tols),\n row: findClusterIndex(rowRefs, getVariantRef(v, 'y', 'height', rowAxis.mode), rowAxis.tols),\n props: Object.fromEntries(\n Object.entries(v.variantProperties).map(([k, val]) => [k, val])\n ),\n colSpan: 1,\n rowSpan: 1,\n }));\n\n // Pre-index grid by row and column for fast lookups\n var gridByRow = {};\n var gridByCol = {};\n for (var gi = 0; gi < grid.length; gi++) {\n var g = grid[gi];\n if (!gridByRow[g.row]) gridByRow[g.row] = [];\n gridByRow[g.row].push(g);\n if (!gridByCol[g.col]) gridByCol[g.col] = [];\n gridByCol[g.col].push(g);\n }\n\n if (DEBUG) {\n console.log('[Step 2] Grid mapping:', grid.map(g => ({ name: g.node.name, col: g.col, row: g.row, props: g.props })));\n }\n\n // For each enum property, determine axis\n const colAxisProps = [];\n const rowAxisProps = [];\n\n for (const prop of enumProps) {\n let variesAlongX = false;\n let variesAlongY = false;\n\n // Check if property varies across columns within same row\n for (let r = 0; r < rowClusters.length; r++) {\n const inRow = gridByRow[r] || [];\n const valuesInRow = new Set(inRow.map(g => g.props[prop.name]));\n if (valuesInRow.size > 1) variesAlongX = true;\n }\n\n // Check if property varies across rows within same column\n for (let c = 0; c < colClusters.length; c++) {\n const inCol = gridByCol[c] || [];\n const valuesInCol = new Set(inCol.map(g => g.props[prop.name]));\n if (valuesInCol.size > 1) variesAlongY = true;\n }\n\n // Diagonal layout: each variant at unique x AND y, so no variation detected within rows/cols.\n // Fall back to checking overall spread to pick the dominant axis.\n if (!variesAlongX && !variesAlongY && prop.values.length > 1) {\n const xSpread = colClusters[colClusters.length - 1] - colClusters[0];\n const ySpread = rowClusters[rowClusters.length - 1] - rowClusters[0];\n if (xSpread >= ySpread) {\n variesAlongX = true;\n } else {\n variesAlongY = true;\n }\n if (DEBUG) {\n console.log('[Step 2] Property \"' + prop.name + '\": diagonal layout detected, xSpread=' + xSpread + ', ySpread=' + ySpread + ' → fallback to ' + (variesAlongX ? 'COLUMN' : 'ROW') + ' axis');\n }\n }\n\n if (DEBUG) {\n console.log('[Step 2] Property \"' + prop.name + '\": variesAlongX=' + variesAlongX + ', variesAlongY=' + variesAlongY + ' → ' + (\n variesAlongX && !variesAlongY ? 'COLUMN axis' :\n variesAlongY && !variesAlongX ? 'ROW axis' :\n variesAlongX && variesAlongY ? 'BOTH (defaulting to ROW)' :\n 'ROW axis (single value)'\n ));\n }\n\n if (variesAlongX && !variesAlongY) {\n colAxisProps.push(prop);\n } else if (variesAlongY && !variesAlongX) {\n rowAxisProps.push(prop);\n } else if (variesAlongX && variesAlongY) {\n // Ambiguous — put on rows by default\n rowAxisProps.push(prop);\n prop._bothAxes = true;\n } else {\n // Single value — still show as a row label (left side)\n rowAxisProps.push(prop);\n }\n }\n\n // Detect single-type grid: one property spread across a multi-row × multi-col grid.\n // Instead of forcing it onto one axis (which produces nonsensical labels), flag it\n // so the generator uses per-item labels below each variant.\n //\n // Also handles spanning variants: when a variant is wider/taller than one cell,\n // clusterVariantAxis merges overlapping bounding boxes and collapses columns.\n // We use raw position clustering (clusterValues) to recover the true grid structure.\n var singleTypeGrid = false;\n if (allowSpanning && enumProps.length === 1 && variants.length > 1) {\n // Check raw positions — clusterValues doesn't do bounding-box overlap merge\n var rawColResult = clusterValues(variants.map(function(v) { return v.x; }));\n var rawRowResult = clusterValues(variants.map(function(v) { return v.y; }));\n var rawColCount = rawColResult.clusters.length;\n var rawRowCount = rawRowResult.clusters.length;\n\n if (rawColCount > 1 && rawRowCount > 1) {\n singleTypeGrid = true;\n colAxisProps.length = 0;\n rowAxisProps.length = 0;\n\n // If overlap merge collapsed columns/rows, rebuild grid from raw positions\n if (rawColCount > colClusters.length || rawRowCount > rowClusters.length) {\n if (DEBUG) { console.log('[Step 2] Spanning variant detected — rebuilding grid from raw positions (' + rawColCount + ' cols × ' + rawRowCount + ' rows, was ' + colClusters.length + ' × ' + rowClusters.length + ')'); }\n\n // Rebuild column data from raw position clusters\n var rawColClusters = rawColResult.clusters; // left edges\n var rawColHighs = rawColResult.highs;\n // Assign each variant to its starting column (closest left-edge cluster)\n // and compute column bounds from non-spanning variants\n var rawColMins = [];\n var rawColMaxes = [];\n var rawColTols = [];\n for (var rci = 0; rci < rawColClusters.length; rci++) {\n rawColMins.push(Infinity);\n rawColMaxes.push(-Infinity);\n rawColTols.push(CLUSTER_TOLERANCE);\n }\n\n // Rebuild row data similarly\n var rawRowClusters = rawRowResult.clusters;\n var rawRowHighs = rawRowResult.highs;\n var rawRowMins = [];\n var rawRowMaxes = [];\n var rawRowTols = [];\n for (var rri = 0; rri < rawRowClusters.length; rri++) {\n rawRowMins.push(Infinity);\n rawRowMaxes.push(-Infinity);\n rawRowTols.push(CLUSTER_TOLERANCE);\n }\n\n // Find column/row for each variant and detect spanning\n var rawGrid = [];\n for (var rvi = 0; rvi < variants.length; rvi++) {\n var rv = variants[rvi];\n var rvLeft = rv.x;\n var rvRight = rv.x + rv.width;\n var rvTop = rv.y;\n var rvBottom = rv.y + rv.height;\n\n // Find starting column (closest cluster to variant's left edge)\n var rvCol = 0;\n var rvColDist = Math.abs(rvLeft - rawColClusters[0]);\n for (var rcci = 1; rcci < rawColClusters.length; rcci++) {\n var d = Math.abs(rvLeft - rawColClusters[rcci]);\n if (d < rvColDist) { rvCol = rcci; rvColDist = d; }\n }\n\n // Find starting row\n var rvRow = 0;\n var rvRowDist = Math.abs(rvTop - rawRowClusters[0]);\n for (var rrci = 1; rrci < rawRowClusters.length; rrci++) {\n var d2 = Math.abs(rvTop - rawRowClusters[rrci]);\n if (d2 < rvRowDist) { rvRow = rrci; rvRowDist = d2; }\n }\n\n // Detect column span: how many columns does this variant cover?\n var rvColSpan = 1;\n for (var rcs = rvCol + 1; rcs < rawColClusters.length; rcs++) {\n if (rvRight > rawColClusters[rcs] + CLUSTER_TOLERANCE) {\n rvColSpan++;\n } else {\n break;\n }\n }\n\n // Detect row span\n var rvRowSpan = 1;\n for (var rrs = rvRow + 1; rrs < rawRowClusters.length; rrs++) {\n if (rvBottom > rawRowClusters[rrs] + CLUSTER_TOLERANCE) {\n rvRowSpan++;\n } else {\n break;\n }\n }\n\n rawGrid.push({\n node: rv,\n col: rvCol,\n row: rvRow,\n colSpan: rvColSpan,\n rowSpan: rvRowSpan,\n props: Object.fromEntries(\n Object.entries(rv.variantProperties).map(function(e) { return [e[0], e[1]]; })\n ),\n });\n\n // Update column bounds (only from non-spanning variants for accurate widths)\n if (rvColSpan === 1) {\n if (rvLeft < rawColMins[rvCol]) rawColMins[rvCol] = rvLeft;\n if (rvRight > rawColMaxes[rvCol]) rawColMaxes[rvCol] = rvRight;\n }\n if (rvRowSpan === 1) {\n if (rvTop < rawRowMins[rvRow]) rawRowMins[rvRow] = rvTop;\n if (rvBottom > rawRowMaxes[rvRow]) rawRowMaxes[rvRow] = rvBottom;\n }\n }\n\n // Fill in any columns/rows that only have spanning variants (use cluster position + tolerance)\n for (var rfci = 0; rfci < rawColClusters.length; rfci++) {\n if (rawColMins[rfci] === Infinity) {\n rawColMins[rfci] = rawColClusters[rfci];\n rawColMaxes[rfci] = rawColClusters[rfci] + 50; // fallback width\n }\n }\n for (var rfri = 0; rfri < rawRowClusters.length; rfri++) {\n if (rawRowMins[rfri] === Infinity) {\n rawRowMins[rfri] = rawRowClusters[rfri];\n rawRowMaxes[rfri] = rawRowClusters[rfri] + 50;\n }\n }\n\n // Replace clustering data\n colClusters = rawColMins;\n colRefs = rawColClusters;\n colAxis = { clusters: rawColClusters, mins: rawColMins, maxes: rawColMaxes, mode: 'left', tols: rawColTols };\n rowClusters = rawRowMins;\n rowRefs = rawRowClusters;\n rowAxis = { clusters: rawRowClusters, mins: rawRowMins, maxes: rawRowMaxes, mode: 'left', tols: rawRowTols };\n\n // Replace grid and indices\n grid = rawGrid;\n gridByRow = {};\n gridByCol = {};\n for (var rgii = 0; rgii < grid.length; rgii++) {\n var rg = grid[rgii];\n if (!gridByRow[rg.row]) gridByRow[rg.row] = [];\n gridByRow[rg.row].push(rg);\n if (!gridByCol[rg.col]) gridByCol[rg.col] = [];\n gridByCol[rg.col].push(rg);\n }\n\n if (DEBUG) {\n console.log('[Step 2] Rebuilt grid with spanning:', grid.map(function(g) {\n return { name: g.node.name, col: g.col, row: g.row, colSpan: g.colSpan, rowSpan: g.rowSpan };\n }));\n }\n }\n\n if (DEBUG) { console.log('[Step 2] Single-type grid detected: \"' + enumProps[0].name + '\" with ' + enumProps[0].values.length + ' values in ' + rawColCount + '×' + rawRowCount + ' grid'); }\n }\n }\n\n // Determine ordering: outer properties change less frequently\n orderByFrequency(colAxisProps, grid, colClusters, 'col');\n orderByFrequency(rowAxisProps, grid, rowClusters, 'row');\n\n if (DEBUG) {\n console.log('[Step 2] Final column-axis properties (top labels):', colAxisProps.map(function(p) { return p.name; }));\n console.log('[Step 2] Final row-axis properties (left labels):', rowAxisProps.map(function(p) { return p.name; }));\n }\n\n return { grid, gridByRow, gridByCol, colClusters, rowClusters, colAxisProps, rowAxisProps, variants, colMaxes: colAxis.maxes, rowMaxes: rowAxis.maxes, colTols: colAxis.tols, rowTols: rowAxis.tols, singleTypeGrid: singleTypeGrid, singleTypeProp: singleTypeGrid ? enumProps[0] : null };\n}\n\nfunction orderByFrequency(axisProps, grid, clusters, axis) {\n // Sort so that properties with fewer changes (larger groups) come first (outer)\n axisProps.sort((a, b) => {\n const changesA = countChanges(a.name, grid, clusters, axis);\n const changesB = countChanges(b.name, grid, clusters, axis);\n return changesA - changesB; // fewer changes = outer\n });\n}\n\nfunction countChanges(propName, grid, clusters, axis) {\n // Count how many times the value changes along the axis\n let changes = 0;\n const key = axis === 'col' ? 'col' : 'row';\n const otherKey = axis === 'col' ? 'row' : 'col';\n\n // Pick the first \"line\" along the other axis\n const firstOther = grid.reduce((min, g) => Math.min(min, g[otherKey]), Infinity);\n const line = grid\n .filter(g => g[otherKey] === firstOther)\n .sort((a, b) => a[key] - b[key]);\n\n for (let i = 1; i < line.length; i++) {\n if (line[i].props[propName] !== line[i - 1].props[propName]) {\n changes++;\n }\n }\n return changes;\n}\n\n// ─── Measure Text Width (Step 5) ─────────────────────────────────────────────\n\nfunction initMeasureNode() {\n _measureNode = figma.createText();\n _measureNode.fontName = { family: _labelFontFamily, style: 'Regular' };\n _measureNode.fontSize = LABEL_FONT_SIZE;\n _measureCache = {};\n}\n\nfunction disposeMeasureNode() {\n if (_measureNode) {\n _measureNode.remove();\n _measureNode = null;\n }\n _measureCache = {};\n}\n\nfunction measureTextWidth(text, fontSize) {\n var key = text + '|' + fontSize;\n if (_measureCache[key] !== undefined) return _measureCache[key];\n if (_measureNode.fontSize !== fontSize) {\n _measureNode.fontSize = fontSize;\n }\n _measureNode.characters = text;\n var w = _measureNode.width;\n _measureCache[key] = w;\n return w;\n}\n\n// ─── Bracket Creation (Step 7) ───────────────────────────────────────────────\n\nfunction createVerticalBracket(height) {\n var vec = figma.createVector();\n vec.name = 'Bracket';\n // C-shape: top cap → spine → bottom cap\n vec.vectorPaths = [{\n windingRule: 'NONZERO',\n data: 'M ' + BRACKET_CAP_LENGTH + ' 0 L 0 0 L 0 ' + height + ' L ' + BRACKET_CAP_LENGTH + ' ' + height\n }];\n vec.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n vec.strokeWeight = BRACKET_STROKE_WEIGHT;\n vec.fills = [];\n return vec;\n}\n\nfunction createHorizontalBracket(width) {\n var vec = figma.createVector();\n vec.name = 'Bracket';\n // U-shape: left cap down → spine across → right cap down\n vec.vectorPaths = [{\n windingRule: 'NONZERO',\n data: 'M 0 ' + BRACKET_CAP_LENGTH + ' L 0 0 L ' + width + ' 0 L ' + width + ' ' + BRACKET_CAP_LENGTH\n }];\n vec.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n vec.strokeWeight = BRACKET_STROKE_WEIGHT;\n vec.fills = [];\n return vec;\n}\n\n// ─── Label Creation (Step 6b, 6c) ────────────────────────────────────────────\n\nfunction createTextNode(text, fontSize) {\n const node = figma.createText();\n node.fontName = { family: _labelFontFamily, style: 'Regular' };\n node.fontSize = fontSize || LABEL_FONT_SIZE;\n node.characters = text;\n node.fills = [{ type: 'SOLID', color: DOC_COLOR }];\n return node;\n}\n\nfunction createRowLabel(value, x, y, width, height, withBracket) {\n const label = figma.createFrame();\n label.name = 'Label';\n label.resize(width, height);\n label.x = x;\n label.y = y;\n label.fills = [];\n label.clipsContent = false;\n\n const textNode = createTextNode(value);\n label.appendChild(textNode);\n\n if (withBracket) {\n const bracketHeight = height - LABEL_PADDING * 2;\n const bracket = createVerticalBracket(bracketHeight > 0 ? bracketHeight : height);\n bracket.x = width - BRACKET_THICKNESS;\n bracket.y = LABEL_PADDING;\n label.appendChild(bracket);\n\n // Center text vertically, right-align to the left of the bracket\n textNode.x = Math.max(0, width - BRACKET_THICKNESS - LABEL_GAP - textNode.width);\n textNode.y = (height - textNode.height) / 2;\n } else {\n // Center text vertically, right-align with spacing from grid edge\n textNode.x = Math.max(0, width - textNode.width - 6);\n textNode.y = (height - textNode.height) / 2;\n }\n\n return label;\n}\n\nfunction createColLabel(value, x, y, width, height, withBracket) {\n const label = figma.createFrame();\n label.name = 'Label';\n label.resize(width, height);\n label.x = x;\n label.y = y;\n label.fills = [];\n label.clipsContent = false;\n\n const textNode = createTextNode(value);\n label.appendChild(textNode);\n\n if (withBracket) {\n const bracketWidth = width - LABEL_PADDING * 2;\n const bracket = createHorizontalBracket(bracketWidth > 0 ? bracketWidth : width);\n bracket.x = LABEL_PADDING;\n bracket.y = height - BRACKET_THICKNESS;\n label.appendChild(bracket);\n\n // Center text horizontally, position above the bracket\n textNode.x = (width - textNode.width) / 2;\n textNode.y = 0;\n } else {\n // Center text horizontally\n textNode.x = (width - textNode.width) / 2;\n textNode.y = (height - textNode.height) / 2;\n }\n\n return label;\n}\n\n// ─── Per-Item Labels (single-type grid) ─────────────────────────────────────\n\nfunction createItemLabel(value, x, y, cellWidth) {\n var label = figma.createFrame();\n label.name = 'Label';\n label.resize(cellWidth, ITEM_LABEL_HEIGHT);\n label.x = x;\n label.y = y;\n label.fills = [];\n label.clipsContent = false;\n\n var textNode = createTextNode(value);\n textNode.x = (cellWidth - textNode.width) / 2;\n textNode.y = (ITEM_LABEL_HEIGHT - textNode.height) / 2;\n label.appendChild(textNode);\n\n return label;\n}\n\n// Build uniform grid layout for single-type grids. Computes cell dimensions,\n// updates colClusters/colWidths/rowClusters/rowHeights in-place, and returns\n// { cellW, cellH, maxVarH } for downstream use.\n// positionFn(gridEntry, x, y) is called for each variant with its new position.\nfunction buildSingleTypeUniformGrid(grid, gridByCol, colClusters, colWidths, rowClusters, rowHeights, singleTypeProp, positionFn) {\n var pad = 24;\n var innerLabelH = ITEM_LABEL_GAP + ITEM_LABEL_HEIGHT;\n\n // Compute uniform cell width: max of non-spanning variant widths and label widths\n var cellW = 0;\n for (var cw = 0; cw < colWidths.length; cw++) {\n if (colWidths[cw] > cellW) cellW = colWidths[cw];\n }\n for (var c = 0; c < colClusters.length; c++) {\n var inCol = gridByCol[c] || [];\n for (var ci = 0; ci < inCol.length; ci++) {\n var lw = measureTextWidth(inCol[ci].props[singleTypeProp.name], LABEL_FONT_SIZE) + LABEL_GAP * 2;\n var effLW = inCol[ci].colSpan > 1 ? lw / inCol[ci].colSpan : lw;\n if (effLW > cellW) cellW = effLW;\n }\n }\n\n var colGap = pad;\n var maxVarH = Math.max.apply(null, rowHeights);\n var cellH = maxVarH + innerLabelH;\n var rowGap = pad;\n\n // Update column positions\n for (var c2 = 0; c2 < colClusters.length; c2++) {\n colClusters[c2] = pad + c2 * (cellW + colGap);\n colWidths[c2] = cellW;\n }\n\n // Update row positions — cell height includes label space\n for (var r = 0; r < rowClusters.length; r++) {\n rowClusters[r] = pad + r * (cellH + rowGap);\n rowHeights[r] = cellH;\n }\n\n // Reposition variants (handling spanning)\n for (var gi = 0; gi < grid.length; gi++) {\n var g = grid[gi];\n var vw = g.node.width;\n var vh = g.node.height;\n var varAreaCenter = rowClusters[g.row] + maxVarH / 2;\n var newX, newY = varAreaCenter - vh / 2;\n\n if (g.colSpan > 1) {\n var spanLeft = colClusters[g.col];\n var spanRight = colClusters[g.col + g.colSpan - 1] + cellW;\n newX = (spanLeft + spanRight) / 2 - vw / 2;\n } else {\n newX = colClusters[g.col] + cellW / 2 - vw / 2;\n }\n positionFn(g, newX, newY);\n }\n\n return { cellW: cellW, cellH: cellH, maxVarH: maxVarH, pad: pad };\n}\n\n// Create per-item labels for a single-type grid, appended to parentFrame.\nfunction createSingleTypeGridLabels(parentFrame, grid, singleTypeProp, colClusters, colWidths, rowClusters, maxVarH, offsetX, offsetY) {\n for (var i = 0; i < grid.length; i++) {\n var g = grid[i];\n var value = g.props[singleTypeProp.name];\n var cellLeft = colClusters[g.col];\n var labelW;\n if (g.colSpan > 1) {\n var spanRight = colClusters[g.col + g.colSpan - 1] + colWidths[g.col + g.colSpan - 1];\n labelW = spanRight - cellLeft;\n } else {\n labelW = colWidths[g.col];\n }\n var labelX = offsetX + cellLeft;\n var labelY = offsetY + rowClusters[g.row] + maxVarH + ITEM_LABEL_GAP;\n parentFrame.appendChild(createItemLabel(value, labelX, labelY, labelW));\n }\n}\n\n// ─── Grid Lines ──────────────────────────────────────────────────────────────\n\nfunction createGridLines(wrapper, colClusters, rowClusters, colWidths, rowHeights, csX, csY, csWidth, csHeight, spanningGrid) {\n const gridFrame = figma.createFrame();\n gridFrame.name = 'Grid';\n gridFrame.fills = [];\n gridFrame.clipsContent = false;\n\n // Grid covers the full component set area\n gridFrame.resize(csWidth, csHeight);\n gridFrame.x = csX;\n gridFrame.y = csY;\n\n // Build a set of rows that each vertical column-boundary should skip (due to spanning)\n // spanningGrid is an array of { col, row, colSpan, rowSpan } entries\n var colSkipRows = null;\n if (spanningGrid) {\n colSkipRows = {}; // colBoundary index → set of row indices to skip\n for (var sgi = 0; sgi < spanningGrid.length; sgi++) {\n var sg = spanningGrid[sgi];\n if (sg.colSpan > 1) {\n // This variant spans from sg.col to sg.col + sg.colSpan - 1\n // Skip vertical lines at column boundaries within this span\n for (var sbc = sg.col + 1; sbc < sg.col + sg.colSpan; sbc++) {\n if (!colSkipRows[sbc]) colSkipRows[sbc] = {};\n for (var sbr = sg.row; sbr < sg.row + sg.rowSpan; sbr++) {\n colSkipRows[sbc][sbr] = true;\n }\n }\n }\n }\n }\n\n // Inner horizontal lines only (skip first and last — no outer border)\n for (let r = 1; r < rowClusters.length; r++) {\n const prevRowBottom = rowClusters[r - 1] + rowHeights[r - 1];\n const midY = (prevRowBottom + rowClusters[r]) / 2;\n const line = figma.createLine();\n line.resize(csWidth, 0);\n line.x = 0;\n line.y = midY;\n line.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n line.strokeWeight = GRID_STROKE_WEIGHT;\n line.dashPattern = GRID_DASH_PATTERN;\n gridFrame.appendChild(line);\n }\n\n // Inner vertical lines (skip first and last — no outer border)\n for (let c = 1; c < colClusters.length; c++) {\n const prevColRight = colClusters[c - 1] + colWidths[c - 1];\n const midX = (prevColRight + colClusters[c]) / 2;\n\n if (colSkipRows && colSkipRows[c]) {\n // Build skip regions: for each skipped row, the vertical line must be absent\n // from the midpoint above the row to the midpoint below (or grid edge for first/last).\n // This matches where horizontal grid lines are drawn.\n var skipRanges = [];\n for (var vr = 0; vr < rowClusters.length; vr++) {\n if (!colSkipRows[c][vr]) continue;\n var skipTop = vr === 0 ? 0 :\n (rowClusters[vr - 1] + rowHeights[vr - 1] + rowClusters[vr]) / 2;\n var skipBottom = vr === rowClusters.length - 1 ? csHeight :\n (rowClusters[vr] + rowHeights[vr] + rowClusters[vr + 1]) / 2;\n // Merge with previous range if overlapping\n if (skipRanges.length > 0 && skipTop <= skipRanges[skipRanges.length - 1].bottom) {\n skipRanges[skipRanges.length - 1].bottom = skipBottom;\n } else {\n skipRanges.push({ top: skipTop, bottom: skipBottom });\n }\n }\n\n // Draw segments in the non-skipped regions\n var drawStart = 0;\n for (var sri = 0; sri < skipRanges.length; sri++) {\n if (skipRanges[sri].top > drawStart) {\n var segLine = figma.createLine();\n segLine.rotation = -90;\n segLine.resize(skipRanges[sri].top - drawStart, 0);\n segLine.x = midX;\n segLine.y = drawStart;\n segLine.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n segLine.strokeWeight = GRID_STROKE_WEIGHT;\n segLine.dashPattern = GRID_DASH_PATTERN;\n gridFrame.appendChild(segLine);\n }\n drawStart = skipRanges[sri].bottom;\n }\n // Final segment after last skip\n if (drawStart < csHeight) {\n var segLineEnd = figma.createLine();\n segLineEnd.rotation = -90;\n segLineEnd.resize(csHeight - drawStart, 0);\n segLineEnd.x = midX;\n segLineEnd.y = drawStart;\n segLineEnd.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n segLineEnd.strokeWeight = GRID_STROKE_WEIGHT;\n segLineEnd.dashPattern = GRID_DASH_PATTERN;\n gridFrame.appendChild(segLineEnd);\n }\n } else {\n // Full-height vertical line (no spanning in this column boundary)\n const line = figma.createLine();\n line.rotation = -90;\n line.resize(csHeight, 0);\n line.x = midX;\n line.y = 0;\n line.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n line.strokeWeight = GRID_STROKE_WEIGHT;\n line.dashPattern = GRID_DASH_PATTERN;\n gridFrame.appendChild(line);\n }\n }\n\n wrapper.appendChild(gridFrame);\n gridFrame.locked = true;\n return gridFrame;\n}\n\n// ─── Remove Old Labels (Step 9) ──────────────────────────────────────────────\n\n// Derive doc generation options from existing wrapper layer names\nfunction deriveDocMeta(wrapper) {\n var meta = { modes: [], booleanProps: [], nestedInstances: [], hasBooleanGrid: false };\n\n function extractSectionHeaders(sectionNode) {\n // Extract header text from label text nodes inside a boolean/nested section\n var headers = [];\n if (!sectionNode || !sectionNode.children) return headers;\n for (var i = 0; i < sectionNode.children.length; i++) {\n var child = sectionNode.children[i];\n // Header text nodes are direct TEXT children with the property values\n if (child.type === 'TEXT' && child.characters) {\n var text = child.characters.trim();\n // Extract the property name from \"PropName: Value\" format\n var colonIdx = text.indexOf(': ');\n if (colonIdx !== -1) {\n var propName = text.substring(0, colonIdx);\n if (headers.indexOf(propName) === -1) headers.push(propName);\n } else if (text.length > 0 && text.length < 60) {\n if (headers.indexOf(text) === -1) headers.push(text);\n }\n }\n }\n return headers;\n }\n\n function scan(node) {\n if (!node || !node.children) return;\n for (var i = 0; i < node.children.length; i++) {\n var child = node.children[i];\n var name = child.name || '';\n\n // Variable modes: \"Mode: <name>\" or \"Mode: <name> (collection1, collection2)\"\n if (name.indexOf('Mode: ') === 0) {\n meta.modes.push(name.substring(6));\n }\n\n // Boolean visibility section — extract property names from headers\n if (name === 'Boolean Visibility') {\n var boolHeaders = extractSectionHeaders(child);\n for (var bh = 0; bh < boolHeaders.length; bh++) {\n if (meta.booleanProps.indexOf(boolHeaders[bh]) === -1) meta.booleanProps.push(boolHeaders[bh]);\n }\n }\n\n // Boolean grid\n if (name === 'Boolean Grid') meta.hasBooleanGrid = true;\n\n // Nested instances section — extract instance names from headers\n if (name === 'Nested Instances') {\n var nestedHeaders = extractSectionHeaders(child);\n for (var nh = 0; nh < nestedHeaders.length; nh++) {\n if (meta.nestedInstances.indexOf(nestedHeaders[nh]) === -1) meta.nestedInstances.push(nestedHeaders[nh]);\n }\n }\n\n // Recurse into containers that hold sub-elements\n if (name === 'Documentation' || name === 'Modes' || name === 'Property Combinations') {\n scan(child);\n }\n }\n }\n scan(wrapper);\n return meta;\n}\n\nfunction findExistingWrapper(componentSet) {\n // Check if the component set is already inside a wrapper frame\n const parent = componentSet.parent;\n if (parent && parent.type === 'FRAME' && parent.name.startsWith('❖ ')) {\n // If wrapper has a stored source ID, verify it matches this CS\n var storedId = parent.getPluginData('sourceComponentSetId');\n if (storedId && storedId !== componentSet.id) return null;\n // Retroactively store ID for old wrappers (pre-fix) so duplicates are caught\n if (!storedId) parent.setPluginData('sourceComponentSetId', componentSet.id);\n return parent;\n }\n return null;\n}\n\n// Find a standalone doc wrapper — a sibling frame with pluginData 'standaloneDoc'\nfunction findStandaloneWrapper(node) {\n // Search siblings (legacy placement inside the same parent)\n var parent = node.parent;\n if (!parent) return null;\n for (var i = 0; i < parent.children.length; i++) {\n var sibling = parent.children[i];\n if (sibling !== node && sibling.type === 'FRAME' && sibling.getPluginData('standaloneDoc') === 'true') {\n var storedId = sibling.getPluginData('sourceComponentSetId');\n if (storedId && storedId !== node.id) continue;\n return sibling;\n }\n }\n // Search grandparent's children (new placement: next to the parent frame)\n var grandparent = parent.parent;\n if (!grandparent) return null;\n for (var gi = 0; gi < grandparent.children.length; gi++) {\n var gSibling = grandparent.children[gi];\n if (gSibling !== parent && gSibling.type === 'FRAME' && gSibling.getPluginData('standaloneDoc') === 'true') {\n var gStoredId = gSibling.getPluginData('sourceComponentSetId');\n if (gStoredId && gStoredId !== node.id) continue;\n return gSibling;\n }\n }\n return null;\n}\n\n// Find a variable modes doc wrapper — tagged with 'variableModesDoc' plugin data.\n// Searches the wrapper's parent (page level) since VM docs sit next to the regular wrapper.\nfunction findVariableModesWrapper(cs) {\n var wrapper = findExistingWrapper(cs);\n var searchParents = [];\n // Search siblings of the regular wrapper (page level)\n if (wrapper && wrapper.parent) searchParents.push(wrapper.parent);\n // Also search siblings of CS itself and its parent's parent\n if (cs.parent) {\n searchParents.push(cs.parent);\n if (cs.parent.parent) searchParents.push(cs.parent.parent);\n }\n for (var spi = 0; spi < searchParents.length; spi++) {\n var sp = searchParents[spi];\n for (var i = 0; i < sp.children.length; i++) {\n var child = sp.children[i];\n if (child.type === 'FRAME' && child.getPluginData('variableModesDoc') === 'true') {\n var storedId = child.getPluginData('sourceComponentSetId');\n if (storedId && storedId !== cs.id) continue;\n return child;\n }\n }\n }\n return null;\n}\n\n// ─── GusPropstar Detection & Removal ─────────────────────────────────────────\n\n// Detect competing GusPropstar docs around a component set.\n// Key signals:\n// - Grid auto layout (layoutWrap === 'WRAP') — GusPropstar uses this, we do not\n// - GROUP named \"labels\" or FRAME named \"instances\" — GusPropstar artifacts\n// Old format: sibling frame with empty name containing GROUP \"labels\" + FRAME \"instances\"\n// New format: parent ❖ frame using grid auto layout, or containing \"labels\"/\"instances\"\nfunction detectGusPropstar(componentSet) {\n var parent = componentSet.parent;\n if (!parent) return null;\n\n // Check if inside a ❖ wrapper that is a GusPropstar frame\n if (parent.type === 'FRAME' && parent.name.startsWith('❖')) {\n // Grid layout is the strongest signal — we never use it\n if (parent.layoutMode === 'GRID') {\n return 'new';\n }\n // Also check for GusPropstar artifacts (GROUP \"labels\" or FRAME \"instances\")\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'GROUP' && child.name.toLowerCase() === 'labels') return 'new';\n if (child.type === 'FRAME' && child.name.toLowerCase() === 'instances') return 'new';\n }\n }\n\n // Check for unnamed wrapper: parent frame (any name) directly contains\n // GROUP \"labels\" or FRAME \"instances\" as siblings of the component set\n if (parent.type === 'FRAME' && parent.children) {\n for (var n = 0; n < parent.children.length; n++) {\n var ch = parent.children[n];\n if (ch === componentSet) continue;\n if (ch.type === 'GROUP' && ch.name.toLowerCase() === 'labels') return 'new';\n if (ch.type === 'FRAME' && ch.name.toLowerCase() === 'instances') return 'new';\n }\n }\n\n // Check for old format: sibling empty-name frame with \"labels\" GROUP / \"instances\" FRAME\n if (parent.children) {\n for (var j = 0; j < parent.children.length; j++) {\n var sibling = parent.children[j];\n if (sibling === componentSet) continue;\n if (sibling.type === 'FRAME' && (sibling.name === '' || sibling.name === ' ')) {\n for (var k = 0; k < sibling.children.length; k++) {\n var sc = sibling.children[k];\n if (sc.type === 'GROUP' && sc.name.toLowerCase() === 'labels') return 'old';\n if (sc.type === 'FRAME' && sc.name.toLowerCase() === 'instances') return 'old';\n }\n }\n }\n }\n\n return null;\n}\n\nfunction removeGusPropstar(componentSet) {\n var format = detectGusPropstar(componentSet);\n if (!format) {\n figma.ui.postMessage({ type: 'error', message: 'No GusPropstar docs found.' });\n return;\n }\n\n var parent = componentSet.parent;\n\n if (format === 'new') {\n // Component set is inside a ❖ wrapper — remove all non-component children, then unwrap\n var wrapper = parent;\n var wrapperParent = wrapper.parent;\n var wrapperIndex = 0;\n for (var i = 0; i < wrapperParent.children.length; i++) {\n if (wrapperParent.children[i] === wrapper) { wrapperIndex = i; break; }\n }\n var wrapperX = wrapper.x;\n var wrapperY = wrapper.y;\n\n // Remove all non-component children (labels, instances, grids, etc.)\n var children = Array.from(wrapper.children);\n for (var j = 0; j < children.length; j++) {\n if (children[j].type !== 'COMPONENT_SET' && children[j].type !== 'COMPONENT') {\n children[j].remove();\n }\n }\n\n // Move component set out of wrapper\n wrapperParent.insertChild(wrapperIndex, componentSet);\n componentSet.x = wrapperX;\n componentSet.y = wrapperY;\n\n // Reset absolute positioning if needed\n if (componentSet.layoutPositioning === 'ABSOLUTE') {\n componentSet.layoutPositioning = 'AUTO';\n }\n\n // Remove the now-empty wrapper\n wrapper.remove();\n }\n\n if (format === 'old') {\n // Remove the sibling empty-name doc frame\n for (var k = 0; k < parent.children.length; k++) {\n var sibling = parent.children[k];\n if (sibling === componentSet) continue;\n if (sibling.type === 'FRAME' && (sibling.name === '' || sibling.name === ' ')) {\n var hasArtifacts = false;\n for (var m = 0; m < sibling.children.length; m++) {\n var sc = sibling.children[m];\n if (sc.type === 'GROUP' && sc.name.toLowerCase() === 'labels') hasArtifacts = true;\n if (sc.type === 'FRAME' && sc.name.toLowerCase() === 'instances') hasArtifacts = true;\n }\n if (hasArtifacts) {\n sibling.remove();\n break;\n }\n }\n }\n }\n\n // Reset component set stroke to default Figma purple with dashes\n componentSet.strokes = [{ type: 'SOLID', color: { r: 0.592, g: 0.278, b: 1.0 }, opacity: 1 }];\n componentSet.dashPattern = [4, 4];\n\n figma.currentPage.selection = [componentSet];\n figma.viewport.scrollAndZoomIntoView([componentSet]);\n figma.ui.postMessage({ type: 'done', message: 'GusPropstar docs removed.' });\n sendSelectionInfo();\n}\n\nfunction removeDocs() {\n var cs = getComponentSet();\n var node = cs;\n if (!node) {\n node = getStandaloneComponent();\n }\n if (!node) {\n figma.ui.postMessage({ type: 'error', message: 'Please select a component set or component.' });\n return;\n }\n\n // Standalone docs are separate frames — skip them, they must be deleted manually\n var standaloneWrapper = findStandaloneWrapper(node);\n if (standaloneWrapper && !findExistingWrapper(node)) {\n figma.ui.postMessage({ type: 'error', message: 'Standalone docs cannot be removed via the plugin. Delete the frame manually.' });\n return;\n }\n\n const wrapper = findExistingWrapper(node);\n if (!wrapper) {\n figma.ui.postMessage({ type: 'error', message: 'No docs found to remove.' });\n return;\n }\n\n // Restore CS strokes if they were saved (boolean grid mode removes them)\n var savedStrokes = node.getPluginData('originalStrokes');\n if (savedStrokes) {\n try {\n var strokeData = JSON.parse(savedStrokes);\n node.strokes = strokeData.strokes;\n node.strokeWeight = strokeData.strokeWeight;\n node.dashPattern = strokeData.dashPattern;\n node.strokeAlign = strokeData.strokeAlign;\n if (strokeData.cornerRadius !== undefined) node.cornerRadius = strokeData.cornerRadius;\n } catch (e) {\n console.log('[removeDocs] Error restoring strokes:', e.message);\n }\n node.setPluginData('originalStrokes', '');\n }\n\n // Restore CS fills if they were saved (generation clears them for grid line visibility)\n var savedFills = node.getPluginData('originalFills');\n if (savedFills) {\n try {\n node.fills = JSON.parse(savedFills);\n } catch (e) {\n console.log('[removeDocs] Error restoring fills:', e.message);\n }\n node.setPluginData('originalFills', '');\n }\n\n // Move node back to the wrapper's parent at the wrapper's position\n const wrapperParent = wrapper.parent;\n const wrapperIndex = wrapperParent.children.indexOf(wrapper);\n const wrapperX = wrapper.x;\n const wrapperY = wrapper.y;\n\n wrapperParent.insertChild(wrapperIndex, node);\n node.x = wrapperX;\n node.y = wrapperY;\n\n // Reset absolute positioning if it was set for auto-layout wrapper\n if (node.layoutPositioning === 'ABSOLUTE') {\n node.layoutPositioning = 'AUTO';\n }\n\n // Remove the wrapper (and all labels/grid inside it)\n wrapper.remove();\n\n // Also remove variable modes wrapper if it exists\n var vmDocsWrapper = findVariableModesWrapper(node);\n if (vmDocsWrapper) {\n vmDocsWrapper.remove();\n console.log('[removeDocs] Variable modes wrapper removed.');\n }\n\n figma.currentPage.selection = [node];\n figma.viewport.scrollAndZoomIntoView([node]);\n\n figma.ui.postMessage({ type: 'done', message: 'Docs removed.' });\n sendSelectionInfo();\n}\n\nfunction changeDocsColor(hex) {\n var cs = getComponentSet();\n if (!cs) cs = getStandaloneComponent();\n if (!cs) return;\n\n var wrapper = findExistingWrapper(cs) || findStandaloneWrapper(cs);\n if (!wrapper) return;\n\n var color = hexToRgb(hex);\n DOC_COLOR = color;\n\n // Recursively update colors on doc nodes only (skip instances and components)\n function updateNode(node) {\n // Never recurse into instances or components — those are actual design content\n if (node.type === 'INSTANCE' || node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {\n return;\n }\n // Update text fills\n if (node.type === 'TEXT') {\n node.fills = [{ type: 'SOLID', color: color }];\n }\n // Update line/vector strokes\n if (node.type === 'LINE' || node.type === 'VECTOR') {\n node.strokes = [{ type: 'SOLID', color: color }];\n }\n // Update dashed border strokes on Grid Border frames\n if (node.type === 'FRAME' && node.name === 'Grid Border' && node.strokes && node.strokes.length > 0) {\n node.strokes = [{ type: 'SOLID', color: color }];\n }\n // Recurse into children\n if (node.children) {\n for (var i = 0; i < node.children.length; i++) {\n updateNode(node.children[i]);\n }\n }\n }\n\n for (var i = 0; i < wrapper.children.length; i++) {\n var child = wrapper.children[i];\n if (child.type === 'FRAME' && child.name !== cs.name && child.type !== 'COMPONENT_SET') {\n updateNode(child);\n }\n }\n\n // Update the component set's stroke color with dashed border\n if (cs.strokes && cs.strokes.length > 0) {\n var newStrokes = [];\n for (var j = 0; j < cs.strokes.length; j++) {\n var s = cs.strokes[j];\n newStrokes.push({ type: s.type, color: color, opacity: s.opacity });\n }\n cs.strokes = newStrokes;\n cs.dashPattern = [4, 4];\n }\n}\n\nfunction toggleGrid(showGrid) {\n const cs = getComponentSet();\n if (!cs) return;\n\n const wrapper = findExistingWrapper(cs) || findStandaloneWrapper(cs);\n if (!wrapper) return;\n\n // Find the container where grid frames live (Documentation frame or wrapper itself)\n var container = wrapper;\n for (var ci = 0; ci < wrapper.children.length; ci++) {\n if (wrapper.children[ci].type === 'FRAME' && wrapper.children[ci].name === 'Documentation') {\n container = wrapper.children[ci];\n break;\n }\n }\n\n // Remove all existing grid frames\n var gridsToRemove = [];\n for (var gi = 0; gi < container.children.length; gi++) {\n if (container.children[gi].type === 'FRAME' && container.children[gi].name === 'Grid') {\n gridsToRemove.push(container.children[gi]);\n }\n }\n for (var ri = 0; ri < gridsToRemove.length; ri++) {\n gridsToRemove[ri].remove();\n }\n\n if (showGrid) {\n // Recalculate grid from current variant layout using alignment-aware clustering\n var variants = cs.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n var colAxis = clusterVariantAxis(variants, 'x', 'width');\n var rowAxis = clusterVariantAxis(variants, 'y', 'height');\n var colClusters = colAxis.mins;\n var rowClusters = rowAxis.mins;\n\n var colWidths = [];\n for (var c = 0; c < colClusters.length; c++) {\n colWidths.push(colAxis.maxes[c] - colClusters[c]);\n }\n\n var rowHeights = [];\n for (var r = 0; r < rowClusters.length; r++) {\n rowHeights.push(rowAxis.maxes[r] - rowClusters[r]);\n }\n\n createGridLines(container, colClusters, rowClusters, colWidths, rowHeights, cs.x, cs.y, cs.width, cs.height);\n }\n}\n\n// Scan all pages for PropStar (GusPropstar) doc wrappers — NOT our own autodocs.\n// PropStar signatures: ❖ frame with GRID layout, or frames containing GROUP \"labels\" / FRAME \"instances\"\nasync function scanFileForDocs() {\n var pages = figma.root.children;\n var results = [];\n figma.notify('Scanning ' + pages.length + ' page(s) for PropStar docs — this could take a while…', { timeout: 60000 });\n\n function isPropStarWrapper(frame) {\n if (frame.type !== 'FRAME') return false;\n // Our own wrappers have pluginData — skip them\n if (frame.getPluginData('sourceComponentSetId') || frame.getPluginData('standaloneDoc') === 'true') return false;\n\n // ❖ frame with grid layout = PropStar\n if (frame.name.startsWith('❖') && frame.layoutMode === 'GRID') return 'grid';\n\n // Frame containing GROUP \"labels\" or FRAME \"instances\" = PropStar artifacts\n for (var i = 0; i < frame.children.length; i++) {\n var child = frame.children[i];\n if (child.type === 'GROUP' && child.name.toLowerCase() === 'labels') return 'labels';\n if (child.type === 'FRAME' && child.name.toLowerCase() === 'instances') return 'instances';\n }\n return false;\n }\n\n for (var p = 0; p < pages.length; p++) {\n var page = pages[p];\n await page.loadAsync();\n var frames = page.findAll(function(n) { return n.type === 'FRAME'; });\n var pageHits = 0;\n for (var f = 0; f < frames.length; f++) {\n var frame = frames[f];\n var signal = isPropStarWrapper(frame);\n if (signal) {\n pageHits++;\n results.push({\n page: page.name,\n pageId: page.id,\n name: frame.name || '(unnamed)',\n id: frame.id,\n signal: signal,\n x: Math.round(frame.x),\n y: Math.round(frame.y),\n w: Math.round(frame.width),\n h: Math.round(frame.height)\n });\n }\n }\n console.log('[Scan] Page ' + (p + 1) + '/' + pages.length + ' \"' + page.name + '\" — ' + pageHits + ' PropStar doc(s)');\n }\n console.log('[Scan] Total: ' + results.length + ' PropStar doc(s) across ' + pages.length + ' page(s)');\n for (var r = 0; r < results.length; r++) {\n var d = results[r];\n console.log(' ' + (r + 1) + '. \"' + d.name + '\" on page \"' + d.page + '\" — signal: ' + d.signal +\n ' — pos: (' + d.x + ', ' + d.y + ') size: ' + d.w + '×' + d.h);\n }\n figma.notify('Found ' + results.length + ' PropStar doc(s)');\n figma.ui.postMessage({ type: 'scanDocsResults', results: results, pageCount: pages.length });\n}\n\nfunction debugGridCoords() {\n var cs = getComponentSet();\n if (!cs) {\n console.log('[Grid Debug] No component set selected');\n return;\n }\n\n var wrapper = findExistingWrapper(cs);\n var variants = cs.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n\n // Alignment-aware clustering\n var colAxis = clusterVariantAxis(variants, 'x', 'width');\n var rowAxis = clusterVariantAxis(variants, 'y', 'height');\n var colClusters = colAxis.mins;\n var rowClusters = rowAxis.mins;\n\n var colWidths = [];\n for (var c = 0; c < colClusters.length; c++) {\n colWidths.push(colAxis.maxes[c] - colClusters[c]);\n }\n\n var rowHeights = [];\n for (var r = 0; r < rowClusters.length; r++) {\n rowHeights.push(rowAxis.maxes[r] - rowClusters[r]);\n }\n\n console.log('=== GRID DEBUG ===');\n console.log('Component set:', cs.name);\n console.log('CS position in wrapper: x=' + cs.x + ', y=' + cs.y);\n console.log('CS size: w=' + cs.width + ', h=' + cs.height);\n console.log('Wrapper:', wrapper ? ('x=' + wrapper.x + ', y=' + wrapper.y + ', w=' + wrapper.width + ', h=' + wrapper.height) : 'none');\n console.log('Variants:', variants.length);\n console.log('Column alignment mode:', colAxis.mode);\n console.log('Row alignment mode:', rowAxis.mode);\n console.log('---');\n console.log('Column refs (' + colAxis.clusters.length + ', ' + colAxis.mode + '):', JSON.stringify(colAxis.clusters));\n console.log('Column mins (left edges):', JSON.stringify(colClusters));\n console.log('Column maxes (right edges):', JSON.stringify(colAxis.maxes));\n console.log('Column widths:', JSON.stringify(colWidths));\n console.log('Row refs (' + rowAxis.clusters.length + ', ' + rowAxis.mode + '):', JSON.stringify(rowAxis.clusters));\n console.log('Row mins (top edges):', JSON.stringify(rowClusters));\n console.log('Row maxes (bottom edges):', JSON.stringify(rowAxis.maxes));\n console.log('Row heights:', JSON.stringify(rowHeights));\n console.log('---');\n\n // Horizontal line positions\n console.log('Horizontal lines (' + (rowClusters.length - 1) + '):');\n for (var ri = 1; ri < rowClusters.length; ri++) {\n var prevBottom = rowClusters[ri - 1] + rowHeights[ri - 1];\n var midY = (prevBottom + rowClusters[ri]) / 2;\n var gap = rowClusters[ri] - prevBottom;\n console.log(' H-line ' + ri + ': prevBottom=' + prevBottom + ' nextTop=' + rowClusters[ri] + ' gap=' + gap + ' → midY=' + midY);\n }\n\n // Vertical line positions\n console.log('Vertical lines (' + (colClusters.length - 1) + '):');\n for (var ci = 1; ci < colClusters.length; ci++) {\n var prevRight = colClusters[ci - 1] + colWidths[ci - 1];\n var midX = (prevRight + colClusters[ci]) / 2;\n var gapX = colClusters[ci] - prevRight;\n console.log(' V-line ' + ci + ': prevRight=' + prevRight + ' nextLeft=' + colClusters[ci] + ' gap=' + gapX + ' → midX=' + midX);\n }\n\n // Sample variant positions (first 10)\n console.log('---');\n console.log('Sample variants (first 10):');\n for (var si = 0; si < Math.min(10, variants.length); si++) {\n var v = variants[si];\n console.log(' \"' + v.name + '\" x=' + v.x + ' y=' + v.y + ' w=' + v.width + ' h=' + v.height + ' centerX=' + (v.x + v.width / 2) + ' centerY=' + (v.y + v.height / 2));\n }\n console.log('=== END GRID DEBUG ===');\n}\n\nfunction debugLabelCoords() {\n var cs = getComponentSet();\n if (!cs) {\n console.log('[Label Debug] No component set selected');\n return;\n }\n\n var wrapper = findExistingWrapper(cs);\n if (!wrapper) {\n console.log('[Label Debug] No wrapper found — generate docs first');\n return;\n }\n\n var variants = cs.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n\n // Re-analyze grid to get column/row info\n var colAxis = clusterVariantAxis(variants, 'x', 'width');\n var rowAxis = clusterVariantAxis(variants, 'y', 'height');\n var colClusters = colAxis.mins;\n var rowClusters = rowAxis.mins;\n\n var colWidths = [];\n for (var c = 0; c < colClusters.length; c++) {\n colWidths.push(colAxis.maxes[c] - colClusters[c]);\n }\n var rowHeights = [];\n for (var r = 0; r < rowClusters.length; r++) {\n rowHeights.push(rowAxis.maxes[r] - rowClusters[r]);\n }\n\n console.log('=== LABEL DEBUG ===');\n console.log('Wrapper: x=' + wrapper.x + ', y=' + wrapper.y + ', w=' + wrapper.width + ', h=' + wrapper.height);\n console.log('CS in wrapper: x=' + cs.x + ', y=' + cs.y + ', w=' + cs.width + ', h=' + cs.height);\n console.log('---');\n\n // Column analysis: variant positions vs cell centers\n console.log('Column analysis (' + colClusters.length + ' cols):');\n for (var ci = 0; ci < colClusters.length; ci++) {\n var varLeft = colClusters[ci];\n var varW = colWidths[ci];\n var varCenterX = varLeft + varW / 2;\n\n // Compute visual cell boundaries (midpoints between adjacent column edges, or CS edge)\n var cellLeft, cellRight;\n if (ci === 0) {\n cellLeft = 0;\n } else {\n var prevRight = colClusters[ci - 1] + colWidths[ci - 1];\n cellLeft = (prevRight + colClusters[ci]) / 2;\n }\n if (ci === colClusters.length - 1) {\n cellRight = cs.width;\n } else {\n var nextLeft = colClusters[ci + 1];\n var curRight = colClusters[ci] + colWidths[ci];\n cellRight = (curRight + nextLeft) / 2;\n }\n var cellCenterX = (cellLeft + cellRight) / 2;\n var offset = varCenterX - cellCenterX;\n\n console.log(' Col ' + ci + ': varLeft=' + varLeft + ' varW=' + varW + ' varCenter=' + varCenterX +\n ' | cell=[' + cellLeft.toFixed(1) + ',' + cellRight.toFixed(1) + '] cellCenter=' + cellCenterX.toFixed(1) +\n ' | offset=' + offset.toFixed(1) + (Math.abs(offset) > 1 ? ' ⚠ OFF-CENTER' : ' ✓'));\n }\n\n console.log('---');\n\n // Row analysis\n console.log('Row analysis (' + rowClusters.length + ' rows):');\n for (var ri = 0; ri < rowClusters.length; ri++) {\n var varTop = rowClusters[ri];\n var varH = rowHeights[ri];\n var varCenterY = varTop + varH / 2;\n\n var cellTop, cellBottom;\n if (ri === 0) {\n cellTop = 0;\n } else {\n var prevBottom = rowClusters[ri - 1] + rowHeights[ri - 1];\n cellTop = (prevBottom + rowClusters[ri]) / 2;\n }\n if (ri === rowClusters.length - 1) {\n cellBottom = cs.height;\n } else {\n var nextTop = rowClusters[ri + 1];\n var curBottom = rowClusters[ri] + rowHeights[ri];\n cellBottom = (curBottom + nextTop) / 2;\n }\n var cellCenterY = (cellTop + cellBottom) / 2;\n var offsetY = varCenterY - cellCenterY;\n\n console.log(' Row ' + ri + ': varTop=' + varTop + ' varH=' + varH + ' varCenter=' + varCenterY +\n ' | cell=[' + cellTop.toFixed(1) + ',' + cellBottom.toFixed(1) + '] cellCenter=' + cellCenterY.toFixed(1) +\n ' | offset=' + offsetY.toFixed(1) + (Math.abs(offsetY) > 1 ? ' ⚠ OFF-CENTER' : ' ✓'));\n }\n\n console.log('---');\n\n // Find the container with labels (Documentation frame or wrapper itself)\n var labelContainer = wrapper;\n for (var dfi = 0; dfi < wrapper.children.length; dfi++) {\n if (wrapper.children[dfi].type === 'FRAME' && wrapper.children[dfi].name === 'Documentation') {\n labelContainer = wrapper.children[dfi];\n break;\n }\n }\n\n // All label frames\n var labelFrames = labelContainer.children.filter(function(child) { return child.name === 'Label'; });\n console.log('Label frames (' + labelFrames.length + '):');\n for (var li = 0; li < labelFrames.length; li++) {\n var lf = labelFrames[li];\n var textChild = lf.children.find(function(ch) { return ch.type === 'TEXT'; });\n var textContent = textChild ? textChild.characters : '?';\n var textX = textChild ? textChild.x.toFixed(1) : '?';\n var textW = textChild ? textChild.width.toFixed(1) : '?';\n console.log(' \"' + textContent + '\" frame: x=' + lf.x + ' y=' + lf.y + ' w=' + lf.width + ' h=' + lf.height +\n ' | text: localX=' + textX + ' textW=' + textW);\n }\n\n console.log('=== END LABEL DEBUG ===');\n}\n\nfunction debugBooleanGrid() {\n var cs = getComponentSet();\n if (!cs) {\n console.log('[Boolean Grid Debug] No component set selected');\n return;\n }\n\n var wrapper = findExistingWrapper(cs);\n if (!wrapper) {\n console.log('[Boolean Grid Debug] No wrapper found — generate docs first');\n return;\n }\n\n // Find the Documentation frame or use wrapper directly\n var container = wrapper;\n for (var dfi = 0; dfi < wrapper.children.length; dfi++) {\n if (wrapper.children[dfi].type === 'FRAME' && wrapper.children[dfi].name === 'Documentation') {\n container = wrapper.children[dfi];\n break;\n }\n }\n\n console.log('=== BOOLEAN GRID DEBUG ===');\n console.log('Wrapper: x=' + wrapper.x + ', y=' + wrapper.y + ', w=' + wrapper.width + ', h=' + wrapper.height);\n console.log('CS: x=' + cs.x + ', y=' + cs.y + ', w=' + cs.width + ', h=' + cs.height);\n console.log('CS strokes:', JSON.stringify(cs.strokes));\n console.log('CS strokeWeight:', cs.strokeWeight, 'dashPattern:', JSON.stringify(cs.dashPattern));\n\n var savedStrokes = cs.getPluginData('originalStrokes');\n console.log('Saved original strokes:', savedStrokes || '(none)');\n console.log('---');\n\n // Boolean Grid Border\n var borderFrames = container.children.filter(function(child) { return child.name === 'Boolean Grid Border'; });\n if (borderFrames.length > 0) {\n for (var bi = 0; bi < borderFrames.length; bi++) {\n var bf = borderFrames[bi];\n console.log('Boolean Grid Border:');\n console.log(' position: x=' + bf.x + ', y=' + bf.y);\n console.log(' size: w=' + bf.width + ', h=' + bf.height);\n console.log(' strokes:', JSON.stringify(bf.strokes));\n console.log(' strokeWeight:', bf.strokeWeight, 'dashPattern:', JSON.stringify(bf.dashPattern));\n console.log(' strokeAlign:', bf.strokeAlign, 'cornerRadius:', bf.cornerRadius);\n console.log(' locked:', bf.locked);\n console.log(' fills:', JSON.stringify(bf.fills));\n console.log(' clipsContent:', bf.clipsContent);\n // Check coverage\n console.log(' covers CS: x=' + bf.x + '==' + cs.x + '? ' + (bf.x === cs.x) +\n ' y=' + bf.y + '==' + cs.y + '? ' + (bf.y === cs.y) +\n ' w=' + bf.width + '==' + cs.width + '? ' + (bf.width === cs.width));\n var borderBottom = bf.y + bf.height;\n var wrapperBottom = wrapper.height;\n console.log(' border bottom: ' + borderBottom + ' wrapper bottom: ' + wrapperBottom + ' match: ' + (Math.abs(borderBottom - wrapperBottom) < 1));\n }\n } else {\n console.log('Boolean Grid Border: NOT FOUND');\n }\n\n console.log('---');\n\n // Boolean Tints\n var tints = container.children.filter(function(child) { return child.name === 'Boolean Tint'; });\n if (tints.length > 0) {\n for (var ti = 0; ti < tints.length; ti++) {\n var t = tints[ti];\n console.log('Boolean Tint ' + ti + ':');\n console.log(' position: x=' + t.x + ', y=' + t.y);\n console.log(' size: w=' + t.width + ', h=' + t.height);\n console.log(' fills:', JSON.stringify(t.fills));\n console.log(' locked:', t.locked);\n var tintBottom = t.y + t.height;\n console.log(' tint bottom: ' + tintBottom);\n }\n } else {\n console.log('Boolean Tints: NOT FOUND');\n }\n\n console.log('---');\n\n // Instances\n var instances = container.children.filter(function(child) { return child.type === 'INSTANCE'; });\n if (instances.length > 0) {\n console.log('Boolean instances (' + instances.length + '):');\n for (var ii = 0; ii < instances.length; ii++) {\n var inst = instances[ii];\n console.log(' ' + inst.name + ': x=' + inst.x + ', y=' + inst.y + ', w=' + inst.width + ', h=' + inst.height +\n ' bottom=' + (inst.y + inst.height));\n }\n } else {\n console.log('Boolean instances: NOT FOUND');\n }\n\n console.log('---');\n\n // Divider lines\n var lines = container.children.filter(function(child) { return child.type === 'LINE'; });\n if (lines.length > 0) {\n console.log('Lines (' + lines.length + '):');\n for (var li = 0; li < lines.length; li++) {\n var line = lines[li];\n console.log(' Line ' + li + ': x=' + line.x + ', y=' + line.y + ', w=' + line.width + ', h=' + line.height +\n ' rotation=' + line.rotation);\n }\n }\n\n // Grid frames\n var grids = container.children.filter(function(child) { return child.name === 'Grid'; });\n if (grids.length > 0) {\n console.log('Grid frames (' + grids.length + '):');\n for (var gi = 0; gi < grids.length; gi++) {\n var g = grids[gi];\n console.log(' Grid ' + gi + ': x=' + g.x + ', y=' + g.y + ', w=' + g.width + ', h=' + g.height);\n }\n }\n\n console.log('=== END BOOLEAN GRID DEBUG ===');\n}\n\nfunction removeOldLabels(wrapper) {\n const toRemove = [];\n\n // Find CS node for stroke restoration\n var csNode = null;\n for (const child of wrapper.children) {\n if (child.type === 'COMPONENT_SET' || child.type === 'COMPONENT') {\n csNode = child;\n break;\n }\n }\n\n // Restore CS strokes if they were saved\n if (csNode) {\n var savedStrokes = csNode.getPluginData('originalStrokes');\n if (savedStrokes) {\n try {\n var strokeData = JSON.parse(savedStrokes);\n csNode.strokes = strokeData.strokes;\n csNode.strokeWeight = strokeData.strokeWeight;\n csNode.dashPattern = strokeData.dashPattern;\n csNode.strokeAlign = strokeData.strokeAlign;\n if (strokeData.cornerRadius !== undefined) csNode.cornerRadius = strokeData.cornerRadius;\n } catch (e) {\n console.log('[removeOldLabels] Error restoring strokes:', e.message);\n }\n }\n var savedFills = csNode.getPluginData('originalFills');\n if (savedFills) {\n try {\n csNode.fills = JSON.parse(savedFills);\n } catch (e) {\n console.log('[removeOldLabels] Error restoring fills:', e.message);\n }\n }\n }\n\n // Check for Documentation frame (new structure) — remove it entirely\n // Also check for individual elements (legacy structure)\n for (const child of wrapper.children) {\n if (child.type === 'FRAME' && child.name === 'Documentation') {\n toRemove.push(child);\n }\n if (child.type === 'FRAME' && (child.name === 'Label' || child.name === 'Bracket' || child.name === 'Grid' || child.name === 'Boolean Visibility' || child.name === 'Nested Instances' || child.name === 'Property Combinations' || child.name === 'Boolean Grid' || child.name === 'Boolean Grid Border')) {\n toRemove.push(child);\n }\n if (child.type === 'RECTANGLE' && child.name === 'Boolean Tint') {\n toRemove.push(child);\n }\n if (child.type === 'TEXT' && child.name === 'Title') {\n toRemove.push(child);\n }\n if (child.type === 'FRAME' && child.name === 'Description') {\n toRemove.push(child);\n }\n if (child.type === 'TEXT' && child.name === 'Documentation Link') {\n toRemove.push(child);\n }\n if (child.type === 'INSTANCE') {\n toRemove.push(child);\n }\n if (child.type === 'LINE') {\n toRemove.push(child);\n }\n }\n for (const node of toRemove) {\n node.remove();\n }\n return toRemove.length;\n}\n\n// ─── Boolean Visibility Section ──────────────────────────────────────────────\n\n// Compute the horizontal gap between columns from variant positions in a component set\nfunction computeVariantColumnGap(variants) {\n if (variants.length < 2) return 40;\n var sorted = variants.slice().sort(function(a, b) { return a.y - b.y || a.x - b.x; });\n var i = 0;\n while (i < sorted.length) {\n var rowY = sorted[i].y;\n var row = [];\n while (i < sorted.length && Math.abs(sorted[i].y - rowY) <= CLUSTER_TOLERANCE) {\n row.push(sorted[i]);\n i++;\n }\n if (row.length >= 2) {\n row.sort(function(a, b) { return a.x - b.x; });\n var gap = row[1].x - (row[0].x + row[0].width);\n if (gap > 0) return Math.round(gap);\n }\n }\n return 40;\n}\n\nfunction computeVariantRowGap(variants) {\n if (variants.length < 2) return 40;\n var sorted = variants.slice().sort(function(a, b) { return a.x - b.x || a.y - b.y; });\n var i = 0;\n while (i < sorted.length) {\n var colX = sorted[i].x;\n var col = [];\n while (i < sorted.length && Math.abs(sorted[i].x - colX) <= CLUSTER_TOLERANCE) {\n col.push(sorted[i]);\n i++;\n }\n if (col.length >= 2) {\n col.sort(function(a, b) { return a.y - b.y; });\n var gap = col[1].y - (col[0].y + col[0].height);\n if (gap > 0) return Math.round(gap);\n }\n }\n return 40;\n}\n\n// Generate all non-empty subsets of an array, sorted by size (singles first, then pairs, etc.)\nfunction getNonEmptySubsets(arr) {\n var result = [];\n var n = arr.length;\n for (var mask = 1; mask < (1 << n); mask++) {\n var subset = [];\n for (var i = 0; i < n; i++) {\n if (mask & (1 << i)) subset.push(arr[i]);\n }\n result.push(subset);\n }\n result.sort(function(a, b) { return a.length - b.length; });\n return result;\n}\n\n// Build boolean groups from properties and combination mode.\n// labelFormat: 'onoff' uses On/Off, anything else uses True/False.\nfunction buildBooleanGroups(boolProps, combinationMode, labelFormat) {\n var subsets;\n if (combinationMode === 'all') {\n subsets = getNonEmptySubsets(boolProps);\n } else if (combinationMode === 'combined' && boolProps.length > 1) {\n subsets = boolProps.map(function(p) { return [p]; });\n subsets.push(boolProps.slice());\n } else {\n subsets = boolProps.map(function(p) { return [p]; });\n }\n var trueLabel = labelFormat === 'onoff' ? 'On' : 'True';\n var falseLabel = labelFormat === 'onoff' ? 'Off' : 'False';\n var groups = [];\n for (var si = 0; si < subsets.length; si++) {\n var subset = subsets[si];\n var labelParts = [];\n var propsToSet = {};\n for (var pi = 0; pi < subset.length; pi++) {\n var nonDefault = !subset[pi].defaultValue;\n labelParts.push(formatLabel(subset[pi].name, nonDefault ? trueLabel : falseLabel));\n propsToSet[subset[pi].key] = nonDefault;\n }\n groups.push({ label: labelParts.join(' + '), props: propsToSet });\n }\n return groups;\n}\n\nasync function createBooleanVisibilitySection(sourceNode, combinationMode, enabledPropNames, displayMode) {\n var boolProps = getBooleanComponentProperties(sourceNode);\n if (enabledPropNames) {\n boolProps = boolProps.filter(function(p) { return enabledPropNames.indexOf(p.name) !== -1; });\n }\n if (boolProps.length === 0) return null;\n\n var standalone = isStandaloneComponent(sourceNode);\n var variants = standalone ? [sourceNode] : sourceNode.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n if (variants.length === 0) return null;\n\n var columns = buildBooleanGroups(boolProps, combinationMode, 'onoff');\n\n if (DEBUG) { console.log('[Boolean Visibility] Found', boolProps.length, 'boolean props, mode:', combinationMode, ', display:', displayMode, ', subsets:', columns.length, ', variants:', variants.length); }\n\n var section = figma.createFrame();\n section.name = 'Boolean Visibility';\n section.fills = [];\n section.clipsContent = false;\n\n var COMBO_GAP = 24;\n var LABEL_INSTANCE_GAP = 6;\n\n var csWidth = sourceNode.width;\n var csHeight = standalone ? variants[0].height : sourceNode.height;\n\n if (displayMode === 'grid') {\n // ─── Grid mode: columns side by side ───\n // First column is \"Default\" (no overrides), then one column per subset\n\n var allColumns = [{ label: 'Default', props: null }].concat(columns);\n var colX = 0;\n var labelHeight = 0;\n\n // First pass: create labels, measure max label height\n var labelNodes = [];\n for (var ci = 0; ci < allColumns.length; ci++) {\n var colLabel = createTextNode(allColumns[ci].label, LABEL_FONT_SIZE);\n labelNodes.push(colLabel);\n if (colLabel.height > labelHeight) labelHeight = colLabel.height;\n }\n labelHeight += LABEL_INSTANCE_GAP;\n\n // Second pass: position columns\n for (var ci2 = 0; ci2 < allColumns.length; ci2++) {\n var col = allColumns[ci2];\n\n // Position label\n labelNodes[ci2].x = colX;\n labelNodes[ci2].y = 0;\n section.appendChild(labelNodes[ci2]);\n\n // Create instances\n for (var vi = 0; vi < variants.length; vi++) {\n try {\n var variant = variants[vi];\n var instance = variant.createInstance();\n if (col.props) instance.setProperties(col.props);\n\n instance.x = colX + (standalone ? 0 : variant.x);\n instance.y = labelHeight + (standalone ? 0 : variant.y);\n section.appendChild(instance);\n } catch (e) {\n console.log('[Boolean Visibility Grid] Error creating instance:', e.message);\n }\n }\n\n colX += csWidth + COMBO_GAP;\n }\n\n var totalWidth = colX > COMBO_GAP ? colX - COMBO_GAP : 10;\n section.resize(totalWidth, labelHeight + csHeight);\n } else if (_autoLayoutExtras) {\n // ─── List mode: vertical stack with auto layout ───\n\n section.layoutMode = 'VERTICAL';\n section.itemSpacing = COMBO_GAP;\n section.counterAxisSizingMode = 'AUTO';\n section.primaryAxisSizingMode = 'AUTO';\n\n var INSTANCE_GAP = standalone ? 40 : computeVariantRowGap(variants);\n var colGap = standalone ? 0 : computeVariantColumnGap(variants);\n\n for (var si2 = 0; si2 < columns.length; si2++) {\n var col2 = columns[si2];\n\n if (DEBUG) { console.log('[Boolean Visibility] Subset ' + si2 + ': \"' + col2.label + '\"'); }\n\n // Combo frame: label + instance rows\n var comboFrame = figma.createFrame();\n comboFrame.name = col2.label;\n comboFrame.fills = [];\n comboFrame.layoutMode = 'VERTICAL';\n comboFrame.itemSpacing = LABEL_INSTANCE_GAP;\n comboFrame.counterAxisSizingMode = 'AUTO';\n comboFrame.primaryAxisSizingMode = 'AUTO';\n\n var label = createTextNode(col2.label, LABEL_FONT_SIZE);\n comboFrame.appendChild(label);\n\n // Create instances, collect with original positions\n var comboInstances = [];\n for (var vi2 = 0; vi2 < variants.length; vi2++) {\n try {\n var variant2 = variants[vi2];\n var instance2 = variant2.createInstance();\n instance2.setProperties(col2.props);\n comboInstances.push({ inst: instance2, origX: standalone ? 0 : variant2.x, origY: standalone ? 0 : variant2.y });\n } catch (e) {\n console.log('[Boolean Visibility] Error creating instance from \"' + variants[vi2].name + '\":', e.message);\n }\n }\n comboInstances.sort(function(a, b) { return a.origY - b.origY || a.origX - b.origX; });\n\n // Rows container\n var rowsContainer = figma.createFrame();\n rowsContainer.name = 'Instances';\n rowsContainer.fills = [];\n rowsContainer.layoutMode = 'VERTICAL';\n rowsContainer.itemSpacing = INSTANCE_GAP;\n rowsContainer.counterAxisSizingMode = 'AUTO';\n rowsContainer.primaryAxisSizingMode = 'AUTO';\n\n var ri = 0;\n while (ri < comboInstances.length) {\n var rowY = comboInstances[ri].origY;\n var rowItems = [];\n while (ri < comboInstances.length && Math.abs(comboInstances[ri].origY - rowY) <= CLUSTER_TOLERANCE) {\n rowItems.push(comboInstances[ri]);\n ri++;\n }\n\n if (rowItems.length === 1) {\n rowsContainer.appendChild(rowItems[0].inst);\n } else {\n rowItems.sort(function(a, b) { return a.origX - b.origX; });\n var rowFrame = figma.createFrame();\n rowFrame.name = 'Row';\n rowFrame.fills = [];\n rowFrame.layoutMode = 'HORIZONTAL';\n rowFrame.itemSpacing = colGap;\n rowFrame.counterAxisSizingMode = 'AUTO';\n rowFrame.primaryAxisSizingMode = 'AUTO';\n for (var rj = 0; rj < rowItems.length; rj++) {\n rowFrame.appendChild(rowItems[rj].inst);\n }\n rowsContainer.appendChild(rowFrame);\n }\n }\n\n comboFrame.appendChild(rowsContainer);\n section.appendChild(comboFrame);\n }\n } else {\n // ─── List mode: vertical stack (manual positioning) ───\n\n var yOffset = 0;\n var maxSectionWidth = 0;\n\n for (var si2 = 0; si2 < columns.length; si2++) {\n var col2 = columns[si2];\n\n if (DEBUG) { console.log('[Boolean Visibility] Subset ' + si2 + ': \"' + col2.label + '\"'); }\n\n var label = createTextNode(col2.label, LABEL_FONT_SIZE);\n label.x = 0;\n label.y = yOffset;\n section.appendChild(label);\n yOffset += label.height + LABEL_INSTANCE_GAP;\n\n var comboInstances = [];\n for (var vi2 = 0; vi2 < variants.length; vi2++) {\n try {\n var variant2 = variants[vi2];\n var instance2 = variant2.createInstance();\n instance2.setProperties(col2.props);\n instance2.x = standalone ? 0 : variant2.x;\n section.appendChild(instance2);\n comboInstances.push({ inst: instance2, origY: standalone ? 0 : variant2.y });\n } catch (e) {\n console.log('[Boolean Visibility] Error creating instance from \"' + variants[vi2].name + '\":', e.message);\n }\n }\n comboInstances.sort(function(a, b) { return a.origY - b.origY; });\n\n var comboHeight = 0;\n var INSTANCE_GAP = standalone ? 40 : computeVariantRowGap(variants);\n var ri = 0;\n while (ri < comboInstances.length) {\n var rowY = comboInstances[ri].origY;\n var rowMaxHeight = 0;\n while (ri < comboInstances.length && Math.abs(comboInstances[ri].origY - rowY) <= CLUSTER_TOLERANCE) {\n comboInstances[ri].inst.y = yOffset + comboHeight;\n if (comboInstances[ri].inst.height > rowMaxHeight) rowMaxHeight = comboInstances[ri].inst.height;\n ri++;\n }\n comboHeight += rowMaxHeight;\n if (ri < comboInstances.length) comboHeight += INSTANCE_GAP;\n }\n\n if (csWidth > maxSectionWidth) maxSectionWidth = csWidth;\n yOffset += (comboHeight > 0 ? comboHeight : csHeight) + COMBO_GAP;\n }\n\n var finalHeight = yOffset > COMBO_GAP ? yOffset - COMBO_GAP : 10;\n section.resize(Math.max(maxSectionWidth, 100), finalHeight);\n }\n\n if (DEBUG) { console.log('[Boolean Visibility] Section size:', section.width + 'x' + section.height); }\n return section;\n}\n\n// ─── Scoped Boolean Visibility Section ────────────────────────────────────────\n\nfunction filterVariantsByProperty(variants, propName, propValue) {\n return variants.filter(function(v) {\n var parts = v.name.split(',');\n for (var i = 0; i < parts.length; i++) {\n var kv = parts[i].trim().split('=');\n if (kv[0].trim() === propName && kv[1].trim() === propValue) return true;\n }\n return false;\n });\n}\n\nfunction filterVariantsByProperties(variants, propValues) {\n return variants.filter(function(v) {\n var parts = v.name.split(',');\n var props = {};\n for (var i = 0; i < parts.length; i++) {\n var kv = parts[i].trim().split('=');\n props[kv[0].trim()] = kv[1].trim();\n }\n for (var key in propValues) {\n if (props[key] !== propValues[key]) return false;\n }\n return true;\n });\n}\n\nasync function createScopedBooleanSection(sourceNode, scopeData, combinationMode, displayMode) {\n var allBoolProps = getBooleanComponentProperties(sourceNode);\n if (allBoolProps.length === 0) return null;\n\n var standalone = isStandaloneComponent(sourceNode);\n var allVariants = standalone ? [sourceNode] : sourceNode.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n if (allVariants.length === 0) return null;\n\n if (DEBUG) {\n console.log('[Scoped Boolean] scope properties:', (scopeData.properties || [scopeData.property]).join(', '), ', groups:', scopeData.groups.length, ', combinationMode:', combinationMode);\n }\n\n var section = figma.createFrame();\n section.name = 'Boolean Visibility';\n section.fills = [];\n section.clipsContent = false;\n\n var GROUP_GAP = 32;\n var COMBO_GAP = 24;\n var LABEL_INSTANCE_GAP = 6;\n var INSTANCE_GAP = standalone ? 40 : computeVariantRowGap(allVariants);\n\n if (_autoLayoutExtras) {\n section.layoutMode = 'VERTICAL';\n section.itemSpacing = GROUP_GAP;\n section.counterAxisSizingMode = 'AUTO';\n section.primaryAxisSizingMode = 'AUTO';\n\n var hasContent = false;\n\n for (var gi = 0; gi < scopeData.groups.length; gi++) {\n var group = scopeData.groups[gi];\n var variants = standalone ? allVariants : (group.values\n ? filterVariantsByProperties(allVariants, group.values)\n : filterVariantsByProperty(allVariants, scopeData.property, group.value));\n if (variants.length === 0) continue;\n var boolProps = allBoolProps.filter(function(p) { return group.booleans.indexOf(p.name) !== -1; });\n if (boolProps.length === 0) continue;\n hasContent = true;\n\n var colGap = standalone ? 0 : computeVariantColumnGap(variants);\n var minX = Infinity, minY = Infinity;\n for (var vi = 0; vi < variants.length; vi++) {\n if (variants[vi].x < minX) minX = variants[vi].x;\n if (variants[vi].y < minY) minY = variants[vi].y;\n }\n\n var groupFrame = figma.createFrame();\n groupFrame.name = group.values ? Object.values(group.values).join(', ') : group.value;\n groupFrame.fills = [];\n groupFrame.layoutMode = 'VERTICAL';\n groupFrame.itemSpacing = LABEL_INSTANCE_GAP;\n groupFrame.counterAxisSizingMode = 'AUTO';\n groupFrame.primaryAxisSizingMode = 'AUTO';\n\n var headerText = group.values ? Object.values(group.values).join(', ') : group.value;\n var groupHeader = createTextNode(headerText, LABEL_FONT_SIZE);\n groupHeader.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.5 }];\n groupFrame.appendChild(groupHeader);\n\n var columns = buildBooleanGroups(boolProps, combinationMode, 'onoff');\n\n var combosContainer = figma.createFrame();\n combosContainer.name = 'Combinations';\n combosContainer.fills = [];\n combosContainer.layoutMode = 'VERTICAL';\n combosContainer.itemSpacing = COMBO_GAP;\n combosContainer.counterAxisSizingMode = 'AUTO';\n combosContainer.primaryAxisSizingMode = 'AUTO';\n\n for (var ci = 0; ci < columns.length; ci++) {\n var col = columns[ci];\n var comboFrame = figma.createFrame();\n comboFrame.name = col.label;\n comboFrame.fills = [];\n comboFrame.layoutMode = 'VERTICAL';\n comboFrame.itemSpacing = LABEL_INSTANCE_GAP;\n comboFrame.counterAxisSizingMode = 'AUTO';\n comboFrame.primaryAxisSizingMode = 'AUTO';\n\n var label = createTextNode(col.label, LABEL_FONT_SIZE);\n comboFrame.appendChild(label);\n\n var scopeInstances = [];\n for (var vi3 = 0; vi3 < variants.length; vi3++) {\n try {\n var variant = variants[vi3];\n var instance = variant.createInstance();\n instance.setProperties(col.props);\n scopeInstances.push({ inst: instance, origX: standalone ? 0 : (variant.x - minX), origY: standalone ? 0 : (variant.y - minY) });\n } catch (e) {\n console.log('[Scoped Boolean] Error creating instance:', e.message);\n }\n }\n scopeInstances.sort(function(a, b) { return a.origY - b.origY || a.origX - b.origX; });\n\n var rowsContainer = figma.createFrame();\n rowsContainer.name = 'Instances';\n rowsContainer.fills = [];\n rowsContainer.layoutMode = 'VERTICAL';\n rowsContainer.itemSpacing = INSTANCE_GAP;\n rowsContainer.counterAxisSizingMode = 'AUTO';\n rowsContainer.primaryAxisSizingMode = 'AUTO';\n\n var sri = 0;\n while (sri < scopeInstances.length) {\n var sRowY = scopeInstances[sri].origY;\n var rowItems = [];\n while (sri < scopeInstances.length && Math.abs(scopeInstances[sri].origY - sRowY) <= CLUSTER_TOLERANCE) {\n rowItems.push(scopeInstances[sri]);\n sri++;\n }\n if (rowItems.length === 1) {\n rowsContainer.appendChild(rowItems[0].inst);\n } else {\n rowItems.sort(function(a, b) { return a.origX - b.origX; });\n var rowFrame = figma.createFrame();\n rowFrame.name = 'Row';\n rowFrame.fills = [];\n rowFrame.layoutMode = 'HORIZONTAL';\n rowFrame.itemSpacing = colGap;\n rowFrame.counterAxisSizingMode = 'AUTO';\n rowFrame.primaryAxisSizingMode = 'AUTO';\n for (var rj = 0; rj < rowItems.length; rj++) {\n rowFrame.appendChild(rowItems[rj].inst);\n }\n rowsContainer.appendChild(rowFrame);\n }\n }\n\n comboFrame.appendChild(rowsContainer);\n combosContainer.appendChild(comboFrame);\n }\n\n groupFrame.appendChild(combosContainer);\n section.appendChild(groupFrame);\n }\n\n if (!hasContent) return null;\n } else {\n // ─── Manual positioning (original behavior) ───\n var yOffset = 0;\n var maxSectionWidth = 0;\n\n for (var gi = 0; gi < scopeData.groups.length; gi++) {\n var group = scopeData.groups[gi];\n var variants = standalone ? allVariants : (group.values\n ? filterVariantsByProperties(allVariants, group.values)\n : filterVariantsByProperty(allVariants, scopeData.property, group.value));\n if (variants.length === 0) continue;\n var boolProps = allBoolProps.filter(function(p) { return group.booleans.indexOf(p.name) !== -1; });\n if (boolProps.length === 0) continue;\n\n var minX = Infinity, minY = Infinity;\n for (var vi = 0; vi < variants.length; vi++) {\n if (variants[vi].x < minX) minX = variants[vi].x;\n if (variants[vi].y < minY) minY = variants[vi].y;\n }\n var groupWidth = 0, groupHeight = 0;\n for (var vi2 = 0; vi2 < variants.length; vi2++) {\n var extX = (variants[vi2].x - minX) + variants[vi2].width;\n var extY = (variants[vi2].y - minY) + variants[vi2].height;\n if (extX > groupWidth) groupWidth = extX;\n if (extY > groupHeight) groupHeight = extY;\n }\n\n var headerText = group.values ? Object.values(group.values).join(', ') : group.value;\n var groupHeader = createTextNode(headerText, LABEL_FONT_SIZE);\n groupHeader.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.5 }];\n groupHeader.x = 0;\n groupHeader.y = yOffset;\n section.appendChild(groupHeader);\n yOffset += groupHeader.height + LABEL_INSTANCE_GAP;\n\n var columns = buildBooleanGroups(boolProps, combinationMode, 'onoff');\n\n for (var ci = 0; ci < columns.length; ci++) {\n var col = columns[ci];\n var label = createTextNode(col.label, LABEL_FONT_SIZE);\n label.x = 0;\n label.y = yOffset;\n section.appendChild(label);\n yOffset += label.height + LABEL_INSTANCE_GAP;\n\n var scopeInstances = [];\n for (var vi3 = 0; vi3 < variants.length; vi3++) {\n try {\n var variant = variants[vi3];\n var instance = variant.createInstance();\n instance.setProperties(col.props);\n instance.x = standalone ? 0 : (variant.x - minX);\n section.appendChild(instance);\n scopeInstances.push({ inst: instance, origY: standalone ? 0 : (variant.y - minY) });\n } catch (e) {\n console.log('[Scoped Boolean] Error creating instance:', e.message);\n }\n }\n scopeInstances.sort(function(a, b) { return a.origY - b.origY; });\n\n var scopeComboH = 0;\n var sri = 0;\n while (sri < scopeInstances.length) {\n var sRowY = scopeInstances[sri].origY;\n var sRowMaxH = 0;\n while (sri < scopeInstances.length && Math.abs(scopeInstances[sri].origY - sRowY) <= CLUSTER_TOLERANCE) {\n scopeInstances[sri].inst.y = yOffset + scopeComboH;\n if (scopeInstances[sri].inst.height > sRowMaxH) sRowMaxH = scopeInstances[sri].inst.height;\n sri++;\n }\n scopeComboH += sRowMaxH;\n if (sri < scopeInstances.length) scopeComboH += INSTANCE_GAP;\n }\n\n if (groupWidth > maxSectionWidth) maxSectionWidth = groupWidth;\n yOffset += (scopeComboH > 0 ? scopeComboH : groupHeight) + COMBO_GAP;\n }\n\n if (gi < scopeData.groups.length - 1) {\n yOffset += GROUP_GAP - COMBO_GAP;\n }\n }\n\n if (yOffset === 0) return null;\n var finalHeight = yOffset > COMBO_GAP ? yOffset - COMBO_GAP : 10;\n section.resize(Math.max(maxSectionWidth, 100), finalHeight);\n }\n\n if (DEBUG) { console.log('[Scoped Boolean] Section size:', section.width + 'x' + section.height); }\n return section;\n}\n\n// ─── Nested Instances Section ────────────────────────────────────────────────\n\nasync function createNestedInstancesSection(sourceNode, mode, enabledNames) {\n var standalone = isStandaloneComponent(sourceNode);\n var nestedSets = standalone\n ? await findNestedComponentSetsForComponent(sourceNode)\n : await findNestedComponentSets(sourceNode);\n if (enabledNames) {\n nestedSets = nestedSets.filter(function(ns) { return enabledNames.indexOf(ns.componentSetName) !== -1; });\n }\n if (nestedSets.length === 0) return null;\n\n var variants = standalone ? [sourceNode] : sourceNode.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n if (variants.length === 0) return null;\n\n if (DEBUG) {\n console.log('[Nested Instances] mode:', mode, ', nested sets:', nestedSets.length, ', standalone:', standalone);\n }\n\n var section = figma.createFrame();\n section.name = 'Nested Instances';\n section.fills = [];\n section.clipsContent = false;\n\n var COMBO_GAP = 24;\n var LABEL_INSTANCE_GAP = 6;\n var PROP_GAP = 32;\n\n // Pre-compute boolean property overrides for parent instances\n var boolProps = getBooleanComponentProperties(sourceNode);\n var boolOverrides = {};\n for (var bp = 0; bp < boolProps.length; bp++) {\n boolOverrides[boolProps[bp].key] = true;\n }\n var hasBoolOverrides = boolProps.length > 0;\n\n if (_autoLayoutExtras) {\n section.layoutMode = 'VERTICAL';\n section.itemSpacing = PROP_GAP;\n section.counterAxisSizingMode = 'AUTO';\n section.primaryAxisSizingMode = 'AUTO';\n\n for (var ni = 0; ni < nestedSets.length; ni++) {\n var nested = nestedSets[ni];\n\n var setFrame = figma.createFrame();\n setFrame.name = nested.componentSetName;\n setFrame.fills = [];\n setFrame.layoutMode = 'VERTICAL';\n setFrame.itemSpacing = LABEL_INSTANCE_GAP;\n setFrame.counterAxisSizingMode = 'AUTO';\n setFrame.primaryAxisSizingMode = 'AUTO';\n\n var csHeader = createTextNode(nested.componentSetName, LABEL_FONT_SIZE);\n csHeader.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.5 }];\n setFrame.appendChild(csHeader);\n\n var nestedGroupProps = nested.variantGroupProperties;\n var defaultProps = nested.defaultVariantProperties;\n var propNames = Object.keys(nestedGroupProps);\n\n if (DEBUG) {\n console.log('[Nested Instances] \"' + nested.componentSetName + '\": ' + propNames.length + ' properties, defaults:', JSON.stringify(defaultProps));\n }\n\n var propsContainer = figma.createFrame();\n propsContainer.name = 'Properties';\n propsContainer.fills = [];\n propsContainer.layoutMode = 'VERTICAL';\n propsContainer.itemSpacing = PROP_GAP;\n propsContainer.counterAxisSizingMode = 'AUTO';\n propsContainer.primaryAxisSizingMode = 'AUTO';\n\n for (var pi = 0; pi < propNames.length; pi++) {\n var propName = propNames[pi];\n var propValues = nestedGroupProps[propName];\n var defaultValue = defaultProps[propName];\n\n var propFrame = figma.createFrame();\n propFrame.name = propName;\n propFrame.fills = [];\n propFrame.layoutMode = 'VERTICAL';\n propFrame.itemSpacing = LABEL_INSTANCE_GAP;\n propFrame.counterAxisSizingMode = 'AUTO';\n propFrame.primaryAxisSizingMode = 'AUTO';\n\n var propHeader = createTextNode(propName, LABEL_FONT_SIZE);\n propHeader.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.35 }];\n propFrame.appendChild(propHeader);\n\n var valuesContainer = figma.createFrame();\n valuesContainer.name = 'Values';\n valuesContainer.fills = [];\n valuesContainer.layoutMode = 'VERTICAL';\n valuesContainer.itemSpacing = COMBO_GAP;\n valuesContainer.counterAxisSizingMode = 'AUTO';\n valuesContainer.primaryAxisSizingMode = 'AUTO';\n\n for (var vi2 = 0; vi2 < propValues.length; vi2++) {\n var value = propValues[vi2];\n if (value === defaultValue) continue;\n\n var targetProps = {};\n for (var key in defaultProps) {\n if (defaultProps.hasOwnProperty(key)) targetProps[key] = defaultProps[key];\n }\n targetProps[propName] = value;\n\n var swapTarget = findVariantByProperties(nested.variants, targetProps);\n if (!swapTarget) continue;\n\n var targetComponent = await figma.getNodeByIdAsync(swapTarget.id);\n if (!targetComponent) continue;\n\n var displayLabel = formatLabel(propName, value);\n var valueFrame = figma.createFrame();\n valueFrame.name = displayLabel;\n valueFrame.fills = [];\n valueFrame.layoutMode = 'VERTICAL';\n valueFrame.itemSpacing = LABEL_INSTANCE_GAP;\n valueFrame.counterAxisSizingMode = 'AUTO';\n valueFrame.primaryAxisSizingMode = 'AUTO';\n\n var label = createTextNode(displayLabel, LABEL_FONT_SIZE);\n valueFrame.appendChild(label);\n\n if (mode === 'representative') {\n var baseIdx = (nested.parentVariantIndices && nested.parentVariantIndices.length > 0)\n ? nested.parentVariantIndices[0] : 0;\n try {\n var instance = variants[baseIdx].createInstance();\n if (hasBoolOverrides) instance.setProperties(boolOverrides);\n var nestedInst = findNestedInstanceByName(instance, nested.instanceName);\n if (nestedInst) nestedInst.swapComponent(targetComponent);\n valueFrame.appendChild(instance);\n } catch (e) {\n console.log('[Nested Instances] Error creating representative instance:', e.message);\n }\n } else {\n var fullIndices = [];\n if (nested.parentVariantIndices && nested.parentVariantIndices.length > 0) {\n fullIndices = nested.parentVariantIndices.slice();\n } else {\n for (var ai = 0; ai < variants.length; ai++) fullIndices.push(ai);\n }\n\n var instancesContainer = figma.createFrame();\n instancesContainer.name = 'Instances';\n instancesContainer.fills = [];\n instancesContainer.layoutMode = 'VERTICAL';\n instancesContainer.itemSpacing = COMBO_GAP;\n instancesContainer.counterAxisSizingMode = 'AUTO';\n instancesContainer.primaryAxisSizingMode = 'AUTO';\n\n for (var fi2 = 0; fi2 < fullIndices.length; fi2++) {\n try {\n var variant = variants[fullIndices[fi2]];\n var inst = variant.createInstance();\n if (hasBoolOverrides) inst.setProperties(boolOverrides);\n var nestedChild = findNestedInstanceByName(inst, nested.instanceName);\n if (nestedChild) nestedChild.swapComponent(targetComponent);\n instancesContainer.appendChild(inst);\n } catch (e) {\n console.log('[Nested Instances] Error creating instance from \"' + variants[fullIndices[fi2]].name + '\":', e.message);\n }\n }\n\n valueFrame.appendChild(instancesContainer);\n }\n\n valuesContainer.appendChild(valueFrame);\n }\n\n propFrame.appendChild(valuesContainer);\n propsContainer.appendChild(propFrame);\n }\n\n setFrame.appendChild(propsContainer);\n section.appendChild(setFrame);\n }\n\n if (section.children.length === 0) return null;\n } else {\n // ─── Manual positioning (original behavior) ───\n var csWidth = sourceNode.width;\n var csHeight = standalone ? variants[0].height : sourceNode.height;\n var yOffset = 0;\n var maxSectionWidth = 0;\n\n for (var ni = 0; ni < nestedSets.length; ni++) {\n var nested = nestedSets[ni];\n\n var csHeader = createTextNode(nested.componentSetName, LABEL_FONT_SIZE);\n csHeader.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.5 }];\n csHeader.x = 0;\n csHeader.y = yOffset;\n section.appendChild(csHeader);\n yOffset += csHeader.height + LABEL_INSTANCE_GAP;\n\n var nestedGroupProps = nested.variantGroupProperties;\n var defaultProps = nested.defaultVariantProperties;\n var propNames = Object.keys(nestedGroupProps);\n\n if (DEBUG) {\n console.log('[Nested Instances] \"' + nested.componentSetName + '\": ' + propNames.length + ' properties, defaults:', JSON.stringify(defaultProps));\n }\n\n for (var pi = 0; pi < propNames.length; pi++) {\n var propName = propNames[pi];\n var propValues = nestedGroupProps[propName];\n var defaultValue = defaultProps[propName];\n\n var propHeader = createTextNode(propName, LABEL_FONT_SIZE);\n propHeader.fills = [{ type: 'SOLID', color: DOC_COLOR, opacity: 0.35 }];\n propHeader.x = 0;\n propHeader.y = yOffset;\n section.appendChild(propHeader);\n yOffset += propHeader.height + LABEL_INSTANCE_GAP;\n\n for (var vi2 = 0; vi2 < propValues.length; vi2++) {\n var value = propValues[vi2];\n if (value === defaultValue) continue;\n\n var targetProps = {};\n for (var key in defaultProps) {\n if (defaultProps.hasOwnProperty(key)) targetProps[key] = defaultProps[key];\n }\n targetProps[propName] = value;\n\n var swapTarget = findVariantByProperties(nested.variants, targetProps);\n if (!swapTarget) continue;\n\n var displayLabel = formatLabel(propName, value);\n var label = createTextNode(displayLabel, LABEL_FONT_SIZE);\n label.x = 0;\n label.y = yOffset;\n section.appendChild(label);\n yOffset += label.height + LABEL_INSTANCE_GAP;\n\n var targetComponent = await figma.getNodeByIdAsync(swapTarget.id);\n if (!targetComponent) continue;\n\n if (mode === 'representative') {\n var indicesToUse = [];\n if (nested.parentVariantIndices && nested.parentVariantIndices.length > 0) {\n indicesToUse.push(nested.parentVariantIndices[0]);\n } else {\n indicesToUse.push(0);\n }\n\n for (var bvi = 0; bvi < indicesToUse.length; bvi++) {\n try {\n var baseVariantIdx = indicesToUse[bvi];\n var instance = variants[baseVariantIdx].createInstance();\n if (hasBoolOverrides) instance.setProperties(boolOverrides);\n var nestedInst = findNestedInstanceByName(instance, nested.instanceName);\n if (nestedInst) nestedInst.swapComponent(targetComponent);\n instance.x = 0;\n instance.y = yOffset;\n section.appendChild(instance);\n if (instance.width > maxSectionWidth) maxSectionWidth = instance.width;\n yOffset += instance.height + COMBO_GAP;\n } catch (e) {\n console.log('[Nested Instances] Error creating representative instance:', e.message);\n }\n }\n } else {\n var fullIndices = [];\n if (nested.parentVariantIndices && nested.parentVariantIndices.length > 0) {\n fullIndices = nested.parentVariantIndices.slice();\n } else {\n for (var ai = 0; ai < variants.length; ai++) fullIndices.push(ai);\n }\n var comboY = yOffset;\n for (var fi2 = 0; fi2 < fullIndices.length; fi2++) {\n try {\n var variant = variants[fullIndices[fi2]];\n var inst = variant.createInstance();\n if (hasBoolOverrides) inst.setProperties(boolOverrides);\n var nestedChild = findNestedInstanceByName(inst, nested.instanceName);\n if (nestedChild) nestedChild.swapComponent(targetComponent);\n inst.x = standalone ? 0 : variant.x;\n inst.y = comboY;\n section.appendChild(inst);\n if (inst.width > maxSectionWidth) maxSectionWidth = inst.width;\n comboY += inst.height + COMBO_GAP;\n } catch (e) {\n console.log('[Nested Instances] Error creating instance from \"' + variants[fullIndices[fi2]].name + '\":', e.message);\n }\n }\n yOffset = comboY;\n }\n }\n\n if (pi < propNames.length - 1) {\n yOffset += PROP_GAP - COMBO_GAP;\n }\n }\n\n if (ni < nestedSets.length - 1) {\n yOffset += PROP_GAP - COMBO_GAP;\n }\n }\n\n var finalHeight = yOffset > COMBO_GAP ? yOffset - COMBO_GAP : 10;\n section.resize(Math.max(maxSectionWidth, 100), finalHeight);\n }\n\n if (DEBUG) { console.log('[Nested Instances] Section size:', section.width + 'x' + section.height); }\n return section;\n}\n\n// Find a nested INSTANCE node by layer name (recursive)\nfunction findNestedInstanceByName(parent, name) {\n if (!('children' in parent)) return null;\n for (var i = 0; i < parent.children.length; i++) {\n var child = parent.children[i];\n if (child.type === 'INSTANCE' && child.name === name) return child;\n var found = findNestedInstanceByName(child, name);\n if (found) return found;\n }\n return null;\n}\n\n// ─── Main Generation (Step 6) ────────────────────────────────────────────────\n\nasync function generateForStandaloneComponent(component, options) {\n var t0 = Date.now();\n if (DEBUG) { console.log('[Generate Standalone] Options:', JSON.stringify(options)); }\n\n // Save settings on the component for later restoration\n saveComponentSettings(component, options);\n\n // Set doc color\n if (options.color) {\n DOC_COLOR = hexToRgb(options.color);\n } else {\n DOC_COLOR = { r: DEFAULT_COLOR.r, g: DEFAULT_COLOR.g, b: DEFAULT_COLOR.b };\n }\n\n _hidePropertyNames = options.hidePropertyNames || false;\n _autoLayoutExtras = options.autoLayoutExtras || false;\n\n if (!_fontLoaded && _fontLoadPromise) {\n figma.ui.postMessage({ type: 'status', message: 'Loading fonts...' });\n await _fontLoadPromise;\n }\n if (!_fontLoaded) throw new Error('Inter font not available. Try restarting Figma.');\n if (options.fontFamily) await loadLabelFont(options.fontFamily);\n initMeasureNode();\n\n // Create or reuse wrapper\n var wrapper = findExistingWrapper(component);\n var originalParent = component.parent;\n var originalX = component.x;\n var originalY = component.y;\n\n if (wrapper) {\n removeOldLabels(wrapper);\n } else {\n wrapper = figma.createFrame();\n wrapper.name = '❖ ' + component.name + (options.docLabel ? ' — ' + options.docLabel : ' — Obra Autodocs');\n wrapper.setPluginData('sourceComponentSetId', component.id);\n wrapper.fills = [];\n wrapper.clipsContent = false;\n\n var compIndex = originalParent.children.indexOf(component);\n originalParent.insertChild(compIndex, wrapper);\n wrapper.x = originalX;\n wrapper.y = originalY;\n\n wrapper.appendChild(component);\n }\n\n // ─── Boolean grid mode for standalone ───\n\n var standaloneBoolGrid = null;\n var sBoolLabelWidth = 0;\n var sBoolLabelHeight = 0;\n var SBOOL_GROUP_GAP = 24; // updated below for single-boolean cases\n\n if (options.showBooleanVisibility && (options.booleanDisplayMode || 'list') === 'grid') {\n var sBoolProps = getBooleanComponentProperties(component);\n if (options.enabledBooleanProps) {\n sBoolProps = sBoolProps.filter(function(p) { return options.enabledBooleanProps.indexOf(p.name) !== -1; });\n }\n if (sBoolProps.length > 0) {\n var sBoolAxis = determineBooleanAxis(component.width, component.height);\n var sBoolGroups = buildBooleanGroups(sBoolProps, options.booleanCombination || 'individual', 'truefalse');\n standaloneBoolGrid = {\n axis: sBoolAxis,\n defaultLabel: 'Base',\n groups: sBoolGroups\n };\n // Single-boolean: no gap (tint sits flush against divider line), multi-boolean: full gap for double-line dividers\n SBOOL_GROUP_GAP = sBoolProps.length > 1 ? 24 : 0;\n\n // Calculate boolean label dimensions\n if (sBoolAxis === 'row') {\n var sMaxBoolTextWidth = measureTextWidth(standaloneBoolGrid.defaultLabel, LABEL_FONT_SIZE);\n for (var sbli = 0; sbli < sBoolGroups.length; sbli++) {\n var sblw = measureTextWidth(sBoolGroups[sbli].label, LABEL_FONT_SIZE);\n if (sblw > sMaxBoolTextWidth) sMaxBoolTextWidth = sblw;\n }\n sBoolLabelWidth = Math.ceil(sMaxBoolTextWidth + LABEL_GAP * 2);\n } else {\n sBoolLabelHeight = SIMPLE_LABEL_ROW_HEIGHT;\n }\n }\n }\n\n // Reserve space for title + description + doc link\n var sTitleOffset = 0;\n var sDescOffset = 0;\n var sDocLinkOffset = 0;\n var sHeaderDescFrame = null;\n var sHeaderDocLinkText = null;\n if (options.showTitle) {\n sTitleOffset = TITLE_FONT_SIZE + TITLE_GAP;\n }\n var sDescText = component.descriptionMarkdown || component.description || '';\n if (options.showDescription && sDescText.trim()) {\n sHeaderDescFrame = createDescriptionFrame(sDescText, Math.max(component.width, 240));\n sDescOffset = sHeaderDescFrame.height + DESC_GAP;\n }\n var sDocLinkUri = getDocLink(component);\n if (options.showDocLink && sDocLinkUri) {\n sHeaderDocLinkText = createDocLinkText(sDocLinkUri);\n sDocLinkOffset = DOC_LINK_FONT_SIZE + DOC_LINK_GAP;\n }\n var sHeaderOffset = sTitleOffset + sDescOffset + sDocLinkOffset;\n sBoolLabelHeight += sHeaderOffset;\n\n // Position component (offset by boolean label area)\n component.constraints = { horizontal: 'MIN', vertical: 'MIN' };\n component.x = sBoolLabelWidth;\n component.y = sBoolLabelHeight;\n\n // Calculate expanded dimensions\n var sExpandedWidth = component.width;\n var sExpandedHeight = component.height;\n if (standaloneBoolGrid) {\n var sNumNonDefault = standaloneBoolGrid.groups.length;\n if (standaloneBoolGrid.axis === 'row') {\n sExpandedHeight = component.height + sNumNonDefault * (component.height + SBOOL_GROUP_GAP);\n } else {\n sExpandedWidth = component.width + sNumNonDefault * (component.width + SBOOL_GROUP_GAP);\n }\n }\n\n wrapper.resize(sBoolLabelWidth + sExpandedWidth, sBoolLabelHeight + sExpandedHeight);\n\n // ─── Header: title + description (standalone component) ───\n\n var scHeaderY = 0;\n if (options.showTitle && sTitleOffset > 0) {\n var scTitleText = createTitleText(component.name);\n scTitleText.x = sBoolLabelWidth;\n scTitleText.y = scHeaderY;\n wrapper.appendChild(scTitleText);\n scHeaderY += sTitleOffset;\n }\n if (sHeaderDescFrame) {\n sHeaderDescFrame.x = sBoolLabelWidth;\n sHeaderDescFrame.y = scHeaderY;\n wrapper.appendChild(sHeaderDescFrame);\n scHeaderY += sDescOffset;\n }\n if (sHeaderDocLinkText) {\n sHeaderDocLinkText.x = sBoolLabelWidth;\n sHeaderDocLinkText.y = scHeaderY;\n wrapper.appendChild(sHeaderDocLinkText);\n }\n\n // Create boolean grid instances + tints + labels\n if (standaloneBoolGrid) {\n for (var sbgi = 0; sbgi < standaloneBoolGrid.groups.length; sbgi++) {\n var sbGroup = standaloneBoolGrid.groups[sbgi];\n var sbGroupIndex = sbgi + 1;\n var sbOffsetX, sbOffsetY;\n\n if (standaloneBoolGrid.axis === 'row') {\n sbOffsetX = sBoolLabelWidth;\n sbOffsetY = sBoolLabelHeight + sbGroupIndex * (component.height + SBOOL_GROUP_GAP);\n } else {\n sbOffsetX = sBoolLabelWidth + sbGroupIndex * (component.width + SBOOL_GROUP_GAP);\n sbOffsetY = sBoolLabelHeight;\n }\n\n var sTint = createBackgroundTint(sbOffsetX, sbOffsetY, component.width, component.height);\n wrapper.appendChild(sTint);\n\n var sInstance = component.createInstance();\n sInstance.setProperties(sbGroup.props);\n sInstance.x = sbOffsetX;\n sInstance.y = sbOffsetY;\n wrapper.appendChild(sInstance);\n }\n\n // Boolean labels\n if (standaloneBoolGrid.axis === 'row') {\n var sDefLabel = createRowLabel(standaloneBoolGrid.defaultLabel, 0, sBoolLabelHeight, sBoolLabelWidth, component.height, false);\n wrapper.appendChild(sDefLabel);\n for (var sbli = 0; sbli < standaloneBoolGrid.groups.length; sbli++) {\n var sbLabelY = sBoolLabelHeight + (sbli + 1) * (component.height + SBOOL_GROUP_GAP);\n var sbLabel = createRowLabel(standaloneBoolGrid.groups[sbli].label, 0, sbLabelY, sBoolLabelWidth, component.height, false);\n wrapper.appendChild(sbLabel);\n }\n } else {\n var sDefLabel = createColLabel(standaloneBoolGrid.defaultLabel, sBoolLabelWidth, 0, component.width, sBoolLabelHeight, false);\n wrapper.appendChild(sDefLabel);\n for (var sbli = 0; sbli < standaloneBoolGrid.groups.length; sbli++) {\n var sbLabelX = sBoolLabelWidth + (sbli + 1) * (component.width + SBOOL_GROUP_GAP);\n var sbLabel = createColLabel(standaloneBoolGrid.groups[sbli].label, sbLabelX, 0, component.width, sBoolLabelHeight, false);\n wrapper.appendChild(sbLabel);\n }\n }\n }\n\n // Build extra sections (boolean list mode + nested)\n var extraSections = [];\n\n if (options.showBooleanVisibility && !standaloneBoolGrid) {\n figma.ui.postMessage({ type: 'status', message: 'Creating boolean visibility examples...' });\n var boolSection;\n if (options.booleanScope) {\n boolSection = await createScopedBooleanSection(component, options.booleanScope, options.booleanCombination || 'individual', options.booleanDisplayMode || 'list');\n } else {\n boolSection = await createBooleanVisibilitySection(component, options.booleanCombination || 'individual', options.enabledBooleanProps, options.booleanDisplayMode || 'list');\n }\n if (boolSection) extraSections.push(boolSection);\n }\n\n if (options.showNestedInstances) {\n figma.ui.postMessage({ type: 'status', message: 'Creating nested instance examples...' });\n var nestedSection = await createNestedInstancesSection(component, options.nestedInstancesMode || 'representative', options.enabledNestedInstances);\n if (nestedSection) extraSections.push(nestedSection);\n }\n\n if (extraSections.length > 0) {\n var EXTRAS_GAP = 24;\n var extrasContainer = figma.createFrame();\n extrasContainer.name = 'Property Combinations';\n extrasContainer.fills = [];\n extrasContainer.clipsContent = false;\n extrasContainer.layoutMode = 'VERTICAL';\n extrasContainer.itemSpacing = EXTRAS_GAP;\n extrasContainer.counterAxisSizingMode = 'AUTO';\n extrasContainer.primaryAxisSizingMode = 'AUTO';\n\n for (var i = 0; i < extraSections.length; i++) {\n extrasContainer.appendChild(extraSections[i]);\n }\n\n if (_autoLayoutExtras) {\n extrasContainer.paddingLeft = sBoolLabelWidth;\n } else {\n extrasContainer.x = sBoolLabelWidth;\n extrasContainer.y = sBoolLabelHeight + sExpandedHeight + EXTRAS_GAP;\n }\n wrapper.appendChild(extrasContainer);\n\n if (!_autoLayoutExtras) {\n wrapper.resize(\n Math.max(wrapper.width, sBoolLabelWidth + extrasContainer.width),\n extrasContainer.y + extrasContainer.height\n );\n }\n }\n\n if (_autoLayoutExtras) {\n // ─── Wrap in Documentation frame + make wrapper auto layout ───\n\n var scDocsFrame = figma.createFrame();\n scDocsFrame.name = 'Documentation';\n scDocsFrame.fills = [];\n scDocsFrame.clipsContent = false;\n scDocsFrame.locked = false;\n scDocsFrame.resize(sBoolLabelWidth + sExpandedWidth, sBoolLabelHeight + sExpandedHeight);\n wrapper.appendChild(scDocsFrame);\n\n var scDocsChildren = [];\n for (var scdi = 0; scdi < wrapper.children.length; scdi++) {\n var scChild = wrapper.children[scdi];\n if (scChild !== scDocsFrame && scChild.name !== 'Property Combinations') {\n scDocsChildren.push(scChild);\n }\n }\n for (var scdi2 = 0; scdi2 < scDocsChildren.length; scdi2++) {\n scDocsChildren[scdi2].constraints = { horizontal: 'MIN', vertical: 'MIN' };\n scDocsFrame.appendChild(scDocsChildren[scdi2]);\n }\n\n var scExtrasInWrapper = null;\n for (var scewi = 0; scewi < wrapper.children.length; scewi++) {\n if (wrapper.children[scewi].name === 'Property Combinations') {\n scExtrasInWrapper = wrapper.children[scewi];\n break;\n }\n }\n if (scExtrasInWrapper) {\n wrapper.appendChild(scExtrasInWrapper);\n }\n\n wrapper.layoutMode = 'VERTICAL';\n wrapper.primaryAxisSizingMode = 'AUTO';\n wrapper.counterAxisSizingMode = 'AUTO';\n wrapper.itemSpacing = 24;\n wrapper.paddingTop = 0;\n wrapper.paddingBottom = 0;\n wrapper.paddingLeft = 0;\n wrapper.paddingRight = 0;\n }\n\n disposeMeasureNode();\n\n var totalMs = Date.now() - t0;\n var totalSec = (totalMs / 1000).toFixed(2);\n console.log('[Perf] === Standalone generation time: ' + totalMs + 'ms (' + totalSec + 's) ===');\n\n figma.currentPage.selection = [wrapper];\n figma.viewport.scrollAndZoomIntoView([wrapper]);\n\n var doneMsg = 'Generated docs in ' + totalSec + 's.';\n figma.notify(doneMsg);\n figma.ui.postMessage({ type: 'done', message: doneMsg });\n}\n\nasync function generate(options = {}) {\n var t0 = Date.now();\n if (DEBUG) { console.log('[Generate] Options received:', JSON.stringify(options)); }\n const cs = getComponentSet();\n if (!cs) {\n var standalone = getStandaloneComponent();\n if (standalone) {\n return generateForStandaloneComponent(standalone, options);\n }\n figma.ui.postMessage({ type: 'error', message: 'Please select a component set or component.' });\n return;\n }\n\n // Variable modes no longer force standalone — they attach to the regular wrapper\n\n // Save settings on the component set for later restoration\n saveComponentSettings(cs, options);\n\n // Set doc color from options\n if (options.color) {\n DOC_COLOR = hexToRgb(options.color);\n } else {\n DOC_COLOR = { r: DEFAULT_COLOR.r, g: DEFAULT_COLOR.g, b: DEFAULT_COLOR.b };\n }\n\n _hidePropertyNames = options.hidePropertyNames || false;\n _autoLayoutExtras = options.autoLayoutExtras || false;\n\n if (!_fontLoaded && _fontLoadPromise) {\n figma.ui.postMessage({ type: 'status', message: 'Loading fonts...' });\n await _fontLoadPromise;\n }\n if (!_fontLoaded) throw new Error('Inter font not available. Try restarting Figma.');\n if (options.fontFamily) await loadLabelFont(options.fontFamily);\n console.log('[Perf] Font loaded:', (Date.now() - t0) + 'ms');\n\n // Init reusable text measurement node\n initMeasureNode();\n\n // Get enum properties\n const enumProps = getEnumProperties(cs, options);\n if (DEBUG) { console.log('[Generate] properties:', enumProps.map(function(p) { return p.name; })); }\n\n var layout, grid, gridByRow, gridByCol, colClusters, rowClusters, colAxisProps, rowAxisProps, variants;\n\n if (enumProps.length === 0) {\n // No enum properties at all — wrap without labels\n variants = cs.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n grid = [];\n gridByRow = {};\n gridByCol = {};\n colClusters = [];\n rowClusters = [];\n colAxisProps = [];\n rowAxisProps = [];\n } else {\n figma.ui.postMessage({ type: 'status', message: 'Analyzing layout...' });\n\n // Detect variants with absolute-positioned overflow (e.g. dropdown menus, tooltips)\n // before layout analysis so effectiveSize() uses visual sizes for clustering\n var overflowInfo = detectComponentSetOverflow(cs);\n if (overflowInfo.hasOverflow) {\n console.log('[Overflow] Accommodating absolute-positioned overflow in grid layout.');\n console.log('[Overflow] Affected: ' + overflowInfo.affectedVariants.length + ' variant(s), max overflow: bottom +' + Math.round(overflowInfo.maxOverflowBottom) + 'px, right +' + Math.round(overflowInfo.maxOverflowRight) + 'px');\n // Build per-variant visual size map: frame size + overflow\n _visualSizeOverrides = {};\n var ovVariants = cs.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n for (var ovi = 0; ovi < ovVariants.length; ovi++) {\n var ov = detectVariantOverflow(ovVariants[ovi]);\n _visualSizeOverrides[ovVariants[ovi].id] = {\n width: ovVariants[ovi].width + Math.max(0, ov.overflowRight),\n height: ovVariants[ovi].height + Math.max(0, ov.overflowBottom)\n };\n }\n figma.notify('Overflow detected: grid adjusted for absolute-positioned elements. Organize tab may not work for this component.', { timeout: 6000 });\n }\n\n // Analyze layout (uses visual sizes via effectiveSize() when overflow is present)\n var tLayout = Date.now();\n layout = analyzeLayout(cs, enumProps, options.gridAlignment || 'auto', options.allowSpanning || false);\n if (DEBUG) { console.log('[Perf] Layout analysis:', (Date.now() - tLayout) + 'ms'); }\n\n // Clear visual size overrides after layout analysis\n _visualSizeOverrides = null;\n\n grid = layout.grid;\n gridByRow = layout.gridByRow;\n gridByCol = layout.gridByCol;\n colClusters = layout.colClusters;\n rowClusters = layout.rowClusters;\n colAxisProps = layout.colAxisProps;\n rowAxisProps = layout.rowAxisProps;\n variants = layout.variants;\n }\n\n var singleTypeGrid = layout ? layout.singleTypeGrid : false;\n var singleTypeProp = layout ? layout.singleTypeProp : null;\n if (singleTypeGrid) {\n console.log('[Generate] Single-type grid mode: per-item labels for \"' + singleTypeProp.name + '\"');\n figma.notify('Single-property grid detected — labels will appear below each item.', { timeout: 4000 });\n }\n\n // ─── Disable auto-layout on CS before any repositioning ───\n // Component sets often have auto-layout (HORIZONTAL with wrap). Any resize or\n // position change would trigger Figma to reflow children. We disable it here,\n // saving and restoring positions since setting layoutMode='NONE' can flatten\n // a wrapped layout (moving children to a single row).\n if (cs.layoutMode && cs.layoutMode !== 'NONE') {\n if (DEBUG) { console.log('[Generate] Disabling CS auto-layout (was: ' + cs.layoutMode + ')'); }\n var _savedPos = {};\n for (var _si = 0; _si < variants.length; _si++) {\n _savedPos[variants[_si].id] = { x: variants[_si].x, y: variants[_si].y };\n }\n var _savedW = cs.width;\n var _savedH = cs.height;\n cs.layoutMode = 'NONE';\n // Restore positions and size that Figma changed during the layoutMode switch\n cs.resize(_savedW, _savedH);\n for (var _ri = 0; _ri < variants.length; _ri++) {\n var _sp = _savedPos[variants[_ri].id];\n variants[_ri].x = _sp.x;\n variants[_ri].y = _sp.y;\n }\n }\n\n figma.ui.postMessage({ type: 'status', message: 'Calculating dimensions...' });\n var tDims = Date.now();\n\n // ─── Boolean grid mode detection ───\n\n var booleanGridInfo = null;\n var BOOL_GROUP_GAP = 24; // updated below for single-boolean cases\n\n if (options.showBooleanVisibility && (options.booleanDisplayMode || 'list') === 'grid') {\n var boolProps = getBooleanComponentProperties(cs);\n if (options.enabledBooleanProps) {\n boolProps = boolProps.filter(function(p) { return options.enabledBooleanProps.indexOf(p.name) !== -1; });\n }\n if (boolProps.length > 0) {\n var boolAxis = determineBooleanAxis(cs.width, cs.height);\n var boolGroups = buildBooleanGroups(boolProps, options.booleanCombination || 'individual', 'truefalse');\n booleanGridInfo = {\n axis: boolAxis,\n defaultLabel: 'Base',\n groups: boolGroups,\n numGroups: boolGroups.length + 1,\n boolPropCount: boolProps.length\n };\n // Single-boolean: no gap (tint sits flush against divider line), multi-boolean: full gap for double-line dividers\n BOOL_GROUP_GAP = boolProps.length > 1 ? 24 : 0;\n if (DEBUG) { console.log('[Boolean Grid] axis:', boolAxis, 'groups:', boolGroups.length, 'default:', booleanGridInfo.defaultLabel, 'gap:', BOOL_GROUP_GAP); }\n }\n }\n\n // ─── Calculate label column widths (for row-axis properties) ───\n\n const rowLabelWidths = [];\n for (let i = 0; i < rowAxisProps.length; i++) {\n const prop = rowAxisProps[i];\n const hasBracket = rowAxisProps.length > 1 && i < rowAxisProps.length - 1;\n\n let maxTextWidth = 0;\n for (const val of prop.values) {\n const displayText = formatLabel(prop.name, val);\n const w = measureTextWidth(displayText, LABEL_FONT_SIZE);\n if (w > maxTextWidth) maxTextWidth = w;\n }\n\n const labelWidth = hasBracket\n ? Math.ceil(maxTextWidth + LABEL_GAP + BRACKET_THICKNESS + LABEL_GAP)\n : Math.ceil(maxTextWidth + LABEL_GAP * 2);\n rowLabelWidths.push(labelWidth);\n }\n\n const totalLabelWidth = rowLabelWidths.reduce((sum, w) => sum + w, 0);\n\n // ─── Calculate label row heights (for column-axis properties) ───\n\n const colLabelHeights = [];\n for (let i = 0; i < colAxisProps.length; i++) {\n const hasBracket = colAxisProps.length > 1 && i < colAxisProps.length - 1;\n colLabelHeights.push(hasBracket ? BRACKET_LABEL_ROW_HEIGHT : SIMPLE_LABEL_ROW_HEIGHT);\n }\n\n const totalLabelHeight = colLabelHeights.reduce((sum, h) => sum + h, 0);\n\n // ─── Boolean grid label dimensions ───\n\n var boolLabelWidth = 0;\n var boolLabelHeight = 0;\n\n if (booleanGridInfo) {\n if (booleanGridInfo.axis === 'row') {\n var hasBoolBracket = rowAxisProps.length > 0;\n var maxBoolTextWidth = measureTextWidth(booleanGridInfo.defaultLabel, LABEL_FONT_SIZE);\n for (var bli = 0; bli < booleanGridInfo.groups.length; bli++) {\n var blw = measureTextWidth(booleanGridInfo.groups[bli].label, LABEL_FONT_SIZE);\n if (blw > maxBoolTextWidth) maxBoolTextWidth = blw;\n }\n boolLabelWidth = hasBoolBracket\n ? Math.ceil(maxBoolTextWidth + LABEL_GAP + BRACKET_THICKNESS + LABEL_GAP)\n : Math.ceil(maxBoolTextWidth + LABEL_GAP * 2);\n } else {\n var hasBoolBracket = colAxisProps.length > 0;\n boolLabelHeight = hasBoolBracket ? BRACKET_LABEL_ROW_HEIGHT : SIMPLE_LABEL_ROW_HEIGHT;\n }\n }\n\n var adjustedTotalLabelWidth = totalLabelWidth + boolLabelWidth;\n var adjustedTotalLabelHeight = totalLabelHeight + boolLabelHeight;\n\n // Reserve space above labels for title, description, and doc link\n var titleOffset = 0;\n var descOffset = 0;\n var docLinkOffset = 0;\n var headerDescFrame = null;\n var headerDocLinkText = null;\n if (options.showTitle) {\n titleOffset = TITLE_FONT_SIZE + TITLE_GAP;\n }\n var descText = cs.descriptionMarkdown || cs.description || '';\n if (options.showDescription && descText.trim()) {\n console.log('[Description RAW]', JSON.stringify(descText));\n headerDescFrame = createDescriptionFrame(descText, Math.max(cs.width, 240));\n descOffset = headerDescFrame.height + DESC_GAP;\n }\n var docLinkUri = getDocLink(cs);\n if (options.showDocLink && docLinkUri) {\n headerDocLinkText = createDocLinkText(docLinkUri);\n docLinkOffset = DOC_LINK_FONT_SIZE + DOC_LINK_GAP;\n }\n var headerOffset = titleOffset + descOffset + docLinkOffset;\n adjustedTotalLabelHeight += headerOffset;\n\n // ─── Calculate column widths and row heights from bounding boxes ───\n\n var colWidths = [];\n if (layout && layout.colMaxes) {\n for (var c = 0; c < colClusters.length; c++) {\n colWidths.push(layout.colMaxes[c] - colClusters[c]);\n }\n }\n\n var rowHeights = [];\n if (layout && layout.rowMaxes) {\n for (var r = 0; r < rowClusters.length; r++) {\n rowHeights.push(layout.rowMaxes[r] - rowClusters[r]);\n }\n }\n\n if (DEBUG) {\n console.log('[Step 3] Column widths (variant sizes):', colWidths);\n console.log('[Step 3] Row heights (variant sizes):', rowHeights);\n }\n\n // ═══════════════════════════════════════════════════════════════════════════════\n // VARIABLE MODES — separate action, generates mode columns in its own frame\n // ═══════════════════════════════════════════════════════════════════════════════\n\n // Variable modes handled in Step 9b below (uses .groups format)\n\n // ═══════════════════════════════════════════════════════════════════════════════\n // STANDALONE DOC MODE — generate docs as a separate frame, CS stays untouched\n // ═══════════════════════════════════════════════════════════════════════════════\n\n if (options.standaloneDoc) {\n figma.ui.postMessage({ type: 'status', message: 'Creating standalone docs...' });\n\n // Build position map for instances (variant ID → position within grid)\n var saPositions = {};\n for (var sapi = 0; sapi < grid.length; sapi++) {\n saPositions[grid[sapi].node.id] = { x: grid[sapi].node.x, y: grid[sapi].node.y };\n }\n\n // ─── Uniform cell grid (compute positions for instances, don't modify CS) ───\n\n if (colAxisProps.length > 0 && colClusters.length > 1) {\n var saInnerColProp = colAxisProps[colAxisProps.length - 1];\n var saColLabelTextWidths = [];\n for (var sac = 0; sac < colClusters.length; sac++) {\n var saInCol = gridByCol[sac] || [];\n if (saInCol.length === 0) { saColLabelTextWidths.push(0); continue; }\n var saValue = saInCol[0].props[saInnerColProp.name];\n var saDisplayText = formatLabel(saInnerColProp.name, saValue);\n saColLabelTextWidths.push(measureTextWidth(saDisplayText, LABEL_FONT_SIZE) + LABEL_GAP * 2);\n }\n var saMaxLabelW = Math.max.apply(null, saColLabelTextWidths);\n var saMaxVariantW = Math.max.apply(null, colWidths);\n if (saMaxLabelW > saMaxVariantW) {\n var saOrigCC = colClusters.slice();\n var saOrigCW = colWidths.slice();\n var saFirstCenter = saOrigCC[0] + saOrigCW[0] / 2;\n var saLastCenter = saOrigCC[saOrigCC.length - 1] + saOrigCW[saOrigCW.length - 1] / 2;\n var saOrigPitch = (saLastCenter - saFirstCenter) / (colClusters.length - 1);\n var saCellW = Math.max(saOrigPitch, saMaxLabelW);\n for (var sac2 = 0; sac2 < colClusters.length; sac2++) {\n var saIdealCenter = saCellW / 2 + sac2 * saCellW;\n var saVarHalfW = saOrigCW[sac2] / 2;\n var saIdealLeft = saIdealCenter - saVarHalfW;\n var saInCol2 = gridByCol[sac2] || [];\n for (var saci = 0; saci < saInCol2.length; saci++) {\n saPositions[saInCol2[saci].node.id].x = saIdealLeft + (saInCol2[saci].node.x - saOrigCC[sac2]);\n }\n colClusters[sac2] = sac2 * saCellW;\n colWidths[sac2] = saCellW;\n }\n }\n }\n\n if (rowAxisProps.length > 0 && rowClusters.length > 1) {\n var saMinRowLabelH = LABEL_FONT_SIZE + LABEL_GAP * 2;\n var saMaxVariantH = Math.max.apply(null, rowHeights);\n if (saMinRowLabelH > saMaxVariantH) {\n var saOrigRC = rowClusters.slice();\n var saOrigRH = rowHeights.slice();\n var saFirstRCenter = saOrigRC[0] + saOrigRH[0] / 2;\n var saLastRCenter = saOrigRC[saOrigRC.length - 1] + saOrigRH[saOrigRH.length - 1] / 2;\n var saOrigRPitch = (saLastRCenter - saFirstRCenter) / (rowClusters.length - 1);\n var saCellH = Math.max(saOrigRPitch, saMinRowLabelH);\n for (var sar = 0; sar < rowClusters.length; sar++) {\n var saRIdealCenter = saCellH / 2 + sar * saCellH;\n var saVarHalfH = saOrigRH[sar] / 2;\n var saRIdealTop = saRIdealCenter - saVarHalfH;\n var saInRow = gridByRow[sar] || [];\n for (var sari = 0; sari < saInRow.length; sari++) {\n saPositions[saInRow[sari].node.id].y = saRIdealTop + (saInRow[sari].node.y - saOrigRC[sar]);\n }\n rowClusters[sar] = sar * saCellH;\n rowHeights[sar] = saCellH;\n }\n }\n }\n\n // ─── Single-type grid: build uniform grid with per-item label space ───\n\n var stGridInfo = null; // shared across standalone + variable modes\n if (singleTypeGrid) {\n stGridInfo = buildSingleTypeUniformGrid(\n grid, gridByCol, colClusters, colWidths, rowClusters, rowHeights, singleTypeProp,\n function(g, x, y) { saPositions[g.node.id].x = x; saPositions[g.node.id].y = y; }\n );\n }\n\n // Compute grid dimensions from clusters\n var saGridW, saGridH;\n if (singleTypeGrid) {\n var saStLastRowBottom = rowClusters[rowClusters.length - 1] + rowHeights[rowHeights.length - 1];\n saGridW = colClusters[colClusters.length - 1] + colWidths[colClusters.length - 1] + stGridInfo.pad;\n saGridH = saStLastRowBottom + stGridInfo.pad;\n } else {\n saGridW = colClusters.length > 0 ? (colClusters[colClusters.length - 1] + colWidths[colWidths.length - 1]) : cs.width;\n saGridH = rowClusters.length > 0 ? (rowClusters[rowClusters.length - 1] + rowHeights[rowHeights.length - 1]) : cs.height;\n }\n\n // ─── Create or reuse standalone wrapper ───\n\n var saWrapper = findStandaloneWrapper(cs);\n if (saWrapper) {\n removeOldLabels(saWrapper);\n } else {\n saWrapper = figma.createFrame();\n saWrapper.name = '❖ ' + cs.name + (options.docLabel ? ' — ' + options.docLabel : ' — Obra Autodocs');\n saWrapper.setPluginData('standaloneDoc', 'true');\n saWrapper.setPluginData('sourceComponentSetId', cs.id);\n saWrapper.fills = [];\n saWrapper.clipsContent = false;\n }\n // Place wrapper to the right of the parent frame (outside of it, on the page/grandparent)\n // If the CS lives directly on the page, place next to the CS itself\n var saParentFrame = cs.parent;\n var saIsOnPage = saParentFrame.type === 'PAGE';\n var saRefFrame = saIsOnPage ? cs : saParentFrame;\n var saPlacementParent = saIsOnPage ? saParentFrame : (saParentFrame.parent || saParentFrame);\n if (saWrapper.parent !== saPlacementParent) {\n saPlacementParent.appendChild(saWrapper);\n }\n saWrapper.x = saRefFrame.x + saRefFrame.width + 48;\n saWrapper.y = saRefFrame.y;\n\n // ─── Header: title + description (standalone doc) ───\n\n var saHeaderY = 0;\n if (options.showTitle && titleOffset > 0) {\n var saTitleText = createTitleText(cs.name);\n saTitleText.x = adjustedTotalLabelWidth;\n saTitleText.y = saHeaderY;\n saWrapper.appendChild(saTitleText);\n saHeaderY += titleOffset;\n }\n if (headerDescFrame) {\n headerDescFrame.x = adjustedTotalLabelWidth;\n headerDescFrame.y = saHeaderY;\n saWrapper.appendChild(headerDescFrame);\n saHeaderY += descOffset;\n }\n if (headerDocLinkText) {\n headerDocLinkText.x = adjustedTotalLabelWidth;\n headerDocLinkText.y = saHeaderY;\n saWrapper.appendChild(headerDocLinkText);\n }\n\n // ─── Create variant instances in grid ───\n\n var saExpandedW = saGridW;\n var saExpandedH = saGridH;\n\n for (var sagi = 0; sagi < grid.length; sagi++) {\n var saG = grid[sagi];\n var saInst = saG.node.createInstance();\n var saPos = saPositions[saG.node.id];\n saInst.x = adjustedTotalLabelWidth + saPos.x;\n saInst.y = adjustedTotalLabelHeight + saPos.y;\n saWrapper.appendChild(saInst);\n }\n\n // ─── Per-item labels for single-type grid ───\n\n if (singleTypeGrid) {\n createSingleTypeGridLabels(saWrapper, grid, singleTypeProp, colClusters, colWidths,\n rowClusters, stGridInfo.maxVarH, adjustedTotalLabelWidth, adjustedTotalLabelHeight);\n }\n\n // ─── Dashed border around grid area ───\n\n var saBorder = figma.createFrame();\n saBorder.name = 'Grid Border';\n saBorder.fills = [];\n saBorder.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n saBorder.dashPattern = [4, 4];\n saBorder.x = adjustedTotalLabelWidth;\n saBorder.y = adjustedTotalLabelHeight;\n saBorder.resize(saGridW, saGridH);\n saWrapper.appendChild(saBorder);\n\n // ─── Boolean grid (standalone) ───\n\n var saBoolGroupHeights = [];\n var saBoolGroupWidths = [];\n var saBoolGroupData = [];\n\n if (booleanGridInfo) {\n var saGridPadTop = rowClusters.length > 0 ? rowClusters[0] : 0;\n var saGridPadLeft = colClusters.length > 0 ? colClusters[0] : 0;\n var saGridPadBottom = saGridH - (rowClusters.length > 0 ? (rowClusters[rowClusters.length - 1] + rowHeights[rowHeights.length - 1]) : saGridH);\n var saGridPadRight = saGridW - (colClusters.length > 0 ? (colClusters[colClusters.length - 1] + colWidths[colWidths.length - 1]) : saGridW);\n\n var saBoolTotalCombs = booleanGridInfo.groups.length * grid.length;\n var saUseBoolAccuracy = options.booleanImprovedAccuracy && saBoolTotalCombs <= BOOL_ACCURACY_MAX_COMBINATIONS;\n\n for (var sabgi = 0; sabgi < booleanGridInfo.groups.length; sabgi++) {\n var saBGroup = booleanGridInfo.groups[sabgi];\n var saBResult = createBooleanInstanceGroup(cs, saBGroup.props, 0, 0);\n var saAdjRow = null, saAdjCol = null;\n\n if (saUseBoolAccuracy) {\n if (rowClusters.length > 0) {\n var saDRC = [], saDRH = [], saDCurY = saGridPadTop;\n for (var sadri = 0; sadri < rowClusters.length; sadri++) {\n var saDMaxH = rowHeights[sadri];\n for (var sadii = 0; sadii < saBResult.instances.length; sadii++) {\n var _saiy = saBResult.instances[sadii].y;\n if (_saiy >= rowClusters[sadri] - CLUSTER_TOLERANCE && _saiy <= rowClusters[sadri] + rowHeights[sadri] + CLUSTER_TOLERANCE) {\n if (saBResult.instances[sadii].height > saDMaxH) saDMaxH = saBResult.instances[sadii].height;\n }\n }\n saDRC.push(saDCurY);\n saDRH.push(saDMaxH);\n if (sadri < rowClusters.length - 1) {\n saDCurY += saDMaxH + (rowClusters[sadri + 1] - (rowClusters[sadri] + rowHeights[sadri]));\n }\n }\n saAdjRow = { clusters: saDRC, heights: saDRH };\n }\n if (colClusters.length > 0) {\n var saDCC = [], saDCW = [], saDCurX = saGridPadLeft;\n for (var sadci = 0; sadci < colClusters.length; sadci++) {\n var saDMaxW = colWidths[sadci];\n for (var sadci2 = 0; sadci2 < saBResult.instances.length; sadci2++) {\n var _saix = saBResult.instances[sadci2].x;\n if (_saix >= colClusters[sadci] - CLUSTER_TOLERANCE && _saix <= colClusters[sadci] + colWidths[sadci] + CLUSTER_TOLERANCE) {\n if (saBResult.instances[sadci2].width > saDMaxW) saDMaxW = saBResult.instances[sadci2].width;\n }\n }\n saDCC.push(saDCurX);\n saDCW.push(saDMaxW);\n if (sadci < colClusters.length - 1) {\n saDCurX += saDMaxW + (colClusters[sadci + 1] - (colClusters[sadci] + colWidths[sadci]));\n }\n }\n saAdjCol = { clusters: saDCC, widths: saDCW };\n }\n }\n\n // Reposition instances to centered positions\n if (saAdjRow || saAdjCol) {\n for (var sarii = 0; sarii < saBResult.instances.length; sarii++) {\n var saRI = saBResult.instances[sarii];\n if (saAdjRow) {\n for (var sarmi = 0; sarmi < rowClusters.length; sarmi++) {\n if (saRI.y >= rowClusters[sarmi] - CLUSTER_TOLERANCE && saRI.y <= rowClusters[sarmi] + rowHeights[sarmi] + CLUSTER_TOLERANCE) {\n saRI.y = saAdjRow.clusters[sarmi] + (saAdjRow.heights[sarmi] - saRI.height) / 2;\n break;\n }\n }\n }\n if (saAdjCol) {\n for (var sacmi = 0; sacmi < colClusters.length; sacmi++) {\n if (saRI.x >= colClusters[sacmi] - CLUSTER_TOLERANCE && saRI.x <= colClusters[sacmi] + colWidths[sacmi] + CLUSTER_TOLERANCE) {\n saRI.x = saAdjCol.clusters[sacmi] + (saAdjCol.widths[sacmi] - saRI.width) / 2;\n break;\n }\n }\n }\n }\n }\n\n var saGroupH, saGroupW;\n if (saAdjRow) {\n var saLRB = saAdjRow.clusters[saAdjRow.clusters.length - 1] + saAdjRow.heights[saAdjRow.heights.length - 1];\n saGroupH = Math.max(saGridH, saLRB + saGridPadBottom);\n } else {\n saGroupH = Math.max(saGridH, saBResult.height + saGridPadBottom);\n }\n if (saAdjCol) {\n var saLCR = saAdjCol.clusters[saAdjCol.clusters.length - 1] + saAdjCol.widths[saAdjCol.widths.length - 1];\n saGroupW = Math.max(saGridW, saLCR + saGridPadRight);\n } else {\n saGroupW = Math.max(saGridW, saBResult.width + saGridPadRight);\n }\n\n saBoolGroupHeights.push(saGroupH);\n saBoolGroupWidths.push(saGroupW);\n saBoolGroupData.push({ instances: saBResult.instances, width: saBResult.width, height: saBResult.height, adjRowData: saAdjRow, adjColData: saAdjCol });\n }\n\n // Calculate expanded dimensions\n if (booleanGridInfo.axis === 'row') {\n var saTotalGH = 0;\n for (var sabhi = 0; sabhi < saBoolGroupHeights.length; sabhi++) {\n saTotalGH += saBoolGroupHeights[sabhi] + BOOL_GROUP_GAP;\n }\n saExpandedH = saGridH + saTotalGH;\n } else {\n var saTotalGW = 0;\n for (var sabwi = 0; sabwi < saBoolGroupWidths.length; sabwi++) {\n saTotalGW += saBoolGroupWidths[sabwi] + BOOL_GROUP_GAP;\n }\n saExpandedW = saGridW + saTotalGW;\n }\n\n // Update border to cover expanded grid\n if (booleanGridInfo.axis === 'row') {\n saBorder.resize(saGridW, saExpandedH);\n } else {\n saBorder.resize(saExpandedW, saGridH);\n }\n }\n\n // Resize wrapper\n saWrapper.resize(\n adjustedTotalLabelWidth + saExpandedW,\n adjustedTotalLabelHeight + saExpandedH\n );\n\n // ─── Position boolean grid instances, tints, and labels ───\n\n if (booleanGridInfo) {\n var saBCumOffset = 0;\n for (var sabgi2 = 0; sabgi2 < booleanGridInfo.groups.length; sabgi2++) {\n var saBGH = saBoolGroupHeights[sabgi2];\n var saBGW = saBoolGroupWidths[sabgi2];\n var saBGOffX, saBGOffY;\n if (booleanGridInfo.axis === 'row') {\n saBGOffX = adjustedTotalLabelWidth;\n saBGOffY = adjustedTotalLabelHeight + saGridH + saBCumOffset + BOOL_GROUP_GAP;\n saBCumOffset += saBGH + BOOL_GROUP_GAP;\n } else {\n saBGOffX = adjustedTotalLabelWidth + saGridW + saBCumOffset + BOOL_GROUP_GAP;\n saBGOffY = adjustedTotalLabelHeight;\n saBCumOffset += saBGW + BOOL_GROUP_GAP;\n }\n saBoolGroupData[sabgi2].offsetX = saBGOffX;\n saBoolGroupData[sabgi2].offsetY = saBGOffY;\n\n // Background tint\n var saTintW = booleanGridInfo.axis === 'row' ? saGridW : saBGW;\n var saTintH = booleanGridInfo.axis === 'row' ? saBGH : saGridH;\n var saTint = createBackgroundTint(saBGOffX, saBGOffY, saTintW, saTintH);\n saWrapper.appendChild(saTint);\n\n // Reposition boolean instances\n var saBInsts = saBoolGroupData[sabgi2].instances;\n for (var sabii = 0; sabii < saBInsts.length; sabii++) {\n saBInsts[sabii].x += saBGOffX;\n saBInsts[sabii].y += saBGOffY;\n saWrapper.appendChild(saBInsts[sabii]);\n }\n }\n\n // Boolean axis labels\n if (booleanGridInfo.axis === 'row') {\n var saHBB = rowAxisProps.length > 0;\n saWrapper.appendChild(createRowLabel(booleanGridInfo.defaultLabel, 0, adjustedTotalLabelHeight, boolLabelWidth, saGridH, saHBB));\n for (var sabli = 0; sabli < booleanGridInfo.groups.length; sabli++) {\n saWrapper.appendChild(createRowLabel(booleanGridInfo.groups[sabli].label, 0, saBoolGroupData[sabli].offsetY, boolLabelWidth, saBoolGroupHeights[sabli], saHBB));\n }\n } else {\n var saHBB = colAxisProps.length > 0;\n saWrapper.appendChild(createColLabel(booleanGridInfo.defaultLabel, adjustedTotalLabelWidth, headerOffset, saGridW, boolLabelHeight, saHBB));\n for (var sabli = 0; sabli < booleanGridInfo.groups.length; sabli++) {\n saWrapper.appendChild(createColLabel(booleanGridInfo.groups[sabli].label, saBoolGroupData[sabli].offsetX, headerOffset, saBoolGroupWidths[sabli], boolLabelHeight, saHBB));\n }\n }\n }\n\n // ─── Row labels (left side) ───\n\n var saLabelXOff = boolLabelWidth;\n for (var sarpi = 0; sarpi < rowAxisProps.length; sarpi++) {\n var saRProp = rowAxisProps[sarpi];\n var saRColW = rowLabelWidths[sarpi];\n var saRIsInner = rowAxisProps.length > 1 && sarpi === rowAxisProps.length - 1;\n var saRHasBracket = !saRIsInner && rowAxisProps.length > 1;\n var saRGroups = getRowGroups(saRProp.name, grid, gridByRow, rowClusters, rowHeights);\n for (var sarvi = 0; sarvi < saRGroups.length; sarvi++) {\n var saRG = saRGroups[sarvi];\n saWrapper.appendChild(createRowLabel(\n formatLabel(saRProp.name, saRG.value), saLabelXOff,\n adjustedTotalLabelHeight + saRG.startY, saRColW,\n saRG.endY - saRG.startY, saRHasBracket\n ));\n }\n saLabelXOff += saRColW;\n }\n\n // ─── Column labels (top) ───\n\n var saLabelYOff = boolLabelHeight + headerOffset;\n for (var sacpi = 0; sacpi < colAxisProps.length; sacpi++) {\n var saCProp = colAxisProps[sacpi];\n var saCRowH = colLabelHeights[sacpi];\n var saCIsInner = colAxisProps.length > 1 && sacpi === colAxisProps.length - 1;\n var saCHasBracket = !saCIsInner && colAxisProps.length > 1;\n var saCGroups = getColGroups(saCProp.name, grid, gridByCol, colClusters, colWidths);\n for (var sacvi = 0; sacvi < saCGroups.length; sacvi++) {\n var saCG = saCGroups[sacvi];\n saWrapper.appendChild(createColLabel(\n formatLabel(saCProp.name, saCG.value),\n adjustedTotalLabelWidth + saCG.startX, saLabelYOff,\n saCG.endX - saCG.startX, saCRowH, saCHasBracket\n ));\n }\n saLabelYOff += saCRowH;\n }\n\n // ─── Repeat labels for boolean groups ───\n\n if (booleanGridInfo) {\n for (var sabrgi = 0; sabrgi < booleanGridInfo.groups.length; sabrgi++) {\n var sabrAdjRC = (saBoolGroupData[sabrgi].adjRowData && saBoolGroupData[sabrgi].adjRowData.clusters) || rowClusters;\n var sabrAdjRH = (saBoolGroupData[sabrgi].adjRowData && saBoolGroupData[sabrgi].adjRowData.heights) || rowHeights;\n var sabrAdjCC = (saBoolGroupData[sabrgi].adjColData && saBoolGroupData[sabrgi].adjColData.clusters) || colClusters;\n var sabrAdjCW = (saBoolGroupData[sabrgi].adjColData && saBoolGroupData[sabrgi].adjColData.widths) || colWidths;\n if (booleanGridInfo.axis === 'row') {\n var sabrLXO = boolLabelWidth;\n for (var sabrpi = 0; sabrpi < rowAxisProps.length; sabrpi++) {\n var sabrP = rowAxisProps[sabrpi];\n var sabrCW = rowLabelWidths[sabrpi];\n var sabrIsInner = rowAxisProps.length > 1 && sabrpi === rowAxisProps.length - 1;\n var sabrHB = !sabrIsInner && rowAxisProps.length > 1;\n var sabrVGs = getRowGroups(sabrP.name, grid, gridByRow, sabrAdjRC, sabrAdjRH);\n for (var sabrvi = 0; sabrvi < sabrVGs.length; sabrvi++) {\n var sabrVG = sabrVGs[sabrvi];\n saWrapper.appendChild(createRowLabel(\n formatLabel(sabrP.name, sabrVG.value), sabrLXO,\n saBoolGroupData[sabrgi].offsetY + sabrVG.startY,\n sabrCW, sabrVG.endY - sabrVG.startY, sabrHB\n ));\n }\n sabrLXO += sabrCW;\n }\n } else {\n var sabrLYO = boolLabelHeight + headerOffset;\n for (var sabrpi = 0; sabrpi < colAxisProps.length; sabrpi++) {\n var sabrP = colAxisProps[sabrpi];\n var sabrRH = colLabelHeights[sabrpi];\n var sabrIsInner = colAxisProps.length > 1 && sabrpi === colAxisProps.length - 1;\n var sabrHB = !sabrIsInner && colAxisProps.length > 1;\n var sabrVGs = getColGroups(sabrP.name, grid, gridByCol, sabrAdjCC, sabrAdjCW);\n for (var sabrvi = 0; sabrvi < sabrVGs.length; sabrvi++) {\n var sabrVG = sabrVGs[sabrvi];\n saWrapper.appendChild(createColLabel(\n formatLabel(sabrP.name, sabrVG.value),\n saBoolGroupData[sabrgi].offsetX + sabrVG.startX, sabrLYO,\n sabrVG.endX - sabrVG.startX, sabrRH, sabrHB\n ));\n }\n sabrLYO += sabrRH;\n }\n }\n }\n }\n\n // ─── Grid lines ───\n\n if (options.showGrid) {\n createGridLines(saWrapper, colClusters, rowClusters, colWidths, rowHeights,\n adjustedTotalLabelWidth, adjustedTotalLabelHeight, saGridW, saGridH,\n singleTypeGrid ? grid : null);\n\n if (booleanGridInfo) {\n for (var saggi = 0; saggi < booleanGridInfo.groups.length; saggi++) {\n var sagRC = (saBoolGroupData[saggi].adjRowData && saBoolGroupData[saggi].adjRowData.clusters) || rowClusters;\n var sagRH = (saBoolGroupData[saggi].adjRowData && saBoolGroupData[saggi].adjRowData.heights) || rowHeights;\n var sagCC = (saBoolGroupData[saggi].adjColData && saBoolGroupData[saggi].adjColData.clusters) || colClusters;\n var sagCW = (saBoolGroupData[saggi].adjColData && saBoolGroupData[saggi].adjColData.widths) || colWidths;\n if (booleanGridInfo.axis === 'row') {\n createGridLines(saWrapper, sagCC, sagRC, sagCW, sagRH,\n saBoolGroupData[saggi].offsetX, saBoolGroupData[saggi].offsetY, saGridW, saBoolGroupHeights[saggi]);\n } else {\n createGridLines(saWrapper, sagCC, sagRC, sagCW, sagRH,\n saBoolGroupData[saggi].offsetX, saBoolGroupData[saggi].offsetY, saBoolGroupWidths[saggi], saGridH);\n }\n }\n\n // Dividers between boolean groups\n var saUseDblLine = booleanGridInfo.boolPropCount > 1;\n for (var sadgi = 0; sadgi < booleanGridInfo.groups.length; sadgi++) {\n var saDivL1 = figma.createLine();\n saDivL1.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n saDivL1.strokeWeight = GRID_STROKE_WEIGHT;\n saDivL1.dashPattern = GRID_DASH_PATTERN;\n if (booleanGridInfo.axis === 'row') {\n var saPrevBot = sadgi === 0 ? (adjustedTotalLabelHeight + saGridH) : (saBoolGroupData[sadgi - 1].offsetY + saBoolGroupHeights[sadgi - 1]);\n saDivL1.resize(saGridW, 0);\n saDivL1.x = adjustedTotalLabelWidth;\n saDivL1.y = saPrevBot;\n } else {\n var saPrevRight = sadgi === 0 ? (adjustedTotalLabelWidth + saGridW) : (saBoolGroupData[sadgi - 1].offsetX + saBoolGroupWidths[sadgi - 1]);\n saDivL1.rotation = -90;\n saDivL1.resize(saGridH, 0);\n saDivL1.x = saPrevRight;\n saDivL1.y = adjustedTotalLabelHeight;\n }\n saWrapper.appendChild(saDivL1);\n if (saUseDblLine) {\n var saDivL2 = figma.createLine();\n saDivL2.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n saDivL2.strokeWeight = GRID_STROKE_WEIGHT;\n saDivL2.dashPattern = GRID_DASH_PATTERN;\n if (booleanGridInfo.axis === 'row') {\n saDivL2.resize(saGridW, 0);\n saDivL2.x = adjustedTotalLabelWidth;\n saDivL2.y = saBoolGroupData[sadgi].offsetY;\n } else {\n saDivL2.rotation = -90;\n saDivL2.resize(saGridH, 0);\n saDivL2.x = saBoolGroupData[sadgi].offsetX;\n saDivL2.y = adjustedTotalLabelHeight;\n }\n saWrapper.appendChild(saDivL2);\n }\n }\n }\n }\n\n // ─── Extra sections ───\n\n var saExtraSections = [];\n if (options.showBooleanVisibility && !booleanGridInfo) {\n figma.ui.postMessage({ type: 'status', message: 'Creating boolean visibility examples...' });\n var saBoolSection;\n if (options.booleanScope) {\n saBoolSection = await createScopedBooleanSection(cs, options.booleanScope, options.booleanCombination || 'individual', options.booleanDisplayMode || 'list');\n } else {\n saBoolSection = await createBooleanVisibilitySection(cs, options.booleanCombination || 'individual', options.enabledBooleanProps, options.booleanDisplayMode || 'list');\n }\n if (saBoolSection) saExtraSections.push(saBoolSection);\n }\n if (options.showNestedInstances) {\n figma.ui.postMessage({ type: 'status', message: 'Creating nested instance examples...' });\n var saNestedSection = await createNestedInstancesSection(cs, options.nestedInstancesMode || 'representative', options.enabledNestedInstances);\n if (saNestedSection) saExtraSections.push(saNestedSection);\n }\n if (saExtraSections.length > 0) {\n var SA_EXTRAS_GAP = 24;\n var saExtras = figma.createFrame();\n saExtras.name = 'Property Combinations';\n saExtras.fills = [];\n saExtras.clipsContent = false;\n saExtras.layoutMode = 'VERTICAL';\n saExtras.itemSpacing = SA_EXTRAS_GAP;\n saExtras.counterAxisSizingMode = 'AUTO';\n saExtras.primaryAxisSizingMode = 'AUTO';\n for (var saei = 0; saei < saExtraSections.length; saei++) {\n saExtras.appendChild(saExtraSections[saei]);\n }\n if (_autoLayoutExtras) {\n saExtras.paddingLeft = adjustedTotalLabelWidth;\n } else {\n saExtras.x = adjustedTotalLabelWidth;\n saExtras.y = adjustedTotalLabelHeight + saExpandedH + SA_EXTRAS_GAP;\n }\n saWrapper.appendChild(saExtras);\n if (!_autoLayoutExtras) {\n saWrapper.resize(\n Math.max(saWrapper.width, adjustedTotalLabelWidth + saExtras.width),\n saExtras.y + saExtras.height\n );\n }\n }\n\n // ─── Variable Modes (experimental) ───\n // Each selected mode creates an extension of the grid to the right.\n // The extension is a frame with the variable mode set — Figma propagates\n // the mode to all child instances inside that frame.\n\n var vmModesContainer = null;\n\n if (options.variableModes && options.variableModes.groups && options.variableModes.groups.length > 0) {\n figma.ui.postMessage({ type: 'status', message: 'Generating variable modes...' });\n\n var vmCollId = options.variableModes.collectionId;\n var vmCollection = await figma.variables.getVariableCollectionByIdAsync(vmCollId);\n var vmModes = options.variableModes.modes;\n console.log('[VarModes] Collection resolved:', vmCollection ? vmCollection.name : 'NOT FOUND');\n var VM_GAP = 48;\n var VM_LABEL_HEIGHT = SIMPLE_LABEL_ROW_HEIGHT;\n\n console.log('[VarModes] collectionId:', vmCollId);\n console.log('[VarModes] modes:', JSON.stringify(vmModes));\n console.log('[VarModes] grid:', grid.length, 'variants, saExpandedW:', saExpandedW, 'saExpandedH:', saExpandedH);\n console.log('[VarModes] colClusters:', JSON.stringify(colClusters), 'colWidths:', JSON.stringify(colWidths));\n\n // Try to find a background color variable from the collection for dark mode support\n var vmBgVariable = null;\n try {\n var vmVarIds = vmCollection.variableIds;\n for (var vmvi = 0; vmvi < vmVarIds.length; vmvi++) {\n var vmVar = await figma.variables.getVariableByIdAsync(vmVarIds[vmvi]);\n if (vmVar && vmVar.resolvedType === 'COLOR') {\n var vmVarName = vmVar.name.toLowerCase();\n if (vmVarName.indexOf('background') !== -1 || vmVarName.indexOf('bg') !== -1 || vmVarName.indexOf('surface') !== -1 || vmVarName.indexOf('body') !== -1) {\n vmBgVariable = vmVar;\n console.log('[VarModes] Found background variable:', vmVar.name, vmVar.id);\n break;\n }\n }\n }\n } catch (vmBgErr) {\n console.warn('[VarModes] Could not search for background variable:', vmBgErr.message);\n }\n\n // Create a \"Modes\" container — will be a sibling of the Documentation frame\n vmModesContainer = figma.createFrame();\n vmModesContainer.name = 'Modes';\n vmModesContainer.fills = [];\n vmModesContainer.clipsContent = false;\n\n // Create one extension per selected (non-default) mode\n var vmCumOffsetX = 0;\n\n for (var vmmi = 0; vmmi < vmModes.length; vmmi++) {\n var vmMode = vmModes[vmmi];\n var vmOffX = vmCumOffsetX;\n\n // Per-mode wrapper frame\n var vmModeWrapper = figma.createFrame();\n vmModeWrapper.name = 'Mode: ' + vmMode.name;\n vmModeWrapper.fills = [];\n vmModeWrapper.clipsContent = false;\n\n // Mode label (above the grid)\n vmModeWrapper.appendChild(createColLabel(vmMode.name, adjustedTotalLabelWidth, boolLabelHeight, saExpandedW, VM_LABEL_HEIGHT, false));\n\n // Mode frame — instances inside inherit this variable mode\n var vmFrame = figma.createFrame();\n vmFrame.name = vmMode.name;\n vmFrame.clipsContent = false;\n vmFrame.x = adjustedTotalLabelWidth;\n vmFrame.y = adjustedTotalLabelHeight;\n vmFrame.resize(saExpandedW, saExpandedH);\n\n // Set background fill — use variable if found, otherwise transparent\n if (vmBgVariable) {\n vmFrame.fills = [figma.variables.setBoundVariableForPaint({ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', vmBgVariable)];\n } else {\n vmFrame.fills = [];\n }\n\n try {\n vmFrame.setExplicitVariableModeForCollection(vmCollection, vmMode.modeId);\n console.log('[VarModes] Mode \"' + vmMode.name + '\" set. explicitVariableModes:', JSON.stringify(vmFrame.explicitVariableModes));\n } catch (vmErr) {\n console.error('[VarModes] Failed to set mode \"' + vmMode.name + '\":', vmErr.message);\n }\n\n // Create instances inside the mode frame (positions relative to grid origin)\n for (var vmgi = 0; vmgi < grid.length; vmgi++) {\n var vmInst = grid[vmgi].node.createInstance();\n var vmPos = saPositions[grid[vmgi].node.id];\n vmInst.x = vmPos.x;\n vmInst.y = vmPos.y;\n vmFrame.appendChild(vmInst);\n }\n vmModeWrapper.appendChild(vmFrame);\n\n // Dashed border (on top of the mode frame)\n var vmBorder = figma.createFrame();\n vmBorder.name = 'Grid Border';\n vmBorder.fills = [];\n vmBorder.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n vmBorder.dashPattern = [4, 4];\n vmBorder.x = adjustedTotalLabelWidth;\n vmBorder.y = adjustedTotalLabelHeight;\n vmBorder.resize(saExpandedW, saExpandedH);\n vmModeWrapper.appendChild(vmBorder);\n\n // Grid lines\n if (options.showGrid) {\n createGridLines(vmModeWrapper, colClusters, rowClusters, colWidths, rowHeights,\n adjustedTotalLabelWidth, adjustedTotalLabelHeight, saExpandedW, saExpandedH,\n singleTypeGrid ? grid : null);\n }\n\n // Repeat row labels for this mode\n for (var vmrpi = 0; vmrpi < rowAxisProps.length; vmrpi++) {\n var vmRProp = rowAxisProps[vmrpi];\n var vmRColW = rowLabelWidths[vmrpi];\n var vmRIsInner = rowAxisProps.length > 1 && vmrpi === rowAxisProps.length - 1;\n var vmRHasBracket = !vmRIsInner && rowAxisProps.length > 1;\n var vmRGroups = getRowGroups(vmRProp.name, grid, gridByRow, rowClusters, rowHeights);\n var vmRLXO = 0;\n for (var vmrpj = 0; vmrpj < vmrpi; vmrpj++) vmRLXO += rowLabelWidths[vmrpj];\n for (var vmrvi = 0; vmrvi < vmRGroups.length; vmrvi++) {\n var vmRG = vmRGroups[vmrvi];\n vmModeWrapper.appendChild(createRowLabel(\n formatLabel(vmRProp.name, vmRG.value),\n vmRLXO, adjustedTotalLabelHeight + vmRG.startY,\n vmRColW, vmRG.endY - vmRG.startY, vmRHasBracket\n ));\n }\n }\n\n // Repeat column labels for this mode\n var vmLYO = boolLabelHeight + VM_LABEL_HEIGHT;\n for (var vmcpi = 0; vmcpi < colAxisProps.length; vmcpi++) {\n var vmCProp = colAxisProps[vmcpi];\n var vmCRowH = colLabelHeights[vmcpi];\n var vmCIsInner = colAxisProps.length > 1 && vmcpi === colAxisProps.length - 1;\n var vmCHasBracket = !vmCIsInner && colAxisProps.length > 1;\n var vmCGroups = getColGroups(vmCProp.name, grid, gridByCol, colClusters, colWidths);\n for (var vmcvi = 0; vmcvi < vmCGroups.length; vmcvi++) {\n var vmCG = vmCGroups[vmcvi];\n vmModeWrapper.appendChild(createColLabel(\n formatLabel(vmCProp.name, vmCG.value),\n adjustedTotalLabelWidth + vmCG.startX, vmLYO,\n vmCG.endX - vmCG.startX, vmCRowH, vmCHasBracket\n ));\n }\n vmLYO += vmCRowH;\n }\n\n // Per-item labels for single-type grid (variable modes)\n if (singleTypeGrid && stGridInfo) {\n createSingleTypeGridLabels(vmModeWrapper, grid, singleTypeProp, colClusters, colWidths,\n rowClusters, stGridInfo.maxVarH, adjustedTotalLabelWidth, adjustedTotalLabelHeight);\n }\n\n // Size the per-mode wrapper to fit its content\n var vmModeW = adjustedTotalLabelWidth + saExpandedW;\n var vmModeH = adjustedTotalLabelHeight + saExpandedH;\n vmModeWrapper.resize(vmModeW, vmModeH);\n\n // Position inside the Modes container\n vmModeWrapper.x = vmCumOffsetX;\n vmModeWrapper.y = 0;\n vmModesContainer.appendChild(vmModeWrapper);\n\n vmCumOffsetX += vmModeW + VM_GAP;\n }\n\n // Size the Modes container\n vmModesContainer.resize(vmCumOffsetX > 0 ? vmCumOffsetX - VM_GAP : 1, vmModesContainer.children.length > 0 ? vmModesContainer.children[0].height : 1);\n }\n\n // ─── Wrap in Documentation frame ───\n\n var saDocsFrame = figma.createFrame();\n saDocsFrame.name = 'Documentation';\n saDocsFrame.fills = [];\n saDocsFrame.clipsContent = false;\n saDocsFrame.locked = false;\n\n // Collect wrapper children into Documentation\n var saDocsChildren = [];\n for (var sadi = 0; sadi < saWrapper.children.length; sadi++) {\n if (_autoLayoutExtras && saWrapper.children[sadi].name === 'Property Combinations') continue;\n saDocsChildren.push(saWrapper.children[sadi]);\n }\n\n // Size Documentation frame\n var saDocsW = adjustedTotalLabelWidth + saExpandedW;\n var saDocsH = adjustedTotalLabelHeight + saExpandedH;\n if (!_autoLayoutExtras && saWrapper.height > saDocsH) saDocsH = saWrapper.height;\n saDocsFrame.resize(saDocsW, saDocsH);\n\n saWrapper.appendChild(saDocsFrame);\n for (var sadi2 = 0; sadi2 < saDocsChildren.length; sadi2++) {\n saDocsChildren[sadi2].constraints = { horizontal: 'MIN', vertical: 'MIN' };\n saDocsFrame.appendChild(saDocsChildren[sadi2]);\n }\n\n // When auto layout, ensure Property Combinations is ordered after docsFrame\n if (_autoLayoutExtras) {\n var saExtrasInWrapper = null;\n for (var saewi = 0; saewi < saWrapper.children.length; saewi++) {\n if (saWrapper.children[saewi].name === 'Property Combinations') {\n saExtrasInWrapper = saWrapper.children[saewi];\n break;\n }\n }\n if (saExtrasInWrapper) {\n saWrapper.appendChild(saExtrasInWrapper);\n }\n }\n\n // ─── Make wrapper auto-layout vertical, hug height ───\n\n var saHasVarModes = vmModesContainer !== null;\n if (_autoLayoutExtras || saHasVarModes) {\n saWrapper.layoutMode = 'VERTICAL';\n saWrapper.primaryAxisSizingMode = 'AUTO';\n saWrapper.counterAxisSizingMode = 'AUTO';\n saWrapper.itemSpacing = _autoLayoutExtras ? 24 : 48;\n saWrapper.paddingTop = 0;\n saWrapper.paddingBottom = 0;\n saWrapper.paddingLeft = 0;\n saWrapper.paddingRight = 0;\n }\n\n // Add Modes container if variable modes were generated\n if (vmModesContainer) {\n saWrapper.appendChild(vmModesContainer);\n }\n\n // ─── Cleanup ───\n\n disposeMeasureNode();\n\n var saTotalMs = Date.now() - t0;\n var saTotalSec = (saTotalMs / 1000).toFixed(2);\n console.log('[Perf] === TOTAL standalone generation time: ' + saTotalMs + 'ms (' + saTotalSec + 's) ===');\n\n figma.currentPage.selection = [saWrapper];\n figma.viewport.scrollAndZoomIntoView([saWrapper]);\n\n var saDoneMsg = 'Generated standalone docs in ' + saTotalSec + 's.';\n figma.notify(saDoneMsg);\n figma.ui.postMessage({ type: 'done', message: saDoneMsg });\n return;\n }\n\n // ─── Ensure columns form a clean uniform grid when labels are wider than variants ───\n // When variants are small (e.g. 16px radio buttons), labels like \"State: Error Focus\"\n // are much wider. In that case, reposition variants into a uniform cell grid so that:\n // 1. Each cell is wide enough for the widest label\n // 2. Variants are centered in their cells (including edge cells)\n // 3. colClusters/colWidths represent cell extents (for label & grid line positioning)\n\n if (colAxisProps.length > 0 && colClusters.length > 1) {\n const innerColProp = colAxisProps[colAxisProps.length - 1];\n\n // Measure text width for each column's innermost label\n const colLabelTextWidths = [];\n for (let c = 0; c < colClusters.length; c++) {\n const inCol = gridByCol[c] || [];\n if (inCol.length === 0) {\n colLabelTextWidths.push(0);\n continue;\n }\n const value = inCol[0].props[innerColProp.name];\n const displayText = formatLabel(innerColProp.name, value);\n const textWidth = measureTextWidth(displayText, LABEL_FONT_SIZE);\n colLabelTextWidths.push(textWidth + LABEL_GAP * 2);\n }\n\n const maxLabelWidth = Math.max(...colLabelTextWidths);\n const maxVariantWidth = Math.max(...colWidths);\n\n // Only reposition when labels are wider than the widest variant\n if (maxLabelWidth > maxVariantWidth) {\n const originalColClusters = colClusters.slice();\n const originalColWidths = colWidths.slice();\n\n // Compute original pitch (center-to-center distance, averaged)\n const firstCenter = originalColClusters[0] + originalColWidths[0] / 2;\n const lastCenter = originalColClusters[colClusters.length - 1] + originalColWidths[colClusters.length - 1] / 2;\n const originalPitch = (lastCenter - firstCenter) / (colClusters.length - 1);\n\n // Uniform cell width = max of original pitch and widest label\n const cellWidth = Math.max(originalPitch, maxLabelWidth);\n\n // Reposition all variants to be centered in uniform cells\n for (let c = 0; c < colClusters.length; c++) {\n const idealCenter = cellWidth / 2 + c * cellWidth;\n const variantHalfWidth = originalColWidths[c] / 2;\n const idealLeft = idealCenter - variantHalfWidth;\n\n const inCol = gridByCol[c] || [];\n for (const item of inCol) {\n item.node.x = idealLeft + (item.node.x - originalColClusters[c]);\n }\n }\n\n // Update colClusters and colWidths to represent cell extents\n for (let c = 0; c < colClusters.length; c++) {\n colClusters[c] = c * cellWidth;\n colWidths[c] = cellWidth;\n }\n\n // Resize component set to fit uniform grid\n cs.resize(cellWidth * colClusters.length, cs.height);\n }\n }\n\n // ─── Same for rows: uniform grid when labels are taller than variants ───\n\n if (rowAxisProps.length > 0 && rowClusters.length > 1) {\n const innerRowProp = rowAxisProps[rowAxisProps.length - 1];\n\n // Row labels need enough height for text\n const minRowLabelHeight = LABEL_FONT_SIZE + LABEL_GAP * 2;\n const maxVariantHeight = Math.max(...rowHeights);\n\n if (minRowLabelHeight > maxVariantHeight) {\n const originalRowClusters = rowClusters.slice();\n const originalRowHeights = rowHeights.slice();\n\n const firstCenter = originalRowClusters[0] + originalRowHeights[0] / 2;\n const lastCenter = originalRowClusters[rowClusters.length - 1] + originalRowHeights[rowClusters.length - 1] / 2;\n const originalPitch = (lastCenter - firstCenter) / (rowClusters.length - 1);\n\n const cellHeight = Math.max(originalPitch, minRowLabelHeight);\n\n for (let r = 0; r < rowClusters.length; r++) {\n const idealCenter = cellHeight / 2 + r * cellHeight;\n const variantHalfHeight = originalRowHeights[r] / 2;\n const idealTop = idealCenter - variantHalfHeight;\n\n const inRow = gridByRow[r] || [];\n for (const item of inRow) {\n item.node.y = idealTop + (item.node.y - originalRowClusters[r]);\n }\n }\n\n for (let r = 0; r < rowClusters.length; r++) {\n rowClusters[r] = r * cellHeight;\n rowHeights[r] = cellHeight;\n }\n\n cs.resize(cs.width, cellHeight * rowClusters.length);\n }\n }\n\n // ─── Single-type grid (regular mode): build a uniform grid with per-item label space ───\n\n var rgStGridInfo = null;\n if (singleTypeGrid) {\n rgStGridInfo = buildSingleTypeUniformGrid(\n grid, gridByCol, colClusters, colWidths, rowClusters, rowHeights, singleTypeProp,\n function(g, x, y) { g.node.x = x; g.node.y = y; }\n );\n\n // Resize CS to fit uniform grid\n var stNewCSW = rgStGridInfo.pad + colClusters.length * rgStGridInfo.cellW + (colClusters.length - 1) * rgStGridInfo.pad + rgStGridInfo.pad;\n var stLastRowBottom = rowClusters[rowClusters.length - 1] + rgStGridInfo.cellH;\n var stNewCSH = stLastRowBottom + rgStGridInfo.pad;\n cs.resize(stNewCSW, stNewCSH);\n }\n\n console.log('[Perf] Dimensions + uniform grid:', (Date.now() - tDims) + 'ms');\n\n figma.ui.postMessage({ type: 'status', message: 'Creating wrapper...' });\n\n // ─── 6a: Create or reuse wrapper frame ───\n\n let wrapper = findExistingWrapper(cs);\n const csOriginalParent = cs.parent;\n const csOriginalX = cs.x;\n const csOriginalY = cs.y;\n\n if (wrapper) {\n removeOldLabels(wrapper);\n } else {\n wrapper = figma.createFrame();\n wrapper.name = '❖ ' + cs.name + (options.docLabel ? ' — ' + options.docLabel : ' — Obra Autodocs');\n wrapper.setPluginData('sourceComponentSetId', cs.id);\n wrapper.fills = [];\n wrapper.clipsContent = false;\n\n // Insert wrapper at the component set's position in its parent\n const csIndex = csOriginalParent.children.indexOf(cs);\n csOriginalParent.insertChild(csIndex, wrapper);\n wrapper.x = csOriginalX;\n wrapper.y = csOriginalY;\n\n // Move component set into wrapper\n wrapper.appendChild(cs);\n }\n\n // Position component set within wrapper (offset by label area)\n // Pin CS to top-left so wrapper.resize() doesn't shift it via constraints\n cs.constraints = { horizontal: 'MIN', vertical: 'MIN' };\n\n cs.x = adjustedTotalLabelWidth;\n cs.y = adjustedTotalLabelHeight;\n\n // Set component set stroke to match doc color with dashes\n cs.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n cs.dashPattern = [4, 4];\n\n // Save and clear CS fills so grid lines are visible through the component set\n cs.setPluginData('originalFills', JSON.stringify(cs.fills));\n cs.fills = [];\n\n // ─── Header: title + description (above all labels and grid) ───\n\n var headerY = 0;\n if (options.showTitle && titleOffset > 0) {\n var titleText = createTitleText(cs.name);\n titleText.x = adjustedTotalLabelWidth;\n titleText.y = headerY;\n wrapper.appendChild(titleText);\n headerY += titleOffset;\n }\n if (headerDescFrame) {\n headerDescFrame.x = adjustedTotalLabelWidth;\n headerDescFrame.y = headerY;\n wrapper.appendChild(headerDescFrame);\n headerY += descOffset;\n }\n if (headerDocLinkText) {\n headerDocLinkText.x = adjustedTotalLabelWidth;\n headerDocLinkText.y = headerY;\n wrapper.appendChild(headerDocLinkText);\n }\n\n // ─── Boolean grid: create instances, measure actual sizes, then position everything ───\n\n var expandedGridWidth = cs.width;\n var expandedGridHeight = cs.height;\n var boolGroupHeights = []; // actual height of each non-default group\n var boolGroupWidths = []; // actual width of each non-default group\n var boolGroupData = []; // { instances, width, height } for each group\n\n if (booleanGridInfo) {\n // Compute CS padding from actual variant extents — instances that grow\n // beyond original variant sizes need the same padding maintained\n var csVariants = cs.children.filter(function(c) { return c.type === 'COMPONENT' && c.visible !== false; });\n var csMaxExtentY = 0;\n var csMaxExtentX = 0;\n for (var cvi = 0; cvi < csVariants.length; cvi++) {\n var cvExtY = csVariants[cvi].y + csVariants[cvi].height;\n var cvExtX = csVariants[cvi].x + csVariants[cvi].width;\n if (cvExtY > csMaxExtentY) csMaxExtentY = cvExtY;\n if (cvExtX > csMaxExtentX) csMaxExtentX = cvExtX;\n }\n var csBottomPadding = cs.height - csMaxExtentY;\n var csRightPadding = cs.width - csMaxExtentX;\n\n // Compute CS layout metrics for adaptive grid\n var csPaddingTop = rowClusters.length > 0 ? rowClusters[0] : 0;\n var csPaddingLeft = colClusters.length > 0 ? colClusters[0] : 0;\n\n // ─── Create real instances and compute group dimensions ───\n // Auto-disable improved accuracy when combinations exceed threshold (protects against slow regeneration)\n var boolTotalCombinations = booleanGridInfo.groups.length * csVariants.length;\n var useBoolAccuracy = options.booleanImprovedAccuracy && boolTotalCombinations <= BOOL_ACCURACY_MAX_COMBINATIONS;\n if (options.booleanImprovedAccuracy && !useBoolAccuracy) {\n console.log('[Boolean Grid] Improved accuracy auto-disabled: ' + boolTotalCombinations + ' combinations exceeds ' + BOOL_ACCURACY_MAX_COMBINATIONS + ' limit');\n }\n var tBoolCreate = Date.now();\n\n // Pass 1: Create instances and measure actual sizes. Do NOT reposition yet —\n // we need all groups measured before we can compute the global (uniform) adj.\n for (var bgi = 0; bgi < booleanGridInfo.groups.length; bgi++) {\n var bGroup = booleanGridInfo.groups[bgi];\n var result = createBooleanInstanceGroup(cs, bGroup.props, 0, 0);\n\n var adjRowData = null;\n var adjColData = null;\n\n // Direct measurement: compute per-group adjusted layout from real instance sizes\n if (useBoolAccuracy) {\n if (rowClusters.length > 0) {\n var dAdjRC = [];\n var dAdjRH = [];\n var dCurY = csPaddingTop;\n for (var dari = 0; dari < rowClusters.length; dari++) {\n var dMaxH = rowHeights[dari];\n for (var daii = 0; daii < result.instances.length; daii++) {\n var _diy = result.instances[daii].y;\n if (_diy >= rowClusters[dari] - CLUSTER_TOLERANCE && _diy <= rowClusters[dari] + rowHeights[dari] + CLUSTER_TOLERANCE) {\n if (result.instances[daii].height > dMaxH) dMaxH = result.instances[daii].height;\n }\n }\n dAdjRC.push(dCurY);\n dAdjRH.push(dMaxH);\n if (dari < rowClusters.length - 1) {\n dCurY += dMaxH + (rowClusters[dari + 1] - (rowClusters[dari] + rowHeights[dari]));\n }\n }\n adjRowData = { clusters: dAdjRC, heights: dAdjRH };\n }\n if (colClusters.length > 0) {\n var dAdjCC = [];\n var dAdjCW = [];\n var dCurX = csPaddingLeft;\n for (var daci = 0; daci < colClusters.length; daci++) {\n var dMaxW = colWidths[daci];\n for (var daii2 = 0; daii2 < result.instances.length; daii2++) {\n var _dix = result.instances[daii2].x;\n if (_dix >= colClusters[daci] - CLUSTER_TOLERANCE && _dix <= colClusters[daci] + colWidths[daci] + CLUSTER_TOLERANCE) {\n if (result.instances[daii2].width > dMaxW) dMaxW = result.instances[daii2].width;\n }\n }\n dAdjCC.push(dCurX);\n dAdjCW.push(dMaxW);\n if (daci < colClusters.length - 1) {\n dCurX += dMaxW + (colClusters[daci + 1] - (colClusters[daci] + colWidths[daci]));\n }\n }\n adjColData = { clusters: dAdjCC, widths: dAdjCW };\n }\n }\n\n // Store measurement results only (no repositioning yet)\n boolGroupData.push({ instances: result.instances, width: result.width, height: result.height, adjRowData: adjRowData, adjColData: adjColData });\n }\n\n // Compute global adj: take the max row height / col width across all boolean groups.\n // For column-axis booleans, all groups and the base CS are side-by-side and must share\n // the same row positions. For row-axis booleans they share column positions.\n var globalAdjRowData = null;\n var globalAdjColData = null;\n if (useBoolAccuracy) {\n if (booleanGridInfo.axis === 'col' && rowClusters.length > 0) {\n var gRH = rowHeights.slice();\n for (var gri = 0; gri < boolGroupData.length; gri++) {\n if (boolGroupData[gri].adjRowData) {\n for (var grii = 0; grii < gRH.length; grii++) {\n if (boolGroupData[gri].adjRowData.heights[grii] > gRH[grii]) gRH[grii] = boolGroupData[gri].adjRowData.heights[grii];\n }\n }\n }\n var rowsGrew = gRH.some(function(h, i) { return h > rowHeights[i]; });\n if (rowsGrew) {\n var gRC = [];\n var gCurY = csPaddingTop;\n for (var gri2 = 0; gri2 < rowClusters.length; gri2++) {\n gRC.push(gCurY);\n if (gri2 < rowClusters.length - 1) {\n gCurY += gRH[gri2] + (rowClusters[gri2 + 1] - (rowClusters[gri2] + rowHeights[gri2]));\n }\n }\n globalAdjRowData = { clusters: gRC, heights: gRH };\n }\n }\n if (booleanGridInfo.axis === 'row' && colClusters.length > 0) {\n var gCW = colWidths.slice();\n for (var gci = 0; gci < boolGroupData.length; gci++) {\n if (boolGroupData[gci].adjColData) {\n for (var gcii = 0; gcii < gCW.length; gcii++) {\n if (boolGroupData[gci].adjColData.widths[gcii] > gCW[gcii]) gCW[gcii] = boolGroupData[gci].adjColData.widths[gcii];\n }\n }\n }\n var colsGrew = gCW.some(function(w, i) { return w > colWidths[i]; });\n if (colsGrew) {\n var gCC = [];\n var gCurX = csPaddingLeft;\n for (var gcii2 = 0; gcii2 < colClusters.length; gcii2++) {\n gCC.push(gCurX);\n if (gcii2 < colClusters.length - 1) {\n gCurX += gCW[gcii2] + (colClusters[gcii2 + 1] - (colClusters[gcii2] + colWidths[gcii2]));\n }\n }\n globalAdjColData = { clusters: gCC, widths: gCW };\n }\n }\n }\n\n // Pass 2: Apply the global (uniform) adj to every boolean group, then reposition their\n // instances and compute each group's final dimensions.\n for (var bgi2a = 0; bgi2a < boolGroupData.length; bgi2a++) {\n var bgData = boolGroupData[bgi2a];\n var effAdjRow = globalAdjRowData || bgData.adjRowData;\n var effAdjCol = globalAdjColData || bgData.adjColData;\n\n // Reposition instances using the effective (globally-uniform) adj data\n if (effAdjRow || effAdjCol) {\n for (var rii = 0; rii < bgData.instances.length; rii++) {\n var inst = bgData.instances[rii];\n if (effAdjRow) {\n for (var rmi = 0; rmi < rowClusters.length; rmi++) {\n if (inst.y >= rowClusters[rmi] - CLUSTER_TOLERANCE && inst.y <= rowClusters[rmi] + rowHeights[rmi] + CLUSTER_TOLERANCE) {\n inst.y = effAdjRow.clusters[rmi];\n break;\n }\n }\n }\n if (effAdjCol) {\n for (var cmi = 0; cmi < colClusters.length; cmi++) {\n if (inst.x >= colClusters[cmi] - CLUSTER_TOLERANCE && inst.x <= colClusters[cmi] + colWidths[cmi] + CLUSTER_TOLERANCE) {\n inst.x = effAdjCol.clusters[cmi];\n break;\n }\n }\n }\n }\n }\n\n // Store effective adj so downstream label/grid-line code uses the uniform values\n bgData.adjRowData = effAdjRow;\n bgData.adjColData = effAdjCol;\n\n // Compute group dimensions using the effective adj data\n var groupHeight, groupWidth;\n if (effAdjRow) {\n var lastRowBottom = effAdjRow.clusters[effAdjRow.clusters.length - 1] + effAdjRow.heights[effAdjRow.heights.length - 1];\n groupHeight = Math.max(cs.height, lastRowBottom + csBottomPadding);\n } else {\n groupHeight = Math.max(cs.height, bgData.height + csBottomPadding);\n }\n if (effAdjCol) {\n var lastColRight = effAdjCol.clusters[effAdjCol.clusters.length - 1] + effAdjCol.widths[effAdjCol.widths.length - 1];\n groupWidth = Math.max(cs.width, lastColRight + csRightPadding);\n } else {\n groupWidth = Math.max(cs.width, bgData.width + csRightPadding);\n }\n\n boolGroupHeights.push(groupHeight);\n boolGroupWidths.push(groupWidth);\n }\n\n // Apply global adj to the base CS so all groups share a uniform grid.\n // For column-axis booleans: expand row heights → reposition CS variants vertically and resize CS.\n // For row-axis booleans: expand col widths → reposition CS variants horizontally and resize CS.\n if (globalAdjRowData) {\n for (var grv = 0; grv < csVariants.length; grv++) {\n var grvY = csVariants[grv].y;\n for (var grvr = 0; grvr < rowClusters.length; grvr++) {\n if (grvY >= rowClusters[grvr] - CLUSTER_TOLERANCE && grvY <= rowClusters[grvr] + rowHeights[grvr] + CLUSTER_TOLERANCE) {\n csVariants[grv].y = globalAdjRowData.clusters[grvr];\n break;\n }\n }\n }\n var newCSHeight = globalAdjRowData.clusters[globalAdjRowData.clusters.length - 1] + globalAdjRowData.heights[globalAdjRowData.heights.length - 1] + csBottomPadding;\n cs.resize(cs.width, newCSHeight);\n // Update rowClusters/rowHeights in-place so all downstream code (labels, grid lines) uses the new positions\n for (var gru = 0; gru < rowClusters.length; gru++) {\n rowClusters[gru] = globalAdjRowData.clusters[gru];\n rowHeights[gru] = globalAdjRowData.heights[gru];\n }\n expandedGridHeight = newCSHeight;\n }\n if (globalAdjColData) {\n for (var gcv = 0; gcv < csVariants.length; gcv++) {\n var gcvX = csVariants[gcv].x;\n for (var gcvc = 0; gcvc < colClusters.length; gcvc++) {\n if (gcvX >= colClusters[gcvc] - CLUSTER_TOLERANCE && gcvX <= colClusters[gcvc] + colWidths[gcvc] + CLUSTER_TOLERANCE) {\n csVariants[gcv].x = globalAdjColData.clusters[gcvc];\n break;\n }\n }\n }\n var newCSWidth = globalAdjColData.clusters[globalAdjColData.clusters.length - 1] + globalAdjColData.widths[globalAdjColData.widths.length - 1] + csRightPadding;\n cs.resize(newCSWidth, cs.height);\n // Update colClusters/colWidths in-place so all downstream code uses the new positions\n for (var gcu = 0; gcu < colClusters.length; gcu++) {\n colClusters[gcu] = globalAdjColData.clusters[gcu];\n colWidths[gcu] = globalAdjColData.widths[gcu];\n }\n expandedGridWidth = newCSWidth;\n }\n\n console.log('⏱ Boolean grid (instances + accuracy):', (Date.now() - tBoolCreate) + 'ms');\n\n // Calculate expanded dimensions using actual group sizes\n if (booleanGridInfo.axis === 'row') {\n var totalGroupHeight = 0;\n for (var bhi = 0; bhi < boolGroupHeights.length; bhi++) {\n totalGroupHeight += boolGroupHeights[bhi] + BOOL_GROUP_GAP;\n }\n expandedGridHeight = cs.height + totalGroupHeight;\n } else {\n var totalGroupWidth = 0;\n for (var bwi = 0; bwi < boolGroupWidths.length; bwi++) {\n totalGroupWidth += boolGroupWidths[bwi] + BOOL_GROUP_GAP;\n }\n expandedGridWidth = cs.width + totalGroupWidth;\n }\n }\n\n // Resize wrapper to fit labels + expanded grid\n wrapper.resize(\n adjustedTotalLabelWidth + expandedGridWidth,\n adjustedTotalLabelHeight + expandedGridHeight\n );\n\n figma.ui.postMessage({ type: 'status', message: 'Creating labels...' });\n var tLabels = Date.now();\n\n // ─── Boolean grid: position instances, tints, and labels ───\n\n if (booleanGridInfo) {\n // Calculate cumulative Y/X offsets for each group\n var bCumulativeOffset = 0;\n\n for (var bgi2 = 0; bgi2 < booleanGridInfo.groups.length; bgi2++) {\n var bGroupIndex = bgi2 + 1;\n var bGroupOffsetX, bGroupOffsetY;\n var bGroupH = boolGroupHeights[bgi2];\n var bGroupW = boolGroupWidths[bgi2];\n\n if (booleanGridInfo.axis === 'row') {\n bGroupOffsetX = adjustedTotalLabelWidth;\n bGroupOffsetY = adjustedTotalLabelHeight + cs.height + bCumulativeOffset + BOOL_GROUP_GAP;\n bCumulativeOffset += bGroupH + BOOL_GROUP_GAP;\n } else {\n bGroupOffsetX = adjustedTotalLabelWidth + cs.width + bCumulativeOffset + BOOL_GROUP_GAP;\n bGroupOffsetY = adjustedTotalLabelHeight;\n bCumulativeOffset += bGroupW + BOOL_GROUP_GAP;\n }\n\n // Store offset for label/grid use\n boolGroupData[bgi2].offsetX = bGroupOffsetX;\n boolGroupData[bgi2].offsetY = bGroupOffsetY;\n\n // Background tint — covers exactly the group content area\n var tintW = booleanGridInfo.axis === 'row' ? cs.width : bGroupW;\n var tintH = booleanGridInfo.axis === 'row' ? bGroupH : cs.height;\n var tint = createBackgroundTint(bGroupOffsetX, bGroupOffsetY, tintW, tintH);\n wrapper.appendChild(tint);\n boolGroupData[bgi2].tint = tint;\n\n // Reposition instances from (0,0) to actual offset\n var bInstances = boolGroupData[bgi2].instances;\n for (var bii = 0; bii < bInstances.length; bii++) {\n bInstances[bii].x += bGroupOffsetX;\n bInstances[bii].y += bGroupOffsetY;\n wrapper.appendChild(bInstances[bii]);\n }\n }\n\n // Boolean axis labels\n if (booleanGridInfo.axis === 'row') {\n var hasBoolBracket = rowAxisProps.length > 0;\n // Default group label\n var bDefaultLabel = createRowLabel(\n booleanGridInfo.defaultLabel, 0, adjustedTotalLabelHeight,\n boolLabelWidth, cs.height, hasBoolBracket\n );\n wrapper.appendChild(bDefaultLabel);\n // Non-default group labels\n for (var bgli = 0; bgli < booleanGridInfo.groups.length; bgli++) {\n var bLabel = createRowLabel(\n booleanGridInfo.groups[bgli].label, 0, boolGroupData[bgli].offsetY,\n boolLabelWidth, boolGroupHeights[bgli], hasBoolBracket\n );\n wrapper.appendChild(bLabel);\n }\n } else {\n var hasBoolBracket = colAxisProps.length > 0;\n // Default group label\n var bDefaultLabel = createColLabel(\n booleanGridInfo.defaultLabel, adjustedTotalLabelWidth, headerOffset,\n cs.width, boolLabelHeight, hasBoolBracket\n );\n wrapper.appendChild(bDefaultLabel);\n // Non-default group labels\n for (var bgli = 0; bgli < booleanGridInfo.groups.length; bgli++) {\n var bLabel = createColLabel(\n booleanGridInfo.groups[bgli].label, boolGroupData[bgli].offsetX, headerOffset,\n boolGroupWidths[bgli], boolLabelHeight, hasBoolBracket\n );\n wrapper.appendChild(bLabel);\n }\n }\n\n // ─── Boolean Grid Border: unified stroke around entire grid area ───\n // Save CS strokes, create a border frame with the same style, remove CS strokes\n var csStrokeData = {\n strokes: JSON.parse(JSON.stringify(cs.strokes)),\n strokeWeight: cs.strokeWeight,\n dashPattern: cs.dashPattern.slice(),\n strokeAlign: cs.strokeAlign,\n cornerRadius: cs.cornerRadius\n };\n cs.setPluginData('originalStrokes', JSON.stringify(csStrokeData));\n\n var borderFrame = figma.createFrame();\n borderFrame.name = 'Boolean Grid Border';\n borderFrame.fills = [];\n borderFrame.clipsContent = false;\n borderFrame.strokes = csStrokeData.strokes;\n borderFrame.strokeWeight = csStrokeData.strokeWeight;\n borderFrame.dashPattern = csStrokeData.dashPattern;\n borderFrame.strokeAlign = csStrokeData.strokeAlign;\n borderFrame.cornerRadius = csStrokeData.cornerRadius;\n borderFrame.locked = true;\n borderFrame.x = cs.x;\n borderFrame.y = cs.y;\n if (booleanGridInfo.axis === 'row') {\n borderFrame.resize(cs.width, expandedGridHeight);\n } else {\n borderFrame.resize(expandedGridWidth, cs.height);\n }\n wrapper.appendChild(borderFrame);\n\n // Remove strokes from CS so the border frame is the only visible border\n cs.strokes = [];\n }\n\n // ─── 6b: Create row labels (left side) ───\n // Row labels are positioned relative to the wrapper.\n // Their y-coordinates need to account for the adjusted label height offset (column label area above).\n\n let labelXOffset = boolLabelWidth;\n for (let pi = 0; pi < rowAxisProps.length; pi++) {\n const prop = rowAxisProps[pi];\n const colWidth = rowLabelWidths[pi];\n const isInner = rowAxisProps.length > 1 && pi === rowAxisProps.length - 1;\n const hasBracket = !isInner && rowAxisProps.length > 1;\n\n // Group rows by this property's value (using original grid positions)\n const valueGroups = getRowGroups(prop.name, grid, gridByRow, rowClusters, rowHeights);\n\n for (const group of valueGroups) {\n // Offset y by totalLabelHeight to sit below column labels\n const groupY = adjustedTotalLabelHeight + group.startY;\n const groupHeight = group.endY - group.startY;\n\n const displayValue = formatLabel(prop.name, group.value);\n\n const label = createRowLabel(\n displayValue,\n labelXOffset,\n groupY,\n colWidth,\n groupHeight,\n hasBracket\n );\n wrapper.appendChild(label);\n }\n\n labelXOffset += colWidth;\n }\n\n // ─── 6c: Create column labels (top) ───\n // Column labels are positioned relative to the wrapper.\n // Their x-coordinates need to account for the totalLabelWidth offset (row label area to the left).\n\n let labelYOffset = boolLabelHeight + headerOffset;\n for (let pi = 0; pi < colAxisProps.length; pi++) {\n const prop = colAxisProps[pi];\n const rowHeight = colLabelHeights[pi];\n const isInner = colAxisProps.length > 1 && pi === colAxisProps.length - 1;\n const hasBracket = !isInner && colAxisProps.length > 1;\n\n // Group columns by this property's value (using original grid positions)\n const valueGroups = getColGroups(prop.name, grid, gridByCol, colClusters, colWidths);\n\n for (const group of valueGroups) {\n // Offset x by totalLabelWidth to sit to the right of row labels\n const groupX = adjustedTotalLabelWidth + group.startX;\n const groupWidth = group.endX - group.startX;\n\n const displayValue = formatLabel(prop.name, group.value);\n\n const label = createColLabel(\n displayValue,\n groupX,\n labelYOffset,\n groupWidth,\n rowHeight,\n hasBracket\n );\n wrapper.appendChild(label);\n }\n\n labelYOffset += rowHeight;\n }\n\n // ─── Per-item labels for single-type grid (regular mode) ───\n\n if (singleTypeGrid && rgStGridInfo) {\n createSingleTypeGridLabels(wrapper, grid, singleTypeProp, colClusters, colWidths,\n rowClusters, rgStGridInfo.maxVarH, adjustedTotalLabelWidth, adjustedTotalLabelHeight);\n }\n\n // ─── Repeat inner variant labels for non-default boolean groups ───\n\n if (booleanGridInfo) {\n for (var brgi = 0; brgi < booleanGridInfo.groups.length; brgi++) {\n var brAdjRC = (boolGroupData[brgi].adjRowData && boolGroupData[brgi].adjRowData.clusters) || rowClusters;\n var brAdjRH = (boolGroupData[brgi].adjRowData && boolGroupData[brgi].adjRowData.heights) || rowHeights;\n var brAdjCC = (boolGroupData[brgi].adjColData && boolGroupData[brgi].adjColData.clusters) || colClusters;\n var brAdjCW = (boolGroupData[brgi].adjColData && boolGroupData[brgi].adjColData.widths) || colWidths;\n if (booleanGridInfo.axis === 'row') {\n // Repeat row labels (left side) for this boolean group\n var brLabelXOffset = boolLabelWidth;\n for (var brpi = 0; brpi < rowAxisProps.length; brpi++) {\n var brProp = rowAxisProps[brpi];\n var brColWidth = rowLabelWidths[brpi];\n var brIsInner = rowAxisProps.length > 1 && brpi === rowAxisProps.length - 1;\n var brHasBracket = !brIsInner && rowAxisProps.length > 1;\n var brValueGroups = getRowGroups(brProp.name, grid, gridByRow, brAdjRC, brAdjRH);\n for (var brvi = 0; brvi < brValueGroups.length; brvi++) {\n var brVGroup = brValueGroups[brvi];\n var brGroupY = boolGroupData[brgi].offsetY + brVGroup.startY;\n var brGroupHeight = brVGroup.endY - brVGroup.startY;\n var brDisplayValue = formatLabel(brProp.name, brVGroup.value);\n var brLabel = createRowLabel(brDisplayValue, brLabelXOffset, brGroupY, brColWidth, brGroupHeight, brHasBracket);\n wrapper.appendChild(brLabel);\n }\n brLabelXOffset += brColWidth;\n }\n } else {\n // Repeat column labels (top) for this boolean group\n var brLabelYOffset = boolLabelHeight + headerOffset;\n for (var brpi = 0; brpi < colAxisProps.length; brpi++) {\n var brProp = colAxisProps[brpi];\n var brRowHeight = colLabelHeights[brpi];\n var brIsInner = colAxisProps.length > 1 && brpi === colAxisProps.length - 1;\n var brHasBracket = !brIsInner && colAxisProps.length > 1;\n var brValueGroups = getColGroups(brProp.name, grid, gridByCol, brAdjCC, brAdjCW);\n for (var brvi = 0; brvi < brValueGroups.length; brvi++) {\n var brVGroup = brValueGroups[brvi];\n var brGroupX = boolGroupData[brgi].offsetX + brVGroup.startX;\n var brGroupWidth = brVGroup.endX - brVGroup.startX;\n var brDisplayValue = formatLabel(brProp.name, brVGroup.value);\n var brLabel = createColLabel(brDisplayValue, brGroupX, brLabelYOffset, brGroupWidth, brRowHeight, brHasBracket);\n wrapper.appendChild(brLabel);\n }\n brLabelYOffset += brRowHeight;\n }\n }\n }\n }\n\n console.log('[Perf] Label creation:', (Date.now() - tLabels) + 'ms');\n\n // ─── Grid lines (if selected) ───\n\n if (options.showGrid) {\n createGridLines(wrapper, colClusters, rowClusters, colWidths, rowHeights, cs.x, cs.y, cs.width, cs.height,\n singleTypeGrid ? grid : null);\n // Additional grid lines for boolean groups (using actual group dimensions)\n if (booleanGridInfo) {\n for (var ggi = 0; ggi < booleanGridInfo.groups.length; ggi++) {\n var gRC = (boolGroupData[ggi].adjRowData && boolGroupData[ggi].adjRowData.clusters) || rowClusters;\n var gRH = (boolGroupData[ggi].adjRowData && boolGroupData[ggi].adjRowData.heights) || rowHeights;\n var gCC = (boolGroupData[ggi].adjColData && boolGroupData[ggi].adjColData.clusters) || colClusters;\n var gCW = (boolGroupData[ggi].adjColData && boolGroupData[ggi].adjColData.widths) || colWidths;\n if (booleanGridInfo.axis === 'row') {\n createGridLines(wrapper, gCC, gRC, gCW, gRH,\n boolGroupData[ggi].offsetX, boolGroupData[ggi].offsetY, cs.width, boolGroupHeights[ggi]);\n } else {\n createGridLines(wrapper, gCC, gRC, gCW, gRH,\n boolGroupData[ggi].offsetX, boolGroupData[ggi].offsetY, boolGroupWidths[ggi], cs.height);\n }\n }\n // Dividers between boolean groups\n // Double-line dividers only for nested boolean groups (multiple bool props);\n // single-line divider for simple single-boolean components\n var useDoubleLine = booleanGridInfo.boolPropCount > 1;\n for (var dgi = 0; dgi < booleanGridInfo.groups.length; dgi++) {\n var divLine1 = figma.createLine();\n divLine1.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n divLine1.strokeWeight = GRID_STROKE_WEIGHT;\n divLine1.dashPattern = GRID_DASH_PATTERN;\n if (booleanGridInfo.axis === 'row') {\n var prevBottom = dgi === 0 ? (cs.y + cs.height) : (boolGroupData[dgi - 1].offsetY + boolGroupHeights[dgi - 1]);\n divLine1.resize(cs.width, 0);\n divLine1.x = cs.x;\n divLine1.y = prevBottom;\n } else {\n var prevRight = dgi === 0 ? (cs.x + cs.width) : (boolGroupData[dgi - 1].offsetX + boolGroupWidths[dgi - 1]);\n divLine1.rotation = -90;\n divLine1.resize(cs.height, 0);\n divLine1.x = prevRight;\n divLine1.y = cs.y;\n }\n wrapper.appendChild(divLine1);\n if (useDoubleLine) {\n var divLine2 = figma.createLine();\n divLine2.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n divLine2.strokeWeight = GRID_STROKE_WEIGHT;\n divLine2.dashPattern = GRID_DASH_PATTERN;\n if (booleanGridInfo.axis === 'row') {\n divLine2.resize(cs.width, 0);\n divLine2.x = cs.x;\n divLine2.y = boolGroupData[dgi].offsetY;\n } else {\n divLine2.rotation = -90;\n divLine2.resize(cs.height, 0);\n divLine2.x = boolGroupData[dgi].offsetX;\n divLine2.y = cs.y;\n }\n wrapper.appendChild(divLine2);\n }\n }\n }\n }\n\n // ─── Extra sections (Boolean visibility + Nested instances) ───\n\n var extraSections = [];\n\n if (options.showBooleanVisibility && !booleanGridInfo) {\n figma.ui.postMessage({ type: 'status', message: 'Creating boolean visibility examples...' });\n var boolSection;\n if (options.booleanScope) {\n boolSection = await createScopedBooleanSection(cs, options.booleanScope, options.booleanCombination || 'individual', options.booleanDisplayMode || 'list');\n } else {\n boolSection = await createBooleanVisibilitySection(cs, options.booleanCombination || 'individual', options.enabledBooleanProps, options.booleanDisplayMode || 'list');\n }\n if (boolSection) extraSections.push(boolSection);\n }\n\n if (options.showNestedInstances) {\n figma.ui.postMessage({ type: 'status', message: 'Creating nested instance examples...' });\n var nestedSection = await createNestedInstancesSection(cs, options.nestedInstancesMode || 'representative', options.enabledNestedInstances);\n if (nestedSection) extraSections.push(nestedSection);\n }\n\n if (extraSections.length > 0) {\n var EXTRAS_GAP = 24;\n var extrasContainer = figma.createFrame();\n extrasContainer.name = 'Property Combinations';\n extrasContainer.fills = [];\n extrasContainer.clipsContent = false;\n extrasContainer.layoutMode = 'VERTICAL';\n extrasContainer.itemSpacing = EXTRAS_GAP;\n extrasContainer.counterAxisSizingMode = 'AUTO';\n extrasContainer.primaryAxisSizingMode = 'AUTO';\n\n for (var ei = 0; ei < extraSections.length; ei++) {\n extrasContainer.appendChild(extraSections[ei]);\n }\n\n if (_autoLayoutExtras) {\n extrasContainer.paddingLeft = adjustedTotalLabelWidth;\n } else {\n extrasContainer.x = adjustedTotalLabelWidth;\n extrasContainer.y = adjustedTotalLabelHeight + expandedGridHeight + EXTRAS_GAP;\n }\n wrapper.appendChild(extrasContainer);\n\n if (!_autoLayoutExtras) {\n wrapper.resize(\n Math.max(wrapper.width, adjustedTotalLabelWidth + extrasContainer.width),\n extrasContainer.y + extrasContainer.height\n );\n }\n if (DEBUG) { console.log('[Generate] Extras container added (' + extraSections.length + ' sections)'); }\n }\n\n // Description is now placed at the top (headerDescFrame) alongside the title\n\n // ─── Step 9: Wrap generated elements in Documentation frame ───\n\n var docsFrame = figma.createFrame();\n docsFrame.name = 'Documentation';\n docsFrame.fills = [];\n docsFrame.clipsContent = false;\n docsFrame.locked = false;\n docsFrame.constraints = { horizontal: 'MIN', vertical: 'MIN' };\n docsFrame.x = 0;\n docsFrame.y = 0;\n // Size to base grid area when auto layout (extras are a sibling), or full wrapper when manual\n if (_autoLayoutExtras) {\n docsFrame.resize(adjustedTotalLabelWidth + expandedGridWidth, adjustedTotalLabelHeight + expandedGridHeight);\n } else {\n docsFrame.resize(wrapper.width, wrapper.height);\n }\n wrapper.appendChild(docsFrame);\n\n // Move generated elements into docs frame (keep CS, docsFrame itself, and Property Combinations out when auto layout)\n var docsChildren = [];\n for (var di = 0; di < wrapper.children.length; di++) {\n var dChild = wrapper.children[di];\n if (dChild.type !== 'COMPONENT_SET' && dChild.type !== 'COMPONENT' && dChild !== docsFrame) {\n if (_autoLayoutExtras && dChild.name === 'Property Combinations') continue;\n docsChildren.push(dChild);\n }\n }\n for (var di2 = 0; di2 < docsChildren.length; di2++) {\n docsChildren[di2].constraints = { horizontal: 'MIN', vertical: 'MIN' };\n docsFrame.appendChild(docsChildren[di2]);\n }\n\n // When auto layout, ensure Property Combinations is ordered after docsFrame\n if (_autoLayoutExtras) {\n var extrasInWrapper = null;\n for (var ewi = 0; ewi < wrapper.children.length; ewi++) {\n if (wrapper.children[ewi].name === 'Property Combinations') {\n extrasInWrapper = wrapper.children[ewi];\n break;\n }\n }\n if (extrasInWrapper) {\n wrapper.appendChild(extrasInWrapper);\n }\n }\n\n // ─── Step 9b: Variable modes ───\n\n if (options.variableModes && options.variableModes.groups && options.variableModes.groups.length > 0) {\n figma.ui.postMessage({ type: 'status', message: 'Generating variable modes...' });\n\n var vmGroups = options.variableModes.groups;\n var VM_GAP = 48;\n var VM_LABEL_HEIGHT = SIMPLE_LABEL_ROW_HEIGHT;\n\n // Resolve all collections referenced by any group\n var vmCollectionCache = {};\n for (var vmci = 0; vmci < vmGroups.length; vmci++) {\n for (var vmcj = 0; vmcj < vmGroups[vmci].collections.length; vmcj++) {\n var vmCid = vmGroups[vmci].collections[vmcj].collectionId;\n if (!vmCollectionCache[vmCid]) {\n vmCollectionCache[vmCid] = await figma.variables.getVariableCollectionByIdAsync(vmCid);\n }\n }\n }\n\n // Try to find a background color variable for dark mode support (search all collections)\n var vmBgVariable = null;\n try {\n for (var vmBgCid in vmCollectionCache) {\n if (vmBgVariable) break;\n var vmBgColl = vmCollectionCache[vmBgCid];\n if (!vmBgColl) continue;\n var vmVarIds = vmBgColl.variableIds;\n for (var vmvi = 0; vmvi < vmVarIds.length; vmvi++) {\n var vmVar = await figma.variables.getVariableByIdAsync(vmVarIds[vmvi]);\n if (vmVar && vmVar.resolvedType === 'COLOR') {\n var vmVarName = vmVar.name.toLowerCase();\n if (vmVarName.indexOf('background') !== -1 || vmVarName.indexOf('bg') !== -1 || vmVarName.indexOf('surface') !== -1 || vmVarName.indexOf('body') !== -1) {\n vmBgVariable = vmVar;\n break;\n }\n }\n }\n }\n } catch (vmBgErr) {\n console.warn('[VarModes] Could not search for background variable:', vmBgErr.message);\n }\n\n var vmModesContainer = figma.createFrame();\n vmModesContainer.name = 'Modes';\n vmModesContainer.fills = [];\n vmModesContainer.clipsContent = false;\n vmModesContainer.layoutMode = 'VERTICAL';\n vmModesContainer.primaryAxisSizingMode = 'AUTO';\n vmModesContainer.counterAxisSizingMode = 'AUTO';\n vmModesContainer.itemSpacing = VM_GAP;\n\n var vmGridW = expandedGridWidth;\n var vmGridH = cs.height;\n\n for (var vmmi = 0; vmmi < vmGroups.length; vmmi++) {\n var vmGroup = vmGroups[vmmi];\n\n // Per-mode wrapper — include collection names for traceability\n var vmModeWrapper = figma.createFrame();\n var vmCollNames = [];\n for (var vmni = 0; vmni < vmGroup.collections.length; vmni++) {\n var vmNC = vmCollectionCache[vmGroup.collections[vmni].collectionId];\n if (vmNC) vmCollNames.push(vmNC.name);\n }\n vmModeWrapper.name = 'Mode: ' + vmGroup.name + (vmCollNames.length > 0 ? ' (' + vmCollNames.join(', ') + ')' : '');\n vmModeWrapper.fills = [];\n vmModeWrapper.clipsContent = false;\n\n // Mode label (sits at top of mode wrapper)\n vmModeWrapper.appendChild(createColLabel(vmGroup.name, adjustedTotalLabelWidth, boolLabelHeight, vmGridW, VM_LABEL_HEIGHT, false));\n\n // The grid area starts after mode label + column labels\n var vmGridY = VM_LABEL_HEIGHT + adjustedTotalLabelHeight;\n\n // Mode frame with variable modes set from all collections in this group\n var vmFrame = figma.createFrame();\n vmFrame.name = vmGroup.name;\n vmFrame.clipsContent = false;\n vmFrame.x = adjustedTotalLabelWidth;\n vmFrame.y = vmGridY;\n vmFrame.resize(vmGridW, vmGridH);\n\n if (vmBgVariable) {\n vmFrame.fills = [figma.variables.setBoundVariableForPaint({ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', vmBgVariable)];\n } else {\n vmFrame.fills = [];\n }\n\n // Apply all collection modes for this group\n for (var vmgci = 0; vmgci < vmGroup.collections.length; vmgci++) {\n var vmGC = vmGroup.collections[vmgci];\n var vmGColl = vmCollectionCache[vmGC.collectionId];\n if (!vmGColl) continue;\n try {\n vmFrame.setExplicitVariableModeForCollection(vmGColl, vmGC.modeId);\n } catch (vmErr) {\n console.error('[VarModes] Failed to set mode \"' + vmGroup.name + '\" for collection \"' + vmGColl.name + '\":', vmErr.message);\n }\n }\n\n // Create instances at variant positions\n for (var vmgi = 0; vmgi < grid.length; vmgi++) {\n var vmInst = grid[vmgi].node.createInstance();\n vmInst.x = grid[vmgi].node.x;\n vmInst.y = grid[vmgi].node.y;\n vmFrame.appendChild(vmInst);\n }\n vmModeWrapper.appendChild(vmFrame);\n\n // Dashed border\n var vmBorder = figma.createFrame();\n vmBorder.name = 'Grid Border';\n vmBorder.fills = [];\n vmBorder.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n vmBorder.dashPattern = [4, 4];\n vmBorder.x = adjustedTotalLabelWidth;\n vmBorder.y = vmGridY;\n vmBorder.resize(vmGridW, vmGridH);\n vmModeWrapper.appendChild(vmBorder);\n\n // Grid lines\n if (options.showGrid) {\n createGridLines(vmModeWrapper, colClusters, rowClusters, colWidths, rowHeights,\n adjustedTotalLabelWidth, vmGridY, vmGridW, vmGridH,\n singleTypeGrid ? grid : null);\n }\n\n // Row labels\n for (var vmrpi = 0; vmrpi < rowAxisProps.length; vmrpi++) {\n var vmRProp = rowAxisProps[vmrpi];\n var vmRColW = rowLabelWidths[vmrpi];\n var vmRIsInner = rowAxisProps.length > 1 && vmrpi === rowAxisProps.length - 1;\n var vmRHasBracket = !vmRIsInner && rowAxisProps.length > 1;\n var vmRGroups = getRowGroups(vmRProp.name, grid, gridByRow, rowClusters, rowHeights);\n var vmRLXO = 0;\n for (var vmrpj = 0; vmrpj < vmrpi; vmrpj++) vmRLXO += rowLabelWidths[vmrpj];\n for (var vmrvi = 0; vmrvi < vmRGroups.length; vmrvi++) {\n var vmRG = vmRGroups[vmrvi];\n vmModeWrapper.appendChild(createRowLabel(\n formatLabel(vmRProp.name, vmRG.value),\n vmRLXO, vmGridY + vmRG.startY,\n vmRColW, vmRG.endY - vmRG.startY, vmRHasBracket\n ));\n }\n }\n\n // Column labels (after mode label, before grid)\n var vmLYO = boolLabelHeight + VM_LABEL_HEIGHT;\n for (var vmcpi = 0; vmcpi < colAxisProps.length; vmcpi++) {\n var vmCProp = colAxisProps[vmcpi];\n var vmCRowH = colLabelHeights[vmcpi];\n var vmCIsInner = colAxisProps.length > 1 && vmcpi === colAxisProps.length - 1;\n var vmCHasBracket = !vmCIsInner && colAxisProps.length > 1;\n var vmCGroups = getColGroups(vmCProp.name, grid, gridByCol, colClusters, colWidths);\n for (var vmcvi = 0; vmcvi < vmCGroups.length; vmcvi++) {\n var vmCG = vmCGroups[vmcvi];\n vmModeWrapper.appendChild(createColLabel(\n formatLabel(vmCProp.name, vmCG.value),\n adjustedTotalLabelWidth + vmCG.startX, vmLYO,\n vmCG.endX - vmCG.startX, vmCRowH, vmCHasBracket\n ));\n }\n vmLYO += vmCRowH;\n }\n\n // Per-item labels for single-type grid (regular mode variable modes)\n if (singleTypeGrid && rgStGridInfo) {\n createSingleTypeGridLabels(vmModeWrapper, grid, singleTypeProp, colClusters, colWidths,\n rowClusters, rgStGridInfo.maxVarH, adjustedTotalLabelWidth, vmGridY);\n }\n\n // Size the per-mode wrapper (mode label + col labels + grid)\n var vmModeW = adjustedTotalLabelWidth + vmGridW;\n var vmModeH = vmGridY + vmGridH;\n vmModeWrapper.resize(vmModeW, vmModeH);\n\n vmModesContainer.appendChild(vmModeWrapper);\n }\n\n wrapper.appendChild(vmModesContainer);\n }\n\n // ─── Make wrapper auto-layout vertical (when auto layout extras enabled or variable modes present) ───\n\n var hasVarModes = options.variableModes && options.variableModes.groups && options.variableModes.groups.length > 0;\n if (_autoLayoutExtras || hasVarModes) {\n wrapper.layoutMode = 'VERTICAL';\n wrapper.primaryAxisSizingMode = 'AUTO';\n wrapper.counterAxisSizingMode = 'AUTO';\n wrapper.itemSpacing = _autoLayoutExtras ? 24 : 48;\n wrapper.paddingTop = 0;\n wrapper.paddingBottom = 0;\n wrapper.paddingLeft = 0;\n wrapper.paddingRight = 0;\n\n cs.layoutPositioning = 'ABSOLUTE';\n cs.x = adjustedTotalLabelWidth;\n cs.y = adjustedTotalLabelHeight;\n }\n\n // Ensure CS is above Documentation in the layer panel (last child = top)\n wrapper.appendChild(cs);\n\n // ─── Step 10: Cleanup ───\n\n disposeMeasureNode();\n\n var totalMs = Date.now() - t0;\n var totalSec = (totalMs / 1000).toFixed(2);\n console.log('[Perf] === TOTAL generation time: ' + totalMs + 'ms (' + totalSec + 's) ===');\n\n figma.currentPage.selection = [wrapper];\n figma.viewport.scrollAndZoomIntoView([wrapper]);\n\n var doneMsg = 'Generated docs in ' + totalSec + 's.';\n figma.notify(doneMsg);\n figma.ui.postMessage({\n type: 'done',\n message: doneMsg\n });\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// VARIABLE MODES — separate frame with mode columns, placed next to regular docs\n// ═══════════════════════════════════════════════════════════════════════════════\n\nasync function generateVariableModes(cs, options, grid, gridByRow, gridByCol,\n colClusters, rowClusters, colWidths, rowHeights,\n colAxisProps, rowAxisProps, colLabelHeights, rowLabelWidths,\n boolLabelHeight, adjustedTotalLabelWidth, adjustedTotalLabelHeight, t0) {\n\n figma.ui.postMessage({ type: 'status', message: 'Generating variable modes...' });\n\n var vmCollId = options.variableModes.collectionId;\n var vmCollection = await figma.variables.getVariableCollectionByIdAsync(vmCollId);\n var vmModes = options.variableModes.modes;\n var VM_GAP = 48;\n var VM_MODE_GAP = 24; // gap between mode columns\n\n // For variable modes, we only care about the base grid labels (no boolean label offsets)\n var vmColLabelHeight = colLabelHeights.reduce(function(s, h) { return s + h; }, 0);\n var vmRowLabelWidth = rowLabelWidths.reduce(function(s, w) { return s + w; }, 0);\n\n // Set doc color\n if (options.color) {\n DOC_COLOR = hexToRgb(options.color);\n } else {\n DOC_COLOR = { r: DEFAULT_COLOR.r, g: DEFAULT_COLOR.g, b: DEFAULT_COLOR.b };\n }\n\n // Pre-fetch all other collections for cross-collection mode matching\n var vmOtherCollections = [];\n try {\n var vmAllColls = await figma.variables.getLocalVariableCollectionsAsync();\n for (var vmaci = 0; vmaci < vmAllColls.length; vmaci++) {\n if (vmAllColls[vmaci].id !== vmCollId) {\n vmOtherCollections.push(vmAllColls[vmaci]);\n }\n }\n } catch (vmFetchErr) {\n console.warn('[VarModes] Could not fetch collections for mode matching:', vmFetchErr.message);\n }\n\n // Try to find a background color variable — search primary collection first, then others\n var vmBgVariable = null;\n var vmBgSearchColls = [vmCollection].concat(vmOtherCollections);\n for (var vmBgSi = 0; vmBgSi < vmBgSearchColls.length && !vmBgVariable; vmBgSi++) {\n try {\n var vmBgColl = vmBgSearchColls[vmBgSi];\n var vmVarIds = vmBgColl.variableIds;\n for (var vmvi = 0; vmvi < vmVarIds.length; vmvi++) {\n var vmVar = await figma.variables.getVariableByIdAsync(vmVarIds[vmvi]);\n if (vmVar && vmVar.resolvedType === 'COLOR') {\n var vmVarName = vmVar.name.toLowerCase();\n if (vmVarName.indexOf('background') !== -1 || vmVarName.indexOf('bg') !== -1 || vmVarName.indexOf('surface') !== -1 || vmVarName.indexOf('body') !== -1) {\n vmBgVariable = vmVar;\n console.log('[VarModes] Found background variable:', vmVar.name, 'in \"' + vmBgColl.name + '\"');\n break;\n }\n }\n }\n } catch (vmBgErr) {\n console.warn('[VarModes] Could not search for background variable:', vmBgErr.message);\n }\n }\n\n // Find or create the variable modes wrapper\n var vmWrapper = findVariableModesWrapper(cs);\n if (vmWrapper) {\n removeOldLabels(vmWrapper);\n } else {\n vmWrapper = figma.createFrame();\n vmWrapper.name = '❖ ' + cs.name + ' — Variable Modes';\n vmWrapper.setPluginData('variableModesDoc', 'true');\n vmWrapper.setPluginData('sourceComponentSetId', cs.id);\n vmWrapper.fills = [];\n vmWrapper.clipsContent = false;\n }\n\n // Place to the right of the regular docs wrapper (or CS parent if no wrapper)\n var vmRefFrame = findExistingWrapper(cs) || cs.parent;\n var vmPlacementParent = vmRefFrame.parent || vmRefFrame;\n if (vmWrapper.parent !== vmPlacementParent) {\n vmPlacementParent.appendChild(vmWrapper);\n }\n vmWrapper.x = vmRefFrame.x + vmRefFrame.width + VM_GAP;\n vmWrapper.y = vmRefFrame.y;\n\n // Use CS dimensions — instances are positioned at their original CS-relative coords\n var vmGridW = cs.width;\n var vmGridH = cs.height;\n\n // Create one mode block per selected mode, stacked vertically\n var vmCumOffsetY = 0;\n var vmMaxW = 0; // track widest mode block for wrapper sizing\n\n for (var vmmi = 0; vmmi < vmModes.length; vmmi++) {\n var vmMode = vmModes[vmmi];\n var vmBlockY = vmCumOffsetY;\n\n // Mode name title\n var vmModeLabel = figma.createText();\n vmModeLabel.fontName = { family: _labelFontFamily, style: 'Regular' };\n vmModeLabel.fontSize = LABEL_FONT_SIZE;\n vmModeLabel.characters = vmMode.name;\n vmModeLabel.fills = [{ type: 'SOLID', color: DOC_COLOR }];\n vmModeLabel.x = vmRowLabelWidth;\n vmModeLabel.y = vmBlockY;\n vmWrapper.appendChild(vmModeLabel);\n vmBlockY += SIMPLE_LABEL_ROW_HEIGHT;\n\n // Column labels for this mode\n var vmLYO = vmBlockY;\n for (var vmcpi = 0; vmcpi < colAxisProps.length; vmcpi++) {\n var vmCProp = colAxisProps[vmcpi];\n var vmCRowH = colLabelHeights[vmcpi];\n var vmCIsInner = colAxisProps.length > 1 && vmcpi === colAxisProps.length - 1;\n var vmCHasBracket = !vmCIsInner && colAxisProps.length > 1;\n var vmCGroups = getColGroups(vmCProp.name, grid, gridByCol, colClusters, colWidths);\n for (var vmcvi = 0; vmcvi < vmCGroups.length; vmcvi++) {\n var vmCG = vmCGroups[vmcvi];\n vmWrapper.appendChild(createColLabel(\n formatLabel(vmCProp.name, vmCG.value),\n vmRowLabelWidth + vmCG.startX, vmLYO,\n vmCG.endX - vmCG.startX, vmCRowH, vmCHasBracket\n ));\n }\n vmLYO += vmCRowH;\n }\n\n var vmGridTopY = vmLYO; // where the grid starts for this mode block\n\n // Row labels for this mode block\n var vmRLXO = 0;\n for (var vmrpi2 = 0; vmrpi2 < rowAxisProps.length; vmrpi2++) {\n var vmRP = rowAxisProps[vmrpi2];\n var vmRII = rowAxisProps.length > 1 && vmrpi2 === rowAxisProps.length - 1;\n var vmRHB = !vmRII && rowAxisProps.length > 1;\n var vmRCW = rowLabelWidths[vmrpi2] || 100;\n var vmRGS = getRowGroups(vmRP.name, grid, gridByRow, rowClusters, rowHeights);\n for (var vmrvi2 = 0; vmrvi2 < vmRGS.length; vmrvi2++) {\n var vmRG2 = vmRGS[vmrvi2];\n vmWrapper.appendChild(createRowLabel(\n formatLabel(vmRP.name, vmRG2.value),\n vmRLXO, vmRG2.startY + vmGridTopY,\n vmRCW, vmRG2.endY - vmRG2.startY, vmRHB\n ));\n }\n vmRLXO += vmRCW;\n }\n\n // Mode frame — instances inside inherit this variable mode\n var vmFrame = figma.createFrame();\n vmFrame.name = 'Mode: ' + vmMode.name;\n vmFrame.clipsContent = true;\n vmFrame.x = vmRowLabelWidth;\n vmFrame.y = vmGridTopY;\n vmFrame.resize(vmGridW, vmGridH);\n vmWrapper.appendChild(vmFrame);\n\n // Set background fill using variable (resolves per mode)\n if (vmBgVariable) {\n vmFrame.fills = [figma.variables.setBoundVariableForPaint({ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', vmBgVariable)];\n } else {\n vmFrame.fills = [];\n }\n\n // Set primary collection mode\n try {\n vmFrame.setExplicitVariableModeForCollection(vmCollection, vmMode.modeId);\n } catch (vmErr) {\n console.error('[VarModes] Failed to set mode \"' + vmMode.name + '\":', vmErr.message);\n }\n\n // Also apply matching modes from other collections\n for (var vmci = 0; vmci < vmOtherCollections.length; vmci++) {\n var vmOtherColl = vmOtherCollections[vmci];\n for (var vmomi = 0; vmomi < vmOtherColl.modes.length; vmomi++) {\n if (vmOtherColl.modes[vmomi].name === vmMode.name) {\n try {\n vmFrame.setExplicitVariableModeForCollection(vmOtherColl, vmOtherColl.modes[vmomi].modeId);\n } catch (vmOtherErr) {\n // Collection may not be used by these instances — skip silently\n }\n break;\n }\n }\n }\n\n // Create instances inside the mode frame\n for (var vmgi = 0; vmgi < grid.length; vmgi++) {\n var vmInst = grid[vmgi].node.createInstance();\n vmInst.x = grid[vmgi].node.x;\n vmInst.y = grid[vmgi].node.y;\n vmFrame.appendChild(vmInst);\n }\n\n // Dashed border\n var vmBorder = figma.createFrame();\n vmBorder.name = 'Grid Border';\n vmBorder.fills = [];\n vmBorder.strokes = [{ type: 'SOLID', color: DOC_COLOR }];\n vmBorder.dashPattern = [4, 4];\n vmBorder.x = vmRowLabelWidth;\n vmBorder.y = vmGridTopY;\n vmBorder.resize(vmGridW, vmGridH);\n vmWrapper.appendChild(vmBorder);\n\n // Grid lines\n if (options.showGrid) {\n createGridLines(vmWrapper, colClusters, rowClusters, colWidths, rowHeights,\n vmRowLabelWidth, vmGridTopY, vmGridW, vmGridH);\n }\n\n var vmBlockW = vmRowLabelWidth + vmGridW;\n if (vmBlockW > vmMaxW) vmMaxW = vmBlockW;\n vmCumOffsetY = vmGridTopY + vmGridH + VM_GAP;\n }\n\n // Resize wrapper to fit all stacked mode blocks\n var vmTotalW = vmMaxW;\n var vmTotalH = vmCumOffsetY - VM_GAP;\n vmWrapper.resize(Math.max(vmTotalW, 1), Math.max(vmTotalH, 1));\n\n // Wrap contents in a Documentation frame\n var vmDocsFrame = figma.createFrame();\n vmDocsFrame.name = 'Documentation';\n vmDocsFrame.fills = [];\n vmDocsFrame.clipsContent = false;\n vmDocsFrame.locked = false;\n vmDocsFrame.constraints = { horizontal: 'MIN', vertical: 'MIN' };\n vmDocsFrame.x = 0;\n vmDocsFrame.y = 0;\n vmDocsFrame.resize(vmWrapper.width, vmWrapper.height);\n vmWrapper.appendChild(vmDocsFrame);\n\n var vmDocsChildren = [];\n for (var vmdi = 0; vmdi < vmWrapper.children.length; vmdi++) {\n var vmDChild = vmWrapper.children[vmdi];\n if (vmDChild !== vmDocsFrame) vmDocsChildren.push(vmDChild);\n }\n for (var vmdi2 = 0; vmdi2 < vmDocsChildren.length; vmdi2++) {\n vmDocsChildren[vmdi2].constraints = { horizontal: 'MIN', vertical: 'MIN' };\n vmDocsFrame.appendChild(vmDocsChildren[vmdi2]);\n }\n\n // Done\n var vmTotalMs = Date.now() - t0;\n var vmTotalSec = (vmTotalMs / 1000).toFixed(2);\n console.log('[VarModes] Generated ' + vmModes.length + ' mode column(s) in ' + vmTotalSec + 's');\n\n figma.currentPage.selection = [vmWrapper];\n figma.viewport.scrollAndZoomIntoView([vmWrapper]);\n\n var vmDoneMsg = 'Generated variable modes docs in ' + vmTotalSec + 's.';\n figma.notify(vmDoneMsg);\n figma.ui.postMessage({ type: 'done', message: vmDoneMsg });\n}\n\n// ─── Grouping Helpers ────────────────────────────────────────────────────────\n\nfunction getRowGroups(propName, grid, gridByRow, rowClusters, rowHeights) {\n // Find contiguous row ranges that share the same property value\n const groups = [];\n let currentValue = null;\n let startRow = 0;\n\n for (let r = 0; r < rowClusters.length; r++) {\n // Get the value of this property for any variant in this row\n const inRow = gridByRow[r] || [];\n if (inRow.length === 0) continue;\n const value = inRow[0].props[propName];\n\n if (value !== currentValue) {\n if (currentValue !== null) {\n groups.push({\n value: currentValue,\n startRow,\n endRow: r - 1,\n startY: rowClusters[startRow],\n endY: rowClusters[r - 1] + rowHeights[r - 1],\n });\n }\n currentValue = value;\n startRow = r;\n }\n }\n\n // Push last group\n if (currentValue !== null) {\n const lastRow = rowClusters.length - 1;\n groups.push({\n value: currentValue,\n startRow,\n endRow: lastRow,\n startY: rowClusters[startRow],\n endY: rowClusters[lastRow] + rowHeights[lastRow],\n });\n }\n\n return groups;\n}\n\nfunction getColGroups(propName, grid, gridByCol, colClusters, colWidths) {\n const groups = [];\n let currentValue = null;\n let startCol = 0;\n\n for (let c = 0; c < colClusters.length; c++) {\n const inCol = gridByCol[c] || [];\n if (inCol.length === 0) continue;\n const value = inCol[0].props[propName];\n\n if (value !== currentValue) {\n if (currentValue !== null) {\n groups.push({\n value: currentValue,\n startCol,\n endCol: c - 1,\n startX: colClusters[startCol],\n endX: colClusters[c - 1] + colWidths[c - 1],\n });\n }\n currentValue = value;\n startCol = c;\n }\n }\n\n if (currentValue !== null) {\n const lastCol = colClusters.length - 1;\n groups.push({\n value: currentValue,\n startCol,\n endCol: lastCol,\n startX: colClusters[startCol],\n endX: colClusters[lastCol] + colWidths[lastCol],\n });\n }\n\n return groups;\n}\n\n// ─── Groupify ─────────────────────────────────────────────────────────────────\n\nfunction getGroupifyComponentSetData(preferredAxis) {\n var selection = figma.currentPage.selection;\n if (selection.length !== 1) {\n return { error: \"Please select exactly one component set.\" };\n }\n\n var node = selection[0];\n\n // If a variant is selected, go up to the component set\n if (node.type === \"COMPONENT\" && node.parent && node.parent.type === \"COMPONENT_SET\") {\n node = node.parent;\n }\n\n if (node.type !== \"COMPONENT_SET\") {\n return { error: \"Please select a component set (a component with variants).\" };\n }\n\n // Extract properties and their values from variant names (visible variants only)\n var properties = {};\n var propertyOrder = [];\n\n for (var i = 0; i < node.children.length; i++) {\n var variant = node.children[i];\n if (variant.type !== 'COMPONENT' || variant.visible === false) continue;\n var pairs = variant.name.split(\",\");\n for (var j = 0; j < pairs.length; j++) {\n var eq = pairs[j].indexOf(\"=\");\n if (eq !== -1) {\n var propName = pairs[j].substring(0, eq).trim();\n var propValue = pairs[j].substring(eq + 1).trim();\n if (!properties[propName]) {\n properties[propName] = [];\n propertyOrder.push(propName);\n }\n if (properties[propName].indexOf(propValue) === -1) {\n properties[propName].push(propValue);\n }\n }\n }\n }\n\n var propsArray = [];\n for (var i = 0; i < propertyOrder.length; i++) {\n propsArray.push({\n name: propertyOrder[i],\n values: properties[propertyOrder[i]]\n });\n }\n\n // Infer current layout config from canvas positions\n var inferred = groupifyInferConfigFromCanvas(node, propsArray, preferredAxis);\n\n return { nodeId: node.id, properties: propsArray, inferred: inferred };\n}\n\n// Optimize property directions to minimize entirely empty rows/columns.\n// Tries all 2^N-2 non-degenerate ROW/COLUMN assignments and picks the one\n// with the fewest empty rows + empty columns. On tie, prefers the assignment\n// closest to the position-based inference (fewest direction changes).\n// preferredAxis: 'x' (more columns), 'y' (more rows), or null/undefined (auto)\nfunction groupifyOptimizeDirections(propsArray, variantKeys, currentConfigs, preferredAxis) {\n var n = propsArray.length;\n if (n < 2) return null; // need at least 2 props for a grid\n if (n > 8) return null; // skip brute-force for large property counts (2^N combinatorial explosion)\n\n var bestScore = Infinity;\n var bestAxisCount = -1; // higher = more combos on preferred axis\n var bestChanges = Infinity;\n var bestMap = null;\n\n // Bitmask: bit i = 0 → ROW, bit i = 1 → COLUMN\n // Skip 0 (all ROW) and (2^n - 1) (all COLUMN)\n var limit = (1 << n) - 1;\n for (var mask = 1; mask < limit; mask++) {\n var rowP = [];\n var colP = [];\n for (var i = 0; i < n; i++) {\n var prop = { name: propsArray[i].name, values: propsArray[i].values };\n if (mask & (1 << i)) {\n colP.push(prop);\n } else {\n rowP.push(prop);\n }\n }\n\n var rowCombos = groupifyBuildCombinations(rowP);\n var colCombos = groupifyBuildCombinations(colP);\n\n // Count empty rows\n var emptyRows = 0;\n for (var r = 0; r < rowCombos.length; r++) {\n var found = false;\n for (var c = 0; c < colCombos.length && !found; c++) {\n var combo = groupifyMergeObjects(rowCombos[r], colCombos[c]);\n if (variantKeys[groupifyComboToKey(combo)]) found = true;\n }\n if (!found) emptyRows++;\n }\n\n // Count empty columns\n var emptyCols = 0;\n for (var c = 0; c < colCombos.length; c++) {\n var found = false;\n for (var r = 0; r < rowCombos.length && !found; r++) {\n var combo = groupifyMergeObjects(rowCombos[r], colCombos[c]);\n if (variantKeys[groupifyComboToKey(combo)]) found = true;\n }\n if (!found) emptyCols++;\n }\n\n var score = emptyRows + emptyCols;\n\n // Preferred axis tiebreaker: maximize combos on preferred axis\n var axisCount = 0;\n if (preferredAxis === 'x') axisCount = colCombos.length;\n else if (preferredAxis === 'y') axisCount = rowCombos.length;\n\n // Fallback tiebreaker: fewest direction changes from position-based inference\n var changes = 0;\n if (currentConfigs) {\n for (var i = 0; i < n; i++) {\n var newDir = (mask & (1 << i)) ? \"COLUMN\" : \"ROW\";\n if (currentConfigs[i].direction !== newDir) changes++;\n }\n }\n\n var isBetter = false;\n if (score < bestScore) {\n isBetter = true;\n } else if (score === bestScore) {\n if (preferredAxis && axisCount > bestAxisCount) {\n isBetter = true;\n } else if ((!preferredAxis || axisCount === bestAxisCount) && changes < bestChanges) {\n isBetter = true;\n }\n }\n\n if (isBetter) {\n bestScore = score;\n bestAxisCount = axisCount;\n bestChanges = changes;\n bestMap = {};\n for (var i = 0; i < n; i++) {\n bestMap[propsArray[i].name] = (mask & (1 << i)) ? \"COLUMN\" : \"ROW\";\n }\n }\n }\n\n // Only return if we improved over the position-based inference,\n // or if a preferred axis was specified (always use optimizer result)\n if (!preferredAxis && currentConfigs) {\n var currentScore = groupifyScoreDirections(propsArray, variantKeys, currentConfigs);\n if (bestScore >= currentScore) return null;\n }\n\n return bestMap;\n}\n\n// Score the current direction assignment (count empty rows + empty cols)\nfunction groupifyScoreDirections(propsArray, variantKeys, configs) {\n var rowP = [], colP = [];\n for (var i = 0; i < configs.length; i++) {\n var prop = { name: propsArray[i].name, values: propsArray[i].values };\n if (configs[i].direction === \"COLUMN\") colP.push(prop);\n else rowP.push(prop);\n }\n\n var rowCombos = groupifyBuildCombinations(rowP);\n var colCombos = groupifyBuildCombinations(colP);\n // Degenerate case: all props on one axis → treat as 1-row or 1-col grid\n if (rowCombos.length === 0) rowCombos = [{}];\n if (colCombos.length === 0) colCombos = [{}];\n var score = 0;\n\n for (var r = 0; r < rowCombos.length; r++) {\n var found = false;\n for (var c = 0; c < colCombos.length && !found; c++) {\n if (variantKeys[groupifyComboToKey(groupifyMergeObjects(rowCombos[r], colCombos[c]))]) found = true;\n }\n if (!found) score++;\n }\n for (var c = 0; c < colCombos.length; c++) {\n var found = false;\n for (var r = 0; r < rowCombos.length && !found; r++) {\n if (variantKeys[groupifyComboToKey(groupifyMergeObjects(rowCombos[r], colCombos[c]))]) found = true;\n }\n if (!found) score++;\n }\n return score;\n}\n\nfunction groupifyInferConfigFromCanvas(node, propsArray, preferredAxis) {\n var children = node.children;\n if (children.length === 0) return null;\n\n var variants = [];\n for (var i = 0; i < children.length; i++) {\n var v = children[i];\n if (v.type !== 'COMPONENT' || v.visible === false) continue;\n var props = {};\n var pairs = v.name.split(\",\");\n for (var j = 0; j < pairs.length; j++) {\n var eq = pairs[j].indexOf(\"=\");\n if (eq !== -1) {\n props[pairs[j].substring(0, eq).trim()] = pairs[j].substring(eq + 1).trim();\n }\n }\n variants.push({ props: props, x: v.x, y: v.y, w: v.width, h: v.height });\n }\n\n var propConfigs = [];\n for (var p = 0; p < propsArray.length; p++) {\n var propName = propsArray[p].name;\n var values = propsArray[p].values;\n\n var sumX = {}, sumY = {}, cnt = {};\n for (var v = 0; v < values.length; v++) {\n sumX[values[v]] = 0;\n sumY[values[v]] = 0;\n cnt[values[v]] = 0;\n }\n for (var i = 0; i < variants.length; i++) {\n var val = variants[i].props[propName];\n if (val && cnt[val] !== undefined) {\n sumX[val] += variants[i].x + variants[i].w / 2;\n sumY[val] += variants[i].y + variants[i].h / 2;\n cnt[val]++;\n }\n }\n\n var centroids = {};\n for (var v = 0; v < values.length; v++) {\n var c = cnt[values[v]];\n centroids[values[v]] = {\n x: c > 0 ? sumX[values[v]] / c : 0,\n y: c > 0 ? sumY[values[v]] / c : 0\n };\n }\n\n var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\n for (var v = 0; v < values.length; v++) {\n var cx = centroids[values[v]].x;\n var cy = centroids[values[v]].y;\n if (cx < minX) minX = cx;\n if (cx > maxX) maxX = cx;\n if (cy < minY) minY = cy;\n if (cy > maxY) maxY = cy;\n }\n var rangeX = maxX - minX;\n var rangeY = maxY - minY;\n\n var direction, sortedValues;\n if (rangeX < 1 && rangeY < 1) {\n direction = \"COLUMN\";\n sortedValues = values.slice();\n } else if (rangeY > rangeX) {\n direction = \"ROW\";\n sortedValues = values.slice().sort(function (a, b) {\n return centroids[a].y - centroids[b].y;\n });\n } else {\n direction = \"COLUMN\";\n sortedValues = values.slice().sort(function (a, b) {\n return centroids[a].x - centroids[b].x;\n });\n }\n\n propConfigs.push({\n name: propName,\n values: sortedValues,\n direction: direction,\n spacing: 48\n });\n }\n\n // Optimize directions to avoid entirely empty rows/columns\n var variantKeys = {};\n for (var i = 0; i < variants.length; i++) {\n var keyParts = [];\n for (var pn in variants[i].props) keyParts.push(pn + \"=\" + variants[i].props[pn]);\n keyParts.sort();\n variantKeys[keyParts.join(\", \")] = true;\n }\n var optimized = groupifyOptimizeDirections(propsArray, variantKeys, propConfigs, preferredAxis);\n if (optimized) {\n for (var p = 0; p < propConfigs.length; p++) {\n propConfigs[p].direction = optimized[propConfigs[p].name];\n }\n }\n\n var yMap = {}, xMap = {};\n for (var i = 0; i < variants.length; i++) {\n var yk = Math.round(variants[i].y);\n var xk = Math.round(variants[i].x);\n if (!yMap[yk]) yMap[yk] = { pos: variants[i].y, size: 0 };\n yMap[yk].size = Math.max(yMap[yk].size, variants[i].h);\n if (!xMap[xk]) xMap[xk] = { pos: variants[i].x, size: 0 };\n xMap[xk].size = Math.max(xMap[xk].size, variants[i].w);\n }\n\n var yRows = groupifyObjToSortedArray(yMap);\n var xCols = groupifyObjToSortedArray(xMap);\n\n var yGaps = groupifyComputeGaps(yRows);\n var xGaps = groupifyComputeGaps(xCols);\n\n var rowProps = [], colProps = [];\n for (var p = 0; p < propConfigs.length; p++) {\n if (propConfigs[p].direction === \"ROW\") rowProps.push(propConfigs[p]);\n else colProps.push(propConfigs[p]);\n }\n groupifyAssignSpacings(yGaps, rowProps);\n groupifyAssignSpacings(xGaps, colProps);\n\n var minVx = Infinity, minVy = Infinity, maxVx = -Infinity, maxVy = -Infinity;\n for (var i = 0; i < variants.length; i++) {\n if (variants[i].x < minVx) minVx = variants[i].x;\n if (variants[i].y < minVy) minVy = variants[i].y;\n var r = variants[i].x + variants[i].w;\n var b = variants[i].y + variants[i].h;\n if (r > maxVx) maxVx = r;\n if (b > maxVy) maxVy = b;\n }\n\n var padding = {\n top: Math.max(0, Math.round(minVy)),\n right: Math.max(0, Math.round(node.width - maxVx)),\n bottom: Math.max(0, Math.round(node.height - maxVy)),\n left: Math.max(0, Math.round(minVx))\n };\n\n var resultProps = [];\n for (var p = 0; p < propConfigs.length; p++) {\n resultProps.push({\n name: propConfigs[p].name,\n values: propConfigs[p].values,\n direction: propConfigs[p].direction,\n spacing: propConfigs[p].spacing\n });\n }\n\n return {\n padding: padding,\n sectionDirection: \"GRID\",\n properties: resultProps\n };\n}\n\nfunction groupifyObjToSortedArray(map) {\n var arr = [];\n for (var k in map) arr.push(map[k]);\n arr.sort(function (a, b) { return a.pos - b.pos; });\n return arr;\n}\n\nfunction groupifyComputeGaps(entries) {\n var gaps = [];\n for (var i = 1; i < entries.length; i++) {\n var gap = Math.round(entries[i].pos - entries[i - 1].pos - entries[i - 1].size);\n if (gap > 0) gaps.push(gap);\n }\n return gaps;\n}\n\nfunction groupifyAssignSpacings(gaps, axisProps) {\n if (axisProps.length === 0 || gaps.length === 0) return;\n\n var sorted = gaps.slice().sort(function (a, b) { return a - b; });\n var clusters = [{ total: sorted[0], count: 1, rep: sorted[0] }];\n for (var i = 1; i < sorted.length; i++) {\n var last = clusters[clusters.length - 1];\n if (sorted[i] - last.rep <= 2) {\n last.total += sorted[i];\n last.count++;\n last.rep = Math.round(last.total / last.count);\n } else {\n clusters.push({ total: sorted[i], count: 1, rep: sorted[i] });\n }\n }\n\n clusters.sort(function (a, b) { return b.count - a.count; });\n\n var propsSorted = axisProps.slice().sort(function (a, b) {\n return b.values.length - a.values.length;\n });\n\n for (var i = 0; i < propsSorted.length && i < clusters.length; i++) {\n propsSorted[i].spacing = clusters[i].rep;\n }\n}\n\nfunction sendGroupifySelectionData(preferredAxis) {\n var _tg0 = Date.now();\n var data = getGroupifyComponentSetData(preferredAxis);\n if (DEBUG) console.log('[Perf] sendGroupifySelectionData: ' + (Date.now() - _tg0) + 'ms');\n if (data.error) {\n figma.ui.postMessage({ type: \"groupify-error\", message: data.error });\n } else {\n figma.ui.postMessage({ type: \"groupify-init\", data: data });\n }\n}\n\n// Quick align: re-infer directions with preferred axis, apply with uniform spacing/padding\nasync function groupifyQuickAlign(msg) {\n var data = getGroupifyComponentSetData(msg.preferredAxis);\n if (data.error) {\n figma.ui.postMessage({ type: \"groupify-error\", message: data.error });\n return;\n }\n\n var sp = msg.spacing || 40;\n var properties = data.inferred.properties;\n for (var i = 0; i < properties.length; i++) {\n properties[i].spacing = sp;\n }\n\n var config = {\n nodeId: data.nodeId,\n sectionDirection: data.inferred.sectionDirection,\n padding: { top: sp, right: sp, bottom: sp, left: sp },\n properties: properties\n };\n\n await applyGroupifyLayout(config);\n\n // Re-send init so UI reflects the new directions, padding, and spacing\n sendGroupifySelectionData(msg.preferredAxis);\n}\n\nasync function applyGroupifyLayout(config) {\n var node = await figma.getNodeByIdAsync(config.nodeId);\n if (!node || node.type !== \"COMPONENT_SET\") {\n figma.ui.postMessage({ type: \"groupify-error\", message: \"Component set not found.\" });\n return;\n }\n\n // Disable auto-layout before positioning (prevents reflow on resize)\n if (node.layoutMode !== 'NONE') {\n node.layoutMode = 'NONE';\n }\n\n var pad = config.padding;\n var properties = config.properties;\n var sectionDirection = config.sectionDirection || \"GRID\";\n\n var sectionProps = [];\n var rowProps = [];\n var colProps = [];\n for (var i = 0; i < properties.length; i++) {\n if (properties[i].direction === \"SECTION\") {\n sectionProps.push(properties[i]);\n } else if (properties[i].direction === \"ROW\") {\n rowProps.push(properties[i]);\n } else {\n colProps.push(properties[i]);\n }\n }\n\n var sectionRowProps, sectionColProps;\n if (sectionProps.length === 0) {\n sectionRowProps = [];\n sectionColProps = [];\n } else if (sectionProps.length === 1 || sectionDirection !== \"GRID\") {\n if (sectionDirection === \"VERTICAL\") {\n sectionRowProps = sectionProps;\n sectionColProps = [];\n } else {\n sectionRowProps = [];\n sectionColProps = sectionProps;\n }\n } else {\n sectionRowProps = sectionProps.slice(0, 1);\n sectionColProps = sectionProps.slice(1);\n }\n\n var sectionRowCombos = groupifyBuildCombinations(sectionRowProps);\n var sectionColCombos = groupifyBuildCombinations(sectionColProps);\n var rowCombos = groupifyBuildCombinations(rowProps);\n var colCombos = groupifyBuildCombinations(colProps);\n if (sectionRowCombos.length === 0) sectionRowCombos = [{}];\n if (sectionColCombos.length === 0) sectionColCombos = [{}];\n if (rowCombos.length === 0) rowCombos = [{}];\n if (colCombos.length === 0) colCombos = [{}];\n\n var variantMap = {};\n for (var i = 0; i < node.children.length; i++) {\n var child = node.children[i];\n if (child.type !== 'COMPONENT' || child.visible === false) continue;\n var key = groupifyVariantToKey(child.name);\n variantMap[key] = child;\n }\n\n var grids = [];\n for (var sr = 0; sr < sectionRowCombos.length; sr++) {\n grids[sr] = [];\n for (var sc = 0; sc < sectionColCombos.length; sc++) {\n var sectionCombo = groupifyMergeObjects(sectionRowCombos[sr], sectionColCombos[sc]);\n var grid = [];\n for (var r = 0; r < rowCombos.length; r++) {\n grid[r] = [];\n for (var c = 0; c < colCombos.length; c++) {\n var fullCombo = groupifyMergeObjects(groupifyMergeObjects(sectionCombo, rowCombos[r]), colCombos[c]);\n grid[r][c] = variantMap[groupifyComboToKey(fullCombo)] || null;\n }\n }\n grids[sr][sc] = grid;\n }\n }\n\n var colWidths = [];\n var rowHeights = [];\n for (var c = 0; c < colCombos.length; c++) {\n colWidths[c] = 0;\n for (var sr = 0; sr < sectionRowCombos.length; sr++) {\n for (var sc = 0; sc < sectionColCombos.length; sc++) {\n for (var r = 0; r < rowCombos.length; r++) {\n var v = grids[sr][sc][r][c];\n if (v) colWidths[c] = Math.max(colWidths[c], Math.max(v.width, 1));\n }\n }\n }\n }\n for (var r = 0; r < rowCombos.length; r++) {\n rowHeights[r] = 0;\n for (var sr = 0; sr < sectionRowCombos.length; sr++) {\n for (var sc = 0; sc < sectionColCombos.length; sc++) {\n for (var c = 0; c < colCombos.length; c++) {\n var v = grids[sr][sc][r][c];\n if (v) rowHeights[r] = Math.max(rowHeights[r], Math.max(v.height, 1));\n }\n }\n }\n }\n\n // Fill empty row/column dimensions with fallback from populated ones\n var maxRowH = 0, maxColW = 0;\n for (var r = 0; r < rowHeights.length; r++) {\n if (rowHeights[r] > maxRowH) maxRowH = rowHeights[r];\n }\n for (var c = 0; c < colWidths.length; c++) {\n if (colWidths[c] > maxColW) maxColW = colWidths[c];\n }\n for (var r = 0; r < rowHeights.length; r++) {\n if (rowHeights[r] === 0) rowHeights[r] = maxRowH;\n }\n for (var c = 0; c < colWidths.length; c++) {\n if (colWidths[c] === 0) colWidths[c] = maxColW;\n }\n\n var rowSpacings = groupifyCalcSpacings(rowCombos, rowProps);\n var colSpacings = groupifyCalcSpacings(colCombos, colProps);\n\n var innerWidth = 0;\n for (var c = 0; c < colWidths.length; c++) {\n innerWidth += colWidths[c];\n if (c > 0) innerWidth += colSpacings[c - 1];\n }\n var innerHeight = 0;\n for (var r = 0; r < rowHeights.length; r++) {\n innerHeight += rowHeights[r];\n if (r > 0) innerHeight += rowSpacings[r - 1];\n }\n\n var sectionRowSpacings = groupifyCalcSpacings(sectionRowCombos, sectionRowProps);\n var sectionColSpacings = groupifyCalcSpacings(sectionColCombos, sectionColProps);\n\n var emptyCells = [];\n var sy = pad.top;\n for (var sr = 0; sr < sectionRowCombos.length; sr++) {\n if (sr > 0) sy += sectionRowSpacings[sr - 1];\n var sx = pad.left;\n for (var sc = 0; sc < sectionColCombos.length; sc++) {\n if (sc > 0) sx += sectionColSpacings[sc - 1];\n\n var iy = sy;\n for (var r = 0; r < rowCombos.length; r++) {\n if (r > 0) iy += rowSpacings[r - 1];\n var ix = sx;\n for (var c = 0; c < colCombos.length; c++) {\n if (c > 0) ix += colSpacings[c - 1];\n var v = grids[sr][sc][r][c];\n if (v) {\n v.x = ix;\n v.y = iy;\n } else {\n emptyCells.push({ x: ix, y: iy, w: colWidths[c], h: rowHeights[r] });\n }\n ix += colWidths[c];\n }\n iy += rowHeights[r];\n }\n\n sx += innerWidth;\n }\n sy += innerHeight;\n }\n\n var totalWidth = pad.left + pad.right;\n for (var sc = 0; sc < sectionColCombos.length; sc++) {\n totalWidth += innerWidth;\n if (sc > 0) totalWidth += sectionColSpacings[sc - 1];\n }\n var totalHeight = pad.top + pad.bottom;\n for (var sr = 0; sr < sectionRowCombos.length; sr++) {\n totalHeight += innerHeight;\n if (sr > 0) totalHeight += sectionRowSpacings[sr - 1];\n }\n\n node.resizeWithoutConstraints(Math.max(1, totalWidth), Math.max(1, totalHeight));\n\n var numSections = sectionRowCombos.length * sectionColCombos.length;\n var sectionInfo = numSections > 1\n ? \" (\" + sectionRowCombos.length + \"\\u00d7\" + sectionColCombos.length + \" sections)\"\n : \"\";\n var emptyInfo = emptyCells.length > 0\n ? \" (\" + emptyCells.length + \" empty)\"\n : \"\";\n figma.ui.postMessage({\n type: \"groupify-success\",\n message: \"Layout applied! \" + rowCombos.length + \" rows \\u00d7 \" + colCombos.length + \" cols\" + sectionInfo + emptyInfo + \".\"\n });\n}\n\nfunction groupifyBuildCombinations(props) {\n if (props.length === 0) return [];\n var result = [{}];\n for (var i = 0; i < props.length; i++) {\n var next = [];\n var values = props[i].values;\n for (var j = 0; j < result.length; j++) {\n for (var k = 0; k < values.length; k++) {\n var combo = {};\n for (var key in result[j]) combo[key] = result[j][key];\n combo[props[i].name] = values[k];\n next.push(combo);\n }\n }\n result = next;\n }\n return result;\n}\n\nfunction groupifyVariantToKey(name) {\n var pairs = name.split(\",\");\n var sorted = [];\n for (var i = 0; i < pairs.length; i++) {\n var eq = pairs[i].indexOf(\"=\");\n if (eq !== -1) {\n sorted.push(pairs[i].substring(0, eq).trim() + \"=\" + pairs[i].substring(eq + 1).trim());\n }\n }\n sorted.sort();\n return sorted.join(\", \");\n}\n\nfunction groupifyComboToKey(combo) {\n var pairs = [];\n for (var key in combo) pairs.push(key + \"=\" + combo[key]);\n pairs.sort();\n return pairs.join(\", \");\n}\n\nfunction groupifyMergeObjects(a, b) {\n var r = {};\n for (var k in a) r[k] = a[k];\n for (var k in b) r[k] = b[k];\n return r;\n}\n\nfunction groupifyCalcSpacings(combos, props) {\n var spacings = [];\n for (var i = 1; i < combos.length; i++) {\n var spacing = 0;\n for (var p = 0; p < props.length; p++) {\n if (combos[i][props[p].name] !== combos[i - 1][props[p].name]) {\n spacing = props[p].spacing;\n break;\n }\n }\n spacings.push(spacing);\n }\n return spacings;\n}\n";
|
|
6
|
+
//# sourceMappingURL=autodocs-body.generated.js.map
|