# Sixbell Telco SDK - Component Library

Welcome to the Sixbell Telco SDK component library. This is a comprehensive Angular component library featuring 25+ pre-built, production-ready UI components built with Angular 19, Tailwind CSS v4, and DaisyUI.

The package is public on npm, but the repository is private and there is no separate public docs page. Because of that, this README keeps setup guidance, provider examples, translation examples, and usage references that public consumers need.

## Overview

The SDK provides:

- **25+ Production-Ready Components**: Button, Card, Form inputs, Data Table, Dialog, Dropdown, and more
- **Utility Services**: Theme management, i18n translation, logging, runtime config, and formatting
- **Directive Support**: Text, auto-focus, click-outside, and only-numbers
- **Tailwind CSS v4 + DaisyUI**: Modern styling with customizable themes
- **Multi-Entry Point Architecture**: Granular imports for optimized bundle sizes
- **Angular 19 Signal-Based**: Modern reactive patterns with Angular signals

## Pre-requirements

- **Node.js**: `>=22.19.0 <23`
- **npm**: `>=10 <11`
- **Angular**: 19.2.x or later
- **Tailwind CSS**: v4.x

## Available Components

### General Components

| Component     | Description                                                                   |
| ------------- | ----------------------------------------------------------------------------- |
| Accordion     | Expandable/collapsible content sections                                       |
| Avatar        | User profile pictures with initials fallback                                  |
| Badge         | Status and label indicators                                                   |
| Button        | Interactive buttons with variants and sizes                                   |
| Card          | Compound content shell with contextual headers, sections, and divided footers |
| Countdown     | Countdown timer with customizable formatting                                  |
| Data Table    | Advanced grid with sorting, filtering, pagination, row actions                |
| Dialog        | Modal dialogs with animations and positioning                                 |
| Divider       | Semantic divider with orientation, placement, and margin control              |
| Dropdown      | Menu dropdown for actions and links                                           |
| Dropdown Menu | Advanced nested menu system                                                   |
| Dual List     | Dual list selection for managing items                                        |
| File Upload   | File uploader and dropzone entry point                                        |
| Icon          | SVG icon component with ng-icons integration                                  |
| Link          | Hyperlink component with router support                                       |
| Notification  | Toast-style notifications                                                     |
| Overlay       | Generic overlay with trigger and content                                      |
| Paginator     | Pagination controls for data                                                  |
| Product Card  | Product display card with image and actions                                   |
| Progress      | Progress bar with variants                                                    |
| Sonner Toast  | Modern toast notifications via ngx-sonner                                     |
| Tab           | Tabbed navigation interface                                                   |
| Table         | Data table with inline and dropdown row actions                               |
| Text          | Polymorphic text component with semantic HTML tags                            |
| Tooltip       | Floating tooltip with positioning                                             |
| Wizard        | Multi-step wizard with validation                                             |

### Form Components

| Component  | Description                                     |
| ---------- | ----------------------------------------------- |
| Checkbox   | Checkbox input with multiple variants           |
| Combobox   | Searchable dropdown with multi-select support   |
| Datepicker | Single and range date selection with time       |
| Form Item  | Layout wrapper for fields + full-width errors   |
| Form Error | Error message display for form fields           |
| Form Group | Vertical spacing helper for grouped fields      |
| Input      | Text input with variants, icons, and validation |
| Radio      | Radio button group selection                    |
| Range      | Slider input for numeric ranges                 |
| Select     | Dropdown select with object support             |
| Switch     | Toggle switch for boolean values                |
| Textarea   | Multi-line text input with character count      |
| Toggle     | Toggle button for boolean states                |

### Form Errors Layout

For full-width validation messages, wrap each field and its error with `st-form-item` and place `st-form-errors` as a sibling (not inside the field component).

```html
<st-form-item>
  <st-input formControlName="controlInput" label="Input" [parentForm]="form"></st-input>
  <st-form-errors [parentForm]="form" formField="controlInput" />
</st-form-item>
```

Migration notes: `projects/sdk/components/forms/form-error/MIGRATION.md`

### Optional Label Indicator

Form fields can display an optional indicator inline with the legend. Enable it with `showOptional` and customize the text with `optionalText`.

```html
<st-input label="Email" [showOptional]="true" [optionalText]="'sdk.formFields.optional'"></st-input>
```

Add the translation key to your SDK translations (example shown for English):

```json
{
  "sdk": {
    "formFields": {
      "optional": "Optional"
    }
  }
}
```

### Utility Services

| Service             | Purpose                                            | Documentation                   |
| ------------------- | -------------------------------------------------- | ------------------------------- |
| Logger Service      | Theme-aware logging with IndexedDB persistence     | [README](utils/logger/)         |
| Theme Service       | Runtime theme management with multi-tenant support | [README](utils/theme/)          |
| Translation Service | Multi-language i18n with ngx-translate integration | [README](utils/translation/)    |
| Formatting Service  | Reactive locale-aware formatting utilities         | [README](utils/formatting/)     |
| Runtime Config      | Last-known-good runtime config loader              | [README](utils/runtime-config/) |

### Directives

| Directive     | Purpose                                               |
| ------------- | ----------------------------------------------------- |
| auto-focus    | Auto-focus elements when visible                      |
| click-outside | Detect clicks outside an element                      |
| only-numbers  | Restrict input to numeric values only                 |
| text          | Apply preset-based typography with line-clamp support |

## Installation

### Step 1: Install Dependencies

```bash
npm install @sixbell-telco/sdk @tailwindcss/typography daisyui @midudev/tailwind-animations
```

### Step 2: Import SDK Styles

In your global `styles.css`:

