@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.
Files changed (115) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/bin/cli.mjs +3 -9
  3. package/bin/lib/check.mjs +242 -49
  4. package/bin/lib/copy.mjs +17 -35
  5. package/bin/lib/gen-runner.mjs +2 -0
  6. package/bin/lib/gen-stubs.mjs +3 -1
  7. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  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 +59 -0
  16. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  17. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  18. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  19. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  20. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  21. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  22. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  23. package/docs/code-style.md +1 -1
  24. package/package.json +5 -1
  25. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  26. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  27. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  28. package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
  29. package/registry/templates/app/src/App.tsx +24 -12
  30. package/src/core/contracts.ts +1 -1
  31. package/src/core/runtime.ts +4 -37
  32. package/src/core/types.ts +5 -23
  33. package/src/design/index.ts +14 -3
  34. package/src/testing/index.ts +6 -26
  35. package/src/ui/appearance.ts +149 -0
  36. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  37. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  38. package/src/ui/components/patterns/confirm.tsx +3 -1
  39. package/src/ui/components/patterns/fill-region.ts +8 -7
  40. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  41. package/src/ui/components/patterns/form.tsx +8 -7
  42. package/src/ui/components/patterns/list.tsx +64 -14
  43. package/src/ui/components/patterns/page.tsx +15 -11
  44. package/src/ui/components/patterns/pane.tsx +9 -240
  45. package/src/ui/components/patterns/presentation.tsx +14 -14
  46. package/src/ui/components/patterns/sidebar.tsx +7 -17
  47. package/src/ui/components/patterns/split.tsx +111 -59
  48. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  49. package/src/ui/components/patterns/structural-media.tsx +26 -0
  50. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  51. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  52. package/src/ui/components/patterns/surface.tsx +620 -0
  53. package/src/ui/components/patterns/trigger.tsx +11 -9
  54. package/src/ui/components/primitives/ask.tsx +155 -83
  55. package/src/ui/components/primitives/button.tsx +11 -9
  56. package/src/ui/components/primitives/checkbox.tsx +8 -8
  57. package/src/ui/components/primitives/command.tsx +1 -1
  58. package/src/ui/components/primitives/composer.tsx +47 -45
  59. package/src/ui/components/primitives/detail.tsx +9 -2
  60. package/src/ui/components/primitives/dialog.tsx +60 -26
  61. package/src/ui/components/primitives/drawer.tsx +58 -12
  62. package/src/ui/components/primitives/icon-picker.tsx +93 -70
  63. package/src/ui/components/primitives/input-group.tsx +35 -28
  64. package/src/ui/components/primitives/input-otp.tsx +16 -16
  65. package/src/ui/components/primitives/input.tsx +20 -10
  66. package/src/ui/components/primitives/item.tsx +9 -12
  67. package/src/ui/components/primitives/menu.tsx +44 -65
  68. package/src/ui/components/primitives/metric-card.tsx +75 -59
  69. package/src/ui/components/primitives/radio-group.tsx +9 -9
  70. package/src/ui/components/primitives/select.tsx +344 -209
  71. package/src/ui/components/primitives/switch.tsx +10 -10
  72. package/src/ui/components/primitives/table.tsx +29 -24
  73. package/src/ui/components/primitives/tabs.tsx +58 -88
  74. package/src/ui/components/primitives/textarea.tsx +6 -6
  75. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  76. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  77. package/src/ui/components/primitives/toggle.tsx +82 -16
  78. package/src/ui/docs/DocBrowser.tsx +77 -69
  79. package/src/ui/docs/content/action-form-card.md +9 -9
  80. package/src/ui/docs/content/action-list.md +85 -70
  81. package/src/ui/docs/content/action-view.md +33 -18
  82. package/src/ui/docs/content/actions.md +6 -7
  83. package/src/ui/docs/content/button.md +1 -1
  84. package/src/ui/docs/content/communication.md +1 -1
  85. package/src/ui/docs/content/customization.md +64 -11
  86. package/src/ui/docs/content/detail.md +8 -6
  87. package/src/ui/docs/content/dialog.md +100 -58
  88. package/src/ui/docs/content/drawer.md +63 -19
  89. package/src/ui/docs/content/field.md +14 -15
  90. package/src/ui/docs/content/icon-tile.md +14 -13
  91. package/src/ui/docs/content/page.md +34 -26
  92. package/src/ui/docs/content/pane.md +41 -290
  93. package/src/ui/docs/content/presentation.md +3 -3
  94. package/src/ui/docs/content/semantic-context.md +0 -1
  95. package/src/ui/docs/content/separator.md +6 -6
  96. package/src/ui/docs/content/sidebar.md +68 -266
  97. package/src/ui/docs/content/skeleton.md +20 -18
  98. package/src/ui/docs/content/split.md +14 -14
  99. package/src/ui/docs/content/surface.md +513 -0
  100. package/src/ui/docs/content/tabs.md +82 -83
  101. package/src/ui/docs/content/testing.md +2 -3
  102. package/src/ui/docs/content/toggle.md +120 -63
  103. package/src/ui/docs/content/tokens.md +124 -40
  104. package/src/ui/docs/registry.tsx +4 -6
  105. package/src/ui/drivers/react.tsx +4 -3
  106. package/src/ui/index.ts +30 -27
  107. package/src/ui/meta.ts +15 -27
  108. package/src/ui/react.tsx +42 -46
  109. package/src/ui/theme.css +311 -64
  110. package/src/vite/design.ts +23 -21
  111. package/src/ui/components/patterns/content-header.tsx +0 -532
  112. package/src/ui/components/patterns/surface-media.tsx +0 -69
  113. package/src/ui/components/primitives/card.tsx +0 -191
  114. package/src/ui/docs/content/card.md +0 -292
  115. package/src/ui/docs/content/content.md +0 -276
