@vit-foundation/ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/account/AccountPanel.svelte +224 -0
- package/dist/components/account/AccountPanel.svelte.d.ts +35 -0
- package/dist/components/account/NewsletterSignup.svelte +91 -0
- package/dist/components/account/NewsletterSignup.svelte.d.ts +13 -0
- package/dist/components/account/index.d.ts +3 -0
- package/dist/components/account/index.js +2 -0
- package/dist/components/auth/AuthPageShell.svelte +108 -0
- package/dist/components/auth/AuthPageShell.svelte.d.ts +27 -0
- package/dist/components/auth/GoogleAuthForm.svelte +72 -0
- package/dist/components/auth/GoogleAuthForm.svelte.d.ts +26 -0
- package/dist/components/auth/LoginForm.svelte +176 -0
- package/dist/components/auth/LoginForm.svelte.d.ts +37 -0
- package/dist/components/auth/SignupForm.svelte +157 -0
- package/dist/components/auth/SignupForm.svelte.d.ts +27 -0
- package/dist/components/auth/index.d.ts +7 -0
- package/dist/components/auth/index.js +4 -0
- package/dist/components/contact/ContactForm.svelte +148 -0
- package/dist/components/contact/ContactForm.svelte.d.ts +27 -0
- package/dist/components/contact/index.d.ts +2 -0
- package/dist/components/contact/index.js +1 -0
- package/dist/components/ui/Honeypot.svelte +44 -0
- package/dist/components/ui/Honeypot.svelte.d.ts +24 -0
- package/dist/components/ui/LocaleField.svelte +21 -0
- package/dist/components/ui/LocaleField.svelte.d.ts +16 -0
- package/dist/components/ui/NewsletterIntentField.svelte +31 -0
- package/dist/components/ui/NewsletterIntentField.svelte.d.ts +28 -0
- package/dist/components/ui/index.d.ts +3 -0
- package/dist/components/ui/index.js +3 -0
- package/dist/components/weeklies/CommentSection.svelte +229 -0
- package/dist/components/weeklies/CommentSection.svelte.d.ts +31 -0
- package/dist/components/weeklies/ReactionBar.svelte +170 -0
- package/dist/components/weeklies/ReactionBar.svelte.d.ts +31 -0
- package/dist/components/weeklies/index.d.ts +4 -0
- package/dist/components/weeklies/index.js +2 -0
- package/dist/config/messages.js +68 -0
- package/dist/config/types.d.ts +70 -0
- package/dist/content/index.d.ts +2 -1
- package/dist/content/index.js +1 -0
- package/dist/content/types.d.ts +34 -0
- package/dist/content/types.js +4 -1
- package/dist/forms/constraints.d.ts +35 -0
- package/dist/forms/constraints.js +19 -0
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.js +2 -0
- package/dist/forms/transport.d.ts +26 -0
- package/dist/forms/transport.js +32 -0
- package/dist/forms/types.d.ts +54 -0
- package/dist/forms/types.js +13 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/styles/base.css +41 -0
- package/dist/testing/remote-form.d.ts +45 -0
- package/dist/testing/remote-form.js +85 -0
- package/dist/utils/contact.d.ts +4 -0
- package/dist/utils/contact.js +15 -0
- package/package.json +27 -3
package/dist/config/types.d.ts
CHANGED
|
@@ -54,6 +54,76 @@ export interface UiMessages {
|
|
|
54
54
|
footer_rights(): string;
|
|
55
55
|
jobs_empty(): string;
|
|
56
56
|
jobs_newsletterNudge(): string;
|
|
57
|
+
login_title(): string;
|
|
58
|
+
login_emailLabel(): string;
|
|
59
|
+
login_passwordLabel(): string;
|
|
60
|
+
login_submit(): string;
|
|
61
|
+
login_divider(): string;
|
|
62
|
+
login_magicLinkSubmit(): string;
|
|
63
|
+
login_magicLinkSuccess(): string;
|
|
64
|
+
login_googleSubmit(): string;
|
|
65
|
+
login_error_invalidCredentials(): string;
|
|
66
|
+
signup_title(): string;
|
|
67
|
+
signup_nameLabel(): string;
|
|
68
|
+
signup_passwordLabel(): string;
|
|
69
|
+
signup_newsletterLabel(): string;
|
|
70
|
+
signup_submit(): string;
|
|
71
|
+
signup_success(): string;
|
|
72
|
+
signup_termsPre(): string;
|
|
73
|
+
signup_termsLink(): string;
|
|
74
|
+
auth_tabsLabel(): string;
|
|
75
|
+
auth_legalNotePre(): string;
|
|
76
|
+
account_nameHeading(): string;
|
|
77
|
+
account_nameLabel(): string;
|
|
78
|
+
account_nameSubmit(): string;
|
|
79
|
+
account_nameSuccess(): string;
|
|
80
|
+
account_newsletterHeading(): string;
|
|
81
|
+
account_newsletterStatusOn(params: {
|
|
82
|
+
email: string;
|
|
83
|
+
}): string;
|
|
84
|
+
account_newsletterStatusOff(): string;
|
|
85
|
+
account_newsletterSubscribe(): string;
|
|
86
|
+
account_newsletterUnsubscribe(): string;
|
|
87
|
+
account_newsletterSuccessOn(): string;
|
|
88
|
+
account_newsletterSuccessOff(): string;
|
|
89
|
+
account_dataHeading(): string;
|
|
90
|
+
account_dataDescription(): string;
|
|
91
|
+
account_dataDownload(): string;
|
|
92
|
+
account_logout(): string;
|
|
93
|
+
account_deleteHeading(): string;
|
|
94
|
+
account_deleteWarning(): string;
|
|
95
|
+
account_deleteConfirmLabel(): string;
|
|
96
|
+
account_deleteSubmit(): string;
|
|
97
|
+
account_error_unavailable(): string;
|
|
98
|
+
newsletter_title(): string;
|
|
99
|
+
newsletter_intro(): string;
|
|
100
|
+
newsletter_promptLoggedOut(): string;
|
|
101
|
+
newsletter_subscribedNote(): string;
|
|
102
|
+
comments_title(): string;
|
|
103
|
+
comments_empty(): string;
|
|
104
|
+
comments_bodyLabel(): string;
|
|
105
|
+
comments_submit(): string;
|
|
106
|
+
comments_replyLabel(): string;
|
|
107
|
+
comments_replySubmit(): string;
|
|
108
|
+
comments_loginPrompt(): string;
|
|
109
|
+
comments_loginLink(): string;
|
|
110
|
+
comments_signupLink(): string;
|
|
111
|
+
reaction_like(): string;
|
|
112
|
+
reaction_love(): string;
|
|
113
|
+
reaction_clap(): string;
|
|
114
|
+
reactions_groupLabel(): string;
|
|
115
|
+
contact_categoryLabel(): string;
|
|
116
|
+
contact_nameLabel(): string;
|
|
117
|
+
contact_emailLabel(): string;
|
|
118
|
+
contact_messageLabel(): string;
|
|
119
|
+
contact_submit(): string;
|
|
120
|
+
contact_success(): string;
|
|
121
|
+
contact_invalid(): string;
|
|
122
|
+
contact_category_collaborate(): string;
|
|
123
|
+
contact_category_event(): string;
|
|
124
|
+
contact_category_press(): string;
|
|
125
|
+
contact_category_brand(): string;
|
|
126
|
+
contact_category_other(): string;
|
|
57
127
|
category_foundation(): string;
|
|
58
128
|
category_lab(): string;
|
|
59
129
|
category_education(): string;
|
package/dist/content/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { renderBody } from './richtext.js';
|
|
2
2
|
export type { RichTextBlock } from './richtext.js';
|
|
3
|
-
export
|
|
3
|
+
export { CONTACT_CATEGORIES, REACTIONS } from './types.js';
|
|
4
|
+
export type { CollaboratorData, CommentData, CommentThreadData, ContactCategory, FieldConstraint, FormFailReason, JobOpeningData, MilestoneCategory, MilestoneData, ProjectCardData, ProjectKind, Reaction, ReactionSummary, ReactionTarget, SortDirection, TeamMemberData, WeeklyCardData } from './types.js';
|
package/dist/content/index.js
CHANGED
package/dist/content/types.d.ts
CHANGED
|
@@ -69,3 +69,37 @@ export interface JobOpeningData {
|
|
|
69
69
|
description: string | null;
|
|
70
70
|
postedOn: string;
|
|
71
71
|
}
|
|
72
|
+
/** The reactions a weekly or comment can carry, in display order. */
|
|
73
|
+
export declare const REACTIONS: readonly ["like", "love", "clap"];
|
|
74
|
+
export type Reaction = (typeof REACTIONS)[number];
|
|
75
|
+
/** One reaction's tally as the bar renders it. */
|
|
76
|
+
export interface ReactionSummary {
|
|
77
|
+
reaction: Reaction;
|
|
78
|
+
count: number;
|
|
79
|
+
/** Whether the signed-in reader is among the reactors. */
|
|
80
|
+
mine: boolean;
|
|
81
|
+
}
|
|
82
|
+
/** The weekly or comment a reaction bar belongs to. */
|
|
83
|
+
export type ReactionTarget = {
|
|
84
|
+
kind: 'weekly';
|
|
85
|
+
slug: string;
|
|
86
|
+
} | {
|
|
87
|
+
kind: 'comment';
|
|
88
|
+
id: number;
|
|
89
|
+
};
|
|
90
|
+
/** One comment as the section renders it. */
|
|
91
|
+
export interface CommentData {
|
|
92
|
+
id: number;
|
|
93
|
+
displayName: string;
|
|
94
|
+
body: string;
|
|
95
|
+
/** ISO timestamp. */
|
|
96
|
+
createdAt: string;
|
|
97
|
+
reactions: ReactionSummary[];
|
|
98
|
+
}
|
|
99
|
+
/** A top-level comment with its flat reply list. */
|
|
100
|
+
export interface CommentThreadData extends CommentData {
|
|
101
|
+
replies: CommentData[];
|
|
102
|
+
}
|
|
103
|
+
/** The contact form's reasons, in display order — the host schema derives its enum from this. */
|
|
104
|
+
export declare const CONTACT_CATEGORIES: readonly ["collaborate", "event", "press", "brand", "other"];
|
|
105
|
+
export type ContactCategory = (typeof CONTACT_CATEGORIES)[number];
|
package/dist/content/types.js
CHANGED
|
@@ -3,4 +3,7 @@
|
|
|
3
3
|
* applied. The website's zod-inferred types satisfy these structurally; the
|
|
4
4
|
* package states them itself so consumers owe it no schema library.
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
/** The reactions a weekly or comment can carry, in display order. */
|
|
7
|
+
export const REACTIONS = ['like', 'love', 'clap'];
|
|
8
|
+
/** The contact form's reasons, in display order — the host schema derives its enum from this. */
|
|
9
|
+
export const CONTACT_CATEGORIES = ['collaborate', 'event', 'press', 'brand', 'other'];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounds of the foundation's form fields — one declaration each, feeding
|
|
3
|
+
* both halves: the host app's schemas build their validations from these, and
|
|
4
|
+
* the form components spread them into the markup's length attributes through
|
|
5
|
+
* Field. A bound written in only one of the two places is enforced on the
|
|
6
|
+
* server and unstated in the browser, or vice versa.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DISPLAY_NAME: {
|
|
9
|
+
readonly min: 1;
|
|
10
|
+
readonly max: 100;
|
|
11
|
+
};
|
|
12
|
+
export declare const EMAIL: {
|
|
13
|
+
readonly max: 320;
|
|
14
|
+
};
|
|
15
|
+
export declare const PASSWORD: {
|
|
16
|
+
readonly min: 8;
|
|
17
|
+
readonly max: 200;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The login password's own constraint: the site's maximum, and no minimum.
|
|
21
|
+
* A `minlength` borrowed from PASSWORD would refuse a valid older password
|
|
22
|
+
* in the browser before the server ever saw it.
|
|
23
|
+
*/
|
|
24
|
+
export declare const LOGIN_PASSWORD: {
|
|
25
|
+
readonly max: 200;
|
|
26
|
+
};
|
|
27
|
+
export declare const COMMENT_BODY: {
|
|
28
|
+
readonly max: 2000;
|
|
29
|
+
};
|
|
30
|
+
export declare const CONTACT_NAME: {
|
|
31
|
+
readonly max: 200;
|
|
32
|
+
};
|
|
33
|
+
export declare const CONTACT_MESSAGE: {
|
|
34
|
+
readonly max: 5000;
|
|
35
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounds of the foundation's form fields — one declaration each, feeding
|
|
3
|
+
* both halves: the host app's schemas build their validations from these, and
|
|
4
|
+
* the form components spread them into the markup's length attributes through
|
|
5
|
+
* Field. A bound written in only one of the two places is enforced on the
|
|
6
|
+
* server and unstated in the browser, or vice versa.
|
|
7
|
+
*/
|
|
8
|
+
export const DISPLAY_NAME = { min: 1, max: 100 };
|
|
9
|
+
export const EMAIL = { max: 320 };
|
|
10
|
+
export const PASSWORD = { min: 8, max: 200 };
|
|
11
|
+
/**
|
|
12
|
+
* The login password's own constraint: the site's maximum, and no minimum.
|
|
13
|
+
* A `minlength` borrowed from PASSWORD would refuse a valid older password
|
|
14
|
+
* in the browser before the server ever saw it.
|
|
15
|
+
*/
|
|
16
|
+
export const LOGIN_PASSWORD = { max: PASSWORD.max };
|
|
17
|
+
export const COMMENT_BODY = { max: 2000 };
|
|
18
|
+
export const CONTACT_NAME = { max: 200 };
|
|
19
|
+
export const CONTACT_MESSAGE = { max: 5000 };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { COMMENT_BODY, CONTACT_MESSAGE, CONTACT_NAME, DISPLAY_NAME, EMAIL, LOGIN_PASSWORD, PASSWORD } from './constraints.js';
|
|
2
|
+
export { hasNewsletterIntent, HONEYPOT_FIELD, isNewsletterIntent, NEWSLETTER_INTENT_PARAM, NEWSLETTER_INTENT_VALUE, withNewsletterIntent } from './transport.js';
|
|
3
|
+
export type { FormFail, FormFieldIssue, FormResultLike, FormResultOf, KeyedRemoteForms, RemoteField, RemoteFormAttributes, RemoteFormInstance } from './types.js';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { COMMENT_BODY, CONTACT_MESSAGE, CONTACT_NAME, DISPLAY_NAME, EMAIL, LOGIN_PASSWORD, PASSWORD } from './constraints.js';
|
|
2
|
+
export { hasNewsletterIntent, HONEYPOT_FIELD, isNewsletterIntent, NEWSLETTER_INTENT_PARAM, NEWSLETTER_INTENT_VALUE, withNewsletterIntent } from './transport.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The form-transport constants shared between this package's markup and the
|
|
3
|
+
* host app's server envelope. The package owns them so the two halves are
|
|
4
|
+
* joined by an import rather than by a string typed twice: a hidden field
|
|
5
|
+
* whose name drifts from the value the server checks fails silently, which
|
|
6
|
+
* is the exact defect these modules exist to prevent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The honeypot field's name. Rendered hidden by Honeypot.svelte; a value in
|
|
10
|
+
* it means a bot. The host app's schemas declare the field under this name
|
|
11
|
+
* and its form envelope checks it before running any handler.
|
|
12
|
+
*/
|
|
13
|
+
export declare const HONEYPOT_FIELD = "website";
|
|
14
|
+
/**
|
|
15
|
+
* The newsletter intent as it travels in a URL or a hidden field. The value
|
|
16
|
+
* is what the host app's schema accepts (`z.literal(NEWSLETTER_INTENT_VALUE)`);
|
|
17
|
+
* the param is the query key the auth pages read.
|
|
18
|
+
*/
|
|
19
|
+
export declare const NEWSLETTER_INTENT_PARAM = "newsletter";
|
|
20
|
+
export declare const NEWSLETTER_INTENT_VALUE = "1";
|
|
21
|
+
/** True when a raw transport value — a URL param, a cookie — carries intent. */
|
|
22
|
+
export declare function isNewsletterIntent(value: string | null | undefined): boolean;
|
|
23
|
+
/** True when the URL arrived from a newsletter prompt (?newsletter=1). */
|
|
24
|
+
export declare function hasNewsletterIntent(url: URL): boolean;
|
|
25
|
+
/** An auth-page path carrying the intent: '/signup' → '/signup?newsletter=1'. */
|
|
26
|
+
export declare function withNewsletterIntent(path: string): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The form-transport constants shared between this package's markup and the
|
|
3
|
+
* host app's server envelope. The package owns them so the two halves are
|
|
4
|
+
* joined by an import rather than by a string typed twice: a hidden field
|
|
5
|
+
* whose name drifts from the value the server checks fails silently, which
|
|
6
|
+
* is the exact defect these modules exist to prevent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The honeypot field's name. Rendered hidden by Honeypot.svelte; a value in
|
|
10
|
+
* it means a bot. The host app's schemas declare the field under this name
|
|
11
|
+
* and its form envelope checks it before running any handler.
|
|
12
|
+
*/
|
|
13
|
+
export const HONEYPOT_FIELD = 'website';
|
|
14
|
+
/**
|
|
15
|
+
* The newsletter intent as it travels in a URL or a hidden field. The value
|
|
16
|
+
* is what the host app's schema accepts (`z.literal(NEWSLETTER_INTENT_VALUE)`);
|
|
17
|
+
* the param is the query key the auth pages read.
|
|
18
|
+
*/
|
|
19
|
+
export const NEWSLETTER_INTENT_PARAM = 'newsletter';
|
|
20
|
+
export const NEWSLETTER_INTENT_VALUE = '1';
|
|
21
|
+
/** True when a raw transport value — a URL param, a cookie — carries intent. */
|
|
22
|
+
export function isNewsletterIntent(value) {
|
|
23
|
+
return value === NEWSLETTER_INTENT_VALUE;
|
|
24
|
+
}
|
|
25
|
+
/** True when the URL arrived from a newsletter prompt (?newsletter=1). */
|
|
26
|
+
export function hasNewsletterIntent(url) {
|
|
27
|
+
return isNewsletterIntent(url.searchParams.get(NEWSLETTER_INTENT_PARAM));
|
|
28
|
+
}
|
|
29
|
+
/** An auth-page path carrying the intent: '/signup' → '/signup?newsletter=1'. */
|
|
30
|
+
export function withNewsletterIntent(path) {
|
|
31
|
+
return `${path}?${NEWSLETTER_INTENT_PARAM}=${NEWSLETTER_INTENT_VALUE}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structural slice of SvelteKit's RemoteForm surface the form components
|
|
3
|
+
* render with. Stated here rather than imported from @sveltejs/kit so the
|
|
4
|
+
* package's only peer stays svelte: any object with this shape works — kit's
|
|
5
|
+
* real remote forms, and the mock in ../testing/remote-form.js.
|
|
6
|
+
*
|
|
7
|
+
* The components take their form as a REQUIRED prop, already preflighted:
|
|
8
|
+
* validation schemas belong to the host app's server contract, so the call
|
|
9
|
+
* site runs `myForm.preflight(mySchema)` and hands the result in. Method
|
|
10
|
+
* syntax throughout — TypeScript checks method parameters bivariantly, which
|
|
11
|
+
* is what lets kit's narrower literal-typed signatures satisfy these.
|
|
12
|
+
*/
|
|
13
|
+
import type { FormFailReason } from '../content/types.js';
|
|
14
|
+
export interface FormFieldIssue {
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
/** One named field of a remote form, as the markup consumes it. */
|
|
18
|
+
export interface RemoteField {
|
|
19
|
+
/** Spreadable input attributes for the given control type. */
|
|
20
|
+
as(type: string, value?: string | boolean): Record<string, unknown>;
|
|
21
|
+
issues(): FormFieldIssue[] | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The spreadable half: `<form {...form}>` — kit enumerates exactly the form
|
|
25
|
+
* attributes plus its submit handler on the instance.
|
|
26
|
+
*/
|
|
27
|
+
export interface RemoteFormAttributes {
|
|
28
|
+
method: 'POST';
|
|
29
|
+
action: string;
|
|
30
|
+
}
|
|
31
|
+
/** A form instance with the given named fields. */
|
|
32
|
+
export type RemoteFormInstance<Fields extends Record<string, RemoteField>, Result = FormResultLike> = RemoteFormAttributes & {
|
|
33
|
+
fields: Fields;
|
|
34
|
+
pending: number;
|
|
35
|
+
result: Result | undefined;
|
|
36
|
+
};
|
|
37
|
+
/** The discriminated result the components branch on. */
|
|
38
|
+
export interface FormResultLike {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A failure carrying one of the shared envelope reasons, plus form-specific extras. */
|
|
43
|
+
export type FormFail<Extra extends string = never> = {
|
|
44
|
+
ok: false;
|
|
45
|
+
reason: FormFailReason | Extra;
|
|
46
|
+
};
|
|
47
|
+
/** The result shape of the foundation's form envelopes, structurally. */
|
|
48
|
+
export type FormResultOf<Ok = object, Extra extends string = never> = ({
|
|
49
|
+
ok: true;
|
|
50
|
+
} & Ok) | FormFail<Extra>;
|
|
51
|
+
/** A keyed-form factory: `.for(id)` yields one instance per key. */
|
|
52
|
+
export interface KeyedRemoteForms<Instance> {
|
|
53
|
+
for(key: string): Instance;
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structural slice of SvelteKit's RemoteForm surface the form components
|
|
3
|
+
* render with. Stated here rather than imported from @sveltejs/kit so the
|
|
4
|
+
* package's only peer stays svelte: any object with this shape works — kit's
|
|
5
|
+
* real remote forms, and the mock in ../testing/remote-form.js.
|
|
6
|
+
*
|
|
7
|
+
* The components take their form as a REQUIRED prop, already preflighted:
|
|
8
|
+
* validation schemas belong to the host app's server contract, so the call
|
|
9
|
+
* site runs `myForm.preflight(mySchema)` and hands the result in. Method
|
|
10
|
+
* syntax throughout — TypeScript checks method parameters bivariantly, which
|
|
11
|
+
* is what lets kit's narrower literal-typed signatures satisfy these.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export * from './components/account/index.js';
|
|
2
|
+
export * from './components/auth/index.js';
|
|
3
|
+
export * from './components/contact/index.js';
|
|
1
4
|
export * from './components/ui/index.js';
|
|
2
5
|
export * from './components/layout/index.js';
|
|
3
6
|
export * from './components/weeklies/index.js';
|
|
@@ -5,9 +8,11 @@ export * from './components/projects/index.js';
|
|
|
5
8
|
export * from './components/team/index.js';
|
|
6
9
|
export * from './components/timeline/index.js';
|
|
7
10
|
export * from './components/jobs/index.js';
|
|
11
|
+
export * from './forms/index.js';
|
|
8
12
|
export * from './config/index.js';
|
|
9
13
|
export * from './edit/index.js';
|
|
10
14
|
export * from './content/index.js';
|
|
11
15
|
export { formatDate, yearOf } from './utils/dates.js';
|
|
16
|
+
export { contactCategoryLabel } from './utils/contact.js';
|
|
12
17
|
export { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from './utils/milestones.js';
|
|
13
18
|
export { isPathUnder } from './utils/paths.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// Components, by group.
|
|
2
|
+
export * from './components/account/index.js';
|
|
3
|
+
export * from './components/auth/index.js';
|
|
4
|
+
export * from './components/contact/index.js';
|
|
2
5
|
export * from './components/ui/index.js';
|
|
3
6
|
export * from './components/layout/index.js';
|
|
4
7
|
export * from './components/weeklies/index.js';
|
|
@@ -6,6 +9,8 @@ export * from './components/projects/index.js';
|
|
|
6
9
|
export * from './components/team/index.js';
|
|
7
10
|
export * from './components/timeline/index.js';
|
|
8
11
|
export * from './components/jobs/index.js';
|
|
12
|
+
// The form seam: structural remote-form types, transport constants, field bounds.
|
|
13
|
+
export * from './forms/index.js';
|
|
9
14
|
// App wiring: configuration context and the edit-mode contract.
|
|
10
15
|
export * from './config/index.js';
|
|
11
16
|
export * from './edit/index.js';
|
|
@@ -13,5 +18,6 @@ export * from './edit/index.js';
|
|
|
13
18
|
export * from './content/index.js';
|
|
14
19
|
// Utilities the components share with their host apps.
|
|
15
20
|
export { formatDate, yearOf } from './utils/dates.js';
|
|
21
|
+
export { contactCategoryLabel } from './utils/contact.js';
|
|
16
22
|
export { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from './utils/milestones.js';
|
|
17
23
|
export { isPathUnder } from './utils/paths.js';
|
package/dist/styles/base.css
CHANGED
|
@@ -44,6 +44,47 @@
|
|
|
44
44
|
background: var(--color-surface);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/* Vertical form: fields stacked with the standard rhythm. Forms that lay out
|
|
48
|
+
* as a row or as contents don't take it. */
|
|
49
|
+
.form-stack {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
gap: var(--space-3);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Submit row. Load-bearing, not decorative: .form-stack stretches its direct
|
|
56
|
+
* children, so a bare button would fill the column. */
|
|
57
|
+
.actions {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-wrap: wrap;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: var(--space-2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Labelled rule between alternatives ("o"), e.g. between login methods. */
|
|
65
|
+
.divider {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: var(--space-2);
|
|
69
|
+
color: var(--color-ink-muted);
|
|
70
|
+
font-size: var(--text-sm);
|
|
71
|
+
margin: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.divider::before,
|
|
75
|
+
.divider::after {
|
|
76
|
+
content: '';
|
|
77
|
+
flex: 1;
|
|
78
|
+
border-top: 1px solid var(--color-hairline);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* The h2 that opens a band within a section — declared globally because
|
|
82
|
+
* scoped styles cannot cross a module edge. Spacing stays with the module. */
|
|
83
|
+
.subsection-heading {
|
|
84
|
+
font-size: var(--text-lg);
|
|
85
|
+
margin-block: var(--space-4) var(--space-2);
|
|
86
|
+
}
|
|
87
|
+
|
|
47
88
|
/* Edit-mode affordance. Global on purpose: the editable element is rendered
|
|
48
89
|
* by the consuming component's own markup (Editable hands it attributes
|
|
49
90
|
* through a snippet), which scoped styles inside Editable can never reach. */
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test/Storybook stand-in for a SvelteKit RemoteForm. Components accept their
|
|
3
|
+
* form as an optional prop defaulting to the real one, so tests and stories
|
|
4
|
+
* substitute through the component's own interface — no module interception.
|
|
5
|
+
* It fakes exactly the surface components render with: the spreadable form
|
|
6
|
+
* attributes, fields.<name>.as()/issues(), fields.allIssues(), result,
|
|
7
|
+
* pending, and the preflight/enhance/for chain. One module — one place to
|
|
8
|
+
* audit when a kit upgrade changes the RemoteForm contract.
|
|
9
|
+
*
|
|
10
|
+
* The type parameter is the prop's type, so a call site reads
|
|
11
|
+
* `createRemoteFormMock<Props['form']>()` and carries no cast of its own.
|
|
12
|
+
*
|
|
13
|
+
* `for(id)` returns a distinct instance per key, and `preflight()` is recorded
|
|
14
|
+
* on the instance it was called on. Both used to return the mock itself, which
|
|
15
|
+
* made a keyed form and its base indistinguishable *by construction*: whether
|
|
16
|
+
* a reply form carried browser-side validation was a question no component
|
|
17
|
+
* test could ask. `preflighted` is the only non-kit member and is test-only.
|
|
18
|
+
*/
|
|
19
|
+
export type FieldIssues = Array<{
|
|
20
|
+
message: string;
|
|
21
|
+
}> | undefined;
|
|
22
|
+
export interface RemoteFormMockOptions {
|
|
23
|
+
/** Live result accessor — a function so tests can change it between renders. */
|
|
24
|
+
result?: () => unknown;
|
|
25
|
+
/** Live per-field validation issues, keyed by field name. */
|
|
26
|
+
issues?: () => Record<string, FieldIssues>;
|
|
27
|
+
/**
|
|
28
|
+
* Live submitting count. A function for the reason `result` is one: a test
|
|
29
|
+
* that renders a control mid-flight needs to move it between renders.
|
|
30
|
+
*
|
|
31
|
+
* Pinned at 0 until `Button` and the reaction chips started reading it —
|
|
32
|
+
* before that no component rendered `pending`, so the mock not offering it
|
|
33
|
+
* cost nothing. Now the disabled and aria-busy states depend on it, and a
|
|
34
|
+
* surface a component renders is exactly what this module exists to fake.
|
|
35
|
+
*/
|
|
36
|
+
pending?: () => number;
|
|
37
|
+
}
|
|
38
|
+
/** The test-only surface a mock adds on top of kit's RemoteForm. */
|
|
39
|
+
export interface RemoteFormProbe {
|
|
40
|
+
/** Whether `preflight()` was called on this instance. */
|
|
41
|
+
readonly preflighted: boolean;
|
|
42
|
+
/** The keyed instance for `id`, as the component under test received it. */
|
|
43
|
+
for(id: string | number): RemoteFormProbe;
|
|
44
|
+
}
|
|
45
|
+
export declare function createRemoteFormMock<T = Record<string, unknown>>(options?: RemoteFormMockOptions): T;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test/Storybook stand-in for a SvelteKit RemoteForm. Components accept their
|
|
3
|
+
* form as an optional prop defaulting to the real one, so tests and stories
|
|
4
|
+
* substitute through the component's own interface — no module interception.
|
|
5
|
+
* It fakes exactly the surface components render with: the spreadable form
|
|
6
|
+
* attributes, fields.<name>.as()/issues(), fields.allIssues(), result,
|
|
7
|
+
* pending, and the preflight/enhance/for chain. One module — one place to
|
|
8
|
+
* audit when a kit upgrade changes the RemoteForm contract.
|
|
9
|
+
*
|
|
10
|
+
* The type parameter is the prop's type, so a call site reads
|
|
11
|
+
* `createRemoteFormMock<Props['form']>()` and carries no cast of its own.
|
|
12
|
+
*
|
|
13
|
+
* `for(id)` returns a distinct instance per key, and `preflight()` is recorded
|
|
14
|
+
* on the instance it was called on. Both used to return the mock itself, which
|
|
15
|
+
* made a keyed form and its base indistinguishable *by construction*: whether
|
|
16
|
+
* a reply form carried browser-side validation was a question no component
|
|
17
|
+
* test could ask. `preflighted` is the only non-kit member and is test-only.
|
|
18
|
+
*/
|
|
19
|
+
export function createRemoteFormMock(options = {}) {
|
|
20
|
+
const getResult = options.result ?? (() => undefined);
|
|
21
|
+
const getIssues = options.issues ?? (() => ({}));
|
|
22
|
+
const getPending = options.pending ?? (() => 0);
|
|
23
|
+
const field = (name) => ({
|
|
24
|
+
as: (type, value) => type === 'text'
|
|
25
|
+
? { name, ...(value !== undefined ? { value } : {}) }
|
|
26
|
+
: { name, type, ...(value !== undefined ? { value } : {}) },
|
|
27
|
+
issues: () => getIssues()[name],
|
|
28
|
+
value: () => undefined,
|
|
29
|
+
set: (input) => input
|
|
30
|
+
});
|
|
31
|
+
const roots = {
|
|
32
|
+
allIssues: () => {
|
|
33
|
+
const all = Object.values(getIssues()).flatMap((entry) => entry ?? []);
|
|
34
|
+
return all.length ? all : undefined;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const fields = new Proxy(roots, {
|
|
38
|
+
get: (target, prop) => (prop in target ? target[prop] : field(String(prop)))
|
|
39
|
+
});
|
|
40
|
+
const keyed = new Map();
|
|
41
|
+
function build(isKeyed) {
|
|
42
|
+
let preflighted = false;
|
|
43
|
+
// Only method/action/onsubmit may be enumerable: components spread the
|
|
44
|
+
// form object onto <form>, exactly like the real RemoteForm does.
|
|
45
|
+
const instance = {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
action: '#mock',
|
|
48
|
+
onsubmit: (event) => event.preventDefault()
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperties(instance, {
|
|
51
|
+
fields: { value: fields },
|
|
52
|
+
result: { get: getResult },
|
|
53
|
+
pending: { get: getPending },
|
|
54
|
+
submitted: { value: false },
|
|
55
|
+
element: { value: null },
|
|
56
|
+
submit: { value: () => Promise.resolve(true) },
|
|
57
|
+
validate: { value: () => Promise.resolve() },
|
|
58
|
+
enhance: { value: () => instance },
|
|
59
|
+
preflight: {
|
|
60
|
+
value: () => {
|
|
61
|
+
preflighted = true;
|
|
62
|
+
return instance;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
preflighted: { get: () => preflighted }
|
|
66
|
+
});
|
|
67
|
+
// kit types `for` as returning Omit<RemoteForm, 'for'>: a keyed instance
|
|
68
|
+
// cannot be keyed again, and the mock says so too.
|
|
69
|
+
if (!isKeyed) {
|
|
70
|
+
Object.defineProperty(instance, 'for', {
|
|
71
|
+
value: (id) => {
|
|
72
|
+
const key = String(id);
|
|
73
|
+
let child = keyed.get(key);
|
|
74
|
+
if (!child) {
|
|
75
|
+
child = build(true);
|
|
76
|
+
keyed.set(key, child);
|
|
77
|
+
}
|
|
78
|
+
return child;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return instance;
|
|
83
|
+
}
|
|
84
|
+
return build(false);
|
|
85
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { UiMessages } from '../config/types.js';
|
|
2
|
+
import type { ContactCategory } from '../content/types.js';
|
|
3
|
+
/** The contact category's label in the host app's copy. */
|
|
4
|
+
export declare function contactCategoryLabel(category: ContactCategory, messages: UiMessages): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The contact category's label in the host app's copy. */
|
|
2
|
+
export function contactCategoryLabel(category, messages) {
|
|
3
|
+
switch (category) {
|
|
4
|
+
case 'collaborate':
|
|
5
|
+
return messages.contact_category_collaborate();
|
|
6
|
+
case 'event':
|
|
7
|
+
return messages.contact_category_event();
|
|
8
|
+
case 'press':
|
|
9
|
+
return messages.contact_category_press();
|
|
10
|
+
case 'brand':
|
|
11
|
+
return messages.contact_category_brand();
|
|
12
|
+
case 'other':
|
|
13
|
+
return messages.contact_category_other();
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vit-foundation/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
"!dist/**/*.spec.*",
|
|
24
24
|
"!dist/fixtures.*",
|
|
25
25
|
"!dist/**/stories/**",
|
|
26
|
-
"!dist/**/tests/**"
|
|
27
|
-
"!dist/testing/**"
|
|
26
|
+
"!dist/**/tests/**"
|
|
28
27
|
],
|
|
29
28
|
"sideEffects": [
|
|
30
29
|
"**/*.css"
|
|
@@ -43,6 +42,21 @@
|
|
|
43
42
|
"svelte": "./dist/components/ui/index.js",
|
|
44
43
|
"default": "./dist/components/ui/index.js"
|
|
45
44
|
},
|
|
45
|
+
"./auth": {
|
|
46
|
+
"types": "./dist/components/auth/index.d.ts",
|
|
47
|
+
"svelte": "./dist/components/auth/index.js",
|
|
48
|
+
"default": "./dist/components/auth/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./account": {
|
|
51
|
+
"types": "./dist/components/account/index.d.ts",
|
|
52
|
+
"svelte": "./dist/components/account/index.js",
|
|
53
|
+
"default": "./dist/components/account/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./contact": {
|
|
56
|
+
"types": "./dist/components/contact/index.d.ts",
|
|
57
|
+
"svelte": "./dist/components/contact/index.js",
|
|
58
|
+
"default": "./dist/components/contact/index.js"
|
|
59
|
+
},
|
|
46
60
|
"./layout": {
|
|
47
61
|
"types": "./dist/components/layout/index.d.ts",
|
|
48
62
|
"svelte": "./dist/components/layout/index.js",
|
|
@@ -83,6 +97,16 @@
|
|
|
83
97
|
"svelte": "./dist/config/index.js",
|
|
84
98
|
"default": "./dist/config/index.js"
|
|
85
99
|
},
|
|
100
|
+
"./forms": {
|
|
101
|
+
"types": "./dist/forms/index.d.ts",
|
|
102
|
+
"svelte": "./dist/forms/index.js",
|
|
103
|
+
"default": "./dist/forms/index.js"
|
|
104
|
+
},
|
|
105
|
+
"./testing": {
|
|
106
|
+
"types": "./dist/testing/remote-form.d.ts",
|
|
107
|
+
"svelte": "./dist/testing/remote-form.js",
|
|
108
|
+
"default": "./dist/testing/remote-form.js"
|
|
109
|
+
},
|
|
86
110
|
"./tokens.css": "./dist/styles/tokens.css",
|
|
87
111
|
"./base.css": "./dist/styles/base.css"
|
|
88
112
|
},
|