@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.
@@ -0,0 +1,332 @@
1
+ import * as i0 from "@angular/core";
2
+ import { Injector, Signal } from "@angular/core";
3
+ import { ModalDismissOrigin, ModalInstance, ModalTemplateContext } from "@rivinia/modal";
4
+ /**
5
+ * Responsive size presets. Dimensions are ceilings, not mandates: every
6
+ * preset is a CSS custom property (`--rv-modal-width-*`) the consumer can
7
+ * override without touching library source. `auto` sizes to content.
8
+ */
9
+ type RiviniaModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'auto';
10
+ /**
11
+ * Placement in the viewport. `start`/`end` are logical (RTL-aware).
12
+ * `fullscreen` covers the viewport (safe-area aware). `side-start` /
13
+ * `side-end` dock a full-height panel to the inline edge. Implemented with
14
+ * plain CSS on the native `<dialog>` — no positioning engine.
15
+ *
16
+ * Responsive sheet mapping (built in, no input needed): viewports narrower
17
+ * than 640px render `center`, `top`, `bottom` and the bottom corners as a
18
+ * bottom sheet (anchored, top-only radius, slide-from-bottom entrance).
19
+ * `fullscreen` and the side panels keep their shape on small screens.
20
+ */
21
+ type RiviniaModalPosition = 'center' | 'top' | 'bottom' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'center-start' | 'center-end' | 'side-start' | 'side-end' | 'fullscreen';
22
+ /** Entrance/exit animation preset. `none` disables motion entirely. */
23
+ type RiviniaModalAnimation = 'fade' | 'scale' | 'slide-top' | 'slide-bottom' | 'slide-start' | 'slide-end' | 'none';
24
+ /** Backdrop click policy (mirrors the headless `ModalDismissPolicy`). */
25
+ type RiviniaModalBackdrop = 'close' | 'static' | 'disabled';
26
+ /**
27
+ * Visual + interaction options for {@link RiviniaModalDialog}. Independent
28
+ * knobs only — no `disableClose` bundle (Escape, backdrop and the close
29
+ * button stay separately configurable, like the headless policy).
30
+ */
31
+ interface RiviniaModalUiOptions {
32
+ /** @default 'md' */
33
+ readonly size?: RiviniaModalSize;
34
+ /** @default 'center' */
35
+ readonly position?: RiviniaModalPosition;
36
+ /** @default 'scale' */
37
+ readonly animation?: RiviniaModalAnimation;
38
+ /** Escape requests dismissal via the configured path. @default true */
39
+ readonly closeOnEscape?: boolean;
40
+ /** Backdrop click behavior. @default 'close' */
41
+ readonly backdrop?: RiviniaModalBackdrop;
42
+ /** Render the header close button. @default true */
43
+ readonly showCloseButton?: boolean;
44
+ }
45
+ /**
46
+ * Marker for custom header content. Put it on the element carrying
47
+ * `rv-modal-header` — its presence (not just the attribute) tells the
48
+ * dialog to hide the default title + close button:
49
+ *
50
+ * ```html
51
+ * <div rv-modal-header rvModalHeader>My header</div>
52
+ * ```
53
+ */
54
+ export declare class RvModalHeader {
55
+ static ɵfac: i0.ɵɵFactoryDeclaration<RvModalHeader, never>;
56
+ static ɵdir: i0.ɵɵDirectiveDeclaration<RvModalHeader, "[rvModalHeader]", never, {}, {}, never, never, true, never>;
57
+ }
58
+ /**
59
+ * Marker for custom footer content. Put it on the element carrying
60
+ * `rv-modal-footer`. Without it the footer region never renders.
61
+ */
62
+ export declare class RvModalFooter {
63
+ static ɵfac: i0.ɵɵFactoryDeclaration<RvModalFooter, never>;
64
+ static ɵdir: i0.ɵɵDirectiveDeclaration<RvModalFooter, "[rvModalFooter]", never, {}, {}, never, never, true, never>;
65
+ }
66
+ /**
67
+ * Ready-made modal dialog on the native `<dialog>` element (top layer,
68
+ * `::backdrop`, inert background, Escape `cancel` event — all platform
69
+ * behavior, zero overlay dependencies).
70
+ *
71
+ * UI-only by design: it owns its own `open` state and never imports the
72
+ * headless controller. Pair it with `@rivinia/modal` through
73
+ * {@link RiviniaModalHost} (or your own adapter) when you want managed
74
+ * lifecycle, guards and typed results.
75
+ *
76
+ * Projection: `[rv-modal-header]` replaces the default header (title +
77
+ * close button); default `<ng-content>` is the body; `[rv-modal-footer]`
78
+ * is optional and renders nothing when absent.
79
+ */
80
+ export declare class RiviniaModalDialog {
81
+ /** Two-way open state. UI-only mode owns this; the host drives it in managed mode. */
82
+ readonly open: import("@angular/core").ModelSignal<boolean>;
83
+ readonly title: import("@angular/core").InputSignal<string>;
84
+ /**
85
+ * Supporting text under the title (shadcn-style description). Rendered in
86
+ * the default header only, and wired as the dialog's `aria-describedby`
87
+ * for free accessible naming. Empty = no description element at all.
88
+ */
89
+ readonly description: import("@angular/core").InputSignal<string>;
90
+ /** Explicit accessible name. When set, `aria-label` wins over the title reference. */
91
+ readonly ariaLabel: import("@angular/core").InputSignal<string>;
92
+ readonly size: import("@angular/core").InputSignal<RiviniaModalSize>;
93
+ readonly position: import("@angular/core").InputSignal<RiviniaModalPosition>;
94
+ readonly animation: import("@angular/core").InputSignal<RiviniaModalAnimation>;
95
+ readonly closeOnEscape: import("@angular/core").InputSignal<boolean>;
96
+ readonly backdrop: import("@angular/core").InputSignal<RiviniaModalBackdrop>;
97
+ readonly showCloseButton: import("@angular/core").InputSignal<boolean>;
98
+ /**
99
+ * Managed mode (for {@link RiviniaModalHost}): UI intents (X button,
100
+ * Escape, backdrop) emit `dismissRequested` INSTEAD of dismissing, so the
101
+ * manager runs guards first and settles results. Drive the exit with the
102
+ * `closing` input; acknowledge teardown with `exitDone`.
103
+ * UI-only mode (`false`, default) dismisses locally.
104
+ */
105
+ readonly managed: import("@angular/core").InputSignal<boolean>;
106
+ /**
107
+ * Managed-mode exit trigger. Set `true` when the manager reports this
108
+ * instance `closing`: the dialog plays its exit animation, then emits
109
+ * `exitDone` (call `confirmClosed()` there). Ignored in UI-only mode.
110
+ */
111
+ readonly closing: import("@angular/core").InputSignal<boolean>;
112
+ /** Settled with a value (primary/confirm actions call `close(value)`). */
113
+ readonly confirmed: import("@angular/core").OutputEmitterRef<unknown>;
114
+ /** Settled without a value (X button, backdrop, Escape, `dismiss()`). */
115
+ readonly dismissed: import("@angular/core").OutputEmitterRef<string | undefined>;
116
+ /** Explicit cancellation (`cancel()`). Kept distinct from dismissal. */
117
+ readonly cancelled: import("@angular/core").OutputEmitterRef<string | undefined>;
118
+ /**
119
+ * Managed-mode intent (X button / Escape / backdrop). Emitted INSTEAD of
120
+ * dismissing — the host routes it to the manager first. Never emitted in
121
+ * UI-only mode.
122
+ */
123
+ readonly dismissRequested: import("@angular/core").OutputEmitterRef<"button" | "escape" | "backdrop">;
124
+ /** Managed-mode exit completion. Acknowledge with `confirmClosed()`. */
125
+ readonly exitDone: import("@angular/core").OutputEmitterRef<void>;
126
+ private readonly dialog;
127
+ private readonly headerSlot;
128
+ private readonly footerSlot;
129
+ private readonly doc;
130
+ private readonly scrollLock;
131
+ private readonly titleId;
132
+ private readonly descriptionId;
133
+ private readonly phase;
134
+ private readonly invoker;
135
+ private exitTimer;
136
+ /** Template state for animation styling. Never rendered open on the server. */
137
+ readonly state: import("@angular/core").Signal<"closed" | "open" | "closing">;
138
+ readonly hasCustomHeader: import("@angular/core").Signal<boolean>;
139
+ readonly hasCustomFooter: import("@angular/core").Signal<boolean>;
140
+ readonly computedLabel: import("@angular/core").Signal<string | null>;
141
+ constructor();
142
+ /** Confirm with a value. No-op while closing/closed. */
143
+ confirm(value?: unknown): boolean;
144
+ /** Backwards-compatible close: confirms when given a value, dismisses otherwise. */
145
+ close(value?: unknown): boolean;
146
+ /** Dismiss without a value (X button, backdrop, Escape route here). */
147
+ dismiss(reason?: string): boolean;
148
+ /** Explicit cancellation. */
149
+ cancel(reason?: string): boolean;
150
+ protected onCloseButton(): void;
151
+ protected onCancel(event: Event): void;
152
+ protected onBackdropClick(event: MouseEvent): void;
153
+ protected onKeydown(event: KeyboardEvent): void;
154
+ private get live();
155
+ private show;
156
+ private hide;
157
+ private finish;
158
+ private teardown;
159
+ /** Plays the exit animation (when warranted), then runs `done`. Idempotent. */
160
+ private playExit;
161
+ private exitDurationMs;
162
+ private finalize;
163
+ private prefersMotion;
164
+ static ɵfac: i0.ɵɵFactoryDeclaration<RiviniaModalDialog, never>;
165
+ static ɵcmp: i0.ɵɵComponentDeclaration<RiviniaModalDialog, "rv-modal-dialog", never, {
166
+ "open": {
167
+ "alias": "open";
168
+ "required": false;
169
+ "isSignal": true;
170
+ };
171
+ "title": {
172
+ "alias": "title";
173
+ "required": false;
174
+ "isSignal": true;
175
+ };
176
+ "description": {
177
+ "alias": "description";
178
+ "required": false;
179
+ "isSignal": true;
180
+ };
181
+ "ariaLabel": {
182
+ "alias": "ariaLabel";
183
+ "required": false;
184
+ "isSignal": true;
185
+ };
186
+ "size": {
187
+ "alias": "size";
188
+ "required": false;
189
+ "isSignal": true;
190
+ };
191
+ "position": {
192
+ "alias": "position";
193
+ "required": false;
194
+ "isSignal": true;
195
+ };
196
+ "animation": {
197
+ "alias": "animation";
198
+ "required": false;
199
+ "isSignal": true;
200
+ };
201
+ "closeOnEscape": {
202
+ "alias": "closeOnEscape";
203
+ "required": false;
204
+ "isSignal": true;
205
+ };
206
+ "backdrop": {
207
+ "alias": "backdrop";
208
+ "required": false;
209
+ "isSignal": true;
210
+ };
211
+ "showCloseButton": {
212
+ "alias": "showCloseButton";
213
+ "required": false;
214
+ "isSignal": true;
215
+ };
216
+ "managed": {
217
+ "alias": "managed";
218
+ "required": false;
219
+ "isSignal": true;
220
+ };
221
+ "closing": {
222
+ "alias": "closing";
223
+ "required": false;
224
+ "isSignal": true;
225
+ };
226
+ }, {
227
+ "open": "openChange";
228
+ "confirmed": "confirmed";
229
+ "dismissed": "dismissed";
230
+ "cancelled": "cancelled";
231
+ "dismissRequested": "dismissRequested";
232
+ "exitDone": "exitDone";
233
+ }, ["headerSlot", "footerSlot"], ["[rv-modal-header]", "*", "[rv-modal-footer]"], true, never>;
234
+ }
235
+ /**
236
+ * Managed adapter: renders every `visible()` instance of the headless
237
+ * `RiviniaModal` inside a {@link RiviniaModalDialog} and routes all UI
238
+ * intents through the manager first (guards run; results settle in Core —
239
+ * the single source of truth). The dialog never settles anything itself in
240
+ * this mode; listen to `ModalHandle.result`, not dialog outputs.
241
+ *
242
+ * Exit coordination: when Core reports an instance `closing`, the dialog
243
+ * plays its exit animation and the host acknowledges with `confirmClosed()`
244
+ * (idempotent — safe if `closingTimeout` auto-finalized first). Match
245
+ * `closingTimeout` to the animation duration so exits stay visible.
246
+ *
247
+ * Title resolution: `titleOf` per instance (default: `metadata.title` →
248
+ * `ref` → `''`). Content descriptors render as-is: templates via
249
+ * `ngTemplateOutlet` with `{$implicit: data, modal}`, components via
250
+ * `NgComponentOutlet` with `inputs` + a per-instance
251
+ * `RIVINIA_MODAL_CONTEXT` injector (the lab-proven pattern).
252
+ *
253
+ * The manager arrives via `[modal]` input (D06 input-composition: the UI
254
+ * entry never instantiates Core itself, so UI-only consumers pay nothing).
255
+ * The input is typed as {@link RiviniaModalHostManager} — the structural
256
+ * slice the host actually drives — so the full manager (or a test double)
257
+ * satisfies it from any module realm:
258
+ *
259
+ * ```html
260
+ * <rv-modal-host [modal]="injectRiviniaModal()" />
261
+ * ```
262
+ */
263
+ interface RiviniaModalHostManager {
264
+ readonly visible: Signal<readonly ModalInstance<unknown>[]>;
265
+ requestDismiss(id: string, origin: ModalDismissOrigin): Promise<boolean>;
266
+ confirmClosed(id: string): boolean;
267
+ close(id: string, value?: unknown): boolean;
268
+ dismiss(id: string, reason?: string): boolean;
269
+ }
270
+ export declare class RiviniaModalHost {
271
+ /**
272
+ * The headless manager to render. Typed as {@link RiviniaModalHostManager}
273
+ * (structural) — pass `injectRiviniaModal()` from your app code.
274
+ */
275
+ readonly modal: import("@angular/core").InputSignal<RiviniaModalHostManager>;
276
+ /** Dialog size for every managed instance. @default 'md' */
277
+ readonly size: import("@angular/core").InputSignal<RiviniaModalSize>;
278
+ /** Render the header close button on every managed instance. @default true */
279
+ readonly showCloseButton: import("@angular/core").InputSignal<boolean>;
280
+ /** Resolve a title per instance. Default: `metadata.title` → `ref` → `''`. */
281
+ readonly titleOf: import("@angular/core").InputSignal<((instance: ModalInstance<unknown>) => string | undefined) | undefined>;
282
+ protected readonly visible: Signal<readonly ModalInstance<unknown>[]>;
283
+ protected titleOfInstance(instance: ModalInstance<unknown>): string;
284
+ protected templateContext(instance: ModalInstance<unknown>): ModalTemplateContext<unknown>;
285
+ protected contextInjector(instance: ModalInstance<unknown>): Injector;
286
+ protected onIntent(id: string, origin: 'button' | 'escape' | 'backdrop'): Promise<void>;
287
+ protected onExitDone(id: string): void;
288
+ private modalContext;
289
+ static ɵfac: i0.ɵɵFactoryDeclaration<RiviniaModalHost, never>;
290
+ static ɵcmp: i0.ɵɵComponentDeclaration<RiviniaModalHost, "rv-modal-host", never, {
291
+ "modal": {
292
+ "alias": "modal";
293
+ "required": true;
294
+ "isSignal": true;
295
+ };
296
+ "size": {
297
+ "alias": "size";
298
+ "required": false;
299
+ "isSignal": true;
300
+ };
301
+ "showCloseButton": {
302
+ "alias": "showCloseButton";
303
+ "required": false;
304
+ "isSignal": true;
305
+ };
306
+ "titleOf": {
307
+ "alias": "titleOf";
308
+ "required": false;
309
+ "isSignal": true;
310
+ };
311
+ }, {}, never, never, true, never>;
312
+ }
313
+ /**
314
+ * Reference-counted background scroll lock. Multiple overlays share one lock:
315
+ * closing one modal never unlocks the page while another still needs it.
316
+ * Restores the exact previous `overflow` value (never blanks app-owned
317
+ * styles). All DOM access happens inside methods — never at module
318
+ * evaluation — and every method is a no-op without a document (SSR).
319
+ */
320
+ export declare class RiviniaModalScrollLock {
321
+ private readonly _doc;
322
+ private _locks;
323
+ private _previousOverflow;
324
+ /** Number of active locks (diagnostics/tests). */
325
+ get active(): number;
326
+ acquire(): void;
327
+ release(): void;
328
+ private get document();
329
+ static ɵfac: i0.ɵɵFactoryDeclaration<RiviniaModalScrollLock, never>;
330
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
331
+ }
332
+ export type { RiviniaModalAnimation, RiviniaModalBackdrop, RiviniaModalHostManager, RiviniaModalPosition, RiviniaModalSize, RiviniaModalUiOptions };
@@ -69,6 +69,13 @@ type ModalContent<TData> = {
69
69
  } | {
70
70
  readonly kind: 'component';
71
71
  readonly component: Type<unknown>;
72
+ /**
73
+ * Component inputs bound by the UI via `NgComponentOutlet`.
74
+ *
75
+ * Type hole (by design, not checked): `Record<string, unknown>` accepts
76
+ * any shape — a misspelled input key compiles and fails silently at
77
+ * render. The UI layer, not Core, owns binding.
78
+ */
72
79
  readonly inputs?: Record<string, unknown>;
73
80
  };
