diff --git a/app/(main)/_landing/stats-bento.tsx b/app/(main)/_landing/stats-bento.tsx index 43efdf23..a7885f60 100644 --- a/app/(main)/_landing/stats-bento.tsx +++ b/app/(main)/_landing/stats-bento.tsx @@ -231,20 +231,22 @@ export default function StatsBento() { {/* Category marquees */} -
+ diff --git a/app/demo/demo-experience.tsx b/app/demo/demo-experience.tsx index 2cf71024..2955b0cf 100644 --- a/app/demo/demo-experience.tsx +++ b/app/demo/demo-experience.tsx @@ -1,7 +1,9 @@ "use client"; import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; import { + CheckIcon, ChevronLeftIcon, ChevronRightIcon, LayoutGridIcon, @@ -21,7 +23,6 @@ import { Icons } from "@/components/icons"; import { CommandEmpty, CommandGroup, - CommandInput, CommandItem, CommandList, Command as CommandRoot, @@ -554,69 +555,76 @@ function DemoPicker({ return ( - +
-
@@ -980,24 +964,8 @@ function DemoShellStyles() { isolation: isolate; } - .demo-picker-shell::before { - content: ""; - pointer-events: none; - position: absolute; - inset: 0; - border-radius: inherit; - background: - linear-gradient(180deg, rgb(255 255 255 / 0.08), transparent 22%), - radial-gradient(circle at 50% 0%, rgb(255 255 255 / 0.06), transparent 42%); - } - - .demo-picker-search [cmdk-input-wrapper] { - border: 0; - padding: 0; - } - - .demo-picker-search [cmdk-input-wrapper] svg { - display: none; + .demo-picker-group + .demo-picker-group { + border-top: 1px solid rgb(255 255 255 / 0.05); } .demo-picker-list { @@ -1016,14 +984,10 @@ function DemoShellStyles() { .demo-picker-item { display: flex; - align-items: flex-start; + align-items: center; gap: 0.625rem; } - .demo-picker-item-live { - box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08); - } - .demo-chrome-pill { min-height: 2.75rem; } diff --git a/app/demo/library/browse/cinema-row-notes.tsx b/app/demo/library/browse/cinema-row-notes.tsx index 179e38fa..06344dc4 100644 --- a/app/demo/library/browse/cinema-row-notes.tsx +++ b/app/demo/library/browse/cinema-row-notes.tsx @@ -72,16 +72,15 @@ export function CinemaRowNotes() { id="demo-notes-title" eyebrow="Recipe" title="Stream browse" - description="Apple TV-style layout: gradient hero with staggered premiere copy, a scrolling poster rail, then a craft panel with twin vertical still columns. TMDB art — demo only." + description="Apple TV-style page: gradient hero, scrolling poster rail, craft panel with two vertical still columns. TMDB posters, demo only." />

- Three bands on one page. The hero is type on a violet radial scrim — eyebrow, title, - tagline, runtime, and CTAs stagger in after load. Below that, premieres scroll - horizontally. The craft panel pairs short copy with two still columns drifting in - opposite directions. + Three bands stacked on one page. Hero copy sits on a violet radial scrim. Eyebrow, + title, tagline, runtime, and CTAs stagger in after load. Premieres scroll sideways + underneath. The craft panel is short copy plus two still columns moving opposite ways.

@@ -91,9 +90,9 @@ export function CinemaRowNotes() {

WaveReveal (mode="word") on the hero title.{" "} Marquee on the premieres rail and both still columns ( - pauseOnHover, applyMask={"{false}"} — local masks instead). - Hero rise/fall and delay math live in the demo file as cinema-hero-rise{" "} - plus heroSequenceDelays(). + pauseOnHover, applyMask={"{false}"}; we use local masks + instead). Hero rise/fall and delay math live in the demo file as{" "} + cinema-hero-rise plus heroSequenceDelays().

@@ -102,9 +101,9 @@ export function CinemaRowNotes() {

- heroSequenceDelays() derives every entrance offset from the title word - count and WaveReveal timing — eyebrow through the premieres rail share one - sequence. cinema-hero-rise is a local keyframe (blur +{" "} + heroSequenceDelays() works out every entrance offset from the title word + count and WaveReveal timing. Eyebrow through the premieres rail all share + one sequence. cinema-hero-rise is a local keyframe (blur +{" "} translateY) on everything except the title words.

@@ -128,7 +127,7 @@ export function CinemaRowNotes() { {HERO_SEQUENCE_SNIPPET} - + {PREMIERES_SNIPPET} {STILLS_SNIPPET} diff --git a/app/demo/library/footer/footer-wordmark-notes.tsx b/app/demo/library/footer/footer-wordmark-notes.tsx index dae320c0..425d28fa 100644 --- a/app/demo/library/footer/footer-wordmark-notes.tsx +++ b/app/demo/library/footer/footer-wordmark-notes.tsx @@ -43,7 +43,7 @@ export function FooterWordmarkNotes() { The type sits in the field. It doesn't float on top like a heading.

- Not a clone — same hierarchy. One blue family in oklch, Syne 800 on the + Not a clone, but same hierarchy. One blue family in oklch, Syne 800 on the wordmark, IBM Plex on the links. Lucien uses a static texture; we put boids behind the gradient at low opacity instead.

@@ -52,7 +52,7 @@ export function FooterWordmarkNotes() { -

Two Animata pieces — boids and SiblingFocusNav. The rest is layout and CSS.

+

Two Animata pieces: boids and SiblingFocusNav. The rest is layout and CSS.

@@ -66,7 +66,7 @@ export function FooterWordmarkNotes() {

The wordmark is SVG <text>, not a styled heading. - That's the trick for edge-to-edge scaling — no clipping, no fighting{" "} + That's what makes edge-to-edge scaling work. No clipping, no fighting{" "} clamp() on a single HTML line.

@@ -90,7 +90,7 @@ export function FooterWordmarkNotes() {

- Pulled from the demo file at build time. Copy what you need below — component docs are + Pulled from the demo file at build time. Copy what you need below. Component docs are linked above.

diff --git a/app/demo/library/hero/launch-shift-notes.tsx b/app/demo/library/hero/launch-shift-notes.tsx index 3394c0f2..55633749 100644 --- a/app/demo/library/hero/launch-shift-notes.tsx +++ b/app/demo/library/hero/launch-shift-notes.tsx @@ -30,8 +30,8 @@ export function LaunchShiftNotes() { @@ -47,8 +47,8 @@ export function LaunchShiftNotes() {

Black field, warm off-white body text, violet on the badge. Headline fades white to - gray, two lines so the gradient doesn't muddy the second row. Boids behind a - vignette for edge motion. + gray, split across two lines so the gradient doesn't muddy the second row. Boids + behind a vignette for edge motion.

diff --git a/app/demo/library/hero/photographer-portfolio-notes.tsx b/app/demo/library/hero/photographer-portfolio-notes.tsx index 65d3ab05..8e9ff783 100644 --- a/app/demo/library/hero/photographer-portfolio-notes.tsx +++ b/app/demo/library/hero/photographer-portfolio-notes.tsx @@ -14,7 +14,7 @@ const TRAIL_EXCLUDE_SNIPPET = ``; -const LAYOUT_SNIPPET = `{/* mobile: column stack · desktop: 2:3 row, both pinned to bottom */} +const LAYOUT_SNIPPET = `{/* mobile: column stack; desktop: 2:3 row, both pinned to bottom */}
{/* hero */}
@@ -59,15 +59,15 @@ export function PhotographerPortfolioNotes() {

- Fake site for Maya, a wedding photographer. White page, black type, not much else — name - and contact in the bottom-left, a tall stack of prints on the right. On phone the stack - goes full width; on desktop it sits on the right edge. Wedding stills from Lummi follow - the cursor behind everything. + Fake site for Maya, a wedding photographer. White page, black type, not much else. Name + and contact sit in the bottom-left; a tall stack of prints fills the right. On phone the + stack goes full width; on desktop it hugs the right edge. Wedding stills from Lummi + follow the cursor behind everything.

The first thing you see is not the page. SplitReveal covers the screen, loads every image, and ticks progress on the center seam. When the batch finishes, the - halves slide apart. The layout was already there — just hidden. + halves slide apart. The layout was already there, just hidden.

Whichever print is on top gets corner brackets, like a viewfinder. The rest are plain @@ -79,14 +79,14 @@ export function PhotographerPortfolioNotes() {

- SplitReveal sits next to the page as a fixed overlay — pass{" "} + SplitReveal sits next to the page as a fixed overlay. Pass{" "} images, wire onComplete, done. We skipped the{" "} .Content wrapper on purpose; the real layout mounts normally underneath.

CardStack advances the portfolio. Autoplay only starts after the preloader clears. PrintCaption mirrors the top frame with fake shutter metadata. - Camera and map pin icons are Lucide Animated — they only move on hover so the hero stays + Camera and map pin icons are Lucide Animated. They only move on hover so the hero stays quiet.

@@ -100,8 +100,8 @@ export function PhotographerPortfolioNotes() {

- All image URLs live in LUMMI_ASSETS — six stack frames, twelve for trails, - one avatar — cropped through a shared lummi() helper.{" "} + All image URLs live in LUMMI_ASSETS: six stack frames, twelve for trails, + one avatar. All cropped through a shared lummi() helper.{" "} PRELOAD_IMAGES dedupes before SplitReveal sees them.

@@ -112,26 +112,26 @@ export function PhotographerPortfolioNotes() {

Print width was the annoying part. We tried JS measurement first; on desktop the caption and stack kept drifting out of sync. Switched to a size container on the print column - and capped width with min(100cqw, calc((100cqh - 4.5rem) * 8 / 11)) — 4:5 - frame plus a 10% peek strip (aspect-[8/1]) that fits the viewport height. - No ResizeObserver. + and capped width with min(100cqw, calc((100cqh - 4.5rem) * 8 / 11)) for a + 4:5 frame plus a 10% peek strip (aspect-[8/1]) that fits the viewport + height. No ResizeObserver.

Caption had a white fill at one point. It fought the page background and made the - metadata feel like a sticker — dropped it; caption and page share the same canvas now. + metadata feel like a sticker. Dropped it; caption and page share the same canvas now.

- For trails we first excluded the whole stack. Looked too clean — nothing ever crossed - the prints. Narrowed it to the hero and caption wrappers only. + For trails we first excluded the whole stack. Looked too clean; nothing ever crossed the + prints. Narrowed it to the hero and caption wrappers only.

- + {LAYOUT_SNIPPET} {PRELOADER_SNIPPET} - + {TRAIL_EXCLUDE_SNIPPET}
@@ -139,7 +139,7 @@ export function PhotographerPortfolioNotes() {

- Photos from Lummi — wedding and event searches. IDs + Photos from Lummi, wedding and event searches. IDs are in LUMMI_ASSETS. Fine for demos; use your own shots for anything real.

diff --git a/components/ui/command.tsx b/components/ui/command.tsx index 2cecd910..0bc19908 100644 --- a/components/ui/command.tsx +++ b/components/ui/command.tsx @@ -35,16 +35,27 @@ const CommandDialog = ({ children, ...props }: DialogProps) => { ) } +interface CommandInputProps + extends React.ComponentPropsWithoutRef { + wrapperClassName?: string + hideIcon?: boolean +} + const CommandInput = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( -

- + CommandInputProps +>(({ className, wrapperClassName, hideIcon, ...props }, ref) => ( +
+ {hideIcon ? null : ( + + )} =6.9.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} @@ -368,6 +372,10 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -830,6 +838,10 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -1804,57 +1816,57 @@ packages: '@types/react': '>=16' react: '>=16' - '@next/env@16.2.4': - resolution: {integrity: sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==} + '@next/env@16.2.6': + resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} - '@next/swc-darwin-arm64@16.2.4': - resolution: {integrity: sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==} + '@next/swc-darwin-arm64@16.2.6': + resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.4': - resolution: {integrity: sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==} + '@next/swc-darwin-x64@16.2.6': + resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.4': - resolution: {integrity: sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==} + '@next/swc-linux-arm64-gnu@16.2.6': + resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.4': - resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==} + '@next/swc-linux-arm64-musl@16.2.6': + resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.4': - resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==} + '@next/swc-linux-x64-gnu@16.2.6': + resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.4': - resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==} + '@next/swc-linux-x64-musl@16.2.6': + resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.4': - resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==} + '@next/swc-win32-arm64-msvc@16.2.6': + resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.4': - resolution: {integrity: sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==} + '@next/swc-win32-x64-msvc@16.2.6': + resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2693,7 +2705,7 @@ packages: '@tsparticles/basic@3.9.1': resolution: {integrity: sha512-ijr2dHMx0IQHqhKW3qA8tfwrR2XYbbWYdaJMQuBo2CkwBVIhZ76U+H20Y492j/NXpd1FUnt2aC0l4CEVGVGdeQ==} - deprecated: tsParticles v4 is out, please upgrade for better performances and more features + deprecated: Please update to version 4.1.1 '@tsparticles/engine@3.9.1': resolution: {integrity: sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ==} @@ -3232,8 +3244,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.21: - resolution: {integrity: sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==} + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} engines: {node: '>=6.0.0'} hasBin: true @@ -3322,8 +3334,8 @@ packages: camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - caniuse-lite@1.0.30001790: - resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} @@ -4862,8 +4874,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.2.4: - resolution: {integrity: sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==} + next@16.2.6: + resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -5516,6 +5528,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6189,6 +6206,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.29.0': {} '@babel/core@7.29.0': @@ -6322,6 +6345,8 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helper-wrap-function@7.28.6': @@ -6909,6 +6934,8 @@ snapshots: '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -7608,30 +7635,30 @@ snapshots: '@types/react': 19.2.14 react: 19.2.5 - '@next/env@16.2.4': {} + '@next/env@16.2.6': {} - '@next/swc-darwin-arm64@16.2.4': + '@next/swc-darwin-arm64@16.2.6': optional: true - '@next/swc-darwin-x64@16.2.4': + '@next/swc-darwin-x64@16.2.6': optional: true - '@next/swc-linux-arm64-gnu@16.2.4': + '@next/swc-linux-arm64-gnu@16.2.6': optional: true - '@next/swc-linux-arm64-musl@16.2.4': + '@next/swc-linux-arm64-musl@16.2.6': optional: true - '@next/swc-linux-x64-gnu@16.2.4': + '@next/swc-linux-x64-gnu@16.2.6': optional: true - '@next/swc-linux-x64-musl@16.2.4': + '@next/swc-linux-x64-musl@16.2.6': optional: true - '@next/swc-win32-arm64-msvc@16.2.4': + '@next/swc-win32-arm64-msvc@16.2.6': optional: true - '@next/swc-win32-x64-msvc@16.2.4': + '@next/swc-win32-x64-msvc@16.2.6': optional: true '@opentelemetry/api-logs@0.208.0': @@ -8311,7 +8338,7 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@storybook/nextjs@10.3.5(esbuild@0.17.19)(next@16.2.4(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(type-fest@2.19.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.106.2(esbuild@0.17.19))': + '@storybook/nextjs@10.3.5(esbuild@0.17.19)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(type-fest@2.19.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.106.2(esbuild@0.17.19))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) @@ -8335,7 +8362,7 @@ snapshots: css-loader: 6.11.0(webpack@5.106.2(esbuild@0.17.19)) image-size: 2.0.2 loader-utils: 3.3.1 - next: 16.2.4(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) node-polyfill-webpack-plugin: 2.0.1(webpack@5.106.2(esbuild@0.17.19)) postcss: 8.5.15 postcss-loader: 8.2.1(postcss@8.5.15)(typescript@5.9.3)(webpack@5.106.2(esbuild@0.17.19)) @@ -8508,8 +8535,8 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.29.2 + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -9196,7 +9223,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.21: {} + baseline-browser-mapping@2.10.33: {} big.js@5.2.2: {} @@ -9265,8 +9292,8 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.21 - caniuse-lite: 1.0.30001790 + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 electron-to-chromium: 1.5.344 node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -9310,7 +9337,7 @@ snapshots: pascal-case: 3.1.2 tslib: 2.8.1 - caniuse-lite@1.0.30001790: {} + caniuse-lite@1.0.30001793: {} case-sensitive-paths-webpack-plugin@2.4.0: {} @@ -11201,25 +11228,25 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - next@16.2.4(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - '@next/env': 16.2.4 + '@next/env': 16.2.6 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.21 - caniuse-lite: 1.0.30001790 + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 postcss: 8.4.31 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.4 - '@next/swc-darwin-x64': 16.2.4 - '@next/swc-linux-arm64-gnu': 16.2.4 - '@next/swc-linux-arm64-musl': 16.2.4 - '@next/swc-linux-x64-gnu': 16.2.4 - '@next/swc-linux-x64-musl': 16.2.4 - '@next/swc-win32-arm64-msvc': 16.2.4 - '@next/swc-win32-x64-msvc': 16.2.4 + '@next/swc-darwin-arm64': 16.2.6 + '@next/swc-darwin-x64': 16.2.6 + '@next/swc-linux-arm64-gnu': 16.2.6 + '@next/swc-linux-arm64-musl': 16.2.6 + '@next/swc-linux-x64-gnu': 16.2.6 + '@next/swc-linux-x64-musl': 16.2.6 + '@next/swc-win32-arm64-msvc': 16.2.6 + '@next/swc-win32-x64-msvc': 16.2.6 '@opentelemetry/api': 1.9.1 sharp: 0.34.5 transitivePeerDependencies: @@ -11272,12 +11299,12 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.8.9(next@16.2.4(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5): + nuqs@2.8.9(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.5 optionalDependencies: - next: 16.2.4(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) object-inspect@1.13.4: {} @@ -11985,6 +12012,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -12033,7 +12062,7 @@ snapshots: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.7.4 + semver: 7.8.1 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 diff --git a/scripts/validate-registry-install.js b/scripts/validate-registry-install.js new file mode 100644 index 00000000..115a9aae --- /dev/null +++ b/scripts/validate-registry-install.js @@ -0,0 +1,355 @@ +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const REGISTRY_DIR = path.join(ROOT, "public", "r"); +const SITE_URL = (process.env.NEXT_PUBLIC_APP_URL || "https://animata.design").replace(/\/$/, ""); + +/** Representative installs — one dry-run per bundle shape. */ +const INSTALL_FIXTURES = [ + { + id: "simple", + label: "single file, no npm deps", + item: "preloader/split-reveal.json", + expectFiles: ["components/animata/preloader/split-reveal.tsx"], + expectDeps: [], + }, + { + id: "doc-bundled-shapes", + label: "mdx bundled file refs + shape imports", + item: "card/card-stack.json", + expectFiles: [ + "components/animata/card/card-stack.tsx", + "components/shapes/card-stack-mask-defs.tsx", + ], + expectDeps: ["lucide-react", "motion"], + }, + { + id: "co-located-shared", + label: "co-located helper rewritten to ./shared", + item: "tabs/fluid-tabs.json", + expectFiles: ["components/animata/tabs/fluid-tabs.tsx", "components/animata/tabs/shared.ts"], + expectDeps: ["lucide-react", "motion"], + }, + { + id: "bundled-hook", + label: "bundled hook from @/hooks import", + item: "image/trailing-image.json", + expectFiles: ["components/animata/image/trailing-image.tsx", "hooks/use-mouse-position.ts"], + expectDeps: ["motion"], + }, + { + id: "registry-dependencies", + label: "registryDependencies resolve to sibling items", + item: "card/card-spread.json", + localizeDeps: true, + expectFiles: [ + "components/animata/card/card-spread.tsx", + "components/animata/widget/notes.tsx", + "components/animata/widget/shopping-list.tsx", + ], + expectDeps: [], + }, +]; + +let failed = false; + +function error(message) { + console.error(`[registry-install] ${message}`); + failed = true; +} + +function info(message) { + console.log(`[registry-install] ${message}`); +} + +function walkRegistryJson(dir, acc = []) { + if (!fs.existsSync(dir)) return acc; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walkRegistryJson(full, acc); + } else if (entry.isFile() && entry.name.endsWith(".json")) { + acc.push(full); + } + } + return acc; +} + +function registryUrlToLocalPath(url) { + const prefix = `${SITE_URL}/r/`; + if (!url.startsWith(prefix)) return null; + return path.join(REGISTRY_DIR, url.slice(prefix.length)); +} + +function localizeRegistryItem(itemPath) { + const raw = JSON.parse(fs.readFileSync(itemPath, "utf8")); + if (!raw.registryDependencies?.length) return itemPath; + + const localized = structuredClone(raw); + localized.registryDependencies = raw.registryDependencies.map((dep) => { + const local = registryUrlToLocalPath(dep); + if (!local) return dep; + if (!fs.existsSync(local)) { + error( + `missing local registry dependency for ${path.relative(REGISTRY_DIR, itemPath)}: ${dep}`, + ); + return dep; + } + return local; + }); + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "animata-registry-")); + const outPath = path.join(tmpDir, path.basename(itemPath)); + fs.writeFileSync(outPath, `${JSON.stringify(localized, null, 2)}\n`); + return outPath; +} + +function validateRegistryJsonStatic(jsonPath) { + const rel = path.relative(REGISTRY_DIR, jsonPath); + let item; + + try { + item = JSON.parse(fs.readFileSync(jsonPath, "utf8")); + } catch { + error(`${rel}: invalid JSON`); + return; + } + + if (!item.name || !item.type) { + error(`${rel}: missing name or type`); + } + + if (!Array.isArray(item.files) || item.files.length === 0) { + error(`${rel}: files array is empty`); + return; + } + + const paths = new Set(); + for (const file of item.files) { + if ( + !file.path || + !file.type || + typeof file.content !== "string" || + file.content.trim() === "" + ) { + error(`${rel}: file entry missing path, type, or content`); + continue; + } + if (paths.has(file.path)) { + error(`${rel}: duplicate file path ${file.path}`); + } + paths.add(file.path); + } + + for (const dep of item.registryDependencies ?? []) { + const local = registryUrlToLocalPath(dep); + if (local && !fs.existsSync(local)) { + error(`${rel}: registryDependency not found locally — ${dep}`); + } + } +} + +function createShadcnWorkspace() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "animata-shadcn-")); + + fs.writeFileSync( + path.join(dir, "package.json"), + `${JSON.stringify({ name: "animata-registry-install-test", private: true, type: "module", version: "0.0.0" }, null, 2)}\n`, + ); + + fs.writeFileSync( + path.join(dir, "tsconfig.json"), + `${JSON.stringify( + { + compilerOptions: { + target: "ES2017", + lib: ["dom", "dom.iterable", "esnext"], + allowJs: true, + skipLibCheck: true, + strict: true, + noEmit: true, + esModuleInterop: true, + module: "esnext", + moduleResolution: "bundler", + jsx: "preserve", + paths: { "@/*": ["./*"] }, + }, + include: ["**/*.ts", "**/*.tsx"], + }, + null, + 2, + )}\n`, + ); + + fs.mkdirSync(path.join(dir, "app"), { recursive: true }); + fs.mkdirSync(path.join(dir, "lib"), { recursive: true }); + fs.writeFileSync(path.join(dir, "app", "globals.css"), '@import "tailwindcss";\n'); + fs.writeFileSync( + path.join(dir, "lib", "utils.ts"), + 'export function cn(...inputs: Array) {\n return inputs.filter(Boolean).join(" ");\n}\n', + ); + + fs.writeFileSync( + path.join(dir, "components.json"), + `${JSON.stringify( + { + $schema: "https://ui.shadcn.com/schema.json", + style: "new-york", + rsc: true, + tsx: true, + tailwind: { + css: "app/globals.css", + baseColor: "stone", + cssVariables: true, + prefix: "", + }, + aliases: { + components: "@/components", + utils: "@/lib/utils", + }, + }, + null, + 2, + )}\n`, + ); + + return dir; +} + +function parseDryRunOutput(output) { + const files = []; + const deps = []; + let inDeps = false; + + for (const line of output.split("\n")) { + const fileMatch = line.match(/^│ \+ (.+?) {2}create$/); + if (fileMatch) { + files.push(fileMatch[1].trim()); + continue; + } + + if (line.includes("├ Dependencies")) { + inDeps = true; + continue; + } + + if (inDeps) { + const depMatch = line.match(/^│ \+ (.+)$/); + if (depMatch) { + deps.push(depMatch[1].trim().split("@")[0]); + continue; + } + if (line.includes("├ Files") || line.includes("│ ") || line.startsWith("└")) { + inDeps = false; + } + } + } + + return { files, deps }; +} + +function runShadcnDryRun(workspaceDir, registryItemPath) { + const command = `npx shadcn@latest add -y --dry-run ${JSON.stringify(registryItemPath)}`; + + try { + const output = execSync(command, { + cwd: workspaceDir, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout: 120_000, + env: { ...process.env, CI: "true" }, + }); + return { ok: true, output }; + } catch (err) { + const stdout = err.stdout?.toString?.() ?? ""; + const stderr = err.stderr?.toString?.() ?? ""; + return { ok: false, output: `${stdout}\n${stderr}`.trim() }; + } +} + +function assertFixtureInstall(fixture, workspaceDir) { + const itemPath = path.join(REGISTRY_DIR, fixture.item); + if (!fs.existsSync(itemPath)) { + error(`fixture ${fixture.id}: missing ${fixture.item}`); + return; + } + + const installPath = fixture.localizeDeps ? localizeRegistryItem(itemPath) : itemPath; + const result = runShadcnDryRun(workspaceDir, installPath); + + if (!result.ok) { + error(`fixture ${fixture.id} (${fixture.label}): shadcn dry-run failed\n${result.output}`); + return; + } + + const { files, deps } = parseDryRunOutput(result.output); + + for (const expected of fixture.expectFiles) { + if (!files.includes(expected)) { + error( + `fixture ${fixture.id}: expected file "${expected}" in dry-run output. Got: ${files.join(", ") || "(none)"}`, + ); + } + } + + for (const expectedDep of fixture.expectDeps) { + if (!deps.includes(expectedDep)) { + error( + `fixture ${fixture.id}: expected npm dep "${expectedDep}". Got: ${deps.join(", ") || "(none)"}`, + ); + } + } + + info(`fixture ${fixture.id} OK — ${files.length} file(s), ${deps.length} npm dep(s)`); +} + +function main() { + if (process.argv.includes("--build")) { + execSync("node ./scripts/build-registry.js", { cwd: ROOT, stdio: "inherit" }); + } + + if (!fs.existsSync(REGISTRY_DIR)) { + error(`registry output missing at ${REGISTRY_DIR} — run node scripts/build-registry.js first`); + process.exit(1); + } + + const registryFiles = walkRegistryJson(REGISTRY_DIR); + if (registryFiles.length === 0) { + error("no registry JSON files found"); + process.exit(1); + } + + info(`static validation — ${registryFiles.length} item(s)`); + for (const jsonPath of registryFiles) { + validateRegistryJsonStatic(jsonPath); + } + + if (failed) { + process.exit(1); + } + + info(`shadcn dry-run — ${INSTALL_FIXTURES.length} fixture(s)`); + const workspaceDir = createShadcnWorkspace(); + + try { + for (const fixture of INSTALL_FIXTURES) { + assertFixtureInstall(fixture, workspaceDir); + } + } finally { + fs.rmSync(workspaceDir, { recursive: true, force: true }); + } + + if (failed) { + process.exit(1); + } + + info( + `OK — ${registryFiles.length} registry item(s), ${INSTALL_FIXTURES.length} install fixture(s)`, + ); +} + +main();