Source
libs/zard/src/lib/shared/
Every component, service and utility lives in the monorepo as ordinary Angular source. Nothing in these files is registry-specific.
components/core/services/utils/ The registry is a set of static JSON files published at https://zardui.com/r . Every item carries the full source code of its files, so a client can download an item and write the files directly into a project.
That is the open code philosophy: the components you install are copied into your repository and you own them from that moment on. There is no Zard UI runtime dependency to keep in sync.
https://zardui.com/r/registry.jsonhttps://zardui.com/r/<name>.json — for example https://zardui.com/r/button.jsonhttps://zardui.com/r/blocks-registry.json https://zardui.com/r/blocks/<id>.json zard-cli , which resolves items and writes their files into your project. The registry is not a service: it is the output of a build step that runs in this repository and is published together with the website. Four stages turn Angular source files into static JSON.
libs/zard/src/lib/shared/
Every component, service and utility lives in the monorepo as ordinary Angular source. Nothing in these files is registry-specific.
components/core/services/utils/packages/cli/src/core/registry/registry-data.ts
A single TypeScript array declares what ships. Each entry names the item, where it should land in the consumer project, and which files and dependencies it needs.
namebasePathdependenciesdevDependenciesregistryDependenciesfilesscripts/build-registry.cts
Running npm run build:registry reads each declared file from disk, inlines its full content and writes one JSON per item plus the index. When a component ships doc/overview.md, doc/api.md or demo files, they are folded into the docs and demos fields.
npm run build:registryapps/web/public/r/ → https://zardui.com/r
The generated files are plain static assets of this website. There is no API and no runtime: any HTTP client can read them.
registry.json<name>.jsonblocks/The two kinds of file change at different rates, so they are cached differently.
/r/*public, max-age=31536000, immutable
An item file describes one published version of a component, so it never changes once written and is cached for a year.
/r/registry.jsonpublic, max-age=3600, must-revalidate
The index is the only document that has to reflect new items and new versions, so it is cached for an hour and revalidated afterwards.
Both rules answer with Access-Control-Allow-Origin: * and Content-Type: application/json; charset=utf-8 , so the registry can be read from a browser as easily as from a terminal.
registry.json lists every item the registry knows about. It is a summary: it carries the metadata needed to resolve an install, but never the source code, which only lives in the individual item files.
The excerpt below is the real index reduced to three items — core , utils and button . The published file contains every component of the library.
{
"$schema": "https://zardui.com/schema/registry.json",
"name": "@zard",
"homepage": "https://zardui.com",
"version": "1.0.0-beta.83",
"items": [
{
"name": "core",
"type": "registry:component",
"files": [
"directives/string-template-outlet/string-template-outlet.directive.ts",
"directives/id.directive.ts",
"provider/event-manager-plugins/zard-debounce-event-manager-plugin.ts",
"provider/event-manager-plugins/zard-event-manager-plugin.ts",
"provider/providezard.ts",
"css/tailwind.css",
"index.ts"
]
},
{
"name": "utils",
"type": "registry:component",
"basePath": "utils",
"dependencies": ["tailwind-merge", "clsx"],
"files": ["index.ts", "merge-classes.ts", "number.ts"]
},
{
"name": "button",
"type": "registry:component",
"files": ["button.component.ts", "button.variants.ts", "index.ts"]
}
]
}| Field | Type | Description |
|---|---|---|
$schema | string | Identifier of the registry format, always https://zardui.com/schema/registry.json . |
name | string | Registry namespace — @zard . |
homepage | string | https://zardui.com . |
version | string | Version of the zard-cli package at the moment the registry was built. |
items | array | One summary per item, without the file contents. |
Each entry of items exposes name , type , basePath , dependencies , devDependencies , registryDependencies and files . Every field except name , type and files is optional and only emitted when the item declares it. In the index, files is only the list of file names — string[] — not the objects with content you find in an item file.
Note: the URL in $schema is only an identifier written by the build. There is no JSON Schema document published at that address today, so it does not give you editor validation or autocompletion.
Each item has its own file at /r/<name>.json . This is where the source code lives: every entry of files carries the complete content of one file. The example below is button.json with the contents truncated for readability.
{
"name": "button",
"type": "registry:component",
"files": [
{
"name": "button.component.ts",
"content": "import {\n afterNextRender,\n ChangeDetectionStrategy,\n Component,\n..."
},
{
"name": "button.variants.ts",
"content": "import { cva, type VariantProps } from 'class-variance-authority';\n..."
},
{
"name": "index.ts",
"content": "export * from './button.component';\nexport * from './button.variants';\n"
}
]
}| Field | Type | Description |
|---|---|---|
name | string | Identifier of the item — what you pass to zard-cli add . |
type | string | Always registry:component . |
basePath | string? | Overrides the destination directory. |
files | { name, content }[] | Path of the file relative to the item directory, plus its full source code. |
dependencies | string[]? | npm packages installed along with the item. |
devDependencies | string[]? | npm dev dependencies of the item. |
registryDependencies | string[]? | Other registry items this one requires. |
docs | { overview, api }? | Markdown documentation of the component, consumed by the MCP server. |
demos | { name, content }[]? | Demo components, also consumed by the MCP server. |
The CLI resolves the destination directory from basePath and the aliases declared in your components.json :
basePath: 'core' — or an item literally named core — resolves to aliases.core . basePath: 'services' resolves to aliases.services — this is how dark-mode is installed. basePath: 'utils' resolves to aliases.utils — this is how the utils item is installed. aliases.components/<basePath ?? name> . The -p, --path flag overrides all of the above and resolves to <cwd>/<path>/<basePath ?? name> :
npx zard-cli add button --path src/app/uiWhichever route is taken, the CLI checks that the resolved destination stays inside the project directory and refuses to write outside of it. Writing the files of an item is also transactional: if any file of an item fails to be written, every file already written for that same item is removed before the error is reported.
An item can declare two kinds of dependency: npm packages and other registry items. They are resolved by different parts of the CLI.
dependencies is collected from every item selected for installation, deduplicated, and installed in a single call using the package manager declared in your components.json — npm , yarn , pnpm or bun . If that install fails, the CLI retries it once with --legacy-peer-deps .
{
"name": "utils",
"type": "registry:component",
"basePath": "utils",
"dependencies": ["tailwind-merge", "clsx"],
"files": ["index.ts", "merge-classes.ts", "number.ts"]
}devDependencies is part of the format and is carried from the manifest into the published JSON, but no item declares it today.
registryDependencies points at other items of the same registry, and they are resolved recursively — a dependency that has its own dependencies pulls them in too. An item whose destination directory already exists and is not empty is skipped, unless you pass -o, --overwrite . With -a, --all the recursive walk is skipped altogether, because every item is already part of the install.
{
"name": "sheet",
"type": "registry:component",
"registryDependencies": ["button"],
"files": [
"sheet.component.ts",
"sheet.variants.ts",
"sheet-ref.ts",
"sheet.imports.ts",
"sheet.service.ts",
"index.ts"
]
}The code stored in the registry is the code of this monorepo, so it uses the internal paths of the library. Before writing a file, the CLI rewrites those imports to the aliases configured in your project:
../../shared/utils/utils becomes <aliases.utils>/merge-classes . ../../shared/utils/number becomes <aliases.utils>/number . ../<something> becomes <aliases.components>/<something> . @/shared/<key>/<x> becomes <aliases[key]>/<x> , for every alias you configured. ClassValue imported from class-variance-authority is re-pointed at clsx . {
"aliases": {
"components": "@app/ui/components",
"utils": "@app/ui/utils",
"core": "@app/ui/core",
"services": "@app/ui/services"
}
}import { mergeClasses } from '@/shared/utils/merge-classes';
import { ZardButtonComponent } from '../button/button.component';import { mergeClasses } from '@app/ui/utils/merge-classes';
import { ZardButtonComponent } from '@app/ui/components/button/button.component'; A few packages track the Angular major version. Those are installed as <package>@^<major>.0.0 , where the major is read from the Angular version detected in your project. Today the only package on that list is embla-carousel-angular .
When the detected Angular version is a pre-release — -rc , -next or -canary — the CLI warns that some dependencies may have compatibility issues, and carries on.
The CLI is the intended client. It fetches the index, resolves the item and its dependencies, rewrites the imports and writes the files into your project.
npx zard-cli add buttonpnpm dlx zard-cli add buttonyarn zard-cli add buttonbunx zard-cli add button429 response is honoured: the CLI waits for the number of seconds given in the Retry-After header before trying again. The MCP server reads the same endpoints, with its own 5 minute cache for the indexes and a 10 second request timeout. It is the consumer that surfaces the docs and demos fields of an item, so an AI assistant can read the documentation and the examples of a component alongside its source code.
Nothing about the registry is specific to the CLI. The files are public, static and served with permissive CORS, so any tool that can perform a GET request can read them.
curl https://zardui.com/r/registry.json
curl https://zardui.com/r/button.json Blocks are larger compositions built on top of the components. They live in libs/blocks/src/lib/<id>/ , and each one declares its id , title , description and category in a block.ts file.
The same build step that produces the component registry also emits apps/web/public/r/blocks/<id>.json and the index apps/web/public/r/blocks-registry.json .
{
"blocks": [
{
"id": "login-01",
"title": "Login form",
"description": "A simple login form.",
"category": "Login"
},
{
"id": "login-02",
"title": "Login with cover image",
"description": "A two column login page with a cover image.",
"category": "Login"
},
{
"id": "signup-01",
"title": "Signup form",
"description": "A simple signup form.",
"category": "Signup"
}
]
}{
"id": "login-01",
"title": "Login form",
"description": "A simple login form.",
"category": "Login",
"files": [
{
"name": "login-01.component.html",
"path": "src/components/login-01/login-01.component.html",
"content": "<div class=\"flex min-h-svh w-full items-center justify-center p-6 md:p-10\">\n...",
"language": "html"
},
{
"name": "login-01.component.ts",
"path": "src/components/login-01/login-01.component.ts",
"content": "import { ChangeDetectionStrategy, Component, signal } from '@angular/core';\n...",
"language": "typescript"
}
]
} Note that the file entries of a block are shaped differently from the ones of a component: besides name and content they also carry a path and a language .
The base URL the CLI reads from is embedded at build time. The published package carries https://zardui.com/r , and that value is what every command falls back to.
ZARD_REGISTRY_URL takes precedence over the embedded default and is what actually redirects the CLI at another registry.
ZARD_REGISTRY_URL=https://my-registry.example.com/r npx zard-cli add button The configuration schema also accepts a registryUrl field, which is validated whenever the configuration is loaded.
{
"registryUrl": "https://my-registry.example.com/r"
}The helper that reads this field is not wired into the fetch path yet, so today the field is validated but not used to resolve the base URL. Use the environment variable when you need to switch registries.
A registry URL must use HTTPS. The only exception is localhost and 127.0.0.1 , which may be plain HTTP so that a local registry works during development. A malformed URL, or a remote one over HTTP, is rejected with a configuration error.
Any host that satisfies the following is a valid registry:
GET <base>/registry.json in the index format described above. GET <base>/<name>.json in the item format for every item listed in the index. Content-Type: application/json . Access-Control-Allow-Origin if browsers are meant to read it. Working on a component means rebuilding the registry and serving it yourself. Three scripts cover that loop:
npm run build:registry
npm run serve:registry
npm run clibuild:registry regenerates apps/web/public/r/** from the sources on disk. serve:registry serves those files at http://localhost:4223/r . If apps/web/public/r does not exist yet, it builds the registry first. cli chains the development build of the CLI and the local server, which is the usual entry point. The port defaults to 4223 and can be changed with the REGISTRY_PORT environment variable:
REGISTRY_PORT=5000 npm run serve:registry The local server only serves .json files, sends Access-Control-Allow-Origin: * on every response, and mirrors production caching: items are immutable, while registry.json must be revalidated.
ZARD_REGISTRY_URL=http://localhost:4223/r npx zard-cli add button The development build of the CLI already embeds http://localhost:4223/r as its default, while the production build embeds https://zardui.com/r . Both come from the same placeholder, replaced at build time.
A component only reaches the registry once it is declared in the manifest. Writing the files is not enough — the build reads the manifest, not the directory listing.
libs/zard/src/lib/shared/components/<name>/ . packages/cli/src/core/registry/registry-data.ts , listing every file that should be copied into the user project and declaring dependencies or registryDependencies when the component needs them. npm run build:registry and check the output in apps/web/public/r/ . {
name: 'utils',
basePath: 'utils',
dependencies: ['tailwind-merge', 'clsx'],
files: [
{
name: 'index.ts',
content: '',
},
{
name: 'merge-classes.ts',
content: '',
},
{
name: 'number.ts',
content: '',
},
],
},npm run build:registry
npm run cliFor the rest of the workflow — branch naming, commit conventions and review — see the CONTRIBUTING.md of the repository.