@techstream/quark-create-app 1.9.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.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/index.js +376 -143
  4. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  5. package/templates/base-project/.github/copilot-instructions.md +55 -0
  6. package/templates/base-project/CLAUDE.md +273 -0
  7. package/templates/base-project/README.md +72 -30
  8. package/templates/base-project/apps/web/next.config.js +5 -1
  9. package/templates/base-project/apps/web/package.json +3 -3
  10. package/templates/base-project/apps/web/public/quark.svg +46 -0
  11. package/templates/base-project/apps/web/railway.json +2 -2
  12. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  13. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  14. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  15. package/templates/base-project/apps/web/src/app/api/health/route.js +28 -17
  16. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  17. package/templates/base-project/apps/web/src/app/global-error.js +53 -0
  18. package/templates/base-project/apps/web/src/app/globals.css +121 -15
  19. package/templates/base-project/apps/web/src/app/icon.svg +46 -0
  20. package/templates/base-project/apps/web/src/app/layout.js +1 -0
  21. package/templates/base-project/apps/web/src/app/not-found.js +35 -0
  22. package/templates/base-project/apps/web/src/app/page.js +38 -5
  23. package/templates/base-project/apps/web/src/lib/theme.js +23 -0
  24. package/templates/base-project/apps/web/src/proxy.js +10 -2
  25. package/templates/base-project/package.json +2 -0
  26. package/templates/base-project/packages/db/src/client.js +6 -1
  27. package/templates/base-project/packages/db/src/index.js +1 -0
  28. package/templates/base-project/packages/db/src/ping.js +66 -0
  29. package/templates/base-project/scripts/doctor.js +261 -0
  30. package/templates/base-project/turbo.json +2 -1
  31. package/templates/config/package.json +1 -0
  32. package/templates/jobs/package.json +2 -1
  33. package/templates/ui/README.md +67 -0
  34. package/templates/ui/package.json +1 -0
  35. package/templates/ui/src/badge.js +32 -0
  36. package/templates/ui/src/badge.test.js +42 -0
  37. package/templates/ui/src/button.js +64 -15
  38. package/templates/ui/src/button.test.js +34 -5
  39. package/templates/ui/src/card.js +58 -0
  40. package/templates/ui/src/card.test.js +59 -0
  41. package/templates/ui/src/checkbox.js +35 -0
  42. package/templates/ui/src/checkbox.test.js +35 -0
  43. package/templates/ui/src/dialog.js +139 -0
  44. package/templates/ui/src/dialog.test.js +15 -0
  45. package/templates/ui/src/index.js +16 -0
  46. package/templates/ui/src/input.js +15 -0
  47. package/templates/ui/src/input.test.js +27 -0
  48. package/templates/ui/src/label.js +14 -0
  49. package/templates/ui/src/label.test.js +22 -0
  50. package/templates/ui/src/select.js +42 -0
  51. package/templates/ui/src/select.test.js +27 -0
  52. package/templates/ui/src/skeleton.js +14 -0
  53. package/templates/ui/src/skeleton.test.js +22 -0
  54. package/templates/ui/src/table.js +75 -0
  55. package/templates/ui/src/table.test.js +69 -0
  56. package/templates/ui/src/textarea.js +15 -0
  57. package/templates/ui/src/textarea.test.js +27 -0
  58. package/templates/ui/src/theme-constants.js +24 -0
  59. package/templates/ui/src/theme.js +132 -0
  60. package/templates/ui/src/toast.js +229 -0
  61. package/templates/ui/src/toast.test.js +23 -0
  62. package/templates/{base-project/apps/worker → worker}/package.json +2 -2
  63. package/templates/{base-project/apps/worker → worker}/src/index.js +38 -23
  64. package/templates/{base-project/apps/worker → worker}/src/index.test.js +19 -20
  65. package/templates/base-project/apps/web/public/file.svg +0 -1
  66. package/templates/base-project/apps/web/public/globe.svg +0 -1
  67. package/templates/base-project/apps/web/public/next.svg +0 -1
  68. package/templates/base-project/apps/web/public/vercel.svg +0 -1
  69. package/templates/base-project/apps/web/public/window.svg +0 -1
  70. /package/templates/{base-project/apps/worker → worker}/README.md +0 -0
  71. /package/templates/{base-project/apps/worker → worker}/railway.json +0 -0
  72. /package/templates/{base-project/apps/worker → worker}/src/handlers/email.js +0 -0
  73. /package/templates/{base-project/apps/worker → worker}/src/handlers/files.js +0 -0
  74. /package/templates/{base-project/apps/worker → worker}/src/handlers/index.js +0 -0
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+
3
+ const base =
4
+ "inline-flex items-center rounded-sm border px-2.5 py-0.5 text-xs font-medium tracking-wide";
5
+
6
+ const THEMES = {
7
+ light: {
8
+ default: "border-gray-200 bg-gray-100 text-gray-800",
9
+ success: "border-green-200 bg-green-100 text-green-800",
10
+ warning: "border-yellow-200 bg-yellow-100 text-yellow-800",
11
+ danger: "border-red-200 bg-red-100 text-red-800",
12
+ info: "border-blue-200 bg-blue-100 text-blue-800",
13
+ },
14
+ dark: {
15
+ default: "border-[#1e2535] bg-[#1e2535]/50 text-[#6b7a99]",
16
+ success: "border-emerald-800/50 bg-emerald-900/20 text-emerald-400",
17
+ warning: "border-yellow-800/50 bg-yellow-900/20 text-yellow-400",
18
+ danger: "border-[#ff4757]/30 bg-[#ff4757]/10 text-[#ff4757]",
19
+ info: "border-[#377dff]/30 bg-[#377dff]/10 text-[#377dff]",
20
+ },
21
+ };
22
+
23
+ export function Badge({
24
+ variant = "default",
25
+ theme = "light",
26
+ className = "",
27
+ ...props
28
+ }) {
29
+ const t = THEMES[theme] ?? THEMES.light;
30
+ const cls = `${base} ${t[variant] ?? t.default} ${className}`.trim();
31
+ return React.createElement("span", { className: cls, ...props });
32
+ }
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert";
2
+ import { test } from "node:test";
3
+ import { Badge } from "./badge.js";
4
+
5
+ test("Badge - exports correctly", () => {
6
+ assert(typeof Badge === "function");
7
+ });
8
+
9
+ test("Badge - renders with default props", () => {
10
+ const result = Badge({});
11
+ assert.ok(result);
12
+ });
13
+
14
+ test("Badge - supports default variant", () => {
15
+ const result = Badge({ variant: "default" });
16
+ assert.ok(result);
17
+ });
18
+
19
+ test("Badge - supports success variant", () => {
20
+ const result = Badge({ variant: "success" });
21
+ assert.ok(result);
22
+ });
23
+
24
+ test("Badge - supports warning variant", () => {
25
+ const result = Badge({ variant: "warning" });
26
+ assert.ok(result);
27
+ });
28
+
29
+ test("Badge - supports danger variant", () => {
30
+ const result = Badge({ variant: "danger" });
31
+ assert.ok(result);
32
+ });
33
+
34
+ test("Badge - supports info variant", () => {
35
+ const result = Badge({ variant: "info" });
36
+ assert.ok(result);
37
+ });
38
+
39
+ test("Badge - accepts className override", () => {
40
+ const result = Badge({ className: "custom" });
41
+ assert.ok(result);
42
+ });
@@ -1,19 +1,68 @@
1
1
  import React from "react";
