@softize/opus 13.1.0 → 15.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 +97 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +33 -5
- package/bin/lib/cli-shared.mjs +30 -1
- package/bin/lib/copy.mjs +279 -6
- package/bin/lib/db.mjs +2 -0
- package/docs/adr/0004-page-content-state-is-composed.md +39 -5
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +9 -3
- package/docs/adr/0009-page-title-does-not-carry-a-counter.md +57 -0
- package/docs/adr/0010-page-header-owns-page-chrome.md +73 -0
- package/docs/code-style.md +4 -1
- package/docs/data-layer.md +9 -0
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -3
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
- package/registry/templates/app/src/App.tsx +11 -6
- package/src/core/types.ts +3 -4
- package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
- package/src/ui/components/patterns/confirm.tsx +2 -31
- package/src/ui/components/patterns/content-header.tsx +42 -141
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/form.tsx +15 -15
- package/src/ui/components/patterns/list.tsx +55 -34
- package/src/ui/components/patterns/page-state.tsx +81 -51
- package/src/ui/components/patterns/page.tsx +228 -97
- package/src/ui/components/patterns/state-surface.tsx +262 -0
- package/src/ui/components/patterns/surface-header.tsx +204 -0
- package/src/ui/components/patterns/trigger.tsx +9 -10
- package/src/ui/components/patterns/view.tsx +14 -16
- package/src/ui/components/primitives/alert.tsx +1 -33
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +2 -43
- package/src/ui/components/primitives/button-group.tsx +34 -8
- package/src/ui/components/primitives/button.tsx +31 -35
- package/src/ui/components/primitives/control.ts +69 -0
- package/src/ui/components/primitives/dot.tsx +1 -30
- package/src/ui/components/primitives/input-group.tsx +11 -8
- package/src/ui/components/primitives/item.tsx +3 -1
- package/src/ui/components/primitives/menu.tsx +1 -7
- package/src/ui/components/primitives/pagination.tsx +16 -8
- package/src/ui/components/primitives/select.tsx +2 -2
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +4 -1
- package/src/ui/components/primitives/tabs.tsx +5 -3
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/docs/content/action-form.md +26 -0
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +35 -2
- package/src/ui/docs/content/action-trigger.md +5 -4
- package/src/ui/docs/content/action-view.md +3 -2
- package/src/ui/docs/content/alert.md +16 -4
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +48 -17
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/content.md +5 -4
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/page.md +160 -37
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/semantic-context.md +3 -2
- package/src/ui/docs/content/sidebar.md +2 -42
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/drivers/react.tsx +1 -6
- package/src/ui/meta.ts +8 -8
- package/src/ui/react.tsx +16 -18
- package/src/ui/components/patterns/shell-nav.tsx +0 -154
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,103 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 15.0.0 — 2026-09-09
|
|
11
|
+
|
|
12
|
+
`PageHeader` passa a oferecer `variant="bar"`, uma apresentação compacta da mesma região de título,
|
|
13
|
+
descrição e ações. O novo `PageBack` recebe o destino pai explícito: no header padrão fica acima do
|
|
14
|
+
título com rótulo; na barra vira icon-only com tooltip e nome acessível. Páginas comuns deixam de
|
|
15
|
+
precisar de um chrome paralelo, enquanto `PageActionsTarget` permanece para workspaces imersivos que
|
|
16
|
+
já tenham uma faixa própria. Para hierarquias com mais de um ancestral, `PageNavigation` recebe o
|
|
17
|
+
`Breadcrumb` na mesma posição introdutória e não pode ser combinado com `PageBack`. Ações em barra
|
|
18
|
+
usam a escala compacta: `sm` com texto e `icon-sm` quando exibem somente um ícone.
|
|
19
|
+
|
|
20
|
+
`PageState` passa a centralizar carregamento, falha e vazio sem moldura e a ocupar a altura
|
|
21
|
+
disponível. `Page` reconhece o estado mesmo quando um componente intermediário o renderiza, oculta
|
|
22
|
+
o cabeçalho inteiro nos três estados e o restaura em `ready`. A falha mantém `role="alert"`, deixa
|
|
23
|
+
de usar o visual de `Alert` e apresenta a recuperação como botão `outline` textual. A moldura tracejada continua disponível no `Empty` usado diretamente para
|
|
24
|
+
representar uma região disponível à criação ou vínculo.
|
|
25
|
+
|
|
26
|
+
O CLI carrega o `.env` ao lado do `opus.config.ts` antes de importar o config, em `db`, `seed` e
|
|
27
|
+
`gen`. O config do consumer costuma abrir o banco no topo do módulo, lendo `process.env` na hora
|
|
28
|
+
do import: sem isso, um projeto que guarda a URL no `.env` migrava o banco DEFAULT do config
|
|
29
|
+
enquanto o runtime usava outro, e o schema aplicado sumia do banco que a aplicação abre. Variável
|
|
30
|
+
já definida no ambiente vence o arquivo (a semântica do `--env-file` do Node), então
|
|
31
|
+
`DATABASE_URL=... opus db migrate` e a injeção do container seguem mandando; ausência de `.env`
|
|
32
|
+
é normal e não é erro. Um projeto que já carregava o `.env` por conta própria no `opus.config.ts`
|
|
33
|
+
continua correto: as duas cargas são idempotentes e a primeira vence.
|
|
34
|
+
|
|
35
|
+
**Breaking — API removida.** `Page` deixa de aceitar `count` e o barrel deixa de exportar
|
|
36
|
+
`PageMeta` (ADR 0009). O cabeçalho da página reconhece somente `PageTitle`, `PageDescription`,
|
|
37
|
+
`PageActions` e a região introdutória; um total vai para o conteúdo que o explica — a listagem, a
|
|
38
|
+
métrica ou uma seção com `Content`, onde `ContentMeta` e `Content.count` continuam disponíveis.
|
|
39
|
+
`PageBack` passa a exigir `href`: sem destino, o elemento não era tabulável nem tinha nome
|
|
40
|
+
acessível. `PageNavigation` e `PageBack` não podem ser combinados na mesma página: escolha o
|
|
41
|
+
retorno de um ancestral só ou a trilha completa.
|
|
42
|
+
|
|
43
|
+
**Breaking — comportamento e DOM.** `Page` oculta o `PageHeader` inteiro enquanto o `PageState`
|
|
44
|
+
estiver em carregamento, erro ou vazio, sem opt-out. Teste que procura o título da página durante a
|
|
45
|
+
carga passa a falhar; o nível semântico segue disponível no `role="heading"` da própria superfície
|
|
46
|
+
de estado. Some junto a região introdutória, então uma subpágina em erro fica sem o `PageBack` para
|
|
47
|
+
o pai; quem depende desse retorno oferece a saída pelo `action` do próprio `PageState`. O motivo, a
|
|
48
|
+
alternativa descartada e esse custo estão no adendo da ADR 0004. O erro e o vazio do `PageState`
|
|
49
|
+
trocam de anatomia: sai `[data-slot="alert"]` e `[data-slot="alert-actions"]`, entram `error-state`
|
|
50
|
+
e `state-actions`, e o vazio passa de `data-frame="region"` para `bare`. As ações de linha do
|
|
51
|
+
`ActionList` passam a vir dentro de um `ButtonGroup`, o que acrescenta um `role="group"` por linha;
|
|
52
|
+
as ações da barra (Filtros, Recarregar, Exibição) passaram de `outline` para `ghost`, e o que o
|
|
53
|
+
consumidor entrega em `ActionFilterBar.actions` agora fica depois do grupo de controles auxiliares,
|
|
54
|
+
no fim da barra. A recuperação do aviso inline de erro virou ação só de ícone, com o rótulo no nome
|
|
55
|
+
acessível e no tooltip em vez do texto na tela. Quem afirma texto, variante ou slot nesses pontos
|
|
56
|
+
precisa revisar as asserções.
|
|
57
|
+
|
|
58
|
+
**Breaking — ícones voltam à escala.** `controlGlyph` passa a devolver seletores literais. Montada
|
|
59
|
+
em runtime, a classe saía certa no DOM, mas o scanner do Tailwind não a via e o CSS nunca era
|
|
60
|
+
gerado: na prática, todo ícone de `Button` sem `size-*` próprio vinha no tamanho padrão do lucide.
|
|
61
|
+
Agora todos passam a medir o degrau da escala, então ícones encolhem visivelmente no consumidor. O
|
|
62
|
+
`ActionTrigger` só de ícone usa `icon-xs` (1.5rem) por padrão, o degrau da ação que mora dentro de
|
|
63
|
+
uma linha densa, e passa a respeitar um `size` declarado quando a composição pede mais presença —
|
|
64
|
+
`icon-sm` na barra, por exemplo. Atenção ao inverso: um `size` que antes era ignorado no modo
|
|
65
|
+
ícone passa a valer, então `size="lg"` esquecido ao lado de `icon` agora produz um botão de altura
|
|
66
|
+
de texto com um ícone só.
|
|
67
|
+
|
|
68
|
+
Do lado aditivo, `ButtonGroup` ganha `mode`: `connected`, o comportamento de sempre com as bordas
|
|
69
|
+
coladas, e `spaced`, que só agrupa e espaça — é o que as ações de linha e os controles auxiliares
|
|
70
|
+
da barra do `ActionList` passam a usar. `ActionList` ganha `toolbarActions` para o que o consumidor
|
|
71
|
+
acrescenta no fim da barra de filtros.
|
|
72
|
+
|
|
73
|
+
## 14.0.0 — 2026-09-09
|
|
74
|
+
|
|
75
|
+
Estados vazio, carregando e erro passam a uma composição só. `DataState` compõe `Empty` (moldura
|
|
76
|
+
sólida, `bare` dentro de tabela) e o mesmo `Alert` de contexto `danger` com botão de recuperação
|
|
77
|
+
que `PageState` já usava; o `Spinner` vira decorativo e o contêiner carrega o único `role="status"`.
|
|
78
|
+
As props de estado têm um nome só em `DataState`, `PageState`, `ActionList` e `ActionListDialog`:
|
|
79
|
+
`emptyMessage`, `errorMessage`, `retryLabel`; `PageState` ganha `onRetry` e `ActionView` ganha
|
|
80
|
+
`emptyMessage` como atalho. `ContentHeader` e `PageHeader` renderizam a mesma anatomia
|
|
81
|
+
(`surface-header.tsx`), e `Content` passa a usar `count`, como `Page`.
|
|
82
|
+
|
|
83
|
+
A escala de `size` é uma só (`primitives/control.ts`): `xs`, `sm`, `default`, `lg` (1.5, 2, 2.25
|
|
84
|
+
e 2.5rem) e `icon-xs`, `icon-sm`, `icon`, `icon-lg` (1.5, 1.75, 2.25 e 2.5rem), consumida por
|
|
85
|
+
Button, InputGroupButton, Toggle, Select, Tabs, Switch, Item, Avatar, Spinner, Pagination e
|
|
86
|
+
ActionTrigger. O degrau de 1.75rem que os consumidores forçavam por classe agora é `icon-sm`.
|
|
87
|
+
`Button.icon` recebe um nó (`icon={<Plus />}`), como todos os outros componentes. `LabelHelp` é
|
|
88
|
+
exportado pelo barrel e documentado em `form.md`.
|
|
89
|
+
|
|
90
|
+
O extrator de copy alcança o que antes escapava do inventário: chamadas imperativas de diálogo
|
|
91
|
+
(`dialog.confirm`, `alert`, `prompt`, `choose` — título, descrição e rótulos das ações),
|
|
92
|
+
`TooltipContent`, rótulos em arrays literais locais percorridos por `map`, o `help` de
|
|
93
|
+
`LabelHelp` e elementos JSX com genérico (`<ActionList<A, B>>`). Os campos de estado seguem os
|
|
94
|
+
nomes novos (`emptyMessage`, `errorMessage`, `retryLabel`).
|
|
95
|
+
|
|
96
|
+
**Breaking:** renomeie `emptyText`, `errorText` e `retryText` para `emptyMessage`,
|
|
97
|
+
`errorMessage` e `retryLabel` em `DataState`, `PageState` e `ActionListDialog`
|
|
98
|
+
(`Select.emptyText` não muda). Troque `Content.meta` por `count`; `ContentHeader` só existe
|
|
99
|
+
dentro de `Content`. `icon-sm` passa a medir 1.75rem e `Avatar` segue a escala (`sm` antigo é
|
|
100
|
+
`xs`; `default` cresce para 2.25rem); classes `size-7` forçadas viram `size="icon-sm"`.
|
|
101
|
+
`Button.icon` deixa de aceitar componente: `icon={Trash}` vira `icon={<Trash />}`. Removidos:
|
|
102
|
+
`TbdlibProvider` (use `OpusProvider`), `ShellNav*` (use `SidebarNav`), `confirm()` e `ConfirmHost`
|
|
103
|
+
(use `dialog.confirm` e `DialogHost`), `FieldSpec.hint` (use `help`) e as variantes semânticas
|
|
104
|
+
legadas de Button, Badge, Alert, Dot e MenuItem — declare `context` e `variant`. Testes que
|
|
105
|
+
liam o markup do vazio de `DataState` passam a olhar `[data-slot=empty]`.
|
|
106
|
+
|
|
10
107
|
## 13.1.0 — 2026-09-08
|
|
11
108
|
|
|
12
109
|
Erros deixam de falar inglês e de vazar detalhe interno. `normalizeError` devolve a microcopy fixa
|
package/bin/cli.mjs
CHANGED
|
@@ -328,6 +328,8 @@ Flags
|
|
|
328
328
|
--json (introspect, seed) Saída estruturada em JSON.
|
|
329
329
|
--monorepo (create) Cria a raiz do workspace em vez de um app.
|
|
330
330
|
--config <path> (gen, db, seed) Caminho do opus.config.ts. Default: ./opus.config.ts.
|
|
331
|
+
O .env ao lado desse arquivo é carregado antes do config; variável
|
|
332
|
+
já definida no ambiente vence o arquivo.
|
|
331
333
|
--output <path> (gen) Pasta de saída. Default: a do config, ou ./.gen.
|
|
332
334
|
--profile <nome> (seed) Perfil do dataset. Default: o defaultProfile do seed.
|
|
333
335
|
--scope <nome> (seed) Escopo explícito dos dados. Alternativa: OPUS_SEED_SCOPE.
|
package/bin/lib/check.mjs
CHANGED
|
@@ -107,8 +107,9 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
107
107
|
["PageHeader", new Set(["Page"])],
|
|
108
108
|
["PageBody", new Set(["Page"])],
|
|
109
109
|
["PageTitle", new Set(["PageHeader"])],
|
|
110
|
+
["PageBack", new Set(["PageHeader"])],
|
|
111
|
+
["PageNavigation", new Set(["PageHeader"])],
|
|
110
112
|
["PageDescription", new Set(["PageHeader"])],
|
|
111
|
-
["PageMeta", new Set(["PageHeader"])],
|
|
112
113
|
["PageActions", new Set(["PageHeader"])],
|
|
113
114
|
["ContentHeader", new Set(["Content"])],
|
|
114
115
|
["ContentBody", new Set(["Content"])],
|
|
@@ -166,7 +167,11 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
166
167
|
{
|
|
167
168
|
header: "PageHeader",
|
|
168
169
|
body: "PageBody",
|
|
169
|
-
shorthand: new Set(["title", "description", "
|
|
170
|
+
shorthand: new Set(["title", "description", "actions"]),
|
|
171
|
+
// O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
|
|
172
|
+
// nada sozinho — só apaga o marcador que fazia o gate enxergar aquele Page —, então a
|
|
173
|
+
// prop removida é cobrada aqui, nas duas formas de composição.
|
|
174
|
+
removed: new Map([["count", "o total pertence ao conteúdo que o explica"]]),
|
|
170
175
|
},
|
|
171
176
|
],
|
|
172
177
|
[
|
|
@@ -174,7 +179,8 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
174
179
|
{
|
|
175
180
|
header: "ContentHeader",
|
|
176
181
|
body: "ContentBody",
|
|
177
|
-
shorthand: new Set(["title", "description", "
|
|
182
|
+
shorthand: new Set(["title", "description", "count", "actions"]),
|
|
183
|
+
removed: new Map([["meta", "use `count`"]]),
|
|
178
184
|
},
|
|
179
185
|
],
|
|
180
186
|
]);
|
|
@@ -183,8 +189,9 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
|
183
189
|
"PageHeader",
|
|
184
190
|
"PageBody",
|
|
185
191
|
"PageTitle",
|
|
192
|
+
"PageBack",
|
|
193
|
+
"PageNavigation",
|
|
186
194
|
"PageDescription",
|
|
187
|
-
"PageMeta",
|
|
188
195
|
"PageActions",
|
|
189
196
|
"ContentHeader",
|
|
190
197
|
"ContentBody",
|
|
@@ -202,7 +209,13 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
202
209
|
"PageHeader",
|
|
203
210
|
{
|
|
204
211
|
title: "PageTitle",
|
|
205
|
-
optional: new Set([
|
|
212
|
+
optional: new Set([
|
|
213
|
+
"PageBack",
|
|
214
|
+
"PageNavigation",
|
|
215
|
+
"PageDescription",
|
|
216
|
+
"PageActions",
|
|
217
|
+
]),
|
|
218
|
+
exclusive: [new Set(["PageBack", "PageNavigation"])],
|
|
206
219
|
shorthand: new Set(),
|
|
207
220
|
},
|
|
208
221
|
],
|
|
@@ -898,6 +911,10 @@ export function checkUiStructure(file, text) {
|
|
|
898
911
|
(name) =>
|
|
899
912
|
children.names.filter((child) => child === name).length > 1,
|
|
900
913
|
);
|
|
914
|
+
const combinedExclusive = (header.exclusive ?? []).some(
|
|
915
|
+
(group) =>
|
|
916
|
+
children.names.filter((name) => group.has(name)).length > 1,
|
|
917
|
+
);
|
|
901
918
|
const titleValid =
|
|
902
919
|
header.titleRequired === false ? titles <= 1 : titles === 1;
|
|
903
920
|
const recognizedContent =
|
|
@@ -910,6 +927,7 @@ export function checkUiStructure(file, text) {
|
|
|
910
927
|
!(header.allowOpaque && children.hasOpaque)) ||
|
|
911
928
|
unexpected ||
|
|
912
929
|
duplicatedOptional ||
|
|
930
|
+
combinedExclusive ||
|
|
913
931
|
(children.hasOpaque && !header.allowOpaque)
|
|
914
932
|
) {
|
|
915
933
|
add(
|
|
@@ -932,6 +950,16 @@ export function checkUiStructure(file, text) {
|
|
|
932
950
|
: [],
|
|
933
951
|
),
|
|
934
952
|
);
|
|
953
|
+
for (const [attribute, hint] of root.removed ?? []) {
|
|
954
|
+
if (attributes.has(attribute)) {
|
|
955
|
+
add(
|
|
956
|
+
node,
|
|
957
|
+
component,
|
|
958
|
+
`${component} não aceita mais a propriedade \`${attribute}\`: ${hint}.`,
|
|
959
|
+
"ui-structure-removed-prop",
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
935
963
|
const shorthand = [...root.shorthand].some((attribute) =>
|
|
936
964
|
attributes.has(attribute),
|
|
937
965
|
);
|
package/bin/lib/cli-shared.mjs
CHANGED
|
@@ -55,10 +55,39 @@ export async function fileExists(file) {
|
|
|
55
55
|
// Config do consumer
|
|
56
56
|
// =============================================================================
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Carrega o `.env` ao lado do `opus.config.ts` no processo do CLI, para que ele chegue
|
|
60
|
+
* aos runners (que herdam o ambiente) ANTES de o config do consumer ser importado.
|
|
61
|
+
*
|
|
62
|
+
* O config costuma abrir a conexão do banco no topo do módulo, lendo `process.env` na
|
|
63
|
+
* hora do import: sem isto, `db` e `seed` caem na conexão default e operam um banco
|
|
64
|
+
* diferente do que o runtime do projeto usa. Ausência do arquivo é normal (container e
|
|
65
|
+
* CI injetam por ambiente) e não é erro; qualquer outra falha de leitura sobe.
|
|
66
|
+
*
|
|
67
|
+
* Variável já definida no ambiente VENCE o arquivo (semântica do `--env-file` do Node),
|
|
68
|
+
* então `DATABASE_URL=... opus db migrate` e a injeção do container continuam mandando.
|
|
69
|
+
*
|
|
70
|
+
* Devolve o caminho carregado, ou `null` quando não havia `.env`.
|
|
71
|
+
*/
|
|
72
|
+
export function loadProjectEnv(configPath) {
|
|
73
|
+
const envFile = path.join(path.dirname(configPath), '.env')
|
|
74
|
+
try {
|
|
75
|
+
process.loadEnvFile(envFile)
|
|
76
|
+
return envFile
|
|
77
|
+
} catch (cause) {
|
|
78
|
+
if (cause.code === 'ENOENT') return null
|
|
79
|
+
throw cause
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
/**
|
|
59
84
|
* Resolve o `opus.config.ts` a partir de `flags.config` (default: `./opus.config.ts`),
|
|
60
85
|
* contido no projeto. Lança `Error` com uma mensagem única quando o arquivo não existe;
|
|
61
86
|
* cada comando decide o canal (texto ou JSON) e o código de saída.
|
|
87
|
+
*
|
|
88
|
+
* Resolver o config é também o momento em que o projeto passa a ser conhecido, então o
|
|
89
|
+
* `.env` dele é carregado aqui (ver `loadProjectEnv`) — em um lugar só, para que nenhum
|
|
90
|
+
* comando que importe o config do consumer possa esquecer.
|
|
62
91
|
*/
|
|
63
92
|
export function resolveConfig(flags, cwd = process.cwd()) {
|
|
64
93
|
const root = canonicalProjectDirectory(cwd)
|
|
@@ -69,7 +98,7 @@ export function resolveConfig(flags, cwd = process.cwd()) {
|
|
|
69
98
|
'Passe o caminho com --config <path> ou crie o arquivo na raiz do projeto.',
|
|
70
99
|
)
|
|
71
100
|
}
|
|
72
|
-
return { cwd: root, configPath: config.path }
|
|
101
|
+
return { cwd: root, configPath: config.path, envFile: loadProjectEnv(config.path) }
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
// =============================================================================
|
package/bin/lib/copy.mjs
CHANGED
|
@@ -103,12 +103,23 @@ const JSX_CHILD_ROLES = new Map([
|
|
|
103
103
|
['PopoverDescription', 'description'],
|
|
104
104
|
['PopoverTitle', 'title'],
|
|
105
105
|
['PageDescription', 'description'],
|
|
106
|
+
// O texto do PageBack é o NOME do destino ('Clientes'), como um item de trilha — não um
|
|
107
|
+
// comando. Classificado como `button`, a política universal cobraria verbo de ação.
|
|
108
|
+
['PageBack', 'breadcrumb'],
|
|
106
109
|
['PageTitle', 'title'],
|
|
107
110
|
['TableCaption', 'description'],
|
|
108
111
|
['TableHead', 'heading'],
|
|
109
112
|
['TabsTrigger', 'tab'],
|
|
113
|
+
// A Base não tem papel `tooltip`. O tooltip nomeia um controle icon-only, logo é um
|
|
114
|
+
// fragmento (sem ponto final), não uma frase; `label` é o papel que reflete isso.
|
|
115
|
+
['TooltipContent', 'label'],
|
|
110
116
|
])
|
|
111
117
|
|
|
118
|
+
// Componentes cuja copy entra pelas próprias props e que, como filhos, não acrescentam
|
|
119
|
+
// texto ao pai: `<FieldLabel>Nome<LabelHelp help="…" /></FieldLabel>` inventaria `Nome`
|
|
120
|
+
// como label e a ajuda como helper-text, sem tornar o filho opaco.
|
|
121
|
+
const SELF_CONTAINED_CHILD_COMPONENTS = new Set(['LabelHelp'])
|
|
122
|
+
|
|
112
123
|
const OPTIONAL_CHILD_COMPONENTS = new Set(['ActionForm', 'ActionFormCard', 'ActionFormDialog'])
|
|
113
124
|
const JSX_ACTION_CONSUMERS = new Set([
|
|
114
125
|
'ActionForm', 'ActionFormCard', 'ActionFormDialog', 'ActionList', 'ActionListDialog', 'ActionTrigger', 'ActionView',
|
|
@@ -125,6 +136,14 @@ const JSX_PROP_ROLES = new Map([
|
|
|
125
136
|
['ActionForm', new Map([['submitLabel', 'button'], ['cancelLabel', 'button']])],
|
|
126
137
|
['ActionFormCard', new Map([['title', 'title'], ['description', 'description'], ['submitLabel', 'button'], ['cancelLabel', 'button']])],
|
|
127
138
|
['ActionFormDialog', new Map([['title', 'title'], ['description', 'dialog-body'], ['submitLabel', 'button'], ['cancelLabel', 'button']])],
|
|
139
|
+
// Estados unificados (14.0): a mesma tríade de mensagens em ActionList, ActionListDialog,
|
|
140
|
+
// DataState e PageState; ActionView só tem o vazio.
|
|
141
|
+
['ActionList', new Map([['emptyMessage', 'empty-state'], ['errorMessage', 'error'], ['retryLabel', 'button']])],
|
|
142
|
+
['ActionListDialog', new Map([
|
|
143
|
+
['title', 'title'], ['description', 'dialog-body'],
|
|
144
|
+
['emptyMessage', 'empty-state'], ['errorMessage', 'error'], ['retryLabel', 'button'],
|
|
145
|
+
])],
|
|
146
|
+
['ActionView', new Map([['emptyMessage', 'empty-state']])],
|
|
128
147
|
// `itemLabel` identifica o dado alvo (por exemplo, `customer.name`); não é
|
|
129
148
|
// microcopy estável e, por isso, não pertence ao inventário editorial.
|
|
130
149
|
['ActionTrigger', new Map([['label', 'button']])],
|
|
@@ -132,7 +151,7 @@ const JSX_PROP_ROLES = new Map([
|
|
|
132
151
|
['CommandInput', new Map([['placeholder', 'placeholder']])],
|
|
133
152
|
['Content', new Map([['title', 'title'], ['description', 'description']])],
|
|
134
153
|
['ContentHeader', new Map([['title', 'title'], ['description', 'description']])],
|
|
135
|
-
['DataState', new Map([['
|
|
154
|
+
['DataState', new Map([['emptyMessage', 'empty-state'], ['errorMessage', 'error'], ['retryLabel', 'button']])],
|
|
136
155
|
// A Dock nomeia a barra e cada ação por prop. Sem estas linhas, a copy sairia do inventário
|
|
137
156
|
// exatamente quando uma superfície migra de <Button aria-label> para <DockAction label>.
|
|
138
157
|
['Dock', new Map([['label', 'label']])],
|
|
@@ -140,8 +159,14 @@ const JSX_PROP_ROLES = new Map([
|
|
|
140
159
|
['Input', new Map([['placeholder', 'placeholder']])],
|
|
141
160
|
['InputGroupInput', new Map([['placeholder', 'placeholder']])],
|
|
142
161
|
['InputGroupTextarea', new Map([['placeholder', 'placeholder']])],
|
|
162
|
+
// A ajuda vive num tooltip (portal): não herda transformação nem classe do label.
|
|
163
|
+
['LabelHelp', new Map([['help', 'helper-text']])],
|
|
143
164
|
['MetricCard', new Map([['label', 'label'], ['description', 'description']])],
|
|
144
165
|
['Page', new Map([['title', 'title'], ['description', 'description']])],
|
|
166
|
+
['PageState', new Map([
|
|
167
|
+
['title', 'title'], ['description', 'description'],
|
|
168
|
+
['emptyMessage', 'empty-state'], ['errorMessage', 'error'], ['retryLabel', 'button'],
|
|
169
|
+
])],
|
|
145
170
|
['Select', new Map([
|
|
146
171
|
['placeholder', 'placeholder'],
|
|
147
172
|
['searchPlaceholder', 'placeholder'],
|
|
@@ -215,6 +240,36 @@ const JSX_PORTAL_BOUNDARIES = new Set([
|
|
|
215
240
|
'PopoverContent', 'TooltipContent',
|
|
216
241
|
])
|
|
217
242
|
|
|
243
|
+
// Respostas imperativas `dialog.*` da UI Opus. O DialogHost renderiza em portal com classes
|
|
244
|
+
// próprias; nenhuma transformação do chamador alcança o texto.
|
|
245
|
+
const DIALOG_METHOD_ROLES = new Map([
|
|
246
|
+
['alert', new Map([['title', 'title'], ['description', 'dialog-body'], ['body', 'dialog-body'], ['action', 'button']])],
|
|
247
|
+
['confirm', new Map([
|
|
248
|
+
['title', 'title'], ['description', 'dialog-body'], ['body', 'dialog-body'],
|
|
249
|
+
['action', 'button'], ['cancel', 'button'],
|
|
250
|
+
])],
|
|
251
|
+
['prompt', new Map([
|
|
252
|
+
['title', 'title'], ['description', 'dialog-body'], ['body', 'dialog-body'],
|
|
253
|
+
['action', 'button'], ['cancel', 'button'], ['placeholder', 'placeholder'],
|
|
254
|
+
])],
|
|
255
|
+
['choose', new Map([['title', 'title'], ['description', 'dialog-body'], ['body', 'dialog-body']])],
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
// Métodos de array que não mutam o receptor. Os que devolvem os próprios elementos (num
|
|
259
|
+
// array novo ou avulsos) exigem que o resultado também seja usado de forma estável; os
|
|
260
|
+
// demais só produzem primitivos, novos objetos ou nada.
|
|
261
|
+
const ARRAY_RETURNING_ARRAY_METHODS = new Set(['concat', 'filter', 'flat', 'slice', 'toReversed', 'toSorted'])
|
|
262
|
+
const ELEMENT_PICKING_ARRAY_METHODS = new Set(['at', 'find', 'findLast'])
|
|
263
|
+
const ELEMENT_ITERATING_ARRAY_METHODS = new Set([
|
|
264
|
+
'every', 'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'flatMap', 'forEach', 'map', 'some',
|
|
265
|
+
])
|
|
266
|
+
const READ_ONLY_ARRAY_METHODS = new Set([
|
|
267
|
+
...ARRAY_RETURNING_ARRAY_METHODS, ...ELEMENT_PICKING_ARRAY_METHODS, ...ELEMENT_ITERATING_ARRAY_METHODS,
|
|
268
|
+
'includes', 'indexOf', 'join', 'lastIndexOf',
|
|
269
|
+
])
|
|
270
|
+
// Cadeia que preserva os elementos entre o array literal e a iteração que os lê.
|
|
271
|
+
const ELEMENT_PRESERVING_ARRAY_METHODS = new Set(['filter', 'slice', 'toReversed', 'toSorted'])
|
|
272
|
+
|
|
218
273
|
const digest = (content) => createHash('sha256').update(content).digest('hex')
|
|
219
274
|
const portable = (value) => value.split(path.sep).join('/')
|
|
220
275
|
|
|
@@ -421,6 +476,17 @@ function knownBindAction(call, checker) {
|
|
|
421
476
|
return importedMemberName(call.expression, checker, (module) => CONTRACT_MODULES.has(module)) === 'bindAction'
|
|
422
477
|
}
|
|
423
478
|
|
|
479
|
+
/** Reconhece `dialog.confirm({...})` e `Opus.dialog.alert({...})` importados da UI Opus. */
|
|
480
|
+
function knownDialogCall(call, checker) {
|
|
481
|
+
const callee = unwrap(call.expression)
|
|
482
|
+
if (
|
|
483
|
+
ts.isPropertyAccessExpression(callee) &&
|
|
484
|
+
DIALOG_METHOD_ROLES.has(callee.name.text) &&
|
|
485
|
+
importedMemberName(callee.expression, checker, uiModule) === 'dialog'
|
|
486
|
+
) return { method: callee.name.text, field: `dialog.${callee.name.text}()` }
|
|
487
|
+
return null
|
|
488
|
+
}
|
|
489
|
+
|
|
424
490
|
function safeBindActionBinding(call, checker) {
|
|
425
491
|
if (call.arguments.length < 2) return false
|
|
426
492
|
const binding = unwrap(call.arguments[1])
|
|
@@ -521,6 +587,16 @@ function stableExpressionUse(node, checker, mode, stack) {
|
|
|
521
587
|
const parent = carrier.parent
|
|
522
588
|
|
|
523
589
|
if (assignmentTarget(carrier)) return false
|
|
590
|
+
if (consumerMode === 'aggregate' && ts.isCallExpression(parent) && parent.expression === carrier) {
|
|
591
|
+
const access = unwrap(carrier)
|
|
592
|
+
return (
|
|
593
|
+
ts.isPropertyAccessExpression(access) && arrayValued(access.expression, checker) &&
|
|
594
|
+
readOnlyArrayMethodUse(access.name.text, parent, checker, stack)
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
if (ts.isForOfStatement(parent) && parent.expression === carrier) {
|
|
598
|
+
return consumerMode === 'aggregate' && arrayValued(carrier, checker) && readOnlyForOfBinding(parent, checker)
|
|
599
|
+
}
|
|
524
600
|
if (ts.isVariableDeclaration(parent) && parent.initializer === carrier) {
|
|
525
601
|
if (!ts.isVariableDeclarationList(parent.parent) || (parent.parent.flags & ts.NodeFlags.Const) === 0) return false
|
|
526
602
|
const aliases = bindingNames(parent.name, checker)
|
|
@@ -534,6 +610,8 @@ function stableExpressionUse(node, checker, mode, stack) {
|
|
|
534
610
|
if (ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === carrier)) {
|
|
535
611
|
if (consumerMode === 'aggregate') {
|
|
536
612
|
if (knownDictionaryFactory(parent, checker)) return true
|
|
613
|
+
// O DialogHost copia as opções (`{ ...options }`) e só as lê; nada muta o literal.
|
|
614
|
+
if (parent.arguments[0] === carrier && knownDialogCall(parent, checker) !== null) return true
|
|
537
615
|
return knownContractFactory(parent, checker) && stableFactoryResult(parent, checker, stack)
|
|
538
616
|
}
|
|
539
617
|
if (
|
|
@@ -573,6 +651,166 @@ function stableReferences(binding, checker, mode, stack) {
|
|
|
573
651
|
return stable
|
|
574
652
|
}
|
|
575
653
|
|
|
654
|
+
/**
|
|
655
|
+
* Um parâmetro de callback (ou variável de `for…of`) é somente leitura quando cada uso é
|
|
656
|
+
* `elemento.chave` fora de posição de atribuição. Strings são imutáveis, então uma leitura de
|
|
657
|
+
* propriedade nunca altera o rótulo literal; qualquer outro uso (spread, argumento, retorno,
|
|
658
|
+
* atribuição) poderia mutar ou fazer o objeto escapar e é rejeitado. Destructuring copia os
|
|
659
|
+
* valores de primeiro nível e, por isso, também não alcança o literal.
|
|
660
|
+
*/
|
|
661
|
+
function readOnlyElementBinding(name, checker) {
|
|
662
|
+
if (ts.isObjectBindingPattern(name)) return true
|
|
663
|
+
if (!ts.isIdentifier(name)) return false
|
|
664
|
+
const symbol = bindingSymbol(name, checker)
|
|
665
|
+
if (symbol === undefined) return false
|
|
666
|
+
let readOnly = true
|
|
667
|
+
const visit = (node) => {
|
|
668
|
+
if (!readOnly) return
|
|
669
|
+
if (ts.isIdentifier(node) && sameBinding(node, symbol, checker) && !declarationIdentifier(node, symbol)) {
|
|
670
|
+
const carrier = transparentCarrier(node)
|
|
671
|
+
const access = carrier.parent
|
|
672
|
+
readOnly =
|
|
673
|
+
(ts.isPropertyAccessExpression(access) || ts.isElementAccessExpression(access)) &&
|
|
674
|
+
access.expression === carrier && !assignmentTarget(access)
|
|
675
|
+
}
|
|
676
|
+
ts.forEachChild(node, visit)
|
|
677
|
+
}
|
|
678
|
+
visit(name.getSourceFile())
|
|
679
|
+
return readOnly
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function readOnlyCallback(callback, checker) {
|
|
683
|
+
const value = callback === undefined ? null : unwrap(callback)
|
|
684
|
+
if (value === null || (!ts.isArrowFunction(value) && !ts.isFunctionExpression(value))) return false
|
|
685
|
+
// O terceiro parâmetro expõe o próprio array ao callback; índice e `thisArg` não.
|
|
686
|
+
if (value.parameters.length > 2) return false
|
|
687
|
+
const element = value.parameters[0]
|
|
688
|
+
if (element === undefined) return true
|
|
689
|
+
if (element.dotDotDotToken !== undefined || element.initializer !== undefined) return false
|
|
690
|
+
return readOnlyElementBinding(element.name, checker)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function readOnlyArrayMethodUse(method, call, checker, stack) {
|
|
694
|
+
if (!READ_ONLY_ARRAY_METHODS.has(method)) return false
|
|
695
|
+
if (ELEMENT_ITERATING_ARRAY_METHODS.has(method) && !readOnlyCallback(call.arguments[0], checker)) return false
|
|
696
|
+
if (ARRAY_RETURNING_ARRAY_METHODS.has(method) || ELEMENT_PICKING_ARRAY_METHODS.has(method)) {
|
|
697
|
+
return stableExpressionUse(call, checker, 'aggregate', stack)
|
|
698
|
+
}
|
|
699
|
+
return true
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* A semântica somente leitura de `map`, `filter`… vale para arrays. Um objeto literal pode
|
|
704
|
+
* declarar um método com o mesmo nome e mutar o que quiser; por isso o receptor precisa ser
|
|
705
|
+
* um `const` com array literal ou o resultado de um método que devolve array sobre ele.
|
|
706
|
+
*/
|
|
707
|
+
function arrayValued(node, checker) {
|
|
708
|
+
const value = unwrap(node)
|
|
709
|
+
if (ts.isIdentifier(value)) {
|
|
710
|
+
const binding = constInitializer(value, checker)
|
|
711
|
+
return binding !== null && ts.isArrayLiteralExpression(unwrap(binding.initializer))
|
|
712
|
+
}
|
|
713
|
+
if (ts.isCallExpression(value)) {
|
|
714
|
+
const callee = unwrap(value.expression)
|
|
715
|
+
return ts.isPropertyAccessExpression(callee) && ARRAY_RETURNING_ARRAY_METHODS.has(callee.name.text) &&
|
|
716
|
+
arrayValued(callee.expression, checker)
|
|
717
|
+
}
|
|
718
|
+
return ts.isArrayLiteralExpression(value)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function readOnlyForOfBinding(statement, checker) {
|
|
722
|
+
const initializer = statement.initializer
|
|
723
|
+
if (!ts.isVariableDeclarationList(initializer) || initializer.declarations.length !== 1) return false
|
|
724
|
+
return readOnlyElementBinding(initializer.declarations[0].name, checker)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Localiza o array literal do qual um identificador é elemento: parâmetro de callback de
|
|
729
|
+
* `map`/`forEach`/`filter`… ou variável de `for…of`, possivelmente atrás de `filter`,
|
|
730
|
+
* `slice`, `toSorted` e `toReversed`, que preservam os elementos. Só arrays literais no
|
|
731
|
+
* mesmo arquivo entram; qualquer outra origem devolve null.
|
|
732
|
+
*/
|
|
733
|
+
function elementSource(identifier, checker) {
|
|
734
|
+
const symbol = bindingSymbol(identifier, checker)
|
|
735
|
+
const declaration = symbol?.declarations?.find(
|
|
736
|
+
(item) => ts.isParameter(item) || ts.isBindingElement(item) || ts.isVariableDeclaration(item),
|
|
737
|
+
)
|
|
738
|
+
if (declaration === undefined) return null
|
|
739
|
+
|
|
740
|
+
let key = null
|
|
741
|
+
let binding = declaration
|
|
742
|
+
if (ts.isBindingElement(declaration)) {
|
|
743
|
+
if (declaration.dotDotDotToken !== undefined || declaration.initializer !== undefined) return null
|
|
744
|
+
const pattern = declaration.parent
|
|
745
|
+
if (!ts.isObjectBindingPattern(pattern) || (!ts.isParameter(pattern.parent) && !ts.isVariableDeclaration(pattern.parent))) return null
|
|
746
|
+
const keyNode = declaration.propertyName ?? declaration.name
|
|
747
|
+
if (!ts.isIdentifier(keyNode) && !ts.isStringLiteral(keyNode)) return null
|
|
748
|
+
key = keyNode.text
|
|
749
|
+
binding = pattern.parent
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
let receiver
|
|
753
|
+
if (ts.isParameter(binding)) {
|
|
754
|
+
if (binding.dotDotDotToken !== undefined || binding.initializer !== undefined) return null
|
|
755
|
+
const callback = binding.parent
|
|
756
|
+
if ((!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || callback.parameters[0] !== binding) return null
|
|
757
|
+
const call = transparentCarrier(callback).parent
|
|
758
|
+
if (!ts.isCallExpression(call) || call.arguments[0] !== transparentCarrier(callback)) return null
|
|
759
|
+
const callee = unwrap(call.expression)
|
|
760
|
+
if (!ts.isPropertyAccessExpression(callee) || !ELEMENT_ITERATING_ARRAY_METHODS.has(callee.name.text)) return null
|
|
761
|
+
receiver = callee.expression
|
|
762
|
+
} else if (ts.isVariableDeclaration(binding)) {
|
|
763
|
+
const list = binding.parent
|
|
764
|
+
if (!ts.isVariableDeclarationList(list) || !ts.isForOfStatement(list.parent) || list.parent.initializer !== list) return null
|
|
765
|
+
receiver = list.parent.expression
|
|
766
|
+
} else return null
|
|
767
|
+
|
|
768
|
+
let source = unwrap(receiver)
|
|
769
|
+
while (
|
|
770
|
+
ts.isCallExpression(source) && ts.isPropertyAccessExpression(unwrap(source.expression)) &&
|
|
771
|
+
ELEMENT_PRESERVING_ARRAY_METHODS.has(unwrap(source.expression).name.text)
|
|
772
|
+
) source = unwrap(unwrap(source.expression).expression)
|
|
773
|
+
const array = resolveBinding(source, checker)
|
|
774
|
+
return ts.isArrayLiteralExpression(array) ? { array, key } : null
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Resolve `elemento.chave` (ou a chave destruturada) para o texto literal de cada elemento
|
|
779
|
+
* do array de origem. Um único elemento opaco ou sem texto estático torna o conjunto todo
|
|
780
|
+
* inextraível: o inventário não pode listar só parte do que a superfície renderiza.
|
|
781
|
+
*/
|
|
782
|
+
function elementTexts(node, checker) {
|
|
783
|
+
const value = unwrap(node)
|
|
784
|
+
let identifier
|
|
785
|
+
let key = null
|
|
786
|
+
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(unwrap(value.expression))) {
|
|
787
|
+
identifier = unwrap(value.expression)
|
|
788
|
+
key = value.name.text
|
|
789
|
+
} else if (ts.isElementAccessExpression(value) && ts.isIdentifier(unwrap(value.expression))) {
|
|
790
|
+
identifier = unwrap(value.expression)
|
|
791
|
+
key = value.argumentExpression === undefined ? null : staticText(value.argumentExpression, checker)?.text ?? null
|
|
792
|
+
} else if (ts.isIdentifier(value)) {
|
|
793
|
+
identifier = value
|
|
794
|
+
} else return null
|
|
795
|
+
const source = elementSource(identifier, checker)
|
|
796
|
+
if (source === null) return null
|
|
797
|
+
if (ts.isIdentifier(value)) key = source.key
|
|
798
|
+
else if (source.key !== null) return null
|
|
799
|
+
if (key === null) return null
|
|
800
|
+
|
|
801
|
+
const listed = objects(source.array, checker)
|
|
802
|
+
if (listed.opaque.length > 0 || listed.values.length === 0) return null
|
|
803
|
+
const texts = []
|
|
804
|
+
for (const element of listed.values) {
|
|
805
|
+
const resolved = property(element, key, element.getSourceFile(), checker)
|
|
806
|
+
if (resolved.opaque !== undefined || resolved.candidate === undefined) return null
|
|
807
|
+
const text = staticText(propertyValue(resolved.candidate), checker)
|
|
808
|
+
if (text === null) return null
|
|
809
|
+
texts.push(text)
|
|
810
|
+
}
|
|
811
|
+
return texts
|
|
812
|
+
}
|
|
813
|
+
|
|
576
814
|
/**
|
|
577
815
|
* Objetos e arrays `const` continuam mutáveis. A extração só faz fold quando cada uso
|
|
578
816
|
* está numa allowlist observável: composição `const` igualmente estável, factory Opus
|
|
@@ -982,7 +1220,6 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
982
1220
|
const field = (object, prefix) => {
|
|
983
1221
|
addProperty(object, 'label', 'label', prefix)
|
|
984
1222
|
addProperty(object, 'placeholder', 'placeholder', prefix)
|
|
985
|
-
addProperty(object, 'hint', 'helper-text', prefix)
|
|
986
1223
|
addProperty(object, 'help', 'helper-text', prefix)
|
|
987
1224
|
nestedObject(object, 'options', optionItems, prefix)
|
|
988
1225
|
}
|
|
@@ -1384,6 +1621,11 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1384
1621
|
? { kind: 'decorative' }
|
|
1385
1622
|
: { kind: 'text', ...text, uppercase: inheritedUppercase }
|
|
1386
1623
|
}
|
|
1624
|
+
const listed = elementTexts(value, checker)
|
|
1625
|
+
if (listed !== null) {
|
|
1626
|
+
const values = listed.map((item) => ({ kind: 'text', ...item, uppercase: inheritedUppercase }))
|
|
1627
|
+
return values.length === 1 ? values[0] : { kind: 'alternatives', values, node: values[0].node }
|
|
1628
|
+
}
|
|
1387
1629
|
if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) {
|
|
1388
1630
|
return childNode(value, inheritedUppercase, hiddenAncestor)
|
|
1389
1631
|
}
|
|
@@ -1439,6 +1681,9 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1439
1681
|
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
|
|
1440
1682
|
const opening = ts.isJsxElement(child) ? child.openingElement : child
|
|
1441
1683
|
const component = opusComponent(opening.tagName)
|
|
1684
|
+
if (component !== null && SELF_CONTAINED_CHILD_COMPONENTS.has(component) && ts.isJsxSelfClosingElement(child)) {
|
|
1685
|
+
return { kind: 'decorative' }
|
|
1686
|
+
}
|
|
1442
1687
|
const provenElement = transformableElement(opening, component)
|
|
1443
1688
|
const hidden = attribute(opening.attributes, 'aria-hidden')
|
|
1444
1689
|
if (hidden.opaque !== undefined) return { kind: 'opaque', node: hidden.opaque }
|
|
@@ -1593,7 +1838,10 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1593
1838
|
if (component === null) return
|
|
1594
1839
|
let inherited
|
|
1595
1840
|
const inheritedFor = (className, styleName, portal = false) => {
|
|
1596
|
-
|
|
1841
|
+
// Um componente que renderiza em portal (TooltipContent) não é descendente visual
|
|
1842
|
+
// dos ancestrais no JSX; a transformação deles não alcança o texto.
|
|
1843
|
+
if (portal || JSX_PORTAL_BOUNDARIES.has(component)) return false
|
|
1844
|
+
if (localTransformOverridesInheritance(opening.attributes, className, styleName)) return false
|
|
1597
1845
|
inherited ??= ancestorTransform(element)
|
|
1598
1846
|
return inherited.opaque ? false : inherited.uppercase
|
|
1599
1847
|
}
|
|
@@ -1676,7 +1924,11 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1676
1924
|
}
|
|
1677
1925
|
} else {
|
|
1678
1926
|
const value = raw === null ? null : attributeText(item)
|
|
1679
|
-
|
|
1927
|
+
// `title={group.label}` dentro de `GROUPS.map(...)`: cada literal do array local
|
|
1928
|
+
// vira um texto na própria linha, em vez de um diagnóstico sobre a prop.
|
|
1929
|
+
const listed = value === null && raw !== null ? elementTexts(raw, checker) : null
|
|
1930
|
+
const texts = value !== null ? [value] : listed ?? []
|
|
1931
|
+
if (texts.length === 0) {
|
|
1680
1932
|
diagnostics.push(diagnostic(
|
|
1681
1933
|
file,
|
|
1682
1934
|
sourceFile,
|
|
@@ -1685,8 +1937,8 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1685
1937
|
raw === null ? 'missing-content' : 'content',
|
|
1686
1938
|
))
|
|
1687
1939
|
}
|
|
1688
|
-
|
|
1689
|
-
const entry = { source: file, line: lineOf(sourceFile,
|
|
1940
|
+
for (const text of texts) {
|
|
1941
|
+
const entry = { source: file, line: lineOf(sourceFile, text.node), role, text: text.text }
|
|
1690
1942
|
if (transformed.uppercase) entry.transform = 'uppercase'
|
|
1691
1943
|
entries.push(entry)
|
|
1692
1944
|
}
|
|
@@ -1838,7 +2090,28 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1838
2090
|
}
|
|
1839
2091
|
}
|
|
1840
2092
|
|
|
2093
|
+
/**
|
|
2094
|
+
* `dialog.confirm({ title, description, action })` é copy tão editorial quanto a de um
|
|
2095
|
+
* `<ActionFormDialog>`; a chamada só é imperativa. Um template com interpolação
|
|
2096
|
+
* (`description: describe(name)`) cai no diagnóstico `content` normal, declarável em
|
|
2097
|
+
* `copy.dynamic` como `message-template`.
|
|
2098
|
+
*/
|
|
2099
|
+
function extractDialog(call, { method, field }) {
|
|
2100
|
+
const raw = call.arguments[0]
|
|
2101
|
+
const options = raw === undefined ? null : resolveBinding(raw, checker)
|
|
2102
|
+
if (options === null || !ts.isObjectLiteralExpression(options)) {
|
|
2103
|
+
diagnostics.push(diagnostic(file, sourceFile, raw ?? call, `${field} options`, 'structure'))
|
|
2104
|
+
return
|
|
2105
|
+
}
|
|
2106
|
+
for (const [name, role] of DIALOG_METHOD_ROLES.get(method)) addVisualObjectProperty(options, name, role, `${field}.`)
|
|
2107
|
+
if (method === 'choose') {
|
|
2108
|
+
nestedObject(options, 'actions', (action, prefix) => addVisualObjectProperty(action, 'label', 'button', prefix), `${field}.`)
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1841
2112
|
function visit(node) {
|
|
2113
|
+
const dialogCall = ts.isCallExpression(node) ? knownDialogCall(node, checker) : null
|
|
2114
|
+
if (dialogCall !== null) extractDialog(node, dialogCall)
|
|
1842
2115
|
const factory = ts.isCallExpression(node) ? contractFactory(node.expression) : null
|
|
1843
2116
|
if (ts.isCallExpression(node) && factory !== null && node.arguments.length > 0) {
|
|
1844
2117
|
hasContract = true
|