@forgeailab/create-spark 0.1.1 → 0.1.3
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/.claude/skills/architecture-cutline/SKILL.md +96 -0
- package/.claude/skills/board-review/SKILL.md +77 -0
- package/.claude/skills/code-review/SKILL.md +76 -0
- package/.claude/skills/execute-task/SKILL.md +80 -0
- package/.claude/skills/idea-sharpen/SKILL.md +65 -0
- package/.claude/skills/implementation-brief/SKILL.md +87 -0
- package/.claude/skills/mvp-board/SKILL.md +95 -0
- package/.claude/skills/mvp-grill/SKILL.md +60 -0
- package/.claude/skills/mvp-spec/SKILL.md +78 -0
- package/.claude/skills/new-pack/SKILL.md +156 -0
- package/.claude/skills/next-task/SKILL.md +65 -0
- package/.claude/skills/pack-add/SKILL.md +64 -0
- package/.claude/skills/pack-resolve/SKILL.md +67 -0
- package/.claude/skills/parallel-execution/SKILL.md +68 -0
- package/.claude/skills/qa-verify/SKILL.md +77 -0
- package/.claude/skills/risk-check/SKILL.md +88 -0
- package/.claude/skills/sync-board/SKILL.md +76 -0
- package/.claude/skills/ux-theme/SKILL.md +93 -0
- package/.codex/skills/architecture-cutline/SKILL.md +94 -0
- package/.codex/skills/board-review/SKILL.md +75 -0
- package/.codex/skills/code-review/SKILL.md +73 -0
- package/.codex/skills/execute-task/SKILL.md +76 -0
- package/.codex/skills/idea-sharpen/SKILL.md +63 -0
- package/.codex/skills/implementation-brief/SKILL.md +85 -0
- package/.codex/skills/mvp-board/SKILL.md +93 -0
- package/.codex/skills/mvp-grill/SKILL.md +58 -0
- package/.codex/skills/mvp-spec/SKILL.md +76 -0
- package/.codex/skills/new-pack/SKILL.md +153 -0
- package/.codex/skills/next-task/SKILL.md +64 -0
- package/.codex/skills/pack-add/SKILL.md +62 -0
- package/.codex/skills/pack-resolve/SKILL.md +65 -0
- package/.codex/skills/parallel-execution/SKILL.md +66 -0
- package/.codex/skills/qa-verify/SKILL.md +74 -0
- package/.codex/skills/risk-check/SKILL.md +86 -0
- package/.codex/skills/sync-board/SKILL.md +72 -0
- package/.codex/skills/ux-theme/SKILL.md +91 -0
- package/package.json +10 -4
- package/packs/README.md +132 -0
- package/packs/ai-anthropic/files/app/api/ai/route.ts +57 -0
- package/packs/ai-anthropic/files/lib/anthropic.ts +15 -0
- package/packs/ai-anthropic/pack.toml +32 -0
- package/packs/ai-anthropic/skills/ai-feature-patterns/SKILL.md +87 -0
- package/packs/ai-anthropic/tasks.yaml +9 -0
- package/packs/ai-openai/files/app/api/ai-openai/route.ts +55 -0
- package/packs/ai-openai/files/lib/openai.ts +21 -0
- package/packs/ai-openai/pack.toml +30 -0
- package/packs/ai-openai/tasks.yaml +9 -0
- package/packs/analytics-posthog/files/components/PostHogProvider.tsx +19 -0
- package/packs/analytics-posthog/files/lib/posthog/client.ts +20 -0
- package/packs/analytics-posthog/files/lib/posthog/server.ts +24 -0
- package/packs/analytics-posthog/pack.toml +35 -0
- package/packs/analytics-posthog/tasks.yaml +15 -0
- package/packs/auth-better-auth/files/app/(auth)/login/page.tsx +58 -0
- package/packs/auth-better-auth/files/app/api/auth/[...all]/route.ts +4 -0
- package/packs/auth-better-auth/files/lib/auth.ts +21 -0
- package/packs/auth-better-auth/pack.toml +32 -0
- package/packs/auth-better-auth/tasks.yaml +10 -0
- package/packs/auth-better-auth-pg/files/app/api/auth/[...all]/route.ts +4 -0
- package/packs/auth-better-auth-pg/files/lib/auth.ts +86 -0
- package/packs/auth-better-auth-pg/pack.toml +32 -0
- package/packs/auth-better-auth-pg/tasks.yaml +17 -0
- package/packs/auth-supabase/files/app/(auth)/login/page.tsx +64 -0
- package/packs/auth-supabase/files/app/auth/callback/route.ts +15 -0
- package/packs/auth-supabase/files/middleware.ts +41 -0
- package/packs/auth-supabase/pack.toml +34 -0
- package/packs/auth-supabase/tasks.yaml +10 -0
- package/packs/db-postgres/files/compose/postgres.yml +28 -0
- package/packs/db-postgres/files/docker-compose.include.yml +1 -0
- package/packs/db-postgres/files/docker-compose.yml +6 -0
- package/packs/db-postgres/files/drizzle.config.ts +10 -0
- package/packs/db-postgres/files/lib/db/index.ts +10 -0
- package/packs/db-postgres/files/lib/db/schema.ts +11 -0
- package/packs/db-postgres/pack.toml +53 -0
- package/packs/db-postgres/tasks.yaml +11 -0
- package/packs/db-sqlite/files/drizzle.config.ts +10 -0
- package/packs/db-sqlite/files/lib/db.ts +8 -0
- package/packs/db-sqlite/files/lib/schema.ts +13 -0
- package/packs/db-sqlite/pack.toml +34 -0
- package/packs/db-sqlite/tasks.yaml +6 -0
- package/packs/db-supabase/files/lib/supabase/client.ts +8 -0
- package/packs/db-supabase/files/lib/supabase/server.ts +27 -0
- package/packs/db-supabase/pack.toml +32 -0
- package/packs/db-supabase/skills/supabase-patterns/SKILL.md +82 -0
- package/packs/db-supabase/tasks.yaml +6 -0
- package/packs/deploy-vercel/files/docs/deploy.md +21 -0
- package/packs/deploy-vercel/files/vercel.json +4 -0
- package/packs/deploy-vercel/pack.toml +30 -0
- package/packs/deploy-vercel/tasks.yaml +14 -0
- package/packs/docker-compose-dev/files/.env.docker.example +2 -0
- package/packs/docker-compose-dev/files/compose/redis.yml +17 -0
- package/packs/docker-compose-dev/files/docker-compose.include.yml +1 -0
- package/packs/docker-compose-dev/files/docker-compose.yml +6 -0
- package/packs/docker-compose-dev/pack.toml +38 -0
- package/packs/docker-compose-dev/tasks.yaml +9 -0
- package/packs/email-resend/files/app/api/email/test/route.ts +38 -0
- package/packs/email-resend/files/emails/welcome.tsx +66 -0
- package/packs/email-resend/files/lib/email.ts +40 -0
- package/packs/email-resend/pack.toml +34 -0
- package/packs/email-resend/tasks.yaml +9 -0
- package/packs/example/pack.toml +69 -0
- package/packs/payments-stripe/files/app/api/billing-portal/route.ts +24 -0
- package/packs/payments-stripe/files/app/api/checkout/route.ts +58 -0
- package/packs/payments-stripe/files/app/api/webhooks/stripe/route.ts +84 -0
- package/packs/payments-stripe/files/lib/stripe.ts +60 -0
- package/packs/payments-stripe/pack.toml +49 -0
- package/packs/payments-stripe/skills/stripe-patterns/SKILL.md +93 -0
- package/packs/payments-stripe/tasks.yaml +16 -0
- package/packs/sync-zero/files/components/ZeroProvider.tsx +3 -0
- package/packs/sync-zero/files/compose/zero-cache.yml +26 -0
- package/packs/sync-zero/files/docker-compose.include.yml +1 -0
- package/packs/sync-zero/files/docker-compose.yml +6 -0
- package/packs/sync-zero/files/lib/zero/client.ts +18 -0
- package/packs/sync-zero/files/lib/zero/schema.ts +17 -0
- package/packs/sync-zero/files/zero.config.ts +26 -0
- package/packs/sync-zero/pack.toml +61 -0
- package/packs/sync-zero/skills/zero-patterns/SKILL.md +69 -0
- package/packs/sync-zero/tasks.yaml +16 -0
- package/packs/testing-playwright/files/e2e/example.spec.ts +7 -0
- package/packs/testing-playwright/files/playwright.config.ts +33 -0
- package/packs/testing-playwright/pack.toml +25 -0
- package/packs/testing-playwright/tasks.yaml +9 -0
- package/packs/ui-shadcn/files/app/globals.css +56 -0
- package/packs/ui-shadcn/files/components/ui/button.tsx +47 -0
- package/packs/ui-shadcn/files/components/ui/card.tsx +33 -0
- package/packs/ui-shadcn/files/lib/utils.ts +6 -0
- package/packs/ui-shadcn/files/postcss.config.mjs +7 -0
- package/packs/ui-shadcn/files/tailwind.config.ts +57 -0
- package/packs/ui-shadcn/pack.toml +44 -0
- package/packs/ui-shadcn/skills/shadcn-dashboard-patterns/SKILL.md +85 -0
- package/packs/ui-shadcn/tasks.yaml +6 -0
- package/presets/docs-site.toml +4 -0
- package/presets/internal-tool.toml +4 -0
- package/presets/lean-saas.toml +4 -0
- package/presets/local-ai-mvp.toml +4 -0
- package/presets/saas-classic.toml +4 -0
- package/scripts/sync-skills.ts +223 -0
- package/src/paths.ts +22 -4
- package/templates/README.md +43 -0
- package/templates/astro/README.md +3 -0
- package/templates/astro/template.toml +4 -0
- package/templates/astro-starlight/README.md +3 -0
- package/templates/astro-starlight/template.toml +4 -0
- package/templates/nextjs/.ai/architecture.md +13 -0
- package/templates/nextjs/.ai/board.md +7 -0
- package/templates/nextjs/.ai/product-spec.md +11 -0
- package/templates/nextjs/.claude/skills/.gitkeep +0 -0
- package/templates/nextjs/.codex/skills/.gitkeep +0 -0
- package/templates/nextjs/AGENTS.md +95 -0
- package/templates/nextjs/CLAUDE.md +3 -0
- package/templates/nextjs/README.md +20 -0
- package/templates/nextjs/app/(app)/home/page.tsx +43 -0
- package/templates/nextjs/app/(app)/home/posts-panel.tsx +83 -0
- package/templates/nextjs/app/(app)/layout.tsx +12 -0
- package/templates/nextjs/app/(auth)/login/page.tsx +97 -0
- package/templates/nextjs/app/globals.css +23 -0
- package/templates/nextjs/app/layout.tsx +20 -0
- package/templates/nextjs/app/page.tsx +39 -0
- package/templates/nextjs/lib/auth-placeholder.ts +21 -0
- package/templates/nextjs/lib/posts-placeholder.ts +30 -0
- package/templates/nextjs/next.config.ts +5 -0
- package/templates/nextjs/package.json +26 -0
- package/templates/nextjs/postcss.config.mjs +7 -0
- package/templates/nextjs/spark.config.json +4 -0
- package/templates/nextjs/template.toml +4 -0
- package/templates/nextjs/tsconfig.json +27 -0
- package/templates/nextjs/types/post.ts +13 -0
- package/templates/one/README.md +5 -0
- package/templates/one/template.toml +4 -0
- package/templates/vite-react/README.md +3 -0
- package/templates/vite-react/template.toml +4 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineZeroSchema, number, string, table } from '@forgeailab/spark-sync-zero';
|
|
2
|
+
|
|
3
|
+
// Example schema. Replace with your app's tables.
|
|
4
|
+
const users = table('user')
|
|
5
|
+
.columns({
|
|
6
|
+
id: string(),
|
|
7
|
+
name: string(),
|
|
8
|
+
email: string(),
|
|
9
|
+
createdAt: number(),
|
|
10
|
+
})
|
|
11
|
+
.primaryKey('id');
|
|
12
|
+
|
|
13
|
+
export const { schema, zql } = defineZeroSchema({
|
|
14
|
+
tables: [users],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export type Schema = typeof schema;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type ZeroCacheConfig = {
|
|
2
|
+
upstreamDB: string;
|
|
3
|
+
authSecret: string;
|
|
4
|
+
queryURL: string;
|
|
5
|
+
mutateURL: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function getZeroCacheConfig(): ZeroCacheConfig {
|
|
9
|
+
const upstreamDB = process.env.ZERO_UPSTREAM_DB;
|
|
10
|
+
const authSecret = process.env.ZERO_AUTH_SECRET;
|
|
11
|
+
|
|
12
|
+
if (!upstreamDB) {
|
|
13
|
+
throw new Error("ZERO_UPSTREAM_DB is required to run zero-cache.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!authSecret) {
|
|
17
|
+
throw new Error("ZERO_AUTH_SECRET is required to authenticate Zero clients.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
upstreamDB,
|
|
22
|
+
authSecret,
|
|
23
|
+
queryURL: "http://localhost:3000/api/query",
|
|
24
|
+
mutateURL: "http://localhost:3000/api/mutate",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name = "sync-zero"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "infra"
|
|
4
|
+
description = "Rocicorp Zero client-first sync setup for a Postgres-backed app."
|
|
5
|
+
provides = ["sync"]
|
|
6
|
+
requires = ["db-pg"]
|
|
7
|
+
conflicts = ["sync"]
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = ["nextjs"]
|
|
10
|
+
|
|
11
|
+
[runtime_package]
|
|
12
|
+
package = "@forgeailab/spark-sync-zero"
|
|
13
|
+
version = "^0.1"
|
|
14
|
+
|
|
15
|
+
[dependencies]
|
|
16
|
+
runtime = ["@rocicorp/zero"]
|
|
17
|
+
|
|
18
|
+
[env]
|
|
19
|
+
required = ["ZERO_AUTH_SECRET", "ZERO_UPSTREAM_DB"]
|
|
20
|
+
optional = []
|
|
21
|
+
|
|
22
|
+
[[files]]
|
|
23
|
+
mode = "create"
|
|
24
|
+
from = "zero.config.ts"
|
|
25
|
+
to = "zero.config.ts"
|
|
26
|
+
|
|
27
|
+
[[files]]
|
|
28
|
+
mode = "create"
|
|
29
|
+
from = "lib/zero/schema.ts"
|
|
30
|
+
to = "lib/zero/schema.ts"
|
|
31
|
+
|
|
32
|
+
[[files]]
|
|
33
|
+
mode = "create"
|
|
34
|
+
from = "lib/zero/client.ts"
|
|
35
|
+
to = "lib/zero/client.ts"
|
|
36
|
+
|
|
37
|
+
[[files]]
|
|
38
|
+
mode = "create"
|
|
39
|
+
from = "components/ZeroProvider.tsx"
|
|
40
|
+
to = "components/ZeroProvider.tsx"
|
|
41
|
+
|
|
42
|
+
[[files]]
|
|
43
|
+
mode = "create-or-skip"
|
|
44
|
+
from = "docker-compose.yml"
|
|
45
|
+
to = "docker-compose.yml"
|
|
46
|
+
|
|
47
|
+
[[files]]
|
|
48
|
+
mode = "create"
|
|
49
|
+
from = "compose/zero-cache.yml"
|
|
50
|
+
to = "compose/zero-cache.yml"
|
|
51
|
+
|
|
52
|
+
[[files]]
|
|
53
|
+
mode = "append"
|
|
54
|
+
from = "docker-compose.include.yml"
|
|
55
|
+
to = "docker-compose.yml"
|
|
56
|
+
|
|
57
|
+
[skills]
|
|
58
|
+
copy = ["skills/zero-patterns"]
|
|
59
|
+
|
|
60
|
+
[tasks]
|
|
61
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zero-patterns
|
|
3
|
+
description: Use when implementing Rocicorp Zero client-first sync, schema changes, queries, mutators, or zero-cache setup in an spark project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Zero Patterns
|
|
7
|
+
|
|
8
|
+
## Core Model
|
|
9
|
+
|
|
10
|
+
Zero is client-first. Reads should feel local because the client queries its
|
|
11
|
+
local store first, then syncs with `zero-cache` and the upstream Postgres
|
|
12
|
+
database in the background.
|
|
13
|
+
|
|
14
|
+
Do not treat Zero as a REST wrapper. Keep query definitions and mutators as
|
|
15
|
+
the product contract. Components consume those contracts through Zero React
|
|
16
|
+
helpers instead of hand-building fetch calls for synced data.
|
|
17
|
+
|
|
18
|
+
## Before Coding
|
|
19
|
+
|
|
20
|
+
1. Check which `db` pack is installed.
|
|
21
|
+
2. Find the authoritative database schema and migrations.
|
|
22
|
+
3. Read `lib/zero/schema.ts`.
|
|
23
|
+
4. Identify the smallest set of rows the UI needs.
|
|
24
|
+
5. Confirm auth context before exposing user-scoped data.
|
|
25
|
+
|
|
26
|
+
## Schema Authoring
|
|
27
|
+
|
|
28
|
+
Zero schema mirrors the subset of Postgres that clients can query. Keep it
|
|
29
|
+
small, explicit, and aligned with the database.
|
|
30
|
+
|
|
31
|
+
Use table builders from `@rocicorp/zero`, for example `table`, `string`,
|
|
32
|
+
`boolean`, `number`, `json`, and `enumeration`.
|
|
33
|
+
|
|
34
|
+
Every table must have an explicit primary key. Prefer stable string IDs for
|
|
35
|
+
client-created records. Avoid auto-increment IDs for records created from the
|
|
36
|
+
client because mutators may run more than once.
|
|
37
|
+
|
|
38
|
+
When adding fields, use an expand deploy order: database first, then server
|
|
39
|
+
query or mutate code, then client usage. When removing fields, reverse that:
|
|
40
|
+
client stops using it, then server stops exposing it, then database removes it.
|
|
41
|
+
|
|
42
|
+
## Queries
|
|
43
|
+
|
|
44
|
+
Clients should call named query helpers, not arbitrary server endpoints. Keep
|
|
45
|
+
queries narrow enough that Zero can cache and update them efficiently.
|
|
46
|
+
|
|
47
|
+
## Mutators
|
|
48
|
+
|
|
49
|
+
Mutators are optimistic. They can run on the client and again on the server, so
|
|
50
|
+
they must be deterministic and safe to replay.
|
|
51
|
+
|
|
52
|
+
Generate IDs before calling the mutator and pass them as arguments. Do not
|
|
53
|
+
generate random IDs, timestamps, or external side effects inside a mutator.
|
|
54
|
+
|
|
55
|
+
Validate mutator arguments at the boundary. Keep authorization checks on the
|
|
56
|
+
server path, even if the client path also hides unauthorized actions.
|
|
57
|
+
|
|
58
|
+
## Local Development
|
|
59
|
+
|
|
60
|
+
`zero-cache` needs a Postgres upstream with logical replication enabled. Keep
|
|
61
|
+
`ZERO_UPSTREAM_DB` pointed at the same database your app server uses.
|
|
62
|
+
|
|
63
|
+
## Review Checklist
|
|
64
|
+
|
|
65
|
+
- Schema matches current migrations.
|
|
66
|
+
- Client-visible rows are scoped by query and auth context.
|
|
67
|
+
- Mutators are deterministic and replay-safe.
|
|
68
|
+
- Query shape is indexed or intentionally small.
|
|
69
|
+
- Local setup documents `ZERO_UPSTREAM_DB` and cache reset steps.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
epic: Sync
|
|
2
|
+
tasks:
|
|
3
|
+
- id: ZERO-001
|
|
4
|
+
title: Define initial schema in lib/zero/schema.ts
|
|
5
|
+
status: Clarifying
|
|
6
|
+
acceptance:
|
|
7
|
+
- The Zero schema matches the first database tables that need client-first reads.
|
|
8
|
+
- Every synced table has an explicit primary key and only includes fields needed by the client.
|
|
9
|
+
- Schema changes are deployed in Zero-safe expand or contract order.
|
|
10
|
+
- id: ZERO-002
|
|
11
|
+
title: Run zero-cache locally
|
|
12
|
+
status: Clarifying
|
|
13
|
+
acceptance:
|
|
14
|
+
- zero-cache starts with ZERO_UPSTREAM_DB pointed at a logical-replication-enabled Postgres database.
|
|
15
|
+
- Local app query and mutate endpoints are reachable from zero-cache.
|
|
16
|
+
- The development workflow documents how to reset the local SQLite replica.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
4
|
+
const baseURL = `http://127.0.0.1:${port}`;
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
testDir: "./e2e",
|
|
8
|
+
fullyParallel: true,
|
|
9
|
+
timeout: 30_000,
|
|
10
|
+
expect: {
|
|
11
|
+
timeout: 5_000,
|
|
12
|
+
},
|
|
13
|
+
reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list",
|
|
14
|
+
use: {
|
|
15
|
+
baseURL,
|
|
16
|
+
trace: "on-first-retry",
|
|
17
|
+
},
|
|
18
|
+
webServer: {
|
|
19
|
+
command: "bun dev",
|
|
20
|
+
url: baseURL,
|
|
21
|
+
reuseExistingServer: !process.env.CI,
|
|
22
|
+
timeout: 120_000,
|
|
23
|
+
env: {
|
|
24
|
+
PORT: String(port),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
projects: [
|
|
28
|
+
{
|
|
29
|
+
name: "chromium",
|
|
30
|
+
use: { ...devices["Desktop Chrome"] },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name = "testing-playwright"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "testing"
|
|
4
|
+
description = "Playwright end-to-end test setup for the Next.js scaffold."
|
|
5
|
+
provides = ["e2e"]
|
|
6
|
+
requires = []
|
|
7
|
+
conflicts = []
|
|
8
|
+
requires_runtime = ["server"]
|
|
9
|
+
compatible_scaffolds = ["nextjs"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
dev = ["@playwright/test"]
|
|
13
|
+
|
|
14
|
+
[[files]]
|
|
15
|
+
mode = "create"
|
|
16
|
+
from = "playwright.config.ts"
|
|
17
|
+
to = "playwright.config.ts"
|
|
18
|
+
|
|
19
|
+
[[files]]
|
|
20
|
+
mode = "create"
|
|
21
|
+
from = "e2e/example.spec.ts"
|
|
22
|
+
to = "e2e/example.spec.ts"
|
|
23
|
+
|
|
24
|
+
[tasks]
|
|
25
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
epic: Testing
|
|
2
|
+
|
|
3
|
+
tasks:
|
|
4
|
+
- id: E2E-001
|
|
5
|
+
title: Add a smoke test for the first user-facing flow
|
|
6
|
+
status: Clarifying
|
|
7
|
+
acceptance:
|
|
8
|
+
- The first user-facing flow has a Playwright smoke test.
|
|
9
|
+
- The test runs against the local dev server through Playwright config.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* # >>> spark:ui-shadcn >>> */
|
|
2
|
+
@layer base {
|
|
3
|
+
:root {
|
|
4
|
+
--background: 0 0% 100%;
|
|
5
|
+
--foreground: 222.2 84% 4.9%;
|
|
6
|
+
--card: 0 0% 100%;
|
|
7
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
8
|
+
--popover: 0 0% 100%;
|
|
9
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
10
|
+
--primary: 221.2 83.2% 53.3%;
|
|
11
|
+
--primary-foreground: 210 40% 98%;
|
|
12
|
+
--secondary: 210 40% 96.1%;
|
|
13
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
14
|
+
--muted: 210 40% 96.1%;
|
|
15
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
16
|
+
--accent: 210 40% 96.1%;
|
|
17
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
18
|
+
--destructive: 0 84.2% 60.2%;
|
|
19
|
+
--destructive-foreground: 210 40% 98%;
|
|
20
|
+
--border: 214.3 31.8% 91.4%;
|
|
21
|
+
--input: 214.3 31.8% 91.4%;
|
|
22
|
+
--ring: 221.2 83.2% 53.3%;
|
|
23
|
+
--radius: 0.5rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.dark {
|
|
27
|
+
--background: 222.2 84% 4.9%;
|
|
28
|
+
--foreground: 210 40% 98%;
|
|
29
|
+
--card: 222.2 84% 4.9%;
|
|
30
|
+
--card-foreground: 210 40% 98%;
|
|
31
|
+
--popover: 222.2 84% 4.9%;
|
|
32
|
+
--popover-foreground: 210 40% 98%;
|
|
33
|
+
--primary: 217.2 91.2% 59.8%;
|
|
34
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
35
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
36
|
+
--secondary-foreground: 210 40% 98%;
|
|
37
|
+
--muted: 217.2 32.6% 17.5%;
|
|
38
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
39
|
+
--accent: 217.2 32.6% 17.5%;
|
|
40
|
+
--accent-foreground: 210 40% 98%;
|
|
41
|
+
--destructive: 0 62.8% 30.6%;
|
|
42
|
+
--destructive-foreground: 210 40% 98%;
|
|
43
|
+
--border: 217.2 32.6% 17.5%;
|
|
44
|
+
--input: 217.2 32.6% 17.5%;
|
|
45
|
+
--ring: 224.3 76.3% 48%;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
* {
|
|
49
|
+
@apply border-border;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
body {
|
|
53
|
+
@apply bg-background text-foreground;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/* # <<< spark:ui-shadcn <<< */
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
'inline-flex h-10 items-center justify-center gap-2 whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
12
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
13
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
14
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
15
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
16
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
default: 'h-10 px-4 py-2',
|
|
20
|
+
sm: 'h-9 rounded-md px-3',
|
|
21
|
+
lg: 'h-11 rounded-md px-8',
|
|
22
|
+
icon: 'h-10 w-10',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
variant: 'default',
|
|
27
|
+
size: 'default',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export function Button({
|
|
33
|
+
className,
|
|
34
|
+
variant,
|
|
35
|
+
size,
|
|
36
|
+
asChild = false,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<'button'> &
|
|
39
|
+
VariantProps<typeof buttonVariants> & {
|
|
40
|
+
asChild?: boolean;
|
|
41
|
+
}) {
|
|
42
|
+
const Comp = asChild ? Slot : 'button';
|
|
43
|
+
|
|
44
|
+
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { buttonVariants };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
export function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
14
|
+
return <div className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
18
|
+
return (
|
|
19
|
+
<div className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
|
24
|
+
return <div className={cn('text-sm text-muted-foreground', className)} {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
28
|
+
return <div className={cn('p-6 pt-0', className)} {...props} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
32
|
+
return <div className={cn('flex items-center p-6 pt-0', className)} {...props} />;
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
darkMode: ['class'],
|
|
5
|
+
content: [
|
|
6
|
+
'./app/**/*.{ts,tsx,mdx}',
|
|
7
|
+
'./components/**/*.{ts,tsx,mdx}',
|
|
8
|
+
'./lib/**/*.{ts,tsx,mdx}',
|
|
9
|
+
],
|
|
10
|
+
theme: {
|
|
11
|
+
extend: {
|
|
12
|
+
borderRadius: {
|
|
13
|
+
lg: 'var(--radius)',
|
|
14
|
+
md: 'calc(var(--radius) - 2px)',
|
|
15
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
16
|
+
},
|
|
17
|
+
colors: {
|
|
18
|
+
background: 'hsl(var(--background))',
|
|
19
|
+
foreground: 'hsl(var(--foreground))',
|
|
20
|
+
card: {
|
|
21
|
+
DEFAULT: 'hsl(var(--card))',
|
|
22
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
23
|
+
},
|
|
24
|
+
popover: {
|
|
25
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
26
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
27
|
+
},
|
|
28
|
+
primary: {
|
|
29
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
30
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
31
|
+
},
|
|
32
|
+
secondary: {
|
|
33
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
34
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
35
|
+
},
|
|
36
|
+
muted: {
|
|
37
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
38
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
39
|
+
},
|
|
40
|
+
accent: {
|
|
41
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
42
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
43
|
+
},
|
|
44
|
+
destructive: {
|
|
45
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
46
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
47
|
+
},
|
|
48
|
+
border: 'hsl(var(--border))',
|
|
49
|
+
input: 'hsl(var(--input))',
|
|
50
|
+
ring: 'hsl(var(--ring))',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
plugins: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default config;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name = "ui-shadcn"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
category = "ui"
|
|
4
|
+
description = "Shadcn-style UI primitives for Next.js dashboards."
|
|
5
|
+
provides = ["ui-kit"]
|
|
6
|
+
requires = []
|
|
7
|
+
conflicts = ["ui-kit"]
|
|
8
|
+
requires_runtime = ["react"]
|
|
9
|
+
compatible_scaffolds = ["nextjs"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
runtime = [
|
|
13
|
+
"@radix-ui/react-slot",
|
|
14
|
+
"class-variance-authority",
|
|
15
|
+
"clsx",
|
|
16
|
+
"tailwind-merge",
|
|
17
|
+
"lucide-react",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[[files]]
|
|
21
|
+
mode = "create"
|
|
22
|
+
from = "lib/utils.ts"
|
|
23
|
+
to = "lib/utils.ts"
|
|
24
|
+
|
|
25
|
+
[[files]]
|
|
26
|
+
mode = "create"
|
|
27
|
+
from = "components/ui/button.tsx"
|
|
28
|
+
to = "components/ui/button.tsx"
|
|
29
|
+
|
|
30
|
+
[[files]]
|
|
31
|
+
mode = "create"
|
|
32
|
+
from = "components/ui/card.tsx"
|
|
33
|
+
to = "components/ui/card.tsx"
|
|
34
|
+
|
|
35
|
+
[[files]]
|
|
36
|
+
mode = "append"
|
|
37
|
+
from = "app/globals.css"
|
|
38
|
+
to = "app/globals.css"
|
|
39
|
+
|
|
40
|
+
[skills]
|
|
41
|
+
copy = ["skills/shadcn-dashboard-patterns"]
|
|
42
|
+
|
|
43
|
+
[tasks]
|
|
44
|
+
file = "tasks.yaml"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shadcn-dashboard-patterns
|
|
3
|
+
description: Compose shadcn-style primitives into dense, practical dashboard layouts without drifting into marketing UI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: shadcn-dashboard-patterns
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Build dashboard screens from small primitives that stay easy to scan, resize,
|
|
11
|
+
and extend. Use the shadcn component style as a starting point, not as a reason
|
|
12
|
+
to turn every section into a card.
|
|
13
|
+
|
|
14
|
+
## Defaults
|
|
15
|
+
|
|
16
|
+
- Start with the user's job, not the component catalog.
|
|
17
|
+
- Put navigation, filters, and primary actions where repeat users expect them.
|
|
18
|
+
- Prefer one clear page title, one toolbar, and one main content region.
|
|
19
|
+
- Use cards for repeated objects, stat summaries, forms, and modal content.
|
|
20
|
+
- Do not wrap the whole page in a decorative card.
|
|
21
|
+
- Keep radii at `rounded-lg` or below unless the app theme says otherwise.
|
|
22
|
+
- Use `cn()` for class merging and keep variants close to the component.
|
|
23
|
+
|
|
24
|
+
## Layout
|
|
25
|
+
|
|
26
|
+
- Use a shell with a sidebar or top nav only when there are real destinations.
|
|
27
|
+
- Keep dashboard width constrained with `mx-auto w-full max-w-7xl px-4`.
|
|
28
|
+
- Use `gap-4` or `gap-6`; avoid ornamental whitespace in operational views.
|
|
29
|
+
- Put status, owner, date, and filters near the table or list they affect.
|
|
30
|
+
- Align destructive actions away from the primary happy path.
|
|
31
|
+
- Keep tables full width and let dense data breathe through row height.
|
|
32
|
+
- Use sticky headers only when the table is long enough to justify them.
|
|
33
|
+
|
|
34
|
+
## Cards
|
|
35
|
+
|
|
36
|
+
- A card should represent one thing: a metric, a record, a form, or a chart.
|
|
37
|
+
- Do not nest cards inside cards.
|
|
38
|
+
- Use `CardHeader`, `CardContent`, and `CardFooter` only when each region helps.
|
|
39
|
+
- Small metric cards should use compact labels, one strong value, and a trend.
|
|
40
|
+
- Empty cards should show the empty state for that object, not generic help copy.
|
|
41
|
+
- Repeated cards need stable height or clear wrapping behavior.
|
|
42
|
+
|
|
43
|
+
## Buttons
|
|
44
|
+
|
|
45
|
+
- Primary buttons create or commit work.
|
|
46
|
+
- Secondary buttons reveal adjacent options.
|
|
47
|
+
- Ghost buttons belong in rows, toolbars, and low-emphasis actions.
|
|
48
|
+
- Icon buttons need an accessible label.
|
|
49
|
+
- Use `asChild` when routing through `Link`, not nested buttons or anchors.
|
|
50
|
+
- Keep button labels short and concrete: `Save`, `Invite`, `Export`.
|
|
51
|
+
- Avoid full-width buttons on desktop unless the panel itself is narrow.
|
|
52
|
+
|
|
53
|
+
## Forms
|
|
54
|
+
|
|
55
|
+
- Group fields by workflow, not by database table.
|
|
56
|
+
- Put validation text next to the field that caused it.
|
|
57
|
+
- Use disabled and pending states during server actions.
|
|
58
|
+
- Use destructive copy only for irreversible actions.
|
|
59
|
+
- Keep advanced settings collapsed until they are needed.
|
|
60
|
+
- Use consistent input widths inside the same form group.
|
|
61
|
+
|
|
62
|
+
## Tables And Lists
|
|
63
|
+
|
|
64
|
+
- Use a table when users compare many rows across the same fields.
|
|
65
|
+
- Use a list when records have mixed metadata and short actions.
|
|
66
|
+
- Keep row actions at the trailing edge.
|
|
67
|
+
- Expose bulk actions only after selection exists.
|
|
68
|
+
- Use badges sparingly for status, plan, role, or risk.
|
|
69
|
+
- Do not use color as the only status signal.
|
|
70
|
+
|
|
71
|
+
## Empty, Loading, Error
|
|
72
|
+
|
|
73
|
+
- Empty states should offer the next valid action.
|
|
74
|
+
- Loading states should preserve the final layout shape.
|
|
75
|
+
- Error states should say what failed and offer retry or recovery.
|
|
76
|
+
- Avoid giant empty illustrations inside work dashboards.
|
|
77
|
+
- Do not teach the whole product in an empty state.
|
|
78
|
+
|
|
79
|
+
## Motion And Polish
|
|
80
|
+
|
|
81
|
+
- Prefer subtle color, border, and shadow changes over large motion.
|
|
82
|
+
- Keep hover states predictable across cards, rows, and buttons.
|
|
83
|
+
- Respect reduced motion when adding transitions.
|
|
84
|
+
- Check mobile wrapping for toolbar actions and long entity names.
|
|
85
|
+
- Make the first useful workflow visible without a landing page.
|