@softize/opus 12.10.0 → 12.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bin/lib/check.mjs +1103 -310
  3. package/bin/lib/copy.mjs +11 -0
  4. package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +65 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
  7. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
  8. package/package.json +1 -1
  9. package/registry/instructions/opus.md +5 -0
  10. package/registry/skills/build-opus-ui/SKILL.md +27 -16
  11. package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
  12. package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
  13. package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
  14. package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
  15. package/registry/templates/app/src/App.tsx +1 -1
  16. package/src/core/dictionary.ts +52 -14
  17. package/src/core/index.ts +10 -0
  18. package/src/core/ui-context.ts +29 -0
  19. package/src/schema/drivers/zod.ts +17 -8
  20. package/src/ui/components/patterns/action-form-card.tsx +18 -12
  21. package/src/ui/components/patterns/confirm.tsx +26 -3
  22. package/src/ui/components/patterns/content-header.tsx +335 -61
  23. package/src/ui/components/patterns/data-state.tsx +23 -10
  24. package/src/ui/components/patterns/list.tsx +1096 -777
  25. package/src/ui/components/patterns/page-state.tsx +115 -0
  26. package/src/ui/components/patterns/page.tsx +231 -41
  27. package/src/ui/components/patterns/sidebar.tsx +354 -80
  28. package/src/ui/components/patterns/trigger.tsx +13 -9
  29. package/src/ui/components/patterns/view.tsx +7 -11
  30. package/src/ui/components/primitives/alert-dialog.tsx +7 -5
  31. package/src/ui/components/primitives/alert.tsx +298 -110
  32. package/src/ui/components/primitives/ask.tsx +2 -1
  33. package/src/ui/components/primitives/badge.tsx +91 -30
  34. package/src/ui/components/primitives/button.tsx +99 -60
  35. package/src/ui/components/primitives/calendar.tsx +39 -39
  36. package/src/ui/components/primitives/card.tsx +96 -23
  37. package/src/ui/components/primitives/detail.tsx +2 -2
  38. package/src/ui/components/primitives/dictionary-value.tsx +9 -14
  39. package/src/ui/components/primitives/dot.tsx +74 -21
  40. package/src/ui/components/primitives/drawer.tsx +33 -20
  41. package/src/ui/components/primitives/item.tsx +137 -81
  42. package/src/ui/components/primitives/menu.tsx +11 -3
  43. package/src/ui/components/primitives/metric-card.tsx +133 -0
  44. package/src/ui/components/primitives/table.tsx +2 -2
  45. package/src/ui/docs/DocBrowser.tsx +3 -3
  46. package/src/ui/docs/changelog.tsx +1 -1
  47. package/src/ui/docs/content/action-form-card.md +1 -1
  48. package/src/ui/docs/content/alert-dialog.md +8 -8
  49. package/src/ui/docs/content/alert.md +49 -25
  50. package/src/ui/docs/content/badge.md +18 -19
  51. package/src/ui/docs/content/button.md +12 -9
  52. package/src/ui/docs/content/card.md +5 -5
  53. package/src/ui/docs/content/content.md +44 -0
  54. package/src/ui/docs/content/customization.md +2 -2
  55. package/src/ui/docs/content/detail.md +5 -2
  56. package/src/ui/docs/content/dialog.md +2 -2
  57. package/src/ui/docs/content/dictionary-value.md +11 -10
  58. package/src/ui/docs/content/dot.md +7 -7
  59. package/src/ui/docs/content/drawer.md +6 -3
  60. package/src/ui/docs/content/input-group.md +3 -2
  61. package/src/ui/docs/content/item.md +47 -21
  62. package/src/ui/docs/content/menu.md +5 -4
  63. package/src/ui/docs/content/metric-card.md +41 -0
  64. package/src/ui/docs/content/page-state.md +45 -0
  65. package/src/ui/docs/content/page.md +48 -10
  66. package/src/ui/docs/content/semantic-context.md +63 -0
  67. package/src/ui/docs/content/sidebar.md +4 -4
  68. package/src/ui/docs/content/skeleton.md +2 -2
  69. package/src/ui/docs/content/table.md +3 -3
  70. package/src/ui/docs/content/tokens.md +28 -0
  71. package/src/ui/docs/doc-client.tsx +2 -2
  72. package/src/ui/docs/registry.tsx +596 -228
  73. package/src/ui/lib/semantic-context.ts +30 -0
  74. package/src/ui/meta.ts +292 -270
  75. package/src/ui/react.tsx +378 -111
  76. package/src/ui/theme.css +66 -0
