sveltacular 1.0.33 → 1.0.36

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 (33) hide show
  1. package/dist/forms/button/button.svelte +111 -10
  2. package/dist/forms/button/button.svelte.d.ts +8 -2
  3. package/dist/forms/check-box/check-box-group.svelte +2 -2
  4. package/dist/forms/combo/new-or-existing-combo.svelte +3 -3
  5. package/dist/forms/combo/new-or-existing-combo.svelte.d.ts +2 -2
  6. package/dist/forms/form-field/form-field.svelte +1 -2
  7. package/dist/forms/form.svelte +1 -1
  8. package/dist/forms/index.d.ts +4 -5
  9. package/dist/forms/index.js +5 -6
  10. package/dist/forms/list-box/list-box.svelte +23 -14
  11. package/dist/forms/list-box/list-box.svelte.d.ts +3 -3
  12. package/dist/forms/reference-box/index.d.ts +1 -1
  13. package/dist/forms/reference-box/reference-box.d.ts +3 -2
  14. package/dist/forms/reference-box/reference-box.svelte +6 -11
  15. package/dist/forms/reference-box/reference-box.svelte.d.ts +4 -3
  16. package/dist/forms/tag-box/tag-box.svelte +0 -1
  17. package/dist/forms/text-box/text-box.svelte +1 -1
  18. package/dist/generic/empty/empty.svelte +137 -53
  19. package/dist/generic/empty/empty.svelte.d.ts +11 -4
  20. package/dist/generic/menu/menu.svelte +1 -1
  21. package/dist/generic/overlay.svelte +3 -1
  22. package/dist/generic/overlay.svelte.d.ts +1 -0
  23. package/dist/generic/section/section.svelte +1 -1
  24. package/dist/modals/alert.svelte +5 -5
  25. package/dist/modals/confirm.svelte +6 -6
  26. package/dist/modals/modal.svelte +1 -1
  27. package/dist/modals/prompt.svelte +7 -7
  28. package/dist/navigation/drawer/drawer.svelte +1 -1
  29. package/dist/tables/data-grid.svelte +1 -4
  30. package/dist/types/form.d.ts +17 -3
  31. package/dist/types/size.d.ts +26 -2
  32. package/dist/types/size.js +20 -2
  33. package/package.json +1 -1
@@ -5,19 +5,24 @@
5
5
  */
6
6
  import type { Snippet } from 'svelte';
7
7
  import { navigateTo } from '../../helpers/navigate-to.js';
8
- import type { ButtonVariant, FormFieldSizeOptions } from '../../types/form.js';
8
+ import type { ButtonVariant, ComponentWidth, FormFieldSizeOptions } from '../../types/form.js';
9
+ import type { IconType } from '../../icons/types.js';
9
10
  import Spinner from '../../generic/spinner/spinner.svelte';
11
+ import Icon from '../../icons/icon.svelte';
12
+ import type { ComponentSize } from '../../types';
10
13
 
11
14
  let {
12
15
  /** Optional href for navigation */
13
16
  href = undefined,
14
- /** Button size */
17
+ /** Button size (controls font size and padding) */
15
18
  size = 'md',
19
+ /** Button width */
20
+ width = 'md',
16
21
  /** Button variant/style */
17
22
  variant = 'secondary',
18
23
  /** HTML button type */
19
24
  type = 'button',
20
- /** Display as block (full width) */
25
+ /** Display as block (full width) - DEPRECATED: use width="full" instead */
21
26
  block = false,
22
27
  /** Allow flex growth */
23
28
  flex = false,
@@ -33,6 +38,11 @@
33
38
  collapse = false,
34
39
  /** Delay before re-enabling after click (prevents double-clicks) */
35
40
  repeatSubmitDelay = 500,
41
+ /** Icon to display */
42
+ icon = undefined,
43
+ /** Icon alignment */
44
+ iconAlign = 'left',
45
+ iconSize = 'default',
36
46
  /** Click handler */
37
47
  onClick = undefined,
38
48
  /** Button content */
@@ -40,6 +50,7 @@
40
50
  }: {
41
51
  href?: string | undefined;
42
52
  size?: FormFieldSizeOptions;
53
+ width?: ComponentWidth;
43
54
  variant?: ButtonVariant;
44
55
  type?: 'button' | 'submit' | 'reset';
45
56
  block?: boolean;
@@ -50,8 +61,11 @@
50
61
  noMargin?: boolean;
51
62
  collapse?: boolean;
52
63
  repeatSubmitDelay?: number | 'infinite';
64
+ icon?: IconType | undefined;
65
+ iconAlign?: 'left' | 'right' | 'above' | 'below';
66
+ iconSize?: 'default' | ComponentSize;
53
67
  onClick?: ((e?: Event) => void) | undefined;
54
- children: Snippet;
68
+ children?: Snippet;
55
69
  } = $props();
56
70
 
57
71
  let isDisabled = $derived(disabled || loading);
@@ -73,16 +87,19 @@
73
87
  navigateTo(href);
74
88
  }
75
89
  };
90
+
91
+ const _iconSize = $derived<ComponentSize>(iconSize === 'default' ? size : iconSize);
76
92
  </script>
77
93
 
78
94
  <button
79
95
  {type}
80
96
  onclick={handleClick}
81
- class="{size} {variant} {flex ? 'flex' : ''}"
97
+ class="{size} w-{width} {variant} {flex ? 'flex' : ''} icon-{iconAlign}"
82
98
  class:block
83
99
  class:noMargin
84
100
  class:collapse
85
101
  class:loading
102
+ class:has-icon={!!icon}
86
103
  disabled={isDisabled}
87
104
  aria-label={ariaLabel}
88
105
  aria-busy={loading}
@@ -93,12 +110,25 @@
93
110
  <Spinner {size} variant={variant === 'outline' ? 'secondary' : 'primary'} />
94
111
  </span>
95
112
  {/if}