```css
@import '../node_modules/@sixbell-telco/sdk/_index.css';

@import 'tailwindcss';

@source '../src';
```

> ⚠️ **Important**: Import `_index.css` **before** the tailwind import.

### Step 3: Configure Microfrontend (if applicable)

Add path mapping to `tsconfig.json`:

```json
{
  "compilerOptions": {
    "paths": {
      "@sixbell-telco/sdk/*": ["./node_modules/@sixbell-telco/sdk/*"]
    }
  }
}
```

### Step 4: Optional - VSCode Configuration

Install the Tailwind CSS extension and add to `.vscode/settings.json`:

```json
{
  "editor.quickSuggestions": {
    "strings": "on"
  },
  "files.associations": {
    "*.css": "tailwindcss"
  },
  "tailwindCSS.classFunctions": ["tw", "clsx", "cva", "cn", "tw\\.[a-z-]+"]
}
```

These settings improve your development experience:

- `editor.quickSuggestions`: Enables autosuggestions within string literals
- `files.associations`: Ensures CSS files are treated as Tailwind CSS files
- `tailwindCSS.classFunctions`: Enables Tailwind intellisense in utility functions like `tw()`, `clsx()`, and `cva()`, etc that are used for conditional class name composition

With these settings, VSCode will provide class autocompletion for the library's custom attributes and utility functions.

## Themes and translation configuration (an optional Logger)

Configure all services using runtime providers in your `app.config.ts`. This approach loads configuration from JSON files at runtime, eliminating the need to recompile when updating themes or languages.

### Logger Service Setup

Configure logging with different levels for development and production:

```typescript
import { provideLogger } from '@sixbell-telco/sdk/utils/logger';

export const appConfig: ApplicationConfig = {
  providers: [
    provideLogger({
      enableLogging: true,
      logLevel: isDevMode() ? 'debug' : 'warn', // trace | debug | info | warn | error | fatal
      showTimestamp: true,
      persistToIndexedDB: true, // Persistent storage for debugging
      silentMode: !isDevMode(),
    }),
  ],
};
```

### Translation Service Setup

The Translation Service loads language configuration from a runtime JSON file:

```typescript
import { provideRuntimeTranslation } from '@sixbell-telco/sdk/utils/translation';

export const appConfig: ApplicationConfig = {
  providers: [provideRuntimeTranslation('/assets/translation/translation.json')],
};
```

The `translation.json` file defines available languages and their translation paths:

```json
{
  "meta": {
    "updatedAt": "2026-02-02T00:00:00Z",
    "hash": "showcase-translations-v2",
    "schemaVersion": "3"
  },
  "additionalLanguages": [],
  "defaultLang": "es",
  "excludeLanguages": [],
  "translationPaths": ["/assets/i18n/", "/assets/i18n/sdk/", "/assets/themes/i18n"],
  "locale": {
    "defaultLocale": "en-US",
    "languageToLocale": {
      "en": "en-US",
      "es": "es-CL",
      "pt": "pt-BR"
    }
  }
}
```

### Theme Service Setup

The Theme Service loads theme configuration from a runtime JSON file:

```typescript
import { provideRuntimeTheme } from '@sixbell-telco/sdk/utils/theme';

export const appConfig: ApplicationConfig = {
  providers: [provideRuntimeTheme('/assets/themes/themes.json')],
};
```

The `themes.json` file defines available themes and their variants:

```json
{
  "meta": {
    "updatedAt": "2026-02-02T00:00:00Z",
    "hash": "showcase-themes-v1",
    "schemaVersion": "2"
  },
  "themes": ["/assets/themes/catalog/sixbell_telco.json", "/assets/themes/catalog/wom.json"],
  "defaultTheme": "sixbell_telco",
  "defaultScheme": "system",
  "excludeThemes": []
}
```

### Complete Configuration Example

```typescript
// app.config.ts
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideLogger } from '@sixbell-telco/sdk/utils/logger';
import { provideRuntimeTheme } from '@sixbell-telco/sdk/utils/theme';
import { provideRuntimeTranslation } from '@sixbell-telco/sdk/utils/translation';
import { from, timer } from 'rxjs';
import { switchMap } from 'rxjs/operators';

const runtimeUpdateStream$ = timer(0, environment.runtime.pollIntervalMs).pipe(
  switchMap(() => from(fetch(`${environment.runtime.apiBaseUrl}/api/runtime/updates/poll`))),
  switchMap((response) => from(response.json())),
);

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    provideLogger({
      enableLogging: isDevMode(),
      logLevel: isDevMode() ? 'debug' : 'warn',
      showTimestamp: true,
      showDate: true,
      persistToIndexedDB: true,
      idbRetentionDays: 7,
      idbMaxLogs: 5000,
      silentMode: !isDevMode(),
    }),
    ...(() => {
      const mode = environment.runtime.defaultMode;

      if (mode === 'default') {
        return [provideRuntimeTranslation('/assets/translation/translation.json'), provideRuntimeTheme('/assets/themes/themes.json')];
      }

      if (mode === 'stream' && environment.runtime.enableStream) {
        return [
          provideRuntimeTranslation(`${environment.runtime.apiBaseUrl}/api/runtime/translation/config`, {
            updateStream: runtimeUpdateStream$,
          }),
          provideRuntimeTheme(`${environment.runtime.apiBaseUrl}/api/runtime/theme/config`, {
            updateStream: runtimeUpdateStream$,
          }),
        ];
      }

      return [
        provideRuntimeTranslation(`${environment.runtime.apiBaseUrl}/api/runtime/translation/config`, {
          sse: {
            url: `${environment.runtime.apiBaseUrl}/api/runtime/updates?resource=translation`,
            eventType: 'message',
          },
        }),
        provideRuntimeTheme(`${environment.runtime.apiBaseUrl}/api/runtime/theme/config`, {
          sse: {
            url: `${environment.runtime.apiBaseUrl}/api/runtime/updates?resource=theme`,
            eventType: 'message',
          },
        }),
      ];
    })(),
  ],
};
```

