@indietabletop/appkit 5.5.1 → 6.1.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.
@@ -3,7 +3,19 @@ import type { IndieTabletopClient } from "../client.ts";
3
3
  import type { AppHrefs } from "../hrefs.ts";
4
4
 
5
5
  export type AppConfig = {
6
- appName: string;
6
+ app: {
7
+ /**
8
+ * The app's name, e.g. Space Gits. Do not include the word App in this
9
+ * title -- it will be added automatically in places where it's needed.
10
+ */
11
+ name: string;
12
+
13
+ /**
14
+ * The URL to the app's icon. This should be a maskable-style icon. In
15
+ * other words, it should not include its own transparency.
16
+ */
17
+ icon: string;
18
+ };
7
19
  isDev: boolean;
8
20
  client: IndieTabletopClient;
9
21
  hrefs: AppHrefs;
@@ -114,6 +114,7 @@ export const button = recipe({
114
114
  borderRadius: "0.5rem",
115
115
  padding: "1rem 1.5rem",
116
116
  fontSize: "0.875rem",
117
+ textAlign: "center",
117
118
 
118
119
  "@media": {
119
120
  [Hover.HOVER]: {
@@ -1,6 +1,7 @@
1
1
  import { style } from "@vanilla-extract/css";
2
2
  import { recipe } from "@vanilla-extract/recipes";
3
3
  import { MinWidth } from "../media.ts";
4
+ import { ZIndex } from "../vars.css.ts";
4
5
 
5
6
  const scaleTransition = {
6
7
  transition: "transform 200ms, opacity 200ms",
@@ -40,7 +41,7 @@ export const dialog = recipe({
40
41
  base: {
41
42
  position: "fixed",
42
43
  inset: 0,
43
- zIndex: 100,
44
+ zIndex: ZIndex.DIALOG,
44
45
  margin: "auto",
45
46
  overflow: "auto",
46
47
  opacity: 0,
@@ -0,0 +1,99 @@
1
+ import { http, HttpResponse } from "msw";
2
+ import preview from "../../.storybook/preview.tsx";
3
+ import { sleep } from "../sleep.ts";
4
+ import { SafariCheck } from "./SafariCheck.tsx";
5
+
6
+ function Content() {
7
+ return (
8
+ <div>
9
+ <h1>Safari Check passed</h1>
10
+ <button
11
+ onClick={() => {
12
+ localStorage.clear();
13
+ window.location.reload();
14
+ }}
15
+ >
16
+ Clear local storage
17
+ </button>
18
+ </div>
19
+ );
20
+ }
21
+
22
+ function userAgentResponse(params: { browserName: string }) {
23
+ return http.get("http://mock.api/ua", async () => {
24
+ await sleep(1500);
25
+ return HttpResponse.json({
26
+ browser: { name: params.browserName },
27
+ device: params.browserName.includes("Mobile") ? { type: "mobile" } : {},
28
+ });
29
+ });
30
+ }
31
+
32
+ const meta = preview.meta({
33
+ title: "Components/SafariCheck",
34
+ component: SafariCheck,
35
+ tags: ["autodocs"],
36
+ decorators: [
37
+ (Story) => (
38
+ <div style={{ height: "100svh", display: "grid" }}>
39
+ <Story />
40
+ </div>
41
+ ),
42
+ ],
43
+ parameters: {
44
+ msw: {
45
+ handlers: {
46
+ ua: userAgentResponse({ browserName: "Safari" }),
47
+ },
48
+ },
49
+ },
50
+ });
51
+
52
+ export const SafariDesktop = meta.story({
53
+ args: {
54
+ children: <Content />,
55
+ },
56
+ parameters: {
57
+ msw: {
58
+ handlers: {
59
+ ua: userAgentResponse({ browserName: "Safari" }),
60
+ },
61
+ },
62
+ },
63
+ });
64
+
65
+ export const SafariMobile = meta.story({
66
+ args: {
67
+ children: <Content />,
68
+ },
69
+ parameters: {
70
+ msw: {
71
+ handlers: {
72
+ ua: userAgentResponse({ browserName: "Safari Mobile" }),
73
+ },
74
+ },
75
+ },
76
+ });
77
+
78
+ export const OtherBrowser = meta.story({
79
+ args: {
80
+ children: <Content />,
81
+ },
82
+ parameters: {
83
+ msw: {
84
+ handlers: {
85
+ ua: userAgentResponse({ browserName: "Chrome" }),
86
+ },
87
+ },
88
+ },
89
+ });
90
+
91
+ /**
92
+ * The check behaviour has been disabled.
93
+ */
94
+ export const Disabled = meta.story({
95
+ args: {
96
+ children: <Content />,
97
+ performCheck: false,
98
+ },
99
+ });
@@ -0,0 +1,273 @@
1
+ import { Button, DialogDisclosure, DialogDismiss } from "@ariakit/react";
2
+ import { useCallback, useMemo, useState, type ReactNode } from "react";
3
+ import { number } from "superstruct";
4
+ import useImmutableSWR from "swr/immutable";
5
+ import { Link } from "wouter";
6
+ import { useAppConfig, useClient } from "../AppConfig/AppConfig.tsx";
7
+ import { Failure, Pending, Success } from "../async-op.ts";
8
+ import { interactiveText } from "../common.css.ts";
9
+ import { createSafeStorage } from "../createSafeStorage.ts";
10
+ import { DialogTrigger } from "../DialogTrigger/index.tsx";
11
+ import {
12
+ LetterheadFooter,
13
+ LetterheadHeading,
14
+ LetterheadParagraph,
15
+ } from "../Letterhead/index.tsx";
16
+ import { button } from "../Letterhead/style.css.ts";
17
+ import { LoadingIndicator } from "../LoadingIndicator.tsx";
18
+ import { ModalDialog } from "../ModalDialog/index.tsx";
19
+ import { swrResponseToResult } from "../result/swr.ts";
20
+ import { useCurrentUser } from "../store/index.tsx";
21
+ import type { FailurePayload } from "../types.ts";
22
+ import { useIsInstalled } from "../use-is-installed.ts";
23
+ import addToDockUrl from "./addToDock.svg";
24
+ import addToHomeScreenUrl from "./addToHomeScreen.svg";
25
+ import safariUrl from "./safari.svg";
26
+ import shareIconUrl from "./shareIcon.svg";
27
+ import * as css from "./style.css.ts";
28
+
29
+ function InstallAppDialog({ isMobile }: { isMobile?: boolean }) {
30
+ const config = useAppConfig();
31
+
32
+ const shareIcon = <img src={shareIconUrl} alt="" className={css.icon} />;
33
+ const addIcon = <img src={addToHomeScreenUrl} alt="" className={css.icon} />;
34
+ const addToDockIcon = <img src={addToDockUrl} alt="" className={css.icon} />;
35
+
36
+ const steps = [
37
+ <>
38
+ Open the <strong>Share</strong> menu {shareIcon}
39
+ </>,
40
+
41
+ isMobile ? (
42
+ <>
43
+ Tap <strong>Add to Home Screen</strong> {addIcon}
44
+ </>
45
+ ) : (
46
+ <>
47
+ Click <strong>Add to Dock</strong> {addToDockIcon}
48
+ </>
49
+ ),
50
+
51
+ isMobile ? (
52
+ <>
53
+ Tap <strong>Add</strong> in the top right corner
54
+ </>
55
+ ) : (
56
+ <>
57
+ Click <strong>Add</strong>
58
+ </>
59
+ ),
60
+ ];
61
+
62
+ return (
63
+ <ModalDialog size="small" className={css.dialog}>
64
+ <img
65
+ src={config.app.icon}
66
+ alt=""
67
+ width="512"
68
+ height="512"
69
+ className={css.appIcon}
70
+ />
71
+
72
+ <LetterheadHeading className={css.heading}>
73
+ Install {config.app.name}
74
+ </LetterheadHeading>
75
+
76
+ <LetterheadParagraph className={css.intro}>
77
+ To install this app, follow these 3 steps:
78
+ </LetterheadParagraph>
79
+
80
+ <ol className={css.steps}>
81
+ {steps.map((step, index) => {
82
+ return (
83
+ <li className={css.step} key={index}>
84
+ {step}
85
+ </li>
86
+ );
87
+ })}
88
+ </ol>
89
+
90
+ <div>
91
+ <DialogDismiss className={interactiveText}>Dismiss</DialogDismiss>
92
+ </div>
93
+ </ModalDialog>
94
+ );
95
+ }
96
+
97
+ function SafariPrompt(props: { onDismiss: () => void; isMobile?: boolean }) {
98
+ const { hrefs } = useAppConfig();
99
+
100
+ const delete_your_local_data = <strong>delete your local data</strong>;
101
+
102
+ const install_the_app = <strong>install the app</strong>;
103
+
104
+ const creating_an_account = <strong>creating an account</strong>;
105
+
106
+ return (
107
+ <div className={css.safariPrompt}>
108
+ <div className={css.safariPromptHeader}>
109
+ <img
110
+ src={safariUrl}
111
+ alt=""
112
+ width="64"
113
+ height="64"
114
+ className={css.safariLogo}
115
+ />
116
+
117
+ <LetterheadHeading className={css.emptyStateHeading}>
118
+ Heads up Safari users!
119
+ </LetterheadHeading>
120
+ </div>
121
+ <LetterheadParagraph>
122
+ Safari — unlike other browsers — will {delete_your_local_data} after 7
123
+ days of inactivity, unless you have installed the app.
124
+ </LetterheadParagraph>
125
+ <LetterheadParagraph>
126
+ To prevent accidental data loss, you can either {install_the_app}, make
127
+ sure all your data is safely backed up by {creating_an_account}, or use
128
+ a different browser.
129
+ </LetterheadParagraph>
130
+
131
+ <div className={css.safariPromptActions}>
132
+ <DialogTrigger>
133
+ <InstallAppDialog isMobile={props.isMobile} />
134
+
135
+ <DialogDisclosure className={button()}>Install App</DialogDisclosure>
136
+ </DialogTrigger>
137
+
138
+ <Link className={button()} href={hrefs.join()}>
139
+ Create Account
140
+ </Link>
141
+ </div>
142
+
143
+ <div className={css.safariDismissArea}>
144
+ <LetterheadParagraph align="center">
145
+ <Button className={interactiveText} onClick={props.onDismiss}>
146
+ I don’t mind if my data gets deleted
147
+ </Button>
148
+ </LetterheadParagraph>
149
+ </div>
150
+
151
+ <LetterheadFooter />
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function useUserAgent(props: { performFetch: boolean }) {
157
+ const client = useClient();
158
+
159
+ const swr = useImmutableSWR(
160
+ props.performFetch ? "/ua" : null,
161
+ useCallback(() => client.userAgent(), []),
162
+ );
163
+
164
+ return swrResponseToResult(swr);
165
+ }
166
+
167
+ const storage = createSafeStorage({ safariInstallPromptDismissedAt: number() });
168
+
169
+ function useInstallPromptState() {
170
+ const [dismissedAt, setState] = useState(() =>
171
+ storage.getItem("safariInstallPromptDismissedAt"),
172
+ );
173
+
174
+ return {
175
+ dismissedAt,
176
+ setDismissed: useCallback(() => {
177
+ const timestamp = Date.now();
178
+
179
+ setState(timestamp);
180
+ storage.setItem("safariInstallPromptDismissedAt", timestamp);
181
+ }, []),
182
+ };
183
+ }
184
+
185
+ type SafariCheckResult =
186
+ | Success<{ showPrompt: boolean; isMobile?: boolean }>
187
+ | Failure<FailurePayload>
188
+ | Pending;
189
+
190
+ function useSafariCheck({ performCheck }: { performCheck: boolean }) {
191
+ const currentUser = useCurrentUser();
192
+ const isInstalled = useIsInstalled();
193
+ const { dismissedAt, setDismissed } = useInstallPromptState();
194
+ const installedOrDismissed = isInstalled || !!dismissedAt;
195
+ const userAgentResult = useUserAgent({
196
+ performFetch: performCheck && !currentUser && !installedOrDismissed,
197
+ });
198
+
199
+ const result = useMemo((): SafariCheckResult => {
200
+ return installedOrDismissed || !performCheck
201
+ ? // If the safari prompt was previously dismissed, the app is already
202
+ // installed, or check was disabled explicitly, we skip the prompt.
203
+ new Success({ showPrompt: false })
204
+ : // Otherwise, we want to check whether the name includes Safari (could be
205
+ // Safari or Safari Mobile), and we also report whether this is a mobile
206
+ // device to allow for more targeted install instructions for the user.
207
+ userAgentResult.mapSuccess((value) => {
208
+ return {
209
+ showPrompt: value.browser?.name?.includes("Safari") ?? false,
210
+ isMobile: value.device?.type === "mobile",
211
+ };
212
+ });
213
+ }, [installedOrDismissed, userAgentResult, performCheck]);
214
+
215
+ return { result, setDismissed };
216
+ }
217
+
218
+ /**
219
+ * Checks whether the browser which is running the app is Safari/Safari Mobile
220
+ * and warns the user that their data might be deleted due to inactivity.
221
+ *
222
+ * The warning will be shown if:
223
+ *
224
+ * - The check has not been explicitly disabled via `performCheck: false`
225
+ * - The warning has not been previously dismissed (as reported by localStorage)
226
+ * - The app is not installed (Safari behaves differently in that case)
227
+ * - The user is not already logged in (as data will be safely backed up that way)
228
+ */
229
+ export function SafariCheck(props: {
230
+ children: ReactNode;
231
+
232
+ /**
233
+ * Optionally opt out of the check.
234
+ *
235
+ * This can be useful in cases where, e.g. the user already has some content
236
+ * in the app and we want to avoid checking the user agent, but it is
237
+ * impractical to entirely avoid rendering this component.
238
+ */
239
+ performCheck?: boolean;
240
+ }) {
241
+ const { result, setDismissed } = useSafariCheck({
242
+ performCheck: props.performCheck ?? true,
243
+ });
244
+
245
+ if (result.isPending) {
246
+ return (
247
+ <div className={css.container}>
248
+ <div className={css.compatCheckLoaderContainer}>
249
+ <LoadingIndicator className={css.indicator} />
250
+
251
+ {/* Intentionally not using Letterhead Paragraph to pick up the default
252
+ font size and style. */}
253
+ <p className={css.compatCheckLoaderText}>Checking compatibility...</p>
254
+ </div>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ // Get the success value. Note that if the UA check is a failure we don't
260
+ // want to bother the user with any failure messages. In that case, we just
261
+ // keep rolling as if the showPrompt was false.
262
+ const value = result.valueOrNull();
263
+ if (value?.showPrompt) {
264
+ return (
265
+ <div className={css.container}>
266
+ <SafariPrompt onDismiss={setDismissed} isMobile={value.isMobile} />
267
+ </div>
268
+ );
269
+ }
270
+
271
+ // We're all good, render children.
272
+ return <>{props.children}</>;
273
+ }
@@ -0,0 +1,13 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g clip-path="url(#clip0_277_21)">
3
+ <rect width="16" height="16" fill="none"/>
4
+ <rect x="1.5" y="3" width="13" height="10" rx="1" stroke="black"/>
5
+ <path d="M2 5H14" stroke="black"/>
6
+ <rect x="3.5" y="9.5" width="9" height="2" rx="0.5" fill="black"/>
7
+ </g>
8
+ <defs>
9
+ <clipPath id="clip0_277_21">
10
+ <rect width="16" height="16" fill="none"/>
11
+ </clipPath>
12
+ </defs>
13
+ </svg>
@@ -0,0 +1,12 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g clip-path="url(#clip0_276_9)">
3
+ <rect width="16" height="16" fill="none"/>
4
+ <rect x="2.5" y="2.5" width="11" height="11" rx="2" stroke="black"/>
5
+ <path d="M5 8H8M8 8H11M8 8V11M8 8V5" stroke="black"/>
6
+ </g>
7
+ <defs>
8
+ <clipPath id="clip0_276_9">
9
+ <rect width="16" height="16" fill="none"/>
10
+ </clipPath>
11
+ </defs>
12
+ </svg>
@@ -0,0 +1,32 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="66.165833mm" height="65.803795mm" viewBox="0 0 66.165833 65.803795">
3
+ <defs>
4
+ <linearGradient id="b">
5
+ <stop offset="0" stop-color="#06c2e7"/>
6
+ <stop offset=".25000015" stop-color="#0db8ec"/>
7
+ <stop offset=".5000003" stop-color="#12aef1"/>
8
+ <stop offset=".75000012" stop-color="#1f86f9"/>
9
+ <stop offset="1" stop-color="#107ddd"/>
10
+ </linearGradient>
11
+ <linearGradient id="a">
12
+ <stop offset="0" stop-color="#bdbdbd"/>
13
+ <stop offset="1" stop-color="#fff"/>
14
+ </linearGradient>
15
+ <linearGradient xlink:href="#a" id="d" x1="412.97501" x2="412.97501" y1="237.60777" y2="59.392235" gradientTransform="translate(206.79018 159.77261) scale(.35154)" gradientUnits="userSpaceOnUse"/>
16
+ <filter id="f" width="1.0418189" height="1.0446756" x="-.02090938" y="-.0223378" color-interpolation-filters="sRGB">
17
+ <feGaussianBlur stdDeviation=".95767362"/>
18
+ </filter>
19
+ <filter id="c" width="1.096" height="1.096" x="-.048" y="-.048" color-interpolation-filters="sRGB">
20
+ <feGaussianBlur stdDeviation="3.5643107"/>
21
+ </filter>
22
+ <radialGradient xlink:href="#b" id="e" cx="413.06128" cy="136.81819" r="82.125351" fx="413.06128" fy="136.81819" gradientTransform="translate(194.54473 155.58044) scale(.38143)" gradientUnits="userSpaceOnUse"/>
23
+ </defs>
24
+ <path d="M502.08277 148.5a89.107765 89.107765 0 0 1-89.10777 89.10777A89.107765 89.107765 0 0 1 323.86724 148.5 89.107765 89.107765 0 0 1 412.975 59.392235 89.107765 89.107765 0 0 1 502.08277 148.5Z" filter="url(#c)" opacity=".52999998" paint-order="markers stroke fill" transform="matrix(.33865 0 0 .3261 -106.76956 -14.47833)"/>
25
+ <path fill="url(#d)" stroke="#cdcdcd" stroke-linecap="round" stroke-linejoin="round" stroke-width=".09301235" d="M383.29373 211.97671a31.325188 31.325188 0 0 1-31.32519 31.32519 31.325188 31.325188 0 0 1-31.32518-31.32519 31.325188 31.325188 0 0 1 31.32518-31.32519 31.325188 31.325188 0 0 1 31.32519 31.32519z" paint-order="markers stroke fill" transform="translate(-318.88562 -180.59501)"/>
26
+ <path fill="url(#e)" d="M380.83911 211.97671a28.870571 28.870571 0 0 1-28.87057 28.87057 28.870571 28.870571 0 0 1-28.87057-28.87057 28.870571 28.870571 0 0 1 28.87057-28.87057 28.870571 28.870571 0 0 1 28.87057 28.87057z" paint-order="markers stroke fill" transform="translate(-318.88562 -180.59501)"/>
27
+ <path fill="#f4f2f3" d="M33.08292 4.01671c-.23319 0-.42092.18772-.42092.42092V9.2928c0 .2332.18773.42092.42092.42092.2332 0 .42092-.18772.42092-.42092V4.43763c0-.2332-.18772-.42092-.42092-.42092zm-2.75367.17404c-.0279-.003-.0566-.003-.0856.00035-.23194.0242-.39917.2304-.37495.46234l.21218 2.03119c.0242.23194.23041.39918.46233.37496.23195-.0242.39919-.2304.37496-.46234l-.212-2.03118c-.0212-.20295-.18177-.35637-.37695-.37532zm5.5266.002c-.19519.0188-.35578.17221-.37714.37513l-.21363 2.03102c-.0244.23192.14285.43831.37478.4627.23191.0244.43811-.14268.46251-.3746l.21364-2.03119c.0244-.23192-.14286-.43814-.37478-.46252-.029-.003-.0575-.003-.0854-.00052zm-8.3553.4082c-.028.00022-.0565.003-.085.009-.22814.0483-.37294.27089-.32464.49903l1.00552 4.74981c.0483.22814.27088.37293.49902.32464.22814-.0483.37294-.27072.32465-.49886l-1.00552-4.74998c-.0423-.19963-.21792-.33543-.41401-.3339zm11.18382.004c-.19609-.002-.3718.13394-.41419.33353l-1.00897 4.74925c-.0485.22811.0962.45076.32427.49922.22811.0485.45076-.0962.49921-.32428l1.00897-4.74926c.0485-.2281-.0962-.45076-.32427-.49921-.0285-.006-.057-.009-.085-.009zM24.801 5.36212c-.0545-.005-.11077.001-.16622.0194-.22178.0721-.34238.3085-.27031.53028l.6311 1.94236c.0721.22179.30868.34238.53046.27032.22179-.0721.3422-.30868.27013-.53046l-.63109-1.94236c-.054-.16634-.20059-.27568-.36407-.28958zm16.56765.001c-.16348.0139-.30999.12324-.36406.28957l-.63147 1.94218c-.0721.22177.0484.45837.27014.53046.22178.0721.45837-.0484.53047-.27013l.63146-1.94236c.0721-.22178-.0484-.45837-.27014-.53046-.0554-.018-.11191-.0239-.1664-.0193zm-19.23721.9759c-.0547.001-.11004.013-.16331.0367-.21298.0947-.30836.34244-.21364.55553l1.97197 4.43662c.0947.21308.34244.30836.55553.21364.21298-.0947.30854-.34244.21382-.55553l-1.97216-4.43662c-.071-.15983-.22817-.25351-.39221-.25033zm21.93693.0149c-.16403-.003-.32132.0901-.39257.24979l-1.97798 4.4339c-.095.21296-.00004.46088.21292.55589.21297.095.46088.00005.5559-.21291L44.4446 6.9467c.095-.21297.00005-.46089-.21291-.5559-.0532-.0237-.10864-.0357-.16332-.0369zM19.65353 7.6501c-.0808-.006-.16406.012-.23979.0558-.20196.1166-.27065.37302-.15406.57497l1.02115 1.76869c.1166.20196.373.27065.57496.15405.20195-.1166.27065-.37301.15406-.57497L19.9887 7.85996c-.0729-.12623-.20047-.20041-.33517-.20983zm26.85877 0c-.13468.009-.26211.0836-.33498.20983l-1.02133 1.76868c-.1166.20196-.0477.45837.15424.57497.20196.1166.45837.0479.57497-.15405l1.02114-1.76869c.1166-.20195.0479-.45837-.15406-.57497-.0757-.0437-.15916-.0614-.23998-.0558zM17.24739 9.15083c-.081.003-.16211.029-.2329.0803-.18875.13693-.23048.39911-.0935.58787l2.85086 3.92995c.13693.18876.39929.23049.58805.0936.18876-.13693.23049-.39911.0935-.58787l-2.85104-3.92995c-.0856-.11798-.22004-.17847-.35497-.17386zm31.70122.0214c-.13493-.005-.26941.0555-.35516.17331l-2.8563 3.92614c-.1372.18857-.0958.45086.0928.58805.18858.13718.45087.0959.58806-.0926l2.85613-3.92614c.13718-.18858.0957-.45086-.0928-.58805-.0707-.0514-.15176-.0778-.23272-.0807zm-33.85196 1.78231c-.10744-.006-.21708.0299-.30374.10791-.17332.15602-.18725.42109-.0312.59441l1.36648 1.51799c.15601.17332.42109.18726.59441.0312.17332-.15602.18726-.42127.0312-.59459l-1.3663-1.51781c-.078-.0867-.18339-.13351-.29085-.13916zm35.97562.003c-.10745.006-.21282.0525-.29084.13915l-1.36648 1.51763c-.15606.1733-.14224.43855.0311.59459.17329.15604.43837.14205.59441-.0312l1.36666-1.51762c.15605-.17331.14205-.43856-.0312-.59459-.0867-.078-.19611-.11354-.30357-.10791zm-38.03696 1.97705c-.10745.006-.21266.0525-.29067.13916-.15602.17332-.14207.43839.0312.59441l3.60841 3.24834c.17332.15603.43839.14207.5944-.0312.15603-.17331.14226-.43839-.0311-.59441l-3.60858-3.24834c-.0867-.078-.1963-.11356-.30376-.10791zm40.10831.0142c-.10745-.006-.21722.0298-.30393.10773l-3.61059 3.24581c-.17342.15589-.18768.42097-.0318.5944.1559.17342.42117.18751.59459.0316l3.61077-3.2458c.17342-.1559.1875-.42098.0316-.59441-.078-.0867-.18322-.13361-.29066-.13933zm-41.8225 2.18998c-.13494-.005-.26949.0558-.35515.17367-.13707.18866-.0955.4508.0932.58787l1.65224 1.20044c.18866.13708.45079.0957.58786-.093.13708-.18866.0956-.45098-.093-.58805l-1.65224-1.20044c-.0707-.0514-.15193-.0776-.23289-.0805zm43.53505.0153c-.081.003-.16211.0289-.23289.0803l-1.65297 1.19936c-.18875.13694-.2305.39929-.0936.58805.13695.18875.39912.23031.58787.0934l1.65316-1.19935c.18875-.13694.23031-.39912.0934-.58787-.0856-.11797-.22004-.17847-.35497-.17385zM9.7192 17.48992c-.13469.009-.26211.0836-.33499.20982-.1166.20195-.0479.45837.15405.57497l4.20463 2.42758c.20195.1166.45837.0479.57497-.15405.1166-.20195.0479-.45837-.15405-.57497l-4.20463-2.42759c-.0757-.0437-.15917-.0614-.23998-.0558zm46.72744 0c-.0808-.006-.16425.012-.23998.0558l-4.20463 2.42759c-.20195.1166-.27065.37302-.15405.57497.1166.20195.37302.27065.57497.15405l4.20482-2.42758c.20195-.1166.27064-.37302.15404-.57497-.0729-.12622-.20048-.20041-.33517-.20982zm-47.9386 2.50606c-.16403-.004-.32133.0899-.39258.2496-.095.21298-.00006.46091.21292.5559l1.86532.83202c.21298.095.46091.00007.5559-.2129.095-.21298-.00012-.46091-.21309-.5559l-1.86515-.83202c-.0532-.0238-.10865-.0356-.16332-.0367zm49.15794.0173c-.0547.001-.11024.013-.16351.0367l-1.86569.83057c-.21304.0949-.3083.34267-.21346.55571.0949.21304.34286.3083.5559.21346l1.8657-.83076c.21303-.0948.30811-.34267.21327-.55571-.0711-.15978-.22818-.25323-.39221-.24997zM7.42859 22.61527c-.16349.0137-.31006.12291-.36424.28921-.0722.22172.048.45839.26977.53064l4.61629 1.50418c.22171.0722.45839-.0481.53064-.26977.0722-.22172-.048-.4584-.26977-.53064L7.595 22.6347c-.0554-.0181-.11192-.024-.16641-.0194zm51.31484.018c-.0545-.005-.11078.001-.16623.0194l-4.61736 1.50092c-.22178.0721-.34223.30869-.27014.53046.0721.22177.30868.34223.53046.27014l4.61719-1.50092c.22178-.0721.3424-.30869.27032-.53046-.0541-.16633-.20077-.2757-.36424-.28957zM6.75607 25.36479c-.1961-.002-.37196.13412-.41438.33371-.0485.2281.0962.45073.32427.49922l1.99777.42455c.2281.0485.45072-.0962.49921-.32427.0485-.22811-.0962-.45074-.32427-.49922l-1.99759-.42455c-.0285-.006-.057-.009-.085-.009zm52.65462.004c-.028.00023-.0563.004-.0848.009l-1.99778.42437c-.2281.0485-.37271.27093-.32426.49904.0485.2281.2711.3729.49921.32445l1.99759-.42437c.2281-.0485.3729-.27111.32445-.49922-.0424-.19959-.21829-.33537-.41437-.33371zM6.24704 28.13046c-.1952.0187-.35587.17185-.37731.37477-.0245.2319.14232.43838.37422.46288l4.82829.51048c.2319.0245.43838-.1425.46288-.37441.0245-.2319-.1425-.43838-.37441-.46288l-4.82828-.51048c-.029-.003-.0575-.003-.0854-.00035zm53.6763.0363c-.0279-.003-.0566-.003-.0856.00035l-4.82883.50394c-.23194.0242-.39914.2304-.37496.46233.0242.23194.2304.39918.46234.37496l4.82883-.50394c.23193-.0242.39914-.2304.37496-.46234-.0212-.20294-.1816-.35634-.37678-.37532zM6.16529 30.96149c-.2332 0-.42091.18772-.42091.42092 0 .23319.18771.42091.42091.42091h2.04228c.23319 0 .4211-.18772.4211-.42091 0-.2332-.18791-.42092-.4211-.42092zm51.79298 0c-.23319 0-.42092.18772-.42092.42092.00001.23319.18773.42091.42092.42091h2.04228c.23319 0 .42092-.18772.42092-.42091 0-.2332-.18773-.42092-.42092-.42092zM11.15508 33.2561c-.0279-.003-.0564-.003-.0854.00035l-4.82902.50394c-.23194.0242-.39913.2304-.37495.46233.0242.23194.2304.39918.46233.37496l4.82902-.50394c.23194-.0242.39913-.2304.37495-.46234-.0212-.20294-.18177-.35634-.37695-.37531zm43.85314.0298c-.19521.0187-.35588.17186-.37732.37478-.0245.2319.14233.43838.37423.46288l4.82829.51048c.23191.0245.43837-.14251.46288-.37441.0245-.2319-.14251-.43838-.37441-.46288l-4.8281-.51048c-.029-.003-.0577-.003-.0856-.00035zm-46.2602 2.8436c-.028.00024-.0565.003-.085.009l-1.99777.42436c-.22811.0485-.37271.27111-.32427.49922.0485.22811.27111.37272.49922.32427l1.99777-.42419c.2281-.0485.37271-.27111.32426-.49921-.0424-.1996-.2181-.33537-.41419-.33372zm48.66925.004c-.19609-.002-.37177.13394-.41419.33353-.0485.2281.096.45074.32409.49922l1.99777.42455c.22809.0485.45073-.096.49921-.32409.0485-.2281-.0962-.45092-.32426-.4994l-1.9976-.42455c-.0285-.006-.057-.009-.085-.009zm-45.30519 1.65787c-.0545-.005-.11077.001-.16622.0194L7.3285 39.31168c-.22178.0721-.34223.30869-.27014.53046.0721.22178.30868.34222.53046.27014l4.61719-1.50092c.22178-.0721.34241-.30869.27032-.53046-.0541-.16633-.20077-.2757-.36425-.28957zm41.93713.0149c-.16349.0137-.31005.12292-.36423.28921-.0722.22173.048.4584.26977.53065l4.61628 1.50418c.22172.0722.4584-.0481.53064-.26977.0723-.22172-.048-.4584-.26977-.53065l-4.61628-1.50418c-.0554-.0181-.11191-.024-.16641-.0194zm-43.69909 3.27251c-.0547.001-.11006.0128-.16332.0365l-1.86587.83075c-.21304.0948-.30812.34267-.21328.55571.0949.21304.34268.30812.55571.21328l1.86589-.83058c.21303-.0948.30811-.34267.21327-.55571-.0711-.15978-.22837-.25323-.3924-.24997zm45.45888.016c-.16403-.004-.32133.0899-.39258.24961-.095.21297-.00006.4609.21291.55589l1.86515.83202c.21297.095.46091.00006.5559-.21291.095-.21297.00006-.4609-.21291-.55589l-1.86515-.83203c-.0532-.0238-.10864-.0356-.16332-.0367zm-41.82613.91214c-.0808-.006-.16424.012-.23998.0558L9.53826 44.4903c-.20195.1166-.27065.37302-.15405.57497.1166.20195.37302.27065.57497.15405l4.20463-2.4274c.20195-.1166.27064-.3732.15405-.57515-.0729-.12622-.2003-.20041-.33499-.20982zm38.20028 0c-.13469.009-.26229.0836-.33517.20982-.1166.20195-.0479.45855.15405.57515l4.20463 2.4274c.20196.1166.45855.0479.57515-.15405.1166-.20195.0479-.45837-.15404-.57497l-4.20482-2.42758c-.0757-.0437-.15899-.0614-.2398-.0558zm-39.24903 3.56244c-.081.003-.16211.0291-.2329.0805l-1.65296 1.19935c-.18875.13694-.2305.39912-.0936.58787.13695.18875.39912.2305.58787.0935l1.65314-1.19935c.18877-.13693.23051-.39911.0936-.58786-.0856-.11797-.22022-.17866-.35516-.17404zm40.28761.0142c-.13494-.005-.26948.0558-.35515.17367-.13708.18865-.0955.45098.0932.58805l1.65224 1.20044c.18866.13707.4508.0955.58787-.0932.13707-.18866.0956-.4508-.093-.58787l-1.65224-1.20044c-.0707-.0514-.15193-.0778-.23289-.0807zm-36.54387.14533c-.10743-.006-.21702.0298-.30374.10773l-3.61076 3.2458c-.17342.15589-.18751.42098-.0316.59441.15589.17342.42097.1875.5944.0316l3.61077-3.2458c.17342-.15589.18751-.42098.0316-.59441-.0779-.0867-.18322-.13361-.29067-.13933zm32.80012.0116c-.10745.006-.21283.0525-.29084.13915-.15603.17332-.14207.43839.0312.59441l3.60841 3.24834c.17332.15604.43857.14208.59459-.0312.15603-.17331.14207-.43839-.0312-.5944l-3.6086-3.24835c-.0867-.078-.19611-.11355-.30356-.10791zm-29.37464 3.08358c-.13493-.005-.2696.0554-.35534.1733l-2.85613 3.92614c-.13719.18858-.0959.45087.0926.58805.18857.13719.45087.0959.58805-.0927l2.85613-3.92614c.13718-.18857.0959-.45086-.0926-.58805-.0707-.0514-.15175-.0778-.23271-.0806zm25.93573.0176c-.081.003-.16211.0289-.2329.0803-.18875.13694-.23048.39911-.0936.58787l2.85086 3.92995c.13693.18876.39911.2305.58787.0936.18876-.13693.23049-.3991.0936-.58786l-2.85086-3.92996c-.0856-.11797-.22004-.17846-.35498-.17385zm-29.6228.6064c-.10745.006-.21282.0525-.29084.13915l-1.36649 1.51763c-.15605.1733-.14223.43855.0311.59459.1733.15604.43837.14205.5944-.0313l1.36666-1.51762c.15606-.1733.14206-.43856-.0312-.59459-.0867-.078-.19611-.11354-.30357-.10791zm33.33076.002c-.10745-.006-.21691.0299-.30356.10791-.17333.156-.18726.42108-.0313.5944l1.3663 1.51799c.15602.17333.42109.18726.59442.0312.17332-.15601.18726-.42126.0312-.59459l-1.36631-1.5178c-.078-.0867-.18339-.13351-.29084-.13916zm-25.65524 1.68366c-.16403-.004-.32114.0899-.39239.24961l-1.97816 4.43389c-.095.21297-.00005.46089.21292.5559.21296.095.46089.00005.55589-.21291l1.97815-4.4339c.095-.21296.00005-.46089-.21292-.55589-.0532-.0238-.10881-.0356-.16349-.0367zm17.95556.0122c-.0547.001-.11023.0128-.1635.0365-.21297.0947-.30836.34244-.21363.55553l1.97196 4.43662c.0947.21297.34262.30836.55571.21364.21298-.0947.30836-.34244.21364-.55553l-1.97197-4.43662c-.071-.15973-.22818-.25329-.39221-.25015zM20.61581 52.5046c-.13468.009-.26212.0836-.33498.20982l-1.02115 1.76869c-.11659.20195-.0479.45837.15406.57497.20195.1166.45837.0479.57496-.15405l1.02115-1.76869c.11659-.20195.0479-.45837-.15406-.57497-.0757-.0437-.15916-.0614-.23998-.0558zm24.93421 0c-.0808-.006-.16406.0121-.23979.0558-.20195.1166-.27065.37302-.15405.57497l1.02114 1.76869c.1166.20195.37302.27064.57496.15405.20196-.1166.27066-.37302.15406-.57497l-1.02114-1.76869c-.0729-.12622-.20049-.20041-.33518-.20982zm-17.0545.0634c-.19609-.002-.3718.13394-.41419.33354l-1.00897 4.74926c-.0485.2281.0962.45076.32427.49921.22811.0485.45076-.0962.49922-.32427l1.00896-4.74926c.0485-.2281-.0962-.45076-.32427-.49921-.0285-.006-.057-.009-.085-.009zm9.1599.003c-.028.00022-.0563.003-.0848.009-.22814.0483-.37294.27071-.32465.49885l1.00553 4.74999c.0483.22814.27088.37293.49903.32464.22814-.0483.37293-.27089.32464-.49903l-1.0057-4.74965c-.0423-.19963-.21793-.33543-.41402-.33391zm-4.5725.47905c-.23319 0-.42092.18772-.42092.42092v4.85517c0 .2332.18773.42092.42092.42092.2332 0 .42092-.18772.42092-.42092v-4.85517c0-.2332-.18772-.42092-.42092-.42092zm-7.72657 1.56886c-.16347.0139-.31017.12324-.36423.28957l-.63129 1.94236c-.0721.22178.0484.45837.27014.53047.22177.0721.45836-.0486.53046-.27032l.63128-1.94218c.0721-.22177-.0484-.45836-.27013-.53046-.0554-.018-.11173-.024-.16623-.0194zm15.44987.001c-.0545-.005-.11078.001-.16622.0193-.22178.0721-.34238.30868-.27033.53047l.63111 1.94235c.0721.22179.30868.3422.53046.27014.22178-.0721.34238-.3085.27032-.53028l-.63128-1.94236c-.0541-.16634-.20058-.27568-.36406-.28957zm-10.36543 1.08181c-.1952.0188-.356.17203-.37732.37496l-.21346 2.03119c-.0244.23192.14268.43812.3746.46252.23192.0244.4383-.14268.4627-.3746l.21345-2.03101c.0244-.23192-.14268-.4383-.37458-.4627-.029-.003-.0575-.003-.0854-.00035zm5.26736.002c-.0279-.003-.0566-.003-.0856.00035-.23193.0242-.39917.2304-.37495.46233l.21218 2.03138c.0242.23193.2304.399.46234.37478.23193-.0242.39918-.23041.37496-.46234l-.212-2.03119c-.0212-.20295-.18178-.35637-.37697-.37533z" paint-order="markers stroke fill"/>
28
+ <path d="m469.09621 100.6068-65.50955 38.06124-41.41979 65.20654 60.59382-44.88117z" filter="url(#f)" opacity=".40900005" paint-order="markers stroke fill" transform="translate(-112.09544 -20.8224) scale(.35154)"/>
29
+ <path fill="#ff5150" d="m36.3834003 34.83806178-6.60095092-6.91272438 23.41607429-15.75199774z" paint-order="markers stroke fill"/>
30
+ <path fill="#f1f1f1" d="m36.38339038 34.83805895-6.60095092-6.91272438-16.81512624 22.66471911z" paint-order="markers stroke fill"/>
31
+ <path d="m12.96732 50.59006 23.41607-15.75201 16.81513-22.66472z" opacity=".243"/>
32
+ </svg>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3
+ <title>share</title>
4
+ <g id="share" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5
+ <g id="Group" transform="translate(3.500000, 2.000000)" stroke="#000000">
6
+ <path d="M6.3,4 L8.1,4 C8.59705627,4 9,4.39796911 9,4.88888889 L9,11.1111111 C9,11.6020309 8.59705627,12 8.1,12 L0.9,12 C0.402943725,12 0,11.6020309 0,11.1111111 L0,4.88888889 C0,4.39796911 0.402943725,4 0.9,4 L2.7,4 L2.7,4" id="Path"></path>
7
+ <line x1="4.5" y1="7" x2="4.5" y2="-1.94289029e-16" id="Path-2"></line>
8
+ <polyline id="Path-3" points="2.5 2 4.5 0 6.5 2"></polyline>
9
+ </g>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,106 @@
1
+ import { style } from "@vanilla-extract/css";
2
+ import { fadeIn } from "../animations.css.ts";
3
+
4
+ export const container = style({
5
+ display: "flex",
6
+ alignItems: "center",
7
+ justifyItems: "center",
8
+ flexDirection: "column",
9
+ blockSize: "100%",
10
+ });
11
+
12
+ export const safariPrompt = style({
13
+ backgroundColor: "white",
14
+ borderRadius: "1rem",
15
+ maxWidth: "38rem",
16
+ padding: "clamp(1rem, 5vw, 3rem)",
17
+ margin: "auto",
18
+ });
19
+
20
+ export const safariLogo = style({
21
+ maxWidth: "4rem",
22
+ marginInline: "auto",
23
+ marginBlockEnd: "1rem",
24
+ aspectRatio: "1",
25
+ });
26
+
27
+ export const safariPromptHeader = style({
28
+ marginBlockEnd: "clamp(1.5rem, 4cqw, 2rem)",
29
+ });
30
+
31
+ export const safariPromptActions = style({
32
+ display: "grid",
33
+ gap: "0.5rem",
34
+ gridTemplateColumns: "repeat(auto-fill, minmax(12rem, 1fr))",
35
+ marginBlock: "1.5rem",
36
+ });
37
+
38
+ export const safariDismissArea = style({
39
+ textAlign: "center",
40
+ marginBlock: "2rem 3rem",
41
+ });
42
+
43
+ export const compatCheckLoaderContainer = style({
44
+ maxInlineSize: "16rem",
45
+ margin: "auto",
46
+ });
47
+
48
+ export const indicator = style({
49
+ marginInline: "auto",
50
+ });
51
+
52
+ export const compatCheckLoaderText = style({
53
+ fontStyle: "italic",
54
+ color: `hsl(0 0% 0% / 0.5)`,
55
+ animation: `${fadeIn} 300ms 150ms both`,
56
+ marginBlockStart: "0.5rem",
57
+ textAlign: "center",
58
+ });
59
+
60
+ export const emptyStateHeading = style({
61
+ textAlign: "center",
62
+ marginBlockEnd: "0.5rem",
63
+ });
64
+
65
+ export const appIcon = style({
66
+ aspectRatio: "1",
67
+ borderRadius: "20%",
68
+ boxShadow: "0 0.125rem 0.25rem hsl(0 0% 0% / 0.2)",
69
+ width: "3rem",
70
+ height: "auto",
71
+ marginInline: "auto",
72
+ marginBlockEnd: "1rem",
73
+ });
74
+
75
+ export const dialog = style({
76
+ padding: "2rem",
77
+ textAlign: "center",
78
+ });
79
+
80
+ export const heading = style({
81
+ fontSize: "1.25rem",
82
+ });
83
+
84
+ export const intro = style({
85
+ marginBlockStart: "0.5rem",
86
+ });
87
+
88
+ export const steps = style({
89
+ marginBlock: "2rem",
90
+ borderBlockStart: "1px solid hsl(0 0% 0% / 0.1)",
91
+ textAlign: "left",
92
+ });
93
+
94
+ export const step = style({
95
+ borderBlockEnd: "1px solid hsl(0 0% 0% / 0.1)",
96
+ listStyle: "decimal",
97
+ listStylePosition: "inside",
98
+ paddingBlock: "0.75rem",
99
+ });
100
+
101
+ export const icon = style({
102
+ display: "inline-block",
103
+ position: "relative",
104
+ inlineSize: "1.25rem",
105
+ top: "0.125em",
106
+ });
package/lib/client.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  array,
12
12
  mask,
13
13
  object,
14
+ partial,
14
15
  string,
15
16
  Struct,
16
17
  unknown,
@@ -296,6 +297,32 @@ export class IndieTabletopClient {
296
297
  return result;
297
298
  }
298
299
 
300
+ async userAgent() {
301
+ return await this.fetch(
302
+ "/ua",
303
+ partial({
304
+ browser: partial({
305
+ name: string(),
306
+ version: string(),
307
+ major: string(),
308
+ }),
309
+ device: partial({
310
+ type: string(),
311
+ model: string(),
312
+ vendor: string(),
313
+ }),
314
+ engine: partial({
315
+ name: string(),
316
+ version: string(),
317
+ }),
318
+ os: partial({
319
+ name: string(),
320
+ version: string(),
321
+ }),
322
+ }),
323
+ );
324
+ }
325
+
299
326
  async logout() {
300
327
  const result = await this.fetch(
301
328
  "/v1/sessions",
package/lib/index.ts CHANGED
@@ -20,6 +20,7 @@ export * from "./MiddotSeparated/MiddotSeparated.tsx";
20
20
  export * from "./ModalDialog/index.tsx";
21
21
  export * from "./QRCode/QRCode.tsx";
22
22
  export * from "./ReleaseInfo/index.tsx";
23
+ export * from "./SafariCheck/SafariCheck.tsx";
23
24
  export * from "./ServiceWorkerHandler.tsx";
24
25
  export * from "./ShareButton/ShareButton.tsx";
25
26
  export * from "./SubscribeCard/SubscribeByEmailCard.tsx";
package/lib/vars.css.ts CHANGED
@@ -7,3 +7,7 @@ export const Color = createGlobalTheme(":root", {
7
7
  PALE_GRAY: "#f6f7f7",
8
8
  PURPLE: "#d6446e",
9
9
  });
10
+
11
+ export const ZIndex = createGlobalTheme(":root", {
12
+ DIALOG: "100",
13
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indietabletop/appkit",
3
- "version": "5.5.1",
3
+ "version": "6.1.0",
4
4
  "description": "A collection of modules used in apps built by Indie Tabletop Club",
5
5
  "private": false,
6
6
  "type": "module",
@@ -29,32 +29,32 @@
29
29
  "react": "^18.0.0 || ^19.0.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@storybook/addon-docs": "^10.1.6",
33
- "@storybook/addon-links": "^10.1.6",
34
- "@storybook/react-vite": "^10.1.6",
32
+ "@storybook/addon-docs": "^10.1.11",
33
+ "@storybook/addon-links": "^10.1.11",
34
+ "@storybook/react-vite": "^10.1.11",
35
35
  "@types/react": "^19.2.7",
36
- "msw": "^2.12.4",
36
+ "msw": "^2.12.7",
37
37
  "msw-storybook-addon": "^2.0.6",
38
38
  "np": "^10.2.0",
39
- "storybook": "^10.1.6",
39
+ "storybook": "^10.1.11",
40
40
  "typescript": "^5.9.3",
41
- "vite": "^7.2.7",
42
- "vitest": "^4.0.15"
41
+ "vite": "^7.3.1",
42
+ "vitest": "^4.0.16"
43
43
  },
44
44
  "dependencies": {
45
45
  "@ariakit/react": "^0.4.20",
46
46
  "@indietabletop/tooling": "^5.2.0",
47
- "@indietabletop/types": "^1.1.0",
48
- "@vanilla-extract/css": "^1.17.5",
47
+ "@indietabletop/types": "^1.2.0",
48
+ "@vanilla-extract/css": "^1.18.0",
49
49
  "@vanilla-extract/dynamic": "^2.1.5",
50
50
  "@vanilla-extract/recipes": "^0.5.7",
51
51
  "@vanilla-extract/sprinkles": "^1.6.5",
52
52
  "@xstate/react": "^6.0.0",
53
53
  "nanoid": "^5.1.6",
54
54
  "superstruct": "^2.0.2",
55
- "swr": "^2.3.7",
56
- "wouter": "^3.8.1",
57
- "xstate": "^5.24.0"
55
+ "swr": "^2.3.8",
56
+ "wouter": "^3.9.0",
57
+ "xstate": "^5.25.0"
58
58
  },
59
59
  "msw": {
60
60
  "workerDirectory": [