Compare commits

...

17 Commits

Author SHA1 Message Date
徐涛
5a1454e6c2 feat(ContextMenu): 添加Q2SchemeMenu组件支持自定义颜色配置 2025-07-20 08:24:56 +08:00
徐涛
dd1273dad4 refactor(color_set): 优化颜色匹配算法中的元组结构
将存储颜色匹配结果的元组从(lightness, min_wacg_abs)扩展为(lightness, avg_wacg_abs, sum_wacg_abs)
修改匹配逻辑以同时考虑平均和总和WACG值
2025-07-20 08:01:47 +08:00
徐涛
afaa7d25de feat(q-2-scheme): 添加中性色板并优化自定义颜色展示
- 在Q2Baseline类型中添加neutralSwatch字段
- 将custom重命名为customColors以提高语义清晰度
- 优化预览组件中色板的展示样式,添加标签和间距
- 更新Builder组件处理自定义颜色的映射逻辑
2025-07-20 07:54:17 +08:00
徐涛
edc2a0546e refactor(serialization): 重构颜色模块的序列化实现
将手动实现的序列化逻辑替换为派生宏实现
添加foreign_serializer模块处理特殊序列化需求
优化代码结构并减少重复代码
2025-07-20 07:25:13 +08:00
徐涛
f82575c49b feat(serializer): 添加将Oklch颜色序列化为十六进制字符串的功能 2025-07-20 07:24:55 +08:00
徐涛
a77fb3f18b feat(q-2-scheme): 添加颜色方案预览组件并优化类型定义
新增 Q2SchemePreview 组件用于展示颜色方案的预览效果
将 Map 类型改为 Record 以简化数据结构
2025-07-18 15:45:20 +08:00
徐涛
a7ef8eb576 fix(color-module): 修复暗黑模式下中性色计算错误
调整暗黑模式下的中性色计算逻辑,确保在不同主题下颜色值计算正确。主要修改了outline、shadow、surface等颜色的计算方式,根据is_dark标志使用不同的系数。
2025-07-18 15:45:11 +08:00
徐涛
600c8c92ce perf(serialization): 优化颜色模块的序列化性能
使用直接序列化结构代替中间JSON对象,减少内存分配和转换开销
2025-07-18 13:47:45 +08:00
徐涛
137079e5c6 build(color-module): 添加wasm随机数支持和internment依赖
添加getrandom wasm_js后端配置以支持wasm环境下的随机数生成
添加internment依赖用于优化内存管理
2025-07-18 13:46:43 +08:00
徐涛
a71a635eb8 refactor(color-module): 优化序列化实现并改进颜色计算逻辑
- 使用serde_json简化Swatch和ColorSet的序列化实现
- 修改Swatch.get()方法以使用0-1范围的亮度值
- 改进search_for_common_wacg_color算法,使用平均值替代最小值
- 为ColorSet添加hover字段的序列化
2025-07-18 09:09:58 +08:00
徐涛
8a09806b8c feat(方案构建器): 添加Q2方案构建器界面及功能
实现Q2方案构建器的完整界面,包括颜色选择、自定义颜色管理、自动化参数配置和方案设置
添加构建和保存草稿功能,支持生成完整的色彩方案
包含错误处理和表单验证逻辑
2025-07-17 08:18:54 +08:00
徐涛
459b5ea1ab feat(q-2-scheme): 新增Q2SchemeBuilder组件和样式文件 2025-07-14 23:05:08 +08:00
徐涛
4119a1ab64 fix: 修正Q2方案标签并添加导出功能
修复Q2方案的标签显示不一致问题,将"Q Scheme 2"改为"Q2 Scheme"
在Q2方案页面添加导出功能组件
2025-07-14 23:05:01 +08:00
徐涛
e327885545 feat(配色方案): 添加Q2配色方案支持
新增Q2配色方案相关组件、模型和样式定义
在SchemeSign组件中添加q2样式支持
扩展模型以包含Q2方案类型和存储结构
2025-07-14 22:39:01 +08:00
徐涛
680ca173da chore: 更新bun.lockb文件 2025-07-14 21:55:19 +08:00
徐涛
622b76a621 build: 更新依赖包版本
升级多个 npm 和 Rust 依赖包版本,包括 @iconify/react、react-error-boundary、typescript、vite 等前端依赖,以及 color-module 的 Rust 相关依赖
2025-07-14 21:53:34 +08:00
徐涛
bd4a2c9b49 feat(q_style_2): 新增QStyle2颜色方案模块
添加QStyle2颜色方案模块,包含基础颜色集、色板生成和自动配色功能
实现颜色方案的CSS、SCSS和JavaScript输出支持
新增generate_q_scheme_2_manually函数用于手动生成QStyle2方案
2025-07-14 09:03:59 +08:00
24 changed files with 2175 additions and 26 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -1,7 +1,8 @@
[package]
name = "color-module"
version = "0.1.0"
edition = "2021"
version = "0.1.9"
edition = "2024"
rust-version = "1.88.0"
[lib]
crate-type = ["cdylib"]
@ -9,16 +10,18 @@ crate-type = ["cdylib"]
[dependencies]
color-name = "1.1.0"
enum-iterator = "2.1.0"
getrandom = { version = "0.3.3", features = ["wasm_js"] }
internment = { version = "0.8.6", features = ["arc"] }
linked-hash-map = { version = "0.5.6", features = ["serde", "serde_impl"] }
linked_hash_set = { version = "0.1.5", features = ["serde"] }
palette = { version = "0.7.6", features = ["serde"] }
serde = { version = "1.0.216", features = ["derive"] }
serde = { version = "1.0.219", features = ["derive"] }
serde-wasm-bindgen = "0.6.5"
serde_json = "1.0.134"
serde_repr = "0.1.19"
strum = { version = "0.26.3", features = ["derive", "strum_macros"] }
strum_macros = "0.26.4"
thiserror = "2.0.9"
serde_json = "1.0.140"
serde_repr = "0.1.20"
strum = { version = "0.27.1", features = ["derive", "strum_macros"] }
strum_macros = "0.27.1"
thiserror = "2.0.12"
wasm-bindgen = { version = "0.2.99", features = ["serde", "serde_json", "serde-serialize"] }
web-sys = {version = "0.3.77", features = ["console", "Window"]}

View File

@ -1,2 +1,2 @@
#!/bin/bash
wasm-pack build --release --target web -d ../color_functions
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' wasm-pack build --release --target web -d ../color_functions

View File

@ -0,0 +1,12 @@
use palette::Oklch;
use serde::Serializer;
use crate::convert::map_oklch_to_srgb_hex;
pub fn serialize_oklch_to_hex<S>(color: &Oklch, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let hex_color = map_oklch_to_srgb_hex(color);
serializer.serialize_str(&hex_color)
}

View File

@ -1,10 +1,10 @@
use std::str::FromStr;
use palette::{
FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Oklch, Srgb,
cam16::{Cam16Jch, Parameters},
color_difference::Wcag21RelativeContrast,
convert::FromColorUnclamped,
FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Oklch, Srgb,
};
use wasm_bindgen::prelude::*;
@ -14,6 +14,7 @@ mod color_differ;
mod color_shifting;
mod convert;
mod errors;
mod foreign_serializer;
mod palettes;
mod reversing;
mod schemes;
@ -139,10 +140,6 @@ pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result<f32, err
#[macro_export]
macro_rules! cond {
($s: expr, $a: expr, $b: expr) => {
if $s {
$a
} else {
$b
}
if $s { $a } else { $b }
};
}

View File

