@rivinia/modal 0.0.0-stage → 0.1.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
@@ -1,3 +1,80 @@
1
- # Temporary Holding Version
1
+ # @rivinia/modal
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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)
5
+
6
+ Headless modal manager for Angular 22+. Stack/Queue strategies, typed
7
+ `Promise` results, dismissal policies, async guards, ownership — zero DOM,
8
+ zero styles, zero router dependency. A future `@rivinia/modal/ui` entry point
9
+ will render this state; Core never imports UI.
10
+
11
+ - Version: **0.1.0**
12
+ - Requires: **Angular `^22.0.0`**, `tslib`
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @rivinia/modal
18
+ ```
19
+
20
+ ```ts
21
+ // app.config.ts
22
+ import { provideRiviniaModal } from '@rivinia/modal';
23
+
24
+ providers: [provideRiviniaModal({ strategy: 'stack', maxVisible: 3 })];
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```ts
30
+ export class Users {
31
+ private modals = injectRiviniaModal();
32
+
33
+ async remove(user: User) {
34
+ const handle = this.modals.openModal<DeleteData, boolean>(confirmTemplate, {
35
+ data: { name: user.name },
36
+ ref: 'confirm-delete',
37
+ });
38
+ const result = await handle.result;
39
+ if (result.status === 'confirmed' && result.value) {
40
+ this.delete(user);
41
+ }
42
+ // 'cancelled' | 'dismissed' | 'queued-cancelled' | 'owner-destroyed' | 'rejected'
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## API
48
+
49
+ | Export | Kind | Purpose |
50
+ |---|---|---|
51
+ | `provideRiviniaModal(config?)` | provider | `{ strategy?, dismiss?, maxVisible?, maxQueue?, overflow?, descendants?, presets?, closingTimeout?, diagnostics? }` |
52
+ | `RiviniaModal` | `@Service()` | `openModal/close/dismiss/cancel/requestDismiss/update/confirmClosed/dismissAll/findByRef/useStrategy/debugState` + `active/queued/top` signals |
53
+ | `injectRiviniaModal()` | function | shorthand for `inject(RiviniaModal)` |
54
+ | `RIVINIA_MODAL_CONTEXT` | token | per-instance context for dynamic components |
55
+ | `StackStrategy` / `QueueStrategy` | classes | built-in ordering policies (`ModalStrategy` for custom) |
56
+ | types | types | `ModalHandle`, `ModalResult`, `ModalContent`, `ModalOpenOptions`, `ModalDismissPolicy`, `ModalGuard`, … |
57
+
58
+ Results settle exactly once, at dismissal initiation — exit animations never
59
+ delay callers. `close/dismiss/cancel` are guard-free primitives (booleans);
60
+ `requestDismiss(id, origin)` runs async guards for user intents. Forced paths
61
+ (owner destroy, overflow eviction, `dismissAll`) bypass guards. Render failures
62
+ reject the promise instead of becoming variants.
63
+
64
+ ## Ownership (who cleans up)
65
+
66
+ ```ts
67
+ // Owned by a short-lived component: dies with it (active → owner-destroyed)
68
+ this.modals.openModal(tpl, { owner: this.destroyRef });
69
+
70
+ // Independent (default): lives until closed — for services and app flows
71
+ this.modals.openModal(tpl);
72
+ ```
73
+
74
+ Closing a parent cascades to descendants (`dismissed: parent-closed`) unless
75
+ `descendants: 'prevent-parent-close'` blocks it. Swapping strategies with live
76
+ modals throws — close everything first.
77
+
78
+ ## License
79
+
80
+ MIT.
@@ -0,0 +1,420 @@
1
+ import * as i0 from "@angular/core";
2
+ import { DestroyRef, InjectionToken, Service, computed, inject, makeEnvironmentProviders, signal } from "@angular/core";
3
+ var StackStrategy = class {
4
+ name = "stack";
5
+ maxActive(maxVisible) {
6
+ return Math.max(1, Math.floor(maxVisible));
7
+ }
8
+ admit(visibleCount, _queuedCount, maxVisible) {
9
+ return visibleCount < Math.max(1, Math.floor(maxVisible)) ? "activate" : "queue";
10
+ }
11
+ order(visible) {
12
+ return [...visible].sort((a, b) => a.openedSeq - b.openedSeq);
13
+ }
14
+ };
15
+ var QueueStrategy = class {
16
+ name = "queue";
17
+ maxActive(_maxVisible) {
18
+ return 1;
19
+ }
20
+ admit(visibleCount, _queuedCount, _maxVisible) {
21
+ return visibleCount < 1 ? "activate" : "queue";
22
+ }
23
+ order(visible) {
24
+ return [...visible].sort((a, b) => a.openedSeq - b.openedSeq);
25
+ }
26
+ };
27
+ const RIVINIA_MODAL_CONTEXT = new InjectionToken("RIVINIA_MODAL_CONTEXT");
28
+ const RIVINIA_MODAL_CONFIG = new InjectionToken("RIVINIA_MODAL_CONFIG");
29
+ var RiviniaModal = class RiviniaModal {
30
+ config = inject(RIVINIA_MODAL_CONFIG, { optional: true });
31
+ _instances = signal([], ...ngDevMode ? [{ debugName: "_instances" }] : /* istanbul ignore next */ []);
32
+ _results = /* @__PURE__ */ new Map();
33
+ _strategy = this.resolveStrategy(this.config?.strategy);
34
+ _seq = 0;
35
+ _diagSeq = 0;
36
+ _diagnostics = [];
37
+ active = computed(() => this._strategy.order(this._instances().filter((i) => i.status === "active")), ...ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []);
38
+ queued = computed(() => this._instances().filter((i) => i.status === "queued"), ...ngDevMode ? [{ debugName: "queued" }] : /* istanbul ignore next */ []);
39
+ top = computed(() => {
40
+ const list = this.active();
41
+ return list.length > 0 ? list[list.length - 1] : null;
42
+ }, ...ngDevMode ? [{ debugName: "top" }] : /* istanbul ignore next */ []);
43
+ constructor() {
44
+ inject(DestroyRef).onDestroy(() => {
45
+ for (const record of [...this._instances()]) if (record.status === "queued") {
46
+ this.remove(record.id);
47
+ this.settle(record.id, { status: "queued-cancelled" });
48
+ } else if (record.status !== "closed") {
49
+ this.clearClosingTimer(record);
50
+ this.remove(record.id);
51
+ this.settle(record.id, {
52
+ status: "dismissed",
53
+ reason: "manager-destroyed"
54
+ });
55
+ }
56
+ });
57
+ }
58
+ openModal(content, options = {}) {
59
+ const id = `modal-${++this._seq}`;
60
+ const pending = this.deferred();
61
+ this._results.set(id, pending);
62
+ const handle = {
63
+ id,
64
+ result: pending.promise,
65
+ close: (value) => this.close(id, value),
66
+ dismiss: (reason) => this.dismiss(id, reason),
67
+ cancel: (reason) => this.cancel(id, reason)
68
+ };
69
+ const visibleCount = this._instances().filter((i) => i.status !== "queued").length;
70
+ const intent = this._strategy.admit(visibleCount, this.queuedCount(), this.maxVisible());
71
+ let status = intent === "activate" ? "active" : "queued";
72
+ if (intent === "queue") {
73
+ const outcome = this.admitQueued(id);
74
+ if (outcome === "active" || outcome === "queued") status = outcome;
75
+ else {
76
+ this.settle(id, {
77
+ status: "rejected",
78
+ reason: outcome
79
+ });
80
+ this.diagnose("rejected", id, options.ref ?? null, outcome);
81
+ return handle;
82
+ }
83
+ }
84
+ const record = {
85
+ id,
86
+ ref: options.ref ?? null,
87
+ caller: options.caller ?? null,
88
+ parentId: this.knownParent(options.parentId) ? options.parentId : null,
89
+ data: options.data,
90
+ content,
91
+ status,
92
+ dismiss: {
93
+ closeOnEscape: true,
94
+ backdrop: "close",
95
+ ...this.presetDismiss(options.preset),
96
+ ...options.dismiss
97
+ },
98
+ metadata: { ...options.metadata ?? {} },
99
+ openedSeq: this._seq,
100
+ generation: 0,
101
+ guards: [...options.guards ?? []],
102
+ closingTimer: null,
103
+ ownerCleanup: null
104
+ };
105
+ if (options.parentId !== void 0 && record.parentId === null) this.diagnose("rejected", id, record.ref, "stale-parent");
106
+ this._instances.update((list) => [...list, record]);
107
+ if (options.owner) {
108
+ const off = options.owner.onDestroy(() => this.teardownOwned(id));
109
+ this.patch(id, { ownerCleanup: off });
110
+ }
111
+ this.diagnose(intent === "activate" ? "activated" : "queued", id, record.ref);
112
+ return handle;
113
+ }
114
+ close(id, value) {
115
+ const record = this.get(id);
116
+ if (!record || record.status !== "active" || this.blockedByDescendants(id)) return false;
117
+ this.beginClosing(id, "confirmed", void 0, value);
118
+ return true;
119
+ }
120
+ dismiss(id, reason) {
121
+ const record = this.get(id);
122
+ if (!record || this.blockedByDescendants(id)) return false;
123
+ if (record.status === "active") {
124
+ this.beginClosing(id, "dismissed", reason);
125
+ return true;
126
+ }
127
+ if (record.status === "queued") {
128
+ this.closeDescendants(id, true);
129
+ this.remove(id);
130
+ this.settle(id, {
131
+ status: "dismissed",
132
+ reason
133
+ });
134
+ this.diagnose("dismissed", id, record.ref, reason);
135
+ return true;
136
+ }
137
+ return false;
138
+ }
139
+ cancel(id, reason) {
140
+ const record = this.get(id);
141
+ if (!record || this.blockedByDescendants(id)) return false;
142
+ if (record.status === "queued") {
143
+ this.closeDescendants(id, true);
144
+ this.remove(id);
145
+ this.settle(id, { status: "queued-cancelled" });
146
+ this.diagnose("dismissed", id, record.ref, reason ?? "cancelled");
147
+ return true;
148
+ }
149
+ if (record.status !== "active") return false;
150
+ this.beginClosing(id, "cancelled", reason);
151
+ return true;
152
+ }
153
+ async requestDismiss(id, origin, reason) {
154
+ const record = this.get(id);
155
+ if (!record || record.status !== "active" || this.blockedByDescendants(id)) return false;
156
+ const generation = record.generation;
157
+ for (const guard of record.guards) {
158
+ let allowed;
159
+ try {
160
+ allowed = await guard({
161
+ id,
162
+ origin
163
+ });
164
+ } catch {
165
+ allowed = false;
166
+ }
167
+ const current = this.get(id);
168
+ if (!current || current.status !== "active" || current.generation !== generation) return false;
169
+ if (!allowed) {
170
+ this.diagnose("guard-denied", id, record.ref, origin);
171
+ return false;
172
+ }
173
+ }
174
+ return this.dismiss(id, reason ?? origin);
175
+ }
176
+ update(id, changes) {
177
+ const record = this.get(id);
178
+ if (!record || record.status !== "active" && record.status !== "queued") return false;
179
+ this._instances.update((list) => list.map((t) => t.id === id ? {
180
+ ...t,
181
+ data: changes.data !== void 0 ? changes.data : t.data,
182
+ metadata: changes.metadata !== void 0 ? { ...changes.metadata } : t.metadata
183
+ } : t));
184
+ return true;
185
+ }
186
+ confirmClosed(id) {
187
+ const record = this.get(id);
188
+ if (!record || record.status !== "closing") return false;
189
+ this.finalize(id);
190
+ return true;
191
+ }
192
+ dismissAll(reason = "dismiss-all") {
193
+ for (const record of [...this._instances()]) if (record.status === "queued") {
194
+ this.remove(record.id);
195
+ this.settle(record.id, { status: "queued-cancelled" });
196
+ } else if (record.status === "active") this.beginClosing(record.id, "dismissed", reason);
197
+ else if (record.status === "closing") this.finalize(record.id);
198
+ }
199
+ findByRef(ref) {
200
+ return this._instances().filter((i) => i.ref === ref && i.status !== "closed");
201
+ }
202
+ useStrategy(strategy) {
203
+ if (this._instances().some((i) => i.status !== "closed")) throw new Error("[rivinia/modal] useStrategy() rejected: active, queued or closing modals exist. Close everything first — live migration is not supported.");
204
+ this._strategy = strategy;
205
+ }
206
+ debugState() {
207
+ const all = this._instances();
208
+ return {
209
+ strategy: this._strategy.name,
210
+ active: this.orderedActive().filter((i) => i.status === "active").map((i) => i.id),
211
+ closing: all.filter((i) => i.status === "closing").map((i) => i.id),
212
+ queued: all.filter((i) => i.status === "queued").map((i) => i.id)
213
+ };
214
+ }
215
+ getDiagnostics() {
216
+ return [...this._diagnostics];
217
+ }
218
+ clearDiagnostics() {
219
+ this._diagnostics.length = 0;
220
+ }
221
+ get(id) {
222
+ return this._instances().find((i) => i.id === id);
223
+ }
224
+ queuedCount() {
225
+ return this._instances().filter((i) => i.status === "queued").length;
226
+ }
227
+ orderedActive() {
228
+ return [...this._strategy.order(this._instances().filter((i) => i.status === "active" || i.status === "closing"))];
229
+ }
230
+ maxVisible() {
231
+ return Math.max(1, Math.floor(this.config?.maxVisible ?? 5));
232
+ }
233
+ maxQueue() {
234
+ const value = this.config?.maxQueue ?? Number.POSITIVE_INFINITY;
235
+ return value < 0 ? 0 : value;
236
+ }
237
+ presetDismiss(preset) {
238
+ if (!preset) return {};
239
+ return { ...this.config?.presets?.[preset]?.dismiss ?? {} };
240
+ }
241
+ knownParent(parentId) {
242
+ if (parentId === void 0) return false;
243
+ return this._instances().some((i) => i.id === parentId && i.status !== "closed");
244
+ }
245
+ admitQueued(id) {
246
+ switch (this.config?.overflow ?? "queue") {
247
+ case "reject": return "overflow";
248
+ case "dismiss-oldest": {
249
+ const oldest = this.orderedActive().find((i) => i.status === "active");
250
+ if (oldest) this.beginClosing(oldest.id, "dismissed", "overflow-evicted");
251
+ return "active";
252
+ }
253
+ default: return this.queuedCount() >= this.maxQueue() ? "queue-full" : "queued";
254
+ }
255
+ }
256
+ blockedByDescendants(id) {
257
+ if ((this.config?.descendants ?? "close-with-parent") !== "prevent-parent-close") return false;
258
+ const blocked = this._instances().some((i) => i.parentId === id && i.status !== "closed" && i.status !== "closing");
259
+ if (blocked) this.diagnose("strategy-rejected", id, this.get(id)?.ref ?? null, "prevent-parent-close");
260
+ return blocked;
261
+ }
262
+ beginClosing(id, status, reason, value) {
263
+ const record = this.get(id);
264
+ if (!record || record.status !== "active") return;
265
+ record.generation++;
266
+ this.closeDescendants(id, false);
267
+ this.patch(id, { status: "closing" });
268
+ if (status === "confirmed") this.settle(id, {
269
+ status,
270
+ value
271
+ });
272
+ else this.settle(id, {
273
+ status,
274
+ reason
275
+ });
276
+ this.diagnose("closing", id, record.ref, reason);
277
+ const timeout = Math.max(0, this.config?.closingTimeout ?? 300);
278
+ if (timeout === 0) return;
279
+ const generation = record.generation;
280
+ record.closingTimer = setTimeout(() => {
281
+ const current = this.get(id);
282
+ if (current && current.status === "closing" && current.generation === generation) this.finalize(id);
283
+ }, timeout);
284
+ this.patch(id, { closingTimer: record.closingTimer });
285
+ }
286
+ closeDescendants(parentId, force) {
287
+ if (!force && (this.config?.descendants ?? "close-with-parent") !== "close-with-parent") return;
288
+ for (const child of this._instances().filter((i) => i.parentId === parentId && (i.status === "active" || i.status === "queued"))) if (child.status === "queued") {
289
+ this.remove(child.id);
290
+ this.settle(child.id, { status: "queued-cancelled" });
291
+ this.diagnose("dismissed", child.id, child.ref, "parent-closed");
292
+ } else {
293
+ this.closeDescendants(child.id, force);
294
+ this.beginClosing(child.id, "dismissed", "parent-closed");
295
+ }
296
+ }
297
+ finalize(id, promoteAfter = true) {
298
+ const record = this.get(id);
299
+ if (!record) return;
300
+ this.clearClosingTimer(record);
301
+ this.patch(id, { status: "closed" });
302
+ record.ownerCleanup?.();
303
+ this._instances.update((list) => list.filter((i) => i.id !== id));
304
+ this._results.delete(id);
305
+ this.diagnose("closed", id, record.ref);
306
+ if (promoteAfter) this.promote();
307
+ }
308
+ promote() {
309
+ const width = this._strategy.maxActive(this.maxVisible());
310
+ const visible = () => this._instances().filter((i) => i.status === "active" || i.status === "closing").length;
311
+ while (visible() < width) {
312
+ const head = this._instances().find((i) => i.status === "queued");
313
+ if (!head) break;
314
+ this.patch(head.id, { status: "active" });
315
+ this.diagnose("promoted", head.id, head.ref);
316
+ }
317
+ }
318
+ remove(id) {
319
+ const record = this.get(id);
320
+ if (record) this.clearClosingTimer(record);
321
+ this._instances.update((list) => list.filter((i) => i.id !== id));
322
+ }
323
+ clearClosingTimer(record) {
324
+ if (record.closingTimer) {
325
+ clearTimeout(record.closingTimer);
326
+ record.closingTimer = null;
327
+ }
328
+ }
329
+ teardownOwned(id) {
330
+ const record = this.get(id);
331
+ if (!record || record.status === "closed" || record.status === "closing") return;
332
+ if (record.status === "queued") {
333
+ this.closeDescendants(id, true);
334
+ this.remove(id);
335
+ this.settle(id, { status: "queued-cancelled" });
336
+ this.diagnose("dismissed", id, record.ref, "owner-destroyed");
337
+ return;
338
+ }
339
+ record.generation++;
340
+ this.closeDescendants(id, true);
341
+ this.patch(id, { status: "closing" });
342
+ this.settle(id, { status: "owner-destroyed" });
343
+ this.diagnose("closing", id, record.ref, "owner-destroyed");
344
+ this.finalize(id, false);
345
+ }
346
+ patch(id, changes) {
347
+ this._instances.update((list) => list.map((t) => t.id === id ? {
348
+ ...t,
349
+ ...changes
350
+ } : t));
351
+ }
352
+ deferred() {
353
+ let resolve;
354
+ return {
355
+ promise: new Promise((res) => {
356
+ resolve = res;
357
+ }),
358
+ resolve: (value) => {
359
+ resolve(value);
360
+ },
361
+ settled: false
362
+ };
363
+ }
364
+ settle(id, value) {
365
+ const pending = this._results.get(id);
366
+ if (!pending || pending.settled) return;
367
+ pending.settled = true;
368
+ pending.resolve(value);
369
+ }
370
+ diagnose(type, id, ref, reason) {
371
+ if (!this.config?.diagnostics) return;
372
+ this._diagnostics.push({
373
+ seq: ++this._diagSeq,
374
+ type,
375
+ id,
376
+ ref,
377
+ reason
378
+ });
379
+ while (this._diagnostics.length > 100) this._diagnostics.shift();
380
+ }
381
+ resolveStrategy(configured) {
382
+ if (!configured || configured === "stack") return new StackStrategy();
383
+ if (configured === "queue") return new QueueStrategy();
384
+ return configured;
385
+ }
386
+ static ɵfac = i0.ɵɵngDeclareFactory({
387
+ minVersion: "12.0.0",
388
+ version: "22.2.2",
389
+ ngImport: i0,
390
+ type: RiviniaModal,
391
+ deps: [],
392
+ target: i0.ɵɵFactoryTarget.Service
393
+ });
394
+ static ɵprov = i0.ɵɵngDeclareService({
395
+ minVersion: "22.0.0",
396
+ version: "22.2.2",
397
+ ngImport: i0,
398
+ type: RiviniaModal
399
+ });
400
+ };
401
+ i0.ɵɵngDeclareClassMetadata({
402
+ minVersion: "12.0.0",
403
+ version: "22.2.2",
404
+ ngImport: i0,
405
+ type: RiviniaModal,
406
+ decorators: [{ type: Service }],
407
+ ctorParameters: () => []
408
+ });
409
+ function provideRiviniaModal(config = {}) {
410
+ return makeEnvironmentProviders([{
411
+ provide: RIVINIA_MODAL_CONFIG,
412
+ useValue: config
413
+ }]);
414
+ }
415
+ function injectRiviniaModal() {
416
+ return inject(RiviniaModal);
417
+ }
418
+ export { QueueStrategy, RIVINIA_MODAL_CONTEXT, RiviniaModal, StackStrategy, injectRiviniaModal, provideRiviniaModal };
419
+
420
+ //# sourceMappingURL=rivinia-modal.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rivinia-modal.mjs","names":[],"sources":["../../../projects/modal/src/lib/modal-strategy.ts","../../../projects/modal/src/lib/modal-context.ts","../../../projects/modal/src/lib/modal-config.ts","../../../projects/modal/src/lib/rivinia-modal.ts","../../../projects/modal/src/lib/provide-modal.ts"],"sourcesContent":["/**\n * Ordering/admission policy. Strategies are pure with respect to lifecycle:\n * the manager owns identity, results, timers and disposal; strategies own\n * ordering and activation width. No mode-dependent `if/else` in the manager.\n */\nexport interface ModalStrategy {\n readonly name: string;\n /** Maximum simultaneously active instances under this strategy. */\n maxActive(maxVisible: number): number;\n /**\n * Admission of a new request given current visible/queued sets.\n * Returns the action; the manager executes it (and settles results).\n */\n admit(visibleCount: number, queuedCount: number, maxVisible: number): 'activate' | 'queue';\n /** Display order of visible instances, bottom-first. */\n order<T extends { readonly openedSeq: number }>(visible: readonly T[]): readonly T[];\n}\n\n/** Multiple concurrent modals, topmost (last opened) interactive. */\nexport class StackStrategy implements ModalStrategy {\n readonly name = 'stack';\n maxActive(maxVisible: number): number {\n return Math.max(1, Math.floor(maxVisible));\n }\n admit(visibleCount: number, _queuedCount: number, maxVisible: number): 'activate' | 'queue' {\n return visibleCount < Math.max(1, Math.floor(maxVisible)) ? 'activate' : 'queue';\n }\n order<T extends { readonly openedSeq: number }>(visible: readonly T[]): readonly T[] {\n return [...visible].sort((a, b) => a.openedSeq - b.openedSeq);\n }\n}\n\n/** Strictly serial: one active modal, FIFO activation of the rest. */\nexport class QueueStrategy implements ModalStrategy {\n readonly name = 'queue';\n maxActive(_maxVisible: number): number {\n return 1;\n }\n admit(visibleCount: number, _queuedCount: number, _maxVisible: number): 'activate' | 'queue' {\n return visibleCount < 1 ? 'activate' : 'queue';\n }\n order<T extends { readonly openedSeq: number }>(visible: readonly T[]): readonly T[] {\n return [...visible].sort((a, b) => a.openedSeq - b.openedSeq);\n }\n}\n","import { InjectionToken } from '@angular/core';\nimport type { ModalContext } from './modal-types';\n\n/**\n * Injection token providing the `ModalContext` of the enclosing modal to\n * dynamically created components. Template content receives the same object\n * as `let-modal=\"modal\"`. Never global — one value per modal instance.\n */\nexport const RIVINIA_MODAL_CONTEXT = new InjectionToken<ModalContext<unknown>>(\n 'RIVINIA_MODAL_CONTEXT',\n);\n","import { InjectionToken } from '@angular/core';\nimport type {\n DescendantPolicy,\n ModalDismissPolicy,\n ModalOverflowPolicy,\n} from './modal-types';\n\n/** Named preset: a reusable dismissal shape (explicit options always win). */\nexport interface ModalPreset {\n readonly dismiss?: Partial<ModalDismissPolicy>;\n}\n\n/**\n * Global configuration for `@rivinia/modal`. Everything optional; per-open\n * options win. No styling, positions, icons, or translations here — ever.\n */\nexport interface RiviniaModalConfig {\n /**\n * Ordering/activation policy. `'stack'` = up to `maxVisible` concurrent,\n * topmost interactive. `'queue'` = strictly serial (one active, FIFO rest).\n * A custom `ModalStrategy` instance is also accepted.\n * @default 'stack'\n */\n readonly strategy?: 'stack' | 'queue' | import('./modal-strategy').ModalStrategy;\n /** Global dismissal defaults. @default { closeOnEscape: true, backdrop: 'close' } */\n readonly dismiss?: ModalDismissPolicy;\n /** Capacity of simultaneously active modals. Clamped to ≥ 1. @default 5 */\n readonly maxVisible?: number;\n /** Bound for queued requests (queue policy). @default Infinity */\n readonly maxQueue?: number;\n /** Overflow behavior when full. @default 'queue' (lossless — never drops input) */\n readonly overflow?: ModalOverflowPolicy;\n /** Parent closure with remaining descendants. @default 'close-with-parent' */\n readonly descendants?: DescendantPolicy;\n /** Named dismissal presets referenced by `openModal({ preset })`. */\n readonly presets?: Record<string, ModalPreset>;\n /**\n * Time the manager waits for UI teardown confirmation (`confirmClosed`)\n * before auto-finalizing a `closing` instance. `0` = manual only.\n * @default 300\n */\n readonly closingTimeout?: number;\n /**\n * Bounded transition log for debugging (ring buffer, no payloads).\n * Off by default to stay production-light.\n * @default false\n */\n readonly diagnostics?: boolean;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaModal()`.\n * @internal\n */\nexport const RIVINIA_MODAL_CONFIG = new InjectionToken<RiviniaModalConfig>(\n 'RIVINIA_MODAL_CONFIG',\n);\n\nexport const DEFAULT_MAX_VISIBLE = 5;\nexport const DEFAULT_CLOSING_TIMEOUT = 300;\nexport const DIAGNOSTIC_CAP = 100;\n","import { DestroyRef, computed, inject, Service, signal } from '@angular/core';\nimport {\n DEFAULT_CLOSING_TIMEOUT,\n DEFAULT_MAX_VISIBLE,\n DIAGNOSTIC_CAP,\n RIVINIA_MODAL_CONFIG,\n type RiviniaModalConfig,\n} from './modal-config';\nimport type {\n ModalDiagnostic,\n ModalDismissOrigin,\n ModalGuard,\n ModalHandle,\n ModalInstance,\n ModalOpenOptions,\n ModalResult,\n ModalUpdate,\n} from './modal-types';\nimport { QueueStrategy, StackStrategy, type ModalStrategy } from './modal-strategy';\n\ninterface ModalRecord extends ModalInstance<unknown> {\n openedSeq: number;\n generation: number;\n guards: readonly ModalGuard[];\n closingTimer: ReturnType<typeof setTimeout> | null;\n ownerCleanup: (() => void) | null;\n}\n\ninterface PendingResult {\n promise: Promise<ModalResult<unknown>>;\n resolve: (value: ModalResult<unknown>) => void;\n settled: boolean;\n}\n\n/**\n * Headless modal manager. Owns identity, lifecycle coordination, result\n * settlement and diagnostics. Ordering/admission policy is delegated to a\n * `ModalStrategy` (Stack/Queue/custom) — the manager never branches on mode.\n *\n * Settlement contract (each path settles exactly once, never hangs):\n * - `close()` on active → `confirmed` · `dismiss()` on active → `dismissed`\n * - `cancel()` on active → `cancelled` · on queued → `queued-cancelled`\n * - `dismiss()` on queued → `dismissed` with the reason (pre-activation)\n * - overflow/guard refusal → `rejected` · owner gone → `owner-destroyed`\n * - forced paths (owner, overflow eviction, `dismissAll`) bypass guards\n * - `close/dismiss/cancel` are guard-free primitives; `requestDismiss` runs\n * guards for user intents (escape/backdrop/button) or explicit review\n * - infrastructure failures reject the promise — never a result variant\n *\n * Results settle at dismissal initiation, not after UI teardown: animations\n * never delay callers. The `closing` → `closed` handshake (`confirmClosed`\n * or `closingTimeout` auto-finalize) exists only for exit animations.\n */\n@Service()\nexport class RiviniaModal {\n private readonly config = inject<RiviniaModalConfig>(RIVINIA_MODAL_CONFIG, {\n optional: true,\n });\n\n private readonly _instances = signal<readonly ModalRecord[]>([]);\n private readonly _results = new Map<string, PendingResult>();\n private _strategy: ModalStrategy = this.resolveStrategy(this.config?.strategy);\n private _seq = 0;\n private _diagSeq = 0;\n private readonly _diagnostics: ModalDiagnostic[] = [];\n\n /** Active instances in strategy display order (bottom-first). */\n readonly active = computed<readonly ModalInstance[]>(() =>\n this._strategy.order(this._instances().filter((i) => i.status === 'active')),\n );\n /** Queued instances, FIFO admission order. */\n readonly queued = computed<readonly ModalInstance[]>(() =>\n this._instances().filter((i) => i.status === 'queued'),\n );\n /** Topmost active instance (the only Escape-eligible one), if any. */\n readonly top = computed<ModalInstance | null>(() => {\n const list = this.active();\n return list.length > 0 ? list[list.length - 1] : null;\n });\n\n constructor() {\n inject(DestroyRef).onDestroy(() => {\n for (const record of [...this._instances()]) {\n if (record.status === 'queued') {\n this.remove(record.id);\n this.settle(record.id, { status: 'queued-cancelled' });\n } else if (record.status !== 'closed') {\n this.clearClosingTimer(record);\n this.remove(record.id);\n this.settle(record.id, { status: 'dismissed', reason: 'manager-destroyed' });\n }\n }\n });\n }\n\n openModal<TData = unknown, TResult = unknown>(\n content: import('./modal-types').ModalContent<TData>,\n options: ModalOpenOptions<TData> = {},\n ): ModalHandle<TResult> {\n const id = `modal-${++this._seq}`;\n const pending = this.deferred();\n this._results.set(id, pending);\n const handle: ModalHandle<TResult> = {\n id,\n result: pending.promise as Promise<ModalResult<TResult>>,\n close: (value?: TResult) => this.close(id, value),\n dismiss: (reason?: string) => this.dismiss(id, reason),\n cancel: (reason?: string) => this.cancel(id, reason),\n };\n\n const visibleCount = this._instances().filter((i) => i.status !== 'queued').length;\n const intent = this._strategy.admit(visibleCount, this.queuedCount(), this.maxVisible());\n let status: 'active' | 'queued' = intent === 'activate' ? 'active' : 'queued';\n if (intent === 'queue') {\n const outcome = this.admitQueued(id);\n if (outcome === 'active' || outcome === 'queued') {\n // 'dismiss-oldest' evicted room: the newcomer activates immediately.\n // 'queue' keeps it queued.\n status = outcome;\n } else {\n this.settle(id, { status: 'rejected', reason: outcome });\n this.diagnose('rejected', id, options.ref ?? null, outcome);\n return handle;\n }\n }\n\n const record: ModalRecord = {\n id,\n ref: options.ref ?? null,\n caller: options.caller ?? null,\n parentId: this.knownParent(options.parentId) ? (options.parentId as string) : null,\n data: options.data as unknown,\n content: content as ModalRecord['content'],\n status,\n dismiss: {\n closeOnEscape: true,\n backdrop: 'close',\n ...this.presetDismiss(options.preset),\n ...options.dismiss,\n },\n metadata: { ...(options.metadata ?? {}) },\n openedSeq: this._seq,\n generation: 0,\n guards: [...(options.guards ?? [])],\n closingTimer: null,\n ownerCleanup: null,\n };\n if (options.parentId !== undefined && record.parentId === null) {\n this.diagnose('rejected', id, record.ref, 'stale-parent');\n }\n this._instances.update((list) => [...list, record]);\n\n if (options.owner) {\n const off = options.owner.onDestroy(() => this.teardownOwned(id));\n this.patch(id, { ownerCleanup: off });\n }\n this.diagnose(intent === 'activate' ? 'activated' : 'queued', id, record.ref);\n return handle;\n }\n\n /**\n * Confirms with a value. Active instances only; settles `confirmed`.\n * Blocked (returns `false`) by the prevent-parent-close policy.\n */\n close(id: string, value?: unknown): boolean {\n const record = this.get(id);\n if (!record || record.status !== 'active' || this.blockedByDescendants(id)) return false;\n this.beginClosing(id, 'confirmed', undefined, value);\n return true;\n }\n\n /**\n * Guard-free dismissal primitive. Active → `dismissed`; queued → removed as\n * `dismissed` with the reason. Blocked by prevent-parent-close.\n */\n dismiss(id: string, reason?: string): boolean {\n const record = this.get(id);\n if (!record || this.blockedByDescendants(id)) return false;\n if (record.status === 'active') {\n this.beginClosing(id, 'dismissed', reason);\n return true;\n }\n if (record.status === 'queued') {\n this.closeDescendants(id, true);\n this.remove(id);\n this.settle(id, { status: 'dismissed', reason });\n this.diagnose('dismissed', id, record.ref, reason);\n return true;\n }\n return false;\n }\n\n /**\n * Cancels: active → `cancelled`, queued → removed as `queued-cancelled`.\n * Blocked by prevent-parent-close. Closing/closed/unknown → `false`.\n */\n cancel(id: string, reason?: string): boolean {\n const record = this.get(id);\n if (!record || this.blockedByDescendants(id)) return false;\n if (record.status === 'queued') {\n this.closeDescendants(id, true);\n this.remove(id);\n this.settle(id, { status: 'queued-cancelled' });\n this.diagnose('dismissed', id, record.ref, reason ?? 'cancelled');\n return true;\n }\n if (record.status !== 'active') return false;\n this.beginClosing(id, 'cancelled', reason);\n return true;\n }\n\n /**\n * Guarded dismissal for user intents (escape/backdrop/button) or explicit\n * programmatic review. Guards run sequentially; any `false` or throw denies\n * (stays open + diagnostic). Stale guard completions are ignored.\n */\n async requestDismiss(\n id: string,\n origin: ModalDismissOrigin,\n reason?: string,\n ): Promise<boolean> {\n const record = this.get(id);\n if (!record || record.status !== 'active' || this.blockedByDescendants(id)) return false;\n const generation = record.generation;\n for (const guard of record.guards) {\n let allowed: boolean;\n try {\n allowed = await guard({ id, origin });\n } catch {\n allowed = false;\n }\n const current = this.get(id);\n if (!current || current.status !== 'active' || current.generation !== generation) {\n return false; // Resolved stale: something else already moved it.\n }\n if (!allowed) {\n this.diagnose('guard-denied', id, record.ref, origin);\n return false;\n }\n }\n return this.dismiss(id, reason ?? origin);\n }\n\n /** Merges data/metadata (new object identity so UI re-renders). */\n update<TData>(id: string, changes: import('./modal-types').ModalUpdate<TData>): boolean {\n const record = this.get(id);\n if (!record || (record.status !== 'active' && record.status !== 'queued')) return false;\n this._instances.update((list) =>\n list.map((t) =>\n t.id === id\n ? {\n ...t,\n data: (changes.data !== undefined ? changes.data : t.data) as unknown,\n metadata: changes.metadata !== undefined ? { ...changes.metadata } : t.metadata,\n }\n : t,\n ),\n );\n return true;\n }\n\n /** UI teardown confirmation after exit animations. Auto-finalizes via timeout. */\n confirmClosed(id: string): boolean {\n const record = this.get(id);\n if (!record || record.status !== 'closing') return false;\n this.finalize(id);\n return true;\n }\n\n /** Forced cleanup (bypasses guards): active → `dismissed`, queued → `queued-cancelled`. */\n dismissAll(reason = 'dismiss-all'): void {\n for (const record of [...this._instances()]) {\n if (record.status === 'queued') {\n this.remove(record.id);\n this.settle(record.id, { status: 'queued-cancelled' });\n } else if (record.status === 'active') {\n this.beginClosing(record.id, 'dismissed', reason);\n } else if (record.status === 'closing') {\n this.finalize(record.id);\n }\n }\n }\n\n /** Non-closed snapshots for a definition ref (e.g. avoid duplicate confirms). */\n findByRef(ref: string): ModalInstance[] {\n return this._instances().filter((i) => i.ref === ref && i.status !== 'closed');\n }\n\n /**\n * Swaps the strategy. Rejected with an explicit error while any instance is\n * not closed — silent live migration is never attempted.\n */\n useStrategy(strategy: ModalStrategy): void {\n if (this._instances().some((i) => i.status !== 'closed')) {\n throw new Error(\n '[rivinia/modal] useStrategy() rejected: active, queued or closing modals exist. ' +\n 'Close everything first — live migration is not supported.',\n );\n }\n this._strategy = strategy;\n }\n\n /** Introspection snapshot. Active means interactive; closing instances are\n * still rendered (exit animations) until `confirmClosed`/timeout finalizes. */\n debugState(): { strategy: string; active: string[]; closing: string[]; queued: string[] } {\n const all = this._instances();\n return {\n strategy: this._strategy.name,\n active: this.orderedActive()\n .filter((i) => i.status === 'active')\n .map((i) => i.id),\n closing: all.filter((i) => i.status === 'closing').map((i) => i.id),\n queued: all.filter((i) => i.status === 'queued').map((i) => i.id),\n };\n }\n\n /** Bounded transition log (only when `diagnostics: true`). */\n getDiagnostics(): readonly import('./modal-types').ModalDiagnostic[] {\n return [...this._diagnostics];\n }\n\n clearDiagnostics(): void {\n this._diagnostics.length = 0;\n }\n\n // ------------------------------------------------------------------ internals\n\n private get(id: string): ModalRecord | undefined {\n return this._instances().find((i) => i.id === id);\n }\n\n private queuedCount(): number {\n return this._instances().filter((i) => i.status === 'queued').length;\n }\n\n private orderedActive(): ModalRecord[] {\n return [\n ...this._strategy.order(\n this._instances().filter((i) => i.status === 'active' || i.status === 'closing'),\n ),\n ];\n }\n\n private maxVisible(): number {\n return Math.max(1, Math.floor(this.config?.maxVisible ?? DEFAULT_MAX_VISIBLE));\n }\n\n private maxQueue(): number {\n const value = this.config?.maxQueue ?? Number.POSITIVE_INFINITY;\n return value < 0 ? 0 : value;\n }\n\n private presetDismiss(preset?: string): Record<string, unknown> {\n if (!preset) return {};\n return { ...(this.config?.presets?.[preset]?.dismiss ?? {}) };\n }\n\n private knownParent(parentId: string | undefined): parentId is string {\n if (parentId === undefined) return false;\n return this._instances().some((i) => i.id === parentId && i.status !== 'closed');\n }\n\n /** Overflow handling for queue-intent admissions. Returns rejection reason, or the resulting status. */\n private admitQueued(id: string): string | 'active' | 'queued' {\n switch (this.config?.overflow ?? 'queue') {\n case 'reject':\n return 'overflow';\n case 'dismiss-oldest': {\n // Preempt: evict oldest pure-active instance (system path, no guards).\n const oldest = this.orderedActive().find((i) => i.status === 'active');\n if (oldest) this.beginClosing(oldest.id, 'dismissed', 'overflow-evicted');\n return 'active';\n }\n case 'queue':\n default:\n return this.queuedCount() >= this.maxQueue() ? 'queue-full' : 'queued';\n }\n }\n\n private blockedByDescendants(id: string): boolean {\n if ((this.config?.descendants ?? 'close-with-parent') !== 'prevent-parent-close') {\n return false;\n }\n const blocked = this._instances().some(\n (i) => i.parentId === id && i.status !== 'closed' && i.status !== 'closing',\n );\n if (blocked) this.diagnose('strategy-rejected', id, this.get(id)?.ref ?? null, 'prevent-parent-close');\n return blocked;\n }\n\n private beginClosing(\n id: string,\n status: 'confirmed' | 'dismissed' | 'cancelled',\n reason?: string,\n value?: unknown,\n ): void {\n const record = this.get(id);\n if (!record || record.status !== 'active') return;\n record.generation++;\n this.closeDescendants(id, false);\n this.patch(id, { status: 'closing' });\n if (status === 'confirmed') {\n this.settle(id, { status, value } as ModalResult<unknown>);\n } else {\n this.settle(id, { status, reason } as ModalResult<unknown>);\n }\n this.diagnose('closing', id, record.ref, reason);\n const timeout = Math.max(0, this.config?.closingTimeout ?? DEFAULT_CLOSING_TIMEOUT);\n if (timeout === 0) return; // Manual teardown: UI must call confirmClosed().\n const generation = record.generation;\n record.closingTimer = setTimeout(() => {\n const current = this.get(id);\n if (current && current.status === 'closing' && current.generation === generation) {\n this.finalize(id);\n }\n }, timeout);\n // Patch the timer into the stored record (same object identity held above).\n this.patch(id, { closingTimer: record.closingTimer });\n }\n\n private closeDescendants(parentId: string, force: boolean): void {\n if (!force && (this.config?.descendants ?? 'close-with-parent') !== 'close-with-parent') {\n return;\n }\n for (const child of this._instances().filter(\n (i) => i.parentId === parentId && (i.status === 'active' || i.status === 'queued'),\n )) {\n if (child.status === 'queued') {\n this.remove(child.id);\n this.settle(child.id, { status: 'queued-cancelled' });\n this.diagnose('dismissed', child.id, child.ref, 'parent-closed');\n } else {\n this.closeDescendants(child.id, force);\n this.beginClosing(child.id, 'dismissed', 'parent-closed');\n }\n }\n }\n\n private finalize(id: string, promoteAfter = true): void {\n const record = this.get(id);\n if (!record) return;\n this.clearClosingTimer(record);\n this.patch(id, { status: 'closed' });\n record.ownerCleanup?.();\n this._instances.update((list) => list.filter((i) => i.id !== id));\n this._results.delete(id);\n this.diagnose('closed', id, record.ref);\n if (promoteAfter) this.promote();\n }\n\n private promote(): void {\n const width = this._strategy.maxActive(this.maxVisible());\n const visible = (): number =>\n this._instances().filter((i) => i.status === 'active' || i.status === 'closing').length;\n while (visible() < width) {\n const head = this._instances().find((i) => i.status === 'queued');\n if (!head) break;\n this.patch(head.id, { status: 'active' });\n this.diagnose('promoted', head.id, head.ref);\n }\n }\n\n private remove(id: string): void {\n const record = this.get(id);\n if (record) this.clearClosingTimer(record);\n this._instances.update((list) => list.filter((i) => i.id !== id));\n }\n\n private clearClosingTimer(record: ModalRecord): void {\n if (record.closingTimer) {\n clearTimeout(record.closingTimer);\n record.closingTimer = null;\n }\n }\n\n private teardownOwned(id: string): void {\n const record = this.get(id);\n if (!record || record.status === 'closed' || record.status === 'closing') return;\n if (record.status === 'queued') {\n this.closeDescendants(id, true);\n this.remove(id);\n this.settle(id, { status: 'queued-cancelled' });\n this.diagnose('dismissed', id, record.ref, 'owner-destroyed');\n return;\n }\n record.generation++;\n this.closeDescendants(id, true);\n this.patch(id, { status: 'closing' });\n this.settle(id, { status: 'owner-destroyed' });\n this.diagnose('closing', id, record.ref, 'owner-destroyed');\n // No promotion: tearing down must not activate queued requests\n // only to destroy them a moment later (changes their settlement).\n this.finalize(id, false);\n }\n\n private patch(id: string, changes: Partial<ModalRecord>): void {\n this._instances.update((list) => list.map((t) => (t.id === id ? { ...t, ...changes } : t)));\n }\n\n private deferred<TResult>(): PendingResult & { promise: Promise<ModalResult<TResult>> } {\n let resolve!: (value: ModalResult<unknown>) => void;\n const promise = new Promise<ModalResult<unknown>>((res) => {\n resolve = res;\n });\n return {\n promise: promise as Promise<ModalResult<TResult>>,\n resolve: (value) => {\n resolve(value);\n },\n settled: false,\n };\n }\n\n private settle(id: string, value: ModalResult<unknown>): void {\n const pending = this._results.get(id);\n if (!pending || pending.settled) return; // Exactly-once, even under reentrancy.\n pending.settled = true;\n pending.resolve(value);\n }\n\n private diagnose(\n type: import('./modal-types').ModalDiagnostic['type'],\n id: string,\n ref: string | null,\n reason?: string,\n ): void {\n if (!this.config?.diagnostics) return;\n this._diagnostics.push({ seq: ++this._diagSeq, type, id, ref, reason });\n while (this._diagnostics.length > DIAGNOSTIC_CAP) this._diagnostics.shift();\n }\n\n private resolveStrategy(configured: RiviniaModalConfig['strategy']): ModalStrategy {\n if (!configured || configured === 'stack') return new StackStrategy();\n if (configured === 'queue') return new QueueStrategy();\n return configured;\n }\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { RIVINIA_MODAL_CONFIG, type RiviniaModalConfig } from './modal-config';\nimport { RiviniaModal } from './rivinia-modal';\n\n/**\n * Configures `@rivinia/modal`. The manager is a root singleton; call once at\n * bootstrap. Per-open options always win over these globals. No styling,\n * positions, or icons here — ever.\n *\n * @example\n * ```ts\n * provideRiviniaModal({ strategy: 'stack', maxVisible: 3 })\n * ```\n */\nexport function provideRiviniaModal(config: RiviniaModalConfig = {}): EnvironmentProviders {\n return makeEnvironmentProviders([{ provide: RIVINIA_MODAL_CONFIG, useValue: config }]);\n}\n\n/** Shorthand for `inject(RiviniaModal)`. */\nexport function injectRiviniaModal(): RiviniaModal {\n return inject(RiviniaModal);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "@rivinia/modal",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Headless modal manager for Angular: stack/queue strategies, typed results, guards, ownership.",
5
+ "keywords": [
6
+ "angular",
7
+ "modal",
8
+ "dialog",
9
+ "signals",
10
+ "typescript"
11
+ ],
12
+ "license": "MIT",
13
+ "peerDependencies": {
14
+ "@angular/core": "^22.0.0"
15
+ },
16
+ "sideEffects": false,
17
+ "module": "fesm2022/rivinia-modal.mjs",
18
+ "typings": "types/rivinia-modal.d.ts",
19
+ "exports": {
20
+ "./package.json": {
21
+ "default": "./package.json"
22
+ },
23
+ ".": {
24
+ "types": "./types/rivinia-modal.d.ts",
25
+ "default": "./fesm2022/rivinia-modal.mjs"
26
+ }
27
+ },
28
+ "type": "module",
29
+ "dependencies": {
30
+ "tslib": "^2.3.0"
31
+ }
6
32
  }
