From 82d473bef22a43f666fad15821381244236a2352 Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Mon, 27 Apr 2026 10:05:09 +0200 Subject: [PATCH 01/11] feat: add OSS feature card and feature cards container --- addon/components/o-s-s/feature-card.hbs | 12 +++ .../components/o-s-s/feature-card.stories.js | 90 ++++++++++++++++++ addon/components/o-s-s/feature-card.ts | 66 ++++++++++++++ .../o-s-s/feature-cards-container.hbs | 16 ++++ .../o-s-s/feature-cards-container.stories.js | 70 ++++++++++++++ .../o-s-s/feature-cards-container.ts | 91 +++++++++++++++++++ app/components/o-s-s/feature-card.js | 1 + .../o-s-s/feature-cards-container.js | 1 + app/styles/molecules/feature-card.less | 64 +++++++++++++ .../molecules/feature-cards-container.less | 23 +++++ app/styles/oss-components.less | 2 + tests/dummy/app/controllers/visual.ts | 39 ++++++++ tests/dummy/app/templates/visual.hbs | 25 +++++ .../components/o-s-s/feature-card-test.ts | 75 +++++++++++++++ .../o-s-s/feature-cards-container-test.ts | 70 ++++++++++++++ 15 files changed, 645 insertions(+) create mode 100644 addon/components/o-s-s/feature-card.hbs create mode 100644 addon/components/o-s-s/feature-card.stories.js create mode 100644 addon/components/o-s-s/feature-card.ts create mode 100644 addon/components/o-s-s/feature-cards-container.hbs create mode 100644 addon/components/o-s-s/feature-cards-container.stories.js create mode 100644 addon/components/o-s-s/feature-cards-container.ts create mode 100644 app/components/o-s-s/feature-card.js create mode 100644 app/components/o-s-s/feature-cards-container.js create mode 100644 app/styles/molecules/feature-card.less create mode 100644 app/styles/molecules/feature-cards-container.less create mode 100644 tests/integration/components/o-s-s/feature-card-test.ts create mode 100644 tests/integration/components/o-s-s/feature-cards-container-test.ts diff --git a/addon/components/o-s-s/feature-card.hbs b/addon/components/o-s-s/feature-card.hbs new file mode 100644 index 000000000..34bf75048 --- /dev/null +++ b/addon/components/o-s-s/feature-card.hbs @@ -0,0 +1,12 @@ +
+
+

{{@title}}

+

{{@description}}

