@softize/opus 18.1.1 → 18.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/CHANGELOG.md +35 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +4 -3
- package/src/core/errors.ts +0 -1
- package/src/core/presentation.ts +143 -58
- package/src/core/types.ts +4 -0
- package/src/ui/components/patterns/data-state.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +606 -251
- package/src/ui/components/patterns/page.tsx +16 -5
- package/src/ui/components/patterns/presentation.tsx +228 -63
- package/src/ui/components/patterns/split.tsx +5 -2
- package/src/ui/components/patterns/surface-assistant.tsx +74 -0
- package/src/ui/components/primitives/field.tsx +23 -2
- package/src/ui/components/primitives/metric-card.tsx +64 -5
- package/src/ui/components/primitives/select.tsx +27 -23
- package/src/ui/components/primitives/table.tsx +4 -1
- package/src/ui/docs/content/action-list.md +84 -46
- package/src/ui/docs/content/field.md +6 -1
- package/src/ui/docs/content/metric-card.md +43 -0
- package/src/ui/docs/content/presentation.md +40 -14
- package/src/ui/docs/content/select.md +4 -0
- package/src/ui/docs/content/split.md +80 -19
- package/src/ui/meta.ts +2 -2
- package/src/ui/react.tsx +6 -0
|
@@ -22,20 +22,41 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
|
22
22
|
function FieldLegend({
|
|
23
23
|
className,
|
|
24
24
|
variant = "legend",
|
|
25
|
+
icon,
|
|
26
|
+
children,
|
|
25
27
|
...props
|
|
26
|
-
}: React.ComponentProps<"legend"> & {
|
|
28
|
+
}: React.ComponentProps<"legend"> & {
|
|
29
|
+
variant?: "legend" | "label" | "section"
|
|
30
|
+
icon?: React.ReactNode
|
|
31
|
+
}) {
|
|
27
32
|
return (
|
|
28
33
|
<legend
|
|
29
34
|
data-slot="field-legend"
|
|
30
35
|
data-variant={variant}
|
|
31
36
|
className={cn(
|
|
32
37
|
"mb-3 font-medium",
|
|
38
|
+
icon !== undefined && "flex items-center gap-2",
|
|
33
39
|
"data-[variant=legend]:text-base",
|
|
34
40
|
"data-[variant=label]:text-sm",
|
|
41
|
+
"data-[variant=section]:w-full data-[variant=section]:border-b data-[variant=section]:border-border/70 data-[variant=section]:pb-2 data-[variant=section]:text-sm data-[variant=section]:font-semibold",
|
|
35
42
|
className
|
|
36
43
|
)}
|
|
37
44
|
{...props}
|
|
38
|
-
|
|
45
|
+
>
|
|
46
|
+
{icon !== undefined ? (
|
|
47
|
+
<span
|
|
48
|
+
data-slot="field-legend-icon"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
className={cn(
|
|
51
|
+
"inline-flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4",
|
|
52
|
+
variant === "section" && "size-7 rounded-md bg-muted",
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{icon}
|
|
56
|
+
</span>
|
|
57
|
+
) : null}
|
|
58
|
+
{children}
|
|
59
|
+
</legend>
|
|
39
60
|
)
|
|
40
61
|
}
|
|
41
62
|
|
|
@@ -8,6 +8,15 @@ import { Skeleton } from './skeleton.tsx'
|
|
|
8
8
|
export type MetricCardTone = 'default' | 'warning'
|
|
9
9
|
export type MetricCardContext = Exclude<UiContext, 'primary'>
|
|
10
10
|
|
|
11
|
+
export interface MetricCardSecondaryMetric {
|
|
12
|
+
/** Nome curto da medida complementar. */
|
|
13
|
+
label: React.ReactNode
|
|
14
|
+
/** Valor complementar já formatado pelo consumidor. */
|
|
15
|
+
value: React.ReactNode
|
|
16
|
+
/** Controle complementar próprio, como uma explicação em tooltip. */
|
|
17
|
+
labelAction?: React.ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
const iconContextClasses: Record<MetricCardContext, string> = {
|
|
12
21
|
neutral: 'bg-context-neutral-subtle text-context-neutral-emphasis',
|
|
13
22
|
info: 'bg-context-info-subtle text-context-info-emphasis',
|
|
@@ -16,7 +25,7 @@ const iconContextClasses: Record<MetricCardContext, string> = {
|
|
|
16
25
|
danger: 'bg-context-danger-subtle text-context-danger-emphasis',
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'children'> {
|
|
28
|
+
interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'children' | 'asChild'> {
|
|
20
29
|
/** Controle complementar exibido ao lado do rótulo, como uma explicação em tooltip. */
|
|
21
30
|
labelAction?: React.ReactNode
|
|
22
31
|
/** Contexto que explica recorte, proporção ou significado do valor. */
|
|
@@ -29,6 +38,8 @@ interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'c
|
|
|
29
38
|
tone?: MetricCardTone
|
|
30
39
|
/** Ação relacionada diretamente à medida. */
|
|
31
40
|
action?: React.ReactNode
|
|
41
|
+
/** Medida complementar exibida como uma lâmina posterior sob o card principal. */
|
|
42
|
+
secondaryMetric?: MetricCardSecondaryMetric
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
export type MetricCardProps = MetricCardBaseProps &
|
|
@@ -63,6 +74,7 @@ export function MetricCard({
|
|
|
63
74
|
context,
|
|
64
75
|
tone,
|
|
65
76
|
action,
|
|
77
|
+
secondaryMetric,
|
|
66
78
|
loading = false,
|
|
67
79
|
className,
|
|
68
80
|
...props
|
|
@@ -73,12 +85,16 @@ export function MetricCard({
|
|
|
73
85
|
const hasIcon = icon !== undefined && icon !== null
|
|
74
86
|
const resolvedContext = context ?? (tone === 'warning' ? 'warning' : 'neutral')
|
|
75
87
|
|
|
76
|
-
|
|
88
|
+
const primaryCard = (
|
|
77
89
|
<Card
|
|
78
90
|
data-slot="metric-card"
|
|
79
|
-
aria-busy={loading || undefined}
|
|
80
|
-
className={cn(
|
|
81
|
-
|
|
91
|
+
aria-busy={secondaryMetric === undefined ? loading || undefined : undefined}
|
|
92
|
+
className={cn(
|
|
93
|
+
'min-w-0 p-4',
|
|
94
|
+
secondaryMetric !== undefined && 'relative z-10',
|
|
95
|
+
secondaryMetric === undefined && className,
|
|
96
|
+
)}
|
|
97
|
+
{...(secondaryMetric === undefined ? props : {})}
|
|
82
98
|
>
|
|
83
99
|
{loading ? (
|
|
84
100
|
<div className="space-y-3">
|
|
@@ -130,4 +146,47 @@ export function MetricCard({
|
|
|
130
146
|
)}
|
|
131
147
|
</Card>
|
|
132
148
|
)
|
|
149
|
+
|
|
150
|
+
if (secondaryMetric === undefined) return primaryCard
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
{...props}
|
|
155
|
+
data-slot="metric-card-stack"
|
|
156
|
+
aria-busy={loading || undefined}
|
|
157
|
+
className={cn('relative isolate min-w-0', className)}
|
|
158
|
+
>
|
|
159
|
+
{primaryCard}
|
|
160
|
+
<Card
|
|
161
|
+
data-slot="metric-card-secondary"
|
|
162
|
+
className="relative z-0 -mt-8 flex w-full min-w-0 items-center gap-1.5 bg-[color-mix(in_oklab,var(--card)_88%,black)] px-4 pt-9 pb-1"
|
|
163
|
+
>
|
|
164
|
+
{loading ? (
|
|
165
|
+
<Skeleton className="h-5 w-full" />
|
|
166
|
+
) : (
|
|
167
|
+
<>
|
|
168
|
+
<div
|
|
169
|
+
data-slot="metric-card-secondary-value"
|
|
170
|
+
className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums"
|
|
171
|
+
>
|
|
172
|
+
{secondaryMetric.value}
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
175
|
+
<div
|
|
176
|
+
data-slot="metric-card-secondary-label"
|
|
177
|
+
className="min-w-0 truncate text-xs font-medium text-muted-foreground"
|
|
178
|
+
>
|
|
179
|
+
{secondaryMetric.label}
|
|
180
|
+
</div>
|
|
181
|
+
{secondaryMetric.labelAction !== undefined && (
|
|
182
|
+
<div data-slot="metric-card-secondary-label-action" className="shrink-0">
|
|
183
|
+
{secondaryMetric.labelAction}
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
</>
|
|
188
|
+
)}
|
|
189
|
+
</Card>
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
133
192
|
}
|
|
@@ -27,6 +27,7 @@ import { Command as CommandPrimitive } from 'cmdk'
|
|
|
27
27
|
import { cn } from '../../lib/cn.ts'
|
|
28
28
|
import { focusRing, focusRingWithin, type ControlShape, type ControlSize } from './control.ts'
|
|
29
29
|
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from './command.tsx'
|
|
30
|
+
import { InputGroupButton } from './input-group.tsx'
|
|
30
31
|
import { Popover, PopoverAnchor, PopoverContent } from './popover.tsx'
|
|
31
32
|
|
|
32
33
|
export interface SelectOption {
|
|
@@ -451,25 +452,6 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
451
452
|
placeholder={props.multiple && selectedValues.length > 0 ? '' : placeholder}
|
|
452
453
|
className="sr-only"
|
|
453
454
|
/>
|
|
454
|
-
{clearable === true && selectedValues.length > 0 && disabled !== true && (
|
|
455
|
-
<button
|
|
456
|
-
type="button"
|
|
457
|
-
data-slot="select-clear"
|
|
458
|
-
aria-label="Limpar seleção"
|
|
459
|
-
onPointerDown={(e) => e.preventDefault()}
|
|
460
|
-
onClick={(e) => {
|
|
461
|
-
e.stopPropagation()
|
|
462
|
-
if (props.multiple) props.onChange([])
|
|
463
|
-
else {
|
|
464
|
-
props.onChange('')
|
|
465
|
-
setQuery('')
|
|
466
|
-
}
|
|
467
|
-
}}
|
|
468
|
-
className="flex shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
|
|
469
|
-
>
|
|
470
|
-
<X className="size-3.5" />
|
|
471
|
-
</button>
|
|
472
|
-
)}
|
|
473
455
|
{/* Ação custom (ex.: botão que age sobre o valor) — o clique NÃO abre a lista:
|
|
474
456
|
o slot para a propagação pro controle. */}
|
|
475
457
|
{trailing !== undefined && (
|
|
@@ -492,16 +474,38 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
492
474
|
affordance que diz isso. Antes ele sumia no buscável, porque ali o sinal
|
|
493
475
|
era o cursor de texto — que não existe mais no campo. */}
|
|
494
476
|
<ChevronDownIcon data-slot="select-icon" className={cn('size-4 shrink-0 opacity-50', ghost ? 'h-3.5 w-3.5 opacity-60' : '')} aria-hidden="true" />
|
|
477
|
+
{clearable === true && selectedValues.length > 0 && disabled !== true && (
|
|
478
|
+
<InputGroupButton
|
|
479
|
+
type="button"
|
|
480
|
+
size="icon-xs"
|
|
481
|
+
data-slot="select-clear"
|
|
482
|
+
aria-label="Limpar seleção"
|
|
483
|
+
onPointerDown={(e) => e.preventDefault()}
|
|
484
|
+
onClick={(e) => {
|
|
485
|
+
e.stopPropagation()
|
|
486
|
+
if (props.multiple) props.onChange([])
|
|
487
|
+
else {
|
|
488
|
+
props.onChange('')
|
|
489
|
+
setQuery('')
|
|
490
|
+
}
|
|
491
|
+
}}
|
|
492
|
+
className="-ml-1 -mr-1.5 text-muted-foreground hover:text-foreground"
|
|
493
|
+
>
|
|
494
|
+
<X className="size-3.5" />
|
|
495
|
+
</InputGroupButton>
|
|
496
|
+
)}
|
|
495
497
|
</div>
|
|
496
498
|
</PopoverAnchor>
|
|
497
499
|
<PopoverContent
|
|
498
500
|
align="start"
|
|
499
|
-
// No campo
|
|
500
|
-
//
|
|
501
|
-
//
|
|
501
|
+
// No campo comum a lista parte da largura do controle, mas pode crescer para
|
|
502
|
+
// preservar rótulos maiores sem alargar o gatilho. No ghost, o gatilho já abraça
|
|
503
|
+
// o conteúdo e a lista mantém sua faixa compacta própria.
|
|
502
504
|
className={cn(
|
|
503
505
|
'pointer-events-auto overflow-hidden p-0',
|
|
504
|
-
compact
|
|
506
|
+
compact
|
|
507
|
+
? 'w-auto min-w-56 max-w-80'
|
|
508
|
+
: 'w-max min-w-(--radix-popover-trigger-width) max-w-lg',
|
|
505
509
|
)}
|
|
506
510
|
/*
|
|
507
511
|
* O foco ao abrir é SEMPRE nosso — o Radix nunca mexe.
|
|
@@ -8,9 +8,11 @@ const TableVariantContext = React.createContext<TableVariant>('plain')
|
|
|
8
8
|
export interface TableProps extends React.ComponentProps<"table"> {
|
|
9
9
|
/** `framed` aplica a moldura canônica de datagrid no contêiner da tabela. */
|
|
10
10
|
variant?: TableVariant
|
|
11
|
+
/** Classes do contêiner responsável pela rolagem e pela moldura da tabela. */
|
|
12
|
+
containerClassName?: string
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
function Table({ className, variant = 'plain', ...props }: TableProps) {
|
|
15
|
+
function Table({ className, containerClassName, variant = 'plain', ...props }: TableProps) {
|
|
14
16
|
return (
|
|
15
17
|
<TableVariantContext.Provider value={variant}>
|
|
16
18
|
<div
|
|
@@ -19,6 +21,7 @@ function Table({ className, variant = 'plain', ...props }: TableProps) {
|
|
|
19
21
|
className={cn(
|
|
20
22
|
"relative w-full overflow-x-auto",
|
|
21
23
|
variant === 'framed' && 'rounded-lg border border-border',
|
|
24
|
+
containerClassName,
|
|
22
25
|
)}
|
|
23
26
|
>
|
|
24
27
|
<table
|
|
@@ -60,9 +60,24 @@ superfícies que não são listagens, como relatórios. O estado é controlado p
|
|
|
60
60
|
/>
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Seletores inline
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
Seletores inline de valor único abraçam o valor selecionado até `max-w-80`. Valores maiores são
|
|
64
|
+
truncados no controle, enquanto o dropdown pode crescer além dele para preservar a opção.
|
|
65
|
+
Multiseletores mantêm `w-52`; no modal de filtros avançados, o seletor ocupa toda a largura.
|
|
66
|
+
`filterIcons` associa um ícone leading ao nome do filtro quando a superfície precisa reforçar
|
|
67
|
+
dimensões recorrentes, sem substituir o rótulo. Busca, filtros e período aparecem somente quando
|
|
68
|
+
declarados.
|
|
69
|
+
|
|
70
|
+
Quando o valor de repouso depende da superfície, use `filterDefaults`. O valor padrão participa da
|
|
71
|
+
consulta, mas não exibe a ação de limpar. Depois de uma alteração, o `X` aparece e restaura esse
|
|
72
|
+
valor, inclusive quando ele é definido em runtime, como a pessoa da sessão. Sem declaração, filtros
|
|
73
|
+
simples usam o valor vazio como padrão; por isso, opções como “Todos” também não nascem com `X`.
|
|
74
|
+
|
|
75
|
+
Use `periodPosition` para manter o período antes ou depois dos filtros inline. Quando ele precisa
|
|
76
|
+
apenas aparecer depois de um filtro específico, use `{ afterFilter: 'dateBasis' }`. Se tipo de data
|
|
77
|
+
e período formam uma única decisão, declare `period={{ basisFilter: 'dateBasis' }}`: o Opus os reúne
|
|
78
|
+
num só controle, com ícone no início e uma única ação de limpar no fim. Quando a pessoa muda o tipo
|
|
79
|
+
ou o período padrão, essa ação aparece e restaura os dois valores. Se a âncora de `periodPosition`
|
|
80
|
+
não existir, o período permanece visível no fim dos filtros inline.
|
|
66
81
|
|
|
67
82
|
## Onde cada filtro aparece
|
|
68
83
|
|
|
@@ -70,7 +85,10 @@ Cada filtro declara `placement`. `inline`, o padrão, mantém o controle na barr
|
|
|
70
85
|
ao modal “Filtros”; `external` mantém o filtro no estado navegável e no input, mas deixa a
|
|
71
86
|
apresentação para o consumidor, como cards de status acima da lista. No modal, filtros com a mesma
|
|
72
87
|
`section` formam um grupo, e `advancedFilters.columns` distribui os grupos em até três colunas.
|
|
73
|
-
|
|
88
|
+
Use `advancedFilters.sectionIcons` quando os grupos representarem dimensões recorrentes; a chave é
|
|
89
|
+
o mesmo rótulo de `section`, e a legenda mantém o agrupamento leve com ícone e divisor.
|
|
90
|
+
Um filtro `select` pode declarar `searchable: true` quando a lista fechada for longa; a opção geral
|
|
91
|
+
continua disponível e participa da busca. Campos de data avançados usam calendário. A antiga `advanced: true` continua aceita durante a
|
|
74
92
|
migração e equivale a `placement: 'advanced'`.
|
|
75
93
|
|
|
76
94
|
```tsx
|
|
@@ -79,6 +97,7 @@ filters: {
|
|
|
79
97
|
status: {
|
|
80
98
|
label: 'Status',
|
|
81
99
|
type: 'select',
|
|
100
|
+
searchable: true,
|
|
82
101
|
placement: 'inline',
|
|
83
102
|
options: { kind: 'dictionary', ref: 'workspace.status' },
|
|
84
103
|
},
|
|
@@ -99,7 +118,17 @@ filters: {
|
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
// Na tela:
|
|
102
|
-
<ActionList
|
|
121
|
+
<ActionList
|
|
122
|
+
action={workspaceList}
|
|
123
|
+
input={{}}
|
|
124
|
+
advancedFilters={{
|
|
125
|
+
columns: 2,
|
|
126
|
+
sectionIcons: {
|
|
127
|
+
Relacionamento: <UserRound aria-hidden="true" />,
|
|
128
|
+
Datas: <CalendarDays aria-hidden="true" />,
|
|
129
|
+
},
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
103
132
|
```
|
|
104
133
|
|
|
105
134
|
## Colunas de dicionário
|
|
@@ -327,27 +356,31 @@ render(
|
|
|
327
356
|
|
|
328
357
|
## Propriedades de ActionFilterBar
|
|
329
358
|
|
|
330
|
-
| Propriedade
|
|
331
|
-
|
|
|
332
|
-
| `action`
|
|
333
|
-
| `state`
|
|
334
|
-
| `onStateChange`
|
|
335
|
-
| `filterOptions`
|
|
336
|
-
| `
|
|
337
|
-
| `
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
359
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
360
|
+
| ----------------- | ---------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------ |
|
|
361
|
+
| `action` | `Pick<ListAction, 'filters' \| 'text' \| 'periods'>` | | Declara busca, filtros e períodos disponíveis. |
|
|
362
|
+
| `state` | `ActionFilterState` | | Estado atual da barra. |
|
|
363
|
+
| `onStateChange` | `(next) => void` | | Recebe o estado completo depois de cada alteração. |
|
|
364
|
+
| `filterOptions` | `Record<string, SelectOption[]>` | | Fornece opções de runtime para filtros select e lookup. |
|
|
365
|
+
| `filterDefaults` | `Record<string, string>` | | Define o valor de repouso de filtros simples e o destino da ação de limpar. |
|
|
366
|
+
| `filterIcons` | `Record<string, ReactNode>` | | Associa um ícone leading ao controle de cada filtro. |
|
|
367
|
+
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3; sectionIcons?: Record<string, ReactNode> }` | `{ columns: 1 }` | Define as colunas do diálogo e os ícones opcionais das seções; o Opus deriva a largura correspondente. |
|
|
368
|
+
| `periodPosition` | `'before' \| 'after' \| { afterFilter: string }` | `'before'` | Posiciona o período antes, depois ou imediatamente após um filtro inline. |
|
|
369
|
+
| `period` | `{ basisFilter: string }` | | Reúne o período e o filtro inline indicado em um único controle. |
|
|
370
|
+
| `onRefresh` | `() => Promise<void> \| void` | | Exibe a ação de recarregar e executa a consulta do consumidor. |
|
|
371
|
+
| `refreshing` | `boolean` | `false` | Desabilita e anima a ação de recarregar durante a consulta. |
|
|
372
|
+
| `controls` | `ReactNode` | | Controles auxiliares agrupados com recarregar em um `ButtonGroup` espaçado. |
|
|
373
|
+
| `actions` | `ReactNode` | | Ações do consumidor exibidas depois do grupo de controles, com um intervalo maior. |
|
|
341
374
|
|
|
342
375
|
## Declaração na ListAction
|
|
343
376
|
|
|
344
|
-
| Chave | O que declara
|
|
345
|
-
| --------- |
|
|
346
|
-
| `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência.
|
|
347
|
-
| `filters` | `{ [nome]: { label, type, options?, multiple?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
|
|
348
|
-
| `text` | `{ fields }` — liga a busca; convenção: param `q` no input.
|
|
349
|
-
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`.
|
|
350
|
-
| `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input.
|
|
377
|
+
| Chave | O que declara |
|
|
378
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
379
|
+
| `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
|
|
380
|
+
| `filters` | `{ [nome]: { label, type, options?, multiple?, searchable?, allowEmpty?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `searchable` habilita busca local em `select`; `allowEmpty: false` omite a opção vazia. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
|
|
381
|
+
| `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Ela aparece depois dos filtros inline e imediatamente antes do acesso aos filtros avançados. A busca participa da contagem de controles inline: com mais de um controle, a barra exibe as labels; quando ela é o único recorte, sua label é omitida. |
|
|
382
|
+
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
383
|
+
| `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
|
|
351
384
|
|
|
352
385
|
## useListAction
|
|
353
386
|
|
|
@@ -359,26 +392,31 @@ chamar a action.
|
|
|
359
392
|
|
|
360
393
|
## Propriedades de ActionList
|
|
361
394
|
|
|
362
|
-
| Propriedade | Tipo
|
|
363
|
-
| ----------------------- |
|
|
364
|
-
| `action` | `ListAction<TInput, TItem>`
|
|
365
|
-
| `input` | `TInput`
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
369
|
-
| `
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
377
|
-
| `
|
|
378
|
-
| `
|
|
379
|
-
| `
|
|
380
|
-
| `
|
|
381
|
-
| `
|
|
382
|
-
| `
|
|
383
|
-
| `
|
|
384
|
-
| `
|
|
395
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
396
|
+
| ----------------------- | --------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
397
|
+
| `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item). A paginação envia `limit`/`page` e usa o `total` devolvido. |
|
|
398
|
+
| `input` | `TInput` | | O ESCOPO BASE (ex.: { workspaceId }) — a toolbar soma por cima, nunca sobrescreve. |
|
|
399
|
+
| `layout` | `'flow' \| 'fill'` | `'flow'` | Em `fill`, ocupa a altura disponível e mantém a rolagem dentro da região de resultados. |
|
|
400
|
+
| `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
|
|
401
|
+
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para os filtros select/lookup (chave = nome do filtro). |
|
|
402
|
+
| `filterDefaults` | `Record<string, string>` | | Valores de repouso aplicados à consulta; o `X` só aparece quando o filtro diverge deles e retorna a eles. |
|
|
403
|
+
| `filterIcons` | `Record<string, ReactNode>` | | Ícones leading dos filtros, indexados pelo nome declarado no contrato. |
|
|
404
|
+
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3; sectionIcons?: Record<string, ReactNode> }` | `{ columns: 1 }` | Define as colunas e os ícones opcionais dos grupos de filtros avançados; o Opus deriva a largura correspondente do diálogo. |
|
|
405
|
+
| `periodPosition` | `'before' \| 'after' \| { afterFilter: string }` | `'before'` | Posiciona o período antes, depois ou imediatamente após um filtro inline. |
|
|
406
|
+
| `period` | `{ basisFilter: string }` | | Reúne o período e o filtro inline indicado em um único controle. |
|
|
407
|
+
| `views` | `Record<string, ActionListView<TItem>>` | | Visualizações alternativas `{ label, icon?, render }`; o seletor alterna entre a tabela e estas. |
|
|
408
|
+
| `beforeContent` | `ReactNode` | | Conteúdo entre a toolbar e o corpo, como um resumo que reage ao recorte atual. |
|
|
409
|
+
| `columns` | `ActionListColumn<TItem>[]` | | Tabela EXPLÍCITA — sobrepõe as colunas do contrato (escape hatch). |
|
|
410
|
+
| `children` | `(items, refetch) => ReactNode` | | Modo composição: layout livre; a toolbar segue. Tem precedência sobre columns. |
|
|
411
|
+
| `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?: { title, message? }, destructive? }` — liga a multi-seleção. |
|
|
412
|
+
| `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
|
|
413
|
+
| `pageSize` | `number` | `50` | Itens por página padrão (vira `limit`/`page` no input; handler devolve `total`). O usuário troca no popover de exibição. |
|
|
414
|
+
| `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
|
|
415
|
+
| `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado, por exemplo quando um formulário inline aberto conta como conteúdo. |
|
|
416
|
+
| `loading` | `boolean` | | Carga extra agregada à da consulta, como uma query irmã de que os `children` dependem. |
|
|
417
|
+
| `emptyMessage` | `string` | `'Nenhum resultado.'` | Frase do estado vazio (o `Empty` do `DataState`). |
|
|
418
|
+
| `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
|
|
419
|
+
| `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
|
|
420
|
+
| `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
|
|
421
|
+
| `toolbarActions` | `ReactNode` | | Operações ligadas ao recorte atual, no fim da barra e separadas do grupo de controles. |
|
|
422
|
+
| `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita). A partir de duas, entram num grupo com intervalo compacto; uma só fica solta, sem grupo. Cliques ali não disparam o `onRowClick`. |
|
|
@@ -38,6 +38,10 @@ vincula ao controle, ligue-o ao controle por `aria-labelledby` para manter o nom
|
|
|
38
38
|
Declare `data-invalid="true"` no `Field` para que o rótulo acompanhe o contexto de erro;
|
|
39
39
|
`aria-invalid` marca apenas o controle. O `ActionForm` define os dois atributos sozinho.
|
|
40
40
|
|
|
41
|
+
Quando vários conjuntos compartilham a mesma superfície, use `variant="section"` para separar cada
|
|
42
|
+
grupo com uma linha leve. `icon` acrescenta uma referência visual antes do título sem transformar o
|
|
43
|
+
conjunto em outro card; escolha um ícone que represente a dimensão do grupo, não uma ação.
|
|
44
|
+
|
|
41
45
|
```tsx preview col md
|
|
42
46
|
<FieldSet>
|
|
43
47
|
<FieldLegend>Novo agente</FieldLegend>
|
|
@@ -70,7 +74,8 @@ Declare `data-invalid="true"` no `Field` para que o rótulo acompanhe o contexto
|
|
|
70
74
|
|
|
71
75
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
72
76
|
|---|---|---|---|
|
|
73
|
-
| `variant` | `'legend' \| 'label'` | `'legend'` | Define o porte de título de
|
|
77
|
+
| `variant` | `'legend' \| 'label' \| 'section'` | `'legend'` | Define o porte de título, de rótulo compacto ou de seção com divisor. |
|
|
78
|
+
| `icon` | `ReactNode` | | Exibe um ícone decorativo antes da legenda. Na variante `section`, o ícone recebe um suporte tonal. |
|
|
74
79
|
|
|
75
80
|
## Propriedades de FieldError
|
|
76
81
|
|
|
@@ -31,6 +31,40 @@ relacionada à medida. Um controle que explica o rótulo, como um tooltip, perte
|
|
|
31
31
|
/>
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Com medida complementar
|
|
35
|
+
|
|
36
|
+
Use `secondaryMetric` quando a mesma dimensão precisar mostrar uma segunda leitura sem competir
|
|
37
|
+
com o valor principal. A medida complementar aparece como uma lâmina posterior, com valor, rótulo
|
|
38
|
+
e `labelAction` próprios. Não use essa composição para duas métricas sem relação direta; nesse caso,
|
|
39
|
+
apresente dois cards independentes.
|
|
40
|
+
|
|
41
|
+
```tsx preview col
|
|
42
|
+
<MetricCard
|
|
43
|
+
label="Agendados"
|
|
44
|
+
value="18"
|
|
45
|
+
labelAction={
|
|
46
|
+
<Tooltip>
|
|
47
|
+
<TooltipTrigger asChild>
|
|
48
|
+
<Button size="icon-xs" variant="ghost" aria-label="Sobre Agendados"><Info /></Button>
|
|
49
|
+
</TooltipTrigger>
|
|
50
|
+
<TooltipContent>Eventos atualmente agendados.</TooltipContent>
|
|
51
|
+
</Tooltip>
|
|
52
|
+
}
|
|
53
|
+
secondaryMetric={{
|
|
54
|
+
label: 'agendadas no período',
|
|
55
|
+
value: '7',
|
|
56
|
+
labelAction: (
|
|
57
|
+
<Tooltip>
|
|
58
|
+
<TooltipTrigger asChild>
|
|
59
|
+
<Button size="icon-xs" variant="ghost" aria-label="Sobre o período"><Info /></Button>
|
|
60
|
+
</TooltipTrigger>
|
|
61
|
+
<TooltipContent>Agendamentos registrados dentro do período.</TooltipContent>
|
|
62
|
+
</Tooltip>
|
|
63
|
+
),
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
34
68
|
## Carregamento
|
|
35
69
|
|
|
36
70
|
`loading` preserva a geometria do card com placeholders e marca a superfície como ocupada para
|
|
@@ -49,6 +83,15 @@ tecnologias assistivas. O contêiner da coleção continua responsável pela men
|
|
|
49
83
|
| `loading` | `boolean` | `false` | Substitui rótulo e valor por skeletons e marca `aria-busy`. |
|
|
50
84
|
| `labelAction` | `ReactNode` | | Controle ao lado do rótulo, como uma explicação em tooltip. |
|
|
51
85
|
| `description` | `ReactNode` | | Contexto que explica recorte, proporção ou significado do valor. |
|
|
86
|
+
| `secondaryMetric` | `MetricCardSecondaryMetric` | | Medida complementar exibida como uma lâmina posterior sob o card principal. |
|
|
52
87
|
| `icon` | `ReactNode` | | Ícone decorativo que identifica a natureza da medida. |
|
|
53
88
|
| `context` | `'neutral' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'neutral'` | Contexto semântico do ícone; não altera a superfície do card. |
|
|
54
89
|
| `action` | `ReactNode` | | Ação relacionada diretamente à medida. |
|
|
90
|
+
|
|
91
|
+
## Propriedades de MetricCardSecondaryMetric
|
|
92
|
+
|
|
93
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
| `label` | `ReactNode` | | O nome da medida complementar. |
|
|
96
|
+
| `value` | `ReactNode` | | O valor complementar já formatado pelo consumidor. |
|
|
97
|
+
| `labelAction` | `ReactNode` | | Controle próprio ao lado do rótulo, como uma explicação em tooltip. |
|