@flowtomic/logic 0.1.2 → 0.1.4

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.
@@ -34,6 +34,11 @@ export interface UseAnimatedIndicatorOptions {
34
34
  * @default true
35
35
  */
36
36
  updateOnResize?: boolean;
37
+ /**
38
+ * Valor ativo controlado externamente.
39
+ * Se fornecido, o hook não tentará detectar o elemento ativo via DOM.
40
+ */
41
+ value?: string;
37
42
  }
38
43
  export interface UseAnimatedIndicatorReturn {
39
44
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"useAnimatedIndicator.d.ts","sourceRoot":"","sources":["../../../src/hooks/useAnimatedIndicator/useAnimatedIndicator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACnD;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B;;OAEG;IACH,eAAe,EAAE,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtE;;OAEG;IACH,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C;;OAEG;IACH,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B;;OAEG;IACH,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,2BAA2B,GACnC,0BAA0B,CA+N5B"}
1
+ {"version":3,"file":"useAnimatedIndicator.d.ts","sourceRoot":"","sources":["../../../src/hooks/useAnimatedIndicator/useAnimatedIndicator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACnD;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B;;OAEG;IACH,eAAe,EAAE,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtE;;OAEG;IACH,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C;;OAEG;IACH,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B;;OAEG;IACH,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,2BAA2B,GACnC,0BAA0B,CAgP5B"}
@@ -42,7 +42,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
42
42
  * ```
43
43
  */
44
44
  export function useAnimatedIndicator(options) {
45
- const { containerRef, activeSelector = '[data-state="active"]', getElementValue, updateOnResize = true, } = options;
45
+ const { containerRef, activeSelector = '[data-state="active"]', getElementValue, updateOnResize = true, value: controlledValue, } = options;
46
46
  const elementsRef = useRef(new Map());
47
47
  const updateTimeoutRef = useRef(null);
48
48
  const activeValueRef = useRef(undefined);
@@ -58,6 +58,12 @@ export function useAnimatedIndicator(options) {
58
58
  useEffect(() => {
59
59
  activeValueRef.current = activeValue;
60
60
  }, [activeValue]);
61
+ // Sincronizar valor controlado
62
+ useEffect(() => {
63
+ if (controlledValue !== undefined) {
64
+ setActiveValue(controlledValue);
65
+ }
66
+ }, [controlledValue]);
61
67
  const updateIndicator = useCallback(() => {
62
68
  const currentActiveValue = activeValueRef.current;
63
69
  if (!currentActiveValue) {
@@ -117,7 +123,7 @@ export function useAnimatedIndicator(options) {
117
123
  // Atualizar quando activeValue mudar
118
124
  useEffect(() => {
119
125
  scheduleUpdate();
120
- }, [activeValue, scheduleUpdate]);
126
+ }, [scheduleUpdate]);
121
127
  // Listener de resize
122
128
  useEffect(() => {
123
129
  if (!updateOnResize)
@@ -166,12 +172,20 @@ export function useAnimatedIndicator(options) {
166
172
  activeSelectorRef.current = activeSelector;
167
173
  getElementValueRef.current = getElementValue;
168
174
  }, [activeSelector, getElementValue]);
175
+ // Ref para valor controlado
176
+ const controlledValueRef = useRef(controlledValue);
177
+ useEffect(() => {
178
+ controlledValueRef.current = controlledValue;
179
+ }, [controlledValue]);
169
180
  // Observar mudanças no elemento ativo
170
181
  useEffect(() => {
171
182
  const container = containerRef.current;
172
183
  if (!container)
173
184
  return;
174
185
  const findActiveValue = () => {
186
+ // Se estiver em modo controlado, ignorar detecção DOM
187
+ if (controlledValueRef.current !== undefined)
188
+ return;
175
189
  const activeElement = container.querySelector(activeSelectorRef.current);
176
190
  if (activeElement) {
177
191
  let value;
@@ -0,0 +1,3 @@
1
+ export type { AutocompleteOption, UseAutocompleteOptions, UseAutocompleteReturn, } from "./useAutocomplete";
2
+ export { useAutocomplete } from "./useAutocomplete";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/useAutocomplete/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1 @@
1
+ export { useAutocomplete } from "./useAutocomplete";
@@ -0,0 +1,197 @@
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
+ export interface AutocompleteOption {
53
+ value: string;
54
+ label: string;
55
+ disabled?: boolean;
56
+ }
57
+ export interface UseAutocompleteOptions {
58
+ /**
59
+ * Opções para seleção (API antiga)
60
+ */
61
+ options?: AutocompleteOption[];
62
+ /**
63
+ * Valor selecionado (controlado)
64
+ */
65
+ value?: string;
66
+ /**
67
+ * Valor padrão (não controlado)
68
+ */
69
+ defaultValue?: string;
70
+ /**
71
+ * Callback quando o valor muda
72
+ */
73
+ onValueChange?: (value: string | undefined) => void;
74
+ /**
75
+ * Função de filtro customizada
76
+ */
77
+ filterFunction?: (option: AutocompleteOption, searchTerm: string) => boolean;
78
+ /**
79
+ * Permite valores customizados (não presentes nas opções)
80
+ */
81
+ allowCustomValue?: boolean;
82
+ /**
83
+ * Estado de loading
84
+ */
85
+ isLoading?: boolean;
86
+ /**
87
+ * Mensagem quando não há resultados
88
+ */
89
+ emptyMessage?: string;
90
+ /**
91
+ * Children para composição (modo compound components)
92
+ */
93
+ children?: React.ReactNode;
94
+ }
95
+ export interface UseAutocompleteReturn {
96
+ /**
97
+ * Valor atual do input
98
+ */
99
+ inputValue: string;
100
+ /**
101
+ * Valor selecionado
102
+ */
103
+ selectedValue: string | undefined;
104
+ /**
105
+ * Se o popover está aberto
106
+ */
107
+ isOpen: boolean;
108
+ /**
109
+ * Items filtrados baseado no inputValue
110
+ */
111
+ filteredItems: AutocompleteOption[];
112
+ /**
113
+ * Índice do item destacado (para navegação por teclado)
114
+ */
115
+ highlightedIndex: number;
116
+ /**
117
+ * Estado de loading
118
+ */
119
+ isLoading: boolean;
120
+ /**
121
+ * Se não há items para exibir
122
+ */
123
+ isEmpty: boolean;
124
+ /**
125
+ * Abre o popover
126
+ */
127
+ open: () => void;
128
+ /**
129
+ * Fecha o popover
130
+ */
131
+ close: () => void;
132
+ /**
133
+ * Seleciona um valor
134
+ */
135
+ select: (value: string) => void;
136
+ /**
137
+ * Limpa a seleção
138
+ */
139
+ clear: () => void;
140
+ /**
141
+ * Define o valor do input
142
+ */
143
+ setInputValue: (value: string) => void;
144
+ /**
145
+ * Helper para props do input
146
+ */
147
+ getInputProps: (props?: React.InputHTMLAttributes<HTMLInputElement>) => Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange" | "onFocus" | "onBlur" | "onKeyDown" | "aria-expanded" | "aria-autocomplete" | "aria-controls" | "aria-activedescendant" | "role"> & {
148
+ value: string;
149
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
150
+ onFocus: React.FocusEventHandler<HTMLInputElement>;
151
+ onBlur: React.FocusEventHandler<HTMLInputElement>;
152
+ onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
153
+ "aria-expanded": boolean;
154
+ "aria-autocomplete": "list";
155
+ "aria-controls": string;
156
+ "aria-activedescendant": string | undefined;
157
+ role: "combobox";
158
+ };
159
+ /**
160
+ * Helper para props do popover
161
+ */
162
+ getPopoverProps: (props?: React.ComponentProps<"div">) => React.ComponentProps<"div"> & {
163
+ "aria-label": string;
164
+ role: "listbox";
165
+ };
166
+ /**
167
+ * Helper para props da lista
168
+ */
169
+ getListProps: (props?: React.HTMLAttributes<HTMLUListElement>) => React.HTMLAttributes<HTMLUListElement> & {
170
+ id: string;
171
+ role: "listbox";
172
+ "aria-label": string;
173
+ };
174
+ /**
175
+ * Helper para props de um item
176
+ */
177
+ getItemProps: (item: AutocompleteOption, props?: React.HTMLAttributes<HTMLLIElement>) => Omit<React.HTMLAttributes<HTMLLIElement>, "role" | "aria-selected" | "aria-disabled" | "id" | "onClick" | "onMouseEnter"> & {
178
+ role: "option";
179
+ "aria-selected": boolean;
180
+ "aria-disabled": boolean;
181
+ id: string;
182
+ onClick: (e: React.MouseEvent<HTMLLIElement>) => void;
183
+ onMouseEnter: (e: React.MouseEvent<HTMLLIElement>) => void;
184
+ };
185
+ /**
186
+ * Extrai items dos children (para modo composição)
187
+ */
188
+ extractItemsFromChildren: (children: React.ReactNode) => AutocompleteOption[];
189
+ }
190
+ /**
191
+ * Hook Headless UI para Autocomplete
192
+ *
193
+ * Fornece apenas a lógica e API, sem markup ou styles.
194
+ * Você é responsável por criar o visual.
195
+ */
196
+ export declare function useAutocomplete(options?: UseAutocompleteOptions): UseAutocompleteReturn;
197
+ //# sourceMappingURL=useAutocomplete.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAutocomplete.d.ts","sourceRoot":"","sources":["../../../src/hooks/useAutocomplete/useAutocomplete.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,379 @@
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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
52
+ import * as React 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 = "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
+ ]);
317
+ const getPopoverProps = useCallback((props = {}) => {
318
+ return {
319
+ ...props,
320
+ "aria-label": "Lista de opções",
321
+ role: "listbox",
322
+ };
323
+ }, []);
324
+ const getListProps = useCallback((props = {}) => {
325
+ return {
326
+ ...props,
327
+ id: listId,
328
+ role: "listbox",
329
+ "aria-label": "Opções disponíveis",
330
+ };
331
+ }, [listId]);
332
+ const getItemProps = useCallback((item, props = {}) => {
333
+ const itemId = `${listId}-item-${item.value}`;
334
+ const isSelected = selectedValue === item.value;
335
+ const isHighlighted = filteredItems[highlightedIndex]?.value === item.value;
336
+ const handleClick = (e) => {
337
+ if (!item.disabled) {
338
+ select(item.value);
339
+ }
340
+ props.onClick?.(e);
341
+ };
342
+ const handleMouseEnter = (e) => {
343
+ const index = filteredItems.findIndex((i) => i.value === item.value);
344
+ if (index >= 0) {
345
+ setHighlightedIndex(index);
346
+ }
347
+ props.onMouseEnter?.(e);
348
+ };
349
+ return {
350
+ ...props,
351
+ role: "option",
352
+ "aria-selected": isSelected,
353
+ "aria-disabled": item.disabled ?? false,
354
+ id: itemId,
355
+ onClick: handleClick,
356
+ onMouseEnter: handleMouseEnter,
357
+ className: `${props.className || ""} ${isHighlighted ? "bg-accent" : ""} ${isSelected ? "font-semibold" : ""}`.trim(),
358
+ };
359
+ }, [listId, selectedValue, filteredItems, highlightedIndex, select]);
360
+ return {
361
+ inputValue,
362
+ selectedValue,
363
+ isOpen,
364
+ filteredItems,
365
+ highlightedIndex,
366
+ isLoading,
367
+ isEmpty,
368
+ open,
369
+ close,
370
+ select,
371
+ clear,
372
+ setInputValue,
373
+ getInputProps,
374
+ getPopoverProps,
375
+ getListProps,
376
+ getItemProps,
377
+ extractItemsFromChildren,
378
+ };
379
+ }
@@ -0,0 +1,3 @@
1
+ export type { UseDashboardLayoutProps, UseDashboardLayoutReturn, WidgetLayout, GridConfig, } from "./useDashboardLayout";
2
+ export { useDashboardLayout } from "./useDashboardLayout";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/useDashboardLayout/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,uBAAuB,EACvB,wBAAwB,EACxB,YAAY,EACZ,UAAU,GACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1 @@
1
+ export { useDashboardLayout } from "./useDashboardLayout";