@styloviz/modal 0.1.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/LICENSE +21 -0
- package/README.md +70 -0
- package/fesm2022/styloviz-modal.mjs +363 -0
- package/fesm2022/styloviz-modal.mjs.map +1 -0
- package/package.json +42 -0
- package/types/styloviz-modal.d.ts +167 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StyloViz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @styloviz/modal
|
|
2
|
+
|
|
3
|
+
> Premium dialog overlay with 6 semantic variants, 7 sizes, bottom-sheet & loading states, slot-based projection, focus trap and ESC/backdrop close.
|
|
4
|
+
|
|
5
|
+
Part of the **StyloViz UI Kit** — a premium Angular 21 + Tailwind CSS 4 dashboard component library. Standalone, `OnPush`, strongly typed, dark-mode ready.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @styloviz/core @styloviz/modal
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires `@angular/core` and `@angular/common` >= 21. `@styloviz/core` is a peer dependency shared by every component. Prefer everything at once? `@styloviz/all` installs the whole free tier in one command.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { SvModalComponent } from '@styloviz/modal';
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
standalone: true,
|
|
22
|
+
imports: [SvModalComponent],
|
|
23
|
+
template: `
|
|
24
|
+
<sv-modal [(isOpen)]="isOpen" title="Edit profile" subtitle="Update your details" (closed)="isOpen = false">
|
|
25
|
+
<ng-container slot="body"><p>Body content…</p></ng-container>
|
|
26
|
+
</sv-modal>
|
|
27
|
+
`,
|
|
28
|
+
})
|
|
29
|
+
export class DemoComponent {}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Inputs
|
|
33
|
+
|
|
34
|
+
| Input | Type | Default | Description |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| `isOpen` | `boolean (two-way)` | `false` | Controls open/closed state. Use `[(isOpen)]` for two-way binding. |
|
|
37
|
+
| `title` | `string` | `''` | Dialog title shown in the built-in header. |
|
|
38
|
+
| `subtitle` | `string` | `''` | Optional secondary line rendered beneath the title (also used for aria-describedby). |
|
|
39
|
+
| `variant` | `ModalVariant` | `'default'` | Semantic/visual variant. Drives the header icon badge + accent bar. |
|
|
40
|
+
| `showHeader` | `boolean` | `true` | Render the built-in header bar. |
|
|
41
|
+
| `showHeaderDivider` | `boolean` | `true` | Bottom dividing line under the header. |
|
|
42
|
+
| `showFooterDivider` | `boolean` | `true` | Top dividing line above the footer. |
|
|
43
|
+
| `showFooter` | `boolean` | `true` | Render the footer slot wrapper. |
|
|
44
|
+
| `size` | `ModalSize` | `'md'` | Size of the dialog panel. |
|
|
45
|
+
| `position` | `ModalPosition` | `'center'` | Placement of the dialog panel. |
|
|
46
|
+
| `showIcon` | `boolean` | `true` | Show the built-in variant icon badge in the header. |
|
|
47
|
+
| `closeOnBackdrop` | `boolean` | `true` | Close dialog when backdrop is clicked. |
|
|
48
|
+
| `closeOnEsc` | `boolean` | `true` | Close dialog on ESC key. |
|
|
49
|
+
| `backdropBlur` | `boolean` | `false` | Apply a frosted-glass blur to the backdrop. |
|
|
50
|
+
| `showCloseButton` | `boolean` | `true` | Show the built-in × close button in the header. |
|
|
51
|
+
| `loading` | `boolean` | `false` | Busy state. While `true` the panel shows a spinner overlay and all close paths (button, backdrop, ESC) are blocked so a running task can finish. |
|
|
52
|
+
| `loadingText` | `string` | `'Loading…'` | Accessible label announced while `loading` is true. |
|
|
53
|
+
| `initialFocus` | `string` | `''` | CSS selector for the element to focus first when the modal opens (e.g. `'#name-input'`). Falls back to the first focusable element. |
|
|
54
|
+
| `customClass` | `string` | `''` | Extra CSS classes merged onto the dialog panel. |
|
|
55
|
+
|
|
56
|
+
## Outputs
|
|
57
|
+
|
|
58
|
+
| Output | Type | Description |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `opened` | `void` | Emitted after the modal is opened (by any means). |
|
|
61
|
+
| `closed` | `void` | Emitted after the modal is closed (by any means). |
|
|
62
|
+
| `dismissed` | `ModalCloseReason` | Emitted alongside `closed`, describing what triggered the close. |
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
Full API reference and live demos: https://styloviz.dev/docs/modal
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, PLATFORM_ID, ApplicationRef, viewChild, model, input, output, computed, effect, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { DOCUMENT, isPlatformBrowser, NgClass } from '@angular/common';
|
|
4
|
+
|
|
5
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* ModalComponent — A premium, fully-featured dialog overlay.
|
|
8
|
+
*
|
|
9
|
+
* The backdrop + panel are rendered into `document.body` through a lightweight
|
|
10
|
+
* Angular embedded-view portal, so the modal always covers the full viewport
|
|
11
|
+
* and escapes any parent `overflow:hidden`/`auto` or CSS `transform`/`filter`
|
|
12
|
+
* containment — it is never clipped to or trapped inside a card/wrapper.
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Signal-based open/close via two-way `isOpen` model
|
|
16
|
+
* - Portaled to `<body>` so `position:fixed` is always viewport-relative
|
|
17
|
+
* - 6 semantic variants: default · primary · success · warning · danger · info
|
|
18
|
+
* (each renders a tinted icon badge + accent bar in the header)
|
|
19
|
+
* - 7 sizes: xs · sm · md · lg · xl · 2xl · full
|
|
20
|
+
* - 3 positions: center (default) · top · bottom (mobile-friendly sheet)
|
|
21
|
+
* - Header (title + optional subtitle) / body / footer content projection
|
|
22
|
+
* - Optional loading state that blocks close and shows a spinner overlay
|
|
23
|
+
* - Backdrop click to close (configurable) + optional backdrop blur
|
|
24
|
+
* - ESC key to close (configurable)
|
|
25
|
+
* - Scroll lock on `<body>` with scrollbar-width compensation (no layout shift)
|
|
26
|
+
* - Focus trap within the dialog + configurable initial focus target
|
|
27
|
+
* - `aria-labelledby` / `aria-describedby` wiring for screen readers
|
|
28
|
+
* - Slide-in + fade animation (direction follows `position`)
|
|
29
|
+
* - Full dark-mode support
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* ```html
|
|
33
|
+
* <sv-modal
|
|
34
|
+
* [(isOpen)]="showDialog"
|
|
35
|
+
* variant="danger"
|
|
36
|
+
* title="Delete project"
|
|
37
|
+
* subtitle="This action is permanent">
|
|
38
|
+
* <ng-container slot="body">Are you sure?</ng-container>
|
|
39
|
+
* <ng-container slot="footer">
|
|
40
|
+
* <button (click)="showDialog = false">Cancel</button>
|
|
41
|
+
* <button (click)="onConfirm()">Delete</button>
|
|
42
|
+
* </ng-container>
|
|
43
|
+
* </sv-modal>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
class SvModalComponent {
|
|
47
|
+
platformId = inject(PLATFORM_ID);
|
|
48
|
+
appRef = inject(ApplicationRef);
|
|
49
|
+
document = inject(DOCUMENT);
|
|
50
|
+
// ── Query refs ────────────────────────────────────────────────────────────
|
|
51
|
+
/** The ng-template holding the overlay markup — instantiated into <body> on open. */
|
|
52
|
+
modalTpl = viewChild('modalTpl', ...(ngDevMode ? [{ debugName: "modalTpl" }] : /* istanbul ignore next */ []));
|
|
53
|
+
// ── Inputs ────────────────────────────────────────────────────────────────
|
|
54
|
+
/** Controls open/closed state. Use `[(isOpen)]` for two-way binding. */
|
|
55
|
+
isOpen = model(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
56
|
+
/** Dialog title shown in the built-in header. */
|
|
57
|
+
title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
58
|
+
/** Optional secondary line rendered beneath the title (also used for aria-describedby). */
|
|
59
|
+
subtitle = input('', ...(ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []));
|
|
60
|
+
/** Semantic/visual variant. Drives the header icon badge + accent bar. @default 'default' */
|
|
61
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
62
|
+
/** Render the built-in header bar. @default true */
|
|
63
|
+
showHeader = input(true, ...(ngDevMode ? [{ debugName: "showHeader" }] : /* istanbul ignore next */ []));
|
|
64
|
+
/** Bottom dividing line under the header. @default true */
|
|
65
|
+
showHeaderDivider = input(true, ...(ngDevMode ? [{ debugName: "showHeaderDivider" }] : /* istanbul ignore next */ []));
|
|
66
|
+
/** Top dividing line above the footer. @default true */
|
|
67
|
+
showFooterDivider = input(true, ...(ngDevMode ? [{ debugName: "showFooterDivider" }] : /* istanbul ignore next */ []));
|
|
68
|
+
/** Render the footer slot wrapper. @default true */
|
|
69
|
+
showFooter = input(true, ...(ngDevMode ? [{ debugName: "showFooter" }] : /* istanbul ignore next */ []));
|
|
70
|
+
/** Size of the dialog panel. @default 'md' */
|
|
71
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
72
|
+
/** Placement of the dialog panel. @default 'center' */
|
|
73
|
+
position = input('center', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
74
|
+
/** Show the built-in variant icon badge in the header. @default true */
|
|
75
|
+
showIcon = input(true, ...(ngDevMode ? [{ debugName: "showIcon" }] : /* istanbul ignore next */ []));
|
|
76
|
+
/** Close dialog when backdrop is clicked. @default true */
|
|
77
|
+
closeOnBackdrop = input(true, ...(ngDevMode ? [{ debugName: "closeOnBackdrop" }] : /* istanbul ignore next */ []));
|
|
78
|
+
/** Close dialog on ESC key. @default true */
|
|
79
|
+
closeOnEsc = input(true, ...(ngDevMode ? [{ debugName: "closeOnEsc" }] : /* istanbul ignore next */ []));
|
|
80
|
+
/** Apply a frosted-glass blur to the backdrop. @default false */
|
|
81
|
+
backdropBlur = input(false, ...(ngDevMode ? [{ debugName: "backdropBlur" }] : /* istanbul ignore next */ []));
|
|
82
|
+
/** Show the built-in × close button in the header. @default true */
|
|
83
|
+
showCloseButton = input(true, ...(ngDevMode ? [{ debugName: "showCloseButton" }] : /* istanbul ignore next */ []));
|
|
84
|
+
/**
|
|
85
|
+
* Busy state. While `true` the panel shows a spinner overlay and all close
|
|
86
|
+
* paths (button, backdrop, ESC) are blocked so a running task can finish.
|
|
87
|
+
* @default false
|
|
88
|
+
*/
|
|
89
|
+
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
90
|
+
/** Accessible label announced while `loading` is true. @default 'Loading…' */
|
|
91
|
+
loadingText = input('Loading…', ...(ngDevMode ? [{ debugName: "loadingText" }] : /* istanbul ignore next */ []));
|
|
92
|
+
/**
|
|
93
|
+
* CSS selector for the element to focus first when the modal opens
|
|
94
|
+
* (e.g. `'#name-input'`). Falls back to the first focusable element.
|
|
95
|
+
*/
|
|
96
|
+
initialFocus = input('', ...(ngDevMode ? [{ debugName: "initialFocus" }] : /* istanbul ignore next */ []));
|
|
97
|
+
/** Extra CSS classes merged onto the dialog panel. */
|
|
98
|
+
customClass = input('', ...(ngDevMode ? [{ debugName: "customClass" }] : /* istanbul ignore next */ []));
|
|
99
|
+
// ── Outputs ───────────────────────────────────────────────────────────────
|
|
100
|
+
/** Emitted after the modal is opened (by any means). */
|
|
101
|
+
opened = output();
|
|
102
|
+
/** Emitted after the modal is closed (by any means). */
|
|
103
|
+
closed = output();
|
|
104
|
+
/** Emitted alongside `closed`, describing what triggered the close. */
|
|
105
|
+
dismissed = output();
|
|
106
|
+
// ── Stable element ids (a11y wiring) ───────────────────────────────────────
|
|
107
|
+
uid = Math.random().toString(36).slice(2, 8);
|
|
108
|
+
/** Stable id for the built-in title, used by `aria-labelledby`. */
|
|
109
|
+
titleId = `sv-modal-title-${this.uid}`;
|
|
110
|
+
/** Stable id for the subtitle, used by `aria-describedby`. */
|
|
111
|
+
subtitleId = `sv-modal-desc-${this.uid}`;
|
|
112
|
+
// ── Computed ──────────────────────────────────────────────────────────────
|
|
113
|
+
panelClass = computed(() => {
|
|
114
|
+
const sizeMap = {
|
|
115
|
+
xs: 'w-full max-w-xs',
|
|
116
|
+
sm: 'w-full max-w-sm',
|
|
117
|
+
md: 'w-full max-w-md',
|
|
118
|
+
lg: 'w-full max-w-lg',
|
|
119
|
+
xl: 'w-full max-w-2xl',
|
|
120
|
+
'2xl': 'w-full max-w-4xl',
|
|
121
|
+
full: 'w-screen h-screen max-w-none rounded-none',
|
|
122
|
+
};
|
|
123
|
+
return sizeMap[this.size()];
|
|
124
|
+
}, ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
|
|
125
|
+
/** Alignment of the panel inside the flex overlay. */
|
|
126
|
+
alignClass = computed(() => {
|
|
127
|
+
switch (this.position()) {
|
|
128
|
+
case 'top':
|
|
129
|
+
return 'items-start pt-16';
|
|
130
|
+
case 'bottom':
|
|
131
|
+
return 'items-end pb-0 sm:pb-6';
|
|
132
|
+
default:
|
|
133
|
+
return 'items-center';
|
|
134
|
+
}
|
|
135
|
+
}, ...(ngDevMode ? [{ debugName: "alignClass" }] : /* istanbul ignore next */ []));
|
|
136
|
+
/** Bottom position becomes a full-width sheet on small screens. */
|
|
137
|
+
positionPanelClass = computed(() => this.position() === 'bottom' && this.size() !== 'full'
|
|
138
|
+
? 'rounded-b-none sm:rounded-2xl'
|
|
139
|
+
: '', ...(ngDevMode ? [{ debugName: "positionPanelClass" }] : /* istanbul ignore next */ []));
|
|
140
|
+
/** Animation class — bottom slides up, others slide down + fade. */
|
|
141
|
+
animationClass = computed(() => this.position() === 'bottom' ? 'modal-panel modal-panel--up' : 'modal-panel', ...(ngDevMode ? [{ debugName: "animationClass" }] : /* istanbul ignore next */ []));
|
|
142
|
+
/** Accent classes for the header icon badge, keyed by variant. */
|
|
143
|
+
iconAccentClass = computed(() => {
|
|
144
|
+
const map = {
|
|
145
|
+
default: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300',
|
|
146
|
+
primary: 'bg-primary-50 text-primary-600 dark:bg-primary-500/15 dark:text-primary-400',
|
|
147
|
+
success: 'bg-emerald-50 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400',
|
|
148
|
+
warning: 'bg-amber-50 text-amber-600 dark:bg-amber-500/15 dark:text-amber-400',
|
|
149
|
+
danger: 'bg-red-50 text-red-600 dark:bg-red-500/15 dark:text-red-400',
|
|
150
|
+
info: 'bg-sky-50 text-sky-600 dark:bg-sky-500/15 dark:text-sky-400',
|
|
151
|
+
};
|
|
152
|
+
return map[this.variant()];
|
|
153
|
+
}, ...(ngDevMode ? [{ debugName: "iconAccentClass" }] : /* istanbul ignore next */ []));
|
|
154
|
+
/** Thin accent bar across the top of the panel for semantic variants. */
|
|
155
|
+
accentBarClass = computed(() => {
|
|
156
|
+
const map = {
|
|
157
|
+
default: '',
|
|
158
|
+
primary: 'bg-primary-500',
|
|
159
|
+
success: 'bg-emerald-500',
|
|
160
|
+
warning: 'bg-amber-500',
|
|
161
|
+
danger: 'bg-red-500',
|
|
162
|
+
info: 'bg-sky-500',
|
|
163
|
+
};
|
|
164
|
+
return map[this.variant()];
|
|
165
|
+
}, ...(ngDevMode ? [{ debugName: "accentBarClass" }] : /* istanbul ignore next */ []));
|
|
166
|
+
/** Whether the header icon badge should render. */
|
|
167
|
+
hasIcon = computed(() => this.showIcon() && this.variant() !== 'default', ...(ngDevMode ? [{ debugName: "hasIcon" }] : /* istanbul ignore next */ []));
|
|
168
|
+
/** True when a description should be linked via aria-describedby. */
|
|
169
|
+
hasDescription = computed(() => this.subtitle().length > 0, ...(ngDevMode ? [{ debugName: "hasDescription" }] : /* istanbul ignore next */ []));
|
|
170
|
+
// ── Portal + focus state ──────────────────────────────────────────────────
|
|
171
|
+
/** Container appended to <body> that hosts the overlay while open. */
|
|
172
|
+
portalEl = null;
|
|
173
|
+
/** Embedded view holding the overlay template nodes. */
|
|
174
|
+
portalView = null;
|
|
175
|
+
/** Element focused immediately before the modal opened, restored on close. */
|
|
176
|
+
previouslyFocused = null;
|
|
177
|
+
/** Tracks the prior open state so the effect can emit opened/closed on transitions. */
|
|
178
|
+
wasOpen = false;
|
|
179
|
+
constructor() {
|
|
180
|
+
// React to isOpen changes regardless of how it is set:
|
|
181
|
+
// either via open()/close() methods OR parent [(isOpen)] binding.
|
|
182
|
+
effect(() => {
|
|
183
|
+
const open = this.isOpen();
|
|
184
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
185
|
+
this.wasOpen = open;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (open && !this.wasOpen) {
|
|
189
|
+
// Opening transition
|
|
190
|
+
this.previouslyFocused = this.document.activeElement;
|
|
191
|
+
this.lockScroll();
|
|
192
|
+
this.opened.emit();
|
|
193
|
+
// Mount + focus after the current change-detection pass settles.
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
if (!this.isOpen())
|
|
196
|
+
return;
|
|
197
|
+
this.mountPortal();
|
|
198
|
+
this.applyInitialFocus();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else if (!open && this.wasOpen) {
|
|
202
|
+
// Closing transition (covers close() AND parent [(isOpen)] = false)
|
|
203
|
+
this.destroyPortal();
|
|
204
|
+
this.unlockScroll();
|
|
205
|
+
this.closed.emit();
|
|
206
|
+
this.dismissed.emit(this.pendingReason);
|
|
207
|
+
this.pendingReason = 'programmatic';
|
|
208
|
+
// Return focus to whatever triggered the modal for keyboard continuity
|
|
209
|
+
this.previouslyFocused?.focus?.();
|
|
210
|
+
this.previouslyFocused = null;
|
|
211
|
+
}
|
|
212
|
+
this.wasOpen = open;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
216
|
+
ngOnDestroy() {
|
|
217
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
218
|
+
this.destroyPortal();
|
|
219
|
+
this.unlockScroll();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ── Public open / close ───────────────────────────────────────────────────
|
|
223
|
+
/** Reason attached to the next close transition; reset after each emit. */
|
|
224
|
+
pendingReason = 'programmatic';
|
|
225
|
+
/** Open the modal programmatically. Scroll lock + focus trap handled by effect. */
|
|
226
|
+
open() {
|
|
227
|
+
this.isOpen.set(true);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Close the modal programmatically. Blocked while `loading` is true.
|
|
231
|
+
* The `closed` + `dismissed` outputs fire from the effect.
|
|
232
|
+
*/
|
|
233
|
+
close(reason = 'programmatic') {
|
|
234
|
+
if (this.loading())
|
|
235
|
+
return;
|
|
236
|
+
this.pendingReason = reason;
|
|
237
|
+
this.isOpen.set(false);
|
|
238
|
+
}
|
|
239
|
+
// ── Backdrop click ────────────────────────────────────────────────────────
|
|
240
|
+
onBackdropClick(event) {
|
|
241
|
+
if (!this.closeOnBackdrop() || this.loading())
|
|
242
|
+
return;
|
|
243
|
+
if (event.target === event.currentTarget) {
|
|
244
|
+
this.close('backdrop');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// ── Keyboard ─────────────────────────────────────────────────────────────
|
|
248
|
+
onKeydown(event) {
|
|
249
|
+
if (event.key === 'Escape' && this.closeOnEsc() && !this.loading()) {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
this.close('escape');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (event.key === 'Tab') {
|
|
255
|
+
this.handleTabKey(event);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// ── Portal lifecycle ──────────────────────────────────────────────────────
|
|
259
|
+
mountPortal() {
|
|
260
|
+
if (this.portalView)
|
|
261
|
+
return;
|
|
262
|
+
const tpl = this.modalTpl();
|
|
263
|
+
if (!tpl)
|
|
264
|
+
return;
|
|
265
|
+
this.portalEl = this.document.createElement('div');
|
|
266
|
+
this.portalEl.setAttribute('data-sv-modal-portal', '');
|
|
267
|
+
this.document.body.appendChild(this.portalEl);
|
|
268
|
+
// Escapes ALL parent overflow / transform / filter containment.
|
|
269
|
+
this.portalView = tpl.createEmbeddedView({});
|
|
270
|
+
this.appRef.attachView(this.portalView);
|
|
271
|
+
this.portalView.rootNodes.forEach((n) => this.portalEl.appendChild(n));
|
|
272
|
+
this.portalView.detectChanges();
|
|
273
|
+
}
|
|
274
|
+
destroyPortal() {
|
|
275
|
+
if (this.portalView) {
|
|
276
|
+
this.appRef.detachView(this.portalView);
|
|
277
|
+
this.portalView.destroy();
|
|
278
|
+
this.portalView = null;
|
|
279
|
+
}
|
|
280
|
+
this.portalEl?.remove();
|
|
281
|
+
this.portalEl = null;
|
|
282
|
+
}
|
|
283
|
+
// ── Scroll lock (with scrollbar-width compensation) ────────────────────────
|
|
284
|
+
lockScroll() {
|
|
285
|
+
const scrollbar = window.innerWidth - this.document.documentElement.clientWidth;
|
|
286
|
+
this.document.body.style.overflow = 'hidden';
|
|
287
|
+
if (scrollbar > 0)
|
|
288
|
+
this.document.body.style.paddingRight = `${scrollbar}px`;
|
|
289
|
+
}
|
|
290
|
+
unlockScroll() {
|
|
291
|
+
this.document.body.style.overflow = '';
|
|
292
|
+
this.document.body.style.paddingRight = '';
|
|
293
|
+
}
|
|
294
|
+
// ── Focus management ──────────────────────────────────────────────────────
|
|
295
|
+
/** The dialog panel element inside the body-mounted portal. */
|
|
296
|
+
getPanelEl() {
|
|
297
|
+
return this.portalEl?.querySelector('[role="dialog"]') ?? null;
|
|
298
|
+
}
|
|
299
|
+
applyInitialFocus() {
|
|
300
|
+
const el = this.getPanelEl();
|
|
301
|
+
if (!el)
|
|
302
|
+
return;
|
|
303
|
+
const selector = this.initialFocus();
|
|
304
|
+
if (selector) {
|
|
305
|
+
const target = el.querySelector(selector);
|
|
306
|
+
if (target) {
|
|
307
|
+
target.focus();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const focusable = this.getFocusableElements(el);
|
|
312
|
+
if (focusable.length > 0) {
|
|
313
|
+
focusable[0].focus();
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
el.focus();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
handleTabKey(event) {
|
|
320
|
+
const el = this.getPanelEl();
|
|
321
|
+
if (!el)
|
|
322
|
+
return;
|
|
323
|
+
const focusable = this.getFocusableElements(el);
|
|
324
|
+
if (focusable.length === 0) {
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
el.focus();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const first = focusable[0];
|
|
330
|
+
const last = focusable[focusable.length - 1];
|
|
331
|
+
const active = this.document.activeElement;
|
|
332
|
+
if (event.shiftKey) {
|
|
333
|
+
if (active === first || active === el) {
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
last.focus();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
if (active === last) {
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
first.focus();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
getFocusableElements(container) {
|
|
346
|
+
return Array.from(container.querySelectorAll('a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]')).filter(el => !el.hasAttribute('disabled') &&
|
|
347
|
+
el.getAttribute('aria-hidden') !== 'true' &&
|
|
348
|
+
el.offsetParent !== null);
|
|
349
|
+
}
|
|
350
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
351
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SvModalComponent, isStandalone: true, selector: "sv-modal", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, showHeaderDivider: { classPropertyName: "showHeaderDivider", publicName: "showHeaderDivider", isSignal: true, isRequired: false, transformFunction: null }, showFooterDivider: { classPropertyName: "showFooterDivider", publicName: "showFooterDivider", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, showIcon: { classPropertyName: "showIcon", publicName: "showIcon", isSignal: true, isRequired: false, transformFunction: null }, closeOnBackdrop: { classPropertyName: "closeOnBackdrop", publicName: "closeOnBackdrop", isSignal: true, isRequired: false, transformFunction: null }, closeOnEsc: { classPropertyName: "closeOnEsc", publicName: "closeOnEsc", isSignal: true, isRequired: false, transformFunction: null }, backdropBlur: { classPropertyName: "backdropBlur", publicName: "backdropBlur", isSignal: true, isRequired: false, transformFunction: null }, showCloseButton: { classPropertyName: "showCloseButton", publicName: "showCloseButton", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, initialFocus: { classPropertyName: "initialFocus", publicName: "initialFocus", isSignal: true, isRequired: false, transformFunction: null }, customClass: { classPropertyName: "customClass", publicName: "customClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", opened: "opened", closed: "closed", dismissed: "dismissed" }, viewQueries: [{ propertyName: "modalTpl", first: true, predicate: ["modalTpl"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n The overlay lives inside an <ng-template> and is instantiated into\n document.body via a portal (see SvModalComponent.mountPortal), so the\n fixed backdrop always covers the full viewport and is never clipped by a\n parent's overflow / transform / filter containment.\n-->\n<ng-template #modalTpl>\n <!-- Backdrop + centering wrapper -->\n <div\n [ngClass]=\"[\n 'fixed inset-0 z-50 flex justify-center px-4 py-6 overflow-y-auto',\n 'modal-backdrop',\n backdropBlur() ? 'bg-black/50 backdrop-blur-sm' : 'bg-black/40',\n alignClass()\n ]\"\n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onKeydown($event)\"\n >\n\n <!-- Dialog panel -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"title() ? titleId : null\"\n [attr.aria-label]=\"title() ? null : 'Dialog'\"\n [attr.aria-describedby]=\"hasDescription() ? subtitleId : null\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [ngClass]=\"[\n 'relative flex flex-col max-h-[90vh] overflow-hidden rounded-2xl shadow-2xl',\n 'bg-white dark:bg-gray-800',\n 'ring-1 ring-gray-200 dark:ring-gray-700',\n animationClass(),\n panelClass(),\n positionPanelClass(),\n customClass()\n ]\"\n tabindex=\"-1\"\n >\n\n <!-- \u2500\u2500 Accent bar (semantic variants only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (accentBarClass()) {\n <div [ngClass]=\"['h-1 w-full shrink-0', accentBarClass()]\" aria-hidden=\"true\"></div>\n }\n\n <!-- \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (showHeader()) {\n <div\n [ngClass]=\"[\n 'flex items-start justify-between gap-4 px-6 py-4 shrink-0',\n showHeaderDivider() ? 'border-b border-gray-200 dark:border-gray-700' : ''\n ]\"\n >\n <div class=\"flex min-w-0 flex-1 items-start gap-3.5\">\n <!-- Variant icon badge -->\n @if (hasIcon()) {\n <span\n [ngClass]=\"[\n 'mt-0.5 inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full',\n iconAccentClass()\n ]\"\n aria-hidden=\"true\"\n >\n @switch (variant()) {\n @case ('success') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><path d=\"m9 11 3 3L22 4\"/>\n </svg>\n }\n @case ('warning') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"/>\n <path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/>\n </svg>\n }\n @case ('danger') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6\"/><path d=\"m9 9 6 6\"/>\n </svg>\n }\n @case ('info') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>\n </svg>\n }\n @default {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z\"/>\n </svg>\n }\n }\n </span>\n }\n\n <!-- Built-in title / subtitle or custom header slot -->\n <div class=\"min-w-0 flex-1\">\n @if (title()) {\n <h2 [id]=\"titleId\" class=\"text-lg font-semibold text-gray-900 dark:text-white truncate\">\n {{ title() }}\n </h2>\n @if (subtitle()) {\n <p [id]=\"subtitleId\" class=\"mt-0.5 text-sm text-gray-500 dark:text-gray-400\">\n {{ subtitle() }}\n </p>\n }\n } @else {\n <ng-content select=\"[slot=header]\" />\n }\n </div>\n </div>\n\n <!-- Close button -->\n @if (showCloseButton()) {\n <button\n type=\"button\"\n (click)=\"close('close-button')\"\n [disabled]=\"loading()\"\n aria-label=\"Close dialog\"\n class=\"shrink-0 inline-flex h-8 w-8 items-center justify-center rounded-lg\n text-gray-500 dark:text-gray-400\n hover:bg-gray-100 dark:hover:bg-gray-700\n hover:text-gray-600 dark:hover:text-gray-300\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n disabled:opacity-40 disabled:pointer-events-none\n transition-colors duration-100\"\n >\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\">\n <path d=\"M18 6 6 18M6 6l12 12\"/>\n </svg>\n </button>\n }\n </div>\n }\n\n <!-- \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"flex-1 overflow-y-auto px-6 py-5 text-sm text-gray-700 dark:text-gray-300\">\n <ng-content select=\"[slot=body]\" />\n </div>\n\n <!-- \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (showFooter()) {\n <div\n [ngClass]=\"[\n 'shrink-0 px-6 py-4',\n showFooterDivider() ? 'border-t border-gray-200 dark:border-gray-700' : ''\n ]\"\n >\n <ng-content select=\"[slot=footer]\" />\n </div>\n }\n\n <!-- \u2500\u2500 Loading overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (loading()) {\n <div\n class=\"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3\n bg-white/70 backdrop-blur-[1px] dark:bg-gray-800/70\"\n role=\"status\"\n [attr.aria-label]=\"loadingText()\"\n >\n <svg class=\"h-7 w-7 animate-spin text-primary-500\" viewBox=\"0 0 24 24\" fill=\"none\"\n aria-hidden=\"true\">\n <circle class=\"opacity-20\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"/>\n <path class=\"opacity-90\" fill=\"currentColor\"\n d=\"M4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12h4Z\"/>\n </svg>\n <span class=\"text-xs font-medium text-gray-500 dark:text-gray-400\">{{ loadingText() }}</span>\n </div>\n }\n\n </div>\n </div>\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
352
|
+
}
|
|
353
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvModalComponent, decorators: [{
|
|
354
|
+
type: Component,
|
|
355
|
+
args: [{ selector: 'sv-modal', standalone: true, imports: [NgClass], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n The overlay lives inside an <ng-template> and is instantiated into\n document.body via a portal (see SvModalComponent.mountPortal), so the\n fixed backdrop always covers the full viewport and is never clipped by a\n parent's overflow / transform / filter containment.\n-->\n<ng-template #modalTpl>\n <!-- Backdrop + centering wrapper -->\n <div\n [ngClass]=\"[\n 'fixed inset-0 z-50 flex justify-center px-4 py-6 overflow-y-auto',\n 'modal-backdrop',\n backdropBlur() ? 'bg-black/50 backdrop-blur-sm' : 'bg-black/40',\n alignClass()\n ]\"\n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onKeydown($event)\"\n >\n\n <!-- Dialog panel -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"title() ? titleId : null\"\n [attr.aria-label]=\"title() ? null : 'Dialog'\"\n [attr.aria-describedby]=\"hasDescription() ? subtitleId : null\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [ngClass]=\"[\n 'relative flex flex-col max-h-[90vh] overflow-hidden rounded-2xl shadow-2xl',\n 'bg-white dark:bg-gray-800',\n 'ring-1 ring-gray-200 dark:ring-gray-700',\n animationClass(),\n panelClass(),\n positionPanelClass(),\n customClass()\n ]\"\n tabindex=\"-1\"\n >\n\n <!-- \u2500\u2500 Accent bar (semantic variants only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (accentBarClass()) {\n <div [ngClass]=\"['h-1 w-full shrink-0', accentBarClass()]\" aria-hidden=\"true\"></div>\n }\n\n <!-- \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (showHeader()) {\n <div\n [ngClass]=\"[\n 'flex items-start justify-between gap-4 px-6 py-4 shrink-0',\n showHeaderDivider() ? 'border-b border-gray-200 dark:border-gray-700' : ''\n ]\"\n >\n <div class=\"flex min-w-0 flex-1 items-start gap-3.5\">\n <!-- Variant icon badge -->\n @if (hasIcon()) {\n <span\n [ngClass]=\"[\n 'mt-0.5 inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full',\n iconAccentClass()\n ]\"\n aria-hidden=\"true\"\n >\n @switch (variant()) {\n @case ('success') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><path d=\"m9 11 3 3L22 4\"/>\n </svg>\n }\n @case ('warning') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"/>\n <path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/>\n </svg>\n }\n @case ('danger') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6\"/><path d=\"m9 9 6 6\"/>\n </svg>\n }\n @case ('info') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>\n </svg>\n }\n @default {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z\"/>\n </svg>\n }\n }\n </span>\n }\n\n <!-- Built-in title / subtitle or custom header slot -->\n <div class=\"min-w-0 flex-1\">\n @if (title()) {\n <h2 [id]=\"titleId\" class=\"text-lg font-semibold text-gray-900 dark:text-white truncate\">\n {{ title() }}\n </h2>\n @if (subtitle()) {\n <p [id]=\"subtitleId\" class=\"mt-0.5 text-sm text-gray-500 dark:text-gray-400\">\n {{ subtitle() }}\n </p>\n }\n } @else {\n <ng-content select=\"[slot=header]\" />\n }\n </div>\n </div>\n\n <!-- Close button -->\n @if (showCloseButton()) {\n <button\n type=\"button\"\n (click)=\"close('close-button')\"\n [disabled]=\"loading()\"\n aria-label=\"Close dialog\"\n class=\"shrink-0 inline-flex h-8 w-8 items-center justify-center rounded-lg\n text-gray-500 dark:text-gray-400\n hover:bg-gray-100 dark:hover:bg-gray-700\n hover:text-gray-600 dark:hover:text-gray-300\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n disabled:opacity-40 disabled:pointer-events-none\n transition-colors duration-100\"\n >\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\">\n <path d=\"M18 6 6 18M6 6l12 12\"/>\n </svg>\n </button>\n }\n </div>\n }\n\n <!-- \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"flex-1 overflow-y-auto px-6 py-5 text-sm text-gray-700 dark:text-gray-300\">\n <ng-content select=\"[slot=body]\" />\n </div>\n\n <!-- \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (showFooter()) {\n <div\n [ngClass]=\"[\n 'shrink-0 px-6 py-4',\n showFooterDivider() ? 'border-t border-gray-200 dark:border-gray-700' : ''\n ]\"\n >\n <ng-content select=\"[slot=footer]\" />\n </div>\n }\n\n <!-- \u2500\u2500 Loading overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (loading()) {\n <div\n class=\"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3\n bg-white/70 backdrop-blur-[1px] dark:bg-gray-800/70\"\n role=\"status\"\n [attr.aria-label]=\"loadingText()\"\n >\n <svg class=\"h-7 w-7 animate-spin text-primary-500\" viewBox=\"0 0 24 24\" fill=\"none\"\n aria-hidden=\"true\">\n <circle class=\"opacity-20\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"/>\n <path class=\"opacity-90\" fill=\"currentColor\"\n d=\"M4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12h4Z\"/>\n </svg>\n <span class=\"text-xs font-medium text-gray-500 dark:text-gray-400\">{{ loadingText() }}</span>\n </div>\n }\n\n </div>\n </div>\n</ng-template>\n" }]
|
|
356
|
+
}], ctorParameters: () => [], propDecorators: { modalTpl: [{ type: i0.ViewChild, args: ['modalTpl', { isSignal: true }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }, { type: i0.Output, args: ["isOpenChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], showHeaderDivider: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeaderDivider", required: false }] }], showFooterDivider: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooterDivider", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], showIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIcon", required: false }] }], closeOnBackdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnBackdrop", required: false }] }], closeOnEsc: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnEsc", required: false }] }], backdropBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdropBlur", required: false }] }], showCloseButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCloseButton", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], initialFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialFocus", required: false }] }], customClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "customClass", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Generated bundle index. Do not edit.
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
export { SvModalComponent };
|
|
363
|
+
//# sourceMappingURL=styloviz-modal.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styloviz-modal.mjs","sources":["../../../../projects/modal/src/lib/modal.component.ts","../../../../projects/modal/src/lib/modal.component.html","../../../../projects/modal/src/styloviz-modal.ts"],"sourcesContent":["import {\n ApplicationRef,\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n EmbeddedViewRef,\n inject,\n input,\n model,\n OnDestroy,\n output,\n PLATFORM_ID,\n TemplateRef,\n viewChild,\n} from '@angular/core';\nimport { DOCUMENT, isPlatformBrowser, NgClass } from '@angular/common';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type ModalSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';\nexport type ModalPosition = 'center' | 'top' | 'bottom';\n\n/**\n * Visual/semantic style of the dialog.\n * - `default` — neutral dialog with no accent icon.\n * - `primary` `success` `warning` `danger` `info` — premium accent variants\n * that render a tinted icon badge in the header and a matching accent bar.\n */\nexport type ModalVariant =\n | 'default'\n | 'primary'\n | 'success'\n | 'warning'\n | 'danger'\n | 'info';\n\n/** Why the modal closed — emitted through the `dismissed` output. */\nexport type ModalCloseReason =\n | 'backdrop'\n | 'escape'\n | 'close-button'\n | 'programmatic';\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\n/**\n * ModalComponent — A premium, fully-featured dialog overlay.\n *\n * The backdrop + panel are rendered into `document.body` through a lightweight\n * Angular embedded-view portal, so the modal always covers the full viewport\n * and escapes any parent `overflow:hidden`/`auto` or CSS `transform`/`filter`\n * containment — it is never clipped to or trapped inside a card/wrapper.\n *\n * Features:\n * - Signal-based open/close via two-way `isOpen` model\n * - Portaled to `<body>` so `position:fixed` is always viewport-relative\n * - 6 semantic variants: default · primary · success · warning · danger · info\n * (each renders a tinted icon badge + accent bar in the header)\n * - 7 sizes: xs · sm · md · lg · xl · 2xl · full\n * - 3 positions: center (default) · top · bottom (mobile-friendly sheet)\n * - Header (title + optional subtitle) / body / footer content projection\n * - Optional loading state that blocks close and shows a spinner overlay\n * - Backdrop click to close (configurable) + optional backdrop blur\n * - ESC key to close (configurable)\n * - Scroll lock on `<body>` with scrollbar-width compensation (no layout shift)\n * - Focus trap within the dialog + configurable initial focus target\n * - `aria-labelledby` / `aria-describedby` wiring for screen readers\n * - Slide-in + fade animation (direction follows `position`)\n * - Full dark-mode support\n *\n * Usage:\n * ```html\n * <sv-modal\n * [(isOpen)]=\"showDialog\"\n * variant=\"danger\"\n * title=\"Delete project\"\n * subtitle=\"This action is permanent\">\n * <ng-container slot=\"body\">Are you sure?</ng-container>\n * <ng-container slot=\"footer\">\n * <button (click)=\"showDialog = false\">Cancel</button>\n * <button (click)=\"onConfirm()\">Delete</button>\n * </ng-container>\n * </sv-modal>\n * ```\n */\n@Component({\n selector: 'sv-modal',\n standalone: true,\n imports: [NgClass],\n templateUrl: './modal.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SvModalComponent implements OnDestroy {\n private readonly platformId = inject(PLATFORM_ID);\n private readonly appRef = inject(ApplicationRef);\n private readonly document = inject(DOCUMENT);\n\n // ── Query refs ────────────────────────────────────────────────────────────\n\n /** The ng-template holding the overlay markup — instantiated into <body> on open. */\n private readonly modalTpl = viewChild<TemplateRef<unknown>>('modalTpl');\n\n // ── Inputs ────────────────────────────────────────────────────────────────\n\n /** Controls open/closed state. Use `[(isOpen)]` for two-way binding. */\n readonly isOpen = model<boolean>(false);\n\n /** Dialog title shown in the built-in header. */\n readonly title = input<string>('');\n\n /** Optional secondary line rendered beneath the title (also used for aria-describedby). */\n readonly subtitle = input<string>('');\n\n /** Semantic/visual variant. Drives the header icon badge + accent bar. @default 'default' */\n readonly variant = input<ModalVariant>('default');\n\n /** Render the built-in header bar. @default true */\n readonly showHeader = input<boolean>(true);\n\n /** Bottom dividing line under the header. @default true */\n readonly showHeaderDivider = input<boolean>(true);\n\n /** Top dividing line above the footer. @default true */\n readonly showFooterDivider = input<boolean>(true);\n\n /** Render the footer slot wrapper. @default true */\n readonly showFooter = input<boolean>(true);\n\n /** Size of the dialog panel. @default 'md' */\n readonly size = input<ModalSize>('md');\n\n /** Placement of the dialog panel. @default 'center' */\n readonly position = input<ModalPosition>('center');\n\n /** Show the built-in variant icon badge in the header. @default true */\n readonly showIcon = input<boolean>(true);\n\n /** Close dialog when backdrop is clicked. @default true */\n readonly closeOnBackdrop = input<boolean>(true);\n\n /** Close dialog on ESC key. @default true */\n readonly closeOnEsc = input<boolean>(true);\n\n /** Apply a frosted-glass blur to the backdrop. @default false */\n readonly backdropBlur = input<boolean>(false);\n\n /** Show the built-in × close button in the header. @default true */\n readonly showCloseButton = input<boolean>(true);\n\n /**\n * Busy state. While `true` the panel shows a spinner overlay and all close\n * paths (button, backdrop, ESC) are blocked so a running task can finish.\n * @default false\n */\n readonly loading = input<boolean>(false);\n\n /** Accessible label announced while `loading` is true. @default 'Loading…' */\n readonly loadingText = input<string>('Loading…');\n\n /**\n * CSS selector for the element to focus first when the modal opens\n * (e.g. `'#name-input'`). Falls back to the first focusable element.\n */\n readonly initialFocus = input<string>('');\n\n /** Extra CSS classes merged onto the dialog panel. */\n readonly customClass = input<string>('');\n\n // ── Outputs ───────────────────────────────────────────────────────────────\n\n /** Emitted after the modal is opened (by any means). */\n readonly opened = output<void>();\n\n /** Emitted after the modal is closed (by any means). */\n readonly closed = output<void>();\n\n /** Emitted alongside `closed`, describing what triggered the close. */\n readonly dismissed = output<ModalCloseReason>();\n\n // ── Stable element ids (a11y wiring) ───────────────────────────────────────\n\n private readonly uid = Math.random().toString(36).slice(2, 8);\n /** Stable id for the built-in title, used by `aria-labelledby`. */\n readonly titleId = `sv-modal-title-${this.uid}`;\n /** Stable id for the subtitle, used by `aria-describedby`. */\n readonly subtitleId = `sv-modal-desc-${this.uid}`;\n\n // ── Computed ──────────────────────────────────────────────────────────────\n\n readonly panelClass = computed<string>(() => {\n const sizeMap: Record<ModalSize, string> = {\n xs: 'w-full max-w-xs',\n sm: 'w-full max-w-sm',\n md: 'w-full max-w-md',\n lg: 'w-full max-w-lg',\n xl: 'w-full max-w-2xl',\n '2xl': 'w-full max-w-4xl',\n full: 'w-screen h-screen max-w-none rounded-none',\n };\n return sizeMap[this.size()];\n });\n\n /** Alignment of the panel inside the flex overlay. */\n readonly alignClass = computed<string>(() => {\n switch (this.position()) {\n case 'top':\n return 'items-start pt-16';\n case 'bottom':\n return 'items-end pb-0 sm:pb-6';\n default:\n return 'items-center';\n }\n });\n\n /** Bottom position becomes a full-width sheet on small screens. */\n readonly positionPanelClass = computed<string>(() =>\n this.position() === 'bottom' && this.size() !== 'full'\n ? 'rounded-b-none sm:rounded-2xl'\n : ''\n );\n\n /** Animation class — bottom slides up, others slide down + fade. */\n readonly animationClass = computed<string>(() =>\n this.position() === 'bottom' ? 'modal-panel modal-panel--up' : 'modal-panel'\n );\n\n /** Accent classes for the header icon badge, keyed by variant. */\n readonly iconAccentClass = computed<string>(() => {\n const map: Record<ModalVariant, string> = {\n default: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300',\n primary: 'bg-primary-50 text-primary-600 dark:bg-primary-500/15 dark:text-primary-400',\n success: 'bg-emerald-50 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400',\n warning: 'bg-amber-50 text-amber-600 dark:bg-amber-500/15 dark:text-amber-400',\n danger: 'bg-red-50 text-red-600 dark:bg-red-500/15 dark:text-red-400',\n info: 'bg-sky-50 text-sky-600 dark:bg-sky-500/15 dark:text-sky-400',\n };\n return map[this.variant()];\n });\n\n /** Thin accent bar across the top of the panel for semantic variants. */\n readonly accentBarClass = computed<string>(() => {\n const map: Record<ModalVariant, string> = {\n default: '',\n primary: 'bg-primary-500',\n success: 'bg-emerald-500',\n warning: 'bg-amber-500',\n danger: 'bg-red-500',\n info: 'bg-sky-500',\n };\n return map[this.variant()];\n });\n\n /** Whether the header icon badge should render. */\n readonly hasIcon = computed<boolean>(\n () => this.showIcon() && this.variant() !== 'default'\n );\n\n /** True when a description should be linked via aria-describedby. */\n readonly hasDescription = computed<boolean>(() => this.subtitle().length > 0);\n\n // ── Portal + focus state ──────────────────────────────────────────────────\n\n /** Container appended to <body> that hosts the overlay while open. */\n private portalEl: HTMLElement | null = null;\n /** Embedded view holding the overlay template nodes. */\n private portalView: EmbeddedViewRef<unknown> | null = null;\n /** Element focused immediately before the modal opened, restored on close. */\n private previouslyFocused: HTMLElement | null = null;\n /** Tracks the prior open state so the effect can emit opened/closed on transitions. */\n private wasOpen = false;\n\n constructor() {\n // React to isOpen changes regardless of how it is set:\n // either via open()/close() methods OR parent [(isOpen)] binding.\n effect(() => {\n const open = this.isOpen();\n if (!isPlatformBrowser(this.platformId)) {\n this.wasOpen = open;\n return;\n }\n\n if (open && !this.wasOpen) {\n // Opening transition\n this.previouslyFocused = this.document.activeElement as HTMLElement | null;\n this.lockScroll();\n this.opened.emit();\n // Mount + focus after the current change-detection pass settles.\n setTimeout(() => {\n if (!this.isOpen()) return;\n this.mountPortal();\n this.applyInitialFocus();\n });\n } else if (!open && this.wasOpen) {\n // Closing transition (covers close() AND parent [(isOpen)] = false)\n this.destroyPortal();\n this.unlockScroll();\n this.closed.emit();\n this.dismissed.emit(this.pendingReason);\n this.pendingReason = 'programmatic';\n // Return focus to whatever triggered the modal for keyboard continuity\n this.previouslyFocused?.focus?.();\n this.previouslyFocused = null;\n }\n this.wasOpen = open;\n });\n }\n\n // ── Lifecycle ─────────────────────────────────────────────────────────────\n\n ngOnDestroy(): void {\n if (isPlatformBrowser(this.platformId)) {\n this.destroyPortal();\n this.unlockScroll();\n }\n }\n\n // ── Public open / close ───────────────────────────────────────────────────\n\n /** Reason attached to the next close transition; reset after each emit. */\n private pendingReason: ModalCloseReason = 'programmatic';\n\n /** Open the modal programmatically. Scroll lock + focus trap handled by effect. */\n open(): void {\n this.isOpen.set(true);\n }\n\n /**\n * Close the modal programmatically. Blocked while `loading` is true.\n * The `closed` + `dismissed` outputs fire from the effect.\n */\n close(reason: ModalCloseReason = 'programmatic'): void {\n if (this.loading()) return;\n this.pendingReason = reason;\n this.isOpen.set(false);\n }\n\n // ── Backdrop click ────────────────────────────────────────────────────────\n\n onBackdropClick(event: MouseEvent): void {\n if (!this.closeOnBackdrop() || this.loading()) return;\n if ((event.target as HTMLElement) === event.currentTarget) {\n this.close('backdrop');\n }\n }\n\n // ── Keyboard ─────────────────────────────────────────────────────────────\n\n onKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape' && this.closeOnEsc() && !this.loading()) {\n event.preventDefault();\n this.close('escape');\n return;\n }\n if (event.key === 'Tab') {\n this.handleTabKey(event);\n }\n }\n\n // ── Portal lifecycle ──────────────────────────────────────────────────────\n\n private mountPortal(): void {\n if (this.portalView) return;\n const tpl = this.modalTpl();\n if (!tpl) return;\n\n this.portalEl = this.document.createElement('div');\n this.portalEl.setAttribute('data-sv-modal-portal', '');\n this.document.body.appendChild(this.portalEl);\n\n // Escapes ALL parent overflow / transform / filter containment.\n this.portalView = tpl.createEmbeddedView({});\n this.appRef.attachView(this.portalView);\n this.portalView.rootNodes.forEach((n: Node) => this.portalEl!.appendChild(n));\n this.portalView.detectChanges();\n }\n\n private destroyPortal(): void {\n if (this.portalView) {\n this.appRef.detachView(this.portalView);\n this.portalView.destroy();\n this.portalView = null;\n }\n this.portalEl?.remove();\n this.portalEl = null;\n }\n\n // ── Scroll lock (with scrollbar-width compensation) ────────────────────────\n\n private lockScroll(): void {\n const scrollbar = window.innerWidth - this.document.documentElement.clientWidth;\n this.document.body.style.overflow = 'hidden';\n if (scrollbar > 0) this.document.body.style.paddingRight = `${scrollbar}px`;\n }\n\n private unlockScroll(): void {\n this.document.body.style.overflow = '';\n this.document.body.style.paddingRight = '';\n }\n\n // ── Focus management ──────────────────────────────────────────────────────\n\n /** The dialog panel element inside the body-mounted portal. */\n private getPanelEl(): HTMLElement | null {\n return this.portalEl?.querySelector<HTMLElement>('[role=\"dialog\"]') ?? null;\n }\n\n private applyInitialFocus(): void {\n const el = this.getPanelEl();\n if (!el) return;\n\n const selector = this.initialFocus();\n if (selector) {\n const target = el.querySelector<HTMLElement>(selector);\n if (target) {\n target.focus();\n return;\n }\n }\n\n const focusable = this.getFocusableElements(el);\n if (focusable.length > 0) {\n (focusable[0] as HTMLElement).focus();\n } else {\n el.focus();\n }\n }\n\n private handleTabKey(event: KeyboardEvent): void {\n const el = this.getPanelEl();\n if (!el) return;\n const focusable = this.getFocusableElements(el);\n if (focusable.length === 0) {\n event.preventDefault();\n el.focus();\n return;\n }\n\n const first = focusable[0] as HTMLElement;\n const last = focusable[focusable.length - 1] as HTMLElement;\n const active = this.document.activeElement;\n\n if (event.shiftKey) {\n if (active === first || active === el) {\n event.preventDefault();\n last.focus();\n }\n } else {\n if (active === last) {\n event.preventDefault();\n first.focus();\n }\n }\n }\n\n private getFocusableElements(container: HTMLElement): Element[] {\n return Array.from(\n container.querySelectorAll<HTMLElement>(\n 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"]), [contenteditable=\"true\"]'\n )\n ).filter(\n el =>\n !el.hasAttribute('disabled') &&\n el.getAttribute('aria-hidden') !== 'true' &&\n (el as HTMLElement).offsetParent !== null\n );\n }\n}\n","<!--\n The overlay lives inside an <ng-template> and is instantiated into\n document.body via a portal (see SvModalComponent.mountPortal), so the\n fixed backdrop always covers the full viewport and is never clipped by a\n parent's overflow / transform / filter containment.\n-->\n<ng-template #modalTpl>\n <!-- Backdrop + centering wrapper -->\n <div\n [ngClass]=\"[\n 'fixed inset-0 z-50 flex justify-center px-4 py-6 overflow-y-auto',\n 'modal-backdrop',\n backdropBlur() ? 'bg-black/50 backdrop-blur-sm' : 'bg-black/40',\n alignClass()\n ]\"\n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onKeydown($event)\"\n >\n\n <!-- Dialog panel -->\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"title() ? titleId : null\"\n [attr.aria-label]=\"title() ? null : 'Dialog'\"\n [attr.aria-describedby]=\"hasDescription() ? subtitleId : null\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [ngClass]=\"[\n 'relative flex flex-col max-h-[90vh] overflow-hidden rounded-2xl shadow-2xl',\n 'bg-white dark:bg-gray-800',\n 'ring-1 ring-gray-200 dark:ring-gray-700',\n animationClass(),\n panelClass(),\n positionPanelClass(),\n customClass()\n ]\"\n tabindex=\"-1\"\n >\n\n <!-- ── Accent bar (semantic variants only) ─────────────────────── -->\n @if (accentBarClass()) {\n <div [ngClass]=\"['h-1 w-full shrink-0', accentBarClass()]\" aria-hidden=\"true\"></div>\n }\n\n <!-- ── Header ────────────────────────────────────────────────── -->\n @if (showHeader()) {\n <div\n [ngClass]=\"[\n 'flex items-start justify-between gap-4 px-6 py-4 shrink-0',\n showHeaderDivider() ? 'border-b border-gray-200 dark:border-gray-700' : ''\n ]\"\n >\n <div class=\"flex min-w-0 flex-1 items-start gap-3.5\">\n <!-- Variant icon badge -->\n @if (hasIcon()) {\n <span\n [ngClass]=\"[\n 'mt-0.5 inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full',\n iconAccentClass()\n ]\"\n aria-hidden=\"true\"\n >\n @switch (variant()) {\n @case ('success') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><path d=\"m9 11 3 3L22 4\"/>\n </svg>\n }\n @case ('warning') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"/>\n <path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/>\n </svg>\n }\n @case ('danger') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6\"/><path d=\"m9 9 6 6\"/>\n </svg>\n }\n @case ('info') {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>\n </svg>\n }\n @default {\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z\"/>\n </svg>\n }\n }\n </span>\n }\n\n <!-- Built-in title / subtitle or custom header slot -->\n <div class=\"min-w-0 flex-1\">\n @if (title()) {\n <h2 [id]=\"titleId\" class=\"text-lg font-semibold text-gray-900 dark:text-white truncate\">\n {{ title() }}\n </h2>\n @if (subtitle()) {\n <p [id]=\"subtitleId\" class=\"mt-0.5 text-sm text-gray-500 dark:text-gray-400\">\n {{ subtitle() }}\n </p>\n }\n } @else {\n <ng-content select=\"[slot=header]\" />\n }\n </div>\n </div>\n\n <!-- Close button -->\n @if (showCloseButton()) {\n <button\n type=\"button\"\n (click)=\"close('close-button')\"\n [disabled]=\"loading()\"\n aria-label=\"Close dialog\"\n class=\"shrink-0 inline-flex h-8 w-8 items-center justify-center rounded-lg\n text-gray-500 dark:text-gray-400\n hover:bg-gray-100 dark:hover:bg-gray-700\n hover:text-gray-600 dark:hover:text-gray-300\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500\n disabled:opacity-40 disabled:pointer-events-none\n transition-colors duration-100\"\n >\n <svg class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\">\n <path d=\"M18 6 6 18M6 6l12 12\"/>\n </svg>\n </button>\n }\n </div>\n }\n\n <!-- ── Body ──────────────────────────────────────────────────── -->\n <div class=\"flex-1 overflow-y-auto px-6 py-5 text-sm text-gray-700 dark:text-gray-300\">\n <ng-content select=\"[slot=body]\" />\n </div>\n\n <!-- ── Footer ────────────────────────────────────────────────── -->\n @if (showFooter()) {\n <div\n [ngClass]=\"[\n 'shrink-0 px-6 py-4',\n showFooterDivider() ? 'border-t border-gray-200 dark:border-gray-700' : ''\n ]\"\n >\n <ng-content select=\"[slot=footer]\" />\n </div>\n }\n\n <!-- ── Loading overlay ───────────────────────────────────────── -->\n @if (loading()) {\n <div\n class=\"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3\n bg-white/70 backdrop-blur-[1px] dark:bg-gray-800/70\"\n role=\"status\"\n [attr.aria-label]=\"loadingText()\"\n >\n <svg class=\"h-7 w-7 animate-spin text-primary-500\" viewBox=\"0 0 24 24\" fill=\"none\"\n aria-hidden=\"true\">\n <circle class=\"opacity-20\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"/>\n <path class=\"opacity-90\" fill=\"currentColor\"\n d=\"M4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12h4Z\"/>\n </svg>\n <span class=\"text-xs font-medium text-gray-500 dark:text-gray-400\">{{ loadingText() }}</span>\n </div>\n }\n\n </div>\n </div>\n</ng-template>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@styloviz/modal",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Premium dialog overlay with 6 semantic variants, 7 sizes, bottom-sheet & loading states, slot-based projection, focus trap and ESC/backdrop close.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "sazzad-bs23",
|
|
7
|
+
"homepage": "https://styloviz.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rhossain/angular-tailwind-dashboard-components.git",
|
|
11
|
+
"directory": "projects/modal"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rhossain/angular-tailwind-dashboard-components/issues"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@angular/common": ">=21.0.0",
|
|
18
|
+
"@angular/core": ">=21.0.0"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"module": "fesm2022/styloviz-modal.mjs",
|
|
28
|
+
"typings": "types/styloviz-modal.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": {
|
|
31
|
+
"default": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./types/styloviz-modal.d.ts",
|
|
35
|
+
"default": "./fesm2022/styloviz-modal.mjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"type": "module",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"tslib": "^2.3.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
import { OnDestroy } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
type ModalSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
|
5
|
+
type ModalPosition = 'center' | 'top' | 'bottom';
|
|
6
|
+
/**
|
|
7
|
+
* Visual/semantic style of the dialog.
|
|
8
|
+
* - `default` — neutral dialog with no accent icon.
|
|
9
|
+
* - `primary` `success` `warning` `danger` `info` — premium accent variants
|
|
10
|
+
* that render a tinted icon badge in the header and a matching accent bar.
|
|
11
|
+
*/
|
|
12
|
+
type ModalVariant = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
|
13
|
+
/** Why the modal closed — emitted through the `dismissed` output. */
|
|
14
|
+
type ModalCloseReason = 'backdrop' | 'escape' | 'close-button' | 'programmatic';
|
|
15
|
+
/**
|
|
16
|
+
* ModalComponent — A premium, fully-featured dialog overlay.
|
|
17
|
+
*
|
|
18
|
+
* The backdrop + panel are rendered into `document.body` through a lightweight
|
|
19
|
+
* Angular embedded-view portal, so the modal always covers the full viewport
|
|
20
|
+
* and escapes any parent `overflow:hidden`/`auto` or CSS `transform`/`filter`
|
|
21
|
+
* containment — it is never clipped to or trapped inside a card/wrapper.
|
|
22
|
+
*
|
|
23
|
+
* Features:
|
|
24
|
+
* - Signal-based open/close via two-way `isOpen` model
|
|
25
|
+
* - Portaled to `<body>` so `position:fixed` is always viewport-relative
|
|
26
|
+
* - 6 semantic variants: default · primary · success · warning · danger · info
|
|
27
|
+
* (each renders a tinted icon badge + accent bar in the header)
|
|
28
|
+
* - 7 sizes: xs · sm · md · lg · xl · 2xl · full
|
|
29
|
+
* - 3 positions: center (default) · top · bottom (mobile-friendly sheet)
|
|
30
|
+
* - Header (title + optional subtitle) / body / footer content projection
|
|
31
|
+
* - Optional loading state that blocks close and shows a spinner overlay
|
|
32
|
+
* - Backdrop click to close (configurable) + optional backdrop blur
|
|
33
|
+
* - ESC key to close (configurable)
|
|
34
|
+
* - Scroll lock on `<body>` with scrollbar-width compensation (no layout shift)
|
|
35
|
+
* - Focus trap within the dialog + configurable initial focus target
|
|
36
|
+
* - `aria-labelledby` / `aria-describedby` wiring for screen readers
|
|
37
|
+
* - Slide-in + fade animation (direction follows `position`)
|
|
38
|
+
* - Full dark-mode support
|
|
39
|
+
*
|
|
40
|
+
* Usage:
|
|
41
|
+
* ```html
|
|
42
|
+
* <sv-modal
|
|
43
|
+
* [(isOpen)]="showDialog"
|
|
44
|
+
* variant="danger"
|
|
45
|
+
* title="Delete project"
|
|
46
|
+
* subtitle="This action is permanent">
|
|
47
|
+
* <ng-container slot="body">Are you sure?</ng-container>
|
|
48
|
+
* <ng-container slot="footer">
|
|
49
|
+
* <button (click)="showDialog = false">Cancel</button>
|
|
50
|
+
* <button (click)="onConfirm()">Delete</button>
|
|
51
|
+
* </ng-container>
|
|
52
|
+
* </sv-modal>
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare class SvModalComponent implements OnDestroy {
|
|
56
|
+
private readonly platformId;
|
|
57
|
+
private readonly appRef;
|
|
58
|
+
private readonly document;
|
|
59
|
+
/** The ng-template holding the overlay markup — instantiated into <body> on open. */
|
|
60
|
+
private readonly modalTpl;
|
|
61
|
+
/** Controls open/closed state. Use `[(isOpen)]` for two-way binding. */
|
|
62
|
+
readonly isOpen: _angular_core.ModelSignal<boolean>;
|
|
63
|
+
/** Dialog title shown in the built-in header. */
|
|
64
|
+
readonly title: _angular_core.InputSignal<string>;
|
|
65
|
+
/** Optional secondary line rendered beneath the title (also used for aria-describedby). */
|
|
66
|
+
readonly subtitle: _angular_core.InputSignal<string>;
|
|
67
|
+
/** Semantic/visual variant. Drives the header icon badge + accent bar. @default 'default' */
|
|
68
|
+
readonly variant: _angular_core.InputSignal<ModalVariant>;
|
|
69
|
+
/** Render the built-in header bar. @default true */
|
|
70
|
+
readonly showHeader: _angular_core.InputSignal<boolean>;
|
|
71
|
+
/** Bottom dividing line under the header. @default true */
|
|
72
|
+
readonly showHeaderDivider: _angular_core.InputSignal<boolean>;
|
|
73
|
+
/** Top dividing line above the footer. @default true */
|
|
74
|
+
readonly showFooterDivider: _angular_core.InputSignal<boolean>;
|
|
75
|
+
/** Render the footer slot wrapper. @default true */
|
|
76
|
+
readonly showFooter: _angular_core.InputSignal<boolean>;
|
|
77
|
+
/** Size of the dialog panel. @default 'md' */
|
|
78
|
+
readonly size: _angular_core.InputSignal<ModalSize>;
|
|
79
|
+
/** Placement of the dialog panel. @default 'center' */
|
|
80
|
+
readonly position: _angular_core.InputSignal<ModalPosition>;
|
|
81
|
+
/** Show the built-in variant icon badge in the header. @default true */
|
|
82
|
+
readonly showIcon: _angular_core.InputSignal<boolean>;
|
|
83
|
+
/** Close dialog when backdrop is clicked. @default true */
|
|
84
|
+
readonly closeOnBackdrop: _angular_core.InputSignal<boolean>;
|
|
85
|
+
/** Close dialog on ESC key. @default true */
|
|
86
|
+
readonly closeOnEsc: _angular_core.InputSignal<boolean>;
|
|
87
|
+
/** Apply a frosted-glass blur to the backdrop. @default false */
|
|
88
|
+
readonly backdropBlur: _angular_core.InputSignal<boolean>;
|
|
89
|
+
/** Show the built-in × close button in the header. @default true */
|
|
90
|
+
readonly showCloseButton: _angular_core.InputSignal<boolean>;
|
|
91
|
+
/**
|
|
92
|
+
* Busy state. While `true` the panel shows a spinner overlay and all close
|
|
93
|
+
* paths (button, backdrop, ESC) are blocked so a running task can finish.
|
|
94
|
+
* @default false
|
|
95
|
+
*/
|
|
96
|
+
readonly loading: _angular_core.InputSignal<boolean>;
|
|
97
|
+
/** Accessible label announced while `loading` is true. @default 'Loading…' */
|
|
98
|
+
readonly loadingText: _angular_core.InputSignal<string>;
|
|
99
|
+
/**
|
|
100
|
+
* CSS selector for the element to focus first when the modal opens
|
|
101
|
+
* (e.g. `'#name-input'`). Falls back to the first focusable element.
|
|
102
|
+
*/
|
|
103
|
+
readonly initialFocus: _angular_core.InputSignal<string>;
|
|
104
|
+
/** Extra CSS classes merged onto the dialog panel. */
|
|
105
|
+
readonly customClass: _angular_core.InputSignal<string>;
|
|
106
|
+
/** Emitted after the modal is opened (by any means). */
|
|
107
|
+
readonly opened: _angular_core.OutputEmitterRef<void>;
|
|
108
|
+
/** Emitted after the modal is closed (by any means). */
|
|
109
|
+
readonly closed: _angular_core.OutputEmitterRef<void>;
|
|
110
|
+
/** Emitted alongside `closed`, describing what triggered the close. */
|
|
111
|
+
readonly dismissed: _angular_core.OutputEmitterRef<ModalCloseReason>;
|
|
112
|
+
private readonly uid;
|
|
113
|
+
/** Stable id for the built-in title, used by `aria-labelledby`. */
|
|
114
|
+
readonly titleId: string;
|
|
115
|
+
/** Stable id for the subtitle, used by `aria-describedby`. */
|
|
116
|
+
readonly subtitleId: string;
|
|
117
|
+
readonly panelClass: _angular_core.Signal<string>;
|
|
118
|
+
/** Alignment of the panel inside the flex overlay. */
|
|
119
|
+
readonly alignClass: _angular_core.Signal<string>;
|
|
120
|
+
/** Bottom position becomes a full-width sheet on small screens. */
|
|
121
|
+
readonly positionPanelClass: _angular_core.Signal<string>;
|
|
122
|
+
/** Animation class — bottom slides up, others slide down + fade. */
|
|
123
|
+
readonly animationClass: _angular_core.Signal<string>;
|
|
124
|
+
/** Accent classes for the header icon badge, keyed by variant. */
|
|
125
|
+
readonly iconAccentClass: _angular_core.Signal<string>;
|
|
126
|
+
/** Thin accent bar across the top of the panel for semantic variants. */
|
|
127
|
+
readonly accentBarClass: _angular_core.Signal<string>;
|
|
128
|
+
/** Whether the header icon badge should render. */
|
|
129
|
+
readonly hasIcon: _angular_core.Signal<boolean>;
|
|
130
|
+
/** True when a description should be linked via aria-describedby. */
|
|
131
|
+
readonly hasDescription: _angular_core.Signal<boolean>;
|
|
132
|
+
/** Container appended to <body> that hosts the overlay while open. */
|
|
133
|
+
private portalEl;
|
|
134
|
+
/** Embedded view holding the overlay template nodes. */
|
|
135
|
+
private portalView;
|
|
136
|
+
/** Element focused immediately before the modal opened, restored on close. */
|
|
137
|
+
private previouslyFocused;
|
|
138
|
+
/** Tracks the prior open state so the effect can emit opened/closed on transitions. */
|
|
139
|
+
private wasOpen;
|
|
140
|
+
constructor();
|
|
141
|
+
ngOnDestroy(): void;
|
|
142
|
+
/** Reason attached to the next close transition; reset after each emit. */
|
|
143
|
+
private pendingReason;
|
|
144
|
+
/** Open the modal programmatically. Scroll lock + focus trap handled by effect. */
|
|
145
|
+
open(): void;
|
|
146
|
+
/**
|
|
147
|
+
* Close the modal programmatically. Blocked while `loading` is true.
|
|
148
|
+
* The `closed` + `dismissed` outputs fire from the effect.
|
|
149
|
+
*/
|
|
150
|
+
close(reason?: ModalCloseReason): void;
|
|
151
|
+
onBackdropClick(event: MouseEvent): void;
|
|
152
|
+
onKeydown(event: KeyboardEvent): void;
|
|
153
|
+
private mountPortal;
|
|
154
|
+
private destroyPortal;
|
|
155
|
+
private lockScroll;
|
|
156
|
+
private unlockScroll;
|
|
157
|
+
/** The dialog panel element inside the body-mounted portal. */
|
|
158
|
+
private getPanelEl;
|
|
159
|
+
private applyInitialFocus;
|
|
160
|
+
private handleTabKey;
|
|
161
|
+
private getFocusableElements;
|
|
162
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvModalComponent, never>;
|
|
163
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<SvModalComponent, "sv-modal", never, { "isOpen": { "alias": "isOpen"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "subtitle": { "alias": "subtitle"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "showHeader": { "alias": "showHeader"; "required": false; "isSignal": true; }; "showHeaderDivider": { "alias": "showHeaderDivider"; "required": false; "isSignal": true; }; "showFooterDivider": { "alias": "showFooterDivider"; "required": false; "isSignal": true; }; "showFooter": { "alias": "showFooter"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "position": { "alias": "position"; "required": false; "isSignal": true; }; "showIcon": { "alias": "showIcon"; "required": false; "isSignal": true; }; "closeOnBackdrop": { "alias": "closeOnBackdrop"; "required": false; "isSignal": true; }; "closeOnEsc": { "alias": "closeOnEsc"; "required": false; "isSignal": true; }; "backdropBlur": { "alias": "backdropBlur"; "required": false; "isSignal": true; }; "showCloseButton": { "alias": "showCloseButton"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "loadingText": { "alias": "loadingText"; "required": false; "isSignal": true; }; "initialFocus": { "alias": "initialFocus"; "required": false; "isSignal": true; }; "customClass": { "alias": "customClass"; "required": false; "isSignal": true; }; }, { "isOpen": "isOpenChange"; "opened": "opened"; "closed": "closed"; "dismissed": "dismissed"; }, never, ["[slot=header]", "[slot=body]", "[slot=footer]"], true, never>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { SvModalComponent };
|
|
167
|
+
export type { ModalCloseReason, ModalPosition, ModalSize, ModalVariant };
|