@techstream/quark-create-app 1.8.0 → 1.10.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.
- package/README.md +2 -2
- package/package.json +3 -3
- package/src/index.js +415 -150
- package/src/utils.js +36 -0
- package/src/utils.test.js +63 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.github/copilot-instructions.md +55 -0
- package/templates/base-project/.github/workflows/release.yml +37 -8
- package/templates/base-project/CLAUDE.md +273 -0
- package/templates/base-project/README.md +72 -30
- package/templates/base-project/apps/web/next.config.js +5 -1
- package/templates/base-project/apps/web/package.json +7 -5
- package/templates/base-project/apps/web/public/quark.svg +46 -0
- package/templates/base-project/apps/web/railway.json +2 -2
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +56 -17
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +53 -0
- package/templates/base-project/apps/web/src/app/globals.css +121 -15
- package/templates/base-project/apps/web/src/app/icon.svg +46 -0
- package/templates/base-project/apps/web/src/app/layout.js +1 -0
- package/templates/base-project/apps/web/src/app/not-found.js +35 -0
- package/templates/base-project/apps/web/src/app/page.js +38 -5
- package/templates/base-project/apps/web/src/lib/theme.js +23 -0
- package/templates/base-project/apps/web/src/proxy.js +10 -2
- package/templates/base-project/package.json +16 -1
- package/templates/base-project/packages/db/package.json +4 -4
- package/templates/base-project/packages/db/src/client.js +6 -1
- package/templates/base-project/packages/db/src/index.js +1 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/scripts/doctor.js +261 -0
- package/templates/base-project/turbo.json +2 -1
- package/templates/config/package.json +1 -0
- package/templates/config/src/index.js +1 -3
- package/templates/config/src/validate-env.js +79 -3
- package/templates/jobs/package.json +2 -1
- package/templates/ui/README.md +67 -0
- package/templates/ui/package.json +1 -0
- package/templates/ui/src/badge.js +32 -0
- package/templates/ui/src/badge.test.js +42 -0
- package/templates/ui/src/button.js +64 -15
- package/templates/ui/src/button.test.js +34 -5
- package/templates/ui/src/card.js +58 -0
- package/templates/ui/src/card.test.js +59 -0
- package/templates/ui/src/checkbox.js +35 -0
- package/templates/ui/src/checkbox.test.js +35 -0
- package/templates/ui/src/dialog.js +139 -0
- package/templates/ui/src/dialog.test.js +15 -0
- package/templates/ui/src/index.js +16 -0
- package/templates/ui/src/input.js +15 -0
- package/templates/ui/src/input.test.js +27 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/label.test.js +22 -0
- package/templates/ui/src/select.js +42 -0
- package/templates/ui/src/select.test.js +27 -0
- package/templates/ui/src/skeleton.js +14 -0
- package/templates/ui/src/skeleton.test.js +22 -0
- package/templates/ui/src/table.js +75 -0
- package/templates/ui/src/table.test.js +69 -0
- package/templates/ui/src/textarea.js +15 -0
- package/templates/ui/src/textarea.test.js +27 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +132 -0
- package/templates/ui/src/toast.js +229 -0
- package/templates/ui/src/toast.test.js +23 -0
- package/templates/{base-project/apps/worker → worker}/package.json +2 -2
- package/templates/{base-project/apps/worker → worker}/src/index.js +38 -23
- package/templates/{base-project/apps/worker → worker}/src/index.test.js +19 -20
- package/templates/base-project/apps/web/public/file.svg +0 -1
- package/templates/base-project/apps/web/public/globe.svg +0 -1
- package/templates/base-project/apps/web/public/next.svg +0 -1
- package/templates/base-project/apps/web/public/vercel.svg +0 -1
- package/templates/base-project/apps/web/public/window.svg +0 -1
- /package/templates/{base-project/apps/worker → worker}/README.md +0 -0
- /package/templates/{base-project/apps/worker → worker}/railway.json +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/email.js +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/files.js +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/index.js +0 -0
|
@@ -6,18 +6,47 @@ test("Button - component exports correctly", () => {
|
|
|
6
6
|
assert(typeof Button === "function", "Button should be a function");
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
test("Button -
|
|
10
|
-
|
|
11
|
-
const result = Button({ variant: "primary", className: "custom" });
|
|
9
|
+
test("Button - renders with default props", () => {
|
|
10
|
+
const result = Button({});
|
|
12
11
|
assert.ok(result, "Component should return an element");
|
|
13
12
|
});
|
|
14
13
|
|
|
15
14
|
test("Button - supports primary variant", () => {
|
|
16
15
|
const result = Button({ variant: "primary" });
|
|
17
|
-
assert.ok(result
|
|
16
|
+
assert.ok(result);
|
|
18
17
|
});
|
|
19
18
|
|
|
20
19
|
test("Button - supports secondary variant", () => {
|
|
21
20
|
const result = Button({ variant: "secondary" });
|
|
22
|
-
assert.ok(result
|
|
21
|
+
assert.ok(result);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("Button - supports danger variant", () => {
|
|
25
|
+
const result = Button({ variant: "danger" });
|
|
26
|
+
assert.ok(result);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("Button - supports ghost variant", () => {
|
|
30
|
+
const result = Button({ variant: "ghost" });
|
|
31
|
+
assert.ok(result);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("Button - supports sm size", () => {
|
|
35
|
+
const result = Button({ size: "sm" });
|
|
36
|
+
assert.ok(result);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("Button - supports md size", () => {
|
|
40
|
+
const result = Button({ size: "md" });
|
|
41
|
+
assert.ok(result);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("Button - supports lg size", () => {
|
|
45
|
+
const result = Button({ size: "lg" });
|
|
46
|
+
assert.ok(result);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("Button - accepts className override", () => {
|
|
50
|
+
const result = Button({ className: "custom-class" });
|
|
51
|
+
assert.ok(result);
|
|
23
52
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const THEMES = {
|
|
4
|
+
light: {
|
|
5
|
+
card: "rounded border border-gray-200 bg-white/95 shadow-sm transition-shadow duration-200 hover:shadow-md",
|
|
6
|
+
header: "flex flex-col space-y-1.5 p-6",
|
|
7
|
+
title: "text-lg font-semibold leading-none tracking-tight text-gray-900",
|
|
8
|
+
content: "p-6 pt-0",
|
|
9
|
+
footer: "flex items-center p-6 pt-0",
|
|
10
|
+
},
|
|
11
|
+
dark: {
|
|
12
|
+
card: "rounded border border-[#1e2535] bg-[#0d1117] transition-all duration-200 hover:border-[#377dff]/30",
|
|
13
|
+
header: "flex flex-col space-y-1.5 p-6",
|
|
14
|
+
title: "text-lg font-semibold leading-none tracking-tight text-[#e0e0e0]",
|
|
15
|
+
content: "p-6 pt-0",
|
|
16
|
+
footer: "flex items-center p-6 pt-0",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function Card({ theme = "light", className = "", ...props }) {
|
|
21
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
22
|
+
return React.createElement("div", {
|
|
23
|
+
className: `${t.card} ${className}`.trim(),
|
|
24
|
+
...props,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function CardHeader({ theme = "light", className = "", ...props }) {
|
|
29
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
30
|
+
return React.createElement("div", {
|
|
31
|
+
className: `${t.header} ${className}`.trim(),
|
|
32
|
+
...props,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function CardTitle({ theme = "light", className = "", ...props }) {
|
|
37
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
38
|
+
return React.createElement("h3", {
|
|
39
|
+
className: `${t.title} ${className}`.trim(),
|
|
40
|
+
...props,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function CardContent({ theme = "light", className = "", ...props }) {
|
|
45
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
46
|
+
return React.createElement("div", {
|
|
47
|
+
className: `${t.content} ${className}`.trim(),
|
|
48
|
+
...props,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function CardFooter({ theme = "light", className = "", ...props }) {
|
|
53
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
54
|
+
return React.createElement("div", {
|
|
55
|
+
className: `${t.footer} ${className}`.trim(),
|
|
56
|
+
...props,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardFooter,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from "./card.js";
|
|
10
|
+
|
|
11
|
+
test("Card - exports correctly", () => {
|
|
12
|
+
assert(typeof Card === "function");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("CardHeader - exports correctly", () => {
|
|
16
|
+
assert(typeof CardHeader === "function");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("CardTitle - exports correctly", () => {
|
|
20
|
+
assert(typeof CardTitle === "function");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("CardContent - exports correctly", () => {
|
|
24
|
+
assert(typeof CardContent === "function");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("CardFooter - exports correctly", () => {
|
|
28
|
+
assert(typeof CardFooter === "function");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("Card - renders with default props", () => {
|
|
32
|
+
const result = Card({});
|
|
33
|
+
assert.ok(result);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("CardHeader - renders with default props", () => {
|
|
37
|
+
const result = CardHeader({});
|
|
38
|
+
assert.ok(result);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("CardTitle - renders with default props", () => {
|
|
42
|
+
const result = CardTitle({});
|
|
43
|
+
assert.ok(result);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("CardContent - renders with default props", () => {
|
|
47
|
+
const result = CardContent({});
|
|
48
|
+
assert.ok(result);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("CardFooter - renders with default props", () => {
|
|
52
|
+
const result = CardFooter({});
|
|
53
|
+
assert.ok(result);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("Card - accepts className override", () => {
|
|
57
|
+
const result = Card({ className: "custom" });
|
|
58
|
+
assert.ok(result);
|
|
59
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const THEMES = {
|
|
4
|
+
light: {
|
|
5
|
+
input:
|
|
6
|
+
"h-4 w-4 rounded border-gray-300 text-blue-600 shadow-sm transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-blue-500/40 disabled:cursor-not-allowed disabled:opacity-60",
|
|
7
|
+
label: "text-sm text-gray-700 select-none",
|
|
8
|
+
},
|
|
9
|
+
dark: {
|
|
10
|
+
input:
|
|
11
|
+
"h-4 w-4 rounded border-[#1e2535] bg-[#090d14] accent-[#377dff] cursor-pointer focus-visible:ring-2 focus-visible:ring-[#377dff]/40 disabled:cursor-not-allowed disabled:opacity-40",
|
|
12
|
+
label: "text-sm text-[#6b7a99] font-mono select-none",
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Checkbox({
|
|
17
|
+
id,
|
|
18
|
+
label,
|
|
19
|
+
theme = "light",
|
|
20
|
+
className = "",
|
|
21
|
+
...props
|
|
22
|
+
}) {
|
|
23
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
24
|
+
return React.createElement(
|
|
25
|
+
"div",
|
|
26
|
+
{ className: "flex items-center gap-2" },
|
|
27
|
+
React.createElement("input", {
|
|
28
|
+
id,
|
|
29
|
+
type: "checkbox",
|
|
30
|
+
className: `${t.input} ${className}`.trim(),
|
|
31
|
+
...props,
|
|
32
|
+
}),
|
|
33
|
+
React.createElement("label", { htmlFor: id, className: t.label }, label),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Checkbox } from "./checkbox.js";
|
|
4
|
+
|
|
5
|
+
test("Checkbox - exports correctly", () => {
|
|
6
|
+
assert(typeof Checkbox === "function");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("Checkbox - renders with default props", () => {
|
|
10
|
+
const result = Checkbox({ id: "check1", label: "Accept terms" });
|
|
11
|
+
assert.ok(result);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("Checkbox - accepts className override", () => {
|
|
15
|
+
const result = Checkbox({
|
|
16
|
+
id: "check2",
|
|
17
|
+
label: "Option",
|
|
18
|
+
className: "custom",
|
|
19
|
+
});
|
|
20
|
+
assert.ok(result);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("Checkbox - accepts checked prop", () => {
|
|
24
|
+
const result = Checkbox({
|
|
25
|
+
id: "check3",
|
|
26
|
+
label: "Checked",
|
|
27
|
+
defaultChecked: true,
|
|
28
|
+
});
|
|
29
|
+
assert.ok(result);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("Checkbox - accepts disabled prop", () => {
|
|
33
|
+
const result = Checkbox({ id: "check4", label: "Disabled", disabled: true });
|
|
34
|
+
assert.ok(result);
|
|
35
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dialog — centered modal built on the native <dialog> element.
|
|
6
|
+
*
|
|
7
|
+
* Props:
|
|
8
|
+
* open (bool) — controlled open state
|
|
9
|
+
* onClose (fn) — called when the dialog should close
|
|
10
|
+
* title (string) — header text
|
|
11
|
+
* children — dialog body content
|
|
12
|
+
* theme (string) — 'light' (default) | 'dark'
|
|
13
|
+
* className (string) — merged onto the <dialog> element
|
|
14
|
+
*
|
|
15
|
+
* Architecture:
|
|
16
|
+
* • ALL close paths (header ×, backdrop click, Escape, external button)
|
|
17
|
+
* flow through the `open` prop → useEffect, which owns the single
|
|
18
|
+
* animation + el.close() call. handleClose() just calls onClose().
|
|
19
|
+
* • isClosingRef (ref, not state) prevents the effect from double-firing.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const THEMES = {
|
|
23
|
+
light: {
|
|
24
|
+
dialog:
|
|
25
|
+
"backdrop:bg-black/50 rounded border border-gray-200 bg-white p-0 shadow-2xl w-full max-w-lg",
|
|
26
|
+
header:
|
|
27
|
+
"flex items-center justify-between border-b border-gray-200 px-5 py-4",
|
|
28
|
+
title: "text-base font-semibold text-gray-900",
|
|
29
|
+
close:
|
|
30
|
+
"flex h-8 w-8 cursor-pointer items-center justify-center rounded-sm text-lg text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-700 active:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-300",
|
|
31
|
+
body: "px-5 py-4 text-gray-700",
|
|
32
|
+
},
|
|
33
|
+
dark: {
|
|
34
|
+
dialog:
|
|
35
|
+
"backdrop:bg-black/70 rounded border border-[#1e2535] bg-[#0d1117] p-0 shadow-2xl w-full max-w-lg",
|
|
36
|
+
header:
|
|
37
|
+
"flex items-center justify-between border-b border-[#1e2535] px-5 py-4",
|
|
38
|
+
title: "text-base font-semibold text-[#e0e0e0]",
|
|
39
|
+
close:
|
|
40
|
+
"flex h-8 w-8 cursor-pointer items-center justify-center rounded-sm text-lg text-[#4a4a6a] transition-colors hover:bg-[#1e2535] hover:text-[#e0e0e0] active:bg-[#1e2535] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#377dff]/40",
|
|
41
|
+
body: "px-5 py-4 text-[#6b7a99]",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function Dialog({
|
|
46
|
+
open,
|
|
47
|
+
onClose,
|
|
48
|
+
title,
|
|
49
|
+
children,
|
|
50
|
+
theme = "light",
|
|
51
|
+
className = "",
|
|
52
|
+
}) {
|
|
53
|
+
const ref = useRef(null);
|
|
54
|
+
const [isClosing, setIsClosing] = useState(false);
|
|
55
|
+
const isClosingRef = useRef(false);
|
|
56
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
57
|
+
|
|
58
|
+
// Single source of truth for open/close — drives both animation and native element
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const el = ref.current;
|
|
61
|
+
if (!el) return;
|
|
62
|
+
|
|
63
|
+
if (open) {
|
|
64
|
+
if (!el.open && typeof el.showModal === "function") el.showModal();
|
|
65
|
+
isClosingRef.current = false;
|
|
66
|
+
setIsClosing(false);
|
|
67
|
+
} else {
|
|
68
|
+
// Play exit animation then close native element — regardless of who triggered close
|
|
69
|
+
if (isClosingRef.current) return; // already animating out, don't double-trigger
|
|
70
|
+
isClosingRef.current = true;
|
|
71
|
+
setIsClosing(true);
|
|
72
|
+
const t = setTimeout(() => {
|
|
73
|
+
if (el.open && typeof el.close === "function") el.close();
|
|
74
|
+
setIsClosing(false);
|
|
75
|
+
isClosingRef.current = false;
|
|
76
|
+
}, 180);
|
|
77
|
+
return () => clearTimeout(t);
|
|
78
|
+
}
|
|
79
|
+
}, [open]);
|
|
80
|
+
|
|
81
|
+
// All close-trigger sources just call onClose() — parent sets open=false,
|
|
82
|
+
// which re-runs the effect above and plays the animation.
|
|
83
|
+
const handleClose = useCallback(() => {
|
|
84
|
+
if (!isClosingRef.current) onClose?.();
|
|
85
|
+
}, [onClose]);
|
|
86
|
+
|
|
87
|
+
// Backdrop click (target is the <dialog> element itself)
|
|
88
|
+
const handleDialogClick = useCallback(
|
|
89
|
+
(e) => {
|
|
90
|
+
if (e.target === ref.current) handleClose();
|
|
91
|
+
},
|
|
92
|
+
[handleClose],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Intercept Escape so we can animate before the native dialog closes
|
|
96
|
+
const handleCancel = useCallback(
|
|
97
|
+
(e) => {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
handleClose();
|
|
100
|
+
},
|
|
101
|
+
[handleClose],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const animStyle =
|
|
105
|
+
open || isClosing
|
|
106
|
+
? isClosing
|
|
107
|
+
? { animation: "quark-dialog-out 0.18s ease-in forwards" }
|
|
108
|
+
: { animation: "quark-dialog-in 0.2s ease-out forwards" }
|
|
109
|
+
: {};
|
|
110
|
+
|
|
111
|
+
return React.createElement(
|
|
112
|
+
"dialog",
|
|
113
|
+
{
|
|
114
|
+
ref,
|
|
115
|
+
onClick: handleDialogClick,
|
|
116
|
+
onCancel: handleCancel,
|
|
117
|
+
style: animStyle,
|
|
118
|
+
className: `${t.dialog} ${className}`.trim(),
|
|
119
|
+
},
|
|
120
|
+
// Header
|
|
121
|
+
React.createElement(
|
|
122
|
+
"div",
|
|
123
|
+
{ className: t.header },
|
|
124
|
+
React.createElement("h2", { className: t.title }, title),
|
|
125
|
+
React.createElement(
|
|
126
|
+
"button",
|
|
127
|
+
{
|
|
128
|
+
type: "button",
|
|
129
|
+
"aria-label": "Close dialog",
|
|
130
|
+
onClick: handleClose,
|
|
131
|
+
className: t.close,
|
|
132
|
+
},
|
|
133
|
+
"\u00d7",
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
// Body
|
|
137
|
+
React.createElement("div", { className: t.body }, children),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Dialog } from "./dialog.js";
|
|
4
|
+
|
|
5
|
+
// Dialog uses React hooks (useRef, useEffect) and requires a React renderer to
|
|
6
|
+
// render correctly. Full render tests belong in an integration test suite with
|
|
7
|
+
// jsdom + react-dom. Here we verify the export contract only.
|
|
8
|
+
|
|
9
|
+
test("Dialog - exports correctly", () => {
|
|
10
|
+
assert(typeof Dialog === "function");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("Dialog - has expected function name", () => {
|
|
14
|
+
assert.strictEqual(Dialog.name, "Dialog");
|
|
15
|
+
});
|
|
@@ -1 +1,17 @@
|
|
|
1
|
+
// Server components
|
|
2
|
+
export * from "./badge.js";
|
|
1
3
|
export * from "./button.js";
|
|
4
|
+
export * from "./card.js";
|
|
5
|
+
export * from "./checkbox.js";
|
|
6
|
+
// Client components ("use client")
|
|
7
|
+
export * from "./dialog.js";
|
|
8
|
+
export * from "./input.js";
|
|
9
|
+
export * from "./label.js";
|
|
10
|
+
export * from "./select.js";
|
|
11
|
+
export * from "./skeleton.js";
|
|
12
|
+
export * from "./table.js";
|
|
13
|
+
export * from "./textarea.js";
|
|
14
|
+
export * from "./theme.js";
|
|
15
|
+
// Theme context
|
|
16
|
+
export * from "./theme-constants.js";
|
|
17
|
+
export * from "./toast.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const THEMES = {
|
|
4
|
+
light:
|
|
5
|
+
"block h-10 w-full rounded-sm border border-gray-300 bg-white px-3 text-sm text-gray-900 placeholder-gray-400 shadow-sm transition-all duration-200 hover:border-gray-400 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30 disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed",
|
|
6
|
+
dark: "block h-10 w-full rounded-sm border border-[#1e2535] bg-[#090d14] px-3 text-sm text-[#e0e0e0] placeholder-[#2d3a52] font-mono transition-all duration-200 hover:border-[#377dff]/30 focus-visible:border-[#377dff]/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#377dff]/15 disabled:opacity-30 disabled:cursor-not-allowed",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function Input({ theme = "light", className = "", ...props }) {
|
|
10
|
+
const base = THEMES[theme] ?? THEMES.light;
|
|
11
|
+
return React.createElement("input", {
|
|
12
|
+
className: `${base} ${className}`.trim(),
|
|
13
|
+
...props,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Input } from "./input.js";
|
|
4
|
+
|
|
5
|
+
test("Input - exports correctly", () => {
|
|
6
|
+
assert(typeof Input === "function");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("Input - renders with default props", () => {
|
|
10
|
+
const result = Input({});
|
|
11
|
+
assert.ok(result);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("Input - accepts className override", () => {
|
|
15
|
+
const result = Input({ className: "w-32", placeholder: "test" });
|
|
16
|
+
assert.ok(result);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("Input - accepts type prop", () => {
|
|
20
|
+
const result = Input({ type: "email" });
|
|
21
|
+
assert.ok(result);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("Input - accepts disabled prop", () => {
|
|
25
|
+
const result = Input({ disabled: true });
|
|
26
|
+
assert.ok(result);
|
|
27
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const THEMES = {
|
|
4
|
+
light: "block text-sm font-medium text-gray-700 tracking-tight",
|
|
5
|
+
dark: "block text-sm font-medium text-[#4a4a6a] tracking-tight font-mono",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function Label({ theme = "light", className = "", ...props }) {
|
|
9
|
+
const base = THEMES[theme] ?? THEMES.light;
|
|
10
|
+
return React.createElement("label", {
|
|
11
|
+
className: `${base} ${className}`.trim(),
|
|
12
|
+
...props,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Label } from "./label.js";
|
|
4
|
+
|
|
5
|
+
test("Label - exports correctly", () => {
|
|
6
|
+
assert(typeof Label === "function");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("Label - renders with default props", () => {
|
|
10
|
+
const result = Label({});
|
|
11
|
+
assert.ok(result);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("Label - accepts className override", () => {
|
|
15
|
+
const result = Label({ className: "custom" });
|
|
16
|
+
assert.ok(result);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("Label - accepts htmlFor prop", () => {
|
|
20
|
+
const result = Label({ htmlFor: "my-input", children: "My label" });
|
|
21
|
+
assert.ok(result);
|
|
22
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const THEMES = {
|
|
4
|
+
light: {
|
|
5
|
+
select:
|
|
6
|
+
"block h-10 w-full appearance-none rounded-sm border border-gray-300 bg-white px-3 pr-9 text-sm text-gray-900 shadow-sm transition-all duration-200 cursor-pointer hover:border-gray-400 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30 disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed",
|
|
7
|
+
chevron:
|
|
8
|
+
"absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none",
|
|
9
|
+
},
|
|
10
|
+
dark: {
|
|
11
|
+
select:
|
|
12
|
+
"block h-10 w-full appearance-none rounded-sm border border-[#1e2535] bg-[#090d14] px-3 pr-9 text-sm text-[#e0e0e0] font-mono transition-all duration-200 cursor-pointer hover:border-[#377dff]/30 focus-visible:border-[#377dff]/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#377dff]/15 disabled:opacity-30 disabled:cursor-not-allowed",
|
|
13
|
+
chevron:
|
|
14
|
+
"absolute right-3 top-1/2 -translate-y-1/2 text-[#4a4a6a] pointer-events-none",
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function Select({
|
|
19
|
+
theme = "light",
|
|
20
|
+
className = "",
|
|
21
|
+
children,
|
|
22
|
+
...props
|
|
23
|
+
}) {
|
|
24
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
25
|
+
return React.createElement(
|
|
26
|
+
"div",
|
|
27
|
+
{ className: "relative" },
|
|
28
|
+
React.createElement(
|
|
29
|
+
"select",
|
|
30
|
+
{ className: `${t.select} ${className}`.trim(), ...props },
|
|
31
|
+
children,
|
|
32
|
+
),
|
|
33
|
+
React.createElement(
|
|
34
|
+
"span",
|
|
35
|
+
{
|
|
36
|
+
"aria-hidden": "true",
|
|
37
|
+
className: t.chevron,
|
|
38
|
+
},
|
|
39
|
+
"\u25be",
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Select } from "./select.js";
|
|
4
|
+
|
|
5
|
+
test("Select - exports correctly", () => {
|
|
6
|
+
assert(typeof Select === "function");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("Select - renders with default props", () => {
|
|
10
|
+
const result = Select({});
|
|
11
|
+
assert.ok(result);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("Select - accepts className override", () => {
|
|
15
|
+
const result = Select({ className: "w-48" });
|
|
16
|
+
assert.ok(result);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("Select - accepts children", () => {
|
|
20
|
+
const result = Select({ children: null });
|
|
21
|
+
assert.ok(result);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("Select - accepts disabled prop", () => {
|
|
25
|
+
const result = Select({ disabled: true });
|
|
26
|
+
assert.ok(result);
|
|
27
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const THEMES = {
|
|
4
|
+
light: "animate-pulse rounded-lg bg-gray-200/80",
|
|
5
|
+
dark: "animate-pulse rounded-lg bg-[#1e2535]/70",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function Skeleton({ theme = "light", className = "", ...props }) {
|
|
9
|
+
const base = THEMES[theme] ?? THEMES.light;
|
|
10
|
+
return React.createElement("div", {
|
|
11
|
+
className: `${base} ${className}`.trim(),
|
|
12
|
+
...props,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Skeleton } from "./skeleton.js";
|
|
4
|
+
|
|
5
|
+
test("Skeleton - exports correctly", () => {
|
|
6
|
+
assert(typeof Skeleton === "function");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("Skeleton - renders with default props", () => {
|
|
10
|
+
const result = Skeleton({});
|
|
11
|
+
assert.ok(result);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("Skeleton - accepts className for dimensions", () => {
|
|
15
|
+
const result = Skeleton({ className: "h-4 w-32" });
|
|
16
|
+
assert.ok(result);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("Skeleton - accepts aria props", () => {
|
|
20
|
+
const result = Skeleton({ "aria-label": "Loading..." });
|
|
21
|
+
assert.ok(result);
|
|
22
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const THEMES = {
|
|
4
|
+
light: {
|
|
5
|
+
wrapper: "w-full overflow-auto rounded-xl border border-gray-200 bg-white",
|
|
6
|
+
table: "w-full caption-bottom text-sm",
|
|
7
|
+
header: "border-b bg-gray-50",
|
|
8
|
+
body: "[&_tr:last-child]:border-0",
|
|
9
|
+
row: "border-b border-gray-100 transition-colors hover:bg-gray-50/80 active:bg-gray-100",
|
|
10
|
+
head: "h-11 px-3 text-left align-middle font-medium text-gray-600",
|
|
11
|
+
cell: "p-3 align-middle text-gray-700",
|
|
12
|
+
},
|
|
13
|
+
dark: {
|
|
14
|
+
wrapper:
|
|
15
|
+
"w-full overflow-auto rounded-xl border border-[#1e2535] bg-[#0d1117]",
|
|
16
|
+
table: "w-full caption-bottom text-sm",
|
|
17
|
+
header: "border-b border-[#1e2535] bg-[#090d14]",
|
|
18
|
+
body: "[&_tr:last-child]:border-0",
|
|
19
|
+
row: "border-b border-[#1e2535]/50 transition-colors hover:bg-[#1e2535]/40",
|
|
20
|
+
head: "h-11 px-3 text-left align-middle font-medium text-[#4a4a6a] font-mono text-xs uppercase tracking-widest",
|
|
21
|
+
cell: "p-3 align-middle text-[#e0e0e0] font-mono text-sm",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function Table({ theme = "light", className = "", ...props }) {
|
|
26
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
27
|
+
return React.createElement(
|
|
28
|
+
"div",
|
|
29
|
+
{ className: t.wrapper },
|
|
30
|
+
React.createElement("table", {
|
|
31
|
+
className: `${t.table} ${className}`.trim(),
|
|
32
|
+
...props,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function TableHeader({ theme = "light", className = "", ...props }) {
|
|
38
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
39
|
+
return React.createElement("thead", {
|
|
40
|
+
className: `${t.header} ${className}`.trim(),
|
|
41
|
+
...props,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function TableBody({ theme = "light", className = "", ...props }) {
|
|
46
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
47
|
+
return React.createElement("tbody", {
|
|
48
|
+
className: `${t.body} ${className}`.trim(),
|
|
49
|
+
...props,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function TableRow({ theme = "light", className = "", ...props }) {
|
|
54
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
55
|
+
return React.createElement("tr", {
|
|
56
|
+
className: `${t.row} ${className}`.trim(),
|
|
57
|
+
...props,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function TableHead({ theme = "light", className = "", ...props }) {
|
|
62
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
63
|
+
return React.createElement("th", {
|
|
64
|
+
className: `${t.head} ${className}`.trim(),
|
|
65
|
+
...props,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function TableCell({ theme = "light", className = "", ...props }) {
|
|
70
|
+
const t = THEMES[theme] ?? THEMES.light;
|
|
71
|
+
return React.createElement("td", {
|
|
72
|
+
className: `${t.cell} ${className}`.trim(),
|
|
73
|
+
...props,
|
|
74
|
+
});
|
|
75
|
+
}
|