@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.
- package/README.md +187 -40
- package/fesm2022/sonny-ui-core.mjs +6642 -268
- package/fesm2022/sonny-ui-core.mjs.map +1 -1
- package/package.json +8 -5
- package/schematics/ng-add/index.js +27 -0
- package/schematics/ng-add/schema.json +1 -1
- package/schematics/ng-generate/component/index.js +182 -1
- package/schematics/ng-generate/component/schema.json +2 -2
- package/src/lib/accordion/accordion.directives.spec.ts +173 -0
- package/src/lib/accordion/accordion.directives.ts +143 -0
- package/src/lib/accordion/index.ts +8 -0
- package/src/lib/alert/alert.directives.spec.ts +154 -0
- package/src/lib/alert/alert.directives.ts +67 -0
- package/src/lib/alert/alert.variants.ts +25 -0
- package/src/lib/alert/index.ts +6 -0
- package/src/lib/avatar/avatar.component.spec.ts +75 -0
- package/src/lib/avatar/avatar.component.ts +43 -0
- package/src/lib/avatar/avatar.variants.ts +26 -0
- package/src/lib/avatar/index.ts +2 -0
- package/src/lib/avatar-group/avatar-group.component.spec.ts +74 -0
- package/src/lib/avatar-group/avatar-group.component.ts +88 -0
- package/src/lib/avatar-group/index.ts +1 -0
- package/src/lib/badge/badge.directive.spec.ts +74 -0
- package/src/lib/badge/badge.directive.ts +17 -0
- package/src/lib/badge/badge.variants.ts +29 -0
- package/src/lib/badge/index.ts +2 -0
- package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
- package/src/lib/breadcrumb/breadcrumb.directives.ts +78 -0
- package/src/lib/breadcrumb/index.ts +8 -0
- package/src/lib/button/button.directive.spec.ts +92 -0
- package/src/lib/button/button.directive.ts +28 -0
- package/src/lib/button/button.variants.ts +30 -0
- package/src/lib/button/index.ts +2 -0
- package/src/lib/button-group/button-group.directive.spec.ts +46 -0
- package/src/lib/button-group/button-group.directive.ts +19 -0
- package/src/lib/button-group/button-group.variants.ts +18 -0
- package/src/lib/button-group/index.ts +2 -0
- package/src/lib/calendar/calendar.component.spec.ts +192 -0
- package/src/lib/calendar/calendar.component.ts +342 -0
- package/src/lib/calendar/calendar.types.ts +24 -0
- package/src/lib/calendar/index.ts +7 -0
- package/src/lib/card/card.directives.spec.ts +104 -0
- package/src/lib/card/card.directives.ts +72 -0
- package/src/lib/card/card.variants.ts +28 -0
- package/src/lib/card/index.ts +9 -0
- package/src/lib/carousel/carousel.directives.spec.ts +85 -0
- package/src/lib/carousel/carousel.directives.ts +159 -0
- package/src/lib/carousel/index.ts +8 -0
- package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
- package/src/lib/chat-bubble/chat-bubble.directives.ts +96 -0
- package/src/lib/chat-bubble/index.ts +11 -0
- package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
- package/src/lib/checkbox/checkbox.directive.ts +16 -0
- package/src/lib/checkbox/checkbox.variants.ts +19 -0
- package/src/lib/checkbox/index.ts +2 -0
- package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
- package/src/lib/color-picker/color-picker.component.ts +537 -0
- package/src/lib/color-picker/color-picker.types.ts +24 -0
- package/src/lib/color-picker/color-picker.utils.ts +183 -0
- package/src/lib/color-picker/color-picker.variants.ts +17 -0
- package/src/lib/color-picker/index.ts +20 -0
- package/src/lib/combobox/combobox.component.spec.ts +151 -0
- package/src/lib/combobox/combobox.component.ts +264 -0
- package/src/lib/combobox/combobox.variants.ts +19 -0
- package/src/lib/combobox/index.ts +2 -0
- package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
- package/src/lib/command-palette/command-palette.component.ts +194 -0
- package/src/lib/command-palette/command-palette.service.ts +36 -0
- package/src/lib/command-palette/command-palette.types.ts +23 -0
- package/src/lib/command-palette/index.ts +7 -0
- package/src/lib/data-table/data-table.component.spec.ts +443 -0
- package/src/lib/data-table/data-table.component.ts +602 -0
- package/src/lib/data-table/data-table.directives.ts +31 -0
- package/src/lib/data-table/data-table.types.ts +20 -0
- package/src/lib/data-table/index.ts +13 -0
- package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
- package/src/lib/date-picker/date-picker.component.ts +220 -0
- package/src/lib/date-picker/date-picker.variants.ts +17 -0
- package/src/lib/date-picker/index.ts +2 -0
- package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
- package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
- package/src/lib/date-range-picker/index.ts +1 -0
- package/src/lib/diff/diff.component.spec.ts +47 -0
- package/src/lib/diff/diff.component.ts +82 -0
- package/src/lib/diff/index.ts +1 -0
- package/src/lib/divider/divider.component.spec.ts +48 -0
- package/src/lib/divider/divider.component.ts +51 -0
- package/src/lib/divider/divider.variants.ts +22 -0
- package/src/lib/divider/index.ts +2 -0
- package/src/lib/dock/dock.directives.spec.ts +85 -0
- package/src/lib/dock/dock.directives.ts +81 -0
- package/src/lib/dock/index.ts +1 -0
- package/src/lib/drawer/drawer.directives.spec.ts +62 -0
- package/src/lib/drawer/drawer.directives.ts +80 -0
- package/src/lib/drawer/index.ts +8 -0
- package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
- package/src/lib/dropdown/dropdown.directives.ts +136 -0
- package/src/lib/dropdown/dropdown.variants.ts +27 -0
- package/src/lib/dropdown/index.ts +15 -0
- package/src/lib/fab/fab.directives.spec.ts +60 -0
- package/src/lib/fab/fab.directives.ts +77 -0
- package/src/lib/fab/index.ts +8 -0
- package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
- package/src/lib/fieldset/fieldset.directives.ts +49 -0
- package/src/lib/fieldset/fieldset.variants.ts +15 -0
- package/src/lib/fieldset/index.ts +6 -0
- package/src/lib/file-input/file-input.component.spec.ts +114 -0
- package/src/lib/file-input/file-input.component.ts +155 -0
- package/src/lib/file-input/file-input.variants.ts +25 -0
- package/src/lib/file-input/index.ts +6 -0
- package/src/lib/indicator/index.ts +6 -0
- package/src/lib/indicator/indicator.directives.spec.ts +64 -0
- package/src/lib/indicator/indicator.directives.ts +59 -0
- package/src/lib/input/index.ts +3 -0
- package/src/lib/input/input.directive.spec.ts +103 -0
- package/src/lib/input/input.directive.ts +25 -0
- package/src/lib/input/input.variants.ts +42 -0
- package/src/lib/input/label.directive.ts +16 -0
- package/src/lib/kbd/index.ts +2 -0
- package/src/lib/kbd/kbd.directive.spec.ts +42 -0
- package/src/lib/kbd/kbd.directive.ts +18 -0
- package/src/lib/kbd/kbd.variants.ts +19 -0
- package/src/lib/link/index.ts +2 -0
- package/src/lib/link/link.directive.spec.ts +41 -0
- package/src/lib/link/link.directive.ts +18 -0
- package/src/lib/link/link.variants.ts +20 -0
- package/src/lib/list/index.ts +8 -0
- package/src/lib/list/list.directives.spec.ts +65 -0
- package/src/lib/list/list.directives.ts +81 -0
- package/src/lib/loader/index.ts +2 -0
- package/src/lib/loader/loader.component.spec.ts +58 -0
- package/src/lib/loader/loader.component.ts +47 -0
- package/src/lib/loader/loader.variants.ts +21 -0
- package/src/lib/modal/dialog-ref.ts +19 -0
- package/src/lib/modal/dialog.directives.ts +84 -0
- package/src/lib/modal/dialog.service.spec.ts +52 -0
- package/src/lib/modal/dialog.service.ts +61 -0
- package/src/lib/modal/dialog.types.ts +16 -0
- package/src/lib/modal/index.ts +11 -0
- package/src/lib/navbar/index.ts +7 -0
- package/src/lib/navbar/navbar.directives.spec.ts +59 -0
- package/src/lib/navbar/navbar.directives.ts +57 -0
- package/src/lib/number-input/index.ts +2 -0
- package/src/lib/number-input/number-input.component.spec.ts +151 -0
- package/src/lib/number-input/number-input.component.ts +152 -0
- package/src/lib/number-input/number-input.variants.ts +17 -0
- package/src/lib/otp-input/index.ts +2 -0
- package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
- package/src/lib/otp-input/otp-input.component.ts +274 -0
- package/src/lib/otp-input/otp-input.variants.ts +18 -0
- package/src/lib/pagination/index.ts +6 -0
- package/src/lib/pagination/pagination.component.spec.ts +59 -0
- package/src/lib/pagination/pagination.component.ts +143 -0
- package/src/lib/pagination/pagination.variants.ts +31 -0
- package/src/lib/popover/index.ts +6 -0
- package/src/lib/popover/popover.directives.spec.ts +147 -0
- package/src/lib/popover/popover.directives.ts +151 -0
- package/src/lib/progress/index.ts +7 -0
- package/src/lib/progress/progress.component.spec.ts +117 -0
- package/src/lib/progress/progress.component.ts +64 -0
- package/src/lib/progress/progress.variants.ts +43 -0
- package/src/lib/radial-progress/index.ts +5 -0
- package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
- package/src/lib/radial-progress/radial-progress.component.ts +70 -0
- package/src/lib/radio/index.ts +2 -0
- package/src/lib/radio/radio.directive.spec.ts +46 -0
- package/src/lib/radio/radio.directive.ts +16 -0
- package/src/lib/radio/radio.variants.ts +19 -0
- package/src/lib/rating/index.ts +2 -0
- package/src/lib/rating/rating.component.spec.ts +157 -0
- package/src/lib/rating/rating.component.ts +163 -0
- package/src/lib/rating/rating.variants.ts +20 -0
- package/src/lib/select/index.ts +2 -0
- package/src/lib/select/select.component.spec.ts +112 -0
- package/src/lib/select/select.component.ts +235 -0
- package/src/lib/select/select.variants.ts +19 -0
- package/src/lib/sheet/index.ts +10 -0
- package/src/lib/sheet/sheet-ref.ts +18 -0
- package/src/lib/sheet/sheet.component.spec.ts +67 -0
- package/src/lib/sheet/sheet.directives.ts +70 -0
- package/src/lib/sheet/sheet.service.ts +100 -0
- package/src/lib/sheet/sheet.types.ts +23 -0
- package/src/lib/skeleton/index.ts +2 -0
- package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
- package/src/lib/skeleton/skeleton.directive.ts +21 -0
- package/src/lib/skeleton/skeleton.variants.ts +27 -0
- package/src/lib/slider/index.ts +2 -0
- package/src/lib/slider/slider.component.spec.ts +104 -0
- package/src/lib/slider/slider.component.ts +181 -0
- package/src/lib/slider/slider.variants.ts +25 -0
- package/src/lib/stat/index.ts +8 -0
- package/src/lib/stat/stat.directives.spec.ts +60 -0
- package/src/lib/stat/stat.directives.ts +79 -0
- package/src/lib/status/index.ts +2 -0
- package/src/lib/status/status.directive.spec.ts +43 -0
- package/src/lib/status/status.directive.ts +37 -0
- package/src/lib/status/status.variants.ts +26 -0
- package/src/lib/steps/index.ts +8 -0
- package/src/lib/steps/steps.directives.spec.ts +52 -0
- package/src/lib/steps/steps.directives.ts +78 -0
- package/src/lib/switch/index.ts +2 -0
- package/src/lib/switch/switch.component.spec.ts +98 -0
- package/src/lib/switch/switch.component.ts +76 -0
- package/src/lib/switch/switch.variants.ts +31 -0
- package/src/lib/table/index.ts +12 -0
- package/src/lib/table/table.directives.spec.ts +111 -0
- package/src/lib/table/table.directives.ts +126 -0
- package/src/lib/table/table.variants.ts +36 -0
- package/src/lib/tabs/index.ts +8 -0
- package/src/lib/tabs/tabs.directives.spec.ts +136 -0
- package/src/lib/tabs/tabs.directives.ts +126 -0
- package/src/lib/tabs/tabs.variants.ts +17 -0
- package/src/lib/tag-input/index.ts +2 -0
- package/src/lib/tag-input/tag-input.component.spec.ts +190 -0
- package/src/lib/tag-input/tag-input.component.ts +172 -0
- package/src/lib/tag-input/tag-input.variants.ts +31 -0
- package/src/lib/textarea/index.ts +7 -0
- package/src/lib/textarea/textarea.directive.spec.ts +84 -0
- package/src/lib/textarea/textarea.directive.ts +71 -0
- package/src/lib/textarea/textarea.variants.ts +34 -0
- package/src/lib/timeline/index.ts +11 -0
- package/src/lib/timeline/timeline.directives.spec.ts +55 -0
- package/src/lib/timeline/timeline.directives.ts +85 -0
- package/src/lib/toast/index.ts +3 -0
- package/src/lib/toast/toast.service.spec.ts +71 -0
- package/src/lib/toast/toast.service.ts +60 -0
- package/src/lib/toast/toast.variants.ts +38 -0
- package/src/lib/toast/toaster.component.spec.ts +38 -0
- package/src/lib/toast/toaster.component.ts +81 -0
- package/src/lib/toggle/index.ts +2 -0
- package/src/lib/toggle/toggle.directive.spec.ts +100 -0
- package/src/lib/toggle/toggle.directive.ts +61 -0
- package/src/lib/toggle/toggle.variants.ts +25 -0
- package/src/lib/tooltip/index.ts +2 -0
- package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
- package/src/lib/tooltip/tooltip.directive.ts +130 -0
- package/src/lib/tooltip/tooltip.variants.ts +20 -0
- package/src/lib/validator/index.ts +5 -0
- package/src/lib/validator/validator.directives.spec.ts +47 -0
- package/src/lib/validator/validator.directives.ts +50 -0
- package/src/styles/sonny-theme.css +33 -0
- package/types/sonny-ui-core.d.ts +1443 -13
|
@@ -0,0 +1,192 @@
|
|
|
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 { SnyCalendarComponent } from './calendar.component';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [SnyCalendarComponent],
|
|
9
|
+
template: `<sny-calendar [(value)]="selectedDate" />`,
|
|
10
|
+
})
|
|
11
|
+
class TestHostComponent {
|
|
12
|
+
selectedDate = signal<Date | null>(null);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('SnyCalendarComponent', () => {
|
|
16
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
17
|
+
let host: HTMLElement;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
21
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
22
|
+
fixture.detectChanges();
|
|
23
|
+
host = fixture.nativeElement.querySelector('sny-calendar');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should render a grid', () => {
|
|
27
|
+
const grid = host.querySelector('[role="grid"]');
|
|
28
|
+
expect(grid).not.toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should render day headers', () => {
|
|
32
|
+
const headers = host.querySelectorAll('[role="grid"] > div:not(button)');
|
|
33
|
+
expect(headers.length).toBe(7);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should render 42 day buttons', () => {
|
|
37
|
+
const buttons = host.querySelectorAll('[role="grid"] button');
|
|
38
|
+
expect(buttons.length).toBe(42);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should navigate months', () => {
|
|
42
|
+
const prevBtn = host.querySelector('[aria-label="Previous month"]') as HTMLButtonElement;
|
|
43
|
+
const nextBtn = host.querySelector('[aria-label="Next month"]') as HTMLButtonElement;
|
|
44
|
+
expect(prevBtn).not.toBeNull();
|
|
45
|
+
expect(nextBtn).not.toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should select a date on click', () => {
|
|
49
|
+
const buttons = host.querySelectorAll('[role="grid"] button:not([disabled])');
|
|
50
|
+
const dayButton = Array.from(buttons).find((b) => b.textContent?.trim() === '15') as HTMLButtonElement;
|
|
51
|
+
if (dayButton) {
|
|
52
|
+
dayButton.click();
|
|
53
|
+
fixture.detectChanges();
|
|
54
|
+
expect(fixture.componentInstance.selectedDate()).not.toBeNull();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
@Component({
|
|
60
|
+
standalone: true,
|
|
61
|
+
imports: [ReactiveFormsModule, SnyCalendarComponent],
|
|
62
|
+
template: `<sny-calendar [formControl]="ctrl" />`,
|
|
63
|
+
})
|
|
64
|
+
class ReactiveFormHost {
|
|
65
|
+
ctrl = new FormControl<Date | null>(null);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('SnyCalendarComponent — Reactive Forms', () => {
|
|
69
|
+
let fixture: ComponentFixture<ReactiveFormHost>;
|
|
70
|
+
let host: HTMLElement;
|
|
71
|
+
|
|
72
|
+
beforeEach(async () => {
|
|
73
|
+
await TestBed.configureTestingModule({ imports: [ReactiveFormHost] }).compileComponents();
|
|
74
|
+
fixture = TestBed.createComponent(ReactiveFormHost);
|
|
75
|
+
fixture.detectChanges();
|
|
76
|
+
host = fixture.nativeElement.querySelector('sny-calendar');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should update view when FormControl value changes (writeValue)', () => {
|
|
80
|
+
const date = new Date(2025, 5, 15);
|
|
81
|
+
fixture.componentInstance.ctrl.setValue(date);
|
|
82
|
+
fixture.detectChanges();
|
|
83
|
+
const selected = host.querySelector('[role="grid"] button[aria-selected="true"]');
|
|
84
|
+
expect(selected).not.toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should update FormControl when user interacts (onChange)', () => {
|
|
88
|
+
const buttons = host.querySelectorAll('[role="grid"] button:not([disabled])');
|
|
89
|
+
const dayButton = Array.from(buttons).find((b) => b.textContent?.trim() === '15') as HTMLButtonElement;
|
|
90
|
+
if (dayButton) {
|
|
91
|
+
dayButton.click();
|
|
92
|
+
fixture.detectChanges();
|
|
93
|
+
expect(fixture.componentInstance.ctrl.value).not.toBeNull();
|
|
94
|
+
expect(fixture.componentInstance.ctrl.value!.getDate()).toBe(15);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should disable via FormControl.disable() (setDisabledState)', () => {
|
|
99
|
+
fixture.componentInstance.ctrl.disable();
|
|
100
|
+
fixture.detectChanges();
|
|
101
|
+
const buttons = host.querySelectorAll('[role="grid"] button');
|
|
102
|
+
const allDisabled = Array.from(buttons).every((b) => (b as HTMLButtonElement).disabled || b.getAttribute('aria-disabled') === 'true');
|
|
103
|
+
expect(allDisabled).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// --- Range Mode Tests ---
|
|
108
|
+
|
|
109
|
+
import type { DateRange } from './calendar.types';
|
|
110
|
+
|
|
111
|
+
@Component({
|
|
112
|
+
standalone: true,
|
|
113
|
+
imports: [SnyCalendarComponent],
|
|
114
|
+
template: `<sny-calendar mode="range" [(rangeValue)]="range" />`,
|
|
115
|
+
})
|
|
116
|
+
class RangeTestHost {
|
|
117
|
+
range = signal<DateRange | null>(null);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('SnyCalendarComponent — Range Mode', () => {
|
|
121
|
+
let fixture: ComponentFixture<RangeTestHost>;
|
|
122
|
+
let host: HTMLElement;
|
|
123
|
+
|
|
124
|
+
beforeEach(async () => {
|
|
125
|
+
await TestBed.configureTestingModule({ imports: [RangeTestHost] }).compileComponents();
|
|
126
|
+
fixture = TestBed.createComponent(RangeTestHost);
|
|
127
|
+
fixture.detectChanges();
|
|
128
|
+
host = fixture.nativeElement.querySelector('sny-calendar');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
function clickDay(dayNum: string): void {
|
|
132
|
+
const buttons = host.querySelectorAll('[role="grid"] button:not([disabled])');
|
|
133
|
+
const btn = Array.from(buttons).find((b) => b.textContent?.trim() === dayNum) as HTMLButtonElement;
|
|
134
|
+
btn?.click();
|
|
135
|
+
fixture.detectChanges();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
it('should render 42 buttons in range mode', () => {
|
|
139
|
+
const buttons = host.querySelectorAll('[role="grid"] button');
|
|
140
|
+
expect(buttons.length).toBe(42);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should set range start on first click', () => {
|
|
144
|
+
clickDay('10');
|
|
145
|
+
const range = fixture.componentInstance.range();
|
|
146
|
+
expect(range).not.toBeNull();
|
|
147
|
+
expect(range!.start).not.toBeNull();
|
|
148
|
+
expect(range!.start!.getDate()).toBe(10);
|
|
149
|
+
expect(range!.end).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should set range end on second click', () => {
|
|
153
|
+
clickDay('10');
|
|
154
|
+
clickDay('20');
|
|
155
|
+
const range = fixture.componentInstance.range();
|
|
156
|
+
expect(range!.start!.getDate()).toBe(10);
|
|
157
|
+
expect(range!.end!.getDate()).toBe(20);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should swap if second click is before start', () => {
|
|
161
|
+
clickDay('20');
|
|
162
|
+
clickDay('5');
|
|
163
|
+
const range = fixture.componentInstance.range();
|
|
164
|
+
expect(range!.start!.getDate()).toBe(5);
|
|
165
|
+
expect(range!.end!.getDate()).toBe(20);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should reset range on third click', () => {
|
|
169
|
+
clickDay('10');
|
|
170
|
+
clickDay('20');
|
|
171
|
+
clickDay('15');
|
|
172
|
+
const range = fixture.componentInstance.range();
|
|
173
|
+
expect(range!.start!.getDate()).toBe(15);
|
|
174
|
+
expect(range!.end).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should have range highlight classes when range is set', () => {
|
|
178
|
+
clickDay('10');
|
|
179
|
+
clickDay('15');
|
|
180
|
+
fixture.detectChanges();
|
|
181
|
+
const buttons = host.querySelectorAll('[role="grid"] button');
|
|
182
|
+
const classes = Array.from(buttons).map((b) => b.className);
|
|
183
|
+
const hasRangeStyle = classes.some((c) => c.includes('bg-primary/15') || c.includes('rounded-l-none') || c.includes('rounded-r-none'));
|
|
184
|
+
expect(hasRangeStyle).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should not affect single mode behavior', () => {
|
|
188
|
+
// This test uses the basic TestHostComponent which defaults to single mode
|
|
189
|
+
// Regression test: existing single mode tests above should still pass
|
|
190
|
+
expect(true).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
forwardRef,
|
|
6
|
+
input,
|
|
7
|
+
linkedSignal,
|
|
8
|
+
model,
|
|
9
|
+
signal,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
12
|
+
import { cn } from '../core/utils/cn';
|
|
13
|
+
import type { CalendarDay, CalendarMode, DateRange } from './calendar.types';
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'sny-calendar',
|
|
17
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18
|
+
host: {
|
|
19
|
+
'[class]': 'hostClass()',
|
|
20
|
+
'(keydown)': 'onKeydown($event)',
|
|
21
|
+
'role': 'application',
|
|
22
|
+
'aria-label': 'Calendar',
|
|
23
|
+
},
|
|
24
|
+
providers: [
|
|
25
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SnyCalendarComponent), multi: true },
|
|
26
|
+
],
|
|
27
|
+
template: `
|
|
28
|
+
@if (showNavigation()) {
|
|
29
|
+
<div class="flex items-center justify-between mb-3">
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
class="inline-flex items-center justify-center rounded-md h-8 w-8 hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
33
|
+
(click)="prevMonth()"
|
|
34
|
+
aria-label="Previous month"
|
|
35
|
+
>
|
|
36
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
37
|
+
</button>
|
|
38
|
+
<span class="text-sm font-semibold tracking-tight">{{ monthYearLabel() }}</span>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
class="inline-flex items-center justify-center rounded-md h-8 w-8 hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
42
|
+
(click)="nextMonth()"
|
|
43
|
+
aria-label="Next month"
|
|
44
|
+
>
|
|
45
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
<div role="grid" class="grid grid-cols-7 gap-1">
|
|
51
|
+
@for (dayName of weekDays; track dayName) {
|
|
52
|
+
<div class="text-center text-xs text-muted-foreground font-medium h-9 flex items-center justify-center" role="columnheader">{{ dayName }}</div>
|
|
53
|
+
}
|
|
54
|
+
@for (day of days(); track day.date.getTime()) {
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
[class]="dayClass(day)"
|
|
58
|
+
[disabled]="day.isDisabled"
|
|
59
|
+
[attr.aria-selected]="day.isSelected || day.isRangeStart || day.isRangeEnd || null"
|
|
60
|
+
[attr.aria-current]="day.isToday ? 'date' : null"
|
|
61
|
+
[attr.aria-disabled]="day.isDisabled || null"
|
|
62
|
+
[attr.aria-label]="day.date.toLocaleDateString(locale(), { month: 'long', day: 'numeric', year: 'numeric' })"
|
|
63
|
+
role="gridcell"
|
|
64
|
+
(click)="onDayClick(day.date)"
|
|
65
|
+
(mouseenter)="onDayHover(day.date)"
|
|
66
|
+
(mouseleave)="onDayHover(null)"
|
|
67
|
+
>
|
|
68
|
+
{{ day.day }}
|
|
69
|
+
</button>
|
|
70
|
+
}
|
|
71
|
+
</div>
|
|
72
|
+
`,
|
|
73
|
+
})
|
|
74
|
+
export class SnyCalendarComponent implements ControlValueAccessor {
|
|
75
|
+
// Existing inputs (backwards compatible)
|
|
76
|
+
readonly value = model<Date | null>(null);
|
|
77
|
+
readonly min = input<Date | undefined>(undefined);
|
|
78
|
+
readonly max = input<Date | undefined>(undefined);
|
|
79
|
+
readonly locale = input('en-US');
|
|
80
|
+
readonly class = input<string>('');
|
|
81
|
+
|
|
82
|
+
// Range mode inputs
|
|
83
|
+
readonly mode = input<CalendarMode>('single');
|
|
84
|
+
readonly rangeValue = model<DateRange | null>(null);
|
|
85
|
+
readonly showNavigation = input(true);
|
|
86
|
+
readonly initialViewDate = input<Date | undefined>(undefined);
|
|
87
|
+
readonly borderless = input(false);
|
|
88
|
+
|
|
89
|
+
readonly hostClass = computed(() =>
|
|
90
|
+
this.borderless()
|
|
91
|
+
? 'inline-block p-3 bg-background'
|
|
92
|
+
: 'inline-block p-4 rounded-md border border-border bg-background'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Internal state
|
|
96
|
+
private readonly _disabledByCva = signal(false);
|
|
97
|
+
readonly hoveredDate = signal<Date | null>(null);
|
|
98
|
+
readonly viewDate = linkedSignal(() => this.initialViewDate() ?? new Date());
|
|
99
|
+
readonly weekDays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
100
|
+
|
|
101
|
+
// CVA
|
|
102
|
+
private _onChange: (value: unknown) => void = () => {};
|
|
103
|
+
protected onTouched: () => void = () => {};
|
|
104
|
+
|
|
105
|
+
writeValue(val: unknown): void {
|
|
106
|
+
if (this.mode() === 'range') {
|
|
107
|
+
this.rangeValue.set((val as DateRange) ?? null);
|
|
108
|
+
const range = val as DateRange | null;
|
|
109
|
+
if (range?.start) {
|
|
110
|
+
this.viewDate.set(new Date(range.start.getFullYear(), range.start.getMonth(), 1));
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
this.value.set((val as Date) ?? null);
|
|
114
|
+
if (val) {
|
|
115
|
+
const d = val as Date;
|
|
116
|
+
this.viewDate.set(new Date(d.getFullYear(), d.getMonth(), 1));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
registerOnChange(fn: (value: unknown) => void): void {
|
|
122
|
+
this._onChange = fn;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
registerOnTouched(fn: () => void): void {
|
|
126
|
+
this.onTouched = fn;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setDisabledState(isDisabled: boolean): void {
|
|
130
|
+
this._disabledByCva.set(isDisabled);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Computed
|
|
134
|
+
readonly monthYearLabel = computed(() => {
|
|
135
|
+
const d = this.viewDate();
|
|
136
|
+
return d.toLocaleDateString(this.locale(), { month: 'long', year: 'numeric' });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
readonly days = computed<CalendarDay[]>(() => {
|
|
140
|
+
const view = this.viewDate();
|
|
141
|
+
const year = view.getFullYear();
|
|
142
|
+
const month = view.getMonth();
|
|
143
|
+
const today = new Date();
|
|
144
|
+
const selected = this.value();
|
|
145
|
+
const rangeVal = this.mode() === 'range' ? this.rangeValue() : null;
|
|
146
|
+
const hovered = this.hoveredDate();
|
|
147
|
+
const minDate = this.min();
|
|
148
|
+
const maxDate = this.max();
|
|
149
|
+
|
|
150
|
+
const firstDay = new Date(year, month, 1);
|
|
151
|
+
const startDay = firstDay.getDay();
|
|
152
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
153
|
+
const daysInPrevMonth = new Date(year, month, 0).getDate();
|
|
154
|
+
|
|
155
|
+
const days: CalendarDay[] = [];
|
|
156
|
+
|
|
157
|
+
for (let i = startDay - 1; i >= 0; i--) {
|
|
158
|
+
const date = new Date(year, month - 1, daysInPrevMonth - i);
|
|
159
|
+
days.push(this.createDay(date, false, today, selected, rangeVal, hovered, minDate, maxDate));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
163
|
+
const date = new Date(year, month, d);
|
|
164
|
+
days.push(this.createDay(date, true, today, selected, rangeVal, hovered, minDate, maxDate));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const remaining = 42 - days.length;
|
|
168
|
+
for (let d = 1; d <= remaining; d++) {
|
|
169
|
+
const date = new Date(year, month + 1, d);
|
|
170
|
+
days.push(this.createDay(date, false, today, selected, rangeVal, hovered, minDate, maxDate));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return days;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Navigation
|
|
177
|
+
prevMonth(): void {
|
|
178
|
+
this.viewDate.set(new Date(
|
|
179
|
+
this.viewDate().getFullYear(),
|
|
180
|
+
this.viewDate().getMonth() - 1,
|
|
181
|
+
1,
|
|
182
|
+
));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
nextMonth(): void {
|
|
186
|
+
this.viewDate.set(new Date(
|
|
187
|
+
this.viewDate().getFullYear(),
|
|
188
|
+
this.viewDate().getMonth() + 1,
|
|
189
|
+
1,
|
|
190
|
+
));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Click handler
|
|
194
|
+
onDayClick(date: Date): void {
|
|
195
|
+
if (this.mode() === 'single') {
|
|
196
|
+
this.value.set(date);
|
|
197
|
+
this._onChange(date);
|
|
198
|
+
this.onTouched();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Range mode
|
|
203
|
+
const current = this.rangeValue();
|
|
204
|
+
if (!current?.start || (current.start && current.end)) {
|
|
205
|
+
this.rangeValue.set({ start: date, end: null });
|
|
206
|
+
} else {
|
|
207
|
+
const start = current.start;
|
|
208
|
+
if (date < start) {
|
|
209
|
+
this.rangeValue.set({ start: date, end: start });
|
|
210
|
+
} else if (this.isSameDay(date, start)) {
|
|
211
|
+
this.rangeValue.set({ start: date, end: date });
|
|
212
|
+
} else {
|
|
213
|
+
this.rangeValue.set({ start, end: date });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this._onChange(this.rangeValue());
|
|
217
|
+
this.onTouched();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Hover handler
|
|
221
|
+
onDayHover(date: Date | null): void {
|
|
222
|
+
if (this.mode() === 'range') {
|
|
223
|
+
this.hoveredDate.set(date);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Keyboard
|
|
228
|
+
onKeydown(event: KeyboardEvent): void {
|
|
229
|
+
switch (event.key) {
|
|
230
|
+
case 'ArrowLeft':
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
this.navigateDays(-1);
|
|
233
|
+
break;
|
|
234
|
+
case 'ArrowRight':
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
this.navigateDays(1);
|
|
237
|
+
break;
|
|
238
|
+
case 'ArrowUp':
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
this.navigateDays(-7);
|
|
241
|
+
break;
|
|
242
|
+
case 'ArrowDown':
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
this.navigateDays(7);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Styling
|
|
250
|
+
dayClass(day: CalendarDay): string {
|
|
251
|
+
const isEndpoint = day.isRangeStart || day.isRangeEnd;
|
|
252
|
+
return cn(
|
|
253
|
+
'inline-flex items-center justify-center text-sm h-9 w-9 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
254
|
+
// Shape
|
|
255
|
+
day.isRangeStart && !day.isRangeEnd ? 'rounded-l-md rounded-r-none' :
|
|
256
|
+
day.isRangeEnd && !day.isRangeStart ? 'rounded-r-md rounded-l-none' :
|
|
257
|
+
day.isInRange || day.isRangePreview ? 'rounded-none' :
|
|
258
|
+
'rounded-md',
|
|
259
|
+
// Base text color
|
|
260
|
+
!day.isCurrentMonth && 'text-muted-foreground/40',
|
|
261
|
+
day.isCurrentMonth && !day.isSelected && !isEndpoint && 'text-foreground',
|
|
262
|
+
// Today indicator
|
|
263
|
+
day.isToday && !day.isSelected && !isEndpoint && 'bg-accent text-accent-foreground font-semibold',
|
|
264
|
+
// Single selected
|
|
265
|
+
day.isSelected && this.mode() === 'single' && 'bg-primary text-primary-foreground font-semibold shadow-sm',
|
|
266
|
+
// Range endpoints
|
|
267
|
+
isEndpoint && 'bg-primary text-primary-foreground font-semibold shadow-sm',
|
|
268
|
+
// Range band
|
|
269
|
+
day.isInRange && 'bg-primary/10 text-foreground',
|
|
270
|
+
day.isRangePreview && 'bg-primary/5 text-foreground',
|
|
271
|
+
// States
|
|
272
|
+
day.isDisabled && 'opacity-40 cursor-not-allowed pointer-events-none',
|
|
273
|
+
!day.isDisabled && !day.isSelected && !isEndpoint && 'hover:bg-accent hover:text-accent-foreground cursor-pointer',
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Private helpers
|
|
278
|
+
private navigateDays(offset: number): void {
|
|
279
|
+
const current = this.value() ?? new Date();
|
|
280
|
+
const next = new Date(current);
|
|
281
|
+
next.setDate(next.getDate() + offset);
|
|
282
|
+
this.value.set(next);
|
|
283
|
+
this._onChange(next);
|
|
284
|
+
this.viewDate.set(new Date(next.getFullYear(), next.getMonth(), 1));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private createDay(
|
|
288
|
+
date: Date,
|
|
289
|
+
isCurrentMonth: boolean,
|
|
290
|
+
today: Date,
|
|
291
|
+
selected: Date | null,
|
|
292
|
+
rangeVal: DateRange | null,
|
|
293
|
+
hoveredDate: Date | null,
|
|
294
|
+
minDate: Date | undefined,
|
|
295
|
+
maxDate: Date | undefined,
|
|
296
|
+
): CalendarDay {
|
|
297
|
+
const isToday = this.isSameDay(date, today);
|
|
298
|
+
const isSelected = selected ? this.isSameDay(date, selected) : false;
|
|
299
|
+
const isDisabled =
|
|
300
|
+
this._disabledByCva() ||
|
|
301
|
+
(minDate ? date < minDate : false) ||
|
|
302
|
+
(maxDate ? date > maxDate : false);
|
|
303
|
+
|
|
304
|
+
let isRangeStart = false;
|
|
305
|
+
let isRangeEnd = false;
|
|
306
|
+
let isInRange = false;
|
|
307
|
+
let isRangePreview = false;
|
|
308
|
+
|
|
309
|
+
if (rangeVal) {
|
|
310
|
+
const { start, end } = rangeVal;
|
|
311
|
+
if (start) isRangeStart = this.isSameDay(date, start);
|
|
312
|
+
if (end) isRangeEnd = this.isSameDay(date, end);
|
|
313
|
+
if (start && end) {
|
|
314
|
+
isInRange = date > start && date < end && !isRangeStart && !isRangeEnd;
|
|
315
|
+
}
|
|
316
|
+
// Preview: start set, no end yet, user hovering
|
|
317
|
+
if (start && !end && hoveredDate && !this.isSameDay(hoveredDate, start)) {
|
|
318
|
+
const previewStart = hoveredDate > start ? start : hoveredDate;
|
|
319
|
+
const previewEnd = hoveredDate > start ? hoveredDate : start;
|
|
320
|
+
if (date > previewStart && date < previewEnd) {
|
|
321
|
+
isRangePreview = true;
|
|
322
|
+
}
|
|
323
|
+
if (this.isSameDay(date, hoveredDate) && !isRangeStart) {
|
|
324
|
+
isRangePreview = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
date, day: date.getDate(), isCurrentMonth, isToday, isSelected, isDisabled,
|
|
331
|
+
isRangeStart, isRangeEnd, isInRange, isRangePreview,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private isSameDay(a: Date, b: Date): boolean {
|
|
336
|
+
return (
|
|
337
|
+
a.getFullYear() === b.getFullYear() &&
|
|
338
|
+
a.getMonth() === b.getMonth() &&
|
|
339
|
+
a.getDate() === b.getDate()
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface DateRange {
|
|
2
|
+
start: Date | null;
|
|
3
|
+
end: Date | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CalendarDay {
|
|
7
|
+
date: Date;
|
|
8
|
+
day: number;
|
|
9
|
+
isCurrentMonth: boolean;
|
|
10
|
+
isToday: boolean;
|
|
11
|
+
isSelected: boolean;
|
|
12
|
+
isDisabled: boolean;
|
|
13
|
+
isRangeStart: boolean;
|
|
14
|
+
isRangeEnd: boolean;
|
|
15
|
+
isInRange: boolean;
|
|
16
|
+
isRangePreview: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CalendarMode = 'single' | 'range';
|
|
20
|
+
|
|
21
|
+
export interface DatePickerPreset {
|
|
22
|
+
label: string;
|
|
23
|
+
range: DateRange;
|
|
24
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import {
|
|
4
|
+
SnyCardDirective,
|
|
5
|
+
SnyCardHeaderDirective,
|
|
6
|
+
SnyCardTitleDirective,
|
|
7
|
+
SnyCardDescriptionDirective,
|
|
8
|
+
SnyCardContentDirective,
|
|
9
|
+
SnyCardFooterDirective,
|
|
10
|
+
} from './card.directives';
|
|
11
|
+
import type { CardVariant, CardPadding } from './card.variants';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
standalone: true,
|
|
15
|
+
imports: [
|
|
16
|
+
SnyCardDirective,
|
|
17
|
+
SnyCardHeaderDirective,
|
|
18
|
+
SnyCardTitleDirective,
|
|
19
|
+
SnyCardDescriptionDirective,
|
|
20
|
+
SnyCardContentDirective,
|
|
21
|
+
SnyCardFooterDirective,
|
|
22
|
+
],
|
|
23
|
+
template: `
|
|
24
|
+
<div snyCard [variant]="variant()" [padding]="padding()">
|
|
25
|
+
<div snyCardHeader>
|
|
26
|
+
<h3 snyCardTitle>Title</h3>
|
|
27
|
+
<p snyCardDescription>Description</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div snyCardContent>Content</div>
|
|
30
|
+
<div snyCardFooter>Footer</div>
|
|
31
|
+
</div>
|
|
32
|
+
`,
|
|
33
|
+
})
|
|
34
|
+
class TestHostComponent {
|
|
35
|
+
variant = signal<CardVariant>('default');
|
|
36
|
+
padding = signal<CardPadding>('none');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('Card Directives', () => {
|
|
40
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
await TestBed.configureTestingModule({
|
|
44
|
+
imports: [TestHostComponent],
|
|
45
|
+
}).compileComponents();
|
|
46
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
47
|
+
fixture.detectChanges();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should apply default card classes', () => {
|
|
51
|
+
const card = fixture.nativeElement.querySelector('[snyCard]');
|
|
52
|
+
expect(card.className).toContain('bg-card');
|
|
53
|
+
expect(card.className).toContain('border');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should apply elevated variant', () => {
|
|
57
|
+
fixture.componentInstance.variant.set('elevated');
|
|
58
|
+
fixture.detectChanges();
|
|
59
|
+
const card = fixture.nativeElement.querySelector('[snyCard]');
|
|
60
|
+
expect(card.className).toContain('shadow-lg');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should apply ghost variant', () => {
|
|
64
|
+
fixture.componentInstance.variant.set('ghost');
|
|
65
|
+
fixture.detectChanges();
|
|
66
|
+
const card = fixture.nativeElement.querySelector('[snyCard]');
|
|
67
|
+
expect(card.className).toContain('bg-transparent');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should apply padding', () => {
|
|
71
|
+
fixture.componentInstance.padding.set('md');
|
|
72
|
+
fixture.detectChanges();
|
|
73
|
+
const card = fixture.nativeElement.querySelector('[snyCard]');
|
|
74
|
+
expect(card.className).toContain('p-6');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should render card header', () => {
|
|
78
|
+
const header = fixture.nativeElement.querySelector('[snyCardHeader]');
|
|
79
|
+
expect(header.className).toContain('flex');
|
|
80
|
+
expect(header.className).toContain('p-6');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should render card title', () => {
|
|
84
|
+
const title = fixture.nativeElement.querySelector('[snyCardTitle]');
|
|
85
|
+
expect(title.className).toContain('text-2xl');
|
|
86
|
+
expect(title.className).toContain('font-semibold');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should render card description', () => {
|
|
90
|
+
const desc = fixture.nativeElement.querySelector('[snyCardDescription]');
|
|
91
|
+
expect(desc.className).toContain('text-muted-foreground');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should render content projection', () => {
|
|
95
|
+
const content = fixture.nativeElement.querySelector('[snyCardContent]');
|
|
96
|
+
expect(content.textContent).toContain('Content');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should render card footer', () => {
|
|
100
|
+
const footer = fixture.nativeElement.querySelector('[snyCardFooter]');
|
|
101
|
+
expect(footer.className).toContain('flex');
|
|
102
|
+
expect(footer.className).toContain('items-center');
|
|
103
|
+
});
|
|
104
|
+
});
|