Compare commits
10 Commits
a94626c192
...
612f1ba751
Author | SHA1 | Date | |
---|---|---|---|
|
612f1ba751 | ||
|
c737712d3f | ||
|
67164e35fa | ||
|
9fec4a31e9 | ||
|
6708c40ffb | ||
|
12d6b04ddc | ||
|
f2031f3d8c | ||
|
5e7b1e709d | ||
|
f775c3b78f | ||
|
6bc0779f26 |
51
README.md
51
README.md
|
@ -1,50 +1,5 @@
|
|||
# React + TypeScript + Vite
|
||||
# Color Q
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
提供专业的多角度颜色选择、搭配、研究的网站。
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import react from 'eslint-plugin-react'
|
||||
|
||||
export default tseslint.config({
|
||||
// Set the react version
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
// Add the react plugin
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended rules
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
基于React和WASM构建,不使用任何服务端程序支持。内建常见颜色色卡。
|
||||
|
|
|
@ -10,7 +10,10 @@ crate-type = ["cdylib"]
|
|||
color-name = "1.1.0"
|
||||
palette = { version = "0.7.6", features = ["serde"] }
|
||||
serde = { version = "1.0.216", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.6.5"
|
||||
serde_json = "1.0.134"
|
||||
strum = { version = "0.26.3", features = ["derive", "strum_macros"] }
|
||||
strum_macros = "0.26.4"
|
||||
thiserror = "2.0.9"
|
||||
wasm-bindgen = { version = "0.2.99", features = ["serde", "serde_json", "serde-serialize"] }
|
||||
|
||||
|
|
89
color-module/src/color_card.rs
Normal file
89
color-module/src/color_card.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumIter, EnumString};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ColorDescription {
|
||||
pub name: String,
|
||||
pub pinyin: Vec<String>,
|
||||
pub hue: f32,
|
||||
pub lightness: f32,
|
||||
pub category: String,
|
||||
pub tags: Vec<String>,
|
||||
pub rgb: [u8; 3],
|
||||
pub hsl: [f32; 3],
|
||||
pub lab: [f32; 3],
|
||||
pub oklch: [f32; 3],
|
||||
}
|
||||
|
||||
const COLOR_CARDS_JSON: &str = include_str!("colorcards.json");
|
||||
pub const COLOR_CARDS: LazyLock<Vec<ColorDescription>> =
|
||||
LazyLock::new(|| serde_json::from_str(COLOR_CARDS_JSON).expect("Failed to parse color cards"));
|
||||
const CHROMA_EPSILON: f32 = 0.02;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Display, EnumString, EnumIter)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum Category {
|
||||
Red,
|
||||
Orange,
|
||||
Yellow,
|
||||
Green,
|
||||
Cyan,
|
||||
Blue,
|
||||
Purple,
|
||||
Magenta,
|
||||
White,
|
||||
Black,
|
||||
Gray,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub fn from_oklch_components(lightness: f32, chroma: f32, hue: f32) -> Self {
|
||||
if chroma < CHROMA_EPSILON {
|
||||
if lightness < 0.15 {
|
||||
Category::Black
|
||||
} else if lightness > 0.9 {
|
||||
Category::White
|
||||
} else {
|
||||
Category::Gray
|
||||
}
|
||||
} else {
|
||||
let processed_hue = hue % 360.0;
|
||||
match processed_hue {
|
||||
0.0..=15.0 => Category::Magenta,
|
||||
15.0..=45.0 => Category::Red,
|
||||
45.0..=75.0 => Category::Orange,
|
||||
75.0..=120.0 => Category::Yellow,
|
||||
120.0..=180.0 => Category::Green,
|
||||
180.0..=210.0 => Category::Cyan,
|
||||
210.0..=270.0 => Category::Blue,
|
||||
270.0..=345.0 => Category::Purple,
|
||||
345.0..=360.0 => Category::Magenta,
|
||||
_ => Category::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_oklch(oklch: &[f32; 3]) -> Self {
|
||||
Category::from_oklch_components(oklch[0], oklch[1], oklch[2])
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Category::Red => "Red".to_string(),
|
||||
Category::Orange => "Orange".to_string(),
|
||||
Category::Yellow => "Yellow".to_string(),
|
||||
Category::Green => "Green".to_string(),
|
||||
Category::Cyan => "Cyan".to_string(),
|
||||
Category::Blue => "Blue".to_string(),
|
||||
Category::Purple => "Purple".to_string(),
|
||||
Category::Magenta => "Magenta".to_string(),
|
||||
Category::White => "White".to_string(),
|
||||
Category::Black => "Black".to_string(),
|
||||
Category::Gray => "Gray".to_string(),
|
||||
Category::Unknown => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
1
color-module/src/colorcards.json
Normal file
1
color-module/src/colorcards.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,6 @@
|
|||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use color_card::Category;
|
||||
use palette::{
|
||||
cam16::{Cam16Jch, Parameters},
|
||||
color_difference::Wcag21RelativeContrast,
|
||||
|
@ -7,8 +8,10 @@ use palette::{
|
|||
convert::FromColorUnclamped,
|
||||
Darken, FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Lighten, Mix, Oklch, ShiftHue, Srgb,
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod color_card;
|
||||
mod errors;
|
||||
|
||||
#[wasm_bindgen]
|
||||
|
@ -377,3 +380,45 @@ pub fn tonal_darken_series(
|
|||
|
||||
Ok(color_series.into_iter().rev().collect())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn color_categories() -> Result<JsValue, String> {
|
||||
let categories = Category::iter()
|
||||
.map(|variant| {
|
||||
serde_json::json!({
|
||||
"label": variant.label(),
|
||||
"value": variant.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
serde_wasm_bindgen::to_value(&categories).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn search_color_cards(tag: String, category: Option<String>) -> Result<JsValue, String> {
|
||||
let selected_category = category.and_then(|c| Category::from_str(&c).ok());
|
||||
let all_cards = &*color_card::COLOR_CARDS;
|
||||
let mut cards = all_cards
|
||||
.iter()
|
||||
.filter(|card| card.tags.contains(&tag))
|
||||
.filter(|card| {
|
||||
if let Some(category) = &selected_category {
|
||||
let card_category = Category::from_oklch(&card.oklch);
|
||||
card_category == *category
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cards.sort_by(|a, b| {
|
||||
a.oklch[2]
|
||||
.partial_cmp(&b.oklch[2])
|
||||
.or_else(|| a.oklch[1].partial_cmp(&b.oklch[1]))
|
||||
.or_else(|| a.oklch[0].partial_cmp(&b.oklch[0]))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
serde_wasm_bindgen::to_value(&cards).map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
12
src/App.tsx
12
src/App.tsx
|
@ -1,6 +1,8 @@
|
|||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import { ColorFunctionProvider } from './ColorFunctionContext';
|
||||
import { Notifications } from './components/Notifications';
|
||||
import { ColorCards } from './pages/Cards';
|
||||
import { CardsDetail } from './pages/CardsDetail';
|
||||
import { Harmonies } from './pages/Harmonies';
|
||||
import { Home } from './pages/Home';
|
||||
import { LightenDarken } from './pages/LightenDarken';
|
||||
|
@ -12,6 +14,7 @@ import { SchemeNotFound } from './pages/SchemeNotFound';
|
|||
import { Schemes } from './pages/Schemes';
|
||||
import { TintsShades } from './pages/TintsShades';
|
||||
import { Tones } from './pages/Tones';
|
||||
import { WACGCheck } from './pages/WACG';
|
||||
import { Wheels } from './pages/Wheels';
|
||||
|
||||
const routes = createBrowserRouter([
|
||||
|
@ -35,6 +38,15 @@ const routes = createBrowserRouter([
|
|||
{ path: 'tints-shades', element: <TintsShades /> },
|
||||
{ path: 'lighten-darken', element: <LightenDarken /> },
|
||||
{ path: 'mixer', element: <Mixer /> },
|
||||
{ path: 'wacg', element: <WACGCheck /> },
|
||||
{
|
||||
path: 'cards',
|
||||
element: <ColorCards />,
|
||||
children: [
|
||||
{ path: 'chinese', element: <CardsDetail mainTag="chinese" /> },
|
||||
{ path: 'japanese', element: <CardsDetail mainTag="japanese" /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
6
src/color_functions/color_module.d.ts
vendored
6
src/color_functions/color_module.d.ts
vendored
|
@ -28,6 +28,8 @@ export function triadic(color: string): (string)[];
|
|||
export function series(color: string, expand_amount: number, step: number): (string)[];
|
||||
export function tonal_lighten_series(color: string, expand_amount: number, step: number): (string)[];
|
||||
export function tonal_darken_series(color: string, expand_amount: number, step: number): (string)[];
|
||||
export function color_categories(): any;
|
||||
export function search_color_cards(tag: string, category?: string): any;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
|
@ -61,9 +63,11 @@ export interface InitOutput {
|
|||
readonly series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
readonly tonal_lighten_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
readonly tonal_darken_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
readonly __wbindgen_export_0: WebAssembly.Table;
|
||||
readonly color_categories: () => [number, number, number];
|
||||
readonly search_color_cards: (a: number, b: number, c: number, d: number) => [number, number, number];
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_export_2: WebAssembly.Table;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __externref_drop_slice: (a: number, b: number) => void;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
let wasm;
|
||||
|
||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
|
||||
|
@ -13,13 +11,6 @@ function getUint8ArrayMemory0() {
|
|||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||
|
||||
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||
|
@ -74,8 +65,26 @@ function passStringToWasm0(arg, malloc, realloc) {
|
|||
return ptr;
|
||||
}
|
||||
|
||||
let cachedDataViewMemory0 = null;
|
||||
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_export_0.get(idx);
|
||||
const value = wasm.__wbindgen_export_2.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
|
@ -530,21 +539,12 @@ export function wacg_relative_contrast(fg_color, bg_color) {
|
|||
return ret[0];
|
||||
}
|
||||
|
||||
let cachedDataViewMemory0 = null;
|
||||
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
function getArrayJsValueFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
const mem = getDataViewMemory0();
|
||||
const result = [];
|
||||
for (let i = ptr; i < ptr + 4 * len; i += 4) {
|
||||
result.push(wasm.__wbindgen_export_0.get(mem.getUint32(i, true)));
|
||||
result.push(wasm.__wbindgen_export_2.get(mem.getUint32(i, true)));
|
||||
}
|
||||
wasm.__externref_drop_slice(ptr, len);
|
||||
return result;
|
||||
|
@ -708,6 +708,37 @@ export function tonal_darken_series(color, expand_amount, step) {
|
|||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {any}
|
||||
*/
|
||||
export function color_categories() {
|
||||
const ret = wasm.color_categories();
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
function isLikeNone(x) {
|
||||
return x === undefined || x === null;
|
||||
}
|
||||
/**
|
||||
* @param {string} tag
|
||||
* @param {string | undefined} [category]
|
||||
* @returns {any}
|
||||
*/
|
||||
export function search_color_cards(tag, category) {
|
||||
const ptr0 = passStringToWasm0(tag, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
var ptr1 = isLikeNone(category) ? 0 : passStringToWasm0(category, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.search_color_cards(ptr0, len0, ptr1, len1);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
|
@ -742,8 +773,49 @@ async function __wbg_load(module, imports) {
|
|||
function __wbg_get_imports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbg_String_8f0eb39a4a4c2f66 = function(arg0, arg1) {
|
||||
const ret = String(arg1);
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
};
|
||||
imports.wbg.__wbg_new_254fa9eac11932ae = function() {
|
||||
const ret = new Array();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_688846f374351c92 = function() {
|
||||
const ret = new Object();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_bc96c6a1c0786643 = function() {
|
||||
const ret = new Map();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_set_1d80752d0d5f0b21 = function(arg0, arg1, arg2) {
|
||||
arg0[arg1 >>> 0] = arg2;
|
||||
};
|
||||
imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) {
|
||||
arg0[arg1] = arg2;
|
||||
};
|
||||
imports.wbg.__wbg_set_76818dc3c59a63d5 = function(arg0, arg1, arg2) {
|
||||
const ret = arg0.set(arg1, arg2);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_bigint_from_i64 = function(arg0) {
|
||||
const ret = arg0;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
|
||||
const ret = BigInt.asUintN(64, arg0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
|
||||
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_init_externref_table = function() {
|
||||
const table = wasm.__wbindgen_export_0;
|
||||
const table = wasm.__wbindgen_export_2;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
|
@ -752,10 +824,21 @@ function __wbg_get_imports() {
|
|||
table.set(offset + 3, false);
|
||||
;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_string = function(arg0) {
|
||||
const ret = typeof(arg0) === 'string';
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_number_new = function(arg0) {
|
||||
const ret = arg0;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -29,9 +29,11 @@ export const triadic: (a: number, b: number) => [number, number, number, number]
|
|||
export const series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
export const tonal_lighten_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
export const tonal_darken_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
export const __wbindgen_export_0: WebAssembly.Table;
|
||||
export const color_categories: () => [number, number, number];
|
||||
export const search_color_cards: (a: number, b: number, c: number, d: number) => [number, number, number];
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __wbindgen_export_2: WebAssembly.Table;
|
||||
export const __externref_table_dealloc: (a: number) => void;
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __externref_drop_slice: (a: number, b: number) => void;
|
||||
|
|
|
@ -170,7 +170,7 @@
|
|||
}
|
||||
|
||||
/* 输入框以及输入框组合体默认样式 */
|
||||
:where(input, textarea) {
|
||||
:where(input, textarea, select) {
|
||||
border: 1px solid oklch(from var(--color-bg) calc(l + (1 - l) * 0.1) c h);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4);
|
||||
|
@ -385,4 +385,42 @@
|
|||
border-radius: var(--border-radius-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
/* Badge */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xs) var(--spacing-m);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
font-size: var(--font-size-m);
|
||||
font-weight: bold;
|
||||
line-height: 1.2em;
|
||||
color: var(--color-fg);
|
||||
background-color: var(--color-neutral);
|
||||
&.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&.primary {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
&.secondary {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
&.accent {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
&.danger {
|
||||
background-color: var(--color-danger);
|
||||
}
|
||||
&.warn {
|
||||
background-color: var(--color-warn);
|
||||
}
|
||||
&.success {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
&.info {
|
||||
background-color: var(--color-info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,3 +7,16 @@ export type HarmonyColor = {
|
|||
color: string;
|
||||
ratio: number;
|
||||
};
|
||||
|
||||
export type ColorDescription = {
|
||||
name: string;
|
||||
pinyin: string[];
|
||||
hue: number;
|
||||
lightness: number;
|
||||
category: string;
|
||||
tags: string[];
|
||||
rgb: [number, number, number];
|
||||
hsl: [number, number, number];
|
||||
lab: [number, number, number];
|
||||
oklch: [number, number, number];
|
||||
};
|
||||
|
|
42
src/page-components/cards-detail/ColorCard.module.css
Normal file
42
src/page-components/cards-detail/ColorCard.module.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
@layer pages {
|
||||
.card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-n);
|
||||
font-size: var(--font-size-xxs);
|
||||
line-height: var(--font-size-xxs);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-xs);
|
||||
cursor: pointer;
|
||||
}
|
||||
.color_block {
|
||||
width: 100%;
|
||||
height: 5em;
|
||||
}
|
||||
.description_line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xs) var(--spacing-s);
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xxs);
|
||||
flex: 1;
|
||||
.name {
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--font-size-xs);
|
||||
font-weight: bold;
|
||||
}
|
||||
.en_name {
|
||||
font-style: italic;
|
||||
color: var(--color-neutral-focus);
|
||||
}
|
||||
}
|
||||
.color_value {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
76
src/page-components/cards-detail/ColorCard.tsx
Normal file
76
src/page-components/cards-detail/ColorCard.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { capitalize, isEmpty } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useColorFunction } from '../../ColorFunctionContext';
|
||||
import { useCopyColor } from '../../hooks/useCopyColor';
|
||||
import { ColorDescription } from '../../models';
|
||||
import styles from './ColorCard.module.css';
|
||||
|
||||
type ColorCardProps = {
|
||||
color: ColorDescription;
|
||||
copyMode?: 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
|
||||
};
|
||||
|
||||
export function ColorCard({ color, copyMode }: ColorCardProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const copytToClipboard = useCopyColor();
|
||||
const colorHex = useMemo(() => {
|
||||
const [r, g, b] = color.rgb;
|
||||
if (colorFn) {
|
||||
try {
|
||||
const hex = colorFn.rgb_to_hex(r, g, b);
|
||||
return hex;
|
||||
} catch (e) {
|
||||
console.error('[Convert RGB]', e);
|
||||
}
|
||||
}
|
||||
return `${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
}, [colorFn, color]);
|
||||
const handleCopy = useCallback(() => {
|
||||
switch (copyMode) {
|
||||
case 'rgb':
|
||||
copytToClipboard(`rgb(${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})`);
|
||||
break;
|
||||
case 'hsl':
|
||||
copytToClipboard(
|
||||
`hsl(${color.hsl[0].toFixed(1)}, ${(color.hsl[1] * 100).toFixed(2)}%, ${(
|
||||
color.hsl[2] * 100
|
||||
).toFixed(2)}%)`,
|
||||
);
|
||||
break;
|
||||
case 'lab':
|
||||
copytToClipboard(
|
||||
`lab(${color.lab[0].toFixed(1)}, ${color.lab[1].toFixed(2)}, ${color.lab[2].toFixed(2)})`,
|
||||
);
|
||||
break;
|
||||
case 'oklch':
|
||||
copytToClipboard(
|
||||
`oklch(${(color.oklch[0] * 100).toFixed(2)}%, ${color.oklch[1].toFixed(
|
||||
4,
|
||||
)}, ${color.oklch[2].toFixed(1)})`,
|
||||
);
|
||||
break;
|
||||
case 'hex':
|
||||
default:
|
||||
copytToClipboard(`#${colorHex}`);
|
||||
break;
|
||||
}
|
||||
}, [copytToClipboard, color, copyMode, colorHex]);
|
||||
|
||||
return (
|
||||
<div className={styles.card} onClick={handleCopy}>
|
||||
<div
|
||||
className={styles.color_block}
|
||||
style={{ backgroundColor: `rgb(${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})` }}
|
||||
/>
|
||||
<div className={styles.description_line}>
|
||||
<div className={styles.title}>
|
||||
<span className={styles.name}>{color.name}</span>
|
||||
{!isEmpty(color.pinyin) && (
|
||||
<span className={styles.en_name}>{color.pinyin.map(capitalize).join(' ')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.color_value}>#{colorHex}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
26
src/page-components/color-cards/CardNavigation.tsx
Normal file
26
src/page-components/color-cards/CardNavigation.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import cx from 'clsx';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import styles from './CardsNavigation.module.css';
|
||||
|
||||
export function CardsNavigation() {
|
||||
return (
|
||||
<div className={styles.cards_list}>
|
||||
<menu className={styles.nav_menu}>
|
||||
<li>
|
||||
<NavLink
|
||||
to="chinese"
|
||||
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
|
||||
Chinese Traditional
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="japanese"
|
||||
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
|
||||
Japanese Traditional
|
||||
</NavLink>
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
);
|
||||
}
|
33
src/page-components/color-cards/CardsNavigation.module.css
Normal file
33
src/page-components/color-cards/CardsNavigation.module.css
Normal file
|
@ -0,0 +1,33 @@
|
|||
@layer pages {
|
||||
.cards_list {
|
||||
max-width: calc(var(--spacing) * 125);
|
||||
flex: 1 1 calc(var(--spacing) * 125);
|
||||
padding: calc(var(--spacing) * 4) 0;
|
||||
box-shadow: 2px 0 8px oklch(from var(--color-black) l c h / 65%);
|
||||
z-index: 40;
|
||||
}
|
||||
.nav_menu {
|
||||
flex: 1 0;
|
||||
padding: var(--spacing-n);
|
||||
padding-block-start: var(--spacing-s);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xs);
|
||||
li {
|
||||
list-style: none;
|
||||
a.nav_link {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding-inline: var(--spacing-l);
|
||||
padding-block: var(--spacing-s);
|
||||
&.active {
|
||||
background-color: var(--color-primary-active);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -70,6 +70,13 @@ export function NavigationMenu() {
|
|||
WACG Check
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/compare"
|
||||
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
|
||||
Compare
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/cards"
|
||||
|
|
13
src/page-components/wacg/Ratio.module.css
Normal file
13
src/page-components/wacg/Ratio.module.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
@layer pages {
|
||||
.ratio_layout {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
.ratio {
|
||||
font-size: calc(var(--font-size) * 5);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
13
src/page-components/wacg/Ratio.tsx
Normal file
13
src/page-components/wacg/Ratio.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import styles from './Ratio.module.css';
|
||||
|
||||
type ContrastRatioProps = {
|
||||
ratio: number;
|
||||
};
|
||||
|
||||
export function ContrastRatio({ ratio }: ContrastRatioProps) {
|
||||
return (
|
||||
<div className={styles.ratio_layout}>
|
||||
<div className={styles.ratio}>{ratio.toFixed(2)} : 1</div>
|
||||
</div>
|
||||
);
|
||||
}
|
46
src/page-components/wacg/TextDemo.module.css
Normal file
46
src/page-components/wacg/TextDemo.module.css
Normal file
|
@ -0,0 +1,46 @@
|
|||
@layer pages {
|
||||
.demo_block {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-xxs);
|
||||
padding: var(--spacing-m) var(--spacing-s);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
gap: var(--spacing-s);
|
||||
.normal_text {
|
||||
font-size: 14pt;
|
||||
}
|
||||
.large_text {
|
||||
font-size: 18pt;
|
||||
}
|
||||
.bold_text {
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.wacg_rating {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
.rating_unit {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
.sub_header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.description {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-neutral-focus);
|
||||
}
|
||||
}
|
61
src/page-components/wacg/TextDemo.tsx
Normal file
61
src/page-components/wacg/TextDemo.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import cx from 'clsx';
|
||||
import styles from './TextDemo.module.css';
|
||||
|
||||
type TextDemoProps = {
|
||||
fg: string;
|
||||
bg: string;
|
||||
ratio: number;
|
||||
};
|
||||
|
||||
export function TextDemo({ fg, bg, ratio }: TextDemoProps) {
|
||||
return (
|
||||
<>
|
||||
<header className={styles.sub_header}>
|
||||
<h5>Normal Text</h5>
|
||||
<p className={styles.description}>14pt normal weight text.</p>
|
||||
</header>
|
||||
<div className={styles.demo_block} style={{ backgroundColor: `#${bg}`, color: `#${fg}` }}>
|
||||
<div className={styles.normal_text}>The quick brown fox jumps over the lazy dog.</div>
|
||||
<div className={styles.normal_text}>白日依山尽,黄河入海流。欲穷千里目,更上一层楼。</div>
|
||||
</div>
|
||||
<div className={styles.wacg_rating}>
|
||||
<div className={styles.rating_unit}>
|
||||
<span>WACG AA:</span>
|
||||
<span className={cx('badge', 'uppercase', ratio > 4.5 ? 'success' : 'danger')}>
|
||||
{ratio > 4.5 ? 'pass' : 'failed'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.rating_unit}>
|
||||
<span>WACG AAA:</span>
|
||||
<span className={cx('badge', 'uppercase', ratio > 7 ? 'success' : 'danger')}>
|
||||
{ratio > 7 ? 'pass' : 'failed'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<header className={styles.sub_header}>
|
||||
<h5>Large/Bold Text</h5>
|
||||
<p className={styles.description}>18pt normal weight text and 14pt bold text.</p>
|
||||
</header>
|
||||
<div className={styles.demo_block} style={{ backgroundColor: `#${bg}`, color: `#${fg}` }}>
|
||||
<div className={styles.large_text}>The quick brown fox jumps over the lazy dog.</div>
|
||||
<div className={styles.large_text}>白日依山尽,黄河入海流。欲穷千里目,更上一层楼。</div>
|
||||
<div className={styles.bold_text}>The quick brown fox jumps over the lazy dog.</div>
|
||||
<div className={styles.bold_text}>白日依山尽,黄河入海流。欲穷千里目,更上一层楼。</div>
|
||||
</div>
|
||||
<div className={styles.wacg_rating}>
|
||||
<div className={styles.rating_unit}>
|
||||
<span>WACG AA:</span>
|
||||
<span className={cx('badge', 'uppercase', ratio > 3 ? 'success' : 'danger')}>
|
||||
{ratio > 3 ? 'pass' : 'failed'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.rating_unit}>
|
||||
<span>WACG AAA:</span>
|
||||
<span className={cx('badge', 'uppercase', ratio > 4.5 ? 'success' : 'danger')}>
|
||||
{ratio > 4.5 ? 'pass' : 'failed'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
13
src/pages/Cards.module.css
Normal file
13
src/pages/Cards.module.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
@layer pages {
|
||||
.cards_workspace {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
.cards_container {
|
||||
flex: 1 0;
|
||||
}
|
||||
}
|
14
src/pages/Cards.tsx
Normal file
14
src/pages/Cards.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Outlet } from 'react-router-dom';
|
||||
import { CardsNavigation } from '../page-components/color-cards/CardNavigation';
|
||||
import styles from './Cards.module.css';
|
||||
|
||||
export function ColorCards() {
|
||||
return (
|
||||
<div className={styles.cards_workspace}>
|
||||
<CardsNavigation />
|
||||
<div className={styles.cards_container}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
28
src/pages/CardsDetail.module.css
Normal file
28
src/pages/CardsDetail.module.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
@layer pages {
|
||||
.cards_workspace {
|
||||
padding: var(--spacing-l) var(--spacing-m);
|
||||
flex-direction: column;
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
.cate_select {
|
||||
padding: var(--spacing-xxs) var(--spacing-s);
|
||||
min-width: 7em;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
}
|
||||
.cards_container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-s);
|
||||
padding: 0 var(--spacing-s);
|
||||
.card {
|
||||
flex-basis: calc((100% - var(--spacing-s) * 4) / 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
src/pages/CardsDetail.tsx
Normal file
102
src/pages/CardsDetail.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
import cx from 'clsx';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { ChangeEvent, useMemo, useState } from 'react';
|
||||
import { useColorFunction } from '../ColorFunctionContext';
|
||||
import { HSegmentedControl } from '../components/HSegmentedControl';
|
||||
import { ScrollArea } from '../components/ScrollArea';
|
||||
import { ColorDescription } from '../models';
|
||||
import { ColorCard } from '../page-components/cards-detail/ColorCard';
|
||||
import styles from './CardsDetail.module.css';
|
||||
|
||||
type CardsDetailProps = {
|
||||
mainTag: string;
|
||||
};
|
||||
|
||||
export function CardsDetail({ mainTag }: CardsDetailProps) {
|
||||
const { colorFn } = useColorFunction();
|
||||
const categories = useMemo(() => {
|
||||
if (!colorFn) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const embededCategories = colorFn.color_categories() as { label: string; value: string }[];
|
||||
return embededCategories.filter((cate) => !isEqual(cate.get('value'), 'unknown'));
|
||||
} catch (e) {
|
||||
console.error('[Fetch color categories]', e);
|
||||
}
|
||||
return [];
|
||||
}, [colorFn]);
|
||||
const [colorCategory, setCategory] = useState<string | 'null'>('null');
|
||||
const handleSelectCategory = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedValue = e.target.value;
|
||||
setCategory(selectedValue);
|
||||
};
|
||||
const [mode, setMode] = useState<'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch'>('hex');
|
||||
const colors = useMemo(() => {
|
||||
if (!colorFn) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const colorCate = isEqual(colorCategory, 'null') ? undefined : colorCategory;
|
||||
let tag = '';
|
||||
switch (mainTag) {
|
||||
case 'japanese':
|
||||
tag = 'japanese_traditional';
|
||||
break;
|
||||
case 'chinese':
|
||||
tag = 'chinese_traditional';
|
||||
break;
|
||||
default:
|
||||
tag = '';
|
||||
break;
|
||||
}
|
||||
const embedColors = colorFn.search_color_cards(tag, colorCate) as ColorDescription[];
|
||||
console.debug('[Fetch cards]', embedColors);
|
||||
return embedColors;
|
||||
} catch (e) {
|
||||
console.error('[Fetch colors]', e);
|
||||
}
|
||||
return [];
|
||||
}, [colorFn, mainTag, colorCategory]);
|
||||
|
||||
return (
|
||||
<div className={cx('workspace', styles.cards_workspace)}>
|
||||
<div className={styles.filters}>
|
||||
<span>Show</span>
|
||||
<select
|
||||
className={styles.cate_select}
|
||||
value={colorCategory}
|
||||
onChange={handleSelectCategory}>
|
||||
<option value="null">All</option>
|
||||
{categories.map((cate, index) => (
|
||||
<option key={`${cate.get('value')}-${index}`} value={cate.get('value')}>
|
||||
{cate.get('label')}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span>colors.</span>
|
||||
<div>Copy color value in</div>
|
||||
<HSegmentedControl
|
||||
options={[
|
||||
{ label: 'HEX', value: 'hex' },
|
||||
{ label: 'RGB', value: 'rgb' },
|
||||
{ label: 'HSL', value: 'hsl' },
|
||||
{ label: 'LAB', value: 'lab' },
|
||||
{ label: 'OKLCH', value: 'oklch' },
|
||||
]}
|
||||
value={mode}
|
||||
onChange={setMode}
|
||||
/>
|
||||
</div>
|
||||
<ScrollArea enableY>
|
||||
<div className={styles.cards_container}>
|
||||
{colors.map((c, index) => (
|
||||
<div key={`${c.name}-${index}`} className={styles.card}>
|
||||
<ColorCard color={c} copyMode={mode} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
40
src/pages/WACG.module.css
Normal file
40
src/pages/WACG.module.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
@layer pages {
|
||||
.wacg_workspace {
|
||||
flex-direction: column;
|
||||
}
|
||||
.explore_section {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.function_side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-m);
|
||||
font-size: var(--font-size-s);
|
||||
.mode_navigation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
h5 {
|
||||
padding-block: var(--spacing-m);
|
||||
font-size: var(--font-size-m);
|
||||
}
|
||||
}
|
||||
.wacg_content {
|
||||
flex: 1;
|
||||
padding: 0 var(--spacing-m);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-s);
|
||||
h5 {
|
||||
padding-block: var(--spacing-m);
|
||||
font-size: var(--font-size-m);
|
||||
}
|
||||
}
|
||||
}
|
47
src/pages/WACG.tsx
Normal file
47
src/pages/WACG.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import cx from 'clsx';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useColorFunction } from '../ColorFunctionContext';
|
||||
import { ColorPicker } from '../components/ColorPicker';
|
||||
import { ScrollArea } from '../components/ScrollArea';
|
||||
import { ContrastRatio } from '../page-components/wacg/Ratio';
|
||||
import { TextDemo } from '../page-components/wacg/TextDemo';
|
||||
import styles from './WACG.module.css';
|
||||
|
||||
export function WACGCheck() {
|
||||
const { colorFn } = useColorFunction();
|
||||
const [fgColor, setFgColor] = useState('ffffff');
|
||||
const [bgColor, setBgColor] = useState('000000');
|
||||
const contrastRatio = useMemo(() => {
|
||||
try {
|
||||
if (!colorFn) return 1;
|
||||
const ratio = colorFn.wacg_relative_contrast(fgColor, bgColor);
|
||||
return ratio;
|
||||
} catch (e) {
|
||||
console.error('[WACG Check]', e);
|
||||
}
|
||||
return 1;
|
||||
}, [fgColor, bgColor]);
|
||||
|
||||
return (
|
||||
<div className={cx('workspace', styles.wacg_workspace)}>
|
||||
<header>
|
||||
<h3>WACG Check</h3>
|
||||
</header>
|
||||
<ScrollArea enableY>
|
||||
<section className={styles.explore_section}>
|
||||
<aside className={styles.function_side}>
|
||||
<h5>Foreground Color</h5>
|
||||
<ColorPicker color={fgColor} onSelect={setFgColor} />
|
||||
<h5>Background Color</h5>
|
||||
<ColorPicker color={bgColor} onSelect={setBgColor} />
|
||||
</aside>
|
||||
<div className={styles.wacg_content}>
|
||||
<h5>WACG Contrast Ratio</h5>
|
||||
<ContrastRatio ratio={contrastRatio} />
|
||||
<TextDemo fg={fgColor} bg={bgColor} ratio={contrastRatio} />
|
||||
</div>
|
||||
</section>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user