@schandlergarcia/sf-web-components 1.7.0 → 1.9.0
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.
- package/dist/components/library/cards/ActionList.d.ts +10 -10
- package/dist/components/library/cards/ActionList.js +2 -3
- package/dist/components/library/cards/ActionList.js.map +1 -1
- package/dist/components/library/cards/ActivityCard.d.ts +18 -5
- package/dist/components/library/cards/ActivityCard.js +3 -4
- package/dist/components/library/cards/ActivityCard.js.map +1 -1
- package/dist/components/library/cards/BaseCard.d.ts +30 -24
- package/dist/components/library/cards/BaseCard.js +2 -3
- package/dist/components/library/cards/BaseCard.js.map +1 -1
- package/dist/components/library/cards/CalloutCard.d.ts +11 -9
- package/dist/components/library/cards/CalloutCard.js +2 -3
- package/dist/components/library/cards/CalloutCard.js.map +1 -1
- package/dist/components/library/cards/ChartCard.d.ts +29 -17
- package/dist/components/library/cards/ChartCard.js +13 -14
- package/dist/components/library/cards/ChartCard.js.map +1 -1
- package/dist/components/library/cards/FeedPanel.d.ts +12 -11
- package/dist/components/library/cards/FeedPanel.js +3 -4
- package/dist/components/library/cards/FeedPanel.js.map +1 -1
- package/dist/components/library/cards/ListCard.d.ts +33 -20
- package/dist/components/library/cards/ListCard.js +35 -35
- package/dist/components/library/cards/ListCard.js.map +1 -1
- package/dist/components/library/cards/MetricCard.d.ts +23 -17
- package/dist/components/library/cards/MetricCard.js +10 -11
- package/dist/components/library/cards/MetricCard.js.map +1 -1
- package/dist/components/library/cards/MetricsStrip.d.ts +11 -11
- package/dist/components/library/cards/MetricsStrip.js +1 -1
- package/dist/components/library/cards/MetricsStrip.js.map +1 -1
- package/dist/components/library/cards/SectionCard.d.ts +17 -12
- package/dist/components/library/cards/SectionCard.js +18 -19
- package/dist/components/library/cards/SectionCard.js.map +1 -1
- package/dist/components/library/cards/SemanticMetricCard.d.ts +15 -20
- package/dist/components/library/cards/SemanticMetricCardWithLoading.d.ts +8 -7
- package/dist/components/library/cards/SemanticTableCard.d.ts +13 -18
- package/dist/components/library/cards/SemanticTableCardWithLoading.d.ts +8 -7
- package/dist/components/library/cards/StatusCard.d.ts +29 -15
- package/dist/components/library/cards/StatusCard.js +16 -17
- package/dist/components/library/cards/StatusCard.js.map +1 -1
- package/dist/components/library/cards/TableCard.d.ts +40 -23
- package/dist/components/library/cards/TableCard.js +59 -59
- package/dist/components/library/cards/TableCard.js.map +1 -1
- package/dist/components/library/cards/WidgetCard.d.ts +19 -11
- package/dist/components/library/cards/WidgetCard.js.map +1 -1
- package/dist/components/library/charts/D3Chart.d.ts +23 -16
- package/dist/components/library/charts/D3Chart.js.map +1 -1
- package/dist/components/library/charts/D3ChartTemplates.d.ts +33 -3
- package/dist/components/library/charts/D3ChartTemplates.js +7 -7
- package/dist/components/library/charts/D3ChartTemplates.js.map +1 -1
- package/dist/components/library/charts/GeoMap.d.ts +81 -18
- package/dist/components/library/charts/GeoMap.js +28 -26
- package/dist/components/library/charts/GeoMap.js.map +1 -1
- package/dist/components/library/chat/ChatBar.d.ts +14 -11
- package/dist/components/library/chat/ChatBar.js +2 -3
- package/dist/components/library/chat/ChatBar.js.map +1 -1
- package/dist/components/library/chat/ChatInput.d.ts +9 -8
- package/dist/components/library/chat/ChatInput.js.map +1 -1
- package/dist/components/library/chat/ChatMessage.d.ts +17 -4
- package/dist/components/library/chat/ChatMessage.js.map +1 -1
- package/dist/components/library/chat/ChatMessageList.d.ts +11 -8
- package/dist/components/library/chat/ChatMessageList.js.map +1 -1
- package/dist/components/library/chat/ChatPanel.d.ts +16 -12
- package/dist/components/library/chat/ChatPanel.js +8 -9
- package/dist/components/library/chat/ChatPanel.js.map +1 -1
- package/dist/components/library/chat/ChatSuggestions.d.ts +5 -4
- package/dist/components/library/chat/ChatSuggestions.js +2 -3
- package/dist/components/library/chat/ChatSuggestions.js.map +1 -1
- package/dist/components/library/chat/ChatToolCall.d.ts +11 -3
- package/dist/components/library/chat/ChatToolCall.js.map +1 -1
- package/dist/components/library/chat/ChatTypingIndicator.d.ts +4 -3
- package/dist/components/library/chat/ChatTypingIndicator.js +2 -3
- package/dist/components/library/chat/ChatTypingIndicator.js.map +1 -1
- package/dist/components/library/chat/ChatWelcome.d.ts +9 -7
- package/dist/components/library/chat/ChatWelcome.js +6 -7
- package/dist/components/library/chat/ChatWelcome.js.map +1 -1
- package/dist/components/library/chat/index.d.ts +10 -0
- package/dist/components/library/chat/useChatState.d.ts +36 -11
- package/dist/components/library/chat/useChatState.js +63 -46
- package/dist/components/library/chat/useChatState.js.map +1 -1
- package/dist/components/library/data/DataModeProvider.d.ts +15 -11
- package/dist/components/library/data/DataModeProvider.js +1 -1
- package/dist/components/library/data/DataModeProvider.js.map +1 -1
- package/dist/components/library/data/DataModeToggle.d.ts +4 -3
- package/dist/components/library/data/DataModeToggle.js +4 -5
- package/dist/components/library/data/DataModeToggle.js.map +1 -1
- package/dist/components/library/data/chartDataProvider.d.ts +41 -3
- package/dist/components/library/data/filterUtils.d.ts +38 -9
- package/dist/components/library/data/filterUtils.js.map +1 -1
- package/dist/components/library/data/useDataSource.d.ts +6 -4
- package/dist/components/library/data/useDataSource.js.map +1 -1
- package/dist/components/library/data/usePageFilters.d.ts +31 -5
- package/dist/components/library/data/usePageFilters.js +6 -2
- package/dist/components/library/data/usePageFilters.js.map +1 -1
- package/dist/components/library/filters/FilterBar.d.ts +18 -8
- package/dist/components/library/filters/FilterBar.js +2 -3
- package/dist/components/library/filters/FilterBar.js.map +1 -1
- package/dist/components/library/filters/SearchFilter.d.ts +7 -6
- package/dist/components/library/filters/SearchFilter.js +2 -3
- package/dist/components/library/filters/SearchFilter.js.map +1 -1
- package/dist/components/library/filters/SelectFilter.d.ts +13 -7
- package/dist/components/library/filters/SelectFilter.js +2 -3
- package/dist/components/library/filters/SelectFilter.js.map +1 -1
- package/dist/components/library/filters/ToggleFilter.d.ts +7 -5
- package/dist/components/library/filters/ToggleFilter.js +2 -3
- package/dist/components/library/filters/ToggleFilter.js.map +1 -1
- package/dist/components/library/forms/FormField.d.ts +10 -8
- package/dist/components/library/forms/FormField.js +3 -4
- package/dist/components/library/forms/FormField.js.map +1 -1
- package/dist/components/library/forms/FormModal.d.ts +23 -14
- package/dist/components/library/forms/FormModal.js.map +1 -1
- package/dist/components/library/forms/FormRenderer.d.ts +29 -9
- package/dist/components/library/forms/FormRenderer.js +6 -7
- package/dist/components/library/forms/FormRenderer.js.map +1 -1
- package/dist/components/library/forms/FormSection.d.ts +10 -8
- package/dist/components/library/forms/FormSection.js +2 -3
- package/dist/components/library/forms/FormSection.js.map +1 -1
- package/dist/components/library/forms/index.d.ts +5 -0
- package/dist/components/library/forms/useFormState.d.ts +23 -15
- package/dist/components/library/forms/useFormState.js +53 -47
- package/dist/components/library/forms/useFormState.js.map +1 -1
- package/dist/components/library/index.d.ts +92 -73
- package/dist/components/library/index.js +25 -25
- package/dist/components/library/index.js.map +1 -1
- package/dist/components/library/layout/PageContainer.d.ts +6 -4
- package/dist/components/library/layout/PageContainer.js +4 -5
- package/dist/components/library/layout/PageContainer.js.map +1 -1
- package/dist/components/library/skeletons/CardSkeleton.d.ts +5 -4
- package/dist/components/library/skeletons/CardSkeleton.js +2 -3
- package/dist/components/library/skeletons/CardSkeleton.js.map +1 -1
- package/dist/components/library/theme/AppThemeProvider.d.ts +13 -50
- package/dist/components/library/theme/AppThemeProvider.js.map +1 -1
- package/dist/components/library/theme/tokens.d.ts +45 -44
- package/dist/components/library/theme/tokens.js.map +1 -1
- package/package.json +4 -1
- package/src/components/library/cards/{ActionList.jsx → ActionList.tsx} +13 -9
- package/src/components/library/cards/{ActivityCard.jsx → ActivityCard.tsx} +33 -4
- package/src/components/library/cards/{BaseCard.jsx → BaseCard.tsx} +33 -6
- package/src/components/library/cards/{CalloutCard.jsx → CalloutCard.tsx} +12 -10
- package/src/components/library/cards/{ChartCard.jsx → ChartCard.tsx} +32 -6
- package/src/components/library/cards/{FeedPanel.jsx → FeedPanel.tsx} +13 -2
- package/src/components/library/cards/{ListCard.jsx → ListCard.tsx} +43 -7
- package/src/components/library/cards/{MetricCard.jsx → MetricCard.tsx} +25 -6
- package/src/components/library/cards/{MetricsStrip.jsx → MetricsStrip.tsx} +22 -12
- package/src/components/library/cards/{SectionCard.jsx → SectionCard.tsx} +27 -8
- package/src/components/library/cards/{SemanticMetricCard.jsx → SemanticMetricCard.tsx} +18 -6
- package/src/components/library/cards/{SemanticMetricCardWithLoading.jsx → SemanticMetricCardWithLoading.tsx} +9 -3
- package/src/components/library/cards/{SemanticTableCard.jsx → SemanticTableCard.tsx} +16 -5
- package/src/components/library/cards/{SemanticTableCardWithLoading.jsx → SemanticTableCardWithLoading.tsx} +9 -5
- package/src/components/library/cards/{StatusCard.jsx → StatusCard.tsx} +61 -12
- package/src/components/library/cards/{TableCard.jsx → TableCard.tsx} +51 -12
- package/src/components/library/cards/{WidgetCard.jsx → WidgetCard.tsx} +28 -5
- package/src/components/library/charts/{D3Chart.jsx → D3Chart.tsx} +27 -7
- package/src/components/library/charts/{D3ChartTemplates.jsx → D3ChartTemplates.tsx} +60 -28
- package/src/components/library/charts/{GeoMap.jsx → GeoMap.tsx} +106 -17
- package/src/components/library/chat/{ChatBar.jsx → ChatBar.tsx} +19 -8
- package/src/components/library/chat/{ChatInput.jsx → ChatInput.tsx} +13 -11
- package/src/components/library/chat/{ChatMessage.jsx → ChatMessage.tsx} +22 -9
- package/src/components/library/chat/{ChatMessageList.jsx → ChatMessageList.tsx} +13 -11
- package/src/components/library/chat/{ChatPanel.jsx → ChatPanel.tsx} +16 -13
- package/src/components/library/chat/{ChatSuggestions.jsx → ChatSuggestions.tsx} +6 -5
- package/src/components/library/chat/{ChatToolCall.jsx → ChatToolCall.tsx} +14 -4
- package/src/components/library/chat/{ChatTypingIndicator.jsx → ChatTypingIndicator.tsx} +5 -2
- package/src/components/library/chat/{ChatWelcome.jsx → ChatWelcome.tsx} +9 -7
- package/src/components/library/chat/index.tsx +26 -0
- package/src/components/library/chat/useChatState.tsx +181 -0
- package/src/components/library/data/{DataModeProvider.jsx → DataModeProvider.tsx} +25 -8
- package/src/components/library/data/{DataModeToggle.jsx → DataModeToggle.tsx} +5 -2
- package/src/components/library/data/{chartDataProvider.jsx → chartDataProvider.tsx} +49 -5
- package/src/components/library/data/{filterUtils.jsx → filterUtils.tsx} +58 -12
- package/src/components/library/data/{useDataSource.jsx → useDataSource.tsx} +9 -2
- package/src/components/library/data/{usePageFilters.jsx → usePageFilters.tsx} +49 -9
- package/src/components/library/filters/{FilterBar.jsx → FilterBar.tsx} +21 -11
- package/src/components/library/filters/{SearchFilter.jsx → SearchFilter.tsx} +8 -2
- package/src/components/library/filters/{SelectFilter.jsx → SelectFilter.tsx} +15 -8
- package/src/components/library/filters/{ToggleFilter.jsx → ToggleFilter.tsx} +7 -6
- package/src/components/library/forms/{FormField.jsx → FormField.tsx} +91 -45
- package/src/components/library/forms/{FormModal.jsx → FormModal.tsx} +21 -20
- package/src/components/library/forms/{FormRenderer.jsx → FormRenderer.tsx} +32 -10
- package/src/components/library/forms/{FormSection.jsx → FormSection.tsx} +13 -7
- package/src/components/library/forms/index.tsx +11 -0
- package/src/components/library/forms/{useFormState.jsx → useFormState.tsx} +43 -23
- package/src/components/library/{index.jsx → index.ts} +14 -14
- package/src/components/library/layout/{PageContainer.jsx → PageContainer.tsx} +6 -3
- package/src/components/library/skeletons/{CardSkeleton.jsx → CardSkeleton.tsx} +5 -4
- package/src/components/library/theme/{AppThemeProvider.jsx → AppThemeProvider.tsx} +20 -7
- package/src/components/library/theme/{tokens.jsx → tokens.tsx} +37 -3
- package/src/components/library/chat/index.jsx +0 -10
- package/src/components/library/chat/useChatState.jsx +0 -130
- package/src/components/library/forms/index.jsx +0 -5
- /package/src/components/library/filters/{index.jsx → index.ts} +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
import { useState, useMemo, useCallback } from "react";
|
|
2
|
-
import { applyFilters, sortByKey } from "./filterUtils";
|
|
2
|
+
import { applyFilters, sortByKey, FilterDefinition } from "./filterUtils";
|
|
3
|
+
|
|
4
|
+
export type SortDirection = "asc" | "desc";
|
|
5
|
+
|
|
6
|
+
export interface SortState {
|
|
7
|
+
key: string;
|
|
8
|
+
direction: SortDirection;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DateRange {
|
|
12
|
+
start?: Date | string;
|
|
13
|
+
end?: Date | string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type FilterValue = string | boolean | DateRange | null;
|
|
17
|
+
|
|
18
|
+
export interface FilterValues {
|
|
19
|
+
[key: string]: FilterValue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UsePageFiltersOptions<T> {
|
|
23
|
+
data?: T[];
|
|
24
|
+
filters?: FilterDefinition[];
|
|
25
|
+
defaultSort?: SortState | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UsePageFiltersReturn<T> {
|
|
29
|
+
values: FilterValues;
|
|
30
|
+
setFilter: (id: string, value: FilterValue) => void;
|
|
31
|
+
resetFilters: () => void;
|
|
32
|
+
sort: SortState | null;
|
|
33
|
+
setSort: (key: string | null, direction?: SortDirection) => void;
|
|
34
|
+
toggleSort: (key: string) => void;
|
|
35
|
+
filteredData: T[];
|
|
36
|
+
sortedData: T[];
|
|
37
|
+
activeFilterCount: number;
|
|
38
|
+
}
|
|
3
39
|
|
|
4
40
|
/**
|
|
5
41
|
* Hook for managing page-level filter and sort state.
|
|
@@ -21,9 +57,13 @@ import { applyFilters, sortByKey } from "./filterUtils";
|
|
|
21
57
|
* defaultSort: { key: "timestamp", direction: "desc" },
|
|
22
58
|
* });
|
|
23
59
|
*/
|
|
24
|
-
export default function usePageFilters
|
|
60
|
+
export default function usePageFilters<T = any>({
|
|
61
|
+
data = [],
|
|
62
|
+
filters = [],
|
|
63
|
+
defaultSort = null,
|
|
64
|
+
}: UsePageFiltersOptions<T> = {}): UsePageFiltersReturn<T> {
|
|
25
65
|
const initialValues = useMemo(() => {
|
|
26
|
-
const v = {};
|
|
66
|
+
const v: FilterValues = {};
|
|
27
67
|
for (const f of filters) {
|
|
28
68
|
if (f.defaultValue !== undefined) {
|
|
29
69
|
v[f.id] = f.defaultValue;
|
|
@@ -40,10 +80,10 @@ export default function usePageFilters({ data = [], filters = [], defaultSort =
|
|
|
40
80
|
return v;
|
|
41
81
|
}, [filters]);
|
|
42
82
|
|
|
43
|
-
const [values, setValues] = useState(initialValues);
|
|
44
|
-
const [sort, setSortState] = useState(defaultSort);
|
|
83
|
+
const [values, setValues] = useState<FilterValues>(initialValues);
|
|
84
|
+
const [sort, setSortState] = useState<SortState | null>(defaultSort);
|
|
45
85
|
|
|
46
|
-
const setFilter = useCallback((id, value) => {
|
|
86
|
+
const setFilter = useCallback((id: string, value: FilterValue) => {
|
|
47
87
|
setValues((prev) => ({ ...prev, [id]: value }));
|
|
48
88
|
}, []);
|
|
49
89
|
|
|
@@ -51,11 +91,11 @@ export default function usePageFilters({ data = [], filters = [], defaultSort =
|
|
|
51
91
|
setValues(initialValues);
|
|
52
92
|
}, [initialValues]);
|
|
53
93
|
|
|
54
|
-
const setSort = useCallback((key, direction) => {
|
|
94
|
+
const setSort = useCallback((key: string | null, direction?: SortDirection) => {
|
|
55
95
|
setSortState(key ? { key, direction: direction ?? "asc" } : null);
|
|
56
96
|
}, []);
|
|
57
97
|
|
|
58
|
-
const toggleSort = useCallback((key) => {
|
|
98
|
+
const toggleSort = useCallback((key: string) => {
|
|
59
99
|
setSortState((prev) => {
|
|
60
100
|
if (prev?.key !== key) return { key, direction: "asc" };
|
|
61
101
|
if (prev.direction === "asc") return { key, direction: "desc" };
|
|
@@ -77,7 +117,7 @@ export default function usePageFilters({ data = [], filters = [], defaultSort =
|
|
|
77
117
|
let count = 0;
|
|
78
118
|
for (const f of filters) {
|
|
79
119
|
const v = values[f.id];
|
|
80
|
-
if (f.type === "search" && v && v.trim()) count++;
|
|
120
|
+
if (f.type === "search" && v && typeof v === "string" && v.trim()) count++;
|
|
81
121
|
else if (f.type === "select" && v && v !== "all") count++;
|
|
82
122
|
else if (f.type === "toggle" && v) count++;
|
|
83
123
|
else if (f.type === "dateRange" && v) count++;
|
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import SearchFilter from "./SearchFilter";
|
|
3
|
-
import SelectFilter from "./SelectFilter";
|
|
2
|
+
import SelectFilter, { SelectOption } from "./SelectFilter";
|
|
4
3
|
import ToggleFilter from "./ToggleFilter";
|
|
5
|
-
import {
|
|
4
|
+
import { XMarkIcon } from "@heroicons/react/24/outline";
|
|
5
|
+
|
|
6
|
+
export interface FilterDefinition {
|
|
7
|
+
id: string;
|
|
8
|
+
type: "search" | "select" | "toggle";
|
|
9
|
+
label?: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
options?: (string | SelectOption)[];
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FilterBarProps {
|
|
16
|
+
filters?: FilterDefinition[];
|
|
17
|
+
values?: Record<string, any>;
|
|
18
|
+
onChange?: (filterId: string, value: any) => void;
|
|
19
|
+
onReset?: () => void;
|
|
20
|
+
activeCount?: number;
|
|
21
|
+
layout?: "inline" | "stacked";
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
/**
|
|
8
25
|
* Renders a row of filter controls from a definitions array.
|
|
9
26
|
* Pairs with usePageFilters hook for state management.
|
|
10
|
-
*
|
|
11
|
-
* @param {Array} filters — filter definitions [{ id, type, ... }]
|
|
12
|
-
* @param {Object} values — current filter values keyed by filter id
|
|
13
|
-
* @param {Function} onChange — (filterId, value) => void
|
|
14
|
-
* @param {Function} onReset — () => void
|
|
15
|
-
* @param {number} activeCount — number of active filters (for badge)
|
|
16
|
-
* @param {string} layout — "inline" (default) or "stacked"
|
|
17
27
|
*/
|
|
18
28
|
export default function FilterBar({
|
|
19
29
|
filters = [],
|
|
@@ -22,7 +32,7 @@ export default function FilterBar({
|
|
|
22
32
|
onReset,
|
|
23
33
|
activeCount = 0,
|
|
24
34
|
layout = "inline",
|
|
25
|
-
}) {
|
|
35
|
+
}: FilterBarProps) {
|
|
26
36
|
if (!filters.length) return null;
|
|
27
37
|
|
|
28
38
|
const isStacked = layout === "stacked";
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
|
3
2
|
|
|
3
|
+
export interface SearchFilterProps {
|
|
4
|
+
value?: string;
|
|
5
|
+
onChange?: (value: string) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
export default function SearchFilter({
|
|
5
11
|
value = "",
|
|
6
12
|
onChange,
|
|
7
13
|
placeholder = "Search…",
|
|
8
14
|
className = "",
|
|
9
|
-
}) {
|
|
15
|
+
}: SearchFilterProps) {
|
|
10
16
|
return (
|
|
11
17
|
<div className={["relative", className].filter(Boolean).join(" ")}>
|
|
12
18
|
<MagnifyingGlassIcon
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
|
3
2
|
|
|
3
|
+
export interface SelectOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SelectFilterProps {
|
|
9
|
+
value?: string;
|
|
10
|
+
onChange?: (value: string) => void;
|
|
11
|
+
options?: (string | SelectOption)[];
|
|
12
|
+
label?: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
/**
|
|
5
18
|
* Dropdown select filter.
|
|
6
|
-
*
|
|
7
|
-
* @param {string} value — current selected value
|
|
8
|
-
* @param {Function} onChange — (value) => void
|
|
9
|
-
* @param {Array} options — [{ value, label }] or ["string", ...]
|
|
10
|
-
* @param {string} label — visible label
|
|
11
|
-
* @param {string} placeholder — placeholder when no value selected
|
|
12
19
|
*/
|
|
13
20
|
export default function SelectFilter({
|
|
14
21
|
value = "all",
|
|
@@ -17,7 +24,7 @@ export default function SelectFilter({
|
|
|
17
24
|
label,
|
|
18
25
|
placeholder,
|
|
19
26
|
className = "",
|
|
20
|
-
}) {
|
|
27
|
+
}: SelectFilterProps) {
|
|
21
28
|
const normalizedOptions = options.map((opt) =>
|
|
22
29
|
typeof opt === "string" ? { value: opt, label: opt } : opt
|
|
23
30
|
);
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
export interface ToggleFilterProps {
|
|
2
|
+
value?: boolean;
|
|
3
|
+
onChange?: (value: boolean) => void;
|
|
4
|
+
label?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Toggle switch filter.
|
|
5
|
-
*
|
|
6
|
-
* @param {boolean} value — current on/off state
|
|
7
|
-
* @param {Function} onChange — (boolean) => void
|
|
8
|
-
* @param {string} label — visible label
|
|
9
10
|
*/
|
|
10
11
|
export default function ToggleFilter({
|
|
11
12
|
value = false,
|
|
12
13
|
onChange,
|
|
13
14
|
label,
|
|
14
15
|
className = "",
|
|
15
|
-
}) {
|
|
16
|
+
}: ToggleFilterProps) {
|
|
16
17
|
return (
|
|
17
18
|
<label
|
|
18
19
|
className={[
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import * as React from "react";
|
|
2
2
|
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
|
3
|
+
import type { FormField as FormFieldType } from "./FormRenderer";
|
|
3
4
|
|
|
4
5
|
const INPUT_BASE =
|
|
5
6
|
"h-10 w-full rounded-lg border border-slate-200 bg-white px-3 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-50 dark:placeholder:text-slate-500 dark:focus:ring-offset-slate-950";
|
|
@@ -7,11 +8,17 @@ const INPUT_BASE =
|
|
|
7
8
|
const INPUT_ERROR =
|
|
8
9
|
"border-red-300 focus:ring-red-500 dark:border-red-700 dark:focus:ring-red-500";
|
|
9
10
|
|
|
10
|
-
function cx(...classes) {
|
|
11
|
+
function cx(...classes: (string | boolean | undefined)[]): string {
|
|
11
12
|
return classes.filter(Boolean).join(" ");
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
interface FieldLabelProps {
|
|
16
|
+
label?: string;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
htmlFor: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function FieldLabel({ label, required, htmlFor }: FieldLabelProps) {
|
|
15
22
|
if (!label) return null;
|
|
16
23
|
return (
|
|
17
24
|
<label htmlFor={htmlFor} className="block text-sm font-medium text-slate-700 dark:text-slate-200">
|
|
@@ -21,20 +28,36 @@ function FieldLabel({ label, required, htmlFor }) {
|
|
|
21
28
|
);
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
interface FieldErrorProps {
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function FieldError({ error }: FieldErrorProps) {
|
|
25
36
|
if (!error) return null;
|
|
26
37
|
return <p className="text-xs text-red-600 dark:text-red-400">{error}</p>;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
interface FieldDescriptionProps {
|
|
41
|
+
description?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function FieldDescription({ description }: FieldDescriptionProps) {
|
|
30
45
|
if (!description) return null;
|
|
31
46
|
return <p className="text-xs text-slate-500 dark:text-slate-400">{description}</p>;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
// ─── Individual field renderers ───
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
interface FieldRendererProps {
|
|
52
|
+
field: FormFieldType & Record<string, unknown>;
|
|
53
|
+
value: unknown;
|
|
54
|
+
onChange: (value: unknown) => void;
|
|
55
|
+
onBlur: () => void;
|
|
56
|
+
error?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function TextField({ field, value, onChange, onBlur, error }: FieldRendererProps) {
|
|
60
|
+
const inputType = (field.inputType as string | undefined) ?? field.type;
|
|
38
61
|
const type = { text: "text", email: "email", url: "url", number: "number", date: "date" }[inputType] ?? "text";
|
|
39
62
|
|
|
40
63
|
return (
|
|
@@ -42,32 +65,32 @@ function TextField({ field, value, onChange, onBlur, error }) {
|
|
|
42
65
|
id={field.id}
|
|
43
66
|
name={field.id}
|
|
44
67
|
type={type}
|
|
45
|
-
value={value ?? ""}
|
|
68
|
+
value={(value as string | number) ?? ""}
|
|
46
69
|
onChange={(e) => onChange(field.type === "number" ? e.target.value : e.target.value)}
|
|
47
70
|
onBlur={onBlur}
|
|
48
|
-
placeholder={field.placeholder}
|
|
49
|
-
disabled={field.disabled}
|
|
50
|
-
readOnly={field.readOnly}
|
|
51
|
-
min={field.min}
|
|
52
|
-
max={field.max}
|
|
53
|
-
step={field.step}
|
|
71
|
+
placeholder={field.placeholder as string | undefined}
|
|
72
|
+
disabled={field.disabled as boolean | undefined}
|
|
73
|
+
readOnly={field.readOnly as boolean | undefined}
|
|
74
|
+
min={field.min as number | undefined}
|
|
75
|
+
max={field.max as number | undefined}
|
|
76
|
+
step={field.step as number | undefined}
|
|
54
77
|
className={cx(INPUT_BASE, error && INPUT_ERROR)}
|
|
55
78
|
/>
|
|
56
79
|
);
|
|
57
80
|
}
|
|
58
81
|
|
|
59
|
-
function TextareaField({ field, value, onChange, onBlur, error }) {
|
|
82
|
+
function TextareaField({ field, value, onChange, onBlur, error }: FieldRendererProps) {
|
|
60
83
|
return (
|
|
61
84
|
<textarea
|
|
62
85
|
id={field.id}
|
|
63
86
|
name={field.id}
|
|
64
|
-
value={value ?? ""}
|
|
87
|
+
value={(value as string) ?? ""}
|
|
65
88
|
onChange={(e) => onChange(e.target.value)}
|
|
66
89
|
onBlur={onBlur}
|
|
67
|
-
placeholder={field.placeholder}
|
|
68
|
-
disabled={field.disabled}
|
|
69
|
-
readOnly={field.readOnly}
|
|
70
|
-
rows={field.rows ?? 3}
|
|
90
|
+
placeholder={field.placeholder as string | undefined}
|
|
91
|
+
disabled={field.disabled as boolean | undefined}
|
|
92
|
+
readOnly={field.readOnly as boolean | undefined}
|
|
93
|
+
rows={(field.rows as number | undefined) ?? 3}
|
|
71
94
|
className={cx(
|
|
72
95
|
"w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-50 dark:placeholder:text-slate-500 dark:focus:ring-offset-slate-950",
|
|
73
96
|
error && INPUT_ERROR
|
|
@@ -76,8 +99,13 @@ function TextareaField({ field, value, onChange, onBlur, error }) {
|
|
|
76
99
|
);
|
|
77
100
|
}
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
interface SelectOption {
|
|
103
|
+
value: string;
|
|
104
|
+
label: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function SelectField({ field, value, onChange, onBlur, error }: FieldRendererProps) {
|
|
108
|
+
const options = ((field.options as (string | SelectOption)[] | undefined) ?? []).map((opt) =>
|
|
81
109
|
typeof opt === "string" ? { value: opt, label: opt } : opt
|
|
82
110
|
);
|
|
83
111
|
|
|
@@ -86,17 +114,17 @@ function SelectField({ field, value, onChange, onBlur, error }) {
|
|
|
86
114
|
<select
|
|
87
115
|
id={field.id}
|
|
88
116
|
name={field.id}
|
|
89
|
-
value={value ?? ""}
|
|
117
|
+
value={(value as string) ?? ""}
|
|
90
118
|
onChange={(e) => onChange(e.target.value)}
|
|
91
119
|
onBlur={onBlur}
|
|
92
|
-
disabled={field.disabled}
|
|
120
|
+
disabled={field.disabled as boolean | undefined}
|
|
93
121
|
className={cx(
|
|
94
122
|
"h-10 w-full appearance-none rounded-lg border border-slate-200 bg-white py-0 pl-3 pr-9 text-sm font-medium text-slate-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-200 dark:focus:ring-offset-slate-950",
|
|
95
123
|
error && INPUT_ERROR
|
|
96
124
|
)}
|
|
97
125
|
>
|
|
98
126
|
{field.placeholder ? (
|
|
99
|
-
<option value="">{field.placeholder}</option>
|
|
127
|
+
<option value="">{field.placeholder as string}</option>
|
|
100
128
|
) : null}
|
|
101
129
|
{options.map((opt) => (
|
|
102
130
|
<option key={opt.value} value={opt.value}>
|
|
@@ -112,11 +140,16 @@ function SelectField({ field, value, onChange, onBlur, error }) {
|
|
|
112
140
|
);
|
|
113
141
|
}
|
|
114
142
|
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
interface RadioOption extends SelectOption {
|
|
144
|
+
description?: string;
|
|
145
|
+
disabled?: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function RadioField({ field, value, onChange }: FieldRendererProps) {
|
|
149
|
+
const options = ((field.options as (string | RadioOption)[] | undefined) ?? []).map((opt) =>
|
|
117
150
|
typeof opt === "string" ? { value: opt, label: opt } : opt
|
|
118
151
|
);
|
|
119
|
-
const layout = field.layout ?? (options.length <= 4 ? "horizontal" : "vertical");
|
|
152
|
+
const layout = (field.layout as string | undefined) ?? (options.length <= 4 ? "horizontal" : "vertical");
|
|
120
153
|
|
|
121
154
|
return (
|
|
122
155
|
<div
|
|
@@ -138,7 +171,7 @@ function RadioField({ field, value, onChange }) {
|
|
|
138
171
|
value={opt.value}
|
|
139
172
|
checked={value === opt.value}
|
|
140
173
|
onChange={() => onChange(opt.value)}
|
|
141
|
-
disabled={field.disabled || opt.disabled}
|
|
174
|
+
disabled={(field.disabled as boolean | undefined) || opt.disabled}
|
|
142
175
|
className="h-4 w-4 border-slate-300 text-brand-600 focus:ring-brand-500 dark:border-slate-600 dark:bg-slate-800"
|
|
143
176
|
/>
|
|
144
177
|
{opt.label}
|
|
@@ -151,7 +184,7 @@ function RadioField({ field, value, onChange }) {
|
|
|
151
184
|
);
|
|
152
185
|
}
|
|
153
186
|
|
|
154
|
-
function CheckboxField({ field, value, onChange }) {
|
|
187
|
+
function CheckboxField({ field, value, onChange }: FieldRendererProps) {
|
|
155
188
|
return (
|
|
156
189
|
<label className="inline-flex cursor-pointer items-center gap-2.5 text-sm text-slate-700 dark:text-slate-200">
|
|
157
190
|
<input
|
|
@@ -160,22 +193,26 @@ function CheckboxField({ field, value, onChange }) {
|
|
|
160
193
|
name={field.id}
|
|
161
194
|
checked={Boolean(value)}
|
|
162
195
|
onChange={(e) => onChange(e.target.checked)}
|
|
163
|
-
disabled={field.disabled}
|
|
196
|
+
disabled={field.disabled as boolean | undefined}
|
|
164
197
|
className="h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500 dark:border-slate-600 dark:bg-slate-800"
|
|
165
198
|
/>
|
|
166
|
-
{field.checkboxLabel ?? field.label}
|
|
199
|
+
{(field.checkboxLabel as string | undefined) ?? field.label}
|
|
167
200
|
</label>
|
|
168
201
|
);
|
|
169
202
|
}
|
|
170
203
|
|
|
171
|
-
|
|
204
|
+
interface CheckboxOption extends SelectOption {
|
|
205
|
+
disabled?: boolean;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function CheckboxGroupField({ field, value, onChange }: FieldRendererProps) {
|
|
172
209
|
const selected = Array.isArray(value) ? value : [];
|
|
173
|
-
const options = (field.options ?? []).map((opt) =>
|
|
210
|
+
const options = ((field.options as (string | CheckboxOption)[] | undefined) ?? []).map((opt) =>
|
|
174
211
|
typeof opt === "string" ? { value: opt, label: opt } : opt
|
|
175
212
|
);
|
|
176
|
-
const layout = field.layout ?? (options.length <= 4 ? "horizontal" : "vertical");
|
|
213
|
+
const layout = (field.layout as string | undefined) ?? (options.length <= 4 ? "horizontal" : "vertical");
|
|
177
214
|
|
|
178
|
-
function toggleValue(optValue) {
|
|
215
|
+
function toggleValue(optValue: string) {
|
|
179
216
|
const next = selected.includes(optValue)
|
|
180
217
|
? selected.filter((v) => v !== optValue)
|
|
181
218
|
: [...selected, optValue];
|
|
@@ -198,7 +235,7 @@ function CheckboxGroupField({ field, value, onChange }) {
|
|
|
198
235
|
type="checkbox"
|
|
199
236
|
checked={selected.includes(opt.value)}
|
|
200
237
|
onChange={() => toggleValue(opt.value)}
|
|
201
|
-
disabled={field.disabled || opt.disabled}
|
|
238
|
+
disabled={(field.disabled as boolean | undefined) || opt.disabled}
|
|
202
239
|
className="h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500 dark:border-slate-600 dark:bg-slate-800"
|
|
203
240
|
/>
|
|
204
241
|
{opt.label}
|
|
@@ -208,7 +245,7 @@ function CheckboxGroupField({ field, value, onChange }) {
|
|
|
208
245
|
);
|
|
209
246
|
}
|
|
210
247
|
|
|
211
|
-
function ToggleField({ field, value, onChange }) {
|
|
248
|
+
function ToggleField({ field, value, onChange }: FieldRendererProps) {
|
|
212
249
|
const checked = Boolean(value);
|
|
213
250
|
|
|
214
251
|
return (
|
|
@@ -218,7 +255,7 @@ function ToggleField({ field, value, onChange }) {
|
|
|
218
255
|
role="switch"
|
|
219
256
|
aria-checked={checked}
|
|
220
257
|
onClick={() => onChange(!checked)}
|
|
221
|
-
disabled={field.disabled}
|
|
258
|
+
disabled={field.disabled as boolean | undefined}
|
|
222
259
|
className={cx(
|
|
223
260
|
"relative inline-flex h-6 w-11 shrink-0 rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:focus:ring-offset-slate-950",
|
|
224
261
|
checked ? "bg-brand-500" : "bg-slate-200 dark:bg-slate-700",
|
|
@@ -234,7 +271,7 @@ function ToggleField({ field, value, onChange }) {
|
|
|
234
271
|
/>
|
|
235
272
|
</button>
|
|
236
273
|
{field.toggleLabel ? (
|
|
237
|
-
<span className="text-sm text-slate-700 dark:text-slate-200">{field.toggleLabel}</span>
|
|
274
|
+
<span className="text-sm text-slate-700 dark:text-slate-200">{field.toggleLabel as string}</span>
|
|
238
275
|
) : null}
|
|
239
276
|
</div>
|
|
240
277
|
);
|
|
@@ -242,7 +279,7 @@ function ToggleField({ field, value, onChange }) {
|
|
|
242
279
|
|
|
243
280
|
// ─── Main FormField ───
|
|
244
281
|
|
|
245
|
-
const FIELD_RENDERERS = {
|
|
282
|
+
const FIELD_RENDERERS: Record<string, React.ComponentType<FieldRendererProps>> = {
|
|
246
283
|
text: TextField,
|
|
247
284
|
email: TextField,
|
|
248
285
|
url: TextField,
|
|
@@ -256,11 +293,20 @@ const FIELD_RENDERERS = {
|
|
|
256
293
|
toggle: ToggleField,
|
|
257
294
|
};
|
|
258
295
|
|
|
296
|
+
export interface FormFieldProps {
|
|
297
|
+
field: FormFieldType & Record<string, unknown>;
|
|
298
|
+
value: unknown;
|
|
299
|
+
error?: string;
|
|
300
|
+
touched?: boolean;
|
|
301
|
+
onChange: (value: unknown) => void;
|
|
302
|
+
onBlur: () => void;
|
|
303
|
+
}
|
|
304
|
+
|
|
259
305
|
/**
|
|
260
306
|
* Renders a single form field with label, description, error message,
|
|
261
307
|
* and the appropriate input type.
|
|
262
308
|
*/
|
|
263
|
-
export default function FormField({ field, value, error, touched, onChange, onBlur }) {
|
|
309
|
+
export default function FormField({ field, value, error, touched, onChange, onBlur }: FormFieldProps) {
|
|
264
310
|
const Renderer = FIELD_RENDERERS[field.type];
|
|
265
311
|
if (!Renderer) return null;
|
|
266
312
|
|
|
@@ -273,17 +319,17 @@ export default function FormField({ field, value, error, touched, onChange, onBl
|
|
|
273
319
|
<FieldLabel label={field.label} required={field.required} htmlFor={field.id} />
|
|
274
320
|
) : null}
|
|
275
321
|
{field.description && field.type !== "toggle" ? (
|
|
276
|
-
<FieldDescription description={field.description} />
|
|
322
|
+
<FieldDescription description={field.description as string} />
|
|
277
323
|
) : null}
|
|
278
324
|
<Renderer
|
|
279
325
|
field={field}
|
|
280
326
|
value={value}
|
|
281
327
|
onChange={onChange}
|
|
282
328
|
onBlur={onBlur}
|
|
283
|
-
error={showError}
|
|
329
|
+
error={Boolean(showError)}
|
|
284
330
|
/>
|
|
285
331
|
{field.type === "toggle" && field.description ? (
|
|
286
|
-
<FieldDescription description={field.description} />
|
|
332
|
+
<FieldDescription description={field.description as string} />
|
|
287
333
|
) : null}
|
|
288
334
|
{showError ? <FieldError error={error} /> : null}
|
|
289
335
|
</div>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useCallback } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { motion, AnimatePresence } from "framer-motion";
|
|
4
4
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
|
5
5
|
import Spinner from "../ui/Spinner";
|
|
6
6
|
import FormRenderer from "./FormRenderer";
|
|
7
7
|
import useFormState from "./useFormState";
|
|
8
|
+
import type { FormSection } from "./FormRenderer";
|
|
8
9
|
|
|
9
10
|
const OVERLAY_VARIANTS = {
|
|
10
11
|
hidden: { opacity: 0 },
|
|
@@ -13,35 +14,35 @@ const OVERLAY_VARIANTS = {
|
|
|
13
14
|
|
|
14
15
|
const PANEL_VARIANTS = {
|
|
15
16
|
hidden: { opacity: 0, y: 24, scale: 0.97 },
|
|
16
|
-
visible: { opacity: 1, y: 0, scale: 1, transition: { type: "spring", damping: 25, stiffness: 350 } },
|
|
17
|
+
visible: { opacity: 1, y: 0, scale: 1, transition: { type: "spring" as const, damping: 25, stiffness: 350 } },
|
|
17
18
|
exit: { opacity: 0, y: 16, scale: 0.97, transition: { duration: 0.15 } },
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
-
* Size → max-width mapping.
|
|
22
|
-
*/
|
|
23
21
|
const SIZE_CLASSES = {
|
|
24
22
|
sm: "max-w-md",
|
|
25
23
|
md: "max-w-xl",
|
|
26
24
|
lg: "max-w-2xl",
|
|
27
25
|
xl: "max-w-4xl",
|
|
28
|
-
};
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export interface FormModalProps {
|
|
29
|
+
isOpen?: boolean;
|
|
30
|
+
onClose?: () => void;
|
|
31
|
+
title: string;
|
|
32
|
+
subtitle?: string;
|
|
33
|
+
sections?: FormSection[];
|
|
34
|
+
initialValues?: Record<string, unknown>;
|
|
35
|
+
onSubmit?: (values: Record<string, unknown>) => void | Promise<void>;
|
|
36
|
+
submitLabel?: string;
|
|
37
|
+
cancelLabel?: string;
|
|
38
|
+
size?: keyof typeof SIZE_CLASSES;
|
|
39
|
+
destructive?: boolean;
|
|
40
|
+
minSubmitMs?: number;
|
|
41
|
+
}
|
|
29
42
|
|
|
30
43
|
/**
|
|
31
44
|
* Modal dialog for creating or editing records.
|
|
32
45
|
* Wraps FormRenderer + useFormState in an animated overlay.
|
|
33
|
-
*
|
|
34
|
-
* @param {boolean} isOpen — whether the modal is visible
|
|
35
|
-
* @param {Function} onClose — close handler
|
|
36
|
-
* @param {string} title — modal title (e.g. "Edit Service", "New Incident")
|
|
37
|
-
* @param {string} subtitle — optional subtitle
|
|
38
|
-
* @param {Array} sections — form schema sections
|
|
39
|
-
* @param {Object} initialValues — prefill for editing (empty = create mode)
|
|
40
|
-
* @param {Function} onSubmit — async (values) => void, called on valid submit
|
|
41
|
-
* @param {string} submitLabel — submit button text (default: "Save")
|
|
42
|
-
* @param {string} cancelLabel — cancel button text (default: "Cancel")
|
|
43
|
-
* @param {string} size — "sm" | "md" | "lg" | "xl" (default: "lg")
|
|
44
|
-
* @param {boolean} destructive — if true, submit button is red (for delete confirmations)
|
|
45
46
|
*/
|
|
46
47
|
export default function FormModal({
|
|
47
48
|
isOpen = false,
|
|
@@ -56,7 +57,7 @@ export default function FormModal({
|
|
|
56
57
|
size = "lg",
|
|
57
58
|
destructive = false,
|
|
58
59
|
minSubmitMs,
|
|
59
|
-
}) {
|
|
60
|
+
}: FormModalProps) {
|
|
60
61
|
const form = useFormState({
|
|
61
62
|
initialValues,
|
|
62
63
|
sections,
|
|
@@ -73,7 +74,7 @@ export default function FormModal({
|
|
|
73
74
|
}, [isOpen]);
|
|
74
75
|
|
|
75
76
|
const onKeyDown = useCallback(
|
|
76
|
-
(e) => {
|
|
77
|
+
(e: KeyboardEvent) => {
|
|
77
78
|
if (e.key === "Escape") onClose?.();
|
|
78
79
|
},
|
|
79
80
|
[onClose]
|
|
@@ -1,17 +1,39 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import FormSection from "./FormSection";
|
|
3
2
|
|
|
3
|
+
export interface FormSection {
|
|
4
|
+
id?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
fields: FormField[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FormField {
|
|
11
|
+
id: string;
|
|
12
|
+
type: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
requiredMessage?: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
validate?: (value: unknown, values: Record<string, unknown>) => string | undefined;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FormRendererProps {
|
|
25
|
+
sections?: FormSection[];
|
|
26
|
+
values?: Record<string, unknown>;
|
|
27
|
+
errors?: Record<string, string>;
|
|
28
|
+
touched?: Record<string, boolean>;
|
|
29
|
+
onFieldChange: (fieldId: string, value: unknown) => void;
|
|
30
|
+
onFieldBlur: (fieldId: string) => void;
|
|
31
|
+
formError?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
4
34
|
/**
|
|
5
35
|
* Renders a complete form from a schema definition.
|
|
6
36
|
* Pairs with useFormState for state management.
|
|
7
|
-
*
|
|
8
|
-
* @param {Array} sections — form schema sections
|
|
9
|
-
* @param {Object} values — current form values
|
|
10
|
-
* @param {Object} errors — current validation errors
|
|
11
|
-
* @param {Object} touched — which fields have been touched
|
|
12
|
-
* @param {Function} onFieldChange — (fieldId, value) => void
|
|
13
|
-
* @param {Function} onFieldBlur — (fieldId) => void
|
|
14
|
-
* @param {string} formError — top-level form error (e.g. submission failure)
|
|
15
37
|
*/
|
|
16
38
|
export default function FormRenderer({
|
|
17
39
|
sections = [],
|
|
@@ -21,7 +43,7 @@ export default function FormRenderer({
|
|
|
21
43
|
onFieldChange,
|
|
22
44
|
onFieldBlur,
|
|
23
45
|
formError,
|
|
24
|
-
}) {
|
|
46
|
+
}: FormRendererProps) {
|
|
25
47
|
return (
|
|
26
48
|
<div className="space-y-8">
|
|
27
49
|
{formError ? (
|