@vit-foundation/ui 0.1.0 → 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.
Files changed (56) hide show
  1. package/dist/components/account/AccountPanel.svelte +224 -0
  2. package/dist/components/account/AccountPanel.svelte.d.ts +37 -0
  3. package/dist/components/account/NewsletterSignup.svelte +91 -0
  4. package/dist/components/account/NewsletterSignup.svelte.d.ts +13 -0
  5. package/dist/components/account/index.d.ts +3 -0
  6. package/dist/components/account/index.js +2 -0
  7. package/dist/components/auth/AuthPageShell.svelte +108 -0
  8. package/dist/components/auth/AuthPageShell.svelte.d.ts +27 -0
  9. package/dist/components/auth/GoogleAuthForm.svelte +72 -0
  10. package/dist/components/auth/GoogleAuthForm.svelte.d.ts +26 -0
  11. package/dist/components/auth/LoginForm.svelte +176 -0
  12. package/dist/components/auth/LoginForm.svelte.d.ts +37 -0
  13. package/dist/components/auth/SignupForm.svelte +157 -0
  14. package/dist/components/auth/SignupForm.svelte.d.ts +27 -0
  15. package/dist/components/auth/index.d.ts +7 -0
  16. package/dist/components/auth/index.js +4 -0
  17. package/dist/components/contact/ContactForm.svelte +148 -0
  18. package/dist/components/contact/ContactForm.svelte.d.ts +27 -0
  19. package/dist/components/contact/index.d.ts +2 -0
  20. package/dist/components/contact/index.js +1 -0
  21. package/dist/components/ui/Honeypot.svelte +44 -0
  22. package/dist/components/ui/Honeypot.svelte.d.ts +24 -0
  23. package/dist/components/ui/LocaleField.svelte +21 -0
  24. package/dist/components/ui/LocaleField.svelte.d.ts +16 -0
  25. package/dist/components/ui/NewsletterIntentField.svelte +31 -0
  26. package/dist/components/ui/NewsletterIntentField.svelte.d.ts +28 -0
  27. package/dist/components/ui/index.d.ts +3 -0
  28. package/dist/components/ui/index.js +3 -0
  29. package/dist/components/weeklies/CommentSection.svelte +229 -0
  30. package/dist/components/weeklies/CommentSection.svelte.d.ts +30 -0
  31. package/dist/components/weeklies/ReactionBar.svelte +170 -0
  32. package/dist/components/weeklies/ReactionBar.svelte.d.ts +31 -0
  33. package/dist/components/weeklies/index.d.ts +4 -0
  34. package/dist/components/weeklies/index.js +2 -0
  35. package/dist/config/messages.js +68 -0
  36. package/dist/config/types.d.ts +70 -0
  37. package/dist/content/index.d.ts +2 -1
  38. package/dist/content/index.js +1 -0
  39. package/dist/content/types.d.ts +34 -0
  40. package/dist/content/types.js +4 -1
  41. package/dist/forms/constraints.d.ts +35 -0
  42. package/dist/forms/constraints.js +19 -0
  43. package/dist/forms/index.d.ts +3 -0
  44. package/dist/forms/index.js +2 -0
  45. package/dist/forms/transport.d.ts +26 -0
  46. package/dist/forms/transport.js +32 -0
  47. package/dist/forms/types.d.ts +54 -0
  48. package/dist/forms/types.js +13 -0
  49. package/dist/index.d.ts +5 -0
  50. package/dist/index.js +6 -0
  51. package/dist/styles/base.css +41 -0
  52. package/dist/testing/remote-form.d.ts +45 -0
  53. package/dist/testing/remote-form.js +85 -0
  54. package/dist/utils/contact.d.ts +4 -0
  55. package/dist/utils/contact.js +15 -0
  56. package/package.json +28 -4
