@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +40 -581
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ - Documentation: README restructured into docs/modal/ pages; stale pre-1.0 wording removed.
6
+
3
7
  ## 1.0.0
4
8
 
5
9
  - First stable release of the modal lifecycle and native-dialog UI. Destroyed dialogs now release lifecycle resources safely.
package/README.md CHANGED
@@ -1,603 +1,62 @@
1
1
  # @rivinia/modal
2
2
 
3
- <p align="center">
4
- <img src="https://raw.githubusercontent.com/abdelfattahqandil21-oss/Revinia/modal/v1.0.0/assets/rivinia-abstract-mark-light.svg" alt="Rivinia" width="340" />
5
- </p>
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
- An Angular modal manager with typed results, stack/queue admission, dismissal
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
- - Install: `npm install @rivinia/modal`
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
- provideRiviniaModal({
344
- dismiss: { closeOnEscape: true, backdrop: 'static' },
345
- presets: {
346
- critical: { dismiss: { closeOnEscape: false, backdrop: 'static' } },
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
- Dismissal policy fields merge independently in this order: built-in defaults,
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
- **The opener is attributable.** `openModal` is normally called from an event
546
- handler, which is not an injection context, so the manager cannot discover the
547
- caller. Naming it puts `data-caller` on the dialog, which the DevTools
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
- ```ts
551
- export class UsersScreen {
552
- open() {
553
- this.modal.openModal(this.detailTpl, { caller: UsersScreen });
554
- }
555
- }
556
- ```
27
+ ## Install
557
28
 
558
- ```html
559
- <dialog class="rv-modal" data-caller="UsersScreen">…</dialog>
29
+ ```bash
30
+ npm install @rivinia/modal
560
31
  ```
561
32
 
562
- **Smoothness.** The dialog transitions its own box — width, height and the
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
- The core entry point exports:
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
- - `provideRiviniaModal()` and `injectRiviniaModal()` for configuration and
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
- `RiviniaModal` exposes readonly `active`, `queued`, `closing`, `visible`, and
586
- `top` signals, along with `openModal`, `close`, `dismiss`, `cancel`,
587
- `requestDismiss`, `update`, `confirmClosed`, `dismissAll`, `findByRef`,
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 `@rivinia/modal/ui` entry point exports `RiviniaModalDialog`,
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
- ## License
47
+ ## Compatibility
598
48
 
599
- MIT.
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
- ## Further reading
53
+ ## Go further
602
54
 
603
- [Package design and usage notes](https://github.com/abdelfattahqandil21-oss/Revinia/blob/modal/v1.0.0/docs/decisions/D029-rivinia-modal-ui-on-native-dialog-no-cdk.md).
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) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/modal",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Headless modal manager for Angular: stack/queue strategies, typed results, guards, ownership.",
5
5
  "keywords": [
6
6
  "angular",