@rivinia/modal 0.3.0 → 0.5.0

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.3.0**
11
+ - Version: **0.5.0**
12
12
  - Requires: **Angular `^22.0.0`**, `tslib`
13
13
 
14
14
  ## Install
@@ -47,14 +47,14 @@ export class Users {
47
47
 
48
48
  ## API
49
49
 
50
- | Export | Kind | Purpose |
51
- |---|---|---|
52
- | `provideRiviniaModal(config?)` | provider | `{ strategy?, dismiss?, maxVisible?, maxQueue?, overflow?, descendants?, presets?, closingTimeout?, diagnostics? }` |
53
- | `RiviniaModal` | `@Service()` | `openModal/close/dismiss/cancel/requestDismiss/update/confirmClosed/dismissAll/findByRef/useStrategy/debugState` + `active/queued/top` signals |
54
- | `injectRiviniaModal()` | function | shorthand for `inject(RiviniaModal)` |
55
- | `RIVINIA_MODAL_CONTEXT` | token | per-instance context for dynamic components |
56
- | `StackStrategy` / `QueueStrategy` | classes | built-in ordering policies (`ModalStrategy` for custom) |
57
- | types | types | `ModalHandle`, `ModalResult`, `ModalContent`, `ModalOpenOptions`, `ModalDismissPolicy`, `ModalGuard`, … |
50
+ | Export | Kind | Purpose |
51
+ | --------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `provideRiviniaModal(config?)` | provider | `{ strategy?, dismiss?, maxVisible?, maxQueue?, overflow?, descendants?, presets?, closingTimeout?, diagnostics? }` |
53
+ | `RiviniaModal` | `@Service()` | `openModal/close/dismiss/cancel/requestDismiss/update/confirmClosed/dismissAll/findByRef/useStrategy/debugState` + `active/queued/closing/visible/top` signals |
54
+ | `injectRiviniaModal()` | function | shorthand for `inject(RiviniaModal)` |
55
+ | `RIVINIA_MODAL_CONTEXT` | token | per-instance context for dynamic components |
56
+ | `StackStrategy` / `QueueStrategy` | classes | built-in ordering policies (`ModalStrategy` for custom) |
57
+ | types | types | `ModalHandle`, `ModalResult`, `ModalContent`, `ModalOpenOptions`, `ModalDismissPolicy`, `ModalGuard`, … |
58
58
 
59
59
  Results settle exactly once, at dismissal initiation — exit animations never
60
60
  delay callers. `close/dismiss/cancel` are guard-free primitives (booleans);
@@ -86,6 +86,99 @@ 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). Toasts: MEASURED (Chrome, `elementFromPoint`) —
166
+ neither a high `z-index` nor `popover="manual"` paints above an open modal
167
+ dialog; toasts queue behind it. No modal→toast dependency by design.
168
+
169
+ ## Type holes (honest, by design)
170
+
171
+ Core checks `ModalHandle<TResult>.close(result?: TResult)` and
172
+ `ModalResult`/`ModalContext.data` typing (see `modal-type-safety.spec.ts`
173
+ `@ts-expect-error` tests). It does NOT check:
174
+
175
+ - `ModalContext.close(result?: unknown)` — any payload compiles; the mismatch
176
+ reaches the opener's awaited `ModalResult` unchecked.
177
+ - `inputs?: Record<string, unknown>` (options + component content) — any shape
178
+ compiles; misspelled keys fail silently at render. The UI owns binding.
179
+ - The internal deferred-promise cast to `Promise<ModalResult<TResult>>` —
180
+ `close()` settles whatever it receives, never validated.
181
+
89
182
  ## License
90
183
 
91
184
  MIT.