diff --git a/packages/propel/src/components/preview-card/index.tsx b/packages/propel/src/components/preview-card/index.tsx index db1417f2..b8e6dbf3 100644 --- a/packages/propel/src/components/preview-card/index.tsx +++ b/packages/propel/src/components/preview-card/index.tsx @@ -5,6 +5,14 @@ export { type PreviewCardProps, PreviewCardArrow, type PreviewCardArrowProps, + PreviewCardBody, + type PreviewCardBodyProps, + PreviewCardDescription, + type PreviewCardDescriptionProps, + PreviewCardImage, + type PreviewCardImageProps, + PreviewCardTitle, + type PreviewCardTitleProps, PreviewCardTrigger, type PreviewCardTriggerProps, } from "../../ui/preview-card"; diff --git a/packages/propel/src/components/preview-card/preview-card.stories.tsx b/packages/propel/src/components/preview-card/preview-card.stories.tsx index 69fc8617..0d6e21cc 100644 --- a/packages/propel/src/components/preview-card/preview-card.stories.tsx +++ b/packages/propel/src/components/preview-card/preview-card.stories.tsx @@ -1,7 +1,15 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, userEvent, waitFor, within } from "storybook/test"; -import { PreviewCard, PreviewCardArrow, PreviewCardContent, PreviewCardTrigger } from "./index"; +import { + PreviewCard, + PreviewCardArrow, + PreviewCardBody, + PreviewCardContent, + PreviewCardDescription, + PreviewCardTitle, + PreviewCardTrigger, +} from "./index"; // Components-tier story: uses the ready-made `PreviewCardContent`, which composes // the portal/backdrop/positioner/popup so a consumer only writes the trigger and @@ -10,7 +18,14 @@ import { PreviewCard, PreviewCardArrow, PreviewCardContent, PreviewCardTrigger } const meta = { title: "Components/PreviewCard", component: PreviewCard, - subcomponents: { PreviewCardTrigger, PreviewCardContent, PreviewCardArrow }, + subcomponents: { + PreviewCardTrigger, + PreviewCardContent, + PreviewCardArrow, + PreviewCardBody, + PreviewCardTitle, + PreviewCardDescription, + }, } satisfies Meta; export default meta; @@ -37,12 +52,12 @@ export const Default: Story = { Plane -
- Plane - + + Plane + Open-source project management for issues, sprints, and roadmaps. - -
+ +
{" "} diff --git a/packages/propel/src/ui/preview-card/index.tsx b/packages/propel/src/ui/preview-card/index.tsx index 6e1b8c41..be0b08e9 100644 --- a/packages/propel/src/ui/preview-card/index.tsx +++ b/packages/propel/src/ui/preview-card/index.tsx @@ -1,8 +1,15 @@ export { PreviewCard, type PreviewCardProps } from "./preview-card"; export { PreviewCardArrow, type PreviewCardArrowProps } from "./preview-card-arrow"; export { PreviewCardBackdrop, type PreviewCardBackdropProps } from "./preview-card-backdrop"; +export { PreviewCardBody, type PreviewCardBodyProps } from "./preview-card-body"; +export { + PreviewCardDescription, + type PreviewCardDescriptionProps, +} from "./preview-card-description"; +export { PreviewCardImage, type PreviewCardImageProps } from "./preview-card-image"; export { PreviewCardPopup, type PreviewCardPopupProps } from "./preview-card-popup"; export { PreviewCardPortal, type PreviewCardPortalProps } from "./preview-card-portal"; export { PreviewCardPositioner, type PreviewCardPositionerProps } from "./preview-card-positioner"; +export { PreviewCardTitle, type PreviewCardTitleProps } from "./preview-card-title"; export { PreviewCardTrigger, type PreviewCardTriggerProps } from "./preview-card-trigger"; export { PreviewCardViewport, type PreviewCardViewportProps } from "./preview-card-viewport"; diff --git a/packages/propel/src/ui/preview-card/preview-card-arrow.tsx b/packages/propel/src/ui/preview-card/preview-card-arrow.tsx index 0d240f8a..ad816c0b 100644 --- a/packages/propel/src/ui/preview-card/preview-card-arrow.tsx +++ b/packages/propel/src/ui/preview-card/preview-card-arrow.tsx @@ -1,8 +1,10 @@ import { PreviewCard as BasePreviewCard } from "@base-ui/react/preview-card"; +import { previewCardArrowVariants } from "./variants"; + export type PreviewCardArrowProps = Omit; /** The optional caret pointing from the popup back to the trigger. Maps 1:1 to `PreviewCard.Arrow`. */ export function PreviewCardArrow(props: PreviewCardArrowProps) { - return ; + return ; } diff --git a/packages/propel/src/ui/preview-card/preview-card-body.tsx b/packages/propel/src/ui/preview-card/preview-card-body.tsx new file mode 100644 index 00000000..e64c54c0 --- /dev/null +++ b/packages/propel/src/ui/preview-card/preview-card-body.tsx @@ -0,0 +1,18 @@ +import type * as React from "react"; + +import { previewCardBodyVariants } from "./variants"; + +export type PreviewCardBodyProps = Omit< + React.ComponentPropsWithoutRef<"div">, + "className" | "style" +>; + +/** + * The text content area of the card — typically holds a `PreviewCardTitle` and + * `PreviewCardDescription` stacked in a column. Owns the padding so a full-bleed `PreviewCardImage` + * can sit edge-to-edge above it; both the column layout and the padding are "always the same" per + * the design spec. + */ +export function PreviewCardBody(props: PreviewCardBodyProps) { + return
; +} diff --git a/packages/propel/src/ui/preview-card/preview-card-description.tsx b/packages/propel/src/ui/preview-card/preview-card-description.tsx new file mode 100644 index 00000000..fce96979 --- /dev/null +++ b/packages/propel/src/ui/preview-card/preview-card-description.tsx @@ -0,0 +1,16 @@ +import type * as React from "react"; + +import { previewCardDescriptionVariants } from "./variants"; + +export type PreviewCardDescriptionProps = Omit< + React.ComponentPropsWithoutRef<"p">, + "className" | "style" +>; + +/** + * Supporting description text beneath the title. Bakes in the secondary text style — 13px in + * secondary text colour — per the "always the same" items in the design spec. + */ +export function PreviewCardDescription(props: PreviewCardDescriptionProps) { + return

; +} diff --git a/packages/propel/src/ui/preview-card/preview-card-image.tsx b/packages/propel/src/ui/preview-card/preview-card-image.tsx new file mode 100644 index 00000000..5b71c64a --- /dev/null +++ b/packages/propel/src/ui/preview-card/preview-card-image.tsx @@ -0,0 +1,17 @@ +import type * as React from "react"; + +import { previewCardImageVariants } from "./variants"; + +export type PreviewCardImageProps = Omit< + React.ComponentPropsWithoutRef<"img">, + "className" | "style" +>; + +/** + * The thumbnail image shown inside the popup. Bakes in overflow-hidden and object-cover so the + * thumbnail always clips and fills its container — these are "always the same" per the design spec. + * Width and height are set by the consumer's layout. + */ +export function PreviewCardImage(props: PreviewCardImageProps) { + return ; +} diff --git a/packages/propel/src/ui/preview-card/preview-card-title.tsx b/packages/propel/src/ui/preview-card/preview-card-title.tsx new file mode 100644 index 00000000..0755fc20 --- /dev/null +++ b/packages/propel/src/ui/preview-card/preview-card-title.tsx @@ -0,0 +1,16 @@ +import type * as React from "react"; + +import { previewCardTitleVariants } from "./variants"; + +export type PreviewCardTitleProps = Omit< + React.ComponentPropsWithoutRef<"p">, + "className" | "style" +>; + +/** + * The card's primary heading. Bakes in the title font style — semibold 14px in primary text colour + * — per the "always the same" items in the design spec. + */ +export function PreviewCardTitle(props: PreviewCardTitleProps) { + return

; +} diff --git a/packages/propel/src/ui/preview-card/preview-card.stories.tsx b/packages/propel/src/ui/preview-card/preview-card.stories.tsx index 8d000e4c..d6f5cdb0 100644 --- a/packages/propel/src/ui/preview-card/preview-card.stories.tsx +++ b/packages/propel/src/ui/preview-card/preview-card.stories.tsx @@ -5,9 +5,12 @@ import { PreviewCard, PreviewCardArrow, PreviewCardBackdrop, + PreviewCardBody, + PreviewCardDescription, PreviewCardPopup, PreviewCardPortal, PreviewCardPositioner, + PreviewCardTitle, PreviewCardTrigger, PreviewCardViewport, } from "./index"; @@ -29,6 +32,9 @@ const meta = { PreviewCardViewport, PreviewCardPopup, PreviewCardArrow, + PreviewCardBody, + PreviewCardTitle, + PreviewCardDescription, }, } satisfies Meta; @@ -59,12 +65,12 @@ export const Anatomy: Story = { -

- Plane - + + Plane + Open-source project management for issues, sprints, and roadmaps. - -
+ + diff --git a/packages/propel/src/ui/preview-card/variants.ts b/packages/propel/src/ui/preview-card/variants.ts index 51a502f6..f7611c19 100644 --- a/packages/propel/src/ui/preview-card/variants.ts +++ b/packages/propel/src/ui/preview-card/variants.ts @@ -4,22 +4,52 @@ import { cva, cx } from "class-variance-authority"; // open state through `data-*` attributes, so there are no styling axes // (variant/tone/magnitude) to expose. The cva pairings below hold the static // chrome for every styled part in one place, with no `className` at the boundary. +// +// The anatomy also includes four propel-extended parts — Body, Image, Title, and +// Description — that have no Base UI counterpart. Their styles below encode the +// "always the same" items from the design spec: the text content area's layout + +// padding (Body), the image overflow/cover-fit (Image), and the title/description +// font hierarchy (Title/Description). export const previewCardPositionerVariants = cva("z-50 outline-none"); -// Like the shared anchored popup, but with `p-3` and a `max-w-80` to comfortably -// hold the richer link-preview content. +// The card surface. Padding lives on `PreviewCardBody` (per the spec, padding is +// "within the text content area") rather than here, so the popup is the bare +// surface: a `PreviewCardImage` clips itself (it carries its own overflow-hidden + +// rounding) and the body supplies its own padding. The popup keeps no inner padding +// so the arrow is never clipped. `max-w-80` keeps the link-preview content to a +// comfortable width. export const previewCardPopupVariants = cva( cx( - "max-w-80 origin-(--transform-origin) rounded-lg border-sm border-subtle bg-layer-1 p-3 shadow-overlay-100 outline-none", + "max-w-80 origin-(--transform-origin) rounded-lg border-sm border-subtle bg-layer-1 shadow-overlay-100 outline-none", "transition-[opacity,transform] duration-150", "data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0", ), ); +// Body: the text content area. Per the spec, the content layout (a column with the +// title above the description) and the padding around it are "always the same", so +// they live here rather than in the consumer's composition. +export const previewCardBodyVariants = cva("flex flex-col gap-1 p-3"); + export const previewCardBackdropVariants = cva( cx( "fixed inset-0 bg-backdrop transition-opacity duration-200", "data-ending-style:opacity-0 data-starting-style:opacity-0", ), ); + +// Arrow inherits the popup's background so the caret blends with the card surface. +export const previewCardArrowVariants = cva("text-layer-1"); + +// Image: overflow-hidden + object-cover bake in the "always the same" thumbnail +// treatment from the design spec (clip + cover-fit); the image rounds itself so it +// can sit inside the popup without the popup needing overflow-hidden (which would +// clip the arrow). Width/height are set by the consumer's layout. +export const previewCardImageVariants = cva("overflow-hidden rounded-md object-cover"); + +// Title: matches the heading hierarchy in the Figma spec. +export const previewCardTitleVariants = cva("text-14 font-semibold text-primary"); + +// Description: secondary supporting text beneath the title. +export const previewCardDescriptionVariants = cva("text-13 text-secondary");