@sonny-ui/core 0.1.0-alpha.1 → 0.1.0-alpha.11

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.
Files changed (198) hide show
  1. package/README.md +101 -32
  2. package/fesm2022/sonny-ui-core.mjs +3031 -42
  3. package/fesm2022/sonny-ui-core.mjs.map +1 -1
  4. package/package.json +8 -5
  5. package/schematics/ng-add/schema.json +1 -1
  6. package/schematics/ng-generate/component/index.js +182 -1
  7. package/schematics/ng-generate/component/schema.json +2 -2
  8. package/src/lib/accordion/accordion.directives.spec.ts +173 -0
  9. package/src/lib/accordion/accordion.directives.ts +147 -0
  10. package/src/lib/accordion/index.ts +8 -0
  11. package/src/lib/alert/alert.directives.spec.ts +154 -0
  12. package/src/lib/alert/alert.directives.ts +70 -0
  13. package/src/lib/alert/alert.variants.ts +25 -0
  14. package/src/lib/alert/index.ts +6 -0
  15. package/src/lib/avatar/avatar.component.spec.ts +75 -0
  16. package/src/lib/avatar/avatar.component.ts +44 -0
  17. package/src/lib/avatar/avatar.variants.ts +26 -0
  18. package/src/lib/avatar/index.ts +2 -0
  19. package/src/lib/badge/badge.directive.spec.ts +74 -0
  20. package/src/lib/badge/badge.directive.ts +18 -0
  21. package/src/lib/badge/badge.variants.ts +29 -0
  22. package/src/lib/badge/index.ts +2 -0
  23. package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
  24. package/src/lib/breadcrumb/breadcrumb.directives.ts +84 -0
  25. package/src/lib/breadcrumb/index.ts +8 -0
  26. package/src/lib/button/button.directive.spec.ts +92 -0
  27. package/src/lib/button/button.directive.ts +29 -0
  28. package/src/lib/button/button.variants.ts +30 -0
  29. package/src/lib/button/index.ts +2 -0
  30. package/src/lib/button-group/button-group.directive.spec.ts +46 -0
  31. package/src/lib/button-group/button-group.directive.ts +20 -0
  32. package/src/lib/button-group/button-group.variants.ts +18 -0
  33. package/src/lib/button-group/index.ts +2 -0
  34. package/src/lib/calendar/calendar.component.spec.ts +105 -0
  35. package/src/lib/calendar/calendar.component.ts +231 -0
  36. package/src/lib/calendar/index.ts +1 -0
  37. package/src/lib/card/card.directives.spec.ts +104 -0
  38. package/src/lib/card/card.directives.ts +78 -0
  39. package/src/lib/card/card.variants.ts +28 -0
  40. package/src/lib/card/index.ts +9 -0
  41. package/src/lib/carousel/carousel.directives.spec.ts +85 -0
  42. package/src/lib/carousel/carousel.directives.ts +164 -0
  43. package/src/lib/carousel/index.ts +8 -0
  44. package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
  45. package/src/lib/chat-bubble/chat-bubble.directives.ts +102 -0
  46. package/src/lib/chat-bubble/index.ts +11 -0
  47. package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
  48. package/src/lib/checkbox/checkbox.directive.ts +17 -0
  49. package/src/lib/checkbox/checkbox.variants.ts +19 -0
  50. package/src/lib/checkbox/index.ts +2 -0
  51. package/src/lib/combobox/combobox.component.spec.ts +151 -0
  52. package/src/lib/combobox/combobox.component.ts +279 -0
  53. package/src/lib/combobox/combobox.variants.ts +19 -0
  54. package/src/lib/combobox/index.ts +2 -0
  55. package/src/lib/diff/diff.component.spec.ts +47 -0
  56. package/src/lib/diff/diff.component.ts +83 -0
  57. package/src/lib/diff/index.ts +1 -0
  58. package/src/lib/divider/divider.component.spec.ts +48 -0
  59. package/src/lib/divider/divider.component.ts +52 -0
  60. package/src/lib/divider/divider.variants.ts +22 -0
  61. package/src/lib/divider/index.ts +2 -0
  62. package/src/lib/dock/dock.directives.spec.ts +85 -0
  63. package/src/lib/dock/dock.directives.ts +83 -0
  64. package/src/lib/dock/index.ts +1 -0
  65. package/src/lib/drawer/drawer.directives.spec.ts +62 -0
  66. package/src/lib/drawer/drawer.directives.ts +83 -0
  67. package/src/lib/drawer/index.ts +8 -0
  68. package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
  69. package/src/lib/dropdown/dropdown.directives.ts +143 -0
  70. package/src/lib/dropdown/dropdown.variants.ts +27 -0
  71. package/src/lib/dropdown/index.ts +15 -0
  72. package/src/lib/fab/fab.directives.spec.ts +60 -0
  73. package/src/lib/fab/fab.directives.ts +80 -0
  74. package/src/lib/fab/index.ts +8 -0
  75. package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
  76. package/src/lib/fieldset/fieldset.directives.ts +52 -0
  77. package/src/lib/fieldset/fieldset.variants.ts +15 -0
  78. package/src/lib/fieldset/index.ts +6 -0
  79. package/src/lib/file-input/file-input.component.spec.ts +114 -0
  80. package/src/lib/file-input/file-input.component.ts +168 -0
  81. package/src/lib/file-input/file-input.variants.ts +25 -0
  82. package/src/lib/file-input/index.ts +6 -0
  83. package/src/lib/indicator/index.ts +6 -0
  84. package/src/lib/indicator/indicator.directives.spec.ts +64 -0
  85. package/src/lib/indicator/indicator.directives.ts +61 -0
  86. package/src/lib/input/index.ts +3 -0
  87. package/src/lib/input/input.directive.spec.ts +103 -0
  88. package/src/lib/input/input.directive.ts +26 -0
  89. package/src/lib/input/input.variants.ts +42 -0
  90. package/src/lib/input/label.directive.ts +17 -0
  91. package/src/lib/kbd/index.ts +2 -0
  92. package/src/lib/kbd/kbd.directive.spec.ts +42 -0
  93. package/src/lib/kbd/kbd.directive.ts +19 -0
  94. package/src/lib/kbd/kbd.variants.ts +19 -0
  95. package/src/lib/link/index.ts +2 -0
  96. package/src/lib/link/link.directive.spec.ts +41 -0
  97. package/src/lib/link/link.directive.ts +19 -0
  98. package/src/lib/link/link.variants.ts +20 -0
  99. package/src/lib/list/index.ts +8 -0
  100. package/src/lib/list/list.directives.spec.ts +65 -0
  101. package/src/lib/list/list.directives.ts +86 -0
  102. package/src/lib/loader/index.ts +2 -0
  103. package/src/lib/loader/loader.component.spec.ts +58 -0
  104. package/src/lib/loader/loader.component.ts +48 -0
  105. package/src/lib/loader/loader.variants.ts +21 -0
  106. package/src/lib/modal/dialog-ref.ts +19 -0
  107. package/src/lib/modal/dialog.directives.ts +90 -0
  108. package/src/lib/modal/dialog.service.spec.ts +52 -0
  109. package/src/lib/modal/dialog.service.ts +61 -0
  110. package/src/lib/modal/dialog.types.ts +16 -0
  111. package/src/lib/modal/index.ts +11 -0
  112. package/src/lib/navbar/index.ts +7 -0
  113. package/src/lib/navbar/navbar.directives.spec.ts +59 -0
  114. package/src/lib/navbar/navbar.directives.ts +61 -0
  115. package/src/lib/pagination/index.ts +6 -0
  116. package/src/lib/pagination/pagination.component.spec.ts +59 -0
  117. package/src/lib/pagination/pagination.component.ts +144 -0
  118. package/src/lib/pagination/pagination.variants.ts +31 -0
  119. package/src/lib/progress/index.ts +7 -0
  120. package/src/lib/progress/progress.component.spec.ts +117 -0
  121. package/src/lib/progress/progress.component.ts +65 -0
  122. package/src/lib/progress/progress.variants.ts +43 -0
  123. package/src/lib/radial-progress/index.ts +5 -0
  124. package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
  125. package/src/lib/radial-progress/radial-progress.component.ts +71 -0
  126. package/src/lib/radio/index.ts +2 -0
  127. package/src/lib/radio/radio.directive.spec.ts +46 -0
  128. package/src/lib/radio/radio.directive.ts +17 -0
  129. package/src/lib/radio/radio.variants.ts +19 -0
  130. package/src/lib/rating/index.ts +2 -0
  131. package/src/lib/rating/rating.component.spec.ts +157 -0
  132. package/src/lib/rating/rating.component.ts +171 -0
  133. package/src/lib/rating/rating.variants.ts +20 -0
  134. package/src/lib/select/index.ts +2 -0
  135. package/src/lib/select/select.component.spec.ts +112 -0
  136. package/src/lib/select/select.component.ts +250 -0
  137. package/src/lib/select/select.variants.ts +19 -0
  138. package/src/lib/sheet/index.ts +10 -0
  139. package/src/lib/sheet/sheet-ref.ts +18 -0
  140. package/src/lib/sheet/sheet.component.spec.ts +67 -0
  141. package/src/lib/sheet/sheet.directives.ts +75 -0
  142. package/src/lib/sheet/sheet.service.ts +100 -0
  143. package/src/lib/sheet/sheet.types.ts +23 -0
  144. package/src/lib/skeleton/index.ts +2 -0
  145. package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
  146. package/src/lib/skeleton/skeleton.directive.ts +22 -0
  147. package/src/lib/skeleton/skeleton.variants.ts +27 -0
  148. package/src/lib/slider/index.ts +2 -0
  149. package/src/lib/slider/slider.component.spec.ts +104 -0
  150. package/src/lib/slider/slider.component.ts +188 -0
  151. package/src/lib/slider/slider.variants.ts +25 -0
  152. package/src/lib/stat/index.ts +8 -0
  153. package/src/lib/stat/stat.directives.spec.ts +60 -0
  154. package/src/lib/stat/stat.directives.ts +84 -0
  155. package/src/lib/status/index.ts +2 -0
  156. package/src/lib/status/status.directive.spec.ts +43 -0
  157. package/src/lib/status/status.directive.ts +38 -0
  158. package/src/lib/status/status.variants.ts +26 -0
  159. package/src/lib/steps/index.ts +8 -0
  160. package/src/lib/steps/steps.directives.spec.ts +52 -0
  161. package/src/lib/steps/steps.directives.ts +80 -0
  162. package/src/lib/switch/index.ts +2 -0
  163. package/src/lib/switch/switch.component.spec.ts +98 -0
  164. package/src/lib/switch/switch.component.ts +84 -0
  165. package/src/lib/switch/switch.variants.ts +31 -0
  166. package/src/lib/table/index.ts +12 -0
  167. package/src/lib/table/table.directives.spec.ts +111 -0
  168. package/src/lib/table/table.directives.ts +134 -0
  169. package/src/lib/table/table.variants.ts +36 -0
  170. package/src/lib/tabs/index.ts +8 -0
  171. package/src/lib/tabs/tabs.directives.spec.ts +136 -0
  172. package/src/lib/tabs/tabs.directives.ts +130 -0
  173. package/src/lib/tabs/tabs.variants.ts +17 -0
  174. package/src/lib/textarea/index.ts +7 -0
  175. package/src/lib/textarea/textarea.directive.spec.ts +84 -0
  176. package/src/lib/textarea/textarea.directive.ts +72 -0
  177. package/src/lib/textarea/textarea.variants.ts +34 -0
  178. package/src/lib/timeline/index.ts +11 -0
  179. package/src/lib/timeline/timeline.directives.spec.ts +55 -0
  180. package/src/lib/timeline/timeline.directives.ts +90 -0
  181. package/src/lib/toast/index.ts +3 -0
  182. package/src/lib/toast/toast.service.spec.ts +71 -0
  183. package/src/lib/toast/toast.service.ts +60 -0
  184. package/src/lib/toast/toast.variants.ts +38 -0
  185. package/src/lib/toast/toaster.component.spec.ts +38 -0
  186. package/src/lib/toast/toaster.component.ts +82 -0
  187. package/src/lib/toggle/index.ts +2 -0
  188. package/src/lib/toggle/toggle.directive.spec.ts +100 -0
  189. package/src/lib/toggle/toggle.directive.ts +73 -0
  190. package/src/lib/toggle/toggle.variants.ts +25 -0
  191. package/src/lib/tooltip/index.ts +2 -0
  192. package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
  193. package/src/lib/tooltip/tooltip.directive.ts +131 -0
  194. package/src/lib/tooltip/tooltip.variants.ts +20 -0
  195. package/src/lib/validator/index.ts +5 -0
  196. package/src/lib/validator/validator.directives.spec.ts +47 -0
  197. package/src/lib/validator/validator.directives.ts +52 -0
  198. package/types/sonny-ui-core.d.ts +878 -11
