@rivinia/modal 0.4.0 → 0.5.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/README.md CHANGED
@@ -8,7 +8,7 @@ Headless modal manager for Angular 22+. Stack/Queue strategies, typed
8
8
  zero styles, zero router dependency. A future `@rivinia/modal/ui` entry point
9
9
  will render this state; Core never imports UI.
10
10
 
11
- - Version: **0.4.0**
11
+ - Version: **0.5.0**
12
12
  - Requires: **Angular `^22.0.0`**, `tslib`
13
13
 
14
14
  ## Install
@@ -86,6 +86,108 @@ Closing a parent cascades to descendants (`dismissed: parent-closed`) unless
86
86
  `descendants: 'prevent-parent-close'` blocks it. Swapping strategies with live
87
87
  modals throws — close everything first.
88
88
 
89
+ ## Ready-made UI (`@rivinia/modal/ui`)
90
+
91
+ Optional secondary entry point. Zero new runtime dependencies (platform
92
+ `<dialog>` + Angular only), no design system, no CSS framework. Headless
93
+ consumers never load it (proven: core FESM contains zero UI markers).
94
+
95
+ ```ts
96
+ import { RiviniaModalDialog } from '@rivinia/modal/ui'; // UI-only
97
+ import { RiviniaModalHost } from '@rivinia/modal/ui'; // managed adapter
98
+ ```
99
+
100
+ **Mode A — UI-only** (no controller; owns its `open` state):
101
+
102
+ ```html
103
+ <rv-modal-dialog [(open)]="open" [title]="'Delete?'" [size]="'sm'" (confirmed)="remove()">
104
+ <p>Delete {{ user.name }}?</p>
105
+ </rv-modal-dialog>
106
+ ```
107
+
108
+ **Mode B — headless + custom UI:** keep using `@rivinia/modal` alone.
109
+ Nothing here loads, no styles leak.
110
+
111
+ **Mode C — headless + ready-made UI** (single source of truth stays Core):
112
+
113
+ ```html
114
+ <rv-modal-host [modal]="injectRiviniaModal()" />
115
+ ```
116
+
117
+ Intents (X, Escape, backdrop) route through `requestDismiss` (guards run);
118
+ exits play, then the host calls `confirmClosed()` (match `closingTimeout`
119
+ to the animation duration; default 300 ms covers the 150 ms default).
120
+
121
+ Custom chrome (no X button, custom header/footer, own actions in content):
122
+
123
+ ```html
124
+ <rv-modal-dialog [(open)]="open">
125
+ <div rv-modal-header rvModalHeader>My header, no X</div>
126
+ <p>Body.</p>
127
+ <div rv-modal-footer rvModalFooter><button (click)="save()">Save</button></div>
128
+ </rv-modal-dialog>
129
+ ```
130
+
131
+ No header at all: omit the title (no custom header either). Header rules:
132
+ title + optional `description` (wired as `aria-describedby`), top-aligned
133
+ with a shrink-proof X; an empty heading collapses (no dead space); a custom
134
+ header renders inside the same padded row. Focus goes to the title when
135
+ there is no close button, to the dialog otherwise. No footer:
136
+ omit the footer slot (renders nothing). Positions: `center` (default),
137
+ `top/bottom`, `-start/-end` variants (RTL-aware), `side-start/side-end`
138
+ full-height panels, `fullscreen`. Under 640px viewports, center/top/bottom
139
+ shapes become bottom sheets automatically (top-only radius, slide-up
140
+ entrance); panels and fullscreen keep their shape.
141
+ Sizes `sm/md/lg/xl/full/auto` are ceilings via `--rv-modal-width-*` vars.
142
+
143
+ Theming (no theme service; consumer owns it):
144
+
145
+ ```css
146
+ :root {
147
+ --rv-modal-bg: #1c1c1e;
148
+ --rv-modal-fg: #f5f5f7;
149
+ }
150
+ @media (prefers-color-scheme: dark) {
151
+ .rv-modal {
152
+ /* built-in dark defaults already apply; override vars to brand it */
153
+ }
154
+ }
155
+ ```
156
+
157
+ Plus: `prefers-reduced-motion` disables animation; `animation: 'none'`
158
+ opt-out; `backdrop: 'close' | 'static' | 'disabled'`; Escape/backdrop/X
159
+ policies stay independent (no `disableClose` bundle); long content scrolls
160
+ inside (`rv-modal-body`); `scrollbar-gutter: stable` while open;
161
+ reference-counted scroll lock restores previous styles; focus trap + focus
162
+ restore + `aria-labelledby`/`aria-label`; `dir=rtl` works via logical
163
+ properties; SSR renders closed and opens after hydration; lazy-load the
164
+ entry (`loadComponent`) to split it into its own chunk (~16.7 kB raw,
165
+ ~4.9 kB transfer, measured).
166
+
167
+ ## Toasts above an open modal (measured pattern)
168
+
169
+ A toast host outside the dialog can never paint above it — neither a high
170
+ `z-index` nor `popover="manual"` escapes the modal top layer (verified in
171
+ Chrome via `elementFromPoint`). When an error toast must appear over an open
172
+ modal (e.g. a data error inside the form), host the list **inside** the
173
+ dialog content instead: as a descendant of the top-layer element it paints
174
+ above the dialog background with zero extra code and zero coupling:
175
+
176
+ ```html
177
+ <rv-modal-dialog [(open)]="open" [title]="'Edit user'">
178
+ <!-- paints above this dialog; positions are still viewport-anchored -->
179
+ <rv-toast-list [manager]="injectRiviniaToast()" [position]="'top-end'" />
180
+ <form>…</form>
181
+ </rv-modal-dialog>
182
+ ```
183
+
184
+ Notes: positions stay viewport-anchored (`fixed`), so `top-end` means the
185
+ viewport corner in both LTR and RTL (logical properties). Toasts fired
186
+ while no dialog is open still need a page-level list — the two lists share
187
+ the same manager, so nothing is lost or duplicated. Toasts: MEASURED (Chrome, `elementFromPoint`) —
188
+ neither a high `z-index` nor `popover="manual"` paints above an open modal
189
+ dialog; toasts queue behind it. No modal→toast dependency by design.
190
+
89
191
  ## Type holes (honest, by design)
90
192
 
91
193
  Core checks `ModalHandle<TResult>.close(result?: TResult)` and
@@ -99,6 +201,14 @@ Core checks `ModalHandle<TResult>.close(result?: TResult)` and
99
201
  - The internal deferred-promise cast to `Promise<ModalResult<TResult>>` —
100
202
  `close()` settles whatever it receives, never validated.
101
203
 
204
+ ## Browser back button (non-goal)
205
+
206
+ Native `<dialog>.showModal()` does not integrate with history: the back
207
+ button neither closes the dialog nor is blocked by it (platform behavior,
208
+ not a Rivinia choice). If your app needs back-to-dismiss, wire it at the
209
+ app layer (router guard / `popstate` → `dismiss()`); the manager's
210
+ `dismissRequested` + guards flow stays the single teardown path.
211
+
102
212
  ## License
103
213
 
104
214
  MIT.