diff --git a/src/lib/components/drawer/drawer.component.ts b/src/lib/components/drawer/drawer.component.ts new file mode 100644 index 0000000..fc3b032 --- /dev/null +++ b/src/lib/components/drawer/drawer.component.ts @@ -0,0 +1,74 @@ +import { + Component, + ContentChild, + EventEmitter, + Input, + Output, + TemplateRef, +} from '@angular/core'; +import { Drawer } from 'primeng/drawer'; +import { SharedModule } from 'primeng/api'; +import { NgTemplateOutlet } from '@angular/common'; + +@Component({ + selector: 'extra-drawer', + standalone: true, + imports: [Drawer, SharedModule, NgTemplateOutlet], + template: ` + + + @if (headerTemplate) { + + + + } + @if (footerTemplate) { + + + + } + + `, +}) +export class ExtraDrawerComponent { + @Input() visible = false; + @Input() header: string | undefined; + @Input() position: 'left' | 'right' | 'top' | 'bottom' = 'right'; + @Input() size: 'default' | 'sm' | 'lg' | 'xlg' = 'default'; + @Input() modal = true; + @Input() fullScreen = false; + @Input() dismissible = true; + @Input() showCloseIcon = true; + @Input() closeOnEscape = true; + @Input() blockScroll = true; + + @Output() visibleChange = new EventEmitter(); + @Output() onShow = new EventEmitter(); + @Output() onHide = new EventEmitter(); + + @ContentChild('drawerHeader') headerTemplate: TemplateRef | null = + null; + + @ContentChild('drawerFooter') footerTemplate: TemplateRef | null = + null; + + get sizeClass(): string { + if (this.size === 'default') return ''; + return `p-drawer-${this.size}`; + } +} diff --git a/src/lib/components/drawer/ng-package.json b/src/lib/components/drawer/ng-package.json new file mode 100644 index 0000000..ecdf8fe --- /dev/null +++ b/src/lib/components/drawer/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public_api.ts" + } +} + diff --git a/src/lib/components/drawer/public_api.ts b/src/lib/components/drawer/public_api.ts new file mode 100644 index 0000000..4ce8158 --- /dev/null +++ b/src/lib/components/drawer/public_api.ts @@ -0,0 +1,4 @@ +export * from './drawer.component'; + + + diff --git a/src/lib/providers/prime-preset/map-tokens.ts b/src/lib/providers/prime-preset/map-tokens.ts index d5947c4..e3a65a2 100644 --- a/src/lib/providers/prime-preset/map-tokens.ts +++ b/src/lib/providers/prime-preset/map-tokens.ts @@ -24,6 +24,7 @@ import { inputotpCss } from './tokens/components/inputotp'; import { carouselCss } from './tokens/components/carousel'; import { galleriaCss } from './tokens/components/galleria'; import { confirmDialogCss } from './tokens/components/confirm-dialog'; +import { drawerCss } from './tokens/components/drawer'; const presetTokens: Preset = { primitive: tokens.primitive as unknown as AuraBaseDesignTokens['primitive'], @@ -104,6 +105,10 @@ const presetTokens: Preset = { confirmdialog: { ...(tokens.components.confirmdialog as unknown as ComponentsDesignTokens['confirmdialog']), css: confirmDialogCss + }, + drawer: { + ...(tokens.components.drawer as unknown as ComponentsDesignTokens['drawer']), + css: drawerCss } } as ComponentsDesignTokens }; diff --git a/src/lib/providers/prime-preset/tokens/components/drawer.ts b/src/lib/providers/prime-preset/tokens/components/drawer.ts new file mode 100644 index 0000000..d04561e --- /dev/null +++ b/src/lib/providers/prime-preset/tokens/components/drawer.ts @@ -0,0 +1,66 @@ +const drawerCss = ({ dt }: { dt: (token: string) => string }): string => ` + +/* Скругление углов */ +.p-drawer.p-component { + border-radius: ${dt('drawer.extend.borderRadius')}; +} + +/* Нижняя граница и внутренние отступы заголовка */ +.p-drawer.p-component .p-drawer-header { + border-bottom: 1px solid ${dt('drawer.extend.extHeader.borderColor')}; + padding: ${dt('overlay.modal.padding.300')} ${dt('overlay.modal.padding.300')} ${dt('overlay.modal.padding.100')} ${dt('overlay.modal.padding.300')}; +} + +/* Типографика */ +.p-drawer.p-component .p-drawer-title { + font-weight: ${dt('drawer.title.fontWeight')}; + font-size: ${dt('drawer.title.fontSize')}; +} + +/* Кнопки действий в заголовке - расстояние между элементами */ +.p-drawer.p-component .p-drawer-header-actions { + gap: ${dt('drawer.extend.extHeader.gap')}; +} + +/* Внутренние отступы контента и футера */ +.p-drawer.p-component .p-drawer-content { + padding: ${dt('overlay.modal.padding.300')}; +} + +.p-drawer.p-component .p-drawer-footer { + padding: 0 ${dt('overlay.modal.padding.300')} ${dt('overlay.modal.padding.300')} ${dt('overlay.modal.padding.300')}; +} + +/* Боковые drawer (слева/справа) - базовые размеры и отступы от краев экрана */ +.p-drawer.p-component.p-drawer-left, +.p-drawer.p-component.p-drawer-right { + margin: ${dt('drawer.extend.padding')}; + width: ${dt('drawer.root.width')}; + height: calc(100% - ${dt('drawer.extend.padding')} * 2); +} + +.p-drawer.p-component.p-drawer-left.p-drawer-sm, +.p-drawer.p-component.p-drawer-right.p-drawer-sm { + width: ${dt('drawer.sm.width')}; +} + +.p-drawer.p-component.p-drawer-left.p-drawer-lg, +.p-drawer.p-component.p-drawer-right.p-drawer-lg { + width: ${dt('drawer.lg.width')}; +} + +.p-drawer.p-component.p-drawer-left.p-drawer-xlg, +.p-drawer.p-component.p-drawer-right.p-drawer-xlg { + width: ${dt('drawer.xlg.width')}; +} + +/* Горизонтальные drawer (сверху/снизу) - базовые размеры и отступы от краев экрана */ +.p-drawer.p-component.p-drawer-top, +.p-drawer.p-component.p-drawer-bottom { + margin: ${dt('drawer.extend.padding')}; + width: calc(100% - ${dt('drawer.extend.padding')} * 2); +} + +`; + +export { drawerCss }; diff --git a/src/lib/providers/prime-preset/tokens/tokens.json b/src/lib/providers/prime-preset/tokens/tokens.json index 4667831..1bb8d70 100644 --- a/src/lib/providers/prime-preset/tokens/tokens.json +++ b/src/lib/providers/prime-preset/tokens/tokens.json @@ -2813,10 +2813,20 @@ "background": "{overlay.modal.background}", "borderColor": "{overlay.modal.borderColor}", "color": "{overlay.modal.color}", - "shadow": "{overlay.modal.shadow}" + "shadow": "{overlay.modal.shadow}", + "width": "{overlay.width}" + }, + "sm": { + "width": "{overlay.sm.width}" + }, + "lg": { + "width": "{overlay.lg.width}" + }, + "xlg": { + "width": "{overlay.xlg.width}" }, "header": { - "padding": "{overlay.modal.padding.300} {overlay.modal.padding.300} {overlay.modal.padding.100} {overlay.modal.padding.300} " + "padding": "{overlay.modal.padding.300} {overlay.modal.padding.300} {overlay.modal.padding.200} {overlay.modal.padding.300} " }, "title": { "fontSize": "{fonts.fontSize.500}", diff --git a/src/stories/components/drawer/drawer.stories.ts b/src/stories/components/drawer/drawer.stories.ts new file mode 100644 index 0000000..7b57933 --- /dev/null +++ b/src/stories/components/drawer/drawer.stories.ts @@ -0,0 +1,208 @@ +import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { ExtraDrawerComponent } from '../../../lib/components/drawer/drawer.component'; +import { ExtraButtonComponent } from '../../../lib/components/button/button.component'; +import { DrawerSizesComponent, Sizes as SizesStory } from './examples/drawer-sizes.component'; +import { DrawerPositionComponent, Position as PositionStory } from './examples/drawer-position.component'; +import { DrawerWithFooterComponent, WithFooter as WithFooterStory } from './examples/drawer-with-footer.component'; +import { DrawerFullScreenComponent, FullScreen as FullScreenStory } from './examples/drawer-full-screen.component'; +import { DrawerWithoutModalComponent, WithoutModal as WithoutModalStory } from './examples/drawer-without-modal.component'; +import { DrawerWithCustomHeaderComponent, WithCustomHeader as WithCustomHeaderStory } from './examples/drawer-with-custom-header.component'; + +type DrawerArgs = ExtraDrawerComponent & { visible: boolean }; + +const meta: Meta = { + title: 'Components/Overlay/Drawer', + component: ExtraDrawerComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [ + ExtraDrawerComponent, + ExtraButtonComponent, + DrawerSizesComponent, + DrawerPositionComponent, + DrawerWithFooterComponent, + DrawerFullScreenComponent, + DrawerWithoutModalComponent, + DrawerWithCustomHeaderComponent, + ], + }), + ], + parameters: { + docs: { + description: { + component: `Drawer — панель, отображаемая как оверлей у края экрана. + +\`\`\`typescript +import { ExtraDrawerComponent } from '@cdek-it/angular-ui-kit'; +\`\`\``, + }, + }, + designTokens: { prefix: '--p-drawer' }, + }, + argTypes: { + visible: { + control: 'boolean', + description: 'Видимость drawer (two-way binding)', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + header: { + control: 'text', + description: 'Заголовок drawer', + table: { + category: 'Props', + defaultValue: { summary: 'undefined' }, + type: { summary: 'string' }, + }, + }, + position: { + control: 'select', + options: ['left', 'right', 'top', 'bottom'], + description: 'Позиция drawer', + table: { + category: 'Props', + defaultValue: { summary: 'right' }, + type: { summary: "'left' | 'right' | 'top' | 'bottom'" }, + }, + }, + size: { + control: 'select', + options: ['default', 'sm', 'lg', 'xlg'], + description: 'Размер drawer', + table: { + category: 'Props', + defaultValue: { summary: 'default' }, + type: { summary: "'default' | 'sm' | 'lg' | 'xlg'" }, + }, + }, + modal: { + control: 'boolean', + description: 'Показывать модальный оверлей', + table: { + category: 'Props', + defaultValue: { summary: 'true' }, + type: { summary: 'boolean' }, + }, + }, + fullScreen: { + control: 'boolean', + description: 'Полноэкранный режим', + table: { + category: 'Props', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, + dismissible: { + control: 'boolean', + description: 'Закрытие по клику на оверлей', + table: { + category: 'Props', + defaultValue: { summary: 'true' }, + type: { summary: 'boolean' }, + }, + }, + showCloseIcon: { + control: 'boolean', + description: 'Показывать кнопку закрытия', + table: { + category: 'Props', + defaultValue: { summary: 'true' }, + type: { summary: 'boolean' }, + }, + }, + closeOnEscape: { + control: 'boolean', + description: 'Закрытие по Escape', + table: { + category: 'Props', + defaultValue: { summary: 'true' }, + type: { summary: 'boolean' }, + }, + }, + blockScroll: { + control: 'boolean', + description: 'Блокировка прокрутки страницы', + table: { + category: 'Props', + defaultValue: { summary: 'true' }, + type: { summary: 'boolean' }, + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// ── Default ─────────────────────────────────────────────────────────────────── + +export const Default: Story = { + name: 'Default', + render: (args) => ({ + props: { ...args, visible: false }, + template: ` + + +

