@umituz/react-native-video-editor 1.1.63 → 1.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/application/services/EditorService.ts +151 -0
  3. package/src/application/usecases/LayerUseCases.ts +192 -0
  4. package/src/infrastructure/repositories/LayerRepositoryImpl.ts +158 -0
  5. package/src/infrastructure/services/image-layer-operations.service.ts +1 -1
  6. package/src/infrastructure/services/shape-layer-operations.service.ts +1 -1
  7. package/src/infrastructure/services/text-layer-operations.service.ts +1 -1
  8. package/src/infrastructure/utils/debounce.utils.ts +69 -0
  9. package/src/infrastructure/utils/image-processing.utils.ts +73 -0
  10. package/src/infrastructure/utils/position-calculations.utils.ts +45 -161
  11. package/src/presentation/components/EditorTimeline.tsx +29 -11
  12. package/src/presentation/components/EditorToolPanel.tsx +71 -172
  13. package/src/presentation/components/LayerActionsMenu.tsx +97 -159
  14. package/src/presentation/components/SceneActionsMenu.tsx +34 -44
  15. package/src/presentation/components/SubtitleListPanel.tsx +55 -28
  16. package/src/presentation/components/SubtitleStylePicker.tsx +36 -156
  17. package/src/presentation/components/collage/CollageCanvas.tsx +2 -2
  18. package/src/presentation/components/collage/CollageLayoutSelector.tsx +0 -4
  19. package/src/presentation/components/draggable-layer/LayerContent.tsx +7 -2
  20. package/src/presentation/components/generic/ActionMenu.tsx +110 -0
  21. package/src/presentation/components/generic/Editor.tsx +65 -0
  22. package/src/presentation/components/generic/Selector.tsx +96 -0
  23. package/src/presentation/components/generic/Toolbar.tsx +77 -0
  24. package/src/presentation/components/image-layer/ImagePreview.tsx +7 -1
  25. package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +20 -55
  26. package/src/presentation/components/subtitle/SubtitleListItem.tsx +4 -5
  27. package/src/presentation/components/subtitle/SubtitleModal.tsx +2 -2
  28. package/src/presentation/components/subtitle/useSubtitleForm.ts +1 -1
  29. package/src/presentation/components/text-layer/ColorPicker.tsx +21 -55
  30. package/src/presentation/components/text-layer/FontSizeSelector.tsx +19 -50
  31. package/src/presentation/components/text-layer/OptionSelector.tsx +18 -55
  32. package/src/presentation/components/text-layer/TextAlignSelector.tsx +24 -51
  33. package/src/presentation/hooks/generic/use-layer-form.hook.ts +19 -16
  34. package/src/presentation/hooks/generic/useForm.ts +99 -0
  35. package/src/presentation/hooks/generic/useList.ts +117 -0
  36. package/src/presentation/hooks/useDraggableLayerGestures.ts +28 -25
  37. package/src/presentation/hooks/useEditorPlayback.ts +19 -2
  38. package/src/presentation/hooks/useImageLayerForm.ts +9 -6
  39. package/src/presentation/hooks/useMenuActions.tsx +19 -4
  40. package/src/presentation/hooks/useTextLayerForm.ts +9 -6
