@softize/opus 27.0.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 +73 -0
- package/bin/lib/check.mjs +139 -30
- package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +14 -8
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
- package/docs/adr/0057-doc-browser-permite-busca-no-chrome-do-consumidor.md +39 -5
- 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/DocBrowser.tsx +122 -12
- 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/documentation-portal.md +40 -1
- 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/docs/types.ts +2 -0
- 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: {
|