@softize/opus 18.0.1 → 18.1.1

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 (129) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/PROMOTED.md +4 -5
  3. package/README.md +5 -4
  4. package/bin/cli.mjs +4 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +3 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
  8. package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
  9. package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
  10. package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +54 -0
  11. package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
  12. package/docs/adr/0019-productive-surfaces-use-compact-density.md +65 -0
  13. package/docs/code-style.md +2 -2
  14. package/docs/consumer-upgrade-propagation.md +1 -1
  15. package/docs/data-products.md +5 -3
  16. package/docs/protocol.md +6 -6
  17. package/docs/relative-unit-scale.md +9 -2
  18. package/docs/releasing.md +28 -4
  19. package/package.json +1 -1
  20. package/registry/skills/build-opus-ui/references/evaluations.md +3 -1
  21. package/registry/skills/build-opus-ui/references/ui-patterns.md +20 -4
  22. package/src/auth/drivers/jwt.ts +2 -1
  23. package/src/core/runtime.ts +32 -5
  24. package/src/core/types.ts +16 -7
  25. package/src/mcp/index.ts +13 -1
  26. package/src/ui/components/patterns/action-list-dialog.tsx +2 -2
  27. package/src/ui/components/patterns/content-header.tsx +2 -2
  28. package/src/ui/components/patterns/form-dialog.tsx +7 -2
  29. package/src/ui/components/patterns/form.tsx +1 -1
  30. package/src/ui/components/patterns/list.tsx +239 -47
  31. package/src/ui/components/patterns/presentation.tsx +7 -5
  32. package/src/ui/components/patterns/sidebar.tsx +1 -1
  33. package/src/ui/components/patterns/state-surface.tsx +2 -2
  34. package/src/ui/components/patterns/surface-header.tsx +4 -4
  35. package/src/ui/components/primitives/alert.tsx +2 -2
  36. package/src/ui/components/primitives/breadcrumb.tsx +1 -1
  37. package/src/ui/components/primitives/button-group.tsx +1 -1
  38. package/src/ui/components/primitives/button.tsx +3 -3
  39. package/src/ui/components/primitives/calendar.tsx +1 -1
  40. package/src/ui/components/primitives/card.tsx +1 -1
  41. package/src/ui/components/primitives/close-button.tsx +40 -0
  42. package/src/ui/components/primitives/detail.tsx +68 -30
  43. package/src/ui/components/primitives/dialog.tsx +36 -21
  44. package/src/ui/components/primitives/drawer.tsx +26 -19
  45. package/src/ui/components/primitives/empty-value.tsx +3 -3
  46. package/src/ui/components/primitives/empty.tsx +1 -1
  47. package/src/ui/components/primitives/field.tsx +12 -12
  48. package/src/ui/components/primitives/icon-picker.tsx +1 -1
  49. package/src/ui/components/primitives/input-group.tsx +1 -1
  50. package/src/ui/components/primitives/input.tsx +2 -2
  51. package/src/ui/components/primitives/item.tsx +5 -5
  52. package/src/ui/components/primitives/pagination.tsx +4 -4
  53. package/src/ui/components/primitives/radio-group.tsx +1 -1
  54. package/src/ui/components/primitives/select.tsx +3 -3
  55. package/src/ui/components/primitives/table.tsx +26 -17
  56. package/src/ui/components/primitives/tabs.tsx +80 -23
  57. package/src/ui/components/primitives/textarea.tsx +1 -1
  58. package/src/ui/components/primitives/toggle-group.tsx +9 -2
  59. package/src/ui/docs/content/action-form-dialog.md +11 -4
  60. package/src/ui/docs/content/action-form.md +13 -3
  61. package/src/ui/docs/content/action-list-dialog.md +5 -7
  62. package/src/ui/docs/content/action-list.md +53 -5
  63. package/src/ui/docs/content/action-trigger.md +9 -5
  64. package/src/ui/docs/content/action-view.md +12 -8
  65. package/src/ui/docs/content/actions.md +36 -13
  66. package/src/ui/docs/content/ai.md +26 -7
  67. package/src/ui/docs/content/alert.md +6 -3
  68. package/src/ui/docs/content/aspect-ratio.md +2 -2
  69. package/src/ui/docs/content/auth.md +25 -10
  70. package/src/ui/docs/content/avatar.md +1 -1
  71. package/src/ui/docs/content/badge.md +2 -2
  72. package/src/ui/docs/content/breadcrumb.md +3 -2
  73. package/src/ui/docs/content/button.md +33 -8
  74. package/src/ui/docs/content/calendar.md +1 -1
  75. package/src/ui/docs/content/card.md +1 -1
  76. package/src/ui/docs/content/carousel.md +14 -3
  77. package/src/ui/docs/content/chat.md +1 -1
  78. package/src/ui/docs/content/cli.md +13 -7
  79. package/src/ui/docs/content/command.md +34 -2
  80. package/src/ui/docs/content/composer.md +1 -1
  81. package/src/ui/docs/content/content.md +5 -4
  82. package/src/ui/docs/content/customization.md +12 -2
  83. package/src/ui/docs/content/cycle.md +7 -5
  84. package/src/ui/docs/content/data-state.md +6 -5
  85. package/src/ui/docs/content/data.md +3 -3
  86. package/src/ui/docs/content/detail.md +12 -10
  87. package/src/ui/docs/content/dialog.md +14 -7
  88. package/src/ui/docs/content/dictionary-value.md +1 -1
  89. package/src/ui/docs/content/dock.md +23 -2
  90. package/src/ui/docs/content/dot.md +0 -2
  91. package/src/ui/docs/content/drawer.md +7 -4
  92. package/src/ui/docs/content/empty-value.md +4 -4
  93. package/src/ui/docs/content/empty.md +1 -4
  94. package/src/ui/docs/content/events.md +1 -1
  95. package/src/ui/docs/content/field.md +21 -12
  96. package/src/ui/docs/content/getting-started.md +4 -2
  97. package/src/ui/docs/content/icon-picker.md +2 -2
  98. package/src/ui/docs/content/input-otp.md +2 -0
  99. package/src/ui/docs/content/input.md +2 -3
  100. package/src/ui/docs/content/item.md +6 -3
  101. package/src/ui/docs/content/kbd.md +2 -1
  102. package/src/ui/docs/content/mcp.md +10 -4
  103. package/src/ui/docs/content/menu.md +27 -0
  104. package/src/ui/docs/content/page.md +20 -6
  105. package/src/ui/docs/content/pagination.md +9 -2
  106. package/src/ui/docs/content/popover.md +2 -2
  107. package/src/ui/docs/content/presentation.md +48 -47
  108. package/src/ui/docs/content/progress.md +2 -6
  109. package/src/ui/docs/content/runtime.md +8 -5
  110. package/src/ui/docs/content/scheduler.md +1 -1
  111. package/src/ui/docs/content/select.md +13 -8
  112. package/src/ui/docs/content/sidebar.md +3 -2
  113. package/src/ui/docs/content/skeleton.md +1 -1
  114. package/src/ui/docs/content/slider.md +4 -4
  115. package/src/ui/docs/content/spinner.md +3 -3
  116. package/src/ui/docs/content/tabs.md +22 -12
  117. package/src/ui/docs/content/testing.md +4 -2
  118. package/src/ui/docs/content/toast.md +5 -6
  119. package/src/ui/docs/content/toggle.md +37 -0
  120. package/src/ui/docs/content/tokens.md +45 -2
  121. package/src/ui/docs/content/tooltip.md +4 -3
  122. package/src/ui/docs/content/truncate.md +3 -2
  123. package/src/ui/docs/content/ui.md +3 -1
  124. package/src/ui/docs/content/upgrading.md +43 -13
  125. package/src/ui/docs/doc-client.tsx +1 -1
  126. package/src/ui/docs/registry.tsx +30 -5
  127. package/src/ui/meta.ts +4 -4
  128. package/src/ui/react.tsx +1 -0
  129. package/src/ui/theme.css +3 -0
