@webamoki/web-svelte 0.7.0 → 0.7.2

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 (28) hide show
  1. package/dist/components/form/IconInputWrapper.svelte +39 -0
  2. package/dist/components/form/IconInputWrapper.svelte.d.ts +20 -0
  3. package/dist/components/form/fields/DateField.svelte +12 -4
  4. package/dist/components/form/fields/DateField.svelte.d.ts +3 -1
  5. package/dist/components/form/fields/MessageField.svelte +19 -10
  6. package/dist/components/form/fields/MessageField.svelte.d.ts +2 -0
  7. package/dist/components/form/fields/NumberField.svelte +26 -3
  8. package/dist/components/form/fields/NumberField.svelte.d.ts +3 -0
  9. package/dist/components/form/fields/PasswordField.svelte +14 -5
  10. package/dist/components/form/fields/PasswordField.svelte.d.ts +3 -0
  11. package/dist/components/form/fields/SelectField.svelte +13 -5
  12. package/dist/components/form/fields/SelectField.svelte.d.ts +2 -0
  13. package/dist/components/form/fields/SelectMultiField.svelte +13 -5
  14. package/dist/components/form/fields/SelectMultiField.svelte.d.ts +2 -0
  15. package/dist/components/form/fields/TextField.svelte +11 -2
  16. package/dist/components/form/fields/TextField.svelte.d.ts +2 -0
  17. package/dist/components/form/fields/TextFieldNullable.svelte +22 -2
  18. package/dist/components/form/fields/TextFieldNullable.svelte.d.ts +2 -0
  19. package/dist/components/form/fields/TimeField.svelte +27 -4
  20. package/dist/components/form/fields/TimeField.svelte.d.ts +4 -1
  21. package/dist/components/form/index.d.ts +3 -2
  22. package/dist/components/form/index.js +3 -2
  23. package/dist/utils/email/README.md +209 -0
  24. package/dist/utils/email/index.d.ts +1 -0
  25. package/dist/utils/email/index.js +1 -0
  26. package/dist/utils/email/ses.d.ts +22 -0
  27. package/dist/utils/email/ses.js +95 -0
  28. package/package.json +10 -1
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import type { Component, Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ icon?: Component;
6
+ /**
7
+ * Position of the icon vertically.
8
+ * - 'center': Centers vertically (for single-line inputs)
9
+ * - 'top': Aligns to top with padding (for multi-line inputs like textarea)
10
+ */
11
+ iconPosition?: 'center' | 'top';
12
+ /**
13
+ * Whether this wrapper should grow to fill flex container (used in layouts with buttons)
14
+ */
15
+ flex?: boolean;
16
+ children: Snippet<[{ class: string }]>;
17
+ }
18
+
19
+ let { icon, iconPosition = 'center', flex = false, children }: Props = $props();
20
+
21
+ const iconClass =
22
+ iconPosition === 'top'
23
+ ? 'pointer-events-none absolute top-4 left-4 text-gray-500'
24
+ : 'pointer-events-none absolute top-1/2 left-4 -translate-y-1/2 text-gray-500';
25
+
26
+ const wrapperClass = flex ? 'relative flex-1' : 'relative';
27
+ </script>
28
+
29
+ {#if icon}
30
+ {@const Icon = icon}
31
+ <div class={wrapperClass}>
32
+ <div class={iconClass}>
33
+ <Icon class="size-5" />
34
+ </div>
35
+ {@render children({ class: 'pl-12' })}
36
+ </div>
37
+ {:else}
38
+ {@render children({ class: '' })}
39
+ {/if}
@@ -0,0 +1,20 @@
1
+ import type { Component, Snippet } from 'svelte';
2
+ interface Props {
3
+ icon?: Component;
4
+ /**
5
+ * Position of the icon vertically.
6
+ * - 'center': Centers vertically (for single-line inputs)
7
+ * - 'top': Aligns to top with padding (for multi-line inputs like textarea)
8
+ */
9
+ iconPosition?: 'center' | 'top';
10
+ /**
11
+ * Whether this wrapper should grow to fill flex container (used in layouts with buttons)
12
+ */
13
+ flex?: boolean;
14
+ children: Snippet<[{
15
+ class: string;
16
+ }]>;
17
+ }
18
+ declare const IconInputWrapper: Component<Props, {}, "">;
19
+ type IconInputWrapper = ReturnType<typeof IconInputWrapper>;
20
+ export default IconInputWrapper;
@@ -1,14 +1,18 @@
1
1
  <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
2
- import type { FormPath } from 'sveltekit-superforms';
3
- import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
2
+ import IconInputWrapper from '../IconInputWrapper.svelte';
4
3
  import { Input } from '../../../shadcn/components/ui/input/index.js';
4
+ import { cn } from '../../../shadcn/utils.js';
5
5
  import { CalendarDate } from '@internationalized/date';
6
+ import type { Component } from 'svelte';
7
+ import type { FormPath } from 'sveltekit-superforms';
8
+ import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
6
9
 
7
10
  interface Props extends FieldWrapperProps<T, U, M> {
8
11
  value?: CalendarDate;
9
12
  class?: string;
13
+ icon?: Component;
10
14
  }
11
- let { value = $bindable(), class: className, ...fieldProps }: Props = $props();
15
+ let { value = $bindable(), class: className, icon, ...fieldProps }: Props = $props();
12
16
 
13
17
  // Getter: format CalendarDate → string (YYYY-MM-DD)
14
18
  function get(): string {
@@ -34,6 +38,10 @@
34
38
 
35
39
  <FieldWrapper {...fieldProps}>
36
40
  {#snippet formElem(props)}
37
- <Input type="date" bind:value={get, set} class={className} {...props} />
41
+ <IconInputWrapper {icon}>
42
+ {#snippet children({ class: iconClass })}
43
+ <Input type="date" bind:value={get, set} class={cn(iconClass, className)} {...props} />
44
+ {/snippet}
45
+ </IconInputWrapper>
38
46
  {/snippet}
39
47
  </FieldWrapper>
@@ -1,10 +1,12 @@
1
+ import { CalendarDate } from '@internationalized/date';
2
+ import type { Component } from 'svelte';
1
3
  import type { FormPath } from 'sveltekit-superforms';
2
4
  import { type FieldWrapperProps } from '../FieldWrapper.svelte';
3
- import { CalendarDate } from '@internationalized/date';
4
5
  declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
5
6
  props: FieldWrapperProps<T, U, M> & {
6
7
  value?: CalendarDate;
7
8
  class?: string;
9
+ icon?: Component;
8
10
  };
9
11
  exports: {};
10
12
  bindings: "value";
@@ -1,8 +1,11 @@
1
1
  <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
2
- import type { FormPath } from 'sveltekit-superforms';
3
- import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
2
+ import IconInputWrapper from '../IconInputWrapper.svelte';
4
3
  import { Textarea } from '../../../shadcn/components/ui/textarea/index.js';
4
+ import { cn } from '../../../shadcn/utils.js';
5
5
  import { Lock, LockOpen } from '@lucide/svelte';
6
+ import type { Component } from 'svelte';
7
+ import type { FormPath } from 'sveltekit-superforms';
8
+ import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
6
9
 
7
10
  interface Props extends FieldWrapperProps<T, U, M> {
8
11
  value?: string;
@@ -11,6 +14,7 @@
11
14
  defaultHeight?: number;
12
15
  showLock?: boolean;
13
16
  defaultLocked?: boolean;
17
+ icon?: Component;
14
18
  }
15
19
  let {
16
20
  value = $bindable(),
@@ -19,6 +23,7 @@
19
23
  defaultHeight = 100,
20
24
  showLock = true,
21
25
  defaultLocked = false,
26
+ icon,
22
27
  ...fieldProps
23
28
  }: Props = $props();
24
29
 
@@ -28,14 +33,18 @@
28
33
  <FieldWrapper {...fieldProps}>
29
34
  {#snippet formElem(props)}
30
35
  <div class="flex w-full items-start gap-2">
31
- <!-- Textarea itself -->
32
- <Textarea
33
- bind:value
34
- class={`${className || ''} ${locked ? 'resize-none' : 'resize-y'}`}
35
- {placeholder}
36
- style={defaultHeight ? `height: ${defaultHeight}px` : undefined}
37
- {...props}
38
- />
36
+ <!-- Textarea itself with optional left icon -->
37
+ <IconInputWrapper {icon} iconPosition="top" flex>
38
+ {#snippet children({ class: iconClass })}
39
+ <Textarea
40
+ bind:value
41
+ class={cn(iconClass, className || '', locked ? 'resize-none' : 'resize-y')}
42
+ {placeholder}
43
+ style={defaultHeight ? `height: ${defaultHeight}px` : undefined}
44
+ {...props}
45
+ />
46
+ {/snippet}
47
+ </IconInputWrapper>
39
48
 
40
49
  <!-- Lock/unlock button -->
41
50
  {#if showLock}
@@ -1,3 +1,4 @@
1
+ import type { Component } from 'svelte';
1
2
  import type { FormPath } from 'sveltekit-superforms';
2
3
  import { type FieldWrapperProps } from '../FieldWrapper.svelte';
3
4
  declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
@@ -8,6 +9,7 @@ declare function $$render<T extends Record<string, unknown>, U extends FormPath<
8
9
  defaultHeight?: number;
9
10
  showLock?: boolean;
10
11
  defaultLocked?: boolean;
12
+ icon?: Component;
11
13
  };
12
14
  exports: {};
13
15
  bindings: "value";
@@ -1,18 +1,41 @@
1
1
  <script lang="ts" generics="V,T extends Record<string, unknown>, U extends FormPath<T>, M">
2
+ import IconInputWrapper from '../IconInputWrapper.svelte';
3
+ import { Input } from '../../../shadcn/components/ui/input/index.js';
4
+ import { cn } from '../../../shadcn/utils.js';
5
+ import type { Component } from 'svelte';
2
6
  import type { FormPath } from 'sveltekit-superforms';
3
7
  import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
4
- import { Input } from '../../../shadcn/components/ui/input/index.js';
5
8
 
6
9
  interface Props extends FieldWrapperProps<T, U, M> {
7
10
  value?: V;
8
11
  class?: string;
9
12
  step?: HTMLInputElement['step'];
13
+ placeholder?: string;
14
+ icon?: Component;
10
15
  }
11
- let { value = $bindable(), class: className, step, ...fieldProps }: Props = $props();
16
+ let {
17
+ value = $bindable(),
18
+ class: className,
19
+ step,
20
+ placeholder,
21
+ icon,
22
+ ...fieldProps
23
+ }: Props = $props();
12
24
  </script>
13
25
 
14
26
  <FieldWrapper {...fieldProps}>
15
27
  {#snippet formElem(props)}
16
- <Input type="number" bind:value class={className} {step} {...props} />
28
+ <IconInputWrapper {icon}>
29
+ {#snippet children({ class: iconClass })}
30
+ <Input
31
+ type="number"
32
+ bind:value
33
+ class={cn(iconClass, className)}
34
+ {placeholder}
35
+ {step}
36
+ {...props}
37
+ />
38
+ {/snippet}
39
+ </IconInputWrapper>
17
40
  {/snippet}
18
41
  </FieldWrapper>
@@ -1,3 +1,4 @@
1
+ import type { Component } from 'svelte';
1
2
  import type { FormPath } from 'sveltekit-superforms';
2
3
  import { type FieldWrapperProps } from '../FieldWrapper.svelte';
3
4
  declare function $$render<V, T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
@@ -5,6 +6,8 @@ declare function $$render<V, T extends Record<string, unknown>, U extends FormPa
5
6
  value?: V;
6
7
  class?: string;
7
8
  step?: HTMLInputElement["step"];
9
+ placeholder?: string;
10
+ icon?: Component;
8
11
  };
9
12
  exports: {};
10
13
  bindings: "value";
@@ -1,13 +1,18 @@
1
1
  <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
2
- import type { FormPath } from 'sveltekit-superforms';
3
- import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
2
+ import IconInputWrapper from '../IconInputWrapper.svelte';
4
3
  import { Input } from '../../../shadcn/components/ui/input/index.js';
4
+ import { cn } from '../../../shadcn/utils.js';
5
5
  import { Eye, EyeOff } from '@lucide/svelte';
6
+ import type { Component } from 'svelte';
7
+ import type { FormPath } from 'sveltekit-superforms';
8
+ import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
6
9
 
7
10
  interface Props extends FieldWrapperProps<T, U, M> {
8
11
  value?: string;
12
+ class?: string;
13
+ icon?: Component;
9
14
  }
10
- let { value = $bindable(), ...fieldProps }: Props = $props();
15
+ let { value = $bindable(), class: className, icon, ...fieldProps }: Props = $props();
11
16
 
12
17
  let show = $state(false);
13
18
  let inputType = $derived(show ? 'text' : 'password');
@@ -16,8 +21,12 @@
16
21
  <FieldWrapper {...fieldProps}>
17
22
  {#snippet formElem(props)}
18
23
  <div class="flex w-full items-stretch gap-2">
19
- <!-- Input itself -->
20
- <Input type={inputType} bind:value {...props} />
24
+ <!-- Input itself with optional left icon -->
25
+ <IconInputWrapper {icon} flex>
26
+ {#snippet children({ class: iconClass })}
27
+ <Input type={inputType} bind:value class={cn(iconClass, className)} {...props} />
28
+ {/snippet}
29
+ </IconInputWrapper>
21
30
 
22
31
  <!-- Show/hide button outside input -->
23
32
  <button
@@ -1,8 +1,11 @@
1
+ import type { Component } from 'svelte';
1
2
  import type { FormPath } from 'sveltekit-superforms';
2
3
  import { type FieldWrapperProps } from '../FieldWrapper.svelte';
3
4
  declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
4
5
  props: FieldWrapperProps<T, U, M> & {
5
6
  value?: string;
7
+ class?: string;
8
+ icon?: Component;
6
9
  };
7
10
  exports: {};
8
11
  bindings: "value";
@@ -2,6 +2,7 @@
2
2
  lang="ts"
3
3
  generics="I,V, K extends string | number,T extends Record<string, unknown>, U extends FormPath<T>, M"
4
4
  >
5
+ import IconInputWrapper from '../IconInputWrapper.svelte';
5
6
  import {
6
7
  Select,
7
8
  SelectContent,
@@ -11,6 +12,7 @@
11
12
  SelectTrigger
12
13
  } from '../../../shadcn/components/ui/select/index.js';
13
14
  import { cn } from '../../../shadcn/utils.js';
15
+ import type { Component } from 'svelte';
14
16
  import type { FormPath } from 'sveltekit-superforms';
15
17
  import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
16
18
 
@@ -23,6 +25,7 @@
23
25
  onchange?: (value: V | undefined) => void;
24
26
  class?: string;
25
27
  placeholder: string;
28
+ icon?: Component;
26
29
  }
27
30
  let {
28
31
  value = $bindable(undefined),
@@ -33,6 +36,7 @@
33
36
  onchange,
34
37
  placeholder,
35
38
  items,
39
+ icon,
36
40
  ...fieldProps
37
41
  }: Props = $props();
38
42
  let valueToItem: Map<V, I> = new Map(items.map((item) => [getValue(item), item] as const));
@@ -63,11 +67,15 @@
63
67
  <FieldWrapper {...fieldProps}>
64
68
  {#snippet formElem(props)}
65
69
  <Select type="single" {...props} bind:value={getKeyFromValue, setValueFromKey}>
66
- <SelectTrigger class={cn('w-full cursor-pointer truncate', className)}>
67
- <span class="block truncate">
68
- {value ? getLabel(valueToItem.get(value)!) : placeholder}
69
- </span>
70
- </SelectTrigger>
70
+ <IconInputWrapper {icon}>
71
+ {#snippet children({ class: iconClass })}
72
+ <SelectTrigger class={cn('w-full cursor-pointer truncate', iconClass, className)}>
73
+ <span class="block truncate">
74
+ {value ? getLabel(valueToItem.get(value)!) : placeholder}
75
+ </span>
76
+ </SelectTrigger>
77
+ {/snippet}
78
+ </IconInputWrapper>
71
79
  <SelectContent>
72
80
  <SelectGroup>
73
81
  <SelectLabel>{fieldProps.label}</SelectLabel>
@@ -1,3 +1,4 @@
1
+ import type { Component } from 'svelte';
1
2
  import type { FormPath } from 'sveltekit-superforms';
2
3
  import { type FieldWrapperProps } from '../FieldWrapper.svelte';
3
4
  declare function $$render<I, V, K extends string | number, T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
@@ -10,6 +11,7 @@ declare function $$render<I, V, K extends string | number, T extends Record<stri
10
11
  onchange?: (value: V | undefined) => void;
11
12
  class?: string;
12
13
  placeholder: string;
14
+ icon?: Component;
13
15
  };
14
16
  exports: {};
15
17
  bindings: "value";
@@ -2,6 +2,7 @@
2
2
  lang="ts"
3
3
  generics="I,V, K extends string | number,T extends Record<string, unknown>, U extends FormPath<T>, M"
4
4
  >
5
+ import IconInputWrapper from '../IconInputWrapper.svelte';
5
6
  import {
6
7
  Select,
7
8
  SelectContent,
@@ -11,6 +12,7 @@
11
12
  SelectTrigger
12
13
  } from '../../../shadcn/components/ui/select/index.js';
13
14
  import { cn } from '../../../shadcn/utils.js';
15
+ import type { Component } from 'svelte';
14
16
  import type { FormPath } from 'sveltekit-superforms';
15
17
  import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
16
18
 
@@ -23,6 +25,7 @@
23
25
  onchange?: (value: V[]) => void;
24
26
  class?: string;
25
27
  placeholder: string;
28
+ icon?: Component;
26
29
  }
27
30
  let {
28
31
  values = $bindable([]),
@@ -33,6 +36,7 @@
33
36
  onchange,
34
37
  placeholder,
35
38
  items,
39
+ icon,
36
40
  ...fieldProps
37
41
  }: Props = $props();
38
42
  let valueToItem: Map<V, I> = new Map(items.map((item) => [getValue(item), item] as const));
@@ -70,11 +74,15 @@
70
74
  <FieldWrapper {...fieldProps}>
71
75
  {#snippet formElem(props)}
72
76
  <Select type="multiple" {...props} bind:value={getKeyFromValue, setValueFromKey}>
73
- <SelectTrigger class={cn('w-full cursor-pointer truncate', className)}>
74
- <span class="block truncate">
75
- {getPreview()}
76
- </span>
77
- </SelectTrigger>
77
+ <IconInputWrapper {icon}>
78
+ {#snippet children({ class: iconClass })}
79
+ <SelectTrigger class={cn('w-full cursor-pointer truncate', iconClass, className)}>
80
+ <span class="block truncate">
81
+ {getPreview()}
82
+ </span>
83
+ </SelectTrigger>
84
+ {/snippet}
85
+ </IconInputWrapper>
78
86
  <SelectContent>
79
87
  <SelectGroup>
80
88
  <SelectLabel>{fieldProps.label}</SelectLabel>
@@ -1,3 +1,4 @@
1
+ import type { Component } from 'svelte';
1
2
  import type { FormPath } from 'sveltekit-superforms';
2
3
  import { type FieldWrapperProps } from '../FieldWrapper.svelte';
3
4
  declare function $$render<I, V, K extends string | number, T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
@@ -10,6 +11,7 @@ declare function $$render<I, V, K extends string | number, T extends Record<stri
10
11
  onchange?: (value: V[]) => void;
11
12
  class?: string;
12
13
  placeholder: string;
14
+ icon?: Component;
13
15
  };
14
16
  exports: {};
15
17
  bindings: "values";
@@ -1,25 +1,34 @@
1
1
  <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
2
+ import IconInputWrapper from '../IconInputWrapper.svelte';
3
+ import { Input } from '../../../shadcn/components/ui/input/index.js';
4
+ import { cn } from '../../../shadcn/utils.js';
5
+ import type { Component } from 'svelte';
2
6
  import type { FormPath } from 'sveltekit-superforms';
3
7
  import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
4
- import { Input } from '../../../shadcn/components/ui/input/index.js';
5
8
 
6
9
  interface Props extends FieldWrapperProps<T, U, M> {
7
10
  value?: string;
8
11
  type?: HTMLInputElement['type'];
9
12
  class?: string;
10
13
  placeholder?: string;
14
+ icon?: Component;
11
15
  }
12
16
  let {
13
17
  value = $bindable(),
14
18
  type = 'text',
15
19
  class: className,
16
20
  placeholder,
21
+ icon,
17
22
  ...fieldProps
18
23
  }: Props = $props();
19
24
  </script>
20
25
 
21
26
  <FieldWrapper {...fieldProps}>
22
27
  {#snippet formElem(props)}
23
- <Input {type} bind:value class={className} {placeholder} {...props} />
28
+ <IconInputWrapper {icon}>
29
+ {#snippet children({ class: iconClass })}
30
+ <Input {type} bind:value class={cn(iconClass, className)} {placeholder} {...props} />
31
+ {/snippet}
32
+ </IconInputWrapper>
24
33
  {/snippet}
25
34
  </FieldWrapper>
@@ -1,3 +1,4 @@
1
+ import type { Component } from 'svelte';
1
2
  import type { FormPath } from 'sveltekit-superforms';
2
3
  import { type FieldWrapperProps } from '../FieldWrapper.svelte';
3
4
  declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
@@ -6,6 +7,7 @@ declare function $$render<T extends Record<string, unknown>, U extends FormPath<
6
7
  type?: HTMLInputElement["type"];
7
8
  class?: string;
8
9
  placeholder?: string;
10
+ icon?: Component;
9
11
  };
10
12
  exports: {};
11
13
  bindings: "value";
@@ -1,19 +1,23 @@
1
1
  <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
2
+ import { Input } from '../../../shadcn/components/ui/input/index.js';
3
+ import { cn } from '../../../shadcn/utils.js';
4
+ import type { Component } from 'svelte';
2
5
  import type { FormPath } from 'sveltekit-superforms';
3
6
  import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
4
- import { Input } from '../../../shadcn/components/ui/input/index.js';
5
7
 
6
8
  interface Props extends FieldWrapperProps<T, U, M> {
7
9
  value?: string | null;
8
10
  type?: HTMLInputElement['type'];
9
11
  class?: string;
10
12
  placeholder?: string;
13
+ icon?: Component;
11
14
  }
12
15
  let {
13
16
  value = $bindable(),
14
17
  type = 'text',
15
18
  class: className,
16
19
  placeholder,
20
+ icon,
17
21
  ...fieldProps
18
22
  }: Props = $props();
19
23
 
@@ -28,6 +32,22 @@
28
32
 
29
33
  <FieldWrapper {...fieldProps}>
30
34
  {#snippet formElem(props)}
31
- <Input {type} bind:value={get, set} class={className} {placeholder} {...props} />
35
+ {#if icon}
36
+ {@const Icon = icon}
37
+ <div class="relative">
38
+ <div class="pointer-events-none absolute top-1/2 left-4 -translate-y-1/2 text-gray-500">
39
+ <Icon class="size-5" />
40
+ </div>
41
+ <Input
42
+ {type}
43
+ bind:value={get, set}
44
+ class={cn('pl-12', className)}
45
+ {placeholder}
46
+ {...props}
47
+ />
48
+ </div>
49
+ {:else}
50
+ <Input {type} bind:value={get, set} class={className} {placeholder} {...props} />
51
+ {/if}
32
52
  {/snippet}
33
53
  </FieldWrapper>
@@ -1,3 +1,4 @@
1
+ import type { Component } from 'svelte';
1
2
  import type { FormPath } from 'sveltekit-superforms';
2
3
  import { type FieldWrapperProps } from '../FieldWrapper.svelte';
3
4
  declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
@@ -6,6 +7,7 @@ declare function $$render<T extends Record<string, unknown>, U extends FormPath<
6
7
  type?: HTMLInputElement["type"];
7
8
  class?: string;
8
9
  placeholder?: string;
10
+ icon?: Component;
9
11
  };
10
12
  exports: {};
11
13
  bindings: "value";
@@ -1,15 +1,27 @@
1
1
  <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>, M">
2
- import type { FormPath } from 'sveltekit-superforms';
3
- import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
2
+ import IconInputWrapper from '../IconInputWrapper.svelte';
4
3
  import { Input } from '../../../shadcn/components/ui/input/index.js';
4
+ import { cn } from '../../../shadcn/utils.js';
5
5
  import { Time } from '@internationalized/date';
6
+ import type { Component } from 'svelte';
7
+ import type { FormPath } from 'sveltekit-superforms';
8
+ import FieldWrapper, { type FieldWrapperProps } from '../FieldWrapper.svelte';
6
9
 
7
10
  interface Props extends FieldWrapperProps<T, U, M> {
8
11
  value?: Time;
9
12
  class?: string;
10
13
  step?: HTMLInputElement['step'];
14
+ placeholder?: string;
15
+ icon?: Component;
11
16
  }
12
- let { value = $bindable(), class: className, step, ...fieldProps }: Props = $props();
17
+ let {
18
+ value = $bindable(),
19
+ class: className,
20
+ step,
21
+ placeholder,
22
+ icon,
23
+ ...fieldProps
24
+ }: Props = $props();
13
25
  // Getter: format Time as string depending on step
14
26
  function get(): string {
15
27
  if (!value) return '00:00:00';
@@ -44,6 +56,17 @@
44
56
 
45
57
  <FieldWrapper {...fieldProps}>
46
58
  {#snippet formElem(props)}
47
- <Input type="time" bind:value={get, set} class={className} {step} {...props} />
59
+ <IconInputWrapper {icon}>
60
+ {#snippet children({ class: iconClass })}
61
+ <Input
62
+ type="time"
63
+ bind:value={get, set}
64
+ class={cn(iconClass, className)}
65
+ {placeholder}
66
+ {step}
67
+ {...props}
68
+ />
69
+ {/snippet}
70
+ </IconInputWrapper>
48
71
  {/snippet}
49
72
  </FieldWrapper>
@@ -1,11 +1,14 @@
1
+ import { Time } from '@internationalized/date';
2
+ import type { Component } from 'svelte';
1
3
  import type { FormPath } from 'sveltekit-superforms';
2
4
  import { type FieldWrapperProps } from '../FieldWrapper.svelte';
3
- import { Time } from '@internationalized/date';
4
5
  declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>, M>(): {
5
6
  props: FieldWrapperProps<T, U, M> & {
6
7
  value?: Time;
7
8
  class?: string;
8
9
  step?: HTMLInputElement["step"];
10
+ placeholder?: string;
11
+ icon?: Component;
9
12
  };
10
13
  exports: {};
11
14
  bindings: "value";
@@ -3,15 +3,16 @@ import ChoiceField from './fields/ChoiceField.svelte';
3
3
  import ChoiceMultiField from './fields/ChoiceMultiField.svelte';
4
4
  import DateField from './fields/DateField.svelte';
5
5
  import HexColorField from './fields/HexColorField.svelte';
6
+ import MessageField from './fields/MessageField.svelte';
6
7
  import NumberField from './fields/NumberField.svelte';
7
8
  import PasswordField from './fields/PasswordField.svelte';
8
9
  import SelectField from './fields/SelectField.svelte';
9
10
  import SelectMultiField from './fields/SelectMultiField.svelte';
10
11
  import TextField from './fields/TextField.svelte';
11
12
  import TextFieldNullable from './fields/TextFieldNullable.svelte';
12
- import MessageField from './fields/MessageField.svelte';
13
13
  import TimeField from './fields/TimeField.svelte';
14
14
  import WeekdayChoiceField from './fields/WeekdayChoiceField.svelte';
15
15
  import WeekdayChoiceMultiField from './fields/WeekdayChoiceMultiField.svelte';
16
16
  import Form from './Form.svelte';
17
- export { Button, ChoiceField, ChoiceMultiField, DateField, Form, HexColorField, NumberField, PasswordField, SelectField, SelectMultiField, TextField, TextFieldNullable, MessageField, TimeField, WeekdayChoiceField, WeekdayChoiceMultiField };
17
+ import IconInputWrapper from './IconInputWrapper.svelte';
18
+ export { Button, ChoiceField, ChoiceMultiField, DateField, Form, HexColorField, IconInputWrapper, MessageField, NumberField, PasswordField, SelectField, SelectMultiField, TextField, TextFieldNullable, TimeField, WeekdayChoiceField, WeekdayChoiceMultiField };
@@ -3,15 +3,16 @@ import ChoiceField from './fields/ChoiceField.svelte';
3
3
  import ChoiceMultiField from './fields/ChoiceMultiField.svelte';
4
4
  import DateField from './fields/DateField.svelte';
5
5
  import HexColorField from './fields/HexColorField.svelte';
6
+ import MessageField from './fields/MessageField.svelte';
6
7
  import NumberField from './fields/NumberField.svelte';
7
8
  import PasswordField from './fields/PasswordField.svelte';
8
9
  import SelectField from './fields/SelectField.svelte';
9
10
  import SelectMultiField from './fields/SelectMultiField.svelte';
10
11
  import TextField from './fields/TextField.svelte';
11
12
  import TextFieldNullable from './fields/TextFieldNullable.svelte';
12
- import MessageField from './fields/MessageField.svelte';
13
13
  import TimeField from './fields/TimeField.svelte';
14
14
  import WeekdayChoiceField from './fields/WeekdayChoiceField.svelte';
15
15
  import WeekdayChoiceMultiField from './fields/WeekdayChoiceMultiField.svelte';
16
16
  import Form from './Form.svelte';
17
- export { Button, ChoiceField, ChoiceMultiField, DateField, Form, HexColorField, NumberField, PasswordField, SelectField, SelectMultiField, TextField, TextFieldNullable, MessageField, TimeField, WeekdayChoiceField, WeekdayChoiceMultiField };
17
+ import IconInputWrapper from './IconInputWrapper.svelte';
18
+ export { Button, ChoiceField, ChoiceMultiField, DateField, Form, HexColorField, IconInputWrapper, MessageField, NumberField, PasswordField, SelectField, SelectMultiField, TextField, TextFieldNullable, TimeField, WeekdayChoiceField, WeekdayChoiceMultiField };
@@ -0,0 +1,209 @@
1
+ # Email Utility Package
2
+
3
+ This package provides utilities for sending emails using AWS Simple Email Service (SES).
4
+
5
+ ## Installation
6
+
7
+ The AWS SES SDK is already included as a dependency. You'll need to configure your AWS credentials and region.
8
+
9
+ ## Configuration
10
+
11
+ ### Environment Variables
12
+
13
+ The email utility requires the following environment variables:
14
+
15
+ - `AWS_REGION` - AWS region where SES is configured (e.g., `us-east-1`)
16
+ - `AWS_ACCESS_KEY_ID` - AWS access key ID
17
+ - `AWS_SECRET_ACCESS_KEY` - AWS secret access key
18
+
19
+ ### SES Setup
20
+
21
+ Before using this utility, ensure:
22
+
23
+ 1. Your AWS account has SES enabled in your chosen region
24
+ 2. Your sender email address(es) are verified in SES
25
+ 3. If in SES sandbox mode, recipient addresses must also be verified
26
+ 4. Your account has the necessary IAM permissions to send emails via SES
27
+
28
+ ## Usage
29
+
30
+ ### Basic Example
31
+
32
+ ```typescript
33
+ import { sendEmail } from '@webamoki/web-svelte/utils/email';
34
+
35
+ // Send a simple text email
36
+ const messageId = await sendEmail({
37
+ to: 'recipient@example.com',
38
+ subject: 'Hello from SES',
39
+ text: 'This is a plain text email.',
40
+ from: 'sender@example.com'
41
+ });
42
+
43
+ console.log('Email sent with message ID:', messageId);
44
+ ```
45
+
46
+ ### HTML Email
47
+
48
+ ```typescript
49
+ await sendEmail({
50
+ to: 'recipient@example.com',
51
+ subject: 'Welcome!',
52
+ html: '<h1>Welcome to our service!</h1><p>Thanks for signing up.</p>',
53
+ text: 'Welcome to our service! Thanks for signing up.', // Fallback for email clients that don't support HTML
54
+ from: 'noreply@example.com',
55
+ fromName: 'My Application'
56
+ });
57
+ ```
58
+
59
+ ### Multiple Recipients
60
+
61
+ ```typescript
62
+ await sendEmail({
63
+ to: ['user1@example.com', 'user2@example.com'],
64
+ cc: 'manager@example.com',
65
+ bcc: ['archive@example.com', 'backup@example.com'],
66
+ subject: 'Team Update',
67
+ text: 'This is a team-wide announcement.',
68
+ from: 'team@example.com'
69
+ });
70
+ ```
71
+
72
+ ### With Reply-To
73
+
74
+ ```typescript
75
+ await sendEmail({
76
+ to: 'customer@example.com',
77
+ subject: 'Your order confirmation',
78
+ html: '<p>Your order has been confirmed!</p>',
79
+ from: 'noreply@example.com',
80
+ fromName: 'Order System',
81
+ replyTo: 'support@example.com'
82
+ });
83
+ ```
84
+
85
+ ## API Reference
86
+
87
+ ### `sendEmail(options: SendEmailOptions): Promise<string>`
88
+
89
+ Sends an email via AWS SES.
90
+
91
+ **Returns:** Promise that resolves to the SES message ID (string) on success.
92
+
93
+ **Throws:** Error with descriptive message if:
94
+
95
+ - Validation fails (missing required fields, invalid recipients, etc.)
96
+ - AWS SES returns an error (credentials, permissions, service issues)
97
+ - SES response does not contain a MessageId (unlikely but handled explicitly)
98
+
99
+ ### `SendEmailOptions`
100
+
101
+ ```typescript
102
+ interface SendEmailOptions {
103
+ to: string | string[]; // Required: recipient email address(es)
104
+ cc?: string | string[]; // Optional: CC recipients
105
+ bcc?: string | string[]; // Optional: BCC recipients
106
+ subject: string; // Required: email subject
107
+ text?: string; // Optional: plain text body
108
+ html?: string; // Optional: HTML body
109
+ from: string; // Required: sender email
110
+ fromName?: string; // Optional: sender display name
111
+ replyTo?: string | string[]; // Optional: reply-to address(es)
112
+ }
113
+ ```
114
+
115
+ ## Error Handling
116
+
117
+ The `sendEmail` function validates inputs and will throw errors for:
118
+
119
+ - Missing required fields (`to`, `from`, `subject`, at least one of `text` or `html`)
120
+ - Empty recipient list (no valid email addresses after filtering)
121
+ - AWS SES errors (credentials, permissions, service issues)
122
+
123
+ **Note:** The function automatically filters out empty strings and whitespace-only values from recipient arrays (`to`, `cc`, `bcc`, `replyTo`). For example, `['', 'valid@example.com', ' ']` will be processed as `['valid@example.com']`.
124
+
125
+ Always wrap calls in try-catch:
126
+
127
+ ```typescript
128
+ try {
129
+ const messageId = await sendEmail({
130
+ to: 'user@example.com',
131
+ subject: 'Test',
132
+ text: 'Hello!',
133
+ from: 'sender@example.com'
134
+ });
135
+ console.log('Success:', messageId);
136
+ } catch (error) {
137
+ console.error('Failed to send email:', error.message);
138
+ // Handle error appropriately
139
+ }
140
+ ```
141
+
142
+ ## SvelteKit Integration
143
+
144
+ ### Server-Only Usage
145
+
146
+ ⚠️ **Important:** This utility should only be used in server-side code (e.g., `+page.server.ts`, `+server.ts`, or server-side functions) as it requires AWS credentials.
147
+
148
+ ### Example: Contact Form Handler
149
+
150
+ ```typescript
151
+ // src/routes/contact/+page.server.ts
152
+ import { sendEmail } from '@webamoki/web-svelte/utils/email';
153
+ import type { Actions } from './$types';
154
+
155
+ export const actions: Actions = {
156
+ default: async ({ request }) => {
157
+ const formData = await request.formData();
158
+ const email = formData.get('email') as string;
159
+ const message = formData.get('message') as string;
160
+
161
+ try {
162
+ await sendEmail({
163
+ to: 'admin@example.com',
164
+ subject: `Contact form submission from ${email}`,
165
+ text: message,
166
+ from: 'noreply@example.com',
167
+ replyTo: email
168
+ });
169
+
170
+ return { success: true };
171
+ } catch (error) {
172
+ console.error('Email error:', error);
173
+ return { success: false, error: 'Failed to send message' };
174
+ }
175
+ }
176
+ };
177
+ ```
178
+
179
+ ## Best Practices
180
+
181
+ 1. **Never expose AWS credentials in client-side code**
182
+ 2. **Use environment variables for configuration**
183
+ 3. **Verify sender addresses in SES before deploying**
184
+ 4. **Always provide both `text` and `html` versions when sending HTML emails**
185
+ 5. **Handle errors gracefully and log failures for monitoring**
186
+ 6. **Consider rate limits** - SES has sending limits based on your account status
187
+ 7. **Use BCC for bulk emails** to avoid exposing all recipients
188
+ 8. **Set appropriate Reply-To addresses** for better user experience
189
+
190
+ ## Troubleshooting
191
+
192
+ ### "Sender not verified" error
193
+
194
+ Verify your sender email address in the AWS SES console for your region.
195
+
196
+ ### "Access Denied" error
197
+
198
+ Ensure your AWS credentials have the `ses:SendEmail` permission.
199
+
200
+ ### Emails not arriving
201
+
202
+ - Check SES sending statistics in AWS console
203
+ - Verify you're not in sandbox mode (or that recipients are verified)
204
+ - Check spam folders
205
+ - Review bounce and complaint notifications in SES
206
+
207
+ ## License
208
+
209
+ MIT
@@ -0,0 +1 @@
1
+ export { sendEmail, type SendEmailOptions } from './ses.js';
@@ -0,0 +1 @@
1
+ export { sendEmail } from './ses.js';
@@ -0,0 +1,22 @@
1
+ export interface SendEmailOptions {
2
+ to: string | string[];
3
+ cc?: string | string[];
4
+ bcc?: string | string[];
5
+ subject: string;
6
+ text?: string;
7
+ html?: string;
8
+ from: string;
9
+ fromName?: string;
10
+ replyTo?: string | string[];
11
+ }
12
+ /**
13
+ * Send an email using AWS SES.
14
+ *
15
+ * Environment variables required:
16
+ * - AWS_REGION
17
+ * - AWS_ACCESS_KEY_ID
18
+ * - AWS_SECRET_ACCESS_KEY
19
+ *
20
+ * @returns messageId returned by SES
21
+ */
22
+ export declare function sendEmail(options: SendEmailOptions): Promise<string>;
@@ -0,0 +1,95 @@
1
+ import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
2
+ // Create SES client once at module level for reuse across function calls
3
+ const sesClient = new SESClient({ region: process.env.AWS_REGION });
4
+ /**
5
+ * Send an email using AWS SES.
6
+ *
7
+ * Environment variables required:
8
+ * - AWS_REGION
9
+ * - AWS_ACCESS_KEY_ID
10
+ * - AWS_SECRET_ACCESS_KEY
11
+ *
12
+ * @returns messageId returned by SES
13
+ */
14
+ export async function sendEmail(options) {
15
+ if (!options)
16
+ throw new Error('sendEmail: options is required');
17
+ const { to, cc, bcc, subject, text, html, from, fromName, replyTo } = options;
18
+ if (!subject) {
19
+ throw new Error('sendEmail: subject is required');
20
+ }
21
+ if (!text && !html) {
22
+ throw new Error('sendEmail: at least one of text or html body must be provided');
23
+ }
24
+ if (!from) {
25
+ throw new Error('sendEmail: sender `from` is required');
26
+ }
27
+ // Normalize and validate addresses: convert to array and filter out empty/whitespace strings
28
+ const normalizeAddresses = (addr) => {
29
+ if (addr === undefined)
30
+ return undefined;
31
+ const addresses = Array.isArray(addr) ? addr : [addr];
32
+ const filtered = addresses.filter((a) => a && a.trim() !== '');
33
+ return filtered.length > 0 ? filtered : undefined;
34
+ };
35
+ const toAddresses = normalizeAddresses(to);
36
+ const ccAddresses = normalizeAddresses(cc);
37
+ const bccAddresses = normalizeAddresses(bcc);
38
+ const replyToAddresses = normalizeAddresses(replyTo);
39
+ if (!toAddresses || toAddresses.length === 0) {
40
+ throw new Error('sendEmail: at least one valid recipient is required (to)');
41
+ }
42
+ // Format source with optional fromName
43
+ const source = fromName ? `${fromName} <${from}>` : from;
44
+ // Build message body
45
+ const Message = {
46
+ Subject: {
47
+ Charset: 'UTF-8',
48
+ Data: subject
49
+ },
50
+ Body: {}
51
+ };
52
+ if (html) {
53
+ Message.Body.Html = {
54
+ Charset: 'UTF-8',
55
+ Data: html
56
+ };
57
+ }
58
+ if (text) {
59
+ Message.Body.Text = {
60
+ Charset: 'UTF-8',
61
+ Data: text
62
+ };
63
+ }
64
+ const params = {
65
+ Source: source,
66
+ Destination: {
67
+ ToAddresses: toAddresses,
68
+ CcAddresses: ccAddresses,
69
+ BccAddresses: bccAddresses
70
+ },
71
+ Message,
72
+ ReplyToAddresses: replyToAddresses
73
+ };
74
+ try {
75
+ const command = new SendEmailCommand(params);
76
+ const res = await sesClient.send(command);
77
+ if (!res.MessageId) {
78
+ throw new Error('sendEmail: SES response did not contain a MessageId');
79
+ }
80
+ return res.MessageId;
81
+ }
82
+ catch (err) {
83
+ // Normalize error for callers
84
+ const message = err instanceof Error ? err.message : String(err);
85
+ let code;
86
+ if (err && typeof err === 'object') {
87
+ const e = err;
88
+ if (typeof e['name'] === 'string')
89
+ code = e['name'];
90
+ }
91
+ const details = { message, code };
92
+ // Re-throw a clear error for the caller to handle
93
+ throw new Error(`sendEmail: failed to send email: ${JSON.stringify(details)}`);
94
+ }
95
+ }
package/package.json CHANGED
@@ -3,8 +3,12 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.7.0",
6
+ "version": "0.7.2",
7
7
  "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Webamoki/Web-svelte"
11
+ },
8
12
  "files": [
9
13
  "dist",
10
14
  "!dist/**/*.test.*",
@@ -43,6 +47,10 @@
43
47
  "types": "./dist/utils/form/index.d.ts",
44
48
  "import": "./dist/utils/form/index.js"
45
49
  },
50
+ "./utils/email": {
51
+ "types": "./dist/utils/email/index.d.ts",
52
+ "import": "./dist/utils/email/index.js"
53
+ },
46
54
  "./server/form-handler": {
47
55
  "types": "./dist/server/form-handler.d.ts",
48
56
  "import": "./dist/server/form-handler.js"
@@ -102,6 +110,7 @@
102
110
  "svelte"
103
111
  ],
104
112
  "dependencies": {
113
+ "@aws-sdk/client-ses": "^3.948.0",
105
114
  "@internationalized/date": "^3.10.0",
106
115
  "@lucide/svelte": "^0.553.0",
107
116
  "@sveltejs/adapter-auto": "^7.0.0",