Drawer content.

+
+ `, + }), + args: { + header: 'Drawer', + position: 'right', + size: 'default', + modal: true, + fullScreen: false, + dismissible: true, + showCloseIcon: true, + closeOnEscape: true, + blockScroll: true, + }, + parameters: { + docs: { + description: { + story: 'Базовый пример компонента. Используйте Controls для интерактивного изменения пропсов.', + }, + }, + }, +}; + +// ── Sizes ───────────────────────────────────────────────────────────────────── + +export const Sizes: Story = SizesStory; + +// ── Position ────────────────────────────────────────────────────────────────── + +export const Position: Story = PositionStory; + +// ── WithFooter ──────────────────────────────────────────────────────────────── + +export const WithFooter: Story = WithFooterStory; + +// ── FullScreen ──────────────────────────────────────────────────────────────── + +export const FullScreen: Story = FullScreenStory; + +// ── WithoutModal ────────────────────────────────────────────────────────────── + +export const WithoutModal: Story = WithoutModalStory; + +// ── WithCustomHeader ───────────────────────────────────────────────────────── + +export const WithCustomHeader: Story = WithCustomHeaderStory; diff --git a/src/stories/components/drawer/examples/drawer-full-screen.component.ts b/src/stories/components/drawer/examples/drawer-full-screen.component.ts new file mode 100644 index 0000000..544ccb8 --- /dev/null +++ b/src/stories/components/drawer/examples/drawer-full-screen.component.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { ExtraDrawerComponent } from '../../../../lib/components/drawer/drawer.component'; +import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; + +const template = ` + + + +