@ -1,18 +1,20 @@
use std::collections::HashMap;
use internment::Intern;
use material_design_2::MaterialDesign2Scheme;
use material_design_3::MaterialDesign3Scheme;
use material_design_3_dynamic::{build_baseline, build_dynamic_scheme, build_swatches, Variant};
use material_design_3_dynamic::{Variant, build_baseline, build_dynamic_scheme, build_swatches};
use q_style::{QScheme, SchemeSetting};
use swatch_style::{SwatchEntry, SwatchSchemeSetting};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
use crate::errors;
use crate::{errors, schemes::q_style_2::QScheme2};
pub mod material_design_2;
pub mod material_design_3;
pub mod material_design_3_dynamic;
pub mod q_style;
pub mod q_style_2;
pub mod swatch_style;
pub trait SchemeExport {
@ -22,6 +24,10 @@ pub trait SchemeExport {
fn output_javascript_object(&self) -> String;
}
pub fn get_static_str(s: &str) -> &'static str {
Intern::new(s.to_string()).as_ref()
}
#[wasm_bindgen]
pub fn generate_material_design_3_scheme(
source_color: &str,
@ -135,6 +141,50 @@ pub fn generate_q_scheme_manually(
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_q_scheme_2_manually(
primary_color: &str,
secondary_color: Option<String>,
tertiary_color: Option<String>,
accent_color: Option<String>,
danger_color: &str,
success_color: &str,
warn_color: &str,
info_color: &str,
fg_color: &str,
bg_color: &str,
custom_colors: JsValue,
setting: SchemeSetting,
) -> Result<JsValue, errors::ColorError> {
let mut scheme = QScheme2::new(
primary_color,
secondary_color.as_deref(),
tertiary_color.as_deref(),
accent_color.as_deref(),
danger_color,
success_color,
warn_color,
info_color,
fg_color,
bg_color,
setting,
)?;
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
for (name, color) in custom_colors {
scheme.add_custom_color(&name, &color)?;
}
Ok(serde_wasm_bindgen::to_value(&(
scheme.clone(),
scheme.output_css_variables(),
scheme.output_css_auto_scheme_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_swatch_scheme(
colors: Vec<SwatchEntry>,

View File

@ -3,7 +3,6 @@ use std::str::FromStr;
use baseline::Baseline;
use linked_hash_set::LinkedHashSet;
use palette::FromColor;
use scheme_setting::{ColorExpand, WACGSetting};
use serde::Serialize;
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
@ -16,7 +15,8 @@ mod color_set;
mod neutral_swatch;
mod scheme_setting;
pub use scheme_setting::{ColorShifting, SchemeSetting};
pub use neutral_swatch::NeutralSwatch;
pub use scheme_setting::{ColorExpand, ColorShifting, SchemeSetting, WACGSetting};
#[derive(Debug, Clone, Serialize)]
pub struct QScheme {

View File

@ -0,0 +1,513 @@
use std::{collections::HashMap, sync::Arc};
use linked_hash_map::LinkedHashMap;
use palette::{
Oklch, ShiftHue,
color_theory::{Analogous, Complementary, SplitComplementary, Tetradic, Triadic},
};
use serde::Serialize;
use crate::{
convert::map_oklch_to_srgb_hex,
errors,
schemes::{
q_style::{ColorExpand, NeutralSwatch, SchemeSetting},
q_style_2::{color_set::ColorSet, swatch::Swatch},
},
};
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorUnit {
pub root: ColorSet,
pub surface: ColorSet,
pub swatch: Swatch,
}
impl ColorUnit {
pub fn new(
color: &Oklch,
neutral_swatch: &Arc<NeutralSwatch>,
foreground_lightness: f32,
settings: &Arc<SchemeSetting>,
) -> Self {
let root = ColorSet::new(color, neutral_swatch, foreground_lightness, settings);
let surface = root.generate_surface_set();
let swatch = root.generate_swatch();
Self {
root,
surface,
swatch,
}
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut css_variables = Vec::new();
css_variables.extend(self.root.to_css_variables(prefix, name));
css_variables.extend(self.surface.to_css_variables(prefix, name));
css_variables.extend(self.swatch.to_css_variables(prefix, name));
css_variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut css_auto_scheme_collection = LinkedHashMap::new();
css_auto_scheme_collection.extend(self.root.to_css_auto_scheme_collection(name));
css_auto_scheme_collection.extend(self.surface.to_css_auto_scheme_collection(name));
css_auto_scheme_collection.extend(self.swatch.to_css_auto_scheme_collection(name));
css_auto_scheme_collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut scss_variables = Vec::new();
scss_variables.extend(self.root.to_scss_variables(prefix, name));
scss_variables.extend(self.surface.to_scss_variables(prefix, name));
scss_variables.extend(self.swatch.to_scss_variables(prefix, name));
scss_variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut js_object_fields = Vec::new();
js_object_fields.extend(self.root.to_javascript_fields(name));
js_object_fields.extend(self.surface.to_javascript_fields(name));
js_object_fields.extend(self.swatch.to_javascript_fields(name));
js_object_fields
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Baseline {
pub primary: ColorUnit,
pub secondary: Option<ColorUnit>,
pub tertiary: Option<ColorUnit>,
pub accent: Option<ColorUnit>,
pub neutral: ColorSet,
pub neutral_variant: ColorSet,
pub surface: ColorSet,
pub surface_variant: ColorSet,
#[serde(serialize_with = "crate::schemes::q_style_2::swatch::serialize_neutral_swatch")]
pub neutral_swatch: Arc<NeutralSwatch>,
pub danger: ColorUnit,
pub success: ColorUnit,
pub warn: ColorUnit,
pub info: ColorUnit,
pub custom_colors: HashMap<String, ColorUnit>,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub shadow: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub overlay: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub outline: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub outline_variant: Oklch,
#[serde(skip)]
pub neutral_lightness: f32,
#[serde(skip)]
pub scheme_settings: Arc<SchemeSetting>,
#[serde(skip)]
pub is_dark: bool,
}
impl Baseline {
pub fn new(
primary: &Oklch,
secondary: Option<&Oklch>,
tertiary: Option<&Oklch>,
accent: Option<&Oklch>,
danger: &Oklch,
success: &Oklch,
warn: &Oklch,
info: &Oklch,
neutral_lightest: &Oklch,
neutral_darkest: &Oklch,
settings: &Arc<SchemeSetting>,
is_dark: bool,
) -> Self {
let (final_secondary, final_tertiary, final_accent) = match settings.expand_method {
ColorExpand::Complementary => {
let sec_color = secondary.cloned().or(Some(primary.complementary()));
(sec_color, tertiary.cloned(), accent.cloned())
}
ColorExpand::Analogous => {
let analogous_color = primary.analogous();
(
secondary.cloned().or(Some(analogous_color.0)),
tertiary.cloned().or(Some(analogous_color.1)),
accent.cloned(),
)
}
ColorExpand::AnalogousAndComplementary => {
let analogous_color = primary.analogous();
let complementary_color = primary.complementary();
(
secondary.cloned().or(Some(analogous_color.0)),
tertiary.cloned().or(Some(analogous_color.1)),
accent.cloned().or(Some(complementary_color)),
)
}
ColorExpand::Triadic => {
let triadic_color = primary.triadic();
(
secondary.cloned().or(Some(triadic_color.0)),
tertiary.cloned().or(Some(triadic_color.1)),
accent.cloned(),
)
}
ColorExpand::SplitComplementary => {
let split_complementary_color = primary.split_complementary();
(
secondary.cloned().or(Some(split_complementary_color.0)),
tertiary.cloned(),
accent.cloned().or(Some(split_complementary_color.1)),
)
}
ColorExpand::Tetradic => {
let tetradic_color = primary.tetradic();
(
secondary.cloned().or(Some(tetradic_color.0)),
tertiary.cloned().or(Some(tetradic_color.2)),
accent.cloned().or(Some(tetradic_color.1)),
)
}
ColorExpand::Square => {
let c_90 = primary.shift_hue(90.0);
let complementary_color = primary.complementary();
let c_270 = primary.shift_hue(270.0);
(
secondary.cloned().or(Some(c_90)),
tertiary.cloned().or(Some(c_270)),
accent.cloned().or(Some(complementary_color)),
)
}
};
let reference_lightness = if is_dark {
neutral_darkest.l
} else {
neutral_lightest.l
};
let neutral_swatch = Arc::new(NeutralSwatch::new(*neutral_lightest, *neutral_darkest));
let outline_color =
neutral_swatch.get(neutral_lightest.l * if is_dark { 0.5 } else { 0.7 });
let outline_variant_color =
neutral_swatch.get(neutral_lightest.l * if is_dark { 0.3 } else { 0.8 });
let shadow_color = neutral_swatch.get(0.1);
let overlay_color =
neutral_swatch.get(neutral_lightest.l * if is_dark { 0.4 } else { 0.5 });
let neutral_color = neutral_swatch.get(if is_dark { 0.35 } else { 0.65 });
let neutral_variant_color = neutral_swatch.get(if is_dark { 0.45 } else { 0.55 });
let surface_color = neutral_swatch.get(if is_dark { 0.10 } else { 0.98 });
let surface_variant_color = neutral_swatch.get(if is_dark { 0.20 } else { 0.85 });
let neutral_set = ColorSet::new(
&neutral_color,
&neutral_swatch,
reference_lightness,
settings,
);
let neutral_variant_set = ColorSet::new(
&neutral_variant_color,
&neutral_swatch,
reference_lightness,
settings,
);
let surface_set = ColorSet::new(
&surface_color,
&neutral_swatch,
reference_lightness,
settings,
);
let surface_variant_set = ColorSet::new(
&surface_variant_color,
&neutral_swatch,
reference_lightness,
settings,
);
let primary_unit = ColorUnit::new(primary, &neutral_swatch, reference_lightness, settings);
let secondary_unit = final_secondary
.map(|color| ColorUnit::new(&color, &neutral_swatch, reference_lightness, settings));
let tertiary_unit = final_tertiary
.map(|color| ColorUnit::new(&color, &neutral_swatch, reference_lightness, settings));
let accent_unit = final_accent
.map(|color| ColorUnit::new(&color, &neutral_swatch, reference_lightness, settings));
let danger_unit = ColorUnit::new(danger, &neutral_swatch, reference_lightness, settings);
let success_unit = ColorUnit::new(success, &neutral_swatch, reference_lightness, settings);
let warn_unit = ColorUnit::new(warn, &neutral_swatch, reference_lightness, settings);
let info_unit = ColorUnit::new(info, &neutral_swatch, reference_lightness, settings);
Self {
primary: primary_unit,
secondary: secondary_unit,
tertiary: tertiary_unit,
accent: accent_unit,
neutral: neutral_set,
neutral_variant: neutral_variant_set,
surface: surface_set,
surface_variant: surface_variant_set,
neutral_swatch,
danger: danger_unit,
success: success_unit,
warn: warn_unit,
info: info_unit,
custom_colors: HashMap::new(),
shadow: shadow_color,
overlay: overlay_color,
outline: outline_color,
outline_variant: outline_variant_color,
neutral_lightness: reference_lightness,
scheme_settings: settings.clone(),
is_dark,
}
}
pub fn add_custom_color(
&mut self,
name: &str,
color: &Oklch,
) -> Result<(), errors::ColorError> {
let custom_color = ColorUnit::new(
color,
&self.neutral_swatch,
self.neutral_lightness,
&self.scheme_settings,
);
self.custom_colors.insert(name.to_string(), custom_color);
Ok(())
}
pub fn to_css_variables(&self) -> Vec<String> {
let mut css_variables = Vec::new();
let scheme_mode = if self.is_dark { "dark" } else { "light" };
css_variables.extend(self.primary.to_css_variables(scheme_mode, "primary"));
if let Some(secondary) = &self.secondary {
css_variables.extend(secondary.to_css_variables(scheme_mode, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
css_variables.extend(tertiary.to_css_variables(scheme_mode, "tertiary"));
}
if let Some(accent) = &self.accent {
css_variables.extend(accent.to_css_variables(scheme_mode, "accent"));
}
css_variables.extend(self.neutral.to_css_variables(scheme_mode, "neutral"));
css_variables.extend(
self.neutral_variant
.to_css_variables(scheme_mode, "neutral-variant"),
);
css_variables.extend(self.surface.to_css_variables(scheme_mode, "surface"));
css_variables.extend(
self.surface_variant
.to_css_variables(scheme_mode, "surface-variant"),
);
css_variables.extend(self.danger.to_css_variables(scheme_mode, "danger"));
css_variables.extend(self.success.to_css_variables(scheme_mode, "success"));
css_variables.extend(self.warn.to_css_variables(scheme_mode, "warn"));
css_variables.extend(self.info.to_css_variables(scheme_mode, "info"));
css_variables.push(format!(
"--color-{scheme_mode}-shadow: #{};",
map_oklch_to_srgb_hex(&self.shadow)
));
css_variables.push(format!(
"--color-{scheme_mode}-overlay: #{};",
map_oklch_to_srgb_hex(&self.overlay)
));
css_variables.push(format!(
"--color-{scheme_mode}-outlint: #{};",
map_oklch_to_srgb_hex(&self.outline)
));
css_variables.push(format!(
"--color-{scheme_mode}-outline-variant: #{};",
map_oklch_to_srgb_hex(&self.outline_variant)
));
for (name, color_unit) in &self.custom_colors {
css_variables.extend(color_unit.to_css_variables(scheme_mode, name));
}
css_variables
}
pub fn to_css_auto_scheme_collection(&self) -> LinkedHashMap<String, String> {
let mut css_variables = LinkedHashMap::new();
css_variables.extend(self.primary.to_css_auto_scheme_collection("primary"));
if let Some(secondary) = &self.secondary {
css_variables.extend(secondary.to_css_auto_scheme_collection("secondary"));
}
if let Some(tertiary) = &self.tertiary {
css_variables.extend(tertiary.to_css_auto_scheme_collection("tertiary"));
}
if let Some(accent) = &self.accent {
css_variables.extend(accent.to_css_auto_scheme_collection("accent"));
}
css_variables.extend(self.neutral.to_css_auto_scheme_collection("neutral"));
css_variables.extend(
self.neutral_variant
.to_css_auto_scheme_collection("neutral-variant"),
);
css_variables.extend(self.surface.to_css_auto_scheme_collection("surface"));
css_variables.extend(
self.surface_variant
.to_css_auto_scheme_collection("surface-variant"),
);
css_variables.insert("shadow".to_string(), map_oklch_to_srgb_hex(&self.shadow));
css_variables.insert("overlay".to_string(), map_oklch_to_srgb_hex(&self.overlay));
css_variables.insert("outline".to_string(), map_oklch_to_srgb_hex(&self.outline));
css_variables.insert(
"outline-variant".to_string(),
map_oklch_to_srgb_hex(&self.outline_variant),
);
for (name, color) in &self.custom_colors {
css_variables.extend(color.to_css_auto_scheme_collection(name));
}
css_variables
}
pub fn to_scss_variables(&self) -> Vec<String> {
let mut scss_variables = Vec::new();
let scheme_mode = if self.is_dark { "dark" } else { "light" };
scss_variables.extend(self.primary.to_scss_variables(scheme_mode, "primary"));
if let Some(secondary) = &self.secondary {
scss_variables.extend(secondary.to_scss_variables(scheme_mode, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
scss_variables.extend(tertiary.to_scss_variables(scheme_mode, "tertiary"));
}
if let Some(accent) = &self.accent {
scss_variables.extend(accent.to_scss_variables(scheme_mode, "accent"));
}
scss_variables.extend(self.neutral.to_scss_variables(scheme_mode, "neutral"));
scss_variables.extend(
self.neutral_variant
.to_scss_variables(scheme_mode, "neutral-variant"),
);
scss_variables.extend(self.surface.to_scss_variables(scheme_mode, "surface"));
scss_variables.extend(
self.surface_variant
.to_scss_variables(scheme_mode, "surface-variant"),
);
scss_variables.push(format!(
"--color-{scheme_mode}-shadow: #{};",
map_oklch_to_srgb_hex(&self.shadow)
));
scss_variables.push(format!(
"--color-{scheme_mode}-overlay: #{};",
map_oklch_to_srgb_hex(&self.overlay)
));
scss_variables.push(format!(
"--color-{scheme_mode}-outlint: #{};",
map_oklch_to_srgb_hex(&self.outline)
));
scss_variables.push(format!(
"--color-{scheme_mode}-outline-variant: #{};",
map_oklch_to_srgb_hex(&self.outline_variant)
));
scss_variables
}
pub fn to_javascript_fields(&self) -> Vec<String> {
let mut javascript_fields = Vec::new();
let scheme_mode = if self.is_dark { "dark" } else { "light" };
javascript_fields.push(format!("{scheme_mode}: {{"));
let indent = " ".repeat(4);
for line in self.primary.to_javascript_fields("primary").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.secondary
.as_ref()
.map(|s| s.to_javascript_fields("secondary"))
.unwrap_or(Vec::new())
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.tertiary
.as_ref()
.map(|s| s.to_javascript_fields("tertiary"))
.unwrap_or(Vec::new())
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.accent
.as_ref()
.map(|s| s.to_javascript_fields("accent"))
.unwrap_or(Vec::new())
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.neutral.to_javascript_fields("neutral").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.neutral_variant
.to_javascript_fields("neutral_variant")
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.surface.to_javascript_fields("surface").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.surface_variant
.to_javascript_fields("surface_variant")
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
javascript_fields.push(format!(
"{indent}shadow: '#{}',",
map_oklch_to_srgb_hex(&self.shadow)
));
javascript_fields.push(format!(
"{indent}overlay: '#{}',",
map_oklch_to_srgb_hex(&self.overlay)
));
javascript_fields.push(format!(
"{indent}outline: '#{}',",
map_oklch_to_srgb_hex(&self.outline)
));
javascript_fields.push(format!(
"{indent}outlineVariant: '#{}',",
map_oklch_to_srgb_hex(&self.outline_variant)
));
for (name, color) in &self.custom_colors {
let color_lines = color.to_javascript_fields(name);
javascript_fields.extend(color_lines.iter().map(|s| format!("{indent}{s}")));
}
javascript_fields.push("}".to_string());
javascript_fields
}
}

View File

@ -0,0 +1,282 @@
use core::f32;
use std::sync::Arc;
use linked_hash_map::LinkedHashMap;
use palette::{Oklch, color_difference::Wcag21RelativeContrast, luma::Luma};
use serde::Serialize;
use crate::{
convert::{map_oklch_to_luma, map_oklch_to_srgb_hex},
schemes::{
q_style::{NeutralSwatch, SchemeSetting, WACGSetting},
q_style_2::swatch::Swatch,
},
};
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorSet {
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub root: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub active: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub focus: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub hover: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub disabled: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub on_root: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub on_disabled: Oklch,
#[serde(skip)]
pub neutral_swatch: Arc<NeutralSwatch>,
#[serde(skip)]
pub neutral_lightness: f32,
#[serde(skip)]
pub scheme_settings: Arc<SchemeSetting>,
}
#[inline]
fn match_wacg(original: &Oklch<f32>, reference: &Luma) -> f32 {
let luma_original = map_oklch_to_luma(original);
luma_original.relative_contrast(*reference)
}
fn search_for_common_wacg_color(
reference_colors: &[&Oklch],
neutral_swatch: &NeutralSwatch,
minium_ratio: f32,
) -> Oklch {
// store in: (lightness, avg_wacg_abs, sum_wacg_abs)
let mut minium_match: (f32, f32, f32) = (0.0, f32::INFINITY, 0.0);
let mut closest_match: (f32, f32, f32) = (f32::INFINITY, f32::INFINITY, 0.0);
for scan_lightness in (0..=100).map(|x| x as f32 / 100.0) {
let new_target = neutral_swatch.get(scan_lightness);
let new_target_luma = map_oklch_to_luma(&new_target);
let reference_wacgs_sum: f32 = reference_colors
.iter()
.map(|ref_color| match_wacg(&ref_color, &new_target_luma) - minium_ratio)
.sum();
let reference_wacgs = reference_wacgs_sum / reference_colors.len() as f32;
if reference_wacgs.abs() < closest_match.1.abs() && reference_wacgs_sum > closest_match.2 {
closest_match = (scan_lightness, reference_wacgs, reference_wacgs_sum);
}
if reference_wacgs >= 0.0
&& reference_wacgs.abs() < minium_match.1.abs()
&& reference_wacgs_sum > minium_match.2
{
minium_match = (scan_lightness, reference_wacgs, reference_wacgs_sum);
}
}
if minium_match.1 != f32::INFINITY {
neutral_swatch.get(minium_match.0)
} else {
neutral_swatch.get(closest_match.0)
}
}
impl ColorSet {
pub fn new(
color: &Oklch,
neutral_swatch: &Arc<NeutralSwatch>,
neutral_lightness: f32,
settings: &Arc<SchemeSetting>,
) -> Self {
let neutral_swatch = Arc::clone(neutral_swatch);
let settings = Arc::clone(settings);
let root = color.clone();
let hover = color * settings.hover;
let active = color * settings.active;
let focus = color * settings.focus;
let disabled = color * settings.disabled;
let color_list = &[&root, &hover, &active, &focus];
let (on_root, on_disabled) = match settings.wacg_follows {
WACGSetting::Fixed => (
neutral_swatch.get(neutral_lightness),
neutral_swatch.get(neutral_lightness),
),
WACGSetting::AutomaticAA => (
search_for_common_wacg_color(color_list, &neutral_swatch, 4.5),
search_for_common_wacg_color(&[&disabled], &neutral_swatch, 4.5),
),
WACGSetting::AutomaticAAA => (
search_for_common_wacg_color(color_list, &neutral_swatch, 7.0),
search_for_common_wacg_color(&[&disabled], &neutral_swatch, 7.0),
),
WACGSetting::HighContrast => (
search_for_common_wacg_color(color_list, &neutral_swatch, 21.0),
search_for_common_wacg_color(&[&disabled], &neutral_swatch, 21.0),
),
};
Self {
root,
active,
focus,
hover,
disabled,
on_root,
on_disabled,
neutral_swatch,
neutral_lightness,
scheme_settings: settings,
}
}
pub fn generate_surface_set(&self) -> Self {
let root_swatch = Swatch::new(&self.root);
let root_lightness = self.root.l;
let surface_lightness = if root_lightness + 40.0 > 90.0 {
root_lightness - 40.0
} else {
root_lightness + 40.0
};
let surface_color = root_swatch.get(surface_lightness);
Self::new(
&surface_color,
&self.neutral_swatch,
self.neutral_lightness,
&self.scheme_settings,
)
}
pub fn generate_swatch(&self) -> Swatch {
Swatch::new(&self.root)
}
fn root_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.root)
}
fn hover_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.hover)
}
fn active_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.active)
}
fn focus_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.focus)
}
fn disabled_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.disabled)
}
fn on_root_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.on_root)
}
fn on_disabled_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.on_disabled)
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.push(format!("--color-{prefix}-{name}: #{};", self.root_hex()));
variables.push(format!(
"--color-{prefix}-{name}-hover: #{};",
self.hover_hex()
));
variables.push(format!(
"--color-{prefix}-{name}-active: #{};",
self.active_hex()
));
variables.push(format!(
"--color-{prefix}-{name}-focus: #{};",
self.focus_hex()
));
variables.push(format!(
"--color-{prefix}-{name}-disabled: ${};",
self.disabled_hex()
));
variables.push(format!(
"--color-{prefix}-on-{name}: #{};",
self.on_root_hex()
));
variables.push(format!(
"--color-{prefix}-on-{name}-disabled: #{};",
self.on_disabled_hex()
));
variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
collection.insert(format!("{name}"), self.root_hex());
collection.insert(format!("{name}-hover"), self.hover_hex());
collection.insert(format!("{name}-active"), self.active_hex());
collection.insert(format!("{name}-focus"), self.focus_hex());
collection.insert(format!("{name}-disabled"), self.disabled_hex());
collection.insert(format!("on-{name}"), self.on_root_hex());
collection.insert(format!("on-{name}-disabled"), self.on_disabled_hex());
collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.push(format!("&color-{prefix}-{name}: #{};", self.root_hex()));
variables.push(format!(
"$color-{prefix}-{name}-hover: #{};",
self.hover_hex()
));
variables.push(format!(
"$color-{prefix}-{name}-active: #{};",
self.active_hex()
));
variables.push(format!(
"$color-{prefix}-{name}-focus: ${};",
self.focus_hex()
));
variables.push(format!(
"$color-{prefix}-{name}-disabled: #{};",
self.disabled_hex()
));
variables.push(format!(
"$color-{prefix}-on-{name}: #{};",
self.on_root_hex()
));
variables.push(format!(
"$color-{prefix}-on-{name}-disabled: #{};",
self.on_disabled_hex()
));
variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut variables = Vec::new();
let capitalized_name = name
.chars()
.next()
.unwrap()
.to_ascii_uppercase()
.to_string()
+ &name[1..];
variables.push(format!("{name}: '#{}',", self.root_hex()));
variables.push(format!("{name}Hover: '#{}',", self.hover_hex()));
variables.push(format!("{name}Active: '#{}',", self.active_hex()));
variables.push(format!("{name}Focus: '#{}',", self.focus_hex()));
variables.push(format!("{name}Disabled: '#{}',", self.disabled_hex()));
variables.push(format!("on{capitalized_name}: '#{}',", self.on_root_hex()));
variables.push(format!(
"on{capitalized_name}Disabled: '#{}',",
self.on_disabled_hex()
));
variables
}
}