### Reactive Formatting Setup

Use SDK formatting pipes and services instead of Angular native locale pipes:

```html
{{ createdAt | stDate:'DD/MM/YYYY HH:mm:ss' }} {{ total | stDecimal:'1.0-2' }} {{ amount | stCurrency:'CLP':'symbol':'1.0-0' }} {{ ratio |
stPercent:'1.0-2' }}
```

```typescript
import { Component, computed, inject } from '@angular/core';
import { FormattingService } from '@sixbell-telco/sdk/utils/formatting';

@Component({
  selector: 'app-formatting-example',
  template: `{{ formattedAmount() }}`,
})
export class FormattingExampleComponent {
  private readonly formatting = inject(FormattingService);

  amount = 1234.5;

  formattedAmount = computed(() =>
    this.formatting.formatCurrency(this.amount, 'CLP', {
      display: 'symbol',
      digitsInfo: '1.0-0',
    }),
  );
}
```

**Key Benefits of Runtime Configuration:**

- **No Recompilation**: Update themes and languages without rebuilding your application
- **Multi-Tenant Support**: Easily switch themes and language sets per deployment environment
- **Caching Strategy**: RuntimeConfigStore keeps last-known-good config in Dexie
- **Hot Updates**: Changes to `themes.json` and `translation.json` take effect immediately
- **Persistent Preferences**: User language and theme preferences are stored and restored automatically

## Adding Custom Themes

Themes are managed through a runtime configuration file (`themes.json`) with theme JSON files. This approach allows for:

- **Zero Recompilation**: Add/modify themes without rebuilding
- **Multi-Tenant Support**: Different themes per deployment
- **Dynamic Assets**: Brand-specific logos and images per theme
- **Runtime Fonts**: Load custom fonts for each theme
- **Hot Updates**: Changes take effect immediately

### 1. Update `themes.json`

Add your custom theme to `/assets/themes/themes.json`:

```json
{
  "meta": {
    "updatedAt": "2026-02-02T00:00:00Z",
    "hash": "showcase-themes-v1",
    "schemaVersion": "2"
  },
  "themes": ["/assets/themes/catalog/sixbell_telco.json", "/assets/themes/catalog/wom.json"],
  "defaultTheme": "sixbell_telco",
  "defaultScheme": "system",
  "excludeThemes": []
}
```

**Configuration Structure:**

- `themes`: theme JSON paths
- `defaultTheme`: initial theme on app startup
- `defaultScheme`: initial color scheme (`'system'`, `'light'`, or `'dark'`)

### 2. Define DaisyUI Themes in Global Styles

In your global `styles.css`, import the SDK and define your theme CSS:

```css
@import '../node_modules/@sixbell-telco/sdk/_index.css';

@import 'tailwindcss';

@source '../src';
```

### 3. Use Themes in Components

The ThemeService automatically loads available themes from `themes.json`:

```typescript
import { Component, ChangeDetectionStrategy, inject, computed } from '@angular/core';
import { ThemeService, ThemeAssetsService } from '@sixbell-telco/sdk/utils/theme';
import { ButtonComponent } from '@sixbell-telco/sdk/components/button';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-theme-switcher',
  standalone: true,
  imports: [CommonModule, ButtonComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>
      <select (change)="themeService.setTheme($event.target.value)">
        @for (theme of themeService.getAvailableThemes(); track theme) {
          <option [value]="theme" [selected]="themeService.selectedTheme() === theme">
            {{ themeService.getLocalizedThemeName(theme) }}
          </option>
        }
      </select>

      <select (change)="themeService.setScheme($event.target.value)">
        @for (scheme of themeService.getAvailableSchemes(); track scheme) {
          <option [value]="scheme" [selected]="themeService.selectedScheme() === scheme">
            {{ themeService.getLocalizedSchemeName(scheme) }}
          </option>
        }
      </select>

      <img [src]="logoUrl()" alt="Brand Logo" />
    </div>
  `,
})
export class ThemeSwitcherComponent {
  themeService = inject(ThemeService);
  private assetsService = inject(ThemeAssetsService);

