@molecule/app-ui-angular

Framework · ui · App (browser) · v1.0.2 · Apache-2.0

Angular UI components for molecule.dev

npm install @molecule/app-ui-angular

npm · Source on GitHub · Implements @molecule/app-ui

How it works

@molecule/app-ui-angular adapts the @molecule/* cores to the ui framework on the app (browser) side.

import { Component } from '@angular/core'
import { setClassMap } from '@molecule/app-ui'
import { classMap } from '@molecule/app-ui-tailwind'
import { MoleculeButton, MoleculeInput } from '@molecule/app-ui-angular'

// Once at startup, before the first component renders:
setClassMap(classMap)

@Component({
  selector: 'app-save-panel',
  standalone: true,
  imports: [MoleculeButton, MoleculeInput],
  template: `
    <mol-input placeholder="Name" [value]="name" (handleInput)="onName($event)"></mol-input>
    <mol-button color="primary" [loading]="saving" (handleClick)="save()">Save</mol-button>
  `,
})
class SavePanel {
  name = ''
  saving = false
  onName(event: Event): void {
    this.name = (event.target as HTMLInputElement).value
  }
  save(): void {}
}

Works with: @molecule/app-i18n, @molecule/app-icons, @molecule/app-ui

Reference

Angular UI components for molecule.dev.

Provides standalone Angular components implementing a SUBSET of the @molecule/app-ui component interfaces, styled through the UIClassMap abstraction. Ships 12 components — MoleculeAlert, MoleculeBadge, MoleculeButton, MoleculeCheckbox, MoleculeInput, MoleculeModal, MoleculeRadioGroup, MoleculeSelect, MoleculeSpinner, MoleculeSwitch, MoleculeToast, MoleculeTooltip — each a standalone component with a mol-* selector. Layout/data components (Card, Table, Tabs, Accordion, Dropdown, Icon, …) are not implemented for Angular yet; compose them from ClassMap classes via getClassMap().

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { Component } from '@angular/core'
import { setClassMap } from '@molecule/app-ui'
import { classMap } from '@molecule/app-ui-tailwind'
import { MoleculeButton, MoleculeInput } from '@molecule/app-ui-angular'

// Once at startup, before the first component renders:
setClassMap(classMap)

@Component({
  selector: 'app-save-panel',
  standalone: true,
  imports: [MoleculeButton, MoleculeInput],
  template: `
    <mol-input placeholder="Name" [value]="name" (handleInput)="onName($event)"></mol-input>
    <mol-button color="primary" [loading]="saving" (handleClick)="save()">Save</mol-button>
  `,
})
class SavePanel {
  name = ''
  saving = false
  onName(event: Event): void {
    this.name = (event.target as HTMLInputElement).value
  }
  save(): void {}
}

Type

framework

Installation

npm install @molecule/app-ui-angular @angular/core @angular/platform-browser @molecule/app-i18n @molecule/app-icons @molecule/app-ui

API

Interfaces

CSSProperties

Framework-agnostic CSS properties. Mirrors React.CSSProperties but without React dependency.

interface CSSProperties {
  [key: string]: string | number | undefined
}

BaseProps

Base props shared by all components.

interface BaseProps {
  /**
   * Additional CSS class name(s).
   */
  className?: string
  /**
   * Inline styles.
   */
  style?: CSSProperties
  /**
   * Test ID for automated testing.
   */
  testId?: string
  /**
   * Automation ID for AI agents and E2E tests. Maps to the `data-mol-id`
   * HTML attribute. Use `molId()` from `./automation.js` to generate
   * semantic IDs. (Tooling only — screen readers do not expose `data-*`
   * attributes; accessible names come from labels/`aria-*`.)
   */
  automationId?: string
  /**
   * Whether the component is disabled.
   */
  disabled?: boolean
}

HTMLElementProps

Base props for HTML elements. Framework bindings should extend this with framework-specific attributes.

interface HTMLElementProps extends BaseProps {
  id?: string
  title?: string
  tabIndex?: number
  role?: string
  'aria-label'?: string
  'aria-labelledby'?: string
  'aria-describedby'?: string
  'aria-hidden'?: boolean
  'aria-disabled'?: boolean
  'aria-expanded'?: boolean
  'aria-selected'?: boolean
  'aria-checked'?: boolean | 'mixed'
  'aria-pressed'?: boolean | 'mixed'
  'aria-invalid'?: boolean
  'aria-required'?: boolean
  'aria-readonly'?: boolean
  'aria-busy'?: boolean
  'aria-live'?: 'off' | 'polite' | 'assertive'
  onClick?: MouseEventHandler
  onDoubleClick?: MouseEventHandler
  onMouseEnter?: MouseEventHandler
  onMouseLeave?: MouseEventHandler
  onFocus?: FocusEventHandler
  onBlur?: FocusEventHandler
  onKeyDown?: KeyboardEventHandler
  onKeyUp?: KeyboardEventHandler
  onKeyPress?: KeyboardEventHandler
}

InputElementProps

Base props for input elements.

interface InputElementProps extends HTMLElementProps {
  name?: string
  value?: string | number | readonly string[]
  defaultValue?: string | number | readonly string[]
  placeholder?: string
  required?: boolean
  readOnly?: boolean
  autoFocus?: boolean
  autoComplete?: string
  maxLength?: number
  minLength?: number
  pattern?: string
  onChange?: ChangeEventHandler
  onInput?: FormEventHandler
}

ButtonElementProps

Base props for button elements.

interface ButtonElementProps extends HTMLElementProps {
  /**
   * Defaults to `'button'` in every framework binding, so a button never submits a
   * form by accident. A form's primary action must set `type="submit"`: Enter then
   * submits the form, and tests that select `button[type="submit"]` find it.
   */
  type?: 'button' | 'submit' | 'reset'
  name?: string
  value?: string
  form?: string
}

TextareaElementProps

Base props for textarea elements.

interface TextareaElementProps extends HTMLElementProps {
  name?: string
  value?: string
  defaultValue?: string
  placeholder?: string
  required?: boolean
  readOnly?: boolean
  autoFocus?: boolean
  rows?: number
  cols?: number
  maxLength?: number
  minLength?: number
  wrap?: 'hard' | 'soft' | 'off'
  onChange?: ChangeEventHandler
  onInput?: FormEventHandler
}

SelectElementProps

Base props for select elements.

interface SelectElementProps extends HTMLElementProps {
  name?: string
  required?: boolean
  autoFocus?: boolean
  multiple?: boolean
  onChange?: ChangeEventHandler
}

FormElementProps

Base props for form elements.

interface FormElementProps extends HTMLElementProps {
  action?: string
  method?: 'get' | 'post'
  encType?: string
  target?: string
  noValidate?: boolean
  autoComplete?: 'on' | 'off'
  onSubmit?: FormEventHandler
  onReset?: FormEventHandler
}

ButtonProps

Props for the Button component.

interface ButtonProps extends ButtonElementProps {
  /**
   * Button content.
   */
  children?: Children
  /**
   * Visual variant.
   */
  variant?: ButtonVariant
  /**
   * Color scheme.
   */
  color?: ColorVariant
  /**
   * Button size.
   */
  size?: ButtonSize
  /**
   * Whether the button is in a loading state.
   */
  loading?: boolean
  /**
   * Loading text to display.
   */
  loadingText?: string
  /**
   * Whether the button takes full width.
   */
  fullWidth?: boolean
  /**
   * Icon to display before the label.
   */
  leftIcon?: Children
  /**
   * Icon to display after the label.
   */
  rightIcon?: Children
}

InputProps

Props for the Input component (text field, email, password, etc.).

interface InputProps extends InputElementProps {
  /**
   * Input type.
   */
  type?: InputType
  /**
   * Input size.
   */
  size?: Size
  /**
   * Horizontal text alignment inside the input (see
   * `InputClassOptions.align`). Defaults to the bond's own style.
   */
  align?: 'left' | 'center'
  /**
   * Label text.
   */
  label?: string
  /**
   * Error message.
   */
  error?: string
  /**
   * Hint/help text.
   */
  hint?: string
  /**
   * Element to display on the left.
   */
  leftElement?: Children
  /**
   * Element to display on the right.
   */
  rightElement?: Children
  /**
   * Whether to show a clear button.
   */
  clearable?: boolean
  /**
   * Called when the clear button is clicked.
   */
  onClear?: () => void
  /**
   * Accessible label for the clear button.
   * @default 'Clear'
   */
  clearLabel?: string
}

