@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 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 usam respectivamente `sm`, `md` e `lg` como
127
- defaults; os aliases por papel e as variáveis intermediárias `--elevation-*` saem.
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 na 9.0.0 — ${replacement}.`,
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 ||
@@ -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
- - Forma e cor podem continuar semânticas (`rounded-card`, `bg-popover`), pois não
36
- representam uma intensidade intercambiável.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "9.1.0",
3
+ "version": "10.0.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
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 a rota real
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-dialog 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",
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
+ }
@@ -76,7 +76,7 @@ function Calendar({
76
76
  defaultClassNames.dropdown_root
77
77
  ),
78
78
  dropdown: cn(
79
- "absolute inset-0 bg-popover opacity-0",
79
+ "absolute inset-0 bg-popover text-popover-foreground opacity-0",
80
80
  defaultClassNames.dropdown
81
81
  ),
82
82
  caption_label: cn(
@@ -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: `rounded-lg` (não `rounded-xl`), FLAT (sem `shadow-sm`) e SEM `flex/gap/py`
8
- * forçados. O upstream embute `py-6 gap-6` + slots `px-6`, o que briga com card-box
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-card border border-border bg-card text-card-foreground shadow-sm', className)} {...props} />
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-card + border + shadow-sm), com o enviar dentro.
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-card border border-border bg-card shadow-sm transition focus-within:ring-2 focus-within:ring-primary/15',
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-dialog border border-border bg-card 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",
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-popover 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",
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-popover 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",
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-popover 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",
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-card` + `border` + `shadow-sm` ver Tokens & Tema → Escala de elevaçã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).
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-card` + `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.
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-card border border-border bg-card text-xs shadow-' + level}
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). E a forma também tem papel: `rounded-card/popover/dialog`
114
- > são a forma das superfícies elevadas (separada da intensidade de `shadow-*`), derivados de `--radius` — a
115
- > identidade por app escala tudo junto. Controles (botões, inputs) seguem na escala sm–xl.
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} />
@@ -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-lg, 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: rounded-lg, sem shadow, sem flex/gap forçados (o upstream brigava com card-box simples).',
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));