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:
|
基于React和WASM构建,不使用任何服务端程序支持。内建常见颜色色卡。
|
||||||
|
|
||||||
- [@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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
@ -10,7 +10,10 @@ crate-type = ["cdylib"]
|
|||||||
color-name = "1.1.0"
|
color-name = "1.1.0"
|
||||||
palette = { version = "0.7.6", features = ["serde"] }
|
palette = { version = "0.7.6", features = ["serde"] }
|
||||||
serde = { version = "1.0.216", features = ["derive"] }
|
serde = { version = "1.0.216", features = ["derive"] }
|
||||||
|
serde-wasm-bindgen = "0.6.5"
|
||||||
serde_json = "1.0.134"
|
serde_json = "1.0.134"
|
||||||
|
strum = { version = "0.26.3", features = ["derive", "strum_macros"] }
|
||||||
|
strum_macros = "0.26.4"
|
||||||
thiserror = "2.0.9"
|
thiserror = "2.0.9"
|
||||||
wasm-bindgen = { version = "0.2.99", features = ["serde", "serde_json", "serde-serialize"] }
|
wasm-bindgen = { version = "0.2.99", features = ["serde", "serde_json", "serde-serialize"] }
|
||||||
|
|
||||||
|
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 std::{str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use color_card::Category;
|
||||||
use palette::{
|
use palette::{
|
||||||
cam16::{Cam16Jch, Parameters},
|
cam16::{Cam16Jch, Parameters},
|
||||||
color_difference::Wcag21RelativeContrast,
|
color_difference::Wcag21RelativeContrast,
|
||||||
@ -7,8 +8,10 @@ use palette::{
|
|||||||
convert::FromColorUnclamped,
|
convert::FromColorUnclamped,
|
||||||
Darken, FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Lighten, Mix, Oklch, ShiftHue, Srgb,
|
Darken, FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Lighten, Mix, Oklch, ShiftHue, Srgb,
|
||||||
};
|
};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
mod color_card;
|
||||||
mod errors;
|
mod errors;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@ -377,3 +380,45 @@ pub fn tonal_darken_series(
|
|||||||
|
|
||||||
Ok(color_series.into_iter().rev().collect())
|
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 { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||||
import { ColorFunctionProvider } from './ColorFunctionContext';
|
import { ColorFunctionProvider } from './ColorFunctionContext';
|
||||||
import { Notifications } from './components/Notifications';
|
import { Notifications } from './components/Notifications';
|
||||||
|
import { ColorCards } from './pages/Cards';
|
||||||
|
import { CardsDetail } from './pages/CardsDetail';
|
||||||
import { Harmonies } from './pages/Harmonies';
|
import { Harmonies } from './pages/Harmonies';
|
||||||
import { Home } from './pages/Home';
|
import { Home } from './pages/Home';
|
||||||
import { LightenDarken } from './pages/LightenDarken';
|
import { LightenDarken } from './pages/LightenDarken';
|
||||||
@ -12,6 +14,7 @@ import { SchemeNotFound } from './pages/SchemeNotFound';
|
|||||||
import { Schemes } from './pages/Schemes';
|
import { Schemes } from './pages/Schemes';
|
||||||
import { TintsShades } from './pages/TintsShades';
|
import { TintsShades } from './pages/TintsShades';
|
||||||
import { Tones } from './pages/Tones';
|
import { Tones } from './pages/Tones';
|
||||||
|
import { WACGCheck } from './pages/WACG';
|
||||||
import { Wheels } from './pages/Wheels';
|
import { Wheels } from './pages/Wheels';
|
||||||
|
|
||||||
const routes = createBrowserRouter([
|
const routes = createBrowserRouter([
|
||||||
@ -35,6 +38,15 @@ const routes = createBrowserRouter([
|
|||||||
{ path: 'tints-shades', element: <TintsShades /> },
|
{ path: 'tints-shades', element: <TintsShades /> },
|
||||||
{ path: 'lighten-darken', element: <LightenDarken /> },
|
{ path: 'lighten-darken', element: <LightenDarken /> },
|
||||||
{ path: 'mixer', element: <Mixer /> },
|
{ 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 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_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 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;
|
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 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_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 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_malloc: (a: number, b: number) => number;
|
||||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: 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 __externref_table_dealloc: (a: number) => void;
|
||||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
readonly __externref_drop_slice: (a: number, b: number) => void;
|
readonly __externref_drop_slice: (a: number, b: number) => void;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
let wasm;
|
let wasm;
|
||||||
|
|
||||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
let WASM_VECTOR_LEN = 0;
|
||||||
|
|
||||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
|
||||||
|
|
||||||
let cachedUint8ArrayMemory0 = null;
|
let cachedUint8ArrayMemory0 = null;
|
||||||
|
|
||||||
@ -13,13 +11,6 @@ function getUint8ArrayMemory0() {
|
|||||||
return cachedUint8ArrayMemory0;
|
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 cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||||
|
|
||||||
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||||
@ -74,8 +65,26 @@ function passStringToWasm0(arg, malloc, realloc) {
|
|||||||
return ptr;
|
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) {
|
function takeFromExternrefTable0(idx) {
|
||||||
const value = wasm.__wbindgen_export_0.get(idx);
|
const value = wasm.__wbindgen_export_2.get(idx);
|
||||||
wasm.__externref_table_dealloc(idx);
|
wasm.__externref_table_dealloc(idx);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@ -530,21 +539,12 @@ export function wacg_relative_contrast(fg_color, bg_color) {
|
|||||||
return ret[0];
|
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) {
|
function getArrayJsValueFromWasm0(ptr, len) {
|
||||||
ptr = ptr >>> 0;
|
ptr = ptr >>> 0;
|
||||||
const mem = getDataViewMemory0();
|
const mem = getDataViewMemory0();
|
||||||
const result = [];
|
const result = [];
|
||||||
for (let i = ptr; i < ptr + 4 * len; i += 4) {
|
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);
|
wasm.__externref_drop_slice(ptr, len);
|
||||||
return result;
|
return result;
|
||||||
@ -708,6 +708,37 @@ export function tonal_darken_series(color, expand_amount, step) {
|
|||||||
return v2;
|
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) {
|
async function __wbg_load(module, imports) {
|
||||||
if (typeof Response === 'function' && module instanceof Response) {
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||||
@ -742,8 +773,49 @@ async function __wbg_load(module, imports) {
|
|||||||
function __wbg_get_imports() {
|
function __wbg_get_imports() {
|
||||||
const imports = {};
|
const imports = {};
|
||||||
imports.wbg = {};
|
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() {
|
imports.wbg.__wbindgen_init_externref_table = function() {
|
||||||
const table = wasm.__wbindgen_export_0;
|
const table = wasm.__wbindgen_export_2;
|
||||||
const offset = table.grow(4);
|
const offset = table.grow(4);
|
||||||
table.set(0, undefined);
|
table.set(0, undefined);
|
||||||
table.set(offset + 0, undefined);
|
table.set(offset + 0, undefined);
|
||||||
@ -752,10 +824,21 @@ function __wbg_get_imports() {
|
|||||||
table.set(offset + 3, false);
|
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) {
|
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||||
const ret = getStringFromWasm0(arg0, arg1);
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||||
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||||
|
};
|
||||||
|
|
||||||
return imports;
|
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 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_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 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_malloc: (a: number, b: number) => number;
|
||||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: 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 __externref_table_dealloc: (a: number) => void;
|
||||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
export const __externref_drop_slice: (a: number, b: number) => void;
|
export const __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: 1px solid oklch(from var(--color-bg) calc(l + (1 - l) * 0.1) c h);
|
||||||
border-radius: var(--border-radius-xxs);
|
border-radius: var(--border-radius-xxs);
|
||||||
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4);
|
||||||
@ -385,4 +385,42 @@
|
|||||||
border-radius: var(--border-radius-xxs);
|
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;
|
color: string;
|
||||||
ratio: number;
|
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
|
WACG Check
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
to="/compare"
|
||||||
|
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
|
||||||
|
Compare
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/cards"
|
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…
x
Reference in New Issue
Block a user