@wizards-space/ui
v0.13.3
Published
Angular 20 component library on top of ng-zorro-antd 20, themed entirely through CSS custom properties. Ships **no** vendor SDKs — no Firebase, no API clients, no i18n runtime — so it drops into any app.
Readme
@wizards-space/ui
Angular 20 component library on top of ng-zorro-antd 20, themed entirely through CSS custom properties. Ships no vendor SDKs — no Firebase, no API clients, no i18n runtime — so it drops into any app.
This document is written to be followed step by step, including by a coding agent setting up a fresh application. Follow the sections in order.
1. Install
npm install @wizards-space/uiPeer dependencies your app must already have:
| Package | Range |
| --- | --- |
| @angular/animations, @angular/common, @angular/core, @angular/forms, @angular/platform-browser, @angular/router | ^20.1.0 |
| ng-zorro-antd | ^20.1.0 |
| rxjs | ^7.8.0 |
2. Wire up styles
2.1 Add the global stylesheet
The library's stylesheet must be loaded once, globally. It is LESS and imports
ng-zorro's variable theme, so it has to go through the build's styles array — not
through a component's styleUrls.
In angular.json / project.json, in the build target options:
{
"styles": [
"src/styles.less",
"node_modules/@wizards-space/ui/styles/styles.less"
],
"stylePreprocessorOptions": {
"includePaths": ["node_modules/@wizards-space/ui/styles"]
}
}includePaths is required — partials reference each other by bare name.
Repeat the same two entries in the serve target if it does not inherit from build,
and in the Storybook target if the app has one.
2.2 Copy the assets
Fonts and icons are referenced by relative URL from the stylesheet, so they must be
served from /assets:
{
"assets": [
{
"glob": "**/*",
"input": "node_modules/@wizards-space/ui/assets/fonts",
"output": "/assets/fonts"
},
{
"glob": "**/*",
"input": "node_modules/@wizards-space/ui/assets/icons",
"output": "/assets/icons"
},
{
"glob": "**/*",
"input": "node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets"
}
]
}The last entry is ng-zorro's icon set — omit it only if the app already provides it.
2.3 Activate a theme at bootstrap
Nothing is styled until the wiz-theme attribute is set on <body>. Do it once, at
startup:
// app.config.ts
import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core';
import { WizThemeService } from '@wizards-space/ui';
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => inject(WizThemeService).init()),
// …
],
};At this point the app renders with the library's default look. Sections 3 and 4 make it yours.
3. Theming — how it works
Tokens are layered, each layer resolving into the one below:
primitives → colors (semantic) → components → ng-zorro LESS variablescomponents— ~1600 tokens, ~95% of them plainvar(--wiz-colors-…)redirects. You almost never touch this layer.colors— the semantic layer, split into three groups:| Group | Count | Role | | --- | --- | --- | |
--wiz-colors-base-*| 160 | raw palette — shared | |--wiz-colors-neutral-*| 57 | greys, borders, backgrounds — usually shared | |--wiz-colors-brand-*| 61 | this is what an app overrides |
Each layer is scoped by an attribute selector, e.g. [wiz-theme~="colors-light"].
WizThemeService writes a space-separated list of layer names onto <body>:
<body wiz-theme="colors-light editor-dark dimensions-default wiz-styles typography components">Because ~= matches any single word in that list, adding your own layer is just
appending a name — your layer is loaded after the library's, so it wins.
4. Give the app its own brand
Step 1 — write a brand layer
Create src/styles/brand.css. Override only the --wiz-colors-brand-* tokens you
care about; anything you omit falls through to the library default.
[wiz-theme~='brand-myapp'] {
--wiz-colors-brand-color-primary: rgb(88, 60, 220);
--wiz-colors-brand-color-primary-hover: rgb(112, 88, 232);
--wiz-colors-brand-color-primary-active: rgb(66, 42, 180);
--wiz-colors-brand-color-primary-bg: rgb(240, 236, 255);
--wiz-colors-brand-color-primary-bg-hover: rgb(226, 220, 252);
--wiz-colors-brand-color-primary-border: rgb(190, 176, 246);
--wiz-colors-brand-color-primary-border-hover: rgb(150, 128, 240);
--wiz-colors-brand-color-primary-text: rgb(88, 60, 220);
--wiz-colors-brand-color-primary-text-hover: rgb(112, 88, 232);
--wiz-colors-brand-color-primary-text-active: rgb(66, 42, 180);
}The full list of overridable names is in
node_modules/@wizards-space/ui/styles/figma/wiz-ds-colors-light.css — every
--wiz-colors-brand-* declaration there is a valid override. Provide a matching
brand-myapp-dark layer if the app supports dark mode.
Register it after the library stylesheet:
"styles": [
"node_modules/@wizards-space/ui/styles/styles.less",
"src/styles/brand.css"
]Step 2 — tell WizThemeService to apply it
// app.config.ts
import {
WizThemeService,
WIZ_THEME_CONFIG,
WIZ_DEFAULT_THEME_LAYERS,
} from '@wizards-space/ui';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: WIZ_THEME_CONFIG,
useValue: {
layers: {
light: `${WIZ_DEFAULT_THEME_LAYERS.light} brand-myapp`,
dark: `${WIZ_DEFAULT_THEME_LAYERS.dark} brand-myapp-dark`,
},
defaultTheme: 'light',
// Namespace this per app — several apps on one origin share localStorage.
storageKey: 'myapp_theme',
},
},
provideAppInitializer(() => inject(WizThemeService).init()),
],
};That is the whole theming setup. WizThemeSwitcherComponent toggles light/dark
against this config; WizThemeService.themeChange emits the resolved theme.
Component-level styling
Inside your own components, always reference tokens rather than literal colours:
.panel {
background: var(--wiz-colors-neutral-color-bg-container);
border: 1px solid var(--wiz-colors-neutral-color-border);
color: var(--wiz-colors-neutral-color-text);
}A hard-coded hex will not follow the theme and will break dark mode.
Known global overrides
The partials were adopted 1:1 from Prometeus and still carry a few app-specific global rules. They are slated to move back into Prometeus; until then, be aware of them:
.ant-cardis forced towidth: 100%and gets amin-widthbelow thesmbreakpoint (_panel.less)..gap-col,.row-green-background,.table-row-green-background,.table-row-red-background,.table-text-color-green,.table-text-color-red(_table.css);.gap-coluses hard-coded colours..wsc-panelreferences undefined tokens, so it renders without border and radius (_panel.less).
5. Optional providers
Two features need the app to supply an implementation. Skip a section if you do not use those components.
5.1 File storage
WizDisplayImgComponent, WizUploadFileDragAndDropComponent and WizDownloadFileService
talk to storage through the WIZ_FILE_STORAGE token. The library deliberately ships
no adapter.
import { WizFileStorage, WizFileMetadata } from '@wizards-space/ui';
import {
deleteObject, getDownloadURL, getMetadata, getStorage, ref, uploadBytes,
} from 'firebase/storage';
export class FirebaseFileStorage implements WizFileStorage {
private readonly storage = getStorage();
getDownloadUrl(path: string): Promise<string> {
return getDownloadURL(ref(this.storage, path));
}
async exists(path: string): Promise<boolean> {
try {
await getMetadata(ref(this.storage, path));
return true;
} catch (error) {
if ((error as { code?: string }).code === 'storage/object-not-found') return false;
throw error;
}
}
async getMetadata(path: string): Promise<WizFileMetadata> {
const meta = await getMetadata(ref(this.storage, path));
return { name: meta.name, size: meta.size, contentType: meta.contentType };
}
async upload(path: string, file: File): Promise<void> {
await uploadBytes(ref(this.storage, path), file);
}
async delete(path: string): Promise<void> {
await deleteObject(ref(this.storage, path));
}
}{ provide: WIZ_FILE_STORAGE, useClass: FirebaseFileStorage }WizInMemoryFileStorage is exported for tests, Storybook and prototyping.
5.2 ng-zorro modal and notification services
WizModalService and WizNotificationService wrap ng-zorro's, which must exist in the
root injector:
import { importProvidersFrom } from '@angular/core';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { provideAnimations } from '@angular/platform-browser/animations';
providers: [provideAnimations(), importProvidersFrom(NzModalModule)]6. Using components
Everything is standalone — import the component directly:
import { Component } from '@angular/core';
import { WizInputComponent, WizPageTitleComponent } from '@wizards-space/ui';
@Component({
selector: 'app-example',
imports: [WizPageTitleComponent, WizInputComponent],
template: `
<wiz-page-title titleText="Users" [showBack]="true" backLink="/users" />
<wiz-input [formControl]="name" label="Name" />
`,
})
export class ExampleComponent {}[showBack]="true" adds a back arrow to wiz-page-title: with backLink (same forms as routerLink) it is a link,
without it one step back in history. A (back) listener replaces either default.
Naming — two prefixes, on purpose
| Layer | Prefix | Examples |
| --- | --- | --- |
| TypeScript symbols | Wiz | WizInputComponent, WizThemeService, WizEmptyValuePipe |
| Element selectors | wiz- | <wiz-input>, <wiz-page-title> |
| CSS custom properties | --wiz- | --wiz-colors-brand-color-primary |
| Theme attribute / layer names | wiz- | <body wiz-theme="colors-light …"> |
The design tokens use --wiz- because they are generated from Figma by the
VVVariable Exporter plugin — renaming them by hand would be undone by the next
export. They live in a namespace of their own, so they never collide with the
TypeScript symbols.
There is no --wiz- prefix. Writing var(--wiz-…) fails silently: CSS does not
warn about an undefined custom property, the declaration is simply dropped. Five such
typos went unnoticed in this library for months. If a style has no effect, check the
prefix first.
What's in the box
| Area | Components |
| --- | --- |
| Form controls | WizInput, WizInputNumber, WizTextarea, WizSelect, WizNestedSelect, WizCheckbox, WizStaticCheckbox, WizDatepicker, WizAutocomplete, WizSearch, WizControlTemplates, WizFormErrors, WizErrorMessage |
| Layout & shell | WizViewLayout, WizContainer, WizGrid, WizHeader, WizFooter, WizNavbar, WizNavigatorExpander, WizSwitcherExpander, WizPageTitle, WizBreadcrumbs, WizScrollHorizontal, WizTabs, WizMobileDropdown, WizTopBarSupportMenu |
| Content | WizCard, WizCardSimple, WizCardTitle, WizTile, WizTileEmpty, WizStatisticCard, WizEntity, WizContentDisplay, WizDisplayLine, WizText, WizEmpty, WizCollapse, WizStatusBadge, WizTooltip, WizLogo, WizRoleIcon, WizOnlineStatusIndicatedText |
| Feedback | WizAlert + WizAlertService, WizNotificationService, WizDeleteModal + WizModalService, WizLoadingSpinner |
| Files & media | WizDisplayImg, WizUploadFileDragAndDrop, WizUserAvatar, WizUserAvatarUpload, WizDownloadFileService |
| Theming & config | WizThemeService, WizThemeSwitcher, WIZ_THEME_CONFIG, WIZ_UI_CONFIG |
| Utilities | WizEmptyValuePipe, WizSafeHtmlPipe, WizTemplateBinderDirective, WizPostalCodeMaskDirective, WizAbstractControlDirective, WizLayoutService, WizResponsiveService |
Browse them with live controls:
npx nx storybook ui7. Localisation
The library contains no i18n runtime and does not depend on @angular/localize.
Every user-facing string is an input with an English default, so apps pass their own:
<wiz-list-buttons
addButtonLabel="Dodaj pozycję"
backButtonLabel="Wróć"
generateFileButtonLabel="Generuj plik" />The same applies to WizUploadFileDragAndDrop (text, hint, fileExistsError),
WizDeleteModal (titlePrefix, cancelLabel, confirmLabel) and
WizUserAvatarUpload (text, hint).
Strings repeated on almost every screen don't have to be passed each time — set them once
through WIZ_UI_CONFIG. Keys you leave out keep their English default, and inputs still
override the config per instance:
// app.config.ts
import { WIZ_UI_CONFIG } from '@wizards-space/ui';
providers: [
{
provide: WIZ_UI_CONFIG,
useValue: {
emptyPlaceholder: '---', // wiz-entity, empty value
backLabel: 'Wróć', // wiz-list-buttons, wiz-page-title back arrow
moreActionsLabel: 'Więcej akcji', // wiz-page-title, actions menu on phones
generateFileLabel: 'Generuj plik', // wiz-list-buttons
homeLabel: 'Strona główna', // wiz-breadcrumbs, first item
homeLink: '/app/dashboard', // wiz-breadcrumbs, first item
},
},
];8. Setup checklist
- [ ]
@wizards-space/uiinstalled, peer dependency versions match - [ ]
styles.lessadded to the build'sstyles, withincludePathsset - [ ] fonts + icons registered in
assets - [ ]
WizThemeService.init()runs at bootstrap - [ ] brand layer written, added to
stylesafter the library stylesheet - [ ]
WIZ_THEME_CONFIGprovided, with the brand layer appended andstorageKeynamespaced - [ ]
WIZ_FILE_STORAGEprovided — only if file/image components are used - [ ]
WIZ_UI_CONFIGprovided — only if the app is not in English - [ ]
provideAnimations()+NzModalModulein root — only if modals are used - [ ] a component renders with the app's brand colour, in light and dark
