@softize/opus 18.4.0 → 18.5.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 +12 -0
- package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +46 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +65 -0
- package/package.json +1 -1
- package/src/ui/components/patterns/content-header.tsx +168 -41
- package/src/ui/components/patterns/list.tsx +43 -13
- package/src/ui/components/primitives/detail.tsx +1 -1
- package/src/ui/components/primitives/empty.tsx +2 -1
- package/src/ui/components/primitives/field.tsx +38 -16
- package/src/ui/components/primitives/icon-tile.tsx +91 -0
- package/src/ui/components/primitives/item.tsx +2 -1
- package/src/ui/components/primitives/table.tsx +1 -1
- package/src/ui/docs/content/action-list.md +61 -52
- package/src/ui/docs/content/content.md +84 -8
- package/src/ui/docs/content/field.md +78 -30
- package/src/ui/docs/content/icon-tile.md +47 -0
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/meta.ts +9 -3
- package/src/ui/react.tsx +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ 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
|
+
## 18.5.0 — 2026-09-22
|
|
11
|
+
|
|
12
|
+
`Content` passa a concentrar estrutura e apresentação de blocos com cabeçalho e corpo: aceita
|
|
13
|
+
`as="section" | "fieldset"`, `variant="page" | "section"` e moldura opcional com
|
|
14
|
+
`frameVariant="default" | "legend"`. O novo
|
|
15
|
+
`IconTile` mantém a moldura do ícone como composição explícita, permitindo usar o mesmo ícone com
|
|
16
|
+
ou sem esse apoio visual.
|
|
17
|
+
|
|
18
|
+
`ActionList` deixa de aplicar moldura por padrão. No layout `fill`, mantém o cabeçalho fixo sobre a
|
|
19
|
+
cor da superfície, com borda inferior durante a rolagem. Filtros avançados podem escolher o tratamento das seções sem
|
|
20
|
+
recriar o fieldset no consumidor. Labels de campos e detalhes compartilham a mesma escala tipográfica.
|
|
21
|
+
|
|
10
22
|
## 18.4.0 — 2026-09-21
|
|
11
23
|
|
|
12
24
|
`executions` acrescenta infraestrutura experimental de acompanhamento: snapshots versionados,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ADR 0020 — Molduras de ícone são compostas explicitamente
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-18.
|
|
5
|
+
|
|
6
|
+
## Contexto
|
|
7
|
+
|
|
8
|
+
Várias interfaces apresentam um ícone dentro de uma pequena superfície quadrada. A mesma forma
|
|
9
|
+
aparecia incorporada em componentes como `FieldLegend`, `ItemMedia` e `EmptyMedia`, ou era
|
|
10
|
+
reconstruída pelo consumidor. Em `FieldLegend`, isso também vinculava a moldura à variante da
|
|
11
|
+
legenda: o consumidor conseguia escolher o ícone, mas não se ele deveria receber a superfície.
|
|
12
|
+
|
|
13
|
+
## Decisão
|
|
14
|
+
|
|
15
|
+
`IconTile` passa a representar a moldura visual não interativa de um ícone. Ele compartilha a
|
|
16
|
+
escala de tamanhos dos controles, declara a cor por `context` e oferece os tratamentos `subtle` e
|
|
17
|
+
`outline`.
|
|
18
|
+
|
|
19
|
+
Slots que aceitam conteúdo, como `FieldLegend.icon`, continuam genéricos. O consumidor passa um
|
|
20
|
+
ícone diretamente quando quer somente o glifo ou compõe `<IconTile><Icon /></IconTile>` quando a
|
|
21
|
+
moldura reforça a hierarquia. `ItemMedia variant="icon"` e `EmptyMedia variant="icon"` reutilizam
|
|
22
|
+
a mesma base visual porque essas variantes já prometem a moldura.
|
|
23
|
+
|
|
24
|
+
`IconTile` é somente apresentação. Quando o ícone aciona algo, deve-se usar `Button` com tamanho
|
|
25
|
+
de ícone e nome acessível.
|
|
26
|
+
|
|
27
|
+
## Consequências
|
|
28
|
+
|
|
29
|
+
- A presença da moldura deixa de ser uma regra implícita da legenda.
|
|
30
|
+
- Tamanho, arredondamento e preenchimento convergem entre consumidores.
|
|
31
|
+
- Um slot permanece flexível sem acumular propriedades como `iconFramed` ou `iconTile`.
|
|
32
|
+
- Migrações devem distinguir molduras decorativas de botões e indicadores semânticos.
|
|
33
|
+
|
|
34
|
+
## Alternativas descartadas
|
|
35
|
+
|
|
36
|
+
- **Adicionar uma propriedade à `FieldLegend`:** resolveria apenas uma superfície e ampliaria o
|
|
37
|
+
contrato toda vez que outra composição precisasse escolher a moldura.
|
|
38
|
+
- **Manter classes livres em cada consumidor:** preservaria divergências de escala e acabamento.
|
|
39
|
+
- **Usar `Button` desabilitado:** comunicaria interatividade onde não existe ação.
|
|
40
|
+
|
|
41
|
+
## Verificação
|
|
42
|
+
|
|
43
|
+
- teste estrutural cobre tamanho, variante e tamanho do glifo de `IconTile`;
|
|
44
|
+
- teste de `FieldLegend` confirma que o slot não adiciona moldura por conta própria;
|
|
45
|
+
- teste da API pública garante que o componente e seus metadados sejam exportados;
|
|
46
|
+
- documentação de `Field` mostra a composição explícita.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ADR 0021 — Content reúne estrutura visual e elemento semântico
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-19.
|
|
5
|
+
|
|
6
|
+
## Contexto
|
|
7
|
+
|
|
8
|
+
Regiões com título e corpo usam `Content`, mas grupos de formulário precisam reconstruir a mesma
|
|
9
|
+
estrutura com `FieldSet` e `FieldLegend`. Molduras aplicadas diretamente em `DetailGroup` também
|
|
10
|
+
separam visualmente o título dos dados que ele nomeia. Essa sobreposição aumenta a quantidade de
|
|
11
|
+
estruturas que a documentação e os consumidores precisam combinar.
|
|
12
|
+
|
|
13
|
+
## Decisão
|
|
14
|
+
|
|
15
|
+
`Content` continua responsável pela anatomia `Header + Body` e passa a aceitar:
|
|
16
|
+
|
|
17
|
+
- `framed`, que aplica a moldura à região inteira;
|
|
18
|
+
- `as="fieldset"`, que renderiza a raiz como `fieldset` e o título como `legend`.
|
|
19
|
+
- `frameVariant="legend"`, que integra visualmente a legenda à borda quando esse acabamento for
|
|
20
|
+
escolhido de forma explícita.
|
|
21
|
+
|
|
22
|
+
O default permanece `as="section"` sem moldura. `FieldGroup` organiza campos dentro do corpo e
|
|
23
|
+
`DetailGroup` preserva a semântica de lista de definições sem assumir a moldura da região.
|
|
24
|
+
`FieldSet` permanece disponível para compatibilidade e para composições que não precisam da
|
|
25
|
+
anatomia completa de `Content`.
|
|
26
|
+
|
|
27
|
+
No modo `fieldset`, a legenda aceita somente o título. Contador, descrição e ações não são filhos
|
|
28
|
+
válidos de `legend`; consumidores que precisarem dessas regiões devem mantê-las no corpo ou usar
|
|
29
|
+
uma `section`. A escolha de `fieldset` não define o acabamento visual: quando `framed` estiver
|
|
30
|
+
presente, a moldura é contínua por padrão. Os filtros avançados preservam a legenda integrada à
|
|
31
|
+
borda declarando `frameVariant="legend"`.
|
|
32
|
+
|
|
33
|
+
## Consequências
|
|
34
|
+
|
|
35
|
+
- A mesma API nomeia e delimita regiões de leitura e grupos de formulário.
|
|
36
|
+
- A escolha do elemento semântico não fica acoplada à presença da moldura.
|
|
37
|
+
- O recorte visual da borda deixa de ser consequência implícita do elemento semântico.
|
|
38
|
+
- A forma curta continua sendo o caminho comum, enquanto a composição explícita permanece
|
|
39
|
+
disponível nos dois elementos.
|
|
40
|
+
- A migração de `FieldSet` pode ser gradual e não exige remoção nesta versão.
|
|
41
|
+
|
|
42
|
+
## Alternativas consideradas
|
|
43
|
+
|
|
44
|
+
### Criar Panel
|
|
45
|
+
|
|
46
|
+
Separaria a moldura de `Content`, mas criaria outra raiz com header e body para documentar e
|
|
47
|
+
compor. Foi descartada porque `Content` já possui essa responsabilidade.
|
|
48
|
+
|
|
49
|
+
### Renomear FieldSet para FieldGroup
|
|
50
|
+
|
|
51
|
+
Produziria simetria nominal com `DetailGroup`, mas perderia a distinção entre a fronteira semântica
|
|
52
|
+
do formulário e o layout interno dos campos. Foi descartada porque `FieldGroup` já representa esse
|
|
53
|
+
layout.
|
|
54
|
+
|
|
55
|
+
### Adicionar um booleano fieldset
|
|
56
|
+
|
|
57
|
+
Permitiria alternar o elemento, mas trataria semântica como estado visual. Foi descartada em favor
|
|
58
|
+
de `as`, que explicita o elemento renderizado.
|
|
59
|
+
|
|
60
|
+
## Verificação
|
|
61
|
+
|
|
62
|
+
- Testes de UI confirmam `section` e heading como defaults.
|
|
63
|
+
- Testes de UI confirmam `fieldset`, `legend`, props nativas e ausência de heading nesse modo.
|
|
64
|
+
- Testes reprovam slots incompatíveis com a legenda.
|
|
65
|
+
- A documentação demonstra `Content + DetailGroup` e `Content as="fieldset" + FieldGroup`.
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isValidElement,
|
|
5
5
|
useContext,
|
|
6
6
|
useId,
|
|
7
|
+
type ComponentProps,
|
|
7
8
|
type HTMLAttributes,
|
|
8
9
|
type ReactElement,
|
|
9
10
|
type ReactNode,
|
|
@@ -18,8 +19,13 @@ import {
|
|
|
18
19
|
|
|
19
20
|
export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
20
21
|
export type ContentHeaderVariant = SurfaceHeaderVariant;
|
|
22
|
+
export type ContentElement = "section" | "fieldset";
|
|
23
|
+
export type ContentFrameVariant = "default" | "legend";
|
|
21
24
|
|
|
22
25
|
interface ContentContextValue {
|
|
26
|
+
element: ContentElement;
|
|
27
|
+
frameVariant: ContentFrameVariant;
|
|
28
|
+
framed: boolean;
|
|
23
29
|
level: ContentHeaderLevel;
|
|
24
30
|
titleId: string;
|
|
25
31
|
variant: ContentHeaderVariant;
|
|
@@ -38,17 +44,37 @@ function requiredContext<T>(
|
|
|
38
44
|
return value;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
interface
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
> {
|
|
45
|
-
/** Nível semântico do heading, independente de seu destaque visual. */
|
|
46
|
-
level?: ContentHeaderLevel;
|
|
47
|
+
interface ContentSharedProps {
|
|
48
|
+
/** Aplica a moldura estrutural da região sem transferi-la ao conteúdo interno. */
|
|
49
|
+
framed?: boolean;
|
|
47
50
|
/** Hierarquia visual: `section` (default) ou `page`, a mesma de PageHeader. */
|
|
48
51
|
variant?: ContentHeaderVariant;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
interface
|
|
54
|
+
interface ContentSectionBaseProps
|
|
55
|
+
extends
|
|
56
|
+
ContentSharedProps,
|
|
57
|
+
Omit<ComponentProps<"section">, "children" | "title"> {
|
|
58
|
+
/** Elemento semântico da região. */
|
|
59
|
+
as?: "section";
|
|
60
|
+
frameVariant?: "default";
|
|
61
|
+
/** Nível semântico do heading, independente de seu destaque visual. */
|
|
62
|
+
level?: ContentHeaderLevel;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ContentFieldsetBaseProps
|
|
66
|
+
extends
|
|
67
|
+
ContentSharedProps,
|
|
68
|
+
Omit<ComponentProps<"fieldset">, "children" | "title"> {
|
|
69
|
+
/** Usa a semântica nativa de grupo de campos; o título vira `legend`. */
|
|
70
|
+
as: "fieldset";
|
|
71
|
+
/** Tratamento visual da moldura: contínua (default) ou recortada pela legenda. */
|
|
72
|
+
frameVariant?: ContentFrameVariant;
|
|
73
|
+
level?: never;
|
|
74
|
+
variant?: "section";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ContentSectionShorthandProps extends ContentSectionBaseProps {
|
|
52
78
|
title: ReactNode;
|
|
53
79
|
/** Total de itens ao lado do título da seção. A página não carrega contador (ADR 0009). */
|
|
54
80
|
count?: number;
|
|
@@ -57,7 +83,15 @@ interface ContentShorthandProps extends ContentBaseProps {
|
|
|
57
83
|
children: ReactNode;
|
|
58
84
|
}
|
|
59
85
|
|
|
60
|
-
interface
|
|
86
|
+
interface ContentFieldsetShorthandProps extends ContentFieldsetBaseProps {
|
|
87
|
+
title: ReactNode;
|
|
88
|
+
count?: never;
|
|
89
|
+
description?: never;
|
|
90
|
+
actions?: never;
|
|
91
|
+
children: ReactNode;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ContentComposedProps {
|
|
61
95
|
title?: never;
|
|
62
96
|
count?: never;
|
|
63
97
|
description?: never;
|
|
@@ -65,22 +99,36 @@ interface ContentComposedProps extends ContentBaseProps {
|
|
|
65
99
|
children: ReactNode;
|
|
66
100
|
}
|
|
67
101
|
|
|
68
|
-
export type ContentProps =
|
|
102
|
+
export type ContentProps =
|
|
103
|
+
| ContentSectionShorthandProps
|
|
104
|
+
| ContentFieldsetShorthandProps
|
|
105
|
+
| ((ContentSectionBaseProps | ContentFieldsetBaseProps) &
|
|
106
|
+
ContentComposedProps);
|
|
69
107
|
|
|
70
108
|
/** Região de conteúdo nomeada. A forma curta e a explícita produzem a mesma anatomia. */
|
|
71
109
|
export function Content({
|
|
110
|
+
as = "section",
|
|
72
111
|
title,
|
|
73
112
|
count,
|
|
74
113
|
description,
|
|
75
114
|
actions,
|
|
76
115
|
level = 2,
|
|
77
116
|
variant = "section",
|
|
117
|
+
framed = false,
|
|
118
|
+
frameVariant = "default",
|
|
78
119
|
children,
|
|
79
120
|
className,
|
|
80
121
|
...props
|
|
81
122
|
}: ContentProps): ReactElement {
|
|
82
123
|
const titleId = useId();
|
|
83
|
-
const context = {
|
|
124
|
+
const context = {
|
|
125
|
+
element: as,
|
|
126
|
+
frameVariant,
|
|
127
|
+
framed,
|
|
128
|
+
level: level as ContentHeaderLevel,
|
|
129
|
+
titleId,
|
|
130
|
+
variant,
|
|
131
|
+
};
|
|
84
132
|
const shorthand = title !== undefined;
|
|
85
133
|
const nodes = Children.toArray(children);
|
|
86
134
|
|
|
@@ -110,34 +158,64 @@ export function Content({
|
|
|
110
158
|
}
|
|
111
159
|
}
|
|
112
160
|
|
|
161
|
+
const anatomy = shorthand ? (
|
|
162
|
+
<>
|
|
163
|
+
<ContentHeader>
|
|
164
|
+
<ContentTitle>{title}</ContentTitle>
|
|
165
|
+
{count !== undefined && <ContentMeta>{count}</ContentMeta>}
|
|
166
|
+
{description !== undefined && (
|
|
167
|
+
<ContentDescription>{description}</ContentDescription>
|
|
168
|
+
)}
|
|
169
|
+
{actions !== undefined && <ContentActions>{actions}</ContentActions>}
|
|
170
|
+
</ContentHeader>
|
|
171
|
+
<ContentBody>{children}</ContentBody>
|
|
172
|
+
</>
|
|
173
|
+
) : (
|
|
174
|
+
children
|
|
175
|
+
);
|
|
176
|
+
const ariaLabelledBy =
|
|
177
|
+
props["aria-label"] === undefined ? titleId : undefined;
|
|
178
|
+
|
|
113
179
|
return (
|
|
114
180
|
<ContentContext.Provider value={context}>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
181
|
+
{as === "fieldset" ? (
|
|
182
|
+
<fieldset
|
|
183
|
+
data-slot="content"
|
|
184
|
+
data-as="fieldset"
|
|
185
|
+
data-framed={framed}
|
|
186
|
+
data-frame-variant={frameVariant}
|
|
187
|
+
aria-labelledby={ariaLabelledBy}
|
|
188
|
+
className={cn(
|
|
189
|
+
!framed && "space-y-3",
|
|
190
|
+
framed &&
|
|
191
|
+
frameVariant === "default" &&
|
|
192
|
+
"rounded-lg border border-border p-4",
|
|
193
|
+
framed &&
|
|
194
|
+
frameVariant === "legend" &&
|
|
195
|
+
"space-y-3 rounded-lg border border-border px-4 pb-4",
|
|
196
|
+
className,
|
|
197
|
+
)}
|
|
198
|
+
{...(props as ComponentProps<"fieldset">)}
|
|
199
|
+
>
|
|
200
|
+
{anatomy}
|
|
201
|
+
</fieldset>
|
|
202
|
+
) : (
|
|
203
|
+
<section
|
|
204
|
+
data-slot="content"
|
|
205
|
+
data-as="section"
|
|
206
|
+
data-framed={framed}
|
|
207
|
+
data-frame-variant={frameVariant}
|
|
208
|
+
aria-labelledby={ariaLabelledBy}
|
|
209
|
+
className={cn(
|
|
210
|
+
"space-y-3",
|
|
211
|
+
framed && "rounded-lg border border-border p-4",
|
|
212
|
+
className,
|
|
213
|
+
)}
|
|
214
|
+
{...(props as ComponentProps<"section">)}
|
|
215
|
+
>
|
|
216
|
+
{anatomy}
|
|
217
|
+
</section>
|
|
218
|
+
)}
|
|
141
219
|
</ContentContext.Provider>
|
|
142
220
|
);
|
|
143
221
|
}
|
|
@@ -158,6 +236,37 @@ export function ContentHeader({
|
|
|
158
236
|
"ContentHeader",
|
|
159
237
|
"Content",
|
|
160
238
|
);
|
|
239
|
+
if (content.element === "fieldset") {
|
|
240
|
+
const nodes = Children.toArray(children);
|
|
241
|
+
if (
|
|
242
|
+
nodes.length !== 1 ||
|
|
243
|
+
!isValidElement(nodes[0]) ||
|
|
244
|
+
nodes[0].type !== ContentTitle
|
|
245
|
+
) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"ContentHeader de fieldset exige somente um ContentTitle; contador, descrição e ações ficam fora da legenda.",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return (
|
|
251
|
+
<ContentHeaderContext.Provider value={content}>
|
|
252
|
+
<legend
|
|
253
|
+
data-slot="content-header"
|
|
254
|
+
className={cn(
|
|
255
|
+
content.framed &&
|
|
256
|
+
content.frameVariant === "default" &&
|
|
257
|
+
"float-left w-full",
|
|
258
|
+
content.framed &&
|
|
259
|
+
content.frameVariant === "legend" &&
|
|
260
|
+
"-ml-2 w-auto px-2",
|
|
261
|
+
className,
|
|
262
|
+
)}
|
|
263
|
+
{...props}
|
|
264
|
+
>
|
|
265
|
+
{children}
|
|
266
|
+
</legend>
|
|
267
|
+
</ContentHeaderContext.Provider>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
161
270
|
return (
|
|
162
271
|
<ContentHeaderContext.Provider value={content}>
|
|
163
272
|
<SurfaceHeader
|
|
@@ -195,14 +304,15 @@ export function ContentTitle({
|
|
|
195
304
|
className,
|
|
196
305
|
id,
|
|
197
306
|
...props
|
|
198
|
-
}: HTMLAttributes<
|
|
307
|
+
}: HTMLAttributes<HTMLElement>): ReactElement {
|
|
199
308
|
const context = requiredContext(
|
|
200
309
|
useContext(ContentHeaderContext),
|
|
201
310
|
"ContentTitle",
|
|
202
311
|
"ContentHeader",
|
|
203
312
|
);
|
|
204
|
-
const Heading =
|
|
205
|
-
|
|
313
|
+
const Heading =
|
|
314
|
+
context.element === "fieldset" ? "span" : headingTags[context.level];
|
|
315
|
+
usePageHeadingPresence(context.element !== "fieldset" && context.level === 1);
|
|
206
316
|
return (
|
|
207
317
|
<Heading
|
|
208
318
|
id={id ?? context.titleId}
|
|
@@ -279,6 +389,23 @@ export function ContentBody({
|
|
|
279
389
|
className,
|
|
280
390
|
...props
|
|
281
391
|
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
282
|
-
requiredContext(
|
|
283
|
-
|
|
392
|
+
const content = requiredContext(
|
|
393
|
+
useContext(ContentContext),
|
|
394
|
+
"ContentBody",
|
|
395
|
+
"Content",
|
|
396
|
+
);
|
|
397
|
+
return (
|
|
398
|
+
<div
|
|
399
|
+
data-slot="content-body"
|
|
400
|
+
className={cn(
|
|
401
|
+
content.element === "fieldset" && "space-y-3",
|
|
402
|
+
content.element === "fieldset" &&
|
|
403
|
+
content.framed &&
|
|
404
|
+
content.frameVariant === "default" &&
|
|
405
|
+
"clear-left pt-3",
|
|
406
|
+
className,
|
|
407
|
+
)}
|
|
408
|
+
{...props}
|
|
409
|
+
/>
|
|
410
|
+
);
|
|
284
411
|
}
|
|
@@ -111,8 +111,13 @@ import {
|
|
|
111
111
|
InputGroupInput,
|
|
112
112
|
} from "../primitives/input-group.tsx";
|
|
113
113
|
import { Separator } from "../primitives/separator.tsx";
|
|
114
|
-
import {
|
|
115
|
-
|
|
114
|
+
import {
|
|
115
|
+
Field,
|
|
116
|
+
FieldGroup,
|
|
117
|
+
FieldLabel,
|
|
118
|
+
type FieldSetVariant,
|
|
119
|
+
} from "../primitives/field.tsx";
|
|
120
|
+
import { Content } from "./content-header.tsx";
|
|
116
121
|
import { Search } from "lucide-react";
|
|
117
122
|
import { Select, type SelectOption } from "../primitives/select.tsx";
|
|
118
123
|
import {
|
|
@@ -307,6 +312,8 @@ export interface AdvancedFiltersLayout {
|
|
|
307
312
|
columns?: 1 | 2 | 3;
|
|
308
313
|
/** Ícone de cada seção, indexado pelo mesmo rótulo declarado nos filtros. */
|
|
309
314
|
sectionIcons?: Record<string, ReactNode>;
|
|
315
|
+
/** Tratamento visual de cada seção. `framed` integra a legenda à moldura do conjunto. */
|
|
316
|
+
sectionVariant?: FieldSetVariant;
|
|
310
317
|
}
|
|
311
318
|
|
|
312
319
|
/** Uma view alternativa da MESMA listagem (board, galeria, lista, calendário…). A
|
|
@@ -1105,6 +1112,7 @@ function AdvancedFiltersDialog({
|
|
|
1105
1112
|
filterDefaults,
|
|
1106
1113
|
filterIcons,
|
|
1107
1114
|
sectionIcons,
|
|
1115
|
+
sectionVariant = "plain",
|
|
1108
1116
|
cascade,
|
|
1109
1117
|
columns = 1,
|
|
1110
1118
|
}: {
|
|
@@ -1117,6 +1125,7 @@ function AdvancedFiltersDialog({
|
|
|
1117
1125
|
filterDefaults?: Record<string, string> | undefined;
|
|
1118
1126
|
filterIcons?: Record<string, ReactNode> | undefined;
|
|
1119
1127
|
sectionIcons?: Record<string, ReactNode> | undefined;
|
|
1128
|
+
sectionVariant?: FieldSetVariant | undefined;
|
|
1120
1129
|
columns?: 1 | 2 | 3;
|
|
1121
1130
|
/** A cascata do `depends` do pai — mudar um campo limpa os dependentes no draft. */
|
|
1122
1131
|
cascade: (
|
|
@@ -1153,8 +1162,10 @@ function AdvancedFiltersDialog({
|
|
|
1153
1162
|
}[columns];
|
|
1154
1163
|
|
|
1155
1164
|
const field = ([name, spec]: [string, FilterSpec]): React.ReactElement => (
|
|
1156
|
-
<
|
|
1157
|
-
<
|
|
1165
|
+
<Field key={name}>
|
|
1166
|
+
<FieldLabel htmlFor={`filter-${name}`}>
|
|
1167
|
+
{text(spec.label, name)}
|
|
1168
|
+
</FieldLabel>
|
|
1158
1169
|
<FilterField
|
|
1159
1170
|
name={name}
|
|
1160
1171
|
spec={spec}
|
|
@@ -1166,7 +1177,7 @@ function AdvancedFiltersDialog({
|
|
|
1166
1177
|
defaultValue={filterDefaults?.[name]}
|
|
1167
1178
|
icon={filterIcons?.[name]}
|
|
1168
1179
|
/>
|
|
1169
|
-
</
|
|
1180
|
+
</Field>
|
|
1170
1181
|
);
|
|
1171
1182
|
|
|
1172
1183
|
const clear = (): void => {
|
|
@@ -1194,14 +1205,32 @@ function AdvancedFiltersDialog({
|
|
|
1194
1205
|
<DialogBody className="space-y-4">
|
|
1195
1206
|
{sectioned ? (
|
|
1196
1207
|
sections.map(([section, entries]) => (
|
|
1197
|
-
<
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1208
|
+
<Content
|
|
1209
|
+
key={section}
|
|
1210
|
+
as="fieldset"
|
|
1211
|
+
framed={sectionVariant === "framed"}
|
|
1212
|
+
frameVariant={
|
|
1213
|
+
sectionVariant === "framed" ? "legend" : "default"
|
|
1214
|
+
}
|
|
1215
|
+
title={
|
|
1216
|
+
<span className="flex items-center gap-2">
|
|
1217
|
+
{sectionIcons?.[section] !== undefined && (
|
|
1218
|
+
<span
|
|
1219
|
+
data-slot="content-title-icon"
|
|
1220
|
+
aria-hidden="true"
|
|
1221
|
+
className="inline-flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4"
|
|
1222
|
+
>
|
|
1223
|
+
{sectionIcons[section]}
|
|
1224
|
+
</span>
|
|
1225
|
+
)}
|
|
1226
|
+
{section}
|
|
1227
|
+
</span>
|
|
1228
|
+
}
|
|
1229
|
+
>
|
|
1201
1230
|
<FieldGroup className={cn("grid gap-4", gridColumns)}>
|
|
1202
1231
|
{entries.map(field)}
|
|
1203
1232
|
</FieldGroup>
|
|
1204
|
-
</
|
|
1233
|
+
</Content>
|
|
1205
1234
|
))
|
|
1206
1235
|
) : (
|
|
1207
1236
|
<FieldGroup className={cn("grid gap-4", gridColumns)}>
|
|
@@ -1597,6 +1626,7 @@ export function ActionFilterBar({
|
|
|
1597
1626
|
filterIcons={filterIcons}
|
|
1598
1627
|
columns={advancedFilters?.columns}
|
|
1599
1628
|
sectionIcons={advancedFilters?.sectionIcons}
|
|
1629
|
+
sectionVariant={advancedFilters?.sectionVariant}
|
|
1600
1630
|
/>
|
|
1601
1631
|
)}
|
|
1602
1632
|
</div>
|
|
@@ -2111,7 +2141,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2111
2141
|
} else {
|
|
2112
2142
|
content = (
|
|
2113
2143
|
<Table
|
|
2114
|
-
variant="
|
|
2144
|
+
variant="plain"
|
|
2115
2145
|
className={tableStateActive ? "h-full" : undefined}
|
|
2116
2146
|
containerClassName={
|
|
2117
2147
|
tableFillsAvailableHeight
|
|
@@ -2122,7 +2152,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2122
2152
|
<TableHeader
|
|
2123
2153
|
className={
|
|
2124
2154
|
tableFillsAvailableHeight
|
|
2125
|
-
? "sticky top-0 z-10 bg-
|
|
2155
|
+
? "sticky top-0 z-10 bg-background shadow-[inset_0_-0.0625rem_0_var(--border)] [&_tr]:border-b-0"
|
|
2126
2156
|
: undefined
|
|
2127
2157
|
}
|
|
2128
2158
|
>
|
|
@@ -2270,7 +2300,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2270
2300
|
total !== undefined && !isError ? (
|
|
2271
2301
|
<div
|
|
2272
2302
|
data-slot="action-list-footer"
|
|
2273
|
-
className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 px-3 py-1.5 text-xs text-muted-foreground"
|
|
2303
|
+
className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 border-t border-border px-3 py-1.5 text-xs text-muted-foreground"
|
|
2274
2304
|
>
|
|
2275
2305
|
<span>
|
|
2276
2306
|
Página {state.page} de {pages}
|
|
@@ -131,7 +131,7 @@ function DetailField({
|
|
|
131
131
|
<dt
|
|
132
132
|
data-slot="detail-field-label"
|
|
133
133
|
className={cn(
|
|
134
|
-
'flex items-center gap-2.5 text-
|
|
134
|
+
'flex items-center gap-2.5 text-sm leading-snug font-medium text-muted-foreground',
|
|
135
135
|
splitHorizontalField && 'bg-muted/30 p-3 dark:bg-input/30',
|
|
136
136
|
)}
|
|
137
137
|
>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
2
2
|
|
|
3
3
|
import { cn } from '../../lib/cn.ts'
|
|
4
|
+
import { iconTileVariants } from './icon-tile.tsx'
|
|
4
5
|
|
|
5
6
|
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
7
|
return (
|
|
@@ -34,7 +35,7 @@ const emptyMediaVariants = cva(
|
|
|
34
35
|
variants: {
|
|
35
36
|
variant: {
|
|
36
37
|
default: "bg-transparent",
|
|
37
|
-
icon:
|
|
38
|
+
icon: iconTileVariants({ size: "lg", variant: "subtle" }),
|
|
38
39
|
},
|
|
39
40
|
},
|
|
40
41
|
defaultVariants: {
|