@lucasbernardosn/react-utils 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @lucasbernardosn/react-utils
2
+
3
+ Hooks e helpers de React sem relação com UI: contexto estrito, refs combinadas, media query e
4
+ área de transferência. É peer do [`@lucasbernardosn/react-ui`](https://www.npmjs.com/package/@lucasbernardosn/react-ui)
5
+ e do [`@lucasbernardosn/react-table`](https://www.npmjs.com/package/@lucasbernardosn/react-table),
6
+ e serve também sozinho.
7
+
8
+ ## Instalação
9
+
10
+ ```bash
11
+ pnpm add @lucasbernardosn/react-utils react
12
+ ```
13
+
14
+ O React (19) é peer: o app e o pacote usam a mesma cópia.
15
+
16
+ ## Hooks
17
+
18
+ ### `useMediaQuery`
19
+
20
+ Se uma media query CSS casa, atualizado quando a viewport ou o dispositivo mudam. No servidor e
21
+ na hidratação o valor é o `defaultValue` (`false`, por padrão), e o navegador corrige logo em
22
+ seguida.
23
+
24
+ ```tsx
25
+ import { useMediaQuery } from '@lucasbernardosn/react-utils';
26
+
27
+ const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
28
+ const isWide = useMediaQuery('(min-width: 64rem)', { defaultValue: true });
29
+ ```
30
+
31
+ ### `useCopyToClipboard`
32
+
33
+ Copia um texto para a área de transferência. O `copy` devolve uma promise com o resultado da
34
+ cópia, e o `copiedText` guarda o último texto copiado (`null` antes da primeira cópia ou depois de
35
+ uma que falhou). Sem a API Clipboard (fora de HTTPS, no servidor), o `copy` resolve `false` em vez
36
+ de lançar.
37
+
38
+ ```tsx
39
+ import { useCopyToClipboard } from '@lucasbernardosn/react-utils';
40
+
41
+ const { copiedText, copy } = useCopyToClipboard();
42
+
43
+ <button type="button" onClick={() => copy(invoice.key)}>
44
+ {copiedText === invoice.key ? 'Copiado' : 'Copiar a chave'}
45
+ </button>;
46
+ ```
47
+
48
+ ## Helpers
49
+
50
+ ### `createContext`
51
+
52
+ Cria um contexto estrito: o hook lança um erro quando chamado fora do Provider, com o nome do
53
+ contexto e os componentes que o fornecem na mensagem. O Provider e o hook recebem o nome na
54
+ desestruturação.
55
+
56
+ ```tsx
57
+ import { createContext } from '@lucasbernardosn/react-utils';
58
+
59
+ export const [CartContextProvider, useCartContext] = createContext<Cart>({
60
+ name: 'CartContext',
61
+ providers: ['CartContextProvider'],
62
+ });
63
+ ```
64
+
65
+ ### `mergeRefs`
66
+
67
+ Junta várias refs (callback ou objeto) numa ref callback só; as `null` e `undefined` são
68
+ ignoradas. Serve para um elemento que precisa ir para uma ref interna e para a ref recebida por
69
+ props.
70
+
71
+ ```tsx
72
+ import { mergeRefs } from '@lucasbernardosn/react-utils';
73
+
74
+ <div ref={mergeRefs(localRef, props.ref)} />;
75
+ ```
76
+
77
+ ## Licença
78
+
79
+ MIT.
@@ -1,2 +1,3 @@
1
1
  import { useCopyToClipboard } from "./use-copy-to-clipboard.js";
2
- export { useCopyToClipboard };
2
+ import { UseMediaQueryOptions, useMediaQuery } from "./use-media-query.js";
3
+ export { UseMediaQueryOptions, useCopyToClipboard, useMediaQuery };
@@ -1,2 +1,3 @@
1
1
  import { useCopyToClipboard } from "./use-copy-to-clipboard.js";
2
- export { useCopyToClipboard };
2
+ import { useMediaQuery } from "./use-media-query.js";
3
+ export { useCopyToClipboard, useMediaQuery };
@@ -1,23 +1,17 @@
1
1
  //#region src/hooks/use-media-query.d.ts
2
- /**
3
- * Configurações opcionais para controlar o comportamento inicial do hook useMediaQuery.
4
- */
5
- type UseMediaQueryOptions = {
6
- /** Valor padrão retornado quando `initializeWithValue` é false. */
2
+ export type UseMediaQueryOptions = {
3
+ /**
4
+ * Valor no servidor e na hidratação, antes de o navegador responder.
5
+ *
6
+ * @default false
7
+ */
7
8
  defaultValue?: boolean;
8
- /** Define se deve inicializar com o valor atual da media query. */
9
- initializeWithValue?: boolean;
10
9
  };
11
10
  /**
12
- * Monitora continuamente mudanças em media queries CSS, reagindo automaticamente
13
- * a alterações no tamanho da viewport ou outras características do dispositivo.
14
- *
15
- * O hook gerencia automaticamente a adição e remoção de listeners, prevenindo
16
- * vazamentos de memória quando o componente é desmontado.
11
+ * Acompanha se uma media query CSS casa, reagindo às mudanças da viewport ou do dispositivo.
17
12
  *
18
- * @param query - String da media query CSS a ser monitorada (ex: `"(min-width: 768px)"`).
19
- * @param options - Configurações opcionais para controlar o comportamento inicial do hook.
20
- * @returns Verdadeiro quando a media query atual corresponde à condição especificada, falso caso contrário.
13
+ * Usa `useSyncExternalStore`: no servidor e na hidratação o valor é o `defaultValue`, e o
14
+ * navegador corrige logo em seguida. O listener é removido ao desmontar ou ao trocar a `query`.
21
15
  */
22
16
  export declare const useMediaQuery: (query: string, options?: UseMediaQueryOptions) => boolean;
23
17
  //#endregion
@@ -1,39 +1,26 @@
1
- import { useCallback, useLayoutEffect, useState } from "react";
1
+ import { useCallback, useSyncExternalStore } from "react";
2
2
  //#region src/hooks/use-media-query.ts
3
3
  /**
4
- * Monitora continuamente mudanças em media queries CSS, reagindo automaticamente
5
- * a alterações no tamanho da viewport ou outras características do dispositivo.
4
+ * Acompanha se uma media query CSS casa, reagindo às mudanças da viewport ou do dispositivo.
6
5
  *
7
- * O hook gerencia automaticamente a adição e remoção de listeners, prevenindo
8
- * vazamentos de memória quando o componente é desmontado.
9
- *
10
- * @param query - String da media query CSS a ser monitorada (ex: `"(min-width: 768px)"`).
11
- * @param options - Configurações opcionais para controlar o comportamento inicial do hook.
12
- * @returns Verdadeiro quando a media query atual corresponde à condição especificada, falso caso contrário.
6
+ * Usa `useSyncExternalStore`: no servidor e na hidratação o valor é o `defaultValue`, e o
7
+ * navegador corrige logo em seguida. O listener é removido ao desmontar ou ao trocar a `query`.
13
8
  */
14
9
  const useMediaQuery = (query, options = {}) => {
15
- const { defaultValue = false, initializeWithValue = true } = options;
16
- const getMatches = useCallback((query) => {
17
- return window.matchMedia(query).matches;
18
- }, []);
19
- const [matches, setMatches] = useState(() => {
20
- if (initializeWithValue) return getMatches(query);
21
- return defaultValue;
22
- });
23
- const handleChange = useCallback(() => {
24
- setMatches(getMatches(query));
25
- }, [query, getMatches]);
26
- useLayoutEffect(() => {
27
- const matchMedia = window.matchMedia(query);
28
- handleChange();
29
- if (matchMedia.addListener) matchMedia.addListener(handleChange);
30
- else matchMedia.addEventListener("change", handleChange);
10
+ const { defaultValue = false } = options;
11
+ /**
12
+ * Memorizado pela `query`: o React refaz a assinatura sempre que o `subscribe` muda, e uma
13
+ * função nova a cada render tiraria e poria o listener em todo render.
14
+ */
15
+ const subscribe = useCallback((onStoreChange) => {
16
+ const mediaQueryList = window.matchMedia(query);
17
+ mediaQueryList.addEventListener("change", onStoreChange);
31
18
  return () => {
32
- if (matchMedia.removeListener) matchMedia.removeListener(handleChange);
33
- else matchMedia.removeEventListener("change", handleChange);
19
+ mediaQueryList.removeEventListener("change", onStoreChange);
34
20
  };
35
- }, [query, handleChange]);
36
- return matches;
21
+ }, [query]);
22
+ const getSnapshot = () => window.matchMedia(query).matches;
23
+ return useSyncExternalStore(subscribe, getSnapshot, () => defaultValue);
37
24
  };
38
25
  //#endregion
39
26
  export { useMediaQuery };
@@ -1 +1 @@
1
- {"version":3,"file":"use-media-query.js","names":[],"sources":["../../src/hooks/use-media-query.ts"],"sourcesContent":["import { useCallback, useLayoutEffect, useState } from 'react';\n\n/**\n * Configurações opcionais para controlar o comportamento inicial do hook useMediaQuery.\n */\ntype UseMediaQueryOptions = {\n /** Valor padrão retornado quando `initializeWithValue` é false. */\n defaultValue?: boolean;\n /** Define se deve inicializar com o valor atual da media query. */\n initializeWithValue?: boolean;\n};\n\n/**\n * Monitora continuamente mudanças em media queries CSS, reagindo automaticamente\n * a alterações no tamanho da viewport ou outras características do dispositivo.\n *\n * O hook gerencia automaticamente a adição e remoção de listeners, prevenindo\n * vazamentos de memória quando o componente é desmontado.\n *\n * @param query - String da media query CSS a ser monitorada (ex: `\"(min-width: 768px)\"`).\n * @param options - Configurações opcionais para controlar o comportamento inicial do hook.\n * @returns Verdadeiro quando a media query atual corresponde à condição especificada, falso caso contrário.\n */\nexport const useMediaQuery = (\n query: string,\n options: UseMediaQueryOptions = {},\n): boolean => {\n const { defaultValue = false, initializeWithValue = true } = options;\n\n const getMatches = useCallback((query: string): boolean => {\n return window.matchMedia(query).matches;\n }, []);\n\n const [matches, setMatches] = useState<boolean>(() => {\n if (initializeWithValue) {\n return getMatches(query);\n }\n\n return defaultValue;\n });\n\n const handleChange = useCallback(() => {\n setMatches(getMatches(query));\n }, [query, getMatches]);\n\n useLayoutEffect(() => {\n const matchMedia = window.matchMedia(query);\n\n handleChange();\n\n /*\n * Safari < 14 não suporta `addEventListener` em MediaQueryList —\n * usa os métodos deprecated `addListener`/`removeListener` como fallback.\n */\n if (matchMedia.addListener) {\n matchMedia.addListener(handleChange);\n } else {\n matchMedia.addEventListener('change', handleChange);\n }\n\n return () => {\n if (matchMedia.removeListener) {\n matchMedia.removeListener(handleChange);\n } else {\n matchMedia.removeEventListener('change', handleChange);\n }\n };\n }, [query, handleChange]);\n\n return matches;\n};\n"],"mappings":";;;;;;;;;;;;;AAuBA,MAAa,iBACX,OACA,UAAgC,CAAC,MACrB;CACZ,MAAM,EAAE,eAAe,OAAO,sBAAsB,SAAS;CAE7D,MAAM,aAAa,aAAa,UAA2B;EACzD,OAAO,OAAO,WAAW,KAAK,CAAC,CAAC;CAClC,GAAG,CAAC,CAAC;CAEL,MAAM,CAAC,SAAS,cAAc,eAAwB;EACpD,IAAI,qBACF,OAAO,WAAW,KAAK;EAGzB,OAAO;CACT,CAAC;CAED,MAAM,eAAe,kBAAkB;EACrC,WAAW,WAAW,KAAK,CAAC;CAC9B,GAAG,CAAC,OAAO,UAAU,CAAC;CAEtB,sBAAsB;EACpB,MAAM,aAAa,OAAO,WAAW,KAAK;EAE1C,aAAa;EAMb,IAAI,WAAW,aACb,WAAW,YAAY,YAAY;OAEnC,WAAW,iBAAiB,UAAU,YAAY;EAGpD,aAAa;GACX,IAAI,WAAW,gBACb,WAAW,eAAe,YAAY;QAEtC,WAAW,oBAAoB,UAAU,YAAY;EAEzD;CACF,GAAG,CAAC,OAAO,YAAY,CAAC;CAExB,OAAO;AACT"}
1
+ {"version":3,"file":"use-media-query.js","names":[],"sources":["../../src/hooks/use-media-query.ts"],"sourcesContent":["import { useCallback, useSyncExternalStore } from 'react';\n\nexport type UseMediaQueryOptions = {\n /**\n * Valor no servidor e na hidratação, antes de o navegador responder.\n *\n * @default false\n */\n defaultValue?: boolean;\n};\n\n/**\n * Acompanha se uma media query CSS casa, reagindo às mudanças da viewport ou do dispositivo.\n *\n * Usa `useSyncExternalStore`: no servidor e na hidratação o valor é o `defaultValue`, e o\n * navegador corrige logo em seguida. O listener é removido ao desmontar ou ao trocar a `query`.\n */\nexport const useMediaQuery = (\n query: string,\n options: UseMediaQueryOptions = {},\n): boolean => {\n const { defaultValue = false } = options;\n\n /**\n * Memorizado pela `query`: o React refaz a assinatura sempre que o `subscribe` muda, e uma\n * função nova a cada render tiraria e poria o listener em todo render.\n */\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const mediaQueryList = window.matchMedia(query);\n\n mediaQueryList.addEventListener('change', onStoreChange);\n\n return () => {\n mediaQueryList.removeEventListener('change', onStoreChange);\n };\n },\n [query],\n );\n\n const getSnapshot = () => window.matchMedia(query).matches;\n\n return useSyncExternalStore(subscribe, getSnapshot, () => defaultValue);\n};\n"],"mappings":";;;;;;;;AAiBA,MAAa,iBACX,OACA,UAAgC,CAAC,MACrB;CACZ,MAAM,EAAE,eAAe,UAAU;;;;;CAMjC,MAAM,YAAY,aACf,kBAA8B;EAC7B,MAAM,iBAAiB,OAAO,WAAW,KAAK;EAE9C,eAAe,iBAAiB,UAAU,aAAa;EAEvD,aAAa;GACX,eAAe,oBAAoB,UAAU,aAAa;EAC5D;CACF,GACA,CAAC,KAAK,CACR;CAEA,MAAM,oBAAoB,OAAO,WAAW,KAAK,CAAC,CAAC;CAEnD,OAAO,qBAAqB,WAAW,mBAAmB,YAAY;AACxE"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { useCopyToClipboard } from "./hooks/use-copy-to-clipboard.js";
2
+ import { UseMediaQueryOptions, useMediaQuery } from "./hooks/use-media-query.js";
2
3
  import { CreateContextOptions, CreateContextReturn, createContext } from "./utils/create-context.js";
3
4
  import { mergeRefs } from "./utils/merge-refs.js";
4
5
  import "./utils/index.js";
5
- export { CreateContextOptions, CreateContextReturn, createContext, mergeRefs, useCopyToClipboard };
6
+ export { CreateContextOptions, CreateContextReturn, UseMediaQueryOptions, createContext, mergeRefs, useCopyToClipboard, useMediaQuery };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { useCopyToClipboard } from "./hooks/use-copy-to-clipboard.js";
2
+ import { useMediaQuery } from "./hooks/use-media-query.js";
2
3
  import { createContext } from "./utils/create-context.js";
3
4
  import { mergeRefs } from "./utils/merge-refs.js";
4
- export { createContext, mergeRefs, useCopyToClipboard };
5
+ export { createContext, mergeRefs, useCopyToClipboard, useMediaQuery };
@@ -1 +1 @@
1
- {"version":3,"file":"create-context.js","names":["createReactContext"],"sources":["../../src/utils/create-context.ts"],"sourcesContent":["import type React from 'react';\nimport { createContext as createReactContext, use } from 'react';\n\n/**\n * Valor default de todo contexto criado aqui: só é lido quando não há Provider\n * acima. Como é um símbolo privado, nenhum valor legítimo (`0`, `false`, `''`,\n * `undefined`) se confunde com ele.\n */\nconst MISSING_PROVIDER: unique symbol = Symbol('MissingProvider');\n\n/**\n * Opções do `createContext`.\n */\nexport type CreateContextOptions = {\n /**\n * Nome exibido no React DevTools e na mensagem de erro.\n *\n * @example 'TabsContext'\n */\n name: string;\n /**\n * Componentes públicos que fornecem o contexto, citados na mensagem de erro.\n *\n * Na lib, o Provider é interno: quem é de fora fornece o contexto pelo\n * `Root` ou pelo `RootProvider`, então são esses nomes que entram aqui. Em\n * contexto de app, em que o Provider é a própria API pública, entra ele.\n *\n * @example ['Splitter.Root', 'Splitter.RootProvider']\n */\n providers: [string, ...string[]];\n};\n\n/**\n * `Error.captureStackTrace` é exclusivo do V8 e fica fora dos tipos da lib\n * padrão do TypeScript.\n */\ntype V8ErrorConstructor = ErrorConstructor & {\n captureStackTrace?: (target: object, constructorOpt?: () => unknown) => void;\n};\n\n/**\n * Junta os nomes com vírgula e \"ou\" antes do último, cada um entre crases.\n */\nconst providersListFormat = new Intl.ListFormat('pt-BR', {\n type: 'disjunction',\n});\n\n/**\n * Retorno do `createContext`: o Provider e o hook que lê o valor dele.\n *\n * O Provider é tipado como `React.Provider` (e não `React.Context`) para que o\n * valor só seja lido pelo hook: `use(Provider)` não compila.\n *\n * @template TValue - Tipo do valor armazenado no contexto.\n */\nexport type CreateContextReturn<TValue> = [\n provider: React.Provider<TValue>,\n useContext: () => TValue,\n];\n\n/**\n * Cria um contexto estrito: o hook lança `ContextError` quando chamado fora do\n * Provider.\n *\n * Os nomes do Provider e do hook são escolhidos na desestruturação. Convenção:\n * `XContextProvider` e `useXContext`.\n *\n * @template TValue - Tipo do valor armazenado no contexto.\n * @param options - Nome do contexto e do componente que o provê.\n * @returns Tupla com o Provider e o hook.\n *\n * @example\n * ```tsx\n * export const [SplitterContextProvider, useSplitterContext] =\n * createContext<UseSplitterContext>({\n * name: 'SplitterContext',\n * providers: ['Splitter.Root', 'Splitter.RootProvider'],\n * });\n *\n * <SplitterContextProvider value={splitter}>{children}</SplitterContextProvider>;\n *\n * const splitter = useSplitterContext();\n * ```\n */\nexport const createContext = <TValue>(\n options: CreateContextOptions,\n): CreateContextReturn<TValue> => {\n const { name, providers } = options;\n\n const errorMessage = `\\`${name}\\` não encontrado: o componente precisa estar dentro de ${providersListFormat.format(\n providers.map((provider) => `\\`${provider}\\``),\n )}.`;\n\n const Context = createReactContext<TValue | typeof MISSING_PROVIDER>(\n MISSING_PROVIDER,\n );\n\n Context.displayName = name;\n\n const useContext = (): TValue => {\n const value = use(Context);\n\n if (value === MISSING_PROVIDER) {\n const error = new Error(errorMessage);\n\n error.name = 'ContextError';\n\n // Tira o hook do stack: a primeira linha passa a ser o componente que o\n // chamou. Só existe em engines V8; nas outras, o stack fica completo.\n (Error as V8ErrorConstructor).captureStackTrace?.(error, useContext);\n\n throw error;\n }\n\n return value;\n };\n\n return [Context, useContext];\n};\n"],"mappings":";;;;;;;AAQA,MAAM,mBAAkC,OAAO,iBAAiB;;;;AAmChE,MAAM,sBAAsB,IAAI,KAAK,WAAW,SAAS,EACvD,MAAM,cACR,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AAuCD,MAAa,iBACX,YACgC;CAChC,MAAM,EAAE,MAAM,cAAc;CAE5B,MAAM,eAAe,KAAK,KAAK,0DAA0D,oBAAoB,OAC3G,UAAU,KAAK,aAAa,KAAK,SAAS,GAAG,CAC/C,EAAE;CAEF,MAAM,UAAUA,gBACd,gBACF;CAEA,QAAQ,cAAc;CAEtB,MAAM,mBAA2B;EAC/B,MAAM,QAAQ,IAAI,OAAO;EAEzB,IAAI,UAAU,kBAAkB;GAC9B,MAAM,QAAQ,IAAI,MAAM,YAAY;GAEpC,MAAM,OAAO;GAIb,MAA8B,oBAAoB,OAAO,UAAU;GAEnE,MAAM;EACR;EAEA,OAAO;CACT;CAEA,OAAO,CAAC,SAAS,UAAU;AAC7B"}
1
+ {"version":3,"file":"create-context.js","names":["createReactContext"],"sources":["../../src/utils/create-context.ts"],"sourcesContent":["import type React from 'react';\nimport { createContext as createReactContext, use } from 'react';\n\n/**\n * Valor default de todo contexto criado aqui: só é lido quando não há Provider\n * acima. Como é um símbolo privado, nenhum valor legítimo (`0`, `false`, `''`,\n * `undefined`) se confunde com ele.\n */\nconst MISSING_PROVIDER: unique symbol = Symbol('MissingProvider');\n\n/**\n * Opções do `createContext`.\n */\nexport type CreateContextOptions = {\n /**\n * Nome exibido no React DevTools e na mensagem de erro.\n *\n * @example 'TabsContext'\n */\n name: string;\n /**\n * Componentes públicos que fornecem o contexto, citados na mensagem de erro.\n *\n * Na lib, o Provider é interno: quem é de fora fornece o contexto pelo\n * `Root` ou pelo `RootProvider`, então são esses nomes que entram aqui. Em\n * contexto de app, em que o Provider é a própria API pública, entra ele.\n *\n * @example ['Splitter.Root', 'Splitter.RootProvider']\n */\n providers: [string, ...string[]];\n};\n\n/**\n * `Error.captureStackTrace` é exclusivo do V8 e fica fora dos tipos da lib\n * padrão do TypeScript.\n */\ntype V8ErrorConstructor = ErrorConstructor & {\n captureStackTrace?: (target: object, constructorOpt?: () => unknown) => void;\n};\n\n/**\n * Junta os nomes com vírgula e \"ou\" antes do último, cada um entre crases.\n */\nconst providersListFormat = new Intl.ListFormat('pt-BR', {\n type: 'disjunction',\n});\n\n/**\n * Retorno do `createContext`: o Provider e o hook que lê o valor dele.\n *\n * O Provider é tipado como `React.Provider` (e não `React.Context`) para que o\n * valor só seja lido pelo hook: `use(Provider)` não compila.\n *\n * @template TValue - Tipo do valor armazenado no contexto.\n */\nexport type CreateContextReturn<TValue> = [\n provider: React.Provider<TValue>,\n useContext: () => TValue,\n];\n\n/**\n * Cria um contexto estrito: o hook lança `ContextError` quando chamado fora do\n * Provider.\n *\n * Os nomes do Provider e do hook são escolhidos na desestruturação. Convenção:\n * `XContextProvider` e `useXContext`.\n *\n * @template TValue - Tipo do valor armazenado no contexto.\n * @param options - Nome do contexto e do componente que o provê.\n * @returns Tupla com o Provider e o hook.\n *\n * @example\n * ```tsx\n * export const [SplitterContextProvider, useSplitterContext] =\n * createContext<UseSplitterContext>({\n * name: 'SplitterContext',\n * providers: ['Splitter.Root', 'Splitter.RootProvider'],\n * });\n *\n * <SplitterContextProvider value={splitter}>{children}</SplitterContextProvider>;\n *\n * const splitter = useSplitterContext();\n * ```\n */\nexport const createContext = <TValue>(\n options: CreateContextOptions,\n): CreateContextReturn<TValue> => {\n const { name, providers } = options;\n\n const errorMessage = `\\`${name}\\` não encontrado: o componente precisa estar dentro de ${providersListFormat.format(\n providers.map((provider) => `\\`${provider}\\``),\n )}.`;\n\n const Context = createReactContext<TValue | typeof MISSING_PROVIDER>(\n MISSING_PROVIDER,\n );\n\n Context.displayName = name;\n\n // biome-ignore lint/correctness/noComponentHookFactories: a factory roda uma vez, no nível do módulo, e o hook criado é estável.\n const useContext = (): TValue => {\n const value = use(Context);\n\n if (value === MISSING_PROVIDER) {\n const error = new Error(errorMessage);\n\n error.name = 'ContextError';\n\n // Tira o hook do stack: a primeira linha passa a ser o componente que o\n // chamou. Só existe em engines V8; nas outras, o stack fica completo.\n const errorConstructor: V8ErrorConstructor = Error;\n\n errorConstructor.captureStackTrace?.(error, useContext);\n\n throw error;\n }\n\n return value;\n };\n\n return [Context, useContext];\n};\n"],"mappings":";;;;;;;AAQA,MAAM,mBAAkC,OAAO,iBAAiB;;;;AAmChE,MAAM,sBAAsB,IAAI,KAAK,WAAW,SAAS,EACvD,MAAM,cACR,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AAuCD,MAAa,iBACX,YACgC;CAChC,MAAM,EAAE,MAAM,cAAc;CAE5B,MAAM,eAAe,KAAK,KAAK,0DAA0D,oBAAoB,OAC3G,UAAU,KAAK,aAAa,KAAK,SAAS,GAAG,CAC/C,EAAE;CAEF,MAAM,UAAUA,gBACd,gBACF;CAEA,QAAQ,cAAc;CAGtB,MAAM,mBAA2B;EAC/B,MAAM,QAAQ,IAAI,OAAO;EAEzB,IAAI,UAAU,kBAAkB;GAC9B,MAAM,QAAQ,IAAI,MAAM,YAAY;GAEpC,MAAM,OAAO;GAMb,MAAiB,oBAAoB,OAAO,UAAU;GAEtD,MAAM;EACR;EAEA,OAAO;CACT;CAEA,OAAO,CAAC,SAAS,UAAU;AAC7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"merge-refs.js","names":[],"sources":["../../src/utils/merge-refs.ts"],"sourcesContent":["import type React from 'react';\n\n/**\n * Atribui `value` a uma ref, normalizando callback refs e RefObjects.\n *\n * Necessário porque o React expõe dois formatos de ref e não há uma API\n * unificada para escrevê-los — esta função esconde esse detalhe dos\n * consumidores.\n *\n * @template T - Tipo do valor referenciado (ex: elemento DOM ou instância).\n * @param ref - Ref callback, RefObject ou `undefined`/`null` (ignorada).\n * @param value - Valor a ser atribuído à ref.\n */\nfunction setRef<T>(ref: React.Ref<T> | undefined, value: T | null): void {\n if (typeof ref === 'function') {\n (ref as React.RefCallback<T>)(value);\n } else if (ref != null) {\n (ref as React.RefObject<T | null>).current = value;\n }\n}\n\n/**\n * Combina múltiplas refs em uma única ref callback.\n *\n * Refs `undefined`/`null` são ignoradas silenciosamente. Útil para expor um\n * elemento tanto para uma ref interna quanto para uma ref encaminhada\n * (`forwardRef`) simultaneamente.\n *\n * @template T - Tipo do valor referenciado (ex: elemento DOM ou instância).\n * @param refs - Lista de refs (callback, RefObject, `undefined` ou `null`) a serem combinadas.\n * @returns Ref callback única que propaga o node para todas as refs recebidas.\n */\nexport function mergeRefs<T>(\n ...refs: (React.Ref<T> | undefined)[]\n): React.RefCallback<T> {\n return (node: T | null) => {\n for (const ref of refs) {\n setRef(ref, node);\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;AAaA,SAAS,OAAU,KAA+B,OAAuB;CACvE,IAAI,OAAO,QAAQ,YACjB,IAA8B,KAAK;MAC9B,IAAI,OAAO,MAChB,IAAmC,UAAU;AAEjD;;;;;;;;;;;;AAaA,SAAgB,UACd,GAAG,MACmB;CACtB,QAAQ,SAAmB;EACzB,KAAK,MAAM,OAAO,MAChB,OAAO,KAAK,IAAI;CAEpB;AACF"}
1
+ {"version":3,"file":"merge-refs.js","names":[],"sources":["../../src/utils/merge-refs.ts"],"sourcesContent":["import type React from 'react';\n\n/**\n * Atribui `value` a uma ref, normalizando callback refs e RefObjects.\n *\n * Necessário porque o React expõe dois formatos de ref e não há uma API\n * unificada para escrevê-los — esta função esconde esse detalhe dos\n * consumidores.\n *\n * @template T - Tipo do valor referenciado (ex: elemento DOM ou instância).\n * @param ref - Ref callback, RefObject ou `undefined`/`null` (ignorada).\n * @param value - Valor a ser atribuído à ref.\n */\nfunction setRef<T>(ref: React.Ref<T> | undefined, value: T | null): void {\n if (typeof ref === 'function') {\n ref(value);\n } else if (ref != null) {\n ref.current = value;\n }\n}\n\n/**\n * Combina múltiplas refs em uma única ref callback.\n *\n * Refs `undefined`/`null` são ignoradas silenciosamente. Útil para expor um\n * elemento tanto para uma ref interna quanto para uma ref encaminhada\n * (`forwardRef`) simultaneamente.\n *\n * @template T - Tipo do valor referenciado (ex: elemento DOM ou instância).\n * @param refs - Lista de refs (callback, RefObject, `undefined` ou `null`) a serem combinadas.\n * @returns Ref callback única que propaga o node para todas as refs recebidas.\n */\nexport function mergeRefs<T>(\n ...refs: (React.Ref<T> | undefined)[]\n): React.RefCallback<T> {\n return (node: T | null) => {\n for (const ref of refs) {\n setRef(ref, node);\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;AAaA,SAAS,OAAU,KAA+B,OAAuB;CACvE,IAAI,OAAO,QAAQ,YACjB,IAAI,KAAK;MACJ,IAAI,OAAO,MAChB,IAAI,UAAU;AAElB;;;;;;;;;;;;AAaA,SAAgB,UACd,GAAG,MACmB;CACtB,QAAQ,SAAmB;EACzB,KAAK,MAAM,OAAO,MAChB,OAAO,KAAK,IAAI;CAEpB;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucasbernardosn/react-utils",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "React hooks and utilities.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -9,18 +9,17 @@
9
9
  "default": "./dist/index.js"
10
10
  }
11
11
  },
12
- "dependencies": {},
13
12
  "devDependencies": {
14
- "@biomejs/biome": "2.5.15",
15
- "@lucasbernardosn/tsconfig": "1.0.0",
13
+ "@biomejs/biome": "2.6.3",
14
+ "@lucasbernardosn/ts-config": "0.2.0",
16
15
  "@testing-library/jest-dom": "7.0.1",
17
16
  "@testing-library/react": "16.3.3",
18
- "@testing-library/user-event": "14.6.7",
19
17
  "@types/node": "26.6.3",
20
18
  "@types/react": "19.3.0",
21
19
  "@types/react-dom": "19.3.0",
22
20
  "@vitejs/plugin-react": "6.1.1",
23
21
  "jsdom": "30.1.1",
22
+ "publint": "0.3.25",
24
23
  "react": "19.3.0",
25
24
  "react-dom": "19.3.0",
26
25
  "tsdown": "0.23.0",
@@ -28,8 +27,7 @@
28
27
  "vitest": "5.0.3"
29
28
  },
30
29
  "peerDependencies": {
31
- "react": "^19.0.0",
32
- "react-dom": "^19.0.0"
30
+ "react": "^19.0.0"
33
31
  },
34
32
  "files": [
35
33
  "dist"
@@ -42,7 +40,7 @@
42
40
  "repository": {
43
41
  "type": "git",
44
42
  "url": "git+https://github.com/LucasBernardoSN/react-monorepo.git",
45
- "directory": "packages/react/utils"
43
+ "directory": "packages/react-utils"
46
44
  },
47
45
  "scripts": {
48
46
  "build": "tsdown",
@@ -51,6 +49,7 @@
51
49
  "format": "biome format --write",
52
50
  "check": "biome check --write",
53
51
  "test": "vitest",
54
- "test:run": "vitest run"
52
+ "test:run": "vitest run",
53
+ "typecheck": "tsc --noEmit"
55
54
  }
56
55
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-copy-to-clipboard.d.ts","names":[],"sources":["../../src/hooks/use-copy-to-clipboard.ts"],"mappings":";;KAGK;;KAGA,UAAU,iBAAiB;;;;KAK3B;;EAEH,YAAY;;EAEZ,MAAM;;;;;;;;;;;qBAYK,0BAAyB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-media-query.d.ts","names":[],"sources":["../../src/hooks/use-media-query.ts"],"mappings":";;;;KAKK;;EAEH;;EAEA;;;;;;;;;;;;;qBAcW,gBACX,eACA,UAAS"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"create-context.d.ts","names":[],"sources":["../../src/utils/create-context.ts"],"mappings":";;;;;YAaY;;;;;;EAMV;;;;;;;;;;EAUA;;;;;;;;;;YA0BU,oBAAoB,WAC9B,UAAU,MAAM,SAAS,SACzB,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;qBA2BP,gBAAiB,QAC5B,SAAS,yBACR,oBAAoB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"merge-refs.d.ts","names":[],"sources":["../../src/utils/merge-refs.ts"],"mappings":";;;;;;;;;;;;;wBAgCgB,UAAU,MACrB,OAAO,MAAM,IAAI,oBACnB,MAAM,YAAY"}