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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +187 -40
  2. package/fesm2022/sonny-ui-core.mjs +6642 -268
  3. package/fesm2022/sonny-ui-core.mjs.map +1 -1
  4. package/package.json +8 -5
  5. package/schematics/ng-add/index.js +27 -0
  6. package/schematics/ng-add/schema.json +1 -1
  7. package/schematics/ng-generate/component/index.js +182 -1
  8. package/schematics/ng-generate/component/schema.json +2 -2
  9. package/src/lib/accordion/accordion.directives.spec.ts +173 -0
  10. package/src/lib/accordion/accordion.directives.ts +143 -0
  11. package/src/lib/accordion/index.ts +8 -0
  12. package/src/lib/alert/alert.directives.spec.ts +154 -0
  13. package/src/lib/alert/alert.directives.ts +67 -0
  14. package/src/lib/alert/alert.variants.ts +25 -0
  15. package/src/lib/alert/index.ts +6 -0
  16. package/src/lib/avatar/avatar.component.spec.ts +75 -0
  17. package/src/lib/avatar/avatar.component.ts +43 -0
  18. package/src/lib/avatar/avatar.variants.ts +26 -0
  19. package/src/lib/avatar/index.ts +2 -0
  20. package/src/lib/avatar-group/avatar-group.component.spec.ts +74 -0
  21. package/src/lib/avatar-group/avatar-group.component.ts +88 -0
  22. package/src/lib/avatar-group/index.ts +1 -0
  23. package/src/lib/badge/badge.directive.spec.ts +74 -0
  24. package/src/lib/badge/badge.directive.ts +17 -0
  25. package/src/lib/badge/badge.variants.ts +29 -0
  26. package/src/lib/badge/index.ts +2 -0
  27. package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
  28. package/src/lib/breadcrumb/breadcrumb.directives.ts +78 -0
  29. package/src/lib/breadcrumb/index.ts +8 -0
  30. package/src/lib/button/button.directive.spec.ts +92 -0
  31. package/src/lib/button/button.directive.ts +28 -0
  32. package/src/lib/button/button.variants.ts +30 -0
  33. package/src/lib/button/index.ts +2 -0
  34. package/src/lib/button-group/button-group.directive.spec.ts +46 -0
  35. package/src/lib/button-group/button-group.directive.ts +19 -0
  36. package/src/lib/button-group/button-group.variants.ts +18 -0
  37. package/src/lib/button-group/index.ts +2 -0
  38. package/src/lib/calendar/calendar.component.spec.ts +192 -0
  39. package/src/lib/calendar/calendar.component.ts +342 -0
  40. package/src/lib/calendar/calendar.types.ts +24 -0
  41. package/src/lib/calendar/index.ts +7 -0
  42. package/src/lib/card/card.directives.spec.ts +104 -0
  43. package/src/lib/card/card.directives.ts +72 -0
  44. package/src/lib/card/card.variants.ts +28 -0
  45. package/src/lib/card/index.ts +9 -0
  46. package/src/lib/carousel/carousel.directives.spec.ts +85 -0
  47. package/src/lib/carousel/carousel.directives.ts +159 -0
  48. package/src/lib/carousel/index.ts +8 -0
  49. package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
  50. package/src/lib/chat-bubble/chat-bubble.directives.ts +96 -0
  51. package/src/lib/chat-bubble/index.ts +11 -0
  52. package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
  53. package/src/lib/checkbox/checkbox.directive.ts +16 -0
  54. package/src/lib/checkbox/checkbox.variants.ts +19 -0
  55. package/src/lib/checkbox/index.ts +2 -0
  56. package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
  57. package/src/lib/color-picker/color-picker.component.ts +537 -0
  58. package/src/lib/color-picker/color-picker.types.ts +24 -0
  59. package/src/lib/color-picker/color-picker.utils.ts +183 -0
  60. package/src/lib/color-picker/color-picker.variants.ts +17 -0
  61. package/src/lib/color-picker/index.ts +20 -0
  62. package/src/lib/combobox/combobox.component.spec.ts +151 -0
  63. package/src/lib/combobox/combobox.component.ts +264 -0
  64. package/src/lib/combobox/combobox.variants.ts +19 -0
  65. package/src/lib/combobox/index.ts +2 -0
  66. package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
  67. package/src/lib/command-palette/command-palette.component.ts +194 -0
  68. package/src/lib/command-palette/command-palette.service.ts +36 -0
  69. package/src/lib/command-palette/command-palette.types.ts +23 -0
  70. package/src/lib/command-palette/index.ts +7 -0
  71. package/src/lib/data-table/data-table.component.spec.ts +443 -0
  72. package/src/lib/data-table/data-table.component.ts +602 -0
  73. package/src/lib/data-table/data-table.directives.ts +31 -0
  74. package/src/lib/data-table/data-table.types.ts +20 -0
  75. package/src/lib/data-table/index.ts +13 -0
  76. package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
  77. package/src/lib/date-picker/date-picker.component.ts +220 -0
  78. package/src/lib/date-picker/date-picker.variants.ts +17 -0
  79. package/src/lib/date-picker/index.ts +2 -0
  80. package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
  81. package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
  82. package/src/lib/date-range-picker/index.ts +1 -0
  83. package/src/lib/diff/diff.component.spec.ts +47 -0
  84. package/src/lib/diff/diff.component.ts +82 -0
  85. package/src/lib/diff/index.ts +1 -0
  86. package/src/lib/divider/divider.component.spec.ts +48 -0
  87. package/src/lib/divider/divider.component.ts +51 -0
  88. package/src/lib/divider/divider.variants.ts +22 -0
  89. package/src/lib/divider/index.ts +2 -0
  90. package/src/lib/dock/dock.directives.spec.ts +85 -0
  91. package/src/lib/dock/dock.directives.ts +81 -0
  92. package/src/lib/dock/index.ts +1 -0
  93. package/src/lib/drawer/drawer.directives.spec.ts +62 -0
  94. package/src/lib/drawer/drawer.directives.ts +80 -0
  95. package/src/lib/drawer/index.ts +8 -0
  96. package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
  97. package/src/lib/dropdown/dropdown.directives.ts +136 -0
  98. package/src/lib/dropdown/dropdown.variants.ts +27 -0
  99. package/src/lib/dropdown/index.ts +15 -0
  100. package/src/lib/fab/fab.directives.spec.ts +60 -0
  101. package/src/lib/fab/fab.directives.ts +77 -0
  102. package/src/lib/fab/index.ts +8 -0
  103. package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
  104. package/src/lib/fieldset/fieldset.directives.ts +49 -0
  105. package/src/lib/fieldset/fieldset.variants.ts +15 -0
  106. package/src/lib/fieldset/index.ts +6 -0
  107. package/src/lib/file-input/file-input.component.spec.ts +114 -0
  108. package/src/lib/file-input/file-input.component.ts +155 -0
  109. package/src/lib/file-input/file-input.variants.ts +25 -0
  110. package/src/lib/file-input/index.ts +6 -0
  111. package/src/lib/indicator/index.ts +6 -0
  112. package/src/lib/indicator/indicator.directives.spec.ts +64 -0
  113. package/src/lib/indicator/indicator.directives.ts +59 -0
  114. package/src/lib/input/index.ts +3 -0
  115. package/src/lib/input/input.directive.spec.ts +103 -0
  116. package/src/lib/input/input.directive.ts +25 -0
  117. package/src/lib/input/input.variants.ts +42 -0
  118. package/src/lib/input/label.directive.ts +16 -0
  119. package/src/lib/kbd/index.ts +2 -0
  120. package/src/lib/kbd/kbd.directive.spec.ts +42 -0
  121. package/src/lib/kbd/kbd.directive.ts +18 -0
  122. package/src/lib/kbd/kbd.variants.ts +19 -0
  123. package/src/lib/link/index.ts +2 -0
  124. package/src/lib/link/link.directive.spec.ts +41 -0
  125. package/src/lib/link/link.directive.ts +18 -0
  126. package/src/lib/link/link.variants.ts +20 -0
  127. package/src/lib/list/index.ts +8 -0
  128. package/src/lib/list/list.directives.spec.ts +65 -0
  129. package/src/lib/list/list.directives.ts +81 -0
  130. package/src/lib/loader/index.ts +2 -0
  131. package/src/lib/loader/loader.component.spec.ts +58 -0
  132. package/src/lib/loader/loader.component.ts +47 -0
  133. package/src/lib/loader/loader.variants.ts +21 -0
  134. package/src/lib/modal/dialog-ref.ts +19 -0
  135. package/src/lib/modal/dialog.directives.ts +84 -0
  136. package/src/lib/modal/dialog.service.spec.ts +52 -0
  137. package/src/lib/modal/dialog.service.ts +61 -0
  138. package/src/lib/modal/dialog.types.ts +16 -0
  139. package/src/lib/modal/index.ts +11 -0
  140. package/src/lib/navbar/index.ts +7 -0
  141. package/src/lib/navbar/navbar.directives.spec.ts +59 -0
  142. package/src/lib/navbar/navbar.directives.ts +57 -0
  143. package/src/lib/number-input/index.ts +2 -0
  144. package/src/lib/number-input/number-input.component.spec.ts +151 -0
  145. package/src/lib/number-input/number-input.component.ts +152 -0
  146. package/src/lib/number-input/number-input.variants.ts +17 -0
  147. package/src/lib/otp-input/index.ts +2 -0
  148. package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
  149. package/src/lib/otp-input/otp-input.component.ts +274 -0
  150. package/src/lib/otp-input/otp-input.variants.ts +18 -0
  151. package/src/lib/pagination/index.ts +6 -0
  152. package/src/lib/pagination/pagination.component.spec.ts +59 -0
  153. package/src/lib/pagination/pagination.component.ts +143 -0
  154. package/src/lib/pagination/pagination.variants.ts +31 -0
  155. package/src/lib/popover/index.ts +6 -0
  156. package/src/lib/popover/popover.directives.spec.ts +147 -0
  157. package/src/lib/popover/popover.directives.ts +151 -0
  158. package/src/lib/progress/index.ts +7 -0
  159. package/src/lib/progress/progress.component.spec.ts +117 -0
  160. package/src/lib/progress/progress.component.ts +64 -0
  161. package/src/lib/progress/progress.variants.ts +43 -0
  162. package/src/lib/radial-progress/index.ts +5 -0
  163. package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
  164. package/src/lib/radial-progress/radial-progress.component.ts +70 -0
  165. package/src/lib/radio/index.ts +2 -0
  166. package/src/lib/radio/radio.directive.spec.ts +46 -0
  167. package/src/lib/radio/radio.directive.ts +16 -0
  168. package/src/lib/radio/radio.variants.ts +19 -0
  169. package/src/lib/rating/index.ts +2 -0
  170. package/src/lib/rating/rating.component.spec.ts +157 -0
  171. package/src/lib/rating/rating.component.ts +163 -0
  172. package/src/lib/rating/rating.variants.ts +20 -0
  173. package/src/lib/select/index.ts +2 -0
  174. package/src/lib/select/select.component.spec.ts +112 -0
  175. package/src/lib/select/select.component.ts +235 -0
  176. package/src/lib/select/select.variants.ts +19 -0
  177. package/src/lib/sheet/index.ts +10 -0
  178. package/src/lib/sheet/sheet-ref.ts +18 -0
  179. package/src/lib/sheet/sheet.component.spec.ts +67 -0
  180. package/src/lib/sheet/sheet.directives.ts +70 -0
  181. package/src/lib/sheet/sheet.service.ts +100 -0
  182. package/src/lib/sheet/sheet.types.ts +23 -0
  183. package/src/lib/skeleton/index.ts +2 -0
  184. package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
  185. package/src/lib/skeleton/skeleton.directive.ts +21 -0
  186. package/src/lib/skeleton/skeleton.variants.ts +27 -0
  187. package/src/lib/slider/index.ts +2 -0
  188. package/src/lib/slider/slider.component.spec.ts +104 -0
  189. package/src/lib/slider/slider.component.ts +181 -0
  190. package/src/lib/slider/slider.variants.ts +25 -0
  191. package/src/lib/stat/index.ts +8 -0
  192. package/src/lib/stat/stat.directives.spec.ts +60 -0
  193. package/src/lib/stat/stat.directives.ts +79 -0
  194. package/src/lib/status/index.ts +2 -0
  195. package/src/lib/status/status.directive.spec.ts +43 -0
  196. package/src/lib/status/status.directive.ts +37 -0
  197. package/src/lib/status/status.variants.ts +26 -0
  198. package/src/lib/steps/index.ts +8 -0
  199. package/src/lib/steps/steps.directives.spec.ts +52 -0
  200. package/src/lib/steps/steps.directives.ts +78 -0
  201. package/src/lib/switch/index.ts +2 -0
  202. package/src/lib/switch/switch.component.spec.ts +98 -0
  203. package/src/lib/switch/switch.component.ts +76 -0
  204. package/src/lib/switch/switch.variants.ts +31 -0
  205. package/src/lib/table/index.ts +12 -0
  206. package/src/lib/table/table.directives.spec.ts +111 -0
  207. package/src/lib/table/table.directives.ts +126 -0
  208. package/src/lib/table/table.variants.ts +36 -0
  209. package/src/lib/tabs/index.ts +8 -0
  210. package/src/lib/tabs/tabs.directives.spec.ts +136 -0
  211. package/src/lib/tabs/tabs.directives.ts +126 -0
  212. package/src/lib/tabs/tabs.variants.ts +17 -0
  213. package/src/lib/tag-input/index.ts +2 -0
  214. package/src/lib/tag-input/tag-input.component.spec.ts +190 -0
  215. package/src/lib/tag-input/tag-input.component.ts +172 -0
  216. package/src/lib/tag-input/tag-input.variants.ts +31 -0
  217. package/src/lib/textarea/index.ts +7 -0
  218. package/src/lib/textarea/textarea.directive.spec.ts +84 -0
  219. package/src/lib/textarea/textarea.directive.ts +71 -0
  220. package/src/lib/textarea/textarea.variants.ts +34 -0
  221. package/src/lib/timeline/index.ts +11 -0
  222. package/src/lib/timeline/timeline.directives.spec.ts +55 -0
  223. package/src/lib/timeline/timeline.directives.ts +85 -0
  224. package/src/lib/toast/index.ts +3 -0
  225. package/src/lib/toast/toast.service.spec.ts +71 -0
  226. package/src/lib/toast/toast.service.ts +60 -0
  227. package/src/lib/toast/toast.variants.ts +38 -0
  228. package/src/lib/toast/toaster.component.spec.ts +38 -0
  229. package/src/lib/toast/toaster.component.ts +81 -0
  230. package/src/lib/toggle/index.ts +2 -0
  231. package/src/lib/toggle/toggle.directive.spec.ts +100 -0
  232. package/src/lib/toggle/toggle.directive.ts +61 -0
  233. package/src/lib/toggle/toggle.variants.ts +25 -0
  234. package/src/lib/tooltip/index.ts +2 -0
  235. package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
  236. package/src/lib/tooltip/tooltip.directive.ts +130 -0
  237. package/src/lib/tooltip/tooltip.variants.ts +20 -0
  238. package/src/lib/validator/index.ts +5 -0
  239. package/src/lib/validator/validator.directives.spec.ts +47 -0
  240. package/src/lib/validator/validator.directives.ts +50 -0
  241. package/src/styles/sonny-theme.css +33 -0
  242. package/types/sonny-ui-core.d.ts +1443 -13
