@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,46 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
2
+ <!--
3
+ Quark logo — SVG master
4
+ Geometry mirrors QuarkAnimation.js constants (ring/dash thickness ratio ~0.20)
5
+
6
+ Ring: center (100,100), centerline radius 65, stroke-width 26
7
+ Gap: centred at 45.8° (dashAngle=0.8 rad), half-width 27.1° (0.472 rad)
8
+ Dash: radial at 45.8°, from radius 23.4 → 103.4 (proportional to animation dL)
9
+ stroke-width 28 (= dashThickness/ringThickness × 26 = 2.8/2.6 × 26)
10
+
11
+ Colors: #2d3436 (dark/top) · #377dff (blue/bottom-left) · #ff4757 (red/tail)
12
+ -->
13
+
14
+ <!-- Dark arc — upper semicircle: left (180°) → top (270°) → right (360°) -->
15
+ <path
16
+ d="M 35,100 A 65,65 0 0,1 165,100"
17
+ stroke="#2d3436"
18
+ stroke-width="26"
19
+ stroke-linecap="butt"
20
+ />
21
+
22
+ <!-- Blue arc — bottom-left: gap-end (72.9°) → left (180°) -->
23
+ <path
24
+ d="M 119.1,162.1 A 65,65 0 0,1 35,100"
25
+ stroke="#377dff"
26
+ stroke-width="26"
27
+ stroke-linecap="butt"
28
+ />
29
+
30
+ <!-- Red arc — tiny pre-gap sliver: right (0°) → gap-start (18.8°) -->
31
+ <path
32
+ d="M 165,100 A 65,65 0 0,1 161.5,121"
33
+ stroke="#ff4757"
34
+ stroke-width="26"
35
+ stroke-linecap="butt"
36
+ />
37
+
38
+ <!-- Red dash — radial tail through the gap -->
39
+ <line
40
+ x1="116.3" y1="116.8"
41
+ x2="172" y2="174.2"
42
+ stroke="#ff4757"
43
+ stroke-width="28"
44
+ stroke-linecap="butt"
45
+ />
46
+ </svg>
@@ -6,10 +6,10 @@
6
6
  "watchPatterns": ["apps/web/**", "packages/**"]
7
7
  },
