@softize/opus 19.0.0 → 20.1.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 +239 -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/0020-icon-tiles-are-explicitly-composed.md +8 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -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 +109 -18
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/list.tsx +91 -92
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/presentation.tsx +52 -37
- package/src/ui/components/patterns/split.tsx +44 -4
- package/src/ui/components/patterns/surface-anatomy.ts +35 -8
- package/src/ui/components/patterns/surface-header.tsx +58 -25
- 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 +1 -1
- 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 +2 -1
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/dialog.tsx +42 -11
- package/src/ui/components/primitives/drawer.tsx +10 -2
- 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/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 +141 -58
- 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 +41 -15
- 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 +119 -27
- 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 +107 -24
- 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 +63 -19
- 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 +38 -15
- 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 +12 -4
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +118 -12
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +55 -5
- 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 +3 -2
- package/src/ui/docs/content/skeleton.md +6 -1
- 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 +19 -0
- 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 +6 -0
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +32 -32
- package/src/ui/react.tsx +1 -0
- package/src/vite/design.ts +16 -4
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
useSurfaceLayout,
|
|
12
12
|
type SurfaceLayout,
|
|
13
13
|
} from "../patterns/surface-anatomy.ts";
|
|
14
|
+
import { FillBoundary } from "../patterns/fill-region.ts";
|
|
14
15
|
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
15
16
|
import { SurfaceHeader } from "../patterns/surface-header.tsx";
|
|
16
17
|
import {
|
|
@@ -45,6 +46,24 @@ const DialogContentContext = React.createContext<DialogContentContextValue>({
|
|
|
45
46
|
closeSize: "sm",
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Compõe o handler de dispensa do consumidor com o bloqueio de `closeDisabled`: o handler dele
|
|
51
|
+
* roda primeiro e, com a superfície bloqueada, o evento é cancelado para que Esc, clique fora ou
|
|
52
|
+
* foco fora não a fechem. Sem bloqueio, devolve o handler original.
|
|
53
|
+
*/
|
|
54
|
+
export function preventDismissWhen<
|
|
55
|
+
E extends { defaultPrevented: boolean; preventDefault(): void },
|
|
56
|
+
>(
|
|
57
|
+
blocked: boolean,
|
|
58
|
+
handler: ((event: E) => void) | undefined,
|
|
59
|
+
): ((event: E) => void) | undefined {
|
|
60
|
+
if (!blocked) return handler;
|
|
61
|
+
return (event) => {
|
|
62
|
+
handler?.(event);
|
|
63
|
+
if (!event.defaultPrevented) event.preventDefault();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
function useDialogContext(): DialogContextValue {
|
|
49
68
|
return React.useContext(DialogContext);
|
|
50
69
|
}
|
|
@@ -192,11 +211,16 @@ export function DialogContent({
|
|
|
192
211
|
closeSize = "sm",
|
|
193
212
|
layout = "sectioned",
|
|
194
213
|
"aria-describedby": ariaDescribedBy,
|
|
214
|
+
onEscapeKeyDown,
|
|
215
|
+
onInteractOutside,
|
|
195
216
|
...props
|
|
196
217
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
197
218
|
/** Mostra a ação “Fechar” no modo padrão. O modo `alert` exige uma resposta identificável. */
|
|
198
219
|
showCloseButton?: boolean;
|
|
199
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* Bloqueia o fechamento enquanto a superfície está ocupada: desabilita a ação “Fechar”, que
|
|
222
|
+
* continua visível, e cancela Esc e clique ou foco fora depois dos handlers do consumidor.
|
|
223
|
+
*/
|
|
200
224
|
closeDisabled?: boolean;
|
|
201
225
|
/** Tamanho da ação circular de fechar no cabeçalho. */
|
|
202
226
|
closeSize?: CloseButtonSize;
|
|
@@ -206,7 +230,7 @@ export function DialogContent({
|
|
|
206
230
|
const { mode } = useDialogContext();
|
|
207
231
|
const showClose = showCloseButton ?? mode !== "alert";
|
|
208
232
|
const contentClasses =
|
|
209
|
-
"fixed left-[50%] top-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-clip rounded-xl border border-transparent bg-background text-foreground shadow-xl duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-border/70";
|
|
233
|
+
"fixed left-[50%] top-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-clip rounded-xl border border-transparent bg-background text-foreground shadow-xl duration-200 outline-none [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-border/70";
|
|
210
234
|
|
|
211
235
|
if (mode === "alert") {
|
|
212
236
|
return (
|
|
@@ -217,17 +241,22 @@ export function DialogContent({
|
|
|
217
241
|
data-mode="alert"
|
|
218
242
|
className={cn(contentClasses, "grid max-w-xs gap-4 p-6", className)}
|
|
219
243
|
aria-describedby={ariaDescribedBy}
|
|
244
|
+
onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
|
|
220
245
|
{...props}
|
|
221
246
|
>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
247
|
+
{/* Sem anatomia seccionada (exceção da ADR 0023), mas a altura continua sendo
|
|
248
|
+
decisão do diálogo: a cadeia `fill` da página não atravessa o portal. */}
|
|
249
|
+
<FillBoundary>
|
|
250
|
+
<DialogContentContext.Provider
|
|
251
|
+
value={{
|
|
252
|
+
showCloseButton: false,
|
|
253
|
+
closeDisabled: true,
|
|
254
|
+
closeSize,
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
{children}
|
|
258
|
+
</DialogContentContext.Provider>
|
|
259
|
+
</FillBoundary>
|
|
231
260
|
</AlertDialogPrimitive.Content>
|
|
232
261
|
</DialogPortal>
|
|
233
262
|
);
|
|
@@ -246,6 +275,8 @@ export function DialogContent({
|
|
|
246
275
|
className,
|
|
247
276
|
)}
|
|
248
277
|
aria-describedby={ariaDescribedBy}
|
|
278
|
+
onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
|
|
279
|
+
onInteractOutside={preventDismissWhen(closeDisabled, onInteractOutside)}
|
|
249
280
|
{...props}
|
|
250
281
|
>
|
|
251
282
|
<SurfaceLayoutProvider value={layout}>
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
CloseButton,
|
|
22
22
|
type CloseButtonSize,
|
|
23
23
|
} from "./close-button.tsx";
|
|
24
|
-
import { overlayClasses } from "./dialog.tsx";
|
|
24
|
+
import { overlayClasses, preventDismissWhen } from "./dialog.tsx";
|
|
25
25
|
|
|
26
26
|
interface DrawerContentContextValue {
|
|
27
27
|
showCloseButton: boolean;
|
|
@@ -95,10 +95,16 @@ function DrawerContent({
|
|
|
95
95
|
closeSize = "sm",
|
|
96
96
|
layout = "sectioned",
|
|
97
97
|
"aria-describedby": ariaDescribedBy,
|
|
98
|
+
onEscapeKeyDown,
|
|
99
|
+
onInteractOutside,
|
|
98
100
|
...props
|
|
99
101
|
}: React.ComponentProps<typeof DrawerPrimitive.Content> & {
|
|
100
102
|
side?: "top" | "right" | "bottom" | "left";
|
|
101
103
|
showCloseButton?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Bloqueia o fechamento enquanto a superfície está ocupada: desabilita a ação “Fechar”, que
|
|
106
|
+
* continua visível, e cancela Esc e clique ou foco fora depois dos handlers do consumidor.
|
|
107
|
+
*/
|
|
102
108
|
closeDisabled?: boolean;
|
|
103
109
|
closeSize?: CloseButtonSize;
|
|
104
110
|
/** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
@@ -111,7 +117,7 @@ function DrawerContent({
|
|
|
111
117
|
data-slot="drawer-content"
|
|
112
118
|
data-layout={layout}
|
|
113
119
|
className={cn(
|
|
114
|
-
"fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
120
|
+
"fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
115
121
|
side === "right" &&
|
|
116
122
|
"inset-y-0 right-0 h-full w-3/4 max-w-sm border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
|
|
117
123
|
side === "left" &&
|
|
@@ -123,6 +129,8 @@ function DrawerContent({
|
|
|
123
129
|
className,
|
|
124
130
|
)}
|
|
125
131
|
aria-describedby={ariaDescribedBy}
|
|
132
|
+
onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
|
|
133
|
+
onInteractOutside={preventDismissWhen(closeDisabled, onInteractOutside)}
|
|
126
134
|
{...props}
|
|
127
135
|
>
|
|
128
136
|
<SurfaceLayoutProvider value={layout}>
|
|
@@ -69,7 +69,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
69
69
|
)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function EmptyDescription({ className, ...props }: React.ComponentProps<"
|
|
72
|
+
function EmptyDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
73
73
|
return (
|
|
74
74
|
<div
|
|
75
75
|
data-slot="empty-description"
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
type LucideIcon,
|
|
47
47
|
} from 'lucide-react'
|
|
48
48
|
import { cn } from '../../lib/cn.ts'
|
|
49
|
-
import { focusRing } from './control.ts'
|
|
49
|
+
import { controlHeight, focusRing } from './control.ts'
|
|
50
50
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './command.tsx'
|
|
51
51
|
import { Popover, PopoverContent, PopoverTrigger } from './popover.tsx'
|
|
52
52
|
|
|
@@ -142,8 +142,12 @@ export function IconPicker({
|
|
|
142
142
|
aria-describedby={ariaDescribedBy}
|
|
143
143
|
data-slot="icon-picker"
|
|
144
144
|
className={cn(
|
|
145
|
-
|
|
145
|
+
// Mesma altura do Input e do Select no tamanho default: lado a lado num form, alinham.
|
|
146
|
+
'flex w-full items-center gap-2 rounded-md border border-input bg-muted/30 px-3 text-sm outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30',
|
|
147
|
+
controlHeight.default,
|
|
146
148
|
focusRing,
|
|
149
|
+
// As mesmas classes de inválido do Input e do Select: o erro aparece na moldura.
|
|
150
|
+
'aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40',
|
|
147
151
|
className,
|
|
148
152
|
)}
|
|
149
153
|
>
|
|
@@ -41,6 +41,13 @@ export interface IconTileProps
|
|
|
41
41
|
extends React.ComponentProps<'span'>,
|
|
42
42
|
IconTileStyleOptions {}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Preenche os padrões de `context`, `variant` e `size` (`neutral`, `subtle`, `default`).
|
|
46
|
+
*
|
|
47
|
+
* @deprecated Não há decisão a tomar com os valores resolvidos fora do próprio IconTile. Para a
|
|
48
|
+
* moldura, renderize `IconTile`; para aplicá-la a outro elemento, use `iconTileVariants`, que já
|
|
49
|
+
* aplica os mesmos padrões. Sai do pacote numa próxima versão major.
|
|
50
|
+
*/
|
|
44
51
|
export function resolveIconTileStyle({
|
|
45
52
|
context,
|
|
46
53
|
variant,
|
|
@@ -37,7 +37,12 @@ function InputGroup({ className, shape = 'default', ...props }: React.ComponentP
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const inputGroupAddonVariants = cva(
|
|
40
|
-
|
|
40
|
+
[
|
|
41
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-0.3125rem)] [&>svg:not([class*='size-'])]:size-4",
|
|
42
|
+
// O addon acompanha o controle desabilitado sem depender do `data-disabled` manual. Só o
|
|
43
|
+
// controle conta: um InputGroupButton desabilitado não apaga o grupo inteiro.
|
|
44
|
+
"group-has-[[data-slot=input-group-control]:disabled]/input-group:opacity-50",
|
|
45
|
+
],
|
|
41
46
|
{
|
|
42
47
|
variants: {
|
|
43
48
|
align: {
|
|
@@ -72,7 +77,9 @@ function InputGroupAddon({
|
|
|
72
77
|
if ((e.target as HTMLElement).closest("button")) {
|
|
73
78
|
return
|
|
74
79
|
}
|
|
75
|
-
e.currentTarget.parentElement
|
|
80
|
+
e.currentTarget.parentElement
|
|
81
|
+
?.querySelector<HTMLElement>("input, textarea")
|
|
82
|
+
?.focus()
|
|
76
83
|
}}
|
|
77
84
|
{...props}
|
|
78
85
|
/>
|
|
@@ -49,7 +49,7 @@ function InputOTPSlot({
|
|
|
49
49
|
data-slot="input-otp-slot"
|
|
50
50
|
data-active={isActive}
|
|
51
51
|
className={cn(
|
|
52
|
-
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-context-danger data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[0.1875rem] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-context-danger data-[active=true]:aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-context-danger/40",
|
|
52
|
+
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input bg-muted/30 text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-context-danger data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[0.1875rem] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-context-danger data-[active=true]:aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-context-danger/40",
|
|
53
53
|
className
|
|
54
54
|
)}
|
|
55
55
|
{...props}
|
|
@@ -15,7 +15,7 @@ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
|
15
15
|
)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function KbdGroup({ className, ...props }: React.ComponentProps<"
|
|
18
|
+
function KbdGroup({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
19
19
|
return (
|
|
20
20
|
<kbd
|
|
21
21
|
data-slot="kbd-group"
|
|
@@ -12,6 +12,9 @@ function Label({
|
|
|
12
12
|
data-slot="label"
|
|
13
13
|
className={cn(
|
|
14
14
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
15
|
+
// O Field declara o grupo nomeado `group/field`: sem o sufixo, o Label não enxerga o
|
|
16
|
+
// `data-disabled` dele e continua aceso ao lado de um controle desabilitado.
|
|
17
|
+
"group-data-[disabled=true]/field:pointer-events-none group-data-[disabled=true]/field:opacity-50",
|
|
15
18
|
className
|
|
16
19
|
)}
|
|
17
20
|
{...props}
|
|
@@ -12,6 +12,11 @@ function Menu({
|
|
|
12
12
|
return <MenuPrimitive.Root data-slot="menu" {...props} />
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated `MenuContent` já renderiza o painel num portal próprio, então envolvê-lo com
|
|
17
|
+
* `MenuPortal` não muda onde o painel aparece. Use `MenuContent` diretamente. Sai do pacote numa
|
|
18
|
+
* próxima versão major.
|
|
19
|
+
*/
|
|
15
20
|
function MenuPortal({
|
|
16
21
|
...props
|
|
17
22
|
}: React.ComponentProps<typeof MenuPrimitive.Portal>) {
|
|
@@ -59,7 +59,7 @@ function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
59
59
|
)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function PopoverTitle({ className, ...props }: React.ComponentProps<"
|
|
62
|
+
function PopoverTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
63
63
|
return (
|
|
64
64
|
<div
|
|
65
65
|
data-slot="popover-title"
|
|
@@ -32,13 +32,21 @@ export interface ProgressProps extends React.ComponentProps<typeof ProgressPrimi
|
|
|
32
32
|
function Progress({
|
|
33
33
|
className,
|
|
34
34
|
value,
|
|
35
|
+
max = 100,
|
|
35
36
|
context = 'primary',
|
|
36
37
|
...props
|
|
37
38
|
}: ProgressProps) {
|
|
39
|
+
// A largura é a fração de `max`, não de 100: `value={3} max={4}` enche três quartos. Um `max`
|
|
40
|
+
// não positivo cai em 100, como o Radix faz no Root.
|
|
41
|
+
const scale = max > 0 ? max : 100
|
|
42
|
+
const percent = ((value ?? 0) / scale) * 100
|
|
38
43
|
return (
|
|
39
44
|
<ProgressPrimitive.Root
|
|
40
45
|
data-slot="progress"
|
|
41
46
|
data-context={context}
|
|
47
|
+
// O Root precisa do `value` para anunciar o andamento (aria-valuenow, data-state).
|
|
48
|
+
value={value}
|
|
49
|
+
max={max}
|
|
42
50
|
className={cn(
|
|
43
51
|
"relative h-2 w-full overflow-hidden rounded-full",
|
|
44
52
|
trackClasses[context],
|
|
@@ -49,7 +57,7 @@ function Progress({
|
|
|
49
57
|
<ProgressPrimitive.Indicator
|
|
50
58
|
data-slot="progress-indicator"
|
|
51
59
|
className={cn("h-full w-full flex-1 transition-all", indicatorClasses[context])}
|
|
52
|
-
style={{ transform: `translateX(-${100 -
|
|
60
|
+
style={{ transform: `translateX(-${100 - percent}%)` }}
|
|
53
61
|
/>
|
|
54
62
|
</ProgressPrimitive.Root>
|
|
55
63
|
)
|
|
@@ -4,14 +4,40 @@ import { Slider as SliderPrimitive } from "radix-ui"
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
5
|
import { focusRing } from './control.ts'
|
|
6
6
|
|
|
7
|
+
export interface SliderProps extends React.ComponentProps<typeof SliderPrimitive.Root> {
|
|
8
|
+
/**
|
|
9
|
+
* Nome de cada thumb, na ordem de `value`. Use em intervalos, em que cada controle precisa de
|
|
10
|
+
* um nome próprio (`['Custo mínimo', 'Custo máximo']`). Prevalece sobre `aria-label` e
|
|
11
|
+
* `aria-labelledby` no thumb correspondente.
|
|
12
|
+
*/
|
|
13
|
+
thumbLabels?: readonly string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Quem recebe foco e anuncia o valor é o thumb (`role="slider"`), não a raiz. Por isso
|
|
17
|
+
// `aria-label` e `aria-labelledby` vão para os thumbs, como o Select faz com o campo interno;
|
|
18
|
+
// na raiz, sem papel, não nomeariam nada. Em intervalo, `thumbLabels` dá um nome a cada controle.
|
|
19
|
+
// Sem nome declarado, o Radix usaria "Minimum"/"Maximum": o fallback fica em português. Em
|
|
20
|
+
// intervalo sem `thumbLabels`, o nome comum se compõe com o fallback ("Preço, mínimo"), senão os
|
|
21
|
+
// dois thumbs teriam o mesmo nome; com `aria-labelledby`, o fallback entra por um span oculto
|
|
22
|
+
// referenciado junto (`hidden` sai da tela, mas continua valendo como rótulo referenciado).
|
|
23
|
+
function thumbFallback(index: number, total: number): string | undefined {
|
|
24
|
+
if (total > 2) return `Valor ${index + 1} de ${total}`
|
|
25
|
+
if (total === 2) return index === 0 ? 'Mínimo' : 'Máximo'
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
function Slider({
|
|
8
30
|
className,
|
|
9
31
|
defaultValue,
|
|
10
32
|
value,
|
|
11
33
|
min = 0,
|
|
12
34
|
max = 100,
|
|
35
|
+
thumbLabels,
|
|
36
|
+
'aria-label': ariaLabel,
|
|
37
|
+
'aria-labelledby': ariaLabelledby,
|
|
13
38
|
...props
|
|
14
|
-
}:
|
|
39
|
+
}: SliderProps) {
|
|
40
|
+
const fallbackIdBase = React.useId()
|
|
15
41
|
const _values = React.useMemo(
|
|
16
42
|
() =>
|
|
17
43
|
Array.isArray(value)
|
|
@@ -48,16 +74,41 @@ function Slider({
|
|
|
48
74
|
)}
|
|
49
75
|
/>
|
|
50
76
|
</SliderPrimitive.Track>
|
|
51
|
-
{Array.from({ length: _values.length }, (_, index) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
{Array.from({ length: _values.length }, (_, index) => {
|
|
78
|
+
const thumbLabel = thumbLabels?.[index]
|
|
79
|
+
const fallback = thumbFallback(index, _values.length)
|
|
80
|
+
const fallbackId = `${fallbackIdBase}-thumb-${index}`
|
|
81
|
+
const composeLabelledby = thumbLabel === undefined && ariaLabelledby !== undefined && fallback !== undefined
|
|
82
|
+
const label =
|
|
83
|
+
thumbLabel ??
|
|
84
|
+
(ariaLabel !== undefined && fallback !== undefined
|
|
85
|
+
? `${ariaLabel}, ${fallback.toLowerCase()}`
|
|
86
|
+
: ariaLabel ?? fallback)
|
|
87
|
+
return (
|
|
88
|
+
<React.Fragment key={index}>
|
|
89
|
+
{composeLabelledby && (
|
|
90
|
+
<span id={fallbackId} hidden>
|
|
91
|
+
{fallback}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
<SliderPrimitive.Thumb
|
|
95
|
+
data-slot="slider-thumb"
|
|
96
|
+
aria-label={composeLabelledby ? undefined : label}
|
|
97
|
+
aria-labelledby={
|
|
98
|
+
thumbLabel !== undefined
|
|
99
|
+
? undefined
|
|
100
|
+
: composeLabelledby
|
|
101
|
+
? `${ariaLabelledby} ${fallbackId}`
|
|
102
|
+
: ariaLabelledby
|
|
103
|
+
}
|
|
104
|
+
className={cn(
|
|
105
|
+
"block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm transition-[color,box-shadow] hover:ring-[0.1875rem] hover:ring-ring/50 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
|
|
106
|
+
focusRing,
|
|
107
|
+
)}
|
|
108
|
+
/>
|
|
109
|
+
</React.Fragment>
|
|
110
|
+
)
|
|
111
|
+
})}
|
|
61
112
|
</SliderPrimitive.Root>
|
|
62
113
|
)
|
|
63
114
|
}
|
|
@@ -2,6 +2,7 @@ import * as React from 'react'
|
|
|
2
2
|
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip.tsx'
|
|
3
3
|
import { useOverflowing } from '../../lib/overflow.ts'
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
import { focusRing } from './control.ts'
|
|
5
6
|
|
|
6
7
|
export interface TruncateProps extends React.ComponentProps<'span'> {
|
|
7
8
|
/** Conteúdo do tooltip quando transborda. Default: os próprios children. */
|
|
@@ -17,13 +18,21 @@ export interface TruncateProps extends React.ComponentProps<'span'> {
|
|
|
17
18
|
* Requer `<TooltipProvider>` na raiz do app (o esqueleto do `opus create` já monta).
|
|
18
19
|
* Com `fade`, a mesma medição decide quando esmaecer o fim da linha em vez de cortar
|
|
19
20
|
* em reticências — sem transbordo, nada é aplicado.
|
|
21
|
+
* Com o texto cortado, o span entra na ordem de tabulação para que o teclado alcance a dica;
|
|
22
|
+
* sem corte, fica fora dela, porque não há nada escondido para revelar.
|
|
20
23
|
*/
|
|
21
24
|
export function Truncate({ tooltip, fade = false, children, className, ...props }: TruncateProps): React.ReactElement {
|
|
22
25
|
const { ref, overflowing } = useOverflowing<HTMLSpanElement>(children)
|
|
23
26
|
const cut = fade ? (overflowing ? 'truncate-fade' : 'overflow-hidden whitespace-nowrap') : 'truncate'
|
|
24
27
|
|
|
25
28
|
const span = (
|
|
26
|
-
<span
|
|
29
|
+
<span
|
|
30
|
+
ref={ref}
|
|
31
|
+
data-slot="truncate"
|
|
32
|
+
tabIndex={overflowing ? 0 : undefined}
|
|
33
|
+
className={cn('block', cut, overflowing && ['rounded-sm outline-none', focusRing], className)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
27
36
|
{children}
|
|
28
37
|
</span>
|
|
29
38
|
)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
## Um painel por vez
|
|
2
2
|
|
|
3
|
+
Use `Accordion` para dividir um conteúdo longo em painéis que a pessoa expande sob demanda, cada um
|
|
4
|
+
com seu título. Para um único bloco recolhível, use [`Collapsible`](/ui/collapsible).
|
|
5
|
+
|
|
3
6
|
Use `type="single"` quando somente um painel deve permanecer aberto. Com `collapsible`, a pessoa
|
|
4
|
-
também pode fechar o painel atual; sem essa propriedade, um item
|
|
7
|
+
também pode fechar o painel atual; sem essa propriedade, depois que um item é aberto, sempre haverá
|
|
8
|
+
um item expandido.
|
|
5
9
|
`defaultValue` define o item inicialmente aberto no modo não controlado.
|
|
6
10
|
|
|
7
11
|
```tsx preview
|
|
@@ -9,41 +13,43 @@ também pode fechar o painel atual; sem essa propriedade, um item permanece expa
|
|
|
9
13
|
<AccordionItem value="overview">
|
|
10
14
|
<AccordionTrigger>Visão geral</AccordionTrigger>
|
|
11
15
|
<AccordionContent>
|
|
12
|
-
<p className="text-muted-foreground">
|
|
16
|
+
<p className="text-muted-foreground">Corolla XEi 2022, placa ABC-1D23.</p>
|
|
13
17
|
</AccordionContent>
|
|
14
18
|
</AccordionItem>
|
|
15
|
-
<AccordionItem value="
|
|
16
|
-
<AccordionTrigger>
|
|
19
|
+
<AccordionItem value="services">
|
|
20
|
+
<AccordionTrigger>Serviços</AccordionTrigger>
|
|
17
21
|
<AccordionContent>
|
|
18
|
-
<p className="text-muted-foreground">
|
|
22
|
+
<p className="text-muted-foreground">Troca de óleo, filtro de ar e alinhamento.</p>
|
|
19
23
|
</AccordionContent>
|
|
20
24
|
</AccordionItem>
|
|
21
|
-
<AccordionItem value="
|
|
22
|
-
<AccordionTrigger>
|
|
25
|
+
<AccordionItem value="parts">
|
|
26
|
+
<AccordionTrigger>Peças</AccordionTrigger>
|
|
23
27
|
<AccordionContent>
|
|
24
|
-
<p className="text-muted-foreground">
|
|
28
|
+
<p className="text-muted-foreground">Filtro de óleo e pastilhas de freio.</p>
|
|
25
29
|
</AccordionContent>
|
|
26
30
|
</AccordionItem>
|
|
27
31
|
</Accordion>
|
|
28
32
|
```
|
|
29
33
|
|
|
34
|
+
O `AccordionTrigger` já traz o ícone que gira conforme o estado; não acrescente outro.
|
|
35
|
+
|
|
30
36
|
## Múltiplos abertos
|
|
31
37
|
|
|
32
38
|
Use `type="multiple"` quando os painéis puderem permanecer abertos ao mesmo tempo. Nesse modo,
|
|
33
39
|
`defaultValue` recebe um array com os itens inicialmente expandidos.
|
|
34
40
|
|
|
35
41
|
```tsx preview
|
|
36
|
-
<Accordion type="multiple" defaultValue={['
|
|
37
|
-
<AccordionItem value="
|
|
38
|
-
<AccordionTrigger>
|
|
42
|
+
<Accordion type="multiple" defaultValue={['notes', 'parts']} className="w-full">
|
|
43
|
+
<AccordionItem value="notes">
|
|
44
|
+
<AccordionTrigger>Observações</AccordionTrigger>
|
|
39
45
|
<AccordionContent>
|
|
40
|
-
<p className="text-muted-foreground">O
|
|
46
|
+
<p className="text-muted-foreground">O cliente relatou ruído ao frear.</p>
|
|
41
47
|
</AccordionContent>
|
|
42
48
|
</AccordionItem>
|
|
43
|
-
<AccordionItem value="
|
|
44
|
-
<AccordionTrigger>
|
|
49
|
+
<AccordionItem value="parts">
|
|
50
|
+
<AccordionTrigger>Peças</AccordionTrigger>
|
|
45
51
|
<AccordionContent>
|
|
46
|
-
<p className="text-muted-foreground">
|
|
52
|
+
<p className="text-muted-foreground">Pastilhas de freio substituídas.</p>
|
|
47
53
|
</AccordionContent>
|
|
48
54
|
</AccordionItem>
|
|
49
55
|
</Accordion>
|
|
@@ -55,38 +61,79 @@ Use `value` e `onValueChange` quando outro elemento ou estado do aplicativo tamb
|
|
|
55
61
|
controlar o painel aberto.
|
|
56
62
|
|
|
57
63
|
```tsx preview
|
|
58
|
-
const [open, setOpen] = React.useState('
|
|
64
|
+
const [open, setOpen] = React.useState('diagnosis')
|
|
59
65
|
|
|
60
66
|
render(
|
|
61
67
|
<Accordion type="single" collapsible value={open} onValueChange={setOpen} className="w-full">
|
|
62
|
-
<AccordionItem value="
|
|
63
|
-
<AccordionTrigger>
|
|
68
|
+
<AccordionItem value="diagnosis">
|
|
69
|
+
<AccordionTrigger>Diagnóstico</AccordionTrigger>
|
|
64
70
|
<AccordionContent>
|
|
65
|
-
<p className="text-muted-foreground">
|
|
71
|
+
<p className="text-muted-foreground">Identifica a causa do ruído relatado pelo cliente.</p>
|
|
66
72
|
</AccordionContent>
|
|
67
73
|
</AccordionItem>
|
|
68
|
-
<AccordionItem value="
|
|
69
|
-
<AccordionTrigger>
|
|
74
|
+
<AccordionItem value="execution">
|
|
75
|
+
<AccordionTrigger>Execução</AccordionTrigger>
|
|
70
76
|
<AccordionContent>
|
|
71
|
-
<p className="text-muted-foreground">
|
|
77
|
+
<p className="text-muted-foreground">Substitui as peças aprovadas no orçamento.</p>
|
|
72
78
|
</AccordionContent>
|
|
73
79
|
</AccordionItem>
|
|
74
80
|
</Accordion>,
|
|
75
81
|
)
|
|
76
82
|
```
|
|
77
83
|
|
|
84
|
+
## Item indisponível
|
|
85
|
+
|
|
86
|
+
Use `disabled` no `AccordionItem` quando um painel existe, mas ainda não pode ser aberto. O gatilho
|
|
87
|
+
fica esmaecido e não responde ao clique nem ao teclado.
|
|
88
|
+
|
|
89
|
+
```tsx preview
|
|
90
|
+
<Accordion type="single" collapsible className="w-full">
|
|
91
|
+
<AccordionItem value="quote">
|
|
92
|
+
<AccordionTrigger>Orçamento</AccordionTrigger>
|
|
93
|
+
<AccordionContent>
|
|
94
|
+
<p className="text-muted-foreground">Aprovado pelo cliente em 12 de maio.</p>
|
|
95
|
+
</AccordionContent>
|
|
96
|
+
</AccordionItem>
|
|
97
|
+
<AccordionItem value="invoice" disabled>
|
|
98
|
+
<AccordionTrigger>Nota fiscal</AccordionTrigger>
|
|
99
|
+
<AccordionContent>
|
|
100
|
+
<p className="text-muted-foreground">Emitida depois da entrega do veículo.</p>
|
|
101
|
+
</AccordionContent>
|
|
102
|
+
</AccordionItem>
|
|
103
|
+
</Accordion>
|
|
104
|
+
```
|
|
105
|
+
|
|
78
106
|
## Propriedades de Accordion
|
|
79
107
|
|
|
80
108
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
81
109
|
|---|---|---|---|
|
|
82
|
-
| `type` | `'single' \| 'multiple'` | | `single` abre um painel por vez; `multiple` permite manter vários abertos. |
|
|
110
|
+
| `type` | `'single' \| 'multiple'` | | Obrigatório. `single` abre um painel por vez; `multiple` permite manter vários abertos. |
|
|
83
111
|
| `collapsible` | `boolean` | `false` | No modo `single`, permite fechar o item aberto. |
|
|
84
|
-
| `defaultValue` | `string
|
|
85
|
-
| `value` | `string
|
|
86
|
-
| `onValueChange` | `(value) => void` | | Chamado quando a pessoa abre ou fecha um item. |
|
|
112
|
+
| `defaultValue` | `string` em `single`; `string[]` em `multiple` | | Item ou itens inicialmente abertos no modo não controlado. |
|
|
113
|
+
| `value` | `string` em `single`; `string[]` em `multiple` | | Item ou itens abertos no modo controlado. Use com `onValueChange`. |
|
|
114
|
+
| `onValueChange` | `(value: string) => void` em `single`; `(value: string[]) => void` em `multiple` | | Chamado quando a pessoa abre ou fecha um item. Em `single`, recebe `''` quando o item aberto é fechado. |
|
|
115
|
+
| `disabled` | `boolean` | `false` | Desabilita todos os itens. |
|
|
116
|
+
| `className` | `string` | | Ajusta o contêiner. |
|
|
87
117
|
|
|
88
118
|
## Propriedades de AccordionItem
|
|
89
119
|
|
|
90
120
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
91
121
|
|---|---|---|---|
|
|
92
|
-
| `value` | `string` | | Identificador usado por `defaultValue` e `value` no `Accordion`. |
|
|
122
|
+
| `value` | `string` | | Obrigatório. Identificador usado por `defaultValue` e `value` no `Accordion`. |
|
|
123
|
+
| `disabled` | `boolean` | `false` | Impede abrir ou fechar este item. |
|
|
124
|
+
| `className` | `string` | | Ajusta o item, que traz um divisor inferior exceto no último. |
|
|
125
|
+
|
|
126
|
+
## Propriedades de AccordionTrigger
|
|
127
|
+
|
|
128
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| `children` | `ReactNode` | | Rótulo do painel. O ícone de estado é acrescentado depois dele. |
|
|
131
|
+
| `className` | `string` | | Ajusta o botão do gatilho. |
|
|
132
|
+
|
|
133
|
+
## Propriedades de AccordionContent
|
|
134
|
+
|
|
135
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
136
|
+
|---|---|---|---|
|
|
137
|
+
| `children` | `ReactNode` | | Conteúdo do painel. |
|
|
138
|
+
| `className` | `string` | | Ajusta o recuo interno do conteúdo, aplicado a um contêiner dentro da região animada. |
|
|
139
|
+
| `forceMount` | `boolean` | `false` | Mantém o conteúdo montado mesmo fechado. |
|