Skip to content
Merged
8 changes: 8 additions & 0 deletions addon/components/o-s-s/feature-card.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class={{this.computedClasses}} ...attributes>
<div class="oss-feature-card__content">
<span class="font-size-md font-weight-semibold">{{@title}}</span>
<p class="oss-feature-card__description text-ellipsis">{{@description}}</p>
</div>

<img class="oss-feature-card__illustration" src={{@image.src}} alt={{this.imageAlt}} />
</div>
90 changes: 90 additions & 0 deletions addon/components/o-s-s/feature-card.stories.js
Original file line number Diff line number Diff line change
@@ -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`
<OSS::FeatureCard
@title={{this.title}}
@description={{this.description}}
@image={{this.image}}
@colorVariant={{this.colorVariant}}
@shadowVariant={{this.shadowVariant}}
/>
`,
context: args
});

export const Default = Template.bind({});
Default.args = defaultArgs;
77 changes: 77 additions & 0 deletions addon/components/o-s-s/feature-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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];
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 | ReturnType<IntlService['t']>;
description: string | ReturnType<IntlService['t']>;
image: OSSFeatureCardImage;
colorVariant?: OSSFeatureCardColorVariant;
shadowVariant?: OSSFeatureCardShadowVariant;
};

export default class OSSFeatureCard extends Component<OSSFeatureCardArgs> {
constructor(owner: unknown, args: OSSFeatureCardArgs) {
super(owner, args);

assert(
'[OSS::FeatureCard] The @title parameter is mandatory',
(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.toString().trim().length > 0
);
Comment thread
edouardmisset marked this conversation as resolved.
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;
}
Comment on lines +58 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happened if the set a wrong color or shadow value?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

From my understanding, if that happens we fail one of the asserts above (L. 42, L. 48). It seems to be the way other components do it (see empty-state or dialog components).


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 ?? '';
}
}
12 changes: 12 additions & 0 deletions addon/components/o-s-s/feature-cards-container.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="oss-feature-cards-container" ...attributes>
{{#each this.cardsWithComputedVariants as |card|}}
<OSS::FeatureCard
@title={{card.title}}
@description={{card.description}}
@image={{card.image}}
@colorVariant={{card.colorVariant}}
@shadowVariant={{card.shadowVariant}}
class="oss-feature-cards-container__item"
/>
{{/each}}
</div>
98 changes: 98 additions & 0 deletions addon/components/o-s-s/feature-cards-container.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { hbs } from 'ember-cli-htmlbars';

export default {
title: 'Components/OSS::FeatureCardsContainer',
component: 'feature-cards-container',
argTypes: {
cards: {
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 }; colorVariant?: "blue"|"violet"|"yellow"; shadowVariant?: "sm"|"lg" }>'
},
defaultValue: { summary: 'undefined' }
},
control: { type: 'object' },
type: { required: true }
}
},
parameters: {
docs: {
description: {
component:
'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.'
}
}
}
};

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,
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,
colorVariant: 'violet',
shadowVariant: 'lg'
},
{
title: 'Campaign performance tracking',
description: 'Track contribution stages, orders and ROI, then sync results to your CRM.',
image: defaultImage,
colorVariant: 'yellow',
shadowVariant: 'sm'
}
]
};

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)
};

const Template = (args) => ({
template: hbs`
<div style="display: flex; justify-content: center; overflow-x: auto; min-inline-size: 950px;">
<OSS::FeatureCardsContainer @cards={{this.cards}} />
</div>
`,
context: 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;

export const OneCard = Template.bind({});
OneCard.args = oneCardArgs;
56 changes: 56 additions & 0 deletions addon/components/o-s-s/feature-cards-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { assert } from '@ember/debug';
import Component from '@glimmer/component';

import type { OSSFeatureCardArgs, OSSFeatureCardColorVariant, OSSFeatureCardShadowVariant } from './feature-card';

type OSSFeatureCardsContainerArgs = {
cards: OSSFeatureCardArgs[];
};

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<OSSFeatureCardsContainerArgs> {
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 between 1 and 3 cards',
args.cards.length >= 1 && args.cards.length <= 3
);
Comment thread
Miexil marked this conversation as resolved.
}

get cardsWithComputedVariants(): OSSFeatureCardArgs[] {
const cards = this.args.cards ?? [];

return cards.map((card, index) => {
const cardCount = cards.length;
const colorVariant = card.colorVariant ?? getDefaultCardColorVariant(cardCount, index);
const shadowVariant = card.shadowVariant ?? getDefaultCardShadowVariant(cardCount, index);

return {
...card,
colorVariant,
shadowVariant
};
});
}
}
1 change: 1 addition & 0 deletions app/components/o-s-s/feature-card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@upfluence/oss-components/components/o-s-s/feature-card';
1 change: 1 addition & 0 deletions app/components/o-s-s/feature-cards-container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@upfluence/oss-components/components/o-s-s/feature-cards-container';
Loading
Loading