@@ -103,6 +103,11 @@ import {
103
103
  } from "../primitives/input-group.tsx";
104
104
  import { Separator } from "../primitives/separator.tsx";
105
105
  import { Label } from "../primitives/label.tsx";
106
+ import {
107
+ FieldGroup,
108
+ FieldLegend,
109
+ FieldSet,
110
+ } from "../primitives/field.tsx";
106
111
  import { Search } from "lucide-react";
107
112
  import { Select, type SelectOption } from "../primitives/select.tsx";
108
113
  import {
@@ -145,6 +150,10 @@ function isEmptyValue(v: unknown): boolean {
145
150
  );
146
151
  }
147
152
 
153
+ function filterPlacement(spec: FilterSpec): "inline" | "advanced" | "external" {
154
+ return spec.placement ?? (spec.advanced === true ? "advanced" : "inline");
155
+ }
156
+
148
157
  // =============================================================================
149
158
  // Tipos públicos
150
159
  // =============================================================================
@@ -264,6 +273,9 @@ export interface ActionFilterBarProps {
264
273
  state: ActionFilterState;
265
274
  onStateChange: (next: ActionFilterState) => void;
266
275
  filterOptions?: Record<string, SelectOption[]>;
276
+ /** Composição visual do diálogo de filtros avançados. A largura é derivada
277
+ * internamente do número de colunas. */
278
+ advancedFilters?: AdvancedFiltersLayout;
267
279
  onRefresh?: () => Promise<void> | void;
268
280
  refreshing?: boolean;
269
281
  /** Controles auxiliares, agrupados com o refresh em um ButtonGroup espaçado. */
@@ -272,6 +284,11 @@ export interface ActionFilterBarProps {
272
284
  actions?: ReactNode;
273
285
  }
274
286
 
287
+ export interface AdvancedFiltersLayout {
288
+ /** Número de colunas dos grupos de filtros. */
289
+ columns?: 1 | 2 | 3;
290
+ }
291
+
275
292
  /** Uma view alternativa da MESMA listagem (board, galeria, lista, calendário…). A
276
293
  * apresentação é de quem chama; o pattern dá o segment, o estado e os dados. */
277
294
  export interface ActionListView<TItem> {
@@ -307,6 +324,8 @@ export interface ActionListProps<TInput, TItem> {
307
324
  cells?: Record<string, (item: TItem) => ReactNode>;
308
325
  /** Opções de runtime pros filtros select/lookup (chave = nome do filtro). */
309
326
  filterOptions?: Record<string, SelectOption[]>;
327
+ /** Composição visual do diálogo de filtros avançados. Default: uma coluna. */
328
+ advancedFilters?: AdvancedFiltersLayout;
310
329
  /** Views alternativas da mesma listagem — o segment na toolbar alterna entre a
311
330
  * tabela ('table', quando há columns) e estas. Ex.: { board: { label, render } }. */
312
331
  views?: Record<string, ActionListView<TItem>>;
@@ -547,9 +566,20 @@ function FilterField({
547
566
  />
548
567
  );
549
568
  }
550
- // text · number · date: aplica no blur/Enter (texto) ou no change (date).
551
- const type =
552
- spec.type === "number" ? "number" : spec.type === "date" ? "date" : "text";
569
+ if (spec.type === "date") {
570
+ return (
571
+ <DateFilterField
572
+ id={`filter-${name}`}
573
+ value={(value as string | undefined) ?? ""}
574
+ placeholder={placeholder}
575
+ className={cn("w-40", className)}
576
+ disabled={!enabled}
577
+ onChange={onChange}
578
+ />
579
+ );
580
+ }
581
+ // text · number: aplica no blur/Enter.
582
+ const type = spec.type === "number" ? "number" : "text";
553
583
  return (
554
584
  <ClearableInput
555
585
  id={`filter-${name}`}
@@ -563,6 +593,106 @@ function FilterField({
563
593
  );
564
594
  }
565
595
 
596
+ function parseIsoDay(value: string): Date | undefined {
597
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return undefined;
598
+ const [year, month, day] = value.split("-").map(Number);
599
+ const parsed = new Date(year ?? 0, (month ?? 1) - 1, day ?? 1);
600
+ return parsed.getFullYear() === year &&
601
+ parsed.getMonth() === (month ?? 1) - 1 &&
602
+ parsed.getDate() === day
603
+ ? parsed
604
+ : undefined;
605
+ }
606
+
607
+ function isValidDateFilterValue(value: unknown, multiple: boolean): boolean {
608
+ if (multiple) {
609
+ return (
610
+ Array.isArray(value) &&
611
+ value.length > 0 &&
612
+ value.every(
613
+ (item) => typeof item === "string" && parseIsoDay(item) !== undefined,
614
+ )
615
+ );
616
+ }
617
+ return typeof value === "string" && parseIsoDay(value) !== undefined;
618
+ }
619
+
620
+ function toIsoDay(date: Date): string {
621
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
622
+ }
623
+
624
+ function DateFilterField({
625
+ id,
626
+ value,
627
+ placeholder,
628
+ className,
629
+ disabled,
630
+ onChange,
631
+ }: {
632
+ id: string;
633
+ value: string;
634
+ placeholder: string;
635
+ className?: string;
636
+ disabled: boolean;
637
+ onChange: (value: unknown) => void;
638
+ }): React.ReactElement {
639
+ const [open, setOpen] = useState(false);
640
+ const selected = parseIsoDay(value);
641
+ const label =
642
+ selected?.toLocaleDateString("pt-BR", {
643
+ day: "2-digit",
644
+ month: "2-digit",
645
+ year: "numeric",
646
+ }) ?? placeholder;
647
+
648
+ return (
649
+ <Popover open={open} onOpenChange={setOpen}>
650
+ <PopoverTrigger asChild>
651
+ <Button
652
+ id={id}
653
+ type="button"
654
+ variant="outline"
655
+ className={cn("justify-start gap-2 font-normal", className)}
656
+ disabled={disabled}
657
+ >
658
+ <CalendarIcon className="h-3.5 w-3.5 shrink-0" />
659
+ <span className={cn("truncate", selected === undefined && "text-muted-foreground")}>
660
+ {label}
661
+ </span>
662
+ </Button>
663
+ </PopoverTrigger>
664
+ <PopoverContent align="start" className="w-auto p-0">
665
+ <Calendar
666
+ mode="single"
667
+ defaultMonth={selected}
668
+ selected={selected}
669
+ onSelect={(day) => {
670
+ if (day === undefined) return;
671
+ onChange(toIsoDay(day));
672
+ setOpen(false);
673
+ }}
674
+ />
675
+ {selected !== undefined && (
676
+ <div className="border-t border-border p-2">
677
+ <Button
678
+ type="button"
679
+ variant="ghost"
680
+ size="sm"
681
+ className="w-full"
682
+ onClick={() => {
683
+ onChange("");
684
+ setOpen(false);
685
+ }}
686
+ >
687
+ Limpar data
688
+ </Button>
689
+ </div>
690
+ )}
691
+ </PopoverContent>
692
+ </Popover>
693
+ );
694
+ }
695
+
566
696
  /** Input com limpar (a receita clearable dos textos): Input/InputGroup são locked, o X
567
697
  * entra por composição — addon à direita, visível só com texto. Uncontrolled (aplica
568
698
  * no blur/Enter, como a busca); o X zera o campo E aplica o vazio. */
@@ -831,6 +961,7 @@ function AdvancedFiltersDialog({
831
961
  onApply,
832
962
  filterOptions,
833
963
  cascade,
964
+ columns = 1,
834
965
  }: {
835
966
  open: boolean;
836
967
  onOpenChange: (o: boolean) => void;
@@ -838,6 +969,7 @@ function AdvancedFiltersDialog({
838
969
  values: Record<string, unknown>;
839
970
  onApply: (next: Record<string, unknown>) => void;
840
971
  filterOptions?: Record<string, SelectOption[]> | undefined;
972
+ columns?: 1 | 2 | 3;
841
973
  /** A cascata do `depends` do pai — mudar um campo limpa os dependentes no draft. */
842
974
  cascade: (
843
975
  changed: string,
@@ -850,53 +982,83 @@ function AdvancedFiltersDialog({
850
982
  if (open) setDraft(values);
851
983
  }, [open, values]);
852
984
 
985
+ const sections = useMemo(() => {
986
+ const grouped = new Map<string, Array<[string, FilterSpec]>>();
987
+ for (const entry of specs) {
988
+ const section = text(entry[1].section, "Outros");
989
+ const current = grouped.get(section) ?? [];
990
+ current.push(entry);
991
+ grouped.set(section, current);
992
+ }
993
+ return [...grouped];
994
+ }, [specs]);
995
+ const sectioned = specs.some(([, spec]) => spec.section !== undefined);
996
+ const dialogWidth = {
997
+ 1: "max-w-sm",
998
+ 2: "max-w-2xl",
999
+ 3: "max-w-4xl",
1000
+ }[columns];
1001
+ const gridColumns = {
1002
+ 1: "grid-cols-1",
1003
+ 2: "grid-cols-2",
1004
+ 3: "grid-cols-3",
1005
+ }[columns];
1006
+
1007
+ const field = ([name, spec]: [string, FilterSpec]): React.ReactElement => (
1008
+ <div key={name} className="space-y-2">
1009
+ <Label htmlFor={`filter-${name}`}>{text(spec.label, name)}</Label>
1010
+ <FilterField
1011
+ name={name}
1012
+ spec={spec}
1013
+ value={draft[name]}
1014
+ allValues={draft}
1015
+ className="w-full"
1016
+ onChange={(v) => setDraft((d) => cascade(name, { ...d, [name]: v }))}
1017
+ options={optionsFor(name, spec, filterOptions)}
1018
+ />
1019
+ </div>
1020
+ );
1021
+
1022
+ const clear = (): void => {
1023
+ const cleared = { ...draft };
1024
+ for (const [name] of specs) delete cleared[name];
1025
+ onApply(cleared);
1026
+ onOpenChange(false);
1027
+ };
1028
+
1029
+ const apply = (): void => {
1030
+ onApply(draft);
1031
+ onOpenChange(false);
1032
+ };
1033
+
853
1034
  return (
854
1035
  <Dialog open={open} onOpenChange={onOpenChange}>
855
- {/* Estreito de propósito (form de filtros, campos em coluna full width); sem
856
- subtítulo — o título basta (aria-describedby explícito cala o aviso do Radix). */}
857
- <DialogContent className="sm:max-w-sm" aria-describedby={undefined}>
1036
+ <DialogContent className={dialogWidth} aria-describedby={undefined}>
858
1037
  <DialogHeader>
859
1038
  <DialogTitle>Filtros</DialogTitle>
860
1039
  </DialogHeader>
861
1040
  <DialogBody className="space-y-4">
862
- {specs.map(([name, spec]) => (
863
- <div key={name} className="space-y-2">
864
- <Label htmlFor={`filter-${name}`}>{text(spec.label, name)}</Label>
865
- <FilterField
866
- name={name}
867
- spec={spec}
868
- value={draft[name]}
869
- allValues={draft}
870
- className="w-full"
871
- onChange={(v) =>
872
- setDraft((d) => cascade(name, { ...d, [name]: v }))
873
- }
874
- options={optionsFor(name, spec, filterOptions)}
875
- />
876
- </div>
877
- ))}
1041
+ {sectioned
1042
+ ? sections.map(([section, entries]) => (
1043
+ <FieldSet key={section}>
1044
+ <FieldLegend variant="label">{section}</FieldLegend>
1045
+ <FieldGroup className={cn("grid gap-4", gridColumns)}>
1046
+ {entries.map(field)}
1047
+ </FieldGroup>
1048
+ </FieldSet>
1049
+ ))
1050
+ : (
1051
+ <FieldGroup className={cn("grid gap-4", gridColumns)}>
1052
+ {specs.map(field)}
1053
+ </FieldGroup>
1054
+ )}
878
1055
  </DialogBody>
879
1056
  <DialogFooter>
880
1057
  <ButtonGroup mode="spaced" distribution="equal">
881
- <Button
882
- variant="outline"
883
- onClick={() => {
884
- const cleared = { ...draft };
885
- for (const [name] of specs) delete cleared[name];
886
- onApply(cleared);
887
- onOpenChange(false);
888
- }}
889
- >
1058
+ <Button variant="outline" onClick={clear}>
890
1059
  Limpar
891
1060
  </Button>
892
- <Button
893
- onClick={() => {
894
- onApply(draft);
895
- onOpenChange(false);
896
- }}
897
- >
898
- Aplicar
899
- </Button>
1061
+ <Button onClick={apply}>Aplicar</Button>
900
1062
  </ButtonGroup>
901
1063
  </DialogFooter>
902
1064
  </DialogContent>
@@ -910,6 +1072,7 @@ export function ActionFilterBar({
910
1072
  state,
911
1073
  onStateChange,
912
1074
  filterOptions,
1075
+ advancedFilters,
913
1076
  onRefresh,
914
1077
  refreshing = false,
915
1078
  controls,
@@ -917,7 +1080,12 @@ export function ActionFilterBar({
917
1080
  }: ActionFilterBarProps): React.ReactElement {
918
1081
  const dicts = useDicts();
919
1082
  const filterSpecs = Object.entries(action.filters ?? {});
920
- const inlineSpecs = filterSpecs.filter(([, spec]) => spec.advanced !== true);
1083
+ const visibleFilterSpecs = filterSpecs.filter(
1084
+ ([, spec]) => filterPlacement(spec) !== "external",
1085
+ );
1086
+ const inlineSpecs = visibleFilterSpecs.filter(
1087
+ ([, spec]) => filterPlacement(spec) === "inline",
1088
+ );
921
1089
  const hasSearch = (action.text?.fields.length ?? 0) > 0;
922
1090
  const periods = action.periods ?? [];
923
1091
  const defaultPeriod =
@@ -976,9 +1144,9 @@ export function ActionFilterBar({
976
1144
  const overflowSpecs = Number.isFinite(inlineFit)
977
1145
  ? inlineSpecs.slice(inlineFit)
978
1146
  : [];
979
- const modalSpecs = filterSpecs.filter(
1147
+ const modalSpecs = visibleFilterSpecs.filter(
980
1148
  ([name, spec]) =>
981
- spec.advanced === true ||
1149
+ filterPlacement(spec) === "advanced" ||
982
1150
  overflowSpecs.some(([overflowName]) => overflowName === name),
983
1151
  );
984
1152
  const setFilter = (name: string, value: unknown): void => {
@@ -1003,7 +1171,7 @@ export function ActionFilterBar({
1003
1171
  };
1004
1172
  });
1005
1173
  const searchFirst =
1006
- hasSearch && periods.length === 0 && filterSpecs.length === 0;
1174
+ hasSearch && periods.length === 0 && visibleFilterSpecs.length === 0;
1007
1175
  const searchBox = hasSearch ? (
1008
1176
  <ClearableInput
1009
1177
  className="w-64 min-w-36"
@@ -1139,6 +1307,7 @@ export function ActionFilterBar({
1139
1307
  cascade={cascadeClear}
1140
1308
  onApply={(filters) => onStateChange({ ...state, filters })}
1141
1309
  filterOptions={filterOptions}
1310
+ columns={advancedFilters?.columns}
1142
1311
  />
1143
1312
  )}
1144
1313
  </div>
@@ -1182,6 +1351,7 @@ export function ActionList<TInput, TItem>({
1182
1351
  columns,
1183
1352
  cells,
1184
1353
  filterOptions,
1354
+ advancedFilters,
1185
1355
  views,
1186
1356
  batch,
1187
1357
  rowId = (item: TItem) => String((item as { id?: unknown }).id ?? ""),
@@ -1235,7 +1405,9 @@ export function ActionList<TInput, TItem>({
1235
1405
  const setStateResetPage = (next: ActionListState): void =>
1236
1406
  setState({ ...next, page: 1 });
1237
1407
 
1238
- const filterSpecs = Object.entries(action.filters ?? {});
1408
+ const filterSpecs = Object.entries(action.filters ?? {}).filter(
1409
+ ([, spec]) => filterPlacement(spec) !== "external",
1410
+ );
1239
1411
  const hasSearch = (action.text?.fields.length ?? 0) > 0;
1240
1412
 
1241
1413
  // — Views: 'table' (quando há colunas) + as nomeadas. O segment só existe com 2+. —
@@ -1263,7 +1435,14 @@ export function ActionList<TInput, TItem>({
1263
1435
  const effectiveInput = useMemo(() => {
1264
1436
  const filled: Record<string, unknown> = {};
1265
1437
  for (const [name, v] of Object.entries(state.filters)) {
1266
- if (!isEmptyValue(v)) filled[name] = v;
1438
+ if (isEmptyValue(v)) continue;
1439
+ const filter = action.filters?.[name];
1440
+ if (
1441
+ filter?.type === "date" &&
1442
+ !isValidDateFilterValue(v, filter.multiple === true)
1443
+ )
1444
+ continue;
1445
+ filled[name] = v;
1267
1446
  }
1268
1447
  // Período: preset materializa AQUI (a URL guarda o preset relativo); custom usa o
1269
1448
  // range. state.period null = o default do contrato (sempre aplicado).
@@ -1438,6 +1617,7 @@ export function ActionList<TInput, TItem>({
1438
1617
  state={state}
1439
1618
  onStateChange={(next) => setStateResetPage({ ...state, ...next })}
1440
1619
  filterOptions={filterOptions}
1620
+ advancedFilters={advancedFilters}
1441
1621
  onRefresh={() => refetch()}
1442
1622
  refreshing={isLoading}
1443
1623
  controls={
@@ -1901,8 +2081,14 @@ export function listParamsToState(
1901
2081
  const filters: Record<string, unknown> = {};
1902
2082
  for (const [name, spec] of Object.entries(action.filters ?? {})) {
1903
2083
  const v = params.get(name);
1904
- if (v !== null && v !== "")
1905
- filters[name] = spec.multiple === true ? v.split(",") : v;
2084
+ if (v === null || v === "") continue;
2085
+ const value = spec.multiple === true ? v.split(",") : v;
2086
+ if (
2087
+ spec.type === "date" &&
2088
+ !isValidDateFilterValue(value, spec.multiple === true)
2089
+ )
2090
+ continue;
2091
+ filters[name] = value;
1906
2092
  }
1907
2093
  const cols = params.get("cols");
1908
2094
  const limitParam = Number(params.get("limit") ?? "");
@@ -1945,6 +2131,12 @@ export function listStateToParams(
1945
2131
  }
1946
2132
  for (const [name, value] of Object.entries(state.filters)) {
1947
2133
  if (isEmptyValue(value)) continue;
2134
+ const filter = action.filters?.[name];
2135
+ if (
2136
+ filter?.type === "date" &&
2137
+ !isValidDateFilterValue(value, filter.multiple === true)
2138
+ )
2139
+ continue;
1948
2140
  qs.set(name, Array.isArray(value) ? value.join(",") : String(value));
1949
2141
  }
1950
2142
  if (state.period === "custom") {
@@ -136,7 +136,10 @@ function PresentationFrame({
136
136
  {hasBodyFooter ? (
137
137
  <span
138
138
  ref={setFooterNode}
139
- className="contents [&>*]:min-w-0 [&>*]:w-full [&>*]:flex-1"
139
+ className={cn(
140
+ "contents",
141
+ equal && "[&>*]:min-w-0 [&>*]:w-full [&>*]:flex-1",
142
+ )}
140
143
  />
141
144
  ) : null}
142
145
  {footerActions}
@@ -258,7 +261,7 @@ function PresentationFrame({
258
261
  </DialogHeader>
259
262
  <DialogBody>{body}</DialogBody>
260
263
  {hasFooter ? (
261
- <DialogFooter>{renderFooterActions(true)}</DialogFooter>
264
+ <DialogFooter>{renderFooterActions()}</DialogFooter>
262
265
  ) : null}
263
266
  </DialogContent>
264
267
  </Dialog>
@@ -293,7 +296,7 @@ function PresentationFrame({
293
296
  </DrawerHeader>
294
297
  <DrawerBody>{body}</DrawerBody>
295
298
  {hasFooter ? (
296
- <DrawerFooter>{renderFooterActions(true)}</DrawerFooter>
299
+ <DrawerFooter>{renderFooterActions()}</DrawerFooter>
297
300
  ) : null}
298
301
  </DrawerContent>
299
302
  </Drawer>
@@ -554,7 +557,6 @@ export function Presentation({
554
557
  defaultValues={bodyInput}
555
558
  submitLabel={definition.body.submitLabel}
556
559
  onCancel={invocation.surface === "page" ? undefined : closeSurface}
557
- cancelVariant="outline"
558
560
  onSuccess={(data) => applyEffects(definition.body.onSuccess, data)}
559
561
  disabled={hasSurfaceBlock}
560
562
  onLoadingChange={setBodyLoading}
@@ -695,7 +697,7 @@ function PresentationInspectionDialog({
695
697
 
696
698
  return (
697
699
  <Dialog open={open} onOpenChange={onOpenChange}>
698
- <DialogContent className="sm:max-w-3xl" aria-describedby={undefined}>
700
+ <DialogContent className="max-w-3xl" aria-describedby={undefined}>
699
701
  <DialogHeader>
700
702
  <DialogTitle>{title}</DialogTitle>
701
703
  </DialogHeader>
@@ -94,7 +94,7 @@ export function PaneBody({
94
94
  );
95
95
  }
96
96
 
97
- /** @deprecated Use PaneBody. Mantido durante a versão 12 para migração gradual. */
97
+ /** @deprecated Use PaneBody. Alias mantido para migração; sai numa próxima versão major. */
98
98
  export function PaneContent({
99
99
  className,
100
100
  children,
@@ -238,8 +238,8 @@ export function EmptySurface({
238
238
  data-frame={frame}
239
239
  className={cn(
240
240
  frame === 'structural' && 'border-solid',
241
- frame === 'bare' && 'border-0 p-0 md:p-0',
242
- compact && 'gap-4 p-6 md:p-6',
241
+ frame === 'bare' && 'border-0 p-0',
242
+ compact && 'gap-4 p-6',
243
243
  className,
244
244
  )}
245
245
  >
@@ -28,13 +28,13 @@ export const surfaceHeaderClasses: Record<
28
28
  title: "text-3xl font-semibold tracking-tight",
29
29
  description: "mt-1 truncate text-sm text-muted-foreground",
30
30
  count: "font-mono text-base text-muted-foreground/60",
31
- actions: "flex w-full shrink-0 items-center gap-4 sm:w-auto",
31
+ actions: "flex w-auto shrink-0 items-center gap-4",
32
32
  },
33
33
  section: {
34
34
  title: "text-sm font-semibold",
35
35
  description: "mt-0.5 truncate text-xs text-muted-foreground",
36
36
  count: "font-mono text-xs text-muted-foreground/60",
37
- actions: "flex w-full shrink-0 items-center gap-2 sm:w-auto",
37
+ actions: "flex w-auto shrink-0 items-center gap-2",
38
38
  },
39
39
  };
40
40
 
@@ -182,7 +182,7 @@ export function SurfaceHeader({
182
182
  data-slot={`${slot}-header-row`}
183
183
  className={cn(
184
184
  actionSlots.length > 0 &&
185
- "flex flex-col items-start gap-4 sm:flex-row sm:items-end sm:justify-between",
185
+ "flex flex-row items-end justify-between gap-4",
186
186
  )}
187
187
  >
188
188
  {heading}
@@ -204,7 +204,7 @@ export function SurfaceHeader({
204
204
  leadingPlacement !== "inline" &&
205
205
  leading.length === 0 &&
206
206
  actionSlots.length > 0 &&
207
- "flex flex-col items-start gap-4 sm:flex-row sm:items-end sm:justify-between",
207
+ "flex flex-row items-end justify-between gap-4",
208
208
  className,
209
209
  )}
210
210
  {...props}
@@ -50,7 +50,7 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
50
50
  export function alertVariants(options: AlertStyleOptions = {}): string {
51
51
  const resolved = resolveAlertStyle(options);
52
52
  return cn(
53
- "relative grid w-full grid-cols-[0_minmax(0,1fr)_auto] items-stretch gap-x-0 gap-y-1 rounded-lg border p-4 text-sm has-[>[data-slot=alert-media]]:grid-cols-[3rem_minmax(0,1fr)_auto]",
53
+ "relative grid w-full grid-cols-[0_minmax(0,1fr)_auto] items-stretch gap-x-0 gap-y-1 rounded-lg border p-3 text-sm has-[>[data-slot=alert-media]]:grid-cols-[3rem_minmax(0,1fr)_auto]",
54
54
  resolved.variant === "outline"
55
55
  ? semanticOutlineClasses[resolved.context]
56
56
  : semanticSubtleClasses[resolved.context],
@@ -282,7 +282,7 @@ export function AlertDescription({
282
282
  <div
283
283
  data-slot="alert-description"
284
284
  className={cn(
285
- "text-sm text-muted-foreground [&>*+*]:mt-1 [&_p]:leading-relaxed",
285
+ "text-muted-foreground [&>*+*]:mt-1 [&_p]:leading-relaxed",
286
286
  className,
287
287
  )}
288
288
  {...props}
@@ -13,7 +13,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
13
13
  <ol
14
14
  data-slot="breadcrumb-list"
15
15
  className={cn(
16
- "flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
16
+ "flex flex-wrap items-center gap-2.5 text-sm break-words text-muted-foreground",
17
17
  className
18
18
  )}
19
19
  {...props}
@@ -23,7 +23,7 @@ const buttonGroupVariants = cva(
23
23
  distribution: {
24
24
  content: "",
25
25
  equal:
26
- "w-full flex-col-reverse gap-2 sm:flex-row [&>*]:min-w-0 [&>*]:w-full [&>*]:flex-1",
26
+ "w-full gap-2 [&>*]:min-w-0 [&>*]:w-full [&>*]:flex-1",
27
27
  },
28
28
  },
29
29
  compoundVariants: [
@@ -56,9 +56,9 @@ const subtleClasses: Record<ButtonContext, string> = {
56
56
  }
57
57
 
58
58
  const outlineClasses: Record<ButtonContext, string> = {
59
- neutral: 'border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground',
60
- primary: 'border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground',
61
- danger: 'border border-context-danger-border bg-background text-context-danger-emphasis hover:bg-context-danger-subtle',
59
+ neutral: 'border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground',
60
+ primary: 'border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground',
61
+ danger: 'border border-context-danger-border bg-transparent text-context-danger-emphasis hover:bg-context-danger-subtle',
62
62
  }
63
63
 
64
64
  const ghostClasses: Record<ButtonContext, string> = {
@@ -71,7 +71,7 @@ function Calendar({
71
71
  classNames={{
72
72
  root: cn("w-fit", defaultClassNames.root),
73
73
  months: cn(
74
- "relative flex flex-col gap-4 md:flex-row",
74
+ "relative flex flex-row gap-4",
75
75
  defaultClassNames.months,
76
76
  ),
77
77
  month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
@@ -101,7 +101,7 @@ export function CardBody({
101
101
  );
102
102
  }
103
103
 
104
- /** @deprecated Use CardBody. Mantido durante a versão 12 para migração gradual. */
104
+ /** @deprecated Use CardBody. Alias mantido para migração; sai numa próxima versão major. */
105
105
  export function CardContent({
106
106
  className,
107
107
  ...props
@@ -0,0 +1,40 @@
1
+ import * as React from "react";
2
+ import { XIcon } from "lucide-react";
3
+
4
+ import { Button, type ButtonProps } from "./button.tsx";
5
+
6
+ export type CloseButtonSize = "xs" | "sm";
7
+
8
+ const buttonSizeByCloseSize = {
9
+ xs: "icon-xs",
10
+ sm: "icon-sm",
11
+ } as const;
12
+
13
+ export interface CloseButtonProps extends Omit<
14
+ ButtonProps,
15
+ "busy" | "children" | "context" | "icon" | "shape" | "size" | "type" | "variant"
16
+ > {
17
+ size?: CloseButtonSize;
18
+ }
19
+
20
+ export function CloseButton({
21
+ size = "sm",
22
+ disabled = false,
23
+ "aria-label": ariaLabel = "Fechar",
24
+ ...props
25
+ }: CloseButtonProps): React.ReactElement {
26
+ return (
27
+ <Button
28
+ {...props}
29
+ type="button"
30
+ context="neutral"
31
+ variant="subtle"
32
+ size={buttonSizeByCloseSize[size]}
33
+ shape="pill"
34
+ disabled={disabled}
35
+ aria-label={ariaLabel}
36
+ >
37
+ <XIcon aria-hidden />
38
+ </Button>
39
+ );
40
+ }