TextareaProps

Props for the Textarea component.

interface TextareaProps extends TextareaElementProps {
  /**
   * Text size tier (see `TextareaClassOptions.size`) — pass the same
   * `size` as a neighboring Input for identical font sizes.
   */
  size?: Size
  /**
   * Label text.
   */
  label?: string
  /**
   * Error message.
   */
  error?: string
  /**
   * Hint/help text.
   */
  hint?: string
  /**
   * Whether the textarea auto-resizes.
   */
  autoResize?: boolean
  /**
   * Minimum number of rows.
   */
  minRows?: number
  /**
   * Maximum number of rows.
   */
  maxRows?: number
}

SelectOption

A single option in a Select dropdown.

interface SelectOption<T = string> {
  /**
   * Option value.
   */
  value: T
  /**
   * Display label.
   */
  label: string
  /**
   * Whether the option is disabled.
   */
  disabled?: boolean
  /**
   * Option group (for grouped selects).
   */
  group?: string
}

SelectProps

Props for the Select dropdown component (single or multi-select).

interface SelectProps<T = string> extends SelectElementProps {
  /**
   * Select options.
   */
  options: SelectOption<T>[]
  /**
   * Current value.
   */
  value?: T
  /**
   * Change handler (with typed value).
   */
  onValueChange?: (value: T) => void
  /**
   * Select size.
   */
  size?: Size
  /**
   * Label text.
   */
  label?: string
  /**
   * Placeholder text.
   */
  placeholder?: string
  /**
   * Error message.
   */
  error?: string
  /**
   * Hint/help text.
   */
  hint?: string
  /**
   * Whether to allow clearing the selection.
   */
  clearable?: boolean
}

CheckboxProps

Props for the Checkbox component.

interface CheckboxProps extends InputElementProps {
  /**
   * Checkbox label.
   */
  label?: Children
  /**
   * Whether the checkbox is checked.
   */
  checked?: boolean
  /**
   * Whether the checkbox is in an indeterminate state.
   */
  indeterminate?: boolean
  /**
   * Checkbox size.
   */
  size?: Size
  /**
   * Error message.
   */
  error?: string
}

RadioOption

A single option in a RadioGroup.

interface RadioOption<T = string> {
  /**
   * Option value.
   */
  value: T
  /**
   * Display label.
   */
  label: Children
  /**
   * Whether the option is disabled.
   */
  disabled?: boolean
}

RadioGroupProps

Props for the RadioGroup component.

interface RadioGroupProps<T = string> extends BaseProps {
  /**
   * Radio options.
   */
  options: RadioOption<T>[]
  /**
   * Current value.
   */
  value?: T
  /**
   * Change handler.
   */
  onChange?: (value: T) => void
  /**
   * Radio size.
   */
  size?: Size
  /**
   * Group label.
   */
  label?: string
  /**
   * Shared `name` attribute for the group's radio inputs (used for native
   * form submission). When omitted, a unique per-instance name is generated
   * so separate groups never merge — the visible `label` is deliberately
   * NOT used as the name, because two groups with the same label (e.g. two
   * "Size" pickers) would otherwise form ONE native radio group and
   * deselect each other.
   */
  name?: string
  /**
   * Layout direction.
   */
  direction?: 'horizontal' | 'vertical'
  /**
   * Error message.
   */
  error?: string
}

SwitchProps

Props for the Switch/Toggle component.

interface SwitchProps extends InputElementProps {
  /**
   * Switch label.
   */
  label?: Children
  /**
   * Whether the switch is on.
   */
  checked?: boolean
  /**
   * Switch size.
   */
  size?: Size
  /**
   * Color when on.
   */
  color?: ColorVariant
}

ModalProps

Props for the Modal/Dialog component.

interface ModalProps extends HTMLElementProps {
  /**
   * Whether the modal is open.
   */
  open: boolean
  /**
   * Called when the modal should close.
   */
  onClose: () => void
  /**
   * Modal title.
   */
  title?: string
  /**
   * Modal content.
   */
  children?: Children
  /**
   * Modal size.
   */
  size?: ModalSize
  /**
   * Whether to show a close button.
   */
  showCloseButton?: boolean
  /**
   * Whether clicking the overlay closes the modal.
   */
  closeOnOverlayClick?: boolean
  /**
   * Whether pressing Escape closes the modal.
   */
  closeOnEscape?: boolean
  /**
   * Footer content (typically action buttons).
   */
  footer?: Children
  /**
   * Whether the modal is centered vertically.
   */
  centered?: boolean
  /**
   * Whether to prevent body scroll when open.
   */
  preventScroll?: boolean
  /**
   * Accessible label for the close button.
   * @default 'Close'
   */
  closeLabel?: string
}

SpinnerProps

Props for the Spinner/loading indicator component.

interface SpinnerProps extends BaseProps {
  /**
   * Spinner size.
   */
  size?: Size
  /**
   * Spinner color.
   */
  color?: ColorVariant | string
  /**
   * Loading label (for accessibility).
   */
  label?: string
  /**
   * Spinner thickness.
   */
  thickness?: number
}

AvatarProps

Props for the Avatar component.

interface AvatarProps extends HTMLElementProps {
  /**
   * Image source URL.
   */
  src?: string
  /**
   * Alt text for the image.
   */
  alt?: string
  /**
   * Name for fallback initials.
   */
  name?: string
  /**
   * Avatar size.
   */
  size?: Size | number
  /**
   * Whether the avatar is rounded.
   */
  rounded?: boolean
  /**
   * Fallback element when no image.
   */
  fallback?: Children
}

BadgeProps

Props for the Badge component (status labels, counts, tags).

interface BadgeProps extends HTMLElementProps {
  /**
   * Badge content.
   */
  children?: Children
  /**
   * Badge color.
   */
  color?: ColorVariant
  /**
   * Badge variant.
   */
  variant?: 'solid' | 'outline' | 'subtle'
  /**
   * Badge size.
   */
  size?: Size
  /**
   * Whether the badge is rounded.
   */
  rounded?: boolean
}

TooltipProps

Props for the Tooltip component (hover/focus popover with informational text).

interface TooltipProps extends HTMLElementProps {
  /**
   * Tooltip content.
   */
  content: Children
  /**
   * Element that triggers the tooltip.
   */
  children: Children
  /**
   * Tooltip placement.
   */
  placement?: TooltipPlacement
  /**
   * Delay before showing (ms).
   */
  delay?: number
  /**
   * Whether the tooltip has an arrow.
   */
  hasArrow?: boolean
}

CardProps

Props for the Card container component (elevated, outlined, or filled surface).

interface CardProps extends HTMLElementProps {
  /**
   * Card content.
   */
  children?: Children
  /**
   * Card variant.
   */
  variant?: 'elevated' | 'outlined' | 'filled'
  /**
   * Whether the card is interactive (clickable).
   */
  interactive?: boolean
  /**
   * Padding size.
   */
  padding?: Size | 'none'
}

ContainerProps

Props for the Container layout component.

interface ContainerProps extends HTMLElementProps {
  /**
   * Container content.
   */
  children?: Children
  /**
   * Maximum width.
   */
  maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string
  /**
   * Whether to center the container.
   */
  centered?: boolean
  /**
   * Horizontal padding.
   */
  paddingX?: Size | string
}

FlexProps

Flex container props.

interface FlexProps extends HTMLElementProps {
  /**
   * Flex content.
   */
  children?: Children
  /**
   * Flex direction.
   */
  direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse'
  /**
   * Justify content.
   */
  justify?: 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly'
  /**
   * Align items.
   */
  align?: 'start' | 'end' | 'center' | 'baseline' | 'stretch'
  /**
   * Flex wrap.
   */
  wrap?: 'wrap' | 'nowrap' | 'wrap-reverse'
  /**
   * Gap between items.
   */
  gap?: Size | string | number
}

