@0x0501/mystack 0.0.0-stage → 0.1.0-bootstrap.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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -2
  3. package/assets/auth.sqlite.schema.ts +479 -0
  4. package/assets/bun-sqlite.test.ts +46 -0
  5. package/assets/d1.test.ts +41 -0
  6. package/assets/drizzle.config.ts +2 -0
  7. package/assets/generate-auth-schema.mjs +25 -0
  8. package/assets/platform.sqlite.schema.ts +46 -0
  9. package/assets/polar-checkout.ts +10 -0
  10. package/assets/polar-route.ts +19 -0
  11. package/assets/polar.test.ts +30 -0
  12. package/assets/polar.ts +12 -0
  13. package/assets/purchase.bun-sqlite.ts +38 -0
  14. package/assets/purchase.sqlite.ts +39 -0
  15. package/assets/sqlite-migrations/0000_thin_phalanx.sql +295 -0
  16. package/assets/sqlite-migrations/meta/0000_snapshot.json +2058 -0
  17. package/assets/sqlite-migrations/meta/_journal.json +13 -0
  18. package/assets/turso.test.ts +40 -0
  19. package/package.json +44 -5
  20. package/src/adapters/database.mjs +448 -0
  21. package/src/adapters/deployment.mjs +101 -0
  22. package/src/adapters/email.mjs +75 -0
  23. package/src/adapters/payments.mjs +270 -0
  24. package/src/catalog.mjs +128 -0
  25. package/src/cli.mjs +294 -0
  26. package/src/generate.mjs +183 -0
  27. package/src/template.mjs +93 -0
  28. package/template/.env.docker +16 -0
  29. package/template/.env.example +67 -0
  30. package/template/DESIGN.md +58 -0
  31. package/template/LICENSE +21 -0
  32. package/template/biome.json +46 -0
  33. package/template/docker-compose.yml +47 -0
  34. package/template/docs/project-guide.md +61 -0
  35. package/template/drizzle/0000_create-app-role.sql +54 -0
  36. package/template/drizzle/0001_add-rls-context.sql +21 -0
  37. package/template/drizzle/0002_platform_tables.sql +320 -0
  38. package/template/drizzle/meta/0002_snapshot.json +2125 -0
  39. package/template/drizzle/meta/_journal.json +27 -0
  40. package/template/drizzle.config.ts +25 -0
  41. package/template/e2e/authenticated-ui.spec.ts +40 -0
  42. package/template/e2e/headless-design.spec.ts +66 -0
  43. package/template/e2e/navigation-loading.spec.ts +70 -0
  44. package/template/e2e/theme-responsive.spec.ts +260 -0
  45. package/template/gitignore +24 -0
  46. package/template/messages/de.json +212 -0
  47. package/template/messages/en.json +212 -0
  48. package/template/package.json +101 -0
  49. package/template/paraglide.config.ts +49 -0
  50. package/template/playwright.config.ts +58 -0
  51. package/template/project.inlang/settings.json +12 -0
  52. package/template/public/drizzle.svg +1 -0
  53. package/template/public/favicon.ico +0 -0
  54. package/template/public/logo192.png +0 -0
  55. package/template/public/logo512.png +0 -0
  56. package/template/public/manifest.json +25 -0
  57. package/template/public/robots.txt +3 -0
  58. package/template/scripts/check-client-bundle.ts +150 -0
  59. package/template/scripts/check-server-fns.ts +290 -0
  60. package/template/scripts/check-sql-raw.ts +50 -0
  61. package/template/scripts/compile-paraglide.ts +12 -0
  62. package/template/scripts/postgres/README.md +76 -0
  63. package/template/scripts/postgres/create-app-role.sh +60 -0
  64. package/template/scripts/seed-local.ts +331 -0
  65. package/template/scripts/validate-prod-env.ts +53 -0
  66. package/template/src/__tests__/README.md +9 -0
  67. package/template/src/__tests__/account-security.test.ts +664 -0
  68. package/template/src/__tests__/admin-http-surface.test.ts +311 -0
  69. package/template/src/__tests__/admin-peer-protection.test.ts +178 -0
  70. package/template/src/__tests__/auth-harness.ts +104 -0
  71. package/template/src/__tests__/auth-rate-limit.test.ts +43 -0
  72. package/template/src/__tests__/auth-redirect.test.ts +120 -0
  73. package/template/src/__tests__/body-limit.test.ts +47 -0
  74. package/template/src/__tests__/creem.test.ts +62 -0
  75. package/template/src/__tests__/demo-checkout.test.ts +141 -0
  76. package/template/src/__tests__/email-locale.test.ts +110 -0
  77. package/template/src/__tests__/global-setup.ts +80 -0
  78. package/template/src/__tests__/input-rules.test.ts +0 -0
  79. package/template/src/__tests__/message-parity.test.ts +38 -0
  80. package/template/src/__tests__/navigation-loading.test.tsx +252 -0
  81. package/template/src/__tests__/nowpayments.test.ts +106 -0
  82. package/template/src/__tests__/paraglide-url-contract.test.ts +87 -0
  83. package/template/src/__tests__/purchase-fulfillment.test.ts +204 -0
  84. package/template/src/__tests__/query-unauthorized-redirect.test.ts +95 -0
  85. package/template/src/__tests__/rls-policy.test.ts +120 -0
  86. package/template/src/__tests__/same-page-locale-href.test.ts +47 -0
  87. package/template/src/__tests__/search-params-oauth.test.ts +57 -0
  88. package/template/src/__tests__/security-headers.test.ts +12 -0
  89. package/template/src/__tests__/server-fn-error-contract.test.ts +173 -0
  90. package/template/src/__tests__/stripe.test.ts +84 -0
  91. package/template/src/__tests__/test-db.ts +32 -0
  92. package/template/src/__tests__/test-rls-db.ts +23 -0
  93. package/template/src/__tests__/user-update-guard.test.ts +109 -0
  94. package/template/src/components/LocaleSwitcher.tsx +79 -0
  95. package/template/src/components/ThemeToggle.tsx +47 -0
  96. package/template/src/components/auth/social.tsx +90 -0
  97. package/template/src/components/auth/turnstile.tsx +67 -0
  98. package/template/src/components/root/navigation-progress.tsx +32 -0
  99. package/template/src/components/root/not-found.tsx +20 -0
  100. package/template/src/components/root/pending.tsx +9 -0
  101. package/template/src/components/root/root-document.tsx +37 -0
  102. package/template/src/components/root/root-error.tsx +34 -0
  103. package/template/src/components/shells.tsx +201 -0
  104. package/template/src/components/ui/styles.ts +9 -0
  105. package/template/src/db/auth.schema.ts +455 -0
  106. package/template/src/db/helper.ts +38 -0
  107. package/template/src/db/index.ts +15 -0
  108. package/template/src/db/platform.schema.ts +122 -0
  109. package/template/src/db/schema.ts +2 -0
  110. package/template/src/env.ts +65 -0
  111. package/template/src/hooks/use-theme.ts +32 -0
  112. package/template/src/integrations/better-auth/header-user.tsx +36 -0
  113. package/template/src/integrations/better-auth/session-query.ts +11 -0
  114. package/template/src/integrations/react-email/PasswordOtpEmail.tsx +62 -0
  115. package/template/src/integrations/react-email/ResetPasswordEmail.tsx +64 -0
  116. package/template/src/integrations/react-email/VerifyEmail.tsx +72 -0
  117. package/template/src/integrations/react-email/theme.ts +75 -0
  118. package/template/src/integrations/tanstack-query/devtools.tsx +6 -0
  119. package/template/src/integrations/tanstack-query/root-provider.tsx +60 -0
  120. package/template/src/lib/auth-client.ts +27 -0
  121. package/template/src/lib/auth-paths.ts +94 -0
  122. package/template/src/lib/auth-plugins/existing-email-signup.ts +86 -0
  123. package/template/src/lib/auth-plugins/field-rules.ts +84 -0
  124. package/template/src/lib/auth-plugins/social-two-factor.ts +194 -0
  125. package/template/src/lib/auth-timing.ts +9 -0
  126. package/template/src/lib/auth.ts +279 -0
  127. package/template/src/lib/captcha.ts +11 -0
  128. package/template/src/lib/email.tsx +162 -0
  129. package/template/src/lib/github.ts +18 -0
  130. package/template/src/lib/locale-cookie.ts +12 -0
  131. package/template/src/lib/localized-pages.ts +25 -0
  132. package/template/src/lib/oauth-metadata.ts +19 -0
  133. package/template/src/lib/permissions.ts +29 -0
  134. package/template/src/lib/security-headers.ts +20 -0
  135. package/template/src/lib/site.ts +59 -0
  136. package/template/src/lib/theme.ts +42 -0
  137. package/template/src/lib/user-update-guard.ts +35 -0
  138. package/template/src/middlewares/admin.ts +46 -0
  139. package/template/src/middlewares/auth-rate-limit.ts +37 -0
  140. package/template/src/middlewares/better-auth.ts +15 -0
  141. package/template/src/middlewares/body-limit.ts +62 -0
  142. package/template/src/middlewares/database.ts +13 -0
  143. package/template/src/middlewares/protected.ts +45 -0
  144. package/template/src/routeTree.gen.ts +536 -0
  145. package/template/src/router.tsx +53 -0
  146. package/template/src/routes/[.]well-known/oauth-authorization-server/api/auth.ts +18 -0
  147. package/template/src/routes/[.]well-known/oauth-protected-resource/index.ts +32 -0
  148. package/template/src/routes/__root.tsx +175 -0
  149. package/template/src/routes/admin/audit.tsx +74 -0
  150. package/template/src/routes/admin/index.tsx +7 -0
  151. package/template/src/routes/admin/oauth-apps.tsx +164 -0
  152. package/template/src/routes/admin/route.tsx +27 -0
  153. package/template/src/routes/admin/users.tsx +215 -0
  154. package/template/src/routes/api/auth/$.ts +14 -0
  155. package/template/src/routes/api/auth/[.]well-known/openid-configuration.ts +15 -0
  156. package/template/src/routes/api/webhooks/creem.ts +69 -0
  157. package/template/src/routes/api/webhooks/nowpayments.ts +62 -0
  158. package/template/src/routes/api/webhooks/stripe.ts +74 -0
  159. package/template/src/routes/dashboard/account.tsx +430 -0
  160. package/template/src/routes/dashboard/index.tsx +128 -0
  161. package/template/src/routes/dashboard/route.tsx +17 -0
  162. package/template/src/routes/forgot-password.tsx +65 -0
  163. package/template/src/routes/index.tsx +85 -0
  164. package/template/src/routes/oauth/consent.tsx +77 -0
  165. package/template/src/routes/reset-password.tsx +78 -0
  166. package/template/src/routes/sign-in.tsx +174 -0
  167. package/template/src/routes/sign-up.tsx +135 -0
  168. package/template/src/routes/two-factor.tsx +74 -0
  169. package/template/src/server/README.md +3 -0
  170. package/template/src/server/account-security.ts +163 -0
  171. package/template/src/server/admin-audit.ts +37 -0
  172. package/template/src/server/admin-users.ts +112 -0
  173. package/template/src/server/auth.ts +44 -0
  174. package/template/src/server/demo-checkout.ts +67 -0
  175. package/template/src/server/oauth-clients.ts +83 -0
  176. package/template/src/server/payment-toggles.ts +39 -0
  177. package/template/src/server.ts +20 -0
  178. package/template/src/services/README.md +3 -0
  179. package/template/src/services/account-security.ts +893 -0
  180. package/template/src/services/admin-security.ts +164 -0
  181. package/template/src/services/admin-users.ts +181 -0
  182. package/template/src/services/auth-rate-limit.ts +32 -0
  183. package/template/src/services/creem.ts +83 -0
  184. package/template/src/services/demo-checkout.ts +100 -0
  185. package/template/src/services/nowpayments.ts +154 -0
  186. package/template/src/services/oauth-client.ts +85 -0
  187. package/template/src/services/oauth-clients.ts +15 -0
  188. package/template/src/services/payment-providers.ts +157 -0
  189. package/template/src/services/payment-toggles.ts +48 -0
  190. package/template/src/services/purchase.ts +233 -0
  191. package/template/src/services/stripe.ts +106 -0
  192. package/template/src/start.ts +35 -0
  193. package/template/src/styles/layout.css +93 -0
  194. package/template/src/styles/theme.css +84 -0
  195. package/template/src/styles/ui.css +233 -0
  196. package/template/src/styles.css +90 -0
  197. package/template/src/utils/api-error.ts +129 -0
  198. package/template/src/utils/auth-errors.ts +157 -0
  199. package/template/src/utils/auth-redirect.ts +61 -0
  200. package/template/src/utils/body-limit.ts +84 -0
  201. package/template/src/utils/cn.ts +6 -0
  202. package/template/src/utils/input-rules.ts +172 -0
  203. package/template/src/utils/locale-paths.ts +18 -0
  204. package/template/src/utils/same-page-locale-href.ts +20 -0
  205. package/template/src/utils/search-params.ts +71 -0
  206. package/template/tsconfig.json +30 -0
  207. package/template/tsr.config.json +6 -0
  208. package/template/vite.config.ts +24 -0
  209. package/template/vitest.config.ts +26 -0
  210. package/template/worker-configuration.d.ts +14660 -0
  211. package/template/wrangler.jsonc +39 -0
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Stripe Checkout webhook: signature verification and the framework-agnostic
3
+ * callback handler.
4
+ *
5
+ * Kept free of `env` and framework glue so it is unit-testable; the route
6
+ * (src/routes/api/webhooks/stripe.ts) supplies the secret and the
7
+ * service-context db.
8
+ */
9
+ import { createHmac, timingSafeEqual } from "node:crypto";
10
+ import type { Database } from "@/db";
11
+ import { markPurchasePaid } from "@/services/purchase";
12
+
13
+ /** The one event that releases funds. */
14
+ const PAID_EVENT = "checkout.session.completed";
15
+
16
+ /**
17
+ * Verify a `stripe-signature` header: HMAC-SHA256 (hex) over
18
+ * `${timestamp}.${rawBody}`, compared constant-time against the `v1` scheme,
19
+ * with a replay window. Returns false for anything malformed — never throws on
20
+ * bad input, because a throw would answer "retry me" and Stripe would replay a
21
+ * hostile body.
22
+ */
23
+ export function verifyStripeSignature(
24
+ secret: string,
25
+ rawBody: string,
26
+ signatureHeader: string | null | undefined,
27
+ toleranceSec = 300,
28
+ ): boolean {
29
+ if (!signatureHeader) return false;
30
+ const parts = Object.fromEntries(
31
+ signatureHeader.split(",").map((p) => {
32
+ const [k, v] = p.split("=");
33
+ return [k.trim(), v];
34
+ }),
35
+ );
36
+ const timestamp = parts.t;
37
+ const sig = parts.v1;
38
+ if (!timestamp || !sig) return false;
39
+ const age = Math.abs(Date.now() / 1000 - Number(timestamp));
40
+ if (Number.isNaN(age) || age > toleranceSec) return false;
41
+ const expected = createHmac("sha256", secret)
42
+ .update(`${timestamp}.${rawBody}`)
43
+ .digest("hex");
44
+ try {
45
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ export type StripeWebhookResult =
52
+ | "invalid_signature"
53
+ | "invalid_payload"
54
+ | "credited"
55
+ | "amount_mismatch"
56
+ | "unknown_order"
57
+ | "noop";
58
+
59
+ /**
60
+ * Handle a Stripe webhook: verify the signature, then fulfill only on
61
+ * `checkout.session.completed`, joining on the Checkout Session id (= Purchase
62
+ * `externalId`). `amount_total` is passed as an integrity check only —
63
+ * fulfillment still trusts the Purchase row's own amount and fails closed on a
64
+ * mismatch. Every other event type leaves the Purchase untouched.
65
+ *
66
+ * `db` must already carry service RLS context (the route wraps this in
67
+ * withRlsService). Throws only on a genuine processing/DB error — including a
68
+ * throw from the purchase-paid hook — so the route can answer 5xx and Stripe
69
+ * retries; every mapped outcome resolves so the route answers 200 and Stripe
70
+ * stops retrying.
71
+ */
72
+ export async function handleStripeWebhook(
73
+ db: Database,
74
+ args: { rawBody: string; signature: string | null; secret: string },
75
+ ): Promise<StripeWebhookResult> {
76
+ if (!verifyStripeSignature(args.secret, args.rawBody, args.signature)) {
77
+ return "invalid_signature";
78
+ }
79
+
80
+ // Stripe signs the raw string, so a valid signature over an unparseable body
81
+ // means the sender signed garbage. That is a client error, not a retry.
82
+ let event: {
83
+ type?: string;
84
+ data?: { object?: { id?: string; amount_total?: number } };
85
+ };
86
+ try {
87
+ event = JSON.parse(args.rawBody) as typeof event;
88
+ } catch {
89
+ return "invalid_payload";
90
+ }
91
+
92
+ if (event.type !== PAID_EVENT) return "noop";
93
+ const externalId = event.data?.object?.id;
94
+ if (!externalId) return "noop";
95
+
96
+ const result = await markPurchasePaid(db, {
97
+ provider: "stripe",
98
+ externalId,
99
+ reportedAmount: event.data?.object?.amount_total,
100
+ });
101
+ if (result.status === "not_found") return "unknown_order";
102
+ if (result.status === "amount_mismatch") return "amount_mismatch";
103
+ return result.status === "fulfilled" || result.status === "already_paid"
104
+ ? "credited"
105
+ : "noop";
106
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Global Start config: CSRF for server functions + HttpError wire serialization.
3
+ * Locale is handled by paraglideMiddleware + router rewrite — not here.
4
+ */
5
+ import { createSerializationAdapter } from "@tanstack/react-router";
6
+ import { createCsrfMiddleware, createStart } from "@tanstack/react-start";
7
+ import { HttpError } from "@/utils/api-error";
8
+
9
+ const csrfMiddleware = createCsrfMiddleware({
10
+ filter: (ctx) => ctx.handlerType === "serverFn",
11
+ });
12
+
13
+ // A thrown HttpError has to reach the client with its status intact. User
14
+ // adapters are composed *ahead* of the router's default shallow error
15
+ // serializer, so this one matches first and the shallow one never sees the
16
+ // value; without it a refusal degrades to a bare `Error` carrying only
17
+ // `message`.
18
+ //
19
+ // This registration is load-bearing and must never lag behind the middleware
20
+ // that throw: shipping the throws alone loses `.status` on the wire.
21
+ const httpErrorAdapter = createSerializationAdapter({
22
+ key: "tanstack-starter/HttpError",
23
+ test: (value): value is HttpError => value instanceof HttpError,
24
+ toSerializable: ({ status, error, message }: HttpError) => ({
25
+ status,
26
+ error,
27
+ message,
28
+ }),
29
+ fromSerializable: (value) => new HttpError(value),
30
+ });
31
+
32
+ export const startInstance = createStart(() => ({
33
+ serializationAdapters: [httpErrorAdapter],
34
+ requestMiddleware: [csrfMiddleware],
35
+ }));
@@ -0,0 +1,93 @@
1
+ /* Replaceable page layouts. Tailwind utilities take precedence. */
2
+ @layer components {
3
+ /* Simple page width — mobile to 4K */
4
+ .page {
5
+ width: 100%;
6
+ max-width: 42rem;
7
+ margin-inline: auto;
8
+ padding: 1.5rem 1rem 3rem;
9
+ }
10
+
11
+ .page--wide {
12
+ max-width: 56rem;
13
+ }
14
+
15
+ .page--narrow {
16
+ max-width: 24rem;
17
+ }
18
+
19
+ @media (min-width: 640px) {
20
+ .page {
21
+ padding: 2rem 1.5rem 4rem;
22
+ }
23
+ }
24
+
25
+ @media (min-width: 2560px) {
26
+ .page {
27
+ max-width: 48rem;
28
+ padding: 3rem 2rem 5rem;
29
+ font-size: 16px;
30
+ }
31
+ .page--wide {
32
+ max-width: 64rem;
33
+ }
34
+ }
35
+
36
+ .auth-shell {
37
+ min-height: 100dvh;
38
+ display: flex;
39
+ flex-direction: column;
40
+ justify-content: center;
41
+ }
42
+
43
+ .auth-shell__header {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ gap: 0.5rem;
48
+ margin-bottom: 1.5rem;
49
+ }
50
+
51
+ .shell-title {
52
+ font-size: 1.25rem;
53
+ font-weight: 500;
54
+ letter-spacing: -0.025em;
55
+ }
56
+
57
+ .auth-shell__title {
58
+ margin-bottom: 1.5rem;
59
+ }
60
+
61
+ .auth-shell__content {
62
+ padding: 1rem;
63
+ }
64
+
65
+ .app-shell__header {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 0.75rem;
69
+ margin-bottom: 2rem;
70
+ border-bottom: 1px solid var(--border);
71
+ padding-bottom: 1rem;
72
+ }
73
+
74
+ .shell-nav-link:hover {
75
+ text-decoration: underline;
76
+ }
77
+
78
+ .shell-nav-link[aria-current="page"] {
79
+ font-weight: 600;
80
+ text-decoration: underline;
81
+ }
82
+
83
+ @media (min-width: 640px) {
84
+ .auth-shell__content {
85
+ padding: 1.25rem;
86
+ }
87
+ .app-shell__header {
88
+ flex-direction: row;
89
+ align-items: center;
90
+ justify-content: space-between;
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,84 @@
1
+ /* Replace these defaults to apply a product identity. */
2
+ @layer base {
3
+ :root {
4
+ color-scheme: light;
5
+ --background: #ffffff;
6
+ --foreground: #111111;
7
+ --muted: #f5f5f5;
8
+ --muted-foreground: #666666;
9
+ --border: #e5e5e5;
10
+ --input: #d4d4d4;
11
+ --ring: #111111;
12
+ --card: #ffffff;
13
+ --card-foreground: #111111;
14
+ --primary: #111111;
15
+ --primary-foreground: #ffffff;
16
+ --secondary: #f5f5f5;
17
+ --secondary-foreground: #111111;
18
+ --accent: #f0f0f0;
19
+ --accent-foreground: #111111;
20
+ --destructive: #c00;
21
+ --destructive-foreground: #ffffff;
22
+ --success: #0a7a3e;
23
+ --success-foreground: #ffffff;
24
+ --warning: #a65f00;
25
+ --overlay: rgb(0 0 0 / 0.4);
26
+ --radius: 0.375rem;
27
+ --font-sans-stack:
28
+ system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
29
+ sans-serif;
30
+ }
31
+
32
+ .dark {
33
+ color-scheme: dark;
34
+ --background: #0a0a0a;
35
+ --foreground: #fafafa;
36
+ --muted: #171717;
37
+ --muted-foreground: #a3a3a3;
38
+ --border: #262626;
39
+ --input: #404040;
40
+ --ring: #fafafa;
41
+ --card: #0a0a0a;
42
+ --card-foreground: #fafafa;
43
+ --primary: #fafafa;
44
+ --primary-foreground: #0a0a0a;
45
+ --secondary: #171717;
46
+ --secondary-foreground: #fafafa;
47
+ --accent: #1a1a1a;
48
+ --accent-foreground: #fafafa;
49
+ --destructive: #f87171;
50
+ --destructive-foreground: #0a0a0a;
51
+ --success: #4ade80;
52
+ --success-foreground: #0a0a0a;
53
+ --warning: #fbbf24;
54
+ --overlay: rgb(0 0 0 / 0.6);
55
+ }
56
+ }
57
+
58
+ @theme inline {
59
+ --color-background: var(--background);
60
+ --color-foreground: var(--foreground);
61
+ --color-muted: var(--muted);
62
+ --color-muted-foreground: var(--muted-foreground);
63
+ --color-border: var(--border);
64
+ --color-input: var(--input);
65
+ --color-ring: var(--ring);
66
+ --color-card: var(--card);
67
+ --color-card-foreground: var(--card-foreground);
68
+ --color-primary: var(--primary);
69
+ --color-primary-foreground: var(--primary-foreground);
70
+ --color-secondary: var(--secondary);
71
+ --color-secondary-foreground: var(--secondary-foreground);
72
+ --color-accent: var(--accent);
73
+ --color-accent-foreground: var(--accent-foreground);
74
+ --color-destructive: var(--destructive);
75
+ --color-destructive-foreground: var(--destructive-foreground);
76
+ --color-success: var(--success);
77
+ --color-success-foreground: var(--success-foreground);
78
+ --color-warning: var(--warning);
79
+ --color-overlay: var(--overlay);
80
+ --radius-sm: calc(var(--radius) - 2px);
81
+ --radius-md: var(--radius);
82
+ --radius-lg: calc(var(--radius) + 2px);
83
+ --font-sans: var(--font-sans-stack);
84
+ }
@@ -0,0 +1,233 @@
1
+ /* Opt-in default presentation; Base UI itself remains unstyled. */
2
+ @layer components {
3
+ .ui-input,
4
+ .ui-select-trigger {
5
+ display: flex;
6
+ align-items: center;
7
+ width: 100%;
8
+ min-height: 2.25rem;
9
+ border: 1px solid var(--input);
10
+ border-radius: var(--radius);
11
+ padding: 0.375rem 0.75rem;
12
+ background: var(--background);
13
+ color: var(--foreground);
14
+ font-size: 0.875rem;
15
+ }
16
+
17
+ .ui-select-trigger {
18
+ justify-content: space-between;
19
+ gap: 0.5rem;
20
+ }
21
+
22
+ .ui-button {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ min-height: 2.25rem;
27
+ border-radius: var(--radius);
28
+ padding: 0.375rem 0.75rem;
29
+ font-size: 0.875rem;
30
+ font-weight: 500;
31
+ }
32
+
33
+ .ui-button--primary {
34
+ background: var(--primary);
35
+ color: var(--primary-foreground);
36
+ }
37
+
38
+ .ui-button--primary:hover:not(:disabled) {
39
+ opacity: 0.9;
40
+ }
41
+
42
+ .ui-button--secondary {
43
+ border: 1px solid var(--border);
44
+ background: var(--background);
45
+ color: var(--foreground);
46
+ }
47
+
48
+ .ui-button--secondary:hover:not(:disabled) {
49
+ background: var(--muted);
50
+ }
51
+
52
+ .ui-card {
53
+ border: 1px solid var(--border);
54
+ background: var(--card);
55
+ color: var(--card-foreground);
56
+ }
57
+
58
+ .ui-label {
59
+ display: block;
60
+ margin-bottom: 0.25rem;
61
+ color: var(--foreground);
62
+ font-size: 0.875rem;
63
+ }
64
+
65
+ .ui-muted,
66
+ .ui-select-icon {
67
+ color: var(--muted-foreground);
68
+ }
69
+
70
+ .ui-muted {
71
+ font-size: 0.875rem;
72
+ }
73
+
74
+ .ui-link {
75
+ text-decoration: underline;
76
+ text-underline-offset: 2px;
77
+ }
78
+
79
+ .ui-link:hover {
80
+ opacity: 0.8;
81
+ }
82
+
83
+ .ui-positioner {
84
+ z-index: 50;
85
+ }
86
+
87
+ .ui-select-popup,
88
+ .ui-menu-popup {
89
+ border: 1px solid var(--border);
90
+ border-radius: var(--radius);
91
+ padding-block: 0.25rem;
92
+ background: var(--background);
93
+ color: var(--foreground);
94
+ transform-origin: var(--transform-origin);
95
+ }
96
+
97
+ .ui-select-popup {
98
+ max-height: min(15rem, var(--available-height));
99
+ min-width: var(--anchor-width);
100
+ overflow: auto;
101
+ }
102
+
103
+ .ui-menu-popup {
104
+ min-width: 9rem;
105
+ overflow: hidden;
106
+ }
107
+
108
+ .ui-select-item,
109
+ .ui-menu-item {
110
+ display: block;
111
+ padding: 0.375rem 0.75rem;
112
+ font-size: 0.875rem;
113
+ cursor: pointer;
114
+ }
115
+
116
+ .ui-select-item[data-highlighted],
117
+ .ui-menu-item[data-highlighted] {
118
+ background: var(--muted);
119
+ }
120
+
121
+ .ui-select-item[data-selected],
122
+ .ui-menu-item[aria-current="true"] {
123
+ font-weight: 500;
124
+ }
125
+
126
+ .ui-checkbox {
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ width: 1rem;
131
+ height: 1rem;
132
+ flex-shrink: 0;
133
+ border: 1px solid var(--input);
134
+ border-radius: calc(var(--radius) - 2px);
135
+ background: var(--background);
136
+ }
137
+
138
+ .ui-checkbox[data-checked] {
139
+ border-color: var(--primary);
140
+ background: var(--primary);
141
+ color: var(--primary-foreground);
142
+ }
143
+
144
+ .ui-checkbox-indicator {
145
+ font-size: 0.625rem;
146
+ line-height: 1;
147
+ }
148
+
149
+ .ui-tabs-list {
150
+ display: flex;
151
+ flex-wrap: wrap;
152
+ gap: 1rem;
153
+ margin-bottom: 1rem;
154
+ border-bottom: 1px solid var(--border);
155
+ }
156
+
157
+ /* Tabs use data-active; Select uses data-selected. */
158
+ .ui-tabs-tab {
159
+ position: relative;
160
+ margin-bottom: -1px;
161
+ border-bottom: 2px solid transparent;
162
+ padding-bottom: 0.5rem;
163
+ color: var(--muted-foreground);
164
+ font-size: 0.875rem;
165
+ }
166
+
167
+ .ui-tabs-tab:hover,
168
+ .ui-tabs-tab[data-active] {
169
+ color: var(--foreground);
170
+ }
171
+
172
+ .ui-tabs-tab[data-active] {
173
+ border-color: var(--primary);
174
+ }
175
+
176
+ .ui-input:disabled,
177
+ .ui-select-trigger[data-disabled],
178
+ .ui-select-item[data-disabled],
179
+ .ui-checkbox[data-disabled],
180
+ .ui-tabs-tab[data-disabled] {
181
+ cursor: not-allowed;
182
+ opacity: 0.5;
183
+ }
184
+
185
+ .ui-theme-option,
186
+ .ui-locale-trigger {
187
+ display: inline-flex;
188
+ align-items: center;
189
+ gap: 0.25rem;
190
+ min-height: 2rem;
191
+ border-radius: var(--radius);
192
+ padding-inline: 0.5rem;
193
+ color: var(--muted-foreground);
194
+ font-size: 0.75rem;
195
+ }
196
+
197
+ .ui-theme-option:hover,
198
+ .ui-locale-trigger:hover {
199
+ color: var(--foreground);
200
+ }
201
+
202
+ .ui-theme-option[aria-pressed="true"] {
203
+ background: var(--primary);
204
+ color: var(--primary-foreground);
205
+ }
206
+
207
+ .ui-locale-trigger--button {
208
+ border: 1px solid var(--border);
209
+ color: var(--foreground);
210
+ }
211
+
212
+ .ui-locale-trigger--button:hover {
213
+ background: var(--muted);
214
+ }
215
+
216
+ /* Motion is a presentation choice. Only these defaults opt into it. */
217
+ @media (prefers-reduced-motion: no-preference) {
218
+ .ui-select-popup,
219
+ .ui-menu-popup {
220
+ transition:
221
+ opacity 150ms ease,
222
+ transform 150ms ease;
223
+ }
224
+
225
+ .ui-select-popup[data-starting-style],
226
+ .ui-select-popup[data-ending-style],
227
+ .ui-menu-popup[data-starting-style],
228
+ .ui-menu-popup[data-ending-style] {
229
+ opacity: 0;
230
+ transform: translateY(-0.25rem) scale(0.98);
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,90 @@
1
+ @import "tailwindcss";
2
+ @import "./styles/theme.css";
3
+ @import "./styles/layout.css";
4
+ @import "./styles/ui.css";
5
+
6
+ @custom-variant dark (&:where(.dark, .dark *));
7
+
8
+ /* Base UI portal stacking + iOS Safari — official quick start */
9
+ @layer base {
10
+ .root {
11
+ isolation: isolate;
12
+ min-height: 100dvh;
13
+ display: flex;
14
+ flex-direction: column;
15
+ background: var(--background);
16
+ color: var(--foreground);
17
+ }
18
+
19
+ body {
20
+ position: relative;
21
+ margin: 0;
22
+ min-height: 100%;
23
+ font-family: var(--font-sans-stack);
24
+ font-size: 15px;
25
+ line-height: 1.5;
26
+ background: var(--background);
27
+ color: var(--foreground);
28
+ -webkit-font-smoothing: antialiased;
29
+ }
30
+
31
+ html,
32
+ body,
33
+ #app {
34
+ min-height: 100%;
35
+ }
36
+
37
+ *,
38
+ *::before,
39
+ *::after {
40
+ box-sizing: border-box;
41
+ border-color: var(--border);
42
+ }
43
+
44
+ input,
45
+ button,
46
+ textarea,
47
+ select {
48
+ font: inherit;
49
+ color: inherit;
50
+ }
51
+
52
+ input:not([type="checkbox"]):not([type="radio"]),
53
+ textarea,
54
+ select {
55
+ color: var(--foreground);
56
+ background: var(--background);
57
+ }
58
+
59
+ input::placeholder,
60
+ textarea::placeholder {
61
+ color: var(--muted-foreground);
62
+ opacity: 1;
63
+ }
64
+
65
+ button {
66
+ cursor: pointer;
67
+ }
68
+
69
+ button:disabled,
70
+ input:disabled,
71
+ textarea:disabled,
72
+ select:disabled {
73
+ cursor: not-allowed;
74
+ opacity: 0.5;
75
+ }
76
+
77
+ a {
78
+ color: inherit;
79
+ text-underline-offset: 2px;
80
+ }
81
+
82
+ :focus-visible {
83
+ outline: 2px solid var(--ring);
84
+ outline-offset: 2px;
85
+ }
86
+
87
+ table {
88
+ border-collapse: collapse;
89
+ }
90
+ }