@@ -4,12 +4,24 @@
4
4
  - Campos, labels, mensagens e invalidações pertencem ao contrato quando são parte da
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
- - `Page` fornece o `<main>`, o container centralizado com teto padrão de `72rem` e o header
8
- de página. Alterar `className` apenas quando a superfície tiver uma necessidade real de
9
- largura; não reconstruir esse container em cada rota.
10
- - `ContentHeader` compartilha a composição de título, descrição, metadados e ações entre
11
- páginas e seções. `variant` define o destaque visual; `level` preserva separadamente a
12
- hierarquia semântica do heading.
7
+ - `Page` fornece o `<main>` e o container centralizado com teto padrão de `80rem`. Sua forma
8
+ explícita é `Page > PageHeader (PageTitle, PageDescription, PageMeta, PageActions) + PageBody`;
9
+ `title`, `description`, `meta` e `actions` no próprio `Page` são a abreviação para o caso direto.
10
+ Não misturar as duas formas. Alterar `className` apenas quando a superfície tiver uma necessidade
11
+ real de largura; não reconstruir esse container em cada rota.
12
+ - `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
13
+ forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
14
+ explícita, fica dentro de `PageBody`. O cabeçalho permanece visível. Estados de seção ou coleção
15
+ continuam em `DataState`, `ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a
16
+ estado da página.
17
+ - `Content` delimita uma seção e segue a mesma anatomia: `Content > ContentHeader (ContentTitle,
18
+ ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader` nunca fica
19
+ solto. `title`, `description`, `meta` e `actions` no `Content` são a abreviação para o caso
20
+ direto e não podem ser misturados ao header explícito. `level` preserva a hierarquia semântica
21
+ do heading.
22
+ - Card, Drawer e Pane nomeiam a região principal como `CardBody`, `DrawerBody` e `PaneBody`.
23
+ `*Content` permanece reservado a raízes técnicas ou painéis cujo papel não é o corpo de uma
24
+ estrutura, como `DialogContent`, `PopoverContent` e `TabsContent`.
13
25
  - Componentes compartilhados não impõem margem externa; páginas e shells compõem layout.
14
26
  - A fonte raiz pertence ao navegador e à aplicação. Medidas escaláveis usam `rem` ou a escala
15
27
  relativa do Tailwind; `px` fica restrito a hairlines e compensações ligadas a essas bordas.
@@ -55,18 +67,23 @@
55
67
  valor trunca no trigger e a opção inteira fica na lista. Não dimensionar filtro pelo conteúdo
56
68
  nem pela label; o modal usa `w-full`.
57
69
  - Catálogo e API efetivos vêm dos exports da versão instalada, não de memória ou exemplo antigo.
70
+ - Em componentes semânticos, `context` responde por que há destaque e `variant` responde como ele
71
+ aparece. O vocabulário canônico é `neutral | primary | info | success | warning | danger`; as
72
+ variantes visuais são `solid | subtle | outline | ghost | link`, limitadas por componente.
73
+ `light`/`dark` são temas, `secondary` não substitui estado neutro e `destructive` é comportamento
74
+ de action projetado visualmente como `danger`.
58
75
 
59
76
  ## Dicionários e dimensões
60
77
 
61
78
  O dicionário declara o papel; a tela só escolhe onde o valor fica. Tabela de decisão aplicada
62
79
  por `DictionaryValue` e pelas colunas de `ActionList`:
63
80
 
