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