  logoUrl = computed(
    () => this.assetsService.getAssetUrl(this.themeService.selectedTheme(), 'logo', this.themeService.resolvedScheme()) || '/default-logo.svg',
  );
}
```

### 4. Add Theme Names to Translations

Store theme and scheme display names in translation files. Create `/assets/themes/i18n/en.json`:

**English (`en.json`):**

```json
{
  "sdk": {
    "theme": {
      "catalog": {
        "sixbell_telco": "Sixbell telco",
        "wom": "Wom"
      },
      "schemes": {
        "system": "System",
        "light": "Light",
        "dark": "Dark"
      }
    }
  }
}
```

**Spanish (`es.json`):**

```json
{
  "sdk": {
    "theme": {
      "catalog": {
        "sixbell_telco": "Sixbell telco",
        "wom": "Wom"
      },
      "schemes": {
        "system": "Sistema",
        "light": "Claro",
        "dark": "Oscuro"
      }
    }
  }
}
```

**Portuguese (`pt.json`):**

```json
{
  "sdk": {
    "theme": {
      "catalog": {
        "sixbell_telco": "Sixbell telco",
        "wom": "Wom"
      },
      "schemes": {
        "system": "Sistema",
        "light": "Claro",
        "dark": "Escuro"
      }
    }
  }
}
```

Your themes are now fully configured. The ThemeService automatically:

- Loads theme definitions from `themes.json`
- Injects CSS variables via signals
- Resolves dynamic assets based on current theme and scheme
- Persists user preferences to localStorage
- Detects system dark mode preference when scheme is set to `'system'`

## Adding Custom Languages

Languages are now managed through a runtime configuration file (`translation.json`), eliminating the need to reconfigure and recompile. This approach allows for:

- **Zero Recompilation**: Add or modify languages without rebuilding
- **Multi-Tenant Languages**: Different language sets per deployment environment
- **Hot Updates**: Changes to translation configuration take effect immediately
- **Flexible Structure**: Organize translation files any way you need

### 1. Create or Update `translation.json`

Create `/assets/translation/translation.json` in your application:

```json
{
  "meta": {
    "updatedAt": "2026-02-02T00:00:00Z",
    "hash": "showcase-translations-v2",
    "schemaVersion": "3"
  },
  "additionalLanguages": [],
  "defaultLang": "es",
  "excludeLanguages": [],
  "translationPaths": ["/assets/i18n/", "/assets/i18n/sdk/", "/assets/themes/i18n"],
  "locale": {
    "defaultLocale": "en-US",
    "languageToLocale": {
      "en": "en-US",
      "es": "es-CL",
      "pt": "pt-BR"
    }
  }
}
```

**Configuration Structure:**

- `defaultLang`: language code used on app initialization
- `translationPaths`: list of directories that contain `{lang}.json`
- `additionalLanguages`: extra languages beyond the built-in SDK defaults
- `excludeLanguages`: built-in languages to remove from the available list
- `locale.languageToLocale`: mapping used by `FormattingService` and the formatting pipes

### 2. Create Translation Files

Create language translation files at the paths defined in `translation.json`. The minimal translation structure includes SDK translations:

**English (`/assets/i18n/sdk/en.json`):**

```json
{
  "sdk": {
    "fileUpload": {
      "dropzone": {
        "selectPrompt": "Click to select files or drag and drop here",
        "maxSizeLabel": "Max size:",
        "allowedTypesLabel": "Allowed types:",
        "invalidFileType": "Invalid file type",
        "fileTooLarge": "File too large",
        "fileCounter": "{{current}} of {{max}} files",
        "maxFilesExceeded": "Maximum {{max}} file(s) allowed",
        "noValidFiles": "No valid files selected",
        "fileActions": {
          "play": "Play audio",
          "pause": "Pause audio",
          "download": "Download file",
          "remove": "Remove file"
        }
      },
      "fileUploader": {
        "allowedTypesLabel": "Allowed types:"
      }
    },
    "audioPlayer": {
      "trackListTitle": "Tracks",
      "trackInfo": {
        "title": "No title",
        "description": "No description"
      }
    },
    "countdown": {
      "days": "Days",
      "hours": "Hours",
      "minutes": "Minutes",
      "seconds": "Seconds"
    },
    "dualList": {
      "searchPlaceHolder": "Search"
    },
    "combobox": {
      "searchPlaceholder": "Search",
      "placeholder": "Select",
      "noResultsFound": "No results found",
      "clearAll": "Clear All",
      "clearSelection": "Clear selection",
      "searchOptions": "Search options",
      "loading": "Loading..."
    },
    "select": {
      "placeholder": "Select"
    },
    "formErrors": {
      "validation": {
        "required": "*This field is required",
        "email": "*Please enter a valid email address",
        "minLength": "*Must be at least {{min}} characters",
        "maxLength": "*Must be no more than {{max}} characters",
        "min": "*Value must be at least {{min}}",
        "max": "*Value must be no more than {{max}}",
        "pattern": "*Invalid format",
        "unhandledError": "*Unhandled error"
      }
    },
    "formFields": {
      "optional": "Optional"
    },
    "textarea": {
      "maxCharacters": "{{current}} of {{max}} characters"
    },
    "table": {
      "entriesByPage": "Entries per page",
      "perPage": "{{count}} / page",
      "totalEntries": "{{total}} entries",
      "entriesRange": "{{range}} of {{total}} entries",
      "emptyListMessage": "No items to show",
      "noData": {
        "title": "No data available",
        "message": "There are no records to display at this time."
      },
      "noResults": {
        "title": "No results found",
        "message": "Try adjusting your search criteria"
      }
    },
    "dataTable": {
      "entriesByPage": "Entries per page",
      "perPage": "{{count}} / page",
      "totalEntries": "{{total}} entries",
      "entriesRange": "{{range}} of {{total}} entries",
      "emptyListMessage": "No items to show",
      "noData": {
        "title": "No data available",
        "message": "There are no records to display at this time."
      },
      "noResults": {
        "title": "No results found",
        "message": "Try adjusting your search criteria"
      }
    },
    "wizard": {
      "wizardMarker": {
        "completed": "Completed",
        "inProgress": "In progress",
        "pending": "Pending"
      },
      "wizardWrapper": {
        "back": "Back",
        "previous": "Previous",
        "end": "End",
        "next": "Next",
        "step": "Step {{index}}"
      }
    },
    "datePicker": {
      "quickActions": {
        "title": "Quick Select",
        "lastHour": "Last hour",
        "last3Hours": "Last 3 hours",
        "last6Hours": "Last 6 hours",
        "last12Hours": "Last 12 hours",
        "last24Hours": "Last 24 hours",
        "last7Days": "Last 7 days",
        "last30Days": "Last 30 days",
        "last6Months": "Last 6 months",
        "lastYear": "Last year",
        "clear": "Clear"
      },
      "labels": {
        "from": "From",
        "to": "To"
      }
    }
  }
}
```

**Spanish (`/assets/i18n/es.json`):**

```json
{
  "sdk": {
    "fileUpload": {
      "dropzone": {
        "selectPrompt": "Haz clic para seleccionar archivos o arrastra y suelta aquí",
        "maxSizeLabel": "Tamaño máximo:",
        "allowedTypesLabel": "Tipos permitidos:",
        "invalidFileType": "Tipo de archivo no válido",
        "fileTooLarge": "Archivo demasiado grande",
        "fileCounter": "{{current}} de {{max}} archivos",
        "maxFilesExceeded": "Máximo de {{max}} archivo(s) permitido",
        "noValidFiles": "No se han seleccionado archivos válidos",
        "fileActions": {
          "play": "Reproducir audio",
          "pause": "Pausar audio",
          "download": "Descargar archivo",
          "remove": "Quitar archivo"
        }
      },
      "fileUploader": {
        "allowedTypesLabel": "Tipos permitidos:"
      }
    },
    "audioPlayer": {
      "trackListTitle": "Pistas",
      "trackInfo": {
        "title": "Sin título",
        "description": "Sin descripción"
      }
    },
    "countdown": {
      "days": "Días",
      "hours": "Horas",
      "minutes": "Minutos",
      "seconds": "Segundos"
    },
    "dualList": {
      "searchPlaceHolder": "Buscar"
    },
    "combobox": {
      "searchPlaceholder": "Buscar",
      "placeholder": "Seleccione",
      "noResultsFound": "No se encontraron resultados",
      "clearAll": "Limpiar Todo",
      "clearSelection": "Limpiar selección",
      "searchOptions": "Buscar opciones",
      "loading": "Cargando..."
    },
    "select": {
      "placeholder": "Seleccione"
    },
    "formErrors": {
      "validation": {
        "required": "*Este campo es obligatorio",
        "email": "*Por favor ingresa una dirección de correo válida",
        "minLength": "*Debe tener al menos {{min}} caracteres",
        "maxLength": "*No debe tener más de {{max}} caracteres",
        "min": "*El valor debe ser al menos {{min}}",
        "max": "*El valor no debe superar {{max}}",
        "pattern": "*Formato inválido",
        "unhandledError": "*Error inesperado"
      }
    },
    "textarea": {
      "maxCharacters": "{{current}} de {{max}} caracteres"
    },
    "table": {
      "entriesByPage": "Entradas por página",
      "perPage": "{{count}} / página",
      "totalEntries": "{{total}} entradas",
      "entriesRange": "{{range}} de {{total}} entradas",
      "emptyListMessage": "No hay elementos para mostrar"
    },
    "dataTable": {
      "entriesByPage": "Entradas por página",
      "perPage": "{{count}} / página",
      "totalEntries": "{{total}} entradas",
      "entriesRange": "{{range}} de {{total}} entradas",
      "emptyListMessage": "No hay elementos para mostrar",
      "noData": {
        "title": "No hay datos disponibles",
        "message": "No hay registros para mostrar en este momento."
      },
      "noResults": {
        "title": "No se encontraron resultados",
        "message": "Intenta ajustar tus criterios de búsqueda"
      }
    },
    "wizard": {
      "wizardMarker": {
        "completed": "Completado",
        "inProgress": "En progreso",
        "pending": "Pendiente"
      },
      "wizardWrapper": {
        "back": "Atrás",
        "previous": "Anterior",
        "end": "Finalizar",
        "next": "Siguiente",
        "step": "Paso {{index}}"
      }
    },
    "datePicker": {
      "quickActions": {
        "title": "Selección Rápida",
        "lastHour": "Última hora",
        "last3Hours": "Últimas 3 horas",
        "last6Hours": "Últimas 6 horas",
        "last12Hours": "Últimas 12 horas",
        "last24Hours": "Últimas 24 horas",
        "last7Days": "Últimos 7 días",
        "last30Days": "Últimos 30 días",
        "last6Months": "Últimos 6 meses",
        "lastYear": "Último año",
        "clear": "Limpiar"
      },
      "labels": {
        "from": "Desde",
        "to": "Hasta"
      }
    }
  }
}
```

**Portuguese (`/assets/i18n/pt.json`):**

```json
{
  "sdk": {
    "fileUpload": {
      "dropzone": {
        "selectPrompt": "Haz clic para seleccionar archivos o arrastra y suelta aquí",
        "maxSizeLabel": "Tamaño máximo:",
        "allowedTypesLabel": "Tipos permitidos:",
        "invalidFileType": "Tipo de archivo no válido",
        "fileTooLarge": "Archivo demasiado grande",
        "fileCounter": "{{current}} de {{max}} archivos",
        "maxFilesExceeded": "Máximo de {{max}} archivo(s) permitido",
        "noValidFiles": "No se han seleccionado archivos válidos",
        "fileActions": {
          "play": "Reproducir audio",
          "pause": "Pausar audio",
          "download": "Descargar archivo",
          "remove": "Quitar archivo"
        }
      },
      "fileUploader": {
        "allowedTypesLabel": "Tipos permitidos:"
      }
    },
    "audioPlayer": {
      "trackListTitle": "Trilhas",
      "trackInfo": {
        "title": "Sem título",
        "description": "Sem descrição"
      }
    },
    "countdown": {
      "days": "Dias",
      "hours": "Horas",
      "minutes": "Minutos",
      "seconds": "Segundos"
    },
    "dualList": {
      "searchPlaceHolder": "Pesquisar"
    },
    "combobox": {
      "searchPlaceholder": "Pesquisar",
      "placeholder": "Selecione",
      "noResultsFound": "Nenhum resultado encontrado",
      "clearAll": "Limpar Tudo",
      "clearSelection": "Limpar seleção",
      "searchOptions": "Pesquisar opções",
      "loading": "Carregando..."
    },
    "select": {
      "placeholder": "Selecione"
    },
    "formErrors": {
      "validation": {
        "required": "*Este campo é obrigatório",
        "email": "*Por favor, insira um endereço de e-mail válido",
        "minLength": "*Deve ter pelo menos {{min}} caracteres",
        "maxLength": "*Deve ter no máximo {{max}} caracteres",
        "min": "*O valor deve ser de pelo menos {{min}}",
        "max": "*O valor deve ser de no máximo {{max}}",
        "pattern": "*Formato inválido",
        "unhandledError": "*Erro não tratado"
      }
    },
    "textarea": {
      "maxCharacters": "{{current}} de {{max}} caracteres"
    },
    "table": {
      "entriesByPage": "Entradas por página",
      "perPage": "{{count}} / página",
      "totalEntries": "{{total}} entradas",
      "entriesRange": "{{range}} de {{total}} entradas",
      "emptyListMessage": "Nenhum item para mostrar"
    },
    "dataTable": {
      "entriesByPage": "Entradas por página",
      "perPage": "{{count}} / página",
      "totalEntries": "{{total}} entradas",
      "entriesRange": "{{range}} de {{total}} entradas",
      "emptyListMessage": "Nenhum item para mostrar",
      "noData": {
        "title": "Nenhum dado disponível",
        "message": "Não há registros para exibir neste momento."
      },
      "noResults": {
        "title": "Nenhum resultado encontrado",
        "message": "Tente ajustar seus critérios de pesquisa"
      }
    },
    "wizard": {
      "wizardMarker": {
        "completed": "Concluído",
        "inProgress": "Em andamento",
        "pending": "Pendente"
      },
      "wizardWrapper": {
        "back": "Voltar",
        "previous": "Anterior",
        "end": "Fim",
        "next": "Próximo",
        "step": "Etapa {{index}}"
      }
    },
    "datePicker": {
      "quickActions": {
        "title": "Seleção Rápida",
        "lastHour": "Última hora",
        "last3Hours": "Últimas 3 horas",
        "last6Hours": "Últimas 6 horas",
        "last12Hours": "Últimas 12 horas",
        "last24Hours": "Últimas 24 horas",
        "last7Days": "Últimos 7 dias",
        "last30Days": "Últimos 30 dias",
        "last6Months": "Últimos 6 meses",
        "lastYear": "Último ano",
        "clear": "Limpar"
      },
      "labels": {
        "from": "De",
        "to": "Até"
      }
    }
  }
}
```

### 3. Access Language Functionality in Components

The TranslationService automatically loads available languages from `translation.json`:

```typescript
// In any component
import { TranslationService } from '@sixbell-telco/sdk/utils/translation';
import { inject } from '@angular/core';

