@soxa-ui/angular 0.0.0-stage → 0.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuri Soldatov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,614 @@
1
- # Temporary Holding Version
1
+ # @soxa-ui/angular
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **The Angular adapter: Soxa UI components rendered on the headless models of the core.**
4
+
5
+ Part of [Soxa UI](https://github.com/youra-h/soxa-ui) — UI components with the logic written once: a
6
+ framework-agnostic core and thin adapters that render it in Vue, React, Angular, Svelte, Solid and
7
+ Web Components.
8
+
9
+ The adapter is wiring, not behavior: it connects a core model to Angular's signals and renders the
10
+ markup — BEM classes `so-*` and `data-*` state attributes. Props, events and slots come from the
11
+ component descriptor, so they match the other five adapters; only the spelling is Angular's: a
12
+ component is always a prefixed attribute, never a tag of its own — on your own element when it has
13
+ an element to render into (`<button so-button>`, `<div so-input>`; the toasts manager has its
14
+ region, so it is `<div so-toasts>`), and on an `<ng-container>` when it renders no element of its
15
+ own (`<ng-container so-virtual>`, `<ng-container so-drag-and-drop>`, like
16
+ `<ng-container so-locale-provider>`) — events are outputs
17
+ named `so` + the core name in camelCase (`show` → `soShow`, `show:before` → `soShowBefore`,
18
+ `element:ready` → `soElementReady`), and slots are filled through the `slot` attribute. Every
19
+ ordinary event output has the prefix: Angular binds `(x)` both to an output and to the DOM event of that name, so
20
+ `(input)` on a field is the browser's event, as on a native `<input>`, and the component's own is
21
+ `(soChangeValue)`.
22
+
23
+ The components are standalone and zoneless — state is a signal, so Zone.js is not required. They
24
+ render on the server (`@angular/platform-server`) and hydrate in the browser
25
+ (`provideClientHydration`); component ids match on both sides.
26
+
27
+ > **Status:** the Angular adapter is in progress — Button, Icon, Label, Frame, ProgressSpinner,
28
+ > ProgressLinear (`<span so-progress-linear>`), Skeleton, Input, NumberInput (a number in the
29
+ > locale's format with − and + step buttons; the form gets the number), Textarea (multi-line,
30
+ > growing with its text, with a character counter), CheckBox, Switch, Slider (`<span so-slider>`),
31
+ > Rating (`<span so-rating>`), the form field `<div so-field>` (label, helper and error linked to
32
+ > the control inside it), Scroller (`<div so-scroller>`), RadioGroup, ListBox, Tags
33
+ > (`<div so-tags>`), Select (`<div so-select>`, a field with a list in a panel), Tabs, Accordion,
34
+ > Pagination (`<nav so-pagination>`), Table, the dates — DateInput (`<div so-date-input>`, a date
35
+ > field of parts), Calendar (`<div so-calendar>`) and DatePicker (`<div so-date-picker>`, a date
36
+ > field with a calendar in a panel) —, the `Virtual` window for long lists and the
37
+ > `<ng-container so-drag-and-drop>` wrapper that lets users reorder the items of Tabs, ListBox and
38
+ > Accordion by dragging (`(soMove)`, `(soMoveBefore)` can cancel) are ported, the rest of the set is on its
39
+ > way. Frame moves the element you wrote into `target` (`body` by default) in the browser; with
40
+ > `contained` it stays in place. Popover (`<span so-popover>`) and Tooltip (`<span so-tooltip>`)
41
+ > are panels on a Frame anchored to the element you write them on. Menu (`<span so-menu>`) is an
42
+ > action menu at a button or, with `openOn="contextmenu"`, on right click, with submenus,
43
+ > checkbox and radio items and an item with its own panel. The modal window Dialog
44
+ > (`<div so-dialog>`) and the sliding panel Drawer (`<div so-drawer>`) are layers themselves, with a
45
+ > backdrop placed before them. Toasts (`<div so-toasts>` around the application, a toast from code
46
+ > with `useToasts().show(…)`) show notifications at an edge of the window. `so-locale-provider`
47
+ > gives a subtree its language and library strings. See
48
+ > [the adapter table](https://github.com/youra-h/soxa-ui#components).
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ npm install @angular/forms @soxa-ui/angular @soxa-ui/setup @soxa-ui/theme-oren @soxa-ui/icons-material
54
+ ```
55
+
56
+ Install `@soxa-ui/setup` as a direct dependency for the `useIcons` and `useTheme` imports below.
57
+ Keep it on the same version as `@soxa-ui/angular`.
58
+
59
+ `@angular/core`, `@angular/common` and `@angular/platform-browser` (Angular 22.2+) are peer
60
+ dependencies. A theme and an icon pack are separate packages — pick the ones you want.
61
+
62
+ Add `provideAttrs()` to the providers of your application config — the one shared by the browser and
63
+ the server:
64
+
65
+ ```ts
66
+ import { ApplicationConfig } from '@angular/core'
67
+ import { provideAttrs } from '@soxa-ui/angular'
68
+
69
+ export const appConfig: ApplicationConfig = {
70
+ providers: [provideAttrs()],
71
+ }
72
+ ```
73
+
74
+ It lets the bindings and listeners you write on a component reach the node inside it they belong to
75
+ (see the fields below). It is required: without it a field, Select, a collection item, ListBox,
76
+ Tabs, Table and Toasts throw an error telling you to add it. Put it in the application config, not in the providers
77
+ of a route or a component — there it cannot replace the renderer of the application.
78
+
79
+ ## Usage
80
+
81
+ ```ts
82
+ import { Component } from '@angular/core'
83
+ import { TButtonComponent } from '@soxa-ui/angular'
84
+
85
+ @Component({
86
+ selector: 'app-toolbar',
87
+ standalone: true,
88
+ imports: [TButtonComponent],
89
+ template: `<button so-button text="Save" (soActionPress)="onPress()"></button>`,
90
+ })
91
+ export class ToolbarComponent {
92
+ onPress(): void {
93
+ console.log('pressed')
94
+ }
95
+ }
96
+ ```
97
+
98
+ There is no wrapper: the element you write is the component's root. Any tag works and keeps its
99
+ browser behavior — `<button so-button>` is a native button, `<a so-button href="/next">` is a link,
100
+ `<div so-button>` is a `div` that gets `role="button"` and `tabindex="0"`. The tag is read from the
101
+ element, so there is no `tag` input. Your `class`, `style`, attributes and event listeners stay on
102
+ the element. What the component writes on it — a class, an attribute, a style property — is the
103
+ component's, the way a host binding (`[attr.x]`, `[class.x]`) is: it writes over a value you set
104
+ under the same name, so `<div so-button tabindex="-1">` still gets `tabindex="0"`. Names the
105
+ component does not write stay yours. `rendered` set to `false` hides the element instead of
106
+ removing it — the element is yours — and your inline `display` wins over hiding.
107
+
108
+ Import the component you use: without `TButtonComponent` in `imports`, `<button so-button>` is a
109
+ plain button and Angular reports nothing; only an input binding such as `[text]` fails (NG8002).
110
+
111
+ The same component can be driven from code — pass a core instance as `ctrl`, and both sides see the
112
+ same state and events:
113
+
114
+ ```ts
115
+ template: `<button so-button [ctrl]="btn" (soActionPress)="btn.disabled = true"></button>`
116
+ ```
117
+
118
+ An icon from the registered pack is an `<svg>` with the icon's role — import `RoleIconDirective`
119
+ next to `TIconComponent`. Your own drawing is registered as a role too (`useIcons`): the icon renders
120
+ nothing written inside it.
121
+
122
+ ```ts
123
+ template: `<svg so-icon so-role-icon="close" aria_label="Close"></svg>`
124
+ ```
125
+
126
+ A field — Input, NumberInput, Textarea, CheckBox, Switch — is written on a `div` or, for the inline
127
+ CheckBox and Switch, a `span` (`<div so-input>`, `<div so-number-input>`, `<div so-textarea>`,
128
+ `<span so-check-box>`, `<span so-switch>`) and renders a native `<input>` (a `<textarea>`) inside,
129
+ so your markup is split the way Vue and React split it: `class` and `style` stay on the field,
130
+ while everything else you write on it goes to the `<input>` — static attributes (`autocomplete`,
131
+ `data-*`), bindings (`[attr.aria-describedby]`, `[aria-label]`, `[title]`) and listeners
132
+ (`(focus)`, `(blur)`), which is what `provideAttrs()` is for. A field is not an `<input>` itself —
133
+ `<input so-input>` throws, since its template would have nowhere to render. A field gets its
134
+ visible name from a Label around it:
135
+
136
+ ```ts
137
+ template: `<label so-label text="Remember me"
138
+ ><span so-check-box (soChangeValue)="remember($event)"></span
139
+ ></label>
140
+ <div so-input clearable name="City" autocomplete="address-level2" (blur)="check()"></div>`
141
+ ```
142
+
143
+ Slots that hand data in are `<ng-template slot>` — the content next to an Input gets the field
144
+ itself:
145
+
146
+ ```ts
147
+ template: `<div so-input>
148
+ <ng-template slot="trailing" let-ctrl="ctrl">{{ ctrl.value?.length ?? 0 }}/40</ng-template>
149
+ </div>`
150
+ ```
151
+
152
+ A number field shows its number in the locale's format — `1,234.5`, `12%` or `RUB 1,234.50` with
153
+ `formatOptions` — and steps it within `min` and `max` with the arrow keys and the − and + buttons,
154
+ which repeat while held. The form gets the number, not the text: `name` goes to a hidden `<input>`
155
+ with `1234.5`, while what you write on the field goes to the text `<input>`. `(soChangeValue)`
156
+ comes on every typed digit and step, `(soCommit)` once per finished action — leaving the field,
157
+ Enter, releasing a step button. The icons of the step buttons are templates,
158
+ `<ng-template slot="decrement-icon">` and `increment-icon`:
159
+
160
+ ```ts
161
+ template: `<div so-number-input name="qty" [min]="0" [value]="qty()" (soCommit)="save($event.newValue)"></div>`
162
+ ```
163
+
164
+ A slider and a rating are fields as well: `<span so-slider>` has a native `<input type="range">` in
165
+ each thumb, `<span so-rating>` one under its row of stars, and the form gets their values under
166
+ `name`. A number is one thumb, an array is a thumb per item (`[value]="[20, 80]"`). The pointer
167
+ works on the track, never on the inputs, so what you write on these two stays on the element; only
168
+ a rating's `id` is an input, and it goes to its range input, where `<label for>` finds it. With
169
+ several thumbs the slider is a group named by `aria_label`, and each thumb takes its own name from
170
+ `thumbLabels`. The `mark` and `thumb` slots of a slider and the `icon` slot of a rating are
171
+ templates with the data of their mark, thumb or star:
172
+
173
+ ```ts
174
+ template: `<label so-label text="Volume" position="top"><span so-slider name="volume" [value]="40"></span></label>
175
+ <span so-slider [value]="[20, 80]" aria_label="Price" [thumbLabels]="['From', 'To']" tooltip="always">
176
+ <ng-template slot="thumb" let-value="value">{{ value }} ₽</ng-template>
177
+ </span>
178
+ <span so-rating name="score" [value]="3.5" [step]="0.5" aria_label="Score" (soCommit)="rate($event.newValue)"></span>`
179
+ ```
180
+
181
+ A collection — RadioGroup, ListBox, Tabs, Accordion — is an owner element with its items inside
182
+ (`<div so-radio-group>`, `<div so-list-box>`, `<div so-tabs>`, `<div so-accordion>`). The parts are
183
+ components of their own, written as separate elements, since Angular has no dot syntax:
184
+ `<div so-list-box-item>`, `<div so-tabs-item>`, `<div so-accordion-item>`,
185
+ `<label so-radio-group-item>` and the tab panel `<div so-tabs-content>`. Items written in the markup
186
+ join the collection in document order, on the server too; without them the owner renders its
187
+ `items`. Inputs of the collection — `items`, `engine`, `mode`, an item's `selected` or `active` — are
188
+ inputs of the same element:
189
+
190
+ ```ts
191
+ template: `<div so-list-box mode="multiple" aria-label="Cities" (soChangeValue)="pick($event.newValue)">
192
+ <div so-list-box-item value="a" text="Moscow"></div>
193
+ <div so-list-box-item value="b" text="Kazan" selected></div>
194
+ </div>
195
+ <div so-tabs>
196
+ <div so-tabs-item value="a" text="General" active></div>
197
+ <div so-tabs-item value="b" text="Mail"></div>
198
+ <div so-tabs-content slot="content" value="a">…</div>
199
+ <div so-tabs-content slot="content" value="b">…</div>
200
+ </div>`
201
+ ```
202
+
203
+ A list box and tabs split your markup like a field: `class` and `style` stay on the element — a
204
+ frame around the list, next to the header and footer of a list box or the panels of tabs — while
205
+ everything else you write on it (`aria-label`, `id`, `tabindex`, `data-*`, `[attr.x]`, `(keydown)`)
206
+ goes to the list itself, `[role=listbox]` or `[role=tablist]`, which it names. An item splits your
207
+ markup the same way: `class` and `style` stay on the item, the rest (`title`, `id`, `(click)`) goes
208
+ to its row — or to the `<input>` of a radio. Animation and transition events (`(transitionend)`)
209
+ stay on the element you wrote: that is where they happen. Inside
210
+ a Label, write the radio on an inline tag, `<span so-radio-group-item>`: a `label` inside a `label`
211
+ is not allowed. Items rendered from `items` get their slots from the owner — `item`, `item-leading`
212
+ and the rest receive the item along with their scope:
213
+
214
+ ```ts
215
+ template: `<div so-radio-group [items]="options">
216
+ <ng-template slot="item" let-item="item" let-active="active">{{ item.value }}</ng-template>
217
+ </div>`
218
+ ```
219
+
220
+ The `[so-attributes]` directive on such a component goes the same way as the rest of your markup:
221
+ the set lands on the `<input>`, the row or the list, on top of what the component puts there
222
+ itself.
223
+
224
+ A slot by which a part decides whether to render its own default — the close icon of a tab, the
225
+ arrows of an accordion section, the edges of the tab list — is an `<ng-template slot>` only, and a
226
+ template given to the owner renders in every item in place of its own. A tab panel that is not
227
+ active stays in place hidden, its content not inserted; a markup tab closed with its close button
228
+ is hidden the same way — the element is yours.
229
+
230
+ A tab panel takes its content as is or as a template, `<ng-template slot="default">`, and a given
231
+ template is drawn instead. Write heavy content — a table, a calendar, a form — as a template: the
232
+ panel creates it when its tab is chosen and removes it when the tab is left, so an inactive panel
233
+ holds nothing. Content written as is Angular creates right away, with the template that wrote it:
234
+ the components of every panel live as long as the tabs do, an inactive panel only keeps them out of
235
+ the document.
236
+
237
+ ```ts
238
+ template: `<div so-tabs>
239
+ <div so-tabs-item value="a" text="General" active></div>
240
+ <div so-tabs-item value="b" text="History"></div>
241
+ <div so-tabs-content slot="content" value="a">…</div>
242
+ <div so-tabs-content slot="content" value="b">
243
+ <ng-template slot="default"><div so-table [items]="rows" [columns]="columns"></div></ng-template>
244
+ </div>
245
+ </div>`
246
+ ```
247
+
248
+ A tag set, `<div so-tags>`, is the row itself: your markup stays on it, next to its role. Tags are
249
+ `<div so-tags-item>` parts or come from `items`; with `mode` they become selectable, and with
250
+ `closable` every tag gets a close button. To lay the shown tags out yourself — in a scroller strip,
251
+ or a row with the rest in a popover — give the set a `default` template: its context carries
252
+ `shown`, and every tag is rendered in it exactly once.
253
+
254
+ ```ts
255
+ template: `<div so-tags closable [items]="cities" (soChangeValue)="pick($event.newValue)"></div>
256
+ <div so-tags overflow="scroll" [items]="cities">
257
+ <ng-template slot="default" let-shown="shown">
258
+ <div so-scroller>
259
+ @for (tag of shown; track tag.uid) {
260
+ <div so-tags-item [ctrl]="tag"></div>
261
+ }
262
+ </div>
263
+ </ng-template>
264
+ </div>`
265
+ ```
266
+
267
+ A select, `<div so-select>`, is a field with the list in a panel anchored to the element, moved into
268
+ `body` like a Frame. Options are `<div so-select-item>` parts, grouped with
269
+ `<div so-select-group label="…">`, or come from `items`; with `mode="multiple"` the chosen ones
270
+ show as tags in the field, `editable` lets you type, and `editableMode="filter"` narrows the list.
271
+ It splits your markup like a field: `class` and `style` stay on the element, while everything else
272
+ — `aria-label`, `id`, `[attr.x]`, `(keyup)` — goes to the `<input>` of its field. The `field`,
273
+ `tags`, `clear`, `arrow-icon` and item slots are templates; `leading`, `trailing` and `empty` are
274
+ elements by the `slot` attribute. Options join the collection on the server too, so the server
275
+ renders the field with the chosen text; the panel exists only in the browser.
276
+
277
+ ```ts
278
+ template: `<div so-select placeholder="City" aria-label="City" (soChangeValue)="pick($event.newValue)">
279
+ <div so-select-item value="msk" text="Moscow"></div>
280
+ <div so-select-group label="Volga">
281
+ <div so-select-item value="kzn" text="Kazan"></div>
282
+ </div>
283
+ </div>`
284
+ ```
285
+
286
+ A date field, `<div so-date-input>`, is a group of parts in the format of the subtree's locale —
287
+ day, month and year, with `kind="datetime"` also the time; the form gets the date as a string. A
288
+ calendar, `<div so-calendar>`, draws its month grids itself — a day is a cell,
289
+ `<td so-calendar-item>` — with one pair of paging buttons and a month and year picker under each
290
+ month title. `<div so-date-picker>` is a date field, or two for a range, with a calendar in a panel
291
+ anchored to the element and moved into `body` like a Frame. None of them has a native field to pass
292
+ your markup to, so it all stays on the element you write them on, the `id` of a date field and a
293
+ date picker too. The slots are templates: the calendar's `item` (`let-item`, `let-text`),
294
+ `prev-icon`, `next-icon` and `footer`; the date field's `leading`, `clear` and `trailing`; the date
295
+ picker's `clear`, `trigger-icon` and `item`.
296
+
297
+ ```ts
298
+ template: `<div so-date-picker mode="range" aria_label="Trip" (soChangeValue)="trip.set($event.newValue)"></div>
299
+ <div so-calendar [months]="months">
300
+ <ng-template slot="item" let-text="text"><b>{{ text }}</b></ng-template>
301
+ </div>`
302
+ ```
303
+
304
+ Pagination draws its pages itself — from `total` and `pageSize`, or from a list of `pages` — and
305
+ has no content of its own: a page is an inner part, `<li so-pagination-item>` with a button in it,
306
+ and the `item` slot gives every page your template with the page and its text. A scroller lays its
307
+ content out in its viewport, between the two buttons; a role for the row and what goes with it
308
+ comes to the viewport as `viewportAria`. The arrow icons of both are templates, `prev-icon` and
309
+ `next-icon`:
310
+
311
+ ```ts
312
+ template: `<nav so-pagination [total]="200" [value]="page()" aria_label="Result pages" (soChangeValue)="page.set($event.newValue)"></nav>
313
+ <div so-scroller>
314
+ <button so-button text="First"></button>
315
+ <button so-button text="Second"></button>
316
+ </div>`
317
+ ```
318
+
319
+ A table takes its columns and rows as data only — `columns` and `items`, or an `engine` — and draws
320
+ its headers (`th[so-table-column]`) and rows (`tr[so-table-row]`) itself: sorting, row selection
321
+ with "select all", column resizing and reordering, a grid mode and a sticky head. The element you
322
+ write it on, `<div so-table>`, is the table's scroll viewport, with the `<table>` inside it, and it
323
+ splits your markup like a field: `class`, `style` and hiding stay on the viewport, while everything
324
+ else you write on it (`aria-label`, `id`, `data-*`, bindings and listeners) goes to the `<table>`,
325
+ which it names, next to the classes and ARIA of the table. A header and a cell are filled through
326
+ templates — a template given to the table renders in every header or cell — and an empty table
327
+ through the `empty` slot:
328
+
329
+ ```ts
330
+ template: `<div so-table [items]="rows" [columns]="columns" mode="multiple" aria-label="Staff" (soColumnResize)="save($event)">
331
+ <ng-template slot="cell" let-column="column" let-value="value">…</ng-template>
332
+ <p slot="empty">No data</p>
333
+ </div>`
334
+ ```
335
+
336
+ Without a `cell` template a value is rendered as the cell's text from the core (`cell.text`) — an
337
+ object as JSON; the `soText` pipe (`TextPipe`) gives your own template the same text. A long list renders only
338
+ what is visible inside `<ng-container so-virtual>` — the same with a table; the window renders no
339
+ element of its own, so the layout around the list stays as it is, and `enabled` turns the window
340
+ off:
341
+
342
+ ```ts
343
+ template: `<ng-container so-virtual><div so-list-box [items]="cities"></div></ng-container>`
344
+ ```
345
+
346
+ A panel over the page is a Frame. The element you write is the panel, and it is that element which
347
+ moves into `target` (`body` by default) once the page has rendered in the browser — with your
348
+ `class`, attributes and event listeners on it. Its content keeps the context of the place it is
349
+ written in, such as the language of the surrounding `so-locale-provider`. Like a CDK overlay, the
350
+ panel exists only in the browser: on the server, and while `target` matches nothing, there is no
351
+ panel — the element you wrote stays in place, empty and hidden. `contained` keeps the panel in
352
+ place, positioned in its nearest positioned ancestor.
353
+
354
+ ```ts
355
+ template: `<div so-frame [visible]="open()" [x]="10" [y]="20" class="app-menu">…</div>`
356
+ ```
357
+
358
+ Angular inserts and moves nodes relative to the ones it rendered, so a Frame must not be the first
359
+ node of an `@for` item that gets items inserted before it or reordered — Angular fails with
360
+ NG05106. Wrap it in an element: `@for (item of items; track item.id) { <section><div so-frame>…`.
361
+ The panel follows its place. When Angular takes the place off the page while the component lives
362
+ on — a Frame inside a conditional `<ng-content>` of another component, or in a detached view — the
363
+ panel leaves the page with it, and when the place comes back, the panel is back in `target`.
364
+
365
+ A popover and a tooltip are written on your element, `<span so-popover>` and `<span so-tooltip>`,
366
+ which wraps the trigger and anchors their panel — a Frame of their own, moved into `body`. The
367
+ trigger is a template, `<ng-template slot="trigger">`: its context carries what links the trigger
368
+ to the panel — `triggerAria` (`aria-expanded`, `aria-controls` or the tooltip's `aria-describedby`)
369
+ and, for a popover, `triggerDataset` with the pressed look — and you put it on your own element
370
+ with the `[so-attributes]` directive. Import `SlotDirective` and `AttributesDirective` along with
371
+ the component. The trigger stays the same node as the panel opens and closes, so focus stays on
372
+ it. The content of a popover is its panel's content, next to a close button (`closable`, the
373
+ `close-icon` template); the content of a tooltip is its text. A popover takes its content as is
374
+ or as a template, `<ng-template slot="default">`, and a given template is drawn instead. Write it
375
+ as a template when it should not be created right away: the popover creates the template itself,
376
+ and with `lazyMount` its components come with the first open of the panel. Content written as is
377
+ Angular creates right away, with the template that wrote it, and `lazyMount` only keeps it out of
378
+ the document until the first open. `rendered` set to `false` hides the panel along with your
379
+ element. Like a Frame, the panel exists only in the browser: on the server it is an empty hidden
380
+ element inside yours, and the trigger already links to it.
381
+
382
+ ```ts
383
+ template: `<span so-popover aria_label="Filters" lazyMount (soChangeOpen)="opened.set($event.newValue)">
384
+ <ng-template slot="trigger" let-triggerAria="triggerAria" let-triggerDataset="triggerDataset">
385
+ <button so-button text="Filters" [so-attributes]="{ ...triggerAria, ...triggerDataset }"></button>
386
+ </ng-template>
387
+ <ng-template slot="default">…</ng-template>
388
+ </span>
389
+ <span so-tooltip>
390
+ <ng-template slot="trigger" let-triggerAria="triggerAria">
391
+ <button so-button text="Save" [so-attributes]="triggerAria"></button>
392
+ </ng-template>
393
+ Save a draft
394
+ </span>`
395
+ ```
396
+
397
+ A menu is written on your element too, `<span so-menu>`, with its trigger template as in a popover
398
+ — a button, or with `openOn="contextmenu"` the area it is called over by a right click, a long
399
+ touch or Shift+F10, and then the panel opens at that point. The items are parts on your own
400
+ elements: `<div so-menu-item>` (an action — `(soChoose)` on the menu tells which one was chosen),
401
+ `<div so-menu-checkbox-item>`, `<div so-menu-radio-item>` inside `<div so-menu-radio-group>`
402
+ (the group's `[value]` and `(soChangeValue)`), `<div so-menu-group label="…">`,
403
+ `<div so-menu-separator>`, `<div so-menu-submenu>` with items of its own and
404
+ `<div so-menu-popover-item>` with any content in its own side panel — as in a popover, a
405
+ `<ng-template slot="default">` with `lazyMount` is created with the first open of the panel, and
406
+ the row text is the `header` template. A checkbox or a radio item is
407
+ checked with `[selected]` and `(soChangeSelected)`. The items of a menu and of a submenu are written
408
+ as is or as a template, `<ng-template slot="default">`, and a given template is drawn instead.
409
+ Write them as a template when they should not be created right away — say, a menu in every row of a
410
+ table: the menu creates the template itself, with the first open of its panel, and the items stay
411
+ until the menu is removed. Items written as is Angular creates with the menu, and the menu only
412
+ keeps them out of the document until the first open. Without items in the markup, the menu draws them
413
+ from `[items]`: an item with nested `items` is a submenu, groups come from `_.group` or `groupBy`,
414
+ and the item slots are templates — `item`, `item-leading`, `item-trailing`, `item-header` and
415
+ `item-arrow-icon`, with the item in the context; a submenu from data gets them under the same names.
416
+ The `item-content` template is the panel of a submenu from data: write menu items in it, and they
417
+ belong to the submenu it is drawn in, not to the menu you wrote it in. Given, it replaces the items
418
+ from data in every submenu from data, even where it draws nothing. A group, a radio group or a
419
+ submenu written inside the template keeps its own items.
420
+
421
+ ```ts
422
+ template: `<span so-menu (soChoose)="run($event.value)">
423
+ <ng-template slot="trigger" let-triggerAria="triggerAria" let-triggerDataset="triggerDataset">
424
+ <button so-button text="Actions" [so-attributes]="{ ...triggerAria, ...triggerDataset }"></button>
425
+ </ng-template>
426
+ <div so-menu-item value="edit" text="Edit"><span slot="trailing">Ctrl+E</span></div>
427
+ <div so-menu-submenu value="share" text="Share">
428
+ <ng-template slot="default">
429
+ <div so-menu-item value="link" text="Link"></div>
430
+ <div so-menu-item value="mail" text="Mail"></div>
431
+ </ng-template>
432
+ </div>
433
+ <div so-menu-popover-item value="due" text="Due date" lazyMount>
434
+ <ng-template slot="default"><div so-calendar></div></ng-template>
435
+ </div>
436
+ <div so-menu-separator></div>
437
+ <div so-menu-item value="remove" text="Delete"></div>
438
+ </span>`
439
+ ```
440
+
441
+ A modal window and a sliding panel are written on your element, `<div so-dialog>` and
442
+ `<div so-drawer>`, and that element is the panel: it moves into `target` (`body` by default) with
443
+ your `class`, attributes and listeners, while a drawer with `contained` stays in place, positioned
444
+ in its nearest positioned ancestor. The backdrop is placed right before the panel by the component
445
+ itself — in the target and in place — and hides with it. Whether the panel is open is `visible`:
446
+ bind it and listen to `(soChangeVisible)`. The close button, Escape, a press on the backdrop and a
447
+ drawer's swipe ask the panel to close, and `(soCloseBefore)` can refuse with
448
+ `$event.preventDefault()`. The title and the footer are slots by the `slot` attribute
449
+ (`<span slot="title">`, `<button so-button slot="footer">`), the rest of the content is the body;
450
+ the icons of the header buttons are templates — `close-icon`, and for a dialog with `maximizable`
451
+ also `maximize-icon` and `restore-icon`. Like a Frame, the panel exists only in the browser: on the
452
+ server it is your element, empty and hidden, and inside `@for` it needs a wrapping element.
453
+
454
+ ```ts
455
+ template: `<button so-button text="Settings" (click)="open.set(true)"></button>
456
+ <div so-dialog [visible]="open()" (soChangeVisible)="open.set($event.newValue)">
457
+ <span slot="title">Settings</span>
458
+ …
459
+ <button so-button slot="footer" text="Done" (click)="open.set(false)"></button>
460
+ </div>`
461
+ ```
462
+
463
+ Toasts are shown from code. `<div so-toasts>` wraps the application and has no box of its own: the
464
+ application inside it stays in place, and the region with the toasts moves to `target` (`body` by
465
+ default) in the browser — with everything you wrote on the `div`: `class`, `style`, attributes,
466
+ `[class.x]`, `[style.x]`, `[attr.x]` and listeners. A component under the manager takes the queue
467
+ with `useToasts()` in a field or its constructor, and `show` takes a text or options and returns the
468
+ toast — the core instance you can change or close. Content of your own (`render`) is a component
469
+ class with a `toast` input. The `item`, `item-icon`, `item-content` and `item-close-icon` slots are
470
+ templates with the toast in their context. Like the other layers, the region exists only in the
471
+ browser; the application inside the manager renders on the server and hydrates as usual.
472
+
473
+ ```ts
474
+ @Component({
475
+ selector: 'app-undo',
476
+ imports: [TButtonComponent],
477
+ template: `<button so-button text="Undo" (click)="toast().close()"></button>`,
478
+ })
479
+ export class Undo {
480
+ readonly toast = input.required<IToastsItem>()
481
+ }
482
+
483
+ @Component({
484
+ selector: 'app-save',
485
+ imports: [TButtonComponent],
486
+ template: `<button so-button text="Save" (click)="save()"></button>`,
487
+ })
488
+ export class Save {
489
+ private readonly toasts = useToasts()
490
+
491
+ save(): void {
492
+ this.toasts.show({ text: 'Draft saved', type: 'success', render: Undo })
493
+ }
494
+ }
495
+
496
+ // In the application template:
497
+ // <div so-toasts placement="bottom-end"><router-outlet /></div>
498
+ ```
499
+
500
+ The language and the strings the library renders itself come from the nearest
501
+ `LocaleProviderDirective` above — it can sit on an `ng-container`, and changing `locale` updates
502
+ the components without re-creating them:
503
+
504
+ ```ts
505
+ template: `<ng-container so-locale-provider [locale]="locale()"><router-outlet /></ng-container>`
506
+ ```
507
+
508
+ The application plugs in a theme and an icon pack once, in its entry point:
509
+
510
+ ```ts
511
+ import { useIcons, useTheme } from '@soxa-ui/setup'
512
+ import * as material from '@soxa-ui/icons-material'
513
+ import oren from '@soxa-ui/theme-oren/setup'
514
+
515
+ useIcons(material)
516
+ useTheme(oren)
517
+ ```
518
+
519
+ The theme's stylesheet goes into the global styles, `src/styles.css` — the file listed in `styles`
520
+ of `angular.json`:
521
+
522
+ ```css
523
+ @import '@soxa-ui/theme-oren';
524
+ ```
525
+
526
+ Not an `import` in `main.ts`: Angular CLI puts CSS imported from a module into a chunk the page
527
+ never loads, and the components stay unstyled. The values of the theme, such as
528
+ `variant="accent"`, reach the template type check with `@soxa-ui/theme-oren/setup`.
529
+
530
+ The `initial` budget of Angular CLI counts the raw bytes of the initial JavaScript and the global
531
+ styles, and a project made by `ng new` fails the build above 1 MB. `@soxa-ui/theme-oren` holds the
532
+ CSS of every component — 361 kB raw, 36 kB gzip — and a page with a Button, an Input, a Select, a
533
+ Dialog, a Table and a DatePicker adds 677 kB of JavaScript (169 kB gzip): over the threshold. Import
534
+ the stylesheets of the components your pages use instead — each one with the shared part of the
535
+ theme:
536
+
537
+ ```css
538
+ @import '@soxa-ui/theme-oren/button.css';
539
+ @import '@soxa-ui/theme-oren/dialog.css';
540
+ @import '@soxa-ui/theme-oren/input.css';
541
+ @import '@soxa-ui/theme-oren/select.css';
542
+ @import '@soxa-ui/theme-oren/table.css';
543
+ @import '@soxa-ui/theme-oren/date-picker.css';
544
+ ```
545
+
546
+ The same page then loads 205 kB of CSS, 882 kB in all, within the budget (the theme's README,
547
+ [Only the components you use](https://github.com/youra-h/soxa-ui/tree/main/packages/themes/oren#only-the-components-you-use)).
548
+
549
+ ## Documentation
550
+
551
+ - [Soxa UI README](https://github.com/youra-h/soxa-ui#readme) — the component set and what each
552
+ adapter implements so far.
553
+ - [Architecture overview](https://github.com/youra-h/soxa-ui/blob/main/docs/architecture.md) —
554
+ layers, descriptors, plugins, collections, adapters.
555
+
556
+ ## Public API
557
+
558
+ The package entry exports application components, their consumer types, the locale provider,
559
+ and the supported icon and toast helpers. Component props, slots, events, refs and core models
560
+ passed through `ctrl` remain the supported ways to customize the UI. Models come from
561
+ `@soxa-ui/core`; theme, icon and external-plugin registration comes from `@soxa-ui/setup`.
562
+
563
+ Assembly functions, adapter profiles, bindings, generated metadata and rendering templates
564
+ are private implementation details. They are no longer exported from the package entry, and
565
+ there is no public `/internal` entry. Code that imported them must use the component contract
566
+ instead. Building a new adapter component from Soxa's assembly helpers is not a supported
567
+ public SDK.
568
+
569
+ `SlotDirective`, `AttributesDirective`, `RoleIconDirective`, `TextPipe`, `LocaleProviderDirective` and `provideAttrs` are application
570
+ API. The `ɵSubtreeDirective` and `ɵValueAccessorDirective` exports are required by Angular AOT
571
+ and are private; applications must not import them.
572
+
573
+ ## License
574
+
575
+ [MIT](https://github.com/youra-h/soxa-ui/blob/main/LICENSE)
576
+
577
+ ## Two-way bindings
578
+
579
+ Writable inputs with change triggers expose a typed `<inputName>Change` output without `so`:
580
+
581
+ ```html
582
+ <div so-input [(value)]="text"></div>
583
+ <label so-check-box [(value)]="checked"></label>
584
+ <div so-list-box [(value)]="selection"></div>
585
+ ```
586
+
587
+ Model outputs emit the current property value. Ordinary events keep their `so` prefix and event
588
+ payload: `(valueChange)="text = $event"` receives the value, while `(soChangeValue)` receives
589
+ `{ oldValue, newValue }`. Namespaced plugin inputs follow the same rule, for example
590
+ `[(aria_label)]="label"`. Read-only properties have no model output.
591
+
592
+ ## Angular Forms
593
+
594
+ All components with a writable `value` input support Angular Forms automatically. Install
595
+ `@angular/forms` at the same version as `@angular/core`. Import `FormsModule` for `ngModel`,
596
+ or `ReactiveFormsModule` for `formControl` and `formControlName`, alongside the Soxa components:
597
+
598
+ ```html
599
+ <div so-input [(ngModel)]="name"></div>
600
+ <label so-check-box [formControl]="accepted"></label>
601
+ <form [formGroup]="profile">
602
+ <div so-input formControlName="name"></div>
603
+ </form>
604
+ ```
605
+
606
+ `writeValue` updates the component without echoing a form change. User changes update the
607
+ form, form disabled state reaches the component, and leaving the control marks it touched.
608
+ Focus moves between nodes inside the control do not mark it touched. Angular's
609
+ `updateOn: 'blur'` and `updateOn: 'submit'` keep their usual behavior.
610
+
611
+ Angular's `null` reset value clears the component to the descriptor default (for example,
612
+ undefined for Input, false for CheckBox, or zero for Slider); the form retains `null`.
613
+ A non-nullable FormControl resets to its own initial value. Use one form binding per control;
614
+ `[(value)]` remains available for components managed without Angular Forms.