@indietabletop/appkit 3.2.0-0 → 3.2.0-10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/lib/ExternalLink.tsx +10 -0
  2. package/lib/FormSubmitButton.tsx +58 -0
  3. package/lib/FullscreenDismissBlocker.tsx +23 -0
  4. package/lib/IndieTabletopClubLogo.tsx +44 -0
  5. package/lib/IndieTabletopClubSymbol.tsx +37 -0
  6. package/lib/Letterhead/index.tsx +85 -0
  7. package/lib/Letterhead/stories.tsx +45 -0
  8. package/lib/Letterhead/style.css.ts +141 -0
  9. package/lib/LetterheadForm/LetterheadReadonlyTextField.stories.tsx +21 -0
  10. package/lib/LetterheadForm/LetterheadSubmitError.stories.tsx +23 -0
  11. package/lib/LetterheadForm/LetterheadTextField.stories.tsx +23 -0
  12. package/lib/LetterheadForm/index.tsx +94 -0
  13. package/lib/LetterheadForm/style.css.ts +80 -0
  14. package/lib/LoadingIndicator.tsx +39 -0
  15. package/lib/ServiceWorkerHandler.tsx +53 -0
  16. package/lib/animations.css.ts +17 -0
  17. package/lib/append-copy-to-text.test.ts +29 -0
  18. package/lib/append-copy-to-text.ts +35 -0
  19. package/lib/async-op.ts +246 -0
  20. package/lib/atomic.css.ts +11 -0
  21. package/{dist/caught-value.js → lib/caught-value.ts} +10 -8
  22. package/lib/class-names.ts +33 -0
  23. package/lib/client.ts +288 -0
  24. package/lib/common.css.ts +48 -0
  25. package/lib/failureMessages.test.ts +138 -0
  26. package/lib/failureMessages.ts +76 -0
  27. package/lib/globals.css.ts +45 -0
  28. package/{dist/index.d.ts → lib/index.ts} +9 -1
  29. package/lib/internal.css.ts +10 -0
  30. package/lib/media.ts +50 -0
  31. package/lib/storybook/decorators.tsx +10 -0
  32. package/lib/structs.ts +17 -0
  33. package/{dist/types.d.ts → lib/types.ts} +11 -6
  34. package/lib/use-async-op.ts +16 -0
  35. package/lib/use-document-background-color.ts +16 -0
  36. package/lib/use-form.ts +73 -0
  37. package/{dist/use-is-installed.js → lib/use-is-installed.ts} +7 -3
  38. package/lib/use-media-query.ts +21 -0
  39. package/lib/use-reverting-state.ts +32 -0
  40. package/lib/use-scroll-restoration.ts +99 -0
  41. package/lib/validations.ts +25 -0
  42. package/lib/vars.css.ts +9 -0
  43. package/package.json +15 -7
  44. package/dist/ExternalLink.d.ts +0 -3
  45. package/dist/ExternalLink.js +0 -4
  46. package/dist/FormSubmitButton.d.ts +0 -7
  47. package/dist/FormSubmitButton.js +0 -16
  48. package/dist/FullscreenDismissBlocker.d.ts +0 -5
  49. package/dist/FullscreenDismissBlocker.js +0 -19
  50. package/dist/IndieTabletopClubFooter.d.ts +0 -1
  51. package/dist/IndieTabletopClubFooter.js +0 -17
  52. package/dist/IndieTabletopClubLogo.d.ts +0 -7
  53. package/dist/IndieTabletopClubLogo.js +0 -6
  54. package/dist/IndieTabletopClubSymbol.d.ts +0 -7
  55. package/dist/IndieTabletopClubSymbol.js +0 -5
  56. package/dist/Letterhead.d.ts +0 -6
  57. package/dist/Letterhead.js +0 -14
  58. package/dist/LetterheadFooter.d.ts +0 -1
  59. package/dist/LetterheadFooter.js +0 -17
  60. package/dist/LoadingIndicator.d.ts +0 -3
  61. package/dist/LoadingIndicator.js +0 -17
  62. package/dist/ServiceWorkerHandler.d.ts +0 -11
  63. package/dist/ServiceWorkerHandler.js +0 -42
  64. package/dist/animations.css.d.ts +0 -3
  65. package/dist/animations.css.js +0 -14
  66. package/dist/append-copy-to-text.d.ts +0 -10
  67. package/dist/append-copy-to-text.js +0 -29
  68. package/dist/async-op.d.ts +0 -87
  69. package/dist/async-op.js +0 -223
  70. package/dist/caught-value.d.ts +0 -15
  71. package/dist/class-names.d.ts +0 -4
  72. package/dist/class-names.js +0 -6
  73. package/dist/client.d.ts +0 -117
  74. package/dist/client.js +0 -201
  75. package/dist/common.css.d.ts +0 -5
  76. package/dist/common.css.js +0 -38
  77. package/dist/defineNetlifyConfig.d.ts +0 -34
  78. package/dist/defineNetlifyConfig.js +0 -35
  79. package/dist/external-link.d.ts +0 -4
  80. package/dist/external-link.js +0 -4
  81. package/dist/form-submit-button.d.ts +0 -7
  82. package/dist/form-submit-button.js +0 -16
  83. package/dist/fullscreen-dismiss-blocker.d.ts +0 -5
  84. package/dist/fullscreen-dismiss-blocker.js +0 -19
  85. package/dist/globals.css.d.ts +0 -1
  86. package/dist/globals.css.js +0 -35
  87. package/dist/index.js +0 -25
  88. package/dist/internal.css.d.ts +0 -4
  89. package/dist/internal.css.js +0 -21
  90. package/dist/media.d.ts +0 -39
  91. package/dist/media.js +0 -49
  92. package/dist/service-worker-handler.d.ts +0 -11
  93. package/dist/service-worker-handler.js +0 -42
  94. package/dist/structs.d.ts +0 -20
  95. package/dist/structs.js +0 -15
  96. package/dist/types.js +0 -1
  97. package/dist/use-async-op.d.ts +0 -6
  98. package/dist/use-async-op.js +0 -12
  99. package/dist/use-document-background-color.d.ts +0 -4
  100. package/dist/use-document-background-color.js +0 -14
  101. package/dist/use-form.d.ts +0 -29
  102. package/dist/use-form.js +0 -33
  103. package/dist/use-is-installed.d.ts +0 -8
  104. package/dist/use-media-query.d.ts +0 -1
  105. package/dist/use-media-query.js +0 -15
  106. package/dist/use-reverting-state.d.ts +0 -5
  107. package/dist/use-reverting-state.js +0 -26
  108. package/dist/use-scroll-restoration.d.ts +0 -25
  109. package/dist/use-scroll-restoration.js +0 -67
