@schandlergarcia/sf-web-components 1.8.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/SemanticTableCard.d.ts +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/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/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 +1 -1
- package/src/components/library/cards/SemanticMetricCard.tsx +1 -1
- package/src/components/library/cards/SemanticTableCard.tsx +3 -3
- 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/{index.jsx → index.ts} +14 -14
- 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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from "react";
|
|
2
|
+
import { ToolCall } from "./ChatToolCall";
|
|
3
|
+
|
|
4
|
+
let _nextId = 1;
|
|
5
|
+
function uid(): string {
|
|
6
|
+
return `msg-${Date.now()}-${_nextId++}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ChatMessage {
|
|
10
|
+
id: string;
|
|
11
|
+
role: "user" | "assistant" | "system";
|
|
12
|
+
content?: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
components?: unknown[];
|
|
15
|
+
toolCalls?: ToolCall[];
|
|
16
|
+
isError?: boolean;
|
|
17
|
+
isStreaming?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ChatStateHelpers {
|
|
21
|
+
addMessage: (msg: Partial<ChatMessage>) => string;
|
|
22
|
+
updateMessage: (id: string, updates: Partial<ChatMessage>) => void;
|
|
23
|
+
appendChunk: (id: string, chunk: string) => void;
|
|
24
|
+
setStreaming: (streaming: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseChatStateOptions {
|
|
28
|
+
initialMessages?: Partial<ChatMessage>[];
|
|
29
|
+
onSend?: (
|
|
30
|
+
userMessage: ChatMessage,
|
|
31
|
+
allMessages: ChatMessage[],
|
|
32
|
+
helpers: ChatStateHelpers
|
|
33
|
+
) => Promise<Partial<ChatMessage> | void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseChatStateReturn {
|
|
37
|
+
messages: ChatMessage[];
|
|
38
|
+
isLoading: boolean;
|
|
39
|
+
isStreaming: boolean;
|
|
40
|
+
error: string | null;
|
|
41
|
+
sendMessage: (content: string, extra?: Partial<ChatMessage>) => Promise<void>;
|
|
42
|
+
addMessage: (msg: Partial<ChatMessage>) => string;
|
|
43
|
+
updateMessage: (id: string, updates: Partial<ChatMessage>) => void;
|
|
44
|
+
appendChunk: (id: string, chunk: string) => void;
|
|
45
|
+
removeMessage: (id: string) => void;
|
|
46
|
+
clearMessages: () => void;
|
|
47
|
+
retryLast: () => void;
|
|
48
|
+
setError: (error: string | null) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Core state management hook for AI chat.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const chat = useChatState({
|
|
56
|
+
* onSend: async (msg, history) => {
|
|
57
|
+
* const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ messages: history }) });
|
|
58
|
+
* const data = await res.json();
|
|
59
|
+
* return { role: "assistant", content: data.reply, components: data.components };
|
|
60
|
+
* },
|
|
61
|
+
* });
|
|
62
|
+
*/
|
|
63
|
+
export default function useChatState({ initialMessages = [], onSend }: UseChatStateOptions = {}): UseChatStateReturn {
|
|
64
|
+
const [messages, setMessages] = useState<ChatMessage[]>(() =>
|
|
65
|
+
initialMessages.map((m) => ({
|
|
66
|
+
id: uid(),
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
role: m.role || "user",
|
|
69
|
+
...m
|
|
70
|
+
} as ChatMessage))
|
|
71
|
+
);
|
|
72
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
73
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
74
|
+
const [error, setError] = useState<string | null>(null);
|
|
75
|
+
const onSendRef = useRef(onSend);
|
|
76
|
+
onSendRef.current = onSend;
|
|
77
|
+
|
|
78
|
+
const addMessage = useCallback((msg: Partial<ChatMessage>): string => {
|
|
79
|
+
const full: ChatMessage = {
|
|
80
|
+
id: uid(),
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
role: msg.role || "assistant",
|
|
83
|
+
...msg
|
|
84
|
+
} as ChatMessage;
|
|
85
|
+
setMessages((prev) => [...prev, full]);
|
|
86
|
+
return full.id;
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const updateMessage = useCallback((id: string, updates: Partial<ChatMessage>) => {
|
|
90
|
+
setMessages((prev) =>
|
|
91
|
+
prev.map((m) => (m.id === id ? { ...m, ...updates } : m))
|
|
92
|
+
);
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
const appendChunk = useCallback((id: string, chunk: string) => {
|
|
96
|
+
setMessages((prev) =>
|
|
97
|
+
prev.map((m) =>
|
|
98
|
+
m.id === id ? { ...m, content: (m.content ?? "") + chunk } : m
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const removeMessage = useCallback((id: string) => {
|
|
104
|
+
setMessages((prev) => prev.filter((m) => m.id !== id));
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const clearMessages = useCallback(() => {
|
|
108
|
+
setMessages([]);
|
|
109
|
+
setError(null);
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const sendMessage = useCallback(
|
|
113
|
+
async (content: string, extra: Partial<ChatMessage> = {}) => {
|
|
114
|
+
if (!content?.trim()) return;
|
|
115
|
+
setError(null);
|
|
116
|
+
|
|
117
|
+
const userMsg: ChatMessage = {
|
|
118
|
+
role: "user",
|
|
119
|
+
content: content.trim(),
|
|
120
|
+
...extra,
|
|
121
|
+
id: uid(),
|
|
122
|
+
timestamp: new Date().toISOString()
|
|
123
|
+
} as ChatMessage;
|
|
124
|
+
|
|
125
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
126
|
+
setIsLoading(true);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const allMsgs = [...messages, userMsg];
|
|
130
|
+
const result = await onSendRef.current?.(userMsg, allMsgs, {
|
|
131
|
+
addMessage,
|
|
132
|
+
updateMessage,
|
|
133
|
+
appendChunk,
|
|
134
|
+
setStreaming: setIsStreaming,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (result && typeof result === "object" && result.role) {
|
|
138
|
+
addMessage(result);
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to send message";
|
|
142
|
+
setError(errorMessage);
|
|
143
|
+
addMessage({
|
|
144
|
+
role: "system",
|
|
145
|
+
content: err instanceof Error ? err.message : "Something went wrong. Please try again.",
|
|
146
|
+
isError: true,
|
|
147
|
+
});
|
|
148
|
+
} finally {
|
|
149
|
+
setIsLoading(false);
|
|
150
|
+
setIsStreaming(false);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
[messages, addMessage, updateMessage, appendChunk]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const retryLast = useCallback(() => {
|
|
157
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
158
|
+
if (!lastUser) return;
|
|
159
|
+
|
|
160
|
+
const idx = messages.lastIndexOf(lastUser);
|
|
161
|
+
setMessages(messages.slice(0, idx));
|
|
162
|
+
setError(null);
|
|
163
|
+
|
|
164
|
+
sendMessage(lastUser.content || "");
|
|
165
|
+
}, [messages, sendMessage]);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
messages,
|
|
169
|
+
isLoading,
|
|
170
|
+
isStreaming,
|
|
171
|
+
error,
|
|
172
|
+
sendMessage,
|
|
173
|
+
addMessage,
|
|
174
|
+
updateMessage,
|
|
175
|
+
appendChunk,
|
|
176
|
+
removeMessage,
|
|
177
|
+
clearMessages,
|
|
178
|
+
retryLast,
|
|
179
|
+
setError,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export type DataMode = "sample" | "live";
|
|
4
|
+
|
|
5
|
+
export interface DataModeContextValue {
|
|
6
|
+
mode: DataMode;
|
|
7
|
+
isSample: boolean;
|
|
8
|
+
isLive: boolean;
|
|
9
|
+
toggle: () => void;
|
|
10
|
+
setMode: (mode: DataMode) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DataModeContext = React.createContext<DataModeContextValue>({
|
|
4
14
|
mode: "sample",
|
|
5
15
|
isSample: true,
|
|
6
16
|
isLive: false,
|
|
@@ -9,30 +19,37 @@ const DataModeContext = React.createContext({
|
|
|
9
19
|
});
|
|
10
20
|
|
|
11
21
|
const STORAGE_KEY = "app-data-mode";
|
|
12
|
-
const VALID_MODES = ["sample", "live"];
|
|
22
|
+
const VALID_MODES: DataMode[] = ["sample", "live"];
|
|
13
23
|
|
|
14
24
|
/**
|
|
15
25
|
* Read the current data mode from any component.
|
|
16
26
|
*
|
|
17
27
|
* @returns {{ mode: "sample"|"live", isSample: boolean, isLive: boolean, toggle: () => void, setMode: (mode) => void }}
|
|
18
28
|
*/
|
|
19
|
-
export function useDataMode() {
|
|
29
|
+
export function useDataMode(): DataModeContextValue {
|
|
20
30
|
return React.useContext(DataModeContext);
|
|
21
31
|
}
|
|
22
32
|
|
|
33
|
+
export interface DataModeProviderProps {
|
|
34
|
+
initialMode?: DataMode;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
/**
|
|
24
39
|
* Provides global data-mode state (sample vs live) to the component tree.
|
|
25
40
|
* Persists to localStorage so the choice survives page reloads.
|
|
26
41
|
*
|
|
27
42
|
* Wrap once in _app.js alongside AppThemeProvider.
|
|
28
43
|
*/
|
|
29
|
-
export default function DataModeProvider({ initialMode = "sample", children }) {
|
|
30
|
-
const [mode, setModeState] = React.useState(initialMode);
|
|
44
|
+
export default function DataModeProvider({ initialMode = "sample", children }: DataModeProviderProps) {
|
|
45
|
+
const [mode, setModeState] = React.useState<DataMode>(initialMode);
|
|
31
46
|
|
|
32
47
|
React.useEffect(() => {
|
|
33
48
|
try {
|
|
34
49
|
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
35
|
-
if (VALID_MODES.includes(stored))
|
|
50
|
+
if (stored && VALID_MODES.includes(stored as DataMode)) {
|
|
51
|
+
setModeState(stored as DataMode);
|
|
52
|
+
}
|
|
36
53
|
} catch {
|
|
37
54
|
// SSR or storage unavailable
|
|
38
55
|
}
|
|
@@ -46,11 +63,11 @@ export default function DataModeProvider({ initialMode = "sample", children }) {
|
|
|
46
63
|
}
|
|
47
64
|
}, [mode]);
|
|
48
65
|
|
|
49
|
-
const setMode = React.useCallback((m) => {
|
|
66
|
+
const setMode = React.useCallback((m: DataMode) => {
|
|
50
67
|
if (VALID_MODES.includes(m)) setModeState(m);
|
|
51
68
|
}, []);
|
|
52
69
|
|
|
53
|
-
const value = React.useMemo(
|
|
70
|
+
const value = React.useMemo<DataModeContextValue>(
|
|
54
71
|
() => ({
|
|
55
72
|
mode,
|
|
56
73
|
isSample: mode === "sample",
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import { useDataMode } from "./DataModeProvider";
|
|
3
2
|
import { BeakerIcon, SignalIcon } from "@heroicons/react/24/outline";
|
|
4
3
|
|
|
4
|
+
export interface DataModeToggleProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Pill toggle for switching between sample and live data modes.
|
|
7
10
|
* Place in the AppShell header next to the theme toggle.
|
|
8
11
|
*/
|
|
9
|
-
export default function DataModeToggle({ className = "" }) {
|
|
12
|
+
export default function DataModeToggle({ className = "" }: DataModeToggleProps) {
|
|
10
13
|
const { mode, toggle } = useDataMode();
|
|
11
14
|
const isSample = mode === "sample";
|
|
12
15
|
|
|
@@ -1,7 +1,52 @@
|
|
|
1
1
|
// Minimal semantic data provider (seed data + lookup helpers).
|
|
2
2
|
// This is intentionally local/in-memory for now; later we can swap to API-backed providers.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
export type ChangeType = "positive" | "negative" | "neutral";
|
|
5
|
+
export type MetricColor = "primary" | "success" | "warning" | "danger";
|
|
6
|
+
export type ColumnType = "currency" | "percentage" | "number" | "text";
|
|
7
|
+
|
|
8
|
+
export interface SemanticMetric {
|
|
9
|
+
metricId: string;
|
|
10
|
+
title: string;
|
|
11
|
+
subtitle: string;
|
|
12
|
+
value: string;
|
|
13
|
+
change?: string;
|
|
14
|
+
changeType?: ChangeType;
|
|
15
|
+
color?: MetricColor;
|
|
16
|
+
trend?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TableColumn {
|
|
20
|
+
key: string;
|
|
21
|
+
label: string;
|
|
22
|
+
type?: ColumnType;
|
|
23
|
+
sortable?: boolean;
|
|
24
|
+
mono?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TableRow {
|
|
28
|
+
id: number;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SemanticTable {
|
|
33
|
+
title: string;
|
|
34
|
+
subtitle: string;
|
|
35
|
+
columns: TableColumn[];
|
|
36
|
+
rows: TableRow[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SemanticDataset {
|
|
40
|
+
title: string;
|
|
41
|
+
metrics?: SemanticMetric[];
|
|
42
|
+
table?: SemanticTable;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SemanticDatasets {
|
|
46
|
+
[key: string]: SemanticDataset;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SEMANTIC_DATASETS: SemanticDatasets = {
|
|
5
50
|
sales_pipeline_qtr: {
|
|
6
51
|
title: "Sales Pipeline (Quarter)",
|
|
7
52
|
metrics: [
|
|
@@ -44,18 +89,17 @@ const SEMANTIC_DATASETS = {
|
|
|
44
89
|
}
|
|
45
90
|
};
|
|
46
91
|
|
|
47
|
-
export function listSemanticIds() {
|
|
92
|
+
export function listSemanticIds(): string[] {
|
|
48
93
|
return Object.keys(SEMANTIC_DATASETS);
|
|
49
94
|
}
|
|
50
95
|
|
|
51
|
-
export function getSemanticDataset(semanticId) {
|
|
96
|
+
export function getSemanticDataset(semanticId: string): SemanticDataset | null {
|
|
52
97
|
if (!semanticId) return null;
|
|
53
98
|
return SEMANTIC_DATASETS[semanticId] ?? null;
|
|
54
99
|
}
|
|
55
100
|
|
|
56
|
-
export function getSemanticMetric(semanticId, metricId) {
|
|
101
|
+
export function getSemanticMetric(semanticId: string, metricId: string): SemanticMetric | null {
|
|
57
102
|
const ds = getSemanticDataset(semanticId);
|
|
58
103
|
if (!ds?.metrics) return null;
|
|
59
104
|
return ds.metrics.find((m) => m.metricId === metricId) ?? null;
|
|
60
105
|
}
|
|
61
|
-
|
|
@@ -3,6 +3,52 @@
|
|
|
3
3
|
* Stateless — combine with usePageFilters hook for state management.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export type SortDirection = "asc" | "desc";
|
|
7
|
+
|
|
8
|
+
export interface DateRange {
|
|
9
|
+
start?: Date | string;
|
|
10
|
+
end?: Date | string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FilterType = "search" | "select" | "toggle" | "dateRange";
|
|
14
|
+
|
|
15
|
+
export interface BaseFilterDefinition {
|
|
16
|
+
id: string;
|
|
17
|
+
type: FilterType;
|
|
18
|
+
defaultValue?: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SearchFilterDefinition extends BaseFilterDefinition {
|
|
22
|
+
type: "search";
|
|
23
|
+
keys: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SelectFilterDefinition extends BaseFilterDefinition {
|
|
27
|
+
type: "select";
|
|
28
|
+
key: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ToggleFilterDefinition extends BaseFilterDefinition {
|
|
32
|
+
type: "toggle";
|
|
33
|
+
key: string;
|
|
34
|
+
matchValue?: any;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DateRangeFilterDefinition extends BaseFilterDefinition {
|
|
38
|
+
type: "dateRange";
|
|
39
|
+
key: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FilterDefinition =
|
|
43
|
+
| SearchFilterDefinition
|
|
44
|
+
| SelectFilterDefinition
|
|
45
|
+
| ToggleFilterDefinition
|
|
46
|
+
| DateRangeFilterDefinition;
|
|
47
|
+
|
|
48
|
+
export interface FilterValues {
|
|
49
|
+
[key: string]: any;
|
|
50
|
+
}
|
|
51
|
+
|
|
6
52
|
/**
|
|
7
53
|
* Text search across multiple keys.
|
|
8
54
|
* @param {Array} data
|
|
@@ -10,12 +56,12 @@
|
|
|
10
56
|
* @param {string[]} keys — object keys to search within
|
|
11
57
|
* @returns {Array} filtered data
|
|
12
58
|
*/
|
|
13
|
-
export function filterBySearch(data, query, keys = []) {
|
|
59
|
+
export function filterBySearch<T>(data: T[], query: string, keys: string[] = []): T[] {
|
|
14
60
|
if (!query || !query.trim()) return data;
|
|
15
61
|
const q = query.trim().toLowerCase();
|
|
16
62
|
return data.filter((row) =>
|
|
17
63
|
keys.some((key) => {
|
|
18
|
-
const val = row?.[key];
|
|
64
|
+
const val = (row as any)?.[key];
|
|
19
65
|
return val != null && String(val).toLowerCase().includes(q);
|
|
20
66
|
})
|
|
21
67
|
);
|
|
@@ -29,10 +75,10 @@ export function filterBySearch(data, query, keys = []) {
|
|
|
29
75
|
* @param {*} value — value to match (exact, case-insensitive for strings)
|
|
30
76
|
* @returns {Array}
|
|
31
77
|
*/
|
|
32
|
-
export function filterByValue(data, key, value) {
|
|
78
|
+
export function filterByValue<T>(data: T[], key: string, value: any): T[] {
|
|
33
79
|
if (value == null || value === "" || value === "all") return data;
|
|
34
80
|
return data.filter((row) => {
|
|
35
|
-
const v = row?.[key];
|
|
81
|
+
const v = (row as any)?.[key];
|
|
36
82
|
if (typeof v === "string" && typeof value === "string") {
|
|
37
83
|
return v.toLowerCase() === value.toLowerCase();
|
|
38
84
|
}
|
|
@@ -49,10 +95,10 @@ export function filterByValue(data, key, value) {
|
|
|
49
95
|
* @param {*} matchValue — value that key should equal when active (default: truthy check)
|
|
50
96
|
* @returns {Array}
|
|
51
97
|
*/
|
|
52
|
-
export function filterByToggle(data, key, isActive, matchValue) {
|
|
98
|
+
export function filterByToggle<T>(data: T[], key: string, isActive: boolean, matchValue?: any): T[] {
|
|
53
99
|
if (!isActive) return data;
|
|
54
100
|
return data.filter((row) => {
|
|
55
|
-
const v = row?.[key];
|
|
101
|
+
const v = (row as any)?.[key];
|
|
56
102
|
if (matchValue !== undefined) return v === matchValue;
|
|
57
103
|
return Boolean(v);
|
|
58
104
|
});
|
|
@@ -65,14 +111,14 @@ export function filterByToggle(data, key, isActive, matchValue) {
|
|
|
65
111
|
* @param {{ start?: Date|string, end?: Date|string }} range
|
|
66
112
|
* @returns {Array}
|
|
67
113
|
*/
|
|
68
|
-
export function filterByDateRange(data, key, range) {
|
|
114
|
+
export function filterByDateRange<T>(data: T[], key: string, range: DateRange | null): T[] {
|
|
69
115
|
if (!range) return data;
|
|
70
116
|
const start = range.start ? new Date(range.start) : null;
|
|
71
117
|
const end = range.end ? new Date(range.end) : null;
|
|
72
118
|
if (!start && !end) return data;
|
|
73
119
|
|
|
74
120
|
return data.filter((row) => {
|
|
75
|
-
const raw = row?.[key];
|
|
121
|
+
const raw = (row as any)?.[key];
|
|
76
122
|
if (raw == null) return false;
|
|
77
123
|
const d = raw instanceof Date ? raw : new Date(raw);
|
|
78
124
|
if (Number.isNaN(d.getTime())) return false;
|
|
@@ -89,12 +135,12 @@ export function filterByDateRange(data, key, range) {
|
|
|
89
135
|
* @param {"asc"|"desc"} direction
|
|
90
136
|
* @returns {Array} new sorted array
|
|
91
137
|
*/
|
|
92
|
-
export function sortByKey(data, key, direction = "asc") {
|
|
138
|
+
export function sortByKey<T>(data: T[], key: string, direction: SortDirection = "asc"): T[] {
|
|
93
139
|
if (!key) return data;
|
|
94
140
|
const dir = direction === "desc" ? -1 : 1;
|
|
95
141
|
return [...data].sort((a, b) => {
|
|
96
|
-
const av = a?.[key];
|
|
97
|
-
const bv = b?.[key];
|
|
142
|
+
const av = (a as any)?.[key];
|
|
143
|
+
const bv = (b as any)?.[key];
|
|
98
144
|
if (av == null && bv == null) return 0;
|
|
99
145
|
if (av == null) return -1 * dir;
|
|
100
146
|
if (bv == null) return 1 * dir;
|
|
@@ -112,7 +158,7 @@ export function sortByKey(data, key, direction = "asc") {
|
|
|
112
158
|
* @param {Object} values — current filter values keyed by filter id
|
|
113
159
|
* @returns {Array} filtered data
|
|
114
160
|
*/
|
|
115
|
-
export function applyFilters(data, filters = [], values = {}) {
|
|
161
|
+
export function applyFilters<T>(data: T[], filters: FilterDefinition[] = [], values: FilterValues = {}): T[] {
|
|
116
162
|
let result = data;
|
|
117
163
|
|
|
118
164
|
for (const filter of filters) {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { useDataMode } from "./DataModeProvider";
|
|
3
3
|
|
|
4
|
+
export type DataSourceValue<T> = T | (() => T);
|
|
5
|
+
|
|
6
|
+
export interface UseDataSourceOptions<T> {
|
|
7
|
+
sample: DataSourceValue<T>;
|
|
8
|
+
live: DataSourceValue<T>;
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Select between sample and live data based on the global data mode.
|
|
6
13
|
*
|
|
@@ -23,11 +30,11 @@ import { useDataMode } from "./DataModeProvider";
|
|
|
23
30
|
* live: () => computeFromAPI(apiData),
|
|
24
31
|
* });
|
|
25
32
|
*/
|
|
26
|
-
export default function useDataSource({ sample, live }) {
|
|
33
|
+
export default function useDataSource<T>({ sample, live }: UseDataSourceOptions<T>): T {
|
|
27
34
|
const { mode } = useDataMode();
|
|
28
35
|
|
|
29
36
|
return useMemo(() => {
|
|
30
37
|
const source = mode === "sample" ? sample : live;
|
|
31
|
-
return typeof source === "function" ? source() : source;
|
|
38
|
+
return typeof source === "function" ? (source as () => T)() : source;
|
|
32
39
|
}, [mode, sample, live]);
|
|
33
40
|
}
|
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
export { default as AppThemeProvider, useThemeMode } from "./theme/AppThemeProvider";
|
|
4
4
|
|
|
@@ -105,21 +105,21 @@ export { default as HeroUISeparator, Separator } from "./heroui/Separator";
|
|
|
105
105
|
export { default as HeroUIPagination, Pagination } from "./heroui/Pagination";
|
|
106
106
|
|
|
107
107
|
// Breadcrumb subcomponents for shadcn compatibility
|
|
108
|
-
export const Breadcrumb = ({ children, ...props }) => React.createElement('nav', { 'aria-label': 'breadcrumb', ...props }, children);
|
|
109
|
-
export const BreadcrumbList = ({ children, ...props }) => React.createElement('ol', { className: 'flex flex-wrap items-center gap-1.5 break-words text-sm text-slate-500 dark:text-slate-400', ...props }, children);
|
|
110
|
-
export const BreadcrumbItem = ({ children, ...props }) => React.createElement('li', { className: 'inline-flex items-center gap-1.5', ...props }, children);
|
|
111
|
-
export const BreadcrumbLink = ({ href, children, asChild, ...props }) => asChild ? React.createElement('span', props, children) : React.createElement('a', { href, className: 'transition-colors hover:text-slate-900 dark:hover:text-slate-50', ...props }, children);
|
|
112
|
-
export const BreadcrumbPage = ({ children, ...props }) => React.createElement('span', { role: 'link', 'aria-disabled': 'true', 'aria-current': 'page', className: 'font-normal text-slate-900 dark:text-slate-50', ...props }, children);
|
|
113
|
-
export const BreadcrumbSeparator = ({ children, ...props }) => React.createElement('li', { role: 'presentation', 'aria-hidden': 'true', ...props }, children ?? '/');
|
|
114
|
-
export const BreadcrumbEllipsis = (props) => React.createElement('span', { role: 'presentation', 'aria-hidden': 'true', ...props }, '...');
|
|
108
|
+
export const Breadcrumb = ({ children, ...props }: { children?: React.ReactNode; [key: string]: any }) => React.createElement('nav', { 'aria-label': 'breadcrumb', ...props }, children);
|
|
109
|
+
export const BreadcrumbList = ({ children, ...props }: { children?: React.ReactNode; [key: string]: any }) => React.createElement('ol', { className: 'flex flex-wrap items-center gap-1.5 break-words text-sm text-slate-500 dark:text-slate-400', ...props }, children);
|
|
110
|
+
export const BreadcrumbItem = ({ children, ...props }: { children?: React.ReactNode; [key: string]: any }) => React.createElement('li', { className: 'inline-flex items-center gap-1.5', ...props }, children);
|
|
111
|
+
export const BreadcrumbLink = ({ href, children, asChild, ...props }: { href?: string; children?: React.ReactNode; asChild?: boolean; [key: string]: any }) => asChild ? React.createElement('span', props, children) : React.createElement('a', { href, className: 'transition-colors hover:text-slate-900 dark:hover:text-slate-50', ...props }, children);
|
|
112
|
+
export const BreadcrumbPage = ({ children, ...props }: { children?: React.ReactNode; [key: string]: any }) => React.createElement('span', { role: 'link', 'aria-disabled': 'true', 'aria-current': 'page', className: 'font-normal text-slate-900 dark:text-slate-50', ...props }, children);
|
|
113
|
+
export const BreadcrumbSeparator = ({ children, ...props }: { children?: React.ReactNode; [key: string]: any }) => React.createElement('li', { role: 'presentation', 'aria-hidden': 'true', ...props }, children ?? '/');
|
|
114
|
+
export const BreadcrumbEllipsis = (props: { [key: string]: any }) => React.createElement('span', { role: 'presentation', 'aria-hidden': 'true', ...props }, '...');
|
|
115
115
|
|
|
116
116
|
// Pagination subcomponents for shadcn compatibility
|
|
117
|
-
export const PaginationContent = ({ children, ...props }) => React.createElement('ul', { className: 'flex flex-row items-center gap-1', ...props }, children);
|
|
118
|
-
export const PaginationItem = ({ children, ...props }) => React.createElement('li', props, children);
|
|
119
|
-
export const PaginationLink = ({ href, isActive, children, ...props }) => React.createElement('a', { href, 'aria-current': isActive ? 'page' : undefined, className: `inline-flex items-center justify-center rounded-md text-sm font-medium h-9 min-w-9 px-4 py-2 ${isActive ? 'bg-slate-900 text-white dark:bg-slate-50 dark:text-slate-900' : 'hover:bg-slate-100 dark:hover:bg-slate-800'}`, ...props }, children);
|
|
120
|
-
export const PaginationPrevious = ({ href, ...props }) => React.createElement(PaginationLink, { href, ...props }, 'Previous');
|
|
121
|
-
export const PaginationNext = ({ href, ...props }) => React.createElement(PaginationLink, { href, ...props }, 'Next');
|
|
122
|
-
export const PaginationEllipsis = (props) => React.createElement('span', { 'aria-hidden': true, ...props }, '...');
|
|
117
|
+
export const PaginationContent = ({ children, ...props }: { children?: React.ReactNode; [key: string]: any }) => React.createElement('ul', { className: 'flex flex-row items-center gap-1', ...props }, children);
|
|
118
|
+
export const PaginationItem = ({ children, ...props }: { children?: React.ReactNode; [key: string]: any }) => React.createElement('li', props, children);
|
|
119
|
+
export const PaginationLink = ({ href, isActive, children, ...props }: { href?: string; isActive?: boolean; children?: React.ReactNode; [key: string]: any }) => React.createElement('a', { href, 'aria-current': isActive ? 'page' : undefined, className: `inline-flex items-center justify-center rounded-md text-sm font-medium h-9 min-w-9 px-4 py-2 ${isActive ? 'bg-slate-900 text-white dark:bg-slate-50 dark:text-slate-900' : 'hover:bg-slate-100 dark:hover:bg-slate-800'}`, ...props }, children);
|
|
120
|
+
export const PaginationPrevious = ({ href, ...props }: { href?: string; [key: string]: any }) => React.createElement(PaginationLink, { href, ...props }, 'Previous');
|
|
121
|
+
export const PaginationNext = ({ href, ...props }: { href?: string; [key: string]: any }) => React.createElement(PaginationLink, { href, ...props }, 'Next');
|
|
122
|
+
export const PaginationEllipsis = (props: { [key: string]: any }) => React.createElement('span', { 'aria-hidden': true, ...props }, '...');
|
|
123
123
|
|
|
124
124
|
// HeroUI wrappers — overlays
|
|
125
125
|
export { default as HeroUIDrawer } from "./heroui/Drawer";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
export interface CardSkeletonProps {
|
|
2
|
+
lines?: number;
|
|
3
|
+
className?: string;
|
|
4
|
+
}
|
|
2
5
|
|
|
3
|
-
export default function CardSkeleton({ lines = 3, className = "" }) {
|
|
6
|
+
export default function CardSkeleton({ lines = 3, className = "" }: CardSkeletonProps) {
|
|
4
7
|
return (
|
|
5
8
|
<div
|
|
6
9
|
className={[
|
|
@@ -26,5 +29,3 @@ export default function CardSkeleton({ lines = 3, className = "" }) {
|
|
|
26
29
|
</div>
|
|
27
30
|
);
|
|
28
31
|
}
|
|
29
|
-
|
|
30
|
-
|