@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.
- package/dist/components/atoms/Button/Button.stories.d.ts +6 -0
- package/dist/components/atoms/Button/Button.stories.js +7 -0
- package/dist/components/atoms/Button/Button.svelte +15 -17
- package/dist/components/atoms/Button/Button.svelte.d.ts +1 -0
- package/dist/components/atoms/Button/ButtonStory.svelte +2 -0
- package/dist/components/atoms/Button/ButtonStory.svelte.d.ts +1 -0
- package/dist/components/molecules/Combobox/Combobox.stories.d.ts +6 -0
- package/dist/components/molecules/Combobox/Combobox.stories.js +5 -2
- package/dist/components/molecules/Combobox/Combobox.svelte +155 -64
- package/dist/components/molecules/Combobox/Combobox.svelte.d.ts +15 -0
- package/dist/components/molecules/Combobox/ComboboxStory.svelte +25 -12
- package/dist/components/molecules/Combobox/ComboboxStory.svelte.d.ts +1 -0
- package/dist/components/molecules/Combobox/combobox-context.d.ts +12 -0
- package/dist/components/molecules/Combobox/combobox-context.js +10 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItem.stories.d.ts +9 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItem.stories.js +8 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItem.svelte +121 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItem.svelte.d.ts +33 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItemStory.svelte +54 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItemStory.svelte.d.ts +3 -0
- package/dist/components/molecules/Form/Form.svelte +36 -10
- package/dist/components/molecules/FormField/FormField.svelte +56 -19
- package/dist/components/molecules/FormField/FormField.svelte.d.ts +21 -6
- package/dist/components/molecules/FormPasswordInput/FormPasswordInput.svelte +38 -9
- package/dist/components/molecules/FormPasswordInput/FormPasswordInput.svelte.d.ts +8 -1
- package/dist/components/organisms/BlogEditor/BlogEditorStory.svelte +1 -1
- package/dist/components/organisms/FileUploader/FileUploader.svelte +7 -3
- package/dist/components/organisms/InvoicePage/InvoicePage.svelte +1 -1
- package/dist/components/organisms/ProfileHeaderUploader/ProfileHeaderUploader.svelte +30 -10
- package/dist/components/organisms/ProfileHeaderUploader/ProfileHeaderUploader.svelte.d.ts +4 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/styles.css +6 -0
- package/dist/utils/formContext.d.ts +58 -0
- package/dist/utils/formContext.js +79 -1
- package/package.json +16 -16
|
@@ -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>
|
|
@@ -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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
156
|
+
if (kitForm && !didInitRemoteFormId) {
|
|
157
|
+
didInitRemoteFormId = true;
|
|
158
|
+
remoteFormId = untrack(() => getRemoteFormId(kitForm.action));
|
|
159
|
+
}
|
|
150
160
|
|
|
151
|
-
|
|
152
|
-
if (!kitForm) return;
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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({
|
|
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 || !
|
|
126
|
+
if (!bindData || !logicalName || !form) return;
|
|
90
127
|
applyFormDataSync({
|
|
91
|
-
fromCtx: form.data[
|
|
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 &&
|
|
103
|
-
form.setData(
|
|
104
|
-
form.clearError(
|
|
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 (
|
|
146
|
+
if (logicalName && form) form.clearError(logicalName);
|
|
110
147
|
}
|
|
111
148
|
|
|
112
149
|
function handleInput(e: Event) {
|
|
113
|
-
if (bindData &&
|
|
114
|
-
form.setData(
|
|
115
|
-
form.clearError(
|
|
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
|
-
{
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
42
|
-
*
|
|
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({
|
|
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 || !
|
|
81
|
+
if (!bindData || !logicalName || !form) return;
|
|
53
82
|
applyFormDataSync({
|
|
54
|
-
fromCtx: form.data[
|
|
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 &&
|
|
65
|
-
form.setData(
|
|
66
|
-
form.clearError(
|
|
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
|
-
{
|
|
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: '
|
|
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
|
|
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"
|