feat(checkout): add reward order confirmation feature with schema migrations

- Add new reward-order-confirmation feature library with components and store
- Implement checkout completion orchestrator service for order finalization
- Migrate checkout/oms/crm models to Zod schemas for better type safety
- Add order creation facade and display order schemas
- Update shopping cart facade with order completion flow
- Add comprehensive tests for shopping cart facade
- Update routing to include order confirmation page
This commit is contained in:
Lorenz Hilpert
2025-10-21 14:28:52 +02:00
parent b96d889da5
commit 2b5da00249
259 changed files with 61347 additions and 2652 deletions

View File

@@ -1,7 +1,503 @@
# ui-bullet-list
# @isa/ui/bullet-list
This library was generated with [Nx](https://nx.dev).
A lightweight bullet list component system for Angular applications supporting customizable icons and hierarchical content presentation.
## Running unit tests
## Overview
Run `nx test ui-bullet-list` to execute the unit tests.
The Bullet List library provides a pair of components (`ui-bullet-list` and `ui-bullet-list-item`) that work together to create visually consistent bullet-point lists with icon support. The system uses Angular's host-based component injection to propagate icon configuration from parent to children, enabling both uniform and customized list appearances.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [Component API](#component-api)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Architecture Notes](#architecture-notes)
- [Testing](#testing)
- [Dependencies](#dependencies)
## Features
- **Parent-child icon inheritance** - Default icon flows from list to items
- **Per-item icon override** - Individual items can specify custom icons
- **Angular signals integration** - Reactive icon computation with `computed()`
- **Standalone components** - Modern Angular architecture with explicit imports
- **Icon library integration** - Uses `@ng-icons/core` with ISA icon set
- **OnPush change detection** - Optimized performance
- **Host-based injection** - Seamless parent-child communication
- **Content projection** - Flexible content rendering via `<ng-content>`
## Quick Start
### 1. Import Components
```typescript
import { Component } from '@angular/core';
import { BulletListComponent, BulletListItemComponent } from '@isa/ui/bullet-list';
@Component({
selector: 'app-feature-list',
imports: [BulletListComponent, BulletListItemComponent],
template: `...`
})
export class FeatureListComponent {}
```
### 2. Basic Usage
```html
<ui-bullet-list>
<ui-bullet-list-item>First feature</ui-bullet-list-item>
<ui-bullet-list-item>Second feature</ui-bullet-list-item>
<ui-bullet-list-item>Third feature</ui-bullet-list-item>
</ui-bullet-list>
```
### 3. Custom Icons
```html
<!-- Custom icon for entire list -->
<ui-bullet-list [icon]="'isaActionCheck'">
<ui-bullet-list-item>Completed task</ui-bullet-list-item>
<ui-bullet-list-item>Another completed task</ui-bullet-list-item>
</ui-bullet-list>
<!-- Mixed icons - per-item override -->
<ui-bullet-list>
<ui-bullet-list-item [icon]="'isaActionCheck'">Done</ui-bullet-list-item>
<ui-bullet-list-item [icon]="'isaActionClock'">In progress</ui-bullet-list-item>
<ui-bullet-list-item>Default icon</ui-bullet-list-item>
</ui-bullet-list>
```
## Component API
### BulletListComponent
Container component for bullet list items.
#### Selector
```typescript
'ui-bullet-list'
```
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `icon` | `string` | `'isaActionChevronRight'` | Default icon name for all child items |
#### Host Bindings
- `class`: `'ui-bullet-list'` - Base CSS class for styling
#### Template
Content projection only - wraps child items:
```html
<ng-content></ng-content>
```
#### Providers
Automatically provides `isaActionChevronRight` icon to the icon registry.
---
### BulletListItemComponent
Individual item within a bullet list.
#### Selector
```typescript
'ui-bullet-list-item'
```
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `icon` | `string \| undefined` | `undefined` | Custom icon override (uses parent icon if undefined) |
#### Computed Properties
| Property | Type | Description |
|----------|------|-------------|
| `iconName()` | `string \| undefined` | Computed icon name - resolves to item icon or falls back to parent icon |
#### Host Bindings
- `class`: `'ui-bullet-list-item'` - Base CSS class for styling
#### Template
```html
<ng-icon [name]="iconName()" size="1.5rem"></ng-icon>
<ng-content></ng-content>
```
## Usage Examples
### Basic Feature List
```typescript
import { Component } from '@angular/core';
import { BulletListComponent, BulletListItemComponent } from '@isa/ui/bullet-list';
@Component({
selector: 'app-product-features',
imports: [BulletListComponent, BulletListItemComponent],
template: `
<h2>Product Features</h2>
<ui-bullet-list>
<ui-bullet-list-item>Fast performance</ui-bullet-list-item>
<ui-bullet-list-item>Easy to use</ui-bullet-list-item>
<ui-bullet-list-item>Highly customizable</ui-bullet-list-item>
</ui-bullet-list>
`
})
export class ProductFeaturesComponent {}
```
### Custom List Icon
```typescript
@Component({
selector: 'app-task-list',
imports: [BulletListComponent, BulletListItemComponent],
template: `
<h2>Completed Tasks</h2>
<ui-bullet-list [icon]="'isaActionCheck'">
<ui-bullet-list-item>Project setup complete</ui-bullet-list-item>
<ui-bullet-list-item>Tests passing</ui-bullet-list-item>
<ui-bullet-list-item>Documentation updated</ui-bullet-list-item>
</ui-bullet-list>
`
})
export class TaskListComponent {}
```
### Mixed Icons with Status Indicators
```typescript
@Component({
selector: 'app-status-list',
imports: [BulletListComponent, BulletListItemComponent],
template: `
<h2>Task Status</h2>
<ui-bullet-list>
<ui-bullet-list-item [icon]="'isaActionCheck'">
Authentication implemented
</ui-bullet-list-item>
<ui-bullet-list-item [icon]="'isaActionClock'">
API integration in progress
</ui-bullet-list-item>
<ui-bullet-list-item [icon]="'isaActionWarning'">
Performance testing pending
</ui-bullet-list-item>
</ui-bullet-list>
`
})
export class StatusListComponent {}
```
### Dynamic Icon Selection
```typescript
import { Component, signal } from '@angular/core';
import { BulletListComponent, BulletListItemComponent } from '@isa/ui/bullet-list';
@Component({
selector: 'app-dynamic-list',
imports: [BulletListComponent, BulletListItemComponent],
template: `
<ui-bullet-list [icon]="currentIcon()">
<ui-bullet-list-item>Item 1</ui-bullet-list-item>
<ui-bullet-list-item>Item 2</ui-bullet-list-item>
<ui-bullet-list-item>Item 3</ui-bullet-list-item>
</ui-bullet-list>
<button (click)="changeIcon()">Toggle Icon</button>
`
})
export class DynamicListComponent {
currentIcon = signal('isaActionChevronRight');
changeIcon() {
const newIcon = this.currentIcon() === 'isaActionChevronRight'
? 'isaActionCheck'
: 'isaActionChevronRight';
this.currentIcon.set(newIcon);
}
}
```
### Nested Content with Rich HTML
```typescript
@Component({
selector: 'app-rich-list',
imports: [BulletListComponent, BulletListItemComponent],
template: `
<ui-bullet-list>
<ui-bullet-list-item>
<strong>Bold heading:</strong> Regular text description
</ui-bullet-list-item>
<ui-bullet-list-item>
<a href="/docs">Link to documentation</a>
</ui-bullet-list-item>
<ui-bullet-list-item>
<span class="highlight">Highlighted content</span> with normal text
</ui-bullet-list-item>
</ui-bullet-list>
`
})
export class RichListComponent {}
```
### Conditional Items with Control Flow
```typescript
import { Component, signal } from '@angular/core';
import { BulletListComponent, BulletListItemComponent } from '@isa/ui/bullet-list';
@Component({
selector: 'app-conditional-list',
imports: [BulletListComponent, BulletListItemComponent],
template: `
<ui-bullet-list>
<ui-bullet-list-item>Always visible</ui-bullet-list-item>
@if (showAdditional()) {
<ui-bullet-list-item>Conditional item 1</ui-bullet-list-item>
<ui-bullet-list-item>Conditional item 2</ui-bullet-list-item>
}
@for (item of dynamicItems(); track item.id) {
<ui-bullet-list-item [icon]="item.icon">
{{ item.text }}
</ui-bullet-list-item>
}
</ui-bullet-list>
`
})
export class ConditionalListComponent {
showAdditional = signal(true);
dynamicItems = signal([
{ id: 1, text: 'Dynamic 1', icon: 'isaActionCheck' },
{ id: 2, text: 'Dynamic 2', icon: 'isaActionClock' }
]);
}
```
## Styling and Customization
### CSS Classes
The components expose the following CSS classes for styling:
```css
/* List container */
.ui-bullet-list {
/* Add spacing, layout, etc. */
}
/* Individual list items */
.ui-bullet-list-item {
/* Style item layout */
}
/* Icon within items (via ng-icon) */
.ui-bullet-list-item ng-icon {
/* Customize icon appearance */
}
```
### Example Styling
```scss
// Custom spacing and layout
.ui-bullet-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.ui-bullet-list-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
ng-icon {
flex-shrink: 0;
margin-top: 0.125rem; // Align with text
color: var(--isa-accent-500);
}
}
// Status-specific styling
.ui-bullet-list-item {
&.success ng-icon {
color: var(--isa-success-500);
}
&.warning ng-icon {
color: var(--isa-warning-500);
}
&.error ng-icon {
color: var(--isa-error-500);
}
}
```
### Icon Size Customization
The default icon size is `1.5rem`. To customize globally:
```scss
.ui-bullet-list-item ng-icon {
font-size: 1.25rem; // Smaller icons
width: 1.25rem;
height: 1.25rem;
}
```
## Architecture Notes
### Parent-Child Communication Pattern
The library uses Angular's dependency injection with the `host` flag to enable seamless communication:
```typescript
// In BulletListItemComponent
#bulletList = inject(BulletListComponent, { optional: true, host: true });
```
This pattern:
- Only injects the **direct parent** `BulletListComponent`
- Returns `null` if no parent exists (via `optional: true`)
- Enables icon inheritance without prop drilling
### Icon Resolution Logic
```typescript
iconName = computed(() => {
return this.icon() ?? this.#bulletList?.icon();
});
```
Resolution order:
1. Use item's own `icon` input if provided
2. Fall back to parent list's `icon` input
3. Fall back to default `'isaActionChevronRight'`
### Change Detection Strategy
Both components use `OnPush` change detection for optimal performance:
- Changes only trigger when inputs change
- Computed signals automatically track dependencies
- No manual change detection needed
### Standalone Architecture
Components are fully standalone with explicit imports:
```typescript
imports: [NgIcon] // BulletListItemComponent
imports: [] // BulletListComponent (no dependencies)
```
## Testing
The library uses **Vitest** with **Angular Testing Utilities** for testing.
### Running Tests
```bash
# Run tests for this library
npx nx test ui-bullet-list --skip-nx-cache
# Run tests with coverage
npx nx test ui-bullet-list --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-bullet-list --watch
```
### Test Coverage
The library includes comprehensive tests covering:
- **Component rendering** - Verifies components render correctly
- **Icon inheritance** - Tests parent-to-child icon propagation
- **Icon override** - Tests per-item icon customization
- **Content projection** - Validates ng-content rendering
- **Host injection** - Tests optional host injection behavior
- **Signal reactivity** - Tests computed signal updates
### Example Test
```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { BulletListComponent, BulletListItemComponent } from '@isa/ui/bullet-list';
describe('BulletListItemComponent', () => {
let component: BulletListItemComponent;
let fixture: ComponentFixture<BulletListItemComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BulletListItemComponent, BulletListComponent]
});
fixture = TestBed.createComponent(BulletListItemComponent);
component = fixture.componentInstance;
});
it('should use parent icon when no icon provided', () => {
// Arrange: Create parent list with custom icon
const parentFixture = TestBed.createComponent(BulletListComponent);
parentFixture.componentInstance.icon.set('isaActionCheck');
// Act: Create child item
const childFixture = TestBed.createComponent(BulletListItemComponent);
// Assert: Child should inherit parent icon
expect(childFixture.componentInstance.iconName()).toBe('isaActionCheck');
});
it('should override parent icon when icon input provided', () => {
// Arrange & Act
component.icon.set('isaActionWarning');
fixture.detectChanges();
// Assert
expect(component.iconName()).toBe('isaActionWarning');
});
});
```
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework (v20.1.2)
- `@ng-icons/core` - Icon component library
- `@isa/icons` - ISA icon set (provides `isaActionChevronRight`)
### Path Alias
Import from: `@isa/ui/bullet-list`
### Peer Dependencies
The components require the icon library to be properly configured at the application level. Ensure `@ng-icons/core` is installed and configured.
## Best Practices
1. **Consistent Icon Usage** - Use the same icon set throughout your application for visual consistency
2. **Semantic Icons** - Choose icons that match the context (check marks for completed items, chevrons for navigation)
3. **Accessibility** - Ensure icon meanings are clear from surrounding text
4. **Performance** - The OnPush strategy means you can safely use these components in large lists
5. **Content Structure** - Keep list items concise; use nested HTML for complex content
6. **Icon Registration** - Only icons used in the list need to be registered (parent component handles default)
## License
Internal ISA Frontend library - not for external distribution.

View File

@@ -1,7 +1,966 @@
# ui-buttons
# @isa/ui/buttons
This library was generated with [Nx](https://nx.dev).
A comprehensive button component library for Angular applications providing five specialized button components with consistent styling, loading states, and accessibility features.
## Running unit tests
## Overview
Run `nx test ui-buttons` to execute the unit tests.
The UI Buttons library provides a complete suite of button components for the ISA application, each optimized for specific use cases. All components leverage Angular signals for reactive state management, support customizable sizes and colors, and follow the ISA design system. The library includes standard buttons, text/link-style buttons, icon-only buttons, info/help buttons, and stateful buttons with success/error states and auto-dismiss functionality.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Accessibility](#accessibility)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)
## Features
- **Five specialized button components** - Standard, text, icon, info, and stateful buttons
- **Reactive signal-based state** - All inputs and computed properties use Angular signals
- **Loading states** - Built-in pending/loading indicators with spinners
- **Customizable sizing** - Small, medium, and large sizes for all button types
- **Theme support** - Multiple color schemes (primary, secondary, brand, tertiary, neutral)
- **Stateful feedback** - Success/error states with configurable auto-dismiss
- **Disabled state handling** - Proper disabled styling and ARIA attributes
- **Keyboard navigation** - Full keyboard accessibility with tab index support
- **OnPush change detection** - Optimized performance with minimal re-renders
- **No encapsulation** - View encapsulation disabled for flexible styling
- **Animation support** - Smooth transitions for stateful button state changes
## Quick Start
### 1. Import Components
```typescript
import { Component } from '@angular/core';
import {
ButtonComponent,
TextButtonComponent,
IconButtonComponent,
InfoButtonComponent,
StatefulButtonComponent
} from '@isa/ui/buttons';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [
ButtonComponent,
TextButtonComponent,
IconButtonComponent,
InfoButtonComponent,
StatefulButtonComponent
],
template: '...'
})
export class MyComponent {}
```
### 2. Use Standard Button
```html
<button uiButton color="primary" size="large" [pending]="isLoading">
Submit Form
</button>
```
### 3. Use Text Button (Link Style)
```html
<button uiTextButton color="strong" (click)="handleClick()">
Learn More
</button>
```
### 4. Use Icon Button
```html
<button uiIconButton
name="isaActionEdit"
color="primary"
size="large"
[pending]="isSaving">
</button>
```
### 5. Use Info Button
```html
<button uiInfoButton [disabled]="false">
<ng-icon name="isaActionInfo"></ng-icon>
</button>
```
### 6. Use Stateful Button
```html
<ui-stateful-button
[(state)]="buttonState"
defaultContent="Save Changes"
successContent="Changes Saved!"
errorContent="Save Failed"
errorAction="Retry"
color="primary"
[dismiss]="3000"
(clicked)="handleSave()"
(action)="handleRetry()">
</ui-stateful-button>
```
## Core Concepts
### Button Component Types
The library provides five specialized button components, each optimized for specific use cases:
#### 1. ButtonComponent (Standard Button)
- **Purpose**: Primary action buttons for forms, dialogs, and main interactions
- **Selector**: `ui-button, [uiButton]`
- **Key Features**:
- Customizable size (small, medium, large)
- Four color themes (primary, secondary, brand, tertiary)
- Loading spinner overlay in pending state
- Preserves button dimensions during loading
- Disabled state support
#### 2. TextButtonComponent (Link-Style Button)
- **Purpose**: Secondary actions, navigation links, and lightweight interactions
- **Selector**: `ui-text-button, [uiTextButton]`
- **Key Features**:
- Text-based styling (no background)
- Three color themes (normal, strong, subtle)
- Pending state with inline spinner
- Smaller footprint than standard buttons
#### 3. IconButtonComponent (Icon-Only Button)
- **Purpose**: Compact actions, toolbar buttons, and icon-based interactions
- **Selector**: `ui-icon-button, [uiIconButton]`
- **Key Features**:
- Icon-only display with configurable icon name
- Five color themes (primary, secondary, brand, tertiary, neutral)
- Three sizes with responsive icon sizing
- Automatic loading icon replacement
- Enhanced ARIA attributes for accessibility
#### 4. InfoButtonComponent (Help/Info Button)
- **Purpose**: Help buttons, tooltips triggers, and informational actions
- **Selector**: `ui-info-button, [uiInfoButton]`
- **Key Features**:
- Fixed styling optimized for info icons
- Pending and disabled states
- Minimal visual footprint
- Designed for use with tooltip directives
#### 5. StatefulButtonComponent (Success/Error States)
- **Purpose**: Form submissions, save actions, and operations requiring feedback
- **Selector**: `ui-stateful-button`
- **Key Features**:
- Three states: default, success, error
- Configurable content for each state
- Auto-dismiss with configurable timeout
- Width animations during state transitions
- Optional error action button
- Two-way binding for state management
### Signal-Based Reactivity
All button components use Angular signals for reactive state management:
```typescript
// Input signals
size = input<ButtonSize>('medium');
color = input<ButtonColor>('primary');
pending = input<boolean>(false);
disabled = input<boolean>(false);
// Computed signals
sizeClass = computed(() => `ui-button__${this.size()}`);
colorClass = computed(() => `ui-button__${this.color()}`);
pendingClass = computed(() => this.pending() ? 'ui-button__pending' : '');
disabledClass = computed(() => this.disabled() ? 'disabled' : '');
```
### Host Binding Pattern
Components use Angular host binding for dynamic CSS classes:
```typescript
host: {
'[class]': '["ui-button", sizeClass(), colorClass(), pendingClass(), disabledClass()]',
'[tabindex]': 'tabIndex()',
'[disabled]': 'disabled()',
}
```
### Loading State Handling
All button components support loading states with different rendering strategies:
- **Standard Button**: Overlay spinner preserving button dimensions
- **Text Button**: Inline spinner replacing content
- **Icon Button**: Replaces icon with loading icon
- **Info Button**: Overlay spinner
- **Stateful Button**: Inherits from standard button behavior
## API Reference
### ButtonComponent
Standard button component with customizable appearance and loading state.
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `size` | `ButtonSize` | `'medium'` | Button size: 'small', 'medium', or 'large' |
| `color` | `ButtonColor` | `'primary'` | Color theme: 'primary', 'secondary', 'brand', or 'tertiary' |
| `pending` | `boolean` | `false` | Shows loading spinner when true |
| `disabled` | `boolean` | `false` | Disables button when true |
| `tabIndex` | `number` | `0` | Tab index for keyboard navigation |
#### Example
```html
<button uiButton
color="brand"
size="large"
[pending]="isLoading"
[disabled]="!isValid"
(click)="handleSubmit()">
Submit Form
</button>
```
### TextButtonComponent
Link-style button for secondary actions and navigation.
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `size` | `TextButtonSize` | `'medium'` | Button size: 'small', 'medium', or 'large' |
| `color` | `TextButtonColor` | `'normal'` | Color theme: 'normal', 'strong', or 'subtle' |
| `pending` | `boolean` | `false` | Shows loading spinner when true |
| `disabled` | `boolean` | `false` | Disables button when true |
| `tabIndex` | `number` | `0` | Tab index for keyboard navigation |
#### Example
```html
<button uiTextButton
color="strong"
size="medium"
(click)="handleCancel()">
Cancel
</button>
```
### IconButtonComponent
Icon-only button for compact actions and toolbars.
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | `string` | `undefined` | Icon name from @isa/icons |
| `size` | `IconButtonSize` | `'large'` | Button size: 'small', 'medium', or 'large' |
| `color` | `IconButtonColor` | `'primary'` | Color theme: 'primary', 'secondary', 'brand', 'tertiary', or 'neutral' |
| `pending` | `boolean` | `false` | Shows loading icon when true |
| `disabled` | `boolean` | `false` | Disables button when true |
| `tabIndex` | `number` | `0` | Tab index for keyboard navigation |
#### Computed Properties
- `iconName`: Returns 'isaLoading' when pending, otherwise returns the input name
- `iconSize`: Returns computed icon size ('1rem', '1.25rem', or '1.5rem') based on button size
#### Example
```html
<button uiIconButton
name="isaActionEdit"
color="primary"
size="large"
[pending]="isSaving"
[disabled]="!canEdit"
(click)="handleEdit()">
</button>
```
### InfoButtonComponent
Help/info button optimized for tooltips and informational content.
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `pending` | `boolean` | `false` | Shows loading spinner when true |
| `disabled` | `boolean` | `false` | Disables button when true |
| `tabIndex` | `number` | `0` | Tab index for keyboard navigation |
#### Example
```html
<button uiInfoButton
[disabled]="false"
(click)="showHelp()">
<ng-icon name="isaActionInfo"></ng-icon>
</button>
```
### StatefulButtonComponent
Button with success/error states and auto-dismiss functionality.
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `defaultContent` | `string` | required | Content displayed in default state |
| `successContent` | `string` | required | Content displayed in success state |
| `errorContent` | `string` | required | Content displayed in error state |
| `errorAction` | `string` | `undefined` | Optional action button text in error state |
| `defaultWidth` | `string` | `'100%'` | Button width in default state |
| `successWidth` | `string` | `'100%'` | Button width in success state |
| `errorWidth` | `string` | `'100%'` | Button width in error state |
| `dismiss` | `number` | `5000` | Auto-dismiss timeout in milliseconds (0 to disable) |
| `color` | `ButtonColor` | `'primary'` | Color theme |
| `size` | `ButtonSize` | `'medium'` | Button size |
| `pending` | `boolean` | `false` | Shows loading spinner when true |
| `disabled` | `boolean` | `false` | Disables button when true |
#### Outputs
| Output | Type | Description |
|--------|------|-------------|
| `clicked` | `void` | Emitted when button is clicked in default state |
| `action` | `void` | Emitted when error action button is clicked |
#### Two-Way Binding
| Property | Type | Description |
|----------|------|-------------|
| `state` | `StatefulButtonState` | Current state: 'default', 'success', or 'error' |
#### Example
```typescript
import { Component, signal } from '@angular/core';
import { StatefulButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-save-form',
standalone: true,
imports: [StatefulButtonComponent],
template: `
<ui-stateful-button
[(state)]="saveState"
defaultContent="Save Changes"
successContent="Changes Saved Successfully!"
errorContent="Failed to Save Changes"
errorAction="Retry"
color="primary"
size="large"
[dismiss]="3000"
[pending]="isSaving()"
(clicked)="handleSave()"
(action)="handleRetry()">
</ui-stateful-button>
`
})
export class SaveFormComponent {
saveState = signal<'default' | 'success' | 'error'>('default');
isSaving = signal(false);
async handleSave(): Promise<void> {
this.isSaving.set(true);
try {
await this.saveFormData();
this.saveState.set('success');
} catch (error) {
this.saveState.set('error');
} finally {
this.isSaving.set(false);
}
}
handleRetry(): void {
this.saveState.set('default');
this.handleSave();
}
}
```
## Usage Examples
### Standard Button with Loading State
```typescript
import { Component, signal } from '@angular/core';
import { ButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-submit-form',
standalone: true,
imports: [ButtonComponent],
template: `
<button uiButton
color="primary"
size="large"
[pending]="isSubmitting()"
[disabled]="!isFormValid()"
(click)="handleSubmit()">
Submit Application
</button>
`
})
export class SubmitFormComponent {
isSubmitting = signal(false);
isFormValid = signal(true);
async handleSubmit(): Promise<void> {
this.isSubmitting.set(true);
try {
await this.submitForm();
} finally {
this.isSubmitting.set(false);
}
}
}
```
### Text Button for Navigation
```typescript
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { TextButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-navigation-links',
standalone: true,
imports: [TextButtonComponent],
template: `
<nav>
<button uiTextButton color="strong" (click)="navigateHome()">
Home
</button>
<button uiTextButton color="normal" (click)="navigateAbout()">
About
</button>
<button uiTextButton color="subtle" (click)="navigateHelp()">
Help
</button>
</nav>
`
})
export class NavigationLinksComponent {
constructor(private router: Router) {}
navigateHome(): void {
this.router.navigate(['/']);
}
navigateAbout(): void {
this.router.navigate(['/about']);
}
navigateHelp(): void {
this.router.navigate(['/help']);
}
}
```
### Icon Button Toolbar
```typescript
import { Component, signal } from '@angular/core';
import { IconButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-editor-toolbar',
standalone: true,
imports: [IconButtonComponent],
template: `
<div class="toolbar">
<button uiIconButton
name="isaActionEdit"
color="primary"
size="medium"
[disabled]="!canEdit()"
(click)="handleEdit()">
</button>
<button uiIconButton
name="isaActionDelete"
color="tertiary"
size="medium"
[disabled]="!canDelete()"
(click)="handleDelete()">
</button>
<button uiIconButton
name="isaActionSave"
color="brand"
size="medium"
[pending]="isSaving()"
(click)="handleSave()">
</button>
</div>
`
})
export class EditorToolbarComponent {
canEdit = signal(true);
canDelete = signal(true);
isSaving = signal(false);
handleEdit(): void {
console.log('Edit clicked');
}
handleDelete(): void {
console.log('Delete clicked');
}
async handleSave(): Promise<void> {
this.isSaving.set(true);
try {
await this.saveDocument();
} finally {
this.isSaving.set(false);
}
}
}
```
### Mixed Button Types in Dialog
```typescript
import { Component, signal } from '@angular/core';
import {
ButtonComponent,
TextButtonComponent,
IconButtonComponent
} from '@isa/ui/buttons';
@Component({
selector: 'app-confirmation-dialog',
standalone: true,
imports: [ButtonComponent, TextButtonComponent, IconButtonComponent],
template: `
<div class="dialog">
<div class="dialog-header">
<h2>Confirm Deletion</h2>
<button uiIconButton
name="isaActionClose"
color="neutral"
size="small"
(click)="handleClose()">
</button>
</div>
<div class="dialog-content">
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
</div>
<div class="dialog-actions">
<button uiTextButton
color="subtle"
(click)="handleCancel()">
Cancel
</button>
<button uiButton
color="tertiary"
[pending]="isDeleting()"
(click)="handleDelete()">
Delete Item
</button>
</div>
</div>
`
})
export class ConfirmationDialogComponent {
isDeleting = signal(false);
async handleDelete(): Promise<void> {
this.isDeleting.set(true);
try {
await this.deleteItem();
this.handleClose();
} finally {
this.isDeleting.set(false);
}
}
handleCancel(): void {
this.handleClose();
}
handleClose(): void {
// Close dialog logic
}
private async deleteItem(): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
```
## Styling and Customization
### Size Classes
All button components (except InfoButton) support three sizes:
```typescript
export const ButtonSize = {
Small: 'small',
Medium: 'medium',
Large: 'large',
} as const;
```
CSS classes applied: `ui-button__small`, `ui-button__medium`, `ui-button__large`
### Color Themes
#### Standard Button Colors
```typescript
export const ButtonColor = {
Primary: 'primary', // Default blue theme
Secondary: 'secondary', // Gray theme
Brand: 'brand', // ISA brand color
Tertiary: 'tertiary', // Red/warning theme
} as const;
```
CSS classes applied: `ui-button__primary`, `ui-button__secondary`, etc.
#### Text Button Colors
```typescript
export const TextButtonColor = {
Normal: 'normal', // Standard text color
Strong: 'strong', // Bold/emphasized text
Subtle: 'subtle', // Muted text
} as const;
```
#### Icon Button Colors
```typescript
export const IconButtonColor = {
Primary: 'primary',
Secondary: 'secondary',
Brand: 'brand',
Tertiary: 'tertiary',
Neutral: 'neutral', // Additional neutral theme
} as const;
```
### State Classes
All buttons apply these state-based classes:
- **Pending**: `ui-button__pending`, `ui-text-button__pending`, `ui-info-button--pending`
- **Disabled**: `disabled` (applied to all button types)
### Custom Styling
Override styles using specific class selectors:
```scss
// Custom primary button styling
.ui-button.ui-button__primary {
background-color: custom-color;
border-radius: 8px;
}
// Custom text button hover
.ui-text-button.ui-text-button__strong:hover {
text-decoration: underline;
}
// Custom icon button sizing
.ui-icon-button.ui-icon-button__large {
width: 48px;
height: 48px;
}
```
## Accessibility
All button components implement comprehensive accessibility features:
### ARIA Attributes
#### Standard, Text, and Info Buttons
- `[disabled]`: Applied via host binding
- Proper button semantics via `<button>` element
- Tab index support for keyboard navigation
#### Icon Button (Enhanced ARIA)
```typescript
host: {
'[attr.aria-disabled]': 'disabled()',
'[attr.aria-label]': 'name()', // Icon name as label
'[attr.aria-busy]': 'pending()', // Loading state
'[attr.role]': 'pending() ? "progressbar" : "button"'
}
```
### Keyboard Navigation
All buttons support:
- **Tab**: Focus navigation
- **Enter/Space**: Trigger click event
- **Custom tab index**: Configurable via `tabIndex` input
### Screen Reader Support
- Icon buttons include `aria-label` with icon name
- Loading states announce via `aria-busy`
- Disabled states properly conveyed via `aria-disabled`
- Stateful buttons maintain role semantics during state changes
## Testing
The library uses **Jest** for testing.
### Running Tests
```bash
# Run tests for this library
npx nx test ui-buttons --skip-nx-cache
# Run tests with coverage
npx nx test ui-buttons --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-buttons --watch
```
### Test Coverage
All components include comprehensive unit tests covering:
- **Input signals** - Size, color, pending, disabled states
- **Computed properties** - CSS class generation
- **Host bindings** - Dynamic class and attribute application
- **Event handling** - Click events, state changes (stateful button)
- **Loading states** - Spinner rendering and icon replacement
- **Accessibility** - ARIA attributes and keyboard navigation
- **State management** - Stateful button state transitions and auto-dismiss
## Architecture Notes
### Component Architecture
The library follows a consistent architecture pattern across all button types:
```
ButtonComponent (Standalone)
├─→ Signal-based inputs (size, color, pending, disabled, tabIndex)
├─→ Computed properties (CSS classes)
├─→ Host bindings (dynamic attributes and classes)
├─→ Template (content projection + conditional spinner)
└─→ ViewEncapsulation.None (flexible styling)
```
### Design Patterns
#### 1. Host Binding for Dynamic Styling
All components use host binding to apply CSS classes:
```typescript
host: {
'[class]': '["ui-button", sizeClass(), colorClass(), pendingClass(), disabledClass()]',
'[tabindex]': 'tabIndex()',
'[disabled]': 'disabled()',
}
```
**Benefits**:
- No manual DOM manipulation required
- Reactive updates via signals
- Type-safe class application
- Optimized change detection
#### 2. Signal-Based Reactivity
Components leverage Angular signals for all reactive state:
```typescript
// Input signals
size = input<ButtonSize>('medium');
color = input<ButtonColor>('primary');
pending = input<boolean>(false);
// Computed signals
sizeClass = computed(() => `ui-button__${this.size()}`);
pendingClass = computed(() => this.pending() ? 'ui-button__pending' : '');
```
**Benefits**:
- Fine-grained reactivity
- Automatic dependency tracking
- Minimal re-renders with OnPush
- Clear data flow
#### 3. ViewEncapsulation.None
All components disable view encapsulation:
```typescript
encapsulation: ViewEncapsulation.None
```
**Rationale**:
- Allows global ISA design system styles to apply
- Enables custom theming and overrides
- Simplifies CSS class naming (no shadow DOM)
- Consistent with ISA application styling strategy
#### 4. Content Projection
Standard, text, and info buttons use content projection:
```html
<ng-content></ng-content>
@if (pending()) {
<div class="ui-button__spinner-container">
<ng-icon name="isaLoading"></ng-icon>
</div>
}
```
**Benefits**:
- Flexible button content
- Supports text, icons, and mixed content
- Preserves button dimensions during loading
### Stateful Button Architecture
The StatefulButtonComponent uses a more complex architecture:
```
StatefulButtonComponent
├─→ Extends ButtonComponent functionality
├─→ State management (default, success, error)
├─→ Auto-dismiss timer with cleanup
├─→ Width animations via AnimationBuilder
├─→ Effect-based state watching
├─→ Two-way state binding with model()
└─→ Conditional error action button
```
**Key Implementation Details**:
1. **State Effect with Auto-Dismiss**:
```typescript
effect(() => {
const currentState = this.state();
const dismissTimeout = this.dismiss();
this.clearDismissTimer();
if (dismissTimeout && (currentState === 'success' || currentState === 'error')) {
this.dismissTimer = setTimeout(() => {
this.changeState('default');
}, dismissTimeout);
}
});
```
2. **Width Animations**:
```typescript
#makeWidthAnimation(width: string): void {
const animation = this.#animationBuilder.build([
style({ width: '*' }),
animate('250ms', style({ width }))
]);
const player = animation.create(this.buttonElement().nativeElement);
player.play();
}
```
3. **Lifecycle Management**:
```typescript
ngOnDestroy(): void {
this.clearDismissTimer(); // Prevent memory leaks
}
```
### Performance Considerations
1. **OnPush Change Detection**: All components use `ChangeDetectionStrategy.OnPush` for optimal performance
2. **Signal-Based Updates**: Computed properties only recalculate when dependencies change
3. **Minimal Re-renders**: Host bindings update only affected attributes
4. **Animation Performance**: StatefulButton uses AnimationBuilder for performant width transitions
### Known Architectural Considerations
#### 1. ViewEncapsulation Strategy (Low Priority)
**Current State**:
- All components use `ViewEncapsulation.None`
- Relies on BEM-like class naming for style isolation
**Consideration**:
- Could enable encapsulation with CSS custom properties
- Would require design system refactoring
**Impact**: Low - current approach works well with ISA design system
#### 2. Stateful Button Complexity (Medium Priority)
**Current State**:
- Combines multiple concerns (state, animations, timers)
- Complex lifecycle management
**Consideration**:
- Could extract animation logic to separate service
- Could use directive composition for state management
- Timer management could be centralized
**Impact**: Medium - component works well but is less maintainable than simpler buttons
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework
- `@angular/animations` - Animations (StatefulButton only)
- `@ng-icons/core` - Icon rendering
- `@isa/icons` - ISA icon library (loading icon)
### Optional Dependencies
- `@angular/forms` - For use with reactive forms (stateful button state binding)
### Path Alias
Import from: `@isa/ui/buttons`
### Project Configuration
- **Project Name**: `ui-buttons`
- **Prefix**: `ui`
- **Testing**: Jest
- **Source Root**: `libs/ui/buttons/src`
## License
Internal ISA Frontend library - not for external distribution.

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,829 @@
# ui-empty-state
# @isa/ui/empty-state
This library was generated with [Nx](https://nx.dev).
A standalone Angular component library providing consistent empty state displays for various scenarios (no results, no articles, all done, select action). Part of the ISA Design System.
## Running unit tests
## Overview
Run `nx test ui-empty-state` to execute the unit tests.
The Empty State component library delivers a reusable, accessible empty state component with multiple appearance variants and customizable content. It provides visual feedback to users when data is unavailable, actions are complete, or user input is required.
### Key Features
- **Multiple Appearance Variants**: 4 pre-configured visual styles (NoResults, NoArticles, AllDone, SelectAction)
- **Embedded SVG Icons**: Rich, contextual SVG illustrations with dynamic rendering
- **Content Projection**: Flexible action area for custom buttons/links via `<ng-content>`
- **Signal-Based Architecture**: Modern Angular signals for reactive icon rendering
- **Sanitized HTML**: Secure SVG rendering using Angular's DomSanitizer
- **OnPush Change Detection**: Optimized performance with minimal re-renders
- **Type-Safe API**: Strongly typed appearance options with TypeScript const assertions
- **SCSS Styling**: Component-scoped styles with CSS class hooks
## Installation
This library is part of the ISA monorepo and uses path aliases for imports:
```typescript
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
```
## Quick Start
### Basic Usage
```typescript
import { Component } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
@Component({
selector: 'app-search-results',
standalone: true,
imports: [EmptyStateComponent],
template: `
<ui-empty-state
title="Keine Ergebnisse gefunden"
description="Versuchen Sie es mit anderen Suchbegriffen oder Filtern."
[appearance]="EmptyStateAppearance.NoResults"
/>
`
})
export class SearchResultsComponent {
EmptyStateAppearance = EmptyStateAppearance;
}
```
### With Custom Actions
```typescript
import { Component } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
import { ButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [EmptyStateComponent, ButtonComponent],
template: `
<ui-empty-state
title="Keine Artikel verfügbar"
description="Es wurden keine passenden Artikel gefunden."
[appearance]="EmptyStateAppearance.NoArticles"
>
<ui-button (click)="resetFilters()">Filter zurücksetzen</ui-button>
<ui-button variant="secondary" (click)="goBack()">Zurück</ui-button>
</ui-empty-state>
`
})
export class ProductListComponent {
EmptyStateAppearance = EmptyStateAppearance;
resetFilters(): void {
// Reset filter logic
}
goBack(): void {
// Navigation logic
}
}
```
## API Reference
### EmptyStateComponent
**Selector**: `ui-empty-state`
#### Inputs
| Input | Type | Default | Required | Description |
|-------|------|---------|----------|-------------|
| `title` | `string` | - | ✅ | Main heading text for the empty state |
| `description` | `string` | - | ✅ | Supporting description text |
| `appearance` | `EmptyStateAppearance` | `EmptyStateAppearance.NoResults` | ❌ | Visual variant (determines icon) |
#### Content Projection
- **Default Slot**: Projects custom action buttons/links into the `.ui-empty-state-actions` container
#### Host Properties
- **CSS Class**: `.ui-empty-state` - Applied to the host element for styling
#### Component Configuration
- **Change Detection**: `OnPush` - Optimized for performance
- **View Encapsulation**: `None` - Allows global styles to cascade
- **Standalone**: `true` - Can be imported directly without NgModule
### EmptyStateAppearance Type
```typescript
export const EmptyStateAppearance = {
NoResults: 'noResults', // Search/filter returned no results
NoArticles: 'noArticles', // No articles/products available
AllDone: 'allDone', // Task completion state
SelectAction: 'selectAction' // User needs to select an action
} as const;
export type EmptyStateAppearance =
(typeof EmptyStateAppearance)[keyof typeof EmptyStateAppearance];
```
#### Appearance Variants
| Variant | Icon | Use Case |
|---------|------|----------|
| `NoResults` | Document with magnifying glass and X | Search returned no matches, filters excluded all items |
| `NoArticles` | Document with X mark | Product catalog is empty, no items in category |
| `AllDone` | Coffee cup with steam | All tasks completed, inbox zero state |
| `SelectAction` | Hand pointer with dropdown | User needs to choose from dropdown/menu |
## Usage Examples
### Example 1: Search Results Empty State
```typescript
import { Component, computed, signal } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
import { ButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-customer-search',
standalone: true,
imports: [EmptyStateComponent, ButtonComponent],
template: `
@if (hasResults()) {
<!-- Display results -->
} @else {
<ui-empty-state
title="Keine Kunden gefunden"
description="Ihre Suche ergab keine Treffer. Überprüfen Sie Ihre Suchkriterien oder versuchen Sie es mit anderen Begriffen."
[appearance]="EmptyStateAppearance.NoResults"
>
<ui-button (click)="clearSearch()">Suche zurücksetzen</ui-button>
</ui-empty-state>
}
`
})
export class CustomerSearchComponent {
EmptyStateAppearance = EmptyStateAppearance;
searchResults = signal<Customer[]>([]);
hasResults = computed(() => this.searchResults().length > 0);
clearSearch(): void {
this.searchResults.set([]);
}
}
```
### Example 2: Task Completion State
```typescript
import { Component } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
import { Router } from '@angular/router';
@Component({
selector: 'app-return-process',
standalone: true,
imports: [EmptyStateComponent],
template: `
<ui-empty-state
title="Alle Rücksendungen bearbeitet"
description="Großartig! Sie haben alle ausstehenden Rücksendungen abgeschlossen."
[appearance]="EmptyStateAppearance.AllDone"
>
<ui-button (click)="goToDashboard()">Zum Dashboard</ui-button>
</ui-empty-state>
`
})
export class ReturnProcessComponent {
EmptyStateAppearance = EmptyStateAppearance;
constructor(private router: Router) {}
goToDashboard(): void {
this.router.navigate(['/dashboard']);
}
}
```
### Example 3: Action Selection Required
```typescript
import { Component } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
@Component({
selector: 'app-workflow-start',
standalone: true,
imports: [EmptyStateComponent],
template: `
<ui-empty-state
title="Wählen Sie eine Aktion aus"
description="Bitte wählen Sie aus dem Dropdown-Menü oben eine Aktion aus, um fortzufahren."
[appearance]="EmptyStateAppearance.SelectAction"
/>
`
})
export class WorkflowStartComponent {
EmptyStateAppearance = EmptyStateAppearance;
}
```
### Example 4: Conditional Appearance Based on Context
```typescript
import { Component, computed, signal } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
type EmptyReason = 'no-results' | 'no-articles' | 'completed' | 'select-action';
@Component({
selector: 'app-dynamic-empty-state',
standalone: true,
imports: [EmptyStateComponent],
template: `
<ui-empty-state
[title]="emptyTitle()"
[description]="emptyDescription()"
[appearance]="emptyAppearance()"
>
@if (reason() === 'no-results') {
<ui-button (click)="resetFilters()">Filter zurücksetzen</ui-button>
}
</ui-empty-state>
`
})
export class DynamicEmptyStateComponent {
reason = signal<EmptyReason>('no-results');
emptyAppearance = computed(() => {
switch (this.reason()) {
case 'no-results':
return EmptyStateAppearance.NoResults;
case 'no-articles':
return EmptyStateAppearance.NoArticles;
case 'completed':
return EmptyStateAppearance.AllDone;
case 'select-action':
return EmptyStateAppearance.SelectAction;
default:
return EmptyStateAppearance.NoResults;
}
});
emptyTitle = computed(() => {
switch (this.reason()) {
case 'no-results':
return 'Keine Ergebnisse';
case 'no-articles':
return 'Keine Artikel';
case 'completed':
return 'Alles erledigt';
case 'select-action':
return 'Aktion auswählen';
default:
return 'Leer';
}
});
emptyDescription = computed(() => {
// Description logic based on reason
return 'Beschreibung basierend auf dem Kontext';
});
resetFilters(): void {
// Reset logic
}
}
```
## Architecture Notes
### Component Structure
```
EmptyStateComponent
├── Host Element (.ui-empty-state)
│ ├── Sub-Icon Container (@if sanitizedSubIcon())
│ │ └── [innerHTML] (AllDone steam, SelectAction hand)
│ ├── Main Icon Circle (.ui-empty-state-circle)
│ │ └── [innerHTML] (Primary SVG icon)
│ ├── Title (.ui-empty-state-title)
│ │ └── {{ title() }}
│ ├── Description (.ui-empty-state-description)
│ │ └── {{ description() }}
│ └── Actions Container (.ui-empty-state-actions)
│ └── <ng-content> (Projected buttons/links)
```
### Signal Computation Flow
```typescript
// 1. Input signals (set by parent component)
appearance: InputSignal<EmptyStateAppearance>
// 2. Derived computed signals
icon: Signal<string> = computed(() => {
// Maps appearance to primary SVG constant
})
subIcon: Signal<string> = computed(() => {
// Returns secondary SVG for AllDone/SelectAction, empty string otherwise
})
// 3. Sanitized computed signals
sanitizedIcon: Signal<SafeHtml> = computed(() => {
// Bypasses security for safe HTML rendering
})
sanitizedSubIcon: Signal<SafeHtml | undefined> = computed(() => {
// Conditionally sanitizes sub-icon if present
})
```
### SVG Icon Management
**Constants Module** (`constants.ts`):
- Stores complete SVG markup as string constants
- Each SVG includes viewBox, dimensions, and semantic path data
- Icons designed for ISA color palette (`#CED4DA`, `#6C757D`)
**Icon Selection Logic**:
```typescript
icon = computed(() => {
const appearance = this.appearance();
switch (appearance) {
case EmptyStateAppearance.NoArticles:
return NO_ARTICLES;
case EmptyStateAppearance.AllDone:
return ALL_DONE_CUP;
case EmptyStateAppearance.SelectAction:
return SELECT_ACTION_OBJECT_DROPDOWN;
case EmptyStateAppearance.NoResults:
default:
return NO_RESULTS;
}
});
```
### Security Considerations
**DomSanitizer Usage**:
- All SVG content sanitized via `bypassSecurityTrustHtml()`
- Safe because SVG constants are internal, trusted sources
- Prevents XSS attacks from user-supplied content
- Template binding via `[innerHTML]` for dynamic rendering
**Why Sanitization is Safe Here**:
1. SVG strings are hardcoded constants (not user input)
2. No dynamic interpolation of external data into SVGs
3. Icons are static, design-system-approved assets
4. DomSanitizer explicitly bypasses only for known-safe content
### Styling Architecture
**Component SCSS** (`empty-state.component.scss`):
- Scoped to `.ui-empty-state` class
- Defines layout, spacing, and typography
- Uses ISA design tokens for consistency
- CSS classes target: `.ui-empty-state-circle`, `.ui-empty-state-title`, `.ui-empty-state-description`, `.ui-empty-state-actions`
**Global Style Integration**:
- `ViewEncapsulation.None` allows external styles to cascade
- Tailwind utilities can style projected content
- Maintains design system consistency
### Performance Characteristics
**Optimization Strategies**:
- **OnPush Change Detection**: Component only re-renders when inputs change
- **Computed Signals**: Icon selection memoized, recalculates only on appearance change
- **Conditional Rendering**: Sub-icons only render when present (`@if sanitizedSubIcon()`)
- **Static Constants**: SVG strings stored in memory once, reused across instances
**Bundle Impact**:
- Standalone component with minimal dependencies
- SVG icons increase bundle size (~4KB total for all icons)
- No external image assets required
- Tree-shakeable via ES modules
## Dependencies
### Angular Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| `@angular/core` | 20.1.2 | Component framework, signals, dependency injection |
| `@angular/platform-browser` | 20.1.2 | DomSanitizer for secure HTML rendering |
### Internal Dependencies
None - this library is fully self-contained with no dependencies on other `@isa/*` libraries.
### Peer Dependencies
This library expects the following packages in the consuming application:
```json
{
"peerDependencies": {
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0"
}
}
```
## Testing
### Test Configuration
**Framework**: Jest (legacy, migrating to Vitest)
**Test File**: `empty-state.component.spec.ts`
**Configuration**: `jest.config.ts` (extends workspace defaults)
### Running Tests
```bash
# Run tests with fresh results (skip Nx cache)
npx nx test ui-empty-state --skip-nx-cache
# Run tests in watch mode
npx nx test ui-empty-state --watch
# Run tests with coverage
npx nx test ui-empty-state --code-coverage --skip-nx-cache
```
### Testing Recommendations
#### Unit Test Coverage
**Test appearance variants**:
```typescript
it('should display NoResults icon by default', () => {
const fixture = TestBed.createComponent(EmptyStateComponent);
fixture.componentRef.setInput('title', 'No Results');
fixture.componentRef.setInput('description', 'Try again');
fixture.detectChanges();
const compiled = fixture.nativeElement;
const iconElement = compiled.querySelector('.ui-empty-state-circle');
expect(iconElement.innerHTML).toContain('M75.5 43.794V30.1846');
});
```
**Test content projection**:
```typescript
it('should project custom actions into actions container', () => {
const fixture = TestBed.createComponent(EmptyStateComponent);
fixture.componentRef.setInput('title', 'Test');
fixture.componentRef.setInput('description', 'Test');
const compiled = fixture.nativeElement;
const actionsContainer = compiled.querySelector('.ui-empty-state-actions');
const projectedButton = actionsContainer.querySelector('button');
expect(projectedButton).toBeTruthy();
expect(projectedButton.textContent).toContain('Custom Action');
});
```
**Test sanitization**:
```typescript
it('should sanitize icon HTML for security', () => {
const component = new EmptyStateComponent();
const sanitizer = TestBed.inject(DomSanitizer);
// Verify sanitizer is injected
expect(component['#sanitizer']).toBeDefined();
// Verify sanitized output
const sanitizedIcon = component.sanitizedIcon();
expect(sanitizedIcon).toBeTruthy();
});
```
#### Integration Test Scenarios
**Test with routing actions**:
```typescript
@Component({
template: `
<ui-empty-state
title="Test"
description="Test description"
[appearance]="EmptyStateAppearance.NoResults"
>
<button (click)="navigate()">Go Home</button>
</ui-empty-state>
`
})
class TestHostComponent {
EmptyStateAppearance = EmptyStateAppearance;
navigated = false;
navigate(): void {
this.navigated = true;
}
}
it('should trigger navigation when action button clicked', () => {
const fixture = TestBed.createComponent(TestHostComponent);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(fixture.componentInstance.navigated).toBe(true);
});
```
## Best Practices
### 1. Choose Appropriate Appearance
Match the appearance variant to the user's context:
```typescript
// ✅ GOOD: Context-appropriate variant
<ui-empty-state
title="Keine Suchergebnisse"
description="..."
[appearance]="EmptyStateAppearance.NoResults"
/>
// ❌ BAD: Misleading variant
<ui-empty-state
title="Keine Suchergebnisse"
description="..."
[appearance]="EmptyStateAppearance.AllDone" // Wrong icon!
/>
```
### 2. Provide Actionable Guidance
Always include helpful descriptions and recovery actions:
```typescript
// ✅ GOOD: Clear guidance with actions
<ui-empty-state
title="Keine Artikel gefunden"
description="Versuchen Sie, Ihre Suchkriterien anzupassen oder Filter zurückzusetzen."
>
<ui-button (click)="resetFilters()">Filter zurücksetzen</ui-button>
<ui-button variant="secondary" (click)="clearSearch()">Suche löschen</ui-button>
</ui-empty-state>
// ❌ BAD: Vague, no actions
<ui-empty-state
title="Nichts gefunden"
description="Keine Daten"
/>
```
### 3. Use Computed Signals for Dynamic Content
Leverage Angular signals for reactive empty states:
```typescript
// ✅ GOOD: Reactive, computed approach
export class ProductListComponent {
products = signal<Product[]>([]);
isLoading = signal(false);
emptyTitle = computed(() => {
if (this.isLoading()) return 'Laden...';
return 'Keine Produkte gefunden';
});
emptyDescription = computed(() => {
if (this.isLoading()) return 'Bitte warten...';
return 'Versuchen Sie es mit anderen Filtern.';
});
}
// ❌ BAD: Static, non-reactive
export class ProductListComponent {
emptyTitle = 'Keine Produkte'; // Won't update dynamically
}
```
### 4. Maintain Consistent Messaging
Follow ISA language guidelines for title/description text:
```typescript
// ✅ GOOD: Professional, helpful tone
title="Keine Rücksendungen vorhanden"
description="Es liegen derzeit keine Rücksendungen vor. Neue Rücksendungen erscheinen automatisch hier."
// ❌ BAD: Casual, unhelpful
title="Ups, nichts da!"
description="Schade."
```
### 5. Test All Appearance Variants
Ensure all visual variants render correctly:
```typescript
describe('EmptyStateComponent Appearances', () => {
const appearances = [
EmptyStateAppearance.NoResults,
EmptyStateAppearance.NoArticles,
EmptyStateAppearance.AllDone,
EmptyStateAppearance.SelectAction
];
appearances.forEach(appearance => {
it(`should render ${appearance} appearance correctly`, () => {
const fixture = TestBed.createComponent(EmptyStateComponent);
fixture.componentRef.setInput('title', 'Test');
fixture.componentRef.setInput('description', 'Test');
fixture.componentRef.setInput('appearance', appearance);
fixture.detectChanges();
const iconElement = fixture.nativeElement.querySelector('.ui-empty-state-circle');
expect(iconElement).toBeTruthy();
expect(iconElement.innerHTML).toContain('svg');
});
});
});
```
### 6. Accessibility Considerations
Ensure empty states are accessible:
```typescript
// ✅ GOOD: Semantic HTML with ARIA
<ui-empty-state
title="Keine Ergebnisse"
description="Ihre Suche ergab keine Treffer"
role="status"
aria-live="polite"
>
<ui-button>Filter zurücksetzen</ui-button>
</ui-empty-state>
// Consider adding to template:
// <div role="status" aria-live="polite">
// <ui-empty-state .../>
// </div>
```
### 7. Avoid Overuse
Don't use empty states for loading or error conditions:
```typescript
// ✅ GOOD: Empty state for actual empty data
@if (products().length === 0 && !isLoading()) {
<ui-empty-state .../>
}
// ❌ BAD: Empty state for loading
@if (isLoading()) {
<ui-empty-state title="Laden..." description="Bitte warten"/>
}
// Use a loading spinner instead!
```
## Common Pitfalls
### 1. Forgetting Required Inputs
```typescript
// ❌ ERROR: Missing required inputs
<ui-empty-state [appearance]="EmptyStateAppearance.NoResults" />
// Angular will throw an error for missing title/description
// ✅ CORRECT: All required inputs provided
<ui-empty-state
title="Erforderlich"
description="Erforderlich"
[appearance]="EmptyStateAppearance.NoResults"
/>
```
### 2. Incorrect Appearance Type
```typescript
// ❌ ERROR: Invalid string literal
<ui-empty-state
title="Test"
description="Test"
appearance="no-results" // TypeScript error!
/>
// ✅ CORRECT: Use EmptyStateAppearance enum
<ui-empty-state
title="Test"
description="Test"
[appearance]="EmptyStateAppearance.NoResults"
/>
```
### 3. Projecting Non-Action Content
```typescript
// ❌ BAD: Projecting large content blocks
<ui-empty-state title="Test" description="Test">
<div>
<p>Long paragraph...</p>
<table>...</table>
</div>
</ui-empty-state>
// ✅ GOOD: Project only action buttons
<ui-empty-state title="Test" description="Test">
<ui-button>Action 1</ui-button>
<ui-button variant="secondary">Action 2</ui-button>
</ui-empty-state>
```
## Migration Guide
### From Custom Empty States to @isa/ui/empty-state
**Before**:
```typescript
@Component({
template: `
<div class="empty-state">
<img src="/assets/no-results.svg" alt="No Results">
<h2>Keine Ergebnisse</h2>
<p>Versuchen Sie es erneut.</p>
<button (click)="retry()">Erneut versuchen</button>
</div>
`
})
```
**After**:
```typescript
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
@Component({
standalone: true,
imports: [EmptyStateComponent],
template: `
<ui-empty-state
title="Keine Ergebnisse"
description="Versuchen Sie es erneut."
[appearance]="EmptyStateAppearance.NoResults"
>
<ui-button (click)="retry()">Erneut versuchen</ui-button>
</ui-empty-state>
`
})
```
**Benefits**:
- Consistent design across application
- Built-in icons, no asset management
- Accessible, semantic HTML structure
- Optimized rendering performance
## Related Documentation
- **ISA Design System**: Design guidelines for empty states
- **@isa/ui/buttons**: Button components for empty state actions
- **Angular Signals Guide**: Understanding reactive signal patterns
- **Security Best Practices**: DomSanitizer usage guidelines
## Support and Contributing
For questions, issues, or contributions related to the Empty State component:
1. Check existing tests in `empty-state.component.spec.ts` for usage examples
2. Review SCSS styles for customization guidance
3. Consult ISA Design System for visual/UX standards
4. Follow Angular standalone component best practices
## Changelog
### Current Version
**Features**:
- Standalone component architecture
- 4 appearance variants with embedded SVG icons
- Signal-based reactive rendering
- Content projection for custom actions
- OnPush change detection optimization
- Secure HTML sanitization
**Testing**:
- Jest configuration (migrating to Vitest)
- Component unit tests with Spectator (migrating to Angular Testing Library)
---
**Package**: `@isa/ui/empty-state`
**Path Alias**: `@isa/ui/empty-state`
**Entry Point**: `libs/ui/empty-state/src/index.ts`
**Selector**: `ui-empty-state`
**Type**: Standalone Angular Component Library

View File

@@ -1,7 +1,609 @@
# ui-input-controls
# @isa/ui/input-controls
This library was generated with [Nx](https://nx.dev).
A comprehensive collection of form input components and directives for Angular applications supporting reactive forms, template-driven forms, and accessibility features.
## Running unit tests
## Overview
Run `nx test ui-input-controls` to execute the unit tests.
The Input Controls library provides a complete suite of reusable form components built on Angular's reactive forms API and Angular CDK. It includes text fields, checkboxes, dropdowns, chips, checklists, listboxes, and inline inputs - all designed to work seamlessly with Angular Forms while maintaining consistent styling and behavior across the ISA application.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Form Integration](#form-integration)
- [Accessibility](#accessibility)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)
## Features
- **Complete form control suite** - 8 component types covering common input scenarios
- **Reactive Forms integration** - ControlValueAccessor implementation for all form controls
- **Template-driven forms** - Full ngModel support for two-way binding
- **Keyboard navigation** - Arrow keys, Enter, Escape, and Tab support
- **Accessibility (a11y)** - ARIA attributes, screen reader support, keyboard navigation
- **Customizable appearance** - Multiple size and appearance variants for each control
- **Validation support** - Integration with Angular's built-in validators
- **CDK integration** - Built on Angular CDK primitives (listbox, a11y, overlay)
- **Signal-based inputs** - Modern Angular signals for reactive property management
- **Type-safe** - Full TypeScript support with generic type parameters
## Quick Start
### 1. Import Components
```typescript
import { Component } from '@angular/core';
import {
CheckboxComponent,
DropdownButtonComponent,
DropdownOptionComponent,
TextFieldComponent,
InputControlDirective,
} from '@isa/ui/input-controls';
import { ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-my-form',
imports: [
ReactiveFormsModule,
CheckboxComponent,
DropdownButtonComponent,
DropdownOptionComponent,
TextFieldComponent,
InputControlDirective,
],
template: '...'
})
export class MyFormComponent {}
```
### 2. Basic Text Field
```html
<ui-text-field>
<input
type="text"
uiInputControl
formControlName="username"
placeholder="Enter username"
/>
</ui-text-field>
```
### 3. Checkbox
```html
<ui-checkbox>
<input type="checkbox" formControlName="acceptTerms" />
</ui-checkbox>
```
### 4. Dropdown
```html
<ui-dropdown [(ngModel)]="selectedOption" label="Select option">
<ui-dropdown-option [value]="1">Option 1</ui-dropdown-option>
<ui-dropdown-option [value]="2">Option 2</ui-dropdown-option>
<ui-dropdown-option [value]="3">Option 3</ui-dropdown-option>
</ui-dropdown>
```
## Core Concepts
### Component Categories
The library provides three main categories of form controls:
#### 1. Text Input Controls
- **TextFieldComponent** - Standard text input with container, labels, and errors
- **TextareaComponent** - Multi-line text input
- **InlineInputComponent** - Compact inline text input for space-constrained UIs
#### 2. Selection Controls
- **CheckboxComponent** - Single checkbox with bullet or checkbox appearance
- **ChecklistComponent** - Multiple checkboxes working as a group
- **DropdownButtonComponent** - Select dropdown with keyboard navigation
- **ChipsComponent** - Single-select chip group
- **ListboxDirective** - CDK listbox wrapper for custom list selections
#### 3. Supporting Components
- **InputControlDirective** - Core directive for input field integration
- **TextFieldContainerComponent** - Layout container for text fields
- **TextFieldErrorsComponent** - Validation error display
- **TextFieldClearComponent** - Clear button for text inputs
- **CheckboxLabelDirective** - Label styling for checkboxes
- **ChecklistValueDirective** - Value binding for checklist items
- **ChipOptionComponent** - Individual chip option
- **DropdownOptionComponent** - Individual dropdown option
- **ListboxItemDirective** - Individual listbox item
### Control Value Accessor Pattern
All form controls implement Angular's `ControlValueAccessor` interface, enabling seamless integration with both reactive and template-driven forms:
```typescript
export class DropdownButtonComponent<T> implements ControlValueAccessor {
value = model<T>();
disabled = model<boolean>(false);
writeValue(obj: unknown): void { ... }
registerOnChange(fn: (value: T) => void): void { ... }
registerOnTouched(fn: () => void): void { ... }
setDisabledState(isDisabled: boolean): void { ... }
}
```
### Appearance Variants
#### Checkbox Appearances
```typescript
const CheckboxAppearance = {
Bullet: 'bullet', // Round bullet-style selector
Checkbox: 'checkbox', // Traditional square checkbox (default)
} as const;
```
#### Text Field Sizes
```typescript
const TextFieldSize = {
Small: 'small',
Medium: 'medium', // Default
Large: 'large',
} as const;
```
## API Reference
### CheckboxComponent
A customizable checkbox component supporting different visual appearances.
**Selector:** `ui-checkbox`
**Inputs:**
- `appearance: CheckboxAppearance` - Visual style ('checkbox' | 'bullet'). Default: 'checkbox'
**Usage:**
```html
<!-- Default checkbox appearance -->
<ui-checkbox>
<input type="checkbox" />
</ui-checkbox>
<!-- Bullet appearance -->
<ui-checkbox [appearance]="CheckboxAppearance.Bullet">
<input type="checkbox" />
</ui-checkbox>
```
---
### DropdownButtonComponent<T>
A dropdown select component with keyboard navigation and CDK overlay integration.
**Selector:** `ui-dropdown`
**Inputs:**
- `appearance: DropdownAppearance` - Visual style. Default: 'accent-outline'
- `label: string` - Dropdown label text
- `showSelectedValue: boolean` - Show selected option text. Default: true
- `tabIndex: number` - Tab index for keyboard navigation. Default: 0
- `id: string` - Optional element ID
**Outputs:**
- `value: ModelSignal<T>` - Two-way bindable selected value
- `disabled: ModelSignal<boolean>` - Disabled state
**Methods:**
- `open(): void` - Opens the dropdown overlay
- `close(): void` - Closes the dropdown overlay
- `select(option, options?): void` - Selects an option programmatically
**Keyboard Navigation:**
- `Arrow Down/Up` - Navigate through options
- `Enter` - Select highlighted option and close
- `Escape` - Close dropdown
**Usage:**
```typescript
interface Product {
id: number;
name: string;
}
products: Product[] = [
{ id: 1, name: 'Product A' },
{ id: 2, name: 'Product B' }
];
selectedProduct: Product;
```
```html
<ui-dropdown
[(value)]="selectedProduct"
label="Select Product"
appearance="accent-outline">
@for (product of products; track product.id) {
<ui-dropdown-option [value]="product">
{{ product.name }}
</ui-dropdown-option>
}
</ui-dropdown>
```
---
### ChipsComponent<T>
Single-selection chip group with form integration.
**Selector:** `ui-chips`
**Outputs:**
- `value: ModelSignal<T>` - Selected chip value
- `disabled: ModelSignal<boolean>` - Disabled state
**Methods:**
- `select(value: T, options?): void` - Selects a chip
- `toggle(value: T, options?): void` - Toggles chip selection
- `isSelected(value: T): boolean` - Checks if value is selected
**Usage:**
```typescript
type Size = 'S' | 'M' | 'L' | 'XL';
selectedSize: Size = 'M';
```
```html
<ui-chips [(ngModel)]="selectedSize">
<ui-chip [value]="'S'">Small</ui-chip>
<ui-chip [value]="'M'">Medium</ui-chip>
<ui-chip [value]="'L'">Large</ui-chip>
<ui-chip [value]="'XL'">Extra Large</ui-chip>
</ui-chips>
```
---
### ChecklistComponent
Multi-select checkbox group returning an array of selected values.
**Selector:** `ui-checklist`
**Outputs:**
- `value: ModelSignal<unknown[]>` - Array of selected values
**Usage:**
```typescript
selectedFruits: string[] = ['apple', 'banana'];
```
```html
<ui-checklist [(ngModel)]="selectedFruits">
<label class="ui-checkbox-label">
<ui-checkbox>
<input type="checkbox" [uiChecklistValue]="'apple'">
</ui-checkbox>
Apple
</label>
<label class="ui-checkbox-label">
<ui-checkbox>
<input type="checkbox" [uiChecklistValue]="'banana'">
</ui-checkbox>
Banana
</label>
</ui-checklist>
```
---
### TextFieldComponent
Container component for text input fields with consistent styling.
**Selector:** `ui-text-field`
**Inputs:**
- `size: TextFieldSize` - Field size ('small' | 'medium' | 'large'). Default: 'medium'
**Content Projection:**
- Requires `InputControlDirective` on child input element
**Usage:**
```html
<ui-text-field size="medium">
<input
type="text"
uiInputControl
formControlName="email"
placeholder="Enter email"
/>
</ui-text-field>
```
---
### InlineInputComponent
Compact inline text input for space-constrained interfaces.
**Selector:** `ui-inline-input`
**Inputs:**
- `size: InlineInputSize` - Field size ('small' | 'medium'). Default: 'medium'
**Usage:**
```html
<ui-inline-input size="small">
<input type="text" uiInputControl [(ngModel)]="quantity" />
</ui-inline-input>
```
---
### ListboxDirective
Wrapper directive for Angular CDK listbox with ISA styling.
**Selector:** `[uiListbox]`
**Inputs (via CDK):**
- `value: any` - Selected value(s)
- `compareWith: (o1: any, o2: any) => boolean` - Comparison function
- `disabled: boolean` - Disabled state
**Outputs (via CDK):**
- `valueChange: EventEmitter` - Emits when selection changes
**Usage:**
```html
<div uiListbox [value]="selectedItem" (valueChange)="handleChange($event)">
<div uiListboxItem [value]="item1">Item 1</div>
<div uiListboxItem [value]="item2">Item 2</div>
</div>
```
## Usage Examples
### Reactive Form with Validation
```typescript
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import {
TextFieldComponent,
TextFieldContainerComponent,
TextFieldErrorsComponent,
InputControlDirective,
CheckboxComponent,
DropdownButtonComponent,
DropdownOptionComponent
} from '@isa/ui/input-controls';
@Component({
selector: 'app-user-form',
imports: [
ReactiveFormsModule,
TextFieldComponent,
TextFieldContainerComponent,
TextFieldErrorsComponent,
InputControlDirective,
CheckboxComponent,
DropdownButtonComponent,
DropdownOptionComponent
],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<ui-text-field-container>
<label>Email</label>
<ui-text-field>
<input
type="email"
uiInputControl
formControlName="email"
placeholder="user@example.com"
/>
</ui-text-field>
<ui-text-field-errors />
</ui-text-field-container>
<ui-dropdown formControlName="role" label="Select Role">
<ui-dropdown-option [value]="'admin'">Administrator</ui-dropdown-option>
<ui-dropdown-option [value]="'user'">User</ui-dropdown-option>
</ui-dropdown>
<label class="ui-checkbox-label">
<ui-checkbox>
<input type="checkbox" formControlName="acceptTerms" />
</ui-checkbox>
I accept the terms and conditions
</label>
<button type="submit" [disabled]="!userForm.valid">Submit</button>
</form>
`
})
export class UserFormComponent {
#fb = inject(FormBuilder);
userForm = this.#fb.group({
email: ['', [Validators.required, Validators.email]],
role: ['user', Validators.required],
acceptTerms: [false, Validators.requiredTrue]
});
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
}
}
}
```
## Form Integration
### Reactive Forms
All components implement `ControlValueAccessor` and work with `FormControl`:
```typescript
import { FormControl, Validators } from '@angular/forms';
emailControl = new FormControl('', [Validators.required, Validators.email]);
roleControl = new FormControl<string>('user');
agreeControl = new FormControl(false, Validators.requiredTrue);
```
```html
<ui-text-field>
<input uiInputControl [formControl]="emailControl" />
</ui-text-field>
<ui-dropdown [formControl]="roleControl" label="Role">...</ui-dropdown>
<ui-checkbox><input [formControl]="agreeControl" /></ui-checkbox>
```
### Template-Driven Forms
Use `ngModel` for two-way binding:
```html
<ui-text-field>
<input uiInputControl [(ngModel)]="email" name="email" />
</ui-text-field>
<ui-dropdown [(ngModel)]="role" name="role" label="Role">...</ui-dropdown>
```
## Accessibility
### Keyboard Navigation
All components support comprehensive keyboard navigation:
**Dropdown:**
- `Arrow Down/Up` - Navigate through options
- `Enter` - Select highlighted option and close
- `Escape` - Close dropdown without selection
- `Tab` - Move focus to next element
**Listbox:**
- `Arrow Down/Up` - Navigate items
- `Home/End` - Jump to first/last item
- `Space/Enter` - Select item
**Checkbox/Checklist:**
- `Space` - Toggle checkbox state
- `Tab` - Move between checkboxes
### ARIA Attributes
Components include proper ARIA attributes for screen readers:
```html
<!-- Dropdown -->
<ui-dropdown
role="combobox"
aria-haspopup="listbox"
[attr.aria-expanded]="isOpen()">
</ui-dropdown>
<!-- Listbox -->
<div uiListbox role="listbox">
<div uiListboxItem role="option"></div>
</div>
```
## Testing
The library uses **Jest** with **Spectator** for testing.
### Running Tests
```bash
# Run tests for this library
npx nx test ui-input-controls --skip-nx-cache
# Run tests with coverage
npx nx test ui-input-controls --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-input-controls --watch
```
### Test Coverage
The library includes comprehensive unit tests covering:
- **Component rendering** - All visual states and variants
- **Form integration** - ControlValueAccessor implementation
- **Keyboard navigation** - Arrow keys, Enter, Escape, Tab
- **Validation** - Error states and validation display
- **Accessibility** - ARIA attributes and screen reader support
- **User interactions** - Click, keyboard, focus events
## Architecture Notes
### Design Patterns
#### 1. ControlValueAccessor Pattern
All form controls implement the CVA interface for seamless Angular Forms integration.
**Benefits:**
- Works with both reactive and template-driven forms
- Automatic validation integration
- Consistent API across all controls
#### 2. Signal-Based Reactivity
Components use Angular signals for reactive state management:
```typescript
appearance = input<CheckboxAppearance>(CheckboxAppearance.Checkbox);
appearanceClass = computed(() =>
this.appearance() === CheckboxAppearance.Bullet
? 'ui-checkbox__bullet'
: 'ui-checkbox__checkbox'
);
```
#### 3. CDK Integration
Leverages Angular CDK for complex interactions:
- **Overlay** - Dropdown positioning (CdkConnectedOverlay)
- **A11y** - Keyboard navigation (ActiveDescendantKeyManager)
- **Listbox** - Accessible list selection (CdkListbox)
### Performance Considerations
1. **OnPush Change Detection** - All components use OnPush strategy
2. **Signal Reactivity** - Computed signals prevent unnecessary re-renders
3. **KeyManager** - Efficient keyboard navigation without DOM queries
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework
- `@angular/forms` - Forms module
- `@angular/cdk/a11y` - Accessibility utilities
- `@angular/cdk/listbox` - Listbox primitive
- `@angular/cdk/overlay` - Overlay positioning
- `@angular/cdk/collections` - SelectionModel
- `@ng-icons/core` - Icon system
- `@isa/icons` - ISA icon library
- `lodash` - Utility functions (isEqual)
### Path Alias
Import from: `@isa/ui/input-controls`
## License
Internal ISA Frontend library - not for external distribution.

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,813 @@
# label
# @isa/ui/label
This library was generated with [Nx](https://nx.dev).
A flexible label component for displaying tags and notices with configurable priority levels across Angular applications.
## Running unit tests
## Overview
Run `nx test label` to execute the unit tests.
The Label UI library provides a standalone Angular component for displaying visual labels with semantic meaning. It supports two distinct label types (Tag and Notice) and three priority levels (High, Medium, Low), enabling consistent visual communication of information status, categorization, and importance throughout the ISA application.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)
## Features
- **Two label types** - Tag and Notice for different semantic contexts
- **Three priority levels** - High, Medium, and Low for visual hierarchy
- **Standalone component** - Modern Angular architecture with explicit imports
- **Signal-based reactivity** - Uses Angular signals for efficient updates
- **Type-safe API** - TypeScript enums for label type and priority configuration
- **CSS class composition** - Dynamic class generation based on type and priority
- **OnPush change detection** - Optimized rendering performance
- **ViewEncapsulation.None** - Flexible styling with global CSS classes
- **Content projection** - Full control over label content via ng-content
- **Computed classes** - Reactive CSS class computation using Angular signals
## Quick Start
### 1. Import the Component
```typescript
import { Component } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-product-status',
template: `
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
In Stock
</ui-label>
`,
imports: [LabelComponent]
})
export class ProductStatusComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
}
```
### 2. Basic Tag Label
```typescript
@Component({
template: `
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.Medium">
New Arrival
</ui-label>
`,
imports: [LabelComponent]
})
export class ProductBadgeComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
}
```
### 3. Notice Label
```typescript
@Component({
template: `
<ui-label [type]="Labeltype.Notice" [priority]="LabelPriority.High">
Action Required
</ui-label>
`,
imports: [LabelComponent]
})
export class AlertComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
}
```
## Core Concepts
### Label Types
The component supports two distinct label types, each with specific semantic meaning:
#### 1. Tag (Default)
- **Purpose**: Categorization, status indication, metadata display
- **Use Cases**: Product categories, order status, item tags, filters
- **Visual Style**: Typically compact, pill-shaped design
- **CSS Class**: `ui-label__tag`
#### 2. Notice
- **Purpose**: Notifications, alerts, important messages
- **Use Cases**: Error messages, warnings, system notifications, user alerts
- **Visual Style**: Typically more prominent, attention-grabbing design
- **CSS Class**: `ui-label__notice`
### Priority Levels
Each label type can have one of three priority levels that control visual hierarchy:
#### 1. High Priority (Default)
- **Purpose**: Critical information, urgent status, primary actions
- **Visual Characteristics**: Most prominent styling, often with bold colors
- **Use Cases**: Error states, critical alerts, primary status
- **CSS Classes**:
- `ui-label__tag-priority-high` (for Tag type)
- `ui-label__notice-priority-high` (for Notice type)
#### 2. Medium Priority
- **Purpose**: Important but non-critical information
- **Visual Characteristics**: Moderate emphasis, balanced contrast
- **Use Cases**: Warnings, secondary status, informational notices
- **CSS Classes**:
- `ui-label__tag-priority-medium` (for Tag type)
- `ui-label__notice-priority-medium` (for Notice type)
#### 3. Low Priority
- **Purpose**: Supplementary information, subtle indicators
- **Visual Characteristics**: Minimal emphasis, subtle colors
- **Use Cases**: Hints, optional metadata, background information
- **CSS Classes**:
- `ui-label__tag-priority-low` (for Tag type)
- `ui-label__notice-priority-low` (for Notice type)
### Signal-Based Reactivity
The component uses Angular signals for reactive CSS class computation:
```typescript
// Signal inputs
type = input<Labeltype>(Labeltype.Tag);
priority = input<LabelPriority>(LabelPriority.High);
// Computed signal for type class
typeClass = computed(() => `ui-label__${this.type()}`);
// Computed signal for priority class (combines type and priority)
priorityClass = computed(() => `${this.typeClass()}-priority-${this.priority()}`);
```
This approach provides:
- **Automatic updates** - Classes update when inputs change
- **Efficient rendering** - Only recomputes when dependencies change
- **Type safety** - TypeScript ensures valid type/priority combinations
### CSS Class Structure
The component applies a hierarchical class structure:
```
ui-label (base class)
├── ui-label__tag (type class for Tag)
│ ├── ui-label__tag-priority-high (Tag + High priority)
│ ├── ui-label__tag-priority-medium (Tag + Medium priority)
│ └── ui-label__tag-priority-low (Tag + Low priority)
└── ui-label__notice (type class for Notice)
├── ui-label__notice-priority-high (Notice + High priority)
├── ui-label__notice-priority-medium (Notice + Medium priority)
└── ui-label__notice-priority-low (Notice + Low priority)
```
## API Reference
### LabelComponent
Standalone component for displaying labels with type and priority.
#### Selector
```html
<ui-label>Content</ui-label>
```
#### Inputs
##### `type`
- **Type**: `Labeltype`
- **Default**: `Labeltype.Tag`
- **Description**: The semantic type of the label
- **Values**:
- `Labeltype.Tag` - For categorization and status
- `Labeltype.Notice` - For notifications and alerts
**Example:**
```html
<ui-label [type]="Labeltype.Notice">Important Notice</ui-label>
```
##### `priority`
- **Type**: `LabelPriority`
- **Default**: `LabelPriority.High`
- **Description**: The visual priority level of the label
- **Values**:
- `LabelPriority.High` - Highest visual emphasis
- `LabelPriority.Medium` - Moderate visual emphasis
- `LabelPriority.Low` - Subtle visual emphasis
**Example:**
```html
<ui-label [priority]="LabelPriority.Medium">Optional Info</ui-label>
```
#### Computed Properties
##### `typeClass()`
- **Type**: `Signal<string>`
- **Returns**: CSS class string based on current type
- **Format**: `ui-label__${type}`
- **Example**: `"ui-label__tag"` or `"ui-label__notice"`
##### `priorityClass()`
- **Type**: `Signal<string>`
- **Returns**: CSS class string combining type and priority
- **Format**: `ui-label__${type}-priority-${priority}`
- **Example**: `"ui-label__tag-priority-high"`
#### Host Classes
The component automatically applies the following classes to its host element:
```typescript
['ui-label', typeClass(), priorityClass()]
```
Example rendered classes:
```html
<ui-label class="ui-label ui-label__tag ui-label__tag-priority-high">
Content
</ui-label>
```
### Type Definitions
#### Labeltype
TypeScript enum for label type values:
```typescript
export const Labeltype = {
Tag: 'tag',
Notice: 'notice',
} as const;
export type Labeltype = (typeof Labeltype)[keyof typeof Labeltype];
```
**Usage:**
```typescript
import { Labeltype } from '@isa/ui/label';
// In template
[type]="Labeltype.Tag"
[type]="Labeltype.Notice"
// In component class
readonly labelType = Labeltype.Tag;
```
#### LabelPriority
TypeScript enum for priority level values:
```typescript
export const LabelPriority = {
High: 'high',
Medium: 'medium',
Low: 'low',
} as const;
export type LabelPriority = (typeof LabelPriority)[keyof typeof LabelPriority];
```
**Usage:**
```typescript
import { LabelPriority } from '@isa/ui/label';
// In template
[priority]="LabelPriority.High"
[priority]="LabelPriority.Medium"
[priority]="LabelPriority.Low"
// In component class
readonly priority = LabelPriority.Medium;
```
## Usage Examples
### Product Status Tags
```typescript
import { Component } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-product-card',
template: `
<div class="product-card">
<h3>{{ product.name }}</h3>
<!-- In Stock - High priority tag -->
<ui-label
[type]="Labeltype.Tag"
[priority]="LabelPriority.High">
In Stock
</ui-label>
<!-- Category - Medium priority tag -->
<ui-label
[type]="Labeltype.Tag"
[priority]="LabelPriority.Medium">
Electronics
</ui-label>
<!-- Condition - Low priority tag -->
<ui-label
[type]="Labeltype.Tag"
[priority]="LabelPriority.Low">
New
</ui-label>
</div>
`,
imports: [LabelComponent]
})
export class ProductCardComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
product = {
name: 'Wireless Headphones',
inStock: true,
category: 'Electronics'
};
}
```
### Order Status Indicators
```typescript
import { Component, input, computed } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-order-status',
template: `
<ui-label
[type]="Labeltype.Tag"
[priority]="statusPriority()">
{{ statusText() }}
</ui-label>
`,
imports: [LabelComponent]
})
export class OrderStatusComponent {
Labeltype = Labeltype;
status = input.required<'pending' | 'processing' | 'shipped' | 'delivered'>();
statusText = computed(() => {
const statusMap = {
pending: 'Pending',
processing: 'Processing',
shipped: 'Shipped',
delivered: 'Delivered'
};
return statusMap[this.status()];
});
statusPriority = computed(() => {
const priorityMap = {
pending: LabelPriority.High,
processing: LabelPriority.High,
shipped: LabelPriority.Medium,
delivered: LabelPriority.Low
};
return priorityMap[this.status()];
});
}
```
### System Notifications
```typescript
import { Component, input } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
interface Notification {
id: number;
message: string;
type: 'error' | 'warning' | 'info';
}
@Component({
selector: 'app-notification-item',
template: `
<div class="notification">
<ui-label
[type]="Labeltype.Notice"
[priority]="getNotificationPriority()">
{{ getNotificationLabel() }}
</ui-label>
<p>{{ notification().message }}</p>
</div>
`,
imports: [LabelComponent]
})
export class NotificationItemComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
notification = input.required<Notification>();
getNotificationPriority(): LabelPriority {
const type = this.notification().type;
switch (type) {
case 'error':
return LabelPriority.High;
case 'warning':
return LabelPriority.Medium;
case 'info':
return LabelPriority.Low;
}
}
getNotificationLabel(): string {
const type = this.notification().type;
return type.charAt(0).toUpperCase() + type.slice(1);
}
}
```
### Dynamic Labels with NgFor
```typescript
import { Component } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-product-tags',
template: `
<div class="tags-container">
@for (tag of tags; track tag.id) {
<ui-label
[type]="Labeltype.Tag"
[priority]="tag.priority">
{{ tag.label }}
</ui-label>
}
</div>
`,
imports: [LabelComponent]
})
export class ProductTagsComponent {
Labeltype = Labeltype;
tags = [
{ id: 1, label: 'Sale', priority: LabelPriority.High },
{ id: 2, label: 'Free Shipping', priority: LabelPriority.Medium },
{ id: 3, label: 'Eco-Friendly', priority: LabelPriority.Low },
];
}
```
### Conditional Label Display
```typescript
import { Component, input, computed } from '@angular/core';
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
@Component({
selector: 'app-inventory-status',
template: `
@if (showStockLabel()) {
<ui-label
[type]="Labeltype.Tag"
[priority]="stockPriority()">
{{ stockLabel() }}
</ui-label>
}
`,
imports: [LabelComponent]
})
export class InventoryStatusComponent {
Labeltype = Labeltype;
LabelPriority = LabelPriority;
stockQuantity = input.required<number>();
showStockLabel = computed(() => this.stockQuantity() < 10);
stockLabel = computed(() => {
const qty = this.stockQuantity();
if (qty === 0) return 'Out of Stock';
if (qty < 5) return 'Low Stock';
return 'Limited Stock';
});
stockPriority = computed(() => {
const qty = this.stockQuantity();
if (qty === 0) return LabelPriority.High;
if (qty < 5) return LabelPriority.High;
return LabelPriority.Medium;
});
}
```
## Styling and Customization
### CSS Class Hierarchy
The component generates a predictable class structure for styling:
```css
/* Base class applied to all labels */
.ui-label {
display: inline-block;
font-family: var(--isa-font-family);
/* Base styles */
}
/* Type-specific base styles */
.ui-label__tag {
/* Tag-specific base styles */
}
.ui-label__notice {
/* Notice-specific base styles */
}
/* Priority-specific styles for Tags */
.ui-label__tag-priority-high {
background-color: var(--isa-accent-red);
color: white;
font-weight: 600;
}
.ui-label__tag-priority-medium {
background-color: var(--isa-accent-yellow);
color: var(--isa-text-primary);
font-weight: 500;
}
.ui-label__tag-priority-low {
background-color: var(--isa-neutral-200);
color: var(--isa-text-secondary);
font-weight: 400;
}
/* Priority-specific styles for Notices */
.ui-label__notice-priority-high {
border-left: 4px solid var(--isa-accent-red);
background-color: var(--isa-accent-red-light);
padding: 0.5rem 1rem;
}
.ui-label__notice-priority-medium {
border-left: 4px solid var(--isa-accent-yellow);
background-color: var(--isa-accent-yellow-light);
padding: 0.5rem 1rem;
}
.ui-label__notice-priority-low {
border-left: 4px solid var(--isa-neutral-300);
background-color: var(--isa-neutral-100);
padding: 0.5rem 1rem;
}
```
### Custom Styling Example
```scss
// Override specific label styles
.product-card {
ui-label {
border-radius: 4px;
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
// Custom high-priority tag style
.ui-label__tag-priority-high {
animation: pulse 2s infinite;
}
// Custom notice styles
.ui-label__notice {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
```
### Tailwind CSS Integration
The component works seamlessly with Tailwind's design system:
```html
<!-- Using Tailwind utility classes alongside the component -->
<ui-label
[type]="Labeltype.Tag"
[priority]="LabelPriority.High"
class="rounded-full px-3 py-1 text-xs font-semibold">
Featured
</ui-label>
```
## Testing
The library uses **Vitest** with **Angular Testing Utilities** for testing.
### Running Tests
```bash
# Run tests for this library
npx nx test label --skip-nx-cache
# Run tests with coverage
npx nx test label --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test label --watch
```
### Test Structure
The library includes comprehensive unit tests covering:
- **Type input** - Validates correct CSS class generation for each type
- **Priority input** - Validates correct CSS class generation for each priority
- **Default values** - Tests default type and priority behavior
- **Content projection** - Tests ng-content rendering
- **Signal reactivity** - Tests computed class updates when inputs change
- **Class composition** - Tests correct combination of base, type, and priority classes
### Example Test
```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { LabelComponent, Labeltype, LabelPriority } from './label.component';
describe('LabelComponent', () => {
let component: LabelComponent;
let fixture: ComponentFixture<LabelComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [LabelComponent]
});
fixture = TestBed.createComponent(LabelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have default type as Tag', () => {
expect(component.type()).toBe(Labeltype.Tag);
});
it('should have default priority as High', () => {
expect(component.priority()).toBe(LabelPriority.High);
});
it('should generate correct type class for Tag', () => {
fixture.componentRef.setInput('type', Labeltype.Tag);
expect(component.typeClass()).toBe('ui-label__tag');
});
it('should generate correct priority class', () => {
fixture.componentRef.setInput('type', Labeltype.Tag);
fixture.componentRef.setInput('priority', LabelPriority.Medium);
expect(component.priorityClass()).toBe('ui-label__tag-priority-medium');
});
it('should apply all classes to host element', () => {
fixture.componentRef.setInput('type', Labeltype.Notice);
fixture.componentRef.setInput('priority', LabelPriority.Low);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.classList.contains('ui-label')).toBe(true);
expect(element.classList.contains('ui-label__notice')).toBe(true);
expect(element.classList.contains('ui-label__notice-priority-low')).toBe(true);
});
});
```
## Architecture Notes
### Current Architecture
The library follows Angular standalone component architecture:
```
LabelComponent (Standalone)
├── Signal Inputs
│ ├── type: Signal<Labeltype>
│ └── priority: Signal<LabelPriority>
├── Computed Signals
│ ├── typeClass(): string
│ └── priorityClass(): string
└── Host Binding
└── [class]: computed classes array
```
### Design Decisions
#### 1. Standalone Component Architecture
**Decision**: Use standalone component instead of NgModule
**Rationale**:
- Aligns with Angular 20.1.2 best practices
- Reduces boilerplate and complexity
- Improves tree-shaking and bundle optimization
- Enables explicit, localized imports
**Impact**: Simpler consumption in feature modules
#### 2. Signal-Based Reactivity
**Decision**: Use `input()` and `computed()` signals instead of traditional inputs
**Rationale**:
- Modern Angular reactivity model
- Automatic dependency tracking
- Better performance with change detection
- Type-safe reactive transformations
**Impact**: Efficient CSS class updates without manual change detection
#### 3. ViewEncapsulation.None
**Decision**: Use ViewEncapsulation.None instead of Emulated or ShadowDOM
**Rationale**:
- Enables global styling through BEM-like class naming
- Consistent with ISA design system approach
- Easier integration with Tailwind CSS
- Flexible theming capabilities
**Impact**: Requires careful CSS class naming to avoid conflicts
#### 4. OnPush Change Detection
**Decision**: Use OnPush change detection strategy
**Rationale**:
- Optimal performance with signal-based inputs
- Reduces unnecessary change detection cycles
- Signals automatically trigger required updates
- Best practice for presentational components
**Impact**: Better performance, especially in large lists
#### 5. TypeScript Const Assertions for Enums
**Decision**: Use const objects with type inference instead of traditional enums
**Rationale**:
- Better type safety with literal types
- Improved autocomplete in IDEs
- Smaller JavaScript bundle size
- More flexible than traditional TypeScript enums
**Impact**: Type-safe API with minimal runtime overhead
### Performance Considerations
1. **Signal-based inputs** - Only recompute classes when inputs actually change
2. **OnPush change detection** - Minimal change detection overhead
3. **Computed signals** - Efficient memoization of class strings
4. **No template logic** - All computation in component class
5. **Lightweight DOM** - Simple ng-content projection with no wrapper elements
### Future Enhancements
Potential improvements identified:
1. **Icon Support** - Add optional icon input for visual enhancement
2. **Dismissible Labels** - Add optional close button with output event
3. **Animation Support** - Add entry/exit animations for dynamic labels
4. **Theme Variants** - Support custom color themes via injection tokens
5. **Size Variants** - Add size input (small, medium, large)
6. **Accessibility Improvements** - Add ARIA attributes for screen readers
7. **Tooltip Integration** - Built-in tooltip support for truncated content
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework (20.1.2)
- `@angular/common` - CommonModule for basic directives
### Optional Libraries
None - this is a pure presentation component with no external dependencies.
### Path Alias
Import from: `@isa/ui/label`
## License
Internal ISA Frontend library - not for external distribution.

View File

@@ -1,7 +1,786 @@
# ui-menu
# @isa/ui/menu
This library was generated with [Nx](https://nx.dev).
A lightweight Angular component library providing accessible menu components built on Angular CDK Menu. Part of the ISA Design System.
## Running unit tests
## Overview
Run `nx test ui-menu` to execute the unit tests.
The Menu component library delivers reusable, accessible menu components that wrap Angular CDK Menu directives with ISA-specific styling and conventions. It provides a simple API for creating dropdown menus, context menus, and other menu-based UI patterns.
### Key Features
- **Angular CDK Integration**: Built on `@angular/cdk/menu` for robust accessibility and keyboard navigation
- **Host Directives Pattern**: Leverages Angular host directives for clean, declarative API
- **Minimal Overhead**: Thin wrapper around CDK with ISA styling hooks
- **CSS-Based Styling**: Uses Tailwind and ISA design tokens via CSS classes
- **Keyboard Navigation**: Full keyboard support (Arrow keys, Enter, Escape) via CDK
- **ARIA Compliance**: Automatic ARIA attributes for screen readers
- **Standalone Components**: Modern Angular standalone architecture
## Installation
This library is part of the ISA monorepo and uses path aliases for imports:
```typescript
import { UiMenu, MenuComponent, MenuItemDirective } from '@isa/ui/menu';
```
## Quick Start
### Basic Usage
```typescript
import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
@Component({
selector: 'app-example',
standalone: true,
imports: [UiMenu],
template: `
<ui-menu>
<button uiMenuItem>Action 1</button>
<button uiMenuItem>Action 2</button>
<button uiMenuItem>Action 3</button>
</ui-menu>
`
})
export class ExampleComponent {}
```
### Dropdown Menu Example
```typescript
import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';
@Component({
selector: 'app-dropdown-example',
standalone: true,
imports: [UiMenu, CdkMenuTrigger],
template: `
<button [cdkMenuTriggerFor]="menu">Open Menu</button>
<ng-template #menu>
<ui-menu>
<button uiMenuItem (click)="onEdit()">Edit</button>
<button uiMenuItem (click)="onDelete()">Delete</button>
<button uiMenuItem (click)="onShare()">Share</button>
</ui-menu>
</ng-template>
`
})
export class DropdownExampleComponent {
onEdit() { console.log('Edit clicked'); }
onDelete() { console.log('Delete clicked'); }
onShare() { console.log('Share clicked'); }
}
```
## API Reference
### MenuComponent
**Selector**: `ui-menu`
A container component for menu items that provides accessibility and keyboard navigation.
#### Host Directives
- **CdkMenu**: Provides core menu functionality, keyboard navigation, and ARIA attributes
#### Host Properties
- **CSS Class**: `.ui-menu` - Applied to the host element for styling
#### Template
- Simple content projection: `<ng-content></ng-content>`
####Configuration
- **Change Detection**: Default (inherited from Angular)
- **View Encapsulation**: Default
- **Standalone**: `false` (import via `UiMenu` array)
### MenuItemDirective
**Selector**: `[uiMenuItem]`
A directive that marks elements as menu items, adding appropriate styling and behavior.
#### Host Directives
- **CdkMenuItem**: Provides menu item functionality, click handling, and ARIA attributes
#### Host Properties
- **CSS Class**: `.ui-menu-item` - Applied to the host element for styling
#### Configuration
- **Standalone**: `false` (import via `UiMenu` array)
### UiMenu Import Array
Convenience import for both menu components:
```typescript
export const UiMenu = [MenuComponent, MenuItemDirective];
```
**Usage**:
```typescript
imports: [UiMenu] // Imports both MenuComponent and MenuItemDirective
```
## Usage Examples
### Example 1: Simple Menu List
```typescript
import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
@Component({
selector: 'app-simple-menu',
standalone: true,
imports: [UiMenu],
template: `
<ui-menu>
<button uiMenuItem>Home</button>
<button uiMenuItem>About</button>
<button uiMenuItem>Contact</button>
</ui-menu>
`
})
export class SimpleMenuComponent {}
```
### Example 2: Context Menu with Icons
```typescript
import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkContextMenuTrigger } from '@angular/cdk/menu';
import { NgIconComponent } from '@ng-icons/core';
@Component({
selector: 'app-context-menu',
standalone: true,
imports: [UiMenu, CdkContextMenuTrigger, NgIconComponent],
template: `
<div [cdkContextMenuTriggerFor]="contextMenu" class="context-area">
Right-click here
</div>
<ng-template #contextMenu>
<ui-menu>
<button uiMenuItem>
<ng-icon name="isaActionCopy"></ng-icon>
Copy
</button>
<button uiMenuItem>
<ng-icon name="isaActionPaste"></ng-icon>
Paste
</button>
<button uiMenuItem>
<ng-icon name="isaActionDelete"></ng-icon>
Delete
</button>
</ui-menu>
</ng-template>
`
})
export class ContextMenuComponent {}
```
### Example 3: Nested Menus (Submenus)
```typescript
import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';
@Component({
selector: 'app-nested-menu',
standalone: true,
imports: [UiMenu, CdkMenuTrigger],
template: `
<button [cdkMenuTriggerFor]="mainMenu">File</button>
<ng-template #mainMenu>
<ui-menu>
<button uiMenuItem>New File</button>
<button uiMenuItem [cdkMenuTriggerFor]="openMenu">Open</button>
<button uiMenuItem>Save</button>
</ui-menu>
</ng-template>
<ng-template #openMenu>
<ui-menu>
<button uiMenuItem>Recent Files</button>
<button uiMenuItem>Browse...</button>
</ui-menu>
</ng-template>
`
})
export class NestedMenuComponent {}
```
### Example 4: Menu with Disabled Items
```typescript
import { Component, signal } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';
@Component({
selector: 'app-disabled-menu',
standalone: true,
imports: [UiMenu, CdkMenuTrigger],
template: `
<button [cdkMenuTriggerFor]="menu">Actions</button>
<ng-template #menu>
<ui-menu>
<button uiMenuItem [disabled]="false">Edit</button>
<button uiMenuItem [disabled]="!canDelete()">Delete</button>
<button uiMenuItem [disabled]="false">Share</button>
</ui-menu>
</ng-template>
`
})
export class DisabledMenuComponent {
canDelete = signal(false); // Dynamic disabled state
}
```
### Example 5: Menu with Click Handlers
```typescript
import { Component } from '@angular/core';
import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';
import { Router } from '@angular/router';
@Component({
selector: 'app-navigation-menu',
standalone: true,
imports: [UiMenu, CdkMenuTrigger],
template: `
<button [cdkMenuTriggerFor]="menu">Navigate</button>
<ng-template #menu>
<ui-menu>
<button uiMenuItem (click)="navigateTo('/dashboard')">Dashboard</button>
<button uiMenuItem (click)="navigateTo('/reports')">Reports</button>
<button uiMenuItem (click)="navigateTo('/settings')">Settings</button>
<button uiMenuItem (click)="logout()">Logout</button>
</ui-menu>
</ng-template>
`
})
export class NavigationMenuComponent {
constructor(private router: Router) {}
navigateTo(path: string): void {
this.router.navigate([path]);
}
logout(): void {
// Logout logic
}
}
```
## Architecture Notes
### Component Structure
```
MenuComponent
├── Host Element (<ui-menu>)
│ ├── Host Directive: CdkMenu
│ │ ├── Keyboard Navigation (Arrow keys, Enter, Escape)
│ │ ├── ARIA Attributes (role="menu")
│ │ └── Focus Management
│ ├── CSS Class: .ui-menu
│ └── <ng-content> (Projects menu items)
MenuItemDirective
├── Host Element ([uiMenuItem])
│ ├── Host Directive: CdkMenuItem
│ │ ├── Click Handling
│ │ ├── ARIA Attributes (role="menuitem")
│ │ └── Keyboard Interaction
│ └── CSS Class: .ui-menu-item
```
### Host Directives Pattern
**Why Host Directives?**
This library uses Angular's **host directives** feature introduced in Angular 15 to:
1. **Reuse CDK Logic**: Leverage `@angular/cdk/menu` without re-implementing functionality
2. **Clean API**: Hide CDK complexity while exposing ISA-specific styling
3. **Maintainability**: CDK updates automatically benefit this library
4. **Type Safety**: Full TypeScript support for all CDK features
**How It Works**:
```typescript
@Component({
selector: 'ui-menu',
template: '<ng-content></ng-content>',
host: { class: 'ui-menu' },
hostDirectives: [CdkMenu], // Applies CdkMenu to this component's host
})
export class MenuComponent {}
```
When you use `<ui-menu>`, it automatically:
- Applies all `CdkMenu` behaviors (keyboard nav, ARIA, etc.)
- Adds `.ui-menu` CSS class for styling
- Projects child content (menu items)
### CDK Menu Features
**Automatic Features** (via CdkMenu):
- **Keyboard Navigation**:
- `↓` / `↑`: Navigate menu items
- `Enter` / `Space`: Activate item
- `Escape`: Close menu
- `Tab`: Move focus out of menu
- **ARIA Attributes**:
- `role="menu"` on menu container
- `role="menuitem"` on menu items
- `aria-disabled` on disabled items
- `aria-haspopup` for submenus
- **Focus Management**:
- Auto-focus first item on open
- Circular focus wrapping
- Focus restoration on close
### Styling Architecture
**CSS Class Hooks**:
```scss
// Applied by MenuComponent
.ui-menu {
// Container styles (padding, background, border, shadow)
}
// Applied by MenuItemDirective
.ui-menu-item {
// Item styles (padding, hover, focus, active states)
}
// Disabled state (via CDK)
.ui-menu-item[disabled] {
// Disabled styles (opacity, cursor, pointer-events)
}
```
**Tailwind Integration**:
The library provides CSS class hooks that can be styled via:
- Global SCSS files
- Tailwind `@apply` directives
- Custom Tailwind plugins (ISA menu plugin)
**Example Tailwind Configuration**:
```css
/* styles.scss */
.ui-menu {
@apply bg-white shadow-lg rounded-md py-1;
}
.ui-menu-item {
@apply px-4 py-2 hover:bg-gray-100 cursor-pointer;
}
.ui-menu-item[disabled] {
@apply opacity-50 cursor-not-allowed;
}
```
### Integration with Angular CDK
**Required CDK Directives** (imported separately):
```typescript
import { CdkMenuTrigger } from '@angular/cdk/menu'; // For dropdown triggers
import { CdkContextMenuTrigger } from '@angular/cdk/menu'; // For context menus
```
**CDK Menu Trigger**:
```typescript
<button [cdkMenuTriggerFor]="menuTemplate">Open Menu</button>
<ng-template #menuTemplate>
<ui-menu>...</ui-menu>
</ng-template>
```
**CDK Context Menu Trigger**:
```typescript
<div [cdkContextMenuTriggerFor]="contextMenuTemplate">Right-click me</div>
<ng-template #contextMenuTemplate>
<ui-menu>...</ui-menu>
</ng-template>
```
### Performance Characteristics
**Optimization Strategies**:
- **Lightweight Components**: Minimal overhead over CDK base
- **No Template**: MenuComponent uses empty template (content projection only)
- **No Change Detection**: Default strategy (components are simple wrappers)
- **CDK Optimizations**: Inherits CDK's efficient event handling and focus management
**Bundle Impact**:
- **Component Size**: ~2KB (both components combined)
- **CDK Dependency**: ~15KB (shared with other CDK features)
- **Total Overhead**: Minimal, mostly CDK dependency
## Dependencies
### Angular Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| `@angular/core` | 20.1.2 | Component framework and dependency injection |
| `@angular/cdk/menu` | 20.1.2 | CDK Menu directives for accessibility and keyboard nav |
### Internal Dependencies
None - this library has no dependencies on other `@isa/*` libraries.
### Peer Dependencies
```json
{
"peerDependencies": {
"@angular/core": "^20.0.0",
"@angular/cdk": "^20.0.0"
}
}
```
## Testing
### Test Configuration
**Framework**: Jest
**Configuration**: `jest.config.ts` (extends workspace defaults)
### Running Tests
```bash
# Run tests with fresh results
npx nx test ui-menu --skip-nx-cache
# Run tests in watch mode
npx nx test ui-menu --watch
# Run tests with coverage
npx nx test ui-menu --code-coverage --skip-nx-cache
```
### Testing Recommendations
#### Unit Test Coverage
**Test menu rendering**:
```typescript
import { TestBed } from '@angular/core/testing';
import { MenuComponent, MenuItemDirective } from '@isa/ui/menu';
describe('MenuComponent', () => {
it('should render menu with items', () => {
const fixture = TestBed.createComponent(MenuComponent);
fixture.detectChanges();
const menuElement = fixture.nativeElement;
expect(menuElement.classList.contains('ui-menu')).toBe(true);
});
});
```
**Test menu item directive**:
```typescript
describe('MenuItemDirective', () => {
it('should apply ui-menu-item class', () => {
// Create test component with directive
const fixture = TestBed.createComponent(TestHostComponent);
fixture.detectChanges();
const menuItem = fixture.nativeElement.querySelector('[uiMenuItem]');
expect(menuItem.classList.contains('ui-menu-item')).toBe(true);
});
});
```
**Test keyboard navigation** (via CDK):
```typescript
it('should navigate menu items with keyboard', () => {
const fixture = TestBed.createComponent(MenuComponent);
fixture.detectChanges();
const menuElement = fixture.nativeElement;
// Simulate down arrow
menuElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
// Verify focus moved
expect(document.activeElement).toBe(firstMenuItem);
});
```
#### Integration Test Scenarios
**Test with CDK trigger**:
```typescript
import { CdkMenuTrigger } from '@angular/cdk/menu';
@Component({
template: `
<button [cdkMenuTriggerFor]="menu" #trigger="cdkMenuTrigger">Open</button>
<ng-template #menu>
<ui-menu>
<button uiMenuItem>Item 1</button>
</ui-menu>
</ng-template>
`
})
class TestHostComponent {}
it('should open menu on trigger click', () => {
const fixture = TestBed.createComponent(TestHostComponent);
fixture.detectChanges();
const triggerButton = fixture.nativeElement.querySelector('button');
triggerButton.click();
fixture.detectChanges();
// Verify menu is open
const menu = document.querySelector('ui-menu');
expect(menu).toBeTruthy();
});
```
## Best Practices
### 1. Use CDK Triggers for Dropdown Menus
```typescript
// ✅ GOOD: Use CdkMenuTrigger for dropdowns
<button [cdkMenuTriggerFor]="menu">Open Menu</button>
<ng-template #menu>
<ui-menu>...</ui-menu>
</ng-template>
// ❌ BAD: Manual show/hide logic
<button (click)="showMenu = true">Open Menu</button>
<ui-menu *ngIf="showMenu">...</ui-menu>
```
### 2. Always Use MenuItemDirective on Interactive Elements
```typescript
// ✅ GOOD: Apply directive to buttons
<ui-menu>
<button uiMenuItem>Action</button>
</ui-menu>
// ❌ BAD: Non-interactive elements
<ui-menu>
<div uiMenuItem>Action</div> // Won't receive keyboard events properly
</ui-menu>
```
### 3. Leverage CDK Features
```typescript
// ✅ GOOD: Use CDK for submenus
<button uiMenuItem [cdkMenuTriggerFor]="submenu">More</button>
<ng-template #submenu>
<ui-menu>...</ui-menu>
</ng-template>
// ❌ BAD: Manual submenu implementation
<button uiMenuItem (click)="toggleSubmenu()">More</button>
<ui-menu *ngIf="submenuOpen">...</ui-menu>
```
### 4. Provide ARIA Labels for Icon-Only Items
```typescript
// ✅ GOOD: Accessible icon-only menu item
<button uiMenuItem aria-label="Delete">
<ng-icon name="isaActionDelete"></ng-icon>
</button>
// ❌ BAD: No accessible label
<button uiMenuItem>
<ng-icon name="isaActionDelete"></ng-icon>
</button>
```
### 5. Use Semantic HTML
```typescript
// ✅ GOOD: Use buttons for actions
<ui-menu>
<button uiMenuItem>Edit</button>
<button uiMenuItem>Delete</button>
</ui-menu>
// ❌ BAD: Use divs/spans
<ui-menu>
<div uiMenuItem>Edit</div>
<span uiMenuItem>Delete</span>
</ui-menu>
```
### 6. Handle Menu Item Clicks Properly
```typescript
// ✅ GOOD: Use click handlers on menu items
<button uiMenuItem (click)="handleAction()">Action</button>
// ❌ BAD: Wrap in extra divs with click handlers
<div (click)="handleAction()">
<button uiMenuItem>Action</button>
</div>
```
## Common Pitfalls
### 1. Forgetting CDK Imports
```typescript
// ❌ ERROR: Missing CdkMenuTrigger import
imports: [UiMenu]
// ✅ CORRECT: Import CDK directives separately
imports: [UiMenu, CdkMenuTrigger]
```
### 2. Using Menu Without Template
```typescript
// ❌ ERROR: Menu needs ng-template with trigger
<ui-menu>
<button uiMenuItem>Item</button>
</ui-menu>
// ✅ CORRECT: Use with CDK trigger and template
<button [cdkMenuTriggerFor]="menu">Open</button>
<ng-template #menu>
<ui-menu>
<button uiMenuItem>Item</button>
</ui-menu>
</ng-template>
```
### 3. Incorrect Selector Usage
```typescript
// ❌ ERROR: Wrong directive name
<button menuItem>Action</button>
// ✅ CORRECT: Use uiMenuItem
<button uiMenuItem>Action</button>
```
## Migration Guide
### From Custom Menu to @isa/ui/menu
**Before**:
```typescript
@Component({
template: `
<div class="custom-menu">
<div class="menu-item" (click)="action1()">Action 1</div>
<div class="menu-item" (click)="action2()">Action 2</div>
</div>
`
})
```
**After**:
```typescript
import { UiMenu } from '@isa/ui/menu';
import { CdkMenuTrigger } from '@angular/cdk/menu';
@Component({
standalone: true,
imports: [UiMenu, CdkMenuTrigger],
template: `
<button [cdkMenuTriggerFor]="menu">Open Menu</button>
<ng-template #menu>
<ui-menu>
<button uiMenuItem (click)="action1()">Action 1</button>
<button uiMenuItem (click)="action2()">Action 2</button>
</ui-menu>
</ng-template>
`
})
```
**Benefits**:
- Automatic keyboard navigation
- ARIA compliance
- Focus management
- Consistent styling
- Less custom code
## Related Documentation
- **Angular CDK Menu**: [Official CDK Menu Documentation](https://material.angular.io/cdk/menu/overview)
- **Host Directives**: Angular guide on host directives
- **@isa/ui/buttons**: Button components often used with menus
- **ISA Design System**: Menu styling guidelines
## Support and Contributing
For questions, issues, or contributions related to the Menu components:
1. Review Angular CDK Menu documentation for advanced features
2. Check ISA Design System for styling standards
3. Consult Tailwind configuration for menu styling utilities
4. Follow Angular standalone component best practices
## Changelog
### Current Version
**Features**:
- Standalone-ready component and directive
- Angular CDK Menu integration via host directives
- Minimal API surface (component + directive)
- Full keyboard navigation and ARIA support
- Tailwind/ISA design system styling hooks
**Testing**:
- Jest configuration
- Unit tests for component and directive
---
**Package**: `@isa/ui/menu`
**Path Alias**: `@isa/ui/menu`
**Entry Point**: `libs/ui/menu/src/index.ts`
**Selectors**: `ui-menu`, `[uiMenuItem]`
**Type**: Angular Component Library (CDK Wrapper)

View File

@@ -1,7 +1,512 @@
# ui-progress-bar
# @isa/ui/progress-bar
This library was generated with [Nx](https://nx.dev).
A lightweight Angular progress bar component supporting both determinate and indeterminate modes.
## Running unit tests
## Overview
Run `nx test ui-progress-bar` to execute the unit tests.
The Progress Bar library provides a simple, performant component for visualizing progress or loading states. It supports two modes: determinate (with specific progress value) and indeterminate (continuous loading animation), making it suitable for various use cases from file uploads to background processing indicators.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Testing](#testing)
- [Dependencies](#dependencies)
## Features
- **Two display modes** - Determinate (percentage-based) and indeterminate (continuous animation)
- **Customizable progress** - Configurable value and max value for flexible percentage calculations
- **Standalone component** - Fully standalone, no module imports required
- **Signal-based inputs** - Reactive updates using Angular signals
- **Computed width** - Automatically calculates bar width based on value/maxValue ratio
- **OnPush change detection** - Optimized for performance
- **ViewEncapsulation.None** - Allows global styling customization
## Quick Start
### 1. Import the Component
```typescript
import { Component, signal } from '@angular/core';
import { ProgressBarComponent, ProgressBarMode } from '@isa/ui/progress-bar';
@Component({
selector: 'app-upload',
standalone: true,
imports: [ProgressBarComponent],
template: '...'
})
export class UploadComponent {
uploadProgress = signal(0);
}
```
### 2. Determinate Mode (Default)
```html
<!-- Show specific progress percentage -->
<ui-progress-bar
[value]="uploadProgress()"
[maxValue]="100"
></ui-progress-bar>
```
### 3. Indeterminate Mode
```html
<!-- Show continuous loading animation -->
<ui-progress-bar
[mode]="'indeterminate'"
></ui-progress-bar>
```
### 4. Custom Value Range
```html
<!-- Progress out of custom max value -->
<ui-progress-bar
[value]="processedItems()"
[maxValue]="totalItems()"
></ui-progress-bar>
```
## API Reference
### ProgressBarComponent
Standalone component that displays a visual progress indicator.
**Selector:** `ui-progress-bar`
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `mode` | `ProgressBarMode` | `'determinate'` | Display mode: `'determinate'` or `'indeterminate'` |
| `value` | `number` | `50` | Current progress value (used in determinate mode) |
| `maxValue` | `number` | `100` | Maximum value for percentage calculation |
#### ProgressBarMode Type
```typescript
export const ProgressBarMode = {
Determinate: 'determinate', // Percentage-based progress
Indeterminate: 'indeterminate' // Continuous loading animation
} as const;
export type ProgressBarMode =
(typeof ProgressBarMode)[keyof typeof ProgressBarMode];
```
#### Computed Properties
##### `modeClass(): string`
Returns the CSS class for the current mode:
- `'ui-progress-bar__determinate'` - For determinate mode
- `'ui-progress-bar__indeterminate'` - For indeterminate mode
##### `width(): string`
Calculates the progress bar width:
- **Indeterminate mode**: Returns `'100%'`
- **Determinate mode**: Returns `(value / maxValue * 100)%`
#### Host Classes
- `ui-progress-bar` - Always applied
- `ui-progress-bar__determinate` or `ui-progress-bar__indeterminate` - Based on mode
#### Template Structure
```html
<div class="ui-progress-bar__bar" [style.width]="width()"></div>
```
## Usage Examples
### File Upload Progress
```typescript
import { Component, signal } from '@angular/core';
import { ProgressBarComponent } from '@isa/ui/progress-bar';
@Component({
selector: 'app-file-upload',
standalone: true,
imports: [ProgressBarComponent],
template: `
<div class="upload-container">
<h3>Uploading {{ fileName() }}</h3>
<ui-progress-bar
[value]="uploadProgress()"
[maxValue]="100"
data-what="upload-progress-bar"
></ui-progress-bar>
<p>{{ uploadProgress() }}% complete</p>
</div>
`
})
export class FileUploadComponent {
fileName = signal('document.pdf');
uploadProgress = signal(0);
async uploadFile(file: File): Promise<void> {
this.fileName.set(file.name);
this.uploadProgress.set(0);
// Simulate upload progress
const interval = setInterval(() => {
this.uploadProgress.update(p => {
if (p >= 100) {
clearInterval(interval);
return 100;
}
return p + 10;
});
}, 500);
}
}
```
### Loading Indicator
```typescript
import { Component, signal } from '@angular/core';
import { ProgressBarComponent, ProgressBarMode } from '@isa/ui/progress-bar';
@Component({
selector: 'app-data-loader',
standalone: true,
imports: [ProgressBarComponent],
template: `
@if (isLoading()) {
<ui-progress-bar
[mode]="'indeterminate'"
data-what="loading-indicator"
></ui-progress-bar>
}
<div class="content">
@for (item of data(); track item.id) {
<div>{{ item.name }}</div>
}
</div>
`
})
export class DataLoaderComponent {
isLoading = signal(true);
data = signal<DataItem[]>([]);
async loadData(): Promise<void> {
this.isLoading.set(true);
try {
const result = await this.dataService.fetch();
this.data.set(result);
} finally {
this.isLoading.set(false);
}
}
}
```
### Batch Processing Progress
```typescript
import { Component, computed, signal } from '@angular/core';
import { ProgressBarComponent } from '@isa/ui/progress-bar';
@Component({
selector: 'app-batch-processor',
standalone: true,
imports: [ProgressBarComponent],
template: `
<div class="batch-progress">
<h3>Processing Items</h3>
<ui-progress-bar
[value]="processedCount()"
[maxValue]="totalCount()"
></ui-progress-bar>
<p>
{{ processedCount() }} of {{ totalCount() }} items processed
({{ percentComplete() }}%)
</p>
</div>
`
})
export class BatchProcessorComponent {
processedCount = signal(0);
totalCount = signal(100);
percentComplete = computed(() => {
const total = this.totalCount();
if (total === 0) return 0;
return Math.round((this.processedCount() / total) * 100);
});
async processItems(items: Item[]): Promise<void> {
this.totalCount.set(items.length);
this.processedCount.set(0);
for (const item of items) {
await this.processItem(item);
this.processedCount.update(c => c + 1);
}
}
}
```
### Multi-Step Progress
```typescript
import { Component, computed, signal } from '@angular/core';
import { ProgressBarComponent } from '@isa/ui/progress-bar';
@Component({
selector: 'app-wizard',
standalone: true,
imports: [ProgressBarComponent],
template: `
<div class="wizard">
<ui-progress-bar
[value]="currentStep()"
[maxValue]="totalSteps()"
></ui-progress-bar>
<div class="step-indicator">
Step {{ currentStep() }} of {{ totalSteps() }}
</div>
<div class="step-content">
@switch (currentStep()) {
@case (1) {
<div>Step 1: Basic Information</div>
}
@case (2) {
<div>Step 2: Address Details</div>
}
@case (3) {
<div>Step 3: Payment Method</div>
}
@case (4) {
<div>Step 4: Review & Confirm</div>
}
}
</div>
<div class="actions">
<button
[disabled]="currentStep() === 1"
(click)="previousStep()"
>
Previous
</button>
<button
[disabled]="currentStep() === totalSteps()"
(click)="nextStep()"
>
Next
</button>
</div>
</div>
`
})
export class WizardComponent {
currentStep = signal(1);
totalSteps = signal(4);
nextStep(): void {
this.currentStep.update(s => Math.min(s + 1, this.totalSteps()));
}
previousStep(): void {
this.currentStep.update(s => Math.max(s - 1, 1));
}
}
```
## Styling and Customization
### Default Classes
```html
<!-- Determinate mode -->
<ui-progress-bar class="ui-progress-bar ui-progress-bar__determinate">
<div class="ui-progress-bar__bar" style="width: 50%;"></div>
</ui-progress-bar>
<!-- Indeterminate mode -->
<ui-progress-bar class="ui-progress-bar ui-progress-bar__indeterminate">
<div class="ui-progress-bar__bar" style="width: 100%;"></div>
</ui-progress-bar>
```
### Custom Styling
```scss
// Base progress bar container
.ui-progress-bar {
width: 100%;
height: 4px;
background-color: #e0e0e0;
border-radius: 2px;
overflow: hidden;
}
// The moving/filling bar
.ui-progress-bar__bar {
height: 100%;
background-color: #2196f3;
transition: width 0.3s ease-in-out;
}
// Determinate mode specific styles
.ui-progress-bar__determinate .ui-progress-bar__bar {
background: linear-gradient(90deg, #1976d2 0%, #2196f3 100%);
}
// Indeterminate mode animation
.ui-progress-bar__indeterminate .ui-progress-bar__bar {
animation: indeterminate-progress 2s linear infinite;
background: linear-gradient(
90deg,
transparent 0%,
#2196f3 50%,
transparent 100%
);
}
@keyframes indeterminate-progress {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
// Color variants
.ui-progress-bar--success .ui-progress-bar__bar {
background-color: #4caf50;
}
.ui-progress-bar--warning .ui-progress-bar__bar {
background-color: #ff9800;
}
.ui-progress-bar--error .ui-progress-bar__bar {
background-color: #f44336;
}
```
## Testing
The library uses **Jest** for testing.
### Running Tests
```bash
# Run tests for progress-bar
npx nx test ui-progress-bar --skip-nx-cache
# Run tests with coverage
npx nx test ui-progress-bar --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-progress-bar --watch
```
### Test Coverage
Tests should cover:
- **Mode switching** - Determinate and indeterminate modes
- **Width calculation** - Correct percentage calculation from value/maxValue
- **Boundary conditions** - 0%, 100%, values exceeding maxValue
- **Signal reactivity** - Updates when value/maxValue change
- **Class application** - Correct mode classes applied
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework (v20.1.2)
### Path Alias
Import from: `@isa/ui/progress-bar`
### Peer Dependencies
- Angular 20.1.2 or higher
- TypeScript 5.8.3 or higher
## Best Practices
### 1. Use Appropriate Mode
Choose the mode based on whether progress is measurable:
```html
<!-- Measurable progress - use determinate -->
<ui-progress-bar [value]="uploadedBytes" [maxValue]="totalBytes"></ui-progress-bar>
<!-- Unknown duration - use indeterminate -->
<ui-progress-bar [mode]="'indeterminate'"></ui-progress-bar>
```
### 2. Provide Context
Always accompany the progress bar with text:
```html
<div class="progress-container">
<label>Uploading...</label>
<ui-progress-bar [value]="progress" [maxValue]="100"></ui-progress-bar>
<span>{{ progress }}%</span>
</div>
```
### 3. Handle Edge Cases
Validate values to prevent division by zero or negative percentages:
```typescript
percentComplete = computed(() => {
const max = this.maxValue();
const val = this.value();
if (max === 0) return 0;
return Math.min(Math.max((val / max) * 100, 0), 100);
});
```
### 4. Smooth Updates
For smoother visual transitions, update progress in reasonable increments:
```typescript
// Good: Update every 5-10%
updateProgress(newValue: number) {
if (newValue - this.progress() >= 5) {
this.progress.set(newValue);
}
}
// Bad: Update every tiny increment
updateProgress(newValue: number) {
this.progress.set(newValue); // Updates too frequently
}
```
## License
Internal ISA Frontend library - not for external distribution.

View File

@@ -1,7 +1,773 @@
# ui-search-bar
# @isa/ui/search-bar
This library was generated with [Nx](https://nx.dev).
A feature-rich Angular search bar component with integrated clear functionality and customizable appearance modes.
## Running unit tests
## Overview
Run `nx test ui-search-bar` to execute the unit tests.
The Search Bar library provides a flexible and accessible search input component designed for the ISA application. It supports two appearance modes (main and results), integrates seamlessly with Angular Forms using `NgControl`, and includes a dedicated clear button component with automatic focus restoration.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Architecture Notes](#architecture-notes)
- [Testing](#testing)
- [Dependencies](#dependencies)
## Features
- **Two appearance modes** - Main and results views with distinct styling
- **Angular Forms integration** - Works with `FormControl` and `NgControl` via content projection
- **Integrated clear button** - Dedicated component with automatic focus restoration
- **Icon support** - Prefix and suffix icon slots using `ng-icons`
- **Signal-based reactivity** - Computed appearance classes for efficient updates
- **OnPush change detection** - Optimized performance
- **Accessibility** - Proper focus management and ARIA attributes
- **Content projection** - Flexible composition with multiple slot patterns
## Quick Start
### 1. Import the Components
```typescript
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';
import { IconButtonComponent } from '@isa/ui/buttons';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaActionSearch } from '@isa/icons';
@Component({
selector: 'app-search',
standalone: true,
imports: [
ReactiveFormsModule,
UiSearchBarComponent,
UiSearchBarClearComponent,
IconButtonComponent,
NgIconComponent
],
providers: [provideIcons({ isaActionSearch })],
template: '...'
})
export class SearchComponent {
searchControl = new FormControl('');
}
```
### 2. Basic Usage
```html
<ui-search-bar>
<input
type="text"
[formControl]="searchControl"
placeholder="Search..."
data-what="search-input"
/>
<ui-search-bar-clear></ui-search-bar-clear>
<ui-icon-button name="isaActionSearch"></ui-icon-button>
</ui-search-bar>
```
### 3. With Prefix Icon
```html
<ui-search-bar appearance="results">
<ui-icon-button prefix name="isaActionSearch"></ui-icon-button>
<input
type="text"
[formControl]="searchControl"
placeholder="Filter results..."
/>
<ui-search-bar-clear></ui-search-bar-clear>
</ui-search-bar>
```
### 4. Custom Reset Value
```html
<ui-search-bar>
<input type="text" [formControl]="searchControl" />
<!-- Reset to specific value instead of null -->
<ui-search-bar-clear [value]="defaultSearchTerm"></ui-search-bar-clear>
<ui-icon-button name="isaActionSearch"></ui-icon-button>
</ui-search-bar>
```
## API Reference
### UiSearchBarComponent
Container component that provides structure and styling for search inputs.
**Selector:** `ui-search-bar`
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `appearance` | `SearchbarAppearance` | `'main'` | Visual appearance mode: `'main'` or `'results'` |
#### SearchbarAppearance Type
```typescript
export const SearchbarAppearance = {
Main: 'main', // Primary search bar appearance
Results: 'results' // Results filtering appearance
} as const;
export type SearchbarAppearance =
(typeof SearchbarAppearance)[keyof typeof SearchbarAppearance];
```
#### Content Projection Slots
The component uses content projection with specific selectors:
```html
<!-- Prefix icon slot -->
<ng-content select="ui-icon-button[prefix]"></ng-content>
<!-- Input field slot (required) -->
<ng-content select="input[type=text]"></ng-content>
<!-- Actions container -->
<div class="ui-search-bar__actions">
<!-- Clear button slot -->
<ng-content select="ui-search-bar-clear"></ng-content>
<!-- Suffix icon slot -->
<ng-content select="ui-icon-button"></ng-content>
</div>
```
#### Host Classes
- `ui-search-bar` - Always applied
- `ui-search-bar__main` - When `appearance` is `'main'`
- `ui-search-bar__results` - When `appearance` is `'results'`
#### Implementation Details
- Uses `contentChild.required()` to access the projected `NgControl`
- Automatically retrieves the input's `ElementRef` for focus management
- Computed `appearanceClass` updates reactively based on input signal
### UiSearchBarClearComponent
Standalone component that provides clear/reset functionality for the search input.
**Selector:** `ui-search-bar-clear`
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `value` | `unknown` | `undefined` | Value to set when clearing (defaults to `null` if not provided) |
#### Features
- Displays close icon (`isaActionClose`) from `@isa/icons`
- Automatically resets the parent search bar's form control
- Restores focus to the input field after reset (using `asapScheduler`)
- Standalone component with `ViewEncapsulation.None`
#### Host Attributes
- **Class**: `ui-search-bar__action__close`
- **Click Handler**: Triggers `reset()` method
- **Data Attribute**: `data-which="clear-search-icon"` for E2E testing
#### Methods
##### `reset(value?: unknown): void`
Resets the search input to the specified value and restores focus.
**Parameters:**
- `value?: unknown` - Optional override value (uses `value` input if not provided)
**Behavior:**
1. Calls `inputControl().reset(resetValue)` on the parent search bar's form control
2. Schedules focus restoration using `asapScheduler`
3. Focuses the input element via `inputControlElementRef()`
## Usage Examples
### Basic Search Bar
```typescript
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';
import { IconButtonComponent } from '@isa/ui/buttons';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaActionSearch } from '@isa/icons';
@Component({
selector: 'app-product-search',
standalone: true,
imports: [
ReactiveFormsModule,
UiSearchBarComponent,
UiSearchBarClearComponent,
IconButtonComponent
],
providers: [provideIcons({ isaActionSearch })],
template: `
<ui-search-bar>
<input
type="text"
[formControl]="searchControl"
placeholder="Search products..."
data-what="product-search-input"
data-which="main-search"
/>
<ui-search-bar-clear></ui-search-bar-clear>
<ui-icon-button
name="isaActionSearch"
(click)="performSearch()"
data-what="search-button"
></ui-icon-button>
</ui-search-bar>
`
})
export class ProductSearchComponent {
searchControl = new FormControl('');
performSearch(): void {
const query = this.searchControl.value;
console.log('Searching for:', query);
}
}
```
### Results Filter Bar
```typescript
import { Component, computed, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';
import { IconButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-results-filter',
standalone: true,
imports: [
ReactiveFormsModule,
UiSearchBarComponent,
UiSearchBarClearComponent,
IconButtonComponent
],
template: `
<ui-search-bar appearance="results">
<ui-icon-button prefix name="isaActionSearch"></ui-icon-button>
<input
type="text"
[formControl]="filterControl"
placeholder="Filter {{ totalResults() }} results..."
data-what="results-filter-input"
/>
<ui-search-bar-clear></ui-search-bar-clear>
</ui-search-bar>
<div class="results">
@for (item of filteredResults(); track item.id) {
<div class="result-item">{{ item.name }}</div>
}
</div>
`
})
export class ResultsFilterComponent {
filterControl = new FormControl('');
filterValue = toSignal(this.filterControl.valueChanges, { initialValue: '' });
allResults = signal([
{ id: 1, name: 'Product A' },
{ id: 2, name: 'Product B' },
{ id: 3, name: 'Product C' }
]);
filteredResults = computed(() => {
const filter = this.filterValue()?.toLowerCase() || '';
return this.allResults().filter(item =>
item.name.toLowerCase().includes(filter)
);
});
totalResults = computed(() => this.allResults().length);
}
```
### With Custom Reset Value
```typescript
@Component({
selector: 'app-category-search',
standalone: true,
imports: [
ReactiveFormsModule,
UiSearchBarComponent,
UiSearchBarClearComponent,
IconButtonComponent
],
template: `
<ui-search-bar>
<input
type="text"
[formControl]="searchControl"
placeholder="Search in {{ currentCategory }}..."
/>
<!-- Reset to current category instead of empty string -->
<ui-search-bar-clear [value]="currentCategory"></ui-search-bar-clear>
<ui-icon-button name="isaActionSearch"></ui-icon-button>
</ui-search-bar>
`
})
export class CategorySearchComponent {
currentCategory = 'Electronics';
searchControl = new FormControl(this.currentCategory);
}
```
### Reactive Search with Debouncing
```typescript
import { Component, inject, OnInit } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';
@Component({
selector: 'app-live-search',
standalone: true,
imports: [
ReactiveFormsModule,
UiSearchBarComponent,
UiSearchBarClearComponent,
IconButtonComponent
],
template: `
<ui-search-bar>
<input
type="text"
[formControl]="searchControl"
placeholder="Type to search..."
/>
<ui-search-bar-clear></ui-search-bar-clear>
<ui-icon-button name="isaActionSearch"></ui-icon-button>
</ui-search-bar>
@if (isSearching()) {
<div class="loading">Searching...</div>
}
<div class="search-results">
@for (result of searchResults(); track result.id) {
<div>{{ result.title }}</div>
}
</div>
`
})
export class LiveSearchComponent implements OnInit {
#searchService = inject(SearchService);
searchControl = new FormControl('');
isSearching = signal(false);
searchResults = signal<SearchResult[]>([]);
ngOnInit() {
this.searchControl.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe(query => {
if (query) {
this.performSearch(query);
} else {
this.searchResults.set([]);
}
});
}
async performSearch(query: string): Promise<void> {
this.isSearching.set(true);
try {
const results = await this.#searchService.search(query);
this.searchResults.set(results);
} finally {
this.isSearching.set(false);
}
}
}
```
### Advanced Multi-Slot Layout
```html
<ui-search-bar appearance="main">
<!-- Prefix icon for visual context -->
<ui-icon-button
prefix
name="isaActionSearch"
color="neutral"
data-what="search-prefix-icon"
></ui-icon-button>
<!-- Main input field -->
<input
type="text"
[formControl]="searchControl"
placeholder="Search by name, SKU, or barcode..."
(keydown.enter)="performSearch()"
data-what="advanced-search-input"
/>
<!-- Clear button (only visible when input has value) -->
@if (searchControl.value) {
<ui-search-bar-clear data-what="clear-search"></ui-search-bar-clear>
}
<!-- Search action button -->
<ui-icon-button
name="isaActionSearch"
color="brand"
[pending]="isSearching()"
(click)="performSearch()"
data-what="search-submit-button"
></ui-icon-button>
</ui-search-bar>
```
## Styling and Customization
### Host Classes
The component applies dynamic CSS classes based on appearance:
```typescript
// Main appearance
<ui-search-bar class="ui-search-bar ui-search-bar__main">
// Results appearance
<ui-search-bar class="ui-search-bar ui-search-bar__results">
```
### Custom Styles
Override the default styles using CSS:
```scss
// Customize main search bar
.ui-search-bar__main {
background-color: #ffffff;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 0.5rem;
input {
font-size: 1rem;
color: #333;
}
}
// Customize results filter bar
.ui-search-bar__results {
background-color: #f5f5f5;
border: 1px solid #d0d0d0;
border-radius: 4px;
input {
font-size: 0.875rem;
color: #666;
}
}
// Customize actions container
.ui-search-bar__actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
// Customize clear button
.ui-search-bar__action__close {
cursor: pointer;
color: #999;
transition: color 0.2s;
&:hover {
color: #333;
}
}
```
### ViewEncapsulation
Both components use `ViewEncapsulation.None`, allowing for:
- Global theme customization
- Consistent styling across the application
- Easy CSS variable integration
## Architecture Notes
### Design Pattern
The library uses a **Container + Action Component** pattern:
```
UiSearchBarComponent (Container)
Content Projection (Composition)
UiSearchBarClearComponent (Action)
```
### Content Child Pattern
The search bar accesses projected content using `contentChild`:
```typescript
inputControl = contentChild.required(NgControl, { read: NgControl });
inputControlElementRef = contentChild.required(NgControl, { read: ElementRef });
```
This provides:
- Type-safe access to the form control
- Direct DOM element reference for focus management
- Compile-time validation that required content is projected
### Focus Management
The clear button uses `asapScheduler` to restore focus asynchronously:
```typescript
reset(value?: unknown): void {
const resetValue = value ?? this.value();
this.searchBar.inputControl().reset(resetValue);
// Schedule focus after change detection
asapScheduler.schedule(() => {
this.searchBar.inputControlElementRef().nativeElement.focus();
});
}
```
This ensures:
- DOM updates complete before focus
- Smooth user experience
- No timing race conditions
### Parent-Child Communication
`UiSearchBarClearComponent` injects its parent:
```typescript
private searchBar = inject(UiSearchBarComponent);
```
This enables:
- Direct access to form control
- Focus restoration on input element
- Tight coupling between clear button and search bar
### Performance Considerations
1. **OnPush Change Detection** - Both components use OnPush strategy
2. **Computed Appearance Class** - Reactively updates without manual tracking
3. **Signal-Based Inputs** - Efficient change propagation
4. **Minimal DOM Operations** - Focus management only when needed
## Testing
The library uses **Jest** for testing.
### Running Tests
```bash
# Run tests for search-bar
npx nx test ui-search-bar --skip-nx-cache
# Run tests with coverage
npx nx test ui-search-bar --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-search-bar --watch
```
### Test Coverage
Tests should cover:
- **Appearance modes** - Correct class application for main and results
- **Content projection** - Input and icon slots work correctly
- **Clear functionality** - Reset and focus restoration
- **Form control integration** - NgControl binding and updates
- **Focus management** - Input receives focus after clear
- **E2E attributes** - `data-what` and `data-which` attributes present
### Example Test
```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Component } from '@angular/core';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';
@Component({
standalone: true,
imports: [ReactiveFormsModule, UiSearchBarComponent, UiSearchBarClearComponent],
template: `
<ui-search-bar [appearance]="appearance">
<input type="text" [formControl]="searchControl" />
<ui-search-bar-clear></ui-search-bar-clear>
</ui-search-bar>
`
})
class TestComponent {
searchControl = new FormControl('test value');
appearance: SearchbarAppearance = 'main';
}
describe('UiSearchBarComponent', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TestComponent]
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should apply main appearance class', () => {
const element = fixture.nativeElement.querySelector('.ui-search-bar');
expect(element.classList.contains('ui-search-bar__main')).toBe(true);
});
it('should clear input when clear button is clicked', () => {
const clearButton = fixture.nativeElement.querySelector('.ui-search-bar__action__close');
clearButton.click();
fixture.detectChanges();
expect(component.searchControl.value).toBeNull();
});
it('should restore focus after clear', async () => {
const input = fixture.nativeElement.querySelector('input');
const clearButton = fixture.nativeElement.querySelector('.ui-search-bar__action__close');
clearButton.click();
await fixture.whenStable();
expect(document.activeElement).toBe(input);
});
});
```
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework (v20.1.2)
- `@angular/forms` - Angular Forms for NgControl integration
- `@ng-icons/core` - Icon component system
- `@isa/icons` - ISA icon library
- `rxjs` - RxJS for `asapScheduler`
### Optional Dependencies
- `@isa/ui/buttons` - For icon button components (recommended)
### Path Alias
Import from: `@isa/ui/search-bar`
### Peer Dependencies
- Angular 20.1.2 or higher
- TypeScript 5.8.3 or higher
## Best Practices
### 1. Always Provide E2E Attributes
Include `data-what` and `data-which` attributes for testing:
```html
<ui-search-bar>
<input
type="text"
[formControl]="searchControl"
data-what="search-input"
data-which="product-search"
/>
<ui-search-bar-clear data-what="clear-button"></ui-search-bar-clear>
</ui-search-bar>
```
### 2. Use Reactive Forms
Always use `FormControl` for proper two-way binding:
```typescript
// Good: Reactive form control
searchControl = new FormControl('');
// Bad: ngModel or direct value binding
searchValue = '';
```
### 3. Debounce Search Operations
For live search, always debounce user input:
```typescript
ngOnInit() {
this.searchControl.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe(query => this.search(query));
}
```
### 4. Choose Appropriate Appearance
Use `'main'` for primary search, `'results'` for filtering:
```html
<!-- Primary search interface -->
<ui-search-bar appearance="main">...</ui-search-bar>
<!-- Results filtering -->
<ui-search-bar appearance="results">...</ui-search-bar>
```
### 5. Handle Empty States
Provide visual feedback when search returns no results:
```html
<ui-search-bar>
<input [formControl]="searchControl" />
<ui-search-bar-clear></ui-search-bar-clear>
</ui-search-bar>
@if (searchControl.value && results().length === 0) {
<div class="empty-state">No results found</div>
}
```
## License
Internal ISA Frontend library - not for external distribution.

View File

@@ -1,7 +1,482 @@
# ui-skeleton-loader
# @isa/ui/skeleton-loader
This library was generated with [Nx](https://nx.dev).
A lightweight Angular structural directive and component for displaying skeleton loading states during asynchronous operations.
## Running unit tests
## Overview
Run `nx test ui-skeleton-loader` to execute the unit tests.
The Skeleton Loader library provides a simple yet effective way to show loading placeholders while content is being fetched. It uses a structural directive pattern that conditionally replaces content with an animated skeleton loader, improving perceived performance and user experience during data loading.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Architecture Notes](#architecture-notes)
- [Testing](#testing)
- [Dependencies](#dependencies)
## Features
- **Structural directive pattern** - Seamlessly replaces content with skeleton loader
- **Signal-based reactivity** - Uses Angular signals for efficient change detection
- **Customizable dimensions** - Configure width and height via directive inputs
- **OnPush change detection** - Optimized for performance
- **Minimal footprint** - Lightweight implementation with no external dependencies
- **Smooth transitions** - Automatically handles view switching between loading and content states
- **Standalone component** - Can be used independently of the directive
## Quick Start
### 1. Import the Directive
```typescript
import { Component } from '@angular/core';
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [SkeletonLoaderDirective],
template: '...'
})
export class ProductListComponent {
isLoading = true;
}
```
### 2. Basic Usage
```html
<!-- Replace content with skeleton while loading -->
<div *uiSkeletonLoader="isLoading">
<h2>Product Name</h2>
<p>Product description goes here...</p>
</div>
```
### 3. Custom Dimensions
```html
<!-- Specify exact dimensions for the skeleton -->
<div *uiSkeletonLoader="isLoading; width: '200px'; height: '50px'">
Content to hide while loading
</div>
```
### 4. Using the Component Directly
```typescript
import { SkeletonLoaderComponent } from '@isa/ui/skeleton-loader';
@Component({
selector: 'app-card',
standalone: true,
imports: [SkeletonLoaderComponent],
template: `
@if (isLoading) {
<ui-skeleton-loader style="width: 100%; height: 200px;"></ui-skeleton-loader>
} @else {
<div class="card-content">{{ content }}</div>
}
`
})
export class CardComponent {
isLoading = true;
content = '';
}
```
## API Reference
### SkeletonLoaderDirective
Structural directive that conditionally replaces its content with a skeleton loader.
**Selector:** `[uiSkeletonLoader]`
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `uiSkeletonLoader` | `boolean` | `false` | When `true`, shows skeleton loader; when `false`, shows original content |
| `uiSkeletonLoaderWidth` | `string \| undefined` | `'inherit'` | CSS width value for the skeleton loader (e.g., '100px', '50%', 'inherit') |
| `uiSkeletonLoaderHeight` | `string \| undefined` | `'inherit'` | CSS height value for the skeleton loader (e.g., '20px', '100%', 'inherit') |
#### Implementation Details
- Uses `ViewContainerRef` to dynamically create/destroy views
- Employs `effect()` for reactive rendering based on input signals
- Automatically clears previous view before creating new one
- Applies dimension styles using `untracked()` to prevent circular updates
### SkeletonLoaderComponent
Standalone component that renders a skeleton loading animation.
**Selector:** `ui-skeleton-loader`
#### Features
- OnPush change detection strategy
- ViewEncapsulation.None for global styling
- Host class: `ui-skeleton-loader`
- Single animated bar element with class `ui-skeleton-loader-bar`
#### Template Structure
```html
<div class="ui-skeleton-loader-bar"></div>
```
## Usage Examples
### Basic Loading State
```typescript
import { Component, signal } from '@angular/core';
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [SkeletonLoaderDirective],
template: `
<div *uiSkeletonLoader="isLoading()">
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
</div>
`
})
export class UserProfileComponent {
isLoading = signal(true);
user = { name: '', email: '' };
ngOnInit() {
// Simulate API call
setTimeout(() => {
this.user = { name: 'John Doe', email: 'john@example.com' };
this.isLoading.set(false);
}, 2000);
}
}
```
### List with Multiple Skeletons
```html
<div class="product-list">
@for (item of items; track item.id) {
<div
class="product-card"
*uiSkeletonLoader="isLoading; width: '100%'; height: '120px'"
>
<img [src]="item.image" alt="{{ item.name }}">
<h3>{{ item.name }}</h3>
<p>{{ item.price | currency }}</p>
</div>
}
</div>
```
### Table Row Skeletons
```html
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (row of tableData; track row.id) {
<tr *uiSkeletonLoader="isLoadingRow(row.id); height: '48px'">
<td>{{ row.name }}</td>
<td>{{ row.email }}</td>
<td>{{ row.status }}</td>
</tr>
}
</tbody>
</table>
```
### Using with RxJS Observables
```typescript
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
@Component({
selector: 'app-data-viewer',
standalone: true,
imports: [SkeletonLoaderDirective],
template: `
<div *uiSkeletonLoader="isLoading(); width: '100%'; height: '400px'">
@if (data(); as dataValue) {
<pre>{{ dataValue | json }}</pre>
}
</div>
`
})
export class DataViewerComponent {
#dataService = inject(DataService);
data$ = this.#dataService.getData();
data = toSignal(this.data$);
isLoading = toSignal(
this.data$.pipe(map(data => !data)),
{ initialValue: true }
);
}
```
### Standalone Component Usage
```html
<!-- When you need direct control over skeleton rendering -->
<div class="custom-container">
@if (isLoading) {
<ui-skeleton-loader
style="width: 100%; height: 200px; margin-bottom: 1rem;"
></ui-skeleton-loader>
<ui-skeleton-loader
style="width: 80%; height: 100px;"
></ui-skeleton-loader>
} @else {
<div class="actual-content">
{{ content }}
</div>
}
</div>
```
## Styling and Customization
### Default Styling
The skeleton loader applies the following host classes:
- `ui-skeleton-loader` - Applied to the component host element
- `ui-skeleton-loader-bar` - Applied to the animated bar element
### Custom Styles
You can customize the skeleton appearance using CSS:
```scss
// Custom skeleton colors and animation
.ui-skeleton-loader {
background-color: #f0f0f0;
border-radius: 4px;
}
.ui-skeleton-loader-bar {
background: linear-gradient(
90deg,
#f0f0f0 0%,
#e0e0e0 50%,
#f0f0f0 100%
);
animation: skeleton-loading 1.5s ease-in-out infinite;
}
@keyframes skeleton-loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
```
### Dimension Inheritance
When `width` or `height` are not specified (or set to `'inherit'`), the skeleton loader inherits dimensions from its parent container:
```html
<!-- Skeleton inherits parent's dimensions -->
<div style="width: 300px; height: 150px;">
<div *uiSkeletonLoader="isLoading">
Content
</div>
</div>
```
### ViewEncapsulation
The component uses `ViewEncapsulation.None`, allowing global styles to apply. This enables:
- Consistent styling across the application
- Easy theming via global CSS variables
- Override styles from parent components
## Architecture Notes
### Design Pattern
The library follows the **Structural Directive + Presentation Component** pattern:
```
SkeletonLoaderDirective (Structural Logic)
Creates/Destroys views dynamically
SkeletonLoaderComponent (Presentation)
```
### Signal-Based Reactivity
The directive uses Angular signals for optimal performance:
```typescript
// Reactive rendering
render = effect(() => {
const condition = this.uiSkeletonLoader();
if (condition && !this.componentRef) {
// Show skeleton
} else if (!condition && !this.embeddedViewRef) {
// Show content
}
});
// Reactive style updates
updateStyles = effect(() => {
this.applyStyles();
});
```
### View Management
The directive efficiently manages view lifecycle:
1. **Loading State** (`condition = true`):
- Clears existing views
- Creates `SkeletonLoaderComponent` instance
- Applies dimension styles
- Clears embedded view reference
2. **Loaded State** (`condition = false`):
- Clears existing views
- Creates embedded view from template
- Clears component reference
### Memory Management
- Component/view references are properly cleaned up
- Uses `untracked()` to prevent circular signal updates
- OnPush change detection reduces unnecessary checks
### Performance Considerations
1. **Minimal DOM Operations** - Only creates/destroys views when state changes
2. **Signal Efficiency** - Uses computed values and effects for reactive updates
3. **OnPush Strategy** - Component only checks when inputs change
4. **Untracked Styles** - Style application doesn't trigger additional effects
## Testing
The library uses **Jest** for testing.
### Running Tests
```bash
# Run tests for skeleton-loader
npx nx test ui-skeleton-loader --skip-nx-cache
# Run tests with coverage
npx nx test ui-skeleton-loader --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-skeleton-loader --watch
```
### Test Coverage
The library includes comprehensive tests covering:
- **Directive behavior** - View creation/destruction based on condition
- **Dimension application** - Width/height style application
- **Signal reactivity** - Proper effect execution
- **View lifecycle** - Component and template ref management
- **Edge cases** - Undefined dimensions, rapid state changes
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework (v20.1.2)
### Path Alias
Import from: `@isa/ui/skeleton-loader`
### Peer Dependencies
- Angular 20.1.2 or higher
- TypeScript 5.8.3 or higher
## Best Practices
### 1. Use for Async Operations
Always tie the skeleton loader to actual loading states:
```typescript
// Good: Tied to actual data loading
isLoading = toSignal(this.data$.pipe(map(data => !data)));
// Bad: Arbitrary timeout
setTimeout(() => this.isLoading = false, 1000);
```
### 2. Match Skeleton to Content
Ensure skeleton dimensions approximate actual content:
```html
<!-- Good: Skeleton matches content dimensions -->
<div *uiSkeletonLoader="isLoading; width: '100%'; height: '48px'">
<h2 class="text-xl">{{ title }}</h2> <!-- ~48px height -->
</div>
<!-- Bad: Skeleton much smaller than content -->
<div *uiSkeletonLoader="isLoading; height: '20px'">
<div class="large-card">...</div> <!-- 300px height -->
</div>
```
### 3. Avoid Nested Skeletons
Don't nest skeleton loader directives:
```html
<!-- Bad: Nested skeletons -->
<div *uiSkeletonLoader="isLoadingOuter">
<div *uiSkeletonLoader="isLoadingInner">
Content
</div>
</div>
<!-- Good: Single skeleton or separate skeletons -->
<div *uiSkeletonLoader="isLoading">
<div>Content</div>
</div>
```
### 4. Use Inherit for Flexible Layouts
Let the skeleton inherit dimensions for responsive layouts:
```html
<!-- Skeleton automatically adapts to container -->
<div class="responsive-container">
<div *uiSkeletonLoader="isLoading">
{{ content }}
</div>
</div>
```
## License
Internal ISA Frontend library - not for external distribution.

View File

@@ -1,7 +1,614 @@
# ui-toolbar
# @isa/ui/toolbar
This library was generated with [Nx](https://nx.dev).
A flexible toolbar container component for Angular applications with configurable sizing and content projection.
## Running unit tests
## Overview
Run `nx test ui-toolbar` to execute the unit tests.
The Toolbar library provides a single component (`ui-toolbar`) that serves as a styled container for toolbar content. It supports two size variants (small and medium) and uses content projection to allow flexible composition of toolbar items such as buttons, inputs, and action controls.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [Component API](#component-api)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Architecture Notes](#architecture-notes)
- [Testing](#testing)
- [Dependencies](#dependencies)
## Features
- **Two size variants** - Small and medium toolbar heights
- **Content projection** - Full flexibility for toolbar content composition
- **ViewEncapsulation.None** - Allows external styling
- **OnPush change detection** - Optimized performance
- **Signal-based sizing** - Reactive size computation with `computed()`
- **Host class bindings** - Dynamic CSS class application
- **Standalone component** - Modern Angular architecture
## Quick Start
### 1. Import Component
```typescript
import { Component } from '@angular/core';
import { ToolbarComponent } from '@isa/ui/toolbar';
@Component({
selector: 'app-page-header',
imports: [ToolbarComponent],
template: `...`
})
export class PageHeaderComponent {}
```
### 2. Basic Usage
```html
<ui-toolbar>
<h1>Page Title</h1>
<button>Action</button>
</ui-toolbar>
```
### 3. Size Variants
```html
<!-- Medium toolbar (default) -->
<ui-toolbar>
<span>Default medium toolbar</span>
</ui-toolbar>
<!-- Small toolbar -->
<ui-toolbar [size]="'small'">
<span>Compact toolbar</span>
</ui-toolbar>
```
## Component API
### ToolbarComponent
Container component for toolbar content with size variants.
#### Selector
```typescript
'ui-toolbar'
```
#### Inputs
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `size` | `ToolbarSize` | `'medium'` | Size variant of the toolbar |
#### Size Options
```typescript
export const ToolbarSize = {
Small: 'small',
Medium: 'medium',
} as const;
export type ToolbarSize = (typeof ToolbarSize)[keyof typeof ToolbarSize];
```
#### Computed Properties
| Property | Type | Description |
|----------|------|-------------|
| `sizeClass()` | `string` | Computed CSS class based on size (`ui-toolbar__small` or `ui-toolbar__medium`) |
#### Host Bindings
- `class`: `['ui-toolbar', sizeClass()]` - Base class and size-specific class
#### Template
```html
<ng-content></ng-content>
```
#### Encapsulation
- Uses `ViewEncapsulation.None` to allow external styling of toolbar content
## Usage Examples
### Page Header Toolbar
```typescript
import { Component } from '@angular/core';
import { ToolbarComponent } from '@isa/ui/toolbar';
import { ButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-page-header',
imports: [ToolbarComponent, ButtonComponent],
template: `
<ui-toolbar>
<h1 class="page-title">Dashboard</h1>
<div class="toolbar-actions">
<ui-button (click)="refresh()">Refresh</ui-button>
<ui-button appearance="accent" (click)="create()">Create</ui-button>
</div>
</ui-toolbar>
`,
styles: [`
.page-title {
flex: 1;
margin: 0;
}
.toolbar-actions {
display: flex;
gap: 0.5rem;
}
`]
})
export class PageHeaderComponent {
refresh() { /* ... */ }
create() { /* ... */ }
}
```
### Search Toolbar
```typescript
import { Component } from '@angular/core';
import { ToolbarComponent, ToolbarSize } from '@isa/ui/toolbar';
import { TextFieldComponent, InputControlDirective } from '@isa/ui/input-controls';
@Component({
selector: 'app-search-toolbar',
imports: [ToolbarComponent, TextFieldComponent, InputControlDirective],
template: `
<ui-toolbar [size]="ToolbarSize.Small">
<ui-text-field>
<input uiInputControl type="text" placeholder="Search..." />
</ui-text-field>
<button (click)="search()">Search</button>
</ui-toolbar>
`
})
export class SearchToolbarComponent {
readonly ToolbarSize = ToolbarSize;
search() { /* ... */ }
}
```
### Filter Toolbar with Multiple Controls
```typescript
import { Component, signal } from '@angular/core';
import { ToolbarComponent } from '@isa/ui/toolbar';
import { DropdownButtonComponent, DropdownOptionComponent } from '@isa/ui/input-controls';
@Component({
selector: 'app-filter-toolbar',
imports: [ToolbarComponent, DropdownButtonComponent, DropdownOptionComponent],
template: `
<ui-toolbar>
<span class="filter-label">Filters:</span>
<ui-dropdown [(value)]="selectedStatus" label="Status">
<ui-dropdown-option [value]="'all'">All</ui-dropdown-option>
<ui-dropdown-option [value]="'active'">Active</ui-dropdown-option>
<ui-dropdown-option [value]="'inactive'">Inactive</ui-dropdown-option>
</ui-dropdown>
<ui-dropdown [(value)]="selectedCategory" label="Category">
<ui-dropdown-option [value]="'all'">All</ui-dropdown-option>
<ui-dropdown-option [value]="'electronics'">Electronics</ui-dropdown-option>
<ui-dropdown-option [value]="'books'">Books</ui-dropdown-option>
</ui-dropdown>
<button (click)="resetFilters()">Reset</button>
</ui-toolbar>
`,
styles: [`
.filter-label {
font-weight: 600;
margin-right: 0.5rem;
}
`]
})
export class FilterToolbarComponent {
selectedStatus = signal('all');
selectedCategory = signal('all');
resetFilters() {
this.selectedStatus.set('all');
this.selectedCategory.set('all');
}
}
```
### Dynamic Size Toolbar
```typescript
import { Component, signal } from '@angular/core';
import { ToolbarComponent, ToolbarSize } from '@isa/ui/toolbar';
@Component({
selector: 'app-dynamic-toolbar',
imports: [ToolbarComponent],
template: `
<ui-toolbar [size]="currentSize()">
<span>Toolbar Content</span>
<button (click)="toggleSize()">Toggle Size</button>
</ui-toolbar>
`
})
export class DynamicToolbarComponent {
currentSize = signal<ToolbarSize>(ToolbarSize.Medium);
toggleSize() {
const newSize = this.currentSize() === ToolbarSize.Medium
? ToolbarSize.Small
: ToolbarSize.Medium;
this.currentSize.set(newSize);
}
}
```
### Toolbar with Icons and Actions
```typescript
import { Component } from '@angular/core';
import { ToolbarComponent } from '@isa/ui/toolbar';
import { NgIcon } from '@ng-icons/core';
import { ButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'app-actions-toolbar',
imports: [ToolbarComponent, ButtonComponent, NgIcon],
template: `
<ui-toolbar>
<div class="toolbar-section">
<ng-icon name="isaActionMenu" size="1.5rem"></ng-icon>
<span class="toolbar-title">Orders</span>
</div>
<div class="toolbar-section">
<ui-button (click)="export()">
<ng-icon name="isaActionDownload"></ng-icon>
Export
</ui-button>
<ui-button appearance="accent" (click)="createOrder()">
<ng-icon name="isaActionPlus"></ng-icon>
New Order
</ui-button>
</div>
</ui-toolbar>
`,
styles: [`
.toolbar-section {
display: flex;
align-items: center;
gap: 0.75rem;
}
.toolbar-title {
font-size: 1.25rem;
font-weight: 600;
}
`]
})
export class ActionsToolbarComponent {
export() { /* ... */ }
createOrder() { /* ... */ }
}
```
### Responsive Toolbar with Breakpoints
```typescript
import { Component, computed } from '@angular/core';
import { ToolbarComponent, ToolbarSize } from '@isa/ui/toolbar';
import { breakpoint, Breakpoint } from '@isa/ui/layout';
@Component({
selector: 'app-responsive-toolbar',
imports: [ToolbarComponent],
template: `
<ui-toolbar [size]="toolbarSize()">
@if (isDesktop()) {
<h1>Full Desktop Title</h1>
<div class="actions">
<button>Action 1</button>
<button>Action 2</button>
<button>Action 3</button>
</div>
} @else {
<h1>Mobile</h1>
<button>Menu</button>
}
</ui-toolbar>
`
})
export class ResponsiveToolbarComponent {
isDesktop = breakpoint([Breakpoint.Desktop, Breakpoint.DekstopL, Breakpoint.DekstopXL]);
toolbarSize = computed(() =>
this.isDesktop() ? ToolbarSize.Medium : ToolbarSize.Small
);
}
```
## Styling and Customization
### CSS Classes
The component exposes the following CSS classes for styling:
```css
/* Base toolbar class */
.ui-toolbar {
/* Base toolbar styles */
}
/* Medium toolbar (default) */
.ui-toolbar__medium {
/* Medium size styles */
}
/* Small toolbar */
.ui-toolbar__small {
/* Small size styles */
}
```
### Example Styling
```scss
// Base toolbar styling
.ui-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
background-color: var(--isa-surface-100);
border-bottom: 1px solid var(--isa-border-200);
// Medium toolbar (default height)
&.ui-toolbar__medium {
height: 4rem;
padding: 0 1.5rem;
}
// Small toolbar (compact height)
&.ui-toolbar__small {
height: 3rem;
padding: 0 1rem;
font-size: 0.875rem;
}
}
// Toolbar sections and layouts
.ui-toolbar {
.toolbar-section {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toolbar-title {
font-weight: 600;
font-size: 1.125rem;
}
.toolbar-actions {
display: flex;
gap: 0.5rem;
margin-left: auto;
}
}
```
### Dark Theme Example
```scss
// Dark mode toolbar
.dark-theme {
.ui-toolbar {
background-color: var(--isa-surface-900);
border-bottom-color: var(--isa-border-700);
color: var(--isa-text-inverse);
}
}
```
## Architecture Notes
### Size Management
The component uses Angular signals for reactive size management:
```typescript
size = input<ToolbarSize>('medium');
sizeClass = computed(() => `ui-toolbar__${this.size()}`);
```
Benefits:
- **Automatic updates** - Size changes trigger class recomputation
- **Type safety** - ToolbarSize type ensures valid values
- **Performance** - OnPush detection with signals
### ViewEncapsulation.None
The component uses `ViewEncapsulation.None` to allow external styling:
```typescript
@Component({
encapsulation: ViewEncapsulation.None
})
```
This enables:
- Parent components can style toolbar content
- Global styles can target toolbar elements
- Flexible theme integration
### Content Projection
The toolbar uses simple content projection for maximum flexibility:
```html
<ng-content></ng-content>
```
This allows any content structure:
- Buttons, inputs, dropdowns
- Custom components
- Icons and text
- Complex layouts
### Standalone Architecture
The component is fully standalone with no dependencies:
```typescript
@Component({
standalone: true,
imports: []
})
```
## Testing
The library uses **Jest** for testing.
### Running Tests
```bash
# Run tests for this library
npx nx test ui-toolbar --skip-nx-cache
# Run tests with coverage
npx nx test ui-toolbar --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-toolbar --watch
```
### Test Coverage
The library should include tests covering:
- **Component rendering** - Verifies toolbar renders correctly
- **Size variants** - Tests both small and medium sizes
- **Content projection** - Validates content appears correctly
- **Class bindings** - Tests dynamic class application
- **Signal reactivity** - Tests size changes update classes
### Example Test
```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToolbarComponent, ToolbarSize } from './toolbar.component';
describe('ToolbarComponent', () => {
let component: ToolbarComponent;
let fixture: ComponentFixture<ToolbarComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ToolbarComponent]
});
fixture = TestBed.createComponent(ToolbarComponent);
component = fixture.componentInstance;
});
it('should apply medium size by default', () => {
fixture.detectChanges();
expect(component.sizeClass()).toBe('ui-toolbar__medium');
});
it('should apply small size when specified', () => {
component.size = ToolbarSize.Small;
fixture.detectChanges();
expect(component.sizeClass()).toBe('ui-toolbar__small');
});
it('should project content correctly', () => {
const compiled = fixture.nativeElement;
compiled.innerHTML = '<span>Test Content</span>';
fixture.detectChanges();
expect(compiled.textContent).toContain('Test Content');
});
});
```
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework (v20.1.2)
### Path Alias
Import from: `@isa/ui/toolbar`
### No External Dependencies
This component has no external dependencies beyond Angular core, making it lightweight and easy to integrate.
## Best Practices
1. **Consistent Sizing** - Use the same toolbar size throughout similar contexts (e.g., all page headers use medium)
2. **Flexbox Layouts** - Structure toolbar content using flexbox for responsive behavior
3. **Semantic HTML** - Use appropriate HTML elements within the toolbar (headings, buttons, etc.)
4. **Accessibility** - Ensure toolbar controls are keyboard accessible and have proper ARIA labels
5. **Content Organization** - Group related toolbar items visually using spacing and dividers
6. **Responsive Design** - Consider using the breakpoint service to adapt toolbar content for different screen sizes
## Common Patterns
### Left-Right Layout
```html
<ui-toolbar>
<div class="toolbar-left">
<h1>Title</h1>
<span>Subtitle</span>
</div>
<div class="toolbar-right">
<button>Action 1</button>
<button>Action 2</button>
</div>
</ui-toolbar>
<style>
.ui-toolbar {
display: flex;
justify-content: space-between;
}
</style>
```
### Three-Section Layout
```html
<ui-toolbar>
<div class="toolbar-left">Left content</div>
<div class="toolbar-center">Center content</div>
<div class="toolbar-right">Right content</div>
</ui-toolbar>
<style>
.ui-toolbar {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
}
.toolbar-center {
justify-self: center;
}
.toolbar-right {
justify-self: end;
}
</style>
```
## License
Internal ISA Frontend library - not for external distribution.