@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/bin/cli.mjs +6 -5
  3. package/bin/lib/check.mjs +23 -8
  4. package/bin/lib/copy.mjs +1 -1
  5. package/bin/lib/gen-manifest.mjs +2 -0
  6. package/bin/lib/gen-runner.mjs +7 -0
  7. package/bin/lib/gen-stubs.mjs +3 -1
  8. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
  9. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
  10. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
  11. package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
  12. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  13. package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
  14. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
  15. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +60 -0
  16. package/docs/adr/0041-actions-declare-effect-and-personal-data.md +139 -0
  17. package/docs/code-style.md +1 -1
  18. package/docs/protocol.md +15 -2
  19. package/package.json +1 -1
  20. package/registry/skills/build-opus-ui/references/ui-patterns.md +1 -1
  21. package/registry/skills/create-opus-action/SKILL.md +7 -2
  22. package/registry/skills/create-opus-action/references/contract-and-binding.md +10 -5
  23. package/registry/skills/create-opus-action/references/evaluations.md +3 -0
  24. package/registry/skills/create-opus-action/scripts/scaffold.mjs +6 -0
  25. package/registry/templates/app/src/domains/tasks/actions/list.ts +2 -0
  26. package/src/core/action-nature.ts +40 -0
  27. package/src/core/audit.ts +12 -4
  28. package/src/core/contracts.ts +1 -1
  29. package/src/core/index.ts +2 -0
  30. package/src/core/runtime.ts +57 -35
  31. package/src/core/types.ts +45 -23
  32. package/src/design/index.ts +14 -3
  33. package/src/mcp/index.ts +6 -0
  34. package/src/testing/index.ts +6 -26
  35. package/src/ui/components/patterns/content-header.tsx +5 -9
  36. package/src/ui/components/patterns/list.tsx +55 -1
  37. package/src/ui/components/patterns/presentation.tsx +1 -1
  38. package/src/ui/components/primitives/icon-picker.tsx +6 -0
  39. package/src/ui/components/primitives/tabs.tsx +55 -87
  40. package/src/ui/components/primitives/toggle.tsx +1 -1
  41. package/src/ui/docs/content/action-list.md +13 -4
  42. package/src/ui/docs/content/actions.md +36 -8
  43. package/src/ui/docs/content/content.md +5 -6
  44. package/src/ui/docs/content/mcp.md +6 -0
  45. package/src/ui/docs/content/semantic-context.md +0 -1
  46. package/src/ui/docs/content/tabs.md +68 -69
  47. package/src/ui/docs/content/testing.md +2 -3
  48. package/src/ui/docs/content/toggle.md +15 -10
  49. package/src/ui/drivers/react.tsx +4 -3
  50. package/src/ui/meta.ts +2 -2
  51. package/src/ui/react.tsx +2 -3
  52. 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
- * `mockHandler`, `authorize` e `loads` recebem `ParsedIn`.
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' | 'mockHandler'> {
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
 
@@ -9,7 +9,7 @@
9
9
  * respostas ao runtime de design.
10
10
  */
11
11
 
12
- import { z } from 'zod'
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 zod não tem gerador: a resposta é `null`.
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
- if (!(output instanceof z.ZodType)) return null
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
  }))
@@ -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
- // Import direto do runtime (fora do index público): a régua handler×mockHandler
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: contrato puro (sem handler) só executa pelo `mockHandler` obsoleto. */
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
- * A escolha do executável segue a régua do `execute()`: em `OPUS_MODE=design`,
282
- * `mockHandler` presente vence; senão, `handler` real. Contrato puro (sem
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
- const handlerToRun =
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 handlerToRun(ctx, parsed, loaded)
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: ContentHeaderVariant;
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 context = {
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" && "space-y-3",
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) => ({ value: o.value, label: o.label }));
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} variant="page">
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,