@sonny-ui/core 0.1.0-alpha.2 → 0.1.0-alpha.20

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 (242) hide show
  1. package/README.md +187 -40
  2. package/fesm2022/sonny-ui-core.mjs +6642 -268
  3. package/fesm2022/sonny-ui-core.mjs.map +1 -1
  4. package/package.json +8 -5
  5. package/schematics/ng-add/index.js +27 -0
  6. package/schematics/ng-add/schema.json +1 -1
  7. package/schematics/ng-generate/component/index.js +182 -1
  8. package/schematics/ng-generate/component/schema.json +2 -2
  9. package/src/lib/accordion/accordion.directives.spec.ts +173 -0
  10. package/src/lib/accordion/accordion.directives.ts +143 -0
  11. package/src/lib/accordion/index.ts +8 -0
  12. package/src/lib/alert/alert.directives.spec.ts +154 -0
  13. package/src/lib/alert/alert.directives.ts +67 -0
  14. package/src/lib/alert/alert.variants.ts +25 -0
  15. package/src/lib/alert/index.ts +6 -0
  16. package/src/lib/avatar/avatar.component.spec.ts +75 -0
  17. package/src/lib/avatar/avatar.component.ts +43 -0
  18. package/src/lib/avatar/avatar.variants.ts +26 -0
  19. package/src/lib/avatar/index.ts +2 -0
  20. package/src/lib/avatar-group/avatar-group.component.spec.ts +74 -0
  21. package/src/lib/avatar-group/avatar-group.component.ts +88 -0
  22. package/src/lib/avatar-group/index.ts +1 -0
  23. package/src/lib/badge/badge.directive.spec.ts +74 -0
  24. package/src/lib/badge/badge.directive.ts +17 -0
  25. package/src/lib/badge/badge.variants.ts +29 -0
  26. package/src/lib/badge/index.ts +2 -0
  27. package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
  28. package/src/lib/breadcrumb/breadcrumb.directives.ts +78 -0
  29. package/src/lib/breadcrumb/index.ts +8 -0
  30. package/src/lib/button/button.directive.spec.ts +92 -0
  31. package/src/lib/button/button.directive.ts +28 -0
  32. package/src/lib/button/button.variants.ts +30 -0
  33. package/src/lib/button/index.ts +2 -0
  34. package/src/lib/button-group/button-group.directive.spec.ts +46 -0
  35. package/src/lib/button-group/button-group.directive.ts +19 -0
  36. package/src/lib/button-group/button-group.variants.ts +18 -0
  37. package/src/lib/button-group/index.ts +2 -0
  38. package/src/lib/calendar/calendar.component.spec.ts +192 -0
  39. package/src/lib/calendar/calendar.component.ts +342 -0
  40. package/src/lib/calendar/calendar.types.ts +24 -0
  41. package/src/lib/calendar/index.ts +7 -0
  42. package/src/lib/card/card.directives.spec.ts +104 -0
  43. package/src/lib/card/card.directives.ts +72 -0
  44. package/src/lib/card/card.variants.ts +28 -0
  45. package/src/lib/card/index.ts +9 -0
  46. package/src/lib/carousel/carousel.directives.spec.ts +85 -0
  47. package/src/lib/carousel/carousel.directives.ts +159 -0
  48. package/src/lib/carousel/index.ts +8 -0
  49. package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
  50. package/src/lib/chat-bubble/chat-bubble.directives.ts +96 -0
  51. package/src/lib/chat-bubble/index.ts +11 -0
  52. package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
  53. package/src/lib/checkbox/checkbox.directive.ts +16 -0
  54. package/src/lib/checkbox/checkbox.variants.ts +19 -0
  55. package/src/lib/checkbox/index.ts +2 -0
  56. package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
  57. package/src/lib/color-picker/color-picker.component.ts +537 -0
  58. package/src/lib/color-picker/color-picker.types.ts +24 -0
  59. package/src/lib/color-picker/color-picker.utils.ts +183 -0
  60. package/src/lib/color-picker/color-picker.variants.ts +17 -0
  61. package/src/lib/color-picker/index.ts +20 -0
  62. package/src/lib/combobox/combobox.component.spec.ts +151 -0
  63. package/src/lib/combobox/combobox.component.ts +264 -0
  64. package/src/lib/combobox/combobox.variants.ts +19 -0
  65. package/src/lib/combobox/index.ts +2 -0
  66. package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
  67. package/src/lib/command-palette/command-palette.component.ts +194 -0
  68. package/src/lib/command-palette/command-palette.service.ts +36 -0
  69. package/src/lib/command-palette/command-palette.types.ts +23 -0
  70. package/src/lib/command-palette/index.ts +7 -0
  71. package/src/lib/data-table/data-table.component.spec.ts +443 -0
  72. package/src/lib/data-table/data-table.component.ts +602 -0
  73. package/src/lib/data-table/data-table.directives.ts +31 -0
  74. package/src/lib/data-table/data-table.types.ts +20 -0
  75. package/src/lib/data-table/index.ts +13 -0
  76. package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
  77. package/src/lib/date-picker/date-picker.component.ts +220 -0
  78. package/src/lib/date-picker/date-picker.variants.ts +17 -0
  79. package/src/lib/date-picker/index.ts +2 -0
  80. package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
  81. package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
  82. package/src/lib/date-range-picker/index.ts +1 -0
  83. package/src/lib/diff/diff.component.spec.ts +47 -0
  84. package/src/lib/diff/diff.component.ts +82 -0
  85. package/src/lib/diff/index.ts +1 -0
  86. package/src/lib/divider/divider.component.spec.ts +48 -0
  87. package/src/lib/divider/divider.component.ts +51 -0
  88. package/src/lib/divider/divider.variants.ts +22 -0
  89. package/src/lib/divider/index.ts +2 -0
  90. package/src/lib/dock/dock.directives.spec.ts +85 -0
  91. package/src/lib/dock/dock.directives.ts +81 -0
  92. package/src/lib/dock/index.ts +1 -0
  93. package/src/lib/drawer/drawer.directives.spec.ts +62 -0
  94. package/src/lib/drawer/drawer.directives.ts +80 -0
  95. package/src/lib/drawer/index.ts +8 -0
  96. package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
  97. package/src/lib/dropdown/dropdown.directives.ts +136 -0
  98. package/src/lib/dropdown/dropdown.variants.ts +27 -0
  99. package/src/lib/dropdown/index.ts +15 -0
  100. package/src/lib/fab/fab.directives.spec.ts +60 -0
  101. package/src/lib/fab/fab.directives.ts +77 -0
  102. package/src/lib/fab/index.ts +8 -0
  103. package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
  104. package/src/lib/fieldset/fieldset.directives.ts +49 -0
  105. package/src/lib/fieldset/fieldset.variants.ts +15 -0
  106. package/src/lib/fieldset/index.ts +6 -0
  107. package/src/lib/file-input/file-input.component.spec.ts +114 -0
  108. package/src/lib/file-input/file-input.component.ts +155 -0
  109. package/src/lib/file-input/file-input.variants.ts +25 -0
  110. package/src/lib/file-input/index.ts +6 -0
  111. package/src/lib/indicator/index.ts +6 -0
  112. package/src/lib/indicator/indicator.directives.spec.ts +64 -0
  113. package/src/lib/indicator/indicator.directives.ts +59 -0
  114. package/src/lib/input/index.ts +3 -0
  115. package/src/lib/input/input.directive.spec.ts +103 -0
  116. package/src/lib/input/input.directive.ts +25 -0
  117. package/src/lib/input/input.variants.ts +42 -0
  118. package/src/lib/input/label.directive.ts +16 -0
  119. package/src/lib/kbd/index.ts +2 -0
  120. package/src/lib/kbd/kbd.directive.spec.ts +42 -0
  121. package/src/lib/kbd/kbd.directive.ts +18 -0
  122. package/src/lib/kbd/kbd.variants.ts +19 -0
  123. package/src/lib/link/index.ts +2 -0
  124. package/src/lib/link/link.directive.spec.ts +41 -0
  125. package/src/lib/link/link.directive.ts +18 -0
  126. package/src/lib/link/link.variants.ts +20 -0
  127. package/src/lib/list/index.ts +8 -0
  128. package/src/lib/list/list.directives.spec.ts +65 -0
  129. package/src/lib/list/list.directives.ts +81 -0
  130. package/src/lib/loader/index.ts +2 -0
  131. package/src/lib/loader/loader.component.spec.ts +58 -0
  132. package/src/lib/loader/loader.component.ts +47 -0
  133. package/src/lib/loader/loader.variants.ts +21 -0
  134. package/src/lib/modal/dialog-ref.ts +19 -0
  135. package/src/lib/modal/dialog.directives.ts +84 -0
  136. package/src/lib/modal/dialog.service.spec.ts +52 -0
  137. package/src/lib/modal/dialog.service.ts +61 -0
  138. package/src/lib/modal/dialog.types.ts +16 -0
  139. package/src/lib/modal/index.ts +11 -0
  140. package/src/lib/navbar/index.ts +7 -0
  141. package/src/lib/navbar/navbar.directives.spec.ts +59 -0
  142. package/src/lib/navbar/navbar.directives.ts +57 -0
  143. package/src/lib/number-input/index.ts +2 -0
  144. package/src/lib/number-input/number-input.component.spec.ts +151 -0
  145. package/src/lib/number-input/number-input.component.ts +152 -0
  146. package/src/lib/number-input/number-input.variants.ts +17 -0
  147. package/src/lib/otp-input/index.ts +2 -0
  148. package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
  149. package/src/lib/otp-input/otp-input.component.ts +274 -0
  150. package/src/lib/otp-input/otp-input.variants.ts +18 -0
  151. package/src/lib/pagination/index.ts +6 -0
  152. package/src/lib/pagination/pagination.component.spec.ts +59 -0
  153. package/src/lib/pagination/pagination.component.ts +143 -0
  154. package/src/lib/pagination/pagination.variants.ts +31 -0
  155. package/src/lib/popover/index.ts +6 -0
  156. package/src/lib/popover/popover.directives.spec.ts +147 -0
  157. package/src/lib/popover/popover.directives.ts +151 -0
  158. package/src/lib/progress/index.ts +7 -0
  159. package/src/lib/progress/progress.component.spec.ts +117 -0
  160. package/src/lib/progress/progress.component.ts +64 -0
  161. package/src/lib/progress/progress.variants.ts +43 -0
  162. package/src/lib/radial-progress/index.ts +5 -0
  163. package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
  164. package/src/lib/radial-progress/radial-progress.component.ts +70 -0
  165. package/src/lib/radio/index.ts +2 -0
  166. package/src/lib/radio/radio.directive.spec.ts +46 -0
  167. package/src/lib/radio/radio.directive.ts +16 -0
  168. package/src/lib/radio/radio.variants.ts +19 -0
  169. package/src/lib/rating/index.ts +2 -0
  170. package/src/lib/rating/rating.component.spec.ts +157 -0
  171. package/src/lib/rating/rating.component.ts +163 -0
  172. package/src/lib/rating/rating.variants.ts +20 -0
  173. package/src/lib/select/index.ts +2 -0
  174. package/src/lib/select/select.component.spec.ts +112 -0
  175. package/src/lib/select/select.component.ts +235 -0
  176. package/src/lib/select/select.variants.ts +19 -0
  177. package/src/lib/sheet/index.ts +10 -0
  178. package/src/lib/sheet/sheet-ref.ts +18 -0
  179. package/src/lib/sheet/sheet.component.spec.ts +67 -0
  180. package/src/lib/sheet/sheet.directives.ts +70 -0
  181. package/src/lib/sheet/sheet.service.ts +100 -0
  182. package/src/lib/sheet/sheet.types.ts +23 -0
  183. package/src/lib/skeleton/index.ts +2 -0
  184. package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
  185. package/src/lib/skeleton/skeleton.directive.ts +21 -0
  186. package/src/lib/skeleton/skeleton.variants.ts +27 -0
  187. package/src/lib/slider/index.ts +2 -0
  188. package/src/lib/slider/slider.component.spec.ts +104 -0
  189. package/src/lib/slider/slider.component.ts +181 -0
  190. package/src/lib/slider/slider.variants.ts +25 -0
  191. package/src/lib/stat/index.ts +8 -0
  192. package/src/lib/stat/stat.directives.spec.ts +60 -0
  193. package/src/lib/stat/stat.directives.ts +79 -0
  194. package/src/lib/status/index.ts +2 -0
  195. package/src/lib/status/status.directive.spec.ts +43 -0
  196. package/src/lib/status/status.directive.ts +37 -0
  197. package/src/lib/status/status.variants.ts +26 -0
  198. package/src/lib/steps/index.ts +8 -0
  199. package/src/lib/steps/steps.directives.spec.ts +52 -0
  200. package/src/lib/steps/steps.directives.ts +78 -0
  201. package/src/lib/switch/index.ts +2 -0
  202. package/src/lib/switch/switch.component.spec.ts +98 -0
  203. package/src/lib/switch/switch.component.ts +76 -0
  204. package/src/lib/switch/switch.variants.ts +31 -0
  205. package/src/lib/table/index.ts +12 -0
  206. package/src/lib/table/table.directives.spec.ts +111 -0
  207. package/src/lib/table/table.directives.ts +126 -0
  208. package/src/lib/table/table.variants.ts +36 -0
  209. package/src/lib/tabs/index.ts +8 -0
  210. package/src/lib/tabs/tabs.directives.spec.ts +136 -0
  211. package/src/lib/tabs/tabs.directives.ts +126 -0
  212. package/src/lib/tabs/tabs.variants.ts +17 -0
  213. package/src/lib/tag-input/index.ts +2 -0
  214. package/src/lib/tag-input/tag-input.component.spec.ts +190 -0
  215. package/src/lib/tag-input/tag-input.component.ts +172 -0
  216. package/src/lib/tag-input/tag-input.variants.ts +31 -0
  217. package/src/lib/textarea/index.ts +7 -0
  218. package/src/lib/textarea/textarea.directive.spec.ts +84 -0
  219. package/src/lib/textarea/textarea.directive.ts +71 -0
  220. package/src/lib/textarea/textarea.variants.ts +34 -0
  221. package/src/lib/timeline/index.ts +11 -0
  222. package/src/lib/timeline/timeline.directives.spec.ts +55 -0
  223. package/src/lib/timeline/timeline.directives.ts +85 -0
  224. package/src/lib/toast/index.ts +3 -0
  225. package/src/lib/toast/toast.service.spec.ts +71 -0
  226. package/src/lib/toast/toast.service.ts +60 -0
  227. package/src/lib/toast/toast.variants.ts +38 -0
  228. package/src/lib/toast/toaster.component.spec.ts +38 -0
  229. package/src/lib/toast/toaster.component.ts +81 -0
  230. package/src/lib/toggle/index.ts +2 -0
  231. package/src/lib/toggle/toggle.directive.spec.ts +100 -0
  232. package/src/lib/toggle/toggle.directive.ts +61 -0
  233. package/src/lib/toggle/toggle.variants.ts +25 -0
  234. package/src/lib/tooltip/index.ts +2 -0
  235. package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
  236. package/src/lib/tooltip/tooltip.directive.ts +130 -0
  237. package/src/lib/tooltip/tooltip.variants.ts +20 -0
  238. package/src/lib/validator/index.ts +5 -0
  239. package/src/lib/validator/validator.directives.spec.ts +47 -0
  240. package/src/lib/validator/validator.directives.ts +50 -0
  241. package/src/styles/sonny-theme.css +33 -0
  242. package/types/sonny-ui-core.d.ts +1443 -13
