@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 +111 -1
- package/fesm2022/rivinia-modal-ui.mjs +1003 -0
- package/fesm2022/rivinia-modal-ui.mjs.map +1 -0
- package/package.json +6 -1
- package/types/rivinia-modal-ui.d.ts +332 -0
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.
|
|
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.
|