Compare commits

102 Commits
Author SHA1 Message Date
mytheclipsebotreview b986bc28b6 fix: auto-fix code quality [skip ci] 2026-09-21 15:42:51 +07:00
asepharyana ac6c6a4061 test: verify discord format 2026-09-21 15:39:38 +07:00
asepharyana dbf6cfbc12 Merge pull request #20 from asepharyana/tmp-dependabot-fix
fix: stop dependabot auto-merge from landing breaking majors
2026-09-21 11:07:15 +07:00
asepharyana 02b4437eb3 fix: stop dependabot auto-merge from landing breaking majors
Root causes (this round):
- dependabot.yml had been overwritten to a minimal version with NO
  ignore rules -> major bumps allowed (typescript 7, eslint 10, tsparticles 4)
- main branch had NO branch protection -> 'gh pr merge --auto' merged
  even with failing CI
- auto-merge workflow had no check-run verification step

Fixes:
- Branch protection on main: required status check 'lint + typecheck +
  test + build' (strict) so auto-merge cannot land failing changes
- dependabot-auto-merge.yml: verify CI check-run conclusion == success
  (actions/github-script poll with timeout) before enabling auto-merge
- dependabot.yml: restore ignore rules for eslint/typescript/@tsparticles
  majors + eslint-config-next + eslint-plugin-react
- Rollback toolchain to known-good exact pins:
  typescript 6.0.3, eslint 9.39.5, @tsparticles/{react,engine,slim} 3.x
  (exact versions, no ranges dependabot can widen)
