@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowtomic/logic",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Hooks headless e lógica reutilizável para React",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"scripts": {
|
|
29
29
|
"dev": "bun run --watch src/index.ts",
|
|
30
|
-
"build": "biome check . && tsc --emitDeclarationOnly && bun build ./src/index.ts --outdir ./dist --target browser --format esm --minify --external react --external react/jsx-runtime",
|
|
30
|
+
"build": "biome check . && tsc --emitDeclarationOnly && bun build --production ./src/index.ts --outdir ./dist --target browser --format esm --minify --external react --external react/jsx-runtime",
|
|
31
31
|
"type-check": "tsc --noEmit",
|
|
32
|
-
"clean": "rm -rf dist",
|
|
32
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
33
33
|
"lint": "biome check .",
|
|
34
34
|
"lint:fix": "biome check --write .",
|
|
35
35
|
"format": "biome format --write .",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useAnimatedIndicator } from "./useAnimatedIndicator";
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useAnimatedIndicator - Headless UI Hook
|
|
3
|
-
*
|
|
4
|
-
* Hook reutilizável para animação de indicador que se move entre elementos
|
|
5
|
-
* Fornece apenas: lógica, estado e cálculos de posição
|
|
6
|
-
* NÃO fornece: markup, styles ou implementações pré-construídas
|
|
7
|
-
*
|
|
8
|
-
* Padrão Headless UI: você controla o markup e styles
|
|
9
|
-
*/
|
|
10
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
11
|
-
/**
|
|
12
|
-
* Hook headless para animação de indicador
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```tsx
|
|
16
|
-
* const containerRef = useRef<HTMLDivElement>(null);
|
|
17
|
-
* const { indicatorStyle, registerElement, unregisterElement } = useAnimatedIndicator({
|
|
18
|
-
* containerRef,
|
|
19
|
-
* activeSelector: '[data-state="active"]',
|
|
20
|
-
* });
|
|
21
|
-
*
|
|
22
|
-
* return (
|
|
23
|
-
* <div ref={containerRef}>
|
|
24
|
-
* {items.map((item) => (
|
|
25
|
-
* <button
|
|
26
|
-
* ref={(el) => registerElement(el, item.id)}
|
|
27
|
-
* data-state={isActive ? "active" : "inactive"}
|
|
28
|
-
* >
|
|
29
|
-
* {item.label}
|
|
30
|
-
* </button>
|
|
31
|
-
* ))}
|
|
32
|
-
* <motion.div
|
|
33
|
-
* style={{
|
|
34
|
-
* left: `${indicatorStyle.left}px`,
|
|
35
|
-
* top: `${indicatorStyle.top}px`,
|
|
36
|
-
* width: indicatorStyle.width,
|
|
37
|
-
* height: indicatorStyle.height,
|
|
38
|
-
* }}
|
|
39
|
-
* />
|
|
40
|
-
* </div>
|
|
41
|
-
* );
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
export function useAnimatedIndicator(options) {
|
|
45
|
-
const { containerRef, activeSelector = '[data-state="active"]', getElementValue, updateOnResize = true, value: controlledValue, } = options;
|
|
46
|
-
const elementsRef = useRef(new Map());
|
|
47
|
-
const updateTimeoutRef = useRef(null);
|
|
48
|
-
const activeValueRef = useRef(undefined);
|
|
49
|
-
const [indicatorStyle, setIndicatorStyle] = useState({
|
|
50
|
-
left: 0,
|
|
51
|
-
width: 0,
|
|
52
|
-
height: 0,
|
|
53
|
-
top: 0,
|
|
54
|
-
opacity: 0,
|
|
55
|
-
});
|
|
56
|
-
const [activeValue, setActiveValue] = useState();
|
|
57
|
-
// Sincronizar ref com state
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
activeValueRef.current = activeValue;
|
|
60
|
-
}, [activeValue]);
|
|
61
|
-
// Sincronizar valor controlado
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (controlledValue !== undefined) {
|
|
64
|
-
setActiveValue(controlledValue);
|
|
65
|
-
}
|
|
66
|
-
}, [controlledValue]);
|
|
67
|
-
const updateIndicator = useCallback(() => {
|
|
68
|
-
const currentActiveValue = activeValueRef.current;
|
|
69
|
-
if (!currentActiveValue) {
|
|
70
|
-
setIndicatorStyle((prev) => {
|
|
71
|
-
if (prev.opacity === 0)
|
|
72
|
-
return prev;
|
|
73
|
-
return { left: 0, width: 0, height: 0, top: 0, opacity: 0 };
|
|
74
|
-
});
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const activeElement = elementsRef.current.get(currentActiveValue);
|
|
78
|
-
const container = containerRef.current;
|
|
79
|
-
if (activeElement && container) {
|
|
80
|
-
const elementRect = activeElement.getBoundingClientRect();
|
|
81
|
-
const containerRect = container.getBoundingClientRect();
|
|
82
|
-
// Calcular posição relativa ao container
|
|
83
|
-
const left = elementRect.left - containerRect.left;
|
|
84
|
-
const width = elementRect.width;
|
|
85
|
-
const height = elementRect.height;
|
|
86
|
-
const top = elementRect.top - containerRect.top;
|
|
87
|
-
setIndicatorStyle((prev) => {
|
|
88
|
-
// Evitar atualização se os valores não mudaram
|
|
89
|
-
if (prev.left === left &&
|
|
90
|
-
prev.width === width &&
|
|
91
|
-
prev.height === height &&
|
|
92
|
-
prev.top === top &&
|
|
93
|
-
prev.opacity === 1) {
|
|
94
|
-
return prev;
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
left,
|
|
98
|
-
width,
|
|
99
|
-
height,
|
|
100
|
-
top,
|
|
101
|
-
opacity: 1,
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}, [containerRef]);
|
|
106
|
-
// Função para agendar atualização (debounce) - estável
|
|
107
|
-
const scheduleUpdateRef = useRef(undefined);
|
|
108
|
-
// Atualizar a função no ref sempre que updateIndicator mudar
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
scheduleUpdateRef.current = () => {
|
|
111
|
-
if (updateTimeoutRef.current !== null) {
|
|
112
|
-
cancelAnimationFrame(updateTimeoutRef.current);
|
|
113
|
-
}
|
|
114
|
-
updateTimeoutRef.current = requestAnimationFrame(() => {
|
|
115
|
-
updateIndicator();
|
|
116
|
-
updateTimeoutRef.current = null;
|
|
117
|
-
});
|
|
118
|
-
};
|
|
119
|
-
}, [updateIndicator]);
|
|
120
|
-
const scheduleUpdate = useCallback(() => {
|
|
121
|
-
scheduleUpdateRef.current?.();
|
|
122
|
-
}, []);
|
|
123
|
-
// Atualizar quando activeValue mudar
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
scheduleUpdate();
|
|
126
|
-
}, [scheduleUpdate]);
|
|
127
|
-
// Listener de resize
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
if (!updateOnResize)
|
|
130
|
-
return;
|
|
131
|
-
const handleResize = () => scheduleUpdate();
|
|
132
|
-
window.addEventListener("resize", handleResize);
|
|
133
|
-
return () => {
|
|
134
|
-
window.removeEventListener("resize", handleResize);
|
|
135
|
-
if (updateTimeoutRef.current !== null) {
|
|
136
|
-
cancelAnimationFrame(updateTimeoutRef.current);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
}, [scheduleUpdate, updateOnResize]);
|
|
140
|
-
const registerElement = useCallback((element, value) => {
|
|
141
|
-
const previousElement = elementsRef.current.get(value);
|
|
142
|
-
// Só atualizar se o elemento realmente mudou
|
|
143
|
-
if (element === previousElement) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (element) {
|
|
147
|
-
elementsRef.current.set(value, element);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
// Só remover se o elemento existia antes
|
|
151
|
-
if (previousElement) {
|
|
152
|
-
elementsRef.current.delete(value);
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
return; // Elemento já não existe, não precisa atualizar
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// Agendar atualização de forma assíncrona para evitar loops
|
|
159
|
-
// Só atualizar se temos um elemento válido
|
|
160
|
-
if (element) {
|
|
161
|
-
scheduleUpdate();
|
|
162
|
-
}
|
|
163
|
-
}, [scheduleUpdate]);
|
|
164
|
-
const unregisterElement = useCallback((value) => {
|
|
165
|
-
elementsRef.current.delete(value);
|
|
166
|
-
scheduleUpdate();
|
|
167
|
-
}, [scheduleUpdate]);
|
|
168
|
-
// Refs para opções estáveis
|
|
169
|
-
const activeSelectorRef = useRef(activeSelector);
|
|
170
|
-
const getElementValueRef = useRef(getElementValue);
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
activeSelectorRef.current = activeSelector;
|
|
173
|
-
getElementValueRef.current = getElementValue;
|
|
174
|
-
}, [activeSelector, getElementValue]);
|
|
175
|
-
// Ref para valor controlado
|
|
176
|
-
const controlledValueRef = useRef(controlledValue);
|
|
177
|
-
useEffect(() => {
|
|
178
|
-
controlledValueRef.current = controlledValue;
|
|
179
|
-
}, [controlledValue]);
|
|
180
|
-
// Observar mudanças no elemento ativo
|
|
181
|
-
useEffect(() => {
|
|
182
|
-
const container = containerRef.current;
|
|
183
|
-
if (!container)
|
|
184
|
-
return;
|
|
185
|
-
const findActiveValue = () => {
|
|
186
|
-
// Se estiver em modo controlado, ignorar detecção DOM
|
|
187
|
-
if (controlledValueRef.current !== undefined)
|
|
188
|
-
return;
|
|
189
|
-
const activeElement = container.querySelector(activeSelectorRef.current);
|
|
190
|
-
if (activeElement) {
|
|
191
|
-
let value;
|
|
192
|
-
if (getElementValueRef.current) {
|
|
193
|
-
value = getElementValueRef.current(activeElement);
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
// Tentar obter do atributo data-value
|
|
197
|
-
value = activeElement.getAttribute("data-value") || undefined;
|
|
198
|
-
// Se não tiver data-value, buscar no Map pelo elemento
|
|
199
|
-
if (!value) {
|
|
200
|
-
for (const [mapValue, mapElement] of elementsRef.current.entries()) {
|
|
201
|
-
if (mapElement === activeElement) {
|
|
202
|
-
value = mapValue;
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if (value) {
|
|
209
|
-
setActiveValue((prev) => {
|
|
210
|
-
// Só atualizar se o valor realmente mudou
|
|
211
|
-
if (prev !== value) {
|
|
212
|
-
return value;
|
|
213
|
-
}
|
|
214
|
-
return prev;
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
const observer = new MutationObserver(() => {
|
|
220
|
-
// Usar requestAnimationFrame para evitar chamadas síncronas
|
|
221
|
-
requestAnimationFrame(findActiveValue);
|
|
222
|
-
});
|
|
223
|
-
observer.observe(container, {
|
|
224
|
-
attributes: true,
|
|
225
|
-
attributeFilter: ["data-state", "data-active", "aria-selected"],
|
|
226
|
-
subtree: true,
|
|
227
|
-
});
|
|
228
|
-
// Verificar estado inicial com delay para evitar loop
|
|
229
|
-
const timeoutId = setTimeout(findActiveValue, 0);
|
|
230
|
-
return () => {
|
|
231
|
-
observer.disconnect();
|
|
232
|
-
clearTimeout(timeoutId);
|
|
233
|
-
};
|
|
234
|
-
}, [containerRef]);
|
|
235
|
-
return {
|
|
236
|
-
indicatorStyle,
|
|
237
|
-
registerElement,
|
|
238
|
-
unregisterElement,
|
|
239
|
-
updateIndicator,
|
|
240
|
-
activeValue,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useAutocomplete } from "./useAutocomplete";
|
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useAutocomplete - 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 MyCustomAutocomplete() {
|
|
12
|
-
* const {
|
|
13
|
-
* inputValue,
|
|
14
|
-
* selectedValue,
|
|
15
|
-
* isOpen,
|
|
16
|
-
* filteredItems,
|
|
17
|
-
* getInputProps,
|
|
18
|
-
* getPopoverProps,
|
|
19
|
-
* getListProps,
|
|
20
|
-
* getItemProps,
|
|
21
|
-
* open,
|
|
22
|
-
* close,
|
|
23
|
-
* select,
|
|
24
|
-
* clear,
|
|
25
|
-
* } = useAutocomplete({
|
|
26
|
-
* options: [
|
|
27
|
-
* { value: "1", label: "Option 1" },
|
|
28
|
-
* { value: "2", label: "Option 2" },
|
|
29
|
-
* ],
|
|
30
|
-
* });
|
|
31
|
-
*
|
|
32
|
-
* return (
|
|
33
|
-
* <div>
|
|
34
|
-
* <input {...getInputProps()} />
|
|
35
|
-
* {isOpen && (
|
|
36
|
-
* <div {...getPopoverProps()}>
|
|
37
|
-
* <ul {...getListProps()}>
|
|
38
|
-
* {filteredItems.map((item) => (
|
|
39
|
-
* <li key={item.value} {...getItemProps(item)}>
|
|
40
|
-
* {item.label}
|
|
41
|
-
* </li>
|
|
42
|
-
* ))}
|
|
43
|
-
* </ul>
|
|
44
|
-
* </div>
|
|
45
|
-
* )}
|
|
46
|
-
* </div>
|
|
47
|
-
* );
|
|
48
|
-
* }
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
51
|
-
import * as React from "react";
|
|
52
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
53
|
-
/**
|
|
54
|
-
* Função de filtro padrão (case-insensitive)
|
|
55
|
-
*/
|
|
56
|
-
function defaultFilter(option, searchTerm) {
|
|
57
|
-
return option.label.toLowerCase().includes(searchTerm.toLowerCase());
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Extrai items dos children em modo composição
|
|
61
|
-
*/
|
|
62
|
-
function extractItemsFromChildren(children) {
|
|
63
|
-
const items = [];
|
|
64
|
-
const processChild = (child) => {
|
|
65
|
-
if (!React.isValidElement(child)) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const props = child.props;
|
|
69
|
-
// Se tiver prop 'value', é um Autocomplete.Item
|
|
70
|
-
if (props.value !== undefined) {
|
|
71
|
-
const value = String(props.value);
|
|
72
|
-
let label = value;
|
|
73
|
-
// Extrair o texto dos children
|
|
74
|
-
if (props.children !== undefined && props.children !== null) {
|
|
75
|
-
if (typeof props.children === "string") {
|
|
76
|
-
label = props.children;
|
|
77
|
-
}
|
|
78
|
-
else if (typeof props.children === "number") {
|
|
79
|
-
label = String(props.children);
|
|
80
|
-
}
|
|
81
|
-
else if (React.isValidElement(props.children)) {
|
|
82
|
-
const childProps = props.children.props;
|
|
83
|
-
if (typeof childProps?.children === "string") {
|
|
84
|
-
label = childProps.children;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
label = String(props.children);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
else if (Array.isArray(props.children)) {
|
|
91
|
-
React.Children.forEach(props.children, (c) => {
|
|
92
|
-
if (typeof c === "string" && label === value) {
|
|
93
|
-
label = c;
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
label = String(props.children);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
items.push({
|
|
102
|
-
value,
|
|
103
|
-
label,
|
|
104
|
-
disabled: props.disabled,
|
|
105
|
-
});
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
// Se tiver prop 'title', é um AutocompleteSection - processar seus children
|
|
109
|
-
if (props.title !== undefined) {
|
|
110
|
-
if (props.children) {
|
|
111
|
-
React.Children.forEach(props.children, processChild);
|
|
112
|
-
}
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
// Se tiver children mas não é Item nem Section, processar recursivamente
|
|
116
|
-
if (props.children !== undefined && props.children !== null) {
|
|
117
|
-
if (typeof props.children === "string" ||
|
|
118
|
-
typeof props.children === "number" ||
|
|
119
|
-
typeof props.children === "boolean") {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (Array.isArray(props.children) || React.isValidElement(props.children)) {
|
|
123
|
-
React.Children.forEach(props.children, processChild);
|
|
124
|
-
}
|
|
125
|
-
else if (typeof props.children === "object") {
|
|
126
|
-
React.Children.forEach(props.children, processChild);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
React.Children.forEach(children, processChild);
|
|
131
|
-
return items;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Hook Headless UI para Autocomplete
|
|
135
|
-
*
|
|
136
|
-
* Fornece apenas a lógica e API, sem markup ou styles.
|
|
137
|
-
* Você é responsável por criar o visual.
|
|
138
|
-
*/
|
|
139
|
-
export function useAutocomplete(options = {}) {
|
|
140
|
-
const { options: providedOptions = [], value: controlledValue, defaultValue, onValueChange, filterFunction = defaultFilter, allowCustomValue = false, isLoading = false, emptyMessage: _emptyMessage = "Nenhum resultado encontrado.", children, } = options;
|
|
141
|
-
// Determinar se está usando composição ou API antiga
|
|
142
|
-
const useComposition = children !== undefined;
|
|
143
|
-
// Estado interno
|
|
144
|
-
const [inputValue, setInputValueState] = useState("");
|
|
145
|
-
const [selectedValue, setSelectedValue] = useState(controlledValue ?? defaultValue);
|
|
146
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
147
|
-
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
148
|
-
// Determinar se é controlado
|
|
149
|
-
const isControlled = controlledValue !== undefined;
|
|
150
|
-
// Extrair items dos children se em modo composição
|
|
151
|
-
const itemsFromChildren = useMemo(() => {
|
|
152
|
-
if (!useComposition)
|
|
153
|
-
return [];
|
|
154
|
-
return extractItemsFromChildren(children);
|
|
155
|
-
}, [useComposition, children]);
|
|
156
|
-
// Items disponíveis (API antiga ou composição)
|
|
157
|
-
const allItems = useMemo(() => {
|
|
158
|
-
if (useComposition) {
|
|
159
|
-
return itemsFromChildren;
|
|
160
|
-
}
|
|
161
|
-
return providedOptions;
|
|
162
|
-
}, [useComposition, itemsFromChildren, providedOptions]);
|
|
163
|
-
// Filtrar items baseado no inputValue
|
|
164
|
-
const filteredItems = useMemo(() => {
|
|
165
|
-
if (!inputValue.trim()) {
|
|
166
|
-
return allItems;
|
|
167
|
-
}
|
|
168
|
-
return allItems.filter((item) => filterFunction(item, inputValue));
|
|
169
|
-
}, [allItems, inputValue, filterFunction]);
|
|
170
|
-
// Estado derivado
|
|
171
|
-
const isEmpty = filteredItems.length === 0 && !isLoading;
|
|
172
|
-
// IDs para acessibilidade
|
|
173
|
-
const listIdRef = useRef(`autocomplete-list-${React.useId()}`);
|
|
174
|
-
const listId = listIdRef.current;
|
|
175
|
-
// Sincronizar com valor controlado
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
if (isControlled && controlledValue !== undefined) {
|
|
178
|
-
const option = allItems.find((opt) => opt.value === controlledValue);
|
|
179
|
-
if (option) {
|
|
180
|
-
setSelectedValue(controlledValue);
|
|
181
|
-
setInputValueState(option.label);
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
setSelectedValue(undefined);
|
|
185
|
-
setInputValueState(controlledValue);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}, [isControlled, controlledValue, allItems]);
|
|
189
|
-
// Sincronizar inputValue quando seleção muda (apenas para API antiga)
|
|
190
|
-
useEffect(() => {
|
|
191
|
-
if (!useComposition && selectedValue) {
|
|
192
|
-
const option = allItems.find((opt) => opt.value === selectedValue);
|
|
193
|
-
if (option && inputValue !== option.label) {
|
|
194
|
-
setInputValueState(option.label);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}, [selectedValue, allItems, useComposition, inputValue]);
|
|
198
|
-
// Controles
|
|
199
|
-
const open = useCallback(() => {
|
|
200
|
-
if (filteredItems.length > 0 || isLoading) {
|
|
201
|
-
setIsOpen(true);
|
|
202
|
-
}
|
|
203
|
-
}, [filteredItems.length, isLoading]);
|
|
204
|
-
const close = useCallback(() => {
|
|
205
|
-
setIsOpen(false);
|
|
206
|
-
setHighlightedIndex(-1);
|
|
207
|
-
}, []);
|
|
208
|
-
const select = useCallback((value) => {
|
|
209
|
-
const option = allItems.find((opt) => opt.value === value);
|
|
210
|
-
if (option) {
|
|
211
|
-
setSelectedValue(value);
|
|
212
|
-
setInputValueState(option.label);
|
|
213
|
-
onValueChange?.(value);
|
|
214
|
-
close();
|
|
215
|
-
}
|
|
216
|
-
else if (allowCustomValue) {
|
|
217
|
-
setSelectedValue(value);
|
|
218
|
-
setInputValueState(value);
|
|
219
|
-
onValueChange?.(value);
|
|
220
|
-
close();
|
|
221
|
-
}
|
|
222
|
-
}, [allItems, allowCustomValue, onValueChange, close]);
|
|
223
|
-
const clear = useCallback(() => {
|
|
224
|
-
setSelectedValue(undefined);
|
|
225
|
-
setInputValueState("");
|
|
226
|
-
onValueChange?.(undefined);
|
|
227
|
-
close();
|
|
228
|
-
}, [onValueChange, close]);
|
|
229
|
-
const setInputValue = useCallback((value) => {
|
|
230
|
-
setInputValueState(value);
|
|
231
|
-
if (value.trim() && (filteredItems.length > 0 || isLoading)) {
|
|
232
|
-
setIsOpen(true);
|
|
233
|
-
}
|
|
234
|
-
else if (!value.trim()) {
|
|
235
|
-
setIsOpen(false);
|
|
236
|
-
}
|
|
237
|
-
}, [filteredItems.length, isLoading]);
|
|
238
|
-
// Helpers de props
|
|
239
|
-
const getInputProps = useCallback((props = {}) => {
|
|
240
|
-
const highlightedItem = filteredItems[highlightedIndex];
|
|
241
|
-
const activeDescendant = highlightedItem
|
|
242
|
-
? `${listId}-item-${highlightedItem.value}`
|
|
243
|
-
: undefined;
|
|
244
|
-
const handleFocus = (e) => {
|
|
245
|
-
open();
|
|
246
|
-
props.onFocus?.(e);
|
|
247
|
-
};
|
|
248
|
-
const handleBlur = (e) => {
|
|
249
|
-
// Delay para permitir cliques nos items
|
|
250
|
-
setTimeout(() => {
|
|
251
|
-
// Não fechar o popover no onBlur - deixar o Popover gerenciar o fechamento
|
|
252
|
-
// O Popover do Radix UI já gerencia o fechamento quando você clica fora
|
|
253
|
-
// via onOpenChange. Fechar aqui causa conflito e comportamento inconsistente.
|
|
254
|
-
props.onBlur?.(e);
|
|
255
|
-
}, 200);
|
|
256
|
-
};
|
|
257
|
-
return {
|
|
258
|
-
...props,
|
|
259
|
-
value: inputValue,
|
|
260
|
-
onChange: (e) => {
|
|
261
|
-
setInputValue(e.target.value);
|
|
262
|
-
props.onChange?.(e);
|
|
263
|
-
},
|
|
264
|
-
onFocus: handleFocus,
|
|
265
|
-
onBlur: handleBlur,
|
|
266
|
-
onKeyDown: (e) => {
|
|
267
|
-
if (e.key === "ArrowDown") {
|
|
268
|
-
e.preventDefault();
|
|
269
|
-
if (isOpen) {
|
|
270
|
-
setHighlightedIndex((prev) => (prev < filteredItems.length - 1 ? prev + 1 : prev));
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
open();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
else if (e.key === "ArrowUp") {
|
|
277
|
-
e.preventDefault();
|
|
278
|
-
if (isOpen) {
|
|
279
|
-
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1));
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
else if (e.key === "Enter") {
|
|
283
|
-
e.preventDefault();
|
|
284
|
-
if (isOpen && highlightedItem) {
|
|
285
|
-
select(highlightedItem.value);
|
|
286
|
-
}
|
|
287
|
-
else if (isOpen && filteredItems.length === 1) {
|
|
288
|
-
select(filteredItems[0].value);
|
|
289
|
-
}
|
|
290
|
-
else if (allowCustomValue && inputValue.trim()) {
|
|
291
|
-
select(inputValue);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
else if (e.key === "Escape") {
|
|
295
|
-
e.preventDefault();
|
|
296
|
-
close();
|
|
297
|
-
}
|
|
298
|
-
props.onKeyDown?.(e);
|
|
299
|
-
},
|
|
300
|
-
"aria-expanded": isOpen,
|
|
301
|
-
"aria-autocomplete": "list",
|
|
302
|
-
"aria-controls": listId,
|
|
303
|
-
"aria-activedescendant": activeDescendant,
|
|
304
|
-
role: "combobox",
|
|
305
|
-
};
|
|
306
|
-
}, [
|
|
307
|
-
inputValue,
|
|
308
|
-
isOpen,
|
|
309
|
-
filteredItems,
|
|
310
|
-
highlightedIndex,
|
|
311
|
-
listId,
|
|
312
|
-
open,
|
|
313
|
-
close,
|
|
314
|
-
select,
|
|
315
|
-
allowCustomValue,
|
|
316
|
-
setInputValue,
|
|
317
|
-
]);
|
|
318
|
-
const getPopoverProps = useCallback((props = {}) => {
|
|
319
|
-
return {
|
|
320
|
-
...props,
|
|
321
|
-
"aria-label": "Lista de opções",
|
|
322
|
-
role: "listbox",
|
|
323
|
-
};
|
|
324
|
-
}, []);
|
|
325
|
-
const getListProps = useCallback((props = {}) => {
|
|
326
|
-
return {
|
|
327
|
-
...props,
|
|
328
|
-
id: listId,
|
|
329
|
-
role: "listbox",
|
|
330
|
-
"aria-label": "Opções disponíveis",
|
|
331
|
-
};
|
|
332
|
-
}, [listId]);
|
|
333
|
-
const getItemProps = useCallback((item, props = {}) => {
|
|
334
|
-
const itemId = `${listId}-item-${item.value}`;
|
|
335
|
-
const isSelected = selectedValue === item.value;
|
|
336
|
-
const isHighlighted = filteredItems[highlightedIndex]?.value === item.value;
|
|
337
|
-
const handleClick = (e) => {
|
|
338
|
-
if (!item.disabled) {
|
|
339
|
-
select(item.value);
|
|
340
|
-
}
|
|
341
|
-
props.onClick?.(e);
|
|
342
|
-
};
|
|
343
|
-
const handleMouseEnter = (e) => {
|
|
344
|
-
const index = filteredItems.findIndex((i) => i.value === item.value);
|
|
345
|
-
if (index >= 0) {
|
|
346
|
-
setHighlightedIndex(index);
|
|
347
|
-
}
|
|
348
|
-
props.onMouseEnter?.(e);
|
|
349
|
-
};
|
|
350
|
-
return {
|
|
351
|
-
...props,
|
|
352
|
-
role: "option",
|
|
353
|
-
"aria-selected": isSelected,
|
|
354
|
-
"aria-disabled": item.disabled ?? false,
|
|
355
|
-
id: itemId,
|
|
356
|
-
onClick: handleClick,
|
|
357
|
-
onMouseEnter: handleMouseEnter,
|
|
358
|
-
className: `${props.className || ""} ${isHighlighted ? "bg-accent" : ""} ${isSelected ? "font-semibold" : ""}`.trim(),
|
|
359
|
-
};
|
|
360
|
-
}, [listId, selectedValue, filteredItems, highlightedIndex, select]);
|
|
361
|
-
return {
|
|
362
|
-
inputValue,
|
|
363
|
-
selectedValue,
|
|
364
|
-
isOpen,
|
|
365
|
-
filteredItems,
|
|
366
|
-
highlightedIndex,
|
|
367
|
-
isLoading,
|
|
368
|
-
isEmpty,
|
|
369
|
-
open,
|
|
370
|
-
close,
|
|
371
|
-
select,
|
|
372
|
-
clear,
|
|
373
|
-
setInputValue,
|
|
374
|
-
getInputProps,
|
|
375
|
-
getPopoverProps,
|
|
376
|
-
getListProps,
|
|
377
|
-
getItemProps,
|
|
378
|
-
extractItemsFromChildren,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { useDashboardLayout } from "./useDashboardLayout";
|