@noahbaumgartner/svelte-ui 0.0.1

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 (149) hide show
  1. package/README.md +65 -0
  2. package/dist/components/alert-dialog/AlertDialog.svelte +161 -0
  3. package/dist/components/alert-dialog/AlertDialog.svelte.d.ts +20 -0
  4. package/dist/components/avatar/Avatar.svelte +85 -0
  5. package/dist/components/avatar/Avatar.svelte.d.ts +12 -0
  6. package/dist/components/badge/Badge.svelte +49 -0
  7. package/dist/components/badge/Badge.svelte.d.ts +12 -0
  8. package/dist/components/breadcrumb/Breadcrumb.svelte +37 -0
  9. package/dist/components/breadcrumb/Breadcrumb.svelte.d.ts +9 -0
  10. package/dist/components/breadcrumb/BreadcrumbEllipsis.svelte +32 -0
  11. package/dist/components/breadcrumb/BreadcrumbEllipsis.svelte.d.ts +10 -0
  12. package/dist/components/breadcrumb/BreadcrumbItem.svelte +71 -0
  13. package/dist/components/breadcrumb/BreadcrumbItem.svelte.d.ts +12 -0
  14. package/dist/components/breadcrumb/BreadcrumbSeparator.svelte +18 -0
  15. package/dist/components/breadcrumb/BreadcrumbSeparator.svelte.d.ts +18 -0
  16. package/dist/components/button/Button.svelte +247 -0
  17. package/dist/components/button/Button.svelte.d.ts +24 -0
  18. package/dist/components/button-group/ButtonGroup.svelte +89 -0
  19. package/dist/components/button-group/ButtonGroup.svelte.d.ts +12 -0
  20. package/dist/components/calendar/Calendar.svelte +401 -0
  21. package/dist/components/calendar/Calendar.svelte.d.ts +17 -0
  22. package/dist/components/calendar/dates.d.ts +12 -0
  23. package/dist/components/calendar/dates.js +48 -0
  24. package/dist/components/card/Card.svelte +55 -0
  25. package/dist/components/card/Card.svelte.d.ts +10 -0
  26. package/dist/components/card/CardAction.svelte +24 -0
  27. package/dist/components/card/CardAction.svelte.d.ts +9 -0
  28. package/dist/components/card/CardContent.svelte +20 -0
  29. package/dist/components/card/CardContent.svelte.d.ts +8 -0
  30. package/dist/components/card/CardDescription.svelte +23 -0
  31. package/dist/components/card/CardDescription.svelte.d.ts +8 -0
  32. package/dist/components/card/CardFooter.svelte +28 -0
  33. package/dist/components/card/CardFooter.svelte.d.ts +9 -0
  34. package/dist/components/card/CardHeader.svelte +29 -0
  35. package/dist/components/card/CardHeader.svelte.d.ts +9 -0
  36. package/dist/components/card/CardTitle.svelte +23 -0
  37. package/dist/components/card/CardTitle.svelte.d.ts +8 -0
  38. package/dist/components/checkbox/Checkbox.svelte +93 -0
  39. package/dist/components/checkbox/Checkbox.svelte.d.ts +5 -0
  40. package/dist/components/color-picker/ColorPicker.svelte +233 -0
  41. package/dist/components/color-picker/ColorPicker.svelte.d.ts +10 -0
  42. package/dist/components/context-menu/ContextMenu.svelte +201 -0
  43. package/dist/components/context-menu/ContextMenu.svelte.d.ts +14 -0
  44. package/dist/components/context-menu/ContextMenuItem.svelte +105 -0
  45. package/dist/components/context-menu/ContextMenuItem.svelte.d.ts +14 -0
  46. package/dist/components/context-menu/context.d.ts +14 -0
  47. package/dist/components/context-menu/context.js +2 -0
  48. package/dist/components/dialog/Dialog.svelte +225 -0
  49. package/dist/components/dialog/Dialog.svelte.d.ts +22 -0
  50. package/dist/components/field/Field.svelte +78 -0
  51. package/dist/components/field/Field.svelte.d.ts +11 -0
  52. package/dist/components/field/FieldContent.svelte +25 -0
  53. package/dist/components/field/FieldContent.svelte.d.ts +9 -0
  54. package/dist/components/field/FieldDescription.svelte +34 -0
  55. package/dist/components/field/FieldDescription.svelte.d.ts +9 -0
  56. package/dist/components/field/FieldError.svelte +45 -0
  57. package/dist/components/field/FieldError.svelte.d.ts +11 -0
  58. package/dist/components/field/FieldGroup.svelte +30 -0
  59. package/dist/components/field/FieldGroup.svelte.d.ts +9 -0
  60. package/dist/components/field/FieldLegend.svelte +34 -0
  61. package/dist/components/field/FieldLegend.svelte.d.ts +10 -0
  62. package/dist/components/field/FieldSeparator.svelte +31 -0
  63. package/dist/components/field/FieldSeparator.svelte.d.ts +9 -0
  64. package/dist/components/field/FieldSet.svelte +33 -0
  65. package/dist/components/field/FieldSet.svelte.d.ts +9 -0
  66. package/dist/components/input/Input.svelte +459 -0
  67. package/dist/components/input/Input.svelte.d.ts +10 -0
  68. package/dist/components/input-group/InputGroup.svelte +88 -0
  69. package/dist/components/input-group/InputGroup.svelte.d.ts +9 -0
  70. package/dist/components/input-group/InputGroupAddon.svelte +88 -0
  71. package/dist/components/input-group/InputGroupAddon.svelte.d.ts +11 -0
  72. package/dist/components/kbd/Kbd.svelte +46 -0
  73. package/dist/components/kbd/Kbd.svelte.d.ts +8 -0
  74. package/dist/components/kbd/KbdGroup.svelte +27 -0
  75. package/dist/components/kbd/KbdGroup.svelte.d.ts +8 -0
  76. package/dist/components/label/Label.svelte +48 -0
  77. package/dist/components/label/Label.svelte.d.ts +9 -0
  78. package/dist/components/link/Link.svelte +40 -0
  79. package/dist/components/link/Link.svelte.d.ts +10 -0
  80. package/dist/components/navbar/Navbar.svelte +266 -0
  81. package/dist/components/navbar/Navbar.svelte.d.ts +26 -0
  82. package/dist/components/navigation-menu/NavigationMenu.svelte +95 -0
  83. package/dist/components/navigation-menu/NavigationMenu.svelte.d.ts +12 -0
  84. package/dist/components/navigation-menu/NavigationMenuItem.svelte +257 -0
  85. package/dist/components/navigation-menu/NavigationMenuItem.svelte.d.ts +15 -0
  86. package/dist/components/navigation-menu/NavigationMenuLink.svelte +91 -0
  87. package/dist/components/navigation-menu/NavigationMenuLink.svelte.d.ts +16 -0
  88. package/dist/components/navigation-menu/context.d.ts +15 -0
  89. package/dist/components/navigation-menu/context.js +2 -0
  90. package/dist/components/popover/Popover.svelte +295 -0
  91. package/dist/components/popover/Popover.svelte.d.ts +34 -0
  92. package/dist/components/radio-group/RadioGroup.svelte +86 -0
  93. package/dist/components/radio-group/RadioGroup.svelte.d.ts +16 -0
  94. package/dist/components/radio-group/RadioGroupItem.svelte +95 -0
  95. package/dist/components/radio-group/RadioGroupItem.svelte.d.ts +7 -0
  96. package/dist/components/radio-group/context.d.ts +9 -0
  97. package/dist/components/radio-group/context.js +2 -0
  98. package/dist/components/separator/Separator.svelte +41 -0
  99. package/dist/components/separator/Separator.svelte.d.ts +10 -0
  100. package/dist/components/sidebar/Sidebar.svelte +180 -0
  101. package/dist/components/sidebar/Sidebar.svelte.d.ts +14 -0
  102. package/dist/components/sidebar/SidebarContent.svelte +30 -0
  103. package/dist/components/sidebar/SidebarContent.svelte.d.ts +9 -0
  104. package/dist/components/sidebar/SidebarFooter.svelte +23 -0
  105. package/dist/components/sidebar/SidebarFooter.svelte.d.ts +8 -0
  106. package/dist/components/sidebar/SidebarGroup.svelte +55 -0
  107. package/dist/components/sidebar/SidebarGroup.svelte.d.ts +11 -0
  108. package/dist/components/sidebar/SidebarHeader.svelte +23 -0
  109. package/dist/components/sidebar/SidebarHeader.svelte.d.ts +8 -0
  110. package/dist/components/sidebar/SidebarInset.svelte +25 -0
  111. package/dist/components/sidebar/SidebarInset.svelte.d.ts +9 -0
  112. package/dist/components/sidebar/SidebarMenu.svelte +27 -0
  113. package/dist/components/sidebar/SidebarMenu.svelte.d.ts +9 -0
  114. package/dist/components/sidebar/SidebarMenuItem.svelte +221 -0
  115. package/dist/components/sidebar/SidebarMenuItem.svelte.d.ts +23 -0
  116. package/dist/components/sidebar/SidebarProvider.svelte +62 -0
  117. package/dist/components/sidebar/SidebarProvider.svelte.d.ts +11 -0
  118. package/dist/components/sidebar/SidebarSeparator.svelte +5 -0
  119. package/dist/components/sidebar/SidebarSeparator.svelte.d.ts +18 -0
  120. package/dist/components/sidebar/SidebarTrigger.svelte +24 -0
  121. package/dist/components/sidebar/SidebarTrigger.svelte.d.ts +7 -0
  122. package/dist/components/sidebar/context.d.ts +15 -0
  123. package/dist/components/sidebar/context.js +2 -0
  124. package/dist/components/skeleton/Skeleton.svelte +30 -0
  125. package/dist/components/skeleton/Skeleton.svelte.d.ts +5 -0
  126. package/dist/components/spinner/Spinner.svelte +72 -0
  127. package/dist/components/spinner/Spinner.svelte.d.ts +9 -0
  128. package/dist/components/switch/Switch.svelte +88 -0
  129. package/dist/components/switch/Switch.svelte.d.ts +5 -0
  130. package/dist/components/table/Table.svelte +276 -0
  131. package/dist/components/table/Table.svelte.d.ts +60 -0
  132. package/dist/components/textarea/Textarea.svelte +57 -0
  133. package/dist/components/textarea/Textarea.svelte.d.ts +4 -0
  134. package/dist/components/time-picker/TimePicker.svelte +162 -0
  135. package/dist/components/time-picker/TimePicker.svelte.d.ts +11 -0
  136. package/dist/components/toaster/Toaster.svelte +151 -0
  137. package/dist/components/toaster/Toaster.svelte.d.ts +7 -0
  138. package/dist/components/toaster/toast.svelte.d.ts +28 -0
  139. package/dist/components/toaster/toast.svelte.js +22 -0
  140. package/dist/components/toggle/Toggle.svelte +31 -0
  141. package/dist/components/toggle/Toggle.svelte.d.ts +8 -0
  142. package/dist/components/tooltip/Tooltip.svelte +224 -0
  143. package/dist/components/tooltip/Tooltip.svelte.d.ts +27 -0
  144. package/dist/icons.d.ts +2 -0
  145. package/dist/icons.js +4 -0
  146. package/dist/index.d.ts +73 -0
  147. package/dist/index.js +66 -0
  148. package/dist/styles/tokens.css +50 -0
  149. package/package.json +88 -0
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
6
+ /** CardTitle, CardDescription and optionally a CardAction. */
7
+ children: Snippet;
8
+ };
9
+
10
+ let { class: className, children, ...rest }: Props = $props();
11
+ </script>
12
+
13
+ <div {...rest} class={['card-header', className]}>
14
+ {@render children()}
15
+ </div>
16
+
17
+ <style>
18
+ .card-header {
19
+ display: grid;
20
+ grid-auto-rows: min-content;
21
+ align-items: start;
22
+ gap: 4px;
23
+ padding-inline: var(--card-spacing);
24
+ }
25
+
26
+ .card-header:has(> :global(.card-action)) {
27
+ grid-template-columns: 1fr auto;
28
+ }
29
+ </style>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
4
+ /** CardTitle, CardDescription and optionally a CardAction. */
5
+ children: Snippet;
6
+ };
7
+ declare const CardHeader: import("svelte").Component<Props, {}, "">;
8
+ type CardHeader = ReturnType<typeof CardHeader>;
9
+ export default CardHeader;
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ type Props = Omit<HTMLAttributes<HTMLHeadingElement>, 'children'> & {
6
+ children: Snippet;
7
+ };
8
+
9
+ let { class: className, children, ...rest }: Props = $props();
10
+ </script>
11
+
12
+ <h3 {...rest} class={['card-title', className]}>
13
+ {@render children()}
14
+ </h3>
15
+
16
+ <style>
17
+ .card-title {
18
+ margin: 0;
19
+ font-size: 15px;
20
+ font-weight: 600;
21
+ line-height: 1.4;
22
+ }
23
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Props = Omit<HTMLAttributes<HTMLHeadingElement>, 'children'> & {
4
+ children: Snippet;
5
+ };
6
+ declare const CardTitle: import("svelte").Component<Props, {}, "">;
7
+ type CardTitle = ReturnType<typeof CardTitle>;
8
+ export default CardTitle;
@@ -0,0 +1,93 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { Check, Minus } from '@lucide/svelte';
4
+
5
+ type Props = Omit<HTMLInputAttributes, 'type'>;
6
+
7
+ let {
8
+ checked = $bindable(false),
9
+ indeterminate = $bindable(false),
10
+ class: className,
11
+ ...rest
12
+ }: Props = $props();
13
+ </script>
14
+
15
+ <span class={['checkbox', className]}>
16
+ <input {...rest} type="checkbox" bind:checked bind:indeterminate />
17
+ {#if indeterminate}
18
+ <Minus class="checkbox-icon" aria-hidden="true" />
19
+ {:else}
20
+ <Check class="checkbox-icon" aria-hidden="true" />
21
+ {/if}
22
+ </span>
23
+
24
+ <style>
25
+ .checkbox {
26
+ position: relative;
27
+ display: inline-flex;
28
+ flex-shrink: 0;
29
+ width: 16px;
30
+ height: 16px;
31
+ }
32
+
33
+ input {
34
+ appearance: none;
35
+ box-sizing: border-box;
36
+ width: 100%;
37
+ height: 100%;
38
+ margin: 0;
39
+ background-color: transparent;
40
+ border: 1px solid var(--color-border-strong);
41
+ border-radius: 5px;
42
+ outline: none;
43
+ cursor: pointer;
44
+ transition:
45
+ background-color 200ms ease,
46
+ border-color 200ms ease;
47
+ }
48
+
49
+ input:checked,
50
+ input:indeterminate {
51
+ background-color: var(--color-accent);
52
+ border-color: var(--color-accent);
53
+ }
54
+
55
+ input:focus-visible {
56
+ outline: 2px solid var(--color-accent);
57
+ outline-offset: 2px;
58
+ }
59
+
60
+ input[aria-invalid='true'] {
61
+ border-color: var(--color-destructive);
62
+ }
63
+
64
+ input:disabled {
65
+ cursor: not-allowed;
66
+ opacity: 0.5;
67
+ }
68
+
69
+ .checkbox :global(.checkbox-icon) {
70
+ position: absolute;
71
+ inset: 2px;
72
+ width: 12px;
73
+ height: 12px;
74
+ color: var(--color-accent-foreground);
75
+ stroke-width: 3;
76
+ pointer-events: none;
77
+ opacity: 0;
78
+ }
79
+
80
+ input:checked + :global(.checkbox-icon),
81
+ input:indeterminate + :global(.checkbox-icon) {
82
+ opacity: 1;
83
+ }
84
+
85
+ input:disabled + :global(.checkbox-icon) {
86
+ opacity: 0;
87
+ }
88
+
89
+ input:checked:disabled + :global(.checkbox-icon),
90
+ input:indeterminate:disabled + :global(.checkbox-icon) {
91
+ opacity: 0.5;
92
+ }
93
+ </style>
@@ -0,0 +1,5 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ type Props = Omit<HTMLInputAttributes, 'type'>;
3
+ declare const Checkbox: import("svelte").Component<Props, {}, "checked" | "indeterminate">;
4
+ type Checkbox = ReturnType<typeof Checkbox>;
5
+ export default Checkbox;
@@ -0,0 +1,233 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import Input from '../input/Input.svelte';
4
+
5
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> & {
6
+ /** Bindable, `#rrggbb` like `<input type="color">`. */
7
+ value?: string;
8
+ /** Called on every change while dragging or typing. */
9
+ onselect?: (value: string) => void;
10
+ };
11
+
12
+ let { value = $bindable('#000000'), onselect, class: className, ...rest }: Props = $props();
13
+
14
+ type Hsv = { h: number; s: number; v: number };
15
+
16
+ function hexToHsv(hex: string): Hsv {
17
+ const n = parseInt(hex.slice(1), 16);
18
+ const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) => c / 255);
19
+ const max = Math.max(r, g, b);
20
+ const d = max - Math.min(r, g, b);
21
+ let h = 0;
22
+ if (d) {
23
+ if (max === r) h = ((g - b) / d) % 6;
24
+ else if (max === g) h = (b - r) / d + 2;
25
+ else h = (r - g) / d + 4;
26
+ }
27
+ return { h: (h * 60 + 360) % 360, s: max ? d / max : 0, v: max };
28
+ }
29
+
30
+ function hsvToHex({ h, s, v }: Hsv) {
31
+ const f = (n: number) => {
32
+ const k = (n + h / 60) % 6;
33
+ return Math.round((v - v * s * Math.max(0, Math.min(k, 4 - k, 1))) * 255);
34
+ };
35
+ return '#' + [f(5), f(3), f(1)].map((c) => c.toString(16).padStart(2, '0')).join('');
36
+ }
37
+
38
+ const isHex = (text: string) => /^#[0-9a-f]{6}$/i.test(text);
39
+
40
+ /* Kept separately so hue survives when saturation or brightness hit 0 */
41
+ let hsv = $state(hexToHsv(isHex(value) ? value : '#000000'));
42
+ let emitted = value;
43
+ let hexText = $state(value);
44
+
45
+ $effect.pre(() => {
46
+ if (value !== emitted && isHex(value)) {
47
+ hsv = hexToHsv(value);
48
+ hexText = value;
49
+ emitted = value;
50
+ }
51
+ });
52
+
53
+ function emit(next: Hsv) {
54
+ hsv = next;
55
+ value = emitted = hsvToHex(next);
56
+ hexText = value;
57
+ onselect?.(value);
58
+ }
59
+
60
+ const clamp = (n: number) => Math.min(1, Math.max(0, n));
61
+
62
+ function drag(event: PointerEvent, update: (x: number, y: number) => void) {
63
+ const el = event.currentTarget as HTMLElement;
64
+ el.setPointerCapture(event.pointerId);
65
+ el.focus();
66
+ const move = (e: PointerEvent) => {
67
+ const rect = el.getBoundingClientRect();
68
+ update(
69
+ clamp((e.clientX - rect.left) / rect.width),
70
+ clamp((e.clientY - rect.top) / rect.height)
71
+ );
72
+ };
73
+ move(event);
74
+ el.onpointermove = move;
75
+ el.onpointerup = () => (el.onpointermove = el.onpointerup = null);
76
+ }
77
+
78
+ function areaKeydown(event: KeyboardEvent) {
79
+ const delta = event.shiftKey ? 0.1 : 0.01;
80
+ const change = {
81
+ ArrowLeft: { s: -delta, v: 0 },
82
+ ArrowRight: { s: delta, v: 0 },
83
+ ArrowUp: { s: 0, v: delta },
84
+ ArrowDown: { s: 0, v: -delta }
85
+ }[event.key];
86
+ if (!change) return;
87
+ event.preventDefault();
88
+ emit({ h: hsv.h, s: clamp(hsv.s + change.s), v: clamp(hsv.v + change.v) });
89
+ }
90
+
91
+ function hueKeydown(event: KeyboardEvent) {
92
+ const delta = event.shiftKey ? 10 : 1;
93
+ const change = { ArrowLeft: -delta, ArrowDown: -delta, ArrowRight: delta, ArrowUp: delta }[
94
+ event.key
95
+ ];
96
+ if (change === undefined) return;
97
+ event.preventDefault();
98
+ emit({ ...hsv, h: Math.min(360, Math.max(0, hsv.h + change)) });
99
+ }
100
+
101
+ function hexInput(event: Event) {
102
+ const text = (event.currentTarget as HTMLInputElement).value.trim();
103
+ const hex = text.startsWith('#') ? text : `#${text}`;
104
+ if (isHex(hex)) emit(hexToHsv(hex.toLowerCase()));
105
+ }
106
+ </script>
107
+
108
+ <div {...rest} class={['color-picker', className]}>
109
+ <div
110
+ class="color-picker-area"
111
+ role="slider"
112
+ tabindex="0"
113
+ aria-label="Saturation and brightness"
114
+ aria-valuenow={Math.round(hsv.s * 100)}
115
+ aria-valuetext="Saturation {Math.round(hsv.s * 100)}%, brightness {Math.round(hsv.v * 100)}%"
116
+ style:--hue={hsv.h}
117
+ onpointerdown={(event) => drag(event, (x, y) => emit({ h: hsv.h, s: x, v: 1 - y }))}
118
+ onkeydown={areaKeydown}
119
+ >
120
+ <span
121
+ class="color-picker-thumb"
122
+ style:left="{hsv.s * 100}%"
123
+ style:top="{(1 - hsv.v) * 100}%"
124
+ style:background-color={value}
125
+ ></span>
126
+ </div>
127
+
128
+ <div
129
+ class="color-picker-hue"
130
+ role="slider"
131
+ tabindex="0"
132
+ aria-label="Hue"
133
+ aria-valuemin={0}
134
+ aria-valuemax={360}
135
+ aria-valuenow={Math.round(hsv.h)}
136
+ onpointerdown={(event) => drag(event, (x) => emit({ ...hsv, h: x * 360 }))}
137
+ onkeydown={hueKeydown}
138
+ >
139
+ <span
140
+ class="color-picker-thumb"
141
+ style:left="{(hsv.h / 360) * 100}%"
142
+ style:background-color="hsl({hsv.h} 100% 50%)"
143
+ ></span>
144
+ </div>
145
+
146
+ <div class="color-picker-footer">
147
+ <span class="color-picker-swatch" style:background-color={value}></span>
148
+ <Input value={hexText} aria-label="Hex color" spellcheck="false" oninput={hexInput} />
149
+ </div>
150
+ </div>
151
+
152
+ <style>
153
+ .color-picker {
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: 12px;
157
+ width: 224px;
158
+ user-select: none;
159
+ }
160
+
161
+ .color-picker-area {
162
+ position: relative;
163
+ height: 150px;
164
+ border-radius: 8px;
165
+ background:
166
+ linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent),
167
+ hsl(var(--hue) 100% 50%);
168
+ outline: none;
169
+ cursor: crosshair;
170
+ touch-action: none;
171
+ }
172
+
173
+ .color-picker-hue {
174
+ position: relative;
175
+ height: 12px;
176
+ margin: 0 6px;
177
+ border-radius: 999px;
178
+ background: linear-gradient(
179
+ to right,
180
+ #f00 0%,
181
+ #ff0 17%,
182
+ #0f0 33%,
183
+ #0ff 50%,
184
+ #00f 67%,
185
+ #f0f 83%,
186
+ #f00 100%
187
+ );
188
+ outline: none;
189
+ cursor: pointer;
190
+ touch-action: none;
191
+ }
192
+
193
+ /* Let the hue thumb reach both ends of the rounded track */
194
+ .color-picker-hue::before {
195
+ content: '';
196
+ position: absolute;
197
+ inset: 0 -6px;
198
+ }
199
+
200
+ .color-picker-thumb {
201
+ position: absolute;
202
+ top: 50%;
203
+ width: 14px;
204
+ height: 14px;
205
+ box-sizing: border-box;
206
+ border: 2px solid #fff;
207
+ border-radius: 999px;
208
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
209
+ transform: translate(-50%, -50%);
210
+ pointer-events: none;
211
+ }
212
+
213
+ .color-picker-area:focus-visible .color-picker-thumb,
214
+ .color-picker-hue:focus-visible .color-picker-thumb {
215
+ outline: 2px solid var(--color-accent);
216
+ outline-offset: 2px;
217
+ }
218
+
219
+ .color-picker-footer {
220
+ display: flex;
221
+ align-items: center;
222
+ gap: 8px;
223
+ }
224
+
225
+ .color-picker-swatch {
226
+ flex-shrink: 0;
227
+ width: 32px;
228
+ height: 32px;
229
+ box-sizing: border-box;
230
+ border: 1px solid var(--color-border);
231
+ border-radius: 10px;
232
+ }
233
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> & {
3
+ /** Bindable, `#rrggbb` like `<input type="color">`. */
4
+ value?: string;
5
+ /** Called on every change while dragging or typing. */
6
+ onselect?: (value: string) => void;
7
+ };
8
+ declare const ColorPicker: import("svelte").Component<Props, {}, "value">;
9
+ type ColorPicker = ReturnType<typeof ColorPicker>;
10
+ export default ColorPicker;
@@ -0,0 +1,201 @@
1
+ <script lang="ts">
2
+ import { tick, type Snippet } from 'svelte';
3
+ import { scale } from 'svelte/transition';
4
+ import { setMenuContext, type ContextMenuTriggerProps } from './context.js';
5
+
6
+ type Props = {
7
+ /** Whether the menu is open. Bindable. */
8
+ open?: boolean;
9
+ /** Horizontal alignment of the menu relative to the trigger. */
10
+ align?: 'start' | 'center' | 'end';
11
+ /** Renders the trigger. Spread the given props onto it, e.g. `<Button {...props} />`. */
12
+ trigger: Snippet<[ContextMenuTriggerProps]>;
13
+ children: Snippet;
14
+ };
15
+
16
+ let { open = $bindable(false), align = 'start', trigger, children }: Props = $props();
17
+
18
+ const id = $props.id();
19
+ let side: 'top' | 'bottom' = $state('bottom');
20
+ let wrapperEl: HTMLDivElement;
21
+ let contentEl: HTMLDivElement | undefined = $state();
22
+
23
+ function items() {
24
+ return Array.from(
25
+ contentEl?.querySelectorAll<HTMLElement>('[role^="menuitem"]:not(:disabled)') ?? []
26
+ );
27
+ }
28
+
29
+ async function show(focus: 'first' | 'last') {
30
+ open = true;
31
+ await tick();
32
+ const list = items();
33
+ const selected = list.find((item) => item.getAttribute('aria-checked') === 'true');
34
+ (selected ?? (focus === 'first' ? list[0] : list.at(-1)))?.focus();
35
+ }
36
+
37
+ function close({ restoreFocus = false } = {}) {
38
+ open = false;
39
+ if (restoreFocus) wrapperEl.querySelector<HTMLElement>('[aria-haspopup="menu"]')?.focus();
40
+ }
41
+
42
+ setMenuContext({ close: () => close({ restoreFocus: true }) });
43
+
44
+ const triggerProps: ContextMenuTriggerProps = $derived({
45
+ onclick: () => (open ? close() : show('first')),
46
+ onkeydown: (event: KeyboardEvent) => {
47
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
48
+ event.preventDefault();
49
+ show(event.key === 'ArrowDown' ? 'first' : 'last');
50
+ }
51
+ },
52
+ 'aria-haspopup': 'menu',
53
+ 'aria-expanded': open,
54
+ 'aria-controls': `${id}-menu`
55
+ });
56
+
57
+ function handleMenuKeydown(event: KeyboardEvent) {
58
+ const list = items();
59
+ const index = list.indexOf(document.activeElement as HTMLElement);
60
+ const last = list.length - 1;
61
+ const target = {
62
+ ArrowDown: index === last ? 0 : index + 1,
63
+ ArrowUp: index <= 0 ? last : index - 1,
64
+ Home: 0,
65
+ End: last
66
+ }[event.key];
67
+
68
+ if (target !== undefined) {
69
+ event.preventDefault();
70
+ list[target]?.focus();
71
+ } else if (event.key === 'Tab') {
72
+ close();
73
+ }
74
+ }
75
+
76
+ $effect(() => {
77
+ if (!open) return;
78
+
79
+ tick().then(() => {
80
+ if (!contentEl) return;
81
+ const triggerRect = wrapperEl.getBoundingClientRect();
82
+ const contentHeight = contentEl.offsetHeight;
83
+ const spaceBelow = window.innerHeight - triggerRect.bottom;
84
+ side =
85
+ spaceBelow < contentHeight + 8 && triggerRect.top > contentHeight + 8 ? 'top' : 'bottom';
86
+ });
87
+
88
+ function handlePointerDown(event: PointerEvent) {
89
+ if (!wrapperEl.contains(event.target as Node)) close();
90
+ }
91
+
92
+ function handleKeydown(event: KeyboardEvent) {
93
+ if (event.key === 'Escape') close({ restoreFocus: true });
94
+ }
95
+
96
+ function handleScroll() {
97
+ close();
98
+ }
99
+
100
+ window.addEventListener('pointerdown', handlePointerDown);
101
+ window.addEventListener('keydown', handleKeydown);
102
+ window.addEventListener('scroll', handleScroll, true);
103
+
104
+ return () => {
105
+ window.removeEventListener('pointerdown', handlePointerDown);
106
+ window.removeEventListener('keydown', handleKeydown);
107
+ window.removeEventListener('scroll', handleScroll, true);
108
+ };
109
+ });
110
+ </script>
111
+
112
+ <div class="context-menu" bind:this={wrapperEl}>
113
+ {@render trigger(triggerProps)}
114
+
115
+ {#if open}
116
+ <div
117
+ id="{id}-menu"
118
+ role="menu"
119
+ tabindex="-1"
120
+ class={[
121
+ 'context-menu-content',
122
+ `context-menu-content--${side}`,
123
+ `context-menu-content--${align}`
124
+ ]}
125
+ bind:this={contentEl}
126
+ onkeydown={handleMenuKeydown}
127
+ transition:scale={{ duration: 140, start: 0.95 }}
128
+ >
129
+ {@render children()}
130
+ </div>
131
+ {/if}
132
+ </div>
133
+
134
+ <style>
135
+ .context-menu {
136
+ position: relative;
137
+ display: inline-flex;
138
+ height: fit-content;
139
+ }
140
+
141
+ .context-menu-content {
142
+ position: absolute;
143
+ z-index: 60;
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 2px;
147
+ min-width: 160px;
148
+ padding: 4px;
149
+ background-color: var(--color-bg);
150
+ border: 1px solid var(--color-border);
151
+ border-radius: 10px;
152
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
153
+ outline: none;
154
+ user-select: none;
155
+ }
156
+
157
+ .context-menu-content--bottom {
158
+ top: calc(100% + 8px);
159
+ }
160
+
161
+ .context-menu-content--top {
162
+ bottom: calc(100% + 8px);
163
+ }
164
+
165
+ .context-menu-content--start {
166
+ left: 0;
167
+ }
168
+
169
+ .context-menu-content--center {
170
+ left: 50%;
171
+ transform: translateX(-50%);
172
+ }
173
+
174
+ .context-menu-content--end {
175
+ right: 0;
176
+ }
177
+
178
+ .context-menu-content--bottom.context-menu-content--start {
179
+ transform-origin: top left;
180
+ }
181
+
182
+ .context-menu-content--bottom.context-menu-content--center {
183
+ transform-origin: top center;
184
+ }
185
+
186
+ .context-menu-content--bottom.context-menu-content--end {
187
+ transform-origin: top right;
188
+ }
189
+
190
+ .context-menu-content--top.context-menu-content--start {
191
+ transform-origin: bottom left;
192
+ }
193
+
194
+ .context-menu-content--top.context-menu-content--center {
195
+ transform-origin: bottom center;
196
+ }
197
+
198
+ .context-menu-content--top.context-menu-content--end {
199
+ transform-origin: bottom right;
200
+ }
201
+ </style>
@@ -0,0 +1,14 @@
1
+ import { type Snippet } from 'svelte';
2
+ import { type ContextMenuTriggerProps } from './context.js';
3
+ type Props = {
4
+ /** Whether the menu is open. Bindable. */
5
+ open?: boolean;
6
+ /** Horizontal alignment of the menu relative to the trigger. */
7
+ align?: 'start' | 'center' | 'end';
8
+ /** Renders the trigger. Spread the given props onto it, e.g. `<Button {...props} />`. */
9
+ trigger: Snippet<[ContextMenuTriggerProps]>;
10
+ children: Snippet;
11
+ };
12
+ declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
13
+ type ContextMenu = ReturnType<typeof ContextMenu>;
14
+ export default ContextMenu;