@@ -1,12 +1,14 @@
1
1
  /**
2
- * ColorPickerHorizontal Component
2
+ * Color Picker Horizontal Component
3
3
  * Horizontal scrolling color picker for shape layer
4
+ * REFACTORED: Uses generic Selector with colorPreview mode (28 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, ScrollView, StyleSheet, TouchableOpacity } from "react-native";
8
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
7
+ import React, { useMemo } from "react";
8
+ import { View } from "react-native";
9
+ import { AtomicText } from "@umituz/react-native-design-system/atoms";
9
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { Selector, type SelectorItem } from "../generic/Selector";
10
12
  import { SHAPE_COLORS } from "../../../infrastructure/constants/shape-layer.constants";
11
13
 
12
14
  interface ColorPickerHorizontalProps {
@@ -22,8 +24,13 @@ export const ColorPickerHorizontal: React.FC<ColorPickerHorizontalProps> = ({
22
24
  }) => {
23
25
  const tokens = useAppDesignTokens();
24
26
 
27
+ const items = useMemo<SelectorItem[]>(
28
+ () => SHAPE_COLORS.map((color) => ({ value: color.value, label: "", color: color.value })),
29
+ [],
30
+ );
31
+
25
32
  return (
26
- <View style={styles.section}>
33
+ <View style={{ marginBottom: tokens.spacing.md }}>
27
34
  <AtomicText
28
35
  type="bodyMedium"
29
36
  style={{
@@ -34,56 +41,14 @@ export const ColorPickerHorizontal: React.FC<ColorPickerHorizontalProps> = ({
34
41
  >
35
42
  {title}
36
43
  </AtomicText>
37
-
38
- <ScrollView
39
- horizontal
40
- showsHorizontalScrollIndicator={false}
41
- style={styles.colorsScroll}
42
- >
43
- {SHAPE_COLORS.map((color) => (
44
- <TouchableOpacity
45
- key={color.value}
46
- style={[
47
- styles.colorButton,
48
- {
49
- backgroundColor: color.value,
50
- borderColor:
51
- selectedColor === color.value
52
- ? tokens.colors.primary
53
- : tokens.colors.borderLight,
54
- borderWidth: selectedColor === color.value ? 3 : 2,
55
- },
56
- ]}
57
- onPress={() => onColorChange(color.value)}
58
- >
59
- {selectedColor === color.value && (
60
- <AtomicIcon
61
- name="checkmark-outline"
62
- size="sm"
63
- color={color.value === "#FFFFFF" ? "primary" : "onSurface"}
64
- />
65
- )}
66
- </TouchableOpacity>
67
- ))}
68
- </ScrollView>
44
+ <Selector
45
+ items={items}
46
+ selectedValue={selectedColor}
47
+ onSelect={onColorChange}
48
+ orientation="horizontal"
49
+ colorPreview
50
+ testID="color-picker-horizontal"
51
+ />
69
52
  </View>
70
53
  );
71
54
  };
72
-
73
- const styles = StyleSheet.create({
74
- section: {
75
- marginBottom: 24,
76
- },
77
- colorsScroll: {
78
- marginHorizontal: -16,
79
- paddingHorizontal: 16,
80
- },
81
- colorButton: {
82
- width: 50,
83
- height: 50,
84
- borderRadius: 25,
85
- marginRight: 12,
86
- alignItems: "center",
87
- justifyContent: "center",
88
- },
89
- });
@@ -29,7 +29,6 @@ export const SubtitleListItem: React.FC<SubtitleListItemProps> = ({
29
29
  item: {
30
30
  flexDirection: "row" as const,
31
31
  alignItems: "center" as const,
32
- backgroundColor: tokens.colors.surface,
33
32
  borderRadius: tokens.borders.radius.md,
34
33
  marginHorizontal: tokens.spacing.md,
35
34
  marginBottom: tokens.spacing.sm,
@@ -70,17 +69,17 @@ export const SubtitleListItem: React.FC<SubtitleListItemProps> = ({
70
69
  activeOpacity={0.7}
71
70
  >
72
71
  <View style={styles.itemTimeRow}>
73
- <AtomicText variant="caption" color="textSecondary">
72
+ <AtomicText color="textSecondary">
74
73
  {formatTimeDetailed(subtitle.startTime)}
75
74
  </AtomicText>
76
- <AtomicText variant="caption" color="textSecondary">
75
+ <AtomicText color="textSecondary">
77
76
 
78
77
  </AtomicText>
79
- <AtomicText variant="caption" color="textSecondary">
78
+ <AtomicText color="textSecondary">
80
79
  {formatTimeDetailed(subtitle.endTime)}
81
80
  </AtomicText>
82
81
  </View>
83
- <AtomicText variant="body" color="textPrimary" numberOfLines={2}>
82
+ <AtomicText color="textPrimary" numberOfLines={2}>
84
83
  {subtitle.text}
85
84
  </AtomicText>
86
85
  </TouchableOpacity>
@@ -55,7 +55,7 @@ export const SubtitleModal: React.FC<SubtitleModalProps> = ({
55
55
  paddingHorizontal: tokens.spacing.md,
56
56
  paddingTop: tokens.spacing.md,
57
57
  paddingBottom: tokens.spacing.xl,
58
- maxHeight: "90%",
58
+ maxHeight: "90%" as const,
59
59
  },
60
60
  handle: {
61
61
  width: 36,
@@ -136,7 +136,7 @@ export const SubtitleModal: React.FC<SubtitleModalProps> = ({
136
136
  />
137
137
  </View>
138
138
 
139
- <SubtitleStylePicker value={style} onChange={onChangeStyle} />
139
+ <SubtitleStylePicker style={style} previewText={text} onChange={onChangeStyle} t={(key) => key} />
140
140
 
141
141
  <View style={styles.actionRow}>
142
142
  <TouchableOpacity style={styles.cancelBtn} onPress={onCancel}>
@@ -3,7 +3,7 @@
3
3
  * Manages subtitle form state and operations
4
4
  */
