@r2digisolutions/components 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/components/atoms/Button/Button.stories.d.ts +6 -0
  2. package/dist/components/atoms/Button/Button.stories.js +7 -0
  3. package/dist/components/atoms/Button/Button.svelte +15 -17
  4. package/dist/components/atoms/Button/Button.svelte.d.ts +1 -0
  5. package/dist/components/atoms/Button/ButtonStory.svelte +2 -0
  6. package/dist/components/atoms/Button/ButtonStory.svelte.d.ts +1 -0
  7. package/dist/components/molecules/Combobox/Combobox.stories.d.ts +6 -0
  8. package/dist/components/molecules/Combobox/Combobox.stories.js +5 -2
  9. package/dist/components/molecules/Combobox/Combobox.svelte +155 -64
  10. package/dist/components/molecules/Combobox/Combobox.svelte.d.ts +15 -0
  11. package/dist/components/molecules/Combobox/ComboboxStory.svelte +25 -12
  12. package/dist/components/molecules/Combobox/ComboboxStory.svelte.d.ts +1 -0
  13. package/dist/components/molecules/Combobox/combobox-context.d.ts +12 -0
  14. package/dist/components/molecules/Combobox/combobox-context.js +10 -0
  15. package/dist/components/molecules/ComboboxItem/ComboboxItem.stories.d.ts +9 -0
  16. package/dist/components/molecules/ComboboxItem/ComboboxItem.stories.js +8 -0
  17. package/dist/components/molecules/ComboboxItem/ComboboxItem.svelte +121 -0
  18. package/dist/components/molecules/ComboboxItem/ComboboxItem.svelte.d.ts +33 -0
  19. package/dist/components/molecules/ComboboxItem/ComboboxItemStory.svelte +54 -0
  20. package/dist/components/molecules/ComboboxItem/ComboboxItemStory.svelte.d.ts +3 -0
  21. package/dist/components/molecules/Form/Form.svelte +36 -10
  22. package/dist/components/molecules/FormField/FormField.svelte +56 -19
  23. package/dist/components/molecules/FormField/FormField.svelte.d.ts +21 -6
  24. package/dist/components/molecules/FormPasswordInput/FormPasswordInput.svelte +38 -9
  25. package/dist/components/molecules/FormPasswordInput/FormPasswordInput.svelte.d.ts +8 -1
  26. package/dist/components/organisms/BlogEditor/BlogEditorStory.svelte +1 -1
  27. package/dist/components/organisms/FileUploader/FileUploader.svelte +7 -3
  28. package/dist/components/organisms/InvoicePage/InvoicePage.svelte +1 -1
  29. package/dist/components/organisms/ProfileHeaderUploader/ProfileHeaderUploader.svelte +30 -10
  30. package/dist/components/organisms/ProfileHeaderUploader/ProfileHeaderUploader.svelte.d.ts +4 -0
  31. package/dist/index.d.ts +6 -3
  32. package/dist/index.js +3 -1
  33. package/dist/styles.css +6 -0
  34. package/dist/utils/formContext.d.ts +58 -0
  35. package/dist/utils/formContext.js +79 -1
  36. package/package.json +16 -16
