@noahbaumgartner/svelte-ui 0.0.4 → 0.0.6

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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -13
  3. package/dist/components/accordion/Accordion.svelte +44 -0
  4. package/dist/components/accordion/Accordion.svelte.d.ts +13 -0
  5. package/dist/components/accordion/AccordionItem.svelte +145 -0
  6. package/dist/components/accordion/AccordionItem.svelte.d.ts +17 -0
  7. package/dist/components/accordion/context.d.ts +5 -0
  8. package/dist/components/accordion/context.js +2 -0
  9. package/dist/components/alert-dialog/AlertDialog.svelte +108 -76
  10. package/dist/components/alert-dialog/AlertDialog.svelte.d.ts +5 -2
  11. package/dist/components/avatar/Avatar.svelte +71 -49
  12. package/dist/components/avatar/Avatar.svelte.d.ts +5 -2
  13. package/dist/components/avatar-group/AvatarGroup.svelte +103 -0
  14. package/dist/components/avatar-group/AvatarGroup.svelte.d.ts +19 -0
  15. package/dist/components/badge/Badge.svelte +50 -28
  16. package/dist/components/badge/Badge.svelte.d.ts +6 -4
  17. package/dist/components/breadcrumb/Breadcrumb.svelte +21 -17
  18. package/dist/components/breadcrumb/Breadcrumb.svelte.d.ts +3 -1
  19. package/dist/components/breadcrumb/BreadcrumbEllipsis.svelte +17 -6
  20. package/dist/components/breadcrumb/BreadcrumbEllipsis.svelte.d.ts +5 -2
  21. package/dist/components/breadcrumb/BreadcrumbItem.svelte +75 -36
  22. package/dist/components/breadcrumb/BreadcrumbItem.svelte.d.ts +8 -5
  23. package/dist/components/breadcrumb/BreadcrumbSeparator.svelte +32 -9
  24. package/dist/components/breadcrumb/BreadcrumbSeparator.svelte.d.ts +10 -17
  25. package/dist/components/button/Button.svelte +206 -168
  26. package/dist/components/button/Button.svelte.d.ts +9 -5
  27. package/dist/components/button-group/ButtonGroup.svelte +52 -55
  28. package/dist/components/button-group/ButtonGroup.svelte.d.ts +3 -1
  29. package/dist/components/calendar/Calendar.svelte +232 -144
  30. package/dist/components/calendar/Calendar.svelte.d.ts +19 -2
  31. package/dist/components/calendar/dates.d.ts +4 -1
  32. package/dist/components/calendar/dates.js +8 -2
  33. package/dist/components/card/Card.svelte +48 -32
  34. package/dist/components/card/Card.svelte.d.ts +3 -1
  35. package/dist/components/card/CardAction.svelte +11 -7
  36. package/dist/components/card/CardAction.svelte.d.ts +3 -1
  37. package/dist/components/card/CardContent.svelte +8 -4
  38. package/dist/components/card/CardContent.svelte.d.ts +3 -1
  39. package/dist/components/card/CardDescription.svelte +11 -7
  40. package/dist/components/card/CardDescription.svelte.d.ts +3 -1
  41. package/dist/components/card/CardFooter.svelte +15 -11
  42. package/dist/components/card/CardFooter.svelte.d.ts +3 -1
  43. package/dist/components/card/CardHeader.svelte +15 -11
  44. package/dist/components/card/CardHeader.svelte.d.ts +3 -1
  45. package/dist/components/card/CardTitle.svelte +11 -7
  46. package/dist/components/card/CardTitle.svelte.d.ts +3 -1
  47. package/dist/components/checkbox/Checkbox.svelte +152 -61
  48. package/dist/components/checkbox/Checkbox.svelte.d.ts +5 -2
  49. package/dist/components/color-picker/ColorPicker.svelte +131 -78
  50. package/dist/components/color-picker/ColorPicker.svelte.d.ts +10 -1
  51. package/dist/components/context-menu/ContextMenu.svelte +78 -61
  52. package/dist/components/context-menu/ContextMenu.svelte.d.ts +5 -2
  53. package/dist/components/context-menu/ContextMenuInfo.svelte +91 -0
  54. package/dist/components/context-menu/ContextMenuInfo.svelte.d.ts +19 -0
  55. package/dist/components/context-menu/ContextMenuItem.svelte +102 -57
  56. package/dist/components/context-menu/ContextMenuItem.svelte.d.ts +10 -5
  57. package/dist/components/context-menu/ContextMenuLabel.svelte +28 -0
  58. package/dist/components/context-menu/ContextMenuLabel.svelte.d.ts +10 -0
  59. package/dist/components/context-menu/ContextMenuSeparator.svelte +29 -0
  60. package/dist/components/context-menu/ContextMenuSeparator.svelte.d.ts +8 -0
  61. package/dist/components/dialog/Dialog.svelte +149 -108
  62. package/dist/components/dialog/Dialog.svelte.d.ts +7 -2
  63. package/dist/components/drawer/Drawer.svelte +419 -0
  64. package/dist/components/drawer/Drawer.svelte.d.ts +28 -0
  65. package/dist/components/empty-state/EmptyState.svelte +106 -0
  66. package/dist/components/empty-state/EmptyState.svelte.d.ts +19 -0
  67. package/dist/components/entry/Entry.svelte +94 -0
  68. package/dist/components/entry/Entry.svelte.d.ts +16 -0
  69. package/dist/components/field/Field.svelte +66 -45
  70. package/dist/components/field/Field.svelte.d.ts +2 -1
  71. package/dist/components/field/FieldContent.svelte +11 -8
  72. package/dist/components/field/FieldContent.svelte.d.ts +2 -1
  73. package/dist/components/field/FieldDescription.svelte +18 -15
  74. package/dist/components/field/FieldDescription.svelte.d.ts +2 -1
  75. package/dist/components/field/FieldError.svelte +22 -13
  76. package/dist/components/field/FieldError.svelte.d.ts +2 -1
  77. package/dist/components/field/FieldGroup.svelte +15 -12
  78. package/dist/components/field/FieldGroup.svelte.d.ts +2 -1
  79. package/dist/components/field/FieldLegend.svelte +23 -14
  80. package/dist/components/field/FieldLegend.svelte.d.ts +2 -1
  81. package/dist/components/field/FieldSeparator.svelte +12 -9
  82. package/dist/components/field/FieldSeparator.svelte.d.ts +2 -1
  83. package/dist/components/field/FieldSet.svelte +18 -15
  84. package/dist/components/field/FieldSet.svelte.d.ts +2 -1
  85. package/dist/components/footer/Footer.svelte +105 -0
  86. package/dist/components/footer/Footer.svelte.d.ts +14 -0
  87. package/dist/components/footer/FooterColumn.svelte +37 -0
  88. package/dist/components/footer/FooterColumn.svelte.d.ts +11 -0
  89. package/dist/components/image/Image.svelte +61 -0
  90. package/dist/components/image/Image.svelte.d.ts +18 -0
  91. package/dist/components/input/Input.svelte +307 -208
  92. package/dist/components/input/Input.svelte.d.ts +12 -1
  93. package/dist/components/input-group/InputGroup.svelte +101 -61
  94. package/dist/components/input-group/InputGroup.svelte.d.ts +2 -1
  95. package/dist/components/input-group/InputGroupAddon.svelte +58 -49
  96. package/dist/components/kbd/Kbd.svelte +39 -30
  97. package/dist/components/kbd/Kbd.svelte.d.ts +2 -1
  98. package/dist/components/kbd/KbdGroup.svelte +15 -12
  99. package/dist/components/kbd/KbdGroup.svelte.d.ts +2 -1
  100. package/dist/components/label/Label.svelte +47 -28
  101. package/dist/components/label/Label.svelte.d.ts +2 -1
  102. package/dist/components/link/Link.svelte +71 -17
  103. package/dist/components/link/Link.svelte.d.ts +2 -1
  104. package/dist/components/mesh-gradient/MeshGradient.svelte +97 -0
  105. package/dist/components/mesh-gradient/MeshGradient.svelte.d.ts +11 -0
  106. package/dist/components/navbar/Navbar.svelte +137 -99
  107. package/dist/components/navbar/Navbar.svelte.d.ts +4 -2
  108. package/dist/components/navigation-menu/NavigationMenu.svelte +37 -20
  109. package/dist/components/navigation-menu/NavigationMenu.svelte.d.ts +4 -2
  110. package/dist/components/navigation-menu/NavigationMenuItem.svelte +131 -89
  111. package/dist/components/navigation-menu/NavigationMenuItem.svelte.d.ts +4 -2
  112. package/dist/components/navigation-menu/NavigationMenuLink.svelte +90 -51
  113. package/dist/components/navigation-menu/NavigationMenuLink.svelte.d.ts +7 -5
  114. package/dist/components/number-stepper/NumberStepper.svelte +101 -0
  115. package/dist/components/number-stepper/NumberStepper.svelte.d.ts +21 -0
  116. package/dist/components/popover/Popover.svelte +85 -65
  117. package/dist/components/popover/Popover.svelte.d.ts +5 -2
  118. package/dist/components/preview-card/PreviewCard.svelte +184 -0
  119. package/dist/components/preview-card/PreviewCard.svelte.d.ts +23 -0
  120. package/dist/components/progress/Progress.svelte +111 -0
  121. package/dist/components/progress/Progress.svelte.d.ts +13 -0
  122. package/dist/components/radio-group/RadioGroup.svelte +24 -19
  123. package/dist/components/radio-group/RadioGroup.svelte.d.ts +2 -1
  124. package/dist/components/radio-group/RadioGroupItem.svelte +101 -54
  125. package/dist/components/radio-group/RadioGroupItem.svelte.d.ts +2 -1
  126. package/dist/components/section/Section.svelte +114 -0
  127. package/dist/components/section/Section.svelte.d.ts +22 -0
  128. package/dist/components/select/Select.svelte +474 -0
  129. package/dist/components/select/Select.svelte.d.ts +48 -0
  130. package/dist/components/separator/Separator.svelte +21 -10
  131. package/dist/components/separator/Separator.svelte.d.ts +2 -1
  132. package/dist/components/sidebar/Sidebar.svelte +158 -87
  133. package/dist/components/sidebar/Sidebar.svelte.d.ts +6 -1
  134. package/dist/components/sidebar/SidebarContent.svelte +15 -12
  135. package/dist/components/sidebar/SidebarContent.svelte.d.ts +2 -1
  136. package/dist/components/sidebar/SidebarFooter.svelte +10 -7
  137. package/dist/components/sidebar/SidebarFooter.svelte.d.ts +2 -1
  138. package/dist/components/sidebar/SidebarGroup.svelte +35 -25
  139. package/dist/components/sidebar/SidebarGroup.svelte.d.ts +2 -1
  140. package/dist/components/sidebar/SidebarHeader.svelte +37 -8
  141. package/dist/components/sidebar/SidebarHeader.svelte.d.ts +6 -1
  142. package/dist/components/sidebar/SidebarInset.svelte +11 -8
  143. package/dist/components/sidebar/SidebarInset.svelte.d.ts +2 -1
  144. package/dist/components/sidebar/SidebarMenu.svelte +13 -10
  145. package/dist/components/sidebar/SidebarMenu.svelte.d.ts +2 -1
  146. package/dist/components/sidebar/SidebarMenuItem.svelte +195 -114
  147. package/dist/components/sidebar/SidebarMenuItem.svelte.d.ts +11 -5
  148. package/dist/components/sidebar/SidebarProvider.svelte +18 -9
  149. package/dist/components/sidebar/SidebarProvider.svelte.d.ts +2 -1
  150. package/dist/components/sidebar/SidebarSeparator.svelte +6 -1
  151. package/dist/components/sidebar/SidebarSeparator.svelte.d.ts +6 -17
  152. package/dist/components/sidebar/SidebarTrigger.svelte +17 -5
  153. package/dist/components/sidebar/SidebarTrigger.svelte.d.ts +4 -2
  154. package/dist/components/skeleton/Skeleton.svelte +26 -16
  155. package/dist/components/skeleton/Skeleton.svelte.d.ts +4 -2
  156. package/dist/components/spinner/Spinner.svelte +71 -46
  157. package/dist/components/spinner/Spinner.svelte.d.ts +2 -1
  158. package/dist/components/switch/Switch.svelte +126 -65
  159. package/dist/components/switch/Switch.svelte.d.ts +4 -2
  160. package/dist/components/table/Table.svelte +135 -76
  161. package/dist/components/table/Table.svelte.d.ts +12 -2
  162. package/dist/components/tabs/Tabs.svelte +337 -0
  163. package/dist/components/tabs/Tabs.svelte.d.ts +50 -0
  164. package/dist/components/textarea/Textarea.svelte +76 -42
  165. package/dist/components/textarea/Textarea.svelte.d.ts +4 -1
  166. package/dist/components/time-picker/TimePicker.svelte +214 -85
  167. package/dist/components/time-picker/TimePicker.svelte.d.ts +18 -1
  168. package/dist/components/toaster/Toaster.svelte +132 -70
  169. package/dist/components/toaster/Toaster.svelte.d.ts +18 -2
  170. package/dist/components/toaster/toast.svelte.d.ts +3 -3
  171. package/dist/components/toggle/Toggle.svelte +25 -4
  172. package/dist/components/toggle/Toggle.svelte.d.ts +1 -1
  173. package/dist/components/tooltip/Tooltip.svelte +87 -58
  174. package/dist/components/tooltip/Tooltip.svelte.d.ts +4 -2
  175. package/dist/icon.d.ts +12 -0
  176. package/dist/icon.js +1 -0
  177. package/dist/index.d.ts +21 -0
  178. package/dist/index.js +19 -0
  179. package/dist/internal/icons/ArrowDown.svelte +14 -0
  180. package/dist/internal/icons/ArrowDown.svelte.d.ts +4 -0
  181. package/dist/internal/icons/ArrowUp.svelte +14 -0
  182. package/dist/internal/icons/ArrowUp.svelte.d.ts +4 -0
  183. package/dist/internal/icons/Calendar.svelte +16 -0
  184. package/dist/internal/icons/Calendar.svelte.d.ts +4 -0
  185. package/dist/internal/icons/Check.svelte +8 -0
  186. package/dist/internal/icons/Check.svelte.d.ts +4 -0
  187. package/dist/internal/icons/ChevronDown.svelte +8 -0
  188. package/dist/internal/icons/ChevronDown.svelte.d.ts +4 -0
  189. package/dist/internal/icons/ChevronLeft.svelte +8 -0
  190. package/dist/internal/icons/ChevronLeft.svelte.d.ts +4 -0
  191. package/dist/internal/icons/ChevronRight.svelte +8 -0
  192. package/dist/internal/icons/ChevronRight.svelte.d.ts +4 -0
  193. package/dist/internal/icons/ChevronUp.svelte +8 -0
  194. package/dist/internal/icons/ChevronUp.svelte.d.ts +4 -0
  195. package/dist/internal/icons/ChevronsUpDown.svelte +14 -0
  196. package/dist/internal/icons/ChevronsUpDown.svelte.d.ts +4 -0
  197. package/dist/internal/icons/Clock.svelte +14 -0
  198. package/dist/internal/icons/Clock.svelte.d.ts +4 -0
  199. package/dist/internal/icons/Ellipsis.svelte +15 -0
  200. package/dist/internal/icons/Ellipsis.svelte.d.ts +4 -0
  201. package/dist/internal/icons/Icon.svelte +33 -0
  202. package/dist/internal/icons/Icon.svelte.d.ts +8 -0
  203. package/dist/internal/icons/Minus.svelte +8 -0
  204. package/dist/internal/icons/Minus.svelte.d.ts +4 -0
  205. package/dist/internal/icons/PanelLeft.svelte +14 -0
  206. package/dist/internal/icons/PanelLeft.svelte.d.ts +4 -0
  207. package/dist/internal/icons/Plus.svelte +14 -0
  208. package/dist/internal/icons/Plus.svelte.d.ts +4 -0
  209. package/dist/internal/icons/User.svelte +14 -0
  210. package/dist/internal/icons/User.svelte.d.ts +4 -0
  211. package/dist/internal/icons/X.svelte +14 -0
  212. package/dist/internal/icons/X.svelte.d.ts +4 -0
  213. package/dist/styles/tokens.css +39 -41
  214. package/package.json +5 -10
  215. package/dist/icons.d.ts +0 -2
  216. package/dist/icons.js +0 -4
