@flowtomic/logic 0.1.0 → 0.1.2
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/hooks/useAnimatedIndicator/index.d.ts +3 -0
- package/dist/hooks/useAnimatedIndicator/index.d.ts.map +1 -0
- package/dist/hooks/useAnimatedIndicator/index.js +1 -0
- package/dist/hooks/useAnimatedIndicator/useAnimatedIndicator.d.ts +94 -0
- package/dist/hooks/useAnimatedIndicator/useAnimatedIndicator.d.ts.map +1 -0
- package/dist/hooks/useAnimatedIndicator/useAnimatedIndicator.js +228 -0
- package/dist/hooks/useGenealogy/index.d.ts +3 -0
- package/dist/hooks/useGenealogy/index.d.ts.map +1 -0
- package/dist/hooks/useGenealogy/index.js +1 -0
- package/dist/hooks/useGenealogy/useGenealogy.d.ts +96 -0
- package/dist/hooks/useGenealogy/useGenealogy.d.ts.map +1 -0
- package/dist/hooks/useGenealogy/useGenealogy.js +335 -0
- package/dist/hooks/useMoblile/index.js +1 -868
- package/dist/hooks/useProjectProgress/index.d.ts +3 -0
- package/dist/hooks/useProjectProgress/index.d.ts.map +1 -0
- package/dist/hooks/useProjectProgress/index.js +1 -0
- package/dist/hooks/useProjectProgress/useProjectProgress.d.ts +103 -0
- package/dist/hooks/useProjectProgress/useProjectProgress.d.ts.map +1 -0
- package/dist/hooks/useProjectProgress/useProjectProgress.js +124 -0
- package/dist/hooks/useProjectStats/index.d.ts +3 -0
- package/dist/hooks/useProjectStats/index.d.ts.map +1 -0
- package/dist/hooks/useProjectStats/index.js +1 -0
- package/dist/hooks/useProjectStats/useProjectStats.d.ts +130 -0
- package/dist/hooks/useProjectStats/useProjectStats.d.ts.map +1 -0
- package/dist/hooks/useProjectStats/useProjectStats.js +87 -0
- package/dist/hooks/useResizable/index.d.ts +1 -1
- package/dist/hooks/useResizable/index.d.ts.map +1 -1
- package/dist/hooks/useScriptEditor/index.d.ts +3 -0
- package/dist/hooks/useScriptEditor/index.d.ts.map +1 -0
- package/dist/hooks/useScriptEditor/index.js +1 -0
- package/dist/hooks/useScriptEditor/useScriptEditor.d.ts +147 -0
- package/dist/hooks/useScriptEditor/useScriptEditor.d.ts.map +1 -0
- package/dist/hooks/useScriptEditor/useScriptEditor.js +247 -0
- package/dist/hooks/useStatCard/useStatCard.d.ts +44 -5
- package/dist/hooks/useStatCard/useStatCard.d.ts.map +1 -1
- package/dist/hooks/useStatCard/useStatCard.js +81 -20
- package/dist/hooks/useTimeTracker/index.d.ts +3 -0
- package/dist/hooks/useTimeTracker/index.d.ts.map +1 -0
- package/dist/hooks/useTimeTracker/index.js +1 -0
- package/dist/hooks/useTimeTracker/useTimeTracker.d.ts +106 -0
- package/dist/hooks/useTimeTracker/useTimeTracker.d.ts.map +1 -0
- package/dist/hooks/useTimeTracker/useTimeTracker.js +143 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/package.json +2 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useScriptEditor - Headless UI Hook
|
|
3
|
+
*
|
|
4
|
+
* Fornece apenas: lógica, estado, processamento e API
|
|
5
|
+
* NÃO fornece: markup, styles ou implementações pré-construídas
|
|
6
|
+
*
|
|
7
|
+
* Padrão Headless UI: você controla o markup e styles
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* function MyScriptEditor() {
|
|
12
|
+
* const {
|
|
13
|
+
* script,
|
|
14
|
+
* setScript,
|
|
15
|
+
* terminalLines,
|
|
16
|
+
* preview,
|
|
17
|
+
* isRunning,
|
|
18
|
+
* isConnected,
|
|
19
|
+
* executeScript,
|
|
20
|
+
* stopExecution,
|
|
21
|
+
* clearTerminal,
|
|
22
|
+
* } = useScriptEditor({
|
|
23
|
+
* wsUrl: "ws://localhost:8080/ws/terminal",
|
|
24
|
+
* executeScript: async (script) => {
|
|
25
|
+
* const response = await fetch("/api/scripts/execute", {
|
|
26
|
+
* method: "POST",
|
|
27
|
+
* body: JSON.stringify({ script }),
|
|
28
|
+
* });
|
|
29
|
+
* return response.json();
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* return (
|
|
34
|
+
* <div>
|
|
35
|
+
* <textarea value={script} onChange={(e) => setScript(e.target.value)} />
|
|
36
|
+
* <button onClick={executeScript}>Executar</button>
|
|
37
|
+
* <div>
|
|
38
|
+
* {terminalLines.map((line) => (
|
|
39
|
+
* <div key={line.id}>{line.content}</div>
|
|
40
|
+
* ))}
|
|
41
|
+
* </div>
|
|
42
|
+
* </div>
|
|
43
|
+
* );
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
48
|
+
/**
|
|
49
|
+
* Hook headless para gerenciar editor de scripts com terminal interativo
|
|
50
|
+
*/
|
|
51
|
+
export function useScriptEditor(options = {}) {
|
|
52
|
+
const { wsUrl, executeScript: executeScriptFn, onOutput, onError, autoConnect = true, maxReconnectAttempts = 3, defaultScript = "", } = options;
|
|
53
|
+
const [script, setScript] = useState(defaultScript);
|
|
54
|
+
const [terminalLines, setTerminalLines] = useState([]);
|
|
55
|
+
const [preview, setPreview] = useState("");
|
|
56
|
+
const [activeTab, setActiveTab] = useState("terminal");
|
|
57
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
58
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
59
|
+
const wsRef = useRef(null);
|
|
60
|
+
const reconnectAttemptsRef = useRef(0);
|
|
61
|
+
const shouldReconnectRef = useRef(true);
|
|
62
|
+
// Adicionar linha ao terminal
|
|
63
|
+
const addTerminalLine = useCallback((type, content) => {
|
|
64
|
+
const line = {
|
|
65
|
+
id: `${Date.now()}-${Math.random()}`,
|
|
66
|
+
type,
|
|
67
|
+
content,
|
|
68
|
+
timestamp: new Date(),
|
|
69
|
+
};
|
|
70
|
+
setTerminalLines((prev) => [...prev, line]);
|
|
71
|
+
onOutput?.(line);
|
|
72
|
+
}, [onOutput]);
|
|
73
|
+
// Conectar WebSocket
|
|
74
|
+
const connectWebSocket = useCallback(() => {
|
|
75
|
+
if (!wsUrl)
|
|
76
|
+
return;
|
|
77
|
+
// Verificar se já existe uma conexão ativa
|
|
78
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Fechar conexão anterior se existir
|
|
82
|
+
if (wsRef.current) {
|
|
83
|
+
try {
|
|
84
|
+
wsRef.current.close();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Ignorar erros ao fechar
|
|
88
|
+
}
|
|
89
|
+
wsRef.current = null;
|
|
90
|
+
}
|
|
91
|
+
if (!shouldReconnectRef.current)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
const ws = new WebSocket(wsUrl);
|
|
95
|
+
wsRef.current = ws;
|
|
96
|
+
ws.onopen = () => {
|
|
97
|
+
setIsConnected(true);
|
|
98
|
+
reconnectAttemptsRef.current = 0;
|
|
99
|
+
};
|
|
100
|
+
ws.onmessage = (event) => {
|
|
101
|
+
try {
|
|
102
|
+
const data = JSON.parse(event.data);
|
|
103
|
+
if (data.type === "output") {
|
|
104
|
+
addTerminalLine("output", data.content);
|
|
105
|
+
}
|
|
106
|
+
else if (data.type === "error") {
|
|
107
|
+
addTerminalLine("error", data.content);
|
|
108
|
+
}
|
|
109
|
+
else if (data.type === "result") {
|
|
110
|
+
setPreview(JSON.stringify(data.content, null, 2));
|
|
111
|
+
setActiveTab("preview");
|
|
112
|
+
}
|
|
113
|
+
else if (data.type === "system") {
|
|
114
|
+
// Mensagens de sistema não são exibidas no terminal
|
|
115
|
+
console.log("[useScriptEditor] Mensagem de sistema:", data.content);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
addTerminalLine("output", event.data);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
ws.onerror = (error) => {
|
|
123
|
+
console.error("[useScriptEditor] WebSocket error:", error);
|
|
124
|
+
setIsConnected(false);
|
|
125
|
+
onError?.(new Error("Erro na conexão WebSocket"));
|
|
126
|
+
};
|
|
127
|
+
ws.onclose = (event) => {
|
|
128
|
+
setIsConnected(false);
|
|
129
|
+
wsRef.current = null;
|
|
130
|
+
// Tentar reconectar apenas se não excedeu tentativas e ainda deve reconectar
|
|
131
|
+
if (event.code !== 1000 &&
|
|
132
|
+
shouldReconnectRef.current &&
|
|
133
|
+
reconnectAttemptsRef.current < maxReconnectAttempts) {
|
|
134
|
+
reconnectAttemptsRef.current += 1;
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
if (shouldReconnectRef.current) {
|
|
137
|
+
connectWebSocket();
|
|
138
|
+
}
|
|
139
|
+
}, 3000);
|
|
140
|
+
}
|
|
141
|
+
else if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
|
|
142
|
+
console.warn("[useScriptEditor] Máximo de tentativas de reconexão atingido");
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
console.error("[useScriptEditor] Erro ao conectar WebSocket:", error);
|
|
148
|
+
setIsConnected(false);
|
|
149
|
+
onError?.(error instanceof Error ? error : new Error("Erro desconhecido ao conectar"));
|
|
150
|
+
}
|
|
151
|
+
}, [wsUrl, maxReconnectAttempts, addTerminalLine, onError]);
|
|
152
|
+
// Conectar WebSocket quando montar (se autoConnect)
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (autoConnect && wsUrl) {
|
|
155
|
+
const connectTimeout = setTimeout(() => {
|
|
156
|
+
connectWebSocket();
|
|
157
|
+
}, 100);
|
|
158
|
+
return () => {
|
|
159
|
+
clearTimeout(connectTimeout);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}, [autoConnect, wsUrl, connectWebSocket]);
|
|
163
|
+
// Limpar conexão ao desmontar
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
return () => {
|
|
166
|
+
shouldReconnectRef.current = false;
|
|
167
|
+
if (wsRef.current) {
|
|
168
|
+
try {
|
|
169
|
+
wsRef.current.close(1000, "Component unmounting");
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Ignorar erros ao fechar
|
|
173
|
+
}
|
|
174
|
+
wsRef.current = null;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}, []);
|
|
178
|
+
// Executar script
|
|
179
|
+
const executeScript = useCallback(async () => {
|
|
180
|
+
if (!script.trim()) {
|
|
181
|
+
addTerminalLine("error", "❌ Script vazio");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
setIsRunning(true);
|
|
185
|
+
addTerminalLine("input", script);
|
|
186
|
+
try {
|
|
187
|
+
// Enviar script via WebSocket se conectado
|
|
188
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
189
|
+
wsRef.current.send(JSON.stringify({ type: "execute", script }));
|
|
190
|
+
}
|
|
191
|
+
else if (executeScriptFn) {
|
|
192
|
+
// Fallback para HTTP se WebSocket não estiver disponível
|
|
193
|
+
try {
|
|
194
|
+
const result = await executeScriptFn(script);
|
|
195
|
+
if (result.output) {
|
|
196
|
+
addTerminalLine("output", result.output);
|
|
197
|
+
}
|
|
198
|
+
if (result.error) {
|
|
199
|
+
addTerminalLine("error", result.error);
|
|
200
|
+
}
|
|
201
|
+
if (result.result) {
|
|
202
|
+
setPreview(JSON.stringify(result.result, null, 2));
|
|
203
|
+
setActiveTab("preview");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
addTerminalLine("error", `❌ Erro na requisição: ${error instanceof Error ? error.message : "Erro desconhecido"}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
addTerminalLine("error", "❌ Nenhuma forma de execução disponível (WebSocket ou HTTP)");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
addTerminalLine("error", `❌ Erro ao executar: ${error instanceof Error ? error.message : "Erro desconhecido"}`);
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
setIsRunning(false);
|
|
219
|
+
}
|
|
220
|
+
}, [script, executeScriptFn, addTerminalLine]);
|
|
221
|
+
// Parar execução
|
|
222
|
+
const stopExecution = useCallback(() => {
|
|
223
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
224
|
+
wsRef.current.send(JSON.stringify({ type: "stop" }));
|
|
225
|
+
}
|
|
226
|
+
setIsRunning(false);
|
|
227
|
+
}, []);
|
|
228
|
+
// Limpar terminal
|
|
229
|
+
const clearTerminal = useCallback(() => {
|
|
230
|
+
setTerminalLines([]);
|
|
231
|
+
setPreview("");
|
|
232
|
+
}, []);
|
|
233
|
+
return {
|
|
234
|
+
script,
|
|
235
|
+
setScript,
|
|
236
|
+
terminalLines,
|
|
237
|
+
preview,
|
|
238
|
+
activeTab,
|
|
239
|
+
setActiveTab,
|
|
240
|
+
isRunning,
|
|
241
|
+
isConnected,
|
|
242
|
+
executeScript,
|
|
243
|
+
stopExecution,
|
|
244
|
+
clearTerminal,
|
|
245
|
+
addTerminalLine,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```tsx
|
|
11
|
+
* // Cálculo automático do delta a partir de lastMonth
|
|
11
12
|
* function MyCustomStatCard() {
|
|
12
13
|
* const { formattedValue, trend, getCardProps } = useStatCard({
|
|
13
14
|
* value: 122380,
|
|
14
|
-
* delta: 15.
|
|
15
|
-
* lastMonth: 105922,
|
|
15
|
+
* lastMonth: 105922, // delta será calculado automaticamente: +15.5%
|
|
16
16
|
* })
|
|
17
17
|
*
|
|
18
18
|
* return (
|
|
@@ -22,35 +22,65 @@
|
|
|
22
22
|
* </div>
|
|
23
23
|
* )
|
|
24
24
|
* }
|
|
25
|
+
*
|
|
26
|
+
* // Ou forneça delta explicitamente para casos especiais
|
|
27
|
+
* function CustomDeltaCard() {
|
|
28
|
+
* const { formattedValue, trend } = useStatCard({
|
|
29
|
+
* value: 122380,
|
|
30
|
+
* delta: 15.1, // delta explícito tem prioridade
|
|
31
|
+
* lastMonth: 105922,
|
|
32
|
+
* })
|
|
33
|
+
* }
|
|
25
34
|
* ```
|
|
26
35
|
*/
|
|
27
36
|
export interface StatCardData {
|
|
28
37
|
/**
|
|
29
38
|
* Valor principal do card
|
|
30
39
|
*/
|
|
31
|
-
value: number;
|
|
40
|
+
value: number | string;
|
|
32
41
|
/**
|
|
33
42
|
* Percentual de variação (positivo ou negativo)
|
|
43
|
+
* Se não fornecido, será calculado automaticamente quando `lastMonth` estiver disponível.
|
|
44
|
+
* Fórmula: ((value - lastMonth) / lastMonth) * 100
|
|
34
45
|
*/
|
|
35
46
|
delta?: number;
|
|
36
47
|
/**
|
|
37
48
|
* Valor do mês anterior para comparação
|
|
49
|
+
* Se fornecido e `delta` não estiver definido, o delta será calculado automaticamente.
|
|
38
50
|
*/
|
|
39
51
|
lastMonth?: number;
|
|
40
52
|
/**
|
|
41
53
|
* Prefixo para formatação do valor (ex: '$', 'R$')
|
|
54
|
+
* Se `locale` e `currency` forem fornecidos, será ignorado em favor da formatação de moeda
|
|
42
55
|
*/
|
|
43
56
|
prefix?: string;
|
|
44
57
|
/**
|
|
45
58
|
* Sufixo para formatação do valor (ex: 'M', 'K')
|
|
46
59
|
*/
|
|
47
60
|
suffix?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Locale para formatação numérica (ex: 'pt-BR', 'en-US', 'de-DE')
|
|
63
|
+
* Padrão: 'pt-BR' (formata com ponto como separador de milhares)
|
|
64
|
+
*/
|
|
65
|
+
locale?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Código da moeda para formatação (ex: 'BRL', 'USD', 'EUR')
|
|
68
|
+
* Se fornecido, usa formatação de moeda do locale
|
|
69
|
+
*/
|
|
70
|
+
currency?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Como exibir a moeda ('symbol' | 'narrowSymbol' | 'code' | 'name')
|
|
73
|
+
* Padrão: 'symbol' (ex: R$ para BRL, $ para USD)
|
|
74
|
+
*/
|
|
75
|
+
currencyDisplay?: "symbol" | "narrowSymbol" | "code" | "name";
|
|
48
76
|
/**
|
|
49
77
|
* Função customizada de formatação do valor principal
|
|
78
|
+
* Tem prioridade sobre locale e currency
|
|
50
79
|
*/
|
|
51
80
|
format?: (value: number) => string;
|
|
52
81
|
/**
|
|
53
82
|
* Função customizada de formatação do valor do mês anterior
|
|
83
|
+
* Tem prioridade sobre locale e currency
|
|
54
84
|
*/
|
|
55
85
|
lastFormat?: (value: number) => string;
|
|
56
86
|
/**
|
|
@@ -124,11 +154,11 @@ export interface UseStatCardReturn {
|
|
|
124
154
|
*
|
|
125
155
|
* @example
|
|
126
156
|
* ```tsx
|
|
157
|
+
* // Cálculo automático do delta a partir de lastMonth
|
|
127
158
|
* function MyCustomStatCard() {
|
|
128
159
|
* const { formattedValue, trend, getCardProps } = useStatCard({
|
|
129
160
|
* value: 122380,
|
|
130
|
-
* delta: 15.
|
|
131
|
-
* lastMonth: 105922,
|
|
161
|
+
* lastMonth: 105922, // delta será calculado automaticamente: +15.5%
|
|
132
162
|
* })
|
|
133
163
|
*
|
|
134
164
|
* return (
|
|
@@ -138,6 +168,15 @@ export interface UseStatCardReturn {
|
|
|
138
168
|
* </div>
|
|
139
169
|
* )
|
|
140
170
|
* }
|
|
171
|
+
*
|
|
172
|
+
* // Ou forneça delta explicitamente para casos especiais
|
|
173
|
+
* function CustomDeltaCard() {
|
|
174
|
+
* const { formattedValue, trend } = useStatCard({
|
|
175
|
+
* value: 122380,
|
|
176
|
+
* delta: 15.1, // delta explícito tem prioridade
|
|
177
|
+
* lastMonth: 105922,
|
|
178
|
+
* })
|
|
179
|
+
* }
|
|
141
180
|
* ```
|
|
142
181
|
*/
|
|
143
182
|
export declare function useStatCard(data: StatCardData): UseStatCardReturn;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStatCard.d.ts","sourceRoot":"","sources":["../../../src/hooks/useStatCard/useStatCard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"useStatCard.d.ts","sourceRoot":"","sources":["../../../src/hooks/useStatCard/useStatCard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAIH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAEvB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,eAAe,CAAC,EAAE,QAAQ,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;IAE9D;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAEnC;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAEvC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,UAAU,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,OAAO,EAAE,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC;IAEjD;;OAEG;IACH,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAElC;;OAEG;IACH,KAAK,EAAE,SAAS,CAAC;IAEjB;;OAEG;IACH,UAAU,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,YAAY,EAAE,CAAC,KAAK,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK;QAC7F,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IAEF;;OAEG;IACH,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAwCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,iBAAiB,CAwHjE"}
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```tsx
|
|
11
|
+
* // Cálculo automático do delta a partir de lastMonth
|
|
11
12
|
* function MyCustomStatCard() {
|
|
12
13
|
* const { formattedValue, trend, getCardProps } = useStatCard({
|
|
13
14
|
* value: 122380,
|
|
14
|
-
* delta: 15.
|
|
15
|
-
* lastMonth: 105922,
|
|
15
|
+
* lastMonth: 105922, // delta será calculado automaticamente: +15.5%
|
|
16
16
|
* })
|
|
17
17
|
*
|
|
18
18
|
* return (
|
|
@@ -22,23 +22,48 @@
|
|
|
22
22
|
* </div>
|
|
23
23
|
* )
|
|
24
24
|
* }
|
|
25
|
+
*
|
|
26
|
+
* // Ou forneça delta explicitamente para casos especiais
|
|
27
|
+
* function CustomDeltaCard() {
|
|
28
|
+
* const { formattedValue, trend } = useStatCard({
|
|
29
|
+
* value: 122380,
|
|
30
|
+
* delta: 15.1, // delta explícito tem prioridade
|
|
31
|
+
* lastMonth: 105922,
|
|
32
|
+
* })
|
|
33
|
+
* }
|
|
25
34
|
* ```
|
|
26
35
|
*/
|
|
27
36
|
import { useCallback, useMemo } from "react";
|
|
28
37
|
/**
|
|
29
|
-
* Formata um número seguindo padrões comuns
|
|
30
|
-
* - Valores >= 1.000.000: formata como "X.XM"
|
|
31
|
-
* - Valores >= 1.000: formata com separador de milhares
|
|
32
|
-
* - Valores < 1.000: retorna
|
|
38
|
+
* Formata um número seguindo padrões comuns com suporte a locale
|
|
39
|
+
* - Valores >= 1.000.000: formata como "X.XM" (respeitando locale para decimais)
|
|
40
|
+
* - Valores >= 1.000: formata com separador de milhares conforme locale
|
|
41
|
+
* - Valores < 1.000: retorna formatado conforme locale
|
|
33
42
|
*/
|
|
34
|
-
function formatNumberDefault(n) {
|
|
43
|
+
function formatNumberDefault(n, locale = "pt-BR") {
|
|
35
44
|
if (n >= 1_000_000) {
|
|
36
|
-
|
|
45
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
46
|
+
minimumFractionDigits: 1,
|
|
47
|
+
maximumFractionDigits: 1,
|
|
48
|
+
});
|
|
49
|
+
return `${formatter.format(n / 1_000_000)}M`;
|
|
37
50
|
}
|
|
38
51
|
if (n >= 1_000) {
|
|
39
|
-
return
|
|
52
|
+
return new Intl.NumberFormat(locale).format(n);
|
|
40
53
|
}
|
|
41
|
-
return
|
|
54
|
+
return new Intl.NumberFormat(locale).format(n);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Formata um número como moeda usando locale
|
|
58
|
+
*/
|
|
59
|
+
function formatCurrency(n, locale = "pt-BR", currency = "BRL", currencyDisplay = "symbol") {
|
|
60
|
+
return new Intl.NumberFormat(locale, {
|
|
61
|
+
style: "currency",
|
|
62
|
+
currency,
|
|
63
|
+
currencyDisplay,
|
|
64
|
+
minimumFractionDigits: 0,
|
|
65
|
+
maximumFractionDigits: 2,
|
|
66
|
+
}).format(n);
|
|
42
67
|
}
|
|
43
68
|
/**
|
|
44
69
|
* Hook Headless UI para StatCard
|
|
@@ -48,11 +73,11 @@ function formatNumberDefault(n) {
|
|
|
48
73
|
*
|
|
49
74
|
* @example
|
|
50
75
|
* ```tsx
|
|
76
|
+
* // Cálculo automático do delta a partir de lastMonth
|
|
51
77
|
* function MyCustomStatCard() {
|
|
52
78
|
* const { formattedValue, trend, getCardProps } = useStatCard({
|
|
53
79
|
* value: 122380,
|
|
54
|
-
* delta: 15.
|
|
55
|
-
* lastMonth: 105922,
|
|
80
|
+
* lastMonth: 105922, // delta será calculado automaticamente: +15.5%
|
|
56
81
|
* })
|
|
57
82
|
*
|
|
58
83
|
* return (
|
|
@@ -62,10 +87,32 @@ function formatNumberDefault(n) {
|
|
|
62
87
|
* </div>
|
|
63
88
|
* )
|
|
64
89
|
* }
|
|
90
|
+
*
|
|
91
|
+
* // Ou forneça delta explicitamente para casos especiais
|
|
92
|
+
* function CustomDeltaCard() {
|
|
93
|
+
* const { formattedValue, trend } = useStatCard({
|
|
94
|
+
* value: 122380,
|
|
95
|
+
* delta: 15.1, // delta explícito tem prioridade
|
|
96
|
+
* lastMonth: 105922,
|
|
97
|
+
* })
|
|
98
|
+
* }
|
|
65
99
|
* ```
|
|
66
100
|
*/
|
|
67
101
|
export function useStatCard(data) {
|
|
68
|
-
const { value, delta
|
|
102
|
+
const { value, delta: explicitDelta, lastMonth, prefix = "", suffix = "", locale = "pt-BR", currency, currencyDisplay = "symbol", format, lastFormat, positive: explicitPositive, } = data;
|
|
103
|
+
// Calcula delta automaticamente se não fornecido mas lastMonth estiver disponível
|
|
104
|
+
const delta = useMemo(() => {
|
|
105
|
+
// Se delta foi fornecido explicitamente, usa ele (prioridade)
|
|
106
|
+
if (explicitDelta !== undefined) {
|
|
107
|
+
return explicitDelta;
|
|
108
|
+
}
|
|
109
|
+
// Se lastMonth foi fornecido, calcula o delta automaticamente
|
|
110
|
+
if (lastMonth !== undefined && lastMonth !== 0) {
|
|
111
|
+
return ((Number(value) - Number(lastMonth)) / Number(lastMonth)) * 100;
|
|
112
|
+
}
|
|
113
|
+
// Caso contrário, retorna 0
|
|
114
|
+
return 0;
|
|
115
|
+
}, [value, lastMonth, explicitDelta]);
|
|
69
116
|
// Calcula se a tendência é positiva
|
|
70
117
|
const isPositive = useMemo(() => {
|
|
71
118
|
if (explicitPositive !== undefined) {
|
|
@@ -89,10 +136,16 @@ export function useStatCard(data) {
|
|
|
89
136
|
// Formata o valor principal
|
|
90
137
|
const formattedValue = useMemo(() => {
|
|
91
138
|
if (format) {
|
|
92
|
-
return format(value);
|
|
139
|
+
return format(Number(value));
|
|
140
|
+
}
|
|
141
|
+
const numValue = Number(value);
|
|
142
|
+
if (currency) {
|
|
143
|
+
// Se currency está definido, usa formatação de moeda (ignora prefix)
|
|
144
|
+
return (formatCurrency(numValue, locale, currency, currencyDisplay) + (suffix ? ` ${suffix}` : ""));
|
|
93
145
|
}
|
|
94
|
-
|
|
95
|
-
|
|
146
|
+
// Caso contrário, usa formatação numérica padrão com prefix/suffix
|
|
147
|
+
return prefix + formatNumberDefault(numValue, locale) + suffix;
|
|
148
|
+
}, [value, prefix, suffix, locale, currency, currencyDisplay, format]);
|
|
96
149
|
// Formata o valor do mês anterior
|
|
97
150
|
const formattedLastMonth = useMemo(() => {
|
|
98
151
|
if (!lastMonth)
|
|
@@ -100,8 +153,13 @@ export function useStatCard(data) {
|
|
|
100
153
|
if (lastFormat) {
|
|
101
154
|
return lastFormat(lastMonth);
|
|
102
155
|
}
|
|
103
|
-
|
|
104
|
-
|
|
156
|
+
if (currency) {
|
|
157
|
+
// Se currency está definido, usa formatação de moeda (ignora prefix)
|
|
158
|
+
return formatCurrency(lastMonth, locale, currency, currencyDisplay);
|
|
159
|
+
}
|
|
160
|
+
// Caso contrário, usa formatação numérica padrão com prefix/suffix
|
|
161
|
+
return prefix + formatNumberDefault(lastMonth, locale) + suffix;
|
|
162
|
+
}, [lastMonth, prefix, suffix, locale, currency, currencyDisplay, lastFormat]);
|
|
105
163
|
// Props helper para o card
|
|
106
164
|
const getCardProps = useCallback((props = {}) => {
|
|
107
165
|
return {
|
|
@@ -113,8 +171,11 @@ export function useStatCard(data) {
|
|
|
113
171
|
}, [formattedValue]);
|
|
114
172
|
// Função utilitária de formatação
|
|
115
173
|
const formatNumber = useCallback((n) => {
|
|
116
|
-
|
|
117
|
-
|
|
174
|
+
if (currency) {
|
|
175
|
+
return formatCurrency(n, locale, currency, currencyDisplay);
|
|
176
|
+
}
|
|
177
|
+
return formatNumberDefault(n, locale);
|
|
178
|
+
}, [locale, currency, currencyDisplay]);
|
|
118
179
|
return {
|
|
119
180
|
formattedValue,
|
|
120
181
|
formattedLastMonth,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/useTimeTracker/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useTimeTracker } from "./useTimeTracker";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTimeTracker - Headless UI Hook
|
|
3
|
+
*
|
|
4
|
+
* Fornece apenas: lógica, estado, processamento e API
|
|
5
|
+
* NÃO fornece: markup, styles ou implementações pré-construídas
|
|
6
|
+
*
|
|
7
|
+
* Padrão Headless UI: você controla o markup e styles
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* function MyTimer() {
|
|
12
|
+
* const { time, formattedTime, isRunning, start, pause, stop, reset } = useTimeTracker({
|
|
13
|
+
* initialTime: 0,
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* return (
|
|
17
|
+
* <div>
|
|
18
|
+
* <span>{formattedTime}</span>
|
|
19
|
+
* <button onClick={start}>Start</button>
|
|
20
|
+
* <button onClick={pause}>Pause</button>
|
|
21
|
+
* <button onClick={stop}>Stop</button>
|
|
22
|
+
* <button onClick={reset}>Reset</button>
|
|
23
|
+
* </div>
|
|
24
|
+
* )
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export interface UseTimeTrackerOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Tempo inicial em segundos
|
|
31
|
+
* @default 0
|
|
32
|
+
*/
|
|
33
|
+
initialTime?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Callback quando o timer é pausado
|
|
36
|
+
*/
|
|
37
|
+
onPause?: (time: number) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Callback quando o timer é parado
|
|
40
|
+
*/
|
|
41
|
+
onStop?: (time: number) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Callback quando o timer é iniciado
|
|
44
|
+
*/
|
|
45
|
+
onStart?: (time: number) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Callback quando o timer é retomado
|
|
48
|
+
*/
|
|
49
|
+
onResume?: (time: number) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Callback quando o timer é resetado
|
|
52
|
+
*/
|
|
53
|
+
onReset?: () => void;
|
|
54
|
+
/**
|
|
55
|
+
* Formato de tempo customizado
|
|
56
|
+
* @default "HH:mm:ss"
|
|
57
|
+
*/
|
|
58
|
+
format?: "HH:mm:ss" | "mm:ss" | "ss" | ((seconds: number) => string);
|
|
59
|
+
}
|
|
60
|
+
export interface UseTimeTrackerReturn {
|
|
61
|
+
/**
|
|
62
|
+
* Tempo atual em segundos
|
|
63
|
+
*/
|
|
64
|
+
time: number;
|
|
65
|
+
/**
|
|
66
|
+
* Tempo formatado como string (HH:mm:ss por padrão)
|
|
67
|
+
*/
|
|
68
|
+
formattedTime: string;
|
|
69
|
+
/**
|
|
70
|
+
* Se o timer está rodando
|
|
71
|
+
*/
|
|
72
|
+
isRunning: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Se o timer está pausado
|
|
75
|
+
*/
|
|
76
|
+
isPaused: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Se o timer está parado (não iniciado ou resetado)
|
|
79
|
+
*/
|
|
80
|
+
isStopped: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Inicia o timer
|
|
83
|
+
*/
|
|
84
|
+
start: () => void;
|
|
85
|
+
/**
|
|
86
|
+
* Pausa o timer
|
|
87
|
+
*/
|
|
88
|
+
pause: () => void;
|
|
89
|
+
/**
|
|
90
|
+
* Para o timer (pausa e mantém o tempo)
|
|
91
|
+
*/
|
|
92
|
+
stop: () => void;
|
|
93
|
+
/**
|
|
94
|
+
* Retoma o timer após pausa
|
|
95
|
+
*/
|
|
96
|
+
resume: () => void;
|
|
97
|
+
/**
|
|
98
|
+
* Reseta o timer para o tempo inicial
|
|
99
|
+
*/
|
|
100
|
+
reset: () => void;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Hook headless para gerenciar timer
|
|
104
|
+
*/
|
|
105
|
+
export declare function useTimeTracker(options?: UseTimeTrackerOptions): UseTimeTrackerReturn;
|
|
106
|
+
//# sourceMappingURL=useTimeTracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTimeTracker.d.ts","sourceRoot":"","sources":["../../../src/hooks/useTimeTracker/useTimeTracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhC;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAElC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;OAGG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;CACtE;AAED,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,SAAS,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,SAAS,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAElB;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,IAAI,CAAC;IAEnB;;OAEG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,oBAAoB,CAyIxF"}
|