@@ -0,0 +1,8 @@
1
+ import ComboboxItemStory from './ComboboxItemStory.svelte';
2
+ const meta = {
3
+ title: 'Molecules/ComboboxItem',
4
+ component: ComboboxItemStory,
5
+ tags: ['autodocs']
6
+ };
7
+ export default meta;
8
+ export const Default = {};
@@ -0,0 +1,121 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Check from '@lucide/svelte/icons/check';
4
+ import { getComboboxContext } from '../Combobox/combobox-context.js';
5
+
6
+ interface ComboboxItemProps {
7
+ value: string;
8
+ /** Used for default text and query filtering when nested in `Combobox`. */
9
+ label?: string;
10
+ disabled?: boolean;
11
+ /** Extra strings matched against the parent `Combobox` query. */
12
+ keywords?: string[];
13
+ /**
14
+ * When omitted, selection comes from the parent `Combobox`.
15
+ * Pass explicitly when using the item standalone.
16
+ */
17
+ selected?: boolean;
18
+ /**
19
+ * When omitted, highlight comes from the parent `Combobox`.
20
+ * Pass explicitly when using the item standalone.
21
+ */
22
+ highlighted?: boolean;
23
+ /**
24
+ * Register this row for keyboard navigation / query filtering.
25
+ * Internal `Combobox` rows set this to `false` (parent already filters).
26
+ */
27
+ register?: boolean;
28
+ class?: string;
29
+ children?: Snippet;
30
+ leading?: Snippet;
31
+ trailing?: Snippet;
32
+ onclick?: () => void;
33
+ onhighlight?: () => void;
34
+ }
35
+
36
+ let {
37
+ value,
38
+ label = '',
39
+ disabled = false,
40
+ keywords = [],
41
+ selected,
42
+ highlighted,
43
+ register = true,
44
+ class: className = '',
45
+ children,
46
+ leading,
47
+ trailing,
48
+ onclick,
49
+ onhighlight
50
+ }: ComboboxItemProps = $props();
51
+
52
+ const ctx = getComboboxContext();
53
+
54
+ const matchesQuery = $derived.by(() => {
55
+ if (!register || !ctx) return true;
56
+ const q = ctx.getQuery().trim().toLowerCase();
57
+ if (!q) return true;
58
+ const blob = [label, value, ...keywords].join(' ').toLowerCase();
59
+ return blob.includes(q);
60
+ });
61
+
62
+ const isSelected = $derived(selected ?? (ctx ? ctx.getValue() === value : false));
63
+ const isHighlighted = $derived(highlighted ?? (ctx ? ctx.getHighlighted() === value : false));
64
+ const active = $derived(!disabled && (isHighlighted || isSelected));
65
+
66
+ $effect(() => {
67
+ if (!register || !ctx || disabled || !matchesQuery) return;
68
+ return ctx.register(value, disabled, label);
69
+ });
70
+
71
+ function activate() {
72
+ if (disabled) return;
73
+ if (onclick) {
74
+ onclick();
75
+ return;
76
+ }
77
+ ctx?.select(value);
78
+ }
79
+ </script>
80
+
81
+ {#if matchesQuery}
82
+ <button
83
+ type="button"
84
+ role="option"
85
+ data-value={value}
86
+ {disabled}
87
+ aria-selected={isSelected}
88
+ aria-disabled={disabled}
89
+ onpointerenter={() => {
90
+ if (disabled) return;
91
+ onhighlight?.();
92
+ ctx?.highlight(value);
93
+ }}
94
+ onclick={activate}
95
+ class={[
96
+ 'gap-2 rounded-lg px-2.5 py-2 text-sm flex w-full items-center text-left transition-colors',
97
+ 'focus-visible:ring-brand-500/30 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset',
98
+ disabled && 'cursor-not-allowed opacity-40',
99
+ !disabled && (active ? 'bg-brand-500 text-white' : 'text-primary hover:bg-surface-overlay'),
100
+ className
101
+ ]}
102
+ >
103
+ {#if leading}
104
+ <span class="flex shrink-0 items-center">{@render leading()}</span>
105
+ {/if}
106
+
107
+ <span class={['min-w-0 flex-1', !children && 'truncate']}>
108
+ {#if children}
109
+ {@render children()}
110
+ {:else}
111
+ {label || value}
112
+ {/if}
113
+ </span>
114
+
115
+ {#if trailing}
116
+ <span class="flex shrink-0 items-center">{@render trailing()}</span>
117
+ {:else if isSelected}
118
+ <Check class="h-4 w-4 shrink-0" strokeWidth={2.5} />
119
+ {/if}
120
+ </button>
121
+ {/if}
@@ -0,0 +1,33 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface ComboboxItemProps {
3
+ value: string;
4
+ /** Used for default text and query filtering when nested in `Combobox`. */
5
+ label?: string;
6
+ disabled?: boolean;
7
+ /** Extra strings matched against the parent `Combobox` query. */
8
+ keywords?: string[];
9
+ /**
10
+ * When omitted, selection comes from the parent `Combobox`.
11
+ * Pass explicitly when using the item standalone.
12
+ */
13
+ selected?: boolean;
14
+ /**
15
+ * When omitted, highlight comes from the parent `Combobox`.
16
+ * Pass explicitly when using the item standalone.
17
+ */
18
+ highlighted?: boolean;
19
+ /**
20
+ * Register this row for keyboard navigation / query filtering.
21
+ * Internal `Combobox` rows set this to `false` (parent already filters).
22
+ */
23
+ register?: boolean;
24
+ class?: string;
25
+ children?: Snippet;
26
+ leading?: Snippet;
27
+ trailing?: Snippet;
28
+ onclick?: () => void;
29
+ onhighlight?: () => void;
30
+ }
31
+ declare const ComboboxItem: import("svelte").Component<ComboboxItemProps, {}, "">;
32
+ type ComboboxItem = ReturnType<typeof ComboboxItem>;
33
+ export default ComboboxItem;
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import ComboboxItem from './ComboboxItem.svelte';
3
+
4
+ let selected = $state('svelte');
5
+ let highlighted = $state('svelte');
6
+
7
+ const frameworks = [
8
+ { value: 'svelte', label: 'Svelte', hint: 'Compiler UI' },
9
+ { value: 'react', label: 'React', hint: 'Component model' },
10
+ { value: 'vue', label: 'Vue', hint: 'Progressive' },
11
+ { value: 'solid', label: 'Solid', hint: 'Fine-grained' },
12
+ { value: 'qwik', label: 'Qwik', hint: 'Resumable', disabled: true }
13
+ ];
14
+ </script>
15
+
16
+ <div class="max-w-sm space-y-3 w-full">
17
+ <div class="space-y-1">
18
+ <p class="text-sm font-semibold text-primary">ComboboxItem</p>
19
+ <p class="text-xs text-secondary">
20
+ Fila de opción para listas de Combobox. Funciona sola o anidada en
21
+ <code class="text-primary">Combobox</code>.
22
+ </p>
23
+ </div>
24
+
25
+ <div class="rounded-xl border-border bg-surface-elevated p-1.5 shadow-sm border">
26
+ {#each frameworks as fw (fw.value)}
27
+ <ComboboxItem
28
+ value={fw.value}
29
+ label={fw.label}
30
+ disabled={fw.disabled}
31
+ selected={selected === fw.value}
32
+ highlighted={highlighted === fw.value}
33
+ onclick={() => (selected = fw.value)}
34
+ onhighlight={() => (highlighted = fw.value)}
35
+ >
36
+ <span class="min-w-0 flex flex-col">
37
+ <span class="truncate">{fw.label}</span>
38
+ <span
39
+ class={[
40
+ 'truncate text-[11px]',
41
+ selected === fw.value || highlighted === fw.value ? 'text-white/80' : 'text-muted'
42
+ ]}
43
+ >
44
+ {fw.hint}
45
+ </span>
46
+ </span>
47
+ </ComboboxItem>
48
+ {/each}
49
+ </div>
50
+
51
+ <p class="text-xs text-muted">
52
+ Value: <span class="text-primary">{selected}</span>
53
+ </p>
54
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const ComboboxItemStory: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ComboboxItemStory = ReturnType<typeof ComboboxItemStory>;
3
+ export default ComboboxItemStory;
@@ -3,11 +3,12 @@
3
3
  generics="TData extends FormDataValues = FormDataValues, TInput extends RemoteFormInput | void = RemoteFormInput, TOutput = unknown"
4
4
  >
5
5
  import type { Snippet } from 'svelte';
6
- import { setContext } from 'svelte';
6
+ import { setContext, untrack } from 'svelte';
7
7
  import type { RemoteFormInput } from '@sveltejs/kit';
8
8
  import Alert from '../Alert/Alert.svelte';
9
9
  import {
10
10
  FORM_CONTEXT_KEY,
11
+ getRemoteFormId,
11
12
  isRemoteForm,
12
13
  remoteIssuesToErrors,
13
14
  type FormContext,
@@ -79,6 +80,9 @@
79
80
  const isRemote = $derived(remote != null);
80
81
  const remotePending = $derived(Boolean(kitForm?.pending));
81
82
  const busy = $derived(loading || remotePending || disabled);
83
+ // Prefer page-provided `inputName` on fields; optional inferred id is set once below.
84
+ let remoteFormId = $state<string | null>(null);
85
+ let didInitRemoteFormId = false;
82
86
 
83
87
  const errorEntries = $derived(Object.entries(errors).filter(([, msg]) => Boolean(msg)));
84
88
  const hasErrors = $derived(errorEntries.length > 0);
@@ -135,6 +139,12 @@
135
139
  get result() {
136
140
  return (result ?? kitForm?.result) as TOutput | undefined;
137
141
  },
142
+ get remote() {
143
+ return kitForm as FormContext<TData, TOutput>['remote'];
144
+ },
145
+ get remoteFormId() {
146
+ return remoteFormId;
147
+ },
138
148
  setError,
139
149
  clearError,
140
150
  clearErrors,
@@ -143,19 +153,31 @@
143
153
  getData
144
154
  } satisfies FormContext<TData, TOutput>);
145
155
 
146
- $effect(() => {
147
- if (!kitForm || !syncRemoteIssues) return;
148
- errors = remoteIssuesToErrors(kitForm.fields.allIssues());
149
- });
156
+ if (kitForm && !didInitRemoteFormId) {
157
+ didInitRemoteFormId = true;
158
+ remoteFormId = untrack(() => getRemoteFormId(kitForm.action));
159
+ }
150
160
 
151
- $effect(() => {
152
- if (!kitForm) return;
153
- if (kitForm.result !== undefined) result = kitForm.result;
154
- });
161
+ function syncRemoteIssuesNow() {
162
+ if (!kitForm || !syncRemoteIssues) return;
163
+ const next = remoteIssuesToErrors(kitForm.fields.allIssues());
164
+ errors = next;
165
+ if (Object.keys(next).length > 0) submitted = true;
166
+ }
155
167
 
156
168
  function handleSubmit(e: SubmitEvent) {
157
169
  if (isRemote) {
158
170
  submitted = true;
171
+ queueMicrotask(() => {
172
+ const check = () => {
173
+ if (kitForm && kitForm.pending > 0) {
174
+ requestAnimationFrame(check);
175
+ return;
176
+ }
177
+ syncRemoteIssuesNow();
178
+ };
179
+ check();
180
+ });
159
181
  return;
160
182
  }
161
183
  e.preventDefault();
@@ -189,7 +211,11 @@
189
211
  <ul class="mt-1 list-disc space-y-0.5 pl-4 text-sm">
190
212
  {#each errorEntries as [field, message] (field)}
191
213
  <li>
192
- <span class="font-medium capitalize">{field}</span>: {message}
214
+ {#if field === '_form'}
215
+ {message}
216
+ {:else}
217
+ <span class="font-medium capitalize">{field}</span>: {message}
218
+ {/if}
193
219
  </li>
194
220
  {/each}
195
221
  </ul>
@@ -4,6 +4,8 @@
4
4
  import {
5
5
  getFormContext,
6
6
  resolveFormFieldState,
7
+ resolveRemoteInputProps,
8
+ parseRemoteFieldName,
7
9
  applyFormDataSync,
8
10
  type FormFieldStatus
9
11
  } from '../../../utils/formContext.js';
@@ -22,7 +24,18 @@
22
24
 
23
25
  /**
24
26
  * Form-bound text input (email, tel, url, search, …).
25
- * There is no separate `FormTextInput` — use this component.
27
+ *
28
+ * Kit remote: pass the encoded `name` (and `type`) from `.as()`, **not** the full
29
+ * spread — Kit’s `value` get/set fights `$bindable` and can loop.
30
+ *
31
+ * @example
32
+ * ```svelte
33
+ * <FormField
34
+ * name={login_user.fields.email.as('email').name}
35
+ * type="email"
36
+ * label="Email"
37
+ * />
38
+ * ```
26
39
  */
27
40
  interface FormFieldProps {
28
41
  id?: string;
@@ -41,19 +54,23 @@
41
54
  size?: 'sm' | 'md' | 'lg';
42
55
  /**
43
56
  * When true and inside `<Form>`, keep `value` in sync with `form.data[name]`.
44
- * Requires `name`.
57
+ * Uses the logical field name (strips Kit `/formId` when present).
45
58
  */
46
59
  bindData?: boolean;
47
- leadIcon?: Snippet;
48
- trailIcon?: Snippet;
49
60
  /**
50
- * Custom control instead of the default `<Input>`.
51
- * Receives resolved status / disabled / helpers from form context.
61
+ * Override HTML `name` if logical `name` is separate.
62
+ * Prefer passing Kit `.as(...).name` as `name` instead.
52
63
  */
64
+ inputName?: string;
65
+ leadIcon?: Snippet;
66
+ trailIcon?: Snippet;
53
67
  control?: Snippet<[FormFieldControlProps]>;
54
68
  class?: string;
55
69
  oninput?: (e: Event) => void;
56
70
  onchange?: (e: Event) => void;
71
+ /** Absorbed if a Kit `.as()` object is spread by mistake. */
72
+ defaultValue?: unknown;
73
+ 'aria-invalid'?: boolean | 'true' | 'false';
57
74
  }
58
75
 
59
76
  let {
@@ -72,23 +89,43 @@
72
89
  clearable = false,
73
90
  size = 'md',
74
91
  bindData = false,
92
+ inputName,
75
93
  leadIcon,
76
94
  trailIcon,
77
95
  control,
78
96
  class: className = '',
79
97
  oninput,
80
- onchange
98
+ onchange,
99
+ defaultValue: _kitDefaultValue = undefined,
100
+ 'aria-invalid': _ariaInvalid = undefined
81
101
  }: FormFieldProps = $props();
82
102
 
83
103
  const form = getFormContext();
104
+ const parsed = $derived(parseRemoteFieldName(name, form?.remoteFormId));
105
+ const logicalName = $derived(parsed.logicalName);
106
+
84
107
  const resolved = $derived(
85
- resolveFormFieldState({ name, errorMessage, helperText, status, disabled, form })
108
+ resolveFormFieldState({
109
+ name: logicalName,
110
+ errorMessage,
111
+ helperText,
112
+ status,
113
+ disabled,
114
+ form
115
+ })
86
116
  );
87
117
 
118
+ /** HTML `name`: override → encoded as-is → else encode with remoteFormId. */
119
+ const htmlName = $derived.by(() => {
120
+ if (inputName) return inputName;
121
+ if (parsed.isEncoded) return parsed.htmlName;
122
+ return resolveRemoteInputProps(form?.remoteFormId, name, type).name;
123
+ });
124
+
88
125
  $effect(() => {
89
- if (!bindData || !name || !form) return;
126
+ if (!bindData || !logicalName || !form) return;
90
127
  applyFormDataSync({
91
- fromCtx: form.data[name],
128
+ fromCtx: form.data[logicalName],
92
129
  getLocal: () => value,
93
130
  setLocal: (v) => {
94
131
  value = v;
@@ -99,20 +136,20 @@
99
136
 
100
137
  function setValue(next: string) {
101
138
  value = next;
102
- if (bindData && name && form) {
103
- form.setData(name, next);
104
- form.clearError(name);
139
+ if (bindData && logicalName && form) {
140
+ form.setData(logicalName, next);
141
+ form.clearError(logicalName);
105
142
  }
106
143
  }
107
144
 
108
145
  function clearFieldError() {
109
- if (name && form) form.clearError(name);
146
+ if (logicalName && form) form.clearError(logicalName);
110
147
  }
111
148
 
112
149
  function handleInput(e: Event) {
113
- if (bindData && name && form) {
114
- form.setData(name, (e.currentTarget as HTMLInputElement).value);
115
- form.clearError(name);
150
+ if (bindData && logicalName && form) {
151
+ form.setData(logicalName, (e.currentTarget as HTMLInputElement).value);
152
+ form.clearError(logicalName);
116
153
  }
117
154
  oninput?.(e);
118
155
  }
@@ -122,7 +159,7 @@
122
159
  {#if control}
123
160
  {@render control({
124
161
  id,
125
- name,
162
+ name: logicalName,
126
163
  value,
127
164
  status: resolved.status,
128
165
  helperText: resolved.helperText,
@@ -134,7 +171,7 @@
134
171
  {:else}
135
172
  <Input
136
173
  {id}
137
- {name}
174
+ name={htmlName}
138
175
  {label}
139
176
  {placeholder}
140
177
  {type}
@@ -13,7 +13,18 @@ export interface FormFieldControlProps {
13
13
  }
14
14
  /**
15
15
  * Form-bound text input (email, tel, url, search, …).
16
- * There is no separate `FormTextInput` — use this component.
16
+ *
17
+ * Kit remote: pass the encoded `name` (and `type`) from `.as()`, **not** the full
18
+ * spread — Kit’s `value` get/set fights `$bindable` and can loop.
19
+ *
20
+ * @example
21
+ * ```svelte
22
+ * <FormField
23
+ * name={login_user.fields.email.as('email').name}
24
+ * type="email"
25
+ * label="Email"
26
+ * />
27
+ * ```
17
28
  */
18
29
  interface FormFieldProps {
19
30
  id?: string;
@@ -32,19 +43,23 @@ interface FormFieldProps {
32
43
  size?: 'sm' | 'md' | 'lg';
33
44
  /**
34
45
  * When true and inside `<Form>`, keep `value` in sync with `form.data[name]`.
35
- * Requires `name`.
46
+ * Uses the logical field name (strips Kit `/formId` when present).
36
47
  */
37
48
  bindData?: boolean;
38
- leadIcon?: Snippet;
39
- trailIcon?: Snippet;
40
49
  /**
41
- * Custom control instead of the default `<Input>`.
42
- * Receives resolved status / disabled / helpers from form context.
50
+ * Override HTML `name` if logical `name` is separate.
51
+ * Prefer passing Kit `.as(...).name` as `name` instead.
43
52
  */
53
+ inputName?: string;
54
+ leadIcon?: Snippet;
55
+ trailIcon?: Snippet;
44
56
  control?: Snippet<[FormFieldControlProps]>;
45
57
  class?: string;
46
58
  oninput?: (e: Event) => void;
47
59
  onchange?: (e: Event) => void;
60
+ /** Absorbed if a Kit `.as()` object is spread by mistake. */
61
+ defaultValue?: unknown;
62
+ 'aria-invalid'?: boolean | 'true' | 'false';
48
63
  }
49
64
  declare const FormField: import("svelte").Component<FormFieldProps, {}, "value">;
50
65
  type FormField = ReturnType<typeof FormField>;
@@ -3,9 +3,15 @@
3
3
  import {
4
4
  getFormContext,
5
5
  resolveFormFieldState,
6
+ resolveRemoteInputProps,
7
+ parseRemoteFieldName,
6
8
  applyFormDataSync
7
9
  } from '../../../utils/formContext.js';
8
10
 
11
+ /**
12
+ * Kit remote: pass `.as('password').name` as `name`, not the full `.as()` spread
13
+ * (Kit’s `value` get/set fights local binding).
14
+ */
9
15
  interface FormPasswordInputProps {
10
16
  id?: string;
11
17
  name?: string;
@@ -18,11 +24,14 @@
18
24
  disabled?: boolean;
19
25
  required?: boolean;
20
26
  size?: 'sm' | 'md' | 'lg';
21
- /** Sync with `form.data[name]` when inside `<Form>`. */
22
27
  bindData?: boolean;
28
+ inputName?: string;
23
29
  class?: string;
24
30
  oninput?: (e: Event) => void;
25
31
  onchange?: (e: Event) => void;
32
+ defaultValue?: unknown;
33
+ type?: string;
34
+ 'aria-invalid'?: boolean | 'true' | 'false';
26
35
  }
27
36
 
28
37
  let {
@@ -38,20 +47,40 @@
38
47
  required = false,
39
48
  size = 'md',
40
49
  bindData = false,
50
+ inputName,
41
51
  class: className = '',
42
52
  oninput,
43
- onchange
53
+ onchange,
54
+ defaultValue: _kitDefaultValue = undefined,
55
+ type: _kitType = undefined,
56
+ 'aria-invalid': _ariaInvalid = undefined
44
57
  }: FormPasswordInputProps = $props();
45
58
 
46
59
  const form = getFormContext();
60
+ const parsed = $derived(parseRemoteFieldName(name, form?.remoteFormId));
61
+ const logicalName = $derived(parsed.logicalName);
62
+
47
63
  const resolved = $derived(
48
- resolveFormFieldState({ name, errorMessage, helperText, status, disabled, form })
64
+ resolveFormFieldState({
65
+ name: logicalName,
66
+ errorMessage,
67
+ helperText,
68
+ status,
69
+ disabled,
70
+ form
71
+ })
49
72
  );
50
73
 
74
+ const htmlName = $derived.by(() => {
75
+ if (inputName) return inputName;
76
+ if (parsed.isEncoded) return parsed.htmlName;
77
+ return resolveRemoteInputProps(form?.remoteFormId, name, 'password').name;
78
+ });
79
+
51
80
  $effect(() => {
52
- if (!bindData || !name || !form) return;
81
+ if (!bindData || !logicalName || !form) return;
53
82
  applyFormDataSync({
54
- fromCtx: form.data[name],
83
+ fromCtx: form.data[logicalName],
55
84
  getLocal: () => value,
56
85
  setLocal: (v) => {
57
86
  value = v;
@@ -61,9 +90,9 @@
61
90
  });
62
91
 
63
92
  function handleInput(e: Event) {
64
- if (bindData && name && form) {
65
- form.setData(name, (e.currentTarget as HTMLInputElement).value);
66
- form.clearError(name);
93
+ if (bindData && logicalName && form) {
94
+ form.setData(logicalName, (e.currentTarget as HTMLInputElement).value);
95
+ form.clearError(logicalName);
67
96
  }
68
97
  oninput?.(e);
69
98
  }
@@ -72,7 +101,7 @@
72
101
  <div class={['w-full', className]}>
73
102
  <PasswordInput
74
103
  {id}
75
- {name}
104
+ name={htmlName}
76
105
  {label}
77
106
  {placeholder}
78
107
  disabled={resolved.disabled}
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Kit remote: pass `.as('password').name` as `name`, not the full `.as()` spread
3
+ * (Kit’s `value` get/set fights local binding).
4
+ */
1
5
  interface FormPasswordInputProps {
2
6
  id?: string;
3
7
  name?: string;
@@ -10,11 +14,14 @@ interface FormPasswordInputProps {
10
14
  disabled?: boolean;
11
15
  required?: boolean;
12
16
  size?: 'sm' | 'md' | 'lg';
13
- /** Sync with `form.data[name]` when inside `<Form>`. */
14
17
  bindData?: boolean;
18
+ inputName?: string;
15
19
  class?: string;
16
20
  oninput?: (e: Event) => void;
17
21
  onchange?: (e: Event) => void;
22
+ defaultValue?: unknown;
23
+ type?: string;
24
+ 'aria-invalid'?: boolean | 'true' | 'false';
18
25
  }
19
26
  declare const FormPasswordInput: import("svelte").Component<FormPasswordInputProps, {}, "value">;
20
27
  type FormPasswordInput = ReturnType<typeof FormPasswordInput>;
@@ -82,7 +82,7 @@
82
82
  let value = $state<BlogDocument>({
83
83
  ...emptyBlogDocument('Shipping our component library'),
84
84
  description: 'A richer block model with media, layout, and registered components.',
85
- author: 'R2 Digisolutions',
85
+ author: 'Evoteg',
86
86
  publishedAt: '2026-07-28',
87
87
  tags: ['design-system', 'svelte', 'blog'],
88
88
  coverImage: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=1200&q=80',
@@ -525,14 +525,18 @@
525
525
  'focus-visible:ring-2 focus-visible:ring-brand-500/30 focus-visible:border-brand-500',
526
526
  isDragging
527
527
  ? 'border-brand-500 bg-brand-500/10'
528
- : hasFile
528
+ : hasFile || externalSrc
529
529
  ? 'border-transparent'
530
530
  : 'border-border bg-surface-elevated hover:border-border-strong',
531
531
  disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
532
532
  ]}
533
533
  >
534
- {#if primaryFile?.previewUrl}
535
- <img src={primaryFile.previewUrl} alt="" class="h-full w-full object-cover" />
534
+ {#if primaryFile?.previewUrl || externalSrc}
535
+ <img
536
+ src={primaryFile?.previewUrl || externalSrc}
537
+ alt=""
538
+ class="h-full w-full object-cover"
539
+ />
536
540
  {#if !disabled}
537
541
  <div
538
542
  class="absolute inset-0 flex items-center justify-center bg-black/45 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
@@ -52,7 +52,7 @@
52
52
  paidAt,
53
53
  status = 'sent',
54
54
  from = {
55
- name: 'R2 Digi Solutions',
55
+ name: 'Evoteg',
56
56
  email: 'billing@r2digi.com',
57
57
  addressLines: ['Calle Mayor 12', '28013 Madrid, Spain'],
58
58
  taxId: 'ES B12345678'