@softize/opus 18.5.0 → 20.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 (151) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/check.mjs +198 -10
  4. package/bin/lib/copy.mjs +29 -0
  5. package/bin/lib/mcp.mjs +1 -1
  6. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
  7. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  8. package/docs/data-products.md +15 -9
  9. package/docs/protocol.md +3 -4
  10. package/docs/releasing.md +2 -2
  11. package/package.json +2 -2
  12. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  13. package/registry/templates/app/package.json +1 -1
  14. package/src/core/runtime.ts +96 -8
  15. package/src/core/types.ts +5 -2
  16. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  17. package/src/ui/components/patterns/content-header.tsx +16 -12
  18. package/src/ui/components/patterns/fill-region.ts +150 -0
  19. package/src/ui/components/patterns/heading-level.ts +16 -0
  20. package/src/ui/components/patterns/list.tsx +103 -73
  21. package/src/ui/components/patterns/page.tsx +20 -4
  22. package/src/ui/components/patterns/pane.tsx +294 -0
  23. package/src/ui/components/patterns/presentation.tsx +81 -54
  24. package/src/ui/components/patterns/sidebar.tsx +13 -72
  25. package/src/ui/components/patterns/split.tsx +51 -38
  26. package/src/ui/components/patterns/surface-anatomy.ts +246 -0
  27. package/src/ui/components/patterns/surface-header.tsx +51 -6
  28. package/src/ui/components/patterns/surface-media.tsx +69 -0
  29. package/src/ui/components/patterns/trigger.tsx +12 -2
  30. package/src/ui/components/primitives/alert.tsx +9 -1
  31. package/src/ui/components/primitives/avatar.tsx +7 -3
  32. package/src/ui/components/primitives/card.tsx +91 -29
  33. package/src/ui/components/primitives/carousel.tsx +34 -5
  34. package/src/ui/components/primitives/checkbox.tsx +11 -3
  35. package/src/ui/components/primitives/command.tsx +14 -5
  36. package/src/ui/components/primitives/copyable.tsx +120 -10
  37. package/src/ui/components/primitives/detail.tsx +10 -13
  38. package/src/ui/components/primitives/dialog.tsx +179 -45
  39. package/src/ui/components/primitives/drawer.tsx +131 -24
  40. package/src/ui/components/primitives/empty.tsx +1 -1
  41. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  42. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  43. package/src/ui/components/primitives/input-group.tsx +9 -2
  44. package/src/ui/components/primitives/input-otp.tsx +1 -1
  45. package/src/ui/components/primitives/kbd.tsx +1 -1
  46. package/src/ui/components/primitives/label.tsx +3 -0
  47. package/src/ui/components/primitives/menu.tsx +5 -0
  48. package/src/ui/components/primitives/metric-card.tsx +98 -39
  49. package/src/ui/components/primitives/popover.tsx +1 -1
  50. package/src/ui/components/primitives/progress.tsx +9 -1
  51. package/src/ui/components/primitives/slider.tsx +62 -11
  52. package/src/ui/components/primitives/truncate.tsx +10 -1
  53. package/src/ui/docs/content/accordion.md +74 -27
  54. package/src/ui/docs/content/action-form-card.md +24 -15
  55. package/src/ui/docs/content/action-form-dialog.md +17 -7
  56. package/src/ui/docs/content/action-form.md +92 -77
  57. package/src/ui/docs/content/action-list-dialog.md +75 -38
  58. package/src/ui/docs/content/action-list.md +134 -52
  59. package/src/ui/docs/content/action-trigger.md +27 -10
  60. package/src/ui/docs/content/action-view.md +14 -14
  61. package/src/ui/docs/content/actions.md +161 -66
  62. package/src/ui/docs/content/ai.md +43 -44
  63. package/src/ui/docs/content/alert.md +83 -29
  64. package/src/ui/docs/content/ask.md +51 -26
  65. package/src/ui/docs/content/aspect-ratio.md +22 -24
  66. package/src/ui/docs/content/audit.md +23 -19
  67. package/src/ui/docs/content/auth.md +38 -19
  68. package/src/ui/docs/content/avatar.md +54 -5
  69. package/src/ui/docs/content/badge.md +29 -9
  70. package/src/ui/docs/content/breadcrumb.md +21 -8
  71. package/src/ui/docs/content/button.md +73 -43
  72. package/src/ui/docs/content/calendar.md +134 -26
  73. package/src/ui/docs/content/card.md +252 -35
  74. package/src/ui/docs/content/carousel.md +37 -6
  75. package/src/ui/docs/content/chat.md +118 -37
  76. package/src/ui/docs/content/checkbox.md +73 -7
  77. package/src/ui/docs/content/cli.md +94 -55
  78. package/src/ui/docs/content/collapsible.md +43 -19
  79. package/src/ui/docs/content/command.md +30 -8
  80. package/src/ui/docs/content/composer.md +23 -8
  81. package/src/ui/docs/content/content.md +37 -7
  82. package/src/ui/docs/content/copyable.md +39 -8
  83. package/src/ui/docs/content/customization.md +55 -46
  84. package/src/ui/docs/content/cycle.md +33 -18
  85. package/src/ui/docs/content/data-products.md +115 -0
  86. package/src/ui/docs/content/data-state.md +20 -6
  87. package/src/ui/docs/content/data.md +20 -19
  88. package/src/ui/docs/content/detail.md +12 -5
  89. package/src/ui/docs/content/dialog.md +219 -28
  90. package/src/ui/docs/content/dictionary-value.md +4 -4
  91. package/src/ui/docs/content/dock.md +54 -19
  92. package/src/ui/docs/content/dot.md +35 -5
  93. package/src/ui/docs/content/drawer.md +193 -22
  94. package/src/ui/docs/content/empty-value.md +20 -6
  95. package/src/ui/docs/content/empty.md +24 -3
  96. package/src/ui/docs/content/events.md +76 -16
  97. package/src/ui/docs/content/executions.md +9 -9
  98. package/src/ui/docs/content/field.md +21 -6
  99. package/src/ui/docs/content/getting-started.md +44 -34
  100. package/src/ui/docs/content/icon-picker.md +19 -13
  101. package/src/ui/docs/content/icon-tile.md +28 -5
  102. package/src/ui/docs/content/input-otp.md +58 -7
  103. package/src/ui/docs/content/input.md +22 -3
  104. package/src/ui/docs/content/item.md +53 -6
  105. package/src/ui/docs/content/kbd.md +5 -1
  106. package/src/ui/docs/content/label.md +13 -4
  107. package/src/ui/docs/content/log.md +5 -5
  108. package/src/ui/docs/content/markdown.md +5 -4
  109. package/src/ui/docs/content/mcp.md +49 -23
  110. package/src/ui/docs/content/menu.md +95 -10
  111. package/src/ui/docs/content/metric-card.md +68 -21
  112. package/src/ui/docs/content/observability.md +10 -9
  113. package/src/ui/docs/content/page.md +125 -11
  114. package/src/ui/docs/content/pagination.md +13 -5
  115. package/src/ui/docs/content/pane.md +300 -0
  116. package/src/ui/docs/content/popover.md +42 -5
  117. package/src/ui/docs/content/presentation.md +91 -47
  118. package/src/ui/docs/content/progress.md +27 -14
  119. package/src/ui/docs/content/queue.md +39 -21
  120. package/src/ui/docs/content/radio-group.md +26 -4
  121. package/src/ui/docs/content/router.md +29 -10
  122. package/src/ui/docs/content/runtime.md +90 -54
  123. package/src/ui/docs/content/scheduler.md +53 -21
  124. package/src/ui/docs/content/scroll-area.md +42 -35
  125. package/src/ui/docs/content/select.md +78 -33
  126. package/src/ui/docs/content/semantic-context.md +21 -2
  127. package/src/ui/docs/content/separator.md +15 -9
  128. package/src/ui/docs/content/sidebar.md +25 -45
  129. package/src/ui/docs/content/skeleton.md +13 -4
  130. package/src/ui/docs/content/slider.md +37 -9
  131. package/src/ui/docs/content/spinner.md +4 -3
  132. package/src/ui/docs/content/split.md +34 -20
  133. package/src/ui/docs/content/storage.md +8 -9
  134. package/src/ui/docs/content/switch.md +10 -7
  135. package/src/ui/docs/content/table.md +46 -8
  136. package/src/ui/docs/content/tabs.md +14 -4
  137. package/src/ui/docs/content/testing.md +76 -31
  138. package/src/ui/docs/content/textarea.md +5 -4
  139. package/src/ui/docs/content/toast.md +48 -7
  140. package/src/ui/docs/content/toggle.md +3 -3
  141. package/src/ui/docs/content/tokens.md +15 -26
  142. package/src/ui/docs/content/tooltip.md +18 -2
  143. package/src/ui/docs/content/truncate.md +36 -6
  144. package/src/ui/docs/content/ui.md +74 -22
  145. package/src/ui/docs/content/upgrading.md +45 -36
  146. package/src/ui/docs/plugin.ts +36 -2
  147. package/src/ui/docs/registry.tsx +14 -2
  148. package/src/ui/drivers/react.tsx +16 -8
  149. package/src/ui/meta.ts +38 -32
  150. package/src/ui/react.tsx +14 -0
  151. package/src/vite/design.ts +16 -4