@@ -0,0 +1,340 @@
1
+ import * as i0 from "@angular/core";
2
+ import { DestroyRef, EnvironmentProviders, InjectionToken, TemplateRef, Type } from "@angular/core";
3
+ /**
4
+ * Ordering/admission policy. Strategies are pure with respect to lifecycle:
5
+ * the manager owns identity, results, timers and disposal; strategies own
6
+ * ordering and activation width. No mode-dependent `if/else` in the manager.
7
+ */
8
+ interface ModalStrategy {
9
+ readonly name: string;
10
+ /** Maximum simultaneously active instances under this strategy. */
11
+ maxActive(maxVisible: number): number;
12
+ /**
13
+ * Admission of a new request given current visible/queued sets.
14
+ * Returns the action; the manager executes it (and settles results).
15
+ */
16
+ admit(visibleCount: number, queuedCount: number, maxVisible: number): 'activate' | 'queue';
17
+ /** Display order of visible instances, bottom-first. */
18
+ order<T extends {
19
+ readonly openedSeq: number;
20
+ }>(visible: readonly T[]): readonly T[];
21
+ }
22
+ /** Multiple concurrent modals, topmost (last opened) interactive. */
23
+ export declare class StackStrategy implements ModalStrategy {
24
+ readonly name = "stack";
25
+ maxActive(maxVisible: number): number;
26
+ admit(visibleCount: number, _queuedCount: number, maxVisible: number): 'activate' | 'queue';
27
+ order<T extends {
28
+ readonly openedSeq: number;
29
+ }>(visible: readonly T[]): readonly T[];
30
+ }
31
+ /** Strictly serial: one active modal, FIFO activation of the rest. */
32
+ export declare class QueueStrategy implements ModalStrategy {
33
+ readonly name = "queue";
34
+ maxActive(_maxVisible: number): number;
35
+ admit(visibleCount: number, _queuedCount: number, _maxVisible: number): 'activate' | 'queue';
36
+ order<T extends {
37
+ readonly openedSeq: number;
38
+ }>(visible: readonly T[]): readonly T[];
39
+ }
40
+ /**
41
+ * Discriminated result of a modal request. Every terminal path settles exactly
42
+ * once — infrastructure failures reject the promise instead of becoming variants.
43
+ */
44
+ type ModalResult<T> = {
45
+ readonly status: 'confirmed';
46
+ readonly value: T;
47
+ } | {
48
+ readonly status: 'cancelled';
49
+ readonly reason?: string;
50
+ } | {
51
+ readonly status: 'dismissed';
52
+ readonly reason?: string;
53
+ } | {
54
+ readonly status: 'owner-destroyed';
55
+ } | {
56
+ readonly status: 'queued-cancelled';
57
+ } | {
58
+ readonly status: 'rejected';
59
+ readonly reason?: string;
60
+ };
61
+ /** Lifecycle state of one modal instance. Single source of truth: the manager. */
62
+ type ModalStatus = 'queued' | 'active' | 'closing' | 'closed';
63
+ /** Origin of a dismissal intent. Guards see it; forced paths bypass guards. */
64
+ type ModalDismissOrigin = 'escape' | 'backdrop' | 'button' | 'program' | 'system';
65
+ /** Content a modal renders. Core stores the descriptor; UI interprets it. */
66
+ type ModalContent<TData> = {
67
+ readonly kind: 'template';
68
+ readonly template: TemplateRef<ModalTemplateContext<TData>>;
69
+ } | {
70
+ readonly kind: 'component';
71
+ readonly component: Type<unknown>;
72
+ readonly inputs?: Record<string, unknown>;
73
+ };
74
+ /** Template context for `TemplateRef` content. */
75
+ interface ModalTemplateContext<TData> {
76
+ /** Typed input data (`let-data`). */
77
+ readonly $implicit: TData;
78
+ /** Modal operations (`let-modal="modal"`). */
79
+ readonly modal: ModalContext<TData>;
80
+ }
81
+ /**
82
+ * Operations exposed to modal content (injected token value for components,
83
+ * template context for templates). Scoped to ONE instance — never global.
84
+ */
85
+ interface ModalContext<TData> {
86
+ readonly id: string;
87
+ readonly data: TData;
88
+ readonly parentId: string | null;
89
+ close(result?: unknown): boolean;
90
+ dismiss(reason?: string): boolean;
91
+ }
92
+ /**
93
+ * Guard hook run by `requestDismiss` (user intents: escape/backdrop/button,
94
+ * or explicit programmatic review). Return `false` (or a rejecting promise)
95
+ * to deny. Throwing denies safely. Never blocks forced disposal.
96
+ */
97
+ type ModalGuard = (context: {
98
+ readonly id: string;
99
+ readonly origin: ModalDismissOrigin;
100
+ }) => boolean | Promise<boolean>;
101
+ /** Dismissal policy: global defaults, per-open overrides win. */
102
+ interface ModalDismissPolicy {
103
+ /** Escape closes the topmost eligible modal. @default true */
104
+ readonly closeOnEscape?: boolean;
105
+ /** Backdrop click behavior. @default 'close' */
106
+ readonly backdrop?: 'close' | 'static' | 'disabled';
107
+ }
108
+ /** Overflow behavior when capacity is full. @default 'queue' (lossless). */
109
+ type ModalOverflowPolicy = 'dismiss-oldest' | 'queue' | 'reject';
110
+ /** What closing a parent does to its descendants. */
111
+ type DescendantPolicy = 'close-with-parent' | 'prevent-parent-close';
112
+ /** Public, UI-consumable snapshot of one modal instance. */
113
+ interface ModalInstance<TData = unknown> {
114
+ readonly id: string;
115
+ /** Stable key for a modal definition (e.g. `'confirm-delete'`). */
116
+ readonly ref: string | null;
117
+ /** Request origin label for diagnostics (e.g. component/feature name). */
118
+ readonly caller: string | null;
119
+ readonly parentId: string | null;
120
+ readonly data: TData;
121
+ readonly content: ModalContent<TData>;
122
+ readonly status: ModalStatus;
123
+ readonly dismiss: ModalDismissPolicy;
124
+ readonly metadata: Readonly<Record<string, unknown>>;
125
+ }
126
+ /**
127
+ * Caller-side handle. `result` settles exactly once per the manager contract;
128
+ * repeated terminal calls are safe no-ops returning `false`.
129
+ */
130
+ interface ModalHandle<TResult = unknown> {
131
+ readonly id: string;
132
+ readonly result: Promise<ModalResult<TResult>>;
133
+ close(result?: TResult): boolean;
134
+ dismiss(reason?: string): boolean;
135
+ cancel(reason?: string): boolean;
136
+ }
137
+ /** Arguments to `openModal()`. */
138
+ interface ModalOpenOptions<TData = unknown> {
139
+ readonly data?: TData;
140
+ readonly ref?: string;
141
+ readonly caller?: string;
142
+ readonly parentId?: string;
143
+ /**
144
+ * Lifecycle owner. Destruction settles active modals as `owner-destroyed`
145
+ * and queued ones as `queued-cancelled`. Default: independent lifetime.
146
+ */
147
+ readonly owner?: DestroyRef;
148
+ /** Named preset from global config (explicit options always win). */
149
+ readonly preset?: string;
150
+ readonly dismiss?: Partial<ModalDismissPolicy>;
151
+ readonly guards?: readonly ModalGuard[];
152
+ readonly metadata?: Record<string, unknown>;
153
+ }
154
+ /** Partial changes for `update()` (content shape itself is immutable). */
155
+ interface ModalUpdate<TData = unknown> {
156
+ readonly data?: TData;
157
+ readonly metadata?: Record<string, unknown>;
158
+ }
159
+ /** Bounded diagnostic record. Never carries payloads. */
160
+ interface ModalDiagnostic {
161
+ readonly seq: number;
162
+ readonly type: 'opened' | 'activated' | 'queued' | 'promoted' | 'closing' | 'closed' | 'dismissed' | 'rejected' | 'guard-denied' | 'strategy-rejected';
163
+ readonly id: string;
164
+ readonly ref: string | null;
165
+ readonly reason?: string;
166
+ }
167
+ /** Named preset: a reusable dismissal shape (explicit options always win). */
168
+ interface ModalPreset {
169
+ readonly dismiss?: Partial<ModalDismissPolicy>;
170
+ }
171
+ /**
172
+ * Global configuration for `@rivinia/modal`. Everything optional; per-open
173
+ * options win. No styling, positions, icons, or translations here — ever.
174
+ */
175
+ interface RiviniaModalConfig {
176
+ /**
177
+ * Ordering/activation policy. `'stack'` = up to `maxVisible` concurrent,
178
+ * topmost interactive. `'queue'` = strictly serial (one active, FIFO rest).
179
+ * A custom `ModalStrategy` instance is also accepted.
180
+ * @default 'stack'
181
+ */
182
+ readonly strategy?: 'stack' | 'queue' | ModalStrategy;
183
+ /** Global dismissal defaults. @default { closeOnEscape: true, backdrop: 'close' } */
184
+ readonly dismiss?: ModalDismissPolicy;
185
+ /** Capacity of simultaneously active modals. Clamped to ≥ 1. @default 5 */
186
+ readonly maxVisible?: number;
187
+ /** Bound for queued requests (queue policy). @default Infinity */
188
+ readonly maxQueue?: number;
189
+ /** Overflow behavior when full. @default 'queue' (lossless — never drops input) */
190
+ readonly overflow?: ModalOverflowPolicy;
191
+ /** Parent closure with remaining descendants. @default 'close-with-parent' */
192
+ readonly descendants?: DescendantPolicy;
193
+ /** Named dismissal presets referenced by `openModal({ preset })`. */
194
+ readonly presets?: Record<string, ModalPreset>;
195
+ /**
196
+ * Time the manager waits for UI teardown confirmation (`confirmClosed`)
197
+ * before auto-finalizing a `closing` instance. `0` = manual only.
198
+ * @default 300
199
+ */
200
+ readonly closingTimeout?: number;
201
+ /**
202
+ * Bounded transition log for debugging (ring buffer, no payloads).
203
+ * Off by default to stay production-light.
204
+ * @default false
205
+ */
206
+ readonly diagnostics?: boolean;
207
+ }
208
+ /**
209
+ * Internal configuration token. Consumers must use `provideRiviniaModal()`.
210
+ * @internal
211
+ */
212
+ declare const RIVINIA_MODAL_CONFIG: InjectionToken<RiviniaModalConfig>;
213
+ declare const DEFAULT_MAX_VISIBLE = 5;
214
+ declare const DEFAULT_CLOSING_TIMEOUT = 300;
215
+ declare const DIAGNOSTIC_CAP = 100;
216
+ /**
217
+ * Injection token providing the `ModalContext` of the enclosing modal to
218
+ * dynamically created components. Template content receives the same object
219
+ * as `let-modal="modal"`. Never global — one value per modal instance.
220
+ */
221
+ export declare const RIVINIA_MODAL_CONTEXT: InjectionToken<ModalContext<unknown>>;
222
+ /**
223
+ * Headless modal manager. Owns identity, lifecycle coordination, result
224
+ * settlement and diagnostics. Ordering/admission policy is delegated to a
225
+ * `ModalStrategy` (Stack/Queue/custom) — the manager never branches on mode.
226
+ *
227
+ * Settlement contract (each path settles exactly once, never hangs):
228
+ * - `close()` on active → `confirmed` · `dismiss()` on active → `dismissed`
229
+ * - `cancel()` on active → `cancelled` · on queued → `queued-cancelled`
230
+ * - `dismiss()` on queued → `dismissed` with the reason (pre-activation)
231
+ * - overflow/guard refusal → `rejected` · owner gone → `owner-destroyed`
232
+ * - forced paths (owner, overflow eviction, `dismissAll`) bypass guards
233
+ * - `close/dismiss/cancel` are guard-free primitives; `requestDismiss` runs
234
+ * guards for user intents (escape/backdrop/button) or explicit review
235
+ * - infrastructure failures reject the promise — never a result variant
236
+ *
237
+ * Results settle at dismissal initiation, not after UI teardown: animations
238
+ * never delay callers. The `closing` → `closed` handshake (`confirmClosed`
239
+ * or `closingTimeout` auto-finalize) exists only for exit animations.
240
+ */
241
+ export declare class RiviniaModal {
242
+ private readonly config;
243
+ private readonly _instances;
244
+ private readonly _results;
245
+ private _strategy;
246
+ private _seq;
247
+ private _diagSeq;
248
+ private readonly _diagnostics;
249
+ /** Active instances in strategy display order (bottom-first). */
250
+ readonly active: import("@angular/core").Signal<readonly ModalInstance<unknown>[]>;
251
+ /** Queued instances, FIFO admission order. */
252
+ readonly queued: import("@angular/core").Signal<readonly ModalInstance<unknown>[]>;
253
+ /** Topmost active instance (the only Escape-eligible one), if any. */
254
+ readonly top: import("@angular/core").Signal<ModalInstance<unknown> | null>;
255
+ constructor();
256
+ openModal<TData = unknown, TResult = unknown>(content: ModalContent<TData>, options?: ModalOpenOptions<TData>): ModalHandle<TResult>;
257
+ /**
258
+ * Confirms with a value. Active instances only; settles `confirmed`.
259
+ * Blocked (returns `false`) by the prevent-parent-close policy.
260
+ */
261
+ close(id: string, value?: unknown): boolean;
262
+ /**
263
+ * Guard-free dismissal primitive. Active → `dismissed`; queued → removed as
264
+ * `dismissed` with the reason. Blocked by prevent-parent-close.
265
+ */
266
+ dismiss(id: string, reason?: string): boolean;
267
+ /**
268
+ * Cancels: active → `cancelled`, queued → removed as `queued-cancelled`.
269
+ * Blocked by prevent-parent-close. Closing/closed/unknown → `false`.
270
+ */
271
+ cancel(id: string, reason?: string): boolean;
272
+ /**
273
+ * Guarded dismissal for user intents (escape/backdrop/button) or explicit
274
+ * programmatic review. Guards run sequentially; any `false` or throw denies
275
+ * (stays open + diagnostic). Stale guard completions are ignored.
276
+ */
277
+ requestDismiss(id: string, origin: ModalDismissOrigin, reason?: string): Promise<boolean>;
278
+ /** Merges data/metadata (new object identity so UI re-renders). */
279
+ update<TData>(id: string, changes: ModalUpdate<TData>): boolean;
280
+ /** UI teardown confirmation after exit animations. Auto-finalizes via timeout. */
281
+ confirmClosed(id: string): boolean;
282
+ /** Forced cleanup (bypasses guards): active → `dismissed`, queued → `queued-cancelled`. */
283
+ dismissAll(reason?: string): void;
284
+ /** Non-closed snapshots for a definition ref (e.g. avoid duplicate confirms). */
285
+ findByRef(ref: string): ModalInstance[];
286
+ /**
287
+ * Swaps the strategy. Rejected with an explicit error while any instance is
288
+ * not closed — silent live migration is never attempted.
289
+ */
290
+ useStrategy(strategy: ModalStrategy): void;
291
+ /** Introspection snapshot. Active means interactive; closing instances are
292
+ * still rendered (exit animations) until `confirmClosed`/timeout finalizes. */
293
+ debugState(): {
294
+ strategy: string;
295
+ active: string[];
296
+ closing: string[];
297
+ queued: string[];
298
+ };
299
+ /** Bounded transition log (only when `diagnostics: true`). */
300
+ getDiagnostics(): readonly ModalDiagnostic[];
301
+ clearDiagnostics(): void;
302
+ private get;
303
+ private queuedCount;
304
+ private orderedActive;
305
+ private maxVisible;
306
+ private maxQueue;
307
+ private presetDismiss;
308
+ private knownParent;
309
+ /** Overflow handling for queue-intent admissions. Returns rejection reason, or the resulting status. */
310
+ private admitQueued;
311
+ private blockedByDescendants;
312
+ private beginClosing;
313
+ private closeDescendants;
314
+ private finalize;
315
+ private promote;
316
+ private remove;
317
+ private clearClosingTimer;
318
+ private teardownOwned;
319
+ private patch;
320
+ private deferred;
321
+ private settle;
322
+ private diagnose;
323
+ private resolveStrategy;
324
+ static ɵfac: i0.ɵɵFactoryDeclaration<RiviniaModal, never>;
325
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
326
+ }
327
+ /**
328
+ * Configures `@rivinia/modal`. The manager is a root singleton; call once at
329
+ * bootstrap. Per-open options always win over these globals. No styling,
330
+ * positions, or icons here — ever.
331
+ *
332
+ * @example
333
+ * ```ts
334
+ * provideRiviniaModal({ strategy: 'stack', maxVisible: 3 })
335
+ * ```
336
+ */
337
+ export declare function provideRiviniaModal(config?: RiviniaModalConfig): EnvironmentProviders;
338
+ /** Shorthand for `inject(RiviniaModal)`. */
339
+ export declare function injectRiviniaModal(): RiviniaModal;
340
+ export type { DescendantPolicy, ModalContent, ModalContext, ModalDiagnostic, ModalDismissOrigin, ModalDismissPolicy, ModalGuard, ModalHandle, ModalInstance, ModalOpenOptions, ModalOverflowPolicy, ModalPreset, ModalResult, ModalStatus, ModalStrategy, ModalTemplateContext, ModalUpdate, RiviniaModalConfig };