74
81
  /** Template context for `TemplateRef` content. */
@@ -86,6 +93,12 @@ interface ModalContext<TData> {
86
93
  readonly id: string;
87
94
  readonly data: TData;
88
95
  readonly parentId: string | null;
96
+ /**
97
+ * Type hole (by design): `result` is `unknown`, so passing the wrong payload
98
+ * type compiles — the mismatch surfaces only in the `ModalResult` value the
99
+ * opener awaits. Prefer the typed `ModalHandle<TResult>.close()` at the
100
+ * call site, which IS checked (see type tests).
101
+ */
89
102
  close(result?: unknown): boolean;
90
103
  dismiss(reason?: string): boolean;
91
104
  }
@@ -150,7 +163,11 @@ interface ModalOpenOptions<TData = unknown> {
150
163
  readonly dismiss?: Partial<ModalDismissPolicy>;
151
164
  readonly guards?: readonly ModalGuard[];
152
165
  readonly metadata?: Record<string, unknown>;
153
- /** Inputs for component content (bound by the UI via `NgComponentOutlet`). */
166
+ /** Inputs for component content (bound by the UI via `NgComponentOutlet`).
167
+ *
168
+ * Type hole (by design, not checked): any shape compiles; misspelled keys
169
+ * fail silently at render. See `ModalContent` inputs.
170
+ */
154
171
  readonly inputs?: Record<string, unknown>;
155
172
  }
156
173
  /** Partial changes for `update()` (content shape itself is immutable). */
@@ -254,6 +271,10 @@ export declare class RiviniaModal {
254
271
  readonly active: import("@angular/core").Signal<readonly ModalInstance<unknown>[]>;
255
272
  /** Queued instances, FIFO admission order. */
256
273
  readonly queued: import("@angular/core").Signal<readonly ModalInstance<unknown>[]>;
274
+ /** Closing instances (exit animations), in strategy display order. */
275
+ readonly closing: import("@angular/core").Signal<readonly ModalInstance<unknown>[]>;
276
+ /** Everything rendered (active + closing), in strategy display order. */
277
+ readonly visible: import("@angular/core").Signal<readonly ModalInstance<unknown>[]>;
257
278
  /** Topmost active instance (the only Escape-eligible one), if any. */
258
279
  readonly top: import("@angular/core").Signal<ModalInstance<unknown> | null>;
259
280
  constructor();