GridProps

Grid container props.

interface GridProps extends HTMLElementProps {
  /**
   * Grid content.
   */
  children?: Children
  /**
   * Number of columns.
   */
  columns?: number | string
  /**
   * Number of rows.
   */
  rows?: number | string
  /**
   * Gap between items.
   */
  gap?: Size | string | number
  /**
   * Column gap.
   */
  columnGap?: Size | string | number
  /**
   * Row gap.
   */
  rowGap?: Size | string | number
}

SpacerProps

Props for the Spacer layout component (adds whitespace between elements).

interface SpacerProps extends BaseProps {
  /**
   * Space size.
   */
  size?: Size | string | number
  /**
   * Whether the spacer is horizontal.
   */
  horizontal?: boolean
}

FormProps

Props for the Form component (wraps inputs with submission handling and validation).

interface FormProps extends FormElementProps {
  /**
   * Form content.
   */
  children?: Children
  /**
   * Submit handler with form data.
   */
  onFormSubmit?: (data: Record<string, unknown>) => void | Promise<void>
  /**
   * Whether the form is submitting.
   */
  submitting?: boolean
}

FormFieldProps

Form field wrapper props.

interface FormFieldProps extends HTMLElementProps {
  /**
   * Field content.
   */
  children?: Children
  /**
   * Field label.
   */
  label?: string
  /**
   * Field name.
   */
  name?: string
  /**
   * Error message.
   */
  error?: string
  /**
   * Hint/help text.
   */
  hint?: string
  /**
   * Whether the field is required.
   */
  required?: boolean
}

AlertProps

Props for the Alert component.

interface AlertProps extends HTMLElementProps {
  /**
   * Alert content.
   */
  children?: Children
  /**
   * Alert title.
   */
  title?: string
  /**
   * Alert status/type.
   */
  status?: ColorVariant
  /**
   * Alert variant.
   */
  variant?: 'solid' | 'subtle' | 'outline' | 'left-accent'
  /**
   * Whether the alert is dismissible.
   */
  dismissible?: boolean
  /**
   * Called when dismissed.
   */
  onDismiss?: () => void
  /**
   * Icon to display.
   */
  icon?: Children
  /**
   * Accessible label for the dismiss button.
   * @default 'Dismiss'
   */
  dismissLabel?: string
}

ToastProps

Props for the Toast/notification component.

interface ToastProps extends HTMLElementProps {
  /**
   * Toast content.
   */
  children?: Children
  /**
   * Toast title.
   */
  title?: string
  /**
   * Toast description.
   */
  description?: string
  /**
   * Toast status/type.
   */
  status?: ColorVariant
  /**
   * Duration in milliseconds (0 for persistent).
   */
  duration?: number
  /**
   * Whether the toast is dismissible.
   */
  dismissible?: boolean
  /**
   * Called when dismissed.
   */
  onDismiss?: () => void
  /**
   * Toast position.
   */
  position?: 'top' | 'top-right' | 'top-left' | 'bottom' | 'bottom-right' | 'bottom-left'
  /**
   * Accessible label for the close button.
   * @default 'Close'
   */
  closeLabel?: string
}

TabItem

A single tab in a Tabs component.

interface TabItem<T = string> {
  /**
   * Tab value/id.
   */
  value: T
  /**
   * Tab label.
   */
  label: Children
  /**
   * Tab content.
   */
  content?: Children
  /**
   * Whether the tab is disabled.
   */
  disabled?: boolean
  /**
   * Icon to display.
   */
  icon?: Children
}

TabsProps

Props for the Tabs component (switchable tabbed content panels).

interface TabsProps<T = string> extends BaseProps {
  /**
   * Tab items.
   */
  items: TabItem<T>[]
  /**
   * Current active tab.
   */
  value?: T
  /**
   * Default active tab.
   */
  defaultValue?: T
  /**
   * Change handler.
   */
  onChange?: (value: T) => void
  /**
   * Tab variant.
   */
  variant?: 'line' | 'enclosed' | 'soft-rounded' | 'solid-rounded'
  /**
   * Tab size.
   */
  size?: Size
  /**
   * Whether tabs are fitted (take full width).
   */
  fitted?: boolean
}

AccordionItem

A single item in an Accordion component.

interface AccordionItem<T = string> {
  /**
   * Item value/id.
   */
  value: T
  /**
   * Item header/trigger.
   */
  header: Children
  /**
   * Item content.
   */
  content: Children
  /**
   * Whether the item is disabled.
   */
  disabled?: boolean
}

AccordionProps

Props for the Accordion component.

interface AccordionProps<T = string> extends BaseProps {
  /**
   * Accordion items.
   */
  items: AccordionItem<T>[]
  /**
   * Expanded item(s).
   */
  value?: T | T[]
  /**
   * Default expanded item(s).
   */
  defaultValue?: T | T[]
  /**
   * Change handler.
   */
  onChange?: (value: T | T[]) => void
  /**
   * Whether multiple items can be expanded.
   */
  multiple?: boolean
  /**
   * Whether items can be collapsed.
   */
  collapsible?: boolean
}

TableColumn

Table column definition.

interface TableColumn<T> {
  /**
   * Column key (data property).
   */
  key: keyof T | string
  /**
   * Column header.
   */
  header: Children
  /**
   * Custom cell renderer.
   */
  render?: (value: unknown, row: T, index: number) => Children
  /**
   * Column width.
   */
  width?: string | number
  /**
   * Whether the column is sortable.
   */
  sortable?: boolean
  /**
   * Text alignment.
   */
  align?: 'left' | 'center' | 'right'
}

TableProps

Props for the Table component.

interface TableProps<T> extends HTMLElementProps {
  /**
   * Table data.
   */
  data: T[]
  /**
   * Column definitions.
   */
  columns: TableColumn<T>[]
  /**
   * Row key extractor.
   */
  rowKey?: keyof T | ((row: T) => string | number)
  /**
   * Whether to show borders.
   */
  bordered?: boolean
  /**
   * Whether rows are striped.
   */
  striped?: boolean
  /**
   * Whether rows are hoverable.
   */
  hoverable?: boolean
  /**
   * Table size.
   */
  size?: Size
  /**
   * Empty state content.
   */
  emptyContent?: Children
  /**
   * Loading state.
   */
  loading?: boolean
  /**
   * Sort configuration.
   */
  sort?: {
    key: string
    direction: 'asc' | 'desc'
  }
  /**
   * Sort change handler.
   */
  onSort?: (key: string, direction: 'asc' | 'desc') => void
  /**
   * Row click handler.
   */
  onRowClick?: (row: T, index: number) => void
}

PaginationProps

Props for the Pagination component (page navigation with current page, total, page size, and change callbacks).

interface PaginationProps extends BaseProps {
  /**
   * Current page (1-indexed).
   */
  page: number
  /**
   * Total number of pages.
   */
  totalPages: number
  /**
   * Page change handler.
   */
  onChange: (page: number) => void
  /**
   * Number of sibling pages to show.
   */
  siblings?: number
  /**
   * Number of boundary pages to show.
   */
  boundaries?: number
  /**
   * Pagination size.
   */
  size?: Size
  /**
   * Whether to show first/last buttons.
   */
  showFirstLast?: boolean
  /**
   * Whether to show previous/next buttons.
   */
  showPrevNext?: boolean
  /**
   * Accessible labels for pagination controls.
   */
  labels?: {
    nav?: string
    first?: string
    previous?: string
    next?: string
    last?: string
    goToPage?: (page: number) => string
  }
}

SkeletonProps

Props for the Skeleton loading placeholder component.

interface SkeletonProps extends BaseProps {
  /**
   * Skeleton width.
   */
  width?: string | number
  /**
   * Skeleton height.
   */
  height?: string | number
  /**
   * Whether the skeleton is circular.
   */
  circle?: boolean
  /**
   * Border radius.
   */
  borderRadius?: string | number
  /**
   * Animation type.
   */
  animation?: 'pulse' | 'wave' | 'none'
}

Types

Children