View File

@ -0,0 +1,157 @@
use std::{str::FromStr, sync::Arc};
use linked_hash_set::LinkedHashSet;
use palette::FromColor;
use serde::Serialize;
use crate::{
errors, parse_option_to_oklch, parse_to_oklch,
schemes::{SchemeExport, q_style::SchemeSetting, q_style_2::baseline::Baseline},
};
mod baseline;
mod color_set;
mod swatch;
#[derive(Debug, Clone, Serialize)]
pub struct QScheme2 {
pub light: Baseline,
pub dark: Baseline,
#[serde(skip)]
_settings: Arc<SchemeSetting>,
}
impl QScheme2 {
pub fn new(
primary: &str,
secondary: Option<&str>,
tertiary: Option<&str>,
accent: Option<&str>,
danger: &str,
success: &str,
warn: &str,
info: &str,
foreground: &str,
background: &str,
setting: SchemeSetting,
) -> Result<Self, errors::ColorError> {
let primary = parse_to_oklch!(primary);
let secondary = parse_option_to_oklch!(secondary);
let tertiary = parse_option_to_oklch!(tertiary);
let accent = parse_option_to_oklch!(accent);
let danger = parse_to_oklch!(danger);
let success = parse_to_oklch!(success);
let warn = parse_to_oklch!(warn);
let info = parse_to_oklch!(info);
let foreground = parse_to_oklch!(foreground);
let background = parse_to_oklch!(background);
let settings = Arc::new(setting);
let light_scheme = Baseline::new(
&primary,
secondary.as_ref(),
tertiary.as_ref(),
accent.as_ref(),
&danger,
&success,
&warn,
&info,
&foreground,
&background,
&settings,
false,
);
let dark_scheme = Baseline::new(
&(&primary * settings.dark_convert),
secondary.map(|c| c * settings.dark_convert).as_ref(),
tertiary.map(|c| c * settings.dark_convert).as_ref(),
accent.map(|c| c * settings.dark_convert).as_ref(),
&(danger * settings.dark_convert),
&(success * settings.dark_convert),
&(warn * settings.dark_convert),
&(info * settings.dark_convert),
&foreground,
&background,
&settings,
true,
);
Ok(Self {
light: light_scheme,
dark: dark_scheme,
_settings: settings,
})
}
pub fn add_custom_color(&mut self, name: &str, color: &str) -> Result<(), errors::ColorError> {
let custom_color = parse_to_oklch!(color);
self.light.add_custom_color(name, &custom_color)?;
self.dark
.add_custom_color(name, &(custom_color * self._settings.dark_convert))?;
Ok(())
}
}
impl SchemeExport for QScheme2 {
fn output_css_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_css_variables());
variables.extend(self.dark.to_css_variables());
variables.join("\n")
}
fn output_css_auto_scheme_variables(&self) -> String {
let mut collection = Vec::new();
let mut keys = LinkedHashSet::new();
let light_collection = self.light.to_css_auto_scheme_collection();
let dark_collection = self.dark.to_css_auto_scheme_collection();
keys.extend(light_collection.keys().cloned());
keys.extend(dark_collection.keys().cloned());
for key in keys {
match (light_collection.get(&key), dark_collection.get(&key)) {
(Some(light), Some(dark)) => {
collection.push(format!("--color-{key}: light-dark(#{light}, #{dark});"));
}
(Some(color), None) | (None, Some(color)) => {
collection.push(format!("--color-{key}: #{color}"));
}
(None, None) => {}
}
}
collection.join("\n")
}
fn output_scss_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_scss_variables());
variables.extend(self.dark.to_scss_variables());
variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut javascript_object = Vec::new();
let indent = " ".repeat(4);
javascript_object.push("{".to_string());
for line in self.light.to_javascript_fields() {
javascript_object.push(format!("{indent}{line}"));
}
for line in self.dark.to_javascript_fields() {
javascript_object.push(format!("{indent}{line}"));
}
javascript_object.push("}".to_string());
javascript_object.join("\n")
}
}