5
5
 
6
- import { useState, useCallback, useMemo } from "react";
6
+ import { useState, useCallback } from "react";
7
7
  import { DEFAULT_SUBTITLE_STYLE } from "../../../infrastructure/constants/subtitle.constants";
8
8
  import type { Subtitle, SubtitleStyle } from "../../../domain/entities/video-project.types";
9
9
 
@@ -1,13 +1,15 @@
1
1
  /**
2
- * ColorPicker Component
2
+ * Color Picker Component
3
3
  * Color picker for text layer
4
+ * REFACTORED: Uses generic Selector with colorPreview mode (35 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, StyleSheet, TouchableOpacity } from "react-native";
8
- import { useLocalization } from "@umituz/react-native-settings";
9
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
7
+ import React, { useMemo } from "react";
8
+ import { View } from "react-native";
9
+ import { AtomicText } from "@umituz/react-native-design-system/atoms";
10
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { useLocalization } from "@umituz/react-native-settings";
12
+ import { Selector, type SelectorItem } from "../generic/Selector";
11
13
  import { TEXT_COLORS } from "../../../infrastructure/constants/text-layer.constants";
12
14
 
13
15
  interface ColorPickerProps {
@@ -22,8 +24,13 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
22
24
  const { t } = useLocalization();
23
25
  const tokens = useAppDesignTokens();
24
26
 
27
+ const items = useMemo<SelectorItem[]>(
28
+ () => TEXT_COLORS.map((color) => ({ value: color, label: "", color })),
29
+ [],
30
+ );
31
+
25
32
  return (
26
- <View style={styles.section}>
33
+ <View style={{ marginBottom: tokens.spacing.md }}>
27
34
  <AtomicText
28
35
  type="bodyMedium"
29
36
  style={{
@@ -34,55 +41,14 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
34
41
  >
35
42
  {t("editor.properties.color")}
36
43
  </AtomicText>
37
- <View style={styles.colorGrid}>
38
- {TEXT_COLORS.map((color) => (
39
- <TouchableOpacity
40
- key={color}
41
- style={[
42
- styles.colorButton,
43
- {
44
- backgroundColor: color,
45
- borderColor:
46
- selectedColor === color
47
- ? tokens.colors.primary
48
- : tokens.colors.borderLight,
49
- borderWidth: selectedColor === color ? 3 : 1,
50
- },
51
- ]}
52
- onPress={() => onColorChange(color)}
53
- >
54
- {selectedColor === color && (
55
- <AtomicIcon
56
- name="checkmark-outline"
57
- size="sm"
58
- color={
59
- color === "#FFFFFF" || color === "#FCD34D"
60
- ? "primary"
61
- : "onSurface"
62
- }
63
- />
64
- )}
65
- </TouchableOpacity>
66
- ))}
67
- </View>
44
+ <Selector
45
+ items={items}
46
+ selectedValue={selectedColor}
47
+ onSelect={onColorChange}
48
+ orientation="grid"
49
+ colorPreview
50
+ testID="color-picker"
51
+ />
68
52
  </View>
69
53
  );
70
54
  };
71
-
72
- const styles = StyleSheet.create({
73
- section: {
74
- marginBottom: 24,
75
- },
76
- colorGrid: {
77
- flexDirection: "row",
78
- flexWrap: "wrap",
79
- gap: 12,
80
- },
81
- colorButton: {
82
- width: 50,
83
- height: 50,
84
- borderRadius: 25,
85
- alignItems: "center",
86
- justifyContent: "center",
87
- },
88
- });
@@ -1,13 +1,15 @@
1
1
  /**
2
- * FontSizeSelector Component
2
+ * Font Size Selector Component
3
3
  * Font size selector for text layer
4
+ * REFACTORED: Uses generic Selector component (45 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
8
- import { useLocalization } from "@umituz/react-native-settings";
7
+ import React, { useMemo } from "react";
8
+ import { View } from "react-native";
9
9
  import { AtomicText } from "@umituz/react-native-design-system/atoms";
10
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { useLocalization } from "@umituz/react-native-settings";
12
+ import { Selector, type SelectorItem } from "../generic/Selector";
11
13
  import { FONT_SIZES } from "../../../infrastructure/constants/text-layer.constants";
12
14
 
13
15
  interface FontSizeSelectorProps {
@@ -22,8 +24,13 @@ export const FontSizeSelector: React.FC<FontSizeSelectorProps> = ({
22
24
  const { t } = useLocalization();
23
25
  const tokens = useAppDesignTokens();
24
26
 
27
+ const items = useMemo<SelectorItem<number>[]>(
28
+ () => FONT_SIZES.map((size) => ({ value: size, label: `${size}px` })),
29
+ [],
30
+ );
31
+
25
32
  return (
26
- <View style={styles.section}>
33
+ <View style={{ marginBottom: tokens.spacing.md }}>
27
34
  <AtomicText
28
35
  type="bodyMedium"
29
36
  style={{
@@ -34,51 +41,13 @@ export const FontSizeSelector: React.FC<FontSizeSelectorProps> = ({
34
41
  >
35
42
  {t("editor.properties.font_size")}: {fontSize}px
36
43
  </AtomicText>
37
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
38
- {FONT_SIZES.map((size) => (
39
- <TouchableOpacity
40
- key={size}
41
- style={[
42
- styles.sizeButton,
43
- {
44
- backgroundColor:
45
- fontSize === size
46
- ? tokens.colors.primary
47
- : tokens.colors.surface,
48
- borderColor:
49
- fontSize === size
50
- ? tokens.colors.primary
51
- : tokens.colors.borderLight,
52
- },
53
- ]}
54
- onPress={() => onFontSizeChange(size)}
55
- >
56
- <AtomicText
57
- type="bodySmall"
58
- style={{
59
- color:
60
- fontSize === size ? tokens.colors.onPrimary : tokens.colors.textPrimary,
61
- fontWeight: fontSize === size ? "600" : "400",
62
- }}
63
- >
64
- {size}
65
- </AtomicText>
66
- </TouchableOpacity>
67
- ))}
68
- </ScrollView>
44
+ <Selector
45
+ items={items}
46
+ selectedValue={fontSize}
47
+ onSelect={onFontSizeChange}
48
+ orientation="horizontal"
49
+ testID="font-size-selector"
50
+ />
69
51
  </View>
70
52
  );
71
53
  };
72
-
73
- const styles = StyleSheet.create({
74
- section: {
75
- marginBottom: 24,
76
- },
77
- sizeButton: {
78
- paddingHorizontal: 16,
79
- paddingVertical: 10,
80
- borderRadius: 8,
81
- borderWidth: 1,
82
- marginRight: 8,
83
- },
84
- });
@@ -1,12 +1,14 @@
1
1
  /**
2
- * OptionSelector Component
2
+ * Option Selector Component
3
3
  * Reusable selector for font family, font weight, etc.
4
+ * REFACTORED: Uses generic Selector component (29 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, StyleSheet, TouchableOpacity } from "react-native";
7
+ import React, { useMemo } from "react";
8
+ import { View } from "react-native";
8
9
  import { AtomicText } from "@umituz/react-native-design-system/atoms";
9
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { Selector, type SelectorItem } from "../generic/Selector";
10
12
 
11
13
  interface Option {
12
14
  label: string;
@@ -28,8 +30,13 @@ export const OptionSelector: React.FC<OptionSelectorProps> = ({
28
30
  }) => {
29
31
  const tokens = useAppDesignTokens();
30
32
 
33
+ const items = useMemo<SelectorItem<string>[]>(
34
+ () => options.map((option) => ({ value: option.value, label: option.label })),
35
+ [options],
36
+ );
37
+
31
38
  return (
32
- <View style={styles.section}>
39
+ <View style={{ marginBottom: tokens.spacing.md }}>
33
40
  <AtomicText
34
41
  type="bodyMedium"
35
42
  style={{
@@ -40,57 +47,13 @@ export const OptionSelector: React.FC<OptionSelectorProps> = ({
40
47
  >
41
48
  {title}
42
49
  </AtomicText>
43
- <View style={styles.optionsGrid}>
44
- {options.map((option) => (
45
- <TouchableOpacity
46
- key={option.value}
47
- style={[
48
- styles.optionButton,
49
- {
50
- backgroundColor:
51
- selectedValue === option.value
52
- ? tokens.colors.primary + "20"
53
- : tokens.colors.surface,
54
- borderColor:
55
- selectedValue === option.value
56
- ? tokens.colors.primary
57
- : tokens.colors.borderLight,
58
- },
59
- ]}
60
- onPress={() => onValueChange(option.value)}
61
- >
62
- <AtomicText
63
- type="labelSmall"
64
- style={{
65
- color:
66
- selectedValue === option.value
67
- ? tokens.colors.primary
68
- : tokens.colors.textPrimary,
69
- fontWeight: selectedValue === option.value ? "600" : "400",
70
- }}
71
- >
72
- {option.label}
73
- </AtomicText>
74
- </TouchableOpacity>
75
- ))}
76
- </View>
50
+ <Selector
51
+ items={items}
52
+ selectedValue={selectedValue}
53
+ onSelect={onValueChange}
54
+ orientation="grid"
55
+ testID="option-selector"
56
+ />
77
57
  </View>
78
58
  );
79
59
  };
80
-
81
- const styles = StyleSheet.create({
82
- section: {
83
- marginBottom: 24,
84
- },
85
- optionsGrid: {
86
- flexDirection: "row",
87
- flexWrap: "wrap",
88
- gap: 8,
89
- },
90
- optionButton: {
91
- paddingHorizontal: 16,
92
- paddingVertical: 10,
93
- borderRadius: 8,
94
- borderWidth: 1,
95
- },
96
- });
@@ -1,13 +1,15 @@
1
1
  /**
2
- * TextAlignSelector Component
2
+ * Text Align Selector Component
3
3
  * Text alignment selector for text layer
4
+ * REFACTORED: Uses generic Selector component (38 lines)
4
5
  */