@@ -66,6 +66,7 @@ import {
66
66
  type DictLike,
67
67
  } from "../../drivers/react.tsx";
68
68
  import { cn } from "../../lib/cn.ts";
69
+ import { FillBoundary, useFillLeaf } from "./fill-region.ts";
69
70
  import { objectSchemaShape } from "../../lib/object-schema.ts";
70
71
  import { Badge } from "../primitives/badge.tsx";
71
72
  import { DictionaryValue } from "../primitives/dictionary-value.tsx";
@@ -343,7 +344,12 @@ export interface ActionListProps<TInput, TItem> {
343
344
  action: ListActionLike;
344
345
  /** Input BASE (escopo fixo, ex.: { workspaceId }) — a toolbar soma por cima. */
345
346
  input: TInput;
346
- /** `fill` ocupa a altura disponível e mantém a rolagem dentro da região de resultados. */
347
+ /**
348
+ * `fill` ocupa a altura disponível e mantém a rolagem dentro da região de resultados.
349
+ * 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
351
+ * `flow` nos demais casos (ADR 0024).
352
+ */
347
353
  layout?: "flow" | "fill";
348
354
  /** Ações do consumidor no fim da barra de filtros, depois dos controles de exibição. */
349
355
  toolbarActions?: ReactNode;
@@ -1666,7 +1672,7 @@ function RowActions({ children }: { children: ReactNode }) {
1666
1672
  export function ActionList<TInput, TItem>({
1667
1673
  action,
1668
1674
  input,
1669
- layout = "flow",
1675
+ layout: layoutProp,
1670
1676
  toolbarActions,
1671
1677
  columns,
1672
1678
  cells,
@@ -1693,6 +1699,7 @@ export function ActionList<TInput, TItem>({
1693
1699
  onStateChange,
1694
1700
  }: ActionListProps<TInput, TItem>) {
1695
1701
  const confirmBodyId = useId();
1702
+ const { layout, ref: rootRef } = useFillLeaf<HTMLDivElement>(layoutProp);
1696
1703
  // Query-backed (useListAction): busca declarativa por [name, input] E refetch
1697
1704
  // automático quando um form/trigger invalida a action (`invalidates: ['x.list']`).
1698
1705
  // O input efetivo é computado abaixo; o hook re-busca quando ele muda.
@@ -2144,8 +2151,11 @@ export function ActionList<TInput, TItem>({
2144
2151
  variant="plain"
2145
2152
  className={tableStateActive ? "h-full" : undefined}
2146
2153
  containerClassName={
2154
+ // Quem rola é a linha de resultados, acima: aqui o contêiner abre mão da
2155
+ // rolagem horizontal para não virar a referência do `sticky` do cabeçalho.
2156
+ // `h-full` só no estado sem linhas, para a moldura vazia ocupar a altura.
2147
2157
  tableFillsAvailableHeight
2148
- ? "absolute inset-0 overflow-auto"
2158
+ ? cn("overflow-x-visible", tableStateActive && "h-full")
2149
2159
  : undefined
2150
2160
  }
2151
2161
  >
@@ -2290,7 +2300,14 @@ export function ActionList<TInput, TItem>({
2290
2300
  ) : null;
2291
2301
 
2292
2302
  // — Rodapé (o estilo da casa): Página X de Y à esquerda · pager CENTRALIZADO ·
2293
- // total à direita (grid de 3 colunas com laterais 1fr = centro geométrico) —
2303
+ // total à direita (grid de 3 colunas com laterais 1fr = centro geométrico).
2304
+ // Encosta na última linha: a borda de cima é o fecho da tabela, não a de um
2305
+ // bloco solto, então o rodapé não entra no `gap` da pilha.
2306
+ // Altura: 2.5rem de piso (a mesma do `TableHead`) para a faixa não mudar de
2307
+ // tamanho quando o pager aparece — ele ocupa 1.75rem e cabe dentro do piso.
2308
+ // O padding fica em 0.25rem só para o dia em que o conteúdo passar disso:
2309
+ // com `py-2` o padding empurraria o pager para fora e a faixa voltaria a
2310
+ // crescer —
2294
2311
  const pages =
2295
2312
  total !== undefined
2296
2313
  ? Math.max(1, Math.ceil(total / (state.limit ?? pageSize)))
@@ -2300,7 +2317,7 @@ export function ActionList<TInput, TItem>({
2300
2317
  total !== undefined && !isError ? (
2301
2318
  <div
2302
2319
  data-slot="action-list-footer"
2303
- className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 border-t border-border px-3 py-1.5 text-xs text-muted-foreground"
2320
+ className="grid min-h-10 grid-cols-[1fr_auto_1fr] items-center gap-3 border-t border-border px-3 py-1 text-xs text-muted-foreground"
2304
2321
  >
2305
2322
  <span>
2306
2323
  Página {state.page} de {pages}
@@ -2357,6 +2374,7 @@ export function ActionList<TInput, TItem>({
2357
2374
  return (
2358
2375
  <TooltipProvider>
2359
2376
  <div
2377
+ ref={rootRef}
2360
2378
  data-action={action.name}
2361
2379
  data-layout={layout}
2362
2380
  className={cn(
@@ -2365,75 +2383,87 @@ export function ActionList<TInput, TItem>({
2365
2383
  : "space-y-3",
2366
2384
  )}
2367
2385
  >
2368
- {toolbar}
2369
- {beforeContent}
2370
- {selectionBar}
2371
- {/* Refetch com a lista já na tela: esmaece e trava interação até chegar. */}
2372
- <div
2373
- aria-busy={busy}
2374
- className={cn(
2375
- layout === "fill" && "relative flex min-h-0 flex-1 flex-col",
2376
- layout === "fill" &&
2377
- (tableFillsAvailableHeight ? "overflow-hidden" : "overflow-auto"),
2378
- busy && items.length > 0 && "pointer-events-none opacity-60",
2379
- )}
2380
- >
2381
- {body}
2382
- </div>
2383
- {footer}
2384
- {confirming !== null && (
2385
- <Dialog
2386
- mode="alert"
2387
- open
2388
- onOpenChange={(o) => {
2389
- if (!o) setConfirming(null);
2390
- }}
2391
- >
2392
- <DialogContent
2393
- aria-describedby={
2394
- confirming.confirm?.message === undefined
2395
- ? undefined
2396
- : confirmBodyId
2397
- }
2398
- >
2399
- <DialogHeader>
2400
- <DialogTitle>
2401
- {confirming.confirm?.title ?? confirming.label}
2402
- </DialogTitle>
2403
- </DialogHeader>
2404
- {confirming.confirm?.message !== undefined && (
2405
- <DialogBody
2406
- id={confirmBodyId}
2407
- className="text-sm text-muted-foreground"
2408
- >
2409
- {confirming.confirm.message}
2410
- </DialogBody>
2386
+ {/* A lista é a folha da cadeia de altura: o que ela hospeda decide a própria. */}
2387
+ <FillBoundary>
2388
+ {toolbar}
2389
+ {beforeContent}
2390
+ {selectionBar}
2391
+ {/* Tabela e rodapé formam um bloco só: o `gap` da pilha separa a barra de
2392
+ ferramentas do conteúdo, nunca o conteúdo do seu próprio rodapé. Duas
2393
+ linhas: a dos resultados absorve a altura que sobra, a do rodapé se mede
2394
+ pelo conteúdo. Em `flow` a primeira linha também é `1fr`, mas o bloco não
2395
+ tem altura própria e ela vira a altura do conteúdo. */}
2396
+ <div className={cn("grid min-h-0 grid-rows-[1fr_auto]", layout === "fill" && "flex-1")}>
2397
+ {/* A rolagem mora aqui, nos dois eixos: é este elemento que o `sticky` do
2398
+ cabeçalho enxerga. Um contêiner de rolagem entre ele e a tabela roubaria
2399
+ essa referência e o cabeçalho voltaria a subir junto com as linhas.
2400
+ Refetch com a lista já na tela: esmaece e trava interação até chegar. */}
2401
+ <div
2402
+ aria-busy={busy}
2403
+ className={cn(
2404
+ "min-h-0",
2405
+ layout === "fill" && "overflow-auto",
2406
+ busy && items.length > 0 && "pointer-events-none opacity-60",
2411
2407
  )}
2412
- <DialogFooter>
2413
- <ButtonGroup mode="spaced" distribution="equal">
2414
- <DialogClose initialFocus asChild>
2415
- <Button variant="outline">Cancelar</Button>
2416
- </DialogClose>
2417
- <DialogClose asChild>
2418
- <Button
2419
- context={
2420
- confirming.destructive === true ? "danger" : "primary"
2421
- }
2422
- variant="solid"
2423
- onClick={() => {
2424
- const a = confirming;
2425
- setConfirming(null);
2426
- void runBatch(a);
2427
- }}
2428
- >
2429
- {confirming.label}
2430
- </Button>
2431
- </DialogClose>
2432
- </ButtonGroup>
2433
- </DialogFooter>
2434
- </DialogContent>
2435
- </Dialog>
2436
- )}
2408
+ >
2409
+ {body}
2410
+ </div>
2411
+ {footer}
2412
+ </div>
2413
+ {confirming !== null && (
2414
+ <Dialog
2415
+ mode="alert"
2416
+ open
2417
+ onOpenChange={(o) => {
2418
+ if (!o) setConfirming(null);
2419
+ }}
2420
+ >
2421
+ <DialogContent
2422
+ aria-describedby={
2423
+ confirming.confirm?.message === undefined
2424
+ ? undefined
2425
+ : confirmBodyId
2426
+ }
2427
+ >
2428
+ <DialogHeader>
2429
+ <DialogTitle>
2430
+ {confirming.confirm?.title ?? confirming.label}
2431
+ </DialogTitle>
2432
+ </DialogHeader>
2433
+ {confirming.confirm?.message !== undefined && (
2434
+ <DialogBody
2435
+ id={confirmBodyId}
2436
+ className="text-sm text-muted-foreground"
2437
+ >
2438
+ {confirming.confirm.message}
2439
+ </DialogBody>
2440
+ )}
2441
+ <DialogFooter>
2442
+ <ButtonGroup mode="spaced" distribution="equal">
2443
+ <DialogClose initialFocus asChild>
2444
+ <Button variant="outline">Cancelar</Button>
2445
+ </DialogClose>
2446
+ <DialogClose asChild>
2447
+ <Button
2448
+ context={
2449
+ confirming.destructive === true ? "danger" : "primary"
2450
+ }
2451
+ variant="solid"
2452
+ onClick={() => {
2453
+ const a = confirming;
2454
+ setConfirming(null);
2455
+ void runBatch(a);
2456
+ }}
2457
+ >
2458
+ {confirming.label}
2459
+ </Button>
2460
+ </DialogClose>
2461
+ </ButtonGroup>
2462
+ </DialogFooter>
2463
+ </DialogContent>
2464
+ </Dialog>
2465
+ )}
2466
+ </FillBoundary>
2437
2467
  </div>
2438
2468
  </TooltipProvider>
2439
2469
  );
@@ -30,6 +30,11 @@ import {
30
30
  } from "./page-heading-context.tsx";
31
31
  import { SurfaceHeader, surfaceHeaderClasses } from "./surface-header.tsx";
32
32
  import { PageState, PageStatePresenceProvider } from "./page-state.tsx";
33
+ import {
34
+ FillRoot,
35
+ fillRegionClassName,
36
+ useFillRegion,
37
+ } from "./fill-region.ts";
33
38
 
34
39
  interface PageContextValue {}
35
40
 
@@ -205,7 +210,10 @@ interface PageBaseProps extends Omit<
205
210
  > {
206
211
  /** Classes do container. Default: centralizado com `max-w-7xl` (80rem). */
207
212
  className?: string;
208
- /** `fill` limita a página à altura do shell para que regiões internas assumam a rolagem. */
213
+ /**
214
+ * `fill` limita a página à altura do shell para que regiões internas assumam a rolagem.
215
+ * A altura chega sozinha a uma `ActionList` composta por `PageBody` e `Content` (ADR 0024).
216
+ */
209
217
  layout?: "flow" | "fill";
210
218
  }
211
219
 
@@ -455,7 +463,7 @@ export function Page({
455
463
  className,
456
464
  )}
457
465
  >
458
- {content}
466
+ <FillRoot active={layout === "fill"}>{content}</FillRoot>
459
467
  </div>
460
468
  </main>
461
469
  </PageHeadingPresenceProvider>
@@ -671,17 +679,25 @@ export function PageBack({
671
679
 
672
680
  export function PageBody({
673
681
  className,
682
+ children,
674
683
  ...props
675
684
  }: HTMLAttributes<HTMLDivElement>): ReactElement {
676
685
  const page = useContext(PageContext);
677
686
  const integralState = useContext(PageIntegralStateContext);
687
+ const fill = useFillRegion();
678
688
  requireParent(page !== null, "PageBody", "Page");
679
689
  return (
680
690
  <div
681
691
  data-slot="page-body"
682
- className={cn(integralState && "flex min-h-0 flex-1 flex-col", className)}
692
+ {...fill.attributes}
693
+ className={cn(
694
+ (integralState || fill.filling) && fillRegionClassName,
695
+ className,
696
+ )}
683
697
  {...props}
684
- />
698
+ >
699
+ {fill.provide(children)}
700
+ </div>
685
701
  );
686
702
  }
687
703
 
@@ -0,0 +1,294 @@
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.
4
+ *
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.
19
+ */
20
+ import {
21
+ Children,
22
+ Fragment,
23
+ isValidElement,
24
+ type CSSProperties,
25
+ type ReactElement,
26
+ type ReactNode,
27
+ } from "react";
28
+
29
+ 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
+
44
+ export type PaneSize =
45
+ | number
46
+ | `${number}%`
47
+ | `${number}rem`
48
+ | `${number}em`
49
+ | `${number}vh`
50
+ | `${number}vw`
51
+ | `${number}px`;
52
+
53
+ // Números continuam percentuais por compatibilidade com Split/Pane. Strings preservam
54
+ // a unidade explícita para layouts que precisam de um limite físico, como `28rem`.
55
+ export function paneSizeToCss(value: PaneSize): string {
56
+ return typeof value === "number" ? `${value}%` : value;
57
+ }
58
+
59
+ export interface PaneProps {
60
+ /** Identidade estável do painel dentro do grupo. */
61
+ id?: string;
62
+ /** Tamanho inicial. Números representam porcentagens; strings aceitam %, rem, em, vh, vw e px. */
63
+ initialSize?: PaneSize;
64
+ /** Tamanho mínimo quando o split é redimensionável. */
65
+ minSize?: PaneSize;
66
+ /** Tamanho máximo quando o split é redimensionável. */
67
+ maxSize?: PaneSize;
68
+ /** Ocupa o espaço remanescente no layout simples. */
69
+ 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
+ className?: string;
80
+ children: ReactNode;
81
+ }
82
+
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>. */
106
+ export function Pane({
107
+ id,
108
+ initialSize,
109
+ grow = false,
110
+ inset,
111
+ layout = "sectioned",
112
+ className,
113
+ children,
114
+ }: PaneProps): ReactElement {
115
+ const style: CSSProperties | undefined =
116
+ initialSize === undefined
117
+ ? undefined
118
+ : { 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
+ return (
282
+ <div
283
+ data-slot="pane-footer"
284
+ className={cn(
285
+ "shrink-0",
286
+ surfaceRegionClasses("pane", layout, "footer"),
287
+ className,
288
+ )}
289
+ {...props}
290
+ >
291
+ {children}
292
+ </div>
293
+ );
294
+ }