@@ -1,19 +1,19 @@
1
1
  /**
2
- * <ActionFormCard /> — o ActionForm dentro de um Card do Opus (header / conteúdo / rodapé),
3
- * pra estruturar uma seção da página como painel. Segue o padrão do CARD (não a chrome de
4
- * modal): sem divisor nem faixa, com o respiro do Card. Pra form em overlay, use o
5
- * ActionFormDialog; pra form cru (sem chrome), o ActionForm direto.
2
+ * <ActionFormCard /> — o ActionForm dentro de uma Surface elevada (header / conteúdo /
3
+ * rodapé), pra estruturar uma seção da página como painel. Segue o layout contínuo (não a
4
+ * chrome de modal): sem divisor nem faixa. Pra form em overlay, use o ActionFormDialog; pra
5
+ * form cru (sem chrome), o ActionForm direto.
6
6
  */
7
- import {
8
- Card,
9
- CardBody,
10
- CardDescription,
11
- CardFooter,
12
- CardHeader,
13
- CardTitle,
14
- } from "../primitives/card.tsx";
15
7
  import { ActionForm, type ActionFormProps } from "./form.tsx";
16
8
  import type { HeadingLevel } from "./heading-level.ts";
9
+ import {
10
+ Surface,
11
+ SurfaceBody,
12
+ SurfaceDescription,
13
+ SurfaceFooter,
14
+ SurfaceHeader,
15
+ SurfaceTitle,
16
+ } from "./surface.tsx";
17
17
  import { ButtonGroup } from "../primitives/button-group.tsx";
18
18
 
19
19
  export interface ActionFormCardProps<
@@ -23,49 +23,62 @@ export interface ActionFormCardProps<
23
23
  /** Título do header do card. Sem ele, o card começa direto no conteúdo. */
24
24
  title?: string;
25
25
  /**
26
- * Nível semântico do título (`h1`–`h6`), repassado ao `CardTitle` como `level`. Sem
27
- * ele vale o `h3` do Card. Muda só a hierarquia do documento; a escala visual continua
28
- * a do Card.
26
+ * Nível semântico do título (`h1`–`h6`), repassado à `Surface` como `level`. Sem ele
27
+ * vale `h3`: o card costuma ser uma seção dentro de uma página que já tem `h1` e `h2`.
29
28
  */
30
29
  titleLevel?: HeadingLevel;
31
- /** Subtítulo opcional, abaixo do título. */
30
+ /** Subtítulo opcional, abaixo do título. Exige `title`. */
32
31
  description?: string;
33
- /** Classes do Card (a superfície). `className` vai pro <form>. */
32
+ /** Classes da superfície. `className` vai pro <form>. */
34
33
  cardClassName?: string;
35
34
  }
