@mrintel/villain-ui 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/components/buttons/Button.svelte.d.ts +1 -1
  2. package/dist/components/buttons/IconButton.svelte.d.ts +1 -1
  3. package/dist/components/buttons/LinkButton.svelte.d.ts +1 -1
  4. package/dist/components/buttons/buttonClasses.d.ts +2 -0
  5. package/dist/components/buttons/buttonClasses.js +2 -1
  6. package/dist/components/cards/SectionHeader.svelte +8 -3
  7. package/dist/components/cards/SectionHeader.svelte.d.ts +1 -0
  8. package/dist/components/data/Data.types.d.ts +10 -0
  9. package/dist/components/data/EmptyState.svelte +42 -0
  10. package/dist/components/data/EmptyState.svelte.d.ts +13 -0
  11. package/dist/components/data/Stat.svelte +30 -2
  12. package/dist/components/data/Stat.svelte.d.ts +9 -0
  13. package/dist/components/data/WeekHeatmap.svelte +150 -0
  14. package/dist/components/data/WeekHeatmap.svelte.d.ts +47 -0
  15. package/dist/components/data/index.d.ts +3 -1
  16. package/dist/components/data/index.js +2 -0
  17. package/dist/components/forms/Checkbox.svelte +27 -21
  18. package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
  19. package/dist/components/forms/DatePicker.svelte +7 -2
  20. package/dist/components/forms/DatePicker.svelte.d.ts +1 -0
  21. package/dist/components/forms/DateTimePicker.svelte +7 -2
  22. package/dist/components/forms/DateTimePicker.svelte.d.ts +1 -0
  23. package/dist/components/forms/FileUpload.svelte +7 -2
  24. package/dist/components/forms/FileUpload.svelte.d.ts +2 -0
  25. package/dist/components/forms/Input.svelte +95 -10
  26. package/dist/components/forms/Input.svelte.d.ts +8 -0
  27. package/dist/components/forms/RadioGroup.svelte +35 -25
  28. package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
  29. package/dist/components/forms/RangeSlider.svelte +6 -2
  30. package/dist/components/forms/RangeSlider.svelte.d.ts +2 -0
  31. package/dist/components/forms/Select.svelte +39 -30
  32. package/dist/components/forms/Select.svelte.d.ts +1 -0
  33. package/dist/components/forms/SelectMenu.svelte +7 -2
  34. package/dist/components/forms/SelectMenu.svelte.d.ts +1 -0
  35. package/dist/components/forms/Switch.svelte +31 -23
  36. package/dist/components/forms/Switch.svelte.d.ts +2 -0
  37. package/dist/components/forms/Textarea.svelte +28 -19
  38. package/dist/components/forms/Textarea.svelte.d.ts +1 -0
  39. package/dist/components/forms/TimePicker.svelte +7 -2
  40. package/dist/components/forms/TimePicker.svelte.d.ts +1 -0
  41. package/dist/components/overlays/ToastHost.svelte +19 -0
  42. package/dist/components/overlays/ToastHost.svelte.d.ts +8 -0
  43. package/dist/components/overlays/index.d.ts +3 -0
  44. package/dist/components/overlays/index.js +2 -0
  45. package/dist/components/overlays/toast.store.d.ts +32 -0
  46. package/dist/components/overlays/toast.store.js +34 -0
  47. package/dist/components/typography/Heading.svelte +3 -3
  48. package/dist/components/typography/Heading.svelte.d.ts +1 -0
  49. package/dist/components/typography/Text.svelte +2 -2
  50. package/dist/components/typography/Text.svelte.d.ts +1 -0
  51. package/dist/components/wizard/StepForm.svelte +22 -1
  52. package/dist/components/wizard/StepForm.svelte.d.ts +8 -1
  53. package/dist/components/wizard/step.service.js +89 -21
  54. package/dist/components/wizard/step.types.d.ts +32 -0
  55. package/dist/index.d.ts +7 -4
  56. package/dist/index.js +3 -3
  57. package/dist/theme.css +125 -0
  58. package/package.json +1 -1
