Compare commits

...

82 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
徐涛
2b9547a7c2 完成SchemeDetail接受Scheme内容功能。 2025-02-07 22:53:03 +08:00
徐涛
131c43c5cf 修正Swatch Scheme在生成包含原始颜色时的定位错乱问题。 2025-02-07 22:52:01 +08:00
徐涛
505af1c67e 修正是否包含Primary颜色的表单控制。 2025-02-07 22:27:06 +08:00
徐涛
320b750834 完成Swatch Scheme的构建基本功能。 2025-02-07 22:22:33 +08:00
徐涛
6728ca1be2 提升导出页面组件为公共组件。 2025-02-07 22:22:03 +08:00
徐涛
83dcb3f80f 调整Swatch Scheme的部分类型定义。 2025-02-07 17:29:41 +08:00
徐涛
b8018e323d Swatch Scheme的默认亮度参数改为0-1之间的值。 2025-02-07 17:25:21 +08:00
徐涛
d68ac6a3df 增加将Map转换成Object的工具函数。 2025-02-07 17:22:31 +08:00
徐涛
0f5805bb7f 增加Scheme存储对于无效Scheme记录的过滤。 2025-02-07 16:18:22 +08:00
徐涛
a3fb9b656b 增加过滤记录中的空白值。 2025-02-07 16:11:15 +08:00
徐涛
d817024bf3 向Switch组件增加表单可用功能。 2025-02-07 15:07:30 +08:00
徐涛
d98e3a69d9 增加导出Swatch Scheme默认设置的功能。 2025-02-07 14:44:24 +08:00
徐涛
8b0e9699c7 调整Q Scheme Builder中Grid布局中内容的对齐方式。 2025-02-07 13:25:29 +08:00
徐涛
8e71d3c555 增加Swatch Scheme中必要结构体的导出函数。 2025-02-07 13:06:50 +08:00
徐涛
e9c2d4cb16 完成Q Scheme的导出复制功能。 2025-02-07 09:56:40 +08:00
徐涛
2acb69da20 增加pre元素的默认样式。 2025-02-07 09:56:28 +08:00
徐涛
9664983b5c 增加复制文字内容到剪贴板的功能。 2025-02-07 09:56:17 +08:00
徐涛
fc340f3f74 调整WACG适配算法。 2025-02-07 09:30:39 +08:00
徐涛
f9f855e818 调整Q Scheme构造组件对于已记录值的获取和处理。 2025-02-07 09:01:26 +08:00
徐涛
853b9b6b75 调整Q Scheme设置参数的保存类型。 2025-02-07 09:00:53 +08:00
徐涛
41788c4944 改进枚举内容的序列化输出。 2025-02-07 09:00:06 +08:00
徐涛
2bc250fc3d 去掉调试信息,改进结构体序列化方法。 2025-02-07 08:28:50 +08:00
徐涛
7468e28928 调整Q Scheme颜色单元格预览内容格式。 2025-02-07 08:18:21 +08:00
徐涛
ca83ce082b 修改默认导出函数的名称。 2025-02-07 08:14:38 +08:00
徐涛
89b2a2f9d9 增加将Q Scheme配置转换为普通JS对象的方法。 2025-02-07 08:12:45 +08:00
徐涛
74dd9e7354 完成Q Scheme预览功能。 2025-02-07 06:23:43 +08:00
徐涛
592244911f 综合调整Q Scheme的生成算法。 2025-02-07 06:23:16 +08:00
徐涛
08fabb53a2 调整Q Scheme暗色模式前景色和背景色的生成,以及颜色调整系数的使用。 2025-02-06 17:26:45 +08:00
徐涛
0350380df6 Tab增加被动切换标签页的功能。 2025-02-06 16:41:12 +08:00
徐涛
32d8457802 Q Scheme Builder增加用于通知Scheme构建完成的事件。 2025-02-06 14:31:22 +08:00
徐涛
b124bb4eda 基本完成Q Scheme的Builder功能。 2025-02-06 11:21:51 +08:00
徐涛
92229b0de4 增加对于生成的导出变量的记录。 2025-02-06 11:21:27 +08:00
徐涛
e3642cad97 修正wacg自适应推断算法为双向修正策略,以避免指定条件无法满足时死循环的发生。 2025-02-06 10:45:47 +08:00
徐涛
59519e1408 增加web_sys库的支持。 2025-02-06 10:44:34 +08:00
徐涛
56a4786675 增加构建QScheme设置的构造函数。 2025-02-06 06:19:15 +08:00
徐涛
838f0c0fa0 增加对于默认表单值的处理。 2025-02-05 21:40:53 +08:00
徐涛
9fa05824d2 调整浮动ColorPicker的z-index。 2025-01-27 09:42:22 +08:00
徐涛
14851c8284 增加尚未选择颜色的提示。 2025-01-27 08:08:43 +08:00
徐涛
50646ffccf 调整Picker中颜色预览块的大小。 2025-01-27 07:49:40 +08:00
徐涛
a9ad4dea5d 增加一个悬浮Color Picker组件 2025-01-27 07:48:21 +08:00
徐涛
7b26c95a9a 修复ActionIcon可能会被当作表单提交按钮的问题。 2025-01-27 07:34:50 +08:00
徐涛
8efb3ec318 将枚举换成整型值的输出。 2025-01-25 10:57:43 +08:00
徐涛
c4f703906e 增加获取Q Scheme默认配置功能。 2025-01-25 09:32:13 +08:00
徐涛
6dba92a2c5 修正SegmentControl解析Map格式的默认值。 2025-01-25 09:22:17 +08:00
徐涛
4b4428fd3b 增强SegmentControl系列组件对于Map的支持。 2025-01-25 08:56:57 +08:00
徐涛
1b41fb4d22 改进输入框包装器的样式。 2025-01-24 17:18:32 +08:00
徐涛
a3de0f961a 调整存储的Scheme内容。 2025-01-24 16:14:37 +08:00
徐涛
79794ed0f7 调整Scheme详细页面的布局。 2025-01-24 15:18:45 +08:00
徐涛
20757a789a 完善不可识别Scheme的警告页面。 2025-01-24 15:17:07 +08:00
徐涛
f9f984a1b4 重构Scheme编辑器结构。 2025-01-24 15:09:25 +08:00
徐涛
9606106c45 增加自定义Scheme的激活和删除功能。 2025-01-24 14:53:43 +08:00
徐涛
3882ae764f 增加ActionIcon组件。 2025-01-24 11:26:25 +08:00
徐涛
2b17a5de0f 重构对于Scheme类型的展示。 2025-01-24 10:48:36 +08:00
徐涛
b2357811b6 缩小Badge组件的留白尺寸。 2025-01-24 10:46:10 +08:00
徐涛
32273256c0 调整Badge圆角大小。 2025-01-24 10:30:04 +08:00
徐涛
7e2132662f 增加Badge组件。 2025-01-24 10:09:14 +08:00
徐涛
cff2ad0439 重构创建Scheme功能支持多种Scheme类型选择。 2025-01-24 09:11:00 +08:00
徐涛
dc411987bf 增加选择器对于额外配置样式的支持。 2025-01-24 09:00:23 +08:00
徐涛
26ebc3c7e3 更新前端Scheme类型定义。 2025-01-23 16:54:08 +08:00
徐涛
ab4e0b440c 修正一处拼写错误。 2025-01-23 16:02:46 +08:00
120 changed files with 3345 additions and 3322 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -8,14 +8,17 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
color-name = "1.1.0" color-name = "1.1.0"
enum-iterator = "2.1.0"
palette = { version = "0.7.6", features = ["serde"] } palette = { version = "0.7.6", features = ["serde"] }
serde = { version = "1.0.216", features = ["derive"] } serde = { version = "1.0.216", features = ["derive"] }
serde-wasm-bindgen = "0.6.5" serde-wasm-bindgen = "0.6.5"
serde_json = "1.0.134" serde_json = "1.0.134"
serde_repr = "0.1.19"
strum = { version = "0.26.3", features = ["derive", "strum_macros"] } strum = { version = "0.26.3", features = ["derive", "strum_macros"] }
strum_macros = "0.26.4" strum_macros = "0.26.4"
thiserror = "2.0.9" thiserror = "2.0.9"
wasm-bindgen = { version = "0.2.99", features = ["serde", "serde_json", "serde-serialize"] } wasm-bindgen = { version = "0.2.99", features = ["serde", "serde_json", "serde-serialize"] }
web-sys = {version = "0.3.77", features = ["console", "Window"]}
[dev-dependencies] [dev-dependencies]
wasm-bindgen-test = "0.3.49" wasm-bindgen-test = "0.3.49"

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

