@softize/opus 22.1.0 → 23.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 +74 -1
- package/bin/cli.mjs +6 -5
- package/bin/lib/check.mjs +23 -8
- package/bin/lib/copy.mjs +1 -1
- package/bin/lib/gen-manifest.mjs +2 -0
- package/bin/lib/gen-runner.mjs +7 -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/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 +60 -0
- package/docs/adr/0041-actions-declare-effect-and-personal-data.md +139 -0
- package/docs/code-style.md +1 -1
- package/docs/protocol.md +15 -2
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +1 -1
- package/registry/skills/create-opus-action/SKILL.md +7 -2
- package/registry/skills/create-opus-action/references/contract-and-binding.md +10 -5
- package/registry/skills/create-opus-action/references/evaluations.md +3 -0
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +6 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +2 -0
- package/src/core/action-nature.ts +40 -0
- package/src/core/audit.ts +12 -4
- package/src/core/contracts.ts +1 -1
- package/src/core/index.ts +2 -0
- package/src/core/runtime.ts +57 -35
- package/src/core/types.ts +45 -23
- package/src/design/index.ts +14 -3
- package/src/mcp/index.ts +6 -0
- package/src/testing/index.ts +6 -26
- package/src/ui/components/patterns/content-header.tsx +5 -9
- package/src/ui/components/patterns/list.tsx +55 -1
- package/src/ui/components/patterns/presentation.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -0
- package/src/ui/components/primitives/tabs.tsx +55 -87
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/docs/content/action-list.md +13 -4
- package/src/ui/docs/content/actions.md +36 -8
- package/src/ui/docs/content/content.md +5 -6
- package/src/ui/docs/content/mcp.md +6 -0
- package/src/ui/docs/content/semantic-context.md +0 -1
- package/src/ui/docs/content/tabs.md +68 -69
- package/src/ui/docs/content/testing.md +2 -3
- package/src/ui/docs/content/toggle.md +15 -10
- package/src/ui/drivers/react.tsx +4 -3
- package/src/ui/meta.ts +2 -2
- package/src/ui/react.tsx +2 -3
- package/src/vite/design.ts +23 -21
package/src/core/types.ts
CHANGED
|
@@ -356,6 +356,15 @@ export interface AuditRecord {
|
|
|
356
356
|
output?: unknown
|
|
357
357
|
error?: ActionError
|
|
358
358
|
|
|
359
|
+
/** Efeito resolvido da action (ADR 0041): `list` e `view` são sempre leitura. */
|
|
360
|
+
effect?: ActionEffect
|
|
361
|
+
/**
|
|
362
|
+
* Presente quando a action declara `personalData: true` e executou com sucesso (ADR 0041):
|
|
363
|
+
* a requisição expôs dado pessoal a quem a executou. `records` conta os registros devolvidos
|
|
364
|
+
* (itens da página de uma `list`, 1 para um objeto) e é `null` quando a saída não permite contar.
|
|
365
|
+
*/
|
|
366
|
+
personalData?: { records: number | null }
|
|
367
|
+
|
|
359
368
|
severity: 'info' | 'warning' | 'error'
|
|
360
369
|
/** Campo legado preservado para compatibilidade com sinks/consumers existentes. */
|
|
361
370
|
trace?: {
|
|
@@ -629,6 +638,9 @@ export interface FilterSpec {
|
|
|
629
638
|
|
|
630
639
|
type: 'text' | 'select' | 'lookup' | 'date' | 'number'
|
|
631
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'
|
|
632
644
|
/** Habilita busca local nas opções de um `select`; `lookup` já é pesquisável por natureza. */
|
|
633
645
|
searchable?: boolean
|
|
634
646
|
/** Quando `false`, a interface omite a opção vazia. Limpar restaura o default do filtro. */
|
|
@@ -709,6 +721,12 @@ export interface ExpandSpec {
|
|
|
709
721
|
|
|
710
722
|
export type ActionKind = 'simple' | 'form' | 'list' | 'view'
|
|
711
723
|
|
|
724
|
+
/**
|
|
725
|
+
* O que a action faz com o estado (ADR 0041). `list` e `view` são leitura pelo protocolo (`GET`);
|
|
726
|
+
* `simple` e `form` declaram, porque cobrem as duas coisas.
|
|
727
|
+
*/
|
|
728
|
+
export type ActionEffect = 'read' | 'write'
|
|
729
|
+
|
|
712
730
|
export type AuthorizeFn<In, Loaded = Record<string, unknown>> = (
|
|
713
731
|
ctx: ActionContext,
|
|
714
732
|
input: In,
|
|
@@ -784,7 +802,7 @@ export type LoadersSpec<In> = Record<string, LoaderFn<In>>
|
|
|
784
802
|
* aqui.
|
|
785
803
|
* - **`ParsedIn`** — o lado PARSEADO (o output do schema de input, defaults
|
|
786
804
|
* aplicados). O runtime SEMPRE valida antes de executar, então `handler`,
|
|
787
|
-
* `
|
|
805
|
+
* `handler`, `authorize` e `loads` recebem `ParsedIn`.
|
|
788
806
|
*
|
|
789
807
|
* O default `ParsedIn = In` mantém retrocompat com anotações à mão de dois
|
|
790
808
|
* genéricos; `defineAction`/`defineContract` inferem os dois lados do schema.
|
|
@@ -810,6 +828,20 @@ export interface ActionBase<In, Out, ParsedIn = In> {
|
|
|
810
828
|
input: Schema<In, ParsedIn>
|
|
811
829
|
output: Schema<Out>
|
|
812
830
|
|
|
831
|
+
// — Natureza (ADR 0041) —
|
|
832
|
+
/**
|
|
833
|
+
* `read` não altera estado; `write` altera. `list` e `view` já são leitura pelo protocolo e não
|
|
834
|
+
* declaram (declarar `write` nelas é recusado no registro). `simple` e `form` declaram: o `kind`
|
|
835
|
+
* sozinho não diz se a action lê ou escreve. Declarativo — o runtime não impede o handler.
|
|
836
|
+
*/
|
|
837
|
+
effect?: ActionEffect
|
|
838
|
+
/**
|
|
839
|
+
* A resposta contém dados que identificam pessoas: nome, documento, contato, placa ou texto
|
|
840
|
+
* livre sobre alguém. Com `true`, toda execução bem-sucedida é marcada no `AuditRecord` e no
|
|
841
|
+
* span, qualquer que seja o consumidor. Nunca inferido; ausente significa "não declarado".
|
|
842
|
+
*/
|
|
843
|
+
personalData?: boolean
|
|
844
|
+
|
|
813
845
|
// — Autorização (fail-closed default) —
|
|
814
846
|
public?: boolean
|
|
815
847
|
requires?: string | string[]
|
|
@@ -819,16 +851,6 @@ export interface ActionBase<In, Out, ParsedIn = In> {
|
|
|
819
851
|
// — Execução —
|
|
820
852
|
handler: HandlerFn<ParsedIn, Out>
|
|
821
853
|
|
|
822
|
-
/**
|
|
823
|
-
* Handler alternativo executado quando `process.env.OPUS_MODE === 'design'`.
|
|
824
|
-
*
|
|
825
|
-
* @deprecated Desde a 22.1.0 (ADR 0037); sai na próxima major. No modo design, a action sem
|
|
826
|
-
* mock já responde com dado gerado a partir do `output`; quando o design precisar de uma resposta
|
|
827
|
-
* específica, declare um cenário fora do contrato com `defineDesignScenario`, de
|
|
828
|
-
* `@softize/opus/design`, e aponte a opção `scenarios` do `opusDesign()` para ele.
|
|
829
|
-
*/
|
|
830
|
-
mockHandler?: HandlerFn<ParsedIn, Out>
|
|
831
|
-
|
|
832
854
|
// — Comportamento —
|
|
833
855
|
audit?: boolean | AuditConfig
|
|
834
856
|
confirm?: ConfirmSpec
|
|
@@ -893,24 +915,13 @@ export interface SearchFilters {
|
|
|
893
915
|
* envelope — handler é fonte de verdade.
|
|
894
916
|
*/
|
|
895
917
|
export interface ListAction<In, Out, ParsedIn = In>
|
|
896
|
-
extends Omit<ActionBase<In, Out, ParsedIn>, 'handler'
|
|
918
|
+
extends Omit<ActionBase<In, Out, ParsedIn>, 'handler'> {
|
|
897
919
|
kind: 'list'
|
|
898
920
|
handler: (
|
|
899
921
|
ctx: ActionContext,
|
|
900
922
|
input: ParsedIn,
|
|
901
923
|
loaded?: Record<string, unknown>,
|
|
902
924
|
) => Paginated<Out> | Promise<Paginated<Out>>
|
|
903
|
-
/**
|
|
904
|
-
* Mesma semântica de `ActionBase.mockHandler`, mas devolve `Paginated<Out>`
|
|
905
|
-
* (search retorna lista paginada, não item solto).
|
|
906
|
-
*
|
|
907
|
-
* @deprecated Desde a 22.1.0 (ADR 0037); ver `ActionBase.mockHandler`.
|
|
908
|
-
*/
|
|
909
|
-
mockHandler?: (
|
|
910
|
-
ctx: ActionContext,
|
|
911
|
-
input: ParsedIn,
|
|
912
|
-
loaded?: Record<string, unknown>,
|
|
913
|
-
) => Paginated<Out> | Promise<Paginated<Out>>
|
|
914
925
|
/** Colunas declarativas da listagem (a UI deriva a tabela; ver ListColumnSpec). */
|
|
915
926
|
columns?: ListColumnSpec[]
|
|
916
927
|
filters?: Record<string, FilterSpec>
|
|
@@ -1301,12 +1312,23 @@ export interface AiTool {
|
|
|
1301
1312
|
title?: string
|
|
1302
1313
|
description: string
|
|
1303
1314
|
inputSchema: unknown
|
|
1315
|
+
/** Anotações padrão de ferramenta do MCP, derivadas do contrato (ADR 0041). */
|
|
1316
|
+
annotations?: {
|
|
1317
|
+
/** A ferramenta não altera estado (efeito `read`). */
|
|
1318
|
+
readOnlyHint?: boolean
|
|
1319
|
+
/** A ferramenta pode destruir dados (`ai.destructive`); só faz sentido quando não é leitura. */
|
|
1320
|
+
destructiveHint?: boolean
|
|
1321
|
+
}
|
|
1304
1322
|
/** Metadados de governança para o host; não fazem parte do prompt nem concedem acesso. */
|
|
1305
1323
|
metadata?: {
|
|
1306
1324
|
/** Identificadores técnicos dos Produtos de Dados relacionados. */
|
|
1307
1325
|
dataProducts?: string[]
|
|
1308
1326
|
/** Labels humanas por identificador; ausente para runtimes legados. */
|
|
1309
1327
|
dataProductLabels?: Record<string, string>
|
|
1328
|
+
/** Efeito resolvido; ausente quando `simple`/`form` não declaram. */
|
|
1329
|
+
effect?: ActionEffect
|
|
1330
|
+
/** A action declara devolver dado pessoal; ausente quando não declarou. */
|
|
1331
|
+
personalData?: boolean
|
|
1310
1332
|
}
|
|
1311
1333
|
}
|
|
1312
1334
|
|
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/mcp/index.ts
CHANGED
|
@@ -48,9 +48,15 @@ export function createOpusMcpServer(runtime: Runtime, opts: OpusMcpOptions = {})
|
|
|
48
48
|
...(t.title !== undefined ? { title: t.title } : {}),
|
|
49
49
|
description: t.description,
|
|
50
50
|
inputSchema: toJsonSchema(t.inputSchema) as { type: 'object' },
|
|
51
|
+
// Anotações padrão do MCP (ADR 0041): o cliente sabe se a ferramenta só lê.
|
|
52
|
+
...(t.annotations !== undefined ? { annotations: t.annotations } : {}),
|
|
51
53
|
_meta: {
|
|
52
54
|
'com.softize.opus/data-products': t.metadata?.dataProducts ?? [],
|
|
53
55
|
'com.softize.opus/data-product-labels': t.metadata?.dataProductLabels ?? {},
|
|
56
|
+
...(t.metadata?.effect !== undefined ? { 'com.softize.opus/effect': t.metadata.effect } : {}),
|
|
57
|
+
...(t.metadata?.personalData !== undefined
|
|
58
|
+
? { 'com.softize.opus/personal-data': t.metadata.personalData }
|
|
59
|
+
: {}),
|
|
54
60
|
},
|
|
55
61
|
})),
|
|
56
62
|
}))
|
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')
|
|
@@ -19,11 +19,9 @@ import { SurfaceMedia, type SurfaceMediaProps } from "./surface-media.tsx";
|
|
|
19
19
|
import {
|
|
20
20
|
SurfaceHeader,
|
|
21
21
|
surfaceHeaderClasses,
|
|
22
|
-
type SurfaceHeaderVariant,
|
|
23
22
|
} from "./surface-header.tsx";
|
|
24
23
|
|
|
25
24
|
export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
26
|
-
export type ContentHeaderVariant = SurfaceHeaderVariant;
|
|
27
25
|
export type ContentElement = "section" | "fieldset";
|
|
28
26
|
export type ContentFrameVariant = "default" | "legend";
|
|
29
27
|
|
|
@@ -34,7 +32,7 @@ interface ContentContextValue {
|
|
|
34
32
|
layout: SurfaceLayout;
|
|
35
33
|
level: ContentHeaderLevel;
|
|
36
34
|
titleId: string;
|
|
37
|
-
variant:
|
|
35
|
+
variant: "page" | "section";
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
const ContentContext = createContext<ContentContextValue | null>(null);
|
|
@@ -55,8 +53,6 @@ interface ContentSharedProps {
|
|
|
55
53
|
framed?: boolean;
|
|
56
54
|
/** Anatomia interna: contínua (default) ou com header separado do corpo. */
|
|
57
55
|
layout?: SurfaceLayout;
|
|
58
|
-
/** Hierarquia visual: `section` (default) ou `page`, a mesma de PageHeader. */
|
|
59
|
-
variant?: ContentHeaderVariant;
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
interface ContentSectionBaseProps
|
|
@@ -80,7 +76,6 @@ interface ContentFieldsetBaseProps
|
|
|
80
76
|
/** Tratamento visual da moldura: contínua (default) ou recortada pela legenda. */
|
|
81
77
|
frameVariant?: ContentFrameVariant;
|
|
82
78
|
level?: never;
|
|
83
|
-
variant?: "section";
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
interface ContentSectionShorthandProps extends ContentSectionBaseProps {
|
|
@@ -128,7 +123,6 @@ export function Content({
|
|
|
128
123
|
description,
|
|
129
124
|
actions,
|
|
130
125
|
level = 2,
|
|
131
|
-
variant = "section",
|
|
132
126
|
framed = false,
|
|
133
127
|
layout = "continuous",
|
|
134
128
|
frameVariant = "default",
|
|
@@ -138,7 +132,8 @@ export function Content({
|
|
|
138
132
|
}: ContentProps): ReactElement {
|
|
139
133
|
const titleId = useId();
|
|
140
134
|
const fill = useFillRegion();
|
|
141
|
-
const
|
|
135
|
+
const variant = level === 1 && as === "section" ? "page" : "section";
|
|
136
|
+
const context: ContentContextValue = {
|
|
142
137
|
element: as,
|
|
143
138
|
frameVariant,
|
|
144
139
|
framed,
|
|
@@ -245,7 +240,8 @@ export function Content({
|
|
|
245
240
|
{...fill.attributes}
|
|
246
241
|
aria-labelledby={ariaLabelledBy}
|
|
247
242
|
className={cn(
|
|
248
|
-
layout === "continuous" &&
|
|
243
|
+
layout === "continuous" &&
|
|
244
|
+
(variant === "page" ? "space-y-6" : "space-y-3"),
|
|
249
245
|
framed &&
|
|
250
246
|
layout === "continuous" &&
|
|
251
247
|
frameVariant === "default" &&
|
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
InputGroupButton,
|
|
112
112
|
InputGroupInput,
|
|
113
113
|
} from "../primitives/input-group.tsx";
|
|
114
|
+
import { iconPickerIcons } from "../primitives/icon-picker.tsx";
|
|
114
115
|
import { Separator } from "../primitives/separator.tsx";
|
|
115
116
|
import {
|
|
116
117
|
Field,
|
|
@@ -479,7 +480,23 @@ function dictOptionsFor(
|
|
|
479
480
|
if (spec.options?.kind !== "dictionary") return [];
|
|
480
481
|
const dict = dicts[spec.options.ref];
|
|
481
482
|
if (dict === undefined) return [];
|
|
482
|
-
return dict.options().map((o) =>
|
|
483
|
+
return dict.options().map((o) => {
|
|
484
|
+
const Icon = o.icon === undefined ? undefined : iconPickerIcons[o.icon];
|
|
485
|
+
return {
|
|
486
|
+
value: o.value,
|
|
487
|
+
label: o.label,
|
|
488
|
+
...(Icon === undefined
|
|
489
|
+
? {}
|
|
490
|
+
: {
|
|
491
|
+
content: (
|
|
492
|
+
<>
|
|
493
|
+
<Icon aria-hidden="true" />
|
|
494
|
+
<span>{o.label}</span>
|
|
495
|
+
</>
|
|
496
|
+
),
|
|
497
|
+
}),
|
|
498
|
+
};
|
|
499
|
+
});
|
|
483
500
|
}
|
|
484
501
|
|
|
485
502
|
/** Opções vindas de uma action de lookup (`options.kind: 'lookup'`): o Select chama
|
|
@@ -571,6 +588,43 @@ function FilterField({
|
|
|
571
588
|
const changedFromDefault = singleValue !== defaultValue;
|
|
572
589
|
const acceptsEmptyValue = spec.allowEmpty !== false;
|
|
573
590
|
|
|
591
|
+
if (
|
|
592
|
+
spec.type === "select" &&
|
|
593
|
+
spec.multiple !== true &&
|
|
594
|
+
spec.presentation === "toggle"
|
|
595
|
+
) {
|
|
596
|
+
const ALL = "__all__";
|
|
597
|
+
const toggleValue = singleValue === "" ? ALL : singleValue;
|
|
598
|
+
return (
|
|
599
|
+
<ToggleGroup
|
|
600
|
+
type="single"
|
|
601
|
+
variant="outline"
|
|
602
|
+
id={`filter-${name}`}
|
|
603
|
+
value={toggleValue}
|
|
604
|
+
onValueChange={(next) => {
|
|
605
|
+
// O grupo sempre preserva uma escolha ativa. Quando vazio é permitido,
|
|
606
|
+
// a opção explícita representa esse estado em vez de desmarcar o item atual.
|
|
607
|
+
if (next === "") return;
|
|
608
|
+
onChange(next === ALL ? "" : next);
|
|
609
|
+
}}
|
|
610
|
+
aria-label={label}
|
|
611
|
+
disabled={!enabled}
|
|
612
|
+
className={className ?? "max-w-full"}
|
|
613
|
+
>
|
|
614
|
+
{acceptsEmptyValue && (
|
|
615
|
+
<ToggleGroupItem value={ALL}>
|
|
616
|
+
{text(spec.placeholder, "Todos")}
|
|
617
|
+
</ToggleGroupItem>
|
|
618
|
+
)}
|
|
619
|
+
{base.map((option) => (
|
|
620
|
+
<ToggleGroupItem key={option.value} value={option.value}>
|
|
621
|
+
{option.content ?? option.label}
|
|
622
|
+
</ToggleGroupItem>
|
|
623
|
+
))}
|
|
624
|
+
</ToggleGroup>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
574
628
|
if (spec.multiple === true) {
|
|
575
629
|
return (
|
|
576
630
|
<Select
|
|
@@ -221,7 +221,7 @@ function PresentationFrame({
|
|
|
221
221
|
if (surface === "page") {
|
|
222
222
|
if (listBody) {
|
|
223
223
|
const content = (
|
|
224
|
-
<Content level={1}
|
|
224
|
+
<Content level={1}>
|
|
225
225
|
<ContentHeader>
|
|
226
226
|
<ContentTitle>{title}</ContentTitle>
|
|
227
227
|
{headerActions === undefined ? null : (
|
|
@@ -25,9 +25,12 @@ import {
|
|
|
25
25
|
Layers,
|
|
26
26
|
LayoutGrid,
|
|
27
27
|
Lightbulb,
|
|
28
|
+
Mail,
|
|
28
29
|
MapPin,
|
|
30
|
+
MessageCircle,
|
|
29
31
|
MessageSquare,
|
|
30
32
|
Package,
|
|
33
|
+
Phone,
|
|
31
34
|
Rocket,
|
|
32
35
|
Settings,
|
|
33
36
|
ShoppingCart,
|
|
@@ -77,9 +80,12 @@ export const iconPickerIcons: Record<string, LucideIcon> = {
|
|
|
77
80
|
'layers': Layers,
|
|
78
81
|
'layout-grid': LayoutGrid,
|
|
79
82
|
'lightbulb': Lightbulb,
|
|
83
|
+
'mail': Mail,
|
|
80
84
|
'map-pin': MapPin,
|
|
85
|
+
'message-circle': MessageCircle,
|
|
81
86
|
'message-square': MessageSquare,
|
|
82
87
|
'package': Package,
|
|
88
|
+
'phone': Phone,
|
|
83
89
|
'rocket': Rocket,
|
|
84
90
|
'settings': Settings,
|
|
85
91
|
'shopping-cart': ShoppingCart,
|