{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://whynot.design/ir/schema/component.schema.json", "title": "whynot IR — Component Contract", "description": "Technology-neutral contract for one component in the whynot designbook. Extracted one-way from the canonical React designbook (WHYNOT-WP-0002). Adapters consume this to scaffold stubs and detect drift; they never write back to it.", "type": "object", "additionalProperties": false, "required": ["name", "group", "description", "props"], "properties": { "name": { "type": "string", "description": "Canonical component name in PascalCase (e.g. \"Button\"). The Lit adapter maps this to the tag.", "pattern": "^[A-Z][A-Za-z0-9]*$" }, "tag": { "type": "string", "description": "Suggested custom-element tag for web-component adapters (e.g. \"wn-button\"). Advisory; an adapter owns its own naming.", "pattern": "^[a-z][a-z0-9-]*$" }, "group": { "type": "string", "description": "Grouping from the designbook manifest (e.g. \"atoms\", \"chrome\", \"form\"). Mirrors src/elements/.js in the Lit adapter." }, "description": { "type": "string", "description": "One- or two-sentence purpose, sourced from the React component's .prompt.md docs." }, "props": { "type": "array", "description": "Public inputs. Each prop carries its React identity AND its projection onto an HTML attribute, so attribute-driven stacks (Lit, Vue, plain HTML) are first-class.", "items": { "$ref": "#/$defs/prop" } }, "slots": { "type": "array", "description": "Named/default content slots.", "items": { "$ref": "#/$defs/slot" } }, "events": { "type": "array", "description": "Events the component emits.", "items": { "$ref": "#/$defs/event" } }, "variants": { "type": "array", "description": "Variant axes — each axis is a named dimension with discrete values (e.g. axis \"variant\" = [primary, secondary]). Usually derived from an enum prop.", "items": { "$ref": "#/$defs/variantAxis" } }, "docsRef": { "type": "string", "description": "Path (relative to repo root) to the source docs in designbook/, e.g. designbook/components/atoms/Button/Button.prompt.md." }, "exemplarRef": { "type": "string", "description": "Path (relative to repo root) to the reference render under ir/exemplars/, e.g. ir/exemplars/Button.html. Parity (T08) diffs the adapter's render against this." } }, "$defs": { "prop": { "type": "object", "additionalProperties": false, "required": ["name", "type", "attribute"], "properties": { "name": { "type": "string", "description": "React prop name, camelCase (e.g. \"iconEnd\")." }, "type": { "type": "string", "description": "Neutral type. \"enum\" pairs with `enum`. Non-attribute-portable shapes use \"object\", \"function\", or \"node\" and MUST set portable:false.", "enum": ["string", "number", "boolean", "enum", "object", "function", "node"] }, "attribute": { "description": "HTML attribute name an attribute-driven adapter binds this prop to (kebab-case), e.g. \"icon-end\". `false` means the prop is intentionally not exposed as an attribute (property-only or non-portable).", "oneOf": [ { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }, { "type": "boolean", "const": false } ] }, "enum": { "type": "array", "description": "Allowed values when type is \"enum\".", "items": { "type": "string" } }, "default": { "description": "Default value as authored in the React source. Type matches `type`." }, "required": { "type": "boolean", "default": false, "description": "Whether the consumer must supply this prop." }, "portable": { "type": "boolean", "default": true, "description": "False marks props that do not map cleanly to an HTML attribute (objects, render props, callbacks). Adapters MUST surface non-portable props as drift, never silently drop them (see open risks)." }, "description": { "type": "string" } }, "allOf": [ { "if": { "properties": { "type": { "const": "enum" } } }, "then": { "required": ["enum"] } } ] }, "slot": { "type": "object", "additionalProperties": false, "required": ["name"], "properties": { "name": { "type": "string", "description": "Slot name; use \"default\" for the unnamed default slot." }, "description": { "type": "string" }, "required": { "type": "boolean", "default": false } } }, "event": { "type": "object", "additionalProperties": false, "required": ["name"], "properties": { "name": { "type": "string", "description": "Emitted event name as dispatched by the component, e.g. \"wn-dismiss\"." }, "description": { "type": "string" }, "detail": { "type": "string", "description": "Free-text description of the event's detail payload shape." } } }, "variantAxis": { "type": "object", "additionalProperties": false, "required": ["axis", "values"], "properties": { "axis": { "type": "string", "description": "Name of the variant dimension, usually the driving prop name (e.g. \"variant\", \"size\")." }, "values": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Discrete values along this axis." }, "default": { "type": "string", "description": "Default value for the axis." } } } } }