@@ -0,0 +1,154 @@
1
+ import { Component, signal, viewChild } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { SnyAlertDirective, SnyAlertTitleDirective, SnyAlertDescriptionDirective } from './alert.directives';
4
+ import type { AlertVariant } from './alert.variants';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [SnyAlertDirective, SnyAlertTitleDirective, SnyAlertDescriptionDirective],
9
+ template: `
10
+ <div snyAlert [variant]="variant()" [dismissible]="dismissible()">
11
+ <h5 snyAlertTitle>{{ title() }}</h5>
12
+ <p snyAlertDescription>{{ description() }}</p>
13
+ </div>
14
+ `,
15
+ })
16
+ class TestHostComponent {
17
+ variant = signal<AlertVariant>('default');
18
+ dismissible = signal(false);
19
+ title = signal('Test Title');
20
+ description = signal('Test Description');
21
+ alert = viewChild(SnyAlertDirective);
22
+ }
23
+
24
+ describe('SnyAlertDirective', () => {
25
+ let fixture: ComponentFixture<TestHostComponent>;
26
+ let el: HTMLElement;
27
+
28
+ beforeEach(async () => {
29
+ await TestBed.configureTestingModule({
30
+ imports: [TestHostComponent],
31
+ }).compileComponents();
32
+ fixture = TestBed.createComponent(TestHostComponent);
33
+ fixture.detectChanges();
34
+ el = fixture.nativeElement.querySelector('[snyAlert]');
35
+ });
36
+
37
+ it('should apply default variant classes', () => {
38
+ expect(el.className).toContain('bg-background');
39
+ expect(el.className).toContain('rounded-lg');
40
+ });
41
+
42
+ it('should apply destructive variant classes', () => {
43
+ fixture.componentInstance.variant.set('destructive');
44
+ fixture.detectChanges();
45
+ expect(el.className).toContain('text-destructive');
46
+ });
47
+
48
+ it('should apply success variant classes', () => {
49
+ fixture.componentInstance.variant.set('success');
50
+ fixture.detectChanges();
51
+ expect(el.className).toContain('text-green-700');
52
+ });
53
+
54
+ it('should apply warning variant classes', () => {
55
+ fixture.componentInstance.variant.set('warning');
56
+ fixture.detectChanges();
57
+ expect(el.className).toContain('text-yellow-700');
58
+ });
59
+
60
+ it('should apply info variant classes', () => {
61
+ fixture.componentInstance.variant.set('info');
62
+ fixture.detectChanges();
63
+ expect(el.className).toContain('text-blue-700');
64
+ });
65
+
66
+ it('should set role="status" for default and success', () => {
67
+ expect(el.getAttribute('role')).toBe('status');
68
+ fixture.componentInstance.variant.set('success');
69
+ fixture.detectChanges();
70
+ expect(el.getAttribute('role')).toBe('status');
71
+ });
72
+
73
+ it('should set role="alert" for destructive, warning, info', () => {
74
+ fixture.componentInstance.variant.set('destructive');
75
+ fixture.detectChanges();
76
+ expect(el.getAttribute('role')).toBe('alert');
77
+
78
+ fixture.componentInstance.variant.set('warning');
79
+ fixture.detectChanges();
80
+ expect(el.getAttribute('role')).toBe('alert');
81
+
82
+ fixture.componentInstance.variant.set('info');
83
+ fixture.detectChanges();
84
+ expect(el.getAttribute('role')).toBe('alert');
85
+ });
86
+
87
+ it('should set aria-live="assertive" for destructive/warning', () => {
88
+ fixture.componentInstance.variant.set('destructive');
89
+ fixture.detectChanges();
90
+ expect(el.getAttribute('aria-live')).toBe('assertive');
91
+
92
+ fixture.componentInstance.variant.set('warning');
93
+ fixture.detectChanges();
94
+ expect(el.getAttribute('aria-live')).toBe('assertive');
95
+ });
96
+
97
+ it('should set aria-live="polite" for default/success/info', () => {
98
+ expect(el.getAttribute('aria-live')).toBe('polite');
99
+
100
+ fixture.componentInstance.variant.set('success');
101
+ fixture.detectChanges();
102
+ expect(el.getAttribute('aria-live')).toBe('polite');
103
+
104
+ fixture.componentInstance.variant.set('info');
105
+ fixture.detectChanges();
106
+ expect(el.getAttribute('aria-live')).toBe('polite');
107
+ });
108
+
109
+ it('should be visible by default', () => {
110
+ expect(el.style.display).not.toBe('none');
111
+ });
112
+
113
+ it('should hide when dismiss() is called', () => {
114
+ const alert = fixture.componentInstance.alert();
115
+ alert!.dismiss();
116
+ fixture.detectChanges();
117
+ expect(el.style.display).toBe('none');
118
+ });
119
+ });
120
+
121
+ describe('SnyAlertTitleDirective', () => {
122
+ let fixture: ComponentFixture<TestHostComponent>;
123
+
124
+ beforeEach(async () => {
125
+ await TestBed.configureTestingModule({
126
+ imports: [TestHostComponent],
127
+ }).compileComponents();
128
+ fixture = TestBed.createComponent(TestHostComponent);
129
+ fixture.detectChanges();
130
+ });
131
+
132
+ it('should apply title classes', () => {
133
+ const title = fixture.nativeElement.querySelector('[snyAlertTitle]');
134
+ expect(title.className).toContain('font-medium');
135
+ expect(title.className).toContain('tracking-tight');
136
+ });
137
+ });
138
+
139
+ describe('SnyAlertDescriptionDirective', () => {
140
+ let fixture: ComponentFixture<TestHostComponent>;
141
+
142
+ beforeEach(async () => {
143
+ await TestBed.configureTestingModule({
144
+ imports: [TestHostComponent],
145
+ }).compileComponents();
146
+ fixture = TestBed.createComponent(TestHostComponent);
147
+ fixture.detectChanges();
148
+ });
149
+
150
+ it('should apply description classes', () => {
151
+ const desc = fixture.nativeElement.querySelector('[snyAlertDescription]');
152
+ expect(desc.className).toContain('text-sm');
153
+ });
154
+ });
@@ -0,0 +1,70 @@
1
+ import { Directive, computed, input, signal } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import { alertVariants, type AlertVariant } from './alert.variants';
4
+
5
+ @Directive({
6
+ selector: '[snyAlert]',
7
+ exportAs: 'snyAlert',
8
+ standalone: true,
9
+ host: {
10
+ '[class]': 'computedClass()',
11
+ '[attr.role]': 'role()',
12
+ '[attr.aria-live]': 'ariaLive()',
13
+ '[style.display]': 'visible() ? null : "none"',
14
+ },
15
+ })
16
+ export class SnyAlertDirective {
17
+ readonly variant = input<AlertVariant>('default');
18
+ readonly dismissible = input(false);
19
+ readonly class = input<string>('');
20
+
21
+ readonly visible = signal(true);
22
+
23
+ protected readonly role = computed(() => {
24
+ const v = this.variant();
25
+ return v === 'destructive' || v === 'warning' || v === 'info' ? 'alert' : 'status';
26
+ });
27
+
28
+ protected readonly ariaLive = computed(() => {
29
+ const v = this.variant();
30
+ return v === 'destructive' || v === 'warning' ? 'assertive' : 'polite';
31
+ });
32
+
33
+ protected readonly computedClass = computed(() =>
34
+ cn(alertVariants({ variant: this.variant() }), this.class())
35
+ );
36
+
37
+ dismiss(): void {
38
+ this.visible.set(false);
39
+ }
40
+ }
41
+
42
+ @Directive({
43
+ selector: '[snyAlertTitle]',
44
+ standalone: true,
45
+ host: {
46
+ '[class]': 'computedClass()',
47
+ },
48
+ })
49
+ export class SnyAlertTitleDirective {
50
+ readonly class = input<string>('');
51
+
52
+ protected readonly computedClass = computed(() =>
53
+ cn('mb-1 font-medium leading-none tracking-tight', this.class())
54
+ );
55
+ }
56
+
57
+ @Directive({
58
+ selector: '[snyAlertDescription]',
59
+ standalone: true,
60
+ host: {
61
+ '[class]': 'computedClass()',
62
+ },
63
+ })
64
+ export class SnyAlertDescriptionDirective {
65
+ readonly class = input<string>('');
66
+
67
+ protected readonly computedClass = computed(() =>
68
+ cn('text-sm [&_p]:leading-relaxed', this.class())
69
+ );
70
+ }
@@ -0,0 +1,25 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const alertVariants = cva(
4
+ 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
5
+ {
6
+ variants: {
7
+ variant: {
8
+ default:
9
+ 'bg-background text-foreground border-border',
10
+ destructive:
11
+ 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive bg-destructive/10',
12
+ success:
13
+ 'border-green-500/50 text-green-700 dark:text-green-400 dark:border-green-500 [&>svg]:text-green-600 bg-green-50 dark:bg-green-950/30',
14
+ warning:
15
+ 'border-yellow-500/50 text-yellow-700 dark:text-yellow-400 dark:border-yellow-500 [&>svg]:text-yellow-600 bg-yellow-50 dark:bg-yellow-950/30',
16
+ info: 'border-blue-500/50 text-blue-700 dark:text-blue-400 dark:border-blue-500 [&>svg]:text-blue-600 bg-blue-50 dark:bg-blue-950/30',
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: 'default',
21
+ },
22
+ }
23
+ );
24
+
25
+ export type AlertVariant = 'default' | 'destructive' | 'success' | 'warning' | 'info';
@@ -0,0 +1,6 @@
1
+ export {
2
+ SnyAlertDirective,
3
+ SnyAlertTitleDirective,
4
+ SnyAlertDescriptionDirective,
5
+ } from './alert.directives';
6
+ export { alertVariants, type AlertVariant } from './alert.variants';
@@ -0,0 +1,75 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, ComponentFixture } from '@angular/core/testing';
3
+ import { SnyAvatarComponent } from './avatar.component';
4
+ import type { AvatarSize, AvatarVariant } from './avatar.variants';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [SnyAvatarComponent],
9
+ template: `<sny-avatar [src]="src()" [alt]="alt()" [fallback]="fallback()" [size]="size()" [variant]="variant()" />`,
10
+ })
11
+ class TestHostComponent {
12
+ src = signal('');
13
+ alt = signal('John Doe');
14
+ fallback = signal('');
15
+ size = signal<AvatarSize>('md');
16
+ variant = signal<AvatarVariant>('circle');
17
+ }
18
+
19
+ describe('SnyAvatarComponent', () => {
20
+ let fixture: ComponentFixture<TestHostComponent>;
21
+ let el: HTMLElement;
22
+
23
+ beforeEach(async () => {
24
+ await TestBed.configureTestingModule({
25
+ imports: [TestHostComponent],
26
+ }).compileComponents();
27
+
28
+ fixture = TestBed.createComponent(TestHostComponent);
29
+ fixture.detectChanges();
30
+ el = fixture.nativeElement.querySelector('sny-avatar');
31
+ });
32
+
33
+ it('should show fallback text when no src', () => {
34
+ const span = el.querySelector('span');
35
+ expect(span?.textContent?.trim()).toBe('JD');
36
+ });
37
+
38
+ it('should show custom fallback', () => {
39
+ fixture.componentInstance.fallback.set('AB');
40
+ fixture.detectChanges();
41
+ const span = el.querySelector('span');
42
+ expect(span?.textContent?.trim()).toBe('AB');
43
+ });
44
+
45
+ it('should show image when src is provided', () => {
46
+ fixture.componentInstance.src.set('https://example.com/avatar.png');
47
+ fixture.detectChanges();
48
+ const img = el.querySelector('img');
49
+ expect(img).toBeTruthy();
50
+ expect(img?.getAttribute('src')).toBe('https://example.com/avatar.png');
51
+ });
52
+
53
+ it('should apply circle variant by default', () => {
54
+ expect(el.className).toContain('rounded-full');
55
+ });
56
+
57
+ it('should apply rounded variant', () => {
58
+ fixture.componentInstance.variant.set('rounded');
59
+ fixture.detectChanges();
60
+ expect(el.className).toContain('rounded-md');
61
+ });
62
+
63
+ it('should apply size classes', () => {
64
+ fixture.componentInstance.size.set('lg');
65
+ fixture.detectChanges();
66
+ expect(el.className).toContain('h-12');
67
+ expect(el.className).toContain('w-12');
68
+ });
69
+
70
+ it('should apply xl size', () => {
71
+ fixture.componentInstance.size.set('xl');
72
+ fixture.detectChanges();
73
+ expect(el.className).toContain('h-16');
74
+ });
75
+ });
@@ -0,0 +1,44 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import { avatarVariants, type AvatarSize, type AvatarVariant } from './avatar.variants';
4
+
5
+ @Component({
6
+ selector: 'sny-avatar',
7
+ standalone: true,
8
+ changeDetection: ChangeDetectionStrategy.OnPush,
9
+ host: { '[class]': 'computedClass()' },
10
+ template: `
11
+ @if (src() && !error()) {
12
+ <img
13
+ [src]="src()"
14
+ [alt]="alt()"
15
+ class="aspect-square h-full w-full object-cover"
16
+ (error)="error.set(true)"
17
+ />
18
+ } @else {
19
+ <span class="font-medium text-muted-foreground">{{ fallbackText() }}</span>
20
+ }
21
+ `,
22
+ })
23
+ export class SnyAvatarComponent {
24
+ readonly src = input<string>('');
25
+ readonly alt = input<string>('');
26
+ readonly fallback = input<string>('');
27
+ readonly size = input<AvatarSize>('md');
28
+ readonly variant = input<AvatarVariant>('circle');
29
+ readonly class = input<string>('');
30
+
31
+ readonly error = signal(false);
32
+
33
+ protected readonly fallbackText = computed(() => {
34
+ const fb = this.fallback();
35
+ if (fb) return fb;
36
+ const a = this.alt();
37
+ if (a) return a.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2);
38
+ return '?';
39
+ });
40
+
41
+ protected readonly computedClass = computed(() =>
42
+ cn(avatarVariants({ size: this.size(), variant: this.variant() }), this.class())
43
+ );
44
+ }
@@ -0,0 +1,26 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const avatarVariants = cva(
4
+ 'relative inline-flex shrink-0 items-center justify-center overflow-hidden bg-muted',
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: 'h-8 w-8 text-xs',
9
+ md: 'h-10 w-10 text-sm',
10
+ lg: 'h-12 w-12 text-base',
11
+ xl: 'h-16 w-16 text-lg',
12
+ },
13
+ variant: {
14
+ circle: 'rounded-full',
15
+ rounded: 'rounded-md',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ size: 'md',
20
+ variant: 'circle',
21
+ },
22
+ }
23
+ );
24
+
25
+ export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl';
26
+ export type AvatarVariant = 'circle' | 'rounded';
@@ -0,0 +1,2 @@
1
+ export { SnyAvatarComponent } from './avatar.component';
2
+ export { avatarVariants, type AvatarSize, type AvatarVariant } from './avatar.variants';
@@ -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,18 @@
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
+ standalone: true,
8
+ host: { '[class]': 'computedClass()' },
9
+ })
10
+ export class SnyBadgeDirective {
11
+ readonly variant = input<BadgeVariant>('default');
12
+ readonly size = input<BadgeSize>('md');
13
+ readonly class = input<string>('');
14
+
15
+ protected readonly computedClass = computed(() =>
16
+ cn(badgeVariants({ variant: this.variant(), size: this.size() }), this.class())
17
+ );
18
+ }
@@ -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,2 @@
1
+ export { SnyBadgeDirective } from './badge.directive';
2
+ export { badgeVariants, type BadgeVariant, type BadgeSize } from './badge.variants';
@@ -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
+ });