Component Conventions
Rules every tip-* design-system component follows. New components should match these
exactly so the library stays predictable to compose.
Component shape
- Standalone Angular components (
standalone: true), no NgModules. ChangeDetectionStrategy.OnPushon every component, always set explicitly.- Signal inputs/outputs:
input()/input.required()for props,output()for events — never@Input()/@Output()decorators or two-way[(ngModel)]. Value components (Input,Select,Checkbox,Radio) are controlled: parent owns a signal, passes it in as[value]/[checked], and writes it back from(valueChange)/(checkedChange)/(select). - Selector prefix
tip-for every reusable UI primitive undersrc/app/ui/(tip-button,tip-card, …). Feature/pattern components useapp-(app-home,app-orders,app-settings-general). - File layout: one folder per component under
src/app/ui/<name>/, containing<name>.ts(inlinetemplate/stylesfor small components, ortemplateUrl/styleUrlonce a component's markup outgrows a one-liner) and<name>.spec.ts. Multi-part components (avatar+avatar-group,menu+menu-item,toast+toast.service+toast-host) share one folder. - Token-only styling: component
stylesreference CSS custom properties (var(--tip-ink-900),var(--radius-lg),var(--fs-body), …) exclusively — no hard-coded colors, spacing, or type sizes. This is what keeps Tailwind utilities and component CSS bit-for-bit identical (see tokens.md). - Shared types live in
ui/ui.types.ts:Tone(re-exported fromcore/models/enums, the single source of truth — do not redeclare it),ButtonVariant,Size. Component-specific unions (CardVariant,AvatarStatus,SkeletonVariant,InputShape,SelectOption,MenuPlacement, …) live beside their component instead. - Content projection over config objects: icon/trailing/action/footnote slots use
<ng-content select="[slotName]">(e.g.tip-button's[icon]/[iconRight],tip-input's[trailing],tip-modal's[primary-action]/[secondary-action],tip-stat-card's[footnote]). Empty slots are hidden via a:not(:empty)/:emptyCSS pair rather than*ngIf. - Loading state: any component that can appear before data resolves accepts a
loadinginput and renderstip-skeletoninternally (tip-input,tip-select,tip-checkbox,tip-stat-card,tip-nav-item,tip-activity-item,tip-progress-bar) rather than making every call site branch on loading itself.
Icons
@lucide/angular (v1 — not the deprecated lucide-angular package) has no
<lucide-icon name="..."> component; every glyph is its own standalone component. tip-icon
(src/app/ui/icon/icon.ts) wraps LucideDynamicIcon (svg[lucideIcon]) so call sites keep
passing a plain kebab-case name string ("triangle-alert", "shield-alert"). Icons must
be registered via provideLucideIcons(...) in app.config.ts before tip-icon can resolve
them by name — adding a new icon means adding it to that registration, not just using the
name in a template.
Testing
Vitest via @angular/build:unit-test (npm test from app/frontend/). Every tip-*
component ships a co-located <name>.spec.ts covering: default render, each variant/tone,
loading state, and output emission on interaction (click/change/keydown). Feature specs
additionally cover route-driven state (e.g. order-details.spec.ts asserting the page
renders the order resolved from the route param) and async transitions (Orders'
skeleton → data → empty-state sequence).
Dependency direction
features/ → ui/ + core/data/. ui/ components are purely presentational (inputs/outputs
only — no service injection, no routing). features/ compose ui/ components and read from
core/data/ services (OrdersService, DashboardService, AuthService). Never inject a
service directly into a ui/* component.