@@ -1,6 +1,10 @@
1
- <script lang="ts">import { createId } from '../../lib/internal/id.js';
1
+ <script lang="ts">import { createId } from '../../lib/internal/id';
2
2
  import { baseInputClasses, focusClasses, disabledClasses, } from './formClasses';
3
- let { type = 'text', name, value = $bindable(), placeholder, disabled = false, error = false, label, id = createId('input'), autocomplete, oninput, iconBefore, iconAfter, validate, validationMessage, showValidation = true, class: className = '', } = $props();
3
+ let { type = 'text', name, value = $bindable(), placeholder, disabled = false, error = false, label, id = createId('input'), autocomplete, oninput, iconBefore, iconAfter, validate, validationMessage, showValidation = true, revealable = false, min, max, step = 1, class: className = '', } = $props();
4
+ // Password reveal toggle (revealable only applies to type="password")
5
+ let revealed = $state(false);
6
+ const showReveal = $derived(revealable && type === 'password');
7
+ const effectiveType = $derived(showReveal && revealed ? 'text' : type);
4
8
  // Built-in validation patterns
5
9
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
10
  const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
@@ -65,15 +69,22 @@ $effect(() => {
65
69
  });
66
70
  const hasError = $derived(error || (validationError !== null));
67
71
  const errorClasses = $derived(hasError ? 'error-state' : '');
72
+ function clamp(n) {
73
+ if (min !== undefined && n < min)
74
+ return min;
75
+ if (max !== undefined && n > max)
76
+ return max;
77
+ return n;
78
+ }
68
79
  function increment() {
69
80
  if (disabled || type !== 'number')
70
81
  return;
71
- value = Number(value || 0) + 1;
82
+ value = clamp(Number(value || 0) + step);
72
83
  }
73
84
  function decrement() {
74
85
  if (disabled || type !== 'number')
75
86
  return;
76
- value = Number(value || 0) - 1;
87
+ value = clamp(Number(value || 0) - step);
77
88
  }
78
89
  </script>
79
90
 
@@ -94,23 +105,60 @@ function decrement() {
94
105
 
95
106
  <!-- INPUT FIELD -->
96
107
  <input
97
- {type}
108
+ type={effectiveType}
98
109
  {name}
99
110
  {id}
100
111
  {placeholder}
101
112
  {disabled}
102
113
  {autocomplete}
114
+ min={type === 'number' ? min : undefined}
115
+ max={type === 'number' ? max : undefined}
116
+ step={type === 'number' ? step : undefined}
103
117
  bind:value
104
118
  {oninput}
105
119
  class="{baseInputClasses} {focusClasses} {errorClasses} {disabled
106
120
  ? disabledClasses
107
121
  : ''} {className}"
108
122
  class:pl-12={iconBefore}
109
- class:pr-12={iconAfter || type === 'number'}
123
+ class:pr-12={iconAfter || showReveal || type === 'number'}
110
124
  />
111
125
 
112
126
  <!-- AFTER ICON (non-number) -->
