scale-stack 0.0.1-alpha.2 → 0.0.2

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 (49) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +67 -5
  3. package/dist/index.js +4081 -106
  4. package/package.json +7 -2
  5. package/templates/ai-chat/chat-panel.tsx.ejs +70 -0
  6. package/templates/ai-chat/layout.tsx.ejs +35 -0
  7. package/templates/ai-chat/page.tsx.ejs +37 -0
  8. package/templates/ai-chat/route.ts.ejs +68 -0
  9. package/templates/ai-chat/use-chat.ts.ejs +26 -0
  10. package/templates/analytics/analytics-provider.tsx.ejs +32 -0
  11. package/templates/analytics/analytics.ts.ejs +40 -0
  12. package/templates/auth/auth-client.ts.ejs +7 -0
  13. package/templates/auth/auth.ts.ejs +45 -0
  14. package/templates/auth/route.ts.ejs +4 -0
  15. package/templates/auth/sign-in-page.tsx.ejs +122 -0
  16. package/templates/auth/sign-up-page.tsx.ejs +137 -0
  17. package/templates/auth/unauthorized.tsx.ejs +28 -0
  18. package/templates/core/layout.tsx.ejs +46 -0
  19. package/templates/core/loading.tsx.ejs +7 -0
  20. package/templates/core/next.config.ts.ejs +33 -0
  21. package/templates/error-handling/catch-all-not-found-page.tsx.ejs +5 -0
  22. package/templates/error-handling/error.tsx.ejs +33 -0
  23. package/templates/error-handling/global-error.tsx.ejs +32 -0
  24. package/templates/error-handling/not-found.tsx.ejs +28 -0
  25. package/templates/eslint-prettier/.prettierignore.ejs +29 -0
  26. package/templates/eslint-prettier/eslint.config.mjs.ejs +31 -0
  27. package/templates/form-handling/dashboard-page.tsx.ejs +39 -0
  28. package/templates/form-handling/example-form-action.ts.ejs +50 -0
  29. package/templates/form-handling/example-form-schema.ts.ejs +87 -0
  30. package/templates/form-handling/example-form.tsx.ejs +428 -0
  31. package/templates/i18n/ar.json.ejs +77 -0
  32. package/templates/i18n/en.json.ejs +77 -0
  33. package/templates/i18n/locale-layout.tsx.ejs +81 -0
  34. package/templates/i18n/navigation.ts.ejs +5 -0
  35. package/templates/i18n/next-intl.d.ts.ejs +9 -0
  36. package/templates/i18n/request.ts.ejs +15 -0
  37. package/templates/i18n/routing.ts.ejs +7 -0
  38. package/templates/orm/prisma.config.ts.ejs +12 -0
  39. package/templates/orm/prisma.ts.ejs +17 -0
  40. package/templates/orm/schema.prisma.ejs +8 -0
  41. package/templates/pre-commit/prek.toml.ejs +35 -0
  42. package/templates/proxy/proxy.ts.ejs +81 -0
  43. package/templates/server-actions/safe-action.ts.ejs +51 -0
  44. package/templates/ui/client-side-wrappers.tsx.ejs +19 -0
  45. package/templates/ui/page.tsx.ejs +117 -0
  46. package/templates/utility-libs/date-fns.SKILL.md.ejs +34 -0
  47. package/templates/utility-libs/motion.SKILL.md.ejs +234 -0
  48. package/templates/utility-libs/ts-pattern.SKILL.md.ejs +44 -0
  49. package/templates/utility-libs/usehooks.SKILL.md.ejs +38 -0
