diff --git a/animata/card/card-spread.tsx b/animata/card/card-spread.tsx index 354d1dd8..683fe13b 100644 --- a/animata/card/card-spread.tsx +++ b/animata/card/card-spread.tsx @@ -1,17 +1,27 @@ +"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"; +/** 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 ( ); @@ -28,66 +38,121 @@ function RemodelNotes() { ); } +/** Outer layer: hover peek only (ease-out, not ease-cc). */ +const hoverPeekClasses = [ + "group-hover/stack:-translate-y-1 group-hover/stack:-rotate-[1deg]", + "group-hover/stack:-translate-y-2 group-hover/stack:rotate-[2deg]", + "group-hover/stack:-translate-y-3 group-hover/stack:rotate-[3deg]", + "group-hover/stack:-translate-y-4 group-hover/stack:rotate-[4deg]", +] as const; + const cards = [ { component: Notes, - rotationClass: "", - revealClass: "-rotate-[2deg]", + spreadTransform: `translateX(0) rotate(-2deg)`, }, { component: ShoppingList, - rotationClass: "group-hover/spread:rotate-[15deg]", - revealClass: "rotate-[3deg] translate-y-2", + spreadTransform: `translateX(${CARD_STEP}) translateY(0.5rem) rotate(3deg)`, }, - { component: RemodelNotes, - rotationClass: "group-hover/spread:rotate-[30deg]", - revealClass: "-rotate-[2deg] translate-x-1", + spreadTransform: `translateX(calc(2 * ${CARD_STEP})) rotate(-2deg) translateX(0.25rem)`, }, - { component: Reminders, - rotationClass: "group-hover/spread:rotate-[45deg]", - revealClass: "rotate-[2deg]", + spreadTransform: `translateX(calc(3 * ${CARD_STEP})) rotate(2deg)`, }, -]; +] as const; + +const zIndex = ["z-10", "z-20", "z-30", "z-40"] as const; export default function CardSpread() { const [isExpanded, setExpanded] = useState(false); return ( -
- {cards.map((item, index) => { - return ( -
{ - setExpanded(!isExpanded); - e.preventDefault(); - }} - className={cn( - "transition-all duration-500 ease-in-out", - { - absolute: !isExpanded, - "origin-bottom": !isExpanded, - }, - !isExpanded && item.rotationClass, - isExpanded && item.revealClass, - )} - > - -
- ); - })} +
+ + +
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 ( +
+
+ +
+
+ ); + })} +
); } diff --git a/animata/card/collab-card.stories.tsx b/animata/card/collab-card.stories.tsx new file mode 100644 index 00000000..17ad642e --- /dev/null +++ b/animata/card/collab-card.stories.tsx @@ -0,0 +1,56 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import CollabCard from "@/animata/card/collab-card"; + +const meta = { + title: "Card/Collab Card", + component: CollabCard, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + greeting: { control: "text" }, + eyebrow: { control: "text" }, + intro: { control: "text" }, + conjunction: { control: "text" }, + trailing: { control: "text" }, + liveLabel: { control: "text" }, + backgroundUrl: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** Figma multiplayer — dashed frame, live presence, named cursor pills. */ +export const Primary: Story = { + args: { + greeting: "hello!", + eyebrow: "Now in multiplayer", + intro: "editing", + conjunction: "&", + trailing: "", + liveLabel: "Live · 4 editing", + }, + render: (args) => ( +
+ +
+ ), +}; + +/** Bento slot — scales via container queries, not viewport breakpoints. */ +export const InBentoGrid: Story = { + render: () => ( +
+
+ +
+
+ Drop into a wide bento cell — frame, greeting, pills, and cursors track the card width via{" "} + cqi. +
+
+ ), +}; diff --git a/animata/card/collab-card.tsx b/animata/card/collab-card.tsx new file mode 100644 index 00000000..22e5497e --- /dev/null +++ b/animata/card/collab-card.tsx @@ -0,0 +1,339 @@ +import { cn } from "@/lib/utils"; + +export interface Collaborator { + name: string; + /** Tailwind background class for the pill. */ + pill: string; + /** Tailwind text class for the pill label. */ + pillText?: string; + /** Tailwind text color class for the cursor (sets `color`, used by SVG `fill`). */ + cursor: string; +} + +export interface CollabCardProps { + greeting?: string; + eyebrow?: string; + intro?: string; + conjunction?: string; + trailing?: string; + /** Status line beside the live dot, e.g. `live · 4 editing`. */ + liveLabel?: string; + /** Swatch colors for the visible avatar stack (left → right). */ + presenceColors?: string[]; + /** Additional editors not shown as swatches — renders as `+N` on the stack. */ + extraCount?: number; + collaborators?: [Collaborator, Collaborator]; + /** Optional background image URL. Falls back to a dark canvas gradient. */ + backgroundUrl?: string; + className?: string; +} + +const defaultCollaborators: [Collaborator, Collaborator] = [ + { + name: "Dylan", + pill: "bg-[#A259FF]", + pillText: "text-white", + cursor: "text-[#A259FF]", + }, + { + name: "Evan", + pill: "bg-[#FF7262]", + pillText: "text-white", + cursor: "text-[#FF7262]", + }, +]; + +const defaultPresenceColors = ["#A259FF", "#FF7262", "#1ABCFE", "#0ACF83"]; + +function PresenceStack({ + colors, + extraCount, + className, +}: { + colors: string[]; + extraCount: number; + className?: string; +}) { + const overlap = "-ml-[1.08cqi]"; + + return ( + + ); +} + +function Cursor({ className }: { className?: string }) { + return ( + + ); +} + +function ClickBurst({ className }: { className?: string }) { + return ( +