Thank you for your interest in contributing to ZardUI! This guide will help you understand how the project is structured and how you can contribute effectively.
- ✅ Requirements
- 🚀 Initial Setup
- 🏗️ Project Architecture
- 📁 Folder Structure
- 🧩 Developing Components
- 🧱 Developing Blocks
- 📝 Documentation System
- 🧪 Testing
- 🌿 Branch Strategy
- 🔄 Contribution Workflow
- 📋 Commit Patterns
- 🔧 Essential Commands
- Node.js version
>=20.19.0(specified inengines) - npm (included with Node.js)
- Git for version control
-
Fork and clone the repository:
git clone https://github.com/[your-username]/zardui.git cd zardui -
Install dependencies:
npm install
-
Start the development environment:
npm start
The project will run at
http://localhost:4222
ZardUI is a monorepo managed by Nx with the following characteristics:
- Nx 22 with intelligent caching and dependency graph
- Angular 21 with signal-based architecture
- TailwindCSS v4 with PostCSS integration
- TypeScript 5.9 with strict mode
- Conventional commits for automated releases
- Path aliases:
@/*insidelibs/zard/maps tosrc/lib/* - Optimized builds with Angular CLI + esbuild
- Standalone and publishable Angular library
- Components with signal-based inputs (
input()) - CVA (Class Variance Authority) for typed variants
- OnPush change detection for performance
- ViewEncapsulation.None for styling flexibility
- Host binding for dynamic classes
- Pre-built, composable UI blocks (e.g., authentication pages)
- Each block is an Angular component with metadata
- Includes file contents for CLI installation
- Angular application with SSR/SSG (Express + Vercel)
- Interactive documentation with live demos
- Automatic file synchronization system
zard-clibuilt with Commander.js- Installs components and blocks from a registry
zard-mcpbuilt with@modelcontextprotocol/sdk- AI-powered tools for component discovery, documentation, and installation
- 9 tools: list/search/get components, get docs/examples/dependencies, list/get blocks, install components
- Fetches from the Zard registry with caching (5-minute TTL)
- Configurable via
ZARD_REGISTRY_URLenvironment variable
- Local Nx plugin
@zardui/generators - Scaffolds new components and blocks with all required files
zardui/
├── libs/zard/ # 📦 Main library
│ ├── src/lib/shared/components/ # 🧩 Components
│ │ └── [component-name]/ # 📂 Component folder
│ │ ├── [component].component.ts # 🎯 Main component
│ │ ├── [component].variants.ts # 🎨 CVA variants
│ │ ├── [component].component.spec.ts # 🧪 Unit tests
│ │ ├── index.ts # 📤 Barrel export
│ │ ├── demo/ # 📋 Demos for docs
│ │ │ ├── [component].ts # 📤 Demo registry
│ │ │ ├── default.ts # 🏠 Default example
│ │ │ └── [variant].ts # 🔀 Variant examples
│ │ └── doc/ # 📖 Documentation
│ │ ├── overview.md # 📝 Overview
│ │ └── api.md # 🔧 API reference
│ └── src/lib/shared/utils/ # 🛠️ Shared utilities
├── libs/blocks/ # 🧱 Block library
│ └── src/lib/[block-name]/ # 📂 Block folder
│ ├── block.ts # 📋 Block metadata
│ ├── [block].component.ts # 🎯 Angular component
│ └── [block].component.html # 📄 Template
├── apps/web/ # 🌐 Documentation site
│ ├── src/app/ # 📱 Angular application
│ └── public/components/ # 📁 Synced demo/doc files
├── apps/web-e2e/ # 🎭 E2E tests (Playwright)
│ └── src/components/ # 🧪 Component E2E specs
├── tools/generators/ # 🔨 Nx generators
│ └── component/ # 🧩 Component generator
├── packages/cli/ # ⚡ CLI tool
│ └── src/
│ ├── index.ts # 🎯 CLI entry point
│ ├── commands/ # 📦 CLI commands (add, init, etc.)
│ ├── config/ # ⚙️ Configuration management
│ ├── constants/ # 📋 Shared constants
│ ├── core/ # 🔧 Core logic (registry, installer)
│ └── utils/ # 🛠️ Utility functions
├── packages/mcp/ # 🤖 MCP Server
│ └── src/
│ ├── index.ts # 🎯 Server entry point
│ ├── services/ # 🔄 Registry service (caching)
│ ├── tools/ # 🧰 9 MCP tool implementations
│ └── types/ # 📋 Registry type definitions
├── scripts/ # 🤖 Automation scripts
│ ├── generate-files.cts # 🔄 Demo/docs sync
│ ├── generate-installation-guides.cts # 📋 Installation guides
│ ├── build-registry.cts # 📦 Registry build
│ └── sync-blocks.ts # 🔄 Block sync
└── api/ # ☁️ Vercel Edge Functions
└── og.ts # 🖼️ Dynamic OG image generation
The easiest way to create a new component is using the Nx generator:
npm run generate:component
# or
npx nx generate @zardui/generators:component --name=my-component --description="My component description"This creates 8 files and auto-updates 3 existing files (barrel export, component registry, sidebar routes).
import {
ChangeDetectionStrategy,
Component,
computed,
input,
ViewEncapsulation,
} from '@angular/core';
import { mergeClasses } from '@/shared/utils/merge-classes';
import {
myComponentVariants,
type ZardMyComponentTypeVariants,
type ZardMyComponentSizeVariants,
} from './my-component.variants';
import type { ClassValue } from 'clsx';
@Component({
selector: 'z-my-component, [z-my-component]',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: { '[class]': 'classes()' },
template: `<ng-content />`,
})
export class ZardMyComponentComponent {
readonly zType = input<ZardMyComponentTypeVariants>('default');
readonly zSize = input<ZardMyComponentSizeVariants>('default');
readonly class = input<ClassValue>('');
protected readonly classes = computed(() =>
mergeClasses(
myComponentVariants({
zType: this.zType(),
zSize: this.zSize(),
}),
this.class(),
),
);
}- Input prefix: Use
zprefix for component-specific inputs (zType,zSize,zDisabled, etc.) - Boolean inputs: Use
booleanAttributetransform:readonly zDisabled = input(false, { transform: booleanAttribute });
- Class input: Always accept
ClassValuefromclsxfor class merging - Selectors: Support both element and attribute usage:
z-name, [z-name] - Encapsulation: Always use
ViewEncapsulation.None - Change detection: Always use
ChangeDetectionStrategy.OnPush - Path aliases: Use
@/prefix for imports within the library (e.g.,@/shared/utils/merge-classes)
import { cva, type VariantProps } from 'class-variance-authority';
import { mergeClasses } from '@/shared/utils/merge-classes';
export const myComponentVariants = cva(mergeClasses('base-classes-here'), {
variants: {
zType: {
default: 'default-classes',
outline: 'outline-classes',
},
zSize: {
default: 'size-default-classes',
sm: 'size-sm-classes',
lg: 'size-lg-classes',
},
},
defaultVariants: {
zType: 'default',
zSize: 'default',
},
});
type MyComponentVariants = VariantProps<typeof myComponentVariants>;
export type ZardMyComponentTypeVariants = NonNullable<MyComponentVariants['zType']>;
export type ZardMyComponentSizeVariants = NonNullable<MyComponentVariants['zSize']>;- Scaffold component with
npm run generate:component - Implement variants with CVA in
[component].variants.ts - Build the component in
[component].component.ts - Write tests in
[component].component.spec.ts - Create demos in the
demo/folder following existing patterns - Write documentation in
doc/overview.mdanddoc/api.md - Watch system automatically syncs demos and docs to the website
- TailwindCSS v4 with
@tailwindcss/postcss(notailwind.config.js) - Class merging with
tailwind-merge+clsxviamergeClasses()utility - Utility-first approach for consistency
- Design tokens through CSS custom properties
npm run generate:block
# or
npx nx generate @zardui/generators:block --name=my-blockBlocks are pre-built UI compositions that combine multiple Zard components:
// block.ts
import type { Block } from '@/shared/types';
import { MyBlockComponent } from './my-block.component';
export const myBlock: Block = {
id: 'my-block-01',
title: 'My Block',
description: 'Description of the block.',
component: MyBlockComponent,
category: 'Category',
image: { light: 'path/to/light.png', dark: 'path/to/dark.png' },
files: [
{ name: 'my-block.component.ts', path: '...', content: '...', language: 'typescript' },
],
};The scripts/generate-files.cts script monitors changes in:
libs/zard/src/lib/shared/components/*/demo/→apps/web/public/components/*/demo/libs/zard/src/lib/shared/components/*/doc/→apps/web/public/components/*/doc/
Demo .ts files are automatically converted to .md for display on the docs site.
Each component has a demo registry file and individual demo components:
// demo/button.ts - Demo registry
import { ZardDemoButtonDefaultComponent } from './default';
import { ZardDemoButtonTypeComponent } from './type';
export const BUTTON = {
componentName: 'button',
componentType: 'button',
description: 'Displays a button or a component that looks like a button.',
examples: [
{ name: 'default', component: ZardDemoButtonDefaultComponent },
{ name: 'type', component: ZardDemoButtonTypeComponent },
],
};// demo/default.ts - Demo component
import { Component } from '@angular/core';
import { ZardButtonComponent } from '../button.component';
@Component({
selector: 'z-demo-button-default',
standalone: true,
imports: [ZardButtonComponent],
template: `
<button z-button>Default</button>
<button z-button zType="outline">Outline</button>
`,
})
export class ZardDemoButtonDefaultComponent {}- overview.md: Overview, use cases, basic examples
- api.md: Complete API reference, props, events, methods
- Jest 30 with
happy-domenvironment - @testing-library/angular with
render()andscreenAPI - Co-located tests next to components (
[component].component.spec.ts)
- Playwright with
@nx/playwrightintegration - Tests location:
apps/web-e2e/src/components/ - Test target: Component demos at
http://localhost:4222/docs/components/:name - Accessibility:
@axe-core/playwrightfor WCAG 2.1 AA compliance checks
# Unit tests
npm test # Run all unit tests
npm run test:watch # Unit tests in watch mode
# E2E tests
npm run e2e # Run E2E tests (auto-starts dev server)
npm run e2e:ui # Run E2E with Playwright UI (interactive)
npx nx e2e web-e2e -- --grep "Button" # Run specific E2E testimport { render, screen } from '@testing-library/angular';
import { ZardButtonComponent } from './button.component';
describe('ZardButtonComponent', () => {
it('should render with default variant', async () => {
await render('<button z-button>Click me</button>', {
imports: [ZardButtonComponent],
});
const button = screen.getByRole('button', { name: /click me/i });
expect(button).toBeInTheDocument();
});
it('should apply disabled state', async () => {
await render('<button z-button zDisabled>Disabled</button>', {
imports: [ZardButtonComponent],
});
const button = screen.getByRole('button');
expect(button).toHaveAttribute('disabled');
});
});import { test, expect } from '@playwright/test';
import { ComponentDemoPage } from '../utils/component-page';
import { checkA11y } from '../utils/axe-helper';
test.describe('MyComponent', () => {
let demoPage: ComponentDemoPage;
test.beforeEach(async ({ page }) => {
demoPage = new ComponentDemoPage(page, 'my-component');
await demoPage.goto();
});
test('renders default demo', async () => {
await expect(demoPage.firstDemoCard).toBeVisible();
});
test('passes a11y checks', async ({ page }) => {
await checkA11y(page, '#overview');
});
});Since E2E tests run against component demo pages, follow these rules to prevent false failures:
- Test behavior, not content — assert on interactions and ARIA state, not demo text or element counts
- First demo is the test fixture — the first example per component is the primary E2E target; keep it stable
- Same-PR updates — if your change modifies a component or its first demo, update the E2E test in the same PR
- Stable selectors — use component selectors (
z-button), directives ([z-input]), and ARIA attributes ([role="dialog"]) over text or CSS classes
master- Single main branch- Receives direct PRs
- Automatic squash merge
- Automatic release via tags
- Continuous deployment to production
Naming convention:
feat/#<issue-number>-<descriptive-name>
# Example: feat/#42-button-loading
fix/#<issue-number>-<descriptive-name>
# Example: fix/#43-input-focus-bug- 📋 Fork the repository
- 🌿 Create branch directly from
master:git checkout master git pull origin master git checkout -b feat/#123-new-feature - 💻 Develop with as many commits as you want
- 🧪 Run tests:
npm test npm run test:watch # during development
- 🚀 Open PR to
master - 👁️ Review + Merge = Automatic release
- ✅ Automatic squash merge transforms multiple commits into 1 clean commit
- Unit tests passing -
npm test - E2E tests passing -
npm run e2e(if component or first demo changed) - E2E test updated/added if component behavior changed
- Code follows project patterns
- Documentation updated (if necessary)
- Demos working correctly
- No lint/typecheck warnings
- Related issue linked
- Conventional commit in PR title
- Squash merge will be applied automatically
We use Conventional Commits with Emojis for better visual feedback and automated releases.
git add .
git commit -m "✨ feat(button): add new variant"| Emoji | Type | Description | Version Bump |
|---|---|---|---|
| ✨ | feat |
New functionality | Minor (0.x.0) |
| 🐛 | fix |
Bug correction | Patch (0.0.x) |
| 🚀 | perf |
Performance improvements | Patch (0.0.x) |
| ⏪️ | revert |
Revert previous commit | Patch (0.0.x) |
| 📦 | refactor |
Code refactoring | No release |
| 🔧 | ci |
CI/CD | No release |
| 🧪 | test |
Tests | No release |
| 📝 | docs |
Documentation | No release |
| 💄 | style |
Code style | No release |
| 🏗️ | build |
Build system | No release |
| 🚧 | chore |
Maintenance | No release |
Breaking Changes: Add ! after the type for a Major version bump:
✨ feat(button)!: redesign button API # Major: 1.0.0 → 2.0.0We use Husky + commitlint to validate commits automatically:
- ✅ Before commit: lint-staged runs ESLint and Prettier
- ✅ On commit message: commitlint validates format
- ❌ Invalid format → commit rejected with helpful error
emoji type(scope): description
[optional body]
[optional footer]
Examples:
✨ feat(button): add loading state
🐛 fix(input): resolve focus bug
📦 refactor(dialog): improve animation performance
🧪 test(form): add validation tests
📝 docs(readme): update installation guideImportant - Emoji is REQUIRED:
- Emoji at the start (MANDATORY - commit will be rejected without it)
- Type (feat, fix, etc)
- Scope in parentheses (optional)
- Colon and space
- Clear, imperative description
Do NOT add Co-Authored-By trailers to commits.
Examples of valid commits:
✅ ✨ feat(button): add variant
✅ 🐛 fix: resolve bug
✅ 📦 refactor(core): improve performance
❌ feat(button): missing emoji - WILL BE REJECTED
❌ feat: missing emoji - WILL BE REJECTEDnpm start # 🚀 Start dev server (port 4222)
npm run watch:files # 👀 Monitor changes in demos/docs
npm run build # 🏗️ Production build
npm run convert:demos # 🔄 Convert demos to markdown
npm run serve:ssr # 🌐 Serve SSR server locallynpm run generate:component # Scaffold a new component
npm run generate:block # Scaffold a new blocknpm test # 🧪 All unit tests
npm run test:watch # 👁️ Unit tests in watch mode
npm run e2e # 🎭 E2E tests (auto-starts dev server)
npm run e2e:ui # 🖥️ E2E with Playwright UInpm run build:cli # Build the CLI
npm run build:mcp # Build the MCP server
npm run dev:mcp # Build MCP in dev mode (with npm link)
npm run build:registry # Build the component registry
npm run serve:registry # Serve registry locally
npm run sync:blocks # Sync blocks configurationnpm run release:dry-run # Preview release
npm run release # Create release
npm run release:cli # Release CLI package
npm run publish:cli # Publish CLI to npm
npm run publish:mcp # Publish MCP to npmOur release system is fully automated - you don't need to do anything special:
# Example PR with commits:
✨ feat(button): add loading variant
🐛 fix(input): resolve focus issue
📦 refactor(dialog): improve performanceThe auto-release workflow automatically:
- ✅ Analyzes commits since last release
- ✅ Determines version bump (major/minor/patch)
- ✅ Skips if only docs/chore commits
If release is needed, it automatically:
- ✅ Bumps version in
package.json - ✅ Generates/updates
CHANGELOG.md - ✅ Creates git commit:
🔖 chore(release): publish X.Y.Z - ✅ Creates git tag:
zard@X.Y.Z - ✅ Pushes to GitHub
The tag push triggers the publish workflow:
- ✅ Builds packages
- ✅ Publishes to npm
- ✅ Creates GitHub Release with notes
| Commits in PR | Version Bump | Example |
|---|---|---|
Only feat |
Minor | 1.2.3 → 1.3.0 |
Only fix |
Patch | 1.2.3 → 1.2.4 |
feat + fix |
Minor | 1.2.3 → 1.3.0 |
Any with ! |
Major | 1.2.3 → 2.0.0 |
Only docs, chore, etc |
None | No release |
Nothing! Just:
- ✅ Use proper commit format (emoji + type)
- ✅ Get your PR reviewed and approved
- ✅ Squash & merge to master
- ✅ Automation handles the rest!
- 📊 GitHub Actions: Check the "Actions" tab for release status
- 📝 CHANGELOG.md: Auto-updated with each release
- 🏷️ GitHub Releases: Created automatically with notes
- 📦 npm: Published automatically
If you need to create a release manually:
# Dry run (preview)
npm run release:dry-run
# Create release
npm run release
# Create specific version
npx nx release version 1.2.3- Automatic: Every merge to master (if needed)
- No schedule: Releases happen when features/fixes are ready
- Fast: Release created within ~2 minutes of merge
- Current: Angular 21 actively supported
- Previous: Angular 20 actively supported
- LTS: Angular 19 with bug fixes only
- Migration: Documented in CHANGELOG when dropping support
- Issues: Report bugs or request features
- Discussions: General discussions and questions
- Email: For specific questions → gomesluiz.dev@gmail.com
- Nx Documentation
- Angular Docs
- TailwindCSS v4
- CVA Documentation
- Conventional Commits
- Testing Library Angular
Questions? Open an issue or get in touch! Your contribution is very welcome! 🎉