npm package: @scania-nl/tegel-angular-extensions
Description: ![npm version](https://img.shields.io/npm/v/@scania-nl/tegel-angular-extensions) ![angular](https://img.shields.io/badge/angular-19%2B-DD0031?logo=angular) ![typescript](https://img.shields.io/badge/typescript-5%2B-3178C6?logo=typescript) ![zod](https://i
Author: Patrick Groot Koerkamp
Latest version: 0.0.9

![npm version](https://img.shields.io/npm/v/@scania-nl/tegel-angular-extensions)
![angular](https://img.shields.io/badge/angular-19%2B-DD0031?logo=angular)
![typescript](https://img.shields.io/badge/typescript-5%2B-3178C6?logo=typescript)
![zod](https://img.shields.io/badge/zod-schema-3066BE)

# @scania-nl/tegel-angular-extensions

Angular components, services, and directives for working with the [Tegel Angular 17](https://www.npmjs.com/package/@scania/tegel-angular-17) component library.
Provides simple wrappers for toast and modal functionality using Angular 19+ **standalone components** and **dependency injection configuration**.

---

&nbsp;

## Features

1. **Runtime environment configuration**
   - Schema-driven configuration and validation using Zod
   - Deep-partial runtime overrides
   - Runtime-required key enforcements
   - Strong TypeScript inference
2. **Toast management**
   - Signal-based `ToastService` for displaying toasts
   - Customizable toast appearance and behavior
3. **Modal management**
   - Strongly typed modal API
   - Dynamic body content (string, template, or component)
   - Configurable actions and lifecycle callbacks
4. **Standalone components and directives**
   - Drop-in UI integrations for Tegel Angular
5. **Default Nginx config**

- Zero boilerplate - no Angular modules required
- Fully typed and configurable via DI
- Built for Angular 19+ standalone architecture

---

&nbsp;

# Table of Contents

1. [Installation](#installation)
2. [Environment Configuration Overview](#environment-configuration-overview)
   1. [Defining your schema with createEnvKit](#defining-your-schema-with-createenvkit)
   2. [Defining Static Environments (Dev/Prod)](#defining-static-environments-devprod)
   3. [Providing Runtime Configuration](#providing-runtime-configuration)
   4. [Using the ENV_CONFIG Token](#using-the-env_config-token)
   5. [Runtime Configuration Binaries](#runtime-configuration-binaries)
      1. [`docker-entrypoint.sh`](#docker-entrypointsh)
      2. [`extract-env-vars.sh`](#extract-env-varssh)
      3. [`nginx.conf`](#nginxconf)
      4. [Example Docker Commands](#example-docker-commands)
3. [Toasts](#toasts)
   1. [Quick Start](#quick-start)
      1. [Add Providers](#add-providers)
      2. [Use in components](#use-in-components)
   2. [Toast Configuration Options](#toast-configuration-options)
   3. [ToastService API](#toastservice-api)
      1. [ToastService Properties](#toastservice-properties)
      2. [ToastService Methods](#toastservice-methods)
      3. [Toast Lifecycle Hooks](#toast-lifecycle-hooks)
4. [Modals](#modals)
   1. [Quick Start](#quick-start-1)
      1. [Add Providers](#add-providers-1)
      2. [Use in components](#use-in-components-1)
   2. [Modal Configuration Options](#modal-configuration-options)
   3. [ModalService API](#modalservice-api)
      1. [ModalService Properties](#modalservice-properties)
      2. [ModalService Methods](#modalservice-methods)
      3. [Modal Lifecycle Hooks](#modal-lifecycle-hooks)
5. [Components & Directives](#components--directives)
   1. [Components](#components)
      1. [DateTime Picker](#datetime-picker-component-tae-date-time-picker)
      1. [Footer](#footer-component-tae-footer)
   2. [Directives](#directives)
      1. [Hard Refresh](#hard-refresh-directive-taehardrefresh)
      2. [Barcode Scanner](#barcode-scanner-directive-taebarcodescanner)
6. [Appendix](#appendix)
   1. [Dockerfile Example](#runtime-config-dockerfile-example)
7. [License](#license)

---

&nbsp;

# Installation

```bash
npm install @scania-nl/tegel-angular-extensions @scania/tegel-angular-17 @traversable/zod zod
```

> Note: `@scania/tegel-angular-17`, `@traversable/zod`, and `zod` are peer dependencies and must be installed separately.

When creating an Angular project, the following dependencies already should have been installed:

```json
{
  "@angular/common": ">=19",
  "@angular/core": ">=19",
  "@angular/router": ">=19",
  "rxjs": ">=7.8.0"
}
```

---

&nbsp;

# Environment Configuration Overview

The runtime-config system provides:

- Type-safe configuration via Zod schemas
- Static environment validation
- Runtime overrides via .env files (shell scripts included)
- Guaranteed config availability before app bootstrap

&nbsp;

## Defining your schema with createEnvKit

Create a local file: `src/environments/environment-config.ts`

```ts
import { InjectionToken } from '@angular/core';
import { createEnvKit } from '@scania-nl/tegel-angular-extensions';
import { z } from 'zod';

// Create the EnvKit by defining a Zod-schema
export const EnvKit = createEnvKit({
  schema: z
    .object({
      envType: z.enum(['dev', 'preprod', 'staging', 'prod']),
      production: z.boolean(),
      apiUrl: z.url(),
    })
    .strict(),
  runtimeRequiredKeys: ['apiUrl'],
});

// Re-export for static env files
type EnvConfig = typeof EnvKit.types.EnvConfig;
export type StaticEnv = typeof EnvKit.types.StaticEnv;

// Injection Token for EnvConfig
export const ENV_CONFIG = new InjectionToken<EnvConfig>('ENV_CONFIG');
```

&nbsp;

## Defining Static Environments (Dev/Prod)

Initialize the environments using Nx:

```bash
nx g environments
```

This will create two files: `environment.ts` for the production environment configuration and `environment.development.ts` for the local development environment configuration.

`environment.development.ts`:

```ts
import { StaticEnv } from './environment-config';

export const environment: StaticEnv = {
  envType: 'dev',
  production: false,
  apiUrl: 'https://www.company.com/api/',
} satisfies StaticEnv;
```

`environment.ts`:

```ts
import { StaticEnv } from './environment-config';

export const environment: StaticEnv = {
  envType: 'prod',
  production: true,
  // apiUrl: 'https://www.company.com/api/' // apiUrl cannot be defined here
} satisfies StaticEnv;
```

&nbsp;

## Providing Runtime Configuration

Add to `app.config.ts`:

```ts
import { provideRuntimeConfig } from '@scania-nl/tegel-angular-extensions';
import { EnvKit, ENV_CONFIG } from '../environments/environment-config';
import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    // ...
    provideRuntimeConfig(EnvKit, environment, {
      envPath: '/env/runtime.env', // Default
      debug: !isDevMode(), // Defaults to false
      stopOnError: true, // Default
      token: ENV_CONFIG,
    }),
  ],
};
```

&nbsp;

## Using the ENV_CONFIG Token

```ts
import { inject } from '@angular/core';
import { ENV_CONFIG } from '../environments/environment-config';

@Injectable()
export class ApiService {
  private readonly envConfig = inject(ENV_CONFIG);

  constructor() {
    console.log('API base URL:', this.envConfig.apiUrl);
  }
}
```

&nbsp;
This setup involves several key steps:

- At application startup, the code loads `/env/runtime.env`, parses any overrides using Zod (via a deep-partial schema), and merges them with the static configuration.
- Required configuration keys are enforced **only** in production.
- The validated configuration is exposed through Angular DI using the `ENV_CONFIG` InjectionToken.
- The `environment` file is referenced directly here. Angular's build process replaces the `development` environment with the `production` environment via file replacement based on the selected build configuration.

> The `/env/runtime.env` file must be generated by the container during startup.
> Shell script binaries to support this are included the package.

&nbsp;

## Runtime Configuration Binaries

This package ships with two lightweight shell scripts used to generate a runtime configuration file inside the container. They enable true runtime configurability without rebuilding the Angular image. Additionally, the package contains a default `nginx.conf` optimized for Angular application. The files are located in the `/docker` directory.

&nbsp;

### `docker-entrypoint.sh`

- Entry point executed every time the container starts
- Calls the `extract-env-vars.sh` to generate a fresh `runtime.env`
- Lastly, executes the provided Dockerfile `CMD`

&nbsp;

### `extract-env-vars.sh`

- Reads all container environment variables matching a prefix (default: `NG__`)
- Strips the prefix and writes cleaned `KEY=VALUE` pairs to `runtime.env`
- Supports nested keys via ** (e.g., `NG**myFeature\_\_myThreshold`)
- Defaults output to `/usr/share/nginx/html/env/runtime.env`

&nbsp;

### `nginx.conf`

A default Nginx configuration optimized for Angular applications. It provides:

- Performance tuning for static file serving
- Browser caching of compiled assets
- Gzip compression where supported
- Automatic fallback to `index.html` for client-side routing

&nbsp;

### **Example Docker Commands**

```Dockerfile
# Copy the shell scripts to /usr/local/bin
COPY --from=build /app/node_modules/@scania-nl/tegel-angular-extensions/docker/*.sh /usr/local/bin/
# Ensure they shell scripts are executable
RUN chmod +x /usr/local/bin/*
# Use the shared entrypoint from the @scania-nl/tegel-angular-extensions npm package
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# Start the nginx web server in the foreground and keep it running
CMD ["nginx", "-g", "daemon off;"]
```

For a complete Dockerfile example with the shipped `nginx.conf` config, refer to [Runtime Config Dockerfile Example](#runtime-config-dockerfile-example)

---

&nbsp;

# Toasts

A lightweight, standalone toast system that integrates seamlessly with Tegel Angular. Provides configurable, signal-driven notifications for success, error, warning, and information messages.

&nbsp;

## Quick Start

### Add Providers

In your `app.config.ts`, specify the provider with `provideToast()`:

```ts
// app.config.ts
import { provideToast } from '@scania-nl/tegel-angular-extensions';

export const appConfig: ApplicationConfig = {
  providers: [
    provideToast({
      type: 'information', // Default toast type
      title: 'Notification', // Default title
      description: '', // Default description
      duration: 7500, // Auto-dismiss delay (ms)
      closeDuration: 300, // Fade-out animation duration (ms)
      closable: true, // Show a close button
    }),
  ],
};
```

> Note: The configuration is optional, all values shown above are the default settings.

&nbsp;

### Use in components

In any standalone component:

```ts
@Component({
  standalone: true,
  selector: 'my-toast-demo',
  template: `<button (click)="showToast()">Show Toast</button>`,
})
export class MyToastDemoComponent {
  private readonly toastService = inject(ToastService);

  showToast() {
    this.toastService.create({
      type: 'success',
      title: 'Hello Toast',
      description: 'Toast created successfully!',
    });
  }
}
```

---

&nbsp;

## Toast Configuration Options

You can configure the default appearance and behavior of toasts by passing a `ToastConfig` object to `provideToast()` in your `app.config.ts`.

All options are optional. Defaults will be applied if values are not provided.

| Property        | Type                                                 | Default          | Description                                                                |
| --------------- | ---------------------------------------------------- | ---------------- | -------------------------------------------------------------------------- |
| `type`          | `'information' \| 'success' \| 'warning' \| 'error'` | `'information'`  | Default toast type for `create()` calls                                    |
| `title`         | `string`                                             | `'Notification'` | Default title text for toasts                                              |
| `description`   | `string`                                             | `''`             | Default description text                                                   |
| `duration`      | `number`                                             | `7500`           | Duration (ms) before a toast auto-closes (0 = stays until manually closed) |
| `closeDuration` | `number`                                             | `300`            | Duration (ms) for fade-out animation (0 = remove instantly)                |
| `closable`      | `boolean`                                            | `true`           | Whether a close button is shown                                            |

> Note: You can override these defaults per toast when using `create()` or convenience methods like `success()`.

---

&nbsp;

## ToastService API

The `ToastService` provides a signal-based API to create, manage, and dismiss toast notifications in Angular standalone apps. It is automatically available after registering `provideToast()` in your `app.config.ts`.

&nbsp;

### ToastService Properties

| Property       | Type              | Description                                           |
| -------------- | ----------------- | ----------------------------------------------------- |
| `toasts`       | `Signal<Toast[]>` | Read-only list of all toasts (including closed)       |
| `activeToasts` | `Signal<Toast[]>` | List of currently active toasts (`Open` or `Closing`) |

&nbsp;

### ToastService Methods

#### `create(toastOptions: Partial<ToastOptions>): number`

Creates a custom toast with full control over appearance and behavior.

Example:

```ts
toastService.create({
  type: 'success',
  title: 'Saved!',
  description: 'Your changes have been saved.',
  duration: 5000,
});
```

Returns the unique toast ID.

&nbsp;

#### Convenience Methods

Creates a toast of a specific type:

```ts
toastService.success({ title: 'All good!' });
toastService.error({ title: 'Oops!', description: 'Something went wrong.' });
toastService.warning({ title: 'Heads up!' });
toastService.info({ title: 'FYI' });
```

&nbsp;

#### `getToast(id: number): Toast | undefined`

Gets a toast by its ID.

&nbsp;

#### `createRandomToast(props?: Partial<ToastOptions>): number`

Creates a random toast with random type and title. Useful for testing. Returns the toast's unique ID.

```
toastService.createRandomToast();
```

&nbsp;

#### `close(id: number): void`

Triggers the fade-out animation and schedules removal.

&nbsp;

#### `closeAll(): void`

Closes all currently open toasts.

&nbsp;

#### `remove(id: number): void`

Immediately removes a toast (no animation).

&nbsp;

#### `removeAll(): void`

Force-removes all toasts instantly (no animations).

---

&nbsp;

### Toast Lifecycle Hooks

Each toast supports optional lifecycle callbacks:

| Callback           | Description                                  |
| ------------------ | -------------------------------------------- |
| `onCreated(toast)` | Called immediately after toast is created    |
| `onClose(toast)`   | Called when toast is closed (before removal) |
| `onRemoved(toast)` | Called when toast is fully removed           |

Example:

```ts
toastService.success({
  title: 'Logged out',
  duration: 5000,
  onRemoved: (toast) => console.log(`Toast ${toast.id} removed`),
});
```

---

&nbsp;

# Modals

Standalone modal service + host for Tegel's `tds-modal`, with strongly‑typed options and dynamic body support.

&nbsp;

## Quick Start

### Add Providers

In your `app.config.ts`, specify the provider with `provideModal()`:

```ts
// app.config.ts
import { provideModal } from '@scania-nl/tegel-angular-extensions';

export const appConfig: ApplicationConfig = {
  providers: [
    provideModal({
      size: 'md', // Modal size
      actionsPosition: 'static', // Actions slot behavior
      prevent: false, // Prevent overlay click from closing the modal
      closable: true, // Show or hide the close [X] button
      alertDialog: 'dialog', // ARIA role of the modal component
      lazy: false, // Render body only while open
      startOpen: true, // Open modal on creation
      removeOnClose: true, // Remove modal from DOM when closed
    }),
  ],
};
```

Most of these options are inherited from `tds-modal`.

> Note: The configuration is optional, all values shown above are the default settings.

### Use in components

`create()` returns a `ModalRef` that can control the modal instance.

```ts
import { inject } from '@angular/core';
import { ModalService } from '@scania-nl/tegel-angular-extensions';

export class MyComponent {
  private readonly modalService = inject(ModalService);

  openModal() {
    const ref = this.modalService.create({
      header: 'Demo Modal',
      body: 'Hello from modal',
      startOpen: false,
      removeOnClose: false,
    });

    ref.open(); // Opens the modal
    ref.close(); // Closes (hides) the modal
    ref.remove(); // Removes the modal from the DOM
  }
}
```

You get a `ModalRef` back from `create()`, which you can use to control that specific modal instance (`open()`, `close()`, `remove()`).

---

&nbsp;

## Modal Configuration Options

| Option            | Type                           | Default  | Description                 |
| ----------------- | ------------------------------ | -------- | --------------------------- |
| `size`            | `'xs' \| 'sm' \| 'md' \| 'lg'` | `md`     | Modal size                  |
| `actionsPosition` | `'static' \| 'sticky'`         | `static` | Actions slot behavior       |
| `prevent`         | `boolean`                      | `false`  | Prevent overlay close       |
| `closable`        | `boolean`                      | `true`   | Show/hide close button      |
| `alertDialog`     | `'dialog' \| 'alertdialog'`    | `dialog` | ARIA role                   |
| `lazy`            | `boolean`                      | `false`  | Render body only while open |
| `startOpen`       | `boolean`                      | `true`   | Open immediately on create  |
| `removeOnClose`   | `boolean`                      | `true`   | Remove modal after close    |

> Be aware that when `removeOnClose` is set to `false`, the modal content is hidden but **not destroyed**. If your body uses a component with active subscriptions, timers, or sockets, they will continue to run while the modal is hidden. Consider `removeOnClose: true` for component bodies if you want their resources to be disposed on close.

---

&nbsp;

### Per-modal options (create)

These options are passed to `modalService.create()` and override defaults from `provideModal()` on a per‑modal basis. For shared defaults like `size`, `actionsPosition`, `prevent`, `closable`, `alertDialog`, `lazy`, `startOpen`, and `removeOnClose`, see [Modal Configuration Options](#modal-configuration-options) above.

| Option        | Type                                                                | Description                                                 |
| ------------- | ------------------------------------------------------------------- | ----------------------------------------------------------- |
| `header`      | `string \| TemplateRef<unknown>`                                    | Modal header text or template                               |
| `body`        | `string \| { template, context } \| { component, inputs, outputs }` | Modal body content (string, template+context, or component) |
| `buttons`     | `ModalButton[]`                                                     | Action buttons to render in the actions slot                |
| `selector`    | `string`                                                            | CSS selector for focus return element                       |
| `referenceEl` | `HTMLElement`                                                       | Element to return focus to (preferred over selector)        |
| `onClosed`    | `() => void \| Promise<void>`                                       | Called when the modal is closed                             |
| `onRemoved`   | `() => void \| Promise<void>`                                       | Called after the modal is removed                           |

> Note: If you want focus to return to the element that opened the modal, pass either `selector` or `referenceEl` when creating the modal. This avoids Tegel's "Missing focus origin" warning.

---

&nbsp;

### Body content options

The modal body can be provided as a simple string, a template body, or a component descriptor.

&nbsp;

#### 1) String body

```ts
this.modalService.create({
  header: 'Simple',
  body: 'This is a simple modal body',
});
```

&nbsp;

#### 2) Template body (template + optional context)

Signature:

```
body: {
  template: TemplateRef<TContext>;
  context?: TContext;
}
```

&nbsp;
Example:

```html
<ng-template #bodyTpl let-context>
  <p>
    <b>{{ context.substance }}<b> boils at
    <b>{{ context.value }}</b> degrees {{ context.units }}.
  </p>
</ng-template>
```

```ts
interface SubstanceTemperature {
  substance: string;
  value: number;
  units: string;
}

const bodyTpl =
  viewChild.required<TemplateRef<SubstanceTemperature>>('bodyTpl');
this.modalService.create({
  header: 'Template',
  body: {
    template: bodyTpl(),
    context: {
      substance: 'Water',
      value: 100,
      units: 'Celcius',
    },
  },
});
```

&nbsp;

For typed templates, you can use `TypedTemplateDirective` to enforce context types in the template:

```html
<ng-template #bodyTpl let-context [typedContext]="this.bodyTpl()">
  ...
</ng-template>
```

&nbsp;

#### 3) Component body (with inputs/outputs)

Signature:

```ts
body: {
  component: Type<TComponent>;
  inputs: ComponentInputs<TComponent>;
  outputs?: ComponentOutputs<TComponent>;
}
```

&nbsp;

When you pass a component as the modal body, the `inputs` and `outputs` objects are fully typed. This means you get IntelliSense for signal inputs/outputs, and any `input.required(...)` fields are enforced at compile time (missing required inputs will fail the build).

```ts
export class ExampleComponent {
  readonly requiredField = input.required<string>();
  readonly optionalField = input<string>();
  readonly outputField = output<string>();
}
```

```ts
const ref = this.modalService.create({
  header: 'Example',
  body: {
    component: ExampleComponent,
    inputs: {
      requiredField: 'Required',
      // optionalField: 'Optional'
    },
    outputs: {
      outputField: (value: string) => {
        console.log('Value:', value);
        ref.close();
      },
    },
  },
});
```

&nbsp;

### Action buttons

```ts
this.modalService.create({
  header: 'Confirm',
  body: 'Are you sure?',
  buttons: [
    { text: 'OK', variant: 'primary', onClick: async () => console.log('OK') },
    { text: 'Cancel', variant: 'secondary', dismiss: true },
  ],
  startOpen: true,
});
```

- `dismiss`: true adds `data-dismiss-modal` for Tegel's built‑in close behavior.
- `onClick` supports sync or async handlers.

&nbsp;

#### ModalButton options

Modal action buttons map directly to Tegel's `tds-button` configuration. The following options are supported:

| Option     | Type                                                | Description                                                 |
| ---------- | --------------------------------------------------- | ----------------------------------------------------------- |
| `text`     | `string`                                            | Button label text                                           |
| `variant`  | `'primary' \| 'secondary' \| 'success' \| 'danger'` | Visual variant                                              |
| `size`     | `'xs' \| 'sm' \| 'md' \| 'lg'`                      | Button size                                                 |
| `disabled` | `boolean`                                           | Disable the button                                          |
| `dismiss`  | `boolean`                                           | Adds `data-dismiss-modal` to trigger Tegel's close behavior |
| `onClick`  | `() => void \| Promise<void>`                       | Optional click handler (sync or async)                      |

---

&nbsp;

## ModalService API

The `ModalService` provides a signal-based API to create, manage, and dismiss modal instances. It is available after registering `provideModal()` in your `app.config.ts`.

&nbsp;

### ModalService Properties

| Property | Type                         | Description                                        |
| -------- | ---------------------------- | -------------------------------------------------- |
| `modals` | `Signal<Map<string, Modal>>` | Read-only map of all registered modals keyed by id |

&nbsp;

### ModalService Methods

#### `create<T>(options?: ModalOptions<T>): ModalRef`

Creates and registers a new modal. Returns a `ModalRef` for controlling that instance.

```ts
const ref = modalService.create({
  header: 'Hello',
  body: 'Modal content',
  startOpen: false,
});

ref.open();
ref.close();
ref.remove();
```

&nbsp;

#### ModalRef

`create()` returns a `ModalRef` with `open()`, `close()`, and `remove()` methods to control that specific modal instance.

#### `open(id: string): void`

Opens an existing modal by id.

Example:

```ts
const ref = modalService.create({
  header: 'Example',
  body: 'Hello',
  startOpen: false,
});
modalService.open(ref.id); // Equivalent to ref.open()
```

&nbsp;

#### `close(id: string): void`

Closes (hides) a modal by id. If `removeOnClose` is `true`, it is removed from the DOM after closing.

&nbsp;

#### `remove(id: string): void`

Removes a modal from the registry/DOM. If the modal is open, it is closed first.

&nbsp;

#### `closeAll(): void`

Closes all registered modals.

&nbsp;

#### `removeAll(): void`

Removes all registered modals.

---

&nbsp;

### Modal Lifecycle Hooks

Each modal supports optional lifecycle callbacks:

| Callback      | Description                                             |
| ------------- | ------------------------------------------------------- |
| `onClosed()`  | Called when the modal is closed                         |
| `onRemoved()` | Called after the modal is removed from the registry/DOM |

Example:

```ts
modalService.create({
  header: 'Example',
  body: 'Hello',
  onClosed: () => console.log('Modal closed'),
  onRemoved: () => console.log('Modal removed'),
});
```

---

&nbsp;

# Components & Directives

This library includes a set of standalone UI components and utility directives, all prefixed with **`tae`**, designed to extend and complement the Tegel Angular ecosystem. Each piece is lightweight, fully typed, and easy to import into any Angular 19+ application.
&nbsp;

## Components

### DateTime Picker Component (`tae-date-time-picker`) [BETA]

`TaeDateTimePickerComponent` is a standalone, form‑compatible picker that supports `date`, `datetime`, and `time` modes. It implements Angular's `ControlValueAccessor`, so it works with both reactive and template‑driven forms.

> Beta: This component is still evolving. Improvements are planned and minor bugs may occur.

**Inputs:**

| Input           | Type                                  | Default   | Description         |
| --------------- | ------------------------------------- | --------- | ------------------- |
| `label`         | `string`                              | `''`      | Label text          |
| `size`          | `'sm' \| 'md' \| 'lg'`                | `lg`      | Control size        |
| `labelPosition` | `'inside' \| 'outside' \| 'no-label'` | `outside` | Label placement     |
| `state`         | `'error' \| undefined`                | —         | Error state styling |
| `helper`        | `string`                              | `''`      | Helper text         |
| `mode`          | `'date' \| 'datetime' \| 'time'`      | `date`    | Picker mode         |

> Note: `labelPosition` is currently not implemented; it will be supported in a future update.
> Note: `state` is currently only used for styling in the template and does not affect behavior.

&nbsp;

**Example (reactive forms):**

```ts
readonly form = this.fb.group({
  date: this.fb.control<string | null>(null),
  time: this.fb.control<string | null>(null),
  dateTime: this.fb.control<string | null>(null),
});
```

```html
<form [formGroup]="form">
  <tae-date-time-picker label="Date" formControlName="date" mode="date" />
  <tae-date-time-picker label="Time" formControlName="time" mode="time" />
  <tae-date-time-picker
    label="DateTime"
    formControlName="dateTime"
    mode="datetime"
  />
</form>
```

&nbsp;

### Footer Component (`tae-footer`)

`TaeFooterComponent` is an enhanced footer based on the Tegel `TdsFooterComponent`. It preserves the same visual appearance while adding two key improvements:

- A **compact “small” variant** for constrained layouts.
- An optional **version display**, allowing applications to show their build or release version directly in the footer.

This makes it ideal for full-viewport layouts that benefit from space efficiency and clear version visibility.

**Inputs:**

| Input   | Type                  | Default | Description                                                                                |
| ------- | --------------------- | ------- | ------------------------------------------------------------------------------------------ |
| variant | `'normal' \| 'small'` | normal  | Layout style of the footer. `'small'` produces a more compact version.                     |
| version | `string \| undefined` | —       | Optional application version string. If provided, it is displayed left of the Scania logo. |

**Example:**

```html
<tae-footer variant="small" version="v1.0.0" />
```

&nbsp;

## Directives

### Hard Refresh Directive (`taeHardRefresh`)

The `HardRefreshDirective` provides a small UX shortcut: it performs a **full page reload** when the host element is clicked **N times in rapid succession**, where each click must occur within `clickWindowMs` milliseconds of the previous one.

This is especially useful for hard-reloading tablet or mobile applications which are locked in full-screen mode, and thus have no browser buttons like refresh.

**Inputs:**

| Property         | Type     | Default | Description                                                            |
| ---------------- | -------- | ------- | ---------------------------------------------------------------------- |
| `clicksRequired` | `number` | `3`     | Number of clicks required within the window to trigger a hard refresh. |
| `clickWindowMs`  | `number` | `500`   | Time window in milliseconds between two subsequent clicks.             |

**Example:**

```html
<tds-header-brand-symbol
  taeHardRefresh
  [clicksRequired]="3"
  [clickWindowMs]="500"
>
  <a aria-label="Scania - red gryphon on blue shield"></a>
</tds-header-brand-symbol>
```

&nbsp;

### Barcode Scanner Directive (`taeBarcodeScanner`)

The `BarcodeScannerDirective` enables global barcode scanning by listening for rapid character input sequences (typically from a hardware scanner) that terminate with a specific key (default is `Enter`).

It operates outside of Angular's zone to prevent unnecessary change detection cycles on every keypress and includes built-in filtering to ignore modifier keys like `Shift`, `Ctrl`, or `Alt`, ensuring only the actual barcode data is captured.

**Inputs:**

| Input           | Type     | Default   | Description                                                        |
| --------------- | -------- | --------- | ------------------------------------------------------------------ |
| `inputWindowMs` | `number` | `100`     | Max time allowed between consecutive keypresses before discarding. |
| `terminatorKey` | `string` | `'Enter'` | The key that signals the end of a barcode sequence.                |

**Outputs:**

| Output           | Type     | Description                                                 |
| ---------------- | -------- | ----------------------------------------------------------- |
| `barcodeScanned` | `string` | **Required**. Emits the full barcode string once completed. |

**Example:**

```html
<ng-container
  taeBarcodeScanner
  (barcodeScanned)="handleScan($event)"
  [inputWindowMs]="100"
  terminatorKey="Enter"
>
  <p>Scanner is active. Please scan a barcode...</p>
</ng-container>
```

---

&nbsp;

# Appendix

## Runtime Config Dockerfile Example

```Dockerfile
ARG NODE_VERSION=25-alpine
ARG NGINX_VERSION=1.29.3-alpine

# Stage 1: Build the Angular Application
FROM node:${NODE_VERSION} AS build

WORKDIR /app

# Copy package-related files
COPY package*.json ./

# Install project dependencies using npm ci (ensures a clean, reproducible install)
RUN --mount=type=cache,target=/root/.npm npm ci

# Copy the rest of the application source code into the container
COPY . .

# Build the Angular application with the specified app version
RUN npm run build


# Stage 2: Prepare Nginx to Serve Static Files
FROM nginx:${NGINX_VERSION}

# Copy the static build output from the build stage to Nginx's default HTML serving directory
COPY --from=build /app/dist/*/browser /usr/share/nginx/html

# Copy the shell scripts from the @scania-nl/tegel-angular-extensions npm package
COPY --from=build /app/node_modules/@scania-nl/tegel-angular-extensions/docker/*.sh /usr/local/bin/
# Copy the custom Nginx configuration file from the @scania-nl/tegel-angular-extensions npm package
COPY --from=build /app/node_modules/@scania-nl/tegel-angular-extensions/docker/nginx.conf /etc/nginx/nginx.conf

# Ensure the shell scripts are executable
RUN chmod +x /usr/local/bin/*

# Expose ports 80 and 443 to allow HTTP and HTTPS traffic
EXPOSE 80 443

# Use the shared entrypoint from the @scania-nl/tegel-angular-extensions npm package
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

# Start the nginx web server with custom config in the foreground and keep it running
CMD ["nginx", "-c", "/etc/nginx/nginx.conf", "-g", "daemon off;"]
```

---

&nbsp;

# License

Copyright 2025 Scania CV AB.

All files are available under the MIT license. The Scania brand identity, logos and photographs found in this repository are copyrighted Scania CV AB and are not available on an open source basis or to be used as examples or in any other way, if not specifically ordered by Scania CV AB.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.