@indietabletop/appkit 3.2.0-0 → 3.2.0-10

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 (109) hide show
  1. package/lib/ExternalLink.tsx +10 -0
  2. package/lib/FormSubmitButton.tsx +58 -0
  3. package/lib/FullscreenDismissBlocker.tsx +23 -0
  4. package/lib/IndieTabletopClubLogo.tsx +44 -0
  5. package/lib/IndieTabletopClubSymbol.tsx +37 -0
  6. package/lib/Letterhead/index.tsx +85 -0
  7. package/lib/Letterhead/stories.tsx +45 -0
  8. package/lib/Letterhead/style.css.ts +141 -0
  9. package/lib/LetterheadForm/LetterheadReadonlyTextField.stories.tsx +21 -0
  10. package/lib/LetterheadForm/LetterheadSubmitError.stories.tsx +23 -0
  11. package/lib/LetterheadForm/LetterheadTextField.stories.tsx +23 -0
  12. package/lib/LetterheadForm/index.tsx +94 -0
  13. package/lib/LetterheadForm/style.css.ts +80 -0
  14. package/lib/LoadingIndicator.tsx +39 -0
  15. package/lib/ServiceWorkerHandler.tsx +53 -0
  16. package/lib/animations.css.ts +17 -0
  17. package/lib/append-copy-to-text.test.ts +29 -0
  18. package/lib/append-copy-to-text.ts +35 -0
  19. package/lib/async-op.ts +246 -0
  20. package/lib/atomic.css.ts +11 -0
  21. package/{dist/caught-value.js → lib/caught-value.ts} +10 -8
  22. package/lib/class-names.ts +33 -0
  23. package/lib/client.ts +288 -0
  24. package/lib/common.css.ts +48 -0
  25. package/lib/failureMessages.test.ts +138 -0
  26. package/lib/failureMessages.ts +76 -0
  27. package/lib/globals.css.ts +45 -0
  28. package/{dist/index.d.ts → lib/index.ts} +9 -1
  29. package/lib/internal.css.ts +10 -0
  30. package/lib/media.ts +50 -0
  31. package/lib/storybook/decorators.tsx +10 -0
  32. package/lib/structs.ts +17 -0
  33. package/{dist/types.d.ts → lib/types.ts} +11 -6
  34. package/lib/use-async-op.ts +16 -0
  35. package/lib/use-document-background-color.ts +16 -0
  36. package/lib/use-form.ts +73 -0
  37. package/{dist/use-is-installed.js → lib/use-is-installed.ts} +7 -3
  38. package/lib/use-media-query.ts +21 -0
  39. package/lib/use-reverting-state.ts +32 -0
  40. package/lib/use-scroll-restoration.ts +99 -0
  41. package/lib/validations.ts +25 -0
  42. package/lib/vars.css.ts +9 -0
  43. package/package.json +15 -7
  44. package/dist/ExternalLink.d.ts +0 -3
  45. package/dist/ExternalLink.js +0 -4
  46. package/dist/FormSubmitButton.d.ts +0 -7
  47. package/dist/FormSubmitButton.js +0 -16
  48. package/dist/FullscreenDismissBlocker.d.ts +0 -5
  49. package/dist/FullscreenDismissBlocker.js +0 -19
  50. package/dist/IndieTabletopClubFooter.d.ts +0 -1
  51. package/dist/IndieTabletopClubFooter.js +0 -17
  52. package/dist/IndieTabletopClubLogo.d.ts +0 -7
  53. package/dist/IndieTabletopClubLogo.js +0 -6
  54. package/dist/IndieTabletopClubSymbol.d.ts +0 -7
  55. package/dist/IndieTabletopClubSymbol.js +0 -5
  56. package/dist/Letterhead.d.ts +0 -6
  57. package/dist/Letterhead.js +0 -14
  58. package/dist/LetterheadFooter.d.ts +0 -1
  59. package/dist/LetterheadFooter.js +0 -17
  60. package/dist/LoadingIndicator.d.ts +0 -3
  61. package/dist/LoadingIndicator.js +0 -17
  62. package/dist/ServiceWorkerHandler.d.ts +0 -11
  63. package/dist/ServiceWorkerHandler.js +0 -42
  64. package/dist/animations.css.d.ts +0 -3
  65. package/dist/animations.css.js +0 -14
  66. package/dist/append-copy-to-text.d.ts +0 -10
  67. package/dist/append-copy-to-text.js +0 -29
  68. package/dist/async-op.d.ts +0 -87
  69. package/dist/async-op.js +0 -223
  70. package/dist/caught-value.d.ts +0 -15
  71. package/dist/class-names.d.ts +0 -4
  72. package/dist/class-names.js +0 -6
  73. package/dist/client.d.ts +0 -117
  74. package/dist/client.js +0 -201
  75. package/dist/common.css.d.ts +0 -5
  76. package/dist/common.css.js +0 -38
  77. package/dist/defineNetlifyConfig.d.ts +0 -34
  78. package/dist/defineNetlifyConfig.js +0 -35
  79. package/dist/external-link.d.ts +0 -4
  80. package/dist/external-link.js +0 -4
  81. package/dist/form-submit-button.d.ts +0 -7
  82. package/dist/form-submit-button.js +0 -16
  83. package/dist/fullscreen-dismiss-blocker.d.ts +0 -5
  84. package/dist/fullscreen-dismiss-blocker.js +0 -19
  85. package/dist/globals.css.d.ts +0 -1
  86. package/dist/globals.css.js +0 -35
  87. package/dist/index.js +0 -25
  88. package/dist/internal.css.d.ts +0 -4
  89. package/dist/internal.css.js +0 -21
  90. package/dist/media.d.ts +0 -39
  91. package/dist/media.js +0 -49
  92. package/dist/service-worker-handler.d.ts +0 -11
  93. package/dist/service-worker-handler.js +0 -42
  94. package/dist/structs.d.ts +0 -20
  95. package/dist/structs.js +0 -15
  96. package/dist/types.js +0 -1
  97. package/dist/use-async-op.d.ts +0 -6
  98. package/dist/use-async-op.js +0 -12
  99. package/dist/use-document-background-color.d.ts +0 -4
  100. package/dist/use-document-background-color.js +0 -14
  101. package/dist/use-form.d.ts +0 -29
  102. package/dist/use-form.js +0 -33
  103. package/dist/use-is-installed.d.ts +0 -8
  104. package/dist/use-media-query.d.ts +0 -1
  105. package/dist/use-media-query.js +0 -15
  106. package/dist/use-reverting-state.d.ts +0 -5
  107. package/dist/use-reverting-state.js +0 -26
  108. package/dist/use-scroll-restoration.d.ts +0 -25
  109. package/dist/use-scroll-restoration.js +0 -67
