Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/propel/src/components/preview-card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<typeof PreviewCard>;

export default meta;
Expand All @@ -37,12 +52,12 @@ export const Default: Story = {
Plane
</PreviewCardTrigger>
<PreviewCardContent side="top">
<div className="flex w-56 flex-col gap-1">
<span className="text-14 font-semibold text-primary">Plane</span>
<span className="text-13 text-secondary">
<PreviewCardBody>
<PreviewCardTitle>Plane</PreviewCardTitle>
<PreviewCardDescription>
Open-source project management for issues, sprints, and roadmaps.
</span>
</div>
</PreviewCardDescription>
</PreviewCardBody>
<PreviewCardArrow />
</PreviewCardContent>
</PreviewCard>{" "}
Expand Down
7 changes: 7 additions & 0 deletions packages/propel/src/ui/preview-card/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
4 changes: 3 additions & 1 deletion packages/propel/src/ui/preview-card/preview-card-arrow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PreviewCard as BasePreviewCard } from "@base-ui/react/preview-card";

import { previewCardArrowVariants } from "./variants";

export type PreviewCardArrowProps = Omit<BasePreviewCard.Arrow.Props, "className" | "style">;

/** The optional caret pointing from the popup back to the trigger. Maps 1:1 to `PreviewCard.Arrow`. */
export function PreviewCardArrow(props: PreviewCardArrowProps) {
return <BasePreviewCard.Arrow {...props} />;
return <BasePreviewCard.Arrow className={previewCardArrowVariants()} {...props} />;
}
18 changes: 18 additions & 0 deletions packages/propel/src/ui/preview-card/preview-card-body.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className={previewCardBodyVariants()} {...props} />;
}
16 changes: 16 additions & 0 deletions packages/propel/src/ui/preview-card/preview-card-description.tsx
Original file line number Diff line number Diff line change
@@ -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 <p className={previewCardDescriptionVariants()} {...props} />;
}
17 changes: 17 additions & 0 deletions packages/propel/src/ui/preview-card/preview-card-image.tsx
Original file line number Diff line number Diff line change
@@ -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 <img className={previewCardImageVariants()} {...props} />;
}
16 changes: 16 additions & 0 deletions packages/propel/src/ui/preview-card/preview-card-title.tsx
Original file line number Diff line number Diff line change
@@ -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 <p className={previewCardTitleVariants()} {...props} />;
}
16 changes: 11 additions & 5 deletions packages/propel/src/ui/preview-card/preview-card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import {
PreviewCard,
PreviewCardArrow,
PreviewCardBackdrop,
PreviewCardBody,
PreviewCardDescription,
PreviewCardPopup,
PreviewCardPortal,
PreviewCardPositioner,
PreviewCardTitle,
PreviewCardTrigger,
PreviewCardViewport,
} from "./index";
Expand All @@ -29,6 +32,9 @@ const meta = {
PreviewCardViewport,
PreviewCardPopup,
PreviewCardArrow,
PreviewCardBody,
PreviewCardTitle,
PreviewCardDescription,
},
} satisfies Meta<typeof PreviewCard>;

Expand Down Expand Up @@ -59,12 +65,12 @@ export const Anatomy: Story = {
<PreviewCardBackdrop />
<PreviewCardPositioner side="top" sideOffset={4}>
<PreviewCardPopup>
<div className="flex w-56 flex-col gap-1">
<span className="text-14 font-semibold text-primary">Plane</span>
<span className="text-13 text-secondary">
<PreviewCardBody>
<PreviewCardTitle>Plane</PreviewCardTitle>
<PreviewCardDescription>
Open-source project management for issues, sprints, and roadmaps.
</span>
</div>
</PreviewCardDescription>
</PreviewCardBody>
<PreviewCardArrow />
</PreviewCardPopup>
</PreviewCardPositioner>
Expand Down
36 changes: 33 additions & 3 deletions packages/propel/src/ui/preview-card/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");