diff --git a/src/stable/components/ProfileCard/ProfileCard.css b/src/stable/components/ProfileCard/ProfileCard.css new file mode 100644 index 00000000..12ce7562 --- /dev/null +++ b/src/stable/components/ProfileCard/ProfileCard.css @@ -0,0 +1,59 @@ +.profile-card { + display: flex; + align-items: center; + width: 100%; + max-width: 100%; + gap: 1.25em; + text-decoration: none; +} + +a.profile-card { + color: inherit; + text-decoration: none; +} +a.profile-card:visited, +a.profile-card:active, +a.profile-card:hover { + color: inherit; + text-decoration: none; +} + +.profile-image { + object-fit: cover; + width: 20%; + max-width: 120px; + height: auto; + border-radius: 50%; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.profile-details { + display: flex; + flex-direction: column; + flex-grow: 1; + min-width: 0; +} + +.name-container { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.chevron { + background-image: url('data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2716%27%20height=%2716%27%20fill=%27currentColor%27%20class=%27bi%20bi-chevron-right%27%20viewBox=%270%200%2016%2016%27%3E%3Cpath%20fill-rule=%27evenodd%27%20d=%27M4.646%201.646a.5.5%200%200%201%20.708%200l6%206a.5.5%200%200%201%200%20.708l-6%206a.5.5%200%200%201-.708-.708L10.293%208%204.646%202.354a.5.5%200%200%201%200-.708%27/%3E%3C/svg%3E'); + display: inline-block; + width: 16px; + height: 16px; + background-size: contain; + background-repeat: no-repeat; + transition: transform 0.2s ease; +} + +.profile-card:hover .chevron { + transform: translateX(4px); +} + +/*# sourceMappingURL=ProfileCard.css.map */ diff --git a/src/stable/components/ProfileCard/ProfileCard.css.map b/src/stable/components/ProfileCard/ProfileCard.css.map new file mode 100644 index 00000000..254052d3 --- /dev/null +++ b/src/stable/components/ProfileCard/ProfileCard.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["ProfileCard.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;EAGE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA,YACE;;;AAIJ;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE","file":"ProfileCard.css"} \ No newline at end of file diff --git a/src/stable/components/ProfileCard/ProfileCard.js b/src/stable/components/ProfileCard/ProfileCard.js new file mode 100644 index 00000000..4005835e --- /dev/null +++ b/src/stable/components/ProfileCard/ProfileCard.js @@ -0,0 +1,108 @@ +import styles from '!!raw-loader!./ProfileCard.css'; + +const template = document.createElement('template'); +template.innerHTML = ` + + + Profile Image +
+
+ + +
+ +
+
+`; + +class ProfileCard extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({ mode: 'open' }); + shadow.appendChild(template.content.cloneNode(true)); + + // Internal properties + this._href = this.getAttribute('href'); + this._src = this.getAttribute('src'); + this._alt = this.getAttribute('alt'); + this._target = this.getAttribute('target'); + this._rel = this.getAttribute('rel'); + } + + static get observedAttributes() { + return ['src', 'href', 'alt', 'target', 'rel']; + } + + get href() { + return this._href; + } + get src() { + return this._src; + } + get alt() { + return this._alt; + } + get target() { + return this._target; + } + get rel() { + return this._rel; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'src' && newValue !== oldValue) { + this._src = newValue; + this._updateImage(newValue); + } + + if (name === 'href' && newValue !== oldValue) { + this._href = newValue; + this._updateLink(newValue); + } + if (name === 'alt' && newValue !== oldValue) { + this._alt = newValue; + this._updateImage(); + } + if (name === 'target' && newValue !== oldValue) { + this._target = newValue; + this._updateLink(); + } + + if (name === 'rel' && newValue !== oldValue) { + this._rel = newValue; + this._updateLink(); + } + } + + connectedCallback() { + this._updateImage(this.src); + this._updateLink(this.href); + } + + _updateImage() { + const img = this.shadowRoot.querySelector('.profile-image'); + if (img) { + img.src = this._src || ''; + img.alt = this._alt || 'Profile Image'; + } + } + + _updateLink() { + const card = this.shadowRoot.querySelector('.profile-card'); + if (card) { + if (this._href) { + card.href = this._href; + card.target = this._target || '_blank'; + card.rel = this._rel || 'noopener noreferrer'; + } else { + card.removeAttribute('href'); + card.removeAttribute('target'); + card.removeAttribute('rel'); + } + } + } +} + +export { ProfileCard as default }; diff --git a/src/stable/components/ProfileCard/ProfileCard.scss b/src/stable/components/ProfileCard/ProfileCard.scss new file mode 100644 index 00000000..8fbf6601 --- /dev/null +++ b/src/stable/components/ProfileCard/ProfileCard.scss @@ -0,0 +1,57 @@ +.profile-card { + display: flex; + align-items: center; + width: 100%; + max-width: 100%; + gap: 1.25rem; + text-decoration: none; +} + +a.profile-card { + color: inherit; + text-decoration: none; + + &:visited, + &:active, + &:hover { + color: inherit; + text-decoration: none; + } +} + +.profile-image { + object-fit: cover; + width: 20%; + max-width: 120px; + height: auto; + border-radius: 50%; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.profile-details { + display: flex; + flex-direction: column; + flex-grow: 1; + min-width: 0; +} +.name-container { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.chevron { + background-image: url('data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2716%27%20height=%2716%27%20fill=%27currentColor%27%20class=%27bi%20bi-chevron-right%27%20viewBox=%270%200%2016%2016%27%3E%3Cpath%20fill-rule=%27evenodd%27%20d=%27M4.646%201.646a.5.5%200%200%201%20.708%200l6%206a.5.5%200%200%201%200%20.708l-6%206a.5.5%200%200%201-.708-.708L10.293%208%204.646%202.354a.5.5%200%200%201%200-.708%27/%3E%3C/svg%3E'); + display: inline-block; + width: 16px; + height: 16px; + background-size: contain; + background-repeat: no-repeat; + transition: transform 0.2s ease; +} + +.profile-card:hover .chevron { + transform: translateX(4px); +} diff --git a/src/stable/components/ProfileCard/cod-profile-card.js b/src/stable/components/ProfileCard/cod-profile-card.js new file mode 100644 index 00000000..069f785e --- /dev/null +++ b/src/stable/components/ProfileCard/cod-profile-card.js @@ -0,0 +1,2 @@ +import ProfileCard from './ProfileCard'; +customElements.define('cod-profile-card', ProfileCard); diff --git a/src/stable/docs/ProfileCard.mdx b/src/stable/docs/ProfileCard.mdx new file mode 100644 index 00000000..5e76b082 --- /dev/null +++ b/src/stable/docs/ProfileCard.mdx @@ -0,0 +1,92 @@ +import { + Meta, + Canvas, + Title, + Subtitle, + Description, + Primary, + Controls, + Stories, + Story, + Source, +} from '@storybook/blocks'; +import * as ProfileCardStories from '../stories/profilecard.stories'; + +import '../../../.storybook/docs'; // Import all documentation components + + + +# Profile Card + +
+ Profile Cards are components used to display a user's profile information. +
+ +## Usage + + + + + +# Examples + +### Default + + + + + +## Slots + + + + +## HTML Attributes / JS Properties + +Supported HTML attributes. If the attribute is reflected in a JS property, that attribute will have a 'reflects' tag next to the name. + + + + +## Events + + + + +## Methods + + + + +## Custom CSS Properties + + + + +## CSS Parts + + + + +## Dependencies + + + +## Accessibility + +- **Images (`src`)**: When a profile image is included using the `src` attribute, it should be accompanied by an `alt` attribute describing the person (e.g., `alt="Photo of Jane Doe"`). This ensures screen readers can describe the image content. + +- **Links (`href`)**: When the card is made clickable with the `href` attribute, it becomes keyboard-accessible and can be navigated by screen readers. diff --git a/src/stable/stories/profilecard.stories.js b/src/stable/stories/profilecard.stories.js new file mode 100644 index 00000000..c845ed6a --- /dev/null +++ b/src/stable/stories/profilecard.stories.js @@ -0,0 +1,109 @@ +import { html } from 'lit-html'; +import '../components/ProfileCard/cod-profile-card'; +import { expect } from '@storybook/test'; + +export default { + tags: ['stable'], + title: 'Components/ProfileCard', + argTypes: { + src: { control: 'text', name: 'Image Source' }, + alt: { control: 'text', name: 'Alt Text' }, + name: { control: 'text', name: 'Name' }, + titlePrimary: { control: 'text', name: 'Primary Title' }, + titleSecondary: { control: 'text', name: 'Secondary Title' }, + href: { control: 'text', name: 'Profile Link' }, + }, +}; + +export const Default = (args) => html` + + + ${args.name} + ${args.titlePrimary} + ${args.titleSecondary} + +`; + +Default.args = { + src: 'https://placehold.co/400', + alt: 'Photo of Jane Doe', + name: 'Jane Doe', + titlePrimary: 'Frontend Engineer', + titleSecondary: 'Frontend Developer', + href: 'https://example.com', +}; + +export const Test = { + render: () => html` + + + Jane Doe + Frontend Engineer + Frontend Developer + + `, + + play: async ({ canvasElement }) => { + const profileCard = canvasElement.querySelector('cod-profile-card'); + const shadow = profileCard.shadowRoot; + + // ===== TEST 1: Image Presence Test ===== + const img = shadow.querySelector('.profile-image'); + expect(img).not.toBeNull(); + expect(img.tagName).toBe('IMG'); + expect(img.src).toContain('https://placehold.co/400'); + + // ===== TEST 2: Link Element Test ===== + const link = shadow.querySelector('.profile-card'); + expect(link).not.toBeNull(); + expect(link.tagName).toBe('A'); + + // Verify the href attribute + expect(link.hasAttribute('href')).toBe(true); + expect(link.getAttribute('href')).toBe(profileCard.getAttribute('href')); + + // Verify the target and rel attributes + expect(link.getAttribute('target')).toBe('_blank'); + expect(link.getAttribute('rel')).toBe('noopener noreferrer'); + + // ===== TEST 3: Image Alt Text Test ===== + const expectedAlt = profileCard.getAttribute('alt') || 'Profile Image'; + expect(img.alt).toBe(expectedAlt); + }, +};