@@ -1,11 +1,13 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
- import type { LucideIcon } from '@lucide/svelte';
4
+ import type { IconComponent } from '../../icon.js';
5
5
  import Spinner from '../spinner/Spinner.svelte';
6
6
 
7
7
  type Props = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
8
8
  /** Renders an <a> instead of a <button>. External (http) links open in a new tab. */
9
+ /** The button or anchor element. Bindable. */
10
+ ref?: HTMLButtonElement | HTMLAnchorElement | null;
9
11
  href?: string;
10
12
  target?: string;
11
13
  rel?: string;
@@ -14,16 +16,19 @@
14
16
  /** Replaces the icon with a Spinner and blocks clicks. */
15
17
  loading?: boolean;
16
18
  /** `overlay` is a translucent, blurred, fully rounded button for use on top of images or media. */
17
- variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'overlay';
19
+ variant?: 'primary' | 'secondary' | 'ghost' | 'overlay';
18
20
  size?: 'sm' | 'md' | 'lg';
19
- /** Any Lucide icon (import from `svelte-ui/icons`). */
20
- icon?: LucideIcon;
21
+ /** Any icon component, e.g. from `@lucide/svelte`. */
22
+ icon?: IconComponent;
21
23
  /** Accessible name. Required when the button has no label (icon-only). */
