基本完成M3动态Scheme的构建功能。
This commit is contained in:
		| @@ -0,0 +1,52 @@ | ||||
| @layer pages { | ||||
|   .builder_layout { | ||||
|     padding: var(--spacing-s) var(--spacing-m); | ||||
|     font-size: var(--font-size-s); | ||||
|     line-height: 1.3em; | ||||
|     display: grid; | ||||
|     grid-template-columns: repeat(3, 200px); | ||||
|     align-items: center; | ||||
|     gap: var(--spacing-xs); | ||||
|     .label { | ||||
|       max-width: 200px; | ||||
|       grid-column: 1; | ||||
|       padding-inline-end: var(--spacing-m); | ||||
|       text-align: right; | ||||
|     } | ||||
|     .segment_title { | ||||
|       grid-column: 1 / span 2; | ||||
|       text-align: center; | ||||
|     } | ||||
|     .color_picker_row { | ||||
|       grid-column: 2 / span 2; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: var(--spacing-s); | ||||
|     } | ||||
|     .parallel_row { | ||||
|       display: flex; | ||||
|       flex-direction: row; | ||||
|       align-items: center; | ||||
|       gap: var(--spacing-s); | ||||
|     } | ||||
|     h5 { | ||||
|       font-size: var(--font-size-m); | ||||
|       line-height: 1.7em; | ||||
|     } | ||||
|     .error_msg { | ||||
|       color: var(--color-danger); | ||||
|       font-size: var(--font-size-xs); | ||||
|     } | ||||
|     .delete_btn { | ||||
|       font-size: var(--font-size-xs); | ||||
|       color: var(--color-yuebai); | ||||
|       background-color: oklch(from var(--color-danger) l c h / 0.25); | ||||
|       &:hover { | ||||
|         background-color: oklch(from var(--color-danger-hover) l c h / 0.65); | ||||
|       } | ||||
|       &:active { | ||||
|         background-color: oklch(from var(--color-danger-active) l c h / 0.65); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,186 @@ | ||||
| import { includes, isEmpty, isEqual, isNil } from 'lodash-es'; | ||||
| import { useActionState, useCallback, useMemo, useState } from 'react'; | ||||
| import { useColorFunction } from '../../../ColorFunctionContext'; | ||||
| import { FloatColorPicker } from '../../../components/FloatColorPicker'; | ||||
| import { ScrollArea } from '../../../components/ScrollArea'; | ||||
| import { Switch } from '../../../components/Switch'; | ||||
| import { VSegmentedControl } from '../../../components/VSegmentedControl'; | ||||
| import { MaterialDesign3DynamicSchemeStorage } from '../../../material-3-scheme'; | ||||
| import { Option, SchemeContent } from '../../../models'; | ||||
| import { useUpdateScheme } from '../../../stores/schemes'; | ||||
| import { ColorEntry, IdenticalColorEntry } from '../ColorEntry'; | ||||
| import styles from './Builder.module.css'; | ||||
|  | ||||
| export function M3DynamicSchemeBuilder() { | ||||
|   return <ScrollArea enableY></ScrollArea>; | ||||
| type M3DynamicSchemeBuilderProps = { | ||||
|   scheme: SchemeContent<MaterialDesign3DynamicSchemeStorage>; | ||||
|   onBuildCompleted?: () => void; | ||||
| }; | ||||
|  | ||||
| export function M3DynamicSchemeBuilder({ scheme, onBuildCompleted }: M3DynamicSchemeBuilderProps) { | ||||
|   const { colorFn } = useColorFunction(); | ||||
|   const updateScheme = useUpdateScheme(scheme.id); | ||||
|   const originalColors = useMemo(() => { | ||||
|     return Object.entries(scheme.schemeStorage.source?.custom_colors ?? {}).map( | ||||
|       ([name, color], index) => ({ id: `oc_${index}`, name, color } as IdenticalColorEntry), | ||||
|     ); | ||||
|   }, [scheme.schemeStorage.source]); | ||||
|   const [newColors, setNewColors] = useState<IdenticalColorEntry[]>([]); | ||||
|   const [deleted, setDeleted] = useState<string[]>([]); | ||||
|   const addEntryAction = useCallback(() => { | ||||
|     setNewColors((prev) => [...prev, { id: `nc_${prev.length}`, name: '', color: '' }]); | ||||
|   }, []); | ||||
|   const colorKeys = useMemo( | ||||
|     () => | ||||
|       [...originalColors, ...newColors] | ||||
|         .map((color) => color.id) | ||||
|         .filter((c) => !includes(deleted, c)), | ||||
|     [originalColors, newColors, deleted], | ||||
|   ); | ||||
|   const variantOptions = useMemo(() => { | ||||
|     if (!colorFn) return []; | ||||
|     try { | ||||
|       return colorFn.material_design_3_dynamic_variant() as Option[]; | ||||
|     } catch (e) { | ||||
|       console.error('[m3 dynamic builder]', e); | ||||
|     } | ||||
|     return []; | ||||
|   }, []); | ||||
|   const [contrastLevel, setContrastLevel] = useState<number>( | ||||
|     scheme.schemeStorage.source?.constrastLevel ?? 0, | ||||
|   ); | ||||
|  | ||||
|   const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>( | ||||
|     (_state, formData) => { | ||||
|       const errMsg = new Map<string, string>(); | ||||
|  | ||||
|       const sourceColor = formData.get('source') as string; | ||||
|       if (isNil(sourceColor) || isEmpty(sourceColor)) { | ||||
|         errMsg.set('source', 'Source color is required'); | ||||
|       } | ||||
|       if (!isEmpty(errMsg)) return errMsg; | ||||
|  | ||||
|       try { | ||||
|         const dynamicVariant = Number(formData.get('variant')); | ||||
|         const contrastLevel = Number(formData.get('contrast_level')); | ||||
|         const harmonizeCustoms = isEqual(formData.get('harmonize_customs'), 'true'); | ||||
|         const errorColor = formData.get('error') as string; | ||||
|         const customColors: Record<string, string> = {}; | ||||
|         for (const key of colorKeys) { | ||||
|           const name = formData.get(`name_${key}`) as string; | ||||
|           const color = formData.get(`color_${key}`) as string; | ||||
|           if (isNil(name) || isEmpty(name) || isNil(color) || isEmpty(color)) continue; | ||||
|           customColors[name] = color; | ||||
|         } | ||||
|  | ||||
|         const generate_scheme = colorFn.generate_material_design_3_dynamic_scheme( | ||||
|           sourceColor, | ||||
|           isNil(errorColor) || isEmpty(errorColor) ? null : errorColor, | ||||
|           dynamicVariant, | ||||
|           contrastLevel, | ||||
|           harmonizeCustoms, | ||||
|           customColors, | ||||
|         ); | ||||
|         console.debug('[generate m3d]', generate_scheme); | ||||
|       } catch (e) { | ||||
|         console.error('[generate m3d]', e); | ||||
|       } | ||||
|  | ||||
|       return errMsg; | ||||
|     }, | ||||
|     new Map<string, string>(), | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <ScrollArea enableY> | ||||
|       <form action={handleSubmitAction} className={styles.builder_layout}> | ||||
|         <h5 className={styles.segment_title}>Required Colors</h5> | ||||
|         <label className={styles.label}>Source Color</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker | ||||
|             name="source" | ||||
|             color={ | ||||
|               isNil(scheme.schemeStorage.source?.source) || | ||||
|               isEmpty(scheme.schemeStorage.source?.source) | ||||
|                 ? undefined | ||||
|                 : scheme.schemeStorage.source?.source | ||||
|             } | ||||
|           /> | ||||
|           {errMsg.has('source') && <span className={styles.error_msg}>{errMsg.get('source')}</span>} | ||||
|         </div> | ||||
|         <label className={styles.label}>Error Color</label> | ||||
|         <div className={styles.color_picker_row}> | ||||
|           <FloatColorPicker | ||||
|             name="error" | ||||
|             color={ | ||||
|               isNil(scheme.schemeStorage.source?.error) || | ||||
|               isEmpty(scheme.schemeStorage.source.error) | ||||
|                 ? undefined | ||||
|                 : scheme.schemeStorage.source.error | ||||
|             } | ||||
|           /> | ||||
|         </div> | ||||
|         <h5 className={styles.segment_title}>Dynamic Settings</h5> | ||||
|         <label className={styles.label}>Dynamic Variant</label> | ||||
|         <div> | ||||
|           <VSegmentedControl | ||||
|             name="variant" | ||||
|             options={variantOptions} | ||||
|             defaultValue={scheme.schemeStorage.source?.variant} | ||||
|           /> | ||||
|         </div> | ||||
|         <label className={styles.label}>Contrast Level</label> | ||||
|         <div className={styles.parallel_row}> | ||||
|           <input | ||||
|             type="range" | ||||
|             className="picker" | ||||
|             name="contrast_level" | ||||
|             min={-1} | ||||
|             max={1} | ||||
|             step={0.25} | ||||
|             value={contrastLevel} | ||||
|             onChange={(e) => setContrastLevel(parseFloat(e.target.value))} | ||||
|           /> | ||||
|           <span>{contrastLevel}</span> | ||||
|         </div> | ||||
|         <label className={styles.label}>Harmonize Custom Colors</label> | ||||
|         <div> | ||||
|           <Switch | ||||
|             name="harmonize_customs" | ||||
|             checked={scheme.schemeStorage.source?.harmonizeCustoms ?? false} | ||||
|           /> | ||||
|         </div> | ||||
|         <h5 className={styles.segment_title}>Custom Colors</h5> | ||||
|         <label style={{ gridColumn: 1 }}>Name</label> | ||||
|         <label>Color</label> | ||||
|         <div> | ||||
|           <button type="button" className="small" onClick={addEntryAction}> | ||||
|             Add Color | ||||
|           </button> | ||||
|         </div> | ||||
|         {originalColors | ||||
|           .filter((color) => !includes(deleted, color.id)) | ||||
|           .map((color) => ( | ||||
|             <ColorEntry | ||||
|               key={color.id} | ||||
|               entry={color} | ||||
|               onDelete={(index) => setDeleted((prev) => [...prev, index])} | ||||
|             /> | ||||
|           ))} | ||||
|         {newColors | ||||
|           .filter((color) => !includes(deleted, color.id)) | ||||
|           .map((color) => ( | ||||
|             <ColorEntry | ||||
|               key={color.id} | ||||
|               entry={color} | ||||
|               onDelete={(index) => setDeleted((prev) => [...prev, index])} | ||||
|             /> | ||||
|           ))} | ||||
|         <div style={{ gridColumn: '2 / span 2' }}> | ||||
|           <button type="submit" className="primary"> | ||||
|             Build Scheme | ||||
|           </button> | ||||
|         </div> | ||||
|       </form> | ||||
|     </ScrollArea> | ||||
|   ); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user