@usequark/quark-create-app 1.23.11

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 (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ export default function PWARegister() {
6
+ useEffect(() => {
7
+ if ("serviceWorker" in navigator && process.env.NODE_ENV === "production") {
8
+ navigator.serviceWorker
9
+ .register("/sw.js")
10
+ .catch(() => {});
11
+ }
12
+ }, []);
13
+
14
+ return null;
15
+ }
@@ -0,0 +1,32 @@
1
+ import { config, getAppUrl } from "@__QUARK_SCOPE__/config";
2
+
3
+ /**
4
+ * PWA app manifest.
5
+ *
6
+ * This deliberately replaces the base project's `manifest.js` rather than
7
+ * adding a second manifest file: Next.js serves `app/manifest.js` at
8
+ * `/manifest.webmanifest`, and a sibling `app/manifest.json` makes the build
9
+ * fail with "Cannot find module for page: /manifest.webmanifest".
10
+ */
11
+ export default function manifest() {
12
+ return {
13
+ name: config.appName,
14
+ short_name: config.appName,
15
+ description: config.appDescription,
16
+ theme_color: "#000000",
17
+ background_color: "#000000",
18
+ display: "standalone",
19
+ orientation: "portrait-primary",
20
+ start_url: "/",
21
+ scope: "/",
22
+ id: getAppUrl(),
23
+ icons: [
24
+ {
25
+ src: "/quark.svg",
26
+ sizes: "any",
27
+ type: "image/svg+xml",
28
+ purpose: "any maskable",
29
+ },
30
+ ],
31
+ };
32
+ }
@@ -0,0 +1,72 @@
1
+ const CACHE_NAME = "quark-v1";
2
+
3
+ self.addEventListener("install", () => {
4
+ self.skipWaiting();
5
+ });
6
+
7
+ self.addEventListener("activate", (event) => {
8
+ event.waitUntil(
9
+ caches.keys().then((keys) =>
10
+ Promise.all(
11
+ keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)),
12
+ ),
13
+ ),
14
+ );
15
+ self.clients.claim();
16
+ });
17
+
18
+ self.addEventListener("fetch", (event) => {
19
+ const { request } = event;
20
+
21
+ // Never cache API routes, auth, or non-GET requests
22
+ if (
23
+ request.method !== "GET" ||
24
+ request.url.includes("/api/") ||
25
+ request.url.includes("/auth/")
26
+ ) {
27
+ return;
28
+ }
29
+
30
+ // Navigation requests: network first, fall back to cache
31
+ if (request.mode === "navigate") {
32
+ event.respondWith(
33
+ fetch(request)
34
+ .then((response) => {
35
+ const clone = response.clone();
36
+ caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
37
+ return response;
38
+ })
39
+ .catch(() => caches.match(request)),
40
+ );
41
+ return;
42
+ }
43
+
44
+ // Static assets (Next.js chunks, images, fonts): cache first
45
+ if (
46
+ request.url.includes("/_next/static/") ||
47
+ /\.(png|jpg|jpeg|svg|gif|webp|avif|woff|woff2|ico)$/.test(request.url)
48
+ ) {
49
+ event.respondWith(
50
+ caches.match(request).then((cached) => {
51
+ if (cached) return cached;
52
+ return fetch(request).then((response) => {
53
+ const clone = response.clone();
54
+ caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
55
+ return response;
56
+ });
57
+ }),
58
+ );
59
+ return;
60
+ }
61
+
62
+ // Everything else: network first
63
+ event.respondWith(
64
+ fetch(request)
65
+ .then((response) => {
66
+ const clone = response.clone();
67
+ caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
68
+ return response;
69
+ })
70
+ .catch(() => caches.match(request)),
71
+ );
72
+ });
@@ -0,0 +1,44 @@
1
+ # @yourscope/ui
2
+
3
+ Scaffolded UI primitives for your Quark project.
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
+ ## Available Components
14
+
15
+ | Component | Type | Notes |
16
+ |---|---|---|
17
+ | `Button` | server | `variant`, `size`, `icon`, `href` props |
18
+ | `Input` | server | Native input attributes |
19
+ | `Label` | server | Native label attributes |
20
+ | `Textarea` | server | Native textarea attributes |
21
+ | `Select` | server | `children`, `value`, `onChange` |
22
+ | `Checkbox` | server | Native checkbox attributes |
23
+ | `Badge` | server | `variant`: default/primary/success/warning/danger/info |
24
+ | `Card` / `CardHeader` / `CardTitle` / `CardContent` / `CardFooter` | server | Composition primitives |
25
+ | `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableHead` / `TableCell` | server | Data tables |
26
+ | `Skeleton` | server | Loading placeholders |
27
+ | `ErrorBanner` | server | Inline error feedback |
28
+ | `Footer` | server | Public site footer |
29
+ | `Navbar` / `MobileNavbar` | server | Public navigation shells |
30
+ | `RichText` | client | Rich text editor |
31
+ | `Dialog` | client | Modal dialogs |
32
+ | `Toast` / `useToast` | client | Notifications |
33
+ | `ThemeProvider` / `useTheme` | client | Dark/light mode context |
34
+
35
+ ## Rules
36
+
37
+ - All styling via Tailwind CSS utility classes.
38
+ - Import from `@yourscope/ui` — never from `@/components/ui/*`.
39
+ - Every component accepts `className` for Tailwind overrides.
40
+ - Server Components are the default — add `"use client"` only when using hooks or browser APIs.
41
+
42
+ ## Theming
43
+
44
+ CSS variables in `globals.css` drive all component styles. Override tokens to retheme without editing component source. See `globals.css` in your web app for the full token reference.
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@myquark/ui",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./theme-constants": "./src/theme-constants.js"
9
+ },
10
+ "dependencies": {
11
+ "lucide-react": "^1.48.0",
12
+ "next": "16.3.6"
13
+ },
14
+ "devDependencies": {
15
+ "@types/react": "^19.3.0",
16
+ "@types/react-dom": "^19.3.0",
17
+ "jsdom": "^30.1.1",
18
+ "react": "19.3.0",
19
+ "react-dom": "19.3.0"
20
+ }
21
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+
3
+ const base =
4
+ "inline-flex items-center justify-center border font-medium bg-[--badge-bg] border-[--badge-border] text-[--badge-text]";
5
+
6
+ export function Badge({ variant = "default", className = "", ...props }) {
7
+ const cls = `${base} rounded-[--radius-default] ${className}`.trim();
8
+ return React.createElement("span", {
9
+ "data-badge-variant": variant,
10
+ className: cls,
11
+ style: {
12
+ minWidth: "var(--badge-min-width)",
13
+ paddingLeft: "var(--badge-padding-x)",
14
+ paddingRight: "var(--badge-padding-x)",
15
+ paddingTop: "var(--badge-padding-y)",
16
+ paddingBottom: "var(--badge-padding-y)",
17
+ fontSize: "var(--badge-font-size)",
18
+ textTransform: "var(--badge-text-transform)",
19
+ letterSpacing: "var(--badge-text-tracking)",
20
+ },
21
+ ...props,
22
+ });
23
+ }
@@ -0,0 +1,111 @@
1
+ import Link from "next/link.js";
2
+ import React from "react";
3
+ import { Spinner } from "./spinner.js";
4
+
5
+ const base =
6
+ "inline-flex items-center justify-center font-medium tracking-wide transition-all duration-200 linear cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:opacity-40 disabled:cursor-not-allowed active:opacity-80";
7
+
8
+ const variantCls =
9
+ "bg-[--btn-bg] border border-[--btn-border] text-[--btn-text] rounded-[--btn-radius] hover:brightness-[var(--btn-hover-brightness)] hover:shadow-[var(--btn-hover-shadow)] hover:bg-[var(--btn-hover-bg)] hover:text-[var(--btn-hover-text)] hover:opacity-[var(--btn-hover-opacity)] focus-visible:ring-[var(--btn-ring)]";
10
+
11
+ const sizes = {
12
+ sm: "h-8 px-3 text-sm",
13
+ md: "h-10 px-4 text-sm",
14
+ lg: "h-11 px-6 text-base",
15
+ };
16
+
17
+ const iconSizes = {
18
+ sm: "var(--btn-icon-sm)",
19
+ md: "var(--btn-icon-md)",
20
+ lg: "var(--btn-icon-lg)",
21
+ };
22
+
23
+ function buildIcon(icon, size) {
24
+ if (!icon) return null;
25
+
26
+ const iconSize = iconSizes[size] ?? iconSizes.md;
27
+ const iconStyle = { width: iconSize, height: iconSize, flexShrink: 0 };
28
+
29
+ if (React.isValidElement(icon)) {
30
+ const existingClassName = icon.props.className ?? "";
31
+ return React.cloneElement(icon, {
32
+ "aria-hidden": true,
33
+ focusable: false,
34
+ className: `${existingClassName}`.trim(),
35
+ style: { ...iconStyle, ...icon.props.style },
36
+ });
37
+ }
38
+
39
+ return React.createElement(
40
+ "span",
41
+ {
42
+ "aria-hidden": true,
43
+ style: iconStyle,
44
+ },
45
+ icon,
46
+ );
47
+ }
48
+
49
+ export function Button({
50
+ variant = "default",
51
+ size = "md",
52
+ className = "",
53
+ href,
54
+ target,
55
+ rel,
56
+ type = "button",
57
+ icon,
58
+ loading = false,
59
+ children,
60
+ ...props
61
+ }) {
62
+ const hasLabel =
63
+ children !== null && children !== undefined && children !== false;
64
+ const hasIcon = Boolean(icon);
65
+ const spacing = hasIcon && hasLabel ? "gap-2" : "";
66
+ const cls =
67
+ `${base} ${variantCls} ${sizes[size] ?? sizes.md} ${spacing} ${className}`.trim();
68
+ const buttonType =
69
+ type === "submit" || type === "reset" || type === "button"
70
+ ? type
71
+ : "button";
72
+
73
+ const buttonChildren = [];
74
+
75
+ if (loading)
76
+ buttonChildren.push(
77
+ React.createElement(Spinner, { key: "spinner", className: "h-4 w-4" }),
78
+ );
79
+ if (hasIcon) buttonChildren.push(buildIcon(icon, size));
80
+ if (hasLabel) buttonChildren.push(children);
81
+
82
+ if (href) {
83
+ const anchorRel =
84
+ target === "_blank" ? (rel ?? "noopener noreferrer") : rel;
85
+ return React.createElement(
86
+ Link,
87
+ {
88
+ href: loading ? undefined : href,
89
+ target,
90
+ rel: anchorRel,
91
+ className: cls,
92
+ "data-btn-variant": variant,
93
+ "aria-disabled": loading || undefined,
94
+ ...props,
95
+ },
96
+ ...buttonChildren,
97
+ );
98
+ }
99
+
100
+ return React.createElement(
101
+ "button",
102
+ {
103
+ type: buttonType,
104
+ className: cls,
105
+ "data-btn-variant": variant,
106
+ disabled: loading || props.disabled,
107
+ ...props,
108
+ },
109
+ ...buttonChildren,
110
+ );
111
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import { ChevronDown } from "lucide-react";
4
+ import React, { useState } from "react";
5
+
6
+ const cardCls =
7
+ "border border-[--card-border] bg-[--card-bg] transition-colors duration-200 linear";
8
+
9
+ const headerCls = "flex flex-col space-y-1.5 p-6";
10
+
11
+ const titleCls =
12
+ "text-lg font-bold leading-none tracking-tight text-[--card-title-text]";
13
+
14
+ const contentCls = "p-6 pt-0";
15
+
16
+ const footerCls = "flex items-center p-6 pt-0";
17
+
18
+ const collapsibleTriggerCls =
19
+ "flex w-full cursor-pointer items-center justify-between p-6 text-[--card-title-text] transition-colors duration-200 hover:bg-[--card-trigger-hover-bg] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--card-trigger-ring)]";
20
+
21
+ function CollapsibleCard({
22
+ className = "",
23
+ collapsibleLabel = "Card Details",
24
+ defaultOpen = false,
25
+ children,
26
+ ...props
27
+ }) {
28
+ const [isOpen, setIsOpen] = useState(defaultOpen);
29
+
30
+ return React.createElement(
31
+ "div",
32
+ {
33
+ className:
34
+ `${cardCls} h-fit self-start rounded-[--radius-default] ${className}`.trim(),
35
+ ...props,
36
+ },
37
+ React.createElement(
38
+ "button",
39
+ {
40
+ type: "button",
41
+ className: collapsibleTriggerCls,
42
+ onClick: () => setIsOpen((prev) => !prev),
43
+ "aria-expanded": isOpen,
44
+ },
45
+ React.createElement(
46
+ "div",
47
+ { className: "text-base font-bold tracking-tight flex" },
48
+ React.createElement("span", null, collapsibleLabel),
49
+ ),
50
+ React.createElement(
51
+ "span",
52
+ {
53
+ "aria-hidden": "true",
54
+ className: `pointer-events-none ml-3 shrink-0 text-[--card-text-muted] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] ${isOpen ? "rotate-180" : "rotate-0"}`,
55
+ },
56
+ React.createElement(ChevronDown, { size: 16 }),
57
+ ),
58
+ ),
59
+ React.createElement(
60
+ "div",
61
+ {
62
+ className: `border-t border-[--card-border] text-[--card-text-muted] grid transition-[grid-template-rows,opacity,transform] duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none ${isOpen ? "translate-y-0 opacity-100" : "-translate-y-2 opacity-0"}`,
63
+ style: { gridTemplateRows: isOpen ? "1fr" : "0fr" },
64
+ },
65
+ React.createElement(
66
+ "div",
67
+ { className: "overflow-hidden min-h-0" },
68
+ React.createElement("div", { className: "p-6" }, children),
69
+ ),
70
+ ),
71
+ );
72
+ }
73
+
74
+ export function Card({
75
+ className = "",
76
+ variant = "default",
77
+ collapsibleLabel = "Card Details",
78
+ defaultOpen = false,
79
+ children,
80
+ ...props
81
+ }) {
82
+ if (variant === "collapsible") {
83
+ return React.createElement(CollapsibleCard, {
84
+ className,
85
+ collapsibleLabel,
86
+ defaultOpen,
87
+ children,
88
+ ...props,
89
+ });
90
+ }
91
+
92
+ return React.createElement(
93
+ "div",
94
+ {
95
+ className: `${cardCls} rounded-[--radius-default] ${className}`.trim(),
96
+ ...props,
97
+ },
98
+ children,
99
+ );
100
+ }
101
+
102
+ export function CardHeader({ className = "", ...props }) {
103
+ return React.createElement("div", {
104
+ className: `${headerCls} ${className}`.trim(),
105
+ ...props,
106
+ });
107
+ }
108
+
109
+ export function CardTitle({ className = "", ...props }) {
110
+ return React.createElement("h3", {
111
+ className: `${titleCls} ${className}`.trim(),
112
+ ...props,
113
+ });
114
+ }
115
+
116
+ export function CardContent({ className = "", ...props }) {
117
+ return React.createElement("div", {
118
+ className: `${contentCls} ${className}`.trim(),
119
+ ...props,
120
+ });
121
+ }
122
+
123
+ export function CardFooter({ className = "", ...props }) {
124
+ return React.createElement("div", {
125
+ className: `${footerCls} ${className}`.trim(),
126
+ ...props,
127
+ });
128
+ }
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+
3
+ const inputCls =
4
+ "h-4 w-4 rounded border-[--checkbox-border] bg-[--checkbox-bg] accent-[--checkbox-accent] transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-[var(--checkbox-ring)] disabled:cursor-not-allowed disabled:opacity-[var(--checkbox-disabled-opacity)]";
5
+
6
+ const labelCls = "text-sm text-[--checkbox-label-text] select-none";
7
+
8
+ export function Checkbox({ id, label, className = "", ...props }) {
9
+ return React.createElement(
10
+ "div",
11
+ { className: "flex items-center gap-2" },
12
+ React.createElement("input", {
13
+ id,
14
+ type: "checkbox",
15
+ className: `${inputCls} ${className}`.trim(),
16
+ ...props,
17
+ }),
18
+ React.createElement("label", { htmlFor: id, className: labelCls }, label),
19
+ );
20
+ }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+
3
+ export function Container({ className = "", children, ...props }) {
4
+ return React.createElement(
5
+ "section",
6
+ {
7
+ className:
8
+ `relative overflow-hidden rounded-[--radius-default] border border-[--container-border] bg-[--container-bg] ${className}`.trim(),
9
+ ...props,
10
+ },
11
+ children,
12
+ );
13
+ }
@@ -0,0 +1,137 @@
1
+ "use client";
2
+ import React, {
3
+ useCallback,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+
10
+ /**
11
+ * Dialog - centered modal built on the native <dialog> element.
12
+ *
13
+ * Props:
14
+ * open (bool) - controlled open state
15
+ * onClose (fn) - called when the dialog should close
16
+ * title (string) - header text
17
+ * children - dialog body content
18
+ * className (string) - merged onto the <dialog> element
19
+ *
20
+ * Architecture:
21
+ * • ALL close paths (header ×, backdrop click, Escape, external button)
22
+ * flow through the `open` prop → effects, which own the animation +
23
+ * el.open/close calls. handleClose() just calls onClose().
24
+ * • showModal() runs in useLayoutEffect (before browser paint) to prevent
25
+ * the first-load flash where the dialog appears at full opacity before
26
+ * the CSS entry animation can set opacity: 0.
27
+ * • isClosingRef (ref, not state) prevents the close effect from double-firing.
28
+ */
29
+
30
+ const dialogCls =
31
+ "backdrop:bg-[var(--dialog-backdrop)] border border-[--dialog-border] bg-[--dialog-bg] p-0 w-full max-w-lg rounded-[--radius-default]";
32
+
33
+ const headerCls =
34
+ "flex items-center justify-between border-b border-[--dialog-border] px-5 py-4";
35
+
36
+ const titleCls =
37
+ "text-base font-bold tracking-tight text-[--dialog-title-text]";
38
+
39
+ const closeCls =
40
+ "flex h-8 w-8 cursor-pointer items-center justify-center text-lg text-[--dialog-text-muted] transition-colors duration-200 linear hover:text-[--dialog-title-text] hover:bg-[--dialog-close-hover-bg] active:bg-[--dialog-close-hover-bg] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--dialog-close-ring)] rounded-[--radius-default]";
41
+
42
+ const bodyCls = "px-5 py-4 text-[--dialog-text-muted]";
43
+
44
+ export function Dialog({ open, onClose, title, children, className = "" }) {
45
+ const ref = useRef(null);
46
+ const [isClosing, setIsClosing] = useState(false);
47
+ const isClosingRef = useRef(false);
48
+
49
+ // Call showModal() before the browser paints so the CSS entry animation
50
+ // starts from opacity: 0 on the very first open (useEffect fires too late).
51
+ useLayoutEffect(() => {
52
+ const el = ref.current;
53
+ if (!el) return;
54
+ if (open && !el.open && typeof el.showModal === "function") el.showModal();
55
+ }, [open]);
56
+
57
+ // Manage state resets (open) and the exit animation (close).
58
+ useEffect(() => {
59
+ const el = ref.current;
60
+ if (!el) return;
61
+
62
+ if (open) {
63
+ isClosingRef.current = false;
64
+ setIsClosing(false);
65
+ } else {
66
+ // Play exit animation then close native element - regardless of who triggered close
67
+ if (isClosingRef.current) return; // already animating out, don't double-trigger
68
+ isClosingRef.current = true;
69
+ setIsClosing(true);
70
+ const t = setTimeout(() => {
71
+ if (el.open && typeof el.close === "function") el.close();
72
+ setIsClosing(false);
73
+ isClosingRef.current = false;
74
+ }, 180);
75
+ return () => clearTimeout(t);
76
+ }
77
+ }, [open]);
78
+
79
+ // All close-trigger sources just call onClose() - parent sets open=false,
80
+ // which re-runs the effect above and plays the animation.
81
+ const handleClose = useCallback(() => {
82
+ if (!isClosingRef.current) onClose?.();
83
+ }, [onClose]);
84
+
85
+ // Backdrop click (target is the <dialog> element itself)
86
+ const handleDialogClick = useCallback(
87
+ (e) => {
88
+ if (e.target === ref.current) handleClose();
89
+ },
90
+ [handleClose],
91
+ );
92
+
93
+ // Intercept Escape so we can animate before the native dialog closes
94
+ const handleCancel = useCallback(
95
+ (e) => {
96
+ e.preventDefault();
97
+ handleClose();
98
+ },
99
+ [handleClose],
100
+ );
101
+
102
+ const animStyle =
103
+ open || isClosing
104
+ ? isClosing
105
+ ? { animation: "quark-dialog-out 0.18s ease-in forwards" }
106
+ : { animation: "quark-dialog-in 0.2s ease-out forwards" }
107
+ : {};
108
+
109
+ return React.createElement(
110
+ "dialog",
111
+ {
112
+ ref,
113
+ onClick: handleDialogClick,
114
+ onCancel: handleCancel,
115
+ style: animStyle,
116
+ className: `${dialogCls} ${className}`.trim(),
117
+ },
118
+ // Header
119
+ React.createElement(
120
+ "div",
121
+ { className: headerCls },
122
+ React.createElement("h2", { className: titleCls }, title),
123
+ React.createElement(
124
+ "button",
125
+ {
126
+ type: "button",
127
+ "aria-label": "Close dialog",
128
+ onClick: handleClose,
129
+ className: closeCls,
130
+ },
131
+ "\u00d7",
132
+ ),
133
+ ),
134
+ // Body
135
+ React.createElement("div", { className: bodyCls }, children),
136
+ );
137
+ }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+
3
+ export function ErrorBanner({ message, className = "" }) {
4
+ if (!message) return null;
5
+ return React.createElement(
6
+ "div",
7
+ {
8
+ role: "alert",
9
+ "aria-live": "polite",
10
+ className:
11
+ `mb-4 border border-[--error-border] bg-[--error-bg] px-3 py-2 text-sm text-[--error-text] rounded-[--radius-default] ${className}`.trim(),
12
+ },
13
+ message,
14
+ );
15
+ }