diff --git a/packages/cxl-lumo-styles/src/icons.js b/packages/cxl-lumo-styles/src/icons.js index 677423744..a35fb45d1 100644 --- a/packages/cxl-lumo-styles/src/icons.js +++ b/packages/cxl-lumo-styles/src/icons.js @@ -53,6 +53,9 @@ $documentContainer.innerHTML = ` + + + diff --git a/packages/cxl-ui/scss/cxl-course-card.scss b/packages/cxl-ui/scss/cxl-course-card.scss new file mode 100644 index 000000000..7799c80e2 --- /dev/null +++ b/packages/cxl-ui/scss/cxl-course-card.scss @@ -0,0 +1,235 @@ +@use "~@conversionxl/cxl-lumo-styles/scss/mq"; + +:host { + display: flex; + position: relative; + height: max-content; + box-sizing: border-box; + min-height: 300px; + font-size: var(--lumo-font-size-s); + padding: var(--lumo-space-m) var(--lumo-space-l); + background: var(--lumo-tint); + border: 1px solid var(--lumo-contrast-10pct); + border-radius: var(--lumo-border-radius-l); + box-shadow: var(--lumo-box-shadow-xs); + break-inside: avoid; + transform: translateZ(0); // CSS columns @see https://stackoverflow.com/a/55110789/35946 + + // @see https://github.com/conversionxl/aybolit/pull/293 + --video-background: hsla(355.8, 74.7%, 48%, 0.03); // --lumo-primary-color-3pct does not exist + --training-background: hsla(0, 0%, 10%, 0.03); // --lumo-shade-3pct does not exist + --playbook-background: hsla(213, 100%, 62%, 0.03); // No similar base color exists + + // Container / Media queries + @media #{mq.$small} { + .container > .attributes { + display: none; + } + + header .info .attributes { + display: flex; + } + } +} + +:host(:hover) { + border-color: var(--lumo-primary-color); +} + +:host([hidden]) { + display: none; +} + +:host(:first-child) { + margin-top: unset; +} + +:host(:last-child) { + margin-bottom: unset; +} + +:host([theme~="dark"]) { + background-color: var(--lumo-contrast); +} + +:host { + [empty] { + user-select: none; + visibility: hidden; + } +} + +:host([theme~="video"]) { + background-color: var(--video-background); +} + +:host([theme~="training"]) { + background-color: var(--training-background); +} + +:host([theme~="playbook"]) { + background-color: var(--playbook-background); +} + +.container { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: var(--lumo-space-s); + width: 100%; + + > .attributes { + padding-top: 0; + } + } + +.attributes { + display: flex; + padding: var(--lumo-space-s) 0; + align-items: flex-start; + gap: var(--lumo-space-s); + align-self: stretch; + color: var(--lumo-shade-60pct); +} + +header { + display: flex; + align-items: start; + justify-content: space-between; + gap: var(--lumo-space-m); + + .info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--lumo-space-xs); + flex: 1 0 0; + align-self: stretch; + max-width: calc(100% - var(--lumo-space-m) - 80px); + overflow: hidden; + + .title { + color: #1A1A1A; + font-size: var(--lumo-font-size-xl); + font-family: Roboto; + font-style: normal; + font-weight: 700; + line-height: var(--lumo-line-height-xs); + } + + .attributes { + display: none; + } + } + + + .instructor-image { + height: 92px; + width: 80px; + + img { + height: 80px; + border-radius: 100px; + overflow: hidden; + } + } + + .tags span { + &:first-child, &.new { + color: var(--lumo-primary-color) + } + + &:first-child { + text-transform: capitalize; + } + } +} + +.tags { + display: flex; + gap: var(--lumo-space-s); + max-width: 100%; + overflow: hidden; + flex-wrap: wrap; + height: 1.6em; + + ::slotted(span):not(:first-child) { + overflow: hidden; + text-overflow: ellipsis; + } +} + +.content { + .tags { + ::slotted(span) { + font-style: italic; + } + } +} + +footer { + position: relative; + + vaadin-details[theme="reverse"] { + &::part(summary) { + justify-content: flex-start; + gap: var(--lumo-space-s); + font-size: var(--lumo-font-size-s); + } + + &::part(toggle) { + padding: calc(var(--lumo-space-xs) / 4); + margin-left: initial; + font-size: var(--lumo-font-size-m); + transform: rotate(90deg); + } + + &[opened]::part(toggle) { + transform: rotate(-90deg); + } + + &::part(summary-content) { + color: var(--lumo-contrast); + font-weight: bold; + } + + &::part(content) { + padding-bottom: 0; + } + } + + vaadin-button.cta { + position: absolute; + top: 0; + right: 0; + font-weight: bold; + + vaadin-icon { + background: var(--lumo-primary-color-10pct); + border-radius: 100%; + margin-left: var(--lumo-space-xs); + height: var(--lumo-icon-size-s); + width: var(--lumo-icon-size-s); + padding: calc(var(--lumo-space-xs) / 2); + } + } +} + +vaadin-icon.badge-new { + display: none; +} + +:host([new]) { + vaadin-icon.badge-new { + display: block; + position: absolute; + top: calc(-1 * var(--lumo-space-s)); + right: calc(-1 * var(--lumo-space-s)); + height: calc(2 * var(--lumo-space-m)); + width: calc(2 * var(--lumo-space-m)); + background: var(--lumo-primary-color); + padding: 6px; + color: var(--lumo-primary-contrast-color); + border-radius: 100%; + } +} diff --git a/packages/cxl-ui/src/components/cxl-course-card.js b/packages/cxl-ui/src/components/cxl-course-card.js new file mode 100644 index 000000000..824778f8b --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-course-card.js @@ -0,0 +1,102 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { LitElement, html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import '@vaadin/details'; +import '@vaadin/button'; +import cxlCourseCardStyles from '../styles/cxl-course-card-css.js'; + +@customElement('cxl-course-card') +export class CXLCourseCardElement extends LitElement { + static get styles() { + return [cxlCourseCardStyles]; + } + + separator = html` | `; + + @state() _tagsHasChildren = false; + + @state() _moreHasChildren = false; + + @property({ type: String }) id = ''; + + @property({ type: String }) theme = 'course'; + + @property({ type: String }) title = ''; + + @property({ type: String }) time = ''; + + @property({ type: String }) instructor = ''; + + @property({ type: String }) avatar = ''; + + @property({ type: Boolean, reflect: true }) new = false; + + @property({ type: String, attribute: 'cta-label' }) ctaLabel = 'View'; + + @property({ type: Boolean, attribute: 'cta-url' }) ctaUrl = false; + + _slotHasChildren (e) { + const slot = e.target + const { name } = slot + const children = slot.assignedNodes() + this[`_${name}HasChildren`] = !!children.length + } + + render() { + return html` +
+
+
+
+ ${this.theme ? html`${this.theme}`: ''} + ${this.theme && this._tagsHasChildren ? this.separator : ''} + + ${this.new ? html`${this.theme ? this.separator : ''}NEW` : '' } +
+
+ ${this.title} +
+
+
+ ${this.theme.toLowerCase() === 'course' ? html`` : ''} + ${this.time} +
+
+ By: ${this.instructor} +
+
+
+
+ ${this.instructor} +
+
+
+
+ ${this.theme.toLowerCase() === 'course' ? html`` : ''} + ${this.time} +
+
+ By: ${this.instructor} +
+
+
+ +
+ +
+
+
+ +
Read more
+ +
+ ${this.ctaLabel} ${this.theme} +
+ +
+ `; + } +} diff --git a/packages/cxl-ui/src/index-core.js b/packages/cxl-ui/src/index-core.js index 2ce061922..5dbb6ae73 100644 --- a/packages/cxl-ui/src/index-core.js +++ b/packages/cxl-ui/src/index-core.js @@ -15,6 +15,7 @@ import * as Headroom from 'headroom.js'; // CXL. export { CXLAppLayoutElement } from './components/cxl-app-layout.js'; export { CXLCardElement } from './components/cxl-card.js'; +export { CXLCourseCardElement } from './components/cxl-course-card.js'; export { CXLCredentialElement } from './components/cxl-credential.js' export { CXLCheckoutDetailsElement } from './components/cxl-checkout-details.js'; export { CXLMarketingNavElement } from './components/cxl-marketing-nav.js'; diff --git a/packages/cxl-ui/src/index-storybook.js b/packages/cxl-ui/src/index-storybook.js index 2d17ec740..0e071ca35 100644 --- a/packages/cxl-ui/src/index-storybook.js +++ b/packages/cxl-ui/src/index-storybook.js @@ -4,6 +4,7 @@ import * as Headroom from 'headroom.js'; export { CXLAppLayoutElement } from './components/cxl-app-layout.js'; export { CXLCardElement } from './components/cxl-card.js'; +export { CXLCourseCardElement } from './components/cxl-course-card.js'; export { CXLMarketingNavElement } from './components/cxl-marketing-nav.js'; export { CXLSectionElement } from './components/cxl-section.js'; export { CXLStatsElement } from './components/cxl-stats.js'; diff --git a/packages/storybook/cxl-ui/cxl-course-card/[theme=course].stories.js b/packages/storybook/cxl-ui/cxl-course-card/[theme=course].stories.js new file mode 100644 index 000000000..aa3edc1da --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/[theme=course].stories.js @@ -0,0 +1,15 @@ +import { CourseCardTemplate, args } from './template.js' +import '@conversionxl/cxl-ui/src/components/cxl-course-card.js'; +import '@conversionxl/cxl-lumo-styles'; + +export default { + title: 'CXL UI/cxl-course-card', + parameters: { + layout: 'centered' + } +}; + + +export const CXLCourseCard = CourseCardTemplate.bind({}); + +CXLCourseCard.args = args; diff --git a/packages/storybook/cxl-ui/cxl-course-card/[theme=playbook].stories.js b/packages/storybook/cxl-ui/cxl-course-card/[theme=playbook].stories.js new file mode 100644 index 000000000..c4b102353 --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/[theme=playbook].stories.js @@ -0,0 +1,23 @@ +import { CourseCardTemplate, args } from './template.js' +import '@conversionxl/cxl-ui/src/components/cxl-course-card.js'; +import '@conversionxl/cxl-lumo-styles'; + +export default { + title: 'CXL UI/cxl-course-card', + parameters: { + layout: 'centered' + } +}; + + +export const CXLCourseCardPlaybook = CourseCardTemplate.bind({}); + +CXLCourseCardPlaybook.args = { + ...args, + theme: 'playbook', + title: 'The Why’s and How’s of Marketing Attribution', + time: '12 days ago', + description: 'Master the strategies, tactics, metrics, and wisdom you need to become an ABM leader and accelerate the growth of your company and of your career.', + contentTags: [], + tags: ['Growth Marketing'] +}; diff --git a/packages/storybook/cxl-ui/cxl-course-card/[theme=training].stories.js b/packages/storybook/cxl-ui/cxl-course-card/[theme=training].stories.js new file mode 100644 index 000000000..e902ce48f --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/[theme=training].stories.js @@ -0,0 +1,23 @@ +import { CourseCardTemplate, args } from './template.js' +import '@conversionxl/cxl-ui/src/components/cxl-course-card.js'; +import '@conversionxl/cxl-lumo-styles'; + +export default { + title: 'CXL UI/cxl-course-card', + parameters: { + layout: 'centered' + } +}; + + +export const CXLCourseCardTraining = CourseCardTemplate.bind({}); + +CXLCourseCardTraining.args = { + ...args, + theme: 'training', + title: 'A/B testing mastery', + description: 'Understand testing approaches that work (and pitfalls that don’t) to get more wins and insights from optimization efforts.', + tags: ['CRO', 'Branding'], + contentTags: [], + more: '' +}; diff --git a/packages/storybook/cxl-ui/cxl-course-card/[theme=video].stories.js b/packages/storybook/cxl-ui/cxl-course-card/[theme=video].stories.js new file mode 100644 index 000000000..74f9daa67 --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/[theme=video].stories.js @@ -0,0 +1,24 @@ +import { CourseCardTemplate, args } from './template.js' +import '@conversionxl/cxl-ui/src/components/cxl-course-card.js'; +import '@conversionxl/cxl-lumo-styles'; + +export default { + title: 'CXL UI/cxl-course-card', + parameters: { + layout: 'centered' + } +}; + + +export const CXLCourseCardVideo = CourseCardTemplate.bind({}); + +CXLCourseCardVideo.args = { + ...args, + theme: 'video', + title: 'A/B testing mastery', + description: 'Understand testing approaches that work (and pitfalls that don’t) to get more wins and insights from optimization efforts.', + tags: ['CRO'], + contentTags: ['running experiments', 'customer base studies', 'prioritization' ], + time: '4 days ago', + new: true +}; diff --git a/packages/storybook/cxl-ui/cxl-course-card/course-dashboard-varying-heights.stories.js b/packages/storybook/cxl-ui/cxl-course-card/course-dashboard-varying-heights.stories.js new file mode 100644 index 000000000..980155f6a --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/course-dashboard-varying-heights.stories.js @@ -0,0 +1,53 @@ +import { html } from 'lit'; +import '@conversionxl/cxl-ui/src/components/cxl-course-card.js'; +import '@conversionxl/cxl-lumo-styles'; +import { CXLCourseCard } from './[theme=course].stories.js'; +import { CXLCourseCardVideo } from './[theme=video].stories.js'; +import { CXLCourseCardPlaybook } from './[theme=playbook].stories.js'; +import { CXLCourseCardTraining } from './[theme=training].stories.js'; + +export default { + title: 'CXL UI/cxl-course-card', +}; + +const ExtraCardVideoArgs = { + ...CXLCourseCardVideo.args, + contentTags: [...CXLCourseCardVideo.args.contentTags, 'Another tag', 'Much longer content tag' ], + description: `${CXLCourseCardVideo.args.description} One more line to test the layout behavior on longer descriptions, with at least four line of text.` +} + +const ExtraCardTrainingArgs = { + ...CXLCourseCardTraining.args, + description: 'Short description', +} + +const Template = () => html` + + +
+ ${CXLCourseCard(CXLCourseCard.args)} + ${CXLCourseCardPlaybook(CXLCourseCardPlaybook.args)} + ${CXLCourseCardTraining(ExtraCardTrainingArgs)} + ${CXLCourseCardVideo(ExtraCardVideoArgs)} + ${CXLCourseCardVideo(CXLCourseCardVideo.args)} + ${CXLCourseCardTraining(CXLCourseCardTraining.args)} +
+`; + +export const CoursesDashboardDifferentHeights = Template.bind({}); diff --git a/packages/storybook/cxl-ui/cxl-course-card/course-dashboard.stories.js b/packages/storybook/cxl-ui/cxl-course-card/course-dashboard.stories.js new file mode 100644 index 000000000..c50fc163d --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/course-dashboard.stories.js @@ -0,0 +1,40 @@ +import { html } from 'lit'; +import '@conversionxl/cxl-ui/src/components/cxl-course-card.js'; +import '@conversionxl/cxl-lumo-styles'; +import { CXLCourseCard } from './[theme=course].stories.js'; +import { CXLCourseCardVideo } from './[theme=video].stories.js'; +import { CXLCourseCardPlaybook } from './[theme=playbook].stories.js'; +import { CXLCourseCardTraining } from './[theme=training].stories.js'; + +export default { + title: 'CXL UI/cxl-course-card', +}; + +const Template = () => html` + + +
+ ${CXLCourseCard(CXLCourseCard.args)} + ${CXLCourseCardPlaybook(CXLCourseCardPlaybook.args)} + ${CXLCourseCardTraining(CXLCourseCardTraining.args)} + ${CXLCourseCardVideo(CXLCourseCardVideo.args)} +
+`; + +export const CoursesDashboard = Template.bind({}); diff --git a/packages/storybook/cxl-ui/cxl-course-card/template.js b/packages/storybook/cxl-ui/cxl-course-card/template.js new file mode 100644 index 000000000..7c87fc5a2 --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/template.js @@ -0,0 +1,35 @@ +import { html } from 'lit'; + +const renderTags = (tags, slot) => tags.map((tag, i) => html`${i > 0 ? html` | ` : ''}${tag}`); + +export const CourseCardTemplate = (course) => html` + + ${course.tags ? renderTags(course.tags, 'tags') : ''} +

${course.description}

+ ${course.contentTags ? renderTags(course.contentTags, 'content-tags') : ''} + ${course.more ? html`

${course.more}

` : ''} +
+`; + +export const args = { + id: 'cxl-course-1', + title: 'Account based marketing', + time: '3h 00min', + instructor: 'Tom Wesseling', + description: 'Master the strategies, tactics, metrics, and wisdom you need to become an ABM leader and accelerate the growth of your company and of your career.', + contentTags: ['B2B', 'campaigns', 'pilot planning'], + theme: 'course', + tags: ['Marketing', 'Analytics'], + avatar: 'https://cxl.com/institute/wp-content/uploads/2020/05/48192546_10156982340630746_8127333122065825792_n-wpv_400pxx400px_center_center.jpg', + more: "Lorem ipsum dolor sit amet consectetur adipisicing elit.", + new: false +}