@@ -0,0 +1,176 @@
1
+ <script module lang="ts">
2
+ import type { FormResultOf, RemoteField, RemoteFormInstance } from '../../forms/types.js';
3
+
4
+ /** The preflighted password-login form the host page passes in. */
5
+ export type LoginFormInstance = RemoteFormInstance<
6
+ { email: RemoteField; password: RemoteField; newsletter: RemoteField; locale: RemoteField },
7
+ FormResultOf<object, 'invalidCredentials'>
8
+ >;
9
+
10
+ /** The preflighted magic-link form. */
11
+ export type MagicLinkFormInstance = RemoteFormInstance<{
12
+ email: RemoteField;
13
+ locale: RemoteField;
14
+ website: RemoteField;
15
+ }>;
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ import { getUiConfig } from '../../config/context.js';
20
+ import { EMAIL, LOGIN_PASSWORD } from '../../forms/constraints.js';
21
+ import Button from '../ui/Button.svelte';
22
+ import Field from '../ui/Field.svelte';
23
+ import FormErrorFeedback from '../ui/FormErrorFeedback.svelte';
24
+ import FormFeedback from '../ui/FormFeedback.svelte';
25
+ import Honeypot from '../ui/Honeypot.svelte';
26
+ import LocaleField from '../ui/LocaleField.svelte';
27
+ import NewsletterIntentField from '../ui/NewsletterIntentField.svelte';
28
+
29
+ interface Props {
30
+ /**
31
+ * One finished sentence for a bounced auth landing, or null when the
32
+ * visitor arrived normally. The host app owns the closed set of landing
33
+ * failures and the copy for each; this component only renders the one
34
+ * that applies.
35
+ */
36
+ redirectErrorMessage?: string | null;
37
+ /** True when arriving from the newsletter band (?newsletter=1). */
38
+ newsletterIntent?: boolean;
39
+ /**
40
+ * The remote forms, already preflighted against the host app's schemas
41
+ * — validation belongs to the app's server contract, so the call site
42
+ * runs `form.preflight(schema)` and hands the result in. Progressive
43
+ * enhancement is unchanged: without JavaScript each form posts natively
44
+ * and the page re-renders with the result.
45
+ */
46
+ loginForm: LoginFormInstance;
47
+ magicLinkForm: MagicLinkFormInstance;
48
+ }
49
+
50
+ let {
51
+ redirectErrorMessage = null,
52
+ newsletterIntent = false,
53
+ loginForm,
54
+ magicLinkForm
55
+ }: Props = $props();
56
+
57
+ const config = getUiConfig();
58
+ const msg = $derived(config.messages);
59
+ const login = $derived(loginForm);
60
+ const magic = $derived(magicLinkForm);
61
+ </script>
62
+
63
+ <div class="login">
64
+ <form class="form-stack" {...login}>
65
+ <Field
66
+ id="login-email"
67
+ label={msg.login_emailLabel()}
68
+ field={login.fields.email}
69
+ constraint={EMAIL}
70
+ >
71
+ {#snippet children(attrs)}
72
+ <input
73
+ class="control"
74
+ required
75
+ autocomplete="email"
76
+ {...attrs}
77
+ {...login.fields.email.as('email')}
78
+ />
79
+ {/snippet}
80
+ </Field>
81
+
82
+ <Field
83
+ id="login-password"
84
+ label={msg.login_passwordLabel()}
85
+ field={login.fields.password}
86
+ constraint={LOGIN_PASSWORD}
87
+ >
88
+ {#snippet children(attrs)}
89
+ <input
90
+ class="control"
91
+ required
92
+ autocomplete="current-password"
93
+ {...attrs}
94
+ {...login.fields.password.as('password')}
95
+ />
96
+ {/snippet}
97
+ </Field>
98
+
99
+ {#if newsletterIntent}
100
+ <NewsletterIntentField field={login.fields.newsletter} />
101
+ {/if}
102
+ <LocaleField field={login.fields.locale} />
103
+
104
+ <div class="actions">
105
+ <Button type="submit" pending={login.pending}>{msg.login_submit()}</Button>
106
+ </div>
107
+
108
+ {#if login.result && !login.result.ok}
109
+ <FormErrorFeedback
110
+ result={login.result}
111
+ messages={{ invalidCredentials: msg.login_error_invalidCredentials() }}
112
+ />
113
+ {:else if redirectErrorMessage}
114
+ <!-- One sentence per landing, mapped by the host app off its closed
115
+ set of landing failures — a new code fails the build there, not in
116
+ an else arm here. Neither sentence reveals whether an address has
117
+ an account: both are reachable only with a token in hand and
118
+ describe the token's fate, not the address's. -->
119
+ <FormFeedback kind="error">{redirectErrorMessage}</FormFeedback>
120
+ {/if}
121
+ </form>
122
+
123
+ <p class="divider" role="separator">{msg.login_divider()}</p>
124
+
125
+ {#if magic.result?.ok}
126
+ <FormFeedback kind="success">{msg.login_magicLinkSuccess()}</FormFeedback>
127
+ {:else}
128
+ <form class="form-stack" {...magic}>
129
+ <Field
130
+ id="magic-email"
131
+ label={msg.login_emailLabel()}
132
+ field={magic.fields.email}
133
+ constraint={EMAIL}
134
+ >
135
+ {#snippet children(attrs)}
136
+ <input
137
+ class="control"
138
+ required
139
+ autocomplete="email"
140
+ {...attrs}
141
+ {...magic.fields.email.as('email')}
142
+ />
143
+ {/snippet}
144
+ </Field>
145
+
146
+ <!-- Localizes the /auth/confirm landing via the roundtrip cookie. -->
147
+ <LocaleField field={magic.fields.locale} />
148
+
149
+ <!-- Honeypot: invisible to humans, tempting to bots. This form mails an
150
+ attacker-chosen address unauthenticated, and a tripped trap returns
151
+ the same value a real request does — so the success branch above
152
+ renders identically either way. The password form above carries no
153
+ trap: its success is a redirect, so there is no value to fake. -->
154
+ <Honeypot form={magic.fields} id="magic-website" />
155
+
156
+ <div class="actions">
157
+ <Button type="submit" pending={magic.pending}>{msg.login_magicLinkSubmit()}</Button>
158
+ </div>
159
+
160
+ <FormErrorFeedback result={magic.result} />
161
+ </form>
162
+ {/if}
163
+ </div>
164
+
165
+ <style>
166
+ .login {
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: var(--space-3);
170
+ max-width: 24rem;
171
+ }
172
+
173
+ input {
174
+ padding: var(--space-2);
175
+ }
176
+ </style>
@@ -0,0 +1,37 @@
1
+ import type { FormResultOf, RemoteField, RemoteFormInstance } from '../../forms/types.js';
2
+ /** The preflighted password-login form the host page passes in. */
3
+ export type LoginFormInstance = RemoteFormInstance<{
4
+ email: RemoteField;
5
+ password: RemoteField;
6
+ newsletter: RemoteField;
7
+ locale: RemoteField;
8
+ }, FormResultOf<object, 'invalidCredentials'>>;
9
+ /** The preflighted magic-link form. */
10
+ export type MagicLinkFormInstance = RemoteFormInstance<{
11
+ email: RemoteField;
12
+ locale: RemoteField;
13
+ website: RemoteField;
14
+ }>;
15
+ interface Props {
16
+ /**
17
+ * One finished sentence for a bounced auth landing, or null when the
18
+ * visitor arrived normally. The host app owns the closed set of landing
19
+ * failures and the copy for each; this component only renders the one
20
+ * that applies.
21
+ */
22
+ redirectErrorMessage?: string | null;
23
+ /** True when arriving from the newsletter band (?newsletter=1). */
24
+ newsletterIntent?: boolean;
25
+ /**
26
+ * The remote forms, already preflighted against the host app's schemas
27
+ * — validation belongs to the app's server contract, so the call site
28
+ * runs `form.preflight(schema)` and hands the result in. Progressive
29
+ * enhancement is unchanged: without JavaScript each form posts natively
30
+ * and the page re-renders with the result.
31
+ */
32
+ loginForm: LoginFormInstance;
33
+ magicLinkForm: MagicLinkFormInstance;
34
+ }
35
+ declare const LoginForm: import("svelte").Component<Props, {}, "">;
36
+ type LoginForm = ReturnType<typeof LoginForm>;
37
+ export default LoginForm;
@@ -0,0 +1,157 @@
1
+ <script module lang="ts">
2
+ import type { RemoteField, RemoteFormInstance } from '../../forms/types.js';
3
+
4
+ /** The preflighted signup form the host page passes in. */
5
+ export type SignupFormInstance = RemoteFormInstance<{
6
+ displayName: RemoteField;
7
+ email: RemoteField;
8
+ password: RemoteField;
9
+ terms: RemoteField;
10
+ newsletter: RemoteField;
11
+ locale: RemoteField;
12
+ website: RemoteField;
13
+ }>;
14
+ </script>
15
+
16
+ <script lang="ts">
17
+ import { getUiConfig } from '../../config/context.js';
18
+ import { DISPLAY_NAME, EMAIL, PASSWORD } from '../../forms/constraints.js';
19
+ import Button from '../ui/Button.svelte';
20
+ import Field from '../ui/Field.svelte';
21
+ import FormErrorFeedback from '../ui/FormErrorFeedback.svelte';
22
+ import FormFeedback from '../ui/FormFeedback.svelte';
23
+ import Honeypot from '../ui/Honeypot.svelte';
24
+ import Link from '../ui/Link.svelte';
25
+ import LocaleField from '../ui/LocaleField.svelte';
26
+
27
+ interface Props {
28
+ /** True when arriving from the newsletter band (?newsletter=1). */
29
+ newsletterIntent?: boolean;
30
+ /**
31
+ * Callbacks for the single consent block's live state, so the page can
32
+ * mirror it into the Google flow — one set of checkboxes for both
33
+ * signup methods (callback props per the foundation's convention).
34
+ */
35
+ onTermsChange?: (accepted: boolean) => void;
36
+ onNewsletterChange?: (checked: boolean) => void;
37
+ /** The remote form, preflighted by the host page against its schema. */
38
+ signupForm: SignupFormInstance;
39
+ }
40
+
41
+ let { newsletterIntent = false, onTermsChange, onNewsletterChange, signupForm }: Props = $props();
42
+
43
+ const config = getUiConfig();
44
+ const msg = $derived(config.messages);
45
+ const f = $derived(signupForm);
46
+ </script>
47
+
48
+ {#if f.result?.ok}
49
+ <FormFeedback kind="success">{msg.signup_success()}</FormFeedback>
50
+ {:else}
51
+ <form class="form-stack" {...f}>
52
+ <Field
53
+ id="signup-name"
54
+ label={msg.signup_nameLabel()}
55
+ field={f.fields.displayName}
56
+ constraint={DISPLAY_NAME}
57
+ >
58
+ {#snippet children(attrs)}
59
+ <input
60
+ class="control"
61
+ required
62
+ autocomplete="nickname"
63
+ {...attrs}
64
+ {...f.fields.displayName.as('text')}
65
+ />
66
+ {/snippet}
67
+ </Field>
68
+
69
+ <Field
70
+ id="signup-email"
71
+ label={msg.login_emailLabel()}
72
+ field={f.fields.email}
73
+ constraint={EMAIL}
74
+ >
75
+ {#snippet children(attrs)}
76
+ <input
77
+ class="control"
78
+ required
79
+ autocomplete="email"
80
+ {...attrs}
81
+ {...f.fields.email.as('email')}
82
+ />
83
+ {/snippet}
84
+ </Field>
85
+
86
+ <Field
87
+ id="signup-password"
88
+ label={msg.signup_passwordLabel()}
89
+ field={f.fields.password}
90
+ constraint={PASSWORD}
91
+ >
92
+ {#snippet children(attrs)}
93
+ <input
94
+ class="control"
95
+ required
96
+ autocomplete="new-password"
97
+ {...attrs}
98
+ {...f.fields.password.as('password')}
99
+ />
100
+ {/snippet}
101
+ </Field>
102
+
103
+ <label class="checkbox">
104
+ <input
105
+ required
106
+ onchange={(event) => onTermsChange?.(event.currentTarget.checked)}
107
+ {...f.fields.terms.as('checkbox')}
108
+ />
109
+ <span>
110
+ {msg.signup_termsPre()}
111
+ <Link href="/legal">{msg.signup_termsLink()}</Link>
112
+ </span>
113
+ </label>
114
+
115
+ <label class="checkbox">
116
+ <input
117
+ onchange={(event) => onNewsletterChange?.(event.currentTarget.checked)}
118
+ {...f.fields.newsletter.as('checkbox', newsletterIntent)}
119
+ />
120
+ <span>{msg.signup_newsletterLabel()}</span>
121
+ </label>
122
+
123
+ <!-- Localizes the /auth/confirm landing via the roundtrip cookie. -->
124
+ <LocaleField field={f.fields.locale} />
125
+
126
+ <!-- Honeypot: invisible to humans, tempting to bots. -->
127
+ <Honeypot form={f.fields} id="signup-website" />
128
+
129
+ <div class="actions">
130
+ <Button type="submit" pending={f.pending}>{msg.signup_submit()}</Button>
131
+ </div>
132
+ <FormErrorFeedback result={f.result} />
133
+ </form>
134
+ {/if}
135
+
136
+ <style>
137
+ form {
138
+ max-width: 24rem;
139
+ }
140
+
141
+ input {
142
+ padding: var(--space-2);
143
+ }
144
+
145
+ .checkbox {
146
+ display: flex;
147
+ align-items: baseline;
148
+ gap: var(--space-2);
149
+ font-weight: 400;
150
+ font-size: var(--text-sm);
151
+ color: var(--color-ink-secondary);
152
+ }
153
+
154
+ .checkbox input {
155
+ padding: 0;
156
+ }
157
+ </style>
@@ -0,0 +1,27 @@
1
+ import type { RemoteField, RemoteFormInstance } from '../../forms/types.js';
2
+ /** The preflighted signup form the host page passes in. */
3
+ export type SignupFormInstance = RemoteFormInstance<{
4
+ displayName: RemoteField;
5
+ email: RemoteField;
6
+ password: RemoteField;
7
+ terms: RemoteField;
8
+ newsletter: RemoteField;
9
+ locale: RemoteField;
10
+ website: RemoteField;
11
+ }>;
12
+ interface Props {
13
+ /** True when arriving from the newsletter band (?newsletter=1). */
14
+ newsletterIntent?: boolean;
15
+ /**
16
+ * Callbacks for the single consent block's live state, so the page can
17
+ * mirror it into the Google flow — one set of checkboxes for both
18
+ * signup methods (callback props per the foundation's convention).
19
+ */
20
+ onTermsChange?: (accepted: boolean) => void;
21
+ onNewsletterChange?: (checked: boolean) => void;
22
+ /** The remote form, preflighted by the host page against its schema. */
23
+ signupForm: SignupFormInstance;
24
+ }
25
+ declare const SignupForm: import("svelte").Component<Props, {}, "">;
26
+ type SignupForm = ReturnType<typeof SignupForm>;
27
+ export default SignupForm;
@@ -0,0 +1,7 @@
1
+ export { default as AuthPageShell } from './AuthPageShell.svelte';
2
+ export { default as GoogleAuthForm } from './GoogleAuthForm.svelte';
3
+ export type { GoogleLoginFormInstance } from './GoogleAuthForm.svelte';
4
+ export { default as LoginForm } from './LoginForm.svelte';
5
+ export type { LoginFormInstance, MagicLinkFormInstance } from './LoginForm.svelte';
6
+ export { default as SignupForm } from './SignupForm.svelte';
7
+ export type { SignupFormInstance } from './SignupForm.svelte';
@@ -0,0 +1,4 @@
1
+ export { default as AuthPageShell } from './AuthPageShell.svelte';
2
+ export { default as GoogleAuthForm } from './GoogleAuthForm.svelte';
3
+ export { default as LoginForm } from './LoginForm.svelte';
4
+ export { default as SignupForm } from './SignupForm.svelte';
@@ -0,0 +1,148 @@
1
+ <script module lang="ts">
2
+ import type { FormResultOf, RemoteField, RemoteFormInstance } from '../../forms/types.js';
3
+
4
+ /** The preflighted contact form the host page passes in. */
5
+ export type ContactFormInstance = RemoteFormInstance<
6
+ {
7
+ category: RemoteField;
8
+ name: RemoteField;
9
+ email: RemoteField;
10
+ message: RemoteField;
11
+ locale: RemoteField;
12
+ website: RemoteField;
13
+ },
14
+ FormResultOf
15
+ > & {
16
+ fields: { allIssues(): Array<{ message: string }> | undefined };
17
+ };
18
+ </script>
19
+
20
+ <script lang="ts">
21
+ import { getUiConfig } from '../../config/context.js';
22
+ import { CONTACT_MESSAGE, CONTACT_NAME, EMAIL } from '../../forms/constraints.js';
23
+ import { CONTACT_CATEGORIES } from '../../content/types.js';
24
+ import { contactCategoryLabel } from '../../utils/contact.js';
25
+ import Button from '../ui/Button.svelte';
26
+ import Field from '../ui/Field.svelte';
27
+ import FormErrorFeedback from '../ui/FormErrorFeedback.svelte';
28
+ import FormFeedback from '../ui/FormFeedback.svelte';
29
+ import Honeypot from '../ui/Honeypot.svelte';
30
+ import LocaleField from '../ui/LocaleField.svelte';
31
+
32
+ interface Props {
33
+ /**
34
+ * The remote form, preflighted by the host page against its schema.
35
+ * Progressive enhancement is unchanged: without JavaScript the form
36
+ * posts natively and the page re-renders with the result.
37
+ */
38
+ form: ContactFormInstance;
39
+ }
40
+
41
+ let { form }: Props = $props();
42
+
43
+ const config = getUiConfig();
44
+ const msg = $derived(config.messages);
45
+ const f = $derived(form);
46
+
47
+ // Derived, not restated: the package owns which categories exist and in
48
+ // what order (the host schema derives its enum from CONTACT_CATEGORIES),
49
+ // and the messages own the copy for each.
50
+ const categories = CONTACT_CATEGORIES;
51
+ </script>
52
+
53
+ {#if f.result?.ok}
54
+ <FormFeedback kind="success">{msg.contact_success()}</FormFeedback>
55
+ {:else}
56
+ <form class="form-stack" {...f}>
57
+ <Field
58
+ id="contact-category"
59
+ label={msg.contact_categoryLabel()}
60
+ field={f.fields.category}
61
+ constraint={null}
62
+ >
63
+ {#snippet children(attrs)}
64
+ <select class="control" {...attrs} {...f.fields.category.as('select')}>
65
+ {#each categories as category (category)}
66
+ <option value={category}>{contactCategoryLabel(category, msg)}</option>
67
+ {/each}
68
+ </select>
69
+ {/snippet}
70
+ </Field>
71
+
72
+ <Field
73
+ id="contact-name"
74
+ label={msg.contact_nameLabel()}
75
+ field={f.fields.name}
76
+ constraint={CONTACT_NAME}
77
+ >
78
+ {#snippet children(attrs)}
79
+ <input
80
+ class="control"
81
+ required
82
+ autocomplete="name"
83
+ {...attrs}
84
+ {...f.fields.name.as('text')}
85
+ />
86
+ {/snippet}
87
+ </Field>
88
+
89
+ <Field
90
+ id="contact-email"
91
+ label={msg.contact_emailLabel()}
92
+ field={f.fields.email}
93
+ constraint={EMAIL}
94
+ >
95
+ {#snippet children(attrs)}
96
+ <input
97
+ class="control"
98
+ required
99
+ autocomplete="email"
100
+ {...attrs}
101
+ {...f.fields.email.as('email')}
102
+ />
103
+ {/snippet}
104
+ </Field>
105
+
106
+ <Field
107
+ id="contact-message"
108
+ label={msg.contact_messageLabel()}
109
+ field={f.fields.message}
110
+ constraint={CONTACT_MESSAGE}
111
+ >
112
+ {#snippet children(attrs)}
113
+ <textarea class="control" required rows="6" {...attrs} {...f.fields.message.as('text')}
114
+ ></textarea>
115
+ {/snippet}
116
+ </Field>
117
+
118
+ <LocaleField field={f.fields.locale} />
119
+
120
+ <Honeypot form={f.fields} id="contact-website" />
121
+
122
+ <div class="actions">
123
+ <Button type="submit" pending={f.pending}>{msg.contact_submit()}</Button>
124
+ </div>
125
+
126
+ {#if f.result && !f.result.ok}
127
+ <FormErrorFeedback result={f.result} />
128
+ {:else if f.fields.allIssues()?.length}
129
+ <FormFeedback kind="error">{msg.contact_invalid()}</FormFeedback>
130
+ {/if}
131
+ </form>
132
+ {/if}
133
+
134
+ <style>
135
+ form {
136
+ max-width: 32rem;
137
+ }
138
+
139
+ input,
140
+ select,
141
+ textarea {
142
+ padding: var(--space-2);
143
+ }
144
+
145
+ textarea {
146
+ resize: vertical;
147
+ }
148
+ </style>
@@ -0,0 +1,27 @@
1
+ import type { FormResultOf, RemoteField, RemoteFormInstance } from '../../forms/types.js';
2
+ /** The preflighted contact form the host page passes in. */
3
+ export type ContactFormInstance = RemoteFormInstance<{
4
+ category: RemoteField;
5
+ name: RemoteField;
6
+ email: RemoteField;
7
+ message: RemoteField;
8
+ locale: RemoteField;
9
+ website: RemoteField;
10
+ }, FormResultOf> & {
11
+ fields: {
12
+ allIssues(): Array<{
13
+ message: string;
14
+ }> | undefined;
15
+ };
16
+ };
17
+ interface Props {
18
+ /**
19
+ * The remote form, preflighted by the host page against its schema.
20
+ * Progressive enhancement is unchanged: without JavaScript the form
21
+ * posts natively and the page re-renders with the result.
22
+ */
23
+ form: ContactFormInstance;
24
+ }
25
+ declare const ContactForm: import("svelte").Component<Props, {}, "">;
26
+ type ContactForm = ReturnType<typeof ContactForm>;
27
+ export default ContactForm;
@@ -0,0 +1,2 @@
1
+ export { default as ContactForm } from './ContactForm.svelte';
2
+ export type { ContactFormInstance } from './ContactForm.svelte';
@@ -0,0 +1 @@
1
+ export { default as ContactForm } from './ContactForm.svelte';
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import { HONEYPOT_FIELD } from '../../forms/transport.js';
3
+
4
+ /**
5
+ * Spam trap for the public forms: invisible to humans, tempting to bots.
6
+ * The server half is HONEYPOT_FIELD in ../../forms/transport.js, which the
7
+ * host app's form envelope checks before running any handler.
8
+ *
9
+ * This takes the form and reads that field itself. Taking the field meant
10
+ * both callers named it — `f.fields.website` — so the two halves were
11
+ * joined by a string typed twice rather than by the constant, and nothing
12
+ * outside the schemas and the envelope imported it. A trap whose markup
13
+ * name drifts from the schema's is a trap that silently stops catching.
14
+ */
15
+ interface HoneypotField {
16
+ as(type: 'text'): Record<string, unknown>;
17
+ }
18
+
19
+ interface Props {
20
+ /** The remote form; its honeypot field is read by name from the schema. */
21
+ form: Record<typeof HONEYPOT_FIELD, HoneypotField>;
22
+ /** DOM id, unique per page — two honeypot forms can share one. */
23
+ id: string;
24
+ }
25
+
26
+ let { form, id }: Props = $props();
27
+
28
+ const field = $derived(form[HONEYPOT_FIELD]);
29
+ </script>
30
+
31
+ <div class="hp" aria-hidden="true">
32
+ <label for={id}>Website</label>
33
+ <input {id} tabindex="-1" autocomplete="off" {...field.as('text')} />
34
+ </div>
35
+
36
+ <style>
37
+ .hp {
38
+ position: absolute;
39
+ left: -9999px;
40
+ width: 1px;
41
+ height: 1px;
42
+ overflow: hidden;
43
+ }
44
+ </style>
@@ -0,0 +1,24 @@
1
+ import { HONEYPOT_FIELD } from '../../forms/transport.js';
2
+ /**
3
+ * Spam trap for the public forms: invisible to humans, tempting to bots.
4
+ * The server half is HONEYPOT_FIELD in ../../forms/transport.js, which the
5
+ * host app's form envelope checks before running any handler.
6
+ *
7
+ * This takes the form and reads that field itself. Taking the field meant
8
+ * both callers named it — `f.fields.website` — so the two halves were
9
+ * joined by a string typed twice rather than by the constant, and nothing
10
+ * outside the schemas and the envelope imported it. A trap whose markup
11
+ * name drifts from the schema's is a trap that silently stops catching.
12
+ */
13
+ interface HoneypotField {
14
+ as(type: 'text'): Record<string, unknown>;
15
+ }
16
+ interface Props {
17
+ /** The remote form; its honeypot field is read by name from the schema. */
18
+ form: Record<typeof HONEYPOT_FIELD, HoneypotField>;
19
+ /** DOM id, unique per page — two honeypot forms can share one. */
20
+ id: string;
21
+ }
22
+ declare const Honeypot: import("svelte").Component<Props, {}, "">;
23
+ type Honeypot = ReturnType<typeof Honeypot>;
24
+ export default Honeypot;
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import { getUiConfig } from '../../config/context.js';
3
+
4
+ /**
5
+ * The hidden `locale` input every form carries. Without it a no-JS
6
+ * submission is stored under the base locale, because the unprefixed POST
7
+ * target resolves there before the locale cookie is consulted. Rendering it
8
+ * from one module makes the invariant enforceable instead of restated at
9
+ * every form.
10
+ */
11
+ interface Props {
12
+ /** The form's `locale` field: `form.fields.locale`. */
13
+ field: { as(type: 'hidden', value: string): Record<string, unknown> };
14
+ }
15
+
16
+ let { field }: Props = $props();
17
+
18
+ const config = getUiConfig();
19
+ </script>
20
+
21
+ <input {...field.as('hidden', config.locale())} />