lutra 0.1.29 → 0.1.30
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/dist/components/Modal.svelte +57 -54
- package/dist/components/Modal.svelte.d.ts +16 -0
- package/dist/components/ModalContent.svelte +218 -0
- package/dist/components/ModalContent.svelte.d.ts +19 -0
- package/dist/components/ModalTypes.d.ts +29 -0
- package/dist/components/ModalTypes.js +3 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/components/modals.svelte.d.ts +5 -0
- package/dist/components/modals.svelte.js +107 -0
- package/dist/css/1-props.css +30 -1
- package/dist/css/2-base.css +5 -0
- package/dist/css/themes/DefaultTheme.css +20 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import UiContent from "./UIContent.svelte";
|
|
3
|
+
import ModalContent from "./ModalContent.svelte";
|
|
3
4
|
import { getContext, type Snippet } from "svelte";
|
|
4
|
-
import { slidefade } from "../util/transitions.js";
|
|
5
5
|
import { attr } from "../util/attr.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
* <div>bar</div>
|
|
18
18
|
* {/snippet}
|
|
19
19
|
* <Modal trigger={trigger} content={content} />
|
|
20
|
-
* <Modal trigger={trigger} content={content} hover />
|
|
21
20
|
* </div>
|
|
22
21
|
*/
|
|
23
22
|
let {
|
|
@@ -25,7 +24,15 @@
|
|
|
25
24
|
content,
|
|
26
25
|
buttons,
|
|
27
26
|
trigger,
|
|
27
|
+
title,
|
|
28
28
|
shape = "rounded",
|
|
29
|
+
unstyled = false,
|
|
30
|
+
showScrim = true,
|
|
31
|
+
closeOnScrim = true,
|
|
32
|
+
trapFocus = true,
|
|
33
|
+
dismissOnEsc = true,
|
|
34
|
+
maxWidth,
|
|
35
|
+
maxHeight,
|
|
29
36
|
}: {
|
|
30
37
|
/** Whether the modal should be contained with a border */
|
|
31
38
|
contained?: boolean;
|
|
@@ -35,8 +42,24 @@
|
|
|
35
42
|
trigger: Snippet<[attrs: (node: Element) => void]>;
|
|
36
43
|
/** Buttons to be displayed in the modal */
|
|
37
44
|
buttons?: Snippet<[close: () => void]>;
|
|
45
|
+
/** Optional title for the modal (improves a11y) */
|
|
46
|
+
title?: string;
|
|
38
47
|
/** The shape of the modal */
|
|
39
48
|
shape?: "rounded" | "sharp";
|
|
49
|
+
/** Whether to remove default styling */
|
|
50
|
+
unstyled?: boolean;
|
|
51
|
+
/** Whether to show the backdrop scrim */
|
|
52
|
+
showScrim?: boolean;
|
|
53
|
+
/** Whether clicking the scrim closes the modal */
|
|
54
|
+
closeOnScrim?: boolean;
|
|
55
|
+
/** Whether to trap focus within the modal */
|
|
56
|
+
trapFocus?: boolean;
|
|
57
|
+
/** Whether pressing Escape closes the modal */
|
|
58
|
+
dismissOnEsc?: boolean;
|
|
59
|
+
/** Maximum width of the modal */
|
|
60
|
+
maxWidth?: string;
|
|
61
|
+
/** Maximum height of the modal */
|
|
62
|
+
maxHeight?: string;
|
|
40
63
|
} = $props();
|
|
41
64
|
|
|
42
65
|
if(contained === undefined) { contained = getContext('lutra.modal.contained') ?? getContext('lutra.contained') ?? false; }
|
|
@@ -44,20 +67,20 @@
|
|
|
44
67
|
const id = `po-${Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)}`;
|
|
45
68
|
let isOpen = $state(false);
|
|
46
69
|
|
|
47
|
-
function closeModal() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
function closeModal() {
|
|
71
|
+
document.getElementById(id)?.hidePopover();
|
|
72
|
+
isOpen = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toggleModal() {
|
|
76
|
+
isOpen = !isOpen;
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
$effect(() => {
|
|
57
80
|
if(isOpen) {
|
|
58
|
-
document.
|
|
81
|
+
document.documentElement.style.overflow = "hidden";
|
|
59
82
|
} else {
|
|
60
|
-
document.
|
|
83
|
+
document.documentElement.style.overflow = "";
|
|
61
84
|
}
|
|
62
85
|
});
|
|
63
86
|
|
|
@@ -77,19 +100,22 @@
|
|
|
77
100
|
</div>
|
|
78
101
|
{#if isOpen}
|
|
79
102
|
<UiContent>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
<div {id} popover="auto" class="ModalContainer">
|
|
104
|
+
<ModalContent
|
|
105
|
+
{shape}
|
|
106
|
+
{contained}
|
|
107
|
+
{unstyled}
|
|
108
|
+
{showScrim}
|
|
109
|
+
{closeOnScrim}
|
|
110
|
+
{trapFocus}
|
|
111
|
+
{dismissOnEsc}
|
|
112
|
+
{maxWidth}
|
|
113
|
+
{maxHeight}
|
|
114
|
+
{title}
|
|
115
|
+
{buttons}
|
|
116
|
+
children={content}
|
|
117
|
+
close={closeModal}
|
|
118
|
+
/>
|
|
93
119
|
</div>
|
|
94
120
|
</UiContent>
|
|
95
121
|
{/if}
|
|
@@ -100,44 +126,21 @@
|
|
|
100
126
|
position: relative;
|
|
101
127
|
display: inline-block;
|
|
102
128
|
}
|
|
129
|
+
|
|
103
130
|
.ModalContainer {
|
|
104
131
|
border: 0;
|
|
105
|
-
width:
|
|
106
|
-
height:
|
|
107
|
-
background-color: var(--bg-overlay);
|
|
108
|
-
backdrop-filter: var(--overlay-filter);
|
|
132
|
+
width: 100svw;
|
|
133
|
+
height: 100svh;
|
|
109
134
|
overflow-y: auto;
|
|
110
|
-
}
|
|
111
|
-
.ModalContent {
|
|
112
|
-
background: var(--bg, var(--background-main));
|
|
113
|
-
box-shadow: var(--shadow);
|
|
114
|
-
opacity: 1;
|
|
115
|
-
position: absolute;
|
|
116
|
-
left: 50%;
|
|
117
|
-
top: 50%;
|
|
118
|
-
transform: translate(-50%, -50%);
|
|
119
|
-
box-shadow: 0 0.25rem 1rem 0 var(--shadow);
|
|
120
|
-
}
|
|
121
|
-
.ModalContentInsize {
|
|
122
|
-
container-type: inline-size;
|
|
123
|
-
}
|
|
124
|
-
.ModalContent.rounded {
|
|
125
|
-
border-radius: var(--border-radius);
|
|
126
|
-
}
|
|
127
|
-
.ModalContent.contained {
|
|
128
|
-
border: var(--border);
|
|
129
|
-
}
|
|
130
|
-
.ModalActions {
|
|
131
135
|
display: flex;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
justify-content: flex-end;
|
|
135
|
-
padding: 1rem;
|
|
136
|
-
background: var(--bg-subtle) linear-gradient(0deg, transparent, 95%, color-mix(in hsl, transparent 95%, var(--mix-target)));
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: center;
|
|
137
138
|
}
|
|
139
|
+
|
|
138
140
|
[popover] {
|
|
139
141
|
animation: fadeIn 0.2s;
|
|
140
142
|
}
|
|
143
|
+
|
|
141
144
|
@keyframes fadeIn {
|
|
142
145
|
from {
|
|
143
146
|
opacity: 0;
|
|
@@ -8,8 +8,24 @@ type $$ComponentProps = {
|
|
|
8
8
|
trigger: Snippet<[attrs: (node: Element) => void]>;
|
|
9
9
|
/** Buttons to be displayed in the modal */
|
|
10
10
|
buttons?: Snippet<[close: () => void]>;
|
|
11
|
+
/** Optional title for the modal (improves a11y) */
|
|
12
|
+
title?: string;
|
|
11
13
|
/** The shape of the modal */
|
|
12
14
|
shape?: "rounded" | "sharp";
|
|
15
|
+
/** Whether to remove default styling */
|
|
16
|
+
unstyled?: boolean;
|
|
17
|
+
/** Whether to show the backdrop scrim */
|
|
18
|
+
showScrim?: boolean;
|
|
19
|
+
/** Whether clicking the scrim closes the modal */
|
|
20
|
+
closeOnScrim?: boolean;
|
|
21
|
+
/** Whether to trap focus within the modal */
|
|
22
|
+
trapFocus?: boolean;
|
|
23
|
+
/** Whether pressing Escape closes the modal */
|
|
24
|
+
dismissOnEsc?: boolean;
|
|
25
|
+
/** Maximum width of the modal */
|
|
26
|
+
maxWidth?: string;
|
|
27
|
+
/** Maximum height of the modal */
|
|
28
|
+
maxHeight?: string;
|
|
13
29
|
};
|
|
14
30
|
declare const Modal: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
31
|
type Modal = ReturnType<typeof Modal>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { onMount, onDestroy } from 'svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
children,
|
|
7
|
+
buttons,
|
|
8
|
+
close,
|
|
9
|
+
title,
|
|
10
|
+
shape = 'rounded',
|
|
11
|
+
contained = true,
|
|
12
|
+
unstyled = false,
|
|
13
|
+
showScrim = true,
|
|
14
|
+
closeOnScrim = true,
|
|
15
|
+
trapFocus = true,
|
|
16
|
+
dismissOnEsc = true,
|
|
17
|
+
maxWidth,
|
|
18
|
+
maxHeight,
|
|
19
|
+
}: {
|
|
20
|
+
children: Snippet<[close: () => void]>;
|
|
21
|
+
buttons?: Snippet<[close: () => void]>;
|
|
22
|
+
close: () => void;
|
|
23
|
+
title?: string;
|
|
24
|
+
shape?: 'rounded' | 'sharp';
|
|
25
|
+
contained?: boolean;
|
|
26
|
+
unstyled?: boolean;
|
|
27
|
+
showScrim?: boolean;
|
|
28
|
+
closeOnScrim?: boolean;
|
|
29
|
+
trapFocus?: boolean;
|
|
30
|
+
dismissOnEsc?: boolean;
|
|
31
|
+
maxWidth?: string;
|
|
32
|
+
maxHeight?: string;
|
|
33
|
+
} = $props();
|
|
34
|
+
|
|
35
|
+
let dialogEl: HTMLDivElement | null = $state(null);
|
|
36
|
+
let previousActiveElement: HTMLElement | null = null;
|
|
37
|
+
const titleId = title ? `modal-title-${crypto.randomUUID()}` : undefined;
|
|
38
|
+
|
|
39
|
+
onMount(() => {
|
|
40
|
+
previousActiveElement = document.activeElement as HTMLElement;
|
|
41
|
+
if (dialogEl && trapFocus) {
|
|
42
|
+
// Focus first focusable element
|
|
43
|
+
const focusableElements = getFocusableElements();
|
|
44
|
+
if (focusableElements.length > 0) {
|
|
45
|
+
focusableElements[0].focus();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
onDestroy(() => {
|
|
51
|
+
// Restore focus on unmount
|
|
52
|
+
previousActiveElement?.focus();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function getFocusableElements(): HTMLElement[] {
|
|
56
|
+
if (!dialogEl) return [];
|
|
57
|
+
const selector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
58
|
+
return Array.from(dialogEl.querySelectorAll(selector));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
62
|
+
if (dismissOnEsc && e.key === 'Escape') {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
close();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (trapFocus && e.key === 'Tab') {
|
|
69
|
+
const focusableElements = getFocusableElements();
|
|
70
|
+
if (focusableElements.length === 0) return;
|
|
71
|
+
|
|
72
|
+
const firstElement = focusableElements[0];
|
|
73
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
74
|
+
const activeElement = document.activeElement;
|
|
75
|
+
|
|
76
|
+
if (e.shiftKey) {
|
|
77
|
+
// Shift+Tab
|
|
78
|
+
if (activeElement === firstElement) {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
lastElement.focus();
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Tab
|
|
84
|
+
if (activeElement === lastElement) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
firstElement.focus();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleScrimClick(e: MouseEvent) {
|
|
93
|
+
if (closeOnScrim && e.target === e.currentTarget) {
|
|
94
|
+
close();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
{#if showScrim}
|
|
100
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
101
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
102
|
+
<div class="ModalScrim" onclick={handleScrimClick}></div>
|
|
103
|
+
{/if}
|
|
104
|
+
|
|
105
|
+
<div
|
|
106
|
+
class="ModalContent {shape}"
|
|
107
|
+
class:contained
|
|
108
|
+
class:unstyled
|
|
109
|
+
role="dialog"
|
|
110
|
+
aria-modal="true"
|
|
111
|
+
aria-labelledby={titleId}
|
|
112
|
+
tabindex="-1"
|
|
113
|
+
bind:this={dialogEl}
|
|
114
|
+
onkeydown={handleKeydown}
|
|
115
|
+
style="--modal-max-width: {maxWidth}; --modal-max-height: {maxHeight};"
|
|
116
|
+
>
|
|
117
|
+
<div class="ModalContentArea">
|
|
118
|
+
{#if title}
|
|
119
|
+
<h2 id={titleId} class="ModalTitle">{title}</h2>
|
|
120
|
+
{/if}
|
|
121
|
+
{@render children(close)}
|
|
122
|
+
</div>
|
|
123
|
+
{#if buttons}
|
|
124
|
+
<div class="ModalActions">
|
|
125
|
+
{@render buttons(close)}
|
|
126
|
+
</div>
|
|
127
|
+
{/if}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<style>
|
|
131
|
+
.ModalScrim {
|
|
132
|
+
background: var(--scrim-background);
|
|
133
|
+
backdrop-filter: var(--scrim-backdrop-filter);
|
|
134
|
+
position: fixed;
|
|
135
|
+
inset: 0;
|
|
136
|
+
z-index: -1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.ModalContent {
|
|
140
|
+
display: grid;
|
|
141
|
+
grid-template-rows: 1fr auto;
|
|
142
|
+
gap: var(--modal-gap);
|
|
143
|
+
max-width: min(var(--modal-max-width, 40rem), calc(100svw - 2rem));
|
|
144
|
+
max-height: min(var(--modal-max-height, 80svh), calc(100svh - 2rem));
|
|
145
|
+
background: var(--modal-background);
|
|
146
|
+
border: var(--modal-border);
|
|
147
|
+
border-radius: var(--modal-border-radius);
|
|
148
|
+
box-shadow: 0 0.5rem 1rem var(--modal-shadow-color);
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
position: relative;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.ModalContent.sharp {
|
|
154
|
+
border-radius: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.ModalContent:not(.contained) {
|
|
158
|
+
border: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.ModalContent.unstyled {
|
|
162
|
+
background: transparent;
|
|
163
|
+
border: none;
|
|
164
|
+
box-shadow: none;
|
|
165
|
+
max-width: none;
|
|
166
|
+
max-height: none;
|
|
167
|
+
display: block;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.ModalContentArea {
|
|
171
|
+
overflow: auto;
|
|
172
|
+
scrollbar-gutter: stable;
|
|
173
|
+
scrollbar-width: thin;
|
|
174
|
+
padding: var(--modal-padding);
|
|
175
|
+
text-wrap: pretty;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.ModalContent.unstyled .ModalContentArea {
|
|
179
|
+
padding: 0;
|
|
180
|
+
overflow: visible;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.ModalTitle {
|
|
184
|
+
margin-block-start: 0;
|
|
185
|
+
margin-block-end: var(--space-md);
|
|
186
|
+
font-size: var(--font-size-h4);
|
|
187
|
+
font-weight: var(--font-weight-semibold);
|
|
188
|
+
color: var(--text-color-heading);
|
|
189
|
+
line-height: var(--font-line-height-heading);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.ModalContent.unstyled .ModalTitle {
|
|
193
|
+
margin: 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.ModalActions {
|
|
197
|
+
background: var(--modal-actions-background);
|
|
198
|
+
border-top: var(--modal-border-size) var(--modal-border-style) var(--modal-actions-border-color);
|
|
199
|
+
padding: var(--modal-actions-padding);
|
|
200
|
+
display: flex;
|
|
201
|
+
gap: var(--space-sm);
|
|
202
|
+
justify-content: flex-end;
|
|
203
|
+
align-items: center;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.ModalContent.unstyled .ModalActions {
|
|
207
|
+
background: transparent;
|
|
208
|
+
border: none;
|
|
209
|
+
padding: 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@media (prefers-reduced-motion: reduce) {
|
|
213
|
+
.ModalContent {
|
|
214
|
+
transition: none;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
218
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
children: Snippet<[close: () => void]>;
|
|
4
|
+
buttons?: Snippet<[close: () => void]>;
|
|
5
|
+
close: () => void;
|
|
6
|
+
title?: string;
|
|
7
|
+
shape?: 'rounded' | 'sharp';
|
|
8
|
+
contained?: boolean;
|
|
9
|
+
unstyled?: boolean;
|
|
10
|
+
showScrim?: boolean;
|
|
11
|
+
closeOnScrim?: boolean;
|
|
12
|
+
trapFocus?: boolean;
|
|
13
|
+
dismissOnEsc?: boolean;
|
|
14
|
+
maxWidth?: string;
|
|
15
|
+
maxHeight?: string;
|
|
16
|
+
};
|
|
17
|
+
declare const ModalContent: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
18
|
+
type ModalContent = ReturnType<typeof ModalContent>;
|
|
19
|
+
export default ModalContent;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { OverlayPosition } from './overlays.svelte.js';
|
|
3
|
+
export type ModalButton = {
|
|
4
|
+
text: string;
|
|
5
|
+
variant?: 'action' | 'success' | 'danger' | 'ghost' | 'outline' | 'default';
|
|
6
|
+
onclick?: (close: () => void) => void | Promise<void>;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type ModalOptions = {
|
|
11
|
+
content: string | Snippet<[close: () => void]>;
|
|
12
|
+
title?: string;
|
|
13
|
+
buttons?: ModalButton[] | 'ok-cancel' | 'none';
|
|
14
|
+
position?: OverlayPosition;
|
|
15
|
+
width?: string;
|
|
16
|
+
maxWidth?: string;
|
|
17
|
+
maxHeight?: string;
|
|
18
|
+
shape?: 'rounded' | 'sharp';
|
|
19
|
+
contained?: boolean;
|
|
20
|
+
unstyled?: boolean;
|
|
21
|
+
showScrim?: boolean;
|
|
22
|
+
closeOnScrim?: boolean;
|
|
23
|
+
trapFocus?: boolean;
|
|
24
|
+
dismissOnEsc?: boolean;
|
|
25
|
+
onClose?: () => void;
|
|
26
|
+
onOpen?: () => void;
|
|
27
|
+
};
|
|
28
|
+
export declare const cancelButton: ModalButton;
|
|
29
|
+
export declare const okButton: ModalButton;
|
|
@@ -13,6 +13,7 @@ export { default as MenuDropdown } from './MenuDropdown.svelte';
|
|
|
13
13
|
export { default as MenuItem } from './MenuItem.svelte';
|
|
14
14
|
export { default as MenuItemContent } from './MenuItemContent.svelte';
|
|
15
15
|
export { default as Modal } from './Modal.svelte';
|
|
16
|
+
export { default as ModalContent } from './ModalContent.svelte';
|
|
16
17
|
export { default as Notification } from './Notification.svelte';
|
|
17
18
|
export { default as Overlay } from './Overlay.svelte';
|
|
18
19
|
export { default as OverlayContainer } from './OverlayContainer.svelte';
|
|
@@ -24,5 +25,7 @@ export { default as Theme } from './Theme.svelte';
|
|
|
24
25
|
export { default as Tooltip } from './Tooltip.svelte';
|
|
25
26
|
export { default as UIContent } from './UIContent.svelte';
|
|
26
27
|
export * from './MenuTypes.js';
|
|
28
|
+
export * from './ModalTypes.js';
|
|
27
29
|
export * from './notifications.svelte.js';
|
|
30
|
+
export * from './modals.svelte.js';
|
|
28
31
|
export * from './overlays.svelte.js';
|
package/dist/components/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { default as MenuDropdown } from './MenuDropdown.svelte';
|
|
|
13
13
|
export { default as MenuItem } from './MenuItem.svelte';
|
|
14
14
|
export { default as MenuItemContent } from './MenuItemContent.svelte';
|
|
15
15
|
export { default as Modal } from './Modal.svelte';
|
|
16
|
+
export { default as ModalContent } from './ModalContent.svelte';
|
|
16
17
|
export { default as Notification } from './Notification.svelte';
|
|
17
18
|
export { default as Overlay } from './Overlay.svelte';
|
|
18
19
|
export { default as OverlayContainer } from './OverlayContainer.svelte';
|
|
@@ -25,5 +26,7 @@ export { default as Tooltip } from './Tooltip.svelte';
|
|
|
25
26
|
export { default as UIContent } from './UIContent.svelte';
|
|
26
27
|
// Export TypeScript files and stores
|
|
27
28
|
export * from './MenuTypes.js';
|
|
29
|
+
export * from './ModalTypes.js';
|
|
28
30
|
export * from './notifications.svelte.js';
|
|
31
|
+
export * from './modals.svelte.js';
|
|
29
32
|
export * from './overlays.svelte.js';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { addOverlay, removeOverlay } from './overlays.svelte.js';
|
|
2
|
+
import ModalContent from './ModalContent.svelte';
|
|
3
|
+
import { cancelButton, okButton } from './ModalTypes.js';
|
|
4
|
+
let modalCount = $state(0);
|
|
5
|
+
let activeElement = null;
|
|
6
|
+
function lockScroll() {
|
|
7
|
+
if (modalCount === 0) {
|
|
8
|
+
activeElement = document.activeElement;
|
|
9
|
+
document.documentElement.style.overflow = 'hidden';
|
|
10
|
+
}
|
|
11
|
+
modalCount++;
|
|
12
|
+
}
|
|
13
|
+
function unlockScroll() {
|
|
14
|
+
modalCount--;
|
|
15
|
+
if (modalCount === 0) {
|
|
16
|
+
document.documentElement.style.overflow = '';
|
|
17
|
+
activeElement?.focus();
|
|
18
|
+
activeElement = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function openModal(opts) {
|
|
22
|
+
const id = crypto.randomUUID();
|
|
23
|
+
if (typeof opts === 'string') {
|
|
24
|
+
opts = { content: opts };
|
|
25
|
+
}
|
|
26
|
+
const close = () => {
|
|
27
|
+
removeOverlay(id);
|
|
28
|
+
unlockScroll();
|
|
29
|
+
opts.onClose?.();
|
|
30
|
+
};
|
|
31
|
+
lockScroll();
|
|
32
|
+
opts.onOpen?.();
|
|
33
|
+
// Handle defaultButtons shortcut
|
|
34
|
+
let buttons;
|
|
35
|
+
if (opts.buttons === 'ok-cancel') {
|
|
36
|
+
buttons = [cancelButton, okButton];
|
|
37
|
+
}
|
|
38
|
+
else if (opts.buttons === 'none' || opts.buttons === undefined) {
|
|
39
|
+
buttons = undefined;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
buttons = opts.buttons;
|
|
43
|
+
}
|
|
44
|
+
const buttonsSnippet = buttons ? createButtonsSnippet(buttons, close) : undefined;
|
|
45
|
+
addOverlay({
|
|
46
|
+
id,
|
|
47
|
+
z: 200,
|
|
48
|
+
component: ModalContent,
|
|
49
|
+
props: {
|
|
50
|
+
close,
|
|
51
|
+
children: wrapContent(opts.content, close),
|
|
52
|
+
buttons: buttonsSnippet,
|
|
53
|
+
title: opts.title,
|
|
54
|
+
showScrim: opts.showScrim ?? true,
|
|
55
|
+
closeOnScrim: opts.closeOnScrim ?? true,
|
|
56
|
+
trapFocus: opts.trapFocus ?? true,
|
|
57
|
+
dismissOnEsc: opts.dismissOnEsc ?? true,
|
|
58
|
+
unstyled: opts.unstyled ?? false,
|
|
59
|
+
shape: opts.shape ?? 'rounded',
|
|
60
|
+
contained: opts.contained ?? true,
|
|
61
|
+
maxWidth: opts.maxWidth,
|
|
62
|
+
maxHeight: opts.maxHeight,
|
|
63
|
+
},
|
|
64
|
+
position: opts.position || 'center',
|
|
65
|
+
layer: 'modals',
|
|
66
|
+
});
|
|
67
|
+
return { id, close };
|
|
68
|
+
}
|
|
69
|
+
function wrapContent(content, close) {
|
|
70
|
+
if (typeof content === 'string') {
|
|
71
|
+
// Return a snippet that renders the string
|
|
72
|
+
return () => content;
|
|
73
|
+
}
|
|
74
|
+
return content;
|
|
75
|
+
}
|
|
76
|
+
function createButtonsSnippet(buttons, close) {
|
|
77
|
+
// Create a snippet that renders the buttons
|
|
78
|
+
return (closeParam) => {
|
|
79
|
+
// We need to return actual DOM elements, not strings
|
|
80
|
+
// This will be rendered by Svelte's snippet system
|
|
81
|
+
const fragment = document.createDocumentFragment();
|
|
82
|
+
buttons.forEach((btn) => {
|
|
83
|
+
const button = document.createElement('button');
|
|
84
|
+
button.className = `button ${btn.variant || 'default'}`;
|
|
85
|
+
button.textContent = btn.text;
|
|
86
|
+
button.disabled = btn.disabled || btn.loading || false;
|
|
87
|
+
if (btn.onclick) {
|
|
88
|
+
button.addEventListener('click', async () => {
|
|
89
|
+
const result = btn.onclick?.(close);
|
|
90
|
+
if (result instanceof Promise) {
|
|
91
|
+
button.disabled = true;
|
|
92
|
+
button.classList.add('loading');
|
|
93
|
+
try {
|
|
94
|
+
await result;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
button.disabled = false;
|
|
98
|
+
button.classList.remove('loading');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
fragment.appendChild(button);
|
|
104
|
+
});
|
|
105
|
+
return fragment;
|
|
106
|
+
};
|
|
107
|
+
}
|
package/dist/css/1-props.css
CHANGED
|
@@ -460,4 +460,33 @@
|
|
|
460
460
|
@property --table-row-background-even { syntax: "<color>"; inherits: true; initial-value: #f8fafc; }
|
|
461
461
|
@property --table-row-background-hover { syntax: "<color>"; inherits: true; initial-value: #f1f5f9; }
|
|
462
462
|
|
|
463
|
-
@property --table-cell-color { syntax: "*"; inherits: true; initial-value: #1a1a1a; }
|
|
463
|
+
@property --table-cell-color { syntax: "*"; inherits: true; initial-value: #1a1a1a; }
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Modal
|
|
467
|
+
*/
|
|
468
|
+
|
|
469
|
+
@property --modal-background { syntax: "<color>"; inherits: true; initial-value: #ffffff; }
|
|
470
|
+
@property --modal-border-color { syntax: "*"; inherits: true; initial-value: #d1d5db; }
|
|
471
|
+
@property --modal-border-size { syntax: "<length>"; inherits: true; initial-value: 1px; }
|
|
472
|
+
@property --modal-border-style { syntax: "solid | dashed | dotted | double | groove | ridge | inset | outset"; inherits: true; initial-value: solid; }
|
|
473
|
+
@property --modal-border-radius { syntax: "<length>"; inherits: true; initial-value: 8px; }
|
|
474
|
+
@property --modal-shadow-color { syntax: "*"; inherits: true; initial-value: rgba(0, 0, 0, 0.1); }
|
|
475
|
+
|
|
476
|
+
@property --modal-padding-inline { syntax: "<length>"; inherits: true; initial-value: 24px; }
|
|
477
|
+
@property --modal-padding-block { syntax: "<length>"; inherits: true; initial-value: 24px; }
|
|
478
|
+
@property --modal-max-width { syntax: "<length>"; inherits: true; initial-value: 40rem; }
|
|
479
|
+
@property --modal-max-height { syntax: "<length>"; inherits: true; initial-value: 80svh; }
|
|
480
|
+
@property --modal-gap { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
481
|
+
|
|
482
|
+
@property --modal-actions-background { syntax: "<color>"; inherits: true; initial-value: #f8fafc; }
|
|
483
|
+
@property --modal-actions-border-color { syntax: "*"; inherits: true; initial-value: #d1d5db; }
|
|
484
|
+
@property --modal-actions-padding-inline { syntax: "<length>"; inherits: true; initial-value: 16px; }
|
|
485
|
+
@property --modal-actions-padding-block { syntax: "<length>"; inherits: true; initial-value: 16px; }
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Scrim/Backdrop (shared across overlays)
|
|
489
|
+
*/
|
|
490
|
+
|
|
491
|
+
@property --scrim-background { syntax: "<color>"; inherits: true; initial-value: rgba(0, 0, 0, 0.5); }
|
|
492
|
+
@property --scrim-backdrop-filter { syntax: "<string>"; inherits: true; initial-value: blur(2px); }
|
package/dist/css/2-base.css
CHANGED
|
@@ -119,6 +119,11 @@
|
|
|
119
119
|
--table-border: var(--table-border-size) var(--table-border-style) var(--table-border-color);
|
|
120
120
|
--table-cell-padding: var(--table-cell-padding-block) var(--table-cell-padding-inline);
|
|
121
121
|
|
|
122
|
+
/* Modal compound variables */
|
|
123
|
+
--modal-border: var(--modal-border-size) var(--modal-border-style) var(--modal-border-color);
|
|
124
|
+
--modal-padding: var(--modal-padding-block) var(--modal-padding-inline);
|
|
125
|
+
--modal-actions-padding: var(--modal-actions-padding-block) var(--modal-actions-padding-inline);
|
|
126
|
+
|
|
122
127
|
--mix-target: light-dark(black, white);
|
|
123
128
|
}
|
|
124
129
|
|
|
@@ -233,6 +233,26 @@
|
|
|
233
233
|
--table-row-background-even: transparent;
|
|
234
234
|
--table-row-background-hover: color-mix(in srgb, var(--theme-surface-interactive) 60%, transparent);
|
|
235
235
|
--table-cell-color: var(--text-color-p);
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Modals
|
|
239
|
+
*/
|
|
240
|
+
|
|
241
|
+
--modal-background: var(--background-body);
|
|
242
|
+
--modal-border-color: var(--border-color);
|
|
243
|
+
--modal-shadow-color: var(--shadow-color);
|
|
244
|
+
--modal-actions-background: var(--theme-surface-subtlest);
|
|
245
|
+
--modal-actions-border-color: var(--border-color);
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Scrim/Backdrop
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
--scrim-background: light-dark(
|
|
252
|
+
oklch(from var(--lutra-primary-color) calc(l * 0.2) calc(c * 0.01) h / 0.5),
|
|
253
|
+
oklch(from var(--lutra-primary-color) calc(l * 0.1) calc(c * 0.01) h / 0.7)
|
|
254
|
+
);
|
|
255
|
+
--scrim-backdrop-filter: blur(2px);
|
|
236
256
|
|
|
237
257
|
}
|
|
238
258
|
}
|