From 3e8159c792c71ec726879e30a9c1a86ab92d06d4 Mon Sep 17 00:00:00 2001 From: Mohammad Javed Date: Sun, 14 Sep 2025 15:05:47 +0530 Subject: [PATCH 1/2] UI(sheet): Add sheet component --- projects/ui/src/directives/sheet.ts | 154 ++++++++++++++++++++++++++++ projects/ui/src/public-api.ts | 1 + 2 files changed, 155 insertions(+) create mode 100644 projects/ui/src/directives/sheet.ts diff --git a/projects/ui/src/directives/sheet.ts b/projects/ui/src/directives/sheet.ts new file mode 100644 index 0000000..ff68b87 --- /dev/null +++ b/projects/ui/src/directives/sheet.ts @@ -0,0 +1,154 @@ +import { computed, Directive, input } from "@angular/core"; +import { tv, VariantProps } from "tailwind-variants"; +import { NgpDialog, NgpDialogDescription, NgpDialogOverlay, NgpDialogTitle, NgpDialogTrigger } from "ng-primitives/dialog"; + +const sheetVariants = tv({ + slots: { + sheet: 'bg-background data-[enter]:animate-in data-[exit]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[enter]:duration-500', + sheetOverlay: 'data-[enter]:animate-in data-[exit]:animate-out data-[exit]:fade-out-0 data-[enter]:fade-in-0 fixed inset-0 z-50 bg-black/50', + sheetHeader: 'flex flex-col gap-1.5 p-4', + sheetFooter: 'mt-auto flex flex-col gap-2 p-4', + sheetTitle: 'text-foreground font-semibold', + sheetDescription: 'text-muted-foreground text-sm', + sheetTrigger: '', + }, + variants: { + side: { + right: { + sheet: 'data-[exit]:slide-out-to-right data-[enter]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm' + }, + left: { + sheet: 'data-[exit]:slide-out-to-left data-[enter]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm' + }, + top: { + sheet: 'data-[exit]:slide-out-to-top data-[enter]:slide-in-from-top inset-x-0 top-0 h-auto border-b' + }, + bottom: { + sheet: 'data-[exit]:slide-out-to-bottom data-[enter]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t' + } + } + }, + defaultVariants: { + side: 'right' + } +}); + +const { sheet, sheetOverlay, sheetHeader, sheetFooter, sheetTitle, sheetDescription, sheetTrigger } = sheetVariants(); + +type SheetVariants = VariantProps; + +@Directive({ + selector: '[uiSheet]', + exportAs: 'uiSheet', + host: { + '[class]': 'computedClass()' + }, + hostDirectives: [ + { directive: NgpDialog, + inputs: [ + 'ngpDialogRole: uiSheetRole', + 'ngpDialogModal: uiSheetModal' + ] + } + ] +}) +export class UiSheet { + inputClass = input('', { alias: 'class' }); + side = input('right'); + computedClass = computed(() => sheet({ + side: this.side(), + class: this.inputClass() + })); +} + +@Directive({ + selector: '[uiSheetHeader]', + exportAs: 'uiSheetHeader', + host: { + '[class]': 'computedClass()' + }, + hostDirectives: [] +}) +export class UiSheetHeader { + inputClass = input('', { alias: 'class' }); + computedClass = computed(() => sheetHeader({ class: this.inputClass() })); +} + +@Directive({ + selector: '[uiSheetTitle]', + exportAs: 'uiSheetTitle', + host: { + '[class]': 'computedClass()' + }, + hostDirectives: [NgpDialogTitle] +}) +export class UiSheetTitle { + inputClass = input('', { alias: 'class' }); + computedClass = computed(() => sheetTitle({ class: this.inputClass() })); +} + +@Directive({ + selector: '[uiSheetDescription]', + exportAs: 'uiSheetDescription', + host: { + '[class]': 'computedClass()' + }, + hostDirectives: [NgpDialogDescription] +}) +export class UiSheetDescription { + inputClass = input('', { alias: 'class' }); + computedClass = computed(() => sheetDescription({ class: this.inputClass() })); +} + +@Directive({ + selector: '[uiSheetFooter]', + exportAs: 'uiSheetFooter', + host: { + '[class]': 'computedClass()' + }, + hostDirectives: [] +}) +export class UiSheetFooter { + inputClass = input('', { alias: 'class' }); + computedClass = computed(() => sheetFooter({ class: this.inputClass() })); +} + +@Directive({ + selector: '[uiSheetTrigger]', + exportAs: 'uiSheetTrigger', + host: { + '[class]': 'computedClass()' + }, + hostDirectives: [ + { + directive: NgpDialogTrigger, + inputs: [ + 'ngpDialogTrigger: uiSheetTrigger', + 'ngpDialogTriggerCloseOnEscape: uiSheetTriggerCloseOnEscape'] + } + ] +}) +export class UiSheetTrigger { + inputClass = input('', { alias: 'class' }); + computedClass = computed(() => sheetTrigger({ class: this.inputClass() })); +} + +@Directive({ + selector: '[uiSheetOverlay]', + exportAs: 'uiSheetOverlay', + host: { + '[class]': 'computedClass()' + }, + hostDirectives: [ + { + directive: NgpDialogOverlay, + inputs: [ + 'ngpDialogOverlayCloseOnClick: uiSheetOverlayCloseOnClick' + ] + } + ] +}) +export class UiSheetOverlay { + inputClass = input('', { alias: 'class' }); + computedClass = computed(() => sheetOverlay({ class: this.inputClass() })); +} \ No newline at end of file diff --git a/projects/ui/src/public-api.ts b/projects/ui/src/public-api.ts index 17cc14a..7170885 100644 --- a/projects/ui/src/public-api.ts +++ b/projects/ui/src/public-api.ts @@ -5,6 +5,7 @@ export * from './directives/badge'; export * from './directives/card'; export * from './directives/accordion'; export * from './directives/dialog'; +export * from './directives/sheet'; export * from './directives/alert-dialog'; export * from './directives/input'; export * from './directives/textarea'; From 0fc3780629011bebc2b17c5e923bdbabcad573dc Mon Sep 17 00:00:00 2001 From: Mohammad Javed Date: Sun, 14 Sep 2025 15:07:12 +0530 Subject: [PATCH 2/2] docs(sheet): Add sheet docs --- .../docs/components/components.routes.ts | 4 + .../app/pages/docs/components/sheet/sheet.ts | 18 + .../docs/components/sheet/sheet.variants.ts | 377 ++++++++++++++++++ .../app/shared/components/sidebar/sidebar.ts | 1 + registry.json | 13 + 5 files changed, 413 insertions(+) create mode 100644 projects/docs/src/app/pages/docs/components/sheet/sheet.ts create mode 100644 projects/docs/src/app/pages/docs/components/sheet/sheet.variants.ts diff --git a/projects/docs/src/app/pages/docs/components/components.routes.ts b/projects/docs/src/app/pages/docs/components/components.routes.ts index 6f75b4a..224ba08 100644 --- a/projects/docs/src/app/pages/docs/components/components.routes.ts +++ b/projects/docs/src/app/pages/docs/components/components.routes.ts @@ -40,6 +40,10 @@ export const routes: Routes = [ path: 'dialog', loadComponent: () => import('./dialog/dialog').then(m => m.Dialog) }, + { + path: 'sheet', + loadComponent: () => import('./sheet/sheet').then(m => m.Sheet) + }, { path: 'alert-dialog', loadComponent: () => import('./alert-dialog/alert-dialog').then(m => m.AlertDialog) diff --git a/projects/docs/src/app/pages/docs/components/sheet/sheet.ts b/projects/docs/src/app/pages/docs/components/sheet/sheet.ts new file mode 100644 index 0000000..48e4a88 --- /dev/null +++ b/projects/docs/src/app/pages/docs/components/sheet/sheet.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { ComponentPreview } from '@components/component-preview/component-preview'; +import { sheetVariants, sheetMeta } from './sheet.variants'; + +@Component({ + selector: 'docs-sheet', + imports: [ComponentPreview], + template: ` + + + ` +}) +export class Sheet { + sheetMeta = sheetMeta; + sheetVariants = sheetVariants; +} diff --git a/projects/docs/src/app/pages/docs/components/sheet/sheet.variants.ts b/projects/docs/src/app/pages/docs/components/sheet/sheet.variants.ts new file mode 100644 index 0000000..9fe451a --- /dev/null +++ b/projects/docs/src/app/pages/docs/components/sheet/sheet.variants.ts @@ -0,0 +1,377 @@ +import { Component } from '@angular/core'; +import { UiSheet, UiSheetDescription, UiSheetFooter, UiSheetHeader, UiSheetOverlay, UiSheetTitle, UiSheetTrigger } from 'ui'; +import { UiButton } from 'ui'; +import { UiInput } from 'ui'; +import { UiLabel } from 'ui'; +import { IVariant, IComponentMeta } from '@components/component-preview/component-preview'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { lucideX } from '@ng-icons/lucide'; + +@Component({ + selector: 'sheet-default-example', + template: ` + + +
+
+
+

Edit profile

+

Make changes to your profile here. Click save when you're done.

+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ `, + providers: [provideIcons({ lucideX })], + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton, UiInput, UiLabel, NgIcon] +}) +export class SheetDefaultExample {} + +@Component({ + selector: 'sheet-side-right-example', + template: ` + + +
+
+
+

Right Side Sheet

+

This sheet slides in from the right side of the screen.

+
+
+

Content goes here...

+
+
+ +
+
+
+
+ `, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton] +}) +export class SheetSideRightExample {} + +@Component({ + selector: 'sheet-side-left-example', + template: ` + + +
+
+
+

Left Side Sheet

+

This sheet slides in from the left side of the screen.

+
+
+

Content goes here...

+
+
+ +
+
+
+
+ `, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton] +}) +export class SheetSideLeftExample {} + +@Component({ + selector: 'sheet-side-top-example', + template: ` + + +
+
+
+

Top Side Sheet

+

This sheet slides in from the top of the screen.

+
+
+

Content goes here...

+
+
+ +
+
+
+
+ `, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton] +}) +export class SheetSideTopExample {} + +@Component({ + selector: 'sheet-side-bottom-example', + template: ` + + +
+
+
+

Bottom Side Sheet

+

This sheet slides in from the bottom of the screen.

+
+
+

Content goes here...

+
+
+ +
+
+
+
+ `, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton] +}) +export class SheetSideBottomExample {} + +@Component({ + selector: 'sheet-size-example', + template: ` + + +
+
+
+

Custom Size Sheet

+

You can adjust the size of the sheet using CSS classes.

+
+
+

This sheet has custom width classes applied.

+
+
+ +
+
+
+
+ `, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton] +}) +export class SheetSizeExample {} + +export const sheetMeta: IComponentMeta = { + title: 'Sheet', + description: 'Extends the Dialog component to display content that complements the main content of the screen.', + installation: { + package: 'sheet', + import: `import { UiSheet, UiSheetTrigger, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetOverlay } from '@workspace/ui/directives/sheet';`, + usage: `
...
` + }, + api: { + props: [ + { name: 'uiSheetTrigger', type: 'TemplateRef', description: 'Template ref for sheet content.' }, + { name: 'uiSheetModal', type: 'boolean', description: 'Whether the sheet is modal.' }, + { name: 'side', type: "'right' | 'left' | 'top' | 'bottom'", description: 'The edge of the screen where the sheet will appear.' }, + { name: 'class', type: 'string', description: 'Additional CSS classes.' } + ] + } +}; + +export const sheetVariants: IVariant[] = [ + { + title: 'Default', + description: 'Basic sheet with header, content, and footer. Slides in from the right by default.', + code: `import { UiSheet, UiSheetTrigger, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetOverlay } from '@workspace/ui/directives/sheet'; +import { UiButton } from '@workspace/ui/directives/button'; +import { UiInput } from '@workspace/ui/directives/input'; +import { UiLabel } from '@workspace/ui/directives/label'; + +@Component({ + selector: 'sheet-default-example', + template: \` + + +
+
+
+

Edit profile

+

Make changes to your profile here. Click save when you're done.

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ \`, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton, UiInput, UiLabel] +}) +export class SheetDefaultExample {}`, + component: SheetDefaultExample + }, + { + title: 'Side', + description: 'Use the side property to indicate the edge of the screen where the component will appear.', + code: `// Right side (default) +
...
+ +// Left side +
...
+ +// Top side +
...
+ +// Bottom side +
...
`, + component: SheetSideRightExample + }, + { + title: 'Left Side', + description: 'Sheet that slides in from the left side of the screen.', + code: `import { UiSheet, UiSheetTrigger, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetOverlay } from '@workspace/ui/directives/sheet'; +import { UiButton } from '@workspace/ui/directives/button'; + +@Component({ + selector: 'sheet-side-left-example', + template: \` + + +
+
+
+

Left Side Sheet

+

This sheet slides in from the left side of the screen.

+
+
+

Content goes here...

+
+
+ +
+
+
+
+ \`, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton] +}) +export class SheetSideLeftExample {}`, + component: SheetSideLeftExample + }, + { + title: 'Top Side', + description: 'Sheet that slides in from the top of the screen.', + code: `import { UiSheet, UiSheetTrigger, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetOverlay } from '@workspace/ui/directives/sheet'; +import { UiButton } from '@workspace/ui/directives/button'; + +@Component({ + selector: 'sheet-side-top-example', + template: \` + + +
+
+
+

Top Side Sheet

+

This sheet slides in from the top of the screen.

+
+
+

Content goes here...

+
+
+ +
+
+
+
+ \`, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton] +}) +export class SheetSideTopExample {}`, + component: SheetSideTopExample + }, + { + title: 'Bottom Side', + description: 'Sheet that slides in from the bottom of the screen.', + code: `import { UiSheet, UiSheetTrigger, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetOverlay } from '@workspace/ui/directives/sheet'; +import { UiButton } from '@workspace/ui/directives/button'; + +@Component({ + selector: 'sheet-side-bottom-example', + template: \` + + +
+
+
+

Bottom Side Sheet

+

This sheet slides in from the bottom of the screen.

+
+
+

Content goes here...

+
+
+ +
+
+
+
+ \`, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton] +}) +export class SheetSideBottomExample {}`, + component: SheetSideBottomExample + }, + { + title: 'Size', + description: 'You can adjust the size of the sheet using CSS classes.', + code: `import { UiSheet, UiSheetTrigger, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetOverlay } from '@workspace/ui/directives/sheet'; +import { UiButton } from '@workspace/ui/directives/button'; + +@Component({ + selector: 'sheet-size-example', + template: \` + + +
+
+
+

Custom Size Sheet

+

You can adjust the size of the sheet using CSS classes.

+
+
+

This sheet has custom width classes applied.

+
+
+ +
+
+
+
+ \`, + imports: [UiSheet, UiSheetHeader, UiSheetTitle, UiSheetDescription, UiSheetFooter, UiSheetTrigger, UiSheetOverlay, UiButton] +}) +export class SheetSizeExample {}`, + component: SheetSizeExample + } +]; diff --git a/projects/docs/src/app/shared/components/sidebar/sidebar.ts b/projects/docs/src/app/shared/components/sidebar/sidebar.ts index 87bee02..90b674e 100644 --- a/projects/docs/src/app/shared/components/sidebar/sidebar.ts +++ b/projects/docs/src/app/shared/components/sidebar/sidebar.ts @@ -59,6 +59,7 @@ export class Sidebar { { name: 'Card', path: 'card' }, { name: 'Checkbox', path: 'checkbox' }, { name: 'Dialog', path: 'dialog' }, + { name: 'Sheet', path: 'sheet' }, { name: 'Dropdown Menu', path: 'dropdown-menu' }, { name: 'Label', path: 'label' }, { name: 'Input', path: 'input' }, diff --git a/registry.json b/registry.json index 3a37d48..a3f36d9 100644 --- a/registry.json +++ b/registry.json @@ -70,6 +70,19 @@ ], "dependencies": [] }, + { + "name": "sheet", + "type": "registry:directive", + "title": "Sheet Directive", + "description": "A sheet directive for displaying content that slides in from different sides of the screen, extending the Dialog component", + "files": [ + { + "path": "directives/sheet.ts", + "type": "registry:directive" + } + ], + "dependencies": [] + }, { "name": "popover", "type": "registry:directive",