lutra 0.0.6 → 0.0.7

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.
@@ -1,16 +1,10 @@
1
1
  <script>import UiContent from "../layout/UIContent.svelte";
2
2
  import { setContext } from "svelte";
3
3
  let {
4
+ columns,
4
5
  contained,
5
6
  children
6
7
  } = $props();
7
- let columns = $state(0);
8
- setContext("DataTableColumns", {
9
- addColumn: () => {
10
- console.log("addColumn");
11
- columns++;
12
- }
13
- });
14
8
  </script>
15
9
 
16
10
  <UiContent>
@@ -31,6 +25,6 @@ setContext("DataTableColumns", {
31
25
  section.contained {
32
26
  border: var(--border);
33
27
  border-radius: var(--border-radius);
34
- overflow: clip;
28
+ overflow-x: auto;
35
29
  }
36
30
  </style>
@@ -2,6 +2,7 @@ import { SvelteComponent } from "svelte";
2
2
  import { type Snippet } from "svelte";
3
3
  declare const __propDef: {
4
4
  props: {
5
+ columns: number;
5
6
  /** The number of columns to display the data in. */
6
7
  contained?: boolean | undefined;
7
8
  children: Snippet;
@@ -1,6 +1,7 @@
1
1
  <script>import { setContext } from "svelte";
2
2
  let {
3
3
  children,
4
+ actions,
4
5
  header = false
5
6
  } = $props();
6
7
  setContext("DataTableRow", { header });
@@ -8,6 +9,11 @@ setContext("DataTableRow", { header });
8
9
 
9
10
  <svelte:element this={header ? "header" : "section"} class="DataTableRow" class:header>
10
11
  {@render children()}
12
+ {#if actions}
13
+ <div class="Actions">
14
+ {@render actions()}
15
+ </div>
16
+ {/if}
11
17
  </svelte:element>
12
18
 
13
19
  <style>
@@ -17,7 +23,19 @@ setContext("DataTableRow", { header });
17
23
  grid-template-columns: subgrid;
18
24
  gap: 1.5em;
19
25
  align-items: center;
26
+ width: 100%;
27
+ overflow: none;
20
28
  padding: 0.75em calc(1.5em * var(--dtc));
29
+ position: relative;
30
+ }
31
+ .DataTableRow:hover {
32
+ background: color-mix(in srgb, var(--bg-subtle) calc(var(--dtc) * 100%), transparent);
33
+ }
34
+ .DataTableRow:has(.Actions) {
35
+ padding-right: 0;
36
+ }
37
+ .DataTableRow :global(*) {
38
+ white-space: nowrap;
21
39
  }
22
40
  .DataTableRow.header {
23
41
  background: color-mix(in srgb, var(--bg-subtle) calc(var(--dtc) * 100%), transparent);
@@ -25,4 +43,18 @@ setContext("DataTableRow", { header });
25
43
  section:not(:first-child) {
26
44
  border-top: var(--border);
27
45
  }
46
+ .Actions {
47
+ display: flex;
48
+ gap: 0.75em;
49
+ justify-self: end;
50
+ position: sticky;
51
+ top: 0;
52
+ right: 0;
53
+ padding-left: 0.75em;
54
+ padding-right: calc(1em * var(--dtc));
55
+ background: var(--bg-app);
56
+ }
57
+ .DataTableRow:hover .Actions {
58
+ background: color-mix(in srgb, var(--bg-subtle) calc(var(--dtc) * 100%), transparent);
59
+ }
28
60
  </style>
@@ -3,6 +3,9 @@ import { type Snippet } from "svelte";
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  header?: boolean | undefined;
6
+ actions?: ((this: void) => typeof import("svelte").SnippetReturn & {
7
+ _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
8
+ }) | undefined;
6
9
  children: Snippet;
7
10
  };
8
11
  events: {
@@ -0,0 +1,14 @@
1
+ <script>let {
2
+ code,
3
+ message
4
+ } = $props();
5
+ </script>
6
+
7
+ <p class="Error">{message ? message : code}</p>
8
+
9
+ <style>
10
+ .Error {
11
+ color: var(--text-warn);
12
+ font-size: max(0.85em, 11px);
13
+ }
14
+ </style>
@@ -0,0 +1,17 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ code: string;
5
+ message?: string | undefined;
6
+ };
7
+ events: {
8
+ [evt: string]: CustomEvent<any>;
9
+ };
10
+ slots: {};
11
+ };
12
+ export type FieldErrorProps = typeof __propDef.props;
13
+ export type FieldErrorEvents = typeof __propDef.events;
14
+ export type FieldErrorSlots = typeof __propDef.slots;
15
+ export default class FieldError extends SvelteComponent<FieldErrorProps, FieldErrorEvents, FieldErrorSlots> {
16
+ }
17
+ export {};
@@ -1,14 +1,85 @@
1
- <script>import { enhance as enhanceAction } from "$app/forms";
1
+ <script>import { enhance } from "$app/forms";
2
2
  import UiContent from "../layout/UIContent.svelte";
3
+ import { Bodyguard } from "@auth70/bodyguard";
3
4
  import { onMount, setContext } from "svelte";
5
+ import { get } from "svelte/store";
6
+ import { dezerialize } from "@auth70/zodex-esm";
7
+ import { array } from "zod";
8
+ import { arrayPathToStringPath, getIndividualValidators, parseFormIssues } from "./form.js";
4
9
  let {
10
+ form,
11
+ action,
12
+ enctype = "multipart/form-data",
13
+ method = "POST",
5
14
  fullWidth = false,
6
15
  children
7
16
  } = $props();
17
+ setContext("form", form);
18
+ setContext("form.validators", getIndividualValidators(form));
19
+ const schema = dezerialize(form.schema);
20
+ const bodyguard = new Bodyguard();
21
+ let formEl;
22
+ function setFormIssuesAndFields(issues, fields) {
23
+ form.issues = issues;
24
+ form.fields = fields;
25
+ }
26
+ async function validate() {
27
+ form.tainted = true;
28
+ const req = new Request("localhost", {
29
+ method: "POST",
30
+ body: new FormData(formEl)
31
+ });
32
+ const result = await bodyguard.softForm(req, schema.parse);
33
+ if (result.success === true) {
34
+ form.valid = true;
35
+ form.issues = [];
36
+ } else {
37
+ form.valid = false;
38
+ form.issues = parseFormIssues(result.error.issues);
39
+ }
40
+ }
41
+ onMount(() => {
42
+ validate();
43
+ });
8
44
  </script>
9
45
 
10
46
  <UiContent>
11
- <form>
47
+ <form
48
+ {method}
49
+ {action}
50
+ {enctype}
51
+ bind:this={formEl}
52
+ onchange={validate}
53
+ use:enhance={({ formElement, formData, action, cancel, submitter }) => {
54
+ // `formElement` is this `<form>` element
55
+ // `formData` is its `FormData` object that's about to be submitted
56
+ // `action` is the URL to which the form is posted
57
+ // calling `cancel()` will prevent the submission
58
+ // `submitter` is the `HTMLElement` that caused the form to be submitted
59
+
60
+ return async ({ result, update }) => {
61
+ // `result` is an `ActionResult` object
62
+ // `update` is a function which triggers the default logic that would be triggered if this callback wasn't set
63
+ console.log('result', result);
64
+ const resultForm = (result as any).data.form;
65
+ if(result.type === "success") {
66
+ if(resultForm) {
67
+ form.valid = resultForm.valid;
68
+ }
69
+ } else if(result.type === "error") {
70
+ if(resultForm) {
71
+ setFormIssuesAndFields(resultForm.issues, resultForm.fields);
72
+ }
73
+ } else if(result.type === "failure") {
74
+ if(resultForm) {
75
+ setFormIssuesAndFields(resultForm.issues, resultForm.fields);
76
+ }
77
+ } else if(result.type === "redirect") {
78
+ window.location.href = result.location;
79
+ }
80
+ };
81
+ }}
82
+ >
12
83
  {@render children()}
13
84
  </form>
14
85
  </UiContent>
@@ -1,7 +1,12 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  import type { Snippet } from "svelte";
3
+ import type { Form } from "./types.js";
3
4
  declare const __propDef: {
4
5
  props: {
6
+ form: Form<any>;
7
+ action?: string | undefined;
8
+ enctype?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
9
+ method?: "GET" | "POST" | undefined;
5
10
  fullWidth?: boolean | undefined;
6
11
  children: Snippet;
7
12
  };
@@ -1,4 +1,5 @@
1
- <script>import Label from "./Label.svelte";
1
+ <script>import { getContext, unstate } from "svelte";
2
+ import Label from "./Label.svelte";
2
3
  import { createId } from "../utils/id.js";
3
4
  import Copy from "../icons/Copy.svelte";
4
5
  import Done from "../icons/Done.svelte";
@@ -6,7 +7,10 @@ import Show from "../icons/Show.svelte";
6
7
  import Hide from "../icons/Hide.svelte";
7
8
  import Tooltip from "../display/Tooltip.svelte";
8
9
  import IconButton from "../display/IconButton.svelte";
9
- import Icon from "../display/Icon.svelte";
10
+ import { fieldChange, fieldKeydown, ignoreKeys } from "./client.svelte.js";
11
+ import FieldError from "./FieldError.svelte";
12
+ import { getFromObjWithStringPath } from "./form.js";
13
+ import { ZodType } from "zod";
10
14
  let {
11
15
  alt,
12
16
  autocapitalize,
@@ -37,6 +41,13 @@ let {
37
41
  pattern,
38
42
  placeholder,
39
43
  suffix,
44
+ onblur,
45
+ onchange,
46
+ onclick,
47
+ onfocus,
48
+ onkeydown,
49
+ onkeyup,
50
+ onkeypress,
40
51
  prefix,
41
52
  readonly,
42
53
  required,
@@ -46,8 +57,8 @@ let {
46
57
  step,
47
58
  tabindex,
48
59
  title,
49
- type = "text",
50
- value = $bindable(""),
60
+ type,
61
+ value = $bindable(),
51
62
  viewable,
52
63
  webkitdirectory
53
64
  } = $props();
@@ -57,6 +68,12 @@ let viewTitle = $state("Show");
57
68
  let copyTooltipOpen = $state(false);
58
69
  let copyBtnIcon = $state(Copy);
59
70
  let viewBtnIcon = $state(Show);
71
+ const form = getContext("form");
72
+ const field = $derived(form.fields[name]);
73
+ const issue = $derived(form?.issues?.find((issue2) => issue2.name === name));
74
+ const validator = getContext("form.validators")?.[name];
75
+ const data = form?.data;
76
+ const originalData = form?.originalData;
60
77
  function view(e) {
61
78
  e.preventDefault();
62
79
  if (!el)
@@ -111,28 +128,37 @@ function copy(e) {
111
128
  {indeterminate}
112
129
  {inputmode}
113
130
  {list}
114
- {maxlength}
115
- {minlength}
116
- {max}
117
- {min}
131
+ maxlength={maxlength ? maxlength : field?.maxlength}
132
+ minlength={minlength ? minlength : field?.minlength}
133
+ max={max ? max : field?.max}
134
+ min={min ? min : field?.min}
118
135
  {multiple}
119
136
  {name}
120
- {pattern}
137
+ {onblur}
138
+ {onclick}
139
+ onchange={fieldChange(form, name, () => el, validator)}
140
+ {onfocus}
141
+ onkeydown={fieldKeydown(form, name, () => el, validator)}
142
+ {onkeyup}
143
+ {onkeypress}
144
+ pattern={pattern ? pattern : field?.pattern}
121
145
  {placeholder}
122
146
  {readonly}
123
- {required}
147
+ required={required || field?.required}
124
148
  {results}
125
149
  {src}
126
150
  {step}
127
151
  {tabindex}
128
152
  {title}
129
153
  {type}
130
- {value}
154
+ value={value || getFromObjWithStringPath(Object.assign(originalData, data), name) || form?.fields?.[name]?.defaultValue || ''}
131
155
  {webkitdirectory}
132
156
  />
133
157
  {/snippet}
134
-
135
- <div class="FieldContainer {type}" class:checkOrRadio={type === "checkbox" || type === "radio"}>
158
+ <div
159
+ class="FieldContainer {type}"
160
+ class:checkOrRadio={type === "checkbox" || type === "radio"}
161
+ >
136
162
 
137
163
  <Label {id} {label} tip={labelTip} />
138
164
 
@@ -144,6 +170,7 @@ function copy(e) {
144
170
  class="Field {type}"
145
171
  class:hasPrefix={!!prefix}
146
172
  class:hasSuffix={!!suffix}
173
+ class:invalid={field?.tainted && issue?.code}
147
174
  >
148
175
  {#if prefix}
149
176
  <div class="Fix Prefix">
@@ -179,6 +206,9 @@ function copy(e) {
179
206
  </div>
180
207
  {/if}
181
208
  </div>
209
+ {#if field?.tainted && issue?.code}
210
+ <FieldError code={issue.code} message={issue.message} />
211
+ {/if}
182
212
  {/if}
183
213
  </div>
184
214
 
@@ -191,11 +221,15 @@ function copy(e) {
191
221
  }
192
222
  .Field {
193
223
  background-color: var(--field-bg);
194
- border: var(--border-size) var(--border-style) var(--border-color);
224
+ border: var(--field-border-size) var(--field-border-style) var(--field-border-color);
195
225
  border-radius: var(--field-radius);
196
226
  display: flex;
197
227
  padding-inline: 0.15em;
198
228
  }
229
+ .Field:has(input:user-invalid),
230
+ .Field.invalid {
231
+ border-color: var(--field-border-color-error);
232
+ }
199
233
  .Field > *:not(input) {
200
234
  flex-grow: 0;
201
235
  flex-shrink: 0;
@@ -217,6 +251,10 @@ function copy(e) {
217
251
  .Field:has(input:focus-visible) {
218
252
  outline: var(--focus-outline);
219
253
  }
254
+ .Field:has(input:focus-visible:user-invalid),
255
+ .Field.invalid:has(input:focus-visible) {
256
+ outline-color: var(--focus-color-error);
257
+ }
220
258
  /**
221
259
  * Input element
222
260
  */
@@ -25,7 +25,7 @@ declare const __propDef: {
25
25
  /** Whether the input should be disabled. */
26
26
  disabled?: boolean | undefined;
27
27
  /** A hint to the browser for which enter key to display for the input. */
28
- enterkeyhint?: "search" | "enter" | "done" | "go" | "next" | "previous" | "send" | undefined;
28
+ enterkeyhint?: "search" | "done" | "enter" | "go" | "next" | "previous" | "send" | undefined;
29
29
  /** The height of the input element. Valid for image inputs. */
30
30
  height?: number | undefined;
31
31
  /** Help text to display below the input. */
@@ -60,7 +60,21 @@ declare const __propDef: {
60
60
  /** Whether the input should allow multiple values. Valid for email and file inputs. */
61
61
  multiple?: boolean | undefined;
62
62
  /** The name of the input element. */
63
- name?: string | undefined;
63
+ name: string;
64
+ /** The onblur event handler */
65
+ onblur?: ((e: FocusEvent) => void) | undefined;
66
+ /** Onchange event handler */
67
+ onchange?: ((e: Event) => void) | undefined;
68
+ /** Onclick event handler */
69
+ onclick?: ((e: MouseEvent) => void) | undefined;
70
+ /** Onfocus event handler */
71
+ onfocus?: ((e: FocusEvent) => void) | undefined;
72
+ /** Keyup event handler */
73
+ onkeyup?: ((e: KeyboardEvent) => void) | undefined;
74
+ /** Keydown event handler */
75
+ onkeydown?: ((e: KeyboardEvent) => void) | undefined;
76
+ /** Keypress event handler */
77
+ onkeypress?: ((e: KeyboardEvent) => void) | undefined;
64
78
  /** A regular expression that the input's value is checked against. Valid for text, search, url, tel, email, and password. */
65
79
  pattern?: string | undefined;
66
80
  /** Placeholder text to display when the input is empty. */
@@ -92,7 +106,7 @@ declare const __propDef: {
92
106
  /** A string that defines the title of the input element. */
93
107
  title?: string | undefined;
94
108
  /** The type of input to display. */
95
- type?: "number" | "color" | "button" | "search" | "time" | "image" | "text" | "submit" | "email" | "tel" | "url" | "checkbox" | "radio" | "hidden" | "password" | "file" | "range" | "date" | "datetime-local" | "month" | "week" | undefined;
109
+ type?: "number" | "color" | "button" | "search" | "time" | "image" | "text" | "submit" | "email" | "tel" | "url" | "date" | "checkbox" | "radio" | "hidden" | "password" | "file" | "range" | "datetime-local" | "month" | "week" | undefined;
96
110
  /** The value of the input element. */
97
111
  value?: string | undefined;
98
112
  /** Whether the input should be viewable. Valid for password inputs. */
@@ -0,0 +1,45 @@
1
+ import type { ZodType } from "zod";
2
+ import type { Form } from "./types.js";
3
+ /**
4
+ * Use a form in a Svelte component.
5
+ * @param {Form} form - The form to use.
6
+ * @returns
7
+ */
8
+ export declare function useForm(form: Form<any>): {
9
+ form: {
10
+ originalData: any;
11
+ data: any;
12
+ } & Form<any>;
13
+ };
14
+ /**
15
+ * Validate a single field.
16
+ * @param {Form} form - The form to validate.
17
+ * @param {string} name - The name of the field to validate.
18
+ * @param {HTMLInputElement | HTMLSelectElement} el - The element to validate.
19
+ * @param {ZodType} validator - The validator to use.
20
+ */
21
+ export declare function fieldValidate(form: Form<any>, name: string, el?: HTMLInputElement | HTMLSelectElement, validator?: ZodType<any, any>): void;
22
+ /**
23
+ * Keydown event handler for a field.
24
+ * @param {KeyboardEvent} e - The event.
25
+ * @param {Form} form - The form to use.
26
+ * @param {string} name - The name of the field.
27
+ * @param {HTMLInputElement | HTMLSelectElement} el - The element to validate.
28
+ * @param {ZodType} validator - The validator to use.
29
+ * @param {(e: KeyboardEvent) => void} onkeydown - The onkeydown event handler.
30
+ */
31
+ export declare function fieldKeydown(form: Form<any>, name: string, el: () => HTMLInputElement | HTMLSelectElement | undefined, validator?: ZodType<any, any>, onkeydown?: (e: KeyboardEvent) => void): (e: KeyboardEvent) => Promise<void>;
32
+ /**
33
+ * Change event handler for a field.
34
+ * @param {Event} e - The event.
35
+ * @param {Form} form - The form to use.
36
+ * @param {string} name - The name of the field.
37
+ * @param {HTMLInputElement | HTMLSelectElement} el - The element to validate.
38
+ * @param {ZodType} validator - The validator to use.
39
+ * @param {(e: Event) => void} onchange - The onchange event handler.
40
+ */
41
+ export declare function fieldChange(form: Form<any>, name: string, el: () => HTMLInputElement | HTMLSelectElement | undefined, validator?: ZodType<any, any>, onchange?: (e: Event) => void): (e: Event) => Promise<void>;
42
+ /**
43
+ * Keys that should be ignored when marking a field as tainted.
44
+ */
45
+ export declare const ignoreKeys: string[];
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Use a form in a Svelte component.
3
+ * @param {Form} form - The form to use.
4
+ * @returns
5
+ */
6
+ export function useForm(form) {
7
+ let _form = Object.assign({
8
+ originalData: JSON.parse(JSON.stringify(form.data ?? {})),
9
+ data: form.data ?? {},
10
+ }, form);
11
+ // Have to declare a variable separately to use in the return object
12
+ // as the compiler is looking for a declaration.
13
+ let __form = $state(_form);
14
+ return {
15
+ form: __form,
16
+ };
17
+ }
18
+ /**
19
+ * Validate a single field.
20
+ * @param {Form} form - The form to validate.
21
+ * @param {string} name - The name of the field to validate.
22
+ * @param {HTMLInputElement | HTMLSelectElement} el - The element to validate.
23
+ * @param {ZodType} validator - The validator to use.
24
+ */
25
+ export function fieldValidate(form, name, el, validator) {
26
+ if (!validator)
27
+ return;
28
+ const result = validator.safeParse(el?.value);
29
+ if (result.success) {
30
+ form.issues = (form.issues || []).filter((issue) => issue.name !== name);
31
+ form.valid = form.issues.length === 0;
32
+ }
33
+ else {
34
+ form.valid = false;
35
+ form.issues = (form.issues || []).filter((issue) => issue.name !== name);
36
+ form.issues?.push({
37
+ name,
38
+ ...result.error.issues[0],
39
+ });
40
+ }
41
+ }
42
+ /**
43
+ * Keydown event handler for a field.
44
+ * @param {KeyboardEvent} e - The event.
45
+ * @param {Form} form - The form to use.
46
+ * @param {string} name - The name of the field.
47
+ * @param {HTMLInputElement | HTMLSelectElement} el - The element to validate.
48
+ * @param {ZodType} validator - The validator to use.
49
+ * @param {(e: KeyboardEvent) => void} onkeydown - The onkeydown event handler.
50
+ */
51
+ export function fieldKeydown(form, name, el, validator, onkeydown) {
52
+ return async function (e) {
53
+ setTimeout(() => {
54
+ const possibleKey = e?.key;
55
+ if (ignoreKeys.includes(possibleKey))
56
+ return;
57
+ form.data[name] = el()?.value || '';
58
+ fieldValidate(form, name, el(), validator);
59
+ }, 0); // Wait for the key to be updated in the input.
60
+ if (onkeydown)
61
+ return onkeydown(e);
62
+ };
63
+ }
64
+ /**
65
+ * Change event handler for a field.
66
+ * @param {Event} e - The event.
67
+ * @param {Form} form - The form to use.
68
+ * @param {string} name - The name of the field.
69
+ * @param {HTMLInputElement | HTMLSelectElement} el - The element to validate.
70
+ * @param {ZodType} validator - The validator to use.
71
+ * @param {(e: Event) => void} onchange - The onchange event handler.
72
+ */
73
+ export function fieldChange(form, name, el, validator, onchange) {
74
+ return async function (e) {
75
+ form.data[name] = el()?.value || '';
76
+ form.fields[name].tainted = true;
77
+ fieldValidate(form, name, el(), validator);
78
+ if (onchange)
79
+ return onchange(e);
80
+ };
81
+ }
82
+ /**
83
+ * Keys that should be ignored when marking a field as tainted.
84
+ */
85
+ export const ignoreKeys = ['Tab', 'Shift', 'Control', 'Alt', 'Meta', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Escape', 'CapsLock', 'NumLock', 'ScrollLock', 'Pause', 'ContextMenu', 'PrintScreen', 'Help', 'Clear', 'OS', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F12'];
@@ -1,34 +1,52 @@
1
- /// <reference types="svelte" />
2
- import { type Writable } from 'svelte/store';
3
- export declare const FormItems: Writable<Record<string, FormElement | FormElement[]>>;
4
- export type FormElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
5
- export type SuccessType = {
6
- success: true;
7
- value: any;
8
- };
9
- export type ErrorType = {
10
- success: false;
11
- error: string;
12
- };
13
- export type FormRegistry = {
14
- items: Record<string, FormElement | FormElement[]>;
15
- getValues(): Record<string, string>;
16
- register(name: string, el: FormElement | FormElement[]): void;
17
- updateFormValues(): void;
18
- validate(): boolean;
19
- validateField(nameOrElement: string): SuccessType | ErrorType;
20
- validateField(nameOrElement: FormElement): SuccessType | ErrorType;
21
- validateField(nameOrElement: FormElement[]): SuccessType | ErrorType;
22
- validateSingleField(nameOrElement: string | FormElement): SuccessType | ErrorType;
23
- };
24
- export type BeforeSubmitFn = (opts: {
25
- form: HTMLFormElement;
26
- data: FormData;
27
- cancel: () => void;
28
- }) => void | Promise<void>;
29
- export type AddBeforeSubmitFn = (id: string, fn: BeforeSubmitFn) => void;
30
- export declare const getPropertyPaths: (schema: ZodType) => string[];
1
+ import { Bodyguard, type BodyguardFormConfig, type BodyguardResult, type JSONLike } from "@auth70/bodyguard";
2
+ import type { RequestEvent } from "@sveltejs/kit";
3
+ import type { ZodType, infer as Infer, ZodIssue, ZodIssueBase } from "zod";
4
+ import type { Form, FormField, FormIssue, ZodTypes } from "./types.js";
5
+ export declare const bodyguard: Bodyguard;
31
6
  /**
32
- * Keys that should be ignored when marking a field as changed.
7
+ * Get fields from a Zod schema.
8
+ * @param {ZodObject<any>} schema - The schema to get fields from.
9
+ * @returns {Record<string, any>} - The fields from the schema.
33
10
  */
34
- export declare const ignoreChangeKeys: string[];
11
+ export declare function getFieldsFromSchema(schema: ZodType, data?: BodyguardResult<any>, parents?: string[]): Record<string, FormField>;
12
+ /**
13
+ * Server page load call. Optionally with a default object to populate the form with from locals.
14
+ * @param {ZodType} schema - The schema to parse the form with.
15
+ * @param {RequestEvent} event - The event to load the (possible) form from.
16
+ * @param {JSONLike} obj - The default object to populate the form with.
17
+ * @returns {Promise<Form<Z>>} - The form to use.
18
+ */
19
+ export declare function loadForm<Z extends ZodTypes>(schema: Z, event: RequestEvent, obj?: JSONLike): Promise<Form<Z>>;
20
+ /**
21
+ * Parse zod issues into form issues (with a path string).
22
+ * @param {ZodIssue[]} issues - The issues to parse.
23
+ * @returns {FormIssue[]} - The parsed issues.
24
+ */
25
+ export declare function parseFormIssues(issues: (ZodIssue & ZodIssueBase)[]): FormIssue[];
26
+ /**
27
+ * Parse a form using a schema.
28
+ * @param {ZodType} schema - The schema to parse the form with.
29
+ * @param {Request} request - The request to parse.
30
+ * @param {BodyguardFormConfig} opts - The options for the form validation.
31
+ * @returns {Promise<{ valid: false } | { valid: true, data: Infer<Z> }>} - The result of the form validation.
32
+ */
33
+ export declare function parseForm<Z extends ZodType>(schema: Z, event: RequestEvent, opts?: BodyguardFormConfig): Promise<Form<Z>>;
34
+ /**
35
+ * Convert an array path from a Zod issue to a string path for a form field.
36
+ * @param {(string | number)[]} path - The path to convert.
37
+ * @returns {string} - The string path.
38
+ */
39
+ export declare function arrayPathToStringPath(path: (string | number)[]): string;
40
+ /**
41
+ * Get a value from an object using a string path.
42
+ * @param {any} obj - The object to get the value from.
43
+ * @param {string} path - The path to get the value from.
44
+ * @returns {string | number | Date | boolean | object | undefined} - The value from the object.
45
+ */
46
+ export declare function getFromObjWithStringPath(obj: any, path: string): string | number | Date | boolean | object | undefined;
47
+ /**
48
+ * Get individual validators for each field in a form.
49
+ * @param {Form} form - The form to get the validators from.
50
+ * @returns {Record<keyof Infer<T>, (value: any) => boolean>} - The validators for each field.
51
+ */
52
+ export declare function getIndividualValidators<T extends ZodTypes>(form: Form<T>): Record<keyof Infer<T>, (value: any) => boolean>;
package/dist/form/form.js CHANGED
@@ -1,31 +1,322 @@
1
- import { writable } from 'svelte/store';
2
- import { ZodArray, ZodNullable, ZodObject, ZodOptional, union, boolean, literal } from 'zod';
3
- export const FormItems = writable({});
4
- export const getPropertyPaths = (schema) => {
5
- // check if schema is nullable or optional
6
- if (schema instanceof ZodNullable ||
7
- schema instanceof ZodOptional) {
8
- return getPropertyPaths(schema.unwrap());
9
- }
10
- // check if schema is an array
1
+ import { Bodyguard } from "@auth70/bodyguard";
2
+ import { ZodArray, ZodNullable, ZodOptional, ZodEffects, ZodObject, ZodDefault, ZodString, ZodNumber, ZodBoolean, ZodDate, array } from "zod";
3
+ import { zerialize, dezerialize } from "@auth70/zodex-esm";
4
+ export const bodyguard = new Bodyguard();
5
+ /**
6
+ * Map a Zod type to a field type as a string.
7
+ * @param {Zod.ZodType} schema - The Zod schema to map to a field type.
8
+ * @returns {string} - The field type as a string.
9
+ */
10
+ function mapZodTypeToFieldType(schema) {
11
+ if (schema instanceof ZodNullable) {
12
+ return mapZodTypeToFieldType(schema.unwrap());
13
+ }
14
+ if (schema instanceof ZodOptional) {
15
+ return mapZodTypeToFieldType(schema.unwrap());
16
+ }
17
+ if (schema instanceof ZodEffects) {
18
+ return mapZodTypeToFieldType(schema._def?.schema ?? schema);
19
+ }
11
20
  if (schema instanceof ZodArray) {
12
- return getPropertyPaths(schema.element);
21
+ return `Array<${mapZodTypeToFieldType(schema.element)}>`;
13
22
  }
14
- // check if schema is an object
15
23
  if (schema instanceof ZodObject) {
16
- // loop through key/value pairs
17
- const entries = Object.entries(schema.shape);
18
- return entries.flatMap(([key, value]) => {
19
- // get nested keys
20
- const nested = getPropertyPaths(value).map((subKey) => `${key}.${subKey}`);
21
- // return nested keys
22
- return nested.length ? nested : key;
23
- });
24
- }
25
- // return empty array
26
- return [];
27
- };
28
- /**
29
- * Keys that should be ignored when marking a field as changed.
30
- */
31
- export const ignoreChangeKeys = ['Tab', 'Shift', 'Control', 'Alt', 'Meta', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Escape', 'CapsLock', 'NumLock', 'ScrollLock', 'Pause', 'ContextMenu', 'PrintScreen', 'Help', 'Clear', 'OS', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F12'];
24
+ return 'object';
25
+ }
26
+ if (schema instanceof ZodDefault) {
27
+ return mapZodTypeToFieldType(schema._def.innerType);
28
+ }
29
+ if (schema instanceof ZodDate) {
30
+ return 'date';
31
+ }
32
+ if (schema instanceof ZodString) {
33
+ return 'string';
34
+ }
35
+ if (schema instanceof ZodNumber) {
36
+ return 'number';
37
+ }
38
+ if (schema instanceof ZodBoolean) {
39
+ return 'boolean';
40
+ }
41
+ return schema.constructor.name;
42
+ }
43
+ /**
44
+ * Get fields from a Zod schema.
45
+ * @param {ZodObject<any>} schema - The schema to get fields from.
46
+ * @returns {Record<string, any>} - The fields from the schema.
47
+ */
48
+ export function getFieldsFromSchema(schema, data, parents) {
49
+ if (!parents)
50
+ parents = [];
51
+ else
52
+ parents = [...parents];
53
+ const fields = {};
54
+ if (schema instanceof ZodEffects) {
55
+ return getFieldsFromSchema(schema._def.schema, data);
56
+ }
57
+ if (!(schema instanceof ZodObject)) {
58
+ console.error('[lutra] getFieldsFromSchema: Schema is not an object:', schema.constructor.name);
59
+ return fields;
60
+ }
61
+ for (const key in schema.shape) {
62
+ const field = schema.shape[key];
63
+ // Fields can be wrapped like ZodDefault._def.innerType._def.InnerType, etc. We want the last inner type's definition.
64
+ // On the way, let's check if any of the _def include a defaultValue.
65
+ let required = true;
66
+ let defaultValue = undefined;
67
+ let min = undefined;
68
+ let max = undefined;
69
+ let pattern = undefined;
70
+ let minlength = undefined;
71
+ let maxlength = undefined;
72
+ let type = field;
73
+ while (type instanceof ZodEffects || type instanceof ZodOptional || type instanceof ZodDefault || type instanceof ZodNullable) {
74
+ if (type instanceof ZodOptional || type instanceof ZodNullable) {
75
+ required = false;
76
+ type = type._def.innerType;
77
+ }
78
+ if (type instanceof ZodDefault) {
79
+ defaultValue = type._def.defaultValue();
80
+ type = type._def.innerType;
81
+ }
82
+ else {
83
+ type = type._def.innerType;
84
+ }
85
+ }
86
+ // Check for min/max/regex checks
87
+ if (type._def.checks) {
88
+ for (const check of type._def.checks) {
89
+ if (check.kind === 'min') {
90
+ if (field instanceof ZodString || field._def?.innerType instanceof ZodString) {
91
+ minlength = check.value;
92
+ }
93
+ else {
94
+ min = check.value;
95
+ }
96
+ }
97
+ if (check.kind === 'max') {
98
+ if (field instanceof ZodString || field._def?.innerType instanceof ZodString) {
99
+ maxlength = check.value;
100
+ }
101
+ else {
102
+ max = check.value;
103
+ }
104
+ }
105
+ if (check.kind === 'regex') {
106
+ pattern = check.regex.source;
107
+ }
108
+ if (check.kind === 'email') {
109
+ // ...
110
+ }
111
+ }
112
+ }
113
+ if (type instanceof ZodObject) {
114
+ const children = getFieldsFromSchema(type, data, [...parents, key]);
115
+ Object.entries(children).forEach(([childKey, childField]) => {
116
+ const name = [...parents, key, childKey].join('.');
117
+ fields[name] = {
118
+ name,
119
+ tainted: childField.tainted,
120
+ coerce: childField.coerce,
121
+ min: childField.min,
122
+ max: childField.max,
123
+ minlength: childField.minlength,
124
+ maxlength: childField.maxlength,
125
+ pattern: childField.pattern,
126
+ required: childField.required,
127
+ defaultValue: childField.defaultValue,
128
+ isArray: childField.isArray,
129
+ type: childField.type,
130
+ };
131
+ });
132
+ }
133
+ else {
134
+ fields[key] = {
135
+ name: [...parents, key].join('.'),
136
+ tainted: false,
137
+ min,
138
+ max,
139
+ minlength,
140
+ maxlength,
141
+ pattern,
142
+ required,
143
+ defaultValue,
144
+ isArray: field instanceof ZodArray,
145
+ type: mapZodTypeToFieldType(field instanceof ZodArray ? field.element : field),
146
+ };
147
+ }
148
+ }
149
+ return fields;
150
+ }
151
+ /**
152
+ * Server page load call. Optionally with a default object to populate the form with from locals.
153
+ * @param {ZodType} schema - The schema to parse the form with.
154
+ * @param {RequestEvent} event - The event to load the (possible) form from.
155
+ * @param {JSONLike} obj - The default object to populate the form with.
156
+ * @returns {Promise<Form<Z>>} - The form to use.
157
+ */
158
+ export async function loadForm(schema, event, obj) {
159
+ // If there is a form in locals, it's been passed in from parseForm. Return it.
160
+ if (event.locals.form) {
161
+ return event.locals.form;
162
+ }
163
+ // Serialize the schema to send over the wire.
164
+ const serializedSchema = zerialize(schema);
165
+ // Get the fields from the schema.
166
+ const fields = getFieldsFromSchema(schema);
167
+ // Create the form object.
168
+ const form = event.locals.form;
169
+ return {
170
+ valid: form ? form.valid : (obj ? true : false),
171
+ tainted: form ? form.tainted : false,
172
+ schema: serializedSchema,
173
+ posted: form ? form.posted : false,
174
+ fields,
175
+ data: Object.assign(Object.fromEntries(Object.entries(fields).map(([key, value]) => {
176
+ // If the key is in the passed object, use that value. Otherwise, try to use the default value.
177
+ return [key, value.defaultValue];
178
+ })), obj ? obj : {}),
179
+ };
180
+ }
181
+ /**
182
+ * Parse zod issues into form issues (with a path string).
183
+ * @param {ZodIssue[]} issues - The issues to parse.
184
+ * @returns {FormIssue[]} - The parsed issues.
185
+ */
186
+ export function parseFormIssues(issues) {
187
+ return JSON.parse(JSON.stringify(issues.map((issue) => {
188
+ return {
189
+ ...issue,
190
+ name: arrayPathToStringPath(issue.path),
191
+ };
192
+ })));
193
+ }
194
+ /**
195
+ * Parse a form using a schema.
196
+ * @param {ZodType} schema - The schema to parse the form with.
197
+ * @param {Request} request - The request to parse.
198
+ * @param {BodyguardFormConfig} opts - The options for the form validation.
199
+ * @returns {Promise<{ valid: false } | { valid: true, data: Infer<Z> }>} - The result of the form validation.
200
+ */
201
+ export async function parseForm(schema, event, opts) {
202
+ // Use bodyguard to parse the form.
203
+ const data = await bodyguard.softForm(event.request, schema.parse, opts);
204
+ // Get the fields from the schema.
205
+ const fields = getFieldsFromSchema(schema, data);
206
+ // Serialize the schema to send over the wire.
207
+ const serializedSchema = zerialize(schema);
208
+ // Create the form object.
209
+ const form = {
210
+ valid: data.success,
211
+ tainted: !data.success,
212
+ schema: serializedSchema,
213
+ posted: true,
214
+ fields: Object.fromEntries(Object.entries(fields).map(([key, field]) => {
215
+ // Mark fields as tainted if they were in the request.
216
+ return [key, {
217
+ ...field,
218
+ tainted: data.success ? false : data.value.hasOwnProperty(key),
219
+ }];
220
+ })),
221
+ data: data.value ? data.value : {},
222
+ issues: data.success ? undefined : parseFormIssues(data.error.issues),
223
+ };
224
+ // Attach the form to the locals to pass it to the client via loadForm.
225
+ event.locals.form = form;
226
+ return form;
227
+ }
228
+ /**
229
+ * Convert an array path from a Zod issue to a string path for a form field.
230
+ * @param {(string | number)[]} path - The path to convert.
231
+ * @returns {string} - The string path.
232
+ */
233
+ export function arrayPathToStringPath(path) {
234
+ // Joins the path with dots. For arrays, it adds brackets.
235
+ return path.map((p) => {
236
+ if (p.toString().match(/^\d+$/)) {
237
+ return `${p}[]`;
238
+ }
239
+ return p;
240
+ }).join('.');
241
+ }
242
+ /**
243
+ * Get a value from an object using a string path.
244
+ * @param {any} obj - The object to get the value from.
245
+ * @param {string} path - The path to get the value from.
246
+ * @returns {string | number | Date | boolean | object | undefined} - The value from the object.
247
+ */
248
+ export function getFromObjWithStringPath(obj, path) {
249
+ const parts = path.split('.');
250
+ let current = obj;
251
+ for (let i = 0; i < parts.length; i++) {
252
+ const part = parts[i];
253
+ // Support for array access patterns: foo[] or foo[0]
254
+ const arrayMatch = part.match(/^(.+?)(\[\d*\])?$/);
255
+ if (arrayMatch) {
256
+ const [, key, indexPart] = arrayMatch;
257
+ // Navigate into 'properties' if it exists
258
+ if (current.properties && current.properties[key]) {
259
+ current = current.properties[key];
260
+ }
261
+ else if (current[key]) {
262
+ current = current[key];
263
+ }
264
+ else {
265
+ // The path is invalid for the given structure or the index is not a number
266
+ return undefined;
267
+ }
268
+ // If indexPart is defined, it means we're dealing with an array index
269
+ if (indexPart) {
270
+ // Dealing with an array index, e.g., [0]
271
+ const index = parseInt(indexPart.slice(1, -1), 10);
272
+ if (!Array.isArray(current) || isNaN(index)) {
273
+ return undefined;
274
+ }
275
+ current = current[index];
276
+ }
277
+ }
278
+ else {
279
+ // Direct property access
280
+ if (current.properties && current.properties[part]) {
281
+ current = current.properties[part];
282
+ }
283
+ else if (current[part]) {
284
+ current = current[part];
285
+ }
286
+ else {
287
+ return undefined;
288
+ }
289
+ }
290
+ if (current === undefined) {
291
+ return undefined;
292
+ }
293
+ }
294
+ // The value must be directly returnable without checking type,
295
+ // as we might be looking for an object structure.
296
+ return current;
297
+ }
298
+ /**
299
+ * Get individual validators for each field in a form.
300
+ * @param {Form} form - The form to get the validators from.
301
+ * @returns {Record<keyof Infer<T>, (value: any) => boolean>} - The validators for each field.
302
+ */
303
+ export function getIndividualValidators(form) {
304
+ const schema = form.schema;
305
+ const fields = form.fields;
306
+ if (!schema || !fields) {
307
+ console.error('[lutra] getIndividualValidators: Schema or fields not found:', schema, fields);
308
+ return {};
309
+ }
310
+ // Match the dot-notation form fields with the object schema fields.
311
+ // For each field, create a validator function by dezerilizing the schema for that field from the schema.
312
+ // Return the validators, not the parsed data.
313
+ const validators = Object.fromEntries(Object.entries(fields).map(([key, field]) => {
314
+ const schemaField = getFromObjWithStringPath(schema, key);
315
+ if (!schemaField) {
316
+ console.error('[lutra] getIndividualValidators: Schema field not found:', key);
317
+ return [key, (value) => true];
318
+ }
319
+ return [key, dezerialize(schemaField)];
320
+ }));
321
+ return validators;
322
+ }
@@ -2,6 +2,7 @@ export { default as Button } from './Button.svelte';
2
2
  export { default as FieldContainer } from './FieldContainer.svelte';
3
3
  export { default as FieldSection } from './FieldSection.svelte';
4
4
  export { default as FieldActions } from './FieldActions.svelte';
5
+ export { default as FieldError } from './FieldError.svelte';
5
6
  export { default as FieldContent } from './FieldContent.svelte';
6
7
  export { default as Fieldset } from './Fieldset.svelte';
7
8
  export { default as Form } from './Form.svelte';
@@ -9,3 +10,6 @@ export { default as Input } from './Input.svelte';
9
10
  export { default as InputLength } from './InputLength.svelte';
10
11
  export { default as Label } from './Label.svelte';
11
12
  export { default as Select } from './Select.svelte';
13
+ export * from './types.js';
14
+ export * from './form.js';
15
+ export * from './client.svelte.js';
@@ -2,6 +2,7 @@ export { default as Button } from './Button.svelte';
2
2
  export { default as FieldContainer } from './FieldContainer.svelte';
3
3
  export { default as FieldSection } from './FieldSection.svelte';
4
4
  export { default as FieldActions } from './FieldActions.svelte';
5
+ export { default as FieldError } from './FieldError.svelte';
5
6
  export { default as FieldContent } from './FieldContent.svelte';
6
7
  export { default as Fieldset } from './Fieldset.svelte';
7
8
  export { default as Form } from './Form.svelte';
@@ -9,3 +10,6 @@ export { default as Input } from './Input.svelte';
9
10
  export { default as InputLength } from './InputLength.svelte';
10
11
  export { default as Label } from './Label.svelte';
11
12
  export { default as Select } from './Select.svelte';
13
+ export * from './types.js';
14
+ export * from './form.js';
15
+ export * from './client.svelte.js';
@@ -0,0 +1,21 @@
1
+ import { Bodyguard } from "@auth70/bodyguard";
2
+ import type { ActionFailure, RequestEvent } from "@sveltejs/kit";
3
+ import type { z, ZodType } from "zod";
4
+ export declare const bodyguard: Bodyguard;
5
+ export type Issue = {
6
+ message: string;
7
+ path: string[];
8
+ field: string;
9
+ };
10
+ export declare class FormError extends Error {
11
+ issues: Issue[];
12
+ constructor(issues: Issue[], message?: string);
13
+ }
14
+ export declare function formHandler<T, Z extends ZodType<any, any>>(event: RequestEvent, schema: Z, onSuccess: (data: z.infer<Z>) => Promise<T>, { castNumbers, castBooleans, }?: {
15
+ castNumbers?: boolean;
16
+ castBooleans?: boolean;
17
+ }): Promise<T | ActionFailure<{
18
+ issues: any;
19
+ }> | ActionFailure<{
20
+ message: any;
21
+ }>>;
@@ -0,0 +1,48 @@
1
+ import { Bodyguard } from "@auth70/bodyguard";
2
+ import { fail } from "@sveltejs/kit";
3
+ export const bodyguard = new Bodyguard();
4
+ export class FormError extends Error {
5
+ issues;
6
+ constructor(issues, message = 'Form error') {
7
+ super(message);
8
+ this.issues = issues;
9
+ }
10
+ }
11
+ export async function formHandler(event, schema, onSuccess, { castNumbers, castBooleans, } = {
12
+ castNumbers: false,
13
+ castBooleans: false,
14
+ }) {
15
+ const data = await bodyguard.softForm(event.request, schema.parse, {
16
+ castNumbers: castNumbers || false,
17
+ castBooleans: castBooleans || false,
18
+ });
19
+ if (!data.success) {
20
+ try {
21
+ const issues = JSON.parse(data.error);
22
+ console.error('[lutra] Bodyguard failed:', data);
23
+ return fail(400, {
24
+ issues,
25
+ });
26
+ }
27
+ catch (err) {
28
+ console.error('[lutra] Bodyguard failed:', data);
29
+ return fail(400, {
30
+ issues: data.error
31
+ });
32
+ }
33
+ }
34
+ try {
35
+ return Promise.resolve(onSuccess(data.value));
36
+ }
37
+ catch (err) {
38
+ console.error('[lutra] Error in form handler:', err);
39
+ if (err?.issues) {
40
+ return fail(400, {
41
+ issues: err?.issues ?? [],
42
+ });
43
+ }
44
+ return fail(500, {
45
+ message: err.message
46
+ });
47
+ }
48
+ }
@@ -1,4 +1,57 @@
1
+ import type { ZodType, infer as Infer, ZodTuple, ZodBigInt, ZodNaN, ZodSet, ZodVoid, ZodUndefined, ZodNull, ZodAny, ZodUnknown, ZodNever, ZodRecord, ZodMap, ZodDiscriminatedUnion, ZodUnion, ZodIntersection, ZodNativeEnum, ZodEnum, ZodFunction, ZodLazy, ZodLiteral, ZodCatch, ZodPipeline, ZodBranded, ZodPromise, ZodArray, ZodNullable, ZodOptional, ZodEffects, ZodObject, ZodDefault, ZodString, ZodNumber, ZodBoolean, ZodDate } from "zod";
1
2
  export type Autocomplete = 'on' | 'off' | AutocompleteSingleToken | AutocompleteBilling | AutocompleteShipping;
2
3
  export type AutocompleteBilling = 'billing' | `billing ${AutocompleteSingleToken}`;
3
4
  export type AutocompleteShipping = 'shipping' | `shipping ${AutocompleteSingleToken}`;
4
5
  export type AutocompleteSingleToken = `section-${string}` | "name" | "honorific-prefix" | "given-name" | "additional-name" | "family-name" | "honorific-suffix" | "nickname" | "email" | "username" | "new-password" | "current-password" | "one-time-code" | "organization-title" | "organization" | "street-address" | "address-line1" | "address-line2" | "address-line3" | "address-level4" | "address-level3" | "address-level2" | "address-level1" | "country" | "country-name" | "postal-code" | "cc-name" | "cc-given-name" | "cc-additional-name" | "cc-family-name" | "cc-number" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-csc" | "cc-type" | "transaction-currency" | "transaction-amount" | "language" | "bday" | "bday-day" | "bday-month" | "bday-year" | "sex" | "tel" | "tel-country-code" | "tel-national" | "tel-area-code" | "tel-local" | "tel-extension" | "impp" | "url" | "photo" | "webauthn";
6
+ export type Modifiers = ZodOptional<ZodTypes> | ZodNullable<ZodTypes> | ZodDefault<ZodTypes>;
7
+ export type Primitives = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid;
8
+ export type ListCollections = ZodTuple<any, any> | ZodSet<ZodTypes> | ZodArray<ZodTypes>;
9
+ export type KVCollections = ZodObject<any> | ZodRecord<any, ZodTypes> | ZodMap<ZodTypes, ZodTypes>;
10
+ export type ADTs = ZodUnion<readonly [ZodTypes, ...ZodTypes[]]> | ZodDiscriminatedUnion<any, ZodObject<{
11
+ [k: string]: ZodTypes;
12
+ }>[]> | ZodIntersection<ZodTypes, ZodTypes> | ZodNativeEnum<any> | ZodEnum<any>;
13
+ export type ZodTypes = Modifiers | Primitives | ListCollections | KVCollections | ADTs | ZodFunction<any, ZodTypes> | ZodLazy<ZodTypes> | ZodLiteral<any> | ZodEffects<any, any> | ZodCatch<ZodTypes> | ZodPromise<ZodTypes> | ZodBranded<ZodTypes, any> | ZodPipeline<ZodTypes, ZodTypes>;
14
+ export type ZTypeName<T extends ZodTypes> = T["_def"]["typeName"];
15
+ export type FormIssue = {
16
+ name: string;
17
+ code: string;
18
+ message?: string;
19
+ minimum?: number;
20
+ maximum?: number;
21
+ exact?: boolean;
22
+ validation?: string;
23
+ };
24
+ export type Form<T extends ZodType<any, any>> = {
25
+ /** Whether the form is valid. */
26
+ valid: boolean;
27
+ /** Whether the user has interacted with the form. */
28
+ tainted?: boolean;
29
+ /** Whether the form has been posted. */
30
+ posted: boolean;
31
+ /** The schema to parse the form with. */
32
+ schema?: T;
33
+ /** The fields of the form. */
34
+ fields: Record<string, FormField>;
35
+ /** The data of the form. */
36
+ data?: Infer<T>;
37
+ /** Original data for form before editing */
38
+ originalData?: Infer<T>;
39
+ /** The issues of the form. */
40
+ issues?: FormIssue[];
41
+ };
42
+ export type FormField = {
43
+ name: string;
44
+ tainted: boolean;
45
+ coerce?: boolean;
46
+ min?: number;
47
+ max?: number;
48
+ minlength?: number;
49
+ maxlength?: number;
50
+ hasIssue?: boolean;
51
+ pattern?: string;
52
+ required: boolean;
53
+ defaultValue?: string | number | boolean;
54
+ isArray: boolean;
55
+ type: string;
56
+ children?: Record<string, FormField>;
57
+ };
@@ -13,7 +13,6 @@ if (theme === "invert") {
13
13
  }
14
14
  const existingTheme = getContext("theme");
15
15
  setContext("theme", theme);
16
- console.log("theme", theme);
17
16
  </script>
18
17
 
19
18
  <svelte:body style="background-color: var(--bg-app);" />
@@ -43,6 +42,7 @@ console.log("theme", theme);
43
42
  --border-color: var(--l-border-color);
44
43
  --border-subtle-color: var(--l-border-subtle-color);
45
44
  --focus-color: var(--l-focus-color);
45
+ --focus-color-error: var(--l-focus-color-error);
46
46
  --border: var(--l-border);
47
47
  --border-subtle: var(--l-border-subtle);
48
48
  --focus-outline: var(--l-focus-outline);
@@ -58,6 +58,8 @@ console.log("theme", theme);
58
58
  --text-warn-icon: var(--l-text-warn-icon);
59
59
  /* fields */
60
60
  --field-bg: var(--l-field-bg);
61
+ --field-border-color: var(--l-field-border-color);
62
+ --field-border-color-error: var(--l-field-border-color-error);
61
63
  --field-border: var(--l-field-border);
62
64
  --field-text: var(--l-field-text);
63
65
  --field-placeholder: var(--l-field-placeholder);
@@ -133,6 +135,7 @@ console.log("theme", theme);
133
135
  --border-color: var(--d-border-color);
134
136
  --border-subtle-color: var(--d-border-subtle-color);
135
137
  --focus-color: var(--d-focus-color);
138
+ --focus-color-error: var(--d-focus-color-error);
136
139
  --border: var(--d-border);
137
140
  --border-subtle: var(--d-border-subtle);
138
141
  --focus-outline: var(--d-focus-outline);
@@ -149,6 +152,8 @@ console.log("theme", theme);
149
152
  --text-warn-icon: var(--d-text-warn-icon);
150
153
  /* fields */
151
154
  --field-bg: var(--d-field-bg);
155
+ --field-border-color: var(--d-field-border-color);
156
+ --field-border-color-error: var(--d-field-border-color-error);
152
157
  --field-border: var(--d-field-border);
153
158
  --field-text: var(--d-field-text);
154
159
  --field-placeholder: var(--d-field-placeholder);
package/dist/style.css CHANGED
@@ -29,7 +29,9 @@
29
29
  --field-radius: 0.35em;
30
30
  --border-size: 1px;
31
31
  --border-style: solid;
32
- --focus-size: 5px;
32
+ --field-border-size: 1px;
33
+ --field-border-style: solid;
34
+ --focus-size: 2px;
33
35
  --mix-amount: 80%;
34
36
  /* Transitions */
35
37
  --menu-trans: 0.1s ease-in-out;
@@ -46,6 +48,7 @@
46
48
  --l-border-color: hsl(215, 6%, 80%);
47
49
  --l-border-subtle-color: hsl(215, 5%, 87%);
48
50
  --l-focus-color: hsl(215, 60%, 55%);
51
+ --l-focus-color-error: hsl(353, 76%, 58%);
49
52
  --l-border: var(--border-size) var(--border-style) var(--l-border-color);
50
53
  --l-border-subtle: var(--border-size) var(--border-style) var(--l-border-subtle-color);
51
54
  --l-focus-outline: var(--focus-size) solid var(--l-focus-color);
@@ -61,7 +64,9 @@
61
64
  --l-text-warn-icon: #e74c3c;
62
65
  /* fields */
63
66
  --l-field-bg: transparent;
64
- --l-field-border: var(--l-border);
67
+ --l-field-border-color: var(--l-border-color);
68
+ --l-field-border-color-error: var(--l-focus-color-error);
69
+ --l-field-border: var(--field-border-size) var(--field-border-style) var(--l-field-border-color);
65
70
  --l-field-text: var(--l-text);
66
71
  --l-field-placeholder: #999;
67
72
  --l-field-label: #333;
@@ -138,6 +143,7 @@
138
143
  --d-border-color: hsl(215, 8%, 30%);
139
144
  --d-border-subtle-color: hsl(215, 5%, 25%);
140
145
  --d-focus-color: rgb(77, 144, 254);
146
+ --d-focus-color-error: red;
141
147
  --d-border: var(--border-size) var(--border-style) var(--d-border-color);
142
148
  --d-border-subtle: var(--border-size) var(--border-style) var(--d-border-subtle-color);
143
149
  --d-focus-outline: var(--focus-size) solid var(--d-focus-color);
@@ -154,7 +160,9 @@
154
160
  --d-text-warn-icon: #ff6b6b;
155
161
  /* fields */
156
162
  --d-field-bg: transparent;
157
- --d-field-border: var(--d-border);
163
+ --d-field-border-color: var(--d-border-color);
164
+ --d-field-border-color-error: red;
165
+ --d-field-border: var(--field-border-size) var(--field-border-style) var(--d-field-border-color);
158
166
  --d-field-text: var(--d-text);
159
167
  --d-field-placeholder: #666;
160
168
  --d-field-label: #ccc;
@@ -421,6 +429,14 @@ input, textarea, select {
421
429
  transition: border-color 0.1s;
422
430
  }
423
431
 
432
+ input:user-invalid {
433
+ border-color: var(--field-border-color-error);
434
+ }
435
+
436
+ input:user-invalid:focus-visible {
437
+ outline-color: var(--focus-color-error);
438
+ }
439
+
424
440
  select {
425
441
  padding-right: 2em;
426
442
  appearance: none;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lutra",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host 0.0.0.0",
6
6
  "props": "node read_props.js",
@@ -35,7 +35,8 @@
35
35
  },
36
36
  "./form": {
37
37
  "types": "./dist/form/index.d.ts",
38
- "svelte": "./dist/form/index.js"
38
+ "svelte": "./dist/form/index.js",
39
+ "import": "./dist/form/index.js"
39
40
  },
40
41
  "./icons": {
41
42
  "types": "./dist/icons/index.d.ts",
@@ -87,5 +88,10 @@
87
88
  },
88
89
  "svelte": "./dist/index.js",
89
90
  "types": "./dist/index.d.ts",
90
- "type": "module"
91
+ "type": "module",
92
+ "dependencies": {
93
+ "@auth70/bodyguard": "^1.5.2",
94
+ "@auth70/zodex-esm": "^0.7.3",
95
+ "zod": "^3.22.4"
96
+ }
91
97
  }