+
+ + {{this.imageAlt}} + +
\ No newline at end of file diff --git a/addon/components/o-s-s/feature-card.stories.js b/addon/components/o-s-s/feature-card.stories.js new file mode 100644 index 000000000..0b1aa7676 --- /dev/null +++ b/addon/components/o-s-s/feature-card.stories.js @@ -0,0 +1,90 @@ +import { hbs } from 'ember-cli-htmlbars'; + +const ColorVariants = ['blue', 'violet', 'yellow']; +const ShadowVariants = ['sm', 'lg']; + +export default { + title: 'Components/OSS::FeatureCard', + component: 'feature-card', + argTypes: { + title: { + description: 'Card title', + table: { + type: { summary: 'string' }, + defaultValue: { summary: 'undefined' } + }, + control: { type: 'text' }, + type: { required: true } + }, + description: { + description: 'Card description', + table: { + type: { summary: 'string' }, + defaultValue: { summary: 'undefined' } + }, + control: { type: 'text' }, + type: { required: true } + }, + image: { + description: 'Image object including src and optional alt', + table: { + type: { summary: '{ src: string; alt?: string }' }, + defaultValue: { summary: 'undefined' } + }, + control: { type: 'object' }, + type: { required: true } + }, + colorVariant: { + description: 'Card background color variant', + table: { + type: { summary: ColorVariants.join('|') }, + defaultValue: { summary: 'violet' } + }, + options: ColorVariants, + control: { type: 'select' } + }, + shadowVariant: { + description: 'Card shadow variant', + table: { + type: { summary: ShadowVariants.join('|') }, + defaultValue: { summary: 'sm' } + }, + options: ShadowVariants, + control: { type: 'select' } + } + }, + parameters: { + docs: { + description: { + component: 'Displays a card with title, description and illustration. It includes color and shadow variants.' + } + } + } +}; + +const defaultArgs = { + title: 'Audience & content insights', + description: 'Pull demographics and media performance into your BI to target smarter and report faster.', + image: { + src: '/@upfluence/oss-components/assets/images/no-image.svg', + alt: 'No image illustration' + }, + colorVariant: 'violet', + shadowVariant: 'lg' +}; + +const Template = (args) => ({ + template: hbs` + + `, + context: args +}); + +export const Default = Template.bind({}); +Default.args = defaultArgs; diff --git a/addon/components/o-s-s/feature-card.ts b/addon/components/o-s-s/feature-card.ts new file mode 100644 index 000000000..22e6fdaf3 --- /dev/null +++ b/addon/components/o-s-s/feature-card.ts @@ -0,0 +1,66 @@ +import Component from '@glimmer/component'; +import { assert } from '@ember/debug'; + +export const COLOR_VARIANTS = ['blue', 'violet', 'yellow'] as const; +export type OSSFeatureCardColorVariant = (typeof COLOR_VARIANTS)[number]; +const DEFAULT_COLOR_VARIANT = 'violet' as const satisfies OSSFeatureCardColorVariant; + +export const SHADOW_VARIANTS = ['sm', 'lg'] as const; +export type OSSFeatureCardShadowVariant = (typeof SHADOW_VARIANTS)[number]; +const DEFAULT_SHADOW_VARIANT = 'sm' as const satisfies OSSFeatureCardShadowVariant; + +export type OSSFeatureCardImage = { + src: string; + alt?: string; +}; + +export type OSSFeatureCardArgs = { + title: string; + description: string; + image: OSSFeatureCardImage; + colorVariant?: OSSFeatureCardColorVariant; + shadowVariant?: OSSFeatureCardShadowVariant; +}; + +export default class OSSFeatureCard extends Component { + constructor(owner: unknown, args: OSSFeatureCardArgs) { + super(owner, args); + + assert( + '[OSS::FeatureCard] The @title parameter is mandatory', + typeof args.title === 'string' && args.title.trim().length > 0 + ); + assert( + '[OSS::FeatureCard] The @description parameter is mandatory', + typeof args.description === 'string' && args.description.trim().length > 0 + ); + assert( + '[OSS::FeatureCard] The @image parameter is mandatory and must contain a src key', + typeof args?.image?.src === 'string' && args.image.src.trim().length > 0 + ); + if (args.colorVariant) { + assert( + `[OSS::FeatureCard] @colorVariant must be one of: ${COLOR_VARIANTS.join(', ')}`, + COLOR_VARIANTS.includes(args.colorVariant) + ); + } + if (args.shadowVariant) { + assert( + `[OSS::FeatureCard] @shadowVariant must be one of: ${SHADOW_VARIANTS.join(', ')}`, + SHADOW_VARIANTS.includes(args.shadowVariant) + ); + } + } + + get colorVariant(): OSSFeatureCardColorVariant { + return this.args.colorVariant ?? DEFAULT_COLOR_VARIANT; + } + + get shadowVariant(): OSSFeatureCardShadowVariant { + return this.args.shadowVariant ?? DEFAULT_SHADOW_VARIANT; + } + + get imageAlt(): string { + return this.args.image.alt ?? ''; + } +} diff --git a/addon/components/o-s-s/feature-cards-container.hbs b/addon/components/o-s-s/feature-cards-container.hbs new file mode 100644 index 000000000..3c1d5d4be --- /dev/null +++ b/addon/components/o-s-s/feature-cards-container.hbs @@ -0,0 +1,16 @@ +
+ {{#each this.cardsWithLayout as |card|}} +
+ +
+ {{/each}} +
\ No newline at end of file diff --git a/addon/components/o-s-s/feature-cards-container.stories.js b/addon/components/o-s-s/feature-cards-container.stories.js new file mode 100644 index 000000000..d87abc3c9 --- /dev/null +++ b/addon/components/o-s-s/feature-cards-container.stories.js @@ -0,0 +1,70 @@ +import { hbs } from 'ember-cli-htmlbars'; + +export default { + title: 'Components/OSS::FeatureCardsContainer', + component: 'feature-cards-container', + argTypes: { + cards: { + description: '2 or 3 feature cards. Colors, shadows and rotation are automatically set by the container.', + table: { + type: { summary: 'Array<{ title: string; description: string; image: { src: string; alt?: string } }>' }, + defaultValue: { summary: 'undefined' } + }, + control: { type: 'object' }, + type: { required: true } + } + }, + parameters: { + docs: { + description: { + component: + 'Wrapper that lays out 2 or 3 OSS::FeatureCard components with colors, shadows, angles and overlap. See [OSS::FeatureCard](?path=/story/components-oss-featurecard--default) for individual card details.' + } + } + } +}; + +const defaultImage = { + src: '/@upfluence/oss-components/assets/images/no-image.svg', + alt: 'No image illustration' +}; + +const threeCardsArgs = { + cards: [ + { + title: 'Creator discovery at scale', + description: + 'Discover and enrich creators via API using platform, region, and key attributes to power precise scouting.', + image: defaultImage + }, + { + title: 'Audience & content insights', + description: 'Pull demographics and media performance into your BI to target smarter and report faster.', + image: defaultImage + }, + { + title: 'Campaign performance tracking', + description: 'Track contribution stages, orders and ROI, then sync results to your CRM.', + image: defaultImage + } + ] +}; + +const twoCardsArgs = { + cards: threeCardsArgs.cards.slice(0, 2) +}; + +const Template = (args) => ({ + template: hbs` +
+ +
+ `, + context: args +}); + +export const ThreeCards = Template.bind({}); +ThreeCards.args = threeCardsArgs; + +export const TwoCards = Template.bind({}); +TwoCards.args = twoCardsArgs; diff --git a/addon/components/o-s-s/feature-cards-container.ts b/addon/components/o-s-s/feature-cards-container.ts new file mode 100644 index 000000000..3bf2314a7 --- /dev/null +++ b/addon/components/o-s-s/feature-cards-container.ts @@ -0,0 +1,91 @@ +import { assert } from '@ember/debug'; +import Component from '@glimmer/component'; + +import type { OSSFeatureCardArgs } from './feature-card'; + +type OSSFeatureCardsContainerArgs = { + cards: Pick[]; +}; + +type OSSFeatureCardsContainerComputedCard = Required & { + isCenter: boolean; + style: string; +}; + +const TWO_CARDS_OFFSET_X = '45%'; +const THREE_CARD_OFFSET_X = '80%'; +const ROTATION_ANGLE = 11.25; + +const CARDS_LAYOUT = { + 2: [ + { + colorVariant: 'blue', + shadowVariant: 'sm', + rotation: -ROTATION_ANGLE, + offsetX: `-${TWO_CARDS_OFFSET_X}`, + isCenter: false + }, + { + colorVariant: 'yellow', + shadowVariant: 'sm', + rotation: ROTATION_ANGLE, + offsetX: TWO_CARDS_OFFSET_X, + isCenter: false + } + ], + 3: [ + { + colorVariant: 'blue', + shadowVariant: 'sm', + rotation: -ROTATION_ANGLE, + offsetX: `-${THREE_CARD_OFFSET_X}`, + isCenter: false + }, + { colorVariant: 'violet', shadowVariant: 'lg', rotation: 0, offsetX: '0', isCenter: true }, + { + colorVariant: 'yellow', + shadowVariant: 'sm', + rotation: ROTATION_ANGLE, + offsetX: THREE_CARD_OFFSET_X, + isCenter: false + } + ] +} as const satisfies Record< + number, + (Pick & { + rotation: number; + offsetX: string; + })[] +>; + +export default class OSSFeatureCardsContainer extends Component { + constructor(owner: unknown, args: OSSFeatureCardsContainerArgs) { + super(owner, args); + + assert('[OSS::FeatureCardsContainer] The @cards parameter is mandatory', Array.isArray(args.cards)); + assert( + '[OSS::FeatureCardsContainer] @cards must contain exactly 2 or 3 cards', + args.cards.length === 2 || args.cards.length === 3 + ); + } + + get cardsWithLayout(): OSSFeatureCardsContainerComputedCard[] { + const layout = CARDS_LAYOUT[this.args.cards.length as keyof typeof CARDS_LAYOUT]; + assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', !!layout); + + return this.args.cards.map((card, index) => { + const cardLayout = layout[index]; + assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', !!cardLayout); + + const { colorVariant, shadowVariant, isCenter, rotation, offsetX } = cardLayout; + + return { + ...card, + colorVariant, + shadowVariant, + isCenter, + style: `transform: translateX(${offsetX}) rotate(${rotation}deg);` + }; + }); + } +} diff --git a/app/components/o-s-s/feature-card.js b/app/components/o-s-s/feature-card.js new file mode 100644 index 000000000..127673b07 --- /dev/null +++ b/app/components/o-s-s/feature-card.js @@ -0,0 +1 @@ +export { default } from '@upfluence/oss-components/components/o-s-s/feature-card'; diff --git a/app/components/o-s-s/feature-cards-container.js b/app/components/o-s-s/feature-cards-container.js new file mode 100644 index 000000000..6d9cc2829 --- /dev/null +++ b/app/components/o-s-s/feature-cards-container.js @@ -0,0 +1 @@ +export { default } from '@upfluence/oss-components/components/o-s-s/feature-cards-container'; diff --git a/app/styles/molecules/feature-card.less b/app/styles/molecules/feature-card.less new file mode 100644 index 000000000..5b9ae97c0 --- /dev/null +++ b/app/styles/molecules/feature-card.less @@ -0,0 +1,64 @@ +.oss-feature-card { + display: flex; + flex-direction: column; + gap: var(--spacing-px-24); + align-items: flex-start; + justify-content: center; + max-inline-size: 342px; + padding: var(--spacing-px-30) var(--spacing-px-60); + border: 1px solid; + border-radius: var(--border-radius-md); + font-family: var(--font-family-base); + line-height: 1.6; + + &--color-blue { + background-color: var(--color-blue-50); + border-color: var(--color-blue-100); + } + + &--color-violet { + background-color: var(--color-violet-50); + border-color: var(--color-violet-100); + } + + &--color-yellow { + background-color: var(--color-warning-50); + border-color: var(--color-warning-100); + } + + &--shadow-sm { + box-shadow: var(--box-shadow-sm); + } + + &--shadow-lg { + box-shadow: var(--box-shadow-lg); + } + + &__content { + display: flex; + flex-direction: column; + gap: var(--spacing-px-3); + } + + &__title { + font-size: var(--font-size-md); + font-weight: var(--font-weight-semibold); + color: var(--color-gray-900); + text-wrap: pretty; + margin: 0; + } + + &__description { + font-size: var(--font-size-sm); + color: var(--color-gray-600); + text-wrap: balance; + margin: 0; + } + + &__illustration { + inline-size: 100%; + block-size: auto; + max-block-size: 123px; + object-fit: cover; + } +} diff --git a/app/styles/molecules/feature-cards-container.less b/app/styles/molecules/feature-cards-container.less new file mode 100644 index 000000000..f4e74b682 --- /dev/null +++ b/app/styles/molecules/feature-cards-container.less @@ -0,0 +1,23 @@ +.oss-feature-cards-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-block-size: 430px; + inline-size: 100%; + + &__item { + position: absolute; + + &--center { + top: 30px; + } + } + + & > :nth-child(odd) { + top: 76px; + } + & > :nth-child(even) { + z-index: 2; + } +} diff --git a/app/styles/oss-components.less b/app/styles/oss-components.less index baab44087..ce7f4b0b1 100644 --- a/app/styles/oss-components.less +++ b/app/styles/oss-components.less @@ -71,6 +71,8 @@ @import 'molecules/password-input'; @import 'molecules/avatar-group'; @import 'molecules/stack-container'; +@import 'molecules/feature-card'; +@import 'molecules/feature-cards-container'; @import 'molecules/onboarding-state'; @import 'molecules/infinite-select-option'; @import 'organisms/table'; diff --git a/tests/dummy/app/controllers/visual.ts b/tests/dummy/app/controllers/visual.ts index 02407c670..ae30011b2 100644 --- a/tests/dummy/app/controllers/visual.ts +++ b/tests/dummy/app/controllers/visual.ts @@ -3,6 +3,7 @@ import { tracked } from '@glimmer/tracking'; import { action, set } from '@ember/object'; import type { ModeSwitchOption } from '@upfluence/oss-components/components/o-s-s/mode-switch'; import type { FeedbackMessage } from '@upfluence/oss-components/components/o-s-s/input-container'; +import type { OSSFeatureCardArgs, OSSFeatureCardImage } from '@upfluence/oss-components/components/o-s-s/feature-card'; export default class Visual extends Controller { @tracked toggleValue: boolean = false; @@ -112,6 +113,44 @@ export default class Visual extends Controller { @tracked feedbackMessageWarning: FeedbackMessage = { type: 'warning', value: '' }; @tracked feedbackMessageSuccess: FeedbackMessage = { type: 'success', value: '' }; + @tracked featureCardImage: OSSFeatureCardImage = { + src: '/@upfluence/oss-components/assets/images/no-image.svg', + alt: 'No image illustration' + }; + + @tracked twoFeatureCards: OSSFeatureCardArgs[] = [ + { + title: 'Creator discovery at scale', + description: + 'Discover and enrich creators via API using platform, region, and key attributes to power precise, data-driven scouting.', + image: this.featureCardImage + }, + { + title: 'Audience & content insights', + description: 'Pull demographics and media performance into your BI to target smarter and report faster.', + image: this.featureCardImage + } + ]; + @tracked threeFeatureCards: OSSFeatureCardArgs[] = [ + { + title: 'Creator discovery at scale', + description: + 'Discover and enrich creators via API using platform, region, and key attributes to power precise, data-driven scouting.', + image: this.featureCardImage + }, + { + title: 'Audience & content insights', + description: 'Pull demographics and media performance into your BI to target smarter and report faster.', + image: this.featureCardImage + }, + { + title: 'Campaign performance tracking', + description: 'Track contribution stages, orders and discount-code ROI, then sync results to your CRM.', + image: this.featureCardImage + } + ]; + @tracked featureCard: OSSFeatureCardArgs = this.threeFeatureCards[1]!; + @tracked progressBarSuccess: number = 30; @tracked progressBarWarning: number = 25; @tracked progressBarDanger: number = 15; diff --git a/tests/dummy/app/templates/visual.hbs b/tests/dummy/app/templates/visual.hbs index 1f336ff28..e6950b2e3 100644 --- a/tests/dummy/app/templates/visual.hbs +++ b/tests/dummy/app/templates/visual.hbs @@ -949,4 +949,29 @@ + +
+
+ Feature cards +
+
+
+ +
+ +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/tests/integration/components/o-s-s/feature-card-test.ts b/tests/integration/components/o-s-s/feature-card-test.ts new file mode 100644 index 000000000..86736f40d --- /dev/null +++ b/tests/integration/components/o-s-s/feature-card-test.ts @@ -0,0 +1,75 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, setupOnerror } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | o-s-s/feature-card', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.title = 'Audience & content insights'; + this.description = 'Pull demographics and media performance into your BI to target smarter and report faster.'; + this.image = { + src: '/@upfluence/oss-components/assets/images/no-image.svg', + alt: 'No image illustration' + }; + }); + + test('it renders', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.oss-feature-card').exists(); + assert.dom('.oss-feature-card__title').hasText(this.title); + assert.dom('.oss-feature-card__description').hasText(this.description); + assert.dom('.oss-feature-card__illustration').hasAttribute('src', this.image.src); + assert.dom('.oss-feature-card__illustration').hasAttribute('alt', this.image.alt); + }); + + test('it renders extra attributes (splattributes)', async function (assert) { + await render( + hbs`` + ); + + assert.dom('[data-control-name="feature-card-test"]').exists(); + }); + + test('it supports color and shadow variants', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.oss-feature-card').hasClass('oss-feature-card--color-blue'); + assert.dom('.oss-feature-card').hasClass('oss-feature-card--shadow-lg'); + }); + + module('Error management', () => { + test('it throws when @title is missing', async function (assert) { + setupOnerror((err: any) => { + assert.equal(err.message, 'Assertion Failed: [OSS::FeatureCard] The @title parameter is mandatory'); + }); + + await render(hbs``); + }); + + test('it throws when @description is missing', async function (assert) { + setupOnerror((err: any) => { + assert.equal(err.message, 'Assertion Failed: [OSS::FeatureCard] The @description parameter is mandatory'); + }); + + await render(hbs``); + }); + + test('it throws when @image is missing', async function (assert) { + setupOnerror((err: any) => { + assert.equal( + err.message, + 'Assertion Failed: [OSS::FeatureCard] The @image parameter is mandatory and must contain a src key' + ); + }); + + await render(hbs``); + }); + }); +}); diff --git a/tests/integration/components/o-s-s/feature-cards-container-test.ts b/tests/integration/components/o-s-s/feature-cards-container-test.ts new file mode 100644 index 000000000..47f10c989 --- /dev/null +++ b/tests/integration/components/o-s-s/feature-cards-container-test.ts @@ -0,0 +1,70 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, setupOnerror } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | o-s-s/feature-cards-container', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.image = { + src: '/@upfluence/oss-components/assets/images/no-image.svg', + alt: 'No image illustration' + }; + this.cards = [ + { + title: 'Creator discovery at scale', + description: 'Discover and enrich creators via API.', + image: this.image + }, + { + title: 'Audience & content insights', + description: 'Pull demographics and media performance into your BI.', + image: this.image + }, + { + title: 'Campaign performance tracking', + description: 'Track contribution stages and ROI.', + image: this.image + } + ]; + }); + + test('it renders', async function (assert) { + await render(hbs``); + + assert.dom('.oss-feature-cards-container').exists(); + assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 3 }); + }); + + test('it supports 2 cards layout', async function (assert) { + this.set('cards', this.cards.slice(0, 2)); + + await render(hbs``); + + assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 2 }); + }); + + module('Error management', () => { + test('it throws when @cards is missing', async function (assert) { + setupOnerror((err: any) => { + assert.equal(err.message, 'Assertion Failed: [OSS::FeatureCardsContainer] The @cards parameter is mandatory'); + }); + + await render(hbs``); + }); + + test('it throws when @cards has invalid count', async function (assert) { + this.cards = [this.cards[0]]; + + setupOnerror((err: any) => { + assert.equal( + err.message, + 'Assertion Failed: [OSS::FeatureCardsContainer] @cards must contain exactly 2 or 3 cards' + ); + }); + + await render(hbs``); + }); + }); +}); From 5b993fc765892ab77be0d37cce2ff58c1a52b09b Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Mon, 27 Apr 2026 10:29:48 +0200 Subject: [PATCH 02/11] test: improve test coverage Co-authored-by: Copilot --- .../o-s-s/feature-cards-container.ts | 5 +- .../o-s-s/feature-cards-container-test.ts | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/addon/components/o-s-s/feature-cards-container.ts b/addon/components/o-s-s/feature-cards-container.ts index 3bf2314a7..334a2634b 100644 --- a/addon/components/o-s-s/feature-cards-container.ts +++ b/addon/components/o-s-s/feature-cards-container.ts @@ -70,10 +70,11 @@ export default class OSSFeatureCardsContainer extends Component { + return cards.map((card, index) => { const cardLayout = layout[index]; assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', !!cardLayout); diff --git a/tests/integration/components/o-s-s/feature-cards-container-test.ts b/tests/integration/components/o-s-s/feature-cards-container-test.ts index 47f10c989..83ffb8035 100644 --- a/tests/integration/components/o-s-s/feature-cards-container-test.ts +++ b/tests/integration/components/o-s-s/feature-cards-container-test.ts @@ -43,6 +43,61 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook await render(hbs``); assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 2 }); + + assert + .dom('.oss-feature-cards-container__item:nth-child(1)') + .hasAttribute('style', 'transform: translateX(-45%) rotate(-11.25deg);'); + assert + .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') + .hasClass('oss-feature-card--color-blue'); + assert + .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') + .hasClass('oss-feature-card--shadow-sm'); + + assert + .dom('.oss-feature-cards-container__item:nth-child(2)') + .hasAttribute('style', 'transform: translateX(45%) rotate(11.25deg);'); + assert + .dom('.oss-feature-cards-container__item:nth-child(2) .oss-feature-card') + .hasClass('oss-feature-card--color-yellow'); + assert + .dom('.oss-feature-cards-container__item:nth-child(2) .oss-feature-card') + .hasClass('oss-feature-card--shadow-sm'); + }); + + test('it applies computed layout rules for 3 cards', async function (assert) { + await render(hbs``); + + assert + .dom('.oss-feature-cards-container__item:nth-child(1)') + .hasAttribute('style', 'transform: translateX(-80%) rotate(-11.25deg);'); + assert + .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') + .hasClass('oss-feature-card--color-blue'); + assert + .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') + .hasClass('oss-feature-card--shadow-sm'); + + assert + .dom('.oss-feature-cards-container__item:nth-child(2)') + .hasAttribute('style', 'transform: translateX(0) rotate(0deg);'); + assert.dom('.oss-feature-cards-container__item:nth-child(2)').hasClass('oss-feature-cards-container__item--center'); + assert + .dom('.oss-feature-cards-container__item:nth-child(2) .oss-feature-card') + .hasClass('oss-feature-card--color-violet'); + assert + .dom('.oss-feature-cards-container__item:nth-child(2) .oss-feature-card') + .hasClass('oss-feature-card--shadow-lg'); + + assert + .dom('.oss-feature-cards-container__item:nth-child(3)') + .hasAttribute('style', 'transform: translateX(80%) rotate(11.25deg);'); + assert + .dom('.oss-feature-cards-container__item:nth-child(3) .oss-feature-card') + .hasClass('oss-feature-card--color-yellow'); + assert + .dom('.oss-feature-cards-container__item:nth-child(3) .oss-feature-card') + .hasClass('oss-feature-card--shadow-sm'); }); module('Error management', () => { From 1b35098a67219d600204fa4c2e20a29cd550e0e6 Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Mon, 27 Apr 2026 16:15:34 +0200 Subject: [PATCH 03/11] fix: PR comments taken into account Co-authored-by: Copilot --- addon/components/o-s-s/feature-card.hbs | 6 +- .../components/o-s-s/feature-card.stories.js | 14 +-- addon/components/o-s-s/feature-card.ts | 8 ++ .../o-s-s/feature-cards-container.hbs | 5 +- .../o-s-s/feature-cards-container.stories.js | 11 +- .../o-s-s/feature-cards-container.ts | 8 +- tests/dummy/app/controllers/visual.ts | 8 +- .../components/o-s-s/feature-card-test.ts | 101 ++++++++++++++---- .../o-s-s/feature-cards-container-test.ts | 23 +++- 9 files changed, 137 insertions(+), 47 deletions(-) diff --git a/addon/components/o-s-s/feature-card.hbs b/addon/components/o-s-s/feature-card.hbs index 34bf75048..194f863dd 100644 --- a/addon/components/o-s-s/feature-card.hbs +++ b/addon/components/o-s-s/feature-card.hbs @@ -1,12 +1,8 @@ -
+

{{@title}}

{{@description}}

{{this.imageAlt}} -
\ No newline at end of file diff --git a/addon/components/o-s-s/feature-card.stories.js b/addon/components/o-s-s/feature-card.stories.js index 0b1aa7676..eed57a0d3 100644 --- a/addon/components/o-s-s/feature-card.stories.js +++ b/addon/components/o-s-s/feature-card.stories.js @@ -75,13 +75,13 @@ const defaultArgs = { const Template = (args) => ({ template: hbs` - + `, context: args }); diff --git a/addon/components/o-s-s/feature-card.ts b/addon/components/o-s-s/feature-card.ts index 22e6fdaf3..cba5bb4fd 100644 --- a/addon/components/o-s-s/feature-card.ts +++ b/addon/components/o-s-s/feature-card.ts @@ -60,6 +60,14 @@ export default class OSSFeatureCard extends Component { return this.args.shadowVariant ?? DEFAULT_SHADOW_VARIANT; } + get computedClasses(): string { + return [ + 'oss-feature-card', + `oss-feature-card--color-${this.colorVariant}`, + `oss-feature-card--shadow-${this.shadowVariant}` + ].join(' '); + } + get imageAlt(): string { return this.args.image.alt ?? ''; } diff --git a/addon/components/o-s-s/feature-cards-container.hbs b/addon/components/o-s-s/feature-cards-container.hbs index 3c1d5d4be..e1d7a6548 100644 --- a/addon/components/o-s-s/feature-cards-container.hbs +++ b/addon/components/o-s-s/feature-cards-container.hbs @@ -1,9 +1,6 @@
{{#each this.cardsWithLayout as |card|}} -
+
' }, defaultValue: { summary: 'undefined' } @@ -18,7 +18,7 @@ export default { docs: { description: { component: - 'Wrapper that lays out 2 or 3 OSS::FeatureCard components with colors, shadows, angles and overlap. See [OSS::FeatureCard](?path=/story/components-oss-featurecard--default) for individual card details.' + 'Wrapper that lays out 1 to 3 OSS::FeatureCard components with colors, shadows, angles and overlap. See [OSS::FeatureCard](?path=/story/components-oss-featurecard--default) for individual card details.' } } } @@ -54,6 +54,10 @@ const twoCardsArgs = { cards: threeCardsArgs.cards.slice(0, 2) }; +const oneCardArgs = { + cards: threeCardsArgs.cards.slice(0, 1) +}; + const Template = (args) => ({ template: hbs`
@@ -68,3 +72,6 @@ ThreeCards.args = threeCardsArgs; export const TwoCards = Template.bind({}); TwoCards.args = twoCardsArgs; + +export const OneCard = Template.bind({}); +OneCard.args = oneCardArgs; diff --git a/addon/components/o-s-s/feature-cards-container.ts b/addon/components/o-s-s/feature-cards-container.ts index 334a2634b..ccf62011e 100644 --- a/addon/components/o-s-s/feature-cards-container.ts +++ b/addon/components/o-s-s/feature-cards-container.ts @@ -8,6 +8,7 @@ type OSSFeatureCardsContainerArgs = { }; type OSSFeatureCardsContainerComputedCard = Required & { + className: string; isCenter: boolean; style: string; }; @@ -15,8 +16,10 @@ type OSSFeatureCardsContainerComputedCard = Required & { const TWO_CARDS_OFFSET_X = '45%'; const THREE_CARD_OFFSET_X = '80%'; const ROTATION_ANGLE = 11.25; +const BASE_ITEM_CLASS = 'oss-feature-cards-container__item'; const CARDS_LAYOUT = { + 1: [{ colorVariant: 'violet', shadowVariant: 'lg', rotation: 0, offsetX: '0', isCenter: true }], 2: [ { colorVariant: 'blue', @@ -64,8 +67,8 @@ export default class OSSFeatureCardsContainer extends Component= 1 && args.cards.length <= 3 ); } @@ -82,6 +85,7 @@ export default class OSSFeatureCardsContainer extends Component` - ); + module('Rendering', () => { + test('it renders the title', async function (assert) { + await render( + hbs`` + ); - assert.dom('.oss-feature-card').exists(); - assert.dom('.oss-feature-card__title').hasText(this.title); - assert.dom('.oss-feature-card__description').hasText(this.description); - assert.dom('.oss-feature-card__illustration').hasAttribute('src', this.image.src); - assert.dom('.oss-feature-card__illustration').hasAttribute('alt', this.image.alt); - }); + assert.dom('.oss-feature-card').exists(); + assert.dom('.oss-feature-card__title').hasText(this.title); + }); + + test('it renders the description', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.oss-feature-card').exists(); + assert.dom('.oss-feature-card__description').hasText(this.description); + }); + + test('it renders the image', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.oss-feature-card').exists(); + assert.dom('.oss-feature-card__illustration').hasAttribute('src', this.image.src); + assert.dom('.oss-feature-card__illustration').hasAttribute('alt', this.image.alt); + }); - test('it renders extra attributes (splattributes)', async function (assert) { - await render( - hbs`` - ); + test('it renders extra attributes (splattributes)', async function (assert) { + await render( + hbs`` + ); - assert.dom('[data-control-name="feature-card-test"]').exists(); + assert.dom('[data-control-name="feature-card-test"]').exists(); + }); }); - test('it supports color and shadow variants', async function (assert) { - await render( - hbs`` - ); + module('Variants', () => { + COLOR_VARIANTS.forEach((colorVariant) => { + test(`it supports the ${colorVariant} color variant`, async function (assert) { + this.colorVariant = colorVariant; + + await render( + hbs`` + ); + + assert.dom('.oss-feature-card').hasClass(`oss-feature-card--color-${colorVariant}`); + }); + }); + + SHADOW_VARIANTS.forEach((shadowVariant) => { + test(`it supports the ${shadowVariant} shadow variant`, async function (assert) { + this.shadowVariant = shadowVariant; + + await render( + hbs`` + ); - assert.dom('.oss-feature-card').hasClass('oss-feature-card--color-blue'); - assert.dom('.oss-feature-card').hasClass('oss-feature-card--shadow-lg'); + assert.dom('.oss-feature-card').hasClass(`oss-feature-card--shadow-${shadowVariant}`); + }); + }); }); module('Error management', () => { @@ -71,5 +107,28 @@ module('Integration | Component | o-s-s/feature-card', function (hooks) { await render(hbs``); }); + + test('it throws when @colorVariant is unknown', async function (assert) { + setupOnerror((err: any) => { + assert.equal( + err.message, + 'Assertion Failed: [OSS::FeatureCard] @colorVariant must be one of: blue, violet, yellow' + ); + }); + + await render( + hbs`` + ); + }); + + test('it throws when @shadowVariant is unknown', async function (assert) { + setupOnerror((err: any) => { + assert.equal(err.message, 'Assertion Failed: [OSS::FeatureCard] @shadowVariant must be one of: sm, lg'); + }); + + await render( + hbs`` + ); + }); }); }); diff --git a/tests/integration/components/o-s-s/feature-cards-container-test.ts b/tests/integration/components/o-s-s/feature-cards-container-test.ts index 83ffb8035..d2311e32b 100644 --- a/tests/integration/components/o-s-s/feature-cards-container-test.ts +++ b/tests/integration/components/o-s-s/feature-cards-container-test.ts @@ -37,6 +37,25 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 3 }); }); + test('it supports 1 card layout', async function (assert) { + this.set('cards', this.cards.slice(0, 1)); + + await render(hbs``); + + assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 1 }); + + assert + .dom('.oss-feature-cards-container__item:nth-child(1)') + .hasAttribute('style', 'transform: translateX(0) rotate(0deg);'); + assert.dom('.oss-feature-cards-container__item:nth-child(1)').hasClass('oss-feature-cards-container__item--center'); + assert + .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') + .hasClass('oss-feature-card--color-violet'); + assert + .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') + .hasClass('oss-feature-card--shadow-lg'); + }); + test('it supports 2 cards layout', async function (assert) { this.set('cards', this.cards.slice(0, 2)); @@ -110,12 +129,12 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook }); test('it throws when @cards has invalid count', async function (assert) { - this.cards = [this.cards[0]]; + this.cards = []; setupOnerror((err: any) => { assert.equal( err.message, - 'Assertion Failed: [OSS::FeatureCardsContainer] @cards must contain exactly 2 or 3 cards' + 'Assertion Failed: [OSS::FeatureCardsContainer] @cards must contain between 1 and 3 cards' ); }); From e5b7f8de5cf2db9ddfc566ea1dbc9cd8bb6979ac Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Mon, 27 Apr 2026 17:01:54 +0200 Subject: [PATCH 04/11] fix: quick PR fixes --- addon/components/o-s-s/feature-card.ts | 5 +++-- app/styles/molecules/feature-card.less | 5 ++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addon/components/o-s-s/feature-card.ts b/addon/components/o-s-s/feature-card.ts index cba5bb4fd..534854ffa 100644 --- a/addon/components/o-s-s/feature-card.ts +++ b/addon/components/o-s-s/feature-card.ts @@ -1,5 +1,6 @@ import Component from '@glimmer/component'; import { assert } from '@ember/debug'; +import { isSafeString } from '@upfluence/oss-components/utils'; export const COLOR_VARIANTS = ['blue', 'violet', 'yellow'] as const; export type OSSFeatureCardColorVariant = (typeof COLOR_VARIANTS)[number]; @@ -28,11 +29,11 @@ export default class OSSFeatureCard extends Component { assert( '[OSS::FeatureCard] The @title parameter is mandatory', - typeof args.title === 'string' && args.title.trim().length > 0 + typeof (args.title === 'string' || isSafeString(args.title)) && args.title.trim().length > 0 ); assert( '[OSS::FeatureCard] The @description parameter is mandatory', - typeof args.description === 'string' && args.description.trim().length > 0 + typeof (args.description === 'string' || isSafeString(args.description)) && args.description.trim().length > 0 ); assert( '[OSS::FeatureCard] The @image parameter is mandatory and must contain a src key', diff --git a/app/styles/molecules/feature-card.less b/app/styles/molecules/feature-card.less index 5b9ae97c0..439fd060e 100644 --- a/app/styles/molecules/feature-card.less +++ b/app/styles/molecules/feature-card.less @@ -8,8 +8,7 @@ padding: var(--spacing-px-30) var(--spacing-px-60); border: 1px solid; border-radius: var(--border-radius-md); - font-family: var(--font-family-base); - line-height: 1.6; + font-family: var(--font-family-stack); &--color-blue { background-color: var(--color-blue-50); @@ -58,7 +57,7 @@ &__illustration { inline-size: 100%; block-size: auto; - max-block-size: 123px; + max-block-size: 122px; object-fit: cover; } } From ece5c07a56d3ba0f3273dec37a7ccfae1e5f02dc Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Tue, 28 Apr 2026 10:15:11 +0200 Subject: [PATCH 05/11] fix: PR fixes Co-authored-by: Copilot --- addon/components/o-s-s/feature-card.ts | 10 ++++++---- .../integration/components/o-s-s/feature-card-test.ts | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/addon/components/o-s-s/feature-card.ts b/addon/components/o-s-s/feature-card.ts index 534854ffa..d380764bd 100644 --- a/addon/components/o-s-s/feature-card.ts +++ b/addon/components/o-s-s/feature-card.ts @@ -1,6 +1,7 @@ import Component from '@glimmer/component'; import { assert } from '@ember/debug'; import { isSafeString } from '@upfluence/oss-components/utils'; +import type { IntlService } from 'ember-intl'; export const COLOR_VARIANTS = ['blue', 'violet', 'yellow'] as const; export type OSSFeatureCardColorVariant = (typeof COLOR_VARIANTS)[number]; @@ -16,8 +17,8 @@ export type OSSFeatureCardImage = { }; export type OSSFeatureCardArgs = { - title: string; - description: string; + title: string | ReturnType; + description: string | ReturnType; image: OSSFeatureCardImage; colorVariant?: OSSFeatureCardColorVariant; shadowVariant?: OSSFeatureCardShadowVariant; @@ -29,11 +30,12 @@ export default class OSSFeatureCard extends Component { assert( '[OSS::FeatureCard] The @title parameter is mandatory', - typeof (args.title === 'string' || isSafeString(args.title)) && args.title.trim().length > 0 + (typeof args.title === 'string' || isSafeString(args.title)) && args.title.toString().trim().length > 0 ); assert( '[OSS::FeatureCard] The @description parameter is mandatory', - typeof (args.description === 'string' || isSafeString(args.description)) && args.description.trim().length > 0 + (typeof args.description === 'string' || isSafeString(args.description)) && + args.description.toString().trim().length > 0 ); assert( '[OSS::FeatureCard] The @image parameter is mandatory and must contain a src key', diff --git a/tests/integration/components/o-s-s/feature-card-test.ts b/tests/integration/components/o-s-s/feature-card-test.ts index b051a3e96..e96464683 100644 --- a/tests/integration/components/o-s-s/feature-card-test.ts +++ b/tests/integration/components/o-s-s/feature-card-test.ts @@ -3,12 +3,14 @@ import { setupRenderingTest } from 'ember-qunit'; import { render, setupOnerror } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { COLOR_VARIANTS, SHADOW_VARIANTS } from '@upfluence/oss-components/components/o-s-s/feature-card'; +import { setupIntl } from 'ember-intl/test-support'; module('Integration | Component | o-s-s/feature-card', function (hooks) { setupRenderingTest(hooks); + setupIntl(hooks); hooks.beforeEach(function () { - this.title = 'Audience & content insights'; + this.title = this.intl.t('Audience & content insights', { htmlSafe: true }); this.description = 'Pull demographics and media performance into your BI to target smarter and report faster.'; this.image = { src: '/@upfluence/oss-components/assets/images/no-image.svg', @@ -23,7 +25,7 @@ module('Integration | Component | o-s-s/feature-card', function (hooks) { ); assert.dom('.oss-feature-card').exists(); - assert.dom('.oss-feature-card__title').hasText(this.title); + assert.dom('.oss-feature-card__title').hasText(this.title.toString()); }); test('it renders the description', async function (assert) { From 08f87b9208b333f04f44d407ca0d80d42c95077a Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Tue, 28 Apr 2026 11:48:12 +0200 Subject: [PATCH 06/11] fix: improve feature card layout and styling, add default color/shadow variants, refactor, update tests Co-authored-by: Copilot --- addon/components/o-s-s/feature-card.hbs | 2 +- .../o-s-s/feature-cards-container.hbs | 18 +-- .../o-s-s/feature-cards-container.stories.js | 33 +++++- .../o-s-s/feature-cards-container.ts | 106 +++++++++--------- app/styles/molecules/feature-card.less | 7 ++ .../o-s-s/feature-cards-container-test.ts | 100 +++++++++-------- 6 files changed, 149 insertions(+), 117 deletions(-) diff --git a/addon/components/o-s-s/feature-card.hbs b/addon/components/o-s-s/feature-card.hbs index 194f863dd..a2728c002 100644 --- a/addon/components/o-s-s/feature-card.hbs +++ b/addon/components/o-s-s/feature-card.hbs @@ -1,7 +1,7 @@

{{@title}}

-

{{@description}}

+

{{@description}}

{{this.imageAlt}} diff --git a/addon/components/o-s-s/feature-cards-container.hbs b/addon/components/o-s-s/feature-cards-container.hbs index e1d7a6548..74fff8bb5 100644 --- a/addon/components/o-s-s/feature-cards-container.hbs +++ b/addon/components/o-s-s/feature-cards-container.hbs @@ -1,13 +1,13 @@
{{#each this.cardsWithLayout as |card|}} -
- -
+ {{/each}}
\ No newline at end of file diff --git a/addon/components/o-s-s/feature-cards-container.stories.js b/addon/components/o-s-s/feature-cards-container.stories.js index e3ae1dfb9..940b6371a 100644 --- a/addon/components/o-s-s/feature-cards-container.stories.js +++ b/addon/components/o-s-s/feature-cards-container.stories.js @@ -5,9 +5,13 @@ export default { component: 'feature-cards-container', argTypes: { cards: { - description: '1 to 3 feature cards. Colors, shadows and rotation are automatically set by the container.', + description: + '1 to 3 feature cards. Uses the same argument shape as OSS::FeatureCard; overlap and rotation are handled by the container. If colorVariant or shadowVariant are omitted, container defaults are applied.', table: { - type: { summary: 'Array<{ title: string; description: string; image: { src: string; alt?: string } }>' }, + type: { + summary: + 'Array<{ title: string; description: string; image: { src: string; alt?: string }; colorVariant?: "blue"|"violet"|"yellow"; shadowVariant?: "sm"|"lg" }>' + }, defaultValue: { summary: 'undefined' } }, control: { type: 'object' }, @@ -18,7 +22,7 @@ export default { docs: { description: { component: - 'Wrapper that lays out 1 to 3 OSS::FeatureCard components with colors, shadows, angles and overlap. See [OSS::FeatureCard](?path=/story/components-oss-featurecard--default) for individual card details.' + 'Wrapper that lays out 1 to 3 OSS::FeatureCard components with angles and overlap. Card color/shadow values are passed through from each card object, with defaults when variants are omitted. See [OSS::FeatureCard](?path=/story/components-oss-featurecard--default) for individual card details.' } } } @@ -35,17 +39,23 @@ const threeCardsArgs = { title: 'Creator discovery at scale', description: 'Discover and enrich creators via API using platform, region, and key attributes to power precise scouting.', - image: defaultImage + image: defaultImage, + colorVariant: 'blue', + shadowVariant: 'sm' }, { title: 'Audience & content insights', description: 'Pull demographics and media performance into your BI to target smarter and report faster.', - image: defaultImage + image: defaultImage, + colorVariant: 'violet', + shadowVariant: 'lg' }, { title: 'Campaign performance tracking', description: 'Track contribution stages, orders and ROI, then sync results to your CRM.', - image: defaultImage + image: defaultImage, + colorVariant: 'yellow', + shadowVariant: 'sm' } ] }; @@ -54,6 +64,14 @@ const twoCardsArgs = { cards: threeCardsArgs.cards.slice(0, 2) }; +const threeCardsWithDefaultsArgs = { + cards: threeCardsArgs.cards.map(({ title, description, image }) => ({ + title, + description, + image + })) +}; + const oneCardArgs = { cards: threeCardsArgs.cards.slice(0, 1) }; @@ -70,6 +88,9 @@ const Template = (args) => ({ export const ThreeCards = Template.bind({}); ThreeCards.args = threeCardsArgs; +export const ThreeCardsWithContainerDefaults = Template.bind({}); +ThreeCardsWithContainerDefaults.args = threeCardsWithDefaultsArgs; + export const TwoCards = Template.bind({}); TwoCards.args = twoCardsArgs; diff --git a/addon/components/o-s-s/feature-cards-container.ts b/addon/components/o-s-s/feature-cards-container.ts index ccf62011e..e09d3bd40 100644 --- a/addon/components/o-s-s/feature-cards-container.ts +++ b/addon/components/o-s-s/feature-cards-container.ts @@ -1,16 +1,16 @@ import { assert } from '@ember/debug'; import Component from '@glimmer/component'; -import type { OSSFeatureCardArgs } from './feature-card'; +import type { OSSFeatureCardArgs, OSSFeatureCardColorVariant, OSSFeatureCardShadowVariant } from './feature-card'; +import { htmlSafe } from '@ember/template'; type OSSFeatureCardsContainerArgs = { - cards: Pick[]; + cards: OSSFeatureCardArgs[]; }; -type OSSFeatureCardsContainerComputedCard = Required & { +type OSSFeatureCardsContainerComputedCard = OSSFeatureCardArgs & { className: string; - isCenter: boolean; - style: string; + style: ReturnType; }; const TWO_CARDS_OFFSET_X = '45%'; @@ -18,48 +18,42 @@ const THREE_CARD_OFFSET_X = '80%'; const ROTATION_ANGLE = 11.25; const BASE_ITEM_CLASS = 'oss-feature-cards-container__item'; -const CARDS_LAYOUT = { - 1: [{ colorVariant: 'violet', shadowVariant: 'lg', rotation: 0, offsetX: '0', isCenter: true }], - 2: [ - { - colorVariant: 'blue', - shadowVariant: 'sm', - rotation: -ROTATION_ANGLE, - offsetX: `-${TWO_CARDS_OFFSET_X}`, - isCenter: false - }, - { - colorVariant: 'yellow', - shadowVariant: 'sm', - rotation: ROTATION_ANGLE, - offsetX: TWO_CARDS_OFFSET_X, - isCenter: false - } - ], - 3: [ - { - colorVariant: 'blue', - shadowVariant: 'sm', - rotation: -ROTATION_ANGLE, - offsetX: `-${THREE_CARD_OFFSET_X}`, - isCenter: false - }, - { colorVariant: 'violet', shadowVariant: 'lg', rotation: 0, offsetX: '0', isCenter: true }, - { - colorVariant: 'yellow', - shadowVariant: 'sm', - rotation: ROTATION_ANGLE, - offsetX: THREE_CARD_OFFSET_X, - isCenter: false - } - ] -} as const satisfies Record< - number, - (Pick & { - rotation: number; - offsetX: string; - })[] ->; +function getCardRotation(cardsCount: number, index: number): number { + if (isCenterCard(cardsCount, index)) return 0; + + if (cardsCount === 2 || cardsCount === 3) return index === 0 ? -ROTATION_ANGLE : ROTATION_ANGLE; + + assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', false); +} + +function getCardOffsetX(cardsCount: number, index: number): string { + if (isCenterCard(cardsCount, index)) return '0'; + + if (cardsCount === 2) return index === 0 ? `-${TWO_CARDS_OFFSET_X}` : TWO_CARDS_OFFSET_X; + if (cardsCount === 3) return index === 0 ? `-${THREE_CARD_OFFSET_X}` : THREE_CARD_OFFSET_X; + + assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', false); +} + +function isCenterCard(cardsCount: number, index: number): boolean { + return cardsCount === 1 || (cardsCount === 3 && index === 1); +} + +function getDefaultCardColorVariant(cardsCount: number, index: number): OSSFeatureCardColorVariant { + if (isCenterCard(cardsCount, index)) return 'violet'; + + if (cardsCount === 2 || cardsCount === 3) return index === 0 ? 'blue' : 'yellow'; + + assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', false); +} + +function getDefaultCardShadowVariant(cardsCount: number, index: number): OSSFeatureCardShadowVariant { + if (isCenterCard(cardsCount, index)) return 'lg'; + + if (cardsCount === 2 || cardsCount === 3) return 'sm'; + + assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', false); +} export default class OSSFeatureCardsContainer extends Component { constructor(owner: unknown, args: OSSFeatureCardsContainerArgs) { @@ -74,22 +68,22 @@ export default class OSSFeatureCardsContainer extends Component { - const cardLayout = layout[index]; - assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', !!cardLayout); - - const { colorVariant, shadowVariant, isCenter, rotation, offsetX } = cardLayout; + const cardCount = cards.length; + const colorVariant = card.colorVariant ?? getDefaultCardColorVariant(cardCount, index); + const shadowVariant = card.shadowVariant ?? getDefaultCardShadowVariant(cardCount, index); + const className = isCenterCard(cardCount, index) ? `${BASE_ITEM_CLASS}--center` : ''; + const style = htmlSafe( + `transform: translateX(${getCardOffsetX(cardCount, index)}) rotate(${getCardRotation(cardCount, index)}deg);` + ); return { ...card, - className: isCenter ? `${BASE_ITEM_CLASS} ${BASE_ITEM_CLASS}--center` : BASE_ITEM_CLASS, colorVariant, shadowVariant, - isCenter, - style: `transform: translateX(${offsetX}) rotate(${rotation}deg);` + className, + style }; }); } diff --git a/app/styles/molecules/feature-card.less b/app/styles/molecules/feature-card.less index 439fd060e..62724ddea 100644 --- a/app/styles/molecules/feature-card.less +++ b/app/styles/molecules/feature-card.less @@ -48,10 +48,17 @@ } &__description { + --max-number-of-lines: 4; + font-size: var(--font-size-sm); color: var(--color-gray-600); text-wrap: balance; margin: 0; + + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: var(--max-number-of-lines); + line-clamp: var(--max-number-of-lines); } &__illustration { diff --git a/tests/integration/components/o-s-s/feature-cards-container-test.ts b/tests/integration/components/o-s-s/feature-cards-container-test.ts index d2311e32b..d34acc7de 100644 --- a/tests/integration/components/o-s-s/feature-cards-container-test.ts +++ b/tests/integration/components/o-s-s/feature-cards-container-test.ts @@ -2,6 +2,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, setupOnerror } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; +import type { OSSFeatureCardArgs } from '@upfluence/oss-components/components/o-s-s/feature-card'; module('Integration | Component | o-s-s/feature-cards-container', function (hooks) { setupRenderingTest(hooks); @@ -15,12 +16,15 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook { title: 'Creator discovery at scale', description: 'Discover and enrich creators via API.', - image: this.image + image: this.image, + colorVariant: 'yellow', + shadowVariant: 'lg' }, { title: 'Audience & content insights', description: 'Pull demographics and media performance into your BI.', - image: this.image + image: this.image, + colorVariant: 'blue' }, { title: 'Campaign performance tracking', @@ -28,9 +32,17 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook image: this.image } ]; + + this.defaultedCards = this.cards.map((card: OSSFeatureCardArgs) => ({ + title: card.title, + description: card.description, + image: card.image + })); }); test('it renders', async function (assert) { + this.cards = this.cards.slice(0, 3); + await render(hbs``); assert.dom('.oss-feature-cards-container').exists(); @@ -38,22 +50,19 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook }); test('it supports 1 card layout', async function (assert) { - this.set('cards', this.cards.slice(0, 1)); + this.cards = this.cards.slice(0, 1); await render(hbs``); assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 1 }); assert - .dom('.oss-feature-cards-container__item:nth-child(1)') + .dom('.oss-feature-cards-container>:nth-child(1)') .hasAttribute('style', 'transform: translateX(0) rotate(0deg);'); - assert.dom('.oss-feature-cards-container__item:nth-child(1)').hasClass('oss-feature-cards-container__item--center'); - assert - .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') - .hasClass('oss-feature-card--color-violet'); - assert - .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') - .hasClass('oss-feature-card--shadow-lg'); + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-cards-container__item--center'); + + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-yellow'); + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-lg'); }); test('it supports 2 cards layout', async function (assert) { @@ -66,57 +75,58 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook assert .dom('.oss-feature-cards-container__item:nth-child(1)') .hasAttribute('style', 'transform: translateX(-45%) rotate(-11.25deg);'); - assert - .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') - .hasClass('oss-feature-card--color-blue'); - assert - .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') - .hasClass('oss-feature-card--shadow-sm'); + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-yellow'); + + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-lg'); assert .dom('.oss-feature-cards-container__item:nth-child(2)') .hasAttribute('style', 'transform: translateX(45%) rotate(11.25deg);'); - assert - .dom('.oss-feature-cards-container__item:nth-child(2) .oss-feature-card') - .hasClass('oss-feature-card--color-yellow'); - assert - .dom('.oss-feature-cards-container__item:nth-child(2) .oss-feature-card') - .hasClass('oss-feature-card--shadow-sm'); + assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--color-blue'); + + assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--shadow-sm'); }); test('it applies computed layout rules for 3 cards', async function (assert) { + this.cards = this.cards.slice(0, 3); + await render(hbs``); assert - .dom('.oss-feature-cards-container__item:nth-child(1)') + .dom('.oss-feature-cards-container>:nth-child(1)') .hasAttribute('style', 'transform: translateX(-80%) rotate(-11.25deg);'); - assert - .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') - .hasClass('oss-feature-card--color-blue'); - assert - .dom('.oss-feature-cards-container__item:nth-child(1) .oss-feature-card') - .hasClass('oss-feature-card--shadow-sm'); + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-yellow'); + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-lg'); assert - .dom('.oss-feature-cards-container__item:nth-child(2)') + .dom('.oss-feature-cards-container>:nth-child(2)') .hasAttribute('style', 'transform: translateX(0) rotate(0deg);'); - assert.dom('.oss-feature-cards-container__item:nth-child(2)').hasClass('oss-feature-cards-container__item--center'); - assert - .dom('.oss-feature-cards-container__item:nth-child(2) .oss-feature-card') - .hasClass('oss-feature-card--color-violet'); - assert - .dom('.oss-feature-cards-container__item:nth-child(2) .oss-feature-card') - .hasClass('oss-feature-card--shadow-lg'); + assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-cards-container__item--center'); + assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--color-blue'); + assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--shadow-lg'); assert - .dom('.oss-feature-cards-container__item:nth-child(3)') + .dom('.oss-feature-cards-container>:nth-child(3)') .hasAttribute('style', 'transform: translateX(80%) rotate(11.25deg);'); - assert - .dom('.oss-feature-cards-container__item:nth-child(3) .oss-feature-card') - .hasClass('oss-feature-card--color-yellow'); - assert - .dom('.oss-feature-cards-container__item:nth-child(3) .oss-feature-card') - .hasClass('oss-feature-card--shadow-sm'); + assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--color-yellow'); + assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--shadow-sm'); + }); + + test('it sets default color and shadow variants when missing', async function (assert) { + this.cards = this.defaultedCards; + + await render(hbs``); + + assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 3 }); + + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-blue'); + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-sm'); + + assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--color-violet'); + assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--shadow-lg'); + + assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--color-yellow'); + assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--shadow-sm'); }); module('Error management', () => { From a2ae70f8ccb990ae768415dfa95559460450d3da Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Tue, 28 Apr 2026 14:37:19 +0200 Subject: [PATCH 07/11] refactor: move the layout logic in the css Co-authored-by: Copilot --- .../o-s-s/feature-cards-container.hbs | 5 +-- .../o-s-s/feature-cards-container.ts | 38 +------------------ .../molecules/feature-cards-container.less | 38 +++++++++++++++---- .../o-s-s/feature-cards-container-test.ts | 24 +----------- 4 files changed, 36 insertions(+), 69 deletions(-) diff --git a/addon/components/o-s-s/feature-cards-container.hbs b/addon/components/o-s-s/feature-cards-container.hbs index 74fff8bb5..96c6931c6 100644 --- a/addon/components/o-s-s/feature-cards-container.hbs +++ b/addon/components/o-s-s/feature-cards-container.hbs @@ -1,13 +1,12 @@
- {{#each this.cardsWithLayout as |card|}} + {{#each this.cardsWithComputedVariants as |card|}} {{/each}}
\ No newline at end of file diff --git a/addon/components/o-s-s/feature-cards-container.ts b/addon/components/o-s-s/feature-cards-container.ts index e09d3bd40..ea9e9353b 100644 --- a/addon/components/o-s-s/feature-cards-container.ts +++ b/addon/components/o-s-s/feature-cards-container.ts @@ -2,39 +2,11 @@ import { assert } from '@ember/debug'; import Component from '@glimmer/component'; import type { OSSFeatureCardArgs, OSSFeatureCardColorVariant, OSSFeatureCardShadowVariant } from './feature-card'; -import { htmlSafe } from '@ember/template'; type OSSFeatureCardsContainerArgs = { cards: OSSFeatureCardArgs[]; }; -type OSSFeatureCardsContainerComputedCard = OSSFeatureCardArgs & { - className: string; - style: ReturnType; -}; - -const TWO_CARDS_OFFSET_X = '45%'; -const THREE_CARD_OFFSET_X = '80%'; -const ROTATION_ANGLE = 11.25; -const BASE_ITEM_CLASS = 'oss-feature-cards-container__item'; - -function getCardRotation(cardsCount: number, index: number): number { - if (isCenterCard(cardsCount, index)) return 0; - - if (cardsCount === 2 || cardsCount === 3) return index === 0 ? -ROTATION_ANGLE : ROTATION_ANGLE; - - assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', false); -} - -function getCardOffsetX(cardsCount: number, index: number): string { - if (isCenterCard(cardsCount, index)) return '0'; - - if (cardsCount === 2) return index === 0 ? `-${TWO_CARDS_OFFSET_X}` : TWO_CARDS_OFFSET_X; - if (cardsCount === 3) return index === 0 ? `-${THREE_CARD_OFFSET_X}` : THREE_CARD_OFFSET_X; - - assert('[OSS::FeatureCardsContainer] Internal layout configuration mismatch', false); -} - function isCenterCard(cardsCount: number, index: number): boolean { return cardsCount === 1 || (cardsCount === 3 && index === 1); } @@ -66,24 +38,18 @@ export default class OSSFeatureCardsContainer extends Component { const cardCount = cards.length; const colorVariant = card.colorVariant ?? getDefaultCardColorVariant(cardCount, index); const shadowVariant = card.shadowVariant ?? getDefaultCardShadowVariant(cardCount, index); - const className = isCenterCard(cardCount, index) ? `${BASE_ITEM_CLASS}--center` : ''; - const style = htmlSafe( - `transform: translateX(${getCardOffsetX(cardCount, index)}) rotate(${getCardRotation(cardCount, index)}deg);` - ); return { ...card, colorVariant, - shadowVariant, - className, - style + shadowVariant }; }); } diff --git a/app/styles/molecules/feature-cards-container.less b/app/styles/molecules/feature-cards-container.less index f4e74b682..78002fdb7 100644 --- a/app/styles/molecules/feature-cards-container.less +++ b/app/styles/molecules/feature-cards-container.less @@ -1,4 +1,10 @@ .oss-feature-cards-container { + --two-cards-offset-x: 45%; + --three-cards-offset-x: 80%; + --rotation-angle: 11.25deg; + --center-card-top: 30px; + --side-cards-top: 58px; + position: relative; display: flex; align-items: center; @@ -9,15 +15,31 @@ &__item { position: absolute; - &--center { - top: 30px; + // To target child i in a list of n, we can use: :nth-child(i):nth-last-child(n - i + 1) + // Specific 2 cards layout + &:first-child:nth-last-child(2) { + translate: calc(-1 * var(--two-cards-offset-x)); + rotate: calc(-1 * var(--rotation-angle)); + } + &:last-child:nth-child(2) { + translate: var(--two-cards-offset-x); + rotate: var(--rotation-angle); } - } - & > :nth-child(odd) { - top: 76px; - } - & > :nth-child(even) { - z-index: 2; + // Specific 3 cards layout + &:first-child:nth-last-child(3) { + top: var(--side-cards-top); + translate: calc(-1 * var(--three-cards-offset-x)); + rotate: calc(-1 * var(--rotation-angle)); + } + &:nth-child(2):nth-last-child(2) { + top: var(--center-card-top); + z-index: 1; + } + &:last-child:nth-child(3) { + top: var(--side-cards-top); + translate: var(--three-cards-offset-x); + rotate: var(--rotation-angle); + } } } diff --git a/tests/integration/components/o-s-s/feature-cards-container-test.ts b/tests/integration/components/o-s-s/feature-cards-container-test.ts index d34acc7de..5fccd8068 100644 --- a/tests/integration/components/o-s-s/feature-cards-container-test.ts +++ b/tests/integration/components/o-s-s/feature-cards-container-test.ts @@ -55,11 +55,7 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook await render(hbs``); assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 1 }); - - assert - .dom('.oss-feature-cards-container>:nth-child(1)') - .hasAttribute('style', 'transform: translateX(0) rotate(0deg);'); - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-cards-container__item--center'); + assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-cards-container__item'); assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-yellow'); assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-lg'); @@ -72,16 +68,9 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 2 }); - assert - .dom('.oss-feature-cards-container__item:nth-child(1)') - .hasAttribute('style', 'transform: translateX(-45%) rotate(-11.25deg);'); - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-yellow'); assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-lg'); - assert - .dom('.oss-feature-cards-container__item:nth-child(2)') - .hasAttribute('style', 'transform: translateX(45%) rotate(11.25deg);'); assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--color-blue'); assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--shadow-sm'); @@ -92,22 +81,13 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook await render(hbs``); - assert - .dom('.oss-feature-cards-container>:nth-child(1)') - .hasAttribute('style', 'transform: translateX(-80%) rotate(-11.25deg);'); assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-yellow'); assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-lg'); - assert - .dom('.oss-feature-cards-container>:nth-child(2)') - .hasAttribute('style', 'transform: translateX(0) rotate(0deg);'); - assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-cards-container__item--center'); + assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-cards-container__item'); assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--color-blue'); assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--shadow-lg'); - assert - .dom('.oss-feature-cards-container>:nth-child(3)') - .hasAttribute('style', 'transform: translateX(80%) rotate(11.25deg);'); assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--color-yellow'); assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--shadow-sm'); }); From 135d2a2780e4ab30d530f3c403c9ba27b4fe1ccc Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Tue, 28 Apr 2026 15:07:15 +0200 Subject: [PATCH 08/11] test: simplify tests assertions Co-authored-by: Copilot --- .../o-s-s/feature-cards-container-test.ts | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/tests/integration/components/o-s-s/feature-cards-container-test.ts b/tests/integration/components/o-s-s/feature-cards-container-test.ts index 5fccd8068..00b25516c 100644 --- a/tests/integration/components/o-s-s/feature-cards-container-test.ts +++ b/tests/integration/components/o-s-s/feature-cards-container-test.ts @@ -55,10 +55,11 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook await render(hbs``); assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 1 }); - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-cards-container__item'); - - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-yellow'); - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-lg'); + assert + .dom('.oss-feature-cards-container>:first-child') + .hasClass('oss-feature-cards-container__item') + .hasClass('oss-feature-card--color-yellow') + .hasClass('oss-feature-card--shadow-lg'); }); test('it supports 2 cards layout', async function (assert) { @@ -68,12 +69,15 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 2 }); - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-yellow'); - - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-lg'); - assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--color-blue'); + assert + .dom('.oss-feature-cards-container>:first-child') + .hasClass('oss-feature-card--color-yellow') + .hasClass('oss-feature-card--shadow-lg'); - assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--shadow-sm'); + assert + .dom('.oss-feature-cards-container>:last-child') + .hasClass('oss-feature-card--color-blue') + .hasClass('oss-feature-card--shadow-sm'); }); test('it applies computed layout rules for 3 cards', async function (assert) { @@ -81,15 +85,21 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook await render(hbs``); - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-yellow'); - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-lg'); - - assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-cards-container__item'); - assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--color-blue'); - assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--shadow-lg'); - - assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--color-yellow'); - assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--shadow-sm'); + assert + .dom('.oss-feature-cards-container>:first-child') + .hasClass('oss-feature-card--color-yellow') + .hasClass('oss-feature-card--shadow-lg'); + + assert + .dom('.oss-feature-cards-container>:nth-child(2)') + .hasClass('oss-feature-cards-container__item') + .hasClass('oss-feature-card--color-blue') + .hasClass('oss-feature-card--shadow-lg'); + + assert + .dom('.oss-feature-cards-container>:last-child') + .hasClass('oss-feature-card--color-yellow') + .hasClass('oss-feature-card--shadow-sm'); }); test('it sets default color and shadow variants when missing', async function (assert) { @@ -99,14 +109,20 @@ module('Integration | Component | o-s-s/feature-cards-container', function (hook assert.dom('.oss-feature-cards-container .oss-feature-card').exists({ count: 3 }); - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--color-blue'); - assert.dom('.oss-feature-cards-container>:nth-child(1)').hasClass('oss-feature-card--shadow-sm'); + assert + .dom('.oss-feature-cards-container>:nth-child(1)') + .hasClass('oss-feature-card--color-blue') + .hasClass('oss-feature-card--shadow-sm'); - assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--color-violet'); - assert.dom('.oss-feature-cards-container>:nth-child(2)').hasClass('oss-feature-card--shadow-lg'); + assert + .dom('.oss-feature-cards-container>:nth-child(2)') + .hasClass('oss-feature-card--color-violet') + .hasClass('oss-feature-card--shadow-lg'); - assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--color-yellow'); - assert.dom('.oss-feature-cards-container>:nth-child(3)').hasClass('oss-feature-card--shadow-sm'); + assert + .dom('.oss-feature-cards-container>:nth-child(3)') + .hasClass('oss-feature-card--color-yellow') + .hasClass('oss-feature-card--shadow-sm'); }); module('Error management', () => { From f2a61314724a7176fc7b351302a4b021ee72c0fc Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Tue, 28 Apr 2026 15:33:26 +0200 Subject: [PATCH 09/11] fix: PR comments Co-authored-by: Copilot --- addon/components/o-s-s/feature-card.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/components/o-s-s/feature-card.hbs b/addon/components/o-s-s/feature-card.hbs index a2728c002..6e0ed40af 100644 --- a/addon/components/o-s-s/feature-card.hbs +++ b/addon/components/o-s-s/feature-card.hbs @@ -1,6 +1,6 @@
-

{{@title}}

+ {{@title}}

{{@description}}

From e17b8e90ac81459f4a39c8b9743837255b1eae3f Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Tue, 28 Apr 2026 15:35:36 +0200 Subject: [PATCH 10/11] chore: remove unused CSS --- app/styles/molecules/feature-card.less | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/styles/molecules/feature-card.less b/app/styles/molecules/feature-card.less index 62724ddea..7f79ae485 100644 --- a/app/styles/molecules/feature-card.less +++ b/app/styles/molecules/feature-card.less @@ -39,14 +39,6 @@ gap: var(--spacing-px-3); } - &__title { - font-size: var(--font-size-md); - font-weight: var(--font-weight-semibold); - color: var(--color-gray-900); - text-wrap: pretty; - margin: 0; - } - &__description { --max-number-of-lines: 4; From 550b2544586ca7195e152a8afd7b93e35ee4126a Mon Sep 17 00:00:00 2001 From: Edouard Misset Date: Tue, 28 Apr 2026 16:13:30 +0200 Subject: [PATCH 11/11] test: fix test Co-authored-by: Copilot --- tests/integration/components/o-s-s/feature-card-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/components/o-s-s/feature-card-test.ts b/tests/integration/components/o-s-s/feature-card-test.ts index e96464683..834282065 100644 --- a/tests/integration/components/o-s-s/feature-card-test.ts +++ b/tests/integration/components/o-s-s/feature-card-test.ts @@ -25,7 +25,7 @@ module('Integration | Component | o-s-s/feature-card', function (hooks) { ); assert.dom('.oss-feature-card').exists(); - assert.dom('.oss-feature-card__title').hasText(this.title.toString()); + assert.dom('.oss-feature-card>:first-child>:first-child').hasText(this.title.toString()); }); test('it renders the description', async function (assert) {