diff --git a/.changeset/migrate-cardhorizontal-to-css-modules.md b/.changeset/migrate-cardhorizontal-to-css-modules.md
new file mode 100644
index 000000000..533b0385a
--- /dev/null
+++ b/.changeset/migrate-cardhorizontal-to-css-modules.md
@@ -0,0 +1,5 @@
+---
+'@clickhouse/click-ui': patch
+---
+
+Migrate CardHorizontal from styled-components to css modules with no change in behavior
diff --git a/src/components/CardHorizontal/CardHorizontal.module.css b/src/components/CardHorizontal/CardHorizontal.module.css
new file mode 100644
index 000000000..b76138adc
--- /dev/null
+++ b/src/components/CardHorizontal/CardHorizontal.module.css
@@ -0,0 +1,216 @@
+.header {
+ max-width: 100%;
+ gap: inherit;
+}
+
+.description {
+ display: flex;
+ width: 100%;
+ flex: 1;
+ flex-direction: column;
+ align-self: start;
+ gap: var(--click-card-horizontal-space-md-gap);
+}
+
+.cardicon {
+ width: var(--click-card-horizontal-icon-size-all);
+ height: var(--click-card-horizontal-icon-size-all);
+}
+
+.contentwrapper {
+ display: flex;
+ width: 100%;
+ flex-direction: row;
+}
+
+.contentwrapper_size_md {
+ gap: var(--click-card-horizontal-space-md-gap);
+}
+
+.contentwrapper_size_sm {
+ gap: var(--click-card-horizontal-space-sm-gap);
+}
+
+/* stylelint-disable-next-line media-feature-range-notation -- prefix notation
+ required for browser compatibility per .browserslistrc */
+@media (max-width: 768px) {
+ .contentwrapper {
+ flex-direction: column;
+ }
+}
+
+.icontextcontentwrapper {
+ display: flex;
+ width: 100%;
+ flex-direction: row;
+ align-items: center;
+}
+
+.icontextcontentwrapper_alignment_top {
+ align-items: flex-start;
+}
+
+.icontextcontentwrapper_alignment_center {
+ align-items: center;
+}
+
+.icontextcontentwrapper_size_md {
+ gap: var(--click-card-horizontal-space-md-gap);
+}
+
+.icontextcontentwrapper_size_sm {
+ gap: var(--click-card-horizontal-space-sm-gap);
+}
+
+.wrapper {
+ display: inline-flex;
+ width: 100%;
+ max-width: 100%;
+ justify-content: flex-start;
+ align-items: center;
+ align-self: auto;
+ border: 1px solid var(--card-stroke-default);
+ border-radius: var(--click-card-horizontal-radii-all);
+ background: var(--card-bg-default);
+ color: var(--card-title-default);
+ font: var(--click-card-horizontal-typography-title-default);
+}
+
+.wrapper_color_default {
+ --card-bg-default: var(--click-card-horizontal-default-color-background-default);
+ --card-bg-hover: var(--click-card-horizontal-default-color-background-hover);
+ --card-bg-active: var(--click-card-horizontal-default-color-background-active);
+ --card-bg-disabled: var(--click-card-horizontal-default-color-background-disabled);
+ --card-title-default: var(--click-card-horizontal-default-color-title-default);
+ --card-title-hover: var(--click-card-horizontal-default-color-title-hover);
+ --card-title-active: var(--click-card-horizontal-default-color-title-active);
+ --card-title-disabled: var(--click-card-horizontal-default-color-title-disabled);
+ --card-stroke-default: var(--click-card-horizontal-default-color-stroke-default);
+ --card-stroke-hover: var(--click-card-horizontal-default-color-stroke-hover);
+ --card-stroke-active: var(--click-card-horizontal-default-color-stroke-active);
+ --card-stroke-disabled: var(--click-card-horizontal-default-color-stroke-disabled);
+ --card-desc-default: var(--click-card-horizontal-default-color-description-default);
+ --card-desc-hover: var(--click-card-horizontal-default-color-description-hover);
+ --card-desc-active: var(--click-card-horizontal-default-color-description-active);
+ --card-desc-disabled: var(--click-card-horizontal-default-color-description-disabled);
+}
+
+.wrapper_color_muted {
+ --card-bg-default: var(--click-card-horizontal-muted-color-background-default);
+ --card-bg-hover: var(--click-card-horizontal-muted-color-background-hover);
+ --card-bg-active: var(--click-card-horizontal-muted-color-background-active);
+ --card-bg-disabled: var(--click-card-horizontal-muted-color-background-disabled);
+ --card-title-default: var(--click-card-horizontal-muted-color-title-default);
+ --card-title-hover: var(--click-card-horizontal-muted-color-title-hover);
+ --card-title-active: var(--click-card-horizontal-muted-color-title-active);
+ --card-title-disabled: var(--click-card-horizontal-muted-color-title-disabled);
+ --card-stroke-default: var(--click-card-horizontal-muted-color-stroke-default);
+ --card-stroke-hover: var(--click-card-horizontal-muted-color-stroke-hover);
+ --card-stroke-active: var(--click-card-horizontal-muted-color-stroke-active);
+ --card-stroke-disabled: var(--click-card-horizontal-muted-color-stroke-disabled);
+ --card-desc-default: var(--click-card-horizontal-muted-color-description-default);
+ --card-desc-hover: var(--click-card-horizontal-muted-color-description-hover);
+ --card-desc-active: var(--click-card-horizontal-muted-color-description-active);
+ --card-desc-disabled: var(--click-card-horizontal-muted-color-description-disabled);
+}
+
+.wrapper_alignment_top {
+ align-items: flex-start;
+ align-self: stretch;
+}
+
+.wrapper_alignment_center {
+ align-items: center;
+ align-self: auto;
+}
+
+.wrapper_size_md {
+ padding: var(--click-card-horizontal-space-md-y) var(--click-card-horizontal-space-md-x);
+}
+
+.wrapper_size_sm {
+ padding: var(--click-card-horizontal-space-sm-y) var(--click-card-horizontal-space-sm-x);
+}
+
+.wrapper .description {
+ color: var(--card-desc-default);
+ font: var(--click-card-horizontal-typography-description-default);
+}
+
+.wrapper.wrapper_selectable {
+ border-color: var(--card-stroke-hover);
+}
+
+.wrapper.wrapper_selectable.wrapper_selected {
+ border-color: var(--card-stroke-active);
+}
+
+.wrapper:hover {
+ font: var(--click-card-horizontal-typography-title-hover);
+}
+
+.wrapper.wrapper_selectable:hover {
+ border-color: var(--card-stroke-default);
+ background-color: var(--card-bg-hover);
+ color: var(--card-title-hover);
+ cursor: pointer;
+}
+
+.wrapper.wrapper_selectable.wrapper_selected:hover {
+ border-color: var(--card-stroke-active);
+}
+
+.wrapper.wrapper_selectable:hover .description {
+ color: var(--card-desc-hover);
+ font: var(--click-card-horizontal-typography-description-hover);
+}
+
+.wrapper.wrapper_selectable:active,
+.wrapper.wrapper_selectable:focus,
+.wrapper.wrapper_selectable:focus-within {
+ border-color: var(--card-stroke-active);
+ background-color: var(--card-bg-active);
+ color: var(--card-title-active);
+}
+
+.wrapper.wrapper_selectable:active .description,
+.wrapper.wrapper_selectable:focus .description,
+.wrapper.wrapper_selectable:focus-within .description {
+ color: var(--card-desc-active);
+ font: var(--click-card-horizontal-typography-description-active);
+}
+
+/* stylelint-disable no-descending-specificity -- disabled state intentionally
+ defined after hover/active to mirror the source cascade order;
+ pointer-events:none plus tabIndex=-1 prevent hover/focus/active from firing. */
+.wrapper.wrapper_disabled,
+.wrapper.wrapper_disabled:hover,
+.wrapper.wrapper_disabled:active,
+.wrapper.wrapper_disabled:focus,
+.wrapper.wrapper_disabled:focus-within {
+ border: 1px solid var(--card-stroke-disabled);
+ background-color: var(--card-bg-disabled);
+ color: var(--card-title-disabled);
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.wrapper.wrapper_disabled.wrapper_selected,
+.wrapper.wrapper_disabled.wrapper_selected:hover,
+.wrapper.wrapper_disabled.wrapper_selected:active,
+.wrapper.wrapper_disabled.wrapper_selected:focus,
+.wrapper.wrapper_disabled.wrapper_selected:focus-within {
+ border-color: var(--card-stroke-active);
+}
+
+.wrapper.wrapper_disabled .description {
+ color: var(--card-desc-disabled);
+ font: var(--click-card-horizontal-typography-description-disabled);
+}
+
+.wrapper.wrapper_disabled:active,
+.wrapper.wrapper_disabled:focus,
+.wrapper.wrapper_disabled:focus-within {
+ border: 1px solid var(--card-stroke-active);
+}
+/* stylelint-enable no-descending-specificity */
diff --git a/src/components/CardHorizontal/CardHorizontal.stories.tsx b/src/components/CardHorizontal/CardHorizontal.stories.tsx
index c74550bad..f0878c335 100644
--- a/src/components/CardHorizontal/CardHorizontal.stories.tsx
+++ b/src/components/CardHorizontal/CardHorizontal.stories.tsx
@@ -1,14 +1,14 @@
import { Meta, StoryObj } from '@storybook/react-vite';
-import { styled } from 'styled-components';
+import { CSSProperties, ReactNode } from 'react';
import { ICON_NAMES } from '../Icon/IconCommon';
import { CardHorizontal } from '@/components/CardHorizontal';
-const GridCenter = styled.div`
- display: grid;
- width: 60%;
-`;
+const gridCenterStyle: CSSProperties = { display: 'grid', width: '60%' };
+const GridCenter = ({ children }: { children: ReactNode }) => (
+
{children}
+);
const meta: Meta = {
component: CardHorizontal,
@@ -36,11 +36,18 @@ const meta: Meta = {
export default meta;
-export const Playground: StoryObj = {
+type Story = StoryObj;
+
+const baseArgs = {
+ icon: 'building' as const,
+ title: 'Card title',
+ description: 'A description very interesting that presumably relates to the card.',
+ size: 'md' as const,
+};
+
+export const Playground: Story = {
args: {
- icon: 'building',
- title: 'Card title',
- description: 'A description very interesting that presumably relates to the card.',
+ ...baseArgs,
disabled: false,
isSelected: false,
badgeText: '',
@@ -49,6 +56,76 @@ export const Playground: StoryObj = {
badgeIconDir: undefined,
infoText: '',
infoUrl: '',
- size: 'md',
+ },
+};
+
+export const Default: Story = {
+ args: baseArgs,
+};
+
+export const Muted: Story = {
+ args: {
+ ...baseArgs,
+ color: 'muted',
+ },
+};
+
+export const Small: Story = {
+ args: {
+ ...baseArgs,
+ size: 'sm',
+ },
+};
+
+export const AlignmentTop: Story = {
+ args: {
+ ...baseArgs,
+ alignment: 'top',
+ },
+};
+
+export const Selectable: Story = {
+ args: {
+ ...baseArgs,
+ isSelectable: true,
+ },
+};
+
+export const Selected: Story = {
+ args: {
+ ...baseArgs,
+ isSelectable: true,
+ isSelected: true,
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ ...baseArgs,
+ disabled: true,
+ },
+};
+
+export const DisabledSelected: Story = {
+ args: {
+ ...baseArgs,
+ disabled: true,
+ isSelected: true,
+ },
+};
+
+export const WithBadge: Story = {
+ args: {
+ ...baseArgs,
+ badgeText: 'New',
+ badgeState: 'success',
+ },
+};
+
+export const WithButton: Story = {
+ args: {
+ ...baseArgs,
+ infoText: 'Read more',
+ infoUrl: 'https://clickhouse.com',
},
};
diff --git a/src/components/CardHorizontal/CardHorizontal.tsx b/src/components/CardHorizontal/CardHorizontal.tsx
index fe39e0565..67dae323f 100644
--- a/src/components/CardHorizontal/CardHorizontal.tsx
+++ b/src/components/CardHorizontal/CardHorizontal.tsx
@@ -1,187 +1,65 @@
-import { styled } from 'styled-components';
import { Badge } from '@/components/Badge';
import { Button } from '@/components/Button';
import { Container } from '@/components/Container';
import { Icon } from '@/components/Icon';
-import {
- CardHorizontalProps,
- CardSize,
- CardColor,
- CardAlignment,
-} from './CardHorizontal.types';
+import { cn, cva } from '@/lib/cva';
+import { CardHorizontalProps } from './CardHorizontal.types';
+import styles from './CardHorizontal.module.css';
-const Header = styled.div`
- max-width: 100%;
- gap: inherit;
-`;
+const wrapperVariants = cva(styles.wrapper, {
+ variants: {
+ color: {
+ default: styles['wrapper_color_default'],
+ muted: styles['wrapper_color_muted'],
+ },
+ size: {
+ sm: styles['wrapper_size_sm'],
+ md: styles['wrapper_size_md'],
+ },
+ alignment: {
+ center: styles['wrapper_alignment_center'],
+ top: styles['wrapper_alignment_top'],
+ },
+ selectable: {
+ true: styles['wrapper_selectable'],
+ },
+ selected: {
+ true: styles['wrapper_selected'],
+ },
+ disabled: {
+ true: styles['wrapper_disabled'],
+ },
+ },
+ defaultVariants: {
+ color: 'default',
+ size: 'md',
+ alignment: 'center',
+ },
+});
-const Description = styled.div`
- display: flex;
- flex-direction: column;
- align-self: start;
- gap: ${({ theme }) => theme.click.card.horizontal.space.md.gap};
- flex: 1;
- width: 100%;
-`;
+const contentWrapperVariants = cva(styles.contentwrapper, {
+ variants: {
+ size: {
+ sm: styles['contentwrapper_size_sm'],
+ md: styles['contentwrapper_size_md'],
+ },
+ },
+ defaultVariants: { size: 'md' },
+});
-const Wrapper = styled.div<{
- $hasShadow?: boolean;
- $disabled?: boolean;
- $isSelected?: boolean;
- $isSelectable?: boolean;
- $color: CardColor;
- $size?: CardSize;
- $alignment: CardAlignment;
-}>`
- display: inline-flex;
- width: 100%;
- max-width: 100%;
- align-items: ${({ $alignment }) => ($alignment === 'top' ? 'flex-start' : 'center')};
- align-self: ${({ $alignment }) => ($alignment === 'top' ? 'stretch' : 'auto')};
- justify-content: flex-start;
-
- ${({ theme, $color, $size, $isSelected, $isSelectable, $disabled }) => `
- background: ${theme.click.card.horizontal[$color].color.background.default};
- color: ${theme.click.card.horizontal[$color].color.title.default};
- border-radius: ${theme.click.card.horizontal.radii.all};
- border: 1px solid ${
- theme.click.card.horizontal[$color].color.stroke[
- $isSelectable ? ($isSelected ? 'active' : 'hover') : 'default'
- ]
- };
- padding: ${
- $size === 'md'
- ? `${theme.click.card.horizontal.space.md.y} ${theme.click.card.horizontal.space.md.x}`
- : `${theme.click.card.horizontal.space.sm.y} ${theme.click.card.horizontal.space.sm.x}`
- };
- font: ${theme.click.card.horizontal.typography.title.default};
- ${Description} {
- color: ${theme.click.card.horizontal[$color].color.description.default};
- font: ${theme.click.card.horizontal.typography.description.default};
- }
- &:hover{
- background-color: ${
- theme.click.card.horizontal[$color].color.background[
- $isSelectable ? 'hover' : 'default'
- ]
- };
- color: ${
- theme.click.card.horizontal[$color].color.title[
- $isSelectable ? 'hover' : 'default'
- ]
- };
- border: 1px solid ${
- theme.click.card.horizontal[$color].color.stroke[
- $isSelectable ? ($isSelected ? 'active' : 'default') : 'default'
- ]
- };
- cursor: ${$isSelectable ? 'pointer' : 'default'};
- font: ${theme.click.card.horizontal.typography.title.hover};
- ${Description} {
- color: ${
- theme.click.card.horizontal[$color].color.description[
- $isSelectable ? 'hover' : 'default'
- ]
- };
- font: ${
- theme.click.card.horizontal.typography.description[
- $isSelectable ? 'hover' : 'default'
- ]
- };
- }
- }
-
- &:active, &:focus, &:focus-within {
- background-color: ${
- theme.click.card.horizontal[$color].color.background[
- $isSelectable ? 'active' : 'default'
- ]
- };
- color: ${
- theme.click.card.horizontal[$color].color.title[
- $isSelectable ? 'active' : 'default'
- ]
- };
- border: 1px solid ${
- theme.click.card.horizontal[$color].color.stroke[
- $isSelectable ? 'active' : 'default'
- ]
- };
- ${Description} {
- color: ${
- theme.click.card.horizontal[$color].color.description[
- $isSelectable ? 'active' : 'default'
- ]
- };
- font: ${
- theme.click.card.horizontal.typography.description[
- $isSelectable ? 'active' : 'default'
- ]
- };
- }
- }
- ${
- $disabled
- ? `
- pointer-events: none;
- &,
- &:hover,
- &:active, &:focus, &:focus-within {
- background-color: ${
- theme.click.card.horizontal[$color].color.background.disabled
- };
- color: ${theme.click.card.horizontal[$color].color.title.disabled};
- border: 1px solid ${
- theme.click.card.horizontal[$color].color.stroke[
- $isSelected ? 'active' : 'disabled'
- ]
- };
- cursor: not-allowed;
- ${Description} {
- color: ${theme.click.card.horizontal[$color].color.description.disabled};
- font: ${theme.click.card.horizontal.typography.description.disabled};
- }
- },
- &:active, &:focus, &:focus-within {
- border: 1px solid ${theme.click.card.horizontal[$color].color.stroke.active};
- }
- `
- : ''
- }
- `}
-`;
-
-const CardIcon = styled(Icon)`
- ${({ theme }) => `
- height: ${theme.click.card.horizontal.icon.size.all};
- width: ${theme.click.card.horizontal.icon.size.all};
- `}
-`;
-
-const ContentWrapper = styled.div<{ $size: CardSize }>`
- display: flex;
- flex-direction: row;
- width: 100%;
- gap: ${({ theme, $size }) =>
- $size === 'md'
- ? theme.click.card.horizontal.space.md.gap
- : theme.click.card.horizontal.space.sm.gap};
-
- @media (max-width: ${({ theme }) => theme.breakpoint.sizes.md}) {
- flex-direction: column;
- }
-`;
-
-const IconTextContentWrapper = styled.div<{ $size: CardSize; $alignment: CardAlignment }>`
- display: flex;
- flex-direction: row;
- align-items: ${({ $alignment }) => ($alignment === 'top' ? 'flex-start' : 'center')};
- width: 100%;
- gap: ${({ theme, $size }) =>
- $size === 'md'
- ? theme.click.card.horizontal.space.md.gap
- : theme.click.card.horizontal.space.sm.gap};
-`;
+const iconTextContentWrapperVariants = cva(styles.icontextcontentwrapper, {
+ variants: {
+ size: {
+ sm: styles['icontextcontentwrapper_size_sm'],
+ md: styles['icontextcontentwrapper_size_md'],
+ },
+ alignment: {
+ center: styles['icontextcontentwrapper_alignment_center'],
+ top: styles['icontextcontentwrapper_alignment_top'],
+ },
+ },
+ defaultVariants: { size: 'md', alignment: 'center' },
+});
export const CardHorizontal = ({
title,
@@ -201,6 +79,7 @@ export const CardHorizontal = ({
badgeIcon,
badgeIconDir,
onButtonClick,
+ className,
...props
}: CardHorizontalProps) => {
const handleClick = (e: React.MouseEvent) => {
@@ -217,27 +96,30 @@ export const CardHorizontal = ({
}
};
return (
-
-
-
+
+
{icon && (
-
)}
{title && (
-
)}
-
+
)}
- {description &&
{description}}
- {children &&
{children}}
+ {description &&
{description}
}
+ {children &&
{children}
}
-
+
{infoText && (
)}
-
-
+
+
);
};
diff --git a/tests/cards/cardhorizontal.spec.ts b/tests/cards/cardhorizontal.spec.ts
new file mode 100644
index 000000000..57dc4239e
--- /dev/null
+++ b/tests/cards/cardhorizontal.spec.ts
@@ -0,0 +1,353 @@
+import { test as it, expect } from '@playwright/test';
+import { getStoryUrl } from '../utils';
+
+const { describe, use } = it;
+
+const cardLocator = '[aria-disabled]';
+
+describe('CardHorizontal Visual Regression', () => {
+ describe('Light Theme (Storybook Global)', () => {
+ describe('Variants', () => {
+ it('default matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--default', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-default-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('muted color matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--muted', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-muted-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('small size matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--small', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-small-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('alignment top matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--alignment-top', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-alignment-top-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('selectable matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--selectable', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-selectable-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('selected matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--selected', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-selected-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('disabled matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--disabled', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveAttribute('aria-disabled', 'true');
+ await expect(card).toHaveScreenshot('cardhorizontal-disabled-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('disabled selected matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--disabled-selected', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-disabled-selected-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('with badge matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--with-badge', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ const badge = page.getByTestId('horizontal-card-badge');
+ await expect(badge).toBeVisible();
+ await expect(card).toHaveScreenshot('cardhorizontal-with-badge-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('with button matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--with-button', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ const button = page.getByTestId('horizontal-card-button');
+ await expect(button).toBeVisible();
+ await expect(card).toHaveScreenshot('cardhorizontal-with-button-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+ });
+
+ describe('Interactive States', () => {
+ it('hover state on selectable matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--selectable', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await card.hover();
+ await page.waitForTimeout(100);
+ await expect(card).toHaveScreenshot('cardhorizontal-selectable-hover-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('focus state on selectable matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--selectable', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await card.focus();
+ await page.waitForTimeout(100);
+ await expect(card).toHaveScreenshot('cardhorizontal-selectable-focus-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('hover state on default (non-selectable) matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--default', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await card.hover();
+ await page.waitForTimeout(100);
+ await expect(card).toHaveScreenshot('cardhorizontal-default-hover-light.png', {
+ maxDiffPixels: 100,
+ });
+ });
+ });
+ });
+
+ describe('Dark Theme (System prefers-color-scheme)', () => {
+ use({ colorScheme: 'dark' });
+
+ describe('Variants', () => {
+ it('default matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--default'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-default-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('muted color matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--muted'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-muted-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('small size matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--small'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-small-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('alignment top matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--alignment-top'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-alignment-top-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('selectable matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--selectable'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-selectable-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('selected matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--selected'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-selected-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('disabled matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--disabled'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveAttribute('aria-disabled', 'true');
+ await expect(card).toHaveScreenshot('cardhorizontal-disabled-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('disabled selected matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--disabled-selected'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await expect(card).toHaveScreenshot('cardhorizontal-disabled-selected-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('with badge matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--with-badge'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ const badge = page.getByTestId('horizontal-card-badge');
+ await expect(badge).toBeVisible();
+ await expect(card).toHaveScreenshot('cardhorizontal-with-badge-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('with button matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--with-button'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ const button = page.getByTestId('horizontal-card-button');
+ await expect(button).toBeVisible();
+ await expect(card).toHaveScreenshot('cardhorizontal-with-button-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+ });
+
+ describe('Interactive States', () => {
+ it('hover state on selectable matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--selectable'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await card.hover();
+ await page.waitForTimeout(100);
+ await expect(card).toHaveScreenshot('cardhorizontal-selectable-hover-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('focus state on selectable matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--selectable'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await card.focus();
+ await page.waitForTimeout(100);
+ await expect(card).toHaveScreenshot('cardhorizontal-selectable-focus-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+
+ it('hover state on default (non-selectable) matches snapshot', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--default'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+ await card.hover();
+ await page.waitForTimeout(100);
+ await expect(card).toHaveScreenshot('cardhorizontal-default-hover-dark.png', {
+ maxDiffPixels: 100,
+ });
+ });
+ });
+ });
+
+ describe('Accessibility', () => {
+ it('enabled card is focusable via Tab', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--default', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toBeVisible({ timeout: 10000 });
+
+ await page.locator('body').click();
+ await page.keyboard.press('Tab');
+ await expect(card).toBeFocused();
+ });
+
+ it('disabled card exposes aria-disabled and tabIndex=-1', async ({ page }) => {
+ await page.goto(getStoryUrl('cards-horizontal-card--disabled', 'light'), {
+ waitUntil: 'networkidle',
+ });
+ const card = page.locator(cardLocator).first();
+ await expect(card).toHaveAttribute('aria-disabled', 'true');
+ await expect(card).toHaveAttribute('tabIndex', '-1');
+ });
+ });
+});
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-alignment-top-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-alignment-top-dark-chromium-linux.png
new file mode 100644
index 000000000..a84fd24ef
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-alignment-top-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-alignment-top-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-alignment-top-light-chromium-linux.png
new file mode 100644
index 000000000..d14fc0843
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-alignment-top-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-dark-chromium-linux.png
new file mode 100644
index 000000000..692200e05
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-hover-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-hover-dark-chromium-linux.png
new file mode 100644
index 000000000..e750eab40
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-hover-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-hover-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-hover-light-chromium-linux.png
new file mode 100644
index 000000000..ec65a5b36
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-hover-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-light-chromium-linux.png
new file mode 100644
index 000000000..03747d0d6
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-default-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-dark-chromium-linux.png
new file mode 100644
index 000000000..8e9d190f2
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-light-chromium-linux.png
new file mode 100644
index 000000000..551692c59
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-selected-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-selected-dark-chromium-linux.png
new file mode 100644
index 000000000..9697c3e9e
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-selected-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-selected-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-selected-light-chromium-linux.png
new file mode 100644
index 000000000..4a8866c1d
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-disabled-selected-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-muted-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-muted-dark-chromium-linux.png
new file mode 100644
index 000000000..e750eab40
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-muted-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-muted-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-muted-light-chromium-linux.png
new file mode 100644
index 000000000..ec65a5b36
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-muted-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-dark-chromium-linux.png
new file mode 100644
index 000000000..692200e05
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-focus-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-focus-dark-chromium-linux.png
new file mode 100644
index 000000000..04cb69434
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-focus-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-focus-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-focus-light-chromium-linux.png
new file mode 100644
index 000000000..76334191a
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-focus-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-hover-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-hover-dark-chromium-linux.png
new file mode 100644
index 000000000..e750eab40
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-hover-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-hover-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-hover-light-chromium-linux.png
new file mode 100644
index 000000000..ec65a5b36
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-hover-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-light-chromium-linux.png
new file mode 100644
index 000000000..03747d0d6
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selectable-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selected-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selected-dark-chromium-linux.png
new file mode 100644
index 000000000..a2e14d458
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selected-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selected-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selected-light-chromium-linux.png
new file mode 100644
index 000000000..cb08d4059
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-selected-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-small-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-small-dark-chromium-linux.png
new file mode 100644
index 000000000..5a926a983
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-small-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-small-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-small-light-chromium-linux.png
new file mode 100644
index 000000000..dc6ec4974
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-small-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-badge-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-badge-dark-chromium-linux.png
new file mode 100644
index 000000000..f53a781c5
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-badge-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-badge-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-badge-light-chromium-linux.png
new file mode 100644
index 000000000..5905040a7
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-badge-light-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-button-dark-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-button-dark-chromium-linux.png
new file mode 100644
index 000000000..b1e78d2da
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-button-dark-chromium-linux.png differ
diff --git a/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-button-light-chromium-linux.png b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-button-light-chromium-linux.png
new file mode 100644
index 000000000..06fb4f324
Binary files /dev/null and b/tests/cards/cardhorizontal.spec.ts-snapshots/cardhorizontal-with-button-light-chromium-linux.png differ