View File

@ -0,0 +1,119 @@
use std::sync::Arc;
use linked_hash_map::LinkedHashMap;
use palette::Oklch;
use serde::{Serialize, Serializer, ser::SerializeStruct};
use crate::{
convert::map_oklch_to_srgb_hex,
schemes::{get_static_str, q_style::NeutralSwatch},
};
static SWATCH_LIGHTINGS: [u8; 16] = [
10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 75, 80, 85, 90, 95, 98,
];
#[derive(Debug, Clone)]
pub struct Swatch(Oklch);
impl Swatch {
pub fn new(color: &Oklch) -> Self {
Self(color.clone())
}
pub fn get<L: Into<f32>>(&self, lightness: L) -> Oklch {
let request_lightness: f32 = lightness.into() / 100.0;
Oklch {
l: request_lightness.clamp(0.1, 0.98),
..self.0.clone()
}
}
pub fn get_hex<L: Into<f32>>(&self, lightness: L) -> String {
let c = self.get(lightness.into());
map_oklch_to_srgb_hex(&c)
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for l in SWATCH_LIGHTINGS {
variables.push(format!(
"--color-{prefix}-swatch-{name}-{l:02}: #{};",
self.get_hex(l)
));
}
variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
for l in SWATCH_LIGHTINGS {
collection.insert(format!("{name}-{l:02}"), self.get_hex(l));
}
collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for l in SWATCH_LIGHTINGS {
variables.push(format!(
"$color-{prefix}-swatch-{name}-{l:02}: #{};",
self.get_hex(l)
));
}
variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for l in SWATCH_LIGHTINGS {
variables.push(format!("{name}{l:02}: '#{}',", self.get_hex(l)));
}
variables
}
}
impl Serialize for Swatch {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("Swatch", SWATCH_LIGHTINGS.len())?;
for l in SWATCH_LIGHTINGS {
let color = self.get_hex(l);
let key: &'static str = get_static_str(&format!("{l:02}"));
state.serialize_field(key, &color)?;
}
state.end()
}
}
pub fn serialize_neutral_swatch<S>(
swatch: &Arc<NeutralSwatch>,
serailizer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let swatch = swatch.clone();
let mut swatch_struct = serailizer.serialize_struct("NeutralSwatch", SWATCH_LIGHTINGS.len())?;
for l in SWATCH_LIGHTINGS {
let color = swatch.get((l as f32) / 100.0);
let color = map_oklch_to_srgb_hex(&color);
let key: &'static str = get_static_str(&format!("{l:02}"));
swatch_struct.serialize_field(key, &color)?;
}
swatch_struct.end()
}

