@sonny-ui/core 0.1.0-alpha.14 → 0.1.0-alpha.16

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 (30) hide show
  1. package/fesm2022/sonny-ui-core.mjs +2257 -68
  2. package/fesm2022/sonny-ui-core.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/src/lib/calendar/calendar.component.spec.ts +87 -0
  5. package/src/lib/calendar/calendar.component.ts +184 -61
  6. package/src/lib/calendar/calendar.types.ts +24 -0
  7. package/src/lib/calendar/index.ts +6 -0
  8. package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
  9. package/src/lib/color-picker/color-picker.component.ts +537 -0
  10. package/src/lib/color-picker/color-picker.types.ts +24 -0
  11. package/src/lib/color-picker/color-picker.utils.ts +183 -0
  12. package/src/lib/color-picker/color-picker.variants.ts +17 -0
  13. package/src/lib/color-picker/index.ts +20 -0
  14. package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
  15. package/src/lib/command-palette/command-palette.component.ts +195 -0
  16. package/src/lib/command-palette/command-palette.service.ts +36 -0
  17. package/src/lib/command-palette/command-palette.types.ts +23 -0
  18. package/src/lib/command-palette/index.ts +7 -0
  19. package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
  20. package/src/lib/date-picker/date-picker.component.ts +220 -0
  21. package/src/lib/date-picker/date-picker.variants.ts +17 -0
  22. package/src/lib/date-picker/index.ts +2 -0
  23. package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
  24. package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
  25. package/src/lib/date-range-picker/index.ts +1 -0
  26. package/src/lib/otp-input/index.ts +2 -0
  27. package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
  28. package/src/lib/otp-input/otp-input.component.ts +275 -0
  29. package/src/lib/otp-input/otp-input.variants.ts +18 -0
  30. package/types/sonny-ui-core.d.ts +331 -7
@@ -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
+ });