diff --git a/.storybook/preview.js b/.storybook/preview.js index 8fa58500..a7612f79 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -13,6 +13,7 @@ const preview = { color: /(background|color)$/i, date: /Date$/i, }, + expanded: true, }, docs: { toc: true, // Autogenerate table of contents. diff --git a/src/components/usa-card-group/index.js b/src/components/usa-card-group/index.js new file mode 100644 index 00000000..fc9bb62b --- /dev/null +++ b/src/components/usa-card-group/index.js @@ -0,0 +1,24 @@ +import { LitElement, html, unsafeCSS } from "lit"; +import usaCardStyle from "@uswds/uswds/scss/usa-card?inline"; + +export class UsaCardGroup extends LitElement { + static styles = [ + unsafeCSS(usaCardStyle), + ] + + constructor() { + super(); + + this.cards = [...this.children]; + } + + render() { + return html` + + ` + } +} + +window.customElements.define("usa-card-group", UsaCardGroup); diff --git a/src/components/usa-card/card.stories.js b/src/components/usa-card/card.stories.js new file mode 100644 index 00000000..ca967d33 --- /dev/null +++ b/src/components/usa-card/card.stories.js @@ -0,0 +1,429 @@ +import "./index"; +import "../usa-card-group/index"; + +import { html, nothing} from "lit"; + +export default { + title: "Components/Card", + component: "usa-card", + args: { + title: "", + media: "https://designsystem.digital.gov/img/introducing-uswds-2-0/built-to-grow--alt.jpg", + content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Facilis earum tenetur quo cupiditate, eaque qui officia recusandae.", + buttonText: "Visit Florida Keys", + headerFirst: false, + layout: "default" + }, + argTypes: { + buttonText: { + description: "Card button text." + }, + content: { + description: "Card body content." + }, + headerFirst: { + description: "Place the header above above card media." + }, + media: { + description: "Img src for component preview." + }, + layout: { + control: { type: 'radio' }, + options: ['default', 'flag', 'flag-alt'], + description: "Media card layout at wide widths." + }, + title: { + description: "Card header text." + } + }, + render: ({ title, content, buttonText }) => { + return html` + +
+

${title || "Card"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ ` + } +} + +export const Default = { + argTypes: { + media: { + table: { + disable: true + } + }, + headerFirst: { + table: { + disable: true + } + }, + layout: { + table: { + disable: true + } + }, + }, + parameters: { + docs: { + description: { + story: 'A default card with a title, text content, and a button.' + } + } + } +}; + +export const CardWithMedia = { + argTypes: { + headerFirst: { + table: { + disable: true + } + }, + layout: { + table: { + disable: true + } + } + }, + parameters: { + docs: { + description: { + story: 'A card variant featuring media.' + } + } + }, + render: ({ title, media, content, buttonText, headerFirst }) => { + return html` + + Placeholder image +
+

${title || "Card w/ Media"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ ` + } +} + +export const MediaWithHeaderFirst = { + args: { + headerFirst: true + }, + argTypes: { + layout: { + table: { + disable: true + } + } + }, + parameters: { + docs: { + description: { + story: 'A card variant featuring media, displaying the header first.' + } + } + }, + render: ({ title, media, content, buttonText, headerFirst }) => { + return html` + + Placeholder image +
+

${title || "Media with Header first"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ ` + } +} + +export const InsetMedia = { + argTypes: { + headerFirst: { + table: { + disable: true + } + }, + layout: { + table: { + disable: true + } + } + }, + parameters: { + docs: { + description: { + story: 'Indents the media element so it doesn’t extend to the edge of the card.' + } + } + }, + render: ({ title, media, content, buttonText }) => { + return html` + + Placeholder image +
+

${title || "Inset Media"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ ` + } +} + +export const ExdentMedia = { + argTypes: { + headerFirst: { + table: { + disable: true + } + }, + layout: { + table: { + disable: true + } + } + }, + parameters: { + docs: { + description: { + story: 'Extends the media element out over the card border.' + } + } + }, + render: ({ title, media, content, buttonText }) => { + return html` + + Placeholder image +
+

${title || "Exdent media"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ ` + } +} + +export const Flag = { + args: { + layout: "flag", + }, + parameters: { + docs: { + description: { + story: 'Display in a horizontal (“flag”) orientation at a specified width ($theme-card-flag-min-width).' + } + } + }, + render: ({ title, media, content, buttonText, headerFirst, layout }) => { + return html` + + Placeholder image +
+

${title || "Default flag"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ ` + } +} + +export const FlagMediaRightInset = { + args: { + layout: "flag-alt" + }, + parameters: { + docs: { + description: { + story: 'Display in a horizontal (“flag”) orientation with media inset and on the right side.' + } + } + }, + render: ({ title, media, content, buttonText, layout, headerFirst }) => { + return html` + + Placeholder image +
+

${title || "Flag media right inset"} +

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ ` + } +} + +export const CardGroup = { + argTypes: { + headerFirst: { + table: { + disable: true + } + }, + layout: { + table: { + disable: true + } + } + }, + render: ({ title, media, content, buttonText }) => { + return html` + + +
+

${title || "Card"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ + + Placeholder image +
+

${ title || "Card w/ Media"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ + + Placeholder image +
+

${ title || "Media with Header first"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ + + Placeholder image +
+

${ title || "Inset Media"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ + + Placeholder image +
+

${ title || "Exdent media"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ + + Placeholder image +
+

${title || "Default flag"}

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+ + + Placeholder image +
+

${title || "Flag media right inset"} +

+
+
+

+ ${content} +

+
+
+ ${buttonText} +
+
+
+ ` + } +} diff --git a/src/components/usa-card/index.js b/src/components/usa-card/index.js new file mode 100644 index 00000000..0dd97391 --- /dev/null +++ b/src/components/usa-card/index.js @@ -0,0 +1,117 @@ +import { LitElement, html} from "lit"; +import { classMap } from "lit/directives/class-map.js"; +import { cardStyles } from "./usa-card.scss"; + +export class UsaCard extends LitElement { + static styles = [cardStyles]; + + static properties = { + headerFirst: { + attribute: 'header-first', + type: Boolean + }, + inset: { type: Boolean }, + exdent: { type: Boolean}, + layout: { type: String }, + } + + constructor() { + super(); + + this.cardGroup = this.parentElement; + this.header = this.querySelector("[slot='card-header']"); + this.headerContent = [...this.header.children]; + this.media = this.querySelector("[slot='card-media']") + this.body = this.querySelector("[slot='card-body']"); + this.bodyContent = [...this.body.children]; + this.footer = this.querySelector("[slot='card-footer']"); + this.footerContent = [...this.footer.children]; + this.slottedChildren = [...this.children]; + this.slots = this.slottedChildren.map((child) => { + return child.getAttribute("slot") + }) + } + + // Render header + headerTemplate() { + const classes = { + "usa-card__header": true, + "usa-card__header--exdent": this.exdent || this.header.hasAttribute("exdent") + } + + return html` +
+ ${this.headerContent} +
+ `; + } + + // Render media + mediaTemplate() { + if(!this.media) { + return; + } + + const classes = { + "usa-card__media": true, + "usa-card__media--inset": this.media.hasAttribute("inset") && !this.media.hasAttribute("exdent"), + "usa-card__media--exdent": this.exdent || this.media.hasAttribute("exdent") && !this.media.hasAttribute("indent"), + } + + return html` +
+
+ ${this.media} +
+
+ ` + } + + // Render body + bodyTemplate() { + const classes = { + "usa-card__body": true, + "usa-card__body--exdent": this.exdent || this.body.hasAttribute("exdent") + }; + + return html`
${this.bodyContent}
` + } + + // Render footer + footerTemplate() { + const classes = { + "usa-card__footer": true, + "usa-card__footer--exdent": this.exdent || this.footer.hasAttribute("exdent") + } + + return html`
${this.footerContent}
` + } + + // Render card + cardTemplate() { + const classes = { + "usa-card": true, + "usa-card--header-first": this.headerFirst, + "usa-card--flag": this.layout === "flag" || this.layout === "flag-alt", + "usa-card--media-right": this.layout == "flag-alt", + } + return html` +
+
+ ${this.headerTemplate()} + ${this.mediaTemplate()} + ${this.bodyTemplate()} + ${this.footerTemplate()} +
+
+ ` + } + + render() { + return html` + ${this.cardTemplate()} + ` + } +} + +window.customElements.define("usa-card", UsaCard); diff --git a/src/components/usa-card/usa-card.scss.js b/src/components/usa-card/usa-card.scss.js new file mode 100644 index 00000000..f107daec --- /dev/null +++ b/src/components/usa-card/usa-card.scss.js @@ -0,0 +1,23 @@ +import uswdsCoreStyle from "@uswds/uswds/scss/uswds-core?inline"; +import usaCardStyle from "@uswds/uswds/scss/usa-card?inline"; +import usaButtonStyle from "@uswds/uswds/scss/usa-button?inline"; + +import { unsafeCSS, css } from "lit"; + +export const cardStyles = [ + unsafeCSS(uswdsCoreStyle), + unsafeCSS(usaCardStyle), + unsafeCSS(usaButtonStyle), + css` + * { + box-sizing: border-box; + } + + [slot="card-heading"] { + font-family: "Merriweather Web", Georgia, Cambria, "Times New Roman", Times, serif; + font-size: 1.34rem; + line-height: 1.2; + margin: 0px; + } + ` +]