@nqmcreative/ui 0.2.1 → 0.2.3
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 +7 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/trigger.d.ts +6 -0
- package/dist/core/trigger.js +13 -0
- package/dist/styles/matte/Button.svelte +4 -4
- package/dist/styles/matte/Card.svelte +3 -3
- package/dist/styles/matte/Combobox.svelte +2 -2
- package/dist/styles/matte/CommandPalette.svelte +32 -7
- package/dist/styles/matte/CommandPalette.svelte.d.ts +5 -0
- package/dist/styles/matte/DatePicker.svelte +2 -2
- package/dist/styles/matte/Drawer.svelte +2 -2
- package/dist/styles/matte/Dropzone.svelte +1 -1
- package/dist/styles/matte/FeatureCard.svelte +1 -1
- package/dist/styles/matte/Input.svelte +4 -4
- package/dist/styles/matte/MenuItem.svelte +1 -1
- package/dist/styles/matte/Modal.svelte +3 -3
- package/dist/styles/matte/NumberInput.svelte +2 -2
- package/dist/styles/matte/PasswordInput.svelte +2 -2
- package/dist/styles/matte/PricingCard.svelte +3 -3
- package/dist/styles/matte/SegmentedControl.svelte +1 -1
- package/dist/styles/matte/Select.svelte +1 -1
- package/dist/styles/matte/Sidebar.svelte +75 -26
- package/dist/styles/matte/Sidebar.svelte.d.ts +15 -1
- package/dist/styles/matte/StatsBand.svelte +1 -1
- package/dist/styles/matte/Table.svelte +3 -3
- package/dist/styles/matte/Testimonial.svelte +2 -2
- package/dist/styles/matte/Textarea.svelte +1 -1
- package/dist/styles/matte/index.d.ts +1 -1
- package/dist/styles/paper/CommandPalette.svelte +25 -2
- package/dist/styles/paper/CommandPalette.svelte.d.ts +5 -0
- package/dist/styles/paper/Modal.svelte +9 -1
- package/dist/styles/paper/Sidebar.svelte +75 -26
- package/dist/styles/paper/Sidebar.svelte.d.ts +15 -1
- package/dist/styles/paper/fonts.css +34 -7
- package/dist/styles/paper/index.d.ts +1 -1
- package/dist/styles/paper/theme.css +11 -8
- package/dist/styles/sprout/Accordion.svelte +17 -0
- package/dist/styles/sprout/Accordion.svelte.d.ts +8 -0
- package/dist/styles/sprout/AccordionItem.svelte +46 -0
- package/dist/styles/sprout/AccordionItem.svelte.d.ts +14 -0
- package/dist/styles/sprout/Alert.svelte +100 -0
- package/dist/styles/sprout/Alert.svelte.d.ts +18 -0
- package/dist/styles/sprout/Avatar.svelte +60 -0
- package/dist/styles/sprout/Avatar.svelte.d.ts +16 -0
- package/dist/styles/sprout/AvatarGroup.svelte +17 -0
- package/dist/styles/sprout/AvatarGroup.svelte.d.ts +8 -0
- package/dist/styles/sprout/Badge.svelte +58 -0
- package/dist/styles/sprout/Badge.svelte.d.ts +16 -0
- package/dist/styles/sprout/Breadcrumb.svelte +49 -0
- package/dist/styles/sprout/Breadcrumb.svelte.d.ts +14 -0
- package/dist/styles/sprout/Button.svelte +105 -0
- package/dist/styles/sprout/Button.svelte.d.ts +21 -0
- package/dist/styles/sprout/CTASection.svelte +77 -0
- package/dist/styles/sprout/CTASection.svelte.d.ts +18 -0
- package/dist/styles/sprout/Calendar.svelte +202 -0
- package/dist/styles/sprout/Calendar.svelte.d.ts +22 -0
- package/dist/styles/sprout/Card.svelte +82 -0
- package/dist/styles/sprout/Card.svelte.d.ts +21 -0
- package/dist/styles/sprout/Checkbox.svelte +76 -0
- package/dist/styles/sprout/Checkbox.svelte.d.ts +15 -0
- package/dist/styles/sprout/CheckboxGroup.svelte +112 -0
- package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +30 -0
- package/dist/styles/sprout/Combobox.svelte +267 -0
- package/dist/styles/sprout/Combobox.svelte.d.ts +29 -0
- package/dist/styles/sprout/CommandPalette.svelte +233 -0
- package/dist/styles/sprout/CommandPalette.svelte.d.ts +37 -0
- package/dist/styles/sprout/ConfirmDialog.svelte +97 -0
- package/dist/styles/sprout/ConfirmDialog.svelte.d.ts +22 -0
- package/dist/styles/sprout/ContextMenu.svelte +89 -0
- package/dist/styles/sprout/ContextMenu.svelte.d.ts +15 -0
- package/dist/styles/sprout/DatePicker.svelte +193 -0
- package/dist/styles/sprout/DatePicker.svelte.d.ts +23 -0
- package/dist/styles/sprout/Divider.svelte +30 -0
- package/dist/styles/sprout/Divider.svelte.d.ts +10 -0
- package/dist/styles/sprout/Drawer.svelte +166 -0
- package/dist/styles/sprout/Drawer.svelte.d.ts +20 -0
- package/dist/styles/sprout/Dropdown.svelte +95 -0
- package/dist/styles/sprout/Dropdown.svelte.d.ts +23 -0
- package/dist/styles/sprout/Dropzone.svelte +211 -0
- package/dist/styles/sprout/Dropzone.svelte.d.ts +29 -0
- package/dist/styles/sprout/EmptyState.svelte +71 -0
- package/dist/styles/sprout/EmptyState.svelte.d.ts +18 -0
- package/dist/styles/sprout/FeatureCard.svelte +61 -0
- package/dist/styles/sprout/FeatureCard.svelte.d.ts +16 -0
- package/dist/styles/sprout/FeatureGrid.svelte +62 -0
- package/dist/styles/sprout/FeatureGrid.svelte.d.ts +16 -0
- package/dist/styles/sprout/Field.svelte +42 -0
- package/dist/styles/sprout/Field.svelte.d.ts +16 -0
- package/dist/styles/sprout/Footer.svelte +91 -0
- package/dist/styles/sprout/Footer.svelte.d.ts +25 -0
- package/dist/styles/sprout/HeroSection.svelte +86 -0
- package/dist/styles/sprout/HeroSection.svelte.d.ts +21 -0
- package/dist/styles/sprout/Input.svelte +61 -0
- package/dist/styles/sprout/Input.svelte.d.ts +18 -0
- package/dist/styles/sprout/InputGroup.svelte +20 -0
- package/dist/styles/sprout/InputGroup.svelte.d.ts +11 -0
- package/dist/styles/sprout/Kbd.svelte +18 -0
- package/dist/styles/sprout/Kbd.svelte.d.ts +8 -0
- package/dist/styles/sprout/Label.svelte +16 -0
- package/dist/styles/sprout/Label.svelte.d.ts +9 -0
- package/dist/styles/sprout/Link.svelte +44 -0
- package/dist/styles/sprout/Link.svelte.d.ts +16 -0
- package/dist/styles/sprout/LocaleProvider.svelte +24 -0
- package/dist/styles/sprout/LocaleProvider.svelte.d.ts +10 -0
- package/dist/styles/sprout/LogoCloud.svelte +61 -0
- package/dist/styles/sprout/LogoCloud.svelte.d.ts +19 -0
- package/dist/styles/sprout/MenuItem.svelte +64 -0
- package/dist/styles/sprout/MenuItem.svelte.d.ts +18 -0
- package/dist/styles/sprout/MenuSeparator.svelte +22 -0
- package/dist/styles/sprout/MenuSeparator.svelte.d.ts +8 -0
- package/dist/styles/sprout/Modal.svelte +127 -0
- package/dist/styles/sprout/Modal.svelte.d.ts +21 -0
- package/dist/styles/sprout/MultiSelect.svelte +288 -0
- package/dist/styles/sprout/MultiSelect.svelte.d.ts +31 -0
- package/dist/styles/sprout/Navbar.svelte +201 -0
- package/dist/styles/sprout/Navbar.svelte.d.ts +33 -0
- package/dist/styles/sprout/NumberInput.svelte +97 -0
- package/dist/styles/sprout/NumberInput.svelte.d.ts +15 -0
- package/dist/styles/sprout/Pagination.svelte +84 -0
- package/dist/styles/sprout/Pagination.svelte.d.ts +15 -0
- package/dist/styles/sprout/PasswordInput.svelte +101 -0
- package/dist/styles/sprout/PasswordInput.svelte.d.ts +12 -0
- package/dist/styles/sprout/Popover.svelte +90 -0
- package/dist/styles/sprout/Popover.svelte.d.ts +19 -0
- package/dist/styles/sprout/PricingCard.svelte +109 -0
- package/dist/styles/sprout/PricingCard.svelte.d.ts +25 -0
- package/dist/styles/sprout/Progress.svelte +59 -0
- package/dist/styles/sprout/Progress.svelte.d.ts +17 -0
- package/dist/styles/sprout/Radio.svelte +56 -0
- package/dist/styles/sprout/Radio.svelte.d.ts +15 -0
- package/dist/styles/sprout/RadioGroup.svelte +96 -0
- package/dist/styles/sprout/RadioGroup.svelte.d.ts +27 -0
- package/dist/styles/sprout/SegmentedControl.svelte +89 -0
- package/dist/styles/sprout/SegmentedControl.svelte.d.ts +25 -0
- package/dist/styles/sprout/Select.svelte +62 -0
- package/dist/styles/sprout/Select.svelte.d.ts +20 -0
- package/dist/styles/sprout/Sidebar.svelte +255 -0
- package/dist/styles/sprout/Sidebar.svelte.d.ts +50 -0
- package/dist/styles/sprout/Skeleton.svelte +46 -0
- package/dist/styles/sprout/Skeleton.svelte.d.ts +13 -0
- package/dist/styles/sprout/Slider.svelte +80 -0
- package/dist/styles/sprout/Slider.svelte.d.ts +19 -0
- package/dist/styles/sprout/Spinner.svelte +34 -0
- package/dist/styles/sprout/Spinner.svelte.d.ts +13 -0
- package/dist/styles/sprout/Stat.svelte +51 -0
- package/dist/styles/sprout/Stat.svelte.d.ts +16 -0
- package/dist/styles/sprout/StatsBand.svelte +64 -0
- package/dist/styles/sprout/StatsBand.svelte.d.ts +20 -0
- package/dist/styles/sprout/Steps.svelte +143 -0
- package/dist/styles/sprout/Steps.svelte.d.ts +24 -0
- package/dist/styles/sprout/Switch.svelte +65 -0
- package/dist/styles/sprout/Switch.svelte.d.ts +14 -0
- package/dist/styles/sprout/Table.svelte +218 -0
- package/dist/styles/sprout/Table.svelte.d.ts +66 -0
- package/dist/styles/sprout/Tabs.svelte +103 -0
- package/dist/styles/sprout/Tabs.svelte.d.ts +23 -0
- package/dist/styles/sprout/Testimonial.svelte +65 -0
- package/dist/styles/sprout/Testimonial.svelte.d.ts +19 -0
- package/dist/styles/sprout/Textarea.svelte +55 -0
- package/dist/styles/sprout/Textarea.svelte.d.ts +12 -0
- package/dist/styles/sprout/ThemeToggle.svelte +131 -0
- package/dist/styles/sprout/ThemeToggle.svelte.d.ts +16 -0
- package/dist/styles/sprout/Toaster.svelte +133 -0
- package/dist/styles/sprout/Toaster.svelte.d.ts +8 -0
- package/dist/styles/sprout/Tooltip.svelte +34 -0
- package/dist/styles/sprout/Tooltip.svelte.d.ts +12 -0
- package/dist/styles/sprout/fonts.css +77 -0
- package/dist/styles/sprout/index.d.ts +109 -0
- package/dist/styles/sprout/index.js +83 -0
- package/dist/styles/sprout/lift.d.ts +40 -0
- package/dist/styles/sprout/lift.js +36 -0
- package/dist/styles/sprout/theme.css +255 -0
- package/package.json +269 -3
- package/registry.json +139 -66
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { anchored } from '../../core/actions/anchor.js';
|
|
4
|
+
import { clickOutside, focusTrap, navigateList, portal } from '../../core/actions/dismissable.js';
|
|
5
|
+
import { menuItems } from '../../core/trigger.js';
|
|
6
|
+
import { useLocale } from '../../core/locale.svelte.js';
|
|
7
|
+
import { soft } from './lift.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
/** The region that responds to a right-click. */
|
|
11
|
+
children: Snippet;
|
|
12
|
+
/** Menu contents: `MenuItem`, `MenuSeparator`, anything. */
|
|
13
|
+
menu: Snippet;
|
|
14
|
+
open?: boolean;
|
|
15
|
+
label?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
class?: string;
|
|
18
|
+
menuClass?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
children,
|
|
23
|
+
menu,
|
|
24
|
+
open = $bindable(false),
|
|
25
|
+
label,
|
|
26
|
+
disabled = false,
|
|
27
|
+
class: className = '',
|
|
28
|
+
menuClass = ''
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
const t = useLocale();
|
|
32
|
+
|
|
33
|
+
let menuEl: HTMLElement | null = $state(null);
|
|
34
|
+
/**
|
|
35
|
+
* A 0×0 element parked at the pointer, so the same `anchored` action that
|
|
36
|
+
* positions dropdowns can flip and clamp this menu too.
|
|
37
|
+
*/
|
|
38
|
+
let cursor: HTMLElement | null = $state(null);
|
|
39
|
+
|
|
40
|
+
function onContextMenu(event: MouseEvent) {
|
|
41
|
+
if (disabled) return;
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
if (!cursor) {
|
|
44
|
+
cursor = document.createElement('div');
|
|
45
|
+
cursor.style.cssText = 'position:fixed;width:0;height:0;pointer-events:none';
|
|
46
|
+
document.body.appendChild(cursor);
|
|
47
|
+
}
|
|
48
|
+
cursor.style.left = `${event.clientX}px`;
|
|
49
|
+
cursor.style.top = `${event.clientY}px`;
|
|
50
|
+
open = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
$effect(() => () => cursor?.remove());
|
|
54
|
+
|
|
55
|
+
function onMenuKeydown(event: KeyboardEvent) {
|
|
56
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
57
|
+
if (event.key === 'Escape') event.preventDefault();
|
|
58
|
+
event.stopPropagation();
|
|
59
|
+
open = false;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
navigateList(event, menuItems(menuEl));
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
67
|
+
<div oncontextmenu={onContextMenu} class={className}>
|
|
68
|
+
{@render children()}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{#if open}
|
|
72
|
+
<div
|
|
73
|
+
bind:this={menuEl}
|
|
74
|
+
use:portal
|
|
75
|
+
use:anchored={{ anchor: cursor, placement: 'bottom-start', offset: 2 }}
|
|
76
|
+
use:clickOutside={{ onoutside: () => (open = false) }}
|
|
77
|
+
use:focusTrap={{ restore: true }}
|
|
78
|
+
onkeydown={onMenuKeydown}
|
|
79
|
+
onclick={(event) => {
|
|
80
|
+
if ((event.target as HTMLElement).closest('[role="menuitem"]:not([disabled])')) open = false;
|
|
81
|
+
}}
|
|
82
|
+
role="menu"
|
|
83
|
+
aria-label={label ?? t.current.menu}
|
|
84
|
+
tabindex="-1"
|
|
85
|
+
class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-2xl border border-hairline bg-bg py-1 font-sans {soft} {menuClass}"
|
|
86
|
+
>
|
|
87
|
+
{@render menu()}
|
|
88
|
+
</div>
|
|
89
|
+
{/if}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The region that responds to a right-click. */
|
|
4
|
+
children: Snippet;
|
|
5
|
+
/** Menu contents: `MenuItem`, `MenuSeparator`, anything. */
|
|
6
|
+
menu: Snippet;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
label?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
menuClass?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
|
|
14
|
+
type ContextMenu = ReturnType<typeof ContextMenu>;
|
|
15
|
+
export default ContextMenu;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { anchored } from '../../core/actions/anchor.js';
|
|
3
|
+
import { clickOutside, portal } from '../../core/actions/dismissable.js';
|
|
4
|
+
import { dateHint } from '../../core/calendar.js';
|
|
5
|
+
import { formatISO, parseFormatted } from '../../core/date.js';
|
|
6
|
+
import { useLocale } from '../../core/locale.svelte.js';
|
|
7
|
+
import { toneFocusWithinBorder, type Tone } from '../../core/tones.js';
|
|
8
|
+
import Calendar from './Calendar.svelte';
|
|
9
|
+
import { edge, soft } from './lift.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Bindable `YYYY-MM-DD` — the same shape as `<input type="date">`. */
|
|
13
|
+
value?: string;
|
|
14
|
+
/** Inclusive bounds, `YYYY-MM-DD`. */
|
|
15
|
+
min?: string;
|
|
16
|
+
max?: string;
|
|
17
|
+
isDisabled?: (date: Date) => boolean;
|
|
18
|
+
weekStart?: 0 | 1;
|
|
19
|
+
tone?: Tone;
|
|
20
|
+
invalid?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
clearable?: boolean;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
id?: string;
|
|
25
|
+
/** Submits the ISO value with a form. */
|
|
26
|
+
name?: string;
|
|
27
|
+
onchange?: (value: string) => void;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
value = $bindable(''),
|
|
33
|
+
min,
|
|
34
|
+
max,
|
|
35
|
+
isDisabled,
|
|
36
|
+
weekStart = 1,
|
|
37
|
+
tone = 'brand',
|
|
38
|
+
invalid = false,
|
|
39
|
+
disabled = false,
|
|
40
|
+
clearable = true,
|
|
41
|
+
placeholder,
|
|
42
|
+
id,
|
|
43
|
+
name,
|
|
44
|
+
onchange,
|
|
45
|
+
class: className = ''
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
const t = useLocale();
|
|
49
|
+
|
|
50
|
+
let open = $state(false);
|
|
51
|
+
let text = $state('');
|
|
52
|
+
let editing = $state(false);
|
|
53
|
+
let wrapper: HTMLElement | null = $state(null);
|
|
54
|
+
let inputEl: HTMLInputElement | null = $state(null);
|
|
55
|
+
|
|
56
|
+
/** While typing the field shows the raw text; otherwise the formatted value. */
|
|
57
|
+
const display = $derived(editing ? text : formatISO(value, t.current.dateFormat));
|
|
58
|
+
const hint = $derived(placeholder ?? dateHint(t.current.dateFormat));
|
|
59
|
+
|
|
60
|
+
function commit() {
|
|
61
|
+
editing = false;
|
|
62
|
+
if (!text.trim()) {
|
|
63
|
+
if (value) {
|
|
64
|
+
value = '';
|
|
65
|
+
onchange?.('');
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const parsed = parseFormatted(text, t.current.dateFormat);
|
|
70
|
+
if (parsed && parsed !== value) {
|
|
71
|
+
value = parsed;
|
|
72
|
+
onchange?.(parsed);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function onKeydown(event: KeyboardEvent) {
|
|
77
|
+
if (event.key === 'Escape' && open) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
event.stopPropagation();
|
|
80
|
+
open = false;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (event.key === 'Enter') {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
commit();
|
|
86
|
+
open = false;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (event.key === 'ArrowDown' && !open) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
open = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<div
|
|
97
|
+
bind:this={wrapper}
|
|
98
|
+
class="inline-flex w-full items-center gap-1 rounded-xl border bg-bg pr-1 pl-3 {edge} transition-colors duration-150 ease-brand-out
|
|
99
|
+
{invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
|
|
100
|
+
{disabled ? 'pointer-events-none opacity-50' : ''} {className}"
|
|
101
|
+
>
|
|
102
|
+
<input
|
|
103
|
+
bind:this={inputEl}
|
|
104
|
+
{id}
|
|
105
|
+
{disabled}
|
|
106
|
+
type="text"
|
|
107
|
+
inputmode="numeric"
|
|
108
|
+
autocomplete="off"
|
|
109
|
+
role="combobox"
|
|
110
|
+
aria-haspopup="dialog"
|
|
111
|
+
aria-controls="{id ?? 'datepicker'}-calendar"
|
|
112
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
113
|
+
aria-expanded={open}
|
|
114
|
+
placeholder={hint}
|
|
115
|
+
value={display}
|
|
116
|
+
oninput={(event) => {
|
|
117
|
+
editing = true;
|
|
118
|
+
text = event.currentTarget.value;
|
|
119
|
+
}}
|
|
120
|
+
onblur={commit}
|
|
121
|
+
onkeydown={onKeydown}
|
|
122
|
+
class="h-10 w-full min-w-0 bg-transparent font-sans text-sm text-text tabular-nums placeholder:text-text-muted focus:outline-none"
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
{#if name}<input type="hidden" {name} {value} />{/if}
|
|
126
|
+
|
|
127
|
+
{#if clearable && value && !disabled}
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onclick={() => {
|
|
131
|
+
value = '';
|
|
132
|
+
text = '';
|
|
133
|
+
onchange?.('');
|
|
134
|
+
inputEl?.focus();
|
|
135
|
+
}}
|
|
136
|
+
aria-label={t.current.clear}
|
|
137
|
+
class="shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
138
|
+
>
|
|
139
|
+
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
140
|
+
<path
|
|
141
|
+
d="m2 2 10 10M12 2 2 12"
|
|
142
|
+
stroke="currentColor"
|
|
143
|
+
stroke-width="1.6"
|
|
144
|
+
stroke-linecap="round"
|
|
145
|
+
/>
|
|
146
|
+
</svg>
|
|
147
|
+
</button>
|
|
148
|
+
{/if}
|
|
149
|
+
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
{disabled}
|
|
153
|
+
onclick={() => (open = !open)}
|
|
154
|
+
aria-label={t.current.selectDate}
|
|
155
|
+
aria-expanded={open}
|
|
156
|
+
class="shrink-0 rounded p-1.5 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
157
|
+
>
|
|
158
|
+
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
159
|
+
<rect x="3" y="4.5" width="14" height="12" rx="2" stroke="currentColor" stroke-width="1.4" />
|
|
160
|
+
<path
|
|
161
|
+
d="M3 8.5h14M7 3v3M13 3v3"
|
|
162
|
+
stroke="currentColor"
|
|
163
|
+
stroke-width="1.4"
|
|
164
|
+
stroke-linecap="round"
|
|
165
|
+
/>
|
|
166
|
+
</svg>
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{#if open}
|
|
171
|
+
<div
|
|
172
|
+
use:portal
|
|
173
|
+
use:anchored={{ anchor: wrapper, placement: 'bottom-start', offset: 4 }}
|
|
174
|
+
use:clickOutside={{ onoutside: () => (open = false), ignore: [wrapper] }}
|
|
175
|
+
id="{id ?? 'datepicker'}-calendar"
|
|
176
|
+
class="z-50 rounded-2xl border border-hairline bg-bg {soft}"
|
|
177
|
+
>
|
|
178
|
+
<Calendar
|
|
179
|
+
bind:value
|
|
180
|
+
{min}
|
|
181
|
+
{max}
|
|
182
|
+
{isDisabled}
|
|
183
|
+
{weekStart}
|
|
184
|
+
{tone}
|
|
185
|
+
onselect={(next) => {
|
|
186
|
+
editing = false;
|
|
187
|
+
open = false;
|
|
188
|
+
onchange?.(next);
|
|
189
|
+
inputEl?.focus();
|
|
190
|
+
}}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
{/if}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Tone } from '../../core/tones.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Bindable `YYYY-MM-DD` — the same shape as `<input type="date">`. */
|
|
4
|
+
value?: string;
|
|
5
|
+
/** Inclusive bounds, `YYYY-MM-DD`. */
|
|
6
|
+
min?: string;
|
|
7
|
+
max?: string;
|
|
8
|
+
isDisabled?: (date: Date) => boolean;
|
|
9
|
+
weekStart?: 0 | 1;
|
|
10
|
+
tone?: Tone;
|
|
11
|
+
invalid?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
clearable?: boolean;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
/** Submits the ISO value with a form. */
|
|
17
|
+
name?: string;
|
|
18
|
+
onchange?: (value: string) => void;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const DatePicker: import("svelte").Component<Props, {}, "value">;
|
|
22
|
+
type DatePicker = ReturnType<typeof DatePicker>;
|
|
23
|
+
export default DatePicker;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
export type DividerOrientation = 'horizontal' | 'vertical';
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
orientation?: DividerOrientation;
|
|
8
|
+
/** Centred caption drawn over a horizontal rule. */
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { orientation = 'horizontal', label, class: className = '', ...rest }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#if orientation === 'vertical'}
|
|
16
|
+
<div
|
|
17
|
+
role="separator"
|
|
18
|
+
aria-orientation="vertical"
|
|
19
|
+
class="w-px self-stretch bg-hairline {className}"
|
|
20
|
+
{...rest}
|
|
21
|
+
></div>
|
|
22
|
+
{:else if label}
|
|
23
|
+
<div class="flex items-center gap-3 {className}" {...rest}>
|
|
24
|
+
<span class="h-px flex-1 bg-hairline"></span>
|
|
25
|
+
<span class="font-sans text-xs font-medium text-text-muted">{label}</span>
|
|
26
|
+
<span class="h-px flex-1 bg-hairline"></span>
|
|
27
|
+
</div>
|
|
28
|
+
{:else}
|
|
29
|
+
<div role="separator" class="h-px w-full bg-hairline {className}" {...rest}></div>
|
|
30
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
export type DividerOrientation = 'horizontal' | 'vertical';
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
orientation?: DividerOrientation;
|
|
5
|
+
/** Centred caption drawn over a horizontal rule. */
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const Divider: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Divider = ReturnType<typeof Divider>;
|
|
10
|
+
export default Divider;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
4
|
+
import { useLocale } from '../../core/locale.svelte.js';
|
|
5
|
+
import { isBackdropClick, syncDialog } from '../../core/dialog.svelte.js';
|
|
6
|
+
import { float } from './lift.js';
|
|
7
|
+
|
|
8
|
+
export type DrawerSide = 'left' | 'right' | 'top' | 'bottom';
|
|
9
|
+
export type DrawerSize = 'sm' | 'md' | 'lg' | 'full';
|
|
10
|
+
|
|
11
|
+
interface Props extends HTMLDialogAttributes {
|
|
12
|
+
/** Bindable. Built on the native `<dialog>`, so it gets the top layer,
|
|
13
|
+
* the backdrop, Escape handling and focus containment for free. */
|
|
14
|
+
open?: boolean;
|
|
15
|
+
side?: DrawerSide;
|
|
16
|
+
size?: DrawerSize;
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
dismissible?: boolean;
|
|
20
|
+
footer?: Snippet;
|
|
21
|
+
onclose?: () => void;
|
|
22
|
+
children: Snippet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
open = $bindable(false),
|
|
27
|
+
side = 'right',
|
|
28
|
+
size = 'md',
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
dismissible = true,
|
|
32
|
+
footer,
|
|
33
|
+
onclose,
|
|
34
|
+
class: className = '',
|
|
35
|
+
children,
|
|
36
|
+
...rest
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
const t = useLocale();
|
|
40
|
+
let dialog: HTMLDialogElement | null = $state(null);
|
|
41
|
+
|
|
42
|
+
const horizontal = $derived(side === 'left' || side === 'right');
|
|
43
|
+
|
|
44
|
+
const widths: Record<DrawerSize, string> = {
|
|
45
|
+
sm: 'w-[min(20rem,100vw)]',
|
|
46
|
+
md: 'w-[min(28rem,100vw)]',
|
|
47
|
+
lg: 'w-[min(40rem,100vw)]',
|
|
48
|
+
full: 'w-screen'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const heights: Record<DrawerSize, string> = {
|
|
52
|
+
sm: 'h-[min(16rem,100vh)]',
|
|
53
|
+
md: 'h-[min(24rem,100vh)]',
|
|
54
|
+
lg: 'h-[min(36rem,100vh)]',
|
|
55
|
+
full: 'h-screen'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const sides: Record<DrawerSide, string> = {
|
|
59
|
+
left: 'mr-auto ml-0 h-screen max-h-screen border-r',
|
|
60
|
+
right: 'mr-0 ml-auto h-screen max-h-screen border-l',
|
|
61
|
+
top: 'mt-0 mb-auto w-screen max-w-screen border-b',
|
|
62
|
+
bottom: 'mt-auto mb-0 w-screen max-w-screen border-t'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const slide: Record<DrawerSide, string> = {
|
|
66
|
+
left: 'animate-[drawer-left_220ms_cubic-bezier(0,0,0.2,1)]',
|
|
67
|
+
right: 'animate-[drawer-right_220ms_cubic-bezier(0,0,0.2,1)]',
|
|
68
|
+
top: 'animate-[drawer-top_220ms_cubic-bezier(0,0,0.2,1)]',
|
|
69
|
+
bottom: 'animate-[drawer-bottom_220ms_cubic-bezier(0,0,0.2,1)]'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
$effect(() => syncDialog(dialog, open));
|
|
73
|
+
|
|
74
|
+
function close() {
|
|
75
|
+
open = false;
|
|
76
|
+
onclose?.();
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<dialog
|
|
81
|
+
bind:this={dialog}
|
|
82
|
+
onclose={close}
|
|
83
|
+
onclick={(event) => {
|
|
84
|
+
if (dismissible && isBackdropClick(event, dialog)) close();
|
|
85
|
+
}}
|
|
86
|
+
oncancel={(event) => {
|
|
87
|
+
if (!dismissible) event.preventDefault();
|
|
88
|
+
}}
|
|
89
|
+
class="max-h-none max-w-none border-hairline bg-bg p-0 text-text {float} backdrop:bg-text/30 backdrop:backdrop-blur-[1px]
|
|
90
|
+
{sides[side]} {horizontal ? widths[size] : heights[size]} {slide[side]} {className}"
|
|
91
|
+
{...rest}
|
|
92
|
+
>
|
|
93
|
+
<div class="flex h-full flex-col">
|
|
94
|
+
{#if title || dismissible}
|
|
95
|
+
<div class="flex items-start gap-4 border-b border-hairline px-5 py-4">
|
|
96
|
+
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
97
|
+
{#if title}
|
|
98
|
+
<h2 class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
99
|
+
{/if}
|
|
100
|
+
{#if description}
|
|
101
|
+
<p class="font-sans text-[13px] text-text-muted">{description}</p>
|
|
102
|
+
{/if}
|
|
103
|
+
</div>
|
|
104
|
+
{#if dismissible}
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onclick={close}
|
|
108
|
+
aria-label={t.current.close}
|
|
109
|
+
class="-m-1 shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
|
110
|
+
>
|
|
111
|
+
<svg class="size-4" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
112
|
+
<path
|
|
113
|
+
d="m2 2 10 10M12 2 2 12"
|
|
114
|
+
stroke="currentColor"
|
|
115
|
+
stroke-width="1.6"
|
|
116
|
+
stroke-linecap="round"
|
|
117
|
+
/>
|
|
118
|
+
</svg>
|
|
119
|
+
</button>
|
|
120
|
+
{/if}
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
123
|
+
|
|
124
|
+
<div
|
|
125
|
+
class="flex-1 overflow-y-auto px-5 py-4 font-sans text-sm leading-relaxed text-text-secondary"
|
|
126
|
+
>
|
|
127
|
+
{@render children()}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{#if footer}
|
|
131
|
+
<div
|
|
132
|
+
class="flex items-center justify-end gap-2 border-t border-hairline bg-bg-alt px-5 py-3.5"
|
|
133
|
+
>
|
|
134
|
+
{@render footer()}
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
137
|
+
</div>
|
|
138
|
+
</dialog>
|
|
139
|
+
|
|
140
|
+
<style>
|
|
141
|
+
@keyframes -global-drawer-right {
|
|
142
|
+
from {
|
|
143
|
+
transform: translateX(100%);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
@keyframes -global-drawer-left {
|
|
147
|
+
from {
|
|
148
|
+
transform: translateX(-100%);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
@keyframes -global-drawer-top {
|
|
152
|
+
from {
|
|
153
|
+
transform: translateY(-100%);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
@keyframes -global-drawer-bottom {
|
|
157
|
+
from {
|
|
158
|
+
transform: translateY(100%);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
@media (prefers-reduced-motion: reduce) {
|
|
162
|
+
dialog {
|
|
163
|
+
animation: none;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
3
|
+
export type DrawerSide = 'left' | 'right' | 'top' | 'bottom';
|
|
4
|
+
export type DrawerSize = 'sm' | 'md' | 'lg' | 'full';
|
|
5
|
+
interface Props extends HTMLDialogAttributes {
|
|
6
|
+
/** Bindable. Built on the native `<dialog>`, so it gets the top layer,
|
|
7
|
+
* the backdrop, Escape handling and focus containment for free. */
|
|
8
|
+
open?: boolean;
|
|
9
|
+
side?: DrawerSide;
|
|
10
|
+
size?: DrawerSize;
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
dismissible?: boolean;
|
|
14
|
+
footer?: Snippet;
|
|
15
|
+
onclose?: () => void;
|
|
16
|
+
children: Snippet;
|
|
17
|
+
}
|
|
18
|
+
declare const Drawer: import("svelte").Component<Props, {}, "open">;
|
|
19
|
+
type Drawer = ReturnType<typeof Drawer>;
|
|
20
|
+
export default Drawer;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { anchored, type Placement } from '../../core/actions/anchor.js';
|
|
4
|
+
import { clickOutside, focusTrap, navigateList, portal } from '../../core/actions/dismissable.js';
|
|
5
|
+
import { describeTrigger, menuItems, popupTrigger } from '../../core/trigger.js';
|
|
6
|
+
import { useLocale } from '../../core/locale.svelte.js';
|
|
7
|
+
import { soft } from './lift.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
/** The control that opens the menu — put a `Button` in here. */
|
|
11
|
+
trigger: Snippet;
|
|
12
|
+
/** Menu contents: `MenuItem`, `MenuSeparator`, or anything else. */
|
|
13
|
+
children: Snippet;
|
|
14
|
+
open?: boolean;
|
|
15
|
+
placement?: Placement;
|
|
16
|
+
offset?: number;
|
|
17
|
+
/** Stretch the menu to the trigger's width. */
|
|
18
|
+
matchWidth?: boolean;
|
|
19
|
+
/** Close as soon as an item inside is activated. Default true. */
|
|
20
|
+
closeOnSelect?: boolean;
|
|
21
|
+
/** Accessible name for the menu. Defaults to the locale's `menu`. */
|
|
22
|
+
label?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
/** Classes for the wrapper around the trigger. */
|
|
25
|
+
triggerClass?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
trigger,
|
|
30
|
+
children,
|
|
31
|
+
open = $bindable(false),
|
|
32
|
+
placement = 'bottom-start',
|
|
33
|
+
offset = 6,
|
|
34
|
+
matchWidth = false,
|
|
35
|
+
closeOnSelect = true,
|
|
36
|
+
label,
|
|
37
|
+
class: className = '',
|
|
38
|
+
triggerClass = ''
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
const t = useLocale();
|
|
42
|
+
|
|
43
|
+
let anchorEl: HTMLElement | null = $state(null);
|
|
44
|
+
let menuEl: HTMLElement | null = $state(null);
|
|
45
|
+
|
|
46
|
+
// The ARIA state goes on the consumer's own control, not on our wrapper.
|
|
47
|
+
$effect(() => describeTrigger(anchorEl, 'menu', open));
|
|
48
|
+
|
|
49
|
+
function onMenuKeydown(event: KeyboardEvent) {
|
|
50
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
51
|
+
if (event.key === 'Escape') event.preventDefault();
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
open = false;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
navigateList(event, menuItems(menuEl));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function onMenuClick(event: MouseEvent) {
|
|
60
|
+
if (!closeOnSelect) return;
|
|
61
|
+
if ((event.target as HTMLElement).closest('[role="menuitem"]:not([disabled])')) open = false;
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<span
|
|
66
|
+
bind:this={anchorEl}
|
|
67
|
+
use:popupTrigger={{
|
|
68
|
+
toggle: () => (open = !open),
|
|
69
|
+
open: () => (open = true),
|
|
70
|
+
close: () => (open = false),
|
|
71
|
+
arrowOpens: true,
|
|
72
|
+
isOpen: () => open
|
|
73
|
+
}}
|
|
74
|
+
class="inline-flex {triggerClass}"
|
|
75
|
+
>
|
|
76
|
+
{@render trigger()}
|
|
77
|
+
</span>
|
|
78
|
+
|
|
79
|
+
{#if open}
|
|
80
|
+
<div
|
|
81
|
+
bind:this={menuEl}
|
|
82
|
+
use:portal
|
|
83
|
+
use:anchored={{ anchor: anchorEl, placement, offset, matchWidth }}
|
|
84
|
+
use:clickOutside={{ onoutside: () => (open = false), ignore: [anchorEl] }}
|
|
85
|
+
use:focusTrap={{ restore: true }}
|
|
86
|
+
onkeydown={onMenuKeydown}
|
|
87
|
+
onclick={onMenuClick}
|
|
88
|
+
role="menu"
|
|
89
|
+
aria-label={label ?? t.current.menu}
|
|
90
|
+
tabindex="-1"
|
|
91
|
+
class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-2xl border border-hairline bg-bg py-1 font-sans {soft} {className}"
|
|
92
|
+
>
|
|
93
|
+
{@render children()}
|
|
94
|
+
</div>
|
|
95
|
+
{/if}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type Placement } from '../../core/actions/anchor.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The control that opens the menu — put a `Button` in here. */
|
|
5
|
+
trigger: Snippet;
|
|
6
|
+
/** Menu contents: `MenuItem`, `MenuSeparator`, or anything else. */
|
|
7
|
+
children: Snippet;
|
|
8
|
+
open?: boolean;
|
|
9
|
+
placement?: Placement;
|
|
10
|
+
offset?: number;
|
|
11
|
+
/** Stretch the menu to the trigger's width. */
|
|
12
|
+
matchWidth?: boolean;
|
|
13
|
+
/** Close as soon as an item inside is activated. Default true. */
|
|
14
|
+
closeOnSelect?: boolean;
|
|
15
|
+
/** Accessible name for the menu. Defaults to the locale's `menu`. */
|
|
16
|
+
label?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
/** Classes for the wrapper around the trigger. */
|
|
19
|
+
triggerClass?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const Dropdown: import("svelte").Component<Props, {}, "open">;
|
|
22
|
+
type Dropdown = ReturnType<typeof Dropdown>;
|
|
23
|
+
export default Dropdown;
|