@@ -0,0 +1,80 @@
1
+ import { style } from "@vanilla-extract/css";
2
+ import { manofa, minion } from "../common.css.ts";
3
+ import { Color } from "../vars.css.ts";
4
+
5
+ const border = style({
6
+ borderRadius: "0.5rem",
7
+ border: `1px solid ${Color.GRAY}`,
8
+ });
9
+
10
+ export const field = style({
11
+ display: "block",
12
+ });
13
+
14
+ export const fieldLabel = style([
15
+ manofa,
16
+ {
17
+ display: "block",
18
+ textTransform: "uppercase",
19
+ fontSize: "0.75rem",
20
+ fontWeight: 600,
21
+ marginBottom: "0.5rem",
22
+ },
23
+ ]);
24
+
25
+ export const fieldInput = style([
26
+ border,
27
+ minion,
28
+ {
29
+ display: "block",
30
+ width: "100%",
31
+ fontSize: "1rem",
32
+ lineHeight: "1.25rem",
33
+ padding: "1rem 0 1rem 1rem",
34
+
35
+ ":read-only": {
36
+ backgroundColor: "hsl(0 0% 0% / 0.05)",
37
+ },
38
+
39
+ // Hide MS Edge widgets -- we handle them manually
40
+ "::-ms-clear": {
41
+ display: "none",
42
+ },
43
+ "::-ms-reveal": {
44
+ display: "none",
45
+ },
46
+ },
47
+ ]);
48
+
49
+ export const fieldIssue = style({
50
+ color: Color.PURPLE,
51
+ fontSize: "0.875rem",
52
+ marginTop: "0.5rem",
53
+
54
+ ":empty": {
55
+ display: "none",
56
+ },
57
+ });
58
+
59
+ export const fieldHint = style({
60
+ color: Color.MID_GRAY,
61
+ fontSize: "0.875rem",
62
+ marginTop: "0.5rem",
63
+
64
+ selectors: {
65
+ [`${fieldIssue}:not(:empty) + &`]: {
66
+ display: "none",
67
+ },
68
+ },
69
+ });
70
+
71
+ export const submitError = style({
72
+ padding: "1rem",
73
+ color: Color.PURPLE,
74
+ backgroundColor: Color.PALE_GRAY,
75
+ borderRadius: "0.75rem",
76
+
77
+ ":empty": {
78
+ display: "none",
79
+ },
80
+ });
@@ -0,0 +1,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,29 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ appendCopyToText,
4
+ maybeAppendCopyToText,
5
+ } from "./append-copy-to-text.ts";
6
+
7
+ describe("appendCopyToText", () => {
8
+ test("Appends ' (Copy)' to provided string", () => {
9
+ const returnValue = appendCopyToText("Zangrad Raiders");
10
+ expect(returnValue).toBe("Zangrad Raiders (Copy)");
11
+ });
12
+
13
+ test("Adds a copy count number if string already ends in ' (Copy)'", () => {
14
+ const returnValue = appendCopyToText("Zangrad Raiders (Copy)");
15
+ expect(returnValue).toBe("Zangrad Raiders (Copy 2)");
16
+ });
17
+
18
+ test("Increments a copy count number if one already exists", () => {
19
+ const returnValue = appendCopyToText("Zangrad Raiders (Copy 2)");
20
+ expect(returnValue).toBe("Zangrad Raiders (Copy 3)");
21
+ });
22
+ });
23
+
24
+ describe("maybeAppendCopyToText", () => {
25
+ test("Ignores empty strings", () => {
26
+ const returnValue = maybeAppendCopyToText("");
27
+ expect(returnValue).toBe("");
28
+ });
29
+ });
@@ -0,0 +1,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
+ }
@@ -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>;
@@ -0,0 +1,11 @@
1
+ import { createSprinkles, defineProperties } from "@vanilla-extract/sprinkles";
2
+
3
+ const atomic = defineProperties({
4
+ properties: {
5
+ textAlign: ["start", "center", "end"],
6
+ },
7
+ });
8
+
9
+ export const textVariants = createSprinkles(atomic);
10
+
11
+ export type TextVariants = Parameters<typeof textVariants>[0];
@@ -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
- if (value instanceof Error) {
17
- return value.message;
18
- }
19
- if (typeof value === "string") {
20
- return value;
21
- }
22
- return "Unknown error.";
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
  }
@@ -0,0 +1,33 @@
1
+ type Falsy = false | null | undefined;
2
+
3
+ type PropsWithClassName = {
4
+ className?: ClassName;
5
+ };
6
+
7
+ type ClassName = string | PropsWithClassName | Falsy;
8
+
9
+ /**
10
+ * Combines a list of strings or objects with className property into a single
11
+ * string. Falsy values are ignored.
12
+ */
13
+ export function classNames(...classNames: ClassName[]) {
14
+ return classNames
15
+ .filter((cn) => !!cn)
16
+ .map((cn) => (typeof cn === "object" ? cn?.className : cn))
17
+ .join(" ");
18
+ }
19
+
20
+ /**
21
+ * Given a list of strings or objects with the className property, returns an
22
+ * object with className property and combined className. Falsy values will
23
+ * be filtered out.
24
+ *
25
+ * @example
26
+ * <h1 {...clx(props, 'heading', 'bold')}>Hello world!</h1>
27
+ */
28
+
29
+ export function cx(...cns: ClassName[]) {
30
+ return {
31
+ className: classNames(...cns),
32
+ };
33
+ }