@pleta/ui
v0.2.1
Published
Angular UI components, form controls, overlays, and notifications.
Downloads
339
Readme
Pleta UI
All components use separate TypeScript, HTML and CSS files with Angular's default encapsulation. Styles load with the component; no global stylesheet import is needed.
import {
PleInput,
PleInputNumber,
PleInputPassword,
PleSelect,
PleMultiSelect,
PleCheckboxGroup,
PleRadioGroup,
PleToggle,
PleToggleButton,
} from '@pleta/ui';Add the required components to your component's imports.
<input pleInput />
<input pleInputNumber type="number" />
<ple-select inputId="city" [options]="cities" [(value)]="city" />
<ple-multi-select inputId="topics" [options]="topics" [(value)]="selectedTopics" />Inputs enhance native elements. With Reactive Forms, explicitly include
type="number" so Angular selects its number value accessor.
Template inputs and projected slots
Import PleTemplate from @pleta/ui to use named projected slots. TemplateRef inputs retain their existing contexts. Priority: input → first direct projected slot → default. Removing the input restores projection; child component slots stay isolated.
<ple-select inputId="city" [options]="cities" [itemTemplate]="override">
<ng-template pleTemplate="item" let-option let-selected="selected">
<strong>{{ option.label }}</strong>
</ng-template>
</ple-select>Select, MultiSelect, CheckboxGroup, RadioGroup, ToggleButton, Menu and Breadcrumb
support item/itemTemplate. Panel supports header/headerTemplate. Tabs supports
header/headerTemplate and content/contentTemplate. Tabs contentTemplate is now
optional because it can be projected; without either template its panel is empty.
Ordinary button/panel content projection is unchanged. Both APIs share one renderer.
Bare projected contexts do not infer generic types from parent components; typed
TemplateRef inputs remain available. Examples and contexts: Storybook /templates.
Custom selectors
Selectors use a custom popup, not a native select. Options have the exported type
PleOption<T>: { value: T; label: string; disabled?: boolean }.
PleSelectOption<T> remains an alias for existing consumers.
Values must be unique and are compared by reference (Object.is). Use stable
primitive values or preserve object references. null is reserved for an empty
single selection. The single-select value is T | null; the multiselect value is
T[]. Both support [(value)] and Signal Forms [formField].
readonly cities = [
{ value: 'moscow', label: 'Moscow' },
{ value: 'kazan', label: 'Kazan' },
];
readonly city = signal<string | null>(null);Pass one optional template for all items:
<ng-template #item let-option let-selected="selected">
<strong>{{ option.label }}</strong>
<small>{{ option.value }} {{ selected ? 'Selected' : '' }}</small>
</ng-template>
<label for="city">City</label>
<ple-select inputId="city" [options]="cities" [(value)]="city" [itemTemplate]="item" />The template context is PleSelectItemContext<T>: $implicit contains the option
and selected indicates its selection state. Without a template, the popup renders
option.label. The closed control always shows the selected labels. Item templates
must contain non-interactive presentation content, not nested buttons or inputs.
Supply a unique, stable inputId for each instance. It identifies the internal
button and derives the list and option IDs, so labels and SSR use deterministic
identifiers. A native label for should target inputId, not the custom host.
Optional labelledBy and describedBy refer to existing label/hint element IDs.
The disabled, readonly, required, and invalid inputs integrate with Signal
Forms. Controls emit touch on blur and expose focus() and reset().
Arrow keys navigate enabled options; Home/End jump to the first/last enabled option. Enter/Space choose or toggle an option. Typing searches by label. Escape, Tab, outside pointer interaction and blur close the popup. Multiselect remains open when an item is toggled. Disabled options cannot be chosen.
Run npm run storybook from the workspace root for default and custom-template examples.
Checkbox and radio groups
<ple-checkbox-group
inputId="interests"
label="Interests"
[options]="options"
[(value)]="interests"
/>
<ple-radio-group inputId="delivery" label="Delivery" [options]="options" [(value)]="delivery" />Both accept PleOption<T>[], an optional itemTemplate, and Signal Forms
[formField]. The checkbox value is T[]; the radio value is T | null.
Groups use native fieldset, legend, and inputs. Each group needs its own
unique inputId, also used as the native radio group name. label supplies
the legend; describedBy associates hints. disabled, readonly, required,
and invalid integrate with form state. For checkbox groups, required means a
nonempty selection in the form schema, rather than requiring every checkbox.
Toggle and toggle button
<ple-toggle label="Notifications" [(checked)]="notifications" />
<button pleToggleButton label="Status" [options]="statuses" [(value)]="status"></button>PleToggle implements a boolean switch and binds to Signal Forms through
[formField]. PleToggleButton<T> accepts the same PleOption<T> model as
the selectors. Click, Enter, or Space selects the next enabled option and
wraps to the beginning. Disabled options are skipped; an empty list or an
all-disabled list disables the button. With no matching current value, the
first enabled option is selected on the next click. Set the initial value
explicitly to show an initial option rather than the placeholder.
A two-option button exposes aria-pressed (the second option is pressed).
For more options it is an ordinary button announcing the current state in
its accessible name. label is required. The button supports itemTemplate,
disabled, readonly, invalid, and describedBy.
Datepicker
<label for="appointment">Appointment</label>
<ple-datepicker
inputId="appointment"
[(value)]="appointment"
format="dd.MM.yyyy HH:mm"
[minDate]="min"
[maxDate]="max"
showTime
showClear
requireConfirmation
/>Import PleDatepicker from @pleta/ui. It implements Signal Forms
FormValueControl<Date | null> and supports [formField], touch, focus() and
reset() (reset discards the draft; the form owns its value). InputId must be unique.
Selection is popup-only; there is no inline calendar or free-text date parsing.
Public inputs: format, minDate, maxDate, showTime (false),
view: 'date' | 'month' | 'year' (date), showClear (false),
requireConfirmation (false), disabled, readonly, required, invalid,
labelledBy, describedBy, placeholder. Bind [(value)] or listen to valueChange.
Format uses Angular tokens, e.g. dd.MM.yyyy, MM.yyyy, yyyy, dd.MM.yyyy HH:mm. Formatting uses en-US for named tokens; calendar labels use ru-RU and Monday-first weeks. Dates are local calendar values, not UTC serialization. Without time the value is local midnight; with time precision is minutes. Month/year values are the first day of the selected month / January 1. Time is ignored in these views. Min/max are inclusive at the chosen precision: day, minute, month or year. Invalid or reversed bounds prevent opening. Existing invalid external values are not silently changed; validation of form data belongs to the application.
Without confirmation, date/month/year clicks commit immediately. With showTime, the popup remains open to edit time; valid edits commit immediately. Invalid time drafts never commit. With requireConfirmation, the field previews draft changes, but only Apply emits a new value. Cancel, Escape, the close button, trigger toggle, outside click and focus leaving the popup discard the draft. Clear is also staged in confirmation mode and opens the footer if necessary; Apply then emits null. An external value, bounds or mode change cancels the open draft. Required does not prevent clearing; apply the required rule in your form schema.
Navigation: arrows, Home/End, PageUp/PageDown; Enter/Space selects. Header month/year
buttons change the navigation view. Escape restores trigger focus; outside click
does not steal focus. Popup uses the existing floating layer, native buttons and
grid semantics. Native time inputs support hours 0–23 and minutes 0–59.
See Storybook /datepicker for all modes and Signal Forms.
Password
<label for="password">Password</label>
<ple-input-password inputId="password" [formField]="fields.password" autocomplete="new-password" />The string value is available via [(value)] or [formField]. The reveal
button changes visibility without modifying the value. reset() hides the
password, and disabling the control hides it as well. autocomplete defaults
to current-password; use new-password for registration. The control also
accepts name, placeholder, minLength, maxLength, required, invalid,
disabled, readonly, and describedBy. Focus moving between the input and
the reveal button stays within the control; leaving it emits touch.
Layout
Internal shared UI components live in src/lib/_common/: select-panel,
choice-group, and option-content. The common option model lives there too.
Every storybook page has its own directory under apps/storybook/src/app/pages/;
shared storybook components live under apps/storybook/src/app/_common/.
Floating layer, tooltip and popover
PleFloatingLayer in _common is reused by select, multiselect, tooltip,
popover and popup menu. The native Popover API puts the surface in the top
layer; our own positioning implementation handles anchor tracking, flipping and viewport limits without external runtime dependencies. It tracks geometry with requestAnimationFrame while the layer is open and stops on close.
The shared stack dismisses only the top layer on Escape/outside interaction.
<button #help>Help</button>
<ple-tooltip [origin]="help" tooltipId="help-tip" text="More information" />
<button #settings>Settings</button>
<ple-popover [origin]="settings" panelId="settings-panel" label="Settings">
<button type="button">An action</button>
</ple-popover>Use unique tooltip/panel IDs. Tooltip opens on hover/focus and preserves
existing descriptions. Popover toggles on anchor click, focuses its content,
and restores anchor focus on Escape. Both accept Floating UI placement.
Popover supports [(open)] and close(). Origins are DOM elements; with
pleButton, use trigger.element.
Dialog and sidebar
Inject PleDialog to create a dynamic Angular component:
const ref = dialogs.open<ProfileResult, ProfileData>(ProfileEditor, {
title: 'Edit profile',
data: { name: 'Anna' },
owner: destroyRef,
// mode: 'sidebar', side: 'end', // or side: 'start'
});
const result = await ref.closed; // undefined on dismissalInside the component use injectDialogData<ProfileData>() and
injectDialogRef<ProfileResult>(); call ref.close(result) to return a value.
Optional inputs sets component inputs via ComponentRef.setInput;
injector supplies the parent injection scope.
Native modal dialog makes the background inert. Scrolling is locked, Tab
stays inside the dialog, and close restores focus. Nested selectors, tooltips
and dialogs are supported. Sidebar uses the same lifecycle with edge
positioning and slide animations. Reduced motion is respected.
closeOnEscape: false / closeOnBackdrop: false disable those dismissal
methods; the header close button remains available. Passing owner: DestroyRef
disposes the dialog with its caller; closeAll() disposes all immediately.
Call open() only in browser interactions, not during SSR.
Slider
<ple-slider inputId="volume" label="Volume" [min]="0" [max]="100" [(value)]="volume" />Both bounds are required and finite, with min < max. step defaults to 1
and must be positive. External values and changed bounds are normalized to
the interval and the step grid starting at min. Supports disabled,
readonly, invalid, describedBy, touch, and focus().
With Signal Forms, Angular owns min/max bindings. Supply both schema rules
instead of binding min/max on the same element as [formField]:
readonly fields = form(this.model, path => {
min(path.volume, 0);
max(path.volume, 100);
});<ple-slider inputId="volume" label="Volume" [formField]="fields.volume" />File input
Import PleInputFile and use <ple-input-file accept=".pdf,image/*" multiple
[(files)]="files" [loading]="loading()" />. files is a readonly File[] model;
filesChange emits the complete list after selection or removal. External updates
can clear or replace it. Single selection replaces the previous file; multiple
selection appends and deduplicates by name, size and lastModified. Cancelling the
picker keeps the list. Removed files can be selected again.
accept only filters the native picker; validate files in your application.
disabled and loading block selection and removal. Loading displays PleInProgress
and sets aria-busy. The component does not upload files or call endpoints.
Customize the button using buttonTemplate or <ng-template pleTemplate="button">
(import PleTemplate). Context: choose(), disabled (includes loading), loading.
The explicit template input takes precedence. Give your custom button an accessible
name and call choose from its click handler. Examples and API: Storybook /input-file.
Button
<button pleButton severity="success" raised>Save</button>
<button pleButton text raised>Raised text</button>
<a pleButton routerLink="/" outlined>Home</a>
<button pleButton rounded aria-label="Add"><span aria-hidden="true">+</span></button>PleButton is an attribute component: directive-style usage with encapsulated
styles and projected content. There is no label input; text is a visual flag.
Button variants are configured through the inputs listed below.
Severity values: primary, secondary, success, info, warn, danger, help, contrast.
raised, rounded, text, outlined, disabled, iconOnly are boolean inputs.
Raised Text combines two flags; outlined takes precedence over text for the
border. type defaults to button; specify submit or reset when needed.
Anchors preserve native link semantics.
SVG, images, aria-hidden="true" and pleButtonIcon markers allow automatic
icon-only detection, including dynamic content. Otherwise set iconOnly.
Icon-only buttons are fixed at 44 × 44 px; supply aria-label.
Disabled anchors leave the tab order and block click handlers/navigation.
Do not nest interactive elements inside the projected content.
Button group
<ple-button-group label="Actions">
<button pleButton>Save</button>
<a pleButton routerLink="/">Home</a>
<button pleButton>Close</button>
</ple-button-group>PleButtonGroup projects content and joins direct child button/a elements.
The first has only left rounded corners, the last only right rounded corners,
and intermediate elements have none. A single element keeps all four corners.
The group overrides individual rounded variants. Hidden elements are skipped;
dynamic insertion/removal updates the edges. Nested groups are independent.
The default outer radius is 8px; override --ple-button-group-radius on the group.
Original inline corner styles are restored when an element leaves the group.
Use the required label to name the group for assistive technology. Native link
and button behavior, disabled states and tab order are preserved.
Menu
<button #trigger pleButton>Actions</button>
<ple-menu
[items]="items"
menuId="actions"
label="Actions"
popup
[origin]="trigger.element"
align="right"
contentAlign="left"
[itemTemplate]="item"
/>
<ng-template #item let-item let-index="index">
<span>{{ item.label }}</span> <small>{{ item.data?.shortcut }}</small>
</ng-template>Without popup, the menu is inline. Popups use the shared floating layer,
toggle on anchor click, and support [(open)] and close().
align: left/right; contentAlign: left/center/right. Viewport collision
handling takes precedence over alignment when space is insufficient.
PleMenuItem<T = any> has label?, icon? (a PleIconName from @pleta/icons),
separator?, disabled?, visible?, url?, target?, data?: T, and
command?(event). Untyped items have the requested data?: any; consumers
can opt into typed data. selected and command receive { originalEvent, item }.
Action items need a label; separators do not. Menu IDs must be unique.
One item template receives $implicit: item and index. It replaces content
inside the interactive wrapper: do not put buttons, links or inputs inside it.
Arrow keys, Home/End and label search navigate enabled items. Enter/Space
activate, Escape restores anchor focus, and Tab/outside interaction close
without trapping focus. This is a single-level menu; no nested submenus yet.
Browser baseline
Interactive overlays require native Popover API and modal dialog support. Automated browser checks currently run in Microsoft Edge with desktop/mobile viewports and AXE. Firefox, Safari and real assistive technology need a release compatibility pass.
Panel and tabs
PlePanel takes required panelId and title, [(expanded)] (default true),
disabled, and an optional headerTemplate whose implicit value is expanded.
Projected content stays mounted while hidden. Header templates must not contain
interactive elements because the header itself is a button. Disabled prevents
user toggling; the parent can still change expanded.
PleTabs takes required tabsId, label and tabs, plus
[(active)] containing a tab ID. Each tab has a unique id, label and optional
disabled flag. Supply content through contentTemplate or a projected
<ng-template pleTemplate="content">. Content templates receive the tab; optional headerTemplate also
receives selected. Only active content is instantiated, and switching tabs
destroys the previous view. Keep form/business state in the parent when it needs
to persist. Arrow keys and Home/End navigate enabled tabs. Missing/disabled active
IDs display the first enabled tab; with all tabs disabled no content is shown.
Confirmation and toast services
await inject(PleConfirm).confirm({ title, message, severity: 'danger', owner })
returns true only for acceptance; dismissal/rejection returns false. Optional
acceptLabel/rejectLabel customize buttons. Cancellation receives initial focus.
Confirmation uses the existing dialog service and its focus/cleanup behavior.
Place one <ple-toast-outlet /> in the root template. Inject PleToast and call
show({ title, message, severity, duration }), which returns an ID. Call dismiss(id)
or clear() to remove notifications. Duration defaults to 5000 ms; zero is sticky.
The outlet displays up to maxVisible (default 3); queued timers start only when
shown and pause during hover/focus. Positions: top-right, top-left, bottom-right,
bottom-left. Notifications do not take focus. Use persistent notifications for
information the user must explicitly dismiss.
Field, textarea, tag and breadcrumb
textarea[pleTextarea] styles a native textarea and uses native Angular form
integration. Put its content/value in a form or native value binding.
PleField takes required controlId and label, plus hint, required and errors
(a string array). Project a control whose native focusable element has controlId.
The wrapper connects hint/error IDs via aria-describedby, preserving existing
descriptions, and sets aria-invalid for displayed errors. The caller supplies
validation state, e.g. messages from a touched Signal Forms field. It does not
create form validators. Field and hint/error IDs must be unique.
PleTag takes label, severity, optional icon (PleIconName) and removable.
The removed output requests removal; the parent owns visibility and subsequent
focus. PleBreadcrumb takes items with label and optional url/routerLink/icon.
The last item is current and not a link. Optional itemTemplate receives item and
current. Both icon inputs use the SVG set from @pleta/icons; arbitrary SVG icons
can be rendered in custom templates where supported.
Paginator and skeleton
PlePaginator takes total, [(page)] with zero-based index and positive size,
pageSizes, disabled and label. Changing page size resets index to zero. The
parent supplies the corresponding data and clamps/reset its state when totals
change. The table uses this same paginator.
Navigation uses PleButton and SVG icons from @pleta/icons; page size uses PleSelect. The optional pageSizeId names the internal select (generated per app by default). Provide stable unique pageSizeId values when server/client creation order differs.
PleSkeleton accepts CSS width/height strings, shape (rectangle/circle), and
animated (default true). Give equal width and height for a circle. It is purely
decorative and aria-hidden, with no accessible text in its markup. Put aria-busy
and a loading announcement on the containing content. Reduced-motion disables
the shimmer. The /table page uses skeletons in loadingTemplate.
In-progress indicator
Import PleInProgress from @pleta/ui: <ple-in-progress label="Updating users" />.
Its only input is label (default Загрузка). It exposes an indeterminate
progressbar with an accessible name and no numeric ARIA values. Show/hide it
using application loading state. It does not fetch data.
CSS variables: --ple-in-progress-height (4px), --ple-in-progress-radius
(999px), --ple-in-progress-color (#2459c4), --ple-in-progress-track (#e4ecfc),
--ple-in-progress-duration (1.4s). Reduced-motion disables animation;
forced-colors uses system colors. Markup is deterministic with no browser APIs,
timers or Zone.js. Examples: /in-progress, /table-remote.
Loading buttons and dialog overlays
PleButton accepts [loading]. A decorative spinner overlays the projected content
without changing its size or accessible name; aria-busy marks the operation and
both buttons and links block activation while loading. Keep the projected label
unchanged if its dimensions must stay identical. The application owns loading state.
PleDialog.open(Component, config) accepts CSS lengths in width, maxWidth and
edgeOffset, plus position: 'center' | 'top'. Defaults: 560px wide, centered,
16px edge spacing (sidebar: 440px, 0px). Width and height remain constrained by
the viewport; oversized offsets are clamped to keep the surface usable. Sidebars
keep their configured side and also support width, maxWidth and edgeOffset.
dialogs.open(Editor, {
title: 'Edit',
width: '900px',
maxWidth: '720px',
position: 'top',
edgeOffset: '24px',
});Tooltips on elements behind a PleDialog close automatically and pending openings
are cancelled. Such tooltips cannot consume Escape intended for the active modal.
Tooltips inside the active dialog continue to work, including nested dialogs.
Colors and themes
All UI colors, including table and dashboard colors, use inherited --ple-* CSS
custom properties with light defaults. No theme service or runtime switcher is required.
The Storybook /theme page lists every token, its default and purpose, and provides
the complete CSS palette to copy into your application's global stylesheet.
Alternatively, import the optional default palette at the start of your global CSS:
@import '@pleta/ui/theme.css';Override variables in html[data-theme='dark'] or a prefers-color-scheme media
query. Your application chooses the colors and sets the attribute. Set
color-scheme: dark for native controls as appropriate. Apply global themes to
html so service-created dialogs and notifications under body inherit them too.
Pair each accent with its --ple-on-* foreground to maintain readable contrast.
Component overrides such as --ple-table-background and --ple-in-progress-color
remain supported and fall back to the shared palette.
Field errors (Signal Forms and Reactive Forms)
PleFieldError is independent of PleField: use a native label, a control and
an error component. It supports both current Angular forms APIs:
<!-- Signal Forms -->
<label for="name">Name</label>
<input pleInput id="name" [formField]="fields.name" />
<ple-field-error [field]="fields.name" />
<!-- Reactive Forms, inside [formGroup] or formGroupName -->
<input pleInput id="reactive-name" formControlName="name" />
<ple-field-error fieldName="name" />Pass exactly one of field and fieldName. Reactive paths resolve relative to
the nearest ControlContainer, including nested groups/arrays. A missing control
produces a configuration error. Changes made with emitEvent: false do not notify
Reactive Forms subscribers; emit a control event to refresh displayed errors.
Messages can be configured globally or in a component's providers:
providePleFieldErrors({
required: 'Обязательное поле',
spaces: 'Введите хотя бы один непробельный символ',
maxlength: ({ actualLength, requiredLength }) =>
`Максимальная длина ${requiredLength}, текущая ${actualLength}`,
taken: ({ project }: { project: string }) => `Имя уже используется: ${project}`,
});Local [messages] overrides DI messages per error key. DI messages override
explicit Signal Forms error messages, then built-in defaults apply. Unknown keys
fall back to «Некорректное значение». Built-ins: required, email, minlength,
maxlength, min, max, pattern and spaces. spaces is an optional application
validator convention, not a validator supplied by this component.
Strings are literal text, not translation keys or interpolation templates.
Functions receive the error payload. Known validators have typed parameters;
custom callbacks can declare their own parameter type (the validator must return
that shape). Signal Forms minLength/maxLength kinds are normalized to
minlength/maxlength with requiredLength/actualLength, matching Reactive
Forms. Other custom Signal Forms callbacks receive the error object, including
its custom properties. No translation library is required. Pass a computed
messages dictionary or read a locale signal in a formatter for language changes.
Default showWhen="interacted" shows errors after touch, edit or submission.
Other modes: touched, dirty, submitted, always. Disabled/pending fields hide errors.
Signal Forms submission touches invalid fields; use an external [submitted]
flag for explicit submission-only visibility and reset it with the form.
controlId is optional. Signal Forms uses its public field bindings for automatic
ARIA associations; Reactive Forms native PleInput, PleInputNumber and PleTextarea
register automatically. Other Reactive Forms controls can opt in with the exported
PleFieldControl directive (pleFieldControl) or supply an explicit controlId.
Existing aria-describedby tokens are preserved. Errors render on separate lines,
use --ple-error, decorative icons and a polite live region. Avoid rendering both
PleField errors and PleFieldError for the same control.
Storybook /field-error includes both forms APIs, nested groups, overrides and reset.
Package boundaries
Table is exported from @pleta/table, not @pleta/ui. See its README for the full
template and controlled-data API. SVG icons are exported from @pleta/icons.
Build order is icons → ui → table; all workspace build/Storybook scripts handle
this automatically. Public components have dedicated Storybook pages; shared
internal components under _common are exercised through their public owners.
Theme documentation
Theme documentation is consolidated on Storybook /theme, in the @pleta/ui
tab: shared color palette, component overrides, default values and copyable CSS.
The other tabs document table, dashboard and icon styling. The shared palette
also controls table and dashboard; no runtime theme provider is needed.
License
Pleta uses the proprietary Pleta Free Application Use License. Personal and commercial application use is free, including paid products, client projects, and SaaS. Application builds and distribution with bundled Pleta code are permitted. Modifying or forking the library itself, extracting its implementation, and republishing it as a standalone package or reusable library are not permitted except as stated in the license. Include the license in your application's legal notices. Third-party materials retain their own licenses. See the LICENSE file shipped with each package for the full terms.
