@softize/opus 27.1.0 → 28.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/bin/lib/check.mjs +139 -30
- package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
- package/docs/adr/README.md +2 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
- package/src/ui/components/patterns/list.tsx +62 -53
- package/src/ui/components/patterns/page.tsx +146 -38
- package/src/ui/components/patterns/presentation.tsx +44 -59
- package/src/ui/components/patterns/structural-header.tsx +42 -6
- package/src/ui/components/patterns/surface-anatomy.ts +52 -22
- package/src/ui/components/patterns/surface.tsx +2 -3
- package/src/ui/components/primitives/control.ts +2 -2
- package/src/ui/components/primitives/dialog.tsx +7 -2
- package/src/ui/components/primitives/drawer.tsx +7 -2
- package/src/ui/components/primitives/icon-picker.tsx +271 -34
- package/src/ui/components/primitives/pagination.tsx +5 -4
- package/src/ui/components/primitives/tabs.tsx +15 -2
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/docs/content/action-list.md +11 -2
- package/src/ui/docs/content/button.md +7 -7
- package/src/ui/docs/content/compositions.md +35 -23
- package/src/ui/docs/content/dialog.md +18 -6
- package/src/ui/docs/content/drawer.md +17 -5
- package/src/ui/docs/content/icon-picker.md +37 -19
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/page.md +169 -69
- package/src/ui/docs/content/pagination.md +7 -8
- package/src/ui/docs/content/presentation.md +4 -3
- package/src/ui/docs/content/surface.md +8 -5
- package/src/ui/docs/content/tabs.md +12 -4
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +9 -1
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
Briefcase,
|
|
8
8
|
Building2,
|
|
9
9
|
Calendar,
|
|
10
|
+
Car,
|
|
10
11
|
ChartBar,
|
|
11
12
|
ChartLine,
|
|
12
13
|
ChartPie,
|
|
13
|
-
Check,
|
|
14
14
|
ChevronsUpDown,
|
|
15
15
|
ClipboardList,
|
|
16
16
|
Clock,
|
|
@@ -71,6 +71,7 @@ export const iconPickerIcons: Record<string, LucideIcon> = {
|
|
|
71
71
|
briefcase: Briefcase,
|
|
72
72
|
building: Building2,
|
|
73
73
|
calendar: Calendar,
|
|
74
|
+
car: Car,
|
|
74
75
|
"chart-bar": ChartBar,
|
|
75
76
|
"chart-line": ChartLine,
|
|
76
77
|
"chart-pie": ChartPie,
|
|
@@ -110,12 +111,111 @@ export const iconPickerIcons: Record<string, LucideIcon> = {
|
|
|
110
111
|
zap: Zap,
|
|
111
112
|
};
|
|
112
113
|
|
|
114
|
+
/** Rótulos e termos de busca separados dos identificadores persistidos. */
|
|
115
|
+
export const iconPickerSearch: Record<
|
|
116
|
+
string,
|
|
117
|
+
{ label: string; keywords: string[] }
|
|
118
|
+
> = {
|
|
119
|
+
car: { label: "Carro", keywords: ["veículo", "automóvel", "vendas"] },
|
|
120
|
+
activity: { label: "Atividade", keywords: ["pulso", "saúde"] },
|
|
121
|
+
banknote: { label: "Dinheiro", keywords: ["nota", "pagamento", "valor"] },
|
|
122
|
+
bell: { label: "Sino", keywords: ["alerta", "notificação"] },
|
|
123
|
+
box: { label: "Caixa", keywords: ["estoque", "produto"] },
|
|
124
|
+
briefcase: { label: "Maleta", keywords: ["trabalho", "negócio"] },
|
|
125
|
+
building: { label: "Edifício", keywords: ["empresa", "unidade", "prédio"] },
|
|
126
|
+
calendar: { label: "Calendário", keywords: ["agenda", "data", "evento"] },
|
|
127
|
+
"chart-bar": {
|
|
128
|
+
label: "Gráfico de barras",
|
|
129
|
+
keywords: ["relatório", "análise"],
|
|
130
|
+
},
|
|
131
|
+
"chart-line": {
|
|
132
|
+
label: "Gráfico de linhas",
|
|
133
|
+
keywords: ["evolução", "relatório"],
|
|
134
|
+
},
|
|
135
|
+
"chart-pie": {
|
|
136
|
+
label: "Gráfico de pizza",
|
|
137
|
+
keywords: ["percentual", "relatório"],
|
|
138
|
+
},
|
|
139
|
+
"clipboard-list": {
|
|
140
|
+
label: "Prancheta",
|
|
141
|
+
keywords: ["tarefas", "lista", "checklist"],
|
|
142
|
+
},
|
|
143
|
+
clock: { label: "Relógio", keywords: ["hora", "tempo", "prazo"] },
|
|
144
|
+
"credit-card": { label: "Cartão", keywords: ["crédito", "pagamento"] },
|
|
145
|
+
database: { label: "Banco de dados", keywords: ["base", "registros"] },
|
|
146
|
+
"file-text": { label: "Documento", keywords: ["página", "texto", "arquivo"] },
|
|
147
|
+
flag: { label: "Bandeira", keywords: ["marco", "sinalização"] },
|
|
148
|
+
folder: { label: "Pasta", keywords: ["diretório", "arquivos"] },
|
|
149
|
+
gauge: { label: "Medidor", keywords: ["velocidade", "desempenho"] },
|
|
150
|
+
globe: { label: "Globo", keywords: ["mundo", "público", "internet"] },
|
|
151
|
+
heart: { label: "Coração", keywords: ["favorito", "saúde"] },
|
|
152
|
+
layers: { label: "Camadas", keywords: ["coleção", "conjunto"] },
|
|
153
|
+
"layout-grid": { label: "Grade", keywords: ["cartões", "painel"] },
|
|
154
|
+
lightbulb: { label: "Lâmpada", keywords: ["ideia", "sugestão"] },
|
|
155
|
+
mail: {
|
|
156
|
+
label: "Envelope",
|
|
157
|
+
keywords: ["email", "e-mail", "correio", "mensagem"],
|
|
158
|
+
},
|
|
159
|
+
"map-pin": { label: "Localização", keywords: ["endereço", "mapa", "lugar"] },
|
|
160
|
+
"message-circle": {
|
|
161
|
+
label: "Conversa",
|
|
162
|
+
keywords: ["chat", "mensagem", "atendimento"],
|
|
163
|
+
},
|
|
164
|
+
"message-square": { label: "Mensagem", keywords: ["chat", "comentário"] },
|
|
165
|
+
package: { label: "Pacote", keywords: ["entrega", "produto", "encomenda"] },
|
|
166
|
+
phone: { label: "Telefone", keywords: ["ligação", "contato"] },
|
|
167
|
+
rocket: { label: "Foguete", keywords: ["lançamento", "inovação"] },
|
|
168
|
+
settings: { label: "Configurações", keywords: ["ajustes", "engrenagem"] },
|
|
169
|
+
"shopping-cart": { label: "Carrinho", keywords: ["compra", "venda"] },
|
|
170
|
+
star: { label: "Estrela", keywords: ["favorito", "destaque"] },
|
|
171
|
+
store: { label: "Loja", keywords: ["comércio", "unidade"] },
|
|
172
|
+
table: { label: "Tabela", keywords: ["planilha", "catálogo"] },
|
|
173
|
+
target: { label: "Alvo", keywords: ["meta", "objetivo"] },
|
|
174
|
+
"trending-down": { label: "Queda", keywords: ["redução", "tendência"] },
|
|
175
|
+
"trending-up": { label: "Crescimento", keywords: ["aumento", "tendência"] },
|
|
176
|
+
truck: {
|
|
177
|
+
label: "Caminhão",
|
|
178
|
+
keywords: ["transporte", "veículo", "logística"],
|
|
179
|
+
},
|
|
180
|
+
user: { label: "Pessoa", keywords: ["usuário", "cliente"] },
|
|
181
|
+
users: { label: "Pessoas", keywords: ["equipe", "grupo", "clientes"] },
|
|
182
|
+
wallet: { label: "Carteira", keywords: ["saldo", "financeiro"] },
|
|
183
|
+
wrench: { label: "Ferramenta", keywords: ["chave", "manutenção", "serviço"] },
|
|
184
|
+
zap: { label: "Raio", keywords: ["automação", "energia", "fluxo"] },
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export interface IconCatalogEntry {
|
|
188
|
+
name: string;
|
|
189
|
+
label: string;
|
|
190
|
+
keywords: string[];
|
|
191
|
+
}
|
|
192
|
+
export interface IconCatalog {
|
|
193
|
+
load: () => Promise<IconCatalogEntry[]>;
|
|
194
|
+
render: (name: string, className: string) => React.ReactNode;
|
|
195
|
+
}
|
|
196
|
+
const IconCatalogContext = React.createContext<IconCatalog | null>(null);
|
|
197
|
+
export function IconCatalogProvider({
|
|
198
|
+
catalog,
|
|
199
|
+
children,
|
|
200
|
+
}: {
|
|
201
|
+
catalog: IconCatalog;
|
|
202
|
+
children: React.ReactNode;
|
|
203
|
+
}) {
|
|
204
|
+
return (
|
|
205
|
+
<IconCatalogContext.Provider value={catalog}>
|
|
206
|
+
{children}
|
|
207
|
+
</IconCatalogContext.Provider>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
113
211
|
export interface IconPickerProps {
|
|
114
212
|
/** Nome do ícone selecionado (chave da paleta) — '' = nenhum. */
|
|
115
213
|
value: string;
|
|
116
214
|
onChange: (name: string) => void;
|
|
117
215
|
/** Paleta nome → componente. Default: `iconPickerIcons` (curada). */
|
|
118
216
|
icons?: Record<string, LucideIcon>;
|
|
217
|
+
/** Rótulos e sinônimos para busca; não alteram o valor persistido. */
|
|
218
|
+
searchTerms?: typeof iconPickerSearch;
|
|
119
219
|
placeholder?: string;
|
|
120
220
|
disabled?: boolean;
|
|
121
221
|
className?: string;
|
|
@@ -133,7 +233,8 @@ export interface IconPickerProps {
|
|
|
133
233
|
export function IconPicker({
|
|
134
234
|
value,
|
|
135
235
|
onChange,
|
|
136
|
-
icons
|
|
236
|
+
icons: providedIcons,
|
|
237
|
+
searchTerms = iconPickerSearch,
|
|
137
238
|
placeholder = "Selecione um ícone…",
|
|
138
239
|
disabled,
|
|
139
240
|
className,
|
|
@@ -141,11 +242,80 @@ export function IconPicker({
|
|
|
141
242
|
"aria-invalid": ariaInvalid,
|
|
142
243
|
"aria-describedby": ariaDescribedBy,
|
|
143
244
|
}: IconPickerProps): React.ReactElement {
|
|
245
|
+
const contextCatalog = React.useContext(IconCatalogContext);
|
|
246
|
+
const catalog = providedIcons ? null : contextCatalog;
|
|
247
|
+
const icons = providedIcons ?? iconPickerIcons;
|
|
248
|
+
const [entries, setEntries] = React.useState<IconCatalogEntry[] | null>(null);
|
|
249
|
+
const [loadError, setLoadError] = React.useState(false);
|
|
250
|
+
const [attempt, setAttempt] = React.useState(0);
|
|
251
|
+
const [query, setQuery] = React.useState("");
|
|
252
|
+
const [limit, setLimit] = React.useState(64);
|
|
253
|
+
const listRef = React.useRef<HTMLDivElement>(null);
|
|
144
254
|
const [open, setOpen] = React.useState(false);
|
|
145
|
-
const Current = value
|
|
255
|
+
const Current = Object.hasOwn(icons, value) ? icons[value] : undefined;
|
|
256
|
+
const [active, setActive] = React.useState("");
|
|
257
|
+
const loadedCatalog = React.useRef<typeof catalog>(null);
|
|
258
|
+
React.useEffect(() => {
|
|
259
|
+
if (!open || !catalog || loadedCatalog.current === catalog) return;
|
|
260
|
+
setEntries(null);
|
|
261
|
+
let cancelled = false;
|
|
262
|
+
setLoadError(false);
|
|
263
|
+
catalog
|
|
264
|
+
.load()
|
|
265
|
+
.then((items) => {
|
|
266
|
+
if (!cancelled) {
|
|
267
|
+
loadedCatalog.current = catalog;
|
|
268
|
+
setEntries(items);
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
.catch(() => {
|
|
272
|
+
if (!cancelled) setLoadError(true);
|
|
273
|
+
});
|
|
274
|
+
return () => {
|
|
275
|
+
cancelled = true;
|
|
276
|
+
};
|
|
277
|
+
}, [open, catalog, attempt]);
|
|
278
|
+
const normalize = (text: string) =>
|
|
279
|
+
text
|
|
280
|
+
.normalize("NFD")
|
|
281
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
282
|
+
.toLocaleLowerCase("pt-BR");
|
|
283
|
+
const available = catalog
|
|
284
|
+
? loadedCatalog.current === catalog
|
|
285
|
+
? (entries ?? [])
|
|
286
|
+
: []
|
|
287
|
+
: Object.keys(icons).map((name) => ({
|
|
288
|
+
name,
|
|
289
|
+
label: searchTerms[name]?.label ?? name,
|
|
290
|
+
keywords: searchTerms[name]?.keywords ?? [],
|
|
291
|
+
}));
|
|
292
|
+
const filtered = available.filter((entry) =>
|
|
293
|
+
normalize(query)
|
|
294
|
+
.trim()
|
|
295
|
+
.split(/\s+/)
|
|
296
|
+
.every((term) =>
|
|
297
|
+
normalize(
|
|
298
|
+
[entry.name, entry.label, ...entry.keywords].join(" "),
|
|
299
|
+
).includes(term),
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
const renderIcon = (name: string, className: string) => {
|
|
303
|
+
if (catalog) return catalog.render(name, className);
|
|
304
|
+
const Icon = Object.hasOwn(icons, name) ? icons[name] : undefined;
|
|
305
|
+
return Icon ? <Icon className={className} aria-hidden="true" /> : null;
|
|
306
|
+
};
|
|
146
307
|
|
|
147
308
|
return (
|
|
148
|
-
<Popover
|
|
309
|
+
<Popover
|
|
310
|
+
open={open}
|
|
311
|
+
onOpenChange={(next) => {
|
|
312
|
+
setOpen(next);
|
|
313
|
+
if (next) {
|
|
314
|
+
setQuery("");
|
|
315
|
+
setLimit(64);
|
|
316
|
+
}
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
149
319
|
<PopoverTrigger asChild>
|
|
150
320
|
<button
|
|
151
321
|
type="button"
|
|
@@ -164,11 +334,13 @@ export function IconPicker({
|
|
|
164
334
|
className,
|
|
165
335
|
)}
|
|
166
336
|
>
|
|
167
|
-
{Current !== undefined ? (
|
|
337
|
+
{(catalog && value) || Current !== undefined ? (
|
|
168
338
|
<>
|
|
169
|
-
|
|
170
|
-
<span data-slot="icon-picker-value" className="
|
|
171
|
-
{value
|
|
339
|
+
{renderIcon(value, "size-4 shrink-0")}
|
|
340
|
+
<span data-slot="icon-picker-value" className="sr-only">
|
|
341
|
+
{available.find((entry) => entry.name === value)?.label ??
|
|
342
|
+
searchTerms[value]?.label ??
|
|
343
|
+
"Ícone selecionado"}
|
|
172
344
|
</span>
|
|
173
345
|
</>
|
|
174
346
|
) : (
|
|
@@ -179,33 +351,98 @@ export function IconPicker({
|
|
|
179
351
|
</PopoverTrigger>
|
|
180
352
|
<PopoverContent
|
|
181
353
|
align="start"
|
|
182
|
-
className="pointer-events-auto w-
|
|
354
|
+
className="pointer-events-auto w-96 max-w-[calc(100vw-2rem)] p-0"
|
|
355
|
+
onWheel={(event) => event.stopPropagation()}
|
|
356
|
+
onTouchMove={(event) => event.stopPropagation()}
|
|
183
357
|
>
|
|
184
|
-
<Command
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
358
|
+
<Command
|
|
359
|
+
shouldFilter={false}
|
|
360
|
+
value={active}
|
|
361
|
+
onValueChange={setActive}
|
|
362
|
+
onKeyDown={(event) => {
|
|
363
|
+
const step =
|
|
364
|
+
event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
|
|
365
|
+
if (!step) return;
|
|
366
|
+
event.preventDefault();
|
|
367
|
+
const items = Array.from(
|
|
368
|
+
event.currentTarget.querySelectorAll<HTMLElement>(
|
|
369
|
+
"[cmdk-item]:not([hidden])",
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
const index = items.findIndex(
|
|
373
|
+
(item) => item.dataset.value === active,
|
|
374
|
+
);
|
|
375
|
+
const next =
|
|
376
|
+
items[Math.max(0, Math.min(items.length - 1, index + step))];
|
|
377
|
+
if (next) {
|
|
378
|
+
setActive(next.dataset.value ?? "");
|
|
379
|
+
next.scrollIntoView({ block: "nearest" });
|
|
380
|
+
}
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
<CommandInput
|
|
384
|
+
placeholder="Buscar ícone…"
|
|
385
|
+
value={query}
|
|
386
|
+
onValueChange={(next) => {
|
|
387
|
+
if (listRef.current) listRef.current.scrollTop = 0;
|
|
388
|
+
setQuery(next);
|
|
389
|
+
setLimit(64);
|
|
390
|
+
}}
|
|
391
|
+
/>
|
|
392
|
+
{catalog && !entries && !loadError ? (
|
|
393
|
+
<div role="status" className="p-4 text-sm">
|
|
394
|
+
Carregando ícones…
|
|
395
|
+
</div>
|
|
396
|
+
) : loadError ? (
|
|
397
|
+
<div role="alert" className="p-4 text-sm">
|
|
398
|
+
Não foi possível carregar os ícones.{" "}
|
|
399
|
+
<button
|
|
400
|
+
type="button"
|
|
401
|
+
className="underline"
|
|
402
|
+
onClick={() => setAttempt((value) => value + 1)}
|
|
403
|
+
>
|
|
404
|
+
Tentar novamente
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
) : null}
|
|
408
|
+
<CommandList
|
|
409
|
+
ref={listRef}
|
|
410
|
+
onScroll={(event) => {
|
|
411
|
+
const list = event.currentTarget;
|
|
412
|
+
if (
|
|
413
|
+
list.scrollHeight - list.scrollTop - list.clientHeight <=
|
|
414
|
+
list.clientHeight / 2
|
|
415
|
+
) {
|
|
416
|
+
setLimit((count) => Math.min(count + 64, filtered.length));
|
|
417
|
+
}
|
|
418
|
+
}}
|
|
419
|
+
data-slot="icon-picker-list"
|
|
420
|
+
className="max-h-60 overscroll-contain"
|
|
421
|
+
>
|
|
422
|
+
{!loadError && (!catalog || entries) ? (
|
|
423
|
+
<CommandEmpty>Nada encontrado.</CommandEmpty>
|
|
424
|
+
) : null}
|
|
425
|
+
<CommandGroup className="[&_[cmdk-group-items]]:grid [&_[cmdk-group-items]]:grid-cols-8 [&_[cmdk-group-items]]:gap-1">
|
|
426
|
+
{filtered.slice(0, limit).map(({ name, label }) => {
|
|
427
|
+
return (
|
|
428
|
+
<CommandItem
|
|
429
|
+
key={name}
|
|
430
|
+
data-slot="icon-picker-item"
|
|
431
|
+
value={name}
|
|
432
|
+
aria-label={label}
|
|
433
|
+
data-current={name === value}
|
|
434
|
+
className="h-10 justify-center p-0 data-[current=true]:ring-1 data-[current=true]:ring-inset data-[current=true]:ring-primary"
|
|
435
|
+
onSelect={() => {
|
|
436
|
+
onChange(name === value ? "" : name);
|
|
437
|
+
setOpen(false);
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
<span className="flex h-full w-full items-center justify-center">
|
|
441
|
+
{renderIcon(name, "size-5")}
|
|
442
|
+
</span>
|
|
443
|
+
</CommandItem>
|
|
444
|
+
);
|
|
445
|
+
})}
|
|
209
446
|
</CommandGroup>
|
|
210
447
|
</CommandList>
|
|
211
448
|
</Command>
|
|
@@ -3,7 +3,7 @@ import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-re
|
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
5
|
import { buttonVariants, type ButtonProps } from './button.tsx'
|
|
6
|
-
import { controlSquare, type ControlIconSize } from './control.ts'
|
|
6
|
+
import { controlBase, controlSquare, type ControlIconSize } from './control.ts'
|
|
7
7
|
|
|
8
8
|
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
|
9
9
|
return (
|
|
@@ -46,12 +46,13 @@ export interface PaginationLinkProps extends Omit<React.ComponentProps<'a'>, 'ch
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Um link de página. Altura fixa, largura mínima quadrada e largura que cresce com o
|
|
49
|
-
* número (
|
|
49
|
+
* número (largura mínima igual à altura do tamanho escolhido, dígitos tabulares): 5726 e 5727 nunca se sobrepõem. Com
|
|
50
50
|
* `href` é `<a>`; sem `href` é `<button type="button">`, que preserva foco e teclado no
|
|
51
|
-
* modo controlado. Ajuste a escala por `
|
|
51
|
+
* modo controlado. Ajuste a escala por `size`; a largura mínima acompanha a altura do degrau.
|
|
52
52
|
*/
|
|
53
53
|
function PaginationLink({ className, isActive, page, disabled, href, size = 'default', children, ...props }: PaginationLinkProps) {
|
|
54
54
|
const square = typeof size === 'string' && size.startsWith('icon')
|
|
55
|
+
const minWidth = { xs: 'min-w-6', sm: 'min-w-8', default: 'min-w-9', lg: 'min-w-10' }[controlBase(size)]
|
|
55
56
|
const shared = {
|
|
56
57
|
'aria-current': isActive ? ('page' as const) : undefined,
|
|
57
58
|
'aria-label': props['aria-label'] ?? (page !== undefined ? `Página ${page}` : undefined),
|
|
@@ -61,7 +62,7 @@ function PaginationLink({ className, isActive, page, disabled, href, size = 'def
|
|
|
61
62
|
buttonVariants({ variant: isActive ? 'outline' : 'ghost', size }),
|
|
62
63
|
// Número: a largura mínima é quadrada e o conteúdo manda no resto. Escala `icon*`
|
|
63
64
|
// fica quadrada de verdade (sem `has-[>svg]:px-*` do `default`).
|
|
64
|
-
!square && '
|
|
65
|
+
!square && cn('w-auto px-2 tabular-nums', minWidth),
|
|
65
66
|
className,
|
|
66
67
|
),
|
|
67
68
|
}
|
|
@@ -2,6 +2,10 @@ import * as React from "react";
|
|
|
2
2
|
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
3
3
|
|
|
4
4
|
import { cn } from "../../lib/cn.ts";
|
|
5
|
+
import {
|
|
6
|
+
structuralBodyRhythm,
|
|
7
|
+
structuralRegionGap,
|
|
8
|
+
} from "../patterns/surface-anatomy.ts";
|
|
5
9
|
import { controlHeight, type ControlSize } from "./control.ts";
|
|
6
10
|
|
|
7
11
|
// Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
|
|
@@ -45,8 +49,10 @@ function Tabs({
|
|
|
45
49
|
data-framed={framed}
|
|
46
50
|
orientation={orientation}
|
|
47
51
|
className={cn(
|
|
48
|
-
"group/tabs flex
|
|
52
|
+
"group/tabs flex data-[orientation=horizontal]:flex-col",
|
|
53
|
+
structuralRegionGap,
|
|
49
54
|
framed && "overflow-hidden rounded-xl border",
|
|
55
|
+
framed && orientation === "vertical" && "p-4",
|
|
50
56
|
className,
|
|
51
57
|
)}
|
|
52
58
|
{...props}
|
|
@@ -192,10 +198,17 @@ function TabsPanel({
|
|
|
192
198
|
className,
|
|
193
199
|
...props
|
|
194
200
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
201
|
+
const framed = React.useContext(TabsFramedContext);
|
|
202
|
+
const orientation = React.useContext(TabsOrientationContext);
|
|
195
203
|
return (
|
|
196
204
|
<TabsPrimitive.Content
|
|
197
205
|
data-slot="tabs-panel"
|
|
198
|
-
className={cn(
|
|
206
|
+
className={cn(
|
|
207
|
+
"flex-1 outline-none data-[state=inactive]:hidden",
|
|
208
|
+
structuralBodyRhythm,
|
|
209
|
+
framed && orientation === "horizontal" && "px-4 pb-4",
|
|
210
|
+
className,
|
|
211
|
+
)}
|
|
199
212
|
{...props}
|
|
200
213
|
/>
|
|
201
214
|
);
|
|
@@ -19,7 +19,7 @@ const toggleVariants = cva(
|
|
|
19
19
|
variant: {
|
|
20
20
|
default: "bg-transparent",
|
|
21
21
|
outline:
|
|
22
|
-
"border border-input bg-transparent text-muted-foreground hover:bg-interaction-hover hover:text-foreground active:bg-interaction-pressed data-[state=on]:bg-interaction-selected data-[state=on]:text-foreground",
|
|
22
|
+
"border border-input bg-transparent text-muted-foreground hover:bg-interaction-hover/50 hover:text-foreground active:bg-interaction-pressed data-[state=on]:bg-interaction-selected/50 data-[state=on]:text-foreground",
|
|
23
23
|
},
|
|
24
24
|
// Alturas da escala de control.ts (`sm` = o mesmo h-8 de Button e Select).
|
|
25
25
|
size: {
|
|
@@ -536,7 +536,7 @@ render(
|
|
|
536
536
|
| `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. |
|
|
537
537
|
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
538
538
|
| `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. |
|
|
539
|
-
| `grouping` | `{ fields }` — dimensões enumeráveis disponíveis em **Agrupar por**. Cada nome referencia um filtro `select` ou `lookup` com até 12 opções declaradas ou resolvidas por `filterOptions`. Não define agrupamento inicial.
|
|
539
|
+
| `grouping` | `{ fields }` — dimensões enumeráveis disponíveis em **Agrupar por**. Cada nome referencia um filtro `select` ou `lookup` com até 12 opções declaradas ou resolvidas por `filterOptions`. Não define agrupamento inicial. |
|
|
540
540
|
|
|
541
541
|
## useListAction
|
|
542
542
|
|
|
@@ -547,6 +547,14 @@ declarativo (`items`, `total`, `error`, `isLoading`, `isFetching`, `refetch`) so
|
|
|
547
547
|
chamar a action. `options.enabled` suspende a consulta enquanto for `false`; sem a opção, vale
|
|
548
548
|
`true`.
|
|
549
549
|
|
|
550
|
+
## Controles da visualização
|
|
551
|
+
|
|
552
|
+
Controles específicos da visualização ativa, como a orientação de um comparativo, usam
|
|
553
|
+
`viewControls`. O slot fica imediatamente após o seletor de visualização. As configurações da lista
|
|
554
|
+
aparecem antes desse conjunto. O consumidor controla seu estado e quando exibi-lo. Isso mantém a
|
|
555
|
+
configuração visual junto à escolha da visualização, sem deslocar ações sobre o recorte
|
|
556
|
+
para fora de `toolbarActions`.
|
|
557
|
+
|
|
550
558
|
## Propriedades de ActionList
|
|
551
559
|
|
|
552
560
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -555,7 +563,7 @@ chamar a action. `options.enabled` suspende a consulta enquanto for `false`; sem
|
|
|
555
563
|
| `input` | `TInput` | | O escopo base, como `{ workspaceId }`. A toolbar acrescenta busca, filtros e paginação sem sobrescrever essas chaves. |
|
|
556
564
|
| `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`. |
|
|
557
565
|
| `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
|
|
558
|
-
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para filtros select/lookup. Em uma dimensão de agrupamento, a coleção precisa ser completa e ter de 1 a 12 itens.
|
|
566
|
+
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para filtros select/lookup. Em uma dimensão de agrupamento, a coleção precisa ser completa e ter de 1 a 12 itens. |
|
|
559
567
|
| `filterDefaults` | `Record<string, string>` | | Valores de repouso aplicados à consulta; o `X` só aparece quando o filtro diverge deles e retorna a eles. |
|
|
560
568
|
| `filterIcons` | `Record<string, ReactNode>` | | Ícones leading dos filtros, indexados pelo nome declarado no contrato. |
|
|
561
569
|
| `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. |
|
|
@@ -575,5 +583,6 @@ chamar a action. `options.enabled` suspende a consulta enquanto for `false`; sem
|
|
|
575
583
|
| `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
|
|
576
584
|
| `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
|
|
577
585
|
| `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
|
|
586
|
+
| `viewControls` | `ReactNode` | | Controles da visualização ativa, imediatamente após o seletor de visualização. |
|
|
578
587
|
| `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. |
|
|
579
588
|
| `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`. |
|
|
@@ -32,22 +32,22 @@ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em
|
|
|
32
32
|
| Nome | Medida | Uso |
|
|
33
33
|
| --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
34
|
| `xs` | 1.5rem | Ação dentro de um campo (`InputGroupButton`). |
|
|
35
|
-
| `sm` | 2rem |
|
|
36
|
-
| `default` | 2.25rem |
|
|
35
|
+
| `sm` | 2rem | Headers, criação de coleções e operações densas de toolbar ou seção. |
|
|
36
|
+
| `default` | 2.25rem | Footers de Dialog e Drawer e controles comuns; é também a linha de `Input`. |
|
|
37
37
|
| `lg` | 2.5rem | Chamada principal com mais área de toque. |
|
|
38
38
|
| `icon-xs` | 1.5rem | Ação só de ícone dentro de um campo ou de uma linha densa; é o quadrado de `ActionTrigger` com `icon`. |
|
|
39
|
-
| `icon-sm` |
|
|
39
|
+
| `icon-sm` | 2rem | Ação só de ícone em composições compactas que precisam de mais presença; também é o quadrado das setas do pager de `ActionList`. |
|
|
40
40
|
| `icon` | 2.25rem | Ação só de ícone na linha padrão. |
|
|
41
41
|
| `icon-lg` | 2.5rem | Ação só de ícone ao lado de um `lg`. |
|
|
42
42
|
|
|
43
43
|
Escolha o tamanho pela região, não pela importância visual: `variant` e `context` resolvem a
|
|
44
|
-
hierarquia da ação. Headers de Page,
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
hierarquia da ação. Headers de Page, Dialog, Drawer e Surface usam `sm`/`icon-sm`. A criação
|
|
45
|
+
no cabeçalho de uma coleção usa `sm` textual, sem ícone. Footers de
|
|
46
|
+
[`Dialog`](/ui/dialog) e [`Drawer`](/ui/drawer) conservam `default`. Operações densas de toolbar ou de seção usam `sm`;
|
|
47
47
|
ações dentro de linha ou célula usam `xs` ou `icon-xs`. Assim a mesma decisão mantém a mesma altura
|
|
48
48
|
mesmo quando uma superfície troca uma ação secundária por uma primária.
|
|
49
49
|
|
|
50
|
-
O retorno de `PageBack` usa `icon`. O fechamento de Dialog e Drawer é um `CloseButton` circular
|
|
50
|
+
O retorno de `PageBack` usa `icon-sm`. O fechamento de Dialog e Drawer é um `CloseButton` circular
|
|
51
51
|
(`shape="pill"`), `neutral` e `subtle`, no quadrado `icon-sm` por padrão, ou `icon-xs` com
|
|
52
52
|
`closeSize="xs"`.
|
|
53
53
|
|