Framework-agnostic child content. Use unknown to allow any framework's node type (ReactNode, VNode, etc.).

type Children = unknown

EventHandler

Framework-agnostic event handler.

type EventHandler<E = Event> = (event: E) => void
  • event — The DOM event object.

MouseEventHandler

Framework-agnostic mouse event handler.

type MouseEventHandler = EventHandler<MouseEvent>

KeyboardEventHandler

Framework-agnostic keyboard event handler.

type KeyboardEventHandler = EventHandler<KeyboardEvent>

FocusEventHandler

Framework-agnostic focus event handler.

type FocusEventHandler = EventHandler<FocusEvent>

ChangeEventHandler

Framework-agnostic change event handler.

type ChangeEventHandler = EventHandler<Event>

FormEventHandler

Framework-agnostic form event handler.

type FormEventHandler = EventHandler<Event>

Size

Standard size scale used across all molecule UI components (buttons, inputs, badges, etc.).

type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'

ColorVariant

Semantic color variants used across components for status indication.

type ColorVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info'

ButtonVariant

Button visual variant styles.

type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'link'

InputType

Allowed HTML input type attribute values for the Input component.

type InputType =
  | 'text'
  | 'email'
  | 'password'
  | 'number'
  | 'tel'
  | 'url'
  | 'search'
  | 'date'
  | 'time'
  | 'datetime-local'

ModalSize

Modal size variants including full-screen.

type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full'

TooltipPlacement

Position where a tooltip renders relative to its trigger element (top, bottom, left, right, and corner variants).

type TooltipPlacement =
  'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'

Classes

MoleculeAlert

Angular Alert UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-alert',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div role="alert" [class]="alertClasses" [style]="style" [attr.data-testid]="testId">
      @if (showIcon) {
        <span [class]="cm.alertIconWrapper">
          <span [innerHTML]="statusIconSvg"></span>
        </span>
      }
      <div [class]="cm.alertContent">
        @if (title) {
          <h5 [class]="titleClass">{{ title }}</h5>
        }
        <div [class]="descriptionClass">
          <ng-content></ng-content>
        </div>
      </div>
      @if (dismissible) {
        <button
          type="button"
          (click)="dismiss.emit()"
          [class]="cm.alertDismiss"
          [attr.aria-label]="dismissAriaLabel"
        >
          <span [innerHTML]="dismissIconSvg"></span>
        </button>
      }
    </div>
  `,
})
/**
 * Molecule Alert class.
 */
export class MoleculeAlert {
  @Input() title?: string
  @Input() status: ColorVariant = 'info'
  @Input() variant?: 'solid' | 'subtle' | 'outline' | 'left-accent'
  @Input() dismissible = false
  @Input() dismissLabel?: string
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string

  @Output() dismiss = new EventEmitter<void>()

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the accessible dismiss button label.
   * @returns The localized aria-label for the dismiss button.
   */
  get dismissAriaLabel(): string {
    return this.dismissLabel || t('ui.alert.dismiss', undefined, { defaultValue: 'Dismiss' })
  }

  /**
   * Retrieves the CSS class for the alert title.
   * @returns The title class name string.
   */
  get titleClass(): string {
    return this.cm.alertTitle
  }

  /**
   * Retrieves the CSS class for the alert description.
   * @returns The description class name string.
   */
  get descriptionClass(): string {
    return this.cm.alertDescription
  }

  /**
   * Determines the icon type based on the alert status.
   * @returns The mapped icon type string.
   */
  get iconType(): string {
    return statusVariantMap[this.status] || 'default'
  }

  /**
   * Determines whether the status icon should be shown.
   * @returns True if the icon type is not default.
   */
  get showIcon(): boolean {
    return this.iconType !== 'default'
  }

  private statusIconMap: Record<string, IconName> = {
    info: 'info-circle',
    success: 'check-circle',
    warning: 'exclamation-triangle',
    error: 'x-circle',
  }

  /**
   * Generates the SVG markup for the status icon.
   * @returns The SVG string for the current status icon.
   */
  get statusIconSvg(): string {
    const name = this.statusIconMap[this.iconType]
    return name ? getIconSvg(name, this.cm.iconMd) : ''
  }

  /**
   * Generates the SVG markup for the dismiss icon.
   * @returns The SVG string for the dismiss button icon.
   */
  get dismissIconSvg(): string {
    return getIconSvg('x-mark', this.cm.iconSm)
  }

  /**
   * Computes the combined CSS classes for the alert container.
   * @returns The alert container class name string.
   */
  get alertClasses(): string {
    const cmVariant = statusVariantMap[this.status] || 'default'
    return this.cm.cn(this.cm.alert({ variant: cmVariant }), this.className)
  }
}

MoleculeBadge

Angular Badge UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-badge',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <span [class]="badgeClasses" [style]="style" [attr.data-testid]="testId">
      <ng-content></ng-content>
    </span>
  `,
})
/**
 * Molecule Badge class.
 */
export class MoleculeBadge {
  @Input() color: ColorVariant = 'primary'
  @Input() variant: 'solid' | 'outline' | 'subtle' = 'solid'
  @Input() size?: Size
  @Input() rounded = true
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  private get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the combined CSS classes for the badge element.
   * @returns The badge class name string.
   */
  get badgeClasses(): string {
    return this.cm.cn(
      this.cm.badge({ variant: this.color, size: this.size }),
      !this.rounded && this.cm.badgeSquare,
      this.className,
    )
  }
}

MoleculeButton

Angular Button UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-button',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { style: 'display: contents' },
  template: `
    <button
      [type]="type"
      [class]="classes"
      [style]="style"
      [attr.data-testid]="testId"
      [disabled]="disabled || loading"
      [attr.aria-busy]="loading"
      (click)="handleClick.emit($event)"
    >
      @if (loading) {
        <span [class]="cm.buttonSpinner"></span>
      }
      @if (loading && loadingText) {
        {{ loadingText }}
      } @else {
        <ng-content></ng-content>
      }
    </button>
  `,
})
/**
 * Molecule Button class.
 */
export class MoleculeButton {
  @Input() variant: ButtonVariant = 'solid'
  @Input() color: ColorVariant = 'primary'
  @Input() size: Size = 'md'
  @Input() loading = false
  @Input() loadingText?: string
  @Input() fullWidth = false
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string
  @Input() disabled = false
  @Input() type: 'button' | 'submit' | 'reset' = 'button'

  @Output() handleClick = new EventEmitter<MouseEvent>()

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the combined CSS classes for the button element.
   * @returns The button class name string.
   */
  get classes(): string {
    return this.cm.cn(
      this.cm.button({
        variant: this.variant,
        color: this.color,
        size: this.size,
        fullWidth: this.fullWidth,
      }),
      this.className,
    )
  }
}

MoleculeCheckbox

