@indietabletop/appkit 7.0.0-0 → 7.0.0-rc.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 (286) hide show
  1. package/lib/AppConfig/AppConfig.tsx +80 -0
  2. package/lib/AppConfig/formatters.tsx +48 -0
  3. package/lib/AuthCard/AuthCard.stories.ts +34 -0
  4. package/lib/AuthCard/AuthCard.tsx +64 -0
  5. package/lib/AuthCard/style.css.ts +49 -0
  6. package/lib/CacheProvider.tsx +20 -0
  7. package/lib/DialogTrigger/index.tsx +36 -0
  8. package/lib/DocumentTitle/DocumentTitle.tsx +10 -0
  9. package/lib/EnumMapper.ts +50 -0
  10. package/lib/ExternalLink.tsx +10 -0
  11. package/lib/FullscreenDismissBlocker.tsx +23 -0
  12. package/{dist/HistoryState.d.ts → lib/HistoryState.ts} +5 -2
  13. package/lib/IndieTabletopClubLogo.tsx +44 -0
  14. package/lib/IndieTabletopClubSymbol.tsx +37 -0
  15. package/lib/InfoPage/index.tsx +46 -0
  16. package/lib/InfoPage/pages.tsx +36 -0
  17. package/lib/InfoPage/style.css.ts +36 -0
  18. package/lib/Letterhead/index.tsx +85 -0
  19. package/lib/Letterhead/stories.tsx +41 -0
  20. package/lib/Letterhead/style.css.ts +152 -0
  21. package/lib/LetterheadForm/LetterheadReadonlyTextField.stories.tsx +17 -0
  22. package/lib/LetterheadForm/LetterheadSubmitError.stories.tsx +19 -0
  23. package/lib/LetterheadForm/LetterheadTextField.stories.tsx +19 -0
  24. package/lib/LetterheadForm/index.tsx +137 -0
  25. package/lib/LetterheadForm/style.css.ts +89 -0
  26. package/lib/ListView/ListView.stories.tsx +36 -0
  27. package/lib/ListView/ListView.tsx +80 -0
  28. package/lib/ListView/style.css.ts +90 -0
  29. package/lib/LoadingIndicator.tsx +40 -0
  30. package/lib/MiddotSeparated/MiddotSeparated.stories.ts +26 -0
  31. package/lib/MiddotSeparated/MiddotSeparated.tsx +26 -0
  32. package/lib/MiddotSeparated/style.css.ts +10 -0
  33. package/lib/ModalDialog/index.tsx +28 -0
  34. package/lib/ModalDialog/style.css.ts +88 -0
  35. package/lib/ModernIDB/Cursor.ts +91 -0
  36. package/lib/ModernIDB/ModernIDB.ts +337 -0
  37. package/lib/ModernIDB/ModernIDBError.ts +9 -0
  38. package/lib/ModernIDB/ObjectStore.ts +195 -0
  39. package/lib/ModernIDB/ObjectStoreIndex.ts +102 -0
  40. package/lib/ModernIDB/README.md +9 -0
  41. package/lib/ModernIDB/Transaction.ts +40 -0
  42. package/lib/ModernIDB/VersionChangeManager.ts +57 -0
  43. package/lib/ModernIDB/bindings/factory.tsx +165 -0
  44. package/lib/ModernIDB/bindings/index.ts +2 -0
  45. package/{dist/ModernIDB/bindings/types.d.ts → lib/ModernIDB/bindings/types.ts} +32 -13
  46. package/lib/ModernIDB/bindings/utils.tsx +32 -0
  47. package/lib/ModernIDB/index.ts +10 -0
  48. package/lib/ModernIDB/types.ts +120 -0
  49. package/lib/ModernIDB/utils.ts +51 -0
  50. package/lib/QRCode/QRCode.stories.tsx +41 -0
  51. package/lib/QRCode/QRCode.tsx +54 -0
  52. package/lib/QRCode/style.css.ts +23 -0
  53. package/lib/ReleaseInfo/index.tsx +29 -0
  54. package/lib/RichText/RichText.tsx +55 -0
  55. package/lib/RichText/style.css.ts +147 -0
  56. package/lib/RulesetResolver.ts +214 -0
  57. package/lib/SafariCheck/SafariCheck.stories.tsx +99 -0
  58. package/lib/SafariCheck/SafariCheck.tsx +273 -0
  59. package/lib/SafariCheck/addToDock.svg +13 -0
  60. package/lib/SafariCheck/addToHomeScreen.svg +12 -0
  61. package/lib/SafariCheck/safari.svg +32 -0
  62. package/lib/SafariCheck/shareIcon.svg +11 -0
  63. package/lib/SafariCheck/style.css.ts +106 -0
  64. package/lib/ServiceWorkerHandler.tsx +53 -0
  65. package/lib/ShareButton/ShareButton.stories.tsx +58 -0
  66. package/lib/ShareButton/ShareButton.tsx +153 -0
  67. package/lib/ShareButton/test.css.ts +3 -0
  68. package/lib/SubscribeCard/LetterheadInfoCard.tsx +23 -0
  69. package/lib/SubscribeCard/SubscribeByEmailCard.stories.tsx +69 -0
  70. package/lib/SubscribeCard/SubscribeByEmailCard.tsx +183 -0
  71. package/lib/SubscribeCard/SubscribeByPledgeCard.stories.tsx +133 -0
  72. package/lib/SubscribeCard/SubscribeByPledgeCard.tsx +127 -0
  73. package/lib/SubscribeCard/style.css.ts +14 -0
  74. package/lib/Sync/SyncIcon.stories.tsx +67 -0
  75. package/lib/Sync/SyncIcon.tsx +102 -0
  76. package/lib/Sync/SyncLog.tsx +222 -0
  77. package/lib/Sync/SyncLogList.stories.tsx +219 -0
  78. package/lib/Sync/style.css.ts +126 -0
  79. package/lib/account/AccountIcon.tsx +15 -0
  80. package/lib/account/AccountIssueView.tsx +44 -0
  81. package/lib/account/AlreadyLoggedInView.tsx +47 -0
  82. package/lib/account/CurrentUserFetcher.stories.tsx +292 -0
  83. package/lib/account/CurrentUserFetcher.tsx +118 -0
  84. package/lib/account/FailureFallbackView.tsx +36 -0
  85. package/lib/account/JoinCard.stories.tsx +257 -0
  86. package/lib/account/JoinCard.tsx +301 -0
  87. package/lib/account/LoadingView.tsx +14 -0
  88. package/lib/account/LoginCard.stories.tsx +288 -0
  89. package/lib/account/LoginCard.tsx +100 -0
  90. package/lib/account/LoginView.tsx +151 -0
  91. package/lib/account/NoConnectionView.tsx +34 -0
  92. package/lib/account/PasswordResetCard.stories.tsx +242 -0
  93. package/lib/account/PasswordResetCard.tsx +296 -0
  94. package/lib/account/UserMismatchView.tsx +62 -0
  95. package/lib/account/VerifyPage.tsx +195 -0
  96. package/lib/account/iconMask.svg +3 -0
  97. package/lib/account/style.css.ts +81 -0
  98. package/{dist/account/types.d.ts → lib/account/types.ts} +6 -3
  99. package/lib/account/useFetchCurrentUser.tsx +63 -0
  100. package/lib/account/useRedirectPath.ts +21 -0
  101. package/lib/animations.css.ts +17 -0
  102. package/lib/append-copy-to-text.ts +35 -0
  103. package/lib/async-op.ts +298 -0
  104. package/lib/atomic.css.ts +11 -0
  105. package/{dist/caught-value.d.ts → lib/caught-value.ts} +11 -1
  106. package/{dist/class-names.d.ts → lib/class-names.ts} +17 -6
  107. package/lib/client.ts +662 -0
  108. package/lib/common.css.ts +48 -0
  109. package/lib/copyrightRange.ts +10 -0
  110. package/lib/createSafeStorage.ts +91 -0
  111. package/lib/createStrictContext.ts +15 -0
  112. package/lib/failureMessages.ts +108 -0
  113. package/lib/form/FormSubmitButton.tsx +58 -0
  114. package/lib/form/SubmitErrorAlert.tsx +21 -0
  115. package/lib/form/style.css.ts +9 -0
  116. package/lib/globals.css.ts +62 -0
  117. package/lib/groupBy.ts +25 -0
  118. package/lib/hrefs.ts +48 -0
  119. package/lib/idToDate.ts +8 -0
  120. package/lib/ids.ts +6 -0
  121. package/lib/index.ts +78 -0
  122. package/lib/internal.css.ts +10 -0
  123. package/lib/layers.css.ts +3 -0
  124. package/lib/mailto.ts +40 -0
  125. package/lib/media.ts +50 -0
  126. package/lib/random.ts +19 -0
  127. package/lib/result/swr.ts +18 -0
  128. package/{dist/sleep.d.ts → lib/sleep.ts} +3 -1
  129. package/lib/store/index.tsx +292 -0
  130. package/lib/store/store.ts +482 -0
  131. package/lib/store/types.ts +45 -0
  132. package/lib/store/utils.ts +54 -0
  133. package/lib/storybook/decorators.tsx +10 -0
  134. package/lib/structs.ts +3 -0
  135. package/{dist/typeguards.d.ts → lib/typeguards.ts} +3 -1
  136. package/{dist/types.d.ts → lib/types.ts} +23 -12
  137. package/lib/unique.ts +24 -0
  138. package/lib/use-async-op.ts +16 -0
  139. package/lib/use-document-background-color.ts +16 -0
  140. package/lib/use-form.ts +78 -0
  141. package/lib/use-is-installed.ts +17 -0
  142. package/lib/use-media-query.ts +21 -0
  143. package/lib/use-reverting-state.ts +32 -0
  144. package/lib/use-scroll-restoration.ts +99 -0
  145. package/lib/useEnsureValue.ts +31 -0
  146. package/lib/useFetchJson.tsx +52 -0
  147. package/lib/useInvokeClient.ts +54 -0
  148. package/lib/useIsVisible.ts +27 -0
  149. package/lib/utm.ts +92 -0
  150. package/lib/validations.ts +25 -0
  151. package/lib/vars.css.ts +13 -0
  152. package/package.json +23 -29
  153. package/dist/AppConfig/AppConfig.d.ts +0 -29
  154. package/dist/AuthCard/AuthCard.d.ts +0 -10
  155. package/dist/AuthCard/AuthCard.stories.d.ts +0 -34
  156. package/dist/AuthCard/style.css.d.ts +0 -23
  157. package/dist/DialogTrigger/index.d.ts +0 -13
  158. package/dist/DocumentTitle/DocumentTitle.d.ts +0 -3
  159. package/dist/EnumMapper.d.ts +0 -25
  160. package/dist/ExternalLink.d.ts +0 -3
  161. package/dist/FullscreenDismissBlocker.d.ts +0 -5
  162. package/dist/IndieTabletopClubLogo.d.ts +0 -7
  163. package/dist/IndieTabletopClubSymbol.d.ts +0 -7
  164. package/dist/InfoPage/index.d.ts +0 -8
  165. package/dist/InfoPage/pages.d.ts +0 -2
  166. package/dist/InfoPage/style.css.d.ts +0 -5
  167. package/dist/Letterhead/index.d.ts +0 -19
  168. package/dist/Letterhead/stories.d.ts +0 -13
  169. package/dist/Letterhead/style.css.d.ts +0 -46
  170. package/dist/LetterheadForm/LetterheadReadonlyTextField.stories.d.ts +0 -17
  171. package/dist/LetterheadForm/LetterheadSubmitError.stories.d.ts +0 -11
  172. package/dist/LetterheadForm/LetterheadTextField.stories.d.ts +0 -336
  173. package/dist/LetterheadForm/index.d.ts +0 -44
  174. package/dist/LetterheadForm/style.css.d.ts +0 -8
  175. package/dist/LoadingIndicator.d.ts +0 -3
  176. package/dist/MiddotSeparated/MiddotSeparated.d.ts +0 -8
  177. package/dist/MiddotSeparated/MiddotSeparated.stories.d.ts +0 -586
  178. package/dist/MiddotSeparated/style.css.d.ts +0 -1
  179. package/dist/ModalDialog/index.d.ts +0 -12
  180. package/dist/ModalDialog/style.css.d.ts +0 -58
  181. package/dist/ModernIDB/Cursor.d.ts +0 -56
  182. package/dist/ModernIDB/ModernIDB.d.ts +0 -66
  183. package/dist/ModernIDB/ModernIDBError.d.ts +0 -3
  184. package/dist/ModernIDB/ObjectStore.d.ts +0 -112
  185. package/dist/ModernIDB/ObjectStoreIndex.d.ts +0 -53
  186. package/dist/ModernIDB/Transaction.d.ts +0 -16
  187. package/dist/ModernIDB/VersionChangeManager.d.ts +0 -30
  188. package/dist/ModernIDB/bindings/factory.d.ts +0 -12
  189. package/dist/ModernIDB/bindings/index.d.ts +0 -2
  190. package/dist/ModernIDB/bindings/utils.d.ts +0 -2
  191. package/dist/ModernIDB/index.d.ts +0 -10
  192. package/dist/ModernIDB/types.d.ts +0 -88
  193. package/dist/ModernIDB/utils.d.ts +0 -4
  194. package/dist/QRCode/QRCode.d.ts +0 -7
  195. package/dist/QRCode/QRCode.stories.d.ts +0 -33
  196. package/dist/QRCode/style.css.d.ts +0 -4
  197. package/dist/ReleaseInfo/index.d.ts +0 -5
  198. package/dist/RulesetResolver.d.ts +0 -87
  199. package/dist/SafariCheck/SafariCheck.d.ts +0 -23
  200. package/dist/SafariCheck/SafariCheck.stories.d.ts +0 -73
  201. package/dist/SafariCheck/style.css.d.ts +0 -17
  202. package/dist/ServiceWorkerHandler.d.ts +0 -11
  203. package/dist/ShareButton/ShareButton.d.ts +0 -57
  204. package/dist/ShareButton/ShareButton.stories.d.ts +0 -1577
  205. package/dist/ShareButton/test.css.d.ts +0 -1
  206. package/dist/SubscribeCard/LetterheadInfoCard.d.ts +0 -2
  207. package/dist/SubscribeCard/SubscribeByEmailCard.d.ts +0 -24
  208. package/dist/SubscribeCard/SubscribeByEmailCard.stories.d.ts +0 -10
  209. package/dist/SubscribeCard/SubscribeByPledgeCard.d.ts +0 -36
  210. package/dist/SubscribeCard/SubscribeByPledgeCard.stories.d.ts +0 -65
  211. package/dist/SubscribeCard/style.css.d.ts +0 -4
  212. package/dist/account/AccountIssueView.d.ts +0 -3
  213. package/dist/account/AlreadyLoggedInView.d.ts +0 -5
  214. package/dist/account/CurrentUserFetcher.d.ts +0 -20
  215. package/dist/account/CurrentUserFetcher.stories.d.ts +0 -136
  216. package/dist/account/FailureFallbackView.d.ts +0 -1
  217. package/dist/account/JoinCard.d.ts +0 -14
  218. package/dist/account/JoinCard.stories.d.ts +0 -143
  219. package/dist/account/LoadingView.d.ts +0 -1
  220. package/dist/account/LoginCard.d.ts +0 -39
  221. package/dist/account/LoginCard.stories.d.ts +0 -217
  222. package/dist/account/LoginView.d.ts +0 -10
  223. package/dist/account/NoConnectionView.d.ts +0 -4
  224. package/dist/account/PasswordResetCard.d.ts +0 -15
  225. package/dist/account/PasswordResetCard.stories.d.ts +0 -128
  226. package/dist/account/UserMismatchView.d.ts +0 -6
  227. package/dist/account/VerifyPage.d.ts +0 -13
  228. package/dist/account/style.css.d.ts +0 -10
  229. package/dist/account/useFetchCurrentUser.d.ts +0 -28
  230. package/dist/account/useRedirectPath.d.ts +0 -6
  231. package/dist/animations.css.d.ts +0 -3
  232. package/dist/append-copy-to-text.d.ts +0 -10
  233. package/dist/append-copy-to-text.test.d.ts +0 -1
  234. package/dist/appkit.css +0 -1
  235. package/dist/appkit.js +0 -10692
  236. package/dist/async-op.d.ts +0 -101
  237. package/dist/atomic.css.d.ts +0 -6
  238. package/dist/client.d.ts +0 -424
  239. package/dist/common.css.d.ts +0 -5
  240. package/dist/copyrightRange.d.ts +0 -1
  241. package/dist/copyrightRange.test.d.ts +0 -1
  242. package/dist/createSafeStorage.d.ts +0 -34
  243. package/dist/failureMessages.d.ts +0 -20
  244. package/dist/failureMessages.test.d.ts +0 -1
  245. package/dist/form/FormSubmitButton.d.ts +0 -17
  246. package/dist/form/SubmitErrorAlert.d.ts +0 -5
  247. package/dist/form/style.css.d.ts +0 -3
  248. package/dist/globals.css.d.ts +0 -0
  249. package/dist/groupBy.d.ts +0 -1
  250. package/dist/groupBy.test.d.ts +0 -1
  251. package/dist/hrefs.d.ts +0 -32
  252. package/dist/hrefs.test.d.ts +0 -1
  253. package/dist/idToDate.d.ts +0 -5
  254. package/dist/idToDate.test.d.ts +0 -1
  255. package/dist/ids.d.ts +0 -1
  256. package/dist/ids.test.d.ts +0 -1
  257. package/dist/index.d.ts +0 -64
  258. package/dist/internal.css.d.ts +0 -2
  259. package/dist/mailto.d.ts +0 -8
  260. package/dist/mailto.test.d.ts +0 -1
  261. package/dist/media.d.ts +0 -39
  262. package/dist/random.d.ts +0 -3
  263. package/dist/result/swr.d.ts +0 -4
  264. package/dist/store/index.d.ts +0 -237
  265. package/dist/store/store.d.ts +0 -144
  266. package/dist/store/types.d.ts +0 -49
  267. package/dist/store/utils.d.ts +0 -10
  268. package/dist/storybook/decorators.d.ts +0 -3
  269. package/dist/structs.d.ts +0 -1
  270. package/dist/typeguards.test.d.ts +0 -1
  271. package/dist/unique.d.ts +0 -10
  272. package/dist/unique.test.d.ts +0 -1
  273. package/dist/use-async-op.d.ts +0 -6
  274. package/dist/use-document-background-color.d.ts +0 -4
  275. package/dist/use-form.d.ts +0 -29
  276. package/dist/use-is-installed.d.ts +0 -8
  277. package/dist/use-media-query.d.ts +0 -1
  278. package/dist/use-reverting-state.d.ts +0 -5
  279. package/dist/use-scroll-restoration.d.ts +0 -25
  280. package/dist/useEnsureValue.d.ts +0 -6
  281. package/dist/useInvokeClient.d.ts +0 -25
  282. package/dist/useIsVisible.d.ts +0 -4
  283. package/dist/utm.d.ts +0 -58
  284. package/dist/utm.test.d.ts +0 -1
  285. package/dist/validations.d.ts +0 -3
  286. package/dist/vars.css.d.ts +0 -10
