@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,117 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { SnyProgressComponent } from './progress.component';
4
+ import type { ProgressVariant, ProgressSize } from './progress.variants';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [SnyProgressComponent],
9
+ template: `
10
+ <sny-progress
11
+ [value]="value()"
12
+ [max]="max()"
13
+ [variant]="variant()"
14
+ [size]="size()"
15
+ [indeterminate]="indeterminate()"
16
+ [label]="label()"
17
+ />
18
+ `,
19
+ })
20
+ class TestHostComponent {
21
+ value = signal(50);
22
+ max = signal(100);
23
+ variant = signal<ProgressVariant>('default');
24
+ size = signal<ProgressSize>('md');
25
+ indeterminate = signal(false);
26
+ label = signal('Loading');
27
+ }
28
+
29
+ describe('SnyProgressComponent', () => {
30
+ let fixture: ComponentFixture<TestHostComponent>;
31
+ let host: HTMLElement;
32
+
33
+ beforeEach(async () => {
34
+ await TestBed.configureTestingModule({
35
+ imports: [TestHostComponent],
36
+ }).compileComponents();
37
+ fixture = TestBed.createComponent(TestHostComponent);
38
+ fixture.detectChanges();
39
+ host = fixture.nativeElement.querySelector('sny-progress');
40
+ });
41
+
42
+ it('should render with progressbar role', () => {
43
+ expect(host.getAttribute('role')).toBe('progressbar');
44
+ });
45
+
46
+ it('should set aria-valuenow to current value', () => {
47
+ expect(host.getAttribute('aria-valuenow')).toBe('50');
48
+ });
49
+
50
+ it('should set aria-valuemin to 0', () => {
51
+ expect(host.getAttribute('aria-valuemin')).toBe('0');
52
+ });
53
+
54
+ it('should set aria-valuemax to max value', () => {
55
+ expect(host.getAttribute('aria-valuemax')).toBe('100');
56
+ });
57
+
58
+ it('should set aria-label', () => {
59
+ expect(host.getAttribute('aria-label')).toBe('Loading');
60
+ });
61
+
62
+ it('should calculate percentage correctly', () => {
63
+ const bar = host.querySelector('div > div') as HTMLElement;
64
+ expect(bar.style.width).toBe('50%');
65
+ });
66
+
67
+ it('should update percentage when value changes', () => {
68
+ fixture.componentInstance.value.set(75);
69
+ fixture.detectChanges();
70
+ const bar = host.querySelector('div > div') as HTMLElement;
71
+ expect(bar.style.width).toBe('75%');
72
+ expect(host.getAttribute('aria-valuenow')).toBe('75');
73
+ });
74
+
75
+ it('should cap percentage at 100%', () => {
76
+ fixture.componentInstance.value.set(150);
77
+ fixture.detectChanges();
78
+ const bar = host.querySelector('div > div') as HTMLElement;
79
+ expect(bar.style.width).toBe('100%');
80
+ });
81
+
82
+ it('should omit aria-valuenow when indeterminate', () => {
83
+ fixture.componentInstance.indeterminate.set(true);
84
+ fixture.detectChanges();
85
+ expect(host.getAttribute('aria-valuenow')).toBeNull();
86
+ });
87
+
88
+ it('should apply indeterminate animation class', () => {
89
+ fixture.componentInstance.indeterminate.set(true);
90
+ fixture.detectChanges();
91
+ const bar = host.querySelector('div > div') as HTMLElement;
92
+ expect(bar.className).toContain('animate-progress-indeterminate');
93
+ });
94
+
95
+ it('should apply size variant', () => {
96
+ fixture.componentInstance.size.set('lg');
97
+ fixture.detectChanges();
98
+ const track = host.querySelector('div') as HTMLElement;
99
+ expect(track.className).toContain('h-4');
100
+ });
101
+
102
+ it('should apply success variant', () => {
103
+ fixture.componentInstance.variant.set('success');
104
+ fixture.detectChanges();
105
+ const bar = host.querySelector('div > div') as HTMLElement;
106
+ expect(bar.className).toContain('bg-green-600');
107
+ });
108
+
109
+ it('should work with custom max value', () => {
110
+ fixture.componentInstance.max.set(200);
111
+ fixture.componentInstance.value.set(100);
112
+ fixture.detectChanges();
113
+ const bar = host.querySelector('div > div') as HTMLElement;
114
+ expect(bar.style.width).toBe('50%');
115
+ expect(host.getAttribute('aria-valuemax')).toBe('200');
116
+ });
117
+ });
@@ -0,0 +1,65 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import {
4
+ progressTrackVariants,
5
+ progressBarVariants,
6
+ type ProgressVariant,
7
+ type ProgressSize,
8
+ } from './progress.variants';
9
+
10
+ @Component({
11
+ selector: 'sny-progress',
12
+ standalone: true,
13
+ changeDetection: ChangeDetectionStrategy.OnPush,
14
+ host: {
15
+ 'role': 'progressbar',
16
+ '[attr.aria-valuenow]': 'indeterminate() ? null : value()',
17
+ '[attr.aria-valuemin]': '0',
18
+ '[attr.aria-valuemax]': 'max()',
19
+ '[attr.aria-label]': 'label()',
20
+ '[class]': '"w-full"',
21
+ },
22
+ template: `
23
+ <div [class]="trackClass()">
24
+ <div
25
+ [class]="barClass()"
26
+ [style.width.%]="indeterminate() ? null : percentage()"
27
+ ></div>
28
+ </div>
29
+ `,
30
+ styles: `
31
+ @keyframes progress-indeterminate {
32
+ 0% { transform: translateX(-100%); }
33
+ 100% { transform: translateX(400%); }
34
+ }
35
+ :host .animate-progress-indeterminate {
36
+ animation: progress-indeterminate 1.5s ease-in-out infinite;
37
+ }
38
+ `,
39
+ })
40
+ export class SnyProgressComponent {
41
+ readonly value = input(0);
42
+ readonly max = input(100);
43
+ readonly variant = input<ProgressVariant>('default');
44
+ readonly size = input<ProgressSize>('md');
45
+ readonly indeterminate = input(false);
46
+ readonly label = input('Progress');
47
+ readonly class = input<string>('');
48
+
49
+ readonly percentage = computed(() =>
50
+ Math.min(100, (this.value() / this.max()) * 100)
51
+ );
52
+
53
+ protected readonly trackClass = computed(() =>
54
+ cn(progressTrackVariants({ size: this.size() }), this.class())
55
+ );
56
+
57
+ protected readonly barClass = computed(() =>
58
+ cn(
59
+ progressBarVariants({
60
+ variant: this.variant(),
61
+ indeterminate: this.indeterminate(),
62
+ })
63
+ )
64
+ );
65
+ }
@@ -0,0 +1,43 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const progressTrackVariants = cva(
4
+ 'relative w-full overflow-hidden rounded-full bg-secondary',
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: 'h-1.5',
9
+ md: 'h-2.5',
10
+ lg: 'h-4',
11
+ },
12
+ },
13
+ defaultVariants: {
14
+ size: 'md',
15
+ },
16
+ }
17
+ );
18
+
19
+ export const progressBarVariants = cva(
20
+ 'h-full rounded-full transition-all duration-300 ease-in-out',
21
+ {
22
+ variants: {
23
+ variant: {
24
+ default: 'bg-primary',
25
+ success: 'bg-green-600 dark:bg-green-500',
26
+ warning: 'bg-yellow-500 dark:bg-yellow-400',
27
+ error: 'bg-destructive',
28
+ info: 'bg-blue-600 dark:bg-blue-500',
29
+ },
30
+ indeterminate: {
31
+ true: 'animate-progress-indeterminate w-1/3',
32
+ false: '',
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ variant: 'default',
37
+ indeterminate: false,
38
+ },
39
+ }
40
+ );
41
+
42
+ export type ProgressVariant = 'default' | 'success' | 'warning' | 'error' | 'info';
43
+ export type ProgressSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,5 @@
1
+ export {
2
+ SnyRadialProgressComponent,
3
+ type RadialProgressSize,
4
+ type RadialProgressVariant,
5
+ } from './radial-progress.component';
@@ -0,0 +1,41 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { SnyRadialProgressComponent } from './radial-progress.component';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ imports: [SnyRadialProgressComponent],
8
+ template: `<sny-radial-progress [value]="value()">{{ value() }}%</sny-radial-progress>`,
9
+ })
10
+ class TestHostComponent {
11
+ value = signal(75);
12
+ }
13
+
14
+ describe('SnyRadialProgressComponent', () => {
15
+ let fixture: ComponentFixture<TestHostComponent>;
16
+ let host: HTMLElement;
17
+
18
+ beforeEach(async () => {
19
+ await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
20
+ fixture = TestBed.createComponent(TestHostComponent);
21
+ fixture.detectChanges();
22
+ host = fixture.nativeElement.querySelector('sny-radial-progress');
23
+ });
24
+
25
+ it('should render with progressbar role', () => {
26
+ expect(host.getAttribute('role')).toBe('progressbar');
27
+ });
28
+
29
+ it('should set aria-valuenow', () => {
30
+ expect(host.getAttribute('aria-valuenow')).toBe('75');
31
+ });
32
+
33
+ it('should render SVG circles', () => {
34
+ const circles = host.querySelectorAll('circle');
35
+ expect(circles.length).toBe(2);
36
+ });
37
+
38
+ it('should project content', () => {
39
+ expect(host.textContent).toContain('75%');
40
+ });
41
+ });
@@ -0,0 +1,71 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+
4
+ export type RadialProgressSize = 'sm' | 'md' | 'lg';
5
+ export type RadialProgressVariant = 'default' | 'success' | 'warning' | 'error' | 'info';
6
+
7
+ const sizeMap: Record<RadialProgressSize, number> = { sm: 48, md: 72, lg: 96 };
8
+
9
+ const variantColorMap: Record<RadialProgressVariant, string> = {
10
+ default: 'stroke-primary',
11
+ success: 'stroke-green-600 dark:stroke-green-500',
12
+ warning: 'stroke-yellow-500',
13
+ error: 'stroke-destructive',
14
+ info: 'stroke-blue-600 dark:stroke-blue-500',
15
+ };
16
+
17
+ @Component({
18
+ selector: 'sny-radial-progress',
19
+ standalone: true,
20
+ changeDetection: ChangeDetectionStrategy.OnPush,
21
+ host: {
22
+ 'role': 'progressbar',
23
+ '[attr.aria-valuenow]': 'value()',
24
+ '[attr.aria-valuemin]': '0',
25
+ '[attr.aria-valuemax]': '100',
26
+ '[class]': '"inline-flex items-center justify-center"',
27
+ },
28
+ template: `
29
+ <div class="relative" [style.width.px]="svgSize()" [style.height.px]="svgSize()">
30
+ <svg [attr.width]="svgSize()" [attr.height]="svgSize()" class="-rotate-90">
31
+ <circle
32
+ [attr.cx]="svgSize() / 2"
33
+ [attr.cy]="svgSize() / 2"
34
+ [attr.r]="radius()"
35
+ fill="none"
36
+ class="stroke-muted"
37
+ [attr.stroke-width]="thickness()"
38
+ />
39
+ <circle
40
+ [attr.cx]="svgSize() / 2"
41
+ [attr.cy]="svgSize() / 2"
42
+ [attr.r]="radius()"
43
+ fill="none"
44
+ [class]="strokeClass()"
45
+ [attr.stroke-width]="thickness()"
46
+ [attr.stroke-dasharray]="circumference()"
47
+ [attr.stroke-dashoffset]="offset()"
48
+ stroke-linecap="round"
49
+ class="transition-all duration-300"
50
+ />
51
+ </svg>
52
+ <div class="absolute inset-0 flex items-center justify-center">
53
+ <ng-content />
54
+ </div>
55
+ </div>
56
+ `,
57
+ })
58
+ export class SnyRadialProgressComponent {
59
+ readonly value = input(0);
60
+ readonly size = input<RadialProgressSize>('md');
61
+ readonly thickness = input(4);
62
+ readonly variant = input<RadialProgressVariant>('default');
63
+ readonly class = input<string>('');
64
+
65
+ readonly svgSize = computed(() => sizeMap[this.size()]);
66
+ readonly radius = computed(() => (this.svgSize() - this.thickness()) / 2);
67
+ readonly circumference = computed(() => 2 * Math.PI * this.radius());
68
+ readonly offset = computed(() => this.circumference() - (this.value() / 100) * this.circumference());
69
+
70
+ readonly strokeClass = computed(() => variantColorMap[this.variant()]);
71
+ }
@@ -0,0 +1,2 @@
1
+ export { SnyRadioDirective } from './radio.directive';
2
+ export { radioVariants, type RadioSize } from './radio.variants';
@@ -0,0 +1,46 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, ComponentFixture } from '@angular/core/testing';
3
+ import { SnyRadioDirective } from './radio.directive';
4
+ import type { RadioSize } from './radio.variants';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [SnyRadioDirective],
9
+ template: `<input type="radio" snyRadio [size]="size()" />`,
10
+ })
11
+ class TestHostComponent {
12
+ size = signal<RadioSize>('md');
13
+ }
14
+
15
+ describe('SnyRadioDirective', () => {
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');
27
+ });
28
+
29
+ it('should apply default classes', () => {
30
+ expect(el.className).toContain('appearance-none');
31
+ expect(el.className).toContain('rounded-full');
32
+ expect(el.className).toContain('h-4');
33
+ });
34
+
35
+ it('should apply sm size', () => {
36
+ fixture.componentInstance.size.set('sm');
37
+ fixture.detectChanges();
38
+ expect(el.className).toContain('h-3.5');
39
+ });
40
+
41
+ it('should apply lg size', () => {
42
+ fixture.componentInstance.size.set('lg');
43
+ fixture.detectChanges();
44
+ expect(el.className).toContain('h-5');
45
+ });
46
+ });
@@ -0,0 +1,17 @@
1
+ import { Directive, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import { radioVariants, type RadioSize } from './radio.variants';
4
+
5
+ @Directive({
6
+ selector: 'input[type="radio"][snyRadio]',
7
+ standalone: true,
8
+ host: { '[class]': 'computedClass()' },
9
+ })
10
+ export class SnyRadioDirective {
11
+ readonly size = input<RadioSize>('md');
12
+ readonly class = input<string>('');
13
+
14
+ protected readonly computedClass = computed(() =>
15
+ cn(radioVariants({ size: this.size() }), this.class())
16
+ );
17
+ }
@@ -0,0 +1,19 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const radioVariants = cva(
4
+ 'appearance-none rounded-full border border-border bg-background transition-colors checked:border-primary checked:shadow-[inset_0_0_0_3px] checked:shadow-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 RadioSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,2 @@
1
+ export { SnyRatingComponent } from './rating.component';
2
+ export { ratingVariants, type RatingSize, type RatingVariant } from './rating.variants';
@@ -0,0 +1,157 @@
1
+ import { Component, signal, viewChild } from '@angular/core';
2
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
3
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
4
+ import { SnyRatingComponent } from './rating.component';
5
+ import type { RatingSize } from './rating.variants';
6
+
7
+ @Component({
8
+ standalone: true,
9
+ imports: [SnyRatingComponent],
10
+ template: `
11
+ <sny-rating
12
+ [(value)]="value"
13
+ [max]="max()"
14
+ [readonly]="readonly()"
15
+ [size]="size()"
16
+ [half]="half()"
17
+ />
18
+ `,
19
+ })
20
+ class TestHostComponent {
21
+ value = signal(0);
22
+ max = signal(5);
23
+ readonly = signal(false);
24
+ size = signal<RatingSize>('md');
25
+ half = signal(false);
26
+ rating = viewChild(SnyRatingComponent);
27
+ }
28
+
29
+ describe('SnyRatingComponent', () => {
30
+ let fixture: ComponentFixture<TestHostComponent>;
31
+ let host: HTMLElement;
32
+
33
+ beforeEach(async () => {
34
+ await TestBed.configureTestingModule({
35
+ imports: [TestHostComponent],
36
+ }).compileComponents();
37
+ fixture = TestBed.createComponent(TestHostComponent);
38
+ fixture.detectChanges();
39
+ host = fixture.nativeElement.querySelector('sny-rating');
40
+ });
41
+
42
+ it('should render with slider role', () => {
43
+ expect(host.getAttribute('role')).toBe('slider');
44
+ });
45
+
46
+ it('should render correct number of stars', () => {
47
+ const stars = host.querySelectorAll('svg');
48
+ expect(stars.length).toBe(5);
49
+ });
50
+
51
+ it('should set aria-valuenow', () => {
52
+ expect(host.getAttribute('aria-valuenow')).toBe('0');
53
+ fixture.componentInstance.value.set(3);
54
+ fixture.detectChanges();
55
+ expect(host.getAttribute('aria-valuenow')).toBe('3');
56
+ });
57
+
58
+ it('should set value on star click', () => {
59
+ const stars = host.querySelectorAll('svg');
60
+ stars[2].dispatchEvent(new MouseEvent('click'));
61
+ fixture.detectChanges();
62
+ expect(fixture.componentInstance.value()).toBe(3);
63
+ });
64
+
65
+ it('should not change value when readonly', () => {
66
+ fixture.componentInstance.readonly.set(true);
67
+ fixture.detectChanges();
68
+ const stars = host.querySelectorAll('svg');
69
+ stars[2].dispatchEvent(new MouseEvent('click'));
70
+ fixture.detectChanges();
71
+ expect(fixture.componentInstance.value()).toBe(0);
72
+ });
73
+
74
+ it('should handle keyboard ArrowRight', () => {
75
+ host.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
76
+ fixture.detectChanges();
77
+ expect(fixture.componentInstance.value()).toBe(1);
78
+ });
79
+
80
+ it('should handle keyboard ArrowLeft', () => {
81
+ fixture.componentInstance.value.set(3);
82
+ fixture.detectChanges();
83
+ host.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }));
84
+ fixture.detectChanges();
85
+ expect(fixture.componentInstance.value()).toBe(2);
86
+ });
87
+
88
+ it('should handle Home and End keys', () => {
89
+ fixture.componentInstance.value.set(3);
90
+ fixture.detectChanges();
91
+ host.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' }));
92
+ fixture.detectChanges();
93
+ expect(fixture.componentInstance.value()).toBe(0);
94
+
95
+ host.dispatchEvent(new KeyboardEvent('keydown', { key: 'End' }));
96
+ fixture.detectChanges();
97
+ expect(fixture.componentInstance.value()).toBe(5);
98
+ });
99
+
100
+ it('should render with custom max', () => {
101
+ fixture.componentInstance.max.set(10);
102
+ fixture.detectChanges();
103
+ const stars = host.querySelectorAll('svg');
104
+ expect(stars.length).toBe(10);
105
+ });
106
+ });
107
+
108
+ @Component({
109
+ standalone: true,
110
+ imports: [ReactiveFormsModule, SnyRatingComponent],
111
+ template: `<sny-rating [formControl]="ctrl" />`,
112
+ })
113
+ class ReactiveFormHost {
114
+ ctrl = new FormControl(0);
115
+ }
116
+
117
+ describe('SnyRatingComponent — Reactive Forms', () => {
118
+ let fixture: ComponentFixture<ReactiveFormHost>;
119
+ let host: HTMLElement;
120
+
121
+ beforeEach(async () => {
122
+ await TestBed.configureTestingModule({
123
+ imports: [ReactiveFormHost],
124
+ }).compileComponents();
125
+ fixture = TestBed.createComponent(ReactiveFormHost);
126
+ fixture.detectChanges();
127
+ host = fixture.nativeElement.querySelector('sny-rating');
128
+ });
129
+
130
+ it('should update view when FormControl value changes (writeValue)', () => {
131
+ fixture.componentInstance.ctrl.setValue(4);
132
+ fixture.detectChanges();
133
+ expect(host.getAttribute('aria-valuenow')).toBe('4');
134
+ });
135
+
136
+ it('should update FormControl when user interacts (onChange)', () => {
137
+ const stars = host.querySelectorAll('svg');
138
+ stars[2].dispatchEvent(new MouseEvent('click'));
139
+ fixture.detectChanges();
140
+ expect(fixture.componentInstance.ctrl.value).toBe(3);
141
+ });
142
+
143
+ it('should disable via FormControl.disable() (setDisabledState)', () => {
144
+ fixture.componentInstance.ctrl.disable();
145
+ fixture.detectChanges();
146
+ const stars = host.querySelectorAll('svg');
147
+ stars[2].dispatchEvent(new MouseEvent('click'));
148
+ fixture.detectChanges();
149
+ expect(fixture.componentInstance.ctrl.value).toBe(0);
150
+ });
151
+
152
+ it('should mark as touched on blur (onTouched)', () => {
153
+ expect(fixture.componentInstance.ctrl.touched).toBe(false);
154
+ host.dispatchEvent(new Event('blur'));
155
+ expect(fixture.componentInstance.ctrl.touched).toBe(true);
156
+ });
157
+ });