@fuzdev/fuz_app 0.1.1 → 0.2.1

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.
@@ -5,9 +5,10 @@
5
5
  import {Username} from '../auth/account_schema.js';
6
6
  import {PASSWORD_LENGTH_MIN} from '../auth/password.js';
7
7
  import {auth_state_context} from './auth_state.svelte.js';
8
- import {enter_advance} from './enter_advance.js';
8
+ import {FormState} from './form_state.svelte.js';
9
9
 
10
10
  const auth_state = auth_state_context.get();
11
+ const form_state = new FormState();
11
12
 
12
13
  let token = $state('');
13
14
  let username = $state('');
@@ -21,11 +22,19 @@
21
22
  username.trim() &&
22
23
  username_valid &&
23
24
  password.length >= PASSWORD_LENGTH_MIN &&
24
- passwords_match,
25
+ passwords_match &&
26
+ !auth_state.verifying,
25
27
  );
26
28
 
27
29
  const handle_bootstrap = async (): Promise<void> => {
28
- if (!can_submit) return;
30
+ form_state.attempt();
31
+ if (!can_submit) {
32
+ if (!token.trim()) form_state.focus('token');
33
+ else if (!username.trim() || !username_valid) form_state.focus('username');
34
+ else if (password.length < PASSWORD_LENGTH_MIN) form_state.focus('password');
35
+ else if (!passwords_match) form_state.focus('password_confirm');
36
+ return;
37
+ }
29
38
  await auth_state.bootstrap(token.trim(), username.trim(), password);
30
39
  };
31
40
  </script>
@@ -39,11 +48,12 @@
39
48
  e.preventDefault();
40
49
  void handle_bootstrap();
41
50
  }}
42
- {@attach enter_advance()}
51
+ {@attach form_state.form()}
43
52
  >
44
53
  <label>
45
54
  <div class="title">bootstrap token</div>
46
55
  <input
56
+ name="token"
47
57
  type="password"
48
58
  bind:value={token}
49
59
  placeholder="paste token"
@@ -54,6 +64,7 @@
54
64
  <label>
55
65
  <div class="title">username</div>
56
66
  <input
67
+ name="username"
57
68
  type="text"
58
69
  bind:value={username}
59
70
  placeholder="admin"
@@ -61,16 +72,16 @@
61
72
  disabled={auth_state.verifying}
62
73
  />
63
74
  </label>
