diff --git a/CLAUDE.md b/CLAUDE.md index 04be73b8..22ab7fb8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,44 +21,43 @@ Animata is a free, open-source library of animated React components built with N ## Changelog rule — ALWAYS update this -The changelog lives at `content/docs/changelog/` — one MDX file per month plus an index. +Release notes use **four tiers**. Full process, examples, and checklist: [contributing changelog](/docs/contributing/changelog). -### Structure +| Tier | Where | Length | +| --- | --- | --- | +| 1 — Site overview | `content/docs/changelog/index.mdx` | One line per month | +| 2 — Monthly release | `content/docs/changelog/YYYY-MM.mdx` | Paragraph + component cards | +| 3 — Category index | `content/docs/{category}/index.mdx` | Table: date · component · change | +| 4 — Component doc | `content/docs/{category}/{name}.mdx` → `## Changelog` | Dated bullets, most detail | ```text content/docs/changelog/ - index.mdx # overview page — shows 2 most recent months + table linking all others - 2026-04.mdx # April 2026 (newest first) - 2026-03.mdx - ... + index.mdx # tier 1 — recent months + table of all months + YYYY-MM.mdx # tier 2 — one file per month (newest first) +content/docs/{category}/ + index.mdx # tier 3 — Recent changes table + {name}.mdx # tier 4 — ## Changelog at bottom ``` ### When adding a new month -1. Create `content/docs/changelog/YYYY-MM.mdx` with this frontmatter: - ```yaml - --- - title: Month YYYY - description: One-line summary of the main themes. - date: YYYY-MM-DD - --- - ``` -2. Write the content as prose, not bullet lists. Each new component gets a short paragraph — what it is, when you'd use it, anything notable about the implementation. -3. Add the new month to `config/docs.ts` under the Changelog items array (newest first). -4. Update `content/docs/changelog/index.mdx`: promote the new month to "Recent releases" and move the oldest of the current two into the table. +1. Create `content/docs/changelog/YYYY-MM.mdx` with frontmatter (`title`, `description`, `date`). +2. Write tier-2 prose and `` blocks — see [contributing changelog](/docs/contributing/changelog). +3. Add the month to `config/docs.ts` under Changelog (newest first). +4. Update tier 1 in `changelog/index.mdx`: promote to **Recent releases**, demote oldest to **All releases**. ### When updating an existing month -Add to the relevant `YYYY-MM.mdx` file. No need to touch the index unless the summary line there is now misleading. +Add to the relevant `YYYY-MM.mdx`. Update tier 3 (category index table) and tier 4 (component `## Changelog`). Touch tier 1 only if the month summary line is now misleading. ### What warrants a changelog entry -| Action | Update changelog? | -|---|---| -| New component | Yes — new section in the current month file | -| Landing/docs UI update | Yes — brief paragraph | -| Major dependency upgrade | Yes — explain what changed and why it matters | -| User-visible bug fix | Yes — one sentence is fine | +| Action | Update? | +| --- | --- | +| New component | All four tiers | +| User-visible update or fix | Tiers 2–4; tier 1 if it shapes the month | +| Landing/docs UI (site-wide) | Tiers 1–2 | +| Major dependency upgrade | Tiers 1–2 | | Dependency patch bumps | No | | CI / GitHub Actions only | No | | Typo fix in docs | No | @@ -66,9 +65,8 @@ Add to the relevant `YYYY-MM.mdx` file. No need to touch the index unless the su ### Writing style - Past tense. "Added X" not "We're excited to announce X." -- No emojis, no prefix symbols (no ✨ 🛠 🐛). -- Each component gets a description of what it does and when you'd use it — not just its name. -- If a fix has a root cause worth knowing, say so briefly. +- No emojis, no prefix symbols. +- Detail increases down the tiers — one line at the top, bullets at the component doc. ## File map (quick reference) @@ -97,6 +95,7 @@ content/docs/ contributing/ category-glyphs.mdx # Category tile SVG guide (published) category-glyphs-spec.md # Full spec — keep in sync; not under animata/ + changelog.mdx # Four-tier release notes process config/ docs.ts # Sidebar nav config — register new component categories here styles/globals.css # Tailwind v4 theme tokens diff --git a/animata/card/card-spread.css b/animata/card/card-spread.css index 5335bbf4..d374079e 100644 --- a/animata/card/card-spread.css +++ b/animata/card/card-spread.css @@ -1,31 +1,187 @@ +@reference "../../styles/globals.css"; + @layer components { - .card-spread-stage { - /* Quick throw, soft settle — expand on click only */ + .card-spread { --ease-throw: cubic-bezier(0.22, 1.18, 0.36, 1); + --ease-gather: cubic-bezier(0.33, 1, 0.68, 1); + } + + .card-spread-stage { + /* + * overflow-x: auto forces overflow-y: auto — pad vertically so spread + * tilt and hover peek are not clipped. + */ + @apply w-full px-4 py-8; + } + + /* Deck: center the wide grid, clip sides — stack sits at grid center (col 1.5). */ + .card-spread:not(:has(.card-spread-toggle:checked)) .card-spread-stage { + @apply flex items-end justify-center overflow-x-hidden; + } + + /* Spread: horizontal scroll when needed; center the row when it fits. */ + .card-spread:has(.card-spread-toggle:checked) .card-spread-stage { + @apply overflow-x-auto overscroll-x-contain; + } + + @media (min-width: 64rem) { + .card-spread:has(.card-spread-toggle:checked) .card-spread-stage { + @apply flex items-end justify-center; + } + } + + .card-spread-container { + --card-gap: 0.75rem; + /* 4× w-48 + 3× gap-3 — never shrink or deck offsets miss grid tracks */ + @apply grid w-fit min-w-[calc(4*12rem+3*0.75rem)] shrink-0 grid-cols-4 gap-3; + } + + .card-spread-toggle { + @apply sr-only; } - .card-spread-motion--expand { + .card-spread-open { + @apply relative inline-flex cursor-pointer items-center rounded-full border border-border bg-background px-3.5 py-1.5 text-sm font-medium text-foreground transition-colors select-none; + } + + .card-spread-open:hover { + @apply bg-accent/10; + } + + .card-spread-open:has(.card-spread-toggle:focus-visible) { + @apply outline-2 outline-[#ffcc00] outline-offset-2; + } + + .card-spread-open__text--collapse { + @apply hidden; + } + + .card-spread:has(.card-spread-toggle:checked) .card-spread-open__text--expand { + @apply hidden; + } + + .card-spread:has(.card-spread-toggle:checked) .card-spread-open__text--collapse { + @apply inline; + } + + .card-spread-item { + @apply origin-bottom; + } + + .card-spread-item:nth-child(1) { + @apply z-10; + } + + .card-spread-item:nth-child(2) { + @apply z-20; + } + + .card-spread-item:nth-child(3) { + @apply z-30; + } + + .card-spread-item:nth-child(4) { + @apply z-40; + } + + .card-spread-item__internal { + @apply origin-bottom motion-reduce:transition-none; + + transition: transform 520ms var(--ease-gather); + } + + .card-spread:has(.card-spread-toggle:checked) .card-spread-item__internal { transition: transform 480ms var(--ease-throw); } - .card-spread-motion--collapse { - transition: transform 520ms cubic-bezier(0.33, 1, 0.68, 1); + /* + * Deck — 1×4 grid, pull each card toward col 1.5 via transform. + * Same explicit calc() on all breakpoints so spread animates on mobile too. + */ + .card-spread:not(:has(.card-spread-toggle:checked)) .card-spread-item:nth-child(1) + .card-spread-item__internal { + transform: translate3d(calc(150% + 1.125rem), 0, 0); + } + + .card-spread:not(:has(.card-spread-toggle:checked)) + .card-spread-item:nth-child(2) + .card-spread-item__internal { + transform: translate3d(calc(50% + 0.375rem), 0, 0); + } + + .card-spread:not(:has(.card-spread-toggle:checked)) + .card-spread-item:nth-child(3) + .card-spread-item__internal { + transform: translate3d(calc(-50% - 0.375rem), 0, 0); + } + + .card-spread:not(:has(.card-spread-toggle:checked)) + .card-spread-item:nth-child(4) + .card-spread-item__internal { + transform: translate3d(calc(-150% - 1.125rem), 0, 0); + } + + /* Spread — layout slots, light rotation */ + .card-spread:has(.card-spread-toggle:checked) .card-spread-item:nth-child(1) + .card-spread-item__internal { + transform: translate3d(0, 0, 0) rotate(-2deg); + } + + .card-spread:has(.card-spread-toggle:checked) + .card-spread-item:nth-child(2) + .card-spread-item__internal { + transform: translate3d(0, 0.5rem, 0) rotate(3deg); + } + + .card-spread:has(.card-spread-toggle:checked) + .card-spread-item:nth-child(3) + .card-spread-item__internal { + transform: translate3d(0.25rem, 0, 0) rotate(-2deg); + } + + .card-spread:has(.card-spread-toggle:checked) + .card-spread-item:nth-child(4) + .card-spread-item__internal { + transform: translate3d(0, 0, 0) rotate(2deg); + } + + /* Hover peek — deck only */ + .card-spread-item__hover { + @apply origin-bottom transition-transform duration-300 ease-out motion-reduce:transition-none; + } + + .card-spread:not(:has(.card-spread-toggle:checked)) + .card-spread-stage:hover + .card-spread-item:nth-child(1) + .card-spread-item__hover { + @apply -rotate-1; + } + + .card-spread:not(:has(.card-spread-toggle:checked)) + .card-spread-stage:hover + .card-spread-item:nth-child(2) + .card-spread-item__hover { + @apply -rotate-2; } - .card-spread-stage--expand { - transition: width 480ms var(--ease-throw); + .card-spread:not(:has(.card-spread-toggle:checked)) + .card-spread-stage:hover + .card-spread-item:nth-child(3) + .card-spread-item__hover { + @apply rotate-1; } - .card-spread-stage--collapse { - transition: width 520ms cubic-bezier(0.33, 1, 0.68, 1); + .card-spread:not(:has(.card-spread-toggle:checked)) + .card-spread-stage:hover + .card-spread-item:nth-child(4) + .card-spread-item__hover { + @apply rotate-2; } @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; + .card-spread-item__internal, + .card-spread-item__hover { + @apply !duration-[0.01ms]; } } } diff --git a/animata/card/card-spread.tsx b/animata/card/card-spread.tsx index 216571bc..9e32c811 100644 --- a/animata/card/card-spread.tsx +++ b/animata/card/card-spread.tsx @@ -1,21 +1,8 @@ -"use client"; - -import { useState } from "react"; - 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"; - -/** 4× w-48 + 3× gap-3 */ -const SPREAD_WIDTH = "calc(4 * 12rem + 3 * 0.75rem)"; - -const DECK_TRANSFORM = "translateX(0) rotate(0deg)"; - function Reminders() { return ( -
setExpanded((open) => !open)} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - setExpanded((open) => !open); - } - }} - className={cn( - "card-spread-stage group/stack relative h-64 outline-none focus-visible:ring-2 focus-visible:ring-[#ffcc00] focus-visible:ring-offset-2 focus-visible:ring-offset-background", - isExpanded - ? "card-spread-stage--expand cursor-default" - : "card-spread-stage--collapse w-48 cursor-pointer", - )} - style={isExpanded ? { width: SPREAD_WIDTH } : undefined} - > - {cards.map((item, index) => { - const Card = item.component; - return ( -
-
- +
+ + +
+
+ {cards.map((item) => { + const Card = item.component; + + return ( +
+
+
+ +
+
-
- ); - })} + ); + })} +
); diff --git a/animata/card/card-stack-profile.tsx b/animata/card/card-stack-profile.tsx new file mode 100644 index 00000000..3edc21d9 --- /dev/null +++ b/animata/card/card-stack-profile.tsx @@ -0,0 +1,246 @@ +"use client"; + +import type { LucideIcon } from "lucide-react"; +import { Heart, MessageCircle } from "lucide-react"; +import type { ComponentProps, ReactNode } from "react"; + +import CardStack, { + type CardStackItem, + type CardStackLayerMotion, +} from "@/animata/card/card-stack"; +import { CardStackMaskDefs } from "@/components/shapes/card-stack-mask-defs"; +import { cn } from "@/lib/utils"; + +export const CARD_STACK_MASK_IDS = [ + "cardstack_mask_ellipse-1", + "cardstack_mask_flower-14", + "cardstack_mask_flower-1", + "cardstack_mask_misc-5", +] as const; + +export type CardStackProfileMaskId = (typeof CARD_STACK_MASK_IDS)[number]; + +export type CardStackProfileMediaAspect = "fill" | "square" | "4/5" | "3/4" | "16/10"; + +export interface CardStackProfileItem extends CardStackItem { + image: string; + title: string; + tagline: string; + counts?: { + like: number; + comment: number; + }; + maskId: CardStackProfileMaskId; +} + +const CARD_STACK_MASK_STYLE = { + maskSize: "cover", + maskPosition: "center", + maskRepeat: "no-repeat", +} as const; + +const MEDIA_ASPECT_CLASS: Record, string> = { + square: "aspect-square", + "4/5": "aspect-[4/5]", + "3/4": "aspect-[3/4]", + "16/10": "aspect-[16/10]", +}; + +export function CardStackProfileMasks({ className }: { className?: string }) { + return ; +} + +export function CardStackProfileLiveRegion({ + className, + item, +}: { + className?: string; + item: CardStackProfileItem | undefined; +}) { + return ( +

+ {item ? `Showing ${item.title}, ${item.tagline}` : "No cards available"} +

+ ); +} + +export function CardStackProfileHeader({ className, ...props }: ComponentProps<"header">) { + return
; +} + +export function CardStackProfileAvatar({ + src, + className, + ...props +}: ComponentProps<"div"> & { src: string }) { + return ( +
+ +
+ ); +} + +export function CardStackProfileMeta({ + title, + tagline, + className, + ...props +}: ComponentProps<"div"> & { title: string; tagline: string }) { + return ( +
+

+ {title} +

+

+ {tagline} +

+
+ ); +} + +export function CardStackProfileMedia({ + src, + alt, + maskId, + aspect = "square", + className, + style, + ...props +}: Omit, "src" | "alt"> & { + src: string; + alt: string; + maskId: CardStackProfileMaskId; + aspect?: CardStackProfileMediaAspect; +}) { + const maskStyle = { + ...CARD_STACK_MASK_STYLE, + maskImage: `url(#${maskId})`, + WebkitMaskImage: `url(#${maskId})`, + WebkitMaskSize: CARD_STACK_MASK_STYLE.maskSize, + WebkitMaskPosition: CARD_STACK_MASK_STYLE.maskPosition, + WebkitMaskRepeat: CARD_STACK_MASK_STYLE.maskRepeat, + ...style, + }; + + return ( +
+ {alt} +
+ ); +} + +export function CardStackProfileFooter({ className, ...props }: ComponentProps<"footer">) { + return ( +