@flowtomic/logic 0.1.7 → 0.1.8
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/useDashboardLayout/index.d.ts +2 -2
- package/dist/hooks/useDashboardLayout/index.d.ts.map +1 -1
- package/dist/hooks/useDashboardLayout/useDashboardLayout.d.ts +21 -0
- package/dist/hooks/useDashboardLayout/useDashboardLayout.d.ts.map +1 -1
- package/dist/hooks/useResizable/index.d.ts +1 -1
- package/dist/hooks/useResizable/index.d.ts.map +1 -1
- package/dist/hooks/useResizable/useResizable.d.ts +5 -0
- package/dist/hooks/useResizable/useResizable.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +3 -3
- package/dist/hooks/useAnimatedIndicator/index.js +0 -1
- package/dist/hooks/useAnimatedIndicator/useAnimatedIndicator.js +0 -242
- package/dist/hooks/useAutocomplete/index.js +0 -1
- package/dist/hooks/useAutocomplete/useAutocomplete.js +0 -380
- package/dist/hooks/useDashboardLayout/index.js +0 -1
- package/dist/hooks/useDashboardLayout/useDashboardLayout.js +0 -175
- package/dist/hooks/useGenealogy/index.js +0 -1
- package/dist/hooks/useGenealogy/useGenealogy.js +0 -341
- package/dist/hooks/useMoblile/index.js +0 -1
- package/dist/hooks/useMoblile/useMobile.js +0 -15
- package/dist/hooks/useProjectProgress/index.js +0 -1
- package/dist/hooks/useProjectProgress/useProjectProgress.js +0 -124
- package/dist/hooks/useProjectStats/index.js +0 -1
- package/dist/hooks/useProjectStats/useProjectStats.js +0 -87
- package/dist/hooks/useReactTableBack/index.js +0 -1
- package/dist/hooks/useReactTableBack/useReactTableBack.js +0 -124
- package/dist/hooks/useReactTableFront/index.js +0 -1
- package/dist/hooks/useReactTableFront/useReactTableFront.js +0 -112
- package/dist/hooks/useResizable/index.js +0 -1
- package/dist/hooks/useResizable/useResizable.js +0 -129
- package/dist/hooks/useScriptEditor/index.js +0 -1
- package/dist/hooks/useScriptEditor/useScriptEditor.js +0 -247
- package/dist/hooks/useStatCard/index.js +0 -1
- package/dist/hooks/useStatCard/useStatCard.js +0 -187
- package/dist/hooks/useThemeTransition/index.js +0 -1
- package/dist/hooks/useThemeTransition/useThemeTransition.js +0 -35
- package/dist/hooks/useTimeTracker/index.js +0 -1
- package/dist/hooks/useTimeTracker/useTimeTracker.js +0 -143
|
@@ -1,247 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useStatCard } from "./useStatCard";
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useStatCard - 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
|
-
* // Cálculo automático do delta a partir de lastMonth
|
|
12
|
-
* function MyCustomStatCard() {
|
|
13
|
-
* const { formattedValue, trend, getCardProps } = useStatCard({
|
|
14
|
-
* value: 122380,
|
|
15
|
-
* lastMonth: 105922, // delta será calculado automaticamente: +15.5%
|
|
16
|
-
* })
|
|
17
|
-
*
|
|
18
|
-
* return (
|
|
19
|
-
* <div {...getCardProps()}>
|
|
20
|
-
* <span>{formattedValue}</span>
|
|
21
|
-
* <Badge>{trend.percentage}</Badge>
|
|
22
|
-
* </div>
|
|
23
|
-
* )
|
|
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
|
-
* }
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
import { useCallback, useMemo } from "react";
|
|
37
|
-
/**
|
|
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
|
|
42
|
-
*/
|
|
43
|
-
function formatNumberDefault(n, locale = "pt-BR") {
|
|
44
|
-
if (n >= 1_000_000) {
|
|
45
|
-
const formatter = new Intl.NumberFormat(locale, {
|
|
46
|
-
minimumFractionDigits: 1,
|
|
47
|
-
maximumFractionDigits: 1,
|
|
48
|
-
});
|
|
49
|
-
return `${formatter.format(n / 1_000_000)}M`;
|
|
50
|
-
}
|
|
51
|
-
if (n >= 1_000) {
|
|
52
|
-
return new Intl.NumberFormat(locale).format(n);
|
|
53
|
-
}
|
|
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);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Hook Headless UI para StatCard
|
|
70
|
-
*
|
|
71
|
-
* Fornece apenas a lógica e API, sem markup ou styles.
|
|
72
|
-
* Você é responsável por criar o visual.
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* ```tsx
|
|
76
|
-
* // Cálculo automático do delta a partir de lastMonth
|
|
77
|
-
* function MyCustomStatCard() {
|
|
78
|
-
* const { formattedValue, trend, getCardProps } = useStatCard({
|
|
79
|
-
* value: 122380,
|
|
80
|
-
* lastMonth: 105922, // delta será calculado automaticamente: +15.5%
|
|
81
|
-
* })
|
|
82
|
-
*
|
|
83
|
-
* return (
|
|
84
|
-
* <div {...getCardProps()}>
|
|
85
|
-
* <span>{formattedValue}</span>
|
|
86
|
-
* <Badge variant={trend.variant}>{trend.percentage}</Badge>
|
|
87
|
-
* </div>
|
|
88
|
-
* )
|
|
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
|
-
* }
|
|
99
|
-
* ```
|
|
100
|
-
*/
|
|
101
|
-
export function useStatCard(data) {
|
|
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]);
|
|
116
|
-
// Calcula se a tendência é positiva
|
|
117
|
-
const isPositive = useMemo(() => {
|
|
118
|
-
if (explicitPositive !== undefined) {
|
|
119
|
-
return explicitPositive;
|
|
120
|
-
}
|
|
121
|
-
return delta >= 0;
|
|
122
|
-
}, [delta, explicitPositive]);
|
|
123
|
-
// Calcula informações da tendência
|
|
124
|
-
const trend = useMemo(() => {
|
|
125
|
-
const direction = delta > 0 ? "up" : delta < 0 ? "down" : "neutral";
|
|
126
|
-
const variant = direction === "up" ? "success" : direction === "down" ? "destructive" : "secondary";
|
|
127
|
-
const percentage = delta > 0 ? `+${delta.toFixed(1)}%` : delta < 0 ? `${delta.toFixed(1)}%` : "0%";
|
|
128
|
-
return {
|
|
129
|
-
isPositive,
|
|
130
|
-
percentage,
|
|
131
|
-
delta: Math.abs(delta),
|
|
132
|
-
variant,
|
|
133
|
-
direction,
|
|
134
|
-
};
|
|
135
|
-
}, [delta, isPositive]);
|
|
136
|
-
// Formata o valor principal
|
|
137
|
-
const formattedValue = useMemo(() => {
|
|
138
|
-
if (format) {
|
|
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}` : ""));
|
|
145
|
-
}
|
|
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]);
|
|
149
|
-
// Formata o valor do mês anterior
|
|
150
|
-
const formattedLastMonth = useMemo(() => {
|
|
151
|
-
if (!lastMonth)
|
|
152
|
-
return null;
|
|
153
|
-
if (lastFormat) {
|
|
154
|
-
return lastFormat(lastMonth);
|
|
155
|
-
}
|
|
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]);
|
|
163
|
-
// Props helper para o card
|
|
164
|
-
const getCardProps = useCallback((props = {}) => {
|
|
165
|
-
return {
|
|
166
|
-
onClick: props.onClick,
|
|
167
|
-
className: props.className,
|
|
168
|
-
"aria-label": props["aria-label"] || `Estatística: ${formattedValue}`,
|
|
169
|
-
role: props.onClick ? "button" : undefined,
|
|
170
|
-
};
|
|
171
|
-
}, [formattedValue]);
|
|
172
|
-
// Função utilitária de formatação
|
|
173
|
-
const formatNumber = useCallback((n) => {
|
|
174
|
-
if (currency) {
|
|
175
|
-
return formatCurrency(n, locale, currency, currencyDisplay);
|
|
176
|
-
}
|
|
177
|
-
return formatNumberDefault(n, locale);
|
|
178
|
-
}, [locale, currency, currencyDisplay]);
|
|
179
|
-
return {
|
|
180
|
-
formattedValue,
|
|
181
|
-
formattedLastMonth,
|
|
182
|
-
trend,
|
|
183
|
-
isPositive,
|
|
184
|
-
getCardProps,
|
|
185
|
-
formatNumber,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useThemeTransition } from "./useThemeTransition";
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useThemeTransition - Hook para View Transitions API
|
|
3
|
-
*
|
|
4
|
-
* Gerencia transições suaves de tema usando a View Transitions API
|
|
5
|
-
* com fallback automático para navegadores que não suportam
|
|
6
|
-
*/
|
|
7
|
-
import { useCallback } from "react";
|
|
8
|
-
/**
|
|
9
|
-
* Hook para gerenciar transições de tema com View Transitions API
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```tsx
|
|
13
|
-
* const { startTransition } = useThemeTransition()
|
|
14
|
-
*
|
|
15
|
-
* const handleToggle = () => {
|
|
16
|
-
* startTransition(() => {
|
|
17
|
-
* toggleTheme()
|
|
18
|
-
* })
|
|
19
|
-
* }
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
export function useThemeTransition() {
|
|
23
|
-
const startTransition = useCallback((updateFn) => {
|
|
24
|
-
// Verifica se View Transitions API está disponível
|
|
25
|
-
if (typeof document !== "undefined" && "startViewTransition" in document) {
|
|
26
|
-
const doc = document;
|
|
27
|
-
doc.startViewTransition?.(updateFn);
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
// Fallback: executa diretamente sem animação
|
|
31
|
-
updateFn();
|
|
32
|
-
}
|
|
33
|
-
}, []);
|
|
34
|
-
return { startTransition };
|
|
35
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useTimeTracker } from "./useTimeTracker";
|
|
@@ -1,143 +0,0 @@
|
|
|
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
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
29
|
-
/**
|
|
30
|
-
* Hook headless para gerenciar timer
|
|
31
|
-
*/
|
|
32
|
-
export function useTimeTracker(options = {}) {
|
|
33
|
-
const { initialTime = 0, onPause, onStop, onStart, onResume, onReset, format = "HH:mm:ss", } = options;
|
|
34
|
-
const [time, setTime] = useState(initialTime);
|
|
35
|
-
const [isRunning, setIsRunning] = useState(false);
|
|
36
|
-
const [isPaused, setIsPaused] = useState(false);
|
|
37
|
-
const intervalRef = useRef(null);
|
|
38
|
-
const startTimeRef = useRef(null);
|
|
39
|
-
const pausedTimeRef = useRef(0);
|
|
40
|
-
// Limpar intervalo ao desmontar
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
return () => {
|
|
43
|
-
if (intervalRef.current) {
|
|
44
|
-
clearInterval(intervalRef.current);
|
|
45
|
-
intervalRef.current = null;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
}, []);
|
|
49
|
-
// Formatar tempo
|
|
50
|
-
const formatTime = useCallback((seconds) => {
|
|
51
|
-
if (typeof format === "function") {
|
|
52
|
-
return format(seconds);
|
|
53
|
-
}
|
|
54
|
-
const hours = Math.floor(seconds / 3600);
|
|
55
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
56
|
-
const secs = seconds % 60;
|
|
57
|
-
switch (format) {
|
|
58
|
-
case "HH:mm:ss":
|
|
59
|
-
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
60
|
-
case "mm:ss":
|
|
61
|
-
return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
62
|
-
case "ss":
|
|
63
|
-
return String(seconds);
|
|
64
|
-
default:
|
|
65
|
-
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
66
|
-
}
|
|
67
|
-
}, [format]);
|
|
68
|
-
const formattedTime = formatTime(time);
|
|
69
|
-
// Limpar intervalo
|
|
70
|
-
const clearTimerInterval = useCallback(() => {
|
|
71
|
-
if (intervalRef.current) {
|
|
72
|
-
clearInterval(intervalRef.current);
|
|
73
|
-
intervalRef.current = null;
|
|
74
|
-
}
|
|
75
|
-
}, []);
|
|
76
|
-
// Iniciar timer
|
|
77
|
-
const start = useCallback(() => {
|
|
78
|
-
if (isRunning)
|
|
79
|
-
return;
|
|
80
|
-
clearTimerInterval();
|
|
81
|
-
// Se estava pausado, continuar de onde parou
|
|
82
|
-
const startFrom = isPaused ? time : initialTime;
|
|
83
|
-
setTime(startFrom);
|
|
84
|
-
setIsRunning(true);
|
|
85
|
-
setIsPaused(false);
|
|
86
|
-
pausedTimeRef.current = startFrom;
|
|
87
|
-
startTimeRef.current = Date.now() - startFrom * 1000;
|
|
88
|
-
intervalRef.current = setInterval(() => {
|
|
89
|
-
const elapsed = Math.floor((Date.now() - (startTimeRef.current || 0)) / 1000);
|
|
90
|
-
setTime(elapsed);
|
|
91
|
-
}, 100);
|
|
92
|
-
if (!isPaused) {
|
|
93
|
-
onStart?.(startFrom);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
onResume?.(startFrom);
|
|
97
|
-
}
|
|
98
|
-
}, [isRunning, isPaused, time, initialTime, onStart, onResume, clearTimerInterval]);
|
|
99
|
-
// Pausar timer
|
|
100
|
-
const pause = useCallback(() => {
|
|
101
|
-
if (!isRunning)
|
|
102
|
-
return;
|
|
103
|
-
clearTimerInterval();
|
|
104
|
-
setIsRunning(false);
|
|
105
|
-
setIsPaused(true);
|
|
106
|
-
onPause?.(time);
|
|
107
|
-
}, [isRunning, time, onPause, clearTimerInterval]);
|
|
108
|
-
// Parar timer
|
|
109
|
-
const stop = useCallback(() => {
|
|
110
|
-
clearTimerInterval();
|
|
111
|
-
setIsRunning(false);
|
|
112
|
-
setIsPaused(false);
|
|
113
|
-
onStop?.(time);
|
|
114
|
-
}, [time, onStop, clearTimerInterval]);
|
|
115
|
-
// Retomar timer
|
|
116
|
-
const resume = useCallback(() => {
|
|
117
|
-
if (!isPaused)
|
|
118
|
-
return;
|
|
119
|
-
start();
|
|
120
|
-
}, [isPaused, start]);
|
|
121
|
-
// Resetar timer
|
|
122
|
-
const reset = useCallback(() => {
|
|
123
|
-
clearTimerInterval();
|
|
124
|
-
setTime(initialTime);
|
|
125
|
-
setIsRunning(false);
|
|
126
|
-
setIsPaused(false);
|
|
127
|
-
pausedTimeRef.current = 0;
|
|
128
|
-
startTimeRef.current = null;
|
|
129
|
-
onReset?.();
|
|
130
|
-
}, [initialTime, onReset, clearTimerInterval]);
|
|
131
|
-
return {
|
|
132
|
-
time,
|
|
133
|
-
formattedTime,
|
|
134
|
-
isRunning,
|
|
135
|
-
isPaused,
|
|
136
|
-
isStopped: !isRunning && !isPaused,
|
|
137
|
-
start,
|
|
138
|
-
pause,
|
|
139
|
-
stop,
|
|
140
|
-
resume,
|
|
141
|
-
reset,
|
|
142
|
-
};
|
|
143
|
-
}
|