Full screen drawer content.

+
+`; +const styles = ''; + +@Component({ + selector: 'app-drawer-full-screen', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template, + styles, +}) +export class DrawerFullScreenComponent { + visible = false; +} + +export const FullScreen: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + controls: { disable: true }, + docs: { + description: { story: 'Drawer в полноэкранном режиме.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { ExtraDrawerComponent, ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-drawer-full-screen', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template: \` + + + +

Full screen drawer content.

+
+ \`, +}) +export class DrawerFullScreenComponent { + visible = false; +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/drawer/examples/drawer-position.component.ts b/src/stories/components/drawer/examples/drawer-position.component.ts new file mode 100644 index 0000000..a927bb6 --- /dev/null +++ b/src/stories/components/drawer/examples/drawer-position.component.ts @@ -0,0 +1,99 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { ExtraDrawerComponent } from '../../../../lib/components/drawer/drawer.component'; +import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; + +const template = ` +
+ + + + +
+ + +

Left drawer content.

+
+ + +

Right drawer content.

+
+ + +

Top drawer content.

+
+ + +

Bottom drawer content.

+
+`; +const styles = ''; + +@Component({ + selector: 'app-drawer-position', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template, + styles, +}) +export class DrawerPositionComponent { + visibleLeft = false; + visibleRight = false; + visibleTop = false; + visibleBottom = false; +} + +export const Position: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + controls: { disable: true }, + docs: { + description: { story: 'Drawer с различными позициями: left, right, top, bottom.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { ExtraDrawerComponent, ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-drawer-position', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template: \` +
+ + + + +
+ + +

Left drawer content.

+
+ + +

Right drawer content.

+
+ + +

Top drawer content.

+
+ + +

Bottom drawer content.

+
+ \`, +}) +export class DrawerPositionComponent { + visibleLeft = false; + visibleRight = false; + visibleTop = false; + visibleBottom = false; +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/drawer/examples/drawer-sizes.component.ts b/src/stories/components/drawer/examples/drawer-sizes.component.ts new file mode 100644 index 0000000..481152d --- /dev/null +++ b/src/stories/components/drawer/examples/drawer-sizes.component.ts @@ -0,0 +1,99 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { ExtraDrawerComponent } from '../../../../lib/components/drawer/drawer.component'; +import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; + +const template = ` +
+ + + + +
+ + +