Angular Checkbox UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-checkbox',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div [class]="cm.formFieldWrapper">
      <label [class]="cm.controlLabel">
        <input
          type="checkbox"
          [id]="checkboxId"
          [name]="name"
          [checked]="checked"
          [disabled]="disabled"
          [attr.data-state]="checked ? 'checked' : 'unchecked'"
          [attr.aria-invalid]="!!error"
          [class]="checkboxClasses"
          [style]="style"
          [attr.data-testid]="testId"
          (change)="handleChange.emit($event)"
        />
        @if (label) {
          <span [class]="labelClasses">{{ label }}</span>
        }
      </label>
      @if (error) {
        <p [class]="errorClasses">{{ error }}</p>
      }
    </div>
  `,
})
/**
 * Molecule Checkbox class.
 */
export class MoleculeCheckbox {
  @Input() label?: string
  @Input() checked = false
  @Input() indeterminate = false
  @Input() size?: Size
  @Input() error?: string
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string
  @Input() disabled = false
  @Input() name?: string
  @Input() id?: string

  @Output() handleChange = new EventEmitter<Event>()

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Resolves the checkbox input element ID.
   * @returns The resolved ID from the id or name input.
   */
  get checkboxId(): string {
    return this.id || this.name || ''
  }

  /**
   * Computes the combined CSS classes for the checkbox input.
   * @returns The checkbox class name string.
   */
  get checkboxClasses(): string {
    return this.cm.cn(this.cm.checkbox({ error: !!this.error }), this.className)
  }

  /**
   * Computes the CSS classes for the checkbox label text.
   * @returns The label class name string.
   */
  get labelClasses(): string {
    return this.cm.cn(this.cm.controlText, this.disabled && this.cm.controlDisabled)
  }

  /**
   * Computes the CSS classes for the error message.
   * @returns The error class name string.
   */
  get errorClasses(): string {
    return this.cm.cn(this.cm.formError, this.cm.sp('mt', 1))
  }
}

MoleculeInput

Angular Input UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-input',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div [class]="cm.inputWrapper">
      @if (label) {
        <label [attr.for]="inputId" [class]="labelClasses">
          {{ label }}
        </label>
      }
      <div [class]="cm.inputInner">
        <input
          [type]="type"
          [id]="inputId"
          [name]="name"
          [value]="value"
          [placeholder]="placeholder"
          [disabled]="disabled"
          [required]="required"
          [readOnly]="readOnly"
          [attr.aria-invalid]="!!error"
          [attr.aria-describedby]="ariaDescribedBy"
          [class]="inputClasses"
          [style]="style"
          [attr.data-testid]="testId"
          [attr.maxlength]="maxLength"
          [attr.minlength]="minLength"
          [attr.pattern]="pattern"
          [attr.autocomplete]="autoComplete"
          (input)="handleInput.emit($event)"
          (change)="handleChange.emit($event)"
          (focus)="handleFocus.emit($event)"
          (blur)="handleBlur.emit($event)"
        />
        @if (clearable && value) {
          <div [class]="cm.inputClearButton">
            <button
              type="button"
              (click)="handleClear.emit()"
              [class]="cm.inputClearButton"
              [attr.aria-label]="clearAriaLabel"
            >
              <span [innerHTML]="clearIconSvg"></span>
            </button>
          </div>
        }
      </div>
      @if (error) {
        <p [id]="inputId + '-error'" [class]="formErrorClass">
          {{ error }}
        </p>
      }
      @if (hint && !error) {
        <p [id]="inputId + '-hint'" [class]="formHintClass">
          {{ hint }}
        </p>
      }
    </div>
  `,
})
/**
 * Molecule Input class.
 */
export class MoleculeInput {
  @Input() type: InputType = 'text'
  @Input() size: Size = 'md'
  @Input() label?: string
  @Input() error?: string
  @Input() hint?: string
  @Input() clearable = false
  @Input() clearLabel?: string
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string
  @Input() disabled = false
  @Input() required = false
  @Input() readOnly = false
  @Input() value?: string | number
  @Input() placeholder?: string
  @Input() name?: string
  @Input() id?: string
  @Input() maxLength?: number
  @Input() minLength?: number
  @Input() pattern?: string
  @Input() autoComplete?: string

  @Output() handleInput = new EventEmitter<Event>()
  @Output() handleChange = new EventEmitter<Event>()
  @Output() handleFocus = new EventEmitter<FocusEvent>()
  @Output() handleBlur = new EventEmitter<FocusEvent>()
  @Output() handleClear = new EventEmitter<void>()

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the accessible clear button label.
   * @returns The localized aria-label for the clear button.
   */
  get clearAriaLabel(): string {
    return this.clearLabel || t('ui.input.clear', undefined, { defaultValue: 'Clear' })
  }

  /**
   * Retrieves the CSS class for form error messages.
   * @returns The form error class name string.
   */
  get formErrorClass(): string {
    return this.cm.formError
  }

  /**
   * Retrieves the CSS class for form hint messages.
   * @returns The form hint class name string.
   */
  get formHintClass(): string {
    return this.cm.formHint
  }

  /**
   * Resolves the input element ID.
   * @returns The resolved ID from the id or name input.
   */
  get inputId(): string {
    return this.id || this.name || ''
  }

  /**
   * Computes the aria-describedby attribute value.
   * @returns The ID of the describing element, or null if none.
   */
  get ariaDescribedBy(): string | null {
    if (this.error) return `${this.inputId}-error`
    if (this.hint) return `${this.inputId}-hint`
    return null
  }

  /**
   * Computes the CSS classes for the input label.
   * @returns The label class name string.
   */
  get labelClasses(): string {
    return this.cm.cn(this.cm.label({ required: this.required }), this.cm.labelBlock)
  }

  /**
   * Computes the combined CSS classes for the input element.
   * @returns The input class name string.
   */
  get inputClasses(): string {
    return this.cm.cn(
      this.cm.input({ error: !!this.error, size: this.size }),
      this.clearable && this.cm.inputPadRight,
      this.className,
    )
  }

  /**
   * Generates the SVG markup for the clear button icon.
   * @returns The SVG string for the clear icon.
   */
  get clearIconSvg(): string {
    return getIconSvg('x-mark', this.cm.iconSm)
  }
}

MoleculeModal

Angular Modal UI component with UIClassMap-driven styling.

centered (default true) vertically centers the dialog; centered: false top-anchors it instead, via getModalWrapperStyle().

