Skip to content

Commit 9010340

Browse files
authored
Merge pull request #58 from pras75299/chore/backlog-phase-8-meta-version
Chore/backlog phase 8 meta version
2 parents 5db9c52 + fed0408 commit 9010340

92 files changed

Lines changed: 5352 additions & 368 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,10 @@ Three config files drive the entire docs site:
177177
2. Add entry to `registry/config.ts` (name, dependencies, files array, optional `tailwindConfig` for custom keyframes)
178178
3. Add docs metadata to `registry/docs.json`
179179
4. Add demo to `apps/www/config/demos.tsx` (`componentDemos`)
180-
5. Run `pnpm build:registry` from root to regenerate `registry.json`, refresh `apps/www/public/registry/*` and **`apps/www/public/r/*`**, sync `apps/www/components/ui/{component-name}.tsx`, and generate `apps/www/config/components.ts` plus `apps/www/config/docs-scenarios.ts`
180+
5. Add an entry to `registry/changelogs.json` under the new slug — at minimum `[{ "version": "1.0.0", "date": "YYYY-MM-DD", "changes": ["Initial release."] }]`. The build script reads `entries[0]` as the component's `meta.version`; entries must be newest-first (descending semver).
181+
6. Run `pnpm build:registry` from root to regenerate `registry.json`, refresh `apps/www/public/registry/*` and **`apps/www/public/r/*`**, sync `apps/www/components/ui/{component-name}.tsx`, and generate `apps/www/config/components.ts` plus `apps/www/config/docs-scenarios.ts`
181182

182-
When editing an existing component, update `registry/{component}/component.tsx` and then run `pnpm build:registry` to refresh the generated docs copies and registry artifacts.
183+
When editing an existing component, update `registry/{component}/component.tsx` and — if the change is user-visible — prepend a new entry to that slug's array in `registry/changelogs.json` with a bumped semver. Then run `pnpm build:registry` to refresh the generated docs copies and registry artifacts.
183184

184185
## Component Design Rules
185186

