@softize/opus 8.8.0 → 8.8.2

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.8.0",
3
+ "version": "8.8.2",
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",
@@ -305,13 +305,27 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
305
305
  * onde não existe seleção persistente pra exibir.
306
306
  */
307
307
  const triggerText = selectedOption?.label ?? ''
308
- /** `triggerLabel` pode ser JSX (a barra do composer mostra a chave curta), e `value` de
309
- * input só aceita texto — então ele vai num `<span>` por cima, com o input `sr-only`
310
- * guardando o `id` e o rótulo textual pro leitor de tela. */
311
- const customTrigger = !props.multiple && selectedOption?.triggerLabel !== undefined
312
308
 
313
309
  return (
314
- <Popover open={open} onOpenChange={setOpen}>
310
+ <Popover
311
+ open={open}
312
+ onOpenChange={(v) => {
313
+ // Fechar devolve o foco pro gatilho, e o gatilho abre AO RECEBER FOCO — sem
314
+ // guarda, o seletor reabre sozinho ("abre, fecha e abre de novo").
315
+ //
316
+ // A guarda vale UM FRAME, não até ser consumida. Fechar clicando noutro lugar da
317
+ // página não devolve o foco pra cá, então uma trava que espera esse retorno ficaria
318
+ // armada pra sempre e engoliria a PRÓXIMA abertura por teclado — trocando um
319
+ // defeito visível por um que só aparece pra quem navega por Tab.
320
+ if (!v) {
321
+ closingRef.current = true
322
+ requestAnimationFrame(() => {
323
+ closingRef.current = false
324
+ })
325
+ }
326
+ setOpen(v)
327
+ }}
328
+ >
315
329
  <div ref={rootRef} data-slot="select" className={cn(ghost ? 'min-w-0' : 'min-w-40', className)}>
316
330
  <CommandPrimitive data-slot="select-command" shouldFilter={canSearch && onSearch === undefined} className="overflow-visible">
317
331
  <PopoverAnchor asChild>
@@ -361,13 +375,16 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
361
375
  </span>
362
376
  )
363
377
  })}
364
- {/* Input somente-leitura em vez de `<span>`: ele carrega o `id`, e é o que
365
- faz o `htmlFor` do Label achar um campo (clicar no rótulo foca e abre).
366
- Um `<div>` não é rotulável, e um `<button>` não pode conter os botões de
367
- remover chip do modo múltiplo. */}
368
- {customTrigger && (
369
- <span data-slot="select-trigger-label" className="min-w-0 truncate text-sm">
370
- {selectedOption?.triggerLabel}
378
+ {/* O rótulo é SEMPRE um `<span>`. Quando ele morava no `value` de um input,
379
+ o texto rolava pro fim ao ganhar foco (o browser leva o cursor pro final)
380
+ e o começo do título sumia num campo estreito, a pessoa via o meio de
381
+ uma frase. `<span>` trunca com reticências, que é o certo. */}
382
+ {/* No MÚLTIPLO os chips ocupam o lugar do rótulo — mas quando não há nenhum,
383
+ o gatilho ficaria em branco (o input que carregava o placeholder virou
384
+ invisível). Campo mudo é pior que campo vazio: não diz nem o que é. */}
385
+ {(!props.multiple || selectedValues.length === 0) && (
386
+ <span data-slot="select-trigger-label" className={cn('min-w-0 flex-1 truncate text-left text-sm', selectedOption === undefined && 'text-muted-foreground')}>
387
+ {props.multiple ? placeholder : (selectedOption?.triggerLabel ?? selectedOption?.label ?? placeholder)}
371
388
  </span>
372
389
  )}
373
390
  <input
@@ -404,10 +421,7 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
404
421
  }
405
422
  }}
406
423
  placeholder={props.multiple && selectedValues.length > 0 ? '' : placeholder}
407
- className={cn(
408
- 'min-w-0 flex-1 cursor-pointer bg-transparent text-sm caret-transparent outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed',
409
- customTrigger && 'sr-only',
410
- )}
424
+ className="sr-only"
411
425
  />
412
426
  {clearable === true && selectedValues.length > 0 && disabled !== true && (
413
427
  <button
@@ -461,11 +475,16 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
461
475
  'pointer-events-auto overflow-hidden p-0',
462
476
  ghost ? 'w-auto min-w-56 max-w-80' : 'w-(--radix-popover-trigger-width) min-w-40',
463
477
  )}
464
- // Com busca, o foco VAI pra caixa dentro da lista: abrir e já poder digitar é
465
- // o ponto do padrão. Sem busca, segura o foco no gatilho (nada pra digitar).
466
- onOpenAutoFocus={(e) => {
467
- if (!canSearch) e.preventDefault()
468
- }}
478
+ /*
479
+ * O foco ao abrir é SEMPRE nosso o Radix nunca mexe.
480
+ *
481
+ * Deixá-lo agir com busca parecia certo (ele focaria o primeiro elemento
482
+ * focável, que é a caixa), mas ele roda DEPOIS do nosso efeito e movia o foco
483
+ * pro contêiner do conteúdo: a busca ganhava o cursor e o perdia em seguida,
484
+ * que é exatamente o "ganha foco e some" relatado. Com dois donos do foco, o
485
+ * último a escrever vence — e quem é o último depende da ordem de montagem.
486
+ */
487
+ onOpenAutoFocus={(e) => e.preventDefault()}
469
488
  onCloseAutoFocus={(e) => e.preventDefault()}
470
489
  onInteractOutside={(e) => {
471
490
  const target = e.target as HTMLElement