@softize/opus 8.7.0 → 8.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "8.7.0",
3
+ "version": "8.8.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import * as React from "react"
2
2
  import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
3
3
  import { DropdownMenu as MenuPrimitive } from "radix-ui"
4
4
 
5
+ import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts"
5
6
  import { cn } from '../../lib/cn.ts'
6
7
 
7
8
  function Menu({
@@ -34,9 +35,13 @@ function MenuContent({
34
35
  sideOffset = 4,
35
36
  ...props
36
37
  }: React.ComponentProps<typeof MenuPrimitive.Content>) {
38
+ // Mesma razão do Popover: clique dentro de iframe não chega aqui fora.
39
+ const ref = React.useRef<HTMLDivElement>(null)
40
+ useDismissOnIframeFocus(ref)
37
41
  return (
38
42
  <MenuPrimitive.Portal>
39
43
  <MenuPrimitive.Content
44
+ ref={ref}
40
45
  data-slot="menu-content"
41
46
  sideOffset={sideOffset}
42
47
  className={cn(
@@ -1,6 +1,7 @@
1
1
  import * as React from "react"
2
2
  import { Popover as PopoverPrimitive } from "radix-ui"
3
3
 
4
+ import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts"
4
5
  import { cn } from '../../lib/cn.ts'
5
6
 
6
7
  function Popover({
@@ -21,9 +22,14 @@ function PopoverContent({
21
22
  sideOffset = 4,
22
23
  ...props
23
24
  }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
25
+ // Clique dentro de um iframe não chega no documento de fora — sem isto, o popover fica
26
+ // pendurado sobre o conteúdo. Na cabine, o preview É um iframe.
27
+ const ref = React.useRef<HTMLDivElement>(null)
28
+ useDismissOnIframeFocus(ref)
24
29
  return (
25
30
  <PopoverPrimitive.Portal>
26
31
  <PopoverPrimitive.Content
32
+ ref={ref}
27
33
  data-slot="popover-content"
28
34
  align={align}
29
35
  sideOffset={sideOffset}
@@ -217,9 +217,24 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
217
217
  const inputRef = React.useRef<HTMLInputElement>(null)
218
218
  /** Fechando por Esc: o foco volta pro gatilho, e focar abre. Marca pra ignorar UM foco. */
219
219
  const closingRef = React.useRef(false)
220
+ const searchRef = React.useRef<HTMLInputElement>(null)
220
221
 
221
222
  const selectedOption = options.find((o) => o.value === singleValue)
222
223
 
224
+ /**
225
+ * Abrir com busca FOCA a caixa — abrir e já poder digitar é o ponto do padrão.
226
+ *
227
+ * Explícito e não pelo `autoFocus`: aqui dependeria de o Radix (que move o foco ao
228
+ * abrir o popover) e o cmdk concordarem sobre quem manda, e o resultado varia com a
229
+ * ordem de montagem. `requestAnimationFrame` porque o conteúdo do popover entra no DOM
230
+ * depois deste efeito.
231
+ */
232
+ React.useEffect(() => {
233
+ if (!open || !canSearch) return
234
+ const r = requestAnimationFrame(() => searchRef.current?.focus())
235
+ return () => cancelAnimationFrame(r)
236
+ }, [open, canSearch])
237
+
223
238
  /**
224
239
  * O termo de busca zera ao FECHAR.
225
240
  *
@@ -467,8 +482,8 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
467
482
  <div data-slot="select-search-row" className="flex items-center gap-2 border-b px-3">
468
483
  <SearchIcon className="size-4 shrink-0 opacity-50" aria-hidden="true" />
469
484
  <CommandPrimitive.Input
485
+ ref={searchRef}
470
486
  data-slot="select-search"
471
- autoFocus
472
487
  value={query}
473
488
  onValueChange={setQuery}
474
489
  placeholder={searchPlaceholder ?? 'Buscar…'}
@@ -0,0 +1,36 @@
1
+ import * as React from 'react'
2
+
3
+ /**
4
+ * Fecha a camada flutuante quando o clique cai DENTRO de um iframe.
5
+ *
6
+ * O navegador entrega o clique ao documento do iframe, e o documento de fora nunca fica
7
+ * sabendo. Como toda dispensa por "clique fora" escuta eventos do documento de fora, o
8
+ * popover/menu simplesmente NÃO fecha — fica pendurado sobre o conteúdo, e a pessoa
9
+ * precisa achar um pedaço de página livre pra clicar.
10
+ *
11
+ * Não é caso de borda pra nós: a cabine embute o preview num iframe, então metade da tela
12
+ * é justamente a área onde clicar não fecha nada.
13
+ *
14
+ * O sinal utilizável é o `blur` da JANELA somado a "quem está com o foco agora é um
15
+ * iframe". Trocar de aba ou de aplicativo também dispara `blur`, mas ali o elemento ativo
16
+ * não é iframe — e fechar o menu de quem só foi ver outra janela seria pior que o defeito.
17
+ *
18
+ * A dispensa sai como um Escape sintético em vez de uma chamada direta: quem renderiza o
19
+ * conteúdo (Popover, Menu) não tem acesso ao estado de aberto — ele é de quem usa. Escape
20
+ * é o canal que a própria biblioteca já escuta pra dispensar, então funciona sem obrigar
21
+ * cada call site a passar um `onClose`.
22
+ */
23
+ export function useDismissOnIframeFocus(ref: React.RefObject<HTMLElement | null>): void {
24
+ React.useEffect(() => {
25
+ const onBlur = (): void => {
26
+ // `requestAnimationFrame`: no instante do `blur` o `activeElement` ainda pode ser o
27
+ // anterior; o navegador atualiza no tick seguinte.
28
+ requestAnimationFrame(() => {
29
+ if (document.activeElement?.tagName !== 'IFRAME') return
30
+ ref.current?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
31
+ })
32
+ }
33
+ window.addEventListener('blur', onBlur)
34
+ return () => window.removeEventListener('blur', onBlur)
35
+ }, [ref])
36
+ }