export class MyComponent {
  private translationService = inject(TranslationService);

  availableLanguages = this.translationService.getAvailableLanguagesConfig();
  currentLanguage = this.translationService.currentLanguage;

  setLanguage(languageCode: string) {
    this.translationService.setLanguage(languageCode);
  }

  getLanguageName(languageCode: string): string {
    return this.translationService.getLocalizedLanguageName(languageCode);
  }
}
```

### 4. Use Translations in Templates

Translations are automatically loaded and available through the `translate` pipe:

```html
<!-- Access any translation key -->
<h1>{{ 'ui.showcase' | translate }}</h1>

<!-- With parameters -->
<p>{{ 'welcome.message' | translate: { name: userName } }}</p>

<!-- In component -->
<select (change)="setLanguage($event.target.value)">
  @for (lang of availableLanguages; track lang.code) {
  <option [value]="lang.code">{{ getLanguageName(lang.code) }}</option>
  }
</select>
```

### 5. Add Custom Application Translations

Extend the translation files with your application-specific keys:

**English (`/assets/i18n/en.json`):**

```json
{
  "app": {
    "title": "My Application",
    "welcome": "Welcome to our app",
    "features": {
      "description": "Powerful features for your business"
    }
  },
  "sdk": {
    "formFields": {
      "optional": "Optional"
    }
  }
}
```

Your application is now configured with full multi-language support. Language preferences are automatically persisted and restored on return visits.

## Usage Examples

### Using Components

Here's a basic example of using a button component:

```typescript
// app.component.ts
import { ButtonComponent } from '@sixbell-telco/sdk/components/button';