Default size drawer content.

+
+ + +

Small size drawer content.

+
+ + +

Large size drawer content.

+
+ + +

Extra large size drawer content.

+
+`; +const styles = ''; + +@Component({ + selector: 'app-drawer-sizes', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template, + styles, +}) +export class DrawerSizesComponent { + visibleDefault = false; + visibleSm = false; + visibleLg = false; + visibleXlg = false; +} + +export const Sizes: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + controls: { disable: true }, + docs: { + description: { story: 'Drawer в различных размерах: default, sm, lg, xlg.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { ExtraDrawerComponent, ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-drawer-sizes', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template: \` +
+ + + + +
+ + +

Default size drawer content.

+
+ + +

Small size drawer content.

+
+ + +

Large size drawer content.

+
+ + +

Extra large size drawer content.

+
+ \`, +}) +export class DrawerSizesComponent { + visibleDefault = false; + visibleSm = false; + visibleLg = false; + visibleXlg = false; +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/drawer/examples/drawer-with-custom-header.component.ts b/src/stories/components/drawer/examples/drawer-with-custom-header.component.ts new file mode 100644 index 0000000..3e07452 --- /dev/null +++ b/src/stories/components/drawer/examples/drawer-with-custom-header.component.ts @@ -0,0 +1,73 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { ExtraDrawerComponent } from '../../../../lib/components/drawer/drawer.component'; +import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; + +const template = ` + + + + +
+ Custom Header + +
+
+ +