View File

@ -10,7 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@iconify/react": "^5.1.0",
"@iconify/react": "^6.0.0",
"clsx": "^2.1.1",
"color-module": "./color_functions",
"dayjs": "^1.11.13",
@ -18,7 +18,7 @@
"lodash-es": "^4.17.21",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-error-boundary": "^5.0.0",
"react-error-boundary": "^6.0.0",
"react-router-dom": "^7.1.1",
"react-transition-group": "^4.4.5",
"react-use": "^17.6.0",
@ -36,10 +36,10 @@
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"globals": "^16.3.0",
"lightningcss": "^1.28.2",
"typescript": "~5.6.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
"vite": "^7.0.4"
}
}
}

View File

@ -16,6 +16,7 @@ import {
MaterialDesign3DynamicSchemeSource,
MaterialDesign3SchemeSource,
} from '../material-3-scheme';
import { Q2SchemeSource } from '../q-2-scheme';
import { QSchemeSource } from '../q-scheme';
import { currentPickedColor } from '../stores/colors';
import { activeSchemeAtom, useActiveScheme, useUpdateScheme } from '../stores/schemes';
@ -101,6 +102,79 @@ const QSchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
);
};
const Q2SchemeMenu: FC<ContextMenuBodyProps> = ({ color, afterClick }) => {
const { showToast } = useNotification();
const activeSchemeId = useAtomValue(activeSchemeAtom);
const updateScheme = useUpdateScheme(activeSchemeId);
const updateSchemeContent = useCallback(
(content: keyof QSchemeSource) => {
updateScheme((prev) => {
prev.schemeStorage.source[content] = color;
return prev;
});
showToast(
NotificationType.SUCCESS,
`${capitalize(content)} color in active scheme updated.`,
'tabler:settings-up',
3000,
);
afterClick?.();
},
[color, activeSchemeId, updateScheme],
);
const addCustomColor = useCallback(() => {
updateScheme((prev) => {
const source = prev.schemeStorage.source as Q2SchemeSource;
const colorAmount = size(source.custom_colors);
source.custom_colors[`Custom Color ${colorAmount + 1}`] = color;
return prev;
});
showToast(NotificationType.SUCCESS, `New color entry added.`, 'tabler:settings-up', 3000);
afterClick?.();
}, [color, activeSchemeId, updateScheme]);
return (
<>
<hr />
<div className={styles.menu_item} onClick={() => updateSchemeContent('primary')}>
Set as Primary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('secondary')}>
Set as Secondary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('tertiary')}>
Set as Tertiary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('accent')}>
Set as Accent color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('danger')}>
Set as Danger color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('success')}>
Set as Success color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('warning')}>
Set as Warn color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('info')}>
Set as Info color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('foreground')}>
Set as Foreground color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('background')}>
Set as Background color
</div>
<div className={styles.menu_item} onClick={addCustomColor}>
Add to Custom colors
</div>
</>
);
};
const SwatchSchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
const { showToast } = useNotification();
const activeSchemeId = useAtomValue(activeSchemeAtom);
@ -253,6 +327,8 @@ export const ContextMenuBody: FC<ContextMenuBodyProps> = ({ color, afterClick, x
switch (activeScheme?.type) {
case 'q_scheme':
return <QSchemeMenu {...sharedProps} />;
case 'q_2_scheme':
return <Q2SchemeMenu {...sharedProps} />;
case 'swatch_scheme':
return <SwatchSchemeMenu {...sharedProps} />;
case 'material_2':

View File

@ -9,6 +9,9 @@
&.swatch {
background-color: var(--color-pinlan);
}
&.q2 {
background-color: var(--color-jugengzi);
}
&.m2 {
background-color: #03dac6;
color: var(--color-qihei);

View File

@ -16,6 +16,8 @@ export function SchemeSign({ scheme, short = false }: SchemeSignProps) {
switch (scheme) {
case 'q_scheme':
return styles.q;
case 'q_2_scheme':
return styles.q2;
case 'swatch_scheme':
return styles.swatch;
case 'material_2':

View File

@ -4,6 +4,7 @@ import {
MaterialDesign3DynamicSchemeStorage,
MaterialDesign3SchemeStorage,
} from './material-3-scheme';
import { Q2SchemeStorage } from './q-2-scheme';
import { QSchemeStorage } from './q-scheme';
import { SwatchSchemeStorage } from './swatch_scheme';
@ -34,6 +35,7 @@ export type ColorDescription = {
export type SchemeType =
| 'q_scheme'
| 'q_2_scheme'
| 'swatch_scheme'
| 'material_2'
| 'material_3'
@ -45,6 +47,7 @@ export type SchemeTypeOption = {
};
export const SchemeTypeOptions: SchemeTypeOption[] = [
{ label: 'Q Scheme', short: 'Q', value: 'q_scheme' },
{ label: 'Q2 Scheme', short: 'Q2', value: 'q_2_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' },
@ -79,6 +82,7 @@ export type ColorShifting = {
export type SchemeStorage =
| QSchemeStorage
| Q2SchemeStorage
| SwatchSchemeStorage
| MaterialDesign2SchemeStorage
| MaterialDesign3SchemeStorage

View File

@ -0,0 +1,44 @@
import { isEqual, isNil } from 'lodash-es';
import { useState } from 'react';
import { Tab } from '../../components/Tab';
import { SchemeContent } from '../../models';
import { Q2SchemeStorage } from '../../q-2-scheme';
import { isNilOrEmpty } from '../../utls';
import { SchemeExport } from './Export';
import { Q2SchemeBuilder } from './q-2-scheme/Builder';
import Q2SchemePreview from './q-2-scheme/Preview';
const tabOptions = [
{ title: 'Overview', id: 'overview' },
{ title: 'Builder', id: 'builder' },
{ title: 'Exports', id: 'export' },
];
type Q2SchemeProps = {
scheme: SchemeContent<Q2SchemeStorage>;
};
export function Q2Scheme({ scheme }: Q2SchemeProps) {
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)}
disabled={{
overview: isNilOrEmpty(scheme.schemeStorage?.scheme),
export: isNilOrEmpty(scheme.schemeStorage?.cssVariables),
}}
/>
{isEqual(activeTab, 'overview') && <Q2SchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && (
<Q2SchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
)}
{isEqual(activeTab, 'export') && <SchemeExport scheme={scheme} />}
</>
);
}

View File

@ -0,0 +1,44 @@
@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;
}
.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;
}
.button_row {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-s);
}
h5 {
font-size: var(--font-size-m);
line-height: 1.7em;
}
}
}

View File

@ -0,0 +1,532 @@
import { ColorExpand, ColorShifting, SchemeSetting, WACGSetting } from 'color-module';
import { includes, isEmpty, isNil } from 'lodash-es';
import { useActionState, useCallback, useMemo, useState } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { FloatColorPicker } from '../../../components/FloatColorPicker';
import { NotificationType, useNotification } from '../../../components/Notifications';
import { ScrollArea } from '../../../components/ScrollArea';
import { VSegmentedControl } from '../../../components/VSegmentedControl';
import { SchemeContent } from '../../../models';
import { Q2SchemeSource, Q2SchemeStorage } from '../../../q-2-scheme';
import { useUpdateScheme } from '../../../stores/schemes';
import { isNilOrEmpty, mapToObject } from '../../../utls';
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
import styles from './Builder.module.css';
type Q2SchemeBuilderProps = {
scheme: SchemeContent<Q2SchemeStorage>;
onBuildCompleted?: () => void;
};
export function Q2SchemeBuilder({ scheme, onBuildCompleted }: Q2SchemeBuilderProps) {
const { showToast } = useNotification();
const { colorFn } = useColorFunction();
const updateScheme = useUpdateScheme(scheme.id);
// Load scheme setting and scheme default setting.
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('[Q2 Scheme builder]', e);
}
}, [scheme]);
// Collect choices in color scheme settings
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 [];
}, []);
// Custom Colors processing
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],
);
// Collect scheme source
const collectSchemeSource = (formData: FormData): [Q2SchemeSource, QSchemeSetting] => {
const primaryColor = formData.get('primary')?.toString();
const secondaryColor = formData.get('secondary')?.toString();
const tertiaryColor = formData.get('tertiary')?.toString();
const accentColor = formData.get('accent')?.toString();
const dangerColor = formData.get('danger')?.toString();
const successColor = formData.get('success')?.toString();
const warnColor = formData.get('warn')?.toString();
const infoColor = formData.get('info')?.toString();
const foregroundColor = formData.get('foreground')?.toString();
const backgroundColor = formData.get('background')?.toString();
const customColors: Record<string, string> = {};
for (const key of colorKeys) {
const name = formData.get(`name_${key}`)?.toString();
const color = formData.get(`color_${key}`)?.toString();
if (isNil(name) || isEmpty(name) || isNil(color) || isEmpty(color)) continue;
customColors[name] = color;
}
// collect scheme settings
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;
return [
{
primary: primaryColor,
secondary: secondaryColor,
tertiary: tertiaryColor,
accent: accentColor,
danger: dangerColor,
success: successColor,
warn: warnColor,
info: infoColor,
foreground: foregroundColor,
background: backgroundColor,
custom_colors: customColors,
setting: dumpedSetting,
} as Q2SchemeSource,
schemeSetting,
];
};
// Scheme save actions
const handleDraftAction = (formData: FormData) => {
const [source] = collectSchemeSource(formData);
updateScheme((prev) => {
prev.schemeStorage.source = source;
return prev;
});
showToast(NotificationType.SUCCESS, 'Scheme draft saved!', 'tabler:device-floppy', 3000);
};
const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
(_state, formData) => {
const errMsg = new Map<string, string>();
// Check required color
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 [source, settings] = collectSchemeSource(formData);
console.log('[Collected form data]', source, settings);
const generatedScheme = colorFn?.generate_q_scheme_2_manually(
source.primary ?? '',
isEmpty(source.secondary) ? undefined : source.secondary,
isEmpty(source.tertiary) ? undefined : source.tertiary,
isEmpty(source.accent) ? undefined : source.accent,
source.danger ?? '',
source.success ?? '',
source.warn ?? '',
source.info ?? '',
source.foreground ?? '',
source.background ?? '',
source.custom_colors,
settings,
);
console.log('[Generated scheme]', generatedScheme);
updateScheme((prev) => {
prev.schemeStorage.source = source;
prev.schemeStorage.scheme = {
light: {
...generatedScheme[0].light,
customColors: mapToObject(generatedScheme[0].light.customColors),
},
dark: {
...generatedScheme[0].dark,
customColors: mapToObject(generatedScheme[0].dark.customColors),
},
};
prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.cssAutoSchemeVariables = generatedScheme[2];
prev.schemeStorage.scssVariables = generatedScheme[3];
prev.schemeStorage.jsVariables = generatedScheme[4];
return prev;
});
onBuildCompleted?.();
} catch (e) {
console.error('[build q2 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={
isNilOrEmpty(scheme.schemeStorage.source?.primary)
? undefined
: 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={
isNilOrEmpty(scheme.schemeStorage.source?.secondary)
? undefined
: scheme.schemeStorage.source?.secondary
}
/>
</div>
<label className={styles.label}>Tertiary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="tertiary"
color={
isNilOrEmpty(scheme.schemeStorage.source?.tertiary)
? undefined
: scheme.schemeStorage.source?.tertiary
}
/>
</div>
<label className={styles.label}>Accent Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="accent"
color={
isNilOrEmpty(scheme.schemeStorage.source?.accent)
? undefined
: scheme.schemeStorage.source?.accent
}
/>
</div>
<label className={styles.label}>Danger Color*</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="danger"
color={
isNilOrEmpty(scheme.schemeStorage.source?.danger)
? undefined
: 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={
isNilOrEmpty(scheme.schemeStorage.source?.success)
? undefined
: 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={
isNilOrEmpty(scheme.schemeStorage.source?.warn)
? undefined
: scheme.schemeStorage.source?.warn
}
/>
{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={
isNilOrEmpty(scheme.schemeStorage.source?.info)
? undefined
: 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={
isNilOrEmpty(scheme.schemeStorage.source?.foreground)
? undefined
: 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={
isNilOrEmpty(scheme.schemeStorage.source?.background)
? undefined
: scheme.schemeStorage.source?.background
}
/>
{errMsg.has('background') && (
<span className={styles.error_msg}>{errMsg.get('background')}</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])}
/>
))}
<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 className={styles.button_row} style={{ gridColumn: '2 / span 2' }}>
<button type="submit" className="primary">
Build Scheme
</button>
<button type="submit" className="secondary" formAction={handleDraftAction}>
Save Draft
</button>
</div>
</form>
</ScrollArea>
);
}

View File

@ -0,0 +1,62 @@
@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: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xs);
h2 {
font-size: var(--font-size-xl);
font-weight: bold;
line-height: 1.7em;
}
}
.preview_unit {
width: inherit;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: var(--spacing-xs);
}
.preview_indi_block {
width: inherit;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-xs);
}
.preview_swatch {
width: inherit;
display: grid;
grid-template-columns: repeat(16, 1fr);
gap: var(--spacing-xs);
.preview_swatch_cell {
padding: var(--spacing-xs) var(--spacing-s);
.swatch_label {
font-size: var(--font-size-s);
filter: invert(100%);
}
}
}
.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,177 @@
import { capitalize, keys } from 'lodash-es';
import { FC, ReactNode, useMemo } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { ScrollArea } from '../../../components/ScrollArea';
import { SchemeContent } from '../../../models';
import { Q2Baseline, Q2ColorSet, Q2ColorUnit, Q2SchemeStorage } from '../../../q-2-scheme';
import styles from './Preview.module.css';
interface PreviewCellProps {
bg: string;
fg: string;
children: ReactNode;
}
const PreviewCell: FC<PreviewCellProps> = ({ bg, fg, children }) => {
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>
);
};
interface PreviewLineProps {
name: string;
unit: Q2ColorSet;
}
const PreviewLine: FC<PreviewLineProps> = ({ name, unit }) => {
return (
<div className={styles.preview_unit}>
<PreviewCell bg={unit.root} fg={unit.onRoot}>
{name}
</PreviewCell>
<PreviewCell bg={unit.hover} fg={unit.onRoot}>
{name} Hover
</PreviewCell>
<PreviewCell bg={unit.active} fg={unit.onRoot}>
{name} Active
</PreviewCell>
<PreviewCell bg={unit.focus} fg={unit.onRoot}>
{name} Focus
</PreviewCell>
<PreviewCell bg={unit.disabled} fg={unit.onDisabled}>
{name} Disabled
</PreviewCell>
</div>
);
};
interface PreviewSwatchLineProps {
swatch: Record<string, string>;
}
const PreviewSwatchLine: FC<PreviewSwatchLineProps> = ({ swatch }) => {
const cells = useMemo(() => {
const collection: ReactNode[] = [];
for (const key of keys(swatch)) {
const color = swatch[key];
collection.push(
<div
className={styles.preview_swatch_cell}
style={{ backgroundColor: `#${color}` }}
key={key}>
<span className={styles.swatch_label} style={{ color: `#${color}` }}>
{key}
</span>
</div>,
);
}
return collection;
}, [swatch]);
return <div className={styles.preview_swatch}>{cells}</div>;
};
interface PreviewSetProps {
name: string;
colorUnit: Q2ColorUnit;
}
const PreviewSet: FC<PreviewSetProps> = ({ name, colorUnit }) => {
return (
<>
<PreviewLine name={name} unit={colorUnit.root} />
<PreviewLine name={`${name} Surface`} unit={colorUnit.surface} />
<PreviewSwatchLine swatch={colorUnit.swatch} />
</>
);
};
interface PreviewBlockProps {
baseline: Q2Baseline;
title: string;
}
const PreviewBlock: FC<PreviewBlockProps> = ({ baseline, title }) => {
const customSets = useMemo(() => {
const colors = keys(baseline.customColors);
const elements: ReactNode[] = [];
for (const key of colors) {
const color = baseline.customColors[key];
elements.push(<PreviewSet name={capitalize(key)} colorUnit={color} />);
}
return elements;
}, [baseline.customColors]);
return (
<div className={styles.preview_block} style={{ backgroundColor: `#${baseline.surface.root}` }}>
<h2 style={{ color: `#${baseline.surface.onRoot}` }}>{title}</h2>
<PreviewSet name="Primary" colorUnit={baseline.primary} />
<PreviewSet name="Secondary" colorUnit={baseline.secondary} />
<PreviewSet name="Tertiary" colorUnit={baseline.tertiary} />
<PreviewSet name="Accent" colorUnit={baseline.accent} />
<PreviewSet name="Danger" colorUnit={baseline.danger} />
<PreviewSet name="Success" colorUnit={baseline.success} />
<PreviewSet name="Warn" colorUnit={baseline.warn} />
<PreviewSet name="Info" colorUnit={baseline.info} />
<PreviewLine name="Neutral" unit={baseline.neutral} />
<PreviewLine name="Neutral Variant" unit={baseline.neutralVariant} />
<PreviewLine name="Surface" unit={baseline.surface} />
<PreviewLine name="Surface Variant" unit={baseline.surfaceVariant} />
<div className={styles.preview_indi_block}>
<PreviewCell bg={baseline.shadow} fg={baseline.surface.root.onRoot}>
Shadow
</PreviewCell>
<PreviewCell bg={baseline.overlay} fg={baseline.surface.root.onRoot}>
Overlay
</PreviewCell>
<PreviewCell bg={baseline.outline} fg={baseline.surface.root.onRoot}>
Outline
</PreviewCell>
<PreviewCell bg={baseline.outlineVariant} fg={baseline.surface.root.onRoot}>
Outline Variant
</PreviewCell>
</div>
<PreviewSwatchLine swatch={baseline.neutralSwatch} />
{customSets}
</div>
);
};
interface PreviewProps {
scheme: SchemeContent<Q2SchemeStorage>;
}
const Q2SchemePreview: FC<PreviewProps> = ({ scheme }) => {
return (
<ScrollArea enableY>
<div className={styles.preview_layout}>
<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>
</div>
</ScrollArea>
);
};
export default Q2SchemePreview;

