# Trackofy V2 — UI & Code Style Guide

> **Status:** Analysis-only deliverable. Derived entirely from the existing **Dashboard** module (`src/app/panels/dashboard/`) and **Units** module (`src/app/panels/vehicle/`, UI-labelled "Unit List" / "Unit Settings"), plus the shared infrastructure both modules depend on (`src/app/shared/components/table/`, `src/styles.scss`, `src/tailwind.css`). No source files were modified to produce this document.
>
> Where the two modules disagree with each other, both variants are recorded and a recommendation is given — this codebase currently has **two parallel typography/theming idioms** living side by side (see §13 Best Practices and §14 Checklist).

---

## 1. Project Overview

Trackofy V2 is an Angular 19 fleet-tracking SaaS, fully standalone-component (no NgModules), built on:

| Layer | Technology |
|---|---|
| Component framework | Angular 19, standalone components, signals + `BehaviorSubject` mixed state |
| UI library | Angular Material 19, custom M3-token theme (`azure`-derived, overridden in `styles.scss`) |
| Utility CSS | Tailwind CSS 4 (`@import "tailwindcss";` only — no custom config/theme extension file exists) |
| Charts | `ngx-echarts` core registration + a hand-rolled `ChartComponent` wrapper around raw `echarts.init()` |
| Grid layout | `angular-gridster2` (Dashboard graphical page only) |
| Font | Lato (self-hosted `@font-face`, weights 100/300/400/700/900, **also** double-loaded via Google Fonts `<link>` in `index.html` — redundant, see §13) |
| Dark mode | Global `.dark` class on `<html>`/root, toggled by `CommonService`, redefining Material system tokens |

The two modules analyzed represent two different eras/authors of the codebase:

- **Units module** (`vehicle/`) — an older, Material-native implementation: real `MatTableModule`/`MatSort`/`MatPaginator`, `mat-form-field`, `MatDialog` with viewport-relative sizing, heavy `[ngClass]` dark-mode branching.
- **Dashboard module** (`dashboard/`) — a newer, more custom implementation: hand-built `<div>` "cards" (no `MatCardModule` anywhere), `angular-gridster2` widget grid, a custom ECharts wrapper, Tailwind-heavy templates, `animate-pulse` skeleton loaders.

Both are legitimate, working patterns. This guide documents both and recommends which to standardize on for new modules (see §15).

---

## 2. Typography Guide

### 2.1 Font stack

- **Family:** `"Lato"` — declared in `styles.scss` via 9 `@font-face` rules (normal + italic, weights 100/300/400/700/900) loading local `.ttf` assets, redundantly also linked from Google Fonts in `index.html`. **Pick one loading strategy** (see §13).
- **Base body size:** `font-size: 12px` set globally on `body` in `styles.scss`, then forced onto Material internals (`.mdc-*`, `.mat-mdc-*`) via `!important` overrides — the app runs a deliberately **dense/compact** UI, not Material's default density.
- `font-synthesis-weight: none; font-synthesis-style: none;` on `:root` — prevents the browser from faux-bolding/italicizing weights that aren't loaded.

### 2.2 Text hierarchy (as actually implemented)

| Role | Units module | Dashboard module |
|---|---|---|
| Page title | `.text-xl.font-bold` ("Unit List") | `.text-base.font-medium` (breadcrumb tab labels act as page title) |
| Dialog/section title | `text-sm pl-1 font-bold` or `text-xs font-bold` | `text-xl font-bold` (AI Insights headings), `text-lg font-semibold` (AI card headings) |
| Card/widget title | — (no card concept in Units) | `font-semibold text-xs` (vertical-card, vehicle-overview, table-card headers) |
| Stat / big number | — | `text-[16px] font-bold` (vertical-card header value); `text-sm sm:text-xs md:text-lg font-semibold` (vehicle-overview count) |
| Table header | `.text-head { font-size: small; font-weight: 500; }` (main Unit List) **or** `.table-head { font-size: smaller; font-weight: bold; }` (every settings sub-dialog table — 11+ occurrences) | `text-xs font-medium` (dashboard-tabular-page real `mat-table`) |
| Table body | `.text-body { font-size: smaller; }` + `text-xs!` on most `<td>` (Unit List) **or** `.table-data { font-size: smaller; }` (settings tables) | `text-[11px]` / `text-[10px]` (widget mini-tables) |
| Form label | Default `mat-label` (no override) | — |
| Helper/hint text | `text-xs text-red-600` (validation only, no neutral hint convention) | `text-[11px] text-gray-400` |
| Chart axis/tooltip | — | Set programmatically in ECharts option objects: axis `fontSize: 8`/`10`, tooltip `fontSize: 10`, "no data" title `fontSize: 16` |

> **Key finding:** the Units module has **two different table typography pairs** in active use at once — `.text-head`/`.text-body` (small/500, smaller) on the main list, and `.table-head`/`.table-data` (smaller/bold, smaller) copy-pasted across every settings sub-table. Neither is defined in a shared location; each component's `.scss` redeclares them locally. **New modules should not add a third variant** — see §7 for the resolved standard.

### 2.3 Font-weight vocabulary in use

