@softize/opus 18.1.1 → 18.3.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 +35 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +4 -3
- package/src/core/errors.ts +0 -1
- package/src/core/presentation.ts +143 -58
- package/src/core/types.ts +4 -0
- package/src/ui/components/patterns/data-state.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +606 -251
- package/src/ui/components/patterns/page.tsx +16 -5
- package/src/ui/components/patterns/presentation.tsx +228 -63
- package/src/ui/components/patterns/split.tsx +5 -2
- package/src/ui/components/patterns/surface-assistant.tsx +74 -0
- package/src/ui/components/primitives/field.tsx +23 -2
- package/src/ui/components/primitives/metric-card.tsx +64 -5
- package/src/ui/components/primitives/select.tsx +27 -23
- package/src/ui/components/primitives/table.tsx +4 -1
- package/src/ui/docs/content/action-list.md +84 -46
- package/src/ui/docs/content/field.md +6 -1
- package/src/ui/docs/content/metric-card.md +43 -0
- package/src/ui/docs/content/presentation.md +40 -14
- package/src/ui/docs/content/select.md +4 -0
- package/src/ui/docs/content/split.md +80 -19
- package/src/ui/meta.ts +2 -2
- package/src/ui/react.tsx +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,41 @@ 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
|
+
## 18.3.0 — 2026-09-17
|
|
11
|
+
|
|
12
|
+
`ActionList` passa a oferecer composição previsível para barras densas: filtros inline de valor
|
|
13
|
+
único abraçam o conteúdo até o limite da barra, a busca ocupa o espaço flexível restante, dropdowns
|
|
14
|
+
podem ser mais largos que o controle e filtros pesquisáveis podem omitir a opção vazia. Valores de
|
|
15
|
+
repouso, ícones por filtro, agrupamento de tipo de data com período e ícones nas seções avançadas
|
|
16
|
+
ficam declarados pelas props do pattern. A barra deixa de transformar filtros inline em chips por
|
|
17
|
+
falta de espaço; a página preserva sua largura mínima e delega o overflow à superfície correta.
|
|
18
|
+
|
|
19
|
+
Listagens podem usar `layout="fill"` para ocupar a altura disponível e manter a rolagem dentro da
|
|
20
|
+
tabela, com cabeçalho fixo e a mesma moldura nos estados vazio, carregando e erro. `Page` e
|
|
21
|
+
`Presentation` propagam esse contrato sem criar uma segunda rolagem no frame.
|
|
22
|
+
|
|
23
|
+
`MetricCard` aceita `secondaryMetric` para uma leitura complementar diretamente relacionada,
|
|
24
|
+
exibida como lâmina posterior com explicação própria. `FieldLegend` aceita ícone e a variante
|
|
25
|
+
`section`; o `Select` equaliza a ação de limpar com os demais campos e permite que sua lista cresça
|
|
26
|
+
além do gatilho sem alterar o layout.
|
|
27
|
+
|
|
28
|
+
## 18.2.0 — 2026-09-16
|
|
29
|
+
|
|
30
|
+
`Presentation` passa a aceitar um body serializável baseado em componente. O contrato continua
|
|
31
|
+
definindo rota, superfície, navegação e ações, enquanto a aplicação fornece o conteúdo React em
|
|
32
|
+
tempo de execução. Assim, uma página pode adotar a casca dirigida por spec sem precisar migrar de
|
|
33
|
+
uma vez toda a implementação interna para actions declarativas.
|
|
34
|
+
|
|
35
|
+
Recursos podem declarar um gatilho de assistência contextual em `assistant`. A aplicação fornece o
|
|
36
|
+
painel e os dados vivos do recurso por `assistant.render`; identidade, autorização e demais dados
|
|
37
|
+
não entram no manifest. O novo `SurfaceAssistant`, também disponível pela API pública, divide
|
|
38
|
+
somente o body de Page, Dialog ou Drawer. Cabeçalho e rodapé permanecem em toda a largura, e o
|
|
39
|
+
gatilho sai do cabeçalho enquanto o painel aberto assume a identidade visual da assistência.
|
|
40
|
+
|
|
41
|
+
O split contextual usa um único separador redimensionável, sem sobrepor uma segunda borda entre o
|
|
42
|
+
conteúdo e o painel. O conteúdo principal permanece montado durante a conversa e volta a ocupar
|
|
43
|
+
toda a largura quando ela é fechada.
|
|
44
|
+
|
|
10
45
|
## 18.1.1 — 2026-09-16
|
|
11
46
|
|
|
12
47
|
**Correção de segurança.** Quem executa tools de IA só roda o que `aiTools()` anunciou. Até aqui, o
|
package/package.json
CHANGED
|
@@ -37,8 +37,9 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
37
37
|
permanece sempre presente.
|
|
38
38
|
6. Deixar ausência, paginação e largura de filtro com o pattern: célula e `DetailField` já
|
|
39
39
|
representam valor ausente (`EmptyValue`, `empty` para o significado do domínio); listas
|
|
40
|
-
paginam pela primitiva `Pagination`; selects inline de
|
|
41
|
-
|
|
40
|
+
paginam pela primitiva `Pagination`; selects inline de valor único abraçam o valor até o limite
|
|
41
|
+
definido pela barra, e o menu pode ser mais largo que o controle. Não reescrever esses defaults
|
|
42
|
+
na tela.
|
|
42
43
|
7. Tratar página, filtro, seleção e modal importante como estado navegável por URL quando
|
|
43
44
|
o produto precisa de deep link, back/forward ou refresh. Em `Presentation`, declarar `route`
|
|
44
45
|
no artefato e deixar o adaptador apenas conectar a spec ao router da aplicação.
|
|
@@ -122,9 +122,10 @@ ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader`
|
|
|
122
122
|
- Paginação é a primitiva `Pagination`, inclusive dentro de `ActionList`. Número de página tem
|
|
123
123
|
largura mínima e cresce com os dígitos; só as setas são quadradas. Não montar paginador com
|
|
124
124
|
`Button` nem prender números a `size-*`.
|
|
125
|
-
- Select inline de
|
|
126
|
-
valor trunca no
|
|
127
|
-
|
|
125
|
+
- Select inline de valor único abraça o valor selecionado até `max-w-80`. Acima desse limite, o
|
|
126
|
+
valor trunca no gatilho e a opção inteira permanece na lista, que pode ser mais larga.
|
|
127
|
+
Multiseleção mantém `w-52`; o modal usa `w-full`. Não recalcular essa largura na tela nem
|
|
128
|
+
dimensioná-la pela label do filtro.
|
|
128
129
|
- Catálogo e API efetivos vêm dos exports da versão instalada, não de memória ou exemplo antigo.
|
|
129
130
|
- Em componentes semânticos, `context` responde por que há destaque e `variant` responde como ele
|
|
130
131
|
aparece. O vocabulário canônico é `neutral | primary | info | success | warning | danger`; as
|
package/src/core/errors.ts
CHANGED
|
@@ -93,7 +93,6 @@ class ActionErrorImpl extends Error implements ActionError {
|
|
|
93
93
|
readonly i18nParams?: Record<string, unknown>
|
|
94
94
|
readonly field?: string
|
|
95
95
|
readonly issues?: ValidationIssue[]
|
|
96
|
-
override readonly cause?: unknown
|
|
97
96
|
readonly meta?: Record<string, unknown>
|
|
98
97
|
|
|
99
98
|
constructor(input: ErrorInput) {
|
package/src/core/presentation.ts
CHANGED
|
@@ -90,7 +90,7 @@ const presentationFieldSchema = z
|
|
|
90
90
|
})
|
|
91
91
|
.strict();
|
|
92
92
|
|
|
93
|
-
const
|
|
93
|
+
const presentationActionBodySchema = z
|
|
94
94
|
.object({
|
|
95
95
|
action: z.string().min(1),
|
|
96
96
|
input: z.record(presentationBindingSchema).default({}),
|
|
@@ -108,6 +108,27 @@ const presentationBodySchema = z
|
|
|
108
108
|
})
|
|
109
109
|
.strict();
|
|
110
110
|
|
|
111
|
+
const presentationComponentBodySchema = z
|
|
112
|
+
.object({
|
|
113
|
+
/** Boundary estático do conteúdo React fornecido pelo consumidor. */
|
|
114
|
+
component: identifierSchema,
|
|
115
|
+
/** Define se o heading principal nasce na casca ou no conteúdo React. */
|
|
116
|
+
heading: z.enum(["shell", "content"]).default("shell"),
|
|
117
|
+
})
|
|
118
|
+
.strict();
|
|
119
|
+
|
|
120
|
+
const presentationBodySchema = z.union([
|
|
121
|
+
presentationActionBodySchema,
|
|
122
|
+
presentationComponentBodySchema,
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const presentationAssistantSchema = z
|
|
126
|
+
.object({
|
|
127
|
+
/** Nome acessível do botão que abre a assistência contextual. */
|
|
128
|
+
triggerLabel: z.string().min(1),
|
|
129
|
+
})
|
|
130
|
+
.strict();
|
|
131
|
+
|
|
111
132
|
const presentationRouteRecordSchema = z
|
|
112
133
|
.object({
|
|
113
134
|
presentation: identifierSchema,
|
|
@@ -136,6 +157,7 @@ export const presentationSchema = z
|
|
|
136
157
|
title: z.string().min(1),
|
|
137
158
|
route: presentationRouteSchema.optional(),
|
|
138
159
|
body: presentationBodySchema,
|
|
160
|
+
assistant: presentationAssistantSchema.optional(),
|
|
139
161
|
actions: z.array(presentationCommandSchema).default([]),
|
|
140
162
|
})
|
|
141
163
|
.strict();
|
|
@@ -143,6 +165,12 @@ export const presentationSchema = z
|
|
|
143
165
|
export type PresentationDefinition = z.infer<typeof presentationSchema>;
|
|
144
166
|
export type PresentationDefinitionInput = z.input<typeof presentationSchema>;
|
|
145
167
|
export type PresentationCommand = PresentationDefinition["actions"][number];
|
|
168
|
+
export type PresentationActionBody = z.infer<
|
|
169
|
+
typeof presentationActionBodySchema
|
|
170
|
+
>;
|
|
171
|
+
export type PresentationComponentBody = z.infer<
|
|
172
|
+
typeof presentationComponentBodySchema
|
|
173
|
+
>;
|
|
146
174
|
type DefaultedProperty<
|
|
147
175
|
Input,
|
|
148
176
|
Key extends PropertyKey,
|
|
@@ -174,27 +202,48 @@ type DefinedPresentationEffects<
|
|
|
174
202
|
}
|
|
175
203
|
: PresentationCommand["onSuccess"]
|
|
176
204
|
: PresentationCommand["onSuccess"];
|
|
177
|
-
type PresentationOpen = NonNullable<
|
|
205
|
+
type PresentationOpen = NonNullable<PresentationActionBody["open"]>;
|
|
178
206
|
type DefinedPresentationOpen<Open> = Open extends object
|
|
179
207
|
? Omit<Open, "input"> &
|
|
180
208
|
Omit<PresentationOpen, "input"> & {
|
|
181
209
|
input: DefaultedProperty<Open, "input", PresentationOpen["input"]>;
|
|
182
210
|
}
|
|
183
211
|
: never;
|
|
212
|
+
type DefinedPresentationActionBody<Body> =
|
|
213
|
+
Body extends z.input<typeof presentationActionBodySchema>
|
|
214
|
+
? Omit<Body, "input" | "open" | "onSuccess"> &
|
|
215
|
+
Omit<PresentationActionBody, "input" | "open" | "onSuccess"> & {
|
|
216
|
+
input: DefaultedProperty<
|
|
217
|
+
Body,
|
|
218
|
+
"input",
|
|
219
|
+
PresentationActionBody["input"]
|
|
220
|
+
>;
|
|
221
|
+
onSuccess: DefinedPresentationEffects<Body, "onSuccess">;
|
|
222
|
+
} & ("open" extends keyof Body
|
|
223
|
+
? undefined extends Body["open"]
|
|
224
|
+
? {
|
|
225
|
+
open?: DefinedPresentationOpen<
|
|
226
|
+
Exclude<Body["open"], undefined>
|
|
227
|
+
>;
|
|
228
|
+
}
|
|
229
|
+
: { open: DefinedPresentationOpen<Body["open"]> }
|
|
230
|
+
: { open?: PresentationActionBody["open"] })
|
|
231
|
+
: never;
|
|
232
|
+
type DefinedPresentationComponentBody<Body> =
|
|
233
|
+
Body extends z.input<typeof presentationComponentBodySchema>
|
|
234
|
+
? Omit<Body, "heading"> &
|
|
235
|
+
Omit<PresentationComponentBody, "heading"> & {
|
|
236
|
+
heading: DefaultedProperty<
|
|
237
|
+
Body,
|
|
238
|
+
"heading",
|
|
239
|
+
PresentationComponentBody["heading"]
|
|
240
|
+
>;
|
|
241
|
+
}
|
|
242
|
+
: never;
|
|
184
243
|
type DefinedPresentationBody<Body extends PresentationDefinitionInput["body"]> =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
Body,
|
|
189
|
-
"input",
|
|
190
|
-
PresentationDefinition["body"]["input"]
|
|
191
|
-
>;
|
|
192
|
-
onSuccess: DefinedPresentationEffects<Body, "onSuccess">;
|
|
193
|
-
} & ("open" extends keyof Body
|
|
194
|
-
? undefined extends Body["open"]
|
|
195
|
-
? { open?: DefinedPresentationOpen<Exclude<Body["open"], undefined>> }
|
|
196
|
-
: { open: DefinedPresentationOpen<Body["open"]> }
|
|
197
|
-
: { open?: PresentationDefinition["body"]["open"] });
|
|
244
|
+
Body extends { component: string }
|
|
245
|
+
? DefinedPresentationComponentBody<Body>
|
|
246
|
+
: DefinedPresentationActionBody<Body>;
|
|
198
247
|
type DefinedPresentationCommand<Command> = Command extends object
|
|
199
248
|
? Omit<Command, "input" | "blocking" | "onSuccess"> &
|
|
200
249
|
Omit<PresentationCommand, "input" | "blocking" | "onSuccess"> & {
|
|
@@ -272,6 +321,18 @@ export interface PresentationActionRegistry {
|
|
|
272
321
|
readonly [name: string]: ActionContract;
|
|
273
322
|
}
|
|
274
323
|
|
|
324
|
+
export function isPresentationActionBody(
|
|
325
|
+
body: PresentationDefinition["body"],
|
|
326
|
+
): body is PresentationActionBody {
|
|
327
|
+
return "action" in body;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function isPresentationComponentBody(
|
|
331
|
+
body: PresentationDefinition["body"],
|
|
332
|
+
): body is PresentationComponentBody {
|
|
333
|
+
return "component" in body;
|
|
334
|
+
}
|
|
335
|
+
|
|
275
336
|
export interface PresentationRouteMatch {
|
|
276
337
|
definition: PresentationDefinition;
|
|
277
338
|
input: Record<string, string>;
|
|
@@ -312,8 +373,7 @@ export function matchPresentationRoute(
|
|
|
312
373
|
const decoded = decodeRouteSegment(received);
|
|
313
374
|
if (decoded === null) return [];
|
|
314
375
|
input[expected.slice(1)] = decoded;
|
|
315
|
-
}
|
|
316
|
-
else if (expected !== received) return [];
|
|
376
|
+
} else if (expected !== received) return [];
|
|
317
377
|
}
|
|
318
378
|
return [{ definition, input }];
|
|
319
379
|
});
|
|
@@ -527,46 +587,53 @@ export function validatePresentations(
|
|
|
527
587
|
problems.push(`Presentation “${presentation.id}” duplicada.`);
|
|
528
588
|
byId.set(presentation.id, presentation);
|
|
529
589
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
problems.push(
|
|
533
|
-
`A action “${presentation.body.action}” não está disponível.`,
|
|
534
|
-
);
|
|
535
|
-
} else if (bodyAction.kind === "simple") {
|
|
536
|
-
problems.push(
|
|
537
|
-
`A action simple “${bodyAction.name}” não pode ocupar o body.`,
|
|
538
|
-
);
|
|
590
|
+
if (isPresentationComponentBody(presentation.body)) {
|
|
591
|
+
// O conteúdo interno permanece uma fronteira React opaca para a spec da casca.
|
|
539
592
|
} else {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
bodyAction,
|
|
543
|
-
`${presentation.id}.body.input`,
|
|
544
|
-
problems,
|
|
545
|
-
);
|
|
546
|
-
if (
|
|
547
|
-
bodyAction.kind === "view" &&
|
|
548
|
-
presentation.body.fields === undefined
|
|
549
|
-
) {
|
|
593
|
+
const bodyAction = actions[presentation.body.action];
|
|
594
|
+
if (bodyAction === undefined) {
|
|
550
595
|
problems.push(
|
|
551
|
-
`A
|
|
596
|
+
`A action “${presentation.body.action}” não está disponível.`,
|
|
552
597
|
);
|
|
553
|
-
}
|
|
554
|
-
if (
|
|
555
|
-
bodyAction.kind !== "view" &&
|
|
556
|
-
presentation.body.fields !== undefined
|
|
557
|
-
) {
|
|
558
|
-
problems.push(`Fields só podem ser declarados para uma action view.`);
|
|
559
|
-
}
|
|
560
|
-
if (bodyAction.kind !== "list" && presentation.body.open !== undefined) {
|
|
561
|
-
problems.push(`Open só pode ser declarado para uma action list.`);
|
|
562
|
-
}
|
|
563
|
-
if (
|
|
564
|
-
bodyAction.kind !== "form" &&
|
|
565
|
-
presentation.body.submitLabel !== undefined
|
|
566
|
-
) {
|
|
598
|
+
} else if (bodyAction.kind === "simple") {
|
|
567
599
|
problems.push(
|
|
568
|
-
`
|
|
600
|
+
`A action simple “${bodyAction.name}” não pode ocupar o body.`,
|
|
601
|
+
);
|
|
602
|
+
} else {
|
|
603
|
+
validateActionBindings(
|
|
604
|
+
presentation.body.input,
|
|
605
|
+
bodyAction,
|
|
606
|
+
`${presentation.id}.body.input`,
|
|
607
|
+
problems,
|
|
569
608
|
);
|
|
609
|
+
if (
|
|
610
|
+
bodyAction.kind === "view" &&
|
|
611
|
+
presentation.body.fields === undefined
|
|
612
|
+
) {
|
|
613
|
+
problems.push(
|
|
614
|
+
`A Presentation “${presentation.id}” precisa declarar fields para a view.`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (
|
|
618
|
+
bodyAction.kind !== "view" &&
|
|
619
|
+
presentation.body.fields !== undefined
|
|
620
|
+
) {
|
|
621
|
+
problems.push(`Fields só podem ser declarados para uma action view.`);
|
|
622
|
+
}
|
|
623
|
+
if (
|
|
624
|
+
bodyAction.kind !== "list" &&
|
|
625
|
+
presentation.body.open !== undefined
|
|
626
|
+
) {
|
|
627
|
+
problems.push(`Open só pode ser declarado para uma action list.`);
|
|
628
|
+
}
|
|
629
|
+
if (
|
|
630
|
+
bodyAction.kind !== "form" &&
|
|
631
|
+
presentation.body.submitLabel !== undefined
|
|
632
|
+
) {
|
|
633
|
+
problems.push(
|
|
634
|
+
`SubmitLabel só pode ser declarado para uma action form.`,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
570
637
|
}
|
|
571
638
|
}
|
|
572
639
|
|
|
@@ -615,7 +682,10 @@ export function validatePresentations(
|
|
|
615
682
|
problems.push(
|
|
616
683
|
`A route de “${presentation.id}” consulta a Presentation inexistente “${record.presentation}”.`,
|
|
617
684
|
);
|
|
618
|
-
} else if (
|
|
685
|
+
} else if (
|
|
686
|
+
!isPresentationActionBody(source.body) ||
|
|
687
|
+
actions[source.body.action]?.kind !== "list"
|
|
688
|
+
) {
|
|
619
689
|
problems.push(
|
|
620
690
|
`A route de “${presentation.id}” exige que “${record.presentation}” tenha uma list action no body.`,
|
|
621
691
|
);
|
|
@@ -633,7 +703,9 @@ export function validatePresentations(
|
|
|
633
703
|
}
|
|
634
704
|
}
|
|
635
705
|
}
|
|
636
|
-
const open = presentation.body
|
|
706
|
+
const open = isPresentationActionBody(presentation.body)
|
|
707
|
+
? presentation.body.open
|
|
708
|
+
: undefined;
|
|
637
709
|
if (open !== undefined && !byId.has(open.presentation)) {
|
|
638
710
|
problems.push(
|
|
639
711
|
`A lista “${presentation.id}” abre a Presentation inexistente “${open.presentation}”.`,
|
|
@@ -650,7 +722,9 @@ export function validatePresentations(
|
|
|
650
722
|
);
|
|
651
723
|
}
|
|
652
724
|
const action =
|
|
653
|
-
target
|
|
725
|
+
target !== undefined && isPresentationActionBody(target.body)
|
|
726
|
+
? actions[target.body.action]
|
|
727
|
+
: undefined;
|
|
654
728
|
if (action !== undefined) {
|
|
655
729
|
validateActionBindings(
|
|
656
730
|
open.input,
|
|
@@ -660,7 +734,10 @@ export function validatePresentations(
|
|
|
660
734
|
);
|
|
661
735
|
}
|
|
662
736
|
}
|
|
663
|
-
|
|
737
|
+
const bodyEffects = isPresentationActionBody(presentation.body)
|
|
738
|
+
? presentation.body.onSuccess
|
|
739
|
+
: [];
|
|
740
|
+
for (const effect of bodyEffects) {
|
|
664
741
|
if (effect.effect !== "navigate") continue;
|
|
665
742
|
const target = byId.get(effect.presentation);
|
|
666
743
|
if (target === undefined) {
|
|
@@ -678,7 +755,9 @@ export function validatePresentations(
|
|
|
678
755
|
`O body de “${presentation.id}” navega para “${target.id}” como ${effect.surface}, mas sua route declara ${target.route.surface}.`,
|
|
679
756
|
);
|
|
680
757
|
}
|
|
681
|
-
const action =
|
|
758
|
+
const action = isPresentationActionBody(target.body)
|
|
759
|
+
? actions[target.body.action]
|
|
760
|
+
: undefined;
|
|
682
761
|
if (action !== undefined) {
|
|
683
762
|
validateActionBindings(
|
|
684
763
|
effect.input,
|
|
@@ -706,7 +785,9 @@ export function validatePresentations(
|
|
|
706
785
|
`A action “${command.action}” navega para “${target.id}” como ${effect.surface}, mas sua route declara ${target.route.surface}.`,
|
|
707
786
|
);
|
|
708
787
|
}
|
|
709
|
-
const action =
|
|
788
|
+
const action = isPresentationActionBody(target.body)
|
|
789
|
+
? actions[target.body.action]
|
|
790
|
+
: undefined;
|
|
710
791
|
if (action !== undefined) {
|
|
711
792
|
validateActionBindings(
|
|
712
793
|
effect.input,
|
|
@@ -724,6 +805,10 @@ export function validatePresentations(
|
|
|
724
805
|
problems.push(
|
|
725
806
|
`A action “${command.action}” abre a Presentation inexistente “${command.target.presentation}”.`,
|
|
726
807
|
);
|
|
808
|
+
} else if (!isPresentationActionBody(target.body)) {
|
|
809
|
+
problems.push(
|
|
810
|
+
`A Presentation “${target.id}” usa conteúdo de componente e não pode ser aberta pela action “${command.action}”.`,
|
|
811
|
+
);
|
|
727
812
|
} else if (target.body.action !== command.action) {
|
|
728
813
|
problems.push(
|
|
729
814
|
`A Presentation “${target.id}” usa “${target.body.action}”, mas foi aberta por “${command.action}”.`,
|
package/src/core/types.ts
CHANGED
|
@@ -598,6 +598,10 @@ export interface FilterSpec {
|
|
|
598
598
|
|
|
599
599
|
type: 'text' | 'select' | 'lookup' | 'date' | 'number'
|
|
600
600
|
multiple?: boolean
|
|
601
|
+
/** Habilita busca local nas opções de um `select`; `lookup` já é pesquisável por natureza. */
|
|
602
|
+
searchable?: boolean
|
|
603
|
+
/** Quando `false`, a interface omite a opção vazia. Limpar restaura o default do filtro. */
|
|
604
|
+
allowEmpty?: boolean
|
|
601
605
|
mode?: 'server' | 'client'
|
|
602
606
|
operators?: FilterOp[]
|
|
603
607
|
path?: string
|
|
@@ -75,7 +75,7 @@ export function DataState({
|
|
|
75
75
|
if (colSpan !== undefined) {
|
|
76
76
|
return (
|
|
77
77
|
<tr data-slot="data-state" data-status={status}>
|
|
78
|
-
<td colSpan={colSpan} className="px-4 py-4 text-center text-sm">
|
|
78
|
+
<td colSpan={colSpan} className="px-4 py-4 text-center align-middle text-sm">
|
|
79
79
|
{surface}
|
|
80
80
|
</td>
|
|
81
81
|
</tr>
|