@Component({
  selector: 'mol-modal',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (open) {
      <!-- Overlay background -->
      <div [class]="overlayClass" aria-hidden="true"></div>

      <!-- Centering wrapper -->
      <div [class]="wrapperClass" [style]="wrapperStyle" (click)="onOverlayClick($event)">
        <!-- Content -->
        <div
          role="dialog"
          aria-modal="true"
          [attr.aria-labelledby]="title ? 'modal-title' : null"
          [class]="contentClasses"
          [style]="style"
          [attr.data-testid]="testId"
          (click)="$event.stopPropagation()"
        >
          @if (title || showCloseButton) {
            <div [class]="headerClass">
              @if (title) {
                <h2 id="modal-title" [class]="titleClass">{{ title }}</h2>
              } @else if (showCloseButton) {
                <div></div>
              }
              @if (showCloseButton) {
                <button
                  type="button"
                  (click)="close.emit()"
                  [class]="closeClass"
                  [attr.aria-label]="closeAriaLabel"
                >
                  <span [innerHTML]="closeIconSvg"></span>
                </button>
              }
            </div>
          }

          <div [class]="bodyClass">
            <ng-content></ng-content>
          </div>

          @if (hasFooter) {
            <div [class]="footerClass">
              <ng-content select="[modal-footer]"></ng-content>
            </div>
          }
        </div>
      </div>
    }
  `,
})
/**
 * Molecule Modal class.
 */
export class MoleculeModal implements OnInit, OnDestroy, OnChanges {
  @Input() open = false
  @Input() title?: string
  @Input() size: ModalSize = 'md'
  @Input() showCloseButton = true
  @Input() closeLabel?: string
  @Input() closeOnOverlayClick = true
  @Input() closeOnEscape = true
  @Input() centered = true
  @Input() preventScroll = true
  @Input() hasFooter = false
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string

  @Output() close = new EventEmitter<void>()

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the accessible close button label.
   * @returns The localized aria-label for the close button.
   */
  get closeAriaLabel(): string {
    return this.closeLabel || t('ui.modal.close', undefined, { defaultValue: 'Close' })
  }

  private boundEscapeHandler = this.handleEscape.bind(this)

  /**
   * Retrieves the CSS class for the modal wrapper.
   * @returns The wrapper class name string.
   */
  get wrapperClass(): string {
    return this.cm.dialogWrapper
  }

  /**
   * Retrieves the inline style for the modal centering wrapper.
   * @returns An inline style object, or `undefined` when centered.
   */
  get wrapperStyle(): Record<string, string> | undefined {
    return getModalWrapperStyle(this.centered)
  }

  /**
   * Retrieves the CSS class for the modal body.
   * @returns The body class name string.
   */
  get bodyClass(): string {
    return this.cm.dialogBody
  }

  /**
   * Retrieves the CSS class for the modal overlay.
   * @returns The overlay class name string.
   */
  get overlayClass(): string {
    return this.cm.dialogOverlay
  }

  /**
   * Retrieves the CSS class for the modal header.
   * @returns The header class name string.
   */
  get headerClass(): string {
    return this.cm.dialogHeader
  }

  /**
   * Retrieves the CSS class for the modal footer.
   * @returns The footer class name string.
   */
  get footerClass(): string {
    return this.cm.dialogFooter
  }

  /**
   * Retrieves the CSS class for the modal title.
   * @returns The title class name string.
   */
  get titleClass(): string {
    return this.cm.dialogTitle
  }

  /**
   * Retrieves the CSS class for the close button.
   * @returns The close button class name string.
   */
  get closeClass(): string {
    return this.cm.dialogClose
  }

  private sanitizer = inject(DomSanitizer)

  /**
   * Generates the SVG markup for the close button icon.
   * @returns The sanitized SVG for the close icon.
   */
  get closeIconSvg(): SafeHtml {
    return this.sanitizer.bypassSecurityTrustHtml(getIconSvg('x-mark', this.cm.iconMd))
  }

  /**
   * Computes the combined CSS classes for the modal content.
   * @returns The modal content class name string.
   */
  get contentClasses(): string {
    return this.cm.cn(this.cm.modal({ size: this.size }), this.className)
  }

  /**
   * Initializes event listeners on component creation.
   */
  ngOnInit(): void {
    this.updateListeners()
  }

  /**
   * Handles input property changes.
   * @param changes - The changed input properties.
   */
  ngOnChanges(changes: SimpleChanges): void {
    if (changes['open']) {
      this.updateListeners()
      this.updateBodyScroll()
    }
  }

  /**
   * Cleans up event listeners and body scroll on component destruction.
   */
  ngOnDestroy(): void {
    document.removeEventListener('keydown', this.boundEscapeHandler)
    if (this.preventScroll) {
      document.body.style.overflow = ''
    }
  }

  /**
   * Adds or removes the keyboard escape listener based on open state.
   */
  private updateListeners(): void {
    if (this.open) {
      document.addEventListener('keydown', this.boundEscapeHandler)
    } else {
      document.removeEventListener('keydown', this.boundEscapeHandler)
    }
  }

  /**
   * Updates the document body overflow based on open state.
   */
  private updateBodyScroll(): void {
    if (this.preventScroll) {
      document.body.style.overflow = this.open ? 'hidden' : ''
    }
  }

  /**
   * Handles escape key press to close the modal.
   * @param e - The keyboard event.
   */
  private handleEscape(e: KeyboardEvent): void {
    if (this.closeOnEscape && e.key === 'Escape') {
      this.close.emit()
    }
  }

  /**
   * Handles overlay click to close the modal.
   * @param event - The mouse event from the overlay click.
   */
  onOverlayClick(event: MouseEvent): void {
    if (this.closeOnOverlayClick && event.target === event.currentTarget) {
      this.close.emit()
    }
  }
}

MoleculeRadioGroup

Angular RadioGroup UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-radio-group',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div
      [class]="className"
      [style]="style"
      [attr.data-testid]="testId"
      role="radiogroup"
      [attr.aria-label]="label"
    >
      @if (label) {
        <div [class]="groupLabelClasses">{{ label }}</div>
      }
      <div [class]="directionClasses">
        @for (option of options; track option.value) {
          <label [class]="getOptionLabelClasses(option)">
            <input
              type="radio"
              [name]="label"
              [value]="option.value"
              [checked]="value === option.value"
              [disabled]="disabled || option.disabled"
              [attr.data-state]="value === option.value ? 'checked' : 'unchecked'"
              [class]="radioClasses"
              (change)="onRadioChange(option.value)"
            />
            <span [class]="cm.controlText">{{ option.label }}</span>
          </label>
        }
      </div>
      @if (error) {
        <p [class]="errorClasses">{{ error }}</p>
      }
    </div>
  `,
})
/**
 * Molecule Radio Group class.
 */
export class MoleculeRadioGroup {
  @Input() options: RadioOption<string>[] = []
  @Input() value?: string
  @Input() size?: Size
  @Input() label?: string
  @Input() direction: 'horizontal' | 'vertical' = 'vertical'
  @Input() error?: string
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string
  @Input() disabled = false

  @Output() valueChange = new EventEmitter<string>()

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the CSS classes for the radio group label.
   * @returns The group label class name string.
   */
  get groupLabelClasses(): string {
    return this.cm.cn(this.cm.label({}), this.cm.radioGroupLabel)
  }

  /**
   * Computes the CSS classes for the layout direction.
   * @returns The direction layout class name string.
   */
  get directionClasses(): string {
    return this.cm.radioGroupLayout(this.direction)
  }

  /**
   * Computes the CSS classes for radio input elements.
   * @returns The radio input class name string.
   */
  get radioClasses(): string {
    return this.cm.cn(this.cm.radio({ error: !!this.error }), this.cm.cursorPointer)
  }

  /**
   * Computes the CSS classes for the error message.
   * @returns The error class name string.
   */
  get errorClasses(): string {
    return this.cm.cn(this.cm.formError, this.cm.sp('mt', 1))
  }

  /**
   * Computes the CSS classes for an individual radio option label.
   * @param option - The radio option to compute classes for.
   * @returns The option label class name string.
   */
  getOptionLabelClasses(option: RadioOption<string>): string {
    const isDisabled = this.disabled || option.disabled
    return this.cm.cn(this.cm.controlLabel, isDisabled && this.cm.controlDisabled)
  }

  /**
   * Handles radio option selection change.
   * @param optionValue - The value of the selected radio option.
   */
  onRadioChange(optionValue: string): void {
    this.valueChange.emit(optionValue)
  }
}

MoleculeSelect

Angular Select UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-select',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div [class]="cm.inputWrapper">
      @if (label) {
        <label [attr.for]="selectId" [class]="labelClasses">
          {{ label }}
        </label>
      }
      <div [class]="cm.inputInner">
        <select
          [id]="selectId"
          [name]="name"
          [value]="value"
          [disabled]="disabled"
          [required]="required"
          [attr.aria-invalid]="!!error"
          [attr.aria-describedby]="ariaDescribedBy"
          [class]="selectClasses"
          [attr.data-testid]="testId"
          (change)="onSelectChange($event)"
        >
          @if (placeholder) {
            <option value="" disabled>{{ placeholder }}</option>
          }
          @if (clearable) {
            <option value="">--</option>
          }
          @for (option of options; track option.value) {
            <option [value]="option.value" [disabled]="option.disabled">
              {{ option.label }}
            </option>
          }
        </select>
      </div>
      @if (error) {
        <p [id]="selectId + '-error'" [class]="formErrorClass">
          {{ error }}
        </p>
      }
      @if (hint && !error) {
        <p [id]="selectId + '-hint'" [class]="formHintClass">
          {{ hint }}
        </p>
      }
    </div>
  `,
})
/**
 * Molecule Select class.
 */
export class MoleculeSelect {
  @Input() options: SelectOption<string>[] = []
  @Input() value?: string
  @Input() size: Size = 'md'
  @Input() label?: string
  @Input() placeholder?: string
  @Input() error?: string
  @Input() hint?: string
  @Input() clearable = false
  @Input() className?: string
  @Input() testId?: string
  @Input() disabled = false
  @Input() required = false
  @Input() name?: string
  @Input() id?: string

  @Output() valueChange = new EventEmitter<string>()
  @Output() handleChange = new EventEmitter<Event>()

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Retrieves the CSS class for form error messages.
   * @returns The form error class name string.
   */
  get formErrorClass(): string {
    return this.cm.formError
  }

  /**
   * Retrieves the CSS class for form hint messages.
   * @returns The form hint class name string.
   */
  get formHintClass(): string {
    return this.cm.formHint
  }

  /**
   * Resolves the select element ID.
   * @returns The resolved ID from the id or name input.
   */
  get selectId(): string {
    return this.id || this.name || ''
  }

  /**
   * Computes the aria-describedby attribute value.
   * @returns The ID of the describing element, or null if none.
   */
  get ariaDescribedBy(): string | null {
    if (this.error) return `${this.selectId}-error`
    if (this.hint) return `${this.selectId}-hint`
    return null
  }

  /**
   * Computes the CSS classes for the select label.
   * @returns The label class name string.
   */
  get labelClasses(): string {
    return this.cm.cn(this.cm.label({ required: this.required }), this.cm.labelBlock)
  }

  /**
   * Computes the combined CSS classes for the select element.
   * @returns The select class name string.
   */
  get selectClasses(): string {
    return this.cm.cn(
      this.cm.select({ error: !!this.error, size: this.size }),
      this.cm.selectNative,
      this.className,
    )
  }

  /**
   * Handles select value change events.
   * @param event - The change event from the select element.
   */
  onSelectChange(event: Event): void {
    const target = event.target as HTMLSelectElement
    this.handleChange.emit(event)
    this.valueChange.emit(target.value)
  }
}

MoleculeSpinner

Angular Spinner UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-spinner',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div
      role="status"
      [attr.aria-label]="spinnerLabel"
      [class]="spinnerClasses"
      [style]="computedStyle"
      [attr.data-testid]="testId"
    >
      @if (label) {
        <span [class]="cm.srOnly">{{ label }}</span>
      }
    </div>
  `,
})
/**
 * Molecule Spinner class.
 */
