基本完成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 { 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() { | type M3DynamicSchemeBuilderProps = { | ||||||
|   return <ScrollArea enableY></ScrollArea>; |   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