From 47deb998033f9b0fd5d29cb6c995825098173b38 Mon Sep 17 00:00:00 2001 From: Mohammad Javed Date: Sat, 4 Oct 2025 16:37:44 +0530 Subject: [PATCH 1/4] UI(badge): Convert alert-dialog directive to component --- .../alert-dialog/alert-dialog.variants.ts | 148 ++++++++++-------- projects/ui/src/directives/alert-dialog.ts | 91 +++++------ 2 files changed, 129 insertions(+), 110 deletions(-) diff --git a/projects/docs/src/app/pages/docs/components/alert-dialog/alert-dialog.variants.ts b/projects/docs/src/app/pages/docs/components/alert-dialog/alert-dialog.variants.ts index a4f23d8..80213a3 100644 --- a/projects/docs/src/app/pages/docs/components/alert-dialog/alert-dialog.variants.ts +++ b/projects/docs/src/app/pages/docs/components/alert-dialog/alert-dialog.variants.ts @@ -10,21 +10,25 @@ import { lucideTriangleAlert } from '@ng-icons/lucide'; template: ` -
-
-
-

Are you absolutely sure?

-

+ + + + Are you absolutely sure? + This action cannot be undone. This will permanently delete your account and remove your data from our servers. -

-
-
- - -
-
-
+ + + + + + + + + + + +
`, imports: [UiAlertDialog, UiAlertDialogHeader, UiAlertDialogTitle, UiAlertDialogDescription, UiAlertDialogFooter, UiAlertDialogTrigger, UiAlertDialogOverlay, UiAlertDialogCancel, UiAlertDialogAction, UiButton] @@ -36,26 +40,30 @@ export class AlertDialogDefaultExample {} template: ` -
-
-
-
- - - -

Delete Account

-
-

- This action cannot be undone. This will permanently delete your account - and remove your data from our servers. -

-
-
- - -
-
-
+ + + +
+ + + + Delete Account +
+ + This action cannot be undone. This will permanently delete your account + and remove your data from our servers. + +
+ + + + + + + + +
+
`, providers: [provideIcons({ lucideTriangleAlert })], @@ -68,7 +76,7 @@ export const alertDialogMeta: IComponentMeta = { description: 'A modal dialog that interrupts the user with important content and expects a response.', installation: { package: 'alert-dialog', - import: `import { UiAlertDialog, UiAlertDialogTrigger, UiAlertDialogHeader, UiAlertDialogTitle, UiAlertDialogDescription, UiAlertDialogFooter, UiAlertDialogOverlay, UiAlertDialogCancel, UiAlertDialogAction } from '@workspace/ui/directives/alert-dialog';`, + import: `import { UiAlertDialog, UiAlertDialogTrigger, UiAlertDialogHeader, UiAlertDialogTitle, UiAlertDialogDescription, UiAlertDialogFooter, UiAlertDialogOverlay, UiAlertDialogCancel, UiAlertDialogAction } from '@workspace/ui/alert-dialog';`, usage: `
...
` }, api: { @@ -86,29 +94,33 @@ export const alertDialogVariants: IVariant[] = [ { title: 'Default', description: 'Basic alert dialog with header, description, and action buttons.', - code: `import { UiAlertDialog, UiAlertDialogTrigger, UiAlertDialogHeader, UiAlertDialogTitle, UiAlertDialogDescription, UiAlertDialogFooter, UiAlertDialogOverlay, UiAlertDialogCancel, UiAlertDialogAction } from '@workspace/ui/directives/alert-dialog'; -import { UiButton } from '@workspace/ui/directives/button'; + code: `import { UiAlertDialog, UiAlertDialogTrigger, UiAlertDialogHeader, UiAlertDialogTitle, UiAlertDialogDescription, UiAlertDialogFooter, UiAlertDialogOverlay, UiAlertDialogCancel, UiAlertDialogAction } from '@workspace/ui/alert-dialog'; +import { UiButton } from '@workspace/ui/button'; @Component({ selector: 'alert-dialog-default-example', template: \` -
-
-
-

Are you absolutely sure?

-

+ + + + Are you absolutely sure? + This action cannot be undone. This will permanently delete your account and remove your data from our servers. -

-
-
- - -
-
-
+ + + + + + + + + + + +
\`, imports: [UiAlertDialog, UiAlertDialogHeader, UiAlertDialogTitle, UiAlertDialogDescription, UiAlertDialogFooter, UiAlertDialogTrigger, UiAlertDialogOverlay, UiAlertDialogCancel, UiAlertDialogAction, UiButton] @@ -119,8 +131,8 @@ export class AlertDialogDefaultExample {}`, { title: 'Destructive', description: 'Alert dialog with destructive action styling and warning icon.', - code: `import { UiAlertDialog, UiAlertDialogTrigger, UiAlertDialogHeader, UiAlertDialogTitle, UiAlertDialogDescription, UiAlertDialogFooter, UiAlertDialogOverlay, UiAlertDialogCancel, UiAlertDialogAction } from '@workspace/ui/directives/alert-dialog'; -import { UiButton } from '@workspace/ui/directives/button'; + code: `import { UiAlertDialog, UiAlertDialogTrigger, UiAlertDialogHeader, UiAlertDialogTitle, UiAlertDialogDescription, UiAlertDialogFooter, UiAlertDialogOverlay, UiAlertDialogCancel, UiAlertDialogAction } from '@workspace/ui/alert-dialog'; +import { UiButton } from '@workspace/ui/button'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucideTriangle } from '@ng-icons/lucide'; @@ -129,24 +141,30 @@ import { lucideTriangle } from '@ng-icons/lucide'; template: \` -
-
-
+ + +
- -

Delete Account

+ + + + Delete Account
-

+ This action cannot be undone. This will permanently delete your account and remove your data from our servers. -

-
-
- - -
-
-
+ + + + + + + + + + + +
\`, providers: [provideIcons({ lucideTriangle })], diff --git a/projects/ui/src/directives/alert-dialog.ts b/projects/ui/src/directives/alert-dialog.ts index ff18110..9e17afc 100644 --- a/projects/ui/src/directives/alert-dialog.ts +++ b/projects/ui/src/directives/alert-dialog.ts @@ -1,4 +1,4 @@ -import { computed, Directive, input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"; import { tv } from "tailwind-variants"; import { NgpDialog, NgpDialogDescription, NgpDialogOverlay, NgpDialogTitle, NgpDialogTrigger, provideDialogConfig } from "ng-primitives/dialog"; @@ -18,82 +18,84 @@ const alertDialogVariants = tv({ const { alertDialog, alertDialogOverlay, alertDialogHeader, alertDialogFooter, alertDialogTitle, alertDialogDescription, alertDialogTrigger, alertDialogCancel, alertDialogAction } = alertDialogVariants(); -@Directive({ - selector: '[uiAlertDialog]', +@Component({ + selector: 'ui-alert-dialog', exportAs: 'uiAlertDialog', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' }, - hostDirectives: [ - { - directive: NgpDialog, - inputs: [ - 'ngpDialogRole: uiAlertDialogRole', - 'ngpDialogModal: uiAlertDialogModal' - ] - } - ] + hostDirectives: [NgpDialog], + template: `` }) export class UiAlertDialog { inputClass = input('', { alias: 'class' }); computedClass = computed(() => alertDialog({ class: this.inputClass() })); } -@Directive({ - selector: '[uiAlertDialogHeader]', +@Component({ + selector: 'ui-alert-dialog-header', exportAs: 'uiAlertDialogHeader', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' }, - hostDirectives: [] + hostDirectives: [], + template: `` }) export class UiAlertDialogHeader { inputClass = input('', { alias: 'class' }); computedClass = computed(() => alertDialogHeader({ class: this.inputClass() })); } -@Directive({ - selector: '[uiAlertDialogTitle]', +@Component({ + selector: 'ui-alert-dialog-title', exportAs: 'uiAlertDialogTitle', host: { '[class]': 'computedClass()' }, - hostDirectives: [NgpDialogTitle] + hostDirectives: [NgpDialogTitle], + template: `` }) export class UiAlertDialogTitle { inputClass = input('', { alias: 'class' }); computedClass = computed(() => alertDialogTitle({ class: this.inputClass() })); } -@Directive({ - selector: '[uiAlertDialogDescription]', +@Component({ + selector: 'ui-alert-dialog-description', exportAs: 'uiAlertDialogDescription', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' }, - hostDirectives: [NgpDialogDescription] + hostDirectives: [NgpDialogDescription], + template: `` }) export class UiAlertDialogDescription { inputClass = input('', { alias: 'class' }); computedClass = computed(() => alertDialogDescription({ class: this.inputClass() })); } -@Directive({ - selector: '[uiAlertDialogFooter]', +@Component({ + selector: 'ui-alert-dialog-footer', exportAs: 'uiAlertDialogFooter', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' }, - hostDirectives: [] + hostDirectives: [], + template: `` }) export class UiAlertDialogFooter { inputClass = input('', { alias: 'class' }); computedClass = computed(() => alertDialogFooter({ class: this.inputClass() })); } -@Directive({ +@Component({ selector: '[uiAlertDialogTrigger]', exportAs: 'uiAlertDialogTrigger', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' }, @@ -101,59 +103,58 @@ export class UiAlertDialogFooter { { directive: NgpDialogTrigger, inputs: [ - 'ngpDialogTrigger: uiAlertDialogTrigger', - 'ngpDialogTriggerCloseOnEscape: uiAlertDialogTriggerCloseOnEscape' + 'ngpDialogTrigger: uiAlertDialogTrigger' ] } ], - providers: [provideDialogConfig({ closeOnEscape: false })] + providers: [provideDialogConfig({ closeOnEscape: false })], + template: `` }) export class UiAlertDialogTrigger { inputClass = input('', { alias: 'class' }); computedClass = computed(() => alertDialogTrigger({ class: this.inputClass() })); } -@Directive({ - selector: '[uiAlertDialogOverlay]', +@Component({ + selector: 'ui-alert-dialog-overlay', exportAs: 'uiAlertDialogOverlay', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' }, - hostDirectives: [ - { - directive: NgpDialogOverlay, - inputs: [ - 'ngpDialogOverlayCloseOnClick: uiAlertDialogOverlayCloseOnClick' - ] - } - ], - providers: [provideDialogConfig({ closeOnClick: false })] + hostDirectives: [NgpDialogOverlay], + providers: [provideDialogConfig({ closeOnClick: false })], + template: `` }) export class UiAlertDialogOverlay { inputClass = input('', { alias: 'class' }); computedClass = computed(() => alertDialogOverlay({ class: this.inputClass() })); } -@Directive({ - selector: '[uiAlertDialogCancel]', +@Component({ + selector: 'ui-alert-dialog-cancel', exportAs: 'uiAlertDialogCancel', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' }, - hostDirectives: [] + hostDirectives: [], + template: `` }) export class UiAlertDialogCancel { inputClass = input('', { alias: 'class' }); computedClass = computed(() => alertDialogCancel({ class: this.inputClass() })); } -@Directive({ - selector: '[uiAlertDialogAction]', +@Component({ + selector: 'ui-alert-dialog-action', exportAs: 'uiAlertDialogAction', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' }, - hostDirectives: [] + hostDirectives: [], + template: `` }) export class UiAlertDialogAction { inputClass = input('', { alias: 'class' }); From 204e5eac0e5a5651ce525100e975fd3ac4ccf606 Mon Sep 17 00:00:00 2001 From: Mohammad Javed Date: Sat, 4 Oct 2025 22:32:15 +0530 Subject: [PATCH 2/4] UI(breadcrumb): Convert breadcrumb directive to component --- .../breadcrumb/breadcrumb.variants.ts | 268 +++++++++--------- projects/ui/src/directives/breadcrumb.ts | 65 +++-- 2 files changed, 169 insertions(+), 164 deletions(-) diff --git a/projects/docs/src/app/pages/docs/components/breadcrumb/breadcrumb.variants.ts b/projects/docs/src/app/pages/docs/components/breadcrumb/breadcrumb.variants.ts index 9012def..2afca21 100644 --- a/projects/docs/src/app/pages/docs/components/breadcrumb/breadcrumb.variants.ts +++ b/projects/docs/src/app/pages/docs/components/breadcrumb/breadcrumb.variants.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { UiBreadcrumb, UiBreadcrumbItem, UiBreadcrumbLink, UiBreadcrumbPage, UiBreadcrumbSeparator, UiBreadcrumbList, UiBreadcrumbEllipsis, UiDropdownMenu, UiDropdownMenuTrigger, UiDropdownMenuItem } from 'ui'; +import { UiBreadcrumb, UiBreadcrumbItem, UiBreadcrumbLink, UiBreadcrumbPage, UiBreadcrumbSeparator, UiBreadcrumbList, UiDropdownMenu, UiDropdownMenuTrigger, UiDropdownMenuItem, UiBreadcrumbEllipsis } from 'ui'; import { IVariant, IComponentMeta } from '@components/component-preview/component-preview'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucideChevronRight, lucideHouse, lucideFileText, lucideSettings, lucideEllipsis } from '@ng-icons/lucide'; @@ -9,25 +9,25 @@ import { lucideChevronRight, lucideHouse, lucideFileText, lucideSettings, lucide selector: 'breadcrumb-default-example', standalone: true, template: ` - + + + Breadcrumb + + + `, imports: [UiBreadcrumb, UiBreadcrumbList, UiBreadcrumbItem, UiBreadcrumbLink, UiBreadcrumbPage, UiBreadcrumbSeparator, NgIcon], providers: [provideIcons({ lucideChevronRight })] @@ -38,34 +38,34 @@ export class BreadcrumbDefaultExample {} selector: 'breadcrumb-with-icons-example', standalone: true, template: ` - + + + + `, imports: [UiBreadcrumb, UiBreadcrumbList, UiBreadcrumbItem, UiBreadcrumbLink, UiBreadcrumbPage, UiBreadcrumbSeparator, NgIcon], providers: [provideIcons({ lucideChevronRight, lucideHouse, lucideFileText, lucideSettings })] @@ -76,40 +76,38 @@ export class BreadcrumbWithIconsExample {} selector: 'breadcrumb-collapsed-example', standalone: true, template: ` - + + + Breadcrumb + + + `, imports: [UiBreadcrumb, UiBreadcrumbList, UiBreadcrumbItem, UiBreadcrumbLink, UiBreadcrumbPage, UiBreadcrumbSeparator, UiBreadcrumbEllipsis, NgIcon, UiDropdownMenu, UiDropdownMenuTrigger, UiDropdownMenuItem], providers: [provideIcons({ lucideChevronRight, lucideEllipsis })] @@ -134,25 +132,25 @@ import { lucideChevronRight } from '@ng-icons/lucide'; @Component({ selector: 'breadcrumb-default-example', template: \` - + + + Breadcrumb + + + \`, imports: [UiBreadcrumb, UiBreadcrumbList, UiBreadcrumbItem, UiBreadcrumbLink, UiBreadcrumbPage, UiBreadcrumbSeparator, NgIcon], providers: [provideIcons({ lucideChevronRight })] @@ -177,34 +175,34 @@ import { lucideChevronRight, lucideHouse, lucideFileText, lucideSettings } from @Component({ selector: 'breadcrumb-with-icons-example', template: \` - + + + + \`, imports: [UiBreadcrumb, UiBreadcrumbList, UiBreadcrumbItem, UiBreadcrumbLink, UiBreadcrumbPage, UiBreadcrumbSeparator, NgIcon], providers: [provideIcons({ lucideChevronRight, lucideHouse, lucideFileText, lucideSettings })] @@ -231,18 +229,16 @@ import { lucideChevronRight, lucideEllipsis } from '@ng-icons/lucide'; @Component({ selector: 'breadcrumb-collapsed-example', template: \` - + + + Breadcrumb + + + \`, imports: [UiBreadcrumb, UiBreadcrumbList, UiBreadcrumbItem, UiBreadcrumbLink, UiBreadcrumbPage, UiBreadcrumbSeparator, UiBreadcrumbEllipsis, NgIcon, UiDropdownMenu, UiDropdownMenuTrigger, UiDropdownMenuItem], providers: [provideIcons({ lucideChevronRight, lucideEllipsis })] @@ -281,19 +277,19 @@ export const breadcrumbMeta: IComponentMeta = { package: 'breadcrumb', import: `import { UiBreadcrumb, UiBreadcrumbList, UiBreadcrumbItem, UiBreadcrumbLink, UiBreadcrumbPage, UiBreadcrumbSeparator, UiBreadcrumbEllipsis } from '@workspace/ui/directives/breadcrumb'; import { UiDropdownMenu, UiDropdownMenuTrigger, UiDropdownMenuItem } from '@workspace/ui/directives/dropdown-menu';`, - usage: `` + + + Current + + +` }, api: { props: [ diff --git a/projects/ui/src/directives/breadcrumb.ts b/projects/ui/src/directives/breadcrumb.ts index 00a40ec..0dbb116 100644 --- a/projects/ui/src/directives/breadcrumb.ts +++ b/projects/ui/src/directives/breadcrumb.ts @@ -1,5 +1,7 @@ -import { computed, Directive, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { tv } from 'tailwind-variants'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { lucideEllipsis } from '@ng-icons/lucide'; const breadcrumbVariants = tv({ slots: { @@ -15,84 +17,91 @@ const breadcrumbVariants = tv({ const { breadcrumb, breadcrumbList, breadcrumbItem, breadcrumbLink, breadcrumbPage, breadcrumbSeparator, breadcrumbEllipsis } = breadcrumbVariants(); -@Directive({ - selector: '[uiBreadcrumb]', +@Component({ + selector: 'ui-breadcrumb', exportAs: 'uiBreadcrumb', - host: { - '[class]': 'computedClass()' - } + changeDetection: ChangeDetectionStrategy.OnPush, + template: `` }) export class UiBreadcrumb { inputClass = input('', { alias: 'class' }); computedClass = computed(() => breadcrumb({ class: this.inputClass() })); } -@Directive({ - selector: '[uiBreadcrumbList]', +@Component({ + selector: 'ui-breadcrumb-list', exportAs: 'uiBreadcrumbList', - host: { - '[class]': 'computedClass()' - } + changeDetection: ChangeDetectionStrategy.OnPush, + template: `
` }) export class UiBreadcrumbList { inputClass = input('', { alias: 'class' }); computedClass = computed(() => breadcrumbList({ class: this.inputClass() })); } -@Directive({ - selector: '[uiBreadcrumbItem]', +@Component({ + selector: 'ui-breadcrumb-item', exportAs: 'uiBreadcrumbItem', - host: { - '[class]': 'computedClass()' - } + changeDetection: ChangeDetectionStrategy.OnPush, + template: `
  • ` }) export class UiBreadcrumbItem { inputClass = input('', { alias: 'class' }); computedClass = computed(() => breadcrumbItem({ class: this.inputClass() })); } -@Directive({ - selector: '[uiBreadcrumbLink]', +@Component({ + selector: 'ui-breadcrumb-link', exportAs: 'uiBreadcrumbLink', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiBreadcrumbLink { inputClass = input('', { alias: 'class' }); computedClass = computed(() => breadcrumbLink({ class: this.inputClass() })); } -@Directive({ - selector: '[uiBreadcrumbPage]', +@Component({ + selector: 'ui-breadcrumb-page', exportAs: 'uiBreadcrumbPage', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiBreadcrumbPage { inputClass = input('', { alias: 'class' }); computedClass = computed(() => breadcrumbPage({ class: this.inputClass() })); } -@Directive({ - selector: '[uiBreadcrumbSeparator]', +@Component({ + selector: 'ui-breadcrumb-separator', exportAs: 'uiBreadcrumbSeparator', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiBreadcrumbSeparator { inputClass = input('', { alias: 'class' }); computedClass = computed(() => breadcrumbSeparator({ class: this.inputClass() })); } -@Directive({ - selector: '[uiBreadcrumbEllipsis]', +@Component({ + selector: 'ui-breadcrumb-ellipsis', exportAs: 'uiBreadcrumbEllipsis', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: ``, + imports: [NgIcon], + providers: [provideIcons({ lucideEllipsis })] }) export class UiBreadcrumbEllipsis { inputClass = input('', { alias: 'class' }); From be8c94285c18ff68fc66a2a930786fc54c0389c2 Mon Sep 17 00:00:00 2001 From: Mohammad Javed Date: Sat, 4 Oct 2025 22:41:30 +0530 Subject: [PATCH 3/4] UI(card): Convert card directives to components --- .../docs/components/card/card.variants.ts | 62 +++++++++---------- projects/ui/src/directives/card.ts | 58 ++++++++++------- 2 files changed, 67 insertions(+), 53 deletions(-) diff --git a/projects/docs/src/app/pages/docs/components/card/card.variants.ts b/projects/docs/src/app/pages/docs/components/card/card.variants.ts index 60c60d0..28a3037 100644 --- a/projects/docs/src/app/pages/docs/components/card/card.variants.ts +++ b/projects/docs/src/app/pages/docs/components/card/card.variants.ts @@ -7,17 +7,17 @@ import { IVariant, IComponentMeta } from '@components/component-preview/componen @Component({ selector: 'card-default-example', template: ` -
    -
    -
    Login to your account
    -
    + + + Login to your account + Enter your email below to login to your account -
    -
    + + -
    -
    -
    + + +
    @@ -35,16 +35,16 @@ import { IVariant, IComponentMeta } from '@components/component-preview/componen
    -
    -
    + + -
    -
    + + `, imports: [UiCard, UiCardHeader, UiCardTitle, UiCardDescription, UiCardAction, UiCardContent, UiCardFooter, UiLabel, UiInput, UiButton, UiFormField], host: { @@ -81,26 +81,26 @@ export const cardVariants: IVariant[] = [ title: 'Default', description: 'Login to your account', code: `import { Component } from '@angular/core'; -import { UiCard, UiCardAction, UiCardContent, UiCardDescription, UiCardFooter, UiCardHeader, UiCardTitle } from '@workspace/ui/directives/card'; -import { UiInput } from '@workspace/ui/directives/input'; -import { UiLabel } from '@workspace/ui/directives/label'; -import { UiButton } from '@workspace/ui/directives/button'; -import { UiFormField } from '@workspace/ui/directives/form-field'; +import { UiCard, UiCardAction, UiCardContent, UiCardDescription, UiCardFooter, UiCardHeader, UiCardTitle } from '@workspace/ui/card'; +import { UiInput } from '@workspace/ui/input'; +import { UiLabel } from '@workspace/ui/label'; +import { UiButton } from '@workspace/ui/button'; +import { UiFormField } from '@workspace/ui/form-field'; @Component({ selector: 'card-default-example', template: \` -
    -
    -
    Login to your account
    -
    + + + Login to your account + Enter your email below to login to your account -
    -
    + + -
    -
    -
    + + +
    @@ -118,16 +118,16 @@ import { UiFormField } from '@workspace/ui/directives/form-field';
    -
    -
    + + -
    -
    + + \`, imports: [UiCard, UiCardHeader, UiCardTitle, UiCardDescription, UiCardAction, UiCardContent, UiCardFooter, UiLabel, UiInput, UiButton, UiFormField], host: { diff --git a/projects/ui/src/directives/card.ts b/projects/ui/src/directives/card.ts index 944f015..784b23e 100644 --- a/projects/ui/src/directives/card.ts +++ b/projects/ui/src/directives/card.ts @@ -1,4 +1,4 @@ -import { computed, Directive, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { tv } from 'tailwind-variants'; const badgeVariants = tv({ @@ -15,84 +15,98 @@ const badgeVariants = tv({ const { card, cardHeader, cardTitle, cardDescription, cardAction, cardContent, cardFooter } = badgeVariants(); -@Directive({ - selector: '[uiCard]', +@Component({ + selector: 'ui-card', exportAs: 'uiCard', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiCard { inputClass = input('', { alias: 'class' }); computedClass = computed(() => card({ class: this.inputClass() })); } -@Directive({ - selector: '[uiCardHeader]', +@Component({ + selector: 'ui-card-header', exportAs: 'uiCardHeader', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiCardHeader { inputClass = input('', { alias: 'class' }); computedClass = computed(() => cardHeader({ class: this.inputClass() })); } -@Directive({ - selector: '[uiCardTitle]', +@Component({ + selector: 'ui-card-title', exportAs: 'uiCardTitle', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiCardTitle { inputClass = input('', { alias: 'class' }); computedClass = computed(() => cardTitle({ class: this.inputClass() })); } -@Directive({ - selector: '[uiCardDescription]', +@Component({ + selector: 'ui-card-description', exportAs: 'uiCardDescription', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiCardDescription { inputClass = input('', { alias: 'class' }); computedClass = computed(() => cardDescription({ class: this.inputClass() })); } -@Directive({ - selector: '[uiCardAction]', +@Component({ + selector: 'ui-card-action', exportAs: 'uiCardAction', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiCardAction { inputClass = input('', { alias: 'class' }); computedClass = computed(() => cardAction({ class: this.inputClass() })); } -@Directive({ - selector: '[uiCardContent]', +@Component({ + selector: 'ui-card-content', exportAs: 'uiCardContent', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiCardContent { inputClass = input('', { alias: 'class' }); computedClass = computed(() => cardContent({ class: this.inputClass() })); } -@Directive({ - selector: '[uiCardFooter]', +@Component({ + selector: 'ui-card-footer', exportAs: 'uiCardFooter', + changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'computedClass()' - } + }, + template: `` }) export class UiCardFooter { inputClass = input('', { alias: 'class' }); From 1e556785a97413121ca31e1eb06f70a943b7fcda Mon Sep 17 00:00:00 2001 From: Mohammad Javed Date: Mon, 6 Oct 2025 23:23:24 +0530 Subject: [PATCH 4/4] UI(checkbox): Convert checkbox directive to components --- .../components/checkbox/checkbox.variants.ts | 522 +++++++----------- projects/ui/src/directives/checkbox.ts | 43 +- projects/ui/src/directives/form-field.ts | 2 +- projects/ui/src/directives/label.ts | 2 +- 4 files changed, 225 insertions(+), 344 deletions(-) diff --git a/projects/docs/src/app/pages/docs/components/checkbox/checkbox.variants.ts b/projects/docs/src/app/pages/docs/components/checkbox/checkbox.variants.ts index 76a480b..b01c3a6 100644 --- a/projects/docs/src/app/pages/docs/components/checkbox/checkbox.variants.ts +++ b/projects/docs/src/app/pages/docs/components/checkbox/checkbox.variants.ts @@ -1,7 +1,5 @@ -import { Component, model } from '@angular/core'; +import { Component, signal } from '@angular/core'; import { IVariant, IComponentMeta } from '@components/component-preview/component-preview'; -import { NgIcon, provideIcons } from '@ng-icons/core'; -import { lucideCheck } from '@ng-icons/lucide'; import { UiCheckbox, UiLabel, UiButton, UiFormField, UiDescription } from 'ui'; // Default checkbox example @@ -10,19 +8,11 @@ import { UiCheckbox, UiLabel, UiButton, UiFormField, UiDescription } from 'ui'; template: `
    - +
    - +

    @@ -31,23 +21,13 @@ import { UiCheckbox, UiLabel, UiButton, UiFormField, UiDescription } from 'ui';

    - +
    - +

    Enable notifications @@ -61,17 +41,19 @@ import { UiCheckbox, UiLabel, UiButton, UiFormField, UiDescription } from 'ui';

    `, standalone: true, - imports: [UiCheckbox, NgIcon, UiLabel, UiFormField, UiDescription], - providers: [provideIcons({ lucideCheck })], + imports: [UiCheckbox, UiLabel, UiFormField, UiDescription], host: { 'class': 'w-2/3 space-y-6 mx-auto' } }) export class CheckboxDefaultExample { - termsChecked = model(false); - termsWithDescChecked = model(true); - disabledChecked = model(false); - notificationsChecked = model(true); + termsChecked = signal(false); + termsWithDescChecked = signal(true); + disabledChecked = signal(false); + notificationsChecked = signal(true); + termsCheckedChange(event: boolean) { + console.log('termsCheckedChange', event); + } } // Checkbox with label example @@ -79,20 +61,15 @@ export class CheckboxDefaultExample { selector: 'checkbox-with-label-example', template: `
    - +
    `, standalone: true, - imports: [UiCheckbox, UiLabel, NgIcon, UiFormField], - providers: [provideIcons({ lucideCheck })] + imports: [UiCheckbox, UiLabel, UiFormField] }) export class CheckboxWithLabelExample { - checked = model(true); + checked = signal(true); } // Checkbox with description example @@ -101,11 +78,7 @@ export class CheckboxDefaultExample { template: `
    - +

    @@ -116,11 +89,10 @@ export class CheckboxDefaultExample {

    `, standalone: true, - imports: [UiCheckbox, UiLabel, NgIcon, UiFormField, UiDescription], - providers: [provideIcons({ lucideCheck })] + imports: [UiCheckbox, UiLabel, UiFormField, UiDescription] }) export class CheckboxWithDescriptionExample { - checked = model(true); + checked = signal(true); } // Disabled checkbox example @@ -129,24 +101,19 @@ export class CheckboxWithDescriptionExample { template: `
    - +
    `, standalone: true, - imports: [UiCheckbox, UiLabel, NgIcon, UiFormField], - providers: [provideIcons({ lucideCheck })], + imports: [UiCheckbox, UiLabel, UiFormField], host: { 'class': 'w-2/3 space-y-6 mx-auto' } }) export class CheckboxDisabledExample { - checked = model(false); + checked = signal(false); } // Checkbox with custom styling example @@ -155,12 +122,9 @@ export class CheckboxDisabledExample { template: `
    - +

    Enable notifications @@ -173,11 +137,10 @@ export class CheckboxDisabledExample {

    `, standalone: true, - imports: [UiCheckbox, NgIcon, UiFormField, UiDescription, UiLabel], - providers: [provideIcons({ lucideCheck })] + imports: [UiCheckbox, UiFormField, UiDescription, UiLabel] }) export class CheckboxCustomStylingExample { - checked = model(true); + checked = signal(true); } // Form with multiple checkboxes example @@ -196,66 +159,42 @@ export class CheckboxCustomStylingExample {
    - +
    - +
    - +
    - +
    - +
    - +
    @@ -278,19 +217,18 @@ export class CheckboxCustomStylingExample {
    `, standalone: true, - imports: [UiCheckbox, UiLabel, UiButton, NgIcon, UiFormField], - providers: [provideIcons({ lucideCheck })], + imports: [UiCheckbox, UiLabel, UiButton, UiFormField], host: { 'class': 'w-2/3 space-y-6 mx-auto' } }) export class CheckboxFormExample { - recents = model(true); - home = model(true); - applications = model(false); - desktop = model(false); - downloads = model(false); - documents = model(false); + recents = signal(true); + home = signal(true); + applications = signal(false); + desktop = signal(false); + downloads = signal(false); + documents = signal(false); get selectedItems(): string[] { const items = [ @@ -317,12 +255,8 @@ export const checkboxMeta: IComponentMeta = { title: 'Checkbox', description: 'A control that allows the user to toggle between checked and not checked.', installation: { - import: `import { UiCheckbox } from '@workspace/ui/directives/checkbox';`, - usage: `` + import: `import { UiCheckbox } from '@workspace/ui/checkbox';`, + usage: `` }, api: { props: [ @@ -381,125 +315,97 @@ export const checkboxVariants: IVariant[] = [ { title: 'Default', description: 'A basic checkbox control.', - code: `import { Component, model } from '@angular/core'; -import { NgIcon, provideIcons } from '@ng-icons/core'; -import { lucideCheck } from '@ng-icons/lucide'; -import { UiCheckbox, UiLabel, UiFormField, UiDescription } from '@workspace/ui/directives/checkbox'; + code: `import { Component, signal } from '@angular/core'; +import { UiCheckbox, UiLabel, UiFormField, UiDescription } from '@workspace/ui/checkbox'; @Component({ selector: 'checkbox-default-example', template: \`
    -
    - - -
    -
    - -
    - -

    - By clicking this checkbox, you agree to the terms and conditions. -

    +
    + +
    -
    -
    - - -
    -
    -
    - -
    -

    - Enable notifications -

    -

    - You can enable or disable notifications at any time. -

    -
    +
    + +
    + +

    + By clicking this checkbox, you agree to the terms and conditions. +

    +
    +
    +
    + + +
    +
    +
    + +
    +

    + Enable notifications +

    +

    + You can enable or disable notifications at any time. +

    +
    +
    -
    \`, standalone: true, - imports: [UiCheckbox, NgIcon, UiLabel, UiFormField, UiDescription], - providers: [provideIcons({ lucideCheck })] + imports: [UiCheckbox, UiLabel, UiFormField, UiDescription], + host: { + 'class': 'w-2/3 space-y-6 mx-auto' + } }) export class CheckboxDefaultExample { - termsChecked = model(false); - termsWithDescChecked = model(true); - disabledChecked = model(false); - notificationsChecked = model(true); + termsChecked = signal(false); + termsWithDescChecked = signal(true); + disabledChecked = signal(false); + notificationsChecked = signal(true); + termsCheckedChange(event: boolean) { + console.log('termsCheckedChange', event); + } }`, component: CheckboxDefaultExample }, { title: 'With Label', description: 'A checkbox with a label.', - code: `import { Component, model } from '@angular/core'; -import { NgIcon, provideIcons } from '@ng-icons/core'; -import { lucideCheck } from '@ng-icons/lucide'; -import { UiCheckbox, UiLabel, NgIcon, UiFormField } from '@workspace/ui/directives/checkbox'; + code: `import { Component, signal } from '@angular/core'; +import { UiCheckbox, UiLabel, UiFormField } from '@workspace/ui/checkbox'; @Component({ - selector: 'checkbox-with-label-example', - template: \` -
    - - + selector: 'checkbox-with-label-example', + template: \` +
    + +
    - \`, - standalone: true, - imports: [UiCheckbox, UiLabel, NgIcon, UiFormField], - providers: [provideIcons({ lucideCheck })] -}) -export class CheckboxWithLabelExample { - checked = model(true); -}`, + \`, + standalone: true, + imports: [UiCheckbox, UiLabel, UiFormField] + }) + export class CheckboxWithLabelExample { + checked = signal(true); + }`, component: CheckboxWithLabelExample }, { title: 'With Description', description: 'A checkbox with additional descriptive text.', - code: `import { Component, model } from '@angular/core'; -import { NgIcon, provideIcons } from '@ng-icons/core'; -import { lucideCheck } from '@ng-icons/lucide'; -import { UiCheckbox, UiLabel, UiFormField, UiDescription } from '@workspace/ui/directives/checkbox'; + code: `import { Component, signal } from '@angular/core'; +import { UiCheckbox, UiLabel, UiFormField, UiDescription } from '@workspace/ui/checkbox'; @Component({ selector: 'checkbox-with-description-example', template: \`
    - +

    @@ -510,199 +416,164 @@ import { UiCheckbox, UiLabel, UiFormField, UiDescription } from '@workspace/ui/d

    \`, standalone: true, - imports: [UiCheckbox, UiLabel, NgIcon, UiFormField, UiDescription], - providers: [provideIcons({ lucideCheck })] + imports: [UiCheckbox, UiLabel, UiFormField, UiDescription] }) export class CheckboxWithDescriptionExample { - checked = model(true); + checked = signal(true); }`, component: CheckboxWithDescriptionExample }, { title: 'Disabled', description: 'A disabled checkbox that cannot be interacted with.', - code: `import { Component, model } from '@angular/core'; -import { NgIcon, provideIcons } from '@ng-icons/core'; -import { lucideCheck } from '@ng-icons/lucide'; -import { UiCheckbox, UiLabel, UiFormField } from '@workspace/ui/directives/checkbox'; + code: `import { Component, signal } from '@angular/core'; +import { UiCheckbox, UiLabel, UiFormField } from '@workspace/ui/checkbox'; @Component({ selector: 'checkbox-disabled-example', template: \` -
    -
    - +
    +
    +
    \`, standalone: true, - imports: [UiCheckbox, UiLabel, NgIcon, UiFormField], - providers: [provideIcons({ lucideCheck })] + imports: [UiCheckbox, UiLabel, UiFormField], + host: { + 'class': 'w-2/3 space-y-6 mx-auto' + } }) export class CheckboxDisabledExample { - checked = model(false); + checked = signal(false); }`, component: CheckboxDisabledExample }, { title: 'Custom Styling', description: 'A checkbox with custom colors and hover effects.', - code: `import { Component, model } from '@angular/core'; -import { NgIcon, provideIcons } from '@ng-icons/core'; -import { lucideCheck } from '@ng-icons/lucide'; -import { UiCheckbox, UiFormField, UiDescription } from '@workspace/ui/directives/checkbox'; + code: `import { Component, signal } from '@angular/core'; +import { UiCheckbox, UiFormField, UiDescription, UiLabel } from '@workspace/ui/checkbox'; @Component({ selector: 'checkbox-custom-styling-example', template: \` -
    +
    - +
    -

    - Enable notifications -

    -

    - You can enable or disable notifications at any time. -

    +

    + Enable notifications +

    +

    + You can enable or disable notifications at any time. +

    +
    -
    \`, standalone: true, - imports: [UiCheckbox, NgIcon, UiFormField, UiDescription], - providers: [provideIcons({ lucideCheck })] + imports: [UiCheckbox, UiFormField, UiDescription, UiLabel] }) export class CheckboxCustomStylingExample { - checked = model(true); + checked = signal(true); }`, component: CheckboxCustomStylingExample }, { title: 'Form with Multiple Checkboxes', description: 'Multiple checkboxes in a form with state management.', - code: `import { Component, model } from '@angular/core'; -import { NgIcon, provideIcons } from '@ng-icons/core'; -import { lucideCheck } from '@ng-icons/lucide'; -import { UiCheckbox, UiLabel, UiButton, UiFormField } from '@workspace/ui/directives/checkbox'; + code: `import { Component, signal } from '@angular/core'; +import { UiCheckbox, UiLabel, UiButton, UiFormField } from '@workspace/ui/checkbox'; @Component({ selector: 'checkbox-form-example', template: \` -
    -
    -

    Sidebar

    -

    - Select the items you want to display in the sidebar. -

    -
    - -
    -
    -
    - - -
    +
    +
    +
    +

    Sidebar

    +

    + Select the items you want to display in the sidebar. +

    -
    -
    - - +
    +
    +
    + + +
    -
    - -
    -
    - - + +
    +
    + + +
    -
    - -
    -
    - - + +
    +
    + + +
    -
    - -
    -
    - - + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + + +
    -
    -
    - - + + + @if (selectedItems.length > 0) { +
    + Selected: {{ selectedItems.join(', ') }}
    -
    + }
    - - - - @if (selectedItems.length > 0) { -
    - Selected: {{ selectedItems.join(', ') }} -
    - }
    \`, standalone: true, - imports: [UiCheckbox, UiLabel, UiButton, NgIcon, UiFormField], - providers: [provideIcons({ lucideCheck })] + imports: [UiCheckbox, UiLabel, UiButton, UiFormField], + host: { + 'class': 'w-2/3 space-y-6 mx-auto' + } }) export class CheckboxFormExample { - recents = model(true); - home = model(true); - applications = model(false); - desktop = model(false); - downloads = model(false); - documents = model(false); + recents = signal(true); + home = signal(true); + applications = signal(false); + desktop = signal(false); + downloads = signal(false); + documents = signal(false); get selectedItems(): string[] { const items = [ @@ -721,6 +592,7 @@ export class CheckboxFormExample { submitForm(): void { console.log('Selected items:', this.selectedItems); + // In a real app, you would submit this data } }`, component: CheckboxFormExample diff --git a/projects/ui/src/directives/checkbox.ts b/projects/ui/src/directives/checkbox.ts index 170bd99..48f941d 100644 --- a/projects/ui/src/directives/checkbox.ts +++ b/projects/ui/src/directives/checkbox.ts @@ -1,33 +1,42 @@ -import { computed, Directive, input } from "@angular/core"; +import { computed, Component, input, ChangeDetectionStrategy, model, booleanAttribute, output } from "@angular/core"; import { tv } from "tailwind-variants"; import { NgpCheckbox } from "ng-primitives/checkbox"; +import { NgIcon, provideIcons } from "@ng-icons/core"; +import { lucideCheck } from "@ng-icons/lucide"; +import { BooleanInput } from "@angular/cdk/coercion"; const checkboxVariants = tv({ base: "peer border-input dark:bg-input/30 data-[checked]:bg-primary data-[checked]:text-primary-foreground dark:data-[checked]:bg-primary data-[checked]:border-primary data-[focus-visible]:border-ring data-[focus-visible]:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none data-[focus-visible]:ring-[3px] data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 flex items-center justify-center text-current transition-none" }); -@Directive({ - selector: '[uiCheckbox]', +@Component({ + selector: 'ui-checkbox', exportAs: 'uiCheckbox', + changeDetection: ChangeDetectionStrategy.OnPush, host: { - '[class]': 'computedClass()' + 'class': 'peer', + '[attr.data-disabled]': 'this.disabled() || null' }, - hostDirectives: [{ - directive: NgpCheckbox, - inputs: [ - 'ngpCheckboxChecked: checked', - 'ngpCheckboxIndeterminate: indeterminate', - 'ngpCheckboxRequired: required', - 'ngpCheckboxDisabled: disabled', - ], - outputs: [ - 'ngpCheckboxCheckedChange: checkedChange', - 'ngpCheckboxIndeterminateChange: indeterminateChange', - ], - }], + template: ``, + imports: [NgIcon, NgpCheckbox], + providers: [provideIcons({ lucideCheck })] }) export class UiCheckbox { inputClass = input('', { alias: 'class' }); computedClass = computed(() => checkboxVariants({ class: this.inputClass() })); + readonly checked = model(false); + readonly required = input(false, { + transform: booleanAttribute, + }); + readonly disabled = input(false, { + transform: booleanAttribute, + }); + readonly indeterminate = input(false, { + transform: booleanAttribute, + }); } diff --git a/projects/ui/src/directives/form-field.ts b/projects/ui/src/directives/form-field.ts index 75450a1..e646933 100644 --- a/projects/ui/src/directives/form-field.ts +++ b/projects/ui/src/directives/form-field.ts @@ -5,7 +5,7 @@ import { NgpFormField, NgpLabel, NgpDescription, NgpError } from "ng-primitives/ const formFieldVariants = tv({ slots: { field: '', - label: 'flex items-center gap-2 text-sm leading-none font-medium select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50', + label: 'flex items-center gap-2 text-sm leading-none font-medium select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 peer-data-[disabled]:cursor-not-allowed peer-data-[disabled]:opacity-50', description: 'text-sm text-muted-foreground', error: 'text-sm font-medium text-destructive' } diff --git a/projects/ui/src/directives/label.ts b/projects/ui/src/directives/label.ts index c372ccf..64c9ebb 100644 --- a/projects/ui/src/directives/label.ts +++ b/projects/ui/src/directives/label.ts @@ -4,7 +4,7 @@ import { NgpLabel } from "ng-primitives/form-field"; const labelVariants = tv({ slots: { - label: 'flex items-center gap-2 text-sm leading-none font-medium select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50' + label: 'flex items-center gap-2 text-sm leading-none font-medium select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 peer-data-[disabled]:cursor-not-allowed peer-data-[disabled]:opacity-50' } });