@Component({
  imports: [ButtonComponent],
  template: `
    <div>
      <h1>Awesome button</h1>
      <st-button variant="primary">Click me!</st-button>
    </div>
  `
})
```

### Using Text Directive & Component

The **Text Directive** (`stText`) applies preset-based typography styles directly on any HTML element. The **Text Component** (`st-text`) is a polymorphic wrapper that renders the correct semantic HTML tag.

Both support class overrides — any Tailwind class you put in `class=""` will override the corresponding directive/component default via `tailwind-merge`.

#### Text Directive

```typescript
import { TextDirective } from '@sixbell-telco/sdk/directives/text';

@Component({
  imports: [TextDirective],
  template: `
    <!-- Basic presets (1 = largest heading, 10 = smallest body) -->
    <h1 stText="1">Hero Title</h1>
    <p stText="7">Body text (default preset)</p>
    <span stText="9">Small label</span>

    <!-- Override weight and color via inputs -->
    <p stText="7" stTextWeight="bold" stTextColor="primary">Bold primary text</p>

    <!-- Override any prop via class (wins over defaults) -->
    <p stText="7" class="text-error font-semibold">Error text with semibold weight</p>
    <p stText="7" class="text-3xl leading-loose">Larger size and loose line-height</p>

    <!-- Line clamp -->
    <p stText="7" stTextLineClamp="2">Long text clamped to 2 lines...</p>
  `
})
```

**Available inputs:**

| Input             | Default     | Options                                                                             |
| ----------------- | ----------- | ----------------------------------------------------------------------------------- |
| `stText`          | `'7'`       | `'1'`–`'10'` (preset scale)                                                         |
| `stTextWeight`    | `'normal'`  | `'bold'`, `'semibold'`, `'medium'`, `'normal'`, `'light'`, `'inherit'`              |
| `stTextColor`     | `'inherit'` | `'base'`, `'base-placeholder'`, `'primary'`, `'secondary'`, `'accent'`, `'inherit'` |
| `stTextOverflow`  | `'clip'`    | `'clip'`, `'ellipsis'`, `'truncate'`                                                |
| `stTextLineClamp` | `undefined` | `'1'`–`'6'`, `'none'`                                                               |

**Class overrides:** Any Tailwind utility in `class=""` overrides the corresponding directive default (color, weight, font-size, line-height, tracking, etc.) via `tailwind-merge`.

#### Text Component

```typescript
import { TextComponent } from '@sixbell-telco/sdk/components/text';

