@softize/opus 12.5.0 → 12.5.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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 12.5.2 — 2026-08-24
|
|
11
|
+
|
|
12
|
+
`SidebarItem` aplica `className` sempre na linha, recolhida ou aberta. Antes a prop mudava de alvo
|
|
13
|
+
conforme o rail — ia para o botão no modo recolhido e para a linha no aberto —, e o consumidor não
|
|
14
|
+
tinha como saber o que estava estilizando.
|
|
15
|
+
|
|
16
|
+
O controle de desdobrar também deixa de depender de `icon`. Um item com `onToggle` e sem ícone
|
|
17
|
+
perdia o chevron em silêncio; agora ele ocupa sozinho o lugar do leading e fica visível, já que não
|
|
18
|
+
há ícone para revezar com ele no hover.
|
|
19
|
+
|
|
20
|
+
## 12.5.1 — 2026-08-24
|
|
21
|
+
|
|
22
|
+
`Dock` passa a fazer o roving tabindex que uma `toolbar` promete: a barra tem um único ponto de
|
|
23
|
+
entrada no Tab, e as setas andam entre as ações a partir dele. Antes as setas eram um caminho a
|
|
24
|
+
mais e a barra continuava cobrando um Tab por ícone, que é justamente o que o padrão evita.
|
|
25
|
+
|
|
26
|
+
A mudança é interna ao componente e não altera a API.
|
|
27
|
+
|
|
10
28
|
## 12.5.0 — 2026-08-24
|
|
11
29
|
|
|
12
30
|
Cache vira porta do protocolo. `CacheAdapter` chega aos handlers como `ctx.cache`, com a superfície
|
package/package.json
CHANGED
|
@@ -36,6 +36,18 @@ export interface DockProps {
|
|
|
36
36
|
export function Dock({ position = 'bottom', label, className, children }: DockProps): React.ReactElement {
|
|
37
37
|
const ref = React.useRef<HTMLDivElement>(null)
|
|
38
38
|
|
|
39
|
+
// Roving tabindex: uma toolbar tem UM ponto de entrada no Tab; dentro dela, as setas andam.
|
|
40
|
+
// Sem isto as setas seriam um caminho a mais, e a barra continuaria cobrando um Tab por ícone.
|
|
41
|
+
const roving = React.useCallback((focado?: HTMLElement) => {
|
|
42
|
+
const items = Array.from(ref.current?.querySelectorAll<HTMLButtonElement>('button') ?? [])
|
|
43
|
+
const entrada = items.find((item) => item === focado) ?? items.find((item) => !item.disabled)
|
|
44
|
+
for (const item of items) item.tabIndex = item === entrada ? 0 : -1
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
roving()
|
|
49
|
+
})
|
|
50
|
+
|
|
39
51
|
// Navegação de toolbar: as setas andam entre as ações, e Home/End vão às pontas. Sem isso
|
|
40
52
|
// uma barra com dez ícones cobra dez Tabs de quem navega por teclado.
|
|
41
53
|
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
|
@@ -54,7 +66,10 @@ export function Dock({ position = 'bottom', label, className, children }: DockPr
|
|
|
54
66
|
: event.key === 'ArrowRight'
|
|
55
67
|
? (current + 1) % items.length
|
|
56
68
|
: (current - 1 + items.length) % items.length
|
|
57
|
-
items[next]
|
|
69
|
+
const alvo = items[next]
|
|
70
|
+
if (alvo === undefined) return
|
|
71
|
+
roving(alvo)
|
|
72
|
+
alvo.focus()
|
|
58
73
|
}
|
|
59
74
|
|
|
60
75
|
return (
|
|
@@ -65,6 +80,7 @@ export function Dock({ position = 'bottom', label, className, children }: DockPr
|
|
|
65
80
|
aria-label={label}
|
|
66
81
|
aria-orientation="horizontal"
|
|
67
82
|
onKeyDown={onKeyDown}
|
|
83
|
+
onFocus={(event) => roving(event.target as HTMLElement)}
|
|
68
84
|
className={cn(
|
|
69
85
|
'absolute z-10 flex max-w-[calc(100%-1.5rem)] flex-row items-center gap-1.5 overflow-x-auto',
|
|
70
86
|
'rounded-2xl border border-border bg-background/95 p-1.5 shadow-lg backdrop-blur',
|
|
@@ -76,28 +76,31 @@ export function SidebarItem({ label, icon, badge, active = false, disabled = fal
|
|
|
76
76
|
const { ref: labelRef, overflowing: labelCut } = useOverflowing<HTMLSpanElement>(label)
|
|
77
77
|
if (collapsed) {
|
|
78
78
|
const button = (
|
|
79
|
-
<button type="button" disabled={disabled} aria-current={active ? 'page' : undefined} onClick={onClick} className={cn('mx-auto grid size-9 place-items-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-40', 'outline-none focus-visible:ring-2 focus-visible:ring-ring/50', active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60'
|
|
79
|
+
<button type="button" disabled={disabled} aria-current={active ? 'page' : undefined} onClick={onClick} className={cn('mx-auto grid size-9 place-items-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-40', 'outline-none focus-visible:ring-2 focus-visible:ring-ring/50', active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60')}>
|
|
80
80
|
{icon}
|
|
81
81
|
</button>
|
|
82
82
|
)
|
|
83
|
-
|
|
83
|
+
// `className` vai no MESMO elemento nos dois modos — a linha —, senão a prop mudaria de alvo
|
|
84
|
+
// conforme o rail estivesse recolhido, e o consumidor não teria como saber o que estilizou.
|
|
85
|
+
return <div data-slot="sidebar-item" className={className} {...dragProps}><Tooltip><TooltipTrigger asChild>{button}</TooltipTrigger><TooltipContent side="right">{label}{tooltipHint}</TooltipContent></Tooltip></div>
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
return (
|
|
87
89
|
<div data-slot="sidebar-item" className={cn('group/sidebar-item relative flex items-center rounded-md transition-colors', disabled ? 'text-muted-foreground/50 opacity-40' : 'hover:bg-muted/60', active && 'bg-muted', dropPosition === 'inside' && 'bg-primary/5 ring-2 ring-inset ring-primary/60', className)} {...dragProps}>
|
|
88
90
|
{(dropPosition === 'before' || dropPosition === 'after') && <span data-slot="sidebar-item-drop-indicator" className={cn('pointer-events-none absolute inset-x-1 z-20 h-0.5 rounded-full bg-primary before:absolute before:-left-1 before:top-1/2 before:size-2 before:-translate-y-1/2 before:rounded-full before:border-2 before:border-primary before:bg-background', dropPosition === 'before' ? '-top-px' : '-bottom-px')} />}
|
|
89
|
-
{icon !== undefined && (
|
|
91
|
+
{(icon !== undefined || onToggle !== undefined) && (
|
|
90
92
|
<span data-slot="sidebar-item-leading" className="pointer-events-none absolute left-2.5 top-1/2 z-10 grid size-4 -translate-y-1/2 place-items-center">
|
|
91
|
-
|
|
93
|
+
{/* Sem ícone, o chevron não se esconde: ele é o único ocupante do lugar. */}
|
|
94
|
+
{icon !== undefined && <span className={cn('absolute inset-0 flex items-center justify-center transition-opacity', onToggle !== undefined && 'group-hover/sidebar-item:opacity-0 group-has-[[data-slot=sidebar-item-toggle]:focus-visible]/sidebar-item:opacity-0')}>{icon}</span>}
|
|
92
95
|
{onToggle !== undefined && (
|
|
93
|
-
<button type="button" data-slot="sidebar-item-toggle" onClick={onToggle} aria-label={expanded ? `Fechar ${label}` : `Abrir ${label}`} aria-expanded={expanded} className=
|
|
96
|
+
<button type="button" data-slot="sidebar-item-toggle" onClick={onToggle} aria-label={expanded ? `Fechar ${label}` : `Abrir ${label}`} aria-expanded={expanded} className={cn('absolute inset-0 grid place-items-center rounded-sm text-muted-foreground transition-[color,opacity] outline-none hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/50', icon === undefined ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0 group-hover/sidebar-item:pointer-events-auto group-hover/sidebar-item:opacity-100')}>
|
|
94
97
|
<ChevronRight className={cn('size-3 transition-transform', expanded && 'rotate-90')} />
|
|
95
98
|
</button>
|
|
96
99
|
)}
|
|
97
100
|
</span>
|
|
98
101
|
)}
|
|
99
102
|
<button type="button" disabled={disabled} aria-current={active ? 'page' : undefined} onClick={onClick} className={cn('flex min-w-0 flex-1 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-sm disabled:pointer-events-none', 'outline-none focus-visible:ring-2 focus-visible:ring-ring/50', disabled ? 'text-muted-foreground/50' : active ? 'font-medium text-foreground' : 'text-foreground/80')}>
|
|
100
|
-
{icon !== undefined && <span data-slot="sidebar-item-leading-space" aria-hidden className="size-4 shrink-0" />}
|
|
103
|
+
{(icon !== undefined || onToggle !== undefined) && <span data-slot="sidebar-item-leading-space" aria-hidden className="size-4 shrink-0" />}
|
|
101
104
|
<span ref={labelRef} data-slot="sidebar-item-label" className={cn('min-w-0 flex-1 overflow-hidden whitespace-nowrap group-has-[[data-slot=dot]]/sidebar-item:mr-[1.1875rem]', labelCut && 'truncate-fade')}>{label}</span>
|
|
102
105
|
{badge !== undefined && <span data-slot="sidebar-item-badge" className={cn('shrink-0 transition-opacity has-[[data-slot=dot]]:absolute has-[[data-slot=dot]]:right-1 has-[[data-slot=dot]]:top-1/2 has-[[data-slot=dot]]:z-10 has-[[data-slot=dot]]:grid has-[[data-slot=dot]]:size-6 has-[[data-slot=dot]]:-translate-y-1/2 has-[[data-slot=dot]]:place-items-center', hasActions && 'group-hover/sidebar-item:has-[[data-slot=dot]]:opacity-0')}>{badge}</span>}
|
|
103
106
|
</button>
|