Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added libs/labs/assets/placeholder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions libs/labs/grid-container/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"lib": {
"entryFile": "src/index.ts",
"styleIncludePaths": ["../src/sass"]
}
}
1 change: 1 addition & 0 deletions libs/labs/grid-container/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GridContainer } from './lib/grid-container';
8 changes: 8 additions & 0 deletions libs/labs/grid-container/src/lib/grid-container.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
:host {
--_ang-grid-container-item-min-width: var(--ang-grid-container-item-min-width, 17rem);

display: grid;
gap: 1.5rem;
grid-template-columns: repeat(auto-fit, minmax(var(--_ang-grid-container-item-min-width), 1fr));
justify-items: center;
}
19 changes: 19 additions & 0 deletions libs/labs/grid-container/src/lib/grid-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';

/**
* Layout component that allows for grid arrangement of child elements.
*/
@Component({
selector: 'ang-grid-container',
imports: [],
template: '<ng-content />',
styleUrl: './grid-container.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[style.--ang-grid-container-item-min-width]': 'itemMinWidth()',
},
})
export class GridContainer {
/** Minimum item width */
readonly itemMinWidth = input<string>();
}
6 changes: 6 additions & 0 deletions libs/labs/profile-card/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"lib": {
"entryFile": "src/index.ts",
"styleIncludePaths": ["../src/sass"]
}
}
1 change: 1 addition & 0 deletions libs/labs/profile-card/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/profile-card';
10 changes: 10 additions & 0 deletions libs/labs/profile-card/src/lib/profile-card.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div class="picture">
<img [attr.src]="pictureUrl()" [attr.alt]="`Profile picture of ${name()}`" />
</div>
<div class="content">
<div class="name">{{ name() }}</div>
<p class="description">{{ description() }}</p>
</div>
<div class="actions">
<ng-content />
</div>
74 changes: 74 additions & 0 deletions libs/labs/profile-card/src/lib/profile-card.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
@use '@angular/material' as mat;
@use 'internal/token-utils';
@use 'tokens' as *;

:host {
display: flex;
flex-direction: column;
width: 17rem;
color: token-utils.slot(card-color, $config);

.picture {
width: 11.25rem;
height: 13.375rem;
clip-path: polygon(0% 0%, 100% 0%, 100% 90%, 90% 100%, 0% 100%);
border-left: 0.75rem solid #ff0043;
margin-bottom: 2rem;

img {
height: 100%;
width: 100%;
object-fit: cover;
}
}

.content {
margin-bottom: 1rem;
flex-grow: 1;

.name {
font-family: token-utils.slot(name-font, $config);
font-size: token-utils.slot(name-font-size, $config);
letter-spacing: token-utils.slot(name-letter-spacing, $config);
line-height: token-utils.slot(name-line-height, $config);
font-weight: token-utils.slot(name-font-weight, $config);
color: token-utils.slot(name-color, $config);
margin-bottom: 0.5rem;
}

.description {
font-family: token-utils.slot(description-font, $config);
font-size: token-utils.slot(description-font-size, $config);
letter-spacing: token-utils.slot(description-letter-spacing, $config);
line-height: token-utils.slot(description-line-height, $config);
font-weight: token-utils.slot(description-font-weight, $config);
margin: 0;
}
}

&.ang-profile-card-center-content {
text-align: center;

.picture {
margin: 0 auto 2rem auto;
}

.content {
margin-bottom: 0.5rem;
}

.actions {
display: flex;
justify-content: center;
}
}

.actions {
font-family: token-utils.slot(action-font, $config);
font-size: token-utils.slot(action-font-size, $config);
letter-spacing: token-utils.slot(action-letter-spacing, $config);
line-height: token-utils.slot(action-line-height, $config);
font-weight: token-utils.slot(action-font-weight, $config);
height: 2.5rem;
}
}
65 changes: 65 additions & 0 deletions libs/labs/profile-card/src/lib/profile-card.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ComponentInput, render, screen } from '@testing-library/angular';
import { ProfileCard } from './profile-card';

describe('ProfileCard', () => {
type SetupOptions = {
inputs?: ComponentInput<ProfileCard>;
content?: string;
};

function setup({ inputs = {}, content = '<a href="/profile">View profile</a>' }: SetupOptions = {}) {
return render(
`<ang-profile-card
[pictureUrl]="pictureUrl"
[name]="name"
[description]="description"
[centerContent]="centerContent"
>
${content}
</ang-profile-card>`,
{
imports: [ProfileCard],
componentProperties: {
pictureUrl: '/assets/profile.png',
name: 'Ada Lovelace',
description: 'Mathematician',
centerContent: false,
...inputs,
},
},
);
}

it('renders the name and description text', async () => {
await setup();

expect(screen.getByText('Ada Lovelace')).toBeInTheDocument();
expect(screen.getByText('Mathematician')).toBeInTheDocument();
});

it('renders the profile picture with src and accessible alt text', async () => {
await setup();

const image = screen.getByRole('img', { name: 'Profile picture of Ada Lovelace' });

expect(image).toHaveAttribute('src', '/assets/profile.png');
});

it('renders projected action content', async () => {
await setup({ content: '<button type="button">Connect</button>' });

expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument();
});

it('applies the center-content host class when centerContent is true', async () => {
const { container } = await setup({ inputs: { centerContent: true } });

expect(container.querySelector('ang-profile-card')).toHaveClass('ang-profile-card-center-content');
});

it('does not apply the center-content host class by default', async () => {
const { container } = await setup();

expect(container.querySelector('ang-profile-card')).not.toHaveClass('ang-profile-card-center-content');
});
});
92 changes: 92 additions & 0 deletions libs/labs/profile-card/src/lib/profile-card.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { MatIconModule } from '@angular/material/icon';
import { AnyLink } from '@atlasng/common';
import { TextLink } from '@atlasng/design-system/text-link';
import { type Meta, type StoryObj } from '@storybook/angular';
import { ProfileCard } from './profile-card';
import { SocialMediaButton } from '@atlasng/design-system/buttons/social-media';

