@softize/opus 12.7.1 → 12.8.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 (34) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/bin/lib/gen-dicts.mjs +11 -1
  3. package/bin/lib/gen-runner.mjs +8 -2
  4. package/bin/lib/materialize.mjs +5 -2
  5. package/docs/adr/0003-dictionary-presentation-is-declared.md +160 -0
  6. package/package.json +1 -1
  7. package/registry/skills/build-opus-ui/SKILL.md +31 -10
  8. package/registry/skills/build-opus-ui/references/evaluations.md +25 -0
  9. package/registry/skills/build-opus-ui/references/ui-patterns.md +85 -0
  10. package/registry/skills/implement-opus-change/SKILL.md +4 -3
  11. package/registry/skills/model-opus-dictionary/SKILL.md +76 -0
  12. package/registry/skills/model-opus-dictionary/agents/openai.yaml +4 -0
  13. package/registry/skills/model-opus-dictionary/references/evaluations.md +18 -0
  14. package/src/core/dictionary.ts +152 -0
  15. package/src/core/index.ts +18 -0
  16. package/src/core/types.ts +9 -1
  17. package/src/schema/drivers/zod.ts +46 -13
  18. package/src/ui/components/patterns/list.tsx +136 -51
  19. package/src/ui/components/primitives/badge.tsx +3 -0
  20. package/src/ui/components/primitives/detail.tsx +12 -1
  21. package/src/ui/components/primitives/dictionary-value.tsx +141 -0
  22. package/src/ui/components/primitives/empty-value.tsx +50 -0
  23. package/src/ui/components/primitives/pagination.tsx +86 -56
  24. package/src/ui/docs/content/action-list-dialog.md +1 -1
  25. package/src/ui/docs/content/action-list.md +34 -9
  26. package/src/ui/docs/content/badge.md +6 -3
  27. package/src/ui/docs/content/detail.md +19 -2
  28. package/src/ui/docs/content/dictionary-value.md +120 -0
  29. package/src/ui/docs/content/empty-value.md +48 -0
  30. package/src/ui/docs/content/pagination.md +54 -17
  31. package/src/ui/docs/doc-client.tsx +21 -4
  32. package/src/ui/docs/registry.tsx +6 -0
  33. package/src/ui/meta.ts +14 -2
  34. package/src/ui/react.tsx +6 -0
