@r2digisolutions/components 0.6.0 → 0.6.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.
- 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 +24 -24
|
@@ -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"
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
label?: string;
|
|
18
18
|
coverHelperText?: string;
|
|
19
19
|
avatarHelperText?: string;
|
|
20
|
+
/** Existing remote cover URL (shown when no local File is selected). */
|
|
21
|
+
coverSrc?: string;
|
|
22
|
+
/** Existing remote avatar URL (shown when no local File is selected). */
|
|
23
|
+
avatarSrc?: string;
|
|
20
24
|
accept?: string;
|
|
21
25
|
maxCoverMb?: number;
|
|
22
26
|
maxAvatarMb?: number;
|
|
@@ -30,6 +34,8 @@
|
|
|
30
34
|
label = 'Profile media',
|
|
31
35
|
coverHelperText = 'Cover · PNG or JPG · max 10MB',
|
|
32
36
|
avatarHelperText = 'Avatar · square image recommended',
|
|
37
|
+
coverSrc = '',
|
|
38
|
+
avatarSrc = '',
|
|
33
39
|
accept = 'image/*',
|
|
34
40
|
maxCoverMb = 10,
|
|
35
41
|
maxAvatarMb = 5,
|
|
@@ -52,6 +58,12 @@
|
|
|
52
58
|
const coverInputId = $derived(`${id}-cover`);
|
|
53
59
|
const avatarInputId = $derived(`${id}-avatar`);
|
|
54
60
|
|
|
61
|
+
const coverDisplayUrl = $derived(cover?.previewUrl || coverSrc || '');
|
|
62
|
+
const avatarDisplayUrl = $derived(avatar?.previewUrl || avatarSrc || '');
|
|
63
|
+
const hasCoverMedia = $derived(Boolean(coverDisplayUrl));
|
|
64
|
+
const hasAvatarMedia = $derived(Boolean(avatarDisplayUrl));
|
|
65
|
+
const hasLocalMedia = $derived(Boolean(cover || avatar));
|
|
66
|
+
|
|
55
67
|
function revoke(media: ProfileMedia | null) {
|
|
56
68
|
if (media?.previewUrl) URL.revokeObjectURL(media.previewUrl);
|
|
57
69
|
}
|
|
@@ -171,7 +183,7 @@
|
|
|
171
183
|
/>
|
|
172
184
|
|
|
173
185
|
<div
|
|
174
|
-
class="relative
|
|
186
|
+
class="relative rounded-xl border border-border bg-surface-elevated shadow-sm"
|
|
175
187
|
aria-labelledby={label ? labelId : undefined}
|
|
176
188
|
aria-describedby={[coverHelperText || avatarHelperText ? helperId : '', errorMessage ? errorId : '']
|
|
177
189
|
.filter(Boolean)
|
|
@@ -182,7 +194,11 @@
|
|
|
182
194
|
<div
|
|
183
195
|
role="button"
|
|
184
196
|
tabindex={disabled ? -1 : 0}
|
|
185
|
-
aria-label={
|
|
197
|
+
aria-label={hasCoverMedia
|
|
198
|
+
? cover
|
|
199
|
+
? `Change cover photo, ${cover.name}`
|
|
200
|
+
: 'Change cover photo'
|
|
201
|
+
: 'Upload cover photo'}
|
|
186
202
|
aria-disabled={disabled || undefined}
|
|
187
203
|
ondragenter={(e) => {
|
|
188
204
|
e.preventDefault();
|
|
@@ -200,14 +216,14 @@
|
|
|
200
216
|
}}
|
|
201
217
|
onclick={() => !disabled && coverInput?.click()}
|
|
202
218
|
class={[
|
|
203
|
-
'group relative aspect-3/1 min-h-36 w-full overflow-hidden outline-none transition-colors',
|
|
219
|
+
'group relative aspect-3/1 min-h-36 w-full overflow-hidden rounded-t-xl outline-none transition-colors',
|
|
204
220
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/40',
|
|
205
221
|
coverDragging ? 'bg-brand-500/10' : 'bg-surface-overlay',
|
|
206
222
|
disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
|
|
207
223
|
]}
|
|
208
224
|
>
|
|
209
|
-
{#if
|
|
210
|
-
<img src={
|
|
225
|
+
{#if hasCoverMedia}
|
|
226
|
+
<img src={coverDisplayUrl} alt="" class="h-full w-full object-cover" />
|
|
211
227
|
{#if !disabled}
|
|
212
228
|
<div
|
|
213
229
|
class="absolute inset-0 flex items-start justify-end bg-linear-to-b from-black/35 via-transparent to-transparent p-3 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
|
|
@@ -267,7 +283,11 @@
|
|
|
267
283
|
<div
|
|
268
284
|
role="button"
|
|
269
285
|
tabindex={disabled ? -1 : 0}
|
|
270
|
-
aria-label={
|
|
286
|
+
aria-label={hasAvatarMedia
|
|
287
|
+
? avatar
|
|
288
|
+
? `Change profile photo, ${avatar.name}`
|
|
289
|
+
: 'Change profile photo'
|
|
290
|
+
: 'Upload profile photo'}
|
|
271
291
|
aria-disabled={disabled || undefined}
|
|
272
292
|
ondragenter={(e) => {
|
|
273
293
|
e.preventDefault();
|
|
@@ -298,14 +318,14 @@
|
|
|
298
318
|
if (!disabled) avatarInput?.click();
|
|
299
319
|
}}
|
|
300
320
|
class={[
|
|
301
|
-
'group relative h-20 w-20 shrink-0 overflow-hidden rounded-full border-4 border-surface-elevated bg-surface-overlay shadow-md outline-none transition sm:h-24 sm:w-24',
|
|
321
|
+
'group relative z-10 h-20 w-20 shrink-0 overflow-hidden rounded-full border-4 border-surface-elevated bg-surface-overlay shadow-md outline-none transition sm:h-24 sm:w-24',
|
|
302
322
|
'focus-visible:ring-2 focus-visible:ring-brand-500/40 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-elevated',
|
|
303
323
|
avatarDragging ? 'ring-2 ring-brand-500' : '',
|
|
304
324
|
disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
|
|
305
325
|
]}
|
|
306
326
|
>
|
|
307
|
-
{#if
|
|
308
|
-
<img src={
|
|
327
|
+
{#if hasAvatarMedia}
|
|
328
|
+
<img src={avatarDisplayUrl} alt="" class="h-full w-full object-cover" />
|
|
309
329
|
{#if !disabled}
|
|
310
330
|
<div
|
|
311
331
|
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"
|
|
@@ -339,7 +359,7 @@
|
|
|
339
359
|
</div>
|
|
340
360
|
|
|
341
361
|
<div class="min-w-0 flex-1 pb-1">
|
|
342
|
-
{#if
|
|
362
|
+
{#if hasLocalMedia}
|
|
343
363
|
<div class="flex flex-wrap items-center gap-2">
|
|
344
364
|
{#if cover && !disabled}
|
|
345
365
|
<button
|
|
@@ -7,6 +7,10 @@ interface ProfileHeaderUploaderProps {
|
|
|
7
7
|
label?: string;
|
|
8
8
|
coverHelperText?: string;
|
|
9
9
|
avatarHelperText?: string;
|
|
10
|
+
/** Existing remote cover URL (shown when no local File is selected). */
|
|
11
|
+
coverSrc?: string;
|
|
12
|
+
/** Existing remote avatar URL (shown when no local File is selected). */
|
|
13
|
+
avatarSrc?: string;
|
|
10
14
|
accept?: string;
|
|
11
15
|
maxCoverMb?: number;
|
|
12
16
|
maxAvatarMb?: number;
|