@softize/opus 9.1.0 → 10.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 +34 -3
- package/bin/lib/check.mjs +37 -1
- package/docs/elevation-scale.md +2 -2
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +8 -3
- package/registry/skills/build-opus-ui/references/evaluations.md +2 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +3 -0
- package/src/ui/components/primitives/alert-dialog.tsx +1 -1
- package/src/ui/components/primitives/ask.tsx +186 -0
- package/src/ui/components/primitives/calendar.tsx +1 -1
- package/src/ui/components/primitives/card.tsx +3 -3
- package/src/ui/components/primitives/chat.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +2 -2
- package/src/ui/components/primitives/dialog.tsx +1 -1
- package/src/ui/components/primitives/menu.tsx +2 -2
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/docs/content/ask.md +31 -0
- package/src/ui/docs/content/card.md +1 -1
- package/src/ui/docs/content/composer.md +1 -1
- package/src/ui/docs/content/tokens.md +4 -7
- package/src/ui/docs/registry.tsx +2 -0
- package/src/ui/meta.ts +7 -1
- package/src/ui/react.tsx +2 -0
- package/src/ui/theme.css +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,37 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
11
11
|
> tinha ficado sem registro nenhum, o que é exatamente o caso que este arquivo existe
|
|
12
12
|
> pra cobrir.
|
|
13
13
|
|
|
14
|
+
## 10.0.0 — 2026-08-20
|
|
15
|
+
|
|
16
|
+
**Breaking — forma usa a escala do Tailwind e superfícies sempre carregam seu foreground.**
|
|
17
|
+
|
|
18
|
+
- `Card` volta a ser flat por padrão, alinhando a implementação ao contrato já documentado
|
|
19
|
+
no componente e no catálogo. Consumidores que precisam de elevação devem declarar a
|
|
20
|
+
intensidade explicitamente (`shadow-sm`, `shadow-md`, etc.).
|
|
21
|
+
- Forma deixa de criar uma taxonomia paralela por componente: `rounded-card` e
|
|
22
|
+
`rounded-dialog` migram para `rounded-xl`; `rounded-popover`, para `rounded-md`. Os tokens
|
|
23
|
+
`--radius-card/popover/dialog` saem, e `opus check` acusa classes e overrides antigos.
|
|
24
|
+
- Superfícies `card` e `popover` passam a exigir seu foreground no mesmo fragmento de
|
|
25
|
+
classes. Dialog, Composer, Chat e o fundo nativo do Calendar foram corrigidos; `opus check`
|
|
26
|
+
agora reprova `bg-card` sem `text-card-foreground` e `bg-popover` sem
|
|
27
|
+
`text-popover-foreground`, evitando que a igualdade acidental com o foreground global
|
|
28
|
+
esconda temas quebrados.
|
|
29
|
+
|
|
30
|
+
**Migração:** troque `rounded-card` e `rounded-dialog` por `rounded-xl`, e
|
|
31
|
+
`rounded-popover` por `rounded-md`. Remova overrides de `--radius-card`,
|
|
32
|
+
`--radius-dialog` e `--radius-popover`; ajuste `--radius` ou a escala `--radius-*` quando
|
|
33
|
+
necessário. Sempre declare `bg-card text-card-foreground` e
|
|
34
|
+
`bg-popover text-popover-foreground` juntos. `opus check` aponta todos esses casos.
|
|
35
|
+
|
|
36
|
+
## 9.1.1 — 2026-08-20
|
|
37
|
+
|
|
38
|
+
**`Ask` leva elicitação estruturada para `@softize/opus/ui/react`.** O componente
|
|
39
|
+
controlado apresenta de uma a quatro `AskQuestion`, deriva `AskAnswer[]` sem tipos
|
|
40
|
+
paralelos e suporta seleção única por radiogroup, seleção múltipla por pills e texto livre
|
|
41
|
+
sempre disponível. Descrições usam Tooltip; teclado, validação, estados `disabled`/`busy`
|
|
42
|
+
e submit acessível ficam no componente. Transporte, SSE, persistência e integração com
|
|
43
|
+
`ChatEvent` continuam sob responsabilidade do consumidor.
|
|
44
|
+
|
|
14
45
|
## 9.1.0 — 2026-08-19
|
|
15
46
|
|
|
16
47
|
**Observabilidade ganha uma porta vendor-neutral no runtime.** `ObservabilityAdapter`
|
|
@@ -80,7 +111,6 @@ todas as mensagens que apenas têm comportamento `sticky` disponível.
|
|
|
80
111
|
fronteira memoizada separada do estado do composer, e cada bloco Markdown reaproveita o
|
|
81
112
|
resultado enquanto seu conteúdo não muda. Conversas longas mantêm o custo de digitação
|
|
82
113
|
constante; durante streaming, somente a mensagem alterada volta a passar pelo parser.
|
|
83
|
-
|
|
84
114
|
## 9.0.3 — 2026-08-12
|
|
85
115
|
|
|
86
116
|
**O pre-push ignora worktrees internos do Maestro.** Projetos sob `.maestro/` pertencem
|
|
@@ -123,8 +153,9 @@ depender de coincidências do call site:
|
|
|
123
153
|
removidos, deixando `ring-*` para foco e estados transitórios.
|
|
124
154
|
- a elevação volta ao vocabulário de intensidade do Tailwind (`shadow-sm`, `shadow-md`,
|
|
125
155
|
`shadow-lg` etc.), com a composição clara e difusa controlada pelo tema do Opus.
|
|
126
|
-
`Card`/`Composer`, menus/popovers e modais
|
|
127
|
-
defaults; os aliases por papel e as variáveis intermediárias
|
|
156
|
+
Na 9.0.0, `Card`/`Composer`, menus/popovers e modais passaram a usar respectivamente
|
|
157
|
+
`sm`, `md` e `lg` como defaults; os aliases por papel e as variáveis intermediárias
|
|
158
|
+
`--elevation-*` saíram. O default do `Card` foi posteriormente removido na 10.0.0.
|
|
128
159
|
- `shadow-lg` ganha spread negativo e menos opacidade: mantém alcance, mas deixa de
|
|
129
160
|
engrossar visualmente a borda no ponto de contato com a superfície.
|
|
130
161
|
- toda a escala passa a declarar a receita diretamente em `--shadow-*`, preservando a
|
package/bin/lib/check.mjs
CHANGED
|
@@ -53,11 +53,25 @@ const REMOVED_UI_TOKENS = new Map([
|
|
|
53
53
|
['shadow-card', 'use `shadow-sm`'],
|
|
54
54
|
['shadow-popover', 'use `shadow-md`'],
|
|
55
55
|
['shadow-dialog', 'use `shadow-lg`'],
|
|
56
|
+
['rounded-card', 'use `rounded-xl`'],
|
|
57
|
+
['rounded-popover', 'use `rounded-md`'],
|
|
58
|
+
['rounded-dialog', 'use `rounded-xl`'],
|
|
56
59
|
['--edge-value', 'remova o override; a aresta agora usa `--border`'],
|
|
57
60
|
['--color-edge', 'remova o token; a aresta agora usa `--border`'],
|
|
58
61
|
['--elevation-card', 'use/ajuste `--shadow-sm`'],
|
|
59
62
|
['--elevation-popover', 'use/ajuste `--shadow-md`'],
|
|
60
63
|
['--elevation-dialog', 'use/ajuste `--shadow-lg`'],
|
|
64
|
+
['--radius-card', 'use/ajuste `--radius-xl`'],
|
|
65
|
+
['--radius-popover', 'use/ajuste `--radius-md`'],
|
|
66
|
+
['--radius-dialog', 'use/ajuste `--radius-xl`'],
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
// Superfícies com foreground próprio formam um par indivisível. Exigir as duas
|
|
70
|
+
// utilities no mesmo fragmento de classes torna a regra local, revisável e evita que a
|
|
71
|
+
// igualdade acidental com `--foreground` esconda um tema quebrado.
|
|
72
|
+
const UI_SURFACE_PAIRS = new Map([
|
|
73
|
+
['bg-card', 'text-card-foreground'],
|
|
74
|
+
['bg-popover', 'text-popover-foreground'],
|
|
61
75
|
])
|
|
62
76
|
|
|
63
77
|
/** Os três nomes que denotam action/contrato — o arquivo sem nenhum deles é pulado. */
|
|
@@ -399,7 +413,7 @@ export function checkUiMigrations(file, text) {
|
|
|
399
413
|
action: token,
|
|
400
414
|
line,
|
|
401
415
|
file,
|
|
402
|
-
message: `token removido
|
|
416
|
+
message: `token removido — ${replacement}.`,
|
|
403
417
|
})
|
|
404
418
|
}
|
|
405
419
|
for (const [index, raw] of source.split(/\r?\n/).entries()) {
|
|
@@ -422,6 +436,28 @@ export function checkUiMigrations(file, text) {
|
|
|
422
436
|
const objectProperty = /\b(?!aria(?:Label|Description)\b|label\b|title\b|description\b|text\b|message\b|note\b|content\b)[A-Za-z_$][\w$-]*\s*:\s*$/.test(beforeValue)
|
|
423
437
|
const classListCall = /\bclassList\.(?:add|remove|toggle|replace|contains)\s*\([^)]*$/.test(beforeValue)
|
|
424
438
|
const stylePropertyCall = /\b(?:style\.)?setProperty\s*\([^)]*$/.test(beforeValue)
|
|
439
|
+
const utilityContext = classAttribute || classExpression || utilityHelper || classBinding || objectProperty || classListCall
|
|
440
|
+
const utilities = value.trim().split(/\s+/).filter(Boolean)
|
|
441
|
+
for (const [background, foreground] of UI_SURFACE_PAIRS) {
|
|
442
|
+
const pairKey = `surface:${background}`
|
|
443
|
+
const hasUtility = (expected) => utilities.some((utility) => {
|
|
444
|
+
const normalized = utility.replace(/^!/, '').replace(/!$/, '')
|
|
445
|
+
return normalized === expected || normalized.startsWith(`${expected}/`)
|
|
446
|
+
})
|
|
447
|
+
const hasBackground = hasUtility(background)
|
|
448
|
+
const hasForeground = hasUtility(foreground)
|
|
449
|
+
if (!seen.has(pairKey) && utilityContext && hasBackground && !hasForeground) {
|
|
450
|
+
seen.add(pairKey)
|
|
451
|
+
findings.push({
|
|
452
|
+
rule: 'unpaired-ui-surface',
|
|
453
|
+
level: 'error',
|
|
454
|
+
action: background,
|
|
455
|
+
line: startLine,
|
|
456
|
+
file,
|
|
457
|
+
message: `superfície sem o foreground correspondente — use \`${background} ${foreground}\` no mesmo fragmento de classes.`,
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
}
|
|
425
461
|
for (const [token, replacement] of REMOVED_UI_TOKENS) {
|
|
426
462
|
const usedAsUtility = matchesToken(value, token) && (
|
|
427
463
|
classAttribute || classExpression || utilityHelper || classBinding || objectProperty ||
|
package/docs/elevation-scale.md
CHANGED
|
@@ -32,8 +32,8 @@ uma superfície que precisava apenas de mais presença acabava recebendo um nome
|
|
|
32
32
|
- Qualquer componente pode subir ou descer um degrau sem adotar o nome de outro papel.
|
|
33
33
|
- A escala global afeta todo uso legítimo de `shadow-*`, inclusive consumidores externos;
|
|
34
34
|
isso é intencional e fica declarado no tema.
|
|
35
|
-
-
|
|
36
|
-
|
|
35
|
+
- Cor pode continuar semântica (`bg-popover`); forma usa diretamente a escala
|
|
36
|
+
(`rounded-md`, `rounded-xl`). Nenhuma delas representa intensidade intercambiável.
|
|
37
37
|
|
|
38
38
|
## Alternativas descartadas
|
|
39
39
|
|
package/package.json
CHANGED
|
@@ -20,12 +20,17 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
20
20
|
4. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
|
|
21
21
|
seu interior.
|
|
22
22
|
5. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
|
|
23
|
-
6.
|
|
23
|
+
6. Tratar tokens de superfície como pares indivisíveis no mesmo fragmento de classes:
|
|
24
|
+
`bg-card text-card-foreground` e `bg-popover text-popover-foreground`. Não depender da
|
|
25
|
+
igualdade atual com `--foreground`, porque o app pode sobrescrever cada par.
|
|
26
|
+
7. Usar diretamente a escala `rounded-*`; não criar radius por nome de componente quando
|
|
27
|
+
`rounded-sm` a `rounded-xl` já expressam a forma.
|
|
28
|
+
8. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
|
|
24
29
|
|
|
25
30
|
## Verificação
|
|
26
31
|
|
|
27
|
-
Rodar testes focados, typecheck e build da superfície. Inspecionar visualmente
|
|
28
|
-
e validar navegação por URL quando aplicável.
|
|
32
|
+
Rodar `opus check`, testes focados, typecheck e build da superfície. Inspecionar visualmente
|
|
33
|
+
a rota real e validar navegação por URL quando aplicável.
|
|
29
34
|
|
|
30
35
|
## Limites
|
|
31
36
|
|
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
- Dispara: “Monte a tela de edição usando a form action do Opus.”
|
|
4
4
|
- Não dispara: “Ajuste o CSS de um e-mail estático.”
|
|
5
5
|
- Execução: implementar uma lista com modal roteável e provar loading, erro, vazio e back.
|
|
6
|
+
- Reprova: criar uma casca `bg-card` que herda o texto global ou introduzir `rounded-widget`
|
|
7
|
+
quando a escala `rounded-*` atende ao papel.
|
|
@@ -5,4 +5,7 @@
|
|
|
5
5
|
operação, não a uma tela isolada.
|
|
6
6
|
- URL representa estado que precisa sobreviver a refresh, deep link ou histórico.
|
|
7
7
|
- Componentes compartilhados não impõem margem externa; páginas e shells compõem layout.
|
|
8
|
+
- Superfície semântica e foreground são um par local (`bg-card text-card-foreground`,
|
|
9
|
+
`bg-popover text-popover-foreground`); herança do foreground global não substitui o par.
|
|
10
|
+
- Forma usa a escala `rounded-*`; nome de componente não cria uma segunda escala de radius.
|
|
8
11
|
- Catálogo e API efetivos vêm dos exports da versão instalada, não de memória ou exemplo antigo.
|
|
@@ -52,7 +52,7 @@ function AlertDialogContent({
|
|
|
52
52
|
<AlertDialogPrimitive.Content
|
|
53
53
|
data-slot="alert-dialog-content"
|
|
54
54
|
className={cn(
|
|
55
|
-
"group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-xs translate-x-[-50%] translate-y-[-50%] gap-4 rounded-
|
|
55
|
+
"group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-xs translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border border-border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
56
56
|
className
|
|
57
57
|
)}
|
|
58
58
|
{...props}
|
|
@@ -0,0 +1,186 @@
|
|
|
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'
|
|
8
|
+
|
|
9
|
+
export interface AskProps {
|
|
10
|
+
/** Uma a quatro perguntas do contrato de elicitação. */
|
|
11
|
+
questions: AskQuestion[]
|
|
12
|
+
/** Respostas controladas, na mesma ordem de `questions`. Entradas ausentes são vazias. */
|
|
13
|
+
answers: AskAnswer[]
|
|
14
|
+
onChange: (answers: AskAnswer[]) => void
|
|
15
|
+
/** 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
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
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))
|
|
25
|
+
return {
|
|
26
|
+
header: question.header?.trim() || question.question,
|
|
27
|
+
selected: question.multiSelect ? selected : selected.slice(0, 1),
|
|
28
|
+
...(answer?.text === undefined ? {} : { text: answer.text }),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isAnswered(answer: AskAnswer): boolean {
|
|
33
|
+
return answer.selected.length > 0 || (answer.text?.trim() ?? '') !== ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Elicitação estruturada controlada. Não executa transporte nem mantém estado de domínio. */
|
|
37
|
+
export function Ask({
|
|
38
|
+
questions,
|
|
39
|
+
answers,
|
|
40
|
+
onChange,
|
|
41
|
+
onSubmit,
|
|
42
|
+
disabled = false,
|
|
43
|
+
busy = false,
|
|
44
|
+
className,
|
|
45
|
+
}: 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
|
|
54
|
+
|
|
55
|
+
const changeAt = (index: number, next: AskAnswer): void => {
|
|
56
|
+
const nextAnswers = [...normalized]
|
|
57
|
+
nextAnswers[index] = next
|
|
58
|
+
onChange(nextAnswers)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Card
|
|
63
|
+
asChild
|
|
64
|
+
className={cn('p-4', className)}
|
|
65
|
+
aria-busy={busy || undefined}
|
|
66
|
+
aria-disabled={disabled || undefined}
|
|
67
|
+
>
|
|
68
|
+
<form
|
|
69
|
+
data-slot="ask"
|
|
70
|
+
noValidate
|
|
71
|
+
onSubmit={(event) => {
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
setSubmittedSignature(questionSignature)
|
|
74
|
+
if (allAnswered && !locked) onSubmit(normalized)
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<TooltipProvider>
|
|
78
|
+
<div className="space-y-4">
|
|
79
|
+
{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)
|
|
84
|
+
|
|
85
|
+
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">
|
|
88
|
+
{question.question}
|
|
89
|
+
</legend>
|
|
90
|
+
<div
|
|
91
|
+
role={question.multiSelect ? 'group' : 'radiogroup'}
|
|
92
|
+
aria-labelledby={questionId}
|
|
93
|
+
aria-describedby={invalid ? errorId : undefined}
|
|
94
|
+
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
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{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
|
|
113
|
+
const optionButton = (
|
|
114
|
+
<Button
|
|
115
|
+
type="button"
|
|
116
|
+
size="sm"
|
|
117
|
+
shape="pill"
|
|
118
|
+
variant={selected ? 'secondary' : 'outline'}
|
|
119
|
+
role={question.multiSelect ? undefined : 'radio'}
|
|
120
|
+
aria-checked={question.multiSelect ? undefined : selected}
|
|
121
|
+
aria-pressed={question.multiSelect ? selected : undefined}
|
|
122
|
+
tabIndex={question.multiSelect
|
|
123
|
+
? undefined
|
|
124
|
+
: selected || (firstSelected === undefined && option.label === firstOption)
|
|
125
|
+
? 0
|
|
126
|
+
: -1}
|
|
127
|
+
disabled={locked}
|
|
128
|
+
onClick={() => {
|
|
129
|
+
const selectedOptions = question.multiSelect
|
|
130
|
+
? selected
|
|
131
|
+
? answer.selected.filter((label) => label !== option.label)
|
|
132
|
+
: [...answer.selected, option.label]
|
|
133
|
+
: [option.label]
|
|
134
|
+
changeAt(questionIndex, { ...answer, selected: selectedOptions })
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
{option.label}
|
|
138
|
+
</Button>
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return option.description ? (
|
|
142
|
+
<Tooltip key={option.label}>
|
|
143
|
+
<TooltipTrigger asChild>{optionButton}</TooltipTrigger>
|
|
144
|
+
<TooltipContent>{option.description}</TooltipContent>
|
|
145
|
+
</Tooltip>
|
|
146
|
+
) : (
|
|
147
|
+
<React.Fragment key={option.label}>{optionButton}</React.Fragment>
|
|
148
|
+
)
|
|
149
|
+
})}
|
|
150
|
+
</div>
|
|
151
|
+
<Input
|
|
152
|
+
value={answer.text ?? ''}
|
|
153
|
+
onChange={(event) => changeAt(questionIndex, { ...answer, text: event.target.value })}
|
|
154
|
+
placeholder="Outro (opcional)…"
|
|
155
|
+
aria-label={`Outra resposta para: ${question.question}`}
|
|
156
|
+
aria-invalid={invalid || undefined}
|
|
157
|
+
aria-describedby={invalid ? errorId : undefined}
|
|
158
|
+
disabled={locked}
|
|
159
|
+
/>
|
|
160
|
+
{invalid && (
|
|
161
|
+
<p id={errorId} className="text-sm text-destructive">
|
|
162
|
+
Escolha uma opção ou escreva uma resposta.
|
|
163
|
+
</p>
|
|
164
|
+
)}
|
|
165
|
+
</fieldset>
|
|
166
|
+
)
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
</TooltipProvider>
|
|
170
|
+
{!validCount && (
|
|
171
|
+
<p role="alert" className="mt-3 text-sm text-destructive">
|
|
172
|
+
Ask aceita de 1 a 4 perguntas.
|
|
173
|
+
</p>
|
|
174
|
+
)}
|
|
175
|
+
<div aria-live="polite" className="sr-only">
|
|
176
|
+
{submitted && !allAnswered ? 'Há perguntas sem resposta.' : ''}
|
|
177
|
+
</div>
|
|
178
|
+
<div className="mt-4 flex justify-end">
|
|
179
|
+
<Button type="submit" size="sm" busy={busy} disabled={disabled || !validCount}>
|
|
180
|
+
Responder
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
</form>
|
|
184
|
+
</Card>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
@@ -4,8 +4,8 @@ import { cn } from '../../lib/cn.ts'
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Card — a superfície da casa: SÓ a casca (borda + raio + bg). Divergência DECLARADA vs
|
|
7
|
-
* shadcn:
|
|
8
|
-
*
|
|
7
|
+
* shadcn: FLAT (sem `shadow-sm`) e SEM `flex/gap/py` forçados. O upstream embute
|
|
8
|
+
* `py-6 gap-6` + slots `px-6`, o que briga com card-box
|
|
9
9
|
* simples (o `gap-6` do Card + o `space-y` do consumidor = espaçamento dobrado) — por isso
|
|
10
10
|
* a casa hand-rollava. Aqui o padding mora no SLOT (Header/Content/Footer) ou no `className`
|
|
11
11
|
* do consumidor (box simples) — mesmo modelo do Dialog.
|
|
@@ -24,7 +24,7 @@ export function Card({
|
|
|
24
24
|
}): React.ReactElement {
|
|
25
25
|
const Comp = asChild ? Slot : 'div'
|
|
26
26
|
return (
|
|
27
|
-
<Comp data-slot="card" className={cn('rounded-
|
|
27
|
+
<Comp data-slot="card" className={cn('rounded-xl border border-border bg-card text-card-foreground', className)} {...props} />
|
|
28
28
|
)
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -163,7 +163,7 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
163
163
|
href={item.artifact.ref}
|
|
164
164
|
target="_blank"
|
|
165
165
|
rel="noreferrer"
|
|
166
|
-
className="rounded-xl border bg-card px-3.5 py-2 text-sm underline-offset-2 hover:underline"
|
|
166
|
+
className="rounded-xl border bg-card px-3.5 py-2 text-sm text-card-foreground underline-offset-2 hover:underline"
|
|
167
167
|
>
|
|
168
168
|
{item.artifact.title ?? item.artifact.ref}
|
|
169
169
|
</a>
|
|
@@ -34,7 +34,7 @@ export interface ComposerProps {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* <Composer /> — a caixa de escrever da casa: textarea (Enter envia / Shift+Enter quebra
|
|
37
|
-
* linha) numa pílula elevada (rounded-
|
|
37
|
+
* linha) numa pílula elevada (rounded-xl + border + shadow-sm), com o enviar dentro.
|
|
38
38
|
* É o composer do `<Chat>` extraído pra ser reusável SOZINHO — o Maestro cria sessão com
|
|
39
39
|
* ele (sem chat nenhum), e a GB prende seletores nele. Com `actions`, ganha uma barra
|
|
40
40
|
* embaixo pros seletores; sem, é a linha única de sempre.
|
|
@@ -111,7 +111,7 @@ export function Composer({
|
|
|
111
111
|
fire()
|
|
112
112
|
}}
|
|
113
113
|
className={cn(
|
|
114
|
-
'rounded-
|
|
114
|
+
'rounded-xl border border-border bg-card text-card-foreground shadow-sm transition',
|
|
115
115
|
twoRow ? 'p-2' : 'relative flex items-end gap-2 p-2 pl-3.5',
|
|
116
116
|
className,
|
|
117
117
|
)}
|
|
@@ -54,7 +54,7 @@ export function DialogContent({
|
|
|
54
54
|
// Superfície PURA: só a casca (borda/bg/raio/sombra) + coluna flex com teto de
|
|
55
55
|
// altura. O padding mora nos SLOTS (Header/Body/Footer), não aqui — assim ninguém
|
|
56
56
|
// precisa negar margem pra escapar de um p-6 embutido.
|
|
57
|
-
"fixed left-[50%] top-[50%] z-50 flex max-h-[85vh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-
|
|
57
|
+
"fixed left-[50%] top-[50%] z-50 flex max-h-[85vh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
|
58
58
|
className,
|
|
59
59
|
)}
|
|
60
60
|
{...props}
|
|
@@ -45,7 +45,7 @@ function MenuContent({
|
|
|
45
45
|
data-slot="menu-content"
|
|
46
46
|
sideOffset={sideOffset}
|
|
47
47
|
className={cn(
|
|
48
|
-
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-
|
|
48
|
+
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
49
49
|
className
|
|
50
50
|
)}
|
|
51
51
|
{...props}
|
|
@@ -233,7 +233,7 @@ function MenuSubContent({
|
|
|
233
233
|
<MenuPrimitive.SubContent
|
|
234
234
|
data-slot="menu-sub-content"
|
|
235
235
|
className={cn(
|
|
236
|
-
"z-50 min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-
|
|
236
|
+
"z-50 min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
237
237
|
className
|
|
238
238
|
)}
|
|
239
239
|
{...props}
|
|
@@ -34,7 +34,7 @@ function PopoverContent({
|
|
|
34
34
|
align={align}
|
|
35
35
|
sideOffset={sideOffset}
|
|
36
36
|
className={cn(
|
|
37
|
-
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-
|
|
37
|
+
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
38
38
|
className
|
|
39
39
|
)}
|
|
40
40
|
{...props}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Elicitação estruturada controlada baseada diretamente em `AskQuestion` e `AskAnswer`. O componente só apresenta e valida respostas; transporte, persistência e continuação do agente pertencem ao consumidor.
|
|
2
|
+
|
|
3
|
+
```tsx preview
|
|
4
|
+
const [answers, setAnswers] = React.useState([])
|
|
5
|
+
|
|
6
|
+
render(
|
|
7
|
+
<Ask
|
|
8
|
+
questions={[
|
|
9
|
+
{
|
|
10
|
+
question: 'Qual ambiente devemos usar?',
|
|
11
|
+
header: 'Ambiente',
|
|
12
|
+
options: [
|
|
13
|
+
{ label: 'Produção', description: 'Usa dados e serviços reais.' },
|
|
14
|
+
{ label: 'Homologação', description: 'Usa o ambiente de validação.' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
question: 'Quais verificações devo executar?',
|
|
19
|
+
header: 'Verificações',
|
|
20
|
+
multiSelect: true,
|
|
21
|
+
options: [{ label: 'Testes' }, { label: 'Typecheck' }, { label: 'Build' }],
|
|
22
|
+
},
|
|
23
|
+
]}
|
|
24
|
+
answers={answers}
|
|
25
|
+
onChange={setAnswers}
|
|
26
|
+
onSubmit={(next) => console.log(next)}
|
|
27
|
+
/>,
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Cada pergunta exige pelo menos uma opção válida selecionada **ou** texto livre não vazio. `busy` mostra o progresso no botão e trava os controles; `disabled` apenas trava a interação. A ordem de `answers` acompanha a ordem de `questions`, e o `header` de cada resposta é derivado de `question.header` (ou do próprio enunciado quando o header estiver vazio).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Box simples
|
|
2
2
|
|
|
3
|
-
O uso mais comum: a superfície de conteúdo em repouso (`rounded-
|
|
3
|
+
O uso mais comum: a superfície de conteúdo em repouso (`rounded-xl` + `border`), flat por padrão, com o padding pelo `className`. O Card não força flex/gap/padding — você compõe o miolo (sem brigar com `space-y`/`flex` seu). Quando a hierarquia realmente pedir elevação, declare a intensidade explicitamente, por exemplo `className="shadow-sm"`.
|
|
4
4
|
|
|
5
5
|
```tsx preview col
|
|
6
6
|
<Card className="p-4">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Básico
|
|
2
2
|
|
|
3
|
-
A caixa de escrever da casa: textarea numa pílula elevada (`rounded-
|
|
3
|
+
A caixa de escrever da casa: textarea numa pílula elevada (`rounded-xl` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/components/chat) extraído — use SOZINHO quando há entrada de texto mas não um chat (ex.: criar uma sessão). Controlado: o dono do texto é você. Os callbacks opcionais `onHistoryPrevious` e `onHistoryNext` permitem que esse dono consuma **↑/↓**; sem eles, as setas mantêm o comportamento nativo da textarea.
|
|
4
4
|
|
|
5
5
|
```tsx preview col
|
|
6
6
|
const [text, setText] = React.useState('')
|
|
@@ -87,7 +87,7 @@ render(
|
|
|
87
87
|
{LEVELS.map(([level, desc]) => (
|
|
88
88
|
<div key={level} className="flex flex-col items-center gap-3">
|
|
89
89
|
<div
|
|
90
|
-
className={'flex h-24 w-40 items-center justify-center rounded-
|
|
90
|
+
className={'flex h-24 w-40 items-center justify-center rounded-xl border border-border bg-card text-xs text-card-foreground shadow-' + level}
|
|
91
91
|
>
|
|
92
92
|
Texto
|
|
93
93
|
</div>
|
|
@@ -110,9 +110,9 @@ render(
|
|
|
110
110
|
## Radius
|
|
111
111
|
|
|
112
112
|
> Escala mapeada no `@theme inline` preservando o sentido pré-v4 dos utilitários `rounded-*`
|
|
113
|
-
> (neutraliza o rename do Tailwind v4).
|
|
114
|
-
>
|
|
115
|
-
>
|
|
113
|
+
> (neutraliza o rename do Tailwind v4). Componentes e superfícies usam diretamente essa
|
|
114
|
+
> escala: a forma não cria uma segunda taxonomia por componente. A identidade por app
|
|
115
|
+
> sobrescreve `--radius` e escala tudo junto.
|
|
116
116
|
|
|
117
117
|
```tsx preview
|
|
118
118
|
render(
|
|
@@ -122,9 +122,6 @@ render(
|
|
|
122
122
|
['rounded-md', 'radius − 2px'],
|
|
123
123
|
['rounded-lg', 'radius (0.625rem)'],
|
|
124
124
|
['rounded-xl', 'radius + 4px'],
|
|
125
|
-
['rounded-popover', 'papel popover (radius − 2px)'],
|
|
126
|
-
['rounded-dialog', 'papel dialog (radius + 6px)'],
|
|
127
|
-
['rounded-card', 'papel card (radius + 6px)'],
|
|
128
125
|
].map(([cls, calc]) => (
|
|
129
126
|
<div key={cls} className="flex items-center gap-3">
|
|
130
127
|
<div className={'h-12 w-12 border-2 border-foreground/30 bg-muted ' + cls} />
|
package/src/ui/docs/registry.tsx
CHANGED
|
@@ -56,6 +56,7 @@ import buttonGroupMd from './content/button-group.md?raw'
|
|
|
56
56
|
import calendarMd from './content/calendar.md?raw'
|
|
57
57
|
import cardMd from './content/card.md?raw'
|
|
58
58
|
import carouselMd from './content/carousel.md?raw'
|
|
59
|
+
import askMd from './content/ask.md?raw'
|
|
59
60
|
import chatMd from './content/chat.md?raw'
|
|
60
61
|
import checkboxMd from './content/checkbox.md?raw'
|
|
61
62
|
import collapsibleMd from './content/collapsible.md?raw'
|
|
@@ -316,6 +317,7 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
316
317
|
{
|
|
317
318
|
label: 'IA',
|
|
318
319
|
pages: [
|
|
320
|
+
{ slug: 'ask', title: 'Ask', render: comp('Ask', 'ask', askMd) },
|
|
319
321
|
{ slug: 'chat', title: 'Chat', render: comp('Chat', 'chat', chatMd) },
|
|
320
322
|
{ slug: 'composer', title: 'Composer', render: comp('Composer', 'composer', composerMd) },
|
|
321
323
|
],
|
package/src/ui/meta.ts
CHANGED
|
@@ -11,6 +11,12 @@ import type { ComponentMeta } from './index.ts'
|
|
|
11
11
|
|
|
12
12
|
/** Todos os metas, chaveados pelo nome canônico (kebab). Fonte única — zero duplicação. */
|
|
13
13
|
export const componentMeta = {
|
|
14
|
+
'ask': {
|
|
15
|
+
name: 'ask',
|
|
16
|
+
ancestry: 'opus',
|
|
17
|
+
whenToUse:
|
|
18
|
+
'Elicitação estruturada controlada para 1–4 perguntas `AskQuestion`: opções single/multi em pills, texto livre opcional, validação e submit de `AskAnswer[]`. Não faz transporte, persistência nem integração automática com ChatEvent; o consumidor controla `answers`/`onChange` e conecta `onSubmit` ao canal apropriado.',
|
|
19
|
+
},
|
|
14
20
|
'alert': {
|
|
15
21
|
name: 'alert',
|
|
16
22
|
ancestry: 'opus',
|
|
@@ -33,7 +39,7 @@ export const componentMeta = {
|
|
|
33
39
|
name: 'card',
|
|
34
40
|
ancestry: 'shadcn',
|
|
35
41
|
whenToUse:
|
|
36
|
-
'Superfície da casa (bg-card + borda + rounded-
|
|
42
|
+
'Superfície da casa (bg-card + text-card-foreground + borda + rounded-xl, flat). Box simples: `<Card className="p-4">…</Card>`. Estruturado: Card > CardHeader(CardTitle/CardDescription) + CardContent + CardFooter — o padding mora nos slots (como o Dialog). Divergência declarada vs shadcn: sem shadow e sem flex/gap forçados (o upstream brigava com card-box simples).',
|
|
37
43
|
},
|
|
38
44
|
'chat': {
|
|
39
45
|
name: 'chat',
|
package/src/ui/react.tsx
CHANGED
|
@@ -19,6 +19,8 @@ export { Input } from './components/primitives/input.tsx'
|
|
|
19
19
|
|
|
20
20
|
export { Chat } from './components/primitives/chat.tsx'
|
|
21
21
|
export type { ChatProps, ChatMessage, ChatArtifact, ChatTranscriptItem } from './components/primitives/chat.tsx'
|
|
22
|
+
export { Ask } from './components/primitives/ask.tsx'
|
|
23
|
+
export type { AskProps } from './components/primitives/ask.tsx'
|
|
22
24
|
export { Composer } from './components/primitives/composer.tsx'
|
|
23
25
|
export type { ComposerProps } from './components/primitives/composer.tsx'
|
|
24
26
|
export { Copyable } from './components/primitives/copyable.tsx'
|
package/src/ui/theme.css
CHANGED
|
@@ -137,13 +137,6 @@
|
|
|
137
137
|
--radius-lg: var(--radius);
|
|
138
138
|
--radius-xl: calc(var(--radius) + 4px);
|
|
139
139
|
|
|
140
|
-
/* Forma por papel (rounded-card/popover/dialog), separada da intensidade de elevação.
|
|
141
|
-
Derivados de --radius: a identidade por app (sobrescrever --radius) escala os papéis
|
|
142
|
-
junto. Controles ficam na escala sm–xl. */
|
|
143
|
-
--radius-card: calc(var(--radius) + 6px);
|
|
144
|
-
--radius-popover: calc(var(--radius) - 2px);
|
|
145
|
-
--radius-dialog: calc(var(--radius) + 6px);
|
|
146
|
-
|
|
147
140
|
/* Escala Tailwind, com receita direta para o compilador preservar modifiers `/opacity`.
|
|
148
141
|
`light-dark()` mantém o peso adequado em cada tema sem uma variável intermediária. */
|
|
149
142
|
--shadow-2xs: 0 1px 2px -1px light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.12));
|