Compare commits

...

22 Commits

Author SHA1 Message Date
徐涛
2ddffe1b12 调整涉及WASM内结构体的实例化位置。 2025-02-10 16:43:30 +08:00
徐涛
3eae8116e7 更改WASM的引入方式。 2025-02-10 16:17:59 +08:00
徐涛
12e76b658e 调整首页描述。 2025-02-10 14:47:06 +08:00
徐涛
2ec3578e1c 修复或屏蔽编译错误。 2025-02-10 14:42:17 +08:00
徐涛
f944d48e1b 增加部分结构的构造函数。 2025-02-10 14:31:32 +08:00
徐涛
88e3d1f928 修正大部分的编译错误。 2025-02-10 14:28:34 +08:00
徐涛
2144cd548a 修正Vite设置。 2025-02-10 12:22:43 +08:00
徐涛
2c47369772 基本完成M2 Scheme的预览。 2025-02-10 09:51:15 +08:00
徐涛
2f51a80c91 调整M2 Scheme中Surface颜色的生成。 2025-02-10 09:39:39 +08:00
徐涛
546ca97b10 基本完成M2 Scheme的构建功能。 2025-02-10 08:38:40 +08:00
徐涛
6b262f536d 调整M3 Scheme部分内容的预览展示。 2025-02-08 17:17:56 +08:00
徐涛
6aa3875919 修正一处书写错误。 2025-02-08 16:43:32 +08:00
徐涛
0369f238f2 将M3 Scheme的颜色生成从Cam16Jch重构为Lch。 2025-02-08 16:38:51 +08:00
徐涛
c60aefaaff 微调M3 Scheme生成参数。 2025-02-08 15:28:03 +08:00
徐涛
7bfe9a7620 基本完成M3 Scheme的生成功能。 2025-02-08 15:17:04 +08:00
徐涛
1b044c66d7 调整M3 Scheme预览中色块的布局。 2025-02-08 15:16:18 +08:00
徐涛
e74ffc9721 基本完成M3 Scheme的预览页面。 2025-02-08 15:10:56 +08:00
徐涛
1553c51621 完成M3 Scheme构建页面的功能。 2025-02-08 13:54:12 +08:00
徐涛
14d775e956 修正M3 Scheme的生成算法。 2025-02-08 13:49:58 +08:00
徐涛
e2806a0cc5 修正Cam16Jch到sRGB的转换算法。 2025-02-08 13:34:55 +08:00
徐涛
5d3fc2903b 提取用来录入自定义颜色的公共ColorEntry组件。 2025-02-08 10:11:02 +08:00
徐涛
71feeb4efc 放置默认的M3 Scheme和M2 Scheme的主要切换页面。 2025-02-07 22:59:35 +08:00
83 changed files with 1646 additions and 3419 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
wasm-pack build --release --target web -d ../src/color_functions wasm-pack build --release --target web -d ../color_functions

View File

@@ -12,6 +12,18 @@ pub struct HctDiffference {
pub lightness: Differ, pub lightness: Differ,
} }
#[wasm_bindgen]
impl HctDiffference {
#[wasm_bindgen(constructor)]
pub fn new(hue: Differ, chroma: Differ, lightness: Differ) -> Self {
Self {
hue,
chroma,
lightness,
}
}
}
impl ColorDifference for Cam16Jch<f32> { impl ColorDifference for Cam16Jch<f32> {
type Difference = HctDiffference; type Difference = HctDiffference;

View File

@@ -12,6 +12,18 @@ pub struct HSLDifference {
pub lightness: Differ, pub lightness: Differ,
} }
#[wasm_bindgen]
impl HSLDifference {
#[wasm_bindgen(constructor)]
pub fn new(hue: Differ, saturation: Differ, lightness: Differ) -> Self {
Self {
hue,
saturation,
lightness,
}
}
}
impl ColorDifference for Hsl { impl ColorDifference for Hsl {
type Difference = HSLDifference; type Difference = HSLDifference;

View File

@@ -13,6 +13,14 @@ pub struct Differ {
pub percent: f32, pub percent: f32,
} }
#[wasm_bindgen]
impl Differ {
#[wasm_bindgen(constructor)]
pub fn new(delta: f32, percent: f32) -> Self {
Self { delta, percent }
}
}
pub trait ColorDifference { pub trait ColorDifference {
type Difference; type Difference;

View File

@@ -12,6 +12,18 @@ pub struct OklchDifference {
pub lightness: Differ, pub lightness: Differ,
} }
#[wasm_bindgen]
impl OklchDifference {
#[wasm_bindgen(constructor)]
pub fn new(hue: Differ, chroma: Differ, lightness: Differ) -> Self {
Self {
hue,
chroma,
lightness,
}
}
}
impl ColorDifference for Oklch { impl ColorDifference for Oklch {
type Difference = OklchDifference; type Difference = OklchDifference;

View File

@@ -12,6 +12,14 @@ pub struct RGBDifference {
pub b: Differ, pub b: Differ,
} }
#[wasm_bindgen]
impl RGBDifference {
#[wasm_bindgen(constructor)]
pub fn new(r: Differ, g: Differ, b: Differ) -> Self {
Self { r, g, b }
}
}
impl ColorDifference for Srgb { impl ColorDifference for Srgb {
type Difference = RGBDifference; type Difference = RGBDifference;

View File

@@ -2,30 +2,63 @@ use palette::{
cam16::{Cam16Jch, Parameters}, cam16::{Cam16Jch, Parameters},
convert::FromColorUnclamped, convert::FromColorUnclamped,
luma::Luma, luma::Luma,
Hsl, IntoColor, IsWithinBounds, Oklab, Oklch, Srgb, Hsl, IntoColor, IsWithinBounds, Lch, Lchuv, Oklab, Oklch, Srgb,
}; };
#[allow(dead_code)]
pub fn map_cam16jch_to_srgb(origin: &Cam16Jch<f32>) -> Srgb { pub fn map_cam16jch_to_srgb(origin: &Cam16Jch<f32>) -> Srgb {
let mut new_original = Cam16Jch::new(origin.lightness, origin.chroma, origin.hue); let original_xyz = origin.into_xyz(Parameters::default_static_wp(40.0));
const FACTOR: f32 = 0.99; let mut new_srgb = Srgb::from_color_unclamped(original_xyz);
loop {
let new_srgb =
Srgb::from_color_unclamped(new_original.into_xyz(Parameters::default_static_wp(40.0)));
if new_srgb.is_within_bounds() { if new_srgb.is_within_bounds() {
return new_srgb;
}
let lchuv: Lchuv = Lchuv::from_color_unclamped(original_xyz);
let mut c: f32 = lchuv.chroma;
let original_c = c;
let h = lchuv.hue;
let l = lchuv.l;
loop {
let new_lchuv = Lchuv::new(l, c, h);
new_srgb = new_lchuv.into_color();
c -= original_c / 1000.0;
if c > 0.0 && (new_srgb.is_within_bounds()) {
break new_srgb; break new_srgb;
} }
new_original = Cam16Jch::new(
new_original.lightness,
new_original.chroma * FACTOR,
new_original.hue,
);
} }
} }
#[allow(dead_code)]
pub fn map_cam16jch_to_srgb_hex(origin: &Cam16Jch<f32>) -> String { pub fn map_cam16jch_to_srgb_hex(origin: &Cam16Jch<f32>) -> String {
format!("{:x}", map_cam16jch_to_srgb(origin).into_format::<u8>()) format!("{:x}", map_cam16jch_to_srgb(origin).into_format::<u8>())
} }
pub fn map_lch_to_srgb(origin: &Lch) -> Srgb {
let mut new_srgb: Srgb = (*origin).into_color();
if new_srgb.is_within_bounds() {
return new_srgb;
}
let mut c: f32 = origin.chroma;
let original_c = c;
let h = origin.hue;
let l = origin.l;
loop {
let new_lch = Lch::new(l, c, h);
new_srgb = new_lch.into_color();
c -= original_c / 1000.0;
if c > 0.0 && (new_srgb.is_within_bounds()) {
break new_srgb;
}
}
}
pub fn map_lch_to_srgb_hex(origin: &Lch) -> String {
format!("{:x}", map_lch_to_srgb(origin).into_format::<u8>())
}
pub fn map_hsl_to_srgb(origin: &Hsl) -> Srgb { pub fn map_hsl_to_srgb(origin: &Hsl) -> Srgb {
let mut new_original = Hsl::new(origin.hue, origin.saturation, origin.lightness); let mut new_original = Hsl::new(origin.hue, origin.saturation, origin.lightness);
const FACTOR: f32 = 0.99; const FACTOR: f32 = 0.99;

View File

@@ -11,6 +11,19 @@ pub struct MixReversing {
pub average: f32, pub average: f32,
} }
#[wasm_bindgen]
impl MixReversing {
#[wasm_bindgen(constructor)]
pub fn new(r_factor: f32, g_factor: f32, b_factor: f32, average: f32) -> Self {
Self {
r_factor,
g_factor,
b_factor,
average,
}
}
}
impl MixReversing { impl MixReversing {
pub fn from_tint_rgb(basic_color: Rgb, mixed_result: Rgb) -> Self { pub fn from_tint_rgb(basic_color: Rgb, mixed_result: Rgb) -> Self {
let r_factor = if basic_color.red == 1.0 { let r_factor = if basic_color.red == 1.0 {

View File

@@ -45,18 +45,8 @@ impl M2BaselineColors {
None, None,
)?, )?,
error: M2ColorSet::from_swatch(&error_swatch, &neutral_swatch, dark_baseline, None)?, error: M2ColorSet::from_swatch(&error_swatch, &neutral_swatch, dark_baseline, None)?,
background: M2ColorSet::from_swatch( background: M2ColorSet::surface(&neutral_swatch, dark_baseline)?,
&neutral_swatch, surface: M2ColorSet::surface(&neutral_swatch, dark_baseline)?,
&neutral_swatch,
dark_baseline,
None,
)?,
surface: M2ColorSet::from_swatch(
&neutral_swatch,
&neutral_swatch,
dark_baseline,
None,
)?,
shadow: map_hsl_to_srgb_hex(&neutral_swatch.tone(SwatchIndex::SI900)), shadow: map_hsl_to_srgb_hex(&neutral_swatch.tone(SwatchIndex::SI900)),
custom_colors: HashMap::new(), custom_colors: HashMap::new(),
neutral_swatch, neutral_swatch,

View File

@@ -48,6 +48,27 @@ impl M2ColorSet {
} }
} }
pub fn surface(neutral: &M2Swatch, dark: bool) -> Result<Self, errors::ColorError> {
let root_color_index = if dark {
super::swatch::SwatchIndex::SI900
} else {
super::swatch::SwatchIndex::SI50
};
if dark {
Ok(Self {
root: map_hsl_to_srgb_hex(&neutral.desaturated_tone(root_color_index)),
variant: map_hsl_to_srgb_hex(&neutral.desaturated_tone(root_color_index + 2)),
on: map_hsl_to_srgb_hex(&neutral.tone(super::swatch::SwatchIndex::SI50)),
})
} else {
Ok(Self {
root: map_hsl_to_srgb_hex(&neutral.tone(root_color_index)),
variant: map_hsl_to_srgb_hex(&neutral.tone(root_color_index - 2)),
on: map_hsl_to_srgb_hex(&neutral.tone(super::swatch::SwatchIndex::SI900)),
})
}
}
pub fn to_css_variable(&self, prefix: &str, name: &str) -> Vec<String> { pub fn to_css_variable(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new(); let mut variable_lines = Vec::new();

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use serde::Serialize; use serde::Serialize;
use crate::convert::map_cam16jch_to_srgb_hex; use crate::convert::map_lch_to_srgb_hex;
use super::{color_set::M3ColorSet, surface::M3SurfaceSet, tonal_palette::TonalPalette}; use super::{color_set::M3ColorSet, surface::M3SurfaceSet, tonal_palette::TonalPalette};
@@ -16,7 +16,7 @@ pub struct M3BaselineColors {
pub outline: String, pub outline: String,
pub outline_variant: String, pub outline_variant: String,
pub scrim: String, pub scrim: String,
pub shadown: String, pub shadow: String,
pub customs: HashMap<String, M3ColorSet>, pub customs: HashMap<String, M3ColorSet>,
dark_set: bool, dark_set: bool,
} }
@@ -62,10 +62,10 @@ impl M3BaselineColors {
tertiary, tertiary,
error, error,
surface, surface,
outline: map_cam16jch_to_srgb_hex(&outline), outline: map_lch_to_srgb_hex(&outline),
outline_variant: map_cam16jch_to_srgb_hex(&outline_variant), outline_variant: map_lch_to_srgb_hex(&outline_variant),
scrim: map_cam16jch_to_srgb_hex(&scrim), scrim: map_lch_to_srgb_hex(&scrim),
shadown: map_cam16jch_to_srgb_hex(&shadow), shadow: map_lch_to_srgb_hex(&shadow),
customs: HashMap::new(), customs: HashMap::new(),
dark_set, dark_set,
} }
@@ -95,7 +95,7 @@ impl M3BaselineColors {
prefix, self.outline_variant prefix, self.outline_variant
)); ));
css_variables.push(format!("--color-{}-scrim: #{};", prefix, self.scrim)); css_variables.push(format!("--color-{}-scrim: #{};", prefix, self.scrim));
css_variables.push(format!("--color-{}-shadow: #{};", prefix, self.shadown)); css_variables.push(format!("--color-{}-shadow: #{};", prefix, self.shadow));
for (name, color_set) in &self.customs { for (name, color_set) in &self.customs {
css_variables.extend(color_set.to_css_variables(prefix, name)); css_variables.extend(color_set.to_css_variables(prefix, name));
} }
@@ -118,7 +118,7 @@ impl M3BaselineColors {
prefix, self.outline_variant prefix, self.outline_variant
)); ));
scss_variables.push(format!("$color-{}-scrim: #{};", prefix, self.scrim)); scss_variables.push(format!("$color-{}-scrim: #{};", prefix, self.scrim));
scss_variables.push(format!("$color-{}-shadow: #{};", prefix, self.shadown)); scss_variables.push(format!("$color-{}-shadow: #{};", prefix, self.shadow));
for (name, color_set) in &self.customs { for (name, color_set) in &self.customs {
scss_variables.extend(color_set.to_scss_variables(prefix, name)); scss_variables.extend(color_set.to_scss_variables(prefix, name));
} }
@@ -147,7 +147,7 @@ impl M3BaselineColors {
prefix, self.outline_variant prefix, self.outline_variant
)); ));
js_object_fields.push(format!("{}Scrim: '#{}',", prefix, self.scrim)); js_object_fields.push(format!("{}Scrim: '#{}',", prefix, self.scrim));
js_object_fields.push(format!("{}Shadow: '#{}',", prefix, self.shadown)); js_object_fields.push(format!("{}Shadow: '#{}',", prefix, self.shadow));
for (name, color_set) in &self.customs { for (name, color_set) in &self.customs {
js_object_fields.extend(color_set.to_javascript_object_fields(prefix, name)); js_object_fields.extend(color_set.to_javascript_object_fields(prefix, name));
} }

View File

