Skip to content

Commit fed0408

Browse files
committed
fix(registry): harden conic-gradient palettes and calendar date validation per #58 review
1 parent 4ff4c60 commit fed0408

13 files changed

Lines changed: 80 additions & 18 deletions

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export function IridescentSweepBackground({
4040
const safeSpeed = Number.isFinite(speed) && speed > 0 ? speed : 0;
4141
const cycle = reduced ? 0 : safeSpeed;
4242
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;
4347

4448
return (
4549
<div
@@ -57,7 +61,7 @@ export function IridescentSweepBackground({
5761
<motion.div
5862
className="absolute inset-[-20%]"
5963
style={{
60-
background: `conic-gradient(from 0deg at 50% 50%, ${palette.join(", ")})`,
64+
background: `conic-gradient(from 0deg at 50% 50%, ${resolvedPalette.join(", ")})`,
6165
filter: "blur(60px) saturate(1.4)",
6266
mixBlendMode: "screen",
6367
opacity: 0.85,

apps/www/components/ui/hero-reference-pulse.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ export function ReferencePulseBackground({
4646
// Clamp speed defensively — negative or non-finite values would yield invalid durations.
4747
const safeSpeed = Number.isFinite(speed) && speed > 0 ? speed : 0;
4848
const cycle = reduced ? 0 : safeSpeed;
49-
const resolvedConic = conicPalette ?? conicPaletteFromHue(hue);
49+
// `??` only fires on null/undefined — an empty array would yield
50+
// `conic-gradient(from 0deg, )`, which is invalid CSS. Treat an empty array
51+
// as "no palette supplied" and fall back to the hue-derived stops.
52+
const resolvedConic =
53+
conicPalette && conicPalette.length > 0 ? conicPalette : conicPaletteFromHue(hue);
5054
const conicGradient = `conic-gradient(from 0deg, ${resolvedConic.join(", ")})`;
5155

5256
return (

apps/www/public/r/hero-iridescent-sweep.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"files": [
1313
{
1414
"path": "components/ui/hero-iridescent-sweep.tsx",
15-
"content": "\"use client\";\n\nimport { useId, type ComponentProps, type ReactNode } from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_FOIL_PALETTE = [\n \"#ff9bd1\",\n \"#ffd66b\",\n \"#9bffd6\",\n \"#6bb5ff\",\n \"#c89bff\",\n \"#ff9bd1\",\n] as const;\n\ntype IridescentSweepBackgroundProps = {\n className?: string;\n /** Conic-gradient rotation cycle in seconds. */\n speed?: number;\n /** Hue offset in degrees — shifts the whole foil. */\n hue?: number;\n /** Grain intensity 0–1. */\n grain?: number;\n /** Section / vignette base (e.g. `#0e0a14`). */\n baseColor?: string;\n /** Holographic foil stops for the rotating conic gradient. */\n palette?: ReadonlyArray<string>;\n};\n\nexport function IridescentSweepBackground({\n className,\n speed = 22,\n hue = 0,\n grain = 0.35,\n baseColor = \"#0e0a14\",\n palette = DEFAULT_FOIL_PALETTE,\n}: IridescentSweepBackgroundProps) {\n const reduced = useReducedMotion();\n // Clamp speed defensively — negative or non-finite values would yield invalid durations.\n const safeSpeed = Number.isFinite(speed) && speed > 0 ? speed : 0;\n const cycle = reduced ? 0 : safeSpeed;\n const filterId = useId();\n\n return (\n <div\n aria-hidden\n className={cn(\"absolute inset-0 overflow-hidden\", className)}\n style={{ backgroundColor: baseColor, filter: `hue-rotate(${hue}deg)` }}\n >\n <svg className=\"absolute -z-10 h-0 w-0\" aria-hidden>\n <filter id={filterId}>\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.85\" numOctaves=\"2\" stitchTiles=\"stitch\" />\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.7 0\" />\n </filter>\n </svg>\n\n <motion.div\n className=\"absolute inset-[-20%]\"\n style={{\n background: `conic-gradient(from 0deg at 50% 50%, ${palette.join(\", \")})`,\n filter: \"blur(60px) saturate(1.4)\",\n mixBlendMode: \"screen\",\n opacity: 0.85,\n }}\n initial={false}\n animate={cycle ? { rotate: 360 } : { rotate: 0 }}\n transition={cycle ? { duration: cycle, ease: \"linear\", repeat: Infinity } : undefined}\n />\n <motion.div\n className=\"absolute inset-[-10%]\"\n style={{\n background:\n \"linear-gradient(120deg, transparent 30%, rgba(255,255,255,0.35) 45%, transparent 60%)\",\n mixBlendMode: \"overlay\",\n }}\n initial={{ x: \"-60%\" }}\n animate={cycle ? { x: \"60%\" } : { x: \"0%\" }}\n transition={cycle ? { duration: 6, ease: \"easeInOut\", repeat: Infinity, repeatDelay: 1.4 } : undefined}\n />\n <div\n className=\"absolute inset-0\"\n style={{\n background: `radial-gradient(ellipse at center, transparent 30%, ${baseColor} 92%)`,\n }}\n />\n <div\n className=\"pointer-events-none absolute inset-0 mix-blend-overlay\"\n style={{ filter: `url(#${filterId})`, opacity: grain }}\n >\n <div className=\"absolute inset-0 bg-white/30\" />\n </div>\n <div\n className=\"pointer-events-none absolute inset-0 opacity-[0.05]\"\n style={{\n backgroundImage:\n \"linear-gradient(0deg, rgba(255,255,255,0.6) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.6) 1px, transparent 1px)\",\n backgroundSize: \"44px 44px\",\n }}\n />\n </div>\n );\n}\n\ntype IridescentSweepHeroProps = Omit<ComponentProps<\"section\">, \"children\"> & {\n children?: ReactNode;\n backgroundProps?: IridescentSweepBackgroundProps;\n};\n\nexport function IridescentSweepHero({\n children,\n className,\n backgroundProps,\n ...rest\n}: IridescentSweepHeroProps) {\n return (\n <section\n {...rest}\n className={cn(\n \"relative isolate flex min-h-[100svh] w-full items-center justify-center overflow-hidden bg-[#0e0a14] text-white\",\n className,\n )}\n >\n <IridescentSweepBackground {...backgroundProps} />\n <div className=\"relative z-10 mx-auto flex max-w-3xl flex-col items-center px-6 text-center\">\n {children ?? <DefaultSweepContent />}\n </div>\n </section>\n );\n}\n\nfunction DefaultSweepContent() {\n return (\n <>\n <motion.span\n initial={{ opacity: 0, scale: 0.96 }}\n animate={{ opacity: 1, scale: 1 }}\n transition={{ delay: 0.06, type: \"spring\", stiffness: 140, damping: 22 }}\n className=\"mb-5 inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs uppercase tracking-[0.22em] text-white/85 backdrop-blur\"\n >\n <FoilDot />\n Edition · MMXXVI\n </motion.span>\n <FoilHeadline text=\"Pressed in light.\" />\n <motion.p\n initial={{ opacity: 0, y: 14 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ delay: 0.42, type: \"spring\", stiffness: 120, damping: 18 }}\n className=\"mt-5 max-w-xl text-pretty text-base text-white/75 sm:text-lg\"\n >\n Conic foil under a turbulence grain, with a sheen that sweeps across\n the type on a slow cadence. Editorial energy, zero plug-ins.\n </motion.p>\n <motion.div\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ delay: 0.54, type: \"spring\", stiffness: 120, damping: 18 }}\n className=\"mt-9 flex flex-wrap items-center justify-center gap-3\"\n >\n <button\n type=\"button\"\n className=\"group inline-flex h-11 items-center gap-2 rounded-full bg-white px-6 text-sm font-medium text-neutral-900 transition-transform duration-200 hover:-translate-y-px\"\n >\n <span>Reserve a copy</span>\n <span aria-hidden className=\"transition-transform duration-200 group-hover:translate-x-0.5\">\n →\n </span>\n </button>\n <button\n type=\"button\"\n className=\"inline-flex h-11 items-center gap-2 rounded-full border border-white/25 px-6 text-sm font-medium text-white/90 backdrop-blur transition-colors hover:bg-white/10\"\n >\n Read the editorial\n </button>\n </motion.div>\n </>\n );\n}\n\nfunction FoilDot() {\n return (\n <span\n aria-hidden\n className=\"h-1.5 w-1.5 rounded-full\"\n style={{\n background:\n \"conic-gradient(from 0deg, #ff9bd1, #ffd66b, #9bffd6, #6bb5ff, #c89bff, #ff9bd1)\",\n boxShadow: \"0 0 12px 2px rgba(255,255,255,0.4)\",\n }}\n />\n );\n}\n\nfunction FoilHeadline({ text }: { text: string }) {\n return (\n <h1 className=\"relative inline-block text-balance text-4xl font-semibold tracking-tight sm:text-6xl\">\n <span\n className=\"bg-clip-text text-transparent\"\n style={{\n backgroundImage:\n \"linear-gradient(120deg, #ffffff 0%, #f5e6ff 28%, #ffe9c2 50%, #c8f1ff 72%, #ffffff 100%)\",\n }}\n >\n {text}\n </span>\n <motion.span\n aria-hidden\n className=\"pointer-events-none absolute inset-0 bg-clip-text text-transparent\"\n style={{\n backgroundImage:\n \"linear-gradient(120deg, transparent 30%, rgba(255,255,255,0.95) 50%, transparent 70%)\",\n backgroundSize: \"200% 100%\",\n }}\n initial={{ backgroundPositionX: \"120%\" }}\n animate={{ backgroundPositionX: \"-20%\" }}\n transition={{ delay: 0.3, duration: 1.6, ease: \"easeOut\" }}\n >\n {text}\n </motion.span>\n </h1>\n );\n}\n",
15+
"content": "\"use client\";\n\nimport { useId, type ComponentProps, type ReactNode } from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_FOIL_PALETTE = [\n \"#ff9bd1\",\n \"#ffd66b\",\n \"#9bffd6\",\n \"#6bb5ff\",\n \"#c89bff\",\n \"#ff9bd1\",\n] as const;\n\ntype IridescentSweepBackgroundProps = {\n className?: string;\n /** Conic-gradient rotation cycle in seconds. */\n speed?: number;\n /** Hue offset in degrees — shifts the whole foil. */\n hue?: number;\n /** Grain intensity 0–1. */\n grain?: number;\n /** Section / vignette base (e.g. `#0e0a14`). */\n baseColor?: string;\n /** Holographic foil stops for the rotating conic gradient. */\n palette?: ReadonlyArray<string>;\n};\n\nexport function IridescentSweepBackground({\n className,\n speed = 22,\n hue = 0,\n grain = 0.35,\n baseColor = \"#0e0a14\",\n palette = DEFAULT_FOIL_PALETTE,\n}: IridescentSweepBackgroundProps) {\n const reduced = useReducedMotion();\n // Clamp speed defensively — negative or non-finite values would yield invalid durations.\n const safeSpeed = Number.isFinite(speed) && speed > 0 ? speed : 0;\n const cycle = reduced ? 0 : safeSpeed;\n const filterId = useId();\n // Default-only fires on undefined; treat an explicit empty array as \"no\n // palette\" so `palette.join(\", \")` can't produce an invalid `conic-gradient(...)`.\n const resolvedPalette =\n palette && palette.length > 0 ? palette : DEFAULT_FOIL_PALETTE;\n\n return (\n <div\n aria-hidden\n className={cn(\"absolute inset-0 overflow-hidden\", className)}\n style={{ backgroundColor: baseColor, filter: `hue-rotate(${hue}deg)` }}\n >\n <svg className=\"absolute -z-10 h-0 w-0\" aria-hidden>\n <filter id={filterId}>\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.85\" numOctaves=\"2\" stitchTiles=\"stitch\" />\n <feColorMatrix type=\"matrix\" values=\"0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.7 0\" />\n </filter>\n </svg>\n\n <motion.div\n className=\"absolute inset-[-20%]\"\n style={{\n background: `conic-gradient(from 0deg at 50% 50%, ${resolvedPalette.join(\", \")})`,\n filter: \"blur(60px) saturate(1.4)\",\n mixBlendMode: \"screen\",\n opacity: 0.85,\n }}\n initial={false}\n animate={cycle ? { rotate: 360 } : { rotate: 0 }}\n transition={cycle ? { duration: cycle, ease: \"linear\", repeat: Infinity } : undefined}\n />\n <motion.div\n className=\"absolute inset-[-10%]\"\n style={{\n background:\n \"linear-gradient(120deg, transparent 30%, rgba(255,255,255,0.35) 45%, transparent 60%)\",\n mixBlendMode: \"overlay\",\n }}\n initial={{ x: \"-60%\" }}\n animate={cycle ? { x: \"60%\" } : { x: \"0%\" }}\n transition={cycle ? { duration: 6, ease: \"easeInOut\", repeat: Infinity, repeatDelay: 1.4 } : undefined}\n />\n <div\n className=\"absolute inset-0\"\n style={{\n background: `radial-gradient(ellipse at center, transparent 30%, ${baseColor} 92%)`,\n }}\n />\n <div\n className=\"pointer-events-none absolute inset-0 mix-blend-overlay\"\n style={{ filter: `url(#${filterId})`, opacity: grain }}\n >\n <div className=\"absolute inset-0 bg-white/30\" />\n </div>\n <div\n className=\"pointer-events-none absolute inset-0 opacity-[0.05]\"\n style={{\n backgroundImage:\n \"linear-gradient(0deg, rgba(255,255,255,0.6) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.6) 1px, transparent 1px)\",\n backgroundSize: \"44px 44px\",\n }}\n />\n </div>\n );\n}\n\ntype IridescentSweepHeroProps = Omit<ComponentProps<\"section\">, \"children\"> & {\n children?: ReactNode;\n backgroundProps?: IridescentSweepBackgroundProps;\n};\n\nexport function IridescentSweepHero({\n children,\n className,\n backgroundProps,\n ...rest\n}: IridescentSweepHeroProps) {\n return (\n <section\n {...rest}\n className={cn(\n \"relative isolate flex min-h-[100svh] w-full items-center justify-center overflow-hidden bg-[#0e0a14] text-white\",\n className,\n )}\n >\n <IridescentSweepBackground {...backgroundProps} />\n <div className=\"relative z-10 mx-auto flex max-w-3xl flex-col items-center px-6 text-center\">\n {children ?? <DefaultSweepContent />}\n </div>\n </section>\n );\n}\n\nfunction DefaultSweepContent() {\n return (\n <>\n <motion.span\n initial={{ opacity: 0, scale: 0.96 }}\n animate={{ opacity: 1, scale: 1 }}\n transition={{ delay: 0.06, type: \"spring\", stiffness: 140, damping: 22 }}\n className=\"mb-5 inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs uppercase tracking-[0.22em] text-white/85 backdrop-blur\"\n >\n <FoilDot />\n Edition · MMXXVI\n </motion.span>\n <FoilHeadline text=\"Pressed in light.\" />\n <motion.p\n initial={{ opacity: 0, y: 14 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ delay: 0.42, type: \"spring\", stiffness: 120, damping: 18 }}\n className=\"mt-5 max-w-xl text-pretty text-base text-white/75 sm:text-lg\"\n >\n Conic foil under a turbulence grain, with a sheen that sweeps across\n the type on a slow cadence. Editorial energy, zero plug-ins.\n </motion.p>\n <motion.div\n initial={{ opacity: 0, y: 12 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ delay: 0.54, type: \"spring\", stiffness: 120, damping: 18 }}\n className=\"mt-9 flex flex-wrap items-center justify-center gap-3\"\n >\n <button\n type=\"button\"\n className=\"group inline-flex h-11 items-center gap-2 rounded-full bg-white px-6 text-sm font-medium text-neutral-900 transition-transform duration-200 hover:-translate-y-px\"\n >\n <span>Reserve a copy</span>\n <span aria-hidden className=\"transition-transform duration-200 group-hover:translate-x-0.5\">\n →\n </span>\n </button>\n <button\n type=\"button\"\n className=\"inline-flex h-11 items-center gap-2 rounded-full border border-white/25 px-6 text-sm font-medium text-white/90 backdrop-blur transition-colors hover:bg-white/10\"\n >\n Read the editorial\n </button>\n </motion.div>\n </>\n );\n}\n\nfunction FoilDot() {\n return (\n <span\n aria-hidden\n className=\"h-1.5 w-1.5 rounded-full\"\n style={{\n background:\n \"conic-gradient(from 0deg, #ff9bd1, #ffd66b, #9bffd6, #6bb5ff, #c89bff, #ff9bd1)\",\n boxShadow: \"0 0 12px 2px rgba(255,255,255,0.4)\",\n }}\n />\n );\n}\n\nfunction FoilHeadline({ text }: { text: string }) {\n return (\n <h1 className=\"relative inline-block text-balance text-4xl font-semibold tracking-tight sm:text-6xl\">\n <span\n className=\"bg-clip-text text-transparent\"\n style={{\n backgroundImage:\n \"linear-gradient(120deg, #ffffff 0%, #f5e6ff 28%, #ffe9c2 50%, #c8f1ff 72%, #ffffff 100%)\",\n }}\n >\n {text}\n </span>\n <motion.span\n aria-hidden\n className=\"pointer-events-none absolute inset-0 bg-clip-text text-transparent\"\n style={{\n backgroundImage:\n \"linear-gradient(120deg, transparent 30%, rgba(255,255,255,0.95) 50%, transparent 70%)\",\n backgroundSize: \"200% 100%\",\n }}\n initial={{ backgroundPositionX: \"120%\" }}\n animate={{ backgroundPositionX: \"-20%\" }}\n transition={{ delay: 0.3, duration: 1.6, ease: \"easeOut\" }}\n >\n {text}\n </motion.span>\n </h1>\n );\n}\n",
1616
"type": "registry:component",
1717
"target": "components/ui/hero-iridescent-sweep.tsx"
1818
}

0 commit comments

Comments
 (0)