@softize/opus 13.1.0 → 15.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.
- package/CHANGELOG.md +97 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +33 -5
- package/bin/lib/cli-shared.mjs +30 -1
- package/bin/lib/copy.mjs +279 -6
- package/bin/lib/db.mjs +2 -0
- package/docs/adr/0004-page-content-state-is-composed.md +39 -5
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +9 -3
- package/docs/adr/0009-page-title-does-not-carry-a-counter.md +57 -0
- package/docs/adr/0010-page-header-owns-page-chrome.md +73 -0
- package/docs/code-style.md +4 -1
- package/docs/data-layer.md +9 -0
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -3
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
- package/registry/templates/app/src/App.tsx +11 -6
- package/src/core/types.ts +3 -4
- package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
- package/src/ui/components/patterns/confirm.tsx +2 -31
- package/src/ui/components/patterns/content-header.tsx +42 -141
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/form.tsx +15 -15
- package/src/ui/components/patterns/list.tsx +55 -34
- package/src/ui/components/patterns/page-state.tsx +81 -51
- package/src/ui/components/patterns/page.tsx +228 -97
- package/src/ui/components/patterns/state-surface.tsx +262 -0
- package/src/ui/components/patterns/surface-header.tsx +204 -0
- package/src/ui/components/patterns/trigger.tsx +9 -10
- package/src/ui/components/patterns/view.tsx +14 -16
- package/src/ui/components/primitives/alert.tsx +1 -33
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +2 -43
- package/src/ui/components/primitives/button-group.tsx +34 -8
- package/src/ui/components/primitives/button.tsx +31 -35
- package/src/ui/components/primitives/control.ts +69 -0
- package/src/ui/components/primitives/dot.tsx +1 -30
- package/src/ui/components/primitives/input-group.tsx +11 -8
- package/src/ui/components/primitives/item.tsx +3 -1
- package/src/ui/components/primitives/menu.tsx +1 -7
- package/src/ui/components/primitives/pagination.tsx +16 -8
- package/src/ui/components/primitives/select.tsx +2 -2
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +4 -1
- package/src/ui/components/primitives/tabs.tsx +5 -3
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/docs/content/action-form.md +26 -0
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +35 -2
- package/src/ui/docs/content/action-trigger.md +5 -4
- package/src/ui/docs/content/action-view.md +3 -2
- package/src/ui/docs/content/alert.md +16 -4
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +48 -17
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/content.md +5 -4
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/page.md +160 -37
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/semantic-context.md +3 -2
- package/src/ui/docs/content/sidebar.md +2 -42
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/drivers/react.tsx +1 -6
- package/src/ui/meta.ts +8 -8
- package/src/ui/react.tsx +16 -18
- package/src/ui/components/patterns/shell-nav.tsx +0 -154
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
PaginationPrevious,
|
|
75
75
|
} from "../primitives/pagination.tsx";
|
|
76
76
|
import { Button } from "../primitives/button.tsx";
|
|
77
|
+
import { ButtonGroup } from "../primitives/button-group.tsx";
|
|
77
78
|
import { Calendar } from "../primitives/calendar.tsx";
|
|
78
79
|
import { Checkbox } from "../primitives/checkbox.tsx";
|
|
79
80
|
import {
|
|
@@ -262,7 +263,9 @@ export interface ActionFilterBarProps {
|
|
|
262
263
|
filterOptions?: Record<string, SelectOption[]>;
|
|
263
264
|
onRefresh?: () => Promise<void> | void;
|
|
264
265
|
refreshing?: boolean;
|
|
265
|
-
/**
|
|
266
|
+
/** Controles auxiliares, agrupados com o refresh em um ButtonGroup espaçado. */
|
|
267
|
+
controls?: ReactNode;
|
|
268
|
+
/** Ações do consumidor, separadas do grupo de controles. */
|
|
266
269
|
actions?: ReactNode;
|
|
267
270
|
}
|
|
268
271
|
|
|
@@ -293,6 +296,8 @@ export interface ActionListProps<TInput, TItem> {
|
|
|
293
296
|
action: ListActionLike;
|
|
294
297
|
/** Input BASE (escopo fixo, ex.: { workspaceId }) — a toolbar soma por cima. */
|
|
295
298
|
input: TInput;
|
|
299
|
+
/** Ações do consumidor no fim da barra de filtros, depois dos controles de exibição. */
|
|
300
|
+
toolbarActions?: ReactNode;
|
|
296
301
|
/** Tabela explícita — sobrepõe as colunas do contrato. */
|
|
297
302
|
columns?: ActionListColumn<TItem>[];
|
|
298
303
|
/** Células custom POR CIMA das colunas do contrato (chave = column.key). */
|
|
@@ -309,8 +314,12 @@ export interface ActionListProps<TInput, TItem> {
|
|
|
309
314
|
/** Itens por página (o pattern manda `limit` e `page` no input; o handler implementa
|
|
310
315
|
* o OFFSET e devolve `total`). O rodapé mostra o total e o pager quando transborda. */
|
|
311
316
|
pageSize?: number;
|
|
312
|
-
/** Mensagem
|
|
317
|
+
/** Mensagem do estado vazio. Default: "Nenhum resultado." */
|
|
313
318
|
emptyMessage?: string;
|
|
319
|
+
/** Mensagem do estado de erro (calma, sem o texto técnico). Default do DataState. */
|
|
320
|
+
errorMessage?: string;
|
|
321
|
+
/** Rótulo do botão de recuperação no erro. Default: "Tentar de novo". */
|
|
322
|
+
retryLabel?: string;
|
|
314
323
|
/** Sobrepõe o vazio derivado (items.length === 0) — ex.: form inline aberto conta
|
|
315
324
|
* como conteúdo, então os children devem renderizar mesmo com a lista vazia. */
|
|
316
325
|
empty?: (items: TItem[]) => boolean;
|
|
@@ -900,6 +909,7 @@ export function ActionFilterBar({
|
|
|
900
909
|
filterOptions,
|
|
901
910
|
onRefresh,
|
|
902
911
|
refreshing = false,
|
|
912
|
+
controls,
|
|
903
913
|
actions,
|
|
904
914
|
}: ActionFilterBarProps): React.ReactElement {
|
|
905
915
|
const dicts = useDicts();
|
|
@@ -1043,7 +1053,7 @@ export function ActionFilterBar({
|
|
|
1043
1053
|
<Tooltip>
|
|
1044
1054
|
<TooltipTrigger asChild>
|
|
1045
1055
|
<Button
|
|
1046
|
-
variant="
|
|
1056
|
+
variant="ghost"
|
|
1047
1057
|
size="icon"
|
|
1048
1058
|
className="shrink-0"
|
|
1049
1059
|
aria-label="Filtros"
|
|
@@ -1057,27 +1067,32 @@ export function ActionFilterBar({
|
|
|
1057
1067
|
)}
|
|
1058
1068
|
<div className="ml-auto flex min-w-0 items-center gap-2">
|
|
1059
1069
|
{!searchFirst && searchBox}
|
|
1060
|
-
{onRefresh !== undefined && (
|
|
1061
|
-
<
|
|
1062
|
-
|
|
1063
|
-
<
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1070
|
+
{(onRefresh !== undefined || controls !== undefined) && (
|
|
1071
|
+
<ButtonGroup mode="spaced">
|
|
1072
|
+
{onRefresh !== undefined && (
|
|
1073
|
+
<Tooltip>
|
|
1074
|
+
<TooltipTrigger asChild>
|
|
1075
|
+
<Button
|
|
1076
|
+
variant="ghost"
|
|
1077
|
+
size="icon"
|
|
1078
|
+
className="shrink-0"
|
|
1079
|
+
aria-label="Recarregar"
|
|
1080
|
+
onClick={() => void onRefresh()}
|
|
1081
|
+
disabled={refreshing}
|
|
1082
|
+
>
|
|
1083
|
+
<RefreshCw
|
|
1084
|
+
className={cn(
|
|
1085
|
+
"h-3.5 w-3.5",
|
|
1086
|
+
refreshing && "animate-spin",
|
|
1087
|
+
)}
|
|
1088
|
+
/>
|
|
1089
|
+
</Button>
|
|
1090
|
+
</TooltipTrigger>
|
|
1091
|
+
<TooltipContent>Recarregar</TooltipContent>
|
|
1092
|
+
</Tooltip>
|
|
1093
|
+
)}
|
|
1094
|
+
{controls}
|
|
1095
|
+
</ButtonGroup>
|
|
1081
1096
|
)}
|
|
1082
1097
|
{actions}
|
|
1083
1098
|
</div>
|
|
@@ -1130,6 +1145,7 @@ export function ActionFilterBar({
|
|
|
1130
1145
|
export function ActionList<TInput, TItem>({
|
|
1131
1146
|
action,
|
|
1132
1147
|
input,
|
|
1148
|
+
toolbarActions,
|
|
1133
1149
|
columns,
|
|
1134
1150
|
cells,
|
|
1135
1151
|
filterOptions,
|
|
@@ -1138,6 +1154,8 @@ export function ActionList<TInput, TItem>({
|
|
|
1138
1154
|
rowId = (item: TItem) => String((item as { id?: unknown }).id ?? ""),
|
|
1139
1155
|
pageSize = 50,
|
|
1140
1156
|
emptyMessage = "Nenhum resultado.",
|
|
1157
|
+
errorMessage,
|
|
1158
|
+
retryLabel,
|
|
1141
1159
|
empty,
|
|
1142
1160
|
loading: extraLoading,
|
|
1143
1161
|
onRowClick,
|
|
@@ -1388,7 +1406,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1388
1406
|
filterOptions={filterOptions}
|
|
1389
1407
|
onRefresh={() => refetch()}
|
|
1390
1408
|
refreshing={isLoading}
|
|
1391
|
-
|
|
1409
|
+
controls={
|
|
1392
1410
|
<>
|
|
1393
1411
|
{hasSegment && (
|
|
1394
1412
|
<ToggleGroup
|
|
@@ -1427,7 +1445,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1427
1445
|
<TooltipTrigger asChild>
|
|
1428
1446
|
<PopoverTrigger asChild>
|
|
1429
1447
|
<Button
|
|
1430
|
-
variant="
|
|
1448
|
+
variant="ghost"
|
|
1431
1449
|
size="icon"
|
|
1432
1450
|
className="shrink-0"
|
|
1433
1451
|
aria-label="Exibição"
|
|
@@ -1511,6 +1529,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1511
1529
|
</Popover>
|
|
1512
1530
|
</>
|
|
1513
1531
|
}
|
|
1532
|
+
actions={toolbarActions}
|
|
1514
1533
|
/>
|
|
1515
1534
|
</div>
|
|
1516
1535
|
) : null;
|
|
@@ -1615,7 +1634,9 @@ export function ActionList<TInput, TItem>({
|
|
|
1615
1634
|
className="w-px whitespace-nowrap text-right"
|
|
1616
1635
|
onClick={(e) => e.stopPropagation()}
|
|
1617
1636
|
>
|
|
1618
|
-
|
|
1637
|
+
<ButtonGroup mode="spaced" className="ml-auto">
|
|
1638
|
+
{rowActions(item)}
|
|
1639
|
+
</ButtonGroup>
|
|
1619
1640
|
</TableCell>
|
|
1620
1641
|
)}
|
|
1621
1642
|
</TableRow>
|
|
@@ -1629,7 +1650,9 @@ export function ActionList<TInput, TItem>({
|
|
|
1629
1650
|
loading={busy && items.length === 0 && !(empty !== undefined && !isEmpty)}
|
|
1630
1651
|
error={isError ? error : null}
|
|
1631
1652
|
empty={isEmpty}
|
|
1632
|
-
|
|
1653
|
+
emptyMessage={emptyMessage}
|
|
1654
|
+
{...(errorMessage !== undefined ? { errorMessage } : {})}
|
|
1655
|
+
{...(retryLabel !== undefined ? { retryLabel } : {})}
|
|
1633
1656
|
onRetry={() => refetch()}
|
|
1634
1657
|
>
|
|
1635
1658
|
{content}
|
|
@@ -1697,15 +1720,14 @@ export function ActionList<TInput, TItem>({
|
|
|
1697
1720
|
Página {state.page} de {pages}
|
|
1698
1721
|
</span>
|
|
1699
1722
|
{pages > 1 && (
|
|
1700
|
-
// A primitiva pública na escala densa do rodapé: setas
|
|
1723
|
+
// A primitiva pública na escala densa do rodapé: setas no quadrado `icon-sm`,
|
|
1701
1724
|
// números `h-7 min-w-7` que crescem com os dígitos (5726 e 5727 não se colam).
|
|
1702
1725
|
<Pagination className="mx-0 w-auto">
|
|
1703
1726
|
<PaginationContent className="gap-0.5">
|
|
1704
1727
|
<PaginationItem>
|
|
1705
1728
|
<PaginationPrevious
|
|
1706
1729
|
iconOnly
|
|
1707
|
-
|
|
1708
|
-
iconClassName="size-3.5"
|
|
1730
|
+
size="icon-sm"
|
|
1709
1731
|
disabled={state.page <= 1}
|
|
1710
1732
|
onClick={() => goTo(state.page - 1)}
|
|
1711
1733
|
/>
|
|
@@ -1714,7 +1736,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1714
1736
|
n === "…" ? (
|
|
1715
1737
|
// eslint-disable-next-line react/no-array-index-key
|
|
1716
1738
|
<PaginationItem key={`e${i}`}>
|
|
1717
|
-
<PaginationEllipsis
|
|
1739
|
+
<PaginationEllipsis size="icon-sm" />
|
|
1718
1740
|
</PaginationItem>
|
|
1719
1741
|
) : (
|
|
1720
1742
|
<PaginationItem key={n}>
|
|
@@ -1730,8 +1752,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1730
1752
|
<PaginationItem>
|
|
1731
1753
|
<PaginationNext
|
|
1732
1754
|
iconOnly
|
|
1733
|
-
|
|
1734
|
-
iconClassName="size-3.5"
|
|
1755
|
+
size="icon-sm"
|
|
1735
1756
|
disabled={state.page >= pages}
|
|
1736
1757
|
onClick={() => goTo(state.page + 1)}
|
|
1737
1758
|
/>
|
|
@@ -3,25 +3,43 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Page continua responsável somente pelo esqueleto. Na forma curta, PageState pode ser escrito
|
|
5
5
|
* como filho direto porque Page cria PageBody; na composição explícita, ele fica dentro de
|
|
6
|
-
* PageBody. Estados de uma seção ou coleção continuam em DataState, ActionView e ActionList
|
|
6
|
+
* PageBody. Estados de uma seção ou coleção continuam em DataState, ActionView e ActionList —
|
|
7
|
+
* as superfícies são as mesmas (state-surface.tsx), aqui na escala da página.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import
|
|
10
|
-
|
|
10
|
+
import {
|
|
11
|
+
createContext,
|
|
12
|
+
useContext,
|
|
13
|
+
useLayoutEffect,
|
|
14
|
+
type ReactNode,
|
|
15
|
+
} from "react";
|
|
11
16
|
import { cn } from "../../lib/cn.ts";
|
|
12
|
-
import { Alert } from "../primitives/alert.tsx";
|
|
13
17
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
EmptyTitle,
|
|
20
|
-
} from "../primitives/empty.tsx";
|
|
21
|
-
import { Spinner } from "../primitives/spinner.tsx";
|
|
18
|
+
DEFAULT_RETRY_LABEL,
|
|
19
|
+
EmptySurface,
|
|
20
|
+
ErrorSurface,
|
|
21
|
+
LoadingSurface,
|
|
22
|
+
} from "./state-surface.tsx";
|
|
22
23
|
|
|
23
24
|
export type PageStateStatus = "loading" | "error" | "empty" | "ready";
|
|
24
25
|
|
|
26
|
+
const PageStatePresenceContext = createContext<(() => () => void) | null>(null);
|
|
27
|
+
|
|
28
|
+
/** Integração interna: permite que Page reconheça um PageState mesmo dentro de outro componente. */
|
|
29
|
+
export function PageStatePresenceProvider({
|
|
30
|
+
register,
|
|
31
|
+
children,
|
|
32
|
+
}: {
|
|
33
|
+
register: () => () => void;
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
}): React.ReactElement {
|
|
36
|
+
return (
|
|
37
|
+
<PageStatePresenceContext.Provider value={register}>
|
|
38
|
+
{children}
|
|
39
|
+
</PageStatePresenceContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
export interface PageStateProps {
|
|
26
44
|
status: PageStateStatus;
|
|
27
45
|
/** Título específico do estado. Defaults seguros existem para loading, error e empty. */
|
|
@@ -30,8 +48,16 @@ export interface PageStateProps {
|
|
|
30
48
|
description?: ReactNode;
|
|
31
49
|
/** Ícone decorativo. Error usa TriangleAlert por default; empty não inventa um ícone. */
|
|
32
50
|
icon?: ReactNode;
|
|
33
|
-
/** Ação aplicável ao estado, como
|
|
51
|
+
/** Ação aplicável ao estado, como escolher ou criar. */
|
|
34
52
|
action?: ReactNode;
|
|
53
|
+
/** Título do vazio quando `title` não for informado. Default: "Nada por aqui". */
|
|
54
|
+
emptyMessage?: string;
|
|
55
|
+
/** Título do erro quando `title` não for informado. Default: "Não foi possível carregar esta página". */
|
|
56
|
+
errorMessage?: string;
|
|
57
|
+
/** Recuperação do erro: com `onRetry`, o botão de tentar de novo entra ao lado de `action`. */
|
|
58
|
+
onRetry?: () => Promise<void> | void;
|
|
59
|
+
/** Rótulo do botão de recuperação. Default: "Tentar de novo". */
|
|
60
|
+
retryLabel?: string;
|
|
35
61
|
/** Conteúdo normal da página, renderizado somente em ready. */
|
|
36
62
|
children?: ReactNode;
|
|
37
63
|
className?: string;
|
|
@@ -43,37 +69,39 @@ export function PageState({
|
|
|
43
69
|
description,
|
|
44
70
|
icon,
|
|
45
71
|
action,
|
|
72
|
+
emptyMessage = "Nada por aqui",
|
|
73
|
+
errorMessage = "Não foi possível carregar esta página",
|
|
74
|
+
onRetry,
|
|
75
|
+
retryLabel = DEFAULT_RETRY_LABEL,
|
|
46
76
|
children,
|
|
47
77
|
className,
|
|
48
78
|
}: PageStateProps): React.ReactElement {
|
|
79
|
+
const registerPresence = useContext(PageStatePresenceContext);
|
|
80
|
+
useLayoutEffect(() => {
|
|
81
|
+
if (status === "ready" || registerPresence === null) return;
|
|
82
|
+
return registerPresence();
|
|
83
|
+
}, [registerPresence, status]);
|
|
84
|
+
|
|
49
85
|
if (status === "ready") return <>{children}</>;
|
|
50
86
|
|
|
87
|
+
const stateClassName = cn(
|
|
88
|
+
"flex min-h-64 flex-1 items-center justify-center",
|
|
89
|
+
className,
|
|
90
|
+
);
|
|
91
|
+
|
|
51
92
|
if (status === "loading") {
|
|
52
93
|
return (
|
|
53
94
|
<div
|
|
54
95
|
data-slot="page-state"
|
|
55
96
|
data-status="loading"
|
|
56
|
-
|
|
57
|
-
aria-live="polite"
|
|
58
|
-
className={cn(
|
|
59
|
-
"flex min-h-64 flex-col items-center justify-center gap-3 text-center",
|
|
60
|
-
className,
|
|
61
|
-
)}
|
|
97
|
+
className={stateClassName}
|
|
62
98
|
>
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<p
|
|
70
|
-
data-slot="page-state-description"
|
|
71
|
-
className="text-sm text-muted-foreground"
|
|
72
|
-
>
|
|
73
|
-
{description}
|
|
74
|
-
</p>
|
|
75
|
-
)}
|
|
76
|
-
</div>
|
|
99
|
+
<LoadingSurface
|
|
100
|
+
className="w-full min-h-64"
|
|
101
|
+
title={title ?? "Carregando…"}
|
|
102
|
+
description={description}
|
|
103
|
+
titleLevel={1}
|
|
104
|
+
/>
|
|
77
105
|
</div>
|
|
78
106
|
);
|
|
79
107
|
}
|
|
@@ -83,33 +111,35 @@ export function PageState({
|
|
|
83
111
|
<div
|
|
84
112
|
data-slot="page-state"
|
|
85
113
|
data-status="error"
|
|
86
|
-
className={
|
|
114
|
+
className={stateClassName}
|
|
87
115
|
>
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
<ErrorSurface
|
|
117
|
+
presentation="state"
|
|
118
|
+
className="w-full max-w-xl"
|
|
119
|
+
titleLevel={1}
|
|
120
|
+
icon={icon}
|
|
121
|
+
title={title ?? errorMessage}
|
|
93
122
|
description={description}
|
|
123
|
+
{...(onRetry !== undefined ? { onRetry } : {})}
|
|
124
|
+
retryLabel={retryLabel}
|
|
94
125
|
>
|
|
95
126
|
{action}
|
|
96
|
-
</
|
|
127
|
+
</ErrorSurface>
|
|
97
128
|
</div>
|
|
98
129
|
);
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
return (
|
|
102
|
-
<div data-slot="page-state" data-status="empty" className={
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
</Empty>
|
|
133
|
+
<div data-slot="page-state" data-status="empty" className={stateClassName}>
|
|
134
|
+
<EmptySurface
|
|
135
|
+
frame="bare"
|
|
136
|
+
className="min-h-64"
|
|
137
|
+
titleLevel={1}
|
|
138
|
+
icon={icon}
|
|
139
|
+
title={title ?? emptyMessage}
|
|
140
|
+
description={description}
|
|
141
|
+
action={action}
|
|
142
|
+
/>
|
|
113
143
|
</div>
|
|
114
144
|
);
|
|
115
145
|
}
|