@softize/opus 30.0.0 → 31.1.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 (64) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/bin/lib/check.mjs +109 -101
  3. package/bin/lib/copy.mjs +2973 -1801
  4. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
  5. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
  6. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  7. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  8. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  9. package/docs/adr/0066-civil-date-input-and-calendar-list.md +28 -0
  10. package/docs/adr/README.md +6 -0
  11. package/package.json +1 -1
  12. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  13. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  15. package/src/core/presentation.ts +938 -32
  16. package/src/core/types.ts +2 -0
  17. package/src/ui/components/patterns/form.tsx +22 -1
  18. package/src/ui/components/patterns/grouped-list.ts +2 -2
  19. package/src/ui/components/patterns/list-calendar.tsx +129 -0
  20. package/src/ui/components/patterns/list.tsx +612 -382
  21. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  22. package/src/ui/components/patterns/page.tsx +227 -197
  23. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  24. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  25. package/src/ui/components/patterns/presentation.tsx +1142 -229
  26. package/src/ui/components/patterns/structural-header.tsx +23 -32
  27. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  28. package/src/ui/components/patterns/surface.tsx +19 -29
  29. package/src/ui/components/patterns/trigger.tsx +10 -2
  30. package/src/ui/components/primitives/calendar.tsx +39 -18
  31. package/src/ui/components/primitives/close-button.tsx +11 -3
  32. package/src/ui/components/primitives/command.tsx +2 -8
  33. package/src/ui/components/primitives/date-input.tsx +276 -0
  34. package/src/ui/components/primitives/detail.tsx +1 -1
  35. package/src/ui/components/primitives/dialog.tsx +19 -81
  36. package/src/ui/components/primitives/drawer.tsx +17 -79
  37. package/src/ui/components/primitives/field.tsx +1 -1
  38. package/src/ui/components/primitives/pagination.tsx +3 -2
  39. package/src/ui/components/primitives/table.tsx +1 -1
  40. package/src/ui/components/primitives/tabs.tsx +13 -2
  41. package/src/ui/docs/content/action-form.md +6 -1
  42. package/src/ui/docs/content/action-list.md +156 -68
  43. package/src/ui/docs/content/action-trigger.md +5 -0
  44. package/src/ui/docs/content/button.md +5 -6
  45. package/src/ui/docs/content/calendar-month-view.md +50 -0
  46. package/src/ui/docs/content/calendar.md +6 -1
  47. package/src/ui/docs/content/command.md +62 -50
  48. package/src/ui/docs/content/compositions.md +129 -459
  49. package/src/ui/docs/content/date-input.md +67 -0
  50. package/src/ui/docs/content/dialog.md +44 -112
  51. package/src/ui/docs/content/drawer.md +44 -103
  52. package/src/ui/docs/content/field.md +2 -1
  53. package/src/ui/docs/content/input.md +3 -0
  54. package/src/ui/docs/content/page.md +204 -412
  55. package/src/ui/docs/content/presentation.md +371 -23
  56. package/src/ui/docs/content/separator.md +1 -1
  57. package/src/ui/docs/content/surface.md +65 -120
  58. package/src/ui/docs/content/tokens.md +18 -4
  59. package/src/ui/docs/registry.tsx +38 -9
  60. package/src/ui/drivers/context.tsx +11 -4
  61. package/src/ui/drivers/react.tsx +27 -7
  62. package/src/ui/meta.ts +17 -6
  63. package/src/ui/react.tsx +41 -8
  64. package/src/ui/theme.css +44 -33
@@ -3,22 +3,17 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
3
  import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
4
4
  import { cn } from "../../lib/cn.ts";
5
5
  import {
6
- SurfaceLayoutProvider,
7
6
  surfaceActionsClasses,
8
7
  surfaceDescriptionClasses,
9
8
  surfaceHeaderRowClasses,
10
9
  surfaceRegionClasses,
11
10
  surfaceTitleClasses,
12
- useSurfaceLayout,
13
- type SurfaceLayout,
14
11
  } from "../patterns/surface-anatomy.ts";
15
12
  import { FillBoundary } from "../patterns/fill-region.ts";
16
13
  import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
