create-react-adam 0.2.1 → 0.3.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.
@@ -0,0 +1,27 @@
1
+ import { ComponentPropsWithoutRef } from "react";
2
+ import { ButtonVariant } from "../types/ui";
3
+
4
+ interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
5
+ variant?: ButtonVariant;
6
+ }
7
+
8
+ const variantClasses: Record<ButtonVariant, string> = {
9
+ primary: "bg-brand-primary hover:bg-brand-primaryHover",
10
+ success: "bg-brand-success hover:bg-brand-success/90",
11
+ danger: "bg-brand-danger hover:bg-brand-danger/90",
12
+ };
13
+
14
+ const Button = ({ variant = "primary", className, ...props }: ButtonProps) => (
15
+ <button
16
+ className={[
17
+ "text-brand-white rounded-lg px-4 py-2 transition-colors",
18
+ variantClasses[variant],
19
+ className,
20
+ ]
21
+ .filter(Boolean)
22
+ .join(" ")}
23
+ {...props}
24
+ />
25
+ );
26
+
27
+ export default Button;
@@ -1,6 +1,11 @@
1
+ import { useEffect } from "react";
1
2
  import { Link } from "wouter";
2
3
 
3
4
  const About = () => {
5
+ useEffect(() => {
6
+ document.title = "About | __PROJECT_NAME__";
7
+ }, []);
8
+
4
9
  return (
5
10
  <div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
6
11
  <div className="mx-auto max-w-2xl px-4 text-center">
@@ -1,4 +1,6 @@
1
+ import { useEffect } from "react";
1
2
  import { Link } from "wouter";
3
+ import Button from "../../components/Button";
2
4
  import { useReactPersist } from "../../utils/Storage";
3
5
  import { useUrlState } from "../../utils/useUrlState";
4
6
 
@@ -6,6 +8,10 @@ const About = () => {
6
8
  const [sharedCount, setSharedCount] = useReactPersist("sharedCounter", 0);
7
9
  const [urlCount, setUrlCount] = useUrlState("counter", 0);
8
10
 
11
+ useEffect(() => {
12
+ document.title = "About | __PROJECT_NAME__";
13
+ }, []);
14
+
9
15
  return (
10
16
  <div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
11
17
  <div className="mx-auto max-w-2xl px-4 text-center">
@@ -26,21 +32,23 @@ const About = () => {
26
32
  This counter is shared between Home and About pages
27
33
  </p>
28
34
  <div className="flex items-center justify-center gap-4">
29
- <button
35
+ <Button
36
+ variant="danger"
37
+ aria-label="Decrease shared counter"
30
38
  onClick={() => setSharedCount(sharedCount - 1)}
31
- className="bg-brand-danger text-brand-white hover:bg-brand-danger/90 rounded-lg px-4 py-2 transition-colors"
32
39
  >
33
40
  -
34
- </button>
41
+ </Button>
35
42
  <span className="text-brand-black text-3xl font-bold">
36
43
  {sharedCount}
37
44
  </span>
38
- <button
45
+ <Button
46
+ variant="success"
47
+ aria-label="Increase shared counter"
39
48
  onClick={() => setSharedCount(sharedCount + 1)}
40
- className="bg-brand-success text-brand-white hover:bg-brand-success/90 rounded-lg px-4 py-2 transition-colors"
41
49
  >
42
50
  +
43
- </button>
51
+ </Button>
44
52
  </div>
45
53
  </div>
46
54
 
@@ -52,21 +60,23 @@ const About = () => {
52
60
  This counter syncs with URL parameters
53
61
  </p>
54
62
  <div className="flex items-center justify-center gap-4">
55
- <button
63
+ <Button
64
+ variant="danger"
65
+ aria-label="Decrease URL counter"
56
66
  onClick={() => setUrlCount(urlCount - 1)}
57
- className="bg-brand-danger text-brand-white hover:bg-brand-danger/90 rounded-lg px-4 py-2 transition-colors"
58
67
  >
59
68
  -
60
- </button>
69
+ </Button>
61
70
  <span className="text-brand-black text-3xl font-bold">
62
71
  {urlCount}
63
72
  </span>
64
- <button
73
+ <Button
74
+ variant="success"
75
+ aria-label="Increase URL counter"
65
76
  onClick={() => setUrlCount(urlCount + 1)}
66
- className="bg-brand-success text-brand-white hover:bg-brand-success/90 rounded-lg px-4 py-2 transition-colors"
67
77
  >
68
78
  +
69
- </button>
79
+ </Button>
70
80
  </div>
71
81
  </div>
72
82
  </div>
@@ -1,6 +1,14 @@
1
+ import { useEffect, useState } from "react";
1
2
  import { Link } from "wouter";
3
+ import Button from "../../components/Button";
2
4
 
3
5
  const Home = () => {
6
+ const [count, setCount] = useState(0);
7
+
8
+ useEffect(() => {
9
+ document.title = "__PROJECT_NAME__";
10
+ }, []);
11
+
4
12
  return (
5
13
  <div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
6
14
  <div className="mx-auto max-w-2xl px-4 text-center">
@@ -11,7 +19,7 @@ const Home = () => {
11
19
  Built with React, TypeScript, Vite, Wouter, and Tailwind CSS
12
20
  </p>
13
21
 
14
- <div className="mb-8">
22
+ <div className="mb-8 space-y-6">
15
23
  <div className="bg-brand-white rounded-lg p-6 shadow-md">
16
24
  <h2 className="text-brand-black mb-4 text-2xl font-semibold">
17
25
  Get Started
@@ -27,6 +35,34 @@ const Home = () => {
27
35
  This is a clean starting point for your React application.
28
36
  </p>
29
37
  </div>
38
+
39
+ <div className="bg-brand-white rounded-lg p-6 shadow-md">
40
+ <h2 className="text-brand-black mb-4 text-2xl font-semibold">
41
+ Counter
42
+ </h2>
43
+ <p className="text-brand-gray mb-4">
44
+ A simple counter using the shared Button component
45
+ </p>
46
+ <div className="flex items-center justify-center gap-4">
47
+ <Button
48
+ variant="danger"
49
+ aria-label="Decrease counter"
50
+ onClick={() => setCount(count - 1)}
51
+ >
52
+ -
53
+ </Button>
54
+ <span className="text-brand-black text-3xl font-bold">
55
+ {count}
56
+ </span>
57
+ <Button
58
+ variant="success"
59
+ aria-label="Increase counter"
60
+ onClick={() => setCount(count + 1)}
61
+ >
62
+ +
63
+ </Button>
64
+ </div>
65
+ </div>
30
66
  </div>
31
67
 
32
68
  <div className="flex justify-center gap-4">
@@ -1,4 +1,6 @@
1
+ import { useEffect } from "react";
1
2
  import { Link } from "wouter";
3
+ import Button from "../../components/Button";
2
4
  import { useReactPersist } from "../../utils/Storage";
3
5
  import { useUrlState } from "../../utils/useUrlState";
4
6
 
@@ -6,6 +8,10 @@ const Home = () => {
6
8
  const [sharedCount, setSharedCount] = useReactPersist("sharedCounter", 0);
7
9
  const [urlCount, setUrlCount] = useUrlState("counter", 0);
8
10
 
11
+ useEffect(() => {
12
+ document.title = "__PROJECT_NAME__";
13
+ }, []);
14
+
9
15
  return (
10
16
  <div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
11
17
  <div className="mx-auto max-w-2xl px-4 text-center">
@@ -25,21 +31,23 @@ const Home = () => {
25
31
  This counter is shared between Home and About pages
26
32
  </p>
27
33
  <div className="flex items-center justify-center gap-4">
28
- <button
34
+ <Button
35
+ variant="danger"
36
+ aria-label="Decrease shared counter"
29
37
  onClick={() => setSharedCount(sharedCount - 1)}
30
- className="bg-brand-danger text-brand-white hover:bg-brand-danger/90 rounded-lg px-4 py-2 transition-colors"
31
38
  >
32
39
  -
33
- </button>
40
+ </Button>
34
41
  <span className="text-brand-black text-3xl font-bold">
35
42
  {sharedCount}
36
43
  </span>
37
- <button
44
+ <Button
45
+ variant="success"
46
+ aria-label="Increase shared counter"
38
47
  onClick={() => setSharedCount(sharedCount + 1)}
39
- className="bg-brand-success text-brand-white hover:bg-brand-success/90 rounded-lg px-4 py-2 transition-colors"
40
48
  >
41
49
  +
42
- </button>
50
+ </Button>
43
51
  </div>
44
52
  </div>
45
53
 
@@ -51,21 +59,23 @@ const Home = () => {
51
59
  This counter syncs with URL parameters
52
60
  </p>
53
61
  <div className="flex items-center justify-center gap-4">
54
- <button
62
+ <Button
63
+ variant="danger"
64
+ aria-label="Decrease URL counter"
55
65
  onClick={() => setUrlCount(urlCount - 1)}
56
- className="bg-brand-danger text-brand-white hover:bg-brand-danger/90 rounded-lg px-4 py-2 transition-colors"
57
66
  >
58
67
  -
59
- </button>
68
+ </Button>
60
69
  <span className="text-brand-black text-3xl font-bold">
61
70
  {urlCount}
62
71
  </span>
63
- <button
72
+ <Button
73
+ variant="success"
74
+ aria-label="Increase URL counter"
64
75
  onClick={() => setUrlCount(urlCount + 1)}
65
- className="bg-brand-success text-brand-white hover:bg-brand-success/90 rounded-lg px-4 py-2 transition-colors"
66
76
  >
67
77
  +
68
- </button>
78
+ </Button>
69
79
  </div>
70
80
  </div>
71
81
  </div>
@@ -1,23 +1,30 @@
1
+ import { useEffect } from "react";
1
2
  import { Link } from "wouter";
2
3
 
3
- const NotFound = () => (
4
- <div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
5
- <div className="text-center">
6
- <h1 className="text-brand-black text-9xl font-bold">404</h1>
7
- <p className="text-brand-black mt-4 text-2xl font-semibold">
8
- Page Not Found
9
- </p>
10
- <p className="text-brand-gray mt-2">
11
- The page you're looking for doesn't exist.
12
- </p>
13
- <Link
14
- href="/"
15
- className="bg-brand-primary text-brand-white hover:bg-brand-primaryHover mt-6 inline-block rounded-lg px-6 py-3 transition-colors"
16
- >
17
- Go Home
18
- </Link>
4
+ const NotFound = () => {
5
+ useEffect(() => {
6
+ document.title = "Page Not Found | __PROJECT_NAME__";
7
+ }, []);
8
+
9
+ return (
10
+ <div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
11
+ <div className="text-center">
12
+ <h1 className="text-brand-black text-9xl font-bold">404</h1>
13
+ <p className="text-brand-black mt-4 text-2xl font-semibold">
14
+ Page Not Found
15
+ </p>
16
+ <p className="text-brand-gray mt-2">
17
+ The page you're looking for doesn't exist.
18
+ </p>
19
+ <Link
20
+ href="/"
21
+ className="bg-brand-primary text-brand-white hover:bg-brand-primaryHover mt-6 inline-block rounded-lg px-6 py-3 transition-colors"
22
+ >
23
+ Go Home
24
+ </Link>
25
+ </div>
19
26
  </div>
20
- </div>
21
- );
27
+ );
28
+ };
22
29
 
23
30
  export default NotFound;
@@ -0,0 +1 @@
1
+ export type ButtonVariant = "primary" | "success" | "danger";
@@ -1,3 +1,24 @@
1
- export default function classNames(...classes: string[]) {
2
- return classes.filter(Boolean).join(" ");
1
+ type ClassValue = string | false | null | undefined | Record<string, boolean>;
2
+
3
+ export default function classNames(...classes: ClassValue[]): string {
4
+ const result: string[] = [];
5
+
6
+ for (const entry of classes) {
7
+ if (!entry) {
8
+ continue;
9
+ }
10
+
11
+ if (typeof entry === "string") {
12
+ result.push(entry);
13
+ continue;
14
+ }
15
+
16
+ for (const [name, isEnabled] of Object.entries(entry)) {
17
+ if (isEnabled) {
18
+ result.push(name);
19
+ }
20
+ }
21
+ }
22
+
23
+ return result.join(" ");
3
24
  }
@@ -0,0 +1,22 @@
1
+ // Browsers store setTimeout delays as a 32-bit signed integer, so any delay
2
+ // above 2^31 - 1 ms (~24.8 days) overflows and fires immediately.
3
+ const MAX_TIMEOUT_MS = 2_147_483_647;
4
+
5
+ /**
6
+ * Drop-in replacement for setTimeout that clamps delays larger than the
7
+ * maximum 32-bit signed integer (2,147,483,647 ms / ~24.8 days) instead of
8
+ * letting them overflow and fire immediately.
9
+ */
10
+ export function safeTimeout<TArgs extends unknown[]>(
11
+ callback: (...callbackArgs: TArgs) => void,
12
+ delay: number,
13
+ ...args: TArgs
14
+ ): ReturnType<typeof globalThis.setTimeout> {
15
+ if (delay > MAX_TIMEOUT_MS) {
16
+ console.error(
17
+ `safeTimeout: delay ${delay} exceeds the maximum of ${MAX_TIMEOUT_MS} ms (~24.8 days). Using the maximum value.`,
18
+ );
19
+ delay = MAX_TIMEOUT_MS;
20
+ }
21
+ return globalThis.setTimeout(callback, delay, ...args);
22
+ }
@@ -4,16 +4,28 @@ import { useLocation, useSearch } from "wouter";
4
4
  export function useUrlState(
5
5
  key: string,
6
6
  defaultValue: number,
7
- ): [number, (value: number) => void] {
7
+ ): [number, (value: number) => void];
8
+ export function useUrlState(
9
+ key: string,
10
+ defaultValue: string,
11
+ ): [string, (value: string) => void];
12
+ export function useUrlState(
13
+ key: string,
14
+ defaultValue: string | number,
15
+ ): [string, (value: string) => void] | [number, (value: number) => void] {
8
16
  const [, setLocation] = useLocation();
9
17
  const searchParams = useSearch();
10
18
 
11
19
  const params = new URLSearchParams(searchParams);
12
20
  const urlValue = params.get(key);
13
- const value = urlValue ? Number.parseInt(urlValue, 10) : defaultValue;
21
+ const value = urlValue
22
+ ? typeof defaultValue === "number"
23
+ ? Number.parseInt(urlValue, 10)
24
+ : urlValue
25
+ : defaultValue;
14
26
 
15
27
  const setValue = useCallback(
16
- (newValue: number) => {
28
+ (newValue: string | number) => {
17
29
  const currentParams = new URLSearchParams(globalThis.location.search);
18
30
  currentParams.set(key, newValue.toString());
19
31
  setLocation(
@@ -23,5 +35,6 @@ export function useUrlState(
23
35
  [key, setLocation],
24
36
  );
25
37
 
26
- return [value, setValue];
38
+ // The overloads guarantee value/setValue agree with the default's type.
39
+ return [value, setValue] as [string, (value: string) => void];
27
40
  }