@softize/opus 22.2.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.
- package/CHANGELOG.md +120 -1
- package/bin/cli.mjs +3 -9
- package/bin/lib/check.mjs +242 -49
- package/bin/lib/copy.mjs +17 -35
- package/bin/lib/gen-runner.mjs +2 -0
- package/bin/lib/gen-stubs.mjs +3 -1
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/docs/code-style.md +1 -1
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/core/contracts.ts +1 -1
- package/src/core/runtime.ts +4 -37
- package/src/core/types.ts +5 -23
- package/src/design/index.ts +14 -3
- package/src/testing/index.ts +6 -26
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +64 -14
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +93 -70
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +58 -88
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +85 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/actions.md +6 -7
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +100 -58
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +34 -26
- package/src/ui/docs/content/pane.md +41 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/semantic-context.md +0 -1
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +82 -83
- package/src/ui/docs/content/testing.md +2 -3
- package/src/ui/docs/content/toggle.md +120 -63
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +4 -6
- package/src/ui/drivers/react.tsx +4 -3
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +42 -46
- package/src/ui/theme.css +311 -64
- package/src/vite/design.ts +23 -21
- package/src/ui/components/patterns/content-header.tsx +0 -532
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- package/src/ui/docs/content/content.md +0 -276
|
@@ -1,36 +1,43 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import type { AskAnswer, AskQuestion } from
|
|
3
|
-
import { cn } from
|
|
4
|
-
import { Button } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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) =>
|
|
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) =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
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
|
-
<
|
|
71
|
+
<Surface
|
|
63
72
|
asChild
|
|
64
|
-
|
|
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
|
|
87
|
-
|
|
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 ?
|
|
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={
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 ?
|
|
119
|
-
variant={selected ?
|
|
120
|
-
role={question.multiSelect ? undefined :
|
|
121
|
-
aria-checked={
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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(
|
|
182
|
+
? answer.selected.filter(
|
|
183
|
+
(label) => label !== option.label,
|
|
184
|
+
)
|
|
133
185
|
: [...answer.selected, option.label]
|
|
134
|
-
: [option.label]
|
|
135
|
-
changeAt(questionIndex, {
|
|
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>
|
|
199
|
+
<TooltipTrigger asChild>
|
|
200
|
+
{optionButton}
|
|
201
|
+
</TooltipTrigger>
|
|
145
202
|
<TooltipContent>{option.description}</TooltipContent>
|
|
146
203
|
</Tooltip>
|
|
147
204
|
) : (
|
|
148
|
-
<React.Fragment key={option.label}>
|
|
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) =>
|
|
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
|
|
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 ?
|
|
244
|
+
{submitted && !allAnswered ? "Há perguntas sem resposta." : ""}
|
|
178
245
|
</div>
|
|
179
246
|
<div className="mt-4 flex justify-end">
|
|
180
|
-
<Button
|
|
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
|
-
</
|
|
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
|
|
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:
|
|
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-
|
|
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-
|
|
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:
|
|
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
|
|
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:
|
|
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-
|
|
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
|
|
2
|
-
import { SendHorizontalIcon } from
|
|
3
|
-
import { cn } from
|
|
4
|
-
import { Button } from
|
|
5
|
-
import { Spinner } from
|
|
6
|
-
import { Textarea } from
|
|
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 =
|
|
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() !==
|
|
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 ===
|
|
69
|
-
e.preventDefault()
|
|
70
|
-
return
|
|
68
|
+
if (e.key === "ArrowUp" && onHistoryPrevious?.()) {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
return;
|
|
71
71
|
}
|
|
72
|
-
if (e.key ===
|
|
73
|
-
e.preventDefault()
|
|
74
|
-
return
|
|
72
|
+
if (e.key === "ArrowDown" && onHistoryNext?.()) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
return;
|
|
75
75
|
}
|
|
76
|
-
if (e.key ===
|
|
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
|
-
// `
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
115
|
-
twoRow ?
|
|
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
|
|
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
|
|
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
|
|
127
|
+
"group-data-[variant=framed]/detail-group:bg-transparent",
|
|
121
128
|
className,
|
|
122
129
|
)}
|
|
123
130
|
style={
|