diff --git a/public/assets/images/avatar/avatar.png b/public/assets/images/avatar/avatar.png new file mode 100644 index 00000000..7c0bd535 Binary files /dev/null and b/public/assets/images/avatar/avatar.png differ diff --git a/src/lib/components/avatar/avatar.component.ts b/src/lib/components/avatar/avatar.component.ts new file mode 100644 index 00000000..de933fd7 --- /dev/null +++ b/src/lib/components/avatar/avatar.component.ts @@ -0,0 +1,51 @@ +import { Component, HostBinding, Input } from '@angular/core'; +import { Avatar } from 'primeng/avatar'; +import { AvatarGroup } from 'primeng/avatargroup'; + +export type AvatarSize = 'normal' | 'large' | 'xlarge'; +export type AvatarShape = 'square' | 'circle'; + +@Component({ + selector: 'avatar', + standalone: true, + imports: [Avatar], + template: ` + + `, +}) +export class AvatarComponent { + @Input() label = ''; + @Input() icon = ''; + @Input() image = ''; + @Input() size: AvatarSize = 'normal'; + @Input() shape: AvatarShape = 'square'; + + @HostBinding('class') get hostClass(): string { + const classes = ['ui-avatar']; + if (this.size === 'large') classes.push('ui-avatar-lg'); + if (this.size === 'xlarge') classes.push('ui-avatar-xl'); + return classes.join(' '); + } + + get primeSize(): 'normal' | 'large' | 'xlarge' | undefined { + return this.size === 'normal' ? undefined : this.size; + } +} + +@Component({ + selector: 'avatar-group', + standalone: true, + imports: [AvatarGroup], + template: ` + + + + `, +}) +export class AvatarGroupComponent { } diff --git a/src/prime-preset/map-tokens.ts b/src/prime-preset/map-tokens.ts index 39627587..896d4d4e 100644 --- a/src/prime-preset/map-tokens.ts +++ b/src/prime-preset/map-tokens.ts @@ -3,6 +3,7 @@ import type { ComponentsDesignTokens } from '@primeuix/themes/types'; import type { AuraBaseDesignTokens } from '@primeuix/themes/aura/base'; import tokens from './tokens/tokens.json'; +import { avatarCss } from './tokens/components/avatar'; import { buttonCss } from './tokens/components/button'; const presetTokens: Preset = { @@ -10,6 +11,10 @@ const presetTokens: Preset = { semantic: tokens.semantic as unknown as AuraBaseDesignTokens['semantic'], components: { ...(tokens.components as unknown as ComponentsDesignTokens), + avatar: { + ...(tokens.components.avatar as unknown as ComponentsDesignTokens['avatar']), + css: avatarCss, + }, button: { ...(tokens.components.button as unknown as ComponentsDesignTokens['button']), css: buttonCss, diff --git a/src/prime-preset/tokens/components/avatar.ts b/src/prime-preset/tokens/components/avatar.ts new file mode 100644 index 00000000..2c80d571 --- /dev/null +++ b/src/prime-preset/tokens/components/avatar.ts @@ -0,0 +1,33 @@ +export const avatarCss = ({ dt }: { dt: (token: string) => string }): string => ` + :root { + --p-avatar-extend-border-color: ${dt('avatar.extend.borderColor')}; + --p-avatar-extend-circle-border-radius: ${dt('avatar.extend.circle.borderRadius')}; + --p-avatar-group-border-color: ${dt('content.background')}; + --p-avatar-group-offset: calc(-1 * ${dt('media.padding.300')}); + --p-avatar-lg-group-offset: calc(-1 * ${dt('media.padding.300')}); + --p-avatar-xl-group-offset: calc(-1 * ${dt('media.padding.600')}); + } + + /* ─── Группировка: отступы для кастомных классов хост-элемента ─── */ + .p-avatar-group .ui-avatar + .ui-avatar { + margin-inline-start: var(--p-avatar-group-offset); + } + + .p-avatar-group .ui-avatar-lg + .ui-avatar-lg { + margin-inline-start: var(--p-avatar-lg-group-offset); + } + + .p-avatar-group .ui-avatar-xl + .ui-avatar-xl { + margin-inline-start: var(--p-avatar-xl-group-offset); + } + + /* ─── Круглая форма: clip изображения по максимальному border-radius ─── */ + .p-avatar.p-avatar-circle { + border-radius: var(--p-avatar-extend-circle-border-radius); + overflow: hidden; + } + + .p-overlaybadge.p-overlaybadge { + width: fit-content; + } +`; diff --git a/src/prime-preset/tokens/tokens.json b/src/prime-preset/tokens/tokens.json index f9ea56a8..8fb504b7 100644 --- a/src/prime-preset/tokens/tokens.json +++ b/src/prime-preset/tokens/tokens.json @@ -1509,7 +1509,7 @@ }, "group": { "borderColor": "{content.background}", - "offset": "{media.padding.300}" + "offset": "-{media.padding.300}" }, "lg": { "width": "{media.size.400}", @@ -1519,7 +1519,7 @@ "size": "{media.icon.size.100}" }, "group": { - "offset": "{media.padding.300}" + "offset": "-{media.padding.300}" } }, "xl": { @@ -1529,7 +1529,7 @@ "size": "{media.icon.size.200}" }, "group": { - "offset": "{media.padding.600}" + "offset": "-{media.padding.600}" }, "fontSize": "{fonts.fontSize.500}" } diff --git a/src/stories/components/avatar/avatar.stories.ts b/src/stories/components/avatar/avatar.stories.ts new file mode 100644 index 00000000..6f760953 --- /dev/null +++ b/src/stories/components/avatar/avatar.stories.ts @@ -0,0 +1,297 @@ +import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { OverlayBadge } from 'primeng/overlaybadge'; +import { AvatarComponent, AvatarGroupComponent } from '../../../lib/components/avatar/avatar.component'; + +const meta: Meta = { + title: 'Components/Misc/Avatar', + component: AvatarComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [AvatarComponent, AvatarGroupComponent, OverlayBadge], + }), + ], + parameters: { + docs: { + description: { + component: `Аватар представляет пользователя или сущность. Может содержать текст, иконку или изображение. [PrimeNG Avatar](https://primeng.org/avatar). + +\`\`\`typescript +import { AvatarComponent, AvatarGroupComponent } from '@cdek-it/angular-ui-kit'; +\`\`\``, + }, + }, + designTokens: { prefix: '--p-avatar' }, + }, + argTypes: { + // ── Props ──────────────────────────────────────────────── + label: { + control: 'text', + description: 'Текст внутри аватара', + table: { + category: 'Props', + defaultValue: { summary: "''" }, + type: { summary: 'string' }, + }, + }, + icon: { + control: 'text', + description: 'CSS-класс иконки (например: ti ti-user)', + table: { + category: 'Props', + defaultValue: { summary: "''" }, + type: { summary: 'string' }, + }, + }, + image: { + control: 'text', + description: 'URL изображения', + table: { + category: 'Props', + defaultValue: { summary: "''" }, + type: { summary: 'string' }, + }, + }, + size: { + control: 'select', + options: ['normal', 'large', 'xlarge'], + description: 'Размер аватара', + table: { + category: 'Props', + defaultValue: { summary: 'normal' }, + type: { summary: "'normal' | 'large' | 'xlarge'" }, + }, + }, + shape: { + control: 'select', + options: ['square', 'circle'], + description: 'Форма аватара', + table: { + category: 'Props', + defaultValue: { summary: 'square' }, + type: { summary: "'square' | 'circle'" }, + }, + }, + }, +}; + +const commonTemplate = ` + +`; + +export default meta; +type Story = StoryObj; + +// ── Default ────────────────────────────────────────────────────────────────── + +export const Default: Story = { + name: 'Default', + render: (args) => { + const parts: string[] = []; + + if (args.label) parts.push(`label="${args.label}"`); + if (args.icon) parts.push(`icon="${args.icon}"`); + if (args.image) parts.push(`image="${args.image}"`); + if (args.size && args.size !== 'normal') parts.push(`size="${args.size}"`); + if (args.shape && args.shape !== 'square') parts.push(`shape="${args.shape}"`); + + const template = parts.length + ? `` + : ``; + + return { props: args, template }; + }, + args: { + label: 'A', + size: 'normal', + shape: 'square', + }, + parameters: { + docs: { + description: { + story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.', + }, + }, + }, +}; + +// ── Label ──────────────────────────────────────────────────────────────────── + +export const Label: Story = { + render: (args) => ({ props: args, template: commonTemplate }), + args: { label: 'A', size: 'normal', shape: 'square' }, + parameters: { + docs: { + description: { story: 'Аватар с текстовой меткой.' }, + source: { + code: ``, + }, + }, + }, +}; + +// ── Icon ───────────────────────────────────────────────────────────────────── + +export const Icon: Story = { + render: (args) => ({ props: args, template: commonTemplate }), + args: { icon: 'ti ti-user', size: 'normal', shape: 'square' }, + parameters: { + docs: { + description: { story: 'Аватар с иконкой.' }, + source: { + code: ``, + }, + }, + }, +}; + +// ── Image ──────────────────────────────────────────────────────────────────── + +export const Image: Story = { + render: (args) => ({ props: args, template: commonTemplate }), + args: { image: '/assets/images/avatar/avatar.png', size: 'normal', shape: 'square' }, + parameters: { + docs: { + description: { story: 'Аватар с изображением. shape="square" — без обрезки, shape="circle" — с обрезкой по кругу.' }, + source: { + code: ``, + }, + }, + }, +}; + +// ── Sizes ──────────────────────────────────────────────────────────────────── + +export const Sizes: Story = { + render: (args) => ({ props: args, template: commonTemplate }), + args: { label: 'L', size: 'large', shape: 'square' }, + parameters: { + docs: { + description: { story: 'Размер аватара. Доступны: normal, large, xlarge.' }, + source: { + code: ``, + }, + }, + }, +}; + +// ── Shapes ─────────────────────────────────────────────────────────────────── + +export const Shapes: Story = { + render: (args) => ({ props: args, template: commonTemplate }), + args: { label: 'C', size: 'normal', shape: 'circle' }, + parameters: { + docs: { + description: { story: 'Форма аватара. circle — круглый, square — квадратный (по умолчанию).' }, + source: { + code: ``, + }, + }, + }, +}; + +// ── Group ──────────────────────────────────────────────────────────────────── +// Исключение: avatar-group — составной компонент, +// дочерние элементы — это его суть, не дублирование. + +export const Group: Story = { + render: () => ({ + template: ` + + + + + + + + + `, + }), + parameters: { + docs: { + description: { story: 'Группа аватаров с перекрытием.' }, + source: { + code: ` + + + +`, + }, + }, + }, +}; + +// ── LabelWithBadge ─────────────────────────────────────────────────────────── + +export const LabelWithBadge: Story = { + render: (args) => ({ + props: args, + template: ` + + + + `, + }), + parameters: { + docs: { + description: { story: 'Аватар с текстовой меткой и бейджем через OverlayBadge.' }, + source: { + code: ` + +`, + }, + }, + }, +}; + +// ── IconWithBadge ──────────────────────────────────────────────────────────── + +export const IconWithBadge: Story = { + render: (args) => ({ + props: args, + template: ` + + + + `, + }), + parameters: { + docs: { + description: { story: 'Аватар с иконкой и бейджем через OverlayBadge.' }, + source: { + code: ` + +`, + }, + }, + }, +}; + +// ── ImageWithBadge ─────────────────────────────────────────────────────────── + +export const ImageWithBadge: Story = { + render: (args) => ({ + props: args, + template: ` + + + + `, + }), + parameters: { + docs: { + description: { story: 'Аватар с изображением и бейджем через OverlayBadge.' }, + source: { + code: ` + +`, + }, + }, + }, +}; diff --git a/src/stories/components/avatar/examples/avatar-group.component.ts b/src/stories/components/avatar/examples/avatar-group.component.ts new file mode 100644 index 00000000..867d1b41 --- /dev/null +++ b/src/stories/components/avatar/examples/avatar-group.component.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { AvatarComponent, AvatarGroupComponent } from '../../../../lib/components/avatar/avatar.component'; + +const template = ` +
+ + + + + + + + +
+`; +const styles = ''; + +@Component({ + selector: 'app-avatar-group', + standalone: true, + imports: [AvatarComponent, AvatarGroupComponent], + template, + styles, +}) +export class AvatarGroupExampleComponent {} + +export const Group: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Группа аватаров с перекрытием.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { AvatarComponent, AvatarGroupComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-avatar-group', + standalone: true, + imports: [AvatarComponent, AvatarGroupComponent], + template: \` + + + + + + + \`, +}) +export class AvatarGroupExampleComponent {} + `, + }, + }, + }, +}; diff --git a/src/stories/components/avatar/examples/avatar-icon-badge.component.ts b/src/stories/components/avatar/examples/avatar-icon-badge.component.ts new file mode 100644 index 00000000..e4b8fa96 --- /dev/null +++ b/src/stories/components/avatar/examples/avatar-icon-badge.component.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { OverlayBadge } from 'primeng/overlaybadge'; +import { AvatarComponent } from '../../../../lib/components/avatar/avatar.component'; + +const template = ` +
+
+ + + + + + +
+
+`; +const styles = ''; + +@Component({ + selector: 'app-avatar-icon-badge', + standalone: true, + imports: [AvatarComponent, OverlayBadge], + template, + styles, +}) +export class AvatarIconBadgeComponent {} + +export const IconWithBadge: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Аватары с иконкой и бейджем через OverlayBadge.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { OverlayBadge } from 'primeng/overlaybadge'; +import { AvatarComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-avatar-icon-badge', + standalone: true, + imports: [AvatarComponent, OverlayBadge], + template: \` +
+ + + + + + +
+ \`, +}) +export class AvatarIconBadgeComponent {} + `, + }, + }, + }, +}; diff --git a/src/stories/components/avatar/examples/avatar-icon.component.ts b/src/stories/components/avatar/examples/avatar-icon.component.ts new file mode 100644 index 00000000..9dd91bdb --- /dev/null +++ b/src/stories/components/avatar/examples/avatar-icon.component.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { AvatarComponent } from '../../../../lib/components/avatar/avatar.component'; + +const template = ` +
+
+ + + +
+
+`; +const styles = ''; + +@Component({ + selector: 'app-avatar-icon', + standalone: true, + imports: [AvatarComponent], + template, + styles, +}) +export class AvatarIconComponent {} + +export const Icon: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Аватары с иконкой разных размеров.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { AvatarComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-avatar-icon', + standalone: true, + imports: [AvatarComponent], + template: \` +
+ + + +
+ \`, +}) +export class AvatarIconComponent {} + `, + }, + }, + }, +}; diff --git a/src/stories/components/avatar/examples/avatar-image-badge.component.ts b/src/stories/components/avatar/examples/avatar-image-badge.component.ts new file mode 100644 index 00000000..2117577b --- /dev/null +++ b/src/stories/components/avatar/examples/avatar-image-badge.component.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { OverlayBadge } from 'primeng/overlaybadge'; +import { AvatarComponent } from '../../../../lib/components/avatar/avatar.component'; + +const template = ` +
+
+ + + + + + +
+
+`; +const styles = ''; + +@Component({ + selector: 'app-avatar-image-badge', + standalone: true, + imports: [AvatarComponent, OverlayBadge], + template, + styles, +}) +export class AvatarImageBadgeComponent {} + +export const ImageWithBadge: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Аватары с изображением и бейджем через OverlayBadge.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { OverlayBadge } from 'primeng/overlaybadge'; +import { AvatarComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-avatar-image-badge', + standalone: true, + imports: [AvatarComponent, OverlayBadge], + template: \` +
+ + + + + + +
+ \`, +}) +export class AvatarImageBadgeComponent {} + `, + }, + }, + }, +}; diff --git a/src/stories/components/avatar/examples/avatar-image.component.ts b/src/stories/components/avatar/examples/avatar-image.component.ts new file mode 100644 index 00000000..c11580dc --- /dev/null +++ b/src/stories/components/avatar/examples/avatar-image.component.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { AvatarComponent } from '../../../../lib/components/avatar/avatar.component'; + +const template = ` +
+
+ + + +
+
+`; +const styles = ''; + +@Component({ + selector: 'app-avatar-image', + standalone: true, + imports: [AvatarComponent], + template, + styles, +}) +export class AvatarImageComponent {} + +export const Image: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Аватары с изображением разных размеров.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { AvatarComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-avatar-image', + standalone: true, + imports: [AvatarComponent], + template: \` +
+ + + +
+ \`, +}) +export class AvatarImageComponent {} + `, + }, + }, + }, +}; diff --git a/src/stories/components/avatar/examples/avatar-label-badge.component.ts b/src/stories/components/avatar/examples/avatar-label-badge.component.ts new file mode 100644 index 00000000..bda2e9a0 --- /dev/null +++ b/src/stories/components/avatar/examples/avatar-label-badge.component.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { OverlayBadge } from 'primeng/overlaybadge'; +import { AvatarComponent } from '../../../../lib/components/avatar/avatar.component'; + +const template = ` +
+
+ + + + + + +
+
+`; +const styles = ''; + +@Component({ + selector: 'app-avatar-label-badge', + standalone: true, + imports: [AvatarComponent, OverlayBadge], + template, + styles, +}) +export class AvatarLabelBadgeComponent {} + +export const LabelWithBadge: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Аватары с текстом и бейджем через OverlayBadge.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { OverlayBadge } from 'primeng/overlaybadge'; +import { AvatarComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-avatar-label-badge', + standalone: true, + imports: [AvatarComponent, OverlayBadge], + template: \` +
+ + + + + + +
+ \`, +}) +export class AvatarLabelBadgeComponent {} + `, + }, + }, + }, +}; diff --git a/src/stories/components/avatar/examples/avatar-label.component.ts b/src/stories/components/avatar/examples/avatar-label.component.ts new file mode 100644 index 00000000..a33588c3 --- /dev/null +++ b/src/stories/components/avatar/examples/avatar-label.component.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { AvatarComponent } from '../../../../lib/components/avatar/avatar.component'; + +const template = ` +
+
+ + + +
+
+`; +const styles = ''; + +@Component({ + selector: 'app-avatar-label', + standalone: true, + imports: [AvatarComponent], + template, + styles, +}) +export class AvatarLabelComponent {} + +export const Label: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Аватары с текстовой меткой разных размеров.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { AvatarComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-avatar-label', + standalone: true, + imports: [AvatarComponent], + template: \` +
+ + + +
+ \`, +}) +export class AvatarLabelComponent {} + `, + }, + }, + }, +}; diff --git a/src/stories/components/avatar/examples/avatar-shapes.component.ts b/src/stories/components/avatar/examples/avatar-shapes.component.ts new file mode 100644 index 00000000..c4693b30 --- /dev/null +++ b/src/stories/components/avatar/examples/avatar-shapes.component.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { AvatarComponent } from '../../../../lib/components/avatar/avatar.component'; + +const template = ` +
+
+ + +
+
+`; +const styles = ''; + +@Component({ + selector: 'app-avatar-shapes', + standalone: true, + imports: [AvatarComponent], + template, + styles, +}) +export class AvatarShapesComponent {} + +export const Shapes: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Формы аватара: square (по умолчанию) и circle.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { AvatarComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-avatar-shapes', + standalone: true, + imports: [AvatarComponent], + template: \` +
+ + +
+ \`, +}) +export class AvatarShapesComponent {} + `, + }, + }, + }, +}; diff --git a/src/stories/components/avatar/examples/avatar-sizes.component.ts b/src/stories/components/avatar/examples/avatar-sizes.component.ts new file mode 100644 index 00000000..99ff3370 --- /dev/null +++ b/src/stories/components/avatar/examples/avatar-sizes.component.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { AvatarComponent } from '../../../../lib/components/avatar/avatar.component'; + +const template = ` +
+
+ + + +
+
+`; +const styles = ''; + +@Component({ + selector: 'app-avatar-sizes', + standalone: true, + imports: [AvatarComponent], + template, + styles, +}) +export class AvatarSizesComponent {} + +export const Sizes: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + docs: { + description: { + story: 'Все доступные размеры аватара: normal, large, xlarge.', + }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { AvatarComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-avatar-sizes', + standalone: true, + imports: [AvatarComponent], + template: \` +
+ + + +
+ \`, +}) +export class AvatarSizesComponent {} + `, + }, + }, + }, +};