@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>
@@ -1,3 +1,4 @@
1
+ import "./globals.css";
1
2
  import { getSiteMetadata } from "../lib/seo/site-metadata.js";
2
3
 
3
4
  export const metadata = getSiteMetadata();
@@ -0,0 +1,35 @@
1
+ import Link from "next/link";
2
+
3
+ export const metadata = {
4
+ title: "404 — Not Found",
5
+ };
6
+
7
+ export default function NotFound() {
8
+ return (
9
+ <main
10
+ style={{ background: "#05070a" }}
11
+ className="fixed inset-0 flex flex-col items-center justify-center"
12
+ >
13
+ <p
14
+ className="font-mono uppercase"
15
+ style={{ color: "#3a3a4a", fontSize: "11px", letterSpacing: "0.15em" }}
16
+ >
17
+ 404
18
+ </p>
19
+ <p
20
+ className="font-mono mt-2"
21
+ style={{ color: "#2a2a3a", fontSize: "11px" }}
22
+ >
23
+ page not found
24
+ </p>
25
+ <nav
26
+ className="mt-6"
27
+ style={{ fontSize: "11px", fontFamily: "monospace" }}
28
+ >
29
+ <Link href="/" className="quark-home-link">
30
+ ← home
31
+ </Link>
32
+ </nav>
33
+ </main>
34
+ );
35
+ }
@@ -1,10 +1,43 @@
1
+ import HealthIndicator from "./_components/HealthIndicator.js";
2
+ import HomeThemeToggle from "./_components/HomeThemeToggle.js";
3
+ import QuarkAnimation from "./_components/QuarkAnimation.js";
4
+
1
5
  export default function Home() {
2
6
  return (
3
- <main className="min-h-screen flex flex-col items-center justify-center p-8">
4
- <h1 className="text-4xl font-bold text-center mb-4">Quark</h1>
5
- <p className="text-lg text-gray-600 text-center max-w-md">
6
- Welcome to the Quark monorepo. This is a Next.js 16 app with React 19.
7
- </p>
7
+ <main className="quark-home-main min-h-screen flex flex-col items-center justify-center">
8
+ {/* Hero: animated ASCII logo, centered */}
9
+ <QuarkAnimation />
10
+
11
+ {/* Footer — flows naturally below the animation */}
12
+ <div className="quark-home-footer flex flex-col items-center gap-2 pt-8 pb-8">
13
+ {/* Identity */}
14
+ <p className="quark-home-label font-mono uppercase">Your Quark App</p>
15
+
16
+ {/* Navigation */}
17
+ <nav className="flex items-center gap-2">
18
+ <a href="/api/health" className="quark-home-link">
19
+ health
20
+ </a>
21
+ <span className="quark-home-sep">·</span>
22
+ <a href="/playground" className="quark-home-link">
23
+ playground
24
+ </a>
25
+ <span className="quark-home-sep">·</span>
26
+ <a
27
+ href="https://www.npmjs.com/package/@techstream/quark-create-app"
28
+ target="_blank"
29
+ rel="noopener noreferrer"
30
+ className="quark-home-link"
31
+ >
32
+ npm
33
+ </a>
34
+ <span className="quark-home-sep">·</span>
35
+ <HomeThemeToggle />
36
+ </nav>
37
+
38
+ {/* Status — supplementary, last */}
39
+ <HealthIndicator />
40
+ </div>
8
41
  </main>
9
42
  );
10
43
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Theme system constants for the home page shell.
3
+ *
4
+ * These three values form the contract between HomeThemeToggle and any
5
+ * ThemeProvider that may be present in the tree (e.g. from @techstream/quark-ui).
6
+ *
7
+ * An identical copy lives in packages/ui/src/theme-constants.js — each
8
+ * package owns its own copy so there is no cross-package import. If you
9
+ * rename any of these, update both files and the CSS selectors in globals.css.
10
+ */
11
+
12
+ /** Key used to persist the user's explicit theme choice in localStorage. */
13
+ export const THEME_STORAGE_KEY = "quark-theme";
14
+
15
+ /**
16
+ * Attribute set on <html> so CSS custom properties can react to JS state.
17
+ * Use via element.setAttribute(THEME_ATTR, value) / element.getAttribute(THEME_ATTR).
18
+ * Referenced in globals.css as [data-theme="light"] / [data-theme="dark"].
19
+ */
20
+ export const THEME_ATTR = "data-theme";
21
+
22
+ /** CustomEvent name dispatched when the theme changes outside a ThemeProvider. */
23
+ export const THEME_CHANGE_EVENT = "quark-theme-change";
@@ -92,7 +92,16 @@ const CORS_CONFIG = {
92
92
 
93
93
  /**
94
94
  * Security headers configuration
95
+ *
96
+ * In development, Turbopack's hot-reload runtime uses eval() for module
97
+ * evaluation. 'unsafe-eval' is therefore added to script-src only when
98
+ * NODE_ENV is not 'production' — it must never reach a production build.
95
99
  */
100
+ const _scriptSrc =
101
+ process.env.NODE_ENV !== "production"
102
+ ? "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
103
+ : "script-src 'self' 'unsafe-inline'";
104
+
96
105
  const SECURITY_HEADERS = {
97
106
  "X-DNS-Prefetch-Control": "on",
98
107
  "Strict-Transport-Security": "max-age=63072000; includeSubDomains",
@@ -100,8 +109,7 @@ const SECURITY_HEADERS = {
100
109
  "X-Content-Type-Options": "nosniff",
101
110
  "Referrer-Policy": "strict-origin-when-cross-origin",
102
111
  "Permissions-Policy": "camera=(), microphone=(), geolocation=()",
103
- "Content-Security-Policy":
104
- "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';",
112
+ "Content-Security-Policy": `default-src 'self'; ${_scriptSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';`,
105
113
  };
106
114
 
107
115
  /**
@@ -16,6 +16,8 @@
16
16
  "db:push": "turbo run db:push",
17
17
  "db:seed": "turbo run db:seed",
18
18
  "db:studio": "turbo run db:studio",
19
+ "doctor": "node scripts/doctor.js",
20
+ "doctor:fix": "node scripts/doctor.js --fix",
19
21
  "prepare": "simple-git-hooks"
20
22
  },
21
23
  "simple-git-hooks": {
@@ -41,10 +41,15 @@ function getPrismaClient() {
41
41
  /**
42
42
  * Prisma client singleton. Lazily initialized on first use so the module
43
43
  * can be imported safely at build time (no DB env vars needed).
44
+ *
45
+ * Methods are bound to the real client so `this` inside Prisma internals is
46
+ * always the PrismaClient instance, never the Proxy wrapper.
44
47
  */
45
48
  export const prisma = new Proxy(/** @type {PrismaClient} */ ({}), {
46
49
  get(_target, prop) {
47
- return getPrismaClient()[prop];
50
+ const client = getPrismaClient();
51
+ const value = client[prop];
52
+ return typeof value === "function" ? value.bind(client) : value;
48
53
  },
49
54
  });
50
55
 
@@ -1,3 +1,4 @@
1
1
  export * from "./client.js";
2
+ export * from "./ping.js";
2
3
  export * from "./queries.js";
3
4
  export * from "./schemas.js";
@@ -0,0 +1,66 @@
1
+ import { getConnectionString } from "./connection.js";
2
+
3
+ /**
4
+ * Pings PostgreSQL to verify connectivity.
5
+ * Uses a raw `pg.Client` directly so the check is immune to Prisma
6
+ * adapter quirks — in particular, Prisma 7 + @prisma/adapter-pg surfaces
7
+ * connection failures as a misleading "Invalid invocation" error rather
8
+ * than a proper ECONNREFUSED.
9
+ *
10
+ * @param {object} [options]
11
+ * @param {number} [options.timeout=3000] - Connection/query timeout in milliseconds.
12
+ * @returns {Promise<{ status: "ok", latencyMs: number } | { status: "error", message: string }>}
13
+ */
14
+ export async function pingDatabase({ timeout = 3000 } = {}) {
15
+ /** @type {import("pg").Client | null} */
16
+ let client = null;
17
+ let connectionString;
18
+
19
+ try {
20
+ const { default: pg } = await import("pg");
21
+ connectionString = getConnectionString();
22
+
23
+ client = new pg.Client({
24
+ connectionString,
25
+ connectionTimeoutMillis: timeout,
26
+ query_timeout: timeout,
27
+ });
28
+
29
+ await client.connect();
30
+
31
+ const start = performance.now();
32
+ await client.query("SELECT 1");
33
+ const latencyMs = Math.round(performance.now() - start);
34
+
35
+ return { status: "ok", latencyMs };
36
+ } catch (/** @type {any} */ error) {
37
+ let message;
38
+ if (
39
+ error?.code === "MODULE_NOT_FOUND" ||
40
+ error?.code === "ERR_MODULE_NOT_FOUND"
41
+ ) {
42
+ message = "pg is not installed";
43
+ } else if (
44
+ error?.code === "ECONNREFUSED" ||
45
+ /ECONNREFUSED/i.test(error?.message ?? "")
46
+ ) {
47
+ try {
48
+ const { hostname, port } = new URL(connectionString);
49
+ message = `PostgreSQL unreachable at ${hostname}:${port || "5432"}`;
50
+ } catch {
51
+ message = "PostgreSQL unreachable";
52
+ }
53
+ } else if (error?.code === "ENOTFOUND") {
54
+ message = `PostgreSQL host not found: ${error.hostname ?? "unknown"}`;
55
+ } else {
56
+ message = error?.message ?? String(error);
57
+ }
58
+ return { status: "error", message };
59
+ } finally {
60
+ try {
61
+ await client?.end();
62
+ } catch {
63
+ // ignore disconnect errors
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/doctor.js
4
+ *
5
+ * Audits this project for unfinished post-scaffold customisation.
6
+ * Run it at any time — it is safe, read-only by default.
7
+ *
8
+ * Usage:
9
+ * pnpm doctor # Audit only
10
+ * pnpm doctor:fix # Audit + auto-remove Quark aesthetic scaffolding
11
+ *
12
+ * Extend the CHECKS array below to add your own project-specific rules.
13
+ * This script has zero external dependencies.
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const ROOT = path.resolve(__dirname, "..");
22
+ const FIX = process.argv.includes("--fix");
23
+
24
+ // ─── ANSI helpers ─────────────────────────────────────────────────────────────
25
+
26
+ const c = {
27
+ reset: "\x1b[0m",
28
+ bold: "\x1b[1m",
29
+ dim: "\x1b[2m",
30
+ red: "\x1b[31m",
31
+ green: "\x1b[32m",
32
+ yellow: "\x1b[33m",
33
+ blue: "\x1b[34m",
34
+ };
35
+
36
+ const fmt = {
37
+ bold: (s) => `${c.bold}${s}${c.reset}`,
38
+ dim: (s) => `${c.dim}${s}${c.reset}`,
39
+ red: (s) => `${c.red}${s}${c.reset}`,
40
+ green: (s) => `${c.green}${s}${c.reset}`,
41
+ yellow: (s) => `${c.yellow}${s}${c.reset}`,
42
+ blue: (s) => `${c.blue}${s}${c.reset}`,
43
+ };
44
+
45
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
46
+
47
+ function exists(rel) {
48
+ return fs.existsSync(path.join(ROOT, rel));
49
+ }
50
+
51
+ function read(rel) {
52
+ const abs = path.join(ROOT, rel);
53
+ return fs.existsSync(abs) ? fs.readFileSync(abs, "utf-8") : null;
54
+ }
55
+
56
+ /** Remove a file or directory tree (relative to ROOT). */
57
+ function remove(rel) {
58
+ const abs = path.join(ROOT, rel);
59
+ fs.rmSync(abs, { recursive: true, force: true });
60
+ }
61
+
62
+ // ─── Finding model ────────────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * @typedef {{ category: string, status: 'error'|'warn'|'info', key: string,
66
+ * message: string, detail: string, fix: string, fixable: boolean }} Finding
67
+ */
68
+
69
+ /** @type {Finding[]} */
70
+ const findings = [];
71
+
72
+ function warn(key, category, message, detail, fix, fixable = false) {
73
+ findings.push({ category, status: "warn", key, message, detail, fix, fixable });
74
+ }
75
+
76
+ function error(key, category, message, detail, fix, fixable = false) {
77
+ findings.push({ category, status: "error", key, message, detail, fix, fixable });
78
+ }
79
+
80
+ function info(key, category, message, detail, fix) {
81
+ findings.push({ category, status: "info", key, message, detail, fix, fixable: false });
82
+ }
83
+
84
+ // ─── Checks ───────────────────────────────────────────────────────────────────
85
+
86
+ // Read .quark-link.json to understand what packages were included at scaffold time.
87
+ const quarkLink = (() => {
88
+ try {
89
+ return JSON.parse(read(".quark-link.json") ?? "{}");
90
+ } catch {
91
+ return {};
92
+ }
93
+ })();
94
+ const hasUI = Array.isArray(quarkLink.packages) && quarkLink.packages.includes("ui");
95
+
96
+ // ── Check 1: Quark Animation still present ────────────────────────────────────
97
+ if (exists("apps/web/src/app/_components/QuarkAnimation.js")) {
98
+ warn(
99
+ "quark-animation",
100
+ "branding",
101
+ "QuarkAnimation is still in the project",
102
+ "apps/web/src/app/_components/QuarkAnimation.js",
103
+ "Replace or remove the animation and update the home page with your own hero content",
104
+ true,
105
+ );
106
+ }
107
+
108
+ // ── Check 2: Playground page present (only relevant with UI package) ──────────
109
+ if (hasUI && exists("apps/web/src/app/playground")) {
110
+ warn(
111
+ "playground",
112
+ "branding",
113
+ "Playground page is still present",
114
+ "apps/web/src/app/playground/",
115
+ "Consider removing the playground before going to production",
116
+ true,
117
+ );
118
+ }
119
+
120
+ // ── Check 3: APP_NAME / APP_DESCRIPTION still reference "Quark" ───────────────
121
+ const envContent = read(".env");
122
+ if (envContent) {
123
+ const nameMatch = envContent.match(/^APP_NAME=(.+)$/m);
124
+ const descMatch = envContent.match(/^APP_DESCRIPTION=(.+)$/m);
125
+ const nameVal = nameMatch?.[1]?.trim() ?? "";
126
+ const descVal = descMatch?.[1]?.trim() ?? "";
127
+
128
+ if (/\bquark\b/i.test(nameVal) || /\bquark\b/i.test(descVal)) {
129
+ warn(
130
+ "app-identity",
131
+ "metadata",
132
+ 'APP_NAME or APP_DESCRIPTION still references "Quark"',
133
+ `.env → APP_NAME="${nameVal}", APP_DESCRIPTION="${descVal}"`,
134
+ "Update APP_NAME and APP_DESCRIPTION in your .env to match your project",
135
+ );
136
+ }
137
+ }
138
+
139
+ // ── Check 4: Placeholder secrets (CHANGE_ME) left in .env ────────────────────
140
+ if (envContent && envContent.includes("CHANGE_ME")) {
141
+ const lines = envContent
142
+ .split("\n")
143
+ .map((l, i) => ({ line: i + 1, text: l }))
144
+ .filter(({ text }) => text.includes("CHANGE_ME"))
145
+ .map(({ line, text }) => ` line ${line}: ${text.split("=")[0]}`);
146
+
147
+ error(
148
+ "secrets",
149
+ "security",
150
+ "CHANGE_ME placeholders found in .env — rotate these before deploying",
151
+ lines.join("\n"),
152
+ "Replace every CHANGE_ME value with a real secret",
153
+ );
154
+ }
155
+
156
+ // ── Check 5: .env missing vars that exist in .env.example ────────────────────
157
+ const exampleContent = read(".env.example");
158
+ if (envContent && exampleContent) {
159
+ const defined = new Set(
160
+ envContent
161
+ .split("\n")
162
+ .filter((l) => l.includes("=") && !l.startsWith("#"))
163
+ .map((l) => l.split("=")[0].trim()),
164
+ );
165
+ const missing = exampleContent
166
+ .split("\n")
167
+ .filter((l) => l.includes("=") && !l.startsWith("#"))
168
+ .map((l) => l.split("=")[0].trim())
169
+ .filter((k) => k && !defined.has(k));
170
+
171
+ if (missing.length > 0) {
172
+ warn(
173
+ "env-missing",
174
+ "configuration",
175
+ `.env.example defines ${missing.length} key(s) not present in .env`,
176
+ missing.map((k) => ` ${k}`).join("\n"),
177
+ "Add the missing keys to your .env (copy from .env.example and fill in values)",
178
+ );
179
+ }
180
+ }
181
+
182
+ // ── Check 6: README still contains Quark template content ────────────────────
183
+ const readmeContent = read("README.md");
184
+ if (readmeContent && /quark/i.test(readmeContent)) {
185
+ info(
186
+ "readme",
187
+ "documentation",
188
+ "README.md still contains references to Quark",
189
+ "README.md",
190
+ "Update the README to describe your own project",
191
+ );
192
+ }
193
+
194
+ // ─── --fix: auto-remove fixable items ────────────────────────────────────────
195
+
196
+ const STATUS_ICON = { error: "✗", warn: "⚠", info: "·" };
197
+ const STATUS_COLOR = { error: fmt.red, warn: fmt.yellow, info: fmt.dim };
198
+
199
+ // Print the report first so users see what was found before anything is changed.
200
+ console.log(fmt.bold(fmt.blue("\n🩺 Quark Doctor\n")));
201
+
202
+ if (findings.length === 0) {
203
+ console.log(fmt.green(" ✓ Nothing to do — project looks clean!\n"));
204
+ process.exit(0);
205
+ }
206
+
207
+ // Group by category
208
+ const categories = [...new Set(findings.map((f) => f.category))];
209
+ for (const cat of categories) {
210
+ console.log(fmt.bold(` ${cat}`));
211
+ for (const f of findings.filter((f) => f.category === cat)) {
212
+ const icon = STATUS_ICON[f.status];
213
+ const colorFn = STATUS_COLOR[f.status];
214
+ console.log(` ${colorFn(`${icon} ${f.message}`)}`);
215
+ if (f.detail) {
216
+ for (const line of f.detail.split("\n")) {
217
+ console.log(fmt.dim(` ${line}`));
218
+ }
219
+ }
220
+ if (!FIX || !f.fixable) {
221
+ console.log(fmt.dim(` → ${f.fix}`));
222
+ }
223
+ }
224
+ console.log();
225
+ }
226
+
227
+ const errorCount = findings.filter((f) => f.status === "error").length;
228
+ const warnCount = findings.filter((f) => f.status === "warn").length;
229
+ const fixableCount = findings.filter((f) => f.fixable).length;
230
+
231
+ const parts = [];
232
+ if (errorCount) parts.push(fmt.red(`${errorCount} error${errorCount > 1 ? "s" : ""}`));
233
+ if (warnCount) parts.push(fmt.yellow(`${warnCount} warning${warnCount > 1 ? "s" : ""}`));
234
+ console.log(fmt.bold(` Summary: ${parts.join(", ")}`));
235
+
236
+ if (FIX) {
237
+ const fixable = findings.filter((f) => f.fixable);
238
+ if (fixable.length === 0) {
239
+ console.log(fmt.dim(" No auto-fixable items found.\n"));
240
+ } else {
241
+ console.log(fmt.bold(fmt.blue("\n🔧 Applying fixes…\n")));
242
+ for (const f of fixable) {
243
+ if (f.key === "quark-animation") {
244
+ remove("apps/web/src/app/_components/QuarkAnimation.js");
245
+ console.log(fmt.green(` ✓ Removed QuarkAnimation.js`));
246
+ } else if (f.key === "playground") {
247
+ remove("apps/web/src/app/playground");
248
+ console.log(fmt.green(` ✓ Removed apps/web/src/app/playground/`));
249
+ }
250
+ }
251
+ console.log();
252
+ }
253
+ } else if (fixableCount > 0) {
254
+ console.log(
255
+ fmt.dim(` ${fixableCount} item(s) can be auto-removed with: pnpm doctor:fix\n`),
256
+ );
257
+ } else {
258
+ console.log();
259
+ }
260
+
261
+ if (errorCount > 0) process.exit(1);
@@ -18,7 +18,8 @@
18
18
  "outputs": ["node_modules/.prisma/client"]
19
19
  },
20
20
  "db:migrate": {
21
- "cache": false
21
+ "cache": false,
22
+ "interactive": true
22
23
  },
23
24
  "db:push": {
24
25
  "cache": false
@@ -2,6 +2,7 @@
2
2
  "name": "@myquark/config",
3
3
  "version": "1.0.0",
4
4
  "type": "module",
5
+ "main": "src/index.js",
5
6
  "exports": {
6
7
  ".": "./src/index.js",
7
8
  "./app-url": "./src/app-url.js",
@@ -2,7 +2,8 @@
2
2
  "name": "@myquark/jobs",
3
3
  "version": "1.0.0",
4
4
  "type": "module",
5
+ "main": "src/index.js",
5
6
  "dependencies": {
6
- "bullmq": "^5.70.2"
7
+ "bullmq": "^5.70.4"
7
8
  }
8
9
  }
@@ -0,0 +1,67 @@
1
+ # @yourscope/ui
2
+
3
+ Scaffolded UI primitives for your Quark project. These components are **yours** — modify, extend, or replace them freely.
4
+
5
+ > This package is scaffolded via `quark-create-app`. There is no version sync back to Quark after scaffolding.
6
+
7
+ ## Import
8
+
9
+ ```javascript
10
+ import { Button, Card, Badge } from '@yourscope/ui';
11
+ ```
12
+
13
+ ## Components
14
+
15
+ ### Button
16
+ Props: `variant` ('primary' | 'secondary' | 'danger' | 'ghost', default: 'primary'), `size` ('sm' | 'md' | 'lg', default: 'md'), `className`, all native button attributes.
17
+
18
+ ### Input
19
+ Props: `className`, all native input attributes.
20
+
21
+ ### Label
22
+ Props: `className`, all native label attributes.
23
+
24
+ ### Textarea
25
+ Props: `className`, all native textarea attributes.
26
+
27
+ ### Select
28
+ Props: `className`, `children` (option elements), all native select attributes.
29
+
30
+ ### Checkbox
31
+ Props: `id`, `label` (string), `className`, all native checkbox input attributes.
32
+
33
+ ### Badge
34
+ Props: `variant` ('default' | 'success' | 'warning' | 'danger' | 'info', default: 'default'), `className`.
35
+
36
+ ### Card / CardHeader / CardTitle / CardContent / CardFooter
37
+ Composable card container. All parts accept `className`.
38
+
39
+ ### Table / TableHeader / TableBody / TableRow / TableHead / TableCell
40
+ Composable table. `Table` wraps in a scrollable container. All parts accept `className`.
41
+
42
+ ### Skeleton
43
+ Props: `className` (use to set width/height for the placeholder shape).
44
+
45
+ ### Dialog
46
+ `"use client"` — Props: `open` (bool), `onClose` (fn), `title` (string), `children`, `className`.
47
+
48
+ ### Toast / useToast
49
+ `"use client"` — `Toast` props: `message`, `variant` ('default' | 'success' | 'error'), `onClose` (fn), `visible` (bool).
50
+ `useToast()` returns `{ show(message, variant?), hide, toastProps }`. Spread `toastProps` onto `<Toast />`.
51
+
52
+ ```javascript
53
+ // Example
54
+ const { show, toastProps } = useToast();
55
+ return (
56
+ <>
57
+ <Button onClick={() => show('Saved!', 'success')}>Save</Button>
58
+ <Toast {...toastProps} />
59
+ </>
60
+ );
61
+ ```
62
+
63
+ ## Design notes
64
+ - Tailwind CSS only. No CSS-in-JS, no external dependencies.
65
+ - All components accept `className` for overrides.
66
+ - Server Component compatible except Dialog and Toast (marked `"use client"`).
67
+ - Accessible: ARIA attributes, focus management on interactive elements.
@@ -2,6 +2,7 @@
2
2
  "name": "@myquark/ui",
3
3
  "version": "1.0.0",
4
4
  "type": "module",
5
+ "main": "src/index.js",
5
6
  "devDependencies": {
6
7
  "@types/react": "^19.2.14",
7
8
  "@types/react-dom": "^19.2.3",