@softize/opus 22.2.0 → 24.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 +120 -1
- package/bin/cli.mjs +3 -9
- package/bin/lib/check.mjs +242 -49
- package/bin/lib/copy.mjs +17 -35
- package/bin/lib/gen-runner.mjs +2 -0
- package/bin/lib/gen-stubs.mjs +3 -1
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/docs/code-style.md +1 -1
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/core/contracts.ts +1 -1
- package/src/core/runtime.ts +4 -37
- package/src/core/types.ts +5 -23
- package/src/design/index.ts +14 -3
- package/src/testing/index.ts +6 -26
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +64 -14
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +93 -70
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +58 -88
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +85 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/actions.md +6 -7
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +100 -58
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +34 -26
- package/src/ui/docs/content/pane.md +41 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/semantic-context.md +0 -1
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +82 -83
- package/src/ui/docs/content/testing.md +2 -3
- package/src/ui/docs/content/toggle.md +120 -63
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +4 -6
- package/src/ui/drivers/react.tsx +4 -3
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +42 -46
- package/src/ui/theme.css +311 -64
- package/src/vite/design.ts +23 -21
- package/src/ui/components/patterns/content-header.tsx +0 -532
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- package/src/ui/docs/content/content.md +0 -276
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
- Execução estrutural: montar uma página de relatório isolada com `Page > PageHeader + PageBody`,
|
|
7
7
|
outra em `PageShell` com navegação contextual acima de `Page > PageHeader + PageBody`, garantindo
|
|
8
8
|
exatamente um `PageTitle` no header, e uma coleção nomeada com
|
|
9
|
-
`
|
|
10
|
-
`
|
|
9
|
+
`Surface > SurfaceHeader + SurfaceBody > ActionList`. Provar que criação fica em
|
|
10
|
+
`SurfaceActions`, no extremo oposto ao título, como botão textual `default` sem ícone e com
|
|
11
11
|
`Criar recurso`, enquanto uma operação dependente do recorte usa `toolbarActions`; incluir
|
|
12
12
|
`ActionFilterBar` separado do renderer, `ItemGroup` para
|
|
13
13
|
uma coleção secundária e um `ActionFormDialog`; provar teto padrão de `80rem`, hierarquia por
|
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
- Execução roteável: declarar Page, criação e edição com `Presentation.route`; provar deep link,
|
|
19
19
|
hidratação do registro, fechamento para a lista e projeção integral no manifest.
|
|
20
20
|
- Execução abreviada: montar outra página com `<Page title actions>` e uma seção com
|
|
21
|
-
`<
|
|
21
|
+
`<Surface title description actions>`, provando que ambas produzem a mesma anatomia e que o lint
|
|
22
22
|
rejeita a mistura entre props abreviadas e headers explícitos.
|
|
23
|
-
- Execução de forma: compor uma tabela dentro de
|
|
23
|
+
- Execução de forma: compor uma tabela dentro de `Surface` sem moldura duplicada, manter a moldura
|
|
24
24
|
estrutural standalone em `rounded-lg` e os controles internos em `rounded-md`.
|
|
25
|
-
- Execução de
|
|
26
|
-
`DetailGroup` com divisores; provar header com divisor,
|
|
27
|
-
consumidor que reconstruam essa anatomia.
|
|
25
|
+
- Execução de Surface seccionada: compor `Surface framed layout="sectioned"` com mídia e
|
|
26
|
+
`DetailGroup` com divisores; provar header com divisor, `SurfaceBody flush` sem recuo e ausência
|
|
27
|
+
de classes no consumidor que reconstruam essa anatomia. Provar `as="fieldset"` com `legend`
|
|
28
|
+
nativa sem `frameVariant`.
|
|
28
29
|
- Reprova: criar uma casca `bg-card` que herda o texto global ou introduzir `rounded-widget`
|
|
29
30
|
quando a escala `rounded-*` atende ao papel.
|
|
30
31
|
- Reprova: trocar automaticamente o radius de um controle quando apenas seu tamanho muda ou
|
|
@@ -32,8 +33,8 @@
|
|
|
32
33
|
- Reprova: reconstruir manualmente o container de página, usar `Empty` tracejado como vazio de
|
|
33
34
|
tabela, alinhar toda coluna numérica à direita por inferência ou destacar “Cancelar” como ação
|
|
34
35
|
primária em modal.
|
|
35
|
-
- Reprova: deixar `
|
|
36
|
-
explícita, usar `
|
|
36
|
+
- Reprova: deixar `SurfaceHeader` fora de `Surface`, omitir `PageBody`/`SurfaceBody` na composição
|
|
37
|
+
explícita, usar `Card*` ou `Content*` em código novo ou tratar `DialogContent` como Body.
|
|
37
38
|
- Execução de dicionários: montar a lista de clientes com “Pessoa física/Empresa” e
|
|
38
39
|
“Prospect/Cliente”; provar que o tipo ocupa a coluna “Tipo” como classificação (badge `outline`
|
|
39
40
|
com os ícones declarados `user` e `building`), que o estágio ocupa a coluna “Estágio” como badge
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
lista; não reconstruir essa decisão com condicionais no componente da página.
|
|
10
10
|
- `Page` fornece o `<main>` e o container centralizado com teto padrão de `80rem`. Sua forma
|
|
11
11
|
explícita exige `PageBody`, exatamente um heading principal e aceita `PageHeader` e `PageFooter`.
|
|
12
|
-
Declare esse heading com `PageTitle` no header, `
|
|
12
|
+
Declare esse heading com `PageTitle` no header, `Surface level={1}` ou um `PageState` ativo no
|
|
13
13
|
body. `PageHeader` é a única introdução estrutural e reúne navegação, título e ações. `title` e `actions` no
|
|
14
14
|
próprio `Page` são a abreviação para o caso direto.
|
|
15
15
|
Não misturar as duas formas. Alterar `className` apenas quando a superfície tiver uma necessidade
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
ações somente com ícone de chrome usam `icon`. O tamanho `sm` fica para ações operacionais em
|
|
27
27
|
toolbar, seção ou coleção densa, e `xs`/`icon-xs` para ações internas de linha ou célula. Contexto
|
|
28
28
|
e variante resolvem a hierarquia visual sem alterar essa medida. Em uma coleção, a ação de
|
|
29
|
-
criar usa botão textual `default`, sem ícone, no extremo oposto do `
|
|
30
|
-
|
|
29
|
+
criar usa botão textual `default`, sem ícone, no extremo oposto do `SurfaceHeader`. Não monte
|
|
30
|
+
cabeçalho paralelo, portal ou seletor global para reconstruir essa composição.
|
|
31
31
|
- `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
|
|
32
32
|
forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
|
|
33
33
|
explícita, fica sozinho dentro de `PageBody`. Nos três estados ativos, o cabeçalho da Page
|
|
@@ -37,22 +37,23 @@
|
|
|
37
37
|
`role="alert"`, usa a mesma composição central e sem moldura dos demais estados e apresenta a
|
|
38
38
|
recuperação como botão `outline` textual. Estados de seção ou coleção continuam em `DataState`,
|
|
39
39
|
`ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a estado da página.
|
|
40
|
-
- `
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
direto e não podem ser misturados ao header explícito.
|
|
44
|
-
no extremo oposto ao título; a criação usa botão textual `default`, sem ícone, com
|
|
40
|
+
- `Surface` delimita uma região e segue a anatomia `Surface > SurfaceHeader (SurfaceMedia,
|
|
41
|
+
SurfaceTitle, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + SurfaceFooter`.
|
|
42
|
+
Os slots nunca ficam soltos. `title`, `description`, `count`, `media` e `actions` são a abreviação
|
|
43
|
+
para o caso direto e não podem ser misturados ao header explícito. Em `level={1}`, ações da
|
|
44
|
+
coleção ficam no extremo oposto ao título; a criação usa botão textual `default`, sem ícone, com
|
|
45
|
+
`Criar recurso`.
|
|
45
46
|
O diálogo de criação usa `Criar recurso` e o de edição, `Editar recurso`. `level` preserva a
|
|
46
|
-
hierarquia semântica do heading. `
|
|
47
|
-
direto já possui recuo interno, como `DetailGroup` com
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- Uma página centrada em coleção compõe `Content + ActionList`. O `ContentHeader` recebe título e
|
|
47
|
+
hierarquia semântica do heading. `Surface framed` aceita `layout="sectioned"`; declare
|
|
48
|
+
`SurfaceBody flush` quando o filho direto já possui recuo interno, como `DetailGroup` com
|
|
49
|
+
divisores, lista ou tabela. `as="fieldset"` cria a `legend` nativa sem variante de moldura.
|
|
50
|
+
- Uma página centrada em coleção compõe `Surface + ActionList`. O `SurfaceHeader` recebe título e
|
|
51
51
|
criação; o `ActionList` mantém busca, filtros, atualização, resultados, estados e paginação.
|
|
52
52
|
`toolbarActions` fica reservado a operações que dependem do recorte atual.
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
- `Surface` e Drawer nomeiam a região principal como `SurfaceBody` e `DrawerBody`. `Pane` é apenas
|
|
54
|
+
uma unidade dimensionável de `Split` e não possui anatomia interna. `*Content` permanece
|
|
55
|
+
reservado a raízes técnicas ou painéis cujo papel não é o corpo de uma estrutura, como
|
|
56
|
+
`DialogContent`, `PopoverContent` e `TabsPanel`.
|
|
56
57
|
- Componentes compartilhados não impõem margem externa; páginas e shells compõem layout.
|
|
57
58
|
- A UI assume layout desktop fixo e largura mínima suportada de `64rem`. Não introduzir
|
|
58
59
|
breakpoints de viewport ou container queries isoladas; uma futura responsividade precisa começar
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Contrato e binding
|
|
2
2
|
|
|
3
3
|
O contrato contém `name`, `kind`, documentação, schemas, metadata de UI e autorização
|
|
4
|
-
declarativa compartilhável. Ele não declara dados de design: no modo design, a action
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
declarativa compartilhável. Ele não declara dados de design: no modo design, a action responde
|
|
5
|
+
com dado gerado a partir do `output`, e um cenário específico, quando necessário, fica fora do
|
|
6
|
+
contrato em `defineDesignScenario` (ADR 0037).
|
|
7
7
|
|
|
8
8
|
O binding contém `handler`, `loads`, autorização dependente de dados carregados, execução
|
|
9
9
|
em background, eventos e idempotência. `bindAction(contract, binding)` produz o `ActionDef`
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { z } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
DataState,
|
|
5
|
+
Surface,
|
|
6
|
+
useListAction,
|
|
7
|
+
} from "@softize/opus/ui/react";
|
|
8
|
+
import { Task, taskList } from "./domains/tasks/actions/list.ts";
|
|
4
9
|
|
|
5
|
-
type TaskItem = z.infer<typeof Task
|
|
10
|
+
type TaskItem = z.infer<typeof Task>;
|
|
6
11
|
|
|
7
12
|
/** Página inicial do esqueleto: prova a fundação (tema + componentes do Opus) e o
|
|
8
13
|
* domínio-exemplo. Troque tudo — este arquivo é seu. */
|
|
@@ -10,14 +15,16 @@ export function App(): React.ReactElement {
|
|
|
10
15
|
// O front consome o CONTRATO via /api — o plugin opusDesign (vite.config.ts)
|
|
11
16
|
// serve o backend no próprio dev server: dev normal executa o handler real;
|
|
12
17
|
// `pnpm dev:design` responde com dado gerado a partir do contrato, isolado de qualquer backend.
|
|
13
|
-
const { items, isLoading, error, refetch } =
|
|
18
|
+
const { items, isLoading, error, refetch } =
|
|
19
|
+
useListAction<TaskItem>(taskList);
|
|
14
20
|
|
|
15
21
|
return (
|
|
16
22
|
<main className="flex min-h-full items-center justify-center bg-background p-6">
|
|
17
|
-
<
|
|
18
|
-
<h1 className="mb-1 text-lg font-semibold">{
|
|
23
|
+
<Surface framed elevation="raised" className="w-full max-w-md p-6">
|
|
24
|
+
<h1 className="mb-1 text-lg font-semibold">{"{{APP_NAME}}"}</h1>
|
|
19
25
|
<p className="mb-4 text-sm text-muted-foreground">
|
|
20
|
-
Esqueleto criado pelo opus create. A spec vive nas declarações do
|
|
26
|
+
Esqueleto criado pelo opus create. A spec vive nas declarações do
|
|
27
|
+
domínio.
|
|
21
28
|
</p>
|
|
22
29
|
{/* Carregando, erro e vazio saem do DataState; a lista só cuida dos itens. */}
|
|
23
30
|
<DataState
|
|
@@ -29,14 +36,19 @@ export function App(): React.ReactElement {
|
|
|
29
36
|
>
|
|
30
37
|
<ul className="space-y-2">
|
|
31
38
|
{items.map((t) => (
|
|
32
|
-
<li
|
|
39
|
+
<li
|
|
40
|
+
key={t.id}
|
|
41
|
+
className="flex items-center justify-between gap-2 text-sm"
|
|
42
|
+
>
|
|
33
43
|
<span>{t.title}</span>
|
|
34
|
-
<Badge context={t.done ?
|
|
44
|
+
<Badge context={t.done ? "success" : "neutral"}>
|
|
45
|
+
{t.done ? "Feita" : "Aberta"}
|
|
46
|
+
</Badge>
|
|
35
47
|
</li>
|
|
36
48
|
))}
|
|
37
49
|
</ul>
|
|
38
50
|
</DataState>
|
|
39
|
-
</
|
|
51
|
+
</Surface>
|
|
40
52
|
</main>
|
|
41
|
-
)
|
|
53
|
+
);
|
|
42
54
|
}
|
package/src/core/contracts.ts
CHANGED
|
@@ -42,7 +42,7 @@ type BindingKeys = 'handler' | 'loads' | 'background' | 'emits' | 'idempotency'
|
|
|
42
42
|
* `ParsedIn` (o lado PARSEADO do schema de input — defaults aplicados) defaulta
|
|
43
43
|
* a `any` nos contratos, de propósito: quem anota contrato à mão é o CLIENTE
|
|
44
44
|
* (`FormContract<MeuInput, MeuOutput>` em hooks/props), que só conhece o fio —
|
|
45
|
-
* e `authorize
|
|
45
|
+
* e `authorize` é contravariante em `ParsedIn`, então um
|
|
46
46
|
* default estreito rejeitaria contrato real cujo schema tem `.default()`.
|
|
47
47
|
* Quem precisa do parseado com precisão (`bindAction`) o infere do schema.
|
|
48
48
|
*/
|
package/src/core/runtime.ts
CHANGED
|
@@ -833,16 +833,7 @@ export class Runtime {
|
|
|
833
833
|
}
|
|
834
834
|
|
|
835
835
|
// — 5. Handler ————————————————————————————————————————————————————————
|
|
836
|
-
|
|
837
|
-
// no handler real se não houver. Em qualquer outro modo, handler real
|
|
838
|
-
// sempre manda. Leitura de env mora aqui, no boundary do execute — o
|
|
839
|
-
// resto do core continua sem tocar `process.env`.
|
|
840
|
-
const isDesignMode = isOpusDesignMode()
|
|
841
|
-
const handlerToRun =
|
|
842
|
-
isDesignMode && action.mockHandler !== undefined
|
|
843
|
-
? action.mockHandler
|
|
844
|
-
: action.handler
|
|
845
|
-
const output = await handlerToRun(
|
|
836
|
+
const output = await action.handler(
|
|
846
837
|
ctx,
|
|
847
838
|
validatedInput,
|
|
848
839
|
loaded,
|
|
@@ -850,8 +841,6 @@ export class Runtime {
|
|
|
850
841
|
? progress
|
|
851
842
|
: undefined,
|
|
852
843
|
)
|
|
853
|
-
const usedMock = handlerToRun === action.mockHandler
|
|
854
|
-
|
|
855
844
|
// — 6. Validate output (dev only) —————————————————————————————————————
|
|
856
845
|
// Search retorna Paginated<Item> mas `output` schema descreve só Item;
|
|
857
846
|
// skipamos pra evitar falso negativo. v1 pode validar items[].
|
|
@@ -874,7 +863,6 @@ export class Runtime {
|
|
|
874
863
|
ctx,
|
|
875
864
|
input: validatedInput,
|
|
876
865
|
output: validatedOutput,
|
|
877
|
-
mode: usedMock ? 'design' : undefined,
|
|
878
866
|
}),
|
|
879
867
|
action.audit,
|
|
880
868
|
)
|
|
@@ -1271,8 +1259,6 @@ export class Runtime {
|
|
|
1271
1259
|
input: unknown
|
|
1272
1260
|
output?: unknown
|
|
1273
1261
|
error?: ReturnType<typeof error>
|
|
1274
|
-
/** Quando `'design'`, sinaliza que o mockHandler foi executado. */
|
|
1275
|
-
mode?: 'design' | undefined
|
|
1276
1262
|
}): AuditRecord {
|
|
1277
1263
|
const record: AuditRecord = {
|
|
1278
1264
|
id: args.actionId,
|
|
@@ -1293,13 +1279,12 @@ export class Runtime {
|
|
|
1293
1279
|
if (args.nature !== undefined) {
|
|
1294
1280
|
const effect = resolveActionEffect(args.nature)
|
|
1295
1281
|
if (effect !== undefined) record.effect = effect
|
|
1296
|
-
// A exposição só existe quando a action devolveu algo
|
|
1297
|
-
//
|
|
1298
|
-
if (args.nature.personalData === true && args.outcome === 'success'
|
|
1282
|
+
// A exposição só existe quando a action devolveu algo. O plugin de design remove o marcador
|
|
1283
|
+
// das respostas simuladas antes de registrar a action no runtime.
|
|
1284
|
+
if (args.nature.personalData === true && args.outcome === 'success') {
|
|
1299
1285
|
record.personalData = { records: countReturnedRecords(args.nature.kind, args.output) }
|
|
1300
1286
|
}
|
|
1301
1287
|
}
|
|
1302
|
-
if (args.mode !== undefined) record.meta = { mode: args.mode }
|
|
1303
1288
|
return record
|
|
1304
1289
|
}
|
|
1305
1290
|
|
|
@@ -2095,21 +2080,3 @@ function generateId(): string {
|
|
|
2095
2080
|
// fallback determinístico-suficiente quando crypto não disponível
|
|
2096
2081
|
return `id_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`
|
|
2097
2082
|
}
|
|
2098
|
-
|
|
2099
|
-
/**
|
|
2100
|
-
* Único ponto do core que lê `process.env`. Usado pelo `execute()` pra
|
|
2101
|
-
* decidir entre `handler` real e `mockHandler`. Isolado em função pra
|
|
2102
|
-
* testabilidade — testes podem `vi.stubEnv('OPUS_MODE', ...)`.
|
|
2103
|
-
*
|
|
2104
|
-
* Mantém a invariante "core nunca lê env" intacta no resto do código:
|
|
2105
|
-
* adapters/config seguem recebendo valores literais.
|
|
2106
|
-
*
|
|
2107
|
-
* Exportado (fora do index público) pro harness de `testing` aplicar a MESMA
|
|
2108
|
-
* régua no `runAction` — a semântica de design mora num lugar só.
|
|
2109
|
-
*/
|
|
2110
|
-
export function isOpusDesignMode(): boolean {
|
|
2111
|
-
// Acesso via globalThis: defensivo pra runtimes sem `process` (browser, edge) E
|
|
2112
|
-
// não exige @types/node no consumidor que compila este source (apps web).
|
|
2113
|
-
const g = globalThis as { process?: { env?: Record<string, string | undefined> } }
|
|
2114
|
-
return g.process?.env?.['OPUS_MODE'] === 'design'
|
|
2115
|
-
}
|
package/src/core/types.ts
CHANGED
|
@@ -638,6 +638,9 @@ export interface FilterSpec {
|
|
|
638
638
|
|
|
639
639
|
type: 'text' | 'select' | 'lookup' | 'date' | 'number'
|
|
640
640
|
multiple?: boolean
|
|
641
|
+
/** Controle usado para um `select` de escolha única. `select` é o padrão; `toggle`
|
|
642
|
+
* mantém todas as opções curtas visíveis em um grupo conectado. */
|
|
643
|
+
presentation?: 'select' | 'toggle'
|
|
641
644
|
/** Habilita busca local nas opções de um `select`; `lookup` já é pesquisável por natureza. */
|
|
642
645
|
searchable?: boolean
|
|
643
646
|
/** Quando `false`, a interface omite a opção vazia. Limpar restaura o default do filtro. */
|
|
@@ -799,7 +802,7 @@ export type LoadersSpec<In> = Record<string, LoaderFn<In>>
|
|
|
799
802
|
* aqui.
|
|
800
803
|
* - **`ParsedIn`** — o lado PARSEADO (o output do schema de input, defaults
|
|
801
804
|
* aplicados). O runtime SEMPRE valida antes de executar, então `handler`,
|
|
802
|
-
* `
|
|
805
|
+
* `handler`, `authorize` e `loads` recebem `ParsedIn`.
|
|
803
806
|
*
|
|
804
807
|
* O default `ParsedIn = In` mantém retrocompat com anotações à mão de dois
|
|
805
808
|
* genéricos; `defineAction`/`defineContract` inferem os dois lados do schema.
|
|
@@ -848,16 +851,6 @@ export interface ActionBase<In, Out, ParsedIn = In> {
|
|
|
848
851
|
// — Execução —
|
|
849
852
|
handler: HandlerFn<ParsedIn, Out>
|
|
850
853
|
|
|
851
|
-
/**
|
|
852
|
-
* Handler alternativo executado quando `process.env.OPUS_MODE === 'design'`.
|
|
853
|
-
*
|
|
854
|
-
* @deprecated Desde a 22.1.0 (ADR 0037); sai na próxima major. No modo design, a action sem
|
|
855
|
-
* mock já responde com dado gerado a partir do `output`; quando o design precisar de uma resposta
|
|
856
|
-
* específica, declare um cenário fora do contrato com `defineDesignScenario`, de
|
|
857
|
-
* `@softize/opus/design`, e aponte a opção `scenarios` do `opusDesign()` para ele.
|
|
858
|
-
*/
|
|
859
|
-
mockHandler?: HandlerFn<ParsedIn, Out>
|
|
860
|
-
|
|
861
854
|
// — Comportamento —
|
|
862
855
|
audit?: boolean | AuditConfig
|
|
863
856
|
confirm?: ConfirmSpec
|
|
@@ -922,24 +915,13 @@ export interface SearchFilters {
|
|
|
922
915
|
* envelope — handler é fonte de verdade.
|
|
923
916
|
*/
|
|
924
917
|
export interface ListAction<In, Out, ParsedIn = In>
|
|
925
|
-
extends Omit<ActionBase<In, Out, ParsedIn>, 'handler'
|
|
918
|
+
extends Omit<ActionBase<In, Out, ParsedIn>, 'handler'> {
|
|
926
919
|
kind: 'list'
|
|
927
920
|
handler: (
|
|
928
921
|
ctx: ActionContext,
|
|
929
922
|
input: ParsedIn,
|
|
930
923
|
loaded?: Record<string, unknown>,
|
|
931
924
|
) => Paginated<Out> | Promise<Paginated<Out>>
|
|
932
|
-
/**
|
|
933
|
-
* Mesma semântica de `ActionBase.mockHandler`, mas devolve `Paginated<Out>`
|
|
934
|
-
* (search retorna lista paginada, não item solto).
|
|
935
|
-
*
|
|
936
|
-
* @deprecated Desde a 22.1.0 (ADR 0037); ver `ActionBase.mockHandler`.
|
|
937
|
-
*/
|
|
938
|
-
mockHandler?: (
|
|
939
|
-
ctx: ActionContext,
|
|
940
|
-
input: ParsedIn,
|
|
941
|
-
loaded?: Record<string, unknown>,
|
|
942
|
-
) => Paginated<Out> | Promise<Paginated<Out>>
|
|
943
925
|
/** Colunas declarativas da listagem (a UI deriva a tabela; ver ListColumnSpec). */
|
|
944
926
|
columns?: ListColumnSpec[]
|
|
945
927
|
filters?: Record<string, FilterSpec>
|
package/src/design/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* respostas ao runtime de design.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import type { ZodTypeAny } from 'zod'
|
|
13
13
|
import type { ActionContext, InferOutput, Paginated } from '../core/types.ts'
|
|
14
14
|
import { fake, fakeMany } from '../testing/fake.ts'
|
|
15
15
|
|
|
@@ -102,11 +102,14 @@ interface DesignActionShape {
|
|
|
102
102
|
* - `list` devolve três páginas do tamanho pedido em `limit` ({@link DESIGN_PAGE_SIZE} quando a
|
|
103
103
|
* entrada não pede) e respeita a paginação declarada; busca e filtros não filtram o dado gerado.
|
|
104
104
|
* - Nas demais, a saída ecoa o `id` da entrada quando o schema da saída aceita esse valor.
|
|
105
|
-
* - Saída que não é schema
|
|
105
|
+
* - Saída que não é schema Zod não tem gerador: a resposta é `null`.
|
|
106
106
|
*/
|
|
107
107
|
export function designResponse(action: DesignActionShape, input: unknown): unknown {
|
|
108
108
|
const output = action.output
|
|
109
|
-
|
|
109
|
+
// O contrato vem do grafo do consumidor e pode carregar outra instância física do Zod.
|
|
110
|
+
// `instanceof` confundiria identidade de módulo com contrato; o gerador usa somente a
|
|
111
|
+
// estrutura pública/estável que já recebe (`safeParse`) e a definição introspectável (`_def`).
|
|
112
|
+
if (!isZodSchema(output)) return null
|
|
110
113
|
const seed = `${action.name}:${canonicalJson(withoutPagination(input))}`
|
|
111
114
|
|
|
112
115
|
if (action.kind === 'list') {
|
|
@@ -177,3 +180,11 @@ function canonicalJson(value: unknown): string {
|
|
|
177
180
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
178
181
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
179
182
|
}
|
|
183
|
+
|
|
184
|
+
function isZodSchema(value: unknown): value is ZodTypeAny {
|
|
185
|
+
return (
|
|
186
|
+
isRecord(value) &&
|
|
187
|
+
isRecord(value._def) &&
|
|
188
|
+
typeof value.safeParse === 'function'
|
|
189
|
+
)
|
|
190
|
+
}
|
package/src/testing/index.ts
CHANGED
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
* valida output (pulado em `list`, como no runtime). Falha estoura o MESMO
|
|
15
15
|
* `ActionError` tipado do runtime (`validation.invalid_input`, `auth.unauthenticated`,
|
|
16
16
|
* `auth.system_forbidden`, `auth.forbidden`…). A opção `schedule` executa como o sistema.
|
|
17
|
-
* Em `OPUS_MODE=design` roteia pro `mockHandler` como o runtime — contrato puro
|
|
18
|
-
* (sem handler) é testável em design; fora dele, erro claro (`testing.no_handler`).
|
|
19
17
|
* • `memStorage()` — StorageAdapter em memória pra testar handler que anexa
|
|
20
18
|
* arquivo, com a mesma régua de key dos drivers reais.
|
|
21
19
|
*
|
|
@@ -30,9 +28,7 @@ import { evalExpression } from '../dsl/eval.ts'
|
|
|
30
28
|
import { parseExpression } from '../dsl/parser.ts'
|
|
31
29
|
import { normalizeKey } from '../storage/index.ts'
|
|
32
30
|
import { error } from '../core/errors.ts'
|
|
33
|
-
|
|
34
|
-
// tem UMA fonte — o harness nunca pode divergir do execute().
|
|
35
|
-
import { assertAuthenticated, isOpusDesignMode, markSystemActor } from '../core/runtime.ts'
|
|
31
|
+
import { assertAuthenticated, markSystemActor } from '../core/runtime.ts'
|
|
36
32
|
import type {
|
|
37
33
|
ActionContext,
|
|
38
34
|
ActionKind,
|
|
@@ -206,14 +202,8 @@ export interface TestableAction<In, Out, ParsedIn = In> {
|
|
|
206
202
|
authorize?: AuthorizeSpec<ParsedIn>
|
|
207
203
|
input: Schema<In, ParsedIn>
|
|
208
204
|
output: Schema<Out> | Schema<unknown, Out>
|
|
209
|
-
/** Opcional
|
|
205
|
+
/** Opcional para produzir um erro útil quando um contrato puro chega ao harness. */
|
|
210
206
|
handler?: HandlerFn<ParsedIn, Out>
|
|
211
|
-
/**
|
|
212
|
-
* Em `OPUS_MODE=design` vence o handler real — mesma régua do runtime.
|
|
213
|
-
*
|
|
214
|
-
* @deprecated Desde a 22.1.0 (ADR 0037); teste a action ligada ao handler.
|
|
215
|
-
*/
|
|
216
|
-
mockHandler?: HandlerFn<ParsedIn, Out>
|
|
217
207
|
}
|
|
218
208
|
|
|
219
209
|
/** Variante list: o schema descreve o ITEM, o handler devolve `Paginated<Item>`. */
|
|
@@ -230,11 +220,6 @@ export interface TestableListAction<In, Item, ParsedIn = In> {
|
|
|
230
220
|
input: ParsedIn,
|
|
231
221
|
loaded?: Record<string, unknown>,
|
|
232
222
|
) => Paginated<Item> | Promise<Paginated<Item>>
|
|
233
|
-
mockHandler?: (
|
|
234
|
-
ctx: ActionContext,
|
|
235
|
-
input: ParsedIn,
|
|
236
|
-
loaded?: Record<string, unknown>,
|
|
237
|
-
) => Paginated<Item> | Promise<Paginated<Item>>
|
|
238
223
|
}
|
|
239
224
|
|
|
240
225
|
export interface RunActionOptions extends TestContextOptions {
|
|
@@ -278,9 +263,8 @@ async function validate<T>(
|
|
|
278
263
|
* validado + o contexto observável (`emitted`, `logged`); falha estoura o mesmo
|
|
279
264
|
* `ActionError` do runtime — afirme com `expect(...).rejects`.
|
|
280
265
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* handler) só roda em design — fora dele, `testing.no_handler`.
|
|
266
|
+
* Contrato puro sem handler termina em `testing.no_handler`: dados de design
|
|
267
|
+
* pertencem ao plugin `opusDesign()` e não são comportamento da action.
|
|
284
268
|
*/
|
|
285
269
|
export async function runAction<In, Item, ParsedIn = In>(
|
|
286
270
|
action: TestableListAction<In, Item, ParsedIn>,
|
|
@@ -325,18 +309,14 @@ export async function runAction<In, Out, ParsedIn = In>(
|
|
|
325
309
|
if (decision !== true) throw decision
|
|
326
310
|
}
|
|
327
311
|
|
|
328
|
-
|
|
329
|
-
isOpusDesignMode() && action.mockHandler !== undefined
|
|
330
|
-
? action.mockHandler
|
|
331
|
-
: action.handler
|
|
332
|
-
if (handlerToRun === undefined) {
|
|
312
|
+
if (action.handler === undefined) {
|
|
333
313
|
throw error({
|
|
334
314
|
code: 'testing.no_handler',
|
|
335
315
|
category: 'internal',
|
|
336
316
|
message: `Action "${action.name}" não tem handler executável: teste o ActionDef produzido pelo bindAction. Os dados do modo design são gerados pelo plugin opusDesign() e não são comportamento da action.`,
|
|
337
317
|
})
|
|
338
318
|
}
|
|
339
|
-
const raw = await
|
|
319
|
+
const raw = await action.handler(ctx, parsed, loaded)
|
|
340
320
|
|
|
341
321
|
// Mesmo skip do runtime: list devolve Paginated<Item>, o schema descreve Item.
|
|
342
322
|
const output = action.kind === 'list' ? (raw as Out) : await validate<Out>(action.output, raw, 'output')
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/ui/appearance — personalização de aparência sem React (ADR 0044).
|
|
3
|
+
*
|
|
4
|
+
* O tema deriva a escala neutra de quatro eixos inteiros (0 a 100) declarados por modo em
|
|
5
|
+
* `theme.css`: `--appearance-light-*` e `--appearance-dark-*`. Este módulo publica os tipos, os
|
|
6
|
+
* presets oficiais e o helper que converte overrides parciais nas custom properties que o
|
|
7
|
+
* consumidor aplica onde quiser (em geral, inline em `<html>`).
|
|
8
|
+
*
|
|
9
|
+
* Reset não pertence ao Opus UI: um eixo sem override vira `null`, e o consumidor remove a
|
|
10
|
+
* propriedade para voltar ao cascade — ou seja, ao preset do tema ou ao CSS do próprio app.
|
|
11
|
+
* Persistência e interface de ajuste também ficam com o consumidor.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type AppearanceMode = "light" | "dark";
|
|
15
|
+
|
|
16
|
+
export const appearanceModes = [
|
|
17
|
+
"light",
|
|
18
|
+
"dark",
|
|
19
|
+
] as const satisfies readonly AppearanceMode[];
|
|
20
|
+
|
|
21
|
+
export type AppearanceAxis =
|
|
22
|
+
"colorIntensity" | "contrast" | "surfaceBrightness" | "layerContrast";
|
|
23
|
+
|
|
24
|
+
/** Ordem canônica dos eixos: a mesma usada em documentação, presets e custom properties. */
|
|
25
|
+
export const appearanceAxes = [
|
|
26
|
+
"colorIntensity",
|
|
27
|
+
"contrast",
|
|
28
|
+
"surfaceBrightness",
|
|
29
|
+
"layerContrast",
|
|
30
|
+
] as const satisfies readonly AppearanceAxis[];
|
|
31
|
+
|
|
32
|
+
/** Valor completo de um modo: todo eixo é um inteiro entre `APPEARANCE_MIN` e `APPEARANCE_MAX`. */
|
|
33
|
+
export type AppearanceSettings = Readonly<Record<AppearanceAxis, number>>;
|
|
34
|
+
|
|
35
|
+
/** Override parcial: eixo ausente (ou não finito) segue o cascade em vez de ser reescrito. */
|
|
36
|
+
export type AppearanceOverrides = Partial<
|
|
37
|
+
Record<AppearanceAxis, number | null | undefined>
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
export const APPEARANCE_MIN = 0;
|
|
41
|
+
export const APPEARANCE_MAX = 100;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Presets oficiais por modo — reproduzem exatamente os valores do tema sem personalização.
|
|
45
|
+
* Intensidade, contraste e contraste entre camadas partem de zero: o preset é o piso, e o ajuste
|
|
46
|
+
* só acrescenta cor ou separação. No claro, a superfície elevada já é branca; o brilho parte do
|
|
47
|
+
* máximo e só pode diminuir. No escuro, o brilho fica no meio para permitir um escuro mais
|
|
48
|
+
* profundo ou mais suave.
|
|
49
|
+
*/
|
|
50
|
+
export const appearancePresets: Readonly<
|
|
51
|
+
Record<AppearanceMode, AppearanceSettings>
|
|
52
|
+
> = {
|
|
53
|
+
light: {
|
|
54
|
+
colorIntensity: 0,
|
|
55
|
+
contrast: 0,
|
|
56
|
+
surfaceBrightness: 100,
|
|
57
|
+
layerContrast: 0,
|
|
58
|
+
},
|
|
59
|
+
dark: {
|
|
60
|
+
colorIntensity: 0,
|
|
61
|
+
contrast: 0,
|
|
62
|
+
surfaceBrightness: 50,
|
|
63
|
+
layerContrast: 0,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const AXIS_PROPERTY: Record<AppearanceAxis, string> = {
|
|
68
|
+
colorIntensity: "color-intensity",
|
|
69
|
+
contrast: "contrast",
|
|
70
|
+
surfaceBrightness: "surface-brightness",
|
|
71
|
+
layerContrast: "layer-contrast",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type AppearanceCustomProperty =
|
|
75
|
+
`--appearance-${AppearanceMode}-${string}`;
|
|
76
|
+
|
|
77
|
+
/** Nome da custom property que `theme.css` lê para um eixo de um modo. */
|
|
78
|
+
export function appearanceCustomProperty(
|
|
79
|
+
mode: AppearanceMode,
|
|
80
|
+
axis: AppearanceAxis,
|
|
81
|
+
): AppearanceCustomProperty {
|
|
82
|
+
return `--appearance-${mode}-${AXIS_PROPERTY[axis]}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Normaliza um valor de eixo: arredonda para inteiro e limita ao intervalo. Retorna `undefined`
|
|
87
|
+
* para o que não é um número finito — o consumidor pode ter lido um valor corrompido de
|
|
88
|
+
* persistência, e a resposta correta é seguir o cascade, não travar a interface.
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeAppearanceValue(value: unknown): number | undefined {
|
|
91
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
92
|
+
return Math.min(APPEARANCE_MAX, Math.max(APPEARANCE_MIN, Math.round(value)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Preset do modo com os overrides válidos aplicados por cima. */
|
|
96
|
+
export function resolveAppearance(
|
|
97
|
+
mode: AppearanceMode,
|
|
98
|
+
overrides: AppearanceOverrides = {},
|
|
99
|
+
): AppearanceSettings {
|
|
100
|
+
const preset = appearancePresets[mode];
|
|
101
|
+
const resolved = { ...preset } as Record<AppearanceAxis, number>;
|
|
102
|
+
for (const axis of appearanceAxes) {
|
|
103
|
+
const value = normalizeAppearanceValue(overrides[axis]);
|
|
104
|
+
if (value !== undefined) resolved[axis] = value;
|
|
105
|
+
}
|
|
106
|
+
return resolved;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Converte overrides parciais nas custom properties do modo. Todo eixo aparece no resultado:
|
|
111
|
+
* com valor normalizado quando há override e `null` quando não há, para o consumidor remover a
|
|
112
|
+
* propriedade e deixar o cascade valer. Assim, o mesmo mapa serve para aplicar e para desfazer.
|
|
113
|
+
*/
|
|
114
|
+
export function appearanceCustomProperties(
|
|
115
|
+
mode: AppearanceMode,
|
|
116
|
+
overrides: AppearanceOverrides = {},
|
|
117
|
+
): Record<AppearanceCustomProperty, string | null> {
|
|
118
|
+
const properties = {} as Record<AppearanceCustomProperty, string | null>;
|
|
119
|
+
for (const axis of appearanceAxes) {
|
|
120
|
+
const value = normalizeAppearanceValue(overrides[axis]);
|
|
121
|
+
properties[appearanceCustomProperty(mode, axis)] =
|
|
122
|
+
value === undefined ? null : String(value);
|
|
123
|
+
}
|
|
124
|
+
return properties;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Alvo mínimo para aplicar propriedades: `CSSStyleDeclaration` satisfaz, e um stub de teste também. */
|
|
128
|
+
export interface AppearanceStyleTarget {
|
|
129
|
+
setProperty(name: string, value: string): void;
|
|
130
|
+
removeProperty(name: string): string | void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Aplica os overrides de um modo num alvo de estilo. Eixos sem override são removidos, então
|
|
135
|
+
* chamar sem overrides devolve o elemento ao cascade — é o reset, feito pelo consumidor, sem
|
|
136
|
+
* estado no Opus.
|
|
137
|
+
*/
|
|
138
|
+
export function applyAppearanceCustomProperties(
|
|
139
|
+
target: AppearanceStyleTarget,
|
|
140
|
+
mode: AppearanceMode,
|
|
141
|
+
overrides: AppearanceOverrides = {},
|
|
142
|
+
): void {
|
|
143
|
+
for (const [name, value] of Object.entries(
|
|
144
|
+
appearanceCustomProperties(mode, overrides),
|
|
145
|
+
)) {
|
|
146
|
+
if (value === null) target.removeProperty(name);
|
|
147
|
+
else target.setProperty(name, value);
|
|
148
|
+
}
|
|
149
|
+
}
|