Skip to content
Draft
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
37 changes: 37 additions & 0 deletions packages/lib/components/card/Overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Canvas, Meta, Source, Subtitle, Title } from "@storybook/blocks";

import * as stories from "./card.stories";

<Meta of={stories} />

<Title />
<Subtitle>
A card is used to display content and actions on a single topic. They provide a flexible and extensible content container that groups related information in an easily scannable format.
</Subtitle>

## How to get started
Start by importing the component. Once imported, the `<cx-card>` component is ready to use.
``` ts
// Web component
import '@computas/designsystem/card';

// React
import { CxCard } from '@computas/designsystem/card/react';
```

## Default
A card can consist of an **image**, **title**, **subtitle** and **body content**. The **title** and **image** can be set as properties, while **subtitle** and **body** use slots for maximum flexibility.
<Canvas of={stories.Default} />

## Content slots
The card supports several content slots for customization:

- **`subtitle`** - For metadata like time, location, or categories
- **`body`** - For additional content like buttons, tags, or other actions

<Source
code={`<cx-card image="https://example.com/image.jpg" title="Card Title">
<span slot="subtitle">14:00 - 16:00 • Oslo, Norway</span>
<button slot="body" class="cx-btn__secondary">Learn more</button>
</cx-card>`}
/>
33 changes: 33 additions & 0 deletions packages/lib/components/card/card.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import './card.js';

const meta: Meta = {
title: 'Components/Card',
component: 'cx-card',
parameters: {
layout: 'centered',
},
argTypes: {
title: { control: 'text' },
image: { control: 'text' },
},
};

export default meta;
type Story = StoryObj;