36
35
 
37
36
  export function ActionFormCard<TInput extends Record<string, unknown>, TData>({
38
37
  title,
39
- titleLevel,
38
+ titleLevel = 3,
40
39
  description,
41
40
  cardClassName,
42
41
  ...rest
43
42
  }: ActionFormCardProps<TInput, TData>) {
43
+ if (description !== undefined && title === undefined) {
44
+ throw new Error(
45
+ "ActionFormCard exige title quando description é informada.",
46
+ );
47
+ }
44
48
  return (
45
- <Card className={cardClassName}>
46
- {(title !== undefined || description !== undefined) && (
47
- <CardHeader>
48
- {title !== undefined && (
49
- <CardTitle level={titleLevel}>{title}</CardTitle>
50
- )}
51
- {description !== undefined && (
52
- <CardDescription>{description}</CardDescription>
53
- )}
54
- </CardHeader>
55
- )}
49
+ <Surface
50
+ framed
51
+ elevation="raised"
52
+ level={titleLevel}
53
+ className={cardClassName}
54
+ >
55
+ {/* As três regiões são irmãs dentro do <form>: o ritmo e o respiro de cada uma
56
+ derivam da vizinhança, e os botões precisam ficar dentro do form. */}
56
57
  <ActionForm
57
58
  {...rest}
58
- body={(fields) => <CardBody>{fields}</CardBody>}
59
+ body={(fields) => (
60
+ <>
61
+ {title !== undefined && (
62
+ <SurfaceHeader>
63
+ <SurfaceTitle>{title}</SurfaceTitle>
64
+ {description !== undefined && (
65
+ <SurfaceDescription>{description}</SurfaceDescription>
66
+ )}
67
+ </SurfaceHeader>
68
+ )}
69
+ <SurfaceBody>{fields}</SurfaceBody>
70
+ </>
71
+ )}
59
72
  footer={(actions) => (
60
- <CardFooter className="justify-end">
73
+ <SurfaceFooter>
61
74
  {rest.onCancel === undefined ? (
62
75
  actions
63
76
  ) : (
64
77
  <ButtonGroup mode="spaced">{actions}</ButtonGroup>
65
78
  )}
66
- </CardFooter>
79
+ </SurfaceFooter>
67
80
  )}
68
81
  />
69
- </Card>
82
+ </Surface>
70
83
  );
71
84
  }
@@ -86,14 +86,12 @@ export function ActionListDialog<
86
86
  </DialogHeader>
87
87
  <DialogBody>
88
88
  {intro !== undefined && (
89
- <div id={introId} className="mb-4">
90
- {intro}
91
- </div>
89
+ <div id={introId}>{intro}</div>
92
90
  )}