@@ -0,0 +1,120 @@
1
+ Um valor de dicionário (`t.dict`) apresentado do jeito que o **dicionário** declarou. A tela não
2
+ decide se o valor vira badge, cor ou ícone: ela passa o dicionário e o código, e o renderer
3
+ aplica os defaults da apresentação declarada. O rótulo está sempre presente como texto — cor e
4
+ ícone nunca comunicam sozinhos.
5
+
6
+ ```tsx preview
7
+ <DictionaryValue
8
+ dict={{ keys: ['pf', 'pj'], entries: { pf: { label: 'Pessoa física', icon: 'user' }, pj: { label: 'Empresa', icon: 'building' } }, presentation: 'classification' }}
9
+ value="pj"
10
+ />
11
+ <DictionaryValue
12
+ dict={{ keys: ['prospect', 'customer'], entries: { prospect: { label: 'Prospect' }, customer: { label: 'Cliente', tone: 'success' } }, presentation: 'stage' }}
13
+ value="customer"
14
+ />
15
+ <DictionaryValue
16
+ dict={{ keys: ['open', 'blocked'], entries: { open: { label: 'Aberto', tone: 'warning', icon: 'clock', description: 'Aguarda uma decisão da Fonte.' }, blocked: { label: 'Bloqueado', tone: 'danger' } }, presentation: 'status' }}
17
+ value="open"
18
+ />
19
+ <DictionaryValue
20
+ dict={{ keys: ['nbs', 'followize'], entries: { nbs: { label: 'NBS' }, followize: { label: 'Followize' } } }}
21
+ value="nbs"
22
+ />
23
+ ```
24
+
25
+ ## O dicionário declara o papel
26
+
27
+ `presentation` fica no `t.dict`, ao lado de `doc`. Sem ele, o valor é texto: a apresentação é
28
+ declarada, nunca inferida pelo nome do dicionário.
29
+
30
+ ```ts
31
+ export const customerKindDict = t.dict(
32
+ {
33
+ pf: { label: 'Pessoa física', icon: 'user' },
34
+ pj: { label: 'Empresa', icon: 'building' },
35
+ },
36
+ { doc: 'Natureza da parte no cadastro global.', presentation: 'classification' },
37
+ )
38
+
39
+ export const customerStageDict = t.dict(
40
+ {
41
+ prospect: { label: 'Prospect', description: 'Relacionamento ainda em prospecção.' },
42
+ customer: { label: 'Cliente', tone: 'success' },
43
+ },
44
+ { doc: 'Estágio comercial atual da parte.', presentation: 'stage' },
45
+ )
46
+ ```
47
+
48
+ | Papel | Quando | Forma | Variante |
49
+ |---|---|---|---|
50
+ | `classification` | Tipo, categoria, natureza — uma dimensão estável de comparação. | Badge | `outline`; ignora `tone`. |
51
+ | `status` | Situação operacional que muda com o tempo. | Badge | Tonal pelo `tone` da entrada; `neutral` sem tom. Nunca `outline`. |
52
+ | `stage` | Etapa de um ciclo ou funil. | Badge | Igual a `status`. |
53
+ | `plain` ou ausente | Valor que só precisa ser legível. | Texto | — |
54
+
55
+ ## Por entrada: tom, ícone e descrição
56
+
57
+ - `tone` (`neutral` · `info` · `success` · `warning` · `danger`) só existe em status e estágio;
58
+ `t.dict` rejeita o tom em classificação e em `plain`.
59
+ - `icon` é um nome do catálogo do Opus (`iconPickerIcons`). Nome fora do catálogo não renderiza
60
+ ícone; o renderer não inventa. Vocabulário próprio entra por `icons`.
61
+ - `description` é o texto curto para a pessoa e aparece em tooltip focável. Descrição igual ao
62
+ rótulo não vira tooltip. `doc` é o entendimento de negócio para manifest e Lens; não aparece.
63
+
64
+ ```tsx preview
65
+ <DictionaryValue
66
+ dict={{ keys: ['closed'], entries: { closed: { label: 'Fechado', icon: 'nao-existe', description: 'Fechado' } }, presentation: 'status' }}
67
+ value="closed"
68
+ />
69
+ <DictionaryValue
70
+ dict={{ keys: ['closed'], entries: { closed: { label: 'Fechado', icon: 'archive' } }, presentation: 'status' }}
71
+ value="closed"
72
+ icons={{ archive: Archive }}
73
+ />
74
+ ```
75
+
76
+ ## Override explícito
77
+
78
+ Os defaults vêm do dicionário; a tela sobrepõe só quando tem um motivo, e sobrepõe às claras.
79
+
80
+ ```tsx preview
81
+ <DictionaryValue
82
+ dict={{ keys: ['customer'], entries: { customer: { label: 'Cliente', tone: 'success' } }, presentation: 'stage' }}
83
+ value="customer"
84
+ presentation="plain"
85
+ />
86
+ <DictionaryValue
87
+ dict={{ keys: ['pf'], entries: { pf: { label: 'Pessoa física', icon: 'user' } }, presentation: 'classification' }}
88
+ value="pf"
89
+ icon={false}
90
+ />
91
+ <DictionaryValue
92
+ dict={{ keys: ['pf'], entries: { pf: { label: 'Pessoa física' } }, presentation: 'classification' }}
93
+ value={null}
94
+ fallback={<span className="text-muted-foreground">—</span>}
95
+ />
96
+ ```
97
+
98
+ Valor fora do dicionário aparece como texto com o próprio código, marcado com
99
+ `data-dictionary-unknown`, sem badge, ícone ou tooltip, mesmo com `variant` sobreposto.
100
+
101
+ ## Em ActionList
102
+
103
+ Coluna cujo campo do schema de saída é um `t.dict().zod()` já renderiza `DictionaryValue` com o
104
+ papel declarado, sem `cells`. Quando o campo de saída é uma string comum, a coluna nomeia o
105
+ dicionário registrado no provider: `{ key: 'source', label: 'Fonte', dictionary: 'customerSource' }`.
106
+ Dimensões independentes (tipo e estágio, por exemplo) ficam em colunas distintas; não empilhar
107
+ uma sob a outra como texto secundário.
108
+
109
+ ## Props
110
+
111
+ | Prop | Tipo | Default | Descrição |
112
+ |---|---|---|---|
113
+ | `dict` | `DictType \| LogicalTypeMeta \| DictionaryDescriptor` | — | O dicionário (`t.dict`), a meta lida do schema ou um descritor normalizado. |
114
+ | `value` | `string \| null \| undefined` | — | O código. Vazio renderiza `fallback`. |
115
+ | `presentation` | `'classification' \| 'status' \| 'stage' \| 'plain'` | do dicionário | Sobrepõe o papel só nesta ocorrência. |
116
+ | `variant` | variante de `Badge` | pelo papel | Sobrepõe a variante e promove texto a badge. |
117
+ | `icon` | `boolean \| ReactNode` | `true` | `false` esconde o ícone declarado; um nó substitui. |
118
+ | `icons` | `Record<string, LucideIcon>` | `iconPickerIcons` | Catálogo nome → componente. |
119
+ | `tooltip` | `boolean` | `true` | `false` suprime a descrição. |
120
+ | `fallback` | `ReactNode` | `null` | Conteúdo quando `value` é vazio. |
@@ -0,0 +1,48 @@
1
+ Valor ausente tem uma representação padrão. Em célula compacta de tabela aparece o travessão,
2
+ e a leitura assistiva recebe “Não informado”; em texto corrido, como no `DetailField`, aparece o
3
+ próprio rótulo. As colunas de `ActionList` e o `DetailField` já fazem isso sozinhos; um renderer
4
+ customizado reutiliza a primitiva em vez de repetir a condicional.
5
+
6
+ ```tsx preview
7
+ <div className="flex items-center gap-6 text-sm">
8
+ <EmptyValue />
9
+ <EmptyValue compact />
10
+ <EmptyValue label="Nunca enviado" />
11
+ <EmptyValue compact label="Sem vencimento" />
12
+ </div>
13
+ ```
14
+
15
+ ## O que conta como ausência
16
+
17
+ `null`, `undefined`, string vazia e string só com espaços. `0`, `false` e coleções vazias são
18
+ valores legítimos: quem apresenta booleanos e listas decide a própria forma. `isAbsentValue`
19
+ exporta a mesma regra para renderers customizados.
20
+
21
+ ## O domínio dá o significado
22
+
23
+ A ausência raramente é neutra: um envio que nunca aconteceu, um prazo que não existe, um campo
24
+ que não se aplica. `label` troca o texto, e `ListColumnSpec.empty` faz o mesmo na coluna do
25
+ contrato, sem condicional na tela.
26
+
27
+ ```tsx preview col
28
+ <DetailGroup columns={2}>
29
+ <DetailField label="E-mail" value="" />
30
+ <DetailField label="Último envio" value={null} empty="Nunca enviado" />
31
+ <DetailField label="Saldo" value={0} />
32
+ <DetailField label="Vencimento" value={undefined} empty="Sem vencimento" />
33
+ </DetailGroup>
34
+ ```
35
+
36
+ ```ts
37
+ columns: [
38
+ { key: 'email', label: 'E-mail' }, // — · “Não informado”
39
+ { key: 'sentAt', label: 'Último envio', empty: 'Nunca enviado' }, // — · “Nunca enviado”
40
+ ]
41
+ ```
42
+
43
+ ## Props
44
+
45
+ | Prop | Tipo | Default | Descrição |
46
+ |---|---|---|---|
47
+ | `label` | `string` | `'Não informado'` | O que a ausência significa neste domínio. |
48
+ | `compact` | `boolean` | `false` | Célula compacta: travessão visível, rótulo só para leitura assistiva. |
@@ -1,6 +1,10 @@
1
1
  ## Básico