@@ -0,0 +1,340 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ ElementRef,
6
+ forwardRef,
7
+ inject,
8
+ input,
9
+ model,
10
+ OnDestroy,
11
+ signal,
12
+ viewChild,
13
+ } from '@angular/core';
14
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
15
+ import { cn } from '../core/utils/cn';
16
+ import { SnyCalendarComponent } from '../calendar/calendar.component';
17
+ import type { DateRange, DatePickerPreset } from '../calendar/calendar.types';
18
+ import { datePickerTriggerVariants, type DatePickerSize } from '../date-picker/date-picker.variants';
19
+
20
+ @Component({
21
+ selector: 'sny-date-range-picker',
22
+ changeDetection: ChangeDetectionStrategy.OnPush,
23
+ imports: [SnyCalendarComponent],
24
+ host: {
25
+ class: 'relative inline-block w-full',
26
+ '(document:click)': 'onDocumentClick($event)',
27
+ '(keydown.escape)': 'onEscape()',
28
+ },
29
+ providers: [
30
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SnyDateRangePickerComponent), multi: true },
31
+ ],
32
+ template: `
33
+ <button
34
+ #triggerEl
35
+ type="button"
36
+ role="combobox"
37
+ [attr.aria-expanded]="open()"
38
+ aria-haspopup="dialog"
39
+ [disabled]="isDisabled()"
40
+ [class]="triggerClass()"
41
+ (click)="toggle()"
42
+ (blur)="onTouched()"
43
+ >
44
+ <span [class]="displayValue() ? 'truncate' : 'text-muted-foreground truncate'">
45
+ {{ displayValue() || placeholder() }}
46
+ </span>
47
+ <div class="flex items-center gap-1 shrink-0">
48
+ @if (clearable() && value()?.start) {
49
+ <button
50
+ type="button"
51
+ class="rounded-sm p-0.5 hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
52
+ (click)="clear($event)"
53
+ aria-label="Clear date range"
54
+ >
55
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
56
+ </button>
57
+ }
58
+ <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" class="shrink-0 text-muted-foreground"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></svg>
59
+ </div>
60
+ </button>
61
+
62
+ @if (open()) {
63
+ <div
64
+ #dropdownEl
65
+ role="dialog"
66
+ aria-modal="true"
67
+ aria-label="Choose date range"
68
+ class="fixed z-50 rounded-md border border-border bg-popover text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95"
69
+ >
70
+ <div class="flex flex-col sm:flex-row">
71
+ <!-- Presets sidebar -->
72
+ @if (presets().length > 0) {
73
+ <div class="border-b sm:border-b-0 sm:border-r border-border p-3 space-y-0.5 sm:min-w-[150px]">
74
+ <p class="px-3 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">Presets</p>
75
+ @for (preset of presets(); track preset.label) {
76
+ <button
77
+ type="button"
78
+ class="w-full text-left px-3 py-2 text-sm rounded-md hover:bg-accent hover:text-accent-foreground transition-colors cursor-pointer"
79
+ (mousedown)="selectPreset(preset); $event.preventDefault()"
80
+ >
81
+ {{ preset.label }}
82
+ </button>
83
+ }
84
+ </div>
85
+ }
86
+
87
+ <!-- Calendar(s) -->
88
+ <div class="flex flex-col sm:flex-row">
89
+ @if (dualCalendar()) {
90
+ <!-- Left calendar -->
91
+ <div class="p-1">
92
+ <div class="flex items-center justify-between px-3 py-2">
93
+ <button
94
+ type="button"
95
+ class="inline-flex items-center justify-center rounded-md h-8 w-8 hover:bg-accent hover:text-accent-foreground transition-colors"
96
+ (click)="prevMonth()"
97
+ aria-label="Previous month"
98
+ >
99
+ <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>
100
+ </button>
101
+ <span class="text-sm font-semibold tracking-tight">{{ leftMonthLabel() }}</span>
102
+ <div class="w-8"></div>
103
+ </div>
104
+ <sny-calendar
105
+ mode="range"
106
+ [(rangeValue)]="internalRange"
107
+ [min]="min()"
108
+ [max]="max()"
109
+ [locale]="locale()"
110
+ [showNavigation]="false"
111
+ [borderless]="true"
112
+ [initialViewDate]="leftViewDate()"
113
+ (rangeValueChange)="onRangeChanged($event)"
114
+ />
115
+ </div>
116
+ <div class="border-t sm:border-t-0 sm:border-l border-border"></div>
117
+ <!-- Right calendar -->
118
+ <div class="p-1">
119
+ <div class="flex items-center justify-between px-3 py-2">
120
+ <div class="w-8"></div>
121
+ <span class="text-sm font-semibold tracking-tight">{{ rightMonthLabel() }}</span>
122
+ <button
123
+ type="button"
124
+ class="inline-flex items-center justify-center rounded-md h-8 w-8 hover:bg-accent hover:text-accent-foreground transition-colors"
125
+ (click)="nextMonth()"
126
+ aria-label="Next month"
127
+ >
128
+ <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>
129
+ </button>
130
+ </div>
131
+ <sny-calendar
132
+ mode="range"
133
+ [(rangeValue)]="internalRange"
134
+ [min]="min()"
135
+ [max]="max()"
136
+ [locale]="locale()"
137
+ [showNavigation]="false"
138
+ [borderless]="true"
139
+ [initialViewDate]="rightViewDate()"
140
+ (rangeValueChange)="onRangeChanged($event)"
141
+ />
142
+ </div>
143
+ } @else {
144
+ <!-- Single calendar -->
145
+ <sny-calendar
146
+ mode="range"
147
+ [(rangeValue)]="internalRange"
148
+ [min]="min()"
149
+ [max]="max()"
150
+ [locale]="locale()"
151
+ (rangeValueChange)="onRangeChanged($event)"
152
+ />
153
+ }
154
+ </div>
155
+ </div>
156
+ </div>
157
+ }
158
+ `,
159
+ })
160
+ export class SnyDateRangePickerComponent implements ControlValueAccessor, OnDestroy {
161
+ readonly value = model<DateRange | null>(null);
162
+ readonly placeholder = input('Pick a date range...');
163
+ readonly size = input<DatePickerSize>('md');
164
+ readonly locale = input('en-US');
165
+ readonly dateFormat = input<Intl.DateTimeFormatOptions>({
166
+ month: 'short',
167
+ day: 'numeric',
168
+ year: 'numeric',
169
+ });
170
+ readonly separator = input(' \u2014 ');
171
+ readonly dualCalendar = input(false);
172
+ readonly presets = input<DatePickerPreset[]>([]);
173
+ readonly min = input<Date | undefined>(undefined);
174
+ readonly max = input<Date | undefined>(undefined);
175
+ readonly clearable = input(true);
176
+ readonly disabled = input(false);
177
+ readonly class = input<string>('');
178
+
179
+ readonly open = signal(false);
180
+ readonly internalRange = signal<DateRange | null>(null);
181
+ readonly leftViewDate = signal(new Date());
182
+
183
+ private readonly _disabledByCva = signal(false);
184
+ protected readonly isDisabled = computed(() => this.disabled() || this._disabledByCva());
185
+
186
+ private readonly triggerRef = viewChild<ElementRef<HTMLButtonElement>>('triggerEl');
187
+ private readonly dropdownRef = viewChild<ElementRef<HTMLDivElement>>('dropdownEl');
188
+ private readonly elRef = inject(ElementRef);
189
+
190
+ private scrollHandler: (() => void) | null = null;
191
+ private resizeHandler: (() => void) | null = null;
192
+
193
+ private _onChange: (value: DateRange | null) => void = () => {};
194
+ protected onTouched: () => void = () => {};
195
+
196
+ // Computed
197
+ readonly rightViewDate = computed(() => {
198
+ const d = this.leftViewDate();
199
+ return new Date(d.getFullYear(), d.getMonth() + 1, 1);
200
+ });
201
+
202
+ readonly leftMonthLabel = computed(() =>
203
+ this.leftViewDate().toLocaleDateString(this.locale(), { month: 'long', year: 'numeric' })
204
+ );
205
+
206
+ readonly rightMonthLabel = computed(() =>
207
+ this.rightViewDate().toLocaleDateString(this.locale(), { month: 'long', year: 'numeric' })
208
+ );
209
+
210
+ readonly displayValue = computed(() => {
211
+ const r = this.value();
212
+ if (!r?.start) return '';
213
+ const fmt = (d: Date) => d.toLocaleDateString(this.locale(), this.dateFormat());
214
+ if (!r.end) return fmt(r.start) + this.separator() + '...';
215
+ return fmt(r.start) + this.separator() + fmt(r.end);
216
+ });
217
+
218
+ protected readonly triggerClass = computed(() =>
219
+ cn(datePickerTriggerVariants({ size: this.size() }), this.class())
220
+ );
221
+
222
+ // CVA
223
+ writeValue(val: DateRange | null): void {
224
+ this.value.set(val ?? null);
225
+ this.internalRange.set(val ?? null);
226
+ if (val?.start) {
227
+ this.leftViewDate.set(new Date(val.start.getFullYear(), val.start.getMonth(), 1));
228
+ }
229
+ }
230
+
231
+ registerOnChange(fn: (value: DateRange | null) => void): void {
232
+ this._onChange = fn;
233
+ }
234
+
235
+ registerOnTouched(fn: () => void): void {
236
+ this.onTouched = fn;
237
+ }
238
+
239
+ setDisabledState(isDisabled: boolean): void {
240
+ this._disabledByCva.set(isDisabled);
241
+ }
242
+
243
+ // Actions
244
+ onRangeChanged(range: DateRange | null): void {
245
+ this.internalRange.set(range);
246
+ if (range?.start && range?.end) {
247
+ this.value.set(range);
248
+ this._onChange(range);
249
+ setTimeout(() => this.close(), 150);
250
+ }
251
+ }
252
+
253
+ selectPreset(preset: DatePickerPreset): void {
254
+ this.value.set(preset.range);
255
+ this.internalRange.set(preset.range);
256
+ this._onChange(preset.range);
257
+ this.close();
258
+ }
259
+
260
+ clear(event: Event): void {
261
+ event.stopPropagation();
262
+ this.value.set(null);
263
+ this.internalRange.set(null);
264
+ this._onChange(null);
265
+ }
266
+
267
+ prevMonth(): void {
268
+ this.leftViewDate.update((d) => new Date(d.getFullYear(), d.getMonth() - 1, 1));
269
+ }
270
+
271
+ nextMonth(): void {
272
+ this.leftViewDate.update((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1));
273
+ }
274
+
275
+ toggle(): void {
276
+ if (this.open()) {
277
+ this.close();
278
+ } else {
279
+ this.internalRange.set(this.value());
280
+ this.updateDropdownPosition();
281
+ this.open.set(true);
282
+ this.addGlobalListeners();
283
+ setTimeout(() => this.updateDropdownPosition());
284
+ }
285
+ }
286
+
287
+ close(): void {
288
+ this.open.set(false);
289
+ this.removeGlobalListeners();
290
+ }
291
+
292
+ // Positioning
293
+ private updateDropdownPosition(): void {
294
+ const trigger = this.triggerRef()?.nativeElement;
295
+ if (!trigger) return;
296
+ const rect = trigger.getBoundingClientRect();
297
+ const dropdown = this.dropdownRef()?.nativeElement;
298
+ if (dropdown) {
299
+ dropdown.style.top = `${rect.bottom + 4}px`;
300
+ dropdown.style.left = `${rect.left}px`;
301
+ }
302
+ }
303
+
304
+ private addGlobalListeners(): void {
305
+ this.removeGlobalListeners();
306
+ this.scrollHandler = () => {
307
+ requestAnimationFrame(() => this.updateDropdownPosition());
308
+ };
309
+ this.resizeHandler = () => {
310
+ requestAnimationFrame(() => this.updateDropdownPosition());
311
+ };
312
+ document.addEventListener('scroll', this.scrollHandler, { capture: true, passive: true });
313
+ window.addEventListener('resize', this.resizeHandler, { passive: true });
314
+ }
315
+
316
+ private removeGlobalListeners(): void {
317
+ if (this.scrollHandler) {
318
+ document.removeEventListener('scroll', this.scrollHandler, { capture: true } as EventListenerOptions);
319
+ this.scrollHandler = null;
320
+ }
321
+ if (this.resizeHandler) {
322
+ window.removeEventListener('resize', this.resizeHandler);
323
+ this.resizeHandler = null;
324
+ }
325
+ }
326
+
327
+ ngOnDestroy(): void {
328
+ this.removeGlobalListeners();
329
+ }
330
+
331
+ onDocumentClick(event: MouseEvent): void {
332
+ if (!this.elRef.nativeElement.contains(event.target)) {
333
+ this.close();
334
+ }
335
+ }
336
+
337
+ onEscape(): void {
338
+ this.close();
339
+ }
340
+ }
@@ -0,0 +1 @@
1
+ export { SnyDateRangePickerComponent } from './date-range-picker.component';
@@ -0,0 +1,47 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { SnyDiffComponent } from './diff.component';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ imports: [SnyDiffComponent],
8
+ template: `
9
+ <sny-diff [(value)]="value">
10
+ <div snyDiffBefore>Before</div>
11
+ <div snyDiffAfter>After</div>
12
+ </sny-diff>
13
+ `,
14
+ })
15
+ class TestHostComponent {
16
+ value = signal(50);
17
+ }
18
+
19
+ describe('SnyDiffComponent', () => {
20
+ let fixture: ComponentFixture<TestHostComponent>;
21
+ let host: HTMLElement;
22
+
23
+ beforeEach(async () => {
24
+ await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
25
+ fixture = TestBed.createComponent(TestHostComponent);
26
+ fixture.detectChanges();
27
+ host = fixture.nativeElement.querySelector('sny-diff');
28
+ });
29
+
30
+ it('should render slider handle', () => {
31
+ const slider = host.querySelector('[role="slider"]');
32
+ expect(slider).not.toBeNull();
33
+ expect(slider!.getAttribute('aria-valuenow')).toBe('50');
34
+ });
35
+
36
+ it('should handle keyboard ArrowLeft', () => {
37
+ host.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }));
38
+ fixture.detectChanges();
39
+ expect(fixture.componentInstance.value()).toBe(45);
40
+ });
41
+
42
+ it('should handle keyboard ArrowRight', () => {
43
+ host.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
44
+ fixture.detectChanges();
45
+ expect(fixture.componentInstance.value()).toBe(55);
46
+ });
47
+ });
@@ -0,0 +1,82 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, model, signal } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+
4
+ @Component({
5
+ selector: 'sny-diff',
6
+ changeDetection: ChangeDetectionStrategy.OnPush,
7
+ host: {
8
+ '[class]': '"relative overflow-hidden select-none w-full"',
9
+ '(pointerdown)': 'onPointerDown($event)',
10
+ '(pointermove)': 'onPointerMove($event)',
11
+ '(pointerup)': 'onPointerUp()',
12
+ '(keydown)': 'onKeydown($event)',
13
+ },
14
+ template: `
15
+ <div class="relative w-full" [style.aspect-ratio]="'16/9'">
16
+ <div class="absolute inset-0">
17
+ <ng-content select="[snyDiffAfter]" />
18
+ </div>
19
+ <div class="absolute inset-0 overflow-hidden" [style.width.%]="value()">
20
+ <ng-content select="[snyDiffBefore]" />
21
+ </div>
22
+ <div
23
+ class="absolute top-0 bottom-0 w-1 bg-foreground cursor-col-resize z-10"
24
+ [style.left.%]="value()"
25
+ role="slider"
26
+ tabindex="0"
27
+ [attr.aria-valuenow]="value()"
28
+ aria-valuemin="0"
29
+ aria-valuemax="100"
30
+ aria-label="Comparison slider"
31
+ >
32
+ <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-foreground/80 flex items-center justify-center text-background text-xs">
33
+
34
+ </div>
35
+ </div>
36
+ </div>
37
+ `,
38
+ })
39
+ export class SnyDiffComponent {
40
+ readonly value = model(50);
41
+ readonly orientation = input<'horizontal' | 'vertical'>('horizontal');
42
+ readonly class = input<string>('');
43
+
44
+ readonly isDragging = signal(false);
45
+
46
+ onPointerDown(event: PointerEvent): void {
47
+ this.isDragging.set(true);
48
+ (event.target as HTMLElement).setPointerCapture?.(event.pointerId);
49
+ this.updateValue(event);
50
+ }
51
+
52
+ onPointerMove(event: PointerEvent): void {
53
+ if (!this.isDragging()) return;
54
+ this.updateValue(event);
55
+ }
56
+
57
+ onPointerUp(): void {
58
+ this.isDragging.set(false);
59
+ }
60
+
61
+ onKeydown(event: KeyboardEvent): void {
62
+ switch (event.key) {
63
+ case 'ArrowLeft':
64
+ event.preventDefault();
65
+ this.value.update((v) => Math.max(0, v - 5));
66
+ break;
67
+ case 'ArrowRight':
68
+ event.preventDefault();
69
+ this.value.update((v) => Math.min(100, v + 5));
70
+ break;
71
+ }
72
+ }
73
+
74
+ private updateValue(event: PointerEvent): void {
75
+ const target = (event.currentTarget as HTMLElement).closest('sny-diff');
76
+ if (!target) return;
77
+ const rect = target.getBoundingClientRect();
78
+ const x = event.clientX - rect.left;
79
+ const pct = Math.max(0, Math.min(100, (x / rect.width) * 100));
80
+ this.value.set(Math.round(pct));
81
+ }
82
+ }
@@ -0,0 +1 @@
1
+ export { SnyDiffComponent } from './diff.component';
@@ -0,0 +1,48 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { SnyDividerComponent } from './divider.component';
4
+ import type { DividerOrientation } from './divider.variants';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [SnyDividerComponent],
9
+ template: `<sny-divider [orientation]="orientation()" [label]="label()" />`,
10
+ })
11
+ class TestHostComponent {
12
+ orientation = signal<DividerOrientation>('horizontal');
13
+ label = signal('');
14
+ }
15
+
16
+ describe('SnyDividerComponent', () => {
17
+ let fixture: ComponentFixture<TestHostComponent>;
18
+ let host: HTMLElement;
19
+
20
+ beforeEach(async () => {
21
+ await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
22
+ fixture = TestBed.createComponent(TestHostComponent);
23
+ fixture.detectChanges();
24
+ host = fixture.nativeElement.querySelector('sny-divider');
25
+ });
26
+
27
+ it('should have separator role', () => {
28
+ expect(host.getAttribute('role')).toBe('separator');
29
+ });
30
+
31
+ it('should set aria-orientation', () => {
32
+ expect(host.getAttribute('aria-orientation')).toBe('horizontal');
33
+ fixture.componentInstance.orientation.set('vertical');
34
+ fixture.detectChanges();
35
+ expect(host.getAttribute('aria-orientation')).toBe('vertical');
36
+ });
37
+
38
+ it('should render simple divider without label', () => {
39
+ const div = host.querySelector('div');
40
+ expect(div!.className).toContain('bg-border');
41
+ });
42
+
43
+ it('should render label when provided', () => {
44
+ fixture.componentInstance.label.set('OR');
45
+ fixture.detectChanges();
46
+ expect(host.textContent).toContain('OR');
47
+ });
48
+ });
@@ -0,0 +1,51 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import {
4
+ dividerVariants,
5
+ type DividerOrientation,
6
+ type DividerVariant,
7
+ } from './divider.variants';
8
+
9
+ @Component({
10
+ selector: 'sny-divider',
11
+ changeDetection: ChangeDetectionStrategy.OnPush,
12
+ host: {
13
+ 'role': 'separator',
14
+ '[attr.aria-orientation]': 'orientation()',
15
+ },
16
+ template: `
17
+ @if (hasLabel()) {
18
+ <div [class]="labelContainerClass()">
19
+ <div [class]="lineClass()"></div>
20
+ <span class="px-2 text-xs text-muted-foreground">{{ label() }}</span>
21
+ <div [class]="lineClass()"></div>
22
+ </div>
23
+ } @else {
24
+ <div [class]="dividerClass()"></div>
25
+ }
26
+ `,
27
+ })
28
+ export class SnyDividerComponent {
29
+ readonly orientation = input<DividerOrientation>('horizontal');
30
+ readonly variant = input<DividerVariant>('solid');
31
+ readonly label = input<string>('');
32
+ readonly class = input<string>('');
33
+
34
+ readonly hasLabel = computed(() => !!this.label());
35
+
36
+ protected readonly dividerClass = computed(() =>
37
+ cn(dividerVariants({ orientation: this.orientation(), variant: this.variant() }), this.class())
38
+ );
39
+
40
+ protected readonly lineClass = computed(() =>
41
+ cn('flex-1 bg-border', this.orientation() === 'horizontal' ? 'h-[1px]' : 'w-[1px]')
42
+ );
43
+
44
+ protected readonly labelContainerClass = computed(() =>
45
+ cn(
46
+ 'flex items-center',
47
+ this.orientation() === 'horizontal' ? 'flex-row' : 'flex-col',
48
+ this.class()
49
+ )
50
+ );
51
+ }
@@ -0,0 +1,22 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const dividerVariants = cva('shrink-0 bg-border', {
4
+ variants: {
5
+ orientation: {
6
+ horizontal: 'h-[1px] w-full',
7
+ vertical: 'h-full w-[1px]',
8
+ },
9
+ variant: {
10
+ solid: '',
11
+ dashed: 'border-dashed',
12
+ dotted: 'border-dotted',
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ orientation: 'horizontal',
17
+ variant: 'solid',
18
+ },
19
+ });
20
+
21
+ export type DividerOrientation = 'horizontal' | 'vertical';
22
+ export type DividerVariant = 'solid' | 'dashed' | 'dotted';
@@ -0,0 +1,2 @@
1
+ export { SnyDividerComponent } from './divider.component';
2
+ export { dividerVariants, type DividerOrientation, type DividerVariant } from './divider.variants';
@@ -0,0 +1,85 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { SnyDockDirective, SnyDockItemDirective, type DockPosition } from './dock.directives';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ imports: [SnyDockDirective, SnyDockItemDirective],
8
+ template: `
9
+ <div snyDock [position]="position()">
10
+ <button snyDockItem [active]="true">Home</button>
11
+ <button snyDockItem>Settings</button>
12
+ </div>
13
+ `,
14
+ })
15
+ class TestHostComponent {
16
+ position = signal<DockPosition>('bottom');
17
+ }
18
+
19
+ describe('SnyDockDirective', () => {
20
+ let fixture: ComponentFixture<TestHostComponent>;
21
+
22
+ beforeEach(async () => {
23
+ await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
24
+ fixture = TestBed.createComponent(TestHostComponent);
25
+ fixture.detectChanges();
26
+ });
27
+
28
+ it('should render with toolbar role', () => {
29
+ const dock = fixture.nativeElement.querySelector('[snyDock]');
30
+ expect(dock.getAttribute('role')).toBe('toolbar');
31
+ });
32
+
33
+ it('should position at bottom by default', () => {
34
+ const dock = fixture.nativeElement.querySelector('[snyDock]');
35
+ expect(dock.className).toContain('bottom-4');
36
+ });
37
+
38
+ it('should position at top', () => {
39
+ fixture.componentInstance.position.set('top');
40
+ fixture.detectChanges();
41
+ const dock = fixture.nativeElement.querySelector('[snyDock]');
42
+ expect(dock.className).toContain('top-4');
43
+ });
44
+
45
+ it('should apply active state to dock item', () => {
46
+ const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
47
+ expect(items[0].className).toContain('bg-primary');
48
+ });
49
+
50
+ it('should move focus with ArrowRight', () => {
51
+ const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
52
+ (items[0] as HTMLElement).focus();
53
+ items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
54
+ fixture.detectChanges();
55
+ const updated = fixture.nativeElement.querySelectorAll('[snyDockItem]');
56
+ expect(document.activeElement).toBe(updated[1]);
57
+ });
58
+
59
+ it('should move focus with ArrowLeft', () => {
60
+ const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
61
+ (items[1] as HTMLElement).focus();
62
+ items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
63
+ fixture.detectChanges();
64
+ const updated = fixture.nativeElement.querySelectorAll('[snyDockItem]');
65
+ expect(document.activeElement).toBe(updated[0]);
66
+ });
67
+
68
+ it('should move focus to first with Home', () => {
69
+ const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
70
+ (items[1] as HTMLElement).focus();
71
+ items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
72
+ fixture.detectChanges();
73
+ const updated = fixture.nativeElement.querySelectorAll('[snyDockItem]');
74
+ expect(document.activeElement).toBe(updated[0]);
75
+ });
76
+
77
+ it('should move focus to last with End', () => {
78
+ const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
79
+ (items[0] as HTMLElement).focus();
80
+ items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
81
+ fixture.detectChanges();
82
+ const updated = fixture.nativeElement.querySelectorAll('[snyDockItem]');
83
+ expect(document.activeElement).toBe(updated[1]);
84
+ });
85
+ });