64
- | Papel declarado | Exemplos | Forma | Variante | Ícone | Tooltip |
65
- |---|---|---|---|---|---|
66
- | `classification` | Tipo de cliente, categoria, natureza | Badge | `outline`; ignora `tone` | se a entrada declara `icon` do catálogo | se `description` acrescenta ao rótulo |
67
- | `status` | Aberto, resolvido, degradado | Badge | tonal pelo `tone`; `neutral` sem tom; nunca `outline` | idem | idem |
68
- | `stage` | Prospect, cliente; etapa do funil | Badge | igual a `status` | idem | idem |
69
- | `plain` ou ausente | Fonte, formato, período | Texto | — | idem | idem |
81
+ | Papel declarado | Exemplos | Forma | Contexto | Variante | Ícone | Tooltip |
82
+ |---|---|---|---|---|---|---|
83
+ | `classification` | Tipo de cliente, categoria, natureza | Badge | `neutral` | `outline`; ignora `context` da entrada | se a entrada declara `icon` do catálogo | se `description` acrescenta ao rótulo |
84
+ | `status` | Aberto, resolvido, degradado | Badge | pela entrada; `neutral` por padrão | `subtle`; nunca `outline` | idem | idem |
85
+ | `stage` | Prospect, cliente; etapa do funil | Badge | igual a `status` | `subtle` | idem | idem |
86
+ | `plain` ou ausente | Fonte, formato, período | Texto | — | — | idem | idem |
70
87
 
71
88
  Regras que não dependem da tabela:
72
89
 
@@ -88,7 +105,7 @@ export const customerKindDict = t.dict(
88
105
  { doc: 'Natureza da parte no cadastro global.', presentation: 'classification' },
89
106
  )
90
107
  export const customerStageDict = t.dict(
91
- { prospect: { label: 'Prospect' }, customer: { label: 'Cliente', tone: 'success' } },
108
+ { prospect: { label: 'Prospect' }, customer: { label: 'Cliente', context: 'success' } },
92
109
  { doc: 'Estágio comercial atual da parte.', presentation: 'stage' },
93
110
  )
94
111
  // contrato:
@@ -112,7 +129,7 @@ cells={{
112
129
  ),
113
130
  stage: (item) =>
114
131
  item.stage === 'customer'
115
- ? <Badge variant="secondary">Cliente</Badge>
132
+ ? <Badge context="neutral" variant="solid">Cliente</Badge>
116
133
  : <Badge variant="outline">Prospect</Badge>,
117
134
  }}
118
135
 
