Compare commits
17 Commits
2bbb46ced1
...
5a1454e6c2
Author | SHA1 | Date | |
---|---|---|---|
|
5a1454e6c2 | ||
|
dd1273dad4 | ||
|
afaa7d25de | ||
|
edc2a0546e | ||
|
f82575c49b | ||
|
a77fb3f18b | ||
|
a7ef8eb576 | ||
|
600c8c92ce | ||
|
137079e5c6 | ||
|
a71a635eb8 | ||
|
8a09806b8c | ||
|
459b5ea1ab | ||
|
4119a1ab64 | ||
|
e327885545 | ||
|
680ca173da | ||
|
622b76a621 | ||
|
bd4a2c9b49 |
@ -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"]}
|
||||
|
||||
|
@ -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
|
||||
|
12
color-module/src/foreign_serializer.rs
Normal file
12
color-module/src/foreign_serializer.rs
Normal 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)
|
||||
}
|
@ -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 }
|
||||
};
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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 {
|
||||
|
513
color-module/src/schemes/q_style_2/baseline.rs
Normal file
513
color-module/src/schemes/q_style_2/baseline.rs
Normal 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
|
||||
}
|
||||
}
|
282
color-module/src/schemes/q_style_2/color_set.rs
Normal file
282
color-module/src/schemes/q_style_2/color_set.rs
Normal 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
|
||||
}
|
||||
}
|
157
color-module/src/schemes/q_style_2/mod.rs
Normal file
157
color-module/src/schemes/q_style_2/mod.rs
Normal 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")
|
||||
}
|
||||
}
|
119
color-module/src/schemes/q_style_2/swatch.rs
Normal file
119
color-module/src/schemes/q_style_2/swatch.rs
Normal 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()
|
||||
}
|
12
package.json
12
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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':
|
||||
|
@ -9,6 +9,9 @@
|
||||
&.swatch {
|
||||
background-color: var(--color-pinlan);
|
||||
}
|
||||
&.q2 {
|
||||
background-color: var(--color-jugengzi);
|
||||
}
|
||||
&.m2 {
|
||||
background-color: #03dac6;
|
||||
color: var(--color-qihei);
|
||||
|
@ -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':
|
||||
|
@ -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
|
||||
|
44
src/page-components/scheme/Q2Scheme.tsx
Normal file
44
src/page-components/scheme/Q2Scheme.tsx
Normal 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} />}
|
||||
</>
|
||||
);
|
||||
}
|
44
src/page-components/scheme/q-2-scheme/Builder.module.css
Normal file
44
src/page-components/scheme/q-2-scheme/Builder.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
532
src/page-components/scheme/q-2-scheme/Builder.tsx
Normal file
532
src/page-components/scheme/q-2-scheme/Builder.tsx
Normal 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>
|
||||
);
|
||||
}
|
62
src/page-components/scheme/q-2-scheme/Preview.module.css
Normal file
62
src/page-components/scheme/q-2-scheme/Preview.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
177
src/page-components/scheme/q-2-scheme/Preview.tsx
Normal file
177
src/page-components/scheme/q-2-scheme/Preview.tsx
Normal 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;
|
@ -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
67
src/q-2-scheme.ts
Normal 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;
|
||||
};
|
@ -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%);
|
||||
|
Loading…
x
Reference in New Issue
Block a user