export class MoleculeSpinner {
  @Input() size: Size = 'md'
  @Input() color?: ColorVariant | string
  @Input() label?: string
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the accessible spinner label.
   * @returns The localized aria-label for the spinner.
   */
  get spinnerLabel(): string {
    return this.label || t('ui.spinner.loading', undefined, { defaultValue: 'Loading' })
  }

  /**
   * Computes the combined CSS classes for the spinner element.
   * @returns The spinner class name string.
   */
  get spinnerClasses(): string {
    return this.cm.cn(this.cm.spinner({ size: this.size }), this.className)
  }

  /**
   * Computes the merged inline styles including custom color.
   * @returns The computed style object with optional custom border color.
   */
  get computedStyle(): Record<string, string> {
    const base: Record<string, string> = { ...this.style }
    if (
      this.color &&
      typeof this.color === 'string' &&
      !['primary', 'secondary', 'success', 'warning', 'error', 'info'].includes(this.color)
    ) {
      base['borderColor'] = this.color
      base['borderTopColor'] = 'transparent'
    }
    return base
  }
}

MoleculeSwitch

Angular Switch UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-switch',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <label [class]="wrapperClasses">
      <button
        type="button"
        role="switch"
        [attr.aria-checked]="checked"
        [disabled]="disabled"
        [attr.data-state]="state"
        [class]="switchClasses"
        [style]="style"
        [attr.data-testid]="testId"
        (click)="onToggle()"
      >
        <span [attr.data-state]="state" [class]="thumbClasses"></span>
      </button>
      @if (label) {
        <span [class]="cm.controlText">{{ label }}</span>
      }
    </label>
  `,
})
/**
 * Molecule Switch class.
 */
export class MoleculeSwitch {
  @Input() label?: string
  @Input() checked = false
  @Input() size: Size = 'md'
  @Input() color?: ColorVariant
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string
  @Input() disabled = false

  @Output() checkedChange = new EventEmitter<boolean>()

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the current checked/unchecked state string.
   * @returns The state string, either "checked" or "unchecked".
   */
  get state(): string {
    return this.checked ? 'checked' : 'unchecked'
  }

  /**
   * Computes the CSS classes for the switch wrapper label.
   * @returns The wrapper class name string.
   */
  get wrapperClasses(): string {
    return this.cm.cn(this.cm.controlLabel, this.disabled && this.cm.controlDisabled)
  }

  /**
   * Computes the combined CSS classes for the switch button.
   * @returns The switch class name string.
   */
  get switchClasses(): string {
    return this.cm.cn(this.cm.switchBase({ size: this.size }), this.className)
  }

  /**
   * Computes the CSS classes for the switch thumb indicator.
   * @returns The thumb class name string.
   */
  get thumbClasses(): string {
    return this.cm.switchThumb({ size: this.size })
  }

  /**
   * Handles the switch toggle action.
   */
  onToggle(): void {
    if (!this.disabled) {
      this.checkedChange.emit(!this.checked)
    }
  }
}

MoleculeToast

Angular Toast UI component with UIClassMap-driven styling.

@Component({
  selector: 'mol-toast',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (isVisible) {
      <div
        role="alert"
        data-state="open"
        [class]="toastClasses"
        [style]="style"
        [attr.data-testid]="testId"
      >
        @if (showIcon) {
          <span [class]="cm.toastIconWrapper">
            <span [innerHTML]="statusIconSvg"></span>
          </span>
        }
        <div [class]="cm.toastContentWrapper">
          @if (title) {
            <div [class]="titleClass">{{ title }}</div>
          }
          @if (description) {
            <div [class]="descriptionClass">{{ description }}</div>
          } @else {
            <div [class]="descriptionClass">
              <ng-content></ng-content>
            </div>
          }
        </div>
        @if (dismissible) {
          <button
            type="button"
            (click)="onDismiss()"
            [class]="closeButtonClass"
            [attr.aria-label]="closeAriaLabel"
          >
            <span [innerHTML]="dismissIconSvg"></span>
          </button>
        }
      </div>
    }
  `,
})
/**
 * Molecule Toast class.
 */
export class MoleculeToast implements OnInit, OnDestroy {
  @Input() title?: string
  @Input() description?: string
  @Input() status: ColorVariant = 'info'
  @Input() duration = 5000
  @Input() dismissible = true
  @Input() closeLabel?: string
  @Input() position: 'top' | 'top-right' | 'top-left' | 'bottom' | 'bottom-right' | 'bottom-left' =
    'bottom-right'
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string

  @Output() dismiss = new EventEmitter<void>()

  isVisible = true
  private timer?: ReturnType<typeof setTimeout>

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the accessible close button label.
   * @returns The localized aria-label for the close button.
   */
  get closeAriaLabel(): string {
    return this.closeLabel || t('ui.toast.close', undefined, { defaultValue: 'Close' })
  }

  /**
   * Retrieves the CSS class for the toast title.
   * @returns The title class name string.
   */
  get titleClass(): string {
    return this.cm.toastTitle
  }

  /**
   * Retrieves the CSS class for the toast description.
   * @returns The description class name string.
   */
  get descriptionClass(): string {
    return this.cm.toastDescription
  }

  /**
   * Retrieves the CSS class for the close button.
   * @returns The close button class name string.
   */
  get closeButtonClass(): string {
    return this.cm.toastClose
  }

  /**
   * Determines the icon type based on the toast status.
   * @returns The mapped icon type string.
   */
  get iconType(): string {
    return statusVariantMap[this.status] || 'default'
  }

  /**
   * Determines whether the status icon should be shown.
   * @returns True if the icon type is not default.
   */
  get showIcon(): boolean {
    return this.iconType !== 'default'
  }

  private statusIconMap: Record<string, IconName> = {
    info: 'info-circle',
    success: 'check-circle',
    warning: 'exclamation-triangle',
    error: 'x-circle',
  }

  /**
   * Generates the SVG markup for the status icon.
   * @returns The SVG string for the current status icon.
   */
  get statusIconSvg(): string {
    const name = this.statusIconMap[this.iconType]
    return name ? getIconSvg(name, this.cm.iconMd) : ''
  }

  /**
   * Generates the SVG markup for the dismiss icon.
   * @returns The SVG string for the dismiss button icon.
   */
  get dismissIconSvg(): string {
    return getIconSvg('x-mark', this.cm.iconSm)
  }

  /**
   * Computes the combined CSS classes for the toast container.
   * @returns The toast class name string.
   */
  get toastClasses(): string {
    const variant = statusVariantMap[this.status] || 'default'
    return this.cm.cn(this.cm.toast({ variant }), this.className)
  }

  /**
   * Starts the auto-dismiss timer on component creation.
   */
  ngOnInit(): void {
    if (this.duration > 0) {
      this.timer = setTimeout(() => {
        this.isVisible = false
        this.dismiss.emit()
      }, this.duration)
    }
  }

  /**
   * Clears the auto-dismiss timer on component destruction.
   */
  ngOnDestroy(): void {
    if (this.timer) {
      clearTimeout(this.timer)
    }
  }