17
14
  import {
18
15
  StructuralHeader,
19
16
  structuralMetaClasses,
20
- structuralHeaderClasses,
21
- validateSurfaceIntro,
22
17
  } from "../patterns/structural-header.tsx";
23
18
  import {
24
19
  StructuralMedia,
@@ -65,11 +60,10 @@ interface DialogContentContextValue {
65
60
  const DialogContentContext = React.createContext<DialogContentContextValue>({
66
61
  showCloseButton: false,
67
62
  closeDisabled: false,
68
- closeSize: "sm",
63
+ closeSize: "xs",
69
64
  });
70
65
 
71
66
  const DialogMetaContext = React.createContext(false);
72
- const DialogIntroContext = React.createContext(false);
73
67
 
74
68
  /**
75
69
  * Compõe o handler de dispensa do consumidor com o bloqueio de `closeDisabled`: o handler dele
@@ -417,8 +411,7 @@ export function DialogContent({
417
411
  children,
418
412
  showCloseButton,
419
413
  closeDisabled = false,
420
- closeSize = "sm",
421
- layout = "sectioned",
414
+ closeSize = "xs",
422
415
  "aria-describedby": ariaDescribedBy,
423
416
  onEscapeKeyDown,
424
417
  onPointerDownOutside,
@@ -438,8 +431,6 @@ export function DialogContent({
438
431
  closeDisabled?: boolean;
439
432
  /** Tamanho da ação circular de fechar no cabeçalho. */
440
433
  closeSize?: CloseButtonSize;
441
- /** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
442
- layout?: SurfaceLayout;
443
434
  }) {
444
435
  const { mode, open, modal } = useDialogContext();
445
436
  const layer = useLayerPortal();
@@ -529,7 +520,6 @@ export function DialogContent({
529
520
  ref={composedRef}
530
521
  data-slot="dialog-content"
531
522
  data-mode="default"
532
- data-layout={layout}
533
523
  {...{ [LAYER_MODAL_ATTRIBUTE]: internal || undefined }}
534
524
  className={cn(
535
525
  contentClasses,
@@ -550,13 +540,13 @@ export function DialogContent({
550
540
  {...props}
551
541
  >
552
542
  <LayerPortalProvider value={nestedPortal}>
553
- <SurfaceLayoutProvider value={layout}>
543
+ <FillBoundary>
554
544
  <DialogContentContext.Provider
555
545
  value={{ showCloseButton: showClose, closeDisabled, closeSize }}
556
546
  >
557
547
  {children}
558
548
  </DialogContentContext.Provider>
559
- </SurfaceLayoutProvider>
549
+ </FillBoundary>
560
550
  </LayerPortalProvider>
561
551
  </DialogPrimitive.Content>
562
552
  </DialogPortal>
@@ -566,16 +556,21 @@ export function DialogContent({
566
556
  export function DialogHeader({
567
557
  className,
568
558
  children,
559
+ tabs,
569
560
  ...props
570
- }: React.ComponentProps<"div">) {
561
+ }: React.ComponentProps<"div"> & {
562
+ tabs?: React.ReactNode;
563
+ }) {
571
564
  const { mode } = useDialogContext();
572
- const layout = useSurfaceLayout("dialog");
573
565
  const { showCloseButton, closeDisabled, closeSize } =
574
566
  React.useContext(DialogContentContext);
575
567
 
576
568
  // O modo de alerta é a exceção declarada da ADR 0023: a resposta obrigatória empilha
577
569
  // mídia, título e corpo centralizados, e o padding mora na raiz do conteúdo.
578
570
  if (mode === "alert") {
571
+ if (tabs !== undefined && tabs !== null && tabs !== false) {
572
+ throw new Error("DialogHeader de alerta não aceita abas.");
573
+ }
579
574
  return (
580
575
  <div
581
576
  data-slot="dialog-header"
@@ -593,6 +588,7 @@ export function DialogHeader({
593
588
  return (
594
589
  <DialogMetaContext.Provider value>
595
590
  <StructuralHeader
591
+ tabs={tabs}
596
592
  name="DialogHeader"
597
593
  slot="dialog"
598
594
  slots={{
@@ -604,11 +600,11 @@ export function DialogHeader({
604
600
  actions: DialogActions,
605
601
  }}
606
602
  leadingPlacement="inline"
607
- titleRequired={false}
603
+ titleRequired
608
604
  rowClassName={surfaceHeaderRowClasses("dialog")}
609
605
  className={cn(
610
606
  "text-left",
611
- surfaceRegionClasses("dialog", layout, "header"),
607
+ surfaceRegionClasses("dialog", "header"),
612
608
  className,
613
609
  )}
614
610
  trailing={
@@ -652,36 +648,6 @@ export function DialogActions({
652
648
  );
653
649
  }
654
650
 
655
- /** Título e ações da composição principal, como primeiro bloco de DialogBody. */
656
- export function DialogIntro({
657
- className,
658
- children,
659
- ...props
660
- }: React.ComponentProps<"div">) {
661
- return (
662
- <DialogIntroContext.Provider value>
663
- <DialogMetaContext.Provider value>
664
- <StructuralHeader
665
- name="DialogIntro"
666
- slot="dialog-intro"
667
- slots={{
668
- title: DialogTitle,
669
- description: DialogDescription,
670
- meta: DialogMeta,
671
- actions: DialogIntroActions,
672
- }}
673
- leadingPlacement="inline"
674
- titleRequired
675
- className={cn("text-left", className)}
676
- {...props}
677
- >
678
- {children}
679
- </StructuralHeader>
680
- </DialogMetaContext.Provider>
681
- </DialogIntroContext.Provider>
682
- );
683
- }
684
-
685
651
  /** Badges, datas e autoria abaixo do título e da orientação. */
686
652
  export function DialogMeta({
687
653
  className,
@@ -693,10 +659,7 @@ export function DialogMeta({
693
659
  throw new Error(
694
660
  'DialogMeta não está disponível em Dialog com mode="alert"; coloque as informações no corpo do alerta.',
695
661
  );
696
- if (!inside)
697
- throw new Error(
698
- "DialogMeta deve estar dentro de DialogHeader ou DialogIntro.",
699
- );
662
+ if (!inside) throw new Error("DialogMeta deve estar dentro de DialogHeader.");
700
663
  return (
701
664
  <div
702
665
  data-slot="dialog-meta"
@@ -706,20 +669,6 @@ export function DialogMeta({
706
669
  );
707
670
  }
708
671
 
709
- /** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
710
- export function DialogIntroActions({
711
- className,
712
- ...props
713
- }: React.ComponentProps<"div">) {
714
- return (
715
- <div
716
- data-slot="dialog-intro-actions"
717
- className={cn(surfaceActionsClasses, className)}
718
- {...props}
719
- />
720
- );
721
- }
722
-
723
672
  export function DialogMedia({ className, ...props }: StructuralMediaProps) {
724
673
  return (
725
674
  <StructuralMedia slot="dialog-media" {...props} className={className} />
@@ -733,8 +682,6 @@ export function DialogBody({
733
682
  ...props
734
683
  }: React.ComponentProps<"div"> & { flush?: boolean }) {
735
684
  const { mode } = useDialogContext();
736
- const layout = useSurfaceLayout("dialog");
737
- validateSurfaceIntro(props.children, DialogIntro, "DialogBody");
738
685
  return (
739
686
  <div
740
687
  data-slot="dialog-body"
@@ -743,7 +690,7 @@ export function DialogBody({
743
690
  ? "flex min-w-0 flex-col gap-6"
744
691
  : cn(
745
692
  "min-h-0 flex-1 overflow-y-auto",
746
- surfaceRegionClasses("dialog", layout, "body", { flush }),
693
+ surfaceRegionClasses("dialog", "body", { flush }),
747
694
  ),
748
695
  className,
749
696
  )}
@@ -759,7 +706,6 @@ export function DialogFooter({
759
706
  ...props
760
707
  }: React.ComponentProps<"div">) {
761
708
  const { mode } = useDialogContext();
762
- const layout = useSurfaceLayout("dialog");
763
709
  return (
764
710
  <div
765
711
  data-slot="dialog-footer"
@@ -768,7 +714,7 @@ export function DialogFooter({
768
714
  ? "w-full"
769
715
  : cn(
770
716
  "flex shrink-0 flex-row justify-end gap-2",
771
- surfaceRegionClasses("dialog", layout, "footer"),
717
+ surfaceRegionClasses("dialog", "footer"),
772
718
  ),
773
719
  className,
774
720
  )}
@@ -787,19 +733,11 @@ export function DialogTitle({
787
733
  level?: HeadingLevel;
788
734
  }) {
789
735
  const { mode } = useDialogContext();
790
- const intro = React.useContext(DialogIntroContext);
791
- const layout = useSurfaceLayout("dialog");
792
736
  const Title =
793
737
  mode === "alert" ? AlertDialogPrimitive.Title : DialogPrimitive.Title;
794
738
  const classes = cn(
795
- mode === "alert"
796
- ? undefined
797
- : intro
798
- ? "min-w-0 whitespace-normal"
799
- : "min-w-0 truncate",
800
- intro
801
- ? structuralHeaderClasses.page.title
802
- : surfaceTitleClasses("dialog", layout),
739
+ mode === "alert" ? undefined : "min-w-0 truncate",
740
+ surfaceTitleClasses("dialog"),
803
741
  className,
804
742
  );
805
743
  // O nível troca a TAG do mesmo nó, via `asChild` — nunca aninha um heading dentro
@@ -3,21 +3,17 @@ import { Dialog as DrawerPrimitive } from "radix-ui";
3
3
 
4
4
  import { cn } from "../../lib/cn.ts";
5
5
  import {
6
- SurfaceLayoutProvider,
7
6
  surfaceActionsClasses,
8
7
  surfaceDescriptionClasses,
9
8
  surfaceHeaderRowClasses,
10
9
  surfaceRegionClasses,
11
10
  surfaceTitleClasses,
12
- useSurfaceLayout,
13
- type SurfaceLayout,
14
11
  } from "../patterns/surface-anatomy.ts";
12
+ import { FillBoundary } from "../patterns/fill-region.ts";
15
13
  import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
16
14
  import {
17
15
  StructuralHeader,
18
16
  structuralMetaClasses,
19
- structuralHeaderClasses,
20
- validateSurfaceIntro,
21
17
  } from "../patterns/structural-header.tsx";
22
18
  import {
23
19
  StructuralMedia,
@@ -48,11 +44,10 @@ interface DrawerContentContextValue {
48
44
  const DrawerContentContext = React.createContext<DrawerContentContextValue>({
49
45
  showCloseButton: false,
50
46
  closeDisabled: false,
51
- closeSize: "sm",
47
+ closeSize: "xs",
52
48
  });
53
49
 
54
50
  const DrawerMetaContext = React.createContext(false);
55
- const DrawerIntroContext = React.createContext(false);
56
51
 
57
52
  /** Estado de aberto que a pilha de modais da camada flutuante acompanha (ADR 0052). */
58
53
  const DrawerStateContext = React.createContext({ open: false, modal: true });
@@ -150,8 +145,7 @@ function DrawerContent({
150
145
  side = "right",
151
146
  showCloseButton = true,
152
147
  closeDisabled = false,
153
- closeSize = "sm",
154
- layout = "sectioned",
148
+ closeSize = "xs",
155
149
  "aria-describedby": ariaDescribedBy,
156
150
  onEscapeKeyDown,
157
151
  onPointerDownOutside,
@@ -170,8 +164,6 @@ function DrawerContent({
170
164
  */
171
165
  closeDisabled?: boolean;
172
166
  closeSize?: CloseButtonSize;
173
- /** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
174
- layout?: SurfaceLayout;
175
167
  }) {
176
168
  const { open, modal } = React.useContext(DrawerStateContext);
177
169
  const layer = useLayerPortal();
@@ -209,7 +201,6 @@ function DrawerContent({
209
201
  <DrawerPrimitive.Content
210
202
  ref={composedRef}
211
203
  data-slot="drawer-content"
212
- data-layout={layout}
213
204
  {...{ [LAYER_MODAL_ATTRIBUTE]: internal || undefined }}
214
205
  className={cn(
215
206
  "fixed z-50 flex flex-col overflow-hidden rounded-xl border bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
@@ -237,13 +228,13 @@ function DrawerContent({
237
228
  {...props}
238
229
  >
239
230
  <LayerPortalProvider value={nestedPortal}>
240
- <SurfaceLayoutProvider value={layout}>
231
+ <FillBoundary>
241
232
  <DrawerContentContext.Provider
242
233
  value={{ showCloseButton, closeDisabled, closeSize }}
243
234
  >
244
235
  {children}
245
236
  </DrawerContentContext.Provider>
246
- </SurfaceLayoutProvider>
237
+ </FillBoundary>
247
238
  </LayerPortalProvider>
248
239
  </DrawerPrimitive.Content>
249
240
  </DrawerPortal>
@@ -253,14 +244,17 @@ function DrawerContent({
253
244
  function DrawerHeader({
254
245
  className,
255
246
  children,
247
+ tabs,
256
248
  ...props
257
- }: React.ComponentProps<"div">) {
258
- const layout = useSurfaceLayout("drawer");
249
+ }: React.ComponentProps<"div"> & {
250
+ tabs?: React.ReactNode;
251
+ }) {
259
252
  const { showCloseButton, closeDisabled, closeSize } =
260
253
  React.useContext(DrawerContentContext);
261
254
  return (
262
255
  <DrawerMetaContext.Provider value>
263
256
  <StructuralHeader
257
+ tabs={tabs}
264
258
  name="DrawerHeader"
265
259
  slot="drawer"
266
260
  slots={{
@@ -272,11 +266,11 @@ function DrawerHeader({
272
266
  actions: DrawerActions,
273
267
  }}
274
268
  leadingPlacement="inline"
275
- titleRequired={false}
269
+ titleRequired
276
270
  rowClassName={surfaceHeaderRowClasses("drawer")}
277
271
  className={cn(
278
272
  "text-left",
279
- surfaceRegionClasses("drawer", layout, "header"),
273
+ surfaceRegionClasses("drawer", "header"),
280
274
  className,
281
275
  )}
282
276
  trailing={
@@ -322,46 +316,13 @@ function DrawerActions({ className, ...props }: React.ComponentProps<"div">) {
322
316
  );
323
317
  }
324
318
 
325
- /** Título e ações da composição principal, como primeiro bloco de DrawerBody. */
326
- function DrawerIntro({
327
- className,
328
- children,
329
- ...props
330
- }: React.ComponentProps<"div">) {
331
- return (
332
- <DrawerIntroContext.Provider value>
333
- <DrawerMetaContext.Provider value>
334
- <StructuralHeader
335
- name="DrawerIntro"
336
- slot="drawer-intro"
337
- slots={{
338
- title: DrawerTitle,
339
- description: DrawerDescription,
340
- meta: DrawerMeta,
341
- actions: DrawerIntroActions,
342
- }}
343
- leadingPlacement="inline"
344
- titleRequired
345
- className={cn("text-left", className)}
346
- {...props}
347
- >
348
- {children}
349
- </StructuralHeader>
350
- </DrawerMetaContext.Provider>
351
- </DrawerIntroContext.Provider>
352
- );
353
- }
354
-
355
319
  /** Badges, datas e autoria abaixo do título e da orientação. */
356
320
  export function DrawerMeta({
357
321
  className,
358
322
  ...props
359
323
  }: React.ComponentProps<"div">) {
360
324
  const inside = React.useContext(DrawerMetaContext);
361
- if (!inside)
362
- throw new Error(
363
- "DrawerMeta deve estar dentro de DrawerHeader ou DrawerIntro.",
364
- );
325
+ if (!inside) throw new Error("DrawerMeta deve estar dentro de DrawerHeader.");
365
326
  return (
366
327
  <div
367
328
  data-slot="drawer-meta"
@@ -371,34 +332,18 @@ export function DrawerMeta({
371
332
  );
372
333
  }
373
334
 
374
- /** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
375
- function DrawerIntroActions({
376
- className,
377
- ...props
378
- }: React.ComponentProps<"div">) {
379
- return (
380
- <div
381
- data-slot="drawer-intro-actions"
382
- className={cn(surfaceActionsClasses, className)}
383
- {...props}
384
- />
385
- );
386
- }
387
-
388
335
  function DrawerBody({
389
336
  flush = false,
390
337
  className,
391
338
  style,
392
339
  ...props
393
340
  }: React.ComponentProps<"div"> & { flush?: boolean }) {
394
- const layout = useSurfaceLayout("drawer");
395
- validateSurfaceIntro(props.children, DrawerIntro, "DrawerBody");
396
341
  return (
397
342
  <div
398
343
  data-slot="drawer-body"
399
344
  className={cn(
400
345
  "min-h-0 flex-1 overflow-y-auto",
401
- surfaceRegionClasses("drawer", layout, "body", { flush }),
346
+ surfaceRegionClasses("drawer", "body", { flush }),
402
347
  className,
403
348
  )}
404
349
  style={style}
@@ -409,7 +354,6 @@ function DrawerBody({
409
354
  }
410
355
 
411
356
  function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
412
- const layout = useSurfaceLayout("drawer");
413
357
  return (
414
358
  <div
415
359
  data-slot="drawer-footer"
@@ -418,7 +362,7 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
418
362
  // preenche a altura da janela, e sem isso o rodapé subiria atrás do corpo. No
419
363
  // Dialog a altura é do conteúdo, então ele não precisa (ADR 0023).
420
364
  "mt-auto flex shrink-0 flex-row justify-end gap-2",
421
- surfaceRegionClasses("drawer", layout, "footer"),
365
+ surfaceRegionClasses("drawer", "footer"),
422
366
  className,
423
367
  )}
424
368
  {...props}
@@ -435,13 +379,9 @@ function DrawerTitle({
435
379
  /** Hierarquia do documento. Sem ele vale o `h2` que a primitiva emite. */
436
380
  level?: HeadingLevel;
437
381
  }) {
438
- const intro = React.useContext(DrawerIntroContext);
439
- const layout = useSurfaceLayout("drawer");
440
382
  const classes = cn(
441
- intro ? "min-w-0 whitespace-normal" : "min-w-0 truncate",
442
- intro
443
- ? structuralHeaderClasses.page.title
444
- : surfaceTitleClasses("drawer", layout),
383
+ "min-w-0 truncate",
384
+ surfaceTitleClasses("drawer"),
445
385
  className,
446
386
  );
447
387
  // O nível troca a TAG do mesmo nó, via `asChild` — a escala continua vindo do layout.
@@ -468,8 +408,6 @@ function DrawerTitle({
468
408
 
469
409
  export {
470
410
  DrawerActions,
471
- DrawerIntro,
472
- DrawerIntroActions,
473
411
  DrawerMedia,
474
412
  DrawerNavigation,
475
413
  Drawer,
@@ -184,7 +184,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
184
184
  <p
185
185
  data-slot="field-description"
186
186
  className={cn(
187
- "text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
187
+ "text-xs leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
188
188
  "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
189
189
  "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
190
190
  className
@@ -32,7 +32,7 @@ function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
32
32
  }
33
33
 
34
34
  export interface PaginationLinkProps extends Omit<React.ComponentProps<'a'>, 'children'> {
35
- /** Página atual: vira `outline` e ganha `aria-current="page"`. */
35
+ /** Página atual: usa fundo `subtle` e ganha `aria-current="page"`. */
36
36
  isActive?: boolean
37
37
  /** A escala de control.ts, via Button. `default` (números: altura fixa, largura mínima
38
38
  * quadrada que cresce com os dígitos) ou um `icon*` (setas quadradas, sem padding). */
@@ -59,7 +59,8 @@ function PaginationLink({ className, isActive, page, disabled, href, size = 'def
59
59
  'data-slot': 'pagination-link',
60
60
  'data-active': isActive,
61
61
  className: cn(
62
- buttonVariants({ variant: isActive ? 'outline' : 'ghost', size }),
62
+ buttonVariants({ context: 'neutral', variant: isActive ? 'subtle' : 'ghost', size }),
63
+ isActive && 'font-semibold',
63
64
  // Número: a largura mínima é quadrada e o conteúdo manda no resto. Escala `icon*`
64
65
  // fica quadrada de verdade (sem `has-[>svg]:px-*` do `default`).
65
66
  !square && cn('w-auto px-2 tabular-nums', minWidth),
@@ -95,7 +95,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
95
95
  <th
96
96
  data-slot="table-head"
97
97
  className={cn(
98
- "h-10 px-3 text-left align-middle font-medium whitespace-normal text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
98
+ "h-10 px-3 py-2 text-left align-middle font-medium whitespace-normal text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
99
99
  className,
100
100
  )}
101
101
  {...props}
@@ -7,6 +7,7 @@ import {
7
7
  structuralRegionGap,
8
8
  } from "../patterns/surface-anatomy.ts";
9
9
  import { controlHeight, type ControlSize } from "./control.ts";
10
+ import { useFillRegion, fillRegionClassName } from "../patterns/fill-region.ts";
10
11
 
11
12
  // Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
12
13
  // não tinha, e uma fileira densa ficava com as abas 0.25rem mais altas
@@ -18,6 +19,9 @@ const TabsSizeContext = React.createContext<TabsSize>("default");
18
19
  const TabsDistributionContext =
19
20
  React.createContext<TabsDistribution>("content");
20
21
  const TabsFramedContext = React.createContext(false);
22
+
23
+ /** Uso interno: o header estrutural fornece o divisor da faixa de abas. */
24
+ export const TabsHeaderContext = React.createContext(false);
21
25
  type TabsOrientation = "horizontal" | "vertical";
22
26
  const TabsOrientationContext =
23
27
  React.createContext<TabsOrientation>("horizontal");
@@ -70,6 +74,7 @@ function TabsList({
70
74
  style,
71
75
  ...props
72
76
  }: React.ComponentProps<typeof TabsPrimitive.List>) {
77
+ const inHeader = React.useContext(TabsHeaderContext);
73
78
  // O size vem do <Tabs> (contexto) — a altura mora aqui, na list.
74
79
  const size = React.useContext(TabsSizeContext);
75
80
  const orientation = React.useContext(TabsOrientationContext);
@@ -101,7 +106,7 @@ function TabsList({
101
106
  }}
102
107
  {...props}
103
108
  >
104
- {hasHorizontalLine && (
109
+ {hasHorizontalLine && !inHeader && (
105
110
  <span
106
111
  aria-hidden="true"
107
112
  data-slot="tabs-line"
@@ -196,21 +201,27 @@ function TabsListItem({
196
201
 
197
202
  function TabsPanel({
198
203
  className,
204
+ children,
199
205
  ...props
200
206
  }: React.ComponentProps<typeof TabsPrimitive.Content>) {
201
207
  const framed = React.useContext(TabsFramedContext);
202
208
  const orientation = React.useContext(TabsOrientationContext);
209
+ const fill = useFillRegion();
203
210
  return (
204
211
  <TabsPrimitive.Content
205
212
  data-slot="tabs-panel"
213
+ {...fill.attributes}
206
214
  className={cn(
207
215
  "flex-1 outline-none data-[state=inactive]:hidden",
208
216
  structuralBodyRhythm,
217
+ fill.filling && fillRegionClassName,
209
218
  framed && orientation === "horizontal" && "px-4 pb-4",
210
219
  className,
211
220
  )}
212
221
  {...props}
213
- />
222
+ >
223
+ {fill.provide(children)}
224
+ </TabsPrimitive.Content>
214
225
  );
215
226
  }
216
227
 
@@ -122,7 +122,12 @@ referência; itens estáticos permanecem como declarados no contrato.
122
122
 
123
123
  ## Widgets declarativos
124
124
 
125
- `FieldSpec.widget` aceita `textarea`, `code`, `lines`, `refItems`, `icon` e `toggle-group`.
125
+ `FieldSpec.widget` aceita `date`, `password`, `textarea`, `code`, `lines`, `refItems`, `icon` e `toggle-group`.
126
+ Para uma data civil, declare `widget: 'date'` e use uma string `YYYY-MM-DD` no schema e no
127
+ valor inicial. O formulário usa o [`DateInput`](/ui/date-input), com digitação em `DD/MM/AAAA`
128
+ e calendário em pt-BR. O valor válido enviado continua sendo a data civil, sem conversão para UTC.
129
+ Texto incompleto ou inválido chega ao schema para validação; use um schema de data civil.
130
+ `password` usa um input de senha com `autocomplete="new-password"`, adequado a cadastro e redefinição.
126
131
  O literal canônico do grupo de escolhas é sempre kebab-case: `widget: 'toggle-group'`. Em enum ele
127
132
  renderiza seleção única; em array de enum, seleção múltipla. As opções seguem a mesma precedência e
128
133
  o mesmo `SelectOption` rico dos selects. Use esse widget quando cada opção funcionar como uma