@rivinia/modal 0.6.1 → 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 1.0.1
4
+
5
+ - Documentation: README restructured into docs/modal/ pages; stale pre-1.0 wording removed.
6
+
7
+ ## 1.0.0
8
+
9
+ - First stable release of the modal lifecycle and native-dialog UI. Destroyed dialogs now release lifecycle resources safely.
10
+
11
+ ## Unreleased
12
+
13
+ - Release scroll locks and exit timers when a modal dialog is destroyed while open or closing.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rivinia contributors
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,595 +1,62 @@
1
1
  # @rivinia/modal
2
2
 
3
- An Angular modal manager with typed results, stack/queue admission, dismissal
4
- policies, async guards, and optional ready-made UI. The core package owns
5
- modal state and lifecycle but does not render or style dialogs.
3
+ [![npm version](https://img.shields.io/npm/v/@rivinia/modal.svg)](https://www.npmjs.com/package/@rivinia/modal)
4
+ [![license](https://img.shields.io/npm/l/@rivinia/modal.svg)](https://www.npmjs.com/package/@rivinia/modal)
6
5
 
7
- - Install: `npm install @rivinia/modal`
8
- - Package version: **0.6.0**
9
- - Peer dependencies: `@angular/core ^22.0.0` and `@angular/common ^22.0.0`
10
- - `@rivinia/modal` is the headless manager.
11
- - `@rivinia/modal/ui` contains standalone dialog and host components.
6
+ An Angular modal manager with typed results, lifecycle policies, and optional ready-made UI.
12
7
 
13
- ## Configure the manager
14
-
15
- Register the provider once in the application's root environment providers.
16
- The defaults use a stack with up to five visible modals and queue overflow:
17
-
18
- ```ts
19
- // app.config.ts
20
- import type { ApplicationConfig } from '@angular/core';
21
- import { provideRiviniaModal } from '@rivinia/modal';
22
-
23
- export const appConfig: ApplicationConfig = {
24
- providers: [
25
- provideRiviniaModal({
26
- strategy: 'stack',
27
- maxVisible: 3,
28
- closingTimeout: 300,
29
- }),
30
- ],
31
- };
32
- ```
33
-
34
- `ng add @rivinia/modal` can insert `provideRiviniaModal()` into a detected
35
- standalone app configuration. It does not add a rendering host; add one as
36
- described below. You can also configure the provider manually as above.
37
-
38
- ## Open a typed modal
39
-
40
- The examples use standalone components and separate `.ts`/`.html` files. Mount
41
- one shared `RiviniaModalHost` for the manager. This first version uses the
42
- classic `@ViewChild` query, which is still supported in Angular 22:
8
+ Package version: **1.0.1**
43
9
 
44
10
  ```ts
45
- import { Component, TemplateRef, ViewChild, signal } from '@angular/core';
46
- import type { ModalHandle, ModalTemplateContext } from '@rivinia/modal';
47
- import { injectRiviniaModal } from '@rivinia/modal';
48
- import { RiviniaModalHost } from '@rivinia/modal/ui';
49
-
50
- interface User {
51
- readonly id: string;
52
- readonly name: string;
53
- }
54
-
55
- interface DeleteData {
56
- readonly id: string;
57
- readonly name: string;
58
- }
59
-
60
- @Component({
61
- imports: [RiviniaModalHost],
62
- templateUrl: './users-page.component.html',
63
- })
64
- export class UsersPage {
65
- private readonly manager = injectRiviniaModal();
66
- protected readonly modals = this.manager;
67
- readonly users = signal<User[]>([{ id: '42', name: 'Ada Lovelace' }]);
68
-
69
- @ViewChild('deleteConfirmation', { static: true })
70
- private deleteTemplate!: TemplateRef<ModalTemplateContext<DeleteData>>;
71
-
72
- private pendingDelete: ModalHandle<boolean> | null = null;
73
-
74
- async askToDelete(user: User): Promise<void> {
75
- if (this.pendingDelete) return;
76
-
77
- const handle = this.manager.openModal<DeleteData, boolean>(this.deleteTemplate, {
78
- data: { id: user.id, name: user.name },
79
- ref: 'confirm-delete',
80
- metadata: { title: 'Delete user' },
81
- dismiss: { backdrop: 'static' },
82
- });
83
- this.pendingDelete = handle;
84
-
85
- try {
86
- const result = await handle.result;
87
- if (result.status === 'confirmed' && result.value) {
88
- this.users.update((users) => users.filter((entry) => entry.id !== user.id));
89
- }
90
- } finally {
91
- if (this.pendingDelete === handle) this.pendingDelete = null;
92
- }
93
- }
94
-
95
- confirmDelete(): void {
96
- // ModalHandle<boolean> checks that the confirmed value is a boolean.
97
- this.pendingDelete?.close(true);
98
- }
99
-
100
- cancelDelete(): void {
101
- this.pendingDelete?.cancel('user-cancelled');
102
- }
103
- }
104
- ```
105
-
106
- ```html
107
- <!-- src/app/users-page.component.html -->
108
- @for (user of users(); track user.id) {
109
- <button type="button" (click)="askToDelete(user)">Delete {{ user.name }}</button>
110
- }
111
-
112
- <ng-template #deleteConfirmation let-data>
113
- <p>Delete {{ data.name }}?</p>
114
- <button type="button" (click)="confirmDelete()">Delete user</button>
115
- <button type="button" (click)="cancelDelete()">Cancel</button>
116
- </ng-template>
117
-
118
- <rv-modal-host [modal]="modals" size="sm" />
119
- ```
120
-
121
- ### Signal-based view query
122
-
123
- For new code, Angular 22 also supports the `viewChild` signal query. Keep the
124
- same HTML; call the query signal to get its `TemplateRef`:
125
-
126
- ```ts
127
- // src/app/users-page.component.ts
128
- import { Component, TemplateRef, signal, viewChild } from '@angular/core';
129
- import type { ModalHandle, ModalTemplateContext } from '@rivinia/modal';
130
- import { injectRiviniaModal } from '@rivinia/modal';
131
- import { RiviniaModalHost } from '@rivinia/modal/ui';
132
-
133
- interface User {
134
- readonly id: string;
135
- readonly name: string;
136
- }
137
-
138
- interface DeleteData {
139
- readonly id: string;
140
- readonly name: string;
141
- }
142
-
143
- @Component({
144
- imports: [RiviniaModalHost],
145
- templateUrl: './users-page.component.html',
146
- })
147
- export class UsersPage {
148
- private readonly manager = injectRiviniaModal();
149
- protected readonly modals = this.manager;
150
- readonly users = signal<User[]>([{ id: '42', name: 'Ada Lovelace' }]);
151
- private readonly deleteTemplate =
152
- viewChild.required<TemplateRef<ModalTemplateContext<DeleteData>>>('deleteConfirmation');
153
- private pendingDelete: ModalHandle<boolean> | null = null;
154
-
155
- async askToDelete(user: User): Promise<void> {
156
- if (this.pendingDelete) return;
157
-
158
- const handle = this.manager.openModal<DeleteData, boolean>(this.deleteTemplate(), {
159
- data: { id: user.id, name: user.name },
160
- ref: 'confirm-delete',
161
- metadata: { title: 'Delete user' },
162
- dismiss: { backdrop: 'static' },
163
- });
164
- this.pendingDelete = handle;
165
- try {
166
- const result = await handle.result;
167
- if (result.status === 'confirmed' && result.value) {
168
- this.users.update((users) => users.filter((entry) => entry.id !== user.id));
169
- }
170
- } finally {
171
- if (this.pendingDelete === handle) this.pendingDelete = null;
172
- }
173
- }
174
-
175
- confirmDelete(): void {
176
- this.pendingDelete?.close(true);
177
- }
178
-
179
- cancelDelete(): void {
180
- this.pendingDelete?.cancel('user-cancelled');
181
- }
182
- }
183
- ```
184
-
185
- The UI host uses `metadata.title` by default, so the example renders
186
- “Delete user” without custom title logic. Import `RiviniaModalHost` into the
187
- standalone component that renders it. All examples in this guide target
188
- standalone Angular 22 applications.
189
-
190
- `openModal<TData, TResult>()` accepts a `TemplateRef`, a component type, or a
191
- `ModalContent<TData>` descriptor. Template content receives `data` as
192
- `let-data` and the per-modal operations as `let-modal="modal"`. For component
193
- content, pass `inputs` in the options; those keys are `Record<string, unknown>`
194
- and are not checked against the component's inputs at compile time. The
195
- optional `RIVINIA_MODAL_CONTEXT` injection token supplies the same per-modal
196
- context to a dynamically created component.
197
-
198
- ### Component content, modal context, and `NgComponentOutlet`
199
-
200
- When the content is a component type, the host renders it with Angular's
201
- `NgComponentOutlet` and creates an injector scoped to that modal instance.
202
- Inject `RIVINIA_MODAL_CONTEXT` for that instance's `id`, `data`, `close()`, and
203
- `dismiss()`; ordinary app services still resolve from the surrounding
204
- injectors. The host supplies this context, so do not build a separate
205
- `Injector` just to pass modal data or lifecycle operations.
206
-
207
- ```ts
208
- // src/app/delete-confirmation.component.ts
209
- import { Component, inject } from '@angular/core';
210
- import { RIVINIA_MODAL_CONTEXT } from '@rivinia/modal';
211
-
212
- @Component({
213
- templateUrl: './delete-confirmation.component.html',
214
- })
215
- export class DeleteConfirmationComponent {
216
- readonly modal = inject(RIVINIA_MODAL_CONTEXT);
217
-
218
- confirm(): void {
219
- this.modal.close(true);
220
- }
221
-
222
- cancel(): void {
223
- this.modal.dismiss('user-cancelled');
224
- }
225
- }
226
- ```
227
-
228
- ```html
229
- <!-- src/app/delete-confirmation.component.html -->
230
- <h2>Delete this user?</h2>
231
- <button type="button" (click)="confirm()">Delete</button>
232
- <button type="button" (click)="cancel()">Cancel</button>
233
- ```
234
-
235
- Open the component type through the same manager and host used for template
236
- content:
237
-
238
- ```ts
239
- const handle = this.manager.openModal<DeleteConfirmationComponent, DeleteData, boolean>(
240
- DeleteConfirmationComponent,
241
- {
242
- data: { id: user.id, name: user.name },
243
- metadata: { title: 'Delete user' },
244
- },
245
- );
246
- const result = await handle.result;
247
- if (result.status === 'confirmed' && result.value) {
248
- this.removeUser(user.id);
249
- }
250
- ```
251
-
252
- The injection token is typed as `ModalContext<unknown>` because content may
253
- come from different callers. Narrow or validate `modal.data` before reading
254
- application-specific fields. The opener's `ModalHandle<boolean>` remains
255
- typed, so `handle.close()` checks the result type. Component `inputs` can also
256
- be supplied in `openModal` options, but their keys are not type-checked.
257
-
258
- ### Showing a toast inside an open modal
259
-
260
- The toast list is fixed-position UI, not a popover. A native `<dialog>` is in
261
- the browser's top layer, so a toast list rendered outside it can appear behind
262
- the dialog regardless of its `z-index`. When a notification belongs to the
263
- open modal, put its `RiviniaToastList` inside the modal template so it is
264
- rendered inside the dialog:
265
-
266
- ```ts
267
- // src/app/payment-page.component.ts
268
- import { Component, TemplateRef, inject, viewChild } from '@angular/core';
269
- import type { ModalTemplateContext } from '@rivinia/modal';
270
- import { RiviniaModal } from '@rivinia/modal';
271
- import { RiviniaModalHost } from '@rivinia/modal/ui';
272
- import { RiviniaToast } from '@rivinia/toast';
273
- import { RiviniaToastList } from '@rivinia/toast/ui';
274
-
275
- @Component({
276
- imports: [RiviniaModalHost, RiviniaToastList],
277
- templateUrl: './payment-page.component.html',
278
- })
279
- export class PaymentPageComponent {
280
- readonly modal = inject(RiviniaModal);
281
- readonly toast = inject(RiviniaToast);
282
- private readonly paymentTemplate =
283
- viewChild.required<TemplateRef<ModalTemplateContext<void>>>('paymentConfirmation');
284
-
285
- openPayment(): void {
286
- this.modal.openModal<void, void>(this.paymentTemplate(), {
287
- data: undefined,
288
- metadata: { title: 'Confirm payment' },
289
- });
290
- }
291
-
292
- showPaymentSaved(): void {
293
- this.toast.show({ message: 'Payment saved', kind: 'success' });
294
- }
295
- }
296
- ```
297
-
298
- ```html
299
- <!-- src/app/payment-page.component.html -->
300
- <button type="button" (click)="openPayment()">Pay invoice</button>
301
-
302
- <ng-template #paymentConfirmation let-modal="modal">
303
- <p>Submit payment for invoice INV-2048?</p>
304
- <button type="button" (click)="showPaymentSaved()">Show saved notice</button>
305
- <button type="button" (click)="modal.close()">Done</button>
306
- <rv-toast-list [manager]="toast" position="top-end" />
307
- </ng-template>
308
-
309
- <rv-modal-host [modal]="modal" size="sm" />
310
- ```
311
-
312
- Register `provideRiviniaToast()` as well as `provideRiviniaModal()` in the
313
- application's root providers. Use one `RiviniaToastList` per toast manager at a
314
- time: if the app already renders that same manager globally, move or gate that
315
- list while this modal is open rather than mounting a second list and showing
316
- every notification twice.
317
-
318
- The result is a discriminated `ModalResult<TResult>`:
319
-
320
- - `confirmed` carries the typed `value`.
321
- - `cancelled` and `dismissed` may carry a `reason`.
322
- - `owner-destroyed` and `queued-cancelled` identify lifecycle cleanup.
323
- - `rejected` identifies a request refused by the configured admission or
324
- overflow policy.
325
-
326
- Narrow on `result.status` before reading `result.value`. Results settle once
327
- when closing starts; callers do not wait for the exit animation. A component
328
- that owns a handle may use `close(value)`, `cancel(reason)`, or
329
- `dismiss(reason)`. `ModalHandle.close()` enforces `TResult`; the `close()`
330
- method on template/component context accepts `unknown`, so prefer the handle
331
- when the result type matters.
332
-
333
- ## Dismissal, guards, and lifecycle
334
-
335
- The default dismissal policy allows Escape and backdrop click. Configure
336
- global defaults, named presets, or per-modal overrides:
337
-
338
- ```ts
339
- provideRiviniaModal({
340
- dismiss: { closeOnEscape: true, backdrop: 'static' },
341
- presets: {
342
- critical: { dismiss: { closeOnEscape: false, backdrop: 'static' } },
343
- },
11
+ const handle = this.manager.openModal<DeleteData, boolean>(this.deleteTemplate, {
12
+ data: { id: user.id, name: user.name },
13
+ ref: 'confirm-delete',
14
+ metadata: { title: 'Delete user' },
15
+ dismiss: { backdrop: 'static' },
344
16
  });
17
+ const result = await handle.result;
18
+ if (result.status === 'confirmed' && result.value) this.removeUser(user.id);
345
19
  ```
346
20
 
347
- Dismissal policy fields merge independently in this order: built-in defaults,
348
- global config, named preset, and per-open options. A missing key at a later
349
- layer does not erase an earlier setting. `backdrop` may be `close`, `static`,
350
- or `disabled`.
351
-
352
- `RiviniaModalHost` routes Escape, backdrop, and close-button intents through
353
- `requestDismiss()`, which runs any async `guards` supplied when opening. A
354
- guard that returns `false` or throws keeps the modal open. The direct
355
- `close()`, `dismiss()`, and `cancel()` methods on a handle are guard-free;
356
- use `requestDismiss(id, origin)` when programmatic code should also be checked.
357
- Forced cleanup such as owner destruction and `dismissAll()` bypasses guards.
358
-
359
- The manager supports `strategy: 'stack' | 'queue'` (or a custom
360
- `ModalStrategy`). Stack allows up to `maxVisible` active modals, with the
361
- latest on top; queue activates one at a time in FIFO order. A modal in its
362
- exit animation normally occupies a visible slot until the host confirms
363
- teardown or the timeout finalizes it; `dismiss-oldest` preempts an active
364
- instance to make room while its exit runs. When capacity is full, `overflow`
365
- may queue (default), dismiss the oldest active modal, or reject the new request.
366
- `maxQueue` bounds queued requests when queue overflow is enabled.
367
-
368
- An optional `owner: DestroyRef` ties a modal to a component/service lifetime.
369
- Destroying the owner settles an active request as `owner-destroyed`; a queued
370
- request settles as `queued-cancelled`. Without an owner, the modal is
371
- independent of the caller's lifetime. Set `parentId` to associate a child
372
- modal with a parent; by default, closing a parent closes its descendants.
373
- `descendants: 'prevent-parent-close'` instead blocks parent closure while a
374
- descendant remains open.
375
-
376
- When a modal enters `closing`, its result has already settled but it remains
377
- visible while the UI exits. The UI host calls `confirmClosed()` after its exit
378
- animation. The default `closingTimeout` is 300 ms; set it to cover the
379
- renderer’s exit duration, or set it to `0` when a custom renderer will always
380
- acknowledge closure manually. Changing strategy with active, queued, or
381
- closing modals throws; close them first.
382
-
383
- ## Ready-made UI
384
-
385
- `@rivinia/modal/ui` is optional. It provides a native `<dialog>` component
386
- (`RiviniaModalDialog`) for UI-only use and `RiviniaModalHost` to render
387
- manager-owned modals. The host passes dismissal intents to the manager before
388
- closing, so guards and promise results remain coordinated.
389
-
390
- For a UI-only dialog, import the standalone component and bind its `open`
391
- model. This mode has no manager, guards, or `ModalHandle` promise:
392
-
393
- ```ts
394
- import { Component } from '@angular/core';
395
- import { RiviniaModalDialog } from '@rivinia/modal/ui';
396
-
397
- @Component({
398
- imports: [RiviniaModalDialog],
399
- templateUrl: './preview-dialog.component.html',
400
- })
401
- export class PreviewDialog {
402
- open = false;
403
- }
404
- ```
405
-
406
- ```html
407
- <!-- src/app/preview-dialog.component.html -->
408
- <button type="button" (click)="open = true">Preview</button>
409
- <rv-modal-dialog #dialog [(open)]="open" title="Preview">
410
- <p>Dialog content.</p>
411
- <button type="button" (click)="dialog.confirm(true)">Continue</button>
412
- <button type="button" (click)="dialog.cancel('cancelled')">Cancel</button>
413
- </rv-modal-dialog>
414
- ```
415
-
416
- `RiviniaModalDialog` accepts `open`, `title`, `description`, `ariaLabel`,
417
- `size`, `position`, `animation`, `closeOnEscape`, `backdrop`, and
418
- `showCloseButton`; its `open` model supports `[(open)]`. In UI-only mode it
419
- emits `confirmed`, `dismissed`, and `cancelled`. In managed use, the host
420
- accepts the required `modal` manager input plus `size`, `showCloseButton`,
421
- and `titleOf` overrides; observe the manager's `ModalHandle.result` instead
422
- of the dialog outputs.
423
- `RvModalHeader` and `RvModalFooter` are marker directives for custom header
424
- and footer projection; import the marker used by the template and apply it
425
- alongside `rv-modal-header` or `rv-modal-footer`. A custom header replaces the
426
- default title and close button. Without a footer slot, no footer region is
427
- rendered.
428
-
429
- The native dialog provides focus containment and restores focus to its
430
- invoker. The UI includes scroll locking, reduced-motion support, RTL-aware
431
- logical positions, and responsive bottom-sheet styling below 640px for
432
- center/top/bottom placements. The UI exposes size, position, animation,
433
- backdrop, Escape, and close-button inputs. During server rendering it remains
434
- closed and opens on the client after hydration. Theme it with CSS custom
435
- properties such as `--rv-modal-bg`, `--rv-modal-fg`, `--rv-modal-border`,
436
- `--rv-modal-radius`, and `--rv-modal-width-md`.
437
-
438
- ## Theming
439
-
440
- Every component reads **its own token → the shared semantic layer → its
441
- built-in default**:
442
-
443
- ```css
444
- --rv-modal-bg → --rv-bg → #ffffff
445
- ```
446
-
447
- | Level | Token | Scope |
448
- | -------------- | --------------- | ----------------------------------- |
449
- | 1. per package | `--rv-modal-bg` | that dialog only |
450
- | 2. global | `--rv-bg` | every Rivinia component, everywhere |
451
- | 3. default | — | what you get if you set nothing |
452
-
453
- ```css
454
- @import '@rivinia/theme';
455
-
456
- :root {
457
- --rv-bg: #ffffff;
458
- --rv-fg: #111111;
459
- --rv-border: #e0e0e0;
460
- }
461
- ```
462
-
463
- Its defaults sit in `@layer rivinia-theme`, so your unlayered `:root` rules win
464
- wherever they appear. **Map it on `:root`, never on a component element** — an
465
- element-level declaration shadows the root value and the override silently
466
- stops working.
467
-
468
- ### The library's own tokens
469
-
470
- | Token | Default | Applies to |
471
- | --------------------- | ----------------------------- | -------------- |
472
- | `--rv-modal-bg` | `#ffffff` | the dialog |
473
- | `--rv-modal-fg` | `#111111` | the dialog |
474
- | `--rv-modal-muted` | `#555555` | secondary text |
475
- | `--rv-modal-border` | `#e0e0e0` | the dialog |
476
- | `--rv-modal-radius` | `12px` | the dialog |
477
- | `--rv-modal-shadow` | `0 24px 64px rgba(0,0,0,.28)` | the dialog |
478
- | `--rv-modal-backdrop` | `rgba(0,0,0,.5)` | the scrim |
479
- | `--rv-modal-pad` | `20px` | the dialog |
480
- | `--rv-modal-width-md` | `32rem` | the dialog |
481
-
482
- **No forced dark mode** — there is no `prefers-color-scheme` block in this
483
- package, so a dialog never overrides your explicit choice.
484
-
485
- ### Which styling stack?
486
-
487
- | Stack | Import |
488
- | --------------- | -------------------------------------------- |
489
- | **Plain CSS** | `@import '@rivinia/theme';` |
490
- | **Tailwind v4** | `@import '@rivinia/theme/tailwind.css';` |
491
- | **Bootstrap** | `@import '@rivinia/theme/bootstrap.css';` |
492
- | **Sass / SCSS** | `@use '@rivinia/theme/scss';` |
493
- | **Less** | `@import (less) '@rivinia/theme/theme.css';` |
494
-
495
- ## Sizing, stacking and who opened it
496
-
497
- **A size per dialog.** The host's `size` is the default for every instance; a
498
- dialog may override it when it is opened:
499
-
500
- ```ts
501
- this.modal.openModal(this.confirmTpl, { ref: 'confirm', size: 'sm' });
502
- ```
503
-
504
- ```html
505
- <rv-modal-host [modal]="modal" size="lg" />
506
- ```
507
-
508
- The presets are **responsive** — each is a `clamp()`, so it scales with the
509
- viewport between a floor and a ceiling instead of being a fixed width that
510
- either overflows or gets capped:
511
-
512
- | Preset | Width |
513
- | ------ | --------------------------- |
514
- | `sm` | `clamp(16rem, 40vw, 24rem)` |
515
- | `md` | `clamp(20rem, 55vw, 32rem)` |
516
- | `lg` | `clamp(24rem, 70vw, 48rem)` |
517
- | `xl` | `clamp(28rem, 85vw, 64rem)` |
518
- | `full` | `calc(100dvw - 2rem)` |
519
- | `auto` | sized to its content |
520
-
521
- Every one is a plain custom property, so the consumer overrides the preset in
522
- CSS without touching the library:
523
-
524
- ```css
525
- my-scope {
526
- --rv-modal-width-lg: clamp(30rem, 80vw, 60rem);
527
- }
528
- ```
529
-
530
- **One dialog can cover the ones below it.** A stack shows every dialog, so the
531
- ones below stay visible behind the top one — right for a wizard, wrong for a
532
- flow where the detail view REPLACES the list view:
21
+ ## Why @rivinia/modal
533
22
 
534
- ```ts
535
- this.modal.openModal(this.detailTpl, { ref: 'detail', coversOthers: true });
536
- ```
23
+ - Modal data and results are typed from the call site to the consumer.
24
+ - Stack or queue admission and async dismissal guards are configurable.
25
+ - Core lifecycle is separate from the optional dialog components.
537
26
 
538
- The dialogs below are not rendered while the cover is up, and they come back
539
- when it closes. They stay in the manager the whole time.
27
+ ## Install
540
28
 
541
- **The opener is attributable.** `openModal` is normally called from an event
542
- handler, which is not an injection context, so the manager cannot discover the
543
- caller. Naming it puts `data-caller` on the dialog, which the DevTools
544
- inspector shows — so a dialog that appears unexpectedly points at its source:
545
-
546
- ```ts
547
- export class UsersScreen {
548
- open() {
549
- this.modal.openModal(this.detailTpl, { caller: UsersScreen });
550
- }
551
- }
29
+ ```bash
30
+ npm install @rivinia/modal
552
31
  ```
553
32
 
554
- ```html
555
- <dialog class="rv-modal" data-caller="UsersScreen">…</dialog>
556
- ```
33
+ ## Quick start
557
34
 
558
- **Smoothness.** The dialog transitions its own box — width, height and the
559
- offsets — so a size change or a dialog opening on top of another is a
560
- movement rather than a jump. The stack change is additionally wrapped in a
561
- **view transition** where the browser supports it, so the lower dialog is
562
- painted continuously instead of flickering. During a viewport resize the
563
- transition is switched off (`data-resizing="true"`): the dialog's size is
564
- derived from the viewport, so animating towards a moving target lags behind
565
- the pointer.
35
+ 1. Register `provideRiviniaModal()` and mount `RiviniaModalHost`.
36
+ 2. Open typed modal content from a component.
37
+ 3. Render the modal host and handle its typed result.
566
38
 
567
- ## Public core exports
39
+ ## Two ways to use it
568
40
 
569
- The core entry point exports:
41
+ | Ready-made UI | Headless manager |
42
+ | --- | --- |
43
+ | `@rivinia/modal/ui` | `@rivinia/modal` |
570
44
 
571
- - `provideRiviniaModal()` and `injectRiviniaModal()` for configuration and
572
- injection, plus `RiviniaModal` for direct manager APIs.
573
- - `StackStrategy`, `QueueStrategy`, and the `ModalStrategy` contract.
574
- - `RiviniaModalConfig`, `ModalPreset`, `ModalHandle`, `ModalResult`,
575
- `ModalContent`, `ModalOpenOptions`, `ModalUpdate`, `ModalInstance`,
576
- `ModalTemplateContext`, `ModalContext`, `ModalGuard`,
577
- `ModalDismissPolicy`, `ModalDismissOrigin`, `ModalOverflowPolicy`,
578
- `ModalStatus`, `ModalDiagnostic`, and `DescendantPolicy`.
579
- - `RIVINIA_MODAL_CONTEXT` for dynamic modal components.
45
+ The UI and controller are independent; use either or combine them.
580
46
 
581
- `RiviniaModal` exposes readonly `active`, `queued`, `closing`, `visible`, and
582
- `top` signals, along with `openModal`, `close`, `dismiss`, `cancel`,
583
- `requestDismiss`, `update`, `confirmClosed`, `dismissAll`, `findByRef`,
584
- `useStrategy`, and diagnostics helpers. Inspect the signal values by calling
585
- them, for example `manager.top()`.
47
+ ## Compatibility
586
48
 
587
- The `@rivinia/modal/ui` entry point exports `RiviniaModalDialog`,
588
- `RiviniaModalHost`, `RiviniaModalHostManager`, `RiviniaModalScrollLock`,
589
- `RvModalHeader`, and `RvModalFooter`, plus the `RiviniaModalAnimation`,
590
- `RiviniaModalBackdrop`, `RiviniaModalPosition`, `RiviniaModalSize`, and
591
- `RiviniaModalUiOptions` types.
49
+ | Angular | Node | Browser verification |
50
+ | --- | --- | --- |
51
+ | `^22.0.0` | `^22.22.3 or ^24.15.0 or >=26.0.0` | Chromium and Firefox; native Safari not verified |
592
52
 
593
- ## License
53
+ ## Go further
594
54
 
595
- MIT.
55
+ | Topic | Read |
56
+ | --- | --- |
57
+ | Typed modals | [Typed modals](https://github.com/abdelfattahqandil21-oss/Revinia/blob/modal/v1.0.1/docs/modal/typed-modals.md) |
58
+ | Lifecycle and guards | [Lifecycle and guards](https://github.com/abdelfattahqandil21-oss/Revinia/blob/modal/v1.0.1/docs/modal/lifecycle-and-guards.md) |
59
+ | Ready-made UI | [Ready-made UI](https://github.com/abdelfattahqandil21-oss/Revinia/blob/modal/v1.0.1/docs/modal/ready-made-ui.md) |
60
+ | Theming | [Theming](https://github.com/abdelfattahqandil21-oss/Revinia/blob/modal/v1.0.1/docs/modal/theming.md) |
61
+ | Sizing and stacking | [Sizing and stacking](https://github.com/abdelfattahqandil21-oss/Revinia/blob/modal/v1.0.1/docs/modal/sizing-and-stacking.md) |
62
+ | API reference | [API reference](https://github.com/abdelfattahqandil21-oss/Revinia/blob/modal/v1.0.1/docs/modal/api.md) |