@foxui/colors 0.4.0
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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/components/color-gradient-picker/ColorGradientPicker.svelte +229 -0
- package/dist/components/color-gradient-picker/ColorGradientPicker.svelte.d.ts +16 -0
- package/dist/components/color-gradient-picker/index.d.ts +1 -0
- package/dist/components/color-gradient-picker/index.js +1 -0
- package/dist/components/color-picker/LICENSE +42 -0
- package/dist/components/color-picker/base/ColorPicker.svelte +312 -0
- package/dist/components/color-picker/base/ColorPicker.svelte.d.ts +23 -0
- package/dist/components/color-picker/base/color.d.ts +47 -0
- package/dist/components/color-picker/base/color.js +96 -0
- package/dist/components/color-picker/base/constants.d.ts +5 -0
- package/dist/components/color-picker/base/constants.js +5 -0
- package/dist/components/color-picker/base/index.d.ts +3 -0
- package/dist/components/color-picker/base/index.js +3 -0
- package/dist/components/color-picker/base/render.d.ts +5 -0
- package/dist/components/color-picker/base/render.js +77 -0
- package/dist/components/color-picker/base/shaders.d.ts +2 -0
- package/dist/components/color-picker/base/shaders.js +8 -0
- package/dist/components/color-picker/index.d.ts +2 -0
- package/dist/components/color-picker/index.js +2 -0
- package/dist/components/color-picker/popover/PopoverColorPicker.svelte +75 -0
- package/dist/components/color-picker/popover/PopoverColorPicker.svelte.d.ts +26 -0
- package/dist/components/color-select/ColorSelect.svelte +82 -0
- package/dist/components/color-select/ColorSelect.svelte.d.ts +17 -0
- package/dist/components/color-select/index.d.ts +1 -0
- package/dist/components/color-select/index.js +1 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.js +5 -0
- package/dist/components/select-theme/SelectTheme.svelte +124 -0
- package/dist/components/select-theme/SelectTheme.svelte.d.ts +10 -0
- package/dist/components/select-theme/SelectThemePopover.svelte +49 -0
- package/dist/components/select-theme/SelectThemePopover.svelte.d.ts +10 -0
- package/dist/components/select-theme/index.d.ts +2 -0
- package/dist/components/select-theme/index.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +1 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License Copyright (c) 2025 flo-bit
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of
|
|
4
|
+
charge, to any person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use, copy, modify, merge,
|
|
7
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice
|
|
12
|
+
(including the next paragraph) shall be included in all copies or substantial
|
|
13
|
+
portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
16
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
18
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
19
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 🦊 fox ui
|
|
2
|
+
|
|
3
|
+
svelte 5 + tailwind 4 ui kit, colors components
|
|
4
|
+
|
|
5
|
+
- [Color Gradient Picker](https://flo-bit.dev/ui-kit/components/colors/color-gradient-picker)
|
|
6
|
+
- [Color Picker](https://flo-bit.dev/ui-kit/components/colors/color-picker)
|
|
7
|
+
- [Color Select](https://flo-bit.dev/ui-kit/components/colors/color-select)
|
|
8
|
+
|
|
9
|
+
> **This is a public alpha release. Expect bugs and breaking changes.**
|
|
10
|
+
|
|
11
|
+
[See all components here](https://flo-bit.dev/ui-kit)
|
|
12
|
+
|
|
13
|
+
For a guide on how to use this ui kit, see the [Quickstart Guide](https://flo-bit.dev/ui-kit/docs/quick-start).
|
|
14
|
+
|
|
15
|
+
Read more about [the philosophy/aim of this project here](https://flo-bit.dev/ui-kit/docs/philosophy).
|
|
16
|
+
|
|
17
|
+
For more information about development, contributing and the like, see the main [README](https://github.com/flo-bit/ui-kit/blob/main/README.md).
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, Button, Popover } from '@foxui/core';
|
|
3
|
+
|
|
4
|
+
import { ColorPicker, type RGB } from '../color-picker/base';
|
|
5
|
+
import { DragGesture } from '@use-gesture/vanilla';
|
|
6
|
+
|
|
7
|
+
type ColorStop = { rgb: RGB; position: number };
|
|
8
|
+
|
|
9
|
+
let initialColors: ColorStop[] = $state([
|
|
10
|
+
{ rgb: { r: 0, g: 0, b: 1 }, position: 0 },
|
|
11
|
+
{ rgb: { r: 1, g: 0, b: 0 }, position: 1 }
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
colors = $bindable(initialColors),
|
|
16
|
+
class: className,
|
|
17
|
+
defaultNewColor = { r: 0, g: 0, b: 0 },
|
|
18
|
+
size = 'default',
|
|
19
|
+
onchange
|
|
20
|
+
}: {
|
|
21
|
+
rgb?: RGB;
|
|
22
|
+
|
|
23
|
+
colors?: ColorStop[];
|
|
24
|
+
|
|
25
|
+
class?: string;
|
|
26
|
+
|
|
27
|
+
defaultNewColor?: RGB;
|
|
28
|
+
|
|
29
|
+
size?: 'default' | 'sm';
|
|
30
|
+
|
|
31
|
+
onchange?: (colors: ColorStop[]) => void;
|
|
32
|
+
} = $props();
|
|
33
|
+
|
|
34
|
+
const gradientString = $derived(
|
|
35
|
+
`linear-gradient(to right, ${colors
|
|
36
|
+
.toSorted((a, b) => a.position - b.position)
|
|
37
|
+
.map(
|
|
38
|
+
({ rgb, position }) =>
|
|
39
|
+
`rgb(${rgb.r * 255}, ${rgb.g * 255}, ${rgb.b * 255}) ${position * 100}%`
|
|
40
|
+
)
|
|
41
|
+
.join(', ')})`
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
let colorsRef = $state<(HTMLButtonElement | null)[]>(new Array(colors.length).fill(null));
|
|
45
|
+
let gradientRef = $state<HTMLDivElement | undefined>(undefined);
|
|
46
|
+
let isDragging = $state(new Array(colors.length).fill(false));
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
let gestures: DragGesture[] = [];
|
|
50
|
+
|
|
51
|
+
colorsRef.forEach((ref, i) => {
|
|
52
|
+
if (!ref) return;
|
|
53
|
+
|
|
54
|
+
gestures.push(
|
|
55
|
+
new DragGesture(
|
|
56
|
+
ref,
|
|
57
|
+
(state) => {
|
|
58
|
+
const { delta } = state;
|
|
59
|
+
|
|
60
|
+
const newPosition = delta[0] / (gradientRef?.clientWidth ?? 1);
|
|
61
|
+
|
|
62
|
+
colors[i].position += newPosition;
|
|
63
|
+
colors[i].position = Math.max(0, Math.min(1, colors[i].position));
|
|
64
|
+
|
|
65
|
+
if (Math.abs(state.offset[0]) > 10) {
|
|
66
|
+
isDragging[i] = state.active;
|
|
67
|
+
} else {
|
|
68
|
+
isDragging[i] = state.active;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (state.tap) {
|
|
72
|
+
allOpen[i] = !allOpen[i];
|
|
73
|
+
} else {
|
|
74
|
+
onchange?.(colors);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
preventDefault: true,
|
|
79
|
+
eventOptions: {
|
|
80
|
+
passive: false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
gestures.forEach((gesture) => gesture.destroy());
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let allOpen = $state(new Array(colors.length).fill(false));
|
|
93
|
+
|
|
94
|
+
function getOpen(i: number) {
|
|
95
|
+
if (isDragging[i]) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return allOpen[i];
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
103
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
104
|
+
<div
|
|
105
|
+
class={cn(
|
|
106
|
+
'border-base-400 dark:border-base-600 relative w-full cursor-pointer touch-none rounded-2xl border',
|
|
107
|
+
size === 'sm' ? 'h-4' : 'h-8',
|
|
108
|
+
className
|
|
109
|
+
)}
|
|
110
|
+
style={'background: ' + gradientString}
|
|
111
|
+
bind:this={gradientRef}
|
|
112
|
+
onclick={(e) => {
|
|
113
|
+
// get target
|
|
114
|
+
const target = e.target as HTMLDivElement;
|
|
115
|
+
// if target is not the gradient itself return
|
|
116
|
+
if (target !== gradientRef) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let rect = gradientRef?.getBoundingClientRect();
|
|
120
|
+
// get the position of the click
|
|
121
|
+
const position = (e.clientX - rect.left) / (gradientRef?.clientWidth ?? 1);
|
|
122
|
+
// add a new color
|
|
123
|
+
colors.push({
|
|
124
|
+
rgb: { r: defaultNewColor.r, g: defaultNewColor.g, b: defaultNewColor.b },
|
|
125
|
+
position: position
|
|
126
|
+
});
|
|
127
|
+
colorsRef.push(null);
|
|
128
|
+
allOpen.push(true);
|
|
129
|
+
onchange?.(colors);
|
|
130
|
+
}}
|
|
131
|
+
onkeydown={(e) => {
|
|
132
|
+
if (e.key === '+') {
|
|
133
|
+
colors.push({
|
|
134
|
+
rgb: { r: defaultNewColor.r, g: defaultNewColor.g, b: defaultNewColor.b },
|
|
135
|
+
position: 0.5
|
|
136
|
+
});
|
|
137
|
+
colorsRef.push(null);
|
|
138
|
+
allOpen.push(true);
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{#each colors as color, i}
|
|
143
|
+
<Popover
|
|
144
|
+
bind:open={() => getOpen(i), (open) => {}}
|
|
145
|
+
side="bottom"
|
|
146
|
+
sideOffset={10}
|
|
147
|
+
onInteractOutside={() => {
|
|
148
|
+
allOpen[i] = false;
|
|
149
|
+
}}
|
|
150
|
+
onEscapeKeydown={() => {
|
|
151
|
+
allOpen[i] = false;
|
|
152
|
+
}}
|
|
153
|
+
onkeydown={(e) => {
|
|
154
|
+
if (e.key === 'Backspace' && colors.length > 2) {
|
|
155
|
+
colors.splice(i, 1);
|
|
156
|
+
onchange?.(colors);
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
class="p-1 pl-2 pr-0"
|
|
160
|
+
>
|
|
161
|
+
{#snippet child({ props })}
|
|
162
|
+
<button
|
|
163
|
+
{...props}
|
|
164
|
+
class={cn(
|
|
165
|
+
'absolute left-0 touch-none',
|
|
166
|
+
'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-full focus-visible:outline-2 focus-visible:outline-offset-2',
|
|
167
|
+
size === 'sm' ? '-top-1.5' : '-top-4'
|
|
168
|
+
)}
|
|
169
|
+
style={`left: calc(${color.position * 100}% - 16px)`}
|
|
170
|
+
bind:this={colorsRef[i]}
|
|
171
|
+
tabindex={Math.floor(color.position * 100 + 10)}
|
|
172
|
+
onkeydown={(e) => {
|
|
173
|
+
if (e.key === 'Enter') {
|
|
174
|
+
allOpen[i] = !allOpen[i];
|
|
175
|
+
} else if (e.key === 'Backspace' && colors.length > 2) {
|
|
176
|
+
colors.splice(i, 1);
|
|
177
|
+
onchange?.(colors);
|
|
178
|
+
}
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<div
|
|
182
|
+
class={cn(
|
|
183
|
+
'ring-base-400 dark:ring-base-500 focus-visible:outline-accent-500 shadow-base-900/50 z-10 rounded-full shadow-lg ring',
|
|
184
|
+
size === 'sm' ? 'size-6' : 'size-8'
|
|
185
|
+
)}
|
|
186
|
+
style={`background-color: rgb(${color.rgb.r * 255}, ${color.rgb.g * 255}, ${color.rgb.b * 255});`}
|
|
187
|
+
></div>
|
|
188
|
+
<span class="sr-only">Color Picker</span>
|
|
189
|
+
</button>
|
|
190
|
+
{/snippet}
|
|
191
|
+
<div>
|
|
192
|
+
<ColorPicker
|
|
193
|
+
bind:rgb={colors[i].rgb}
|
|
194
|
+
onchange={() => {
|
|
195
|
+
onchange?.(colors);
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
|
|
199
|
+
<div class="flex w-full justify-center">
|
|
200
|
+
{#if colors.length > 2}
|
|
201
|
+
<Button
|
|
202
|
+
variant="link"
|
|
203
|
+
class="mb-1 backdrop-blur-none"
|
|
204
|
+
onclick={() => {
|
|
205
|
+
colors.splice(i, 1);
|
|
206
|
+
onchange?.(colors);
|
|
207
|
+
}}
|
|
208
|
+
size="icon"
|
|
209
|
+
>
|
|
210
|
+
<svg
|
|
211
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
212
|
+
viewBox="0 0 24 24"
|
|
213
|
+
fill="currentColor"
|
|
214
|
+
class="size-6"
|
|
215
|
+
>
|
|
216
|
+
<path
|
|
217
|
+
fill-rule="evenodd"
|
|
218
|
+
d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z"
|
|
219
|
+
clip-rule="evenodd"
|
|
220
|
+
/>
|
|
221
|
+
</svg>
|
|
222
|
+
Delete
|
|
223
|
+
</Button>
|
|
224
|
+
{/if}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</Popover>
|
|
228
|
+
{/each}
|
|
229
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RGB } from '../color-picker/base';
|
|
2
|
+
type ColorStop = {
|
|
3
|
+
rgb: RGB;
|
|
4
|
+
position: number;
|
|
5
|
+
};
|
|
6
|
+
type $$ComponentProps = {
|
|
7
|
+
rgb?: RGB;
|
|
8
|
+
colors?: ColorStop[];
|
|
9
|
+
class?: string;
|
|
10
|
+
defaultNewColor?: RGB;
|
|
11
|
+
size?: 'default' | 'sm';
|
|
12
|
+
onchange?: (colors: ColorStop[]) => void;
|
|
13
|
+
};
|
|
14
|
+
declare const ColorGradientPicker: import("svelte").Component<$$ComponentProps, {}, "colors">;
|
|
15
|
+
type ColorGradientPicker = ReturnType<typeof ColorGradientPicker>;
|
|
16
|
+
export default ColorGradientPicker;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ColorGradientPicker } from './ColorGradientPicker.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ColorGradientPicker } from './ColorGradientPicker.svelte';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from https://github.com/CaptainCodeman/svelte-color-select
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2022 Simon Green
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
24
|
+
|
|
25
|
+
Based on https://bottosson.github.io/posts/colorpicker/
|
|
26
|
+
Copyright (c) 2021 Björn Ottosson
|
|
27
|
+
|
|
28
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
29
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
30
|
+
the Software without restriction, including without limitation the rights to
|
|
31
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
32
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
33
|
+
so, subject to the following conditions:
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
37
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
38
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
39
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
40
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
41
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
42
|
+
SOFTWARE.
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { eps, picker_size, slider_width, border_size, gap_size } from './constants';
|
|
3
|
+
import { render_main_image, render_slider_image } from './render';
|
|
4
|
+
import type { RGB, OKlab, OKhsv, OKlch } from './color';
|
|
5
|
+
import {
|
|
6
|
+
okhsv_to_oklab,
|
|
7
|
+
okhsv_to_rgb,
|
|
8
|
+
okhsv_to_oklch,
|
|
9
|
+
rgb_to_hex,
|
|
10
|
+
oklab_to_okhsv,
|
|
11
|
+
rgb_to_okhsv,
|
|
12
|
+
oklab_to_rgb
|
|
13
|
+
} from './color';
|
|
14
|
+
|
|
15
|
+
import { cn } from '@foxui/core';
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
rgb = $bindable(),
|
|
19
|
+
oklab = $bindable(),
|
|
20
|
+
okhsv = $bindable(),
|
|
21
|
+
class: className,
|
|
22
|
+
onchange,
|
|
23
|
+
quickSelects = $bindable([])
|
|
24
|
+
}: {
|
|
25
|
+
rgb?: RGB;
|
|
26
|
+
oklab?: OKlab;
|
|
27
|
+
okhsv?: OKhsv;
|
|
28
|
+
class?: string;
|
|
29
|
+
onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void;
|
|
30
|
+
quickSelects?: {
|
|
31
|
+
label: string;
|
|
32
|
+
rgb?: RGB;
|
|
33
|
+
oklab?: OKlab;
|
|
34
|
+
okhsv?: OKhsv;
|
|
35
|
+
}[];
|
|
36
|
+
} = $props();
|
|
37
|
+
|
|
38
|
+
const width = picker_size + slider_width + gap_size + border_size * 2;
|
|
39
|
+
const height = picker_size + border_size * 2;
|
|
40
|
+
|
|
41
|
+
let color = $derived(convertToInternal(rgb, oklab, okhsv));
|
|
42
|
+
let uihsv = $derived(scale_to_ui(color));
|
|
43
|
+
|
|
44
|
+
function scale_to_ui(okhsv: OKhsv): OKhsv {
|
|
45
|
+
return {
|
|
46
|
+
h: clamp_ui(okhsv.h / 360),
|
|
47
|
+
s: clamp_ui(okhsv.s),
|
|
48
|
+
v: clamp_ui(1 - okhsv.v)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function clamp(x: number) {
|
|
53
|
+
return x < eps ? eps : x > 1 - eps ? 1 - eps : x;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clamp_ui(value: number) {
|
|
57
|
+
return value < 0 ? 0 : value > 1 ? picker_size : value * picker_size;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function convertToInternal(
|
|
61
|
+
rgb: RGB | undefined,
|
|
62
|
+
oklab: OKlab | undefined,
|
|
63
|
+
okhsv: OKhsv | undefined
|
|
64
|
+
) {
|
|
65
|
+
if (okhsv) {
|
|
66
|
+
return okhsv;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (oklab) {
|
|
70
|
+
return oklab_to_okhsv(oklab);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (rgb) {
|
|
74
|
+
return rgb_to_okhsv(rgb);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw 'rgb, oklab, or okhsv required';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function update_input() {
|
|
81
|
+
let new_rgb = okhsv_to_rgb(color);
|
|
82
|
+
let new_oklab = okhsv_to_oklab(color);
|
|
83
|
+
let new_oklch = okhsv_to_oklch(color);
|
|
84
|
+
let new_hex = rgb_to_hex(new_rgb);
|
|
85
|
+
let new_okhsv = color;
|
|
86
|
+
|
|
87
|
+
if (okhsv) {
|
|
88
|
+
okhsv = color;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (oklab) {
|
|
92
|
+
oklab = new_oklab;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (rgb) {
|
|
96
|
+
rgb = new_rgb;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onchange?.({
|
|
100
|
+
rgb: new_rgb,
|
|
101
|
+
oklab: new_oklab,
|
|
102
|
+
okhsv: new_okhsv,
|
|
103
|
+
oklch: new_oklch,
|
|
104
|
+
hex: new_hex
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function update_sv(x: number, y: number) {
|
|
109
|
+
let new_s = clamp(x / picker_size);
|
|
110
|
+
let new_v = clamp(1 - y / picker_size);
|
|
111
|
+
|
|
112
|
+
color.s = new_s;
|
|
113
|
+
color.v = new_v;
|
|
114
|
+
|
|
115
|
+
update_input();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function update_h(x: number, y: number) {
|
|
119
|
+
let h = clamp(y / picker_size);
|
|
120
|
+
color.h = h * 360;
|
|
121
|
+
color.s = Math.max(color.s, 0.00001);
|
|
122
|
+
color.v = Math.max(color.v, 0.00001);
|
|
123
|
+
|
|
124
|
+
update_input();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function pointer(node: HTMLCanvasElement, fn: (x: number, y: number) => void) {
|
|
128
|
+
let active = false;
|
|
129
|
+
|
|
130
|
+
function update(event: PointerEvent) {
|
|
131
|
+
const x = event.offsetX;
|
|
132
|
+
const y = event.offsetY;
|
|
133
|
+
fn(x, y);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function onpointerdown(event: PointerEvent) {
|
|
137
|
+
event.stopPropagation();
|
|
138
|
+
node.setPointerCapture(event.pointerId);
|
|
139
|
+
update(event);
|
|
140
|
+
active = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function onpointermove(event: PointerEvent) {
|
|
144
|
+
event.stopPropagation();
|
|
145
|
+
if (active) {
|
|
146
|
+
update(event);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function onpointerend(event: PointerEvent) {
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
node.releasePointerCapture(event.pointerId);
|
|
153
|
+
active = false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
node.addEventListener('pointerdown', onpointerdown, { passive: true });
|
|
157
|
+
node.addEventListener('pointermove', onpointermove, { passive: true });
|
|
158
|
+
node.addEventListener('pointerup', onpointerend, { passive: true });
|
|
159
|
+
node.addEventListener('pointercancel', onpointerend, { passive: true });
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
destroy() {
|
|
163
|
+
node.removeEventListener('pointerdown', onpointerdown);
|
|
164
|
+
node.removeEventListener('pointermove', onpointermove);
|
|
165
|
+
node.removeEventListener('pointerup', onpointerend);
|
|
166
|
+
node.removeEventListener('pointercancel', onpointerend);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onKeydown(event: KeyboardEvent) {
|
|
172
|
+
const keyHandled = () => {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
event.stopPropagation();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const step = event.shiftKey ? 10 : 1;
|
|
178
|
+
|
|
179
|
+
switch (event.key) {
|
|
180
|
+
case 'ArrowUp':
|
|
181
|
+
if (event.altKey) {
|
|
182
|
+
update_h(0, Math.round(uihsv.h! - step));
|
|
183
|
+
} else {
|
|
184
|
+
update_sv(uihsv.s, Math.round(uihsv.v - step));
|
|
185
|
+
}
|
|
186
|
+
keyHandled();
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case 'ArrowDown':
|
|
190
|
+
if (event.altKey) {
|
|
191
|
+
update_h(0, Math.round(uihsv.h! + step));
|
|
192
|
+
} else {
|
|
193
|
+
update_sv(uihsv.s, Math.round(uihsv.v + step));
|
|
194
|
+
}
|
|
195
|
+
keyHandled();
|
|
196
|
+
break;
|
|
197
|
+
|
|
198
|
+
case 'ArrowLeft':
|
|
199
|
+
update_sv(Math.round(uihsv.s - step), uihsv.v);
|
|
200
|
+
keyHandled();
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
case 'ArrowRight':
|
|
204
|
+
update_sv(Math.round(uihsv.s + step), uihsv.v);
|
|
205
|
+
keyHandled();
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function convertToRgb(
|
|
211
|
+
rgb: RGB | undefined,
|
|
212
|
+
oklab: OKlab | undefined,
|
|
213
|
+
okhsv: OKhsv | undefined
|
|
214
|
+
): RGB {
|
|
215
|
+
if (okhsv) {
|
|
216
|
+
return okhsv_to_rgb(okhsv);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (oklab) {
|
|
220
|
+
return oklab_to_rgb(oklab);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (rgb) {
|
|
224
|
+
return rgb;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
throw 'rgb, oklab, or okhsv required';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getRgbString(rgb: RGB) {
|
|
231
|
+
return `rgb(${rgb.r * 255}, ${rgb.g * 255}, ${rgb.b * 255})`;
|
|
232
|
+
}
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
236
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
237
|
+
<div
|
|
238
|
+
class={cn(
|
|
239
|
+
'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 relative rounded-xl border-none focus-visible:outline-2 focus-visible:outline-offset-2',
|
|
240
|
+
className
|
|
241
|
+
)}
|
|
242
|
+
tabindex="0"
|
|
243
|
+
style:width="{width}px"
|
|
244
|
+
style:height="{height}px"
|
|
245
|
+
onkeydown={onKeydown}
|
|
246
|
+
>
|
|
247
|
+
<canvas
|
|
248
|
+
id="okhsv_sv_canvas"
|
|
249
|
+
width={picker_size}
|
|
250
|
+
height={picker_size}
|
|
251
|
+
style:top="{border_size}px"
|
|
252
|
+
style:left="{border_size}px"
|
|
253
|
+
class="absolute touch-none rounded-xl"
|
|
254
|
+
use:pointer={update_sv}
|
|
255
|
+
use:render_main_image={color.h}
|
|
256
|
+
></canvas>
|
|
257
|
+
<canvas
|
|
258
|
+
width={slider_width}
|
|
259
|
+
height={picker_size}
|
|
260
|
+
style:top="{border_size}px"
|
|
261
|
+
style:left="{picker_size + gap_size}px"
|
|
262
|
+
class="absolute touch-none rounded-xl"
|
|
263
|
+
use:pointer={update_h}
|
|
264
|
+
use:render_slider_image
|
|
265
|
+
></canvas>
|
|
266
|
+
|
|
267
|
+
<svg {width} {height} class="pointer-events-none absolute touch-none">
|
|
268
|
+
<g transform="translate({border_size},{border_size})">
|
|
269
|
+
<g transform="translate({uihsv.s},{uihsv.v})">
|
|
270
|
+
<circle cx="0" cy="0" r="5" fill="none" stroke-width="1.75" class="stroke-base-50" />
|
|
271
|
+
<circle cx="0" cy="0" r="6" fill="none" stroke-width="1.25" class="stroke-base-950" />
|
|
272
|
+
</g>
|
|
273
|
+
</g>
|
|
274
|
+
<g transform="translate({picker_size + gap_size},{border_size})">
|
|
275
|
+
<g transform="translate(0,{uihsv.h})">
|
|
276
|
+
<polygon points="-7,-4 -1,0 -7,4" stroke-width="0.8" class="stroke-base-950 fill-base-50" />
|
|
277
|
+
<polygon
|
|
278
|
+
points="{slider_width + 7},-4 {slider_width + 1},0 {slider_width + 7},4"
|
|
279
|
+
stroke-width="0.8"
|
|
280
|
+
class="stroke-base-950 fill-base-50"
|
|
281
|
+
/>
|
|
282
|
+
</g>
|
|
283
|
+
</g>
|
|
284
|
+
</svg>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{#if quickSelects.length > 0}
|
|
288
|
+
<div class="grid grid-cols-7 gap-2 px-2 pt-2 pb-3"
|
|
289
|
+
style:width="{width}px">
|
|
290
|
+
{#each quickSelects as quickSelect}
|
|
291
|
+
<button
|
|
292
|
+
class={cn(
|
|
293
|
+
'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-full focus-visible:outline-2 focus-visible:outline-offset-2',
|
|
294
|
+
'group'
|
|
295
|
+
)}
|
|
296
|
+
onclick={() => {
|
|
297
|
+
color = convertToInternal(quickSelect.rgb, quickSelect.oklab, quickSelect.okhsv);
|
|
298
|
+
|
|
299
|
+
update_input();
|
|
300
|
+
}}
|
|
301
|
+
>
|
|
302
|
+
<div
|
|
303
|
+
class="border-base-300 dark:border-base-700 focus-visible:outline-accent-500 z-10 size-7 rounded-full border group-hover:scale-105 group-active:scale-95 transition-all duration-100"
|
|
304
|
+
style="background-color: {getRgbString(
|
|
305
|
+
convertToRgb(quickSelect.rgb, quickSelect.oklab, quickSelect.okhsv)
|
|
306
|
+
)};"
|
|
307
|
+
></div>
|
|
308
|
+
<span class="sr-only">Select {quickSelect.label}</span>
|
|
309
|
+
</button>
|
|
310
|
+
{/each}
|
|
311
|
+
</div>
|
|
312
|
+
{/if}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RGB, OKlab, OKhsv, OKlch } from './color';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
rgb?: RGB;
|
|
4
|
+
oklab?: OKlab;
|
|
5
|
+
okhsv?: OKhsv;
|
|
6
|
+
class?: string;
|
|
7
|
+
onchange?: (color: {
|
|
8
|
+
hex: string;
|
|
9
|
+
rgb: RGB;
|
|
10
|
+
oklab: OKlab;
|
|
11
|
+
okhsv: OKhsv;
|
|
12
|
+
oklch: OKlch;
|
|
13
|
+
}) => void;
|
|
14
|
+
quickSelects?: {
|
|
15
|
+
label: string;
|
|
16
|
+
rgb?: RGB;
|
|
17
|
+
oklab?: OKlab;
|
|
18
|
+
okhsv?: OKhsv;
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
21
|
+
declare const ColorPicker: import("svelte").Component<$$ComponentProps, {}, "rgb" | "oklab" | "okhsv" | "quickSelects">;
|
|
22
|
+
type ColorPicker = ReturnType<typeof ColorPicker>;
|
|
23
|
+
export default ColorPicker;
|