@tolle_/tolle-ui 18.2.24 → 18.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/lib/accordion-item.component.mjs +17 -5
- package/esm2022/lib/alert-dialog-dynamic.component.mjs +1 -1
- package/esm2022/lib/alert-dialog.component.mjs +48 -10
- package/esm2022/lib/alert-dialog.service.mjs +13 -2
- package/esm2022/lib/alert.component.mjs +5 -5
- package/esm2022/lib/avatar.component.mjs +30 -9
- package/esm2022/lib/badge.component.mjs +41 -15
- package/esm2022/lib/breadcrumb-link.component.mjs +5 -5
- package/esm2022/lib/button.component.mjs +14 -3
- package/esm2022/lib/calendar.component.mjs +99 -17
- package/esm2022/lib/card.component.mjs +29 -9
- package/esm2022/lib/carousel/carousel.component.mjs +19 -3
- package/esm2022/lib/checkbox.component.mjs +44 -21
- package/esm2022/lib/collapsible.component.mjs +12 -4
- package/esm2022/lib/country-selector.component.mjs +140 -20
- package/esm2022/lib/data-table.component.mjs +27 -16
- package/esm2022/lib/date-picker.component.mjs +24 -4
- package/esm2022/lib/date-range-picker.component.mjs +37 -9
- package/esm2022/lib/dropdown-item.component.mjs +19 -5
- package/esm2022/lib/dropdown-label.component.mjs +3 -3
- package/esm2022/lib/dropdown-menu.component.mjs +3 -3
- package/esm2022/lib/dropdown-separator.component.mjs +3 -3
- package/esm2022/lib/dropdown-trigger.directive.mjs +7 -3
- package/esm2022/lib/empty-state.component.mjs +3 -3
- package/esm2022/lib/hover-card.component.mjs +4 -2
- package/esm2022/lib/input.component.mjs +7 -5
- package/esm2022/lib/label.component.mjs +2 -2
- package/esm2022/lib/masked-input.component.mjs +3 -3
- package/esm2022/lib/modal.component.mjs +58 -22
- package/esm2022/lib/modal.service.mjs +13 -1
- package/esm2022/lib/multi-select.component.mjs +228 -20
- package/esm2022/lib/otp.component.mjs +6 -3
- package/esm2022/lib/pagination.component.mjs +19 -9
- package/esm2022/lib/progress.component.mjs +8 -5
- package/esm2022/lib/radio-group.component.mjs +51 -3
- package/esm2022/lib/radio-item.component.mjs +46 -8
- package/esm2022/lib/radio-service.mjs +60 -1
- package/esm2022/lib/range-calendar.component.mjs +317 -98
- package/esm2022/lib/resizable-panel.component.mjs +5 -4
- package/esm2022/lib/scroll-area.component.mjs +5 -5
- package/esm2022/lib/segment.component.mjs +73 -7
- package/esm2022/lib/select-item.component.mjs +20 -2
- package/esm2022/lib/select-separator.component.mjs +3 -3
- package/esm2022/lib/select.component.mjs +221 -14
- package/esm2022/lib/separator.component.mjs +12 -6
- package/esm2022/lib/sheet-ref.mjs +8 -1
- package/esm2022/lib/sheet-wrapper.component.mjs +27 -11
- package/esm2022/lib/sheet.component.mjs +57 -16
- package/esm2022/lib/sheet.service.mjs +5 -1
- package/esm2022/lib/sidebar.component.mjs +73 -15
- package/esm2022/lib/skeleton.component.mjs +19 -5
- package/esm2022/lib/switch.component.mjs +59 -53
- package/esm2022/lib/tabs.component.mjs +75 -13
- package/esm2022/lib/tag-input.component.mjs +6 -3
- package/esm2022/lib/textarea.component.mjs +9 -7
- package/esm2022/lib/theme.service.mjs +114 -138
- package/esm2022/lib/toaster.component.mjs +19 -9
- package/esm2022/lib/toggle-group.component.mjs +76 -4
- package/esm2022/lib/tolle-config.mjs +1 -1
- package/esm2022/lib/tooltip.directive.mjs +41 -17
- package/esm2022/lib/utils/color.mjs +324 -0
- package/esm2022/public-api.mjs +2 -1
- package/fesm2022/tolle-ui.mjs +2645 -637
- package/fesm2022/tolle-ui.mjs.map +1 -1
- package/lib/alert-dialog.component.d.ts +10 -1
- package/lib/avatar.component.d.ts +16 -3
- package/lib/badge.component.d.ts +17 -5
- package/lib/button.component.d.ts +10 -3
- package/lib/calendar.component.d.ts +21 -3
- package/lib/card.component.d.ts +6 -0
- package/lib/carousel/carousel.component.d.ts +1 -0
- package/lib/checkbox.component.d.ts +12 -4
- package/lib/country-selector.component.d.ts +11 -2
- package/lib/date-picker.component.d.ts +1 -0
- package/lib/date-range-picker.component.d.ts +4 -1
- package/lib/dropdown-item.component.d.ts +4 -2
- package/lib/dropdown-trigger.directive.d.ts +1 -1
- package/lib/modal.component.d.ts +6 -0
- package/lib/multi-select.component.d.ts +20 -0
- package/lib/otp.component.d.ts +1 -0
- package/lib/progress.component.d.ts +1 -0
- package/lib/radio-group.component.d.ts +8 -0
- package/lib/radio-item.component.d.ts +10 -4
- package/lib/radio-service.d.ts +29 -0
- package/lib/range-calendar.component.d.ts +33 -4
- package/lib/segment.component.d.ts +13 -0
- package/lib/select-item.component.d.ts +4 -1
- package/lib/select.component.d.ts +20 -0
- package/lib/separator.component.d.ts +2 -0
- package/lib/sheet-wrapper.component.d.ts +4 -0
- package/lib/sheet.component.d.ts +10 -1
- package/lib/sidebar.component.d.ts +2 -0
- package/lib/skeleton.component.d.ts +9 -3
- package/lib/switch.component.d.ts +13 -20
- package/lib/tabs.component.d.ts +11 -1
- package/lib/theme.service.d.ts +21 -6
- package/lib/toaster.component.d.ts +2 -2
- package/lib/toggle-group.component.d.ts +20 -2
- package/lib/toggle.component.d.ts +1 -1
- package/lib/tolle-config.d.ts +8 -0
- package/lib/tooltip.directive.d.ts +3 -0
- package/lib/utils/color.d.ts +87 -0
- package/package.json +4 -1
- package/preset.js +28 -0
- package/public-api.d.ts +1 -0
- package/registry/docs-content.json +4214 -0
- package/registry/llms-full.txt +1599 -0
- package/registry/llms.txt +83 -0
- package/registry/manifest.json +4925 -0
- package/registry/r/accordion-item.json +21 -0
- package/registry/r/accordion.json +23 -0
- package/registry/r/alert-dialog-dynamic.json +31 -0
- package/registry/r/alert-dialog.json +26 -0
- package/registry/r/alert.json +23 -0
- package/registry/r/aspect-ratio.json +21 -0
- package/registry/r/avatar-fallback.json +16 -0
- package/registry/r/avatar.json +23 -0
- package/registry/r/badge.json +23 -0
- package/registry/r/breadcrumb-item.json +16 -0
- package/registry/r/breadcrumb-link.json +16 -0
- package/registry/r/breadcrumb-separator.json +16 -0
- package/registry/r/breadcrumb.json +21 -0
- package/registry/r/button-group.json +21 -0
- package/registry/r/button.json +23 -0
- package/registry/r/calendar.json +23 -0
- package/registry/r/card.json +21 -0
- package/registry/r/carousel.json +18 -0
- package/registry/r/checkbox.json +23 -0
- package/registry/r/collapsible.json +21 -0
- package/registry/r/context-menu-trigger.json +23 -0
- package/registry/r/context-menu.json +28 -0
- package/registry/r/country-selector.json +31 -0
- package/registry/r/data-table.json +38 -0
- package/registry/r/date-picker.json +27 -0
- package/registry/r/date-range-picker.json +32 -0
- package/registry/r/dropdown-item.json +21 -0
- package/registry/r/dropdown-label.json +16 -0
- package/registry/r/dropdown-menu.json +16 -0
- package/registry/r/dropdown-separator.json +16 -0
- package/registry/r/dropdown-trigger.json +20 -0
- package/registry/r/empty-state.json +23 -0
- package/registry/r/hover-card.json +23 -0
- package/registry/r/input.json +21 -0
- package/registry/r/label.json +21 -0
- package/registry/r/masked-input.json +21 -0
- package/registry/r/modal.json +36 -0
- package/registry/r/multi-select.json +33 -0
- package/registry/r/otp-group.json +16 -0
- package/registry/r/otp-slot.json +21 -0
- package/registry/r/otp.json +21 -0
- package/registry/r/pagination.json +32 -0
- package/registry/r/phone-number-input.json +34 -0
- package/registry/r/popover-content.json +21 -0
- package/registry/r/popover.json +18 -0
- package/registry/r/progress.json +21 -0
- package/registry/r/radio-group.json +26 -0
- package/registry/r/radio-item.json +26 -0
- package/registry/r/range-calendar.json +28 -0
- package/registry/r/resizable-panel-item.json +23 -0
- package/registry/r/resizable-panel.json +23 -0
- package/registry/r/resizable.json +21 -0
- package/registry/r/scroll-area.json +21 -0
- package/registry/r/segment.json +21 -0
- package/registry/r/select-group.json +21 -0
- package/registry/r/select-item.json +26 -0
- package/registry/r/select-separator.json +21 -0
- package/registry/r/select.json +31 -0
- package/registry/r/separator.json +21 -0
- package/registry/r/sheet-wrapper.json +31 -0
- package/registry/r/sheet.json +21 -0
- package/registry/r/sidebar.json +21 -0
- package/registry/r/skeleton.json +23 -0
- package/registry/r/slider.json +21 -0
- package/registry/r/switch.json +23 -0
- package/registry/r/tabs.json +23 -0
- package/registry/r/tag-input.json +25 -0
- package/registry/r/textarea.json +21 -0
- package/registry/r/toaster.json +26 -0
- package/registry/r/toggle-group.json +25 -0
- package/registry/r/toggle.json +23 -0
- package/registry/r/tolle-cell.json +16 -0
- package/registry/r/tooltip.json +23 -0
- package/registry/registry.json +1540 -0
- package/theme.css +56 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "accordion-item",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Accordion Item",
|
|
5
|
+
"description": "Accordion Item component.",
|
|
6
|
+
"category": "layout",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "accordion-item.component.ts",
|
|
12
|
+
"target": "ui/accordion-item.component.ts",
|
|
13
|
+
"content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { trigger, state, style, transition, animate } from '@angular/animations';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-accordion-item',\n standalone: true,\n imports: [CommonModule],\n animations: [\n trigger('expandCollapse', [\n state('collapsed', style({\n height: '0px',\n opacity: '0',\n overflow: 'hidden',\n visibility: 'hidden'\n })),\n state('expanded', style({\n height: '*', // \"Star\" means actual content height\n opacity: '1',\n overflow: 'hidden',\n visibility: 'visible'\n })),\n // Use cubic-bezier to match Tailwind/shadcn-ui default ease\n transition('collapsed <=> expanded', [\n animate('300ms cubic-bezier(0.87, 0, 0.13, 1)')\n ])\n ])\n ],\n template: `\n <div [class]=\"cn('flex flex-col border-b border-border', class)\">\n <button\n type=\"button\"\n (click)=\"toggle()\"\n [id]=\"'acc-trigger-'+id\"\n [attr.aria-controls]=\"'acc-content-'+id\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n class=\"flex flex-1 items-center justify-between py-4 font-medium transition-all group [&[data-state=open]>i]:rotate-180 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background\"\n >\n <span class=\"text-left group-hover:underline\">{{ title }}</span>\n <i class=\"ri-arrow-down-s-line text-muted-foreground text-lg transition-transform duration-200\"></i>\n </button>\n\n <div\n [@expandCollapse]=\"isOpen ? 'expanded' : 'collapsed'\"\n [id]=\"'acc-content-'+id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'acc-trigger-'+id\"\n [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n class=\"overflow-hidden\">\n <div class=\"pb-4 pt-0 text-sm text-muted-foreground\">\n <ng-content></ng-content>\n </div>\n </div>\n </div>\n `\n})\nexport class AccordionItemComponent {\n @Input() title: string = '';\n @Input() class: string = '';\n @Input() id!: string | number;\n\n isOpen = false;\n\n // This will be set by the parent Accordion component\n onToggle?: (id: string | number) => void;\n\n toggle() {\n if (this.onToggle) {\n this.onToggle(this.id);\n } else {\n this.isOpen = !this.isOpen;\n }\n }\n\n protected readonly cn = cn;\n}\n"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "utils/cn.ts",
|
|
17
|
+
"target": "ui/utils/cn.ts",
|
|
18
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "accordion",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Accordion",
|
|
5
|
+
"description": "Accordion component.",
|
|
6
|
+
"category": "layout",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"accordion-item"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "accordion.component.ts",
|
|
14
|
+
"target": "ui/accordion.component.ts",
|
|
15
|
+
"content": "import { Component, Input, ContentChildren, QueryList, AfterContentInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { AccordionItemComponent } from './accordion-item.component';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-accordion',\n standalone: true,\n imports: [CommonModule], // No AccordionItemComponent import needed here if projected via ng-content\n template: `\n <div [class]=\"cn('w-full', class)\">\n <ng-content></ng-content>\n </div>\n `\n})\nexport class AccordionComponent implements AfterContentInit {\n @Input() type: 'single' | 'multiple' = 'single';\n @Input() class: string = '';\n\n @ContentChildren(AccordionItemComponent) items!: QueryList<AccordionItemComponent>;\n\n ngAfterContentInit() {\n // 1. Assign IDs and Listeners on load\n this.initItems();\n\n // 2. Re-init if items change dynamically (optional but good for robustness)\n this.items.changes.subscribe(() => this.initItems());\n }\n\n private initItems() {\n this.items.forEach((item, index) => {\n // Auto-assign ID if missing\n if (item.id === undefined) item.id = `accordion-item-${index}`;\n\n // Set up the toggle bridge\n item.onToggle = (id) => this.handleToggle(id);\n });\n }\n\n private handleToggle(selectedId: string | number) {\n this.items.forEach(item => {\n if (item.id === selectedId) {\n // Toggle the clicked item\n item.isOpen = !item.isOpen;\n } else if (this.type === 'single') {\n // Close others if in single mode\n item.isOpen = false;\n }\n });\n }\n\n protected readonly cn = cn;\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "alert-dialog-dynamic",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Alert Dialog Dynamic",
|
|
5
|
+
"description": "Alert Dialog Dynamic component.",
|
|
6
|
+
"category": "overlays",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"alert-dialog",
|
|
9
|
+
"button"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [
|
|
12
|
+
"class-variance-authority"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "alert-dialog-dynamic.component.ts",
|
|
17
|
+
"target": "ui/alert-dialog-dynamic.component.ts",
|
|
18
|
+
"content": "import { Component, Inject, inject, OnInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport {\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent\n} from './alert-dialog.component';\nimport { ButtonComponent } from './button.component';\nimport { AlertDialogConfig, AlertDialogRef } from './alert-dialog.types';\n\n@Component({\n selector: 'tolle-alert-dialog-dynamic',\n standalone: true,\n imports: [\n CommonModule,\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent,\n ButtonComponent\n ],\n template: `\n <tolle-alert-dialog [open]=\"true\" (openChange)=\"onOpenChange($event)\">\n <tolle-alert-dialog-content [size]=\"config.size || 'lg'\">\n <tolle-alert-dialog-header>\n <tolle-alert-dialog-title>{{ config.title }}</tolle-alert-dialog-title>\n <tolle-alert-dialog-description>\n {{ config.description }}\n </tolle-alert-dialog-description>\n </tolle-alert-dialog-header>\n <tolle-alert-dialog-footer>\n <tolle-alert-dialog-cancel (click)=\"close(false)\">\n <tolle-button variant=\"outline\">{{ config.cancelText || 'Cancel' }}</tolle-button>\n </tolle-alert-dialog-cancel>\n <tolle-alert-dialog-action (click)=\"close(true)\">\n <tolle-button [variant]=\"config.variant === 'destructive' ? 'destructive' : 'default'\">\n {{ config.actionText || 'Continue' }}\n </tolle-button>\n </tolle-alert-dialog-action>\n </tolle-alert-dialog-footer>\n </tolle-alert-dialog-content>\n </tolle-alert-dialog>\n `\n})\nexport class AlertDialogDynamicComponent {\n config!: AlertDialogConfig;\n dialogRef!: AlertDialogRef;\n\n onOpenChange(open: boolean) {\n if (!open) {\n this.close(false);\n }\n }\n\n close(result: boolean) {\n this.dialogRef.close(result);\n }\n}\n"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "alert-dialog.types.ts",
|
|
22
|
+
"target": "ui/alert-dialog.types.ts",
|
|
23
|
+
"content": "import { Subject, Observable } from 'rxjs';\n\nexport type AlertDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | 'fit';\n\nexport interface AlertDialogConfig {\n title: string;\n description: string;\n cancelText?: string;\n actionText?: string;\n variant?: 'default' | 'destructive';\n size?: AlertDialogSize;\n}\n\nexport class AlertDialogRef {\n private readonly _afterClosed = new Subject<boolean>();\n afterClosed$: Observable<boolean> = this._afterClosed.asObservable();\n\n close(result: boolean = false): void {\n this._afterClosed.next(result);\n this._afterClosed.complete();\n }\n}\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "utils/cn.ts",
|
|
27
|
+
"target": "ui/utils/cn.ts",
|
|
28
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "alert-dialog",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Alert Dialog",
|
|
5
|
+
"description": "Alert Dialog component.",
|
|
6
|
+
"category": "overlays",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "alert-dialog.component.ts",
|
|
12
|
+
"target": "ui/alert-dialog.component.ts",
|
|
13
|
+
"content": "import { Component, Input, Output, EventEmitter, Injectable, inject, TemplateRef, ViewChild, ViewContainerRef, OnDestroy, OnInit, ContentChild, forwardRef } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { Overlay, OverlayRef, OverlayConfig } from '@angular/cdk/overlay';\nimport { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { cn } from './utils/cn';\nimport { BehaviorSubject } from 'rxjs';\nimport { AlertDialogSize } from './alert-dialog.types';\n\n@Injectable()\nclass AlertDialogInternalService {\n private openSubject = new BehaviorSubject<boolean>(false);\n open$ = this.openSubject.asObservable();\n\n setOpen(value: boolean) {\n this.openSubject.next(value);\n }\n\n getOpen() {\n return this.openSubject.getValue();\n }\n\n toggle() {\n this.setOpen(!this.getOpen());\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog',\n standalone: true,\n imports: [CommonModule],\n providers: [AlertDialogInternalService],\n template: `<ng-content></ng-content>`\n})\nexport class AlertDialogComponent {\n @Input() set open(val: boolean) {\n this.alertDialogService.setOpen(val);\n }\n @Output() openChange = new EventEmitter<boolean>();\n\n private alertDialogService = inject(AlertDialogInternalService);\n\n constructor() {\n this.alertDialogService.open$.subscribe(val => {\n this.openChange.emit(val);\n });\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-trigger',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: {\n '(click)': 'onOpen()'\n }\n})\nexport class AlertDialogTriggerComponent {\n private alertDialogService = inject(AlertDialogInternalService);\n\n onOpen() {\n this.alertDialogService.setOpen(true);\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-portal',\n standalone: true,\n imports: [CommonModule],\n template: `\n <ng-template #portalContent>\n <ng-content></ng-content>\n </ng-template>\n `\n})\nexport class AlertDialogPortalComponent implements OnInit, OnDestroy {\n @ViewChild('portalContent', { static: true }) portalContent!: TemplateRef<any>;\n private alertDialogService = inject(AlertDialogInternalService);\n private overlay = inject(Overlay);\n private viewContainerRef = inject(ViewContainerRef);\n private overlayRef?: OverlayRef;\n /** Element focused before the dialog opened, restored on close. */\n private previouslyFocused?: HTMLElement | null;\n\n ngOnInit() {\n this.alertDialogService.open$.subscribe(open => {\n if (open) {\n this.show();\n } else {\n this.hide();\n }\n });\n }\n\n private show() {\n if (this.overlayRef) return;\n\n // Remember the trigger so focus can be restored on close.\n this.previouslyFocused = document.activeElement as HTMLElement | null;\n\n const config = new OverlayConfig({\n hasBackdrop: true,\n backdropClass: ['cdk-overlay-backdrop', 'bg-black/80', 'backdrop-blur-sm', 'data-[state=open]:animate-in', 'data-[state=closed]:animate-out', 'data-[state=closed]:fade-out-0', 'data-[state=open]:fade-in-0'],\n positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),\n scrollStrategy: this.overlay.scrollStrategies.block()\n });\n\n this.overlayRef = this.overlay.create(config);\n\n // Alert-dialog semantics: backdrop clicks must NOT dismiss.\n // Escape still closes the dialog.\n this.overlayRef.keydownEvents().subscribe((event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault();\n this.alertDialogService.setOpen(false);\n }\n });\n\n const portal = new TemplatePortal(this.portalContent, this.viewContainerRef);\n this.overlayRef.attach(portal);\n }\n\n private hide() {\n if (this.overlayRef) {\n this.overlayRef.detach();\n this.overlayRef.dispose();\n this.overlayRef = undefined;\n // Restore focus to the trigger.\n this.previouslyFocused?.focus?.();\n }\n }\n\n ngOnDestroy() {\n this.hide();\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-content',\n standalone: true,\n imports: [CommonModule, A11yModule],\n template: `<div [class]=\"computedClass\" [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"titleCmp?.id || null\"\n [attr.aria-describedby]=\"descriptionCmp?.id || null\"\n cdkTrapFocus cdkTrapFocusAutoCapture><ng-content></ng-content></div>`,\n host: {\n '[class]': '\"contents\"'\n }\n})\nexport class AlertDialogContentComponent {\n @Input() class: string = '';\n @Input() size: AlertDialogSize = 'lg';\n\n /** Projected title/description used to build the dialog's accessible name. */\n @ContentChild(forwardRef(() => AlertDialogTitleComponent)) titleCmp?: AlertDialogTitleComponent;\n @ContentChild(forwardRef(() => AlertDialogDescriptionComponent)) descriptionCmp?: AlertDialogDescriptionComponent;\n\n private alertDialogService = inject(AlertDialogInternalService);\n isOpen = false;\n\n constructor() {\n this.alertDialogService.open$.subscribe(val => {\n this.isOpen = val;\n });\n }\n\n get computedClass() {\n return cn(\n \"fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-50 grid w-full gap-4 border border-input bg-background p-6 shadow-lg data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] rounded-lg\",\n {\n 'max-w-xs': this.size === 'xs',\n 'max-w-sm': this.size === 'sm',\n 'max-w-md': this.size === 'md',\n 'max-w-lg': this.size === 'lg',\n 'max-w-xl': this.size === 'xl',\n 'max-w-2xl': this.size === '2xl',\n 'max-w-full': this.size === 'full',\n 'max-w-fit': this.size === 'fit',\n },\n this.class\n );\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-header',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass' }\n})\nexport class AlertDialogHeaderComponent {\n @Input() class: string = '';\n get computedClass() { return cn(\"flex flex-col space-y-2 text-center sm:text-left\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-footer',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass' }\n})\nexport class AlertDialogFooterComponent {\n @Input() class: string = '';\n get computedClass() { return cn(\"flex flex-row justify-end space-x-2\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-title',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass', '[attr.id]': 'id' }\n})\nexport class AlertDialogTitleComponent {\n @Input() class: string = '';\n /** Stable id referenced by the dialog's `aria-labelledby`. */\n readonly id = `tolle-alert-dialog-title-${Math.random().toString(36).substr(2, 9)}`;\n get computedClass() { return cn(\"text-lg font-semibold text-foreground\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-description',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass', '[attr.id]': 'id' }\n})\nexport class AlertDialogDescriptionComponent {\n @Input() class: string = '';\n /** Stable id referenced by the dialog's `aria-describedby`. */\n readonly id = `tolle-alert-dialog-description-${Math.random().toString(36).substr(2, 9)}`;\n get computedClass() { return cn(\"text-sm text-muted-foreground\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-action',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`\n})\nexport class AlertDialogActionComponent {\n}\n\n@Component({\n selector: 'tolle-alert-dialog-cancel',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`\n})\nexport class AlertDialogCancelComponent {\n}\n"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "alert-dialog.types.ts",
|
|
17
|
+
"target": "ui/alert-dialog.types.ts",
|
|
18
|
+
"content": "import { Subject, Observable } from 'rxjs';\n\nexport type AlertDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | 'fit';\n\nexport interface AlertDialogConfig {\n title: string;\n description: string;\n cancelText?: string;\n actionText?: string;\n variant?: 'default' | 'destructive';\n size?: AlertDialogSize;\n}\n\nexport class AlertDialogRef {\n private readonly _afterClosed = new Subject<boolean>();\n afterClosed$: Observable<boolean> = this._afterClosed.asObservable();\n\n close(result: boolean = false): void {\n this._afterClosed.next(result);\n this._afterClosed.complete();\n }\n}\n"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "utils/cn.ts",
|
|
22
|
+
"target": "ui/utils/cn.ts",
|
|
23
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "alert",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Alert",
|
|
5
|
+
"description": "Alert component with variant options.",
|
|
6
|
+
"category": "feedback",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "alert.component.ts",
|
|
14
|
+
"target": "ui/alert.component.ts",
|
|
15
|
+
"content": "import { Component, Input, Output, EventEmitter } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { trigger, transition, style, animate } from '@angular/animations';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst alertVariants = cva(\n \"relative w-full rounded-lg border p-4 [&>i+div]:translate-y-[-3px] [&>i]:absolute [&>i]:left-4 [&>i]:top-4 [&>i]:text-foreground [&>i~div]:pl-7\",\n {\n variants: {\n variant: {\n default: \"bg-background text-foreground\",\n destructive: \"border-destructive/50 text-destructive [&>i]:text-destructive\",\n success: \"border-success/50 text-success [&>i]:text-success\",\n warning: \"border-warning/50 text-warning [&>i]:text-warning\",\n info: \"border-info/50 text-info [&>i]:text-info\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\ntype AlertVariants = VariantProps<typeof alertVariants>;\n\n@Component({\n selector: 'tolle-alert',\n standalone: true,\n imports: [CommonModule],\n animations: [\n trigger('fade', [\n transition(':leave', [\n style({ opacity: 1, transform: 'scale(1)' }),\n animate('300ms ease-in-out', style({ opacity: 0, transform: 'scale(0.95)', height: 0, margin: 0, padding: 0 }))\n ])\n ])\n ],\n template: `\n <div\n *ngIf=\"!dismissed\"\n @fade\n [class]=\"cn(alertVariants({ variant }), class)\"\n role=\"alert\"\n >\n <ng-content select=\"[icon]\"></ng-content>\n\n <button\n *ngIf=\"dismissible\"\n (click)=\"dismiss()\"\n class=\"absolute right-2 top-2 rounded-md p-1 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\"\n type=\"button\"\n >\n <i class=\"ri-close-line text-lg\"></i>\n <span class=\"sr-only\">Close</span>\n </button>\n\n <div>\n <h5 *ngIf=\"title\" class=\"mb-1 font-medium leading-none tracking-tight\">\n {{ title }}\n </h5>\n <div class=\"text-sm [&_p]:leading-relaxed opacity-90\">\n <ng-content></ng-content>\n </div>\n </div>\n </div>\n `\n})\nexport class AlertComponent {\n @Input() variant: AlertVariants['variant'] = 'default';\n @Input() title?: string;\n @Input() class: string = '';\n @Input() dismissible: boolean = false;\n\n @Output() onClose = new EventEmitter<void>();\n\n dismissed = false;\n protected alertVariants = alertVariants;\n protected cn = cn;\n\n dismiss() {\n this.dismissed = true;\n this.onClose.emit();\n }\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aspect-ratio",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Aspect Ratio",
|
|
5
|
+
"description": "Aspect Ratio component.",
|
|
6
|
+
"category": "layout",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "aspect-ratio.component.ts",
|
|
12
|
+
"target": "ui/aspect-ratio.component.ts",
|
|
13
|
+
"content": "import { Component, Input, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-aspect-ratio',\n standalone: true,\n imports: [CommonModule],\n template: `\n <div\n #container\n [class]=\"cn('relative w-full overflow-hidden bg-muted/20', class)\"\n [style.aspect-ratio]=\"ratio\"\n >\n <div *ngIf=\"isLoading && !hasError\" class=\"absolute inset-0 z-10 animate-pulse bg-muted flex items-center justify-center\">\n <i class=\"ri-image-line text-muted-foreground/40 text-3xl\"></i>\n </div>\n\n <div *ngIf=\"hasError\" class=\"absolute inset-0 z-20 bg-secondary flex flex-col items-center justify-center gap-2\">\n <i class=\"ri-image-warning-line text-destructive text-2xl\"></i>\n <span class=\"text-[10px] font-medium uppercase tracking-wider text-muted-foreground\">Failed to load</span>\n </div>\n\n <ng-content></ng-content>\n </div>\n `,\n styles: [`\n :host {\n display: block;\n width: 100%;\n }\n\n /* Target elements projected into the ng-content */\n :host ::ng-deep img,\n :host ::ng-deep video,\n :host ::ng-deep iframe {\n position: absolute;\n height: 100%;\n width: 100%;\n inset: 0;\n object-fit: cover;\n transition: opacity 0.3s ease-in-out;\n }\n\n /* Target direct div wrappers if needed */\n :host ::ng-deep .aspect-ratio-content {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n `]\n})\nexport class AspectRatioComponent implements AfterViewInit, OnDestroy {\n @Input() ratio: string | number = '16 / 9';\n @Input() class: string = '';\n\n @ViewChild('container') container!: ElementRef<HTMLElement>;\n\n isLoading = true;\n hasError = false;\n\n private cleanupListeners: (() => void)[] = [];\n\n constructor(private cdr: ChangeDetectorRef) { }\n\n ngAfterViewInit() {\n const el = this.container.nativeElement;\n\n // Use capture phase because load/error do not bubble\n const onLoad = (event: Event) => {\n // Check if the event target is inside our container\n if (el.contains(event.target as Node)) {\n this.onLoad();\n }\n };\n\n const onError = (event: Event) => {\n if (el.contains(event.target as Node)) {\n this.onError();\n }\n };\n\n el.addEventListener('load', onLoad, true);\n el.addEventListener('error', onError, true);\n\n this.cleanupListeners.push(() => {\n el.removeEventListener('load', onLoad, true);\n el.removeEventListener('error', onError, true);\n });\n\n // Check if there are even any media elements. If not, don't show loader.\n // Or if images were already cached/loaded before AfterViewInit\n setTimeout(() => {\n const media = el.querySelectorAll('img, video, iframe');\n if (media.length === 0) {\n this.onLoad();\n } else {\n // Double check if all images are already complete (cached)\n const allComplete = Array.from(media).every(m => {\n if (m instanceof HTMLImageElement) return m.complete;\n return false; // For video/iframe we wait for event\n });\n if (allComplete) {\n this.onLoad();\n }\n }\n }, 50);\n }\n\n onLoad() {\n this.isLoading = false;\n this.hasError = false;\n this.cdr.detectChanges();\n }\n\n onError() {\n this.isLoading = false;\n this.hasError = true;\n this.cdr.detectChanges();\n }\n\n ngOnDestroy() {\n this.cleanupListeners.forEach(cleanup => cleanup());\n }\n\n protected cn = cn;\n}\n"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "utils/cn.ts",
|
|
17
|
+
"target": "ui/utils/cn.ts",
|
|
18
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "avatar-fallback",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Avatar Fallback",
|
|
5
|
+
"description": "Avatar Fallback component.",
|
|
6
|
+
"category": "media",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "avatar-fallback.component.ts",
|
|
12
|
+
"target": "ui/avatar-fallback.component.ts",
|
|
13
|
+
"content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-avatar-fallback',\n standalone: true,\n imports: [],\n template: `\n <div class=\"flex h-full w-full items-center justify-center bg-muted text-muted-foreground font-medium uppercase\">\n <ng-content></ng-content>\n </div>\n `,\n styles: ``\n})\nexport class AvatarFallbackComponent {\n\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "avatar",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Avatar",
|
|
5
|
+
"description": "Avatar component with shape and size options.",
|
|
6
|
+
"category": "media",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "avatar.component.ts",
|
|
14
|
+
"target": "ui/avatar.component.ts",
|
|
15
|
+
"content": "import { ChangeDetectorRef, Component, Input, HostBinding } from '@angular/core';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\nimport { NgIf } from '@angular/common';\n\nconst avatarVariants = cva(\n 'relative flex shrink-0 overflow-hidden bg-muted',\n {\n variants: {\n shape: {\n circle: 'rounded-full',\n square: 'rounded-md',\n },\n size: {\n sm: 'h-8 w-8 text-xs',\n default: 'h-10 w-10',\n lg: 'h-16 w-16 text-lg',\n xl: 'h-24 w-24 text-xl',\n },\n },\n defaultVariants: {\n shape: 'circle',\n size: 'default',\n },\n }\n);\n\nexport type AvatarProps = VariantProps<typeof avatarVariants>;\n\n@Component({\n selector: 'tolle-avatar',\n standalone: true,\n imports: [NgIf],\n template: `\n <!-- Image Layer -->\n <img *ngIf=\"src && !hasError\"\n [src]=\"src\"\n [alt]=\"alt\"\n (load)=\"onLoad()\"\n (error)=\"onError()\"\n [class.opacity-0]=\"isLoading\"\n class=\"h-full w-full object-cover transition-opacity duration-300\" />\n\n <!-- Fallback Layer -->\n <div *ngIf=\"hasError || !src || isLoading\" class=\"flex h-full w-full items-center justify-center bg-muted\">\n <ng-content></ng-content>\n </div>\n `\n})\nexport class AvatarComponent {\n /** Image URL. When absent or failed to load, projected fallback content is shown. */\n @Input() src?: string;\n /** Alternative text for the avatar image. @default '' */\n @Input() alt: string = '';\n /** Size of the avatar. @default 'default' */\n @Input() size: AvatarProps['size'] = 'default';\n /** Shape of the avatar. @default 'circle' */\n @Input() shape: AvatarProps['shape'] = 'circle';\n /** Extra Tailwind classes merged onto the avatar via `cn()` (last-wins). */\n @Input() class = '';\n\n isLoading = true;\n hasError = false;\n\n constructor(private cdr: ChangeDetectorRef) {}\n\n onLoad() {\n this.isLoading = false;\n this.cdr.detectChanges();\n }\n\n onError() {\n this.isLoading = false;\n this.hasError = true;\n this.cdr.detectChanges();\n }\n\n // Apply styles directly to the <tolle-avatar> tag\n @HostBinding('class')\n get hostClasses() {\n return cn(avatarVariants({ shape: this.shape, size: this.size }), this.class);\n }\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "badge",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Badge",
|
|
5
|
+
"description": "Badge component with variant and size options.",
|
|
6
|
+
"category": "feedback",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "badge.component.ts",
|
|
14
|
+
"target": "ui/badge.component.ts",
|
|
15
|
+
"content": "import { Component, Input, Output, EventEmitter } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst badgeVariants = cva(\n 'inline-flex w-fit items-center justify-center rounded-md border px-2 py-0.5 font-medium transition-colors gap-1 whitespace-nowrap',\n {\n variants: {\n variant: {\n default: 'border-transparent bg-primary text-primary-foreground',\n secondary: 'border-transparent bg-secondary text-secondary-foreground',\n outline: 'text-foreground border-border bg-transparent',\n destructive: 'border-transparent bg-destructive text-destructive-foreground',\n },\n size: {\n xs: 'px-1.5 py-0 text-[10px]',\n sm: 'px-2 py-0 text-[11px]',\n default: 'text-xs',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n }\n);\n\nexport type BadgeProps = VariantProps<typeof badgeVariants>;\n\n@Component({\n selector: 'tolle-badge',\n standalone: true,\n imports: [CommonModule],\n template: `\n <div [class]=\"computedClass\">\n <ng-content select=\"[prefix]\"></ng-content>\n\n <span class=\"truncate\">\n <ng-content></ng-content>\n </span>\n\n <button\n *ngIf=\"removable\"\n type=\"button\"\n [attr.aria-label]=\"removeLabel\"\n (click)=\"onRemove.emit($event)\"\n class=\"ml-1 -mr-1 rounded-md transition-colors hover:bg-foreground/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\">\n <i class=\"ri-close-line\" aria-hidden=\"true\"></i>\n </button>\n </div>\n `,\n})\nexport class BadgeComponent {\n /** Visual style of the badge. @default 'default' */\n @Input() variant: BadgeProps['variant'] = 'default';\n /** Size of the badge. @default 'default' */\n @Input() size: BadgeProps['size'] = 'default';\n /** Shows a trailing dismiss (×) button that emits `onRemove`. @default false */\n @Input() removable = false;\n /** Accessible label for the dismiss button. @default 'Remove' */\n @Input() removeLabel = 'Remove';\n /** Extra Tailwind classes merged onto the badge via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted with the click event when the dismiss button is pressed. */\n @Output() onRemove = new EventEmitter<MouseEvent>();\n\n get computedClass() {\n return cn(\n badgeVariants({ variant: this.variant, size: this.size }),\n this.class\n );\n }\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "breadcrumb-item",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Breadcrumb Item",
|
|
5
|
+
"description": "Breadcrumb Item component.",
|
|
6
|
+
"category": "navigation",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "breadcrumb-item.component.ts",
|
|
12
|
+
"target": "ui/breadcrumb-item.component.ts",
|
|
13
|
+
"content": "import {Component, inject, OnInit} from '@angular/core';\n\n@Component({\n selector: 'tolle-breadcrumb-item',\n standalone: true,\n imports: [],\n template: `\n <li class=\"inline-flex items-center gap-1.5\">\n <ng-content></ng-content>\n </li>\n `,\n styles: ``\n})\nexport class BreadcrumbItemComponent {\n\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "breadcrumb-link",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Breadcrumb Link",
|
|
5
|
+
"description": "Breadcrumb Link component.",
|
|
6
|
+
"category": "navigation",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "breadcrumb-link.component.ts",
|
|
12
|
+
"target": "ui/breadcrumb-link.component.ts",
|
|
13
|
+
"content": "import {Component, Input} from '@angular/core';\nimport {NgIf, NgTemplateOutlet} from '@angular/common';\n\n@Component({\n selector: 'tolle-breadcrumb-link',\n standalone: true,\n imports: [\n NgIf,\n NgTemplateOutlet\n ],\n template: `\n <ng-template #content>\n <ng-content></ng-content>\n </ng-template>\n\n <ng-container *ngIf=\"active; else linkTemplate\">\n <span\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n class=\"font-normal text-foreground\"\n >\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </span>\n </ng-container>\n\n <ng-template #linkTemplate>\n <a class=\"transition-colors hover:text-foreground cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm\">\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </a>\n </ng-template>\n `,\n styles: ``\n})\nexport class BreadcrumbLinkComponent {\n @Input() active: boolean = false;\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "breadcrumb-separator",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Breadcrumb Separator",
|
|
5
|
+
"description": "Breadcrumb Separator component.",
|
|
6
|
+
"category": "layout",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "breadcrumb-separator.component.ts",
|
|
12
|
+
"target": "ui/breadcrumb-separator.component.ts",
|
|
13
|
+
"content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-breadcrumb-separator',\n standalone: true,\n imports: [],\n template: `\n <li role=\"presentation\" aria-hidden=\"true\" class=\"[&>i]:size-3.5\">\n <ng-content>\n <i class=\"ri-arrow-right-s-line\"></i>\n </ng-content>\n </li>\n `,\n styles: ``\n})\nexport class BreadcrumbSeparatorComponent {\n\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "breadcrumb",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Breadcrumb",
|
|
5
|
+
"description": "Breadcrumb component.",
|
|
6
|
+
"category": "navigation",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "breadcrumb.component.ts",
|
|
12
|
+
"target": "ui/breadcrumb.component.ts",
|
|
13
|
+
"content": "import {Component, Input} from '@angular/core';\nimport { cn } from \"./utils/cn\";\n\n@Component({\n selector: 'tolle-breadcrumb',\n standalone: true,\n imports: [],\n template: `\n <nav aria-label=\"breadcrumb\" [class]=\"cn('flex flex-wrap items-center break-words text-sm text-muted-foreground', class)\">\n <ol class=\"flex flex-wrap items-center gap-1.5 break-words\">\n <ng-content></ng-content>\n </ol>\n </nav>\n `,\n styles: ``\n})\nexport class BreadcrumbComponent {\n @Input() class: string = '';\n protected cn = cn;\n}\n"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "utils/cn.ts",
|
|
17
|
+
"target": "ui/utils/cn.ts",
|
|
18
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "button-group",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Button Group",
|
|
5
|
+
"description": "Button Group component.",
|
|
6
|
+
"category": "actions",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "button-group.component.ts",
|
|
12
|
+
"target": "ui/button-group.component.ts",
|
|
13
|
+
"content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-button-group',\n standalone: true,\n imports: [CommonModule],\n template: `\n <div [class]=\"cn('inline-flex items-center -space-x-px rounded-md shadow-sm', class)\">\n <ng-content></ng-content>\n </div>\n `,\n styles: [`\n :host {\n display: inline-block;\n }\n\n /* 1. First Button: Remove right rounding and keep border */\n :host ::ng-deep tolle-button:first-child button {\n @apply rounded-r-none;\n }\n\n /* 2. Middle Buttons: Remove all rounding and the left border to avoid double-thickness */\n :host ::ng-deep tolle-button:not(:first-child):not(:last-child) button {\n @apply rounded-none border-l-0;\n }\n\n /* 3. Last Button: Remove left rounding and left border */\n :host ::ng-deep tolle-button:last-child:not(:first-child) button {\n @apply rounded-l-none border-l-0;\n }\n\n /* 4. Hover/Focus State: Bring the active button to the front so the focus ring isn't cut off */\n :host ::ng-deep tolle-button button:hover,\n :host ::ng-deep tolle-button button:focus {\n @apply z-10 relative;\n }\n `]\n})\nexport class ButtonGroupComponent {\n @Input() class: string = '';\n protected cn = cn;\n}\n"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "utils/cn.ts",
|
|
17
|
+
"target": "ui/utils/cn.ts",
|
|
18
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "button",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Button",
|
|
5
|
+
"description": "Button component with variant and size and busy options.",
|
|
6
|
+
"category": "actions",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "button.component.ts",
|
|
14
|
+
"target": "ui/button.component.ts",
|
|
15
|
+
"content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst buttonVariants = cva(\n \"tolle-button-base inline-flex items-center justify-center rounded-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background active:scale-[0.98]\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n outline: \"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground text-foreground\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-10 px-4 py-2\",\n xs: \"h-8 px-2 py-1 text-xs\",\n sm: \"h-9 rounded-md px-3\",\n lg: \"h-11 rounded-md px-8\",\n \"icon-xs\": \"h-8 w-8\",\n \"icon-sm\": \"h-9 w-9\",\n icon: \"h-10 w-10\",\n \"icon-lg\": \"h-11 w-11\",\n },\n busy: { true: \"tolle-button--busy relative !cursor-wait !pointer-events-none\" }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport type ButtonProps = VariantProps<typeof buttonVariants>;\n\n@Component({\n selector: 'tolle-button',\n standalone: true,\n imports: [CommonModule],\n template: `\n <button\n [type]=\"type\"\n [class]=\"computedClass\"\n [disabled]=\"disabled || busy\"\n [attr.aria-busy]=\"busy\"\n >\n <div *ngIf=\"busy\" class=\"absolute inset-0 flex items-center justify-center\">\n <svg class=\"animate-spin h-5 w-5 text-current\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n\n <span class=\"flex items-center justify-center w-full h-full pointer-events-none\" [class.invisible]=\"busy\">\n <ng-content></ng-content>\n </span>\n </button>\n `,\n host: {\n 'class': 'tolle-button-wrapper inline-block align-middle'\n }\n})\nexport class ButtonComponent {\n /** Extra Tailwind classes merged onto the button via `cn()` (last-wins). */\n @Input() class: string = '';\n /** Visual style of the button. @default 'default' */\n @Input() variant: ButtonProps['variant'] = 'default';\n /** Size of the button, including icon-only sizes. @default 'default' */\n @Input() size: ButtonProps['size'] = 'default';\n /** Native button type. Defaults to 'button' so it never submits a form unexpectedly. @default 'button' */\n @Input() type: 'button' | 'submit' | 'reset' = 'button';\n /** Disables the button and blocks pointer events. @default false */\n @Input() disabled: boolean = false;\n /** Shows a spinner, sets `aria-busy`, and prevents interaction while a task runs. @default false */\n @Input() busy: boolean = false;\n\n get computedClass() {\n return cn(\n buttonVariants({\n variant: this.variant,\n size: this.size,\n busy: this.busy\n }),\n this.class\n );\n }\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "calendar",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Calendar",
|
|
5
|
+
"description": "Calendar component.",
|
|
6
|
+
"category": "date-time",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"date-fns"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "calendar.component.ts",
|
|
14
|
+
"target": "ui/calendar.component.ts",
|
|
15
|
+
"content": "import {\n Component, Input, OnInit, forwardRef, Output, EventEmitter, ChangeDetectorRef\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport {\n addDays, addMonths, subMonths, startOfMonth, endOfMonth,\n startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth,\n isSameDay, isToday, setMonth, setYear, addYears, subYears,\n isBefore, startOfDay, format\n} from 'date-fns';\nimport { cn } from './utils/cn';\n\nexport type CalendarMode = 'date' | 'month' | 'year';\n\n@Component({\n selector: 'tolle-calendar',\n standalone: true,\n imports: [CommonModule, FormsModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => CalendarComponent),\n multi: true\n }\n ],\n template: `\n <div [class]=\"cn('text-popover-foreground inline-block min-w-fit', bordered ? 'p-3 border rounded-md bg-background shadow-sm' : '', class)\">\n <!-- Header with Navigation -->\n <div class=\"flex items-center justify-between pt-1 pb-4 gap-2\">\n <!-- View Selector -->\n <div class=\"flex items-center gap-1\">\n <button *ngIf=\"mode !== 'year'\"\n type=\"button\"\n (click)=\"setView('month')\"\n [class]=\"cn(\n 'text-sm font-semibold px-2 py-1 rounded transition-colors',\n currentView === 'month' ? 'bg-secondary text-secondary-foreground' : 'hover:bg-accent hover:text-accent-foreground'\n )\">\n {{ formatMonthYear(viewDate, 'month') }}\n </button>\n\n <button\n type=\"button\"\n (click)=\"setView('year')\"\n [class]=\"cn(\n 'text-sm font-semibold px-2 py-1 rounded transition-colors',\n currentView === 'year' ? 'bg-secondary text-secondary-foreground' : 'hover:bg-accent hover:text-accent-foreground'\n )\">\n {{ formatMonthYear(viewDate, 'year') }}\n </button>\n </div>\n\n <!-- Navigation Buttons -->\n <div class=\"flex items-center space-x-1\">\n <button type=\"button\" (click)=\"prev()\" [class]=\"navBtnClass\" aria-label=\"Previous month\">\n <i class=\"ri-arrow-left-s-line text-lg\"></i>\n </button>\n <button type=\"button\" (click)=\"next()\" [class]=\"navBtnClass\" aria-label=\"Next month\">\n <i class=\"ri-arrow-right-s-line text-lg\"></i>\n </button>\n </div>\n </div>\n\n <!-- DATE MODE -->\n <div *ngIf=\"currentView === 'date' && mode === 'date'\" class=\"space-y-2 animate-in fade-in zoom-in-95 duration-200\">\n <div class=\"grid grid-cols-7 gap-1 w-full\">\n <span *ngFor=\"let day of weekDays\" class=\"text-[0.8rem] text-muted-foreground font-normal text-center w-9\">\n {{ day }}\n </span>\n </div>\n <div role=\"grid\" class=\"grid grid-cols-7 gap-1 w-full\" (keydown)=\"onGridKeydown($event)\">\n <button\n *ngFor=\"let date of daysInMonth\"\n type=\"button\"\n role=\"gridcell\"\n [attr.aria-selected]=\"isSelected(date)\"\n [attr.aria-label]=\"(date | date:'fullDate')\"\n (click)=\"selectDate(date)\"\n [disabled]=\"isDateDisabled(date)\"\n [class]=\"getDayClass(date)\"\n >\n {{ formatDate(date, 'day') }}\n </button>\n </div>\n </div>\n\n <!-- MONTH SELECTOR (for date mode and month mode) -->\n <div *ngIf=\"(currentView === 'month')\"\n class=\"grid grid-cols-3 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button\n *ngFor=\"let month of months; let i = index\"\n type=\"button\"\n (click)=\"selectMonth(i)\"\n [class]=\"getMonthClass(i)\"\n >\n {{ month }}\n </button>\n </div>\n\n <!-- YEAR SELECTOR (for date mode and year mode) -->\n <div *ngIf=\"(currentView === 'year') \"\n class=\"grid grid-cols-4 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button\n *ngFor=\"let year of years\"\n type=\"button\"\n (click)=\"selectYear(year)\"\n [class]=\"getYearClass(year)\"\n >\n {{ year }}\n </button>\n </div>\n\n <!-- Quick Actions -->\n <div *ngIf=\"showQuickActions\" class=\"border-t pt-3 mt-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <button\n type=\"button\"\n (click)=\"selectToday()\"\n [class]=\"quickActionBtnClass\"\n [disabled]=\"isTodayDisabled()\"\n >\n Today\n </button>\n <button\n type=\"button\"\n (click)=\"clear()\"\n [class]=\"quickActionBtnClass\"\n >\n Clear\n </button>\n </div>\n </div>\n </div>\n `\n})\nexport class CalendarComponent implements OnInit, ControlValueAccessor {\n @Input() class = '';\n /** Renders the calendar's own border, background and shadow. Set `false` when\n * nesting inside a card/popover that already provides the chrome. @default true */\n @Input() bordered = true;\n @Input() mode: CalendarMode = 'date';\n @Input() disablePastDates = false;\n @Input() showQuickActions = true;\n @Input() minDate?: Date;\n @Input() maxDate?: Date;\n @Input() formatMonthFn?: (date: Date) => string;\n @Input() formatYearFn?: (date: Date) => string;\n @Input() formatDateFn?: (date: Date) => string;\n\n @Output() dateSelect = new EventEmitter<Date | null>();\n\n currentView: 'date' | 'month' | 'year' = 'date';\n viewDate: Date = new Date();\n selectedDate: Date | null = null;\n\n weekDays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];\n daysInMonth: Date[] = [];\n months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n years: number[] = [];\n yearRangeStart: number;\n\n navBtnClass = cn('h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 border border-input rounded-md flex items-center justify-center hover:bg-accent hover:text-accent-foreground transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring');\n quickActionBtnClass = cn('px-3 py-1.5 text-sm rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors');\n\n onTouched: () => void = () => {};\n onChange: (value: any) => void = () => {};\n\n protected cn = cn;\n\n constructor(private cdr: ChangeDetectorRef) {\n this.yearRangeStart = new Date().getFullYear() - 6;\n }\n\n ngOnInit() {\n // Initialize based on mode\n if (this.mode === 'month') {\n this.currentView = 'month';\n } else if (this.mode === 'year') {\n this.currentView = 'year';\n }\n\n this.generateDays();\n this.generateYears();\n }\n\n // Format helpers\n formatMonthYear(date: Date, type: 'month' | 'year'): string {\n if (type === 'month' && this.formatMonthFn) {\n return this.formatMonthFn(date);\n }\n if (type === 'year' && this.formatYearFn) {\n return this.formatYearFn(date);\n }\n return type === 'month' ? format(date, 'MMMM') : format(date, 'yyyy');\n }\n\n formatDate(date: Date, type: 'day' | 'month' | 'year'): string {\n if (type === 'day' && this.formatDateFn) {\n return this.formatDateFn(date);\n }\n return format(date, type === 'day' ? 'd' : type === 'month' ? 'MMM' : 'yyyy');\n }\n\n generateDays() {\n if (this.mode !== 'date') return;\n\n const start = startOfWeek(startOfMonth(this.viewDate));\n const end = endOfWeek(endOfMonth(this.viewDate));\n this.daysInMonth = eachDayOfInterval({ start, end });\n }\n\n generateYears() {\n const currentYear = this.viewDate.getFullYear();\n\n if (this.mode === 'year') {\n // For year picker, show a 12-year grid\n this.years = Array.from({ length: 12 }, (_, i) => this.yearRangeStart + i);\n } else {\n // For date mode year selector, show 16 years centered on current\n this.years = Array.from({ length: 16 }, (_, i) => currentYear - 6 + i);\n }\n }\n\n setView(view: 'date' | 'month' | 'year') {\n this.currentView = view;\n if (view === 'year') {\n this.generateYears();\n }\n }\n\n prev() {\n if (this.mode === 'date') {\n if (this.currentView === 'date') {\n this.viewDate = subMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = subYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n }\n } else if (this.mode === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n } else if (this.mode === 'year') {\n this.yearRangeStart -= 12;\n this.generateYears();\n } else if (this.mode === 'month-year') {\n if (this.currentView === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n } else {\n this.yearRangeStart -= 12;\n this.generateYears();\n }\n }\n }\n\n next() {\n if (this.mode === 'date') {\n if (this.currentView === 'date') {\n this.viewDate = addMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = addYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n }\n } else if (this.mode === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n } else if (this.mode === 'year') {\n this.yearRangeStart += 12;\n this.generateYears();\n } else if (this.mode === 'month-year') {\n if (this.currentView === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n } else {\n this.yearRangeStart += 12;\n this.generateYears();\n }\n }\n }\n\n prevYears() {\n this.yearRangeStart -= 12;\n this.generateYears();\n }\n\n nextYears() {\n this.yearRangeStart += 12;\n this.generateYears();\n }\n\n selectDate(date: Date) {\n if (this.isDateDisabled(date)) return;\n\n this.selectedDate = date;\n this.onChange(date);\n this.onTouched();\n this.dateSelect.emit(date);\n }\n\n /**\n * Grid keyboard navigation for the day view (react-day-picker semantics).\n * Arrows move by day/week, Home/End to week edges, PageUp/PageDown by month.\n * Enter/Space are left to the native day <button> (which selects on click and\n * does not scroll the page).\n */\n onGridKeydown(event: KeyboardEvent) {\n if (this.currentView !== 'date' || this.mode !== 'date') return;\n\n const gridEl = event.currentTarget as HTMLElement;\n const buttons = Array.from(gridEl.querySelectorAll('button')) as HTMLElement[];\n const idx = buttons.indexOf(event.target as HTMLElement);\n if (idx < 0) return;\n\n const current = this.daysInMonth[idx];\n if (!current) return;\n\n let next: Date | null = null;\n switch (event.key) {\n case 'ArrowLeft': next = addDays(current, -1); break;\n case 'ArrowRight': next = addDays(current, 1); break;\n case 'ArrowUp': next = addDays(current, -7); break;\n case 'ArrowDown': next = addDays(current, 7); break;\n case 'Home': next = startOfWeek(current); break;\n case 'End': next = endOfWeek(current); break;\n case 'PageUp': next = subMonths(current, 1); break;\n case 'PageDown': next = addMonths(current, 1); break;\n default: return;\n }\n\n event.preventDefault();\n this.focusDay(next, gridEl);\n }\n\n /**\n * Moves focus to the day button matching `date`. If the day is not present in\n * the current grid, the view advances/retreats to its month, regenerates and\n * then focuses it.\n */\n private focusDay(date: Date, gridEl: HTMLElement) {\n let idx = this.daysInMonth.findIndex(d => isSameDay(d, date));\n if (idx < 0) {\n this.viewDate = startOfMonth(date);\n this.generateDays();\n this.cdr.detectChanges(); // re-render the grid synchronously\n idx = this.daysInMonth.findIndex(d => isSameDay(d, date));\n }\n if (idx < 0) return;\n\n const buttons = Array.from(gridEl.querySelectorAll('button')) as HTMLElement[];\n buttons[idx]?.focus();\n }\n\n selectMonth(monthIndex: number) {\n if (this.mode === 'date') {\n this.viewDate = setMonth(this.viewDate, monthIndex);\n this.currentView = 'date';\n this.generateDays();\n } else if (this.mode === 'month') {\n this.viewDate = setMonth(this.viewDate, monthIndex);\n this.selectedDate = this.viewDate;\n this.onChange( this.viewDate);\n this.onTouched();\n this.dateSelect.emit( this.viewDate);\n }\n }\n\n selectYear(year: number) {\n if (this.mode === 'date') {\n this.viewDate = setYear(this.viewDate, year);\n this.currentView = 'date';\n this.generateDays();\n } else if (this.mode === 'year' || this.mode === 'month') {\n this.viewDate = setYear(this.viewDate, year);\n this.selectedDate = this.viewDate;\n this.onChange( this.viewDate);\n this.onTouched();\n this.dateSelect.emit( this.viewDate);\n }\n }\n\n isSelected(date: Date): boolean {\n return !!(this.selectedDate && isSameDay(date, this.selectedDate));\n }\n\n getDayClass(date: Date) {\n const isSelected = this.selectedDate && isSameDay(date, this.selectedDate);\n const isTodayDate = isToday(date);\n const isOutside = !isSameMonth(date, this.viewDate);\n const isDisabled = this.isDateDisabled(date);\n\n return cn(\n 'h-9 w-9 p-0 font-normal text-sm rounded-md transition-all flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n !isSelected && !isDisabled && 'hover:bg-accent hover:text-accent-foreground',\n isSelected && 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground',\n !isSelected && isTodayDate && 'bg-accent text-accent-foreground',\n (isOutside || isDisabled) && 'text-muted-foreground opacity-50',\n isDisabled && 'cursor-not-allowed'\n );\n }\n\n getMonthClass(monthIndex: number) {\n const isSelected = this.selectedDate &&\n this.selectedDate.getMonth() === monthIndex &&\n this.selectedDate.getFullYear() === this.viewDate.getFullYear();\n const isCurrent = new Date().getMonth() === monthIndex &&\n new Date().getFullYear() === this.viewDate.getFullYear();\n\n return cn(\n 'text-sm py-2.5 rounded-md transition-colors',\n isSelected ? 'bg-primary text-primary-foreground hover:bg-primary' :\n isCurrent ? 'border border-primary/30 text-primary' :\n 'hover:bg-accent hover:text-accent-foreground'\n );\n }\n\n getYearClass(year: number) {\n const isSelected = this.selectedDate &&\n this.selectedDate.getFullYear() === year;\n const isCurrent = new Date().getFullYear() === year;\n\n return cn(\n 'text-sm py-2 rounded-md transition-colors',\n isSelected ? 'bg-primary text-primary-foreground hover:bg-primary' :\n isCurrent ? 'border border-primary/30 text-primary' :\n 'hover:bg-accent hover:text-accent-foreground'\n );\n }\n\n isDateDisabled(date: Date): boolean {\n if (this.disablePastDates && isBefore(date, startOfDay(new Date()))) {\n return true;\n }\n if (this.minDate && isBefore(date, this.minDate)) {\n return true;\n }\n return !!(this.maxDate && isBefore(this.maxDate, date));\n\n }\n\n isTodayDisabled(): boolean {\n return this.isDateDisabled(new Date());\n }\n\n selectToday() {\n if (!this.isTodayDisabled()) {\n this.selectDate(new Date());\n }\n }\n\n clear() {\n this.selectedDate = null;\n this.onChange(null);\n this.onTouched();\n this.dateSelect.emit(null);\n }\n\n // CVA Implementation\n writeValue(obj: any): void {\n if (obj) {\n const date = new Date(obj);\n if (!isNaN(date.getTime())) {\n this.selectedDate = date;\n this.viewDate = date;\n this.generateDays();\n this.generateYears();\n }\n }\n }\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "card",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Card",
|
|
5
|
+
"description": "Card component.",
|
|
6
|
+
"category": "layout",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "card.component.ts",
|
|
12
|
+
"target": "ui/card.component.ts",
|
|
13
|
+
"content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-card',\n standalone: true,\n imports: [CommonModule],\n // Card owns the vertical rhythm: `flex-col` + `gap-6` spaces the sections and\n // `py-6` gives top/bottom padding. Sub-sections only add horizontal padding\n // (`px-6`). This means a card renders correctly with ANY combination of\n // header / content / footer — including content-only — with no `pt-0` hacks.\n template: `\n <div [class]=\"cn('flex flex-col gap-6 rounded-lg border border-border bg-card text-card-foreground py-6 shadow-sm', class)\">\n <ng-content></ng-content>\n </div>\n `,\n})\nexport class CardComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-header',\n standalone: true,\n template: `<div [class]=\"cn('flex flex-col gap-1.5 px-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class CardHeaderComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-title',\n standalone: true,\n template: `<h3 [class]=\"cn('font-semibold leading-none tracking-tight', class)\"><ng-content></ng-content></h3>`,\n})\nexport class CardTitleComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-description',\n standalone: true,\n template: `<p [class]=\"cn('text-sm text-muted-foreground', class)\"><ng-content></ng-content></p>`,\n})\nexport class CardDescriptionComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-content',\n standalone: true,\n template: `<div [class]=\"cn('px-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class CardContentComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-footer',\n standalone: true,\n template: `<div [class]=\"cn('flex items-center px-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class CardFooterComponent { @Input() class = ''; protected cn = cn; }\n"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "utils/cn.ts",
|
|
17
|
+
"target": "ui/utils/cn.ts",
|
|
18
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "carousel",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Carousel",
|
|
5
|
+
"description": "Carousel component.",
|
|
6
|
+
"category": "media",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"embla-carousel"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "carousel/carousel.component.ts",
|
|
14
|
+
"target": "ui/carousel/carousel.component.ts",
|
|
15
|
+
"content": "import {\n Component,\n Input,\n Output,\n EventEmitter,\n ElementRef,\n ViewChild,\n AfterViewInit,\n OnDestroy,\n OnInit,\n OnChanges,\n ContentChild,\n forwardRef,\n Injectable,\n Directive,\n HostListener,\n inject,\n ChangeDetectorRef,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport EmblaCarousel, {\n EmblaCarouselType,\n EmblaOptionsType,\n EmblaPluginType,\n} from 'embla-carousel';\nimport { Subject } from 'rxjs';\n\n@Injectable()\nexport class CarouselContext {\n api$ = new Subject<EmblaCarouselType>();\n api?: EmblaCarouselType;\n orientation: 'horizontal' | 'vertical' = 'horizontal';\n canScrollPrev = false;\n canScrollNext = false;\n\n constructor(private cdr: ChangeDetectorRef) { }\n\n setApi(api: EmblaCarouselType) {\n this.api = api;\n this.api$.next(api);\n this.updateState();\n\n api.on('select', () => this.updateState());\n api.on('reInit', () => this.updateState());\n }\n\n updateState() {\n if (!this.api) return;\n this.canScrollPrev = this.api.canScrollPrev();\n this.canScrollNext = this.api.canScrollNext();\n this.cdr.detectChanges();\n }\n\n scrollPrev() {\n this.api?.scrollPrev();\n }\n\n scrollNext() {\n this.api?.scrollNext();\n }\n}\n\n@Component({\n selector: 'tolle-carousel',\n standalone: true,\n imports: [CommonModule],\n providers: [CarouselContext],\n template: `\n <div\n class=\"relative\"\n role=\"region\"\n aria-roledescription=\"carousel\"\n tabindex=\"0\"\n (keydown)=\"onKeydown($event)\"\n [class.flex-col]=\"orientation === 'vertical'\"\n >\n <ng-content></ng-content>\n </div>\n `,\n host: {\n '[class.flex]': 'orientation === \"vertical\"',\n }\n})\nexport class CarouselComponent implements AfterViewInit, OnDestroy {\n @Input() opts: EmblaOptionsType = {};\n @Input() plugins: EmblaPluginType[] = [];\n @Input() orientation: 'horizontal' | 'vertical' = 'horizontal';\n\n @Output() api = new EventEmitter<EmblaCarouselType>();\n\n private carouselContext = inject(CarouselContext);\n private emblaApi?: EmblaCarouselType;\n\n @ViewChild('container', { static: false }) containerRef?: ElementRef;\n\n constructor() { }\n\n ngOnInit() {\n this.carouselContext.orientation = this.orientation;\n }\n\n ngOnChanges() {\n this.carouselContext.orientation = this.orientation;\n }\n\n ngAfterViewInit() {\n // We expect tolleCarouselContent to provide the viewport element\n }\n\n onKeydown(event: KeyboardEvent) {\n if (event.key === 'ArrowLeft') {\n event.preventDefault();\n this.carouselContext.scrollPrev();\n } else if (event.key === 'ArrowRight') {\n event.preventDefault();\n this.carouselContext.scrollNext();\n }\n }\n\n initApi(viewport: HTMLElement) {\n this.emblaApi = EmblaCarousel(viewport, {\n ...this.opts,\n axis: this.orientation === 'horizontal' ? 'x' : 'y',\n }, this.plugins);\n\n this.carouselContext.setApi(this.emblaApi);\n this.api.emit(this.emblaApi);\n }\n\n ngOnDestroy() {\n this.emblaApi?.destroy();\n }\n}\n\n@Directive({\n selector: '[tolleCarouselContent]',\n standalone: true,\n host: {\n 'class': 'overflow-hidden',\n }\n})\nexport class CarouselContentDirective implements AfterViewInit {\n private carousel = inject(CarouselComponent);\n private el = inject(ElementRef);\n\n ngAfterViewInit() {\n this.carousel.initApi(this.el.nativeElement);\n }\n}\n\n@Directive({\n selector: '[tolleCarouselContainer]',\n standalone: true,\n host: {\n '[class]': 'carouselContext.orientation === \"horizontal\" ? \"flex -ml-4\" : \"flex flex-col -mt-4\"',\n }\n})\nexport class CarouselContainerDirective {\n carouselContext = inject(CarouselContext);\n}\n\n@Directive({\n selector: '[tolleCarouselItem]',\n standalone: true,\n host: {\n 'role': 'group',\n 'aria-roledescription': 'slide',\n '[class]': 'carouselContext.orientation === \"horizontal\" ? \"min-w-0 shrink-0 grow-0 basis-full pl-4\" : \"min-w-0 shrink-0 grow-0 basis-full pt-4\"',\n }\n})\nexport class CarouselItemDirective {\n carouselContext = inject(CarouselContext);\n}\n\n@Directive({\n selector: '[tolleCarouselPrevious]',\n standalone: true,\n host: {\n 'aria-label': 'Previous slide',\n '[attr.disabled]': '!carouselContext.canScrollPrev ? true : null',\n '(click)': 'carouselContext.scrollPrev()',\n }\n})\nexport class CarouselPreviousDirective {\n carouselContext = inject(CarouselContext);\n}\n\n@Directive({\n selector: '[tolleCarouselNext]',\n standalone: true,\n host: {\n 'aria-label': 'Next slide',\n '[attr.disabled]': '!carouselContext.canScrollNext ? true : null',\n '(click)': 'carouselContext.scrollNext()',\n }\n})\nexport class CarouselNextDirective {\n carouselContext = inject(CarouselContext);\n}\n"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "checkbox",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Checkbox",
|
|
5
|
+
"description": "Checkbox component with size and checked options.",
|
|
6
|
+
"category": "forms",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "checkbox.component.ts",
|
|
14
|
+
"target": "ui/checkbox.component.ts",
|
|
15
|
+
"content": "import { Component, Input, forwardRef, ChangeDetectorRef } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst checkboxVariants = cva(\n 'group flex shrink-0 cursor-pointer items-center justify-center p-0 rounded-sm border border-primary ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',\n {\n variants: {\n size: {\n xs: 'h-3.5 w-3.5',\n sm: 'h-4 w-4',\n default: 'h-5 w-5',\n lg: 'h-6 w-6',\n },\n checked: {\n true: 'bg-primary text-primary-foreground',\n false: 'bg-transparent',\n },\n },\n defaultVariants: {\n size: 'default',\n checked: false,\n },\n }\n);\n\nexport type CheckboxProps = VariantProps<typeof checkboxVariants>;\n\n@Component({\n selector: 'tolle-checkbox',\n standalone: true,\n imports: [CommonModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => CheckboxComponent),\n multi: true\n }\n ],\n template: `\n <button\n type=\"button\"\n role=\"checkbox\"\n [attr.aria-checked]=\"checked\"\n [attr.data-state]=\"checked ? 'checked' : 'unchecked'\"\n [disabled]=\"disabled\"\n (click)=\"toggle()\"\n [class]=\"computedClass\"\n >\n <i\n *ngIf=\"checked\"\n class=\"ri-check-line font-bold\"\n [class]=\"size === 'xs' ? 'text-[10px]' : 'text-[14px]'\"\n aria-hidden=\"true\"\n ></i>\n </button>\n `\n})\nexport class CheckboxComponent implements ControlValueAccessor {\n /** Extra Tailwind classes merged onto the checkbox via `cn()` (last-wins). */\n @Input() class = '';\n /** Disables the checkbox and blocks toggling. @default false */\n @Input() disabled = false;\n /** Size of the checkbox. @default 'default' */\n @Input() size: CheckboxProps['size'] = 'default';\n\n checked = false;\n\n onChange: any = () => {};\n onTouched: any = () => {};\n\n constructor(private cdr: ChangeDetectorRef) {}\n\n get computedClass() {\n return cn(\n checkboxVariants({ size: this.size, checked: this.checked }),\n this.class\n );\n }\n\n toggle() {\n if (this.disabled) return;\n this.checked = !this.checked;\n this.onChange(this.checked);\n this.onTouched();\n }\n\n // --- ControlValueAccessor ---\n writeValue(value: boolean): void {\n this.checked = value;\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; this.cdr.markForCheck(); }\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "collapsible",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Collapsible",
|
|
5
|
+
"description": "Collapsible component.",
|
|
6
|
+
"category": "layout",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "collapsible.component.ts",
|
|
12
|
+
"target": "ui/collapsible.component.ts",
|
|
13
|
+
"content": "import { Component, Input, Output, EventEmitter, Injectable, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\nimport { BehaviorSubject } from 'rxjs';\nimport { trigger, state, style, transition, animate } from '@angular/animations';\n\n@Injectable()\nclass CollapsibleService {\n private openSubject = new BehaviorSubject<boolean>(false);\n open$ = this.openSubject.asObservable();\n\n setOpen(value: boolean) {\n this.openSubject.next(value);\n }\n\n getOpen() {\n return this.openSubject.getValue();\n }\n\n toggle() {\n this.setOpen(!this.getOpen());\n }\n}\n\n@Component({\n selector: 'tolle-collapsible',\n standalone: true,\n imports: [CommonModule],\n providers: [CollapsibleService],\n template: `<ng-content></ng-content>`,\n host: {\n '[class]': 'computedClass'\n }\n})\nexport class CollapsibleComponent {\n @Input() set open(val: boolean) {\n this.collapsibleService.setOpen(val);\n }\n @Output() openChange = new EventEmitter<boolean>();\n @Input() class: string = '';\n\n private collapsibleService = inject(CollapsibleService);\n\n ngOnInit() {\n this.collapsibleService.open$.subscribe(val => {\n this.openChange.emit(val);\n });\n }\n\n get computedClass() {\n return cn(\"w-full\", this.class);\n }\n}\n\n@Component({\n selector: 'tolle-collapsible-trigger',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: {\n 'role': 'button',\n '[attr.tabindex]': '0',\n '[attr.aria-expanded]': 'isOpen',\n '[attr.data-state]': 'isOpen ? \"open\" : \"closed\"',\n '(click)': 'onToggle()',\n '(keydown.enter)': 'onToggle()',\n '(keydown.space)': 'onToggle(); $event.preventDefault()',\n '[class]': 'computedClass'\n }\n})\nexport class CollapsibleTriggerComponent {\n @Input() class: string = '';\n\n private collapsibleService = inject(CollapsibleService);\n isOpen = false;\n\n constructor() {\n this.collapsibleService.open$.subscribe(open => {\n this.isOpen = open;\n });\n }\n\n onToggle() {\n this.collapsibleService.toggle();\n }\n\n get computedClass() {\n return cn(\"cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\", this.class);\n }\n}\n\n@Component({\n selector: 'tolle-collapsible-content',\n standalone: true,\n imports: [CommonModule],\n template: `\n <div [@expandCollapse]=\"isOpen ? 'open' : 'closed'\" class=\"overflow-hidden\">\n <ng-content></ng-content>\n </div>\n `,\n animations: [\n trigger('expandCollapse', [\n state('closed', style({ height: '0px', opacity: 0 })),\n state('open', style({ height: '*', opacity: 1 })),\n transition('closed <=> open', animate('300ms cubic-bezier(0.4, 0, 0.2, 1)'))\n ])\n ],\n host: {\n '[attr.data-state]': 'isOpen ? \"open\" : \"closed\"',\n // `inert` (not `hidden`) so the height/opacity collapse animation still runs\n // while removing collapsed content from the tab order + pointer interaction.\n '[attr.inert]': 'isOpen ? null : \"\"',\n '[class]': 'computedClass'\n }\n})\nexport class CollapsibleContentComponent {\n @Input() class: string = '';\n\n private collapsibleService = inject(CollapsibleService);\n isOpen = false;\n\n constructor() {\n this.collapsibleService.open$.subscribe(open => {\n this.isOpen = open;\n });\n }\n\n get computedClass() {\n return cn(\"\", this.class);\n }\n}\n"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "utils/cn.ts",
|
|
17
|
+
"target": "ui/utils/cn.ts",
|
|
18
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|