@softize/opus 23.0.0 → 24.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +241 -42
  4. package/bin/lib/copy.mjs +16 -34
  5. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  6. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
  7. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
  8. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  9. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  10. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  11. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  12. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  13. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  14. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  15. package/package.json +5 -1
  16. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  17. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  18. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  19. package/registry/templates/app/src/App.tsx +24 -12
  20. package/src/ui/appearance.ts +149 -0
  21. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  22. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  23. package/src/ui/components/patterns/confirm.tsx +3 -1
  24. package/src/ui/components/patterns/fill-region.ts +8 -7
  25. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  26. package/src/ui/components/patterns/form.tsx +8 -7
  27. package/src/ui/components/patterns/list.tsx +15 -19
  28. package/src/ui/components/patterns/page.tsx +15 -11
  29. package/src/ui/components/patterns/pane.tsx +9 -240
  30. package/src/ui/components/patterns/presentation.tsx +14 -14
  31. package/src/ui/components/patterns/sidebar.tsx +7 -17
  32. package/src/ui/components/patterns/split.tsx +111 -59
  33. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  34. package/src/ui/components/patterns/structural-media.tsx +26 -0
  35. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  36. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  37. package/src/ui/components/patterns/surface.tsx +620 -0
  38. package/src/ui/components/patterns/trigger.tsx +11 -9
  39. package/src/ui/components/primitives/ask.tsx +155 -83
  40. package/src/ui/components/primitives/button.tsx +11 -9
  41. package/src/ui/components/primitives/checkbox.tsx +8 -8
  42. package/src/ui/components/primitives/command.tsx +1 -1
  43. package/src/ui/components/primitives/composer.tsx +47 -45
  44. package/src/ui/components/primitives/detail.tsx +9 -2
  45. package/src/ui/components/primitives/dialog.tsx +60 -26
  46. package/src/ui/components/primitives/drawer.tsx +58 -12
  47. package/src/ui/components/primitives/icon-picker.tsx +90 -73
  48. package/src/ui/components/primitives/input-group.tsx +35 -28
  49. package/src/ui/components/primitives/input-otp.tsx +16 -16
  50. package/src/ui/components/primitives/input.tsx +20 -10
  51. package/src/ui/components/primitives/item.tsx +9 -12
  52. package/src/ui/components/primitives/menu.tsx +44 -65
  53. package/src/ui/components/primitives/metric-card.tsx +75 -59
  54. package/src/ui/components/primitives/radio-group.tsx +9 -9
  55. package/src/ui/components/primitives/select.tsx +344 -209
  56. package/src/ui/components/primitives/switch.tsx +10 -10
  57. package/src/ui/components/primitives/table.tsx +29 -24
  58. package/src/ui/components/primitives/tabs.tsx +4 -2
  59. package/src/ui/components/primitives/textarea.tsx +6 -6
  60. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  61. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  62. package/src/ui/components/primitives/toggle.tsx +82 -16
  63. package/src/ui/docs/DocBrowser.tsx +77 -69
  64. package/src/ui/docs/content/action-form-card.md +9 -9
  65. package/src/ui/docs/content/action-list.md +76 -70
  66. package/src/ui/docs/content/action-view.md +33 -18
  67. package/src/ui/docs/content/button.md +1 -1
  68. package/src/ui/docs/content/communication.md +1 -1
  69. package/src/ui/docs/content/customization.md +64 -11
  70. package/src/ui/docs/content/detail.md +8 -6
  71. package/src/ui/docs/content/dialog.md +100 -58
  72. package/src/ui/docs/content/drawer.md +63 -19
  73. package/src/ui/docs/content/field.md +14 -15
  74. package/src/ui/docs/content/icon-tile.md +14 -13
  75. package/src/ui/docs/content/page.md +34 -26
  76. package/src/ui/docs/content/pane.md +41 -290
  77. package/src/ui/docs/content/presentation.md +3 -3
  78. package/src/ui/docs/content/separator.md +6 -6
  79. package/src/ui/docs/content/sidebar.md +68 -266
  80. package/src/ui/docs/content/skeleton.md +20 -18
  81. package/src/ui/docs/content/split.md +14 -14
  82. package/src/ui/docs/content/surface.md +513 -0
  83. package/src/ui/docs/content/tabs.md +20 -20
  84. package/src/ui/docs/content/toggle.md +117 -65
  85. package/src/ui/docs/content/tokens.md +124 -40
  86. package/src/ui/docs/registry.tsx +4 -6
  87. package/src/ui/index.ts +30 -27
  88. package/src/ui/meta.ts +15 -27
  89. package/src/ui/react.tsx +40 -43
  90. package/src/ui/theme.css +311 -64
  91. package/src/ui/components/patterns/content-header.tsx +0 -528
  92. package/src/ui/components/patterns/surface-media.tsx +0 -69
  93. package/src/ui/components/primitives/card.tsx +0 -191
  94. package/src/ui/docs/content/card.md +0 -292
  95. package/src/ui/docs/content/content.md +0 -275
