This file provides context for AI assistants working on this codebase.
This is @visa/nova-angular (v6.0.2), the Angular implementation of the Visa Product Design System (VPDS/Nova). It is a component library published as an npm package, accompanied by a workshop documentation app.
projects/
nova-angular/ # The publishable Angular component library
src/
index.ts # Public API barrel file
lib/ # ~105 component/directive directories (flat structure)
workshop/ # Documentation site / example showcase app
src/app/
components/ # Component doc pages and examples
patterns/ # Complex pattern examples (dynamic-table, file-upload, etc.)
utilities/ # Utility doc pages
foundations/ # Foundation doc pages
services/ # Service doc pages
shared/ # Shared workshop components and services
bin/ # Build scripts (route-gen, metadata, themes, stats)
- Angular 21 (library supports ^19 || ^20 || ^21)
- TypeScript 5.9
- pnpm (v8.12.0) - enforced via
preinstallcheck - ng-packagr for library builds
- Jest 30 with
@testing-library/angularfor tests - @floating-ui/dom for positioning (tooltips, dropdowns, comboboxes)
- @visa/nova-styles for base CSS/theme tokens
- @visa/nova-icons-angular for icons
- Prettier for formatting (120 char width, single quotes, no trailing commas, 2-space indent)
pnpm build:lib # Build the library (output: dist/nova-angular/)
pnpm build:docs # Build the workshop documentation app
pnpm test:lib # Run library unit tests
pnpm test:lib:watch # Run library tests in watch mode
pnpm test:docs # Run workshop tests
pnpm test # Run all tests (both projects)
pnpm start # Serve the workshop app locally
pnpm generate-routes # Auto-generate workshop route files (run during prepare)
pnpm generate:themes # Generate theme files (run during prepare)
pnpm api # Generate Compodoc API docs (run during prepare)Most library building blocks are standalone Directives, not Components. They use the host property to bind CSS classes and attributes to the host element. Only use a Component when a template is truly needed (e.g., CircularProgressComponent, IconComponent, InputContainerComponent, ListboxItemComponent, VisaLogoComponent).
// Typical directive pattern
@Directive({
host: {
class: 'v-button',
'[class.v-button-secondary]': 'buttonColor() === "secondary"',
'[attr.disabled]': 'disabled() ? "disabled" : null',
},
selector: '[v-button]',
standalone: true,
})
export class ButtonDirective { ... }The library is fully migrated to Angular signals. Use these APIs exclusively:
input()/input.required()for inputs (not@Input())model()for two-way bindable valuesoutput()for outputs (orEventEmitterfor pre-existing patterns)signal()/computed()for internal stateeffect()for side effectscontentChild()/contentChildren()for content queries (not@ContentChild)viewChild()/viewChildren()for view queries
All directives apply v- prefixed CSS classes from @visa/nova-styles. Selectors follow attribute syntax: [v-button], [v-combobox], [v-divider], etc.
Use as const objects with derived types instead of TypeScript enums:
export const ButtonSize = {
SMALL: "small",
MEDIUM: "medium",
LARGE: "large",
} as const;
export type ButtonSize = (typeof ButtonSize)[keyof typeof ButtonSize];ListenerServiceis provided per-directive (inproviders: [ListenerService]) for lifecycle-aware listener cleanupFloatingUIService,NovaLibService,ListboxServiceareprovidedIn: 'root'- Use
inject()function instead of constructor injection - Parent/child communication is done via
inject(ParentDirective, { optional: true, host: true })
Inputs that map to native HTML attributes use the alias option:
readonly disabledInput = input<boolean | null, unknown>(null, {
alias: 'disabled',
transform: booleanAttribute,
});The internal signal name often has an Input suffix (e.g., disabledInput) while a separate computed() named disabled merges the user input with parent/child state.
Every .ts, .html, and .scss file in the library must include the Apache 2.0 license header:
/**
* © 2025 Visa
*
* Licensed under the Apache License, Version 2.0 (the "License");
* ...
**/Each directive/component lives in its own directory under projects/nova-angular/src/lib/:
button/
button.directive.ts # The directive/component
button.directive.spec.ts # Unit tests
button.constants.ts # Related constants/types (if any)
__snapshots__/ # Jest snapshots (if any)
Each example is a standalone component in a *.docs.ts file:
components/button/
button.docs.ts # Main docs page component (aggregates examples)
button.docs.html # Main docs page template
button.docs.spec.ts # Snapshot test for docs
button.routes.ts # Auto-generated route file
primary-default/
primary-default.docs.ts # Individual example component
primary-default.docs.html # Example template
Workshop example components use /** #docs */ comment before @Component to mark them for route generation. They import NovaLibModule and are standalone: true with ChangeDetectionStrategy.OnPush.
Tests use @testing-library/angular:
import { render } from "@testing-library/angular";
it("should render defaults correctly", async () => {
const { container } = await render("<button v-button>Content</button>", {
imports: [ButtonDirective],
});
expect(container).toMatchSnapshot();
});- Library tests:
projects/nova-angular/jest.config.ts - Workshop tests:
projects/workshop/jest.config.ts - Accessibility testing via
jest-axeis available - Snapshot testing is the primary approach for verifying rendered output
All public exports go through projects/nova-angular/src/index.ts. This file exports:
- Three NgModules:
NovaLibModule,NovaCommonModule,NovaFormsModule - All directives and components individually (for tree-shaking)
- Constants and types
- Services (
FloatingUIService,NovaLibService,ComboboxService,ListboxService,IdGenerator,AppReadyService) - The
PaginationControlutility
When adding a new component or directive, it must be:
- Exported from
index.ts - Added to the appropriate NgModule(s) in both
importsandexportsarrays
| Service | Scope | Purpose |
|---|---|---|
NovaLibService |
Root | Arrow key navigation, tab management, aria-current |
FloatingUIService |
Root | Floating UI positioning for tooltips/dropdowns/comboboxes |
ListboxService |
Root | Listbox scroll and selection management |
ComboboxService |
Root | Combobox filtering logic |
ListenerService |
Per-directive | Lifecycle-aware cleanup of Renderer2 listeners and subscriptions |
IdGenerator |
Root | Unique ID generation for accessibility attributes |
Route files (*.routes.ts) under projects/workshop/ are auto-generated by bin/route-gen.mjs. Do not manually edit files marked with /** This file is autogenerated */. Run pnpm generate-routes to regenerate them.
Husky runs on pre-commit:
- Prettier formatting on workshop component docs files
bin/workshop/check-docs-naming.jsto verify naming conventions
When writing or modifying workshop documentation, examples, or code:
- Always consult VPDS content guidelines via the design-api-dev MCP before writing component documentation
- Use MCP tools to get guidelines for the specific component you're documenting:
- Use
mcp__design-api-dev__get_guidelineswith the library name, version, and component name - Apply the returned content guidelines to ensure documentation follows VPDS standards
- Use
- Check for component properties using
mcp__design-api-dev__get_propertiesto ensure all available properties are accurately documented - Reference official examples using
mcp__design-api-dev__get_exampleswhen creating workshop examples
This ensures all workshop content aligns with official VPDS content standards and guidelines.
- Do not use TypeScript enums; use
as constobjects with derived types - Do not use
@Input()/@Output()/@ContentChild()decorators; use signal-based equivalents - Do not use Zone.js-dependent patterns (e.g.,
setTimeoutfor change detection) - Do not manually edit auto-generated route files
- Do not import from deep library paths; import from
@visa/nova-angular - Do not add components to NgModules without also exporting them