@widgetic/design-system 0.5.23 → 0.5.25
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/CopyableHeader.svelte +5 -3
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/modals/BaseModal.svelte +257 -166
- package/dist/components/modals/BaseModal.svelte.d.ts +3 -0
- package/dist/components/modals/NewItemDialog.svelte +5 -7
- package/dist/components/modals/Portal.svelte +9 -7
- package/dist/components/modals/RenameDialog.svelte +5 -7
- package/dist/components/modals/modalStore.d.ts +9 -1
- package/dist/components/modals/modalStore.js +44 -11
- package/dist/components/navigation/Breadcrumbs.svelte +10 -4
- package/dist/components/navigation/breadcrumbsAvailableWidth.d.ts +10 -0
- package/dist/components/navigation/breadcrumbsAvailableWidth.js +10 -0
- package/dist/components/search-input/SearchInput.svelte +25 -13
- package/dist/components/search-input/SearchInput.svelte.d.ts +4 -0
- package/dist/components/search-input/index2.d.ts +14 -9
- package/dist/components/shell/NavDrawer.svelte +1 -1
- package/dist/components/shell/NavRow.svelte +4 -1
- package/dist/components/shell/SettingsDialog.svelte +48 -29
- package/dist/components/shell/SettingsDialog.svelte.d.ts +0 -4
- package/dist/components/shell/WidgetRow.svelte.d.ts +1 -1
- package/dist/components/shell/index.d.ts +0 -1
- package/dist/components/shell/index.js +0 -1
- package/dist/components/shell/nested-modal-guard.d.ts +25 -0
- package/dist/components/shell/nested-modal-guard.js +33 -0
- package/dist/components/ui/alert/index.d.ts +2 -2
- package/dist/components/ui/alert/index.js +4 -1
- package/dist/components/ui/avatar/avatar.svelte +22 -3
- package/dist/components/ui/button/button.svelte +1 -1
- package/dist/components/ui/button/button.svelte.d.ts +2 -2
- package/dist/components/ui/calendar/calendar.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-item.svelte +1 -1
- package/dist/components/ui/command/command-link-item.svelte +1 -1
- package/dist/components/ui/dialog/dialog-content.svelte +8 -1
- package/dist/components/ui/dialog/dialog-overlay.svelte +8 -1
- package/dist/components/ui/drawer/drawer-overlay.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
- package/dist/components/ui/icon-button/icon-button.svelte +1 -1
- package/dist/components/ui/icon-button/icon-button.svelte.d.ts +2 -2
- package/dist/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
- package/dist/components/ui/select/select-content.svelte +5 -0
- package/dist/components/ui/select/select-item.svelte +8 -1
- package/dist/components/ui/select/select-trigger.svelte +3 -2
- package/dist/components/ui/stepper-button/stepper-button.svelte +1 -1
- package/dist/components/ui/switch/switch.svelte +4 -1
- package/dist/styles/global.css +3 -2
- package/dist/styles/tokens.css +21 -1
- package/dist/utils/inputModality.d.ts +16 -0
- package/dist/utils/inputModality.js +55 -0
- package/dist/utils/overlayMotion.d.ts +49 -0
- package/dist/utils/overlayMotion.js +71 -0
- package/package.json +1 -1
- package/dist/components/shell/messages.d.ts +0 -7
- package/dist/components/shell/messages.js +0 -5
|
@@ -130,9 +130,10 @@
|
|
|
130
130
|
}
|
|
131
131
|
</script>
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
<!-- `min-w-0` on the root and on the title row: a `truncate` title has its whole text as min-content, so inside a grid or flex parent (a dialog) the row would push the copy button out of the parent instead of cutting the title. -->
|
|
134
|
+
<div class={cn('copyable-header flex w-full min-w-0 flex-col gap-3', className)} {style}>
|
|
134
135
|
<!-- Title row: section label + copy control (Figma 32px). -->
|
|
135
|
-
<div class={cn('flex h-8 items-center justify-between gap-3', headerBackgroundColor)}>
|
|
136
|
+
<div class={cn('flex h-8 min-w-0 items-center justify-between gap-3', headerBackgroundColor)}>
|
|
136
137
|
<div class={cn('text-heading-section text-foreground min-w-0 truncate', headerTextClass)}>
|
|
137
138
|
{headerText}
|
|
138
139
|
</div>
|
|
@@ -262,7 +263,8 @@
|
|
|
262
263
|
</div>
|
|
263
264
|
|
|
264
265
|
{#if showHelpRow}
|
|
265
|
-
|
|
266
|
+
<!-- Figma: the help row is 20 high (the 14/20 link sets it), whatever the size of the link. -->
|
|
267
|
+
<div class="flex min-h-5 flex-wrap items-center gap-1">
|
|
266
268
|
{#if helpPrompt}
|
|
267
269
|
<span class="text-caption text-muted-foreground">{helpPrompt}</span>
|
|
268
270
|
{/if}
|
|
@@ -16,7 +16,7 @@ export { Breadcrumbs, type BreadcrumbsProps } from './navigation';
|
|
|
16
16
|
export { default as PDFViewer } from './pdf/PDFViewer.svelte';
|
|
17
17
|
export { SearchInput } from './search-input';
|
|
18
18
|
export { ShadEditor, checkShadEditorMessages, type ShadEditorMessageKey, type TipTapEditor, type TipTapEditorLevel } from './shad-editor';
|
|
19
|
-
export { NavRow, navRowVariants, NavSectionLabel, ProfileRow, profileRowVariants, Sidebar, WidgetRow, widgetRowVariants, PageHeader, TopBar, NavDrawer, AppShell, ErrorState, defaultErrorStateMessages, type ErrorStateMessages, NotificationRow, type NotificationRowProps, SettingsNav, type SettingsNavProps, SettingsDialog, type SettingsDialogProps, Stat, type StatProps, statVariants, Banner, type BannerProps, type BannerTone, bannerVariants, Link, type LinkProps, type LinkSize, linkVariants, type SettingsNavGroup, type SettingsNavItem, settingsNavItemIsActive, externalLinkAttributes, statHintClass, type StatTone
|
|
19
|
+
export { NavRow, navRowVariants, NavSectionLabel, ProfileRow, profileRowVariants, Sidebar, WidgetRow, widgetRowVariants, PageHeader, TopBar, NavDrawer, AppShell, ErrorState, defaultErrorStateMessages, type ErrorStateMessages, NotificationRow, type NotificationRowProps, SettingsNav, type SettingsNavProps, SettingsDialog, type SettingsDialogProps, Stat, type StatProps, statVariants, Banner, type BannerProps, type BannerTone, bannerVariants, Link, type LinkProps, type LinkSize, linkVariants, type SettingsNavGroup, type SettingsNavItem, settingsNavItemIsActive, externalLinkAttributes, statHintClass, type StatTone } from './shell';
|
|
20
20
|
export { ToasterModal, showToast } from './toast';
|
|
21
21
|
export * as Accordion from './ui/accordion';
|
|
22
22
|
export * as Alert from './ui/alert';
|
package/dist/components/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { Breadcrumbs } from './navigation';
|
|
|
17
17
|
export { default as PDFViewer } from './pdf/PDFViewer.svelte';
|
|
18
18
|
export { SearchInput } from './search-input';
|
|
19
19
|
export { ShadEditor, checkShadEditorMessages } from './shad-editor';
|
|
20
|
-
export { NavRow, navRowVariants, NavSectionLabel, ProfileRow, profileRowVariants, Sidebar, WidgetRow, widgetRowVariants, PageHeader, TopBar, NavDrawer, AppShell, ErrorState, defaultErrorStateMessages, NotificationRow, SettingsNav, SettingsDialog, Stat, statVariants, Banner, bannerVariants, Link, linkVariants, settingsNavItemIsActive, externalLinkAttributes, statHintClass
|
|
20
|
+
export { NavRow, navRowVariants, NavSectionLabel, ProfileRow, profileRowVariants, Sidebar, WidgetRow, widgetRowVariants, PageHeader, TopBar, NavDrawer, AppShell, ErrorState, defaultErrorStateMessages, NotificationRow, SettingsNav, SettingsDialog, Stat, statVariants, Banner, bannerVariants, Link, linkVariants, settingsNavItemIsActive, externalLinkAttributes, statHintClass } from './shell';
|
|
21
21
|
export { ToasterModal, showToast } from './toast';
|
|
22
22
|
export * as Accordion from './ui/accordion';
|
|
23
23
|
export * as Alert from './ui/alert';
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// Svelte imports
|
|
3
3
|
import { fade, scale } from 'svelte/transition';
|
|
4
|
-
import {
|
|
5
|
-
import { createEventDispatcher, onMount } from 'svelte';
|
|
4
|
+
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
|
6
5
|
const dispatch = createEventDispatcher();
|
|
7
6
|
|
|
8
7
|
// Design system componentsimports
|
|
@@ -10,7 +9,20 @@
|
|
|
10
9
|
import { IconX } from '@tabler/icons-svelte';
|
|
11
10
|
|
|
12
11
|
// Modal store
|
|
13
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
modalState,
|
|
14
|
+
openModal,
|
|
15
|
+
closeModal,
|
|
16
|
+
registerModalInstance,
|
|
17
|
+
unregisterModalInstance,
|
|
18
|
+
isTopModalInstance
|
|
19
|
+
} from './modalStore';
|
|
20
|
+
import {
|
|
21
|
+
overlayBackdropBlurPx,
|
|
22
|
+
overlayContentScaleStart,
|
|
23
|
+
overlayTransitionDurationMs,
|
|
24
|
+
overlayTransitionEasing
|
|
25
|
+
} from '../../utils/overlayMotion.js';
|
|
14
26
|
|
|
15
27
|
/* COMPONENT PROPS */
|
|
16
28
|
// class and style props
|
|
@@ -42,15 +54,21 @@
|
|
|
42
54
|
// A bare modal has no title row, no close button and no padding: its content draws all of it (the File Browser's
|
|
43
55
|
// search dialog is one: the search field is the top row). Close it with Escape or a click on the backdrop.
|
|
44
56
|
export let bare: boolean = false;
|
|
57
|
+
/** When false, clicks and taps on the scrim do not close (destructive confirms can set this). Default matches Figma: scrim dismisses. */
|
|
58
|
+
export let closeOnBackdropClick = true;
|
|
59
|
+
/** When false, Escape does not close. Default: Escape dismisses unless a nested control already handled the key. */
|
|
60
|
+
export let closeOnEscape = true;
|
|
45
61
|
|
|
46
62
|
// children prop(svelte 5) to replace the traditional slot(svelte 4)
|
|
47
63
|
export let children: any;
|
|
48
64
|
|
|
49
65
|
/* CONSTANTS */
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
66
|
+
const transitionDuration = overlayTransitionDurationMs;
|
|
67
|
+
const backdropBlur = `${overlayBackdropBlurPx}px`;
|
|
68
|
+
const easing = overlayTransitionEasing;
|
|
69
|
+
const transitionOpts = { duration: transitionDuration, easing };
|
|
70
|
+
|
|
71
|
+
const focusRingGutterPx = 4;
|
|
54
72
|
|
|
55
73
|
// Update constants for modal constraints
|
|
56
74
|
const PADDING = 24; // Consistent padding for sides and bottom
|
|
@@ -66,7 +84,7 @@
|
|
|
66
84
|
// All Content Container vars
|
|
67
85
|
const contentPadding = 16;
|
|
68
86
|
let allContentHeight = 0;
|
|
69
|
-
// The card draws a 1px border and 24px of padding (a bare modal has
|
|
87
|
+
// The card draws a 1px border and 24px of padding (a bare modal has none) around the title row and the content.
|
|
70
88
|
const CARD_BORDER = 1;
|
|
71
89
|
const CARD_PADDING = 24;
|
|
72
90
|
$: cardChromeHeight = 2 * CARD_BORDER + (bare ? 0 : 2 * CARD_PADDING);
|
|
@@ -83,6 +101,17 @@
|
|
|
83
101
|
$: contentAvailableHeight = Math.max(0, modalHeight - cardChromeHeight - titleRowHeight);
|
|
84
102
|
let updateContentDimInterval: any;
|
|
85
103
|
|
|
104
|
+
/** Stays true until exit transitions finish or the instance is destroyed. */
|
|
105
|
+
let keepPortalMounted = false;
|
|
106
|
+
/** True while exit `out:` runs after `open` became false (parent `{#if}` or bind). */
|
|
107
|
+
let exiting = false;
|
|
108
|
+
let prevOpenForExit = open;
|
|
109
|
+
/** `openModal` pushed and this instance registered — popped only after outro or destroy. */
|
|
110
|
+
let storePushed = false;
|
|
111
|
+
let instanceToken: symbol | undefined;
|
|
112
|
+
|
|
113
|
+
let reduceMotion = false;
|
|
114
|
+
|
|
86
115
|
// Show logs
|
|
87
116
|
let showLogs = false;
|
|
88
117
|
|
|
@@ -96,73 +125,67 @@
|
|
|
96
125
|
}
|
|
97
126
|
}
|
|
98
127
|
|
|
128
|
+
function pushModalStore() {
|
|
129
|
+
if (!storePushed) {
|
|
130
|
+
openModal(modalType, modalData);
|
|
131
|
+
instanceToken = registerModalInstance();
|
|
132
|
+
storePushed = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function popModalStore() {
|
|
137
|
+
if (storePushed) {
|
|
138
|
+
closeModal();
|
|
139
|
+
if (instanceToken !== undefined) {
|
|
140
|
+
unregisterModalInstance(instanceToken);
|
|
141
|
+
instanceToken = undefined;
|
|
142
|
+
}
|
|
143
|
+
storePushed = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isTopModal(): boolean {
|
|
148
|
+
return instanceToken !== undefined && isTopModalInstance(instanceToken);
|
|
149
|
+
}
|
|
150
|
+
|
|
99
151
|
/**
|
|
100
152
|
* LIFECYCLE HOOKS
|
|
101
153
|
* */
|
|
102
154
|
// Check stored state on mount and reopen if needed
|
|
103
155
|
onMount(() => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
try {
|
|
157
|
+
reduceMotion =
|
|
158
|
+
typeof window !== 'undefined' &&
|
|
159
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
|
|
160
|
+
} catch {
|
|
161
|
+
reduceMotion = false;
|
|
162
|
+
}
|
|
110
163
|
|
|
164
|
+
if ($modalState.isOpen && !open) {
|
|
111
165
|
// Only reopen if this is the correct modal type
|
|
112
166
|
if ($modalState.modalType === modalType) {
|
|
113
|
-
// if(showLogs) console.log('Restoring modal state for:', modalType);
|
|
114
167
|
open = true;
|
|
115
168
|
modalData = $modalState.data;
|
|
116
169
|
dispatch('restore', $modalState.data);
|
|
117
170
|
}
|
|
118
171
|
}
|
|
119
172
|
|
|
120
|
-
// Initialize update content dimensions
|
|
121
|
-
// updateViewportDimensions();
|
|
122
|
-
|
|
123
|
-
// Add keydown listener
|
|
124
173
|
window.addEventListener('keydown', handleKeydown);
|
|
125
174
|
|
|
126
|
-
// Add resize listener
|
|
127
|
-
// window.addEventListener('resize', updateViewportDimensions);
|
|
128
|
-
|
|
129
175
|
return () => {
|
|
130
|
-
// window.removeEventListener('resize', updateViewportDimensions);
|
|
131
176
|
window.removeEventListener('keydown', handleKeydown);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (open) {
|
|
135
|
-
closeModal();
|
|
136
|
-
toggleUpdateContentDimensions(false);
|
|
137
|
-
}
|
|
177
|
+
popModalStore();
|
|
178
|
+
toggleUpdateContentDimensions(false);
|
|
138
179
|
};
|
|
139
180
|
});
|
|
140
181
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// read window width and height
|
|
145
|
-
// viewportWidth = window.innerWidth;
|
|
146
|
-
// viewportHeight = window.innerHeight;
|
|
147
|
-
|
|
148
|
-
// update the conetent holder height
|
|
149
|
-
// setTimeout(() => {
|
|
150
|
-
allContentHeight = 2 * contentPadding + titleHeight + contentHolderHeight;
|
|
151
|
-
// if(showLogs) console.log('Modal current height:', allContentHeight);
|
|
152
|
-
// let availableHeight = modalHeight - 2 * contentPadding;
|
|
153
|
-
// if(showLogs) console.log('Modal current height:', allContentHeight, availableHeight);
|
|
154
|
-
// contentHolderHeight = allContentHeight > availableHeight ? (availableHeight - titleHeight) + 'px' : 'auto';
|
|
155
|
-
// modalFinalHeight = titleHeight + contentHeight + 2 * PADDING;
|
|
156
|
-
// if(showLogs) console.log('Modal current height:', modalHeight, modalFinalHeight);
|
|
157
|
-
// modalFinalHeight = Math.min(modalHeight, contentCurrentHeight);
|
|
158
|
-
// }, 10);
|
|
159
|
-
// if(showLogs) console.log('Updating viewport dimensions:', viewportWidth, viewportHeight);
|
|
160
|
-
}
|
|
182
|
+
onDestroy(() => {
|
|
183
|
+
popModalStore();
|
|
184
|
+
});
|
|
161
185
|
|
|
162
186
|
// Reactive Statement to Calculate dynamic modal position and dimensions
|
|
163
187
|
$: modalStyle = (() => {
|
|
164
188
|
if (viewportWidth === 0 || viewportHeight === 0) {
|
|
165
|
-
// Return default styles during SSR
|
|
166
189
|
return {
|
|
167
190
|
width: width,
|
|
168
191
|
height: height,
|
|
@@ -173,22 +196,17 @@
|
|
|
173
196
|
};
|
|
174
197
|
}
|
|
175
198
|
|
|
176
|
-
// Parse provided dimensions
|
|
177
199
|
const providedWidth = parseInt(width);
|
|
178
200
|
const providedHeight = parseInt(height);
|
|
179
201
|
|
|
180
|
-
// Calculate maximum dimensions
|
|
181
202
|
const maxWidth = viewportWidth - 2 * PADDING;
|
|
182
203
|
const maxHeight = viewportHeight - 2 * PADDING;
|
|
183
204
|
|
|
184
|
-
// Calculate actual dimensions
|
|
185
205
|
const actualWidth = Math.min(providedWidth, maxWidth);
|
|
186
206
|
const actualHeight = Math.min(providedHeight, maxHeight);
|
|
187
207
|
|
|
188
|
-
// If the actual height is less than maxHeight, center it vertically(but ensure it never goes above PADDING
|
|
189
208
|
const centeredTop = Math.max(PADDING, (viewportHeight - actualHeight) / 2);
|
|
190
209
|
|
|
191
|
-
// Return the calculated styles
|
|
192
210
|
return {
|
|
193
211
|
width: `${actualWidth}px`,
|
|
194
212
|
height: `${actualHeight}px`,
|
|
@@ -200,9 +218,9 @@
|
|
|
200
218
|
})();
|
|
201
219
|
|
|
202
220
|
function handleKeydown(event: KeyboardEvent) {
|
|
221
|
+
if (!isTopModal()) return;
|
|
203
222
|
if (!open) return;
|
|
204
223
|
|
|
205
|
-
// Only log keyboard events that we actually handle
|
|
206
224
|
if (['Backspace', 'Escape', 'Space'].includes(event.key)) {
|
|
207
225
|
// if(showLogs) console.log('Modal handling key:', event.key, 'Target:', (event.target as HTMLElement).tagName);
|
|
208
226
|
}
|
|
@@ -211,22 +229,26 @@
|
|
|
211
229
|
const isInput =
|
|
212
230
|
target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
213
231
|
|
|
214
|
-
|
|
232
|
+
if (event.key === 'Escape' && closeOnEscape) {
|
|
233
|
+
// Escape: close from inputs too unless a nested listbox/combobox already called preventDefault.
|
|
234
|
+
if (!event.defaultPrevented) {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
event.stopPropagation();
|
|
237
|
+
handleClose();
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
215
242
|
if (isInput) {
|
|
216
243
|
return;
|
|
217
244
|
}
|
|
218
245
|
|
|
219
|
-
// Prevent backspace navigation when not in input
|
|
220
246
|
if (event.key === 'Backspace') {
|
|
221
247
|
event.preventDefault();
|
|
222
248
|
event.stopPropagation();
|
|
223
249
|
return;
|
|
224
250
|
}
|
|
225
251
|
|
|
226
|
-
if (event.key === 'Escape') {
|
|
227
|
-
handleClose();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
252
|
if (enableSpaceKey && open && event.code === 'Space') {
|
|
231
253
|
event.preventDefault();
|
|
232
254
|
event.stopPropagation();
|
|
@@ -235,53 +257,57 @@
|
|
|
235
257
|
}
|
|
236
258
|
|
|
237
259
|
function handleClose() {
|
|
238
|
-
// if(showLogs) console.log('handleClose');
|
|
239
|
-
|
|
240
|
-
// change the open state to false
|
|
241
260
|
open = false;
|
|
242
|
-
|
|
243
|
-
// Close the modal
|
|
244
|
-
closeModal();
|
|
245
|
-
|
|
246
|
-
// Stop updating the content dimensions
|
|
247
261
|
toggleUpdateContentDimensions(false);
|
|
248
|
-
|
|
249
|
-
// Dispatch close event
|
|
250
262
|
dispatch('close');
|
|
251
263
|
}
|
|
252
264
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// Open the modal if the open state is true
|
|
261
|
-
if (open) {
|
|
262
|
-
openModal(modalType, modalData);
|
|
263
|
-
toggleUpdateContentDimensions(true);
|
|
264
|
-
} else {
|
|
265
|
-
closeModal();
|
|
266
|
-
toggleUpdateContentDimensions(false);
|
|
267
|
-
}
|
|
265
|
+
function handleBackdropPointerDown(event: PointerEvent) {
|
|
266
|
+
if (!open || !closeOnBackdropClick || !isTopModal()) return;
|
|
267
|
+
// Only a press that *starts* on the scrim closes; starting inside the card and releasing outside does not.
|
|
268
|
+
if (event.target !== event.currentTarget) return;
|
|
269
|
+
event.preventDefault();
|
|
270
|
+
event.stopPropagation();
|
|
271
|
+
handleClose();
|
|
268
272
|
}
|
|
269
273
|
|
|
270
|
-
// Improved backdrop click handler
|
|
271
274
|
function handleBackdropClick(event: MouseEvent) {
|
|
272
|
-
|
|
275
|
+
if (!open || !closeOnBackdropClick || !isTopModal()) return;
|
|
276
|
+
// Only a press that *starts* on the scrim closes; starting inside the card and releasing outside does not.
|
|
277
|
+
if (event.target !== event.currentTarget) return;
|
|
273
278
|
event.preventDefault();
|
|
274
279
|
event.stopPropagation();
|
|
280
|
+
}
|
|
275
281
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
282
|
+
function handlePanelOutroEnd() {
|
|
283
|
+
exiting = false;
|
|
284
|
+
popModalStore();
|
|
285
|
+
if (!open) {
|
|
286
|
+
keepPortalMounted = false;
|
|
280
287
|
}
|
|
281
288
|
}
|
|
289
|
+
|
|
290
|
+
// Only update the modal store when `open` is true (push once per open); closing is handled in outro / destroy.
|
|
291
|
+
$: if (open) {
|
|
292
|
+
pushModalStore();
|
|
293
|
+
keepPortalMounted = true;
|
|
294
|
+
exiting = false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
$: if (prevOpenForExit && !open) {
|
|
298
|
+
exiting = true;
|
|
299
|
+
keepPortalMounted = true;
|
|
300
|
+
}
|
|
301
|
+
$: prevOpenForExit = open;
|
|
302
|
+
|
|
303
|
+
$: if (open) {
|
|
304
|
+
toggleUpdateContentDimensions(true);
|
|
305
|
+
} else if (!exiting) {
|
|
306
|
+
toggleUpdateContentDimensions(false);
|
|
307
|
+
}
|
|
282
308
|
</script>
|
|
283
309
|
|
|
284
|
-
{#if
|
|
310
|
+
{#if keepPortalMounted}
|
|
285
311
|
<Portal
|
|
286
312
|
class="{className ? className + ' ' : ''}base-modal-portal"
|
|
287
313
|
target={target || `modal-${modalType || 'default'}`}
|
|
@@ -289,26 +315,31 @@
|
|
|
289
315
|
bind:clientWidth={viewportWidth}
|
|
290
316
|
bind:clientHeight={viewportHeight}
|
|
291
317
|
>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
318
|
+
{#if open || exiting}
|
|
319
|
+
<!-- Host-scoped scrim (absolute fills the portal host, not the viewport). -->
|
|
320
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
321
|
+
<div class="modal-dismiss-layer pointer-events-none absolute inset-0 z-[1000]">
|
|
322
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
323
|
+
<div
|
|
324
|
+
role="presentation"
|
|
325
|
+
data-widgetic-overlay="base-modal-backdrop"
|
|
326
|
+
class="modal-backdrop-container bg-muted-foreground/80 pointer-events-auto absolute inset-0"
|
|
327
|
+
style="backdrop-filter: blur({backdropBlur}); z-index: 0;"
|
|
328
|
+
in:fade|global={reduceMotion ? { duration: 0 } : transitionOpts}
|
|
329
|
+
out:fade|global={reduceMotion ? { duration: 0 } : transitionOpts}
|
|
330
|
+
on:pointerdown={handleBackdropPointerDown}
|
|
331
|
+
on:click={handleBackdropClick}
|
|
332
|
+
></div>
|
|
333
|
+
<div
|
|
334
|
+
class="modal-root-container pointer-events-none absolute inset-0 z-[1] flex items-center justify-center px-6"
|
|
335
|
+
>
|
|
336
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
337
|
+
<div
|
|
338
|
+
data-widgetic-overlay="base-modal-panel"
|
|
339
|
+
class="modal-content-inner-container bg-background border-border rounded-panel pointer-events-auto relative max-h-[calc(100vh-3rem)] select-text overflow-hidden border {bare
|
|
340
|
+
? 'p-0'
|
|
341
|
+
: 'p-6'}"
|
|
342
|
+
style="
|
|
312
343
|
width: {modalStyle.width};
|
|
313
344
|
height: {allContentHeight}px;
|
|
314
345
|
min-width: {bare ? 100 : 2 * contentPadding + 100}px;
|
|
@@ -321,58 +352,111 @@
|
|
|
321
352
|
-webkit-user-select: text;
|
|
322
353
|
-moz-user-select: text;
|
|
323
354
|
-ms-user-select: text;
|
|
324
|
-
z-index:
|
|
355
|
+
z-index: 2;
|
|
325
356
|
"
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<IconButton variant="ghost" label={closeLabel} onclick={handleClose}>
|
|
347
|
-
<IconX class="size-4 stroke-[1.333]" />
|
|
348
|
-
</IconButton>
|
|
349
|
-
</div>
|
|
350
|
-
{/if}
|
|
351
|
-
|
|
352
|
-
<!-- Content Slot Container -->
|
|
353
|
-
{#snippet contentSlot()}
|
|
354
|
-
<div class="modal-content-slot-container w-full" bind:clientHeight={contentHolderHeight}>
|
|
355
|
-
{@render children()}
|
|
356
|
-
</div>
|
|
357
|
-
{/snippet}
|
|
358
|
-
|
|
359
|
-
<!-- Content with Scroll Area -->
|
|
360
|
-
{#if showScroll}
|
|
361
|
-
<WScrollArea
|
|
362
|
-
class="modal-content-container scroll-container"
|
|
363
|
-
type="always"
|
|
364
|
-
scrollOutside={true}
|
|
365
|
-
style="height: {contentAvailableHeight}px"
|
|
357
|
+
bind:offsetHeight={modalHeight}
|
|
358
|
+
in:scale|global={reduceMotion
|
|
359
|
+
? { duration: 0 }
|
|
360
|
+
: {
|
|
361
|
+
duration: transitionDuration,
|
|
362
|
+
start: overlayContentScaleStart,
|
|
363
|
+
opacity: 0,
|
|
364
|
+
easing
|
|
365
|
+
}}
|
|
366
|
+
out:scale|global={reduceMotion
|
|
367
|
+
? { duration: 0 }
|
|
368
|
+
: {
|
|
369
|
+
duration: transitionDuration,
|
|
370
|
+
start: overlayContentScaleStart,
|
|
371
|
+
opacity: 0,
|
|
372
|
+
easing
|
|
373
|
+
}}
|
|
374
|
+
on:outroend={handlePanelOutroEnd}
|
|
375
|
+
on:pointerdown|stopPropagation
|
|
376
|
+
on:click|stopPropagation
|
|
366
377
|
>
|
|
367
|
-
{
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
378
|
+
{#if !bare}
|
|
379
|
+
<div
|
|
380
|
+
bind:clientHeight={titleHeight}
|
|
381
|
+
class="modal-title-container flex items-start justify-between gap-2 {titlePadding === 'small'
|
|
382
|
+
? 'mb-2'
|
|
383
|
+
: 'mb-4'}"
|
|
384
|
+
aria-selected={isSelected}
|
|
385
|
+
>
|
|
386
|
+
{#if title}
|
|
387
|
+
<h2 class="text-heading-page text-foreground min-w-0 flex-1 break-words text-start">
|
|
388
|
+
{title}
|
|
389
|
+
</h2>
|
|
390
|
+
{:else}
|
|
391
|
+
<div class="min-w-0 flex-1"></div>
|
|
392
|
+
{/if}
|
|
393
|
+
<IconButton variant="ghost" label={closeLabel} onclick={handleClose}>
|
|
394
|
+
<IconX class="size-4 stroke-[1.333]" />
|
|
395
|
+
</IconButton>
|
|
396
|
+
</div>
|
|
397
|
+
{/if}
|
|
398
|
+
|
|
399
|
+
{#snippet contentSlot()}
|
|
400
|
+
<div
|
|
401
|
+
class="modal-content-slot-container w-full"
|
|
402
|
+
bind:clientHeight={contentHolderHeight}
|
|
403
|
+
>
|
|
404
|
+
{@render children()}
|
|
405
|
+
</div>
|
|
406
|
+
{/snippet}
|
|
407
|
+
|
|
408
|
+
{#if bare}
|
|
409
|
+
{#if showScroll}
|
|
410
|
+
<WScrollArea
|
|
411
|
+
class="modal-content-container scroll-container"
|
|
412
|
+
type="always"
|
|
413
|
+
scrollOutside={true}
|
|
414
|
+
style="height: {contentAvailableHeight}px"
|
|
415
|
+
>
|
|
416
|
+
{@render contentSlot()}
|
|
417
|
+
</WScrollArea>
|
|
418
|
+
{:else}
|
|
419
|
+
<div class="modal-content-container" style="height: {contentAvailableHeight}px;">
|
|
420
|
+
{@render contentSlot()}
|
|
421
|
+
</div>
|
|
422
|
+
{/if}
|
|
423
|
+
{:else}
|
|
424
|
+
<!--
|
|
425
|
+
Focus ring gutter: the clip wrapper uses margin -4px / padding +4px so the overflow clip edge sits
|
|
426
|
+
outside the content box; the inner flow/scroll root keeps the same footprint as before (no inner
|
|
427
|
+
padding), so layout parity stays 25px from the card border. For showScroll, the scroll root is
|
|
428
|
+
extended by 8px and an inner wrapper provides the 4px pad for rings without changing measured height.
|
|
429
|
+
-->
|
|
430
|
+
<div
|
|
431
|
+
class="modal-focus-ring-clip min-h-0 overflow-hidden"
|
|
432
|
+
style="margin: -{focusRingGutterPx}px; padding: {focusRingGutterPx}px; border-radius: inherit;"
|
|
433
|
+
>
|
|
434
|
+
{#if showScroll}
|
|
435
|
+
<WScrollArea
|
|
436
|
+
class="modal-scroll-gutter-host scroll-container max-h-full min-h-0"
|
|
437
|
+
type="always"
|
|
438
|
+
scrollOutside={true}
|
|
439
|
+
style="margin: -{focusRingGutterPx}px; width: calc(100% + {2 *
|
|
440
|
+
focusRingGutterPx}px); height: {contentAvailableHeight + 2 * focusRingGutterPx}px;"
|
|
441
|
+
>
|
|
442
|
+
<div class="modal-scroll-focus-gutter box-border min-h-0 p-1">
|
|
443
|
+
{@render contentSlot()}
|
|
444
|
+
</div>
|
|
445
|
+
</WScrollArea>
|
|
446
|
+
{:else}
|
|
447
|
+
<div
|
|
448
|
+
class="modal-content-flow max-h-full min-h-0"
|
|
449
|
+
style="height: {contentAvailableHeight}px;"
|
|
450
|
+
>
|
|
451
|
+
{@render contentSlot()}
|
|
452
|
+
</div>
|
|
453
|
+
{/if}
|
|
454
|
+
</div>
|
|
455
|
+
{/if}
|
|
372
456
|
</div>
|
|
373
|
-
|
|
457
|
+
</div>
|
|
374
458
|
</div>
|
|
375
|
-
|
|
459
|
+
{/if}
|
|
376
460
|
</Portal>
|
|
377
461
|
{/if}
|
|
378
462
|
|
|
@@ -383,9 +467,18 @@
|
|
|
383
467
|
overflow: hidden;
|
|
384
468
|
}
|
|
385
469
|
|
|
386
|
-
/*
|
|
470
|
+
/* Non-scroll body: no overflow:hidden here — the outer card + clip wrapper handle clipping; avoids cutting focus rings. */
|
|
471
|
+
.modal-content-flow {
|
|
472
|
+
width: 100%;
|
|
473
|
+
overflow: visible;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.modal-dismiss-layer {
|
|
477
|
+
pointer-events: none;
|
|
478
|
+
}
|
|
479
|
+
|
|
387
480
|
.modal-backdrop-container {
|
|
388
|
-
pointer-events:
|
|
481
|
+
pointer-events: auto;
|
|
389
482
|
}
|
|
390
483
|
|
|
391
484
|
:global(.ScrollAreaViewport) {
|
|
@@ -398,7 +491,6 @@
|
|
|
398
491
|
display: none;
|
|
399
492
|
}
|
|
400
493
|
|
|
401
|
-
/* Ensure text selection works in modals */
|
|
402
494
|
:global(.base-modal-portal),
|
|
403
495
|
:global(.modal-content-inner-container),
|
|
404
496
|
:global(.modal-content-root-container) {
|
|
@@ -408,7 +500,6 @@
|
|
|
408
500
|
-ms-user-select: text !important;
|
|
409
501
|
}
|
|
410
502
|
|
|
411
|
-
/* Ensure text selection works in input fields */
|
|
412
503
|
:global(.base-modal-portal input),
|
|
413
504
|
:global(.base-modal-portal textarea),
|
|
414
505
|
:global(.base-modal-portal select),
|
|
@@ -29,8 +29,11 @@ declare const BaseModal: $$__sveltets_2_IsomorphicComponent<{
|
|
|
29
29
|
animateSize?: boolean;
|
|
30
30
|
closeLabel?: string;
|
|
31
31
|
bare?: boolean;
|
|
32
|
+
/** When false, clicks and taps on the scrim do not close (destructive confirms can set this). Default matches Figma: scrim dismisses. */ closeOnBackdropClick?: boolean;
|
|
33
|
+
/** When false, Escape does not close. Default: Escape dismisses unless a nested control already handled the key. */ closeOnEscape?: boolean;
|
|
32
34
|
children: any;
|
|
33
35
|
}, {
|
|
36
|
+
pointerdown: PointerEvent;
|
|
34
37
|
click: PointerEvent;
|
|
35
38
|
restore: CustomEvent<any>;
|
|
36
39
|
spaceKey: CustomEvent<any>;
|
|
@@ -172,14 +172,12 @@
|
|
|
172
172
|
aria-invalid={Boolean(currentError)}
|
|
173
173
|
class={currentError ? 'border-destructive' : ''}
|
|
174
174
|
onkeydown={(e) => {
|
|
175
|
-
if (e.key
|
|
176
|
-
handleClose();
|
|
177
|
-
} else {
|
|
175
|
+
if (e.key !== 'Escape') {
|
|
178
176
|
e.stopPropagation();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
}
|
|
178
|
+
if (e.key === 'Enter') {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
handleSubmit();
|
|
183
181
|
}
|
|
184
182
|
}}
|
|
185
183
|
oninput={(e) => {
|