Start9 Frontend — Angular + Taiga UI house style
Canonical for every Start9 web UI, distilled from the fleet's codebases and from the refactor and review history that shaped them. Follow it and new code matches the fleet; deviate and expect it to be rewritten in review.
Authority order: Taiga UI docs > this skill > neighboring code. The codebases are
mid-migration — "matches existing repo patterns" is not a quality bar; old code is often
exactly what this skill exists to eliminate. Never guess a Taiga API: verify against the
taiga-ui-mcp MCP server or https://taiga-ui.dev/llms-full.txt.
Reference files (read on demand — don't preload)
| File | Read when |
|---|---|
references/bootstrap.md |
creating/touching app.config, main.ts, angular.json, icons, theming, or a new app/repo |
references/components.md |
writing any component or template (selectors, hostDirectives, signal APIs, control flow) |
references/styling.md |
about to write ANY CSS (the escalation ladder, breakpoints, tokens) |
references/di.md |
providers, tokens, inject patterns |
references/state.md |
services, data flow, loading/error UX, API/RPC integration |
references/forms.md |
any form, validation, or submit flow |
references/overlays.md |
dialogs, confirms, toasts, dropdowns, hints, drawers |
references/routing.md |
route files, guards, navigation, URL state |
references/conventions.md |
i18n strings, file naming, folder structure, import order |
references/shared-libs.md |
before writing ANY utility/component in monorepo apps — it may exist in @start9labs/shared or marketplace |
references/antipatterns.md |
reviewing code, refactoring, or upgrading Angular/Taiga (before→after catalog + verbatim review quotes) |
references/taiga.md |
unsure of a Taiga 5 API; v4→v5 renames; need→primitive lookup table |
references/recipes.md |
step-by-step: new page, dialog, form, table, control component, endpoint |
references/repos.md |
repo-specific rules and build/verify commands |
The fleet
| App | Location | Angular | Taiga | Zone | Theme | i18n | Backend |
|---|---|---|---|---|---|---|---|
StartOS ui |
start-technologies projects/start-os/web/ui |
22 | 5.11 | zone.js (zoneless pending) | dark, provideTaiga({mode}) |
yes (shared dicts) | JSON-RPC + PatchDB push |
setup-wizard |
projects/start-os/web/setup-wizard |
22 | 5.11 | zone.js | dark | yes (shared) | JSON-RPC |
start-tunnel |
projects/start-tunnel/web |
22 | 5.11 | zoneless | dark | yes (local dicts) | JSON-RPC + PatchDB |
start-wrt |
projects/start-wrt/web |
22 | 5.11 | zoneless | dual (TUI_DARK_MODE) |
yes (local dicts) | JSON-RPC, own HTTP stack, 5s polling |
brochure-marketplace |
projects/brochure-marketplace |
22 | 5.11 | zone.js (legacy) | dark | yes (shared) | registry RPC direct |
start9-store |
ops/start9-store/web |
22 | 5.22 | zoneless | light | no | REST + Zod via /api BFF, SSR |
ops-server |
ops/ops-server/web |
22 | 5.14 | zoneless | dark, #07a4ff, Montserrat |
no | REST /_api, same-origin Express |
support-server |
ops/support-server/web |
22 | 5.22 | zoneless | dual (OS preference), StartOS tokens | yes (local dicts) | Frappe /api/method + socket.io, same-origin; web/mock in dev |
TypeScript ~6.0, rxjs ~7.8 everywhere. Taiga is pinned exact — bump only with the maintainer's blessing. Monorepo apps share one Angular workspace rooted at the repo root; ops repos are standalone. Zoneless is the target state — write all new code zoneless-safe. This table is the only place in the fleet where stack versions are written down — update it with every Angular/Taiga bump (other repos' docs deliberately carry no version specifics).
Doctrine
- Taiga does it all. Components, layout, forms, dialogs, icons, theming, animation. If you're hand-rolling HTML/CSS/JS for something that feels generic, Taiga ships it — look it up first. "If you think Taiga can't do something, you're probably wrong."
- Never guess a Taiga API. Taiga 5 is fast-moving and easy to hallucinate. Verify every component/directive/token against the docs before use.
- Configure the design system, don't fight it. The escalation ladder for any visual need:
① a Taiga primitive/appearance → ② an option provider (root or component
providers) → ③ a--tui-*design-token override in the theme sheet → ④ a sharedg-*utility class → ⑤ a few lines of:hostlayout CSS. Reaching for ⑤ first means you missed ①–④. - Delete code. The canonical refactors are net-negative. The best version has fewer lines, fewer files, fewer wrappers, fewer names. Don't restate framework defaults, don't name single-use values, don't duplicate markup per breakpoint.
- Signals at the component boundary; RxJS composes once, in services. Components read
signals; services own streams and convert with
toSignalat the edge. Data is never manually subscribed in a component. - One source of truth per fact. Nav links live in one object; validation messages in one provider map; sizes in option providers; colors in the theme sheet.
- Everything host-related goes in the decorator.
host: {}for classes/attrs/listeners/ style bindings,hostDirectivesfor composition.@HostBinding/@HostListenerare dead. - English strings are i18n keys (monorepo apps): every user-facing string goes through
| i18nand exists in all five dictionaries;tscenforces via thei18nKeytype. - Verification is
tsc+ Prettier, not tests. No unit-test runner is wired up anywhere.npm run check(strict +strictTemplates), the i18n check, a prod build, and manual verification are the bar. Don't claim "tests pass"; don't add a test framework unasked. - The docs ship with the change — this skill first. This skill is the fleet-wide
frontend source of truth: the ops repos reach it through committed symlinks, and stack
versions live only in its fleet table. When frontend conventions, versions, or idioms
change, update
SKILL.mdand the affectedreferences/*.mdin the same change; other repos' docs carry only project-specific facts.
If you learned Angular anywhere else — the surprise index
- No
NgModuleanywhere — standalone components only. - No
.html/.scsscomponent files. 100% inlinetemplate:andstyles:— one file per component. - No constructor parameter injection.
inject()in field initializers, even chained:protected route = toSignal(inject(Router).events). - No
ngOnInit. Field initializers; a bareconstructor() { this.load() }kicks off fetches;afterNextRenderfor browser-only side effects. - No
*ngIf/*ngFor/ngClass/ngStyle.@if/@for(withtrack),@let,@switch;[class.x]and unit-typed[style.prop.unit]bindings. - No
@Input/@Output/@ViewChilddecorators.input(),input.required(),output(),model(),viewChild(),contentChild(). - No
provideAnimations(). Taiga 5 animates with CSS (TuiAnimated). - No explicit event-plugins provider.
provideTaiga()bundles it — that's what makes(submit.prevent),(click.self),.stop,.capturemodifiers work. - No
TuiAlertService. Toasts areTuiNotificationService; blocking loaders areTuiNotificationMiddleService. - No
FormBuilder.inject(NonNullableFormBuilder).group({...})with array shorthand;[(ngModel)]="signal"for single ad-hoc fields. - No route-level
providers, no resolvers, few guards (inlinecanMatcharrows). Providers go on components — lazy-route providers spin up confusing semi-root injectors. - No
@mediaqueries for the app-standard mobile swap:tui-root._mobile &CSS,TUI_BREAKPOINT/WA_IS_MOBILEsignals, template@ifswaps — one DOM, never two. - No BEM, no barrels, no
index.tsre-exports in apps. Taiga-style_stateclasses and sharedg-*utilities; deep relative orsrc/-absolute imports. - No semicolons. Prettier:
singleQuote,semi: false,arrowParens: "avoid",trailingComma: "all",htmlWhitespaceSensitivity: "ignore",tabWidth: 2. - No ESLint, no unit tests. Prettier runs via husky/lint-staged — never
git commit --no-verify; fix the formatting. - Suffixless files and classes in new code:
routes/devices/index.tsexportingexport default class Devices, plusdialog.ts,table.ts,service.ts— notdevices-page.component.ts/DevicesPageComponent. - Services can be Observables (
class ConnectionService extends Observable<boolean>); tokens can be signals (new InjectionToken('…', { factory: () => signal(false) })). DOCUMENTand@Service()import from@angular/corenow —@Service()is the emerging norm for new services over@Injectable({providedIn: 'root'}).- The mock backend is a DI swap:
useClass: useMocks ? MockApiService : LiveApiService.
Review checklist (greppable)
Any hit is a finding unless it matches a documented exception in the reference files:
NgModule constructor(private *ngIf / *ngFor
ngClass / ngStyle @Input( / @Output( / @ViewChild(
@HostBinding / @HostListener
templateUrl / styleUrl
FormBuilder (not NonNullable) new FormGroup( : any
provideAnimations TuiAlertService tuiFieldError
ngOnInit setTimeout .subscribe( [outside allowed shapes]
::ng-deep !important letter-spacing / text-transform
#[0-9a-f]{3,6} [outside the theme sheet] @media [outside documented divergences]
window. / document. / localStorage [outside infrastructure]
providers: [ on a route track $index [on entity lists]
input<T | null>(null) display: grid on :host of a wrapper around one child
Softer review questions: does a computed just reshape for the template (→ pipe)? Is a value
named but used once (→ inline)? Is the same appearance/size attribute repeated (→ option
provider)? Is there a second DOM for mobile (→ one DOM + _mobile CSS)? Did copy ship in
Title Case (→ sentence case)? Is a Taiga API used that you didn't verify against the docs?