@@ -0,0 +1,72 @@
1
+ import { Directive, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import { cardVariants, type CardVariant, type CardPadding } from './card.variants';
4
+
5
+ @Directive({
6
+ selector: '[snyCard]',
7
+ host: { '[class]': 'computedClass()' },
8
+ })
9
+ export class SnyCardDirective {
10
+ readonly variant = input<CardVariant>('default');
11
+ readonly padding = input<CardPadding>('none');
12
+ readonly class = input<string>('');
13
+
14
+ protected readonly computedClass = computed(() =>
15
+ cn(cardVariants({ variant: this.variant(), padding: this.padding() }), this.class())
16
+ );
17
+ }
18
+
19
+ @Directive({
20
+ selector: '[snyCardHeader]',
21
+ host: { '[class]': 'computedClass()' },
22
+ })
23
+ export class SnyCardHeaderDirective {
24
+ readonly class = input<string>('');
25
+ protected readonly computedClass = computed(() =>
26
+ cn('flex flex-col space-y-1.5 p-6', this.class())
27
+ );
28
+ }
29
+
30
+ @Directive({
31
+ selector: '[snyCardTitle]',
32
+ host: { '[class]': 'computedClass()' },
33
+ })
34
+ export class SnyCardTitleDirective {
35
+ readonly class = input<string>('');
36
+ protected readonly computedClass = computed(() =>
37
+ cn('text-2xl font-semibold leading-none tracking-tight', this.class())
38
+ );
39
+ }
40
+
41
+ @Directive({
42
+ selector: '[snyCardDescription]',
43
+ host: { '[class]': 'computedClass()' },
44
+ })
45
+ export class SnyCardDescriptionDirective {
46
+ readonly class = input<string>('');
47
+ protected readonly computedClass = computed(() =>
48
+ cn('text-sm text-muted-foreground', this.class())
49
+ );
50
+ }
51
+
52
+ @Directive({
53
+ selector: '[snyCardContent]',
54
+ host: { '[class]': 'computedClass()' },
55
+ })
56
+ export class SnyCardContentDirective {
57
+ readonly class = input<string>('');
58
+ protected readonly computedClass = computed(() =>
59
+ cn('p-6 pt-0', this.class())
60
+ );
61
+ }
62
+
63
+ @Directive({
64
+ selector: '[snyCardFooter]',
65
+ host: { '[class]': 'computedClass()' },
66
+ })
67
+ export class SnyCardFooterDirective {
68
+ readonly class = input<string>('');
69
+ protected readonly computedClass = computed(() =>
70
+ cn('flex items-center p-6 pt-0', this.class())
71
+ );
72
+ }
@@ -0,0 +1,28 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const cardVariants = cva(
4
+ 'rounded-sm text-card-foreground',
5
+ {
6
+ variants: {
7
+ variant: {
8
+ default: 'bg-card border border-border',
9
+ outline: 'border-2 border-border bg-transparent',
10
+ elevated: 'bg-card shadow-lg',
11
+ ghost: 'bg-transparent',
12
+ },
13
+ padding: {
14
+ none: '',
15
+ sm: 'p-4',
16
+ md: 'p-6',
17
+ lg: 'p-8',
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: 'default',
22
+ padding: 'none',
23
+ },
24
+ }
25
+ );
26
+
27
+ export type CardVariant = 'default' | 'outline' | 'elevated' | 'ghost';
28
+ export type CardPadding = 'none' | 'sm' | 'md' | 'lg';
@@ -0,0 +1,9 @@
1
+ export {
2
+ SnyCardDirective,
3
+ SnyCardHeaderDirective,
4
+ SnyCardTitleDirective,
5
+ SnyCardDescriptionDirective,
6
+ SnyCardContentDirective,
7
+ SnyCardFooterDirective,
8
+ } from './card.directives';
9
+ export { cardVariants, type CardVariant, type CardPadding } from './card.variants';
@@ -0,0 +1,85 @@
1
+ import { Component, viewChild } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { SnyCarouselDirective, SnyCarouselContentDirective, SnyCarouselItemDirective } from './carousel.directives';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ imports: [SnyCarouselDirective, SnyCarouselContentDirective, SnyCarouselItemDirective],
8
+ template: `
9
+ <div snyCarousel [loop]="true">
10
+ <div snyCarouselContent>
11
+ <div snyCarouselItem>Slide 1</div>
12
+ <div snyCarouselItem>Slide 2</div>
13
+ <div snyCarouselItem>Slide 3</div>
14
+ </div>
15
+ </div>
16
+ `,
17
+ })
18
+ class TestHostComponent {
19
+ carousel = viewChild(SnyCarouselDirective);
20
+ }
21
+
22
+ describe('SnyCarouselDirective', () => {
23
+ let fixture: ComponentFixture<TestHostComponent>;
24
+
25
+ beforeEach(async () => {
26
+ await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
27
+ fixture = TestBed.createComponent(TestHostComponent);
28
+ fixture.detectChanges();
29
+ await fixture.whenStable();
30
+ fixture.detectChanges();
31
+ });
32
+
33
+ it('should render with region role', () => {
34
+ const el = fixture.nativeElement.querySelector('[snyCarousel]');
35
+ expect(el.getAttribute('role')).toBe('region');
36
+ expect(el.getAttribute('aria-roledescription')).toBe('carousel');
37
+ });
38
+
39
+ it('should render slides with group role', () => {
40
+ const items = fixture.nativeElement.querySelectorAll('[snyCarouselItem]');
41
+ expect(items.length).toBe(3);
42
+ expect(items[0].getAttribute('role')).toBe('group');
43
+ });
44
+
45
+ it('should detect items via contentChildren', () => {
46
+ const c = fixture.componentInstance.carousel()!;
47
+ expect(c.totalItems()).toBe(3);
48
+ });
49
+
50
+ it('should navigate to next', () => {
51
+ const c = fixture.componentInstance.carousel()!;
52
+ expect(c.currentIndex()).toBe(0);
53
+ c.next();
54
+ expect(c.currentIndex()).toBe(1);
55
+ });
56
+
57
+ it('should loop around', () => {
58
+ const c = fixture.componentInstance.carousel()!;
59
+ c.goTo(2);
60
+ c.next();
61
+ expect(c.currentIndex()).toBe(0);
62
+ });
63
+
64
+ it('should navigate prev with loop', () => {
65
+ const c = fixture.componentInstance.carousel()!;
66
+ c.prev();
67
+ expect(c.currentIndex()).toBe(2);
68
+ });
69
+
70
+ it('should navigate next on ArrowRight keydown', () => {
71
+ const c = fixture.componentInstance.carousel()!;
72
+ const host = fixture.nativeElement.querySelector('[snyCarousel]');
73
+ expect(c.currentIndex()).toBe(0);
74
+ host.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
75
+ expect(c.currentIndex()).toBe(1);
76
+ });
77
+
78
+ it('should navigate prev on ArrowLeft keydown', () => {
79
+ const c = fixture.componentInstance.carousel()!;
80
+ const host = fixture.nativeElement.querySelector('[snyCarousel]');
81
+ c.goTo(1);
82
+ host.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }));
83
+ expect(c.currentIndex()).toBe(0);
84
+ });
85
+ });
@@ -0,0 +1,159 @@
1
+ import {
2
+ Directive, InjectionToken, computed, contentChildren, inject, input, signal, effect, OnDestroy,
3
+ } from '@angular/core';
4
+ import { cn } from '../core/utils/cn';
5
+
6
+ export const SNY_CAROUSEL = new InjectionToken<SnyCarouselDirective>('SnyCarousel');
7
+
8
+ @Directive({
9
+ selector: '[snyCarouselItem]',
10
+ host: {
11
+ 'role': 'group',
12
+ '[attr.aria-roledescription]': '"slide"',
13
+ '[class]': 'computedClass()',
14
+ },
15
+ })
16
+ export class SnyCarouselItemDirective {
17
+ readonly class = input<string>('');
18
+ protected readonly computedClass = computed(() =>
19
+ cn('min-w-0 shrink-0 grow-0 basis-full pl-4', this.class())
20
+ );
21
+ }
22
+
23
+ @Directive({
24
+ selector: '[snyCarouselContent]',
25
+ host: {
26
+ '[class]': 'computedClass()',
27
+ '[style.transform]': 'transformStyle()',
28
+ },
29
+ })
30
+ export class SnyCarouselContentDirective {
31
+ private readonly carousel = inject(SNY_CAROUSEL);
32
+ readonly class = input<string>('');
33
+
34
+ protected readonly computedClass = computed(() =>
35
+ cn('flex -ml-4 transition-transform duration-300 ease-in-out', this.class())
36
+ );
37
+
38
+ protected readonly transformStyle = computed(() =>
39
+ `translateX(-${this.carousel.currentIndex() * 100}%)`
40
+ );
41
+ }
42
+
43
+ @Directive({
44
+ selector: '[snyCarousel]',
45
+ exportAs: 'snyCarousel',
46
+ providers: [{ provide: SNY_CAROUSEL, useExisting: SnyCarouselDirective }],
47
+ host: {
48
+ 'role': 'region',
49
+ '[attr.aria-roledescription]': '"carousel"',
50
+ 'aria-label': 'Carousel',
51
+ 'tabindex': '0',
52
+ '[class]': 'computedClass()',
53
+ '(keydown)': 'onKeydown($event)',
54
+ },
55
+ })
56
+ export class SnyCarouselDirective implements OnDestroy {
57
+ readonly orientation = input<'horizontal' | 'vertical'>('horizontal');
58
+ readonly loop = input(false);
59
+ readonly autoplay = input(0);
60
+ readonly class = input<string>('');
61
+
62
+ readonly items = contentChildren(SnyCarouselItemDirective, { descendants: true });
63
+ readonly currentIndex = signal(0);
64
+ readonly totalItems = computed(() => this.items().length);
65
+
66
+ private autoplayInterval: ReturnType<typeof setInterval> | null = null;
67
+
68
+ constructor() {
69
+ effect(() => {
70
+ const ms = this.autoplay();
71
+ this.clearAutoplay();
72
+ if (ms > 0) {
73
+ this.autoplayInterval = setInterval(() => this.next(), ms);
74
+ }
75
+ });
76
+ }
77
+
78
+ next(): void {
79
+ const total = this.totalItems();
80
+ if (total === 0) return;
81
+ this.currentIndex.update((i) => {
82
+ if (i >= total - 1) return this.loop() ? 0 : i;
83
+ return i + 1;
84
+ });
85
+ }
86
+
87
+ prev(): void {
88
+ const total = this.totalItems();
89
+ if (total === 0) return;
90
+ this.currentIndex.update((i) => {
91
+ if (i <= 0) return this.loop() ? total - 1 : i;
92
+ return i - 1;
93
+ });
94
+ }
95
+
96
+ goTo(index: number): void {
97
+ this.currentIndex.set(Math.max(0, Math.min(index, this.totalItems() - 1)));
98
+ }
99
+
100
+ onKeydown(event: KeyboardEvent): void {
101
+ switch (event.key) {
102
+ case 'ArrowLeft':
103
+ event.preventDefault();
104
+ this.prev();
105
+ break;
106
+ case 'ArrowRight':
107
+ event.preventDefault();
108
+ this.next();
109
+ break;
110
+ }
111
+ }
112
+
113
+ ngOnDestroy(): void {
114
+ this.clearAutoplay();
115
+ }
116
+
117
+ private clearAutoplay(): void {
118
+ if (this.autoplayInterval) {
119
+ clearInterval(this.autoplayInterval);
120
+ this.autoplayInterval = null;
121
+ }
122
+ }
123
+
124
+ protected readonly computedClass = computed(() =>
125
+ cn('relative overflow-hidden', this.class())
126
+ );
127
+ }
128
+
129
+ @Directive({
130
+ selector: '[snyCarouselPrev]',
131
+ host: {
132
+ '(click)': 'carousel.prev()',
133
+ '[attr.aria-label]': '"Previous slide"',
134
+ '[class]': 'computedClass()',
135
+ },
136
+ })
137
+ export class SnyCarouselPrevDirective {
138
+ readonly carousel = inject(SNY_CAROUSEL);
139
+ readonly class = input<string>('');
140
+ protected readonly computedClass = computed(() =>
141
+ cn('absolute left-2 top-1/2 -translate-y-1/2 z-10', this.class())
142
+ );
143
+ }
144
+
145
+ @Directive({
146
+ selector: '[snyCarouselNext]',
147
+ host: {
148
+ '(click)': 'carousel.next()',
149
+ '[attr.aria-label]': '"Next slide"',
150
+ '[class]': 'computedClass()',
151
+ },
152
+ })
153
+ export class SnyCarouselNextDirective {
154
+ readonly carousel = inject(SNY_CAROUSEL);
155
+ readonly class = input<string>('');
156
+ protected readonly computedClass = computed(() =>
157
+ cn('absolute right-2 top-1/2 -translate-y-1/2 z-10', this.class())
158
+ );
159
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ SnyCarouselDirective,
3
+ SnyCarouselContentDirective,
4
+ SnyCarouselItemDirective,
5
+ SnyCarouselPrevDirective,
6
+ SnyCarouselNextDirective,
7
+ SNY_CAROUSEL,
8
+ } from './carousel.directives';
@@ -0,0 +1,52 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { SnyChatBubbleDirective, SnyChatBubbleContentDirective, type ChatBubbleAlign, type ChatBubbleContentVariant } from './chat-bubble.directives';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ imports: [SnyChatBubbleDirective, SnyChatBubbleContentDirective],
8
+ template: `
9
+ <div snyChatBubble [align]="align()">
10
+ <div snyChatBubbleContent [variant]="variant()">Hello!</div>
11
+ </div>
12
+ `,
13
+ })
14
+ class TestHostComponent {
15
+ align = signal<ChatBubbleAlign>('start');
16
+ variant = signal<ChatBubbleContentVariant>('default');
17
+ }
18
+
19
+ describe('SnyChatBubbleDirective', () => {
20
+ let fixture: ComponentFixture<TestHostComponent>;
21
+
22
+ beforeEach(async () => {
23
+ await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
24
+ fixture = TestBed.createComponent(TestHostComponent);
25
+ fixture.detectChanges();
26
+ });
27
+
28
+ it('should render with article role', () => {
29
+ const bubble = fixture.nativeElement.querySelector('[snyChatBubble]');
30
+ expect(bubble.getAttribute('role')).toBe('article');
31
+ });
32
+
33
+ it('should apply start alignment by default', () => {
34
+ const bubble = fixture.nativeElement.querySelector('[snyChatBubble]');
35
+ expect(bubble.className).toContain('flex');
36
+ expect(bubble.className).not.toContain('flex-row-reverse');
37
+ });
38
+
39
+ it('should apply end alignment', () => {
40
+ fixture.componentInstance.align.set('end');
41
+ fixture.detectChanges();
42
+ const bubble = fixture.nativeElement.querySelector('[snyChatBubble]');
43
+ expect(bubble.className).toContain('flex-row-reverse');
44
+ });
45
+
46
+ it('should apply primary content variant', () => {
47
+ fixture.componentInstance.variant.set('primary');
48
+ fixture.detectChanges();
49
+ const content = fixture.nativeElement.querySelector('[snyChatBubbleContent]');
50
+ expect(content.className).toContain('bg-primary');
51
+ });
52
+ });
@@ -0,0 +1,96 @@
1
+ import { Directive, InjectionToken, computed, inject, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+
4
+ export type ChatBubbleAlign = 'start' | 'end';
5
+ export type ChatBubbleContentVariant = 'default' | 'primary' | 'secondary' | 'accent';
6
+
7
+ export const SNY_CHAT_BUBBLE = new InjectionToken<SnyChatBubbleDirective>('SnyChatBubble');
8
+
9
+ @Directive({
10
+ selector: '[snyChatBubble]',
11
+ providers: [{ provide: SNY_CHAT_BUBBLE, useExisting: SnyChatBubbleDirective }],
12
+ host: {
13
+ 'role': 'article',
14
+ '[class]': 'computedClass()',
15
+ },
16
+ })
17
+ export class SnyChatBubbleDirective {
18
+ readonly align = input<ChatBubbleAlign>('start');
19
+ readonly class = input<string>('');
20
+
21
+ protected readonly computedClass = computed(() =>
22
+ cn(
23
+ 'flex gap-3 mb-4',
24
+ this.align() === 'end' && 'flex-row-reverse',
25
+ this.class()
26
+ )
27
+ );
28
+ }
29
+
30
+ @Directive({
31
+ selector: '[snyChatBubbleAvatar]',
32
+ host: { '[class]': 'computedClass()' },
33
+ })
34
+ export class SnyChatBubbleAvatarDirective {
35
+ readonly class = input<string>('');
36
+ protected readonly computedClass = computed(() =>
37
+ cn('flex-shrink-0 w-10 h-10 rounded-full overflow-hidden', this.class())
38
+ );
39
+ }
40
+
41
+ @Directive({
42
+ selector: '[snyChatBubbleHeader]',
43
+ host: { '[class]': 'computedClass()' },
44
+ })
45
+ export class SnyChatBubbleHeaderDirective {
46
+ readonly class = input<string>('');
47
+ protected readonly computedClass = computed(() =>
48
+ cn('text-xs text-muted-foreground mb-1', this.class())
49
+ );
50
+ }
51
+
52
+ @Directive({
53
+ selector: '[snyChatBubbleContent]',
54
+ host: { '[class]': 'computedClass()' },
55
+ })
56
+ export class SnyChatBubbleContentDirective {
57
+ readonly variant = input<ChatBubbleContentVariant>('default');
58
+ readonly class = input<string>('');
59
+
60
+ protected readonly computedClass = computed(() => {
61
+ const v = this.variant();
62
+ const variantClass =
63
+ v === 'primary' ? 'bg-primary text-primary-foreground' :
64
+ v === 'secondary' ? 'bg-blue-100 text-blue-900 dark:bg-blue-900 dark:text-blue-100' :
65
+ v === 'accent' ? 'bg-violet-100 text-violet-900 dark:bg-violet-900 dark:text-violet-100' :
66
+ 'bg-muted';
67
+ return cn('rounded-lg px-3 py-2 text-sm max-w-[80%]', variantClass, this.class());
68
+ });
69
+ }
70
+
71
+ @Directive({
72
+ selector: '[snyChatBubbleFooter]',
73
+ host: { '[class]': 'computedClass()' },
74
+ })
75
+ export class SnyChatBubbleFooterDirective {
76
+ readonly class = input<string>('');
77
+ protected readonly computedClass = computed(() =>
78
+ cn('text-xs text-muted-foreground mt-1', this.class())
79
+ );
80
+ }
81
+
82
+ @Directive({
83
+ selector: '[snyChatBubbleBody]',
84
+ host: { '[class]': 'computedClass()' },
85
+ })
86
+ export class SnyChatBubbleBodyDirective {
87
+ private readonly chatBubble = inject(SNY_CHAT_BUBBLE);
88
+ readonly class = input<string>('');
89
+ protected readonly computedClass = computed(() =>
90
+ cn(
91
+ 'flex flex-col',
92
+ this.chatBubble.align() === 'end' && 'items-end',
93
+ this.class()
94
+ )
95
+ );
96
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ SnyChatBubbleDirective,
3
+ SnyChatBubbleAvatarDirective,
4
+ SnyChatBubbleHeaderDirective,
5
+ SnyChatBubbleContentDirective,
6
+ SnyChatBubbleFooterDirective,
7
+ SnyChatBubbleBodyDirective,
8
+ SNY_CHAT_BUBBLE,
9
+ type ChatBubbleAlign,
10
+ type ChatBubbleContentVariant,
11
+ } from './chat-bubble.directives';
@@ -0,0 +1,57 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, ComponentFixture } from '@angular/core/testing';
3
+ import { SnyCheckboxDirective } from './checkbox.directive';
4
+ import type { CheckboxSize } from './checkbox.variants';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [SnyCheckboxDirective],
9
+ template: `<input type="checkbox" snyCheckbox [size]="size()" />`,
10
+ })
11
+ class TestHostComponent {
12
+ size = signal<CheckboxSize>('md');
13
+ }
14
+
15
+ describe('SnyCheckboxDirective', () => {
16
+ let fixture: ComponentFixture<TestHostComponent>;
17
+ let el: HTMLInputElement;
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('input[type="checkbox"]');
27
+ });
28
+
29
+ it('should apply default classes', () => {
30
+ expect(el.className).toContain('appearance-none');
31
+ expect(el.className).toContain('rounded-sm');
32
+ expect(el.className).toContain('border');
33
+ });
34
+
35
+ it('should apply default md size', () => {
36
+ expect(el.className).toContain('h-4');
37
+ expect(el.className).toContain('w-4');
38
+ });
39
+
40
+ it('should apply sm size', () => {
41
+ fixture.componentInstance.size.set('sm');
42
+ fixture.detectChanges();
43
+ expect(el.className).toContain('h-3.5');
44
+ expect(el.className).toContain('w-3.5');
45
+ });
46
+
47
+ it('should apply lg size', () => {
48
+ fixture.componentInstance.size.set('lg');
49
+ fixture.detectChanges();
50
+ expect(el.className).toContain('h-5');
51
+ expect(el.className).toContain('w-5');
52
+ });
53
+
54
+ it('should have checked styles in class list', () => {
55
+ expect(el.className).toContain('checked:bg-primary');
56
+ });
57
+ });
@@ -0,0 +1,16 @@
1
+ import { Directive, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import { checkboxVariants, type CheckboxSize } from './checkbox.variants';
4
+
5
+ @Directive({
6
+ selector: 'input[type="checkbox"][snyCheckbox]',
7
+ host: { '[class]': 'computedClass()' },
8
+ })
9
+ export class SnyCheckboxDirective {
10
+ readonly size = input<CheckboxSize>('md');
11
+ readonly class = input<string>('');
12
+
13
+ protected readonly computedClass = computed(() =>
14
+ cn(checkboxVariants({ size: this.size() }), this.class())
15
+ );
16
+ }
@@ -0,0 +1,19 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const checkboxVariants = cva(
4
+ 'peer appearance-none shrink-0 rounded-sm border border-border bg-background transition-colors checked:bg-primary checked:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: 'h-3.5 w-3.5',
9
+ md: 'h-4 w-4',
10
+ lg: 'h-5 w-5',
11
+ },
12
+ },
13
+ defaultVariants: {
14
+ size: 'md',
15
+ },
16
+ }
17
+ );
18
+
19
+ export type CheckboxSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,2 @@
1
+ export { SnyCheckboxDirective } from './checkbox.directive';
2
+ export { checkboxVariants, type CheckboxSize } from './checkbox.variants';