64
- {#if username && !username_valid}
75
+ {#if form_state.show('username') && username && !username_valid}
65
76
  <p class="color_c_50 font_size_sm mt_0 mb_xs">
66
77
  3-39 chars, starts with a letter, ends with letter/number, middle allows dash/underscore
67
78
  </p>
68
79
  {/if}
69
80
  <fieldset>
70
- <legend>password</legend>
71
81
  <label>
72
82
  <div class="title">password (min {PASSWORD_LENGTH_MIN} characters)</div>
73
83
  <input
84
+ name="password"
74
85
  type="password"
75
86
  bind:value={password}
76
87
  placeholder="password"
@@ -78,9 +89,15 @@
78
89
  disabled={auth_state.verifying}
79
90
  />
80
91
  </label>
92
+ {#if form_state.show('password') && password && password.length < PASSWORD_LENGTH_MIN}
93
+ <p class="color_c_50 font_size_sm mt_0 mb_xs">
94
+ password must be at least {PASSWORD_LENGTH_MIN} characters
95
+ </p>
96
+ {/if}
81
97
  <label>
82
98
  <div class="title">confirm password</div>
83
99
  <input
100
+ name="password_confirm"
84
101
  type="password"
85
102
  bind:value={password_confirm}
86
103
  placeholder="confirm password"
@@ -88,14 +105,14 @@
88
105
  disabled={auth_state.verifying}
89
106
  />
90
107
  </label>
91
- {#if password && password_confirm && !passwords_match}
108
+ {#if form_state.show('password_confirm') && password && password_confirm && !passwords_match}
92
109
  <p class="color_c_50 font_size_sm mt_0 mb_xs">passwords do not match</p>
93
110
  {/if}
94
111
  </fieldset>
95
112
  <div class="row gap_sm">
96
113
  <PendingButton
97
114
  pending={auth_state.verifying}
98
- disabled={!can_submit}
115
+ disabled={auth_state.verifying}
99
116
  onclick={handle_bootstrap}
100
117
  class={auth_state.verify_error ? 'color_c' : ''}
101
118
  >
@@ -1 +1 @@
1
- {"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"AAwFA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"AAqGA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -5,7 +5,7 @@
5
5
  import {autofocus} from '@fuzdev/fuz_ui/autofocus.svelte.js';
6
6
 
7
7
  import {auth_state_context} from './auth_state.svelte.js';
8
- import {enter_advance} from './enter_advance.js';
8
+ import {FormState} from './form_state.svelte.js';
9
9
 
10
10
  const {
11
11
  username_label = 'username or email',
@@ -16,6 +16,7 @@
16
16
  } = $props();
17
17
 
18
18
  const auth_state = auth_state_context.get();
19
+ const form_state = new FormState();
19
20
 
20
21
  let username = $state('');
21
22
  let password = $state('');
@@ -23,13 +24,20 @@
23
24
  const handle_login = async (): Promise<void> => {
24
25
  const u = username.trim();
25
26
  const p = password;
26
- if (u && p) {
27
- const success = await auth_state.login(u, p);
28
- if (success) {
29
- username = '';
30
- password = '';
31
- await goto(redirect_on_login);
32
- }
27
+ if (!u) {
28
+ form_state.focus('username');
29
+ return;
30
+ }
31
+ if (!p) {
32
+ form_state.focus('password');
33
+ return;
34
+ }
35
+ const success = await auth_state.login(u, p);
36
+ if (success) {
37
+ form_state.reset();
38
+ username = '';
39
+ password = '';
40
+ await goto(redirect_on_login);
33
41
  }
34
42
  };
35
43
  </script>
@@ -43,11 +51,12 @@
43
51
  e.preventDefault();
44
52
  void handle_login();
45
53
  }}
46
- {@attach enter_advance()}
54
+ {@attach form_state.form()}
47
55
  >
48
56
  <label>
49
57
  <div class="title">{username_label}</div>
50
58
  <input
59
+ name="username"
51
60
  type="text"
52
61
  bind:value={username}
53
62
  placeholder={username_label}
@@ -59,6 +68,7 @@
59
68
  <label>
60
69
  <div class="title">password</div>
61
70
  <input
71
+ name="password"
62
72
  type="password"
63
73
  bind:value={password}
64
74
  placeholder="password"
@@ -69,7 +79,7 @@
69
79
  <div class="row gap_sm">
70
80
  <PendingButton
71
81
  pending={auth_state.verifying}
72
- disabled={!username.trim() || !password}
82
+ disabled={auth_state.verifying}
73
83
  onclick={handle_login}
74
84
  class={auth_state.verify_error ? 'color_c' : ''}
75
85
  >
@@ -1 +1 @@
1
- {"version":3,"file":"LoginForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/LoginForm.svelte"],"names":[],"mappings":"AAWC,KAAK,gBAAgB,GAAI;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AA4DH,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"LoginForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/LoginForm.svelte"],"names":[],"mappings":"AAWC,KAAK,gBAAgB,GAAI;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAoEH,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -7,7 +7,7 @@
7
7
  import {Username} from '../auth/account_schema.js';
8
8
  import {PASSWORD_LENGTH_MIN} from '../auth/password.js';
9
9
  import {auth_state_context} from './auth_state.svelte.js';
10
- import {enter_advance} from './enter_advance.js';
10
+ import {FormState} from './form_state.svelte.js';
11
11
 
12
12
  const {
13
13
  redirect_on_signup = resolve('/account' as any),
@@ -16,6 +16,7 @@
16
16
  } = $props();
17
17
 
18
18
  const auth_state = auth_state_context.get();
19
+ const form_state = new FormState();
19
20
 
20
21
  let username = $state('');
21
22
  let email = $state('');
@@ -33,9 +34,16 @@
33
34
  );
34
35
 
35
36
  const handle_signup = async (): Promise<void> => {
36
- if (!can_submit) return;
37
+ form_state.attempt();
38
+ if (!can_submit) {
39
+ if (!username.trim() || !username_valid) form_state.focus('username');
40
+ else if (password.length < PASSWORD_LENGTH_MIN) form_state.focus('password');
41
+ else if (!passwords_match) form_state.focus('password_confirm');
42
+ return;
43
+ }
37
44
  const success = await auth_state.signup(username.trim(), password, email.trim() || undefined);
38
45
  if (success) {
46
+ form_state.reset();
39
47
  await goto(redirect_on_signup);
40
48
  }
41
49
  };
@@ -50,11 +58,12 @@
50
58
  e.preventDefault();
51
59
  void handle_signup();
52
60
  }}
53
- {@attach enter_advance()}
61
+ {@attach form_state.form()}
54
62
  >
55
63
  <label>
56
64
  <div class="title">username</div>
57
65
  <input
66
+ name="username"
58
67
  type="text"
59
68
  bind:value={username}
60
69
  placeholder="username"
@@ -63,7 +72,7 @@
63
72
  {@attach autofocus()}
64
73
  />
65
74
  </label>
66
- {#if username && !username_valid}
75
+ {#if form_state.show('username') && username && !username_valid}
67
76
  <p class="color_c_50 font_size_sm mt_0 mb_xs">
68
77
  3-39 chars, starts with a letter, ends with letter/number, middle allows dash/underscore
69
78
  </p>
@@ -71,6 +80,7 @@
71
80
  <label>
72
81
  <div class="title">email (optional)</div>
73
82
  <input
83
+ name="email"
74
84
  type="email"
75
85
  bind:value={email}
76
86
  placeholder="email"
@@ -79,10 +89,10 @@
79
89
  />
80
90
  </label>
81
91
  <fieldset>
82
- <legend>password</legend>
83
92
  <label>
84
93
  <div class="title">password (min {PASSWORD_LENGTH_MIN} characters)</div>
85
94
  <input
95
+ name="password"
86
96
  type="password"
87
97
  bind:value={password}
88
98
  placeholder="password"
@@ -90,9 +100,15 @@
90
100
  disabled={auth_state.verifying}
91
101
  />
92
102
  </label>
103
+ {#if form_state.show('password') && password && password.length < PASSWORD_LENGTH_MIN}
104
+ <p class="color_c_50 font_size_sm mt_0 mb_xs">
105
+ password must be at least {PASSWORD_LENGTH_MIN} characters
106
+ </p>
107
+ {/if}
93
108
  <label>
94
109
  <div class="title">confirm password</div>
95
110
  <input
111
+ name="password_confirm"
96
112
  type="password"
97
113
  bind:value={password_confirm}
98
114
  placeholder="confirm password"
@@ -100,14 +116,14 @@
100
116
  disabled={auth_state.verifying}
101
117
  />
102
118
  </label>
103
- {#if password && password_confirm && !passwords_match}
119
+ {#if form_state.show('password_confirm') && password && password_confirm && !passwords_match}
104
120
  <p class="color_c_50 font_size_sm mt_0 mb_xs">passwords do not match</p>
105
121
  {/if}
106
122
  </fieldset>
107
123
  <div class="row gap_sm">
108
124
  <PendingButton
109
125
  pending={auth_state.verifying}
110
- disabled={!can_submit}
126
+ disabled={auth_state.verifying}
111
127
  onclick={handle_signup}
112
128
  class={auth_state.verify_error ? 'color_c' : ''}
113
129
  >
@@ -1 +1 @@
1
- {"version":3,"file":"SignupForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/SignupForm.svelte"],"names":[],"mappings":"AAaC,KAAK,gBAAgB,GAAI;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAuFH,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"SignupForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/SignupForm.svelte"],"names":[],"mappings":"AAaC,KAAK,gBAAgB,GAAI;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAmGH,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Reactive form state for controlling when field errors appear and focusing invalid fields.
3
+ *
4
+ * Tracks per-field `touched` state (set on blur via delegated `focusout`) and form-level
5
+ * `attempted` state (set on submit attempt). Errors show after a field is blurred or
6
+ * after a submit attempt, avoiding premature validation while the user is still typing.
7
+ *
8
+ * The {@link FormState.form | form} attachment also handles Enter key advancing
9
+ * between focusable elements.
10
+ *
11
+ * All trackable inputs must have a `name` attribute — an error is thrown in dev
12
+ * if an input without `name` loses focus.
13
+ *
14
+ * @example
15
+ * ```svelte
16
+ * <script>
17
+ * const form_state = new FormState();
18
+ * let username = $state('');
19
+ * const username_valid = $derived(Username.safeParse(username).success);
20
+ * const can_submit = $derived(username.trim() && username_valid);
21
+ *
22
+ * const handle_submit = async () => {
23
+ * form_state.attempt();
24
+ * if (!can_submit) {
25
+ * if (!username.trim() || !username_valid) form_state.focus('username');
26
+ * return;
27
+ * }
28
+ * // submit...
29
+ * };
30
+ * </script>
31
+ *
32
+ * <form {@attach form_state.form()} onsubmit={(e) => { e.preventDefault(); void handle_submit(); }}>
33
+ * <input name="username" bind:value={username} />
34
+ * {#if form_state.show('username') && username && !username_valid}
35
+ * <p>error message</p>
36
+ * {/if}
37
+ * <PendingButton onclick={handle_submit}>submit</PendingButton>
38
+ * </form>
39
+ * ```
40
+ *
41
+ * @module
42
+ */
43
+ import type { Attachment } from 'svelte/attachments';
44
+ export declare class FormState {
45
+ #private;
46
+ /**
47
+ * Whether a submit attempt has been made.
48
+ */
49
+ get attempted(): boolean;
50
+ /**
51
+ * Creates a form attachment that handles Enter key advancing between
52
+ * focusable elements and tracks field touched state via delegated `focusout`.
53
+ *
54
+ * Fields are identified by their `name` attribute.
55
+ */
56
+ form(): Attachment<HTMLFormElement>;
57
+ /**
58
+ * Whether a field has been blurred at least once.
59
+ */
60
+ is_touched(field: string): boolean;
61
+ /**
62
+ * Whether to show validation errors for a field.
63
+ * Returns `true` if the field has been blurred or a submit attempt was made.
64
+ */
65
+ show(field: string): boolean;
66
+ /**
67
+ * Programmatically marks a field as touched without requiring a blur event.
68
+ */
69
+ touch(field: string): void;
70
+ /**
71
+ * Focuses the named input within the form.
72
+ */
73
+ focus(field: string): void;
74
+ /**
75
+ * Marks the form as having been submitted, causing all field errors to show.
76
+ */
77
+ attempt(): void;
78
+ /**
79
+ * Resets all touched and attempted state.
80
+ */
81
+ reset(): void;
82
+ }
83
+ //# sourceMappingURL=form_state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/form_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AASnD,qBAAa,SAAS;;IAKrB;;OAEG;IACH,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;;OAKG;IACH,IAAI,IAAI,UAAU,CAAC,eAAe,CAAC;IAuCnC;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIlC;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI5B;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAe1B;;OAEG;IACH,OAAO,IAAI,IAAI;IAIf;;OAEG;IACH,KAAK,IAAI,IAAI;CAIb"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Reactive form state for controlling when field errors appear and focusing invalid fields.
3
+ *
4
+ * Tracks per-field `touched` state (set on blur via delegated `focusout`) and form-level
5
+ * `attempted` state (set on submit attempt). Errors show after a field is blurred or
6
+ * after a submit attempt, avoiding premature validation while the user is still typing.
7
+ *
8
+ * The {@link FormState.form | form} attachment also handles Enter key advancing
9
+ * between focusable elements.
10
+ *
11
+ * All trackable inputs must have a `name` attribute — an error is thrown in dev
12
+ * if an input without `name` loses focus.
13
+ *
14
+ * @example
15
+ * ```svelte
16
+ * <script>
17
+ * const form_state = new FormState();
18
+ * let username = $state('');
19
+ * const username_valid = $derived(Username.safeParse(username).success);
20
+ * const can_submit = $derived(username.trim() && username_valid);
21
+ *
22
+ * const handle_submit = async () => {
23
+ * form_state.attempt();
24
+ * if (!can_submit) {
25
+ * if (!username.trim() || !username_valid) form_state.focus('username');
26
+ * return;
27
+ * }
28
+ * // submit...
29
+ * };
30
+ * </script>
31
+ *
32
+ * <form {@attach form_state.form()} onsubmit={(e) => { e.preventDefault(); void handle_submit(); }}>
33
+ * <input name="username" bind:value={username} />
34
+ * {#if form_state.show('username') && username && !username_valid}
35
+ * <p>error message</p>
36
+ * {/if}
37
+ * <PendingButton onclick={handle_submit}>submit</PendingButton>
38
+ * </form>
39
+ * ```
40
+ *
41
+ * @module
42
+ */
43
+ import { DEV } from 'esm-env';
44
+ import { on } from 'svelte/events';
45
+ import { SvelteSet } from 'svelte/reactivity';
46
+ const FOCUSABLE_SELECTOR = 'input:not(:disabled), button:not(:disabled)';
47
+ const FORM_INPUT_SELECTOR = 'input, textarea, select';
48
+ export class FormState {
49
+ #touched = new SvelteSet();
50
+ #form = null;
51
+ #attempted = $state(false);
52
+ /**
53
+ * Whether a submit attempt has been made.
54
+ */
55
+ get attempted() {
56
+ return this.#attempted;
57
+ }
58
+ /**
59
+ * Creates a form attachment that handles Enter key advancing between
60
+ * focusable elements and tracks field touched state via delegated `focusout`.
61
+ *
62
+ * Fields are identified by their `name` attribute.
63
+ */
64
+ form() {
65
+ if (DEV && this.#form) {
66
+ throw new Error('FormState: form() called while already attached to a form.');
67
+ }
68
+ return (form) => {
69
+ this.#form = form;
70
+ const keydown_cleanup = on(form, 'keydown', (e) => {
71
+ if (e.key !== 'Enter')
72
+ return;
73
+ if (!(e.target instanceof HTMLInputElement))
74
+ return;
75
+ const elements = Array.from(form.querySelectorAll(FOCUSABLE_SELECTOR));
76
+ const index = elements.indexOf(e.target);
77
+ if (index < 0)
78
+ return;
79
+ e.preventDefault();
80
+ elements[(index + 1) % elements.length].focus();
81
+ });
82
+ const focusout_cleanup = on(form, 'focusout', (e) => {
83
+ const target = e.target;
84
+ if (target instanceof HTMLElement && target.matches(FORM_INPUT_SELECTOR)) {
85
+ const name = target.name;
86
+ if (DEV && !name) {
87
+ throw new Error('FormState: input missing name attribute. All inputs in a FormState form must have a name.');
88
+ }
89
+ if (name) {
90
+ this.#touched.add(name);
91
+ }
92
+ }
93
+ });
94
+ return () => {
95
+ keydown_cleanup();
96
+ focusout_cleanup();
97
+ this.#form = null;
98
+ };
99
+ };
100
+ }
101
+ /**
102
+ * Whether a field has been blurred at least once.
103
+ */
104
+ is_touched(field) {
105
+ return this.#touched.has(field);
106
+ }
107
+ /**
108
+ * Whether to show validation errors for a field.
109
+ * Returns `true` if the field has been blurred or a submit attempt was made.
110
+ */
111
+ show(field) {
112
+ return this.#touched.has(field) || this.#attempted;
113
+ }
114
+ /**
115
+ * Programmatically marks a field as touched without requiring a blur event.
116
+ */
117
+ touch(field) {
118
+ this.#touched.add(field);
119
+ }
120
+ /**
121
+ * Focuses the named input within the form.
122
+ */
123
+ focus(field) {
124
+ if (DEV && !this.#form) {
125
+ console.warn('FormState: focus() called before form() attachment is active.');
126
+ return;
127
+ }
128
+ const el = this.#form?.querySelector(`[name="${field}"]`);
129
+ if (DEV && !el) {
130
+ console.warn(`FormState: no element found with name="${field}". Check for typos in the name attribute or form_state.focus() call.`);
131
+ return;
132
+ }
133
+ el?.focus({ focusVisible: true });
134
+ }
135
+ /**
136
+ * Marks the form as having been submitted, causing all field errors to show.
137
+ */
138
+ attempt() {
139
+ this.#attempted = true;
140
+ }
141
+ /**
142
+ * Resets all touched and attempted state.
143
+ */
144
+ reset() {
145
+ this.#touched.clear();
146
+ this.#attempted = false;
147
+ }
148
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
7
- "logo_alt": "a friendly pixelated spider facing you",
7
+ "logo_alt": "a friendly teal spider facing you",
8
8
  "license": "MIT",
9
9
  "homepage": "https://app.fuz.dev/",
10
10
  "repository": "https://github.com/fuzdev/fuz_app",
@@ -45,7 +45,7 @@
45
45
  "@fuzdev/blake3_wasm": "^0.1.0",
46
46
  "@fuzdev/fuz_code": "^0.45.1",
47
47
  "@fuzdev/fuz_css": "^0.58.0",
48
- "@fuzdev/fuz_ui": "^0.191.1",
48
+ "@fuzdev/fuz_ui": "^0.191.2",
49
49
  "@fuzdev/fuz_util": "^0.55.0",
50
50
  "@fuzdev/gro": "^0.197.2",
51
51
  "@jridgewell/trace-mapping": "^0.3.31",
@@ -1,13 +0,0 @@
1
- /**
2
- * Svelte action that makes Enter advance to the next focusable form element
3
- * (inputs and buttons), or activate the element if it's already the last one.
4
- *
5
- * @example
6
- * ```svelte
7
- * <form {@attach enter_advance()}>
8
- * ```
9
- *
10
- * @module
11
- */
12
- export declare const enter_advance: () => ((form: HTMLFormElement) => () => void);
13
- //# sourceMappingURL=enter_advance.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"enter_advance.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/enter_advance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,eAAO,MAAM,aAAa,QAAO,CAAC,CAAC,IAAI,EAAE,eAAe,KAAK,MAAM,IAAI,CAiBtE,CAAC"}
@@ -1,30 +0,0 @@
1
- /**
2
- * Svelte action that makes Enter advance to the next focusable form element
3
- * (inputs and buttons), or activate the element if it's already the last one.
4
- *
5
- * @example
6
- * ```svelte
7
- * <form {@attach enter_advance()}>
8
- * ```
9
- *
10
- * @module
11
- */
12
- const FOCUSABLE_SELECTOR = 'input:not(:disabled), button:not(:disabled)';
13
- export const enter_advance = () => {
14
- return (form) => {
15
- const handle_keydown = (e) => {
16
- if (e.key !== 'Enter')
17
- return;
18
- if (!(e.target instanceof HTMLInputElement))
19
- return;
20
- const elements = Array.from(form.querySelectorAll(FOCUSABLE_SELECTOR));
21
- const index = elements.indexOf(e.target);
22
- if (index < 0)
23
- return;
24
- e.preventDefault();
25
- elements[(index + 1) % elements.length].focus();
26
- };
27
- form.addEventListener('keydown', handle_keydown);
28
- return () => form.removeEventListener('keydown', handle_keydown);
29
- };
30
- };