Compare commits
	
		
			6 Commits
		
	
	
		
			ef3ef2b349
			...
			2638bbd99a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2638bbd99a | ||
| 
						 | 
					f284a7ef62 | ||
| 
						 | 
					00d1e425c0 | ||
| 
						 | 
					0a5d475655 | ||
| 
						 | 
					a1f63cd724 | ||
| 
						 | 
					5f3d58f0f5 | 
@@ -23,6 +23,12 @@
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--spacing-s);
 | 
			
		||||
    }
 | 
			
		||||
    .button_row {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: row;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--spacing-s);
 | 
			
		||||
    }
 | 
			
		||||
    h5 {
 | 
			
		||||
      font-size: var(--font-size-m);
 | 
			
		||||
      line-height: 1.7em;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,12 @@ 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 { NotificationType, useNotification } from '../../../components/Notifications';
 | 
			
		||||
import { ScrollArea } from '../../../components/ScrollArea';
 | 
			
		||||
import { MaterialDesign2SchemeStorage } from '../../../material-2-scheme';
 | 
			
		||||
import {
 | 
			
		||||
  MaterialDesign2SchemeSource,
 | 
			
		||||
  MaterialDesign2SchemeStorage,
 | 
			
		||||
} from '../../../material-2-scheme';
 | 
			
		||||
import { SchemeContent } from '../../../models';
 | 
			
		||||
import { useUpdateScheme } from '../../../stores/schemes';
 | 
			
		||||
import { mapToObject } from '../../../utls';
 | 
			
		||||
@@ -16,6 +20,7 @@ type M2SchemeBuilderProps = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function M2SchemeBuilder({ scheme, onBuildComplete }: M2SchemeBuilderProps) {
 | 
			
		||||
  const { showToast } = useNotification();
 | 
			
		||||
  const { colorFn } = useColorFunction();
 | 
			
		||||
  const updateScheme = useUpdateScheme(scheme.id);
 | 
			
		||||
  const originalColors = useMemo(() => {
 | 
			
		||||
@@ -35,45 +40,67 @@ export function M2SchemeBuilder({ scheme, onBuildComplete }: M2SchemeBuilderProp
 | 
			
		||||
        .filter((c) => !includes(deleted, c)),
 | 
			
		||||
    [originalColors, newColors, deleted],
 | 
			
		||||
  );
 | 
			
		||||
  const colectSchemeSource = (formData: FormData): MaterialDesign2SchemeSource => {
 | 
			
		||||
    const primaryColor = formData.get('primary') as string;
 | 
			
		||||
    const secondaryColor = formData.get('secondary') as string;
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      primary: isNil(primaryColor) || isEmpty(primaryColor) ? null : primaryColor,
 | 
			
		||||
      secondary: isNil(secondaryColor) || isEmpty(secondaryColor) ? null : secondaryColor,
 | 
			
		||||
      error: isNil(errorColor) || isEmpty(errorColor) ? null : errorColor,
 | 
			
		||||
      custom_colors: customColors,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [, handleDraftAction] = useActionState<Map<string, string>, FormData>(
 | 
			
		||||
    (_state, formData) => {
 | 
			
		||||
      const errMsg = new Map<string, string>();
 | 
			
		||||
 | 
			
		||||
      const collectedSource = colectSchemeSource(formData);
 | 
			
		||||
      updateScheme((prev) => {
 | 
			
		||||
        prev.schemeStorage.source = collectedSource;
 | 
			
		||||
        return prev;
 | 
			
		||||
      });
 | 
			
		||||
      setNewColors([]);
 | 
			
		||||
 | 
			
		||||
      showToast(NotificationType.SUCCESS, 'Scheme draft saved!', 'tabler:device-floppy', 3000);
 | 
			
		||||
 | 
			
		||||
      return errMsg;
 | 
			
		||||
    },
 | 
			
		||||
    new Map<string, string>(),
 | 
			
		||||
  );
 | 
			
		||||
  const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
 | 
			
		||||
    (_state, formData) => {
 | 
			
		||||
      const errMsg = new Map<string, string>();
 | 
			
		||||
      try {
 | 
			
		||||
        const primaryColor = formData.get('primary') as string;
 | 
			
		||||
        if (isNil(primaryColor) || isEmpty(primaryColor)) {
 | 
			
		||||
        const collected = colectSchemeSource(formData);
 | 
			
		||||
        if (isNil(collected.primary) || isEmpty(collected.primary)) {
 | 
			
		||||
          errMsg.set('primary', 'Primary color is required');
 | 
			
		||||
        }
 | 
			
		||||
        const secondaryColor = formData.get('secondary') as string;
 | 
			
		||||
        if (isNil(secondaryColor) || isEmpty(secondaryColor)) {
 | 
			
		||||
        if (isNil(collected.secondary) || isEmpty(collected.secondary)) {
 | 
			
		||||
          errMsg.set('secondary', 'Secondary color is required');
 | 
			
		||||
        }
 | 
			
		||||
        const errorColor = formData.get('error') as string;
 | 
			
		||||
        if (isNil(errorColor) || isEmpty(errorColor)) {
 | 
			
		||||
        if (isNil(collected.error) || isEmpty(collected.error)) {
 | 
			
		||||
          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,
 | 
			
		||||
          collected.primary,
 | 
			
		||||
          collected.secondary,
 | 
			
		||||
          collected.error,
 | 
			
		||||
          collected.custom_colors,
 | 
			
		||||
        );
 | 
			
		||||
        updateScheme((prev) => {
 | 
			
		||||
          prev.schemeStorage.source = {
 | 
			
		||||
            primary: primaryColor,
 | 
			
		||||
            secondary: secondaryColor,
 | 
			
		||||
            error: errorColor,
 | 
			
		||||
            custom_colors: customColors,
 | 
			
		||||
          };
 | 
			
		||||
          prev.schemeStorage.source = collected;
 | 
			
		||||
          prev.schemeStorage.scheme = merge(generatedScheme[0], {
 | 
			
		||||
            light: { custom_colors: mapToObject(generatedScheme[0].light.custom_colors) },
 | 
			
		||||
            dark: { custom_colors: mapToObject(generatedScheme[0].dark.custom_colors) },
 | 
			
		||||
@@ -168,10 +195,13 @@ export function M2SchemeBuilder({ scheme, onBuildComplete }: M2SchemeBuilderProp
 | 
			
		||||
              onDelete={(index) => setDeleted((prev) => [...prev, index])}
 | 
			
		||||
            />
 | 
			
		||||
          ))}
 | 
			
		||||
        <div style={{ gridColumn: '2 / span 2' }}>
 | 
			
		||||
        <div className={styles.button_row} style={{ gridColumn: '2 / span 2' }}>
 | 
			
		||||
          <button type="submit" className="primary">
 | 
			
		||||
            Build Scheme
 | 
			
		||||
          </button>
 | 
			
		||||
          <button type="submit" className="secondary" formAction={handleDraftAction}>
 | 
			
		||||
            Save Draft
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </ScrollArea>
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,12 @@
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--spacing-s);
 | 
			
		||||
    }
 | 
			
		||||
    .button_row {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: row;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--spacing-s);
 | 
			
		||||
    }
 | 
			
		||||
    h5 {
 | 
			
		||||
      font-size: var(--font-size-m);
 | 
			
		||||
      line-height: 1.7em;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,14 @@ 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 { NotificationType, useNotification } from '../../../components/Notifications';
 | 
			
		||||
import { ScrollArea } from '../../../components/ScrollArea';
 | 
			
		||||
import { Switch } from '../../../components/Switch';
 | 
			
		||||
import { VSegmentedControl } from '../../../components/VSegmentedControl';
 | 
			
		||||
import { MaterialDesign3DynamicSchemeStorage } from '../../../material-3-scheme';
 | 
			
		||||
import {
 | 
			
		||||
  MaterialDesign3DynamicSchemeSource,
 | 
			
		||||
  MaterialDesign3DynamicSchemeStorage,
 | 
			
		||||
} from '../../../material-3-scheme';
 | 
			
		||||
import { Option, SchemeContent } from '../../../models';
 | 
			
		||||
import { useUpdateScheme } from '../../../stores/schemes';
 | 
			
		||||
import { mapToObject } from '../../../utls';
 | 
			
		||||
@@ -18,6 +22,7 @@ type M3DynamicSchemeBuilderProps = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function M3DynamicSchemeBuilder({ scheme, onBuildCompleted }: M3DynamicSchemeBuilderProps) {
 | 
			
		||||
  const { showToast } = useNotification();
 | 
			
		||||
  const { colorFn } = useColorFunction();
 | 
			
		||||
  const updateScheme = useUpdateScheme(scheme.id);
 | 
			
		||||
  const originalColors = useMemo(() => {
 | 
			
		||||
@@ -49,47 +54,69 @@ export function M3DynamicSchemeBuilder({ scheme, onBuildCompleted }: M3DynamicSc
 | 
			
		||||
  const [contrastLevel, setContrastLevel] = useState<number>(
 | 
			
		||||
    () => scheme.schemeStorage.source?.contrastLevel ?? 1,
 | 
			
		||||
  );
 | 
			
		||||
  const collectSchemeSource = (formData: FormData): MaterialDesign3DynamicSchemeSource => {
 | 
			
		||||
    const sourceColor = formData.get('source') as string;
 | 
			
		||||
    const dynamicVariant = Number(formData.get('variant'));
 | 
			
		||||
    const contrast = 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      source: isNil(sourceColor) || isEmpty(sourceColor) ? null : sourceColor,
 | 
			
		||||
      error: isNil(errorColor) || isEmpty(errorColor) ? null : errorColor,
 | 
			
		||||
      custom_colors: customColors,
 | 
			
		||||
      variant: dynamicVariant,
 | 
			
		||||
      contrastLevel: contrast,
 | 
			
		||||
      harmonizeCustoms: harmonizeCustoms,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [, handleDraftAction] = useActionState<Map<string, string>, FormData>(
 | 
			
		||||
    (_state, formData) => {
 | 
			
		||||
      const errMsg = new Map<string, string>();
 | 
			
		||||
 | 
			
		||||
      const collectedSource = collectSchemeSource(formData);
 | 
			
		||||
 | 
			
		||||
      updateScheme((prev) => {
 | 
			
		||||
        prev.schemeStorage.source = collectedSource;
 | 
			
		||||
        return prev;
 | 
			
		||||
      });
 | 
			
		||||
      setNewColors([]);
 | 
			
		||||
 | 
			
		||||
      showToast(NotificationType.SUCCESS, 'Scheme draft saved!', 'tabler:device-floppy', 3000);
 | 
			
		||||
 | 
			
		||||
      return errMsg;
 | 
			
		||||
    },
 | 
			
		||||
    new Map<string, string>(),
 | 
			
		||||
  );
 | 
			
		||||
  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)) {
 | 
			
		||||
      const collectedSource = collectSchemeSource(formData);
 | 
			
		||||
      if (isNil(collectedSource.source) || isEmpty(collectedSource.source)) {
 | 
			
		||||
        errMsg.set('source', 'Source color is required');
 | 
			
		||||
      }
 | 
			
		||||
      if (!isEmpty(errMsg)) return errMsg;
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const dynamicVariant = Number(formData.get('variant'));
 | 
			
		||||
        const contrast = 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,
 | 
			
		||||
          collectedSource.source,
 | 
			
		||||
          collectedSource.error,
 | 
			
		||||
          collectedSource.variant,
 | 
			
		||||
          collectedSource.contrastLevel,
 | 
			
		||||
          collectedSource.harmonizeCustoms,
 | 
			
		||||
          collectedSource.custom_colors,
 | 
			
		||||
        );
 | 
			
		||||
        updateScheme((prev) => {
 | 
			
		||||
          prev.schemeStorage.source = {
 | 
			
		||||
            source: sourceColor,
 | 
			
		||||
            error: errorColor,
 | 
			
		||||
            custom_colors: customColors,
 | 
			
		||||
            variant: dynamicVariant,
 | 
			
		||||
            contrastLevel: contrast,
 | 
			
		||||
            harmonizeCustoms: harmonizeCustoms,
 | 
			
		||||
          };
 | 
			
		||||
          prev.schemeStorage.source = collectedSource;
 | 
			
		||||
          prev.schemeStorage.scheme = {
 | 
			
		||||
            white: generate_scheme[0].white,
 | 
			
		||||
            black: generate_scheme[0].black,
 | 
			
		||||
@@ -204,10 +231,13 @@ export function M3DynamicSchemeBuilder({ scheme, onBuildCompleted }: M3DynamicSc
 | 
			
		||||
              onDelete={(index) => setDeleted((prev) => [...prev, index])}
 | 
			
		||||
            />
 | 
			
		||||
          ))}
 | 
			
		||||
        <div style={{ gridColumn: '2 / span 2' }}>
 | 
			
		||||
        <div className={styles.button_row} style={{ gridColumn: '2 / span 2' }}>
 | 
			
		||||
          <button type="submit" className="primary">
 | 
			
		||||
            Build Scheme
 | 
			
		||||
          </button>
 | 
			
		||||
          <button type="submit" className="secondary" formAction={handleDraftAction}>
 | 
			
		||||
            Save Draft
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </ScrollArea>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,12 @@
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--spacing-s);
 | 
			
		||||
    }
 | 
			
		||||
    .button_row {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: row;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--spacing-s);
 | 
			
		||||
    }
 | 
			
		||||
    h5 {
 | 
			
		||||
      font-size: var(--font-size-m);
 | 
			
		||||
      line-height: 1.7em;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,13 @@ import { includes, isEmpty, isNil } from 'lodash-es';
 | 
			
		||||
import { useActionState, useCallback, useMemo, useState } from 'react';
 | 
			
		||||
import { useColorFunction } from '../../../ColorFunctionContext';
 | 
			
		||||
import { FloatColorPicker } from '../../../components/FloatColorPicker';
 | 
			
		||||
import { NotificationType, useNotification } from '../../../components/Notifications';
 | 
			
		||||
import { ScrollArea } from '../../../components/ScrollArea';
 | 
			
		||||
import { MaterialDesign3Scheme, MaterialDesign3SchemeStorage } from '../../../material-3-scheme';
 | 
			
		||||
import {
 | 
			
		||||
  MaterialDesign3Scheme,
 | 
			
		||||
  MaterialDesign3SchemeSource,
 | 
			
		||||
  MaterialDesign3SchemeStorage,
 | 
			
		||||
} from '../../../material-3-scheme';
 | 
			
		||||
import { SchemeContent } from '../../../models';
 | 
			
		||||
import { useUpdateScheme } from '../../../stores/schemes';
 | 
			
		||||
import { mapToObject } from '../../../utls';
 | 
			
		||||
@@ -16,6 +21,7 @@ type M3SchemeBuilderProps = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function M3SchemeBuilder({ scheme, onBuildCompleted }: M3SchemeBuilderProps) {
 | 
			
		||||
  const { showToast } = useNotification();
 | 
			
		||||
  const { colorFn } = useColorFunction();
 | 
			
		||||
  const updateScheme = useUpdateScheme(scheme.id);
 | 
			
		||||
  const originalColors = useMemo(() => {
 | 
			
		||||
@@ -36,40 +42,62 @@ export function M3SchemeBuilder({ scheme, onBuildCompleted }: M3SchemeBuilderPro
 | 
			
		||||
    [originalColors, newColors, deleted],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const collectSchemeSource = (formData: FormData): MaterialDesign3SchemeSource => {
 | 
			
		||||
    const sourceColor = formData.get('source') as string;
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      source: isNil(sourceColor) || isEmpty(sourceColor) ? null : sourceColor,
 | 
			
		||||
      error: isNil(errorColor) || isEmpty(errorColor) ? null : errorColor,
 | 
			
		||||
      custom_colors: customColors,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [, handleDraftAction] = useActionState<Map<string, string>, FormData>(
 | 
			
		||||
    (_state, formData) => {
 | 
			
		||||
      const errMsg = new Map<string, string>();
 | 
			
		||||
 | 
			
		||||
      const collectedSource = collectSchemeSource(formData);
 | 
			
		||||
      updateScheme((prev) => {
 | 
			
		||||
        prev.schemeStorage.source = collectedSource;
 | 
			
		||||
        return prev;
 | 
			
		||||
      });
 | 
			
		||||
      setNewColors([]);
 | 
			
		||||
 | 
			
		||||
      showToast(NotificationType.SUCCESS, 'Scheme draft saved!', 'tabler:device-floppy', 3000);
 | 
			
		||||
 | 
			
		||||
      return errMsg;
 | 
			
		||||
    },
 | 
			
		||||
    new Map<string, string>(),
 | 
			
		||||
  );
 | 
			
		||||
  const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
 | 
			
		||||
    (_state, formData) => {
 | 
			
		||||
      const errMsg = new Map<string, string>();
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const sourceColor = formData.get('source') as string;
 | 
			
		||||
        if (isNil(sourceColor) || isEmpty(sourceColor)) {
 | 
			
		||||
        const collectedSource = collectSchemeSource(formData);
 | 
			
		||||
        if (isNil(collectedSource.source) || isEmpty(collectedSource.source)) {
 | 
			
		||||
          errMsg.set('source', 'Source color is required');
 | 
			
		||||
        }
 | 
			
		||||
        const errorColor = formData.get('error') as string;
 | 
			
		||||
        if (isNil(errorColor) || isEmpty(errorColor)) {
 | 
			
		||||
        if (isNil(collectedSource.error) || isEmpty(collectedSource.error)) {
 | 
			
		||||
          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_3_scheme(
 | 
			
		||||
          sourceColor,
 | 
			
		||||
          errorColor,
 | 
			
		||||
          customColors,
 | 
			
		||||
          collectedSource.source,
 | 
			
		||||
          collectedSource.error,
 | 
			
		||||
          collectedSource.custom_colors,
 | 
			
		||||
        );
 | 
			
		||||
        updateScheme((prev) => {
 | 
			
		||||
          prev.schemeStorage.source = {
 | 
			
		||||
            source: sourceColor as string,
 | 
			
		||||
            error: errorColor as string,
 | 
			
		||||
            custom_colors: customColors,
 | 
			
		||||
          };
 | 
			
		||||
          prev.schemeStorage.source = collectedSource;
 | 
			
		||||
          prev.schemeStorage.scheme = {
 | 
			
		||||
            white: generatedScheme[0].white,
 | 
			
		||||
            black: generatedScheme[0].black,
 | 
			
		||||
@@ -155,10 +183,13 @@ export function M3SchemeBuilder({ scheme, onBuildCompleted }: M3SchemeBuilderPro
 | 
			
		||||
              onDelete={(index) => setDeleted((prev) => [...prev, index])}
 | 
			
		||||
            />
 | 
			
		||||
          ))}
 | 
			
		||||
        <div style={{ gridColumn: '2 / span 2' }}>
 | 
			
		||||
        <div className={styles.button_row} style={{ gridColumn: '2 / span 2' }}>
 | 
			
		||||
          <button type="submit" className="primary">
 | 
			
		||||
            Build Scheme
 | 
			
		||||
          </button>
 | 
			
		||||
          <button type="submit" className="secondary" formAction={handleDraftAction}>
 | 
			
		||||
            Save Draft
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </ScrollArea>
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,12 @@
 | 
			
		||||
    .parameter_input {
 | 
			
		||||
      max-width: 8em;
 | 
			
		||||
    }
 | 
			
		||||
    .button_row {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: row;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--spacing-s);
 | 
			
		||||
    }
 | 
			
		||||
    h5 {
 | 
			
		||||
      font-size: var(--font-size-m);
 | 
			
		||||
      line-height: 1.7em;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import { ColorShifting, SwatchEntry, SwatchSchemeSetting } from 'color-module';
 | 
			
		||||
import { includes, isEmpty, isEqual, isNaN } from 'lodash-es';
 | 
			
		||||
import { useActionState, useCallback, useMemo, useState } from 'react';
 | 
			
		||||
import { useColorFunction } from '../../../ColorFunctionContext';
 | 
			
		||||
import { NotificationType, useNotification } from '../../../components/Notifications';
 | 
			
		||||
import { ScrollArea } from '../../../components/ScrollArea';
 | 
			
		||||
import { Switch } from '../../../components/Switch';
 | 
			
		||||
import { SchemeContent } from '../../../models';
 | 
			
		||||
@@ -10,6 +11,7 @@ import {
 | 
			
		||||
  QSwatchEntry,
 | 
			
		||||
  QSwatchSchemeSetting,
 | 
			
		||||
  SwatchScheme,
 | 
			
		||||
  SwatchSchemeSource,
 | 
			
		||||
  SwatchSchemeStorage,
 | 
			
		||||
} from '../../../swatch_scheme';
 | 
			
		||||
import { mapToObject } from '../../../utls';
 | 
			
		||||
@@ -22,6 +24,7 @@ type SwatchSchemeBuilderProps = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function SwatchSchemeBuilder({ scheme, onBuildCompleted }: SwatchSchemeBuilderProps) {
 | 
			
		||||
  const { showToast } = useNotification();
 | 
			
		||||
  const { colorFn } = useColorFunction();
 | 
			
		||||
  const updateScheme = useUpdateScheme(scheme.id);
 | 
			
		||||
  const originalColors = useMemo(() => {
 | 
			
		||||
@@ -64,63 +67,95 @@ export function SwatchSchemeBuilder({ scheme, onBuildCompleted }: SwatchSchemeBu
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }, [scheme.schemeStorage.source]);
 | 
			
		||||
  const collectSchemeSource = (formData: FormData): SwatchSchemeSource => {
 | 
			
		||||
    const swatchAmount = Number(formData.get('amount'));
 | 
			
		||||
    const minLightness = Number(formData.get('min_lightness'));
 | 
			
		||||
    const maxLightness = Number(formData.get('max_lightness'));
 | 
			
		||||
    const includePrimary = isEqual(formData.get('include_primary'), 'true');
 | 
			
		||||
    const darkConvertChroma = Number(formData.get('dark_chroma')) / 100.0;
 | 
			
		||||
    const darkConvertLightness = Number(formData.get('dark_lightness')) / 100.0;
 | 
			
		||||
 | 
			
		||||
    const swatchSetting = new SwatchSchemeSetting(
 | 
			
		||||
      swatchAmount,
 | 
			
		||||
      minLightness / 100.0,
 | 
			
		||||
      maxLightness / 100.0,
 | 
			
		||||
      includePrimary,
 | 
			
		||||
      new ColorShifting(darkConvertChroma, darkConvertLightness),
 | 
			
		||||
    );
 | 
			
		||||
    const dumpedSettings = swatchSetting.toJsValue() as QSwatchSchemeSetting;
 | 
			
		||||
    const entries: SwatchEntry[] = [];
 | 
			
		||||
    for (const key of colorKeys) {
 | 
			
		||||
      const name = String(formData.get(`name_${key}`));
 | 
			
		||||
      const color = String(formData.get(`color_${key}`));
 | 
			
		||||
      if (isEmpty(name) || isEmpty(color)) continue;
 | 
			
		||||
      entries.push(new SwatchEntry(name, color));
 | 
			
		||||
    }
 | 
			
		||||
    const dumpedEntries = entries.map((entry) => entry.toJsValue() as QSwatchEntry);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      colors: dumpedEntries,
 | 
			
		||||
      setting: dumpedSettings,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [, handleDraftAction] = useActionState<Map<string, string>, FormData>(
 | 
			
		||||
    (_state, formData) => {
 | 
			
		||||
      const errMsg = new Map<string, string>();
 | 
			
		||||
 | 
			
		||||
      const collected = collectSchemeSource(formData);
 | 
			
		||||
      updateScheme((prev) => {
 | 
			
		||||
        prev.schemeStorage.source = collected;
 | 
			
		||||
        return prev;
 | 
			
		||||
      });
 | 
			
		||||
      setNewColors([]);
 | 
			
		||||
 | 
			
		||||
      showToast(NotificationType.SUCCESS, 'Scheme draft saved!', 'tabler:device-floppy', 3000);
 | 
			
		||||
 | 
			
		||||
      return errMsg;
 | 
			
		||||
    },
 | 
			
		||||
    new Map<string, string>(),
 | 
			
		||||
  );
 | 
			
		||||
  const [errMsg, handleSubmitAction] = useActionState<Map<string, string>, FormData>(
 | 
			
		||||
    (_state, formData) => {
 | 
			
		||||
      const errMsg = new Map<string, string>();
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const swatchAmount = Number(formData.get('amount'));
 | 
			
		||||
        if (isNaN(swatchAmount) || swatchAmount <= 0) {
 | 
			
		||||
        const collected = collectSchemeSource(formData);
 | 
			
		||||
        if (isNaN(collected.setting.amount) || collected.setting.amount <= 0) {
 | 
			
		||||
          errMsg.set('amount', 'MUST be a positive number');
 | 
			
		||||
        }
 | 
			
		||||
        if (swatchAmount > 30) {
 | 
			
		||||
        if (collected.setting.amount > 30) {
 | 
			
		||||
          errMsg.set('amount', 'MUST be less than 30');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const minLightness = Number(formData.get('min_lightness'));
 | 
			
		||||
        if (isNaN(minLightness) || minLightness < 0 || minLightness > 100) {
 | 
			
		||||
        if (
 | 
			
		||||
          isNaN(collected.setting.min_lightness) ||
 | 
			
		||||
          collected.setting.min_lightness < 0 ||
 | 
			
		||||
          collected.setting.min_lightness > 1.0
 | 
			
		||||
        ) {
 | 
			
		||||
          errMsg.set('min', 'MUST be a number between 0 and 100');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const maxLightness = Number(formData.get('max_lightness'));
 | 
			
		||||
        if (isNaN(maxLightness) || maxLightness < 0 || maxLightness > 100) {
 | 
			
		||||
        if (
 | 
			
		||||
          isNaN(collected.setting.max_lightness) ||
 | 
			
		||||
          collected.setting.max_lightness < 0 ||
 | 
			
		||||
          collected.setting.max_lightness > 1.0
 | 
			
		||||
        ) {
 | 
			
		||||
          errMsg.set('max', 'MUST be a number between 0 and 100');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const includePrimary = isEqual(formData.get('include_primary'), 'true');
 | 
			
		||||
        const darkConvertChroma = Number(formData.get('dark_chroma')) / 100.0;
 | 
			
		||||
        const darkConvertLightness = Number(formData.get('dark_lightness')) / 100.0;
 | 
			
		||||
 | 
			
		||||
        const swatchSetting = new SwatchSchemeSetting(
 | 
			
		||||
          swatchAmount,
 | 
			
		||||
          minLightness / 100.0,
 | 
			
		||||
          maxLightness / 100.0,
 | 
			
		||||
          includePrimary,
 | 
			
		||||
          new ColorShifting(darkConvertChroma, darkConvertLightness),
 | 
			
		||||
        );
 | 
			
		||||
        const dumpedSettings = swatchSetting.toJsValue() as QSwatchSchemeSetting;
 | 
			
		||||
        const entries: SwatchEntry[] = [];
 | 
			
		||||
        for (const key of colorKeys) {
 | 
			
		||||
          const name = String(formData.get(`name_${key}`));
 | 
			
		||||
          const color = String(formData.get(`color_${key}`));
 | 
			
		||||
          if (isEmpty(name) || isEmpty(color)) continue;
 | 
			
		||||
          entries.push(new SwatchEntry(name, color));
 | 
			
		||||
        }
 | 
			
		||||
        const dumpedEntries = entries.map((entry) => entry.toJsValue() as QSwatchEntry);
 | 
			
		||||
        if (isEmpty(entries)) {
 | 
			
		||||
        if (isEmpty(collected.colors)) {
 | 
			
		||||
          errMsg.set('color', 'At least one color is required');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isEmpty(errMsg)) return errMsg;
 | 
			
		||||
 | 
			
		||||
        const generatedScheme = colorFn?.generate_swatch_scheme(entries, swatchSetting);
 | 
			
		||||
        console.debug('[generated scheme]', generatedScheme);
 | 
			
		||||
        const generatedScheme = colorFn?.generate_swatch_scheme(
 | 
			
		||||
          collected.colors,
 | 
			
		||||
          collected.setting,
 | 
			
		||||
        );
 | 
			
		||||
        updateScheme((prev) => {
 | 
			
		||||
          prev.schemeStorage.source = {
 | 
			
		||||
            colors: dumpedEntries,
 | 
			
		||||
            setting: dumpedSettings,
 | 
			
		||||
          };
 | 
			
		||||
          prev.schemeStorage.source = collected;
 | 
			
		||||
          prev.schemeStorage.scheme = mapToObject(generatedScheme[0]) as SwatchScheme;
 | 
			
		||||
          prev.schemeStorage.cssVariables = generatedScheme[1];
 | 
			
		||||
          prev.schemeStorage.cssAutoSchemeVariables = generatedScheme[2];
 | 
			
		||||
@@ -237,10 +272,13 @@ export function SwatchSchemeBuilder({ scheme, onBuildCompleted }: SwatchSchemeBu
 | 
			
		||||
            <span className={styles.error_msg}>{errMsg.get('color')}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        <div style={{ gridColumn: '2 / span 2' }}>
 | 
			
		||||
        <div className={styles.button_row} style={{ gridColumn: '2 / span 2' }}>
 | 
			
		||||
          <button type="submit" className="primary">
 | 
			
		||||
            Build Scheme
 | 
			
		||||
          </button>
 | 
			
		||||
          <button type="submit" className="secondary" formAction={handleDraftAction}>
 | 
			
		||||
            Save Draft
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </ScrollArea>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user