lutra 0.0.12 → 0.0.13

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.
@@ -17,14 +17,15 @@ let {
17
17
  section {
18
18
  container-type: inline-size;
19
19
  display: grid;
20
- grid-template-columns: repeat(calc(var(--dtcols) - 1), auto) 1fr;
20
+ grid-template-columns: repeat(var(--dtcols), minmax(min-content, 1fr));
21
21
  border-radius: var(--border-radius);
22
- width: 100%;
23
22
  font-size: var(--font-size, 1em);
23
+ margin-inline: calc((var(--dtc) - 1) * 1em);
24
+ width: calc(100% + (var(--dtc) - 1 * 2em));
25
+ overflow-x: auto;
24
26
  }
25
27
  section.contained {
26
28
  border: var(--border);
27
29
  border-radius: var(--border-radius);
28
- overflow-x: auto;
29
30
  }
30
31
  </style>
@@ -19,22 +19,21 @@ setContext("DataTableRow", { header });
19
19
  <style>
20
20
  .DataTableRow {
21
21
  display: grid;
22
- grid-column: 1 / calc(var(--dtcols) + 1);
23
22
  grid-template-columns: subgrid;
23
+ grid-column: 1 / -1;
24
24
  gap: 1.5em;
25
25
  align-items: center;
26
26
  width: 100%;
27
- overflow: none;
28
- padding: 0.75em calc(1.5em * var(--dtc));
27
+ padding: 0.75em 1em;
29
28
  position: relative;
30
29
  }
31
30
  .DataTableRow:hover {
32
- background: color-mix(in srgb, var(--bg-subtle) calc(var(--dtc) * 100%), transparent);
31
+ background: var(--bg-subtle);
33
32
  }
34
33
  .DataTableRow:has(.Actions) {
35
34
  padding-right: 0;
36
35
  }
37
- .DataTableRow :global(*) {
36
+ .DataTableRow :global(> *) {
38
37
  white-space: nowrap;
39
38
  }
40
39
  .DataTableRow.header {
@@ -7,17 +7,21 @@ let {
7
7
  </script>
8
8
 
9
9
  <Tooltip {tip}>
10
- <button onclick={(e) => e.preventDefault()}>
11
- <Icon icon={Help} />
12
- </button>
10
+ <a href="#foo" onclick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
11
+ <Icon icon={Help} --icon-width="16px" --icon-height="16px" --cursor="help" />
12
+ </a>
13
13
  </Tooltip>
14
14
 
15
15
  <style>
16
- button {
16
+ a {
17
17
  border-radius: 50%;
18
+ color: var(--text);
19
+ height: 16px;
20
+ width: 16px;
21
+ display: inline-block;
18
22
  }
19
- button:focus,
20
- button:active {
21
- outline-offset: -3px;
23
+ a:focus,
24
+ a:active {
25
+ outline-offset: -2px;
22
26
  }
23
27
  </style>
@@ -17,19 +17,20 @@ let {
17
17
 
18
18
  <style>
19
19
  .Icon {
20
- display: inline-flex;
20
+ display: flex;
21
21
  align-items: center;
22
22
  justify-content: center;
23
23
  font-size: 1em;
24
24
  width: var(--icon-width, var(--font-size, 1em));
25
25
  height: var(--icon-height, var(--font-size, 1em));
26
26
  overflow: clip;
27
- vertical-align: var(--vertical-align, baseline);
27
+ vertical-align: var(--vertical-align, text-bottom);
28
28
  cursor: var(--cursor, default);
29
29
  }
30
+ img,
30
31
  .Icon :global(svg) {
31
32
  width: 100%;
32
33
  height: 100%;
33
- display: inline-block;
34
+ display: block;
34
35
  }
35
36
  </style>
@@ -32,9 +32,8 @@ const id = `tt-${Math.random().toString(36).substring(2, 15) + Math.random().toS
32
32
  <style>
33
33
  .Tooltip {
34
34
  position: relative;
35
- height: 100%;
36
35
  display: inline-flex;
37
- align-items: center;
36
+ vertical-align: var(--vertical-align, text-bottom);
38
37
  }
39
38
  .TooltipContainer {
40
39
  position: absolute;
@@ -58,8 +57,8 @@ const id = `tt-${Math.random().toString(36).substring(2, 15) + Math.random().toS
58
57
  border-left: var(--border-subtle);
59
58
  border-right: var(--border-subtle);
60
59
  display: block;
61
- font-size: max(0.85rem, 12px);
62
- line-height: 1.4;
60
+ font-size: max(0.75rem, 11px);
61
+ line-height: 1.35;
63
62
  font-weight: 500;
64
63
  color: var(--text);
65
64
  max-width: clamp(5ch, 100%, 35ch);
@@ -72,6 +71,10 @@ const id = `tt-${Math.random().toString(36).substring(2, 15) + Math.random().toS
72
71
  .TooltipContent :global(b) {
73
72
  font-weight: 700;
74
73
  }
74
+ .TooltipTrigger {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ }
75
78
  .Tooltip:has(.TooltipTrigger:hover):not(.open) .TooltipContainer {
76
79
  animation: fadeIn 0.2s var(--delay, 0.5s) ease-in-out forwards;
77
80
  }
@@ -16,6 +16,7 @@
16
16
  gap: 1.35em;
17
17
  background: color-mix(in srgb, var(--bg-subtle) calc(var(--fcc) * 100%), transparent);
18
18
  padding: calc(1.5em * var(--fcc)) calc(1.5em * var(--fcc));
19
+ grid-column: 1 / -1;
19
20
  }
20
21
  @container (max-width: 400px) {
21
22
  .FieldActions {
@@ -13,7 +13,7 @@
13
13
  container-type: inline-size;
14
14
  display: grid;
15
15
  gap: 1.5em;
16
- grid-template-columns: 1fr;
16
+ grid-template-columns: 250px 1fr;
17
17
  border-radius: var(--border-radius);
18
18
  }
19
19
  .FieldContainer.contained {
@@ -36,11 +36,12 @@ let {
36
36
  padding: 0;
37
37
  display: grid;
38
38
  gap: 0rem;
39
- grid-template-columns: 1fr;
39
+ grid-template-columns: subgrid;
40
40
  }
41
41
  .FieldSection .FieldSectionTitle {
42
42
  display: flex;
43
43
  flex-direction: column;
44
+ grid-column: 0 / 1;
44
45
  background-color: var(--base);
45
46
  gap: 0.25rem;
46
47
  padding: 1em 1.5em;
@@ -51,20 +52,21 @@ let {
51
52
  }
52
53
  .FieldSection .FieldSectionFields {
53
54
  padding: calc(var(--padding, 1.5em) * var(--fcc));
55
+ grid-column: 1 / -1;
54
56
  display: grid;
55
57
  gap: 1.5rem;
56
58
  }
57
59
  @container (min-width: 600px) {
58
60
  .FieldSection {
59
61
  gap: 3rem;
60
- padding: 2rem;
61
- grid-template-columns: 1fr;
62
+ padding: calc(2rem * var(--fcc));
63
+ /*grid-template-columns: 1fr;*/
62
64
  border-bottom: 1px dotted var(--border-light);
63
65
  margin-bottom: 3rem;
64
66
  border-radius: 0;
65
67
  }
66
68
  .FieldSection:has(.FieldSectionTitle) {
67
- grid-template-columns: minmax(180px, 1fr) 3fr;
69
+ /*grid-template-columns: minmax(180px, 1fr) 3fr;*/
68
70
  }
69
71
  .FieldSection:last-child {
70
72
  border-bottom: none;
@@ -86,8 +88,8 @@ let {
86
88
  }
87
89
  @container (min-width: 1024px) {
88
90
  .FieldSection {
89
- grid-template-columns: minmax(300px, 1fr) 3fr;
90
- padding: 3rem;
91
+ /*grid-template-columns: minmax(300px, 1fr) 3fr;*/
92
+ padding: calc(3rem * var(--fcc));
91
93
  }
92
94
  .FieldSection:not(:first-child) {
93
95
  border-top: 0;
@@ -16,26 +16,30 @@ let {
16
16
  } = $props();
17
17
  setContext("form", form);
18
18
  setContext("form.validators", getIndividualValidators(form));
19
- const schema = dezerialize(form.schema);
19
+ const schema = form?.schema ? dezerialize(form?.schema) : null;
20
20
  const bodyguard = new Bodyguard();
21
21
  let formEl;
22
22
  function setFormIssuesAndFields(issues, fields) {
23
- form.issues = issues;
24
- form.fields = fields;
23
+ if (form) {
24
+ form.issues = issues;
25
+ form.fields = fields;
26
+ }
25
27
  }
26
28
  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);
29
+ if (form && schema) {
30
+ form.tainted = true;
31
+ const req = new Request("localhost", {
32
+ method: "POST",
33
+ body: new FormData(formEl)
34
+ });
35
+ const result = await bodyguard.softForm(req, schema.parse);
36
+ if (result.success === true) {
37
+ form.valid = true;
38
+ form.issues = [];
39
+ } else {
40
+ form.valid = false;
41
+ form.issues = parseFormIssues(result.error.issues);
42
+ }
39
43
  }
40
44
  }
41
45
  onMount(() => {
@@ -61,19 +65,20 @@ onMount(() => {
61
65
  // `result` is an `ActionResult` object
62
66
  // `update` is a function which triggers the default logic that would be triggered if this callback wasn't set
63
67
  console.log('result', result);
64
- const resultForm = result?.data?.form;
68
+ const resultForm = result.type !== "redirect" && result.type !== "error" ? result?.data?.form : null;
65
69
  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);
70
+ if(resultForm && form) {
71
+ form.valid = Object.assign({ valid: false }, resultForm).valid ?? false;
72
72
  }
73
73
  } else if(result.type === "failure") {
74
- if(resultForm) {
75
- setFormIssuesAndFields(resultForm.issues, resultForm.fields);
74
+ if(resultForm && form) {
75
+ setFormIssuesAndFields(
76
+ Object.assign({ issues: [] }, resultForm).issues, // Have to assign to avoid type error as we cant use `as` here
77
+ Object.assign({ fields: [] }, resultForm).fields,
78
+ );
76
79
  }
80
+ } else if(result.type === "error") {
81
+ console.error('[lutra] Error from form enhance call', result.error);
77
82
  } else if(result.type === "redirect") {
78
83
  window.location.href = result.location;
79
84
  }
@@ -3,7 +3,7 @@ import type { Snippet } from "svelte";
3
3
  import type { Form } from "./types.js";
4
4
  declare const __propDef: {
5
5
  props: {
6
- form: Form<any>;
6
+ form?: Form<any> | undefined;
7
7
  action?: string | undefined;
8
8
  enctype?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
9
9
  method?: "GET" | "POST" | undefined;
@@ -13,6 +13,7 @@ import { ZodType } from "zod";
13
13
  import FieldContent from "./FieldContent.svelte";
14
14
  let {
15
15
  alt,
16
+ attrs,
16
17
  autocapitalize,
17
18
  autocomplete,
18
19
  autocorrect,
@@ -69,7 +70,7 @@ let copyTooltipOpen = $state(false);
69
70
  let copyBtnIcon = $state(Copy);
70
71
  let viewBtnIcon = $state(Show);
71
72
  const form = getContext("form");
72
- const field = $derived(form.fields[name]);
73
+ const field = $derived(form?.fields[name]);
73
74
  const issue = $derived(form?.issues?.find((issue2) => issue2.name === name));
74
75
  const validator = getContext("form.validators")?.[name];
75
76
  const data = form?.data;
@@ -115,6 +116,7 @@ function copy(e) {
115
116
  <input
116
117
  bind:this={el}
117
118
  {alt}
119
+ {...attrs}
118
120
  autocapitalize={typeof autocapitalize === 'string' ? autocapitalize : (typeof autocapitalize === 'boolean' ? (autocapitalize ? 'on' : 'off') : undefined)}
119
121
  {autocomplete}
120
122
  autocorrect={typeof autocorrect === 'boolean' ? (autocorrect ? 'on' : 'off') : undefined}
@@ -151,7 +153,7 @@ function copy(e) {
151
153
  {tabindex}
152
154
  {title}
153
155
  {type}
154
- value={value || getFromObjWithStringPath(Object.assign(originalData, data), name) || form?.fields?.[name]?.defaultValue || ''}
156
+ value={value || getFromObjWithStringPath(Object.assign(originalData ?? {}, data ?? {}), name) || form?.fields?.[name]?.defaultValue || ''}
155
157
  {webkitdirectory}
156
158
  />
157
159
  {/snippet}
@@ -4,6 +4,8 @@ declare const __propDef: {
4
4
  props: {
5
5
  /** alt attribute for the image type. Required for accessibility */
6
6
  alt?: string | undefined;
7
+ /** Additional attributes to add to the input element. */
8
+ attrs?: Record<string, string> | undefined;
7
9
  /** Whether the input should be autocapitalized. */
8
10
  autocapitalize?: boolean | "none" | "off" | "on" | "sentences" | "words" | "characters" | undefined;
9
11
  /** Specifies whether autocomplete is enabled for the input. */
@@ -117,7 +119,7 @@ declare const __propDef: {
117
119
  events: {
118
120
  [evt: string]: CustomEvent<any>;
119
121
  };
120
- slots: {}; /** Onclick event handler */
122
+ slots: {};
121
123
  };
122
124
  export type InputProps = typeof __propDef.props;
123
125
  export type InputEvents = typeof __propDef.events;
@@ -13,27 +13,14 @@ import { getFromObjWithStringPath } from "./form.js";
13
13
  import { ZodType } from "zod";
14
14
  import FieldContent from "./FieldContent.svelte";
15
15
  let {
16
- alt,
17
- checked,
16
+ attrs,
18
17
  children,
19
- contained,
20
- defaultValue,
21
- dirname,
22
18
  disabled,
23
19
  help,
24
20
  id = $bindable(createId()),
25
21
  label,
26
22
  labelTip,
27
- list,
28
- maxlength,
29
- minlength,
30
- max,
31
- min,
32
- multiple,
33
23
  name,
34
- pattern,
35
- placeholder,
36
- suffix,
37
24
  onblur,
38
25
  onchange,
39
26
  onclick,
@@ -42,23 +29,16 @@ let {
42
29
  onkeyup,
43
30
  onkeypress,
44
31
  options,
45
- prefix,
46
- readonly,
32
+ placeholder,
47
33
  required,
48
34
  results,
49
- shape = "rounded",
50
- src,
35
+ shape = "default",
51
36
  tabindex,
52
37
  value = $bindable()
53
38
  } = $props();
54
39
  let el = $state();
55
- let copyTitle = $state("Copy");
56
- let viewTitle = $state("Show");
57
- let copyTooltipOpen = $state(false);
58
- let copyBtnIcon = $state(Copy);
59
- let viewBtnIcon = $state(Show);
60
40
  const form = getContext("form");
61
- const field = $derived(form.fields[name]);
41
+ const field = $derived(form?.fields[name]);
62
42
  const issue = $derived(form?.issues?.find((issue2) => issue2.name === name));
63
43
  const validator = getContext("form.validators")?.[name];
64
44
  const data = form?.data;
@@ -74,6 +54,7 @@ const originalData = form?.originalData;
74
54
  >
75
55
  <select
76
56
  bind:this={el}
57
+ {...attrs}
77
58
  {disabled}
78
59
  {id}
79
60
  {name}
@@ -88,7 +69,7 @@ const originalData = form?.originalData;
88
69
  required={required || field?.required}
89
70
  {results}
90
71
  {tabindex}
91
- value={value || getFromObjWithStringPath(Object.assign(originalData, data), name) || form?.fields?.[name]?.defaultValue || ''}
72
+ value={value || getFromObjWithStringPath(Object.assign(originalData ?? {}, data ?? {}), name) || form?.fields?.[name]?.defaultValue || ''}
92
73
  >
93
74
  {#if children}
94
75
  {@render children()}
@@ -1,20 +1,12 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
- /** alt attribute for the image type. Required for accessibility */
5
- alt?: string | undefined;
6
- /** Whether the input should be checked. */
7
- checked?: boolean | undefined;
4
+ /** Additional attributes to add to the input element. */
5
+ attrs?: Record<string, string> | undefined;
8
6
  /** Options for the select element. */
9
7
  children?: ((this: void) => typeof import("svelte").SnippetReturn & {
10
8
  _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
11
9
  }) | undefined;
12
- /** Whether the input should be contained. */
13
- contained?: boolean | undefined;
14
- /** The default value of the input element. */
15
- defaultValue?: string | undefined;
16
- /** Form field name for the directionality of the element's text content during form submission */
17
- dirname?: string | undefined;
18
10
  /** Whether the input should be disabled. */
19
11
  disabled?: boolean | undefined;
20
12
  /** Help text to display below the input. */
@@ -31,18 +23,6 @@ declare const __propDef: {
31
23
  labelTip?: string | ((this: void) => typeof import("svelte").SnippetReturn & {
32
24
  _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
33
25
  }) | undefined;
34
- /** The id of a datalist element that contains pre-defined options for the input element. */
35
- list?: string | undefined;
36
- /** The maximum number of characters (as UTF-16 code units) the user can enter into the input. Valid for text, search, url, tel, email, and password. */
37
- maxlength?: number | undefined;
38
- /** The minimum number of characters (as UTF-16 code units) the user can enter into the input. Valid for text, search, url, tel, email, and password. */
39
- minlength?: number | undefined;
40
- /** The maximum value of the input element. Valid for date, month, week, time, datetime-local, number, and range. */
41
- max?: number | undefined;
42
- /** The minimum value of the input element. Valid for date, month, week, time, datetime-local, number, and range. */
43
- min?: number | undefined;
44
- /** Whether the input should allow multiple values. Valid for email and file inputs. */
45
- multiple?: boolean | undefined;
46
26
  /** The name of the input element. */
47
27
  name: string;
48
28
  /** The onblur event handler */
@@ -64,8 +44,6 @@ declare const __propDef: {
64
44
  value: string;
65
45
  label: string;
66
46
  }[] | undefined;
67
- /** A regular expression that the input's value is checked against. Valid for text, search, url, tel, email, and password. */
68
- pattern?: string | undefined;
69
47
  /** Placeholder text to display when the input is empty. */
70
48
  placeholder?: string | undefined;
71
49
  /** Suffix content, to display after the input. */
@@ -76,8 +54,6 @@ declare const __propDef: {
76
54
  prefix?: string | ((this: void) => typeof import("svelte").SnippetReturn & {
77
55
  _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\"";
78
56
  }) | undefined;
79
- /** Whether the input should be read-only. */
80
- readonly?: boolean | undefined;
81
57
  /** Whether the input should be required. */
82
58
  required?: boolean | undefined;
83
59
  /** The maximum number of items that should be displayed in the drop-down list of previous search queries. Safari only. */
@@ -51,11 +51,13 @@ export function fieldValidate(form, name, el, validator) {
51
51
  export function fieldKeydown(form, name, el, validator, onkeydown) {
52
52
  return async function (e) {
53
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);
54
+ if (form) {
55
+ const possibleKey = e?.key;
56
+ if (ignoreKeys.includes(possibleKey))
57
+ return;
58
+ form.data[name] = el()?.value || '';
59
+ fieldValidate(form, name, el(), validator);
60
+ }
59
61
  }, 0); // Wait for the key to be updated in the input.
60
62
  if (onkeydown)
61
63
  return onkeydown(e);
@@ -72,10 +74,12 @@ export function fieldKeydown(form, name, el, validator, onkeydown) {
72
74
  */
73
75
  export function fieldChange(form, name, el, validator, onchange) {
74
76
  return async function (e) {
75
- console.log('fieldChange', name, el()?.value, validator);
76
- form.data[name] = el()?.value || '';
77
- form.fields[name].tainted = true;
78
- fieldValidate(form, name, el(), validator);
77
+ if (form) {
78
+ console.log('fieldChange', name, el()?.value, validator);
79
+ form.data[name] = el()?.value || '';
80
+ form.fields[name].tainted = true;
81
+ fieldValidate(form, name, el(), validator);
82
+ }
79
83
  if (onchange)
80
84
  return onchange(e);
81
85
  };
@@ -49,4 +49,4 @@ export declare function getFromObjWithStringPath(obj: any, path: string): string
49
49
  * @param {Form} form - The form to get the validators from.
50
50
  * @returns {Record<keyof Infer<T>, (value: any) => boolean>} - The validators for each field.
51
51
  */
52
- export declare function getIndividualValidators<T extends ZodTypes>(form: Form<T>): Record<keyof Infer<T>, (value: any) => boolean>;
52
+ export declare function getIndividualValidators<T extends ZodTypes>(form?: Form<T>): Record<keyof Infer<T>, (value: any) => boolean>;
package/dist/form/form.js CHANGED
@@ -246,6 +246,8 @@ export function arrayPathToStringPath(path) {
246
246
  * @returns {string | number | Date | boolean | object | undefined} - The value from the object.
247
247
  */
248
248
  export function getFromObjWithStringPath(obj, path) {
249
+ if (!obj)
250
+ return undefined;
249
251
  const parts = path.split('.');
250
252
  let current = obj;
251
253
  for (let i = 0; i < parts.length; i++) {
@@ -301,6 +303,8 @@ export function getFromObjWithStringPath(obj, path) {
301
303
  * @returns {Record<keyof Infer<T>, (value: any) => boolean>} - The validators for each field.
302
304
  */
303
305
  export function getIndividualValidators(form) {
306
+ if (!form)
307
+ return {};
304
308
  const schema = form.schema;
305
309
  const fields = form.fields;
306
310
  if (!schema || !fields) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lutra",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host 0.0.0.0",
6
6
  "props": "node read_props.js",