2
2
 
3
- É composição: Pagination embrulha PaginationContent, e cada PaginationItem segura um link. isActive no PaginationLink marca a página atual (vira outline).
3
+ É composição: Pagination embrulha PaginationContent, e cada PaginationItem segura um link. `page`
4
+ dá o número e o nome acessível (“Página N”); `isActive` marca a página atual (vira outline). Cada
5
+ número tem altura fixa e largura mínima quadrada que cresce com os dígitos — 5726 e 5727 nunca se
6
+ colam. As setas seguem quadradas e já falam pt-BR; `label` localiza, `iconOnly` deixa só a seta.
7
+ É o único paginador da casa: `ActionList` compõe esta primitiva no rodapé, na escala densa.
4
8
 
5
9
  ```tsx preview
6
10
  <Pagination>
@@ -9,15 +13,13 @@
9
13
  <PaginationPrevious href="#" />
10
14
  </PaginationItem>
11
15
  <PaginationItem>
12
- <PaginationLink href="#">1</PaginationLink>
16
+ <PaginationLink href="#" page={1} />
13
17
  </PaginationItem>
14
18
  <PaginationItem>
15
- <PaginationLink href="#" isActive>
16
- 2
17
- </PaginationLink>
19
+ <PaginationLink href="#" page={2} isActive />
18
20
  </PaginationItem>
19
21
  <PaginationItem>
20
- <PaginationLink href="#">3</PaginationLink>
22
+ <PaginationLink href="#" page={3} />
21
23
  </PaginationItem>
22
24
  <PaginationItem>
23
25
  <PaginationNext href="#" />
@@ -26,6 +28,37 @@
26
28
  </Pagination>
27
29
  ```
