Skip to content

Contributing

Contributions to Catnip are welcome. This guide covers how to contribute.

Development Setup

bash
pnpm install
pnpm dev

Runs 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 defaults
  • Catnip{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.

StepWhat to do
1. DesignAlign 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 folderAdd 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. CatnipLinkcatnip-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 typesRegister Catnip{Name} and catnip-{name} in packages/components/src/components.d.ts under GlobalComponents.
7. Test selectorsAdd 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 pagesAdd 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 tabsRegister 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. SidebarAdd 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 registryAdd 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 accuracyKeep 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 buildFrom 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 & changesetAfter 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

  • ComponentCatalogueTabs missing — slug not added to catalogue-doc-tabs.ts.
  • Playground state does not update — registry template or overrides use the wrong binding (e.g. .checked instead of .modelValue).
  • Phantom playground controlsoverrides keys must exist on the component; do not invent props that are not in Catnip{Name}.types.ts.
  • Docs vs implementation drift — prop defaults and event names in specs.md should match defaultProps and defineEmits.

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 + ComponentPlayground id; it manipulates the real custom element in the DOM.
  • Full-page REPL at /playground loads public/playground/catnip-components.es.js (copied by apps/docs/scripts/copy-logos.js). The library build leaves @signicat/catnip-icons/svg and @signicat/catnip-assets/illustrations/svg as externals, so the copy script also mirrors those packages under public/playground/deps/ and apps/docs/.vitepress/theme/components/PlaygroundRepl.vue maps 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 predev or pnpm run prebuild in apps/docs (or node apps/docs/scripts/copy-logos.js) so public/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:

bash
pnpm changeset

Choose 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 any unless justified

Catnip Design System by Signicat