@synthaxai/ui 1.0.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 +262 -0
- package/dist/app.css +2 -0
- package/dist/app.html +12 -0
- package/dist/data-display/DataTable/DataTable.svelte +773 -0
- package/dist/data-display/DataTable/DataTable.svelte.d.ts +120 -0
- package/dist/data-display/DataTable/DataTable.svelte.d.ts.map +1 -0
- package/dist/data-display/DataTable/index.d.ts +2 -0
- package/dist/data-display/DataTable/index.d.ts.map +1 -0
- package/dist/data-display/DataTable/index.js +1 -0
- package/dist/data-display/StatCard/StatCard.svelte +409 -0
- package/dist/data-display/StatCard/StatCard.svelte.d.ts +63 -0
- package/dist/data-display/StatCard/StatCard.svelte.d.ts.map +1 -0
- package/dist/data-display/StatCard/index.d.ts +2 -0
- package/dist/data-display/StatCard/index.d.ts.map +1 -0
- package/dist/data-display/StatCard/index.js +1 -0
- package/dist/data-display/index.d.ts +8 -0
- package/dist/data-display/index.d.ts.map +1 -0
- package/dist/data-display/index.js +7 -0
- package/dist/dialogs/ConfirmDialog/ConfirmDialog.svelte +693 -0
- package/dist/dialogs/ConfirmDialog/ConfirmDialog.svelte.d.ts +66 -0
- package/dist/dialogs/ConfirmDialog/ConfirmDialog.svelte.d.ts.map +1 -0
- package/dist/dialogs/ConfirmDialog/index.d.ts +2 -0
- package/dist/dialogs/ConfirmDialog/index.d.ts.map +1 -0
- package/dist/dialogs/ConfirmDialog/index.js +1 -0
- package/dist/dialogs/Modal/Modal.svelte +441 -0
- package/dist/dialogs/Modal/Modal.svelte.d.ts +69 -0
- package/dist/dialogs/Modal/Modal.svelte.d.ts.map +1 -0
- package/dist/dialogs/Modal/index.d.ts +2 -0
- package/dist/dialogs/Modal/index.d.ts.map +1 -0
- package/dist/dialogs/Modal/index.js +1 -0
- package/dist/dialogs/index.d.ts +8 -0
- package/dist/dialogs/index.d.ts.map +1 -0
- package/dist/dialogs/index.js +7 -0
- package/dist/feedback/Alert/Alert.svelte +565 -0
- package/dist/feedback/Alert/Alert.svelte.d.ts +60 -0
- package/dist/feedback/Alert/Alert.svelte.d.ts.map +1 -0
- package/dist/feedback/Alert/index.d.ts +2 -0
- package/dist/feedback/Alert/index.d.ts.map +1 -0
- package/dist/feedback/Alert/index.js +1 -0
- package/dist/feedback/EmptyState/EmptyState.svelte +377 -0
- package/dist/feedback/EmptyState/EmptyState.svelte.d.ts +63 -0
- package/dist/feedback/EmptyState/EmptyState.svelte.d.ts.map +1 -0
- package/dist/feedback/EmptyState/index.d.ts +2 -0
- package/dist/feedback/EmptyState/index.d.ts.map +1 -0
- package/dist/feedback/EmptyState/index.js +1 -0
- package/dist/feedback/ProgressBar/ProgressBar.svelte +585 -0
- package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts +68 -0
- package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts.map +1 -0
- package/dist/feedback/ProgressBar/index.d.ts +2 -0
- package/dist/feedback/ProgressBar/index.d.ts.map +1 -0
- package/dist/feedback/ProgressBar/index.js +1 -0
- package/dist/feedback/Skeleton/Skeleton.svelte +568 -0
- package/dist/feedback/Skeleton/Skeleton.svelte.d.ts +54 -0
- package/dist/feedback/Skeleton/Skeleton.svelte.d.ts.map +1 -0
- package/dist/feedback/Skeleton/index.d.ts +2 -0
- package/dist/feedback/Skeleton/index.d.ts.map +1 -0
- package/dist/feedback/Skeleton/index.js +1 -0
- package/dist/feedback/Spinner/Spinner.svelte +434 -0
- package/dist/feedback/Spinner/Spinner.svelte.d.ts +49 -0
- package/dist/feedback/Spinner/Spinner.svelte.d.ts.map +1 -0
- package/dist/feedback/Spinner/index.d.ts +2 -0
- package/dist/feedback/Spinner/index.d.ts.map +1 -0
- package/dist/feedback/Spinner/index.js +1 -0
- package/dist/feedback/Toast/Toast.svelte +587 -0
- package/dist/feedback/Toast/Toast.svelte.d.ts +55 -0
- package/dist/feedback/Toast/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast/ToastContainer.svelte +168 -0
- package/dist/feedback/Toast/ToastContainer.svelte.d.ts +28 -0
- package/dist/feedback/Toast/ToastContainer.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast/index.d.ts +4 -0
- package/dist/feedback/Toast/index.d.ts.map +1 -0
- package/dist/feedback/Toast/index.js +3 -0
- package/dist/feedback/Toast/toast-store.d.ts +72 -0
- package/dist/feedback/Toast/toast-store.d.ts.map +1 -0
- package/dist/feedback/Toast/toast-store.js +157 -0
- package/dist/feedback/index.d.ts +13 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +12 -0
- package/dist/forms/Checkbox/Checkbox.svelte +404 -0
- package/dist/forms/Checkbox/Checkbox.svelte.d.ts +62 -0
- package/dist/forms/Checkbox/Checkbox.svelte.d.ts.map +1 -0
- package/dist/forms/Checkbox/index.d.ts +2 -0
- package/dist/forms/Checkbox/index.d.ts.map +1 -0
- package/dist/forms/Checkbox/index.js +1 -0
- package/dist/forms/FormField/FormField.svelte +299 -0
- package/dist/forms/FormField/FormField.svelte.d.ts +43 -0
- package/dist/forms/FormField/FormField.svelte.d.ts.map +1 -0
- package/dist/forms/FormField/index.d.ts +2 -0
- package/dist/forms/FormField/index.d.ts.map +1 -0
- package/dist/forms/FormField/index.js +1 -0
- package/dist/forms/RadioGroup/RadioGroup.svelte +418 -0
- package/dist/forms/RadioGroup/RadioGroup.svelte.d.ts +70 -0
- package/dist/forms/RadioGroup/RadioGroup.svelte.d.ts.map +1 -0
- package/dist/forms/RadioGroup/index.d.ts +2 -0
- package/dist/forms/RadioGroup/index.d.ts.map +1 -0
- package/dist/forms/RadioGroup/index.js +1 -0
- package/dist/forms/Select/Select.svelte +548 -0
- package/dist/forms/Select/Select.svelte.d.ts +74 -0
- package/dist/forms/Select/Select.svelte.d.ts.map +1 -0
- package/dist/forms/Select/index.d.ts +2 -0
- package/dist/forms/Select/index.d.ts.map +1 -0
- package/dist/forms/Select/index.js +1 -0
- package/dist/forms/TextInput/TextInput.svelte +628 -0
- package/dist/forms/TextInput/TextInput.svelte.d.ts +97 -0
- package/dist/forms/TextInput/TextInput.svelte.d.ts.map +1 -0
- package/dist/forms/TextInput/index.d.ts +2 -0
- package/dist/forms/TextInput/index.d.ts.map +1 -0
- package/dist/forms/TextInput/index.js +1 -0
- package/dist/forms/Textarea/Textarea.svelte +587 -0
- package/dist/forms/Textarea/Textarea.svelte.d.ts +71 -0
- package/dist/forms/Textarea/Textarea.svelte.d.ts.map +1 -0
- package/dist/forms/Textarea/index.d.ts +2 -0
- package/dist/forms/Textarea/index.d.ts.map +1 -0
- package/dist/forms/Textarea/index.js +1 -0
- package/dist/forms/index.d.ts +13 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +12 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/layout/Card/Card.svelte +316 -0
- package/dist/layout/Card/Card.svelte.d.ts +63 -0
- package/dist/layout/Card/Card.svelte.d.ts.map +1 -0
- package/dist/layout/Card/index.d.ts +2 -0
- package/dist/layout/Card/index.d.ts.map +1 -0
- package/dist/layout/Card/index.js +1 -0
- package/dist/layout/Container/Container.svelte +252 -0
- package/dist/layout/Container/Container.svelte.d.ts +50 -0
- package/dist/layout/Container/Container.svelte.d.ts.map +1 -0
- package/dist/layout/Container/index.d.ts +2 -0
- package/dist/layout/Container/index.d.ts.map +1 -0
- package/dist/layout/Container/index.js +1 -0
- package/dist/layout/index.d.ts +8 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +7 -0
- package/dist/navigation/StepIndicator/StepIndicator.svelte +601 -0
- package/dist/navigation/StepIndicator/StepIndicator.svelte.d.ts +70 -0
- package/dist/navigation/StepIndicator/StepIndicator.svelte.d.ts.map +1 -0
- package/dist/navigation/StepIndicator/index.d.ts +2 -0
- package/dist/navigation/StepIndicator/index.d.ts.map +1 -0
- package/dist/navigation/StepIndicator/index.js +1 -0
- package/dist/navigation/index.d.ts +7 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +6 -0
- package/dist/primitives/Badge/Badge.svelte +365 -0
- package/dist/primitives/Badge/Badge.svelte.d.ts +39 -0
- package/dist/primitives/Badge/Badge.svelte.d.ts.map +1 -0
- package/dist/primitives/Badge/index.d.ts +2 -0
- package/dist/primitives/Badge/index.d.ts.map +1 -0
- package/dist/primitives/Badge/index.js +1 -0
- package/dist/primitives/Button/Button.svelte +430 -0
- package/dist/primitives/Button/Button.svelte.d.ts +50 -0
- package/dist/primitives/Button/Button.svelte.d.ts.map +1 -0
- package/dist/primitives/Button/index.d.ts +2 -0
- package/dist/primitives/Button/index.d.ts.map +1 -0
- package/dist/primitives/Button/index.js +1 -0
- package/dist/primitives/index.d.ts +9 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +8 -0
- package/dist/routes/+layout.svelte +12 -0
- package/dist/routes/+layout.svelte.d.ts +12 -0
- package/dist/routes/+layout.svelte.d.ts.map +1 -0
- package/dist/routes/+page.svelte +53 -0
- package/dist/routes/+page.svelte.d.ts +27 -0
- package/dist/routes/+page.svelte.d.ts.map +1 -0
- package/dist/styles/tokens.css +399 -0
- package/dist/types/index.d.ts +175 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/accessibility.d.ts +103 -0
- package/dist/utils/accessibility.d.ts.map +1 -0
- package/dist/utils/accessibility.js +202 -0
- package/dist/utils/cn.d.ts +71 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/dist/utils/cn.js +61 -0
- package/dist/utils/form-styles.d.ts +76 -0
- package/dist/utils/form-styles.d.ts.map +1 -0
- package/dist/utils/form-styles.js +95 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +13 -0
- package/dist/utils/keyboard.d.ts +94 -0
- package/dist/utils/keyboard.d.ts.map +1 -0
- package/dist/utils/keyboard.js +179 -0
- package/package.json +119 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type DialogVariant = 'default' | 'danger' | 'warning' | 'success';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Whether the dialog is open */
|
|
5
|
+
open?: boolean;
|
|
6
|
+
/** Dialog title */
|
|
7
|
+
title?: string;
|
|
8
|
+
/** Dialog message */
|
|
9
|
+
message?: string;
|
|
10
|
+
/** Text for confirm button */
|
|
11
|
+
confirmText?: string;
|
|
12
|
+
/** Text for cancel button */
|
|
13
|
+
cancelText?: string;
|
|
14
|
+
/** Visual variant */
|
|
15
|
+
variant?: DialogVariant;
|
|
16
|
+
/** Whether the dialog is in loading state */
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
/** Error message to display */
|
|
19
|
+
error?: string;
|
|
20
|
+
/** Focus confirm button on open (default: focus cancel for destructive actions) */
|
|
21
|
+
focusConfirm?: boolean;
|
|
22
|
+
/** Show close button in header */
|
|
23
|
+
showCloseButton?: boolean;
|
|
24
|
+
/** Require typing confirmation text for destructive actions */
|
|
25
|
+
requireConfirmation?: string;
|
|
26
|
+
/** Custom content instead of message */
|
|
27
|
+
children?: Snippet;
|
|
28
|
+
/** Custom icon snippet */
|
|
29
|
+
icon?: Snippet;
|
|
30
|
+
/** Additional CSS classes */
|
|
31
|
+
class?: string;
|
|
32
|
+
/** Confirm handler */
|
|
33
|
+
onconfirm?: () => void;
|
|
34
|
+
/** Cancel handler */
|
|
35
|
+
oncancel?: () => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* ConfirmDialog
|
|
39
|
+
*
|
|
40
|
+
* A world-class confirmation dialog for destructive or important actions in healthcare apps.
|
|
41
|
+
* Features proper focus management, semantic variants, and full accessibility.
|
|
42
|
+
*
|
|
43
|
+
* Features:
|
|
44
|
+
* - Multiple variants (default, danger, warning, success)
|
|
45
|
+
* - Focus management per WCAG guidelines
|
|
46
|
+
* - Loading state with spinner
|
|
47
|
+
* - Error display
|
|
48
|
+
* - Optional destructive confirmation (type to confirm)
|
|
49
|
+
* - Custom content support
|
|
50
|
+
* - Role-based semantics (dialog vs alertdialog)
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* <ConfirmDialog
|
|
54
|
+
* open={showConfirm}
|
|
55
|
+
* title="Delete Patient Record"
|
|
56
|
+
* message="This action cannot be undone. Are you sure you want to delete this patient record?"
|
|
57
|
+
* variant="danger"
|
|
58
|
+
* confirmText="Delete"
|
|
59
|
+
* onconfirm={handleDelete}
|
|
60
|
+
* oncancel={() => showConfirm = false}
|
|
61
|
+
* />
|
|
62
|
+
*/
|
|
63
|
+
declare const ConfirmDialog: import("svelte").Component<Props, {}, "">;
|
|
64
|
+
type ConfirmDialog = ReturnType<typeof ConfirmDialog>;
|
|
65
|
+
export default ConfirmDialog;
|
|
66
|
+
//# sourceMappingURL=ConfirmDialog.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.svelte.d.ts","sourceRoot":"","sources":["../../../src/dialogs/ConfirmDialog/ConfirmDialog.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKrC,KAAK,aAAa,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;AAElE,UAAU,KAAK;IACd,iCAAiC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB;IACrB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kCAAkC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0BAA0B;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAmOF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dialogs/ConfirmDialog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ConfirmDialog } from './ConfirmDialog.svelte';
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Modal
|
|
3
|
+
|
|
4
|
+
A world-class modal dialog component designed for healthcare applications.
|
|
5
|
+
Features proper focus management, keyboard navigation, and full accessibility.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Focus trap and focus restoration (WCAG 2.1 compliant)
|
|
9
|
+
- Multiple size variants including full-screen
|
|
10
|
+
- Scroll behavior options (body vs modal)
|
|
11
|
+
- Smooth enter/exit animations
|
|
12
|
+
- Dark mode support
|
|
13
|
+
- Prevent accidental close for critical modals
|
|
14
|
+
- Customizable header, body, and footer
|
|
15
|
+
|
|
16
|
+
@example
|
|
17
|
+
<Modal open={isOpen} onclose={() => isOpen = false} title="Patient Details">
|
|
18
|
+
<p>Modal content here</p>
|
|
19
|
+
|
|
20
|
+
{#snippet footer()}
|
|
21
|
+
<Button variant="secondary" onclick={() => isOpen = false}>Cancel</Button>
|
|
22
|
+
<Button variant="primary" onclick={handleSave}>Save</Button>
|
|
23
|
+
{/snippet}
|
|
24
|
+
</Modal>
|
|
25
|
+
-->
|
|
26
|
+
<script lang="ts">
|
|
27
|
+
import type { Snippet } from 'svelte';
|
|
28
|
+
import { X } from 'lucide-svelte';
|
|
29
|
+
import { createFocusTrap, Keys, generateId } from '../../utils/keyboard.js';
|
|
30
|
+
|
|
31
|
+
type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
|
32
|
+
type ModalPosition = 'center' | 'top';
|
|
33
|
+
type ScrollBehavior = 'inside' | 'outside';
|
|
34
|
+
|
|
35
|
+
interface Props {
|
|
36
|
+
/** Whether the modal is open */
|
|
37
|
+
open?: boolean;
|
|
38
|
+
/** Custom ID for the modal (auto-generated if not provided) */
|
|
39
|
+
id?: string;
|
|
40
|
+
/** Modal title for header and accessibility */
|
|
41
|
+
title?: string;
|
|
42
|
+
/** Size of the modal */
|
|
43
|
+
size?: ModalSize;
|
|
44
|
+
/** Position of the modal */
|
|
45
|
+
position?: ModalPosition;
|
|
46
|
+
/** Scroll behavior - 'inside' scrolls modal body, 'outside' scrolls the page */
|
|
47
|
+
scrollBehavior?: ScrollBehavior;
|
|
48
|
+
/** Whether to show close button in header */
|
|
49
|
+
showCloseButton?: boolean;
|
|
50
|
+
/** Whether clicking backdrop closes modal */
|
|
51
|
+
closeOnBackdrop?: boolean;
|
|
52
|
+
/** Whether pressing Escape closes modal */
|
|
53
|
+
closeOnEscape?: boolean;
|
|
54
|
+
/** Prevent closing (for critical modals with required actions) */
|
|
55
|
+
preventClose?: boolean;
|
|
56
|
+
/** Element to focus when modal opens (defaults to first focusable or close button) */
|
|
57
|
+
initialFocus?: HTMLElement | null;
|
|
58
|
+
/** Header content snippet (overrides title) */
|
|
59
|
+
header?: Snippet;
|
|
60
|
+
/** Footer content snippet */
|
|
61
|
+
footer?: Snippet;
|
|
62
|
+
/** Main content */
|
|
63
|
+
children?: Snippet;
|
|
64
|
+
/** Additional CSS classes for the modal container */
|
|
65
|
+
class?: string;
|
|
66
|
+
/** Close handler */
|
|
67
|
+
onclose?: () => void;
|
|
68
|
+
/** Test ID for e2e testing */
|
|
69
|
+
testId?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let {
|
|
73
|
+
open = false,
|
|
74
|
+
id,
|
|
75
|
+
title = 'Dialog',
|
|
76
|
+
size = 'md',
|
|
77
|
+
position = 'center',
|
|
78
|
+
scrollBehavior = 'inside',
|
|
79
|
+
showCloseButton = true,
|
|
80
|
+
closeOnBackdrop = true,
|
|
81
|
+
closeOnEscape = true,
|
|
82
|
+
preventClose = false,
|
|
83
|
+
initialFocus,
|
|
84
|
+
header,
|
|
85
|
+
footer,
|
|
86
|
+
children,
|
|
87
|
+
class: className = '',
|
|
88
|
+
onclose,
|
|
89
|
+
testId
|
|
90
|
+
}: Props = $props();
|
|
91
|
+
|
|
92
|
+
const generatedId = generateId('modal');
|
|
93
|
+
const modalId = $derived(id || generatedId);
|
|
94
|
+
const titleId = $derived(`${modalId}-title`);
|
|
95
|
+
const descriptionId = $derived(`${modalId}-description`);
|
|
96
|
+
|
|
97
|
+
let dialogElement = $state<HTMLDivElement | null>(null);
|
|
98
|
+
let closeButtonElement = $state<HTMLButtonElement | null>(null);
|
|
99
|
+
let focusTrap: ReturnType<typeof createFocusTrap> | null = null;
|
|
100
|
+
let previouslyFocusedElement: HTMLElement | null = null;
|
|
101
|
+
|
|
102
|
+
// Calculate scrollbar width to prevent layout shift
|
|
103
|
+
function getScrollbarWidth(): number {
|
|
104
|
+
return window.innerWidth - document.documentElement.clientWidth;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle focus trap and focus restoration
|
|
108
|
+
$effect(() => {
|
|
109
|
+
if (open && dialogElement) {
|
|
110
|
+
// Save the currently focused element
|
|
111
|
+
previouslyFocusedElement = document.activeElement as HTMLElement;
|
|
112
|
+
|
|
113
|
+
// Create and activate focus trap
|
|
114
|
+
focusTrap = createFocusTrap(dialogElement);
|
|
115
|
+
focusTrap.activate();
|
|
116
|
+
|
|
117
|
+
// Lock body scroll and prevent layout shift
|
|
118
|
+
const scrollbarWidth = getScrollbarWidth();
|
|
119
|
+
document.body.style.overflow = 'hidden';
|
|
120
|
+
if (scrollbarWidth > 0) {
|
|
121
|
+
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Focus initial element or first focusable
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
if (initialFocus) {
|
|
127
|
+
initialFocus.focus();
|
|
128
|
+
} else if (closeButtonElement) {
|
|
129
|
+
closeButtonElement.focus();
|
|
130
|
+
}
|
|
131
|
+
}, 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
focusTrap?.deactivate();
|
|
136
|
+
document.body.style.overflow = '';
|
|
137
|
+
document.body.style.paddingRight = '';
|
|
138
|
+
|
|
139
|
+
// Restore focus
|
|
140
|
+
if (previouslyFocusedElement?.focus) {
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
previouslyFocusedElement?.focus();
|
|
143
|
+
}, 0);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
function handleClose() {
|
|
149
|
+
if (!preventClose) {
|
|
150
|
+
onclose?.();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function handleBackdropClick(e: MouseEvent) {
|
|
155
|
+
if (closeOnBackdrop && e.target === e.currentTarget) {
|
|
156
|
+
handleClose();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
161
|
+
if (closeOnEscape && e.key === Keys.Escape) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
handleClose();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<svelte:window onkeydown={open ? handleKeyDown : undefined} />
|
|
169
|
+
|
|
170
|
+
{#if open}
|
|
171
|
+
<!-- Backdrop -->
|
|
172
|
+
<div
|
|
173
|
+
class="modal-backdrop modal-position-{position}"
|
|
174
|
+
role="presentation"
|
|
175
|
+
onclick={handleBackdropClick}
|
|
176
|
+
onkeydown={() => {}}
|
|
177
|
+
>
|
|
178
|
+
<!-- Modal container -->
|
|
179
|
+
<div
|
|
180
|
+
bind:this={dialogElement}
|
|
181
|
+
id={modalId}
|
|
182
|
+
class="modal-container modal-{size} modal-scroll-{scrollBehavior} {className}"
|
|
183
|
+
role="dialog"
|
|
184
|
+
aria-modal="true"
|
|
185
|
+
aria-labelledby={titleId}
|
|
186
|
+
aria-describedby={children ? descriptionId : undefined}
|
|
187
|
+
data-testid={testId}
|
|
188
|
+
>
|
|
189
|
+
<!-- Header -->
|
|
190
|
+
{#if header || title || showCloseButton}
|
|
191
|
+
<div class="modal-header">
|
|
192
|
+
<div id={titleId} class="modal-title-wrapper">
|
|
193
|
+
{#if header}
|
|
194
|
+
{@render header()}
|
|
195
|
+
{:else if title}
|
|
196
|
+
<h2 class="modal-title">{title}</h2>
|
|
197
|
+
{/if}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{#if showCloseButton && !preventClose}
|
|
201
|
+
<button
|
|
202
|
+
bind:this={closeButtonElement}
|
|
203
|
+
type="button"
|
|
204
|
+
class="modal-close-btn"
|
|
205
|
+
onclick={handleClose}
|
|
206
|
+
aria-label="Close dialog"
|
|
207
|
+
>
|
|
208
|
+
<X size={20} aria-hidden="true" />
|
|
209
|
+
</button>
|
|
210
|
+
{/if}
|
|
211
|
+
</div>
|
|
212
|
+
{/if}
|
|
213
|
+
|
|
214
|
+
<!-- Body -->
|
|
215
|
+
{#if children}
|
|
216
|
+
<div id={descriptionId} class="modal-body">
|
|
217
|
+
{@render children()}
|
|
218
|
+
</div>
|
|
219
|
+
{/if}
|
|
220
|
+
|
|
221
|
+
<!-- Footer -->
|
|
222
|
+
{#if footer}
|
|
223
|
+
<div class="modal-footer">
|
|
224
|
+
{@render footer()}
|
|
225
|
+
</div>
|
|
226
|
+
{/if}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
{/if}
|
|
230
|
+
|
|
231
|
+
<style>
|
|
232
|
+
/* ========================================
|
|
233
|
+
BACKDROP
|
|
234
|
+
======================================== */
|
|
235
|
+
.modal-backdrop {
|
|
236
|
+
position: fixed;
|
|
237
|
+
inset: 0;
|
|
238
|
+
z-index: 1000;
|
|
239
|
+
display: flex;
|
|
240
|
+
justify-content: center;
|
|
241
|
+
padding: 1rem;
|
|
242
|
+
background-color: var(--ui-backdrop);
|
|
243
|
+
backdrop-filter: blur(4px);
|
|
244
|
+
-webkit-backdrop-filter: blur(4px);
|
|
245
|
+
animation: modal-fade-in 0.2s ease-out;
|
|
246
|
+
overflow-y: auto;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.modal-position-center {
|
|
250
|
+
align-items: center;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.modal-position-top {
|
|
254
|
+
align-items: flex-start;
|
|
255
|
+
padding-top: 5rem;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@keyframes modal-fade-in {
|
|
259
|
+
from { opacity: 0; }
|
|
260
|
+
to { opacity: 1; }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ========================================
|
|
264
|
+
CONTAINER
|
|
265
|
+
======================================== */
|
|
266
|
+
.modal-container {
|
|
267
|
+
position: relative;
|
|
268
|
+
display: flex;
|
|
269
|
+
flex-direction: column;
|
|
270
|
+
width: 100%;
|
|
271
|
+
max-height: calc(100vh - 2rem);
|
|
272
|
+
background-color: var(--ui-bg-primary);
|
|
273
|
+
border: 1px solid var(--ui-border-default);
|
|
274
|
+
border-radius: 1rem;
|
|
275
|
+
box-shadow:
|
|
276
|
+
0 20px 25px -5px rgb(0 0 0 / 0.1),
|
|
277
|
+
0 8px 10px -6px rgb(0 0 0 / 0.1);
|
|
278
|
+
animation: modal-scale-in 0.3s ease-out;
|
|
279
|
+
overflow: hidden;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@keyframes modal-scale-in {
|
|
283
|
+
from {
|
|
284
|
+
opacity: 0;
|
|
285
|
+
transform: scale(0.95) translateY(10px);
|
|
286
|
+
}
|
|
287
|
+
to {
|
|
288
|
+
opacity: 1;
|
|
289
|
+
transform: scale(1) translateY(0);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* Size variants */
|
|
294
|
+
.modal-sm { max-width: 24rem; }
|
|
295
|
+
.modal-md { max-width: 28rem; }
|
|
296
|
+
.modal-lg { max-width: 32rem; }
|
|
297
|
+
.modal-xl { max-width: 36rem; }
|
|
298
|
+
.modal-2xl { max-width: 42rem; }
|
|
299
|
+
.modal-full { max-width: 56rem; }
|
|
300
|
+
|
|
301
|
+
/* Scroll behavior */
|
|
302
|
+
.modal-scroll-inside {
|
|
303
|
+
max-height: calc(100vh - 4rem);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.modal-scroll-inside .modal-body {
|
|
307
|
+
overflow-y: auto;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.modal-scroll-outside {
|
|
311
|
+
max-height: none;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* ========================================
|
|
315
|
+
HEADER
|
|
316
|
+
======================================== */
|
|
317
|
+
.modal-header {
|
|
318
|
+
display: flex;
|
|
319
|
+
align-items: center;
|
|
320
|
+
justify-content: space-between;
|
|
321
|
+
gap: 1rem;
|
|
322
|
+
padding: 1rem 1.5rem;
|
|
323
|
+
border-bottom: 1px solid var(--ui-border-default);
|
|
324
|
+
background-color: var(--ui-bg-secondary);
|
|
325
|
+
border-radius: 1rem 1rem 0 0;
|
|
326
|
+
flex-shrink: 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.modal-title-wrapper {
|
|
330
|
+
flex: 1;
|
|
331
|
+
min-width: 0;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.modal-title {
|
|
335
|
+
margin: 0;
|
|
336
|
+
font-size: 1.125rem;
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
color: var(--ui-text-primary);
|
|
339
|
+
line-height: 1.4;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* ========================================
|
|
343
|
+
CLOSE BUTTON
|
|
344
|
+
======================================== */
|
|
345
|
+
.modal-close-btn {
|
|
346
|
+
display: flex;
|
|
347
|
+
align-items: center;
|
|
348
|
+
justify-content: center;
|
|
349
|
+
padding: 0.375rem;
|
|
350
|
+
background: transparent;
|
|
351
|
+
border: none;
|
|
352
|
+
border-radius: 0.5rem;
|
|
353
|
+
color: var(--ui-text-tertiary);
|
|
354
|
+
cursor: pointer;
|
|
355
|
+
transition: all 0.15s ease;
|
|
356
|
+
flex-shrink: 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.modal-close-btn:hover {
|
|
360
|
+
background-color: var(--ui-bg-tertiary);
|
|
361
|
+
color: var(--ui-text-primary);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.modal-close-btn:focus-visible {
|
|
365
|
+
outline: none;
|
|
366
|
+
box-shadow: 0 0 0 3px rgb(var(--ui-color-primary) / 0.4);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
:global([data-theme='dark']) .modal-close-btn:focus-visible {
|
|
370
|
+
box-shadow: 0 0 0 3px rgb(var(--ui-color-primary-light) / 0.5);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@media (prefers-color-scheme: dark) {
|
|
374
|
+
:global(:root:not([data-theme='light'])) .modal-close-btn:focus-visible {
|
|
375
|
+
box-shadow: 0 0 0 3px rgb(var(--ui-color-primary-light) / 0.5);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* ========================================
|
|
380
|
+
BODY
|
|
381
|
+
======================================== */
|
|
382
|
+
.modal-body {
|
|
383
|
+
padding: 1.25rem 1.5rem;
|
|
384
|
+
flex: 1;
|
|
385
|
+
min-height: 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* ========================================
|
|
389
|
+
FOOTER
|
|
390
|
+
======================================== */
|
|
391
|
+
.modal-footer {
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
justify-content: flex-end;
|
|
395
|
+
gap: 0.75rem;
|
|
396
|
+
padding: 1rem 1.5rem;
|
|
397
|
+
border-top: 1px solid var(--ui-border-default);
|
|
398
|
+
background-color: var(--ui-bg-secondary);
|
|
399
|
+
border-radius: 0 0 1rem 1rem;
|
|
400
|
+
flex-shrink: 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* ========================================
|
|
404
|
+
REDUCED MOTION
|
|
405
|
+
======================================== */
|
|
406
|
+
@media (prefers-reduced-motion: reduce) {
|
|
407
|
+
.modal-backdrop,
|
|
408
|
+
.modal-container {
|
|
409
|
+
animation: none;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* ========================================
|
|
414
|
+
RESPONSIVE
|
|
415
|
+
======================================== */
|
|
416
|
+
@media (max-width: 640px) {
|
|
417
|
+
.modal-backdrop {
|
|
418
|
+
padding: 0;
|
|
419
|
+
align-items: flex-end;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.modal-position-top {
|
|
423
|
+
padding-top: 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.modal-container {
|
|
427
|
+
max-width: 100%;
|
|
428
|
+
max-height: 90vh;
|
|
429
|
+
border-radius: 1rem 1rem 0 0;
|
|
430
|
+
margin: 0;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.modal-header {
|
|
434
|
+
border-radius: 1rem 1rem 0 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.modal-footer {
|
|
438
|
+
border-radius: 0;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
|
3
|
+
type ModalPosition = 'center' | 'top';
|
|
4
|
+
type ScrollBehavior = 'inside' | 'outside';
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Whether the modal is open */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
/** Custom ID for the modal (auto-generated if not provided) */
|
|
9
|
+
id?: string;
|
|
10
|
+
/** Modal title for header and accessibility */
|
|
11
|
+
title?: string;
|
|
12
|
+
/** Size of the modal */
|
|
13
|
+
size?: ModalSize;
|
|
14
|
+
/** Position of the modal */
|
|
15
|
+
position?: ModalPosition;
|
|
16
|
+
/** Scroll behavior - 'inside' scrolls modal body, 'outside' scrolls the page */
|
|
17
|
+
scrollBehavior?: ScrollBehavior;
|
|
18
|
+
/** Whether to show close button in header */
|
|
19
|
+
showCloseButton?: boolean;
|
|
20
|
+
/** Whether clicking backdrop closes modal */
|
|
21
|
+
closeOnBackdrop?: boolean;
|
|
22
|
+
/** Whether pressing Escape closes modal */
|
|
23
|
+
closeOnEscape?: boolean;
|
|
24
|
+
/** Prevent closing (for critical modals with required actions) */
|
|
25
|
+
preventClose?: boolean;
|
|
26
|
+
/** Element to focus when modal opens (defaults to first focusable or close button) */
|
|
27
|
+
initialFocus?: HTMLElement | null;
|
|
28
|
+
/** Header content snippet (overrides title) */
|
|
29
|
+
header?: Snippet;
|
|
30
|
+
/** Footer content snippet */
|
|
31
|
+
footer?: Snippet;
|
|
32
|
+
/** Main content */
|
|
33
|
+
children?: Snippet;
|
|
34
|
+
/** Additional CSS classes for the modal container */
|
|
35
|
+
class?: string;
|
|
36
|
+
/** Close handler */
|
|
37
|
+
onclose?: () => void;
|
|
38
|
+
/** Test ID for e2e testing */
|
|
39
|
+
testId?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Modal
|
|
43
|
+
*
|
|
44
|
+
* A world-class modal dialog component designed for healthcare applications.
|
|
45
|
+
* Features proper focus management, keyboard navigation, and full accessibility.
|
|
46
|
+
*
|
|
47
|
+
* Features:
|
|
48
|
+
* - Focus trap and focus restoration (WCAG 2.1 compliant)
|
|
49
|
+
* - Multiple size variants including full-screen
|
|
50
|
+
* - Scroll behavior options (body vs modal)
|
|
51
|
+
* - Smooth enter/exit animations
|
|
52
|
+
* - Dark mode support
|
|
53
|
+
* - Prevent accidental close for critical modals
|
|
54
|
+
* - Customizable header, body, and footer
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* <Modal open={isOpen} onclose={() => isOpen = false} title="Patient Details">
|
|
58
|
+
* <p>Modal content here</p>
|
|
59
|
+
*
|
|
60
|
+
* {#snippet footer()}
|
|
61
|
+
* <Button variant="secondary" onclick={() => isOpen = false}>Cancel</Button>
|
|
62
|
+
* <Button variant="primary" onclick={handleSave}>Save</Button>
|
|
63
|
+
* {/snippet}
|
|
64
|
+
* </Modal>
|
|
65
|
+
*/
|
|
66
|
+
declare const Modal: import("svelte").Component<Props, {}, "">;
|
|
67
|
+
type Modal = ReturnType<typeof Modal>;
|
|
68
|
+
export default Modal;
|
|
69
|
+
//# sourceMappingURL=Modal.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Modal.svelte.d.ts","sourceRoot":"","sources":["../../../src/dialogs/Modal/Modal.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKrC,KAAK,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC;AAC5D,KAAK,aAAa,GAAG,QAAQ,GAAG,KAAK,CAAC;AACtC,KAAK,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE3C,UAAU,KAAK;IACd,gCAAgC;IAChC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,+DAA+D;IAC/D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,4BAA4B;IAC5B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,gFAAgF;IAChF,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,6CAA6C;IAC7C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,6CAA6C;IAC7C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,2CAA2C;IAC3C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kEAAkE;IAClE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sFAAsF;IACtF,YAAY,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAClC,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6BAA6B;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,mBAAmB;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AA0JF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,QAAA,MAAM,KAAK,2CAAwC,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dialogs/Modal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Modal } from './Modal.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dialogs/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC"}
|