@softize/opus 21.0.1 → 22.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/bin/cli.mjs +9 -1
  3. package/bin/lib/check.mjs +37 -29
  4. package/docs/adr/0004-page-content-state-is-composed.md +3 -2
  5. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +3 -0
  6. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +23 -14
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -0
  8. package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +7 -4
  9. package/docs/adr/0016-list-collection-header-belongs-to-content.md +13 -8
  10. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +27 -26
  11. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +1 -0
  12. package/docs/adr/0029-page-header-is-the-single-page-introduction.md +71 -0
  13. package/docs/adr/0030-framed-content-can-use-sectioned-anatomy.md +89 -0
  14. package/docs/adr/0031-detail-dividers-own-the-tabular-label-treatment.md +61 -0
  15. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +59 -0
  16. package/docs/adr/0033-drawer-uses-an-inset-structural-frame.md +72 -0
  17. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +60 -0
  18. package/docs/adr/0035-button-group-separates-frame-relationship-and-distribution.md +63 -0
  19. package/docs/adr/0036-tabs-declares-trigger-distribution.md +58 -0
  20. package/docs/adr/0037-design-data-lives-outside-the-contract.md +117 -0
  21. package/docs/radius-scale.md +5 -2
  22. package/package.json +5 -1
  23. package/registry/skills/build-opus-ui/SKILL.md +9 -6
  24. package/registry/skills/build-opus-ui/references/evaluations.md +5 -2
  25. package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -16
  26. package/registry/skills/create-opus-action/references/contract-and-binding.md +5 -3
  27. package/registry/skills/create-opus-action/scripts/scaffold.mjs +0 -4
  28. package/registry/skills/test-opus-action/SKILL.md +2 -2
  29. package/registry/skills/test-opus-action/references/harness.md +2 -2
  30. package/registry/templates/app/src/App.tsx +1 -1
  31. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +2 -19
  32. package/registry/templates/app/src/domains/tasks/actions/list.ts +3 -5
  33. package/registry/templates/app/vite.config.ts +3 -2
  34. package/src/core/domain.ts +2 -2
  35. package/src/core/types.ts +6 -20
  36. package/src/design/index.ts +179 -0
  37. package/src/testing/fake.ts +2 -2
  38. package/src/testing/index.ts +7 -3
  39. package/src/ui/components/patterns/content-header.tsx +51 -13
  40. package/src/ui/components/patterns/list.tsx +51 -33
  41. package/src/ui/components/patterns/page.tsx +139 -251
  42. package/src/ui/components/patterns/presentation.tsx +2 -6
  43. package/src/ui/components/primitives/button-group.tsx +80 -8
  44. package/src/ui/components/primitives/button.tsx +85 -60
  45. package/src/ui/components/primitives/detail.tsx +63 -57
  46. package/src/ui/components/primitives/dialog.tsx +8 -11
  47. package/src/ui/components/primitives/drawer.tsx +16 -12
  48. package/src/ui/components/primitives/tabs.tsx +123 -54
  49. package/src/ui/docs/content/actions.md +49 -43
  50. package/src/ui/docs/content/button.md +55 -29
  51. package/src/ui/docs/content/content.md +61 -36
  52. package/src/ui/docs/content/detail.md +28 -19
  53. package/src/ui/docs/content/drawer.md +53 -47
  54. package/src/ui/docs/content/input.md +51 -35
  55. package/src/ui/docs/content/page.md +84 -95
  56. package/src/ui/docs/content/pane.md +83 -63
  57. package/src/ui/docs/content/presentation.md +20 -20
  58. package/src/ui/docs/content/tabs.md +107 -36
  59. package/src/ui/docs/content/testing.md +9 -25
  60. package/src/ui/meta.ts +5 -5
  61. package/src/ui/react.tsx +0 -1
  62. package/src/vite/design.ts +133 -23
