Contributing
Contributions to Catnip are welcome. This guide covers how to contribute.
Development Setup
pnpm install
pnpm devRuns component dev server and token/watch builds.
Component Structure
Each component lives in packages/components/src/components/Catnip{Name}/:
Catnip{Name}.ce.vue— Vue SFC (Custom Element)Catnip{Name}.types.ts— Props and defaultsCatnip{Name}.scss— Styles
Adding a Component
New Web Components are registered automatically: packages/components/src/core/index.ts globs Catnip*/Catnip*.ce.vue, and packages/components/src/core/styleManager.ts globs matching Catnip{Name}.scss. You do not manually register the tag in index.ts unless you are changing core behaviour.
If the component participates in native form association (like a control that should submit with a form), add its PascalCase name (e.g. TextField) to FORM_ELEMENTS in packages/components/src/core/configBasedLogic.ts.
Checklist (what every component needs)
Use an existing sibling (for example Button, Divider, or Link) as the template.
| Step | What to do |
|---|---|
| 1. Design | Align visuals and states with Figma (or the spec you are given). Prefer semantic tokens from packages/design-tokens; in SCSS use var(--…) with the same kind of fallbacks as other components (see CatnipButton.scss). |
| 2. Implementation folder | Add packages/components/src/components/Catnip{Name}/ with Catnip{Name}.ce.vue, Catnip{Name}.types.ts, and Catnip{Name}.scss. Name the folder and files so the custom element tag is catnip-{kebab-name} (e.g. CatnipLink → catnip-link). |
3. SFC (.ce.vue) | Use <script setup lang="ts">, withDefaults(defineProps<Catnip{Name}Props>(), defaultProps) from the types file, and useTestSelectors from packages/components/src/composables/useTestSelectors.ts with the matching testSelectors key. Keep the same patterns as existing components for slots, data-test, and accessibility. |
4. Types (.types.ts) | Export Catnip{Name}Props, defaultProps, and any string enums as Catnip{Name}{Prop} objects (used by the docs registry to build select controls). Optionally export catnip{Name}Slots / catnip{Name}Events arrays for human-readable manifest-style docs (the Custom Elements Manifest is generated from the SFC via vue-component-meta, not from these arrays). |
5. Styles (.scss) | BEM-style root class catnip-{name} and modifiers; focus rings and motion should match house patterns. |
| 6. Global types | Register Catnip{Name} and catnip-{name} in packages/components/src/components.d.ts under GlobalComponents. |
| 7. Test selectors | Add Catnip{Name} with at least { root: "catnip-{name}" } in packages/components/src/data/testSelectors.ts. On composed hosts, follow Component API — Test selectors: one key per role for distinct embedded instances, shared keys or a wrapper for repeated rows, and nested objects only where a child’s testSelectors are forwarded (see Multiple subcomponents of the same type). |
| 8. Docs pages | Add catalogue docs as apps/docs/components/{slug}/{overview,specs,guidelines}.md (kebab slug, e.g. link/overview.md), with sidebar linking to /components/{slug}/overview. Use ComponentCatalogueTabs at the top of each tab file (see Button). Mirror tone and structure of existing catalogue components. |
| 9. Catalogue tabs | Register the slug in apps/docs/.vitepress/theme/data/catalogue-doc-tabs.ts (CATALOGUE_DOC_TAB_SLUGS). Without this, ComponentCatalogueTabs does not render and sidebar highlight breaks on Specs/Guidelines routes. |
| 10. Sidebar | Add an entry under Components → Catalogue in apps/docs/.vitepress/config.ts (tabbed components link to .../overview). Sidebar active state for tab routes is handled in Layout.vue + custom.css via catnip-sidebar-catalogue-active. |
| 11. Playground registry | Add a catnip-{name} entry to componentConfigs in apps/docs/.vitepress/theme/data/component-registry.ts. Without this, buildRegistry() skips the component: the config supplies playgroundTemplate, optional defaultSlotContent, exclude (e.g. testSelectors), and overrides for controls. Overrides must match real prop names from the types file (e.g. modelValue, not checked). Use import "@signicat/catnip-components"; in templates like other entries. |
| 12. Specs accuracy | Keep apps/docs/components/{slug}/specs.md in sync with defaultProps and emitted events. The Custom Elements Manifest (dist/custom-elements.json) is generated from the SFC — treat specs as the human-readable contract. |
| 13. Verify build | From packages/components, run pnpm run build. Confirm dist/custom-elements.json lists catnip-{name} and that declaration emit has no errors. Run pnpm lint and pnpm type-check at the repo root when appropriate. |
| 14. Changelog & changeset | After merging, add a changeset (pnpm changeset) for packages you touched. When cutting a release, update apps/docs/changelog/changelog.md so entries match what actually shipped (prop names, APIs, and scope). |
Common gotchas
ComponentCatalogueTabsmissing — slug not added tocatalogue-doc-tabs.ts.- Playground state does not update — registry template or overrides use the wrong binding (e.g.
.checkedinstead of.modelValue). - Phantom playground controls —
overrideskeys must exist on the component; do not invent props that are not inCatnip{Name}.types.ts. - Docs vs implementation drift — prop defaults and event names in
specs.mdshould matchdefaultPropsanddefineEmits.
Composing Catnip components
When a component embeds another Catnip component and needs to expose that child’s props, prefer a nested prop named after the child component instead of aliases. Keep the child’s prop names unchanged inside that object, for example catnipInput.placeholder rather than a parent-specific inputPlaceholder.
Forwarded child slots should use the same namespace, for example slot="catnipInput.leftIconSlot". Keep parent-owned behaviour top-level and document any child props that the parent reserves or overrides to preserve invariants, such as inner modelValue, type, hideLabel, or form name.
Docs site and /playground (Vue REPL)
- Component playground (embedded in each component page) uses the same registry and only needs the markdown +
ComponentPlaygroundid; it manipulates the real custom element in the DOM. - Full-page REPL at
/playgroundloadspublic/playground/catnip-components.es.js(copied byapps/docs/scripts/copy-logos.js). The library build leaves@signicat/catnip-icons/svgand@signicat/catnip-assets/illustrations/svgas externals, so the copy script also mirrors those packages underpublic/playground/deps/andapps/docs/.vitepress/theme/components/PlaygroundRepl.vuemaps those specifiers in the import map. If you introduce new bare import specifiers in the published ES bundle, extend the copy script and the REPL import map the same way; otherwise the REPL will throw “Failed to resolve module specifier” at runtime. - After pulling changes or building packages, run
pnpm run predevorpnpm run prebuildinapps/docs(ornode apps/docs/scripts/copy-logos.js) sopublic/playground/is up to date. That directory is gitignored.
Design Tokens
Tokens live in packages/design-tokens/src/tokens/. Edit JSON, then run pnpm build in that package.
Changesets
Use Changesets for versioning:
pnpm changesetChoose the affected packages and bump type. This generates a changeset file for the next release.
Code Style
- Prettier: 140 char width, 2-space indent
- TypeScript strict mode
- No
anyunless justified