@@ -48,16 +48,19 @@ impl Baseline {
let outline_color = neutral_swatch.get(background.l * 0.7); let outline_color = neutral_swatch.get(background.l * 0.7);
Self { Self {
primary: ColorSet::new(primary, &neutral_swatch, &setting), primary: ColorSet::new(primary, &neutral_swatch, foreground.l, &setting),
secondary: secondary.map(|color| ColorSet::new(&color, &neutral_swatch, &setting)), secondary: secondary
tertiary: tertiary.map(|color| ColorSet::new(&color, &neutral_swatch, &setting)), .map(|color| ColorSet::new(&color, &neutral_swatch, foreground.l, &setting)),
accent: accent.map(|color| ColorSet::new(&color, &neutral_swatch, &setting)), tertiary: tertiary
neutral: ColorSet::new(&neutral_color, &neutral_swatch, &setting), .map(|color| ColorSet::new(&color, &neutral_swatch, foreground.l, &setting)),
danger: ColorSet::new(danger, &neutral_swatch, &setting), accent: accent
success: ColorSet::new(success, &neutral_swatch, &setting), .map(|color| ColorSet::new(&color, &neutral_swatch, foreground.l, &setting)),
warning: ColorSet::new(warning, &neutral_swatch, &setting), neutral: ColorSet::new(&neutral_color, &neutral_swatch, foreground.l, &setting),
info: ColorSet::new(info, &neutral_swatch, &setting), danger: ColorSet::new(danger, &neutral_swatch, foreground.l, &setting),
outline: ColorSet::new(&outline_color, &neutral_swatch, &setting), success: ColorSet::new(success, &neutral_swatch, foreground.l, &setting),
warning: ColorSet::new(warning, &neutral_swatch, foreground.l, &setting),
info: ColorSet::new(info, &neutral_swatch, foreground.l, &setting),
outline: ColorSet::new(&outline_color, &neutral_swatch, foreground.l, &setting),
foreground: *foreground, foreground: *foreground,
background: *background, background: *background,
_neutral_swatch: neutral_swatch, _neutral_swatch: neutral_swatch,

View File

@@ -24,34 +24,43 @@ pub struct ColorSet {
fn fit_to_wacg(reference: &Oklch, neutral_swatch: &NeutralSwatch, ratio: f32) -> Oklch { fn fit_to_wacg(reference: &Oklch, neutral_swatch: &NeutralSwatch, ratio: f32) -> Oklch {
let reference_luma = map_oklch_to_luma(reference); let reference_luma = map_oklch_to_luma(reference);
let mut new_target = neutral_swatch.get(reference.l);
let quick_factor: f32 = if reference.l <= 0.5 { 0.05 } else { -0.05 };
let fine_factor: f32 = if reference.l <= 0.5 { 0.01 } else { -0.01 };
let match_wacg = |original: &Oklch<f32>, reference: &Luma| { let match_wacg = |original: &Oklch<f32>, reference: &Luma| {
let luma = map_oklch_to_luma(original); let luma = map_oklch_to_luma(original);
luma.relative_contrast(*reference) luma.relative_contrast(*reference)
}; };
while match_wacg(&new_target, &reference_luma) < ratio { let mut fit_contrast = (f32::INFINITY, f32::NEG_INFINITY);
new_target.l = new_target.l * (1.0 + quick_factor); let mut closest_contrast = (f32::INFINITY, f32::NEG_INFINITY);
if new_target.l > 1.0 { for scan_lightness in (0..=100).map(|x| x as f32 / 100.0) {
new_target.l = 1.0; let new_target = neutral_swatch.get(scan_lightness);
break; let contrast_ratio = match_wacg(&new_target, &reference_luma);
if (contrast_ratio - ratio).abs() < (closest_contrast.0 - ratio).abs()
&& scan_lightness > closest_contrast.1
{
closest_contrast = (contrast_ratio, scan_lightness);
}
if contrast_ratio >= ratio
&& (contrast_ratio - ratio).abs() < (closest_contrast.0 - ratio).abs()
{
fit_contrast = (contrast_ratio, scan_lightness);
} }
} }
while match_wacg(&new_target, &reference_luma) < ratio {
new_target.l = new_target.l * (1.0 + fine_factor); neutral_swatch.get(if fit_contrast.0 == f32::INFINITY {
if new_target.l > 1.0 { closest_contrast.1
new_target.l = 1.0; } else {
break; fit_contrast.1
} })
}
new_target
} }
impl ColorSet { impl ColorSet {
pub fn new(color: &Oklch, neutral_swatch: &NeutralSwatch, setting: &SchemeSetting) -> Self { pub fn new(
color: &Oklch,
neutral_swatch: &NeutralSwatch,
foreground_lightness: f32,
setting: &SchemeSetting,
) -> Self {
let root = color.clone(); let root = color.clone();
let hover = color * setting.hover; let hover = color * setting.hover;
let active = color * setting.active; let active = color * setting.active;
@@ -60,11 +69,11 @@ impl ColorSet {
let (on_root, on_hover, on_active, on_focus, on_disabled) = match setting.wacg_follows { let (on_root, on_hover, on_active, on_focus, on_disabled) = match setting.wacg_follows {
WACGSetting::Fixed => ( WACGSetting::Fixed => (
neutral_swatch.get(root.l), neutral_swatch.get(foreground_lightness),
neutral_swatch.get(hover.l), neutral_swatch.get(foreground_lightness),
neutral_swatch.get(active.l), neutral_swatch.get(foreground_lightness),
neutral_swatch.get(focus.l), neutral_swatch.get(foreground_lightness),
neutral_swatch.get(disabled.l), neutral_swatch.get(foreground_lightness),
), ),
WACGSetting::AutomaticAA => ( WACGSetting::AutomaticAA => (
fit_to_wacg(&root, neutral_swatch, 4.5), fit_to_wacg(&root, neutral_swatch, 4.5),
@@ -80,6 +89,13 @@ impl ColorSet {
fit_to_wacg(&focus, neutral_swatch, 7.0), fit_to_wacg(&focus, neutral_swatch, 7.0),
fit_to_wacg(&disabled, neutral_swatch, 7.0), fit_to_wacg(&disabled, neutral_swatch, 7.0),
), ),
WACGSetting::HighContrast => (
fit_to_wacg(&root, neutral_swatch, 21.0),
fit_to_wacg(&hover, neutral_swatch, 21.0),
fit_to_wacg(&active, neutral_swatch, 21.0),
fit_to_wacg(&focus, neutral_swatch, 21.0),
fit_to_wacg(&disabled, neutral_swatch, 21.0),
),
}; };
Self { Self {

View File

@@ -4,7 +4,6 @@ use baseline::Baseline;
use palette::FromColor; use palette::FromColor;
use scheme_setting::{ColorExpand, WACGSetting}; use scheme_setting::{ColorExpand, WACGSetting};
use serde::Serialize; use serde::Serialize;
use strum::IntoEnumIterator;
use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use crate::{errors, parse_option_to_oklch, parse_to_oklch}; use crate::{errors, parse_option_to_oklch, parse_to_oklch};
@@ -59,8 +58,8 @@ impl QScheme {
&(success * setting.dark_convert), &(success * setting.dark_convert),
&(warning * setting.dark_convert), &(warning * setting.dark_convert),
&(info * setting.dark_convert), &(info * setting.dark_convert),
&(foreground * setting.dark_convert), &(&background * (setting.dark_convert / 2.0)),
&(background * setting.dark_convert), &(&foreground * setting.dark_convert),
setting.clone(), setting.clone(),
), ),
}) })
@@ -113,7 +112,7 @@ impl QScheme {
&(success * setting.dark_convert), &(success * setting.dark_convert),
&(warning * setting.dark_convert), &(warning * setting.dark_convert),
&(info * setting.dark_convert), &(info * setting.dark_convert),
&(foreground * setting.dark_convert), &(foreground * (setting.dark_convert / 2.0)),
&(background * setting.dark_convert), &(background * setting.dark_convert),
setting.clone(), setting.clone(),
), ),
@@ -169,11 +168,11 @@ impl SchemeExport for QScheme {
#[wasm_bindgen] #[wasm_bindgen]
pub fn q_scheme_color_expanding_methods() -> Result<JsValue, String> { pub fn q_scheme_color_expanding_methods() -> Result<JsValue, String> {
let methods = ColorExpand::iter() let methods = enum_iterator::all::<ColorExpand>()
.map(|variant| { .map(|variant| {
serde_json::json!({ serde_json::json!({
"label": variant.label(), "label": variant.label(),
"value": variant.to_string(), "value": variant as u8,
}) })
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -183,14 +182,19 @@ pub fn q_scheme_color_expanding_methods() -> Result<JsValue, String> {
#[wasm_bindgen] #[wasm_bindgen]
pub fn q_scheme_wacg_settings() -> Result<JsValue, String> { pub fn q_scheme_wacg_settings() -> Result<JsValue, String> {
let settings = WACGSetting::iter() let settings = enum_iterator::all::<WACGSetting>()
.map(|setting| { .map(|setting| {
serde_json::json!({ serde_json::json!({
"label": setting.label(), "label": setting.label(),
"value": setting.to_string(), "value": setting as u8,
}) })
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
serde_wasm_bindgen::to_value(&settings).map_err(|e| e.to_string()) serde_wasm_bindgen::to_value(&settings).map_err(|e| e.to_string())
} }
#[wasm_bindgen]
pub fn q_scheme_default_settings() -> SchemeSetting {
SchemeSetting::default()
}

View File

@@ -1,9 +1,11 @@
use std::ops::Mul; use std::ops::{Div, Mul};
use enum_iterator::Sequence;
use palette::Oklch; use palette::Oklch;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter, EnumString}; use serde_repr::{Deserialize_repr, Serialize_repr};
use wasm_bindgen::prelude::wasm_bindgen; use strum::Display;
use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[wasm_bindgen] #[wasm_bindgen]
@@ -12,6 +14,19 @@ pub struct ColorShifting {
pub lightness: f32, pub lightness: f32,
} }
#[wasm_bindgen]
impl ColorShifting {
#[wasm_bindgen(constructor)]
pub fn new(chroma: f32, lightness: f32) -> Self {
ColorShifting { chroma, lightness }
}
#[wasm_bindgen(js_name = toJsValue)]
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
Ok(serde_wasm_bindgen::to_value(self)?)
}
}
impl Mul<ColorShifting> for Oklch<f32> { impl Mul<ColorShifting> for Oklch<f32> {
type Output = Oklch<f32>; type Output = Oklch<f32>;
@@ -21,13 +36,13 @@ impl Mul<ColorShifting> for Oklch<f32> {
+ if rhs.lightness > 0.0 { + if rhs.lightness > 0.0 {
(1.0 - self.l) * rhs.lightness (1.0 - self.l) * rhs.lightness
} else { } else {
self.l * rhs.lightness * -1.0 self.l * rhs.lightness
}, },
self.chroma self.chroma
+ if rhs.chroma > 0.0 { + if rhs.chroma > 0.0 {
(100.0 - self.chroma) * rhs.chroma (100.0 - self.chroma) * rhs.chroma
} else { } else {
-(self.chroma * rhs.chroma) self.chroma * rhs.chroma
}, },
self.hue, self.hue,
) )
@@ -43,19 +58,41 @@ impl Mul<ColorShifting> for &Oklch<f32> {
+ if rhs.lightness > 0.0 { + if rhs.lightness > 0.0 {
(1.0 - self.l) * rhs.lightness (1.0 - self.l) * rhs.lightness
} else { } else {
self.l * rhs.lightness * -1.0 self.l * rhs.lightness
}, },
self.chroma self.chroma
+ if rhs.chroma > 0.0 { + if rhs.chroma > 0.0 {
(100.0 - self.chroma) * rhs.chroma (100.0 - self.chroma) * rhs.chroma
} else { } else {
-(self.chroma * rhs.chroma) self.chroma * rhs.chroma
}, },
self.hue, self.hue,
) )
} }
} }
impl Div<f32> for ColorShifting {
type Output = ColorShifting;
fn div(self, rhs: f32) -> Self::Output {
ColorShifting {
chroma: self.chroma / rhs,
lightness: self.lightness / rhs,
}
}
}
impl Div<f32> for &ColorShifting {
type Output = ColorShifting;
fn div(self, rhs: f32) -> Self::Output {
ColorShifting {
chroma: self.chroma / rhs,
lightness: self.lightness / rhs,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[wasm_bindgen] #[wasm_bindgen]
pub struct SchemeSetting { pub struct SchemeSetting {
@@ -68,9 +105,9 @@ pub struct SchemeSetting {
pub wacg_follows: WACGSetting, pub wacg_follows: WACGSetting,
} }
#[derive(Debug, Clone, Copy, Display, EnumString, EnumIter, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Display, Sequence, Serialize_repr, Deserialize_repr)]
#[strum(serialize_all = "lowercase")]
#[wasm_bindgen] #[wasm_bindgen]
#[repr(u8)]
pub enum ColorExpand { pub enum ColorExpand {
Complementary, Complementary,
Analogous, Analogous,
@@ -95,13 +132,14 @@ impl ColorExpand {
} }
} }
#[derive(Debug, Clone, Copy, Display, EnumString, EnumIter, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Display, Sequence, Serialize_repr, Deserialize_repr)]
#[strum(serialize_all = "lowercase")]
#[wasm_bindgen] #[wasm_bindgen]
#[repr(u8)]
pub enum WACGSetting { pub enum WACGSetting {
Fixed, Fixed,
AutomaticAA, AutomaticAA,
AutomaticAAA, AutomaticAAA,
HighContrast,
} }
impl WACGSetting { impl WACGSetting {
@@ -110,6 +148,7 @@ impl WACGSetting {
WACGSetting::Fixed => "Fixed".to_string(), WACGSetting::Fixed => "Fixed".to_string(),
WACGSetting::AutomaticAA => "Automatic AA".to_string(), WACGSetting::AutomaticAA => "Automatic AA".to_string(),
WACGSetting::AutomaticAAA => "Automatic AAA".to_string(), WACGSetting::AutomaticAAA => "Automatic AAA".to_string(),
WACGSetting::HighContrast => "High Contrast".to_string(),
} }
} }
} }
@@ -131,7 +170,7 @@ impl Default for SchemeSetting {
}, },
disabled: ColorShifting { disabled: ColorShifting {
chroma: -0.9, chroma: -0.9,
lightness: -0.2, lightness: 0.2,
}, },
dark_convert: ColorShifting { dark_convert: ColorShifting {
chroma: -0.3, chroma: -0.3,
@@ -142,3 +181,32 @@ impl Default for SchemeSetting {
} }
} }
} }
#[wasm_bindgen]
impl SchemeSetting {
#[wasm_bindgen(constructor)]
pub fn new(
hover: ColorShifting,
active: ColorShifting,
focus: ColorShifting,
disabled: ColorShifting,
dark_convert: ColorShifting,
expand_method: ColorExpand,
wacg_follows: WACGSetting,
) -> Self {
SchemeSetting {
hover,
active,
focus,
disabled,
dark_convert,
expand_method,
wacg_follows,
}
}
#[wasm_bindgen(js_name = toJsValue)]
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
Ok(serde_wasm_bindgen::to_value(self)?)
}
}

View File

@@ -1,4 +1,5 @@
use palette::FromColor; use palette::FromColor;
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
@@ -19,13 +20,29 @@ pub struct SwatchScheme {
dark: HashMap<String, Swatch>, dark: HashMap<String, Swatch>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[wasm_bindgen(getter_with_clone)] #[wasm_bindgen(getter_with_clone)]
pub struct SwatchEntry { pub struct SwatchEntry {
pub name: String, pub name: String,
pub color: String, pub color: String,
} }
#[wasm_bindgen]
impl SwatchEntry {
#[wasm_bindgen(constructor)]
pub fn new(name: &str, color: &str) -> Self {
Self {
name: name.to_string(),
color: color.to_string(),
}
}
#[wasm_bindgen(js_name = toJsValue)]
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
Ok(serde_wasm_bindgen::to_value(self)?)
}
}
impl SwatchScheme { impl SwatchScheme {
pub fn new( pub fn new(
colors: Vec<SwatchEntry>, colors: Vec<SwatchEntry>,
@@ -120,3 +137,8 @@ impl SchemeExport for SwatchScheme {
object.join("\n") object.join("\n")
} }
} }
#[wasm_bindgen]
pub fn swatch_scheme_default_settings() -> SwatchSchemeSetting {
SwatchSchemeSetting::default()
}

View File

@@ -1,5 +1,5 @@
use serde::Serialize; use serde::Serialize;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};
use crate::schemes::q_style::ColorShifting; use crate::schemes::q_style::ColorShifting;
@@ -17,8 +17,8 @@ impl Default for SwatchSchemeSetting {
fn default() -> Self { fn default() -> Self {
Self { Self {
amount: 10, amount: 10,
min_lightness: 10.0, min_lightness: 0.1,
max_lightness: 90.0, max_lightness: 0.9,
include_primary: false, include_primary: false,
dark_convert: ColorShifting { dark_convert: ColorShifting {
chroma: -0.3, chroma: -0.3,
@@ -27,3 +27,28 @@ impl Default for SwatchSchemeSetting {
} }
} }
} }
#[wasm_bindgen]
impl SwatchSchemeSetting {
#[wasm_bindgen(constructor)]
pub fn new(
amount: usize,
min_lightness: f32,
max_lightness: f32,
include_primary: bool,
dark_convert: ColorShifting,
) -> Self {
Self {
amount,
min_lightness,
max_lightness,
include_primary,
dark_convert,
}
}
#[wasm_bindgen(js_name = toJsValue)]
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
Ok(serde_wasm_bindgen::to_value(self)?)
}
}

View File

@@ -24,28 +24,26 @@ impl Swatch {
} }
} }
fn find_interval(&self) -> (usize, usize) { fn find_interval(&self) -> usize {
if !self.include_primary { if !self.include_primary {
return (0, 0); return 0;
} }
if self.primary_key.l == self.min_key { if self.primary_key.l == self.min_key {
return (0, 1); return 0;
} }
if self.primary_key.l == self.max_key { if self.primary_key.l == self.max_key {
return (self.color_amount - 2, self.color_amount - 1); return self.color_amount - 1;
} }
let step = (self.max_key - self.min_key) / (self.color_amount - 1) as f32; let step = (self.max_key - self.min_key) / (self.color_amount - 1) as f32;
let index = ((self.primary_key.l - self.min_key) / step) as usize; ((self.primary_key.l - self.min_key) / step).ceil() as usize
(index, index + 1)
} }
pub fn swatch(&self) -> Vec<Oklch> { pub fn swatch(&self) -> Vec<Oklch> {
let mut swatch = Vec::new(); let mut swatch = Vec::new();
if self.include_primary { if self.include_primary {
let (_, primary_index) = self.find_interval(); let primary_index = self.find_interval();
if primary_index > 0 { if primary_index > 0 {
let step = (self.max_key - self.min_key) / primary_index as f32; let step = (self.primary_key.l - self.min_key) / (primary_index - 1) as f32;
for i in 0..primary_index { for i in 0..primary_index {
let lightness = self.min_key + step * i as f32; let lightness = self.min_key + step * i as f32;
swatch.push(Oklch { swatch.push(Oklch {
@@ -55,8 +53,8 @@ impl Swatch {
} }
} }
if primary_index < self.color_amount - 1 { if primary_index < self.color_amount - 1 {
let step = let step = (self.max_key - self.primary_key.l)
(self.max_key - self.min_key) / (self.color_amount - primary_index) as f32; / (self.color_amount - primary_index - 1) as f32;
for i in primary_index..self.color_amount { for i in primary_index..self.color_amount {
let lightness = self.min_key + step * i as f32; let lightness = self.min_key + step * i as f32;
swatch.push(Oklch { swatch.push(Oklch {

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

@@ -29,6 +29,7 @@ const routes = createBrowserRouter([
path: 'schemes', path: 'schemes',
element: <Schemes />, element: <Schemes />,
children: [ children: [
{ index: true, element: <div /> },
{ path: 'new', element: <NewScheme /> }, { path: 'new', element: <NewScheme /> },
{ path: 'not-found', element: <SchemeNotFound /> }, { path: 'not-found', element: <SchemeNotFound /> },
{ path: ':id', element: <SchemeDetail /> }, { path: ':id', element: <SchemeDetail /> },

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,300 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export function q_scheme_color_expanding_methods(): any;
export function q_scheme_wacg_settings(): any;
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 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 generate_palette_from_color(reference_color: string, swatch_amount: number, minimum_lightness: number, maximum_lightness: number, use_reference_color?: boolean, reference_color_bias?: number): (string)[];
export function color_categories(): any;
export function search_color_cards(tag: string, category?: string): 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 | undefined, tertiary_color: string | undefined, accent_color: string | 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 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 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 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 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,
}
export class ColorShifting {
private constructor();
free(): void;
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 {
private constructor();
free(): void;
hover: ColorShifting;
active: ColorShifting;
focus: ColorShifting;
disabled: ColorShifting;
dark_convert: ColorShifting;
expand_method: ColorExpand;
wacg_follows: WACGSetting;
}
export class SwatchEntry {
private constructor();
free(): void;
name: string;
color: string;
}
export class SwatchSchemeSetting {
private constructor();
free(): void;
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 q_scheme_color_expanding_methods: () => [number, number, number];
readonly q_scheme_wacg_settings: () => [number, number, number];
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 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 __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 __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_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 __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 __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_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 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 __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 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 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 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 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_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 __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 __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,138 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const q_scheme_color_expanding_methods: () => [number, number, number];
export const q_scheme_wacg_settings: () => [number, number, number];
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 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 __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 __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_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 __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 __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_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 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 __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 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 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 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 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_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 __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 __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

@@ -193,6 +193,7 @@
resize: none; resize: none;
} }
.input_wrapper { .input_wrapper {
width: fit-content;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@@ -440,4 +441,12 @@
background-color: var(--color-info); background-color: var(--color-info);
} }
} }
pre {
padding: var(--spacing-xs) var(--spacing-s);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-xxs);
font-size: var(--font-size-xs);
line-height: 1.4em;
}
} }

View File

@@ -0,0 +1,10 @@
@layer components {
.action_icon {
padding: var(--spacing-xs);
border: none;
border-radius: var(--border-radius-xxs);
line-height: 1em;
.icon {
}
}
}

View File

@@ -0,0 +1,25 @@
import { Icon, IconProps } from '@iconify/react/dist/iconify.js';
import cx from 'clsx';
import { MouseEvent, MouseEventHandler, useCallback } from 'react';
import styles from './ActionIcon.module.css';
type ActionIconProps = {
icon: IconProps['icon'];
onClick?: MouseEventHandler<HTMLButtonElement>;
extendClassName?: HTMLButtonElement['className'];
};
export function ActionIcon({ icon, onClick, extendClassName }: ActionIconProps) {
const handleClick = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
},
[onClick],
);
return (
<button type="button" onClick={handleClick} className={cx(styles.action_icon, extendClassName)}>
<Icon icon={icon} className={styles.icon} />
</button>
);
}

View File

@@ -0,0 +1,12 @@
@layer components {
.badge {
padding: var(--spacing-xxs) var(--spacing-xs);
border-radius: var(--border-radius-xxs);
flex: 0 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
line-height: 1.2em;
}
}

12
src/components/Badge.tsx Normal file
View File

@@ -0,0 +1,12 @@
import cx from 'clsx';
import { ReactNode } from 'react';
import styles from './Badge.module.css';
type BadgeProps = {
extendClassName?: HTMLDivElement['className'];
children?: ReactNode;
};
export function Badge({ extendClassName, children }: BadgeProps) {
return <div className={cx(styles.badge, extendClassName)}>{children}</div>;
}

View File

@@ -3,7 +3,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: var(--spacing-m); gap: var(--spacing-xs);
.extended_input_wrapper {
width: 100%;
}
.rgb_input { .rgb_input {
text-align: right; text-align: right;
text-transform: uppercase; text-transform: uppercase;

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;
} }
@@ -146,7 +146,7 @@ export function ColorComponentInput({ color, onChange }: ColorComponentInputProp
return ( return (
<div className={styles.rgb_input}> <div className={styles.rgb_input}>
<div className={cx('input_wrapper')}> <div className={cx('input_wrapper', styles.extended_input_wrapper)}>
<Icon icon="tabler:hash" /> <Icon icon="tabler:hash" />
<input type="text" value={hex} onChange={updateHex} className={styles.rgb_input} /> <input type="text" value={hex} onChange={updateHex} className={styles.rgb_input} />
</div> </div>

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

@@ -0,0 +1,44 @@
@layer components {
.float_color_picker {
position: relative;
height: 22px;
.preview {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-xs);
z-index: 25;
.preview_block {
height: 100%;
aspect-ratio: 16 / 9;
border-radius: var(--border-radius-xxs);
border: 1px solid var(--color-border);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
font-size: var(--font-size-xs);
}
}
.picker {
position: absolute;
top: calc(100% + 4px);
padding: var(--spacing-s) var(--spacing-s);
border-radius: var(--border-radius-xxs);
border: 1px solid var(--color-border);
background-color: var(--color-bg);
box-shadow: 2px 0 8px oklch(from var(--color-black) l c h / 65%);
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-s);
z-index: 260;
.btns {
display: flex;
flex-direction: row-reverse;
gap: var(--spacing-xs);
}
}
}
}

View File

@@ -0,0 +1,56 @@
import { isEqual, isNil } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react';
import { ActionIcon } from './ActionIcon';
import { ColorPicker } from './ColorPicker';
import styles from './FloatColorPicker.module.css';
type FloatColorPickerProps = {
name?: string;
color?: string | null;
onPick?: (color: string | null | undefined) => void;
};
export function FloatColorPicker({ name, color, onPick }: FloatColorPickerProps) {
const [pickedColor, setPicked] = useState<string | null>(color ?? null);
const [showPicker, setPickerShow] = useState(false);
const handlePickAction = useCallback(
(value: string) => {
setPicked(value);
onPick?.(value);
},
[onPick],
);
useEffect(() => {
if (!isEqual(pickedColor, color)) {
setPicked(color);
}
}, [color]);
return (
<div className={styles.float_color_picker}>
<div className={styles.preview}>
<div
className={styles.preview_block}
onClick={() => setPickerShow(true)}
style={{
backgroundColor: isNil(pickedColor) ? 'rgba(0, 0, 0, 0)' : `#${pickedColor}`,
}}>
{isNil(pickedColor) && <span>N/A</span>}
</div>
<ActionIcon icon="tabler:x" onClick={() => handlePickAction(null)} />
</div>
{showPicker && (
<div className={styles.picker}>
<ColorPicker color={pickedColor ?? null} onSelect={handlePickAction} />
<div className={styles.btns}>
<button type="button" className="primary" onClick={() => setPickerShow(false)}>
Done
</button>
</div>
</div>
)}
{!isNil(name) && <input type="hidden" name={name} value={pickedColor ?? ''} />}
</div>
);
}

View File

@@ -1,17 +1,32 @@
import cx from 'clsx'; import cx from 'clsx';
import { isEqual, isNil } from 'lodash-es'; import { isEqual, isMap, isNil } from 'lodash-es';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import type { Option } from '../models'; import type { Option } from '../models';
import styles from './HSegmentedControl.module.css'; import styles from './HSegmentedControl.module.css';
type HSegmentedControlProps = { type HSegmentedControlProps = {
name?: string;
defaultValue?: Option['value'];
options?: Option[]; options?: Option[];
value?: Option['value']; value?: Option['value'];
onChange?: (value: Option['value']) => void; onChange?: (value: Option['value']) => void;
extendClassName?: HTMLDivElement['className'];
}; };
export function HSegmentedControl({ options = [], value, onChange }: HSegmentedControlProps) { export function HSegmentedControl({
const [selected, setSelected] = useState(value ?? options[0].value ?? null); name,
defaultValue,
options = [],
value,
onChange,
extendClassName,
}: HSegmentedControlProps) {
const [selected, setSelected] = useState(
value ??
defaultValue ??
(isMap(options[0]) ? options[0].get('value') : options[0].value) ??
null,
);
const [sliderPosition, setSliderPosition] = useState(0); const [sliderPosition, setSliderPosition] = useState(0);
const [sliderWidth, setSliderWidth] = useState(0); const [sliderWidth, setSliderWidth] = useState(0);
const sliderRef = useRef<HTMLDivElement>(null); const sliderRef = useRef<HTMLDivElement>(null);
@@ -28,17 +43,22 @@ export function HSegmentedControl({ options = [], value, onChange }: HSegmentedC
}, []); }, []);
return ( return (
<div className={styles.segmented_control}> <div className={cx(styles.segmented_control, extendClassName)}>
<div className={styles.options}> <div className={styles.options}>
{options.map((option, index) => ( {options.map((option, index) => {
const label = isMap(option) ? option.get('label') : option.label;
const value = isMap(option) ? option.get('value') : option.value;
return (
<div <div
key={`${index}_${option.value}`} key={`${index}_${value}`}
className={cx(styles.option, isEqual(selected, option.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(option.value, index)}> onClick={() => handleSelectAction(value, index)}>
{option.label} {label}
</div> </div>
))} );
})}
{!isNil(selected) && ( {!isNil(selected) && (
<div <div
className={styles.slider} className={styles.slider}
@@ -47,6 +67,7 @@ export function HSegmentedControl({ options = [], value, onChange }: HSegmentedC
/> />
)} )}
</div> </div>
{!isNil(name) && <input type="hidden" name={name} value={selected} />}
</div> </div>
); );
} }

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

@@ -0,0 +1,21 @@
@layer components {
.badge {
font-size: var(--font-size-xs);
color: var(--color-yuebai);
background-color: var(--color-mose);
&.q {
background-color: var(--color-mantianxingzi);
}
&.swatch {
background-color: var(--color-pinlan);
}
&.m2 {
background-color: #03dac6;
color: var(--color-qihei);
}
&.m3 {
background-color: #a78fff;
color: var(--color-qihei);
}
}
}

View File

@@ -0,0 +1,33 @@
import cx from 'clsx';
import { isNil } from 'lodash-es';
import { useMemo } from 'react';
import { schemeType, SchemeType } from '../models';
import { Badge } from './Badge';
import styles from './SchemeSign.module.css';
type SchemeSignProps = {
scheme?: SchemeType;
short?: boolean;
};
export function SchemeSign({ scheme, short = false }: SchemeSignProps) {
const schemeName = schemeType(scheme, short);
const signColorStyles = useMemo(() => {
switch (scheme) {
case 'q_scheme':
return styles.q;
case 'swatch_scheme':
return styles.swatch;
case 'material_2':
return styles.m2;
case 'material_3':
return styles.m3;
}
}, [scheme]);
return (
!isNil(scheme) && (
<Badge extendClassName={cx(styles.badge, signColorStyles)}>{schemeName}</Badge>
)
);
}

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

@@ -1,15 +1,16 @@
import cx from 'clsx'; import cx from 'clsx';
import { isEqual } from 'lodash-es'; import { isEqual, isNil } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import styles from './Switch.module.css'; import styles from './Switch.module.css';
type SwitchProps = { type SwitchProps = {
name?: string;
checked?: boolean; checked?: boolean;
disabled?: boolean; disabled?: boolean;
onChange?: (checked: boolean) => void; onChange?: (checked: boolean) => void;
}; };
export function Switch({ checked = false, disabled = false, onChange }: SwitchProps) { export function Switch({ name, checked = false, disabled = false, onChange }: SwitchProps) {
const [isChecked, setIsChecked] = useState(checked); const [isChecked, setIsChecked] = useState(checked);
const handleSwitch = useCallback(() => { const handleSwitch = useCallback(() => {
if (!disabled) { if (!disabled) {
@@ -25,10 +26,14 @@ export function Switch({ checked = false, disabled = false, onChange }: SwitchPr
}, [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 ? 'checked' : undefined} />
)}
</div> </div>
); );
} }

View File

@@ -1,20 +1,30 @@
import cx from 'clsx'; import cx from 'clsx';
import { isEqual } from 'lodash-es'; import { isEqual, isNil } from 'lodash-es';
import { useCallback, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import styles from './Tab.module.css'; import styles from './Tab.module.css';
type TabProps = { type TabProps = {
tabs: { title: string; id: unknown }[]; tabs: { title: string; id: unknown }[];
activeTab?: unknown;
onActive?: (id: unknown) => void; onActive?: (id: unknown) => void;
}; };
export function Tab({ tabs = [], onActive }: TabProps) { export function Tab({ tabs = [], activeTab, onActive }: TabProps) {
const [active, setActive] = useState(0); const [active, setActive] = useState(() =>
isNil(activeTab) ? 0 : tabs.findIndex((tab) => isEqual(tab.id, activeTab)),
);
const handleActivate = useCallback((index: number) => { const handleActivate = useCallback((index: number) => {
setActive(index); setActive(index);
onActive?.(tabs[index].id); onActive?.(tabs[index].id);
}, []); }, []);
useEffect(() => {
const activeIndex = tabs.findIndex((tab) => isEqual(tab.id, activeTab));
if (!isNil(activeIndex) && !isEqual(activeIndex, -1) && !isEqual(activeIndex, active)) {
setActive(activeIndex);
}
}, [activeTab]);
return ( return (
<div className={styles.tabs_container}> <div className={styles.tabs_container}>
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (

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

@@ -1,17 +1,32 @@
import cx from 'clsx'; import cx from 'clsx';
import { isEqual, isNil } from 'lodash-es'; import { isEqual, isMap, isNil } from 'lodash-es';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import type { Option } from '../models'; import type { Option } from '../models';
import styles from './VSegmentedControl.module.css'; import styles from './VSegmentedControl.module.css';
type VSegmentedControlProps = { type VSegmentedControlProps = {
name?: string;
defaultValue?: Option['value'];
options?: Option[]; options?: Option[];
value?: Option['value']; value?: Option['value'];
onChange?: (value: Option['value']) => void; onChange?: (value: Option['value']) => void;
extendClassName?: HTMLDivElement['className'];
}; };
export function VSegmentedControl({ options = [], value, onChange }: VSegmentedControlProps) { export function VSegmentedControl({
const [selected, setSelected] = useState(value ?? options[0].value ?? null); name,
defaultValue,
options = [],
value,
onChange,
extendClassName,
}: VSegmentedControlProps) {
const [selected, setSelected] = useState(
value ??
defaultValue ??
(isMap(options[0]) ? options[0].get('value') : options[0].value) ??
null,
);
const [sliderPosition, setSliderPosition] = useState(0); const [sliderPosition, setSliderPosition] = useState(0);
const [sliderHeight, setSliderHeight] = useState(0); const [sliderHeight, setSliderHeight] = useState(0);
const sliderRef = useRef<HTMLDivElement>(null); const sliderRef = useRef<HTMLDivElement>(null);
@@ -28,17 +43,22 @@ export function VSegmentedControl({ options = [], value, onChange }: VSegmentedC
}, []); }, []);
return ( return (
<div className={styles.segmented_control}> <div className={cx(styles.segmented_control, extendClassName)}>
<div className={styles.options}> <div className={styles.options}>
{options.map((option, index) => ( {options.map((option, index) => {
const label = isMap(option) ? option.get('label') : option.label;
const value = isMap(option) ? option.get('value') : option.value;
return (
<div <div
key={`${index}_${option.value}`} key={`${index}_${value}`}
className={cx(styles.option, isEqual(selected, option.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(option.value, index)}> onClick={() => handleSelectAction(value, index)}>
{option.label} {label}
</div> </div>
))} );
})}
{!isNil(selected) && ( {!isNil(selected) && (
<div <div
className={styles.slider} className={styles.slider}
@@ -47,6 +67,7 @@ export function VSegmentedControl({ options = [], value, onChange }: VSegmentedC
/> />
)} )}
</div> </div>
{!isNil(name) && <input type="hidden" name={name} value={selected} />}
</div> </div>
); );
} }

28
src/hooks/useCopy.ts Normal file
View File

@@ -0,0 +1,28 @@
import { isEmpty, isNil } from 'lodash-es';
import { useCallback, useEffect } from 'react';
import { useCopyToClipboard } from 'react-use';
import { NotificationType, useNotification } from '../components/Notifications';
export function useCopy() {
const { showToast } = useNotification();
const [cpState, copyToClipboard] = useCopyToClipboard();
const copyAction = useCallback((content?: string | null) => {
if (isNil(content) || isEmpty(content)) return;
copyToClipboard(content);
}, []);
useEffect(() => {
if (!isNil(cpState.error)) {
showToast(NotificationType.ERROR, 'Failed to copy to clipboard', 'tabler:alert-circle', 3000);
} else if (!isNil(cpState.value)) {
showToast(
NotificationType.SUCCESS,
`Content copied to clipboard.`,
'tabler:circle-check',
3000,
);
}
}, [cpState]);
return copyAction;
}

View File

@@ -22,13 +22,16 @@ export type MaterialDesign2Scheme = {
}; };
export type MaterialDesign2SchemeSource = { export type MaterialDesign2SchemeSource = {
primary: string; primary: string | null;
secondary: string; secondary: string | null;
error: string; error: string | null;
custom_colors: Record<string, string>; custom_colors?: Record<string, string>;
}; };
export type Materialdesign2SchemeStorage = { export type MaterialDesign2SchemeStorage = {
source: MaterialDesign2SchemeSource; source?: MaterialDesign2SchemeSource;
scheme: MaterialDesign2Scheme; scheme?: MaterialDesign2Scheme;
cssVariables?: string;
scssVariables?: string;
jsVariables?: string;
}; };

View File

@@ -46,12 +46,15 @@ export type MaterialDesign3Scheme = {
}; };
export type MaterialDesign3SchemeSource = { export type MaterialDesign3SchemeSource = {
source: string; source: string | null;
error: string; error: string | null;
custom_colors: Record<string, string>; custom_colors?: Record<string, string>;
}; };
export type MaterialDesign3SchemeStorage = { export type MaterialDesign3SchemeStorage = {
source: MaterialDesign3SchemeSource; source?: MaterialDesign3SchemeSource;
scheme: MaterialDesign3Scheme; scheme?: MaterialDesign3Scheme;
cssVariables?: string;
scssVariables?: string;
jsVariables?: string;
}; };

View File

@@ -1,7 +1,15 @@
export type Option = { import { find, isNil } from 'lodash-es';
import { MaterialDesign2SchemeStorage } from './material-2-scheme';
import { MaterialDesign3SchemeStorage } from './material-3-scheme';
import { QSchemeStorage } from './q-scheme';
import { SwatchSchemeStorage } from './swatch_scheme';
export type Option<T = string | number | null> =
| {
label: string; label: string;
value: string | number | null; value: T;
}; }
| Record<'label' | 'value', T>;
export type HarmonyColor = { export type HarmonyColor = {
color: string; color: string;
@@ -22,6 +30,30 @@ export type ColorDescription = {
}; };
export type SchemeType = 'q_scheme' | 'swatch_scheme' | 'material_2' | 'material_3'; export type SchemeType = 'q_scheme' | 'swatch_scheme' | 'material_2' | 'material_3';
export type SchemeTypeOption = {
label: string;
short: string;
value: SchemeType;
};
export const SchemeTypeOptions: SchemeTypeOption[] = [
{ label: 'Q Scheme', short: 'Q', value: 'q_scheme' },
{ label: 'Swatch Scheme', short: 'Swatch', value: 'swatch_scheme' },
{ label: 'Material Design 2 Scheme', short: 'M2', value: 'material_2' },
{ label: 'Material Design 3 Scheme', short: 'M3', value: 'material_3' },
];
export function schemeType(
value?: SchemeTypeOption['value'] | null,
short?: boolean,
): string | null {
const useShort = short ?? false;
const foundType = find(SchemeTypeOptions, { value }) as SchemeTypeOption | undefined;
if (isNil(foundType)) {
return 'CORRUPTED';
}
return useShort ? foundType.short : foundType.label;
}
export type SchemeContent<SchemeStorage> = { export type SchemeContent<SchemeStorage> = {
id: string; id: string;
name: string; name: string;
@@ -35,3 +67,9 @@ export type ColorShifting = {
chroma: number; chroma: number;
lightness: number; lightness: number;
}; };
export type SchemeStorage =
| QSchemeStorage
| SwatchSchemeStorage
| MaterialDesign2SchemeStorage
| MaterialDesign3SchemeStorage;

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

@@ -0,0 +1,6 @@
@layer pages {
.corrupted {
font-size: var(--font-size-xl);
color: var(--color-danger);
}
}

View File

@@ -0,0 +1,8 @@
import styles from './CorruptedScheme.module.css';
export function CorruptedScheme() {
return (
<div className="center">
<div className={styles.corrupted}>Unrecognizable or corrupted scheme</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
@layer pages {
.export_layout {
padding: var(--spacing-s) var(--spacing-m);
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-s);
.tools {
display: flex;
flex-direction: row;
align-items: center;
font-size: var(--font-size-s);
}
}
}

View File

@@ -0,0 +1,50 @@
import { useMemo, useState } from 'react';
import { HSegmentedControl } from '../../components/HSegmentedControl';
import { Labeled } from '../../components/Labeled';
import { ScrollArea } from '../../components/ScrollArea';
import { useCopy } from '../../hooks/useCopy';
import { Option, SchemeContent, SchemeStorage } from '../../models';
import styles from './Export.module.css';
const exportOptions: Option[] = [
{ label: 'CSS', value: 'css' },
{ label: 'SCSS', value: 'scss' },
{ label: 'Javascript Object', value: 'js_object' },
];
type SchemeExportProps = {
scheme: SchemeContent<SchemeStorage>;
};
export function SchemeExport({ scheme }: SchemeExportProps) {
const [activeExport, setActiveExport] = useState<Option['value']>(exportOptions[0].value);
const exportContent = useMemo(() => {
switch (activeExport) {
case 'css':
return scheme.schemeStorage.cssVariables;
case 'scss':
return scheme.schemeStorage.scssVariables;
case 'js_object':
return scheme.schemeStorage.jsVariables;
}
}, [scheme, activeExport]);
const copyToClipboard = useCopy();
return (
<ScrollArea enableY>
<div className={styles.export_layout}>
<div className={styles.tools}>
<Labeled label="Export Options" inline>
<HSegmentedControl
options={exportOptions}
value={activeExport}
onChange={setActiveExport}
/>
</Labeled>
<button onClick={() => copyToClipboard(exportContent)}>Copy</button>
</div>
<pre>{exportContent}</pre>
</div>
</ScrollArea>
);
}

View File

@@ -0,0 +1,35 @@
import { isEqual, isNil } from 'lodash-es';
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

@@ -0,0 +1,35 @@
import { isEqual, isNil } from 'lodash-es';
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

@@ -0,0 +1,35 @@
import { isEqual, isNil } from 'lodash-es';
import { useState } from 'react';
import { Tab } from '../../components/Tab';
import { SchemeContent } from '../../models';
import { QSchemeStorage } from '../../q-scheme';
import { SchemeExport } from './Export';
import { QSchemeBuilder } from './q-scheme/Builder';
import { QSchemePreview } from './q-scheme/Preview';
const tabOptions = [
{ title: 'Overview', id: 'overview' },
{ title: 'Builder', id: 'builder' },
{ title: 'Exports', id: 'export' },
];
type QSchemeProps = {
scheme: SchemeContent<QSchemeStorage>;
};
export function QScheme({ scheme }: QSchemeProps) {
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') && <QSchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && (
<QSchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
)}
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
</>
);
}

View File

@@ -1,25 +0,0 @@
@layer pages {
.scheme_content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.series_row {
display: flex;
flex-direction: column;
gap: var(--spacing-s);
h4 {
padding-block: var(--spacing-xs);
font-size: var(--font-size-l);
}
ul {
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
gap: var(--spacing-l);
}
}
}

View File

@@ -1,61 +0,0 @@
import { isArray } from 'lodash-es';
import { ColorStand } from '../../components/ColorStand';
import { SchemeSet } from '../../stores/schemes';
import styles from './SchemeContent.module.css';
type ColorSeriesProps = {
title: string;
series: SchemeSet['lightScheme' | 'darkScheme']['primary'];
simpleSeries?: boolean;
};
function ColorSeries({ title, series, simpleSeries = false }: ColorSeriesProps) {
return (
<div className={styles.series_row}>
<h4>{title}</h4>
<ul>
<ColorStand title="Normal" color={series?.normal} />
{simpleSeries ? (
<>
<ColorStand title="Lightness" color={series?.lighten} />
<ColorStand title="Darkness" color={series?.darken} />
</>
) : (
<>
<ColorStand title="Hover" color={series?.hover} />
<ColorStand title="Active" color={series?.active} />
<ColorStand title="Focus" color={series?.focus} />
<ColorStand title="Disabled" color={series?.disabled} />
</>
)}
</ul>
</div>
);
}
type SchemeContentProps = {
scheme: SchemeSet['lightScheme' | 'darkScheme'];
};
export function SchemeContent({ scheme }: SchemeContentProps) {
return (
<div className={styles.scheme_content}>
<ColorSeries title="Foreground Series" series={scheme.foreground} simpleSeries />
<ColorSeries title="Background Series" series={scheme.background} simpleSeries />
<ColorSeries title="Primary Series" series={scheme.primary} />
{isArray(scheme.secondary) ? (
scheme.secondary.map((cSet, index) => (
<ColorSeries title={`Secondary Series ${index + 1}`} series={cSet} />
))
) : (
<ColorSeries title="Secondary Series" series={scheme.secondary} />
)}
<ColorSeries title="Accent Series" series={scheme.accent} />
<ColorSeries title="Neutral Serias" series={scheme.neutral} />
<ColorSeries title="Success Series" series={scheme.success} />
<ColorSeries title="Danger Series" series={scheme.danger} />
<ColorSeries title="Warn Series" series={scheme.warning} />
<ColorSeries title="Info Series" series={scheme.info} />
</div>
);
}

View File

@@ -1,17 +0,0 @@
@layer pages {
.scheme_view_layout {
flex: 1 0;
width: 100%;
padding: calc(var(--spacing) * 4) calc(var(--spacing) * 8);
display: flex;
flex-direction: column;
gap: var(--spacing-m);
overflow: hidden;
}
.preview_switch_container {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-m);
}
}

View File

@@ -1,23 +0,0 @@
import { useState } from 'react';
import { Switch } from '../../components/Switch';
import { SchemeSet } from '../../stores/schemes';
import { SchemeContent } from './SchemeContent';
import styles from './SchemeView.module.css';
type SchemeViewProps = {
scheme: SchemeSet['lightScheme' | 'darkScheme'];
};
export function SchemeView({ scheme }: SchemeViewProps) {
const [enablePreview, setEnablePreview] = useState(false);
return (
<div className={styles.scheme_view_layout}>
<div className={styles.preview_switch_container}>
<span>Preview scheme</span>
<Switch onChange={(checked) => setEnablePreview(checked)} />
</div>
{enablePreview ? <div>SVG Preview</div> : <SchemeContent scheme={scheme} />}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { isEqual, isNil } from 'lodash-es';
import { useState } from 'react';
import { Tab } from '../../components/Tab';
import { SchemeContent } from '../../models';
import { SwatchSchemeStorage } from '../../swatch_scheme';
import { SchemeExport } from './Export';
import { SwatchSchemeBuilder } from './swatch-scheme/Builder';
import { SwatchSchemePreview } from './swatch-scheme/Preview';
const tabOptions = [
{ title: 'Overview', id: 'overview' },
{ title: 'Builder', id: 'builder' },
{ title: 'Exports', id: 'export' },
];
type SwatchSchemeProps = {
scheme: SchemeContent<SwatchSchemeStorage>;
};
export function SwatchScheme({ scheme }: SwatchSchemeProps) {
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') && <SwatchSchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && (
<SwatchSchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
)}
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
</>
);
}

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

@@ -0,0 +1,38 @@
@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(4, 200px);
align-items: center;
gap: var(--spacing-xs);
.label {
max-width: 200px;
grid-column: 1;
padding-inline-end: var(--spacing-m);
text-align: right;
}
.color_picker_row {
grid-column: 2 / span 2;
display: flex;
align-items: center;
gap: var(--spacing-s);
.error_msg {
color: var(--color-danger);
font-size: var(--font-size-xs);
}
}
.segment_title {
grid-column: 1 / span 2;
text-align: center;
}
.parameter_input {
max-width: 8em;
}
h5 {
font-size: var(--font-size-m);
line-height: 1.7em;
}
}
}

View File

@@ -0,0 +1,362 @@
import { ColorExpand, ColorShifting, SchemeSetting, WACGSetting } from 'color-module';
import { every, isEmpty, isNil } from 'lodash-es';
import { useActionState, useMemo } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { ScrollArea } from '../../../components/ScrollArea';
import { VSegmentedControl } from '../../../components/VSegmentedControl';
import { SchemeContent } from '../../../models';
import { QSchemeSetting, QSchemeSource, QSchemeStorage } from '../../../q-scheme';
import { useUpdateScheme } from '../../../stores/schemes';
import { defaultEmptyFormData } from '../../../utls';
import styles from './Builder.module.css';
type QSchemeBuilderProps = {
scheme: SchemeContent<QSchemeStorage>;
onBuildCompleted?: () => void;
};
export function QSchemeBuilder({ scheme, onBuildCompleted }: QSchemeBuilderProps) {
const { colorFn } = useColorFunction();
const updateScheme = useUpdateScheme(scheme.id);
const defaultSetting = useMemo(() => {
try {
if (!colorFn) throw 'Web Assembly functions is not available';
const defaultValues = colorFn.q_scheme_default_settings();
if (scheme.schemeStorage.source?.setting)
return new SchemeSetting(
new ColorShifting(
scheme.schemeStorage.source?.setting?.hover.chroma ?? defaultValues.hover.chroma,
scheme.schemeStorage.source?.setting?.hover.lightness ?? defaultValues.hover.lightness,
),
new ColorShifting(
scheme.schemeStorage.source?.setting?.active.chroma ?? defaultValues.active.chroma,
scheme.schemeStorage.source?.setting?.active.lightness ??
defaultValues.active.lightness,
),
new ColorShifting(
scheme.schemeStorage.source?.setting?.focus.chroma ?? defaultValues.focus.chroma,
scheme.schemeStorage.source?.setting?.focus.lightness ?? defaultValues.focus.lightness,
),
new ColorShifting(
scheme.schemeStorage.source?.setting?.disabled.chroma ?? defaultValues.disabled.chroma,
scheme.schemeStorage.source?.setting?.disabled.lightness ??
defaultValues.disabled.lightness,
),
new ColorShifting(
scheme.schemeStorage.source?.setting?.dark_convert.chroma ??
defaultValues.dark_convert.chroma,
scheme.schemeStorage.source?.setting?.dark_convert.lightness ??
defaultValues.dark_convert.lightness,
),
scheme.schemeStorage.source?.setting?.expand_method ?? defaultValues.expand_method,
scheme.schemeStorage.source?.setting?.wacg_follows ?? defaultValues.wacg_follows,
);
return defaultValues;
} catch (e) {
console.error('[Q scheme builder]', e);
}
return null;
}, [scheme]);
const expandingMethods = useMemo(() => {
try {
if (!colorFn) throw 'Web Assembly functions is not available';
return colorFn.q_scheme_color_expanding_methods();
} catch (e) {
console.error('[Q scheme builder]', e);
}
return [];
}, []);
const wacgFollowStrategies = useMemo(() => {
try {
if (!colorFn) throw 'Web Assembly functions is not available';
return colorFn.q_scheme_wacg_settings();
} catch (e) {
console.error('[Q scheme builder]', e);
}
return [];
}, []);
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>();
const requiredFields = [
'primary',
'danger',
'success',
'warn',
'info',
'foreground',
'background',
];
for (const field of requiredFields) {
if (!formData.get(field)) {
errMsg.set(field, 'This color is required for scheme generating.');
}
}
if (!isEmpty(errMsg)) return errMsg;
try {
const schemeSetting = new SchemeSetting(
new ColorShifting(
Number(formData.get('hover_chroma')) / 100,
Number(formData.get('hover_lightness')) / 100,
),
new ColorShifting(
Number(formData.get('active_chroma')) / 100,
Number(formData.get('active_lightness')) / 100,
),
new ColorShifting(
Number(formData.get('focus_chroma')) / 100,
Number(formData.get('focus_lightness')) / 100,
),
new ColorShifting(
Number(formData.get('disabled_chroma')) / 100,
Number(formData.get('disabled_lightness')) / 100,
),
new ColorShifting(
Number(formData.get('dark_chroma')) / 100,
Number(formData.get('dark_lightness')) / 100,
),
Number(formData.get('expanding')) as ColorExpand,
Number(formData.get('wacg')) as WACGSetting,
);
const dumpedSetting = schemeSetting.toJsValue() as QSchemeSetting;
const source: QSchemeSource = {
primary: defaultEmptyFormData(formData, 'primary', null),
secondary: defaultEmptyFormData(formData, 'secondary', null),
tertiary: defaultEmptyFormData(formData, 'tertiary', null),
accent: defaultEmptyFormData(formData, 'accent', null),
danger: defaultEmptyFormData(formData, 'danger', null),
success: defaultEmptyFormData(formData, 'success', null),
warning: defaultEmptyFormData(formData, 'warn', null),
info: defaultEmptyFormData(formData, 'info', null),
foreground: defaultEmptyFormData(formData, 'foreground', null),
background: defaultEmptyFormData(formData, 'background', null),
setting: dumpedSetting,
};
const generatedScheme = every([source.secondary, source.tertiary, source.accent], isNil)
? colorFn?.generate_q_scheme_automatically(
source.primary ?? '',
source.danger ?? '',
source.success ?? '',
source.warning ?? '',
source.info ?? '',
source.foreground ?? '',
source.background ?? '',
schemeSetting,
)
: colorFn?.generate_q_scheme_manually(
source.primary ?? '',
source.secondary ?? undefined,
source.tertiary ?? undefined,
source.accent ?? undefined,
source.danger ?? '',
source.success ?? '',
source.warning ?? '',
source.info ?? '',
source.foreground ?? '',
source.background ?? '',
schemeSetting,
);
updateScheme((prev) => {
prev.schemeStorage.source = source;
prev.schemeStorage.scheme = generatedScheme[0];
prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.scssVariables = generatedScheme[2];
prev.schemeStorage.jsVariables = generatedScheme[3];
return prev;
});
onBuildCompleted?.();
} catch (e) {
console.error('[build q scheme]', e);
}
return errMsg;
},
new Map<string, string>(),
);
return (
<ScrollArea enableY>
<form action={handleSubmitAction} className={styles.builder_layout}>
<h5 className={styles.segment_title}>Original Colors</h5>
<label className={styles.label}>Primary Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="primary" color={scheme.schemeStorage.source?.primary} />
{errMsg.has('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={scheme.schemeStorage.source?.secondary} />
</div>
<label className={styles.label}>Tertiary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="tertiary" color={scheme.schemeStorage.source?.tertiary} />
</div>
<label className={styles.label}>Accent Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="accent" color={scheme.schemeStorage.source?.accent} />
</div>
<label className={styles.label}>Danger Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="danger" color={scheme.schemeStorage.source?.danger} />
{errMsg.has('danger') && <span className={styles.error_msg}>{errMsg.get('danger')}</span>}
</div>
<label className={styles.label}>Success Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="success" color={scheme.schemeStorage.source?.success} />
{errMsg.has('success') && (
<span className={styles.error_msg}>{errMsg.get('success')}</span>
)}
</div>
<label className={styles.label}>Warning Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="warn" color={scheme.schemeStorage.source?.warning} />
{errMsg.has('warn') && <span className={styles.error_msg}>{errMsg.get('warn')}</span>}
</div>
<label className={styles.label}>Info Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="info" color={scheme.schemeStorage.source?.info} />
{errMsg.has('info') && <span className={styles.error_msg}>{errMsg.get('info')}</span>}
</div>
<label className={styles.label}>Foreground Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="foreground" color={scheme.schemeStorage.source?.foreground} />
{errMsg.has('foreground') && (
<span className={styles.error_msg}>{errMsg.get('foreground')}</span>
)}
</div>
<label className={styles.label}>Background Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker name="background" color={scheme.schemeStorage.source?.background} />
{errMsg.has('background') && (
<span className={styles.error_msg}>{errMsg.get('background')}</span>
)}
</div>
<h5 className={styles.segment_title}>Automated parameters</h5>
<label style={{ gridColumn: 2 }}>Chroma shifting</label>
<label style={{ gridColumn: 3 }}>Lightness shifting</label>
<label className={styles.label}>Hover</label>
<div className="input_wrapper">
<input
type="number"
name="hover_chroma"
defaultValue={((defaultSetting?.hover.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="hover_lightness"
defaultValue={((defaultSetting?.hover.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<label className={styles.label}>Active</label>
<div className="input_wrapper">
<input
type="number"
name="active_chroma"
defaultValue={((defaultSetting?.active.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="active_lightness"
defaultValue={((defaultSetting?.active.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<label className={styles.label}>Focus</label>
<div className="input_wrapper">
<input
type="number"
name="focus_chroma"
defaultValue={((defaultSetting?.focus.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="focus_lightness"
defaultValue={((defaultSetting?.focus.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<label className={styles.label}>Disabled</label>
<div className="input_wrapper">
<input
type="number"
name="disabled_chroma"
defaultValue={((defaultSetting?.disabled.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="disabled_lightness"
defaultValue={((defaultSetting?.disabled.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<label className={styles.label}>Convert to Dark scheme</label>
<div className="input_wrapper">
<input
type="number"
name="dark_chroma"
defaultValue={((defaultSetting?.dark_convert.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="dark_lightness"
defaultValue={((defaultSetting?.dark_convert.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<h5 className={styles.segment_title}>Settings</h5>
<label className={styles.label}>Color Expanding Method</label>
<div style={{ gridColumn: '2 / span 2' }}>
<VSegmentedControl
options={expandingMethods}
name="expanding"
defaultValue={defaultSetting?.expand_method}
/>
</div>
<label className={styles.label}>Follow WACG Standard</label>
<div style={{ gridColumn: '2 / span 2' }}>
<VSegmentedControl
options={wacgFollowStrategies}
name="wacg"
defaultValue={defaultSetting?.wacg_follows}
/>
</div>
<div style={{ gridColumn: '2 / span 2' }}>
<button type="submit" className="primary">
Build Scheme
</button>
</div>
</form>
</ScrollArea>
);
}

View File

@@ -0,0 +1,35 @@
@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-xl) var(--spacing-m);
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: var(--spacing-xs);
h2 {
font-size: var(--font-size-xl);
font-weight: bold;
line-height: 1.7em;
}
}
.preview_cell {
padding: var(--spacing-xs) var(--spacing-s);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-xxs);
font-size: var(--font-size-s);
line-height: 1.5em;
.wacg {
font-size: var(--font-size-xxs);
line-height: 1em;
}
}
}

View File

@@ -0,0 +1,100 @@
import { ReactNode, useMemo } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { ScrollArea } from '../../../components/ScrollArea';
import { SchemeContent } from '../../../models';
import { Baseline, ColorSet, QSchemeStorage } from '../../../q-scheme';
import styles from './Preview.module.css';
type PreviewCellProps = {
bg: string;
fg: string;
children: ReactNode;
};
function PreviewCell({ bg, fg, children }: PreviewCellProps) {
const { colorFn } = useColorFunction();
const wacgRatio = useMemo(() => {
try {
if (!colorFn) return null;
return colorFn.wacg_relative_contrast(fg, bg);
} catch (e) {
console.error('[Error on calc WACG Ratio]', e);
}
return null;
}, [bg, fg]);
return (
<div className={styles.preview_cell} style={{ backgroundColor: `#${bg}`, color: `#${fg}` }}>
<span>{children}</span>
{wacgRatio && <span className={styles.wacg}>WACG {wacgRatio?.toFixed(2)}</span>}
</div>
);
}
type PreviewCellSetProps = {
colorSet: ColorSet;
name: string;
};
function PreviewCellSetGroup({ colorSet, name }: PreviewCellSetProps) {
return (
<>
<PreviewCell bg={colorSet.root} fg={colorSet.onRoot}>
{name}
</PreviewCell>
<PreviewCell bg={colorSet.hover} fg={colorSet.onHover}>
{name} Hover
</PreviewCell>
<PreviewCell bg={colorSet.active} fg={colorSet.onActive}>
{name} Active
</PreviewCell>
<PreviewCell bg={colorSet.focus} fg={colorSet.onFocus}>
{name} Focus
</PreviewCell>
<PreviewCell bg={colorSet.disabled} fg={colorSet.onDisabled}>
{name} Disabled
</PreviewCell>
</>
);
}
type PreviewBlockProps = {
baseline: Baseline;
title: string;
};
function PreviewBlock({ baseline, title }: PreviewBlockProps) {
return (
<div className={styles.preview_block} style={{ backgroundColor: `#${baseline.background}` }}>
<h2 style={{ color: `#${baseline.foreground}`, gridColumn: '1 / span 5' }}>{title}</h2>
<PreviewCellSetGroup colorSet={baseline.primary} name="Primary" />
{baseline.secondary && <PreviewCellSetGroup colorSet={baseline.secondary} name="Secondary" />}
{baseline.tertiary && <PreviewCellSetGroup colorSet={baseline.tertiary} name="Tertiary" />}
{baseline.accent && <PreviewCellSetGroup colorSet={baseline.accent} name="Accent" />}
<PreviewCellSetGroup colorSet={baseline.neutral} name="Neutral" />
<PreviewCellSetGroup colorSet={baseline.danger} name="Danger" />
<PreviewCellSetGroup colorSet={baseline.warning} name="Warning" />
<PreviewCellSetGroup colorSet={baseline.success} name="Success" />
<PreviewCellSetGroup colorSet={baseline.info} name="Info" />
</div>
);
}
type PreviewProps = {
scheme: SchemeContent<QSchemeStorage>;
};
export function QSchemePreview({ scheme }: PreviewProps) {
return (
<ScrollArea enableY>
<div className={styles.preview_layout}>
{scheme.schemeStorage.scheme?.light && (
<PreviewBlock baseline={scheme.schemeStorage.scheme.light} title="Light Scheme" />
)}
{scheme.schemeStorage.scheme?.dark && (
<PreviewBlock baseline={scheme.schemeStorage.scheme.dark} title="Dark Scheme" />
)}
</div>
</ScrollArea>
);
}

View File

@@ -0,0 +1,49 @@
@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;
}
.parameter_row {
grid-column: 2 / span 2;
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.error_msg {
color: var(--color-danger);
font-size: var(--font-size-xs);
}
.segment_title {
grid-column: 1 / span 2;
text-align: center;
}
.parameter_input {
max-width: 8em;
}
h5 {
font-size: var(--font-size-m);
line-height: 1.7em;
}
}
.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,247 @@
import { ColorShifting, SwatchEntry, SwatchSchemeSetting } from 'color-module';
import { includes, isEmpty, isEqual, isNaN } from 'lodash-es';
import { useActionState, useCallback, useMemo, useState } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { ScrollArea } from '../../../components/ScrollArea';
import { Switch } from '../../../components/Switch';
import { SchemeContent } from '../../../models';
import { useUpdateScheme } from '../../../stores/schemes';
import {
QSwatchEntry,
QSwatchSchemeSetting,
SwatchScheme,
SwatchSchemeStorage,
} from '../../../swatch_scheme';
import { mapToObject } from '../../../utls';
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
import styles from './Builder.module.css';
type SwatchSchemeBuilderProps = {
scheme: SchemeContent<SwatchSchemeStorage>;
onBuildCompleted?: () => void;
};
export function SwatchSchemeBuilder({ scheme, onBuildCompleted }: SwatchSchemeBuilderProps) {
const { colorFn } = useColorFunction();
const updateScheme = useUpdateScheme(scheme.id);
const originalColors = useMemo(() => {
return (scheme.schemeStorage.source?.colors ?? []).map(
(color, index) =>
({ id: `oc_${index}`, name: color.name, color: color.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(() => {
return [...originalColors, ...newColors]
.map((color) => color.id)
.filter((id) => !includes(deleted, id));
}, [originalColors, newColors, deleted]);
const defaultSetting = useMemo(() => {
try {
if (!colorFn) throw 'Web Assembly functions is not available';
const defaultValues = colorFn.swatch_scheme_default_settings();
if (scheme.schemeStorage.source?.setting) {
return new SwatchSchemeSetting(
scheme.schemeStorage.source.setting.amount ?? defaultValues.amount,
scheme.schemeStorage.source.setting.min_lightness ?? defaultValues.min_lightness,
scheme.schemeStorage.source.setting.max_lightness ?? defaultValues.max_lightness,
scheme.schemeStorage.source.setting.include_primary ?? defaultValues.include_primary,
new ColorShifting(
scheme.schemeStorage.source.setting.dark_convert.chroma ??
defaultValues.dark_convert.chroma,
scheme.schemeStorage.source.setting.dark_convert.lightness ??
defaultValues.dark_convert.lightness,
),
);
}
return defaultValues;
} catch (e) {
console.error('[Q scheme builder]', e);
}
return null;
}, [scheme.schemeStorage.source]);
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>();
try {
const swatchAmount = Number(formData.get('amount'));
if (isNaN(swatchAmount) || swatchAmount <= 0) {
errMsg.set('amount', 'MUST be a positive number');
}
if (swatchAmount > 30) {
errMsg.set('amount', 'MUST be less than 30');
}
const minLightness = Number(formData.get('min_lightness'));
if (isNaN(minLightness) || minLightness < 0 || minLightness > 100) {
errMsg.set('min', 'MUST be a number between 0 and 100');
}
const maxLightness = Number(formData.get('max_lightness'));
if (isNaN(maxLightness) || maxLightness < 0 || maxLightness > 100) {
errMsg.set('max', 'MUST be a number between 0 and 100');
}
const includePrimary = isEqual(formData.get('include_primary'), 'true');
const darkConvertChroma = Number(formData.get('dark_chroma')) / 100.0;
const darkConvertLightness = Number(formData.get('dark_lightness')) / 100.0;
const swatchSetting = new SwatchSchemeSetting(
swatchAmount,
minLightness / 100.0,
maxLightness / 100.0,
includePrimary,
new ColorShifting(darkConvertChroma, darkConvertLightness),
);
const dumpedSettings = swatchSetting.toJsValue() as QSwatchSchemeSetting;
const entries: SwatchEntry[] = [];
for (const key of colorKeys) {
const name = String(formData.get(`name_${key}`));
const color = String(formData.get(`color_${key}`));
if (isEmpty(name) || isEmpty(color)) continue;
entries.push(new SwatchEntry(name, color));
}
const dumpedEntries = entries.map((entry) => entry.toJsValue() as QSwatchEntry);
if (isEmpty(entries)) {
errMsg.set('color', 'At least one color is required');
}
if (!isEmpty(errMsg)) return errMsg;
const generatedScheme = colorFn?.generate_swatch_scheme(entries, swatchSetting);
console.debug('[generated scheme]', generatedScheme);
updateScheme((prev) => {
prev.schemeStorage.source = {
colors: dumpedEntries,
setting: dumpedSettings,
};
prev.schemeStorage.scheme = mapToObject(generatedScheme[0]) as SwatchScheme;
prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.scssVariables = generatedScheme[2];
prev.schemeStorage.jsVariables = generatedScheme[3];
return prev;
});
onBuildCompleted?.();
} catch (e) {
console.error('[build swatch scheme]', e);
}
return errMsg;
},
new Map<string, string>(),
);
return (
<ScrollArea enableY>
<form action={handleSubmitAction} className={styles.builder_layout}>
<h5 className={styles.segment_title}>Automatic Parameters</h5>
<label className={styles.label}>Swatch Amount</label>
<div className={styles.parameter_row}>
<div className="input_wrapper">
<input
type="number"
name="amount"
defaultValue={defaultSetting?.amount ?? 0}
className={styles.parameter_input}
/>
</div>
{errMsg.has('amount') && <span className={styles.error_msg}>{errMsg.get('amount')}</span>}
</div>
<label className={styles.label}>Minimum Lightness</label>
<div className={styles.parameter_row}>
<div className="input_wrapper">
<input
type="number"
name="min_lightness"
defaultValue={((defaultSetting?.min_lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
{errMsg.has('min') && <span className={styles.error_msg}>{errMsg.get('min')}</span>}
</div>
<label className={styles.label}>Maximum Lightness</label>
<div className={styles.parameter_row}>
<div className="input_wrapper">
<input
type="number"
name="max_lightness"
defaultValue={((defaultSetting?.max_lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
{errMsg.has('max') && <span className={styles.error_msg}>{errMsg.get('max')}</span>}
</div>
<label className={styles.label}>Include Primary Color</label>
<div>
<Switch name="include_primary" checked={defaultSetting?.include_primary ?? false} />
</div>
<label style={{ gridColumn: 2 }}>Chroma shifting</label>
<label style={{ gridColumn: 3 }}>Lightness shifting</label>
<label className={styles.label}>Convert to Dark scheme</label>
<div className="input_wrapper">
<input
type="number"
name="dark_chroma"
defaultValue={((defaultSetting?.dark_convert.chroma ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<div className="input_wrapper">
<input
type="number"
name="dark_lightness"
defaultValue={((defaultSetting?.dark_convert.lightness ?? 0) * 100).toFixed(2)}
className={styles.parameter_input}
/>
<span>%</span>
</div>
<h5 className={styles.segment_title}>Swatch 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((c) => !includes(deleted, c.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
{newColors
.filter((c) => !includes(deleted, c.id))
.map((color) => (
<ColorEntry
key={color.id}
entry={color}
onDelete={(index) => setDeleted((prev) => [...prev, index])}
/>
))}
{errMsg.has('color') && (
<div style={{ gridColumn: '1 / span 2' }}>
<span className={styles.error_msg}>{errMsg.get('color')}</span>
</div>
)}
<div style={{ gridColumn: '2 / span 2' }}>
<button type="submit" className="primary">
Build Scheme
</button>
</div>
</form>
</ScrollArea>
);
}

View File

@@ -0,0 +1,27 @@
@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);
}
.scheme_block {
display: grid;
gap: var(--spacing-m) var(--spacing-xs);
align-items: center;
}
.scheme_cell {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
font-size: var(--font-size-s);
.block {
height: 3em;
}
.label {
text-align: center;
}
}
}

View File

@@ -0,0 +1,67 @@
import { ScrollArea } from '../../../components/ScrollArea';
import { SchemeContent } from '../../../models';
import { SwatchSchemeStorage } from '../../../swatch_scheme';
import styles from './Preview.module.css';
type SwatchCellProps = {
color: string;
};
export function SwatchCell({ color }: SwatchCellProps) {
return (
<div className={styles.scheme_cell}>
<div className={styles.block} style={{ backgroundColor: `#${color}` }} />
</div>
);
}
type SchemeBlockProps = {
amount: number;
scheme: Record<string, string[]>;
};
export function SchemeBlock({ amount, scheme }: SchemeBlockProps) {
return (
<div
className={styles.scheme_block}
style={{ gridTemplateColumns: `minmax(120px, 1fr) repeat(${amount}, 1fr)` }}>
<div />
{Array.from({ length: amount }).map((_, index) => (
<div key={index} className={styles.scheme_cell}>
<div className={styles.label}>{index * 100}</div>
</div>
))}
{Object.entries(scheme).map(([name, colors]) => (
<>
<h6>{name}</h6>
{colors.map((color, index) => (
<SwatchCell key={index} color={color} />
))}
</>
))}
</div>
);
}
type SwatchSchemePreviewProps = {
scheme: SchemeContent<SwatchSchemeStorage>;
};
export function SwatchSchemePreview({ scheme }: SwatchSchemePreviewProps) {
return (
<ScrollArea enableY>
<div className={styles.preview_layout}>
<h2>Light Scheme</h2>
<SchemeBlock
amount={scheme.schemeStorage.source?.setting?.amount ?? 0}
scheme={scheme.schemeStorage.scheme!.light}
/>
<h2>Dark Scheme</h2>
<SchemeBlock
amount={scheme.schemeStorage.source?.setting?.amount ?? 0}
scheme={scheme.schemeStorage.scheme!.dark}
/>
</div>
</ScrollArea>
);
}

View File

@@ -20,6 +20,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: var(--spacing-s);
.scheme_item { .scheme_item {
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 8); padding: calc(var(--spacing) * 2) calc(var(--spacing) * 8);
&.selected { &.selected {
@@ -53,6 +54,42 @@
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--color-accent); color: var(--color-accent);
} }
.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);
}
}
.active_btn {
font-size: var(--font-size-xs);
color: var(--color-yuebai);
background-color: oklch(from var(--color-info) l c h / 0.25);
&:hover {
background-color: oklch(from var(--color-info-hover) l c h / 0.65);
}
&:active {
background-color: oklch(from var(--color-info-active) l c h / 0.65);
}
&.deactive {
background-color: oklch(from var(--color-warn) l c h / 0.25);
&:hover {
background-color: oklch(from var(--color-warn-hover) l c h / 0.65);
}
&:active {
background-color: oklch(from var(--color-warn-active) l c h / 0.65);
}
}
}
.active_badge {
font-size: var(--font-size-xs);
color: var(--color-yuebai);
background-color: var(--color-success);
}
} }
.empty_prompt { .empty_prompt {
padding-inline: calc(var(--spacing) * 6); padding-inline: calc(var(--spacing) * 6);

View File

@@ -1,11 +1,12 @@
import { Icon } from '@iconify/react/dist/iconify.js';
import cx from 'clsx'; import cx from 'clsx';
import dayjs from 'dayjs'; import { useAtom } from 'jotai';
import { useAtomValue } from 'jotai';
import { isEmpty, isEqual } from 'lodash-es'; import { isEmpty, isEqual } from 'lodash-es';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom'; import { Link, useNavigate, useParams } from 'react-router-dom';
import { activeSchemeAtom, useSchemeList } from '../../stores/schemes'; import { ActionIcon } from '../../components/ActionIcon';
import { Badge } from '../../components/Badge';
import { SchemeSign } from '../../components/SchemeSign';
import { activeSchemeAtom, useRemoveScheme, useSchemeList } from '../../stores/schemes';
import styles from './SchemeList.module.css'; import styles from './SchemeList.module.css';
function OperateButtons() { function OperateButtons() {
@@ -25,9 +26,20 @@ type SchemeItemProps = {
function SchemeItem({ item }: SchemeItemProps) { function SchemeItem({ item }: SchemeItemProps) {
const navParams = useParams(); const navParams = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const activedScheme = useAtomValue(activeSchemeAtom); const [activedScheme, setActiveScheme] = useAtom(activeSchemeAtom);
const removeScheme = useRemoveScheme(item.id);
const isActived = useMemo(() => isEqual(activedScheme, item.id), [activedScheme, item.id]); const isActived = useMemo(() => isEqual(activedScheme, item.id), [activedScheme, item.id]);
const isSelected = useMemo(() => isEqual(navParams['id'], item.id), [navParams, item.id]); const isSelected = useMemo(() => isEqual(navParams['id'], item.id), [navParams, item.id]);
const handleActiveScheme = useCallback(() => {
setActiveScheme((prev) => (prev ? null : item.id));
}, [item]);
const handleRemoveScheme = useCallback(() => {
removeScheme();
if (isActived) {
setActiveScheme(null);
}
navigate(-1);
}, [item, isActived]);
return ( return (
<div <div
@@ -35,11 +47,23 @@ function SchemeItem({ item }: SchemeItemProps) {
onClick={() => navigate(item.id)}> onClick={() => navigate(item.id)}>
<div className={styles.name}>{item.name}</div> <div className={styles.name}>{item.name}</div>
<div className={styles.status}> <div className={styles.status}>
<div className={styles.create_time}> <SchemeSign scheme={item.type} short />
created at {dayjs(item.createdAt).format('YYYY-MM-DD')} {isActived && <Badge extendClassName={styles.active_badge}>ACTIVE</Badge>}
</div>
<div className="spacer"></div> <div className="spacer"></div>
{isActived && <Icon icon="tabler:check" className={styles.active_icon} />} {isSelected && (
<>
<ActionIcon
icon="tabler:trash"
extendClassName={styles.delete_btn}
onClick={handleRemoveScheme}
/>
<ActionIcon
icon="tabler:checkbox"
extendClassName={cx(styles.active_btn, isActived && styles.deactive)}
onClick={handleActiveScheme}
/>
</>
)}
</div> </div>
</div> </div>
); );

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);

Some files were not shown because too many files have changed in this diff Show More