@@ -0,0 +1,15 @@
1
+ import { getRequestConfig } from "next-intl/server";
2
+ import { hasLocale } from "next-intl";
3
+ import { routing } from "./routing";
4
+
5
+ export default getRequestConfig(async ({ requestLocale }) => {
6
+ const requested = await requestLocale;
7
+ const locale = hasLocale(routing.locales, requested)
8
+ ? requested
9
+ : routing.defaultLocale;
10
+
11
+ return {
12
+ locale,
13
+ messages: (await import(`../../messages/${locale}.json`)).default,
14
+ };
15
+ });
@@ -0,0 +1,7 @@
1
+ import { defineRouting } from "next-intl/routing";
2
+
3
+ export const routing = defineRouting({
4
+ locales: ["en", "ar"],
5
+ defaultLocale: "en",
6
+ localePrefix: "always",
7
+ });
@@ -0,0 +1,12 @@
1
+ import "dotenv/config";
2
+ import { defineConfig, env } from "prisma/config";
3
+
4
+ export default defineConfig({
5
+ schema: "prisma/schema.prisma",
6
+ migrations: {
7
+ path: "prisma/migrations",
8
+ },
9
+ datasource: {
10
+ url: env("DATABASE_URL"),
11
+ },
12
+ });
@@ -0,0 +1,17 @@
1
+ import { env } from "@/env";
2
+ import { PrismaPg } from "@prisma/adapter-pg";
3
+ import { PrismaClient } from "../../prisma/generated/client";
4
+
5
+ const globalForPrisma = globalThis as unknown as {
6
+ prisma?: PrismaClient;
7
+ };
8
+
9
+ export const prisma =
10
+ globalForPrisma.prisma ??
11
+ new PrismaClient({
12
+ adapter: new PrismaPg({ connectionString: env.DATABASE_URL }),
13
+ });
14
+
15
+ if (process.env.NODE_ENV !== "production") {
16
+ globalForPrisma.prisma = prisma;
17
+ }
@@ -0,0 +1,8 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ output = "./generated"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "postgresql"
8
+ }
@@ -0,0 +1,35 @@
1
+ # --- Built-in fast hooks (offline, zero setup) ---
2
+ [[repos]]
3
+ repo = "builtin"
4
+ hooks = [
5
+ { id = "trailing-whitespace" },
6
+ { id = "end-of-file-fixer" },
7
+ { id = "check-yaml" },
8
+ { id = "check-json" },
9
+ ]
10
+
11
+ # --- Local project hooks (use package.json scripts) ---
12
+ [[repos]]
13
+ repo = "local"
14
+
15
+ [[repos.hooks]]
16
+ id = "eslint"
17
+ name = "eslint"
18
+ language = "system"
19
+ entry = "pnpm lint"
20
+ files = { glob = "*.{ts,tsx,js,jsx,mjs}" }
21
+ pass_filenames = false
22
+
23
+ [[repos.hooks]]
24
+ id = "prettier"
25
+ name = "prettier"
26
+ language = "system"
27
+ entry = "pnpm format"
28
+ files = { glob = "*.{ts,tsx,js,jsx,json,css,md}" }
29
+
30
+ [[repos.hooks]]
31
+ id = "typecheck"
32
+ name = "typecheck"
33
+ language = "system"
34
+ entry = "pnpm typecheck"
35
+ pass_filenames = false
@@ -0,0 +1,81 @@
1
+ import type { NextRequest } from "next/server";
2
+ <% if (hasAuth || !hasI18n) { %>
3
+ import { NextResponse } from "next/server";
4
+ <% } %>
5
+ <% if (hasI18n) { %>import createMiddleware from "next-intl/middleware";
6
+ <% } %><% if (hasAuth) { %>import { getSessionCookie } from "better-auth/cookies";
7
+ <% } %><% if (hasI18n) { %>import { routing } from "./i18n/routing";
8
+ <% if (hasAuth) { %>import { getPathname } from "./i18n/navigation";
9
+ <% } %><% } %>
10
+ <% if (hasI18n) { %>const intlProxy = createMiddleware(routing);
11
+
12
+ <% } %><% if (hasAuth) { %>const PROTECTED_ROUTE_PREFIXES = ["/dashboard"] as const;
13
+
14
+ function normalizePathname(pathname: string): string {
15
+ <% if (hasI18n) { %> const [, maybeLocale, ...rest] = pathname.split("/");
16
+ const normalizedPathname = routing.locales.some(
17
+ (locale) => locale === maybeLocale,
18
+ )
19
+ ? `/${rest.join("/")}`
20
+ : pathname;
21
+
22
+ <% } else { %> const normalizedPathname = pathname;
23
+ <% } %> return normalizedPathname === "/"
24
+ ? "/"
25
+ : normalizedPathname.replace(/\/$/, "");
26
+ }
27
+
28
+ function isProtectedRoute(pathname: string): boolean {
29
+ const normalizedPathname = normalizePathname(pathname);
30
+
31
+ return PROTECTED_ROUTE_PREFIXES.some(
32
+ (prefix) =>
33
+ normalizedPathname === prefix ||
34
+ normalizedPathname.startsWith(`${prefix}/`),
35
+ );
36
+ }
37
+
38
+ function buildSignInUrl(request: NextRequest): URL {
39
+ const url = request.nextUrl.clone();
40
+ <% if (hasI18n) { %> const [, maybeLocale] = request.nextUrl.pathname.split("/");
41
+ const locale =
42
+ routing.locales.find((item) => item === maybeLocale) ??
43
+ routing.defaultLocale;
44
+
45
+ url.pathname = getPathname({ locale, href: "/sign-in" });
46
+ <% } else { %> url.pathname = "/sign-in";
47
+ <% } %> url.searchParams.set(
48
+ "callbackUrl",
49
+ `${request.nextUrl.pathname}${request.nextUrl.search}`,
50
+ );
51
+ return url;
52
+ }
53
+
54
+ function authProxy(request: NextRequest): NextResponse | null {
55
+ if (!isProtectedRoute(request.nextUrl.pathname)) {
56
+ return null;
57
+ }
58
+
59
+ // This proxy check is only for fast redirects; validate the session again
60
+ // before reading protected data in pages, route handlers, and actions.
61
+ if (getSessionCookie(request)) {
62
+ return null;
63
+ }
64
+
65
+ return NextResponse.redirect(buildSignInUrl(request));
66
+ }
67
+
68
+ <% } %>export default function proxy(<% if (hasAuth || hasI18n) { %>request<% } else { %>_request<% } %>: NextRequest) {
69
+ <% if (hasAuth) { %> const authResponse = authProxy(request);
70
+
71
+ if (authResponse) {
72
+ return authResponse;
73
+ }
74
+
75
+ <% } %><% if (hasI18n) { %> return intlProxy(request);
76
+ <% } else { %> return NextResponse.next();
77
+ <% } %>}
78
+
79
+ export const config = {
80
+ matcher: "/((?!api|trpc|_next|_vercel|.*\\..*).*)",
81
+ };
@@ -0,0 +1,51 @@
1
+ import { createSafeActionClient } from "next-safe-action";
2
+ <% if (hasAuth) { %>import { betterAuth } from "@next-safe-action/adapter-better-auth";
3
+ import { auth } from "@/lib/auth";
4
+ <% } %>import { z } from "zod";
5
+
6
+ const actionMetadataSchema = z.object({
7
+ actionName: z.string().min(1),
8
+ });
9
+
10
+ type ActionMetadata = z.infer<typeof actionMetadataSchema>;
11
+ type ActionOutcome = "success" | "error";
12
+
13
+ function logAction(
14
+ metadata: ActionMetadata | undefined,
15
+ durationMs: number,
16
+ outcome: ActionOutcome,
17
+ ): void {
18
+ console.info("[safe-action]", {
19
+ actionName: metadata?.actionName ?? "unknown",
20
+ durationMs,
21
+ outcome,
22
+ });
23
+ }
24
+ export const actionClient = createSafeActionClient({
25
+ defaultValidationErrorsShape: "flattened",
26
+ defineMetadataSchema() {
27
+ return actionMetadataSchema;
28
+ },
29
+ handleServerError(error, { metadata }) {
30
+ console.error("[safe-action] server error", {
31
+ actionName: metadata?.actionName ?? "unknown",
32
+ error,
33
+ });
34
+
35
+ return "Something went wrong. Please try again.";
36
+ },
37
+ }).use(async ({ next, metadata }) => {
38
+ const startedAt = performance.now();
39
+
40
+ try {
41
+ const result = await next();
42
+ logAction(metadata, Math.round(performance.now() - startedAt), "success");
43
+ return result;
44
+ } catch (error) {
45
+ logAction(metadata, Math.round(performance.now() - startedAt), "error");
46
+ throw error;
47
+ }
48
+ });
49
+ <% if (hasAuth) { %>
50
+ export const authActionClient = actionClient.use(betterAuth(auth));
51
+ <% } %>
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { NuqsAdapter } from "nuqs/adapters/next/app";
5
+ import { Toaster } from "@/components/ui/sonner";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ <% if (hasAnalyticsProvider) { %>import { AnalyticsProvider } from "./analytics-provider";
8
+ <% } %>
9
+
10
+ export function ClientSideWrappers({ children }: { children: ReactNode }) {
11
+ return (
12
+ <NuqsAdapter>
13
+ <TooltipProvider>
14
+ <% if (hasAnalyticsProvider) { %><AnalyticsProvider>{children}</AnalyticsProvider><% } else { %>{children}<% } %>
15
+ <Toaster />
16
+ </TooltipProvider>
17
+ </NuqsAdapter>
18
+ );
19
+ }
@@ -0,0 +1,117 @@
1
+ <% if (i18n) { %>import { getTranslations } from "next-intl/server";
2
+ import type { Locale } from "next-intl";
3
+ import { Link } from "@/i18n/navigation";
4
+ <% } else { %>import Link from "next/link";
5
+ <% } %>import { Button } from "@/components/ui/button";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+
14
+ <% if (i18n) { %>type HomeProps = Readonly<{
15
+ params: Promise<{ locale: Locale }>;
16
+ }>;
17
+
18
+ <% } %>export default <% if (i18n) { %>async <% } %>function Home(<% if (i18n) { %>{ params }: HomeProps<% } %>) {
19
+ <% if (i18n) { %> const { locale } = await params;
20
+ const t = await getTranslations({ locale, namespace: "Home" });
21
+
22
+ <% } %>
23
+ return (
24
+ <main className="relative flex flex-1 flex-col overflow-hidden px-6 py-16">
25
+ <div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top_left,var(--muted),transparent_34rem),radial-gradient(circle_at_bottom_right,var(--accent),transparent_28rem)]" />
26
+ <section className="mx-auto flex w-full max-w-6xl flex-col gap-10">
27
+ <div className="max-w-3xl space-y-6">
28
+ <p className="text-sm font-medium tracking-[0.2em] text-muted-foreground uppercase">
29
+ <% if (i18n) { %>{t("eyebrow")}<% } else { %>Scale Stack<% } %>
30
+ </p>
31
+ <div className="space-y-4">
32
+ <h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-balance sm:text-6xl">
33
+ <%= projectName %>
34
+ </h1>
35
+ <p className="max-w-2xl text-lg text-muted-foreground text-balance">
36
+ <% if (i18n) { %>{t("description")}<% } else { %>Your app is wired with shadcn/ui, Tailwind CSS 4, typed server
37
+ actions, form handling, and the Scale Stack guardrails agents
38
+ need to keep moving safely.<% } %>
39
+ </p>
40
+ </div>
41
+ </div>
42
+
43
+ <div className="space-y-3">
44
+ <div>
45
+ <h2 className="text-2xl font-semibold tracking-tight">
46
+ <% if (i18n) { %>{t("explorePages")}<% } else { %>Explore generated pages<% } %>
47
+ </h2>
48
+ <p className="text-muted-foreground">
49
+ <% if (i18n) { %>{t("explorePagesDescription")}<% } else { %>Each card links to a real route in this scaffold.<% } %>
50
+ </p>
51
+ </div>
52
+ <div className="grid gap-4 md:grid-cols-2<% if (hasAuth && hasAiChat) { %> xl:grid-cols-4<% } else { %> xl:grid-cols-3<% } %>">
53
+ <Card className="transition-colors hover:border-primary/50">
54
+ <CardHeader>
55
+ <CardTitle><% if (i18n) { %>{t("dashboard")}<% } else { %>Dashboard<% } %></CardTitle>
56
+ <CardDescription>
57
+ <% if (i18n) { %>{t("dashboardCardDescription")}<% } else { %>Try the generated React Hook Form example backed by Zod,
58
+ shadcn/ui fields, and next-safe-action.<% } %>
59
+ </CardDescription>
60
+ </CardHeader>
61
+ <CardContent>
62
+ <Button asChild variant="secondary">
63
+ <Link href="/dashboard"><% if (i18n) { %>{t("exploreForms")}<% } else { %>Explore forms<% } %></Link>
64
+ </Button>
65
+ </CardContent>
66
+ </Card>
67
+ <% if (hasAiChat) { %>
68
+ <Card className="transition-colors hover:border-primary/50">
69
+ <CardHeader>
70
+ <CardTitle><% if (i18n) { %>{t("aiChat")}<% } else { %>AI chat<% } %></CardTitle>
71
+ <CardDescription>
72
+ <% if (i18n) { %>{t("aiChatCardDescription")}<% } else { %>Open the provider-free AI Elements mock and see the AI SDK
73
+ transport contract ready for your backend.<% } %>
74
+ </CardDescription>
75
+ </CardHeader>
76
+ <CardContent>
77
+ <Button asChild variant="secondary">
78
+ <Link href="/chat"><% if (i18n) { %>{t("openChat")}<% } else { %>Open chat<% } %></Link>
79
+ </Button>
80
+ </CardContent>
81
+ </Card>
82
+ <% } %><% if (hasAuth) { %>
83
+ <Card className="transition-colors hover:border-primary/50">
84
+ <CardHeader>
85
+ <CardTitle><% if (i18n) { %>{t("signIn")}<% } else { %>Sign in<% } %></CardTitle>
86
+ <CardDescription>
87
+ <% if (i18n) { %>{t("<% if (isStatefulAuth) { %>signInStatefulCardDescription<% } else { %>signInCardDescription<% } %>")}<% } else { %>Exercise Better Auth with Microsoft Entra OAuth<% if (isStatefulAuth) { %>
88
+ and Prisma-backed email/password sessions<% } %>.<% } %>
89
+ </CardDescription>
90
+ </CardHeader>
91
+ <CardContent>
92
+ <Button asChild variant="secondary">
93
+ <Link href="/sign-in"><% if (i18n) { %>{t("signIn")}<% } else { %>Sign in<% } %></Link>
94
+ </Button>
95
+ </CardContent>
96
+ </Card>
97
+ <Card className="transition-colors hover:border-primary/50">
98
+ <CardHeader>
99
+ <CardTitle><% if (i18n) { %>{t("signUp")}<% } else { %>Sign up<% } %></CardTitle>
100
+ <CardDescription>
101
+ <% if (i18n) { %>{t("signUpCardDescription")}<% } else { %>Create an account through the same Better Auth setup and keep
102
+ the onboarding route ready for customization.<% } %>
103
+ </CardDescription>
104
+ </CardHeader>
105
+ <CardContent>
106
+ <Button asChild variant="secondary">
107
+ <Link href="/sign-up"><% if (i18n) { %>{t("signUp")}<% } else { %>Sign up<% } %></Link>
108
+ </Button>
109
+ </CardContent>
110
+ </Card>
111
+ <% } %>
112
+ </div>
113
+ </div>
114
+ </section>
115
+ </main>
116
+ );
117
+ }
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: date-fns
3
+ description: Use date-fns for date parsing, formatting, comparison, and arithmetic in this project. Use when working with dates, durations, calendars, relative time, or locale-aware date formatting.
4
+ ---
5
+
6
+ # date-fns
7
+
8
+ ## Instructions
9
+
10
+ - Use focused function imports from `date-fns`, for example `import { format, parseISO } from "date-fns"`.
11
+ - Keep parsing explicit. Use `parseISO` for ISO strings instead of relying on ambiguous `Date` parsing.
12
+ - Treat `Date` values as values passed between helpers. Do not mutate dates in place.
13
+ - Prefer date-fns helpers for comparison and arithmetic instead of manual millisecond math.
14
+ - Use locale-aware formatting when user-facing dates need localization.
15
+
16
+ ## Common Patterns
17
+
18
+ ```ts
19
+ import { format, isAfter, parseISO } from "date-fns";
20
+
21
+ const dueDate = parseISO(task.dueAt);
22
+ const isOverdue = isAfter(new Date(), dueDate);
23
+ const label = format(dueDate, "PPP");
24
+ ```
25
+
26
+ For business rules, keep date operations near the domain logic and name the intent:
27
+
28
+ ```ts
29
+ import { addDays, startOfDay } from "date-fns";
30
+
31
+ export function trialEndsAt(startedAt: Date): Date {
32
+ return startOfDay(addDays(startedAt, 14));
33
+ }
34
+ ```
@@ -0,0 +1,234 @@
1
+ ---
2
+ name: motion
3
+ description: Use Motion for React animations in this project. Use when adding or changing React animations, gestures, layout animations, page transitions, scroll effects, or exit animations with the `motion` package.
4
+ ---
5
+
6
+ # Motion
7
+
8
+ ## When To Use Motion
9
+
10
+ Use Motion when the UI needs React-aware animation:
11
+
12
+ - State-driven animations linked to component props or state.
13
+ - Cross-device gestures such as hover, tap, focus, drag, and in-view effects.
14
+ - Enter and exit animations for conditionally rendered UI.
15
+ - Layout animations, shared element transitions, reorderable lists, or expanding panels.
16
+ - Coordinated parent/child sequences, staggered reveals, or keyframes.
17
+ - Scroll-triggered and scroll-linked animation.
18
+ - SVG path, shape, or attribute animation.
19
+
20
+ Use CSS transitions instead for a simple, isolated effect like a color change on hover.
21
+
22
+ ## Project Rules
23
+
24
+ - Import React APIs from `motion/react`.
25
+ - In the Next.js App Router, put interactive Motion usage in Client Components. Add `"use client"` when using Motion hooks, gestures, state, event handlers, or browser-only APIs.
26
+ - Prefer transforms and opacity for smooth animation. Use `layout` for layout changes instead of manually animating layout properties.
27
+ - Keep animation targets readable and typed. Extract repeated variants/transitions into local constants near the component that owns them.
28
+ - Respect reduced motion. Replace large transform, parallax, and autoplaying motion with opacity or static alternatives.
29
+ - Avoid adding another animation library unless Motion cannot solve the requirement.
30
+
31
+ ## Import Guide
32
+
33
+ ```tsx
34
+ import {
35
+ AnimatePresence,
36
+ MotionConfig,
37
+ motion,
38
+ stagger,
39
+ useAnimate,
40
+ useReducedMotion,
41
+ useScroll,
42
+ useTransform,
43
+ } from "motion/react";
44
+ ```
45
+
46
+ Use only what you need so the package can stay tree-shakable.
47
+
48
+ ## Decision Guide
49
+
50
+ - One element changes with state: use `animate`, `initial`, and `transition`.
51
+ - Hover/tap/focus/drag/in-view behavior: use `whileHover`, `whileTap`, `whileFocus`, `whileDrag`, or `whileInView`.
52
+ - Element leaves the React tree: wrap the conditional region in `AnimatePresence` and define `exit`.
53
+ - Parent and children need coordination: use `variants`, `stagger`, `delayChildren`, and `when`.
54
+ - Element size or position changes: use `layout`; use `layoutId` for shared element transitions.
55
+ - Animation must run from an event or sequence outside render state: use `useAnimate`.
56
+ - Animation follows scroll: use `useScroll` and `useTransform`, with a reduced-motion fallback.
57
+
58
+ ## Patterns
59
+
60
+ ### Microinteraction
61
+
62
+ ```tsx
63
+ "use client";
64
+
65
+ import { motion } from "motion/react";
66
+
67
+ export function SaveButton() {
68
+ return (
69
+ <motion.button
70
+ whileHover={{ scale: 1.03 }}
71
+ whileTap={{ scale: 0.97 }}
72
+ transition={{ type: "spring", stiffness: 500, damping: 30 }}
73
+ >
74
+ Save
75
+ </motion.button>
76
+ );
77
+ }
78
+ ```
79
+
80
+ ### Enter And Exit
81
+
82
+ Use `AnimatePresence` when elements leave the React tree. Give exiting children stable keys.
83
+
84
+ ```tsx
85
+ "use client";
86
+
87
+ import { AnimatePresence, motion } from "motion/react";
88
+
89
+ export function Toast({ message }: { message?: string }) {
90
+ return (
91
+ <AnimatePresence>
92
+ {message ? (
93
+ <motion.div
94
+ key={message}
95
+ initial={{ opacity: 0, y: 8 }}
96
+ animate={{ opacity: 1, y: 0 }}
97
+ exit={{ opacity: 0, y: -8 }}
98
+ >
99
+ {message}
100
+ </motion.div>
101
+ ) : null}
102
+ </AnimatePresence>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ### Staggered Lists
108
+
109
+ Use variants when child animations should be coordinated by the parent.
110
+
111
+ ```tsx
112
+ "use client";
113
+
114
+ import { motion, stagger } from "motion/react";
115
+
116
+ const list = {
117
+ hidden: { opacity: 0 },
118
+ visible: {
119
+ opacity: 1,
120
+ transition: { delayChildren: stagger(0.06), when: "beforeChildren" },
121
+ },
122
+ };
123
+
124
+ const item = {
125
+ hidden: { opacity: 0, y: 8 },
126
+ visible: { opacity: 1, y: 0 },
127
+ };
128
+
129
+ export function ResultList({ results }: { results: string[] }) {
130
+ return (
131
+ <motion.ul initial="hidden" animate="visible" variants={list}>
132
+ {results.map((result) => (
133
+ <motion.li key={result} variants={item}>
134
+ {result}
135
+ </motion.li>
136
+ ))}
137
+ </motion.ul>
138
+ );
139
+ }
140
+ ```
141
+
142
+ ### Layout Animation
143
+
144
+ Use `layout` for size and position changes. Use `layoutId` only for intentional shared-element transitions.
145
+
146
+ ```tsx
147
+ "use client";
148
+
149
+ import { motion } from "motion/react";
150
+
151
+ export function ExpandingCard({ expanded }: { expanded: boolean }) {
152
+ return (
153
+ <motion.article layout className={expanded ? "col-span-2" : undefined}>
154
+ <motion.h2 layout="position">Details</motion.h2>
155
+ </motion.article>
156
+ );
157
+ }
158
+ ```
159
+
160
+ ### Scroll-Linked Animation
161
+
162
+ Use Motion values for scroll-linked transforms, and disable physical movement for reduced-motion users.
163
+
164
+ ```tsx
165
+ "use client";
166
+
167
+ import {
168
+ motion,
169
+ useReducedMotion,
170
+ useScroll,
171
+ useTransform,
172
+ } from "motion/react";
173
+
174
+ export function ReadingProgress() {
175
+ const shouldReduceMotion = useReducedMotion();
176
+ const { scrollYProgress } = useScroll();
177
+ const scaleX = useTransform(scrollYProgress, [0, 1], [0, 1]);
178
+
179
+ return (
180
+ <motion.div
181
+ aria-hidden="true"
182
+ style={{ scaleX: shouldReduceMotion ? 1 : scaleX }}
183
+ className="fixed inset-x-0 top-0 h-1 origin-left"
184
+ />
185
+ );
186
+ }
187
+ ```
188
+
189
+ ### Imperative Sequence
190
+
191
+ Reach for `useAnimate` only when declarative props or variants are not enough.
192
+
193
+ ```tsx
194
+ "use client";
195
+
196
+ import { useEffect } from "react";
197
+ import { useAnimate } from "motion/react";
198
+
199
+ export function IntroSequence() {
200
+ const [scope, animate] = useAnimate();
201
+
202
+ useEffect(() => {
203
+ const controls = animate([
204
+ ["h1", { opacity: [0, 1], y: [12, 0] }],
205
+ ["p", { opacity: [0, 1] }, { at: "-0.1" }],
206
+ ]);
207
+
208
+ return () => controls.stop();
209
+ }, [animate]);
210
+
211
+ return (
212
+ <section ref={scope}>
213
+ <h1>Welcome</h1>
214
+ <p>Everything is ready.</p>
215
+ </section>
216
+ );
217
+ }
218
+ ```
219
+
220
+ ## Accessibility Checklist
221
+
222
+ - Use `MotionConfig reducedMotion="user"` at an app or feature boundary when broad reduced-motion handling is appropriate.
223
+ - Use `useReducedMotion()` for bespoke components.
224
+ - Replace large `x`, `y`, `scale`, rotation, and parallax effects with opacity or static states when reduced motion is enabled.
225
+ - Do not autoplay decorative videos or looping motion for reduced-motion users.
226
+ - Keep focus states visible; do not replace focus indicators with motion-only feedback.
227
+
228
+ ## Performance Checklist
229
+
230
+ - Prefer `opacity` and transforms (`x`, `y`, `scale`, `rotate`) for frequent animations.
231
+ - Avoid animating CSS variables for high-frequency motion because it can trigger paint; prefer Motion values for dynamic transforms.
232
+ - Use `layout` intentionally and sparingly in large lists.
233
+ - Use keyframes for short expressive sequences; keep long-running or repeating animations subtle.
234
+ - Use `initial={false}` when an entrance animation would cause distracting first paint or hydration movement.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: ts-pattern
3
+ description: Use ts-pattern for exhaustive pattern matching in TypeScript. Use when handling discriminated unions, complex branching, nested data shapes, or replacing fragile switch/if chains.
4
+ ---
5
+
6
+ # ts-pattern
7
+
8
+ ## Instructions
9
+
10
+ - Import `match` and patterns from `ts-pattern`: `import { match, P } from "ts-pattern"`.
11
+ - Prefer `match(value).with(...).exhaustive()` for closed discriminated unions so TypeScript verifies every case.
12
+ - Keep union tags explicit and stable, such as `{ type: "success" }` or `{ status: "idle" }`.
13
+ - Use `P` helpers for nested structures, guards, optionals, and wildcard cases when they improve readability.
14
+ - Avoid using `.otherwise()` for closed unions unless there is a deliberate unknown fallback. Prefer `.exhaustive()`.
15
+
16
+ ## Common Patterns
17
+
18
+ ```ts
19
+ import { match } from "ts-pattern";
20
+
21
+ type Result =
22
+ | { type: "success"; value: string }
23
+ | { type: "error"; message: string }
24
+ | { type: "loading" };
25
+
26
+ export function labelFor(result: Result): string {
27
+ return match(result)
28
+ .with({ type: "success" }, ({ value }) => value)
29
+ .with({ type: "error" }, ({ message }) => message)
30
+ .with({ type: "loading" }, () => "Loading...")
31
+ .exhaustive();
32
+ }
33
+ ```
34
+
35
+ Use patterns to make nested branching readable:
36
+
37
+ ```ts
38
+ import { match, P } from "ts-pattern";
39
+
40
+ const canEdit = match(user)
41
+ .with({ role: "admin" }, () => true)
42
+ .with({ role: "member", permissions: P.array("write") }, () => true)
43
+ .otherwise(() => false);
44
+ ```