@@ -1,6 +1,6 @@
use serde::Serialize; use serde::Serialize;
use crate::convert::map_cam16jch_to_srgb_hex; use crate::convert::map_lch_to_srgb_hex;
use super::tonal_palette::TonalPalette; use super::tonal_palette::TonalPalette;
@@ -9,7 +9,7 @@ pub struct M3ColorSet {
pub root: String, pub root: String,
pub on_root: String, pub on_root: String,
pub container: String, pub container: String,
pub on_conatiner: String, pub on_container: String,
pub fixed: String, pub fixed: String,
pub fixed_dim: String, pub fixed_dim: String,
pub on_fixed: String, pub on_fixed: String,
@@ -30,15 +30,15 @@ impl M3ColorSet {
let inverse = palette.tone(80.0); let inverse = palette.tone(80.0);
Self { Self {
root: map_cam16jch_to_srgb_hex(&root), root: map_lch_to_srgb_hex(&root),
on_root: map_cam16jch_to_srgb_hex(&on_root), on_root: map_lch_to_srgb_hex(&on_root),
container: map_cam16jch_to_srgb_hex(&container), container: map_lch_to_srgb_hex(&container),
on_conatiner: map_cam16jch_to_srgb_hex(&on_container), on_container: map_lch_to_srgb_hex(&on_container),
fixed: map_cam16jch_to_srgb_hex(&fixed), fixed: map_lch_to_srgb_hex(&fixed),
fixed_dim: map_cam16jch_to_srgb_hex(&fixed_dim), fixed_dim: map_lch_to_srgb_hex(&fixed_dim),
on_fixed: map_cam16jch_to_srgb_hex(&on_fixed), on_fixed: map_lch_to_srgb_hex(&on_fixed),
fixed_variant: map_cam16jch_to_srgb_hex(&fixed_variant), fixed_variant: map_lch_to_srgb_hex(&fixed_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse), inverse: map_lch_to_srgb_hex(&inverse),
} }
} }
@@ -54,15 +54,15 @@ impl M3ColorSet {
let inverse = palette.tone(40.0); let inverse = palette.tone(40.0);
Self { Self {
root: map_cam16jch_to_srgb_hex(&root), root: map_lch_to_srgb_hex(&root),
on_root: map_cam16jch_to_srgb_hex(&on_root), on_root: map_lch_to_srgb_hex(&on_root),
container: map_cam16jch_to_srgb_hex(&container), container: map_lch_to_srgb_hex(&container),
on_conatiner: map_cam16jch_to_srgb_hex(&on_container), on_container: map_lch_to_srgb_hex(&on_container),
fixed: map_cam16jch_to_srgb_hex(&fixed), fixed: map_lch_to_srgb_hex(&fixed),
fixed_dim: map_cam16jch_to_srgb_hex(&fixed_dim), fixed_dim: map_lch_to_srgb_hex(&fixed_dim),
on_fixed: map_cam16jch_to_srgb_hex(&on_fixed), on_fixed: map_lch_to_srgb_hex(&on_fixed),
fixed_variant: map_cam16jch_to_srgb_hex(&fixed_variant), fixed_variant: map_lch_to_srgb_hex(&fixed_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse), inverse: map_lch_to_srgb_hex(&inverse),
} }
} }
@@ -80,7 +80,7 @@ impl M3ColorSet {
)); ));
variable_lines.push(format!( variable_lines.push(format!(
"--color-{}-on-{}-container: #{};", "--color-{}-on-{}-container: #{};",
prefix, name, self.on_conatiner prefix, name, self.on_container
)); ));
variable_lines.push(format!( variable_lines.push(format!(
"--color-{}-{}-fixed: #{};", "--color-{}-{}-fixed: #{};",
@@ -117,7 +117,7 @@ impl M3ColorSet {
)); ));
variable_lines.push(format!( variable_lines.push(format!(
"$color-{}-on-{}-container: #{};", "$color-{}-on-{}-container: #{};",
prefix, name, self.on_conatiner prefix, name, self.on_container
)); ));
variable_lines.push(format!( variable_lines.push(format!(
"$color-{}-{}-fixed: #{};", "$color-{}-{}-fixed: #{};",
@@ -162,7 +162,7 @@ impl M3ColorSet {
)); ));
variable_lines.push(format!( variable_lines.push(format!(
"{}On{}Container: '#{}',", "{}On{}Container: '#{}',",
prefix, name, self.on_conatiner prefix, name, self.on_container
)); ));
variable_lines.push(format!("{}{}Fixed: '#{}',", prefix, name, self.fixed)); variable_lines.push(format!("{}{}Fixed: '#{}',", prefix, name, self.fixed));
variable_lines.push(format!( variable_lines.push(format!(

View File

@@ -1,12 +1,11 @@
use std::str::FromStr; use std::str::FromStr;
use baseline::M3BaselineColors; use baseline::M3BaselineColors;
use palette::cam16::{Cam16Jch, Parameters}; use palette::{IntoColor, Lch, Srgb};
use palette::{IntoColor, Srgb};
use serde::Serialize; use serde::Serialize;
use tonal_palette::TonalPalette; use tonal_palette::TonalPalette;
use crate::convert::map_cam16jch_to_srgb_hex; use crate::convert::map_lch_to_srgb_hex;
use crate::errors; use crate::errors;
use super::SchemeExport; use super::SchemeExport;
@@ -26,20 +25,14 @@ pub struct MaterialDesign3Scheme {
impl MaterialDesign3Scheme { impl MaterialDesign3Scheme {
pub fn new(source_color: &str, error_color: &str) -> Result<Self, errors::ColorError> { pub fn new(source_color: &str, error_color: &str) -> Result<Self, errors::ColorError> {
let source = Cam16Jch::from_xyz( let source: Lch = Srgb::from_str(source_color)
Srgb::from_str(source_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))? .map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))?
.into_format::<f32>() .into_format::<f32>()
.into_color(), .into_color();
Parameters::default_static_wp(40.0), let error: Lch = Srgb::from_str(error_color)
);
let error = Cam16Jch::from_xyz(
Srgb::from_str(error_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(error_color.to_string()))? .map_err(|_| errors::ColorError::UnrecogniazedRGB(error_color.to_string()))?
.into_format::<f32>() .into_format::<f32>()
.into_color(), .into_color();
Parameters::default_static_wp(40.0),
);
let source_hue = source.hue.into_positive_degrees(); let source_hue = source.hue.into_positive_degrees();
let p = TonalPalette::from_hue_and_chroma(source_hue, source.chroma); let p = TonalPalette::from_hue_and_chroma(source_hue, source.chroma);
let s = TonalPalette::from_hue_and_chroma(source_hue, source.chroma / 3.0); let s = TonalPalette::from_hue_and_chroma(source_hue, source.chroma / 3.0);
@@ -49,8 +42,8 @@ impl MaterialDesign3Scheme {
let e = TonalPalette::from_hue_and_chroma(error.hue.into_positive_degrees(), 84.0); let e = TonalPalette::from_hue_and_chroma(error.hue.into_positive_degrees(), 84.0);
Ok(Self { Ok(Self {
white: map_cam16jch_to_srgb_hex(&Cam16Jch::new(100.0, 0.0, 0.0)), white: map_lch_to_srgb_hex(&Lch::new(100.0, 0.0, 0.0)),
black: map_cam16jch_to_srgb_hex(&Cam16Jch::new(0.0, 0.0, 0.0)), black: map_lch_to_srgb_hex(&Lch::new(0.0, 0.0, 0.0)),
light_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, false), light_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, false),
dark_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, true), dark_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, true),
}) })
@@ -61,13 +54,10 @@ impl MaterialDesign3Scheme {
name: String, name: String,
color: String, color: String,
) -> Result<(), errors::ColorError> { ) -> Result<(), errors::ColorError> {
let custom_color = Cam16Jch::from_xyz( let custom_color: Lch = Srgb::from_str(&color)
Srgb::from_str(&color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.clone()))? .map_err(|_| errors::ColorError::UnrecogniazedRGB(color.clone()))?
.into_format::<f32>() .into_format::<f32>()
.into_color(), .into_color();
Parameters::default_static_wp(40.0),
);
let hue = custom_color.hue.into_positive_degrees(); let hue = custom_color.hue.into_positive_degrees();
let palette = TonalPalette::from_hue_and_chroma(hue, custom_color.chroma); let palette = TonalPalette::from_hue_and_chroma(hue, custom_color.chroma);
self.light_baseline.add_custom_set(name.clone(), &palette); self.light_baseline.add_custom_set(name.clone(), &palette);
@@ -103,8 +93,8 @@ impl SchemeExport for MaterialDesign3Scheme {
let mut js_object = Vec::new(); let mut js_object = Vec::new();
js_object.push("const colorScheme = {".to_string()); js_object.push("const colorScheme = {".to_string());
js_object.push(format!(" white: '#{}'", self.white)); js_object.push(format!(" white: '#{}',", self.white));
js_object.push(format!(" black: '#{}'", self.black)); js_object.push(format!(" black: '#{}',", self.black));
js_object.push(" light: {".to_string()); js_object.push(" light: {".to_string());
js_object.extend( js_object.extend(
self.light_baseline self.light_baseline
@@ -125,6 +115,6 @@ impl SchemeExport for MaterialDesign3Scheme {
js_object.push(" },".to_string()); js_object.push(" },".to_string());
js_object.push("}".to_string()); js_object.push("}".to_string());
js_object.join(",\n") js_object.join("\n")
} }
} }

View File

@@ -1,6 +1,6 @@
use serde::Serialize; use serde::Serialize;
use crate::convert::map_cam16jch_to_srgb_hex; use crate::convert::map_lch_to_srgb_hex;
use super::tonal_palette::TonalPalette; use super::tonal_palette::TonalPalette;
@@ -36,18 +36,18 @@ impl M3SurfaceSet {
let on_inverse = neutral_variant.tone(95.0); let on_inverse = neutral_variant.tone(95.0);
Self { Self {
root: map_cam16jch_to_srgb_hex(&root), root: map_lch_to_srgb_hex(&root),
dim: map_cam16jch_to_srgb_hex(&dim), dim: map_lch_to_srgb_hex(&dim),
bright: map_cam16jch_to_srgb_hex(&bright), bright: map_lch_to_srgb_hex(&bright),
container: map_cam16jch_to_srgb_hex(&container), container: map_lch_to_srgb_hex(&container),
container_lowest: map_cam16jch_to_srgb_hex(&container_lowest), container_lowest: map_lch_to_srgb_hex(&container_lowest),
container_low: map_cam16jch_to_srgb_hex(&container_low), container_low: map_lch_to_srgb_hex(&container_low),
container_high: map_cam16jch_to_srgb_hex(&container_high), container_high: map_lch_to_srgb_hex(&container_high),
container_highest: map_cam16jch_to_srgb_hex(&container_highest), container_highest: map_lch_to_srgb_hex(&container_highest),
on_root: map_cam16jch_to_srgb_hex(&on_root), on_root: map_lch_to_srgb_hex(&on_root),
on_root_variant: map_cam16jch_to_srgb_hex(&on_root_variant), on_root_variant: map_lch_to_srgb_hex(&on_root_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse), inverse: map_lch_to_srgb_hex(&inverse),
on_inverse: map_cam16jch_to_srgb_hex(&on_inverse), on_inverse: map_lch_to_srgb_hex(&on_inverse),
} }
} }
@@ -66,18 +66,18 @@ impl M3SurfaceSet {
let on_inverse = neutral_variant.tone(20.0); let on_inverse = neutral_variant.tone(20.0);
Self { Self {
root: map_cam16jch_to_srgb_hex(&root), root: map_lch_to_srgb_hex(&root),
dim: map_cam16jch_to_srgb_hex(&dim), dim: map_lch_to_srgb_hex(&dim),
bright: map_cam16jch_to_srgb_hex(&bright), bright: map_lch_to_srgb_hex(&bright),
container: map_cam16jch_to_srgb_hex(&container), container: map_lch_to_srgb_hex(&container),
container_lowest: map_cam16jch_to_srgb_hex(&container_lowest), container_lowest: map_lch_to_srgb_hex(&container_lowest),
container_low: map_cam16jch_to_srgb_hex(&container_low), container_low: map_lch_to_srgb_hex(&container_low),
container_high: map_cam16jch_to_srgb_hex(&container_high), container_high: map_lch_to_srgb_hex(&container_high),
container_highest: map_cam16jch_to_srgb_hex(&container_highest), container_highest: map_lch_to_srgb_hex(&container_highest),
on_root: map_cam16jch_to_srgb_hex(&on_root), on_root: map_lch_to_srgb_hex(&on_root),
on_root_variant: map_cam16jch_to_srgb_hex(&on_root_variant), on_root_variant: map_lch_to_srgb_hex(&on_root_variant),
inverse: map_cam16jch_to_srgb_hex(&inverse), inverse: map_lch_to_srgb_hex(&inverse),
on_inverse: map_cam16jch_to_srgb_hex(&on_inverse), on_inverse: map_lch_to_srgb_hex(&on_inverse),
} }
} }

View File

@@ -1,15 +1,12 @@
use std::str::FromStr; use std::str::FromStr;
use palette::{ use palette::{cam16::Cam16Jch, IntoColor, Lch, Srgb};
cam16::{Cam16Jch, Parameters},
IntoColor, Srgb,
};
use crate::errors; use crate::errors;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TonalPalette { pub struct TonalPalette {
pub key_color: Cam16Jch<f32>, pub key_color: Lch,
} }
#[inline] #[inline]
@@ -30,7 +27,7 @@ fn find_max_chroma(cache: &mut Vec<(f32, f32)>, hue: f32, tone: f32) -> f32 {
chroma chroma
} }
fn from_hue_and_chroma(hue: f32, chroma: f32) -> Cam16Jch<f32> { fn from_hue_and_chroma(hue: f32, chroma: f32) -> Lch {
let mut max_chroma_cache = Vec::new(); let mut max_chroma_cache = Vec::new();
let hue = if hue >= 360.0 { hue - 360.0 } else { hue }; let hue = if hue >= 360.0 { hue - 360.0 } else { hue };
const PIVOT_TONE: f32 = 50.0; const PIVOT_TONE: f32 = 50.0;
@@ -51,7 +48,7 @@ fn from_hue_and_chroma(hue: f32, chroma: f32) -> Cam16Jch<f32> {
upper_tone = mid_tone; upper_tone = mid_tone;
} else { } else {
if approximately_equal(lower_tone, mid_tone) { if approximately_equal(lower_tone, mid_tone) {
return Cam16Jch::new(lower_tone, chroma, hue); return Lch::new(lower_tone, chroma, hue);
} }
lower_tone = mid_tone; lower_tone = mid_tone;
} }
@@ -63,20 +60,17 @@ fn from_hue_and_chroma(hue: f32, chroma: f32) -> Cam16Jch<f32> {
} }
} }
} }
Cam16Jch::new(lower_tone, chroma, hue) Lch::new(lower_tone, chroma, hue)
} }
impl TryFrom<String> for TonalPalette { impl TryFrom<String> for TonalPalette {
type Error = errors::ColorError; type Error = errors::ColorError;
fn try_from(value: String) -> Result<Self, Self::Error> { fn try_from(value: String) -> Result<Self, Self::Error> {
let key_color = Cam16Jch::from_xyz( let key_color: Lch = Srgb::from_str(&value)
Srgb::from_str(&value)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(value))? .map_err(|_| errors::ColorError::UnrecogniazedRGB(value))?
.into_format::<f32>() .into_format::<f32>()
.into_color(), .into_color();
Parameters::default_static_wp(40.0),
);
Ok(TonalPalette { key_color }) Ok(TonalPalette { key_color })
} }
} }
@@ -87,8 +81,8 @@ impl TonalPalette {
TonalPalette { key_color } TonalPalette { key_color }
} }
pub fn tone(&self, tone: f32) -> Cam16Jch<f32> { pub fn tone(&self, tone: f32) -> Lch {
let toned_color = Cam16Jch::new(tone, self.key_color.chroma, self.key_color.hue); let toned_color = Lch::new(tone, self.key_color.chroma, self.key_color.hue);
toned_color toned_color
} }

View File

@@ -1,13 +1,18 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <meta name="description"
</head> content="By transforming and selecting various color theories, freely design UI color combinations." />
<body> <title>Color Lab</title>
</head>
<body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@iconify/react": "^5.1.0", "@iconify/react": "^5.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"color-module": "./color_functions",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"jotai": "^2.11.0", "jotai": "^2.11.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",

View File

