@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.
Files changed (78) hide show
  1. package/dist/components/library/cards/SemanticTableCard.d.ts +1 -1
  2. package/dist/components/library/chat/ChatBar.d.ts +14 -11
  3. package/dist/components/library/chat/ChatBar.js +2 -3
  4. package/dist/components/library/chat/ChatBar.js.map +1 -1
  5. package/dist/components/library/chat/ChatInput.d.ts +9 -8
  6. package/dist/components/library/chat/ChatInput.js.map +1 -1
  7. package/dist/components/library/chat/ChatMessage.d.ts +17 -4
  8. package/dist/components/library/chat/ChatMessage.js.map +1 -1
  9. package/dist/components/library/chat/ChatMessageList.d.ts +11 -8
  10. package/dist/components/library/chat/ChatMessageList.js.map +1 -1
  11. package/dist/components/library/chat/ChatPanel.d.ts +16 -12
  12. package/dist/components/library/chat/ChatPanel.js +8 -9
  13. package/dist/components/library/chat/ChatPanel.js.map +1 -1
  14. package/dist/components/library/chat/ChatSuggestions.d.ts +5 -4
  15. package/dist/components/library/chat/ChatSuggestions.js +2 -3
  16. package/dist/components/library/chat/ChatSuggestions.js.map +1 -1
  17. package/dist/components/library/chat/ChatToolCall.d.ts +11 -3
  18. package/dist/components/library/chat/ChatToolCall.js.map +1 -1
  19. package/dist/components/library/chat/ChatTypingIndicator.d.ts +4 -3
  20. package/dist/components/library/chat/ChatTypingIndicator.js +2 -3
  21. package/dist/components/library/chat/ChatTypingIndicator.js.map +1 -1
  22. package/dist/components/library/chat/ChatWelcome.d.ts +9 -7
  23. package/dist/components/library/chat/ChatWelcome.js +6 -7
  24. package/dist/components/library/chat/ChatWelcome.js.map +1 -1
  25. package/dist/components/library/chat/index.d.ts +10 -0
  26. package/dist/components/library/chat/useChatState.d.ts +36 -11
  27. package/dist/components/library/chat/useChatState.js +63 -46
  28. package/dist/components/library/chat/useChatState.js.map +1 -1
  29. package/dist/components/library/data/DataModeProvider.d.ts +15 -11
  30. package/dist/components/library/data/DataModeProvider.js +1 -1
  31. package/dist/components/library/data/DataModeProvider.js.map +1 -1
  32. package/dist/components/library/data/DataModeToggle.d.ts +4 -3
  33. package/dist/components/library/data/DataModeToggle.js +4 -5
  34. package/dist/components/library/data/DataModeToggle.js.map +1 -1
  35. package/dist/components/library/data/chartDataProvider.d.ts +41 -3
  36. package/dist/components/library/data/filterUtils.d.ts +38 -9
  37. package/dist/components/library/data/filterUtils.js.map +1 -1
  38. package/dist/components/library/data/useDataSource.d.ts +6 -4
  39. package/dist/components/library/data/useDataSource.js.map +1 -1
  40. package/dist/components/library/data/usePageFilters.d.ts +31 -5
  41. package/dist/components/library/data/usePageFilters.js +6 -2
  42. package/dist/components/library/data/usePageFilters.js.map +1 -1
  43. package/dist/components/library/index.d.ts +92 -73
  44. package/dist/components/library/index.js +25 -25
  45. package/dist/components/library/index.js.map +1 -1
  46. package/dist/components/library/skeletons/CardSkeleton.d.ts +5 -4
  47. package/dist/components/library/skeletons/CardSkeleton.js +2 -3
  48. package/dist/components/library/skeletons/CardSkeleton.js.map +1 -1
  49. package/dist/components/library/theme/AppThemeProvider.d.ts +13 -50
  50. package/dist/components/library/theme/AppThemeProvider.js.map +1 -1
  51. package/dist/components/library/theme/tokens.d.ts +45 -44
  52. package/dist/components/library/theme/tokens.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/components/library/cards/SemanticMetricCard.tsx +1 -1
  55. package/src/components/library/cards/SemanticTableCard.tsx +3 -3
  56. package/src/components/library/chat/{ChatBar.jsx → ChatBar.tsx} +19 -8
  57. package/src/components/library/chat/{ChatInput.jsx → ChatInput.tsx} +13 -11
  58. package/src/components/library/chat/{ChatMessage.jsx → ChatMessage.tsx} +22 -9
  59. package/src/components/library/chat/{ChatMessageList.jsx → ChatMessageList.tsx} +13 -11
  60. package/src/components/library/chat/{ChatPanel.jsx → ChatPanel.tsx} +16 -13
  61. package/src/components/library/chat/{ChatSuggestions.jsx → ChatSuggestions.tsx} +6 -5
  62. package/src/components/library/chat/{ChatToolCall.jsx → ChatToolCall.tsx} +14 -4
  63. package/src/components/library/chat/{ChatTypingIndicator.jsx → ChatTypingIndicator.tsx} +5 -2
  64. package/src/components/library/chat/{ChatWelcome.jsx → ChatWelcome.tsx} +9 -7
  65. package/src/components/library/chat/index.tsx +26 -0
  66. package/src/components/library/chat/useChatState.tsx +181 -0
  67. package/src/components/library/data/{DataModeProvider.jsx → DataModeProvider.tsx} +25 -8
  68. package/src/components/library/data/{DataModeToggle.jsx → DataModeToggle.tsx} +5 -2
  69. package/src/components/library/data/{chartDataProvider.jsx → chartDataProvider.tsx} +49 -5
  70. package/src/components/library/data/{filterUtils.jsx → filterUtils.tsx} +58 -12
  71. package/src/components/library/data/{useDataSource.jsx → useDataSource.tsx} +9 -2
  72. package/src/components/library/data/{usePageFilters.jsx → usePageFilters.tsx} +49 -9
  73. package/src/components/library/{index.jsx → index.ts} +14 -14
  74. package/src/components/library/skeletons/{CardSkeleton.jsx → CardSkeleton.tsx} +5 -4
  75. package/src/components/library/theme/{AppThemeProvider.jsx → AppThemeProvider.tsx} +20 -7
  76. package/src/components/library/theme/{tokens.jsx → tokens.tsx} +37 -3
  77. package/src/components/library/chat/index.jsx +0 -10
  78. 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
- const DataModeContext = React.createContext({
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)) setModeState(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
- const SEMANTIC_DATASETS = {
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({ data = [], filters = [], defaultSort = null } = {}) {
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
- import React from "react";
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
-