create-skit 0.1.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 (195) hide show
  1. package/README.md +36 -0
  2. package/bin/create-skit.mjs +1064 -0
  3. package/lib/module-application.mjs +281 -0
  4. package/lib/module-resolver.mjs +179 -0
  5. package/modules/README.md +22 -0
  6. package/modules/ai-dx/files/AGENTS.md +116 -0
  7. package/modules/ai-dx/files/ARCHITECTURE.md +103 -0
  8. package/modules/ai-dx/module.json +14 -0
  9. package/modules/ai-dx-claude/files/CLAUDE.md +8 -0
  10. package/modules/ai-dx-claude/module.json +13 -0
  11. package/modules/ai-dx-cursor/files/.cursor/rules/auth.mdc +53 -0
  12. package/modules/ai-dx-cursor/files/.cursor/rules/database.mdc +48 -0
  13. package/modules/ai-dx-cursor/files/.cursor/rules/env.mdc +43 -0
  14. package/modules/ai-dx-cursor/files/.cursor/rules/nextjs.mdc +58 -0
  15. package/modules/ai-dx-cursor/files/.cursor/rules/project.mdc +33 -0
  16. package/modules/ai-dx-cursor/files/.cursor/rules/testing.mdc +55 -0
  17. package/modules/ai-dx-cursor/module.json +18 -0
  18. package/modules/ai-dx-gemini/files/.gemini/GEMINI.md +5 -0
  19. package/modules/ai-dx-gemini/module.json +13 -0
  20. package/modules/auth-core/module.json +8 -0
  21. package/modules/auth-github/module.json +20 -0
  22. package/modules/billing-polar/module.json +20 -0
  23. package/modules/billing-stripe/module.json +23 -0
  24. package/modules/dashboard-shell/files/src/app/globals.css +756 -0
  25. package/modules/dashboard-shell/files/src/app/settings/page.tsx +67 -0
  26. package/modules/dashboard-shell/module.json +11 -0
  27. package/modules/db-pg/module.json +21 -0
  28. package/modules/db-postgresjs/module.json +21 -0
  29. package/modules/deploy-docker/files/.dockerignore +19 -0
  30. package/modules/deploy-docker/files/Dockerfile +25 -0
  31. package/modules/deploy-docker/module.json +11 -0
  32. package/modules/email-resend/module.json +21 -0
  33. package/modules/quality-baseline/module.json +8 -0
  34. package/modules/testing-baseline/module.json +8 -0
  35. package/package.json +40 -0
  36. package/presets/README.md +12 -0
  37. package/presets/blank.json +67 -0
  38. package/presets/dashboard.json +67 -0
  39. package/templates/base-web/.env.example +17 -0
  40. package/templates/base-web/.github/workflows/ci.yml +34 -0
  41. package/templates/base-web/.husky/pre-commit +3 -0
  42. package/templates/base-web/.husky/pre-push +3 -0
  43. package/templates/base-web/.prettierignore +3 -0
  44. package/templates/base-web/README.md +42 -0
  45. package/templates/base-web/drizzle.config.ts +16 -0
  46. package/templates/base-web/eslint.config.mjs +127 -0
  47. package/templates/base-web/manifest.json +5 -0
  48. package/templates/base-web/next-env.d.ts +4 -0
  49. package/templates/base-web/next.config.ts +5 -0
  50. package/templates/base-web/package.json +75 -0
  51. package/templates/base-web/playwright.config.ts +21 -0
  52. package/templates/base-web/prettier.config.mjs +9 -0
  53. package/templates/base-web/proxy.ts +23 -0
  54. package/templates/base-web/src/app/api/auth/[...all]/route.ts +5 -0
  55. package/templates/base-web/src/app/api/billing/checkout/route.ts +26 -0
  56. package/templates/base-web/src/app/api/billing/portal/route.ts +25 -0
  57. package/templates/base-web/src/app/api/email/test/route.ts +28 -0
  58. package/templates/base-web/src/app/api/webhooks/polar/route.ts +5 -0
  59. package/templates/base-web/src/app/api/webhooks/stripe/route.ts +5 -0
  60. package/templates/base-web/src/app/billing/page.tsx +55 -0
  61. package/templates/base-web/src/app/dashboard/page.tsx +15 -0
  62. package/templates/base-web/src/app/email/page.tsx +46 -0
  63. package/templates/base-web/src/app/error.tsx +27 -0
  64. package/templates/base-web/src/app/globals.css +534 -0
  65. package/templates/base-web/src/app/layout.tsx +19 -0
  66. package/templates/base-web/src/app/llms-full.txt/route.ts +158 -0
  67. package/templates/base-web/src/app/llms.txt/route.ts +59 -0
  68. package/templates/base-web/src/app/loading.tsx +24 -0
  69. package/templates/base-web/src/app/not-found.tsx +16 -0
  70. package/templates/base-web/src/app/page.tsx +5 -0
  71. package/templates/base-web/src/app/sign-in/page.tsx +14 -0
  72. package/templates/base-web/src/app/sign-up/page.tsx +14 -0
  73. package/templates/base-web/src/components/auth/email-auth-form.test.tsx +40 -0
  74. package/templates/base-web/src/components/auth/email-auth-form.tsx +128 -0
  75. package/templates/base-web/src/components/auth/sign-out-button.tsx +29 -0
  76. package/templates/base-web/src/db/index.ts +16 -0
  77. package/templates/base-web/src/db/schema/auth.ts +4 -0
  78. package/templates/base-web/src/db/schema/index.ts +2 -0
  79. package/templates/base-web/src/db/schema/projects.ts +17 -0
  80. package/templates/base-web/src/db/seeds/index.ts +32 -0
  81. package/templates/base-web/src/lib/auth-client.ts +5 -0
  82. package/templates/base-web/src/lib/auth-session.ts +21 -0
  83. package/templates/base-web/src/lib/auth.ts +23 -0
  84. package/templates/base-web/src/lib/billing/index.ts +37 -0
  85. package/templates/base-web/src/lib/billing/providers/polar.ts +80 -0
  86. package/templates/base-web/src/lib/billing/providers/stripe.ts +77 -0
  87. package/templates/base-web/src/lib/billing/types.ts +25 -0
  88. package/templates/base-web/src/lib/email/index.ts +19 -0
  89. package/templates/base-web/src/lib/email/templates.test.ts +12 -0
  90. package/templates/base-web/src/lib/email/templates.ts +40 -0
  91. package/templates/base-web/src/lib/env.ts +83 -0
  92. package/templates/base-web/tests/e2e/home.spec.ts +8 -0
  93. package/templates/base-web/tsconfig.json +34 -0
  94. package/templates/base-web/vitest.config.ts +19 -0
  95. package/templates/blank/.env.example +16 -0
  96. package/templates/blank/.github/workflows/ci.yml +34 -0
  97. package/templates/blank/.husky/pre-commit +3 -0
  98. package/templates/blank/.husky/pre-push +3 -0
  99. package/templates/blank/.prettierignore +3 -0
  100. package/templates/blank/drizzle.config.ts +16 -0
  101. package/templates/blank/eslint.config.mjs +127 -0
  102. package/templates/blank/next-env.d.ts +4 -0
  103. package/templates/blank/next.config.ts +5 -0
  104. package/templates/blank/package.json +75 -0
  105. package/templates/blank/playwright.config.ts +21 -0
  106. package/templates/blank/prettier.config.mjs +9 -0
  107. package/templates/blank/proxy.ts +28 -0
  108. package/templates/blank/src/app/api/auth/[...all]/route.ts +5 -0
  109. package/templates/blank/src/app/api/billing/checkout/route.ts +26 -0
  110. package/templates/blank/src/app/api/billing/portal/route.ts +25 -0
  111. package/templates/blank/src/app/api/email/test/route.ts +28 -0
  112. package/templates/blank/src/app/api/webhooks/polar/route.ts +5 -0
  113. package/templates/blank/src/app/api/webhooks/stripe/route.ts +5 -0
  114. package/templates/blank/src/app/billing/page.tsx +70 -0
  115. package/templates/blank/src/app/email/page.tsx +46 -0
  116. package/templates/blank/src/app/globals.css +394 -0
  117. package/templates/blank/src/app/layout.tsx +19 -0
  118. package/templates/blank/src/app/page.tsx +23 -0
  119. package/templates/blank/src/app/sign-in/page.tsx +18 -0
  120. package/templates/blank/src/app/sign-up/page.tsx +18 -0
  121. package/templates/blank/src/components/auth/email-auth-form.test.tsx +39 -0
  122. package/templates/blank/src/components/auth/email-auth-form.tsx +109 -0
  123. package/templates/blank/src/components/auth/sign-out-button.tsx +29 -0
  124. package/templates/blank/src/db/index.ts +16 -0
  125. package/templates/blank/src/db/schema/auth.ts +4 -0
  126. package/templates/blank/src/db/schema/index.ts +2 -0
  127. package/templates/blank/src/db/schema/projects.ts +17 -0
  128. package/templates/blank/src/db/seeds/index.ts +28 -0
  129. package/templates/blank/src/lib/auth-client.ts +5 -0
  130. package/templates/blank/src/lib/auth-session.ts +11 -0
  131. package/templates/blank/src/lib/auth.ts +23 -0
  132. package/templates/blank/src/lib/billing/index.ts +37 -0
  133. package/templates/blank/src/lib/billing/providers/polar.ts +80 -0
  134. package/templates/blank/src/lib/billing/providers/stripe.ts +77 -0
  135. package/templates/blank/src/lib/billing/types.ts +25 -0
  136. package/templates/blank/src/lib/email/index.ts +19 -0
  137. package/templates/blank/src/lib/email/templates.test.ts +15 -0
  138. package/templates/blank/src/lib/email/templates.ts +40 -0
  139. package/templates/blank/src/lib/env.ts +80 -0
  140. package/templates/blank/tsconfig.json +34 -0
  141. package/templates/blank/vitest.config.ts +19 -0
  142. package/templates/dashboard/.env.example +16 -0
  143. package/templates/dashboard/.github/workflows/ci.yml +34 -0
  144. package/templates/dashboard/.husky/pre-commit +3 -0
  145. package/templates/dashboard/.husky/pre-push +3 -0
  146. package/templates/dashboard/.prettierignore +3 -0
  147. package/templates/dashboard/drizzle.config.ts +16 -0
  148. package/templates/dashboard/eslint.config.mjs +127 -0
  149. package/templates/dashboard/next-env.d.ts +4 -0
  150. package/templates/dashboard/next.config.ts +5 -0
  151. package/templates/dashboard/package.json +75 -0
  152. package/templates/dashboard/playwright.config.ts +21 -0
  153. package/templates/dashboard/prettier.config.mjs +9 -0
  154. package/templates/dashboard/proxy.ts +36 -0
  155. package/templates/dashboard/src/app/api/auth/[...all]/route.ts +5 -0
  156. package/templates/dashboard/src/app/api/billing/checkout/route.ts +26 -0
  157. package/templates/dashboard/src/app/api/billing/portal/route.ts +25 -0
  158. package/templates/dashboard/src/app/api/email/test/route.ts +28 -0
  159. package/templates/dashboard/src/app/api/webhooks/polar/route.ts +5 -0
  160. package/templates/dashboard/src/app/api/webhooks/stripe/route.ts +5 -0
  161. package/templates/dashboard/src/app/billing/layout.tsx +22 -0
  162. package/templates/dashboard/src/app/billing/page.tsx +73 -0
  163. package/templates/dashboard/src/app/dashboard/layout.tsx +22 -0
  164. package/templates/dashboard/src/app/dashboard/page.tsx +104 -0
  165. package/templates/dashboard/src/app/email/layout.tsx +22 -0
  166. package/templates/dashboard/src/app/email/page.tsx +54 -0
  167. package/templates/dashboard/src/app/globals.css +1357 -0
  168. package/templates/dashboard/src/app/layout.tsx +25 -0
  169. package/templates/dashboard/src/app/page.tsx +154 -0
  170. package/templates/dashboard/src/app/settings/layout.tsx +22 -0
  171. package/templates/dashboard/src/app/settings/page.tsx +85 -0
  172. package/templates/dashboard/src/app/sign-in/page.tsx +47 -0
  173. package/templates/dashboard/src/app/sign-up/page.tsx +47 -0
  174. package/templates/dashboard/src/components/auth/email-auth-form.test.tsx +39 -0
  175. package/templates/dashboard/src/components/auth/email-auth-form.tsx +160 -0
  176. package/templates/dashboard/src/components/auth/sign-out-button.tsx +29 -0
  177. package/templates/dashboard/src/components/dashboard/shell.tsx +158 -0
  178. package/templates/dashboard/src/db/index.ts +16 -0
  179. package/templates/dashboard/src/db/schema/auth.ts +4 -0
  180. package/templates/dashboard/src/db/schema/index.ts +2 -0
  181. package/templates/dashboard/src/db/schema/projects.ts +17 -0
  182. package/templates/dashboard/src/db/seeds/index.ts +28 -0
  183. package/templates/dashboard/src/lib/auth-client.ts +5 -0
  184. package/templates/dashboard/src/lib/auth-session.ts +11 -0
  185. package/templates/dashboard/src/lib/auth.ts +41 -0
  186. package/templates/dashboard/src/lib/billing/index.ts +37 -0
  187. package/templates/dashboard/src/lib/billing/providers/polar.ts +80 -0
  188. package/templates/dashboard/src/lib/billing/providers/stripe.ts +77 -0
  189. package/templates/dashboard/src/lib/billing/types.ts +25 -0
  190. package/templates/dashboard/src/lib/email/index.ts +19 -0
  191. package/templates/dashboard/src/lib/email/templates.test.ts +15 -0
  192. package/templates/dashboard/src/lib/email/templates.ts +40 -0
  193. package/templates/dashboard/src/lib/env.ts +88 -0
  194. package/templates/dashboard/tsconfig.json +34 -0
  195. package/templates/dashboard/vitest.config.ts +19 -0