8
8
  "deploy": {
9
- "releaseCommand": "pnpm --filter @techstream/quark-db exec prisma migrate deploy",
9
+ "releaseCommand": "pnpm --filter './packages/db' exec prisma migrate deploy",
10
10
  "startCommand": "node apps/web/.next/standalone/server.js",
11
11
  "healthcheckPath": "/api/health",
12
- "healthcheckTimeout": 30,
12
+ "healthcheckTimeout": 120,
13
13
  "restartPolicyType": "ON_FAILURE",
14
14
  "restartPolicyMaxRetries": 5
15
15
  }
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ const STATUS_COLORS = {
6
+ ok: "#22c55e",
7
+ degraded: "#f59e0b",
8
+ error: "#ef4444",
9
+ };
10
+
11
+ function Dot({ status, label }) {
12
+ const dotColor = STATUS_COLORS[status] ?? "var(--quark-health-dot-idle)";
13
+ return (
14
+ <span
15
+ title={`${label}: ${status ?? "checking…"}`}
16
+ style={{
17
+ display: "inline-flex",
18
+ alignItems: "center",
19
+ gap: "4px",
20
+ color: "var(--quark-health-text)",
21
+ fontFamily: "monospace",
22
+ fontSize: "11px",
23
+ }}
24
+ >
25
+ <span
26
+ style={{
27
+ display: "inline-block",
28
+ width: "5px",
29
+ height: "5px",
30
+ borderRadius: "50%",
31
+ backgroundColor: dotColor,
32
+ transition: "background-color 0.3s ease",
33
+ flexShrink: 0,
34
+ }}
35
+ />
36
+ {label}
37
+ </span>
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Fetches /api/health once on mount and renders small coloured dots for
43
+ * database, Redis, and storage status. Shows neutral dots while loading.
44
+ * Fails silently so the home page aesthetic is never disrupted.
45
+ * Colours are driven by CSS custom properties (prefers-color-scheme aware).
46
+ */
47
+ export default function HealthIndicator() {
48
+ const [checks, setChecks] = useState(null);
49
+
50
+ useEffect(() => {
51
+ let cancelled = false;
52
+ fetch("/api/health")
53
+ .then((r) => r.json())
54
+ .then((data) => {
55
+ if (!cancelled) setChecks(data.checks ?? {});
56
+ })
57
+ .catch(() => {
58
+ if (!cancelled) setChecks({});
59
+ });
60
+ return () => {
61
+ cancelled = true;
62
+ };
63
+ }, []);
64
+
65
+ return (
66
+ <div
67
+ style={{
68
+ display: "flex",
69
+ alignItems: "center",
70
+ gap: "8px",
71
+ fontFamily: "monospace",
72
+ fontSize: "11px",
73
+ }}
74
+ >
75
+ <Dot status={checks?.database?.status} label="db" />
76
+ <span style={{ color: "var(--quark-health-sep)" }}>·</span>
77
+ <Dot status={checks?.redis?.status} label="redis" />
78
+ <span style={{ color: "var(--quark-health-sep)" }}>·</span>
79
+ <Dot
80
+ status={checks?.storage?.status}
81
+ label={checks?.storage?.provider ?? "storage"}
82
+ />
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import {
5
+ THEME_ATTR,
6
+ THEME_CHANGE_EVENT,
7
+ THEME_STORAGE_KEY,
8
+ } from "../../lib/theme.js";
9
+
10
+ /**
11
+ * Text-link theme toggle for the home page nav.
12
+ * Shows "light" when dark mode is active (click to switch to light) and vice versa.
13
+ * Styled identically to the other nav links via the quark-home-link class.
14
+ *
15
+ * Uses the same localStorage key as ThemeProvider (THEME_STORAGE_KEY) and sets
16
+ * the same HTML attribute (THEME_ATTR) so CSS variables react instantly. Fires
17
+ * a THEME_CHANGE_EVENT custom event so ThemeProvider (if present) stays in sync
18
+ * without any import coupling between the two packages.
19
+ */
20
+ export default function HomeThemeToggle() {
21
+ const [isDark, setIsDark] = useState(true);
22
+
23
+ useEffect(() => {
24
+ const stored = localStorage.getItem(THEME_STORAGE_KEY);
25
+ if (stored === "light" || stored === "dark") {
26
+ setIsDark(stored === "dark");
27
+ document.documentElement.setAttribute(THEME_ATTR, stored);
28
+ return;
29
+ }
30
+ const osDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
31
+ setIsDark(osDark);
32
+ }, []);
33
+
34
+ function toggle() {
35
+ setIsDark((prev) => {
36
+ const next = prev ? "light" : "dark";
37
+ localStorage.setItem(THEME_STORAGE_KEY, next);
38
+ document.documentElement.setAttribute(THEME_ATTR, next);
39
+ // Notify ThemeProvider (if present in the tree) so React context stays in sync.
40
+ document.dispatchEvent(
41
+ new CustomEvent(THEME_CHANGE_EVENT, { detail: { theme: next } }),
42
+ );
43
+ return !prev;
44
+ });
45
+ }
46
+
47
+ return (
48
+ <button
49
+ type="button"
50
+ onClick={toggle}
51
+ aria-label={`Switch to ${isDark ? "light" : "dark"} mode`}
52
+ className="quark-home-link"
53
+ style={{
54
+ background: "none",
55
+ border: "none",
56
+ padding: 0,
57
+ cursor: "pointer",
58
+ }}
59
+ >
60
+ {isDark ? "light" : "dark"}
61
+ </button>
62
+ );
63
+ }
@@ -0,0 +1,168 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+
5
+ const CHARS = " .:-=+*#%@";
6
+ const W = 110;
7
+ const H = 55;
8
+
9
+ const CONF = {
10
+ zoom: 1.16,
11
+ speed: 1.7,
12
+ dashLength: 16,
13
+ dashThickness: 2.8,
14
+ dashDistFactor: 0.36,
15
+ ringGapMultiplier: 0.3,
16
+ angleLimit: 0.3,
17
+ yBounce: 1,
18
+ ringRadius: 13,
19
+ ringThickness: 2.6,
20
+ yScaleFactor: 1.05,
21
+ dashAngle: 0.8,
22
+ };
23
+
24
+ // Fixed brand palette — all three colors are part of the Quark logo identity.
25
+ const CLASS_COLORS = { s: "#2d3436", b: "#377dff", c: "#ff4757" };
26
+
27
+ export default function QuarkAnimation() {
28
+ const ref = useRef(null);
29
+
30
+ useEffect(() => {
31
+ const el = ref.current;
32
+ if (!el) return;
33
+
34
+ let t = 0;
35
+ let raf;
36
+
37
+ function render() {
38
+ t += 0.01 * CONF.speed;
39
+
40
+ const rY = Math.sin(t) * CONF.angleLimit;
41
+ const yO = Math.sin(t * 1.5) * CONF.yBounce;
42
+ const cB = new Array(W * H).fill(" ");
43
+ const clB = new Array(W * H).fill("");
44
+ const zB = new Float32Array(W * H).fill(-1000);
45
+
46
+ const rR = CONF.ringRadius * CONF.zoom;
47
+ const rT = CONF.ringThickness * CONF.zoom;
48
+ const dL = CONF.dashLength * CONF.zoom;
49
+ const dT = CONF.dashThickness * CONF.zoom;
50
+ const dA = CONF.dashAngle;
51
+ const gap = (dT / rR) * 0.8 + CONF.ringGapMultiplier;
52
+
53
+ // Ring (torus)
54
+ for (let p = 0; p < Math.PI * 2; p += 0.12) {
55
+ for (let th = 0; th < Math.PI * 2; th += 0.04) {
56
+ let d = Math.abs(th - dA);
57
+ if (d > Math.PI) d = 2 * Math.PI - d;
58
+ if (d < gap) continue;
59
+
60
+ const rXx = Math.cos(th);
61
+ const rYy = Math.sin(th);
62
+ const x = (rR + rT * Math.cos(p)) * rXx;
63
+ const y = (rR + rT * Math.cos(p)) * rYy * CONF.yScaleFactor;
64
+ const z = rT * Math.sin(p);
65
+
66
+ const rx = x * Math.cos(rY) + z * Math.sin(rY);
67
+ const rz = -x * Math.sin(rY) + z * Math.cos(rY);
68
+ const ry = y + yO;
69
+
70
+ const xp = Math.floor(W / 2 + rx * 1.5);
71
+ const yp = Math.floor(H / 2 + ry);
72
+
73
+ if (xp >= 0 && xp < W && yp >= 0 && yp < H) {
74
+ const i = yp * W + xp;
75
+ if (rz > zB[i]) {
76
+ zB[i] = rz;
77
+ const nT = ((th % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
78
+ const nD = ((dA % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
79
+ clB[i] = rYy < 0 ? "s" : nT > nD ? "b" : "c";
80
+ cB[i] =
81
+ CHARS[
82
+ Math.floor(
83
+ Math.max(0, Math.min(1, (rz + rT) / (rT * 2) + 0.3)) *
84
+ (CHARS.length - 1),
85
+ )
86
+ ];
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ // Dash (quark)
93
+ for (let l = 0; l < dL; l += 0.6) {
94
+ for (let p = 0; p < Math.PI * 2; p += 0.2) {
95
+ for (let rad = 0; rad < dT; rad += 0.6) {
96
+ const sR = rR * CONF.dashDistFactor;
97
+ const lx = rad * Math.cos(p);
98
+ const ly = rad * Math.sin(p);
99
+ const lz = l;
100
+
101
+ const tx =
102
+ sR * Math.cos(dA) + lz * Math.cos(dA) - lx * Math.sin(dA);
103
+ const ty =
104
+ (sR * Math.sin(dA) + lz * Math.sin(dA) + lx * Math.cos(dA)) *
105
+ CONF.yScaleFactor;
106
+ const tz = ly;
107
+
108
+ const rtx = tx * Math.cos(rY) + tz * Math.sin(rY);
109
+ const rtz = -tx * Math.sin(rY) + tz * Math.cos(rY);
110
+ const rty = ty + yO;
111
+
112
+ const xp = Math.floor(W / 2 + rtx * 1.5);
113
+ const yp = Math.floor(H / 2 + rty);
114
+
115
+ if (xp >= 0 && xp < W && yp >= 0 && yp < H) {
116
+ const i = yp * W + xp;
117
+ if (rtz > zB[i]) {
118
+ zB[i] = rtz;
119
+ clB[i] = "c";
120
+ cB[i] =
121
+ CHARS[
122
+ Math.floor(
123
+ Math.max(0, Math.min(1, (rtz + dT) / (dT * 2) + 0.3)) *
124
+ (CHARS.length - 1),
125
+ )
126
+ ];
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ // Build HTML string — inline colors avoid global CSS pollution
134
+ let o = "";
135
+ for (let i = 0; i < cB.length; i++) {
136
+ if (i > 0 && i % W === 0) o += "\n";
137
+ const cls = clB[i];
138
+ if (cls) {
139
+ o += `<span style="color:${CLASS_COLORS[cls]}">${cB[i]}</span>`;
140
+ } else {
141
+ o += cB[i];
142
+ }
143
+ }
144
+
145
+ el.innerHTML = o;
146
+ raf = requestAnimationFrame(render);
147
+ }
148
+
149
+ render();
150
+ return () => cancelAnimationFrame(raf);
151
+ }, []);
152
+
153
+ return (
154
+ <div
155
+ ref={ref}
156
+ style={{
157
+ lineHeight: "0.82",
158
+ letterSpacing: "0",
159
+ whiteSpace: "pre",
160
+ textAlign: "center",
161
+ fontSize: "11px",
162
+ fontWeight: "bold",
163
+ fontFamily: "monospace",
164
+ color: "#e0e0e0",
165
+ }}
166
+ />
167
+ );
168
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { createLogger, createStorage, pingRedis } from "@techstream/quark-core";
8
- import { prisma } from "@techstream/quark-db";
8
+ import { pingDatabase } from "@techstream/quark-db";
9
9
  import { NextResponse } from "next/server";
10
10
 
11
11
  const logger = createLogger("health");
@@ -15,18 +15,25 @@ const HEALTH_CHECK_TIMEOUT = 5000;
15
15
 
16
16
  export async function GET() {
17
17
  try {
18
- const result = await Promise.race([
19
- runHealthChecks(),
20
- new Promise((_, reject) =>
21
- setTimeout(
22
- () => reject(new Error("Health check timed out")),
23
- HEALTH_CHECK_TIMEOUT,
24
- ),
25
- ),
26
- ]);
18
+ const result = await new Promise((resolve, reject) => {
19
+ const timer = setTimeout(
20
+ () => reject(new Error("Health check timed out")),
21
+ HEALTH_CHECK_TIMEOUT,
22
+ );
23
+ runHealthChecks().then(
24
+ (val) => {
25
+ clearTimeout(timer);
26
+ resolve(val);
27
+ },
28
+ (err) => {
29
+ clearTimeout(timer);
30
+ reject(err);
31
+ },
32
+ );
33
+ });
27
34
 
28
35
  return NextResponse.json(result, {
29
- status: result.status === "ok" ? 200 : 503,
36
+ status: result.status === "error" ? 503 : 200,
30
37
  });
31
38
  } catch (error) {
32
39
  logger.error("Health check failed", {
@@ -56,13 +63,13 @@ async function runHealthChecks() {
56
63
  };
57
64
 
58
65
  // Check database connectivity
59
- try {
60
- await prisma.$queryRaw`SELECT 1`;
61
- health.checks.database = { status: "ok" };
62
- } catch (error) {
66
+ const dbResult = await pingDatabase();
67
+ if (dbResult.status === "ok") {
68
+ health.checks.database = { status: "ok", latencyMs: dbResult.latencyMs };
69
+ } else {
63
70
  health.checks.database = {
64
71
  status: "error",
65
- message: error.message,
72
+ message: dbResult.message,
66
73
  };
67
74
  health.status = "degraded";
68
75
  }
@@ -106,6 +113,10 @@ async function checkStorage() {
106
113
  await storage.delete(sentinelKey);
107
114
  return { status: "ok", provider };
108
115
  } catch (error) {
109
- return { status: "error", provider, message: error.message };
116
+ const message =
117
+ process.env.NODE_ENV === "production"
118
+ ? "Storage unavailable"
119
+ : error.message;
120
+ return { status: "error", provider, message };
110
121
  }
111
122
  }
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ export default function GlobalError({ reset }) {
4
+ return (
5
+ <html lang="en">
6
+ <body style={{ margin: 0, background: "#05070a" }}>
7
+ <main
8
+ className="fixed inset-0 flex flex-col items-center justify-center"
9
+ style={{ background: "#05070a" }}
10
+ >
11
+ <p
12
+ className="font-mono uppercase"
13
+ style={{
14
+ color: "#3a3a4a",
15
+ fontSize: "11px",
16
+ letterSpacing: "0.15em",
17
+ }}
18
+ >
19
+ 500
20
+ </p>
21
+ <p
22
+ className="font-mono mt-2"
23
+ style={{ color: "#2a2a3a", fontSize: "11px" }}
24
+ >
25
+ something went wrong
26
+ </p>
27
+ <nav
28
+ className="mt-6 flex items-center gap-2"
29
+ style={{ fontSize: "11px", fontFamily: "monospace" }}
30
+ >
31
+ <button
32
+ type="button"
33
+ onClick={reset}
34
+ className="quark-home-link"
35
+ style={{
36
+ background: "none",
37
+ border: "none",
38
+ padding: 0,
39
+ cursor: "pointer",
40
+ }}
41
+ >
42
+ try again
43
+ </button>
44
+ <span style={{ color: "#2a2a3a" }}>·</span>
45
+ <a href="/" className="quark-home-link">
46
+ ← home
47
+ </a>
48
+ </nav>
49
+ </main>
50
+ </body>
51
+ </html>
52
+ );
53
+ }
@@ -1,26 +1,132 @@
1
1
  @import "tailwindcss";
2
+ @source "../../../../packages/ui/src/**/*.{js,jsx}";
2
3
 
4
+ /*
5
+ * CSS custom properties for the home page shell.
6
+ * Automatically responds to the OS colour preference via prefers-color-scheme —
7
+ * no JavaScript required. Override these in your own stylesheet as needed.
8
+ */
3
9
  :root {
4
- --background: #ffffff;
5
- --foreground: #171717;
10
+ --quark-page-bg: #05070a;
11
+ --quark-home-muted: #3a3a4a;
12
+ --quark-home-sep: #2a2a3a;
13
+ --quark-home-link-idle: #4a4a6a;
14
+ --quark-home-link-hover: #377dff;
15
+ --quark-health-text: #4a4a6a;
16
+ --quark-health-sep: #2a2a3a;
17
+ --quark-health-dot-idle: #2a2a3a;
18
+ --quark-grid-line: rgba(255, 255, 255, 0.05);
6
19
  }
7
20
 
8
- @theme inline {
9
- --color-background: var(--background);
10
- --color-foreground: var(--foreground);
11
- --font-sans: var(--font-geist-sans);
12
- --font-mono: var(--font-geist-mono);
21
+ @media (prefers-color-scheme: light) {
22
+ /* OS says light AND the user hasn't explicitly pinned dark via the toggle */
23
+ :root:not([data-theme="dark"]) {
24
+ --quark-page-bg: #f7f8fa;
25
+ --quark-home-muted: #9ca3af;
26
+ --quark-home-sep: #d1d5db;
27
+ --quark-home-link-idle: #6b7280;
28
+ --quark-home-link-hover: #1a56db;
29
+ --quark-health-text: #9ca3af;
30
+ --quark-health-sep: #d1d5db;
31
+ --quark-health-dot-idle: #d1d5db;
32
+ --quark-grid-line: rgba(0, 0, 0, 0.07);
33
+ }
13
34
  }
14
35
 
15
- @media (prefers-color-scheme: dark) {
16
- :root {
17
- --background: #0a0a0a;
18
- --foreground: #ededed;
19
- }
36
+ /* User explicitly toggled to light (overrides OS dark preference) */
37
+ :root[data-theme="light"] {
38
+ --quark-page-bg: #f7f8fa;
39
+ --quark-home-muted: #9ca3af;
40
+ --quark-home-sep: #d1d5db;
41
+ --quark-home-link-idle: #6b7280;
42
+ --quark-home-link-hover: #1a56db;
43
+ --quark-health-text: #9ca3af;
44
+ --quark-health-sep: #d1d5db;
45
+ --quark-health-dot-idle: #d1d5db;
46
+ --quark-grid-line: rgba(0, 0, 0, 0.07);
20
47
  }
21
48
 
49
+ /* Pin the document background to match the app shell — prevents overscroll flash */
50
+ html,
22
51
  body {
23
- background: var(--background);
24
- color: var(--foreground);
25
- font-family: Arial, Helvetica, sans-serif;
52
+ background-color: var(--quark-page-bg);
53
+ }
54
+
55
+ .quark-home-main {
56
+ background-color: var(--quark-page-bg);
57
+ background-image:
58
+ linear-gradient(var(--quark-grid-line) 1px, transparent 1px),
59
+ linear-gradient(90deg, var(--quark-grid-line) 1px, transparent 1px);
60
+ background-size: 40px 40px;
61
+ }
62
+
63
+ /* Native dialog — browser default positions it at the top; force viewport-center */
64
+ dialog {
65
+ position: fixed;
66
+ inset: 0;
67
+ margin: auto;
68
+ }
69
+
70
+ /* Home page - footer fades in after animation takes the stage */
71
+ @keyframes quark-home-fade-in {
72
+ from {
73
+ opacity: 0;
74
+ }
75
+ to {
76
+ opacity: 1;
77
+ }
78
+ }
79
+
80
+ .quark-home-footer {
81
+ animation: quark-home-fade-in 0.6s ease-out 1.2s both;
82
+ }
83
+
84
+ .quark-home-label {
85
+ color: var(--quark-home-muted);
86
+ font-size: 11px;
87
+ letter-spacing: 0.15em;
88
+ }
89
+
90
+ .quark-home-sep {
91
+ color: var(--quark-home-sep);
92
+ font-family: monospace;
93
+ }
94
+
95
+ .quark-home-link {
96
+ color: var(--quark-home-link-idle);
97
+ text-decoration: none;
98
+ font-family: monospace;
99
+ transition: color 0.15s ease;
100
+ }
101
+
102
+ .quark-home-link:hover {
103
+ color: var(--quark-home-link-hover);
104
+ }
105
+
106
+ .quark-home-link:focus-visible {
107
+ outline: 1px solid var(--quark-home-link-hover);
108
+ outline-offset: 2px;
109
+ border-radius: 2px;
110
+ }
111
+
112
+ @keyframes quark-dialog-in {
113
+ from {
114
+ opacity: 0;
115
+ transform: scale(0.95) translateY(-8px);
116
+ }
117
+ to {
118
+ opacity: 1;
119
+ transform: scale(1) translateY(0);
120
+ }
121
+ }
122
+
123
+ @keyframes quark-dialog-out {
124
+ from {
125
+ opacity: 1;
126
+ transform: scale(1) translateY(0);
127
+ }
128
+ to {
129
+ opacity: 0;
130
+ transform: scale(0.95) translateY(-8px);
131
+ }
26
132
  }