apps/www/app/blocks/[slug]/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,17 @@ export default async function BlockPage(props: { params: Promise<{ slug: string
108108
{docs?.scenarios?.length ? (
109109
<section className="mb-12 space-y-6 border-t border-neutral-200 pt-8 dark:border-neutral-800">
110110
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white">Scenarios</h2>
111-
<div className="space-y-8">
111+
<div className="space-y-10">
112112
{docs.scenarios.map((s, i) => (
113113
<article key={s.title} className="space-y-3">
114114
<h3 className="text-base font-medium text-neutral-900 dark:text-white">{s.title}</h3>
115115
<p className="text-sm text-neutral-600 dark:text-neutral-400">{s.description}</p>
116+
{s.demoKey ? (
117+
// Lazy-mount: /blocks slug pages stack multiple full-height
118+
// hero demos. Without IO gating, every off-screen scenario
119+
// would run RAF / pointer listeners at once.
120+
<ComponentPreview slug={s.demoKey} variant="block" lazy />
121+
) : null}
116122
<div className="group relative overflow-hidden rounded-lg border border-neutral-200 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-950">
117123
<div
118124
className="overflow-x-auto p-4 text-sm font-mono [&>pre]:!bg-transparent [&>pre]:!p-0"

apps/www/app/blocks/page.tsx

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,56 @@
11
"use client";
22

33
import Link from "next/link";
4-
import { useMemo, useState } from "react";
4+
import { useMemo, useRef, useState } from "react";
55
import { motion } from "motion/react";
66
import { ArrowRight, Blocks as BlocksIcon } from "lucide-react";
77
import { componentsList, isNewComponent } from "@/config/components";
88
import { useTheme } from "@/contexts/theme-context";
99
import { useIsClient } from "@/lib/use-is-client";
1010
import { cn } from "@/lib/utils";
11+
import { BLOCK_THUMBNAIL_SLUG } from "@/config/block-thumbnail-slugs";
1112
import ComponentPreview from "@/components/component-preview";
1213

1314
const blocksList = componentsList.filter((c) => c.kind === "block");
1415

1516
const ALL = "All";
1617

18+
function BlockCardThumbnail({
19+
slug,
20+
isDark,
21+
}: {
22+
slug: string;
23+
isDark: boolean;
24+
}) {
25+
const frameRef = useRef<HTMLDivElement>(null);
26+
return (
27+
<div
28+
ref={frameRef}
29+
className={cn(
30+
"relative aspect-[16/10] overflow-hidden",
31+
isDark ? "bg-neutral-950" : "bg-neutral-100",
32+
)}
33+
>
34+
<div className="pointer-events-none absolute left-1/2 top-1/2 h-[100svh] w-[240%] max-w-none -translate-x-1/2 -translate-y-1/2 origin-center scale-[0.4] sm:scale-[0.48]">
35+
<ComponentPreview
36+
slug={slug}
37+
lazy
38+
lazyRoot={frameRef}
39+
variant="thumbnail"
40+
/>
41+
</div>
42+
<div
43+
className={cn(
44+
"pointer-events-none absolute inset-0",
45+
isDark
46+
? "bg-gradient-to-t from-black/40 via-transparent to-transparent"
47+
: "bg-gradient-to-t from-white/30 via-transparent to-transparent",
48+
)}
49+
/>
50+
</div>
51+
);
52+
}
53+
1754
const cardVariants = {
1855
hidden: { opacity: 0, y: 18, scale: 0.97 },
1956
visible: (i: number) => ({
@@ -177,27 +214,10 @@ export default function BlocksIndex() {
177214
: "border-neutral-200 bg-white hover:border-neutral-300 hover:shadow-lg",
178215
)}
179216
>
180-
<div
181-
className={cn(
182-
"relative aspect-[16/10] overflow-hidden",
183-
isDark ? "bg-neutral-950" : "bg-neutral-100",
184-
)}
185-
>
186-
<div
187-
className="pointer-events-none absolute inset-0 origin-top-left scale-[0.45] sm:scale-[0.55]"
188-
style={{ width: "182%", height: "222%" }}
189-
>
190-
<ComponentPreview slug={block.slug} lazy />
191-
</div>
192-
<div
193-
className={cn(
194-
"pointer-events-none absolute inset-0",
195-
isDark
196-
? "bg-gradient-to-t from-black/40 via-transparent to-transparent"
197-
: "bg-gradient-to-t from-white/30 via-transparent to-transparent",
198-
)}
199-
/>
200-
</div>
217+
<BlockCardThumbnail
218+
slug={BLOCK_THUMBNAIL_SLUG[block.slug] ?? block.slug}
219+
isDark={isDark}
220+
/>
201221
<div className="flex items-start justify-between gap-3 p-4">
202222
<div className="min-w-0">
203223
<div className="flex items-center gap-2">

apps/www/components/component-preview.tsx

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { forwardRef, useRef, type ForwardedRef } from "react";
3+
import { forwardRef, useRef, type ForwardedRef, type RefObject } from "react";
44
import { motion, useInView } from "motion/react";
55
import { componentDemos } from "@/config/demos";
66
import { useTheme } from "@/contexts/theme-context";
@@ -11,7 +11,8 @@ type ComponentPreviewProps = {
1111
/** Extra class names merged on the root motion.div via `cn` (consumer style overrides). */
1212
className?: string;
1313
/** "block" suppresses the grid backdrop and uses a full-hero min-height. */
14-
variant?: "default" | "block";
14+
/** "thumbnail" — `/blocks` index cards: full hero height, content optically centered in crop. */
15+
variant?: "default" | "block" | "thumbnail";
1516
/**
1617
* When true, the inner demo only mounts while the preview is near or inside
1718
* the viewport. Used by `/blocks` to keep dozens of motion timelines / RAF
@@ -23,6 +24,12 @@ type ComponentPreviewProps = {
2324
* benefit from observing them.
2425
*/
2526
lazy?: boolean;
27+
/**
28+
* When `lazy` is set, observe this root for intersection (e.g. the card
29+
* frame). Avoids missed mounts when the preview sits inside a transformed
30+
* scale wrapper on `/blocks`.
31+
*/
32+
lazyRoot?: RefObject<Element | null>;
2633
};
2734

2835
export default function ComponentPreview(props: ComponentPreviewProps) {
@@ -33,11 +40,14 @@ function EagerPreview(props: ComponentPreviewProps) {
3340
return <PreviewShell {...props} shouldRender />;
3441
}
3542

36-
function LazyPreview(props: ComponentPreviewProps) {
43+
function LazyPreview({ lazyRoot, ...props }: ComponentPreviewProps) {
3744
const cardRef = useRef<HTMLDivElement>(null);
3845
// `once: false` → demo unmounts on exit, freeing RAF / pointer listeners.
3946
// 200px above + below the viewport warms the demo before scroll reaches it.
40-
const isInView = useInView(cardRef, { once: false, margin: "200px 0px" });
47+
const isInView = useInView(lazyRoot ?? cardRef, {
48+
once: false,
49+
margin: "200px 0px",
50+
});
4151
return <PreviewShell {...props} shouldRender={isInView} ref={cardRef} />;
4252
}
4353

@@ -60,38 +70,47 @@ const PreviewShell = forwardRef<HTMLDivElement, PreviewShellProps>(function Prev
6070

6171
const isDark = theme === "dark";
6272
const isBlock = variant === "block";
73+
const isThumbnail = variant === "thumbnail";
74+
const isHeroFrame = isBlock || isThumbnail;
6375
const hasOverflowHidden =
64-
isBlock || (slug !== "horizontal-scroll-gallery" && slug !== "outlined-mega-mark");
65-
const isOutlinedMegaMark = !isBlock && slug === "outlined-mega-mark";
76+
isHeroFrame || (slug !== "horizontal-scroll-gallery" && slug !== "outlined-mega-mark");
77+
const isOutlinedMegaMark = !isHeroFrame && slug === "outlined-mega-mark";
6678

6779
return (
6880
<motion.div
6981
ref={ref}
7082
className={cn(
7183
"w-full rounded-xl border relative flex",
72-
isBlock
73-
? "min-h-[70svh] items-stretch"
74-
: isOutlinedMegaMark
75-
? "min-h-[min(32rem,82dvh)] flex-col items-stretch py-4 sm:py-6"
76-
: "min-h-[300px] items-center justify-center",
84+
isThumbnail
85+
? "min-h-[100svh] items-center justify-center"
86+
: isBlock
87+
? "min-h-[70svh] items-stretch"
88+
: isOutlinedMegaMark
89+
? "min-h-[min(32rem,82dvh)] flex-col items-stretch py-4 sm:py-6"
90+
: "min-h-[300px] items-center justify-center",
7791
hasOverflowHidden && "overflow-hidden",
92+
isThumbnail && "rounded-none border-0",
7893
isDark ? "border-neutral-800 bg-neutral-950/50" : "border-neutral-200 bg-neutral-50/80",
7994
className,
8095
)}
8196
initial={false}
8297
animate={{
83-
backgroundColor: isBlock
98+
backgroundColor: isHeroFrame
8499
? isDark
85100
? "rgb(10,10,10)"
86101
: "rgb(255,255,255)"
87102
: isDark
88103
? "rgba(10,10,10,0.5)"
89104
: "rgba(250,250,250,0.8)",
90-
borderColor: isDark ? "rgb(38,38,38)" : "rgb(229,229,229)",
105+
borderColor: isThumbnail
106+
? "transparent"
107+
: isDark
108+
? "rgb(38,38,38)"
109+
: "rgb(229,229,229)",
91110
}}
92111
transition={{ type: "spring", stiffness: 300, damping: 30 }}
93112
>
94-
{!isBlock && (
113+
{!isHeroFrame && (
95114
<div
96115
className={cn(
97116
"absolute inset-0 z-0 opacity-20 pointer-events-none [background-size:24px_24px]",
@@ -112,7 +131,11 @@ const PreviewShell = forwardRef<HTMLDivElement, PreviewShellProps>(function Prev
112131
{shouldRender ? (
113132
<Demo theme={theme} />
114133
) : (
115-
<LazyPlaceholder isBlock={isBlock} isOutlinedMegaMark={isOutlinedMegaMark} />
134+
<LazyPlaceholder
135+
isBlock={isBlock}
136+
isThumbnail={isThumbnail}
137+
isOutlinedMegaMark={isOutlinedMegaMark}
138+
/>
116139
)}
117140
</div>
118141
</motion.div>
@@ -126,21 +149,25 @@ const PreviewShell = forwardRef<HTMLDivElement, PreviewShellProps>(function Prev
126149
*/
127150
function LazyPlaceholder({
128151
isBlock,
152+
isThumbnail,
129153
isOutlinedMegaMark,
130154
}: {
131155
isBlock: boolean;
156+
isThumbnail: boolean;
132157
isOutlinedMegaMark: boolean;
133158
}) {
134159
return (
135160
<div
136161
aria-hidden
137162
className={cn(
138163
"h-full w-full",
139-
isBlock
140-
? "min-h-[70svh]"
141-
: isOutlinedMegaMark
142-
? "min-h-[min(32rem,82dvh)]"
143-
: "min-h-[300px]",
164+
isThumbnail
165+
? "min-h-[100svh]"
166+
: isBlock
167+
? "min-h-[70svh]"
168+
: isOutlinedMegaMark
169+
? "min-h-[min(32rem,82dvh)]"
170+
: "min-h-[300px]",
144171
)}
145172
/>
146173
);

apps/www/components/ui/hero-iridescent-sweep.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import { useId, type ComponentProps, type ReactNode } from "react";
44
import { motion, useReducedMotion } from "motion/react";
55
import { cn } from "@/lib/utils";
66

7+
const DEFAULT_FOIL_PALETTE = [
8+
"#ff9bd1",
9+
"#ffd66b",
10+
"#9bffd6",
11+
"#6bb5ff",
12+
"#c89bff",
13+
"#ff9bd1",
14+
] as const;
15+
716
type IridescentSweepBackgroundProps = {
817
className?: string;
918
/** Conic-gradient rotation cycle in seconds. */
@@ -12,25 +21,35 @@ type IridescentSweepBackgroundProps = {
1221
hue?: number;
1322
/** Grain intensity 0–1. */
1423
grain?: number;
24+
/** Section / vignette base (e.g. `#0e0a14`). */
25+
baseColor?: string;
26+
/** Holographic foil stops for the rotating conic gradient. */
27+
palette?: ReadonlyArray<string>;
1528
};
1629

1730
export function IridescentSweepBackground({
1831
className,
1932
speed = 22,
2033
hue = 0,
2134
grain = 0.35,
35+
baseColor = "#0e0a14",
36+
palette = DEFAULT_FOIL_PALETTE,
2237
}: IridescentSweepBackgroundProps) {
2338
const reduced = useReducedMotion();
2439
// Clamp speed defensively — negative or non-finite values would yield invalid durations.
2540
const safeSpeed = Number.isFinite(speed) && speed > 0 ? speed : 0;
2641
const cycle = reduced ? 0 : safeSpeed;
2742
const filterId = useId();
43+
// Default-only fires on undefined; treat an explicit empty array as "no
44+
// palette" so `palette.join(", ")` can't produce an invalid `conic-gradient(...)`.
45+
const resolvedPalette =
46+
palette && palette.length > 0 ? palette : DEFAULT_FOIL_PALETTE;
2847

2948
return (
3049
<div
3150
aria-hidden
32-
className={cn("absolute inset-0 overflow-hidden bg-[#0e0a14]", className)}
33-
style={{ filter: `hue-rotate(${hue}deg)` }}
51+
className={cn("absolute inset-0 overflow-hidden", className)}
52+
style={{ backgroundColor: baseColor, filter: `hue-rotate(${hue}deg)` }}
3453
>
3554
<svg className="absolute -z-10 h-0 w-0" aria-hidden>
3655
<filter id={filterId}>
@@ -42,8 +61,7 @@ export function IridescentSweepBackground({
4261
<motion.div
4362
className="absolute inset-[-20%]"
4463
style={{
45-
background:
46-
"conic-gradient(from 0deg at 50% 50%, #ff9bd1, #ffd66b, #9bffd6, #6bb5ff, #c89bff, #ff9bd1)",
64+
background: `conic-gradient(from 0deg at 50% 50%, ${resolvedPalette.join(", ")})`,
4765
filter: "blur(60px) saturate(1.4)",
4866
mixBlendMode: "screen",
4967
opacity: 0.85,
@@ -63,7 +81,12 @@ export function IridescentSweepBackground({
6381
animate={cycle ? { x: "60%" } : { x: "0%" }}
6482
transition={cycle ? { duration: 6, ease: "easeInOut", repeat: Infinity, repeatDelay: 1.4 } : undefined}
6583
/>
66-
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_transparent_30%,_#0e0a14_92%)]" />
84+
<div
85+
className="absolute inset-0"
86+
style={{
87+
background: `radial-gradient(ellipse at center, transparent 30%, ${baseColor} 92%)`,
88+
}}
89+
/>
6790
<div
6891
className="pointer-events-none absolute inset-0 mix-blend-overlay"
6992
style={{ filter: `url(#${filterId})`, opacity: grain }}

0 commit comments

Comments
 (0)