5
6
 
6
- import React from "react";
7
- import { View, StyleSheet, TouchableOpacity } from "react-native";
8
- import { useLocalization } from "@umituz/react-native-settings";
9
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
7
+ import React, { useMemo } from "react";
8
+ import { View } from "react-native";
9
+ import { AtomicText } from "@umituz/react-native-design-system/atoms";
10
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
+ import { useLocalization } from "@umituz/react-native-settings";
12
+ import { Selector, type SelectorItem } from "../generic/Selector";
11
13
  import { TEXT_ALIGNS } from "../../../infrastructure/constants/text-layer.constants";
12
14
 
13
15
  interface TextAlignSelectorProps {
@@ -22,8 +24,17 @@ export const TextAlignSelector: React.FC<TextAlignSelectorProps> = ({
22
24
  const { t } = useLocalization();
23
25
  const tokens = useAppDesignTokens();
24
26
 
27
+ const items = useMemo<SelectorItem<"left" | "center" | "right">[]>(
28
+ () => TEXT_ALIGNS.map((align) => ({
29
+ value: align.value,
30
+ label: "",
31
+ icon: align.icon,
32
+ })),
33
+ [],
34
+ );
35
+
25
36
  return (
26
- <View style={styles.section}>
37
+ <View style={{ marginBottom: tokens.spacing.md }}>
27
38
  <AtomicText
28
39
  type="bodyMedium"
29
40
  style={{
@@ -34,51 +45,13 @@ export const TextAlignSelector: React.FC<TextAlignSelectorProps> = ({
34
45
  >
35
46
  {t("editor.properties.text_align")}
36
47
  </AtomicText>
37
- <View style={styles.alignButtons}>
38
- {TEXT_ALIGNS.map((align) => (
39
- <TouchableOpacity
40
- key={align.value}
41
- style={[
42
- styles.alignButton,
43
- {
44
- backgroundColor:
45
- textAlign === align.value
46
- ? tokens.colors.primary
47
- : tokens.colors.surface,
48
- borderColor:
49
- textAlign === align.value
50
- ? tokens.colors.primary
51
- : tokens.colors.borderLight,
52
- },
53
- ]}
54
- onPress={() => onTextAlignChange(align.value)}
55
- >
56
- <AtomicIcon
57
- name={align.icon}
58
- size="md"
59
- color={textAlign === align.value ? "onSurface" : "secondary"}
60
- />
61
- </TouchableOpacity>
62
- ))}
63
- </View>
48
+ <Selector
49
+ items={items}
50
+ selectedValue={textAlign}
51
+ onSelect={onTextAlignChange}
52
+ icon
53
+ testID="text-align-selector"
54
+ />
64
55
  </View>
65
56
  );
66
57
  };
67
-
68
- const styles = StyleSheet.create({
69
- section: {
70
- marginBottom: 24,
71
- },
72
- alignButtons: {
73
- flexDirection: "row",
74
- gap: 8,
75
- },
76
- alignButton: {
77
- flex: 1,
78
- paddingVertical: 12,
79
- borderRadius: 8,
80
- borderWidth: 2,
81
- alignItems: "center",
82
- justifyContent: "center",
83
- },
84
- });
@@ -5,30 +5,30 @@
5
5
  */
6
6
 
7
7
  import { useState, useCallback } from "react";
8
- import type { Layer } from "../../domain/entities/video-project.types";
8
+ import type { Layer, ImageLayer, TextLayer } from "../../../domain/entities/video-project.types";
9
9
 
10
10
  /**
11
11
  * Form field validator function type
12
12
  */
13
- export type ValidatorFn<T> = (value: T[keyof T]) => string | null;
13
+ export type ValidatorFn<T, K extends keyof T = keyof T> = (value: T[K]) => string | null;
14
14
 
15
15
  /**
16
16
  * Layer form configuration
17
17
  */
18
18
  export interface UseLayerFormConfig<T extends Record<string, unknown>> {
19
19
  initialValues: Partial<T>;
20
- validators?: Partial<Record<keyof T, ValidatorFn<T>>>;
21
- buildData: (formState: T) => Partial<Layer>;
20
+ validators?: Record<string, (value: unknown) => string | null>;
21
+ buildData: (formState: T) => Partial<Layer> | Partial<ImageLayer> | Partial<TextLayer>;
22
22
  }
23
23
 
24
24
  /**
25
25
  * Layer form return type
26
26
  */
27
- export interface UseLayerFormReturn<T extends Record<string, unknown>> {
27
+ export interface UseLayerFormReturn<T extends Record<string, unknown>, R = Partial<Layer>> {
28
28
  formState: T;
29
29
  updateField: <K extends keyof T>(field: K, value: T[K]) => void;
30
30
  setFormState: (state: T | ((prev: T) => T)) => void;
31
- buildLayerData: () => Partial<Layer>;
31
+ buildLayerData: () => R;
32
32
  isValid: boolean;
33
33
  errors: Partial<Record<keyof T, string | null>>;
34
34
  validateField: <K extends keyof T>(field: K) => string | null;
@@ -39,9 +39,9 @@ export interface UseLayerFormReturn<T extends Record<string, unknown>> {
39
39
  * Generic hook for managing layer form state
40
40
  * Provides type-safe form management with validation support
41
41
  */
42
- export function useLayerForm<T extends Record<string, unknown>>(
42
+ export function useLayerForm<T extends Record<string, unknown>, R = Partial<Layer>>(
43
43
  config: UseLayerFormConfig<T>,
44
- ): UseLayerFormReturn<T> {
44
+ ): UseLayerFormReturn<T, R> {
45
45
  const { initialValues, validators = {}, buildData } = config;
46
46
 
47
47
  const [formState, setFormState] = useState<T>(
@@ -60,7 +60,7 @@ export function useLayerForm<T extends Record<string, unknown>>(
60
60
  }));
61
61
 
62
62
  // Clear error for this field
63
- if (errors[field]) {
63
+ if (errors[field as keyof typeof errors]) {
64
64
  setErrors((prev) => ({
65
65
  ...prev,
66
66
  [field]: null,
@@ -72,7 +72,7 @@ export function useLayerForm<T extends Record<string, unknown>>(
72
72
 
73
73
  const validateField = useCallback(
74
74
  <K extends keyof T>(field: K): string | null => {
75
- const validator = validators[field];
75
+ const validator = validators[String(field)];
76
76
  if (!validator) return null;
77
77
 
78
78
  const error = validator(formState[field]);
@@ -91,10 +91,13 @@ export function useLayerForm<T extends Record<string, unknown>>(
91
91
  const newErrors: Partial<Record<keyof T, string | null>> = {};
92
92
 
93
93
  for (const field in validators) {
94
- const error = validators[field]!(formState[field]);
95
- if (error) {
96
- newErrors[field] = error;
97
- hasError = true;
94
+ const validator = validators[field];
95
+ if (validator) {
96
+ const error = validator(formState[field as keyof T]);
97
+ if (error) {
98
+ newErrors[field as keyof T] = error;
99
+ hasError = true;
100
+ }
98
101
  }
99
102
  }
100
103
 
@@ -102,8 +105,8 @@ export function useLayerForm<T extends Record<string, unknown>>(
102
105
  return !hasError;
103
106
  }, [formState, validators]);
104
107
 
105
- const buildLayerData = useCallback((): Partial<Layer> => {
106
- return buildData(formState);
108
+ const buildLayerData = useCallback((): R => {
109
+ return buildData(formState) as R;
107
110
  }, [formState, buildData]);
108
111
 
109
112
  const isValid = Object.values(errors).every((error) => error === null);