@@ -1,36 +1,43 @@
1
- import * as React from 'react'
2
- import type { AskAnswer, AskQuestion } from '../../../core/index.ts'
3
- import { cn } from '../../lib/cn.ts'
4
- import { Button } from './button.tsx'
5
- import { Card } from './card.tsx'
6
- import { Input } from './input.tsx'
7
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip.tsx'
1
+ import * as React from "react";
2
+ import type { AskAnswer, AskQuestion } from "../../../core/index.ts";
3
+ import { cn } from "../../lib/cn.ts";
4
+ import { Button } from "./button.tsx";
5
+ import { Input } from "./input.tsx";
6
+ import { Surface } from "../patterns/surface.tsx";
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipProvider,
11
+ TooltipTrigger,
12
+ } from "./tooltip.tsx";
8
13
 
9
14
  export interface AskProps {
10
15
  /** Uma a quatro perguntas do contrato de elicitação. */
11
- questions: AskQuestion[]
16
+ questions: AskQuestion[];
12
17
  /** Respostas controladas, na mesma ordem de `questions`. Entradas ausentes são vazias. */
13
- answers: AskAnswer[]
14
- onChange: (answers: AskAnswer[]) => void
18
+ answers: AskAnswer[];
19
+ onChange: (answers: AskAnswer[]) => void;
15
20
  /** Dispara somente quando toda pergunta tem uma opção ou texto livre. */
16
- onSubmit: (answers: AskAnswer[]) => void
17
- disabled?: boolean
18
- busy?: boolean
19
- className?: string
21
+ onSubmit: (answers: AskAnswer[]) => void;
22
+ disabled?: boolean;
23
+ busy?: boolean;
24
+ className?: string;
20
25
  }
21
26
 
22
27
  function answerFor(question: AskQuestion, answer?: AskAnswer): AskAnswer {
23
- const optionLabels = new Set(question.options.map((option) => option.label))
24
- const selected = (answer?.selected ?? []).filter((label) => optionLabels.has(label))
28
+ const optionLabels = new Set(question.options.map((option) => option.label));
29
+ const selected = (answer?.selected ?? []).filter((label) =>
30
+ optionLabels.has(label),
31
+ );
25
32
  return {
26
33
  header: question.header?.trim() || question.question,
27
34
  selected: question.multiSelect ? selected : selected.slice(0, 1),
28
35
  ...(answer?.text === undefined ? {} : { text: answer.text }),
29
- }
36
+ };
30
37
  }
31
38
 
32
39
  function isAnswered(answer: AskAnswer): boolean {
33
- return answer.selected.length > 0 || (answer.text?.trim() ?? '') !== ''
40
+ return answer.selected.length > 0 || (answer.text?.trim() ?? "") !== "";
34
41
  }
35
42
 
36
43
  /** Elicitação estruturada controlada. Não executa transporte nem mantém estado de domínio. */
