@softize/opus 23.0.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 (95) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +241 -42
  4. package/bin/lib/copy.mjs +16 -34
  5. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  6. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
  7. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
  8. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  9. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  10. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  11. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  12. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  13. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  14. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  15. package/package.json +5 -1
  16. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  17. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  18. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  19. package/registry/templates/app/src/App.tsx +24 -12
  20. package/src/ui/appearance.ts +149 -0
  21. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  22. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  23. package/src/ui/components/patterns/confirm.tsx +3 -1
  24. package/src/ui/components/patterns/fill-region.ts +8 -7
  25. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  26. package/src/ui/components/patterns/form.tsx +8 -7
  27. package/src/ui/components/patterns/list.tsx +15 -19
  28. package/src/ui/components/patterns/page.tsx +15 -11
  29. package/src/ui/components/patterns/pane.tsx +9 -240
  30. package/src/ui/components/patterns/presentation.tsx +14 -14
  31. package/src/ui/components/patterns/sidebar.tsx +7 -17
  32. package/src/ui/components/patterns/split.tsx +111 -59
  33. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  34. package/src/ui/components/patterns/structural-media.tsx +26 -0
  35. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  36. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  37. package/src/ui/components/patterns/surface.tsx +620 -0
  38. package/src/ui/components/patterns/trigger.tsx +11 -9
  39. package/src/ui/components/primitives/ask.tsx +155 -83
  40. package/src/ui/components/primitives/button.tsx +11 -9
  41. package/src/ui/components/primitives/checkbox.tsx +8 -8
  42. package/src/ui/components/primitives/command.tsx +1 -1
  43. package/src/ui/components/primitives/composer.tsx +47 -45
  44. package/src/ui/components/primitives/detail.tsx +9 -2
  45. package/src/ui/components/primitives/dialog.tsx +60 -26
  46. package/src/ui/components/primitives/drawer.tsx +58 -12
  47. package/src/ui/components/primitives/icon-picker.tsx +90 -73
  48. package/src/ui/components/primitives/input-group.tsx +35 -28
  49. package/src/ui/components/primitives/input-otp.tsx +16 -16
  50. package/src/ui/components/primitives/input.tsx +20 -10
  51. package/src/ui/components/primitives/item.tsx +9 -12
  52. package/src/ui/components/primitives/menu.tsx +44 -65
  53. package/src/ui/components/primitives/metric-card.tsx +75 -59
  54. package/src/ui/components/primitives/radio-group.tsx +9 -9
  55. package/src/ui/components/primitives/select.tsx +344 -209
  56. package/src/ui/components/primitives/switch.tsx +10 -10
  57. package/src/ui/components/primitives/table.tsx +29 -24
  58. package/src/ui/components/primitives/tabs.tsx +4 -2
  59. package/src/ui/components/primitives/textarea.tsx +6 -6
  60. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  61. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  62. package/src/ui/components/primitives/toggle.tsx +82 -16
  63. package/src/ui/docs/DocBrowser.tsx +77 -69
  64. package/src/ui/docs/content/action-form-card.md +9 -9
  65. package/src/ui/docs/content/action-list.md +76 -70
  66. package/src/ui/docs/content/action-view.md +33 -18
  67. package/src/ui/docs/content/button.md +1 -1
  68. package/src/ui/docs/content/communication.md +1 -1
  69. package/src/ui/docs/content/customization.md +64 -11
  70. package/src/ui/docs/content/detail.md +8 -6
  71. package/src/ui/docs/content/dialog.md +100 -58
  72. package/src/ui/docs/content/drawer.md +63 -19
  73. package/src/ui/docs/content/field.md +14 -15
  74. package/src/ui/docs/content/icon-tile.md +14 -13
  75. package/src/ui/docs/content/page.md +34 -26
  76. package/src/ui/docs/content/pane.md +41 -290
  77. package/src/ui/docs/content/presentation.md +3 -3
  78. package/src/ui/docs/content/separator.md +6 -6
  79. package/src/ui/docs/content/sidebar.md +68 -266
  80. package/src/ui/docs/content/skeleton.md +20 -18
  81. package/src/ui/docs/content/split.md +14 -14
  82. package/src/ui/docs/content/surface.md +513 -0
  83. package/src/ui/docs/content/tabs.md +20 -20
  84. package/src/ui/docs/content/toggle.md +117 -65
  85. package/src/ui/docs/content/tokens.md +124 -40
  86. package/src/ui/docs/registry.tsx +4 -6
  87. package/src/ui/index.ts +30 -27
  88. package/src/ui/meta.ts +15 -27
  89. package/src/ui/react.tsx +40 -43
  90. package/src/ui/theme.css +311 -64
  91. package/src/ui/components/patterns/content-header.tsx +0 -528
  92. package/src/ui/components/patterns/surface-media.tsx +0 -69
  93. package/src/ui/components/primitives/card.tsx +0 -191
  94. package/src/ui/docs/content/card.md +0 -292
  95. package/src/ui/docs/content/content.md +0 -275
