基本完成M2 Scheme的构建功能。

This commit is contained in:
徐涛 2025-02-10 08:38:40 +08:00
parent 6b262f536d
commit 546ca97b10
2 changed files with 221 additions and 0 deletions

View File

@ -0,0 +1,46 @@
@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);
}
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);
}
}
}
}

View File

@ -0,0 +1,175 @@
import { includes, isEmpty, isNil, merge } 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 { MaterialDesign2SchemeStorage } from '../../../material-2-scheme';
import { SchemeContent } from '../../../models';
import { useUpdateScheme } from '../../../stores/schemes';
import { mapToObject } from '../../../utls';
import { ColorEntry, IdenticalColorEntry } from '../ColorEntry';
import styles from './Builder.module.css';
type M2SchemeBuilderProps = {
scheme: SchemeContent<MaterialDesign2SchemeStorage>;
onBuildComplete?: () => void;
};
export function M2SchemeBuilder({ scheme, onBuildComplete }: M2SchemeBuilderProps) {
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 [errMsg, handleSubmitAction] = useActionState((state, formData) => {
const errMsg = new Map<string, string>();
try {
const primaryColor = formData.get('primary');
if (isNil(primaryColor) || isEmpty(primaryColor)) {
errMsg.set('primary', 'Primary color is required');
}
const secondaryColor = formData.get('secondary');
if (isNil(secondaryColor) || isEmpty(secondaryColor)) {
errMsg.set('secondary', 'Secondary color is required');
}
const errorColor = formData.get('error');
if (isNil(errorColor) || isEmpty(errorColor)) {
errMsg.set('error', 'Error color is required');
}
if (!isEmpty(errMsg)) return errMsg;
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 generatedScheme = colorFn?.generate_material_design_2_scheme(
primaryColor,
secondaryColor,
errorColor,
customColors,
);
updateScheme((prev) => {
prev.schemeStorage.source = {
primary: primaryColor,
secondary: secondaryColor,
error: errorColor,
custom_colors: customColors,
};
prev.schemeStorage.scheme = merge(generatedScheme[0], {
light: { custom_colors: mapToObject(generatedScheme[0].light.custom_colors) },
dark: { custom_colors: mapToObject(generatedScheme[0].dark.custom_colors) },
});
prev.schemeStorage.cssVariables = generatedScheme[1];
prev.schemeStorage.scssVariables = generatedScheme[2];
prev.schemeStorage.jsVariables = generatedScheme[3];
return prev;
});
onBuildComplete?.();
} catch (e) {
console.error('[generate m2 scheme]', 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}>Primary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="primary"
color={
isNil(scheme.schemeStorage.source?.primary) ||
isEmpty(scheme.schemeStorage.source?.primary)
? undefined
: scheme.schemeStorage.source.primary
}
/>
{errMsg.get('primary') && (
<span className={styles.error_msg}>{errMsg.get('primary')}</span>
)}
</div>
<label className={styles.label}>Secondary Color</label>
<div className={styles.color_picker_row}>
<FloatColorPicker
name="secondary"
color={
isNil(scheme.schemeStorage.source?.secondary) ||
isEmpty(scheme.schemeStorage.source.secondary)
? undefined
: scheme.schemeStorage.source.secondary
}
/>
{errMsg.get('secondary') && (
<span className={styles.error_msg}>{errMsg.get('secondary')}</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
}
/>
{errMsg.get('error') && <span className={styles.error_msg}>{errMsg.get('error')}</span>}
</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>
);
}