@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.
- package/CHANGELOG.md +27 -0
- package/bin/lib/check.mjs +1103 -310
- package/bin/lib/copy.mjs +11 -0
- package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
- package/docs/adr/0004-page-content-state-is-composed.md +65 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- package/package.json +1 -1
- package/registry/instructions/opus.md +5 -0
- package/registry/skills/build-opus-ui/SKILL.md +27 -16
- package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
- package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
- package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
- package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
- package/registry/templates/app/src/App.tsx +1 -1
- package/src/core/dictionary.ts +52 -14
- package/src/core/index.ts +10 -0
- package/src/core/ui-context.ts +29 -0
- package/src/schema/drivers/zod.ts +17 -8
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +26 -3
- package/src/ui/components/patterns/content-header.tsx +335 -61
- package/src/ui/components/patterns/data-state.tsx +23 -10
- package/src/ui/components/patterns/list.tsx +1096 -777
- package/src/ui/components/patterns/page-state.tsx +115 -0
- package/src/ui/components/patterns/page.tsx +231 -41
- package/src/ui/components/patterns/sidebar.tsx +354 -80
- package/src/ui/components/patterns/trigger.tsx +13 -9
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert-dialog.tsx +7 -5
- package/src/ui/components/primitives/alert.tsx +298 -110
- package/src/ui/components/primitives/ask.tsx +2 -1
- package/src/ui/components/primitives/badge.tsx +91 -30
- package/src/ui/components/primitives/button.tsx +99 -60
- package/src/ui/components/primitives/calendar.tsx +39 -39
- package/src/ui/components/primitives/card.tsx +96 -23
- package/src/ui/components/primitives/detail.tsx +2 -2
- package/src/ui/components/primitives/dictionary-value.tsx +9 -14
- package/src/ui/components/primitives/dot.tsx +74 -21
- package/src/ui/components/primitives/drawer.tsx +33 -20
- package/src/ui/components/primitives/item.tsx +137 -81
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/docs/DocBrowser.tsx +3 -3
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form-card.md +1 -1
- package/src/ui/docs/content/alert-dialog.md +8 -8
- package/src/ui/docs/content/alert.md +49 -25
- package/src/ui/docs/content/badge.md +18 -19
- package/src/ui/docs/content/button.md +12 -9
- package/src/ui/docs/content/card.md +5 -5
- package/src/ui/docs/content/content.md +44 -0
- package/src/ui/docs/content/customization.md +2 -2
- package/src/ui/docs/content/detail.md +5 -2
- package/src/ui/docs/content/dialog.md +2 -2
- package/src/ui/docs/content/dictionary-value.md +11 -10
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +6 -3
- package/src/ui/docs/content/input-group.md +3 -2
- package/src/ui/docs/content/item.md +47 -21
- package/src/ui/docs/content/menu.md +5 -4
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/page-state.md +45 -0
- package/src/ui/docs/content/page.md +48 -10
- package/src/ui/docs/content/semantic-context.md +63 -0
- package/src/ui/docs/content/sidebar.md +4 -4
- package/src/ui/docs/content/skeleton.md +2 -2
- package/src/ui/docs/content/table.md +3 -3
- package/src/ui/docs/content/tokens.md +28 -0
- package/src/ui/docs/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +596 -228
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +292 -270
- package/src/ui/react.tsx +378 -111
- 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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 `
|
|
67
|
-
| `status` | Aberto, resolvido, degradado | Badge |
|
|
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',
|
|
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="
|
|
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
|
|
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 `
|
|
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 `
|
|
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 `
|
|
15
|
-
projeta `presentation` e que `t.dict` rejeita
|
|
16
|
-
- Reprova: declarar `
|
|
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
|
|
29
|
+
<Badge context={t.done ? 'success' : 'neutral'}>{t.done ? 'Feita' : 'Aberta'}</Badge>
|
|
30
30
|
</li>
|
|
31
31
|
))}
|
|
32
32
|
</ul>
|
package/src/core/dictionary.ts
CHANGED
|
@@ -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
|
-
/**
|
|
17
|
-
export const DICT_TONES =
|
|
18
|
-
|
|
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
|
-
* - `
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
/**
|
|
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 |
|
|
119
|
-
* | status / stage | badge |
|
|
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 {
|
|
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
|
|
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
|
-
?
|
|
177
|
+
? 'subtle'
|
|
142
178
|
: null
|
|
143
179
|
return {
|
|
144
180
|
value,
|
|
145
181
|
label: entry.label,
|
|
146
182
|
known: true,
|
|
147
183
|
presentation,
|
|
148
|
-
|
|
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), `
|
|
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
|
|
721
|
+
const contextual = opts?.presentation === 'status' || opts?.presentation === 'stage'
|
|
720
722
|
for (const key of keys) {
|
|
721
|
-
const
|
|
722
|
-
if (tone
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
CardBody,
|
|
10
10
|
CardDescription,
|
|
11
11
|
CardFooter,
|
|
12
12
|
CardHeader,
|
|
13
13
|
CardTitle,
|
|
14
|
-
} from
|
|
15
|
-
import { ActionForm, type ActionFormProps } from
|
|
14
|
+
} from "../primitives/card.tsx";
|
|
15
|
+
import { ActionForm, type ActionFormProps } from "./form.tsx";
|
|
16
16
|
|
|
17
|
-
export interface ActionFormCardProps<
|
|
18
|
-
extends
|
|
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 &&
|
|
40
|
+
{description !== undefined && (
|
|
41
|
+
<CardDescription>{description}</CardDescription>
|
|
42
|
+
)}
|
|
39
43
|
</CardHeader>
|
|
40
44
|
)}
|
|
41
45
|
<ActionForm
|
|
42
46
|
{...rest}
|
|
43
|
-
body={(fields) => <
|
|
44
|
-
footer={(actions) =>
|
|
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?',
|
|
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
|
-
/**
|
|
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
|
-
|
|
235
|
+
context={context}
|
|
236
|
+
variant="solid"
|
|
214
237
|
onClick={() => drop(current.id, (p) => accept(p, text))}
|
|
215
238
|
>
|
|
216
239
|
{current.action ?? defaultAction}
|