@Component({
  imports: [TextComponent],
  template: `
    <!-- Semantic tags with presets -->
    <st-text as="h1" preset="1">Hero Title</st-text>
    <st-text as="h2" preset="2">Page Title</st-text>
    <st-text as="p" preset="7">Body paragraph</st-text>
    <st-text as="span" preset="9" color="primary">Inline label</st-text>

    <!-- Override via inputs -->
    <st-text as="p" preset="7" weight="bold" color="accent">Bold accent text</st-text>

    <!-- Override via class (wins over defaults) -->
    <st-text as="p" preset="7" class="text-error font-medium">Custom error text</st-text>
    <st-text as="h1" preset="1" class="mb-4 border-b border-accent pb-2">Heading with border</st-text>
  `
})
```

**Available inputs:**

| Input       | Default     | Options                                                                             |
| ----------- | ----------- | ----------------------------------------------------------------------------------- |
| `as`        | `'p'`       | `'h1'`–`'h6'`, `'p'`, `'span'`, `'div'`, `'label'`                                  |
| `preset`    | `'7'`       | `'1'`–`'10'`                                                                        |
| `weight`    | `undefined` | `'bold'`, `'semibold'`, `'medium'`, `'normal'`, `'light'`, `'inherit'`              |
| `color`     | `'base'`    | `'base'`, `'base-placeholder'`, `'primary'`, `'secondary'`, `'accent'`, `'inherit'` |
| `overflow`  | `'clip'`    | `'clip'`, `'ellipsis'`, `'truncate'`                                                |
| `lineClamp` | `undefined` | `'1'`–`'6'`, `'none'`                                                               |

### Using Services in Components

```typescript
// component.ts
import { ThemeService } from '@sixbell-telco/sdk/utils/theme';
import { TranslationService } from '@sixbell-telco/sdk/utils/translation';

@Component({
  template: `
    <div [class]="themeService.finalTheme()">
      <h1>{{ 'welcome.title' | translate }}</h1>
      <p>Current theme: {{ themeService.selectedTheme() }}</p>
      <p>Dark mode: {{ themeService.isDarkTheme() ? 'Yes' : 'No' }}</p>

      <!-- Theme selector -->
      <select (change)="changeTheme($event.target.value)">
        @for (theme of themeService.getAvailableThemes(); track theme) {
          <option [value]="theme">{{ theme }}</option>
        }
      </select>

      <!-- Language selector -->
      <select (change)="changeLanguage($event.target.value)">
        @for (lang of translationService.getAvailableLanguagesConfig(); track lang.code) {
          <option [value]="lang.code">{{ translationService.getLocalizedLanguageName(lang.code) }}</option>
        }
      </select>
    </div>
  `,
})
export class MyComponent {
  themeService = inject(ThemeService);
  translationService = inject(TranslationService);

  changeTheme(theme: string) {
    this.themeService.setTheme(theme);
  }

  changeLanguage(language: string) {
    this.translationService.setLanguage(language);
  }
}
```

## Quick Start

### Using Components

```typescript
import { Component } from '@angular/core';
import { ButtonComponent } from '@sixbell-telco/sdk/components/button';
import {
  CardActionsComponent,
  CardBodyComponent,
  CardComponent,
  CardFooterComponent,
  CardHeaderComponent,
  CardSectionBodyComponent,
  CardSectionComponent,
  CardSectionGroupComponent,
  CardSubtitleComponent,
  CardTitleComponent,
} from '@sixbell-telco/sdk/components/card';

@Component({
  selector: 'app-example',
  standalone: true,
  imports: [
    ButtonComponent,
    CardActionsComponent,
    CardBodyComponent,
    CardComponent,
    CardFooterComponent,
    CardHeaderComponent,
    CardSectionBodyComponent,
    CardSectionComponent,
    CardSectionGroupComponent,
    CardSubtitleComponent,
    CardTitleComponent,
  ],
  template: `
    <st-card>
      <st-card-header>
        <st-card-title>Hello World</st-card-title>
        <st-card-subtitle>Small supporting text</st-card-subtitle>
      </st-card-header>
      <st-card-body>
        <st-card-section-group [divided]="true">
          <st-card-section>
            <st-card-section-body>Welcome to the new compound card API.</st-card-section-body>
          </st-card-section>
        </st-card-section-group>
      </st-card-body>
      <st-card-footer [divided]="true">
        <st-card-actions>
          <st-button variant="primary">Click me!</st-button>
        </st-card-actions>
      </st-card-footer>
    </st-card>
  `,
})
export class ExampleComponent {}
```

### Using Form Components

```typescript
import { Component, signal } from '@angular/core';
import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms';
import { InputComponent } from '@sixbell-telco/sdk/components/forms/input';
import { ButtonComponent } from '@sixbell-telco/sdk/components/button';

