@widgetic/design-system 0.5.23 → 0.5.24
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/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/SettingsDialog.svelte +48 -3
- package/dist/components/shell/WidgetRow.svelte.d.ts +1 -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/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/range-calendar/range-calendar.svelte.d.ts +1 -1
- package/dist/components/ui/select/select-item.svelte +1 -1
- package/dist/styles/global.css +3 -2
- package/dist/utils/overlayMotion.d.ts +49 -0
- package/dist/utils/overlayMotion.js +71 -0
- package/package.json +1 -1
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
interface ModalState {
|
|
2
2
|
isOpen: boolean;
|
|
3
|
+
/** Top-most open modal generation; used with `openModal` / `closeModal` for restore and global `modalState`. */
|
|
4
|
+
generation?: number;
|
|
3
5
|
modalType?: string;
|
|
4
6
|
data?: any;
|
|
5
7
|
}
|
|
6
8
|
export declare const modalState: import("svelte/store").Writable<ModalState>;
|
|
7
9
|
export declare const isModalOpen: import("svelte/store").Writable<boolean>;
|
|
8
|
-
export declare function openModal(modalType?: string, data?: any):
|
|
10
|
+
export declare function openModal(modalType?: string, data?: any): number;
|
|
9
11
|
export declare function closeModal(): void;
|
|
12
|
+
/** Call when a BaseModal instance opens — returns an opaque token used with `isTopModalInstance` / `unregisterModalInstance`. */
|
|
13
|
+
export declare function registerModalInstance(): symbol;
|
|
14
|
+
/** Remove this instance from the stack (e.g. closed or destroyed). Safe if already removed. */
|
|
15
|
+
export declare function unregisterModalInstance(token: symbol): void;
|
|
16
|
+
/** True when this instance should handle backdrop / Escape while several modals are stacked. */
|
|
17
|
+
export declare function isTopModalInstance(token: symbol): boolean;
|
|
10
18
|
export {};
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { writable } from 'svelte/store';
|
|
2
|
-
// import { browser } from '$app/environment';
|
|
3
|
-
// Determine if running in browser (avoids reference errors during SSR)
|
|
4
|
-
const browser = typeof window !== 'undefined';
|
|
5
2
|
// Debug flag – set to true when you need to trace modal behaviour
|
|
6
3
|
const showLogs = false;
|
|
7
4
|
/*
|
|
@@ -12,26 +9,62 @@ const showLogs = false;
|
|
|
12
9
|
* clean slate after any refresh.
|
|
13
10
|
*/
|
|
14
11
|
// Always start with a closed modal on fresh page loads
|
|
15
|
-
const getInitialState = () => ({ isOpen: false });
|
|
12
|
+
const getInitialState = () => ({ isOpen: false, generation: 0 });
|
|
16
13
|
// Central writable store for runtime modal state
|
|
17
14
|
export const modalState = writable(getInitialState());
|
|
18
15
|
// Backwards-compatibility helper – exposes only the boolean flag
|
|
19
16
|
export const isModalOpen = writable(getInitialState().isOpen);
|
|
20
17
|
// Keep `isModalOpen` in sync (without persisting to localStorage)
|
|
21
|
-
modalState.subscribe(state => {
|
|
18
|
+
modalState.subscribe((state) => {
|
|
22
19
|
// if (showLogs) console.log('Modal state updated:', state);
|
|
23
20
|
isModalOpen.set(state.isOpen);
|
|
24
21
|
});
|
|
22
|
+
let generationCounter = 0;
|
|
23
|
+
const openStack = [];
|
|
24
|
+
/** Which BaseModal instance is top-most for backdrop + Escape (independent of `generation` timing). */
|
|
25
|
+
const modalInstanceStack = [];
|
|
26
|
+
function syncStoreFromStack() {
|
|
27
|
+
const top = openStack[openStack.length - 1];
|
|
28
|
+
if (!top) {
|
|
29
|
+
modalState.set(getInitialState());
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
modalState.set({
|
|
33
|
+
isOpen: true,
|
|
34
|
+
generation: top.generation,
|
|
35
|
+
modalType: top.modalType,
|
|
36
|
+
data: top.data
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
25
40
|
// Helper functions to manage modal state
|
|
26
41
|
export function openModal(modalType, data) {
|
|
27
42
|
// if (showLogs) console.log('Opening modal:', { modalType, data });
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
generationCounter += 1;
|
|
44
|
+
const entry = { generation: generationCounter, modalType, data };
|
|
45
|
+
openStack.push(entry);
|
|
46
|
+
syncStoreFromStack();
|
|
47
|
+
return entry.generation;
|
|
33
48
|
}
|
|
34
49
|
export function closeModal() {
|
|
35
50
|
// if (showLogs) console.log('Closing modal');
|
|
36
|
-
|
|
51
|
+
openStack.pop();
|
|
52
|
+
syncStoreFromStack();
|
|
53
|
+
}
|
|
54
|
+
/** Call when a BaseModal instance opens — returns an opaque token used with `isTopModalInstance` / `unregisterModalInstance`. */
|
|
55
|
+
export function registerModalInstance() {
|
|
56
|
+
const token = Symbol('base-modal-instance');
|
|
57
|
+
modalInstanceStack.push(token);
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
/** Remove this instance from the stack (e.g. closed or destroyed). Safe if already removed. */
|
|
61
|
+
export function unregisterModalInstance(token) {
|
|
62
|
+
const idx = modalInstanceStack.lastIndexOf(token);
|
|
63
|
+
if (idx >= 0)
|
|
64
|
+
modalInstanceStack.splice(idx, 1);
|
|
65
|
+
}
|
|
66
|
+
/** True when this instance should handle backdrop / Escape while several modals are stacked. */
|
|
67
|
+
export function isTopModalInstance(token) {
|
|
68
|
+
const n = modalInstanceStack.length;
|
|
69
|
+
return n > 0 && modalInstanceStack[n - 1] === token;
|
|
37
70
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// Types Import
|
|
16
16
|
import type { BreadcrumbsProps } from './types.js';
|
|
17
17
|
import { dirForUserText, useTextDirection } from '../textDirection.js';
|
|
18
|
+
import { breadcrumbsAvailableWidthPx } from './breadcrumbsAvailableWidth.js';
|
|
18
19
|
|
|
19
20
|
// Environment Import
|
|
20
21
|
// import { browser } from '$app/environment';
|
|
@@ -106,8 +107,8 @@
|
|
|
106
107
|
function updateAvailableWidth() {
|
|
107
108
|
if (!browser || !navElement || !breadcrumbsContainer) return;
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
const containerElement =
|
|
110
|
+
const headerAncestor = navElement.closest('header');
|
|
111
|
+
const containerElement = headerAncestor || navElement;
|
|
111
112
|
headerElement = containerElement;
|
|
112
113
|
|
|
113
114
|
// Get all the measurements we need
|
|
@@ -127,7 +128,12 @@
|
|
|
127
128
|
const rightOffset = measurements.rightContent ? measurements.rightContent.width : 24;
|
|
128
129
|
|
|
129
130
|
// Calculate available width for breadcrumbs
|
|
130
|
-
availableWidth =
|
|
131
|
+
availableWidth = breadcrumbsAvailableWidthPx({
|
|
132
|
+
containerWidthPx: measurements.container.width,
|
|
133
|
+
leftOffsetPx: leftOffset,
|
|
134
|
+
rightOffsetPx: rightOffset,
|
|
135
|
+
headerAncestor
|
|
136
|
+
});
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
function handleMediaQuery(e: MediaQueryListEvent | MediaQueryList) {
|
|
@@ -162,7 +168,7 @@
|
|
|
162
168
|
handleMediaQuery(mediaQuery);
|
|
163
169
|
mediaQuery.addEventListener('change', handleMediaQuery);
|
|
164
170
|
|
|
165
|
-
//
|
|
171
|
+
// First try to get the header from FileBrowser structure
|
|
166
172
|
if (navElement) {
|
|
167
173
|
const header = navElement.closest('header');
|
|
168
174
|
if (header) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for Breadcrumbs width math (header vs non-header container).
|
|
3
|
+
*/
|
|
4
|
+
export declare function breadcrumbsLayoutPaddingPx(headerAncestor: HTMLElement | null): number;
|
|
5
|
+
export declare function breadcrumbsAvailableWidthPx(options: {
|
|
6
|
+
containerWidthPx: number;
|
|
7
|
+
leftOffsetPx: number;
|
|
8
|
+
rightOffsetPx: number;
|
|
9
|
+
headerAncestor: HTMLElement | null;
|
|
10
|
+
}): number;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for Breadcrumbs width math (header vs non-header container).
|
|
3
|
+
*/
|
|
4
|
+
export function breadcrumbsLayoutPaddingPx(headerAncestor) {
|
|
5
|
+
return headerAncestor ? 72 : 0;
|
|
6
|
+
}
|
|
7
|
+
export function breadcrumbsAvailableWidthPx(options) {
|
|
8
|
+
const layoutPaddingPx = breadcrumbsLayoutPaddingPx(options.headerAncestor);
|
|
9
|
+
return options.containerWidthPx - options.leftOffsetPx - options.rightOffsetPx - layoutPaddingPx;
|
|
10
|
+
}
|
|
@@ -1,52 +1,64 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
Figma: Search Field (component set 189:1138, node 189:1138).
|
|
3
|
+
The same field works as a plain search control and as the textbox of a combobox: pass `inputAttributes` (role, aria-*,
|
|
4
|
+
autocomplete) from the parent; `type`, `value`, and the input `class` always come from this component so the look stays one.
|
|
3
5
|
-->
|
|
4
6
|
<script lang="ts">
|
|
5
|
-
import IconSearch from
|
|
6
|
-
import IconX from
|
|
7
|
+
import IconSearch from '@tabler/icons-svelte/icons/search';
|
|
8
|
+
import IconX from '@tabler/icons-svelte/icons/x';
|
|
7
9
|
|
|
8
|
-
export let placeholder =
|
|
9
|
-
export let value =
|
|
10
|
+
export let placeholder = 'Search…';
|
|
11
|
+
export let value = '';
|
|
10
12
|
export let onInput: (event: Event) => Promise<void> | void = () => {};
|
|
11
13
|
export let onKeydown: (event: KeyboardEvent) => void = () => {};
|
|
12
14
|
export let onClear: () => void = () => {
|
|
13
|
-
value =
|
|
15
|
+
value = '';
|
|
14
16
|
};
|
|
15
17
|
export let onPaste: (event: ClipboardEvent) => void = () => {};
|
|
16
|
-
export let
|
|
17
|
-
export let
|
|
18
|
-
export let
|
|
18
|
+
export let onFocus: (event: FocusEvent) => void = () => {};
|
|
19
|
+
export let onBlur: (event: FocusEvent) => void = () => {};
|
|
20
|
+
export let containerClass = '';
|
|
21
|
+
export let inputClass = '';
|
|
22
|
+
export let clearLabel = 'Clear search';
|
|
23
|
+
/** Bindable ref to the native input (e.g. combobox controllers call focus()). */
|
|
24
|
+
export let inputElement: HTMLInputElement | null = null;
|
|
25
|
+
/** Spread first; this component’s `type`, `value`, and `class` are set after so they override clashing keys from the spread. */
|
|
26
|
+
export let inputAttributes: Record<string, unknown> = {};
|
|
19
27
|
</script>
|
|
20
28
|
|
|
21
29
|
<div class="w-full {containerClass}">
|
|
22
30
|
<div
|
|
23
|
-
class="group relative flex h-9 w-full items-center gap-2 rounded-lg
|
|
31
|
+
class="group bg-accent duration-fast focus-within:bg-accent hover:bg-accent-hover relative flex h-9 w-full items-center gap-2 rounded-lg px-3 transition-colors"
|
|
24
32
|
>
|
|
25
33
|
<IconSearch
|
|
26
|
-
class="size-4 shrink-0 stroke-[1.333]
|
|
34
|
+
class="text-muted-foreground duration-fast group-focus-within:text-foreground size-4 shrink-0 stroke-[1.333] transition-colors {value
|
|
27
35
|
? 'text-foreground'
|
|
28
36
|
: ''}"
|
|
29
37
|
aria-hidden="true"
|
|
30
38
|
/>
|
|
31
39
|
<input
|
|
40
|
+
{...inputAttributes}
|
|
32
41
|
type="search"
|
|
42
|
+
bind:this={inputElement}
|
|
33
43
|
{placeholder}
|
|
34
44
|
bind:value
|
|
35
45
|
oninput={onInput}
|
|
36
46
|
onkeydown={(e) => {
|
|
37
|
-
if (e.key ===
|
|
47
|
+
if (e.key === 'Backspace' && !value) {
|
|
38
48
|
e.preventDefault();
|
|
39
49
|
return;
|
|
40
50
|
}
|
|
41
51
|
onKeydown(e);
|
|
42
52
|
}}
|
|
43
53
|
onpaste={onPaste}
|
|
44
|
-
|
|
54
|
+
onfocus={onFocus}
|
|
55
|
+
onblur={onBlur}
|
|
56
|
+
class="text-body text-foreground placeholder:text-muted-foreground min-w-0 flex-1 border-0 bg-transparent focus:outline-hidden [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden {inputClass}"
|
|
45
57
|
/>
|
|
46
58
|
{#if value}
|
|
47
59
|
<button
|
|
48
60
|
type="button"
|
|
49
|
-
class="
|
|
61
|
+
class="text-muted-foreground duration-fast hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex size-4 shrink-0 items-center justify-center transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden"
|
|
50
62
|
aria-label={clearLabel}
|
|
51
63
|
onclick={onClear}
|
|
52
64
|
>
|
|
@@ -18,9 +18,13 @@ declare const SearchInput: $$__sveltets_2_IsomorphicComponent<{
|
|
|
18
18
|
onKeydown?: (event: KeyboardEvent) => void;
|
|
19
19
|
onClear?: () => void;
|
|
20
20
|
onPaste?: (event: ClipboardEvent) => void;
|
|
21
|
+
onFocus?: (event: FocusEvent) => void;
|
|
22
|
+
onBlur?: (event: FocusEvent) => void;
|
|
21
23
|
containerClass?: string;
|
|
22
24
|
inputClass?: string;
|
|
23
25
|
clearLabel?: string;
|
|
26
|
+
/** Bindable ref to the native input (e.g. combobox controllers call focus()). */ inputElement?: HTMLInputElement | null;
|
|
27
|
+
/** Spread first; this component’s `type`, `value`, and `class` are set after so they override clashing keys from the spread. */ inputAttributes?: Record<string, unknown>;
|
|
24
28
|
}, {
|
|
25
29
|
[evt: string]: CustomEvent<any>;
|
|
26
30
|
}, {}, {}, string>;
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { SvelteComponent } from 'svelte';
|
|
2
2
|
|
|
3
3
|
export interface SearchInputProps {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
value?: string;
|
|
6
|
+
onInput?: (event: Event) => Promise<void> | void;
|
|
7
|
+
onKeydown?: (event: KeyboardEvent) => void;
|
|
8
|
+
onClear?: () => void;
|
|
9
|
+
onPaste?: (event: ClipboardEvent) => void;
|
|
10
|
+
onFocus?: (event: FocusEvent) => void;
|
|
11
|
+
onBlur?: (event: FocusEvent) => void;
|
|
12
|
+
containerClass?: string;
|
|
13
|
+
inputClass?: string;
|
|
14
|
+
clearLabel?: string;
|
|
15
|
+
inputElement?: HTMLInputElement | null;
|
|
16
|
+
inputAttributes?: Record<string, unknown>;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
export default class SearchInput extends SvelteComponent<SearchInputProps> {}
|
|
19
|
+
export default class SearchInput extends SvelteComponent<SearchInputProps> {}
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
<button
|
|
92
92
|
type="button"
|
|
93
93
|
class={cn(
|
|
94
|
-
'bg-foreground/
|
|
94
|
+
'bg-muted-foreground/80 absolute inset-0 backdrop-blur-[3px] motion-reduce:transition-none',
|
|
95
95
|
visible ? 'opacity-100' : 'opacity-0'
|
|
96
96
|
)}
|
|
97
97
|
style="transition: opacity {transitionMs}ms {transitionEase};"
|
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
import type { Snippet } from 'svelte';
|
|
27
27
|
import * as Dialog from '../ui/dialog/index.js';
|
|
28
28
|
import { cn } from '../../utils.js';
|
|
29
|
+
import { modalState } from '../modals/modalStore.js';
|
|
29
30
|
import SettingsNav from './SettingsNav.svelte';
|
|
30
31
|
import { defaultSettingsDialogMessages, type SettingsDialogMessages } from './messages.js';
|
|
31
32
|
import type { SettingsNavGroup } from './settings-nav-types.js';
|
|
33
|
+
import { createNestedModalTracker } from './nested-modal-guard.js';
|
|
32
34
|
|
|
33
35
|
let {
|
|
34
36
|
open = $bindable(false),
|
|
@@ -46,19 +48,62 @@
|
|
|
46
48
|
children,
|
|
47
49
|
descriptionSlot
|
|
48
50
|
}: SettingsDialogProps = $props();
|
|
51
|
+
|
|
52
|
+
// bits-ui sets `contain: layout style` inline on every dialog content, and `contain: layout` makes that box the containing
|
|
53
|
+
// block of every `position: fixed` element inside it. A pane can hold an inline overlay of its own (a confirm dialog written
|
|
54
|
+
// as `fixed inset-0`): with containment on it would be drawn, and clipped, inside this box instead of over the viewport.
|
|
55
|
+
// The Content below therefore carries `contain-none!`.
|
|
56
|
+
|
|
57
|
+
// A modal a pane opens (ActionModal, RenameDialog, ...) is drawn outside this dialog: while one is open, a click or
|
|
58
|
+
// Escape in it must not close the settings dialog, and the focus trap must let its fields take the focus.
|
|
59
|
+
const nestedModalTracker = createNestedModalTracker();
|
|
60
|
+
const nestedModalOpen = $derived($modalState.isOpen);
|
|
61
|
+
$effect(() => {
|
|
62
|
+
nestedModalTracker.update(nestedModalOpen);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Every way the dialog closes (X, Escape, a click outside) arrives here. The request is refused while a nested modal
|
|
66
|
+
// holds the dialog; the click itself is left alone so the modal still reads its own click.
|
|
67
|
+
function requestOpenChange(nextOpen: boolean): void {
|
|
68
|
+
if (!nextOpen && nestedModalTracker.holdsDialog()) return;
|
|
69
|
+
open = nextOpen;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// bits-ui marks the original Escape key event as handled before BaseModal (a window listener) reads it, and BaseModal
|
|
73
|
+
// leaves a handled Escape alone. While a nested modal is open the key therefore belongs to the modal: this dialog
|
|
74
|
+
// stays open and hands the modal a fresh Escape, so one press closes the modal only.
|
|
75
|
+
function handleEscapeKeydown(event: KeyboardEvent): void {
|
|
76
|
+
if (!nestedModalTracker.open) return;
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
window.dispatchEvent(
|
|
79
|
+
new KeyboardEvent('keydown', {
|
|
80
|
+
key: 'Escape',
|
|
81
|
+
code: 'Escape',
|
|
82
|
+
bubbles: true,
|
|
83
|
+
cancelable: true
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
}
|
|
49
87
|
</script>
|
|
50
88
|
|
|
51
|
-
<Dialog.Root bind:open>
|
|
89
|
+
<Dialog.Root bind:open={() => open, requestOpenChange}>
|
|
52
90
|
<Dialog.Portal>
|
|
53
|
-
|
|
91
|
+
<!-- The scrim is the shared one of every overlay (muted foreground at 80%, 3px blur), set by the dialog overlay itself -->
|
|
92
|
+
<Dialog.Overlay />
|
|
54
93
|
<DialogPrimitive.Content
|
|
55
94
|
class={cn(
|
|
56
95
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 border-border bg-background duration-base fixed z-50 flex overflow-hidden border',
|
|
57
96
|
'inset-0 h-full max-h-none w-full rounded-none',
|
|
58
|
-
|
|
97
|
+
// Centred with auto margins, not with a translate: a CSS transform would make this box the containing block
|
|
98
|
+
// of every `position: fixed` element inside a pane (a legacy confirm dialog would be drawn, and clipped, inside it).
|
|
99
|
+
'lg:h-settings-modal lg:w-settings-modal lg:rounded-panel lg:m-auto lg:max-h-[calc(100vh-2rem)] lg:max-w-[calc(100vw-2rem)]',
|
|
100
|
+
// bits-ui's inline `contain: layout style` wins over a `style` prop, so the override has to be `!important`.
|
|
101
|
+
'contain-none!',
|
|
59
102
|
className
|
|
60
103
|
)}
|
|
61
104
|
aria-describedby={description || descriptionSlot ? 'settings-dialog-description' : undefined}
|
|
105
|
+
trapFocus={!nestedModalOpen}
|
|
106
|
+
onEscapeKeydown={handleEscapeKeydown}
|
|
62
107
|
>
|
|
63
108
|
<div class="flex min-h-0 min-w-0 flex-1 flex-col lg:flex-row">
|
|
64
109
|
<aside
|
|
@@ -9,8 +9,8 @@ export declare const widgetRowVariants: import("tailwind-variants").TVReturnType
|
|
|
9
9
|
};
|
|
10
10
|
}, undefined, "group/widget-row flex min-h-16 w-full max-w-content items-center gap-4 rounded-xl py-3 ps-4 pe-4 transition-colors duration-fast focus-within:outline-hidden", {
|
|
11
11
|
responsiveVariants?: boolean | (string | number | symbol)[] | {
|
|
12
|
-
selected?: boolean | (string | number | symbol)[] | undefined;
|
|
13
12
|
loading?: boolean | (string | number | symbol)[] | undefined;
|
|
13
|
+
selected?: boolean | (string | number | symbol)[] | undefined;
|
|
14
14
|
} | undefined;
|
|
15
15
|
} & import("tailwind-variants/dist/config.js").TWMConfig & {
|
|
16
16
|
twMergeConfig: {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A settings pane often opens a modal of its own (rename, confirm, a form): the `BaseModal` family
|
|
3
|
+
* (`ActionModal`, `NewItemDialog`, `RenameDialog`). Those are drawn in a portal on the body, outside the content of the
|
|
4
|
+
* settings dialog, so the dialog's dismissible layer reads a click in them as a click outside (it would close the
|
|
5
|
+
* settings dialog under the modal), its focus trap pulls the focus out of their fields, and one Escape would close both.
|
|
6
|
+
*
|
|
7
|
+
* The dialog asks this tracker, which is told what `modalState` (the stack every `BaseModal` pushes itself on) says,
|
|
8
|
+
* whether a modal of that family is open, so it can refuse a close request and release its focus trap while one is.
|
|
9
|
+
*
|
|
10
|
+
* It refuses the close request itself and never calls `preventDefault()` on the click. The Escape key is the one
|
|
11
|
+
* exception to "leave the event alone": bits-ui marks the original key event as handled before `BaseModal` reads it, and
|
|
12
|
+
* `BaseModal` closes on Escape only when the key event is not default-prevented, so the dialog hands the modal a fresh
|
|
13
|
+
* Escape (see `SettingsDialog`).
|
|
14
|
+
*/
|
|
15
|
+
/** How long after the last such modal closed a click or a key still belongs to it (its own click closes it first). */
|
|
16
|
+
export declare const NESTED_MODAL_CLOSE_GRACE_MS = 300;
|
|
17
|
+
export type NestedModalTracker = {
|
|
18
|
+
/** Tell the tracker whether a modal of the family is open right now. */
|
|
19
|
+
update(modalIsOpen: boolean): void;
|
|
20
|
+
/** True while a modal of the family is open. */
|
|
21
|
+
readonly open: boolean;
|
|
22
|
+
/** True while one is open, and for a moment after the last one closed. */
|
|
23
|
+
holdsDialog(): boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function createNestedModalTracker(now?: () => number): NestedModalTracker;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A settings pane often opens a modal of its own (rename, confirm, a form): the `BaseModal` family
|
|
3
|
+
* (`ActionModal`, `NewItemDialog`, `RenameDialog`). Those are drawn in a portal on the body, outside the content of the
|
|
4
|
+
* settings dialog, so the dialog's dismissible layer reads a click in them as a click outside (it would close the
|
|
5
|
+
* settings dialog under the modal), its focus trap pulls the focus out of their fields, and one Escape would close both.
|
|
6
|
+
*
|
|
7
|
+
* The dialog asks this tracker, which is told what `modalState` (the stack every `BaseModal` pushes itself on) says,
|
|
8
|
+
* whether a modal of that family is open, so it can refuse a close request and release its focus trap while one is.
|
|
9
|
+
*
|
|
10
|
+
* It refuses the close request itself and never calls `preventDefault()` on the click. The Escape key is the one
|
|
11
|
+
* exception to "leave the event alone": bits-ui marks the original key event as handled before `BaseModal` reads it, and
|
|
12
|
+
* `BaseModal` closes on Escape only when the key event is not default-prevented, so the dialog hands the modal a fresh
|
|
13
|
+
* Escape (see `SettingsDialog`).
|
|
14
|
+
*/
|
|
15
|
+
/** How long after the last such modal closed a click or a key still belongs to it (its own click closes it first). */
|
|
16
|
+
export const NESTED_MODAL_CLOSE_GRACE_MS = 300;
|
|
17
|
+
export function createNestedModalTracker(now = Date.now) {
|
|
18
|
+
let open = false;
|
|
19
|
+
let closedAtMs = Number.NEGATIVE_INFINITY;
|
|
20
|
+
return {
|
|
21
|
+
update(modalIsOpen) {
|
|
22
|
+
if (open && !modalIsOpen)
|
|
23
|
+
closedAtMs = now();
|
|
24
|
+
open = modalIsOpen;
|
|
25
|
+
},
|
|
26
|
+
get open() {
|
|
27
|
+
return open;
|
|
28
|
+
},
|
|
29
|
+
holdsDialog() {
|
|
30
|
+
return open || now() - closedAtMs < NESTED_MODAL_CLOSE_GRACE_MS;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -6,6 +6,6 @@ export type CalendarProps = WithoutChildrenOrChild<CalendarPrimitive.RootProps>
|
|
|
6
6
|
nextLabel?: string;
|
|
7
7
|
};
|
|
8
8
|
import * as Calendar from "./index.js";
|
|
9
|
-
declare const Calendar: import("svelte").Component<CalendarProps, {}, "
|
|
9
|
+
declare const Calendar: import("svelte").Component<CalendarProps, {}, "ref" | "value" | "placeholder">;
|
|
10
10
|
type Calendar = ReturnType<typeof Calendar>;
|
|
11
11
|
export default Calendar;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
<CommandPrimitive.Item
|
|
17
17
|
class={cn(
|
|
18
|
-
"text-body aria-selected:bg-accent aria-selected:text-foreground relative flex h-8 cursor-
|
|
18
|
+
"text-body aria-selected:bg-accent aria-selected:text-foreground relative flex h-8 cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
|
|
19
19
|
className
|
|
20
20
|
)}
|
|
21
21
|
bind:ref
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
<CommandPrimitive.LinkItem
|
|
17
17
|
class={cn(
|
|
18
|
-
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-
|
|
18
|
+
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
19
19
|
className
|
|
20
20
|
)}
|
|
21
21
|
bind:ref
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
import type { Snippet } from "svelte";
|
|
5
5
|
import * as Dialog from "./index.js";
|
|
6
6
|
import { cn } from "../../../utils.js";
|
|
7
|
+
import {
|
|
8
|
+
overlayDialogContentClasses,
|
|
9
|
+
overlayReducedMotionClasses
|
|
10
|
+
} from "../../../utils/overlayMotion.js";
|
|
7
11
|
|
|
8
12
|
let {
|
|
9
13
|
ref = $bindable(null),
|
|
@@ -24,8 +28,11 @@
|
|
|
24
28
|
<Dialog.Overlay />
|
|
25
29
|
<DialogPrimitive.Content
|
|
26
30
|
bind:ref
|
|
31
|
+
data-widgetic-overlay="dialog-panel"
|
|
27
32
|
class={cn(
|
|
28
|
-
"
|
|
33
|
+
"bg-background border-border fixed start-1/2 top-1/2 z-50 grid w-dialog max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-panel border p-6",
|
|
34
|
+
overlayDialogContentClasses,
|
|
35
|
+
overlayReducedMotionClasses,
|
|
29
36
|
className
|
|
30
37
|
)}
|
|
31
38
|
{...restProps}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
<script lang="ts">
|
|
7
7
|
import { cn } from "../../../utils.js";
|
|
8
|
+
import {
|
|
9
|
+
overlayDialogOverlayClasses,
|
|
10
|
+
overlayReducedMotionClasses
|
|
11
|
+
} from "../../../utils/overlayMotion.js";
|
|
8
12
|
|
|
9
13
|
let {
|
|
10
14
|
ref = $bindable(null),
|
|
@@ -15,8 +19,11 @@
|
|
|
15
19
|
|
|
16
20
|
<DialogPrimitive.Overlay
|
|
17
21
|
bind:ref
|
|
22
|
+
data-widgetic-overlay="dialog-backdrop"
|
|
18
23
|
class={cn(
|
|
19
|
-
"
|
|
24
|
+
"fixed inset-0 z-50",
|
|
25
|
+
overlayDialogOverlayClasses,
|
|
26
|
+
overlayReducedMotionClasses,
|
|
20
27
|
className
|
|
21
28
|
)}
|
|
22
29
|
{...restProps}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
bind:checked
|
|
23
23
|
bind:indeterminate
|
|
24
24
|
class={cn(
|
|
25
|
-
"text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex h-8 cursor-
|
|
25
|
+
"text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex h-8 cursor-pointer select-none items-center rounded-lg py-1.5 ps-8 pe-2 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
26
26
|
className
|
|
27
27
|
)}
|
|
28
28
|
{...restProps}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
<DropdownMenuPrimitive.Item
|
|
23
23
|
bind:ref
|
|
24
24
|
class={cn(
|
|
25
|
-
"text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex cursor-
|
|
25
|
+
"text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333] data-[variant=destructive]:text-destructive data-highlighted:data-[variant=destructive]:text-destructive",
|
|
26
26
|
description ? "min-h-12" : "min-h-8",
|
|
27
27
|
inset && "ps-8",
|
|
28
28
|
className
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<DropdownMenuPrimitive.RadioItem
|
|
19
19
|
bind:ref
|
|
20
20
|
class={cn(
|
|
21
|
-
"text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex h-8 cursor-
|
|
21
|
+
"text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex h-8 cursor-pointer select-none items-center rounded-lg py-1.5 ps-8 pe-2 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
22
22
|
className
|
|
23
23
|
)}
|
|
24
24
|
{...restProps}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<DropdownMenuPrimitive.SubTrigger
|
|
18
18
|
bind:ref
|
|
19
19
|
class={cn(
|
|
20
|
-
"text-body data-highlighted:bg-accent data-[state=open]:bg-accent flex h-8 cursor-
|
|
20
|
+
"text-body data-highlighted:bg-accent data-[state=open]:bg-accent flex h-8 cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden transition-colors duration-fast [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
|
|
21
21
|
inset && "ps-8",
|
|
22
22
|
className
|
|
23
23
|
)}
|
|
@@ -6,6 +6,6 @@ export type RangeCalendarProps = WithoutChildrenOrChild<RangeCalendarPrimitive.R
|
|
|
6
6
|
nextLabel?: string;
|
|
7
7
|
};
|
|
8
8
|
import * as RangeCalendar from "./index.js";
|
|
9
|
-
declare const RangeCalendar: import("svelte").Component<RangeCalendarProps, {}, "
|
|
9
|
+
declare const RangeCalendar: import("svelte").Component<RangeCalendarProps, {}, "ref" | "value" | "placeholder">;
|
|
10
10
|
type RangeCalendar = ReturnType<typeof RangeCalendar>;
|
|
11
11
|
export default RangeCalendar;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
bind:ref
|
|
23
23
|
{value}
|
|
24
24
|
class={cn(
|
|
25
|
-
"relative flex h-8 w-full cursor-
|
|
25
|
+
"relative flex h-8 w-full cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 ps-8 text-body outline-hidden data-[highlighted]:bg-accent data-[selected]:bg-brand-subtle data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
|
|
26
26
|
className
|
|
27
27
|
)}
|
|
28
28
|
{...restProps}
|
package/dist/styles/global.css
CHANGED
|
@@ -609,10 +609,11 @@ go 3 levels up in development mode, 2 levels up in production mode*/
|
|
|
609
609
|
}
|
|
610
610
|
|
|
611
611
|
/* Modal System */
|
|
612
|
+
/* The scrim of every overlay: the muted foreground token at 80%, set on the colour, with a 3px blur (Figma `Overlay/Scrim blur`) */
|
|
612
613
|
.modal-overlay {
|
|
613
614
|
@apply fixed inset-0 z-50;
|
|
614
|
-
backdrop-filter: blur(
|
|
615
|
-
background-color:
|
|
615
|
+
backdrop-filter: blur(3px);
|
|
616
|
+
background-color: color-mix(in srgb, var(--muted-foreground) 80%, transparent);
|
|
616
617
|
}
|
|
617
618
|
|
|
618
619
|
.modal-shadow {
|