28
30
 
31
+ ## Números longos
32
+
33
+ A largura mínima é quadrada; o número manda no resto. Na escala densa de um rodapé, ajuste por
34
+ `className` (`h-7 min-w-7 text-xs` nos números; `size-7` mais `iconClassName="size-3.5"` nas
35
+ setas com `iconOnly`).
36
+
37
+ ```tsx preview
38
+ <Pagination>
39
+ <PaginationContent className="gap-0.5">
40
+ <PaginationItem>
41
+ <PaginationPrevious href="#" iconOnly className="size-7" iconClassName="size-3.5" />
42
+ </PaginationItem>
43
+ <PaginationItem>
44
+ <PaginationLink href="#" page={1} className="h-7 min-w-7 text-xs" />
45
+ </PaginationItem>
46
+ <PaginationItem>
47
+ <PaginationEllipsis className="size-7" />
48
+ </PaginationItem>
49
+ <PaginationItem>
50
+ <PaginationLink href="#" page={5726} className="h-7 min-w-7 text-xs" />
51
+ </PaginationItem>
52
+ <PaginationItem>
53
+ <PaginationLink href="#" page={5727} isActive className="h-7 min-w-7 text-xs" />
54
+ </PaginationItem>
55
+ <PaginationItem>
56
+ <PaginationNext href="#" iconOnly className="size-7" iconClassName="size-3.5" />
57
+ </PaginationItem>
58
+ </PaginationContent>
59
+ </Pagination>
60
+ ```
61
+
29
62
  ## Com elipse
30
63
 
31
64
  PaginationEllipsis é o atalho decorativo (aria-hidden) entre blocos de páginas distantes — útil quando a lista de sessões do workspace tem páginas demais pra caber na barra.
@@ -37,18 +70,16 @@ PaginationEllipsis é o atalho decorativo (aria-hidden) entre blocos de páginas
37
70
  <PaginationPrevious href="#" />
38
71
  </PaginationItem>
39
72
  <PaginationItem>
40
- <PaginationLink href="#">1</PaginationLink>
73
+ <PaginationLink href="#" page={1} />
41
74
  </PaginationItem>
42
75
  <PaginationItem>
43
76
  <PaginationEllipsis />
44
77
  </PaginationItem>
45
78
  <PaginationItem>
46
- <PaginationLink href="#" isActive>
47
- 8
48
- </PaginationLink>
79
+ <PaginationLink href="#" page={8} isActive />
49
80
  </PaginationItem>
50
81
  <PaginationItem>
51
- <PaginationLink href="#">9</PaginationLink>
82
+ <PaginationLink href="#" page={9} />
52
83
  </PaginationItem>
53
84
  <PaginationItem>
54
85
  <PaginationNext href="#" />
@@ -59,7 +90,9 @@ PaginationEllipsis é o atalho decorativo (aria-hidden) entre blocos de páginas
59
90
 
60
91
  ## Controlado
61
92
 
62
- Os links são <a> sem href, vire onClick e guarde a página no estado. Aqui a barra pagina os repositórios do workspace Empresa X.
93
+ Sem `href`, cada link vira `<button>`: foco, teclado e `disabled` funcionam como em qualquer
94
+ botão. Guarde a página no estado e responda ao `onClick`. Aqui a barra pagina os repositórios do
95
+ workspace Empresa X.
63
96
 