- Close dependabot PR #19 (tsparticles 4.4.0)
2026-09-21 11:03:34 +07:00
mytheclipsebotreview[bot] b1238d4363 Auto-merge PR #18
chore(deps-dev): bump typescript from 6.0.3 to 7.0.2
2026-09-20 20:23:43 +00:00
mytheclipsebotreview 94a0328cad fix: auto-fix code quality [skip ci] 2026-09-21 03:23:27 +07:00
mytheclipsebotreview 72a49fc2d0 chore: sync bun.lock after dependabot bump 2026-09-21 03:20:44 +07:00
mytheclipsebotreview e6cbdcc51d Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/typescript-7.0.2 2026-09-21 03:20:41 +07:00
mytheclipsebotreview[bot] 9a9880d865 Auto-merge PR #17
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 20:18:23 +00:00
mytheclipsebotreview 960eaabe42 fix: auto-fix code quality [skip ci] 2026-09-21 03:17:57 +07:00
dependabot[bot] 43dad88aea chore(deps-dev): bump typescript from 6.0.3 to 7.0.2
Bumps [typescript](https://github.com/microsoft/TypeScript) from 6.0.3 to 7.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.3...v7.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 20:12:40 +00:00
dependabot[bot] 5fe2746879 chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 20:12:25 +00:00
mytheclipsebotreview[bot] e288be64da Auto-merge PR #16
chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
2026-09-20 20:10:00 +00:00
mytheclipsebotreview b4a72b7154 fix: auto-fix code quality [skip ci] 2026-09-21 03:09:41 +07:00
mytheclipsebotreview 32b561a96b chore: sync bun.lock after dependabot bump 2026-09-21 03:04:43 +07:00
dependabot[bot] e75444ba52 chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
Bumps [@tsparticles/slim](https://github.com/tsparticles/tsparticles/tree/HEAD/bundles/slim) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/bundles/slim/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/bundles/slim)

---
updated-dependencies:
- dependency-name: "@tsparticles/slim"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 20:02:46 +00:00
asepharyana 6270c2701d chore: update dependabot-auto-merge.yml 2026-09-21 03:00:30 +07:00
asepharyana db07b5ae0c chore: update dependabot.yml 2026-09-21 03:00:28 +07:00
asepharyana 6f4a021785 fix: lock toolchain against dependabot major bumps (eslint <10, tsparticles ~3.9)
CI after the overlay fix exposed two regressions from dependabot
auto-merges:
- eslint back to ^10 (crashes eslint-plugin-react: display-name
  getFilename) -> >=9.24.0 <10 (9.0/9.20 lack eslint/config export)
- @tsparticles/engine+slim to ^4 (v4 API drift breaks sparkles.tsx),
  and mixing ^3.0 with slim's nested 3.9.1 broke Engine type identity
  -> all pinned ~3.9.0 so bun hoists a single engine copy

Verified locally: lint/typecheck/test/build/format all green.
2026-09-20 22:54:10 +07:00
asepharyana 142a814039 fix: pin eslint to ^9 — dependabot re-landed v10 which crashes eslint-plugin-react
CI revealed eslint had been bumped to ^10 again (react/display-name:
contextOrFilename.getFilename is not a function). Restore ^9 pin so the
declared toolchain matches the vendored components' validated stack.
2026-09-20 22:48:30 +07:00
asepharyana 5df8306909 fix: resolve 'full black' page — unstuck MultiStepLoader overlay, add dark-mode depth
Root cause of the all-black view: the gallery demo hardcoded
<MultiStepLoader loading /> (loading=true forever), which renders a
fixed inset-0 z-[100] backdrop-blur overlay over the entire viewport.
Replace with an interactive demo (button triggers loader, auto-resets
after 4s).

Also soften flat-black dark backgrounds:
- /ui page: dark:bg-black -> dark:bg-zinc-950 + subtle top radial gradient
- home hero: dark:bg-black -> dark:bg-zinc-950 + white 6% radial gradient
2026-09-20 22:47:12 +07:00
mytheclipsebotreview[bot] b0098c9270 Auto-merge PR #13
chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
2026-09-20 15:40:04 +00:00
mytheclipsebotreview 29cec47928 fix: auto-fix code quality [skip ci] 2026-09-20 22:39:36 +07:00
mytheclipsebotreview 47da9bb340 chore: sync bun.lock after dependabot bump 2026-09-20 22:34:34 +07:00
mytheclipsebotreview e5d3a319a8 Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/engine-4.4.0 2026-09-20 22:34:19 +07:00
mytheclipsebotreview[bot] f5b4f6e2d0 Auto-merge PR #14
chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
2026-09-20 15:28:55 +00:00
mytheclipsebotreview 57f77a3373 fix: auto-fix code quality [skip ci] 2026-09-20 22:28:32 +07:00
mytheclipsebotreview dae0baea3b chore: sync bun.lock after dependabot bump 2026-09-20 22:24:43 +07:00
mytheclipsebotreview 3ce2b9662c Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/slim-4.4.0 2026-09-20 22:24:15 +07:00
mytheclipsebotreview[bot] c5a5611e10 Auto-merge PR #15
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 15:24:10 +00:00
mytheclipsebotreview c4ff57876a fix: auto-fix code quality [skip ci] 2026-09-20 22:23:51 +07:00
mytheclipsebotreview 2e82223689 Merge branch 'main' of https://github.com/asepharyana/nextjs-template into dependabot/npm_and_yarn/eslint-10.10.0 2026-09-20 22:20:51 +07:00
asepharyana 738f533d4c feat(ui): demo all vendored Aceternity components in /ui gallery
Expand the showcase from ~20 to all ~45 vendored components:
- Waves/boxes/spotlights: WavyBackground, Boxes, Spotlight, BackgroundGradient
- Text: Cover, TextGenerateEffect, TextHoverEffect, SquigglyText, TextRevealCard
- Cards/buttons: Meteors, GlareCard, MagneticButton
- Interaction: GooeyInput, Notch, FloatingDock, AnimatedModal, AnimatedTabs
- Scroll: StickyScroll, ContainerScroll, Timeline, HeroParallax, ParallaxScroll,
  MaskContainer (svg-mask), FocusCards, HoverEffect, Tooltip, MultiStepLoader
- GoogleGeminiEffect wired with 5 useTransform pathLengths from scrollYProgress
Full gate green (lint/typecheck/test/build/format) + SSR verified on /ui.
2026-09-20 22:19:37 +07:00
mytheclipsebotreview b1045c1dd2 chore: sync bun.lock after dependabot bump 2026-09-20 22:19:34 +07:00
mytheclipsebotreview 7df7575a05 chore: sync bun.lock after dependabot bump 2026-09-20 22:15:49 +07:00
dependabot[bot] 0b4a05e3d6 chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 15:12:09 +00:00
dependabot[bot] c09f187465 chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
Bumps [@tsparticles/slim](https://github.com/tsparticles/tsparticles/tree/HEAD/bundles/slim) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/bundles/slim/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/bundles/slim)

---
updated-dependencies:
- dependency-name: "@tsparticles/slim"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 15:11:57 +00:00
dependabot[bot] f5a05ef4a1 chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
Bumps [@tsparticles/engine](https://github.com/tsparticles/tsparticles/tree/HEAD/engine) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/engine/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/engine)

---
updated-dependencies:
- dependency-name: "@tsparticles/engine"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 15:11:16 +00:00
asepharyana 0810ff1373 fix(ci): guard dependabot auto-merge on green CI
Root cause of recurring breaking bumps: dependabot-auto-merge ran
gh pr merge --auto with no CI gate and the repo has no branch protection,
so PRs merged the moment checks finished — including FAILED ones
(eslint@10, tsparticles@4, typescript@7 all landed repeatedly).

Add a github-script step: list check-runs for the PR head, fail unless
all checks completed AND none failed. Breaking bumps now block instead
of silently landing.
2026-09-20 22:08:39 +07:00
asepharyana fb91f3df90 chore(dependabot): fix ignore patterns (exact names, no wildcard) + checkout v7
- dependabot ignore for npm does NOT support wildcards (only groups.patterns
  does). The '@tsparticles/*' rule was silently ignored -> v4 bumps kept
  landing. Replace with exact scoped names.
- bump actions/checkout v5->v7 (closes dependabot PR #5); the CI failure on
  that PR was a stale-branch format-check, not checkout v7.
Open @tsparticles PRs now closed (#9 already closed earlier).
2026-09-20 22:08:14 +07:00
asepharyana c9abfc4815 fix(deps): re-pin toolchain after auto-merge regressions (round 2)
dependabot auto-merge re-landed eslint@^10 + @tsparticles@^4 + typescript@7
(no branch protection -> breaking bumps merge even when failing). Re-pin the
known-good stack: eslint ^9, typescript ^6.0.3, tsparticles ^3. Full gate green.
2026-09-20 22:01:57 +07:00
asepharyana 40b6fd3cfc feat(home): Aceternity hero — Spotlight, SparklesCore, Cover, MagneticButton
Replace create-next-app boilerplate home with a signature Aceternity hero:
- Spotlight glow top-left, SparklesCore particle field background
- Cover underlines 'animated UI out of the box'
- MagneticButton on the gallery CTA (href=/ui)
- Staggered fade-up entry via motion/react
Full gate: lint/typecheck/build/test/format green.
2026-09-20 22:01:13 +07:00
mytheclipsebotreview[bot] 8a83096ce6 Auto-merge PR #11
chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
2026-09-20 14:51:37 +00:00
mytheclipsebotreview be8201b60a fix: auto-fix code quality [skip ci] 2026-09-20 21:50:47 +07:00
mytheclipsebotreview e87d41e08a chore: sync bun.lock after dependabot bump 2026-09-20 21:48:58 +07:00
mytheclipsebotreview 57a0891224 Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/engine-4.4.0 2026-09-20 21:48:51 +07:00
mytheclipsebotreview[bot] 0bd9e49964 Auto-merge PR #12
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 14:48:47 +00:00
mytheclipsebotreviewandClaude Opus 5 186c8b172b fix: auto-fix code quality [skip ci]
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 21:48:17 +07:00
dependabot[bot] 456fc31770 chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
Bumps [@tsparticles/engine](https://github.com/tsparticles/tsparticles/tree/HEAD/engine) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/engine/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/engine)

---
updated-dependencies:
- dependency-name: "@tsparticles/engine"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 14:44:14 +00:00
mytheclipsebotreview 145c79c8b2 chore: sync bun.lock after dependabot bump 2026-09-20 21:41:48 +07:00
mytheclipsebotreview 0b634a0a2a Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/eslint-10.10.0 2026-09-20 21:41:45 +07:00
mytheclipsebotreview[bot] a2f9bc0c35 Auto-merge PR #10
chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
2026-09-20 14:41:40 +00:00
mytheclipsebotreview 9abbe01a7b fix: auto-fix code quality [skip ci] 2026-09-20 21:41:11 +07:00
mytheclipsebotreview 72c4fce25e chore: sync bun.lock after dependabot bump 2026-09-20 21:35:46 +07:00
dependabot[bot] d52604971d chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 14:31:26 +00:00
dependabot[bot] 66c8b90d79 chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
Bumps [@tsparticles/slim](https://github.com/tsparticles/tsparticles/tree/HEAD/bundles/slim) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/bundles/slim/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/bundles/slim)

---
updated-dependencies:
- dependency-name: "@tsparticles/slim"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 14:30:32 +00:00
asepharyana 978db63a95 fix(deps): re-pin toolchain after auto-merge regressions
dependabot auto-merge landed eslint@^10 + @tsparticles@^4 (d13b984 pins
ignored, no branch protection). Restore known-good stack: eslint ^9, TS ^6,
tsparticles ^3. Full gate green.
2026-09-20 21:28:14 +07:00
asepharyana dcc6fc2c0a refactor: migrate deprecated middleware convention to proxy (Next 16)
Middlewate file convention is deprecated in Next 16.3.5 (renamed to proxy).
git mv middleware.ts -> proxy.ts, rename exported fn middleware -> proxy.
Build now emits `ƒ Proxy (Middleware)` with no deprecation warning.
2026-09-20 21:27:28 +07:00
mytheclipsebotreview[bot] 29b9a6618c Auto-merge PR #7
chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
2026-09-20 14:11:27 +00:00
mytheclipsebotreview caf9f4dd5e chore: sync bun.lock after dependabot bump 2026-09-20 21:11:21 +07:00
mytheclipsebotreview a6a9d1e236 Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/engine-4.4.0 2026-09-20 21:10:54 +07:00
mytheclipsebotreview[bot] 9a265a760d Auto-merge PR #6
chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
2026-09-20 14:10:46 +00:00
mytheclipsebotreview 57cdb609b8 fix: auto-fix code quality [skip ci] 2026-09-20 21:10:25 +07:00
mytheclipsebotreview 5b51b2f6e9 chore: sync bun.lock after dependabot bump 2026-09-20 21:07:26 +07:00
mytheclipsebotreview bd4581ea2b Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tsparticles/slim-4.4.0 2026-09-20 21:07:00 +07:00
mytheclipsebotreview[bot] 5f9dd1a5c4 Auto-merge PR #8
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 14:06:55 +00:00
mytheclipsebotreview f7e621ef66 chore: sync bun.lock after dependabot bump 2026-09-20 21:06:48 +07:00
dependabot[bot] 2e19b72ce2 chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 13:57:27 +00:00
dependabot[bot] 3f1b11496e chore(deps): bump @tsparticles/engine from 3.9.1 to 4.4.0
Bumps [@tsparticles/engine](https://github.com/tsparticles/tsparticles/tree/HEAD/engine) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/engine/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/engine)

---
updated-dependencies:
- dependency-name: "@tsparticles/engine"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 13:57:02 +00:00
dependabot[bot] 8a46d734a9 chore(deps): bump @tsparticles/slim from 3.9.1 to 4.4.0
Bumps [@tsparticles/slim](https://github.com/tsparticles/tsparticles/tree/HEAD/bundles/slim) from 3.9.1 to 4.4.0.
- [Release notes](https://github.com/tsparticles/tsparticles/releases)
- [Changelog](https://github.com/tsparticles/tsparticles/blob/main/bundles/slim/CHANGELOG.md)
- [Commits](https://github.com/tsparticles/tsparticles/commits/v4.4.0/bundles/slim)

---
updated-dependencies:
- dependency-name: "@tsparticles/slim"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-20 13:56:47 +00:00
asepharyana 943126f6b3 chore(dependabot): ignore semver-major bumps that break the toolchain stack
Prevent recurrence of the breakage fixed in d13b984:
- eslint major (ESLint 10 dropped rule-context getFilename; eslint-plugin-react
  peer range caps at ^9.7) -> react/display-name crash
- typescript major (typescript-eslint 8.x peer caps <6.1.0)
- @tsparticles/* major (registry SparklesCore targets v3 API)
- eslint-config-next + eslint-plugin-react majors (framework/plugin train)

Also add github-actions ecosystem (weekly, grouped) and labels.
2026-09-20 20:54:29 +07:00
asepharyana d13b984ac4 fix(toolchain): pin eslint ^9 + typescript ^6 to unbreak dependabot-bumped stack
dependabot's eslint@^10 bump broke eslint-plugin-react 7.37.5 (peer range caps at
^9.7; eslint 10 dropped context.getFilename) -> react/display-name crash.
TS 7 in typescript-eslint peer range breaks @typescript-eslint/parser load.
Pin eslint@^9.39.5 + typescript@^6.0.3; tsparticles pinned ^3 (registry target API).
lint/typecheck/test/build/format all green.
2026-09-20 20:47:11 +07:00
asepharyana 78ed092e1c chore(deps): add registry deps for vendored showcase (mini-svg-data-uri, simplex-noise, tsparticles) 2026-09-20 20:23:36 +07:00
asepharyana 692d54bab1 fix(showcase): drop stray id prop on showcase Card (typecheck gate) 2026-09-20 19:16:01 +07:00
asepharyana bf75d9deef feat(ui): vendor Aceternity registry components + showcase gallery
Vendor 50 Aceternity UI components into components/ui from the official
registry (ui.aceternity.com/registry/<slug>.json), adapted for this
template's stack: Tailwind v4, motion/react 13, cn util, React 19.

Add a live showcase gallery (/ui + /docs/... navigation is unchanged):
app/ui/page.tsx renders <ShowcaseGallery> with 30+ demos — 3d-pin,
aurora, background-beams, bento, card-stack, card-spotlight,
direction-aware-hover, encrypted-text, evervault-card, flip-words,
hero-highlight, hero-parallax, infinite-moving-cards, moving-border,
sparkles, tracing-beam, animated-tooltip, typewriter-effect,
placeholders-and-vanish-input and friends.

Toolchain stays zero-warning: vendored animation code is scoped in
eslint.config.mjs (react-hooks/react-compiler advisory rules, no-any,
prefer-const, no-img-element) so app code keeps a 0-error gate. typecheck,
build, 7 vitest tests and 2 test suites all green.
2026-09-20 19:10:43 +07:00
mytheclipsebotreview[bot] bc64b74a34 Auto-merge PR #2
chore(deps-dev): bump typescript from 5.9.3 to 7.0.2
2026-09-20 05:24:36 +00:00
mytheclipsebotreviewandClaude Opus 5 e057dd50c8 fix: auto-fix code quality [skip ci]
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 12:24:21 +07:00
mytheclipsebotreview d471418c8f Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/typescript-7.0.2 2026-09-20 12:21:49 +07:00
mytheclipsebotreview 2bf524ff82 fix: auto-fix code quality [skip ci] 2026-09-20 12:13:22 +07:00
mytheclipsebotreview[bot] 2afe84e608 Auto-merge PR #4
chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
2026-09-20 05:11:04 +00:00
mytheclipsebotreviewandClaude Opus 5 c17aaeb9f1 fix: auto-fix code quality [skip ci]
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 12:10:42 +07:00
mytheclipsebotreview cf3c6324c8 fix: auto-fix code quality [skip ci] 2026-09-20 11:56:50 +07:00
mytheclipsebotreview 587c7137c5 fix: auto-fix code quality [skip ci] 2026-09-20 11:47:21 +07:00
mytheclipsebotreview 79b53addf4 fix: auto-fix code quality [skip ci] 2026-09-20 11:39:00 +07:00
mytheclipsebotreview 74ebd6bc10 fix: auto-fix code quality [skip ci] 2026-09-20 11:22:18 +07:00
mytheclipsebotreview d04b3d2b22 Merge branch 'main' of https://github.com/asepharyana/nextjs-template into dependabot/npm_and_yarn/eslint-10.10.0 2026-09-20 11:19:18 +07:00
mytheclipsebotreview 2c5b865c66 Merge branch 'main' of https://github.com/asepharyana/nextjs-template into dependabot/npm_and_yarn/typescript-7.0.2 2026-09-20 11:06:44 +07:00
mytheclipsebotreview[bot] ea02e5d358 Auto-merge PR #3
CI / lint + typecheck + test + build (push) Canceled after 0s
chore(deps-dev): bump @types/node from 20.19.43 to 26.6.1
2026-09-19 20:16:28 +00:00
mytheclipsebotreview[bot] 7138e4a222 Auto-merge PR #1
chore(deps): bump the production group with 2 updates
2026-09-19 20:15:54 +00:00
mytheclipsebotreview a70e08ac33 chore: sync bun.lock after dependabot bump 2026-09-20 03:15:24 +07:00
mytheclipsebotreview a93cecd0ee chore: sync bun.lock after dependabot bump 2026-09-20 03:09:15 +07:00
mytheclipsebotreview 31ba33ee4d chore: sync bun.lock after dependabot bump 2026-09-20 03:08:27 +07:00
mytheclipsebotreview 5b878d9688 chore: sync bun.lock after dependabot bump 2026-09-20 03:07:13 +07:00
dependabot[bot] 99718c8698 chore(deps-dev): bump eslint from 9.39.5 to 10.10.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.5 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.5...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-19 20:05:19 +00:00
dependabot[bot] 31821f84cb chore(deps-dev): bump @types/node from 20.19.43 to 26.6.1
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.19.43 to 26.6.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.6.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-19 20:04:26 +00:00
dependabot[bot] afeca88f80 chore(deps-dev): bump typescript from 5.9.3 to 7.0.2
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 7.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v7.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-19 20:04:04 +00:00
dependabot[bot] 9ee81d06a2 chore(deps): bump the production group with 2 updates
Bumps the production group with 2 updates: [react](https://github.com/react/react/tree/HEAD/packages/react) and [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom).


Updates `react` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react)

Updates `react-dom` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react-dom)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
- dependency-name: react-dom
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-19 20:03:32 +00:00
asepharyana 582d1901f6 feat: add dependabot-auto-merge.yml
CI / lint + typecheck + test + build (push) Canceled after 0s
2026-09-20 03:01:21 +07:00
asepharyana c5b96779aa feat: add dependabot.yml 2026-09-20 03:01:19 +07:00
asepharyana d54aa7e2c2 feat: close remaining gaps — middleware, demo wiring, error UX, env validation
CI / lint + typecheck + test + build (push) Canceled after 0s
- Auth: middleware.ts guards /protected via better-auth session cookie;
  protected page reads session server-side (auth.api.getSession)
- Demo wiring (app/demo + components/demo): react-markdown + remark-gfm +
  shiki CodeBlock, RHF+zod form with react-query mutation + sonner toast,
  react-table items list (legacy API, table v9), /api/items GET+POST
  (zod v4 coercion, schema in lib/items.ts shared with unit tests)
- Error UX: app/error.tsx (retry + digest), not-found.tsx (404), loading.tsx
- Env: lib/env.ts zod-validated (no import-time throw), .env.example sync
  (NEXT_PUBLIC_BETTER_AUTH_URL), auth-client uses env
- Tooling: @vitest/coverage-v8 (test:coverage works), tsconfig cleanup
  (drop .next/dev/dev/types artifact), README rewrite, demo e2e specs
2026-09-19 22:47:26 +07:00
asepharyana 6c6330cd7a fix: use explicit children type in RootLayout (CI typecheck)
CI / lint + typecheck + test + build (push) Canceled after 0s
LayoutProps<'/'> is a Next-generated global type that only exists after
next typegen/build creates .next/types. Fresh CI clones fail tsc --noEmit
because .next doesn't exist yet. Use Readonly<{children}> — no dependency
on generated types.
2026-09-19 21:54:31 +07:00
asepharyana e288f659c2 feat: add CI, Docker, auth, typed API client, shiki code blocks, e2e tests
CI / lint + typecheck + test + build (push) Canceled after 0s
- CI: GitHub Actions (bun 1.3.14, lint/typecheck/test/build/format gates)
- Docker: multi-stage standalone build + .dockerignore (next.config output=standalone)
- Auth: better-auth server (memory adapter, drizzle-swap-ready) + react client
  + /api/auth/[...all] route handler
- API: /api/health + /api/openapi routes, openapi-typescript codegen to
  types/api.ts, typed openapi-fetch client (lib/api, lib/http)
- Shiki: server-side CodeBlock component (escaped HTML)
- E2E: playwright home + health specs on dedicated :3100 (chromium 1243
  installed from GCS after CDN timeout)
- Dev: allow 127.0.0.1/localhost dev origins (HMR for local tooling)
2026-09-19 21:10:44 +07:00
90 changed files with 8477 additions and 125 deletions

No files matched your search

+14
View File
@@ -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
+3
View File
@@ -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
+54
View File
@@ -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 }}
+57
View File
@@ -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"
+42
View File
@@ -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
+3
View File
@@ -48,3 +48,6 @@ test-results
playwright-report
blob-report
.playwright
# planning files
.planning
+34
View File
@@ -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"]
+106 -21
View File
@@ -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.
+4
View File
@@ -0,0 +1,4 @@
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);
+12
View File
@@ -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(),
});
}
+36
View File
@@ -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 });
}
+63
View File
@@ -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" } } },
},
},
},
},
},
});
}
+61
View File
@@ -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>
);
}
+34
View File
@@ -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
View File
@@ -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"
+10
View File
@@ -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>
);
}
+19
View File
@@ -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
View File
@@ -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 />;
}
+31
View File
@@ -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>
);
}
+73
View File
@@ -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>
);
}
+170 -26
View File
@@ -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=="],
+18
View File
@@ -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>
);
}
+101
View File
@@ -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>
);
}
+98
View File
@@ -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>
);
}
+68
View File
@@ -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>
);
}
+733
View File
@@ -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&apos;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>
);
}
+121
View File
@@ -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>
);
};
+154
View File
@@ -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>
);
};
+214
View File
@@ -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]);
};
+122
View File
@@ -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>
);
};
+85
View File
@@ -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>
))}
</>
);
};
+61
View File
@@ -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>
);
};
+139
View File
@@ -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";
+69
View File
@@ -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>
);
};
+72
View File
@@ -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>
);
};
+54
View File
@@ -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>
);
};
+99
View File
@@ -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>
);
};
+61
View File
@@ -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>
);
};
+65
View File
@@ -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>
);
};
+24
View File
@@ -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>
);
};
+219
View File
@@ -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>
);
};
+156
View File
@@ -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,
},
};
+141
View File
@@ -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>
);
};
+96
View File
@@ -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>
);
};
+98
View File
@@ -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">&nbsp;</span>
</motion.span>
))}
</motion.div>
</AnimatePresence>
);
};
+196
View File
@@ -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>
);
}
+64
View File
@@ -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>
);
}
+132
View File
@@ -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