View File

@ -15,8 +15,10 @@ import { CorruptedScheme } from '../page-components/scheme/CorruptedScheme';
import { M2Scheme } from '../page-components/scheme/M2Scheme';
import { M3DynamicScheme } from '../page-components/scheme/M3DynamicScheme';
import { M3Scheme } from '../page-components/scheme/M3Scheme';
import { Q2Scheme } from '../page-components/scheme/Q2Scheme';
import { QScheme } from '../page-components/scheme/QScheme';
import { SwatchScheme } from '../page-components/scheme/SwatchScheme';
import { Q2SchemeSource } from '../q-2-scheme';
import { QSchemeStorage } from '../q-scheme';
import { useScheme, useUpdateScheme } from '../stores/schemes';
import { SwatchSchemeStorage } from '../swatch_scheme';
@ -50,6 +52,8 @@ export function SchemeDetail() {
switch (scheme?.type) {
case 'q_scheme':
return <QScheme scheme={scheme as SchemeContent<QSchemeStorage>} />;
case 'q_2_scheme':
return <Q2Scheme scheme={scheme as SchemeContent<Q2SchemeSource>} />;
case 'swatch_scheme':
return <SwatchScheme scheme={scheme as SchemeContent<SwatchSchemeStorage>} />;
case 'material_2':

67
src/q-2-scheme.ts Normal file
View File

@ -0,0 +1,67 @@
import { QSchemeSetting } from './q-scheme';
export type Q2ColorSet = {
root: string;
hover: string;
active: string;
focus: string;
disabled: string;
onRoot: string;
onDisabled: string;
};
export type Q2ColorUnit = {
root: Q2ColorSet;
surface: Q2ColorSet;
swatch: Record<string, string>;
};
export type Q2Baseline = {
primary: Q2ColorUnit;
secondary: Q2ColorUnit | null;
tertiary: Q2ColorUnit | null;
accent: Q2ColorUnit | null;
neutral: Q2ColorUnit;
neutralVariant: Q2ColorUnit;
surface: Q2ColorUnit;
surfaceVariant: Q2ColorUnit;
neutralSwatch: Record<string, string>;
danger: Q2ColorUnit;
success: Q2ColorUnit;
warn: Q2ColorUnit;
info: Q2ColorUnit;
shadow: string;
overlay: string;
outline: string;
outlineVariant: string;
customColors: Record<string, Q2ColorUnit>;
};
export type Q2Scheme = {
light: Q2Baseline;
dark: Q2Baseline;
};
export type Q2SchemeSource = {
primary: string | null;
secondary: string | null;
tertiary: string | null;
accent: string | null;
danger: string | null;
success: string | null;
warn: string | null;
info: string | null;
foreground: string | null;
background: string | null;
custom_colors?: Record<string, string>;
setting: QSchemeSetting | null;
};
export type Q2SchemeStorage = {
source?: Q2SchemeSource;
scheme?: Q2Scheme;
cssVariables?: string;
cssAutoSchemeVariables?: string;
scssVariables?: string;
jsVariables?: string;
};

View File

@ -47,6 +47,7 @@
--color-youlv: hsl(118, 26%, 19%);
--color-jingtailan: hsl(207, 65%, 43%);
--color-yejuzi: hsl(240, 25%, 43%);
--color-jugengzi: hsl(297, 38%, 38%);
/* background colors */
--color-yunshanlv: hsl(146, 25%, 11%);
--color-wumeizi: hsl(305, 22%, 10%);