@softize/opus 12.9.0 → 12.11.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 +44 -0
- package/bin/lib/check.mjs +1103 -310
- package/bin/lib/copy.mjs +74 -6
- package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
- package/docs/adr/0004-page-content-state-is-composed.md +65 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- package/docs/code-style.md +6 -2
- package/package.json +1 -1
- package/registry/instructions/opus.md +5 -0
- package/registry/skills/build-opus-ui/SKILL.md +27 -16
- package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
- package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
- package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
- package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
- package/registry/templates/app/src/App.tsx +1 -1
- package/src/core/dictionary.ts +52 -14
- package/src/core/index.ts +10 -0
- package/src/core/ui-context.ts +29 -0
- package/src/schema/drivers/zod.ts +34 -19
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +26 -3
- package/src/ui/components/patterns/content-header.tsx +335 -61
- package/src/ui/components/patterns/data-state.tsx +23 -10
- package/src/ui/components/patterns/form.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +1096 -777
- package/src/ui/components/patterns/page-state.tsx +115 -0
- package/src/ui/components/patterns/page.tsx +231 -41
- package/src/ui/components/patterns/sidebar.tsx +354 -80
- package/src/ui/components/patterns/trigger.tsx +13 -9
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert-dialog.tsx +7 -5
- package/src/ui/components/primitives/alert.tsx +298 -80
- package/src/ui/components/primitives/ask.tsx +2 -1
- package/src/ui/components/primitives/badge.tsx +91 -30
- package/src/ui/components/primitives/button.tsx +99 -60
- package/src/ui/components/primitives/calendar.tsx +39 -39
- package/src/ui/components/primitives/card.tsx +96 -23
- package/src/ui/components/primitives/detail.tsx +2 -2
- package/src/ui/components/primitives/dictionary-value.tsx +9 -14
- package/src/ui/components/primitives/dot.tsx +74 -21
- package/src/ui/components/primitives/drawer.tsx +33 -20
- package/src/ui/components/primitives/field.tsx +4 -4
- package/src/ui/components/primitives/item.tsx +137 -81
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/components/primitives/tooltip.tsx +1 -1
- package/src/ui/docs/DocBrowser.tsx +3 -3
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form-card.md +1 -1
- package/src/ui/docs/content/alert-dialog.md +8 -8
- package/src/ui/docs/content/alert.md +54 -23
- package/src/ui/docs/content/badge.md +18 -19
- package/src/ui/docs/content/button.md +12 -9
- package/src/ui/docs/content/card.md +5 -5
- package/src/ui/docs/content/content.md +44 -0
- package/src/ui/docs/content/customization.md +2 -2
- package/src/ui/docs/content/detail.md +5 -2
- package/src/ui/docs/content/dialog.md +2 -2
- package/src/ui/docs/content/dictionary-value.md +11 -10
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +6 -3
- package/src/ui/docs/content/field.md +1 -1
- package/src/ui/docs/content/input-group.md +3 -2
- package/src/ui/docs/content/item.md +47 -21
- package/src/ui/docs/content/menu.md +5 -4
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/page-state.md +45 -0
- package/src/ui/docs/content/page.md +48 -10
- package/src/ui/docs/content/semantic-context.md +63 -0
- package/src/ui/docs/content/sidebar.md +4 -4
- package/src/ui/docs/content/skeleton.md +2 -2
- package/src/ui/docs/content/table.md +3 -3
- package/src/ui/docs/content/tokens.md +28 -0
- package/src/ui/docs/content/tooltip.md +15 -1
- package/src/ui/docs/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +596 -228
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +292 -270
- package/src/ui/react.tsx +378 -111
- package/src/ui/theme.css +66 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <PageState /> — estado integral da área de conteúdo de uma Page.
|
|
3
|
+
*
|
|
4
|
+
* Page continua responsável somente pelo esqueleto. Na forma curta, PageState pode ser escrito
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
import { TriangleAlert } from "lucide-react";
|
|
11
|
+
import { cn } from "../../lib/cn.ts";
|
|
12
|
+
import { Alert } from "../primitives/alert.tsx";
|
|
13
|
+
import {
|
|
14
|
+
Empty,
|
|
15
|
+
EmptyContent,
|
|
16
|
+
EmptyDescription,
|
|
17
|
+
EmptyHeader,
|
|
18
|
+
EmptyMedia,
|
|
19
|
+
EmptyTitle,
|
|
20
|
+
} from "../primitives/empty.tsx";
|
|
21
|
+
import { Spinner } from "../primitives/spinner.tsx";
|
|
22
|
+
|
|
23
|
+
export type PageStateStatus = "loading" | "error" | "empty" | "ready";
|
|
24
|
+
|
|
25
|
+
export interface PageStateProps {
|
|
26
|
+
status: PageStateStatus;
|
|
27
|
+
/** Título específico do estado. Defaults seguros existem para loading, error e empty. */
|
|
28
|
+
title?: ReactNode;
|
|
29
|
+
/** Contexto e próximo passo, quando acrescentarem informação ao título. */
|
|
30
|
+
description?: ReactNode;
|
|
31
|
+
/** Ícone decorativo. Error usa TriangleAlert por default; empty não inventa um ícone. */
|
|
32
|
+
icon?: ReactNode;
|
|
33
|
+
/** Ação aplicável ao estado, como tentar novamente, escolher ou criar. */
|
|
34
|
+
action?: ReactNode;
|
|
35
|
+
/** Conteúdo normal da página, renderizado somente em ready. */
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function PageState({
|
|
41
|
+
status,
|
|
42
|
+
title,
|
|
43
|
+
description,
|
|
44
|
+
icon,
|
|
45
|
+
action,
|
|
46
|
+
children,
|
|
47
|
+
className,
|
|
48
|
+
}: PageStateProps): React.ReactElement {
|
|
49
|
+
if (status === "ready") return <>{children}</>;
|
|
50
|
+
|
|
51
|
+
if (status === "loading") {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="page-state"
|
|
55
|
+
data-status="loading"
|
|
56
|
+
role="status"
|
|
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
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
<Spinner aria-hidden="true" aria-label={undefined} role="presentation" />
|
|
64
|
+
<div className="max-w-md space-y-1">
|
|
65
|
+
<p data-slot="page-state-title" className="text-sm font-medium">
|
|
66
|
+
{title ?? "Carregando…"}
|
|
67
|
+
</p>
|
|
68
|
+
{description !== undefined && (
|
|
69
|
+
<p
|
|
70
|
+
data-slot="page-state-description"
|
|
71
|
+
className="text-sm text-muted-foreground"
|
|
72
|
+
>
|
|
73
|
+
{description}
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (status === "error") {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
data-slot="page-state"
|
|
85
|
+
data-status="error"
|
|
86
|
+
className={cn("flex min-h-64 items-center justify-center", className)}
|
|
87
|
+
>
|
|
88
|
+
<Alert
|
|
89
|
+
className="max-w-xl"
|
|
90
|
+
context="danger"
|
|
91
|
+
icon={icon ?? <TriangleAlert />}
|
|
92
|
+
title={title ?? "Não foi possível carregar esta página"}
|
|
93
|
+
description={description}
|
|
94
|
+
>
|
|
95
|
+
{action}
|
|
96
|
+
</Alert>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div data-slot="page-state" data-status="empty" className={className}>
|
|
103
|
+
<Empty className="min-h-64">
|
|
104
|
+
<EmptyHeader>
|
|
105
|
+
{icon !== undefined && <EmptyMedia variant="icon">{icon}</EmptyMedia>}
|
|
106
|
+
<EmptyTitle>{title ?? "Nada por aqui"}</EmptyTitle>
|
|
107
|
+
{description !== undefined && (
|
|
108
|
+
<EmptyDescription>{description}</EmptyDescription>
|
|
109
|
+
)}
|
|
110
|
+
</EmptyHeader>
|
|
111
|
+
{action !== undefined && <EmptyContent>{action}</EmptyContent>}
|
|
112
|
+
</Empty>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -1,52 +1,242 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* <Page /> —
|
|
2
|
+
* <Page /> — esqueleto composto de página do back-office.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (título, descrição e ações à direita, alinhadas ao fim desse bloco — em geral o botão
|
|
7
|
-
* de criar). O conteúdo é
|
|
8
|
-
* children, sem nada imposto. Presentacional (sem contrato) de propósito: uma página
|
|
9
|
-
* agrega N fontes; o que é action-driven mora dentro (ActionList, listas query-backed).
|
|
4
|
+
* A forma curta cobre o caso comum; a forma explícita expõe a mesma anatomia para composições
|
|
5
|
+
* especiais. Ambas preservam o container centralizado com teto de 80rem.
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
import {
|
|
9
|
+
Children,
|
|
10
|
+
createContext,
|
|
11
|
+
isValidElement,
|
|
12
|
+
useContext,
|
|
13
|
+
type HTMLAttributes,
|
|
14
|
+
type ReactElement,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
} from "react";
|
|
17
|
+
import { cn } from "../../lib/cn.ts";
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const PageContext = createContext(false);
|
|
20
|
+
const PageHeaderContext = createContext(false);
|
|
21
|
+
|
|
22
|
+
function requireParent(
|
|
23
|
+
inside: boolean,
|
|
24
|
+
component: string,
|
|
25
|
+
parent: string,
|
|
26
|
+
): void {
|
|
27
|
+
if (!inside) throw new Error(`${component} deve estar dentro de ${parent}.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PageBaseProps extends Omit<
|
|
31
|
+
HTMLAttributes<HTMLElement>,
|
|
32
|
+
"children" | "title"
|
|
33
|
+
> {
|
|
34
|
+
/** Classes do container. Default: centralizado com `max-w-7xl` (80rem). */
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface PageShorthandProps extends PageBaseProps {
|
|
39
|
+
title: ReactNode;
|
|
40
|
+
/** Total de itens ao lado do título. */
|
|
41
|
+
count?: number;
|
|
42
|
+
description?: ReactNode;
|
|
43
|
+
actions?: ReactNode;
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface PageComposedProps extends PageBaseProps {
|
|
48
|
+
title?: never;
|
|
49
|
+
count?: never;
|
|
50
|
+
description?: never;
|
|
51
|
+
actions?: never;
|
|
52
|
+
children: ReactNode;
|
|
27
53
|
}
|
|
28
54
|
|
|
29
|
-
export
|
|
55
|
+
export type PageProps = PageShorthandProps | PageComposedProps;
|
|
56
|
+
|
|
57
|
+
export function Page({
|
|
58
|
+
title,
|
|
59
|
+
count,
|
|
60
|
+
description,
|
|
61
|
+
actions,
|
|
62
|
+
className,
|
|
63
|
+
children,
|
|
64
|
+
...props
|
|
65
|
+
}: PageProps): ReactElement {
|
|
66
|
+
const shorthand = title !== undefined;
|
|
67
|
+
const nodes = Children.toArray(children);
|
|
68
|
+
if (
|
|
69
|
+
shorthand &&
|
|
70
|
+
nodes.some((node) => isValidElement(node) && node.type === PageHeader)
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Page não permite misturar propriedades de shorthand com PageHeader explícito.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!shorthand) {
|
|
77
|
+
const headers = nodes.filter(
|
|
78
|
+
(node) => isValidElement(node) && node.type === PageHeader,
|
|
79
|
+
);
|
|
80
|
+
const bodies = nodes.filter(
|
|
81
|
+
(node) => isValidElement(node) && node.type === PageBody,
|
|
82
|
+
);
|
|
83
|
+
if (
|
|
84
|
+
headers.length !== 1 ||
|
|
85
|
+
bodies.length !== 1 ||
|
|
86
|
+
headers.length + bodies.length !== nodes.length
|
|
87
|
+
) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Page explícito exige exatamente um PageHeader e um PageBody como filhos diretos.",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<PageContext.Provider value>
|
|
96
|
+
<main data-slot="page" className="min-w-0 flex-1" {...props}>
|
|
97
|
+
<div className={cn("mx-auto max-w-7xl px-8 py-8", className)}>
|
|
98
|
+
{shorthand ? (
|
|
99
|
+
<>
|
|
100
|
+
<PageHeader>
|
|
101
|
+
<PageTitle>{title}</PageTitle>
|
|
102
|
+
{count !== undefined && <PageMeta>{count}</PageMeta>}
|
|
103
|
+
{description !== undefined && (
|
|
104
|
+
<PageDescription>{description}</PageDescription>
|
|
105
|
+
)}
|
|
106
|
+
{actions !== undefined && <PageActions>{actions}</PageActions>}
|
|
107
|
+
</PageHeader>
|
|
108
|
+
<PageBody>{children}</PageBody>
|
|
109
|
+
</>
|
|
110
|
+
) : (
|
|
111
|
+
children
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</main>
|
|
115
|
+
</PageContext.Provider>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function PageHeader({
|
|
120
|
+
className,
|
|
121
|
+
children,
|
|
122
|
+
...props
|
|
123
|
+
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
124
|
+
requireParent(useContext(PageContext), "PageHeader", "Page");
|
|
125
|
+
const nodes = Children.toArray(children);
|
|
126
|
+
const titles = nodes.filter(
|
|
127
|
+
(node) => isValidElement(node) && node.type === PageTitle,
|
|
128
|
+
);
|
|
129
|
+
const metas = nodes.filter(
|
|
130
|
+
(node) => isValidElement(node) && node.type === PageMeta,
|
|
131
|
+
);
|
|
132
|
+
const descriptions = nodes.filter(
|
|
133
|
+
(node) => isValidElement(node) && node.type === PageDescription,
|
|
134
|
+
);
|
|
135
|
+
const actionSlots = nodes.filter(
|
|
136
|
+
(node) => isValidElement(node) && node.type === PageActions,
|
|
137
|
+
);
|
|
138
|
+
const recognized =
|
|
139
|
+
titles.length + metas.length + descriptions.length + actionSlots.length;
|
|
140
|
+
if (
|
|
141
|
+
titles.length !== 1 ||
|
|
142
|
+
metas.length > 1 ||
|
|
143
|
+
descriptions.length > 1 ||
|
|
144
|
+
actionSlots.length > 1 ||
|
|
145
|
+
recognized !== nodes.length
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
"PageHeader exige um PageTitle e aceita no máximo um PageDescription, PageMeta e PageActions como filhos diretos.",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
30
152
|
return (
|
|
31
|
-
<
|
|
32
|
-
<div
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
153
|
+
<PageHeaderContext.Provider value>
|
|
154
|
+
<div
|
|
155
|
+
data-slot="page-header"
|
|
156
|
+
className={cn(
|
|
157
|
+
"mb-6",
|
|
158
|
+
actionSlots.length > 0 &&
|
|
159
|
+
"flex flex-col items-start gap-4 sm:flex-row sm:items-end sm:justify-between",
|
|
160
|
+
className,
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
>
|
|
164
|
+
<div data-slot="page-heading" className="min-w-0">
|
|
165
|
+
<div className="flex items-baseline gap-2">
|
|
166
|
+
{titles}
|
|
167
|
+
{metas}
|
|
168
|
+
</div>
|
|
169
|
+
{descriptions}
|
|
47
170
|
</div>
|
|
48
|
-
{
|
|
171
|
+
{actionSlots}
|
|
49
172
|
</div>
|
|
50
|
-
</
|
|
51
|
-
)
|
|
173
|
+
</PageHeaderContext.Provider>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function PageTitle({
|
|
178
|
+
className,
|
|
179
|
+
...props
|
|
180
|
+
}: HTMLAttributes<HTMLHeadingElement>): ReactElement {
|
|
181
|
+
requireParent(useContext(PageHeaderContext), "PageTitle", "PageHeader");
|
|
182
|
+
return (
|
|
183
|
+
<h1
|
|
184
|
+
data-slot="page-title"
|
|
185
|
+
className={cn("text-2xl font-semibold tracking-tight", className)}
|
|
186
|
+
{...props}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function PageDescription({
|
|
192
|
+
className,
|
|
193
|
+
...props
|
|
194
|
+
}: HTMLAttributes<HTMLParagraphElement>): ReactElement {
|
|
195
|
+
requireParent(useContext(PageHeaderContext), "PageDescription", "PageHeader");
|
|
196
|
+
return (
|
|
197
|
+
<p
|
|
198
|
+
data-slot="page-description"
|
|
199
|
+
className={cn("mt-1 truncate text-sm text-muted-foreground", className)}
|
|
200
|
+
{...props}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function PageMeta({
|
|
206
|
+
className,
|
|
207
|
+
...props
|
|
208
|
+
}: HTMLAttributes<HTMLSpanElement>): ReactElement {
|
|
209
|
+
requireParent(useContext(PageHeaderContext), "PageMeta", "PageHeader");
|
|
210
|
+
return (
|
|
211
|
+
<span
|
|
212
|
+
data-slot="page-meta"
|
|
213
|
+
className={cn("font-mono text-base text-muted-foreground/60", className)}
|
|
214
|
+
{...props}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function PageActions({
|
|
220
|
+
className,
|
|
221
|
+
...props
|
|
222
|
+
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
223
|
+
requireParent(useContext(PageHeaderContext), "PageActions", "PageHeader");
|
|
224
|
+
return (
|
|
225
|
+
<div
|
|
226
|
+
data-slot="page-actions"
|
|
227
|
+
className={cn(
|
|
228
|
+
"flex w-full shrink-0 items-center gap-4 sm:w-auto",
|
|
229
|
+
className,
|
|
230
|
+
)}
|
|
231
|
+
{...props}
|
|
232
|
+
/>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function PageBody({
|
|
237
|
+
className,
|
|
238
|
+
...props
|
|
239
|
+
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
240
|
+
requireParent(useContext(PageContext), "PageBody", "Page");
|
|
241
|
+
return <div data-slot="page-body" className={className} {...props} />;
|
|
52
242
|
}
|