@softize/opus 23.0.0 → 24.0.1
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 +99 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +241 -42
- package/bin/lib/copy.mjs +16 -34
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/docs/adr/0049-inline-navigation-groups-with-the-heading.md +95 -0
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +15 -19
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +90 -27
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +90 -73
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +4 -2
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +76 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/compositions.md +479 -0
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +105 -60
- package/src/ui/docs/content/drawer.md +68 -22
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +44 -34
- package/src/ui/docs/content/pane.md +42 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +20 -20
- package/src/ui/docs/content/toggle.md +117 -65
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +10 -6
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +40 -43
- package/src/ui/theme.css +311 -64
- package/src/ui/components/patterns/content-header.tsx +0 -528
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- package/src/ui/docs/content/content.md +0 -275
|
@@ -12,40 +12,39 @@
|
|
|
12
12
|
* Nos dois, navegar emite `popstate`: host que guarda o path por conta própria acompanha.
|
|
13
13
|
* `basePath` adapta o prefixo das URLs (`/docs` no Maestro, `/__docs` nos apps).
|
|
14
14
|
*/
|
|
15
|
-
import { useEffect, useState } from
|
|
16
|
-
import { DOC_SECTIONS, type DocSection, type DocEntry } from
|
|
17
|
-
import { Pane, Split } from
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
import { DOC_SECTIONS, type DocSection, type DocEntry } from "./registry";
|
|
17
|
+
import { Pane, Split } from "../components/patterns/split.tsx";
|
|
18
18
|
import {
|
|
19
|
-
PaneBody,
|
|
20
19
|
Sidebar,
|
|
21
20
|
SidebarGroupLabel,
|
|
22
21
|
SidebarItem,
|
|
23
22
|
SidebarTreeGroup,
|
|
24
|
-
} from
|
|
25
|
-
import { navigate, usePathname } from
|
|
23
|
+
} from "../components/patterns/sidebar.tsx";
|
|
24
|
+
import { navigate, usePathname } from "../router.ts";
|
|
26
25
|
|
|
27
26
|
function findPage(sections: DocSection[], slug: string): DocEntry | undefined {
|
|
28
27
|
for (const s of sections) {
|
|
29
28
|
for (const g of s.groups) {
|
|
30
|
-
const p = g.pages.find((p) => p.slug === slug)
|
|
31
|
-
if (p) return p
|
|
29
|
+
const p = g.pages.find((p) => p.slug === slug);
|
|
30
|
+
if (p) return p;
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
|
-
return undefined
|
|
33
|
+
return undefined;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
export interface DocBrowserProps {
|
|
38
37
|
/** Prefixo das rotas da doc. Default `/docs` (Maestro); `/__docs` na rota dev dos apps. */
|
|
39
|
-
basePath?: string
|
|
38
|
+
basePath?: string;
|
|
40
39
|
/** Path controlado pelo host (modo Maestro). Ausente = standalone (gerencia o próprio). */
|
|
41
|
-
path?: string
|
|
40
|
+
path?: string;
|
|
42
41
|
/** Seções a exibir (seção → grupo → página). Default = catálogo inteiro. */
|
|
43
|
-
sections?: DocSection[]
|
|
42
|
+
sections?: DocSection[];
|
|
44
43
|
/** Slugs antigos que devem abrir e substituir a URL pela página canônica. */
|
|
45
|
-
redirects?: Readonly<Record<string, string
|
|
44
|
+
redirects?: Readonly<Record<string, string>>;
|
|
46
45
|
/** Monta o `document.title` a partir do título da página atual (ex.: `(t) => `${t} · Opus``).
|
|
47
46
|
* Ausente = não mexe no title. */
|
|
48
|
-
documentTitle?: (pageTitle: string) => string
|
|
47
|
+
documentTitle?: (pageTitle: string) => string;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
function DocSidebarTree({
|
|
@@ -53,67 +52,75 @@ function DocSidebarTree({
|
|
|
53
52
|
activeSlug,
|
|
54
53
|
onSelect,
|
|
55
54
|
}: {
|
|
56
|
-
sections: DocSection[]
|
|
57
|
-
activeSlug?: string
|
|
58
|
-
onSelect: (slug: string) => void
|
|
55
|
+
sections: DocSection[];
|
|
56
|
+
activeSlug?: string;
|
|
57
|
+
onSelect: (slug: string) => void;
|
|
59
58
|
}): React.ReactElement {
|
|
60
|
-
const [closedGroups, setClosedGroups] = useState<Set<string>>(
|
|
59
|
+
const [closedGroups, setClosedGroups] = useState<Set<string>>(
|
|
60
|
+
() => new Set(),
|
|
61
|
+
);
|
|
61
62
|
|
|
62
63
|
useEffect(() => {
|
|
63
|
-
const activeGroup = sections
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const activeGroup = sections
|
|
65
|
+
.flatMap((section, sectionIndex) =>
|
|
66
|
+
section.groups.map((group, groupIndex) => ({
|
|
67
|
+
key: `${sectionIndex}:${groupIndex}`,
|
|
68
|
+
group,
|
|
69
|
+
})),
|
|
70
|
+
)
|
|
71
|
+
.find(({ group }) =>
|
|
72
|
+
group.pages.some((page) => page.slug === activeSlug),
|
|
73
|
+
);
|
|
69
74
|
|
|
70
|
-
if (activeGroup === undefined) return
|
|
75
|
+
if (activeGroup === undefined) return;
|
|
71
76
|
setClosedGroups((current) => {
|
|
72
|
-
if (!current.has(activeGroup.key)) return current
|
|
73
|
-
const next = new Set(current)
|
|
74
|
-
next.delete(activeGroup.key)
|
|
75
|
-
return next
|
|
76
|
-
})
|
|
77
|
-
}, [activeSlug, sections])
|
|
77
|
+
if (!current.has(activeGroup.key)) return current;
|
|
78
|
+
const next = new Set(current);
|
|
79
|
+
next.delete(activeGroup.key);
|
|
80
|
+
return next;
|
|
81
|
+
});
|
|
82
|
+
}, [activeSlug, sections]);
|
|
78
83
|
|
|
79
84
|
const toggleGroup = (key: string): void => {
|
|
80
85
|
setClosedGroups((current) => {
|
|
81
|
-
const next = new Set(current)
|
|
82
|
-
if (next.has(key)) next.delete(key)
|
|
83
|
-
else next.add(key)
|
|
84
|
-
return next
|
|
85
|
-
})
|
|
86
|
-
}
|
|
86
|
+
const next = new Set(current);
|
|
87
|
+
if (next.has(key)) next.delete(key);
|
|
88
|
+
else next.add(key);
|
|
89
|
+
return next;
|
|
90
|
+
});
|
|
91
|
+
};
|
|
87
92
|
|
|
88
93
|
const renderPage = (page: DocEntry): React.ReactElement => (
|
|
89
94
|
<SidebarItem
|
|
90
95
|
key={page.slug}
|
|
91
96
|
label={page.title}
|
|
92
|
-
badge={
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
badge={
|
|
98
|
+
page.badge === undefined ? undefined : (
|
|
99
|
+
<span className="rounded border border-border/60 px-1 font-mono text-[0.625rem] leading-tight text-muted-foreground/60">
|
|
100
|
+
{page.badge}
|
|
101
|
+
</span>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
97
104
|
active={page.slug === activeSlug}
|
|
98
105
|
onClick={() => onSelect(page.slug)}
|
|
99
106
|
/>
|
|
100
|
-
)
|
|
107
|
+
);
|
|
101
108
|
|
|
102
109
|
return (
|
|
103
110
|
<nav aria-label="Navegação da documentação" className="space-y-4 p-2">
|
|
104
111
|
{sections.map((section, sectionIndex) => (
|
|
105
112
|
<div key={`${section.label}:${sectionIndex}`} className="space-y-0.5">
|
|
106
|
-
{section.label.trim() !==
|
|
113
|
+
{section.label.trim() !== "" && (
|
|
107
114
|
<SidebarGroupLabel>{section.label}</SidebarGroupLabel>
|
|
108
115
|
)}
|
|
109
116
|
{section.groups.map((group, groupIndex) => {
|
|
110
|
-
const key = `${sectionIndex}:${groupIndex}
|
|
111
|
-
if (group.pages.length === 0) return null
|
|
112
|
-
if ((group.label ??
|
|
113
|
-
return group.pages.map((page) => renderPage(page))
|
|
117
|
+
const key = `${sectionIndex}:${groupIndex}`;
|
|
118
|
+
if (group.pages.length === 0) return null;
|
|
119
|
+
if ((group.label ?? "").trim() === "") {
|
|
120
|
+
return group.pages.map((page) => renderPage(page));
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
const expanded = !closedGroups.has(key)
|
|
123
|
+
const expanded = !closedGroups.has(key);
|
|
117
124
|
return (
|
|
118
125
|
<div key={key} className="space-y-0.5">
|
|
119
126
|
<SidebarItem
|
|
@@ -129,16 +136,16 @@ function DocSidebarTree({
|
|
|
129
136
|
</SidebarTreeGroup>
|
|
130
137
|
)}
|
|
131
138
|
</div>
|
|
132
|
-
)
|
|
139
|
+
);
|
|
133
140
|
})}
|
|
134
141
|
</div>
|
|
135
142
|
))}
|
|
136
143
|
</nav>
|
|
137
|
-
)
|
|
144
|
+
);
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
export function DocBrowser({
|
|
141
|
-
basePath =
|
|
148
|
+
basePath = "/docs",
|
|
142
149
|
path: controlledPath,
|
|
143
150
|
sections = DOC_SECTIONS,
|
|
144
151
|
redirects = {},
|
|
@@ -147,46 +154,47 @@ export function DocBrowser({
|
|
|
147
154
|
// Standalone: o path vem do router da casa (`usePathname` — pathname É o estado).
|
|
148
155
|
// Controlado: o host manda, e este valor é ignorado. O hook roda sempre (regra dos
|
|
149
156
|
// hooks); o que muda é quem vence no `??`.
|
|
150
|
-
const livePath = usePathname()
|
|
151
|
-
const path = controlledPath ?? livePath
|
|
157
|
+
const livePath = usePathname();
|
|
158
|
+
const path = controlledPath ?? livePath;
|
|
152
159
|
// Default = 1ª página do 1º grupo da 1ª seção (a doc abre na 1ª página).
|
|
153
|
-
const defaultSlug = sections[0]?.groups[0]?.pages[0]?.slug ??
|
|
160
|
+
const defaultSlug = sections[0]?.groups[0]?.pages[0]?.slug ?? "";
|
|
154
161
|
// Slug = TODOS os segmentos após o basePath, unidos (rota aninhada: `/docs/guias/deploy`
|
|
155
162
|
// → `guias/deploy`). Slug flat de 1 segmento continua igual (`join` de 1 elemento = ele).
|
|
156
|
-
const rel = path.startsWith(basePath) ? path.slice(basePath.length) :
|
|
157
|
-
const requestedSlug = rel.split(
|
|
158
|
-
const slug = redirects[requestedSlug] ?? requestedSlug
|
|
159
|
-
const page = findPage(sections, slug) ?? findPage(sections, defaultSlug)
|
|
163
|
+
const rel = path.startsWith(basePath) ? path.slice(basePath.length) : "";
|
|
164
|
+
const requestedSlug = rel.split("/").filter(Boolean).join("/") || defaultSlug;
|
|
165
|
+
const slug = redirects[requestedSlug] ?? requestedSlug;
|
|
166
|
+
const page = findPage(sections, slug) ?? findPage(sections, defaultSlug);
|
|
160
167
|
|
|
161
168
|
// `navigate` emite `popstate`, então o host é notificado nos dois modos — inclusive o
|
|
162
169
|
// que ainda escuta o evento na unha, que era como este componente avisava antes.
|
|
163
|
-
const go = (to: string): void => navigate(to)
|
|
170
|
+
const go = (to: string): void => navigate(to);
|
|
164
171
|
|
|
165
172
|
useEffect(() => {
|
|
166
|
-
if (requestedSlug !== slug)
|
|
167
|
-
|
|
173
|
+
if (requestedSlug !== slug)
|
|
174
|
+
navigate(`${basePath}/${slug}`, { replace: true });
|
|
175
|
+
}, [basePath, requestedSlug, slug]);
|
|
168
176
|
|
|
169
177
|
// O title da aba acompanha a página atual (quando o host fornece o formato).
|
|
170
178
|
useEffect(() => {
|
|
171
|
-
if (page && documentTitle) document.title = documentTitle(page.title)
|
|
172
|
-
}, [page, documentTitle])
|
|
179
|
+
if (page && documentTitle) document.title = documentTitle(page.title);
|
|
180
|
+
}, [page, documentTitle]);
|
|
173
181
|
|
|
174
182
|
return (
|
|
175
183
|
<Split className="h-full">
|
|
176
|
-
<Pane initialSize="14rem"
|
|
184
|
+
<Pane initialSize="14rem">
|
|
177
185
|
<Sidebar className="w-full">
|
|
178
|
-
<
|
|
186
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-3 py-6">
|
|
179
187
|
<DocSidebarTree
|
|
180
188
|
sections={sections}
|
|
181
189
|
activeSlug={page?.slug}
|
|
182
190
|
onSelect={(nextSlug) => go(`${basePath}/${nextSlug}`)}
|
|
183
191
|
/>
|
|
184
|
-
</
|
|
192
|
+
</div>
|
|
185
193
|
</Sidebar>
|
|
186
194
|
</Pane>
|
|
187
|
-
<Pane key={page?.slug} grow
|
|
195
|
+
<Pane key={page?.slug} grow className="overflow-y-auto">
|
|
188
196
|
<div className="mx-auto max-w-3xl px-8 py-8">{page?.render()}</div>
|
|
189
197
|
</Pane>
|
|
190
198
|
</Split>
|
|
191
|
-
)
|
|
199
|
+
);
|
|
192
200
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
## Formulário em uma seção
|
|
2
2
|
|
|
3
3
|
Use `ActionFormCard` quando o formulário ocupar uma seção delimitada da página, ao lado de outros
|
|
4
|
-
blocos. O componente aplica a estrutura e o espaçamento de [`
|
|
4
|
+
blocos. O componente aplica a estrutura e o espaçamento de [`Surface`](/ui/surface) ao
|
|
5
5
|
[`ActionForm`](/ui/action-form), sem a faixa de ações de um modal. O título é opcional e segue a
|
|
6
6
|
mesma regra de um formulário modal: nomeia a operação e o recurso, como `Criar workspace`, enquanto
|
|
7
7
|
a ação principal mantém `Salvar`.
|
|
@@ -16,7 +16,7 @@ As ações ficam alinhadas no fim do rodapé e preservam a largura do conteúdo.
|
|
|
16
16
|
acrescenta `Cancelar`, os dois botões formam um `ButtonGroup` espaçado. Para interromper o fluxo
|
|
17
17
|
com um modal, use [`ActionFormDialog`](/ui/action-form-dialog).
|
|
18
18
|
|
|
19
|
-
O título do card é um heading `h3`, o nível padrão de `
|
|
19
|
+
O título do card é um heading `h3`, o nível padrão de `SurfaceTitle`, adequado a uma seção dentro de
|
|
20
20
|
uma página que já tem o próprio título. Quando o card ocupar outro lugar na hierarquia — por
|
|
21
21
|
exemplo, a seção principal logo abaixo do `h1` da página —, declare `titleLevel={2}`. O nível muda
|
|
22
22
|
só a estrutura do documento, que leitores de tela usam para navegar; a aparência do título continua
|
|
@@ -24,11 +24,11 @@ a mesma.
|
|
|
24
24
|
|
|
25
25
|
## Propriedades de ActionFormCard
|
|
26
26
|
|
|
27
|
-
| Propriedade
|
|
28
|
-
|
|
|
29
|
-
| `title`
|
|
30
|
-
| `titleLevel`
|
|
31
|
-
| `description`
|
|
32
|
-
| `cardClassName`
|
|
33
|
-
| `className`
|
|
27
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
28
|
+
| ------------------- | ---------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `title` | `string` | | Título do cabeçalho. Sem ele e sem `description`, o card começa diretamente pelo corpo. |
|
|
30
|
+
| `titleLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `3` | Nível do heading do título. Muda a hierarquia do documento, não a escala visual. |
|
|
31
|
+
| `description` | `string` | | Texto de apoio abaixo do título. |
|
|
32
|
+
| `cardClassName` | `string` | | Classes aplicadas à superfície do card. |
|
|
33
|
+
| `className` | `string` | | Classes aplicadas ao `<form>` interno. |
|
|
34
34
|
| Demais propriedades | Propriedades de `ActionForm` | | `action`, `defaultValues`, `fieldOptions`, `submitLabel`, `cancelLabel`, `cancelVariant`, `onCancel`, `onSuccess`, `disabled`, `onLoadingChange` e `children` mantêm o comportamento de `ActionForm`. `body` e `footer` não são aceitas: o card fornece o corpo e o rodapé. |
|
|
@@ -34,18 +34,19 @@ render(
|
|
|
34
34
|
|
|
35
35
|
## Coleção nomeada
|
|
36
36
|
|
|
37
|
-
Em uma página, envolva a listagem com [`
|
|
37
|
+
Em uma página, envolva a listagem com [`Surface`](/ui/surface). O título e a criação ocupam
|
|
38
38
|
extremos opostos do cabeçalho da coleção; busca, filtros e atualização permanecem na toolbar do
|
|
39
39
|
`ActionList`.
|
|
40
40
|
|
|
41
41
|
```tsx
|
|
42
|
-
<
|
|
42
|
+
<Surface
|
|
43
|
+
as="section"
|
|
43
44
|
title="Workspaces"
|
|
44
45
|
level={1}
|
|
45
46
|
actions={<Button>Criar workspace</Button>}
|
|
46
47
|
>
|
|
47
48
|
<ActionList action={workspaceList} input={{}} />
|
|
48
|
-
</
|
|
49
|
+
</Surface>
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
Use `toolbarActions` somente quando a operação depender do recorte visível, como exportar os
|
|
@@ -94,12 +95,11 @@ ao modal “Filtros”; `external` mantém o filtro no estado navegável e no in
|
|
|
94
95
|
apresentação para o consumidor, como cards de status acima da lista. No modal, filtros com a mesma
|
|
95
96
|
`section` formam um grupo, e `advancedFilters.columns` distribui os grupos em até três colunas.
|
|
96
97
|
Use `advancedFilters.sectionIcons` quando os grupos representarem dimensões recorrentes; a chave é
|
|
97
|
-
o mesmo rótulo de `section`, e cada grupo usa `
|
|
98
|
-
nativa. O ícone ocupa a mídia do grupo
|
|
99
|
-
glifo
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
a legenda à borda sem atribuir esse design à semântica de `fieldset`.
|
|
98
|
+
o mesmo rótulo de `section`, e cada grupo usa `Surface as="fieldset"` para manter uma legenda
|
|
99
|
+
nativa. O ícone ocupa a mídia do grupo antes do título. Passe um `IconTile` quando quiser moldura;
|
|
100
|
+
um glifo solto permanece sem decoração, e um ícone `null` não desenha nada. Quando os grupos
|
|
101
|
+
precisarem de limites mais explícitos, `advancedFilters.sectionVariant: 'framed'` acrescenta a
|
|
102
|
+
borda da `Surface` sem mudar a semântica do `fieldset`.
|
|
103
103
|
Um filtro `select` pode declarar `searchable: true` quando a lista fechada for longa; a opção geral
|
|
104
104
|
continua disponível e participa da busca. Campos de data avançados usam calendário. A antiga `advanced: true` continua aceita durante a
|
|
105
105
|
migração e equivale a `placement: 'advanced'`.
|
|
@@ -229,7 +229,7 @@ Por padrão, a lista cresce com os resultados e a rolagem acontece na janela ou
|
|
|
229
229
|
hospeda. Quando a coleção é a região principal de uma tela de altura fixa, a barra de filtros e o
|
|
230
230
|
rodapé podem continuar visíveis enquanto só os resultados rolam. Para isso, declare
|
|
231
231
|
`layout="fill"` na [`Page`](/ui/page) e componha a lista com `PageBody` e
|
|
232
|
-
[`
|
|
232
|
+
[`Surface`](/ui/surface), como no exemplo abaixo. Role a tabela para ver o cabeçalho fixo.
|
|
233
233
|
|
|
234
234
|
```tsx preview col
|
|
235
235
|
render(
|
|
@@ -237,17 +237,17 @@ render(
|
|
|
237
237
|
<div className="h-80 w-full overflow-x-auto overflow-y-hidden rounded-lg border border-border">
|
|
238
238
|
<Page layout="fill">
|
|
239
239
|
<PageBody>
|
|
240
|
-
<
|
|
241
|
-
<
|
|
242
|
-
<
|
|
243
|
-
<
|
|
240
|
+
<Surface as="section" level={1}>
|
|
241
|
+
<SurfaceHeader>
|
|
242
|
+
<SurfaceTitle>Workspaces</SurfaceTitle>
|
|
243
|
+
<SurfaceActions>
|
|
244
244
|
<Button>Criar workspace</Button>
|
|
245
|
-
</
|
|
246
|
-
</
|
|
247
|
-
<
|
|
245
|
+
</SurfaceActions>
|
|
246
|
+
</SurfaceHeader>
|
|
247
|
+
<SurfaceBody>
|
|
248
248
|
<ActionList action={docWorkspaceList} input={{}} />
|
|
249
|
-
</
|
|
250
|
-
</
|
|
249
|
+
</SurfaceBody>
|
|
250
|
+
</Surface>
|
|
251
251
|
</PageBody>
|
|
252
252
|
</Page>
|
|
253
253
|
</div>
|
|
@@ -256,7 +256,7 @@ render(
|
|
|
256
256
|
```
|
|
257
257
|
|
|
258
258
|
A página limita a própria altura à do contêiner que a hospeda, normalmente o shell, e `PageBody`,
|
|
259
|
-
`
|
|
259
|
+
`Surface` e `SurfaceBody` repassam essa altura até a lista, que passa a usar `layout="fill"` sem que
|
|
260
260
|
você o declare. Em uma [`Presentation`](/ui/presentation) de lista, basta `layout="fill"` no
|
|
261
261
|
próprio `Presentation`. Só as regiões que contêm a lista mudam: uma seção irmã, como um resumo acima
|
|
262
262
|
da coleção, mantém a altura do próprio conteúdo. Os demais filhos de cada região que repassa a
|
|
@@ -269,16 +269,16 @@ rola, sobre a cor da superfície e com uma linha inferior, e a moldura dos estad
|
|
|
269
269
|
erro e vazio ocupa a altura disponível. Em outras visualizações e em `children`, a rolagem interna
|
|
270
270
|
continua, mas não existe cabeçalho fixo.
|
|
271
271
|
|
|
272
|
-
A lista só assume `fill` sozinha quando é filha direta de `PageBody`, `
|
|
272
|
+
A lista só assume `fill` sozinha quando é filha direta de `PageBody`, `SurfaceBody` ou do corpo de
|
|
273
273
|
uma `Presentation`, e a altura só chega até ela se não houver um contêiner próprio entre esses
|
|
274
274
|
componentes no caminho. Nos demais casos, a decisão é sua:
|
|
275
275
|
|
|
276
276
|
- Para a lista manter a altura dos resultados, como em uma seção secundária cuja rolagem pertence ao
|
|
277
277
|
`PageBody`, declare `layout="flow"`.
|
|
278
|
-
- Um contêiner próprio entre `
|
|
278
|
+
- Um contêiner próprio entre `SurfaceBody` e a lista, como um `TabsPanel`, não repassa a altura, e
|
|
279
279
|
a lista volta a `flow`. Declare `layout="fill"` na lista e dê ao contêiner as classes
|
|
280
280
|
`flex min-h-0 flex-1 flex-col`.
|
|
281
|
-
- Dentro de [`
|
|
281
|
+
- Dentro de [`Surface`](/ui/surface), [`Dialog`](/ui/dialog), [`Drawer`](/ui/drawer) ou
|
|
282
282
|
[`Pane`](/ui/pane), a lista não herda o `fill` da página: a altura do corpo pertence à
|
|
283
283
|
superfície.
|
|
284
284
|
|
|
@@ -335,17 +335,20 @@ render(
|
|
|
335
335
|
render: (workspaces: Workspace[]) => (
|
|
336
336
|
<div className="grid grid-cols-2 gap-3">
|
|
337
337
|
{workspaces.map((w) => (
|
|
338
|
-
<
|
|
339
|
-
<
|
|
340
|
-
<
|
|
341
|
-
<
|
|
338
|
+
<Surface framed elevation="raised" key={w.id}>
|
|
339
|
+
<SurfaceHeader>
|
|
340
|
+
<SurfaceTitle>{w.name}</SurfaceTitle>
|
|
341
|
+
<SurfaceDescription>
|
|
342
342
|
{w.agents === 1 ? "1 agente" : `${w.agents} agentes`}
|
|
343
|
-
</
|
|
344
|
-
<
|
|
345
|
-
<DictionaryValue
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
343
|
+
</SurfaceDescription>
|
|
344
|
+
<SurfaceActions>
|
|
345
|
+
<DictionaryValue
|
|
346
|
+
dict={docWorkspaceStatus}
|
|
347
|
+
value={w.status}
|
|
348
|
+
/>
|
|
349
|
+
</SurfaceActions>
|
|
350
|
+
</SurfaceHeader>
|
|
351
|
+
</Surface>
|
|
349
352
|
))}
|
|
350
353
|
</div>
|
|
351
354
|
),
|
|
@@ -364,8 +367,8 @@ render(
|
|
|
364
367
|
- **`depends: ['outroFiltro']`:** desabilita o filtro até que suas dependências tenham valor e o
|
|
365
368
|
limpa quando uma delas muda.
|
|
366
369
|
- **`presentation: 'toggle'`:** mantém visíveis as opções curtas de um `select` de escolha única;
|
|
367
|
-
estado, URL e input da action continuam iguais
|
|
368
|
-
com `icon` apresentam o ícone junto ao rótulo.
|
|
370
|
+
o `ToggleGroup` usa `mode="segmented"`, enquanto estado, URL e input da action continuam iguais
|
|
371
|
+
aos do controle padrão. Entradas de dicionário com `icon` apresentam o ícone junto ao rótulo.
|
|
369
372
|
- **`options: { kind: 'dictionary', ref: 'sessionStatus' }`:** resolve opções e rótulos por um
|
|
370
373
|
dicionário registrado no `OpusProvider`. `filterOptions` substitui essa fonte quando informado.
|
|
371
374
|
- **`options: { kind: 'lookup', source: 'x.lookup' }`:** consulta uma action com `{ q }` e os valores
|
|
@@ -442,7 +445,10 @@ render(
|
|
|
442
445
|
<ItemDescription>Cliente {w.client}.</ItemDescription>
|
|
443
446
|
</ItemHeader>
|
|
444
447
|
<ItemActions>
|
|
445
|
-
<DictionaryValue
|
|
448
|
+
<DictionaryValue
|
|
449
|
+
dict={docWorkspaceStatus}
|
|
450
|
+
value={w.status}
|
|
451
|
+
/>
|
|
446
452
|
</ItemActions>
|
|
447
453
|
</Item>
|
|
448
454
|
</React.Fragment>
|
|
@@ -475,13 +481,13 @@ render(
|
|
|
475
481
|
|
|
476
482
|
## Declaração na ListAction
|
|
477
483
|
|
|
478
|
-
| Chave | O que declara
|
|
479
|
-
| --------- |
|
|
480
|
-
| `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.
|
|
484
|
+
| Chave | O que declara |
|
|
485
|
+
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
486
|
+
| `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. |
|
|
481
487
|
| `filters` | `{ [nome]: { label, type, options?, multiple?, presentation?, 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. `presentation: 'toggle'` mostra todas as opções curtas de um `select` de escolha única em um `ToggleGroup`; sem a propriedade, o controle continua sendo `Select`. `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. |
|
|
482
|
-
| `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.
|
|
483
|
-
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`.
|
|
484
|
-
| `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.
|
|
488
|
+
| `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. |
|
|
489
|
+
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
490
|
+
| `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. |
|
|
485
491
|
|
|
486
492
|
## useListAction
|
|
487
493
|
|
|
@@ -493,31 +499,31 @@ chamar a action.
|
|
|
493
499
|
|
|
494
500
|
## Propriedades de ActionList
|
|
495
501
|
|
|
496
|
-
| Propriedade | Tipo | Padrão
|
|
497
|
-
| ----------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
498
|
-
| `action` | `ListActionLike` |
|
|
499
|
-
| `input` | `TInput` |
|
|
500
|
-
| `layout` | `'flow' \| 'fill'` | `'fill'` como filha direta de `PageBody`, `
|
|
501
|
-
| `cells` | `Record<string, (item) => ReactNode>` |
|
|
502
|
-
| `filterOptions` | `Record<string, SelectOption[]>` |
|
|
503
|
-
| `filterDefaults` | `Record<string, string>` |
|
|
504
|
-
| `filterIcons` | `Record<string, ReactNode>` |
|
|
505
|
-
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3; sectionIcons?: Record<string, ReactNode>; sectionVariant?: 'plain' \| 'framed' }` | `{ columns: 1, sectionVariant: 'plain' }`
|
|
506
|
-
| `periodPosition` | `'before' \| 'after' \| { afterFilter: string }` | `'before'`
|
|
507
|
-
| `period` | `{ basisFilter: string }` |
|
|
508
|
-
| `views` | `Record<string, ActionListView<TItem>>` |
|
|
509
|
-
| `beforeContent` | `ReactNode` |
|
|
510
|
-
| `columns` | `ActionListColumn<TItem>[]` |
|
|
511
|
-
| `children` | `(items, refetch) => ReactNode` |
|
|
512
|
-
| `batch` | `ActionListBatchAction[]` |
|
|
513
|
-
| `rowId` | `(item) => string` | `item.id`
|
|
514
|
-
| `pageSize` | `number` | `50`
|
|
515
|
-
| `state / onStateChange` | `ActionListState / (next) => void` | interno
|
|
516
|
-
| `empty` | `(items) => boolean` | `items.length === 0`
|
|
517
|
-
| `loading` | `boolean` |
|
|
518
|
-
| `emptyMessage` | `string` | `'Nenhum resultado.'`
|
|
519
|
-
| `errorMessage` | `string` | `'Não foi possível carregar.'`
|
|
520
|
-
| `retryLabel` | `string` | `'Tentar de novo'`
|
|
521
|
-
| `onRowClick` | `(item: TItem) => void` |
|
|
522
|
-
| `toolbarActions` | `ReactNode` |
|
|
523
|
-
| `rowActions` | `(item: TItem) => ReactNode` |
|
|
502
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
503
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
504
|
+
| `action` | `ListActionLike` | | A ListAction do Opus (kind list, output = item). A paginação envia `limit`/`page` e usa o `total` devolvido. |
|
|
505
|
+
| `input` | `TInput` | | O escopo base, como `{ workspaceId }`. A toolbar acrescenta busca, filtros e paginação sem sobrescrever essas chaves. |
|
|
506
|
+
| `layout` | `'flow' \| 'fill'` | `'fill'` como filha direta de `PageBody`, `SurfaceBody` ou do corpo da `Presentation` em uma página `fill`; `'flow'` nos demais casos | Em `fill`, ocupa a altura de um ancestral limitado e rola somente a região de resultados; na tabela, o cabeçalho fica fixo. Sem ancestral limitado, equivale a `flow`. |
|
|
507
|
+
| `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
|
|
508
|
+
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para os filtros select/lookup (chave = nome do filtro). |
|
|
509
|
+
| `filterDefaults` | `Record<string, string>` | | Valores de repouso aplicados à consulta; o `X` só aparece quando o filtro diverge deles e retorna a eles. |
|
|
510
|
+
| `filterIcons` | `Record<string, ReactNode>` | | Ícones leading dos filtros, indexados pelo nome declarado no contrato. |
|
|
511
|
+
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3; sectionIcons?: Record<string, ReactNode>; sectionVariant?: 'plain' \| 'framed' }` | `{ columns: 1, sectionVariant: 'plain' }` | Define as colunas, os ícones e o tratamento dos grupos; o Opus deriva a largura correspondente do diálogo. |
|
|
512
|
+
| `periodPosition` | `'before' \| 'after' \| { afterFilter: string }` | `'before'` | Posiciona o período antes, depois ou imediatamente após um filtro inline. |
|
|
513
|
+
| `period` | `{ basisFilter: string }` | | Reúne o período e o filtro inline indicado em um único controle. |
|
|
514
|
+
| `views` | `Record<string, ActionListView<TItem>>` | | Visualizações alternativas `{ label, icon?, render }`; o seletor alterna entre a tabela e estas. |
|
|
515
|
+
| `beforeContent` | `ReactNode` | | Conteúdo entre a toolbar e o corpo, como um resumo que reage ao recorte atual. |
|
|
516
|
+
| `columns` | `ActionListColumn<TItem>[]` | | Tabela explícita, que substitui as colunas declaradas no contrato. |
|
|
517
|
+
| `children` | `(items, refetch) => ReactNode` | | Modo composição: layout livre; a toolbar segue. Tem precedência sobre columns. |
|
|
518
|
+
| `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?: { title, message? }, destructive? }` — liga a multi-seleção. |
|
|
519
|
+
| `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
|
|
520
|
+
| `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. |
|
|
521
|
+
| `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
|
|
522
|
+
| `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado, por exemplo quando um formulário inline aberto conta como conteúdo. |
|
|
523
|
+
| `loading` | `boolean` | | Carga extra agregada à da consulta, como uma query irmã de que os `children` dependem. |
|
|
524
|
+
| `emptyMessage` | `string` | `'Nenhum resultado.'` | Frase do estado vazio (o `Empty` do `DataState`). |
|
|
525
|
+
| `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
|
|
526
|
+
| `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
|
|
527
|
+
| `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
|
|
528
|
+
| `toolbarActions` | `ReactNode` | | Operações ligadas ao recorte atual, no fim da barra e separadas do grupo de controles. Os nós são renderizados como recebidos: nome acessível e tooltip de uma ação somente com ícone ficam com o consumidor. |
|
|
529
|
+
| `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita). A partir de duas, entram em um grupo com intervalo compacto; uma só fica solta, sem grupo. Cliques ali não disparam o `onRowClick`. |
|