@@ -0,0 +1,28 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { getSession } from "@/lib/auth-session";
4
+ import { sendEmail } from "@/lib/email";
5
+ import { createWelcomeEmailTemplate } from "@/lib/email/templates";
6
+ import { env } from "@/lib/env";
7
+
8
+ export async function POST() {
9
+ const session = await getSession();
10
+
11
+ if (!session) {
12
+ redirect("/sign-in");
13
+ }
14
+
15
+ if (env.EMAIL_PROVIDER === "none") {
16
+ redirect("/email");
17
+ }
18
+
19
+ await sendEmail({
20
+ to: session.user.email,
21
+ template: createWelcomeEmailTemplate({
22
+ appName: "Skit",
23
+ recipientName: session.user.name
24
+ })
25
+ });
26
+
27
+ redirect("/email?sent=1");
28
+ }
@@ -0,0 +1,5 @@
1
+ __POLAR_WEBHOOK_ROUTE_IMPORT__
2
+
3
+ export async function POST(__POLAR_WEBHOOK_ROUTE_SIGNATURE__) {
4
+ __POLAR_WEBHOOK_ROUTE_BODY__
5
+ }
@@ -0,0 +1,5 @@
1
+ __STRIPE_WEBHOOK_ROUTE_IMPORT__
2
+
3
+ export async function POST(__STRIPE_WEBHOOK_ROUTE_SIGNATURE__) {
4
+ __STRIPE_WEBHOOK_ROUTE_BODY__
5
+ }
@@ -0,0 +1,70 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { getSession } from "@/lib/auth-session";
4
+ import { env } from "@/lib/env";
5
+
6
+ export default async function BillingPage({
7
+ searchParams
8
+ }: {
9
+ searchParams: Promise<Record<string, string | string[] | undefined>>;
10
+ }) {
11
+ const session = await getSession();
12
+
13
+ if (!session) {
14
+ redirect("/sign-in");
15
+ }
16
+
17
+ const params = await searchParams;
18
+ const success = params.success === "1";
19
+ const canceled = params.canceled === "1";
20
+ const providers =
21
+ env.BILLING_PROVIDER === "both"
22
+ ? ["stripe", "polar"]
23
+ : env.BILLING_PROVIDER === "none"
24
+ ? []
25
+ : [env.BILLING_PROVIDER];
26
+
27
+ return (
28
+ <main className="shell auth-shell">
29
+ <section className="auth-card">
30
+ <p className="eyebrow">Billing</p>
31
+ <h1>Choose your billing provider</h1>
32
+ <p className="lede">
33
+ Provider abstraction routes all billing actions. Current
34
+ default: <strong>{env.BILLING_PROVIDER}</strong>.
35
+ </p>
36
+
37
+ {success ? <p className="form-success">Checkout completed or redirected successfully.</p> : null}
38
+ {canceled ? <p className="form-error">Checkout was canceled.</p> : null}
39
+
40
+ {providers.length === 0 ? (
41
+ <p className="form-error">
42
+ Billing is disabled. Re-run the starter with a billing provider to enable checkout flows.
43
+ </p>
44
+ ) : (
45
+ <div className="billing-grid">
46
+ {providers.map((provider) => (
47
+ <form
48
+ key={provider}
49
+ action="/api/billing/checkout"
50
+ method="post"
51
+ className="billing-card"
52
+ >
53
+ <input type="hidden" name="provider" value={provider} />
54
+ <h2>{provider === "stripe" ? "Stripe" : "Polar"}</h2>
55
+ <p>
56
+ {provider === "stripe"
57
+ ? "Create a subscription checkout session using the configured Stripe price."
58
+ : "Create a Polar checkout session using the configured product identifier."}
59
+ </p>
60
+ <button type="submit" className="auth-button">
61
+ {provider === "stripe" ? "Start Stripe checkout" : "Start Polar checkout"}
62
+ </button>
63
+ </form>
64
+ ))}
65
+ </div>
66
+ )}
67
+ </section>
68
+ </main>
69
+ );
70
+ }
@@ -0,0 +1,46 @@
1
+ import Link from "next/link";
2
+ import { redirect } from "next/navigation";
3
+
4
+ import { getSession } from "@/lib/auth-session";
5
+ import { env } from "@/lib/env";
6
+
7
+ export default async function EmailPage({
8
+ searchParams
9
+ }: {
10
+ searchParams: Promise<Record<string, string | string[] | undefined>>;
11
+ }) {
12
+ const session = await getSession();
13
+
14
+ if (!session) {
15
+ redirect("/sign-in");
16
+ }
17
+
18
+ const params = await searchParams;
19
+ const sent = params.sent === "1";
20
+ const emailDisabled = env.EMAIL_PROVIDER === "none";
21
+
22
+ return (
23
+ <main className="shell auth-shell">
24
+ <section className="auth-card">
25
+ <p className="eyebrow">Email</p>
26
+ <h1>Transactional email</h1>
27
+ <p className="lede">
28
+ {emailDisabled
29
+ ? "Email is disabled. Set EMAIL_PROVIDER to resend to enable transactional email."
30
+ : "Test your Resend integration and verify that transactional email is wired correctly."}
31
+ </p>
32
+
33
+ {sent ? <p className="form-success">Test email sent to {session.user.email}.</p> : null}
34
+
35
+ <div className="hero-actions" style={{ justifyContent: "flex-start" }}>
36
+ <form action="/api/email/test" method="post">
37
+ <button type="submit" className="auth-button" disabled={emailDisabled}>
38
+ Send test email
39
+ </button>
40
+ </form>
41
+ <Link href="/dashboard" className="btn-ghost">Back to dashboard</Link>
42
+ </div>
43
+ </section>
44
+ </main>
45
+ );
46
+ }
@@ -0,0 +1,394 @@
1
+ /* ── Skit Blank Template ── Design System ─────────────────── */
2
+
3
+ @import url("https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&f[]=general-sans@400,500,600&display=swap");
4
+
5
+ /* ── Tokens ─────────────────────────────────────────────────────── */
6
+
7
+ :root {
8
+ --font-display: "Satoshi", system-ui, sans-serif;
9
+ --font-body: "General Sans", system-ui, sans-serif;
10
+ --font-mono: "SF Mono", "Fira Code", "JetBrains Mono", ui-monospace, monospace;
11
+
12
+ --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
13
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
14
+
15
+ --radius-sm: 8px;
16
+ --radius-md: 12px;
17
+ --radius-lg: 16px;
18
+ --radius-xl: 24px;
19
+ --radius-full: 9999px;
20
+
21
+ --color-bg: #0a0a0f;
22
+ --color-bg-raised: #111118;
23
+ --color-bg-overlay: #16161f;
24
+ --color-bg-subtle: #1c1c27;
25
+ --color-bg-muted: #23232f;
26
+
27
+ --color-border: rgba(255, 255, 255, 0.06);
28
+ --color-border-hover: rgba(255, 255, 255, 0.12);
29
+ --color-border-active: rgba(255, 255, 255, 0.2);
30
+
31
+ --color-text: #f0eef5;
32
+ --color-text-secondary: #9490a5;
33
+ --color-text-muted: #5e5a70;
34
+ --color-text-inverse: #0a0a0f;
35
+
36
+ --color-accent: #00d4aa;
37
+ --color-accent-dim: rgba(0, 212, 170, 0.15);
38
+ --color-accent-glow: rgba(0, 212, 170, 0.3);
39
+ --color-accent-hover: #00eabc;
40
+
41
+ --color-warm: #f5a623;
42
+ --color-warm-dim: rgba(245, 166, 35, 0.12);
43
+
44
+ --color-danger: #f04e4e;
45
+ --color-danger-dim: rgba(240, 78, 78, 0.12);
46
+ --color-success: #34c759;
47
+ --color-success-dim: rgba(52, 199, 89, 0.12);
48
+
49
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
50
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
51
+ --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
52
+ --shadow-glow: 0 0 40px var(--color-accent-glow);
53
+ }
54
+
55
+ /* ── Reset & base ───────────────────────────────────────────────── */
56
+
57
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
58
+
59
+ html {
60
+ -webkit-font-smoothing: antialiased;
61
+ -moz-osx-font-smoothing: grayscale;
62
+ text-rendering: optimizeLegibility;
63
+ scroll-behavior: smooth;
64
+ }
65
+
66
+ body {
67
+ font-family: var(--font-body);
68
+ font-size: 15px;
69
+ line-height: 1.6;
70
+ color: var(--color-text);
71
+ background: var(--color-bg);
72
+ min-height: 100dvh;
73
+ }
74
+
75
+ a { color: inherit; text-decoration: none; }
76
+ button { cursor: pointer; font: inherit; border: none; background: none; }
77
+ input, textarea, select { font: inherit; }
78
+ img { display: block; max-width: 100%; }
79
+
80
+ ::selection {
81
+ background: var(--color-accent-dim);
82
+ color: var(--color-accent);
83
+ }
84
+
85
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
86
+ ::-webkit-scrollbar-track { background: transparent; }
87
+ ::-webkit-scrollbar-thumb { background: var(--color-border-hover); border-radius: 3px; }
88
+
89
+ body::before {
90
+ content: "";
91
+ position: fixed;
92
+ inset: 0;
93
+ z-index: 9999;
94
+ pointer-events: none;
95
+ opacity: 0.025;
96
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
97
+ background-repeat: repeat;
98
+ background-size: 256px 256px;
99
+ }
100
+
101
+ /* ── Animations ─────────────────────────────────────────────────── */
102
+
103
+ @keyframes fade-up {
104
+ from { opacity: 0; transform: translateY(20px); }
105
+ to { opacity: 1; transform: translateY(0); }
106
+ }
107
+ @keyframes fade-down {
108
+ from { opacity: 0; transform: translateY(-10px); }
109
+ to { opacity: 1; transform: translateY(0); }
110
+ }
111
+ @keyframes shimmer {
112
+ 0% { background-position: -200% 0; }
113
+ 100% { background-position: 200% 0; }
114
+ }
115
+
116
+ /* ── Typography ─────────────────────────────────────────────────── */
117
+
118
+ h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 700; line-height: 1.15; letter-spacing: -0.02em; color: var(--color-text); }
119
+ h1 { font-size: clamp(2rem, 5vw, 3.5rem); font-weight: 900; letter-spacing: -0.035em; }
120
+ h2 { font-size: clamp(1.5rem, 3vw, 2.25rem); }
121
+ h3 { font-size: clamp(1.125rem, 2vw, 1.5rem); }
122
+
123
+ .eyebrow {
124
+ font-family: var(--font-mono);
125
+ font-size: 0.75rem;
126
+ font-weight: 500;
127
+ text-transform: uppercase;
128
+ letter-spacing: 0.15em;
129
+ color: var(--color-accent);
130
+ }
131
+
132
+ .lede {
133
+ font-size: 1.0625rem;
134
+ line-height: 1.7;
135
+ color: var(--color-text-secondary);
136
+ max-width: 540px;
137
+ }
138
+
139
+ /* ── Buttons ────────────────────────────────────────────────────── */
140
+
141
+ .btn-primary, .auth-button {
142
+ display: inline-flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ gap: 8px;
146
+ padding: 12px 28px;
147
+ font-family: var(--font-display);
148
+ font-weight: 600;
149
+ font-size: 0.9375rem;
150
+ color: var(--color-text-inverse);
151
+ background: var(--color-accent);
152
+ border-radius: var(--radius-full);
153
+ transition: all 0.2s var(--ease-out-expo);
154
+ position: relative;
155
+ overflow: hidden;
156
+ }
157
+ .btn-primary::before, .auth-button::before {
158
+ content: "";
159
+ position: absolute;
160
+ inset: 0;
161
+ background: linear-gradient(135deg, transparent 40%, rgba(255, 255, 255, 0.2) 50%, transparent 60%);
162
+ background-size: 200% 200%;
163
+ animation: shimmer 3s ease-in-out infinite;
164
+ }
165
+ .btn-primary:hover, .auth-button:hover {
166
+ background: var(--color-accent-hover);
167
+ transform: translateY(-1px);
168
+ box-shadow: var(--shadow-glow);
169
+ }
170
+ .btn-primary:disabled, .auth-button:disabled { opacity: 0.5; pointer-events: none; }
171
+
172
+ .btn-secondary {
173
+ display: inline-flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ gap: 8px;
177
+ padding: 12px 28px;
178
+ font-family: var(--font-display);
179
+ font-weight: 600;
180
+ font-size: 0.9375rem;
181
+ color: var(--color-text);
182
+ background: var(--color-bg-raised);
183
+ border: 1px solid var(--color-border);
184
+ border-radius: var(--radius-full);
185
+ transition: all 0.2s var(--ease-out-expo);
186
+ }
187
+ .btn-secondary:hover {
188
+ border-color: var(--color-border-hover);
189
+ background: var(--color-bg-overlay);
190
+ transform: translateY(-1px);
191
+ }
192
+
193
+ .btn-ghost, .subtle-button {
194
+ display: inline-flex;
195
+ align-items: center;
196
+ justify-content: center;
197
+ gap: 6px;
198
+ padding: 8px 16px;
199
+ font-weight: 500;
200
+ font-size: 0.875rem;
201
+ color: var(--color-text-secondary);
202
+ border-radius: var(--radius-sm);
203
+ transition: all 0.15s ease;
204
+ }
205
+ .btn-ghost:hover, .subtle-button:hover { color: var(--color-text); background: var(--color-bg-subtle); }
206
+
207
+ /* ── Form fields ────────────────────────────────────────────────── */
208
+
209
+ .field {
210
+ display: flex;
211
+ flex-direction: column;
212
+ gap: 6px;
213
+ }
214
+ .field span {
215
+ font-size: 0.8125rem;
216
+ font-weight: 500;
217
+ color: var(--color-text-secondary);
218
+ }
219
+ .field input {
220
+ padding: 11px 16px;
221
+ background: var(--color-bg);
222
+ border: 1px solid var(--color-border);
223
+ border-radius: var(--radius-md);
224
+ color: var(--color-text);
225
+ font-size: 0.9375rem;
226
+ outline: none;
227
+ transition: all 0.2s ease;
228
+ }
229
+ .field input::placeholder { color: var(--color-text-muted); }
230
+ .field input:focus {
231
+ border-color: var(--color-accent);
232
+ box-shadow: 0 0 0 3px var(--color-accent-dim);
233
+ }
234
+
235
+ .form-error {
236
+ padding: 12px 16px;
237
+ background: var(--color-danger-dim);
238
+ border: 1px solid rgba(240, 78, 78, 0.2);
239
+ border-radius: var(--radius-md);
240
+ color: var(--color-danger);
241
+ font-size: 0.875rem;
242
+ font-weight: 500;
243
+ }
244
+ .form-success {
245
+ padding: 12px 16px;
246
+ background: var(--color-success-dim);
247
+ border: 1px solid rgba(52, 199, 89, 0.2);
248
+ border-radius: var(--radius-md);
249
+ color: var(--color-success);
250
+ font-size: 0.875rem;
251
+ font-weight: 500;
252
+ }
253
+
254
+ /* ═══════════════════════════════════════════════════════════════════
255
+ LANDING PAGE
256
+ ═══════════════════════════════════════════════════════════════════ */
257
+
258
+ .shell {
259
+ min-height: 100dvh;
260
+ position: relative;
261
+ overflow: hidden;
262
+ }
263
+
264
+ .shell::before, .shell::after {
265
+ content: "";
266
+ position: absolute;
267
+ border-radius: 50%;
268
+ filter: blur(120px);
269
+ opacity: 0.4;
270
+ pointer-events: none;
271
+ }
272
+ .shell::before {
273
+ width: 600px;
274
+ height: 600px;
275
+ background: radial-gradient(circle, var(--color-accent-glow), transparent 70%);
276
+ top: -200px;
277
+ right: -100px;
278
+ }
279
+ .shell::after {
280
+ width: 500px;
281
+ height: 500px;
282
+ background: radial-gradient(circle, rgba(91, 141, 239, 0.2), transparent 70%);
283
+ bottom: -100px;
284
+ left: -150px;
285
+ }
286
+
287
+ .hero {
288
+ max-width: 720px;
289
+ margin: 0 auto;
290
+ padding: 200px 32px 100px;
291
+ text-align: center;
292
+ position: relative;
293
+ z-index: 1;
294
+ }
295
+ .hero .eyebrow {
296
+ animation: fade-up 0.6s var(--ease-out-expo) 0.1s both;
297
+ display: inline-flex;
298
+ align-items: center;
299
+ gap: 8px;
300
+ padding: 6px 16px;
301
+ background: var(--color-accent-dim);
302
+ border: 1px solid rgba(0, 212, 170, 0.15);
303
+ border-radius: var(--radius-full);
304
+ margin-bottom: 24px;
305
+ }
306
+ .hero h1 {
307
+ animation: fade-up 0.6s var(--ease-out-expo) 0.2s both;
308
+ margin-bottom: 20px;
309
+ }
310
+ .hero h1 em {
311
+ font-style: normal;
312
+ background: linear-gradient(135deg, var(--color-accent), #5b8def);
313
+ background-clip: text;
314
+ -webkit-background-clip: text;
315
+ -webkit-text-fill-color: transparent;
316
+ }
317
+ .hero .lede {
318
+ animation: fade-up 0.6s var(--ease-out-expo) 0.3s both;
319
+ margin: 0 auto 40px;
320
+ }
321
+ .hero-actions {
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ gap: 12px;
326
+ flex-wrap: wrap;
327
+ animation: fade-up 0.6s var(--ease-out-expo) 0.4s both;
328
+ }
329
+
330
+ /* ═══════════════════════════════════════════════════════════════════
331
+ AUTH SCREENS
332
+ ═══════════════════════════════════════════════════════════════════ */
333
+
334
+ .auth-shell {
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ padding: 40px 20px;
339
+ }
340
+
341
+ .auth-card {
342
+ width: 100%;
343
+ max-width: 420px;
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 24px;
347
+ animation: fade-up 0.5s var(--ease-out-expo);
348
+ position: relative;
349
+ z-index: 1;
350
+ }
351
+ .auth-card h1 { font-size: 1.75rem; }
352
+
353
+ .auth-copy { display: flex; flex-direction: column; gap: 8px; }
354
+ .auth-copy .lede { font-size: 0.9375rem; }
355
+ .auth-fields { display: flex; flex-direction: column; gap: 16px; }
356
+
357
+ .auth-switch {
358
+ font-size: 0.875rem;
359
+ color: var(--color-text-secondary);
360
+ text-align: center;
361
+ }
362
+ .auth-switch a {
363
+ color: var(--color-accent);
364
+ font-weight: 500;
365
+ transition: color 0.15s ease;
366
+ }
367
+ .auth-switch a:hover { color: var(--color-accent-hover); }
368
+
369
+ /* ═══════════════════════════════════════════════════════════════════
370
+ BILLING & EMAIL
371
+ ═══════════════════════════════════════════════════════════════════ */
372
+
373
+ .billing-grid {
374
+ display: grid;
375
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
376
+ gap: 16px;
377
+ margin-top: 24px;
378
+ }
379
+ .billing-card {
380
+ background: var(--color-bg-raised);
381
+ border: 1px solid var(--color-border);
382
+ border-radius: var(--radius-lg);
383
+ padding: 24px;
384
+ display: flex;
385
+ flex-direction: column;
386
+ gap: 12px;
387
+ transition: all 0.25s var(--ease-out-expo);
388
+ }
389
+ .billing-card:hover {
390
+ border-color: var(--color-border-hover);
391
+ transform: translateY(-2px);
392
+ }
393
+ .billing-card h2 { font-size: 1.25rem; }
394
+ .billing-card p { font-size: 0.875rem; color: var(--color-text-secondary); line-height: 1.6; flex: 1; }
@@ -0,0 +1,19 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "__PROJECT_NAME__",
6
+ description: "Generated by Skit"
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children
11
+ }: Readonly<{
12
+ children: React.ReactNode;
13
+ }>) {
14
+ return (
15
+ <html lang="en">
16
+ <body>{children}</body>
17
+ </html>
18
+ );
19
+ }
@@ -0,0 +1,23 @@
1
+ import Link from "next/link";
2
+
3
+ export default function HomePage() {
4
+ return (
5
+ <main className="shell">
6
+ <section className="hero">
7
+ <p className="eyebrow">Skit</p>
8
+ <h1>
9
+ Start with a <em>clean SaaS baseline.</em>
10
+ </h1>
11
+ <p className="lede">
12
+ Auth, billing, email, and database scaffolding — all wired up.
13
+ A minimal foundation ready for real product work.
14
+ </p>
15
+ <div className="hero-actions">
16
+ <Link href="/sign-up" className="btn-primary">Create account</Link>
17
+ <Link href="/sign-in" className="btn-secondary">Sign in</Link>
18
+ <Link href="/dashboard" className="btn-ghost">Open dashboard</Link>
19
+ </div>
20
+ </section>
21
+ </main>
22
+ );
23
+ }
@@ -0,0 +1,18 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { EmailAuthForm } from "@/components/auth/email-auth-form";
4
+ import { getSession } from "@/lib/auth-session";
5
+
6
+ export default async function SignInPage() {
7
+ const session = await getSession();
8
+
9
+ if (session) {
10
+ redirect("/dashboard");
11
+ }
12
+
13
+ return (
14
+ <main className="shell auth-shell">
15
+ <EmailAuthForm mode="sign-in" />
16
+ </main>
17
+ );
18
+ }
@@ -0,0 +1,18 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { EmailAuthForm } from "@/components/auth/email-auth-form";
4
+ import { getSession } from "@/lib/auth-session";
5
+
6
+ export default async function SignUpPage() {
7
+ const session = await getSession();
8
+
9
+ if (session) {
10
+ redirect("/dashboard");
11
+ }
12
+
13
+ return (
14
+ <main className="shell auth-shell">
15
+ <EmailAuthForm mode="sign-up" />
16
+ </main>
17
+ );
18
+ }
@@ -0,0 +1,39 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { EmailAuthForm } from "@/components/auth/email-auth-form";
5
+
6
+ import type { ReactNode } from "react";
7
+
8
+ vi.mock("next/link", () => ({
9
+ default: ({ children, href }: { children: ReactNode; href: string }) => (
10
+ <a href={href}>{children}</a>
11
+ )
12
+ }));
13
+
14
+ vi.mock("next/navigation", () => ({
15
+ useRouter: () => ({
16
+ push: vi.fn(),
17
+ refresh: vi.fn()
18
+ })
19
+ }));
20
+
21
+ vi.mock("@/lib/auth-client", () => ({
22
+ authClient: {
23
+ signIn: {
24
+ email: vi.fn()
25
+ },
26
+ signUp: {
27
+ email: vi.fn()
28
+ }
29
+ }
30
+ }));
31
+
32
+ describe("EmailAuthForm", () => {
33
+ it("renders sign-in mode copy", () => {
34
+ render(<EmailAuthForm mode="sign-in" />);
35
+
36
+ expect(screen.getByRole("heading", { name: /sign in to your workspace/i })).toBeTruthy();
37
+ expect(screen.getByLabelText(/email/i)).toBeTruthy();
38
+ });
39
+ });