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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +187 -40
  2. package/fesm2022/sonny-ui-core.mjs +6646 -272
  3. package/fesm2022/sonny-ui-core.mjs.map +1 -1
  4. package/package.json +8 -5
  5. package/schematics/ng-add/index.js +27 -0
  6. package/schematics/ng-add/schema.json +1 -1
  7. package/schematics/ng-generate/component/index.js +182 -1
  8. package/schematics/ng-generate/component/schema.json +2 -2
  9. package/src/lib/accordion/accordion.directives.spec.ts +173 -0
  10. package/src/lib/accordion/accordion.directives.ts +143 -0
  11. package/src/lib/accordion/index.ts +8 -0
  12. package/src/lib/alert/alert.directives.spec.ts +154 -0
  13. package/src/lib/alert/alert.directives.ts +67 -0
  14. package/src/lib/alert/alert.variants.ts +25 -0
  15. package/src/lib/alert/index.ts +6 -0
  16. package/src/lib/avatar/avatar.component.spec.ts +75 -0
  17. package/src/lib/avatar/avatar.component.ts +43 -0
  18. package/src/lib/avatar/avatar.variants.ts +26 -0
  19. package/src/lib/avatar/index.ts +2 -0
  20. package/src/lib/avatar-group/avatar-group.component.spec.ts +74 -0
  21. package/src/lib/avatar-group/avatar-group.component.ts +88 -0
  22. package/src/lib/avatar-group/index.ts +1 -0
  23. package/src/lib/badge/badge.directive.spec.ts +74 -0
  24. package/src/lib/badge/badge.directive.ts +17 -0
  25. package/src/lib/badge/badge.variants.ts +29 -0
  26. package/src/lib/badge/index.ts +2 -0
  27. package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
  28. package/src/lib/breadcrumb/breadcrumb.directives.ts +78 -0
  29. package/src/lib/breadcrumb/index.ts +8 -0
  30. package/src/lib/button/button.directive.spec.ts +92 -0
  31. package/src/lib/button/button.directive.ts +28 -0
  32. package/src/lib/button/button.variants.ts +30 -0
  33. package/src/lib/button/index.ts +2 -0
  34. package/src/lib/button-group/button-group.directive.spec.ts +46 -0
  35. package/src/lib/button-group/button-group.directive.ts +19 -0
  36. package/src/lib/button-group/button-group.variants.ts +18 -0
  37. package/src/lib/button-group/index.ts +2 -0
  38. package/src/lib/calendar/calendar.component.spec.ts +192 -0
  39. package/src/lib/calendar/calendar.component.ts +342 -0
  40. package/src/lib/calendar/calendar.types.ts +24 -0
  41. package/src/lib/calendar/index.ts +7 -0
  42. package/src/lib/card/card.directives.spec.ts +104 -0
  43. package/src/lib/card/card.directives.ts +72 -0
  44. package/src/lib/card/card.variants.ts +28 -0
  45. package/src/lib/card/index.ts +9 -0
  46. package/src/lib/carousel/carousel.directives.spec.ts +85 -0
  47. package/src/lib/carousel/carousel.directives.ts +159 -0
  48. package/src/lib/carousel/index.ts +8 -0
  49. package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
  50. package/src/lib/chat-bubble/chat-bubble.directives.ts +96 -0
  51. package/src/lib/chat-bubble/index.ts +11 -0
  52. package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
  53. package/src/lib/checkbox/checkbox.directive.ts +16 -0
  54. package/src/lib/checkbox/checkbox.variants.ts +19 -0
  55. package/src/lib/checkbox/index.ts +2 -0
  56. package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
  57. package/src/lib/color-picker/color-picker.component.ts +537 -0
  58. package/src/lib/color-picker/color-picker.types.ts +24 -0
  59. package/src/lib/color-picker/color-picker.utils.ts +183 -0
  60. package/src/lib/color-picker/color-picker.variants.ts +17 -0
  61. package/src/lib/color-picker/index.ts +20 -0
  62. package/src/lib/combobox/combobox.component.spec.ts +151 -0
  63. package/src/lib/combobox/combobox.component.ts +264 -0
  64. package/src/lib/combobox/combobox.variants.ts +19 -0
  65. package/src/lib/combobox/index.ts +2 -0
  66. package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
  67. package/src/lib/command-palette/command-palette.component.ts +194 -0
  68. package/src/lib/command-palette/command-palette.service.ts +36 -0
  69. package/src/lib/command-palette/command-palette.types.ts +23 -0
  70. package/src/lib/command-palette/index.ts +7 -0
  71. package/src/lib/data-table/data-table.component.spec.ts +443 -0
  72. package/src/lib/data-table/data-table.component.ts +602 -0
  73. package/src/lib/data-table/data-table.directives.ts +31 -0
  74. package/src/lib/data-table/data-table.types.ts +20 -0
  75. package/src/lib/data-table/index.ts +13 -0
  76. package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
  77. package/src/lib/date-picker/date-picker.component.ts +220 -0
  78. package/src/lib/date-picker/date-picker.variants.ts +17 -0
  79. package/src/lib/date-picker/index.ts +2 -0
  80. package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
  81. package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
  82. package/src/lib/date-range-picker/index.ts +1 -0
  83. package/src/lib/diff/diff.component.spec.ts +47 -0
  84. package/src/lib/diff/diff.component.ts +82 -0
  85. package/src/lib/diff/index.ts +1 -0
  86. package/src/lib/divider/divider.component.spec.ts +48 -0
  87. package/src/lib/divider/divider.component.ts +51 -0
  88. package/src/lib/divider/divider.variants.ts +22 -0
  89. package/src/lib/divider/index.ts +2 -0
  90. package/src/lib/dock/dock.directives.spec.ts +85 -0
  91. package/src/lib/dock/dock.directives.ts +81 -0
  92. package/src/lib/dock/index.ts +1 -0
  93. package/src/lib/drawer/drawer.directives.spec.ts +62 -0
  94. package/src/lib/drawer/drawer.directives.ts +80 -0
  95. package/src/lib/drawer/index.ts +8 -0
  96. package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
  97. package/src/lib/dropdown/dropdown.directives.ts +136 -0
  98. package/src/lib/dropdown/dropdown.variants.ts +27 -0
  99. package/src/lib/dropdown/index.ts +15 -0
  100. package/src/lib/fab/fab.directives.spec.ts +60 -0
  101. package/src/lib/fab/fab.directives.ts +77 -0
  102. package/src/lib/fab/index.ts +8 -0
  103. package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
  104. package/src/lib/fieldset/fieldset.directives.ts +49 -0
  105. package/src/lib/fieldset/fieldset.variants.ts +15 -0
  106. package/src/lib/fieldset/index.ts +6 -0
  107. package/src/lib/file-input/file-input.component.spec.ts +114 -0
  108. package/src/lib/file-input/file-input.component.ts +155 -0
  109. package/src/lib/file-input/file-input.variants.ts +25 -0
  110. package/src/lib/file-input/index.ts +6 -0
  111. package/src/lib/indicator/index.ts +6 -0
  112. package/src/lib/indicator/indicator.directives.spec.ts +64 -0
  113. package/src/lib/indicator/indicator.directives.ts +59 -0
  114. package/src/lib/input/index.ts +3 -0
  115. package/src/lib/input/input.directive.spec.ts +103 -0
  116. package/src/lib/input/input.directive.ts +25 -0
  117. package/src/lib/input/input.variants.ts +42 -0
  118. package/src/lib/input/label.directive.ts +16 -0
  119. package/src/lib/kbd/index.ts +2 -0
  120. package/src/lib/kbd/kbd.directive.spec.ts +42 -0
  121. package/src/lib/kbd/kbd.directive.ts +18 -0
  122. package/src/lib/kbd/kbd.variants.ts +19 -0
  123. package/src/lib/link/index.ts +2 -0
  124. package/src/lib/link/link.directive.spec.ts +41 -0
  125. package/src/lib/link/link.directive.ts +18 -0
  126. package/src/lib/link/link.variants.ts +20 -0
  127. package/src/lib/list/index.ts +8 -0
  128. package/src/lib/list/list.directives.spec.ts +65 -0
  129. package/src/lib/list/list.directives.ts +81 -0
  130. package/src/lib/loader/index.ts +2 -0
  131. package/src/lib/loader/loader.component.spec.ts +58 -0
  132. package/src/lib/loader/loader.component.ts +47 -0
  133. package/src/lib/loader/loader.variants.ts +21 -0
  134. package/src/lib/modal/dialog-ref.ts +19 -0
  135. package/src/lib/modal/dialog.directives.ts +84 -0
  136. package/src/lib/modal/dialog.service.spec.ts +52 -0
  137. package/src/lib/modal/dialog.service.ts +61 -0
  138. package/src/lib/modal/dialog.types.ts +16 -0
  139. package/src/lib/modal/index.ts +11 -0
  140. package/src/lib/navbar/index.ts +7 -0
  141. package/src/lib/navbar/navbar.directives.spec.ts +59 -0
  142. package/src/lib/navbar/navbar.directives.ts +57 -0
  143. package/src/lib/number-input/index.ts +2 -0
  144. package/src/lib/number-input/number-input.component.spec.ts +151 -0
  145. package/src/lib/number-input/number-input.component.ts +152 -0
  146. package/src/lib/number-input/number-input.variants.ts +17 -0
  147. package/src/lib/otp-input/index.ts +2 -0
  148. package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
  149. package/src/lib/otp-input/otp-input.component.ts +274 -0
  150. package/src/lib/otp-input/otp-input.variants.ts +18 -0
  151. package/src/lib/pagination/index.ts +6 -0
  152. package/src/lib/pagination/pagination.component.spec.ts +59 -0
  153. package/src/lib/pagination/pagination.component.ts +143 -0
  154. package/src/lib/pagination/pagination.variants.ts +31 -0
  155. package/src/lib/popover/index.ts +6 -0
  156. package/src/lib/popover/popover.directives.spec.ts +147 -0
  157. package/src/lib/popover/popover.directives.ts +151 -0
  158. package/src/lib/progress/index.ts +7 -0
  159. package/src/lib/progress/progress.component.spec.ts +117 -0
  160. package/src/lib/progress/progress.component.ts +64 -0
  161. package/src/lib/progress/progress.variants.ts +43 -0
  162. package/src/lib/radial-progress/index.ts +5 -0
  163. package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
  164. package/src/lib/radial-progress/radial-progress.component.ts +70 -0
  165. package/src/lib/radio/index.ts +2 -0
  166. package/src/lib/radio/radio.directive.spec.ts +46 -0
  167. package/src/lib/radio/radio.directive.ts +16 -0
  168. package/src/lib/radio/radio.variants.ts +19 -0
  169. package/src/lib/rating/index.ts +2 -0
  170. package/src/lib/rating/rating.component.spec.ts +157 -0
  171. package/src/lib/rating/rating.component.ts +163 -0
  172. package/src/lib/rating/rating.variants.ts +20 -0
  173. package/src/lib/select/index.ts +2 -0
  174. package/src/lib/select/select.component.spec.ts +112 -0
  175. package/src/lib/select/select.component.ts +235 -0
  176. package/src/lib/select/select.variants.ts +19 -0
  177. package/src/lib/sheet/index.ts +10 -0
  178. package/src/lib/sheet/sheet-ref.ts +18 -0
  179. package/src/lib/sheet/sheet.component.spec.ts +67 -0
  180. package/src/lib/sheet/sheet.directives.ts +70 -0
  181. package/src/lib/sheet/sheet.service.ts +100 -0
  182. package/src/lib/sheet/sheet.types.ts +23 -0
  183. package/src/lib/skeleton/index.ts +2 -0
  184. package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
  185. package/src/lib/skeleton/skeleton.directive.ts +21 -0
  186. package/src/lib/skeleton/skeleton.variants.ts +27 -0
  187. package/src/lib/slider/index.ts +2 -0
  188. package/src/lib/slider/slider.component.spec.ts +104 -0
  189. package/src/lib/slider/slider.component.ts +181 -0
  190. package/src/lib/slider/slider.variants.ts +25 -0
  191. package/src/lib/stat/index.ts +8 -0
  192. package/src/lib/stat/stat.directives.spec.ts +60 -0
  193. package/src/lib/stat/stat.directives.ts +79 -0
  194. package/src/lib/status/index.ts +2 -0
  195. package/src/lib/status/status.directive.spec.ts +43 -0
  196. package/src/lib/status/status.directive.ts +37 -0
  197. package/src/lib/status/status.variants.ts +26 -0
  198. package/src/lib/steps/index.ts +8 -0
  199. package/src/lib/steps/steps.directives.spec.ts +52 -0
  200. package/src/lib/steps/steps.directives.ts +78 -0
  201. package/src/lib/switch/index.ts +2 -0
  202. package/src/lib/switch/switch.component.spec.ts +98 -0
  203. package/src/lib/switch/switch.component.ts +76 -0
  204. package/src/lib/switch/switch.variants.ts +31 -0
  205. package/src/lib/table/index.ts +12 -0
  206. package/src/lib/table/table.directives.spec.ts +111 -0
  207. package/src/lib/table/table.directives.ts +126 -0
  208. package/src/lib/table/table.variants.ts +36 -0
  209. package/src/lib/tabs/index.ts +8 -0
  210. package/src/lib/tabs/tabs.directives.spec.ts +136 -0
  211. package/src/lib/tabs/tabs.directives.ts +126 -0
  212. package/src/lib/tabs/tabs.variants.ts +17 -0
  213. package/src/lib/tag-input/index.ts +2 -0
  214. package/src/lib/tag-input/tag-input.component.spec.ts +190 -0
  215. package/src/lib/tag-input/tag-input.component.ts +172 -0
  216. package/src/lib/tag-input/tag-input.variants.ts +31 -0
  217. package/src/lib/textarea/index.ts +7 -0
  218. package/src/lib/textarea/textarea.directive.spec.ts +84 -0
  219. package/src/lib/textarea/textarea.directive.ts +71 -0
  220. package/src/lib/textarea/textarea.variants.ts +34 -0
  221. package/src/lib/timeline/index.ts +11 -0
  222. package/src/lib/timeline/timeline.directives.spec.ts +55 -0
  223. package/src/lib/timeline/timeline.directives.ts +85 -0
  224. package/src/lib/toast/index.ts +3 -0
  225. package/src/lib/toast/toast.service.spec.ts +71 -0
  226. package/src/lib/toast/toast.service.ts +60 -0
  227. package/src/lib/toast/toast.variants.ts +38 -0
  228. package/src/lib/toast/toaster.component.spec.ts +38 -0
  229. package/src/lib/toast/toaster.component.ts +81 -0
  230. package/src/lib/toggle/index.ts +2 -0
  231. package/src/lib/toggle/toggle.directive.spec.ts +100 -0
  232. package/src/lib/toggle/toggle.directive.ts +61 -0
  233. package/src/lib/toggle/toggle.variants.ts +25 -0
  234. package/src/lib/tooltip/index.ts +2 -0
  235. package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
  236. package/src/lib/tooltip/tooltip.directive.ts +130 -0
  237. package/src/lib/tooltip/tooltip.variants.ts +20 -0
  238. package/src/lib/validator/index.ts +5 -0
  239. package/src/lib/validator/validator.directives.spec.ts +47 -0
  240. package/src/lib/validator/validator.directives.ts +50 -0
  241. package/src/styles/sonny-theme.css +45 -0
  242. package/types/sonny-ui-core.d.ts +1443 -13
