lutra 0.1.68 → 0.1.70
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/AspectRatio.svelte +19 -9
- package/dist/components/AspectRatio.svelte.d.ts +2 -1
- package/dist/components/Avatar.svelte +5 -8
- package/dist/components/Close.svelte +24 -27
- package/dist/components/Close.svelte.d.ts +2 -0
- package/dist/components/ContextTip.svelte +3 -2
- package/dist/components/DataList.svelte +111 -0
- package/dist/components/DataList.svelte.d.ts +10 -0
- package/dist/components/DataListTypes.d.ts +14 -0
- package/dist/components/DataListTypes.js +1 -0
- package/dist/components/Dialog.svelte +38 -0
- package/dist/components/Icon.svelte +2 -2
- package/dist/components/IconButton.svelte +10 -22
- package/dist/components/Image.svelte +2 -2
- package/dist/components/Indicator.svelte +2 -1
- package/dist/components/Inset.svelte +13 -0
- package/dist/components/Layout.svelte +7 -3
- package/dist/components/Layout.svelte.d.ts +3 -2
- package/dist/components/MenuDropdown.svelte +12 -2
- package/dist/components/MenuItem.svelte +30 -14
- package/dist/components/MenuItem.svelte.d.ts +6 -0
- package/dist/components/Modal.svelte +36 -20
- package/dist/components/Popover.svelte +43 -13
- package/dist/components/TabbedContent.svelte +1 -1
- package/dist/components/TabbedContentItem.svelte +14 -0
- package/dist/components/TabbedContentItem.svelte.d.ts +4 -0
- package/dist/components/Table.svelte +69 -0
- package/dist/components/Table.svelte.d.ts +7 -0
- package/dist/components/Tabs.svelte +44 -36
- package/dist/components/Tag.svelte +53 -13
- package/dist/components/Tag.svelte.d.ts +4 -0
- package/dist/components/Theme.svelte +121 -94
- package/dist/components/Theme.svelte.d.ts +7 -6
- package/dist/components/Toast.svelte +11 -8
- package/dist/components/Tooltip.svelte +17 -10
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/css/1-props.css +197 -163
- package/dist/css/2-init.css +519 -0
- package/dist/css/{2-base.css → 3-base.css} +42 -131
- package/dist/css/{3-typo.css → 4-typo.css} +3 -1
- package/dist/css/lutra.css +7 -6
- package/dist/css/themes/DefaultTheme.css +26 -4
- package/dist/form/Button.svelte +20 -0
- package/dist/form/Button.svelte.d.ts +9 -0
- package/dist/form/Datepicker.svelte +13 -0
- package/dist/form/Datepicker.svelte.d.ts +3 -0
- package/dist/form/FieldContent.svelte +20 -11
- package/dist/form/FieldError.svelte +1 -1
- package/dist/form/FieldGroup.svelte +84 -0
- package/dist/form/FieldGroup.svelte.d.ts +20 -0
- package/dist/form/Fieldset.svelte +19 -11
- package/dist/form/Form.svelte +137 -63
- package/dist/form/Form.svelte.d.ts +21 -0
- package/dist/form/FormActions.svelte +21 -3
- package/dist/form/FormActions.svelte.d.ts +3 -0
- package/dist/form/FormSection.svelte +22 -20
- package/dist/form/ImageUpload.svelte +50 -30
- package/dist/form/ImageUpload.svelte.d.ts +14 -0
- package/dist/form/Input.svelte +62 -30
- package/dist/form/Input.svelte.d.ts +0 -1
- package/dist/form/InputLength.svelte +5 -5
- package/dist/form/Label.svelte +6 -6
- package/dist/form/LogoUpload.svelte +24 -10
- package/dist/form/Select.svelte +23 -10
- package/dist/form/Select.svelte.d.ts +6 -6
- package/dist/form/Textarea.svelte +11 -1
- package/dist/form/Toggle.svelte +162 -0
- package/dist/form/Toggle.svelte.d.ts +31 -17
- package/dist/form/client.svelte.js +0 -2
- package/dist/form/index.d.ts +1 -0
- package/dist/form/index.js +1 -0
- package/dist/state/Persisted.svelte.d.ts +6 -0
- package/dist/state/Persisted.svelte.js +29 -0
- package/dist/state/theme.svelte.d.ts +7 -0
- package/dist/state/theme.svelte.js +14 -0
- package/dist/types.d.ts +6 -23
- package/dist/types.js +0 -17
- package/dist/util/color.js +2 -2
- package/package.json +5 -4
- package/dist/config.d.ts +0 -30
- package/dist/config.js +0 -18
- /package/dist/css/{4-layout.css → 5-layout.css} +0 -0
- /package/dist/css/{5-media.css → 6-media.css} +0 -0
package/dist/form/Form.svelte
CHANGED
|
@@ -12,6 +12,30 @@
|
|
|
12
12
|
import { getIndividualValidators, parseFormIssues } from "./form.js";
|
|
13
13
|
import { useForm } from "./client.svelte.js";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @description
|
|
17
|
+
* The root form component. Wraps SvelteKit's `enhance` for progressive enhancement,
|
|
18
|
+
* integrates with Zod-based validation via Bodyguard, and provides layout token
|
|
19
|
+
* overrides for all child form sections, fields and actions.
|
|
20
|
+
*
|
|
21
|
+
* @cssprop --form-action-gap -- Gap between action buttons.
|
|
22
|
+
* @cssprop --form-field-gap -- Gap between fields in a section.
|
|
23
|
+
* @cssprop --form-label-gap -- Gap between label and input.
|
|
24
|
+
* @cssprop --form-title-gap -- Gap within a section title.
|
|
25
|
+
* @cssprop --form-section-gap -- Gap between sections.
|
|
26
|
+
* @cssprop --form-padding-block -- Block padding for contained sections.
|
|
27
|
+
* @cssprop --form-padding-inline -- Inline padding for contained sections.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* <Form form={data.form} action="?/save">
|
|
31
|
+
* <FormSection title="Profile">
|
|
32
|
+
* <Input name="name" label="Name" />
|
|
33
|
+
* </FormSection>
|
|
34
|
+
* <FormActions>
|
|
35
|
+
* <Button type="submit">Save</Button>
|
|
36
|
+
* </FormActions>
|
|
37
|
+
* </Form>
|
|
38
|
+
*/
|
|
15
39
|
let {
|
|
16
40
|
name = 'form',
|
|
17
41
|
form: _form,
|
|
@@ -35,15 +59,25 @@
|
|
|
35
59
|
paddingBlock,
|
|
36
60
|
paddingInline,
|
|
37
61
|
}: {
|
|
62
|
+
/** The key under which the form data is returned in the action result. */
|
|
38
63
|
name?: string;
|
|
64
|
+
/** The Lutra form object from the server load function. */
|
|
39
65
|
form?: LutraForm<any>;
|
|
66
|
+
/** Bindable reference to the underlying `<form>` element. */
|
|
40
67
|
formEl?: HTMLFormElement | null;
|
|
68
|
+
/** The form action URL. */
|
|
41
69
|
action?: string;
|
|
70
|
+
/** The form encoding type. */
|
|
42
71
|
enctype?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain";
|
|
72
|
+
/** The HTTP method. */
|
|
43
73
|
method?: "GET" | "POST";
|
|
74
|
+
/** Callback invoked before the form is submitted. Can cancel submission. */
|
|
44
75
|
beforesubmit?: BeforeSubmitFn;
|
|
76
|
+
/** Whether the form should span the full width. */
|
|
45
77
|
fullWidth?: boolean;
|
|
78
|
+
/** Whether to reset fields after a successful update. */
|
|
46
79
|
resetOnUpdate?: boolean;
|
|
80
|
+
/** Callback invoked with the action result after submission. */
|
|
47
81
|
onresult?: (args: {
|
|
48
82
|
formData: FormData;
|
|
49
83
|
formElement: HTMLFormElement;
|
|
@@ -54,16 +88,27 @@
|
|
|
54
88
|
invalidateAll?: boolean | undefined;
|
|
55
89
|
} | undefined) => void;
|
|
56
90
|
}) => void;
|
|
91
|
+
/** Form content. */
|
|
57
92
|
children: Snippet;
|
|
93
|
+
/** Whether the form sections should be contained (bordered). */
|
|
58
94
|
contained?: boolean;
|
|
95
|
+
/** General spacing override. */
|
|
59
96
|
spacing?: string;
|
|
97
|
+
/** Override for `--form-action-gap`. */
|
|
60
98
|
actionGap?: string;
|
|
99
|
+
/** Override for `--form-field-gap`. */
|
|
61
100
|
fieldGap?: string;
|
|
101
|
+
/** Override for `--form-label-gap`. */
|
|
62
102
|
labelGap?: string;
|
|
103
|
+
/** Override for `--form-title-gap`. */
|
|
63
104
|
titleGap?: string;
|
|
105
|
+
/** Override for `--form-section-gap`. */
|
|
64
106
|
sectionGap?: string;
|
|
107
|
+
/** Shorthand override for both `--form-padding-block` and `--form-padding-inline`. */
|
|
65
108
|
padding?: string;
|
|
109
|
+
/** Override for `--form-padding-block`. */
|
|
66
110
|
paddingBlock?: string;
|
|
111
|
+
/** Override for `--form-padding-inline`. */
|
|
67
112
|
paddingInline?: string;
|
|
68
113
|
} = $props();
|
|
69
114
|
|
|
@@ -85,7 +130,6 @@
|
|
|
85
130
|
const bodyguard = new Bodyguard();
|
|
86
131
|
|
|
87
132
|
function setFormIssuesAndFields(issues: any, fields: any) {
|
|
88
|
-
console.log('setFormIssuesAndFields', issues, fields)
|
|
89
133
|
if(form) {
|
|
90
134
|
form.issues = issues;
|
|
91
135
|
form.fields = fields;
|
|
@@ -126,6 +170,7 @@
|
|
|
126
170
|
};
|
|
127
171
|
|
|
128
172
|
const style = `
|
|
173
|
+
--fcc: ${contained ? 1 : 0};
|
|
129
174
|
${actionGap ? `--form-action-gap: ${actionGap};` : ''}
|
|
130
175
|
${fieldGap ? `--form-field-gap: ${fieldGap};` : ''}
|
|
131
176
|
${labelGap ? `--form-label-gap: ${labelGap};` : ''}
|
|
@@ -146,75 +191,104 @@
|
|
|
146
191
|
</script>
|
|
147
192
|
|
|
148
193
|
<UiContent>
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if(
|
|
166
|
-
cancel();
|
|
167
|
-
}}));
|
|
168
|
-
for(const { id, fn } of beforesubmitFunctions) {
|
|
169
|
-
await Promise.resolve(fn({ form: formElement, data: formData, cancel: () => {
|
|
194
|
+
<div class="FormContainer">
|
|
195
|
+
<form
|
|
196
|
+
{method}
|
|
197
|
+
{action}
|
|
198
|
+
{enctype}
|
|
199
|
+
style={style}
|
|
200
|
+
bind:this={formEl}
|
|
201
|
+
onchange={validate}
|
|
202
|
+
use:enhance={async ({ formElement, formData, action, cancel, submitter }) => {
|
|
203
|
+
// `formElement` is this `<form>` element
|
|
204
|
+
// `formData` is its `FormData` object that's about to be submitted
|
|
205
|
+
// `action` is the URL to which the form is posted
|
|
206
|
+
// calling `cancel()` will prevent the submission
|
|
207
|
+
// `submitter` is the `HTMLElement` that caused the form to be submitted
|
|
208
|
+
if(form) form.state = 'loading';
|
|
209
|
+
//await Promise.resolve(beforesubmit(form));
|
|
210
|
+
if(beforesubmit) await Promise.resolve(beforesubmit({ form: formElement, data: formData, cancel: () => {
|
|
170
211
|
if(form) form.state = 'error';
|
|
171
212
|
cancel();
|
|
172
213
|
}}));
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// `update` is a function which triggers the default logic that would be triggered if this callback wasn't set
|
|
179
|
-
console.log('form result', opts);
|
|
180
|
-
if(onresult) {
|
|
181
|
-
console.log('calling onresult', opts);
|
|
182
|
-
onresult(opts);
|
|
214
|
+
for(const { id, fn } of beforesubmitFunctions) {
|
|
215
|
+
await Promise.resolve(fn({ form: formElement, data: formData, cancel: () => {
|
|
216
|
+
if(form) form.state = 'error';
|
|
217
|
+
cancel();
|
|
218
|
+
}}));
|
|
183
219
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
form.valid = Object.assign({ valid: false }, resultForm).valid ?? false;
|
|
220
|
+
return async (opts) => {
|
|
221
|
+
const { result, update } = opts;
|
|
222
|
+
if(onresult) {
|
|
223
|
+
onresult(opts);
|
|
189
224
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
225
|
+
const resultForm = result.type !== "redirect" && result.type !== "error" ? result?.data![name] : null;
|
|
226
|
+
if(result.type === "success") {
|
|
227
|
+
if(resultForm && form) {
|
|
228
|
+
form.valid = Object.assign({ valid: false }, resultForm).valid ?? false;
|
|
229
|
+
}
|
|
230
|
+
if(form) form.state = 'success';
|
|
231
|
+
update({ reset: !!resetOnUpdate });
|
|
232
|
+
} else if(result.type === "failure") {
|
|
233
|
+
if(resultForm && form) {
|
|
234
|
+
setFormIssuesAndFields(
|
|
235
|
+
Object.assign({ issues: [] }, resultForm).issues, // Have to assign to avoid type error as we cant use `as` here
|
|
236
|
+
Object.assign({ fields: [] }, resultForm).fields,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if(form) form.state = 'error';
|
|
240
|
+
} else if(result.type === "error") {
|
|
241
|
+
console.error('[lutra] Error from form enhance call', result.error, opts);
|
|
242
|
+
if(form) form.state = 'error';
|
|
243
|
+
} else if(result.type === "redirect") {
|
|
244
|
+
if(form) form.state = 'success';
|
|
245
|
+
await goto(result.location);
|
|
201
246
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
//window.location.href = result.location;
|
|
209
|
-
if(form) form.state = 'success';
|
|
210
|
-
await goto(result.location);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
}}
|
|
214
|
-
>
|
|
215
|
-
{@render children()}
|
|
216
|
-
</form>
|
|
247
|
+
};
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
{@render children()}
|
|
251
|
+
</form>
|
|
252
|
+
</div>
|
|
217
253
|
</UiContent>
|
|
218
254
|
|
|
219
255
|
<style>
|
|
256
|
+
.FormContainer {
|
|
257
|
+
container-type: inline-size;
|
|
258
|
+
}
|
|
259
|
+
form {
|
|
260
|
+
display: grid;
|
|
261
|
+
grid-template-columns: fit-content(20%) 1fr;
|
|
262
|
+
border: calc(var(--fcc) * var(--form-border-size)) var(--form-border-style) var(--form-border-color);
|
|
263
|
+
border-radius: calc(var(--fcc) * var(--form-border-radius));
|
|
264
|
+
--form-padding-block: var(--space-md);
|
|
265
|
+
--form-padding-inline: var(--space-md);
|
|
266
|
+
--form-section-gap: var(--space-md);
|
|
267
|
+
}
|
|
268
|
+
@container (min-width: 600px) {
|
|
269
|
+
form {
|
|
270
|
+
--form-padding-block: var(--space-lg);
|
|
271
|
+
--form-padding-inline: var(--space-lg);
|
|
272
|
+
--form-section-gap: var(--space-xl);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
@container (min-width: 1000px) {
|
|
276
|
+
form {
|
|
277
|
+
--form-padding-block: var(--space-xl);
|
|
278
|
+
--form-padding-inline: var(--space-xl);
|
|
279
|
+
--form-section-gap: var(--space-xxl);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
@container (min-width: 1400px) {
|
|
283
|
+
form {
|
|
284
|
+
--form-padding-block: var(--space-xxl);
|
|
285
|
+
--form-padding-inline: var(--space-xxl);
|
|
286
|
+
--form-section-gap: var(--space-xxxl);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
@container (max-width: 800px) {
|
|
290
|
+
form {
|
|
291
|
+
grid-template-columns: 1fr;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
220
294
|
</style>
|
|
@@ -2,15 +2,25 @@ import type { ActionResult } from "@sveltejs/kit";
|
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import type { BeforeSubmitFn, LutraForm } from "./types.js";
|
|
4
4
|
type $$ComponentProps = {
|
|
5
|
+
/** The key under which the form data is returned in the action result. */
|
|
5
6
|
name?: string;
|
|
7
|
+
/** The Lutra form object from the server load function. */
|
|
6
8
|
form?: LutraForm<any>;
|
|
9
|
+
/** Bindable reference to the underlying `<form>` element. */
|
|
7
10
|
formEl?: HTMLFormElement | null;
|
|
11
|
+
/** The form action URL. */
|
|
8
12
|
action?: string;
|
|
13
|
+
/** The form encoding type. */
|
|
9
14
|
enctype?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain";
|
|
15
|
+
/** The HTTP method. */
|
|
10
16
|
method?: "GET" | "POST";
|
|
17
|
+
/** Callback invoked before the form is submitted. Can cancel submission. */
|
|
11
18
|
beforesubmit?: BeforeSubmitFn;
|
|
19
|
+
/** Whether the form should span the full width. */
|
|
12
20
|
fullWidth?: boolean;
|
|
21
|
+
/** Whether to reset fields after a successful update. */
|
|
13
22
|
resetOnUpdate?: boolean;
|
|
23
|
+
/** Callback invoked with the action result after submission. */
|
|
14
24
|
onresult?: (args: {
|
|
15
25
|
formData: FormData;
|
|
16
26
|
formElement: HTMLFormElement;
|
|
@@ -21,16 +31,27 @@ type $$ComponentProps = {
|
|
|
21
31
|
invalidateAll?: boolean | undefined;
|
|
22
32
|
} | undefined) => void;
|
|
23
33
|
}) => void;
|
|
34
|
+
/** Form content. */
|
|
24
35
|
children: Snippet;
|
|
36
|
+
/** Whether the form sections should be contained (bordered). */
|
|
25
37
|
contained?: boolean;
|
|
38
|
+
/** General spacing override. */
|
|
26
39
|
spacing?: string;
|
|
40
|
+
/** Override for `--form-action-gap`. */
|
|
27
41
|
actionGap?: string;
|
|
42
|
+
/** Override for `--form-field-gap`. */
|
|
28
43
|
fieldGap?: string;
|
|
44
|
+
/** Override for `--form-label-gap`. */
|
|
29
45
|
labelGap?: string;
|
|
46
|
+
/** Override for `--form-title-gap`. */
|
|
30
47
|
titleGap?: string;
|
|
48
|
+
/** Override for `--form-section-gap`. */
|
|
31
49
|
sectionGap?: string;
|
|
50
|
+
/** Shorthand override for both `--form-padding-block` and `--form-padding-inline`. */
|
|
32
51
|
padding?: string;
|
|
52
|
+
/** Override for `--form-padding-block`. */
|
|
33
53
|
paddingBlock?: string;
|
|
54
|
+
/** Override for `--form-padding-inline`. */
|
|
34
55
|
paddingInline?: string;
|
|
35
56
|
};
|
|
36
57
|
declare const Form: import("svelte").Component<$$ComponentProps, {}, "formEl">;
|
|
@@ -2,20 +2,38 @@
|
|
|
2
2
|
import StringOrSnippet from "../util/StringOrSnippet.svelte";
|
|
3
3
|
import { setContext, type Snippet } from "svelte";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @description
|
|
7
|
+
* Action bar for form buttons. Renders at the bottom of a form with configurable
|
|
8
|
+
* alignment. When the form is contained, inherits background and padding from
|
|
9
|
+
* `--form-background-actions` via the `--fcc` multiplier pattern.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <FormActions>
|
|
13
|
+
* <Button type="submit">Save</Button>
|
|
14
|
+
* </FormActions>
|
|
15
|
+
* <FormActions align="justified" info="Changes are saved automatically.">
|
|
16
|
+
* <Button type="submit">Save</Button>
|
|
17
|
+
* <Button kind="secondary" href="/cancel">Cancel</Button>
|
|
18
|
+
* </FormActions>
|
|
19
|
+
*/
|
|
5
20
|
let {
|
|
6
21
|
align = 'end',
|
|
7
22
|
children,
|
|
8
23
|
info,
|
|
9
24
|
}: {
|
|
25
|
+
/** Alignment of the action buttons. */
|
|
10
26
|
align?: 'justified' | 'start' | 'center' | 'end' | 'full';
|
|
27
|
+
/** Action buttons and other content. */
|
|
11
28
|
children: Snippet;
|
|
29
|
+
/** Optional info text displayed alongside the action buttons. */
|
|
12
30
|
info?: string | Snippet;
|
|
13
31
|
} = $props();
|
|
14
32
|
|
|
15
33
|
setContext('form.actions.align', align);
|
|
16
34
|
</script>
|
|
17
35
|
|
|
18
|
-
<div class="FormActions {align}"
|
|
36
|
+
<div class="FormActions {align}">
|
|
19
37
|
{#if info}
|
|
20
38
|
<div class="Info">
|
|
21
39
|
<StringOrSnippet content={info} />
|
|
@@ -29,7 +47,7 @@
|
|
|
29
47
|
<style>
|
|
30
48
|
.FormActions {
|
|
31
49
|
display: grid;
|
|
32
|
-
background: color-mix(in
|
|
50
|
+
background: color-mix(in oklch, var(--form-background-actions) calc(var(--fcc) * 100%), transparent);
|
|
33
51
|
padding: calc(var(--space-md) * var(--fcc)) calc(var(--space-xl) * var(--fcc));
|
|
34
52
|
grid-column: 1 / -1;
|
|
35
53
|
grid-template-columns: subgrid;
|
|
@@ -57,7 +75,7 @@
|
|
|
57
75
|
justify-content: end;
|
|
58
76
|
grid-column: 1 / -1;
|
|
59
77
|
}
|
|
60
|
-
.FormActions.
|
|
78
|
+
.FormActions:has(.Info) .Actions {
|
|
61
79
|
grid-column: 2 / -1;
|
|
62
80
|
}
|
|
63
81
|
.FormActions.full .Actions {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type Snippet } from "svelte";
|
|
2
2
|
type $$ComponentProps = {
|
|
3
|
+
/** Alignment of the action buttons. */
|
|
3
4
|
align?: 'justified' | 'start' | 'center' | 'end' | 'full';
|
|
5
|
+
/** Action buttons and other content. */
|
|
4
6
|
children: Snippet;
|
|
7
|
+
/** Optional info text displayed alongside the action buttons. */
|
|
5
8
|
info?: string | Snippet;
|
|
6
9
|
};
|
|
7
10
|
declare const FormActions: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -3,7 +3,21 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @description
|
|
6
|
-
* A
|
|
6
|
+
* A section within a form, grouping related fields under an optional title and
|
|
7
|
+
* description. Uses CSS subgrid for alignment with the parent form layout and
|
|
8
|
+
* responds to `@container` queries to collapse into a single column at narrow widths.
|
|
9
|
+
*
|
|
10
|
+
* @cssprop --form-section-gap -- Gap between the title and content areas.
|
|
11
|
+
* @cssprop --form-title-gap -- Gap within the section title (title + description).
|
|
12
|
+
* @cssprop --form-field-gap -- Gap between fields in the content area.
|
|
13
|
+
* @cssprop --form-padding-block -- Block padding when the form is contained.
|
|
14
|
+
* @cssprop --form-padding-inline -- Inline padding when the form is contained.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <FormSection title="Personal Information" description="Enter your details.">
|
|
18
|
+
* <Input name="name" label="Name" />
|
|
19
|
+
* <Input name="email" type="email" label="Email" />
|
|
20
|
+
* </FormSection>
|
|
7
21
|
*/
|
|
8
22
|
let {
|
|
9
23
|
title,
|
|
@@ -53,12 +67,11 @@
|
|
|
53
67
|
.FormSectionTitle {
|
|
54
68
|
display: flex;
|
|
55
69
|
flex-direction: column;
|
|
56
|
-
background-color: var(--base);
|
|
57
70
|
gap: var(--form-title-gap, var(--space-md));
|
|
58
71
|
text-wrap: balance;
|
|
59
72
|
}
|
|
60
73
|
.FormSection:not(:first-child) {
|
|
61
|
-
border-top: calc(var(--fcc) * var(--border-size)) var(--border-style) var(--border-color);
|
|
74
|
+
border-top: calc(var(--fcc) * var(--form-border-size)) var(--form-border-style) var(--form-border-color);
|
|
62
75
|
}
|
|
63
76
|
.FormSectionContent {
|
|
64
77
|
display: grid;
|
|
@@ -67,27 +80,16 @@
|
|
|
67
80
|
.FormSection.noTitle .FormSectionContent {
|
|
68
81
|
grid-column: 1 / -1;
|
|
69
82
|
}
|
|
70
|
-
@
|
|
71
|
-
.FormSection {
|
|
72
|
-
padding: var(--space-xl);
|
|
73
|
-
gap: var(--space-md);
|
|
74
|
-
}
|
|
75
|
-
.FormSectionTitle {
|
|
76
|
-
gap: var(--space-xs);
|
|
77
|
-
padding-block-end: var(--space-md);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
@media(max-width: 640px) {
|
|
83
|
+
@container (max-width: 800px) {
|
|
81
84
|
.FormSection {
|
|
82
|
-
padding: var(--
|
|
83
|
-
|
|
85
|
+
padding-block: calc(var(--fcc) * var(--form-padding-block));
|
|
86
|
+
padding-inline: calc(var(--fcc) * var(--form-padding-inline));
|
|
87
|
+
gap: var(--form-section-gap, var(--space-md));
|
|
84
88
|
}
|
|
85
89
|
.FormSectionTitle {
|
|
86
|
-
gap: var(--
|
|
87
|
-
padding-block-end: var(--
|
|
90
|
+
gap: var(--form-title-gap);
|
|
91
|
+
padding-block-end: var(--form-title-padding-block-end);
|
|
88
92
|
}
|
|
89
|
-
}
|
|
90
|
-
@container (max-width: 800px) {
|
|
91
93
|
.FormSectionTitle,
|
|
92
94
|
.FormSectionContent {
|
|
93
95
|
grid-column: 1 / -1;
|
|
@@ -9,6 +9,23 @@
|
|
|
9
9
|
const imageCompression: ImageCompressionFn = (imageCompressionModule as unknown as { default?: ImageCompressionFn }).default ?? imageCompressionModule as unknown as ImageCompressionFn;
|
|
10
10
|
import { addToast } from "../components/toasts.svelte.js";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @description
|
|
14
|
+
* An image upload component with client-side compression, progress indicator and
|
|
15
|
+
* optional remove button. Supports square, circle and banner display shapes.
|
|
16
|
+
* Uploads to a presigned URL via `PUT`.
|
|
17
|
+
*
|
|
18
|
+
* @cssprop --max-image-width -- Maximum width of the image preview (used in the grid layout).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <ImageUpload
|
|
22
|
+
* uploadUrl="/api/upload"
|
|
23
|
+
* name="avatar"
|
|
24
|
+
* button="Upload"
|
|
25
|
+
* shape="circle"
|
|
26
|
+
* src={user.avatarUrl}
|
|
27
|
+
* />
|
|
28
|
+
*/
|
|
12
29
|
let {
|
|
13
30
|
src,
|
|
14
31
|
uploadUrl,
|
|
@@ -25,19 +42,33 @@
|
|
|
25
42
|
onFile,
|
|
26
43
|
children,
|
|
27
44
|
}: {
|
|
45
|
+
/** Current image source URL. */
|
|
28
46
|
src?: string | null;
|
|
47
|
+
/** The presigned URL to upload the image to. */
|
|
29
48
|
uploadUrl: string;
|
|
49
|
+
/** The form field name for the resulting URL value. */
|
|
30
50
|
name: string;
|
|
51
|
+
/** HTML id for the file input. */
|
|
31
52
|
id?: string;
|
|
53
|
+
/** Title used in the alt text of the preview image. */
|
|
32
54
|
title?: string;
|
|
55
|
+
/** Label for the upload button. */
|
|
33
56
|
button: string;
|
|
57
|
+
/** Maximum file size in bytes before compression is applied. */
|
|
34
58
|
maxSize?: number;
|
|
59
|
+
/** Maximum compressed file size in megabytes. */
|
|
35
60
|
compressSize?: number;
|
|
61
|
+
/** Maximum compressed image width in pixels. */
|
|
36
62
|
compressMaxWidth?: number;
|
|
63
|
+
/** Accepted MIME types for the file input. */
|
|
37
64
|
accept?: string;
|
|
65
|
+
/** Whether a remove button is shown. */
|
|
38
66
|
removable?: boolean;
|
|
67
|
+
/** Display shape of the image preview. */
|
|
39
68
|
shape?: 'square' | 'circle' | 'banner';
|
|
69
|
+
/** Callback invoked with the selected file before upload. */
|
|
40
70
|
onFile?: (file: File) => void;
|
|
71
|
+
/** Optional additional content rendered below the actions. */
|
|
41
72
|
children?: Snippet;
|
|
42
73
|
} = $props();
|
|
43
74
|
|
|
@@ -63,8 +94,6 @@
|
|
|
63
94
|
useWebWorker: true,
|
|
64
95
|
onProgress: (progress: number) => resizeProgress = progress,
|
|
65
96
|
});
|
|
66
|
-
console.log('compressedFile instanceof Blob', compressedFile instanceof Blob); // true
|
|
67
|
-
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
|
|
68
97
|
uploadState = 'uploading';
|
|
69
98
|
|
|
70
99
|
const response = await fetch(uploadUrl, {
|
|
@@ -79,15 +108,10 @@
|
|
|
79
108
|
addToast({ content: 'Something went wrong. Please try again.', autoClose: 3000 })
|
|
80
109
|
cancel();
|
|
81
110
|
}
|
|
82
|
-
//src = new URL(response.url).href.split('?')[0];
|
|
83
|
-
console.log('setting', name, new URL(response.url).href.split('?')[0]);
|
|
84
111
|
const imgUrl = new URL(response.url).href.split('?')[0];
|
|
85
112
|
data.set(name, imgUrl);
|
|
86
113
|
src = imgUrl;
|
|
87
|
-
console.log('response', response, response.url);
|
|
88
|
-
console.log('data', data.forEach((v, k) => console.log(k, v)));
|
|
89
114
|
} catch(err) {
|
|
90
|
-
console.error('error', err);
|
|
91
115
|
addToast({ content: 'Something went wrong. Please try again.', autoClose: 3000 })
|
|
92
116
|
cancel();
|
|
93
117
|
}
|
|
@@ -141,7 +165,7 @@
|
|
|
141
165
|
</script>
|
|
142
166
|
|
|
143
167
|
<div class="ImageUpload {shape}">
|
|
144
|
-
<div class="Image"
|
|
168
|
+
<div class="Image">
|
|
145
169
|
{#if src}
|
|
146
170
|
<img src={src} alt="{title} logo" />
|
|
147
171
|
{/if}
|
|
@@ -194,16 +218,16 @@
|
|
|
194
218
|
display: grid;
|
|
195
219
|
grid-template-columns: 1fr;
|
|
196
220
|
align-items: center;
|
|
197
|
-
gap:
|
|
221
|
+
gap: var(--space-md);
|
|
198
222
|
container-type: inline-size;
|
|
199
223
|
}
|
|
200
224
|
.Image {
|
|
201
|
-
border:
|
|
225
|
+
border: var(--border-size-thin) solid var(--border-color-subtle);
|
|
202
226
|
pointer-events: none;
|
|
203
227
|
position: relative;
|
|
204
228
|
display: none;
|
|
205
229
|
}
|
|
206
|
-
.Image
|
|
230
|
+
.Image:has(img) {
|
|
207
231
|
display: block;
|
|
208
232
|
}
|
|
209
233
|
.Image img {
|
|
@@ -229,32 +253,29 @@
|
|
|
229
253
|
height: 100%;
|
|
230
254
|
width: 100%;
|
|
231
255
|
object-fit: contain;
|
|
232
|
-
padding:
|
|
256
|
+
padding: var(--space-md);
|
|
233
257
|
text-align: center;
|
|
234
|
-
border:
|
|
235
|
-
border-radius: var(--border-radius);
|
|
258
|
+
border: var(--border-size-thin) dashed var(--border-color);
|
|
259
|
+
border-radius: var(--border-radius-base);
|
|
236
260
|
}
|
|
237
261
|
.Loading {
|
|
238
262
|
position: absolute;
|
|
239
|
-
|
|
240
|
-
left: 0;
|
|
241
|
-
right: 0;
|
|
242
|
-
bottom: 0;
|
|
263
|
+
inset: 0;
|
|
243
264
|
z-index: 5;
|
|
244
|
-
backdrop-filter:
|
|
245
|
-
background: var(--
|
|
265
|
+
backdrop-filter: var(--scrim-backdrop-filter);
|
|
266
|
+
background: var(--scrim-background);
|
|
246
267
|
display: flex;
|
|
247
268
|
align-items: center;
|
|
248
269
|
justify-content: center;
|
|
249
270
|
flex-direction: column;
|
|
250
|
-
gap:
|
|
251
|
-
padding:
|
|
271
|
+
gap: var(--space-md);
|
|
272
|
+
padding: var(--space-md);
|
|
252
273
|
}
|
|
253
274
|
input {
|
|
254
275
|
display: none;
|
|
255
276
|
}
|
|
256
277
|
.error {
|
|
257
|
-
margin-
|
|
278
|
+
margin-block-start: var(--space-sm);
|
|
258
279
|
}
|
|
259
280
|
.ImageUpload.banner {
|
|
260
281
|
grid-template-areas: "image";
|
|
@@ -266,7 +287,6 @@
|
|
|
266
287
|
overflow: hidden;
|
|
267
288
|
object-fit: cover;
|
|
268
289
|
height: 15rem;
|
|
269
|
-
aspect-ratio: initial !important;
|
|
270
290
|
}
|
|
271
291
|
.ImageUpload.banner .Image img {
|
|
272
292
|
object-fit: cover;
|
|
@@ -276,28 +296,28 @@
|
|
|
276
296
|
grid-area: image;
|
|
277
297
|
flex-direction: column;
|
|
278
298
|
justify-content: center;
|
|
279
|
-
padding:
|
|
299
|
+
padding: var(--space-md);
|
|
280
300
|
}
|
|
281
301
|
.ImageUpload.banner:hover .ImageUploadActions {
|
|
282
|
-
background: var(--
|
|
302
|
+
background: var(--scrim-background);
|
|
283
303
|
}
|
|
284
304
|
.ImageUploadActions {
|
|
285
305
|
display: grid;
|
|
286
306
|
grid-template-columns: 1fr 1fr;
|
|
287
|
-
gap:
|
|
307
|
+
gap: var(--space-sm);
|
|
288
308
|
align-items: start;
|
|
289
309
|
}
|
|
290
310
|
@container (min-width: 320px) {
|
|
291
311
|
.ImageUpload:not(.banner) {
|
|
292
312
|
grid-template-columns: minmax(auto, var(--max-image-width)) 1fr;
|
|
293
|
-
gap:
|
|
313
|
+
gap: var(--space-xl);
|
|
294
314
|
}
|
|
295
315
|
.ImageUpload.banner .ImageUploadActions {
|
|
296
|
-
padding:
|
|
316
|
+
padding: var(--space-xxl);
|
|
297
317
|
}
|
|
298
318
|
.ImageUploadActions {
|
|
299
319
|
grid-template-columns: 1fr;
|
|
300
|
-
gap:
|
|
320
|
+
gap: var(--space-md);
|
|
301
321
|
}
|
|
302
322
|
}
|
|
303
323
|
</style>
|