@@ -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
- "w-full gap-2 [&>*]:min-w-0 [&>*]:w-full [&>*]:flex-1",
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={orientation}
85
- data-mode={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({ orientation, mode, shape, distribution }),
90
- mode !== "spaced" &&
91
- (orientation === "vertical"
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 'react'
2
- import { Slot } from '@radix-ui/react-slot'
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
3
 
4
- import type { UiContext } from '../../../core/ui-context.ts'
5
- import { cn } from '../../lib/cn.ts'
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 './control.ts'
15
- import { Spinner } from './spinner.tsx'
14
+ } from "./control.ts";
15
+ import { Spinner } from "./spinner.tsx";
16
16
 
17
- export type ButtonContext = Extract<UiContext, 'neutral' | 'primary' | 'danger'>
18
- export type ButtonVariant = 'solid' | 'subtle' | 'outline' | 'ghost' | 'link'
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
- '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'
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
- 'icon-xs': controlSquare['icon-xs'],
41
- 'icon-sm': controlSquare['icon-sm'],
43
+ "icon-xs": controlSquare["icon-xs"],
44
+ "icon-sm": controlSquare["icon-sm"],
42
45
  icon: controlSquare.icon,
43
- 'icon-lg': controlSquare['icon-lg'],
44
- }
46
+ "icon-lg": controlSquare["icon-lg"],
47
+ };
45
48
 
46
49
  const solidClasses: Record<ButtonContext, string> = {
47
- neutral: 'bg-context-neutral text-context-neutral-foreground hover:brightness-95',
48
- primary: 'bg-context-primary text-context-primary-foreground hover:brightness-95',
49
- danger: 'bg-context-danger text-context-danger-foreground hover:brightness-95 focus-visible:ring-context-danger/20',
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: 'bg-context-neutral-subtle text-foreground hover:bg-context-neutral',
54
- primary: 'bg-context-primary-subtle text-context-primary-emphasis hover:bg-context-primary/15',
55
- danger: 'bg-context-danger-subtle text-context-danger-emphasis hover:bg-context-danger/15',
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: 'border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground',
60
- primary: 'border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground',
61
- danger: 'border border-context-danger-border bg-transparent text-context-danger-emphasis hover:bg-context-danger-subtle',
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: 'text-foreground hover:bg-accent hover:text-accent-foreground',
66
- primary: 'text-context-primary-emphasis hover:bg-context-primary-subtle',
67
- danger: 'text-context-danger-emphasis hover:bg-context-danger-subtle',
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: 'text-foreground underline-offset-4 hover:underline',
72
- primary: 'text-context-primary-emphasis underline-offset-4 hover:underline',
73
- danger: 'text-context-danger-emphasis underline-offset-4 hover:underline',
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({ context, variant }: ButtonStyleOptions = {}): {
77
- context: ButtonContext
78
- variant: ButtonVariant
95
+ export function resolveButtonStyle({
96
+ context,
97
+ variant,
98
+ }: ButtonStyleOptions = {}): {
99
+ context: ButtonContext;
100
+ variant: ButtonVariant;
79
101
  } {
80
- const resolvedVariant: ButtonVariant = variant ?? 'solid'
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 === 'solid' || resolvedVariant === 'subtle' ? 'primary' : 'neutral'
85
- return { context: context ?? defaultContext, variant: resolvedVariant }
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 === 'solid'
119
+ resolved.variant === "solid"
96
120
  ? solidClasses[resolved.context]
97
- : resolved.variant === 'subtle'
121
+ : resolved.variant === "subtle"
98
122
  ? subtleClasses[resolved.context]
99
- : resolved.variant === 'outline'
123
+ : resolved.variant === "outline"
100
124
  ? outlineClasses[resolved.context]
101
- : resolved.variant === 'ghost'
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 ?? 'default'],
132
+ sizeClasses[options.size ?? "default"],
109
133
  controlGlyphClass(options.size),
110
- options.shape === 'pill' && 'rounded-full',
111
- )
134
+ options.shape === "pill" && "rounded-full",
135
+ );
112
136
  }
113
137
 
