@nqmcreative/ui 0.1.0

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