From 816408fe1ccb327ba6c41d71705f816a8ed6e442 Mon Sep 17 00:00:00 2001 From: Raphael Mattos Date: Mon, 24 Jul 2023 18:52:01 -0300 Subject: [PATCH] feat(cxl-ui): add `cxl-course-card` component --- package.json | 1 - packages/cxl-lumo-styles/src/icons.js | 3 + packages/cxl-ui/scss/cxl-course-card.scss | 253 ++++++++++++++++++ .../cxl-ui/src/components/cxl-course-card.js | 109 ++++++++ packages/cxl-ui/src/index-core.js | 1 + packages/cxl-ui/src/index-storybook.js | 1 + .../cxl-course-card/[theme=course].stories.js | 15 ++ .../[theme=playbook].stories.js | 23 ++ .../[theme=training].stories.js | 23 ++ .../cxl-course-card/[theme=video].stories.js | 24 ++ ...ourse-dashboard-varying-heights.stories.js | 39 +++ .../course-dashboard.stories.js | 25 ++ .../cxl-ui/cxl-course-card/template.js | 68 +++++ yarn.lock | 19 -- 14 files changed, 584 insertions(+), 20 deletions(-) create mode 100644 packages/cxl-ui/scss/cxl-course-card.scss create mode 100644 packages/cxl-ui/src/components/cxl-course-card.js create mode 100644 packages/storybook/cxl-ui/cxl-course-card/[theme=course].stories.js create mode 100644 packages/storybook/cxl-ui/cxl-course-card/[theme=playbook].stories.js create mode 100644 packages/storybook/cxl-ui/cxl-course-card/[theme=training].stories.js create mode 100644 packages/storybook/cxl-ui/cxl-course-card/[theme=video].stories.js create mode 100644 packages/storybook/cxl-ui/cxl-course-card/course-dashboard-varying-heights.stories.js create mode 100644 packages/storybook/cxl-ui/cxl-course-card/course-dashboard.stories.js create mode 100644 packages/storybook/cxl-ui/cxl-course-card/template.js diff --git a/package.json b/package.json index ab490d5b2..0bbf1acba 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "@commitlint/cli": "^12.0.1", "@commitlint/config-conventional": "^12.0.1", "@commitlint/config-lerna-scopes": "^12.1.4", - "@lit-labs/observers": "^2.0.0", "@open-wc/testing-helpers": "^0.9.5", "@size-limit/file": "^6.0.3", "@webcomponents/webcomponentsjs": "^2.2.10", 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..5f51f3642 --- /dev/null +++ b/packages/cxl-ui/scss/cxl-course-card.scss @@ -0,0 +1,253 @@ +/* stylelint-disable value-no-vendor-prefix, property-no-vendor-prefix -- some of these are necessary for line-clamp implementation */ +@use "~@conversionxl/cxl-lumo-styles/scss/mq"; + +:host { + position: relative; + box-sizing: border-box; + display: flex; + height: max-content; + max-width: calc(100% - 2 * var(--lumo-space-l)); + width: 100%; + min-height: 270px; + padding: var(--lumo-space-m) var(--lumo-space-l); + font-size: var(--lumo-font-size-s); + 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} { + margin: unset; + max-width: unset; + + .container > .attributes { + display: none; + } + + header .info .attributes { + display: flex; + } + } + + [empty] { + visibility: hidden; + user-select: none; + } +} + +: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([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: block; + gap: var(--lumo-space-s); + width: 100%; + transition: all 0.3s ease; + + > .attributes { + padding-top: 0; + } + + .content-wrapper, footer { + margin-top: var(--lumo-space-s); + } +} + +.tags { + display: flex; + gap: var(--lumo-space-s); + min-height: 1.6em; + max-width: 100%; +} + +.attributes { + display: flex; + align-items: flex-start; + align-self: stretch; + padding: var(--lumo-space-s) 0; + gap: var(--lumo-space-s); + 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); + align-self: stretch; + max-width: calc(100% - var(--lumo-space-m) - 80px); + overflow: hidden; + + .name { + font-family: Roboto, sans-serif; + font-size: var(--lumo-font-size-xl); + font-style: normal; + font-weight: 700; + line-height: var(--lumo-line-height-xs); + color: #1A1A1A; + } + + .attributes { + display: none; + } + + .tags { + flex-wrap: nowrap; + + ::slotted(.tag), .tag:not(:first-child):not(.new) { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .tag { + &:first-child, &.new { + color: var(--lumo-primary-color) + } + + &:first-child { + text-transform: capitalize; + } + } + } + } + + .instructor-image { + width: 80px; + height: 92px; + + img { + height: 80px; + overflow: hidden; + border-radius: 100px; + } + } +} + +.content-wrapper { + min-height: calc(var(--lumo-line-height-s) * 6); // Six standard line-heights + line-height: var(--lumo-line-height-s); + + .content { + min-height: calc(var(--lumo-line-height-s) * 3 + 1.25em); // Four standard line-heights plus global inherited

block margins + } + + .tags { + margin-top: var(--lumo-space-s); + transition: height 0.3s ease-in-out; + flex-wrap: wrap; + + ::slotted(span) { + font-style: italic; + line-height: 1; + } + } +} + +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) { + font-weight: 600; + color: var(--lumo-contrast); + } + + &::part(content) { + padding-bottom: 0; + font-size: var(--lumo-font-size-s); + line-height: var(--lumo-line-height-s); + } + } + + .cta { + position: absolute; + top: -6px; + right: 0; + font-weight: 600; + + vaadin-icon { + width: var(--lumo-icon-size-s); + height: var(--lumo-icon-size-s); + padding: calc(var(--lumo-space-xs) / 2); + margin-left: var(--lumo-space-xs); + background: var(--lumo-primary-color-10pct); + border-radius: 100%; + } + } +} + +.badge-new { + display: none; +} + +:host([new]) { + .badge-new { + position: absolute; + top: calc(-1 * var(--lumo-space-s)); + right: calc(-1 * var(--lumo-space-s)); + display: block; + width: calc(2 * var(--lumo-space-m)); + height: calc(2 * var(--lumo-space-m)); + padding: 6px; + color: var(--lumo-primary-contrast-color); + background: var(--lumo-primary-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..9bb7c09e8 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-course-card.js @@ -0,0 +1,109 @@ +/* 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 }) name = ''; + + @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: String, attribute: 'cta-url' }) ctaUrl = ''; + + _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.name} +
+
+
+ ${this.theme.toLowerCase() === 'course' ? html`` : ''} + ${this.time} +
+
+ By: ${this.instructor} +
+
+
+
+ ${this.instructor} +
+
+
+
+ ${this.theme.toLowerCase() === 'course' ? html`` : ''} + ${this.time} +
+
+ By: ${this.instructor} +
+
+
+
+ +
+
+ +
+
+ + +
+ `; + } +} 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..df55115b7 --- /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', + name: '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..810d9ebc3 --- /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', + name: '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..7c6e7474d --- /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', + name: '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..67dcfe103 --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/course-dashboard-varying-heights.stories.js @@ -0,0 +1,39 @@ +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'; +import { sectionStyles } from './template.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', + more: 'More', +} + +const Template = () => html` + ${sectionStyles} + +
+ ${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..abee5d0a5 --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/course-dashboard.stories.js @@ -0,0 +1,25 @@ +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'; +import { sectionStyles } from './template.js'; + +export default { + title: 'CXL UI/cxl-course-card', +}; + +const Template = () => html` + ${sectionStyles} + +
+ ${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..35cfb90d0 --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-course-card/template.js @@ -0,0 +1,68 @@ +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', + name: '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 +} + +export const sectionStyles = html` + +` diff --git a/yarn.lock b/yarn.lock index 6f35335a4..f27cd7aef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2375,25 +2375,6 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" -"@lit-labs/observers@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@lit-labs/observers/-/observers-2.0.0.tgz#b1ab73e43460e97b3910f6be68121bfabf14669a" - integrity sha512-NMbCjJEqp8V9TpTtt8HhzFVymx/WGpTD7iU+FMKSis4u3iBWwu2UYh6KChwFTEPW9xMum70r2IBnaViBINaGTA== - dependencies: - "@lit/reactive-element" "^1.1.0" - -"@lit-labs/ssr-dom-shim@^1.0.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9" - integrity sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ== - -"@lit/reactive-element@^1.1.0": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.2.tgz#c256690f82f2d7d0ffb0b1cdf68dcb1ec86cea28" - integrity sha512-rDfl+QnCYjuIGf5xI2sVJWdYIi56CTCwWa+nidKYX6oIuBYwUbT/vX4qbUDlHiZKJ/3FRNQ/tWJui44p6/stSA== - dependencies: - "@lit-labs/ssr-dom-shim" "^1.0.0" - "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.4.0": version "1.4.1" resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.4.1.tgz#3f587eec5708692135bc9e94cf396130604979f3"