const meta: Meta<ProfileCard> = {
component: ProfileCard,
title: 'Labs/Profile Card',
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/design/BCEJn9KCIbBJ5MzqnojKQp/AtlasNG-Components?node-id=1720-14192',
},
},
args: {
pictureUrl: 'assets/placeholder.png',
name: 'Firstname Lastname',
description: 'Occupation, Company',
centerContent: false,
},
};
export default meta;
type Story = StoryObj<ProfileCard>;

export const Default: Story = {
name: 'Action Button',
render: (args) => ({
props: args,
moduleMetadata: {
imports: [AnyLink, MatIconModule, TextLink],
},
styles: [
`.action-link {
margin-top: .5rem;
}`,
],
template: `
<ang-profile-card
[pictureUrl]="pictureUrl"
[name]="name"
[description]="description"
[centerContent]="centerContent"
>
<a angTextLink angAnyLink="www.example.com" class="action-link">
Action <mat-icon fontIcon="arrow_right_alt"/>
</a>

</ang-profile-card>
`,
}),
};

export const SocialIcons: Story = {
args: {
centerContent: true,
},
render: (args) => ({
props: args,
moduleMetadata: {
imports: [AnyLink, SocialMediaButton],
},
styles: [
`.social-media-actions {
display: flex;
gap: 0.5rem;
align-items: center;

ang-social-media-button {
height: 2.5rem;
}
}
}`,
],
template: `
<ang-profile-card
[pictureUrl]="pictureUrl"
[name]="name"
[description]="description"
[centerContent]="centerContent"
>
<div class="social-media-actions">
<ang-social-media-button id="linkedin"></ang-social-media-button>
<ang-social-media-button id="github"></ang-social-media-button>
<ang-social-media-button id="youtube"></ang-social-media-button>
<ang-social-media-button id="instagram"></ang-social-media-button>
</div>
</ang-profile-card>
`,
}),
};
27 changes: 27 additions & 0 deletions libs/labs/profile-card/src/lib/profile-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, input } from '@angular/core';

/**
* Profile Card for displaying user information and relevant links
*/
@Component({
selector: 'ang-profile-card',
templateUrl: './profile-card.html',
styleUrl: './profile-card.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.ang-profile-card-center-content]': 'centerContent()',
},
})
export class ProfileCard {
/** Field for profile picture URL */
readonly pictureUrl = input.required<string>();

/** Field for profile name */
readonly name = input.required<string>();

/** Field for description */
readonly description = input.required<string>();

/** Whether to center card content */
readonly centerContent = input(false, { transform: booleanAttribute });
}
25 changes: 25 additions & 0 deletions libs/labs/profile-card/src/lib/tokens.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@use 'internal/sass-utils';
@use 'internal/token-utils';

$config: (
namespace: 'ang-profile-card',
tokens: (
card-color: token-utils.sys-token(on-surface-variant),
name-color: token-utils.sys-token(on-surface),
name-font: token-utils.sys-token(title-medium-font),
name-font-size: token-utils.sys-token(title-medium-size),
name-line-height: token-utils.sys-token(title-medium-line-height),
name-letter-spacing: token-utils.sys-token(title-medium-tracking),
name-font-weight: token-utils.sys-token(title-medium-weight),
description-font: token-utils.sys-token(body-medium-font),
description-font-size: token-utils.sys-token(body-medium-size),
description-line-height: token-utils.sys-token(body-medium-line-height),
description-letter-spacing: token-utils.sys-token(body-medium-tracking),
description-font-weight: token-utils.sys-token(body-medium-weight),
action-font: token-utils.sys-token(body-medium-font),
action-font-size: token-utils.sys-token(body-medium-size),
action-line-height: token-utils.sys-token(body-medium-line-height),
action-letter-spacing: token-utils.sys-token(body-medium-tracking),
action-font-weight: token-utils.sys-token(body-medium-weight),
),
);
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"@atlasng/labs": ["./libs/labs/src/index.ts"],
"@atlasng/labs/cookie-modal": ["./libs/labs/cookie-modal/src/index.ts"],
"@atlasng/labs/footer": ["./libs/labs/footer/src/index.ts"],
"@atlasng/labs/grid-container": ["./libs/labs/grid-container/src/index.ts"],
"@atlasng/labs/header-shell": ["./libs/labs/header-shell/src/index.ts"],
"@atlasng/labs/profile-card": ["./libs/labs/profile-card/src/index.ts"],
"@atlasng/labs/section-header": ["./libs/labs/section-header/src/index.ts"],
"@atlasng/labs/skip-to-content-button": ["./libs/labs/skip-to-content-button/src/index.ts"],
"@atlasng/labs/table": ["./libs/labs/table/src/index.ts"],
Expand Down