diff --git a/.claude/agents/code-connect-generator.md b/.claude/agents/code-connect-generator.md new file mode 100644 index 0000000000..a771e65f34 --- /dev/null +++ b/.claude/agents/code-connect-generator.md @@ -0,0 +1,147 @@ +--- +name: code-connect-generator +description: 'Generates Figma Code Connect template files (.figma.ts) for CDS React components. Queries Figma MCP for component properties, finds matching source components, creates template files in __figma__ directories, optionally creates mobile copies, and validates with the figma CLI.' +tools: 'Read, Write, Edit, Bash, ToolSearch, mcp__figma-dev-mode-mcp-server__add_code_connect_map, mcp__figma-dev-mode-mcp-server__create_new_file, mcp__figma-dev-mode-mcp-server__generate_diagram, mcp__figma-dev-mode-mcp-server__generate_figma_design, mcp__figma-dev-mode-mcp-server__get_code_connect_map, mcp__figma-dev-mode-mcp-server__get_code_connect_suggestions, mcp__figma-dev-mode-mcp-server__get_context_for_code_connect, mcp__figma-dev-mode-mcp-server__get_design_context, mcp__figma-dev-mode-mcp-server__get_figjam, mcp__figma-dev-mode-mcp-server__get_libraries, mcp__figma-dev-mode-mcp-server__get_metadata, mcp__figma-dev-mode-mcp-server__get_screenshot, mcp__figma-dev-mode-mcp-server__get_variable_defs, mcp__figma-dev-mode-mcp-server__search_design_system, mcp__figma-dev-mode-mcp-server__send_code_connect_mappings, mcp__figma-dev-mode-mcp-server__upload_assets, mcp__figma-dev-mode-mcp-server__use_figma, mcp__figma-dev-mode-mcp-server__whoami, Skill, WebFetch, WebSearch' +model: sonnet +color: purple +--- + +You are a specialized agent for generating Figma Code Connect template files for the Coinbase Design System (CDS) monorepo. +You will be given a link to a CDS Figma component to process end-to-end. + +## CDS Repository Layout + +- Web components: `packages/web/src/` (npm package `@coinbase/cds-web`) +- Mobile components: `packages/mobile/src/` (npm package `@coinbase/cds-mobile`) +- Web Figma config: `figma.config.web.json` +- Mobile Figma config: `figma.config.mobile.json` +- Figma file key of the Core CDS components: `k5CtyJccNQUGMI5bI4lJ2g` + +## Step 1 — Find a matching React component + +Use Figma MCP's `get_code_connect_suggestions` tool with the component's node ID to find a matching component. + +If the tool cannot find a match, as a backup search `packages/web/src/` and `packages/mobile/src/` for a file matching the Figma component name using Bash `find` or Grep. + +After locating a matching component, study it to extract the component's props interface — you will need to have a deep understanding of the component's features and capabilities to accurately map to Figma properties. + +### Deprecated components + +- After matching a component, read the source code and check for `@deprecated` in the JSDoc block at the top — skip deprecated components +- If deprecated, search `packages/web/src/alpha/` for an updated version and use that instead +- If truly no matching non-deprecated component exists → set `status: "skipped"` with a clear `skipReason` + +## Step 2 — Run the `figma-code-connect` skill + +## Step 3 — Create the template file + +Always use the figma-code-connect skill to create CDS web components in packages/web (e.g. `packages/web/src//__figma__/ComponentName.figma.ts`). +Some components may have a counterpart in packages/mobile, but a separate step below handles porting the created template in web to the mobile package. + +Template placement: create a `__figma__/` directory alongside the component directory and write `ComponentName.figma.ts` inside it + +### Sample template structure + +```ts +// url=https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/✨-CDS-Components?node-id=NODE-ID-DASH-FORMAT +// source=packages/web/src/path/to/ComponentName.tsx +// component=ComponentName +import figma from 'figma'; +const instance = figma.selectedInstance; + +// --- property extractions --- +const label = instance.getString('Label'); +const variant = instance.getEnum('Variant', { + Primary: 'primary', + Secondary: 'secondary', +}); +const disabled = instance.getBoolean('Disabled'); + +// eslint-disable-next-line no-restricted-exports +export default { + example: figma.code`${label}`, + imports: ['import { ComponentName } from "@coinbase/cds-web"'], + id: 'component-name-kebab', + metadata: { nestable: true }, +}; +``` + +### Critical rules + +1. Try to map every VARIANT value exhaustively +2. Only emit JSX props that exist in the component's actual Props interface — never invent props +3. Never hardcode children — use dynamic APIs (`getInstanceSwap`, `findInstance`, etc.) +4. For each Figma property that has no code equivalent, record it in `skippedFigmaProperties` +5. The `id` field must be unique and kebab-case (e.g. `dropdown-list-cell`, `stepper-horizontal`) +6. The `// source=` path must be relative from the project root +7. Always add the eslint-disable comment for the default export which is not allowed by default in this repo. The code connect templates are an exception + +## Step 4 — Create mobile template (when a mobile component exists) + +Check if a corresponding component file exists in `packages/mobile/src/` using the same component name. If it does: + +1. Create `packages/mobile/src//__figma__/ComponentName.figma.ts` +2. Copy the web template, updating: + - `// source=packages/mobile/src/...` path + - Import: `@coinbase/cds-mobile` + - Any mobile-specific prop differences discovered by reading the mobile component's Props interface + - Any mobile-specific properties/variants in Figma +3. Update the `// url=` to use the same Figma node (same component works for both platforms in most cases) + +- Only change the url if there is a distinct, serparate Figma compoent for the mobile platform (this is rare in our case) + +If the component is marked mobile-only in your task, start from the mobile package and create only the mobile template. + +## Step 5 — Validate + +You MUST run both a dry-run publish AND the preview command to validate the new template. + +### Publish (dry-run) + +```bash +# Web +npx figma connect publish --dry-run --config figma.config.web.json --file +``` + +```bash +# Mobile +npx figma connect publish --dry-run --config "figma.config mobile.json" --file +``` + +### Preview + +Run the following command for the new code connect template to see the approximate generated code that it would produce. Compare the output with what you would expect it to contain as well as other legitimate sample code for the React component in the source code of the repo (good examples are either in storybook and/or apps/docs). + +```bash +# Web +npx figma connect preview --config figma.config.web.json +``` + +```bash +# Mobile +npx figma connect preview --config "figma.config mobile.json" +``` + +If validation fails, fix the issue and re-run. Record the final validation output in `validationOutput`. + +## Rate Limiting Recovery + +If any Figma MCP call returns an authentication error: + +1. Rollback/delete any template files created for this component +2. Return `status: "rate_limited"` with the error message in `errorMessage` + +Do NOT retry — report back immediately so the orchestrating workflow can reschedule. + +## Return + +Use StructuredOutput to return your result with these fields: + +- `componentName`: exact Figma component name from your task +- `status`: `"completed"` | `"skipped"` | `"rate_limited"` | `"failed"` +- `webTemplatePath`: relative path from project root (e.g. `packages/web/src/buttons/__figma__/Button.figma.ts`) +- `mobileTemplatePath`: relative path from project root for mobile template +- `skipReason`: explanation when status is `"skipped"` +- `skippedFigmaProperties`: `[{ "property": "PropName", "reason": "No equivalent code prop" }]` +- `validationOutput`: CLI output from the dry-run validation +- `errorMessage`: error details for `"rate_limited"` or `"failed"` status diff --git a/.claude/settings.json b/.claude/settings.json index d34e3c4534..513f1d096b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -28,5 +28,8 @@ ], "deny": [], "ask": [] + }, + "enabledPlugins": { + "figma@claude-plugins-official": true } } diff --git a/.claude/skills/figma.audit-connect/SKILL.md b/.claude/skills/figma.audit-connect/SKILL.md deleted file mode 100644 index 3d00304458..0000000000 --- a/.claude/skills/figma.audit-connect/SKILL.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: figma.audit-connect -description: Examines a Figma Code Connect mapping file and provides a report on the mapping's accuracy and completeness -model: claude-sonnet-4-6 -argument-hint: [Component name or path to component's code connect mapping file] -disable-model-invocation: true ---- - -## Task: Audit Figma Code Connect Mapping - -Audit the specified Figma Code Connect mapping file. - -Before starting, load the `figma.connect-best-practices` SKILL — it contains the canonical rules for every property mapping type (`figma.boolean`, `figma.enum`, `figma.string`, `figma.textContent`, `figma.instance`, `figma.children`, `figma.nestedProps`) and common pitfalls. Do not duplicate that guidance here; refer to it. - -### Inputs - -You will be provided with a name or path to a Figma Code Connect mapping file. -Code Connect files (`.figma.tsx`) are colocated with their corresponding components in this repo, typically within the component's local `__figma__` directory. - -Search for the mapping file and end your task if you cannot find it. - -Within the current mapping file: - -- Study all the property mappings defined in `props: { ... }` of each `figma.connect()` call -- Note which Figma variants are covered (indicated by `variant: { ... }`) -- Note the Figma URL(s) used — you will need these to call the MCP tools - -### Steps - -1. **Retrieve Figma component data** - - Call the Figma MCP **`get_context_for_code_connect`** tool with the fileKey and nodeId from the Figma URL in the mapping file. This is the primary tool for Code Connect authoring. It returns: - - Every component property with its exact name, type (`BOOLEAN`, `VARIANT`, `TEXT`), and key - - All variant option values for `VARIANT` properties - - The full descendant tree showing which layers reference which properties, and which variants each descendant appears in - - This output is the ground truth for your audit. Use it to understand: - - What the exact Figma property names are (they often contain spaces and ordinal words like "show 3rd step") - - Which properties are direct vs. exposed from child layers (the `↳` prefix indicates nested/exposed properties) - - Which component set variants exist and what their option values are - - Which descendant layers are text nodes vs. instance nodes - -2. **Optional: `get_design_context` for visual context** - - If you need to visually inspect what the component looks like (e.g. to understand a variant you're unsure about), call `get_design_context` with `disableCodeConnect: true`. This returns a screenshot and reference code. It is not a substitute for `get_context_for_code_connect` — it does not return structured property metadata — but it can help when the property names alone aren't enough to understand the design intent. - -3. **Secondary checks with `get_metadata`** - - Use `get_metadata` **only** as a targeted follow-up for specific layer-type verification — not as a general overview tool. The specific case where it's needed: - - **For every `figma.textContent('LayerName')` in the mapping file:** call `get_metadata` on that layer's node ID (found in the `descendants` of the `get_context_for_code_connect` output). Confirm the response shows ``. If it shows ``, ``, ``, or any other non-text type, `figma.textContent()` will fail silently at runtime — flag this as an error and suggest a hardcoded placeholder string instead. - -4. **Read the React component source** - - Find and read the component's TypeScript source file and any relevant sub-component source files - - Study the available React props - -5. **Analyze Property Coverage** - - Using the `get_context_for_code_connect` output as your source of truth, create a mapping analysis table where each row is a Figma property: - - | Figma Property | Type | Related React Prop(s) | Mapped? | Mapping Method | Notes | - | -------------- | ---- | --------------------- | ------- | -------------- | ----- | - - For each Figma property, indicate: - - ✅ Fully mapped - - ⚠️ Partially mapped (explain gap) - - ❌ Not mapped (explain why it should or shouldn't be) - - Also check variant coverage: does the mapping file have a `figma.connect()` call for every combination of `VARIANT` properties? Missing variant connects will cause Figma to show the wrong snippet for some design states. - -6. **Check for common mistakes** (reference `figma.connect-best-practices` for full detail) - - `figma.string('propName')` used on a TEXT property that doesn't appear in the component's formal variant names — this passes TypeScript but fails publish with "property does not exist" - - `figma.textContent('LayerName')` used on a layer that is an `` not a `` node - - `figma.children('LayerName')` where the layer name differs across variants — needs split `figma.connect()` calls with `variant: { ... }` filters - - Properties with the `↳` prefix mapped directly with `figma.string()` instead of `figma.nestedProps()` - - Boolean props using `figma.boolean('disabled')` when `disabled` is actually a _value_ of a `state` enum, not its own property - - **Intermediate props that aren't real component props** — e.g. `show3rd: figma.boolean('show 3rd step')` where `show3rd` is not a React prop. These should be assembled into a real prop value using `figma.boolean()` branching instead - - **`example` function with a body** — an `example` that builds intermediate variables before returning JSX is a sign that the props design needs refactoring. The `example` should almost always be a direct JSX return. Note: `figma.boolean()` returns an opaque descriptor, not a JS boolean — `figma.boolean('x') ? a : b` always evaluates as truthy and cannot be used for conditional logic outside `figma.boolean()`'s own `true`/`false` branches - -7. **Generate Report** - - Provide a summary with: - - **Coverage**: X/Y Figma properties mapped - - **Missing Mappings**: Unmapped properties that should be mapped, with suggested mapping method - - **Missing Variants**: Component variant combinations not covered by the current `figma.connect()` calls - - **Incorrect Mappings**: Mappings whose type or method doesn't match the actual Figma property type - - **Unnecessary Mappings**: Mappings that don't correspond to any Figma property - - **Recommended Changes**: Prioritized list of improvements with concrete code snippets diff --git a/.claude/skills/figma.connect-best-practices/SKILL.md b/.claude/skills/figma.connect-best-practices/SKILL.md deleted file mode 100644 index 1ace6da08f..0000000000 --- a/.claude/skills/figma.connect-best-practices/SKILL.md +++ /dev/null @@ -1,367 +0,0 @@ ---- -name: figma.connect-best-practices -description: Guidelines for writing Figma Code Connect property mappings. Use this skill when working on Figma Code Connect files, which typically end in .figma.tsx. ---- - -# Guidelines for writing Figma Code Connect mappings - -## Property Mapping Guidelines - -- figma.enum() - For dropdowns/variants - -```tsx -variant: figma.enum('variant', { - 'Figma Display Name': 'codeValue', - 'Primary': 'primary', - 'Secondary': 'secondary', -}), -``` - -- figma.boolean() - For boolean properties - -```tsx -disabled: figma.boolean('disabled'), -loading: figma.boolean('loading'), -``` - -- figma.boolean() for conditional properties\ - -In some cases, you only want to render a certain prop if it matches some value in Figma. -You can do this either by passing a partial mapping object, or setting the value to undefined. - -```tsx -// Don't render the prop if 'Has label' in figma is `false` -figma.boolean('has label', { - true: figma.string('label'), - false: undefined, -}); -``` - -- figma.string() - For text properties (component properties with text values) - -```tsx -label: figma.string('label'), -``` - -**CRITICAL: Before using `figma.string('propName')`, confirm the property name is a real Figma component property.** The `get_design_context` tool generates a props interface that includes both formal component properties AND text layer values — you cannot tell them apart by looking at the generated code alone. A string property is only valid if its name appears in the variant names returned by `get_metadata` (e.g. `name="..., label=foo, ..."`) or is otherwise explicitly listed as a component property. If it doesn't appear there, it is a text layer value and `figma.string()` will fail validation with "The property does not exist on the Figma component". Use `figma.textContent()` or a hardcoded placeholder instead. - -- figma.textContent() - For text layer content (when text is stored in a layer, not a property) - -A common pattern in Figma design systems is to override text content directly on instances rather than using component properties. Use `figma.textContent()` to extract the actual text from a named text layer. - -```tsx -// Extract text from a layer named 'Title' -title: figma.textContent('Title'), -``` - -**Key difference**: Use `figma.string()` when text is controlled by a Figma component property. Use `figma.textContent()` when text lives as content in a text layer that designers override directly. - -**CRITICAL: `figma.textContent()` only works on actual TEXT layers — never on component instances.** - -Before using `figma.textContent('LayerName')`, always call `get_metadata` on that layer's node ID to verify its type. In the metadata response, the node must appear as a `` element. If it appears as ``, ``, ``, or any other non-text type, `figma.textContent()` will fail at runtime in Figma with a "Layer not found" error even though the layer name is correct. - -When the target layer is a component instance (e.g. a reusable text component), use a hardcoded placeholder string instead: - -```tsx -// ❌ Wrong: 'string.label' is a component instance, not a text layer -label: figma.boolean('show label', { - true: figma.textContent('string.label'), - false: undefined, -}), - -// ✅ Correct: use a placeholder string when the layer is an instance -label: figma.boolean('show label', { - true: 'Your label here.', - false: undefined, -}), -``` - -- figma.instance() - For instance-swap properties (component slots) - -`figma.instance()` returns the JSX from another figma.connect() call that you can use in the example. -This is useful for components that accept a node of another React component as a prop. - -In the example below, Button accepts an instance of Icon as the icon prop. -We would need to have another call to figma.connect() for the `Icon` component somewhere in our code connect setup. - -```tsx -figma.connect(Button, 'https://...', { - props: { - icon: figma.instance('Icon'), - }, - example: ({ icon }) => { - return ; - }, -}); -``` - -**Exposed instance-swap properties (the `↳` prefix):** When `get_context_for_code_connect` returns an INSTANCE_SWAP property whose name begins with `↳` (e.g. `↳ start`, `↳ startCompact`, `↳ media`), pass the **full name including the `↳` character** to `figma.instance()`: - -```tsx -// ↳ start is an exposed (nested) instance-swap property -startNode: figma.boolean('show start', { - true: figma.instance('↳ start'), // ✅ include the ↳ - false: undefined, -}), - -// ❌ Wrong: stripping the ↳ means the property name doesn't match -startNode: figma.boolean('show start', { - true: figma.instance('start'), - false: undefined, -}), -``` - -Do **not** use `figma.nestedProps()` for INSTANCE_SWAP properties — `figma.nestedProps()` is only for nested TEXT or VARIANT properties. The `↳` prefix on an INSTANCE_SWAP simply means it is exposed from a child layer; `figma.instance('↳ name')` is the correct and complete mapping. - -**When the same slot uses different instance-swap properties across variants** (e.g. `↳ start` in the non-compact variant and `↳ startCompact` in the compact variant), split into separate `figma.connect()` calls with `variant: { ... }` filters: - -```tsx -// Non-compact: uses the ↳ start slot -figma.connect(SelectChip, URL, { - variant: { compact: 'false' }, - props: { - startNode: figma.boolean('show start', { - true: figma.instance('↳ start'), - false: undefined, - }), - }, - example: (props) => , -}); - -// Compact: uses the ↳ startCompact slot -figma.connect(SelectChip, URL, { - variant: { compact: 'true' }, - props: { - startNode: figma.boolean('show start', { - true: figma.instance('↳ startCompact'), - false: undefined, - }), - }, - example: (props) => , -}); -``` - -- figma.children() - For child instances by layer name - -Use this property mapping when your React component accepts children. `figma.children` maps a Figma layer name to the `children` prop. - -```tsx -// Maps child instances that aren't bound to an instance-swap prop -icon: figma.children('IconLayer'), -``` - -- figma.nestedProps() - For accessing properties from child component layers - -```tsx -// Access properties from a nested instance layer named 'Avatar' -avatar: figma.nestedProps('Avatar', { - size: figma.enum('size', { ... }), - src: figma.string('src'), -}), -// In example: use avatar.size, avatar.src -``` - -## Understanding Nested Properties (Important) - -In Figma's properties panel, you may see properties with the `↳` symbol (e.g., `↳ subtitle`). This indicates the property is **exposed from a child layer**, not defined directly on the parent component. - -**Why this matters:** The Code Connect validation run during `figma connect publish` has limited coverage. It only validates these prop kinds: - -- `figma.boolean()`, `figma.enum()`, `figma.string()` - validates the property name exists -- `figma.children()` - validates the layer name exists - -These prop kinds are **NOT validated** at all: - -- `figma.nestedProps()` - layer name and inner property mappings are not checked -- `figma.textContent()` - layer name is not checked - -`figma.instance()` behavior depends on context: when used at the top level of `props`, it may be validated against formal Figma instance-swap component properties. When nested inside a `figma.boolean()` branch, validation behavior is inconsistent across CLI versions — treat it as potentially validated. If you receive a "property not found" error for an instance name, replace it with a hardcoded JSX placeholder. - -Additionally, validation does **not recurse** into boolean `true`/`false` branch values. - -This can result in technically incorrect mappings being published to Figma without being caught during validation. - -**Incorrect approach** (will pass validation but fail at runtime): - -```tsx -// ❌ Wrong: 'subtitle' should be a nested property, not a direct component property -subtitle: figma.boolean('show subtitle', { - true: figma.string('subtitle'), - false: undefined, -}), -``` - -**Correct approach using figma.nestedProps():** - -```tsx -// ✅ Correct: Use nestedProps to access properties from the child layer -subtitle: figma.boolean('show subtitle', { - true: figma.nestedProps('subtitle', { - text: figma.string('subtitle'), - }), - false: { text: undefined }, -}), -// In example: use subtitle.text -``` - -**Tip:** When in doubt about whether a property is direct or nested, check if it has the `↳` symbol in Figma's properties panel or in the `get_context_for_code_connect` output. If it does, pick the mapping based on the property **type**: - -- `↳` + **TEXT** → `figma.textContent('layerName')` (verify the layer is a `` node first) -- `↳` + **VARIANT/BOOLEAN** → `figma.nestedProps('layerName', { ... })` -- `↳` + **INSTANCE_SWAP** → `figma.instance('↳ propertyName')` (include the `↳` in the string) - -## Multi-Variant Support - -For components with multiple variants in Figma, create separate figma.connect() calls: - -```tsx -// Default variant -figma.connect(ComponentName, 'figma-url', { - /* props */ -}); - -// Specific variant -figma.connect(ComponentName, 'figma-url', { - variant: { 'show suffix': true }, - props: { - /* variant-specific props */ - }, - example: (props) => , -}); -``` - -**Use variant-specific connects when child layer names differ across variants.** - -`figma.children('LayerName')` only matches layers with that exact name. If different variants use different layer names for the same logical prop (e.g. `Button` for the single-action variant and `ButtonGroup` for the multi-action variant), a single `figma.children()` call will silently produce nothing for the non-matching variants. Split into separate connects with `variant: { ... }` filters: - -```tsx -// ❌ Wrong: 'ButtonGroup' doesn't exist in the single-action variant -figma.connect(Footer, url, { - props: { action: figma.children('ButtonGroup') }, - example: ({ action }) =>