64
97
  ```tsx preview col
65
98
  const [page, setPage] = useState(2)
@@ -70,18 +103,18 @@ render(
70
103
  <PaginationContent>
71
104
  <PaginationItem>
72
105
  <PaginationPrevious
106
+ disabled={page <= 1}
73
107
  onClick={() => setPage((p) => Math.max(1, p - 1))}
74
108
  />
75
109
  </PaginationItem>
76
110
  {pages.map((n) => (
77
111
  <PaginationItem key={n}>
78
- <PaginationLink isActive={n === page} onClick={() => setPage(n)}>
79
- {n}
80
- </PaginationLink>
112
+ <PaginationLink page={n} isActive={n === page} onClick={() => setPage(n)} />
81
113
  </PaginationItem>
82
114
  ))}
83
115
  <PaginationItem>
84
116
  <PaginationNext
117
+ disabled={page >= pages.length}
85
118
  onClick={() => setPage((p) => Math.min(pages.length, p + 1))}
86
119
  />
87
120
  </PaginationItem>
@@ -94,6 +127,10 @@ render(
94
127
 
95
128
  | Prop | Tipo | Default | Descrição |
96
129
  |---|---|---|---|
130
+ | `page (PaginationLink)` | `number` | | Número da página: vira o conteúdo (quando não há `children`) e o nome acessível “Página N”. |
97
131
  | `isActive (PaginationLink)` | `boolean` | `false` | Marca a página atual: vira outline e ganha aria-current="page". Os demais ficam ghost. |
98
- | `size (PaginationLink)` | `'default' \| 'sm' \| 'lg' \| 'icon'` | `'icon'` | Tamanho do link, herdado do Button. Previous/Next forçam default pra caber o texto. |
99
- | `href (PaginationLink)` | `string` | | Cada link é um <a>: passe href pra navegação real, ou use onClick no modo controlado. |
132
+ | `href (PaginationLink)` | `string` | | Com `href` o link é um `<a>`; sem `href` é um `<button>` controlado por `onClick`, com `disabled`. |
133
+ | `size (PaginationLink)` | `'default' \| 'sm' \| 'lg' \| 'icon' \| 'icon-sm' \| 'icon-xs'` | `'default'` | Escala do Button. Números usam `default` (largura mínima quadrada que cresce); as setas com `iconOnly` usam `icon`. |
134
+ | `label (Previous/Next)` | `string` | `'Página anterior'` / `'Próxima página'` | Nome acessível e texto visível das setas. |
135
+ | `iconOnly (Previous/Next)` | `boolean` | `false` | Só a seta, quadrada (`size="icon"`); o `label` continua como nome acessível. |
136
+ | `iconClassName (Previous/Next)` | `string` | | Classe do svg da seta, para a escala densa (`size-3.5`). |
@@ -6,8 +6,8 @@
6
6
  */
7
7
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
8
8
  import { z } from 'zod'
9
- import { defineContract, error } from '../../core/index.ts'
10
- import type { ActionDef, ActionResult, ClientAdapter, Paginated } from '../../core/index.ts'
9
+ import { attachLogicalType, defineContract, error } from '../../core/index.ts'
10
+ import type { ActionDef, ActionResult, ClientAdapter, LogicalTypeMeta, Paginated } from '../../core/index.ts'
11
11
  import { TbdlibProvider } from '../react.tsx'
12
12
 
13
13
  // — Dados enlatados (domínio da casa) —
@@ -28,6 +28,23 @@ const WORKSPACES: DocBrowserWorkspace[] = [
28
28
 
29
29
  const STATUS = ['active', 'onboarding', 'paused'] as const
30
30
 
31
+ /** O vocabulário de status do workspace com a apresentação declarada (ADR 0003). O palco mora
32
+ * na UI, e a fronteira ui→core não deixa importar o driver de schema; isto é exatamente o que
33
+ * `t.dict(entries, { presentation: 'status' })` produz — o enum com a meta lógica anexada. */
34
+ export const docWorkspaceStatus: LogicalTypeMeta = {
35
+ logicalType: 'dict',
36
+ params: {
37
+ keys: [...STATUS],
38
+ entries: {
39
+ active: { label: 'Ativo', tone: 'success', description: 'Agentes em operação para o cliente.' },
40
+ onboarding: { label: 'Onboarding', tone: 'warning' },
41
+ paused: { label: 'Pausado' },
42
+ },
43
+ presentation: 'status',
44
+ },
45
+ }
46
+ const workspaceStatus = attachLogicalType(z.enum(STATUS), docWorkspaceStatus)
47
+
31
48
  // — Contratos fake (mesmo shape dos reais; ver apps/admin/shared/src/workspaces.ts) —
32
49
  export const docWorkspaceCreate = defineContract({
33
50
  name: 'workspace.create',
@@ -65,7 +82,7 @@ export const docWorkspaceList = defineContract({
65
82
  id: z.string(),
66
83
  name: z.string(),
67
84
  client: z.string(),
68
- status: z.enum(STATUS),
85
+ status: workspaceStatus,
69
86
  agents: z.number(),
70
87
  }),
71
88
  // O modo DECLARATIVO do ActionList: a tabela e a toolbar derivam daqui.
@@ -104,7 +121,7 @@ export const docWorkspaceView = defineContract({
104
121
  id: z.string(),
105
122
  name: z.string(),
106
123
  client: z.string(),
107
- status: z.enum(STATUS),
124
+ status: workspaceStatus,
108
125
  agents: z.number(),
109
126
  }),
110
127
  projection: ['id', 'name', 'client', 'status', 'agents'],
@@ -68,6 +68,8 @@ import composerMd from './content/composer.md?raw'
68
68
  import copyableMd from './content/copyable.md?raw'
69
69
  import dialogMd from './content/dialog.md?raw'
70
70
  import detailMd from './content/detail.md?raw'
71
+ import dictionaryValueMd from './content/dictionary-value.md?raw'
72
+ import emptyValueMd from './content/empty-value.md?raw'
71
73
  import drawerMd from './content/drawer.md?raw'
72
74
  import menuMd from './content/menu.md?raw'
73
75
  import emptyMd from './content/empty.md?raw'
@@ -106,6 +108,7 @@ import {
106
108
  docSessionDelete,
107
109
  docWorkspaceCreate,
108
110
  docWorkspaceList,
111
+ docWorkspaceStatus,
109
112
  docWorkspaceView,
110
113
  } from './doc-client'
111
114
  import actionFormMd from './content/action-form.md?raw'
@@ -175,6 +178,7 @@ const patternScope = {
175
178
  DocBrowserActionProvider,
176
179
  docWorkspaceCreate,
177
180
  docWorkspaceList,
181
+ docWorkspaceStatus,
178
182
  docWorkspaceView,
179
183
  docSessionArchive,
180
184
  docSessionDelete,
@@ -365,6 +369,8 @@ export const UI_SECTIONS: DocSection[] = [
365
369
  { slug: 'carousel', title: 'Carousel', render: comp('Carousel', 'carousel', carouselMd) },
366
370
  { slug: 'copyable', title: 'Copyable', render: comp('Copyable', 'copyable', copyableMd) },
367
371
  { slug: 'detail', title: 'Detail', render: comp('Detail', 'detail', detailMd) },
372
+ { slug: 'dictionary-value', title: 'Dictionary Value', render: comp('Dictionary Value', 'dictionary-value', dictionaryValueMd) },
373
+ { slug: 'empty-value', title: 'Empty Value', render: comp('Empty Value', 'empty-value', emptyValueMd) },
368
374
  { slug: 'item', title: 'Item', render: comp('Item', 'item', itemMd) },
369
375
  { slug: 'kbd', title: 'Kbd', render: comp('Kbd', 'kbd', kbdMd) },
370
376
  { slug: 'markdown', title: 'Markdown', render: comp('Markdown', 'markdown', markdownMd) },
package/src/ui/meta.ts CHANGED
@@ -27,7 +27,19 @@ export const componentMeta = {
27
27
  name: 'badge',
28
28
  ancestry: 'shadcn',
29
29
  whenToUse:
30
- 'Rótulo curto de status/categoria. `variant` pra intenção (default/secondary/destructive/outline) ou tom de status (success/warning/info, fill tingido). Não-interativo — pra clique, use Button ou `asChild` num <a>.',
30
+ 'Rótulo curto de status/categoria. `variant` pra intenção (default/secondary/destructive/outline) ou tom de status (success/warning/info/danger, fill tingido). Não-interativo — pra clique, use Button ou `asChild` num <a>. Valor de dicionário com papel declarado usa DictionaryValue em vez de escolher a variante à mão.',
31
+ },
32
+ 'empty-value': {
33
+ name: 'empty-value',
34
+ ancestry: 'opus',
35
+ whenToUse:
36
+ 'Representação padrão de valor ausente (`null`, `undefined`, string vazia ou só espaços): em célula compacta (`compact`) mostra o travessão e reserva “Não informado” à leitura assistiva; em texto corrido mostra o rótulo. `label` troca o significado da ausência no domínio (“Nunca enviado”, “Sem vencimento”). `0` e `false` são valores, nunca ausência. Colunas de ActionList e DetailField já a usam; renderer customizado reutiliza em vez de repetir a condicional.',
37
+ },
38
+ 'dictionary-value': {
39
+ name: 'dictionary-value',
40
+ ancestry: 'opus',
41
+ whenToUse:
42
+ 'Valor de um `t.dict` com a apresentação que o dicionário declarou (ADR 0003): `classification` vira Badge outline, `status`/`stage` viram Badge tonal pelo `tone` da entrada, `plain` ou sem papel vira texto. Ícone só quando a entrada declara `icon` do catálogo; tooltip só quando `description` acrescenta ao rótulo. O texto está sempre presente. Overrides explícitos por `presentation`, `variant`, `icon`, `tooltip` e `fallback`; não inferir badge, cor ou ícone a partir do nome do dicionário. Colunas de ActionList cujo campo é dicionário já usam este renderer.',
31
43
  },
32
44
  'dot': {
33
45
  name: 'dot',
@@ -293,7 +305,7 @@ export const componentMeta = {
293
305
  name: 'pagination',
294
306
  ancestry: 'shadcn',
295
307
  whenToUse:
296
- 'Navegação entre páginas montada por composição (shadcn): Pagination › PaginationContent › PaginationItem com PaginationLink, mais PaginationPrevious/PaginationNext e PaginationEllipsis. Marque a página atual com `isActive` no PaginationLink; os links são `<a>`, então use `href` pra navegação real ou `onClick` no modo controlado. Pra rolagem infinita ou listas curtas, dispense a barra.',
308
+ 'Navegação entre páginas montada por composição: Pagination › PaginationContent › PaginationItem com PaginationLink, mais PaginationPrevious/PaginationNext e PaginationEllipsis. `page` o número e o nome acessível (“Página N”); `isActive` marca a atual. Com `href` o link é `<a>`; sem `href` vira `<button>` (modo controlado, com foco e `disabled`). Os números têm largura mínima quadrada e crescem com os dígitos; as setas seguem quadradas (`iconOnly`). `label` localiza as setas. É o único paginador: ActionList compõe esta primitiva na escala densa do rodapé. Pra rolagem infinita ou listas curtas, dispense a barra.',
297
309
  },
298
310
  'progress': {
299
311
  name: 'progress',
package/src/ui/react.tsx CHANGED
@@ -47,6 +47,11 @@ export { Label } from './components/primitives/label.tsx'
47
47
  export { Badge, badgeVariants } from './components/primitives/badge.tsx'
48
48
  export type { BadgeProps } from './components/primitives/badge.tsx'
49
49
 
50
+ export { DictionaryValue } from './components/primitives/dictionary-value.tsx'
51
+ export { EmptyValue, isAbsentValue } from './components/primitives/empty-value.tsx'
52
+ export type { EmptyValueProps } from './components/primitives/empty-value.tsx'
53
+ export type { DictionaryValueProps, DictionaryValueSource } from './components/primitives/dictionary-value.tsx'
54
+
50
55
  export { DetailField, DetailGroup } from './components/primitives/detail.tsx'
51
56
  export type {
52
57
  DetailFieldProps,
@@ -172,6 +177,7 @@ export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from './comp
172
177
  export { Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemGroup, ItemHeader, ItemMedia, ItemSeparator, ItemTitle } from './components/primitives/item.tsx'
173
178
  export { Kbd, KbdGroup } from './components/primitives/kbd.tsx'
174
179
  export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from './components/primitives/pagination.tsx'
180
+ export type { PaginationLinkProps, PaginationControlProps } from './components/primitives/pagination.tsx'
175
181
  export { Progress } from './components/primitives/progress.tsx'
176
182
  export { RadioGroup, RadioGroupItem } from './components/primitives/radio-group.tsx'
177
183
  export { ScrollArea, ScrollBar } from './components/primitives/scroll-area.tsx'