@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { convert, OKHSL, OKHSV, OKLab, OKLCH, sRGB, deserialize, listColorSpaces } from '@texel/color';
|
|
2
|
+
export type Color = string | RGB | OKlab | OKhsv | OKlch;
|
|
3
|
+
export type SetColorFunction = (color: Color) => void;
|
|
4
|
+
export interface RGB {
|
|
5
|
+
r: number;
|
|
6
|
+
g: number;
|
|
7
|
+
b: number;
|
|
8
|
+
}
|
|
9
|
+
export interface OKlab {
|
|
10
|
+
l: number;
|
|
11
|
+
a: number;
|
|
12
|
+
b: number;
|
|
13
|
+
}
|
|
14
|
+
export interface OKhsv {
|
|
15
|
+
h: number;
|
|
16
|
+
s: number;
|
|
17
|
+
v: number;
|
|
18
|
+
}
|
|
19
|
+
export interface OKhsl {
|
|
20
|
+
h: number;
|
|
21
|
+
s: number;
|
|
22
|
+
l: number;
|
|
23
|
+
}
|
|
24
|
+
export interface OKlch {
|
|
25
|
+
l: number;
|
|
26
|
+
c: number;
|
|
27
|
+
h: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function okhsv_to_oklch(okhsv: OKhsv): OKlch;
|
|
30
|
+
export declare function oklch_to_okhsv(oklch: OKlch): OKhsv;
|
|
31
|
+
export declare function oklab_to_okhsv(oklab: OKlab): OKhsv;
|
|
32
|
+
export declare function okhsv_to_rgb(okhsv: OKhsv): RGB;
|
|
33
|
+
export declare function okhsv_to_oklab(okhsv: OKhsv): OKlab;
|
|
34
|
+
export declare function rgb_to_okhsv(rgb: RGB): OKhsv;
|
|
35
|
+
export declare function rgb_to_oklab(rgb: RGB): OKlab;
|
|
36
|
+
export declare function okhsl_to_rgb(okhsl: OKhsl): RGB;
|
|
37
|
+
export declare function oklab_to_oklch(oklab: OKlab): OKlch;
|
|
38
|
+
export declare function rgb_to_oklch(rgb: RGB): OKlch;
|
|
39
|
+
export declare function oklab_to_rgb(oklab: OKlab): RGB;
|
|
40
|
+
export declare function oklch_to_rgb(oklch: OKlch): RGB;
|
|
41
|
+
export declare function oklch_string_to_oklch(oklch: string): OKlch;
|
|
42
|
+
export declare function rgb_to_hex(rgb: RGB): string;
|
|
43
|
+
export declare function hex_to_rgb(hex: string): RGB;
|
|
44
|
+
export declare function hex_to_okhsv(hex: string): OKhsv;
|
|
45
|
+
export declare function okhsv_to_hex(okhsv: OKhsv): string;
|
|
46
|
+
export declare function convertCSSToHex(css: string): string;
|
|
47
|
+
export { deserialize, convert, listColorSpaces, sRGB, OKLab, OKLCH, OKHSV, OKHSL };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { convert, OKHSL, OKHSV, OKLab, OKLCH, RGBToHex, sRGB, deserialize, listColorSpaces } from '@texel/color';
|
|
2
|
+
export function okhsv_to_oklch(okhsv) {
|
|
3
|
+
const [l, c, h] = convert([okhsv.h, okhsv.s, okhsv.v], OKHSV, OKLCH);
|
|
4
|
+
return { l: l, c: c, h: h };
|
|
5
|
+
}
|
|
6
|
+
export function oklch_to_okhsv(oklch) {
|
|
7
|
+
const [h, s, v] = convert([oklch.h, oklch.c, oklch.l], OKLCH, OKHSV);
|
|
8
|
+
return { h: h ?? 0, s, v };
|
|
9
|
+
}
|
|
10
|
+
export function oklab_to_okhsv(oklab) {
|
|
11
|
+
const [h, s, v] = convert([oklab.l, oklab.a, oklab.b], OKLab, OKHSV);
|
|
12
|
+
return { h: h ?? 0, s, v };
|
|
13
|
+
}
|
|
14
|
+
export function okhsv_to_rgb(okhsv) {
|
|
15
|
+
const [r, g, b] = convert([okhsv.h, okhsv.s, okhsv.v], OKHSV, sRGB);
|
|
16
|
+
return { r, g, b };
|
|
17
|
+
}
|
|
18
|
+
export function okhsv_to_oklab(okhsv) {
|
|
19
|
+
const [l, a, b] = convert([okhsv.h, okhsv.s, okhsv.v], OKHSV, OKLab);
|
|
20
|
+
return { l, a, b };
|
|
21
|
+
}
|
|
22
|
+
export function rgb_to_okhsv(rgb) {
|
|
23
|
+
const [h, s, v] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKHSV);
|
|
24
|
+
return { h, s, v };
|
|
25
|
+
}
|
|
26
|
+
export function rgb_to_oklab(rgb) {
|
|
27
|
+
const [l, a, b] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKLab);
|
|
28
|
+
return { l, a, b };
|
|
29
|
+
}
|
|
30
|
+
export function okhsl_to_rgb(okhsl) {
|
|
31
|
+
const [r, g, b] = convert([okhsl.h, okhsl.s, okhsl.l], OKHSL, sRGB);
|
|
32
|
+
return { r, g, b };
|
|
33
|
+
}
|
|
34
|
+
export function oklab_to_oklch(oklab) {
|
|
35
|
+
const [l, c, h] = convert([oklab.l, oklab.a, oklab.b], OKLab, OKLCH);
|
|
36
|
+
return { l, c, h };
|
|
37
|
+
}
|
|
38
|
+
export function rgb_to_oklch(rgb) {
|
|
39
|
+
const [l, c, h] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKLCH);
|
|
40
|
+
return { l, c, h };
|
|
41
|
+
}
|
|
42
|
+
export function oklab_to_rgb(oklab) {
|
|
43
|
+
const [r, g, b] = convert([oklab.l, oklab.a, oklab.b], OKLab, sRGB);
|
|
44
|
+
return { r, g, b };
|
|
45
|
+
}
|
|
46
|
+
export function oklch_to_rgb(oklch) {
|
|
47
|
+
const [r, g, b] = convert([oklch.l, oklch.c, oklch.h], OKLCH, sRGB);
|
|
48
|
+
return { r, g, b };
|
|
49
|
+
}
|
|
50
|
+
export function oklch_string_to_oklch(oklch) {
|
|
51
|
+
const converted = oklch.split('oklch(')[1].split(')')[0].split(' ');
|
|
52
|
+
return { l: parseFloat(converted[0]), c: parseFloat(converted[1]), h: parseFloat(converted[2]) };
|
|
53
|
+
}
|
|
54
|
+
export function rgb_to_hex(rgb) {
|
|
55
|
+
const r = Math.round(rgb.r * 255);
|
|
56
|
+
const g = Math.round(rgb.g * 255);
|
|
57
|
+
const b = Math.round(rgb.b * 255);
|
|
58
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
59
|
+
}
|
|
60
|
+
export function hex_to_rgb(hex) {
|
|
61
|
+
if (hex.startsWith('#')) {
|
|
62
|
+
hex = hex.slice(1);
|
|
63
|
+
}
|
|
64
|
+
else if (hex.startsWith('0x')) {
|
|
65
|
+
hex = hex.slice(2);
|
|
66
|
+
}
|
|
67
|
+
if (hex.length !== 6) {
|
|
68
|
+
console.warn('Invalid hex color', hex);
|
|
69
|
+
return { r: 0, g: 0, b: 0 };
|
|
70
|
+
}
|
|
71
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
72
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
73
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
74
|
+
const r_ = r / 255;
|
|
75
|
+
const g_ = g / 255;
|
|
76
|
+
const b_ = b / 255;
|
|
77
|
+
return { r: r_, g: g_, b: b_ };
|
|
78
|
+
}
|
|
79
|
+
export function hex_to_okhsv(hex) {
|
|
80
|
+
const rgb = hex_to_rgb(hex);
|
|
81
|
+
return rgb_to_okhsv(rgb);
|
|
82
|
+
}
|
|
83
|
+
export function okhsv_to_hex(okhsv) {
|
|
84
|
+
const rgb = okhsv_to_rgb(okhsv);
|
|
85
|
+
return rgb_to_hex(rgb);
|
|
86
|
+
}
|
|
87
|
+
export function convertCSSToHex(css) {
|
|
88
|
+
const { id, coords } = deserialize(css);
|
|
89
|
+
const space = listColorSpaces().find((f) => id === f.id);
|
|
90
|
+
if (!space) {
|
|
91
|
+
throw new Error('Invalid color space');
|
|
92
|
+
}
|
|
93
|
+
const rgb = convert(coords, space, sRGB);
|
|
94
|
+
return RGBToHex(rgb);
|
|
95
|
+
}
|
|
96
|
+
export { deserialize, convert, listColorSpaces, sRGB, OKLab, OKLCH, OKHSV, OKHSL };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { f_shader, v_shader } from './shaders';
|
|
2
|
+
import { picker_size, slider_width } from './constants';
|
|
3
|
+
import { okhsl_to_rgb } from './color';
|
|
4
|
+
export function render_slider_image(canvas) {
|
|
5
|
+
const ctx = canvas.getContext('2d');
|
|
6
|
+
const data = new Uint8ClampedArray(picker_size * slider_width * 4);
|
|
7
|
+
for (let i = 0; i < picker_size; i++) {
|
|
8
|
+
const a_ = Math.cos((2 * Math.PI * i) / picker_size);
|
|
9
|
+
const b_ = Math.sin((2 * Math.PI * i) / picker_size);
|
|
10
|
+
const rgb = okhsl_to_rgb({
|
|
11
|
+
h: (i / picker_size) * 360,
|
|
12
|
+
s: 0.9,
|
|
13
|
+
l: 0.65 + 0.17 * b_ - 0.08 * a_
|
|
14
|
+
});
|
|
15
|
+
for (let j = 0; j < slider_width; j++) {
|
|
16
|
+
const index = 4 * (i * slider_width + j);
|
|
17
|
+
data[index + 0] = rgb.r * 255;
|
|
18
|
+
data[index + 1] = rgb.g * 255;
|
|
19
|
+
data[index + 2] = rgb.b * 255;
|
|
20
|
+
data[index + 3] = 255;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const imageData = new ImageData(data, slider_width);
|
|
24
|
+
ctx.putImageData(imageData, 0, 0);
|
|
25
|
+
}
|
|
26
|
+
export function render_main_image(canvas, hue) {
|
|
27
|
+
const gl = canvas.getContext('webgl');
|
|
28
|
+
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
29
|
+
const shaderProgram = initShaderProgram(gl, v_shader, f_shader);
|
|
30
|
+
const position = gl.getAttribLocation(shaderProgram, 'position');
|
|
31
|
+
const resolution = gl.getUniformLocation(shaderProgram, 'resolution');
|
|
32
|
+
const hue_rad = gl.getUniformLocation(shaderProgram, 'hue_rad');
|
|
33
|
+
gl.useProgram(shaderProgram);
|
|
34
|
+
gl.uniform2fv(resolution, [gl.canvas.width, gl.canvas.height]);
|
|
35
|
+
const positions = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];
|
|
36
|
+
const positionBuffer = gl.createBuffer();
|
|
37
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
38
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
|
39
|
+
gl.enableVertexAttribArray(position);
|
|
40
|
+
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
|
41
|
+
function update(hue) {
|
|
42
|
+
requestAnimationFrame(() => {
|
|
43
|
+
gl.uniform1f(hue_rad, hue / 360);
|
|
44
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
update(hue);
|
|
48
|
+
return {
|
|
49
|
+
update,
|
|
50
|
+
destroy() {
|
|
51
|
+
gl.deleteProgram(shaderProgram);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function initShaderProgram(gl, vsSource, fsSource) {
|
|
56
|
+
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
57
|
+
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
58
|
+
const shaderProgram = gl.createProgram();
|
|
59
|
+
gl.attachShader(shaderProgram, vertexShader);
|
|
60
|
+
gl.attachShader(shaderProgram, fragmentShader);
|
|
61
|
+
gl.linkProgram(shaderProgram);
|
|
62
|
+
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
|
63
|
+
throw `unable to init shader: ${gl.getProgramInfoLog(shaderProgram)}`;
|
|
64
|
+
}
|
|
65
|
+
return shaderProgram;
|
|
66
|
+
}
|
|
67
|
+
function loadShader(gl, type, source) {
|
|
68
|
+
const shader = gl.createShader(type);
|
|
69
|
+
gl.shaderSource(shader, source);
|
|
70
|
+
gl.compileShader(shader);
|
|
71
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
72
|
+
const info = gl.getShaderInfoLog(shader);
|
|
73
|
+
gl.deleteShader(shader);
|
|
74
|
+
throw `error compiling shaders: ${info}`;
|
|
75
|
+
}
|
|
76
|
+
return shader;
|
|
77
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
var _ = `precision mediump float;uniform vec2 resolution;uniform float hue_rad;precision mediump float;
|
|
2
|
+
#define M_PI 3.1415926536
|
|
3
|
+
float cbrt(float x){return sign(x)*pow(abs(x),1.0/3.0);}float srgb_transfer_function(float a){return .0031308>=a ? 12.92*a : 1.055*pow(a,.4166666666666667)-.055;}vec3 oklab_to_linear_srgb(vec3 c){float l_=c.x+0.3963377774*c.y+0.2158037573*c.z;float m_=c.x-0.1055613458*c.y-0.0638541728*c.z;float s_=c.x-0.0894841775*c.y-1.2914855480*c.z;float l=l_*l_*l_;float m=m_*m_*m_;float s=s_*s_*s_;return vec3(+4.0767416621*l-3.3077115913*m+0.2309699292*s,-1.2684380046*l+2.6097574011*m-0.3413193965*s,-0.0041960863*l-0.7034186147*m+1.7076147010*s);}float compute_max_saturation(float a,float b){float k0,k1,k2,k3,k4,wl,wm,ws;if(-1.88170328*a-0.80936493*b>1.0){k0=+1.19086277;k1=+1.76576728;k2=+0.59662641;k3=+0.75515197;k4=+0.56771245;wl=+4.0767416621;wm=-3.3077115913;ws=+0.2309699292;}else if(1.81444104*a-1.19445276*b>1.0){k0=+0.73956515;k1=-0.45954404;k2=+0.08285427;k3=+0.12541070;k4=+0.14503204;wl=-1.2684380046;wm=+2.6097574011;ws=-0.3413193965;}else{k0=+1.35733652;k1=-0.00915799;k2=-1.15130210;k3=-0.50559606;k4=+0.00692167;wl=-0.0041960863;wm=-0.7034186147;ws=+1.7076147010;}float S=k0+k1*a+k2*b+k3*a*a+k4*a*b;float k_l=+0.3963377774*a+0.2158037573*b;float k_m=-0.1055613458*a-0.0638541728*b;float k_s=-0.0894841775*a-1.2914855480*b;{float l_=1.0+S*k_l;float m_=1.0+S*k_m;float s_=1.0+S*k_s;float l=l_*l_*l_;float m=m_*m_*m_;float s=s_*s_*s_;float l_dS=3.0*k_l*l_*l_;float m_dS=3.0*k_m*m_*m_;float s_dS=3.0*k_s*s_*s_;float l_dS2=6.0*k_l*k_l*l_;float m_dS2=6.0*k_m*k_m*m_;float s_dS2=6.0*k_s*k_s*s_;float f=wl*l+wm*m+ws*s;float f1=wl*l_dS+wm*m_dS+ws*s_dS;float f2=wl*l_dS2+wm*m_dS2+ws*s_dS2;S=S-f*f1/(f1*f1-0.5*f*f2);}return S;}vec2 find_cusp(float a,float b){float S_cusp=compute_max_saturation(a,b);vec3 rgb_at_max=oklab_to_linear_srgb(vec3(1,S_cusp*a,S_cusp*b));float L_cusp=cbrt(1.0/max(max(rgb_at_max.r,rgb_at_max.g),rgb_at_max.b));float C_cusp=L_cusp*S_cusp;return vec2(L_cusp,C_cusp);}float toe_inv(float x){float k_1=0.206;float k_2=0.03;float k_3=(1.0+k_1)/(1.0+k_2);return(x*x+k_1*x)/(k_3*(x+k_2));}vec2 to_ST(vec2 cusp){float L=cusp.x;float C=cusp.y;return vec2(C/L,C/(1.0-L));}vec3 okhsv_to_srgb(vec3 hsv){float h=hsv.x;float s=hsv.y;float v=hsv.z;float a_=cos(2.0*M_PI*h);float b_=sin(2.0*M_PI*h);vec2 cusp=find_cusp(a_,b_);vec2 ST_max=to_ST(cusp);float S_max=ST_max.x;float T_max=ST_max.y;float S_0=0.5;float k=1.0-S_0/S_max;float L_v=1.0-s*S_0/(S_0+T_max-T_max*k*s);float C_v=s*T_max*S_0/(S_0+T_max-T_max*k*s);float L=v*L_v;float C=v*C_v;float L_vt=toe_inv(L_v);float C_vt=C_v*L_vt/L_v;float L_new=toe_inv(L);C=C*L_new/L;L=L_new;vec3 rgb_scale=oklab_to_linear_srgb(vec3(L_vt,a_*C_vt,b_*C_vt));float scale_L=cbrt(1.0/max(max(rgb_scale.r,rgb_scale.g),max(rgb_scale.b,0.0)));L=L*scale_L;C=C*scale_L;vec3 rgb=oklab_to_linear_srgb(vec3(L,C*a_,C*b_));return vec3(srgb_transfer_function(rgb.r),srgb_transfer_function(rgb.g),srgb_transfer_function(rgb.b));}void main(){vec2 uv=gl_FragCoord.xy/resolution;float l=uv.y;float h=hue_rad;vec3 hsl=vec3(h,uv.x,uv.y);vec3 hsvRGB=okhsv_to_srgb(hsl);gl_FragColor=vec4(hsvRGB,1.0);}`, a = "attribute vec4 position;void main(){gl_Position=position;}";
|
|
4
|
+
const l = _, t = a;
|
|
5
|
+
export {
|
|
6
|
+
l as f_shader,
|
|
7
|
+
t as v_shader
|
|
8
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, Popover } from '@foxui/core';
|
|
3
|
+
|
|
4
|
+
import { ColorPicker, type OKhsv, type OKlab, type RGB } from '../base';
|
|
5
|
+
|
|
6
|
+
import { okhsv_to_rgb, oklab_to_rgb, type OKlch } from '../base/color';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
rgb = $bindable(),
|
|
10
|
+
oklab = $bindable(),
|
|
11
|
+
okhsv = $bindable(),
|
|
12
|
+
sideOffset = 10,
|
|
13
|
+
side = 'bottom',
|
|
14
|
+
class: className,
|
|
15
|
+
onchange,
|
|
16
|
+
quickSelects = $bindable([]),
|
|
17
|
+
...restProps
|
|
18
|
+
}: {
|
|
19
|
+
rgb?: RGB;
|
|
20
|
+
oklab?: OKlab;
|
|
21
|
+
okhsv?: OKhsv;
|
|
22
|
+
class?: string;
|
|
23
|
+
sideOffset?: number;
|
|
24
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
25
|
+
onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void;
|
|
26
|
+
quickSelects?: {
|
|
27
|
+
label: string;
|
|
28
|
+
rgb?: RGB;
|
|
29
|
+
oklab?: OKlab;
|
|
30
|
+
okhsv?: OKhsv;
|
|
31
|
+
}[];
|
|
32
|
+
} = $props();
|
|
33
|
+
|
|
34
|
+
let internalColor = $derived(convertToInternal(rgb, oklab, okhsv));
|
|
35
|
+
|
|
36
|
+
function convertToInternal(
|
|
37
|
+
rgb: RGB | undefined,
|
|
38
|
+
oklab: OKlab | undefined,
|
|
39
|
+
okhsv: OKhsv | undefined
|
|
40
|
+
): RGB {
|
|
41
|
+
if (okhsv) {
|
|
42
|
+
return okhsv_to_rgb(okhsv);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (oklab) {
|
|
46
|
+
return oklab_to_rgb(oklab);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (rgb) {
|
|
50
|
+
return rgb;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw 'rgb, oklab, or okhsv required';
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<Popover {side} {sideOffset} class="p-1 pl-2 pr-0">
|
|
58
|
+
{#snippet child({ props })}
|
|
59
|
+
<button
|
|
60
|
+
{...props}
|
|
61
|
+
class={cn(
|
|
62
|
+
'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-full focus-visible:outline-2 focus-visible:outline-offset-2',
|
|
63
|
+
'group'
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
class="border-base-300 dark:border-base-700 focus-visible:outline-accent-500 z-10 size-8 rounded-full border group-hover:scale-105 group-active:scale-95 transition-all duration-100"
|
|
68
|
+
style={`background-color: rgb(${internalColor.r * 255}, ${internalColor.g * 255}, ${internalColor.b * 255});`}
|
|
69
|
+
></div>
|
|
70
|
+
<span class="sr-only">Pick a color</span>
|
|
71
|
+
</button>
|
|
72
|
+
{/snippet}
|
|
73
|
+
|
|
74
|
+
<ColorPicker bind:rgb bind:oklab bind:okhsv class={className} {onchange} {quickSelects} {...restProps} />
|
|
75
|
+
</Popover>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type OKhsv, type OKlab, type RGB } from '../base';
|
|
2
|
+
import { type OKlch } from '../base/color';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
rgb?: RGB;
|
|
5
|
+
oklab?: OKlab;
|
|
6
|
+
okhsv?: OKhsv;
|
|
7
|
+
class?: string;
|
|
8
|
+
sideOffset?: number;
|
|
9
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
10
|
+
onchange?: (color: {
|
|
11
|
+
hex: string;
|
|
12
|
+
rgb: RGB;
|
|
13
|
+
oklab: OKlab;
|
|
14
|
+
okhsv: OKhsv;
|
|
15
|
+
oklch: OKlch;
|
|
16
|
+
}) => void;
|
|
17
|
+
quickSelects?: {
|
|
18
|
+
label: string;
|
|
19
|
+
rgb?: RGB;
|
|
20
|
+
oklab?: OKlab;
|
|
21
|
+
okhsv?: OKhsv;
|
|
22
|
+
}[];
|
|
23
|
+
};
|
|
24
|
+
declare const PopoverColorPicker: import("svelte").Component<$$ComponentProps, {}, "rgb" | "oklab" | "okhsv" | "quickSelects">;
|
|
25
|
+
type PopoverColorPicker = ReturnType<typeof PopoverColorPicker>;
|
|
26
|
+
export default PopoverColorPicker;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { cn } from '@foxui/core';
|
|
5
|
+
|
|
6
|
+
type Color = { class?: string; label: string; value?: string } | string;
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
ref = $bindable(null),
|
|
10
|
+
|
|
11
|
+
class: className,
|
|
12
|
+
colorsClass,
|
|
13
|
+
colors = $bindable([]),
|
|
14
|
+
selected = $bindable(colors[0]),
|
|
15
|
+
|
|
16
|
+
name = $bindable(crypto.randomUUID()),
|
|
17
|
+
|
|
18
|
+
onselected,
|
|
19
|
+
...restProps
|
|
20
|
+
}: WithElementRef<WithoutChildrenOrChild<HTMLAttributes<HTMLDivElement>>> & {
|
|
21
|
+
colors: Color[];
|
|
22
|
+
|
|
23
|
+
selected?: Color;
|
|
24
|
+
|
|
25
|
+
colorsClass?: string;
|
|
26
|
+
|
|
27
|
+
name?: string;
|
|
28
|
+
|
|
29
|
+
onselected?: (color: Color, previous: Color) => void;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
function getLabel(c: Color) {
|
|
33
|
+
if (typeof c === 'string') return c;
|
|
34
|
+
return c.label;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getValue(c: Color) {
|
|
38
|
+
if (typeof c === 'string') return c;
|
|
39
|
+
return c.value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getColorClass(c: Color) {
|
|
43
|
+
if (typeof c === 'string') return '';
|
|
44
|
+
return c.class;
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<div
|
|
49
|
+
class={cn('group flex flex-wrap items-center gap-2', className)}
|
|
50
|
+
{...restProps}
|
|
51
|
+
bind:this={ref}
|
|
52
|
+
>
|
|
53
|
+
{#each colors as color}
|
|
54
|
+
<label
|
|
55
|
+
aria-label={getLabel(color)}
|
|
56
|
+
class={cn(
|
|
57
|
+
'group relative flex cursor-pointer items-center justify-center rounded-full p-0.5 ring-current',
|
|
58
|
+
'has-focus-visible:outline-base-900 dark:has-focus-visible:outline-base-100 has-focus-visible:outline-2 has-focus-visible:outline-offset-0',
|
|
59
|
+
'has-checked:ring-3 hover:scale-105 active:scale-95 transition-all duration-100',
|
|
60
|
+
colorsClass,
|
|
61
|
+
getColorClass(color)
|
|
62
|
+
)}
|
|
63
|
+
style={getValue(color) ? `color: ${getValue(color)}` : undefined}
|
|
64
|
+
>
|
|
65
|
+
<input
|
|
66
|
+
type="radio"
|
|
67
|
+
{name}
|
|
68
|
+
value={getLabel(color)}
|
|
69
|
+
class="sr-only"
|
|
70
|
+
onchange={() => {
|
|
71
|
+
let previous = selected;
|
|
72
|
+
selected = color;
|
|
73
|
+
|
|
74
|
+
if (onselected) onselected(color, previous);
|
|
75
|
+
}}
|
|
76
|
+
checked={getLabel(selected) === getLabel(color)}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<span aria-hidden="true" class="size-8 rounded-full bg-current"></span>
|
|
80
|
+
</label>
|
|
81
|
+
{/each}
|
|
82
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Color = {
|
|
4
|
+
class?: string;
|
|
5
|
+
label: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
} | string;
|
|
8
|
+
type $$ComponentProps = WithElementRef<WithoutChildrenOrChild<HTMLAttributes<HTMLDivElement>>> & {
|
|
9
|
+
colors: Color[];
|
|
10
|
+
selected?: Color;
|
|
11
|
+
colorsClass?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
onselected?: (color: Color, previous: Color) => void;
|
|
14
|
+
};
|
|
15
|
+
declare const ColorSelect: import("svelte").Component<$$ComponentProps, {}, "colors" | "ref" | "name" | "selected">;
|
|
16
|
+
type ColorSelect = ReturnType<typeof ColorSelect>;
|
|
17
|
+
export default ColorSelect;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ColorSelect } from './ColorSelect.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ColorSelect } from './ColorSelect.svelte';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
let accentColor = $state({
|
|
3
|
+
class: '',
|
|
4
|
+
label: ''
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
let baseColor = $state({
|
|
8
|
+
class: '',
|
|
9
|
+
label: ''
|
|
10
|
+
});
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import { Paragraph } from '@foxui/core';
|
|
15
|
+
import { ColorSelect } from '../';
|
|
16
|
+
import { onMount } from 'svelte';
|
|
17
|
+
|
|
18
|
+
let accentColors = [
|
|
19
|
+
{ class: 'text-red-500', label: 'red' },
|
|
20
|
+
{ class: 'text-orange-500', label: 'orange' },
|
|
21
|
+
{ class: 'text-amber-500', label: 'amber' },
|
|
22
|
+
{ class: 'text-yellow-500', label: 'yellow' },
|
|
23
|
+
{ class: 'text-lime-500', label: 'lime' },
|
|
24
|
+
{ class: 'text-green-500', label: 'green' },
|
|
25
|
+
{ class: 'text-emerald-500', label: 'emerald' },
|
|
26
|
+
{ class: 'text-teal-500', label: 'teal' },
|
|
27
|
+
{ class: 'text-cyan-500', label: 'cyan' },
|
|
28
|
+
{ class: 'text-sky-500', label: 'sky' },
|
|
29
|
+
{ class: 'text-blue-500', label: 'blue' },
|
|
30
|
+
{ class: 'text-indigo-500', label: 'indigo' },
|
|
31
|
+
{ class: 'text-violet-500', label: 'violet' },
|
|
32
|
+
{ class: 'text-purple-500', label: 'purple' },
|
|
33
|
+
{ class: 'text-fuchsia-500', label: 'fuchsia' },
|
|
34
|
+
{ class: 'text-pink-500', label: 'pink' },
|
|
35
|
+
{ class: 'text-rose-500', label: 'rose' }
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
let baseColors = [
|
|
39
|
+
{ class: 'text-gray-500', label: 'gray' },
|
|
40
|
+
{ class: 'text-stone-500', label: 'stone' },
|
|
41
|
+
{ class: 'text-zinc-500', label: 'zinc' },
|
|
42
|
+
{ class: 'text-neutral-500', label: 'neutral' },
|
|
43
|
+
{ class: 'text-slate-500', label: 'slate' }
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
let {
|
|
47
|
+
defaultAccentColor = 'pink',
|
|
48
|
+
defaultBaseColor = 'stone',
|
|
49
|
+
selectAccentColor = true,
|
|
50
|
+
selectBaseColor = true,
|
|
51
|
+
onchanged
|
|
52
|
+
}: {
|
|
53
|
+
defaultAccentColor?: string;
|
|
54
|
+
defaultBaseColor?: string;
|
|
55
|
+
selectAccentColor?: boolean;
|
|
56
|
+
selectBaseColor?: boolean;
|
|
57
|
+
onchanged?: (accentColor: string, baseColor: string) => void;
|
|
58
|
+
} = $props();
|
|
59
|
+
|
|
60
|
+
onMount(() => {
|
|
61
|
+
let savedAccentColor = localStorage.getItem('accentColor');
|
|
62
|
+
if (savedAccentColor) {
|
|
63
|
+
accentColor.label = JSON.parse(savedAccentColor);
|
|
64
|
+
} else {
|
|
65
|
+
accentColor.label = defaultAccentColor;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let savedBaseColor = localStorage.getItem('baseColor');
|
|
69
|
+
if (savedBaseColor) {
|
|
70
|
+
baseColor.label = JSON.parse(savedBaseColor);
|
|
71
|
+
} else {
|
|
72
|
+
baseColor.label = defaultBaseColor;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
{#if selectAccentColor}
|
|
78
|
+
<Paragraph class="mb-2">Accent Color</Paragraph>
|
|
79
|
+
<ColorSelect
|
|
80
|
+
bind:selected={accentColor}
|
|
81
|
+
colors={accentColors}
|
|
82
|
+
onselected={(color, previous) => {
|
|
83
|
+
if (typeof previous === 'string' || typeof color === 'string') {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
document.documentElement.classList.remove(previous.label.toLowerCase());
|
|
88
|
+
document.documentElement.classList.add(color.label.toLowerCase());
|
|
89
|
+
|
|
90
|
+
localStorage.setItem('accentColor', JSON.stringify(color.label));
|
|
91
|
+
|
|
92
|
+
window.dispatchEvent(
|
|
93
|
+
new CustomEvent('theme-changed', { detail: { accentColor: color.label } })
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
onchanged?.(color.label, previous.label);
|
|
97
|
+
}}
|
|
98
|
+
class="w-64"
|
|
99
|
+
/>
|
|
100
|
+
{/if}
|
|
101
|
+
|
|
102
|
+
{#if selectBaseColor}
|
|
103
|
+
<Paragraph class="mb-2 mt-4">Base Color</Paragraph>
|
|
104
|
+
<ColorSelect
|
|
105
|
+
bind:selected={baseColor}
|
|
106
|
+
colors={baseColors}
|
|
107
|
+
onselected={(color, previous) => {
|
|
108
|
+
if (typeof previous === 'string' || typeof color === 'string') {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
document.documentElement.classList.remove(previous.label.toLowerCase());
|
|
113
|
+
document.documentElement.classList.add(color.label.toLowerCase());
|
|
114
|
+
|
|
115
|
+
localStorage.setItem('baseColor', JSON.stringify(color.label));
|
|
116
|
+
|
|
117
|
+
window.dispatchEvent(
|
|
118
|
+
new CustomEvent('theme-changed', { detail: { baseColor: color.label } })
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
onchanged?.(color.label, previous.label);
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
defaultAccentColor?: string;
|
|
3
|
+
defaultBaseColor?: string;
|
|
4
|
+
selectAccentColor?: boolean;
|
|
5
|
+
selectBaseColor?: boolean;
|
|
6
|
+
onchanged?: (accentColor: string, baseColor: string) => void;
|
|
7
|
+
};
|
|
8
|
+
declare const SelectTheme: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type SelectTheme = ReturnType<typeof SelectTheme>;
|
|
10
|
+
export default SelectTheme;
|