diff --git a/packages/lib/components/card/Overview.mdx b/packages/lib/components/card/Overview.mdx new file mode 100644 index 0000000..7b10c51 --- /dev/null +++ b/packages/lib/components/card/Overview.mdx @@ -0,0 +1,37 @@ +import { Canvas, Meta, Source, Subtitle, Title } from "@storybook/blocks"; + +import * as stories from "./card.stories"; + + + + +<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>`} +/> \ No newline at end of file diff --git a/packages/lib/components/card/card.stories.ts b/packages/lib/components/card/card.stories.ts new file mode 100644 index 0000000..c23f181 --- /dev/null +++ b/packages/lib/components/card/card.stories.ts @@ -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> + `, +}; diff --git a/packages/lib/components/card/card.ts b/packages/lib/components/card/card.ts new file mode 100644 index 0000000..9c49917 --- /dev/null +++ b/packages/lib/components/card/card.ts @@ -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; + } +} diff --git a/packages/lib/global-css/typography.css b/packages/lib/global-css/typography.css index 04164d3..785e3eb 100644 --- a/packages/lib/global-css/typography.css +++ b/packages/lib/global-css/typography.css @@ -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; diff --git a/packages/lib/global-css/typography/text-1.css b/packages/lib/global-css/typography/text-1.css new file mode 100644 index 0000000..d064f19 --- /dev/null +++ b/packages/lib/global-css/typography/text-1.css @@ -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; +} diff --git a/packages/lib/global-css/typography/text-2.css b/packages/lib/global-css/typography/text-2.css new file mode 100644 index 0000000..455721b --- /dev/null +++ b/packages/lib/global-css/typography/text-2.css @@ -0,0 +1,29 @@ +.cx-text-2 { + font-size: 1.125rem; + font-weight: 400; + line-height: 1.6; +} + +:is(p).cx-text-2 { + max-width: 65ch; + text-wrap: pretty; + word-break: auto-phrase; +} + +.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-clickable-2 { + font-weight: 500; + font-size: 1rem; + line-height: 1; +} diff --git a/packages/lib/global-css/typography/text-3.css b/packages/lib/global-css/typography/text-3.css new file mode 100644 index 0000000..b20722f --- /dev/null +++ b/packages/lib/global-css/typography/text-3.css @@ -0,0 +1,29 @@ +.cx-text-3 { + font-size: 1rem; + font-weight: 400; + line-height: 1.6; +} + +:is(p).cx-text-3 { + max-width: 65ch; + text-wrap: pretty; + word-break: auto-phrase; +} + +.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-clickable-3 { + font-weight: 500; + font-size: 0.875rem; + line-height: 1; +} diff --git a/packages/lib/global-css/typography/text-4.css b/packages/lib/global-css/typography/text-4.css new file mode 100644 index 0000000..24e6ff5 --- /dev/null +++ b/packages/lib/global-css/typography/text-4.css @@ -0,0 +1,23 @@ +.cx-text-4 { + font-size: 0.875rem; + font-weight: 400; + line-height: 1.6; +} + +:is(p).cx-text-4 { + max-width: 65ch; + text-wrap: pretty; + word-break: auto-phrase; +} + +.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; +} diff --git a/packages/lib/global-css/typography/text-jumbo.css b/packages/lib/global-css/typography/text-jumbo.css new file mode 100644 index 0000000..88ec97b --- /dev/null +++ b/packages/lib/global-css/typography/text-jumbo.css @@ -0,0 +1,11 @@ +.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; +} diff --git a/packages/lib/global-css/typography/text-micro.css b/packages/lib/global-css/typography/text-micro.css new file mode 100644 index 0000000..5557763 --- /dev/null +++ b/packages/lib/global-css/typography/text-micro.css @@ -0,0 +1,11 @@ +.cx-text-micro { + font-size: 0.75rem; + font-weight: 400; + line-height: 1.6; +} + +:is(p).cx-text-micro { + max-width: 65ch; + text-wrap: pretty; + word-break: auto-phrase; +} diff --git a/packages/lib/global-css/typography/title-1.css b/packages/lib/global-css/typography/title-1.css new file mode 100644 index 0000000..6ef86ab --- /dev/null +++ b/packages/lib/global-css/typography/title-1.css @@ -0,0 +1,6 @@ +.cx-title-1 { + font-weight: 400; + font-size: 2.75rem; + line-height: 1.6; + text-wrap: balance; +} diff --git a/packages/lib/global-css/typography/title-2.css b/packages/lib/global-css/typography/title-2.css new file mode 100644 index 0000000..f2822d4 --- /dev/null +++ b/packages/lib/global-css/typography/title-2.css @@ -0,0 +1,6 @@ +.cx-title-2 { + font-weight: 600; + font-size: 2.25rem; + line-height: 1.6; + text-wrap: balance; +} diff --git a/packages/lib/global-css/typography/title-3.css b/packages/lib/global-css/typography/title-3.css new file mode 100644 index 0000000..7d86741 --- /dev/null +++ b/packages/lib/global-css/typography/title-3.css @@ -0,0 +1,6 @@ +.cx-title-3 { + font-weight: 600; + font-size: 1.75rem; + line-height: 1.6; + text-wrap: balance; +} diff --git a/packages/lib/global-css/typography/title-4.css b/packages/lib/global-css/typography/title-4.css new file mode 100644 index 0000000..04954ee --- /dev/null +++ b/packages/lib/global-css/typography/title-4.css @@ -0,0 +1,6 @@ +.cx-title-4 { + font-weight: 600; + font-size: 1.5rem; + line-height: 1.6; + text-wrap: balance; +} diff --git a/packages/lib/global-css/typography/title-5.css b/packages/lib/global-css/typography/title-5.css new file mode 100644 index 0000000..649db25 --- /dev/null +++ b/packages/lib/global-css/typography/title-5.css @@ -0,0 +1,6 @@ +.cx-title-5 { + font-weight: 600; + font-size: 1.125rem; + line-height: 1.125; + text-wrap: balance; +} diff --git a/packages/lib/global-styles.css b/packages/lib/global-styles.css index d537c55..eaff636 100644 --- a/packages/lib/global-styles.css +++ b/packages/lib/global-styles.css @@ -13,6 +13,16 @@ @import "global-css/a11y.css"; @import "global-css/margins.css"; @import "global-css/typography.css"; +@import "global-css/typography/title-1.css"; +@import "global-css/typography/title-2.css"; +@import "global-css/typography/title-3.css"; +@import "global-css/typography/title-4.css"; +@import "global-css/typography/title-5.css"; +@import "global-css/typography/text-1.css"; +@import "global-css/typography/text-2.css"; +@import "global-css/typography/text-3.css"; +@import "global-css/typography/text-4.css"; +@import "global-css/typography/text-micro.css"; @import "global-css/animations.css"; /* Components */