package/lib/client.ts ADDED
@@ -0,0 +1,288 @@
1
+ import { type Infer, mask, object, string, Struct } from "superstruct";
2
+ import { Failure, Success } from "./async-op.js";
3
+ import { currentUser, sessionInfo } from "./structs.js";
4
+ import type { CurrentUser, FailurePayload, SessionInfo } from "./types.js";
5
+
6
+ export class IndieTabletopClient {
7
+ origin: string;
8
+ private onCurrentUser?: (currentUser: CurrentUser) => void;
9
+ private onSessionInfo?: (sessionInfo: SessionInfo) => void;
10
+ private onSessionExpired?: () => void;
11
+ private refreshTokenPromise?: Promise<
12
+ Success<{ sessionInfo: SessionInfo }> | Failure<FailurePayload>
13
+ >;
14
+
15
+ constructor(props: {
16
+ apiOrigin: string;
17
+
18
+ /**
19
+ * Runs every time the current user is fetched from the API. Typically, this
20
+ * happens during login, signup, and when the current user is fetched.
21
+ */
22
+ onCurrentUser?: (currentUser: CurrentUser) => void;
23
+
24
+ /**
25
+ * Runs ever time new session info is fetched from the API. Typically, this
26
+ * happends during login, signup, and when tokens are refreshed.
27
+ */
28
+ onSessionInfo?: (sessionInfo: SessionInfo) => void;
29
+
30
+ /**
31
+ * Runs when token refresh is attempted, but fails due to 401 error.
32
+ */
33
+ onSessionExpired?: () => void;
34
+ }) {
35
+ this.origin = props.apiOrigin;
36
+ this.onCurrentUser = props.onCurrentUser;
37
+ this.onSessionInfo = props.onSessionInfo;
38
+ this.onSessionExpired = props.onSessionExpired;
39
+ }
40
+
41
+ protected async fetch<T, S>(
42
+ path: string,
43
+ struct: Struct<T, S>,
44
+ init?: RequestInit & { json?: object },
45
+ ): Promise<Success<Infer<Struct<T, S>>> | Failure<FailurePayload>> {
46
+ // If json was provided, we stringify it. Otherwise we use body.
47
+ const body = init?.json ? JSON.stringify(init.json) : init?.body;
48
+
49
+ // If json was provided, we make sure that content type is correctly set.
50
+ const headers = init?.json
51
+ ? { ...init?.headers, "Content-Type": "application/json" }
52
+ : init?.headers;
53
+
54
+ try {
55
+ const res = await fetch(`${this.origin}${path}`, {
56
+ // Defaults
57
+ credentials: "include",
58
+
59
+ // Overrides
60
+ ...init,
61
+ body,
62
+ headers,
63
+ });
64
+
65
+ if (!res.ok) {
66
+ console.error(res);
67
+ return new Failure({ type: "API_ERROR", code: res.status });
68
+ }
69
+
70
+ try {
71
+ const data = mask(await res.json(), struct);
72
+ return new Success(data);
73
+ } catch (error) {
74
+ console.error(error);
75
+
76
+ return new Failure({ type: "VALIDATION_ERROR" });
77
+ }
78
+ } catch (error) {
79
+ console.error(error);
80
+
81
+ if (error instanceof Error) {
82
+ return new Failure({ type: "NETWORK_ERROR" });
83
+ }
84
+
85
+ return new Failure({ type: "UNKNOWN_ERROR" });
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Fetches data and retries 401 failures after attempting to refresh tokens.
91
+ */
92
+ protected async fetchWithAuth<T, S>(
93
+ path: string,
94
+ struct: Struct<T, S>,
95
+ init?: RequestInit & { json?: object },
96
+ ): Promise<Success<Infer<Struct<T, S>>> | Failure<FailurePayload>> {
97
+ const op = await this.fetch(path, struct, init);
98
+
99
+ if (op.isSuccess) {
100
+ return op;
101
+ }
102
+
103
+ if (op.failure.type === "API_ERROR" && op.failure.code === 401) {
104
+ console.info("API request failed with error 401. Refreshing tokens.");
105
+
106
+ const refreshOp = await this.refreshTokens();
107
+
108
+ if (refreshOp.isSuccess) {
109
+ console.info("Tokens refreshed. Retrying request.");
110
+ return await this.fetch(path, struct, init);
111
+ } else {
112
+ console.info("Could not refresh tokens.");
113
+ }
114
+ }
115
+
116
+ return op;
117
+ }
118
+
119
+ async login(payload: { email: string; password: string }) {
120
+ const result = await this.fetch(
121
+ "/v1/sessions",
122
+ object({
123
+ currentUser: currentUser(),
124
+ sessionInfo: sessionInfo(),
125
+ }),
126
+ {
127
+ method: "POST",
128
+ json: { email: payload.email, plaintextPassword: payload.password },
129
+ },
130
+ );
131
+
132
+ if (result.isSuccess) {
133
+ this.onCurrentUser?.(result.value.currentUser);
134
+ this.onSessionInfo?.(result.value.sessionInfo);
135
+ }
136
+
137
+ return result;
138
+ }
139
+
140
+ async logout() {
141
+ return await this.fetch("/v1/sessions", object({ message: string() }), {
142
+ method: "DELETE",
143
+ });
144
+ }
145
+
146
+ async join(payload: {
147
+ email: string;
148
+ password: string;
149
+ acceptedTos: boolean;
150
+ subscribedToNewsletter: boolean;
151
+ }) {
152
+ const res = await this.fetch(
153
+ "/v1/users",
154
+ object({
155
+ currentUser: currentUser(),
156
+ sessionInfo: sessionInfo(),
157
+ tokenId: string(),
158
+ }),
159
+ {
160
+ method: "POST",
161
+ json: {
162
+ email: payload.email,
163
+ plaintextPassword: payload.password,
164
+ acceptedTos: payload.acceptedTos,
165
+ subscribedToNewsletter: payload.subscribedToNewsletter,
166
+ },
167
+ },
168
+ );
169
+
170
+ if (res.isSuccess) {
171
+ this.onCurrentUser?.(res.value.currentUser);
172
+ this.onSessionInfo?.(res.value.sessionInfo);
173
+ }
174
+
175
+ return res;
176
+ }
177
+
178
+ /**
179
+ * Triggers token refresh process.
180
+ *
181
+ * Note that we do not want to perform multiple concurrent token refresh
182
+ * actions, as that will result in unnecessary 401s. For this reason, a
183
+ * reference to t
184
+ */
185
+ async refreshTokens() {
186
+ // If there is an ongoing token refresh in progress return that. This should
187
+ // only deal the response payload, none of the side-effects and cleanup,
188
+ // which will be handled by the initial invocation.
189
+ const ongoingRequest = this.refreshTokenPromise;
190
+
191
+ if (ongoingRequest) {
192
+ console.info("Token refresh ongoing. Reusing existing promise.");
193
+ return await ongoingRequest;
194
+ }
195
+
196
+ // Cache the promise on an instance property to share a reference from
197
+ // other potential invocations.
198
+ this.refreshTokenPromise = this.fetch(
199
+ "/v1/sessions/access-tokens",
200
+ object({ sessionInfo: sessionInfo() }),
201
+ { method: "POST" },
202
+ );
203
+
204
+ const result = await this.refreshTokenPromise;
205
+
206
+ if (result.isSuccess) {
207
+ this.onSessionInfo?.(result.value.sessionInfo);
208
+ }
209
+
210
+ if (
211
+ result.isFailure &&
212
+ result.failure.type === "API_ERROR" &&
213
+ result.failure.code === 401
214
+ ) {
215
+ this.onSessionExpired?.();
216
+ }
217
+
218
+ // Make sure to reset the shared reference so that subsequent invocations
219
+ // once again initiate token refresh.
220
+ delete this.refreshTokenPromise;
221
+
222
+ return result;
223
+ }
224
+
225
+ async requestPasswordReset(payload: { email: string }) {
226
+ return await this.fetch(
227
+ `/v1/password-reset-tokens`,
228
+ object({ message: string(), tokenId: string() }),
229
+ { method: "POST", json: payload },
230
+ );
231
+ }
232
+
233
+ async checkPasswordResetCode(payload: { tokenId: string; code: string }) {
234
+ const queryParams = new URLSearchParams({ plaintextCode: payload.code });
235
+ return await this.fetch(
236
+ `/v1/password-reset-tokens/${payload.tokenId}?${queryParams}`,
237
+ object({ message: string() }),
238
+ { method: "GET" },
239
+ );
240
+ }
241
+
242
+ async setNewPassword(payload: {
243
+ tokenId: string;
244
+ code: string;
245
+ password: string;
246
+ }) {
247
+ const queryParams = new URLSearchParams({ plaintextCode: payload.code });
248
+ return await this.fetch(
249
+ `/v1/password-reset-tokens/${payload.tokenId}?${queryParams}`,
250
+ object({ message: string() }),
251
+ { method: "PUT", json: { plaintextPassword: payload.password } },
252
+ );
253
+ }
254
+
255
+ async requestUserVerification() {
256
+ return await this.fetch(
257
+ `/v1/user-verification-tokens`,
258
+ object({ message: string(), tokenId: string() }),
259
+ { method: "POST" },
260
+ );
261
+ }
262
+
263
+ async verifyUser(payload: { tokenId: string; code: string }) {
264
+ const queryParams = new URLSearchParams({ plaintextCode: payload.code });
265
+ const req = await this.fetch(
266
+ `/v1/user-verification-tokens/${payload.tokenId}?${queryParams}`,
267
+ object({ message: string() }),
268
+ { method: "PUT" },
269
+ );
270
+
271
+ if (req.isSuccess) {
272
+ await this.refreshTokens();
273
+ await this.getCurrentUser();
274
+ }
275
+
276
+ return req;
277
+ }
278
+
279
+ async getCurrentUser() {
280
+ const result = await this.fetchWithAuth(`/v1/users/me`, currentUser());
281
+
282
+ if (result.isSuccess) {
283
+ this.onCurrentUser?.(result.value);
284
+ }
285
+
286
+ return result;
287
+ }
288
+ }
@@ -0,0 +1,48 @@
1
+ import { style } from "@vanilla-extract/css";
2
+ import { Hover, MinWidth } from "./media.ts";
3
+
4
+ export const itcSymbol = style({
5
+ inlineSize: "2.5rem",
6
+ blockSize: "2.5rem",
7
+ margin: "0rem auto 0.75rem",
8
+
9
+ "@media": {
10
+ [MinWidth.MEDIUM]: {
11
+ marginBlock: "-1rem 1.5rem",
12
+ },
13
+ },
14
+ });
15
+
16
+ export const manofa = style({
17
+ fontFamily: `"manofa", sans-serif`,
18
+ fontFeatureSettings: `"ss01"`,
19
+ letterSpacing: "-.01em",
20
+ });
21
+
22
+ export const minion = style({
23
+ fontFamily: `"minion-pro", serif`,
24
+ });
25
+
26
+ export const itcCard = style([
27
+ minion,
28
+ {
29
+ backgroundColor: "white",
30
+ },
31
+ ]);
32
+
33
+ export const interactiveText = style({
34
+ display: "inline",
35
+ textDecoration: "underline",
36
+ textDecorationColor: "hsl(from currentcolor h s l / 0.3)",
37
+ textUnderlineOffset: "0.15em",
38
+
39
+ "@media": {
40
+ [Hover.HOVER]: {
41
+ transition: "text-decoration-color 200ms",
42
+
43
+ ":hover": {
44
+ textDecorationColor: "hsl(from currentcolor h s l / 1)",
45
+ },
46
+ },
47
+ },
48
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ getFetchFailureMessages,
4
+ getSubmitFailureMessage,
5
+ } from "./failureMessages.ts";
6
+
7
+ describe("getFetchFailureMessages", () => {
8
+ test("Returns correct message for API_ERROR with code 404", () => {
9
+ const result = getFetchFailureMessages({ type: "API_ERROR", code: 404 });
10
+
11
+ expect(result).toMatchInlineSnapshot(`
12
+ {
13
+ "description": "The link you have followed might be broken.",
14
+ "title": "Not found",
15
+ }
16
+ `);
17
+ });
18
+
19
+ test("Returns correct message for API_ERROR with code 500", () => {
20
+ const result = getFetchFailureMessages({ type: "API_ERROR", code: 500 });
21
+
22
+ expect(result).toMatchInlineSnapshot(`
23
+ {
24
+ "description": "This is probably an issue with our servers. You can try refreshing.",
25
+ "title": "Ooops, something went wrong",
26
+ }
27
+ `);
28
+ });
29
+
30
+ test("Returns correct message for API_ERROR with partial override", () => {
31
+ const result = getFetchFailureMessages(
32
+ { type: "API_ERROR", code: 404 },
33
+ { 404: { title: `Army not found` } },
34
+ );
35
+
36
+ expect(result).toMatchInlineSnapshot(`
37
+ {
38
+ "description": "The link you have followed might be broken.",
39
+ "title": "Army not found",
40
+ }
41
+ `);
42
+ });
43
+
44
+ test("Returns correct message for API_ERROR with override", () => {
45
+ const result = getFetchFailureMessages(
46
+ { type: "API_ERROR", code: 404 },
47
+ {
48
+ 404: {
49
+ title: `Army not found`,
50
+ description: `It might have been deleted.`,
51
+ },
52
+ },
53
+ );
54
+
55
+ expect(result).toMatchInlineSnapshot(`
56
+ {
57
+ "description": "It might have been deleted.",
58
+ "title": "Army not found",
59
+ }
60
+ `);
61
+ });
62
+
63
+ test("Returns correct message for NETWORK_ERROR", () => {
64
+ const result = getFetchFailureMessages({ type: "NETWORK_ERROR" });
65
+
66
+ expect(result).toMatchInlineSnapshot(`
67
+ {
68
+ "description": "Check your interent connection and try again.",
69
+ "title": "No connection",
70
+ }
71
+ `);
72
+ });
73
+
74
+ test("Returns correct message for UNKNOWN_ERROR", () => {
75
+ const result = getFetchFailureMessages({ type: "UNKNOWN_ERROR" });
76
+
77
+ expect(result).toMatchInlineSnapshot(`
78
+ {
79
+ "description": "This is probably an issue on our side. You can try refreshing.",
80
+ "title": "Ooops, something went wrong",
81
+ }
82
+ `);
83
+ });
84
+
85
+ test("Returns correct message for an unrecognised error type", () => {
86
+ const result = getFetchFailureMessages({ type: "FOO" as any });
87
+
88
+ expect(result).toMatchInlineSnapshot(`
89
+ {
90
+ "description": "This is probably an issue on our side. You can try refreshing.",
91
+ "title": "Ooops, something went wrong",
92
+ }
93
+ `);
94
+ });
95
+ });
96
+
97
+ describe("getSubmitFailureMessage", () => {
98
+ test("Returns correct message for API_ERROR with code 500", () => {
99
+ const message = getSubmitFailureMessage({ type: "API_ERROR", code: 500 });
100
+ expect(message).toMatchInlineSnapshot(
101
+ `"Could not submit form due to an unexpected server error. Please refresh the page and try again."`,
102
+ );
103
+ });
104
+
105
+ test("Returns correct message for API_ERROR with override", () => {
106
+ const message = getSubmitFailureMessage(
107
+ { type: "API_ERROR", code: 401 },
108
+ { 401: `Username and password do not match. Please try again.` },
109
+ );
110
+ expect(message).toMatchInlineSnapshot(
111
+ `"Username and password do not match. Please try again."`,
112
+ );
113
+ });
114
+
115
+ test("Returns correct message for NETWORK_ERROR", () => {
116
+ const result = getSubmitFailureMessage({ type: "NETWORK_ERROR" });
117
+
118
+ expect(result).toMatchInlineSnapshot(
119
+ `"Could not submit form due to network error. Make sure you are connected to the internet and try again."`,
120
+ );
121
+ });
122
+
123
+ test("Returns correct message for UNKNOWN_ERROR", () => {
124
+ const result = getSubmitFailureMessage({ type: "UNKNOWN_ERROR" });
125
+
126
+ expect(result).toMatchInlineSnapshot(
127
+ `"Could not submit form due to an unexpected error. Please refresh the page and try again."`,
128
+ );
129
+ });
130
+
131
+ test("Returns correct message for an unrecognised error type", () => {
132
+ const result = getSubmitFailureMessage({ type: "FOO" as any });
133
+
134
+ expect(result).toMatchInlineSnapshot(
135
+ `"Could not submit form due to an unexpected error. Please refresh the page and try again."`,
136
+ );
137
+ });
138
+ });
@@ -0,0 +1,76 @@
1
+ import type { FailurePayload } from "./types.ts";
2
+
3
+ type OnOverride<T> = (fallback: T, override?: Partial<T>) => T;
4
+
5
+ function createFailureMessageGetter<T>(
6
+ defaults: Record<number | "fallback" | "connection", T>,
7
+ options: { onOverride: OnOverride<T> },
8
+ ) {
9
+ return function getMessage(
10
+ failure: FailurePayload,
11
+ overrides: Record<number, Partial<T>> = {},
12
+ ) {
13
+ switch (failure.type) {
14
+ case "API_ERROR": {
15
+ return options.onOverride(
16
+ defaults[failure.code] ?? defaults.fallback,
17
+ overrides[failure.code],
18
+ );
19
+ }
20
+
21
+ case "NETWORK_ERROR": {
22
+ return defaults.connection;
23
+ }
24
+
25
+ default: {
26
+ return defaults.fallback;
27
+ }
28
+ }
29
+ };
30
+ }
31
+
32
+ export const getFetchFailureMessages = createFailureMessageGetter(
33
+ {
34
+ 404: {
35
+ title: `Not found`,
36
+ description: `The link you have followed might be broken.`,
37
+ },
38
+ 500: {
39
+ title: `Ooops, something went wrong`,
40
+ description: `This is probably an issue with our servers. You can try refreshing.`,
41
+ },
42
+ connection: {
43
+ title: `No connection`,
44
+ description: `Check your interent connection and try again.`,
45
+ },
46
+ fallback: {
47
+ title: `Ooops, something went wrong`,
48
+ description: `This is probably an issue on our side. You can try refreshing.`,
49
+ },
50
+ },
51
+ {
52
+ onOverride(fallback, override) {
53
+ return { ...fallback, ...override };
54
+ },
55
+ },
56
+ );
57
+
58
+ export const getSubmitFailureMessage = createFailureMessageGetter(
59
+ {
60
+ 500: `Could not submit form due to an unexpected server error. Please refresh the page and try again.`,
61
+ connection: `Could not submit form due to network error. Make sure you are connected to the internet and try again.`,
62
+ fallback: `Could not submit form due to an unexpected error. Please refresh the page and try again.`,
63
+ },
64
+ {
65
+ onOverride(fallback, override) {
66
+ return override ?? fallback;
67
+ },
68
+ },
69
+ );
70
+
71
+ /**
72
+ * @deprecated Use {@link getSubmitFailureMessage} instead.
73
+ */
74
+ export function toKnownFailureMessage(failure: FailurePayload) {
75
+ return getSubmitFailureMessage(failure);
76
+ }
@@ -0,0 +1,45 @@
1
+ import { globalStyle } from "@vanilla-extract/css";
2
+
3
+ // Apply global vars
4
+ import "./vars.css.ts";
5
+
6
+ globalStyle(":root", {
7
+ fontSynthesis: "none",
8
+ textRendering: "optimizeLegibility",
9
+ WebkitFontSmoothing: "antialiased",
10
+ MozOsxFontSmoothing: "grayscale",
11
+ });
12
+
13
+ globalStyle("*", {
14
+ boxSizing: "border-box",
15
+ });
16
+
17
+ globalStyle("img, picture, svg", {
18
+ display: "block",
19
+ });
20
+
21
+ globalStyle("a", {
22
+ display: "block",
23
+ color: "inherit",
24
+ textDecoration: "none",
25
+ });
26
+
27
+ globalStyle("input, textarea", {
28
+ fontFamily: "inherit",
29
+ });
30
+
31
+ globalStyle("button", {
32
+ display: "block",
33
+ fontSize: "inherit",
34
+ fontFamily: "inherit",
35
+ backgroundColor: "transparent",
36
+ border: "none",
37
+ color: "inherit",
38
+ cursor: "pointer",
39
+ padding: 0,
40
+ });
41
+
42
+ globalStyle("body, h1, h2, h3, h4, h5, h6, p, ul, li, ol", {
43
+ margin: 0,
44
+ padding: 0,
45
+ });
@@ -1,10 +1,14 @@
1
+ // Components
1
2
  export * from "./ExternalLink.tsx";
2
3
  export * from "./FormSubmitButton.tsx";
3
4
  export * from "./FullscreenDismissBlocker.tsx";
4
5
  export * from "./IndieTabletopClubSymbol.tsx";
5
- export * from "./LetterheadFooter.tsx";
6
+ export * from "./Letterhead/index.tsx";
7
+ export * from "./LetterheadForm/index.tsx";
6
8
  export * from "./LoadingIndicator.tsx";
7
9
  export * from "./ServiceWorkerHandler.tsx";
10
+
11
+ // Hooks
8
12
  export * from "./use-async-op.ts";
9
13
  export * from "./use-document-background-color.ts";
10
14
  export * from "./use-form.ts";
@@ -12,11 +16,15 @@ export * from "./use-is-installed.ts";
12
16
  export * from "./use-media-query.ts";
13
17
  export * from "./use-reverting-state.ts";
14
18
  export * from "./use-scroll-restoration.ts";
19
+
20
+ // Utils
15
21
  export * from "./append-copy-to-text.ts";
16
22
  export * from "./async-op.ts";
17
23
  export * from "./caught-value.ts";
18
24
  export * from "./class-names.ts";
19
25
  export * from "./client.ts";
26
+ export * from "./failureMessages.ts";
20
27
  export * from "./media.ts";
21
28
  export * from "./structs.ts";
22
29
  export * from "./types.ts";
30
+ export * from "./validations.ts";
@@ -0,0 +1,10 @@
1
+ import { createVar, style } from "@vanilla-extract/css";
2
+ import { bounce } from "./animations.css.ts";
3
+
4
+ export const animationDelay = createVar();
5
+
6
+ export const dot = style({
7
+ fill: "currentcolor",
8
+ opacity: 0.8,
9
+ animation: `${bounce} 2s ${animationDelay} infinite`,
10
+ });
package/lib/media.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
3
+ */
4
+
5
+ export enum PrefersColorScheme {
6
+ LIGHT = "(prefers-color-scheme: light)",
7
+ DARK = "(prefers-color-scheme: dark)",
8
+ }
9
+ /**
10
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
11
+ */
12
+
13
+ export enum PrefersReducedMotion {
14
+ NO_PREFERENCE = "(prefers-reduced-motion: no-preference)",
15
+ REDUCE = "(prefers-reduced-motion: reduce)",
16
+ }
17
+
18
+ export enum Hover {
19
+ NONE = "(hover: none)",
20
+
21
+ // Some Samsung phones incorrectly report that they have "hover" even though they
22
+ // do not. Adding the pointer query correctly filters these phones out.
23
+ HOVER = "(hover: hover) and (pointer: fine)",
24
+ }
25
+
26
+ export enum MediaType {
27
+ PRINT = "print",
28
+ SCREEN = "screen",
29
+ }
30
+
31
+ export enum MinHeight {
32
+ TALL = "(min-height: 40em)",
33
+ }
34
+
35
+ export enum MinWidth {
36
+ SMALL = "(min-width: 28em)",
37
+ MEDIUM = "(min-width: 50em)",
38
+ WIDE = "(min-width: 66em)",
39
+ X_WIDE = "(min-width: 80em)",
40
+ XX_WIDE = "(min-width: 140em)",
41
+ }
42
+
43
+ export enum DisplayMode {
44
+ STANDALONE = "(display-mode: standalone)",
45
+ }
46
+
47
+ export enum Pointer {
48
+ COARSE = "(pointer: coarse)",
49
+ FINE = "(pointer: fine)",
50
+ }
@@ -0,0 +1,10 @@
1
+ import { FormProvider, type FormProviderProps } from "@ariakit/react";
2
+ import { type Decorator } from "@storybook/react-vite";
3
+
4
+ export function form(props?: FormProviderProps): Decorator {
5
+ return (Story) => (
6
+ <FormProvider {...props}>
7
+ <Story />
8
+ </FormProvider>
9
+ );
10
+ }