基本形成色卡页面功能。
This commit is contained in:
parent
6708c40ffb
commit
9fec4a31e9
10
src/App.tsx
10
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';
|
||||
|
@ -37,6 +39,14 @@ const routes = createBrowserRouter([
|
|||
{ 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" /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
74
src/page-components/cards-detail/ColorCard.tsx
Normal file
74
src/page-components/cards-detail/ColorCard.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { capitalize } 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>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user