基本形成色卡页面功能。
This commit is contained in:
		
							
								
								
									
										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> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user