export const Default: Story = {
args: {
title: 'Card Title',
image: 'https://picsum.photos/500/220',
},
render: (args) => html`
<cx-card title="${args.title}" image="${args.image}">
<span slot="subtitle">14:00 - 16:00 • Oslo, Norway</span>
<div slot="body">
<p>Additional body content</p>
</div>
</cx-card>
`,
};
153 changes: 153 additions & 0 deletions packages/lib/components/card/card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { LitElement, css, html, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import text1CSS from '../../global-css/typography/text-1.css?inline';
import text4CSS from '../../global-css/typography/text-4.css?inline';

@customElement('cx-card')
export class Card extends LitElement {
static styles = [
unsafeCSS(text1CSS),
unsafeCSS(text4CSS),
css`
.card {
position: relative;
height: 100%;
width: 100%;
border-radius: 24px;
overflow: hidden;
display: flex;
flex-direction: column;
text-decoration: none;
color: inherit;
}

.card-image {
width: 100%;
height: 192px;
position: relative;
flex-shrink: 0;
}

.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: filter 0.3s ease;
}

/* Blue filter on img for hover */
.card-image::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
mix-blend-mode: multiply;
background: var(--cx-color-background-accent-5);
opacity: 0;
transition: opacity 0.3s ease;
}

.card-info {
flex: 1;
padding: var(--cx-spacing-6);
color: var(--cx-color-text-primary);
background-color: var(--cx-color-background-accent-1-soft);
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}

.card-subtitle {
display: flex;
flex-wrap: wrap;
color: var(--cx-color-text-less-important);
gap: var(--cx-spacing-2);
margin-bottom: var(--cx-spacing-4);
}

.card-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
margin-bottom: var(--cx-spacing-2);
align-content: center;
}

.card-body {
display: flex;
gap: var(--cx-spacing-2);
flex-wrap: wrap;
margin-top: auto;
}

/* Hover effects - only on image */
.card:hover .card-image img {
filter: grayscale(1);
}

.card:hover .card-image::after {
opacity: 1;
}

@media (max-width: 750px) {
.card {
flex-direction: column;
height: auto;
}

.card-image {
width: 100%;
height: 125px;
}

.card-info {
width: 100%;
padding: var(--cx-spacing-4);
flex-direction: column-reverse;
gap: var(--cx-spacing-2);
box-sizing: border-box;
}
}
`,
];

@property({ type: String, reflect: true })
title = '';

@property({ type: String, reflect: true })
image = '';

render() {
return html`
<div class="card">
<div class="card-image">
${this.image ? html`<img src="${this.image}" alt="" />` : html`<slot name="image"></slot>`}
</div>
<div class="card-info">
<div class="card-subtitle cx-text-4">
<slot name="subtitle"></slot>
</div>
${
this.title
? html`<div class="card-title cx-text-1">${this.title}</div>`
: html`<div class="card-title cx-text-1"><slot name="title"></slot></div>`
}
<div class="card-body">
<slot name="body"></slot>
</div>
</div>
</div>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'cx-card': Card;
}
}
146 changes: 0 additions & 146 deletions packages/lib/global-css/typography.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,152 +11,6 @@ button {
font-family: inherit;
}

.cx-title-1,
.cx-title-2,
.cx-title-3,
.cx-title-4,
.cx-title-5 {
text-wrap: balance;
}

.cx-title-1 {
font-weight: 400;
font-size: 2.75rem;
line-height: 1.6;
}

.cx-title-2 {
font-weight: 600;
font-size: 2.25rem;
line-height: 1.6;
}

.cx-title-3 {
font-weight: 600;
font-size: 1.75rem;
line-height: 1.6;
}

.cx-title-4 {
font-weight: 600;
font-size: 1.5rem;
line-height: 1.6;
}

.cx-title-5 {
font-weight: 600;
font-size: 1.125rem;
line-height: 1.125;
}

:is(p):where(.cx-text-1, .cx-text-2, .cx-text-3, .cx-text-4, .cx-text-micro) {
max-width: 65ch;
text-wrap: pretty;
word-break: auto-phrase;
}

.cx-text-1 {
font-weight: 400;
font-size: 1.5rem;
line-height: 1.6;
}

.cx-text-2 {
font-weight: 400;
font-size: 1.125rem;
line-height: 1.6;
}

.cx-text-2-strong {
font-weight: 600;
font-size: 1.125rem;
line-height: 1.6;
}

.cx-text-2-light {
font-weight: 300;
font-size: 1.125rem;
line-height: 1.6;
}

.cx-text-3 {
font-weight: 400;
font-size: 1rem;
line-height: 1.6;
}

.cx-text-3-strong {
font-weight: 600;
font-size: 1rem;
line-height: 1.6;
}

.cx-text-3-light {
font-weight: 300;
font-size: 1rem;
line-height: 1.6;
}

.cx-text-4 {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.6;
}

.cx-text-4-strong {
font-weight: 600;
font-size: 0.875rem;
line-height: 1.6;
}

.cx-text-4-light {
font-weight: 300;
font-size: 0.875rem;
line-height: 1.6;
}

.cx-text-jumbo {
font-weight: 700;
font-size: 3.75rem;
line-height: 1.6;
}

.cx-text-jumbo-mobile {
font-weight: 700;
font-size: 2rem;
line-height: 1.4;
}

.cx-text-micro {
font-weight: 400;
font-size: 0.75rem;
line-height: 1.6;
}

.cx-text-micro-strong {
font-weight: 600;
font-size: 0.75rem;
line-height: 1.6;
}

.cx-text-clickable-1 {
font-weight: 500;
font-size: 1.125rem;
line-height: 1;
}

.cx-text-clickable-2 {
font-weight: 500;
font-size: 1rem;
line-height: 1;
}

.cx-text-clickable-3 {
font-weight: 500;
font-size: 0.875rem;
line-height: 1;
}

.cx-overflow-ellipsis {
white-space: nowrap;
text-overflow: ellipsis;
Expand Down
17 changes: 17 additions & 0 deletions packages/lib/global-css/typography/text-1.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.cx-text-1 {
font-size: 1.5rem;
font-weight: 400;
line-height: 1.6;
}

:is(p).cx-text-1 {
max-width: 65ch;
text-wrap: pretty;
word-break: auto-phrase;
}

.cx-text-clickable-1 {
font-weight: 500;
font-size: 1.125rem;
line-height: 1;
}
Loading