@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.
- package/README.md +187 -40
- package/fesm2022/sonny-ui-core.mjs +6646 -272
- 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 +45 -0
- 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
|
+
}
|