`400` (normal/body) · `500` (medium — Unit List header, dashboard card values-as-medium) · `600` (semibold — dashboard card/section titles) · `700`/`bold` (settings-table headers, stat numbers) · `800` (used in some CAN-module tables, not seen in Dashboard/Units — flag as a third module's convention, do not import into Dashboard/Units work).

---

## 3. Color Palette

### 3.1 Material System (M3) tokens — the source of truth

Defined once in `src/styles.scss` as CSS custom properties, redefined under a `.dark` selector for dark mode. **Always reference these tokens (`var(--mat-sys-*)` or Tailwind's `bg-(--mat-sys-*)` arbitrary-value syntax) rather than hardcoding hex values.**

| Token | Light | Dark | Typical use |
|---|---|---|---|
| `--mat-sys-primary` | `#0b73a1` | `rgb(146 206 246)` | Brand accent, table headers, active states |
| `--mat-sys-on-primary` | `rgb(255 255 255)` | `rgb(17 35 44)` | Text/icons on primary bg |
| `--mat-sys-primary-container` | `rgb(199 231 255)` | `rgb(0 76 108)` | Tinted primary surfaces |
| `--mat-sys-secondary` | `#031b4e` | `rgb(179 197 255)` | Secondary accents |
| `--mat-sys-tertiary` | `rgb(0 105 109)` | `rgb(128 212 216)` | Tertiary accents |
| `--mat-sys-error` | `rgb(186 26 26)` | `rgb(255 180 171)` | Error states |
| `--mat-sys-on-error` | `rgb(255 255 255)` | `rgb(105 0 5)` | Text on error bg |
| `--mat-sys-background` | `rgb(246 250 254)` | `rgb(16 20 23)` | App background |
| `--mat-sys-surface` | `rgb(255 255 255)` | `rgb(28 28 34)` | Card/panel surface |
| `--mat-sys-on-surface` | `rgb(59 65 77)` | `rgb(223 227 231)` | Primary text |
| `--mat-sys-surface-variant` | `rgb(221 227 234)` | `rgb(65 72 77)` | Muted surfaces |
| `--mat-sys-outline` | `rgb(113 120 126)` | `rgb(139 145 152)` | Borders |
| `--mat-sys-outline-variant` | `hwb(0 93% 7%)` | `rgb(54 55 55)` | Subtle borders/dividers |
| `--mat-sys-surface-dim` | `rgb(215 218 223)` | `rgb(139 145 152)` | Dimmed surfaces (also main table header bg in `app-table`) |
| `--mat-sys-surface-container` | `rgb(235 238 243)` | `rgb(28 32 36)` | Nested containers |
| `--mat-sys-surface-container-high` | `rgb(229 232 237)` | `rgb(38 42 46)` | Elevated containers |
| `--mat-sys-surface-container-highest` | `rgb(223 227 231)` | `rgb(49 53 57)` | Highest elevation / **dark-mode table header bg** |
| `--mat-table-hover-shadow` | `#e5e7eb` | `rgb(49 49 49)` | Row hover background |

Two **non-standard, app-added** tokens extend the system: `--mat-sys-on-hover` (`#6f42c1` / `#d3bcfd`) and `--mat-sys-chatbot-primary` (`#0b73a1` both modes) — these are legitimate extensions of the token system, follow the same naming convention if adding more.

### 3.2 Status colors — currently inconsistent

Neither module reads status colors from tokens; both hardcode Tailwind palette hex values ad hoc:

```ts
// dashboard/.../vehicle-overview.component.ts — getTextStyle()
case 'Running':        return { color: '#22c55e' }; // green-500
case 'Idle':            return { color: '#eab308' }; // yellow-500
case 'Stop':             return { color: '#ef4444' }; // red-500
case 'No Data':          return { color: '#a855f7' }; // purple-500
case 'Expired Device':   return { color: '#06b6d4' }; // cyan-500
```

Units module action icons follow a looser but similar convention: green (Excel export), yellow (CSV/edit), red (PDF/delete), blue (print/search/settings), cyan (copy/detail). **There is no shared "status color" service/token set** — every component invents its own mapping. See §15 for a recommendation.

### 3.3 Toast semantic colors (shared, reusable reference)

```scss
.toast.success { background:#eef1ee; border:1px solid oklch(72.3% 0.219 149.579); color:oklch(72.3% 0.219 149.579); }
.toast.error   { background:rgb(255,241,241); border:1px solid rgb(244,71,71); color:red; }
.toast.info    { background:oklch(97% 0.014 254.604); border:1px solid oklch(62.3% 0.214 259.815); color:oklch(62.3% 0.214 259.815); }
.toast.warn    { background:#fcf1e2; border:1px solid oklch(79.5% 0.184 86.047); color:oklch(79.5% 0.184 86.047); }
```

This is the closest thing to a semantic success/error/warning/info palette in the codebase — if a shared status-color utility is introduced, base it on these values for consistency with the toast system users already see.

### 3.4 Dark-mode application patterns

Two idioms coexist:

1. **`[ngClass]` ternary/object driven by a subscribed boolean** (dominant in both modules):
   ```html
   [ngClass]="darkMode ? 'bg-(--mat-sys-surface-container-highest)!' : 'bg-white!'"
   ```
2. **Tailwind `dark:` variant classes** (occasional, e.g. some Dashboard skeleton loaders: `bg-gray-200 dark:bg-gray-700`) — inconsistent with pattern 1 appearing in the *same file*.
3. A **third idiom**, calling `commonService.getDarkMode()` synchronously inline per-binding instead of a bound field — found in some Dashboard widgets (vertical-card, horizontal-table-card, driver-performance-card).

**Recommendation:** standardize on pattern 1 (subscribed `darkMode: boolean` field + `[ngClass]`) since it's the majority pattern in both analyzed modules; treat 2 and 3 as tech debt, not a model to copy.

---

## 4. Spacing System

No formal spacing scale/tokens exist — both modules use Tailwind's default spacing scale directly in templates. Observed conventions:

| Context | Value(s) observed |
|---|---|
| Page container padding | `p-1 md:px-4 md:pt-2 … pb-10` (Dashboard) / `py-2 px-4` (Units dialogs) |
| Card/widget padding | `p-3` (vertical-card), `p-2`/`p-1` (vehicle-overview), `p-2 sm:p-3 md:p-4` (driver-performance-card) |
| Card internal micro-gap | `gap-y-0.5`, `mb-0.5` between header and body — a **repeated, deliberate** micro-spacing convention in Dashboard widgets |
| Form grid | `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-2 gap-x-4 p-4` — reused near-verbatim across general-settings, icon-settings, insurance-settings |
| Table cell padding | Forced globally: `td { padding-block: 6px !important; }` (Units); `px-3 py-2` header/rows (Dashboard tabular) |
| Button row | `flex justify-end py-2 px-4 gap-2/gap-3` |
| Gridster grid | `margin: 16`, `outerMarginLeft:1, outerMarginRight:2, outerMarginTop:2, outerMarginBottom:10`, `fixedRowHeight: 360` |
| Table scroll container height | `max-h-[52vh]` / `max-h-[45vh]` / `max-h-[43vh]` / `max-h-[62vh] lg:max-h-[64vh]` — **varies per component with no shared constant** |
| Header row height (global override) | `--mat-table-header-container-height: 35px`, `--mat-table-row-item-container-height: 35px` |

**Recommendation:** the per-component `max-h-[NNvh]` table-height values should become a small set of named Tailwind-safe constants (e.g. via a shared class) rather than each new table inventing its own vh value — see §15.

---

## 5. Component Design Guidelines

### 5.1 Cards

- **Units module:** no card concept — content sits directly in dialog/page containers.
- **Dashboard module:** cards are **plain `<div>`s styled with Tailwind**, not `MatCardModule` (`mat-card` selector does not appear anywhere in the module). Typical shell: `rounded shadow bg-(--mat-sys-surface-bright)`.
- **Shared `app-table`:** wraps its own container in Material's `mat-elevation-z8` class rather than a custom shadow.

**Standard for new work:** follow the Dashboard convention (custom `<div>` + Tailwind + `--mat-sys-surface*` tokens) for widget-style cards; do not introduce `MatCardModule` into either module without a deliberate migration decision, since neither currently uses it.

### 5.2 Tables

See dedicated §7 — this is the single most detailed, most inconsistent, most important area to get right for new modules.

### 5.3 Forms

- Reactive forms only, `FormBuilder`/`FormGroup`/`FormControl` — confirmed, no template-driven forms anywhere in Units.
- `mat-form-field` appearance is **inconsistent**: `appearance="fill"` in most settings dialogs, `appearance="outline"` on the main list's unit-type filter. **Pick `outline` for new work** — it's the CLAUDE.md-documented default and matches the majority convention documented in `theme-standards.md`.
- Read-only fields: `this.fb.control({ value: '', disabled: true })`.
- Scroll-wheel guard on numeric inputs: `(wheel)="preventWheelChange($event)"` calling `event.preventDefault()` — copy this for any new numeric `matInput`.
- Validation is **implicit** (`[disabled]="form.invalid"` on submit) rather than inline `mat-error` messages in most places — CLAUDE.md/coding-standards.md calls for `mat-error`; new forms should prefer explicit inline errors over silently-disabled submit buttons.

### 5.4 Buttons

- Units/Dashboard: plain `<button>`/`<div>` + Tailwind, not `MatButtonModule`, for icon-style actions (export/print/settings icons).
- Global density override in `styles.scss` forces Material buttons to ~30px height app-wide — any `mat-raised-button`/`mat-stroked-button` used elsewhere in the app already renders compact; no extra sizing classes needed.

### 5.5 Dropdowns / Select

- `mat-select` inside `mat-form-field`, `multiple` + a synthetic "Select All" `mat-checkbox` row injected above the `mat-option` loop (Units unit-type filter) — a reusable pattern for any future multi-select filter.

### 5.6 Search fields

- **Not** wrapped in `mat-form-field` in either module's primary search — both the Units list search and the shared `app-table` search are plain bordered `<div>` + `<input>` combos with a `mat-icon` search trigger. This is a deliberate, repeated pattern; replicate it rather than introducing `mat-form-field` search inputs for consistency with existing tables.

### 5.7 Date pickers

- `mat-datepicker` + `mat-datepicker-toggle`, with custom `mat-datepicker-actions` (Cancel/Clear/Apply), `[matDatepickerFilter]`, and a `(keypress)` guard restricting manual typing to digits/slashes (insurance-settings). This is the fullest date-picker implementation found — use it as the template for future date fields.

### 5.8 Tabs

- **Real `mat-tab-group`/`mat-tab`** is used in exactly one place: `sensors-settings.component.html` (Standard vs Custom sensors).
- Everywhere else that "looks like tabs" (`vehicle-settings` step shell, `service-settings`, Dashboard's top breadcrumb tabs) is a **hand-rolled** `@for`/`@switch` + button-row implementation, not Material tabs. **This is a real inconsistency** — pick one approach for new multi-section UIs (recommend real `mat-tab-group` for simplicity/accessibility unless a custom stepper-with-progress-circle is specifically needed).

### 5.9 Dialogs

Verbatim reusable config for viewport-relative top-level dialogs (Units module, `vehicle.component.ts`):

```ts
this.dialog.open(VehicleSettingsComponent, {
  width: '70vw', height: '75vh', maxWidth: 'none',
  position: { top: '5vh' }, autoFocus: false,
  backdropClass: 'bg-gray-200/60', disableClose: true,
  enterAnimationDuration: '300ms', exitAnimationDuration: '200ms',
  data: { title: `Unit Settings (${name})`, unit, permissionData },
});
```

Nested/child dialogs use smaller fixed widths (`width: '60vw', maxWidth: '95vw'` or `width: '350px'` for confirmations) and typically **omit** `backdropClass`/animation durations (Material defaults apply). Dashboard's `DetailDialogComponent` reuses the same `bg-gray-200/60` backdrop + 300/200ms animation convention. **Standard:** `disableClose: true` always; set `backdropClass: 'bg-gray-200/60'` + explicit animation durations only on the top-level dialog of a flow, let nested dialogs inherit defaults.

### 5.10 Tooltips

`matTooltip` inline attribute everywhere (both modules), often paired with `matTooltipPosition="above"` for row-level icon actions. No wrapper component. Tooltip theming is global (`mat.tooltip-overrides()`: `var(--mat-sys-primary)` background, white 10px text).

### 5.11 Icons

`MatIconModule`/`mat-icon` is near-universal for iconography in both modules; Dashboard's `Command` column and a few export buttons use raw inline `<svg>` instead (with `matTooltip` attached the same way) — acceptable when a Material ligature icon doesn't exist for the concept, but prefer `mat-icon` first.

### 5.12 Badges / Chips

No `MatChipsModule`/badge component found in either module. Dashboard's driver-performance "score" is a styled `<span>`, not a chip. **If a badge/chip pattern is needed for a new module, there is no existing convention to reuse from Dashboard/Units — design it fresh, informed by the toast color palette in §3.3.**

### 5.13 Pagination

- `mat-paginator` used in both modules for real Material tables: `[pageSizeOptions]="[5,10,20]"` (shared `app-table`), `[pageSizeOptions]="[5,10]"` (settings sub-tables), `[pageSizeOptions]="[5,10,20,50]"` (Units main list).
- `showFirstLastButtons` set consistently.
- **Recommendation:** normalize on one `pageSizeOptions` array for all new tables — `[5, 10, 20, 50]` (the Units main-list superset) is the most complete and a safe default.

### 5.14 Loaders / Empty states

- **Loader:** no dedicated spinner component; `SpinnerService` (`start()`/`stop()`, a `Subject`+signal pair) drives a single global full-viewport overlay in `app.component.html`:
  ```html
  <div class="w-[100vw] h-[100vh] bg-white/65 opacity absolute z-100000000 flex justify-center items-center">
    <mat-spinner [diameter]="70" [strokeWidth]="7" color="primary"></mat-spinner>
  </div>
  ```
  Dashboard widgets additionally use **local** `animate-pulse` Tailwind skeleton blocks (and a custom `.shimmer` CSS class) for in-place loading — this is the preferred pattern for widget-level (not full-page) loading, and should be used over ad hoc spinners for new card/table components.
- **Empty state:** no dedicated component; both modules use a literal centered text row — `*matNoDataRow` → `"No data available"` (Units tables) or plain `"No data found"` text (Dashboard). Keep this simple text-row convention for new tables; do not introduce a heavier "empty state illustration" component without a design decision.

### 5.15 Charts

Two chart systems, deliberately layered:
- `ngx-echarts` registered at the page level via `provideEchartsCore` with tree-shaken modules (`BarChart, LineChart, PieChart, ScatterChart`, `TitleComponent, TooltipComponent, GridComponent, LegendComponent`, `CanvasRenderer`).
- Actual rendering goes through a **custom wrapper**, `ChartComponent` (`app-chart`), which calls `echarts.init(el)` manually + a `ResizeObserver` — not the `<div echarts>` directive. A second wrapper, `DoughnutChartComponent` (`app-doughnut-chart`), handles donut/percentage widgets.
- **For any new chart, use `app-chart`/`app-doughnut-chart`, not the raw `ngx-echarts` directive** — the wrapper already handles the core module registration and resize logic.

### 5.16 Filters

- Units: `mat-select multiple` + synthetic "Select All" checkbox (§5.5).
- Dashboard: date-range button group and a "Control Room" toggle button, both plain Tailwind buttons — no `mat-button-toggle-group` usage found.

### 5.17 Sidebars / Headers / Breadcrumbs

- Sidebar/Navbar are explicitly **layout-level, do-not-modify-per-page** components (`shared/components/sidebar`, `shared/components/navbar`) per existing `theme-standards.md` — neither Dashboard nor Units modules touch them directly.
- Dashboard's page header is a **custom breadcrumb-style tab row** (`@for` over `tabs`, `routerLinkActive`, `text-base font-medium`), not `MatTabsModule` and not a literal `<nav>`/breadcrumb component.
- No conventional "breadcrumb trail" (Home > Section > Page) component exists in either module.

---

## 6. Angular Material Implementation Patterns

### 6.1 Modules actually imported, by module

| Material module | Units | Dashboard |
|---|---|---|
| `MatTableModule` | ✅ (main list + every settings sub-table) | ✅ (tabular page only — graphical widgets use raw `<table>`) |
| `MatSortModule` | ✅ | ✅ (tabular page only) |
| `MatPaginatorModule` | ✅ | ✅ (tabular page only) |
| `MatFormFieldModule` / `MatInputModule` | ✅ | ✅ (tabular page only) |
| `MatSelectModule` / `MatOption` | ✅ | — |
| `MatCheckbox` | ✅ | — |
| `MatDialogModule` (service) | ✅ | ✅ |
| `MatDatepickerModule` | ✅ | — |
| `MatRadioModule` | ✅ | — |
| `MatSidenavModule` | ✅ (sensor detail drawer) | — |
| `MatTabsModule` | ✅ (real usage: sensors only; imported-but-hand-rolled elsewhere) | Imported at dashboard.component level but tab UI is hand-rolled |
| `MatIconModule` | ✅ | ✅ |
| `MatTooltipModule` | ✅ | ✅ |
| `MatProgressSpinnerModule` | ✅ | — (uses Tailwind `animate-spin`/`animate-pulse` instead) |
| `MatButtonModule` | ✅ | ❌ not found — plain buttons only |
| `MatCardModule` | ❌ | ❌ not found in either module |
| `MatMenuModule` | ❌ | ❌ — Dashboard "dropdown menus" are custom `group-hover` CSS reveals |
| `MatChipsModule` | ❌ | ❌ |
| `MatSlideToggle` | ❌ | ❌ |

### 6.2 Wrapper components / custom directives

- `app-chart` / `app-doughnut-chart` — ECharts wrappers (§5.15).
- `app-table` — generic Material table wrapper (full breakdown in §7.2).
- No custom directives were found wrapping Material behavior (e.g. no custom `matTooltip` extension, no custom form-field appearance directive).

### 6.3 Standard per-component setup

Every component in both modules declares its own `imports: [...]` array (standalone) — confirmed, no shared "MaterialModule" barrel re-export exists or should be introduced (this matches `coding-standards.md`'s explicit rule).

---

## 7. Table Implementation Standards

This is the most-repeated, most-inconsistent pattern across both modules — treat this section as the canonical reference for any new table.

### 7.1 Two competing "raw Material table" conventions (Units module)

**A — Main list (`vehicle.component.ts`/`.html`), dynamic per-column-name branching:**

```html
@for (column of columns; track $index) {
  @if (column == "settings") {
    <ng-container [matColumnDef]="column"> … icon-only settings column … </ng-container>
  } @else if (column == "Command") {
    <ng-container [matColumnDef]="column"> … inline SVG action column … </ng-container>
  } @else if (column == "Units") {
    <ng-container [matColumnDef]="column" sticky> … sticky, fixed-width, image+text … </ng-container>
  } @else if (column == "Sr No") {
    <ng-container [matColumnDef]="column" sticky> … sticky index column … </ng-container>
  } @else {
    <ng-container [matColumnDef]="column"> … default sortable text column … </ng-container>
  }
}
```

```ts
dataSource!: MatTableDataSource<any>;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
initializeDataSource(): void {
  this.dataSource = new MatTableDataSource(this.tableData || []);
  this.dataSource.paginator = this.paginator;
  this.dataSource.sort = this.sort;
}
```

**B — Settings sub-tables**, one static `ng-container` per named column (no loop), otherwise identical `MatTableDataSource`/`MatSort`/`MatPaginator` wiring. Multi-table components (e.g. `sensors-settings`) use per-table named `#paginator` refs.

### 7.2 The shared `app-table` component — a *third*, signal-based convention

`src/app/shared/components/table/` implements yet another pattern, generic over `<T>`:

```ts
@Input() columns: string[] = [];
@Input() displayedColumnsWithSelect: string[] = [];
@Input() data: T[] = [];
@Input() title: string = '';
@Output() selectionChange = new EventEmitter<T[]>();

selection = new SelectionModel<T>(true, []);
initializeDataSource(): void {
  this.dataSource = new MatTableDataSource(this.data);
  // wrapped in setTimeout() so @ViewChild(MatPaginator)/@ViewChild(MatSort) resolve first
  setTimeout(() => {
    this.dataSource.paginator = this.paginator;
    this.dataSource.sort = this.sort;
  });
}
```

- Headers are **auto-generated** from `columns` via `{{ column | titlecase }}`, with `mat-sort-header` unconditionally on every column (no per-column sortable opt-out — a real limitation vs. the main Units list, which explicitly excludes `Sr No`/`settings`/`Command` from sorting).
- Built-in multi-select checkbox column (`matColumnDef="select"`), "select all" respects `dataSource.filteredData` (search-aware).
- No sticky-column support, no custom cell template hookup (a `TemplateRef` import exists but is dead/unused), no per-column special-casing.
- Search is a plain `<input>` (not `mat-form-field`), same idiom as the Units main list.

**When to use which:** `app-table` is the right choice for a simple, uniform-column list with optional multi-select and no special columns. Reach for the manual `@if`/`@else if` per-column pattern (convention A) only when you need sticky columns, mixed icon/image/action cells, or per-column sortable exclusion — which today only the Units main list needs.

### 7.3 Universal cross-module conventions (apply these to any new table regardless of which pattern you pick)

**Truncation + null-safety (`na` + `truncate` pipes) — apply to every free-text cell:**

```html
[title]="element[column]?.length > 20 ? element[column] : ''"
@if (element[column]?.length > 20) {
  {{ element[column] | truncate: 20 | na }}
} @else {
  {{ element[column] | na }}
}
```

**Empty state (`*matNoDataRow`) — identical shape everywhere, only the message text changes:**

```html
<tr class="text-center" *matNoDataRow>
  <td class="py-4 text-gray-500 text-center" [attr.colspan]="displayedColumns.length">
    No data available
  </td>
</tr>
```

**Search/filter:**

```ts
applyFilter(event: Event): void {
  const filterValue = (event.target as HTMLInputElement).value;
  this.search = filterValue.trim().toLowerCase();
  this.dataSource.filter = this.search;
}
```

**Header/body typography and color** — resolve the two competing pairs from §2.2 to a **single standard for new tables**:

```scss
// Standardize on this pair for any NEW table (matches the majority — settings sub-tables — convention):
.table-head { font-size: smaller; font-weight: bold; }
.table-data { font-size: smaller; }
```

```html
<!-- Header background/text — theme-token driven, works in both themes: -->
<th [ngClass]="{
  'bg-(--mat-sys-primary)! text-white!': !darkMode,
  'bg-(--mat-sys-surface-container-highest)!': darkMode
}">
```

**Row/border styling (global, already applied via `::ng-deep` in both modules — reuse, don't reinvent):**

```scss
::ng-deep .mat-mdc-header-row {
  position: sticky !important; top: 0 !important; z-index: 10;
  color: var(--mat-sys-on-surface-variant) !important;
  background-color: var(--mat-sys-outline-variant) !important;
}
::ng-deep table tr.mat-mdc-row:hover { background-color: var(--mat-table-hover-shadow) !important; }
.light-theme::ng-deep .mdc-data-table__cell,
.light-theme::ng-deep .mdc-data-table__header-cell { border-bottom: 0.5px solid #e0e0e0 !important; }
.dark-theme::ng-deep .mdc-data-table__cell { border-bottom: 0.5px solid #8a8a8a !important; }
```

**Row height (global, do not override per-table):**

```scss
.mat-mdc-header-row { height: var(--mat-table-header-container-height, 35px) !important; }
.mat-mdc-row { height: var(--mat-table-row-item-container-height, 35px) !important; }
```

### 7.4 Export/print

Units main list delegates to a shared `ExportService` (`exportToExcel()`, `exportToCSV()`, `exportToPDF()`, `printTable()`, `copyTable()`), always pre-filtering out non-data columns (`settings`, `Command`, `Icons`) before passing `dataSource.data` through. **Reuse `ExportService` for any new exportable table** rather than writing bespoke export logic.

---

## 8. Folder Structure

Confirmed conventions (matches `.claude/docs/architecture.md`, cross-checked against both modules directly):

```
src/app/panels/dashboard/
├── dashboard.component.{ts,html,css}
└── child/
    ├── control-room/
    ├── dashboard-graphical-page/
    │   ├── dashboard-graphical-page.component.{ts,html,css}
    │   └── child/                        ← 11 widget types, one folder each
    │       ├── ai-insights/
    │       ├── configuration-panel/
    │       ├── detail-dialog/
    │       ├── driver-performance-card/
    │       ├── horizontal-graph-card/     ← STUB, unimplemented (flag, don't copy)
    │       ├── horizontal-table-card/
    │       ├── mdvr-live-card/
    │       ├── trash/
    │       ├── vehicle-health-card/
    │       ├── vehicle-overview/
    │       └── vertical-card/
    │           └── charts/ (chart/, doughnut-chart/)
    └── dashboard-tabular-page/

src/app/panels/vehicle/
├── vehicle.component.{ts,html,scss}       ← "Unit List" page
└── child/
    ├── custom-command-settings/
    └── vehicle-settings/
        ├── vehicle-settings.component.{ts,html,scss}
        └── child/
            ├── general-settings/
            ├── icon-settings/
            ├── alert-settings/
            ├── sensors-settings/
            │   └── child/form/
            └── service-settings/
                ├── service-settings.component.{ts,html,scss}
                └── child/
                    ├── profile-settings/
                    ├── fitness-settings/
                    ├── insurance-settings/
                    ├── pollution-settings/
                    └── service-history-settings/
```

Rules confirmed by direct inspection:

- Kebab-case folders and files throughout, no exceptions found.
- Each component folder holds exactly 4 files: `.component.ts`, `.component.html`, `.component.{scss|css}`, `.component.spec.ts`.
- **Extension inconsistency:** top-level Dashboard files (`dashboard.component.css`, `dashboard-graphical-page.component.css`) use `.css`; nested widgets (`vertical-card`, `ai-insights`, `driver-performance-card`) use `.scss`. **New components should use `.scss`** — it's the majority convention and matches CLAUDE.md.
- `standalone: true` is set explicitly in older files; newer files (`driver-performance-card`, `ai-insights`) omit it, relying on Angular 19's standalone-by-default. Both are valid; explicit is slightly more self-documenting for readers unfamiliar with the default.
- `child/` is the universal nesting convention for parent-owned sub-components at any depth (confirmed 3 levels deep under `dashboard-graphical-page/child/vertical-card/charts/`).

---

## 9. Component Architecture

Matches `coding-standards.md` exactly — confirmed by direct inspection of both modules:

- Standalone components, no NgModules.
- Each component declares its full `imports: []` array — no shared "Material barrel module."
- Templates/styles always in separate files, never `template`/`styles` inline.
- `@ViewChild(MatPaginator)` / `@ViewChild(MatSort)` for table wiring — consistent across every table found.
- Dark mode threaded as `@Input() darkMode = false` down the component tree from a top-level subscriber of `CommonService.darkMode$` (Units settings dialog tree is the clearest example — `vehicle.component` → `vehicle-settings` → `general-settings`/`icon-settings`/`sensors-settings`/etc., all receiving `darkMode` as an `@Input`).
- **Subscription cleanup inconsistency found:** `vehicle.component.ts` and `custom-command-settings.component.ts` use a plain unsubscribed `.subscribe()` on `darkMode$`; `vehicle-settings.component.ts` correctly uses `takeUntilDestroyed(this.destroyRef)`. **New components must use `takeUntilDestroyed` or the `destroy$` Subject pattern** — never a bare `.subscribe()` on a long-lived observable.

---

## 10. SCSS Architecture

- **No BEM, no component-scoped design-token files** in Dashboard/Units — styling leans on (a) Tailwind utility classes directly in templates, and (b) a handful of global CSS custom properties (`--mat-sys-*`) defined once in `styles.scss`.
- `ViewEncapsulation` is left at Angular's default (Emulated) everywhere inspected — no `ViewEncapsulation.None` found in these two modules (unlike the CAN module's shared table, which does use `.None` — do not carry that over here without reason).
- `::ng-deep` is used, always to override Material internals (`.mdc-data-table__cell`, `.mat-mdc-header-row`, `.mat-mdc-paginator-container`) — matches the documented rule in `theme-standards.md` ("no deep selectors unless overriding Material internals with no alternative"). Not consistently wrapped in `:host` in the files inspected — **new `::ng-deep` usage should wrap in `:host` per the documented standard**, even though existing code doesn't always do so.
- Global density/typography overrides live in `styles.scss` (compact checkboxes/radios/buttons, forced `font-size: 12px` on Material internals) — **do not re-override these per-component**; they're already global.
- Scrollbar styling is a repeated, copy-pasted `::-webkit-scrollbar` block (`width/height`, track = `var(--mat-sys-surface)`, thumb = `var(--mat-sys-outline-variant)`, `border-radius: 8px`) — appears in `styles.scss` globally AND re-declared locally in Dashboard's `vertical-card.scss`. Prefer relying on the global rule; only re-declare if a component needs a genuinely different scrollbar treatment.
- Tailwind: `src/tailwind.css` is a single `@import "tailwindcss";` — **no custom theme/color/spacing/breakpoint extension exists in the repo.** Any "custom color" used in a template (e.g. `#0b73a1`, status hexes) is a raw arbitrary-value Tailwind class or inline style, not a configured theme color.

---

## 11. Coding Conventions

Confirmed identical to `.claude/docs/coding-standards.md` by direct inspection — highlights specific to what was actually observed in Dashboard/Units:

| Convention | Confirmed pattern |
|---|---|
| Component file | `kebab-case.component.ts` |
| Component class | `PascalCase` + `Component` |
| Selector | `app-` + kebab-case |
| Interface | `I` prefix, e.g. `IVehicle`, `IVehicleApiResponse` |
| Reactive forms | `FormBuilder`/`FormGroup`/`FormControl`, no template-driven forms found |
| RxJS cleanup | `takeUntilDestroyed(this.destroyRef)` (preferred, per `vehicle-settings.component.ts`) or `destroy$` Subject — **not** bare `.subscribe()` |
| Services | `providedIn: 'root'`, `inject()` not constructor injection, in `core/services/<feature>/` |
| API response shape | `{ status: boolean; message: string; data: T }` — confirmed in `IVehicleApiResponse` |
| Error handling | `ToastService.error()` in the component's `subscribe.error`, `SpinnerService` hidden via `finalize()` |
| Permission checks | `PermissionService.userType()`, `.getUnitPermission(serviceId)` — gates the Unit Settings dialog's data before opening |

---

## 12. Reusable Component Patterns (ready to copy for new modules)

1. **`app-table`** (`shared/components/table/`) — default choice for any new simple list with optional multi-select; see §7.2 for its exact `@Input`/`@Output` contract.
2. **`app-chart` / `app-doughnut-chart`** — always go through these instead of the raw `ngx-echarts` directive (§5.15).
3. **`ConfirmDialogComponent`** — `IConfirmDialogData { title, message, confirmText?, cancelText?, color?, icon? }`, opened at `width: '380px'`–`'350px'`.
4. **`ExportService`** — `.exportToExcel()/.exportToCSV()/.exportToPDF()/.printTable()/.copyTable()`, always column-filtered before passing data through (§7.4).
5. **`ToastService`** — `.success()/.error()/.warning()/.info()`, backed by the color palette in §3.3.
6. **`SpinnerService`** — global full-page overlay; use Tailwind `animate-pulse`/`animate-spin` skeletons instead for widget-local loading (§5.14).
7. **Truncate + `na` pipe combo** (§7.3) — apply to every free-text table cell without exception.
8. **Viewport-relative `MatDialog` config** (§5.9) — copy verbatim for any new full-screen-ish settings dialog.
9. **File-upload pattern** (hidden `<input type="file">` + `matSuffix` trigger + filename pill with remove icon) — from `insurance-settings`, reuse for any new document-upload field.
10. **Numeric-input wheel guard** (`(wheel)="preventWheelChange($event)"`) — apply to every `matInput type="number"`.

---

## 13. Best Practices Already Followed (keep doing these)

- Standalone components, per-component `imports[]`, no shared Material barrel.
- `I`-prefixed model interfaces mirroring API snake_case shape.
- Reactive forms exclusively; disabled read-only fields via `{ value, disabled: true }` controls rather than template `[disabled]` binding alone.
- `finalize()` always hides the spinner regardless of success/error.
- Consistent `MatTableDataSource` + `MatSort` + `MatPaginator` wiring pattern across every table implementation, even where the surrounding markup differs.
- Global, token-driven dark-mode support for Material internals (table borders, hover shadow, header background) — new dark-mode work should extend the `--mat-sys-*` token set rather than hardcoding a second color per theme.
- `ExportService`/`ToastService`/`SpinnerService`/`PermissionService` centralization — no component reimplements these.

## 13a. Inconsistencies Found (do not propagate into new modules)

- **Two typography pairs for tables** (`.text-head`/`.text-body` vs `.table-head`/`.table-data`) — resolve to the latter for new work (§7.3).
- **Three dark-mode idioms** in the same codebase (subscribed boolean + `[ngClass]`, Tailwind `dark:` variant, synchronous `getDarkMode()` getter call) — standardize on the first.
- **`mat-form-field` appearance** varies (`fill` vs `outline`) with no clear rule — standardize on `outline`.
- **"Tabs" that aren't `mat-tab-group`** (`vehicle-settings` stepper, `service-settings`) vs. the one place that does use real tabs (`sensors-settings`) — prefer real `mat-tab-group` for new multi-section UI unless a custom stepper is a deliberate design choice.
- **Bare `.subscribe()` without cleanup** on `darkMode$` in `vehicle.component.ts`/`custom-command-settings.component.ts` — always use `takeUntilDestroyed`/`destroy$` (§9).
- **Font loaded twice** (self-hosted `@font-face` + Google Fonts `<link>`) — pick one.
- **`.css` vs `.scss` extension** split at the top of the Dashboard module — use `.scss` going forward.
- **A stub component** (`horizontal-graph-card`) sitting alongside 10 fully-implemented sibling widgets — either finish or remove before treating the widget set as a complete reference.
- **Per-table hardcoded `max-h-[NNvh]`** scroll heights with no shared constant (§4).
- **No shared status-color token/service** — every component invents its own status→color mapping (§3.2).

---

## 14. UI Consistency Checklist (use before merging a new page/widget)

- [ ] Uses `--mat-sys-*` tokens (or Tailwind `bg-(--mat-sys-*)` syntax) for all surface/text/border colors — no new hardcoded hex except for status colors, and even those should eventually map to a shared status palette.
- [ ] Dark mode handled via a subscribed `darkMode: boolean` field + `[ngClass]`, cleaned up with `takeUntilDestroyed`/`destroy$` — not a bare `.subscribe()`, not a synchronous getter call, not Tailwind `dark:` variants mixed into the same file.
- [ ] Any new table uses `app-table` if it's a simple list, or the manual `@if`/`@else if` per-column pattern only if sticky/special columns are genuinely needed — and either way applies the `na`/`truncate` pipe combo, the standard empty-state row, and the `.table-head`/`.table-data` typography pair (§7).
- [ ] Any new dialog sets `disableClose: true`; top-level dialogs get `backdropClass: 'bg-gray-200/60'` + 300ms/200ms enter/exit animations.
- [ ] `mat-form-field appearance="outline"` on all new form fields.
- [ ] Numeric inputs have the wheel-scroll guard.
- [ ] New charts go through `app-chart`/`app-doughnut-chart`, never the raw `ngx-echarts` directive.
- [ ] Component folder has exactly `.ts` + `.html` + `.scss` (not `.css`) + `.spec.ts`, kebab-case, standalone, own `imports[]`.
- [ ] Any status/semantic color introduced is checked against the toast palette (§3.3) for consistency before inventing a new hex value.
- [ ] File is not a stub — no `<p>x works!</p>` boilerplate left behind once a widget is "done".

---

## 15. Recommendations for Future Development

1. **Pick one table convention and document it as the default** — recommend `app-table` for anything without sticky/special columns; extend it (optional per-column `sortable: false`, optional sticky-column input, optional custom cell `TemplateRef`) rather than continuing to hand-roll a fourth table pattern per new page.
2. **Introduce a shared status-color mapping** (e.g. a small `StatusColorService` or a set of Tailwind-safe utility classes) seeded from the values already used in `vehicle-overview.component.ts` and the toast palette, so "Running/Idle/Stop/Warning/Error" always render the same color everywhere instead of being redefined per component.
3. **Consolidate the two dark-mode-table-header snippets and the two table-typography pairs** into one shared SCSS partial (or a couple of utility classes) that every new table `@use`s / applies, instead of copy-pasting the same 6-line `[ngClass]` map and `.table-head`/`.table-data` rule into every new component's `.scss`.
4. **Decide on real `mat-tab-group` vs. custom stepper** as the standard for multi-section settings UIs, and migrate the odd one out rather than letting both patterns keep spreading.
5. **Remove the duplicate font loading** (keep either the self-hosted `@font-face` set or the Google Fonts `<link>`, not both).
6. **Finish or delete the `horizontal-graph-card` stub** before using the Dashboard widget folder as a "reference implementation" set for new widgets.
7. **Add a small shared "table shell" height constant** (e.g. three named sizes: compact/standard/tall) instead of every table choosing its own `max-h-[NNvh]` value.
8. **When adding new Tailwind usage, consider whether it belongs in a real theme extension** (`tailwind.config`) rather than continuing to grow ad hoc arbitrary-value classes — there is currently no config to extend, which is fine at the current scale but will not scale indefinitely.
9. **Enforce `takeUntilDestroyed`/`destroy$` via lint rule if possible** — the one bare `.subscribe()` found on a long-lived observable is a real (if minor) memory-leak risk pattern that's easy to reintroduce without automated enforcement.
