@softize/opus 13.1.0 → 14.0.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/CHANGELOG.md +34 -0
- package/bin/lib/copy.mjs +276 -6
- package/docs/code-style.md +4 -1
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -3
- package/registry/templates/app/src/App.tsx +11 -6
- package/src/core/types.ts +3 -4
- package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
- package/src/ui/components/patterns/confirm.tsx +2 -31
- package/src/ui/components/patterns/content-header.tsx +42 -141
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/form.tsx +15 -15
- package/src/ui/components/patterns/list.tsx +14 -8
- package/src/ui/components/patterns/page-state.tsx +39 -51
- package/src/ui/components/patterns/page.tsx +18 -53
- package/src/ui/components/patterns/state-surface.tsx +148 -0
- package/src/ui/components/patterns/surface-header.tsx +119 -0
- package/src/ui/components/patterns/trigger.tsx +7 -9
- package/src/ui/components/patterns/view.tsx +14 -16
- package/src/ui/components/primitives/alert.tsx +1 -33
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +2 -43
- package/src/ui/components/primitives/button.tsx +31 -35
- package/src/ui/components/primitives/control.ts +60 -0
- package/src/ui/components/primitives/dot.tsx +1 -30
- package/src/ui/components/primitives/input-group.tsx +11 -8
- package/src/ui/components/primitives/item.tsx +3 -1
- package/src/ui/components/primitives/menu.tsx +1 -7
- package/src/ui/components/primitives/pagination.tsx +16 -8
- package/src/ui/components/primitives/select.tsx +2 -2
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +4 -1
- package/src/ui/components/primitives/tabs.tsx +5 -3
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/docs/content/action-form.md +26 -0
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +3 -1
- package/src/ui/docs/content/action-trigger.md +4 -4
- package/src/ui/docs/content/action-view.md +3 -2
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +30 -14
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/content.md +5 -4
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/page.md +12 -4
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/semantic-context.md +3 -2
- package/src/ui/docs/content/sidebar.md +2 -42
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/drivers/react.tsx +1 -6
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +8 -16
- package/src/ui/components/patterns/shell-nav.tsx +0 -154
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
## Carregamento sem progresso conhecido
|
|
2
2
|
|
|
3
|
-
Use `Spinner` quando a duração ou o progresso da espera não forem conhecidos. `
|
|
4
|
-
|
|
3
|
+
Use `Spinner` quando a duração ou o progresso da espera não forem conhecidos. `size` usa os nomes da
|
|
4
|
+
escala dos controles e mede o glifo do controle homônimo: `sm` é o ícone de um `Button` `sm`, `lg` o
|
|
5
|
+
de um `lg`. O ícone é decorativo; quem nomeia a espera é o contêiner (`role="status"`, como fazem
|
|
6
|
+
`DataState` e `PageState`) ou o texto ao lado.
|
|
5
7
|
|
|
6
8
|
```tsx preview
|
|
9
|
+
<Spinner size="xs" />
|
|
7
10
|
<Spinner size="sm" />
|
|
8
11
|
<Spinner />
|
|
9
12
|
<Spinner size="lg" />
|
|
@@ -20,11 +23,11 @@ andamento.
|
|
|
20
23
|
|
|
21
24
|
## Em carga de conteúdo
|
|
22
25
|
|
|
23
|
-
Espera curta sem forma definida
|
|
24
|
-
Skeleton.
|
|
26
|
+
Espera curta sem forma definida; o contêiner nomeia o estado. Para uma consulta inteira, `DataState`
|
|
27
|
+
já monta esse contêiner. Quando o conteúdo tem forma conhecida (lista, card), prefira Skeleton.
|
|
25
28
|
|
|
26
29
|
```tsx preview
|
|
27
|
-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
30
|
+
<div role="status" className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
28
31
|
<Spinner />
|
|
29
32
|
<span>Carregando sessões…</span>
|
|
30
33
|
</div>
|
|
@@ -34,4 +37,4 @@ Skeleton.
|
|
|
34
37
|
|
|
35
38
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
36
39
|
|---|---|---|---|
|
|
37
|
-
| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | O
|
|
40
|
+
| `size` | `'xs' \| 'sm' \| 'default' \| 'lg'` | `'default'` | O glifo do controle homônimo na escala única (0.875 · 0.875 · 1 · 1.25rem). |
|
|
@@ -66,5 +66,5 @@ disabled esmaece e bloqueia a chave — ligada ou desligada — e o rótulo em p
|
|
|
66
66
|
| `checked` | `boolean` | | O estado, no modo controlado — parear com onCheckedChange. |
|
|
67
67
|
| `onCheckedChange` | `(checked: boolean) => void` | | Chamado a cada alternância, com o novo estado. |
|
|
68
68
|
| `defaultChecked` | `boolean` | `false` | Estado inicial no modo não controlado. |
|
|
69
|
-
| `size` | `'sm' \| 'default'` | `'default'` | Tamanho da chave — sm para densidade em linha de lista. |
|
|
69
|
+
| `size` | `'sm' \| 'default'` | `'default'` | Tamanho da chave, com os nomes da escala única — sm para densidade em linha de lista. |
|
|
70
70
|
| `disabled` | `boolean` | `false` | Esmaece e bloqueia — o Label em par esmaece junto (peer-disabled). |
|
|
@@ -92,7 +92,7 @@ lateral do gatilho.
|
|
|
92
92
|
| `defaultValue` | `string` | | Aba inicial no modo não controlado. |
|
|
93
93
|
| `value` | `string` | | Aba ativa no modo controlado. Use com `onValueChange`. |
|
|
94
94
|
| `onValueChange` | `(value: string) => void` | | Chamado quando a pessoa seleciona outra aba. |
|
|
95
|
-
| `size` | `'default' \| 'sm'` | `'default'` |
|
|
95
|
+
| `size` | `'default' \| 'sm'` | `'default'` | Altura da lista na escala única dos controles (2.25 e 2rem), aplicada em `TabsList`. |
|
|
96
96
|
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção da lista de abas. |
|
|
97
97
|
|
|
98
98
|
## Propriedades de TabsList
|
|
@@ -70,7 +70,7 @@ disabled esmaece e bloqueia o clique — o estado pressed permanece visível.
|
|
|
70
70
|
| `onPressedChange` | `(pressed: boolean) => void` | | Chamado a cada alternância, com o novo estado. |
|
|
71
71
|
| `defaultPressed` | `boolean` | `false` | Estado inicial no modo não controlado. |
|
|
72
72
|
| `variant` | `'default' \| 'outline'` | `'default'` | default não tem borda (fundo só quando ativo); outline carrega a borda. |
|
|
73
|
-
| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Altura
|
|
73
|
+
| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Altura na escala única dos controles (2 · 2.25 · 2.5rem) — sm para toolbar densa, lg para alvo mais confortável. |
|
|
74
74
|
| `disabled` | `boolean` | `false` | Esmaece e bloqueia o clique, preservando o estado visual. |
|
|
75
75
|
|
|
76
76
|
## ToggleGroup
|
package/src/ui/drivers/react.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @softize/opus/ui/react — React driver
|
|
3
3
|
*
|
|
4
4
|
* Hooks + Provider pra invocar actions client-side.
|
|
5
|
-
* - `OpusProvider` — injeta ClientAdapter no contexto
|
|
5
|
+
* - `OpusProvider` — injeta ClientAdapter no contexto
|
|
6
6
|
* - `useAction(action)` — invoca actions simple/form/view
|
|
7
7
|
* - `useLookupAction(action)` — variante pra search (output Paginated)
|
|
8
8
|
*
|
|
@@ -83,11 +83,6 @@ export function OpusProvider({
|
|
|
83
83
|
return <OpusContext.Provider value={value}>{children}</OpusContext.Provider>
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/** @deprecated Renomeado para `OpusProvider`; o alias segue até a próxima série. */
|
|
87
|
-
export const TbdlibProvider = OpusProvider
|
|
88
|
-
/** @deprecated Use `OpusProviderProps`. */
|
|
89
|
-
export type TbdlibProviderProps = OpusProviderProps
|
|
90
|
-
|
|
91
86
|
function useClient(): ClientAdapter {
|
|
92
87
|
const ctx = useContext(OpusContext)
|
|
93
88
|
if (ctx === null) {
|
package/src/ui/meta.ts
CHANGED
|
@@ -69,7 +69,7 @@ export const componentMeta = {
|
|
|
69
69
|
name: "button",
|
|
70
70
|
ancestry: "shadcn",
|
|
71
71
|
whenToUse:
|
|
72
|
-
"Inicie uma ação com um controle clicável. Use `context` para o significado, `variant` para o tratamento visual e `size` para a escala; `busy` comunica o andamento e impede um novo acionamento. Para ações relacionadas, use ButtonGroup.",
|
|
72
|
+
"Inicie uma ação com um controle clicável. Use `context` para o significado, `variant` para o tratamento visual e `size` para a escala compartilhada (`xs`…`lg` e os quadrados `icon-*`); `icon` recebe um nó e `busy` comunica o andamento e impede um novo acionamento. Para ações relacionadas, use ButtonGroup.",
|
|
73
73
|
},
|
|
74
74
|
card: {
|
|
75
75
|
name: "card",
|
|
@@ -117,7 +117,7 @@ export const componentMeta = {
|
|
|
117
117
|
name: "content-header",
|
|
118
118
|
ancestry: "opus",
|
|
119
119
|
whenToUse:
|
|
120
|
-
"Estruture o cabeçalho de um Content com título,
|
|
120
|
+
"Estruture o cabeçalho de um Content com título, contador, descrição e ações — a mesma anatomia de PageHeader. No caso comum, declare esses valores diretamente em Content; componha ContentHeader apenas quando precisar controlar a anatomia.",
|
|
121
121
|
},
|
|
122
122
|
copyable: {
|
|
123
123
|
name: "copyable",
|
|
@@ -183,7 +183,7 @@ export const componentMeta = {
|
|
|
183
183
|
name: "spinner",
|
|
184
184
|
ancestry: "shadcn",
|
|
185
185
|
whenToUse:
|
|
186
|
-
"Indique uma espera sem progresso determinado, como uma ação ou consulta em andamento. Para reservar a forma do conteúdo, use Skeleton.",
|
|
186
|
+
"Indique uma espera sem progresso determinado, como uma ação ou consulta em andamento. O ícone é decorativo: o contêiner (`role=\"status\"`) ou o texto ao lado nomeia a espera. Para reservar a forma do conteúdo, use Skeleton.",
|
|
187
187
|
},
|
|
188
188
|
table: {
|
|
189
189
|
name: "table",
|
|
@@ -327,7 +327,7 @@ export const componentMeta = {
|
|
|
327
327
|
name: "sidebar",
|
|
328
328
|
ancestry: "opus",
|
|
329
329
|
whenToUse:
|
|
330
|
-
"Organize navegação global ou contextual em uma coluna lateral. Split e Pane definem posição e largura; Sidebar fornece a superfície e o colapso, enquanto PaneHeader, PaneBody e PaneFooter estruturam as regiões fixa e rolável. Use SidebarNav para grupos planos
|
|
330
|
+
"Organize navegação global ou contextual em uma coluna lateral. Split e Pane definem posição e largura; Sidebar fornece a superfície e o colapso, enquanto PaneHeader, PaneBody e PaneFooter estruturam as regiões fixa e rolável. Use SidebarNav para grupos planos e componha árvores com SidebarItem e SidebarTreeGroup; cabeçalho e rodapé próprios ficam em PaneHeader e PaneFooter.",
|
|
331
331
|
},
|
|
332
332
|
"scroll-area": {
|
|
333
333
|
name: "scroll-area",
|
|
@@ -412,7 +412,7 @@ export const componentMeta = {
|
|
|
412
412
|
name: "data-state",
|
|
413
413
|
ancestry: "opus",
|
|
414
414
|
whenToUse:
|
|
415
|
-
"Coordene carregamento, erro, vazio e conteúdo de uma consulta assíncrona
|
|
415
|
+
"Coordene carregamento, erro, vazio e conteúdo de uma consulta assíncrona com as mesmas superfícies de PageState: `emptyMessage` compõe Empty com moldura sólida, o erro é um Alert com `onRetry` e `retryLabel`. Use DataState dentro da estrutura que receberá os dados; para uma região disponível à criação, use Empty. Ações em andamento pertencem ao estado `busy` do controle que as iniciou.",
|
|
416
416
|
},
|
|
417
417
|
"action-list-dialog": {
|
|
418
418
|
name: "action-list-dialog",
|
package/src/ui/react.tsx
CHANGED
|
@@ -18,7 +18,12 @@ export type {
|
|
|
18
18
|
ButtonVariant,
|
|
19
19
|
ButtonSize,
|
|
20
20
|
} from "./components/primitives/button.tsx";
|
|
21
|
-
export type {
|
|
21
|
+
export type {
|
|
22
|
+
ControlShape,
|
|
23
|
+
ControlSize,
|
|
24
|
+
ControlIconSize,
|
|
25
|
+
ControlScale,
|
|
26
|
+
} from "./components/primitives/control.ts";
|
|
22
27
|
|
|
23
28
|
export { Input } from "./components/primitives/input.tsx";
|
|
24
29
|
|
|
@@ -358,6 +363,7 @@ export { useOverflowing } from "./lib/overflow.ts";
|
|
|
358
363
|
export {
|
|
359
364
|
ActionForm,
|
|
360
365
|
ActionFormField,
|
|
366
|
+
LabelHelp,
|
|
361
367
|
useActionFormContext,
|
|
362
368
|
} from "./components/patterns/form.tsx";
|
|
363
369
|
export type {
|
|
@@ -395,12 +401,7 @@ export { ActionView } from "./components/patterns/view.tsx";
|
|
|
395
401
|
export type { ActionViewProps } from "./components/patterns/view.tsx";
|
|
396
402
|
|
|
397
403
|
export { ActionTrigger } from "./components/patterns/trigger.tsx";
|
|
398
|
-
export {
|
|
399
|
-
dialog,
|
|
400
|
-
DialogHost,
|
|
401
|
-
confirm,
|
|
402
|
-
ConfirmHost,
|
|
403
|
-
} from "./components/patterns/confirm.tsx";
|
|
404
|
+
export { dialog, DialogHost } from "./components/patterns/confirm.tsx";
|
|
404
405
|
export type {
|
|
405
406
|
AlertOptions,
|
|
406
407
|
ChooseOptions,
|
|
@@ -499,15 +500,6 @@ export type {
|
|
|
499
500
|
SidebarNavItem,
|
|
500
501
|
} from "./components/patterns/sidebar.tsx";
|
|
501
502
|
|
|
502
|
-
// O menu como primitivo pivotável: recolhe quando composto dentro de Sidebar.
|
|
503
|
-
export { ShellNav, ShellNavHeading } from "./components/patterns/shell-nav.tsx";
|
|
504
|
-
export type {
|
|
505
|
-
ShellNavProps,
|
|
506
|
-
ShellNavGroup,
|
|
507
|
-
ShellNavItem,
|
|
508
|
-
ShellNavHeadingProps,
|
|
509
|
-
} from "./components/patterns/shell-nav.tsx";
|
|
510
|
-
|
|
511
503
|
// Roteamento history-based (o pathname É o estado). Escopo pequeno de propósito: leitura
|
|
512
504
|
// reativa da URL + navigate — sem tabela de rotas. Ver src/ui/router.ts.
|
|
513
505
|
export {
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* <ShellNav /> — o menu como primitivo PIVOTÁVEL. Um nav (grupos → itens, com heading e
|
|
3
|
-
* âncora inferior) que serve os DOIS lares do chrome sem flag:
|
|
4
|
-
*
|
|
5
|
-
* - dentro de <Sidebar> → recolhe pra ícone-só (com tooltip) junto com a coluna;
|
|
6
|
-
* - fora de <Sidebar> → permanece expandido.
|
|
7
|
-
*
|
|
8
|
-
* "Tanto faz onde": o componente se adapta ao lugar, sem "modo" configurado.
|
|
9
|
-
*
|
|
10
|
-
* Presentacional e CONTROLADO — o app é dono do `activeId`, do clique e do roteamento.
|
|
11
|
-
* v1 PLANA (grupos → itens); aninhamento (accordion/pasta) e DnD COMPOSTO entram quando o
|
|
12
|
-
* primeiro consumidor de árvore migrar — ver `docs/shellnav.md`.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { ReactNode } from 'react'
|
|
16
|
-
import { cn } from '../../lib/cn.ts'
|
|
17
|
-
import { focusRing } from '../primitives/control.ts'
|
|
18
|
-
import { useSidebarCollapsed } from './sidebar.tsx'
|
|
19
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip.tsx'
|
|
20
|
-
|
|
21
|
-
/** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
|
|
22
|
-
export interface ShellNavItem {
|
|
23
|
-
/** Coordenada estável — volta em `onSelect` e casa com `activeId`. Único em toda a nav. */
|
|
24
|
-
id: string
|
|
25
|
-
label: string
|
|
26
|
-
/** Ícone à esquerda (é o que sobra no modo recolhido — vira o alvo, com o label no tooltip). */
|
|
27
|
-
icon?: ReactNode
|
|
28
|
-
/** Selo à direita (contagem, `beta`). Some no recolhido. */
|
|
29
|
-
badge?: ReactNode
|
|
30
|
-
/** Visível mas inerte. */
|
|
31
|
-
disabled?: boolean
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
|
|
35
|
-
export interface ShellNavGroup {
|
|
36
|
-
/** Cabeçalho do grupo. Ausente/vazio = itens soltos, sem rótulo. Some no recolhido. */
|
|
37
|
-
label?: string
|
|
38
|
-
items: ShellNavItem[]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
|
|
42
|
-
export interface ShellNavProps {
|
|
43
|
-
groups: ShellNavGroup[]
|
|
44
|
-
/** `id` do item ativo. Ausente = nenhum destacado. */
|
|
45
|
-
activeId?: string
|
|
46
|
-
onSelect: (id: string) => void
|
|
47
|
-
/** Topo do nav — título, busca, botão (ex.: o "+" do Relatórios). Some no recolhido. Ver <ShellNavHeading>. */
|
|
48
|
-
heading?: ReactNode
|
|
49
|
-
/** Grupos ANCORADOS embaixo (ex.: Configurações no app), mesmo renderer, separados por filete. */
|
|
50
|
-
footer?: ShellNavGroup[]
|
|
51
|
-
/** Rótulo da landmark `<nav>`. */
|
|
52
|
-
navLabel?: string
|
|
53
|
-
/** Classes da raiz (a coluna). A largura vem do pane/sidebar que a contém. */
|
|
54
|
-
className?: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Rótulo só existe se tiver texto — vazio não vira cabeçalho fantasma. */
|
|
58
|
-
const titled = (label?: string): boolean => (label ?? '').trim() !== ''
|
|
59
|
-
|
|
60
|
-
/** Selo vazio não vira pílula sem texto. `0` conta (contagem legítima). */
|
|
61
|
-
const shown = (badge: ReactNode): boolean => {
|
|
62
|
-
if (badge === undefined || badge === null || badge === false || badge === '') return false
|
|
63
|
-
if (typeof badge === 'number' && Number.isNaN(badge)) return false
|
|
64
|
-
return !(Array.isArray(badge) && badge.length === 0)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Grupo sem item não abre buraco no `space-y` nem deixa rótulo pairando sobre nada. */
|
|
68
|
-
const filled = (group: ShellNavGroup): boolean => group.items.length > 0
|
|
69
|
-
|
|
70
|
-
function renderItem(item: ShellNavItem, activeId: string | undefined, onSelect: (id: string) => void, railed: boolean): React.ReactElement {
|
|
71
|
-
const active = item.id === activeId
|
|
72
|
-
const button = (
|
|
73
|
-
<button
|
|
74
|
-
type="button"
|
|
75
|
-
disabled={item.disabled}
|
|
76
|
-
aria-current={active ? 'page' : undefined}
|
|
77
|
-
onClick={() => onSelect(item.id)}
|
|
78
|
-
className={cn(
|
|
79
|
-
'flex w-full items-center gap-2.5 rounded-md py-1.5 text-left text-sm transition-colors',
|
|
80
|
-
'disabled:pointer-events-none disabled:opacity-40',
|
|
81
|
-
'outline-none',
|
|
82
|
-
focusRing,
|
|
83
|
-
railed ? 'justify-center px-0' : 'px-2.5',
|
|
84
|
-
active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60',
|
|
85
|
-
)}
|
|
86
|
-
>
|
|
87
|
-
{item.icon !== undefined && <span className="flex shrink-0">{item.icon}</span>}
|
|
88
|
-
{!railed && <span className="min-w-0 flex-1 truncate">{item.label}</span>}
|
|
89
|
-
{!railed && shown(item.badge) && <span className="shrink-0">{item.badge}</span>}
|
|
90
|
-
</button>
|
|
91
|
-
)
|
|
92
|
-
if (!railed) return <div key={item.id}>{button}</div>
|
|
93
|
-
// Recolhido: o label migra pro tooltip (à direita) — sem ele, a coluna vira ícones adivinháveis.
|
|
94
|
-
return (
|
|
95
|
-
<Tooltip key={item.id}>
|
|
96
|
-
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
97
|
-
<TooltipContent side="right">{item.label}</TooltipContent>
|
|
98
|
-
</Tooltip>
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function renderGroup(group: ShellNavGroup, gi: number, activeId: string | undefined, onSelect: (id: string) => void, railed: boolean): React.ReactElement | null {
|
|
103
|
-
if (!filled(group)) return null
|
|
104
|
-
return (
|
|
105
|
-
<div key={gi} className="space-y-0.5">
|
|
106
|
-
{!railed && titled(group.label) && (
|
|
107
|
-
<div className="px-2.5 pb-1 text-xs font-medium text-muted-foreground/70">{group.label}</div>
|
|
108
|
-
)}
|
|
109
|
-
{group.items.map((item) => renderItem(item, activeId, onSelect, railed))}
|
|
110
|
-
</div>
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** @deprecated Use `SidebarNav`: mesma navegação, integrada à `Sidebar`. Mantido por compatibilidade. */
|
|
115
|
-
export function ShellNav({ groups, activeId, onSelect, heading, footer, navLabel = 'Navegação', className }: ShellNavProps): React.ReactElement {
|
|
116
|
-
const railed = useSidebarCollapsed()
|
|
117
|
-
|
|
118
|
-
const body = (
|
|
119
|
-
<div data-slot="shell-nav" className={cn('flex h-full min-h-0 flex-col', className)}>
|
|
120
|
-
{!railed && heading}
|
|
121
|
-
<nav aria-label={navLabel} className={cn('min-h-0 flex-1 space-y-4 overflow-y-auto p-2', railed && 'px-1.5')}>
|
|
122
|
-
{groups.map((group, gi) => renderGroup(group, gi, activeId, onSelect, railed))}
|
|
123
|
-
</nav>
|
|
124
|
-
{footer !== undefined && footer.some(filled) && (
|
|
125
|
-
<div className={cn('space-y-4 border-t border-border p-2', railed && 'px-1.5')}>
|
|
126
|
-
{footer.map((group, gi) => renderGroup(group, gi, activeId, onSelect, railed))}
|
|
127
|
-
</div>
|
|
128
|
-
)}
|
|
129
|
-
</div>
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
// Recolhido usa tooltips — garante um provider (nested é ok se o app já tem um).
|
|
133
|
-
return railed ? <TooltipProvider delayDuration={200}>{body}</TooltipProvider> : body
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
|
|
137
|
-
export interface ShellNavHeadingProps {
|
|
138
|
-
/** Título da seção. */
|
|
139
|
-
title: ReactNode
|
|
140
|
-
/** Ação à direita — em geral um botão (o "+" de criar). */
|
|
141
|
-
action?: ReactNode
|
|
142
|
-
className?: string
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** @deprecated Use `SidebarHeader` com `SidebarNav`. Cabeçalho do ShellNav: título + espaço
|
|
146
|
-
* pra um botão. Mantido por compatibilidade. */
|
|
147
|
-
export function ShellNavHeading({ title, action, className }: ShellNavHeadingProps): React.ReactElement {
|
|
148
|
-
return (
|
|
149
|
-
<div className={cn('flex h-12 shrink-0 items-center gap-1.5 border-b border-border px-4', className)}>
|
|
150
|
-
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{title}</span>
|
|
151
|
-
{action !== undefined && <span className="shrink-0">{action}</span>}
|
|
152
|
-
</div>
|
|
153
|
-
)
|
|
154
|
-
}
|