@softize/opus 21.0.1 → 22.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 +47 -0
- package/bin/lib/check.mjs +31 -28
- package/docs/adr/0004-page-content-state-is-composed.md +3 -2
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +3 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +23 -14
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -0
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +7 -4
- package/docs/adr/0016-list-collection-header-belongs-to-content.md +13 -8
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +27 -26
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +1 -0
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +71 -0
- package/docs/adr/0030-framed-content-can-use-sectioned-anatomy.md +89 -0
- package/docs/adr/0031-detail-dividers-own-the-tabular-label-treatment.md +61 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +59 -0
- package/docs/adr/0033-drawer-uses-an-inset-structural-frame.md +72 -0
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +60 -0
- package/docs/adr/0035-button-group-separates-frame-relationship-and-distribution.md +63 -0
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +58 -0
- package/docs/radius-scale.md +5 -2
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +9 -6
- package/registry/skills/build-opus-ui/references/evaluations.md +5 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -16
- package/src/ui/components/patterns/content-header.tsx +51 -13
- package/src/ui/components/patterns/list.tsx +51 -33
- package/src/ui/components/patterns/page.tsx +139 -251
- package/src/ui/components/patterns/presentation.tsx +2 -6
- package/src/ui/components/primitives/button-group.tsx +80 -8
- package/src/ui/components/primitives/button.tsx +85 -60
- package/src/ui/components/primitives/detail.tsx +63 -57
- package/src/ui/components/primitives/dialog.tsx +8 -11
- package/src/ui/components/primitives/drawer.tsx +16 -12
- package/src/ui/components/primitives/tabs.tsx +123 -54
- package/src/ui/docs/content/button.md +55 -29
- package/src/ui/docs/content/content.md +61 -36
- package/src/ui/docs/content/detail.md +28 -19
- package/src/ui/docs/content/drawer.md +53 -47
- package/src/ui/docs/content/input.md +51 -35
- package/src/ui/docs/content/page.md +84 -95
- package/src/ui/docs/content/pane.md +83 -63
- package/src/ui/docs/content/presentation.md +20 -20
- package/src/ui/docs/content/tabs.md +107 -36
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
3
|
import { Slot } from "radix-ui";
|
|
3
4
|
|
|
@@ -15,6 +16,7 @@ const buttonGroupVariants = cva(
|
|
|
15
16
|
mode: {
|
|
16
17
|
connected: "",
|
|
17
18
|
spaced: "gap-1",
|
|
19
|
+
divided: "gap-1",
|
|
18
20
|
},
|
|
19
21
|
shape: {
|
|
20
22
|
default: "",
|
|
@@ -22,8 +24,11 @@ const buttonGroupVariants = cva(
|
|
|
22
24
|
},
|
|
23
25
|
distribution: {
|
|
24
26
|
content: "",
|
|
25
|
-
equal:
|
|
26
|
-
|
|
27
|
+
equal: "w-full gap-2 [&>*]:min-w-0 [&>*]:w-full [&>*]:flex-1",
|
|
28
|
+
},
|
|
29
|
+
framed: {
|
|
30
|
+
true: "border p-1",
|
|
31
|
+
false: "",
|
|
27
32
|
},
|
|
28
33
|
},
|
|
29
34
|
compoundVariants: [
|
|
@@ -59,12 +64,29 @@ const buttonGroupVariants = cva(
|
|
|
59
64
|
className:
|
|
60
65
|
"[&>*]:rounded-full [&>[data-slot=select]_[data-slot=select-control]]:rounded-full [&>[data-slot=select-wrapper]_[data-slot=select]]:rounded-full",
|
|
61
66
|
},
|
|
67
|
+
{
|
|
68
|
+
mode: "divided",
|
|
69
|
+
shape: "pill",
|
|
70
|
+
className:
|
|
71
|
+
"[&>*]:rounded-full [&>[data-slot=select]_[data-slot=select-control]]:rounded-full [&>[data-slot=select-wrapper]_[data-slot=select]]:rounded-full",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
mode: "divided",
|
|
75
|
+
distribution: "equal",
|
|
76
|
+
className: "gap-1",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
framed: true,
|
|
80
|
+
shape: "default",
|
|
81
|
+
className: "rounded-xl",
|
|
82
|
+
},
|
|
62
83
|
],
|
|
63
84
|
defaultVariants: {
|
|
64
85
|
orientation: "horizontal",
|
|
65
86
|
mode: "connected",
|
|
66
87
|
shape: "default",
|
|
67
88
|
distribution: "content",
|
|
89
|
+
framed: false,
|
|
68
90
|
},
|
|
69
91
|
},
|
|
70
92
|
);
|
|
@@ -75,26 +97,76 @@ function ButtonGroup({
|
|
|
75
97
|
mode,
|
|
76
98
|
shape,
|
|
77
99
|
distribution,
|
|
100
|
+
framed = false,
|
|
101
|
+
children,
|
|
78
102
|
...props
|
|
79
103
|
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
|
104
|
+
const effectiveOrientation = orientation ?? "horizontal";
|
|
105
|
+
const effectiveMode = mode ?? "connected";
|
|
106
|
+
const controls = React.Children.toArray(children);
|
|
107
|
+
const content =
|
|
108
|
+
effectiveMode === "divided"
|
|
109
|
+
? controls.flatMap((child, index) => {
|
|
110
|
+
const childKey = React.isValidElement(child) ? child.key : index;
|
|
111
|
+
return [
|
|
112
|
+
...(index === 0
|
|
113
|
+
? []
|
|
114
|
+
: [
|
|
115
|
+
<ButtonGroupSeparator
|
|
116
|
+
key={`button-group-separator-${childKey ?? index}`}
|
|
117
|
+
orientation={
|
|
118
|
+
effectiveOrientation === "horizontal"
|
|
119
|
+
? "vertical"
|
|
120
|
+
: "horizontal"
|
|
121
|
+
}
|
|
122
|
+
className="self-center"
|
|
123
|
+
style={
|
|
124
|
+
effectiveOrientation === "horizontal"
|
|
125
|
+
? {
|
|
126
|
+
flex: "none",
|
|
127
|
+
width: "0.0625rem",
|
|
128
|
+
height: "1.25rem",
|
|
129
|
+
}
|
|
130
|
+
: {
|
|
131
|
+
flex: "none",
|
|
132
|
+
width: "1.25rem",
|
|
133
|
+
height: "0.0625rem",
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/>,
|
|
137
|
+
]),
|
|
138
|
+
child,
|
|
139
|
+
];
|
|
140
|
+
})
|
|
141
|
+
: children;
|
|
142
|
+
|
|
80
143
|
return (
|
|
81
144
|
<div
|
|
82
145
|
role="group"
|
|
83
146
|
data-slot="button-group"
|
|
84
|
-
data-orientation={
|
|
85
|
-
data-mode={
|
|
147
|
+
data-orientation={effectiveOrientation}
|
|
148
|
+
data-mode={effectiveMode}
|
|
86
149
|
data-shape={shape}
|
|
87
150
|
data-distribution={distribution}
|
|
151
|
+
data-framed={framed}
|
|
88
152
|
className={cn(
|
|
89
|
-
buttonGroupVariants({
|
|
90
|
-
|
|
91
|
-
|
|
153
|
+
buttonGroupVariants({
|
|
154
|
+
orientation: effectiveOrientation,
|
|
155
|
+
mode: effectiveMode,
|
|
156
|
+
shape,
|
|
157
|
+
distribution,
|
|
158
|
+
framed,
|
|
159
|
+
}),
|
|
160
|
+
effectiveMode === "connected" &&
|
|
161
|
+
(effectiveOrientation === "vertical"
|
|
92
162
|
? "[&>[data-slot=select]:not(:first-child)_[data-slot=select-control]]:rounded-t-none [&>[data-slot=select]:not(:first-child)_[data-slot=select-control]]:border-t-0 [&>[data-slot=select]:not(:last-child)_[data-slot=select-control]]:rounded-b-none [&>[data-slot=select-wrapper]:not(:first-child)_[data-slot=select]]:rounded-t-none [&>[data-slot=select-wrapper]:not(:first-child)_[data-slot=select]]:border-t-0 [&>[data-slot=select-wrapper]:not(:last-child)_[data-slot=select]]:rounded-b-none"
|
|
93
163
|
: "[&>[data-slot=select]:not(:first-child)_[data-slot=select-control]]:rounded-l-none [&>[data-slot=select]:not(:first-child)_[data-slot=select-control]]:border-l-0 [&>[data-slot=select]:not(:last-child)_[data-slot=select-control]]:rounded-r-none [&>[data-slot=select-wrapper]:not(:first-child)_[data-slot=select]]:rounded-l-none [&>[data-slot=select-wrapper]:not(:first-child)_[data-slot=select]]:border-l-0 [&>[data-slot=select-wrapper]:not(:last-child)_[data-slot=select]]:rounded-r-none"),
|
|
94
164
|
className,
|
|
95
165
|
)}
|
|
96
166
|
{...props}
|
|
97
|
-
|
|
167
|
+
>
|
|
168
|
+
{content}
|
|
169
|
+
</div>
|
|
98
170
|
);
|
|
99
171
|
}
|
|
100
172
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import { Slot } from
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
|
|
4
|
-
import type { UiContext } from
|
|
5
|
-
import { cn } from
|
|
4
|
+
import type { UiContext } from "../../../core/ui-context.ts";
|
|
5
|
+
import { cn } from "../../lib/cn.ts";
|
|
6
6
|
import {
|
|
7
7
|
controlGlyphClass,
|
|
8
8
|
controlGlyphSize,
|
|
@@ -11,25 +11,28 @@ import {
|
|
|
11
11
|
focusRing,
|
|
12
12
|
type ControlScale,
|
|
13
13
|
type ControlShape,
|
|
14
|
-
} from
|
|
15
|
-
import { Spinner } from
|
|
14
|
+
} from "./control.ts";
|
|
15
|
+
import { Spinner } from "./spinner.tsx";
|
|
16
16
|
|
|
17
|
-
export type ButtonContext = Extract<
|
|
18
|
-
|
|
17
|
+
export type ButtonContext = Extract<
|
|
18
|
+
UiContext,
|
|
19
|
+
"neutral" | "primary" | "warning" | "danger"
|
|
20
|
+
>;
|
|
21
|
+
export type ButtonVariant = "solid" | "subtle" | "outline" | "ghost" | "link";
|
|
19
22
|
/** A escala completa de control.ts: alturas de linha e quadrados só-ícone. */
|
|
20
|
-
export type ButtonSize = ControlScale
|
|
23
|
+
export type ButtonSize = ControlScale;
|
|
21
24
|
|
|
22
25
|
export interface ButtonStyleOptions {
|
|
23
26
|
/** Sem `context`, `solid`/`subtle` assumem `primary`; `outline`/`ghost`/`link` assumem `neutral`. */
|
|
24
|
-
context?: ButtonContext | null
|
|
27
|
+
context?: ButtonContext | null;
|
|
25
28
|
/** Tratamento visual. */
|
|
26
|
-
variant?: ButtonVariant | null
|
|
27
|
-
size?: ButtonSize | null
|
|
28
|
-
shape?: ControlShape | null
|
|
29
|
+
variant?: ButtonVariant | null;
|
|
30
|
+
size?: ButtonSize | null;
|
|
31
|
+
shape?: ControlShape | null;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
const baseClasses =
|
|
32
|
-
|
|
35
|
+
"inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all disabled:pointer-events-none disabled:opacity-50 active:brightness-90 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&_svg]:pointer-events-none [&_svg]:shrink-0";
|
|
33
36
|
|
|
34
37
|
/** Padding e ritmo por tamanho; altura e quadrado vêm da escala compartilhada. */
|
|
35
38
|
const sizeClasses: Record<ButtonSize, string> = {
|
|
@@ -37,52 +40,73 @@ const sizeClasses: Record<ButtonSize, string> = {
|
|
|
37
40
|
sm: `${controlHeight.sm} gap-1.5 px-3 has-[>svg]:px-2.5`,
|
|
38
41
|
default: `${controlHeight.default} px-4 py-2 has-[>svg]:px-3`,
|
|
39
42
|
lg: `${controlHeight.lg} px-6 has-[>svg]:px-4`,
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
"icon-xs": controlSquare["icon-xs"],
|
|
44
|
+
"icon-sm": controlSquare["icon-sm"],
|
|
42
45
|
icon: controlSquare.icon,
|
|
43
|
-
|
|
44
|
-
}
|
|
46
|
+
"icon-lg": controlSquare["icon-lg"],
|
|
47
|
+
};
|
|
45
48
|
|
|
46
49
|
const solidClasses: Record<ButtonContext, string> = {
|
|
47
|
-
neutral:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
neutral:
|
|
51
|
+
"bg-context-neutral text-context-neutral-foreground hover:brightness-95",
|
|
52
|
+
primary:
|
|
53
|
+
"bg-context-primary text-context-primary-foreground hover:brightness-95",
|
|
54
|
+
warning:
|
|
55
|
+
"bg-context-warning text-context-warning-foreground hover:brightness-95",
|
|
56
|
+
danger:
|
|
57
|
+
"bg-context-danger text-context-danger-foreground hover:brightness-95 focus-visible:ring-context-danger/20",
|
|
58
|
+
};
|
|
51
59
|
|
|
52
60
|
const subtleClasses: Record<ButtonContext, string> = {
|
|
53
|
-
neutral:
|
|
54
|
-
primary:
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
neutral: "bg-context-neutral-subtle text-foreground hover:bg-context-neutral",
|
|
62
|
+
primary:
|
|
63
|
+
"bg-context-primary-subtle text-context-primary-emphasis hover:bg-context-primary/15",
|
|
64
|
+
warning:
|
|
65
|
+
"bg-context-warning-subtle text-context-warning-emphasis hover:bg-context-warning/15",
|
|
66
|
+
danger:
|
|
67
|
+
"bg-context-danger-subtle text-context-danger-emphasis hover:bg-context-danger/15",
|
|
68
|
+
};
|
|
57
69
|
|
|
58
70
|
const outlineClasses: Record<ButtonContext, string> = {
|
|
59
|
-
neutral:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
neutral:
|
|
72
|
+
"border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
73
|
+
primary:
|
|
74
|
+
"border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
75
|
+
warning:
|
|
76
|
+
"border border-context-warning-border bg-transparent text-context-warning-emphasis hover:bg-context-warning-subtle",
|
|
77
|
+
danger:
|
|
78
|
+
"border border-context-danger-border bg-transparent text-context-danger-emphasis hover:bg-context-danger-subtle",
|
|
79
|
+
};
|
|
63
80
|
|
|
64
81
|
const ghostClasses: Record<ButtonContext, string> = {
|
|
65
|
-
neutral:
|
|
66
|
-
primary:
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
neutral: "text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
83
|
+
primary: "text-context-primary-emphasis hover:bg-context-primary-subtle",
|
|
84
|
+
warning: "text-context-warning-emphasis hover:bg-context-warning-subtle",
|
|
85
|
+
danger: "text-context-danger-emphasis hover:bg-context-danger-subtle",
|
|
86
|
+
};
|
|
69
87
|
|
|
70
88
|
const linkClasses: Record<ButtonContext, string> = {
|
|
71
|
-
neutral:
|
|
72
|
-
primary:
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
neutral: "text-foreground underline-offset-4 hover:underline",
|
|
90
|
+
primary: "text-context-primary-emphasis underline-offset-4 hover:underline",
|
|
91
|
+
warning: "text-context-warning-emphasis underline-offset-4 hover:underline",
|
|
92
|
+
danger: "text-context-danger-emphasis underline-offset-4 hover:underline",
|
|
93
|
+
};
|
|
75
94
|
|
|
76
|
-
export function resolveButtonStyle({
|
|
77
|
-
context
|
|
78
|
-
variant
|
|
95
|
+
export function resolveButtonStyle({
|
|
96
|
+
context,
|
|
97
|
+
variant,
|
|
98
|
+
}: ButtonStyleOptions = {}): {
|
|
99
|
+
context: ButtonContext;
|
|
100
|
+
variant: ButtonVariant;
|
|
79
101
|
} {
|
|
80
|
-
const resolvedVariant: ButtonVariant = variant ??
|
|
102
|
+
const resolvedVariant: ButtonVariant = variant ?? "solid";
|
|
81
103
|
// Tratamentos de superfície (solid/subtle) pedem destaque; os leves (outline/ghost/link)
|
|
82
104
|
// são o chrome discreto da casa e só ganham cor quando o contexto é declarado.
|
|
83
105
|
const defaultContext: ButtonContext =
|
|
84
|
-
resolvedVariant ===
|
|
85
|
-
|
|
106
|
+
resolvedVariant === "solid" || resolvedVariant === "subtle"
|
|
107
|
+
? "primary"
|
|
108
|
+
: "neutral";
|
|
109
|
+
return { context: context ?? defaultContext, variant: resolvedVariant };
|
|
86
110
|
}
|
|
87
111
|
|
|
88
112
|
/**
|
|
@@ -90,35 +114,36 @@ export function resolveButtonStyle({ context, variant }: ButtonStyleOptions = {}
|
|
|
90
114
|
* `size` a medida na escala compartilhada de control.ts.
|
|
91
115
|
*/
|
|
92
116
|
export function buttonVariants(options: ButtonStyleOptions = {}): string {
|
|
93
|
-
const resolved = resolveButtonStyle(options)
|
|
117
|
+
const resolved = resolveButtonStyle(options);
|
|
94
118
|
const variantClasses =
|
|
95
|
-
resolved.variant ===
|
|
119
|
+
resolved.variant === "solid"
|
|
96
120
|
? solidClasses[resolved.context]
|
|
97
|
-
: resolved.variant ===
|
|
121
|
+
: resolved.variant === "subtle"
|
|
98
122
|
? subtleClasses[resolved.context]
|
|
99
|
-
: resolved.variant ===
|
|
123
|
+
: resolved.variant === "outline"
|
|
100
124
|
? outlineClasses[resolved.context]
|
|
101
|
-
: resolved.variant ===
|
|
125
|
+
: resolved.variant === "ghost"
|
|
102
126
|
? ghostClasses[resolved.context]
|
|
103
|
-
: linkClasses[resolved.context]
|
|
127
|
+
: linkClasses[resolved.context];
|
|
104
128
|
return cn(
|
|
105
129
|
baseClasses,
|
|
106
130
|
focusRing,
|
|
107
131
|
variantClasses,
|
|
108
|
-
sizeClasses[options.size ??
|
|
132
|
+
sizeClasses[options.size ?? "default"],
|
|
109
133
|
controlGlyphClass(options.size),
|
|
110
|
-
options.shape ===
|
|
111
|
-
)
|
|
134
|
+
options.shape === "pill" && "rounded-full",
|
|
135
|
+
);
|
|
112
136
|
}
|
|
113
137
|
|
|
114
|
-
export interface ButtonProps
|
|
138
|
+
export interface ButtonProps
|
|
139
|
+
extends React.ComponentProps<"button">, ButtonStyleOptions {
|
|
115
140
|
/** Renderiza como o filho (Radix Slot) em vez de <button> — recompõe sem forkar. */
|
|
116
|
-
asChild?: boolean
|
|
141
|
+
asChild?: boolean;
|
|
117
142
|
/** Estado de ação em andamento; mostra Spinner e desabilita o controle. */
|
|
118
|
-
busy?: boolean
|
|
143
|
+
busy?: boolean;
|
|
119
144
|
/** Ícone à esquerda, como nó (`icon={<Plus />}`); o tamanho do glifo vem do `size`. No
|
|
120
145
|
* `busy`, o Spinner ocupa o mesmo espaço. */
|
|
121
|
-
icon?: React.ReactNode
|
|
146
|
+
icon?: React.ReactNode;
|
|
122
147
|
}
|
|
123
148
|
|
|
124
149
|
export function Button({
|
|
@@ -134,8 +159,8 @@ export function Button({
|
|
|
134
159
|
children,
|
|
135
160
|
...props
|
|
136
161
|
}: ButtonProps): React.ReactElement {
|
|
137
|
-
const Comp = asChild ? Slot :
|
|
138
|
-
const resolved = resolveButtonStyle({ context, variant })
|
|
162
|
+
const Comp = asChild ? Slot : "button";
|
|
163
|
+
const resolved = resolveButtonStyle({ context, variant });
|
|
139
164
|
return (
|
|
140
165
|
<Comp
|
|
141
166
|
data-slot="button"
|
|
@@ -154,5 +179,5 @@ export function Button({
|
|
|
154
179
|
</>
|
|
155
180
|
)}
|
|
156
181
|
</Comp>
|
|
157
|
-
)
|
|
182
|
+
);
|
|
158
183
|
}
|
|
@@ -1,66 +1,68 @@
|
|
|
1
|
-
import * as React from
|
|
1
|
+
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
import { cn } from
|
|
4
|
-
import { EmptyValue, isAbsentValue } from
|
|
3
|
+
import { cn } from "../../lib/cn.ts";
|
|
4
|
+
import { EmptyValue, isAbsentValue } from "./empty-value.tsx";
|
|
5
5
|
|
|
6
|
-
export type DetailGroupVariant =
|
|
7
|
-
export type DetailGroupOrientation =
|
|
8
|
-
export type DetailGroupColumns = 1 | 2 | 3 | 4 |
|
|
6
|
+
export type DetailGroupVariant = "plain" | "framed";
|
|
7
|
+
export type DetailGroupOrientation = "vertical" | "horizontal";
|
|
8
|
+
export type DetailGroupColumns = 1 | 2 | 3 | 4 | "auto";
|
|
9
9
|
|
|
10
|
-
export interface DetailGroupProps extends React.ComponentProps<
|
|
11
|
-
/** `framed` aplica a moldura canônica ao conjunto
|
|
12
|
-
variant?: DetailGroupVariant
|
|
13
|
-
/** Exibe hairlines
|
|
14
|
-
dividers?: boolean
|
|
10
|
+
export interface DetailGroupProps extends React.ComponentProps<"dl"> {
|
|
11
|
+
/** `framed` aplica a moldura canônica ao conjunto. */
|
|
12
|
+
variant?: DetailGroupVariant;
|
|
13
|
+
/** Exibe hairlines entre os campos; na orientação horizontal, aplica o tratamento tabular aos rótulos. */
|
|
14
|
+
dividers?: boolean;
|
|
15
15
|
/** Número fixo de colunas ou distribuição automática por largura mínima. */
|
|
16
|
-
columns?: DetailGroupColumns
|
|
16
|
+
columns?: DetailGroupColumns;
|
|
17
17
|
/** Organiza a chave sobre o valor ou ao lado dele em cada campo. */
|
|
18
|
-
orientation?: DetailGroupOrientation
|
|
18
|
+
orientation?: DetailGroupOrientation;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const columnClasses: Record<DetailGroupColumns, string> = {
|
|
22
|
-
1:
|
|
23
|
-
2:
|
|
24
|
-
3:
|
|
25
|
-
4:
|
|
26
|
-
auto:
|
|
27
|
-
}
|
|
22
|
+
1: "grid-cols-1",
|
|
23
|
+
2: "grid-cols-2",
|
|
24
|
+
3: "grid-cols-3",
|
|
25
|
+
4: "grid-cols-4",
|
|
26
|
+
auto: "grid-cols-[repeat(auto-fit,minmax(min(100%,14rem),1fr))]",
|
|
27
|
+
};
|
|
28
28
|
|
|
29
29
|
const DetailGroupLayoutContext = React.createContext({
|
|
30
|
-
|
|
31
|
-
})
|
|
30
|
+
dividedHorizontalField: false,
|
|
31
|
+
});
|
|
32
32
|
|
|
33
33
|
function DetailGroup({
|
|
34
34
|
className,
|
|
35
35
|
children,
|
|
36
36
|
style,
|
|
37
|
-
variant =
|
|
37
|
+
variant = "plain",
|
|
38
38
|
dividers = false,
|
|
39
39
|
columns = 1,
|
|
40
|
-
orientation =
|
|
40
|
+
orientation = "vertical",
|
|
41
41
|
...props
|
|
42
42
|
}: DetailGroupProps): React.ReactElement {
|
|
43
|
-
const
|
|
43
|
+
const dividedHorizontalField = dividers && orientation === "horizontal";
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
|
-
<DetailGroupLayoutContext.Provider value={{
|
|
46
|
+
<DetailGroupLayoutContext.Provider value={{ dividedHorizontalField }}>
|
|
47
47
|
<dl
|
|
48
48
|
data-slot="detail-group"
|
|
49
49
|
data-variant={variant}
|
|
50
50
|
data-dividers={dividers}
|
|
51
51
|
data-orientation={orientation}
|
|
52
52
|
className={cn(
|
|
53
|
-
|
|
53
|
+
"group/detail-group grid min-w-0 [--detail-label-width:7rem]",
|
|
54
54
|
columnClasses[columns],
|
|
55
|
-
variant ===
|
|
56
|
-
dividers &&
|
|
57
|
-
(variant ===
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
variant === "plain" && !dividers && "gap-x-6 gap-y-4",
|
|
56
|
+
dividers && "gap-px overflow-hidden",
|
|
57
|
+
(variant === "framed" || dividers) &&
|
|
58
|
+
!dividedHorizontalField &&
|
|
59
|
+
"[&>[data-slot=detail-field]]:p-3",
|
|
60
|
+
variant === "framed" &&
|
|
61
|
+
"overflow-hidden rounded-lg border border-border bg-transparent text-foreground",
|
|
60
62
|
// Default da coluna de rótulos como CLASSE, antes do `className`: assim uma classe do
|
|
61
63
|
// consumidor (`[--detail-label-width:9rem]`) a substitui. Em `style` inline, o default
|
|
62
64
|
// vencia qualquer classe e só `style` conseguia ajustar a largura.
|
|
63
|
-
|
|
65
|
+
dividedHorizontalField && "[--detail-label-width:8.5rem]",
|
|
64
66
|
className,
|
|
65
67
|
)}
|
|
66
68
|
style={style}
|
|
@@ -69,19 +71,22 @@ function DetailGroup({
|
|
|
69
71
|
{children}
|
|
70
72
|
</dl>
|
|
71
73
|
</DetailGroupLayoutContext.Provider>
|
|
72
|
-
)
|
|
74
|
+
);
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
export interface DetailFieldProps extends Omit<
|
|
76
|
-
|
|
77
|
+
export interface DetailFieldProps extends Omit<
|
|
78
|
+
React.ComponentProps<"div">,
|
|
79
|
+
"children"
|
|
80
|
+
> {
|
|
81
|
+
label: React.ReactNode;
|
|
77
82
|
/** `null`, `undefined`, string vazia ou só com espaços renderizam a ausência
|
|
78
83
|
* como travessão; “Não informado” ou `empty` preservam o significado para
|
|
79
84
|
* leitura assistiva. `0` e `false` seguem como valores. */
|
|
80
|
-
value: React.ReactNode
|
|
85
|
+
value: React.ReactNode;
|
|
81
86
|
/** Ícone decorativo que identifica o campo antes do par chave/valor. */
|
|
82
|
-
icon?: React.ReactNode
|
|
87
|
+
icon?: React.ReactNode;
|
|
83
88
|
/** O que a ausência significa neste campo: um rótulo (“Nunca enviado”) ou um nó próprio. */
|
|
84
|
-
empty?: React.ReactNode
|
|
89
|
+
empty?: React.ReactNode;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
function DetailField({
|
|
@@ -93,32 +98,33 @@ function DetailField({
|
|
|
93
98
|
style,
|
|
94
99
|
...props
|
|
95
100
|
}: DetailFieldProps): React.ReactElement {
|
|
96
|
-
const {
|
|
101
|
+
const { dividedHorizontalField } = React.useContext(DetailGroupLayoutContext);
|
|
97
102
|
const content = !isAbsentValue(value) ? (
|
|
98
103
|
value
|
|
99
|
-
) : empty === undefined || typeof empty ===
|
|
104
|
+
) : empty === undefined || typeof empty === "string" ? (
|
|
100
105
|
<EmptyValue compact {...(empty !== undefined ? { label: empty } : {})} />
|
|
101
106
|
) : (
|
|
102
107
|
empty
|
|
103
|
-
)
|
|
108
|
+
);
|
|
104
109
|
return (
|
|
105
110
|
<div
|
|
106
111
|
data-slot="detail-field"
|
|
107
112
|
className={cn(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
!
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
"grid min-w-0 items-start",
|
|
114
|
+
"group-data-[orientation=vertical]/detail-group:grid-cols-1",
|
|
115
|
+
!dividedHorizontalField &&
|
|
116
|
+
"group-data-[orientation=horizontal]/detail-group:grid-cols-[minmax(0,var(--detail-label-width))_minmax(0,1fr)] group-data-[orientation=horizontal]/detail-group:items-baseline group-data-[orientation=horizontal]/detail-group:gap-x-3",
|
|
117
|
+
dividedHorizontalField && "items-stretch gap-x-0",
|
|
118
|
+
"group-data-[orientation=vertical]/detail-group:gap-y-1",
|
|
119
|
+
"group-data-[dividers=true]/detail-group:outline group-data-[dividers=true]/detail-group:outline-1 group-data-[dividers=true]/detail-group:outline-border",
|
|
120
|
+
"group-data-[variant=framed]/detail-group:bg-transparent group-data-[variant=framed]/detail-group:text-foreground",
|
|
116
121
|
className,
|
|
117
122
|
)}
|
|
118
123
|
style={
|
|
119
|
-
|
|
124
|
+
dividedHorizontalField
|
|
120
125
|
? {
|
|
121
|
-
gridTemplateColumns:
|
|
126
|
+
gridTemplateColumns:
|
|
127
|
+
"minmax(0, var(--detail-label-width)) minmax(0, 1fr)",
|
|
122
128
|
...style,
|
|
123
129
|
}
|
|
124
130
|
: style
|
|
@@ -128,8 +134,8 @@ function DetailField({
|
|
|
128
134
|
<dt
|
|
129
135
|
data-slot="detail-field-label"
|
|
130
136
|
className={cn(
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
"flex items-center gap-1.5 text-sm leading-snug font-medium text-muted-foreground",
|
|
138
|
+
dividedHorizontalField && "bg-muted/30 p-3 dark:bg-input/30",
|
|
133
139
|
)}
|
|
134
140
|
>
|
|
135
141
|
{icon !== undefined && (
|
|
@@ -146,14 +152,14 @@ function DetailField({
|
|
|
146
152
|
<dd
|
|
147
153
|
data-slot="detail-field-value"
|
|
148
154
|
className={cn(
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
"min-w-0 text-sm leading-snug font-normal",
|
|
156
|
+
dividedHorizontalField && "flex items-center p-3",
|
|
151
157
|
)}
|
|
152
158
|
>
|
|
153
159
|
{content}
|
|
154
160
|
</dd>
|
|
155
161
|
</div>
|
|
156
|
-
)
|
|
162
|
+
);
|
|
157
163
|
}
|
|
158
164
|
|
|
159
|
-
export { DetailGroup, DetailField }
|
|
165
|
+
export { DetailGroup, DetailField };
|
|
@@ -18,10 +18,7 @@ import {
|
|
|
18
18
|
SurfaceMedia,
|
|
19
19
|
type SurfaceMediaProps,
|
|
20
20
|
} from "../patterns/surface-media.tsx";
|
|
21
|
-
import {
|
|
22
|
-
CloseButton,
|
|
23
|
-
type CloseButtonSize,
|
|
24
|
-
} from "./close-button.tsx";
|
|
21
|
+
import { CloseButton, type CloseButtonSize } from "./close-button.tsx";
|
|
25
22
|
|
|
26
23
|
export type DialogMode = "default" | "alert";
|
|
27
24
|
|
|
@@ -349,7 +346,10 @@ export function DialogHeader({
|
|
|
349
346
|
}
|
|
350
347
|
|
|
351
348
|
/** Região introdutória do header — o voltar de um fluxo modal encadeado. */
|
|
352
|
-
export function DialogNavigation({
|
|
349
|
+
export function DialogNavigation({
|
|
350
|
+
className,
|
|
351
|
+
...props
|
|
352
|
+
}: React.ComponentProps<"div">) {
|
|
353
353
|
return (
|
|
354
354
|
<div
|
|
355
355
|
data-slot="dialog-navigation-content"
|
|
@@ -412,11 +412,7 @@ export function DialogBody({
|
|
|
412
412
|
),
|
|
413
413
|
className,
|
|
414
414
|
)}
|
|
415
|
-
style={
|
|
416
|
-
mode === "alert"
|
|
417
|
-
? style
|
|
418
|
-
: ({ "--opus-inline-gutter": "1rem", ...style } as React.CSSProperties)
|
|
419
|
-
}
|
|
415
|
+
style={style}
|
|
420
416
|
{...props}
|
|
421
417
|
/>
|
|
422
418
|
);
|
|
@@ -456,7 +452,8 @@ export function DialogTitle({
|
|
|
456
452
|
}) {
|
|
457
453
|
const { mode } = useDialogContext();
|
|
458
454
|
const layout = useSurfaceLayout("dialog");
|
|
459
|
-
const Title =
|
|
455
|
+
const Title =
|
|
456
|
+
mode === "alert" ? AlertDialogPrimitive.Title : DialogPrimitive.Title;
|
|
460
457
|
const classes = cn(
|
|
461
458
|
mode === "alert" ? undefined : "min-w-0 truncate",
|
|
462
459
|
surfaceTitleClasses("dialog", layout),
|