22
24
  label?: string;
25
+ /** Replaces the icon, e.g. for a custom SVG or an Avatar. */
26
+ leading?: Snippet;
23
27
  children?: Snippet;
24
28
  };
25
29
 
26
30
  let {
31
+ ref = $bindable(null),
27
32
  href,
28
33
  target,
29
34
  rel,
@@ -34,6 +39,7 @@
34
39
  size = 'md',
35
40
  icon: Icon,
36
41
  label,
42
+ leading,
37
43
  class: className,
38
44
  children,
39
45
  ...rest
@@ -42,13 +48,15 @@
42
48
  let iconOnly = $derived(!children);
43
49
  let resolvedTarget = $derived(target ?? (href?.startsWith('http') ? '_blank' : undefined));
44
50
  let resolvedRel = $derived(rel ?? (resolvedTarget === '_blank' ? 'noopener' : undefined));
51
+ /* href comes from the consumer, who resolves it; this library has no routes */
52
+ let linkProps = $derived({ href, target: resolvedTarget, rel: resolvedRel });
45
53
  let classes = $derived([
46
54
  'button',
47
55
  `button--${variant}`,
48
56
  `button--${size}`,
49
57
  {
50
58
  'button--icon-only': iconOnly,
51
- 'button--no-icon': !Icon && !loading,
59
+ 'button--no-icon': !Icon && !leading && !loading,
52
60
  'button--loading': loading
53
61
  },
54
62
  className
@@ -58,19 +66,21 @@
58
66
  {#snippet content()}
59
67
  {#if loading}
60
68
  <Spinner class="button-spinner" aria-hidden="true" />
69
+ {:else if leading}
70
+ {@render leading()}
61
71
  {:else if Icon}<Icon class="button-icon" aria-hidden="true" />{/if}
62
72
  {#if children}<span class="button-label">{@render children()}</span>{/if}
63
73
  {/snippet}
64
74
 
65
75
  {#if href && !disabled && !loading}
66
76
  <!-- href comes from the consumer, who resolves it; this library has no routes -->
67
- <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
68
- <a {...rest} {href} target={resolvedTarget} rel={resolvedRel} aria-label={label} class={classes}>
77
+ <a {...rest} {...linkProps} bind:this={ref} aria-label={label} class={classes}>
69
78
  {@render content()}
70
79
  </a>
71
80
  {:else}
72
81
  <button
73
82
  {...rest}
83
+ bind:this={ref}
74
84
  {type}
75
85
  disabled={disabled || loading}
76
86
  aria-busy={loading || undefined}
@@ -82,166 +92,194 @@
82
92
  {/if}
83
93
 
84
94
  <style>
85
- .button {
86
- display: inline-flex;
87
- align-items: center;
88
- justify-content: center;
89
- gap: 6px;
90
- min-width: 0;
91
- box-sizing: border-box;
92
- border: none;
93
- border-radius: 10px;
94
- font-family: inherit;
95
- font-size: 13px;
96
- text-decoration: none;
97
- outline: none;
98
- cursor: pointer;
99
- user-select: none;
100
- transition:
101
- background-color 200ms ease,
102
- color 200ms ease,
103
- opacity 200ms ease;
104
- }
105
-
106
- .button:disabled {
107
- cursor: not-allowed;
108
- opacity: 0.7;
109
- }
110
-
111
- .button--loading:disabled {
112
- cursor: progress;
113
- opacity: 1;
114
- }
115
-
116
- .button-label {
117
- overflow: hidden;
118
- min-width: 0;
119
- white-space: nowrap;
120
- text-overflow: ellipsis;
121
- }
122
-
123
- .button:focus-visible {
124
- outline: 2px solid var(--color-accent);
125
- outline-offset: 2px;
126
- }
127
-
128
- .button :global(.button-icon) {
129
- width: 16px;
130
- height: 16px;
131
- flex-shrink: 0;
132
- }
133
-
134
- /* 2px smaller than the icon it replaces */
135
- .button :global(.button-spinner) {
136
- --spinner-size: 14px;
137
- }
138
-
139
- .button--sm :global(.button-spinner) {
140
- --spinner-size: 12px;
141
- }
142
-
143
- /* Sizes */
144
-
145
- .button--lg {
146
- height: 36px;
147
- padding: 0 20px 0 16px;
148
- }
149
-
150
- .button--md {
151
- height: 32px;
152
- padding: 0 15px 0 11px;
153
- }
154
-
155
- .button--sm {
156
- height: 28px;
157
- padding: 0 12px 0 9px;
158
- gap: 5px;
159
- border-radius: 8px;
160
- font-size: 12px;
161
- }
162
-
163
- .button--sm :global(.button-icon) {
164
- width: 14px;
165
- height: 14px;
166
- }
167
-
168
- .button--no-icon.button--sm {
169
- padding: 0 12px;
170
- }
171
-
172
- .button--no-icon.button--lg {
173
- padding: 0 20px;
174
- }
175
-
176
- .button--no-icon.button--md {
177
- padding: 0 16px;
178
- }
179
-
180
- /* Icon-only is square */
181
- .button--icon-only.button--lg {
182
- width: 36px;
183
- padding: 0;
184
- }
185
-
186
- .button--icon-only.button--md {
187
- width: 32px;
188
- padding: 0;
189
- }
190
-
191
- .button--icon-only.button--sm {
192
- width: 28px;
193
- padding: 0;
194
- }
195
-
196
- /* Variants */
197
-
198
- .button--primary {
199
- background-color: var(--color-accent);
200
- color: var(--color-accent-foreground);
201
- }
202
-
203
- .button--primary:hover:not(:disabled) {
204
- background-color: var(--color-accent-hover);
205
- }
206
-
207
- .button--secondary {
208
- background-color: var(--color-surface);
209
- color: var(--color-text);
210
- }
211
-
212
- .button--secondary:hover:not(:disabled) {
213
- background-color: var(--color-surface-hover);
214
- }
215
-
216
- .button--outline {
217
- border: 1px solid var(--color-border-strong);
218
- background-color: transparent;
219
- color: var(--color-text);
220
- }
221
-
222
- .button--outline:hover:not(:disabled) {
223
- background-color: var(--color-surface);
224
- }
225
-
226
- .button--ghost {
227
- background-color: transparent;
228
- color: var(--color-text);
229
- }
230
-
231
- .button--ghost:hover:not(:disabled) {
232
- background-color: var(--color-surface);
233
- }
234
-
235
- /* Pill with a label, circle when icon-only */
236
- .button--overlay {
237
- border-radius: 999px;
238
- background-color: color-mix(in srgb, var(--color-surface) 60%, transparent);
239
- backdrop-filter: blur(8px);
240
- -webkit-backdrop-filter: blur(8px);
241
- color: var(--color-text);
242
- }
243
-
244
- .button--overlay:hover:not(:disabled) {
245
- background-color: color-mix(in srgb, var(--color-surface) 75%, transparent);
95
+ @layer svelte-ui {
96
+ .button {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ gap: 6px;
101
+ min-width: 0;
102
+ box-sizing: border-box;
103
+ border: none;
104
+ border-radius: 10px;
105
+ font-family: inherit;
106
+ font-size: 13px;
107
+ text-decoration: none;
108
+ outline: none;
109
+ cursor: pointer;
110
+ user-select: none;
111
+ transition:
112
+ background-color 200ms ease,
113
+ color 200ms ease,
114
+ opacity 200ms ease;
115
+ }
116
+
117
+ .button:disabled {
118
+ cursor: not-allowed;
119
+ opacity: 0.7;
120
+ }
121
+
122
+ .button--loading:disabled {
123
+ cursor: progress;
124
+ opacity: 1;
125
+ }
126
+
127
+ .button-label {
128
+ overflow: hidden;
129
+ min-width: 0;
130
+ white-space: nowrap;
131
+ text-overflow: ellipsis;
132
+ }
133
+
134
+ .button:focus-visible {
135
+ outline: 2px solid var(--color-accent);
136
+ outline-offset: 2px;
137
+ }
138
+
139
+ .button :global(.button-icon) {
140
+ width: 16px;
141
+ height: 16px;
142
+ flex-shrink: 0;
143
+ }
144
+
145
+ /* 2px smaller than the icon it replaces */
146
+ .button :global(.button-spinner) {
147
+ --spinner-size: 14px;
148
+ }
149
+
150
+ .button--sm :global(.button-spinner) {
151
+ --spinner-size: 12px;
152
+ }
153
+
154
+ /* Sizes */
155
+
156
+ .button--lg {
157
+ height: 36px;
158
+ padding: 0 20px 0 16px;
159
+ }
160
+
161
+ .button--md {
162
+ height: 32px;
163
+ padding: 0 15px 0 11px;
164
+ }
165
+
166
+ .button--sm {
167
+ height: 28px;
168
+ padding: 0 12px 0 9px;
169
+ gap: 5px;
170
+ border-radius: 8px;
171
+ font-size: 12px;
172
+ }
173
+
174
+ .button--sm :global(.button-icon) {
175
+ width: 14px;
176
+ height: 14px;
177
+ }
178
+
179
+ .button--no-icon.button--sm {
180
+ padding: 0 12px;
181
+ }
182
+
183
+ .button--no-icon.button--lg {
184
+ padding: 0 20px;
185
+ }
186
+
187
+ .button--no-icon.button--md {
188
+ padding: 0 16px;
189
+ }
190
+
191
+ /* Icon-only is square */
192
+ .button--icon-only.button--lg {
193
+ width: 36px;
194
+ padding: 0;
195
+ }
196
+
197
+ .button--icon-only.button--md {
198
+ width: 32px;
199
+ padding: 0;
200
+ }
201
+
202
+ .button--icon-only.button--sm {
203
+ width: 28px;
204
+ padding: 0;
205
+ }
206
+
207
+ /* Variants */
208
+
209
+ .button--primary {
210
+ background-color: var(--color-accent);
211
+ color: var(--color-accent-foreground);
212
+ }
213
+
214
+ .button--primary:hover:not(:disabled) {
215
+ background-color: var(--color-accent-hover);
216
+ }
217
+
218
+ .button--secondary {
219
+ background-color: var(--color-surface);
220
+ color: var(--color-text);
221
+ }
222
+
223
+ .button--secondary:hover:not(:disabled) {
224
+ background-color: var(--color-surface-hover);
225
+ }
226
+
227
+ .button--ghost {
228
+ background-color: transparent;
229
+ color: var(--color-text);
230
+ }
231
+
232
+ .button--ghost:hover:not(:disabled) {
233
+ background-color: var(--color-surface);
234
+ }
235
+
236
+ /* Pill with a label, circle when icon-only */
237
+ .button--overlay {
238
+ border-radius: 999px;
239
+ background-color: color-mix(in srgb, var(--color-surface) 60%, transparent);
240
+ backdrop-filter: blur(8px);
241
+ -webkit-backdrop-filter: blur(8px);
242
+ color: var(--color-text);
243
+ }
244
+
245
+ .button--overlay:hover:not(:disabled) {
246
+ background-color: color-mix(in srgb, var(--color-surface) 75%, transparent);
247
+ }
248
+
249
+ @media (pointer: coarse) {
250
+ .button {
251
+ min-height: 44px;
252
+ }
253
+
254
+ .button--icon-only {
255
+ min-width: 44px;
256
+ }
257
+ }
258
+
259
+ @media (forced-colors: active) {
260
+ .button {
261
+ border: 1px solid ButtonText;
262
+ }
263
+
264
+ .button:focus-visible {
265
+ outline-color: Highlight;
266
+ }
267
+
268
+ .button--primary {
269
+ border-width: 2px;
270
+ }
271
+
272
+ .button:disabled:not(.button--loading) {
273
+ color: GrayText;
274
+ border-color: GrayText;
275
+ opacity: 1;
276
+ }
277
+ }
278
+
279
+ @media (prefers-reduced-motion: reduce) {
280
+ .button {
281
+ transition: none;
282
+ }
283
+ }
246
284
  }
247
285
  </style>
@@ -1,8 +1,10 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
- import type { LucideIcon } from '@lucide/svelte';
3
+ import type { IconComponent } from '../../icon.js';
4
4
  type Props = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
5
5
  /** Renders an <a> instead of a <button>. External (http) links open in a new tab. */
6
+ /** The button or anchor element. Bindable. */
7
+ ref?: HTMLButtonElement | HTMLAnchorElement | null;
6
8
  href?: string;
7
9
  target?: string;
8
10
  rel?: string;
@@ -11,14 +13,16 @@ type Props = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
11
13
  /** Replaces the icon with a Spinner and blocks clicks. */
12
14
  loading?: boolean;
13
15
  /** `overlay` is a translucent, blurred, fully rounded button for use on top of images or media. */
14
- variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'overlay';
16
+ variant?: 'primary' | 'secondary' | 'ghost' | 'overlay';
15
17
  size?: 'sm' | 'md' | 'lg';
16
- /** Any Lucide icon (import from `svelte-ui/icons`). */
17
- icon?: LucideIcon;
18
+ /** Any icon component, e.g. from `@lucide/svelte`. */
19
+ icon?: IconComponent;
18
20
  /** Accessible name. Required when the button has no label (icon-only). */
19
21
  label?: string;
22
+ /** Replaces the icon, e.g. for a custom SVG or an Avatar. */
23
+ leading?: Snippet;
20
24
  children?: Snippet;
21
25
  };
22
- declare const Button: import("svelte").Component<Props, {}, "">;
26
+ declare const Button: import("svelte").Component<Props, {}, "ref">;
23
27
  type Button = ReturnType<typeof Button>;
24
28
  export default Button;
@@ -3,6 +3,8 @@
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
 
5
5
  type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
6
+ /** The root element. Bindable. */
7
+ ref?: HTMLDivElement | null;
6
8
  orientation?: 'horizontal' | 'vertical';
7
9
  /** Accessible name for the group. */
8
10
  label?: string;
@@ -10,11 +12,19 @@
10
12
  children: Snippet;
11
13
  };
12
14
 
13
- let { orientation = 'horizontal', label, class: className, children, ...rest }: Props = $props();
15
+ let {
16
+ ref = $bindable(null),
17
+ orientation = 'horizontal',
18
+ label,
19
+ class: className,
20
+ children,
21
+ ...rest
22
+ }: Props = $props();
14
23
  </script>
15
24
 
16
25
  <div
17
26
  {...rest}
27
+ bind:this={ref}
18
28
  role="group"
19
29
  aria-label={label}
20
30
  class={['button-group', `button-group--${orientation}`, className]}
@@ -23,67 +33,54 @@
23
33
  </div>
24
34
 
25
35
  <style>
26
- .button-group {
27
- display: inline-flex;
28
- gap: 1px;
29
- width: fit-content;
30
- }
31
-
32
- .button-group--vertical {
33
- flex-direction: column;
34
- }
35
-
36
- /* Keep the focus ring above neighbouring buttons */
37
- .button-group > :global(*) {
38
- position: relative;
39
- }
40
-
41
- .button-group > :global(:is(:focus-visible, :has(:focus-visible))) {
42
- z-index: 1;
43
- }
36
+ @layer svelte-ui {
37
+ .button-group {
38
+ display: inline-flex;
39
+ gap: 1px;
40
+ width: fit-content;
41
+ }
44
42
 
45
- .button-group--vertical > :global(*),
46
- .button-group--vertical > :global(* > .button) {
47
- width: 100%;
48
- }
43
+ .button-group--vertical {
44
+ flex-direction: column;
45
+ }
49
46
 
50
- /* Outline buttons share their borders instead of sitting 1px apart */
51
- .button-group:has(> :global(.button--outline), > :global(* > .button--outline)) {
52
- gap: 0;
53
- }
47
+ /* Keep the focus ring above neighbouring buttons */
48
+ .button-group > :global(*) {
49
+ position: relative;
50
+ }
54
51
 
55
- .button-group--horizontal > :global(.button--outline:not(:first-child)),
56
- .button-group--horizontal > :global(:not(:first-child):has(> .button--outline)) {
57
- margin-inline-start: -1px;
58
- }
52
+ .button-group > :global(:is(:focus-visible, :has(:focus-visible))) {
53
+ z-index: 1;
54
+ }
59
55
 
60
- .button-group--vertical > :global(.button--outline:not(:first-child)),
61
- .button-group--vertical > :global(:not(:first-child):has(> .button--outline)) {
62
- margin-block-start: -1px;
63
- }
56
+ .button-group--vertical > :global(*),
57
+ .button-group--vertical > :global(* > .button) {
58
+ width: 100%;
59
+ }
64
60
 
65
- /* Square the inner corners; `* > .button` reaches a ContextMenu's trigger */
66
- .button-group--horizontal > :global(:not(:first-child)),
67
- .button-group--horizontal > :global(:not(:first-child) > .button) {
68
- border-start-start-radius: 0;
69
- border-end-start-radius: 0;
70
- }
61
+ /* Square the inner corners; `* > .button` reaches a ContextMenu's trigger */
62
+ .button-group--horizontal > :global(:not(:first-child)),
63
+ .button-group--horizontal > :global(:not(:first-child) > .button) {
64
+ border-start-start-radius: 0;
65
+ border-end-start-radius: 0;
66
+ }
71
67
 
72
- .button-group--horizontal > :global(:not(:last-child)),
73
- .button-group--horizontal > :global(:not(:last-child) > .button) {
74
- border-start-end-radius: 0;
75
- border-end-end-radius: 0;
76
- }
68
+ .button-group--horizontal > :global(:not(:last-child)),
69
+ .button-group--horizontal > :global(:not(:last-child) > .button) {
70
+ border-start-end-radius: 0;
71
+ border-end-end-radius: 0;
72
+ }
77
73
 
78
- .button-group--vertical > :global(:not(:first-child)),
79
- .button-group--vertical > :global(:not(:first-child) > .button) {
80
- border-start-start-radius: 0;
81
- border-start-end-radius: 0;
82
- }
74
+ .button-group--vertical > :global(:not(:first-child)),
75
+ .button-group--vertical > :global(:not(:first-child) > .button) {
76
+ border-start-start-radius: 0;
77
+ border-start-end-radius: 0;
78
+ }
83
79
 
84
- .button-group--vertical > :global(:not(:last-child)),
85
- .button-group--vertical > :global(:not(:last-child) > .button) {
86
- border-end-start-radius: 0;
87
- border-end-end-radius: 0;
80
+ .button-group--vertical > :global(:not(:last-child)),
81
+ .button-group--vertical > :global(:not(:last-child) > .button) {
82
+ border-end-start-radius: 0;
83
+ border-end-end-radius: 0;
84
+ }
88
85
  }
89
86
  </style>
@@ -1,12 +1,14 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
4
+ /** The root element. Bindable. */
5
+ ref?: HTMLDivElement | null;
4
6
  orientation?: 'horizontal' | 'vertical';
5
7
  /** Accessible name for the group. */
6
8
  label?: string;
7
9
  /** Buttons, or ContextMenus with a Button trigger (split button). */
8
10
  children: Snippet;
9
11
  };
10
- declare const ButtonGroup: import("svelte").Component<Props, {}, "">;
12
+ declare const ButtonGroup: import("svelte").Component<Props, {}, "ref">;
11
13
  type ButtonGroup = ReturnType<typeof ButtonGroup>;
12
14
  export default ButtonGroup;