93
91
  {(note !== undefined || actions !== undefined) && (
94
92
  <div
95
93
  data-slot="action-list-dialog-toolbar"
96
- className="mb-2 flex min-h-8 items-center justify-between gap-4"
94
+ className="flex min-h-8 items-center justify-between gap-4"
97
95
  >
98
96
  <div className="min-w-0 text-xs text-muted-foreground/70">{note}</div>
99
97
  {actions}
@@ -284,7 +284,9 @@ export function DialogHost(): React.ReactElement {
284
284
  <DialogTitle>{current.title}</DialogTitle>
285
285
  </DialogHeader>
286
286
  {current.body !== undefined && (
287
- <DialogBody id={bodyId}>{current.body}</DialogBody>
287
+ <DialogBody id={bodyId}>
288
+ <div>{current.body}</div>
289
+ </DialogBody>
288
290
  )}
289
291
  {current.kind === "prompt" && (
290
292
  <Input
@@ -7,15 +7,16 @@
7
7
  * coluna flexível. Este módulo faz essa cadeia se formar sem classes no consumidor:
8
8
  *
9
9
  * - a raiz (`FillRoot`) publica o contexto;
10
- * - os elos canônicos (`PageBody`, `Content`, `ContentBody`, o corpo da `Presentation`)
11
- * leem o contexto com `useFillRegion` e o republicam para os filhos;
10
+ * - os elos canônicos (`PageBody`, `Surface` sem elevação, `SurfaceBody`, o corpo da
11
+ * `Presentation`) leem o contexto com `useFillRegion` e o republicam para os filhos;
12
12
  * - a folha (`ActionList`) usa `useFillLeaf`: assume `fill` por padrão quando o elemento
13
13
  * pai é um elo e se registra na cadeia;
14
- * - um elo só vira coluna flexível quando uma folha se registrou abaixo dele. Irmãos sem
15
- * folha mantêm a altura do próprio conteúdo, e uma página `fill` sem lista não muda;
16
- * - superfícies com anatomia própria (Card, Dialog, Drawer, Pane) cortam a cadeia com
17
- * `FillBoundary`: a altura delas é decisão delas, inclusive quando um portal as renderiza
18
- * dentro de uma página `fill`.
14
+ * - os bodies estruturais já são colunas para governar o ritmo; quando uma folha se registra,
15
+ * os elos também passam a ocupar a folga disponível. Irmãos sem folha mantêm a altura do
16
+ * próprio conteúdo, e uma página `fill` sem lista não muda;
17
+ * - superfícies com altura própria (`Surface elevation="raised"`, Dialog, Drawer, Pane)
18
+ * cortam a cadeia com `FillBoundary`: a altura delas é decisão delas, inclusive quando um
19
+ * portal as renderiza dentro de uma página `fill`.
19
20
  *
20
21
  * O registro acontece em `useLayoutEffect`, então as classes chegam antes da primeira pintura.
21
22
  */
@@ -66,9 +66,7 @@ export function ActionFormDialog<
66
66
  body={(fields) => (
67
67
  <DialogBody>
68
68
  {intro !== undefined && (
69
- <div id={introId} className="mb-4">
70
- {intro}
71
- </div>
69
+ <div id={introId}>{intro}</div>
72
70
  )}
73
71
  {fields}
74
72
  </DialogBody>
@@ -49,7 +49,8 @@ import {
49
49
  } from "../primitives/tooltip.tsx";
50
50
  import { Info } from "lucide-react";
51
51
  import { Select, type SelectOption } from "../primitives/select.tsx";
52
- import { ToggleGroup, ToggleGroupItem } from "../primitives/toggle-group.tsx";
52
+ import { Toggle } from "../primitives/toggle.tsx";
53
+ import { ToggleGroup } from "../primitives/toggle-group.tsx";
53
54
  import {
54
55
  Field,
55
56
  FieldError,
@@ -464,14 +465,14 @@ export function ActionFormField({
464
465
  value={(formValues[name] as string[] | undefined) ?? []}
465
466
  onValueChange={(value) => setValue(value)}
466
467
  variant="outline"
467
- spacing={2}
468
+ mode="spaced"
468
469
  aria-labelledby={`${name}-label`}
469
470
  aria-invalid={errorMessage !== undefined}
470
471
  aria-describedby={describedBy}
471
472
  className="grid w-full grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))] items-stretch gap-2"
472
473
  >
473
474
  {options.map((option) => (
474
- <ToggleGroupItem
475
+ <Toggle
475
476
  key={option.value}
476
477
  value={option.value}
477
478
  disabled={option.disabled}
@@ -488,7 +489,7 @@ export function ActionFormField({
488
489
  </span>
489
490
  )}
490
491
  </span>
491
- </ToggleGroupItem>
492
+ </Toggle>
492
493
  ))}
493
494
  </ToggleGroup>
494
495
  ) : (
@@ -502,7 +503,7 @@ export function ActionFormField({
502
503
  }
503
504
  }}
504
505
  variant="outline"
505
- spacing={2}
506
+ mode="spaced"
506
507
  aria-labelledby={`${name}-label`}
507
508
  aria-required={effectiveKind.required}
508
509
  aria-invalid={errorMessage !== undefined}
@@ -510,7 +511,7 @@ export function ActionFormField({
510
511
  className="grid w-full grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))] items-stretch gap-2"
511
512
  >
512
513
  {options.map((option) => (
513
- <ToggleGroupItem
514
+ <Toggle
514
515
  key={option.value}
515
516
  value={option.value}
516
517
  disabled={option.disabled}
@@ -527,7 +528,7 @@ export function ActionFormField({
527
528
  </span>
528
529
  )}
529
530
  </span>
530
- </ToggleGroupItem>
531
+ </Toggle>
531
532
  ))}
532
533
  </ToggleGroup>
533
534
  )
@@ -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,
@@ -118,7 +119,7 @@ import {
118
119
  FieldLabel,
119
120
  type FieldSetVariant,
120
121
  } from "../primitives/field.tsx";
121
- import { Content } from "./content-header.tsx";
122
+ import { Surface } from "./surface.tsx";
122
123
  import { Search } from "lucide-react";
123
124
  import { Select, type SelectOption } from "../primitives/select.tsx";
124
125
  import {
@@ -129,7 +130,8 @@ import {
129
130
  TableHeader,
130
131
  TableRow,
131
132
  } from "../primitives/table.tsx";
132
- import { ToggleGroup, ToggleGroupItem } from "../primitives/toggle-group.tsx";
133
+ import { Toggle } from "../primitives/toggle.tsx";
134
+ import { ToggleGroup } from "../primitives/toggle-group.tsx";
133
135
  import {
134
136
  Tooltip,
135
137
  TooltipContent,
@@ -347,7 +349,7 @@ export interface ActionListProps<TInput, TItem> {
347
349
  /**
348
350
  * `fill` ocupa a altura disponível e mantém a rolagem dentro da região de resultados.
349
351
  * Sem valor, vale `fill` quando o elemento pai repassa a altura de uma `Page` ou
350
- * `Presentation` em `fill` — `PageBody`, `ContentBody` ou o corpo da Presentation — e
352
+ * `Presentation` em `fill` — `PageBody`, `SurfaceBody` ou o corpo da Presentation — e
351
353
  * `flow` nos demais casos (ADR 0024).
352
354
  */
353
355
  layout?: "flow" | "fill";
@@ -479,7 +481,23 @@ function dictOptionsFor(
479
481
  if (spec.options?.kind !== "dictionary") return [];
480
482
  const dict = dicts[spec.options.ref];
481
483
  if (dict === undefined) return [];
482
- return dict.options().map((o) => ({ value: o.value, label: o.label }));
484
+ return dict.options().map((o) => {
485
+ const Icon = o.icon === undefined ? undefined : iconPickerIcons[o.icon];
486
+ return {
487
+ value: o.value,
488
+ label: o.label,
489
+ ...(Icon === undefined
490
+ ? {}
491
+ : {
492
+ content: (
493
+ <>
494
+ <Icon aria-hidden="true" />
495
+ <span>{o.label}</span>
496
+ </>
497
+ ),
498
+ }),
499
+ };
500
+ });
483
501
  }
484
502
 
485
503
  /** Opções vindas de uma action de lookup (`options.kind: 'lookup'`): o Select chama
@@ -571,6 +589,41 @@ function FilterField({
571
589
  const changedFromDefault = singleValue !== defaultValue;
572
590
  const acceptsEmptyValue = spec.allowEmpty !== false;
573
591
 
592
+ if (
593
+ spec.type === "select" &&
594
+ spec.multiple !== true &&
595
+ spec.presentation === "toggle"
596
+ ) {
597
+ const ALL = "__all__";
598
+ const toggleValue = singleValue === "" ? ALL : singleValue;
599
+ return (
600
+ <ToggleGroup
601
+ type="single"
602
+ mode="segmented"
603
+ id={`filter-${name}`}
604
+ value={toggleValue}
605
+ onValueChange={(next) => {
606
+ // O grupo sempre preserva uma escolha ativa. Quando vazio é permitido,
607
+ // a opção explícita representa esse estado em vez de desmarcar o item atual.
608
+ if (next === "") return;
609
+ onChange(next === ALL ? "" : next);
610
+ }}
611
+ aria-label={label}
612
+ disabled={!enabled}
613
+ className={className ?? "max-w-full"}
614
+ >
615
+ {acceptsEmptyValue && (
616
+ <Toggle value={ALL}>{text(spec.placeholder, "Todos")}</Toggle>
617
+ )}
618
+ {base.map((option) => (
619
+ <Toggle key={option.value} value={option.value}>
620
+ {option.content ?? option.label}
621
+ </Toggle>
622
+ ))}
623
+ </ToggleGroup>
624
+ );
625
+ }
626
+
574
627
  if (spec.multiple === true) {
575
628
  return (
576
629
  <Select
@@ -993,7 +1046,7 @@ function PeriodControl({
993
1046
  variant={embedded ? "ghost" : "outline"}
994
1047
  data-slot="action-list-period"
995
1048
  className={cn(
996
- "relative max-w-full gap-1.5 overflow-hidden bg-muted/30 px-3 hover:bg-muted/30 hover:text-foreground dark:bg-input/30 dark:hover:bg-input/30",
1049
+ "relative max-w-full gap-1.5 overflow-hidden bg-control px-3 hover:bg-interaction-hover hover:text-foreground",
997
1050
  // O X e o chevron ocupam posições explícitas na ponta do controle. O
998
1051
  // padding reserva os dois sem depender da largura do texto selecionado.
999
1052
  clearable && "pr-16",
@@ -1217,23 +1270,20 @@ function AdvancedFiltersDialog({
1217
1270
  <DialogHeader>
1218
1271
  <DialogTitle>Filtros</DialogTitle>
1219
1272
  </DialogHeader>
1220
- <DialogBody className="space-y-4">
1273
+ <DialogBody>
1221
1274
  {sectioned ? (
1222
1275
  sections.map(([section, entries]) => (
1223
- <Content
1276
+ <Surface
1224
1277
  key={section}
1225
1278
  as="fieldset"
1226
1279
  framed={sectionVariant === "framed"}
1227
- frameVariant={
1228
- sectionVariant === "framed" ? "legend" : "default"
1229
- }
1230
1280
  title={section}
1231
1281
  media={sectionIcons?.[section]}
1232
1282
  >
1233
1283
  <FieldGroup className={cn("grid gap-4", gridColumns)}>
1234
1284
  {entries.map(field)}
1235
1285
  </FieldGroup>
1236
- </Content>
1286
+ </Surface>
1237
1287
  ))
1238
1288
  ) : (
1239
1289
  <FieldGroup className={cn("grid gap-4", gridColumns)}>
@@ -1431,7 +1481,7 @@ export function ActionFilterBar({
1431
1481
  <div
1432
1482
  data-slot="action-filter-period-group"
1433
1483
  className={cn(
1434
- "flex min-h-9 w-fit min-w-0 max-w-full items-center rounded-md border border-input bg-muted/30 text-foreground transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[0.1875rem] focus-within:ring-ring/50 has-[[data-state=open]]:border-ring has-[[data-state=open]]:ring-[0.1875rem] has-[[data-state=open]]:ring-ring/50 dark:bg-input/30",
1484
+ "flex min-h-9 w-fit min-w-0 max-w-full items-center rounded-md border border-input bg-control text-foreground transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[0.1875rem] focus-within:ring-ring/50 has-[[data-state=open]]:border-ring has-[[data-state=open]]:ring-[0.1875rem] has-[[data-state=open]]:ring-ring/50",
1435
1485
  !periodGroupClearable && "pr-3",
1436
1486
  )}
1437
1487
  >
@@ -1981,9 +2031,9 @@ export function ActionList<TInput, TItem>({
1981
2031
  {viewEntries.map(([key, label, icon]) => (
1982
2032
  <Tooltip key={key}>
1983
2033
  <TooltipTrigger asChild>
1984
- <ToggleGroupItem value={key} aria-label={label}>
2034
+ <Toggle value={key} aria-label={label}>
1985
2035
  {icon ?? label}
1986
- </ToggleGroupItem>
2036
+ </Toggle>
1987
2037
  </TooltipTrigger>
1988
2038
  <TooltipContent>{label}</TooltipContent>
1989
2039
  </Tooltip>
@@ -24,12 +24,15 @@ import { createPortal } from "react-dom";
24
24
  import { ArrowLeft } from "lucide-react";
25
25
  import { cn } from "../../lib/cn.ts";
26
26
  import { Button } from "../primitives/button.tsx";
27
- import { Content } from "./content-header.tsx";
27
+ import { Surface } from "./surface.tsx";
28
28
  import {
29
29
  PageHeadingPresenceProvider,
30
30
  usePageHeadingPresence,
31
31
  } from "./page-heading-context.tsx";
32
- import { SurfaceHeader, surfaceHeaderClasses } from "./surface-header.tsx";
32
+ import {
33
+ StructuralHeader,
34
+ structuralHeaderClasses,
35
+ } from "./structural-header.tsx";
33
36
  import { PageState, PageStatePresenceProvider } from "./page-state.tsx";
34
37
  import { FillRoot, fillRegionClassName, useFillRegion } from "./fill-region.ts";
35
38
 
@@ -137,7 +140,7 @@ interface PageBaseProps extends Omit<
137
140
  className?: string;
138
141
  /**
139
142
  * `fill` limita a página à altura do shell para que regiões internas assumam a rolagem.
140
- * A altura chega sozinha a uma `ActionList` composta por `PageBody` e `Content` (ADR 0024).
143
+ * A altura chega sozinha a uma `ActionList` composta por `PageBody` e `Surface` (ADR 0024).
141
144
  */
142
145
  layout?: "flow" | "fill";
143
146
  }
@@ -198,7 +201,7 @@ function countPageBodyHeadings(node: ReactNode): {
198
201
  return {
199
202
  content:
200
203
  total.content +
201
- (child.type === Content && props.level === 1 ? 1 : 0) +
204
+ (child.type === Surface && props.level === 1 ? 1 : 0) +
202
205
  nested.content,
203
206
  state: total.state + nested.state,
204
207
  };
@@ -211,7 +214,7 @@ function hasOpaquePageBodyContent(node: ReactNode): boolean {
211
214
  return Children.toArray(node).some((child) => {
212
215
  if (!isValidElement(child)) return false;
213
216
  if (typeof child.type !== "string") {
214
- return child.type !== Content && child.type !== PageState;
217
+ return child.type !== Surface && child.type !== PageState;
215
218
  }
216
219
  return hasOpaquePageBodyContent(
217
220
  (child.props as { children?: ReactNode }).children,
@@ -323,7 +326,7 @@ export function Page({
323
326
  );
324
327
  if (headingSources > 1 || (headingSources === 0 && !hasOpaqueBodyContent)) {
325
328
  throw new Error(
326
- "Page explícito exige exatamente um heading principal em PageHeader, Content level={1} ou PageState ativo dentro de PageBody.",
329
+ "Page explícito exige exatamente um heading principal em PageHeader, Surface level={1} ou PageState ativo dentro de PageBody.",
327
330
  );
328
331
  }
329
332
  }
@@ -332,7 +335,7 @@ export function Page({
332
335
  if (integralState !== hasActiveIntegralState) return;
333
336
  if (headingCount.current !== 1) {
334
337
  throw new Error(
335
- "Page explícito exige exatamente um heading principal em PageHeader, Content level={1} ou PageState ativo dentro de PageBody.",
338
+ "Page explícito exige exatamente um heading principal em PageHeader, Surface level={1} ou PageState ativo dentro de PageBody.",
336
339
  );
337
340
  }
338
341
  });
@@ -452,7 +455,7 @@ export function PageHeader({
452
455
  }
453
456
  return (
454
457
  <PageHeaderContext.Provider value>
455
- <SurfaceHeader
458
+ <StructuralHeader
456
459
  name="PageHeader"
457
460
  slot="page"
458
461
  slots={{
@@ -472,7 +475,7 @@ export function PageHeader({
472
475
  )}
473
476
  {structuralChildren}
474
477
  {mergedActions}
475
- </SurfaceHeader>
478
+ </StructuralHeader>
476
479
  </PageHeaderContext.Provider>
477
480
  );
478
481
  }
@@ -487,7 +490,7 @@ export function PageTitle({
487
490
  return (
488
491
  <h1
489
492
  data-slot="page-title"
490
- className={cn(surfaceHeaderClasses.page.title, className)}
493
+ className={cn(structuralHeaderClasses.page.title, className)}
491
494
  {...props}
492
495
  />
493
496
  );
@@ -503,7 +506,7 @@ export function PageActions({
503
506
  const actions = (
504
507
  <div
505
508
  data-slot="page-actions"
506
- className={cn(surfaceHeaderClasses.page.actions, className)}
509
+ className={cn(structuralHeaderClasses.page.actions, className)}
507
510
  {...props}
508
511
  />
509
512
  );
@@ -579,6 +582,7 @@ export function PageBody({
579
582
  data-slot="page-body"
580
583
  {...fill.attributes}
581
584
  className={cn(
585
+ "flex flex-col gap-6 *:shrink-0",
582
586
  (integralState || fill.filling) && fillRegionClassName,
583
587
  className,
584
588
  )}