diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 4f337878cc..89f87cbe4c 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -115,6 +115,20 @@ export namespace Components { */ "homeUrl": string; } + interface PostCard { + /** + * Image position + */ + "imgPosition"?: 'top' | 'bottom'; + /** + * Image source + */ + "imgSrc"?: string; + /** + * Variant of the card + */ + "variant"?: 'card' | 'card-product' | 'card-teaser'; + } /** * @class PostCardControl - representing a stencil component */ @@ -164,6 +178,12 @@ export namespace Components { */ "value": string; } + interface PostCardSection { + /** + * Palette to use on section + */ + "palette"?: 'alternate' | 'default' | 'accent' | 'brand'; + } interface PostClosebutton { } interface PostCollapsible { @@ -612,6 +632,12 @@ declare global { prototype: HTMLPostBreadcrumbsElement; new (): HTMLPostBreadcrumbsElement; }; + interface HTMLPostCardElement extends Components.PostCard, HTMLStencilElement { + } + var HTMLPostCardElement: { + prototype: HTMLPostCardElement; + new (): HTMLPostCardElement; + }; interface HTMLPostCardControlElementEventMap { "postInput": { state: boolean; value: string }; "postChange": { state: boolean; value: string }; @@ -633,6 +659,12 @@ declare global { prototype: HTMLPostCardControlElement; new (): HTMLPostCardControlElement; }; + interface HTMLPostCardSectionElement extends Components.PostCardSection, HTMLStencilElement { + } + var HTMLPostCardSectionElement: { + prototype: HTMLPostCardSectionElement; + new (): HTMLPostCardSectionElement; + }; interface HTMLPostClosebuttonElement extends Components.PostClosebutton, HTMLStencilElement { } var HTMLPostClosebuttonElement: { @@ -902,7 +934,9 @@ declare global { "post-banner": HTMLPostBannerElement; "post-breadcrumb-item": HTMLPostBreadcrumbItemElement; "post-breadcrumbs": HTMLPostBreadcrumbsElement; + "post-card": HTMLPostCardElement; "post-card-control": HTMLPostCardControlElement; + "post-card-section": HTMLPostCardSectionElement; "post-closebutton": HTMLPostClosebuttonElement; "post-collapsible": HTMLPostCollapsibleElement; "post-collapsible-trigger": HTMLPostCollapsibleTriggerElement; @@ -1017,6 +1051,20 @@ declare namespace LocalJSX { */ "homeUrl"?: string; } + interface PostCard { + /** + * Image position + */ + "imgPosition"?: 'top' | 'bottom'; + /** + * Image source + */ + "imgSrc"?: string; + /** + * Variant of the card + */ + "variant"?: 'card' | 'card-product' | 'card-teaser'; + } /** * @class PostCardControl - representing a stencil component */ @@ -1066,6 +1114,12 @@ declare namespace LocalJSX { */ "value"?: string; } + interface PostCardSection { + /** + * Palette to use on section + */ + "palette"?: 'alternate' | 'default' | 'accent' | 'brand'; + } interface PostClosebutton { } interface PostCollapsible { @@ -1373,7 +1427,9 @@ declare namespace LocalJSX { "post-banner": PostBanner; "post-breadcrumb-item": PostBreadcrumbItem; "post-breadcrumbs": PostBreadcrumbs; + "post-card": PostCard; "post-card-control": PostCardControl; + "post-card-section": PostCardSection; "post-closebutton": PostClosebutton; "post-collapsible": PostCollapsible; "post-collapsible-trigger": PostCollapsibleTrigger; @@ -1415,10 +1471,12 @@ declare module "@stencil/core" { "post-banner": LocalJSX.PostBanner & JSXBase.HTMLAttributes; "post-breadcrumb-item": LocalJSX.PostBreadcrumbItem & JSXBase.HTMLAttributes; "post-breadcrumbs": LocalJSX.PostBreadcrumbs & JSXBase.HTMLAttributes; + "post-card": LocalJSX.PostCard & JSXBase.HTMLAttributes; /** * @class PostCardControl - representing a stencil component */ "post-card-control": LocalJSX.PostCardControl & JSXBase.HTMLAttributes; + "post-card-section": LocalJSX.PostCardSection & JSXBase.HTMLAttributes; "post-closebutton": LocalJSX.PostClosebutton & JSXBase.HTMLAttributes; "post-collapsible": LocalJSX.PostCollapsible & JSXBase.HTMLAttributes; "post-collapsible-trigger": LocalJSX.PostCollapsibleTrigger & JSXBase.HTMLAttributes; diff --git a/packages/components/src/components/post-card-section/post-card-section.scss b/packages/components/src/components/post-card-section/post-card-section.scss new file mode 100644 index 0000000000..0e2948b08a --- /dev/null +++ b/packages/components/src/components/post-card-section/post-card-section.scss @@ -0,0 +1,5 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} diff --git a/packages/components/src/components/post-card-section/post-card-section.tsx b/packages/components/src/components/post-card-section/post-card-section.tsx new file mode 100644 index 0000000000..6a6b99413b --- /dev/null +++ b/packages/components/src/components/post-card-section/post-card-section.tsx @@ -0,0 +1,45 @@ +import { Component, Element, h, Host, Prop, State } from '@stencil/core'; +import { version } from '@root/package.json'; + +/** + * @slot default - Slot for the section + */ +@Component({ + tag: 'post-card-section', + styleUrl: 'post-card-section.scss', + shadow: false, +}) +export class PostCardSection { + @Element() host: HTMLPostCardSectionElement; + + @State() hasOneInteractiveElement: boolean; + + /** + * Palette to use on section + */ + @Prop() palette?: 'alternate' | 'default' | 'accent' | 'brand' = 'default'; + + private checkIfInteractive() { + const interactiveElements: NodeListOf = + this.host.querySelectorAll('a, button'); + this.hasOneInteractiveElement = interactiveElements.length === 1; + console.log(interactiveElements, interactiveElements.length, this.hasOneInteractiveElement); + } + + componentWillRender() { + this.checkIfInteractive(); + } + + render() { + return ( + + {this.hasOneInteractiveElement && ( + + + + )} + {!this.hasOneInteractiveElement && } + + ); + } +} diff --git a/packages/components/src/components/post-card-section/readme.md b/packages/components/src/components/post-card-section/readme.md new file mode 100644 index 0000000000..ce8dc66fed --- /dev/null +++ b/packages/components/src/components/post-card-section/readme.md @@ -0,0 +1,35 @@ +# post-card + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| --------- | --------- | ------------------------- | ------------------------------------------------- | ----------- | +| `palette` | `palette` | Palette to use on section | `"accent" \| "alternate" \| "brand" \| "default"` | `'default'` | + + +## Slots + +| Slot | Description | +| ----------- | -------------------- | +| `"default"` | Slot for the section | + + +## Dependencies + +### Depends on + +- [post-linkarea](../post-linkarea) + +### Graph +```mermaid +graph TD; + post-card-section --> post-linkarea + style post-card-section fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-card/post-card.scss b/packages/components/src/components/post-card/post-card.scss new file mode 100644 index 0000000000..0e2948b08a --- /dev/null +++ b/packages/components/src/components/post-card/post-card.scss @@ -0,0 +1,5 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} diff --git a/packages/components/src/components/post-card/post-card.tsx b/packages/components/src/components/post-card/post-card.tsx new file mode 100644 index 0000000000..f1c0122296 --- /dev/null +++ b/packages/components/src/components/post-card/post-card.tsx @@ -0,0 +1,117 @@ +import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; +import { version } from '@root/package.json'; +import { checkNonEmpty } from '@/utils'; + +/** + * @slot default - Slot for the body of the card + * @slot header - Slot for the card header + * @slot footer - Slot for the card footer + */ +@Component({ + tag: 'post-card', + styleUrl: 'post-card.scss', + shadow: false, +}) +export class PostCard { + @Element() host: HTMLPostCardElement; + + @State() hasHeader: boolean; + @State() hasFooter: boolean; + @State() hasOneInteractiveElement: boolean; + + /** + * Image source + */ + @Prop() imgSrc?: string; + + /** + * Variant of the card + */ + @Prop() variant?: 'card' | 'card-product' | 'card-teaser' = 'card'; + + /** + * Image position + */ + @Prop() imgPosition?: 'top' | 'bottom' = 'top'; + + @Watch('imgSrc') + validateImage() { + if (this.variant === 'card-teaser') { + checkNonEmpty(this, 'imgSrc', 'A card teaser requires an image.'); + } + } + + /** + * Class to be added to the card + */ + private variantClass = 'card'; + + private checkIfInteractive() { + const interactiveElements: NodeListOf = + this.host.querySelectorAll('a, button'); + this.hasOneInteractiveElement = interactiveElements.length === 1; + console.log(interactiveElements, interactiveElements.length, this.hasOneInteractiveElement); + } + + componentWillRender() { + this.hasHeader = this.host.querySelectorAll('[slot=header]').length > 0; + this.hasFooter = this.host.querySelectorAll('[slot=footer]').length > 0; + + switch (this.variant) { + case 'card': + this.variantClass = 'card'; + break; + case 'card-product': + this.variantClass = 'card product-card'; + break; + case 'card-teaser': + this.variantClass = 'card teaser-card'; + break; + } + + if (this.variant === 'card-product') { + this.hasOneInteractiveElement = false; + } else { + this.checkIfInteractive(); + } + } + + private renderCardContent() { + return ( +
+ {this.imgSrc && this.imgPosition === 'top' && ( + + )} + {this.hasHeader && ( +
+ +
+ )} + {this.variant === 'card-product' && } + {this.variant !== 'card-product' && ( +
+ +
+ )} + + {this.hasFooter && ( + + )} + {this.imgSrc && this.imgPosition === 'bottom' && ( + + )} +
+ ); + } + + render() { + return ( + + {this.hasOneInteractiveElement && {this.renderCardContent()}} + {!this.hasOneInteractiveElement && this.renderCardContent()} + + ); + } +} diff --git a/packages/components/src/components/post-card/readme.md b/packages/components/src/components/post-card/readme.md new file mode 100644 index 0000000000..dfe7b2d3f4 --- /dev/null +++ b/packages/components/src/components/post-card/readme.md @@ -0,0 +1,39 @@ +# post-card + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------- | -------------- | ------------------- | ------------------------------------------- | ----------- | +| `imgPosition` | `img-position` | Image position | `"bottom" \| "top"` | `'top'` | +| `imgSrc` | `img-src` | Image source | `string` | `undefined` | +| `variant` | `variant` | Variant of the card | `"card" \| "card-product" \| "card-teaser"` | `'card'` | + + +## Slots + +| Slot | Description | +| ----------- | ----------------------------- | +| `"default"` | Slot for the body of the card | +| `"footer"` | Slot for the card footer | +| `"header"` | Slot for the card header | + + +## Dependencies + +### Depends on + +- [post-linkarea](../post-linkarea) + +### Graph +```mermaid +graph TD; + post-card --> post-linkarea + style post-card fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-linkarea/readme.md b/packages/components/src/components/post-linkarea/readme.md index 78ec368a82..5235c1400c 100644 --- a/packages/components/src/components/post-linkarea/readme.md +++ b/packages/components/src/components/post-linkarea/readme.md @@ -5,6 +5,21 @@ +## Dependencies + +### Used by + + - [post-card](../post-card) + - [post-card-section](../post-card-section) + +### Graph +```mermaid +graph TD; + post-card --> post-linkarea + post-card-section --> post-linkarea + style post-linkarea fill:#f9f,stroke:#333,stroke-width:4px +``` + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 59d27159e6..952ceec13a 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -8,7 +8,9 @@ export { PostBackToTop } from './components/post-back-to-top/post-back-to-top'; export { PostBanner } from './components/post-banner/post-banner'; export { PostBreadcrumbs } from './components/post-breadcrumbs/post-breadcrumbs'; export { PostBreadcrumbItem } from './components/post-breadcrumb-item/post-breadcrumb-item'; +export { PostCard } from './components/post-card/post-card'; export { PostCardControl } from './components/post-card-control/post-card-control'; +export { PostCardSection } from './components/post-card-section/post-card-section'; export { PostClosebutton } from './components/post-closebutton/post-closebutton'; export { PostCollapsible } from './components/post-collapsible/post-collapsible'; export { PostCollapsibleTrigger } from './components/post-collapsible-trigger/post-collapsible-trigger'; diff --git a/packages/documentation/src/stories/components/card/card-webcomponent.snapshot.stories.ts b/packages/documentation/src/stories/components/card/card-webcomponent.snapshot.stories.ts new file mode 100644 index 0000000000..a143a86435 --- /dev/null +++ b/packages/documentation/src/stories/components/card/card-webcomponent.snapshot.stories.ts @@ -0,0 +1,199 @@ +import type { StoryObj } from '@storybook/web-components'; +import meta from './card.stories'; +import { html } from 'lit'; +import { bombArgs } from '@/utils'; + +const { id, ...metaWithoutId } = meta; + +export default { + ...metaWithoutId, + title: 'Snapshots', +}; + +type Story = StoryObj; + +export const CardWebComponent: Story = { + render: () => { + // Define basic content template variants + const cardVariants = [ + // Layout related combinations + ...bombArgs({ + imgSrc: ['https://picsum.photos/id/20/300/200', null], + imgPosition: ['top', 'bottom'], + showHeader: [false, true], + showBody: [false, true], + showFooter: [false, true], + interactiveContent: ['button', 'links'], + }), + ] + // Has to show anything + .filter(args => args.showHeader || args.showBody || args.showFooter || args.showImage) + // No single footer + .filter(args => !(args.showFooter && !args.showBody && !args.showHeader)) + // No single header + .filter(args => !(args.showHeader && !args.showBody && !args.showFooter)) + // No header and footer without content in between + .filter(args => !(args.showHeader && args.showFooter && args.showBody === false)) + // Ignore header position if showHeader is false + .filter(args => !(!args.imgSrc && args.imgPosition === 'bottom')) + // Ignore header position if only img is shown + .filter( + args => + !( + args.showImage && + !args.showBody && + !args.showHeader && + !args.showFooter && + args.imagePosition === 'bottom' + ), + ) + // Map default template variants + .map(args => { + const cardBody = html` +
Title
+
Subtitle
+

This is my text

+ ${args.interactiveContent === 'button' + ? html`` + : ''} + ${args.interactiveContent === 'links' + ? html`Link oneLink two` + : ''} + `; + return html` +
+ + ${args.showHeader ? html`
Card header
` : ''} + ${args.showBody ? cardBody : ''} + ${args.showFooter ? html`
Card footer
` : ''} +
+
+ `; + }); + + const cardProductVariants = [ + // Layout related combinations + ...bombArgs({ + interactiveContent: ['button', 'link'], + size: ['small', 'medium', 'large'], + }), + ] + // Map default template variants + .map( + args => html` +
+ + +
+
+

Affordable

+

Sample Product

+

+ With SAMPLE PRODUCT, your letters arrive at their destination cost-effectively + and reliably. +

+
+
+ ${args.interactiveContent === 'link' + ? html` + + Learn more + ` + : ''} +
+
+
+ ${args.size === 'medium' || args.size === 'large' + ? html` + +
Sample Product
+

140 x 90 mm bis B5 (250 x 176 mm)

+
+
bis 500 g
+
1.20
+ +
bis 50 g
+
2.20
+
+
+ ` + : ''} + ${args.size === 'large' + ? html` + + + + + + ` + : ''} +
+
+ `, + ); + + const cardTeaserVariants = [ + // Layout related combinations + ...bombArgs({ + interactiveContent: ['button', 'link'], + }), + ] + // Map default template variants + .map( + args => html` +
+ +

Product

+

This is the product summary.

+ + ${args.interactiveContent === 'button' + ? html`` + : ''} + ${args.interactiveContent === 'link' + ? html`Link one` + : ''} +
+
+ `, + ); + + const palettes = ['palette-alternate', 'palette-default']; + + return html` +

Card web component POC

+

Card product

+
+
${cardProductVariants}
+
+

Card

+ ${palettes.map( + palette => html` +
+
+
${cardVariants}
+
+
+ `, + )} +

Card teaser

+ ${palettes.map( + palette => html` +
+
+
${cardTeaserVariants}
+
+
+ `, + )} + `; + }, +}; diff --git a/packages/documentation/src/stories/components/card/card.docs.mdx b/packages/documentation/src/stories/components/card/card.docs.mdx index d4641f45c0..155639e7ec 100644 --- a/packages/documentation/src/stories/components/card/card.docs.mdx +++ b/packages/documentation/src/stories/components/card/card.docs.mdx @@ -7,7 +7,8 @@ import StylesPackageImport from '@/shared/styles-package-import.mdx';
# Card - + +
@@ -68,3 +69,11 @@ Create lists of content in a card by simply using a flush list group in place of
+ +## Card Web Component POC + +Here are examples of what the card component would look like if it was a web component. + + + + diff --git a/packages/documentation/src/stories/components/card/card.stories.ts b/packages/documentation/src/stories/components/card/card.stories.ts index 2cab6edbfd..1d22175bf6 100644 --- a/packages/documentation/src/stories/components/card/card.stories.ts +++ b/packages/documentation/src/stories/components/card/card.stories.ts @@ -20,6 +20,7 @@ const meta: MetaComponent = { }, }, args: { + isWebComponent: false, showImage: true, imagePosition: 'top', showHeader: false, @@ -223,9 +224,7 @@ function gridContainer(story: StoryFn, context: StoryContext) { // RENDERER function getCardLinks() { return html` - ${['Link Text', 'More Link'].map( - label => html` ${label} `, - )} + ${['Link Text', 'More Link'].map(label => html` ${label} `)} `; } @@ -243,7 +242,9 @@ function getCardBody({ customBody, content, action, showTitle, showSubtitle }: A return html`
${showTitle ? html`
Card Title
` : nothing} - ${showSubtitle ? html`
Card Subtitle
` : nothing} + ${showSubtitle + ? html`
Card Subtitle
` + : nothing}

${content}

${choose( action, @@ -426,3 +427,78 @@ export const BackgroundImage: Story = {
`, }, }; + +export const CardWebComponent: Story = { + decorators: [gridContainer], + render: () => html` + +
Card header
+
Title
+
Subtitle
+

This is my text

+ +
Card footer
+
+ `, +}; + +export const CardProductWebComponent: Story = { + decorators: [gridContainer], + render: () => html` + + +
+
+

Affordable

+

Sample Product

+

+ With SAMPLE PRODUCT, your letters arrive at their destination cost-effectively and + reliably. +

+
+ +
+
+ +
Sample Product
+

140 x 90 mm bis B5 (250 x 176 mm)

+
+
bis 500 g
+
1.20
+ +
bis 50 g
+
2.20
+
+
+ + + + + +
+ `, +}; + +export const CardTeaserWebComponent: Story = { + decorators: [gridContainer], + render: () => html` + +

Product

+

This is the product summary.

+ +
+ `, +}; diff --git a/packages/styles/src/components/card.scss b/packages/styles/src/components/card.scss index 3e834b4c30..8a97cd9c31 100644 --- a/packages/styles/src/components/card.scss +++ b/packages/styles/src/components/card.scss @@ -68,6 +68,16 @@ post-linkarea:has(> .card) { &.product-card { box-shadow: none; + > *:first-child { + border-top-left-radius: tokens.get('card-border-radius'); + border-top-right-radius: tokens.get('card-border-radius'); + } + + > *:last-child { + border-bottom-left-radius: tokens.get('card-border-radius'); + border-bottom-right-radius: tokens.get('card-border-radius'); + } + &:hover, &:focus { z-index: 1;