@sonny-ui/core 0.1.0-alpha.2 → 0.1.0-alpha.21
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/README.md +187 -40
- package/fesm2022/sonny-ui-core.mjs +6646 -272
- package/fesm2022/sonny-ui-core.mjs.map +1 -1
- package/package.json +8 -5
- package/schematics/ng-add/index.js +27 -0
- package/schematics/ng-add/schema.json +1 -1
- package/schematics/ng-generate/component/index.js +182 -1
- package/schematics/ng-generate/component/schema.json +2 -2
- package/src/lib/accordion/accordion.directives.spec.ts +173 -0
- package/src/lib/accordion/accordion.directives.ts +143 -0
- package/src/lib/accordion/index.ts +8 -0
- package/src/lib/alert/alert.directives.spec.ts +154 -0
- package/src/lib/alert/alert.directives.ts +67 -0
- package/src/lib/alert/alert.variants.ts +25 -0
- package/src/lib/alert/index.ts +6 -0
- package/src/lib/avatar/avatar.component.spec.ts +75 -0
- package/src/lib/avatar/avatar.component.ts +43 -0
- package/src/lib/avatar/avatar.variants.ts +26 -0
- package/src/lib/avatar/index.ts +2 -0
- package/src/lib/avatar-group/avatar-group.component.spec.ts +74 -0
- package/src/lib/avatar-group/avatar-group.component.ts +88 -0
- package/src/lib/avatar-group/index.ts +1 -0
- package/src/lib/badge/badge.directive.spec.ts +74 -0
- package/src/lib/badge/badge.directive.ts +17 -0
- package/src/lib/badge/badge.variants.ts +29 -0
- package/src/lib/badge/index.ts +2 -0
- package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
- package/src/lib/breadcrumb/breadcrumb.directives.ts +78 -0
- package/src/lib/breadcrumb/index.ts +8 -0
- package/src/lib/button/button.directive.spec.ts +92 -0
- package/src/lib/button/button.directive.ts +28 -0
- package/src/lib/button/button.variants.ts +30 -0
- package/src/lib/button/index.ts +2 -0
- package/src/lib/button-group/button-group.directive.spec.ts +46 -0
- package/src/lib/button-group/button-group.directive.ts +19 -0
- package/src/lib/button-group/button-group.variants.ts +18 -0
- package/src/lib/button-group/index.ts +2 -0
- package/src/lib/calendar/calendar.component.spec.ts +192 -0
- package/src/lib/calendar/calendar.component.ts +342 -0
- package/src/lib/calendar/calendar.types.ts +24 -0
- package/src/lib/calendar/index.ts +7 -0
- package/src/lib/card/card.directives.spec.ts +104 -0
- package/src/lib/card/card.directives.ts +72 -0
- package/src/lib/card/card.variants.ts +28 -0
- package/src/lib/card/index.ts +9 -0
- package/src/lib/carousel/carousel.directives.spec.ts +85 -0
- package/src/lib/carousel/carousel.directives.ts +159 -0
- package/src/lib/carousel/index.ts +8 -0
- package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
- package/src/lib/chat-bubble/chat-bubble.directives.ts +96 -0
- package/src/lib/chat-bubble/index.ts +11 -0
- package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
- package/src/lib/checkbox/checkbox.directive.ts +16 -0
- package/src/lib/checkbox/checkbox.variants.ts +19 -0
- package/src/lib/checkbox/index.ts +2 -0
- package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
- package/src/lib/color-picker/color-picker.component.ts +537 -0
- package/src/lib/color-picker/color-picker.types.ts +24 -0
- package/src/lib/color-picker/color-picker.utils.ts +183 -0
- package/src/lib/color-picker/color-picker.variants.ts +17 -0
- package/src/lib/color-picker/index.ts +20 -0
- package/src/lib/combobox/combobox.component.spec.ts +151 -0
- package/src/lib/combobox/combobox.component.ts +264 -0
- package/src/lib/combobox/combobox.variants.ts +19 -0
- package/src/lib/combobox/index.ts +2 -0
- package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
- package/src/lib/command-palette/command-palette.component.ts +194 -0
- package/src/lib/command-palette/command-palette.service.ts +36 -0
- package/src/lib/command-palette/command-palette.types.ts +23 -0
- package/src/lib/command-palette/index.ts +7 -0
- package/src/lib/data-table/data-table.component.spec.ts +443 -0
- package/src/lib/data-table/data-table.component.ts +602 -0
- package/src/lib/data-table/data-table.directives.ts +31 -0
- package/src/lib/data-table/data-table.types.ts +20 -0
- package/src/lib/data-table/index.ts +13 -0
- package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
- package/src/lib/date-picker/date-picker.component.ts +220 -0
- package/src/lib/date-picker/date-picker.variants.ts +17 -0
- package/src/lib/date-picker/index.ts +2 -0
- package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
- package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
- package/src/lib/date-range-picker/index.ts +1 -0
- package/src/lib/diff/diff.component.spec.ts +47 -0
- package/src/lib/diff/diff.component.ts +82 -0
- package/src/lib/diff/index.ts +1 -0
- package/src/lib/divider/divider.component.spec.ts +48 -0
- package/src/lib/divider/divider.component.ts +51 -0
- package/src/lib/divider/divider.variants.ts +22 -0
- package/src/lib/divider/index.ts +2 -0
- package/src/lib/dock/dock.directives.spec.ts +85 -0
- package/src/lib/dock/dock.directives.ts +81 -0
- package/src/lib/dock/index.ts +1 -0
- package/src/lib/drawer/drawer.directives.spec.ts +62 -0
- package/src/lib/drawer/drawer.directives.ts +80 -0
- package/src/lib/drawer/index.ts +8 -0
- package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
- package/src/lib/dropdown/dropdown.directives.ts +136 -0
- package/src/lib/dropdown/dropdown.variants.ts +27 -0
- package/src/lib/dropdown/index.ts +15 -0
- package/src/lib/fab/fab.directives.spec.ts +60 -0
- package/src/lib/fab/fab.directives.ts +77 -0
- package/src/lib/fab/index.ts +8 -0
- package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
- package/src/lib/fieldset/fieldset.directives.ts +49 -0
- package/src/lib/fieldset/fieldset.variants.ts +15 -0
- package/src/lib/fieldset/index.ts +6 -0
- package/src/lib/file-input/file-input.component.spec.ts +114 -0
- package/src/lib/file-input/file-input.component.ts +155 -0
- package/src/lib/file-input/file-input.variants.ts +25 -0
- package/src/lib/file-input/index.ts +6 -0
- package/src/lib/indicator/index.ts +6 -0
- package/src/lib/indicator/indicator.directives.spec.ts +64 -0
- package/src/lib/indicator/indicator.directives.ts +59 -0
- package/src/lib/input/index.ts +3 -0
- package/src/lib/input/input.directive.spec.ts +103 -0
- package/src/lib/input/input.directive.ts +25 -0
- package/src/lib/input/input.variants.ts +42 -0
- package/src/lib/input/label.directive.ts +16 -0
- package/src/lib/kbd/index.ts +2 -0
- package/src/lib/kbd/kbd.directive.spec.ts +42 -0
- package/src/lib/kbd/kbd.directive.ts +18 -0
- package/src/lib/kbd/kbd.variants.ts +19 -0
- package/src/lib/link/index.ts +2 -0
- package/src/lib/link/link.directive.spec.ts +41 -0
- package/src/lib/link/link.directive.ts +18 -0
- package/src/lib/link/link.variants.ts +20 -0
- package/src/lib/list/index.ts +8 -0
- package/src/lib/list/list.directives.spec.ts +65 -0
- package/src/lib/list/list.directives.ts +81 -0
- package/src/lib/loader/index.ts +2 -0
- package/src/lib/loader/loader.component.spec.ts +58 -0
- package/src/lib/loader/loader.component.ts +47 -0
- package/src/lib/loader/loader.variants.ts +21 -0
- package/src/lib/modal/dialog-ref.ts +19 -0
- package/src/lib/modal/dialog.directives.ts +84 -0
- package/src/lib/modal/dialog.service.spec.ts +52 -0
- package/src/lib/modal/dialog.service.ts +61 -0
- package/src/lib/modal/dialog.types.ts +16 -0
- package/src/lib/modal/index.ts +11 -0
- package/src/lib/navbar/index.ts +7 -0
- package/src/lib/navbar/navbar.directives.spec.ts +59 -0
- package/src/lib/navbar/navbar.directives.ts +57 -0
- package/src/lib/number-input/index.ts +2 -0
- package/src/lib/number-input/number-input.component.spec.ts +151 -0
- package/src/lib/number-input/number-input.component.ts +152 -0
- package/src/lib/number-input/number-input.variants.ts +17 -0
- package/src/lib/otp-input/index.ts +2 -0
- package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
- package/src/lib/otp-input/otp-input.component.ts +274 -0
- package/src/lib/otp-input/otp-input.variants.ts +18 -0
- package/src/lib/pagination/index.ts +6 -0
- package/src/lib/pagination/pagination.component.spec.ts +59 -0
- package/src/lib/pagination/pagination.component.ts +143 -0
- package/src/lib/pagination/pagination.variants.ts +31 -0
- package/src/lib/popover/index.ts +6 -0
- package/src/lib/popover/popover.directives.spec.ts +147 -0
- package/src/lib/popover/popover.directives.ts +151 -0
- package/src/lib/progress/index.ts +7 -0
- package/src/lib/progress/progress.component.spec.ts +117 -0
- package/src/lib/progress/progress.component.ts +64 -0
- package/src/lib/progress/progress.variants.ts +43 -0
- package/src/lib/radial-progress/index.ts +5 -0
- package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
- package/src/lib/radial-progress/radial-progress.component.ts +70 -0
- package/src/lib/radio/index.ts +2 -0
- package/src/lib/radio/radio.directive.spec.ts +46 -0
- package/src/lib/radio/radio.directive.ts +16 -0
- package/src/lib/radio/radio.variants.ts +19 -0
- package/src/lib/rating/index.ts +2 -0
- package/src/lib/rating/rating.component.spec.ts +157 -0
- package/src/lib/rating/rating.component.ts +163 -0
- package/src/lib/rating/rating.variants.ts +20 -0
- package/src/lib/select/index.ts +2 -0
- package/src/lib/select/select.component.spec.ts +112 -0
- package/src/lib/select/select.component.ts +235 -0
- package/src/lib/select/select.variants.ts +19 -0
- package/src/lib/sheet/index.ts +10 -0
- package/src/lib/sheet/sheet-ref.ts +18 -0
- package/src/lib/sheet/sheet.component.spec.ts +67 -0
- package/src/lib/sheet/sheet.directives.ts +70 -0
- package/src/lib/sheet/sheet.service.ts +100 -0
- package/src/lib/sheet/sheet.types.ts +23 -0
- package/src/lib/skeleton/index.ts +2 -0
- package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
- package/src/lib/skeleton/skeleton.directive.ts +21 -0
- package/src/lib/skeleton/skeleton.variants.ts +27 -0
- package/src/lib/slider/index.ts +2 -0
- package/src/lib/slider/slider.component.spec.ts +104 -0
- package/src/lib/slider/slider.component.ts +181 -0
- package/src/lib/slider/slider.variants.ts +25 -0
- package/src/lib/stat/index.ts +8 -0
- package/src/lib/stat/stat.directives.spec.ts +60 -0
- package/src/lib/stat/stat.directives.ts +79 -0
- package/src/lib/status/index.ts +2 -0
- package/src/lib/status/status.directive.spec.ts +43 -0
- package/src/lib/status/status.directive.ts +37 -0
- package/src/lib/status/status.variants.ts +26 -0
- package/src/lib/steps/index.ts +8 -0
- package/src/lib/steps/steps.directives.spec.ts +52 -0
- package/src/lib/steps/steps.directives.ts +78 -0
- package/src/lib/switch/index.ts +2 -0
- package/src/lib/switch/switch.component.spec.ts +98 -0
- package/src/lib/switch/switch.component.ts +76 -0
- package/src/lib/switch/switch.variants.ts +31 -0
- package/src/lib/table/index.ts +12 -0
- package/src/lib/table/table.directives.spec.ts +111 -0
- package/src/lib/table/table.directives.ts +126 -0
- package/src/lib/table/table.variants.ts +36 -0
- package/src/lib/tabs/index.ts +8 -0
- package/src/lib/tabs/tabs.directives.spec.ts +136 -0
- package/src/lib/tabs/tabs.directives.ts +126 -0
- package/src/lib/tabs/tabs.variants.ts +17 -0
- package/src/lib/tag-input/index.ts +2 -0
- package/src/lib/tag-input/tag-input.component.spec.ts +190 -0
- package/src/lib/tag-input/tag-input.component.ts +172 -0
- package/src/lib/tag-input/tag-input.variants.ts +31 -0
- package/src/lib/textarea/index.ts +7 -0
- package/src/lib/textarea/textarea.directive.spec.ts +84 -0
- package/src/lib/textarea/textarea.directive.ts +71 -0
- package/src/lib/textarea/textarea.variants.ts +34 -0
- package/src/lib/timeline/index.ts +11 -0
- package/src/lib/timeline/timeline.directives.spec.ts +55 -0
- package/src/lib/timeline/timeline.directives.ts +85 -0
- package/src/lib/toast/index.ts +3 -0
- package/src/lib/toast/toast.service.spec.ts +71 -0
- package/src/lib/toast/toast.service.ts +60 -0
- package/src/lib/toast/toast.variants.ts +38 -0
- package/src/lib/toast/toaster.component.spec.ts +38 -0
- package/src/lib/toast/toaster.component.ts +81 -0
- package/src/lib/toggle/index.ts +2 -0
- package/src/lib/toggle/toggle.directive.spec.ts +100 -0
- package/src/lib/toggle/toggle.directive.ts +61 -0
- package/src/lib/toggle/toggle.variants.ts +25 -0
- package/src/lib/tooltip/index.ts +2 -0
- package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
- package/src/lib/tooltip/tooltip.directive.ts +130 -0
- package/src/lib/tooltip/tooltip.variants.ts +20 -0
- package/src/lib/validator/index.ts +5 -0
- package/src/lib/validator/validator.directives.spec.ts +47 -0
- package/src/lib/validator/validator.directives.ts +50 -0
- package/src/styles/sonny-theme.css +45 -0
- package/types/sonny-ui-core.d.ts +1443 -13
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
input,
|
|
6
|
+
} from '@angular/core';
|
|
7
|
+
import { cn } from '../core/utils/cn';
|
|
8
|
+
|
|
9
|
+
export interface AvatarGroupItem {
|
|
10
|
+
src?: string;
|
|
11
|
+
alt?: string;
|
|
12
|
+
fallback?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sizeMap = {
|
|
16
|
+
sm: { avatar: 'h-7 w-7 text-xs', counter: 'h-7 w-7 text-[10px]' },
|
|
17
|
+
md: { avatar: 'h-9 w-9 text-sm', counter: 'h-9 w-9 text-xs' },
|
|
18
|
+
lg: { avatar: 'h-11 w-11 text-base', counter: 'h-11 w-11 text-sm' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const spacingMap = {
|
|
22
|
+
tight: '-space-x-3',
|
|
23
|
+
normal: '-space-x-2',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type AvatarGroupSize = 'sm' | 'md' | 'lg';
|
|
27
|
+
export type AvatarGroupSpacing = 'tight' | 'normal';
|
|
28
|
+
|
|
29
|
+
@Component({
|
|
30
|
+
selector: 'sny-avatar-group',
|
|
31
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
32
|
+
template: `
|
|
33
|
+
<div [class]="containerClass()" role="group" [attr.aria-label]="'Group of ' + items().length + ' users'">
|
|
34
|
+
@for (item of visibleItems(); track $index) {
|
|
35
|
+
@if (item.src) {
|
|
36
|
+
<img
|
|
37
|
+
[src]="item.src"
|
|
38
|
+
[alt]="item.alt ?? ''"
|
|
39
|
+
[class]="avatarClass()"
|
|
40
|
+
/>
|
|
41
|
+
} @else {
|
|
42
|
+
<div [class]="fallbackClass()">
|
|
43
|
+
{{ item.fallback ?? '?' }}
|
|
44
|
+
</div>
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
@if (overflowCount() > 0) {
|
|
48
|
+
<div [class]="counterClass()" [title]="overflowCount() + ' more'">
|
|
49
|
+
+{{ overflowCount() }}
|
|
50
|
+
</div>
|
|
51
|
+
}
|
|
52
|
+
</div>
|
|
53
|
+
`,
|
|
54
|
+
})
|
|
55
|
+
export class SnyAvatarGroupComponent {
|
|
56
|
+
readonly items = input.required<AvatarGroupItem[]>();
|
|
57
|
+
readonly max = input(3);
|
|
58
|
+
readonly size = input<AvatarGroupSize>('md');
|
|
59
|
+
readonly spacing = input<AvatarGroupSpacing>('normal');
|
|
60
|
+
|
|
61
|
+
readonly visibleItems = computed(() => this.items().slice(0, this.max()));
|
|
62
|
+
readonly overflowCount = computed(() => Math.max(0, this.items().length - this.max()));
|
|
63
|
+
|
|
64
|
+
readonly containerClass = computed(() =>
|
|
65
|
+
cn('flex items-center', spacingMap[this.spacing()])
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
readonly avatarClass = computed(() =>
|
|
69
|
+
cn(
|
|
70
|
+
'inline-block rounded-full object-cover ring-2 ring-background',
|
|
71
|
+
sizeMap[this.size()].avatar
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
readonly fallbackClass = computed(() =>
|
|
76
|
+
cn(
|
|
77
|
+
'inline-flex items-center justify-center rounded-full bg-muted text-muted-foreground font-medium ring-2 ring-background',
|
|
78
|
+
sizeMap[this.size()].avatar
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
readonly counterClass = computed(() =>
|
|
83
|
+
cn(
|
|
84
|
+
'inline-flex items-center justify-center rounded-full bg-muted text-muted-foreground font-semibold ring-2 ring-background',
|
|
85
|
+
sizeMap[this.size()].counter
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SnyAvatarGroupComponent, type AvatarGroupItem, type AvatarGroupSize, type AvatarGroupSpacing } from './avatar-group.component';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyBadgeDirective } from './badge.directive';
|
|
4
|
+
import type { BadgeVariant, BadgeSize } from './badge.variants';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [SnyBadgeDirective],
|
|
9
|
+
template: `<span snyBadge [variant]="variant()" [size]="size()">Badge</span>`,
|
|
10
|
+
})
|
|
11
|
+
class TestHostComponent {
|
|
12
|
+
variant = signal<BadgeVariant>('default');
|
|
13
|
+
size = signal<BadgeSize>('md');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('SnyBadgeDirective', () => {
|
|
17
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
18
|
+
let el: HTMLElement;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
await TestBed.configureTestingModule({
|
|
22
|
+
imports: [TestHostComponent],
|
|
23
|
+
}).compileComponents();
|
|
24
|
+
|
|
25
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
26
|
+
fixture.detectChanges();
|
|
27
|
+
el = fixture.nativeElement.querySelector('[snyBadge]');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should apply default variant classes', () => {
|
|
31
|
+
expect(el.className).toContain('bg-primary');
|
|
32
|
+
expect(el.className).toContain('text-primary-foreground');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should apply destructive variant', () => {
|
|
36
|
+
fixture.componentInstance.variant.set('destructive');
|
|
37
|
+
fixture.detectChanges();
|
|
38
|
+
expect(el.className).toContain('bg-destructive');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should apply outline variant', () => {
|
|
42
|
+
fixture.componentInstance.variant.set('outline');
|
|
43
|
+
fixture.detectChanges();
|
|
44
|
+
expect(el.className).toContain('border-border');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should apply success variant', () => {
|
|
48
|
+
fixture.componentInstance.variant.set('success');
|
|
49
|
+
fixture.detectChanges();
|
|
50
|
+
expect(el.className).toContain('bg-green-600');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should apply warning variant', () => {
|
|
54
|
+
fixture.componentInstance.variant.set('warning');
|
|
55
|
+
fixture.detectChanges();
|
|
56
|
+
expect(el.className).toContain('bg-yellow-500');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should apply sm size', () => {
|
|
60
|
+
fixture.componentInstance.size.set('sm');
|
|
61
|
+
fixture.detectChanges();
|
|
62
|
+
expect(el.className).toContain('text-[10px]');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should apply lg size', () => {
|
|
66
|
+
fixture.componentInstance.size.set('lg');
|
|
67
|
+
fixture.detectChanges();
|
|
68
|
+
expect(el.className).toContain('text-sm');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should have rounded-full class', () => {
|
|
72
|
+
expect(el.className).toContain('rounded-full');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import { badgeVariants, type BadgeVariant, type BadgeSize } from './badge.variants';
|
|
4
|
+
|
|
5
|
+
@Directive({
|
|
6
|
+
selector: '[snyBadge]',
|
|
7
|
+
host: { '[class]': 'computedClass()' },
|
|
8
|
+
})
|
|
9
|
+
export class SnyBadgeDirective {
|
|
10
|
+
readonly variant = input<BadgeVariant>('default');
|
|
11
|
+
readonly size = input<BadgeSize>('md');
|
|
12
|
+
readonly class = input<string>('');
|
|
13
|
+
|
|
14
|
+
protected readonly computedClass = computed(() =>
|
|
15
|
+
cn(badgeVariants({ variant: this.variant(), size: this.size() }), this.class())
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const badgeVariants = cva(
|
|
4
|
+
'inline-flex items-center rounded-full border font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
5
|
+
{
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: 'border-transparent bg-primary text-primary-foreground',
|
|
9
|
+
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
10
|
+
outline: 'border-border text-foreground',
|
|
11
|
+
destructive: 'border-transparent bg-destructive text-destructive-foreground',
|
|
12
|
+
success: 'border-transparent bg-green-600 text-white dark:bg-green-500',
|
|
13
|
+
warning: 'border-transparent bg-yellow-500 text-white dark:bg-yellow-400 dark:text-black',
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
sm: 'px-2 py-0.5 text-[10px]',
|
|
17
|
+
md: 'px-2.5 py-0.5 text-xs',
|
|
18
|
+
lg: 'px-3 py-1 text-sm',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: 'default',
|
|
23
|
+
size: 'md',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export type BadgeVariant = 'default' | 'secondary' | 'outline' | 'destructive' | 'success' | 'warning';
|
|
29
|
+
export type BadgeSize = 'sm' | 'md' | 'lg';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import {
|
|
4
|
+
SnyBreadcrumbDirective,
|
|
5
|
+
SnyBreadcrumbListDirective,
|
|
6
|
+
SnyBreadcrumbItemDirective,
|
|
7
|
+
SnyBreadcrumbLinkDirective,
|
|
8
|
+
SnyBreadcrumbSeparatorDirective,
|
|
9
|
+
SnyBreadcrumbPageDirective,
|
|
10
|
+
} from './breadcrumb.directives';
|
|
11
|
+
|
|
12
|
+
@Component({
|
|
13
|
+
standalone: true,
|
|
14
|
+
imports: [
|
|
15
|
+
SnyBreadcrumbDirective,
|
|
16
|
+
SnyBreadcrumbListDirective,
|
|
17
|
+
SnyBreadcrumbItemDirective,
|
|
18
|
+
SnyBreadcrumbLinkDirective,
|
|
19
|
+
SnyBreadcrumbSeparatorDirective,
|
|
20
|
+
SnyBreadcrumbPageDirective,
|
|
21
|
+
],
|
|
22
|
+
template: `
|
|
23
|
+
<nav snyBreadcrumb>
|
|
24
|
+
<ol snyBreadcrumbList>
|
|
25
|
+
<li snyBreadcrumbItem>
|
|
26
|
+
<a snyBreadcrumbLink href="/">Home</a>
|
|
27
|
+
</li>
|
|
28
|
+
<li snyBreadcrumbSeparator>/</li>
|
|
29
|
+
<li snyBreadcrumbItem>
|
|
30
|
+
<span snyBreadcrumbPage>Current</span>
|
|
31
|
+
</li>
|
|
32
|
+
</ol>
|
|
33
|
+
</nav>
|
|
34
|
+
`,
|
|
35
|
+
})
|
|
36
|
+
class TestHostComponent {}
|
|
37
|
+
|
|
38
|
+
describe('Breadcrumb Directives', () => {
|
|
39
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
40
|
+
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
await TestBed.configureTestingModule({
|
|
43
|
+
imports: [TestHostComponent],
|
|
44
|
+
}).compileComponents();
|
|
45
|
+
|
|
46
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
47
|
+
fixture.detectChanges();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should set aria-label on nav', () => {
|
|
51
|
+
const nav = fixture.nativeElement.querySelector('nav');
|
|
52
|
+
expect(nav.getAttribute('aria-label')).toBe('Breadcrumb');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should apply list classes', () => {
|
|
56
|
+
const ol = fixture.nativeElement.querySelector('ol');
|
|
57
|
+
expect(ol.className).toContain('flex');
|
|
58
|
+
expect(ol.className).toContain('items-center');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should apply link hover classes', () => {
|
|
62
|
+
const link = fixture.nativeElement.querySelector('[snyBreadcrumbLink]');
|
|
63
|
+
expect(link.className).toContain('hover:text-foreground');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should set aria-current="page" on page', () => {
|
|
67
|
+
const page = fixture.nativeElement.querySelector('[snyBreadcrumbPage]');
|
|
68
|
+
expect(page.getAttribute('aria-current')).toBe('page');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should set aria-hidden on separator', () => {
|
|
72
|
+
const sep = fixture.nativeElement.querySelector('[snyBreadcrumbSeparator]');
|
|
73
|
+
expect(sep.getAttribute('aria-hidden')).toBe('true');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should set role="presentation" on separator', () => {
|
|
77
|
+
const sep = fixture.nativeElement.querySelector('[snyBreadcrumbSeparator]');
|
|
78
|
+
expect(sep.getAttribute('role')).toBe('presentation');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
|
|
4
|
+
@Directive({
|
|
5
|
+
selector: 'nav[snyBreadcrumb]',
|
|
6
|
+
host: {
|
|
7
|
+
'[class]': 'computedClass()',
|
|
8
|
+
'aria-label': 'Breadcrumb',
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
export class SnyBreadcrumbDirective {
|
|
12
|
+
readonly class = input<string>('');
|
|
13
|
+
protected readonly computedClass = computed(() => cn('', this.class()));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Directive({
|
|
17
|
+
selector: 'ol[snyBreadcrumbList]',
|
|
18
|
+
host: { '[class]': 'computedClass()' },
|
|
19
|
+
})
|
|
20
|
+
export class SnyBreadcrumbListDirective {
|
|
21
|
+
readonly class = input<string>('');
|
|
22
|
+
protected readonly computedClass = computed(() =>
|
|
23
|
+
cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', this.class())
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Directive({
|
|
28
|
+
selector: 'li[snyBreadcrumbItem]',
|
|
29
|
+
host: { '[class]': 'computedClass()' },
|
|
30
|
+
})
|
|
31
|
+
export class SnyBreadcrumbItemDirective {
|
|
32
|
+
readonly class = input<string>('');
|
|
33
|
+
protected readonly computedClass = computed(() =>
|
|
34
|
+
cn('inline-flex items-center gap-1.5', this.class())
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Directive({
|
|
39
|
+
selector: '[snyBreadcrumbLink]',
|
|
40
|
+
host: { '[class]': 'computedClass()' },
|
|
41
|
+
})
|
|
42
|
+
export class SnyBreadcrumbLinkDirective {
|
|
43
|
+
readonly class = input<string>('');
|
|
44
|
+
protected readonly computedClass = computed(() =>
|
|
45
|
+
cn('transition-colors hover:text-foreground', this.class())
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Directive({
|
|
50
|
+
selector: '[snyBreadcrumbSeparator]',
|
|
51
|
+
host: {
|
|
52
|
+
role: 'presentation',
|
|
53
|
+
'[aria-hidden]': 'true',
|
|
54
|
+
'[class]': 'computedClass()',
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
export class SnyBreadcrumbSeparatorDirective {
|
|
58
|
+
readonly class = input<string>('');
|
|
59
|
+
protected readonly computedClass = computed(() =>
|
|
60
|
+
cn('[&>svg]:size-3.5', this.class())
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Directive({
|
|
65
|
+
selector: '[snyBreadcrumbPage]',
|
|
66
|
+
host: {
|
|
67
|
+
role: 'link',
|
|
68
|
+
'aria-disabled': 'true',
|
|
69
|
+
'[attr.aria-current]': '"page"',
|
|
70
|
+
'[class]': 'computedClass()',
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
export class SnyBreadcrumbPageDirective {
|
|
74
|
+
readonly class = input<string>('');
|
|
75
|
+
protected readonly computedClass = computed(() =>
|
|
76
|
+
cn('font-normal text-foreground', this.class())
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyButtonDirective } from './button.directive';
|
|
4
|
+
import type { ButtonVariant, ButtonSize } from './button.variants';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [SnyButtonDirective],
|
|
9
|
+
template: `<button snyBtn [variant]="variant()" [size]="size()" [disabled]="disabled()" [loading]="loading()">Click</button>`,
|
|
10
|
+
})
|
|
11
|
+
class TestHostComponent {
|
|
12
|
+
variant = signal<ButtonVariant>('default');
|
|
13
|
+
size = signal<ButtonSize>('md');
|
|
14
|
+
disabled = signal(false);
|
|
15
|
+
loading = signal(false);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('SnyButtonDirective', () => {
|
|
19
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
20
|
+
let button: HTMLButtonElement;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
await TestBed.configureTestingModule({
|
|
24
|
+
imports: [TestHostComponent],
|
|
25
|
+
}).compileComponents();
|
|
26
|
+
|
|
27
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
28
|
+
fixture.detectChanges();
|
|
29
|
+
button = fixture.nativeElement.querySelector('button');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should apply default variant classes', () => {
|
|
33
|
+
expect(button.className).toContain('bg-primary');
|
|
34
|
+
expect(button.className).toContain('text-primary-foreground');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should apply destructive variant', () => {
|
|
38
|
+
fixture.componentInstance.variant.set('destructive');
|
|
39
|
+
fixture.detectChanges();
|
|
40
|
+
expect(button.className).toContain('bg-destructive');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should apply outline variant', () => {
|
|
44
|
+
fixture.componentInstance.variant.set('outline');
|
|
45
|
+
fixture.detectChanges();
|
|
46
|
+
expect(button.className).toContain('border');
|
|
47
|
+
expect(button.className).toContain('bg-background');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should apply ghost variant', () => {
|
|
51
|
+
fixture.componentInstance.variant.set('ghost');
|
|
52
|
+
fixture.detectChanges();
|
|
53
|
+
expect(button.className).toContain('hover:bg-accent');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should apply sm size', () => {
|
|
57
|
+
fixture.componentInstance.size.set('sm');
|
|
58
|
+
fixture.detectChanges();
|
|
59
|
+
expect(button.className).toContain('h-9');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should apply lg size', () => {
|
|
63
|
+
fixture.componentInstance.size.set('lg');
|
|
64
|
+
fixture.detectChanges();
|
|
65
|
+
expect(button.className).toContain('h-11');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should apply icon size', () => {
|
|
69
|
+
fixture.componentInstance.size.set('icon');
|
|
70
|
+
fixture.detectChanges();
|
|
71
|
+
expect(button.className).toContain('w-10');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should set aria-disabled when disabled', () => {
|
|
75
|
+
fixture.componentInstance.disabled.set(true);
|
|
76
|
+
fixture.detectChanges();
|
|
77
|
+
expect(button.getAttribute('aria-disabled')).toBe('true');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should set aria-disabled when loading', () => {
|
|
81
|
+
fixture.componentInstance.loading.set(true);
|
|
82
|
+
fixture.detectChanges();
|
|
83
|
+
expect(button.getAttribute('aria-disabled')).toBe('true');
|
|
84
|
+
expect(button.className).toContain('cursor-wait');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should set tabindex=-1 when disabled', () => {
|
|
88
|
+
fixture.componentInstance.disabled.set(true);
|
|
89
|
+
fixture.detectChanges();
|
|
90
|
+
expect(button.getAttribute('tabindex')).toBe('-1');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import { buttonVariants, type ButtonVariant, type ButtonSize } from './button.variants';
|
|
4
|
+
|
|
5
|
+
@Directive({
|
|
6
|
+
selector: 'button[snyBtn], a[snyBtn]',
|
|
7
|
+
host: {
|
|
8
|
+
'[class]': 'computedClass()',
|
|
9
|
+
'[attr.aria-disabled]': 'disabled() || loading() || null',
|
|
10
|
+
'[attr.disabled]': 'disabled() || loading() || null',
|
|
11
|
+
'[attr.tabindex]': '(disabled() || loading()) ? -1 : null',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
export class SnyButtonDirective {
|
|
15
|
+
readonly variant = input<ButtonVariant>('default');
|
|
16
|
+
readonly size = input<ButtonSize>('md');
|
|
17
|
+
readonly disabled = input(false);
|
|
18
|
+
readonly loading = input(false);
|
|
19
|
+
readonly class = input<string>('');
|
|
20
|
+
|
|
21
|
+
protected readonly computedClass = computed(() =>
|
|
22
|
+
cn(
|
|
23
|
+
buttonVariants({ variant: this.variant(), size: this.size() }),
|
|
24
|
+
this.loading() && 'cursor-wait opacity-70',
|
|
25
|
+
this.class()
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const buttonVariants = cva(
|
|
4
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
5
|
+
{
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
9
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
10
|
+
outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground',
|
|
11
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
12
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
13
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
sm: 'h-9 rounded-sm px-3',
|
|
17
|
+
md: 'h-10 px-4 py-2',
|
|
18
|
+
lg: 'h-11 rounded-sm px-8',
|
|
19
|
+
icon: 'h-10 w-10',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: 'default',
|
|
24
|
+
size: 'md',
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export type ButtonVariant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
30
|
+
export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyButtonGroupDirective } from './button-group.directive';
|
|
4
|
+
import type { ButtonGroupOrientation } from './button-group.variants';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [SnyButtonGroupDirective],
|
|
9
|
+
template: `<div snyButtonGroup [orientation]="orientation()"><button>A</button><button>B</button></div>`,
|
|
10
|
+
})
|
|
11
|
+
class TestHostComponent {
|
|
12
|
+
orientation = signal<ButtonGroupOrientation>('horizontal');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('SnyButtonGroupDirective', () => {
|
|
16
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
17
|
+
let el: HTMLElement;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await TestBed.configureTestingModule({
|
|
21
|
+
imports: [TestHostComponent],
|
|
22
|
+
}).compileComponents();
|
|
23
|
+
|
|
24
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
25
|
+
fixture.detectChanges();
|
|
26
|
+
el = fixture.nativeElement.querySelector('[snyButtonGroup]');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have role="group"', () => {
|
|
30
|
+
expect(el.getAttribute('role')).toBe('group');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should apply inline-flex', () => {
|
|
34
|
+
expect(el.className).toContain('inline-flex');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should apply horizontal orientation by default', () => {
|
|
38
|
+
expect(el.className).toContain('flex-row');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should apply vertical orientation', () => {
|
|
42
|
+
fixture.componentInstance.orientation.set('vertical');
|
|
43
|
+
fixture.detectChanges();
|
|
44
|
+
expect(el.className).toContain('flex-col');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import { buttonGroupVariants, type ButtonGroupOrientation } from './button-group.variants';
|
|
4
|
+
|
|
5
|
+
@Directive({
|
|
6
|
+
selector: '[snyButtonGroup]',
|
|
7
|
+
host: {
|
|
8
|
+
role: 'group',
|
|
9
|
+
'[class]': 'computedClass()',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
export class SnyButtonGroupDirective {
|
|
13
|
+
readonly orientation = input<ButtonGroupOrientation>('horizontal');
|
|
14
|
+
readonly class = input<string>('');
|
|
15
|
+
|
|
16
|
+
protected readonly computedClass = computed(() =>
|
|
17
|
+
cn(buttonGroupVariants({ orientation: this.orientation() }), this.class())
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const buttonGroupVariants = cva(
|
|
4
|
+
'inline-flex [&>*:not(:first-child):not(:last-child)]:rounded-none [&>*:first-child:not(:last-child)]:rounded-r-none [&>*:last-child:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:-ml-px',
|
|
5
|
+
{
|
|
6
|
+
variants: {
|
|
7
|
+
orientation: {
|
|
8
|
+
horizontal: 'flex-row',
|
|
9
|
+
vertical: 'flex-col [&>*:not(:first-child):not(:last-child)]:rounded-none [&>*:first-child:not(:last-child)]:rounded-b-none [&>*:first-child:not(:last-child)]:rounded-r-sm [&>*:last-child:not(:first-child)]:rounded-t-none [&>*:last-child:not(:first-child)]:rounded-l-sm [&>*:not(:first-child)]:-mt-px [&>*:not(:first-child)]:ml-0',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
orientation: 'horizontal',
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export type ButtonGroupOrientation = 'horizontal' | 'vertical';
|