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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +187 -40
  2. package/fesm2022/sonny-ui-core.mjs +6646 -272
  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 +45 -0
  242. package/types/sonny-ui-core.d.ts +1443 -13
@@ -0,0 +1,328 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
3
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
4
+ import { SnyColorPickerComponent } from './color-picker.component';
5
+ import { hexToRgb, rgbToHex, rgbToHsl, hslToRgb, rgbToHsv, hsvToRgb, parseColor, formatColor, isValidColor } from './color-picker.utils';
6
+ import type { ColorPickerPreset } from './color-picker.types';
7
+
8
+ // --- Utils Tests ---
9
+ describe('Color Picker Utils', () => {
10
+ it('hexToRgb should parse 6-digit hex', () => {
11
+ expect(hexToRgb('#ff0000')).toEqual({ r: 255, g: 0, b: 0 });
12
+ expect(hexToRgb('#00ff00')).toEqual({ r: 0, g: 255, b: 0 });
13
+ });
14
+
15
+ it('hexToRgb should parse 3-digit hex', () => {
16
+ expect(hexToRgb('#f00')).toEqual({ r: 255, g: 0, b: 0 });
17
+ });
18
+
19
+ it('hexToRgb should return null for invalid', () => {
20
+ expect(hexToRgb('#xyz')).toBeNull();
21
+ expect(hexToRgb('')).toBeNull();
22
+ });
23
+
24
+ it('rgbToHex should convert rgb to hex', () => {
25
+ expect(rgbToHex({ r: 255, g: 0, b: 0 })).toBe('#ff0000');
26
+ expect(rgbToHex({ r: 0, g: 0, b: 0 })).toBe('#000000');
27
+ expect(rgbToHex({ r: 255, g: 255, b: 255 })).toBe('#ffffff');
28
+ });
29
+
30
+ it('rgbToHex should clamp out-of-range values', () => {
31
+ expect(rgbToHex({ r: 300, g: -10, b: 128 })).toBe('#ff0080');
32
+ });
33
+
34
+ it('rgbToHsl and hslToRgb should round-trip', () => {
35
+ const rgb = { r: 59, g: 130, b: 246 };
36
+ const hsl = rgbToHsl(rgb);
37
+ const back = hslToRgb(hsl);
38
+ expect(Math.abs(back.r - rgb.r)).toBeLessThanOrEqual(1);
39
+ expect(Math.abs(back.g - rgb.g)).toBeLessThanOrEqual(1);
40
+ expect(Math.abs(back.b - rgb.b)).toBeLessThanOrEqual(1);
41
+ });
42
+
43
+ it('rgbToHsl should handle grayscale', () => {
44
+ const hsl = rgbToHsl({ r: 128, g: 128, b: 128 });
45
+ expect(hsl.s).toBe(0);
46
+ });
47
+
48
+ it('rgbToHsv and hsvToRgb should round-trip', () => {
49
+ const rgb = { r: 100, g: 200, b: 50 };
50
+ const hsv = rgbToHsv(rgb);
51
+ const back = hsvToRgb(hsv);
52
+ expect(Math.abs(back.r - rgb.r)).toBeLessThanOrEqual(1);
53
+ expect(Math.abs(back.g - rgb.g)).toBeLessThanOrEqual(1);
54
+ expect(Math.abs(back.b - rgb.b)).toBeLessThanOrEqual(1);
55
+ });
56
+
57
+ it('rgbToHsv should handle black', () => {
58
+ const hsv = rgbToHsv({ r: 0, g: 0, b: 0 });
59
+ expect(hsv.v).toBe(0);
60
+ expect(hsv.s).toBe(0);
61
+ });
62
+
63
+ it('parseColor should parse hex', () => {
64
+ expect(parseColor('#3b82f6')).toEqual({ r: 59, g: 130, b: 246 });
65
+ });
66
+
67
+ it('parseColor should parse rgb()', () => {
68
+ expect(parseColor('rgb(255, 0, 0)')).toEqual({ r: 255, g: 0, b: 0 });
69
+ });
70
+
71
+ it('parseColor should parse hsl()', () => {
72
+ const result = parseColor('hsl(0, 100%, 50%)');
73
+ expect(result).not.toBeNull();
74
+ expect(result!.r).toBe(255);
75
+ });
76
+
77
+ it('parseColor should return null for invalid', () => {
78
+ expect(parseColor('notacolor')).toBeNull();
79
+ expect(parseColor('')).toBeNull();
80
+ });
81
+
82
+ it('formatColor should format as hex', () => {
83
+ expect(formatColor({ r: 255, g: 0, b: 0 }, 'hex')).toBe('#ff0000');
84
+ });
85
+
86
+ it('formatColor should format as rgb', () => {
87
+ expect(formatColor({ r: 255, g: 0, b: 0 }, 'rgb')).toBe('rgb(255, 0, 0)');
88
+ });
89
+
90
+ it('formatColor should format as hsl', () => {
91
+ const result = formatColor({ r: 255, g: 0, b: 0 }, 'hsl');
92
+ expect(result).toContain('hsl(');
93
+ expect(result).toContain('100%');
94
+ });
95
+
96
+ it('isValidColor should validate', () => {
97
+ expect(isValidColor('#ff0000')).toBe(true);
98
+ expect(isValidColor('rgb(0, 0, 0)')).toBe(true);
99
+ expect(isValidColor('hsl(120, 50%, 50%)')).toBe(true);
100
+ expect(isValidColor('invalid')).toBe(false);
101
+ expect(isValidColor('')).toBe(false);
102
+ });
103
+ });
104
+
105
+ // --- Component Tests ---
106
+ @Component({
107
+ standalone: true,
108
+ imports: [SnyColorPickerComponent],
109
+ template: `
110
+ <sny-color-picker
111
+ [(value)]="color"
112
+ [presets]="presets()"
113
+ [showFavorites]="showFavorites()"
114
+ [inline]="inline()"
115
+ [disabled]="disabled()"
116
+ (colorChange)="lastColor = $event"
117
+ />
118
+ `,
119
+ })
120
+ class TestHostComponent {
121
+ color = signal('#3b82f6');
122
+ presets = signal<ColorPickerPreset[]>([]);
123
+ showFavorites = signal(false);
124
+ inline = signal(false);
125
+ disabled = signal(false);
126
+ lastColor: string | null = null;
127
+ }
128
+
129
+ describe('SnyColorPickerComponent', () => {
130
+ let fixture: ComponentFixture<TestHostComponent>;
131
+ let el: HTMLElement;
132
+
133
+ beforeEach(async () => {
134
+ await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
135
+ fixture = TestBed.createComponent(TestHostComponent);
136
+ fixture.detectChanges();
137
+ el = fixture.nativeElement;
138
+ });
139
+
140
+ it('should render trigger with color swatch', () => {
141
+ const swatch = el.querySelector('[style*="background"]');
142
+ expect(swatch).not.toBeNull();
143
+ const trigger = el.querySelector('button');
144
+ // HSV round-trip may cause ±1 in RGB values
145
+ expect(trigger?.textContent).toContain('#3b8');
146
+ });
147
+
148
+ it('should open popover on click', () => {
149
+ const trigger = el.querySelector('button') as HTMLButtonElement;
150
+ trigger.click();
151
+ fixture.detectChanges();
152
+ const panel = el.querySelector('[role="dialog"]');
153
+ expect(panel).not.toBeNull();
154
+ const satPanel = el.querySelector('.cursor-crosshair');
155
+ expect(satPanel).not.toBeNull();
156
+ });
157
+
158
+ it('should close popover on escape', () => {
159
+ const trigger = el.querySelector('button') as HTMLButtonElement;
160
+ trigger.click();
161
+ fixture.detectChanges();
162
+ expect(el.querySelector('[role="dialog"]')).not.toBeNull();
163
+
164
+ // Dispatch on the host element (HostListener binds to host, not document)
165
+ const host = el.querySelector('sny-color-picker') as HTMLElement;
166
+ host.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
167
+ fixture.detectChanges();
168
+ expect(el.querySelector('[role="dialog"]')).toBeNull();
169
+ });
170
+
171
+ it('should render inline without trigger', () => {
172
+ fixture.componentInstance.inline.set(true);
173
+ fixture.detectChanges();
174
+ const panel = el.querySelector('[role="dialog"]');
175
+ expect(panel).not.toBeNull();
176
+ const satPanel = el.querySelector('.cursor-crosshair');
177
+ expect(satPanel).not.toBeNull();
178
+ // No trigger button in inline mode
179
+ const combobox = el.querySelector('[role="combobox"]');
180
+ expect(combobox).toBeNull();
181
+ });
182
+
183
+ it('should render presets when provided', () => {
184
+ fixture.componentInstance.presets.set([
185
+ { label: 'Reds', colors: ['#ff0000', '#cc0000', '#990000'] },
186
+ ]);
187
+ fixture.componentInstance.inline.set(true);
188
+ fixture.detectChanges();
189
+ const label = el.querySelector('.text-muted-foreground');
190
+ expect(label?.textContent).toContain('Reds');
191
+ const swatches = el.querySelectorAll('[title="#ff0000"], [title="#cc0000"], [title="#990000"]');
192
+ expect(swatches.length).toBe(3);
193
+ });
194
+
195
+ it('should select preset color on click', () => {
196
+ fixture.componentInstance.presets.set([
197
+ { colors: ['#ff0000'] },
198
+ ]);
199
+ fixture.componentInstance.inline.set(true);
200
+ fixture.detectChanges();
201
+
202
+ const swatch = el.querySelector('[title="#ff0000"]') as HTMLButtonElement;
203
+ swatch.click();
204
+ fixture.detectChanges();
205
+
206
+ expect(fixture.componentInstance.color()).toBe('#ff0000');
207
+ expect(fixture.componentInstance.lastColor).toBe('#ff0000');
208
+ });
209
+
210
+ it('should not open when disabled', () => {
211
+ fixture.componentInstance.disabled.set(true);
212
+ fixture.detectChanges();
213
+ const trigger = el.querySelector('button') as HTMLButtonElement;
214
+ expect(trigger.disabled).toBe(true);
215
+ });
216
+
217
+ it('should display format switcher and cycle formats', () => {
218
+ fixture.componentInstance.inline.set(true);
219
+ fixture.detectChanges();
220
+
221
+ const formatBtn = Array.from(el.querySelectorAll('button')).find(
222
+ (b) => b.textContent?.trim().toLowerCase() === 'hex'
223
+ ) as HTMLButtonElement;
224
+ expect(formatBtn).not.toBeNull();
225
+
226
+ // Cycle to rgb
227
+ formatBtn.click();
228
+ fixture.detectChanges();
229
+ const rgbBtn = Array.from(el.querySelectorAll('button')).find(
230
+ (b) => b.textContent?.trim().toLowerCase() === 'rgb'
231
+ );
232
+ expect(rgbBtn).not.toBeNull();
233
+ });
234
+
235
+ it('should have copy button in panel', () => {
236
+ fixture.componentInstance.inline.set(true);
237
+ fixture.detectChanges();
238
+ const copyBtn = el.querySelector('[title="Copy color"]');
239
+ expect(copyBtn).not.toBeNull();
240
+ });
241
+
242
+ it('should validate manual input', () => {
243
+ fixture.componentInstance.inline.set(true);
244
+ fixture.detectChanges();
245
+
246
+ const input = el.querySelector('input') as HTMLInputElement;
247
+ // Type valid hex
248
+ input.value = '#00ff00';
249
+ input.dispatchEvent(new Event('input'));
250
+ input.dispatchEvent(new Event('blur'));
251
+ fixture.detectChanges();
252
+
253
+ expect(fixture.componentInstance.color()).toBe('#00ff00');
254
+ });
255
+
256
+ it('should reject invalid manual input', () => {
257
+ fixture.componentInstance.inline.set(true);
258
+ fixture.detectChanges();
259
+
260
+ const originalColor = fixture.componentInstance.color();
261
+ const input = el.querySelector('input') as HTMLInputElement;
262
+ input.value = 'notacolor';
263
+ input.dispatchEvent(new Event('input'));
264
+ input.dispatchEvent(new Event('blur'));
265
+ fixture.detectChanges();
266
+
267
+ // Should revert to previous valid color
268
+ expect(fixture.componentInstance.color()).toBe(originalColor);
269
+ });
270
+
271
+ it('should show hue slider', () => {
272
+ fixture.componentInstance.inline.set(true);
273
+ fixture.detectChanges();
274
+ const hueTrack = el.querySelector('[style*="linear-gradient"]');
275
+ expect(hueTrack).not.toBeNull();
276
+ });
277
+ });
278
+
279
+ // --- Reactive Forms ---
280
+ @Component({
281
+ standalone: true,
282
+ imports: [ReactiveFormsModule, SnyColorPickerComponent],
283
+ template: `<sny-color-picker [formControl]="ctrl" [inline]="true" />`,
284
+ })
285
+ class ReactiveFormHost {
286
+ ctrl = new FormControl('#ff0000');
287
+ }
288
+
289
+ describe('SnyColorPickerComponent — Reactive Forms', () => {
290
+ let fixture: ComponentFixture<ReactiveFormHost>;
291
+ let el: HTMLElement;
292
+
293
+ beforeEach(async () => {
294
+ await TestBed.configureTestingModule({ imports: [ReactiveFormHost] }).compileComponents();
295
+ fixture = TestBed.createComponent(ReactiveFormHost);
296
+ fixture.detectChanges();
297
+ el = fixture.nativeElement;
298
+ });
299
+
300
+ it('should display initial FormControl value', () => {
301
+ const input = el.querySelector('input') as HTMLInputElement;
302
+ expect(input.value).toContain('#ff0000');
303
+ });
304
+
305
+ it('should update display when FormControl value changes', () => {
306
+ fixture.componentInstance.ctrl.setValue('#00ff00');
307
+ fixture.detectChanges();
308
+ const input = el.querySelector('input') as HTMLInputElement;
309
+ expect(input.value).toContain('#00ff00');
310
+ });
311
+
312
+ it('should update FormControl when user selects a preset', () => {
313
+ // The inline component is rendered, interact with input
314
+ const input = el.querySelector('input') as HTMLInputElement;
315
+ input.value = '#0000ff';
316
+ input.dispatchEvent(new Event('input'));
317
+ input.dispatchEvent(new Event('blur'));
318
+ fixture.detectChanges();
319
+
320
+ expect(fixture.componentInstance.ctrl.value).toBe('#0000ff');
321
+ });
322
+
323
+ it('should disable via FormControl.disable()', () => {
324
+ fixture.componentInstance.ctrl.disable();
325
+ fixture.detectChanges();
326
+ expect(fixture.componentInstance.ctrl.disabled).toBe(true);
327
+ });
328
+ });