From d302d4fc73c38d9b1ee18a42d435d8993182eb93 Mon Sep 17 00:00:00 2001 From: Sudha Shrestha Date: Thu, 11 Jun 2026 19:27:50 +0545 Subject: [PATCH 1/5] refactor(components): extract inline styles to co-located css Move keyframes and selectors Tailwind cannot express into imported .css files, update manual install docs for multi-file touch commands, and bundle co-located css in the registry build. Co-authored-by: Cursor --- CLAUDE.md | 2 +- animata/background/animated-beam.css | 28 ++++ animata/background/animated-beam.tsx | 14 +- animata/background/shooting-stars.css | 32 +++++ animata/background/shooting-stars.tsx | 15 +- animata/button/algolia-blue-button.css | 27 ++++ animata/button/algolia-blue-button.tsx | 16 +-- animata/button/algolia-white-button.css | 27 ++++ animata/button/algolia-white-button.tsx | 16 +-- animata/button/ripple-button.css | 43 ++++++ animata/button/ripple-button.tsx | 28 +--- animata/card/card-spread.css | 31 +++++ animata/card/card-spread.tsx | 34 +---- animata/card/collab-card.css | 129 ++++++++++++++++++ animata/card/collab-card.tsx | 63 +-------- animata/container/animated-border-trail.css | 17 +++ animata/container/animated-border-trail.tsx | 15 +- animata/container/marquee.css | 33 +++++ animata/container/marquee.tsx | 21 +-- animata/fabs/flower-menu.css | 12 ++ animata/fabs/flower-menu.tsx | 14 +- animata/fabs/speed-dial.css | 84 ++++++++++++ animata/fabs/speed-dial.tsx | 41 +----- animata/feature-cards/content-scan.css | 15 ++ animata/feature-cards/content-scan.tsx | 16 +-- animata/preloader/split-reveal.css | 52 +++++++ animata/preloader/split-reveal.tsx | 58 +------- animata/scroll/stacked-sections.css | 5 + animata/scroll/stacked-sections.tsx | 8 +- components/mdx-components.tsx | 19 ++- content/docs/background/animated-beam.mdx | 13 +- content/docs/background/shooting-stars.mdx | 13 +- content/docs/button/algolia-blue-button.mdx | 6 +- content/docs/button/algolia-white-button.mdx | 6 +- content/docs/button/ripple-button.mdx | 6 +- content/docs/card/card-spread.mdx | 6 +- content/docs/card/collab-card.mdx | 6 +- .../docs/container/animated-border-trail.mdx | 10 +- content/docs/container/marquee.mdx | 24 +--- content/docs/contributing/guidelines.mdx | 2 +- content/docs/fabs/flower-menu.mdx | 6 +- content/docs/fabs/speed-dial.mdx | 10 +- content/docs/preloader/split-reveal.mdx | 8 +- content/docs/scroll/stacked-sections.mdx | 6 +- scripts/build-registry.js | 8 ++ 45 files changed, 651 insertions(+), 394 deletions(-) create mode 100644 animata/background/animated-beam.css create mode 100644 animata/background/shooting-stars.css create mode 100644 animata/button/algolia-blue-button.css create mode 100644 animata/button/algolia-white-button.css create mode 100644 animata/button/ripple-button.css create mode 100644 animata/card/card-spread.css create mode 100644 animata/card/collab-card.css create mode 100644 animata/container/animated-border-trail.css create mode 100644 animata/container/marquee.css create mode 100644 animata/fabs/flower-menu.css create mode 100644 animata/fabs/speed-dial.css create mode 100644 animata/feature-cards/content-scan.css create mode 100644 animata/preloader/split-reveal.css create mode 100644 animata/scroll/stacked-sections.css diff --git a/CLAUDE.md b/CLAUDE.md index 1f6f6f07..04be73b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ Animata is a free, open-source library of animated React components built with N - Use `cn()` from `@/lib/utils` for class merging — never raw string concatenation - Tailwind for layout, color, spacing, and transitions on the component itself - Co-located `.css` imported from the TSX for keyframes, pseudo-elements, and selectors Tailwind cannot express (see `roll-text.tsx`, `metis-text.tsx`) -- Inline ` - {/* soft glow */}
- -
- Algolia Blue ); diff --git a/animata/button/algolia-white-button.css b/animata/button/algolia-white-button.css new file mode 100644 index 00000000..b4637944 --- /dev/null +++ b/animata/button/algolia-white-button.css @@ -0,0 +1,27 @@ +@layer components { + .algolia-white-btn { + box-shadow: + rgba(45, 35, 66, 0.4) 0 2px 4px, + rgba(45, 35, 66, 0.3) 0 7px 13px -3px, + #d6d6e7 0 -3px 0 inset; + } + + .algolia-white-btn:hover { + box-shadow: + rgba(45, 35, 66, 0.4) 0 4px 8px, + rgba(45, 35, 66, 0.3) 0 7px 13px -3px, + #d6d6e7 0 -3px 0 inset; + } + + .algolia-white-btn:focus { + box-shadow: + #d6d6e7 0 0 0 1.5px inset, + rgba(45, 35, 66, 0.4) 0 2px 4px, + rgba(45, 35, 66, 0.3) 0 7px 13px -3px, + #d6d6e7 0 -3px 0 inset; + } + + .algolia-white-btn:active { + box-shadow: #d6d6e7 0 3px 7px inset; + } +} diff --git a/animata/button/algolia-white-button.tsx b/animata/button/algolia-white-button.tsx index dfc8f424..ac7d4b04 100644 --- a/animata/button/algolia-white-button.tsx +++ b/animata/button/algolia-white-button.tsx @@ -1,20 +1,8 @@ +import "./algolia-white-button.css"; + export default function AlgoliaWhiteButton() { return ( ); diff --git a/animata/button/ripple-button.css b/animata/button/ripple-button.css new file mode 100644 index 00000000..2f599332 --- /dev/null +++ b/animata/button/ripple-button.css @@ -0,0 +1,43 @@ +@layer components { + .ripple { + position: absolute; + border-radius: 50%; + pointer-events: none; + background-color: #000000; + z-index: 1; + opacity: 0; + transition: transform 50ms linear; + } + + .ripple-enter { + animation: ripple-enter 250ms ease-out forwards; + } + + .ripple-leave { + animation: ripple-leave 250ms ease-out forwards; + } + + @keyframes ripple-enter { + from { + transform: scale(0); + opacity: 1; + } + + to { + transform: scale(1); + opacity: 1; + } + } + + @keyframes ripple-leave { + from { + transform: scale(1); + opacity: 1; + } + + to { + transform: scale(0); + opacity: 1; + } + } +} diff --git a/animata/button/ripple-button.tsx b/animata/button/ripple-button.tsx index 84955925..ebff66f7 100644 --- a/animata/button/ripple-button.tsx +++ b/animata/button/ripple-button.tsx @@ -1,6 +1,9 @@ "use client"; + import { useCallback, useRef, useState } from "react"; +import "./ripple-button.css"; + interface RippleButtonProps extends React.ButtonHTMLAttributes { children: React.ReactNode; } @@ -97,31 +100,6 @@ export default function RippleButton({ children, ...props }: RippleButtonProps) > {children} - ); } diff --git a/animata/card/card-spread.css b/animata/card/card-spread.css new file mode 100644 index 00000000..5335bbf4 --- /dev/null +++ b/animata/card/card-spread.css @@ -0,0 +1,31 @@ +@layer components { + .card-spread-stage { + /* Quick throw, soft settle — expand on click only */ + --ease-throw: cubic-bezier(0.22, 1.18, 0.36, 1); + } + + .card-spread-motion--expand { + transition: transform 480ms var(--ease-throw); + } + + .card-spread-motion--collapse { + transition: transform 520ms cubic-bezier(0.33, 1, 0.68, 1); + } + + .card-spread-stage--expand { + transition: width 480ms var(--ease-throw); + } + + .card-spread-stage--collapse { + transition: width 520ms cubic-bezier(0.33, 1, 0.68, 1); + } + + @media (prefers-reduced-motion: reduce) { + .card-spread-motion--expand, + .card-spread-motion--collapse, + .card-spread-stage--expand, + .card-spread-stage--collapse { + transition-duration: 0.01ms !important; + } + } +} diff --git a/animata/card/card-spread.tsx b/animata/card/card-spread.tsx index 683fe13b..216571bc 100644 --- a/animata/card/card-spread.tsx +++ b/animata/card/card-spread.tsx @@ -6,6 +6,8 @@ import Notes, { NotesCard } from "@/animata/widget/notes"; import ShoppingList from "@/animata/widget/shopping-list"; import { cn } from "@/lib/utils"; +import "./card-spread.css"; + /** w-48 + gap-3 — horizontal step between spread slots */ const CARD_STEP = "12.75rem"; @@ -72,38 +74,6 @@ export default function CardSpread() { return (
- -
{trailing} : null}

- -
); } diff --git a/animata/container/animated-border-trail.css b/animata/container/animated-border-trail.css new file mode 100644 index 00000000..2ff63086 --- /dev/null +++ b/animata/container/animated-border-trail.css @@ -0,0 +1,17 @@ +@layer components { + @property --border-trail-angle { + syntax: ""; + initial-value: 0deg; + inherits: false; + } + + @keyframes border-trail { + 0% { + --border-trail-angle: 0deg; + } + + 100% { + --border-trail-angle: 360deg; + } + } +} diff --git a/animata/container/animated-border-trail.tsx b/animata/container/animated-border-trail.tsx index 4b7c11f6..831011d0 100644 --- a/animata/container/animated-border-trail.tsx +++ b/animata/container/animated-border-trail.tsx @@ -1,5 +1,7 @@ import { cn } from "@/lib/utils"; +import "./animated-border-trail.css"; + interface AnimatedTrailProps extends React.HTMLAttributes { /** * The duration of the animation. @@ -33,19 +35,6 @@ export default function AnimatedBorderTrail({ {...props} className={cn("relative h-fit w-fit overflow-hidden rounded-2xl bg-gray-200 p-px", className)} > -
{ /** * Should the marquee scroll horizontally or vertically. @@ -54,25 +56,6 @@ export default function Marquee({ className, )} > - {Array.from({ length: repeat }).map((_, index) => (
>; href: string; @@ -200,18 +202,6 @@ export default function FlowerMenu({ style={{ width: containerSize, height: containerSize, minHeight: containerSize }} {...props} > - - - -
- -
); }; diff --git a/animata/preloader/split-reveal.css b/animata/preloader/split-reveal.css new file mode 100644 index 00000000..7e349c34 --- /dev/null +++ b/animata/preloader/split-reveal.css @@ -0,0 +1,52 @@ +@layer components { + @keyframes split-reveal-shutter-top { + from { + transform: translate3d(0, 0, 0); + } + + to { + transform: translate3d(0, -100%, 0); + } + } + + @keyframes split-reveal-shutter-bottom { + from { + transform: translate3d(0, 0, 0); + } + + to { + transform: translate3d(0, 100%, 0); + } + } + + [data-split-reveal-overlay] [data-split-reveal-progress] { + opacity: 1; + transition: opacity var(--split-reveal-progress-fade) ease-out; + } + + [data-split-reveal-overlay][data-phase="fade-ui"] [data-split-reveal-progress], + [data-split-reveal-overlay][data-phase="reveal"] [data-split-reveal-progress] { + opacity: 0; + } + + [data-split-reveal-overlay][data-phase="reveal"] [data-split-reveal-shutter="top"] { + animation: split-reveal-shutter-top var(--split-reveal-duration) cubic-bezier(0.76, 0, 0.24, 1) + forwards; + } + + [data-split-reveal-overlay][data-phase="reveal"] [data-split-reveal-shutter="bottom"] { + animation: split-reveal-shutter-bottom var(--split-reveal-duration) + cubic-bezier(0.76, 0, 0.24, 1) forwards; + } + + @media (prefers-reduced-motion: reduce) { + [data-split-reveal-overlay][data-phase="reveal"] [data-split-reveal-shutter] { + animation: none; + opacity: 0; + } + + [data-split-reveal-overlay] [data-split-reveal-progress] { + transition: none; + } + } +} diff --git a/animata/preloader/split-reveal.tsx b/animata/preloader/split-reveal.tsx index bb6e4e56..ebe3b070 100644 --- a/animata/preloader/split-reveal.tsx +++ b/animata/preloader/split-reveal.tsx @@ -15,6 +15,8 @@ import { import { cn } from "@/lib/utils"; +import "./split-reveal.css"; + type PreloaderPhase = "loading" | "fade-ui" | "reveal" | "done"; export interface SplitRevealProgressState { @@ -35,8 +37,6 @@ interface SplitRevealContextValue extends SplitRevealProgressState { const SplitRevealContext = createContext(null); -const REVEAL_EASE = "cubic-bezier(0.76, 0, 0.24, 1)"; - export function useSplitReveal() { const context = use(SplitRevealContext); if (!context) { @@ -167,59 +167,6 @@ function useScrollLock(active: boolean) { }, [active]); } -function SplitRevealStyles() { - return ( - - ); -} - function SplitRevealOverlayFrame({ className, children, ...props }: ComponentProps<"div">) { const { phase, zIndex, revealDuration, progressFadeMs, isActive } = useSplitReveal(); @@ -248,7 +195,6 @@ function SplitRevealOverlayFrame({ className, children, ...props }: ComponentPro data-split-reveal-overlay="" {...props} > - {children} ); diff --git a/animata/scroll/stacked-sections.css b/animata/scroll/stacked-sections.css new file mode 100644 index 00000000..cc4db84a --- /dev/null +++ b/animata/scroll/stacked-sections.css @@ -0,0 +1,5 @@ +@layer components { + [data-stacked-content][data-stacked-covered] { + transform-origin: 50% 0%; + } +} diff --git a/animata/scroll/stacked-sections.tsx b/animata/scroll/stacked-sections.tsx index 5a0c8119..01073f3c 100644 --- a/animata/scroll/stacked-sections.tsx +++ b/animata/scroll/stacked-sections.tsx @@ -4,6 +4,8 @@ import * as React from "react"; import { cn } from "@/lib/utils"; +import "./stacked-sections.css"; + export type StackedSectionsProps = { /** * One pane per direct child — sticky card > inner content (scale only). @@ -165,12 +167,6 @@ export default function StackedSections({ return ( <> - -
(tree: UnistTree) => { const rawString = pre.properties.__rawString__ as string | undefined; if (rawString?.startsWith("mkdir")) { - const path = rawString.split(" ").pop(); + const trimmed = rawString.trim(); + pre.properties.__unix__ = trimmed; + + const touchMatch = trimmed.match(/^mkdir\s+-p\s+(.+?)\s+&&\s+touch\s+(.+)$/); + if (touchMatch) { + const dir = touchMatch[1].trim().replace(/^["']|["']$/g, ""); + const files = touchMatch[2].trim().split(/\s+/); + pre.properties.__windows__ = [ + `mkdir "${dir}"`, + ...files.map((file) => `type null > "${file}"`), + ].join(" && "); + return; + } + + const path = trimmed.split(" ").pop(); if (!path) { return; } @@ -142,7 +157,6 @@ const postProcess = () => (tree: UnistTree) => { const filename = path.split("/").pop() ?? ""; const dir = path.replace(`/${filename}`, ""); pre.properties.__windows__ = `mkdir "${dir}" && type null > ${path}`; - pre.properties.__unix__ = `mkdir -p ${dir} && touch ${path}`; } if (rawString?.startsWith("npm install")) { @@ -165,6 +179,7 @@ const components = { AlertTitle, AlertDescription, InView, + TextAnimatorListDemo, PreviewContainer, PreviewGrid: ({ children }: { children: React.ReactNode }) => (
{children}
diff --git a/content/docs/background/animated-beam.mdx b/content/docs/background/animated-beam.mdx index df9dcce4..da836546 100644 --- a/content/docs/background/animated-beam.mdx +++ b/content/docs/background/animated-beam.mdx @@ -16,22 +16,19 @@ author: harimanok_ -No dependencies - -Everything ships in the one file, including the `meteor` keyframes (in an inline `