@@ -43,25 +50,29 @@ export function Ask({
43
50
  busy = false,
44
51
  className,
45
52
  }: AskProps): React.ReactElement {
46
- const id = React.useId()
47
- const questionSignature = JSON.stringify(questions)
48
- const [submittedSignature, setSubmittedSignature] = React.useState<string>()
49
- const submitted = submittedSignature === questionSignature
50
- const normalized = questions.map((question, index) => answerFor(question, answers[index]))
51
- const validCount = questions.length >= 1 && questions.length <= 4
52
- const allAnswered = validCount && normalized.every(isAnswered)
53
- const locked = disabled || busy
53
+ const id = React.useId();
54
+ const questionSignature = JSON.stringify(questions);
55
+ const [submittedSignature, setSubmittedSignature] = React.useState<string>();
56
+ const submitted = submittedSignature === questionSignature;
57
+ const normalized = questions.map((question, index) =>
58
+ answerFor(question, answers[index]),
59
+ );
60
+ const validCount = questions.length >= 1 && questions.length <= 4;
61
+ const allAnswered = validCount && normalized.every(isAnswered);
62
+ const locked = disabled || busy;
54
63
 
55
64
  const changeAt = (index: number, next: AskAnswer): void => {
56
- const nextAnswers = [...normalized]
57
- nextAnswers[index] = next
58
- onChange(nextAnswers)
59
- }
65
+ const nextAnswers = [...normalized];
66
+ nextAnswers[index] = next;
67
+ onChange(nextAnswers);
68
+ };
60
69
 