2
2
 
3
- export const Button = ({ variant = "primary", className, ...props }) => {
4
- const baseStyles = "px-4 py-2 rounded-md font-medium transition-colors";
5
- const variants = {
6
- primary: "bg-blue-600 text-white hover:bg-blue-700",
7
- secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
8
- };
3
+ const base =
4
+ "inline-flex items-center justify-center rounded-sm font-medium transition-all duration-200 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none active:translate-y-px";
9
5
 
10
- return React.createElement(
11
- "button",
12
- {
13
- type: "button",
14
- className: `${baseStyles} ${variants[variant]} ${className || ""}`,
15
- ...props,
16
- },
17
- props.children,
18
- );
6
+ const THEMES = {
7
+ light: {
8
+ primary:
9
+ "bg-blue-600 text-white shadow-sm hover:bg-blue-700 hover:shadow focus-visible:ring-blue-500",
10
+ secondary:
11
+ "border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 hover:border-gray-300 hover:shadow focus-visible:ring-gray-400",
12
+ danger:
13
+ "bg-red-600 text-white shadow-sm hover:bg-red-700 hover:shadow focus-visible:ring-red-500",
14
+ ghost:
15
+ "bg-transparent text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus-visible:ring-gray-400",
16
+ success:
17
+ "bg-emerald-600 text-white shadow-sm hover:bg-emerald-700 hover:shadow focus-visible:ring-emerald-500",
18
+ warning:
19
+ "bg-amber-500 text-white shadow-sm hover:bg-amber-600 hover:shadow focus-visible:ring-amber-400",
20
+ info: "bg-cyan-600 text-white shadow-sm hover:bg-cyan-700 hover:shadow focus-visible:ring-cyan-500",
21
+ outline:
22
+ "border border-blue-600 text-blue-600 bg-transparent hover:bg-blue-50 focus-visible:ring-blue-500",
23
+ solid:
24
+ "bg-blue-700 text-white shadow-sm hover:bg-blue-800 hover:shadow focus-visible:ring-blue-600",
25
+ },
26
+ dark: {
27
+ primary:
28
+ "bg-[#377dff]/10 border border-[#377dff]/40 text-[#377dff] hover:bg-[#377dff]/20 hover:border-[#377dff]/80 focus-visible:ring-[#377dff]/40 focus-visible:ring-offset-0",
29
+ secondary:
30
+ "border border-[#1e2535] text-[#6b7a99] hover:border-[#377dff]/30 hover:text-[#e0e0e0] focus-visible:ring-[#377dff]/30 focus-visible:ring-offset-0",
31
+ danger:
32
+ "bg-[#ff4757]/10 border border-[#ff4757]/40 text-[#ff4757] hover:bg-[#ff4757]/20 hover:border-[#ff4757]/80 focus-visible:ring-[#ff4757]/40 focus-visible:ring-offset-0",
33
+ ghost:
34
+ "text-[#4a4a6a] hover:bg-[#1e2535] hover:text-[#e0e0e0] focus-visible:ring-[#377dff]/30 focus-visible:ring-offset-0",
35
+ success:
36
+ "bg-emerald-500/10 border border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/20 hover:border-emerald-500/80 focus-visible:ring-emerald-400/40 focus-visible:ring-offset-0",
37
+ warning:
38
+ "bg-amber-500/10 border border-amber-400/40 text-amber-400 hover:bg-amber-500/20 hover:border-amber-400/80 focus-visible:ring-amber-400/40 focus-visible:ring-offset-0",
39
+ info: "bg-cyan-500/10 border border-cyan-400/40 text-cyan-400 hover:bg-cyan-500/20 hover:border-cyan-400/80 focus-visible:ring-cyan-400/40 focus-visible:ring-offset-0",
40
+ outline:
41
+ "border border-[#377dff]/60 text-[#377dff] bg-transparent hover:bg-[#377dff]/10 focus-visible:ring-[#377dff]/40 focus-visible:ring-offset-0",
42
+ solid:
43
+ "bg-[#377dff] text-white hover:bg-[#2563eb] focus-visible:ring-[#377dff]/60 focus-visible:ring-offset-0",
44
+ },
19
45
  };
46
+
47
+ const sizes = {
48
+ sm: "h-8 px-3 text-sm",
49
+ md: "h-10 px-4 text-sm",
50
+ lg: "h-11 px-6 text-base",
51
+ };
52
+
53
+ export function Button({
54
+ variant = "primary",
55
+ size = "md",
56
+ theme = "light",
57
+ className = "",
58
+ ...props
59
+ }) {
60
+ const t = THEMES[theme] ?? THEMES.light;
61
+ const cls =
62
+ `${base} ${t[variant] ?? t.primary} ${sizes[size] ?? sizes.md} ${className}`.trim();
63
+ return React.createElement("button", {
64
+ type: "button",
65
+ className: cls,
66
+ ...props,
67
+ });
68
+ }
@@ -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 - component accepts props", () => {
10
- // Test that component can be called with props
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, "Primary variant should be supported");
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, "Secondary variant should be supported");
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
+ });