@@ -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
  )
@@ -119,7 +119,7 @@ import {
119
119
  FieldLabel,
120
120
  type FieldSetVariant,
121
121
  } from "../primitives/field.tsx";
122
- import { Content } from "./content-header.tsx";
122
+ import { Surface } from "./surface.tsx";
123
123
  import { Search } from "lucide-react";
124
124
  import { Select, type SelectOption } from "../primitives/select.tsx";
125
125
  import {
@@ -130,7 +130,8 @@ import {
130
130
  TableHeader,
131
131
  TableRow,
132
132
  } from "../primitives/table.tsx";
133
- import { ToggleGroup, ToggleGroupItem } from "../primitives/toggle-group.tsx";
133
+ import { Toggle } from "../primitives/toggle.tsx";
134
+ import { ToggleGroup } from "../primitives/toggle-group.tsx";
134
135
  import {
135
136
  Tooltip,
136
137
  TooltipContent,
@@ -348,7 +349,7 @@ export interface ActionListProps<TInput, TItem> {
348
349
  /**
349
350
  * `fill` ocupa a altura disponível e mantém a rolagem dentro da região de resultados.
350
351
  * Sem valor, vale `fill` quando o elemento pai repassa a altura de uma `Page` ou
351
- * `Presentation` em `fill` — `PageBody`, `ContentBody` ou o corpo da Presentation — e
352
+ * `Presentation` em `fill` — `PageBody`, `SurfaceBody` ou o corpo da Presentation — e
352
353
  * `flow` nos demais casos (ADR 0024).
353
354
  */
354
355
  layout?: "flow" | "fill";
@@ -598,7 +599,7 @@ function FilterField({
598
599
  return (
599
600
  <ToggleGroup
600
601
  type="single"
601
- variant="outline"
602
+ mode="segmented"
602
603
  id={`filter-${name}`}
603
604
  value={toggleValue}
604
605
  onValueChange={(next) => {
@@ -612,14 +613,12 @@ function FilterField({
612
613
  className={className ?? "max-w-full"}
613
614
  >
614
615
  {acceptsEmptyValue && (
615
- <ToggleGroupItem value={ALL}>
616
- {text(spec.placeholder, "Todos")}
617
- </ToggleGroupItem>
616
+ <Toggle value={ALL}>{text(spec.placeholder, "Todos")}</Toggle>
618
617
  )}
619
618
  {base.map((option) => (
620
- <ToggleGroupItem key={option.value} value={option.value}>
619
+ <Toggle key={option.value} value={option.value}>
621
620
  {option.content ?? option.label}
622
- </ToggleGroupItem>
621
+ </Toggle>
623
622
  ))}
624
623
  </ToggleGroup>
625
624
  );
@@ -1047,7 +1046,7 @@ function PeriodControl({
1047
1046
  variant={embedded ? "ghost" : "outline"}
1048
1047
  data-slot="action-list-period"
1049
1048
  className={cn(
1050
- "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",
1051
1050
  // O X e o chevron ocupam posições explícitas na ponta do controle. O
1052
1051
  // padding reserva os dois sem depender da largura do texto selecionado.
1053
1052
  clearable && "pr-16",
@@ -1271,23 +1270,20 @@ function AdvancedFiltersDialog({
1271
1270
  <DialogHeader>
1272
1271
  <DialogTitle>Filtros</DialogTitle>
1273
1272
  </DialogHeader>
1274
- <DialogBody className="space-y-4">
1273
+ <DialogBody>
1275
1274
  {sectioned ? (
1276
1275
  sections.map(([section, entries]) => (
1277
- <Content
1276
+ <Surface
1278
1277
  key={section}
1279
1278
  as="fieldset"
1280
1279
  framed={sectionVariant === "framed"}
1281
- frameVariant={
1282
- sectionVariant === "framed" ? "legend" : "default"
1283
- }
1284
1280
  title={section}
1285
1281
  media={sectionIcons?.[section]}
1286
1282
  >
1287
1283
  <FieldGroup className={cn("grid gap-4", gridColumns)}>
1288
1284
  {entries.map(field)}
1289
1285
  </FieldGroup>
1290
- </Content>
1286
+ </Surface>
1291
1287
  ))
1292
1288
  ) : (
1293
1289
  <FieldGroup className={cn("grid gap-4", gridColumns)}>
@@ -1485,7 +1481,7 @@ export function ActionFilterBar({
1485
1481
  <div
1486
1482
  data-slot="action-filter-period-group"
1487
1483
  className={cn(
1488
- "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",
1489
1485
  !periodGroupClearable && "pr-3",
1490
1486
  )}
1491
1487
  >
@@ -2035,9 +2031,9 @@ export function ActionList<TInput, TItem>({
2035
2031
  {viewEntries.map(([key, label, icon]) => (
2036
2032
  <Tooltip key={key}>
2037
2033
  <TooltipTrigger asChild>
2038
- <ToggleGroupItem value={key} aria-label={label}>
2034
+ <Toggle value={key} aria-label={label}>
2039
2035
  {icon ?? label}
2040
- </ToggleGroupItem>
2036
+ </Toggle>
2041
2037
  </TooltipTrigger>
2042
2038
  <TooltipContent>{label}</TooltipContent>
2043
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
  )}
@@ -1,45 +1,12 @@
1
1
  /**
2
- * Pane — a superfície encaixável de um Split: a coluna ou faixa que hospeda navegação,
3
- * lista ou conteúdo, com regiões fixas no topo e no rodapé e um corpo que rola.
2
+ * Pane — unidade dimensionável de um Split.
4
3
  *
5
- * Até a ADR 0023 esta família vivia partida: a raiz em `split.tsx`, os slots em
6
- * `sidebar.tsx`, sem validação entre eles e com padding assimétrico (header sem recuo,
7
- * footer com `p-2`). Agora raiz e slots moram aqui e derivam a anatomia da mesma tabela
8
- * das demais famílias.
9
- *
10
- * <Pane>
11
- * <PaneHeader><PaneMedia>…</PaneMedia><PaneTitle>Conversas</PaneTitle><PaneActions>…</PaneActions></PaneHeader>
12
- * <PaneBody>…</PaneBody>
13
- * <PaneFooter>…</PaneFooter>
14
- * </Pane>
15
- *
16
- * `inset` continua existindo para o pane que NÃO usa slots — a caixa simples que recebe
17
- * conteúdo direto. Quando há slots, o recuo é de cada região, e o inset do pane vira
18
- * `none` sozinho: o divisor precisa alcançar a borda.
4
+ * Pane não é uma superfície visual: não aplica inset, borda, fundo, rolagem nem anatomia
5
+ * interna. O filho decide sua própria apresentação, normalmente com Surface ou Sidebar.
19
6
  */
20
- import {
21
- Children,
22
- Fragment,
23
- isValidElement,
24
- type CSSProperties,
25
- type ReactElement,
26
- type ReactNode,
27
- } from "react";
7
+ import type { CSSProperties, ReactElement, ReactNode } from "react";
28
8
 
29
9
  import { cn } from "../../lib/cn.ts";
30
- import { headingTags, type HeadingLevel } from "./heading-level.ts";
31
- import {
32
- SurfaceLayoutProvider,
33
- surfaceActionsClasses,
34
- surfaceDescriptionClasses,
35
- surfaceHeaderRowClasses,
36
- surfaceRegionClasses,
37
- surfaceTitleClasses,
38
- useSurfaceLayout,
39
- type SurfaceLayout,
40
- } from "./surface-anatomy.ts";
41
- import { SurfaceHeader } from "./surface-header.tsx";
42
- import { SurfaceMedia, type SurfaceMediaProps } from "./surface-media.tsx";
43
10
 
44
11
  export type PaneSize =
45
12
  | number
@@ -67,48 +34,15 @@ export interface PaneProps {
67
34
  maxSize?: PaneSize;
68
35
  /** Ocupa o espaço remanescente no layout simples. */
69
36
  grow?: boolean;
70
- /**
71
- * Respiro interno do pane sem slots. Com header, body ou footer como filhos DIRETOS o
72
- * default passa a `none`, porque aí o recuo é de cada região. Quando os slots vêm
73
- * dentro de outro componente — a `Sidebar`, por exemplo —, o pane não os enxerga:
74
- * declare `inset="none"` você mesmo.
75
- */
76
- inset?: "none" | "sm" | "md" | "lg";
77
- /** Anatomia das regiões: `sectioned` (default) ou `continuous` — ADR 0023. */
78
- layout?: SurfaceLayout;
79
37
  className?: string;
80
38
  children: ReactNode;
81
39
  }
82
40
 
83
- const insets = { none: "", sm: "p-2", md: "p-4", lg: "p-6" } as const;
84
-
85
- /** Expande fragments de primeiro nível antes de procurar os slots. */
86
- function flatten(children: ReactNode): ReactNode[] {
87
- return Children.toArray(children).flatMap((node) =>
88
- isValidElement(node) && node.type === Fragment
89
- ? Children.toArray((node.props as { children?: ReactNode }).children)
90
- : [node],
91
- );
92
- }
93
-
94
- function hasStructuralSlots(children: ReactNode): boolean {
95
- return flatten(children).some(
96
- (node) =>
97
- isValidElement(node) &&
98
- (node.type === PaneHeader ||
99
- node.type === PaneBody ||
100
- node.type === PaneContent ||
101
- node.type === PaneFooter),
102
- );
103
- }
104
-
105
- /** Um conteúdo encaixável. Sua posição é dada pela ordem dentro de <Split>. */
41
+ /** Um conteúdo encaixável. Sua posição é dada pela ordem dentro de Split. */
106
42
  export function Pane({
107
43
  id,
108
44
  initialSize,
109
45
  grow = false,
110
- inset,
111
- layout = "sectioned",
112
46
  className,
113
47
  children,
114
48
  }: PaneProps): ReactElement {
@@ -116,177 +50,12 @@ export function Pane({
116
50
  initialSize === undefined
117
51
  ? undefined
118
52
  : { flexBasis: paneSizeToCss(initialSize) };
119
- const structural = hasStructuralSlots(children);
120
- const resolvedInset = inset ?? (structural ? "none" : "md");
121
- return (
122
- <SurfaceLayoutProvider value={layout}>
123
- <div
124
- id={id === undefined ? undefined : String(id)}
125
- data-slot="pane"
126
- data-inset={resolvedInset}
127
- data-layout={layout}
128
- className={cn(
129
- "min-h-0 min-w-0 shrink-0",
130
- grow && "flex-1",
131
- // Com slots o pane é uma coluna: header fixo, corpo que rola, footer fixo.
132
- structural && "flex flex-col",
133
- insets[resolvedInset],
134
- className,
135
- )}
136
- style={style}
137
- >
138
- {children}
139
- </div>
140
- </SurfaceLayoutProvider>
141
- );
142
- }
143
-
144
- /** Faixa fixa no topo de um pane — marca, contexto ou ações locais. */
145
- export function PaneHeader({
146
- className,
147
- children,
148
- ...props
149
- }: React.ComponentProps<"div">): ReactElement {
150
- const layout = useSurfaceLayout("pane");
151
- return (
152
- <SurfaceHeader
153
- name="PaneHeader"
154
- slot="pane"
155
- slots={{
156
- media: PaneMedia,
157
- title: PaneTitle,
158
- description: PaneDescription,
159
- actions: PaneActions,
160
- }}
161
- leadingPlacement="inline"
162
- titleRequired={false}
163
- // O pane é a superfície genérica: além dos slots, o header hospeda marca, busca ou
164
- // chrome que não são título nem ação (ADR 0023).
165
- allowFreeContent
166
- rowClassName={surfaceHeaderRowClasses("pane")}
167
- className={cn(surfaceRegionClasses("pane", layout, "header"), className)}
168
- {...props}
169
- >
170
- {children}
171
- </SurfaceHeader>
172
- );
173
- }
174
-
175
- /** Moldura de ícone ou imagem que abre a linha do header. */
176
- export function PaneMedia(props: SurfaceMediaProps): ReactElement {
177
- return <SurfaceMedia slot="pane-media" size="xs" {...props} />;
178
- }
179
-
180
- export function PaneTitle({
181
- className,
182
- level = 2,
183
- ...props
184
- }: React.ComponentProps<"h2"> & {
185
- /** Hierarquia do documento (`h1`–`h6`). O tamanho vem do layout, nunca do nível. */
186
- level?: HeadingLevel;
187
- }): ReactElement {
188
- const layout = useSurfaceLayout("pane");
189
- const Comp = headingTags[level];
190
- return (
191
- <Comp
192
- data-slot="pane-title"
193
- className={cn(
194
- "min-w-0 truncate",
195
- surfaceTitleClasses("pane", layout),
196
- className,
197
- )}
198
- {...props}
199
- />
200
- );
201
- }
202
-
203
- export function PaneDescription({
204
- className,
205
- ...props
206
- }: React.ComponentProps<"p">): ReactElement {
207
- return (
208
- <p
209
- data-slot="pane-description"
210
- className={cn("truncate", surfaceDescriptionClasses("pane"), className)}
211
- {...props}
212
- />
213
- );
214
- }
215
-
216
- /** Ações do header — a família posiciona no extremo oposto ao título. */
217
- export function PaneActions({
218
- className,
219
- ...props
220
- }: React.ComponentProps<"div">): ReactElement {
221
- return (
222
- <div
223
- data-slot="pane-actions"
224
- className={cn(surfaceActionsClasses, className)}
225
- {...props}
226
- />
227
- );
228
- }
229
-
230
- /** Região rolável que ocupa o espaço restante de um pane. */
231
- export function PaneBody({
232
- className,
233
- children,
234
- ...props
235
- }: React.ComponentProps<"div">): ReactElement {
236
- const layout = useSurfaceLayout("pane");
237
- return (
238
- <div
239
- data-slot="pane-body"
240
- className={cn(
241
- "min-h-0 flex-1 overflow-y-auto",
242
- surfaceRegionClasses("pane", layout, "body"),
243
- className,
244
- )}
245
- {...props}
246
- >
247
- {children}
248
- </div>
249
- );
250
- }
251
-
252
- /** @deprecated Use PaneBody. Alias mantido para migração; sai numa próxima versão major. */
253
- export function PaneContent({
254
- className,
255
- children,
256
- ...props
257
- }: React.ComponentProps<"div">): ReactElement {
258
- const layout = useSurfaceLayout("pane");
259
- return (
260
- <div
261
- data-slot="pane-content"
262
- className={cn(
263
- "min-h-0 flex-1 overflow-y-auto",
264
- surfaceRegionClasses("pane", layout, "body"),
265
- className,
266
- )}
267
- {...props}
268
- >
269
- {children}
270
- </div>
271
- );
272
- }
273
-
274
- /** Faixa fixa no rodapé de um pane para ações persistentes. */
275
- export function PaneFooter({
276
- className,
277
- children,
278
- ...props
279
- }: React.ComponentProps<"div">): ReactElement {
280
- const layout = useSurfaceLayout("pane");
281
53
  return (
282
54
  <div
283
- data-slot="pane-footer"
284
- className={cn(
285
- "shrink-0",
286
- surfaceRegionClasses("pane", layout, "footer"),
287
- className,
288
- )}
289
- {...props}
55
+ id={id === undefined ? undefined : String(id)}
56
+ data-slot="pane"
57
+ className={cn("min-h-0 min-w-0 shrink-0", grow && "flex-1", className)}
58
+ style={style}
290
59
  >
291
60
  {children}
292
61
  </div>
@@ -75,12 +75,12 @@ import { ActionList, type ActionListState } from "./list.tsx";
75
75
  import { ActionTrigger } from "./trigger.tsx";
76
76
  import { ActionView } from "./view.tsx";
77
77
  import {
78
- Content,
79
- ContentActions,
80
- ContentBody,
81
- ContentHeader,
82
- ContentTitle,
83
- } from "./content-header.tsx";
78
+ Surface,
79
+ SurfaceActions,
80
+ SurfaceBody,
81
+ SurfaceHeader,
82
+ SurfaceTitle,
83
+ } from "./surface.tsx";
84
84
  import { SurfaceAssistant } from "./surface-assistant.tsx";
85
85
  import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
86
86
 
@@ -221,17 +221,17 @@ function PresentationFrame({
221
221
  if (surface === "page") {
222
222
  if (listBody) {
223
223
  const content = (
224
- <Content level={1}>
225
- <ContentHeader>
226
- <ContentTitle>{title}</ContentTitle>
224
+ <Surface as="section" level={1}>
225
+ <SurfaceHeader>
226
+ <SurfaceTitle>{title}</SurfaceTitle>
227
227
  {headerActions === undefined ? null : (
228
- <ContentActions>
228
+ <SurfaceActions>
229
229
  <PresentationActions>{headerActions}</PresentationActions>
230
- </ContentActions>
230
+ </SurfaceActions>
231
231
  )}
232
- </ContentHeader>
233
- <ContentBody>{body}</ContentBody>
234
- </Content>
232
+ </SurfaceHeader>
233
+ <SurfaceBody>{body}</SurfaceBody>
234
+ </Surface>
235
235
  );
236
236
  return (
237
237
  <Page