lutra 0.1.68 → 0.1.69
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/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 +39 -12
- 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/css/1-props.css +64 -51
- package/dist/css/2-init.css +503 -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 +16 -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 +18 -9
- package/dist/form/FieldError.svelte +1 -1
- 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/client.svelte.js +0 -2
- 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
|
@@ -16,13 +16,15 @@ small {
|
|
|
16
16
|
line-height: var(--font-line-height-tight);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
small.smaller
|
|
19
|
+
small.smaller,
|
|
20
|
+
small small {
|
|
20
21
|
font-size: var(--font-size-xs);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/* Form elements inherit base typography */
|
|
24
25
|
input, textarea, select, button {
|
|
25
26
|
font-family: var(--font-family);
|
|
27
|
+
font-size: inherit;
|
|
26
28
|
line-height: var(--font-line-height);
|
|
27
29
|
}
|
|
28
30
|
|
package/dist/css/lutra.css
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
@layer l-
|
|
1
|
+
@layer l-props, l-init, l-base, l-typo, typo, l-layout, l-media, l-default, theme, media, misc;
|
|
2
2
|
|
|
3
|
-
@import "./1-props.css";
|
|
4
|
-
@import "./2-
|
|
5
|
-
@import "./3-
|
|
6
|
-
@import "./4-
|
|
7
|
-
@import "./5-
|
|
3
|
+
@import "./1-props.css" layer(l-props);
|
|
4
|
+
@import "./2-init.css" layer(l-init);
|
|
5
|
+
@import "./3-base.css" layer(l-base);
|
|
6
|
+
@import "./4-typo.css" layer(l-typo);
|
|
7
|
+
@import "./5-layout.css" layer(l-layout);
|
|
8
|
+
@import "./6-media.css" layer(l-media);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
@layer theme
|
|
1
|
+
@layer l-props, l-init, l-base, l-typo, typo, l-layout, l-media, l-default, theme, media, misc;
|
|
2
|
+
|
|
3
|
+
@layer l-default {
|
|
2
4
|
:root {
|
|
3
5
|
--hue: 240deg;
|
|
4
6
|
--chroma: 0.5;
|
|
@@ -172,7 +174,8 @@
|
|
|
172
174
|
|
|
173
175
|
--field-label-color: var(--text-color-p);
|
|
174
176
|
|
|
175
|
-
--form-background
|
|
177
|
+
--form-background: var(--background-body);
|
|
178
|
+
--form-background-actions: color-mix(in oklch, var(--theme-surface-interactive) 35%, transparent);
|
|
176
179
|
|
|
177
180
|
/**
|
|
178
181
|
* Borders
|
|
@@ -201,12 +204,20 @@
|
|
|
201
204
|
--link-color-hover: var(--button-submit-base-color-hover);
|
|
202
205
|
--link-color-active: var(--button-submit-base-color-hover);
|
|
203
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Status Colors
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
--status-default-color: light-dark(#111111, #e0e0e0);
|
|
212
|
+
|
|
204
213
|
/**
|
|
205
214
|
* Menus
|
|
206
215
|
*/
|
|
207
216
|
|
|
208
217
|
--menu-background-color: var(--surface-background);
|
|
209
|
-
--menu-background-color-hover: color-mix(in
|
|
218
|
+
--menu-background-color-hover: color-mix(in oklch, var(--theme-surface-interactive) 35%, transparent);
|
|
219
|
+
--menu-text-color: var(--text-color-p);
|
|
220
|
+
--menu-text-color-subtle: var(--text-color-p-subtle);
|
|
210
221
|
--menu-border-color: var(--border-color-subtle);
|
|
211
222
|
--menu-border-size: var(--border-size-thin);
|
|
212
223
|
--menu-border-style: var(--border-style);
|
|
@@ -221,7 +232,7 @@
|
|
|
221
232
|
--table-header-color: var(--text-color-heading);
|
|
222
233
|
--table-row-background: transparent;
|
|
223
234
|
--table-row-background-even: transparent;
|
|
224
|
-
--table-row-background-hover: color-mix(in
|
|
235
|
+
--table-row-background-hover: color-mix(in oklch, var(--theme-surface-interactive) 60%, transparent);
|
|
225
236
|
--table-cell-color: var(--text-color-p);
|
|
226
237
|
|
|
227
238
|
/**
|
|
@@ -270,6 +281,7 @@
|
|
|
270
281
|
--toast-border: var(--surface-border);
|
|
271
282
|
--toast-border-radius: var(--surface-border-radius);
|
|
272
283
|
--toast-shadow: var(--surface-shadow);
|
|
284
|
+
--toast-title-color: var(--text-color-heading);
|
|
273
285
|
|
|
274
286
|
/**
|
|
275
287
|
* Scrim/Backdrop
|
package/dist/form/Button.svelte
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
import { getContext, type Component, type Snippet } from 'svelte';
|
|
4
4
|
import type { LutraForm } from './types.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @description
|
|
8
|
+
* A polymorphic form button that renders as a `<button>` or `<a>` depending on whether `href` is set.
|
|
9
|
+
* Automatically disables itself while its parent form is in a loading state.
|
|
10
|
+
* Inherits the alignment context set by a parent `FormActions` component.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Button type="submit" kind="default">Save</Button>
|
|
14
|
+
* <Button href="/cancel" kind="secondary">Cancel</Button>
|
|
15
|
+
* <Button kind="warn" icon={TrashIcon}>Delete</Button>
|
|
16
|
+
*/
|
|
6
17
|
let {
|
|
7
18
|
href,
|
|
8
19
|
type = 'button',
|
|
@@ -14,14 +25,23 @@
|
|
|
14
25
|
onclick,
|
|
15
26
|
children,
|
|
16
27
|
}: {
|
|
28
|
+
/** When set, renders as an `<a>` link instead of a `<button>`. */
|
|
17
29
|
href?: string;
|
|
30
|
+
/** The button type attribute. */
|
|
18
31
|
type?: 'button' | 'submit' | 'reset';
|
|
32
|
+
/** Visual style variant. */
|
|
19
33
|
kind?: 'default' | 'outlined' | 'secondary' | 'warn';
|
|
34
|
+
/** Size variant. */
|
|
20
35
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
36
|
+
/** Additional CSS class names. */
|
|
21
37
|
class?: string;
|
|
38
|
+
/** Whether the button is disabled. Also disabled automatically when the parent form is loading. */
|
|
22
39
|
disabled?: boolean;
|
|
40
|
+
/** Icon component or string to render before the label. */
|
|
23
41
|
icon?: string | Component;
|
|
42
|
+
/** Click event handler. */
|
|
24
43
|
onclick?: (event: MouseEvent) => void;
|
|
44
|
+
/** Button label content. */
|
|
25
45
|
children: Snippet;
|
|
26
46
|
} = $props();
|
|
27
47
|
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { type Component, type Snippet } from 'svelte';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
|
+
/** When set, renders as an `<a>` link instead of a `<button>`. */
|
|
3
4
|
href?: string;
|
|
5
|
+
/** The button type attribute. */
|
|
4
6
|
type?: 'button' | 'submit' | 'reset';
|
|
7
|
+
/** Visual style variant. */
|
|
5
8
|
kind?: 'default' | 'outlined' | 'secondary' | 'warn';
|
|
9
|
+
/** Size variant. */
|
|
6
10
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
11
|
+
/** Additional CSS class names. */
|
|
7
12
|
class?: string;
|
|
13
|
+
/** Whether the button is disabled. Also disabled automatically when the parent form is loading. */
|
|
8
14
|
disabled?: boolean;
|
|
15
|
+
/** Icon component or string to render before the label. */
|
|
9
16
|
icon?: string | Component;
|
|
17
|
+
/** Click event handler. */
|
|
10
18
|
onclick?: (event: MouseEvent) => void;
|
|
19
|
+
/** Button label content. */
|
|
11
20
|
children: Snippet;
|
|
12
21
|
};
|
|
13
22
|
declare const Button: Component<$$ComponentProps, {}, "">;
|
|
@@ -4,11 +4,24 @@
|
|
|
4
4
|
import UiContent from "../components/UIContent.svelte";
|
|
5
5
|
import { getLocaleFirstDayOfWeek } from "../util/locale.js";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @description
|
|
9
|
+
* A custom date/time range picker with year, month and day grids.
|
|
10
|
+
* Renders start and end `datetime-local` inputs alongside visual selection components
|
|
11
|
+
* for year, month and day. Respects the user's locale for first day of week.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <Datepicker />
|
|
15
|
+
* <Datepicker range={{ min: new Date('2024-01-01'), max: new Date('2025-12-31') }} />
|
|
16
|
+
*/
|
|
7
17
|
let {
|
|
8
18
|
range
|
|
9
19
|
}: {
|
|
20
|
+
/** Optional date range constraints. */
|
|
10
21
|
range?: {
|
|
22
|
+
/** Minimum selectable date. */
|
|
11
23
|
min?: Date;
|
|
24
|
+
/** Maximum selectable date. */
|
|
12
25
|
max?: Date;
|
|
13
26
|
}
|
|
14
27
|
} = $props();
|
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @description
|
|
10
|
-
*
|
|
10
|
+
* Internal wrapper used by Input, Select and Textarea to render the label, field
|
|
11
|
+
* border, prefix/suffix decorations, help text and error messages in a consistent layout.
|
|
12
|
+
*
|
|
13
|
+
* @cssprop --field-background -- Background color of the field container.
|
|
14
|
+
* @cssprop --field-border-size -- Border width of the field container.
|
|
15
|
+
* @cssprop --field-border-style -- Border style of the field container.
|
|
16
|
+
* @cssprop --field-border-color -- Border color of the field container.
|
|
17
|
+
* @cssprop --field-border-radius -- Border radius of the field container.
|
|
11
18
|
*/
|
|
12
19
|
let {
|
|
13
20
|
id,
|
|
@@ -62,8 +69,7 @@
|
|
|
62
69
|
{#if contained}
|
|
63
70
|
<div
|
|
64
71
|
class="Field"
|
|
65
|
-
|
|
66
|
-
class:hasSuffix={suffix}
|
|
72
|
+
|
|
67
73
|
class:invalid={field?.tainted && issue?.code}
|
|
68
74
|
>
|
|
69
75
|
{#if prefix}
|
|
@@ -105,13 +111,15 @@
|
|
|
105
111
|
.FieldContentContainer {
|
|
106
112
|
display: flex;
|
|
107
113
|
flex-direction: column;
|
|
108
|
-
gap: var(--form-field-gap
|
|
114
|
+
gap: var(--form-field-inside-gap);
|
|
115
|
+
min-width: 0;
|
|
109
116
|
}
|
|
110
117
|
.FieldContent {
|
|
111
118
|
display: flex;
|
|
112
119
|
gap: var(--form-label-gap, var(--space-xs));
|
|
113
120
|
flex-direction: column;
|
|
114
121
|
font-size: 1em;
|
|
122
|
+
min-width: 0;
|
|
115
123
|
}
|
|
116
124
|
.FieldContent.row {
|
|
117
125
|
flex-direction: row-reverse;
|
|
@@ -124,6 +132,7 @@
|
|
|
124
132
|
border: var(--field-border-size) var(--field-border-style) var(--field-border-color);
|
|
125
133
|
border-radius: var(--field-border-radius);
|
|
126
134
|
display: flex;
|
|
135
|
+
min-width: 0;
|
|
127
136
|
}
|
|
128
137
|
.Field.invalid {
|
|
129
138
|
border-color: var(--field-border-color-invalid);
|
|
@@ -138,7 +147,7 @@
|
|
|
138
147
|
padding-inline: var(--form-field-gap, var(--space-md));
|
|
139
148
|
font-size: 1em;
|
|
140
149
|
text-box: trim-both cap alphabetic;
|
|
141
|
-
color: var(--text-subtle);
|
|
150
|
+
color: var(--text-color-p-subtle);
|
|
142
151
|
}
|
|
143
152
|
.Suffix {
|
|
144
153
|
padding-inline-start: 0;
|
|
@@ -158,16 +167,16 @@
|
|
|
158
167
|
outline-color: var(--focus-ring-color-invalid);
|
|
159
168
|
border-color: var(--focus-ring-color-invalid);
|
|
160
169
|
}
|
|
161
|
-
.Field.
|
|
170
|
+
.Field:has(.Prefix) :global(input) {
|
|
162
171
|
padding-inline-start: var(--space-xxs);
|
|
163
172
|
}
|
|
164
173
|
.Field :global(button) {
|
|
165
|
-
margin-
|
|
174
|
+
margin-inline-end: var(--space-xxs);
|
|
166
175
|
}
|
|
167
176
|
.Field :global(button:focus-visible) {
|
|
168
|
-
outline: var(--focus-
|
|
177
|
+
outline: var(--focus-ring);
|
|
169
178
|
outline-offset: 3px;
|
|
170
|
-
border-radius: calc(var(--field-radius) - 2px);
|
|
179
|
+
border-radius: calc(var(--field-border-radius) - 2px);
|
|
171
180
|
}
|
|
172
181
|
.Help {
|
|
173
182
|
font-size: min(11px, var(--font-size-075));
|
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @description
|
|
6
|
-
* A fieldset is a group of related form elements
|
|
6
|
+
* A fieldset is a group of related form elements. Supports responsive column
|
|
7
|
+
* layouts via container queries, optional contained styling with padding,
|
|
8
|
+
* and rounded borders.
|
|
9
|
+
*
|
|
10
|
+
* @cssprop --form-field-gap -- Gap between fields within the fieldset.
|
|
11
|
+
* @cssprop --form-padding-block -- Block padding when contained.
|
|
12
|
+
* @cssprop --form-padding-inline -- Inline padding when contained.
|
|
13
|
+
* @cssprop --field-label-font-size -- Font size for the legend.
|
|
14
|
+
*
|
|
7
15
|
* @example
|
|
8
|
-
* <
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* <Fieldset legend="Personal Information">
|
|
12
|
-
* <Input label="First Name" />
|
|
13
|
-
* <Input label="Last Name" />
|
|
16
|
+
* <Fieldset legend="Personal Information" columns={2}>
|
|
17
|
+
* <Input label="First Name" name="first" />
|
|
18
|
+
* <Input label="Last Name" name="last" />
|
|
14
19
|
* </Fieldset>
|
|
15
20
|
*/
|
|
16
21
|
|
|
@@ -76,16 +81,19 @@
|
|
|
76
81
|
}
|
|
77
82
|
fieldset {
|
|
78
83
|
display: grid;
|
|
84
|
+
border: none;
|
|
85
|
+
margin: 0;
|
|
86
|
+
padding: 0;
|
|
79
87
|
width: var(--width, fit-content);
|
|
80
88
|
grid-template-columns: repeat(var(--lg-cols), 1fr);
|
|
81
|
-
gap: var(--gap
|
|
89
|
+
gap: var(--form-field-gap);
|
|
82
90
|
}
|
|
83
91
|
legend {
|
|
84
|
-
font-weight:
|
|
85
|
-
font-size: var(--font-size
|
|
92
|
+
font-weight: var(--font-weight-normal);
|
|
93
|
+
font-size: var(--field-label-font-size);
|
|
86
94
|
}
|
|
87
95
|
fieldset.contained {
|
|
88
|
-
padding:
|
|
96
|
+
padding: var(--form-padding-block) var(--form-padding-inline);
|
|
89
97
|
}
|
|
90
98
|
fieldset.fullWidth {
|
|
91
99
|
width: 100%;
|
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">;
|