61
70
  return (
62
- <Card
71
+ <Surface
63
72
  asChild
64
- className={cn('p-4', className)}
73
+ framed
74
+ elevation="raised"
75
+ className={cn("p-4", className)}
65
76
  aria-busy={busy || undefined}
66
77
  aria-disabled={disabled || undefined}
67
78
  >
@@ -69,89 +80,142 @@ export function Ask({
69
80
  data-slot="ask"
70
81
  noValidate
71
82
  onSubmit={(event) => {
72
- event.preventDefault()
73
- setSubmittedSignature(questionSignature)
74
- if (allAnswered && !locked) onSubmit(normalized)
83
+ event.preventDefault();
84
+ setSubmittedSignature(questionSignature);
85
+ if (allAnswered && !locked) onSubmit(normalized);
75
86
  }}
76
87
  >
77
88
  <TooltipProvider>
78
89
  <div className="space-y-4">
79
90
  {questions.map((question, questionIndex) => {
80
- const answer = normalized[questionIndex]
81
- const questionId = `${id}-question-${questionIndex}`
82
- const errorId = `${id}-error-${questionIndex}`
83
- const invalid = submitted && !isAnswered(answer)
91
+ const answer = normalized[questionIndex];
92
+ const questionId = `${id}-question-${questionIndex}`;
93
+ const errorId = `${id}-error-${questionIndex}`;
94
+ const invalid = submitted && !isAnswered(answer);
84
95
 
85
96
  return (
86
- <fieldset key={questionId} disabled={locked} className="min-w-0 space-y-2.5">
87
- <legend id={questionId} className="text-sm font-medium text-foreground">
97
+ <fieldset
98
+ key={questionId}
99
+ disabled={locked}
100
+ className="min-w-0 space-y-2.5"
101
+ >
102
+ <legend
103
+ id={questionId}
104
+ className="text-sm font-medium text-foreground"
105
+ >
88
106
  {question.question}
89
107
  </legend>
90
108
  <div
91
- role={question.multiSelect ? 'group' : 'radiogroup'}
109
+ role={question.multiSelect ? "group" : "radiogroup"}
92
110
  aria-labelledby={questionId}
93
111
  aria-describedby={invalid ? errorId : undefined}
94
112
  className="flex flex-wrap gap-2"
95
- onKeyDown={question.multiSelect ? undefined : (event) => {
96
- if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return
97
- const radios = Array.from(
98
- event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="radio"]:not(:disabled)'),
99
- )
100
- const current = radios.indexOf(document.activeElement as HTMLButtonElement)
101
- if (current < 0 || radios.length === 0) return
102
- event.preventDefault()
103
- const direction = event.key === 'ArrowRight' || event.key === 'ArrowDown' ? 1 : -1
104
- const next = radios[(current + direction + radios.length) % radios.length]
105
- next.focus()
106
- next.click()
107
- }}
113
+ onKeyDown={
114
+ question.multiSelect
115
+ ? undefined
116
+ : (event) => {
117
+ if (
118
+ ![
119
+ "ArrowLeft",
120
+ "ArrowRight",
121
+ "ArrowUp",
122
+ "ArrowDown",
123
+ ].includes(event.key)
124
+ )
125
+ return;
126
+ const radios = Array.from(
127
+ event.currentTarget.querySelectorAll<HTMLButtonElement>(
128
+ '[role="radio"]:not(:disabled)',
129
+ ),
130
+ );
131
+ const current = radios.indexOf(
132
+ document.activeElement as HTMLButtonElement,
133
+ );
134
+ if (current < 0 || radios.length === 0) return;
135
+ event.preventDefault();
136
+ const direction =
137
+ event.key === "ArrowRight" ||
138
+ event.key === "ArrowDown"
139
+ ? 1
140
+ : -1;
141
+ const next =
142
+ radios[
143
+ (current + direction + radios.length) %
144
+ radios.length
145
+ ];
146
+ next.focus();
147
+ next.click();
148
+ }
149
+ }
108
150
  >
109
151
  {question.options.map((option) => {
110
- const selected = answer.selected.includes(option.label)
111
- const firstSelected = answer.selected[0]
112
- const firstOption = question.options[0]?.label
152
+ const selected = answer.selected.includes(option.label);
153
+ const firstSelected = answer.selected[0];
154
+ const firstOption = question.options[0]?.label;
113
155
  const optionButton = (
114
156
  <Button
115
157
  type="button"
116
158
  size="sm"
117
159
  shape="pill"
118
- context={selected ? 'primary' : 'neutral'}
119
- variant={selected ? 'solid' : 'outline'}
120
- role={question.multiSelect ? undefined : 'radio'}
121
- aria-checked={question.multiSelect ? undefined : selected}
122
- aria-pressed={question.multiSelect ? selected : undefined}
123
- tabIndex={question.multiSelect
124
- ? undefined
125
- : selected || (firstSelected === undefined && option.label === firstOption)
126
- ? 0
127
- : -1}
160
+ context={selected ? "primary" : "neutral"}
161
+ variant={selected ? "solid" : "outline"}
162
+ role={question.multiSelect ? undefined : "radio"}
163
+ aria-checked={
164
+ question.multiSelect ? undefined : selected
165
+ }
166
+ aria-pressed={
167
+ question.multiSelect ? selected : undefined
168
+ }
169
+ tabIndex={
170
+ question.multiSelect
171
+ ? undefined
172
+ : selected ||
173
+ (firstSelected === undefined &&
174
+ option.label === firstOption)
175
+ ? 0
176
+ : -1
177
+ }
128
178
  disabled={locked}
129
179
  onClick={() => {
130
180
  const selectedOptions = question.multiSelect
131
181
  ? selected
132
- ? answer.selected.filter((label) => label !== option.label)
182
+ ? answer.selected.filter(
183
+ (label) => label !== option.label,
184
+ )
133
185
  : [...answer.selected, option.label]
134
- : [option.label]
135
- changeAt(questionIndex, { ...answer, selected: selectedOptions })
186
+ : [option.label];
187
+ changeAt(questionIndex, {
188
+ ...answer,
189
+ selected: selectedOptions,
190
+ });
136
191
  }}
137
192
  >
138
193
  {option.label}
139
194
  </Button>
140
- )
195
+ );
141
196
 
142
197
  return option.description ? (
143
198
  <Tooltip key={option.label}>
144
- <TooltipTrigger asChild>{optionButton}</TooltipTrigger>
199
+ <TooltipTrigger asChild>
200
+ {optionButton}
201
+ </TooltipTrigger>
145
202
  <TooltipContent>{option.description}</TooltipContent>
146
203
  </Tooltip>
147
204
  ) : (
148
- <React.Fragment key={option.label}>{optionButton}</React.Fragment>
149
- )
205
+ <React.Fragment key={option.label}>
206
+ {optionButton}
207
+ </React.Fragment>
208
+ );
150
209
  })}
151
210
  </div>
152
211
  <Input
153
- value={answer.text ?? ''}
154
- onChange={(event) => changeAt(questionIndex, { ...answer, text: event.target.value })}
212
+ value={answer.text ?? ""}
213
+ onChange={(event) =>
214
+ changeAt(questionIndex, {
215
+ ...answer,
216
+ text: event.target.value,
217
+ })
218
+ }
155
219
  placeholder="Outro (opcional)…"
156
220
  aria-label={`Outra resposta para: ${question.question}`}
157
221
  aria-invalid={invalid || undefined}
@@ -159,12 +223,15 @@ export function Ask({
159
223
  disabled={locked}
160
224
  />
161
225
  {invalid && (
162
- <p id={errorId} className="text-sm text-context-danger-emphasis">
226
+ <p
227
+ id={errorId}
228
+ className="text-sm text-context-danger-emphasis"
229
+ >
163
230
  Escolha uma opção ou escreva uma resposta.
164
231
  </p>
165
232
  )}
166
233
  </fieldset>
167
- )
234
+ );
168
235
  })}
169
236
  </div>
170
237
  </TooltipProvider>
@@ -174,14 +241,19 @@ export function Ask({
174
241
  </p>
175
242
  )}
176
243
  <div aria-live="polite" className="sr-only">
177
- {submitted && !allAnswered ? 'Há perguntas sem resposta.' : ''}
244
+ {submitted && !allAnswered ? "Há perguntas sem resposta." : ""}
178
245
  </div>
179
246
  <div className="mt-4 flex justify-end">
180
- <Button type="submit" size="sm" busy={busy} disabled={disabled || !validCount}>
247
+ <Button
248
+ type="submit"
249
+ size="sm"
250
+ busy={busy}
251
+ disabled={disabled || !validCount}
252
+ >
181
253
  Responder
182
254
  </Button>
183
255
  </div>
184
256
  </form>
185
- </Card>
186
- )
257
+ </Surface>
258
+ );
187
259
  }