@@ -0,0 +1,537 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ ElementRef,
7
+ forwardRef,
8
+ inject,
9
+ input,
10
+ model,
11
+ OnDestroy,
12
+ output,
13
+ signal,
14
+ untracked,
15
+ viewChild,
16
+ } from '@angular/core';
17
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
18
+ import { cn } from '../core/utils/cn';
19
+ import type { ColorFormat, ColorPickerPreset, HSV, RGB } from './color-picker.types';
20
+ import {
21
+ rgbToHex,
22
+ rgbToHsv,
23
+ hsvToRgb,
24
+ parseColor,
25
+ formatColor,
26
+ isValidColor,
27
+ } from './color-picker.utils';
28
+ import { colorPickerTriggerVariants, type ColorPickerSize } from './color-picker.variants';
29
+
30
+ @Component({
31
+ selector: 'sny-color-picker',
32
+ changeDetection: ChangeDetectionStrategy.OnPush,
33
+ host: {
34
+ class: 'relative inline-block',
35
+ '(document:click)': 'onDocumentClick($event)',
36
+ '(keydown.escape)': 'onEscape()',
37
+ },
38
+ providers: [
39
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SnyColorPickerComponent), multi: true },
40
+ ],
41
+ template: `
42
+ <!-- Trigger -->
43
+ @if (!inline()) {
44
+ <button
45
+ #triggerEl
46
+ type="button"
47
+ role="combobox"
48
+ [attr.aria-expanded]="open()"
49
+ aria-haspopup="dialog"
50
+ [disabled]="isDisabled()"
51
+ [class]="triggerClass()"
52
+ (click)="toggle()"
53
+ (blur)="onTouched()"
54
+ >
55
+ <div
56
+ class="h-5 w-5 rounded-sm border border-border shrink-0"
57
+ [style.backgroundColor]="displayValue()"
58
+ ></div>
59
+ <span class="truncate">{{ displayValue() || placeholder() }}</span>
60
+ </button>
61
+ }
62
+
63
+ <!-- Panel -->
64
+ @if (open() || inline()) {
65
+ <div
66
+ #panelEl
67
+ [class]="panelClass()"
68
+ role="dialog"
69
+ aria-modal="true"
70
+ aria-label="Color picker"
71
+ >
72
+ <!-- Saturation/Brightness Panel -->
73
+ <div
74
+ #satPanel
75
+ class="relative h-36 w-full rounded-md cursor-crosshair overflow-hidden"
76
+ [style.background]="saturationBg()"
77
+ (mousedown)="onSatPanelDown($event)"
78
+ (touchstart)="onSatPanelTouch($event)"
79
+ >
80
+ <div class="absolute inset-0 bg-gradient-to-t from-black to-transparent"></div>
81
+ <div
82
+ class="absolute h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-md pointer-events-none"
83
+ [style.left.%]="hsv().s * 100"
84
+ [style.top.%]="(1 - hsv().v) * 100"
85
+ ></div>
86
+ </div>
87
+
88
+ <!-- Hue Slider -->
89
+ <div
90
+ #hueTrack
91
+ class="relative h-3 w-full rounded-full cursor-pointer mt-3"
92
+ style="background: linear-gradient(to right, hsl(0,100%,50%), hsl(60,100%,50%), hsl(120,100%,50%), hsl(180,100%,50%), hsl(240,100%,50%), hsl(300,100%,50%), hsl(360,100%,50%))"
93
+ (mousedown)="onHueDown($event)"
94
+ (touchstart)="onHueTouch($event)"
95
+ >
96
+ <div
97
+ class="absolute top-1/2 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-md pointer-events-none"
98
+ [style.left.%]="hsv().h / 360 * 100"
99
+ [style.backgroundColor]="'hsl(' + hsv().h + ', 100%, 50%)'"
100
+ ></div>
101
+ </div>
102
+
103
+ <!-- Input + Format + Copy + Actions -->
104
+ @if (showInput()) {
105
+ <div class="mt-3 flex items-center gap-1.5">
106
+ <div
107
+ class="h-8 w-8 rounded-sm border border-border shrink-0"
108
+ [style.backgroundColor]="displayValue()"
109
+ ></div>
110
+ <input
111
+ class="flex-1 min-w-0 h-8 rounded-sm border border-border bg-background px-2 text-xs font-mono focus:outline-none focus:ring-1 focus:ring-ring"
112
+ [value]="inputValue()"
113
+ (input)="onInputChange($event)"
114
+ (blur)="commitInput()"
115
+ (keydown.enter)="commitInput()"
116
+ />
117
+ <button
118
+ type="button"
119
+ class="h-8 px-1.5 rounded-sm border border-border text-[10px] font-semibold uppercase hover:bg-accent transition-colors shrink-0"
120
+ (click)="cycleFormat()"
121
+ title="Switch format"
122
+ >
123
+ {{ currentFormat() }}
124
+ </button>
125
+ <button
126
+ type="button"
127
+ class="h-8 w-8 inline-flex items-center justify-center rounded-sm border border-border hover:bg-accent transition-colors shrink-0"
128
+ (click)="copyColor()"
129
+ [title]="copied() ? 'Copied!' : 'Copy color'"
130
+ >
131
+ @if (copied()) {
132
+ <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="M20 6 9 17l-5-5"/></svg>
133
+ } @else {
134
+ <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"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
135
+ }
136
+ </button>
137
+ </div>
138
+ <!-- Secondary actions row -->
139
+ <div class="mt-2 flex items-center gap-1.5">
140
+ @if (showEyeDropper() && hasEyeDropper) {
141
+ <button
142
+ type="button"
143
+ class="h-7 px-2 inline-flex items-center gap-1.5 rounded-sm border border-border text-xs text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
144
+ (click)="pickFromScreen()"
145
+ title="Pick from screen"
146
+ >
147
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m2 22 1-1h3l9-9"/><path d="M3 21v-3l9-9"/><path d="m15 6 3.4-3.4a2.1 2.1 0 1 1 3 3L18 9l.4.4a2.1 2.1 0 1 1-3 3l-3.8-3.8a2.1 2.1 0 1 1 3-3l.4.4Z"/></svg>
148
+ Pick
149
+ </button>
150
+ }
151
+ @if (showFavorites()) {
152
+ <button
153
+ type="button"
154
+ class="h-7 px-2 inline-flex items-center gap-1.5 rounded-sm border border-border text-xs text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
155
+ (click)="addFavorite()"
156
+ title="Save to favorites"
157
+ >
158
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>
159
+ Save
160
+ </button>
161
+ }
162
+ </div>
163
+ }
164
+
165
+ <!-- Presets -->
166
+ @for (preset of presets(); track $index) {
167
+ <div class="mt-3">
168
+ @if (preset.label) {
169
+ <p class="text-xs font-medium text-muted-foreground mb-1.5">{{ preset.label }}</p>
170
+ }
171
+ <div class="flex flex-wrap gap-1.5">
172
+ @for (color of preset.colors; track color) {
173
+ <button
174
+ type="button"
175
+ class="h-6 w-6 rounded-sm border border-border hover:scale-110 transition-transform cursor-pointer"
176
+ [style.backgroundColor]="color"
177
+ [title]="color"
178
+ (click)="selectColor(color)"
179
+ ></button>
180
+ }
181
+ </div>
182
+ </div>
183
+ }
184
+
185
+ <!-- Favorites -->
186
+ @if (showFavorites() && favorites().length > 0) {
187
+ <div class="mt-3">
188
+ <p class="text-xs font-medium text-muted-foreground mb-1.5">Favorites</p>
189
+ <div class="flex flex-wrap gap-1.5">
190
+ @for (fav of favorites(); track fav) {
191
+ <div class="relative group">
192
+ <button
193
+ type="button"
194
+ class="h-6 w-6 rounded-sm border border-border hover:scale-110 transition-transform cursor-pointer"
195
+ [style.backgroundColor]="fav"
196
+ [title]="fav"
197
+ (click)="selectColor(fav)"
198
+ ></button>
199
+ <button
200
+ type="button"
201
+ class="absolute -top-1 -right-1 h-3.5 w-3.5 rounded-full bg-destructive text-destructive-foreground text-[8px] leading-none items-center justify-center hidden group-hover:inline-flex"
202
+ (click)="removeFavorite(fav); $event.stopPropagation()"
203
+ >×</button>
204
+ </div>
205
+ }
206
+ </div>
207
+ </div>
208
+ }
209
+ </div>
210
+ }
211
+ `,
212
+ })
213
+ export class SnyColorPickerComponent implements ControlValueAccessor, OnDestroy {
214
+ // Public API
215
+ readonly value = model('#000000');
216
+ readonly format = input<ColorFormat>('hex');
217
+ readonly presets = input<ColorPickerPreset[]>([]);
218
+ readonly showInput = input(true);
219
+ readonly showEyeDropper = input(true);
220
+ readonly showFavorites = input(false);
221
+ readonly inline = input(false);
222
+ readonly disabled = input(false);
223
+ readonly placeholder = input('Pick a color...');
224
+ readonly size = input<ColorPickerSize>('md');
225
+ readonly class = input<string>('');
226
+
227
+ readonly colorChange = output<string>();
228
+ readonly formatChange = output<ColorFormat>();
229
+
230
+ // Internal state
231
+ readonly hsv = signal<HSV>({ h: 0, s: 0, v: 0 });
232
+ readonly currentFormat = signal<ColorFormat>('hex');
233
+ readonly inputValue = signal('');
234
+ readonly open = signal(false);
235
+ readonly favorites = signal<string[]>([]);
236
+ readonly copied = signal(false);
237
+
238
+ private readonly _disabledByCva = signal(false);
239
+ protected readonly isDisabled = computed(() => this.disabled() || this._disabledByCva());
240
+
241
+ private readonly triggerRef = viewChild<ElementRef<HTMLButtonElement>>('triggerEl');
242
+ private readonly panelRef = viewChild<ElementRef<HTMLDivElement>>('panelEl');
243
+ private readonly satPanelRef = viewChild<ElementRef<HTMLDivElement>>('satPanel');
244
+ private readonly hueTrackRef = viewChild<ElementRef<HTMLDivElement>>('hueTrack');
245
+ private readonly elRef = inject(ElementRef);
246
+
247
+ private moveHandler: ((e: MouseEvent | TouchEvent) => void) | null = null;
248
+ private upHandler: (() => void) | null = null;
249
+ private scrollHandler: (() => void) | null = null;
250
+ private resizeHandler: (() => void) | null = null;
251
+
252
+ private _onChange: (value: string) => void = () => {};
253
+ protected onTouched: () => void = () => {};
254
+
255
+ readonly hasEyeDropper = typeof window !== 'undefined' && 'EyeDropper' in window;
256
+
257
+ // Computed
258
+ readonly rgb = computed<RGB>(() => hsvToRgb(this.hsv()));
259
+ readonly displayValue = computed(() =>
260
+ formatColor(this.rgb(), this.currentFormat())
261
+ );
262
+
263
+ readonly saturationBg = computed(() =>
264
+ `linear-gradient(to right, #fff, hsl(${this.hsv().h}, 100%, 50%))`
265
+ );
266
+
267
+ protected readonly triggerClass = computed(() =>
268
+ cn(colorPickerTriggerVariants({ size: this.size() }), this.class())
269
+ );
270
+
271
+ protected readonly panelClass = computed(() =>
272
+ this.inline()
273
+ ? 'inline-block p-3 rounded-md border border-border bg-popover text-popover-foreground w-60'
274
+ : 'fixed z-50 p-3 rounded-md border border-border bg-popover text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 w-60'
275
+ );
276
+
277
+ constructor() {
278
+ // Sync format input
279
+ effect(() => {
280
+ const fmt = this.format();
281
+ untracked(() => this.currentFormat.set(fmt));
282
+ });
283
+
284
+ // Sync value → HSV when value changes externally
285
+ effect(() => {
286
+ const val = this.value();
287
+ untracked(() => {
288
+ const rgb = parseColor(val);
289
+ if (rgb) {
290
+ this.hsv.set(rgbToHsv(rgb));
291
+ this.inputValue.set(this.displayValue());
292
+ }
293
+ });
294
+ });
295
+ }
296
+
297
+ // CVA
298
+ writeValue(val: string): void {
299
+ this.value.set(val ?? '#000000');
300
+ }
301
+
302
+ registerOnChange(fn: (value: string) => void): void {
303
+ this._onChange = fn;
304
+ }
305
+
306
+ registerOnTouched(fn: () => void): void {
307
+ this.onTouched = fn;
308
+ }
309
+
310
+ setDisabledState(isDisabled: boolean): void {
311
+ this._disabledByCva.set(isDisabled);
312
+ }
313
+
314
+ // Emit helper
315
+ private emitColor(): void {
316
+ const formatted = this.displayValue();
317
+ this.value.set(formatted);
318
+ this.inputValue.set(formatted);
319
+ this._onChange(formatted);
320
+ this.colorChange.emit(formatted);
321
+ }
322
+
323
+ // Saturation panel
324
+ onSatPanelDown(event: MouseEvent): void {
325
+ event.preventDefault();
326
+ this.updateSatFromPosition(event.clientX, event.clientY);
327
+ this.startDrag((e) => {
328
+ const x = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
329
+ const y = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
330
+ this.updateSatFromPosition(x, y);
331
+ });
332
+ }
333
+
334
+ onSatPanelTouch(event: TouchEvent): void {
335
+ this.updateSatFromPosition(event.touches[0].clientX, event.touches[0].clientY);
336
+ this.startDrag((e) => {
337
+ const x = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
338
+ const y = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
339
+ this.updateSatFromPosition(x, y);
340
+ }, true);
341
+ }
342
+
343
+ private updateSatFromPosition(clientX: number, clientY: number): void {
344
+ const panel = this.satPanelRef()?.nativeElement;
345
+ if (!panel) return;
346
+ const rect = panel.getBoundingClientRect();
347
+ const s = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
348
+ const v = Math.max(0, Math.min(1, 1 - (clientY - rect.top) / rect.height));
349
+ this.hsv.update((prev) => ({ ...prev, s, v }));
350
+ this.emitColor();
351
+ }
352
+
353
+ // Hue slider
354
+ onHueDown(event: MouseEvent): void {
355
+ event.preventDefault();
356
+ this.updateHueFromPosition(event.clientX);
357
+ this.startDrag((e) => {
358
+ const x = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
359
+ this.updateHueFromPosition(x);
360
+ });
361
+ }
362
+
363
+ onHueTouch(event: TouchEvent): void {
364
+ this.updateHueFromPosition(event.touches[0].clientX);
365
+ this.startDrag((e) => {
366
+ const x = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
367
+ this.updateHueFromPosition(x);
368
+ }, true);
369
+ }
370
+
371
+ private updateHueFromPosition(clientX: number): void {
372
+ const track = this.hueTrackRef()?.nativeElement;
373
+ if (!track) return;
374
+ const rect = track.getBoundingClientRect();
375
+ const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
376
+ const h = Math.round(percent * 360);
377
+ this.hsv.update((prev) => ({ ...prev, h }));
378
+ this.emitColor();
379
+ }
380
+
381
+ // Drag helpers (same pattern as slider component)
382
+ private startDrag(handler: (e: MouseEvent | TouchEvent) => void, touch = false): void {
383
+ this.removeDragListeners();
384
+ this.moveHandler = handler;
385
+ this.upHandler = () => {
386
+ this.onTouched();
387
+ this.removeDragListeners();
388
+ };
389
+ if (touch) {
390
+ document.addEventListener('touchmove', this.moveHandler as EventListener, { passive: true });
391
+ document.addEventListener('touchend', this.upHandler);
392
+ } else {
393
+ document.addEventListener('mousemove', this.moveHandler as EventListener);
394
+ document.addEventListener('mouseup', this.upHandler);
395
+ }
396
+ }
397
+
398
+ private removeDragListeners(): void {
399
+ if (this.moveHandler) {
400
+ document.removeEventListener('mousemove', this.moveHandler as EventListener);
401
+ document.removeEventListener('touchmove', this.moveHandler as EventListener);
402
+ this.moveHandler = null;
403
+ }
404
+ if (this.upHandler) {
405
+ document.removeEventListener('mouseup', this.upHandler);
406
+ document.removeEventListener('touchend', this.upHandler);
407
+ this.upHandler = null;
408
+ }
409
+ }
410
+
411
+ // Input
412
+ onInputChange(event: Event): void {
413
+ this.inputValue.set((event.target as HTMLInputElement).value);
414
+ }
415
+
416
+ commitInput(): void {
417
+ const val = this.inputValue().trim();
418
+ if (isValidColor(val)) {
419
+ const rgb = parseColor(val)!;
420
+ this.hsv.set(rgbToHsv(rgb));
421
+ this.emitColor();
422
+ } else {
423
+ this.inputValue.set(this.displayValue());
424
+ }
425
+ }
426
+
427
+ // Format
428
+ cycleFormat(): void {
429
+ const formats: ColorFormat[] = ['hex', 'rgb', 'hsl'];
430
+ const idx = formats.indexOf(this.currentFormat());
431
+ const next = formats[(idx + 1) % formats.length];
432
+ this.currentFormat.set(next);
433
+ this.inputValue.set(this.displayValue());
434
+ this.formatChange.emit(next);
435
+ }
436
+
437
+ // Copy
438
+ copyColor(): void {
439
+ navigator.clipboard.writeText(this.displayValue());
440
+ this.copied.set(true);
441
+ setTimeout(() => this.copied.set(false), 2000);
442
+ }
443
+
444
+ // Presets & favorites
445
+ selectColor(color: string): void {
446
+ const rgb = parseColor(color);
447
+ if (rgb) {
448
+ this.hsv.set(rgbToHsv(rgb));
449
+ this.emitColor();
450
+ }
451
+ }
452
+
453
+ addFavorite(): void {
454
+ const hex = rgbToHex(this.rgb());
455
+ this.favorites.update((favs) =>
456
+ favs.includes(hex) ? favs : [...favs, hex]
457
+ );
458
+ }
459
+
460
+ removeFavorite(color: string): void {
461
+ this.favorites.update((favs) => favs.filter((f) => f !== color));
462
+ }
463
+
464
+ // EyeDropper
465
+ async pickFromScreen(): Promise<void> {
466
+ if (!this.hasEyeDropper) return;
467
+ try {
468
+ const dropper = new (window as any).EyeDropper();
469
+ const result = await dropper.open();
470
+ this.selectColor(result.sRGBHex);
471
+ } catch {
472
+ // User cancelled
473
+ }
474
+ }
475
+
476
+ // Popover
477
+ toggle(): void {
478
+ if (this.open()) {
479
+ this.close();
480
+ } else {
481
+ this.open.set(true);
482
+ this.addPositionListeners();
483
+ setTimeout(() => this.updatePanelPosition());
484
+ }
485
+ }
486
+
487
+ close(): void {
488
+ this.open.set(false);
489
+ this.removePositionListeners();
490
+ }
491
+
492
+ private updatePanelPosition(): void {
493
+ if (this.inline()) return;
494
+ const trigger = this.triggerRef()?.nativeElement;
495
+ if (!trigger) return;
496
+ const rect = trigger.getBoundingClientRect();
497
+ const panel = this.panelRef()?.nativeElement;
498
+ if (panel) {
499
+ panel.style.top = `${rect.bottom + 4}px`;
500
+ panel.style.left = `${rect.left}px`;
501
+ }
502
+ }
503
+
504
+ private addPositionListeners(): void {
505
+ this.removePositionListeners();
506
+ this.scrollHandler = () => requestAnimationFrame(() => this.updatePanelPosition());
507
+ this.resizeHandler = () => requestAnimationFrame(() => this.updatePanelPosition());
508
+ document.addEventListener('scroll', this.scrollHandler, { capture: true, passive: true });
509
+ window.addEventListener('resize', this.resizeHandler, { passive: true });
510
+ }
511
+
512
+ private removePositionListeners(): void {
513
+ if (this.scrollHandler) {
514
+ document.removeEventListener('scroll', this.scrollHandler, { capture: true } as EventListenerOptions);
515
+ this.scrollHandler = null;
516
+ }
517
+ if (this.resizeHandler) {
518
+ window.removeEventListener('resize', this.resizeHandler);
519
+ this.resizeHandler = null;
520
+ }
521
+ }
522
+
523
+ onDocumentClick(event: MouseEvent): void {
524
+ if (!this.elRef.nativeElement.contains(event.target)) {
525
+ this.close();
526
+ }
527
+ }
528
+
529
+ onEscape(): void {
530
+ this.close();
531
+ }
532
+
533
+ ngOnDestroy(): void {
534
+ this.removeDragListeners();
535
+ this.removePositionListeners();
536
+ }
537
+ }
@@ -0,0 +1,24 @@
1
+ export type ColorFormat = 'hex' | 'rgb' | 'hsl';
2
+
3
+ export interface RGB {
4
+ r: number;
5
+ g: number;
6
+ b: number;
7
+ }
8
+
9
+ export interface HSL {
10
+ h: number;
11
+ s: number;
12
+ l: number;
13
+ }
14
+
15
+ export interface HSV {
16
+ h: number;
17
+ s: number;
18
+ v: number;
19
+ }
20
+
21
+ export interface ColorPickerPreset {
22
+ label?: string;
23
+ colors: string[];
24
+ }