@@ -0,0 +1,153 @@
1
+ import { Button, type ButtonProps } from "@ariakit/react";
2
+ import { useState, type ReactNode } from "react";
3
+ import { useRevertingState } from "../use-reverting-state.ts";
4
+
5
+ type AriakitButtonProps = Omit<ButtonProps, "children" | "onClick" | "value">;
6
+
7
+ export type CopyToClipboardButtonProps = AriakitButtonProps & {
8
+ value: string | null;
9
+ children: ReactNode;
10
+ revertAfterMs: number;
11
+ };
12
+
13
+ export function CopyToClipboardButton(props: CopyToClipboardButtonProps) {
14
+ const { children, value, revertAfterMs, disabled, ...buttonProps } = props;
15
+ const [copiedTimestamp, setCopiedTimestamp] = useRevertingState<
16
+ number | null
17
+ >(null, revertAfterMs);
18
+
19
+ return (
20
+ <Button
21
+ {...buttonProps}
22
+ disabled={disabled || !value}
23
+ onClick={async () => {
24
+ if (value) {
25
+ await navigator.clipboard.writeText(value);
26
+ setCopiedTimestamp(Date.now());
27
+ }
28
+ }}
29
+ >
30
+ {copiedTimestamp ? "Copied!" : children}
31
+ </Button>
32
+ );
33
+ }
34
+
35
+ export type WebShareButtonProps = AriakitButtonProps & {
36
+ children: ReactNode;
37
+ shareData: ShareData | null;
38
+ revertAfterMs: number;
39
+ };
40
+
41
+ export function WebShareButton(props: WebShareButtonProps) {
42
+ const { shareData, children, revertAfterMs, disabled, ...buttonProps } =
43
+ props;
44
+
45
+ // Tracking sharing state to prevent duplicate invocations of
46
+ // the share dialog.
47
+ const [sharing, setSharing] = useState(false);
48
+
49
+ const [copiedTimestamp, setCopiedTimestamp] = useRevertingState<
50
+ number | null
51
+ >(null, revertAfterMs);
52
+
53
+ return (
54
+ <Button
55
+ {...buttonProps}
56
+ disabled={disabled || !shareData || sharing}
57
+ onClick={async () => {
58
+ if (shareData) {
59
+ try {
60
+ setSharing(true);
61
+
62
+ await navigator.share(shareData);
63
+ setCopiedTimestamp(Date.now());
64
+ } catch {
65
+ console.info("Not shared");
66
+ } finally {
67
+ setSharing(false);
68
+ }
69
+ }
70
+ }}
71
+ >
72
+ {copiedTimestamp ? "Shared!" : children}
73
+ </Button>
74
+ );
75
+ }
76
+
77
+ export type ShareButtonProps = AriakitButtonProps & {
78
+ /**
79
+ * Button label to use when copy to clipboard is used.
80
+ */
81
+ copyLabel: string;
82
+
83
+ /**
84
+ * Button label to use when web share is used.
85
+ */
86
+ shareLabel: string;
87
+
88
+ /**
89
+ * Data to be shared by the button.
90
+ *
91
+ * If using copy to clipboard, only the URL value will be used.
92
+ */
93
+ shareData: ShareData | null;
94
+
95
+ /**
96
+ * How long should the confirmation message be shown within the button?
97
+ *
98
+ * @default 1500
99
+ */
100
+ revertAfterMs?: number;
101
+
102
+ /**
103
+ * Should web share be used instead of copy to clipboard?
104
+ *
105
+ * By default, web share will be used if the browser supports it.
106
+ *
107
+ * @default !!navigator.share
108
+ */
109
+ webShare?: boolean;
110
+ };
111
+
112
+ /**
113
+ * An unstyled button that is pre-configured to either copy provided share data
114
+ * to clipboard, or to use web share, depending on which feature is detected
115
+ * in the browser where the button is running.
116
+ *
117
+ * Props that are not consumed by the Share Button itself are passed to the
118
+ * underlying Ariakit Button component.
119
+ *
120
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
121
+ */
122
+ export function ShareButton(props: ShareButtonProps) {
123
+ const {
124
+ copyLabel,
125
+ shareLabel,
126
+ shareData,
127
+ revertAfterMs = 1500,
128
+ webShare = !!navigator.share,
129
+ ...buttonProps
130
+ } = props;
131
+
132
+ if (webShare) {
133
+ return (
134
+ <WebShareButton
135
+ {...buttonProps}
136
+ shareData={shareData}
137
+ revertAfterMs={revertAfterMs}
138
+ >
139
+ {shareLabel}
140
+ </WebShareButton>
141
+ );
142
+ }
143
+
144
+ return (
145
+ <CopyToClipboardButton
146
+ {...buttonProps}
147
+ revertAfterMs={revertAfterMs}
148
+ value={shareData?.url ?? null}
149
+ >
150
+ {copyLabel}
151
+ </CopyToClipboardButton>
152
+ );
153
+ }
@@ -0,0 +1,3 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const test = style({ backgroundColor: "rebeccapurple", color: "wheat" });
@@ -0,0 +1,23 @@
1
+ import {
2
+ Letterhead,
3
+ LetterheadHeading,
4
+ LetterheadParagraph,
5
+ } from "../Letterhead/index.tsx";
6
+ import * as css from "./style.css.ts";
7
+ import type { ViewContent } from "./SubscribeByPledgeCard.tsx";
8
+
9
+ export function LetterheadInfoCard(props: ViewContent) {
10
+ return (
11
+ <Letterhead>
12
+ <LetterheadHeading className={css.card.heading}>
13
+ {props.title}
14
+ </LetterheadHeading>
15
+
16
+ {typeof props.description === "string" ? (
17
+ <LetterheadParagraph>{props.description}</LetterheadParagraph>
18
+ ) : (
19
+ props.description
20
+ )}
21
+ </Letterhead>
22
+ );
23
+ }
@@ -0,0 +1,69 @@
1
+ import { http, HttpResponse } from "msw";
2
+ import preview from "../../.storybook/preview.tsx";
3
+ import { LetterheadParagraph } from "../Letterhead/index.tsx";
4
+ import { sleep } from "../sleep.ts";
5
+ import { SubscribeByEmailCard } from "./SubscribeByEmailCard.tsx";
6
+
7
+ const handlers = {
8
+ subscribe() {
9
+ return http.post(
10
+ "http://mock.api/v1/newsletters/changelog/subscriptions",
11
+ async () => {
12
+ await sleep(2000);
13
+
14
+ return HttpResponse.json(
15
+ { tokenId: "1", message: "Ok" },
16
+ { status: 201 },
17
+ );
18
+ },
19
+ );
20
+ },
21
+ confirm() {
22
+ return http.put(
23
+ "http://mock.api/v1/newsletters/changelog/newsletter-signup-tokens/:tokenId",
24
+ async () => {
25
+ await sleep(2000);
26
+
27
+ return HttpResponse.json({ message: "Ok" }, { status: 201 });
28
+ },
29
+ );
30
+ },
31
+ };
32
+
33
+ const meta = preview.meta({
34
+ title: "Newsletter/Subscribe By Email Card",
35
+ component: SubscribeByEmailCard,
36
+ tags: ["autodocs"],
37
+ args: {
38
+ content: {
39
+ SUBSCRIBE: {
40
+ title: "Space Gits on Indie Tabletop Club",
41
+ description: (
42
+ <LetterheadParagraph>
43
+ Space Gits is getting a fancy <strong>gang builder</strong> and{" "}
44
+ <strong>smart rules</strong> in late 2025 in collaboration with
45
+ Indie Tabletop Club. Subscribe to ITC&apos;s{" "}
46
+ <em>at‑most‑one‑a‑month</em> newsletter and be among the first to
47
+ gain early access. It&apos;s gonna be rad.
48
+ </LetterheadParagraph>
49
+ ),
50
+ },
51
+ SUBSCRIBE_SUCCESS: {
52
+ title: "Subscribed!",
53
+ description:
54
+ "Thank you for being awesome. Once Space Gits is available for " +
55
+ "early access, you will be among the first to know!",
56
+ },
57
+ },
58
+ },
59
+ parameters: {
60
+ msw: {
61
+ handlers: {
62
+ subscribe: handlers.subscribe(),
63
+ confirm: handlers.confirm(),
64
+ },
65
+ },
66
+ },
67
+ });
68
+
69
+ export const Default = meta.story({});
@@ -0,0 +1,183 @@
1
+ import { Form } from "@ariakit/react";
2
+ import {
3
+ getSubmitFailureMessage,
4
+ Letterhead,
5
+ LetterheadFormActions,
6
+ LetterheadHeader,
7
+ LetterheadHeading,
8
+ LetterheadParagraph,
9
+ LetterheadSubmitButton,
10
+ LetterheadSubmitError,
11
+ LetterheadTextField,
12
+ useForm,
13
+ validEmail,
14
+ } from "@indietabletop/appkit";
15
+ import {
16
+ useState,
17
+ type Dispatch,
18
+ type ReactNode,
19
+ type SetStateAction,
20
+ } from "react";
21
+ import { useAppConfig, useClient } from "../AppConfig/AppConfig.tsx";
22
+ import { LetterheadInfoCard } from "./LetterheadInfoCard.tsx";
23
+ import * as css from "./style.css.ts";
24
+
25
+ type SetStep = Dispatch<SetStateAction<SubscribeStep>>;
26
+
27
+ function SubscribeView(
28
+ props: ViewContent & {
29
+ setStep: SetStep;
30
+ },
31
+ ) {
32
+ const { placeholders } = useAppConfig();
33
+ const { title, description, setStep } = props;
34
+
35
+ const client = useClient();
36
+ const { form, submitName } = useForm({
37
+ defaultValues: {
38
+ email: "",
39
+ },
40
+ validate: {
41
+ email: validEmail,
42
+ },
43
+ async onSubmit({ values }) {
44
+ const result = await client.subscribeToNewsletterByEmail(
45
+ "changelog",
46
+ values.email,
47
+ );
48
+
49
+ return result.mapFailure(getSubmitFailureMessage);
50
+ },
51
+ async onSuccess({ tokenId }, { values }) {
52
+ setStep({ type: "SUBMIT_CODE", email: values.email, tokenId });
53
+ },
54
+ });
55
+
56
+ return (
57
+ <Form store={form} resetOnSubmit={false}>
58
+ <Letterhead>
59
+ <LetterheadHeader>
60
+ <LetterheadHeading className={css.card.heading}>
61
+ {title}
62
+ </LetterheadHeading>
63
+
64
+ {description}
65
+ </LetterheadHeader>
66
+
67
+ <LetterheadTextField
68
+ name={form.names.email}
69
+ label="Your email"
70
+ placeholder={placeholders.email}
71
+ autoComplete="email"
72
+ required
73
+ />
74
+
75
+ <LetterheadFormActions>
76
+ <LetterheadSubmitError name={submitName} />
77
+ <LetterheadSubmitButton>Subscribe</LetterheadSubmitButton>
78
+ </LetterheadFormActions>
79
+ </Letterhead>
80
+ </Form>
81
+ );
82
+ }
83
+
84
+ function SubmitCodeStep(props: {
85
+ tokenId: string;
86
+ setStep: SetStep;
87
+ email: string;
88
+ }) {
89
+ const { tokenId, email, setStep } = props;
90
+ const client = useClient();
91
+
92
+ const { form, submitName } = useForm({
93
+ defaultValues: {
94
+ code: "",
95
+ },
96
+ async onSubmit({ values }) {
97
+ const op = await client.confirmNewsletterSignup(
98
+ "changelog",
99
+ tokenId,
100
+ values.code,
101
+ );
102
+
103
+ return op.mapFailure((failure) => {
104
+ return getSubmitFailureMessage(failure, {
105
+ 404: "🚫 This code is incorrect or expired. Please try again.",
106
+ });
107
+ });
108
+ },
109
+ onSuccess() {
110
+ setStep({ type: "SUBSCRIBE_SUCCESS" });
111
+ },
112
+ });
113
+
114
+ return (
115
+ <Form store={form} resetOnSubmit={false}>
116
+ <Letterhead>
117
+ <LetterheadHeader>
118
+ <LetterheadHeading>Confirm subscription</LetterheadHeading>
119
+ <LetterheadParagraph>
120
+ We've sent a one-time code to <em>{email}</em>. Please, enter the
121
+ code in the field below to confirm your newsletter subscription.
122
+ </LetterheadParagraph>
123
+ </LetterheadHeader>
124
+
125
+ <LetterheadTextField
126
+ name={form.names.code}
127
+ label="Code"
128
+ placeholder="E.g. 123123"
129
+ autoComplete="one-time-code"
130
+ required
131
+ />
132
+
133
+ <LetterheadFormActions>
134
+ <LetterheadSubmitError name={submitName} />
135
+ <LetterheadSubmitButton>Confirm</LetterheadSubmitButton>
136
+ </LetterheadFormActions>
137
+ </Letterhead>
138
+ </Form>
139
+ );
140
+ }
141
+
142
+ type SubscribeStep =
143
+ | { type: "SUBSCRIBE" }
144
+ | { type: "SUBMIT_CODE"; email: string; tokenId: string }
145
+ | { type: "SUBSCRIBE_SUCCESS" };
146
+
147
+ type ViewContent = {
148
+ title: ReactNode;
149
+ description: ReactNode;
150
+ };
151
+
152
+ /**
153
+ * Allows users to sign up to the newsletter using an arbitrary email.
154
+ *
155
+ * Signups are verified using an one-time code, which is sent to the supplied
156
+ * email address.
157
+ */
158
+ export function SubscribeByEmailCard(props: {
159
+ content: Record<Exclude<SubscribeStep["type"], "SUBMIT_CODE">, ViewContent>;
160
+ }) {
161
+ const { content } = props;
162
+ const [step, setStep] = useState<SubscribeStep>({ type: "SUBSCRIBE" });
163
+
164
+ switch (step.type) {
165
+ case "SUBSCRIBE": {
166
+ return <SubscribeView {...content[step.type]} setStep={setStep} />;
167
+ }
168
+
169
+ case "SUBMIT_CODE": {
170
+ return (
171
+ <SubmitCodeStep
172
+ email={step.email}
173
+ tokenId={step.tokenId}
174
+ setStep={setStep}
175
+ />
176
+ );
177
+ }
178
+
179
+ case "SUBSCRIBE_SUCCESS": {
180
+ return <LetterheadInfoCard {...content[step.type]} />;
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,133 @@
1
+ import { http, HttpResponse } from "msw";
2
+ import preview from "../../.storybook/preview.tsx";
3
+ import { LetterheadParagraph } from "../Letterhead/index.tsx";
4
+ import { sleep } from "../sleep.ts";
5
+ import { SubscribeByPledgeCard } from "./SubscribeByPledgeCard.tsx";
6
+
7
+ const pledge = {
8
+ id: "1",
9
+ email: "test@example.com",
10
+ contactSubscribed: false,
11
+ };
12
+
13
+ const subscribeByPledgeId = {
14
+ success() {
15
+ return http.post(
16
+ "http://mock.api/v1/newsletters/changelog/subscriptions",
17
+ async () => {
18
+ await sleep(2000);
19
+
20
+ return HttpResponse.json(
21
+ { message: "Added to newsletter" },
22
+ { status: 201 },
23
+ );
24
+ },
25
+ );
26
+ },
27
+ noConnection() {
28
+ return http.post(
29
+ "http://mock.api/v1/newsletters/changelog/subscriptions",
30
+ async () => {
31
+ return HttpResponse.error();
32
+ },
33
+ );
34
+ },
35
+ error() {
36
+ return http.post(
37
+ "http://mock.api/v1/newsletters/changelog/subscriptions",
38
+ async () => {
39
+ await sleep(2000);
40
+
41
+ return HttpResponse.text("Application error", { status: 500 });
42
+ },
43
+ );
44
+ },
45
+ };
46
+
47
+ /**
48
+ * Allows users to subscribe to ITC's newsletter using the email provided
49
+ * in their pledge.
50
+ */
51
+ const meta = preview.meta({
52
+ title: "Newsletter/Subscribe By Pledge Card",
53
+ component: SubscribeByPledgeCard,
54
+ tags: ["autodocs"],
55
+ args: {
56
+ pledge,
57
+ content: {
58
+ SUBSCRIBE: {
59
+ title: "Space Gits on Indie Tabletop Club",
60
+ description: (
61
+ <LetterheadParagraph>
62
+ Space Gits is getting a fancy <strong>gang builder</strong> and{" "}
63
+ <strong>smart rules</strong> in late 2025 in collaboration with
64
+ Indie Tabletop Club. Subscribe to ITC&apos;s{" "}
65
+ <em>at‑most‑one‑a‑month</em> newsletter and be among the first to
66
+ gain early access. It&apos;s gonna be rad.
67
+ </LetterheadParagraph>
68
+ ),
69
+ },
70
+ SUBSCRIBE_SUCCESS: {
71
+ title: "Subscribed!",
72
+ description:
73
+ "Thank you for being awesome. Once Space Gits is available for " +
74
+ "early access, you will be among the first to know!",
75
+ },
76
+ PREVIOUSLY_SUBSCRIBED: {
77
+ title: "Space Gits on Indie Tabletop Club",
78
+ description: (
79
+ <>
80
+ <LetterheadParagraph>
81
+ Space Gits is getting a fancy <strong>gang builder</strong> and{" "}
82
+ <strong>smart rules</strong> in late 2025 in collaboration with
83
+ Indie Tabletop Club.
84
+ </LetterheadParagraph>
85
+
86
+ <LetterheadParagraph>
87
+ Early access will be announced via ITC&apos;s newsletter, which
88
+ &mdash; according to our records &mdash; you are are already
89
+ subscribed to! Nothing left to do but await the good news in your
90
+ inbox!
91
+ </LetterheadParagraph>
92
+ </>
93
+ ),
94
+ },
95
+ },
96
+ },
97
+ parameters: {
98
+ msw: {
99
+ handlers: {
100
+ subscribeByPledgeId: subscribeByPledgeId.success(),
101
+ },
102
+ },
103
+ },
104
+ });
105
+
106
+ /**
107
+ * The default case, in which the user is not yet subscribed to ITC's newsletter
108
+ * and the submission is successful.
109
+ */
110
+ export const Default = meta.story({});
111
+
112
+ /**
113
+ * Same like the default case, but the form submission fails. Errors are
114
+ * differentiated using the `getSubmitFailureMessage` helper, so all the basics
115
+ * should be covered, as this page doesn't have special cases.
116
+ */
117
+ export const FailureOnSubmit = meta.story({
118
+ parameters: {
119
+ msw: {
120
+ handlers: {
121
+ subscribeByPledgeId: subscribeByPledgeId.error(),
122
+ },
123
+ },
124
+ },
125
+ });
126
+
127
+ /**
128
+ * In this case, the email address associated with this user account is
129
+ * already subscribed to our newsletter.
130
+ */
131
+ export const AlreadySubscribed = meta.story({
132
+ args: { pledge: { ...pledge, contactSubscribed: true } },
133
+ });
@@ -0,0 +1,127 @@
1
+ import { Form } from "@ariakit/react";
2
+ import {
3
+ getSubmitFailureMessage,
4
+ Letterhead,
5
+ LetterheadHeading,
6
+ LetterheadParagraph,
7
+ LetterheadSubmitButton,
8
+ LetterheadSubmitError,
9
+ useForm,
10
+ type RedeemedPledge,
11
+ } from "@indietabletop/appkit";
12
+ import { useState, type ReactNode } from "react";
13
+ import { useClient } from "../AppConfig/AppConfig.tsx";
14
+ import { LetterheadInfoCard } from "./LetterheadInfoCard.tsx";
15
+ import * as css from "./style.css.ts";
16
+
17
+ type Pledge = Pick<RedeemedPledge, "id" | "email" | "contactSubscribed">;
18
+
19
+ function SubscribeView(
20
+ props: ViewContent & {
21
+ onSuccess: () => void;
22
+ pledge: Pledge;
23
+ },
24
+ ) {
25
+ const { pledge, title, description, onSuccess } = props;
26
+
27
+ const client = useClient();
28
+ const { form, submitName } = useForm({
29
+ defaultValues: {},
30
+ onSuccess,
31
+ async onSubmit() {
32
+ const result = await client.subscribeToNewsletterByPledgeId(
33
+ "changelog",
34
+ pledge.id,
35
+ );
36
+
37
+ return result.mapFailure(getSubmitFailureMessage);
38
+ },
39
+ });
40
+
41
+ return (
42
+ <Letterhead>
43
+ <LetterheadHeading className={css.card.heading}>
44
+ {title}
45
+ </LetterheadHeading>
46
+
47
+ {description}
48
+
49
+ <Form store={form} className={css.card.form}>
50
+ <LetterheadSubmitError name={submitName} />
51
+
52
+ <LetterheadSubmitButton>Subscribe</LetterheadSubmitButton>
53
+
54
+ <LetterheadParagraph size="small">
55
+ Using {pledge.email} from your pledge.
56
+ </LetterheadParagraph>
57
+ </Form>
58
+ </Letterhead>
59
+ );
60
+ }
61
+
62
+ type SubscribeStep =
63
+ | "PREVIOUSLY_SUBSCRIBED"
64
+ | "SUBSCRIBE"
65
+ | "SUBSCRIBE_SUCCESS";
66
+
67
+ export type ViewContent = {
68
+ title: ReactNode;
69
+ description: ReactNode;
70
+ };
71
+
72
+ /**
73
+ * Allows users to sign up to the newsletter using the email associated with
74
+ * their pledge ID.
75
+ *
76
+ * Emails are not verified using a one-time code, as the user has just
77
+ * implicitly verified their address by navigating to the page with the unique
78
+ * pledge ID.
79
+ */
80
+ export function SubscribeByPledgeCard(props: {
81
+ /**
82
+ * The pledge data that will be used to determine whether the user should
83
+ * be prompted for signup.
84
+ */
85
+ pledge: Pledge;
86
+
87
+ /**
88
+ * Content for each of the subscribe steps. You probably want to customize it
89
+ * for each step, so it is left to be defined at point of usage.
90
+ *
91
+ * If the `description` is a string, it will be wrapped in
92
+ * `<LetterheadParagraph>`, otherwise it will be left as is.
93
+ */
94
+ content: Record<SubscribeStep, ViewContent>;
95
+ }) {
96
+ const { pledge } = props;
97
+
98
+ const [step, setStep] = useState<SubscribeStep>(
99
+ pledge.contactSubscribed ? "PREVIOUSLY_SUBSCRIBED" : "SUBSCRIBE",
100
+ );
101
+
102
+ const content = props.content[step];
103
+
104
+ switch (step) {
105
+ case "SUBSCRIBE": {
106
+ return (
107
+ <SubscribeView
108
+ {...props}
109
+ {...content}
110
+ onSuccess={() => void setStep("SUBSCRIBE_SUCCESS")}
111
+ />
112
+ );
113
+ }
114
+
115
+ case "PREVIOUSLY_SUBSCRIBED":
116
+ case "SUBSCRIBE_SUCCESS": {
117
+ return <LetterheadInfoCard {...content} />;
118
+ }
119
+ }
120
+ }
121
+
122
+ export {
123
+ /**
124
+ * @deprecated Use {@link SubscribeByPledgeCard}
125
+ */
126
+ SubscribeByPledgeCard as SubscribeCard,
127
+ };
@@ -0,0 +1,14 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const card = {
4
+ heading: style({
5
+ maxInlineSize: "20rem",
6
+ marginInline: "auto",
7
+ }),
8
+ form: style({
9
+ marginBlockStart: "2.5rem",
10
+ display: "flex",
11
+ flexDirection: "column",
12
+ gap: "1rem",
13
+ }),
14
+ };