@@ -32,7 +32,7 @@ export interface ButtonStyleOptions {
32
32
  }
33
33
 
34
34
  const baseClasses =
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";
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 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&_svg]:pointer-events-none [&_svg]:shrink-0";
36
36
 
37
37
  /** Padding e ritmo por tamanho; altura e quadrado vêm da escala compartilhada. */
38
38
  const sizeClasses: Record<ButtonSize, string> = {
@@ -48,17 +48,18 @@ const sizeClasses: Record<ButtonSize, string> = {
48
48
 
49
49
  const solidClasses: Record<ButtonContext, string> = {
50
50
  neutral:
51
- "bg-context-neutral text-context-neutral-foreground hover:brightness-95",
51
+ "bg-context-neutral text-context-neutral-foreground hover:brightness-95 active:brightness-90",
52
52
  primary:
53
- "bg-context-primary text-context-primary-foreground hover:brightness-95",
53
+ "bg-context-primary text-context-primary-foreground hover:brightness-95 active:brightness-90",
54
54
  warning:
55
- "bg-context-warning text-context-warning-foreground hover:brightness-95",
55
+ "bg-context-warning text-context-warning-foreground hover:brightness-95 active:brightness-90",
56
56
  danger:
57
- "bg-context-danger text-context-danger-foreground hover:brightness-95 focus-visible:ring-context-danger/20",
57
+ "bg-context-danger text-context-danger-foreground hover:brightness-95 active:brightness-90 focus-visible:ring-context-danger/20",
58
58
  };
59
59
 
60
60
  const subtleClasses: Record<ButtonContext, string> = {
61
- neutral: "bg-context-neutral-subtle text-foreground hover:bg-context-neutral",
61
+ neutral:
62
+ "bg-context-neutral-subtle text-foreground hover:bg-interaction-hover active:bg-interaction-pressed",
62
63
  primary:
63
64
  "bg-context-primary-subtle text-context-primary-emphasis hover:bg-context-primary/15",
64
65
  warning:
@@ -69,9 +70,9 @@ const subtleClasses: Record<ButtonContext, string> = {
69
70
 
70
71
  const outlineClasses: Record<ButtonContext, string> = {
71
72
  neutral:
72
- "border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground",
73
+ "border border-input bg-transparent text-foreground hover:bg-interaction-hover hover:text-accent-foreground active:bg-interaction-pressed",
73
74
  primary:
74
- "border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground",
75
+ "border border-input bg-transparent text-foreground hover:bg-interaction-hover hover:text-accent-foreground active:bg-interaction-pressed",
75
76
  warning:
76
77
  "border border-context-warning-border bg-transparent text-context-warning-emphasis hover:bg-context-warning-subtle",
77
78
  danger:
@@ -79,7 +80,8 @@ const outlineClasses: Record<ButtonContext, string> = {
79
80
  };
80
81
 
81
82
  const ghostClasses: Record<ButtonContext, string> = {
82
- neutral: "text-foreground hover:bg-accent hover:text-accent-foreground",
83
+ neutral:
84
+ "text-foreground hover:bg-interaction-hover hover:text-accent-foreground active:bg-interaction-pressed",
83
85
  primary: "text-context-primary-emphasis hover:bg-context-primary-subtle",
84
86
  warning: "text-context-warning-emphasis hover:bg-context-warning-subtle",
85
87
  danger: "text-context-danger-emphasis hover:bg-context-danger-subtle",
@@ -1,8 +1,8 @@
1
- import * as React from "react"
2
- import { CheckIcon, MinusIcon } from "lucide-react"
3
- import { Checkbox as CheckboxPrimitive } from "radix-ui"
1
+ import * as React from "react";
2
+ import { CheckIcon, MinusIcon } from "lucide-react";
3
+ import { Checkbox as CheckboxPrimitive } from "radix-ui";
4
4
 
5
- import { cn } from '../../lib/cn.ts'
5
+ import { cn } from "../../lib/cn.ts";
6
6
 
7
7
  function Checkbox({
8
8
  className,
@@ -12,8 +12,8 @@ function Checkbox({
12
12
  <CheckboxPrimitive.Root
13
13
  data-slot="checkbox"
14
14
  className={cn(
15
- "peer group/checkbox size-4 shrink-0 rounded-[0.25rem] border border-input transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-context-danger/40 dark:data-[state=checked]:bg-primary dark:data-[state=indeterminate]:bg-primary",
16
- className
15
+ "peer group/checkbox size-4 shrink-0 rounded-[0.25rem] border border-input bg-control transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground dark:aria-invalid:ring-context-danger/40",
16
+ className,
17
17
  )}
18
18
  {...props}
19
19
  >
@@ -32,7 +32,7 @@ function Checkbox({
32
32
  />
33
33
  </CheckboxPrimitive.Indicator>
34
34
  </CheckboxPrimitive.Root>
35
- )
35
+ );
36
36
  }
37
37
 
38
- export { Checkbox }
38
+ export { Checkbox };
@@ -152,7 +152,7 @@ function CommandItem({
152
152
  <CommandPrimitive.Item
153
153
  data-slot="command-item"
154
154
  className={cn(
155
- "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
155
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-interaction-selected data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
156
156
  className,
157
157
  )}
158
158
  {...props}
@@ -1,35 +1,35 @@
1
- import * as React from 'react'
2
- import { SendHorizontalIcon } from 'lucide-react'
3
- import { cn } from '../../lib/cn.ts'
4
- import { Button } from './button.tsx'
5
- import { Spinner } from './spinner.tsx'
6
- import { Textarea } from './textarea.tsx'
1
+ import * as React from "react";
2
+ import { SendHorizontalIcon } from "lucide-react";
3
+ import { cn } from "../../lib/cn.ts";
4
+ import { Button } from "./button.tsx";
5
+ import { Spinner } from "./spinner.tsx";
6
+ import { Textarea } from "./textarea.tsx";
7
7
 
8
8
  export interface ComposerProps {
9
9
  /** Texto (controlado): o dono do estado é o consumidor — o `<Chat>` por dentro, ou o app
10
10
  * (ex.: o composer de criação de sessão do Maestro, que nem tem histórico). */
11
- value: string
12
- onChange: (value: string) => void
11
+ value: string;
12
+ onChange: (value: string) => void;
13
13
  /** Enter (sem Shift) ou o botão enviar. Só dispara quando dá pra enviar (ver `submitDisabled`). */
14
- onSubmit: () => void
14
+ onSubmit: () => void;
15
15
  /** Navegação opcional pelo histórico do dono do composer. Retorne `true` quando a tecla
16
16
  * foi consumida; o `<Chat>` usa isso para ↑/↓ sem interferir no cursor normal. */
17
- onHistoryPrevious?: () => boolean
18
- onHistoryNext?: () => boolean
17
+ onHistoryPrevious?: () => boolean;
18
+ onHistoryNext?: () => boolean;
19
19
  /** Trava o composer enquanto o turno/ação corre — o enviar vira spinner. */
20
- busy?: boolean
20
+ busy?: boolean;
21
21
  /** Gate EXTRA de envio além de "vazio" e "busy" (ex.: falta escolher o app). Desabilita o
22
22
  * enviar sem esconder a barra. */
23
- submitDisabled?: boolean
24
- placeholder?: string
25
- rows?: number
26
- autoFocus?: boolean
23
+ submitDisabled?: boolean;
24
+ placeholder?: string;
25
+ rows?: number;
26
+ autoFocus?: boolean;
27
27
  /** Controles discretos na barra de ações, à ESQUERDA do enviar (seletores de app, agente,
28
28
  * contexto…). Presente → o composer vira DUAS LINHAS (textarea em cima, barra limpa
29
29
  * embaixo); ausente → linha única (textarea + enviar inline), o composer clássico do Chat.
30
30
  * Estilize os controles como discretos (ghost/sem borda) — a barra não é um formulário. */
31
- actions?: React.ReactNode
32
- className?: string
31
+ actions?: React.ReactNode;
32
+ className?: string;
33
33
  }
34
34
 
35
35
  /**
@@ -47,17 +47,17 @@ export function Composer({
47
47
  onHistoryNext,
48
48
  busy = false,
49
49
  submitDisabled = false,
50
- placeholder = 'Escreva uma mensagem…',
50
+ placeholder = "Escreva uma mensagem…",
51
51
  rows = 1,
52
52
  autoFocus,
53
53
  actions,
54
54
  className,
55
55
  }: ComposerProps): React.ReactElement {
56
- const canSubmit = value.trim() !== '' && !busy && !submitDisabled
56
+ const canSubmit = value.trim() !== "" && !busy && !submitDisabled;
57
57
  const fire = (): void => {
58
- if (canSubmit) onSubmit()
59
- }
60
- const twoRow = actions !== undefined
58
+ if (canSubmit) onSubmit();
59
+ };
60
+ const twoRow = actions !== undefined;
61
61
 
62
62
  const textarea = (
63
63
  <Textarea
@@ -65,30 +65,29 @@ export function Composer({
65
65
  value={value}
66
66
  onChange={(e) => onChange(e.target.value)}
67
67
  onKeyDown={(e) => {
68
- if (e.key === 'ArrowUp' && onHistoryPrevious?.()) {
69
- e.preventDefault()
70
- return
68
+ if (e.key === "ArrowUp" && onHistoryPrevious?.()) {
69
+ e.preventDefault();
70
+ return;
71
71
  }
72
- if (e.key === 'ArrowDown' && onHistoryNext?.()) {
73
- e.preventDefault()
74
- return
72
+ if (e.key === "ArrowDown" && onHistoryNext?.()) {
73
+ e.preventDefault();
74
+ return;
75
75
  }
76
- if (e.key === 'Enter' && !e.shiftKey) {
77
- e.preventDefault()
78
- fire()
76
+ if (e.key === "Enter" && !e.shiftKey) {
77
+ e.preventDefault();
78
+ fire();
79
79
  }
80
80
  }}
81
81
  placeholder={placeholder}
82
82
  rows={rows}
83
83
  className={cn(
84
- // `dark:bg-transparent` sobrescreve o `dark:bg-input/30` do primitivo Textarea —
85
- // dentro da pílula do composer a textarea é PLANA (herda o bg-card), sem fundo de
86
- // input próprio no escuro (o `bg-transparent` sozinho não pega a variante dark).
87
- 'max-h-40 min-h-0 resize-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
88
- twoRow ? 'w-full px-1.5 py-1.5' : 'flex-1 px-0 py-2',
84
+ // `bg-transparent` sobrescreve a superfície `control` do primitivo Textarea — dentro da
85
+ // pílula do composer a textarea é PLANA (herda o bg-card), sem fundo próprio.
86
+ "max-h-40 min-h-0 resize-none border-0 bg-transparent shadow-none focus-visible:ring-0",
87
+ twoRow ? "w-full px-1.5 py-1.5" : "flex-1 px-0 py-2",
89
88
  )}
90
89
  />
91
- )
90
+ );
92
91
  const send = (
93
92
  <Button
94
93
  type="submit"
@@ -101,25 +100,28 @@ export function Composer({
101
100
  >
102
101
  {busy ? <Spinner size="sm" /> : <SendHorizontalIcon className="size-4" />}
103
102
  </Button>
104
- )
103
+ );
105
104
 
106
105
  return (
107
106
  <form
108
107
  data-slot="composer"
109
108
  onSubmit={(e) => {
110
- e.preventDefault()
111
- fire()
109
+ e.preventDefault();
110
+ fire();
112
111
  }}
113
112
  className={cn(
114
- 'rounded-xl border border-border bg-card text-card-foreground shadow-sm transition',
115
- twoRow ? 'p-2' : 'relative flex items-end gap-2 p-2 pl-3.5',
113
+ "rounded-xl border border-border bg-card text-card-foreground shadow-sm transition",
114
+ twoRow ? "p-2" : "relative flex items-end gap-2 p-2 pl-3.5",
116
115
  className,
117
116
  )}
118
117
  >
119
118
  {twoRow ? (
120
119
  <>
121
120
  {textarea}
122
- <div data-slot="composer-actions" className="mt-1 flex items-center justify-between gap-2">
121
+ <div
122
+ data-slot="composer-actions"
123
+ className="mt-1 flex items-center justify-between gap-2"
124
+ >
123
125
  {/* Seletores à esquerda (discretos), enviar à direita. */}
124
126
  <div className="flex min-w-0 items-center gap-1">{actions}</div>
125
127
  {send}
@@ -132,5 +134,5 @@ export function Composer({
132
134
  </>
133
135
  )}
134
136
  </form>
135
- )
137
+ );
136
138
  }
@@ -4,12 +4,15 @@ import { cn } from "../../lib/cn.ts";
4
4
  import { EmptyValue, isAbsentValue } from "./empty-value.tsx";
5
5
 
6
6
  export type DetailGroupVariant = "plain" | "framed";
7
+ export type DetailGroupElevation = "none" | "raised";
7
8
  export type DetailGroupOrientation = "vertical" | "horizontal";
8
9
  export type DetailGroupColumns = 1 | 2 | 3 | 4 | "auto";
9
10
 
10
11
  export interface DetailGroupProps extends React.ComponentProps<"dl"> {
11
12
  /** `framed` aplica a moldura canônica ao conjunto. */
12
13
  variant?: DetailGroupVariant;
14
+ /** `raised` usa o fundo de superfície elevada; `none` herda o fundo do hospedeiro. */
15
+ elevation?: DetailGroupElevation;
13
16
  /** Exibe hairlines entre os campos; na orientação horizontal, aplica o tratamento tabular aos rótulos. */
14
17
  dividers?: boolean;
15
18
  /** Número fixo de colunas ou distribuição automática por largura mínima. */
@@ -35,6 +38,7 @@ function DetailGroup({
35
38
  children,
36
39
  style,
37
40
  variant = "plain",
41
+ elevation = "raised",
38
42
  dividers = false,
39
43
  columns = 1,
40
44
  orientation = "vertical",
@@ -47,6 +51,7 @@ function DetailGroup({
47
51
  <dl
48
52
  data-slot="detail-group"
49
53
  data-variant={variant}
54
+ data-elevation={elevation}
50
55
  data-dividers={dividers}
51
56
  data-orientation={orientation}
52
57
  className={cn(
@@ -58,7 +63,9 @@ function DetailGroup({
58
63
  !dividedHorizontalField &&
59
64
  "[&>[data-slot=detail-field]]:p-3",
60
65
  variant === "framed" &&
61
- "overflow-hidden rounded-lg border border-border bg-transparent text-foreground",
66
+ "overflow-hidden rounded-lg border border-border",
67
+ elevation === "raised" &&
68
+ "bg-card text-card-foreground [--surface-background:var(--card)]",
62
69
  // Default da coluna de rótulos como CLASSE, antes do `className`: assim uma classe do
63
70
  // consumidor (`[--detail-label-width:9rem]`) a substitui. Em `style` inline, o default
64
71
  // vencia qualquer classe e só `style` conseguia ajustar a largura.
@@ -117,7 +124,7 @@ function DetailField({
117
124
  dividedHorizontalField && "items-stretch gap-x-0",
118
125
  "group-data-[orientation=vertical]/detail-group:gap-y-1",
119
126
  "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",
127
+ "group-data-[variant=framed]/detail-group:bg-transparent",
121
128
  className,
122
129
  )}
123
130
  style={