Compare commits
102
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b986bc28b6 | ||
|
|
ac6c6a4061 | ||
|
|
dbf6cfbc12 | ||
|
|
02b4437eb3 | ||
|
|
b1238d4363 | ||
|
|
94a0328cad | ||
|
|
72a49fc2d0 | ||
|
|
e6cbdcc51d | ||
|
|
9a9880d865 | ||
|
|
960eaabe42 | ||
|
|
43dad88aea | ||
|
|
5fe2746879 | ||
|
|
e288be64da | ||
|
|
b4a72b7154 | ||
|
|
32b561a96b | ||
|
|
e75444ba52 | ||
|
|
6270c2701d | ||
|
|
db07b5ae0c | ||
|
|
6f4a021785 | ||
|
|
142a814039 | ||
|
|
5df8306909 | ||
|
|
b0098c9270 | ||
|
|
29cec47928 | ||
|
|
47da9bb340 | ||
|
|
e5d3a319a8 | ||
|
|
f5b4f6e2d0 | ||
|
|
57f77a3373 | ||
|
|
dae0baea3b | ||
|
|
3ce2b9662c | ||
|
|
c5a5611e10 | ||
|
|
c4ff57876a | ||
|
|
2e82223689 | ||
|
|
738f533d4c | ||
|
|
b1045c1dd2 | ||
|
|
7df7575a05 | ||
|
|
0b4a05e3d6 | ||
|
|
c09f187465 | ||
|
|
f5a05ef4a1 | ||
|
|
0810ff1373 | ||
|
|
fb91f3df90 | ||
|
|
c9abfc4815 | ||
|
|
40b6fd3cfc | ||
|
|
8a83096ce6 | ||
|
|
be8201b60a | ||
|
|
e87d41e08a | ||
|
|
57a0891224 | ||
|
|
0bd9e49964 | ||
|
|
186c8b172b | ||
|
|
456fc31770 | ||
|
|
145c79c8b2 | ||
|
|
0b634a0a2a | ||
|
|
a2f9bc0c35 | ||
|
|
9abbe01a7b | ||
|
|
72c4fce25e | ||
|
|
d52604971d | ||
|
|
66c8b90d79 | ||
|
|
978db63a95 | ||
|
|
dcc6fc2c0a | ||
|
|
29b9a6618c | ||
|
|
caf9f4dd5e | ||
|
|
a6a9d1e236 | ||
|
|
9a265a760d | ||
|
|
57cdb609b8 | ||
|
|
5b51b2f6e9 | ||
|
|
bd4581ea2b | ||
|
|
5f9dd1a5c4 | ||
|
|
f7e621ef66 | ||
|
|
2e19b72ce2 | ||
|
|
3f1b11496e | ||
|
|
8a46d734a9 | ||
|
|
943126f6b3 | ||
|
|
d13b984ac4 | ||
|
|
78ed092e1c | ||
|
|
692d54bab1 | ||
|
|
bf75d9deef | ||
|
|
bc64b74a34 | ||
|
|
e057dd50c8 | ||
|
|
d471418c8f | ||
|
|
2bf524ff82 | ||
|
|
2afe84e608 | ||
|
|
c17aaeb9f1 | ||
|
|
cf3c6324c8 | ||
|
|
587c7137c5 | ||
|
|
79b53addf4 | ||
|
|
74ebd6bc10 | ||
|
|
d04b3d2b22 | ||
|
|
2c5b865c66 | ||
|
|
ea02e5d358 | ||
|
|
7138e4a222 | ||
|
|
a70e08ac33 | ||
|
|
a93cecd0ee | ||
|
|
31ba33ee4d | ||
|
|
5b878d9688 | ||
|
|
99718c8698 | ||
|
|
31821f84cb | ||
|
|
afeca88f80 | ||
|
|
9ee81d06a2 | ||
|
|
582d1901f6 | ||
|
|
c5b96779aa | ||
|
|
d54aa7e2c2 | ||
|
|
6c6330cd7a | ||
|
|
e288f659c2 |
No files matched your search
@@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
build
|
||||
.git
|
||||
.env*
|
||||
!.env.example
|
||||
coverage
|
||||
test-results
|
||||
playwright-report
|
||||
.planning
|
||||
*.log
|
||||
.husky
|
||||
next-env.d.ts
|
||||
@@ -4,6 +4,9 @@ BETTER_AUTH_SECRET=
|
||||
# Full absolute URL of the deployed app, e.g. https://example.com
|
||||
BETTER_AUTH_URL=
|
||||
|
||||
# Public auth URL for the browser client (must match BETTER_AUTH_URL in prod)
|
||||
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# Optional: database URL for better-auth (Kysely/Drizzle adapter)
|
||||
# DATABASE_URL=postgres://user:pass@localhost:5432/db
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Dependabot Auto-Merge
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Verify CI check-runs before enabling auto-merge
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const sha = context.payload.pull_request.head.sha;
|
||||
|
||||
// Wait for in-progress runs (with timeout), then collect conclusions
|
||||
const requiredContext = "lint + typecheck + test + build";
|
||||
let conclusion = null;
|
||||
for (let attempt = 0; attempt < 60; attempt++) {
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
owner, repo, ref: sha,
|
||||
});
|
||||
const match = checks.check_runs.find(
|
||||
(r) => r.name === requiredContext || r.output?.title === requiredContext
|
||||
);
|
||||
if (match && match.status === "completed") {
|
||||
conclusion = match.conclusion;
|
||||
break;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 10000));
|
||||
}
|
||||
if (!conclusion) {
|
||||
core.setFailed(`Required check "${requiredContext}" never completed within timeout.`);
|
||||
return;
|
||||
}
|
||||
if (conclusion !== "success") {
|
||||
core.setFailed(`Required check "${requiredContext}" concluded ${conclusion}. NOT merging.`);
|
||||
return;
|
||||
}
|
||||
core.info(`Check "${requiredContext}" passed — safe to enable auto-merge.`);
|
||||
|
||||
- name: Enable auto-merge for Dependabot PR
|
||||
if: success()
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,57 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "dependencies"
|
||||
groups:
|
||||
production:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
development:
|
||||
dependency-type: "development"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
ignore:
|
||||
# ---- Toolchain pins: semver-major bumps break the lint/typecheck stack ----
|
||||
# ESLint 10 dropped rule-context getFilename(); eslint-plugin-react (^7.37.x)
|
||||
# peer range caps at eslint ^9.7 -> react/display-name crash at load.
|
||||
- dependency-name: "eslint"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# typescript-eslint 8.x peer range is ">=4.8.4 <6.1.0" -> TS 7 hard-refuses.
|
||||
- dependency-name: "typescript"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# @tsparticles v4 changed its exported API; the vendored SparklesCore from the
|
||||
# Aceternity registry targets the v3 API (initParticlesEngine, IEffect.fill...).
|
||||
# NOTE: dependabot `ignore` does NOT support wildcards (only `groups` does) —
|
||||
# list each exact scoped package.
|
||||
- dependency-name: "@tsparticles/react"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "@tsparticles/engine"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "@tsparticles/slim"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# eslint-config-next majors follow Next.js majors and can pull incompatible
|
||||
# plugin versions; framework majors are deliberate, not auto-bumps.
|
||||
- dependency-name: "eslint-config-next"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# eslint-plugin-react 8.x targets ESLint 10, which conflicts with the
|
||||
# pinned eslint 9 (and the vendored components' validated toolchain).
|
||||
- dependency-name: "eslint-plugin-react"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
actions:
|
||||
patterns: ["*"]
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
@@ -0,0 +1,42 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
name: lint + typecheck + test + build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.14"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Unit tests
|
||||
run: bun run test
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: Format check
|
||||
run: bun run format:check
|
||||
@@ -48,3 +48,6 @@ test-results
|
||||
playwright-report
|
||||
blob-report
|
||||
.playwright
|
||||
|
||||
# planning files
|
||||
.planning
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# syntax = docker/dockerfile:1
|
||||
|
||||
FROM oven/bun:1.3.14 AS base
|
||||
WORKDIR /app
|
||||
|
||||
# --- deps ---
|
||||
FROM base AS deps
|
||||
COPY package.json bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# --- builder ---
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN bun run build
|
||||
|
||||
# --- runner ---
|
||||
FROM base AS runner
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -1,36 +1,121 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# Next.js Template
|
||||
|
||||
## Getting Started
|
||||
Production-ready Next.js 16 (App Router, Turbopack) starter on Bun, with
|
||||
shadcn/ui Base UI components, TypeScript strict, and a zero-warning
|
||||
lint/typecheck/test/build pipeline.
|
||||
|
||||
First, run the development server:
|
||||
## Stack
|
||||
|
||||
- **Framework:** Next.js 16.3 (App Router, Turbopack, standalone output)
|
||||
- **Runtime:** Bun 1.3.14
|
||||
- **UI:** shadcn/ui Base-Nova (Base UI) + Tailwind CSS v4, lucide-react, motion
|
||||
- **Data:** @tanstack/react-query, @tanstack/react-table
|
||||
- **Forms:** react-hook-form + zod (v4) + @hookform/resolvers
|
||||
- **Auth:** better-auth (memory adapter, swap to Drizzle/Postgres when ready)
|
||||
- **API client:** openapi-fetch + openapi-typescript, ky
|
||||
- **Markdown:** react-markdown + remark-gfm + shiki (server-side highlighting)
|
||||
- **Misc:** zustand, nuqs, sonner, cmdk, vaul, embla-carousel-react, date-fns,
|
||||
next-themes, recharts
|
||||
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
bun install
|
||||
cp .env.example .env # optional: set BETTER_AUTH_SECRET / BETTER_AUTH_URL
|
||||
bun run dev # http://localhost:3000
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
Requires Node ≥ 18 (Bun runtime) — see `packageManager` in package.json.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
## Scripts
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
| Script | Description |
|
||||
| --------------------------------- | ------------------------------------------------------------------- |
|
||||
| `bun run dev` | Start dev server (Turbopack, NODE_ENV pinned to development) |
|
||||
| `bun run build` | Production build (standalone output for Docker) |
|
||||
| `bun run start` | Serve the production build |
|
||||
| `bun run lint` | ESLint (next core-web-vitals + typescript) |
|
||||
| `bun run lint:fix` | ESLint with autofix |
|
||||
| `bun run typecheck` | `tsc --noEmit` (strict + noUnusedLocals/Parameters) |
|
||||
| `bun run test` | Vitest unit tests (jsdom + jest-dom) |
|
||||
| `bun run test:watch` | Vitest watch mode |
|
||||
| `bun run test:coverage` | Vitest with v8 coverage report |
|
||||
| `bun run test:e2e` | Playwright E2E (Chromium, dedicated :3100) |
|
||||
| `bun run format` / `format:check` | Prettier write / check (Tailwind v4 plugin) |
|
||||
| `bun run api:generate` | Regenerate `types/api.ts` from `/api/openapi` (dev server must run) |
|
||||
| `bun run prepare` | Install husky hooks |
|
||||
|
||||
## Learn More
|
||||
Git hooks (husky + lint-staged) run ESLint + Prettier on staged files.
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
## Project structure
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
```
|
||||
app/
|
||||
page.tsx # landing page
|
||||
demo/ # wiring example: markdown+shiki, form, table, session
|
||||
protected/ # route guarded by middleware (better-auth session)
|
||||
error.tsx / loading.tsx / not-found.tsx
|
||||
api/
|
||||
health/ # GET health JSON
|
||||
openapi/ # OpenAPI 3.1 spec for codegen
|
||||
auth/[...all]/ # better-auth handler
|
||||
items/ # GET list / POST create (zod-validated)
|
||||
components/
|
||||
providers.tsx # QueryClient + Theme + Nuqs + Tooltip + Sonner
|
||||
ui/ # shadcn Base-Nova components (CLI-managed)
|
||||
demo/ # form + table wiring examples
|
||||
hooks/ # use-mobile (useSyncExternalStore)
|
||||
lib/
|
||||
auth.ts # better-auth server (memory adapter)
|
||||
auth-client.ts # better-auth react client
|
||||
api.ts # typed openapi-fetch client
|
||||
http.ts # ky wrapper
|
||||
query-client.ts # react-query client (per-request on server)
|
||||
env.ts # zod-validated env (no import-time throw)
|
||||
types/api.ts # generated openapi-typescript types
|
||||
middleware.ts # auth route guard
|
||||
e2e/ # Playwright specs
|
||||
.github/workflows/ci.yml
|
||||
Dockerfile # multi-stage standalone build
|
||||
```
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
## Auth
|
||||
|
||||
## Deploy on Vercel
|
||||
better-auth is wired but intentionally runs on the in-memory adapter so the
|
||||
template builds without a database.
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
1. `bunx @better-auth/cli secret` → put the result in `BETTER_AUTH_SECRET`.
|
||||
2. Set `BETTER_AUTH_URL` (+ `NEXT_PUBLIC_BETTER_AUTH_URL` for the client).
|
||||
3. To persist users, swap `memoryAdapter({})` in `lib/auth.ts` for a real
|
||||
adapter (e.g. `drizzleAdapter(db, { provider: "pg" })`) and add the DB URL.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
Routes under `/protected` require a session (see `middleware.ts`).
|
||||
|
||||
## OpenAPI + typed client
|
||||
|
||||
`app/api/openapi` serves an OpenAPI 3.1 document. Regenerate the typed client:
|
||||
|
||||
```bash
|
||||
bun run dev & # server must be up
|
||||
bun run api:generate # writes types/api.ts
|
||||
```
|
||||
|
||||
Then use `lib/api.ts` (`createClient<paths>` from openapi-fetch).
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
docker build -t nextjs-template .
|
||||
docker run -p 3000:3000 nextjs-template
|
||||
```
|
||||
|
||||
Multi-stage standalone image (~50MB runtime); runs as non-root user; requires
|
||||
NODE_ENV=production and the env vars above at runtime.
|
||||
|
||||
## License
|
||||
|
||||
Private template.
|
||||
|
||||
## Test DC format
|
||||
|
||||
Verifying Discord notification formatting.
|
||||
@@ -0,0 +1,4 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export function GET() {
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
service: "nextjs-template",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createItemSchema, MOCK_ITEMS, type Item, type CreateItemOutput } from "@/lib/items";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ items: MOCK_ITEMS });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = createItemSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Validation failed",
|
||||
details: parsed.error.flatten().fieldErrors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const input: CreateItemOutput = parsed.data;
|
||||
const item: Item = {
|
||||
id: crypto.randomUUID(),
|
||||
...input,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// In a real app: persist to DB here. MOCK_ITEMS is module-scoped, so the
|
||||
// created item lives only for the lifetime of the server process.
|
||||
MOCK_ITEMS.unshift(item);
|
||||
|
||||
return NextResponse.json({ item }, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Minimal OpenAPI document describing the template's own API routes.
|
||||
* Kept in sync with app/api/** — regenerate client types with:
|
||||
* bun run api:generate
|
||||
*/
|
||||
export function GET() {
|
||||
return NextResponse.json({
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "Next.js Template API",
|
||||
version: "0.1.0",
|
||||
},
|
||||
paths: {
|
||||
"/api/health": {
|
||||
get: {
|
||||
operationId: "getHealth",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Service health",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
ok: { type: "boolean" },
|
||||
service: { type: "string" },
|
||||
timestamp: { type: "string", format: "date-time" },
|
||||
},
|
||||
required: ["ok", "service", "timestamp"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/{action}": {
|
||||
parameters: [
|
||||
{
|
||||
name: "action",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
get: {
|
||||
operationId: "authAction",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Auth route (Better Auth handler)",
|
||||
content: { "application/json": { schema: { type: "object" } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CodeBlock } from "@/components/ui/code-block";
|
||||
import { DemoClient } from "@/components/demo/demo-client";
|
||||
|
||||
const SAMPLE_MD = `# Demo Page
|
||||
|
||||
This page shows the core wiring of the template:
|
||||
|
||||
- **Server component** renders this markdown via \`react-markdown\` + \`remark-gfm\`
|
||||
- **Shiki** highlights code blocks server-side
|
||||
- **React Query** fetches \`/api/items\`
|
||||
- **React Hook Form + Zod** validates the "Add item" form
|
||||
- **better-auth** session is read on the server below
|
||||
|
||||
## Code sample
|
||||
|
||||
\`\`\`ts
|
||||
export function getGreeting(name: string): string {
|
||||
return \`Hello, \${name}!\`;
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
> Tip: copy this page as a starting point for real features.
|
||||
`;
|
||||
|
||||
export default function DemoPage() {
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-8 p-8">
|
||||
<article className="prose prose-sm dark:prose-invert">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code(props) {
|
||||
const { children, className, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className ?? "");
|
||||
if (match) {
|
||||
return (
|
||||
<CodeBlock
|
||||
code={String(children).replace(/\n$/, "")}
|
||||
lang={match[1]}
|
||||
className="my-4 overflow-x-auto rounded-lg border text-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code className={className} {...rest}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{SAMPLE_MD}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
|
||||
<DemoClient />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||
<h1 className="text-2xl font-semibold">Something went wrong</h1>
|
||||
<p className="max-w-md text-center text-sm text-muted-foreground">
|
||||
{error.message || "An unexpected error occurred."}
|
||||
</p>
|
||||
{error.digest ? (
|
||||
<p className="font-mono text-xs text-muted-foreground">Error ID: {error.digest}</p>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => reset()}
|
||||
className="rounded-lg border bg-background px-4 py-2 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@ export const metadata: Metadata = {
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center p-8" role="status">
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<span className="size-4 animate-spin rounded-full border-2 border-foreground/20 border-t-foreground" />
|
||||
Loading…
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||
<p className="text-5xl font-bold text-muted-foreground">404</p>
|
||||
<h1 className="text-2xl font-semibold">Page not found</h1>
|
||||
<p className="max-w-md text-center text-sm text-muted-foreground">
|
||||
The page you are looking for does not exist or has been moved.
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-lg border bg-background px-4 py-2 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Back to home
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+2
-66
@@ -1,69 +1,5 @@
|
||||
import Image from "next/image";
|
||||
import { HomeHero } from "@/components/home-hero";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex w-full max-w-3xl flex-1 flex-col items-center justify-between bg-white px-16 py-32 sm:items-start dark:bg-black">
|
||||
<Image
|
||||
className="h-5 w-[100px] dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl leading-10 font-semibold tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the{" "}
|
||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
||||
page.tsx
|
||||
</code>{" "}
|
||||
file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] md:w-[158px] dark:hover:bg-[#ccc]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="h-[14px] w-4 dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={14}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] md:w-[158px] dark:border-white/[.145] dark:hover:bg-[#1a1a1a]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
return <HomeHero />;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import Link from "next/link";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ProtectedPage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex max-w-2xl flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||
<h1 className="text-2xl font-semibold">Protected Page</h1>
|
||||
{session ? (
|
||||
<div className="flex flex-col items-center gap-2 rounded-lg border p-6">
|
||||
<p className="text-lg">
|
||||
Welcome, <span className="font-medium">{session.user.name}</span>!{" "}
|
||||
<span className="text-muted-foreground">({session.user.email})</span>
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Session expires: {new Date(session.session.expiresAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No session found.</p>
|
||||
)}
|
||||
<Link href="/" className="text-sm text-primary underline-offset-4 hover:underline">
|
||||
← Back home
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { ShowcaseGallery } from "@/components/showcase/gallery";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "UI Gallery — Aceternity",
|
||||
description:
|
||||
"Fifty-two Aceternity UI components vendored into the template — animated cards, beams, backgrounds, pins, marquees and more, wired for the App Router.",
|
||||
};
|
||||
|
||||
const SECTIONS = [
|
||||
"3D Pin / Marquee",
|
||||
"Backgrounds & Beams",
|
||||
"Cards & Hover",
|
||||
"Text Effects",
|
||||
"Inputs & Buttons",
|
||||
];
|
||||
|
||||
export default function UiGalleryPage() {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center bg-zinc-50 dark:bg-zinc-950 dark:bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.04),transparent_55%)]">
|
||||
<div className="w-full border-b border-zinc-200 bg-white/80 py-10 backdrop-blur-sm dark:border-zinc-800 dark:bg-zinc-950/80">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-3 px-6">
|
||||
<nav className="text-xs font-medium tracking-wide text-zinc-500 dark:text-zinc-400">
|
||||
<Link
|
||||
href="/"
|
||||
className="transition-colors hover:text-zinc-900 dark:hover:text-zinc-100"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<span className="px-2">/</span>
|
||||
<span className="text-zinc-800 dark:text-zinc-200">UI Gallery</span>
|
||||
</nav>
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-balance text-zinc-900 sm:text-4xl dark:text-zinc-50">
|
||||
Aceternity UI gallery
|
||||
</h1>
|
||||
<p className="max-w-2xl text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||
50 components vendored straight from the{" "}
|
||||
<a
|
||||
href="https://ui.aceternity.com/components"
|
||||
className="font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 hover:decoration-zinc-500 dark:text-zinc-200 dark:decoration-zinc-600"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Aceternity registry
|
||||
</a>{" "}
|
||||
and wired for this template. Everything below is live — hover, click and scroll.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
{SECTIONS.map((s) => (
|
||||
<a
|
||||
key={s}
|
||||
href={`#${s.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`}
|
||||
className="rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-300 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400 dark:hover:border-zinc-700 dark:hover:text-zinc-100"
|
||||
>
|
||||
{s}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="w-full max-w-6xl flex-1 px-6 py-12">
|
||||
<Suspense
|
||||
fallback={<div className="py-24 text-center text-sm text-zinc-400">Loading gallery…</div>}
|
||||
>
|
||||
<ShowcaseGallery />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,8 +8,12 @@
|
||||
"@base-ui/react": "^1.8.0",
|
||||
"@hookform/resolvers": "^5.9.1",
|
||||
"@shadcn/react": "^0.3.1",
|
||||
"@tabler/icons-react": "^3.47.0",
|
||||
"@tanstack/react-query": "^5.103.1",
|
||||
"@tanstack/react-table": "^9.2.4",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/react": "3.0.0",
|
||||
"@tsparticles/slim": "3.9.1",
|
||||
"better-auth": "^1.7.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -20,14 +24,15 @@
|
||||
"input-otp": "^1.5.0",
|
||||
"ky": "^2.1.0",
|
||||
"lucide-react": "^1.47.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"motion": "^13.4.0",
|
||||
"next": "16.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.10.1",
|
||||
"openapi-fetch": "^0.17.0",
|
||||
"react": "19.2.8",
|
||||
"react": "19.3.0",
|
||||
"react-day-picker": "^10.0.1",
|
||||
"react-dom": "19.2.8",
|
||||
"react-dom": "19.3.0",
|
||||
"react-hook-form": "^7.88.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.12.4",
|
||||
@@ -35,6 +40,7 @@
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shadcn": "^4.21.0",
|
||||
"shiki": "^4.4.3",
|
||||
"simplex-noise": "^4.0.3",
|
||||
"sonner": "^2.0.8",
|
||||
"tailwind-merge": "^3.7.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
@@ -48,18 +54,22 @@
|
||||
"@testing-library/dom": "^10.4.2",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
"@types/node": "^20",
|
||||
"@types/node": "^26",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"@vitest/coverage-v8": "^5.0.1",
|
||||
"eslint": "9.39.5",
|
||||
"eslint-config-next": "16.3.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^30.1.0",
|
||||
"lint-staged": "^17.5.1",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"prettier": "^3.9.8",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "^8.70.0",
|
||||
"vitest": "^5.0.1",
|
||||
},
|
||||
},
|
||||
@@ -75,7 +85,7 @@
|
||||
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@7.0.0", "", { "dependencies": { "@csstools/css-calc": "^3.3.0", "@csstools/css-color-parser": "^4.2.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.5.2" } }, "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ=="],
|
||||
|
||||
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@9.2.0", "", { "dependencies": { "bidi-js": "^1.1.0", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2" } }, "sha512-qKvLetaSd/OTWif9JT0eu1SHBg3H7Nvp9SoMwMxqT5tmYyrxea/P1A5pIfGbnIJlOtm3yVeqw+ORxDOkjN1aEw=="],
|
||||
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@9.2.1", "", { "dependencies": { "bidi-js": "^1.1.0", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.3" } }, "sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
@@ -139,6 +149,8 @@
|
||||
|
||||
"@base-ui/utils": ["@base-ui/utils@0.4.0", "", { "dependencies": { "@babel/runtime": "^7.29.7", "@floating-ui/utils": "^0.2.12", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw=="],
|
||||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
|
||||
|
||||
"@better-auth/core": ["@better-auth/core@1.7.5", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.41.1", "@standard-schema/spec": "^1.1.0", "zod": "^4.5.4" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.2", "@opentelemetry/api": "^1.9.0", "better-call": "1.4.0", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-kVlSu4H8OKQfjg4b/Zj5MOaospt83N0JbX38wsDzE58Yw95jzoLine truncated
|
||||
|
||||
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.7.5", "", { "peerDependencies": { "@better-auth/core": "^1.7.5", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-9SM7v1735SoaedRDcDbHc5ULgXEd2vUlEJkvRHpMF2Q9qf59TRh1b5A9hryyecyi56bm/0CNUDU3nY0uVWj5/Q=="],
|
||||
@@ -183,7 +195,7 @@
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
@@ -363,6 +375,12 @@
|
||||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="],
|
||||
|
||||
"@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="],
|
||||
|
||||
"@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="],
|
||||
|
||||
"@redocly/openapi-core": ["@redocly/openapi-core@1.34.20", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.3.2", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-ypeBZ/6BKXR9+7/TtbKhbl4UgD7raHhPS12oknlKno2A8+lnFkxIwiE/Aklu6L2cd/ioH+fCWuMxi9/p3EyAPw=="],
|
||||
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="],
|
||||
|
||||
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.9", "", { "os": "android", "cpu": "arm" }, "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw=="],
|
||||
@@ -427,6 +445,10 @@
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
|
||||
|
||||
"@tabler/icons": ["@tabler/icons@3.47.0", "", {}, "sha512-aCbSemVrGvSA3w0sJi8mka6QrnQCN8wCD1jKNXXIR/h6Q4ffov/yFPyzjTULDM8ETtK4DABUZhWdYlygqy3npA=="],
|
||||
|
||||
"@tabler/icons-react": ["@tabler/icons-react@3.47.0", "", { "dependencies": { "@tabler/icons": "3.47.0" }, "peerDependencies": { "react": ">= 16" } }, "sha512-lQkDkYwqTzRNt+MYNinu+fkcIZLKjYBcI37RWkcY72doOgE/vMVFvrFrGZ2cVo2ueB3zH1dWMIhnGgi8Y5mFJA=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "Line truncated
|
||||
@@ -477,6 +499,80 @@
|
||||
|
||||
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
|
||||
|
||||
"@tsparticles/basic": ["@tsparticles/basic@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1", "@tsparticles/move-base": "3.9.1", "@tsparticles/plugin-hex-color": "3.9.1", "@tsparticles/plugin-hsl-color": "3.9.1", "@tsparticles/plugin-rgb-color": "3.9.1", "@tsparticles/shape-circle": "3.9.1", "@tsparticles/updater-color": "3.9.1", "@tsparticles/updater-opacity": "3.9.1", "@tsparticles/updater-out-modes": "3.9.1", "@tsparticles/updater-size": "3.9.1" } }, "sha512-ijr2dHMx0IQHqhKWLine truncated
|
||||
|
||||
"@tsparticles/engine": ["@tsparticles/engine@3.9.1", "", {}, "sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ=="],
|
||||
|
||||
"@tsparticles/interaction-external-attract": ["@tsparticles/interaction-external-attract@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-5AJGmhzM9o4AVFV24WH5vSqMBzOXEOzIdGLIr+QJf4fRh9ZK62snsusv/ozKgs2KteRYQx+L7c5V3TqcDy2upg=="],
|
||||
|
||||
"@tsparticles/interaction-external-bounce": ["@tsparticles/interaction-external-bounce@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-bv05+h70UIHOTWeTsTI1AeAmX6R3s8nnY74Ea6p6AbQjERzPYIa0XY19nq/hA7+Nrg+EissP5zgoYYeSphr85A=="],
|
||||
|
||||
"@tsparticles/interaction-external-bubble": ["@tsparticles/interaction-external-bubble@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-tbd8ox/1GPl+zr+KyHQVV1bW88GE7OM6i4zql801YIlCDrl9wgTDdDFGIy9X7/cwTvTrCePhrfvdkUamXIribQ=="],
|
||||
|
||||
"@tsparticles/interaction-external-connect": ["@tsparticles/interaction-external-connect@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-sq8YfUNsIORjXHzzW7/AJQtfi/qDqLnYG2qOSE1WOsog39MD30RzmiOloejOkfNeUdcGUcfsDgpUuL3UhzFUOA=="],
|
||||
|
||||
"@tsparticles/interaction-external-grab": ["@tsparticles/interaction-external-grab@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-QwXza+sMMWDaMiFxd8y2tJwUK6c+nNw554+/9+tEZeTTk2fCbB0IJ7p/TH6ZGWDL0vo2muK54Njv2fEey191ow=="],
|
||||
|
||||
"@tsparticles/interaction-external-pause": ["@tsparticles/interaction-external-pause@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-Gzv4/FeNir0U/tVM9zQCqV1k+IAgaFjDU3T30M1AeAsNGh/rCITV2wnT7TOGFkbcla27m4Yxa+Fuab8+8pzm+g=="],
|
||||
|
||||
"@tsparticles/interaction-external-push": ["@tsparticles/interaction-external-push@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-GvnWF9Qy4YkZdx+WJL2iy9IcgLvzOIu3K7aLYJFsQPaxT8d9TF8WlpoMlWKnJID6H5q4JqQuMRKRyWH8aAKyQw=="],
|
||||
|
||||
"@tsparticles/interaction-external-remove": ["@tsparticles/interaction-external-remove@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-yPThm4UDWejDOWW5Qc8KnnS2EfSo5VFcJUQDWc1+Wcj17xe7vdSoiwwOORM0PmNBzdDpSKQrte/gUnoqaUMwOA=="],
|
||||
|
||||
"@tsparticles/interaction-external-repulse": ["@tsparticles/interaction-external-repulse@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-/LBppXkrMdvLHlEKWC7IykFhzrz+9nebT2fwSSFXK4plEBxDlIwnkDxd3FbVOAbnBvx4+L8+fbrEx+RvC8diAw=="],
|
||||
|
||||
"@tsparticles/interaction-external-slow": ["@tsparticles/interaction-external-slow@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-1ZYIR/udBwA9MdSCfgADsbDXKSFS0FMWuPWz7bm79g3sUxcYkihn+/hDhc6GXvNNR46V1ocJjrj0u6pAynS1KQ=="],
|
||||
|
||||
"@tsparticles/interaction-particles-attract": ["@tsparticles/interaction-particles-attract@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-CYYYowJuGwRLUixQcSU/48PTKM8fCUYThe0hXwQ+yRMLAn053VHzL7NNZzKqEIeEyt5oJoy9KcvubjKWbzMBLQ=="],
|
||||
|
||||
"@tsparticles/interaction-particles-collisions": ["@tsparticles/interaction-particles-collisions@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-ggGyjW/3v1yxvYW1IF1EMT15M6w31y5zfNNUPkqd/IXRNPYvm0Z0ayhp+FKmz70M5p0UxxPIQHTvAv9Jqnuj8w=="],
|
||||
|
||||
"@tsparticles/interaction-particles-links": ["@tsparticles/interaction-particles-links@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-MsLbMjy1vY5M5/hu/oa5OSRZAUz49H3+9EBMTIOThiX+a+vpl3sxc9AqNd9gMsPbM4WJlub8T6VBZdyvzez1Vg=="],
|
||||
|
||||
"@tsparticles/move-base": ["@tsparticles/move-base@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-X4huBS27d8srpxwOxliWPUt+NtCwY+8q/cx1DvQxyqmTA8VFCGpcHNwtqiN+9JicgzOvSuaORVqUgwlsc7h4pQ=="],
|
||||
|
||||
"@tsparticles/move-parallax": ["@tsparticles/move-parallax@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-whlOR0bVeyh6J/hvxf/QM3DqvNnITMiAQ0kro6saqSDItAVqg4pYxBfEsSOKq7EhjxNvfhhqR+pFMhp06zoCVA=="],
|
||||
|
||||
"@tsparticles/plugin-easing-quad": ["@tsparticles/plugin-easing-quad@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-C2UJOca5MTDXKUTBXj30Kiqr5UyID+xrY/LxicVWWZPczQW2bBxbIbfq9ULvzGDwBTxE2rdvIB8YFKmDYO45qw=="],
|
||||
|
||||
"@tsparticles/plugin-hex-color": ["@tsparticles/plugin-hex-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-vZgZ12AjUicJvk7AX4K2eAmKEQX/D1VEjEPFhyjbgI7A65eX72M465vVKIgNA6QArLZ1DLs7Z787LOE6GOBWsg=="],
|
||||
|
||||
"@tsparticles/plugin-hsl-color": ["@tsparticles/plugin-hsl-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-jJd1iGgRwX6eeNjc1zUXiJivaqC5UE+SC2A3/NtHwwoQrkfxGWmRHOsVyLnOBRcCPgBp/FpdDe6DIDjCMO715w=="],
|
||||
|
||||
"@tsparticles/plugin-rgb-color": ["@tsparticles/plugin-rgb-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-SBxk7f1KBfXeTnnklbE2Hx4jBgh6I6HOtxb+Os1gTp0oaghZOkWcCD2dP4QbUu7fVNCMOcApPoMNC8RTFcy9wQ=="],
|
||||
|
||||
"@tsparticles/react": ["@tsparticles/react@3.0.0", "", { "peerDependencies": { "@tsparticles/engine": "^3.0.2", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-hjGEtTT1cwv6BcjL+GcVgH++KYs52bIuQGW3PWv7z3tMa8g0bd6RI/vWSLj7p//NZ3uTjEIeilYIUPBh7Jfq/Q=="],
|
||||
|
||||
"@tsparticles/shape-circle": ["@tsparticles/shape-circle@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-DqZFLjbuhVn99WJ+A9ajz9YON72RtCcvubzq6qfjFmtwAK7frvQeb6iDTp6Ze9FUipluxVZWVRG4vWTxi2B+/g=="],
|
||||
|
||||
"@tsparticles/shape-emoji": ["@tsparticles/shape-emoji@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-ifvY63usuT+hipgVHb8gelBHSeF6ryPnMxAAEC1RGHhhXfpSRWMtE6ybr+pSsYU52M3G9+TF84v91pSwNrb9ZQ=="],
|
||||
|
||||
"@tsparticles/shape-image": ["@tsparticles/shape-image@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-fCA5eme8VF3oX8yNVUA0l2SLDKuiZObkijb0z3Ky0qj1HUEVlAuEMhhNDNB9E2iELTrWEix9z7BFMePp2CC7AA=="],
|
||||
|
||||
"@tsparticles/shape-line": ["@tsparticles/shape-line@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-wT8NSp0N9HURyV05f371cHKcNTNqr0/cwUu6WhBzbshkYGy1KZUP9CpRIh5FCrBpTev34mEQfOXDycgfG0KiLQ=="],
|
||||
|
||||
"@tsparticles/shape-polygon": ["@tsparticles/shape-polygon@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-dA77PgZdoLwxnliH6XQM/zF0r4jhT01pw5y7XTeTqws++hg4rTLV9255k6R6eUqKq0FPSW1/WBsBIl7q/MmrqQ=="],
|
||||
|
||||
"@tsparticles/shape-square": ["@tsparticles/shape-square@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-DKGkDnRyZrAm7T2ipqNezJahSWs6xd9O5LQLe5vjrYm1qGwrFxJiQaAdlb00UNrexz1/SA7bEoIg4XKaFa7qhQ=="],
|
||||
|
||||
"@tsparticles/shape-star": ["@tsparticles/shape-star@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-kdMJpi8cdeb6vGrZVSxTG0JIjCwIenggqk0EYeKAwtOGZFBgL7eHhF2F6uu1oq8cJAbXPujEoabnLsz6mW8XaA=="],
|
||||
|
||||
"@tsparticles/slim": ["@tsparticles/slim@3.9.1", "", { "dependencies": { "@tsparticles/basic": "3.9.1", "@tsparticles/engine": "3.9.1", "@tsparticles/interaction-external-attract": "3.9.1", "@tsparticles/interaction-external-bounce": "3.9.1", "@tsparticles/interaction-external-bubble": "3.9.1", "@tsparticles/interaction-external-connect": "3.9.1", "@tsparticles/interaction-external-grab": "3.9.1", "@tsparticles/interaction-external-pause": "3.9.1", "@tsparticles/interaction-external-push": Line truncated
|
||||
|
||||
"@tsparticles/updater-color": ["@tsparticles/updater-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-XGWdscrgEMA8L5E7exsE0f8/2zHKIqnTrZymcyuFBw2DCB6BIV+5z6qaNStpxrhq3DbIxxhqqcybqeOo7+Alpg=="],
|
||||
|
||||
"@tsparticles/updater-life": ["@tsparticles/updater-life@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-Oi8aF2RIwMMsjssUkCB6t3PRpENHjdZf6cX92WNfAuqXtQphr3OMAkYFJFWkvyPFK22AVy3p/cFt6KE5zXxwAA=="],
|
||||
|
||||
"@tsparticles/updater-opacity": ["@tsparticles/updater-opacity@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-w778LQuRZJ+IoWzeRdrGykPYSSaTeWfBvLZ2XwYEkh/Ss961InOxZKIpcS6i5Kp/Zfw0fS1ZAuqeHwuj///Osw=="],
|
||||
|
||||
"@tsparticles/updater-out-modes": ["@tsparticles/updater-out-modes@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-cKQEkAwbru+hhKF+GTsfbOvuBbx2DSB25CxOdhtW2wRvDBoCnngNdLw91rs+0Cex4tgEeibkebrIKFDDE6kELg=="],
|
||||
|
||||
"@tsparticles/updater-rotate": ["@tsparticles/updater-rotate@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-9BfKaGfp28JN82MF2qs6Ae/lJr9EColMfMTHqSKljblwbpVDHte4umuwKl3VjbRt87WD9MGtla66NTUYl+WxuQ=="],
|
||||
|
||||
"@tsparticles/updater-size": ["@tsparticles/updater-size@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-3NSVs0O2ApNKZXfd+y/zNhTXSFeG1Pw4peI8e6z/q5+XLbmue9oiEwoPy/tQLaark3oNj3JU7Q903ZijPyXSzw=="],
|
||||
|
||||
"@tsparticles/updater-stroke-color": ["@tsparticles/updater-stroke-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-3x14+C2is9pZYTg9T2TiA/aM1YMq4wLdYaZDcHm3qO30DZu5oeQq0rm/6w+QOGKYY1Z3Htg9rlSUZkhTHn7eDA=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="],
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
@@ -519,7 +615,7 @@
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
||||
"@types/node": ["@types/node@26.6.2", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="],
|
||||
|
||||
"@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
|
||||
|
||||
@@ -597,6 +693,12 @@
|
||||
|
||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.12.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA=="],
|
||||
|
||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@5.0.1", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/istanbul-lib-coverage": "^1.0.0", "@vitest/istanbul-lib-report": "^1.0.0", "ast-v8-to-istanbul": "^1.0.5", "magicast": "^0.5.4", "obug": "^2.1.4", "std-env": "^4.2.0", "tinyrainbow": "^3.1.1" }, "peerDependencies": { "@vitest/browser": "5.0.1", "vitest": "5.0.1" }, "optionalPeers": ["@vitest/browser"] }, "sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSLine truncated
|
||||
|
||||
"@vitest/istanbul-lib-coverage": ["@vitest/istanbul-lib-coverage@1.0.1", "", {}, "sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA=="],
|
||||
|
||||
"@vitest/istanbul-lib-report": ["@vitest/istanbul-lib-report@1.0.1", "", { "dependencies": { "@vitest/istanbul-lib-coverage": "1.0.1" } }, "sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@5.0.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.1", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@5.0.1", "", {}, "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q=="],
|
||||
@@ -607,6 +709,8 @@
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
@@ -645,6 +749,8 @@
|
||||
|
||||
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
|
||||
|
||||
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"atomically": ["atomically@1.7.0", "", {}, "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="],
|
||||
@@ -695,6 +801,8 @@
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
@@ -723,6 +831,8 @@
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
@@ -1059,6 +1169,8 @@
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
||||
@@ -1075,6 +1187,8 @@
|
||||
|
||||
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||
|
||||
"index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
@@ -1187,6 +1301,8 @@
|
||||
|
||||
"jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="],
|
||||
|
||||
"js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="],
|
||||
@@ -1271,6 +1387,8 @@
|
||||
|
||||
"magic-string": ["magic-string@1.4.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow=="],
|
||||
|
||||
"magicast": ["magicast@0.5.5", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
@@ -1383,6 +1501,8 @@
|
||||
|
||||
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||
|
||||
"mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
@@ -1451,6 +1571,8 @@
|
||||
|
||||
"openapi-fetch": ["openapi-fetch@0.17.0", "", { "dependencies": { "openapi-typescript-helpers": "^0.1.0" } }, "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig=="],
|
||||
|
||||
"openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="],
|
||||
|
||||
"openapi-typescript-helpers": ["openapi-typescript-helpers@0.1.0", "", {}, "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
@@ -1469,7 +1591,7 @@
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
||||
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
"parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="],
|
||||
|
||||
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
@@ -1499,9 +1621,11 @@
|
||||
|
||||
"playwright-core": ["playwright-core@1.63.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="],
|
||||
|
||||
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
|
||||
"postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="],
|
||||
|
||||
@@ -1535,15 +1659,15 @@
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||
"react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
|
||||
|
||||
"react-day-picker": ["react-day-picker@10.0.1", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||
"react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.88.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-QRaLOWhX93YCnMiRfnOFRSwWXZNt8qhm2JTZwypoDvKpSffTJHmpzMXt8U6PV5UThvL3IiiLDUWe2nHMtz6Mmw=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7RLine truncated
|
||||
|
||||
@@ -1619,7 +1743,7 @@
|
||||
|
||||
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
"scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
@@ -1659,6 +1783,8 @@
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"simplex-noise": ["simplex-noise@4.0.3", "", {}, "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||
@@ -1721,11 +1847,11 @@
|
||||
|
||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"systeminformation": ["systeminformation@5.33.12", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-7x1LtTPaopmrXUJmnxW6I98YIDihwiZEETD+hzI9X/GGIghcz8r38vlZ+TAqwQTOhPuZPB1xn/AJ2GPzvn9JfQ=="],
|
||||
"systeminformation": ["systeminformation@5.33.13", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-GtyakkchTQKymM1WeAB652zi3rM1q00RKdc+jZe9KGstuc/mWUwAnQGN/RE5gVYvHIR8xYOUIUD9irVKDtA0wA=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.7.0", "", {}, "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA=="],
|
||||
|
||||
@@ -1741,6 +1867,8 @@
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="],
|
||||
|
||||
"tldts": ["tldts@7.4.13", "", { "dependencies": { "tldts-core": "^7.4.13" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA=="],
|
||||
|
||||
"tldts-core": ["tldts-core@7.4.13", "", {}, "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg=="],
|
||||
@@ -1769,6 +1897,8 @@
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
@@ -1779,7 +1909,7 @@
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.70.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.70.0", "@typescript-eslint/parser": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0", "@typescript-eslint/utils": "8.70.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw=="],
|
||||
|
||||
@@ -1787,7 +1917,7 @@
|
||||
|
||||
"undici": ["undici@8.10.2", "", {}, "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
"undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
@@ -1813,6 +1943,8 @@
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="],
|
||||
|
||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||
|
||||
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||
@@ -1871,6 +2003,10 @@
|
||||
|
||||
"yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="],
|
||||
|
||||
"yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="],
|
||||
@@ -1909,10 +2045,12 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"@next/eslint-plugin-next/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
||||
|
||||
"@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@redocly/openapi-core/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
||||
|
||||
"@reduxjs/toolkit/reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="],
|
||||
|
||||
"@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
@@ -1945,12 +2083,16 @@
|
||||
|
||||
"ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
||||
|
||||
"better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="],
|
||||
|
||||
"body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||
|
||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"conf/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||
@@ -1959,6 +2101,8 @@
|
||||
|
||||
"conf/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
|
||||
"data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
|
||||
|
||||
"dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
|
||||
@@ -1991,8 +2135,6 @@
|
||||
|
||||
"negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||
|
||||
"next/postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
@@ -2003,10 +2145,10 @@
|
||||
|
||||
"pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
||||
|
||||
"pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"shadcn/cn": ["cn@0.2.6", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ=="],
|
||||
@@ -2025,6 +2167,8 @@
|
||||
|
||||
"vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightnLine truncated
|
||||
|
||||
"vite/postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
|
||||
|
||||
"vitest/tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
|
||||
|
||||
"wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||
@@ -2049,10 +2193,10 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@next/eslint-plugin-next/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="],
|
||||
|
||||
"@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="],
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { ItemForm } from "@/components/demo/item-form";
|
||||
import { ItemList } from "@/components/demo/item-list";
|
||||
|
||||
export function DemoClient() {
|
||||
return (
|
||||
<section className="flex flex-col gap-6">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<ItemForm />
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-sm font-semibold">Items (react-query + react-table)</h2>
|
||||
<ItemList />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const itemSchema = z.object({
|
||||
name: z.string().min(1, "Name is required").max(100),
|
||||
quantity: z.coerce
|
||||
.number({ error: "Quantity must be a number" })
|
||||
.int("Must be a whole number")
|
||||
.min(0, "Minimum 0")
|
||||
.max(10000, "Maximum 10000"),
|
||||
});
|
||||
|
||||
type ItemFormValues = z.input<typeof itemSchema>;
|
||||
type ItemSubmitValues = z.output<typeof itemSchema>;
|
||||
|
||||
export function ItemForm() {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<ItemFormValues, unknown, ItemSubmitValues>({
|
||||
resolver: zodResolver(itemSchema),
|
||||
defaultValues: { name: "", quantity: 1 },
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: ItemSubmitValues) => {
|
||||
const res = await fetch("/api/items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => null);
|
||||
throw new Error(err?.error ?? "Failed to create item");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Item created");
|
||||
reset();
|
||||
queryClient.invalidateQueries({ queryKey: ["items"] });
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
toast.error(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit((values) => mutation.mutate(values))}
|
||||
className="flex flex-col gap-3 rounded-lg border p-4"
|
||||
noValidate
|
||||
>
|
||||
<h2 className="text-sm font-semibold">Add item (RHF + zod + react-query)</h2>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="text-xs font-medium">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
{...register("name")}
|
||||
className="rounded-md border bg-background px-3 py-1.5 text-sm outline-none focus:border-ring"
|
||||
placeholder="e.g. USB-C cable"
|
||||
/>
|
||||
{errors.name && <span className="text-xs text-destructive">{errors.name.message}</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="quantity" className="text-xs font-medium">
|
||||
Quantity
|
||||
</label>
|
||||
<input
|
||||
id="quantity"
|
||||
type="number"
|
||||
{...register("quantity")}
|
||||
className="rounded-md border bg-background px-3 py-1.5 text-sm outline-none focus:border-ring"
|
||||
/>
|
||||
{errors.quantity && (
|
||||
<span className="text-xs text-destructive">{errors.quantity.message}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || mutation.isPending}
|
||||
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
{mutation.isPending ? "Creating…" : "Create item"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { flexRender } from "@tanstack/react-table";
|
||||
import {
|
||||
legacyCreateColumnHelper as createColumnHelper,
|
||||
getCoreRowModel,
|
||||
useLegacyTable,
|
||||
type LegacyColumnDef,
|
||||
} from "@tanstack/react-table/legacy";
|
||||
import { format } from "date-fns";
|
||||
import type { Item } from "@/lib/items";
|
||||
|
||||
const columnHelper = createColumnHelper<Item>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor("name", {
|
||||
header: "Name",
|
||||
}),
|
||||
columnHelper.accessor("quantity", {
|
||||
header: "Qty",
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: "Created",
|
||||
cell: (info) => format(new Date(info.getValue<string>()), "MMM d, HH:mm"),
|
||||
}),
|
||||
] as LegacyColumnDef<Item>[];
|
||||
|
||||
export function ItemList() {
|
||||
const { data, isLoading, isError, error, refetch } = useQuery({
|
||||
queryKey: ["items"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/items");
|
||||
if (!res.ok) throw new Error("Failed to load items");
|
||||
const body = await res.json();
|
||||
return body.items as Item[];
|
||||
},
|
||||
});
|
||||
|
||||
const table = useLegacyTable({
|
||||
data: data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <p className="text-sm text-muted-foreground">Loading items…</p>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 text-sm text-destructive">
|
||||
<span>{error.message}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refetch()}
|
||||
className="rounded-md border px-2 py-1 hover:bg-muted"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50 text-left">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th key={header.id} className="px-4 py-2 font-medium">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id} className="border-t hover:bg-muted/30">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className="px-4 py-2">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{data?.length === 0 ? (
|
||||
<p className="px-4 py-3 text-sm text-muted-foreground">No items yet.</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "motion/react";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { MagneticButton } from "@/components/ui/magnetic-button";
|
||||
import { SparklesCore } from "@/components/ui/sparkles";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
|
||||
export function HomeHero() {
|
||||
return (
|
||||
<div className="relative flex w-full flex-1 flex-col items-center justify-center overflow-hidden bg-zinc-50 antialiased dark:bg-zinc-950 dark:bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.06),transparent_60%)]">
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
<Spotlight className="-top-40 left-0 md:-top-20 md:left-60" fill="white" />
|
||||
</div>
|
||||
|
||||
{/* Sparkles fill the hero background */}
|
||||
<div className="absolute inset-0 z-0 h-full w-full">
|
||||
<SparklesCore
|
||||
id="home-sparkles"
|
||||
background="transparent"
|
||||
minSize={0.6}
|
||||
maxSize={1.4}
|
||||
particleDensity={60}
|
||||
particleColor="#71717a"
|
||||
className="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.main
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, ease: "easeOut" }}
|
||||
className="relative z-10 flex w-full max-w-4xl flex-col items-center gap-8 px-6 py-24 text-center"
|
||||
>
|
||||
<span className="rounded-full border border-zinc-200 bg-white/70 px-3 py-1 text-xs font-medium tracking-wide text-zinc-500 backdrop-blur dark:border-zinc-800 dark:bg-zinc-900/70 dark:text-zinc-400">
|
||||
Next.js 16 · Tailwind v4 · motion · 50+ Aceternity components
|
||||
</span>
|
||||
|
||||
<h1 className="max-w-3xl text-4xl leading-tight font-semibold tracking-tight text-zinc-900 sm:text-6xl dark:text-white">
|
||||
A template that ships with <Cover>animated UI out of the box.</Cover>
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl text-base leading-7 text-zinc-600 dark:text-zinc-300">
|
||||
Aceternity components vendored as source, Tailwind v4 native, zero-warning lint. Every
|
||||
effect below is live — build a landing page without starting from scratch.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||
<Link href="/ui" passHref>
|
||||
<MagneticButton strength={0.5} maxDistance={60}>
|
||||
<span className="inline-flex items-center gap-2 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200">
|
||||
Browse the gallery
|
||||
<span aria-hidden>→</span>
|
||||
</span>
|
||||
</MagneticButton>
|
||||
</Link>
|
||||
<Link
|
||||
href="/demo"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-zinc-300 px-6 py-3 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-900"
|
||||
>
|
||||
View the demo wiring
|
||||
</Link>
|
||||
</div>
|
||||
</motion.main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,733 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "motion/react";
|
||||
import { AuroraBackground } from "@/components/ui/aurora-background";
|
||||
import { BackgroundBeams } from "@/components/ui/background-beams";
|
||||
import { BackgroundGradientAnimation } from "@/components/ui/background-gradient-animation";
|
||||
import { BentoGrid, BentoGridItem } from "@/components/ui/bento-grid";
|
||||
import { CardSpotlight } from "@/components/ui/card-spotlight";
|
||||
import { CardStack } from "@/components/ui/card-stack";
|
||||
import { DirectionAwareHover } from "@/components/ui/direction-aware-hover";
|
||||
import { EncryptedText } from "@/components/ui/encrypted-text";
|
||||
import { EvervaultCard } from "@/components/ui/evervault-card";
|
||||
import { FlipWords } from "@/components/ui/flip-words";
|
||||
import { HeroHighlight, Highlight } from "@/components/ui/hero-highlight";
|
||||
import { InfiniteMovingCards } from "@/components/ui/infinite-moving-cards";
|
||||
import { MovingBorder } from "@/components/ui/moving-border";
|
||||
import { PinContainer } from "@/components/ui/3d-pin";
|
||||
import { PlaceholdersAndVanishInput } from "@/components/ui/placeholders-and-vanish-input";
|
||||
import { SparklesCore } from "@/components/ui/sparkles";
|
||||
import { ThreeDMarquee } from "@/components/ui/3d-marquee";
|
||||
import { TracingBeam } from "@/components/ui/tracing-beam";
|
||||
import { TypewriterEffect } from "@/components/ui/typewriter-effect";
|
||||
import { AnimatedTooltip } from "@/components/ui/animated-tooltip";
|
||||
import { Tabs } from "@/components/ui/animated-tabs";
|
||||
import { WavyBackground } from "@/components/ui/wavy-background";
|
||||
import { Boxes } from "@/components/ui/background-boxes";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { TextGenerateEffect } from "@/components/ui/text-generate-effect";
|
||||
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
|
||||
import { SquigglyText } from "@/components/ui/squiggly-text";
|
||||
import {
|
||||
TextRevealCard,
|
||||
TextRevealCardTitle,
|
||||
TextRevealCardDescription,
|
||||
} from "@/components/ui/text-reveal-card";
|
||||
import { Meteors } from "@/components/ui/meteors";
|
||||
import { GlareCard } from "@/components/ui/glare-card";
|
||||
import { MagneticButton } from "@/components/ui/magnetic-button";
|
||||
import { GooeyInput } from "@/components/ui/gooey-input";
|
||||
import { Notch } from "@/components/ui/notch";
|
||||
import { FloatingDock } from "@/components/ui/floating-dock";
|
||||
import { HeroParallax } from "@/components/ui/hero-parallax";
|
||||
import { ParallaxScroll } from "@/components/ui/parallax-scroll";
|
||||
import { ContainerScroll } from "@/components/ui/container-scroll-animation";
|
||||
import { StickyScroll } from "@/components/ui/sticky-scroll-reveal";
|
||||
import { Timeline } from "@/components/ui/timeline";
|
||||
import { FocusCards } from "@/components/ui/focus-cards";
|
||||
import { Tooltip } from "@/components/ui/tooltip-card";
|
||||
import { HoverEffect } from "@/components/ui/card-hover-effect";
|
||||
import { BackgroundGradient } from "@/components/ui/background-gradient";
|
||||
import { MultiStepLoader } from "@/components/ui/multi-step-loader";
|
||||
import {
|
||||
ModalProvider,
|
||||
Modal,
|
||||
ModalTrigger,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from "@/components/ui/animated-modal";
|
||||
import { MaskContainer } from "@/components/ui/svg-mask-effect";
|
||||
import { GoogleGeminiEffect } from "@/components/ui/google-gemini-effect";
|
||||
import { IconHome, IconUser, IconSettings, IconMoon, IconSun } from "@tabler/icons-react";
|
||||
import { useScroll, useTransform, MotionValue } from "motion/react";
|
||||
import { useState } from "react";
|
||||
|
||||
function MultiStepLoaderDemo() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<MultiStepLoader
|
||||
loading={loading}
|
||||
loadingStates={[
|
||||
{ text: "Fetching" },
|
||||
{ text: "Compiling" },
|
||||
{ text: "Rendering" },
|
||||
{ text: "Done" },
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
setTimeout(() => setLoading(false), 4000);
|
||||
}}
|
||||
className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
>
|
||||
{loading ? "Loading…" : "Run loader"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Card({
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
label?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-full flex-col items-center justify-center gap-5 overflow-hidden rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{label && (
|
||||
<span className="absolute right-3 bottom-3 rounded-full border border-zinc-200 bg-zinc-50 px-2 py-0.5 text-[10px] font-medium text-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-500">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionLabel({ id, index, title }: { id: string; index: string; title: string }) {
|
||||
return (
|
||||
<div id={id} className="flex w-full max-w-6xl flex-col items-start gap-1.5">
|
||||
<span className="text-[11px] font-semibold tracking-[0.2em] text-zinc-400 uppercase">
|
||||
{index}
|
||||
</span>
|
||||
<h2 className="text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BENTO_ITEMS = [
|
||||
{
|
||||
title: "Vendored, not installed",
|
||||
description: "Each component ships as source in components/ui — zero registry lock-in.",
|
||||
header: "✦",
|
||||
className: "md:col-span-2",
|
||||
},
|
||||
{
|
||||
title: "Tailwind v4 native",
|
||||
description: "Same cn() util and zinc palette as the rest of the template.",
|
||||
header: "❖",
|
||||
},
|
||||
{
|
||||
title: "Zero-warning lint",
|
||||
description: "Vendored dir has its own scoped eslint override.",
|
||||
header: "✓",
|
||||
className: "md:col-span-2",
|
||||
},
|
||||
];
|
||||
|
||||
const PEOPLE = [
|
||||
{ id: 1, name: "Aceternity", designation: "ui.aceternity.com", image: "/vercel.svg" },
|
||||
{ id: 2, name: "Vercel", designation: "nextjs-template", image: "/next.svg" },
|
||||
{ id: 3, name: "Template", designation: "Shadcn + Aceternity", image: "/vercel.svg" },
|
||||
];
|
||||
|
||||
const STACK_CARDS = [
|
||||
{
|
||||
id: 1,
|
||||
name: "3D Pin",
|
||||
designation: "Hover cards",
|
||||
content: <>A card that lifts on hover, with a beam behind it.</>,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Tracing Beam",
|
||||
designation: "Scroll guide",
|
||||
content: <>A beam that traces your scroll, guiding the eye down the page.</>,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Evervault",
|
||||
designation: "Encrypted hover",
|
||||
content: <>Patterns react to your mouse while the card sits above an animated capsule.</>,
|
||||
},
|
||||
];
|
||||
|
||||
const TESTIMONIALS_MOCK = [
|
||||
{
|
||||
quote: "The fifty components slot straight into the template and just work.",
|
||||
name: "Next.js Template",
|
||||
title: "Vendored showcase",
|
||||
},
|
||||
{
|
||||
quote: "Hover, scroll and type — everything below is live, not a static screenshot.",
|
||||
name: "Aceternity Registry",
|
||||
title: "ui.aceternity.com",
|
||||
},
|
||||
{
|
||||
quote: "Built with motion/react and Tailwind v4, matching the template's own stack.",
|
||||
name: "Motion",
|
||||
title: "motion/react",
|
||||
},
|
||||
];
|
||||
|
||||
export function ShowcaseGallery() {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center gap-20 py-10">
|
||||
<section id="hero" className="flex w-full flex-col items-center gap-6">
|
||||
<AuroraBackground className="w-full overflow-hidden rounded-3xl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="flex flex-col items-center gap-4 px-6 py-24 text-center"
|
||||
>
|
||||
<p className="text-xs font-medium tracking-widest text-zinc-500 uppercase">
|
||||
50 components · vendored · lint-clean
|
||||
</p>
|
||||
<h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-zinc-900 sm:text-5xl dark:text-white">
|
||||
Aceternity UI, wired for this template
|
||||
</h1>
|
||||
<p className="max-w-xl text-sm leading-6 text-zinc-600 dark:text-zinc-300">
|
||||
Animated cards, beams, bento grids and text effects — pulled from the registry,
|
||||
adapted to Tailwind v4 + motion/react, and demoed live on this page.
|
||||
</p>
|
||||
</motion.div>
|
||||
</AuroraBackground>
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="3d-marquee">
|
||||
<ThreeDMarquee
|
||||
className="w-full"
|
||||
images={["/next.svg", "/vercel.svg", "/next.svg", "/vercel.svg"]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="animated-tooltip">
|
||||
<AnimatedTooltip items={PEOPLE} />
|
||||
</Card>
|
||||
<Card label="typewriter-effect">
|
||||
<TypewriterEffect
|
||||
words={[
|
||||
{ text: "Typewriter" },
|
||||
{ text: "effect" },
|
||||
{ text: "…" },
|
||||
{ text: "works." },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="beams" index="01" title="Beams, sparkles & gradients" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="background-beams" className="md:col-span-2">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl">
|
||||
<BackgroundBeams className="absolute inset-0" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-300">
|
||||
BackgroundBeams — hover to see the glow
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="sparkles">
|
||||
<div className="flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<SparklesCore
|
||||
background="transparent"
|
||||
minSize={0.6}
|
||||
maxSize={1.6}
|
||||
particleDensity={90}
|
||||
particleColor="#a1a1aa"
|
||||
className="h-full w-full"
|
||||
/>
|
||||
<p className="absolute bottom-4 text-xs text-zinc-500">SparklesCore</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="background-gradient-animation" className="md:col-span-3">
|
||||
<BackgroundGradientAnimation
|
||||
containerClassName="rounded-xl"
|
||||
className="absolute inset-0 z-20"
|
||||
>
|
||||
<div className="relative z-30 flex flex-col items-center gap-2 px-6 text-center">
|
||||
<p className="text-lg font-semibold text-white">BackgroundGradientAnimation</p>
|
||||
<p className="text-xs text-white/70">Animated aurora wash behind any content</p>
|
||||
</div>
|
||||
</BackgroundGradientAnimation>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="cards" index="02" title="Pins, stacks & hover cards" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="3d-pin" className="md:col-span-2">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<PinContainer title="ui.aceternity.com" href="#cards">
|
||||
<div className="flex h-56 w-72 flex-col items-center justify-center gap-2 rounded-lg bg-zinc-100 p-4 dark:bg-zinc-800">
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">Hover me</p>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">3D Pin</p>
|
||||
</div>
|
||||
</PinContainer>
|
||||
<PinContainer title="bento grid →" href="#bento">
|
||||
<div className="flex h-56 w-72 flex-col items-center justify-center gap-2 rounded-lg bg-zinc-100 p-4 dark:bg-zinc-800">
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">
|
||||
Perspective lift
|
||||
</p>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">on hover</p>
|
||||
</div>
|
||||
</PinContainer>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="card-spotlight">
|
||||
<CardSpotlight className="w-full p-8">
|
||||
<p className="text-sm font-medium text-zinc-100">CardSpotlight</p>
|
||||
<p className="mt-2 text-xs text-zinc-400">Move your cursor for a spotlight.</p>
|
||||
</CardSpotlight>
|
||||
</Card>
|
||||
<Card label="card-stack">
|
||||
<CardStack items={STACK_CARDS} />
|
||||
</Card>
|
||||
<Card label="direction-aware-hover">
|
||||
<DirectionAwareHover imageUrl="/next.svg" className="w-full">
|
||||
<p className="text-sm font-medium text-white">DirectionAwareHover</p>
|
||||
</DirectionAwareHover>
|
||||
</Card>
|
||||
<Card label="bento-grid">
|
||||
<BentoGrid className="mx-auto w-full max-w-3xl">
|
||||
{BENTO_ITEMS.map((item, i) => (
|
||||
<BentoGridItem
|
||||
key={i}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
header={item.header}
|
||||
className={item.className}
|
||||
/>
|
||||
))}
|
||||
</BentoGrid>
|
||||
</Card>
|
||||
<Card label="moving-border">
|
||||
<MovingBorder
|
||||
duration={3000}
|
||||
className="rounded-full border border-zinc-200 bg-white px-6 py-2 text-sm font-medium text-zinc-800 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
|
||||
>
|
||||
MovingBorder
|
||||
</MovingBorder>
|
||||
</Card>
|
||||
<Card label="infinite-moving-cards">
|
||||
<InfiniteMovingCards
|
||||
items={TESTIMONIALS_MOCK}
|
||||
direction="left"
|
||||
speed="slow"
|
||||
pauseOnHover
|
||||
className="w-full"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="text" index="03" title="Highlight, flip & encrypt" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="hero-highlight" className="md:col-span-2">
|
||||
<HeroHighlight containerClassName="w-full rounded-xl">
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="px-4 py-3 text-center font-sans text-2xl text-zinc-900 sm:text-3xl dark:text-white"
|
||||
>
|
||||
Text effects with{" "}
|
||||
<Highlight className="text-black dark:text-white">mouse-follow highlight</Highlight>
|
||||
</motion.h1>
|
||||
</HeroHighlight>
|
||||
</Card>
|
||||
<Card label="flip-words">
|
||||
<div className="text-center text-lg text-zinc-700 dark:text-zinc-200">
|
||||
Animate text like <FlipWords words={["sparkles", "beams", "pins", "marquees"]} />
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="encrypted-text">
|
||||
<EncryptedText
|
||||
text="Aceternity, vendored."
|
||||
className="text-center text-lg text-zinc-800 dark:text-zinc-100"
|
||||
/>
|
||||
</Card>
|
||||
<Card label="evervault-card" className="md:col-span-2">
|
||||
<div className="w-full max-w-md">
|
||||
<EvervaultCard text="hover the capsule" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="tracing-beam" className="md:col-span-2">
|
||||
<TracingBeam className="w-full px-2">
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{[
|
||||
["Beam one", "The beam follows your scroll."],
|
||||
["Beam two", "It traces between sticky sections."],
|
||||
["Beam three", "Pairs perfectly with tall content."],
|
||||
].map(([t, d]) => (
|
||||
<div
|
||||
key={t}
|
||||
className="rounded-xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-800/50"
|
||||
>
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">{t}</p>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">{d}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TracingBeam>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="inputs" index="04" title="Inputs with built-in motion" />
|
||||
<Card label="placeholders-and-vanish-input" className="max-w-2xl">
|
||||
<PlaceholdersAndVanishInput
|
||||
placeholders={["Search ", "Try a phrase…", "The placeholder vanishes on focus"]}
|
||||
onChange={() => {}}
|
||||
onSubmit={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="beams2" index="05" title="Waves, boxes & spotlights" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="wavy-background" className="md:col-span-2">
|
||||
<WavyBackground className="w-full rounded-xl py-16">
|
||||
<p className="relative z-10 text-2xl font-bold text-white">WavyBackground</p>
|
||||
<p className="relative z-10 text-sm text-zinc-300">
|
||||
Animated SVG waves behind content.
|
||||
</p>
|
||||
</WavyBackground>
|
||||
</Card>
|
||||
<Card label="background-boxes">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<Boxes className="absolute inset-0" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-300">
|
||||
Boxes — grid dots pattern
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="spotlight">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<Spotlight className="-top-20 left-0" fill="white" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-200">Spotlight</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="background-gradient" className="md:col-span-2">
|
||||
<BackgroundGradient className="rounded-xl p-6">
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-100">
|
||||
BackgroundGradient — animated border glow
|
||||
</p>
|
||||
</BackgroundGradient>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="text2" index="06" title="More text effects" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="cover">
|
||||
<p className="text-center text-2xl font-bold text-zinc-900 dark:text-white">
|
||||
Outline <Cover>Cover</Cover> underline
|
||||
</p>
|
||||
</Card>
|
||||
<Card label="text-generate-effect">
|
||||
<TextGenerateEffect words="Text that generates itself, character by character." />
|
||||
</Card>
|
||||
<Card label="text-hover-effect">
|
||||
<TextHoverEffect text="HOVER" />
|
||||
</Card>
|
||||
<Card label="squiggly-text">
|
||||
<SquigglyText className="text-2xl font-bold text-zinc-900 dark:text-white">
|
||||
SquigglyText wobble
|
||||
</SquigglyText>
|
||||
</Card>
|
||||
<Card label="text-reveal-card" className="md:col-span-2">
|
||||
<TextRevealCard text="Hover to reveal" revealText="A secret message 👀">
|
||||
<TextRevealCardTitle>TextRevealCard</TextRevealCardTitle>
|
||||
<TextRevealCardDescription>
|
||||
The card's cyan gradient sweeps in on hover.
|
||||
</TextRevealCardDescription>
|
||||
</TextRevealCard>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="cards2" index="07" title="Cards, meteors & buttons" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card label="meteors">
|
||||
<div className="relative flex h-full min-h-[240px] w-full items-center justify-center overflow-hidden rounded-xl bg-zinc-950">
|
||||
<Meteors number={20} className="absolute inset-0" />
|
||||
<p className="relative z-10 text-sm font-medium text-zinc-300">Meteors</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card label="glare-card">
|
||||
<GlareCard className="flex h-full min-h-[240px] items-center justify-center rounded-xl">
|
||||
<p className="text-sm font-medium text-white">GlareCard</p>
|
||||
</GlareCard>
|
||||
</Card>
|
||||
<Card label="magnetic-button">
|
||||
<MagneticButton>
|
||||
<span className="inline-flex items-center gap-2 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white dark:bg-white dark:text-zinc-900">
|
||||
MagneticButton
|
||||
</span>
|
||||
</MagneticButton>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="interaction" index="08" title="Inputs, docks & modals" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Card label="gooey-input">
|
||||
<GooeyInput placeholder="Type to search…" />
|
||||
</Card>
|
||||
<Card label="notch">
|
||||
<Notch
|
||||
items={[
|
||||
{
|
||||
id: "theme",
|
||||
label: "Theme",
|
||||
options: [
|
||||
{ id: "light", label: "Light", icon: <IconSun className="h-4 w-4" /> },
|
||||
{ id: "dark", label: "Dark", icon: <IconMoon className="h-4 w-4" /> },
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="floating-dock" className="md:col-span-2">
|
||||
<FloatingDock
|
||||
items={[
|
||||
{ title: "Home", icon: <IconHome className="h-5 w-5" />, href: "#" },
|
||||
{ title: "Profile", icon: <IconUser className="h-5 w-5" />, href: "#" },
|
||||
{ title: "Settings", icon: <IconSettings className="h-5 w-5" />, href: "#" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="animated-modal" className="md:col-span-2">
|
||||
<ModalProvider>
|
||||
<Modal>
|
||||
<ModalTrigger>
|
||||
<span className="cursor-pointer rounded-full bg-zinc-900 px-5 py-2 text-sm font-medium text-white dark:bg-white dark:text-zinc-900">
|
||||
Open animated modal
|
||||
</span>
|
||||
</ModalTrigger>
|
||||
<ModalBody>
|
||||
<ModalContent>
|
||||
<p className="text-center text-2xl text-zinc-900 dark:text-white">
|
||||
Animated Modal
|
||||
</p>
|
||||
<p className="mt-2 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
||||
The modal scales and fades, with a fancy backdrop.
|
||||
</p>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<button className="rounded-full bg-zinc-900 px-5 py-2 text-sm text-white dark:bg-white dark:text-zinc-900">
|
||||
Close
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</ModalProvider>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="scroll" index="09" title="Scroll & parallax" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6">
|
||||
<Card label="sticky-scroll-reveal">
|
||||
<StickyScroll
|
||||
content={[
|
||||
{
|
||||
title: "StickyScroll",
|
||||
description: "Content sticks while you scroll; titles swap.",
|
||||
},
|
||||
{
|
||||
title: "Second block",
|
||||
description: "The next item rises as the previous fades.",
|
||||
},
|
||||
{
|
||||
title: "Third block",
|
||||
description: "Great for feature lists or onboarding.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="container-scroll-animation">
|
||||
<ContainerScroll
|
||||
titleComponent={
|
||||
<p className="text-2xl font-bold text-zinc-900 dark:text-white">ContainerScroll</p>
|
||||
}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center rounded-xl bg-zinc-100 p-8 dark:bg-zinc-800">
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300">
|
||||
The card scales up as you scroll past.
|
||||
</p>
|
||||
</div>
|
||||
</ContainerScroll>
|
||||
</Card>
|
||||
<Card label="timeline">
|
||||
<Timeline
|
||||
data={[
|
||||
{
|
||||
title: "2026 · Phase 1",
|
||||
content: <>Vendor registry components into the template.</>,
|
||||
},
|
||||
{ title: "2026 · Phase 2", content: <>Showcase gallery + home hero upgrade.</> },
|
||||
{
|
||||
title: "2026 · Phase 3",
|
||||
content: <>Zero-warning toolchain, green CI, auto-deploy.</>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="hover-scroll" index="10" title="Hover cards, parallax & masking" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6">
|
||||
<Card label="card-hover-effect">
|
||||
<HoverEffect
|
||||
className="w-full"
|
||||
items={[
|
||||
{ title: "HoverEffect", description: "Cards lift & glow on hover.", link: "#" },
|
||||
{ title: "Second card", description: "The grid is responsive.", link: "#" },
|
||||
{ title: "Third card", description: "Click to follow the link.", link: "#" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="focus-cards">
|
||||
<FocusCards
|
||||
cards={[
|
||||
{ title: "FocusCards", src: "/vercel.svg" },
|
||||
{ title: "Second", src: "/next.svg" },
|
||||
{ title: "Third", src: "/vercel.svg" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="tooltip-card">
|
||||
<Tooltip content={<p className="text-xs text-zinc-300">Tooltip on hover</p>}>
|
||||
<span className="cursor-pointer text-sm font-medium text-zinc-700 dark:text-zinc-200">
|
||||
Hover this for a tooltip
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
<Card label="multi-step-loader">
|
||||
<MultiStepLoaderDemo />
|
||||
</Card>
|
||||
<Card label="svg-mask-effect">
|
||||
<MaskContainer
|
||||
revealText={<p className="text-4xl font-bold text-zinc-300">Revealed!</p>}
|
||||
className="h-[16rem]"
|
||||
>
|
||||
<p className="text-4xl font-bold text-zinc-900 dark:text-white">
|
||||
Hover over this masked block
|
||||
</p>
|
||||
</MaskContainer>
|
||||
</Card>
|
||||
<Card label="hero-parallax">
|
||||
<HeroParallax
|
||||
products={[
|
||||
{ title: "Next", link: "#", thumbnail: "/next.svg" },
|
||||
{ title: "Vercel", link: "#", thumbnail: "/vercel.svg" },
|
||||
{ title: "Globe", link: "#", thumbnail: "/globe.svg" },
|
||||
{ title: "Window", link: "#", thumbnail: "/window.svg" },
|
||||
{ title: "File", link: "#", thumbnail: "/file.svg" },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card label="parallax-scroll">
|
||||
<ParallaxScroll
|
||||
images={[
|
||||
"/next.svg",
|
||||
"/vercel.svg",
|
||||
"/globe.svg",
|
||||
"/window.svg",
|
||||
"/file.svg",
|
||||
"/next.svg",
|
||||
]}
|
||||
className="w-full"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex w-full flex-col items-center gap-6">
|
||||
<SectionLabel id="advanced" index="11" title="Tabs & gemini" />
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 gap-6">
|
||||
<Card label="animated-tabs">
|
||||
<Tabs
|
||||
tabs={[
|
||||
{
|
||||
title: "Overview",
|
||||
value: "overview",
|
||||
content: <p className="p-4 text-sm">Overview tab</p>,
|
||||
},
|
||||
{
|
||||
title: "Details",
|
||||
value: "details",
|
||||
content: <p className="p-4 text-sm">Details tab</p>,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
value: "settings",
|
||||
content: <p className="p-4 text-sm">Settings tab</p>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<GeminiDemo />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GeminiDemo() {
|
||||
const { scrollYProgress } = useScroll();
|
||||
const pathLengths: MotionValue<number>[] = [
|
||||
useTransform(scrollYProgress, [0, 0.2], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.2, 0.4], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.4, 0.6], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.6, 0.8], [0, 1]),
|
||||
useTransform(scrollYProgress, [0.8, 1], [0, 1]),
|
||||
];
|
||||
return (
|
||||
<Card label="google-gemini-effect" className="overflow-visible">
|
||||
<GoogleGeminiEffect
|
||||
pathLengths={pathLengths}
|
||||
title="Google Gemini Effect"
|
||||
description="Scroll — the SVG paths draw as you scroll."
|
||||
className="w-full"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
export const ThreeDMarquee = ({ images, className }: { images: string[]; className?: string }) => {
|
||||
// Split the images array into 4 equal parts
|
||||
const chunkSize = Math.ceil(images.length / 4);
|
||||
const chunks = Array.from({ length: 4 }, (_, colIndex) => {
|
||||
const start = colIndex * chunkSize;
|
||||
return images.slice(start, start + chunkSize);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={cn("mx-auto block h-[600px] overflow-hidden rounded-2xl max-sm:h-100", className)}
|
||||
>
|
||||
<div className="flex size-full items-center justify-center">
|
||||
<div className="size-[1720px] shrink-0 scale-50 sm:scale-75 lg:scale-100">
|
||||
<div
|
||||
style={{
|
||||
transform: "rotateX(55deg) rotateY(0deg) rotateZ(-45deg)",
|
||||
}}
|
||||
className="relative top-96 right-[50%] grid size-full origin-top-left grid-cols-4 gap-8 transform-3d"
|
||||
>
|
||||
{chunks.map((subarray, colIndex) => (
|
||||
<motion.div
|
||||
animate={{ y: colIndex % 2 === 0 ? 100 : -100 }}
|
||||
transition={{
|
||||
duration: colIndex % 2 === 0 ? 10 : 15,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
}}
|
||||
key={colIndex + "marquee"}
|
||||
className="flex flex-col items-start gap-8"
|
||||
>
|
||||
<GridLineVertical className="-left-4" offset="80px" />
|
||||
{subarray.map((image, imageIndex) => (
|
||||
<div className="relative" key={imageIndex + image}>
|
||||
<GridLineHorizontal className="-top-4" offset="20px" />
|
||||
<motion.img
|
||||
whileHover={{
|
||||
y: -10,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
key={imageIndex + image}
|
||||
src={image}
|
||||
alt={`Image ${imageIndex + 1}`}
|
||||
className="aspect-[970/700] rounded-lg object-cover ring ring-gray-950/5 hover:shadow-2xl"
|
||||
width={970}
|
||||
height={700}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GridLineHorizontal = ({ className, offset }: { className?: string; offset?: string }) => {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--background": "#ffffff",
|
||||
"--color": "rgba(0, 0, 0, 0.2)",
|
||||
"--height": "1px",
|
||||
"--width": "5px",
|
||||
"--fade-stop": "90%",
|
||||
"--offset": offset || "200px", //-100px if you want to keep the line inside
|
||||
"--color-dark": "rgba(255, 255, 255, 0.2)",
|
||||
maskComposite: "exclude",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"absolute left-[calc(var(--offset)/2*-1)] h-[var(--height)] w-[calc(100%+var(--offset))]",
|
||||
"bg-[linear-gradient(to_right,var(--color),var(--color)_50%,transparent_0,transparent)]",
|
||||
"[background-size:var(--width)_var(--height)]",
|
||||
"[mask:linear-gradient(to_left,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_right,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
|
||||
"[mask-composite:exclude]",
|
||||
"z-30",
|
||||
"dark:bg-[linear-gradient(to_right,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
|
||||
className
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
const GridLineVertical = ({ className, offset }: { className?: string; offset?: string }) => {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--background": "#ffffff",
|
||||
"--color": "rgba(0, 0, 0, 0.2)",
|
||||
"--height": "5px",
|
||||
"--width": "1px",
|
||||
"--fade-stop": "90%",
|
||||
"--offset": offset || "150px", //-100px if you want to keep the line inside
|
||||
"--color-dark": "rgba(255, 255, 255, 0.2)",
|
||||
maskComposite: "exclude",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"absolute top-[calc(var(--offset)/2*-1)] h-[calc(100%+var(--offset))] w-[var(--width)]",
|
||||
"bg-[linear-gradient(to_bottom,var(--color),var(--color)_50%,transparent_0,transparent)]",
|
||||
"[background-size:var(--width)_var(--height)]",
|
||||
"[mask:linear-gradient(to_top,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_bottom,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
|
||||
"[mask-composite:exclude]",
|
||||
"z-30",
|
||||
"dark:bg-[linear-gradient(to_bottom,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
|
||||
className
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const PinContainer = ({
|
||||
children,
|
||||
title,
|
||||
href,
|
||||
className,
|
||||
containerClassName,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
href?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}) => {
|
||||
const [transform, setTransform] = useState("translate(-50%,-50%) rotateX(0deg)");
|
||||
|
||||
const onMouseEnter = () => {
|
||||
setTransform("translate(-50%,-50%) rotateX(40deg) scale(0.8)");
|
||||
};
|
||||
const onMouseLeave = () => {
|
||||
setTransform("translate(-50%,-50%) rotateX(0deg) scale(1)");
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
className={cn("group/pin relative z-50 cursor-pointer", containerClassName)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
href={href || "/"}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
perspective: "1000px",
|
||||
transform: "rotateX(70deg) translateZ(0deg)",
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
transform: transform,
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 flex items-start justify-start overflow-hidden rounded-2xl border border-white/[0.1] bg-black p-4 shadow-[0_8px_16px_rgb(0_0_0/0.4)] transition duration-700 group-hover/pin:border-white/[0.2]"
|
||||
>
|
||||
<div className={cn("relative z-50", className)}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
<PinPerspective title={title} href={href} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const PinPerspective = ({ title, href }: { title?: string; href?: string }) => {
|
||||
return (
|
||||
<motion.div className="pointer-events-none z-[60] flex h-80 w-96 items-center justify-center opacity-0 transition duration-500 group-hover/pin:opacity-100">
|
||||
<div className="inset-0 -mt-7 h-full w-full flex-none">
|
||||
<div className="absolute inset-x-0 top-0 flex justify-center">
|
||||
<a
|
||||
href={href}
|
||||
target={"_blank"}
|
||||
className="relative z-10 flex items-center space-x-2 rounded-full bg-zinc-950 px-4 py-0.5 ring-1 ring-white/10"
|
||||
>
|
||||
<span className="relative z-20 inline-block py-0.5 text-xs font-bold text-white">
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-emerald-400/90 to-emerald-400/0 transition-opacity duration-500 group-hover/btn:opacity-40"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
perspective: "1000px",
|
||||
transform: "rotateX(70deg) translateZ(0)",
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
<>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
x: "-50%",
|
||||
y: "-50%",
|
||||
}}
|
||||
animate={{
|
||||
opacity: [0, 1, 0.5, 0],
|
||||
scale: 1,
|
||||
|
||||
z: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
delay: 0,
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
|
||||
></motion.div>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
x: "-50%",
|
||||
y: "-50%",
|
||||
}}
|
||||
animate={{
|
||||
opacity: [0, 1, 0.5, 0],
|
||||
scale: 1,
|
||||
|
||||
z: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
delay: 2,
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
|
||||
></motion.div>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
x: "-50%",
|
||||
y: "-50%",
|
||||
}}
|
||||
animate={{
|
||||
opacity: [0, 1, 0.5, 0],
|
||||
scale: 1,
|
||||
|
||||
z: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
delay: 4,
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 h-[11.25rem] w-[11.25rem] rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)]"
|
||||
></motion.div>
|
||||
</>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 blur-[2px] group-hover/pin:h-40" />
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-cyan-500 group-hover/pin:h-40" />
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[4px] w-[4px] translate-x-[1.5px] translate-y-[14px] rounded-full bg-cyan-600 blur-[3px]" />
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[2px] w-[2px] translate-x-[0.5px] translate-y-[14px] rounded-full bg-cyan-300" />
|
||||
</>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import React, { ReactNode, createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
|
||||
interface ModalContextType {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const ModalContext = createContext<ModalContextType | undefined>(undefined);
|
||||
|
||||
export const ModalProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return <ModalContext.Provider value={{ open, setOpen }}>{children}</ModalContext.Provider>;
|
||||
};
|
||||
|
||||
export const useModal = () => {
|
||||
const context = useContext(ModalContext);
|
||||
if (!context) {
|
||||
throw new Error("useModal must be used within a ModalProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export function Modal({ children }: { children: ReactNode }) {
|
||||
return <ModalProvider>{children}</ModalProvider>;
|
||||
}
|
||||
|
||||
export const ModalTrigger = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { setOpen } = useModal();
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-md px-4 py-2 text-center text-black dark:text-white",
|
||||
className
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalBody = ({ children, className }: { children: ReactNode; className?: string }) => {
|
||||
const { open } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const { setOpen } = useModal();
|
||||
useOutsideClick(modalRef, () => setOpen(false));
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
backdropFilter: "blur(0px)",
|
||||
}}
|
||||
className="fixed inset-0 z-50 flex h-full w-full items-center justify-center [perspective:800px] [transform-style:preserve-3d]"
|
||||
>
|
||||
<Overlay />
|
||||
|
||||
<motion.div
|
||||
ref={modalRef}
|
||||
className={cn(
|
||||
"relative z-50 flex max-h-[90%] min-h-[50%] flex-1 flex-col overflow-hidden border border-transparent bg-white md:max-w-[40%] md:rounded-2xl dark:border-neutral-800 dark:bg-neutral-950",
|
||||
className
|
||||
)}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.5,
|
||||
rotateX: 40,
|
||||
y: 40,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
rotateX: 0,
|
||||
y: 0,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
rotateX: 10,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 15,
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalContent = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return <div className={cn("flex flex-1 flex-col p-8 md:p-10", className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export const ModalFooter = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("flex justify-end bg-gray-100 p-4 dark:bg-neutral-900", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Overlay = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
backdropFilter: "blur(0px)",
|
||||
}}
|
||||
className={`bg-opacity-50 fixed inset-0 z-50 h-full w-full bg-black ${className}`}
|
||||
></motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const CloseIcon = () => {
|
||||
const { setOpen } = useModal();
|
||||
return (
|
||||
<button onClick={() => setOpen(false)} className="group absolute top-4 right-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4 text-black transition duration-200 group-hover:scale-125 group-hover:rotate-3 dark:text-white"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M18 6l-12 12" />
|
||||
<path d="M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to detect clicks outside of a component.
|
||||
// Add it in a separate file, I've added here for simplicity
|
||||
export const useOutsideClick = (
|
||||
ref: React.RefObject<HTMLDivElement | null>,
|
||||
callback: (event: MouseEvent | TouchEvent) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent | TouchEvent) => {
|
||||
// DO NOTHING if the element being clicked is the target element or their children
|
||||
if (!ref.current || ref.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
callback(event);
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", listener);
|
||||
document.addEventListener("touchstart", listener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", listener);
|
||||
document.removeEventListener("touchstart", listener);
|
||||
};
|
||||
}, [ref, callback]);
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Tab = {
|
||||
title: string;
|
||||
value: string;
|
||||
content?: string | React.ReactNode | any;
|
||||
};
|
||||
|
||||
export const Tabs = ({
|
||||
tabs: propTabs,
|
||||
containerClassName,
|
||||
activeTabClassName,
|
||||
tabClassName,
|
||||
contentClassName,
|
||||
}: {
|
||||
tabs: Tab[];
|
||||
containerClassName?: string;
|
||||
activeTabClassName?: string;
|
||||
tabClassName?: string;
|
||||
contentClassName?: string;
|
||||
}) => {
|
||||
const [active, setActive] = useState<Tab>(propTabs[0]);
|
||||
const [tabs, setTabs] = useState<Tab[]>(propTabs);
|
||||
|
||||
const moveSelectedTabToTop = (idx: number) => {
|
||||
const newTabs = [...propTabs];
|
||||
const selectedTab = newTabs.splice(idx, 1);
|
||||
newTabs.unshift(selectedTab[0]);
|
||||
setTabs(newTabs);
|
||||
setActive(newTabs[0]);
|
||||
};
|
||||
|
||||
const [hovering, setHovering] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"no-visible-scrollbar relative flex w-full max-w-full flex-row items-center justify-start overflow-auto [perspective:1000px] sm:overflow-visible",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
{propTabs.map((tab, idx) => (
|
||||
<button
|
||||
key={tab.title}
|
||||
onClick={() => {
|
||||
moveSelectedTabToTop(idx);
|
||||
}}
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
className={cn("relative rounded-full px-4 py-2", tabClassName)}
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
}}
|
||||
>
|
||||
{active.value === tab.value && (
|
||||
<motion.div
|
||||
layoutId="clickedbutton"
|
||||
transition={{ type: "spring", bounce: 0.3, duration: 0.6 }}
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-full bg-gray-200 dark:bg-zinc-800",
|
||||
activeTabClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className="relative block text-black dark:text-white">{tab.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<FadeInDiv
|
||||
tabs={tabs}
|
||||
active={active}
|
||||
key={active.value}
|
||||
hovering={hovering}
|
||||
className={cn("mt-32", contentClassName)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FadeInDiv = ({
|
||||
className,
|
||||
tabs,
|
||||
hovering,
|
||||
}: {
|
||||
className?: string;
|
||||
key?: string;
|
||||
tabs: Tab[];
|
||||
active: Tab;
|
||||
hovering?: boolean;
|
||||
}) => {
|
||||
const isActive = (tab: Tab) => {
|
||||
return tab.value === tabs[0].value;
|
||||
};
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
{tabs.map((tab, idx) => (
|
||||
<motion.div
|
||||
key={tab.value}
|
||||
layoutId={tab.value}
|
||||
style={{
|
||||
scale: 1 - idx * 0.1,
|
||||
top: hovering ? idx * -50 : 0,
|
||||
zIndex: -idx,
|
||||
opacity: idx < 3 ? 1 - idx * 0.1 : 0,
|
||||
}}
|
||||
animate={{
|
||||
y: isActive(tab) ? [0, 40, 0] : 0,
|
||||
}}
|
||||
className={cn("absolute top-0 left-0 h-full w-full", className)}
|
||||
>
|
||||
{tab.content}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { motion, useTransform, AnimatePresence, useMotionValue, useSpring } from "motion/react";
|
||||
|
||||
export const AnimatedTooltip = ({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
id: number;
|
||||
name: string;
|
||||
designation: string;
|
||||
image: string;
|
||||
}[];
|
||||
}) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const springConfig = { stiffness: 100, damping: 15 };
|
||||
const x = useMotionValue(0);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
|
||||
const rotate = useSpring(useTransform(x, [-100, 100], [-45, 45]), springConfig);
|
||||
const translateX = useSpring(useTransform(x, [-100, 100], [-50, 50]), springConfig);
|
||||
|
||||
const handleMouseMove = (event: any) => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
const halfWidth = event.target.offsetWidth / 2;
|
||||
x.set(event.nativeEvent.offsetX - halfWidth);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<div
|
||||
className="group relative -mr-4"
|
||||
key={item.name}
|
||||
onMouseEnter={() => setHoveredIndex(item.id)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{hoveredIndex === item.id && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20, scale: 0.6 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 10,
|
||||
},
|
||||
}}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.6 }}
|
||||
style={{
|
||||
translateX: translateX,
|
||||
rotate: rotate,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
className="absolute -top-16 left-1/2 z-50 flex -translate-x-1/2 flex-col items-center justify-center rounded-md bg-black px-4 py-2 text-xs shadow-xl"
|
||||
>
|
||||
<div className="absolute inset-x-10 -bottom-px z-30 h-px w-[20%] bg-gradient-to-r from-transparent via-emerald-500 to-transparent" />
|
||||
<div className="absolute -bottom-px left-10 z-30 h-px w-[40%] bg-gradient-to-r from-transparent via-sky-500 to-transparent" />
|
||||
<div className="relative z-30 text-base font-bold text-white">{item.name}</div>
|
||||
<div className="text-xs text-white">{item.designation}</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<img
|
||||
onMouseMove={handleMouseMove}
|
||||
height={100}
|
||||
width={100}
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="relative !m-0 h-14 w-14 rounded-full border-2 border-white object-cover object-top !p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
interface AuroraBackgroundProps extends React.HTMLProps<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
showRadialGradient?: boolean;
|
||||
}
|
||||
|
||||
export const AuroraBackground = ({
|
||||
className,
|
||||
children,
|
||||
showRadialGradient = true,
|
||||
...props
|
||||
}: AuroraBackgroundProps) => {
|
||||
return (
|
||||
<main>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-bg relative flex h-[100vh] flex-col items-center justify-center bg-zinc-50 text-slate-950 dark:bg-zinc-900",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={
|
||||
{
|
||||
"--aurora":
|
||||
"repeating-linear-gradient(100deg,#3b82f6_10%,#a5b4fc_15%,#93c5fd_20%,#ddd6fe_25%,#60a5fa_30%)",
|
||||
"--dark-gradient":
|
||||
"repeating-linear-gradient(100deg,#000_0%,#000_7%,transparent_10%,transparent_12%,#000_16%)",
|
||||
"--white-gradient":
|
||||
"repeating-linear-gradient(100deg,#fff_0%,#fff_7%,transparent_10%,transparent_12%,#fff_16%)",
|
||||
|
||||
"--blue-300": "#93c5fd",
|
||||
"--blue-400": "#60a5fa",
|
||||
"--blue-500": "#3b82f6",
|
||||
"--indigo-300": "#a5b4fc",
|
||||
"--violet-200": "#ddd6fe",
|
||||
"--black": "#000",
|
||||
"--white": "#fff",
|
||||
"--transparent": "transparent",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
// I'm sorry but this is what peak developer performance looks like // trigger warning
|
||||
className={cn(
|
||||
`after:animate-aurora pointer-events-none absolute -inset-[10px] [background-image:var(--white-gradient),var(--aurora)] [background-size:300%,_200%] [background-position:50%_50%,50%_50%] opacity-50 blur-[10px] invert filter will-change-transform [--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)] [--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--trLine truncated
|
||||
|
||||
showRadialGradient &&
|
||||
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const BackgroundBeams = React.memo(({ className }: { className?: string }) => {
|
||||
const paths = [
|
||||
"M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875",
|
||||
"M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867",
|
||||
"M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859",
|
||||
"M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851",
|
||||
"M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843",
|
||||
"M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835",
|
||||
"M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827",
|
||||
"M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819",
|
||||
"M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811",
|
||||
"M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803",
|
||||
"M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795",
|
||||
"M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787",
|
||||
"M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779",
|
||||
"M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771",
|
||||
"M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763",
|
||||
"M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755",
|
||||
"M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747",
|
||||
"M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739",
|
||||
"M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731",
|
||||
"M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723",
|
||||
"M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715",
|
||||
"M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707",
|
||||
"M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699",
|
||||
"M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691",
|
||||
"M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683",
|
||||
"M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675",
|
||||
"M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667",
|
||||
"M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659",
|
||||
"M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651",
|
||||
"M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643",
|
||||
"M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635",
|
||||
"M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627",
|
||||
"M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619",
|
||||
"M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611",
|
||||
"M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603",
|
||||
"M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595",
|
||||
"M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587",
|
||||
"M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579",
|
||||
"M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571",
|
||||
"M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563",
|
||||
"M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555",
|
||||
"M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547",
|
||||
"M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539",
|
||||
"M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531",
|
||||
"M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523",
|
||||
"M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515",
|
||||
"M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507",
|
||||
"M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499",
|
||||
"M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491",
|
||||
"M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483",
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex h-full w-full items-center justify-center [mask-size:40px] [mask-repeat:no-repeat]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
className="pointer-events-none absolute z-0 h-full w-full"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 696 316"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 8Line truncated
|
||||
stroke="url(#paint0_radial_242_278)"
|
||||
strokeOpacity="0.05"
|
||||
strokeWidth="0.5"
|
||||
></path>
|
||||
|
||||
{paths.map((path, index) => (
|
||||
<motion.path
|
||||
key={`path-` + index}
|
||||
d={path}
|
||||
stroke={`url(#linearGradient-${index})`}
|
||||
strokeOpacity="0.4"
|
||||
strokeWidth="0.5"
|
||||
></motion.path>
|
||||
))}
|
||||
<defs>
|
||||
{paths.map((_path, index) => (
|
||||
<motion.linearGradient
|
||||
id={`linearGradient-${index}`}
|
||||
key={`gradient-${index}`}
|
||||
initial={{
|
||||
x1: "0%",
|
||||
x2: "0%",
|
||||
y1: "0%",
|
||||
y2: "0%",
|
||||
}}
|
||||
animate={{
|
||||
x1: ["0%", "100%"],
|
||||
x2: ["0%", "95%"],
|
||||
y1: ["0%", "100%"],
|
||||
y2: ["0%", `${93 + Math.random() * 8}%`],
|
||||
}}
|
||||
transition={{
|
||||
duration: Math.random() * 10 + 10,
|
||||
ease: "easeInOut",
|
||||
repeat: Infinity,
|
||||
delay: Math.random() * 10,
|
||||
}}
|
||||
>
|
||||
<stop stopColor="#18CCFC" stopOpacity="0"></stop>
|
||||
<stop stopColor="#18CCFC"></stop>
|
||||
<stop offset="32.5%" stopColor="#6344F5"></stop>
|
||||
<stop offset="100%" stopColor="#AE48FF" stopOpacity="0"></stop>
|
||||
</motion.linearGradient>
|
||||
))}
|
||||
|
||||
<radialGradient
|
||||
id="paint0_radial_242_278"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(352 34) rotate(90) scale(555 1560.62)"
|
||||
>
|
||||
<stop offset="0.0666667" stopColor="#d4d4d4"></stop>
|
||||
<stop offset="0.243243" stopColor="#d4d4d4"></stop>
|
||||
<stop offset="0.43594" stopColor="white" stopOpacity="0"></stop>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
BackgroundBeams.displayName = "BackgroundBeams";
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const BoxesCore = ({ className, ...rest }: { className?: string }) => {
|
||||
const rows = new Array(150).fill(1);
|
||||
const cols = new Array(100).fill(1);
|
||||
const colors = [
|
||||
"#93c5fd",
|
||||
"#f9a8d4",
|
||||
"#86efac",
|
||||
"#fde047",
|
||||
"#fca5a5",
|
||||
"#d8b4fe",
|
||||
"#93c5fd",
|
||||
"#a5b4fc",
|
||||
"#c4b5fd",
|
||||
];
|
||||
const getRandomColor = () => {
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(-40%,-60%) skewX(-48deg) skewY(14deg) scale(0.675) rotate(0deg) translateZ(0)`,
|
||||
}}
|
||||
className={cn(
|
||||
"absolute -top-1/4 left-1/4 z-0 flex h-full w-full -translate-x-1/2 -translate-y-1/2 p-4",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{rows.map((_, i) => (
|
||||
<motion.div key={`row` + i} className="relative h-8 w-16 border-l border-slate-700">
|
||||
{cols.map((_, j) => (
|
||||
<motion.div
|
||||
whileHover={{
|
||||
backgroundColor: `${getRandomColor()}`,
|
||||
transition: { duration: 0 },
|
||||
}}
|
||||
animate={{
|
||||
transition: { duration: 2 },
|
||||
}}
|
||||
key={`col` + j}
|
||||
className="relative h-8 w-16 border-t border-r border-slate-700"
|
||||
>
|
||||
{j % 2 === 0 && i % 2 === 0 ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className="pointer-events-none absolute -top-[14px] -left-[22px] h-6 w-10 stroke-[1px] text-slate-700"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
|
||||
</svg>
|
||||
) : null}
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Boxes = React.memo(BoxesCore);
|
||||
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export const BackgroundGradientAnimation = ({
|
||||
gradientBackgroundStart = "rgb(108, 0, 162)",
|
||||
gradientBackgroundEnd = "rgb(0, 17, 82)",
|
||||
firstColor = "18, 113, 255",
|
||||
secondColor = "221, 74, 255",
|
||||
thirdColor = "100, 220, 255",
|
||||
fourthColor = "200, 50, 50",
|
||||
fifthColor = "180, 180, 50",
|
||||
pointerColor = "140, 100, 255",
|
||||
size = "80%",
|
||||
blendingValue = "hard-light",
|
||||
children,
|
||||
className,
|
||||
interactive = true,
|
||||
containerClassName,
|
||||
}: {
|
||||
gradientBackgroundStart?: string;
|
||||
gradientBackgroundEnd?: string;
|
||||
firstColor?: string;
|
||||
secondColor?: string;
|
||||
thirdColor?: string;
|
||||
fourthColor?: string;
|
||||
fifthColor?: string;
|
||||
pointerColor?: string;
|
||||
size?: string;
|
||||
blendingValue?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
interactive?: boolean;
|
||||
containerClassName?: string;
|
||||
}) => {
|
||||
const interactiveRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [curX, setCurX] = useState(0);
|
||||
const [curY, setCurY] = useState(0);
|
||||
const [tgX, setTgX] = useState(0);
|
||||
const [tgY, setTgY] = useState(0);
|
||||
useEffect(() => {
|
||||
document.body.style.setProperty("--gradient-background-start", gradientBackgroundStart);
|
||||
document.body.style.setProperty("--gradient-background-end", gradientBackgroundEnd);
|
||||
document.body.style.setProperty("--first-color", firstColor);
|
||||
document.body.style.setProperty("--second-color", secondColor);
|
||||
document.body.style.setProperty("--third-color", thirdColor);
|
||||
document.body.style.setProperty("--fourth-color", fourthColor);
|
||||
document.body.style.setProperty("--fifth-color", fifthColor);
|
||||
document.body.style.setProperty("--pointer-color", pointerColor);
|
||||
document.body.style.setProperty("--size", size);
|
||||
document.body.style.setProperty("--blending-value", blendingValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function move() {
|
||||
if (!interactiveRef.current) {
|
||||
return;
|
||||
}
|
||||
setCurX(curX + (tgX - curX) / 20);
|
||||
setCurY(curY + (tgY - curY) / 20);
|
||||
interactiveRef.current.style.transform = `translate(${Math.round(
|
||||
curX
|
||||
)}px, ${Math.round(curY)}px)`;
|
||||
}
|
||||
|
||||
move();
|
||||
}, [tgX, tgY]);
|
||||
|
||||
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (interactiveRef.current) {
|
||||
const rect = interactiveRef.current.getBoundingClientRect();
|
||||
setTgX(event.clientX - rect.left);
|
||||
setTgY(event.clientY - rect.top);
|
||||
}
|
||||
};
|
||||
|
||||
const [isSafari, setIsSafari] = useState(false);
|
||||
useEffect(() => {
|
||||
setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative top-0 left-0 h-screen w-screen overflow-hidden bg-[linear-gradient(40deg,var(--gradient-background-start),var(--gradient-background-end))]",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<svg className="hidden">
|
||||
<defs>
|
||||
<filter id="blurMe">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
|
||||
<feColorMatrix
|
||||
in="blur"
|
||||
mode="matrix"
|
||||
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -8"
|
||||
result="goo"
|
||||
/>
|
||||
<feBlend in="SourceGraphic" in2="goo" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
<div className={cn("", className)}>{children}</div>
|
||||
<div
|
||||
className={cn(
|
||||
"gradients-container h-full w-full blur-lg",
|
||||
isSafari ? "blur-2xl" : "[filter:url(#blurMe)_blur(40px)]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_var(--first-color)_0,_var(--first-color)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:center_center]`,
|
||||
`animate-first`,
|
||||
`opacity-100`
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--second-color),_0.8)_0,_rgba(var(--second-color),_0)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:calc(50%-400px)]`,
|
||||
`animate-second`,
|
||||
`opacity-100`
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--third-color),_0.8)_0,_rgba(var(--third-color),_0)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:calc(50%+400px)]`,
|
||||
`animate-third`,
|
||||
`opacity-100`
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--fourth-color),_0.8)_0,_rgba(var(--fourth-color),_0)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:calc(50%-200px)]`,
|
||||
`animate-fourth`,
|
||||
`opacity-70`
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--fifth-color),_0.8)_0,_rgba(var(--fifth-color),_0)_50%)_no-repeat]`,
|
||||
`top-[calc(50%-var(--size)/2)] left-[calc(50%-var(--size)/2)] h-[var(--size)] w-[var(--size)] [mix-blend-mode:var(--blending-value)]`,
|
||||
`[transform-origin:calc(50%-800px)_calc(50%+800px)]`,
|
||||
`animate-fifth`,
|
||||
`opacity-100`
|
||||
)}
|
||||
></div>
|
||||
|
||||
{interactive && (
|
||||
<div
|
||||
ref={interactiveRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
className={cn(
|
||||
`absolute [background:radial-gradient(circle_at_center,_rgba(var(--pointer-color),_0.8)_0,_rgba(var(--pointer-color),_0)_50%)_no-repeat]`,
|
||||
`-top-1/2 -left-1/2 h-full w-full [mix-blend-mode:var(--blending-value)]`,
|
||||
`opacity-70`
|
||||
)}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import React from "react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
export const BackgroundGradient = ({
|
||||
children,
|
||||
className,
|
||||
containerClassName,
|
||||
animate = true,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
animate?: boolean;
|
||||
}) => {
|
||||
const variants = {
|
||||
initial: {
|
||||
backgroundPosition: "0 50%",
|
||||
},
|
||||
animate: {
|
||||
backgroundPosition: ["0, 50%", "100% 50%", "0 50%"],
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div className={cn("group relative p-[4px]", containerClassName)}>
|
||||
<motion.div
|
||||
variants={animate ? variants : undefined}
|
||||
initial={animate ? "initial" : undefined}
|
||||
animate={animate ? "animate" : undefined}
|
||||
transition={
|
||||
animate
|
||||
? {
|
||||
duration: 5,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
style={{
|
||||
backgroundSize: animate ? "400% 400%" : undefined,
|
||||
}}
|
||||
className={cn(
|
||||
"absolute inset-0 z-[1] rounded-3xl opacity-60 blur-xl transition duration-500 will-change-transform group-hover:opacity-100",
|
||||
"bg-[radial-gradient(circle_farthest-side_at_0_100%,#00ccb1,transparent),radial-gradient(circle_farthest-side_at_100%_0,#7b61ff,transparent),radial-gradient(circle_farthest-side_at_100%_100%,#ffc414,transparent),radial-gradient(circle_farthest-side_at_0_0,#1ca0fb,#141316)]"
|
||||
)}
|
||||
/>
|
||||
<motion.div
|
||||
variants={animate ? variants : undefined}
|
||||
initial={animate ? "initial" : undefined}
|
||||
animate={animate ? "animate" : undefined}
|
||||
transition={
|
||||
animate
|
||||
? {
|
||||
duration: 5,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
style={{
|
||||
backgroundSize: animate ? "400% 400%" : undefined,
|
||||
}}
|
||||
className={cn(
|
||||
"absolute inset-0 z-[1] rounded-3xl will-change-transform",
|
||||
"bg-[radial-gradient(circle_farthest-side_at_0_100%,#00ccb1,transparent),radial-gradient(circle_farthest-side_at_100%_0,#7b61ff,transparent),radial-gradient(circle_farthest-side_at_100%_100%,#ffc414,transparent),radial-gradient(circle_farthest-side_at_0_0,#1ca0fb,#141316)]"
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className={cn("relative z-10", className)}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const BentoGrid = ({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto grid max-w-7xl grid-cols-1 gap-4 md:auto-rows-[18rem] md:grid-cols-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BentoGridItem = ({
|
||||
className,
|
||||
title,
|
||||
description,
|
||||
header,
|
||||
icon,
|
||||
}: {
|
||||
className?: string;
|
||||
title?: string | React.ReactNode;
|
||||
description?: string | React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group/bento row-span-1 flex flex-col justify-between space-y-4 rounded-xl border border-neutral-200 bg-white p-4 shadow-input transition duration-200 hover:shadow-xl dark:border-white/[0.2] dark:bg-black dark:shadow-none",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{header}
|
||||
<div className="transition duration-200 group-hover/bento:translate-x-2">
|
||||
{icon}
|
||||
<div className="mt-2 mb-2 font-sans font-bold text-neutral-600 dark:text-neutral-200">
|
||||
{title}
|
||||
</div>
|
||||
<div className="font-sans text-xs font-normal text-neutral-600 dark:text-neutral-300">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export const HoverEffect = ({
|
||||
items,
|
||||
className,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
}[];
|
||||
className?: string;
|
||||
}) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<div className={cn("grid grid-cols-1 py-10 md:grid-cols-2 lg:grid-cols-3", className)}>
|
||||
{items.map((item, idx) => (
|
||||
<a
|
||||
href={item?.link}
|
||||
key={item?.link}
|
||||
className="group relative block h-full w-full p-2"
|
||||
onMouseEnter={() => setHoveredIndex(idx)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{hoveredIndex === idx && (
|
||||
<motion.span
|
||||
className="absolute inset-0 block h-full w-full rounded-3xl bg-neutral-200 dark:bg-slate-800/[0.8]"
|
||||
layoutId="hoverBackground"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
transition: { duration: 0.15 },
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { duration: 0.15, delay: 0.2 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Card>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
<CardDescription>{item.description}</CardDescription>
|
||||
</Card>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Card = ({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative z-20 h-full w-full overflow-hidden rounded-2xl border border-transparent bg-black p-4 group-hover:border-slate-700 dark:border-white/[0.2]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="relative z-50">
|
||||
<div className="p-4">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const CardTitle = ({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<h4 className={cn("mt-4 font-bold tracking-wide text-zinc-100", className)}>{children}</h4>
|
||||
);
|
||||
};
|
||||
export const CardDescription = ({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<p className={cn("mt-8 text-sm leading-relaxed tracking-wide text-zinc-400", className)}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useMotionValue, motion, useMotionTemplate } from "motion/react";
|
||||
import React, { MouseEvent as ReactMouseEvent, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const CardSpotlight = ({
|
||||
children,
|
||||
radius = 350,
|
||||
color = "#262626",
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
radius?: number;
|
||||
color?: string;
|
||||
children: React.ReactNode;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||
const mouseX = useMotionValue(0);
|
||||
const mouseY = useMotionValue(0);
|
||||
function handleMouseMove({ currentTarget, clientX, clientY }: ReactMouseEvent<HTMLDivElement>) {
|
||||
const { left, top } = currentTarget.getBoundingClientRect();
|
||||
|
||||
mouseX.set(clientX - left);
|
||||
mouseY.set(clientY - top);
|
||||
}
|
||||
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const handleMouseEnter = () => setIsHovering(true);
|
||||
const handleMouseLeave = () => setIsHovering(false);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group/spotlight relative rounded-md border border-neutral-800 bg-black p-10 dark:border-neutral-800",
|
||||
className
|
||||
)}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
{...props}
|
||||
>
|
||||
<motion.div
|
||||
className="pointer-events-none absolute -inset-px z-0 rounded-md opacity-0 transition duration-300 group-hover/spotlight:opacity-100"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
maskImage: useMotionTemplate`
|
||||
radial-gradient(
|
||||
${radius}px circle at ${mouseX}px ${mouseY}px,
|
||||
white,
|
||||
transparent 80%
|
||||
)
|
||||
`,
|
||||
}}
|
||||
>
|
||||
{isHovering && (
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_var(--spotlight-x,50%)_var(--spotlight-y,50%),rgba(59,130,246,0.35),transparent_60%)]" />
|
||||
)}
|
||||
</motion.div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
type Card = {
|
||||
id: number;
|
||||
name: string;
|
||||
designation: string;
|
||||
content: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CardStack = ({
|
||||
items,
|
||||
offset,
|
||||
scaleFactor,
|
||||
}: {
|
||||
items: Card[];
|
||||
offset?: number;
|
||||
scaleFactor?: number;
|
||||
}) => {
|
||||
const CARD_OFFSET = offset || 10;
|
||||
const SCALE_FACTOR = scaleFactor || 0.06;
|
||||
const [cards, setCards] = useState<Card[]>(items);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCards((prevCards: Card[]) => {
|
||||
const newArray = [...prevCards]; // create a copy of the array
|
||||
newArray.unshift(newArray.pop()!); // move the last element to the front
|
||||
return newArray;
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative h-60 w-60 md:h-60 md:w-96">
|
||||
{cards.map((card, index) => {
|
||||
return (
|
||||
<motion.div
|
||||
key={card.id}
|
||||
className="absolute flex h-60 w-60 flex-col justify-between rounded-3xl border border-neutral-200 bg-white p-4 shadow-xl shadow-black/[0.1] md:h-60 md:w-96 dark:border-white/[0.1] dark:bg-black dark:shadow-white/[0.05]"
|
||||
style={{
|
||||
transformOrigin: "top center",
|
||||
}}
|
||||
animate={{
|
||||
top: index * -CARD_OFFSET,
|
||||
scale: 1 - index * SCALE_FACTOR, // decrease scale for cards that are behind
|
||||
zIndex: cards.length - index, // decrease z-index for the cards that are behind
|
||||
}}
|
||||
>
|
||||
<div className="font-normal text-neutral-700 dark:text-neutral-200">{card.content}</div>
|
||||
<div>
|
||||
<p className="font-medium text-neutral-500 dark:text-white">{card.name}</p>
|
||||
<p className="font-normal text-neutral-400 dark:text-neutral-200">
|
||||
{card.designation}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { codeToHtml } from "shiki";
|
||||
|
||||
const DEFAULT_THEME = "github-dark";
|
||||
|
||||
export async function CodeBlock({
|
||||
code,
|
||||
lang = "text",
|
||||
theme = DEFAULT_THEME,
|
||||
className,
|
||||
}: {
|
||||
code: string;
|
||||
lang?: string;
|
||||
theme?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const html = await codeToHtml(code, {
|
||||
lang,
|
||||
theme,
|
||||
});
|
||||
|
||||
// Safe: shiki escapes all input before emitting HTML (no raw user content
|
||||
// reaches innerHTML), so no sanitizer is required here.
|
||||
return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
import React, { useRef } from "react";
|
||||
import { useScroll, useTransform, motion, MotionValue } from "motion/react";
|
||||
|
||||
export const ContainerScroll = ({
|
||||
titleComponent,
|
||||
children,
|
||||
}: {
|
||||
titleComponent: string | React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
});
|
||||
const [isMobile, setIsMobile] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
};
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
return () => {
|
||||
window.removeEventListener("resize", checkMobile);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scaleDimensions = () => {
|
||||
return isMobile ? [0.7, 0.9] : [1.05, 1];
|
||||
};
|
||||
|
||||
const rotate = useTransform(scrollYProgress, [0, 1], [20, 0]);
|
||||
const scale = useTransform(scrollYProgress, [0, 1], scaleDimensions());
|
||||
const translate = useTransform(scrollYProgress, [0, 1], [0, -100]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-[60rem] items-center justify-center p-2 md:h-[80rem] md:p-20"
|
||||
ref={containerRef}
|
||||
>
|
||||
<div
|
||||
className="relative w-full py-10 md:py-40"
|
||||
style={{
|
||||
perspective: "1000px",
|
||||
}}
|
||||
>
|
||||
<Header translate={translate} titleComponent={titleComponent} />
|
||||
<Card rotate={rotate} translate={translate} scale={scale}>
|
||||
{children}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = ({ translate, titleComponent }: any) => {
|
||||
return (
|
||||
<motion.div
|
||||
style={{
|
||||
translateY: translate,
|
||||
}}
|
||||
className="div mx-auto max-w-5xl text-center"
|
||||
>
|
||||
{titleComponent}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Card = ({
|
||||
rotate,
|
||||
scale,
|
||||
children,
|
||||
}: {
|
||||
rotate: MotionValue<number>;
|
||||
scale: MotionValue<number>;
|
||||
translate: MotionValue<number>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<motion.div
|
||||
style={{
|
||||
rotateX: rotate,
|
||||
scale,
|
||||
boxShadow:
|
||||
"0 0 #0000004d, 0 9px 20px #0000004a, 0 37px 37px #00000042, 0 84px 50px #00000026, 0 149px 60px #0000000a, 0 233px 65px #00000003",
|
||||
}}
|
||||
className="mx-auto -mt-12 h-[30rem] w-full max-w-5xl rounded-[30px] border-4 border-[#6C6C6C] bg-[#222222] p-2 shadow-2xl md:h-[40rem] md:p-6"
|
||||
>
|
||||
<div className="h-full w-full overflow-hidden rounded-2xl bg-gray-100 md:rounded-2xl md:p-4 dark:bg-zinc-900">
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
"use client";
|
||||
import React, { useEffect, useId, useState } from "react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SparklesCore } from "@/components/ui/sparkles";
|
||||
|
||||
export const Cover = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
const [beamPositions, setBeamPositions] = useState<number[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
setContainerWidth(ref.current?.clientWidth ?? 0);
|
||||
|
||||
const height = ref.current?.clientHeight ?? 0;
|
||||
const numberOfBeams = Math.floor(height / 10); // Adjust the divisor to control the spacing
|
||||
const positions = Array.from(
|
||||
{ length: numberOfBeams },
|
||||
(_, i) => (i + 1) * (height / (numberOfBeams + 1))
|
||||
);
|
||||
setBeamPositions(positions);
|
||||
}
|
||||
}, [ref.current]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
ref={ref}
|
||||
className="group/cover relative inline-block rounded-sm bg-neutral-100 px-2 py-2 transition duration-200 hover:bg-neutral-900 dark:bg-neutral-900"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{hovered && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
opacity: {
|
||||
duration: 0.2,
|
||||
},
|
||||
}}
|
||||
className="absolute inset-0 h-full w-full overflow-hidden"
|
||||
>
|
||||
<motion.div
|
||||
animate={{
|
||||
translateX: ["-50%", "0%"],
|
||||
}}
|
||||
transition={{
|
||||
translateX: {
|
||||
duration: 10,
|
||||
ease: "linear",
|
||||
repeat: Infinity,
|
||||
},
|
||||
}}
|
||||
className="flex h-full w-[200%]"
|
||||
>
|
||||
<SparklesCore
|
||||
background="transparent"
|
||||
minSize={0.4}
|
||||
maxSize={1}
|
||||
particleDensity={500}
|
||||
className="h-full w-full"
|
||||
particleColor="#FFFFFF"
|
||||
/>
|
||||
<SparklesCore
|
||||
background="transparent"
|
||||
minSize={0.4}
|
||||
maxSize={1}
|
||||
particleDensity={500}
|
||||
className="h-full w-full"
|
||||
particleColor="#FFFFFF"
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{beamPositions.map((position, index) => (
|
||||
<Beam
|
||||
key={index}
|
||||
hovered={hovered}
|
||||
duration={Math.random() * 2 + 1}
|
||||
delay={Math.random() * 2 + 1}
|
||||
width={containerWidth}
|
||||
style={{
|
||||
top: `${position}px`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<motion.span
|
||||
key={String(hovered)}
|
||||
animate={{
|
||||
scale: hovered ? 0.8 : 1,
|
||||
x: hovered ? [0, -30, 30, -30, 30, 0] : 0,
|
||||
y: hovered ? [0, 30, -30, 30, -30, 0] : 0,
|
||||
}}
|
||||
exit={{
|
||||
filter: "none",
|
||||
scale: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
x: {
|
||||
duration: 0.2,
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
},
|
||||
y: {
|
||||
duration: 0.2,
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
},
|
||||
scale: {
|
||||
duration: 0.2,
|
||||
},
|
||||
filter: {
|
||||
duration: 0.2,
|
||||
},
|
||||
}}
|
||||
className={cn(
|
||||
"relative z-20 inline-block text-neutral-900 transition duration-200 group-hover/cover:text-white dark:text-white",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</motion.span>
|
||||
<CircleIcon className="absolute -top-[2px] -right-[2px]" />
|
||||
<CircleIcon className="absolute -right-[2px] -bottom-[2px]" />
|
||||
<CircleIcon className="absolute -top-[2px] -left-[2px]" />
|
||||
<CircleIcon className="absolute -bottom-[2px] -left-[2px]" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Beam = ({
|
||||
className,
|
||||
delay,
|
||||
duration,
|
||||
hovered,
|
||||
width = 600,
|
||||
...svgProps
|
||||
}: {
|
||||
className?: string;
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
hovered?: boolean;
|
||||
width?: number;
|
||||
} & React.ComponentProps<typeof motion.svg>) => {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<motion.svg
|
||||
width={width ?? "600"}
|
||||
height="1"
|
||||
viewBox={`0 0 ${width ?? "600"} 1`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn("absolute inset-x-0 w-full", className)}
|
||||
{...svgProps}
|
||||
>
|
||||
<motion.path d={`M0 0.5H${width ?? "600"}`} stroke={`url(#svgGradient-${id})`} />
|
||||
|
||||
<defs>
|
||||
<motion.linearGradient
|
||||
id={`svgGradient-${id}`}
|
||||
key={String(hovered)}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
initial={{
|
||||
x1: "0%",
|
||||
x2: hovered ? "-10%" : "-5%",
|
||||
y1: 0,
|
||||
y2: 0,
|
||||
}}
|
||||
animate={{
|
||||
x1: "110%",
|
||||
x2: hovered ? "100%" : "105%",
|
||||
y1: 0,
|
||||
y2: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: hovered ? 0.5 : (duration ?? 2),
|
||||
ease: "linear",
|
||||
repeat: Infinity,
|
||||
delay: hovered ? Math.random() * (1 - 0.2) + 0.2 : 0,
|
||||
repeatDelay: hovered ? Math.random() * (2 - 1) + 1 : (delay ?? 1),
|
||||
}}
|
||||
>
|
||||
<stop stopColor="#2EB9DF" stopOpacity="0" />
|
||||
<stop stopColor="#3b82f6" />
|
||||
<stop offset="1" stopColor="#3b82f6" stopOpacity="0" />
|
||||
</motion.linearGradient>
|
||||
</defs>
|
||||
</motion.svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const CircleIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`group pointer-events-none h-2 w-2 animate-pulse rounded-full bg-neutral-600 opacity-20 group-hover/cover:hidden group-hover/cover:bg-white group-hover/cover:opacity-100 dark:bg-white`,
|
||||
className
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const DirectionAwareHover = ({
|
||||
imageUrl,
|
||||
children,
|
||||
childrenClassName,
|
||||
imageClassName,
|
||||
className,
|
||||
}: {
|
||||
imageUrl: string;
|
||||
children: React.ReactNode | string;
|
||||
childrenClassName?: string;
|
||||
imageClassName?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [direction, setDirection] = useState<"top" | "bottom" | "left" | "right" | string>("left");
|
||||
|
||||
const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const direction = getDirection(event, ref.current);
|
||||
console.log("direction", direction);
|
||||
switch (direction) {
|
||||
case 0:
|
||||
setDirection("top");
|
||||
break;
|
||||
case 1:
|
||||
setDirection("right");
|
||||
break;
|
||||
case 2:
|
||||
setDirection("bottom");
|
||||
break;
|
||||
case 3:
|
||||
setDirection("left");
|
||||
break;
|
||||
default:
|
||||
setDirection("left");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const getDirection = (ev: React.MouseEvent<HTMLDivElement, MouseEvent>, obj: HTMLElement) => {
|
||||
const { width: w, height: h, left, top } = obj.getBoundingClientRect();
|
||||
const x = ev.clientX - left - (w / 2) * (w > h ? h / w : 1);
|
||||
const y = ev.clientY - top - (h / 2) * (h > w ? w / h : 1);
|
||||
const d = Math.round(Math.atan2(y, x) / 1.57079633 + 5) % 4;
|
||||
return d;
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
onMouseEnter={handleMouseEnter}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group/card relative h-60 w-60 overflow-hidden rounded-lg bg-transparent md:h-96 md:w-96",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
className="relative h-full w-full"
|
||||
initial="initial"
|
||||
whileHover={direction}
|
||||
exit="exit"
|
||||
>
|
||||
<motion.div className="absolute inset-0 z-10 hidden h-full w-full bg-black/40 transition duration-500 group-hover/card:block" />
|
||||
<motion.div
|
||||
variants={variants}
|
||||
className="relative h-full w-full bg-gray-50 dark:bg-black"
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="image"
|
||||
className={cn("h-full w-full scale-[1.15] object-cover", imageClassName)}
|
||||
width="1000"
|
||||
height="1000"
|
||||
src={imageUrl}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
variants={textVariants}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className={cn("absolute bottom-4 left-4 z-40 text-white", childrenClassName)}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const variants = {
|
||||
initial: {
|
||||
x: 0,
|
||||
},
|
||||
|
||||
exit: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
top: {
|
||||
y: 20,
|
||||
},
|
||||
bottom: {
|
||||
y: -20,
|
||||
},
|
||||
left: {
|
||||
x: 20,
|
||||
},
|
||||
right: {
|
||||
x: -20,
|
||||
},
|
||||
};
|
||||
|
||||
const textVariants = {
|
||||
initial: {
|
||||
y: 0,
|
||||
x: 0,
|
||||
opacity: 0,
|
||||
},
|
||||
exit: {
|
||||
y: 0,
|
||||
x: 0,
|
||||
opacity: 0,
|
||||
},
|
||||
top: {
|
||||
y: -20,
|
||||
opacity: 1,
|
||||
},
|
||||
bottom: {
|
||||
y: 2,
|
||||
opacity: 1,
|
||||
},
|
||||
left: {
|
||||
x: -2,
|
||||
opacity: 1,
|
||||
},
|
||||
right: {
|
||||
x: 20,
|
||||
opacity: 1,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { motion, useInView } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type EncryptedTextProps = {
|
||||
text: string;
|
||||
className?: string;
|
||||
/**
|
||||
* Time in milliseconds between revealing each subsequent real character.
|
||||
* Lower is faster. Defaults to 50ms per character.
|
||||
*/
|
||||
revealDelayMs?: number;
|
||||
/** Optional custom character set to use for the gibberish effect. */
|
||||
charset?: string;
|
||||
/**
|
||||
* Time in milliseconds between gibberish flips for unrevealed characters.
|
||||
* Lower is more jittery. Defaults to 50ms.
|
||||
*/
|
||||
flipDelayMs?: number;
|
||||
/** CSS class for styling the encrypted/scrambled characters */
|
||||
encryptedClassName?: string;
|
||||
/** CSS class for styling the revealed characters */
|
||||
revealedClassName?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_CHARSET =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-={}[];:,.<>/?";
|
||||
|
||||
function generateRandomCharacter(charset: string): string {
|
||||
const index = Math.floor(Math.random() * charset.length);
|
||||
return charset.charAt(index);
|
||||
}
|
||||
|
||||
function generateGibberishPreservingSpaces(original: string, charset: string): string {
|
||||
if (!original) return "";
|
||||
let result = "";
|
||||
for (let i = 0; i < original.length; i += 1) {
|
||||
const ch = original[i];
|
||||
result += ch === " " ? " " : generateRandomCharacter(charset);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const EncryptedText: React.FC<EncryptedTextProps> = ({
|
||||
text,
|
||||
className,
|
||||
revealDelayMs = 50,
|
||||
charset = DEFAULT_CHARSET,
|
||||
flipDelayMs = 50,
|
||||
encryptedClassName,
|
||||
revealedClassName,
|
||||
}) => {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const isInView = useInView(ref, { once: true });
|
||||
|
||||
const [revealCount, setRevealCount] = useState<number>(0);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
const startTimeRef = useRef<number>(0);
|
||||
const lastFlipTimeRef = useRef<number>(0);
|
||||
const scrambleCharsRef = useRef<string[]>(
|
||||
text ? generateGibberishPreservingSpaces(text, charset).split("") : []
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView) return;
|
||||
|
||||
// Reset state for a fresh animation whenever dependencies change
|
||||
const initial = text ? generateGibberishPreservingSpaces(text, charset) : "";
|
||||
scrambleCharsRef.current = initial.split("");
|
||||
startTimeRef.current = performance.now();
|
||||
lastFlipTimeRef.current = startTimeRef.current;
|
||||
setRevealCount(0);
|
||||
|
||||
let isCancelled = false;
|
||||
|
||||
const update = (now: number) => {
|
||||
if (isCancelled) return;
|
||||
|
||||
const elapsedMs = now - startTimeRef.current;
|
||||
const totalLength = text.length;
|
||||
const currentRevealCount = Math.min(
|
||||
totalLength,
|
||||
Math.floor(elapsedMs / Math.max(1, revealDelayMs))
|
||||
);
|
||||
|
||||
setRevealCount(currentRevealCount);
|
||||
|
||||
if (currentRevealCount >= totalLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-randomize unrevealed scramble characters on an interval
|
||||
const timeSinceLastFlip = now - lastFlipTimeRef.current;
|
||||
if (timeSinceLastFlip >= Math.max(0, flipDelayMs)) {
|
||||
for (let index = 0; index < totalLength; index += 1) {
|
||||
if (index >= currentRevealCount) {
|
||||
if (text[index] !== " ") {
|
||||
scrambleCharsRef.current[index] = generateRandomCharacter(charset);
|
||||
} else {
|
||||
scrambleCharsRef.current[index] = " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
lastFlipTimeRef.current = now;
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(update);
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
if (animationFrameRef.current !== null) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [isInView, text, revealDelayMs, charset, flipDelayMs]);
|
||||
|
||||
if (!text) return null;
|
||||
|
||||
return (
|
||||
<motion.span ref={ref} className={cn(className)} aria-label={text} role="text">
|
||||
{text.split("").map((char, index) => {
|
||||
const isRevealed = index < revealCount;
|
||||
const displayChar = isRevealed
|
||||
? char
|
||||
: char === " "
|
||||
? " "
|
||||
: (scrambleCharsRef.current[index] ?? generateRandomCharacter(charset));
|
||||
|
||||
return (
|
||||
<span key={index} className={cn(isRevealed ? revealedClassName : encryptedClassName)}>
|
||||
{displayChar}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</motion.span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
import { useMotionValue } from "motion/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useMotionTemplate, motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const EvervaultCard = ({ text, className }: { text?: string; className?: string }) => {
|
||||
const mouseX = useMotionValue(0);
|
||||
const mouseY = useMotionValue(0);
|
||||
|
||||
const [randomString, setRandomString] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const str = generateRandomString(1500);
|
||||
setRandomString(str);
|
||||
}, []);
|
||||
|
||||
function onMouseMove({ currentTarget, clientX, clientY }: any) {
|
||||
const { left, top } = currentTarget.getBoundingClientRect();
|
||||
mouseX.set(clientX - left);
|
||||
mouseY.set(clientY - top);
|
||||
|
||||
const str = generateRandomString(1500);
|
||||
setRandomString(str);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex aspect-square h-full w-full items-center justify-center bg-transparent p-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onMouseMove={onMouseMove}
|
||||
className="group/card relative flex h-full w-full items-center justify-center overflow-hidden rounded-3xl bg-transparent"
|
||||
>
|
||||
<CardPattern mouseX={mouseX} mouseY={mouseY} randomString={randomString} />
|
||||
<div className="relative z-10 flex items-center justify-center">
|
||||
<div className="relative flex h-44 w-44 items-center justify-center rounded-full text-4xl font-bold text-white">
|
||||
<div className="absolute h-full w-full rounded-full bg-white/[0.8] blur-sm dark:bg-black/[0.8]" />
|
||||
<span className="z-20 text-black dark:text-white">{text}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function CardPattern({ mouseX, mouseY, randomString }: any) {
|
||||
const maskImage = useMotionTemplate`radial-gradient(250px at ${mouseX}px ${mouseY}px, white, transparent)`;
|
||||
const style = { maskImage, WebkitMaskImage: maskImage };
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none">
|
||||
<div className="absolute inset-0 rounded-2xl [mask-image:linear-gradient(white,transparent)] group-hover/card:opacity-50"></div>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-green-500 to-blue-700 opacity-0 backdrop-blur-xl transition duration-500 group-hover/card:opacity-100"
|
||||
style={style}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-2xl opacity-0 mix-blend-overlay group-hover/card:opacity-100"
|
||||
style={style}
|
||||
>
|
||||
<p className="absolute inset-x-0 h-full font-mono text-xs font-bold break-words whitespace-pre-wrap text-white transition duration-500">
|
||||
{randomString}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
export const generateRandomString = (length: number) => {
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const Icon = ({ className, ...rest }: any) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
{...rest}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const FlipWords = ({
|
||||
words,
|
||||
duration = 3000,
|
||||
className,
|
||||
}: {
|
||||
words: string[];
|
||||
duration?: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [currentWord, setCurrentWord] = useState(words[0]);
|
||||
const [isAnimating, setIsAnimating] = useState<boolean>(false);
|
||||
|
||||
// thanks for the fix Julian - https://github.com/Julian-AT
|
||||
const startAnimation = useCallback(() => {
|
||||
const word = words[words.indexOf(currentWord) + 1] || words[0];
|
||||
setCurrentWord(word);
|
||||
setIsAnimating(true);
|
||||
}, [currentWord, words]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnimating)
|
||||
setTimeout(() => {
|
||||
startAnimation();
|
||||
}, duration);
|
||||
}, [isAnimating, duration, startAnimation]);
|
||||
|
||||
return (
|
||||
<AnimatePresence
|
||||
onExitComplete={() => {
|
||||
setIsAnimating(false);
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 10,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: -40,
|
||||
x: 40,
|
||||
filter: "blur(8px)",
|
||||
scale: 2,
|
||||
position: "absolute",
|
||||
}}
|
||||
className={cn(
|
||||
"relative z-10 inline-block px-2 text-left text-neutral-900 dark:text-neutral-100",
|
||||
className
|
||||
)}
|
||||
key={currentWord}
|
||||
>
|
||||
{/* edit suggested by Sajal: https://x.com/DewanganSajal */}
|
||||
{currentWord.split(" ").map((word, wordIndex) => (
|
||||
<motion.span
|
||||
key={word + wordIndex}
|
||||
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
|
||||
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||
transition={{
|
||||
delay: wordIndex * 0.3,
|
||||
duration: 0.3,
|
||||
}}
|
||||
className="inline-block whitespace-nowrap"
|
||||
>
|
||||
{word.split("").map((letter, letterIndex) => (
|
||||
<motion.span
|
||||
key={word + letterIndex}
|
||||
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
|
||||
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||
transition={{
|
||||
delay: wordIndex * 0.3 + letterIndex * 0.05,
|
||||
duration: 0.2,
|
||||
}}
|
||||
className="inline-block"
|
||||
>
|
||||
{letter}
|
||||
</motion.span>
|
||||
))}
|
||||
<span className="inline-block"> </span>
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
/**
|
||||
* Note: Use position fixed according to your needs
|
||||
* Desktop navbar is better positioned at the bottom
|
||||
* Mobile navbar is better positioned at bottom right.
|
||||
**/
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { IconLayoutNavbarCollapse } from "@tabler/icons-react";
|
||||
import {
|
||||
AnimatePresence,
|
||||
MotionValue,
|
||||
motion,
|
||||
useMotionValue,
|
||||
useSpring,
|
||||
useTransform,
|
||||
} from "motion/react";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export const FloatingDock = ({
|
||||
items,
|
||||
desktopClassName,
|
||||
mobileClassName,
|
||||
}: {
|
||||
items: { title: string; icon: React.ReactNode; href: string }[];
|
||||
desktopClassName?: string;
|
||||
mobileClassName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<FloatingDockDesktop items={items} className={desktopClassName} />
|
||||
<FloatingDockMobile items={items} className={mobileClassName} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FloatingDockMobile = ({
|
||||
items,
|
||||
className,
|
||||
}: {
|
||||
items: { title: string; icon: React.ReactNode; href: string }[];
|
||||
className?: string;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div className={cn("relative block md:hidden", className)}>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
layoutId="nav"
|
||||
className="absolute inset-x-0 bottom-full mb-2 flex flex-col gap-2"
|
||||
>
|
||||
{items.map((item, idx) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
transition: {
|
||||
delay: idx * 0.05,
|
||||
},
|
||||
}}
|
||||
transition={{ delay: (items.length - 1 - idx) * 0.05 }}
|
||||
>
|
||||
<a
|
||||
href={item.href}
|
||||
key={item.title}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 dark:bg-neutral-900"
|
||||
>
|
||||
<div className="h-4 w-4">{item.icon}</div>
|
||||
</a>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 dark:bg-neutral-800"
|
||||
>
|
||||
<IconLayoutNavbarCollapse className="h-5 w-5 text-neutral-500 dark:text-neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FloatingDockDesktop = ({
|
||||
items,
|
||||
className,
|
||||
}: {
|
||||
items: { title: string; icon: React.ReactNode; href: string }[];
|
||||
className?: string;
|
||||
}) => {
|
||||
const mouseX = useMotionValue(Infinity);
|
||||
return (
|
||||
<motion.div
|
||||
onMouseMove={(e) => mouseX.set(e.pageX)}
|
||||
onMouseLeave={() => mouseX.set(Infinity)}
|
||||
className={cn(
|
||||
"mx-auto hidden h-16 items-end gap-4 rounded-2xl bg-gray-50 px-4 pb-3 md:flex dark:bg-neutral-900",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<IconContainer mouseX={mouseX} key={item.title} {...item} />
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
function IconContainer({
|
||||
mouseX,
|
||||
title,
|
||||
icon,
|
||||
href,
|
||||
}: {
|
||||
mouseX: MotionValue;
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
href: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const distance = useTransform(mouseX, (val) => {
|
||||
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
|
||||
|
||||
return val - bounds.x - bounds.width / 2;
|
||||
});
|
||||
|
||||
const widthTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
|
||||
const heightTransform = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
|
||||
|
||||
const widthTransformIcon = useTransform(distance, [-150, 0, 150], [20, 40, 20]);
|
||||
const heightTransformIcon = useTransform(distance, [-150, 0, 150], [20, 40, 20]);
|
||||
|
||||
const width = useSpring(widthTransform, {
|
||||
mass: 0.1,
|
||||
stiffness: 150,
|
||||
damping: 12,
|
||||
});
|
||||
const height = useSpring(heightTransform, {
|
||||
mass: 0.1,
|
||||
stiffness: 150,
|
||||
damping: 12,
|
||||
});
|
||||
|
||||
const widthIcon = useSpring(widthTransformIcon, {
|
||||
mass: 0.1,
|
||||
stiffness: 150,
|
||||
damping: 12,
|
||||
});
|
||||
const heightIcon = useSpring(heightTransformIcon, {
|
||||
mass: 0.1,
|
||||
stiffness: 150,
|
||||
damping: 12,
|
||||
});
|
||||
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<a href={href}>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
style={{ width, height }}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
className="relative flex aspect-square items-center justify-center rounded-full bg-gray-200 dark:bg-neutral-800"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{hovered && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, x: "-50%" }}
|
||||
animate={{ opacity: 1, y: 0, x: "-50%" }}
|
||||
exit={{ opacity: 0, y: 2, x: "-50%" }}
|
||||
className="absolute -top-8 left-1/2 w-fit rounded-md border border-gray-200 bg-gray-100 px-2 py-0.5 text-xs whitespace-pre text-neutral-700 dark:border-neutral-900 dark:bg-neutral-800 dark:text-white"
|
||||
>
|
||||
{title}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
style={{ width: widthIcon, height: heightIcon }}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
{icon}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Card = React.memo(
|
||||
({
|
||||
card,
|
||||
index,
|
||||
hovered,
|
||||
setHovered,
|
||||
}: {
|
||||
card: any;
|
||||
index: number;
|
||||
hovered: number | null;
|
||||
setHovered: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
}) => (
|
||||
<div
|
||||
onMouseEnter={() => setHovered(index)}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
className={cn(
|
||||
"relative h-60 w-full overflow-hidden rounded-lg bg-gray-100 transition-all duration-300 ease-out md:h-96 dark:bg-neutral-900",
|
||||
hovered !== null && hovered !== index && "scale-[0.98] blur-sm"
|
||||
)}
|
||||
>
|
||||
<img src={card.src} alt={card.title} className="absolute inset-0 object-cover" />
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex items-end bg-black/50 px-4 py-8 transition-opacity duration-300",
|
||||
hovered === index ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
>
|
||||
<div className="bg-gradient-to-b from-neutral-50 to-neutral-200 bg-clip-text text-xl font-medium text-transparent md:text-2xl">
|
||||
{card.title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
Card.displayName = "Card";
|
||||
|
||||
type Card = {
|
||||
title: string;
|
||||
src: string;
|
||||
};
|
||||
|
||||
export function FocusCards({ cards }: { cards: Card[] }) {
|
||||
const [hovered, setHovered] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid w-full max-w-5xl grid-cols-1 gap-10 md:grid-cols-3 md:px-8">
|
||||
{cards.map((card, index) => (
|
||||
<Card
|
||||
key={card.title}
|
||||
card={card}
|
||||
index={index}
|
||||
hovered={hovered}
|
||||
setHovered={setHovered}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useRef } from "react";
|
||||
|
||||
export const GlareCard = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const isPointerInside = useRef(false);
|
||||
const refElement = useRef<HTMLDivElement>(null);
|
||||
const state = useRef({
|
||||
glare: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
},
|
||||
background: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
},
|
||||
rotate: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
const containerStyle = {
|
||||
"--m-x": "50%",
|
||||
"--m-y": "50%",
|
||||
"--r-x": "0deg",
|
||||
"--r-y": "0deg",
|
||||
"--bg-x": "50%",
|
||||
"--bg-y": "50%",
|
||||
"--duration": "300ms",
|
||||
"--foil-size": "100%",
|
||||
"--opacity": "0",
|
||||
"--radius": "48px",
|
||||
"--easing": "ease",
|
||||
"--transition": "var(--duration) var(--easing)",
|
||||
} as any;
|
||||
|
||||
const backgroundStyle = {
|
||||
"--step": "5%",
|
||||
"--foil-svg": `url("data:image/svg+xml,%3Csvg width='26' height='26' viewBox='0 0 26 26' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.99994 3.419C2.99994 3.419 21.6142 7.43646 22.7921 12.153C23.97 16.8695 3.41838 23.0306 3.41838 23.0306' stroke='white' stroke-width='5' stroke-miterlimit='3.86874' stroke-linecap='round' style='mix-blend-mode:darken'/%3E%3C/svg%3E")`,
|
||||
"--pattern": "var(--foil-svg) center/100% no-repeat",
|
||||
"--rainbow":
|
||||
"repeating-linear-gradient( 0deg,rgb(255,119,115) calc(var(--step) * 1),rgba(255,237,95,1) calc(var(--step) * 2),rgba(168,255,95,1) calc(var(--step) * 3),rgba(131,255,247,1) calc(var(--step) * 4),rgba(120,148,255,1) calc(var(--step) * 5),rgb(216,117,255) calc(var(--step) * 6),rgb(255,119,115) calc(var(--step) * 7) ) 0% var(--bg-y)/200% 700% no-repeat",
|
||||
"--diagonal":
|
||||
"repeating-linear-gradient( 128deg,#0e152e 0%,hsl(180,10%,60%) 3.8%,hsl(180,10%,60%) 4.5%,hsl(180,10%,60%) 5.2%,#0e152e 10%,#0e152e 12% ) var(--bg-x) var(--bg-y)/300% no-repeat",
|
||||
"--shade":
|
||||
"radial-gradient( farthest-corner circle at var(--m-x) var(--m-y),rgba(255,255,255,0.1) 12%,rgba(255,255,255,0.15) 20%,rgba(255,255,255,0.25) 120% ) var(--bg-x) var(--bg-y)/300% no-repeat",
|
||||
backgroundBlendMode: "hue, hue, hue, overlay",
|
||||
};
|
||||
|
||||
const updateStyles = () => {
|
||||
if (refElement.current) {
|
||||
console.log(state.current);
|
||||
const { background, rotate, glare } = state.current;
|
||||
refElement.current?.style.setProperty("--m-x", `${glare.x}%`);
|
||||
refElement.current?.style.setProperty("--m-y", `${glare.y}%`);
|
||||
refElement.current?.style.setProperty("--r-x", `${rotate.x}deg`);
|
||||
refElement.current?.style.setProperty("--r-y", `${rotate.y}deg`);
|
||||
refElement.current?.style.setProperty("--bg-x", `${background.x}%`);
|
||||
refElement.current?.style.setProperty("--bg-y", `${background.y}%`);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
style={containerStyle}
|
||||
className="relative isolate [aspect-ratio:17/21] w-[320px] transition-transform delay-[var(--delay)] duration-[var(--duration)] ease-[var(--easing)] will-change-transform [contain:layout_style] [perspective:600px]"
|
||||
ref={refElement}
|
||||
onPointerMove={(event) => {
|
||||
const rotateFactor = 0.4;
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const position = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
};
|
||||
const percentage = {
|
||||
x: (100 / rect.width) * position.x,
|
||||
y: (100 / rect.height) * position.y,
|
||||
};
|
||||
const delta = {
|
||||
x: percentage.x - 50,
|
||||
y: percentage.y - 50,
|
||||
};
|
||||
|
||||
const { background, rotate, glare } = state.current;
|
||||
background.x = 50 + percentage.x / 4 - 12.5;
|
||||
background.y = 50 + percentage.y / 3 - 16.67;
|
||||
rotate.x = -(delta.x / 3.5);
|
||||
rotate.y = delta.y / 2;
|
||||
rotate.x *= rotateFactor;
|
||||
rotate.y *= rotateFactor;
|
||||
glare.x = percentage.x;
|
||||
glare.y = percentage.y;
|
||||
|
||||
updateStyles();
|
||||
}}
|
||||
onPointerEnter={() => {
|
||||
isPointerInside.current = true;
|
||||
if (refElement.current) {
|
||||
setTimeout(() => {
|
||||
if (isPointerInside.current) {
|
||||
refElement.current?.style.setProperty("--duration", "0s");
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
isPointerInside.current = false;
|
||||
if (refElement.current) {
|
||||
refElement.current.style.removeProperty("--duration");
|
||||
refElement.current?.style.setProperty("--r-x", `0deg`);
|
||||
refElement.current?.style.setProperty("--r-y", `0deg`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full origin-center [transform:rotateY(var(--r-x))_rotateX(var(--r-y))] overflow-hidden rounded-[var(--radius)] border border-slate-800 transition-transform delay-[var(--delay)] duration-[var(--duration)] ease-[var(--easing)] will-change-transform hover:filter-none hover:[--duration:200ms] hover:[--easing:linear] hover:[--opacity:0.6]">
|
||||
<div className="grid h-full w-full mix-blend-soft-light [clip-path:inset(0_0_0_0_round_var(--radius))] [grid-area:1/1]">
|
||||
<div className={cn("h-full w-full bg-slate-950", className)}>{children}</div>
|
||||
</div>
|
||||
<div className="transition-background will-change-background grid h-full w-full opacity-[var(--opacity)] mix-blend-soft-light transition-opacity delay-[var(--delay)] duration-[var(--duration)] ease-[var(--easing)] [background:radial-gradient(farthest-corner_circle_at_var(--m-x)_var(--m-y),_rgba(255,255,255,0.8)_10%,_rgba(255,255,255,0.65)_20%,_rgba(255,255,255,0)_90%)] [clip-path:inset(0_0_1px_0_round_var(--radius))] [grid-area:1/1]" />
|
||||
<div
|
||||
className="will-change-background after:grid-area-[inherit] after:bg-repeat-[inherit] after:bg-attachment-[inherit] after:bg-origin-[inherit] after:bg-clip-[inherit] relative grid h-full w-full opacity-[var(--opacity)] [background-blend-mode:hue_hue_hue_overlay] mix-blend-color-dodge transition-opacity [background:var(--pattern),_var(--rainbow),_var(--diagonal),_var(--shade)] [clip-path:inset(0_0_1px_0_round_var(--radius))] [grid-area:1/1] after:bg-[inherit] after:[background-size:varLine truncated
|
||||
style={{ ...backgroundStyle }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loaded 50 of 90 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user