@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-angularnpm · 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.tsJSDoc, 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 setclassName— 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/core22.0.0@angular/platform-browser22.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 throughgetClassMap(), which THROWS until a ClassMap bond (e.g.@molecule/app-ui-tailwind) is set. Call it inmain.tsbeforebootstrapApplication. -
Components are standalone — add them to the
importsarray of the consuming component or route; there is no NgModule. -
Event outputs are
handle*, not the DOM names:(handleClick)onmol-button,(handleInput)/(handleChange)onmol-input(both emit the raw DOM event — read the value from$event.target). The host element usesdisplay: contents, so the real<button>/<input>lives inside the template.
Translations
Translation strings are provided by @molecule/app-locales-ui.