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

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 +1 -1
  7. package/schematics/ng-generate/component/schema.json +1 -1
  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,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
+ });
@@ -0,0 +1,84 @@
1
+ import { Directive, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+
4
+ @Directive({
5
+ selector: 'nav[snyBreadcrumb]',
6
+ standalone: true,
7
+ host: {
8
+ '[class]': 'computedClass()',
9
+ 'aria-label': 'Breadcrumb',
10
+ },
11
+ })
12
+ export class SnyBreadcrumbDirective {
13
+ readonly class = input<string>('');
14
+ protected readonly computedClass = computed(() => cn('', this.class()));
15
+ }
16
+
17
+ @Directive({
18
+ selector: 'ol[snyBreadcrumbList]',
19
+ standalone: true,
20
+ host: { '[class]': 'computedClass()' },
21
+ })
22
+ export class SnyBreadcrumbListDirective {
23
+ readonly class = input<string>('');
24
+ protected readonly computedClass = computed(() =>
25
+ cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', this.class())
26
+ );
27
+ }
28
+
29
+ @Directive({
30
+ selector: 'li[snyBreadcrumbItem]',
31
+ standalone: true,
32
+ host: { '[class]': 'computedClass()' },
33
+ })
34
+ export class SnyBreadcrumbItemDirective {
35
+ readonly class = input<string>('');
36
+ protected readonly computedClass = computed(() =>
37
+ cn('inline-flex items-center gap-1.5', this.class())
38
+ );
39
+ }
40
+
41
+ @Directive({
42
+ selector: '[snyBreadcrumbLink]',
43
+ standalone: true,
44
+ host: { '[class]': 'computedClass()' },
45
+ })
46
+ export class SnyBreadcrumbLinkDirective {
47
+ readonly class = input<string>('');
48
+ protected readonly computedClass = computed(() =>
49
+ cn('transition-colors hover:text-foreground', this.class())
50
+ );
51
+ }
52
+
53
+ @Directive({
54
+ selector: '[snyBreadcrumbSeparator]',
55
+ standalone: true,
56
+ host: {
57
+ role: 'presentation',
58
+ '[aria-hidden]': 'true',
59
+ '[class]': 'computedClass()',
60
+ },
61
+ })
62
+ export class SnyBreadcrumbSeparatorDirective {
63
+ readonly class = input<string>('');
64
+ protected readonly computedClass = computed(() =>
65
+ cn('[&>svg]:size-3.5', this.class())
66
+ );
67
+ }
68
+
69
+ @Directive({
70
+ selector: '[snyBreadcrumbPage]',
71
+ standalone: true,
72
+ host: {
73
+ role: 'link',
74
+ 'aria-disabled': 'true',
75
+ '[attr.aria-current]': '"page"',
76
+ '[class]': 'computedClass()',
77
+ },
78
+ })
79
+ export class SnyBreadcrumbPageDirective {
80
+ readonly class = input<string>('');
81
+ protected readonly computedClass = computed(() =>
82
+ cn('font-normal text-foreground', this.class())
83
+ );
84
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ SnyBreadcrumbDirective,
3
+ SnyBreadcrumbListDirective,
4
+ SnyBreadcrumbItemDirective,
5
+ SnyBreadcrumbLinkDirective,
6
+ SnyBreadcrumbSeparatorDirective,
7
+ SnyBreadcrumbPageDirective,
8
+ } from './breadcrumb.directives';
@@ -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,29 @@
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
+ standalone: true,
8
+ host: {
9
+ '[class]': 'computedClass()',
10
+ '[attr.aria-disabled]': 'disabled() || loading() || null',
11
+ '[attr.disabled]': 'disabled() || loading() || null',
12
+ '[attr.tabindex]': '(disabled() || loading()) ? -1 : null',
13
+ },
14
+ })
15
+ export class SnyButtonDirective {
16
+ readonly variant = input<ButtonVariant>('default');
17
+ readonly size = input<ButtonSize>('md');
18
+ readonly disabled = input(false);
19
+ readonly loading = input(false);
20
+ readonly class = input<string>('');
21
+
22
+ protected readonly computedClass = computed(() =>
23
+ cn(
24
+ buttonVariants({ variant: this.variant(), size: this.size() }),
25
+ this.loading() && 'relative cursor-wait',
26
+ this.class()
27
+ )
28
+ );
29
+ }
@@ -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,2 @@
1
+ export { SnyButtonDirective } from './button.directive';
2
+ export { buttonVariants, type ButtonVariant, type ButtonSize } from './button.variants';
@@ -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,20 @@
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
+ standalone: true,
8
+ host: {
9
+ role: 'group',
10
+ '[class]': 'computedClass()',
11
+ },
12
+ })
13
+ export class SnyButtonGroupDirective {
14
+ readonly orientation = input<ButtonGroupOrientation>('horizontal');
15
+ readonly class = input<string>('');
16
+
17
+ protected readonly computedClass = computed(() =>
18
+ cn(buttonGroupVariants({ orientation: this.orientation() }), this.class())
19
+ );
20
+ }