@indietabletop/appkit 3.2.0-1 → 3.2.0-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ExternalLink.tsx +10 -0
- package/lib/FormSubmitButton.tsx +48 -0
- package/lib/FullscreenDismissBlocker.tsx +23 -0
- package/lib/IndieTabletopClubLogo.tsx +44 -0
- package/lib/IndieTabletopClubSymbol.tsx +37 -0
- package/lib/Letterhead.tsx +33 -0
- package/lib/LetterheadFooter.tsx +36 -0
- package/lib/LoadingIndicator.tsx +39 -0
- package/lib/ServiceWorkerHandler.tsx +53 -0
- package/lib/animations.css.ts +17 -0
- package/lib/append-copy-to-text.ts +35 -0
- package/lib/async-op.ts +246 -0
- package/{dist/caught-value.js → lib/caught-value.ts} +10 -8
- package/lib/class-names.ts +8 -0
- package/lib/client.ts +288 -0
- package/lib/common.css.ts +46 -0
- package/lib/globals.css.ts +42 -0
- package/{dist/index.d.ts → lib/index.ts} +6 -0
- package/lib/internal.css.ts +26 -0
- package/lib/media.ts +50 -0
- package/lib/structs.ts +17 -0
- package/{dist/types.d.ts → lib/types.ts} +11 -6
- package/lib/use-async-op.ts +16 -0
- package/lib/use-document-background-color.ts +16 -0
- package/lib/use-form.ts +73 -0
- package/{dist/use-is-installed.js → lib/use-is-installed.ts} +7 -3
- package/lib/use-media-query.ts +21 -0
- package/lib/use-reverting-state.ts +32 -0
- package/lib/use-scroll-restoration.ts +99 -0
- package/package.json +5 -6
- package/dist/ExternalLink.d.ts +0 -3
- package/dist/ExternalLink.js +0 -4
- package/dist/FormSubmitButton.d.ts +0 -7
- package/dist/FormSubmitButton.js +0 -16
- package/dist/FullscreenDismissBlocker.d.ts +0 -5
- package/dist/FullscreenDismissBlocker.js +0 -19
- package/dist/IndieTabletopClubLogo.d.ts +0 -7
- package/dist/IndieTabletopClubLogo.js +0 -6
- package/dist/IndieTabletopClubSymbol.d.ts +0 -7
- package/dist/IndieTabletopClubSymbol.js +0 -5
- package/dist/Letterhead.d.ts +0 -6
- package/dist/Letterhead.js +0 -14
- package/dist/LetterheadFooter.d.ts +0 -1
- package/dist/LetterheadFooter.js +0 -17
- package/dist/LoadingIndicator.d.ts +0 -3
- package/dist/LoadingIndicator.js +0 -17
- package/dist/ServiceWorkerHandler.d.ts +0 -11
- package/dist/ServiceWorkerHandler.js +0 -42
- package/dist/animations.css.d.ts +0 -3
- package/dist/animations.css.js +0 -14
- package/dist/append-copy-to-text.d.ts +0 -10
- package/dist/append-copy-to-text.js +0 -29
- package/dist/async-op.d.ts +0 -87
- package/dist/async-op.js +0 -223
- package/dist/caught-value.d.ts +0 -15
- package/dist/class-names.d.ts +0 -4
- package/dist/class-names.js +0 -6
- package/dist/client.d.ts +0 -117
- package/dist/client.js +0 -201
- package/dist/common.css.d.ts +0 -5
- package/dist/common.css.js +0 -38
- package/dist/globals.css.d.ts +0 -1
- package/dist/globals.css.js +0 -35
- package/dist/index.js +0 -25
- package/dist/internal.css.d.ts +0 -4
- package/dist/internal.css.js +0 -21
- package/dist/media.d.ts +0 -39
- package/dist/media.js +0 -49
- package/dist/structs.d.ts +0 -20
- package/dist/structs.js +0 -15
- package/dist/types.js +0 -1
- package/dist/use-async-op.d.ts +0 -6
- package/dist/use-async-op.js +0 -12
- package/dist/use-document-background-color.d.ts +0 -4
- package/dist/use-document-background-color.js +0 -14
- package/dist/use-form.d.ts +0 -29
- package/dist/use-form.js +0 -33
- package/dist/use-is-installed.d.ts +0 -8
- package/dist/use-media-query.d.ts +0 -1
- package/dist/use-media-query.js +0 -15
- package/dist/use-reverting-state.d.ts +0 -5
- package/dist/use-reverting-state.js +0 -26
- package/dist/use-scroll-restoration.d.ts +0 -25
- package/dist/use-scroll-restoration.js +0 -67
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnchorHTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
export type ExternalLinkProps = Omit<
|
|
4
|
+
AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
5
|
+
"rel" | "target"
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export function ExternalLink(props: ExternalLinkProps) {
|
|
9
|
+
return <a {...props} target="_blank" rel="noreferrer noopener" />;
|
|
10
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FormSubmitProps,
|
|
3
|
+
FormSubmit,
|
|
4
|
+
useFormContext,
|
|
5
|
+
useStoreState,
|
|
6
|
+
} from "@ariakit/react";
|
|
7
|
+
import type { ReactNode } from "react";
|
|
8
|
+
import { fadeIn } from "./animations.css.ts";
|
|
9
|
+
|
|
10
|
+
export type FormSubmitButtonProps = FormSubmitProps & {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
loading: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function FormSubmitButton(props: FormSubmitButtonProps) {
|
|
16
|
+
const { children, className, style, loading, ...submitProps } = props;
|
|
17
|
+
const form = useFormContext();
|
|
18
|
+
const isSubmitting = useStoreState(form, (s) => s?.submitting);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<FormSubmit
|
|
22
|
+
{...submitProps}
|
|
23
|
+
className={className}
|
|
24
|
+
style={{ position: "relative", ...style }}
|
|
25
|
+
>
|
|
26
|
+
<span
|
|
27
|
+
style={{ opacity: isSubmitting ? 0 : 1, transition: "200ms opacity" }}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
</span>
|
|
31
|
+
|
|
32
|
+
{isSubmitting && (
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
display: "flex",
|
|
36
|
+
position: "absolute",
|
|
37
|
+
inset: "0",
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
justifyContent: "center",
|
|
40
|
+
animation: `${fadeIn} 200ms 200ms both`,
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{loading}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
</FormSubmit>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
function preventDefaultEscHandling(event: KeyboardEvent) {
|
|
4
|
+
if (event.key === "Escape") {
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* This component prevents the default MacOS behaviour where a fullscreen window
|
|
11
|
+
* gets minimized by pressing Escape.
|
|
12
|
+
*/
|
|
13
|
+
export function FullscreenDismissBlocker() {
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
window.addEventListener("keydown", preventDefaultEscHandling);
|
|
16
|
+
|
|
17
|
+
return () => {
|
|
18
|
+
window.removeEventListener("keydown", preventDefaultEscHandling);
|
|
19
|
+
};
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SVGAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
type IndieTabletopClubLogoProps = Omit<
|
|
4
|
+
SVGAttributes<SVGElement>,
|
|
5
|
+
"width" | "height" | "viewBox"
|
|
6
|
+
> & {
|
|
7
|
+
textColor?: string;
|
|
8
|
+
symbolColor?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function IndieTabletopClubLogo(props: IndieTabletopClubLogoProps) {
|
|
12
|
+
const { textColor = "#161615", symbolColor = "#FF5937", ...svgProps } = props;
|
|
13
|
+
|
|
14
|
+
// prettier-ignore
|
|
15
|
+
return (
|
|
16
|
+
<svg {...svgProps} width="200px" height="13px" viewBox="0 0 200 13">
|
|
17
|
+
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
|
18
|
+
<g transform="translate(-720, -746)" fillRule="nonzero">
|
|
19
|
+
<g transform="translate(720, 746)">
|
|
20
|
+
<path fill={textColor} d="M162.319043,13 C160.39468,13 158.856429,12.433618 157.706354,11.3007467 C156.560409,10.1678541 155.987436,8.6527232 155.987436,6.75532194 C155.987436,5.27087812 156.347738,3.76969784 157.068341,2.25177041 L162.384083,0 C164.685265,0 166.250358,0.625027117 167.07833,1.87508242 L164.464335,4.2356719 C163.658044,3.15862455 162.711348,2.42755671 161.625281,2.04250049 L160.101483,2.04250049 C159.838226,3.28697663 159.70608,4.32217213 159.70608,5.14810843 C159.70608,6.76090433 160.056058,8.03885336 160.754982,8.98197696 C161.453905,9.91951817 162.380986,10.3882888 163.537255,10.3882888 C163.854197,10.3882888 164.179397,10.3408545 164.512857,10.2459861 C164.846317,10.1511176 165.387286,9.95858417 166.134731,9.66839641 L166.61066,10.4217724 L164.053446,13 L162.319043,13 Z" />
|
|
21
|
+
<polygon fill={textColor} points="171.687921 10.7314717 176.244861 10.7314717 175.584135 12.7572036 168.147879 12.7572036 168.147879 0.242755671 171.687921 0.159047705" />
|
|
22
|
+
<path fill={textColor} d="M184.113686,0.242755671 L187.646502,0.159047705 L187.646502,12.7572036 L184.113686,12.840993 L184.113686,11.8699146 L181.435684,13 C179.893303,13 178.79588,12.6512347 178.145478,11.9535969 C177.500239,11.2560662 177.177103,10.078568 177.177103,8.42113445 L177.177103,0.242755671 L180.709918,0.159047705 L180.709918,7.96073207 C180.709918,8.59133836 180.742955,9.07964196 180.806962,9.42564289 C180.877164,9.77163309 181.021698,10.0701998 181.242628,10.3213215 C181.462526,10.572454 181.763982,10.7426151 182.145964,10.8319761 C182.533107,10.9156584 183.057558,10.9575531 183.718283,10.9575531 C183.982573,9.7465177 184.113686,8.7113222 184.113686,7.85190228 L184.113686,0.242755671 Z" />
|
|
23
|
+
<path fill={textColor} d="M197.176432,5.36574657 C198.069443,5.42155974 198.763205,5.69221449 199.257716,6.17772155 C199.752228,6.65765693 200,7.29664216 200,8.09466652 C200,8.95407573 199.690285,9.76047903 199.071887,10.513855 L193.757178,12.7572036 L189.788697,12.7572036 L189.788697,0.242755671 L195.273749,0.242755671 C198.015759,0.242755671 199.386764,1.15239351 199.386764,2.97167348 C199.386764,3.48508176 199.160672,4.06546799 198.709521,4.71282144 L197.176432,5.36574657 Z M194.595474,2.26851757 L193.329772,2.26851757 L193.329772,5.34901012 L195.44306,5.34901012 C195.59895,4.80210751 195.674314,4.40310069 195.66812,4.15196822 C195.66812,3.29813069 195.310915,2.67032095 194.595474,2.26851757 Z M196.031519,10.7314717 C196.197732,10.1846012 196.281355,9.69908348 196.281355,9.27496126 C196.281355,8.40438728 195.907633,7.77098444 195.160187,7.37476345 L193.329772,7.37476345 L193.329772,10.7314717 L196.031519,10.7314717 Z" />
|
|
24
|
+
<polygon fill={textColor} points="0 0.242755671 3.54075462 0.159047705 3.54075462 12.7572036 0 12.840993" />
|
|
25
|
+
<polygon fill={textColor} points="13.6033016 0.242755671 15.5551254 0.159047705 15.5551254 12.7572036 13.2403156 13 7.6267315 5.50804925 7.6267315 12.7572036 5.67487669 12.840993 5.67487669 0.242755671 7.98967617 0 13.6033016 7.55892869" />
|
|
26
|
+
<path fill={textColor} d="M23.8028354,0.242755671 C25.6203462,0.242755671 27.0425574,0.77291326 28.0695723,1.83323058 C29.1019556,2.88795909 29.6181472,4.30543568 29.6181472,6.08564964 C29.6181472,7.48079668 29.2605297,8.95687228 28.5453978,10.513855 L23.2221198,12.7572036 L17.7053733,12.7572036 L17.7053733,0.242755671 L23.8028354,0.242755671 Z M25.5047193,10.7314717 C25.7681835,9.41447811 25.8999156,8.39880489 25.8999156,7.68449492 C25.8999156,5.03370698 24.9588982,3.22837762 23.0769668,2.26851757 L21.2461382,2.26851757 L21.2461382,10.7314717 L25.5047193,10.7314717 Z" />
|
|
27
|
+
<polygon fill={textColor} points="31.090842 0.242755671 34.6316069 0.159047705 34.6316069 12.7572036 31.090842 12.840993" />
|
|
28
|
+
<polygon fill={textColor} points="40.3065146 7.34127984 40.3065146 10.7314717 45.19423 10.7314717 44.532782 12.7572036 36.7657497 12.7572036 36.7657497 0.242755671 45.19423 0.242755671 44.532782 2.26851757 40.3065146 2.26851757 40.3065146 5.31552651 44.3634712 5.31552651 43.7020232 7.34127984" />
|
|
29
|
+
<polygon fill={textColor} points="60.8105757 0.242755671 60.1572836 2.26851757 57.2134427 2.26851757 57.2134427 12.7572036 53.6807303 12.840993 53.6807303 2.26851757 50.0593362 2.26851757 50.720681 0.242755671" />
|
|
30
|
+
<path fill={textColor} d="M67.1548807,13 L65.9773443,9.65164925 L62.2107972,9.65164925 L61.1219425,12.7572036 L59.0087572,13 L63.5012763,0.242755671 L66.3080167,0 L70.6553827,12.3136127 L67.1548807,13 Z M62.9205607,7.63426414 L65.2675808,7.63426414 L64.0980971,4.31938629 L62.9205607,7.63426414 Z" />
|
|
31
|
+
<path fill={textColor} d="M79.1127698,5.36574657 C80.0053684,5.42155974 80.6990267,5.69221449 81.1937448,6.17772155 C81.6883596,6.65765693 81.9357187,7.29664216 81.9357187,8.09466652 C81.9357187,8.95407573 81.6265199,9.76047903 81.0082255,10.513855 L75.6930002,12.7572036 L71.7248286,12.7572036 L71.7248286,0.242755671 L77.2093647,0.242755671 C79.9515812,0.242755671 81.3227927,1.15239351 81.3227927,2.97167348 C81.3227927,3.48508176 81.0969072,4.06546799 80.6452396,4.71282144 L79.1127698,5.36574657 Z M76.5318116,2.26851757 L75.2655935,2.26851757 L75.2655935,5.34901012 L77.3787788,5.34901012 C77.5346687,4.80210751 77.6099295,4.40310069 77.6045611,4.15196822 C77.6045611,3.29813069 77.2470467,2.67032095 76.5318116,2.26851757 Z M77.967547,10.7314717 C78.1341737,10.1846012 78.2175903,9.69908348 78.2175903,9.27496126 C78.2175903,8.40438728 77.8438675,7.77098444 77.096422,7.37476345 L75.2655935,7.37476345 L75.2655935,10.7314717 L77.967547,10.7314717 Z" />
|
|
32
|
+
<polygon fill={textColor} points="86.8282863 10.7314717 91.3853293 10.7314717 90.7238813 12.7572036 83.2875213 12.7572036 83.2875213 0.242755671 86.8282863 0.159047705" />
|
|
33
|
+
<polygon fill={textColor} points="96.0035927 7.34127984 96.0035927 10.7314717 100.891308 10.7314717 100.22986 12.7572036 92.4628277 12.7572036 92.4628277 0.242755671 100.891308 0.242755671 100.22986 2.26851757 96.0035927 2.26851757 96.0035927 5.31552651 100.060549 5.31552651 99.3992045 7.34127984" />
|
|
34
|
+
<polygon fill={textColor} points="111.848921 0.242755671 111.195422 2.26851757 108.252098 2.26851757 108.252098 12.7572036 104.719282 12.840993 104.719282 2.26851757 101.097785 2.26851757 101.759129 0.242755671" />
|
|
35
|
+
<path fill={textColor} d="M117.951339,0 C119.876733,0 121.411887,0.566431327 122.5568,1.69929612 C123.701713,2.83215663 124.274686,4.34728752 124.274686,6.24468878 C124.274686,7.7291326 123.917482,9.23031287 123.20204,10.7481867 L117.887331,13 C115.961936,13 114.423685,12.433618 113.27361,11.3007467 C112.127665,10.1678541 111.555724,8.6527232 111.555724,6.75532194 C111.555724,5.27087812 111.916026,3.76969784 112.636629,2.25177041 L117.951339,0 Z M120.161671,10.9575531 C120.424929,9.71303409 120.559139,8.67783859 120.564301,7.85190228 C120.564301,5.02812459 119.46791,3.09165733 117.274095,2.04250049 L115.668739,2.04250049 C115.405481,3.28697663 115.273336,4.32217213 115.273336,5.14810843 C115.273336,7.97188613 116.37076,9.9083641 118.564574,10.9575531 L120.161671,10.9575531 Z" />
|
|
36
|
+
<path fill={textColor} d="M131.070865,0.242755671 C131.796631,0.242755671 132.44187,0.312514094 133.006584,0.452028798 C133.57646,0.585963257 134.033805,0.764542892 134.377589,0.987766633 C134.727567,1.21099252 135.01457,1.47886143 135.240661,1.79137874 C135.466753,2.09830295 135.622643,2.40523787 135.708331,2.71217279 C135.800213,3.01910771 135.845638,3.34557563 135.845638,3.69156584 C135.845638,4.61795299 135.535923,5.4522468 134.917525,6.19446871 L129.602816,8.4378709 L129.287939,8.4378709 L129.287939,12.7572036 L125.747897,12.840993 L125.747897,0.242755671 L131.070865,0.242755671 Z M131.877157,6.41211757 C132.044403,5.86521495 132.126993,5.35179596 132.126993,4.87187129 C132.126993,3.66087877 131.713008,2.79309063 130.885036,2.26851757 L129.287939,2.26851757 L129.287939,6.41211757 L131.877157,6.41211757 Z" />
|
|
37
|
+
|
|
38
|
+
<polygon fill={symbolColor} points="150.825519 4.65625824 149.752873 9.89656716 144.843891 11.5521149 141.007554 7.96730021 142.080201 2.7269913 146.990216 1.07147567" />
|
|
39
|
+
</g>
|
|
40
|
+
</g>
|
|
41
|
+
</g>
|
|
42
|
+
</svg>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SVGAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
type IndieTabletopClubSymbolProps = Omit<
|
|
4
|
+
SVGAttributes<SVGElement>,
|
|
5
|
+
"width" | "height" | "viewBox"
|
|
6
|
+
> & {
|
|
7
|
+
backgroundColor?: string;
|
|
8
|
+
symbolColor?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function IndieTabletopClubSymbol(props: IndieTabletopClubSymbolProps) {
|
|
12
|
+
const {
|
|
13
|
+
symbolColor = "#FF5937",
|
|
14
|
+
backgroundColor = "#F0F0F0",
|
|
15
|
+
...svgProps
|
|
16
|
+
} = props;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<svg {...svgProps} width="65px" height="65px" viewBox="0 0 65 65">
|
|
20
|
+
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
|
21
|
+
<rect
|
|
22
|
+
fill={backgroundColor}
|
|
23
|
+
x="0"
|
|
24
|
+
y="0"
|
|
25
|
+
width="65"
|
|
26
|
+
height="65"
|
|
27
|
+
rx="11"
|
|
28
|
+
/>
|
|
29
|
+
<polygon
|
|
30
|
+
fill={symbolColor}
|
|
31
|
+
fillRule="nonzero"
|
|
32
|
+
points="52.2105164 26.1156583 47.8803714 46.1156174 28.0633944 52.4341166 12.5765623 38.7524522 16.9067073 18.7524931 36.7278519 12.4341166"
|
|
33
|
+
/>
|
|
34
|
+
</g>
|
|
35
|
+
</svg>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { IndieTabletopClubSymbol } from "./IndieTabletopClubSymbol.tsx";
|
|
3
|
+
import { letterhead } from "./internal.css.ts";
|
|
4
|
+
import { LetterheadFooter } from "./LetterheadFooter.tsx";
|
|
5
|
+
|
|
6
|
+
export type LetterheadProps = {
|
|
7
|
+
headerIcon?: boolean;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Letterhead(props: LetterheadProps) {
|
|
12
|
+
const { children, headerIcon = true } = props;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={letterhead}>
|
|
16
|
+
{headerIcon && (
|
|
17
|
+
<IndieTabletopClubSymbol
|
|
18
|
+
style={{
|
|
19
|
+
marginBlock: "-1rem 1rem",
|
|
20
|
+
marginInline: "auto",
|
|
21
|
+
display: "block",
|
|
22
|
+
inlineSize: "2.5rem",
|
|
23
|
+
blockSize: "2.5rem",
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
)}
|
|
27
|
+
|
|
28
|
+
{children}
|
|
29
|
+
|
|
30
|
+
<LetterheadFooter />
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { interactiveText } from "./common.css.ts";
|
|
2
|
+
import { ExternalLink } from "./ExternalLink.tsx";
|
|
3
|
+
import { IndieTabletopClubLogo } from "./IndieTabletopClubLogo.tsx";
|
|
4
|
+
|
|
5
|
+
export function LetterheadFooter() {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
style={{
|
|
9
|
+
textAlign: "center",
|
|
10
|
+
paddingBlockStart: "2rem",
|
|
11
|
+
borderBlockStart: "1px solid #ececec",
|
|
12
|
+
marginBlockStart: "3rem",
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
<IndieTabletopClubLogo style={{ margin: "0 auto 1.125rem" }} />
|
|
16
|
+
<p
|
|
17
|
+
style={{
|
|
18
|
+
margin: "0 auto",
|
|
19
|
+
maxInlineSize: "25rem",
|
|
20
|
+
fontSize: "0.875rem",
|
|
21
|
+
lineHeight: "1.25rem",
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
Indie Tabletop Club supports independent game creators with high‑quality
|
|
25
|
+
digital tools.{" "}
|
|
26
|
+
<ExternalLink
|
|
27
|
+
href="https://indietabletop.club"
|
|
28
|
+
className={interactiveText}
|
|
29
|
+
>
|
|
30
|
+
Learn more
|
|
31
|
+
</ExternalLink>
|
|
32
|
+
.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { assignInlineVars } from "@vanilla-extract/dynamic";
|
|
2
|
+
import { animationDelay, dot } from "./internal.css.ts";
|
|
3
|
+
|
|
4
|
+
export function LoadingIndicator(props: { className?: string }) {
|
|
5
|
+
const diameter = 10;
|
|
6
|
+
const radius = diameter / 2;
|
|
7
|
+
const gap = diameter;
|
|
8
|
+
const cy = diameter;
|
|
9
|
+
const height = cy * 2;
|
|
10
|
+
const width = diameter * 3 + gap;
|
|
11
|
+
const initialDelay = 300;
|
|
12
|
+
const interBounceDelay = 150;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<svg
|
|
16
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
17
|
+
width={width}
|
|
18
|
+
height={height}
|
|
19
|
+
className={props.className}
|
|
20
|
+
>
|
|
21
|
+
<g stroke="none" fill="inherit">
|
|
22
|
+
{Array.from({ length: 3 }, (_, index) => {
|
|
23
|
+
const delay = `${initialDelay + interBounceDelay * index}ms`;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<circle
|
|
27
|
+
key={index}
|
|
28
|
+
cx={radius * (index + 1) + gap * index}
|
|
29
|
+
cy={cy}
|
|
30
|
+
r={radius}
|
|
31
|
+
className={dot}
|
|
32
|
+
style={assignInlineVars({ [animationDelay]: delay })}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
})}
|
|
36
|
+
</g>
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ReactNode, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This component handles the installation of a service worker.
|
|
5
|
+
*
|
|
6
|
+
* Currently it doesn't do much, but, eventually, it should provide context
|
|
7
|
+
* to nested components communicating the status of the service worker installation.
|
|
8
|
+
*/
|
|
9
|
+
export function ServiceWorkerHandler(props: {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
path: string;
|
|
12
|
+
}) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
async function registerWorker() {
|
|
15
|
+
// Although modern browsers all support service workers, native app's
|
|
16
|
+
// web views (eg. the Facebook app's embedded browsers) do not.
|
|
17
|
+
if ("serviceWorker" in navigator) {
|
|
18
|
+
try {
|
|
19
|
+
const registration = await navigator.serviceWorker.register(
|
|
20
|
+
props.path,
|
|
21
|
+
);
|
|
22
|
+
console.info("Service worker registration obtained.");
|
|
23
|
+
|
|
24
|
+
registration.addEventListener("updatefound", () => {
|
|
25
|
+
const worker = registration.installing;
|
|
26
|
+
console.info("Installing new service worker.");
|
|
27
|
+
|
|
28
|
+
worker?.addEventListener("statechange", ({ target }) => {
|
|
29
|
+
if (target instanceof ServiceWorker) {
|
|
30
|
+
console.info(
|
|
31
|
+
`Service worker state changed: '${target.state}'.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// In rare cases, service worker installation can fail, e.g. due to network
|
|
38
|
+
// connectivity. There is no need to report the error as there is nothing
|
|
39
|
+
// that can be done to prevent this from occassionally happening.
|
|
40
|
+
console.error(error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
void registerWorker();
|
|
46
|
+
|
|
47
|
+
// Note that it is not necessary to 'cleanup' the registration in
|
|
48
|
+
// the useEffect hook. Calling register() multiple times is a no-op.
|
|
49
|
+
// See https://web.dev/articles/service-workers-registration#subsequent_visits
|
|
50
|
+
}, [props.path]);
|
|
51
|
+
|
|
52
|
+
return <>{props.children}</>;
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { keyframes } from "@vanilla-extract/css";
|
|
2
|
+
|
|
3
|
+
export const fadeIn = keyframes({
|
|
4
|
+
from: { opacity: 0 },
|
|
5
|
+
to: { opacity: 1 },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const slideUp = keyframes({
|
|
9
|
+
from: { transform: `translateY(100%)` },
|
|
10
|
+
to: { transform: `translateY(0)` },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const bounce = keyframes({
|
|
14
|
+
"0%": { transform: "translateY(0)" },
|
|
15
|
+
"20%": { transform: "translateY(-20%)" },
|
|
16
|
+
"50%": { transform: "translateY(0)" },
|
|
17
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appends " (Copy)" to the end of the input string if it doesn't already end
|
|
3
|
+
* with " (Copy)", otherwise it appends a number after "Copy", incrementing it
|
|
4
|
+
* if necessary.
|
|
5
|
+
*/
|
|
6
|
+
export function appendCopyToText(input: string): string {
|
|
7
|
+
const regex = /^(?<value>.*) \(Copy(?: (?<count>\d+))?\)$/;
|
|
8
|
+
const match = input.match(regex);
|
|
9
|
+
|
|
10
|
+
// If there isn't a match, we directly append to the input.
|
|
11
|
+
if (!match) {
|
|
12
|
+
return `${input.trim()} (Copy)`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { value, count } = match.groups ?? {};
|
|
16
|
+
|
|
17
|
+
// If `count` capturing group is not present, it means that the input ends
|
|
18
|
+
// with the copy suffix, but it doesn't contain count.
|
|
19
|
+
const nextCount = !count ? 2 : parseInt(count, 10) + 1;
|
|
20
|
+
|
|
21
|
+
return `${value.trim()} (Copy ${nextCount})`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Works like {@link appendCopyToText}, but ignores empty strings.
|
|
26
|
+
*/
|
|
27
|
+
export function maybeAppendCopyToText(input: string) {
|
|
28
|
+
// If input is falsy (i.e. empty string) then we don't want to append
|
|
29
|
+
// anything to it.
|
|
30
|
+
if (!input) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return appendCopyToText(input);
|
|
35
|
+
}
|
package/lib/async-op.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
type Falsy = null | undefined | false | 0 | 0n | "";
|
|
2
|
+
|
|
3
|
+
type Truthy<T> = Exclude<T, Falsy>;
|
|
4
|
+
|
|
5
|
+
interface Operation<SuccessValue, FailureValue> {
|
|
6
|
+
readonly type: "SUCCESS" | "FAILURE" | "PENDING";
|
|
7
|
+
readonly isPending: boolean;
|
|
8
|
+
readonly isSuccess: boolean;
|
|
9
|
+
readonly isFailure: boolean;
|
|
10
|
+
|
|
11
|
+
val: SuccessValue | FailureValue | null;
|
|
12
|
+
|
|
13
|
+
valueOrNull(): SuccessValue | null;
|
|
14
|
+
valueOrThrow(): SuccessValue;
|
|
15
|
+
hasTruthyValue(): boolean;
|
|
16
|
+
failureValueOrNull(): FailureValue | null;
|
|
17
|
+
failureValueOrThrow(): FailureValue;
|
|
18
|
+
|
|
19
|
+
flatMap<T extends AsyncOp<unknown, unknown>>(
|
|
20
|
+
mappingFn: (value: SuccessValue) => T,
|
|
21
|
+
): T | Failure<FailureValue> | Pending;
|
|
22
|
+
|
|
23
|
+
mapSuccess<MappedSuccess>(
|
|
24
|
+
mappingFn: (value: SuccessValue) => MappedSuccess,
|
|
25
|
+
): Operation<MappedSuccess, FailureValue>;
|
|
26
|
+
|
|
27
|
+
mapFailure<MappedFailure>(
|
|
28
|
+
mappingFn: (value: FailureValue) => MappedFailure,
|
|
29
|
+
): Operation<SuccessValue, MappedFailure>;
|
|
30
|
+
|
|
31
|
+
unpack<S, F, P>(
|
|
32
|
+
mapS: (value: SuccessValue) => S,
|
|
33
|
+
mapF: (failure: FailureValue) => F,
|
|
34
|
+
mapP: () => P,
|
|
35
|
+
): S | F | P;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Pending implements Operation<never, never> {
|
|
39
|
+
readonly type = "PENDING" as const;
|
|
40
|
+
readonly isPending = true as const;
|
|
41
|
+
readonly isSuccess = false as const;
|
|
42
|
+
readonly isFailure = false as const;
|
|
43
|
+
|
|
44
|
+
val = null;
|
|
45
|
+
|
|
46
|
+
valueOrNull(): null {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
valueOrThrow(): never {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`AsyncOp value was accessed but the op is in Pending state.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
hasTruthyValue(): false {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
failureValueOrNull(): null {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
failureValueOrThrow(): never {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`AsyncOp failure value was accessed but the op is in Pending state.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
flatMap() {
|
|
71
|
+
return new Pending();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
mapSuccess() {
|
|
75
|
+
return new Pending();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
mapFailure() {
|
|
79
|
+
return new Pending();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
unpack<S, F, P>(
|
|
83
|
+
_mapS: (value: never) => S,
|
|
84
|
+
_mapF: (failure: never) => F,
|
|
85
|
+
mapP: () => P,
|
|
86
|
+
): S | F | P {
|
|
87
|
+
return mapP();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class Success<SuccessValue> implements Operation<SuccessValue, never> {
|
|
92
|
+
readonly type = "SUCCESS" as const;
|
|
93
|
+
readonly isPending = false as const;
|
|
94
|
+
readonly isSuccess = true as const;
|
|
95
|
+
readonly isFailure = false as const;
|
|
96
|
+
readonly value: SuccessValue;
|
|
97
|
+
readonly val: SuccessValue;
|
|
98
|
+
|
|
99
|
+
constructor(value: SuccessValue) {
|
|
100
|
+
this.value = value;
|
|
101
|
+
this.val = value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
valueOrNull(): SuccessValue {
|
|
105
|
+
return this.value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
valueOrThrow(): SuccessValue {
|
|
109
|
+
return this.value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
hasTruthyValue(): this is Success<Truthy<SuccessValue>> {
|
|
113
|
+
return !!this.value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
failureValueOrNull(): null {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
failureValueOrThrow(): never {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`AsyncOp failure value was accessed but the op is in Success state.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
flatMap<T extends AsyncOp<unknown, unknown>>(
|
|
127
|
+
mappingFn: (value: SuccessValue) => T,
|
|
128
|
+
) {
|
|
129
|
+
return mappingFn(this.value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
mapSuccess<MappedValue>(mappingFn: (value: SuccessValue) => MappedValue) {
|
|
133
|
+
return new Success(mappingFn(this.value));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
mapFailure() {
|
|
137
|
+
return new Success(this.value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
unpack<S, F, P>(
|
|
141
|
+
mapS: (value: SuccessValue) => S,
|
|
142
|
+
_mapF: (failure: never) => F,
|
|
143
|
+
_mapP: () => P,
|
|
144
|
+
): S | F | P {
|
|
145
|
+
return mapS(this.value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class Failure<FailureValue> implements Operation<never, FailureValue> {
|
|
150
|
+
readonly type = "FAILURE" as const;
|
|
151
|
+
readonly isPending = false as const;
|
|
152
|
+
readonly isSuccess = false as const;
|
|
153
|
+
readonly isFailure = true as const;
|
|
154
|
+
readonly failure: FailureValue;
|
|
155
|
+
readonly val: FailureValue;
|
|
156
|
+
|
|
157
|
+
constructor(failure: FailureValue) {
|
|
158
|
+
this.failure = failure;
|
|
159
|
+
this.val = failure;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
valueOrNull(): null {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
valueOrThrow(): never {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`AsyncOp value was accessed but the op is in Failure state.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
hasTruthyValue(): false {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
failureValueOrNull(): FailureValue {
|
|
177
|
+
return this.failure;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
failureValueOrThrow(): FailureValue {
|
|
181
|
+
return this.failure;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
flatMap() {
|
|
185
|
+
return new Failure(this.failure);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
mapSuccess() {
|
|
189
|
+
return new Failure(this.failure);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
mapFailure<MappedFailure>(mappingFn: (value: FailureValue) => MappedFailure) {
|
|
193
|
+
return new Failure(mappingFn(this.failure));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
unpack<S, F, P>(
|
|
197
|
+
_mapS: (value: never) => S,
|
|
198
|
+
mapF: (failure: FailureValue) => F,
|
|
199
|
+
_mapP: () => P,
|
|
200
|
+
): S | F | P {
|
|
201
|
+
return mapF(this.failure);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Folds multiple ops into a single op.
|
|
207
|
+
*
|
|
208
|
+
* To return a Success, all ops provided must be a Success. If any Failures are
|
|
209
|
+
* encountered, will return the first one found.
|
|
210
|
+
*
|
|
211
|
+
* If neither of these conditions is true, will return Pending.
|
|
212
|
+
*
|
|
213
|
+
* Note that if passed an empty array, will always return a Success (with an
|
|
214
|
+
* empty array as value). This mimics the semantics of many JS constructs, like
|
|
215
|
+
* Promise.all or Array.prototype.every.
|
|
216
|
+
*/
|
|
217
|
+
export function fold<Ops extends readonly AsyncOp<unknown, unknown>[] | []>(
|
|
218
|
+
ops: Ops,
|
|
219
|
+
): AsyncOp<
|
|
220
|
+
{
|
|
221
|
+
-readonly [Index in keyof Ops]: Ops[Index] extends AsyncOp<infer S, unknown>
|
|
222
|
+
? S
|
|
223
|
+
: never;
|
|
224
|
+
},
|
|
225
|
+
Ops[number] extends AsyncOp<unknown, infer F> ? F : never
|
|
226
|
+
> {
|
|
227
|
+
// Note that due to the semantics of `every`, if the array provided to `fold`
|
|
228
|
+
// is empty, the result will be a Success with an empty array.
|
|
229
|
+
if (ops.every((v) => v.isSuccess)) {
|
|
230
|
+
return new Success(
|
|
231
|
+
(ops as Success<unknown>[]).map((op) => op.value),
|
|
232
|
+
) as never;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const firstFail = ops.find((op) => op.isFailure);
|
|
236
|
+
if (firstFail) {
|
|
237
|
+
return firstFail as never;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return new Pending() as never;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export type AsyncOp<SuccessValue, FailureValue> =
|
|
244
|
+
| Pending
|
|
245
|
+
| Success<SuccessValue>
|
|
246
|
+
| Failure<FailureValue>;
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
* if so, returns its message. If a string was caught, it returns that.
|
|
13
13
|
* Otherwise, it returns "Unknown error".
|
|
14
14
|
*/
|
|
15
|
-
export function caughtValueToString(value) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
export function caughtValueToString(value: unknown): string {
|
|
16
|
+
if (value instanceof Error) {
|
|
17
|
+
return value.message;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof value === "string") {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return "Unknown error.";
|
|
23
25
|
}
|