@indietabletop/appkit 3.2.0-7 → 3.2.0-9

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.
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { LetterheadReadonlyTextField } from "./index.tsx";
3
+
4
+ const meta = {
5
+ title: "ReadonlyTextField",
6
+ component: LetterheadReadonlyTextField,
7
+ tags: ["autodocs"],
8
+ args: {},
9
+ } satisfies Meta<typeof LetterheadReadonlyTextField>;
10
+
11
+ export default meta;
12
+
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ label: "Email",
18
+ value: "john@example.com",
19
+ placeholder: "john@example.com",
20
+ },
21
+ };
@@ -0,0 +1,23 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { form } from "../storybook/decorators.tsx";
3
+ import { LetterheadSubmitError } from "./index.tsx";
4
+
5
+ const meta = {
6
+ title: "LetterheadSubmitError",
7
+ component: LetterheadSubmitError,
8
+ tags: ["autodocs"],
9
+ args: {},
10
+ decorators: [
11
+ form({ defaultErrors: { submit: "This is an error message." } }),
12
+ ],
13
+ } satisfies Meta<typeof LetterheadSubmitError>;
14
+
15
+ export default meta;
16
+
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Default: Story = {
20
+ args: {
21
+ name: "submit",
22
+ },
23
+ };
@@ -0,0 +1,23 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { form } from "../storybook/decorators.tsx";
3
+ import { LetterheadTextField } from "./index.tsx";
4
+
5
+ const meta = {
6
+ title: "FormTextField",
7
+ component: LetterheadTextField,
8
+ tags: ["autodocs"],
9
+ args: {},
10
+ decorators: [form()],
11
+ } satisfies Meta<typeof LetterheadTextField>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ label: "Email",
20
+ placeholder: "john@example.com",
21
+ name: "foo",
22
+ },
23
+ };
@@ -0,0 +1,94 @@
1
+ import {
2
+ FormError,
3
+ FormInput,
4
+ type FormInputProps,
5
+ FormLabel,
6
+ useFormContext,
7
+ useStoreState,
8
+ } from "@ariakit/react";
9
+ import { type ReactNode } from "react";
10
+ import * as css from "./style.css.ts";
11
+
12
+ type LetterheadTextFieldProps = FormInputProps & {
13
+ label: string;
14
+ hint?: ReactNode;
15
+ };
16
+
17
+ export function LetterheadTextField(props: LetterheadTextFieldProps) {
18
+ const { name, label, hint, ...inputProps } = props;
19
+
20
+ return (
21
+ <div>
22
+ <div className={css.field}>
23
+ <FormLabel name={name} className={css.fieldLabel}>
24
+ {label}
25
+ </FormLabel>
26
+
27
+ <FormInput {...inputProps} name={name} className={css.fieldInput} />
28
+ </div>
29
+
30
+ <FormError name={name} className={css.fieldIssue} />
31
+
32
+ {hint && <div className={css.fieldHint}>{hint}</div>}
33
+ </div>
34
+ );
35
+ }
36
+
37
+ type LetterheadReadonlyTextFieldProps = {
38
+ label: string;
39
+ value: string;
40
+ placeholder?: string;
41
+ hint?: ReactNode;
42
+ type?: "text" | "email" | "password";
43
+ };
44
+
45
+ /**
46
+ * Renders a read-only text field.
47
+ *
48
+ * For an editable text field, use {@link LetterheadTextField} along with
49
+ * Ariakit form store.
50
+ */
51
+ export function LetterheadReadonlyTextField(
52
+ props: LetterheadReadonlyTextFieldProps,
53
+ ) {
54
+ return (
55
+ <div>
56
+ <label className={css.field}>
57
+ <span className={css.fieldLabel}>{props.label}</span>
58
+
59
+ <input
60
+ className={css.fieldInput}
61
+ type={props.type ?? "text"}
62
+ value={props.value}
63
+ placeholder={props.placeholder}
64
+ readOnly
65
+ />
66
+ </label>
67
+
68
+ {props.hint && <div className={css.fieldHint}>{props.hint}</div>}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ type LetterheadSubmitErrorProps = {
74
+ name: string;
75
+ };
76
+
77
+ /**
78
+ * Renders an error message from form context.
79
+ *
80
+ * If there is no error message, will be completely omitted from DOM, so there
81
+ * is no need to guard it with an if statement.
82
+ */
83
+ export function LetterheadSubmitError(props: LetterheadSubmitErrorProps) {
84
+ const form = useFormContext();
85
+ const message = useStoreState(form, (s) => {
86
+ return s?.errors[props.name] as string | undefined;
87
+ });
88
+
89
+ return (
90
+ <div role="alert" className={css.submitError}>
91
+ {message}
92
+ </div>
93
+ );
94
+ }
@@ -0,0 +1,80 @@
1
+ import { style } from "@vanilla-extract/css";
2
+ import { manofa, minion } from "../common.css.ts";
3
+ import { Color } from "../vars.css.ts";
4
+
5
+ const border = style({
6
+ borderRadius: "0.5rem",
7
+ border: `1px solid ${Color.GRAY}`,
8
+ });
9
+
10
+ export const field = style({
11
+ display: "block",
12
+ });
13
+
14
+ export const fieldLabel = style([
15
+ manofa,
16
+ {
17
+ display: "block",
18
+ textTransform: "uppercase",
19
+ fontSize: "0.75rem",
20
+ fontWeight: 600,
21
+ marginBottom: "0.5rem",
22
+ },
23
+ ]);
24
+
25
+ export const fieldInput = style([
26
+ border,
27
+ minion,
28
+ {
29
+ display: "block",
30
+ width: "100%",
31
+ fontSize: "1rem",
32
+ lineHeight: "1.25rem",
33
+ padding: "1rem 0 1rem 1rem",
34
+
35
+ ":read-only": {
36
+ backgroundColor: "hsl(0 0% 0% / 0.05)",
37
+ },
38
+
39
+ // Hide MS Edge widgets -- we handle them manually
40
+ "::-ms-clear": {
41
+ display: "none",
42
+ },
43
+ "::-ms-reveal": {
44
+ display: "none",
45
+ },
46
+ },
47
+ ]);
48
+
49
+ export const fieldIssue = style({
50
+ color: Color.PURPLE,
51
+ fontSize: "0.875rem",
52
+ marginTop: "0.5rem",
53
+
54
+ ":empty": {
55
+ display: "none",
56
+ },
57
+ });
58
+
59
+ export const fieldHint = style({
60
+ color: Color.MID_GRAY,
61
+ fontSize: "0.875rem",
62
+ marginTop: "0.5rem",
63
+
64
+ selectors: {
65
+ [`${fieldIssue}:not(:empty) + &`]: {
66
+ display: "none",
67
+ },
68
+ },
69
+ });
70
+
71
+ export const submitError = style({
72
+ padding: "1rem",
73
+ color: Color.PURPLE,
74
+ backgroundColor: Color.PALE_GRAY,
75
+ borderRadius: "0.75rem",
76
+
77
+ ":empty": {
78
+ display: "none",
79
+ },
80
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ appendCopyToText,
4
+ maybeAppendCopyToText,
5
+ } from "./append-copy-to-text.ts";
6
+
7
+ describe("appendCopyToText", () => {
8
+ test("Appends ' (Copy)' to provided string", () => {
9
+ const returnValue = appendCopyToText("Zangrad Raiders");
10
+ expect(returnValue).toBe("Zangrad Raiders (Copy)");
11
+ });
12
+
13
+ test("Adds a copy count number if string already ends in ' (Copy)'", () => {
14
+ const returnValue = appendCopyToText("Zangrad Raiders (Copy)");
15
+ expect(returnValue).toBe("Zangrad Raiders (Copy 2)");
16
+ });
17
+
18
+ test("Increments a copy count number if one already exists", () => {
19
+ const returnValue = appendCopyToText("Zangrad Raiders (Copy 2)");
20
+ expect(returnValue).toBe("Zangrad Raiders (Copy 3)");
21
+ });
22
+ });
23
+
24
+ describe("maybeAppendCopyToText", () => {
25
+ test("Ignores empty strings", () => {
26
+ const returnValue = maybeAppendCopyToText("");
27
+ expect(returnValue).toBe("");
28
+ });
29
+ });
@@ -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
+ }
@@ -1,5 +1,8 @@
1
1
  import { globalStyle } from "@vanilla-extract/css";
2
2
 
3
+ // Apply global vars
4
+ import "./vars.css.ts";
5
+
3
6
  globalStyle(":root", {
4
7
  fontSynthesis: "none",
5
8
  textRendering: "optimizeLegibility",
package/lib/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./FormSubmitButton.tsx";
4
4
  export * from "./FullscreenDismissBlocker.tsx";
5
5
  export * from "./IndieTabletopClubSymbol.tsx";
6
6
  export * from "./Letterhead/index.tsx";
7
+ export * from "./LetterheadForm/index.tsx";
7
8
  export * from "./LoadingIndicator.tsx";
8
9
  export * from "./ServiceWorkerHandler.tsx";
9
10
 
@@ -22,7 +23,7 @@ export * from "./async-op.ts";
22
23
  export * from "./caught-value.ts";
23
24
  export * from "./class-names.ts";
24
25
  export * from "./client.ts";
25
- export * from "./knownFailure.ts";
26
+ export * from "./failureMessages.ts";
26
27
  export * from "./media.ts";
27
28
  export * from "./structs.ts";
28
29
  export * from "./types.ts";
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ import { createGlobalTheme } from "@vanilla-extract/css";
2
+
3
+ export const Color = createGlobalTheme(":root", {
4
+ GRAY: "#ececec",
5
+ MID_GRAY: "hsl(0 0% 50%)",
6
+ LIGHT_GRAY: "#e7e8e8",
7
+ PALE_GRAY: "#f6f7f7",
8
+ PURPLE: "#d6446e",
9
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indietabletop/appkit",
3
- "version": "3.2.0-7",
3
+ "version": "3.2.0-9",
4
4
  "description": "A collection of modules used in apps built by Indie Tabletop Club",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,9 +0,0 @@
1
- import type { FailurePayload } from "./types.ts";
2
-
3
- export function toKnownFailureMessage(failure: FailurePayload) {
4
- if (failure.type === "NETWORK_ERROR") {
5
- return "Could not submit form due to network error. Make sure you are connected to the internet and try again.";
6
- }
7
-
8
- return "Could not submit form due to an unexpected error. Please refresh the page and try again.";
9
- }