113
- {#if iconAfter && type !== 'number'}
127
+ {#if showReveal}
128
+ <button
129
+ type="button"
130
+ onclick={() => (revealed = !revealed)}
131
+ class="absolute right-4 z-10 inline-flex cursor-pointer items-center justify-center text-text-soft transition-colors hover:text-[var(--color-text)]"
132
+ aria-label={revealed ? 'Hide password' : 'Show password'}
133
+ aria-pressed={revealed}
134
+ >
135
+ {#if revealed}
136
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
137
+ <path
138
+ stroke-linecap="round"
139
+ stroke-linejoin="round"
140
+ stroke-width="2"
141
+ d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
142
+ />
143
+ </svg>
144
+ {:else}
145
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
146
+ <path
147
+ stroke-linecap="round"
148
+ stroke-linejoin="round"
149
+ stroke-width="2"
150
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
151
+ />
152
+ <path
153
+ stroke-linecap="round"
154
+ stroke-linejoin="round"
155
+ stroke-width="2"
156
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
157
+ />
158
+ </svg>
159
+ {/if}
160
+ </button>
161
+ {:else if iconAfter && type !== 'number'}
114
162
  <span
115
163
  class="absolute right-4 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none"
116
164
  >
@@ -158,22 +206,59 @@ function decrement() {
158
206
  {/if}
159
207
 
160
208
  <input
161
- {type}
209
+ type={effectiveType}
162
210
  {name}
163
211
  {id}
164
212
  {placeholder}
165
213
  {disabled}
166
214
  {autocomplete}
215
+ min={type === 'number' ? min : undefined}
216
+ max={type === 'number' ? max : undefined}
217
+ step={type === 'number' ? step : undefined}
167
218
  bind:value
168
219
  {oninput}
169
220
  class="{baseInputClasses} {focusClasses} {errorClasses} {disabled
170
221
  ? disabledClasses
171
222
  : ''} {className}"
172
223
  class:pl-12={iconBefore}
173
- class:pr-12={iconAfter || type === 'number'}
224
+ class:pr-12={iconAfter || showReveal || type === 'number'}
174
225
  />
175
226
 
176
- {#if iconAfter && type !== 'number'}
227
+ {#if showReveal}
228
+ <button
229
+ type="button"
230
+ onclick={() => (revealed = !revealed)}
231
+ class="absolute right-4 z-10 inline-flex cursor-pointer items-center justify-center text-text-soft transition-colors hover:text-[var(--color-text)]"
232
+ aria-label={revealed ? 'Hide password' : 'Show password'}
233
+ aria-pressed={revealed}
234
+ >
235
+ {#if revealed}
236
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
237
+ <path
238
+ stroke-linecap="round"
239
+ stroke-linejoin="round"
240
+ stroke-width="2"
241
+ d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
242
+ />
243
+ </svg>
244
+ {:else}
245
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
246
+ <path
247
+ stroke-linecap="round"
248
+ stroke-linejoin="round"
249
+ stroke-width="2"
250
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
251
+ />
252
+ <path
253
+ stroke-linecap="round"
254
+ stroke-linejoin="round"
255
+ stroke-width="2"
256
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
257
+ />
258
+ </svg>
259
+ {/if}
260
+ </button>
261
+ {:else if iconAfter && type !== 'number'}
177
262
  <span
178
263
  class="absolute right-4 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none"
179
264
  >
@@ -14,6 +14,14 @@ export interface Props {
14
14
  validate?: (value: string | number) => boolean | string;
15
15
  validationMessage?: string;
16
16
  showValidation?: boolean;
17
+ /** Password inputs only: show an eye toggle to reveal the value. Replaces iconAfter. */
18
+ revealable?: boolean;
19
+ /** Number inputs only: lower bound enforced by the stepper arrows. */
20
+ min?: number;
21
+ /** Number inputs only: upper bound enforced by the stepper arrows. */
22
+ max?: number;
23
+ /** Number inputs only: stepper arrow increment. @default 1 */
24
+ step?: number;
17
25
  class?: string;
18
26
  }
19
27
  declare const Input: import("svelte").Component<Props, {}, "value">;
@@ -1,5 +1,7 @@
1
- <script lang="ts">let { value = $bindable(), options, name, disabled = false, orientation = 'vertical', label, onchange, class: className = '' } = $props();
1
+ <script lang="ts">let { value = $bindable(), options, name, disabled = false, error = false, orientation = 'vertical', label, onchange, validationMessage, class: className = '' } = $props();
2
+ const hasError = $derived(error || !!validationMessage);
2
3
  const containerClasses = $derived(orientation === 'vertical' ? 'flex flex-col gap-3' : 'flex flex-row gap-4');
4
+ const borderClass = $derived(hasError ? 'border-[var(--color-error)]' : 'border-[var(--color-border-strong)]');
3
5
  export {};
4
6
  </script>
5
7
 
@@ -20,7 +22,7 @@ export {};
20
22
  {disabled}
21
23
  bind:group={value}
22
24
  onchange={onchange}
23
- class="w-6 h-6 rounded-[var(--radius-pill)] border-2 border-[var(--color-border-strong)] bg-transparent appearance-none transition-all duration-200 ease-[var(--ease-luxe)] cursor-pointer checked:border-[var(--color-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)] focus:ring-offset-2 focus:ring-offset-[var(--color-base-1)] relative {disabled ? 'cursor-not-allowed' : ''}"
25
+ class="w-6 h-6 rounded-[var(--radius-pill)] border-2 {borderClass} bg-transparent appearance-none transition-all duration-200 ease-[var(--ease-luxe)] cursor-pointer checked:border-[var(--color-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)] focus:ring-offset-2 focus:ring-offset-[var(--color-base-1)] relative {disabled ? 'cursor-not-allowed' : ''}"
24
26
  />
25
27
  {#if option.iconBefore}
26
28
  <span class="inline-flex items-center justify-center text-text-soft">
@@ -33,32 +35,40 @@ export {};
33
35
  </label>
34
36
  {/each}
35
37
  </div>
38
+ {#if validationMessage}
39
+ <p class="text-error text-xs mt-2">{validationMessage}</p>
40
+ {/if}
36
41
  </fieldset>
37
42
  {:else}
38
- <div class="{containerClasses} {className}">
39
- {#each options as option}
40
- {@const radioId = `${name}-${option.value}`}
41
- <label for={radioId} class="flex items-center gap-2 cursor-pointer {disabled ? 'opacity-50 cursor-not-allowed' : ''}">
42
- <input
43
- type="radio"
44
- id={radioId}
45
- {name}
46
- value={option.value}
47
- {disabled}
48
- bind:group={value}
49
- onchange={onchange}
50
- class="w-6 h-6 rounded-[var(--radius-pill)] border-2 border-[var(--color-border-strong)] bg-transparent appearance-none transition-all duration-200 ease-[var(--ease-luxe)] cursor-pointer checked:border-[var(--color-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)] focus:ring-offset-2 focus:ring-offset-[var(--color-base-1)] relative {disabled ? 'cursor-not-allowed' : ''}"
51
- />
52
- {#if option.iconBefore}
53
- <span class="inline-flex items-center justify-center text-text-soft">
54
- {@render option.iconBefore()}
43
+ <div>
44
+ <div class="{containerClasses} {className}">
45
+ {#each options as option}
46
+ {@const radioId = `${name}-${option.value}`}
47
+ <label for={radioId} class="flex items-center gap-2 cursor-pointer {disabled ? 'opacity-50 cursor-not-allowed' : ''}">
48
+ <input
49
+ type="radio"
50
+ id={radioId}
51
+ {name}
52
+ value={option.value}
53
+ {disabled}
54
+ bind:group={value}
55
+ onchange={onchange}
56
+ class="w-6 h-6 rounded-[var(--radius-pill)] border-2 {borderClass} bg-transparent appearance-none transition-all duration-200 ease-[var(--ease-luxe)] cursor-pointer checked:border-[var(--color-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)] focus:ring-offset-2 focus:ring-offset-[var(--color-base-1)] relative {disabled ? 'cursor-not-allowed' : ''}"
57
+ />
58
+ {#if option.iconBefore}
59
+ <span class="inline-flex items-center justify-center text-text-soft">
60
+ {@render option.iconBefore()}
61
+ </span>
62
+ {/if}
63
+ <span class="text-[var(--color-text)] text-sm select-none">
64
+ {option.label}
55
65
  </span>
56
- {/if}
57
- <span class="text-[var(--color-text)] text-sm select-none">
58
- {option.label}
59
- </span>
60
- </label>
61
- {/each}
66
+ </label>
67
+ {/each}
68
+ </div>
69
+ {#if validationMessage}
70
+ <p class="text-error text-xs mt-2">{validationMessage}</p>
71
+ {/if}
62
72
  </div>
63
73
  {/if}
64
74
 
@@ -7,9 +7,11 @@ export interface Props {
7
7
  }>;
8
8
  name: string;
9
9
  disabled?: boolean;
10
+ error?: boolean;
10
11
  orientation?: 'vertical' | 'horizontal';
11
12
  label?: string;
12
13
  onchange?: (event: Event) => void;
14
+ validationMessage?: string;
13
15
  class?: string;
14
16
  }
15
17
  declare const RadioGroup: import("svelte").Component<Props, {}, "value">;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
- let { value = $bindable(0), min = 0, max = 100, step = 1, disabled = false, label, showValue = true, id = createId('range'), name, oninput } = $props();
2
+ let { value = $bindable(0), min = 0, max = 100, step = 1, disabled = false, error = false, label, showValue = true, id = createId('range'), name, oninput, validationMessage } = $props();
3
+ const hasError = $derived(error || !!validationMessage);
3
4
  const percentage = $derived(max === min ? 0 : ((value - min) / (max - min)) * 100);
4
5
  </script>
5
6
 
@@ -32,8 +33,11 @@ const percentage = $derived(max === min ? 0 : ((value - min) / (max - min)) * 10
32
33
  aria-valuemax={max}
33
34
  aria-valuenow={value}
34
35
  class="w-full h-2 rounded-pill appearance-none cursor-pointer transition-opacity duration-200 {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
35
- style="background: linear-gradient(to right, var(--color-accent) 0%, var(--color-accent) {percentage}%, var(--color-base-3) {percentage}%, var(--color-base-3) 100%); border: 1px solid var(--color-border);"
36
+ style="background: linear-gradient(to right, var(--color-accent) 0%, var(--color-accent) {percentage}%, var(--color-base-3) {percentage}%, var(--color-base-3) 100%); border: 1px solid {hasError ? 'var(--color-error)' : 'var(--color-border)'};"
36
37
  />
38
+ {#if validationMessage}
39
+ <p class="text-error text-xs mt-1.5">{validationMessage}</p>
40
+ {/if}
37
41
  </div>
38
42
 
39
43
  <style>
@@ -4,11 +4,13 @@ interface Props {
4
4
  max?: number;
5
5
  step?: number;
6
6
  disabled?: boolean;
7
+ error?: boolean;
7
8
  label?: string;
8
9
  showValue?: boolean;
9
10
  id?: string;
10
11
  name?: string;
11
12
  oninput?: (event: Event) => void;
13
+ validationMessage?: string;
12
14
  }
13
15
  declare const RangeSlider: import("svelte").Component<Props, {}, "value">;
14
16
  type RangeSlider = ReturnType<typeof RangeSlider>;
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
2
  import { baseInputClasses, focusClasses, disabledClasses, } from './formClasses';
3
- let { value = $bindable(), options, placeholder, disabled = false, error = false, label, id = createId('select'), name, autocomplete, onchange, iconBefore, class: className = '', } = $props();
3
+ let { value = $bindable(), options, placeholder, disabled = false, error = false, label, id = createId('select'), name, autocomplete, onchange, iconBefore, validationMessage, class: className = '', } = $props();
4
4
  const selectClasses = `${baseInputClasses} pr-12 appearance-none bg-no-repeat bg-[right_1rem_center] bg-[length:16px] cursor-pointer select-with-chevron`;
5
- const errorClasses = $derived(error ? 'error-state' : '');
5
+ const hasError = $derived(error || !!validationMessage);
6
+ const errorClasses = $derived(hasError ? 'error-state' : '');
6
7
  </script>
7
8
 
8
9
  {#if label}
@@ -42,39 +43,47 @@ const errorClasses = $derived(error ? 'error-state' : '');
42
43
  {/each}
43
44
  </select>
44
45
  </div>
46
+ {#if validationMessage}
47
+ <p class="text-error text-xs mt-1.5">{validationMessage}</p>
48
+ {/if}
45
49
  </div>
46
50
  {:else}
47
- <div class="relative">
48
- {#if iconBefore}
49
- <span
50
- class="absolute left-4 top-1/2 -translate-y-1/2 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none"
51
- >
52
- {@render iconBefore()}
53
- </span>
54
- {/if}
55
-
56
- <select
57
- {id}
58
- name={name}
59
- {disabled}
60
- {autocomplete}
61
- bind:value
62
- {onchange}
63
- class="{selectClasses} {focusClasses} {errorClasses} {disabled
64
- ? disabledClasses
65
- : ''} {className}"
66
- class:pl-12={iconBefore}
67
- >
68
- {#if placeholder}
69
- <option disabled value="" aria-hidden="true" hidden
70
- >{placeholder}</option
51
+ <div>
52
+ <div class="relative">
53
+ {#if iconBefore}
54
+ <span
55
+ class="absolute left-4 top-1/2 -translate-y-1/2 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none"
71
56
  >
57
+ {@render iconBefore()}
58
+ </span>
72
59
  {/if}
73
60
 
74
- {#each options as option}
75
- <option value={option.value}>{option.label}</option>
76
- {/each}
77
- </select>
61
+ <select
62
+ {id}
63
+ name={name}
64
+ {disabled}
65
+ {autocomplete}
66
+ bind:value
67
+ {onchange}
68
+ class="{selectClasses} {focusClasses} {errorClasses} {disabled
69
+ ? disabledClasses
70
+ : ''} {className}"
71
+ class:pl-12={iconBefore}
72
+ >
73
+ {#if placeholder}
74
+ <option disabled value="" aria-hidden="true" hidden
75
+ >{placeholder}</option
76
+ >
77
+ {/if}
78
+
79
+ {#each options as option}
80
+ <option value={option.value}>{option.label}</option>
81
+ {/each}
82
+ </select>
83
+ </div>
84
+ {#if validationMessage}
85
+ <p class="text-error text-xs mt-1.5">{validationMessage}</p>
86
+ {/if}
78
87
  </div>
79
88
  {/if}
80
89
 
@@ -13,6 +13,7 @@ export interface Props {
13
13
  autocomplete?: HTMLSelectElement['autocomplete'];
14
14
  onchange?: (event: Event) => void;
15
15
  iconBefore?: import('svelte').Snippet;
16
+ validationMessage?: string;
16
17
  class?: string;
17
18
  }
18
19
  declare const Select: import("svelte").Component<Props, {}, "value">;
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
2
  import Dropdown from '../overlays/Dropdown.svelte';
3
3
  import { baseInputClasses, focusClasses, disabledClasses, } from './formClasses';
4
- let { value = $bindable(), options, placeholder = 'Select an option', disabled = false, error = false, label, id = createId('selectmenu'), name, iconBefore, onchange, class: className = '', } = $props();
4
+ let { value = $bindable(), options, placeholder = 'Select an option', disabled = false, error = false, label, id = createId('selectmenu'), name, iconBefore, onchange, validationMessage, class: className = '', } = $props();
5
+ const hasError = $derived(error || !!validationMessage);
5
6
  let open = $state(false);
6
7
  let highlightedIndex = $state(-1);
7
8
  let listboxElement = $state();
@@ -105,7 +106,7 @@ const chevronIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
105
106
  const triggerClasses = $derived(`
106
107
  ${baseInputClasses}
107
108
  ${focusClasses}
108
- ${error ? 'error-state' : ''}
109
+ ${hasError ? 'error-state' : ''}
109
110
  ${disabled ? disabledClasses : ''}
110
111
  ${iconBefore ? 'pl-12' : ''}
111
112
  ${className}
@@ -189,6 +190,10 @@ const triggerClasses = $derived(`
189
190
  {/each}
190
191
  </div>
191
192
  </Dropdown>
193
+
194
+ {#if validationMessage}
195
+ <p class="text-error text-xs mt-1.5">{validationMessage}</p>
196
+ {/if}
192
197
  </div>
193
198
 
194
199
  <style>
@@ -12,6 +12,7 @@ export interface Props {
12
12
  name?: string;
13
13
  iconBefore?: import('svelte').Snippet;
14
14
  onchange?: (value: string) => void;
15
+ validationMessage?: string;
15
16
  class?: string;
16
17
  }
17
18
  declare const SelectMenu: import("svelte").Component<Props, {}, "value">;
@@ -1,30 +1,36 @@
1
1
  <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
- let { checked = $bindable(false), disabled = false, label, id = createId('switch'), name, onchange, iconBefore, class: className = '' } = $props();
2
+ let { checked = $bindable(false), disabled = false, error = false, label, id = createId('switch'), name, onchange, iconBefore, validationMessage, class: className = '' } = $props();
3
+ const hasError = $derived(error || !!validationMessage);
3
4
  </script>
4
5
 
5
- <label for={id} class="flex items-center gap-2 cursor-pointer {disabled ? 'opacity-50 cursor-not-allowed' : ''} {className}">
6
- <input
7
- type="checkbox"
8
- role="switch"
9
- aria-checked={checked}
10
- {id}
11
- name={name}
12
- {disabled}
13
- bind:checked
14
- onchange={onchange}
15
- class="w-14 h-7 rounded-pill bg-base-3 border border-border appearance-none transition-all duration-300 ease-luxe cursor-pointer checked:bg-accent checked:accent-glow focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-base-1 relative {disabled ? 'cursor-not-allowed' : ''}"
16
- />
17
- {#if iconBefore}
18
- <span class="inline-flex items-center justify-center text-text-soft">
19
- {@render iconBefore()}
20
- </span>
6
+ <div>
7
+ <label for={id} class="flex items-center gap-2 cursor-pointer {disabled ? 'opacity-50 cursor-not-allowed' : ''} {className}">
8
+ <input
9
+ type="checkbox"
10
+ role="switch"
11
+ aria-checked={checked}
12
+ {id}
13
+ name={name}
14
+ {disabled}
15
+ bind:checked
16
+ onchange={onchange}
17
+ class="w-14 h-7 rounded-pill bg-base-3 border appearance-none transition-all duration-300 ease-luxe cursor-pointer checked:bg-accent checked:accent-glow focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-base-1 relative {hasError ? 'border-error' : 'border-border'} {disabled ? 'cursor-not-allowed' : ''}"
18
+ />
19
+ {#if iconBefore}
20
+ <span class="inline-flex items-center justify-center text-text-soft">
21
+ {@render iconBefore()}
22
+ </span>
23
+ {/if}
24
+ {#if label}
25
+ <span class="text-text text-sm select-none">
26
+ {label}
27
+ </span>
28
+ {/if}
29
+ </label>
30
+ {#if validationMessage}
31
+ <p class="text-error text-xs mt-1.5 ml-16">{validationMessage}</p>
21
32
  {/if}
22
- {#if label}
23
- <span class="text-text text-sm select-none">
24
- {label}
25
- </span>
26
- {/if}
27
- </label>
33
+ </div>
28
34
 
29
35
  <style>
30
36
  input[type="checkbox"]::after {
@@ -37,9 +43,11 @@ let { checked = $bindable(false), disabled = false, label, id = createId('switch
37
43
  height: 1.25rem;
38
44
  border-radius: var(--radius-pill);
39
45
  background: var(--color-text);
46
+ background-image: none;
40
47
  transition: all 0.3s var(--ease-luxe);
41
48
  }
42
49
 
43
50
  input[type="checkbox"]:checked::after {
44
51
  transform: translate(1.75rem, -50%);
52
+ background-image: none;
45
53
  }</style>
@@ -1,11 +1,13 @@
1
1
  export interface Props {
2
2
  checked?: boolean;
3
3
  disabled?: boolean;
4
+ error?: boolean;
4
5
  label?: string;
5
6
  id?: string;
6
7
  name?: string;
7
8
  onchange?: (event: Event) => void;
8
9
  iconBefore?: import('svelte').Snippet;
10
+ validationMessage?: string;
9
11
  class?: string;
10
12
  }
11
13
  declare const Switch: import("svelte").Component<Props, {}, "checked">;
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
2
  import { baseInputClasses, focusClasses, disabledClasses } from './formClasses';
3
- let { value = $bindable(), placeholder, rows = 4, disabled = false, error = false, label, id = createId('textarea'), name, autocomplete, oninput, iconBefore, class: className = '' } = $props();
3
+ let { value = $bindable(), placeholder, rows = 4, disabled = false, error = false, label, id = createId('textarea'), name, autocomplete, oninput, iconBefore, validationMessage, class: className = '' } = $props();
4
+ const hasError = $derived(error || !!validationMessage);
4
5
  // Icon spacing: pl-12 (3rem) = left-4 (1rem) icon position + ~2rem for icon width and spacing
5
6
  // top-4 keeps icon fixed at top when textarea is resized
6
7
  // This ensures text doesn't overlap with the absolutely positioned icon
7
8
  const textareaClasses = `${baseInputClasses} resize-y min-h-[100px]`;
8
- const errorClasses = $derived(error ? 'error-state' : '');
9
+ const errorClasses = $derived(hasError ? 'error-state' : '');
9
10
  </script>
10
11
 
11
12
  {#if label}
@@ -32,25 +33,33 @@ const errorClasses = $derived(error ? 'error-state' : '');
32
33
  class:pl-12={iconBefore}
33
34
  ></textarea>
34
35
  </div>
36
+ {#if validationMessage}
37
+ <p class="text-error text-xs mt-1.5">{validationMessage}</p>
38
+ {/if}
35
39
  </div>
36
40
  {:else}
37
- <div class="relative">
38
- {#if iconBefore}
39
- <span class="absolute left-4 top-4 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none">
40
- {@render iconBefore()}
41
- </span>
41
+ <div>
42
+ <div class="relative">
43
+ {#if iconBefore}
44
+ <span class="absolute left-4 top-4 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none">
45
+ {@render iconBefore()}
46
+ </span>
47
+ {/if}
48
+ <textarea
49
+ {id}
50
+ name={name}
51
+ {placeholder}
52
+ {disabled}
53
+ {rows}
54
+ {autocomplete}
55
+ bind:value
56
+ oninput={oninput}
57
+ class="{textareaClasses} {focusClasses} {errorClasses} {disabled ? disabledClasses : ''} {className}"
58
+ class:pl-12={iconBefore}
59
+ ></textarea>
60
+ </div>
61
+ {#if validationMessage}
62
+ <p class="text-error text-xs mt-1.5">{validationMessage}</p>
42
63
  {/if}
43
- <textarea
44
- {id}
45
- name={name}
46
- {placeholder}
47
- {disabled}
48
- {rows}
49
- {autocomplete}
50
- bind:value
51
- oninput={oninput}
52
- class="{textareaClasses} {focusClasses} {errorClasses} {disabled ? disabledClasses : ''} {className}"
53
- class:pl-12={iconBefore}
54
- ></textarea>
55
64
  </div>
56
65
  {/if}
@@ -10,6 +10,7 @@ export interface Props {
10
10
  autocomplete?: HTMLTextAreaElement['autocomplete'];
11
11
  oninput?: (event: Event) => void;
12
12
  iconBefore?: import('svelte').Snippet;
13
+ validationMessage?: string;
13
14
  class?: string;
14
15
  }
15
16
  declare const Textarea: import("svelte").Component<Props, {}, "value">;
@@ -2,7 +2,7 @@
2
2
  import { baseInputClasses, focusClasses, disabledClasses, } from './formClasses';
3
3
  import TimePickerGrid from './TimePickerGrid.svelte';
4
4
  import Dropdown from '../overlays/Dropdown.svelte';
5
- let { variant = 'glass', value = $bindable(), min, max, step = 60, placeholder = 'Select time', disabled = false, error = false, label, id = createId('timepicker'), name, autocomplete, is24Hour = true, onchange, class: className = '', } = $props();
5
+ let { variant = 'glass', value = $bindable(), min, max, step = 60, placeholder = 'Select time', disabled = false, error = false, label, id = createId('timepicker'), name, autocomplete, is24Hour = true, onchange, validationMessage, class: className = '', } = $props();
6
6
  let open = $state(false);
7
7
  let pickerElement = $state();
8
8
  let inputElement = $state();
@@ -108,7 +108,8 @@ $effect(() => {
108
108
  });
109
109
  }
110
110
  });
111
- const errorClasses = $derived(error ? 'error-state' : '');
111
+ const hasError = $derived(error || !!validationMessage);
112
+ const errorClasses = $derived(hasError ? 'error-state' : '');
112
113
  </script>
113
114
 
114
115
  <div class="time-picker-wrapper w-full">
@@ -181,6 +182,10 @@ const errorClasses = $derived(error ? 'error-state' : '');
181
182
  />
182
183
  </div>
183
184
  </Dropdown>
185
+
186
+ {#if validationMessage}
187
+ <p class="text-error text-xs mt-1.5">{validationMessage}</p>
188
+ {/if}
184
189
  </div>
185
190
 
186
191
  <style>
@@ -13,6 +13,7 @@ export interface Props {
13
13
  autocomplete?: HTMLInputElement['autocomplete'];
14
14
  is24Hour?: boolean;
15
15
  onchange?: (event: Event) => void;
16
+ validationMessage?: string;
16
17
  class?: string;
17
18
  }
18
19
  declare const TimePicker: import("svelte").Component<Props, {}, "value">;