Drawer content with a custom header template.

+
+`; +const styles = ''; + +@Component({ + selector: 'app-drawer-with-custom-header', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template, + styles, +}) +export class DrawerWithCustomHeaderComponent { + visible = false; +} + +export const WithCustomHeader: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + controls: { disable: true }, + docs: { + description: { story: 'Drawer с кастомным заголовком через ng-template #drawerHeader.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { ExtraDrawerComponent, ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-drawer-with-custom-header', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template: \` + + + + +
+ Custom Header + +
+
+ +

Drawer content with a custom header template.

+
+ \`, +}) +export class DrawerWithCustomHeaderComponent { + visible = false; +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/drawer/examples/drawer-with-footer.component.ts b/src/stories/components/drawer/examples/drawer-with-footer.component.ts new file mode 100644 index 0000000..dccabc7 --- /dev/null +++ b/src/stories/components/drawer/examples/drawer-with-footer.component.ts @@ -0,0 +1,73 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { ExtraDrawerComponent } from '../../../../lib/components/drawer/drawer.component'; +import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; + +const template = ` + + + +

Drawer content with footer actions.

+ + +
+ + +
+
+
+`; +const styles = ''; + +@Component({ + selector: 'app-drawer-with-footer', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template, + styles, +}) +export class DrawerWithFooterComponent { + visible = false; +} + +export const WithFooter: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + controls: { disable: true }, + docs: { + description: { story: 'Drawer с футером, содержащим кнопки действий.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { ExtraDrawerComponent, ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-drawer-with-footer', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template: \` + + + +

Drawer content with footer actions.

+ + +
+ + +
+
+
+ \`, +}) +export class DrawerWithFooterComponent { + visible = false; +} + `, + }, + }, + }, +}; diff --git a/src/stories/components/drawer/examples/drawer-without-modal.component.ts b/src/stories/components/drawer/examples/drawer-without-modal.component.ts new file mode 100644 index 0000000..ba27fdf --- /dev/null +++ b/src/stories/components/drawer/examples/drawer-without-modal.component.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { StoryObj } from '@storybook/angular'; +import { ExtraDrawerComponent } from '../../../../lib/components/drawer/drawer.component'; +import { ExtraButtonComponent } from '../../../../lib/components/button/button.component'; + +const template = ` + + + +

Drawer without backdrop overlay.

+
+`; +const styles = ''; + +@Component({ + selector: 'app-drawer-without-modal', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template, + styles, +}) +export class DrawerWithoutModalComponent { + visible = false; +} + +export const WithoutModal: StoryObj = { + render: () => ({ + template: ``, + }), + parameters: { + controls: { disable: true }, + docs: { + description: { story: 'Drawer без модального оверлея.' }, + source: { + language: 'ts', + code: ` +import { Component } from '@angular/core'; +import { ExtraDrawerComponent, ExtraButtonComponent } from '@cdek-it/angular-ui-kit'; + +@Component({ + selector: 'app-drawer-without-modal', + standalone: true, + imports: [ExtraDrawerComponent, ExtraButtonComponent], + template: \` + + + +

Drawer without backdrop overlay.

+
+ \`, +}) +export class DrawerWithoutModalComponent { + visible = false; +} + `, + }, + }, + }, +};