@softize/opus 18.5.0 → 20.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 +275 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +16 -12
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +103 -73
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +81 -54
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +51 -38
- package/src/ui/components/patterns/surface-anatomy.ts +246 -0
- package/src/ui/components/patterns/surface-header.tsx +51 -6
- package/src/ui/components/patterns/surface-media.tsx +69 -0
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +14 -5
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +179 -45
- package/src/ui/components/primitives/drawer.tsx +131 -24
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +134 -52
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +252 -35
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +37 -7
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +219 -28
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +193 -22
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +28 -5
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +68 -21
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +125 -11
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +300 -0
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +25 -45
- package/src/ui/docs/content/skeleton.md +13 -4
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +34 -20
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +14 -2
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +38 -32
- package/src/ui/react.tsx +14 -0
- package/src/vite/design.ts +16 -4
|
@@ -40,6 +40,8 @@ export const surfaceHeaderClasses: Record<
|
|
|
40
40
|
|
|
41
41
|
interface SurfaceHeaderSlots {
|
|
42
42
|
leading?: ElementType | readonly ElementType[];
|
|
43
|
+
/** Ícone ou imagem que abre a linha, antes do heading (ADR 0023). */
|
|
44
|
+
media?: ElementType;
|
|
43
45
|
title: ElementType;
|
|
44
46
|
count?: ElementType;
|
|
45
47
|
description?: ElementType;
|
|
@@ -59,6 +61,16 @@ export interface SurfaceHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
59
61
|
contentClassName?: string;
|
|
60
62
|
/** Permite uma superfície composta apenas pelas regiões leading/actions. */
|
|
61
63
|
titleRequired?: boolean;
|
|
64
|
+
/** Classes da linha `inline` — as famílias com anatomia própria trazem a sua (ADR 0023). */
|
|
65
|
+
rowClassName?: string;
|
|
66
|
+
/** Nó interno acrescentado ao fim da linha, depois das ações (o botão de fechar). */
|
|
67
|
+
trailing?: ReactNode;
|
|
68
|
+
/**
|
|
69
|
+
* Aceita conteúdo livre além dos slots. Vale para a superfície genérica — um pane
|
|
70
|
+
* hospeda marca, busca ou chrome que não são título nem ação. As famílias que sempre
|
|
71
|
+
* NOMEIAM algo (Card, Dialog, Drawer, Page, Content) continuam estritas.
|
|
72
|
+
*/
|
|
73
|
+
allowFreeContent?: boolean;
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
/** Expande fragments de primeiro nível: o shorthand monta os slots dentro de um `<>`. */
|
|
@@ -77,6 +89,9 @@ export function SurfaceHeader({
|
|
|
77
89
|
leadingPlacement = "above",
|
|
78
90
|
contentClassName,
|
|
79
91
|
titleRequired = true,
|
|
92
|
+
rowClassName,
|
|
93
|
+
trailing,
|
|
94
|
+
allowFreeContent = false,
|
|
80
95
|
className,
|
|
81
96
|
children,
|
|
82
97
|
...props
|
|
@@ -91,13 +106,30 @@ export function SurfaceHeader({
|
|
|
91
106
|
? slots.leading
|
|
92
107
|
: [slots.leading as ElementType];
|
|
93
108
|
const leading = leadingTypes.flatMap(of);
|
|
109
|
+
const medias = slots.media === undefined ? [] : of(slots.media);
|
|
94
110
|
const titles = of(slots.title);
|
|
95
111
|
const counts = slots.count === undefined ? [] : of(slots.count);
|
|
96
112
|
const descriptions =
|
|
97
113
|
slots.description === undefined ? [] : of(slots.description);
|
|
98
114
|
const actionSlots = of(slots.actions);
|
|
115
|
+
const slotTypes = [
|
|
116
|
+
...leadingTypes,
|
|
117
|
+
slots.media,
|
|
118
|
+
slots.title,
|
|
119
|
+
slots.count,
|
|
120
|
+
slots.description,
|
|
121
|
+
slots.actions,
|
|
122
|
+
].filter((type): type is ElementType => type !== undefined);
|
|
123
|
+
const free = allowFreeContent
|
|
124
|
+
? nodes.filter(
|
|
125
|
+
(node) =>
|
|
126
|
+
!(isValidElement(node) && slotTypes.includes(node.type as ElementType)),
|
|
127
|
+
)
|
|
128
|
+
: [];
|
|
99
129
|
const recognized =
|
|
130
|
+
free.length +
|
|
100
131
|
leading.length +
|
|
132
|
+
medias.length +
|
|
101
133
|
titles.length +
|
|
102
134
|
counts.length +
|
|
103
135
|
descriptions.length +
|
|
@@ -106,6 +138,7 @@ export function SurfaceHeader({
|
|
|
106
138
|
if (
|
|
107
139
|
(titleRequired ? titles.length !== 1 : titles.length > 1) ||
|
|
108
140
|
leading.length > 1 ||
|
|
141
|
+
medias.length > 1 ||
|
|
109
142
|
counts.length > 1 ||
|
|
110
143
|
descriptions.length > 1 ||
|
|
111
144
|
actionSlots.length > 1 ||
|
|
@@ -117,6 +150,7 @@ export function SurfaceHeader({
|
|
|
117
150
|
: ((type as { name?: string }).name ?? "");
|
|
118
151
|
const optionalSlots = [
|
|
119
152
|
...leadingTypes,
|
|
153
|
+
slots.media,
|
|
120
154
|
slots.description,
|
|
121
155
|
slots.count,
|
|
122
156
|
slots.actions,
|
|
@@ -140,16 +174,23 @@ export function SurfaceHeader({
|
|
|
140
174
|
);
|
|
141
175
|
}
|
|
142
176
|
|
|
143
|
-
const heading = titles.length +
|
|
177
|
+
const heading = titles.length +
|
|
178
|
+
counts.length +
|
|
179
|
+
descriptions.length +
|
|
180
|
+
free.length >
|
|
181
|
+
0 && (
|
|
144
182
|
<div
|
|
145
183
|
data-slot={`${slot}-heading`}
|
|
146
184
|
className={cn("min-w-0", leadingPlacement === "inline" && "flex-1")}
|
|
147
185
|
>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
186
|
+
{titles.length + counts.length > 0 && (
|
|
187
|
+
<div className="flex items-baseline gap-2">
|
|
188
|
+
{titles}
|
|
189
|
+
{counts}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
152
192
|
{descriptions}
|
|
193
|
+
{free}
|
|
153
194
|
</div>
|
|
154
195
|
);
|
|
155
196
|
// A região introdutória carrega o mesmo `data-slot` nas duas apresentações: seletor de teste
|
|
@@ -166,13 +207,17 @@ export function SurfaceHeader({
|
|
|
166
207
|
{leading}
|
|
167
208
|
</div>
|
|
168
209
|
)}
|
|
210
|
+
{medias}
|
|
169
211
|
{heading}
|
|
170
212
|
{actionSlots}
|
|
213
|
+
{trailing}
|
|
171
214
|
</>
|
|
172
215
|
);
|
|
173
216
|
const layout =
|
|
174
217
|
leadingPlacement === "inline" ? (
|
|
175
|
-
<div className="flex min-w-0 items-center gap-3"
|
|
218
|
+
<div className={cn("flex min-w-0 items-center gap-3", rowClassName)}>
|
|
219
|
+
{row}
|
|
220
|
+
</div>
|
|
176
221
|
) : leading.length > 0 ? (
|
|
177
222
|
<>
|
|
178
223
|
<div data-slot={`${slot}-navigation`} className="mb-4">
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mídia de superfície (interno): a moldura que abre a linha do header — ADR 0023.
|
|
3
|
+
*
|
|
4
|
+
* Reusa `IconTile`; nenhuma família cria outra escala de molduras. O slot público de
|
|
5
|
+
* cada família (`CardMedia`, `DialogMedia`, …) delega para cá, escolhendo apenas o
|
|
6
|
+
* `data-slot` e a escala default.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReactElement } from "react";
|
|
9
|
+
|
|
10
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
IconTile,
|
|
14
|
+
iconTileVariants,
|
|
15
|
+
type IconTileContext,
|
|
16
|
+
type IconTileSize,
|
|
17
|
+
type IconTileVariant,
|
|
18
|
+
} from "../primitives/icon-tile.tsx";
|
|
19
|
+
import { cn } from "../../lib/cn.ts";
|
|
20
|
+
|
|
21
|
+
export interface SurfaceMediaProps
|
|
22
|
+
extends Omit<React.ComponentProps<"span">, "children"> {
|
|
23
|
+
/** Escala da moldura; a família define o default. */
|
|
24
|
+
size?: IconTileSize;
|
|
25
|
+
context?: IconTileContext;
|
|
26
|
+
variant?: IconTileVariant;
|
|
27
|
+
/**
|
|
28
|
+
* Renderiza como o filho: para a marca que já tem visual próprio — um orbe, um
|
|
29
|
+
* logotipo — ocupar o slot sem ganhar a moldura por cima.
|
|
30
|
+
*/
|
|
31
|
+
asChild?: boolean;
|
|
32
|
+
children?: React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SurfaceMedia({
|
|
36
|
+
slot,
|
|
37
|
+
size = "sm",
|
|
38
|
+
context,
|
|
39
|
+
variant,
|
|
40
|
+
asChild = false,
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}: SurfaceMediaProps & { slot: string }): ReactElement {
|
|
44
|
+
if (asChild) {
|
|
45
|
+
return (
|
|
46
|
+
<Slot
|
|
47
|
+
data-slot={slot}
|
|
48
|
+
className={cn(
|
|
49
|
+
iconTileVariants({ size, context: null, variant: null }),
|
|
50
|
+
// Sem fundo nem borda: quem compõe traz o próprio visual.
|
|
51
|
+
"bg-transparent",
|
|
52
|
+
"shrink-0",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return (
|
|
60
|
+
<IconTile
|
|
61
|
+
data-slot={slot}
|
|
62
|
+
size={size}
|
|
63
|
+
{...(context !== undefined ? { context } : {})}
|
|
64
|
+
{...(variant !== undefined ? { variant } : {})}
|
|
65
|
+
className={cn("shrink-0", className)}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -66,6 +66,7 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
66
66
|
title: string;
|
|
67
67
|
/** Conteúdo relevante apresentado antes das ações. */
|
|
68
68
|
body?: ReactNode;
|
|
69
|
+
/** Texto do botão de confirmar. Default: `action.label`; o `label` do gatilho só sem ele. */
|
|
69
70
|
actionLabel?: string;
|
|
70
71
|
cancelLabel?: string;
|
|
71
72
|
};
|
|
@@ -123,6 +124,15 @@ export function ActionTrigger<TInput, TData>({
|
|
|
123
124
|
|
|
124
125
|
const buttonLabel =
|
|
125
126
|
label ?? (typeof action.label === "string" ? action.label : action.name);
|
|
127
|
+
/**
|
|
128
|
+
* Botão de confirmar: `confirmLabel` do contrato, depois o `label` da action. O `label` do
|
|
129
|
+
* gatilho costuma ser um nome acessível que identifica o alvo ("Excluir Unidade Centro"); na
|
|
130
|
+
* pergunta o alvo já está no corpo, então ele só entra quando o contrato não tem rótulo.
|
|
131
|
+
*/
|
|
132
|
+
const confirmActionLabel =
|
|
133
|
+
action.label !== undefined
|
|
134
|
+
? msgText(action.label, buttonLabel)
|
|
135
|
+
: buttonLabel;
|
|
126
136
|
|
|
127
137
|
// Prop sobrepõe; sem prop, o ConfirmSpec DECLARADO no contrato (action.confirm) vale.
|
|
128
138
|
const spec = action.confirm;
|
|
@@ -135,7 +145,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
135
145
|
? { body: msgText(spec.message, "") }
|
|
136
146
|
: {}),
|
|
137
147
|
...(spec.confirmLabel !== undefined
|
|
138
|
-
? { actionLabel: msgText(spec.confirmLabel,
|
|
148
|
+
? { actionLabel: msgText(spec.confirmLabel, confirmActionLabel) }
|
|
139
149
|
: {}),
|
|
140
150
|
...(spec.cancelLabel !== undefined
|
|
141
151
|
? { cancelLabel: msgText(spec.cancelLabel, "Cancelar") }
|
|
@@ -248,7 +258,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
248
258
|
busy={isLoading}
|
|
249
259
|
onClick={fire}
|
|
250
260
|
>
|
|
251
|
-
{confirm.actionLabel ??
|
|
261
|
+
{confirm.actionLabel ?? confirmActionLabel}
|
|
252
262
|
</Button>
|
|
253
263
|
</ButtonGroup>
|
|
254
264
|
</DialogFooter>
|
|
@@ -71,6 +71,14 @@ function alertMediaClasses(context: AlertContext): string {
|
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* `role="alert"` interrompe o leitor de tela; só perigo e atenção justificam a interrupção.
|
|
76
|
+
* Os demais contextos são anunciados sem cortar a leitura, como `role="status"`.
|
|
77
|
+
*/
|
|
78
|
+
function alertRole(context: AlertContext): "alert" | "status" {
|
|
79
|
+
return context === "danger" || context === "warning" ? "alert" : "status";
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
const alertHeaderClasses =
|
|
75
83
|
"col-start-2 row-start-1 flex min-w-0 flex-col justify-center gap-1";
|
|
76
84
|
|
|
@@ -147,7 +155,7 @@ export function Alert({
|
|
|
147
155
|
data-slot="alert"
|
|
148
156
|
data-context={resolved.context}
|
|
149
157
|
data-variant={resolved.variant}
|
|
150
|
-
role=
|
|
158
|
+
role={alertRole(resolved.context)}
|
|
151
159
|
className={cn(alertVariants(resolved), className)}
|
|
152
160
|
{...props}
|
|
153
161
|
>
|
|
@@ -8,6 +8,10 @@ import type { ControlSize } from './control.ts'
|
|
|
8
8
|
* Ejetado (14.0.0): `size` segue a escala de control.ts — o avatar de uma linha `sm` mede o
|
|
9
9
|
* mesmo que o controle `sm` ao lado (2rem); `xs` (1.5rem) é a lista densa; `default` (2.25rem)
|
|
10
10
|
* e `lg` (2.5rem) pareiam com Button/Input. O fallback e o selo acompanham o tamanho.
|
|
11
|
+
*
|
|
12
|
+
* O root não recorta (20.0.0): imagem e fallback herdam o raio do root e se recortam sozinhos, e
|
|
13
|
+
* o AvatarBadge, que transborda o círculo no canto, aparece inteiro. A forma continua declarada
|
|
14
|
+
* no root (`rounded-full` por padrão, `rounded-lg` para logo) e o anel do AvatarGroup a segue.
|
|
11
15
|
*/
|
|
12
16
|
function Avatar({
|
|
13
17
|
className,
|
|
@@ -21,7 +25,7 @@ function Avatar({
|
|
|
21
25
|
data-slot="avatar"
|
|
22
26
|
data-size={size}
|
|
23
27
|
className={cn(
|
|
24
|
-
"group/avatar relative flex shrink-0
|
|
28
|
+
"group/avatar relative flex shrink-0 rounded-full select-none data-[size=xs]:size-6 data-[size=sm]:size-8 data-[size=default]:size-9 data-[size=lg]:size-10",
|
|
25
29
|
className
|
|
26
30
|
)}
|
|
27
31
|
{...props}
|
|
@@ -36,7 +40,7 @@ function AvatarImage({
|
|
|
36
40
|
return (
|
|
37
41
|
<AvatarPrimitive.Image
|
|
38
42
|
data-slot="avatar-image"
|
|
39
|
-
className={cn("aspect-square size-full", className)}
|
|
43
|
+
className={cn("aspect-square size-full rounded-[inherit] object-cover", className)}
|
|
40
44
|
{...props}
|
|
41
45
|
/>
|
|
42
46
|
)
|
|
@@ -50,7 +54,7 @@ function AvatarFallback({
|
|
|
50
54
|
<AvatarPrimitive.Fallback
|
|
51
55
|
data-slot="avatar-fallback"
|
|
52
56
|
className={cn(
|
|
53
|
-
"flex size-full items-center justify-center rounded-
|
|
57
|
+
"flex size-full items-center justify-center overflow-hidden rounded-[inherit] bg-muted text-sm text-muted-foreground group-data-[size=xs]/avatar:text-xs group-data-[size=sm]/avatar:text-xs",
|
|
54
58
|
className
|
|
55
59
|
)}
|
|
56
60
|
{...props}
|
|
@@ -1,61 +1,116 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { cn } from "../../lib/cn.ts";
|
|
4
|
+
import {
|
|
5
|
+
SurfaceLayoutProvider,
|
|
6
|
+
surfaceActionsClasses,
|
|
7
|
+
surfaceDescriptionClasses,
|
|
8
|
+
surfaceHeaderRowClasses,
|
|
9
|
+
surfaceRegionClasses,
|
|
10
|
+
surfaceTitleClasses,
|
|
11
|
+
useSurfaceLayout,
|
|
12
|
+
type SurfaceLayout,
|
|
13
|
+
} from "../patterns/surface-anatomy.ts";
|
|
14
|
+
import { SurfaceHeader } from "../patterns/surface-header.tsx";
|
|
15
|
+
import {
|
|
16
|
+
SurfaceMedia,
|
|
17
|
+
type SurfaceMediaProps,
|
|
18
|
+
} from "../patterns/surface-media.tsx";
|
|
19
|
+
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
4
20
|
|
|
5
21
|
/**
|
|
6
22
|
* Card — a superfície da casa: SÓ a casca (borda + raio + bg). Divergência DECLARADA vs
|
|
7
|
-
* shadcn: FLAT (sem `shadow-sm`) e SEM `flex/gap/py` forçados. O
|
|
8
|
-
*
|
|
9
|
-
* simples (o `gap-6` do Card + o `space-y` do consumidor = espaçamento dobrado) — por isso
|
|
10
|
-
* a casa hand-rollava. Aqui o padding mora no SLOT (Header/Body/Footer) ou no `className`
|
|
11
|
-
* do consumidor (box simples) — mesmo modelo do Dialog.
|
|
23
|
+
* shadcn: FLAT (sem `shadow-sm`) e SEM `flex/gap/py` forçados. O padding mora no SLOT
|
|
24
|
+
* (Header/Body/Footer) ou no `className` do consumidor (box simples).
|
|
12
25
|
*
|
|
13
26
|
* Box simples: <Card className="p-4">…</Card>
|
|
14
|
-
* Estruturado: <Card><CardHeader>…</CardHeader><CardBody>…</CardBody
|
|
27
|
+
* Estruturado: <Card><CardHeader>…</CardHeader><CardBody>…</CardBody></Card>
|
|
28
|
+
*
|
|
29
|
+
* `layout` é o pacote de anatomia da ADR 0023 e vale para a superfície inteira:
|
|
30
|
+
*
|
|
31
|
+
* contínuo (default): sem divisores; o ritmo separa as regiões.
|
|
32
|
+
* seccionado: regiões simétricas, com divisor entre as adjacentes.
|
|
33
|
+
*
|
|
34
|
+
* O divisor é derivado da presença das regiões — o consumidor não escreve `border-b`.
|
|
15
35
|
*/
|
|
16
36
|
export function Card({
|
|
17
37
|
className,
|
|
18
38
|
asChild = false,
|
|
39
|
+
layout = "continuous",
|
|
19
40
|
...props
|
|
20
41
|
}: React.ComponentProps<"div"> & {
|
|
21
42
|
/** Renderiza como o filho (Radix Slot) — pra card CLICÁVEL manter a semântica do filho
|
|
22
43
|
* (ex.: <button>) com a superfície do Card: `<Card asChild><button onClick>…</button></Card>`. */
|
|
23
44
|
asChild?: boolean;
|
|
45
|
+
/** Anatomia da superfície: `continuous` (default) ou `sectioned`. */
|
|
46
|
+
layout?: SurfaceLayout;
|
|
24
47
|
}): React.ReactElement {
|
|
25
48
|
const Comp = asChild ? Slot : "div";
|
|
26
49
|
return (
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
className
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
<SurfaceLayoutProvider value={layout}>
|
|
51
|
+
<Comp
|
|
52
|
+
data-slot="card"
|
|
53
|
+
data-layout={layout}
|
|
54
|
+
className={cn(
|
|
55
|
+
"rounded-xl border border-border bg-card text-card-foreground",
|
|
56
|
+
// Seccionado: o divisor encosta na borda, então o raio precisa recortar.
|
|
57
|
+
layout === "sectioned" && "overflow-hidden",
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
</SurfaceLayoutProvider>
|
|
35
63
|
);
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
export function CardHeader({
|
|
39
67
|
className,
|
|
68
|
+
children,
|
|
40
69
|
...props
|
|
41
70
|
}: React.ComponentProps<"div">): React.ReactElement {
|
|
71
|
+
const layout = useSurfaceLayout("card");
|
|
42
72
|
return (
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
<SurfaceHeader
|
|
74
|
+
name="CardHeader"
|
|
75
|
+
slot="card"
|
|
76
|
+
slots={{
|
|
77
|
+
media: CardMedia,
|
|
78
|
+
title: CardTitle,
|
|
79
|
+
description: CardDescription,
|
|
80
|
+
actions: CardAction,
|
|
81
|
+
}}
|
|
82
|
+
leadingPlacement="inline"
|
|
83
|
+
titleRequired={false}
|
|
84
|
+
rowClassName={surfaceHeaderRowClasses("card")}
|
|
85
|
+
className={cn(surfaceRegionClasses("card", layout, "header"), className)}
|
|
46
86
|
{...props}
|
|
47
|
-
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</SurfaceHeader>
|
|
48
90
|
);
|
|
49
91
|
}
|
|
50
92
|
|
|
93
|
+
/** Moldura de ícone ou imagem que abre a linha do header. */
|
|
94
|
+
export function CardMedia(props: SurfaceMediaProps): React.ReactElement {
|
|
95
|
+
return <SurfaceMedia slot="card-media" {...props} />;
|
|
96
|
+
}
|
|
97
|
+
|
|
51
98
|
export function CardTitle({
|
|
52
99
|
className,
|
|
100
|
+
level = 3,
|
|
101
|
+
asChild = false,
|
|
53
102
|
...props
|
|
54
|
-
}: React.ComponentProps<"
|
|
103
|
+
}: React.ComponentProps<"h3"> & {
|
|
104
|
+
/** Hierarquia do documento (`h1`–`h6`). O tamanho vem do layout, nunca do nível. */
|
|
105
|
+
level?: HeadingLevel;
|
|
106
|
+
asChild?: boolean;
|
|
107
|
+
}): React.ReactElement {
|
|
108
|
+
const layout = useSurfaceLayout("card");
|
|
109
|
+
const Comp = asChild ? Slot : headingTags[level];
|
|
55
110
|
return (
|
|
56
|
-
<
|
|
111
|
+
<Comp
|
|
57
112
|
data-slot="card-title"
|
|
58
|
-
className={cn("
|
|
113
|
+
className={cn(surfaceTitleClasses("card", layout), className)}
|
|
59
114
|
{...props}
|
|
60
115
|
/>
|
|
61
116
|
);
|
|
@@ -64,17 +119,17 @@ export function CardTitle({
|
|
|
64
119
|
export function CardDescription({
|
|
65
120
|
className,
|
|
66
121
|
...props
|
|
67
|
-
}: React.ComponentProps<"
|
|
122
|
+
}: React.ComponentProps<"p">): React.ReactElement {
|
|
68
123
|
return (
|
|
69
|
-
<
|
|
124
|
+
<p
|
|
70
125
|
data-slot="card-description"
|
|
71
|
-
className={cn("
|
|
126
|
+
className={cn(surfaceDescriptionClasses("card"), className)}
|
|
72
127
|
{...props}
|
|
73
128
|
/>
|
|
74
129
|
);
|
|
75
130
|
}
|
|
76
131
|
|
|
77
|
-
/**
|
|
132
|
+
/** Ações do header — a família posiciona no extremo oposto ao título. */
|
|
78
133
|
export function CardAction({
|
|
79
134
|
className,
|
|
80
135
|
...props
|
|
@@ -82,7 +137,7 @@ export function CardAction({
|
|
|
82
137
|
return (
|
|
83
138
|
<div
|
|
84
139
|
data-slot="card-action"
|
|
85
|
-
className={cn(
|
|
140
|
+
className={cn(surfaceActionsClasses, className)}
|
|
86
141
|
{...props}
|
|
87
142
|
/>
|
|
88
143
|
);
|
|
@@ -92,10 +147,11 @@ export function CardBody({
|
|
|
92
147
|
className,
|
|
93
148
|
...props
|
|
94
149
|
}: React.ComponentProps<"div">): React.ReactElement {
|
|
150
|
+
const layout = useSurfaceLayout("card");
|
|
95
151
|
return (
|
|
96
152
|
<div
|
|
97
153
|
data-slot="card-body"
|
|
98
|
-
className={cn("
|
|
154
|
+
className={cn(surfaceRegionClasses("card", layout, "body"), className)}
|
|
99
155
|
{...props}
|
|
100
156
|
/>
|
|
101
157
|
);
|
|
@@ -106,10 +162,11 @@ export function CardContent({
|
|
|
106
162
|
className,
|
|
107
163
|
...props
|
|
108
164
|
}: React.ComponentProps<"div">): React.ReactElement {
|
|
165
|
+
const layout = useSurfaceLayout("card");
|
|
109
166
|
return (
|
|
110
167
|
<div
|
|
111
168
|
data-slot="card-content"
|
|
112
|
-
className={cn("
|
|
169
|
+
className={cn(surfaceRegionClasses("card", layout, "body"), className)}
|
|
113
170
|
{...props}
|
|
114
171
|
/>
|
|
115
172
|
);
|
|
@@ -119,10 +176,15 @@ export function CardFooter({
|
|
|
119
176
|
className,
|
|
120
177
|
...props
|
|
121
178
|
}: React.ComponentProps<"div">): React.ReactElement {
|
|
179
|
+
const layout = useSurfaceLayout("card");
|
|
122
180
|
return (
|
|
123
181
|
<div
|
|
124
182
|
data-slot="card-footer"
|
|
125
|
-
className={cn(
|
|
183
|
+
className={cn(
|
|
184
|
+
"flex items-center justify-end gap-2",
|
|
185
|
+
surfaceRegionClasses("card", layout, "footer"),
|
|
186
|
+
className,
|
|
187
|
+
)}
|
|
126
188
|
{...props}
|
|
127
189
|
/>
|
|
128
190
|
);
|
|
@@ -34,12 +34,33 @@ function useCarousel() {
|
|
|
34
34
|
const context = React.useContext(CarouselContext)
|
|
35
35
|
|
|
36
36
|
if (!context) {
|
|
37
|
-
throw new Error(
|
|
37
|
+
throw new Error(
|
|
38
|
+
"useCarousel só funciona dentro de <Carousel />. Coloque CarouselContent, CarouselItem, CarouselPrevious e CarouselNext dentro de um <Carousel>."
|
|
39
|
+
)
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
return context
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
// Quem usa as setas por conta própria: campos editáveis e widgets compostos que navegam por dentro
|
|
46
|
+
// (Slider, RadioGroup, Tabs, listas, menus; ToggleGroup e Accordion pelos itens de coleção do
|
|
47
|
+
// Radix). A captura do carrossel roda antes deles e, com `preventDefault`, os handlers do Radix
|
|
48
|
+
// desistem do evento — por isso as setas vindas daqui ficam com o widget.
|
|
49
|
+
const ARROW_OWNERS =
|
|
50
|
+
"input, textarea, select, [contenteditable]:not([contenteditable=false]), " +
|
|
51
|
+
"[role=slider], [role=radiogroup], [role=tablist], [role=listbox], [role=menu], [role=menubar], " +
|
|
52
|
+
"[role=combobox], [role=grid], [role=tree], [role=spinbutton], [role=toolbar], " +
|
|
53
|
+
"[data-radix-collection-item]"
|
|
54
|
+
|
|
55
|
+
// Só conta o dono que está DENTRO do carrossel (ou num portal aberto a partir dele): um carrossel
|
|
56
|
+
// posto dentro de uma toolbar ou de uma célula de grid continua respondendo às setas.
|
|
57
|
+
function ownsArrowKeys(target: EventTarget, carousel: EventTarget): boolean {
|
|
58
|
+
if (!(target instanceof HTMLElement)) return false
|
|
59
|
+
if (target.isContentEditable) return true
|
|
60
|
+
const owner = target.closest(ARROW_OWNERS)
|
|
61
|
+
return owner !== null && !(carousel instanceof Node && owner.contains(carousel))
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
function Carousel({
|
|
44
65
|
orientation = "horizontal",
|
|
45
66
|
opts,
|
|
@@ -75,15 +96,22 @@ function Carousel({
|
|
|
75
96
|
|
|
76
97
|
const handleKeyDown = React.useCallback(
|
|
77
98
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
78
|
-
|
|
99
|
+
// As setas de um campo editável dentro do slide movem o cursor ou mudam a opção; o
|
|
100
|
+
// carrossel não as rouba.
|
|
101
|
+
if (ownsArrowKeys(event.target, event.currentTarget)) return
|
|
102
|
+
const [prevKey, nextKey] =
|
|
103
|
+
orientation === "vertical"
|
|
104
|
+
? ["ArrowUp", "ArrowDown"]
|
|
105
|
+
: ["ArrowLeft", "ArrowRight"]
|
|
106
|
+
if (event.key === prevKey) {
|
|
79
107
|
event.preventDefault()
|
|
80
108
|
scrollPrev()
|
|
81
|
-
} else if (event.key ===
|
|
109
|
+
} else if (event.key === nextKey) {
|
|
82
110
|
event.preventDefault()
|
|
83
111
|
scrollNext()
|
|
84
112
|
}
|
|
85
113
|
},
|
|
86
|
-
[scrollPrev, scrollNext]
|
|
114
|
+
[orientation, scrollPrev, scrollNext]
|
|
87
115
|
)
|
|
88
116
|
|
|
89
117
|
React.useEffect(() => {
|
|
@@ -98,7 +126,8 @@ function Carousel({
|
|
|
98
126
|
api.on("select", onSelect)
|
|
99
127
|
|
|
100
128
|
return () => {
|
|
101
|
-
api
|
|
129
|
+
api.off("reInit", onSelect)
|
|
130
|
+
api.off("select", onSelect)
|
|
102
131
|
}
|
|
103
132
|
}, [api, onSelect])
|
|
104
133
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { CheckIcon } from "lucide-react"
|
|
2
|
+
import { CheckIcon, MinusIcon } from "lucide-react"
|
|
3
3
|
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
|
4
4
|
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
@@ -12,7 +12,7 @@ function Checkbox({
|
|
|
12
12
|
<CheckboxPrimitive.Root
|
|
13
13
|
data-slot="checkbox"
|
|
14
14
|
className={cn(
|
|
15
|
-
"peer size-4 shrink-0 rounded-[0.25rem] border border-input transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-context-danger/40 dark:data-[state=checked]:bg-primary",
|
|
15
|
+
"peer group/checkbox size-4 shrink-0 rounded-[0.25rem] border border-input transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-context-danger/40 dark:data-[state=checked]:bg-primary dark:data-[state=indeterminate]:bg-primary",
|
|
16
16
|
className
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -21,7 +21,15 @@ function Checkbox({
|
|
|
21
21
|
data-slot="checkbox-indicator"
|
|
22
22
|
className="grid place-content-center text-current transition-none"
|
|
23
23
|
>
|
|
24
|
-
|
|
24
|
+
{/* Indeterminado ("alguns marcados") tem glifo próprio: o traço não se confunde com o check. */}
|
|
25
|
+
<CheckIcon
|
|
26
|
+
data-slot="checkbox-check"
|
|
27
|
+
className="size-3.5 group-data-[state=indeterminate]/checkbox:hidden"
|
|
28
|
+
/>
|
|
29
|
+
<MinusIcon
|
|
30
|
+
data-slot="checkbox-indeterminate"
|
|
31
|
+
className="hidden size-3.5 group-data-[state=indeterminate]/checkbox:block"
|
|
32
|
+
/>
|
|
25
33
|
</CheckboxPrimitive.Indicator>
|
|
26
34
|
</CheckboxPrimitive.Root>
|
|
27
35
|
)
|
|
@@ -38,15 +38,23 @@ function CommandDialog({
|
|
|
38
38
|
return (
|
|
39
39
|
<Dialog {...props}>
|
|
40
40
|
<DialogContent
|
|
41
|
+
// A paleta é uma superfície contínua: a lista já separa os grupos, e uma faixa
|
|
42
|
+
// sob o cabeçalho competiria com ela.
|
|
43
|
+
layout="continuous"
|
|
41
44
|
className={cn("overflow-hidden p-0", className)}
|
|
42
45
|
showCloseButton={showCloseButton}
|
|
43
46
|
aria-describedby={descriptionId}
|
|
44
47
|
>
|
|
45
|
-
|
|
48
|
+
{/* A descrição só existe para o `aria-describedby`; fora do header, que só
|
|
49
|
+
nomeia a superfície (ADRs 0018 e 0023). */}
|
|
50
|
+
<p id={descriptionId} className="sr-only">
|
|
51
|
+
{description}
|
|
52
|
+
</p>
|
|
53
|
+
{/* O recuo e a escala são os da paleta, não os da família: a caixa de busca
|
|
54
|
+
manda na densidade, e o título aqui é um rótulo acima dela. É a única
|
|
55
|
+
superfície do SDK que diverge nos dois, e por isso está escrito aqui. */}
|
|
56
|
+
<DialogHeader className="px-3 py-2">
|
|
46
57
|
<DialogTitle className="text-sm">{title}</DialogTitle>
|
|
47
|
-
<p id={descriptionId} className="sr-only">
|
|
48
|
-
{description}
|
|
49
|
-
</p>
|
|
50
58
|
</DialogHeader>
|
|
51
59
|
<Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
52
60
|
{children}
|
|
@@ -95,12 +103,13 @@ function CommandList({
|
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
function CommandEmpty({
|
|
106
|
+
className,
|
|
98
107
|
...props
|
|
99
108
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
100
109
|
return (
|
|
101
110
|
<CommandPrimitive.Empty
|
|
102
111
|
data-slot="command-empty"
|
|
103
|
-
className="py-6 text-center text-sm"
|
|
112
|
+
className={cn("py-6 text-center text-sm", className)}
|
|
104
113
|
{...props}
|
|
105
114
|
/>
|
|
106
115
|
);
|