@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.
- package/README.md +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- 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
|
+
}
|