114
- export interface ButtonProps extends React.ComponentProps<'button'>, ButtonStyleOptions {
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 : 'button'
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 'react'
1
+ import * as React from "react";
2
2
 
3
- import { cn } from '../../lib/cn.ts'
4
- import { EmptyValue, isAbsentValue } from './empty-value.tsx'
3
+ import { cn } from "../../lib/cn.ts";
4
+ import { EmptyValue, isAbsentValue } from "./empty-value.tsx";
5
5
 
6
- export type DetailGroupVariant = 'plain' | 'framed'
7
- export type DetailGroupOrientation = 'vertical' | 'horizontal'
8
- export type DetailGroupColumns = 1 | 2 | 3 | 4 | 'auto'
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<'dl'> {
11
- /** `framed` aplica a moldura canônica ao conjunto; na orientação horizontal, a coluna de rótulos ganha fundo sutil. */
12
- variant?: DetailGroupVariant
13
- /** Exibe hairlines somente entre os campos, sem exigir moldura externa. */
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: '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
- }
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
- splitHorizontalField: false,
31
- })
30
+ dividedHorizontalField: false,
31
+ });
32
32
 
33
33
  function DetailGroup({
34
34
  className,
35
35
  children,
36
36
  style,
37
- variant = 'plain',
37
+ variant = "plain",
38
38
  dividers = false,
39
39
  columns = 1,
40
- orientation = 'vertical',
40
+ orientation = "vertical",
41
41
  ...props
42
42
  }: DetailGroupProps): React.ReactElement {
43
- const splitHorizontalField = variant === 'framed' && orientation === 'horizontal'
43
+ const dividedHorizontalField = dividers && orientation === "horizontal";
44
44
 
45
45
  return (
46
- <DetailGroupLayoutContext.Provider value={{ splitHorizontalField }}>
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
- 'group/detail-group grid min-w-0 [--detail-label-width:7rem]',
53
+ "group/detail-group grid min-w-0 [--detail-label-width:7rem]",
54
54
  columnClasses[columns],
55
- variant === 'plain' && !dividers && 'gap-x-6 gap-y-4',
56
- dividers && 'gap-px overflow-hidden',
57
- (variant === 'framed' || dividers) && !splitHorizontalField && '[&>[data-slot=detail-field]]:p-3',
58
- variant === 'framed' &&
59
- 'overflow-hidden rounded-lg border border-border bg-transparent text-foreground',
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
- splitHorizontalField && '[--detail-label-width:8.5rem]',
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<React.ComponentProps<'div'>, 'children'> {
76
- label: React.ReactNode
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 { splitHorizontalField } = React.useContext(DetailGroupLayoutContext)
101
+ const { dividedHorizontalField } = React.useContext(DetailGroupLayoutContext);
97
102
  const content = !isAbsentValue(value) ? (
98
103
  value
99
- ) : empty === undefined || typeof empty === 'string' ? (
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
- 'grid min-w-0 items-start',
109
- 'group-data-[orientation=vertical]/detail-group:grid-cols-1',
110
- !splitHorizontalField &&
111
- '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',
112
- splitHorizontalField && 'items-stretch gap-x-0',
113
- 'group-data-[orientation=vertical]/detail-group:gap-y-1',
114
- 'group-data-[dividers=true]/detail-group:outline group-data-[dividers=true]/detail-group:outline-1 group-data-[dividers=true]/detail-group:outline-border',
115
- 'group-data-[variant=framed]/detail-group:bg-transparent group-data-[variant=framed]/detail-group:text-foreground',
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
- splitHorizontalField
124
+ dividedHorizontalField
120
125
  ? {
121
- gridTemplateColumns: 'minmax(0, var(--detail-label-width)) minmax(0, 1fr)',
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
- 'flex items-center gap-1.5 text-sm leading-snug font-medium text-muted-foreground',
132
- splitHorizontalField && 'bg-muted/30 p-3 dark:bg-input/30',
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
- 'min-w-0 text-sm leading-snug font-normal',
150
- splitHorizontalField && 'flex items-center p-3',
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({ className, ...props }: React.ComponentProps<"div">) {
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 = mode === "alert" ? AlertDialogPrimitive.Title : DialogPrimitive.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),