@@ -124,7 +141,13 @@ cells={{
124
141
 
125
142
  // Tooltip que repete o rótulo; cor como único sinal.
126
143
  <Tooltip>
127
- <TooltipTrigger><Dot variant="success" /></TooltipTrigger>
144
+ <TooltipTrigger><Dot context="success" /></TooltipTrigger>
128
145
  <TooltipContent>Cliente</TooltipContent>
129
146
  </Tooltip>
147
+
148
+ // Contexto semântico usado como variante: a IA perdeu um eixo da API.
149
+ <Badge variant="success">Concluído</Badge>
150
+
151
+ // Risco comportamental usado como nome visual.
152
+ <Button variant="destructive">Excluir</Button>
130
153
  ```
@@ -35,7 +35,8 @@ da mesma instância, sem catálogo repetido em enum, opção estática ou format
35
35
  - **status** — situação operacional que muda com o tempo;
36
36
  - **stage** — etapa de um ciclo ou funil;
37
37
  - **plain** — valor que só precisa ser legível (o mesmo efeito de omitir).
38
- Por entrada, declarar `tone` somente em status e estágio, `icon` somente com nome existente no
38
+ Por entrada, declarar `context` somente em status e estágio, usando `neutral`, `info`, `success`,
39
+ `warning` ou `danger`; `icon` somente com nome existente no
39
40
  catálogo `iconPickerIcons`, e `description` somente quando acrescentar algo ao rótulo. `doc`
40
41
  continua sendo o entendimento de negócio para manifest e Lens; não é tooltip.
41
42
  4. Derivar o schema por `.zod()` e usá-lo em todo contrato que valide o código; não redeclarar a
@@ -68,7 +69,8 @@ contrato ou componente mapeado, regenerar com `opus copy` e executar os checks d
68
69
  - Não tratar metadata de dicionário como autorização ou invariante de integridade.
69
70
  - Não omitir `presentation` esperando que a tela deduza o papel pelo nome do dicionário; sem
70
71
  papel declarado, o valor é texto.
71
- - Não declarar `tone` em classificação, nem `icon` fora do catálogo, nem `description` que
72
+ - Não declarar `context` em classificação, nem usar o alias legado `tone` em código novo, nem
73
+ `icon` fora do catálogo, nem `description` que
72
74
  repita o rótulo.
73
75
 
74
76
  ## Recursos
@@ -11,8 +11,9 @@
11
11
  interface lê rótulos por `labelFor`/`metaFor`.
12
12
  - Execução de apresentação: dado um cadastro com “Pessoa física/Empresa” e “Prospect/Cliente”,
13
13
  declarar o primeiro como `classification` com ícones `user` e `building` do catálogo e o segundo
14
- como `stage` com `tone` explícito, sem `description` que repita o rótulo; provar que o manifest
15
- projeta `presentation` e que `t.dict` rejeita `presentation` ou `tone` fora do vocabulário.
16
- - Reprova: declarar `tone` em uma classificação, inventar `icon` fora do catálogo, copiar `doc`
14
+ como `stage` com `context` explícito, sem `description` que repita o rótulo; provar que o manifest
15
+ projeta `presentation` e `context` e que `t.dict` rejeita valores fora do vocabulário.
16
+ - Reprova: declarar `context` em uma classificação, usar o alias legado `tone` em código novo,
17
+ inventar `icon` fora do catálogo, copiar `doc`
17
18
  para `description` ou deixar `presentation` ausente em um status esperando que a tela infira o
18
19
  badge.
@@ -26,7 +26,7 @@ export function App(): React.ReactElement {
26
26
  {items.map((t) => (
27
27
  <li key={t.id} className="flex items-center justify-between gap-2 text-sm">
28
28
  <span>{t.title}</span>
29
- <Badge variant={t.done ? 'default' : 'secondary'}>{t.done ? 'Feita' : 'Aberta'}</Badge>
29
+ <Badge context={t.done ? 'success' : 'neutral'}>{t.done ? 'Feita' : 'Aberta'}</Badge>
30
30
  </li>
31
31
  ))}
32
32
  </ul>
@@ -8,14 +8,20 @@
8
8
 
9
9
  import { getLogicalType } from './logical-type.ts'
10
10
  import type { LogicalTypeMeta } from './types.ts'
11
+ import {
12
+ DICT_CONTEXTS,
13
+ isDictContext,
14
+ type DictContext,
15
+ } from './ui-context.ts'
11
16
 
12
17
  /** Papel de apresentação declarado no nível do dicionário. */
13
18
  export const DICT_PRESENTATIONS = ['classification', 'status', 'stage', 'plain'] as const
14
19
  export type DictPresentation = (typeof DICT_PRESENTATIONS)[number]
15
20
 
16
- /** Tom explícito de uma entrada — honrado apenas por status e estágios. */
17
- export const DICT_TONES = ['neutral', 'info', 'success', 'warning', 'danger'] as const
18
- export type DictTone = (typeof DICT_TONES)[number]
21
+ /** @deprecated Use `DICT_CONTEXTS` (ADR 0006). */
22
+ export const DICT_TONES = DICT_CONTEXTS
23
+ /** @deprecated Use `DictContext` (ADR 0006). */
24
+ export type DictTone = DictContext
19
25
 
20
26
  /**
21
27
  * Meta de cada chave de um dict. `label` obrigatório; resto livre.
@@ -24,7 +30,7 @@ export type DictTone = (typeof DICT_TONES)[number]
24
30
  * entra/sai) — a fonte rica de consulta rápida; flui pro manifest/Lens e NÃO vira tooltip.
25
31
  * - `description` é o texto curto voltado à pessoa: aparece no tooltip do `DictionaryValue`
26
32
  * e como apoio de opção. Não repetir o rótulo — descrição igual ao rótulo não renderiza.
27
- * - `tone` é o tom tonal do badge de status/estágio; classificação ignora.
33
+ * - `context` é o contexto semântico do status/estágio; classificação ignora.
28
34
  * - `icon` é o identificador estável do catálogo de ícones do Opus (`iconPickerIcons`);
29
35
  * nome fora do catálogo não renderiza ícone.
30
36
  * - `color` permanece metadata livre (lida pela Lens); o renderer não a interpreta.
@@ -33,7 +39,9 @@ export interface DictEntryMeta {
33
39
  label: string
34
40
  doc?: string
35
41
  description?: string
36
- tone?: DictTone
42
+ context?: DictContext
43
+ /** @deprecated Use `context`. Compatibilidade temporária da ADR 0006. */
44
+ tone?: DictContext
37
45
  icon?: string
38
46
  color?: string
39
47
  order?: number
@@ -44,8 +52,16 @@ export function isDictPresentation(value: unknown): value is DictPresentation {
44
52
  return typeof value === 'string' && (DICT_PRESENTATIONS as readonly string[]).includes(value)
45
53
  }
46
54
 
47
- export function isDictTone(value: unknown): value is DictTone {
48
- return typeof value === 'string' && (DICT_TONES as readonly string[]).includes(value)
55
+ /** @deprecated Use `isDictContext` (ADR 0006). */
56
+ export const isDictTone = isDictContext
57
+
58
+ /** Normaliza a metadata canônica e seu alias temporário. */
59
+ export function dictionaryEntryContext(entry: DictEntryMeta): DictContext | undefined {
60
+ return isDictContext(entry.context)
61
+ ? entry.context
62
+ : isDictContext(entry.tone)
63
+ ? entry.tone
64
+ : undefined
49
65
  }
50
66
 
51
67
  /** Forma normalizada de um dicionário, independente de onde a meta foi lida. */
@@ -92,7 +108,11 @@ export interface DictionaryValuePresentation {
92
108
  /** False quando o valor não está no dicionário. */
93
109
  known: boolean
94
110
  presentation: DictPresentation
95
- /** `null` = texto; `'outline'` = classificação; tom = badge tonal de status/estágio. */
111
+ /** Contexto semântico; `null` quando a apresentação é texto. */
112
+ context: DictContext | null
113
+ /** Tratamento visual; `null` quando a apresentação é texto. */
114
+ variant: 'subtle' | 'outline' | null
115
+ /** @deprecated Use `context` e `variant`. */
96
116
  badge: 'outline' | DictTone | null
97
117
  /** Identificador do catálogo, só quando declarado na entrada. */
98
118
  icon: string | null
@@ -115,8 +135,8 @@ export function dictionaryDescription(label: string, description: unknown): stri
115
135
  * Aplica a tabela de decisão da ADR 0003:
116
136
  *
117
137
  * | papel | forma | variante |
118
- * | classification | badge | outline (ignora `tone`) |
119
- * | status / stage | badge | tonal: `tone` ?? neutral |
138
+ * | classification | badge | neutral + outline |
139
+ * | status / stage | badge | `context` + subtle |
120
140
  * | plain / ausente | texto | — |
121
141
  *
122
142
  * Ícone só quando declarado; tooltip só quando `description` acrescenta. Valor fora do
@@ -132,20 +152,38 @@ export function presentDictionaryValue(
132
152
  ? descriptor.entries[value]
133
153
  : undefined
134
154
  if (entry === undefined || typeof entry.label !== 'string') {
135
- return { value, label: value, known: false, presentation, badge: null, icon: null, description: null }
155
+ return {
156
+ value,
157
+ label: value,
158
+ known: false,
159
+ presentation,
160
+ context: null,
161
+ variant: null,
162
+ badge: null,
163
+ icon: null,
164
+ description: null,
165
+ }
136
166
  }
137
- const badge: DictionaryValuePresentation['badge'] =
167
+ const context: DictionaryValuePresentation['context'] =
168
+ presentation === 'classification'
169
+ ? 'neutral'
170
+ : presentation === 'status' || presentation === 'stage'
171
+ ? (dictionaryEntryContext(entry) ?? 'neutral')
172
+ : null
173
+ const variant: DictionaryValuePresentation['variant'] =
138
174
  presentation === 'classification'
139
175
  ? 'outline'
140
176
  : presentation === 'status' || presentation === 'stage'
141
- ? (isDictTone(entry.tone) ? entry.tone : 'neutral')
177
+ ? 'subtle'
142
178
  : null
143
179
  return {
144
180
  value,
145
181
  label: entry.label,
146
182
  known: true,
147
183
  presentation,
148
- badge,
184
+ context,
185
+ variant,
186
+ badge: variant === 'outline' ? 'outline' : context,
149
187
  icon: typeof entry.icon === 'string' && entry.icon.length > 0 ? entry.icon : null,
150
188
  description: dictionaryDescription(entry.label, entry.description),
151
189
  }
package/src/core/index.ts CHANGED
@@ -138,12 +138,22 @@ export { normalizeTraceContext } from './trace.ts'
138
138
  // `@softize/opus/schema` re-exporta os dois pra manter a API de sempre.
139
139
  export { attachLogicalType, getLogicalType } from './logical-type.ts'
140
140
 
141
+ // — Contextos semânticos de UI (ADR 0006) ————————————————————————————————————
142
+ export {
143
+ UI_CONTEXTS,
144
+ DICT_CONTEXTS,
145
+ isUiContext,
146
+ isDictContext,
147
+ } from './ui-context.ts'
148
+ export type { UiContext, DictContext } from './ui-context.ts'
149
+
141
150
  // — Dicionários (apresentação declarada — ADR 0003) ————————————————————————————
142
151
  export {
143
152
  DICT_PRESENTATIONS,
144
153
  DICT_TONES,
145
154
  isDictPresentation,
146
155
  isDictTone,
156
+ dictionaryEntryContext,
147
157
  dictionaryDescriptor,
148
158
  dictionaryDescription,
149
159
  presentDictionaryValue,
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Famílias semânticas compartilhadas entre contratos e UI (ADR 0006).
3
+ *
4
+ * Contexto responde por que um realce existe; cada componente decide como materializá-lo.
5
+ * Tema (light/dark), geometria e variante visual são eixos independentes.
6
+ */
7
+ export const UI_CONTEXTS = [
8
+ 'neutral',
9
+ 'primary',
10
+ 'info',
11
+ 'success',
12
+ 'warning',
13
+ 'danger',
14
+ ] as const
15
+
16
+ export type UiContext = (typeof UI_CONTEXTS)[number]
17
+
18
+ /** Contextos válidos para estados e estágios de domínio. */
19
+ export const DICT_CONTEXTS = ['neutral', 'info', 'success', 'warning', 'danger'] as const
20
+
21
+ export type DictContext = (typeof DICT_CONTEXTS)[number]
22
+
23
+ export function isUiContext(value: unknown): value is UiContext {
24
+ return typeof value === 'string' && (UI_CONTEXTS as readonly string[]).includes(value)
25
+ }
26
+
27
+ export function isDictContext(value: unknown): value is DictContext {
28
+ return typeof value === 'string' && (DICT_CONTEXTS as readonly string[]).includes(value)
29
+ }
@@ -36,6 +36,7 @@ import {
36
36
  type DictEntryMeta,
37
37
  type DictPresentation,
38
38
  } from '../../core/dictionary.ts'
39
+ import { DICT_CONTEXTS, isDictContext } from '../../core/ui-context.ts'
39
40
  import {
40
41
  formatDatetime,
41
42
  formatDate,
@@ -656,7 +657,8 @@ function isLogicalType(value: unknown): value is LogicalType<unknown> {
656
657
  /**
657
658
  * Meta de cada chave de um dict — o tipo mora no core (`@softize/opus/core`, ADR 0003) e é
658
659
  * reexportado aqui pra manter a API: `label` obrigatório, `doc` (negócio, manifest/Lens),
659
- * `description` (tooltip), `tone` (badge tonal de status/estágio), `icon` (catálogo).
660
+ * `description` (tooltip), `context` (contexto semântico de status/estágio), `icon` (catálogo).
661
+ * `tone` permanece como alias temporário de compatibilidade.
660
662
  */
661
663
  export type { DictEntryMeta }
662
664
 
@@ -716,16 +718,23 @@ const dict = <const M extends Record<string, DictEntryMeta>>(
716
718
  `t.dict: presentation "${String(opts.presentation)}" inválida; use ${DICT_PRESENTATIONS.join(' | ')}`,
717
719
  )
718
720
  }
719
- const tonal = opts?.presentation === 'status' || opts?.presentation === 'stage'
721
+ const contextual = opts?.presentation === 'status' || opts?.presentation === 'stage'
720
722
  for (const key of keys) {
721
- const tone = (entries[key] as DictEntryMeta).tone
722
- if (tone === undefined) continue
723
- if (!isDictTone(tone)) {
724
- throw new Error(`t.dict: tone "${String(tone)}" inválido em "${key}"; use ${DICT_TONES.join(' | ')}`)
723
+ const entry = entries[key] as DictEntryMeta
724
+ if (entry.context !== undefined && entry.tone !== undefined) {
725
+ throw new Error(`t.dict: use context ou tone em "${key}", nunca os dois`)
725
726
  }
726
- if (!tonal) {
727
+ const context = entry.context ?? entry.tone
728
+ if (context === undefined) continue
729
+ const legacy = entry.context === undefined
730
+ if (!(legacy ? isDictTone(context) : isDictContext(context))) {
731
+ const values = legacy ? DICT_TONES : DICT_CONTEXTS
732
+ const field = legacy ? 'tone' : 'context'
733
+ throw new Error(`t.dict: ${field} "${String(context)}" inválido em "${key}"; use ${values.join(' | ')}`)
734
+ }
735
+ if (!contextual) {
727
736
  throw new Error(
728
- `t.dict: tone em "${key}" exige presentation "status" ou "stage" (classificação usa outline; plain é texto)`,
737
+ `t.dict: ${legacy ? 'tone' : 'context'} em "${key}" exige presentation "status" ou "stage" (classificação usa outline; plain é texto)`,
729
738
  )
730
739
  }
731
740
  }
@@ -6,22 +6,24 @@
6
6
  */
7
7
  import {
8
8
  Card,
9
- CardContent,
9
+ CardBody,
10
10
  CardDescription,
11
11
  CardFooter,
12
12
  CardHeader,
13
13
  CardTitle,
14
- } from '../primitives/card.tsx'
15
- import { ActionForm, type ActionFormProps } from './form.tsx'
14
+ } from "../primitives/card.tsx";
15
+ import { ActionForm, type ActionFormProps } from "./form.tsx";
16
16
 
17
- export interface ActionFormCardProps<TInput extends Record<string, unknown>, TData>
18
- extends Omit<ActionFormProps<TInput, TData>, 'body' | 'footer'> {
17
+ export interface ActionFormCardProps<
18
+ TInput extends Record<string, unknown>,
19
+ TData,
20
+ > extends Omit<ActionFormProps<TInput, TData>, "body" | "footer"> {
19
21
  /** Título do header do card. Sem ele, o card começa direto no conteúdo. */
20
- title?: string
22
+ title?: string;
21
23
  /** Subtítulo opcional, abaixo do título. */
22
- description?: string
24
+ description?: string;
23
25
  /** Classes do Card (a superfície). `className` vai pro <form>. */
24
- cardClassName?: string
26
+ cardClassName?: string;
25
27
  }
26
28
 
27
29
  export function ActionFormCard<TInput extends Record<string, unknown>, TData>({
@@ -35,14 +37,18 @@ export function ActionFormCard<TInput extends Record<string, unknown>, TData>({
35
37
  {(title !== undefined || description !== undefined) && (
36
38
  <CardHeader>
37
39
  {title !== undefined && <CardTitle>{title}</CardTitle>}
38
- {description !== undefined && <CardDescription>{description}</CardDescription>}
40
+ {description !== undefined && (
41
+ <CardDescription>{description}</CardDescription>
42
+ )}
39
43
  </CardHeader>
40
44
  )}
41
45
  <ActionForm
42
46
  {...rest}
43
- body={(fields) => <CardContent>{fields}</CardContent>}
44
- footer={(actions) => <CardFooter className="justify-end">{actions}</CardFooter>}
47
+ body={(fields) => <CardBody>{fields}</CardBody>}
48
+ footer={(actions) => (
49
+ <CardFooter className="justify-end">{actions}</CardFooter>
50
+ )}
45
51
  />
46
52
  </Card>
47
- )
53
+ );
48
54
  }
@@ -2,7 +2,7 @@
2
2
  * `dialog` — o trio imperativo (alert · confirm · prompt) em UMA linha.
3
3
  *
4
4
  * await dialog.alert({ title: 'Sessão expirada', description: 'Você será desconectado.' })
5
- * if (await dialog.confirm({ title: 'Excluir a sessão?', variant: 'destructive' })) …
5
+ * if (await dialog.confirm({ title: 'Excluir a sessão?', context: 'danger' })) …
6
6
  * const nome = await dialog.prompt({ title: 'Nome do snapshot' })
7
7
  *
8
8
  * Mesma forma do `toast` (superfície imperativa + host no shell), com uma diferença: eles
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import * as React from 'react'
22
+ import type { UiContext } from '../../../core/ui-context.ts'
22
23
  import {
23
24
  AlertDialog,
24
25
  AlertDialogAction,
@@ -50,13 +51,17 @@ export interface AlertOptions extends DialogBase {}
50
51
  /** `confirm` — pergunta de sim/não. */
51
52
  export interface ConfirmOptions extends DialogBase {
52
53
  cancel?: string
53
- /** `destructive` pinta o botão de confirmar — o que apaga tem que parecer o que apaga. */
54
+ /** Contexto do botão de confirmação. */
55
+ context?: Extract<UiContext, 'primary' | 'danger'>
56
+ /** @deprecated Use `context="danger"`. */
54
57
  variant?: 'default' | 'destructive'
55
58
  }
56
59
 
57
60
  /** `prompt` — uma string livre. Form de verdade (campos, contrato) → ActionFormDialog. */
58
61
  export interface PromptOptions extends DialogBase {
59
62
  cancel?: string
63
+ context?: Extract<UiContext, 'primary' | 'danger'>
64
+ /** @deprecated Use `context="danger"`. */
60
65
  variant?: 'default' | 'destructive'
61
66
  placeholder?: string
62
67
  defaultValue?: string
@@ -79,6 +84,16 @@ function requireHost(fn: string): Error | null {
79
84
  )
80
85
  }
81
86
 
87
+ function rejectAmbiguousContext(
88
+ fn: string,
89
+ options: ConfirmOptions | PromptOptions,
90
+ ): Error | null {
91
+ if (options.context === undefined || options.variant === undefined) return null
92
+ return new Error(
93
+ `${fn} não permite combinar context com uma variante semântica legada.`,
94
+ )
95
+ }
96
+
82
97
  export const dialog = {
83
98
  /** Avisa e espera o "OK". `Promise<void>` — só reconhecimento, sem decisão. */
84
99
  alert(options: AlertOptions): Promise<void> {
@@ -90,6 +105,8 @@ export const dialog = {
90
105
  },
91
106
  /** Pergunta sim/não. `Promise<boolean>` — fechar/ESC resolve `false`. */
92
107
  confirm(options: ConfirmOptions): Promise<boolean> {
108
+ const ambiguous = rejectAmbiguousContext('dialog.confirm()', options)
109
+ if (ambiguous !== null) return Promise.reject(ambiguous)
93
110
  const err = requireHost('dialog.confirm()')
94
111
  if (err !== null) return Promise.reject(err)
95
112
  return new Promise<boolean>((resolve) => {
@@ -98,6 +115,8 @@ export const dialog = {
98
115
  },
99
116
  /** Pede uma string. `Promise<string | null>` — fechar/ESC/Cancelar resolve `null`. */
100
117
  prompt(options: PromptOptions): Promise<string | null> {
118
+ const ambiguous = rejectAmbiguousContext('dialog.prompt()', options)
119
+ if (ambiguous !== null) return Promise.reject(ambiguous)
101
120
  const err = requireHost('dialog.prompt()')
102
121
  if (err !== null) return Promise.reject(err)
103
122
  return new Promise<string | null>((resolve) => {
@@ -154,6 +173,9 @@ export function DialogHost(): React.ReactElement {
154
173
  }
155
174
 
156
175
  const variant = current !== undefined && current.kind !== 'alert' ? current.variant : undefined
176
+ const context = current !== undefined && current.kind !== 'alert'
177
+ ? current.context ?? (variant === 'destructive' ? 'danger' : 'primary')
178
+ : 'primary'
157
179
  const defaultAction = current?.kind === 'alert' ? 'OK' : 'Confirmar'
158
180
 
159
181
  return (
@@ -210,7 +232,8 @@ export function DialogHost(): React.ReactElement {
210
232
  </AlertDialogCancel>
211
233
  )}
212
234
  <AlertDialogAction
213
- variant={variant === 'destructive' ? 'destructive' : 'default'}
235
+ context={context}
236
+ variant="solid"
214
237
  onClick={() => drop(current.id, (p) => accept(p, text))}
215
238
  >
216
239
  {current.action ?? defaultAction}