@Component({
  selector: 'app-form-example',
  standalone: true,
  imports: [InputComponent, ButtonComponent, ReactiveFormsModule],
  template: `
    <form [formGroup]="form">
      <st-input formControlName="email" placeholder="Enter email"></st-input>
      <st-button (click)="submit()">Submit</st-button>
    </form>
  `,
})
export class FormExampleComponent {
  form = new FormGroup({
    email: new FormControl(''),
  });

  submit() {
    console.log(this.form.value);
  }
}
```

### Using Services

```typescript
import { Component, inject, effect } from '@angular/core';
import { ThemeService } from '@sixbell-telco/sdk/utils/theme';
import { TranslationService } from '@sixbell-telco/sdk/utils/translation';
import { LoggerService } from '@sixbell-telco/sdk/utils/logger';

@Component({
  selector: 'app-services-example',
  template: `
    <div>
      <p>Current Theme: {{ themeService.selectedTheme() }}</p>
      <p>Dark Mode: {{ themeService.isDarkTheme() ? 'Yes' : 'No' }}</p>
      <button (click)="toggleTheme()">Toggle Theme</button>
    </div>
  `,
})
export class ServicesExampleComponent {
  themeService = inject(ThemeService);
  translationService = inject(TranslationService);
  logger = inject(LoggerService);

  constructor() {
    effect(() => {
      this.logger.info(`Theme changed to: ${this.themeService.selectedTheme()}`);
    });
  }

  toggleTheme() {
    const newScheme = this.themeService.isDarkTheme() ? 'light' : 'dark';
    this.themeService.setScheme(newScheme);
  }
}
```

## Configuration

### Theme Configuration

Runtime themes are loaded from `themes.json`. See [Theme Service README](utils/theme/) for detailed configuration.

### Translation Configuration

Runtime translations are loaded from `translation.json`. See [Translation Service README](utils/translation/) for detailed configuration.

### Logger Configuration

Configure logging levels and output. See [Logger Service README](utils/logger/) for detailed options.

## Architecture

### Multi-Entry Point Imports

Always use specific imports for optimal tree-shaking:

```typescript
// ✅ Correct - specific imports
import { ButtonComponent } from '@sixbell-telco/sdk/components/button';
import { ThemeService } from '@sixbell-telco/sdk/utils/theme';

// ❌ Avoid - barrel exports
import { ButtonComponent } from '@sixbell-telco/sdk';
```

### Component Prefix

All SDK components use the `st-` prefix:

```html
<st-button>Button</st-button>
<st-input type="text" />
<st-card>Card Content</st-card>
<st-divider>OR</st-divider>
```

### Signal-Based API

Components use Angular 19 signals for reactive inputs:

```typescript
variant = input<'primary' | 'secondary'>();
isDisabled = input(false);

@Component({
  template: `<button [disabled]="isDisabled()">{{ variant() }}</button>`,
})
```

## Testing

The SDK includes Jest configuration. Run tests with:

```bash
npm test                # Run all tests
npm run test:watch    # Watch mode
npm run test:coverage # Coverage report
```

See `.agents/skills/angular-testing-jest-ngmocks/SKILL.md` for Angular 19 testing patterns.

## Documentation

- **Component Stories**: View component documentation in [Storybook](../storybook/README.md)
- **Live Examples**: Showcase app at `projects/showcase/`
- **API Documentation**: JSDoc comments in component files
- **Changelog**: See [CHANGELOG.md](./CHANGELOG.md)

## Agent skills release scope

The SDK skill at `.agents/skills/sixbell-sdk/` follows the same lifecycle as `projects/sdk/CHANGELOG.md`:

1. During active development, the skill metadata must point to `Unreleased`.
2. When a release is created, update the skill metadata to the released version or tag.
3. When development starts again, switch the skill metadata back to `Unreleased`.

This keeps the skill aligned with the current codebase instead of the last published package.

## Contributing

When creating new components:

1. Use `st-` prefix for selectors
2. Follow CVA pattern for styling variants
3. Use signal-based inputs
4. Add comprehensive JSDoc comments
5. Create Storybook stories
6. Add unit tests with Jest
7. Update CHANGELOG.md

## Resources

- <a href="https://angular.dev" target="_blank" rel="noopener noreferrer">Angular Documentation</a>
- <a href="https://tailwindcss.com" target="_blank" rel="noopener noreferrer">Tailwind CSS</a>
- <a href="https://daisyui.com" target="_blank" rel="noopener noreferrer">DaisyUI</a>
- <a href="https://cva.style" target="_blank" rel="noopener noreferrer">class-variance-authority</a>
- <a href="https://ng-icons.herokuapp.com" target="_blank" rel="noopener noreferrer">ng-icons</a>
- <a href="https://material.angular.io/cdk" target="_blank" rel="noopener noreferrer">Angular CDK</a>

## License

MIT - See [LICENSE](./LICENSE) file for details

## Support

For issues, questions, or feature requests, please visit the project repository or contact the Sixbell Telco team.

---

**Last updated**: March 19, 2026