  /**
   * Handles the dismiss action by hiding the toast.
   */
  onDismiss(): void {
    this.isVisible = false
    this.dismiss.emit()
  }
}

MoleculeTooltip

Angular Tooltip UI component with UIClassMap-driven styling.

Wraps child content and shows a tooltip on hover/focus. hasArrow renders a small themed pointer at the resolved placement edge.

@Component({
  selector: 'mol-tooltip',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div
      #trigger
      [class]="cm.tooltipTrigger"
      (mouseenter)="show()"
      (mouseleave)="hide()"
      (focus)="show()"
      (blur)="hide()"
    >
      <ng-content></ng-content>
    </div>
    @if (isVisible) {
      <!--
        Outer positioned wrapper carries the top/left/z-index placement; the
        arrow renders as its SIBLING (not nested inside the visible box),
        because tooltipContent sets overflow-hidden — an arrow meant to poke
        past the box's own edge would be clipped invisible if it were a
        descendant of that element instead.
      -->
      <div [style]="positionStyle">
        <div
          #tooltipEl
          role="tooltip"
          [class]="tooltipClasses"
          [style]="style"
          [attr.data-testid]="testId"
        >
          {{ content }}
        </div>
        @if (hasArrow) {
          <span aria-hidden="true" [style]="arrowStyle"></span>
        }
      </div>
    }
  `,
})
/**
 * Molecule Tooltip class.
 */
export class MoleculeTooltip implements OnDestroy {
  @Input() content = ''
  @Input() placement: TooltipPlacement = 'top'
  @Input() delay = 0
  @Input() hasArrow = false
  @Input() className?: string
  @Input() style?: Record<string, string>
  @Input() testId?: string

  @ViewChild('trigger') triggerRef?: ElementRef<HTMLDivElement>
  @ViewChild('tooltipEl') tooltipRef?: ElementRef<HTMLDivElement>

  isVisible = false
  positionStyle: Record<string, string> = {}
  private timeoutId?: ReturnType<typeof setTimeout>

  /**
   * Retrieves the current UI class map.
   * @returns The UIClassMap instance for styling.
   */
  get cm(): UIClassMap {
    return getClassMap()
  }

  /**
   * Computes the combined CSS classes for the tooltip content.
   * @returns The tooltip class name string.
   */
  get tooltipClasses(): string {
    return this.cm.cn(this.cm.tooltipContent, this.className)
  }

  /**
   * Retrieves the inline style for the tooltip's arrow element.
   * @returns Inline style positioning the rotated-square arrow.
   */
  get arrowStyle(): Record<string, string> {
    return getTooltipArrowStyle(this.placement)
  }

  /**
   * Shows the tooltip with optional delay.
   */
  show(): void {
    if (this.delay > 0) {
      this.timeoutId = setTimeout(() => {
        this.isVisible = true
        this.updatePosition()
      }, this.delay)
    } else {
      this.isVisible = true
      requestAnimationFrame(() => this.updatePosition())
    }
  }

  /**
   * Hides the tooltip and cancels any pending delay.
   */
  hide(): void {
    if (this.timeoutId) {
      clearTimeout(this.timeoutId)
    }
    this.isVisible = false
  }

  /**
   * Clears pending timeouts on component destruction.
   */
  ngOnDestroy(): void {
    if (this.timeoutId) {
      clearTimeout(this.timeoutId)
    }
  }

  /**
   * Calculates and applies the tooltip position relative to the trigger.
   */
  private updatePosition(): void {
    if (!this.triggerRef?.nativeElement || !this.tooltipRef?.nativeElement) return

    const triggerRect = this.triggerRef.nativeElement.getBoundingClientRect()
    const tooltipRect = this.tooltipRef.nativeElement.getBoundingClientRect()
    const offset = 8

    const { top, left, width, height } = triggerRect
    const tooltipWidth = tooltipRect.width
    const tooltipHeight = tooltipRect.height

    const positions: Record<TooltipPlacement, { top: number; left: number }> = {
      top: {
        top: top + window.scrollY - tooltipHeight - offset,
        left: left + window.scrollX + width / 2 - tooltipWidth / 2,
      },
      bottom: {
        top: top + window.scrollY + height + offset,
        left: left + window.scrollX + width / 2 - tooltipWidth / 2,
      },
      left: {
        top: top + window.scrollY + height / 2 - tooltipHeight / 2,
        left: left + window.scrollX - tooltipWidth - offset,
      },
      right: {
        top: top + window.scrollY + height / 2 - tooltipHeight / 2,
        left: left + window.scrollX + width + offset,
      },
      'top-start': {
        top: top + window.scrollY - tooltipHeight - offset,
        left: left + window.scrollX,
      },
      'top-end': {
        top: top + window.scrollY - tooltipHeight - offset,
        left: left + window.scrollX + width - tooltipWidth,
      },
      'bottom-start': {
        top: top + window.scrollY + height + offset,
        left: left + window.scrollX,
      },
      'bottom-end': {
        top: top + window.scrollY + height + offset,
        left: left + window.scrollX + width - tooltipWidth,
      },
    }

    const pos = positions[this.placement]
    this.positionStyle = {
      position: 'absolute',
      top: `${pos.top}px`,
      left: `${pos.left}px`,
      zIndex: '9999',
    }
  }
}

Functions

getModalWrapperStyle(centered)

Get the inline style for the modal centering wrapper. dialogWrapper has no dedicated ClassMap resolver option (it's a fixed token, not a { centered }-parameterized one), so a top-anchored (non-centered) layout is expressed via the one inline-style exception the workspace styling rule allows for values ClassMap genuinely cannot express.

A standalone function (rather than a class member) so it's unit-testable without constructing MoleculeModal — the class can't be new'd outside an Angular injection context (sanitizer = inject(DomSanitizer) runs at field-initializer time).

function getModalWrapperStyle(centered: boolean): Record<string, string> | undefined
  • centered — Whether the modal is vertically centered.

Returns: An inline style object, or undefined when centered.

getTooltipArrowStyle(placement)

Returns the arrow's inline position style for a given placement. The arrow has no dedicated ClassMap resolver (tooltip() takes no options), so its shape/position is expressed via a small inline style — the sanctioned exception for values ClassMap cannot express. Its color is NOT hardcoded: var(--color-surface) / var(--color-border) are the same CSS custom properties the bg-surface/border classes in tooltipContent resolve to, so the arrow always matches the tooltip body in both themes.

A standalone function (rather than a class member) so it's unit-testable in isolation, matching getModalWrapperStyle() in modal.component.ts.

function getTooltipArrowStyle(placement: TooltipPlacement): Record<string, string>
  • placement — The tooltip's resolved placement.

Returns: Inline style positioning the rotated-square arrow for that placement.

getIconSvg(name, className)

Generates SVG markup string from an icon name.

function getIconSvg(name: IconName, className: string): string
  • name — Icon name from the icon set
  • className — CSS class for sizing

Returns: SVG markup string

cn(inputs)

Merge class names, filtering out falsy values.

function cn(inputs?: (string | false | null | undefined)[]): string
  • inputs — The inputs.

Returns: The resulting string.

Injection Notes

Requirements

Peer dependencies:

  • @angular/core 22.0.0
  • @angular/platform-browser 22.0.0
  • @molecule/app-i18n ^1.0.1
  • @molecule/app-icons ^1.0.1
  • @molecule/app-ui ^1.0.1

Runtime Dependencies

  • @angular/core

  • @angular/platform-browser

  • @molecule/app-i18n

  • @molecule/app-icons

  • @molecule/app-ui

  • setClassMap() must run before any component renders — every component resolves its classes through getClassMap(), which THROWS until a ClassMap bond (e.g. @molecule/app-ui-tailwind) is set. Call it in main.ts before bootstrapApplication.

  • Components are standalone — add them to the imports array of the consuming component or route; there is no NgModule.

  • Event outputs are handle*, not the DOM names: (handleClick) on mol-button, (handleInput)/(handleChange) on mol-input (both emit the raw DOM event — read the value from $event.target). The host element uses display: contents, so the real <button>/<input> lives inside the template.

Translations

Translation strings are provided by @molecule/app-locales-ui.