96
- {@render children()}
113
+ {#if icon && (iconAlign === 'above' || iconAlign === 'left')}
114
+ <span class="icon-wrapper" aria-hidden="true">
115
+ <Icon type={icon} size={_iconSize} fill="currentColor" />
116
+ </span>
117
+ {/if}
118
+ {#if children}
119
+ <span class="button-content">
120
+ {@render children()}
121
+ </span>
122
+ {/if}
123
+ {#if icon && (iconAlign === 'below' || iconAlign === 'right')}
124
+ <span class="icon-wrapper" aria-hidden="true">
125
+ <Icon type={icon} size={_iconSize} fill="currentColor" />
126
+ </span>
127
+ {/if}
97
128
  </button>
98
129
 
99
130
  <style>button {
100
131
  display: inline-block;
101
- min-width: 10rem;
102
132
  cursor: pointer;
103
133
  margin-top: var(--spacing-sm);
104
134
  margin-bottom: var(--spacing-sm);
@@ -117,6 +147,9 @@
117
147
  white-space: nowrap;
118
148
  font-family: var(--base-font-family);
119
149
  text-shadow: 0 0 0.125rem rgba(0, 0, 0, 0.5);
150
+ display: inline-flex;
151
+ align-items: center;
152
+ justify-content: center;
120
153
  }
121
154
  button[disabled] {
122
155
  opacity: 0.5;
@@ -136,27 +169,60 @@ button.block {
136
169
  display: block;
137
170
  width: 100%;
138
171
  }
172
+ button.xs {
173
+ font-size: var(--font-xs);
174
+ line-height: 0.875rem;
175
+ padding: var(--spacing-xs) var(--spacing-sm);
176
+ gap: 0;
177
+ }
139
178
  button.sm {
140
179
  font-size: var(--font-sm);
141
180
  line-height: 1rem;
142
181
  padding: var(--spacing-xs) var(--spacing-sm);
182
+ gap: 0;
183
+ }
184
+ button.md {
185
+ font-size: var(--font-base);
186
+ line-height: 1.25rem;
187
+ padding: var(--spacing-sm) var(--spacing-base);
188
+ gap: var(--spacing-xs);
143
189
  }
144
190
  button.lg {
145
191
  font-size: var(--font-md);
146
192
  line-height: 1.5rem;
147
193
  padding: var(--spacing-md) var(--spacing-lg);
194
+ gap: var(--spacing-sm);
148
195
  }
149
196
  button.xl {
150
197
  font-size: var(--font-xl);
151
198
  line-height: 1.75rem;
152
199
  padding: var(--spacing-base) var(--spacing-xl);
200
+ gap: var(--spacing-md);
201
+ }
202
+ button.w-auto {
203
+ min-width: auto;
204
+ width: auto;
205
+ }
206
+ button.w-sm {
207
+ min-width: 5rem;
208
+ display: inline-block;
209
+ }
210
+ button.w-md {
211
+ min-width: 10rem;
212
+ display: inline-block;
213
+ }
214
+ button.w-lg {
215
+ min-width: 20rem;
216
+ display: inline-block;
153
217
  }
154
- button.full {
218
+ button.w-xl {
219
+ min-width: 30rem;
220
+ display: inline-block;
221
+ }
222
+ button.w-full {
155
223
  width: 100%;
156
- padding: var(--spacing-sm) 0;
157
224
  display: block;
158
225
  min-width: auto;
159
- flex-grow: 1;
160
226
  }
161
227
  button:hover {
162
228
  background-color: var(--gray-600);
@@ -225,9 +291,44 @@ button.link {
225
291
  button.link:hover {
226
292
  color: var(--base-link-hover-fg);
227
293
  }
294
+ button.ghost {
295
+ background-color: transparent;
296
+ border-color: transparent;
297
+ color: var(--body-fg);
298
+ text-shadow: none;
299
+ }
300
+ button.ghost:hover {
301
+ background-color: var(--button-secondary-bg);
302
+ border-color: var(--button-secondary-border);
303
+ color: var(--button-secondary-fg);
304
+ text-shadow: 0 0 0.125rem rgba(0, 0, 0, 0.5);
305
+ }
228
306
  button .spinner-wrapper {
229
307
  display: inline-flex;
230
308
  align-items: center;
231
309
  margin-right: var(--spacing-xs);
232
310
  vertical-align: middle;
311
+ }
312
+ button .button-content {
313
+ display: inline;
314
+ }
315
+ button .icon-wrapper {
316
+ display: inline-flex;
317
+ align-items: center;
318
+ vertical-align: middle;
319
+ }
320
+ button.icon-left .icon-wrapper {
321
+ margin-right: var(--spacing-xs);
322
+ }
323
+ button.icon-right .icon-wrapper {
324
+ margin-left: var(--spacing-xs);
325
+ }
326
+ button.icon-above, button.icon-below {
327
+ display: inline-flex;
328
+ flex-direction: column;
329
+ align-items: center;
330
+ gap: var(--spacing-xs);
331
+ }
332
+ button.icon-above .icon-wrapper {
333
+ order: -1;
233
334
  }</style>
@@ -3,10 +3,13 @@
3
3
  * @component
4
4
  */
5
5
  import type { Snippet } from 'svelte';
6
- import type { ButtonVariant, FormFieldSizeOptions } from '../../types/form.js';
6
+ import type { ButtonVariant, ComponentWidth, FormFieldSizeOptions } from '../../types/form.js';
7
+ import type { IconType } from '../../icons/types.js';
8
+ import type { ComponentSize } from '../../types';
7
9
  type $$ComponentProps = {
8
10
  href?: string | undefined;
9
11
  size?: FormFieldSizeOptions;
12
+ width?: ComponentWidth;
10
13
  variant?: ButtonVariant;
11
14
  type?: 'button' | 'submit' | 'reset';
12
15
  block?: boolean;
@@ -17,8 +20,11 @@ type $$ComponentProps = {
17
20
  noMargin?: boolean;
18
21
  collapse?: boolean;
19
22
  repeatSubmitDelay?: number | 'infinite';
23
+ icon?: IconType | undefined;
24
+ iconAlign?: 'left' | 'right' | 'above' | 'below';
25
+ iconSize?: 'default' | ComponentSize;
20
26
  onClick?: ((e?: Event) => void) | undefined;
21
- children: Snippet;
27
+ children?: Snippet;
22
28
  };
23
29
  declare const Button: import("svelte").Component<$$ComponentProps, {}, "disabled">;
24
30
  type Button = ReturnType<typeof Button>;
@@ -41,7 +41,7 @@
41
41
  // Reassign instead of mutate to avoid circular dependency
42
42
  const newItems = currentItems.map((item) => ({
43
43
  ...item,
44
- isChecked: currentGroup.includes(item.value ?? '')
44
+ isChecked: currentGroup.includes(item.value != null ? String(item.value) : '')
45
45
  }));
46
46
  itemsWithState = newItems;
47
47
  });
@@ -65,7 +65,7 @@
65
65
  {#each itemsWithState as item}
66
66
  <CheckBox
67
67
  {disabled}
68
- value={item.value ?? undefined}
68
+ value={item.value != null ? String(item.value) : undefined}
69
69
  bind:isChecked={item.isChecked}
70
70
  onChange={handleCheckboxChange}
71
71
  label={item.name}
@@ -8,7 +8,7 @@
8
8
  TextBox
9
9
  } from '../../index.js';
10
10
  import FlexCol from '../../layout/flex-col.svelte';
11
- import type { SearchFunction } from '../list-box/list-box.js';
11
+ import type { SearchFunction } from '../../types/form.js';
12
12
 
13
13
  let {
14
14
  mode = $bindable('existing' as 'new' | 'existing'),
@@ -19,7 +19,7 @@
19
19
  disabled = false,
20
20
  required = false,
21
21
  searchable = false,
22
- search = undefined as SearchFunction | undefined,
22
+ search = undefined,
23
23
  searchPlaceholder = 'Search',
24
24
  newPlaceholder = 'New'
25
25
  }: {
@@ -31,7 +31,7 @@
31
31
  disabled?: boolean;
32
32
  required?: boolean;
33
33
  searchable?: boolean;
34
- search?: SearchFunction | undefined;
34
+ search?: SearchFunction<DropdownOption> | undefined;
35
35
  searchPlaceholder?: string;
36
36
  newPlaceholder?: string;
37
37
  } = $props();
@@ -1,5 +1,5 @@
1
1
  import { type DropdownOption, type FormFieldSizeOptions } from '../../index.js';
2
- import type { SearchFunction } from '../list-box/list-box.js';
2
+ import type { SearchFunction } from '../../types/form.js';
3
3
  type $$ComponentProps = {
4
4
  mode?: 'new' | 'existing';
5
5
  newValue?: string | null;
@@ -9,7 +9,7 @@ type $$ComponentProps = {
9
9
  disabled?: boolean;
10
10
  required?: boolean;
11
11
  searchable?: boolean;
12
- search?: SearchFunction | undefined;
12
+ search?: SearchFunction<DropdownOption> | undefined;
13
13
  searchPlaceholder?: string;
14
14
  newPlaceholder?: string;
15
15
  };
@@ -22,8 +22,7 @@
22
22
  sm: 1,
23
23
  md: 2,
24
24
  lg: 3,
25
- xl: 4,
26
- full: 4 // Map full to same as xl for backwards compatibility
25
+ xl: 4
27
26
  };
28
27
  return flexMap[size];
29
28
  };
@@ -7,7 +7,7 @@
7
7
  method = 'get',
8
8
  title = undefined,
9
9
  action = undefined,
10
- size = 'full',
10
+ size = 'md',
11
11
  onSubmit = undefined,
12
12
  children
13
13
  }: {
@@ -17,11 +17,7 @@ export { default as TextArea } from './text-area/text-area.svelte';
17
17
  export { default as TextBox } from './text-box/text-box.svelte';
18
18
  export { default as TimeBox } from './time-box/time-box.svelte';
19
19
  export { default as UrlBox } from './url-box/url-box.svelte';
20
- export * from './check-box/index.js';
21
- export * from './list-box/index.js';
22
- export * from './phone-box/index.js';
23
- export * from './radio-group/index.js';
24
- export * from './reference-box/index.js';
20
+ export { default as ListBox } from './list-box/list-box.svelte';
25
21
  export { default as Form } from './form.svelte';
26
22
  export { default as FormActions } from './form-actions/form-actions.svelte';
27
23
  export { default as FormField } from './form-field/form-field.svelte';
@@ -31,4 +27,7 @@ export { default as FormLabel } from './form-label/form-label.svelte';
31
27
  export { default as FormSection } from './form-section/form-section.svelte';
32
28
  export { default as FormRow } from './form-row/form-row.svelte';
33
29
  export type { AdditionalButton } from './form-actions/form-actions.svelte';
30
+ export * from './check-box/index.js';
31
+ export * from './phone-box/index.js';
32
+ export * from './radio-group/index.js';
34
33
  export * from './validation.js';
@@ -18,12 +18,7 @@ export { default as TextArea } from './text-area/text-area.svelte';
18
18
  export { default as TextBox } from './text-box/text-box.svelte';
19
19
  export { default as TimeBox } from './time-box/time-box.svelte';
20
20
  export { default as UrlBox } from './url-box/url-box.svelte';
21
- // Form components with barrel files
22
- export * from './check-box/index.js';
23
- export * from './list-box/index.js';
24
- export * from './phone-box/index.js';
25
- export * from './radio-group/index.js';
26
- export * from './reference-box/index.js';
21
+ export { default as ListBox } from './list-box/list-box.svelte';
27
22
  // Form structure components
28
23
  export { default as Form } from './form.svelte';
29
24
  export { default as FormActions } from './form-actions/form-actions.svelte';
@@ -33,5 +28,9 @@ export { default as FormHeader } from './form-header.svelte';
33
28
  export { default as FormLabel } from './form-label/form-label.svelte';
34
29
  export { default as FormSection } from './form-section/form-section.svelte';
35
30
  export { default as FormRow } from './form-row/form-row.svelte';
31
+ // Form components with barrel files
32
+ export * from './check-box/index.js';
33
+ export * from './phone-box/index.js';
34
+ export * from './radio-group/index.js';
36
35
  // Validation utilities
37
36
  export * from './validation.js';
@@ -7,9 +7,9 @@
7
7
  import debounce from '../../helpers/debounce.js';
8
8
  import { browser } from '$app/environment';
9
9
  import { onMount, untrack } from 'svelte';
10
- import type { SearchFunction, CreateNewFunction } from './list-box.js';
11
10
  import Prompt from '../../modals/prompt.svelte';
12
11
  import { ucfirst } from '../../helpers/ucfirst.js';
12
+ import type { CreateNewFunction, SearchFunction } from '../../types/form.js';
13
13
 
14
14
  let {
15
15
  value = $bindable(null as string | null),
@@ -19,7 +19,7 @@
19
19
  required = false,
20
20
  readonly = false,
21
21
  searchable = false,
22
- search = undefined as SearchFunction | undefined,
22
+ search = undefined,
23
23
  placeholder = '',
24
24
  onChange = undefined,
25
25
  onFocus = undefined,
@@ -29,7 +29,7 @@
29
29
  feedback = undefined,
30
30
  virtualScroll = false,
31
31
  itemHeight = 40,
32
- createNew = undefined as CreateNewFunction | undefined,
32
+ createNew = undefined,
33
33
  resourceName = undefined
34
34
  }: {
35
35
  value?: string | null;
@@ -39,7 +39,7 @@
39
39
  required?: boolean;
40
40
  readonly?: boolean;
41
41
  searchable?: boolean;
42
- search?: SearchFunction | undefined;
42
+ search?: SearchFunction<DropdownOption> | undefined;
43
43
  placeholder?: string;
44
44
  onChange?: ((value: string | null) => void) | undefined;
45
45
  onFocus?: ((e: FocusEvent) => void) | undefined;
@@ -49,7 +49,7 @@
49
49
  feedback?: FormFieldFeedback;
50
50
  virtualScroll?: boolean;
51
51
  itemHeight?: number;
52
- createNew?: CreateNewFunction | undefined;
52
+ createNew?: CreateNewFunction<DropdownOption> | undefined;
53
53
  resourceName?: string;
54
54
  } = $props();
55
55
 
@@ -113,7 +113,9 @@
113
113
  });
114
114
 
115
115
  // Check if there are no results when searching
116
- let hasNoResults = $derived(isSearchable && text.trim() && filteredItems.length === 0 && !isLoading);
116
+ let hasNoResults = $derived(
117
+ isSearchable && text.trim() && filteredItems.length === 0 && !isLoading
118
+ );
117
119
 
118
120
  // Get the ID of the highlighted option for ARIA
119
121
  let activeDescendant = $derived(
@@ -139,7 +141,7 @@
139
141
  // When an item is selected from the dropdown menu
140
142
  const onSelect = (item: MenuOption) => {
141
143
  isUserTyping = false;
142
- value = item.value;
144
+ value = item.value != null ? String(item.value) : null;
143
145
  onChange?.(value);
144
146
  text = getText();
145
147
  isMenuOpen = false;
@@ -216,7 +218,12 @@
216
218
 
217
219
  if (e.key === 'Enter') {
218
220
  e.preventDefault();
219
- if (isMenuOpen && highlightIndex >= 0 && highlightIndex < filteredItems.length && filteredItems[highlightIndex]) {
221
+ if (
222
+ isMenuOpen &&
223
+ highlightIndex >= 0 &&
224
+ highlightIndex < filteredItems.length &&
225
+ filteredItems[highlightIndex]
226
+ ) {
220
227
  onSelect(filteredItems[highlightIndex]);
221
228
  } else if (isMenuOpen && createNew && highlightIndex === filteredItems.length) {
222
229
  // "Create new..." is highlighted
@@ -231,7 +238,12 @@
231
238
  }
232
239
 
233
240
  if (e.key === 'Tab') {
234
- if (isMenuOpen && highlightIndex >= 0 && highlightIndex < filteredItems.length && filteredItems[highlightIndex]) {
241
+ if (
242
+ isMenuOpen &&
243
+ highlightIndex >= 0 &&
244
+ highlightIndex < filteredItems.length &&
245
+ filteredItems[highlightIndex]
246
+ ) {
235
247
  e.preventDefault();
236
248
  onSelect(filteredItems[highlightIndex]);
237
249
  } else if (isMenuOpen && createNew && highlightIndex === filteredItems.length) {
@@ -325,7 +337,7 @@
325
337
  localItems = [...localItems, result];
326
338
 
327
339
  // Select the newly created item
328
- value = result.value;
340
+ value = result.value != null ? String(result.value) : null;
329
341
  onChange?.(value);
330
342
  text = result.name;
331
343
  isMenuOpen = false;
@@ -458,7 +470,6 @@
458
470
  closeAfterSelect={true}
459
471
  searchText={isSearchable ? text : ''}
460
472
  {onSelect}
461
- size="full"
462
473
  bind:highlightIndex
463
474
  bind:value
464
475
  {listboxId}
@@ -509,9 +520,7 @@
509
520
  </div>
510
521
  {/if}
511
522
  {#if isCreating}
512
- <div class="creating-indicator" aria-live="polite">
513
- Creating...
514
- </div>
523
+ <div class="creating-indicator" aria-live="polite">Creating...</div>
515
524
  {/if}
516
525
  </Prompt>
517
526
  {/key}
@@ -1,6 +1,6 @@
1
1
  import type { DropdownOption, FormFieldSizeOptions } from '../../types/form.js';
2
2
  import { type FormFieldFeedback } from '../form-field/form-field.svelte';
3
- import type { SearchFunction, CreateNewFunction } from './list-box.js';
3
+ import type { CreateNewFunction, SearchFunction } from '../../types/form.js';
4
4
  type $$ComponentProps = {
5
5
  value?: string | null;
6
6
  items?: DropdownOption[];
@@ -9,7 +9,7 @@ type $$ComponentProps = {
9
9
  required?: boolean;
10
10
  readonly?: boolean;
11
11
  searchable?: boolean;
12
- search?: SearchFunction | undefined;
12
+ search?: SearchFunction<DropdownOption> | undefined;
13
13
  placeholder?: string;
14
14
  onChange?: ((value: string | null) => void) | undefined;
15
15
  onFocus?: ((e: FocusEvent) => void) | undefined;
@@ -19,7 +19,7 @@ type $$ComponentProps = {
19
19
  feedback?: FormFieldFeedback;
20
20
  virtualScroll?: boolean;
21
21
  itemHeight?: number;
22
- createNew?: CreateNewFunction | undefined;
22
+ createNew?: CreateNewFunction<DropdownOption> | undefined;
23
23
  resourceName?: string;
24
24
  };
25
25
  declare const ListBox: import("svelte").Component<$$ComponentProps, {}, "value">;
@@ -1,2 +1,2 @@
1
1
  export { default as ReferenceBox } from './reference-box.svelte';
2
- export type { ReferenceItem, SearchFunction, CreateNewFunction } from './reference-box.js';
2
+ export type { ReferenceItem, SearchFunction, CreateNewFunction, LinkBuilderFunction } from './reference-box.js';
@@ -1,8 +1,9 @@
1
+ import type { SearchFunction as BaseSearchFunction, CreateNewFunction as BaseCreateNewFunction } from '../../types/form.js';
1
2
  export type ReferenceItem = {
2
3
  id: string | number;
3
4
  name: string;
4
5
  description?: string;
5
6
  };
6
- export type SearchFunction = (text: string) => Promise<ReferenceItem[]>;
7
- export type CreateNewFunction = (inputName: string) => Promise<ReferenceItem | null>;
7
+ export type SearchFunction = BaseSearchFunction<ReferenceItem>;
8
+ export type CreateNewFunction = BaseCreateNewFunction<ReferenceItem>;
8
9
  export type LinkBuilderFunction = (item: ReferenceItem) => string | undefined;
@@ -7,15 +7,11 @@
7
7
  import { onMount } from 'svelte';
8
8
  import { browser } from '$app/environment';
9
9
  import debounce from '../../helpers/debounce.js';
10
- import type {
11
- ReferenceItem,
12
- SearchFunction,
13
- CreateNewFunction,
14
- LinkBuilderFunction
15
- } from './reference-box.js';
16
10
  import Prompt from '../../modals/prompt.svelte';
17
11
  import { ucfirst } from '../../helpers/ucfirst.js';
18
12
  import Icon from '../../icons/icon.svelte';
13
+ import type { SearchFunction, CreateNewFunction } from '../../types/form.js';
14
+ import type { ReferenceItem, LinkBuilderFunction } from './reference-box.js';
19
15
 
20
16
  const id = uniqueId();
21
17
  const listboxId = `${id}-listbox`;
@@ -23,8 +19,8 @@
23
19
  let {
24
20
  value = $bindable([] as ReferenceItem[]),
25
21
  items = [] as ReferenceItem[],
26
- search = undefined as SearchFunction | undefined,
27
- createNew = undefined as CreateNewFunction | undefined,
22
+ search = undefined,
23
+ createNew = undefined,
28
24
  linkBuilder = undefined as LinkBuilderFunction | undefined,
29
25
  resourceName = undefined as string | undefined,
30
26
  placeholder = 'Search and add items...',
@@ -39,8 +35,8 @@
39
35
  }: {
40
36
  value?: ReferenceItem[];
41
37
  items?: ReferenceItem[];
42
- search?: SearchFunction | undefined;
43
- createNew?: CreateNewFunction | undefined;
38
+ search?: SearchFunction<ReferenceItem> | undefined;
39
+ createNew?: CreateNewFunction<ReferenceItem> | undefined;
44
40
  linkBuilder?: LinkBuilderFunction | undefined;
45
41
  resourceName?: string | undefined;
46
42
  placeholder?: string;
@@ -433,7 +429,6 @@
433
429
  closeAfterSelect={true}
434
430
  {searchText}
435
431
  onSelect={onSelectFromMenu}
436
- size="full"
437
432
  bind:highlightIndex
438
433
  {listboxId}
439
434
  />
@@ -1,11 +1,12 @@
1
1
  import { type FormFieldFeedback } from '../form-field/form-field.svelte';
2
2
  import type { FormFieldSizeOptions } from '../../types/form.js';
3
- import type { ReferenceItem, SearchFunction, CreateNewFunction, LinkBuilderFunction } from './reference-box.js';
3
+ import type { SearchFunction, CreateNewFunction } from '../../types/form.js';
4
+ import type { ReferenceItem, LinkBuilderFunction } from './reference-box.js';
4
5
  type $$ComponentProps = {
5
6
  value?: ReferenceItem[];
6
7
  items?: ReferenceItem[];
7
- search?: SearchFunction | undefined;
8
- createNew?: CreateNewFunction | undefined;
8
+ search?: SearchFunction<ReferenceItem> | undefined;
9
+ createNew?: CreateNewFunction<ReferenceItem> | undefined;
9
10
  linkBuilder?: LinkBuilderFunction | undefined;
10
11
  resourceName?: string | undefined;
11
12
  placeholder?: string;
@@ -377,7 +377,6 @@
377
377
  closeAfterSelect={true}
378
378
  searchText={newTag}
379
379
  onSelect={onSelectFromMenu}
380
- size="full"
381
380
  bind:highlightIndex
382
381
  {listboxId}
383
382
  />
@@ -14,7 +14,7 @@
14
14
  feedback = undefined,
15
15
  isLoading = false,
16
16
  showCharacterCount = false,
17
- size = 'full',
17
+ size = 'md',
18
18
  type = 'text',
19
19
  disabled = false,
20
20
  required = false,
@@ -1,32 +1,65 @@
1
1
  <script lang="ts">
2
+ /**
3
+ * Empty state component for displaying messages when no data is available.
4
+ * Supports icons, messages, descriptions, and custom content.
5
+ * @component
6
+ */
2
7
  import type { Snippet } from 'svelte';
8
+ import type { IconType } from '../../icons/types.js';
9
+ import Icon from '../../icons/icon.svelte';
3
10
 
4
11
  let {
5
- text = 'No data to display',
6
- size = 'md' as 'sm' | 'md' | 'lg' | 'xl',
7
- orientation = 'vertical' as 'horizontal' | 'vertical',
8
- reverse = false,
9
- align = 'center' as 'center' | 'start' | 'end',
12
+ /** Primary title/heading */
13
+ title = undefined,
14
+ /** Main message text (required if no title) */
15
+ message = 'No data to display',
16
+ /** Optional secondary description */
17
+ description = undefined,
18
+ /** Built-in icon type */
19
+ icon = undefined,
20
+ /** Icon size */
21
+ iconSize = 'xl',
22
+ /** Component size affects padding and text sizing */
23
+ size = 'md',
24
+ /** Custom content (buttons, links, etc.) */
10
25
  children = undefined
11
26
  }: {
12
- text?: string;
27
+ title?: string;
28
+ message?: string;
29
+ description?: string;
30
+ icon?: IconType;
31
+ iconSize?: 'sm' | 'md' | 'lg' | 'xl';
13
32
  size?: 'sm' | 'md' | 'lg' | 'xl';
14
- orientation?: 'horizontal' | 'vertical';
15
- reverse?: boolean;
16
- align?: 'center' | 'start' | 'end';
17
33
  children?: Snippet;
18
34
  } = $props();
19
35
  </script>
20
36
 
21
- <div class="empty {size} {orientation} {reverse ? 'reverse' : ''} {align}">
22
- {#if children}
23
- <div class="icon">
24
- {@render children?.()}
37
+ <div class="empty {size}">
38
+ {#if icon}
39
+ <div class="icon-wrapper">
40
+ <Icon type={icon} size={iconSize} variant="secondary" />
25
41
  </div>
26
42
  {/if}
27
- <div class="text">
28
- {text}
43
+
44
+ <div class="content">
45
+ {#if title}
46
+ <h3 class="title">{title}</h3>
47
+ {/if}
48
+
49
+ {#if message}
50
+ <p class="message">{message}</p>
51
+ {/if}
52
+
53
+ {#if description}
54
+ <p class="description">{description}</p>
55
+ {/if}
29
56
  </div>
57
+
58
+ {#if children}
59
+ <div class="actions">
60
+ {@render children()}
61
+ </div>
62
+ {/if}
30
63
  </div>
31
64
 
32
65
  <style>.empty {
@@ -35,80 +68,131 @@
35
68
  align-items: center;
36
69
  justify-content: center;
37
70
  text-align: center;
38
- color: var(--gray-500);
39
- font-weight: 500;
40
71
  padding: var(--spacing-2xl);
72
+ gap: var(--spacing-lg);
41
73
  }
42
74
 
43
- .start {
44
- justify-content: flex-start;
45
- }
46
-
47
- .end {
48
- justify-content: flex-end;
49
- }
50
-
51
- .horizontal {
52
- flex-direction: row;
53
- gap: var(--spacing-lg);
75
+ .icon-wrapper {
76
+ color: var(--gray-400);
77
+ opacity: 0.8;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
54
81
  }
55
- .horizontal.reverse {
56
- flex-direction: row-reverse;
82
+ .icon-wrapper :global(svg) {
83
+ width: 100%;
84
+ height: 100%;
57
85
  }
58
86
 
59
- .vertical.reverse {
60
- flex-direction: column-reverse;
87
+ .content {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: var(--spacing-sm);
91
+ max-width: 32rem;
61
92
  }
62
93
 
63
- .icon {
64
- margin-bottom: var(--spacing-base);
65
- color: var(--gray-400);
66
- opacity: 0.8;
94
+ .title {
95
+ color: var(--gray-700);
96
+ font-size: var(--font-lg);
97
+ font-weight: 600;
98
+ line-height: 1.4;
99
+ margin: 0;
67
100
  }
68
101
 
69
- .text {
102
+ .message {
70
103
  color: var(--gray-600);
71
104
  font-size: var(--font-md);
105
+ font-weight: 500;
72
106
  line-height: 1.6;
107
+ margin: 0;
108
+ }
109
+
110
+ .description {
111
+ color: var(--gray-500);
112
+ font-size: var(--font-base);
113
+ line-height: 1.5;
114
+ margin: 0;
73
115
  }
74
116
 
117
+ .actions {
118
+ display: flex;
119
+ flex-wrap: wrap;
120
+ gap: var(--spacing-sm);
121
+ justify-content: center;
122
+ align-items: center;
123
+ }
124
+
125
+ /* Size variants */
75
126
  .sm {
76
127
  padding: var(--spacing-lg);
128
+ gap: var(--spacing-base);
77
129
  }
78
- .sm .text {
79
- font-size: var(--font-base);
130
+ .sm .icon-wrapper {
131
+ font-size: 2rem;
80
132
  }
81
- .sm .icon {
82
- height: 2rem;
133
+ .sm .icon-wrapper :global(.icon) {
83
134
  width: 2rem;
135
+ height: 2rem;
84
136
  }
85
-
86
- .md .text {
137
+ .sm .title {
87
138
  font-size: var(--font-md);
88
139
  }
89
- .md .icon {
90
- height: 3rem;
140
+ .sm .message {
141
+ font-size: var(--font-base);
142
+ }
143
+ .sm .description {
144
+ font-size: var(--font-sm);
145
+ }
146
+
147
+ .md .icon-wrapper {
148
+ font-size: 3rem;
149
+ }
150
+ .md .icon-wrapper :global(.icon) {
91
151
  width: 3rem;
152
+ height: 3rem;
92
153
  }
93
154
 
94
155
  .lg {
95
156
  padding: var(--spacing-2xl) var(--spacing-xl);
157
+ gap: var(--spacing-xl);
96
158
  }
97
- .lg .text {
98
- font-size: var(--font-lg);
159
+ .lg .icon-wrapper {
160
+ font-size: 4rem;
99
161
  }
100
- .lg .icon {
101
- height: 4rem;
162
+ .lg .icon-wrapper :global(.icon) {
102
163
  width: 4rem;
164
+ height: 4rem;
165
+ }
166
+ .lg .title {
167
+ font-size: var(--font-xl);
168
+ }
169
+ .lg .message {
170
+ font-size: var(--font-lg);
171
+ }
172
+ .lg .description {
173
+ font-size: var(--font-md);
103
174
  }
104
175
 
105
176
  .xl {
106
177
  padding: 4rem var(--spacing-2xl);
178
+ gap: var(--spacing-2xl);
107
179
  }
108
- .xl .text {
109
- font-size: var(--font-xl);
180
+ .xl .icon-wrapper {
181
+ font-size: 5rem;
110
182
  }
111
- .xl .icon {
112
- height: 5rem;
183
+ .xl .icon-wrapper :global(.icon) {
113
184
  width: 5rem;
185
+ height: 5rem;
186
+ }
187
+ .xl .title {
188
+ font-size: var(--font-2xl);
189
+ }
190
+ .xl .message {
191
+ font-size: var(--font-xl);
192
+ }
193
+ .xl .description {
194
+ font-size: var(--font-lg);
195
+ }
196
+ .xl .actions {
197
+ gap: var(--spacing-base);
114
198
  }</style>
@@ -1,10 +1,17 @@
1
+ /**
2
+ * Empty state component for displaying messages when no data is available.
3
+ * Supports icons, messages, descriptions, and custom content.
4
+ * @component
5
+ */
1
6
  import type { Snippet } from 'svelte';
7
+ import type { IconType } from '../../icons/types.js';
2
8
  type $$ComponentProps = {
3
- text?: string;
9
+ title?: string;
10
+ message?: string;
11
+ description?: string;
12
+ icon?: IconType;
13
+ iconSize?: 'sm' | 'md' | 'lg' | 'xl';
4
14
  size?: 'sm' | 'md' | 'lg' | 'xl';
5
- orientation?: 'horizontal' | 'vertical';
6
- reverse?: boolean;
7
- align?: 'center' | 'start' | 'end';
8
15
  children?: Snippet;
9
16
  };
10
17
  declare const Empty: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -34,7 +34,7 @@
34
34
  } = $props();
35
35
 
36
36
  const selectItem = (item: MenuOption) => {
37
- value = item.value;
37
+ value = item.value != null ? String(item.value) : null;
38
38
  onSelect?.(item);
39
39
  if (closeAfterSelect) open = false;
40
40
  };
@@ -4,11 +4,13 @@
4
4
  let {
5
5
  show = true,
6
6
  onClick = undefined,
7
+ onEscape = undefined,
7
8
  blur = false,
8
9
  children
9
10
  }: {
10
11
  show?: boolean;
11
12
  onClick?: (() => void) | undefined;
13
+ onEscape?: (() => void) | undefined;
12
14
  blur?: boolean;
13
15
  children?: Snippet;
14
16
  } = $props();
@@ -22,7 +24,7 @@
22
24
 
23
25
  const onKeyPress = (event: KeyboardEvent) => {
24
26
  if (event.key === 'Escape') {
25
- onClick?.();
27
+ onEscape?.();
26
28
  }
27
29
  };
28
30
  </script>
@@ -2,6 +2,7 @@ import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  show?: boolean;
4
4
  onClick?: (() => void) | undefined;
5
+ onEscape?: (() => void) | undefined;
5
6
  blur?: boolean;
6
7
  children?: Snippet;
7
8
  };
@@ -8,7 +8,7 @@
8
8
  let {
9
9
  title = undefined,
10
10
  level = 2 as SectionLevel,
11
- size = 'full' as FormFieldSizeOptions,
11
+ size = 'xl' as FormFieldSizeOptions,
12
12
  hidden = false,
13
13
  align = 'left' as 'left' | 'center' | 'right',
14
14
  children
@@ -37,7 +37,7 @@
37
37
  </script>
38
38
 
39
39
  {#if open}
40
- <Overlay onClick={close}>
40
+ <Overlay onEscape={close}>
41
41
  <Dialog
42
42
  {size}
43
43
  role="alertdialog"
@@ -54,10 +54,10 @@
54
54
  <DialogBody>
55
55
  {@render children?.()}
56
56
  </DialogBody>
57
- <Divider />
58
- <DialogFooter>
59
- <Button onClick={close} size="full" variant={buttonVariant}>{buttonText}</Button>
60
- </DialogFooter>
57
+ <Divider />
58
+ <DialogFooter>
59
+ <Button onClick={close} variant={buttonVariant}>{buttonText}</Button>
60
+ </DialogFooter>
61
61
  </Dialog>
62
62
  </Overlay>
63
63
  {/if}
@@ -48,7 +48,7 @@
48
48
  </script>
49
49
 
50
50
  {#if open}
51
- <Overlay onClick={no}>
51
+ <Overlay onEscape={no}>
52
52
  <Dialog
53
53
  {size}
54
54
  role="alertdialog"
@@ -65,11 +65,11 @@
65
65
  <DialogBody>
66
66
  {@render children?.()}
67
67
  </DialogBody>
68
- <Divider />
69
- <DialogFooter>
70
- <Button onClick={no} variant={noVariant} size="full">{noText}</Button>
71
- <Button onClick={yes} variant={yesVariant} size="full">{yesText}</Button>
72
- </DialogFooter>
68
+ <Divider />
69
+ <DialogFooter>
70
+ <Button onClick={no} variant={noVariant}>{noText}</Button>
71
+ <Button onClick={yes} variant={yesVariant}>{yesText}</Button>
72
+ </DialogFooter>
73
73
  </Dialog>
74
74
  </Overlay>
75
75
  {/if}
@@ -80,7 +80,7 @@
80
80
  </script>
81
81
 
82
82
  {#if open}
83
- <Overlay {blur} onClick={close}>
83
+ <Overlay {blur} onEscape={dismissable ? close : undefined}>
84
84
  <div bind:this={dialogElement}>
85
85
  <Dialog {size} {glass} role="dialog" aria-modal="true" aria-labelledby={titleId}>
86
86
  <DialogCloseButton show={_showCloseButton} onClick={close} />
@@ -58,7 +58,7 @@
58
58
  </script>
59
59
 
60
60
  {#if open}
61
- <Overlay onClick={no}>
61
+ <Overlay onEscape={no}>
62
62
  <Dialog
63
63
  {size}
64
64
  role="dialog"
@@ -73,16 +73,16 @@
73
73
  {/if}
74
74
  <DialogCloseButton show={showCloseButton} onClick={no} />
75
75
  <DialogBody>
76
- <TextBox bind:value {placeholder} {type} {required} size="full" />
76
+ <TextBox bind:value {placeholder} {type} {required} />
77
77
  {#if children}
78
78
  {@render children()}
79
79
  {/if}
80
80
  </DialogBody>
81
- <Divider />
82
- <DialogFooter>
83
- <Button onClick={no} variant={cancelVariant} size="full">{cancelText}</Button>
84
- <Button onClick={yes} variant={okVariant} size="full">{okText}</Button>
85
- </DialogFooter>
81
+ <Divider />
82
+ <DialogFooter>
83
+ <Button onClick={no} variant={cancelVariant}>{cancelText}</Button>
84
+ <Button onClick={yes} variant={okVariant}>{okText}</Button>
85
+ </DialogFooter>
86
86
  </Dialog>
87
87
  </Overlay>
88
88
  {/if}
@@ -42,7 +42,7 @@
42
42
  </script>
43
43
 
44
44
  {#if open}
45
- <Overlay onClick={close}>
45
+ <Overlay onEscape={dismissable ? close : undefined}>
46
46
  <div
47
47
  bind:this={drawerElement}
48
48
  class="drawer {position}"
@@ -12,7 +12,6 @@
12
12
  RowActions
13
13
  } from '../types/data.js';
14
14
  import Empty from '../generic/empty/empty.svelte';
15
- import Icon from '../icons/icon.svelte';
16
15
  import Pagination from '../navigation/pagination/pagination.svelte';
17
16
  import Loading from '../placeholders/loading.svelte';
18
17
  import TableCaption from './table-caption.svelte';
@@ -254,9 +253,7 @@
254
253
  {#if rows === undefined}
255
254
  <Loading />
256
255
  {:else}
257
- <Empty>
258
- <Icon type="folder-open" size="lg" />
259
- </Empty>
256
+ <Empty icon="folder-open" iconSize="lg" />
260
257
  {/if}
261
258
  </div>
262
259
  </TableCell>
@@ -1,13 +1,13 @@
1
- export type { ComponentSize as FormFieldSizeOptions } from './size.js';
1
+ export type { ComponentSize as FormFieldSizeOptions, ComponentWidth } from './size.js';
2
2
  /**
3
3
  * Button variant options
4
4
  */
5
- export type ButtonVariant = 'primary' | 'secondary' | 'positive' | 'danger' | 'outline' | 'link';
5
+ export type ButtonVariant = 'primary' | 'secondary' | 'positive' | 'danger' | 'outline' | 'link' | 'ghost';
6
6
  /**
7
7
  * Dropdown option structure
8
8
  */
9
9
  export type DropdownOption = {
10
- value: string | null;
10
+ value: string | number | null;
11
11
  name: string;
12
12
  id?: string;
13
13
  label?: string;
@@ -27,3 +27,17 @@ export type AllowedTextInputTypes = 'text' | 'email' | 'password' | 'search' | '
27
27
  * Radio button value type
28
28
  */
29
29
  export type RadioButtonValue = string | number | boolean | undefined;
30
+ /**
31
+ * Generic search function type
32
+ * @template T The type of item returned (e.g., DropdownOption, ReferenceItem)
33
+ */
34
+ export type SearchFunction<T extends {
35
+ name: string;
36
+ } = DropdownOption> = (text: string) => Promise<T[]>;
37
+ /**
38
+ * Generic create new function type
39
+ * @template T The type of item returned (e.g., DropdownOption, ReferenceItem)
40
+ */
41
+ export type CreateNewFunction<T extends {
42
+ name: string;
43
+ } = DropdownOption> = (inputName: string) => Promise<T | null>;
@@ -1,7 +1,15 @@
1
1
  /**
2
2
  * Unified size type for all components
3
+ * Controls font size, padding, and general component scale
3
4
  */
4
- export type ComponentSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
5
+ export type ComponentSize = 'sm' | 'md' | 'lg' | 'xl';
6
+ /**
7
+ * Width options for components
8
+ * auto: content-based width (no min-width)
9
+ * xs-xl: fixed minimum widths
10
+ * full: fills available space (100%)
11
+ */
12
+ export type ComponentWidth = 'auto' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
5
13
  /**
6
14
  * Size utility functions for consistent spacing and sizing
7
15
  */
@@ -12,11 +20,27 @@ export declare const sizeMap: {
12
20
  readonly xl: "auto";
13
21
  readonly full: "100%";
14
22
  };
23
+ /**
24
+ * Width map for component widths
25
+ */
26
+ export declare const widthMap: {
27
+ readonly auto: "auto";
28
+ readonly xs: "10rem";
29
+ readonly sm: "15rem";
30
+ readonly md: "20rem";
31
+ readonly lg: "25rem";
32
+ readonly xl: "30rem";
33
+ readonly full: "100%";
34
+ };
15
35
  /**
16
36
  * Get max width for a given size
17
37
  */
18
38
  export declare const getMaxWidth: (size: ComponentSize) => string;
19
39
  /**
20
- * Get display type for a given size (block for xl/full, inline-block for others)
40
+ * Get display type for a given size (block for xl, inline-block for others)
21
41
  */
22
42
  export declare const getDisplayType: (size: ComponentSize) => "block" | "inline-block";
43
+ /**
44
+ * Get min-width for a given width option
45
+ */
46
+ export declare const getMinWidth: (width: ComponentWidth) => string;
@@ -8,6 +8,18 @@ export const sizeMap = {
8
8
  xl: 'auto',
9
9
  full: '100%'
10
10
  };
11
+ /**
12
+ * Width map for component widths
13
+ */
14
+ export const widthMap = {
15
+ auto: 'auto',
16
+ xs: '10rem',
17
+ sm: '15rem',
18
+ md: '20rem',
19
+ lg: '25rem',
20
+ xl: '30rem',
21
+ full: '100%'
22
+ };
11
23
  /**
12
24
  * Get max width for a given size
13
25
  */
@@ -15,8 +27,14 @@ export const getMaxWidth = (size) => {
15
27
  return sizeMap[size];
16
28
  };
17
29
  /**
18
- * Get display type for a given size (block for xl/full, inline-block for others)
30
+ * Get display type for a given size (block for xl, inline-block for others)
19
31
  */
20
32
  export const getDisplayType = (size) => {
21
- return ['xl', 'full'].includes(size) ? 'block' : 'inline-block';
33
+ return size === 'xl' ? 'block' : 'inline-block';
34
+ };
35
+ /**
36
+ * Get min-width for a given width option
37
+ */
38
+ export const getMinWidth = (width) => {
39
+ return widthMap[width];
22
40
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltacular",
3
- "version": "1.0.33",
3
+ "version": "1.0.36",
4
4
  "description": "A Svelte component library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",