@@ -1,5 +1,5 @@
import init, * as funcs from 'color-module';
import { createContext, ReactNode, use, useEffect, useMemo, useState, useTransition } from 'react'; import { createContext, ReactNode, use, useEffect, useMemo, useState, useTransition } from 'react';
import init, * as funcs from './color_functions/color_module';
export type ColorFunctionContextType = { export type ColorFunctionContextType = {
colorFn: typeof funcs | null; colorFn: typeof funcs | null;
@@ -23,7 +23,7 @@ export function useColorFunction(): ColorFunctionContextType {
} }
export function ColorFunctionProvider({ children }: WasmProviderProps) { export function ColorFunctionProvider({ children }: WasmProviderProps) {
const [wasmInstance, setWasmInstance] = useState<Wasm.InitOutput | null>(null); const [wasmInstance, setWasmInstance] = useState<typeof funcs | null>(null);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
@@ -42,6 +42,7 @@ export function ColorFunctionProvider({ children }: WasmProviderProps) {
try { try {
await init(); await init();
setWasmInstance(funcs); setWasmInstance(funcs);
console.debug('[Load WASM]', 'Loaded');
} catch (e) { } catch (e) {
console.error('[Load WASM]', e); console.error('[Load WASM]', e);
setError(e); setError(e);

View File

@@ -1,317 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export function color_categories(): any;
export function search_color_cards(tag: string, category?: string | null): any;
export function generate_material_design_3_scheme(source_color: string, error_color: string, custom_colors: any): any;
export function generate_material_design_2_scheme(primary_color: string, secondary_color: string, error_color: string, custom_colors: any): any;
export function generate_q_scheme_automatically(primary_color: string, danger_color: string, success_color: string, warning_color: string, info_color: string, fg_color: string, bg_color: string, setting: SchemeSetting): any;
export function generate_q_scheme_manually(primary_color: string, secondary_color: string | null | undefined, tertiary_color: string | null | undefined, accent_color: string | null | undefined, danger_color: string, success_color: string, warning_color: string, info_color: string, fg_color: string, bg_color: string, setting: SchemeSetting): any;
export function generate_swatch_scheme(colors: SwatchEntry[], setting: SwatchSchemeSetting): any;
export function q_scheme_color_expanding_methods(): any;
export function q_scheme_wacg_settings(): any;
export function q_scheme_default_settings(): SchemeSetting;
export function differ_in_rgb(color: string, other: string): RGBDifference;
export function relative_differ_in_rgb(color: string, other: string): RGBDifference;
export function differ_in_hsl(color: string, other: string): HSLDifference;
export function relative_differ_in_hsl(color: string, other: string): HSLDifference;
export function differ_in_hct(color: string, other: string): HctDiffference;
export function relative_differ_in_hct(color: string, other: string): HctDiffference;
export function differ_in_oklch(color: string, other: string): OklchDifference;
export function relative_differ_in_oklch(color: string, other: string): OklchDifference;
export function tint_scale(basic_color: string, mixed_color: string): MixReversing;
export function shade_scale(basic_color: string, mixed_color: string): MixReversing;
export function lighten(color: string, percent: number): string;
export function lighten_absolute(color: string, value: number): string;
export function darken(color: string, percent: number): string;
export function darken_absolute(color: string, value: number): string;
export function mix(color1: string, color2: string, percent: number): string;
export function tint(color: string, percent: number): string;
export function shade(color: string, percent: number): string;
export function shift_hue(color: string, degree: number): string;
export function analogous_30(color: string): string[];
export function analogous_60(color: string): string[];
export function complementary(color: string): string;
export function split_complementary(color: string): string[];
export function tetradic(color: string): string[];
export function triadic(color: string): string[];
export function represent_rgb(color: string): Uint8Array;
export function rgb_to_hex(r: number, g: number, b: number): string;
export function represent_hsl(color: string): Float32Array;
export function hsl_to_hex(h: number, s: number, l: number): string;
export function represent_lab(color: string): Float32Array;
export function lab_to_hex(l: number, a: number, b: number): string;
export function represent_oklch(color: string): Float32Array;
export function oklch_to_hex(l: number, c: number, h: number): string;
export function represent_hct(color: string): Float32Array;
export function hct_to_hex(hue: number, chroma: number, tone: number): string;
export function wacg_relative_contrast(fg_color: string, bg_color: string): number;
export function generate_palette_from_color(reference_color: string, swatch_amount: number, minimum_lightness: number, maximum_lightness: number, use_reference_color?: boolean | null, reference_color_bias?: number | null): string[];
export function swatch_scheme_default_settings(): SwatchSchemeSetting;
export function series(color: string, expand_amount: number, step: number): string[];
export function tonal_lighten_series(color: string, expand_amount: number, step: number): string[];
export function tonal_darken_series(color: string, expand_amount: number, step: number): string[];
export enum ColorExpand {
Complementary = 0,
Analogous = 1,
AnalogousAndComplementary = 2,
Triadic = 3,
SplitComplementary = 4,
Tetradic = 5,
Square = 6,
}
export enum WACGSetting {
Fixed = 0,
AutomaticAA = 1,
AutomaticAAA = 2,
HighContrast = 3,
}
export class ColorShifting {
free(): void;
constructor(chroma: number, lightness: number);
toJsValue(): any;
chroma: number;
lightness: number;
}
export class Differ {
private constructor();
free(): void;
delta: number;
percent: number;
}
export class HSLDifference {
private constructor();
free(): void;
hue: Differ;
saturation: Differ;
lightness: Differ;
}
export class HctDiffference {
private constructor();
free(): void;
hue: Differ;
chroma: Differ;
lightness: Differ;
}
export class MixReversing {
private constructor();
free(): void;
r_factor: number;
g_factor: number;
b_factor: number;
average: number;
}
export class OklchDifference {
private constructor();
free(): void;
hue: Differ;
chroma: Differ;
lightness: Differ;
}
export class RGBDifference {
private constructor();
free(): void;
r: Differ;
g: Differ;
b: Differ;
}
export class SchemeSetting {
free(): void;
constructor(hover: ColorShifting, active: ColorShifting, focus: ColorShifting, disabled: ColorShifting, dark_convert: ColorShifting, expand_method: ColorExpand, wacg_follows: WACGSetting);
toJsValue(): any;
hover: ColorShifting;
active: ColorShifting;
focus: ColorShifting;
disabled: ColorShifting;
dark_convert: ColorShifting;
expand_method: ColorExpand;
wacg_follows: WACGSetting;
}
export class SwatchEntry {
free(): void;
constructor(name: string, color: string);
toJsValue(): any;
name: string;
color: string;
}
export class SwatchSchemeSetting {
free(): void;
constructor(amount: number, min_lightness: number, max_lightness: number, include_primary: boolean, dark_convert: ColorShifting);
toJsValue(): any;
amount: number;
min_lightness: number;
max_lightness: number;
include_primary: boolean;
dark_convert: ColorShifting;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly color_categories: () => [number, number, number];
readonly search_color_cards: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly __wbg_colorshifting_free: (a: number, b: number) => void;
readonly __wbg_get_colorshifting_chroma: (a: number) => number;
readonly __wbg_set_colorshifting_chroma: (a: number, b: number) => void;
readonly __wbg_get_colorshifting_lightness: (a: number) => number;
readonly __wbg_set_colorshifting_lightness: (a: number, b: number) => void;
readonly colorshifting_new: (a: number, b: number) => number;
readonly colorshifting_toJsValue: (a: number) => [number, number, number];
readonly __wbg_schemesetting_free: (a: number, b: number) => void;
readonly __wbg_get_schemesetting_hover: (a: number) => number;
readonly __wbg_set_schemesetting_hover: (a: number, b: number) => void;
readonly __wbg_get_schemesetting_active: (a: number) => number;
readonly __wbg_set_schemesetting_active: (a: number, b: number) => void;
readonly __wbg_get_schemesetting_focus: (a: number) => number;
readonly __wbg_set_schemesetting_focus: (a: number, b: number) => void;
readonly __wbg_get_schemesetting_disabled: (a: number) => number;
readonly __wbg_set_schemesetting_disabled: (a: number, b: number) => void;
readonly __wbg_get_schemesetting_dark_convert: (a: number) => number;
readonly __wbg_set_schemesetting_dark_convert: (a: number, b: number) => void;
readonly __wbg_get_schemesetting_expand_method: (a: number) => number;
readonly __wbg_set_schemesetting_expand_method: (a: number, b: number) => void;
readonly __wbg_get_schemesetting_wacg_follows: (a: number) => number;
readonly __wbg_set_schemesetting_wacg_follows: (a: number, b: number) => void;
readonly schemesetting_new: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number;
readonly schemesetting_toJsValue: (a: number) => [number, number, number];
readonly generate_material_design_3_scheme: (a: number, b: number, c: number, d: number, e: any) => [number, number, number];
readonly generate_material_design_2_scheme: (a: number, b: number, c: number, d: number, e: number, f: number, g: any) => [number, number, number];
readonly generate_q_scheme_automatically: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number) => [number, number, number];
readonly generate_q_scheme_manually: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number, s: number, t: number, u: number) => [number, number, number];
readonly generate_swatch_scheme: (a: number, b: number, c: number) => [number, number, number];
readonly __wbg_oklchdifference_free: (a: number, b: number) => void;
readonly __wbg_get_oklchdifference_hue: (a: number) => number;
readonly __wbg_set_oklchdifference_hue: (a: number, b: number) => void;
readonly __wbg_get_oklchdifference_chroma: (a: number) => number;
readonly __wbg_set_oklchdifference_chroma: (a: number, b: number) => void;
readonly __wbg_get_oklchdifference_lightness: (a: number) => number;
readonly __wbg_set_oklchdifference_lightness: (a: number, b: number) => void;
readonly q_scheme_color_expanding_methods: () => [number, number, number];
readonly q_scheme_wacg_settings: () => [number, number, number];
readonly q_scheme_default_settings: () => number;
readonly __wbg_swatchschemesetting_free: (a: number, b: number) => void;
readonly __wbg_get_swatchschemesetting_amount: (a: number) => number;
readonly __wbg_set_swatchschemesetting_amount: (a: number, b: number) => void;
readonly __wbg_get_swatchschemesetting_min_lightness: (a: number) => number;
readonly __wbg_set_swatchschemesetting_min_lightness: (a: number, b: number) => void;
readonly __wbg_get_swatchschemesetting_max_lightness: (a: number) => number;
readonly __wbg_set_swatchschemesetting_max_lightness: (a: number, b: number) => void;
readonly __wbg_get_swatchschemesetting_include_primary: (a: number) => number;
readonly __wbg_set_swatchschemesetting_include_primary: (a: number, b: number) => void;
readonly __wbg_get_swatchschemesetting_dark_convert: (a: number) => number;
readonly __wbg_set_swatchschemesetting_dark_convert: (a: number, b: number) => void;
readonly swatchschemesetting_new: (a: number, b: number, c: number, d: number, e: number) => number;
readonly swatchschemesetting_toJsValue: (a: number) => [number, number, number];
readonly __wbg_mixreversing_free: (a: number, b: number) => void;
readonly __wbg_get_mixreversing_r_factor: (a: number) => number;
readonly __wbg_set_mixreversing_r_factor: (a: number, b: number) => void;
readonly __wbg_get_mixreversing_g_factor: (a: number) => number;
readonly __wbg_set_mixreversing_g_factor: (a: number, b: number) => void;
readonly __wbg_get_mixreversing_b_factor: (a: number) => number;
readonly __wbg_set_mixreversing_b_factor: (a: number, b: number) => void;
readonly __wbg_get_mixreversing_average: (a: number) => number;
readonly __wbg_set_mixreversing_average: (a: number, b: number) => void;
readonly differ_in_rgb: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly relative_differ_in_rgb: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly differ_in_hsl: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly relative_differ_in_hsl: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly differ_in_hct: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly relative_differ_in_hct: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly differ_in_oklch: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly relative_differ_in_oklch: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly tint_scale: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly shade_scale: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly __wbg_hctdiffference_free: (a: number, b: number) => void;
readonly __wbg_get_hctdiffference_hue: (a: number) => number;
readonly __wbg_set_hctdiffference_hue: (a: number, b: number) => void;
readonly __wbg_get_hctdiffference_chroma: (a: number) => number;
readonly __wbg_set_hctdiffference_chroma: (a: number, b: number) => void;
readonly __wbg_get_hctdiffference_lightness: (a: number) => number;
readonly __wbg_set_hctdiffference_lightness: (a: number, b: number) => void;
readonly lighten: (a: number, b: number, c: number) => [number, number, number, number];
readonly lighten_absolute: (a: number, b: number, c: number) => [number, number, number, number];
readonly darken: (a: number, b: number, c: number) => [number, number, number, number];
readonly darken_absolute: (a: number, b: number, c: number) => [number, number, number, number];
readonly mix: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
readonly tint: (a: number, b: number, c: number) => [number, number, number, number];
readonly shade: (a: number, b: number, c: number) => [number, number, number, number];
readonly shift_hue: (a: number, b: number, c: number) => [number, number, number, number];
readonly analogous_30: (a: number, b: number) => [number, number, number, number];
readonly analogous_60: (a: number, b: number) => [number, number, number, number];
readonly complementary: (a: number, b: number) => [number, number, number, number];
readonly split_complementary: (a: number, b: number) => [number, number, number, number];
readonly tetradic: (a: number, b: number) => [number, number, number, number];
readonly triadic: (a: number, b: number) => [number, number, number, number];
readonly represent_rgb: (a: number, b: number) => [number, number, number, number];
readonly rgb_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly represent_hsl: (a: number, b: number) => [number, number, number, number];
readonly hsl_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly represent_lab: (a: number, b: number) => [number, number, number, number];
readonly lab_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly represent_oklch: (a: number, b: number) => [number, number, number, number];
readonly oklch_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly represent_hct: (a: number, b: number) => [number, number, number, number];
readonly hct_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly wacg_relative_contrast: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly __wbg_rgbdifference_free: (a: number, b: number) => void;
readonly __wbg_get_rgbdifference_r: (a: number) => number;
readonly __wbg_set_rgbdifference_r: (a: number, b: number) => void;
readonly __wbg_get_rgbdifference_g: (a: number) => number;
readonly __wbg_set_rgbdifference_g: (a: number, b: number) => void;
readonly __wbg_get_rgbdifference_b: (a: number) => number;
readonly __wbg_set_rgbdifference_b: (a: number, b: number) => void;
readonly __wbg_hsldifference_free: (a: number, b: number) => void;
readonly __wbg_get_hsldifference_hue: (a: number) => number;
readonly __wbg_set_hsldifference_hue: (a: number, b: number) => void;
readonly __wbg_get_hsldifference_saturation: (a: number) => number;
readonly __wbg_set_hsldifference_saturation: (a: number, b: number) => void;
readonly __wbg_get_hsldifference_lightness: (a: number) => number;
readonly __wbg_set_hsldifference_lightness: (a: number, b: number) => void;
readonly __wbg_differ_free: (a: number, b: number) => void;
readonly __wbg_get_differ_delta: (a: number) => number;
readonly __wbg_set_differ_delta: (a: number, b: number) => void;
readonly __wbg_get_differ_percent: (a: number) => number;
readonly __wbg_set_differ_percent: (a: number, b: number) => void;
readonly generate_palette_from_color: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
readonly __wbg_swatchentry_free: (a: number, b: number) => void;
readonly __wbg_get_swatchentry_name: (a: number) => [number, number];
readonly __wbg_set_swatchentry_name: (a: number, b: number, c: number) => void;
readonly __wbg_get_swatchentry_color: (a: number) => [number, number];
readonly __wbg_set_swatchentry_color: (a: number, b: number, c: number) => void;
readonly swatchentry_new: (a: number, b: number, c: number, d: number) => number;
readonly swatchentry_toJsValue: (a: number) => [number, number, number];
readonly swatch_scheme_default_settings: () => number;
readonly series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly tonal_lighten_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly tonal_darken_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_export_4: WebAssembly.Table;
readonly __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __externref_drop_slice: (a: number, b: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

File diff suppressed because it is too large Load Diff

View File

@@ -1,148 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const color_categories: () => [number, number, number];
export const search_color_cards: (a: number, b: number, c: number, d: number) => [number, number, number];
export const __wbg_colorshifting_free: (a: number, b: number) => void;
export const __wbg_get_colorshifting_chroma: (a: number) => number;
export const __wbg_set_colorshifting_chroma: (a: number, b: number) => void;
export const __wbg_get_colorshifting_lightness: (a: number) => number;
export const __wbg_set_colorshifting_lightness: (a: number, b: number) => void;
export const colorshifting_new: (a: number, b: number) => number;
export const colorshifting_toJsValue: (a: number) => [number, number, number];
export const __wbg_schemesetting_free: (a: number, b: number) => void;
export const __wbg_get_schemesetting_hover: (a: number) => number;
export const __wbg_set_schemesetting_hover: (a: number, b: number) => void;
export const __wbg_get_schemesetting_active: (a: number) => number;
export const __wbg_set_schemesetting_active: (a: number, b: number) => void;
export const __wbg_get_schemesetting_focus: (a: number) => number;
export const __wbg_set_schemesetting_focus: (a: number, b: number) => void;
export const __wbg_get_schemesetting_disabled: (a: number) => number;
export const __wbg_set_schemesetting_disabled: (a: number, b: number) => void;
export const __wbg_get_schemesetting_dark_convert: (a: number) => number;
export const __wbg_set_schemesetting_dark_convert: (a: number, b: number) => void;
export const __wbg_get_schemesetting_expand_method: (a: number) => number;
export const __wbg_set_schemesetting_expand_method: (a: number, b: number) => void;
export const __wbg_get_schemesetting_wacg_follows: (a: number) => number;
export const __wbg_set_schemesetting_wacg_follows: (a: number, b: number) => void;
export const schemesetting_new: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number;
export const schemesetting_toJsValue: (a: number) => [number, number, number];
export const generate_material_design_3_scheme: (a: number, b: number, c: number, d: number, e: any) => [number, number, number];
export const generate_material_design_2_scheme: (a: number, b: number, c: number, d: number, e: number, f: number, g: any) => [number, number, number];
export const generate_q_scheme_automatically: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number) => [number, number, number];
export const generate_q_scheme_manually: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number, s: number, t: number, u: number) => [number, number, number];
export const generate_swatch_scheme: (a: number, b: number, c: number) => [number, number, number];
export const __wbg_oklchdifference_free: (a: number, b: number) => void;
export const __wbg_get_oklchdifference_hue: (a: number) => number;
export const __wbg_set_oklchdifference_hue: (a: number, b: number) => void;
export const __wbg_get_oklchdifference_chroma: (a: number) => number;
export const __wbg_set_oklchdifference_chroma: (a: number, b: number) => void;
export const __wbg_get_oklchdifference_lightness: (a: number) => number;
export const __wbg_set_oklchdifference_lightness: (a: number, b: number) => void;
export const q_scheme_color_expanding_methods: () => [number, number, number];
export const q_scheme_wacg_settings: () => [number, number, number];
export const q_scheme_default_settings: () => number;
export const __wbg_swatchschemesetting_free: (a: number, b: number) => void;
export const __wbg_get_swatchschemesetting_amount: (a: number) => number;
export const __wbg_set_swatchschemesetting_amount: (a: number, b: number) => void;
export const __wbg_get_swatchschemesetting_min_lightness: (a: number) => number;
export const __wbg_set_swatchschemesetting_min_lightness: (a: number, b: number) => void;
export const __wbg_get_swatchschemesetting_max_lightness: (a: number) => number;
export const __wbg_set_swatchschemesetting_max_lightness: (a: number, b: number) => void;
export const __wbg_get_swatchschemesetting_include_primary: (a: number) => number;
export const __wbg_set_swatchschemesetting_include_primary: (a: number, b: number) => void;
export const __wbg_get_swatchschemesetting_dark_convert: (a: number) => number;
export const __wbg_set_swatchschemesetting_dark_convert: (a: number, b: number) => void;
export const swatchschemesetting_new: (a: number, b: number, c: number, d: number, e: number) => number;
export const swatchschemesetting_toJsValue: (a: number) => [number, number, number];
export const __wbg_mixreversing_free: (a: number, b: number) => void;
export const __wbg_get_mixreversing_r_factor: (a: number) => number;
export const __wbg_set_mixreversing_r_factor: (a: number, b: number) => void;
export const __wbg_get_mixreversing_g_factor: (a: number) => number;
export const __wbg_set_mixreversing_g_factor: (a: number, b: number) => void;
export const __wbg_get_mixreversing_b_factor: (a: number) => number;
export const __wbg_set_mixreversing_b_factor: (a: number, b: number) => void;
export const __wbg_get_mixreversing_average: (a: number) => number;
export const __wbg_set_mixreversing_average: (a: number, b: number) => void;
export const differ_in_rgb: (a: number, b: number, c: number, d: number) => [number, number, number];
export const relative_differ_in_rgb: (a: number, b: number, c: number, d: number) => [number, number, number];
export const differ_in_hsl: (a: number, b: number, c: number, d: number) => [number, number, number];
export const relative_differ_in_hsl: (a: number, b: number, c: number, d: number) => [number, number, number];
export const differ_in_hct: (a: number, b: number, c: number, d: number) => [number, number, number];
export const relative_differ_in_hct: (a: number, b: number, c: number, d: number) => [number, number, number];
export const differ_in_oklch: (a: number, b: number, c: number, d: number) => [number, number, number];
export const relative_differ_in_oklch: (a: number, b: number, c: number, d: number) => [number, number, number];
export const tint_scale: (a: number, b: number, c: number, d: number) => [number, number, number];
export const shade_scale: (a: number, b: number, c: number, d: number) => [number, number, number];
export const __wbg_hctdiffference_free: (a: number, b: number) => void;
export const __wbg_get_hctdiffference_hue: (a: number) => number;
export const __wbg_set_hctdiffference_hue: (a: number, b: number) => void;
export const __wbg_get_hctdiffference_chroma: (a: number) => number;
export const __wbg_set_hctdiffference_chroma: (a: number, b: number) => void;
export const __wbg_get_hctdiffference_lightness: (a: number) => number;
export const __wbg_set_hctdiffference_lightness: (a: number, b: number) => void;
export const lighten: (a: number, b: number, c: number) => [number, number, number, number];
export const lighten_absolute: (a: number, b: number, c: number) => [number, number, number, number];
export const darken: (a: number, b: number, c: number) => [number, number, number, number];
export const darken_absolute: (a: number, b: number, c: number) => [number, number, number, number];
export const mix: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
export const tint: (a: number, b: number, c: number) => [number, number, number, number];
export const shade: (a: number, b: number, c: number) => [number, number, number, number];
export const shift_hue: (a: number, b: number, c: number) => [number, number, number, number];
export const analogous_30: (a: number, b: number) => [number, number, number, number];
export const analogous_60: (a: number, b: number) => [number, number, number, number];
export const complementary: (a: number, b: number) => [number, number, number, number];
export const split_complementary: (a: number, b: number) => [number, number, number, number];
export const tetradic: (a: number, b: number) => [number, number, number, number];
export const triadic: (a: number, b: number) => [number, number, number, number];
export const represent_rgb: (a: number, b: number) => [number, number, number, number];
export const rgb_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const represent_hsl: (a: number, b: number) => [number, number, number, number];
export const hsl_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const represent_lab: (a: number, b: number) => [number, number, number, number];
export const lab_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const represent_oklch: (a: number, b: number) => [number, number, number, number];
export const oklch_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const represent_hct: (a: number, b: number) => [number, number, number, number];
export const hct_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const wacg_relative_contrast: (a: number, b: number, c: number, d: number) => [number, number, number];
export const __wbg_rgbdifference_free: (a: number, b: number) => void;
export const __wbg_get_rgbdifference_r: (a: number) => number;
export const __wbg_set_rgbdifference_r: (a: number, b: number) => void;
export const __wbg_get_rgbdifference_g: (a: number) => number;
export const __wbg_set_rgbdifference_g: (a: number, b: number) => void;
export const __wbg_get_rgbdifference_b: (a: number) => number;
export const __wbg_set_rgbdifference_b: (a: number, b: number) => void;
export const __wbg_hsldifference_free: (a: number, b: number) => void;
export const __wbg_get_hsldifference_hue: (a: number) => number;
export const __wbg_set_hsldifference_hue: (a: number, b: number) => void;
export const __wbg_get_hsldifference_saturation: (a: number) => number;
export const __wbg_set_hsldifference_saturation: (a: number, b: number) => void;
export const __wbg_get_hsldifference_lightness: (a: number) => number;
export const __wbg_set_hsldifference_lightness: (a: number, b: number) => void;
export const __wbg_differ_free: (a: number, b: number) => void;
export const __wbg_get_differ_delta: (a: number) => number;
export const __wbg_set_differ_delta: (a: number, b: number) => void;
export const __wbg_get_differ_percent: (a: number) => number;
export const __wbg_set_differ_percent: (a: number, b: number) => void;
export const generate_palette_from_color: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
export const __wbg_swatchentry_free: (a: number, b: number) => void;
export const __wbg_get_swatchentry_name: (a: number) => [number, number];
export const __wbg_set_swatchentry_name: (a: number, b: number, c: number) => void;
export const __wbg_get_swatchentry_color: (a: number) => [number, number];
export const __wbg_set_swatchentry_color: (a: number, b: number, c: number) => void;
export const swatchentry_new: (a: number, b: number, c: number, d: number) => number;
export const swatchentry_toJsValue: (a: number) => [number, number, number];
export const swatch_scheme_default_settings: () => number;
export const series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const tonal_lighten_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const tonal_darken_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_export_4: WebAssembly.Table;
export const __externref_table_dealloc: (a: number) => void;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __externref_drop_slice: (a: number, b: number) => void;
export const __wbindgen_start: () => void;

View File

@@ -1,15 +0,0 @@
{
"name": "color-module",
"type": "module",
"version": "0.1.0",
"files": [
"color_module_bg.wasm",
"color_module.js",
"color_module.d.ts"
],
"main": "color_module.js",
"types": "color_module.d.ts",
"sideEffects": [
"./snippets/*"
]
}

View File

@@ -1,6 +1,6 @@
import { Icon, IconProps } from '@iconify/react/dist/iconify.js'; import { Icon, IconProps } from '@iconify/react/dist/iconify.js';
import cx from 'clsx'; import cx from 'clsx';
import { MouseEventHandler, useCallback } from 'react'; import { MouseEvent, MouseEventHandler, useCallback } from 'react';
import styles from './ActionIcon.module.css'; import styles from './ActionIcon.module.css';
type ActionIconProps = { type ActionIconProps = {

View File

@@ -87,7 +87,7 @@ export function ColorComponentInput({ color, onChange }: ColorComponentInputProp
} }
}; };
const updateH = (evt: ChangeEvent<HTMLInputElement>) => { const updateH = (evt: ChangeEvent<HTMLInputElement>) => {
const value = parseInt(evt.target.value, 10); let value = parseInt(evt.target.value, 10);
if (value > 360) { if (value > 360) {
value -= 360; value -= 360;
} }

View File

@@ -28,7 +28,7 @@ export function ColorRangePicker({
}: ColorRangePickerProps) { }: ColorRangePickerProps) {
const [pickerValue, setPickerValue] = useState(value); const [pickerValue, setPickerValue] = useState(value);
const handlePickerChange = (evt: ChangeEvent<HTMLInputElement>) => { const handlePickerChange = (evt: ChangeEvent<HTMLInputElement>) => {
const value = evt.target.value as number; const value = Number(evt.target.value);
setPickerValue(valueProcess(value)); setPickerValue(valueProcess(value));
onChange?.(valueProcess(value)); onChange?.(valueProcess(value));
}; };

View File

@@ -4,7 +4,7 @@ import { useRef, useState } from 'react';
import styles from './EditableTitle.module.css'; import styles from './EditableTitle.module.css';
type EditableTitleProps = { type EditableTitleProps = {
title: string; title?: string;
onChange?: (newTitle: string) => void; onChange?: (newTitle: string) => void;
}; };

View File

@@ -6,7 +6,7 @@ import styles from './FloatColorPicker.module.css';
type FloatColorPickerProps = { type FloatColorPickerProps = {
name?: string; name?: string;
color?: string; color?: string | null;
onPick?: (color: string | null | undefined) => void; onPick?: (color: string | null | undefined) => void;
}; };

View File

@@ -52,6 +52,7 @@ export function HSegmentedControl({
<div <div
key={`${index}_${value}`} key={`${index}_${value}`}
className={cx(styles.option, isEqual(selected, value) && styles.selected)} className={cx(styles.option, isEqual(selected, value) && styles.selected)}
//@ts-expect-error TS2322
ref={(el) => (optionsRef.current[index] = el!)} ref={(el) => (optionsRef.current[index] = el!)}
onClick={() => handleSelectAction(value, index)}> onClick={() => handleSelectAction(value, index)}>
{label} {label}

View File

@@ -26,7 +26,7 @@ export function LabeledPicker({
}: LabeledPickerProps) { }: LabeledPickerProps) {
const [pickerValue, setPickerValue] = useState(value ?? min); const [pickerValue, setPickerValue] = useState(value ?? min);
const handlePickerChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handlePickerChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value as number; const value = Number(event.target.value);
setPickerValue(value); setPickerValue(value);
onChange?.(value); onChange?.(value);
}; };

View File

@@ -118,7 +118,7 @@ type ToastProps = {
icon?: string; icon?: string;
duration?: ToastDuration; duration?: ToastDuration;
ref: RefObject<HTMLDivElement>; ref: RefObject<HTMLDivElement>;
closeAction: () => void; closeAction: (tid?: string) => void;
}; };
const Toast = ({ const Toast = ({
kind, kind,
@@ -157,7 +157,7 @@ export function useNotification() {
type NotificationElement = { type NotificationElement = {
id: string; id: string;
element: ReactNode; element: ReactNode;
ref: RefObject<ReactNode>; ref: RefObject<ReactNode | HTMLDivElement>;
}; };
type NotificationsProps = { type NotificationsProps = {
defaultDuration?: number; defaultDuration?: number;
@@ -184,7 +184,7 @@ export function Notifications({
duration?: number, duration?: number,
) => { ) => {
const id = v4(); const id = v4();
const ref = createRef(null); const ref = createRef<ReactNode | HTMLDivElement>();
const newNotify = ( const newNotify = (
<Notification <Notification
kind={kind} kind={kind}
@@ -194,6 +194,7 @@ export function Notifications({
message={message} message={message}
duration={duration ?? defaultDuration} duration={duration ?? defaultDuration}
closeAction={removeNotification} closeAction={removeNotification}
//@ts-expect-error TS2322
ref={ref} ref={ref}
/> />
); );
@@ -207,14 +208,9 @@ export function Notifications({
setToasts((prev) => filter(prev, (n) => !isEqual(n.id, id))); setToasts((prev) => filter(prev, (n) => !isEqual(n.id, id)));
}, []); }, []);
const showToast = useCallback( const showToast = useCallback(
( (kind: NotificationType, message?: string, icon?: string, duration?: ToastDuration) => {
kind: NotificationType,
message?: string,
icon?: IconifyIconProps['icon'],
duration?: ToastDuration,
) => {
const id = v4(); const id = v4();
const ref = createRef(null); const ref = createRef<HTMLDivElement>();
const newToast = ( const newToast = (
<Toast <Toast
kind={kind} kind={kind}
@@ -238,9 +234,6 @@ export function Notifications({
value={{ value={{
addNotification, addNotification,
removeNotification, removeNotification,
showDialog: () => '',
showModalDialog: () => '',
closeDialog: () => {},
showToast, showToast,
}}> }}>
{children} {children}
@@ -250,6 +243,7 @@ export function Notifications({
{notifications.slice(0, maxNotifications).map(({ id, element, ref }) => ( {notifications.slice(0, maxNotifications).map(({ id, element, ref }) => (
<CSSTransition <CSSTransition
key={id} key={id}
//@ts-expect-error TS2322
nodeRef={ref} nodeRef={ref}
unmountOnExit unmountOnExit
timeout={500} timeout={500}
@@ -271,6 +265,7 @@ export function Notifications({
{toasts.slice(0, 1).map(({ id, element, ref }) => ( {toasts.slice(0, 1).map(({ id, element, ref }) => (
<CSSTransition <CSSTransition
key={id} key={id}
//@ts-expect-error TS2322
nodeRef={ref} nodeRef={ref}
unmountOnExit unmountOnExit
timeout={500} timeout={500}

View File

@@ -1,5 +1,5 @@
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { RefObject, useEffect, useRef, useState } from 'react'; import { MouseEvent, RefObject, useEffect, useRef, useState, WheelEvent } from 'react';
import styles from './ScrollArea.module.css'; import styles from './ScrollArea.module.css';
type ScrollBarProps = { type ScrollBarProps = {
@@ -12,10 +12,12 @@ function VerticalScrollBar({ containerRef }: ScrollBarProps) {
const thumbRef = useRef<HTMLDivElement | null>(null); const thumbRef = useRef<HTMLDivElement | null>(null);
const handleMouseDown = (evt: MouseEvent) => { const handleMouseDown = (evt: MouseEvent) => {
evt.preventDefault(); evt.preventDefault();
//@ts-expect-error TS2769
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
//@ts-expect-error TS2769
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
}; };
const handleMouseMove = (evt: MouseEvent) => { const handleMouseMove = (evt: MouseEvent<HTMLDivElement>) => {
evt.preventDefault(); evt.preventDefault();
const container = containerRef?.current; const container = containerRef?.current;
const scrollbar = trackRef.current; const scrollbar = trackRef.current;
@@ -34,7 +36,9 @@ function VerticalScrollBar({ containerRef }: ScrollBarProps) {
}; };
const handleMouseUp = (evt: MouseEvent) => { const handleMouseUp = (evt: MouseEvent) => {
evt.preventDefault(); evt.preventDefault();
//@ts-expect-error TS2769
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
//@ts-expect-error TS2769
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
}; };
@@ -77,7 +81,9 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
const thumbRef = useRef<HTMLDivElement | null>(null); const thumbRef = useRef<HTMLDivElement | null>(null);
const handleMouseDown = (evt: MouseEvent) => { const handleMouseDown = (evt: MouseEvent) => {
evt.preventDefault(); evt.preventDefault();
//@ts-expect-error TS2769
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
//@ts-expect-error TS2769
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
}; };
const handleMouseMove = (evt: MouseEvent) => { const handleMouseMove = (evt: MouseEvent) => {
@@ -99,7 +105,9 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
}; };
const handleMouseUp = (evt: MouseEvent) => { const handleMouseUp = (evt: MouseEvent) => {
evt.preventDefault(); evt.preventDefault();
//@ts-expect-error TS2769
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
//@ts-expect-error TS2769
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
}; };
@@ -129,7 +137,7 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
className={styles.h_thumb} className={styles.h_thumb}
ref={thumbRef} ref={thumbRef}
style={{ left: thumbPos }} style={{ left: thumbPos }}
onMouseDown={handleMouseDown} onMouseDown={(e) => handleMouseDown(e)}
/> />
</div> </div>
); );
@@ -148,10 +156,10 @@ export function ScrollArea({
enableY = false, enableY = false,
normalizedScroll = false, normalizedScroll = false,
}: ScrollAreaProps) { }: ScrollAreaProps) {
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const [xScrollNeeded, setXScrollNeeded] = useState(false); const [xScrollNeeded, setXScrollNeeded] = useState(false);
const [yScrollNeeded, setYScrollNeeded] = useState(false); const [yScrollNeeded, setYScrollNeeded] = useState(false);
const handleWheel = (evt: WheelEvent) => { const handleWheel = (evt: WheelEvent<HTMLDivElement>) => {
const container = scrollContainerRef?.current; const container = scrollContainerRef?.current;
if (enableY && container) { if (enableY && container) {
const delta = evt.deltaY; const delta = evt.deltaY;
@@ -177,7 +185,7 @@ export function ScrollArea({
return ( return (
<div className={styles.scroll_area}> <div className={styles.scroll_area}>
<div className={styles.content} ref={scrollContainerRef} onWheel={handleWheel}> <div className={styles.content} ref={scrollContainerRef} onWheel={(e) => handleWheel(e)}>
{children} {children}
</div> </div>
{enableY && yScrollNeeded && <VerticalScrollBar containerRef={scrollContainerRef} />} {enableY && yScrollNeeded && <VerticalScrollBar containerRef={scrollContainerRef} />}

View File

@@ -26,11 +26,14 @@ export function Switch({ name, checked = false, disabled = false, onChange }: Sw
}, [checked]); }, [checked]);
return ( return (
//@ts-expect-error TS2322
<div className={styles.switch} disabled={disabled}> <div className={styles.switch} disabled={disabled}>
<div <div
className={cx(styles.switch_handle, isChecked && styles.checked)} className={cx(styles.switch_handle, isChecked && styles.checked)}
onClick={handleSwitch}></div> onClick={handleSwitch}></div>
{!isNil(name) && <input type="hidden" name={name} value={isChecked} />} {!isNil(name) && (
<input type="hidden" name={name} value={isChecked ? 'checked' : undefined} />
)}
</div> </div>
); );
} }

View File

@@ -18,7 +18,7 @@ const positionMap = {
export function Tooltip({ content, position = 'top', children }: TooltipProps) { export function Tooltip({ content, position = 'top', children }: TooltipProps) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const contentRef = useRef<HTMLDivElement>(); const contentRef = useRef<HTMLDivElement | null>(null);
return ( return (
<div <div

View File

@@ -52,6 +52,7 @@ export function VSegmentedControl({
<div <div
key={`${index}_${value}`} key={`${index}_${value}`}
className={cx(styles.option, isEqual(selected, value) && styles.selected)} className={cx(styles.option, isEqual(selected, value) && styles.selected)}
//@ts-expect-error TS2322
ref={(el) => (optionsRef.current[index] = el!)} ref={(el) => (optionsRef.current[index] = el!)}
onClick={() => handleSelectAction(value, index)}> onClick={() => handleSelectAction(value, index)}>
{label} {label}

View File

@@ -6,7 +6,7 @@ import { NotificationType, useNotification } from '../components/Notifications';
export function useCopy() { export function useCopy() {
const { showToast } = useNotification(); const { showToast } = useNotification();
const [cpState, copyToClipboard] = useCopyToClipboard(); const [cpState, copyToClipboard] = useCopyToClipboard();
const copyAction = useCallback((content: string) => { const copyAction = useCallback((content?: string | null) => {
if (isNil(content) || isEmpty(content)) return; if (isNil(content) || isEmpty(content)) return;
copyToClipboard(content); copyToClipboard(content);
}, []); }, []);

View File

@@ -4,12 +4,12 @@ import { MaterialDesign3SchemeStorage } from './material-3-scheme';
import { QSchemeStorage } from './q-scheme'; import { QSchemeStorage } from './q-scheme';
import { SwatchSchemeStorage } from './swatch_scheme'; import { SwatchSchemeStorage } from './swatch_scheme';
export type Option = export type Option<T = string | number | null> =
| { | {
label: string; label: string;
value: string | number | null; value: T;
} }
| Record<'label' | 'value', string | number | null>; | Record<'label' | 'value', T>;
export type HarmonyColor = { export type HarmonyColor = {
color: string; color: string;

View File

@@ -1,30 +1,19 @@
import { Differ, HctDiffference } from 'color-module';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { HctDiffference } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext'; import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css'; import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props'; import { CompareMethodProps } from './share-props';
const defaultCompareResult: HctDiffference = {
hue: {
delta: 0,
percent: 0,
},
chroma: {
delta: 0,
percent: 0,
},
lightness: {
delta: 0,
percent: 0,
},
};
export function HCTCompare({ export function HCTCompare({
basic = '000000', basic = '000000',
compare = '000000', compare = '000000',
mode = 'absolute', mode = 'absolute',
}: CompareMethodProps) { }: CompareMethodProps) {
const { colorFn } = useColorFunction(); const { colorFn } = useColorFunction();
const defaultCompareResult: HctDiffference = useMemo(
() => new HctDiffference(new Differ(0, 0), new Differ(0, 0), new Differ(0, 0)),
[],
);
const differ = useMemo(() => { const differ = useMemo(() => {
if (!colorFn) { if (!colorFn) {
return defaultCompareResult; return defaultCompareResult;

View File

@@ -1,30 +1,19 @@
import { Differ, HSLDifference } from 'color-module';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { HSLDifference } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext'; import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css'; import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props'; import { CompareMethodProps } from './share-props';
const defaultCompareResult: HSLDifference = {
hue: {
delta: 0,
percent: 0,
},
saturation: {
delta: 0,
percent: 0,
},
lightness: {
delta: 0,
percent: 0,
},
};
export function HSLCompare({ export function HSLCompare({
basic = '000000', basic = '000000',
compare = '000000', compare = '000000',
mode = 'absolute', mode = 'absolute',
}: CompareMethodProps) { }: CompareMethodProps) {
const { colorFn } = useColorFunction(); const { colorFn } = useColorFunction();
const defaultCompareResult: HSLDifference = useMemo(
() => new HSLDifference(new Differ(0, 0), new Differ(0, 0), new Differ(0, 0)),
[],
);
const differ = useMemo(() => { const differ = useMemo(() => {
if (!colorFn) { if (!colorFn) {
return defaultCompareResult; return defaultCompareResult;

View File

@@ -1,30 +1,19 @@
import { Differ, OklchDifference } from 'color-module';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { OklchDifference } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext'; import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css'; import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props'; import { CompareMethodProps } from './share-props';
const defaultCompareResult: OklchDifference = {
hue: {
delta: 0,
percent: 0,
},
chroma: {
delta: 0,
percent: 0,
},
lightness: {
delta: 0,
percent: 0,
},
};
export function OklchCompare({ export function OklchCompare({
basic = '000000', basic = '000000',
compare = '000000', compare = '000000',
mode = 'absolute', mode = 'absolute',
}: CompareMethodProps) { }: CompareMethodProps) {
const { colorFn } = useColorFunction(); const { colorFn } = useColorFunction();
const defaultCompareResult: OklchDifference = useMemo(
() => new OklchDifference(new Differ(0, 0), new Differ(0, 0), new Differ(0, 0)),
[],
);
const differ = useMemo(() => { const differ = useMemo(() => {
if (!colorFn) { if (!colorFn) {
return defaultCompareResult; return defaultCompareResult;

View File

@@ -1,30 +1,19 @@
import { Differ, RGBDifference } from 'color-module';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { RGBDifference } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext'; import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css'; import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props'; import { CompareMethodProps } from './share-props';
const defaultCompareResult: RGBDifference = {
r: {
delta: 0,
percent: 0,
},
g: {
delta: 0,
percent: 0,
},
b: {
delta: 0,
percent: 0,
},
};
export function RGBCompare({ export function RGBCompare({
basic = '000000', basic = '000000',
compare = '000000', compare = '000000',
mode = 'absolute', mode = 'absolute',
}: CompareMethodProps) { }: CompareMethodProps) {
const { colorFn } = useColorFunction(); const { colorFn } = useColorFunction();
const defaultCompareResult: RGBDifference = useMemo(
() => new RGBDifference(new Differ(0, 0), new Differ(0, 0), new Differ(0, 0)),
[],
);
const differ = useMemo(() => { const differ = useMemo(() => {
if (!colorFn) { if (!colorFn) {
return defaultCompareResult; return defaultCompareResult;

View File

@@ -1,19 +1,13 @@
import cx from 'clsx'; import cx from 'clsx';
import { MixReversing } from 'color-module';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { MixReversing } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext'; import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css'; import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props'; import { CompareMethodProps } from './share-props';
const defaultMixResult: MixReversing = {
r_factor: 0,
g_factor: 0,
b_factor: 0,
average: 0,
};
export function ShadeScale({ basic = '000000', compare = '000000' }: CompareMethodProps) { export function ShadeScale({ basic = '000000', compare = '000000' }: CompareMethodProps) {
const { colorFn } = useColorFunction(); const { colorFn } = useColorFunction();
const defaultMixResult: MixReversing = useMemo(() => new MixReversing(0, 0, 0, 0), []);
const mixFactors = useMemo(() => { const mixFactors = useMemo(() => {
if (!colorFn) { if (!colorFn) {
return defaultMixResult; return defaultMixResult;

View File

@@ -1,19 +1,13 @@
import cx from 'clsx'; import cx from 'clsx';
import { MixReversing } from 'color-module';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { MixReversing } from '../../color_functions/color_module';
import { useColorFunction } from '../../ColorFunctionContext'; import { useColorFunction } from '../../ColorFunctionContext';
import styles from './CompareLayout.module.css'; import styles from './CompareLayout.module.css';
import { CompareMethodProps } from './share-props'; import { CompareMethodProps } from './share-props';
const defaultMixResult: MixReversing = {
r_factor: 0,
g_factor: 0,
b_factor: 0,
average: 0,
};
export function TintScale({ basic = '000000', compare = '000000' }: CompareMethodProps) { export function TintScale({ basic = '000000', compare = '000000' }: CompareMethodProps) {
const { colorFn } = useColorFunction(); const { colorFn } = useColorFunction();
const defaultMixResult: MixReversing = useMemo(() => new MixReversing(0, 0, 0, 0), []);
const mixFactors = useMemo(() => { const mixFactors = useMemo(() => {
if (!colorFn) { if (!colorFn) {
return defaultMixResult; return defaultMixResult;

View File

@@ -23,18 +23,18 @@ export function Darkens({ color, darkens, mix, step, maximum, copyMode }: Darken
switch (mix) { switch (mix) {
case 'progressive': case 'progressive':
for (let i = 1; i <= darkens; i++) { for (let i = 1; i <= darkens; i++) {
const darkenColor = colorFn.darken(last(darkenColors), step); const darkenColor = colorFn.darken(last(darkenColors) ?? '', step ?? 0);
darkenColors.push(darkenColor); darkenColors.push(darkenColor);
} }
break; break;
case 'linear': case 'linear':
for (let i = 1; i <= darkens; i++) { for (let i = 1; i <= darkens; i++) {
const darkenColor = colorFn.darken(color, step * i); const darkenColor = colorFn.darken(color, (step ?? 0) * i);
darkenColors.push(darkenColor); darkenColors.push(darkenColor);
} }
break; break;
case 'average': { case 'average': {
const interval = maximum / darkens / 100; const interval = (maximum ?? 0) / darkens / 100;
for (let i = 1; i <= darkens; i++) { for (let i = 1; i <= darkens; i++) {
const darkenColor = colorFn.darken(color, interval * i); const darkenColor = colorFn.darken(color, interval * i);
darkenColors.push(darkenColor); darkenColors.push(darkenColor);

View File

@@ -23,18 +23,18 @@ export function Lightens({ color, lightens, mix, step, maximum, copyMode }: Ligh
switch (mix) { switch (mix) {
case 'progressive': case 'progressive':
for (let i = 1; i <= lightens; i++) { for (let i = 1; i <= lightens; i++) {
const lightenColor = colorFn.lighten(last(lightenColors), step); const lightenColor = colorFn.lighten(last(lightenColors) ?? '', step ?? 0);
lightenColors.push(lightenColor); lightenColors.push(lightenColor);
} }
break; break;
case 'linear': case 'linear':
for (let i = 1; i <= lightens; i++) { for (let i = 1; i <= lightens; i++) {
const lightenColor = colorFn.lighten(color, step * i); const lightenColor = colorFn.lighten(color, (step ?? 0) * i);
lightenColors.push(lightenColor); lightenColors.push(lightenColor);
} }
break; break;
case 'average': { case 'average': {
const interval = maximum / lightens / 100; const interval = (maximum ?? 0) / lightens / 100;
for (let i = 1; i <= lightens; i++) { for (let i = 1; i <= lightens; i++) {
const lightenColor = colorFn.lighten(color, interval * i); const lightenColor = colorFn.lighten(color, interval * i);
lightenColors.push(lightenColor); lightenColors.push(lightenColor);

View File

@@ -0,0 +1,13 @@
@layer components {
.delete_btn {
font-size: var(--font-size-xs);
color: var(--color-yuebai);
background-color: oklch(from var(--color-danger) l c h / 0.25);
&:hover {
background-color: oklch(from var(--color-danger-hover) l c h / 0.65);
}
&:active {
background-color: oklch(from var(--color-danger-active) l c h / 0.65);
}
}
}

View File

@@ -0,0 +1,38 @@
import { isEmpty } from 'lodash-es';
import { ActionIcon } from '../../components/ActionIcon';
import { FloatColorPicker } from '../../components/FloatColorPicker';
import styles from './colorEntry.module.css';
export type IdenticalColorEntry = {
id: string;
name: string;
color: string;
};
type ColorEntryProps = {
entry: IdenticalColorEntry;
onDelete?: (index: string) => void;
};
export function ColorEntry({ entry, onDelete }: ColorEntryProps) {
return (
<>
<div className="input_wrapper">
<input type="text" name={`name_${entry.id}`} defaultValue={entry.name} />
</div>
<div>
<FloatColorPicker
name={`color_${entry.id}`}
color={isEmpty(entry.color) ? undefined : entry.color}
/>
</div>
<div>
<ActionIcon
icon="tabler:trash"
extendClassName={styles.delete_btn}
onClick={() => onDelete?.(entry.id)}
/>
</div>
</>
);
}

View File

@@ -1,3 +1,35 @@
export function M2Scheme() { import { isEqual, isNil } from 'lodash-es';
return <div>Material Design 2 Scheme</div>; import { useState } from 'react';
import { Tab } from '../../components/Tab';
import { MaterialDesign2SchemeStorage } from '../../material-2-scheme';
import { SchemeContent } from '../../models';
import { SchemeExport } from './Export';
import { M2SchemeBuilder } from './m2-scheme/Builder';
import { M2SchemePreview } from './m2-scheme/Preview';
const tabOptions = [
{ title: 'Overview', id: 'overview' },
{ title: 'Builder', id: 'builder' },
{ title: 'Exports', id: 'export' },
];
type M2SchemeProps = {
scheme: SchemeContent<MaterialDesign2SchemeStorage>;
};
export function M2Scheme({ scheme }: M2SchemeProps) {
const [activeTab, setActiveTab] = useState<(typeof tabOptions)[number]['id']>(() =>
isNil(scheme.schemeStorage.scheme) ? 'builder' : 'overview',
);
return (
<>
<Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
{isEqual(activeTab, 'overview') && <M2SchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && (
<M2SchemeBuilder scheme={scheme} onBuildComplete={() => setActiveTab('overview')} />
)}
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
</>
);
} }

View File

@@ -1,3 +1,35 @@
export function M3Scheme() { import { isEqual, isNil } from 'lodash-es';
return <div>Material Design 3 Scheme</div>; import { useState } from 'react';
import { Tab } from '../../components/Tab';
import { MaterialDesign3SchemeStorage } from '../../material-3-scheme';
import { SchemeContent } from '../../models';
import { SchemeExport } from './Export';
import { M3SchemeBuilder } from './m3-scheme/Builder';
import { M3SchemePreview } from './m3-scheme/Preview';
const tabOptions = [
{ title: 'Overview', id: 'overview' },
{ title: 'Builder', id: 'builder' },
{ title: 'Exports', id: 'export' },
];
type M3SchemeProps = {
scheme: SchemeContent<MaterialDesign3SchemeStorage>;
};
export function M3Scheme({ scheme }: M3SchemeProps) {
const [activeTab, setActiveTab] = useState<(typeof tabOptions)[number]['id']>(() =>
isNil(scheme.schemeStorage.scheme) ? 'builder' : 'overview',
);
return (
<>
<Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
{isEqual(activeTab, 'overview') && <M3SchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && (
<M3SchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
)}
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
</>
);
} }

View File

@@ -24,7 +24,7 @@ export function QScheme({ scheme }: QSchemeProps) {
return ( return (
<> <>
<Tab tabs={tabOptions} activeTab={activeTab} onActive={setActiveTab} /> <Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
{isEqual(activeTab, 'overview') && <QSchemePreview scheme={scheme} />} {isEqual(activeTab, 'overview') && <QSchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && ( {isEqual(activeTab, 'builder') && (
<QSchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} /> <QSchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />

View File

@@ -24,7 +24,7 @@ export function SwatchScheme({ scheme }: SwatchSchemeProps) {
return ( return (
<> <>
<Tab tabs={tabOptions} activeTab={activeTab} onActive={setActiveTab} /> <Tab tabs={tabOptions} activeTab={activeTab} onActive={(v) => setActiveTab(v as string)} />
{isEqual(activeTab, 'overview') && <SwatchSchemePreview scheme={scheme} />} {isEqual(activeTab, 'overview') && <SwatchSchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && ( {isEqual(activeTab, 'builder') && (
<SwatchSchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} /> <SwatchSchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />

View File

@@ -0,0 +1,46 @@
@layer pages {
.builder_layout {
padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-s);
line-height: 1.3em;
display: grid;
grid-template-columns: repeat(3, 200px);
align-items: center;
gap: var(--spacing-xs);
.label {
max-width: 200px;
grid-column: 1;
padding-inline-end: var(--spacing-m);
text-align: right;
}
.segment_title {
grid-column: 1 / span 2;
text-align: center;
}
.color_picker_row {
grid-column: 2 / span 2;
display: flex;
align-items: center;
gap: var(--spacing-s);
}
h5 {
font-size: var(--font-size-m);
line-height: 1.7em;
}
.error_msg {
color: var(--color-danger);
font-size: var(--font-size-xs);
}
.delete_btn {
font-size: var(--font-size-xs);
color: var(--color-yuebai);
background-color: oklch(from var(--color-danger) l c h / 0.25);
&:hover {
background-color: oklch(from var(--color-danger-hover) l c h / 0.65);
}
&:active {
background-color: oklch(from var(--color-danger-active) l c h / 0.65);
}
}
}
}

View File

@@ -0,0 +1,178 @@
import { includes, isEmpty, isNil, merge } from 'lodash-es';
import { useActionState, useCallback, useMemo, useState } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { ScrollArea } from '../../../components/ScrollArea';
import { MaterialDesign2SchemeStorage } from '../../../material-2-scheme';
import { SchemeContent } from '../../../models';
import { useUpdateScheme } from '../../../stores/schemes';
import { mapToObject } from '../../../utls';
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
import styles from './Builder.module.css';
type M2SchemeBuilderProps = {
scheme: SchemeContent<MaterialDesign2SchemeStorage>;
onBuildComplete?: () => void;
};
export function M2SchemeBuilder({ scheme, onBuildComplete }: M2SchemeBuilderProps) {
const { colorFn } = useColorFunction();
const updateScheme = useUpdateScheme(scheme.id);
const originalColors = useMemo(() => {
return Object.entries(scheme.schemeStorage.source?.custom_colors ?? {}).map(
([name, color], index) => ({ id: `oc_${index}`, name, color } as IdenticalColorEntry),
);
}, [scheme.schemeStorage.source]);
const [newColors, setNewColors] = useState<IdenticalColorEntry[]>([]);
const [deleted, setDeleted] = useState<string[]>([]);
const addEntryAction = useCallback(() => {
setNewColors((prev) => [...prev, { id: `nc_${prev.length}`, name: '', color: '' }]);
}, []);
const colorKeys = useMemo(
() =>
[...originalColors, ...newColors]
.map((color) => color.id)
.filter((c) => !includes(deleted, c)),
[originalColors, newColors, deleted],
);
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>();
try {
const primaryColor = formData.get('primary') as string;
if (isNil(primaryColor) || isEmpty(primaryColor)) {
errMsg.set('primary', 'Primary color is required');
}
const secondaryColor = formData.get('secondary') as string;
if (isNil(secondaryColor) || isEmpty(secondaryColor)) {
errMsg.set('secondary', 'Secondary color is required');
}
const errorColor = formData.get('error') as string;
if (isNil(errorColor) || isEmpty(errorColor)) {
errMsg.set('error', 'Error color is required');
}
if (!isEmpty(errMsg)) return errMsg;
const customColors: Record<string, string> = {};
for (const key of colorKeys) {
const name = formData.get(`name_${key}`) as string;
const color = formData.get(`color_${key}`) as string;
if (isNil(name) || isEmpty(name) || isNil(color) || isEmpty(color)) continue;
customColors[name] = color;
}
const generatedScheme = colorFn?.generate_material_design_2_scheme(
primaryColor,
secondaryColor,
errorColor,
customColors,
);
updateScheme((prev) => {
prev.schemeStorage.source = {
primary: primaryColor,
secondary: secondaryColor,
error: errorColor,
custom_colors: customColors,
};
prev.schemeStorage.scheme = merge(generatedScheme[0], {
light: { custom_colors: mapToObject(generatedScheme[0].light.custom_colors) },
dark: { custom_colors: mapToObject(generatedScheme[0].dark.custom_colors) },
});
prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.scssVariables = generatedScheme[2];
prev.schemeStorage.jsVariables = generatedScheme[3];
return prev;
});
onBuildComplete?.();
} catch (e) {
console.error('[generate m2 scheme]', e);
}
return errMsg;
},
new Map<string, string>(),
);
return (
<ScrollArea enableY>
<form action={handleSubmitAction} className={styles.builder_layout}>
<h5 className={styles.segment_title}>Required Colors</h5>
<label className={styles.label}>Primary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="primary"
color={
isNil(scheme.schemeStorage.source?.primary) ||
isEmpty(scheme.schemeStorage.source?.primary)
? undefined
: scheme.schemeStorage.source.primary
}
/>
{errMsg.get('primary') && (
<span className={styles.error_msg}>{errMsg.get('primary')}</span>
)}
</div>
<label className={styles.label}>Secondary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="secondary"
color={
isNil(scheme.schemeStorage.source?.secondary) ||
isEmpty(scheme.schemeStorage.source.secondary)
? undefined
: scheme.schemeStorage.source.secondary
}
/>
{errMsg.get('secondary') && (
<span className={styles.error_msg}>{errMsg.get('secondary')}</span>
)}
</div>
<label className={styles.label}>Error Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="error"
color={
isNil(scheme.schemeStorage.source?.error) ||
isEmpty(scheme.schemeStorage.source.error)
? undefined
: scheme.schemeStorage.source.error
}
/>
{errMsg.get('error') && <span className={styles.error_msg}>{errMsg.get('error')}</span>}
</div>
<h5 className={styles.segment_title}>Custom Colors</h5>
<label style={{ gridColumn: 1 }}>Name</label>
<label>Color</label>
<div>
<button type="button" className="small" onClick={addEntryAction}>
Add Color
</button>
</div>
{originalColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
{newColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
<div style={{ gridColumn: '2 / span 2' }}>
<button type="submit" className="primary">
Build Scheme
</button>
</div>
</form>
</ScrollArea>
);
}

View File

@@ -0,0 +1,41 @@
@layer pages {
.preview_layout {
padding: var(--spacing-s) var(--spacing-m);
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-m);
}
.preview_block {
width: inherit;
padding: var(--spacing-m) var(--spacing-m);
display: flex;
flex-direction: column;
gap: var(--spacing-m);
font-size: var(--font-size-s);
line-height: 1.1em;
h4 {
font-size: var(--font-size-m);
font-weight: bold;
line-height: 1.7em;
}
}
.horizontal_set {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
gap: 0;
}
.color_block {
padding: var(--spacing-s) var(--spacing-xs);
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
.wacg {
font-size: var(--font-size-xxs);
}
}
}

View File

@@ -0,0 +1,94 @@
import { useMemo } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { ScrollArea } from '../../../components/ScrollArea';
import { Baseline, ColorSet, MaterialDesign2SchemeStorage } from '../../../material-2-scheme';
import { SchemeContent } from '../../../models';
import styles from './Preview.module.css';
type M2SchemeFunctionColorProps = {
title: string;
color: ColorSet;
};
function M2SchemeFunctionColor({ title, color }: M2SchemeFunctionColorProps) {
const { colorFn } = useColorFunction();
const rootWacgRatio = useMemo(() => {
try {
return colorFn?.wacg_relative_contrast(color.on, color.root) ?? null;
} catch (e) {
console.error('[calc root wacg]', e);
}
return null;
}, [colorFn, color.on, color.root]);
const variantWacgRatio = useMemo(() => {
try {
return colorFn?.wacg_relative_contrast(color.on, color.variant) ?? null;
} catch (e) {
console.error('[calc variant wacg]', e);
}
return null;
}, [colorFn, color.on, color.variant]);
return (
<div className={styles.horizontal_set}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${color.root}`, color: `#${color.on}` }}>
<span>{title}</span>
<span className={styles.wacg}>WACG: {rootWacgRatio?.toFixed(2)}</span>
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${color.variant}`, color: `#${color.on}` }}>
<span>{title} Variant</span>
<span className={styles.wacg}>WACG: {variantWacgRatio?.toFixed(2)}</span>
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${color.on}`, color: `#${color.root}` }}>
On {title}
</div>
</div>
);
}
type PreviewBlockProps = {
title: string;
baseline: Baseline;
};
function PreviewBlock({ title, baseline }: PreviewBlockProps) {
return (
<div
className={styles.preview_block}
style={{
backgroundColor: `#${baseline.background.root}`,
color: `#${baseline.background.on}`,
}}>
<h4>{title} Scheme</h4>
<M2SchemeFunctionColor title="Primary" color={baseline.primary} />
<M2SchemeFunctionColor title="Secondary" color={baseline.secondary} />
<M2SchemeFunctionColor title="Error" color={baseline.error} />
<M2SchemeFunctionColor title="Background" color={baseline.background} />
<M2SchemeFunctionColor title="Surface" color={baseline.surface} />
{Object.entries(baseline.custom_colors).map(([name, set], index) => (
<M2SchemeFunctionColor key={index} title={name} color={set} />
))}
</div>
);
}
type M2SchemePreviewProps = {
scheme: SchemeContent<MaterialDesign2SchemeStorage>;
};
export function M2SchemePreview({ scheme }: M2SchemePreviewProps) {
return (
<ScrollArea enableY>
<div className={styles.preview_layout}>
<PreviewBlock title="Light" baseline={scheme.schemeStorage.scheme!.light} />
<PreviewBlock title="Dark" baseline={scheme.schemeStorage.scheme!.dark} />
</div>
</ScrollArea>
);
}

View File

@@ -0,0 +1,46 @@
@layer pages {
.builder_layout {
padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-s);
line-height: 1.3em;
display: grid;
grid-template-columns: repeat(3, 200px);
align-items: center;
gap: var(--spacing-xs);
.label {
max-width: 200px;
grid-column: 1;
padding-inline-end: var(--spacing-m);
text-align: right;
}
.segment_title {
grid-column: 1 / span 2;
text-align: center;
}
.color_picker_row {
grid-column: 2 / span 2;
display: flex;
align-items: center;
gap: var(--spacing-s);
}
h5 {
font-size: var(--font-size-m);
line-height: 1.7em;
}
.error_msg {
color: var(--color-danger);
font-size: var(--font-size-xs);
}
.delete_btn {
font-size: var(--font-size-xs);
color: var(--color-yuebai);
background-color: oklch(from var(--color-danger) l c h / 0.25);
&:hover {
background-color: oklch(from var(--color-danger-hover) l c h / 0.65);
}
&:active {
background-color: oklch(from var(--color-danger-active) l c h / 0.65);
}
}
}
}

View File

@@ -0,0 +1,165 @@
import { includes, isEmpty, isNil } from 'lodash-es';
import { useActionState, useCallback, useMemo, useState } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { ScrollArea } from '../../../components/ScrollArea';
import { MaterialDesign3Scheme, MaterialDesign3SchemeStorage } from '../../../material-3-scheme';
import { SchemeContent } from '../../../models';
import { useUpdateScheme } from '../../../stores/schemes';
import { mapToObject } from '../../../utls';
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
import styles from './Builder.module.css';
type M3SchemeBuilderProps = {
scheme: SchemeContent<MaterialDesign3SchemeStorage>;
onBuildCompleted?: () => void;
};
export function M3SchemeBuilder({ scheme, onBuildCompleted }: M3SchemeBuilderProps) {
const { colorFn } = useColorFunction();
const updateScheme = useUpdateScheme(scheme.id);
const originalColors = useMemo(() => {
return Object.entries(scheme.schemeStorage.source?.custom_colors ?? {}).map(
([name, color], index) => ({ id: `oc_${index}`, name, color } as IdenticalColorEntry),
);
}, [scheme.schemeStorage.source]);
const [newColors, setNewColors] = useState<IdenticalColorEntry[]>([]);
const [deleted, setDeleted] = useState<string[]>([]);
const addEntryAction = useCallback(() => {
setNewColors((prev) => [...prev, { id: `nc_${prev.length}`, name: '', color: '' }]);
}, []);
const colorKeys = useMemo(
() =>
[...originalColors, ...newColors]
.map((color) => color.id)
.filter((c) => !includes(deleted, c)),
[originalColors, newColors, deleted],
);
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>();
try {
const sourceColor = formData.get('source') as string;
if (isNil(sourceColor) || isEmpty(sourceColor)) {
errMsg.set('source', 'Source color is required');
}
const errorColor = formData.get('error') as string;
if (isNil(errorColor) || isEmpty(errorColor)) {
errMsg.set('error', 'Error color is required');
}
if (!isEmpty(errMsg)) return errMsg;
const customColors: Record<string, string> = {};
for (const key of colorKeys) {
const name = formData.get(`name_${key}`) as string;
const color = formData.get(`color_${key}`) as string;
if (isNil(name) || isEmpty(name) || isNil(color) || isEmpty(color)) continue;
customColors[name] = color;
}
const generatedScheme = colorFn?.generate_material_design_3_scheme(
sourceColor,
errorColor,
customColors,
);
updateScheme((prev) => {
prev.schemeStorage.source = {
source: sourceColor as string,
error: errorColor as string,
custom_colors: customColors,
};
prev.schemeStorage.scheme = {
white: generatedScheme[0].white,
black: generatedScheme[0].black,
light_baseline: {
...generatedScheme[0].light_baseline,
customs: mapToObject(generatedScheme[0].light_baseline.customs),
},
dark_baseline: {
...generatedScheme[0].dark_baseline,
customs: mapToObject(generatedScheme[0].dark_baseline.customs),
},
} as MaterialDesign3Scheme;
prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.scssVariables = generatedScheme[2];
prev.schemeStorage.jsVariables = generatedScheme[3];
return prev;
});
onBuildCompleted?.();
} catch (e) {
console.error('[generate m3 scheme]', e);
}
return errMsg;
},
new Map<string, string>(),
);
return (
<ScrollArea enableY>
<form action={handleSubmitAction} className={styles.builder_layout}>
<h5 className={styles.segment_title}>Required Colors</h5>
<label className={styles.label}>Source Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="source"
color={
isNil(scheme.schemeStorage.source?.source) ||
isEmpty(scheme.schemeStorage.source?.source)
? undefined
: scheme.schemeStorage.source?.source
}
/>
{errMsg.has('source') && <span className={styles.error_msg}>{errMsg.get('source')}</span>}
</div>
<label className={styles.label}>Error Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="error"
color={
isNil(scheme.schemeStorage.source?.error) ||
isEmpty(scheme.schemeStorage.source.error)
? undefined
: scheme.schemeStorage.source.error
}
/>
{errMsg.has('error') && <span className={styles.error_msg}>{errMsg.get('error')}</span>}
</div>
<h5 className={styles.segment_title}>Custom Colors</h5>
<label style={{ gridColumn: 1 }}>Name</label>
<label>Color</label>
<div>
<button type="button" className="small" onClick={addEntryAction}>
Add Color
</button>
</div>
{originalColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
{newColors
.filter((color) => !includes(deleted, color.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
<div style={{ gridColumn: '2 / span 2' }}>
<button type="submit" className="primary">
Build Scheme
</button>
</div>
</form>
</ScrollArea>
);
}

View File

@@ -0,0 +1,89 @@
@layer pages {
.preview_layout {
padding: var(--spacing-s) var(--spacing-m);
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-m);
}
.preview_block {
width: inherit;
padding: var(--spacing-m) var(--spacing-m);
display: flex;
flex-direction: column;
gap: var(--spacing-m);
font-size: var(--font-size-xs);
line-height: 1.1em;
.main_grid {
display: grid;
grid-template-columns: 3fr 1fr;
gap: var(--spacing-m);
.main_color_sets {
grid-column: 1;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
gap: var(--spacing-xs);
}
.surface_sets {
grid-column: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-xs);
.surface_basics {
flex-grow: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
}
.surface_levels {
flex-grow: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0;
}
.surface_variants {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
}
}
.additional_sets {
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-xs);
.constants {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-s);
}
}
}
h4 {
font-size: var(--font-size-m);
font-weight: bold;
line-height: 1.7em;
}
}
.vertical_set {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
}
.horizontal_set {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
gap: 0;
}
.color_block {
padding: var(--spacing-s) var(--spacing-xs);
flex: 1;
}
}

View File

@@ -0,0 +1,272 @@
import { ScrollArea } from '../../../components/ScrollArea';
import {
Baseline,
ColorSet,
MaterialDesign3SchemeStorage,
Surface,
} from '../../../material-3-scheme';
import { SchemeContent } from '../../../models';
import styles from './Preview.module.css';
type ColorSetProps = {
name: string;
set: ColorSet;
};
function VerticalColorSet({ name, set }: ColorSetProps) {
return (
<div className={styles.vertical_set}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${set.root}`, color: `#${set.on_root}` }}>
{name}
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${set.on_root}`, color: `#${set.root}` }}>
On {name}
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${set.container}`,
color: `#${set.on_container}`,
}}>
{name} Container
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${set.on_container}`,
color: `#${set.container}`,
}}>
On {name} Container
</div>
</div>
);
}
function HorizontalColorSet({ name, set }: ColorSetProps) {
return (
<div className={styles.horizontal_set}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${set.root}`, color: `#${set.on_root}` }}>
{name}
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${set.on_root}`, color: `#${set.root}` }}>
On {name}
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${set.container}`,
color: `#${set.on_container}`,
}}>
{name} Container
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${set.on_container}`,
color: `#${set.container}`,
}}>
On {name} Container
</div>
</div>
);
}
type SurfaceColorSetProps = {
surface: Surface;
outline: string;
outlineVariant: string;
};
function SurfaceColorSet({ surface, outline, outlineVariant }: SurfaceColorSetProps) {
return (
<div className={styles.surface_sets}>
<div className={styles.surface_basics}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.dim}`, color: `#${surface.on_root}` }}>
Surface Dim
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.root}`, color: `#${surface.on_root}` }}>
Surface
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.bright}`, color: `#${surface.on_root}` }}>
Surface Bright
</div>
</div>
<div className={styles.surface_levels}>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.container_lowest}`, color: `#${surface.on_root}` }}>
Surf. Container Lowest
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.container_low}`, color: `#${surface.on_root}` }}>
Surf. Container Low
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.container}`, color: `#${surface.on_root}` }}>
Surf. Container
</div>
<div
className={styles.color_block}
style={{ backgroundColor: `#${surface.container_high}`, color: `#${surface.on_root}` }}>
Surf. Container High
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${surface.container_highest}`,
color: `#${surface.on_root}`,
}}>
Surf. Container Highest
</div>
</div>
<div className={styles.surface_variants}>
<div
className={styles.color_block}
style={{
backgroundColor: `#${surface.on_root}`,
color: `#${surface.root}`,
}}>
On Surface
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${surface.on_root_variant}`,
color: `#${surface.root}`,
}}>
On Surface Var.
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${outline}`,
color: `#${surface.root}`,
}}>
Outline
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${outlineVariant}`,
color: `#${surface.on_root}`,
}}>
Outline Variant
</div>
</div>
</div>
);
}
type AdditionalColorSetsProps = {
baseline: Baseline;
};
function AdditionalColorSets({ baseline }: AdditionalColorSetsProps) {
return (
<div className={styles.additional_sets}>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.surface.inverse}`,
color: `#${baseline.surface.on_inverse}`,
}}>
Inverse Surface
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.surface.on_inverse}`,
color: `#${baseline.surface.inverse}`,
}}>
Inverse On Surface
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.primary.inverse}`,
color: `#${baseline.surface.on_root}`,
}}>
Inverse Primary
</div>
<div className={styles.constants}>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.scrim}`,
color: `#ffffff`,
}}>
Scrim
</div>
<div
className={styles.color_block}
style={{
backgroundColor: `#${baseline.shadow}`,
color: `#ffffff`,
}}>
Shadow
</div>
</div>
</div>
);
}
type PreviewBlockProps = {
title: string;
baseline: Baseline;
};
function PreviewBlock({ title, baseline }: PreviewBlockProps) {
return (
<div className={styles.preview_block} style={{ backgroundColor: `#${baseline.surface.root}` }}>
<h4 style={{ color: `#${baseline.surface.on_root}` }}>{title}</h4>
<div className={styles.main_grid}>
<div className={styles.main_color_sets}>
<VerticalColorSet name="Primary" set={baseline.primary} />
<VerticalColorSet name="Secondary" set={baseline.secondary} />
<VerticalColorSet name="Tertiary" set={baseline.tertiary} />
</div>
<VerticalColorSet name="Error" set={baseline.error} />
<SurfaceColorSet
surface={baseline.surface}
outline={baseline.outline}
outlineVariant={baseline.outline_variant}
/>
<AdditionalColorSets baseline={baseline} />
</div>
{Object.entries(baseline.customs).map(([name, set], index) => (
<HorizontalColorSet key={index} name={name} set={set} />
))}
</div>
);
}
type M3SchemePreviewProps = {
scheme: SchemeContent<MaterialDesign3SchemeStorage>;
};
export function M3SchemePreview({ scheme }: M3SchemePreviewProps) {
return (
<ScrollArea enableY>
<div className={styles.preview_layout}>
<PreviewBlock title="Light Scheme" baseline={scheme.schemeStorage.scheme!.light_baseline} />
<PreviewBlock title="Dark Scheme" baseline={scheme.schemeStorage.scheme!.dark_baseline} />
</div>
</ScrollArea>
);
}

View File

@@ -1,13 +1,8 @@
import { ColorExpand, ColorShifting, SchemeSetting, WACGSetting } from 'color-module';
import { every, isEmpty, isNil } from 'lodash-es'; import { every, isEmpty, isNil } from 'lodash-es';
import { useActionState, useMemo } from 'react'; import { useActionState, useMemo } from 'react';
import {
ColorExpand,
ColorShifting,
SchemeSetting,
WACGSetting,
} from '../../../color_functions/color_module';
import { useColorFunction } from '../../../ColorFunctionContext'; import { useColorFunction } from '../../../ColorFunctionContext';
import { FloatColorPicker } from '../../../components/FloatcolorPicker'; import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { ScrollArea } from '../../../components/ScrollArea'; import { ScrollArea } from '../../../components/ScrollArea';
import { VSegmentedControl } from '../../../components/VSegmentedControl'; import { VSegmentedControl } from '../../../components/VSegmentedControl';
import { SchemeContent } from '../../../models'; import { SchemeContent } from '../../../models';
@@ -82,7 +77,8 @@ export function QSchemeBuilder({ scheme, onBuildCompleted }: QSchemeBuilderProps
return []; return [];
}, []); }, []);
const [errMsg, handleSubmitAction] = useActionState((state, formData) => { const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>(); const errMsg = new Map<string, string>();
const requiredFields = [ const requiredFields = [
'primary', 'primary',
@@ -128,9 +124,9 @@ export function QSchemeBuilder({ scheme, onBuildCompleted }: QSchemeBuilderProps
const source: QSchemeSource = { const source: QSchemeSource = {
primary: defaultEmptyFormData(formData, 'primary', null), primary: defaultEmptyFormData(formData, 'primary', null),
secondary: defaultEmptyFormData(formData, 'secondary', undefined), secondary: defaultEmptyFormData(formData, 'secondary', null),
tertiary: defaultEmptyFormData(formData, 'tertiary', undefined), tertiary: defaultEmptyFormData(formData, 'tertiary', null),
accent: defaultEmptyFormData(formData, 'accent', undefined), accent: defaultEmptyFormData(formData, 'accent', null),
danger: defaultEmptyFormData(formData, 'danger', null), danger: defaultEmptyFormData(formData, 'danger', null),
success: defaultEmptyFormData(formData, 'success', null), success: defaultEmptyFormData(formData, 'success', null),
warning: defaultEmptyFormData(formData, 'warn', null), warning: defaultEmptyFormData(formData, 'warn', null),
@@ -141,26 +137,26 @@ export function QSchemeBuilder({ scheme, onBuildCompleted }: QSchemeBuilderProps
}; };
const generatedScheme = every([source.secondary, source.tertiary, source.accent], isNil) const generatedScheme = every([source.secondary, source.tertiary, source.accent], isNil)
? colorFn?.generate_q_scheme_automatically( ? colorFn?.generate_q_scheme_automatically(
source.primary, source.primary ?? '',
source.danger, source.danger ?? '',
source.success, source.success ?? '',
source.warning, source.warning ?? '',
source.info, source.info ?? '',
source.foreground, source.foreground ?? '',
source.background, source.background ?? '',
schemeSetting, schemeSetting,
) )
: colorFn?.generate_q_scheme_manually( : colorFn?.generate_q_scheme_manually(
source.primary, source.primary ?? '',
source.secondary ?? undefined, source.secondary ?? undefined,
source.tertiary ?? undefined, source.tertiary ?? undefined,
source.accent ?? undefined, source.accent ?? undefined,
source.danger, source.danger ?? '',
source.success, source.success ?? '',
source.warning, source.warning ?? '',
source.info, source.info ?? '',
source.foreground, source.foreground ?? '',
source.background, source.background ?? '',
schemeSetting, schemeSetting,
); );
updateScheme((prev) => { updateScheme((prev) => {
@@ -177,7 +173,9 @@ export function QSchemeBuilder({ scheme, onBuildCompleted }: QSchemeBuilderProps
} }
return errMsg; return errMsg;
}, new Map<string, string>()); },
new Map<string, string>(),
);
return ( return (
<ScrollArea enableY> <ScrollArea enableY>

View File

@@ -1,55 +1,21 @@
import { ColorShifting, SwatchEntry, SwatchSchemeSetting } from 'color-module';
import { includes, isEmpty, isEqual, isNaN } from 'lodash-es'; import { includes, isEmpty, isEqual, isNaN } from 'lodash-es';
import { useActionState, useCallback, useMemo, useState } from 'react'; import { useActionState, useCallback, useMemo, useState } from 'react';
import {
ColorShifting,
SwatchEntry,
SwatchSchemeSetting,
} from '../../../color_functions/color_module';
import { useColorFunction } from '../../../ColorFunctionContext'; import { useColorFunction } from '../../../ColorFunctionContext';
import { ActionIcon } from '../../../components/ActionIcon';
import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { ScrollArea } from '../../../components/ScrollArea'; import { ScrollArea } from '../../../components/ScrollArea';
import { Switch } from '../../../components/Switch'; import { Switch } from '../../../components/Switch';
import { SchemeContent } from '../../../models'; import { SchemeContent } from '../../../models';
import { useUpdateScheme } from '../../../stores/schemes'; import { useUpdateScheme } from '../../../stores/schemes';
import { QSwatchEntry, QSwatchSchemeSetting, SwatchSchemeStorage } from '../../../swatch_scheme'; import {
QSwatchEntry,
QSwatchSchemeSetting,
SwatchScheme,
SwatchSchemeStorage,
} from '../../../swatch_scheme';
import { mapToObject } from '../../../utls'; import { mapToObject } from '../../../utls';
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
import styles from './Builder.module.css'; import styles from './Builder.module.css';
type IdenticalColorEntry = {
id: string;
name: string;
color: string;
};
type ColorEntryProps = {
entry: IdenticalColorEntry;
onDelete?: (index: string) => void;
};
function ColorEntry({ entry, onDelete }: ColorEntryProps) {
return (
<>
<div className="input_wrapper">
<input type="text" name={`name_${entry.id}`} defaultValue={entry.name} />
</div>
<div>
<FloatColorPicker
name={`color_${entry.id}`}
color={isEmpty(entry.color) ? undefined : entry.color}
/>
</div>
<div>
<ActionIcon
icon="tabler:trash"
extendClassName={styles.delete_btn}
onClick={() => onDelete?.(entry.id)}
/>
</div>
</>
);
}
type SwatchSchemeBuilderProps = { type SwatchSchemeBuilderProps = {
scheme: SchemeContent<SwatchSchemeStorage>; scheme: SchemeContent<SwatchSchemeStorage>;
onBuildCompleted?: () => void; onBuildCompleted?: () => void;
@@ -99,7 +65,8 @@ export function SwatchSchemeBuilder({ scheme, onBuildCompleted }: SwatchSchemeBu
return null; return null;
}, [scheme.schemeStorage.source]); }, [scheme.schemeStorage.source]);
const [errMsg, handleSubmitAction] = useActionState((state, formData) => { const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>(); const errMsg = new Map<string, string>();
try { try {
@@ -154,7 +121,7 @@ export function SwatchSchemeBuilder({ scheme, onBuildCompleted }: SwatchSchemeBu
colors: dumpedEntries, colors: dumpedEntries,
setting: dumpedSettings, setting: dumpedSettings,
}; };
prev.schemeStorage.scheme = mapToObject(generatedScheme[0]); prev.schemeStorage.scheme = mapToObject(generatedScheme[0]) as SwatchScheme;
prev.schemeStorage.cssVariables = generatedScheme[1]; prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.scssVariables = generatedScheme[2]; prev.schemeStorage.scssVariables = generatedScheme[2];
prev.schemeStorage.jsVariables = generatedScheme[3]; prev.schemeStorage.jsVariables = generatedScheme[3];
@@ -167,7 +134,9 @@ export function SwatchSchemeBuilder({ scheme, onBuildCompleted }: SwatchSchemeBu
} }
return errMsg; return errMsg;
}, new Map<string, string>()); },
new Map<string, string>(),
);
return ( return (
<ScrollArea enableY> <ScrollArea enableY>

View File

@@ -54,12 +54,12 @@ export function SwatchSchemePreview({ scheme }: SwatchSchemePreviewProps) {
<h2>Light Scheme</h2> <h2>Light Scheme</h2>
<SchemeBlock <SchemeBlock
amount={scheme.schemeStorage.source?.setting?.amount ?? 0} amount={scheme.schemeStorage.source?.setting?.amount ?? 0}
scheme={scheme.schemeStorage.scheme.light} scheme={scheme.schemeStorage.scheme!.light}
/> />
<h2>Dark Scheme</h2> <h2>Dark Scheme</h2>
<SchemeBlock <SchemeBlock
amount={scheme.schemeStorage.source?.setting?.amount ?? 0} amount={scheme.schemeStorage.source?.setting?.amount ?? 0}
scheme={scheme.schemeStorage.scheme.dark} scheme={scheme.schemeStorage.scheme!.dark}
/> />
</div> </div>
</ScrollArea> </ScrollArea>

View File

@@ -23,18 +23,18 @@ export function Shades({ color, shades, mix, step, maximum, copyMode }: ShadesLi
switch (mix) { switch (mix) {
case 'progressive': case 'progressive':
for (let i = 1; i <= shades; i++) { for (let i = 1; i <= shades; i++) {
const shade = colorFn!.shade(last(genColors), step); const shade = colorFn!.shade(last(genColors) ?? '', step ?? 0);
genColors.push(shade); genColors.push(shade);
} }
break; break;
case 'linear': case 'linear':
for (let i = 1; i <= shades; i++) { for (let i = 1; i <= shades; i++) {
const shade = colorFn!.shade(color, step * i); const shade = colorFn!.shade(color, (step ?? 0) * i);
genColors.push(shade); genColors.push(shade);
} }
break; break;
case 'average': { case 'average': {
const interval = maximum / shades / 100; const interval = (maximum ?? 0) / shades / 100;
for (let i = 1; i <= shades; i++) { for (let i = 1; i <= shades; i++) {
const shade = colorFn!.shade(color, interval * i); const shade = colorFn!.shade(color, interval * i);
genColors.push(shade); genColors.push(shade);

View File

@@ -23,18 +23,18 @@ export function Tints({ color, tints, mix, step, maximum, copyMode }: TintsListP
switch (mix) { switch (mix) {
case 'progressive': case 'progressive':
for (let i = 1; i <= tints; i++) { for (let i = 1; i <= tints; i++) {
const tint = colorFn!.tint(last(genColors), step); const tint = colorFn!.tint(last(genColors) ?? '', step ?? 0);
genColors.push(tint); genColors.push(tint);
} }
break; break;
case 'linear': case 'linear':
for (let i = 1; i <= tints; i++) { for (let i = 1; i <= tints; i++) {
const tint = colorFn!.tint(color, step * i); const tint = colorFn!.tint(color, (step ?? 0) * i);
genColors.push(tint); genColors.push(tint);
} }
break; break;
case 'average': { case 'average': {
const interval = maximum / tints / 100; const interval = (maximum ?? 0) / tints / 100;
for (let i = 1; i <= tints; i++) { for (let i = 1; i <= tints; i++) {
const tint = colorFn!.tint(color, interval * i); const tint = colorFn!.tint(color, interval * i);
genColors.push(tint); genColors.push(tint);

View File

@@ -8,6 +8,7 @@ import { ColorDescription } from '../models';
import { ColorCard } from '../page-components/cards-detail/ColorCard'; import { ColorCard } from '../page-components/cards-detail/ColorCard';
import styles from './CardsDetail.module.css'; import styles from './CardsDetail.module.css';
type ColorModes = 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
type CardsDetailProps = { type CardsDetailProps = {
mainTag: string; mainTag: string;
}; };
@@ -20,7 +21,7 @@ export function CardsDetail({ mainTag }: CardsDetailProps) {
} }
try { try {
const embededCategories = colorFn.color_categories() as { label: string; value: string }[]; const embededCategories = colorFn.color_categories() as { label: string; value: string }[];
return embededCategories.filter((cate) => !isEqual(cate.get('value'), 'unknown')); return embededCategories.filter((cate) => !isEqual(cate.value, 'unknown'));
} catch (e) { } catch (e) {
console.error('[Fetch color categories]', e); console.error('[Fetch color categories]', e);
} }
@@ -31,7 +32,7 @@ export function CardsDetail({ mainTag }: CardsDetailProps) {
const selectedValue = e.target.value; const selectedValue = e.target.value;
setCategory(selectedValue); setCategory(selectedValue);
}; };
const [mode, setMode] = useState<'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'>('hex'); const [mode, setMode] = useState<ColorModes>('hex');
const colors = useMemo(() => { const colors = useMemo(() => {
if (!colorFn) { if (!colorFn) {
return []; return [];
@@ -69,8 +70,8 @@ export function CardsDetail({ mainTag }: CardsDetailProps) {
onChange={handleSelectCategory}> onChange={handleSelectCategory}>
<option value="null">All</option> <option value="null">All</option>
{categories.map((cate, index) => ( {categories.map((cate, index) => (
<option key={`${cate.get('value')}-${index}`} value={cate.get('value')}> <option key={`${cate.value}-${index}`} value={cate.value}>
{cate.get('label')} {cate.label}
</option> </option>
))} ))}
</select> </select>
@@ -85,7 +86,7 @@ export function CardsDetail({ mainTag }: CardsDetailProps) {
{ label: 'OKLCH', value: 'oklch' }, { label: 'OKLCH', value: 'oklch' },
]} ]}
value={mode} value={mode}
onChange={setMode} onChange={(v) => setMode(v as ColorModes)}
/> />
</div> </div>
<ScrollArea enableY> <ScrollArea enableY>

View File

@@ -44,7 +44,7 @@ export function ColorCompare() {
{ label: 'Relative', value: 'relative' }, { label: 'Relative', value: 'relative' },
]} ]}
value={analysisMode} value={analysisMode}
onChange={setMode} onChange={(v) => setMode(v as Parameters<typeof setMode>[0])}
/> />
</Labeled> </Labeled>
<RGBCompare basic={basicColor} compare={compareColor} mode={analysisMode} /> <RGBCompare basic={basicColor} compare={compareColor} mode={analysisMode} />

View File

@@ -114,7 +114,7 @@ export function Harmonies() {
<div className={styles.mode_navigation}> <div className={styles.mode_navigation}>
<h5>Color selection method</h5> <h5>Color selection method</h5>
<VSegmentedControl <VSegmentedControl
onChange={setSelectedMode} onChange={(v) => setSelectedMode(v as string)}
options={[ options={[
{ label: 'Complementary', value: 'complementary' }, { label: 'Complementary', value: 'complementary' },
{ label: 'Analogous', value: 'analogous' }, { label: 'Analogous', value: 'analogous' },

View File

@@ -13,6 +13,8 @@ import { Lightens } from '../page-components/lighten-darken/lightens';
import { currentPickedColor } from '../stores/colors'; import { currentPickedColor } from '../stores/colors';
import styles from './LightenDarken.module.css'; import styles from './LightenDarken.module.css';
type ColorModes = 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
export function LightenDarken() { export function LightenDarken() {
const [selectedColor, setSelectedColor] = useAtom(currentPickedColor); const [selectedColor, setSelectedColor] = useAtom(currentPickedColor);
const [lighten, setLighten] = useState(3); const [lighten, setLighten] = useState(3);
@@ -20,7 +22,7 @@ export function LightenDarken() {
const [steps, setSteps] = useState(10); const [steps, setSteps] = useState(10);
const [maximum, setMaximum] = useState(90); const [maximum, setMaximum] = useState(90);
const [mixMode, setMixMode] = useState<'progressive' | 'average'>('progressive'); const [mixMode, setMixMode] = useState<'progressive' | 'average'>('progressive');
const [mode, setMode] = useState<'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'>('hex'); const [mode, setMode] = useState<ColorModes>('hex');
return ( return (
<div className={cx('workspace', styles.lighten_workspace)}> <div className={cx('workspace', styles.lighten_workspace)}>
@@ -123,8 +125,8 @@ export function LightenDarken() {
{ label: 'LAB', value: 'lab' }, { label: 'LAB', value: 'lab' },
{ label: 'OKLCH', value: 'oklch' }, { label: 'OKLCH', value: 'oklch' },
]} ]}
valu={mode} value={mode}
onChange={setMode} onChange={(v) => setMode(v as ColorModes)}
/> />
</div> </div>
</div> </div>

View File

@@ -8,12 +8,14 @@ import { LabeledPicker } from '../components/LabeledPicker';
import { ScrollArea } from '../components/ScrollArea'; import { ScrollArea } from '../components/ScrollArea';
import styles from './Mixer.module.css'; import styles from './Mixer.module.css';
type ColorModes = 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
export function Mixer() { export function Mixer() {
const { colorFn } = useColorFunction(); const { colorFn } = useColorFunction();
const [basicColor, setBasicColor] = useState('000000'); const [basicColor, setBasicColor] = useState('000000');
const [mixColor, setMixColor] = useState('000000'); const [mixColor, setMixColor] = useState('000000');
const [mixRatio, setMixRatio] = useState(0); const [mixRatio, setMixRatio] = useState(0);
const [mode, setMode] = useState<'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'>('hex'); const [mode, setMode] = useState<ColorModes>('hex');
const mixedColor = useMemo(() => { const mixedColor = useMemo(() => {
try { try {
if (!colorFn) { if (!colorFn) {
@@ -68,7 +70,7 @@ export function Mixer() {
{ label: 'OKLCH', value: 'oklch' }, { label: 'OKLCH', value: 'oklch' },
]} ]}
value={mode} value={mode}
onChange={setMode} onChange={(v) => setMode(v as ColorModes)}
/> />
</div> </div>
</div> </div>

View File

@@ -11,7 +11,8 @@ export function NewScheme() {
const createScheme = useCreateScheme(); const createScheme = useCreateScheme();
const navigate = useNavigate(); const navigate = useNavigate();
const [schemeType, setSchemeType] = useState<SchemeTypeOption['value']>('q_scheme'); const [schemeType, setSchemeType] = useState<SchemeTypeOption['value']>('q_scheme');
const [errors, formAction] = useActionState((prevState, formData) => { const [errors, formAction] = useActionState<{ [key: string]: string }, FormData>(
(_prevState, formData): { [key: string]: string } => {
try { try {
const name = formData.get('name') as string; const name = formData.get('name') as string;
if (isNil(name) || isEmpty(name)) { if (isNil(name) || isEmpty(name)) {
@@ -22,10 +23,12 @@ export function NewScheme() {
const newId = createScheme(name, schemeType, description); const newId = createScheme(name, schemeType, description);
navigate(`../${newId}`); navigate(`../${newId}`);
} catch (error) { } catch (error) {
return error; return error as { [key: string]: string };
} }
return {}; return {} as { [key: string]: string };
}, {}); },
{},
);
return ( return (
<form action={formAction} className={styles.create_scheme_form_layout}> <form action={formAction} className={styles.create_scheme_form_layout}>
@@ -37,7 +40,7 @@ export function NewScheme() {
options={SchemeTypeOptions} options={SchemeTypeOptions}
extendClassName={styles.custom_segment} extendClassName={styles.custom_segment}
value={schemeType} value={schemeType}
onChange={setSchemeType} onChange={(v) => setSchemeType(v as SchemeTypeOption['value'])}
/> />
<input type="hidden" name="type" value={schemeType} /> <input type="hidden" name="type" value={schemeType} />
</div> </div>

View File

@@ -11,6 +11,8 @@ import { PaletteColors } from '../page-components/auto-palette/PaletteColors';
import { currentPickedColor } from '../stores/colors'; import { currentPickedColor } from '../stores/colors';
import styles from './Palette.module.css'; import styles from './Palette.module.css';
type ColorModes = 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
export function AutomaticPalette() { export function AutomaticPalette() {
const [selectedColor, setSelectedColor] = useAtom(currentPickedColor); const [selectedColor, setSelectedColor] = useAtom(currentPickedColor);
const [useReferenceColor, setUseReferenceColor] = useState(false); const [useReferenceColor, setUseReferenceColor] = useState(false);
@@ -22,7 +24,7 @@ export function AutomaticPalette() {
const [referenceBias, setReferenceBias] = useState(0); const [referenceBias, setReferenceBias] = useState(0);
const [minLightness, setMinLightness] = useState(10); const [minLightness, setMinLightness] = useState(10);
const [maxLightness, setMaxLightness] = useState(90); const [maxLightness, setMaxLightness] = useState(90);
const [mode, setMode] = useState<'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'>('hex'); const [mode, setMode] = useState<ColorModes>('hex');
useEffect(() => { useEffect(() => {
if (useReferenceColor) { if (useReferenceColor) {
@@ -111,8 +113,8 @@ export function AutomaticPalette() {
{ label: 'LAB', value: 'lab' }, { label: 'LAB', value: 'lab' },
{ label: 'OKLCH', value: 'oklch' }, { label: 'OKLCH', value: 'oklch' },
]} ]}
valu={mode} value={mode}
onChange={setMode} onChange={(v) => setMode(v as ColorModes)}
/> />
</div> </div>
</div> </div>

View File

@@ -5,12 +5,17 @@ import { useNavigate, useParams } from 'react-router-dom';
import { EditableDescription } from '../components/EditableDescription'; import { EditableDescription } from '../components/EditableDescription';
import { EditableTitle } from '../components/EditableTitle'; import { EditableTitle } from '../components/EditableTitle';
import { SchemeSign } from '../components/SchemeSign'; import { SchemeSign } from '../components/SchemeSign';
import { MaterialDesign2SchemeStorage } from '../material-2-scheme';
import { MaterialDesign3SchemeStorage } from '../material-3-scheme';
import { SchemeContent } from '../models';
import { CorruptedScheme } from '../page-components/scheme/CorruptedScheme'; import { CorruptedScheme } from '../page-components/scheme/CorruptedScheme';
import { M2Scheme } from '../page-components/scheme/M2Scheme'; import { M2Scheme } from '../page-components/scheme/M2Scheme';
import { M3Scheme } from '../page-components/scheme/M3Scheme'; import { M3Scheme } from '../page-components/scheme/M3Scheme';
import { QScheme } from '../page-components/scheme/QScheme'; import { QScheme } from '../page-components/scheme/QScheme';
import { SwatchScheme } from '../page-components/scheme/SwatchScheme'; import { SwatchScheme } from '../page-components/scheme/SwatchScheme';
import { QSchemeStorage } from '../q-scheme';
import { useScheme, useUpdateScheme } from '../stores/schemes'; import { useScheme, useUpdateScheme } from '../stores/schemes';
import { SwatchSchemeStorage } from '../swatch_scheme';
import styles from './SchemeDetail.module.css'; import styles from './SchemeDetail.module.css';
export function SchemeDetail() { export function SchemeDetail() {
@@ -40,13 +45,13 @@ export function SchemeDetail() {
const schemeContent = useMemo(() => { const schemeContent = useMemo(() => {
switch (scheme?.type) { switch (scheme?.type) {
case 'q_scheme': case 'q_scheme':
return <QScheme scheme={scheme} />; return <QScheme scheme={scheme as SchemeContent<QSchemeStorage>} />;
case 'swatch_scheme': case 'swatch_scheme':
return <SwatchScheme scheme={scheme} />; return <SwatchScheme scheme={scheme as SchemeContent<SwatchSchemeStorage>} />;
case 'material_2': case 'material_2':
return <M2Scheme scheme={scheme} />; return <M2Scheme scheme={scheme as SchemeContent<MaterialDesign2SchemeStorage>} />;
case 'material_3': case 'material_3':
return <M3Scheme scheme={scheme} />; return <M3Scheme scheme={scheme as SchemeContent<MaterialDesign3SchemeStorage>} />;
default: default:
return <CorruptedScheme />; return <CorruptedScheme />;
} }

View File

@@ -13,6 +13,8 @@ import { Tints } from '../page-components/tints-shades/tints';
import { currentPickedColor } from '../stores/colors'; import { currentPickedColor } from '../stores/colors';
import styles from './TintsShades.module.css'; import styles from './TintsShades.module.css';
type ColorModes = 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
export function TintsShades() { export function TintsShades() {
const [selectedColor, setSelectedColor] = useAtom(currentPickedColor); const [selectedColor, setSelectedColor] = useAtom(currentPickedColor);
const [steps, setSteps] = useState(10); const [steps, setSteps] = useState(10);
@@ -20,7 +22,7 @@ export function TintsShades() {
const [shades, setShades] = useState(3); const [shades, setShades] = useState(3);
const [maximum, setMaximum] = useState(90); const [maximum, setMaximum] = useState(90);
const [mixMode, setMixMode] = useState<'progressive' | 'average'>('progressive'); const [mixMode, setMixMode] = useState<'progressive' | 'average'>('progressive');
const [mode, setMode] = useState<'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'>('hex'); const [mode, setMode] = useState<ColorModes>('hex');
return ( return (
<div className={cx('workspace', styles.tints_workspace)}> <div className={cx('workspace', styles.tints_workspace)}>
@@ -122,8 +124,8 @@ export function TintsShades() {
{ label: 'LAB', value: 'lab' }, { label: 'LAB', value: 'lab' },
{ label: 'OKLCH', value: 'oklch' }, { label: 'OKLCH', value: 'oklch' },
]} ]}
valu={mode} value={mode}
onChange={setMode} onChange={(v) => setMode(v as ColorModes)}
/> />
</div> </div>
</div> </div>

View File

@@ -11,13 +11,15 @@ import { ScrollArea } from '../components/ScrollArea';
import { currentPickedColor } from '../stores/colors'; import { currentPickedColor } from '../stores/colors';
import styles from './Tones.module.css'; import styles from './Tones.module.css';
type ColorModes = 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
export function Tones() { export function Tones() {
const { colorFn } = useColorFunction(); const { colorFn } = useColorFunction();
const [selectedColor, setSelectedColor] = useAtom(currentPickedColor); const [selectedColor, setSelectedColor] = useAtom(currentPickedColor);
const [steps, setSteps] = useState(10); const [steps, setSteps] = useState(10);
const [tones, setTones] = useState(3); const [tones, setTones] = useState(3);
const [seedBias, setSeedBias] = useState(0); const [seedBias, setSeedBias] = useState(0);
const [mode, setMode] = useState<'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'>('hex'); const [mode, setMode] = useState<ColorModes>('hex');
const colors = useMemo(() => { const colors = useMemo(() => {
try { try {
const lightenColors = colorFn!.tonal_lighten_series( const lightenColors = colorFn!.tonal_lighten_series(
@@ -98,8 +100,8 @@ export function Tones() {
{ label: 'LAB', value: 'lab' }, { label: 'LAB', value: 'lab' },
{ label: 'OKLCH', value: 'oklch' }, { label: 'OKLCH', value: 'oklch' },
]} ]}
valu={mode} value={mode}
onChange={setMode} onChange={(v) => setMode(v as ColorModes)}
/> />
</div> </div>
</div> </div>

View File

@@ -9,9 +9,11 @@ import { ColorWheel } from '../page-components/wheels/ColorWheel';
import { currentPickedColor } from '../stores/colors'; import { currentPickedColor } from '../stores/colors';
import styles from './Wheels.module.css'; import styles from './Wheels.module.css';
type HighlightMode = Parameters<typeof ColorWheel>[0]['highlightMode'];
export function Wheels() { export function Wheels() {
const [selectedColor, setSelectedColor] = useAtom(currentPickedColor); const [selectedColor, setSelectedColor] = useAtom(currentPickedColor);
const [selectedMode, setSelectedMode] = useState('complementary'); const [selectedMode, setSelectedMode] = useState<HighlightMode>('complementary');
const [steps, setSteps] = useState(10); const [steps, setSteps] = useState(10);
const [tones, setTones] = useState(3); const [tones, setTones] = useState(3);
@@ -31,7 +33,7 @@ export function Wheels() {
<div className={styles.mode_navigation}> <div className={styles.mode_navigation}>
<h5>Color selection method</h5> <h5>Color selection method</h5>
<VSegmentedControl <VSegmentedControl
onChange={setSelectedMode} onChange={(v) => setSelectedMode(v as HighlightMode)}
options={[ options={[
{ label: 'Complementary', value: 'complementary' }, { label: 'Complementary', value: 'complementary' },
{ label: 'Analogous', value: 'analogous' }, { label: 'Analogous', value: 'analogous' },

View File

@@ -1,4 +1,4 @@
import { SchemeSetting } from './color_functions/color_module'; import { SchemeSetting } from 'color-module';
export type ColorSet = { export type ColorSet = {
root: string; root: string;
@@ -47,7 +47,7 @@ export type QSchemeSource = {
warning: string | null; warning: string | null;
info: string | null; info: string | null;
foreground: string | null; foreground: string | null;
background: strin | nullg; background: string | null;
setting: QSchemeSetting | null; setting: QSchemeSetting | null;
}; };

View File

@@ -42,7 +42,10 @@ export type SchemeSet = {
const schemesAtom = atomWithStorage<SchemeContent<SchemeStorage>[]>('schemes', []); const schemesAtom = atomWithStorage<SchemeContent<SchemeStorage>[]>('schemes', []);
export const activeSchemeAtom = atomWithStorage<string | null>('activeScheme', null); export const activeSchemeAtom = atomWithStorage<string | null>('activeScheme', null);
export function useSchemeList(): Pick<SchemeContent<SchemeStorage>, 'id' | 'name' | 'createdAt'>[] { export function useSchemeList(): Pick<
SchemeContent<SchemeStorage>,
'id' | 'name' | 'createdAt' | 'type'
>[] {
const schemes = useAtomValue(schemesAtom); const schemes = useAtomValue(schemesAtom);
const sortedSchemes = useMemo( const sortedSchemes = useMemo(
() => () =>
@@ -56,9 +59,12 @@ export function useSchemeList(): Pick<SchemeContent<SchemeStorage>, 'id' | 'name
return sortedSchemes; return sortedSchemes;
} }
export function useScheme(id: string): SchemeContent<SchemeStorage> | null { export function useScheme(id?: string | null): SchemeContent<SchemeStorage> | null {
const schemes = useAtomValue(schemesAtom); const schemes = useAtomValue(schemesAtom);
const scheme = useMemo(() => schemes.find((s) => isEqual(id, s.id)) ?? null, [schemes, id]); const scheme = useMemo(
() => schemes.find((s) => !isNil(id) && isEqual(id, s.id)) ?? null,
[schemes, id],
);
return scheme; return scheme;
} }
@@ -71,11 +77,11 @@ export function useActiveScheme(): SchemeContent<SchemeStorage> | null {
export function useCreateScheme(): ( export function useCreateScheme(): (
name: string, name: string,
type: SchemeType, type: SchemeType,
description?: string, description?: string | null,
) => string { ) => string {
const updateSchemes = useSetAtom(schemesAtom); const updateSchemes = useSetAtom(schemesAtom);
const createSchemeAction = useCallback( const createSchemeAction = useCallback(
(name: string, type: SchemeType, description?: string) => { (name: string, type: SchemeType, description?: string | null) => {
const newId = v4(); const newId = v4();
updateSchemes((prev) => [ updateSchemes((prev) => [
...prev.filter((s) => !isNil(s)), ...prev.filter((s) => !isNil(s)),
@@ -97,7 +103,7 @@ export function useCreateScheme(): (
} }
export function useUpdateScheme( export function useUpdateScheme(
id: string, id?: string | null,
): (updater: (prev: SchemeContent<SchemeStorage>) => SchemeContent<SchemeStorage>) => void { ): (updater: (prev: SchemeContent<SchemeStorage>) => SchemeContent<SchemeStorage>) => void {
const updateSchemes = useSetAtom(schemesAtom); const updateSchemes = useSetAtom(schemesAtom);
const updateAction = useCallback( const updateAction = useCallback(
@@ -107,7 +113,7 @@ export function useUpdateScheme(
prev, prev,
(acc, scheme) => { (acc, scheme) => {
if (!isNil(scheme)) { if (!isNil(scheme)) {
if (isEqual(id, scheme.id)) { if (!isNil(id) && isEqual(id, scheme.id)) {
acc.push(updater(scheme)); acc.push(updater(scheme));
} else { } else {
acc.push(scheme); acc.push(scheme);

View File

@@ -1,4 +1,4 @@
import { SwatchEntry, SwatchSchemeSetting } from './color_functions/color_module'; import { SwatchEntry, SwatchSchemeSetting } from 'color-module';
export type SwatchScheme = { export type SwatchScheme = {
light: Record<string, string[]>; light: Record<string, string[]>;

View File

@@ -1,7 +1,7 @@
import { isEmpty, isNil } from 'lodash-es'; import { isEmpty, isNil } from 'lodash-es';
export function defaultEmptyFormData<D>(formData: FormData, param: string, defaultValue: D): D { export function defaultEmptyFormData<D>(formData: FormData, param: string, defaultValue: D): D {
const value = formData.get(param); const value = formData.get(param) as D;
if (isNil(value) || isEmpty(value)) { if (isNil(value) || isEmpty(value)) {
return defaultValue; return defaultValue;
} }
@@ -15,10 +15,8 @@ export function defaultEmptyValue<T, D>(value: T, defaultValue: D): T | D {
return value; return value;
} }
export function mapToObject<K extends string | number | symbol, V>( export function mapToObject<K extends string | number | symbol, V>(map: Map<K, V>): Record<K, V> {
map: Map<K, V>, const obj = {} as Record<K, V>;
): Record<K, V extends Map<unknown, unknown> ? unknown : V> {
const obj: Record<K, V extends Map<unknown, unknown> ? unknown : V> = {};
map.forEach((value, key) => { map.forEach((value, key) => {
if (value instanceof Map) { if (value instanceof Map) {
obj[key] = mapToObject(value); obj[key] = mapToObject(value);

View File

@@ -3,10 +3,13 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@@ -14,13 +17,14 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": true, "strict": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["src"] "include": [
"src"
]
} }

View File

@@ -2,23 +2,25 @@
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022", "target": "ES2022",
"lib": ["ES2023"], "lib": [
"ES2023"
],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"isolatedModules": true, "isolatedModules": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
/* Linting */ /* Linting */
"strict": true, "strict": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["vite.config.ts"] "include": [
"vite.config.ts"
]
} }

View File

@@ -1,11 +1,11 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc'; import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
css: { css: {
transform: 'lightningcss', transformer: 'lightningcss',
lightningcss: { lightningcss: {
cssModules: true, cssModules: true,
}, },