Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
32 changes: 32 additions & 0 deletions packages/lib/components/card/card.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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' },
href: { control: 'text' },
},
};

export default meta;
type Story = StoryObj;

export const Default: Story = {
args: {
title: 'Title',
image: 'https://picsum.photos/500/220',
},
render: (args) => html`
<cx-card title="${args.title}" image="${args.image}" href="${args.href || ''}">
<span slot="subtitle">14:00 - 16:00 • Oslo, Norway</span>
<span slot="other">Other content</span>
</cx-card>
`,
};
201 changes: 201 additions & 0 deletions packages/lib/components/card/card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('cx-card')
export class Card extends LitElement {
static styles = css`
:host {
Comment thread
rebalv marked this conversation as resolved.
Outdated
display: block;
}

.card {
position: relative;
height: 440px;
width: 100%;
max-width: 500px;
Comment thread
rebalv marked this conversation as resolved.
Outdated
border-radius: 24px;
overflow: hidden;
display: flex;
flex-direction: column;
text-decoration: none;
color: inherit;
}

.card.clickable {
cursor: pointer;
}

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

.card-image img,
.card-image ::slotted(img),
.card-image ::slotted(video),
.card-image ::slotted(*) {
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-wrapper {
flex: 1;
padding: var(--cx-spacing-6);
color: var(--cx-color-text-primary);
background-color: var(--cx-color-background-accent-1-soft);
}

.card-info {
display: flex;
flex-direction: column;
height: 100%;
}

.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;
max-height: 65px;
Comment thread
rebalv marked this conversation as resolved.
Outdated
min-height: 65px;
overflow: hidden;
font-size: 24px;
Comment thread
rebalv marked this conversation as resolved.
Outdated
font-weight: 400;
margin-bottom: var(--cx-spacing-2);
line-height: 2rem;
align-content: center;
}

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

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

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

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

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

.card-info-wrapper {
width: 100%;
padding: var(--cx-spacing-4);
}

.card-info {
flex-direction: column-reverse;
gap: var(--cx-spacing-2);
}

.card-subtitle {
font-size: 12px;
Comment thread
rebalv marked this conversation as resolved.
Outdated
}

.card-title {
margin-top: unset;
min-height: unset;
font-size: 18px;
}
}
`;

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

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

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

render() {
const cardContent = html`
<div class="card-image">
${this.image ? html`
<img src="${this.image}" alt="" />
` : html`
<slot name="image"></slot>
`}
</div>
<div class="card-info-wrapper">
<div class="card-info">
<div class="card-subtitle">
<slot name="subtitle"></slot>
</div>
${this.title ? html`
<div class="card-title">${this.title}</div>
` : html`
<div class="card-title">
<slot name="title"></slot>
</div>
`}
<div class="card-other">
<slot name="other"></slot>
</div>
</div>
Comment thread
rebalv marked this conversation as resolved.
</div>
`;

return this.href ? html`
<a href="${this.href}" class="card clickable">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vi burde egentlig unngå bake in <a tags i komponentene våre fordi mange rammeverk har sine egne Lenke komponenter for å fungere som en SPA. En vanlig <a tag vil gi en full refreshe og navigasjon på siden. Det blir også problemer med skjermleser hvis card inneholder knapper i tillegg.

Så tenker at om man heller lar konsumenten av card f.eks wrappe den i en Lenke komponent selv hvis det skal brukes som lenke så vil det alltid funke :)

${cardContent}
</a>
` : html`
<div class="card">
${cardContent}
</div>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'cx-card': Card;
}
}
Loading