@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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/index.js +376 -143
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.github/copilot-instructions.md +55 -0
- 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 +3 -3
- 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 +28 -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 +2 -0
- 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/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
|
@@ -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>
|
|
@@ -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
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
50
|
+
const client = getPrismaClient();
|
|
51
|
+
const value = client[prop];
|
|
52
|
+
return typeof value === "function" ? value.bind(client) : value;
|
|
48
53
|
},
|
|
49
54
|
});
|
|
50
55
|
|
|
@@ -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);
|
|
@@ -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.
|