@softize/opus 13.0.0 → 14.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 +81 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/copy.mjs +276 -6
- package/bin/lib/db.mjs +16 -74
- package/bin/lib/gen-openapi.mjs +3 -3
- package/bin/lib/gen-runner.mjs +1 -1
- package/bin/lib/gen.mjs +14 -69
- package/bin/lib/mcp.mjs +3 -1
- package/bin/lib/seed.mjs +5 -62
- package/docs/code-style.md +4 -1
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/instructions/opus.md +3 -3
- package/registry/templates/app/package.json +1 -1
- package/registry/templates/app/src/App.tsx +11 -6
- package/registry/templates/app/src/main.tsx +4 -4
- package/src/audit/drivers/console.ts +1 -0
- package/src/auth/drivers/better-auth.ts +1 -0
- package/src/auth/drivers/jwt.ts +1 -0
- package/src/cache/drivers/memory.ts +1 -0
- package/src/client/drivers/fetch.ts +2 -1
- package/src/core/actions.ts +6 -1
- package/src/core/audit.ts +9 -3
- package/src/core/contracts.ts +7 -0
- package/src/core/domain.ts +1 -1
- package/src/core/errors.ts +18 -15
- package/src/core/index.ts +4 -2
- package/src/core/package-version.ts +26 -0
- package/src/core/reactions.ts +1 -1
- package/src/core/runtime.ts +33 -23
- package/src/core/schedules.ts +1 -1
- package/src/core/types.ts +5 -6
- package/src/dsl/eval.ts +2 -2
- package/src/dsl/kysely.ts +2 -2
- package/src/dsl/loads.ts +1 -1
- package/src/dsl/parser.ts +5 -5
- package/src/events/drivers/mitt.ts +1 -0
- package/src/mcp/index.ts +2 -1
- package/src/observability/drivers/opentelemetry.ts +1 -0
- package/src/queue/drivers/bullmq.ts +3 -3
- package/src/scheduler/drivers/node-cron.ts +3 -2
- package/src/scheduler/every.ts +7 -7
- package/src/schema/openapi.ts +3 -3
- package/src/seed/index.ts +29 -0
- package/src/server/drivers/fastify.ts +5 -2
- package/src/server/drivers/node.ts +9 -6
- package/src/server/index.ts +3 -1
- package/src/storage/drivers/fs.ts +1 -0
- package/src/testing/index.ts +3 -3
- 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 +44 -137
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +26 -22
- package/src/ui/components/patterns/list.tsx +18 -12
- package/src/ui/components/patterns/page-state.tsx +39 -51
- package/src/ui/components/patterns/page.tsx +37 -54
- package/src/ui/components/patterns/sidebar.tsx +17 -6
- package/src/ui/components/patterns/state-surface.tsx +148 -0
- package/src/ui/components/patterns/surface-header.tsx +119 -0
- package/src/ui/components/patterns/trigger.tsx +21 -25
- package/src/ui/components/patterns/view.tsx +29 -22
- package/src/ui/components/primitives/alert.tsx +1 -27
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +5 -41
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +39 -30
- package/src/ui/components/primitives/calendar.tsx +28 -2
- package/src/ui/components/primitives/carousel.tsx +3 -3
- package/src/ui/components/primitives/chat.tsx +1 -1
- package/src/ui/components/primitives/checkbox.tsx +1 -1
- package/src/ui/components/primitives/command.tsx +2 -2
- package/src/ui/components/primitives/control.ts +72 -0
- package/src/ui/components/primitives/copyable.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +12 -7
- package/src/ui/components/primitives/dot.tsx +1 -25
- package/src/ui/components/primitives/drawer.tsx +10 -3
- package/src/ui/components/primitives/field.tsx +3 -3
- package/src/ui/components/primitives/icon-picker.tsx +3 -1
- package/src/ui/components/primitives/input-group.tsx +12 -9
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +2 -2
- 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/progress.tsx +32 -3
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/resizable.tsx +3 -1
- package/src/ui/components/primitives/select.tsx +6 -6
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +3 -0
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +5 -1
- package/src/ui/components/primitives/tabs.tsx +6 -3
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/components/primitives/tooltip.tsx +1 -0
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form.md +36 -3
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +13 -2
- package/src/ui/docs/content/action-trigger.md +12 -5
- package/src/ui/docs/content/action-view.md +11 -3
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +30 -14
- package/src/ui/docs/content/calendar.md +13 -0
- package/src/ui/docs/content/card.md +26 -0
- package/src/ui/docs/content/chat.md +20 -0
- package/src/ui/docs/content/cli.md +71 -19
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/composer.md +15 -0
- package/src/ui/docs/content/content.md +17 -1
- package/src/ui/docs/content/copyable.md +8 -0
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/detail.md +19 -1
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/dictionary-value.md +9 -2
- package/src/ui/docs/content/dock.md +8 -0
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/empty.md +2 -2
- package/src/ui/docs/content/getting-started.md +2 -2
- package/src/ui/docs/content/icon-picker.md +11 -0
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/label.md +7 -0
- package/src/ui/docs/content/menu.md +6 -0
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/page.md +20 -4
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/popover.md +6 -0
- package/src/ui/docs/content/progress.md +8 -11
- package/src/ui/docs/content/select.md +5 -5
- package/src/ui/docs/content/semantic-context.md +5 -4
- package/src/ui/docs/content/sidebar.md +13 -47
- package/src/ui/docs/content/skeleton.md +6 -0
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/split.md +21 -0
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/textarea.md +7 -0
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/docs/content/tokens.md +4 -4
- package/src/ui/docs/content/truncate.md +8 -0
- package/src/ui/docs/content/ui.md +14 -0
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +5 -5
- package/src/ui/docs/standalone.tsx +2 -2
- package/src/ui/drivers/react.tsx +12 -12
- package/src/ui/lib/action-errors.ts +45 -0
- package/src/ui/lib/zod-pt-br.ts +31 -4
- package/src/ui/meta.ts +8 -8
- package/src/ui/react.tsx +10 -16
- package/src/ui/theme.css +10 -8
- package/src/vite/design.ts +6 -18
- package/src/ui/components/patterns/shell-nav.tsx +0 -147
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Children,
|
|
3
|
-
Fragment,
|
|
4
3
|
createContext,
|
|
5
4
|
isValidElement,
|
|
6
5
|
useContext,
|
|
@@ -10,9 +9,14 @@ import {
|
|
|
10
9
|
type ReactNode,
|
|
11
10
|
} from "react";
|
|
12
11
|
import { cn } from "../../lib/cn.ts";
|
|
12
|
+
import {
|
|
13
|
+
SurfaceHeader,
|
|
14
|
+
surfaceHeaderClasses,
|
|
15
|
+
type SurfaceHeaderVariant,
|
|
16
|
+
} from "./surface-header.tsx";
|
|
13
17
|
|
|
14
18
|
export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
15
|
-
export type ContentHeaderVariant =
|
|
19
|
+
export type ContentHeaderVariant = SurfaceHeaderVariant;
|
|
16
20
|
|
|
17
21
|
interface ContentContextValue {
|
|
18
22
|
level: ContentHeaderLevel;
|
|
@@ -39,14 +43,14 @@ interface ContentBaseProps extends Omit<
|
|
|
39
43
|
> {
|
|
40
44
|
/** Nível semântico do heading, independente de seu destaque visual. */
|
|
41
45
|
level?: ContentHeaderLevel;
|
|
42
|
-
/** Hierarquia visual
|
|
46
|
+
/** Hierarquia visual: `section` (default) ou `page`, a mesma de PageHeader. */
|
|
43
47
|
variant?: ContentHeaderVariant;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
interface ContentShorthandProps extends ContentBaseProps {
|
|
47
51
|
title: ReactNode;
|
|
48
|
-
/**
|
|
49
|
-
|
|
52
|
+
/** Total de itens ao lado do título — o mesmo `count` de Page. */
|
|
53
|
+
count?: number;
|
|
50
54
|
description?: ReactNode;
|
|
51
55
|
actions?: ReactNode;
|
|
52
56
|
children: ReactNode;
|
|
@@ -54,7 +58,7 @@ interface ContentShorthandProps extends ContentBaseProps {
|
|
|
54
58
|
|
|
55
59
|
interface ContentComposedProps extends ContentBaseProps {
|
|
56
60
|
title?: never;
|
|
57
|
-
|
|
61
|
+
count?: never;
|
|
58
62
|
description?: never;
|
|
59
63
|
actions?: never;
|
|
60
64
|
children: ReactNode;
|
|
@@ -65,7 +69,7 @@ export type ContentProps = ContentShorthandProps | ContentComposedProps;
|
|
|
65
69
|
/** Região de conteúdo nomeada. A forma curta e a explícita produzem a mesma anatomia. */
|
|
66
70
|
export function Content({
|
|
67
71
|
title,
|
|
68
|
-
|
|
72
|
+
count,
|
|
69
73
|
description,
|
|
70
74
|
actions,
|
|
71
75
|
level = 2,
|
|
@@ -119,7 +123,7 @@ export function Content({
|
|
|
119
123
|
<>
|
|
120
124
|
<ContentHeader>
|
|
121
125
|
<ContentTitle>{title}</ContentTitle>
|
|
122
|
-
{
|
|
126
|
+
{count !== undefined && <ContentMeta>{count}</ContentMeta>}
|
|
123
127
|
{description !== undefined && (
|
|
124
128
|
<ContentDescription>{description}</ContentDescription>
|
|
125
129
|
)}
|
|
@@ -137,128 +141,36 @@ export function Content({
|
|
|
137
141
|
);
|
|
138
142
|
}
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
HTMLAttributes<HTMLDivElement>,
|
|
142
|
-
"children" | "title"
|
|
143
|
-
> {
|
|
144
|
-
/** @deprecated Prefira os slots explícitos ou o shorthand de Content. */
|
|
145
|
-
title: ReactNode;
|
|
146
|
-
meta?: ReactNode;
|
|
147
|
-
description?: ReactNode;
|
|
148
|
-
actions?: ReactNode;
|
|
149
|
-
/** @deprecated Declare o nível em Content. Compatibilidade da série 12. */
|
|
150
|
-
level?: ContentHeaderLevel;
|
|
151
|
-
/** @deprecated Declare a hierarquia visual em Content. Compatibilidade da série 12. */
|
|
152
|
-
variant?: ContentHeaderVariant;
|
|
153
|
-
children?: never;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
interface ContentHeaderComposedProps extends Omit<
|
|
157
|
-
HTMLAttributes<HTMLDivElement>,
|
|
158
|
-
"title"
|
|
159
|
-
> {
|
|
160
|
-
/** @deprecated Declare o nível em Content. Compatibilidade da série 12. */
|
|
161
|
-
level?: ContentHeaderLevel;
|
|
162
|
-
/** @deprecated Declare a hierarquia visual em Content. Compatibilidade da série 12. */
|
|
163
|
-
variant?: ContentHeaderVariant;
|
|
164
|
-
title?: never;
|
|
165
|
-
meta?: never;
|
|
166
|
-
description?: never;
|
|
167
|
-
actions?: never;
|
|
168
|
-
children: ReactNode;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export type ContentHeaderProps =
|
|
172
|
-
ContentHeaderShorthandProps | ContentHeaderComposedProps;
|
|
144
|
+
export type ContentHeaderProps = Omit<HTMLAttributes<HTMLDivElement>, "title">;
|
|
173
145
|
|
|
174
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* Header estrutural de Content; não é uma região autônoma. A anatomia é a de
|
|
148
|
+
* `SurfaceHeader`, a mesma de `PageHeader`: só os nomes dos slots e a hierarquia mudam.
|
|
149
|
+
*/
|
|
175
150
|
export function ContentHeader({
|
|
176
|
-
title,
|
|
177
|
-
meta,
|
|
178
|
-
description,
|
|
179
|
-
actions,
|
|
180
|
-
level,
|
|
181
|
-
variant,
|
|
182
151
|
children,
|
|
183
|
-
className,
|
|
184
152
|
...props
|
|
185
153
|
}: ContentHeaderProps): ReactElement {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
throw new Error("ContentHeader deve estar dentro de Content.");
|
|
191
|
-
}
|
|
192
|
-
const content: ContentContextValue = {
|
|
193
|
-
level: level ?? parent?.level ?? 2,
|
|
194
|
-
titleId: parent?.titleId ?? standaloneTitleId,
|
|
195
|
-
variant: variant ?? parent?.variant ?? "section",
|
|
196
|
-
};
|
|
197
|
-
const nodes = Children.toArray(
|
|
198
|
-
shorthand ? (
|
|
199
|
-
<>
|
|
200
|
-
<ContentTitle>{title}</ContentTitle>
|
|
201
|
-
{meta !== undefined && <ContentMeta>{meta}</ContentMeta>}
|
|
202
|
-
{description !== undefined && (
|
|
203
|
-
<ContentDescription>{description}</ContentDescription>
|
|
204
|
-
)}
|
|
205
|
-
{actions !== undefined && <ContentActions>{actions}</ContentActions>}
|
|
206
|
-
</>
|
|
207
|
-
) : (
|
|
208
|
-
children
|
|
209
|
-
),
|
|
210
|
-
).flatMap((node) =>
|
|
211
|
-
isValidElement(node) && node.type === Fragment
|
|
212
|
-
? Children.toArray((node.props as { children?: ReactNode }).children)
|
|
213
|
-
: [node],
|
|
214
|
-
);
|
|
215
|
-
const titles = nodes.filter(
|
|
216
|
-
(node) => isValidElement(node) && node.type === ContentTitle,
|
|
217
|
-
);
|
|
218
|
-
const metas = nodes.filter(
|
|
219
|
-
(node) => isValidElement(node) && node.type === ContentMeta,
|
|
220
|
-
);
|
|
221
|
-
const descriptions = nodes.filter(
|
|
222
|
-
(node) => isValidElement(node) && node.type === ContentDescription,
|
|
223
|
-
);
|
|
224
|
-
const actionSlots = nodes.filter(
|
|
225
|
-
(node) => isValidElement(node) && node.type === ContentActions,
|
|
154
|
+
const content = requiredContext(
|
|
155
|
+
useContext(ContentContext),
|
|
156
|
+
"ContentHeader",
|
|
157
|
+
"Content",
|
|
226
158
|
);
|
|
227
|
-
const recognized =
|
|
228
|
-
titles.length + metas.length + descriptions.length + actionSlots.length;
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
titles.length !== 1 ||
|
|
232
|
-
metas.length > 1 ||
|
|
233
|
-
descriptions.length > 1 ||
|
|
234
|
-
actionSlots.length > 1 ||
|
|
235
|
-
recognized !== nodes.length
|
|
236
|
-
) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
"ContentHeader exige um ContentTitle e aceita no máximo um ContentDescription, ContentMeta e ContentActions como filhos diretos.",
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
159
|
return (
|
|
243
160
|
<ContentHeaderContext.Provider value={content}>
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
161
|
+
<SurfaceHeader
|
|
162
|
+
name="ContentHeader"
|
|
163
|
+
slot="content"
|
|
164
|
+
slots={{
|
|
165
|
+
title: ContentTitle,
|
|
166
|
+
count: ContentMeta,
|
|
167
|
+
description: ContentDescription,
|
|
168
|
+
actions: ContentActions,
|
|
169
|
+
}}
|
|
251
170
|
{...props}
|
|
252
171
|
>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
{titles}
|
|
256
|
-
{metas}
|
|
257
|
-
</div>
|
|
258
|
-
{descriptions}
|
|
259
|
-
</div>
|
|
260
|
-
{actionSlots}
|
|
261
|
-
</div>
|
|
172
|
+
{children}
|
|
173
|
+
</SurfaceHeader>
|
|
262
174
|
</ContentHeaderContext.Provider>
|
|
263
175
|
);
|
|
264
176
|
}
|
|
@@ -287,11 +199,7 @@ export function ContentTitle({
|
|
|
287
199
|
<Heading
|
|
288
200
|
id={id ?? context.titleId}
|
|
289
201
|
data-slot="content-title"
|
|
290
|
-
className={cn(
|
|
291
|
-
"font-semibold",
|
|
292
|
-
context.variant === "page" ? "text-2xl tracking-tight" : "text-sm",
|
|
293
|
-
className,
|
|
294
|
-
)}
|
|
202
|
+
className={cn(surfaceHeaderClasses[context.variant].title, className)}
|
|
295
203
|
{...props}
|
|
296
204
|
/>
|
|
297
205
|
);
|
|
@@ -309,26 +217,29 @@ export function ContentDescription({
|
|
|
309
217
|
return (
|
|
310
218
|
<p
|
|
311
219
|
data-slot="content-description"
|
|
312
|
-
className={cn(
|
|
313
|
-
"truncate text-muted-foreground",
|
|
314
|
-
context.variant === "page" ? "mt-1 text-sm" : "mt-0.5 text-xs",
|
|
315
|
-
className,
|
|
316
|
-
)}
|
|
220
|
+
className={cn(surfaceHeaderClasses[context.variant].description, className)}
|
|
317
221
|
{...props}
|
|
318
222
|
/>
|
|
319
223
|
);
|
|
320
224
|
}
|
|
321
225
|
|
|
226
|
+
/** O contador ao lado do título — o slot `count` do shorthand. */
|
|
322
227
|
export function ContentMeta({
|
|
323
228
|
className,
|
|
324
229
|
...props
|
|
325
230
|
}: HTMLAttributes<HTMLSpanElement>): ReactElement {
|
|
326
|
-
requiredContext(
|
|
231
|
+
const context = requiredContext(
|
|
327
232
|
useContext(ContentHeaderContext),
|
|
328
233
|
"ContentMeta",
|
|
329
234
|
"ContentHeader",
|
|
330
235
|
);
|
|
331
|
-
return
|
|
236
|
+
return (
|
|
237
|
+
<span
|
|
238
|
+
data-slot="content-meta"
|
|
239
|
+
className={cn(surfaceHeaderClasses[context.variant].count, className)}
|
|
240
|
+
{...props}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
332
243
|
}
|
|
333
244
|
|
|
334
245
|
export function ContentActions({
|
|
@@ -343,11 +254,7 @@ export function ContentActions({
|
|
|
343
254
|
return (
|
|
344
255
|
<div
|
|
345
256
|
data-slot="content-actions"
|
|
346
|
-
className={cn(
|
|
347
|
-
"flex w-full shrink-0 items-center sm:w-auto",
|
|
348
|
-
context.variant === "page" ? "gap-4" : "gap-2",
|
|
349
|
-
className,
|
|
350
|
-
)}
|
|
257
|
+
className={cn(surfaceHeaderClasses[context.variant].actions, className)}
|
|
351
258
|
{...props}
|
|
352
259
|
/>
|
|
353
260
|
);
|
|
@@ -2,31 +2,39 @@
|
|
|
2
2
|
* <DataState /> — o estado "carregando" da casa: o que mostrar ANTES do conteúdo chegar.
|
|
3
3
|
*
|
|
4
4
|
* Orquestra os estados de uma carga assíncrona (lista/fetch) num lugar só: erro (calmo, sem
|
|
5
|
-
* jargão na tela), carregando (Spinner centralizado), vazio (
|
|
5
|
+
* jargão na tela), carregando (Spinner centralizado), vazio (Empty) e, por fim, o conteúdo.
|
|
6
6
|
* Dissolve o `error ? … : isLoading ? <Spinner/> : empty ? … : children` repetido em toda
|
|
7
7
|
* tela. NÃO confundir com "processando" (AÇÃO em andamento depois do clique) — isso é o
|
|
8
8
|
* `busy` do Button. Pra placeholder com forma de conteúdo, use Skeleton.
|
|
9
|
+
*
|
|
10
|
+
* As superfícies são as mesmas de PageState (state-surface.tsx); aqui elas ficam na escala de
|
|
11
|
+
* uma seção: o vazio é a frase discreta com moldura sólida e o erro é o Alert com recuperação.
|
|
9
12
|
*/
|
|
10
13
|
import * as React from 'react'
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_EMPTY_MESSAGE,
|
|
16
|
+
DEFAULT_ERROR_MESSAGE,
|
|
17
|
+
DEFAULT_RETRY_LABEL,
|
|
18
|
+
EmptySurface,
|
|
19
|
+
ErrorSurface,
|
|
20
|
+
LoadingSurface,
|
|
21
|
+
} from './state-surface.tsx'
|
|
15
22
|
|
|
16
23
|
export interface DataStateProps {
|
|
17
24
|
/** Carregando (antes do conteúdo) — mostra o Spinner centralizado. */
|
|
18
25
|
loading?: boolean
|
|
19
|
-
/** Erro da carga. A mensagem técnica NÃO vai pra tela (passe `
|
|
26
|
+
/** Erro da carga. A mensagem técnica NÃO vai pra tela (passe `errorMessage` pra um aviso). */
|
|
20
27
|
error?: { message?: string } | null
|
|
21
|
-
/** Sem itens — mostra o `
|
|
28
|
+
/** Sem itens — mostra o `emptyMessage`. */
|
|
22
29
|
empty?: boolean
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
/** Aviso de erro (calmo, orientado
|
|
26
|
-
|
|
30
|
+
/** Frase do vazio (pt-BR, ex.: "Nenhum papel."). Default: "Nada por aqui.". */
|
|
31
|
+
emptyMessage?: string
|
|
32
|
+
/** Aviso de erro (calmo, orientado à pessoa). Default: "Não foi possível carregar.". */
|
|
33
|
+
errorMessage?: string
|
|
27
34
|
/** Recuperação opcional exibida somente no estado de erro. */
|
|
28
35
|
onRetry?: () => Promise<void> | void
|
|
29
|
-
|
|
36
|
+
/** Rótulo do botão de recuperação. Default: "Tentar de novo". */
|
|
37
|
+
retryLabel?: string
|
|
30
38
|
/** Em TABELA: renderiza o estado como UMA linha (<tr><td colSpan>), não um bloco — pra
|
|
31
39
|
* caber direto no <tbody>. Passe o nº de colunas da tabela. */
|
|
32
40
|
colSpan?: number
|
|
@@ -39,78 +47,44 @@ export function DataState({
|
|
|
39
47
|
loading = false,
|
|
40
48
|
error = null,
|
|
41
49
|
empty = false,
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
emptyMessage = DEFAULT_EMPTY_MESSAGE,
|
|
51
|
+
errorMessage = DEFAULT_ERROR_MESSAGE,
|
|
44
52
|
onRetry,
|
|
45
|
-
|
|
53
|
+
retryLabel = DEFAULT_RETRY_LABEL,
|
|
46
54
|
colSpan,
|
|
47
55
|
children,
|
|
48
56
|
}: DataStateProps): React.ReactElement {
|
|
49
57
|
// Há conteúdo: passa direto (no modo tabela, são as <tr> dos itens).
|
|
50
58
|
if (!error && !loading && !empty) return <>{children}</>
|
|
51
59
|
|
|
60
|
+
const status = error ? 'error' : loading ? 'loading' : 'empty'
|
|
61
|
+
const surface = error ? (
|
|
62
|
+
<ErrorSurface
|
|
63
|
+
title={errorMessage}
|
|
64
|
+
{...(onRetry !== undefined ? { onRetry } : {})}
|
|
65
|
+
retryLabel={retryLabel}
|
|
66
|
+
/>
|
|
67
|
+
) : loading ? (
|
|
68
|
+
<LoadingSurface className="py-10" />
|
|
69
|
+
) : (
|
|
70
|
+
// A tabela já traz a moldura: dentro dela o vazio é só a frase (`bare`).
|
|
71
|
+
<EmptySurface title={emptyMessage} compact frame={colSpan === undefined ? 'structural' : 'bare'} />
|
|
72
|
+
)
|
|
73
|
+
|
|
52
74
|
// Modo TABELA: o estado vira UMA linha de largura cheia (<td colSpan>), pra caber no <tbody>.
|
|
53
75
|
if (colSpan !== undefined) {
|
|
54
|
-
const status = error ? 'error' : loading ? 'loading' : 'empty'
|
|
55
76
|
return (
|
|
56
77
|
<tr data-slot="data-state" data-status={status}>
|
|
57
|
-
<td
|
|
58
|
-
|
|
59
|
-
className={cn('px-4 py-10 text-center text-sm', error ? 'text-destructive' : 'text-muted-foreground/60')}
|
|
60
|
-
>
|
|
61
|
-
{error ? (
|
|
62
|
-
<div className="flex flex-col items-center gap-3">
|
|
63
|
-
<span>{errorText ?? 'Não foi possível carregar.'}</span>
|
|
64
|
-
{onRetry !== undefined && (
|
|
65
|
-
<Button variant="outline" size="sm" onClick={() => void onRetry()}>
|
|
66
|
-
{retryText}
|
|
67
|
-
</Button>
|
|
68
|
-
)}
|
|
69
|
-
</div>
|
|
70
|
-
) : loading ? (
|
|
71
|
-
<Spinner className="mx-auto" />
|
|
72
|
-
) : (
|
|
73
|
-
emptyText
|
|
74
|
-
)}
|
|
78
|
+
<td colSpan={colSpan} className="px-4 py-4 text-center text-sm">
|
|
79
|
+
{surface}
|
|
75
80
|
</td>
|
|
76
81
|
</tr>
|
|
77
82
|
)
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
// Modo BLOCO (default).
|
|
81
|
-
if (error) {
|
|
82
|
-
return (
|
|
83
|
-
<div data-slot="data-state" data-status="error">
|
|
84
|
-
<Alert context="danger" description={errorText ?? 'Não foi possível carregar.'}>
|
|
85
|
-
{onRetry !== undefined && (
|
|
86
|
-
<Button variant="outline" size="sm" onClick={() => void onRetry()}>
|
|
87
|
-
{retryText}
|
|
88
|
-
</Button>
|
|
89
|
-
)}
|
|
90
|
-
</Alert>
|
|
91
|
-
</div>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
if (loading) {
|
|
95
|
-
return (
|
|
96
|
-
<div
|
|
97
|
-
data-slot="data-state"
|
|
98
|
-
data-status="loading"
|
|
99
|
-
role="status"
|
|
100
|
-
aria-live="polite"
|
|
101
|
-
className="py-10 text-center text-muted-foreground"
|
|
102
|
-
>
|
|
103
|
-
<Spinner className="mx-auto" />
|
|
104
|
-
</div>
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
85
|
return (
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
className="rounded-lg border border-border py-10 text-center text-sm text-muted-foreground/60"
|
|
112
|
-
>
|
|
113
|
-
{emptyText}
|
|
114
|
-
</p>
|
|
86
|
+
<div data-slot="data-state" data-status={status}>
|
|
87
|
+
{surface}
|
|
88
|
+
</div>
|
|
115
89
|
)
|
|
116
90
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as React from 'react'
|
|
16
|
+
import type { UiContext } from '../../../core/ui-context.ts'
|
|
16
17
|
import { cn } from '../../lib/cn.ts'
|
|
17
18
|
import { Button } from '../primitives/button.tsx'
|
|
18
19
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
|
|
@@ -142,7 +143,8 @@ export function DockAction({ icon, label, pressed, disabled = false, hint, class
|
|
|
142
143
|
<Button
|
|
143
144
|
data-slot="dock-action"
|
|
144
145
|
size="icon"
|
|
145
|
-
|
|
146
|
+
context={pressed === true ? 'primary' : 'neutral'}
|
|
147
|
+
variant={pressed === true ? 'solid' : 'ghost'}
|
|
146
148
|
aria-label={label}
|
|
147
149
|
aria-pressed={pressed}
|
|
148
150
|
disabled={disabled}
|
|
@@ -162,9 +164,22 @@ const STATUS_POSITION = {
|
|
|
162
164
|
'top-left': 'top-3 left-3',
|
|
163
165
|
} as const
|
|
164
166
|
|
|
167
|
+
export type SurfaceStatusContext = Exclude<UiContext, 'primary'>
|
|
168
|
+
|
|
169
|
+
/** `neutral` mantém o cinza do chrome; os demais tingem borda e texto pela família semântica. */
|
|
170
|
+
const STATUS_CONTEXT: Record<SurfaceStatusContext, string> = {
|
|
171
|
+
neutral: 'border-border text-muted-foreground',
|
|
172
|
+
info: 'border-context-info-border text-context-info-emphasis',
|
|
173
|
+
success: 'border-context-success-border text-context-success-emphasis',
|
|
174
|
+
warning: 'border-context-warning-border text-context-warning-emphasis',
|
|
175
|
+
danger: 'border-context-danger-border text-context-danger-emphasis',
|
|
176
|
+
}
|
|
177
|
+
|
|
165
178
|
export interface SurfaceStatusProps {
|
|
166
179
|
/** Canto da superfície onde o estado se ancora. */
|
|
167
180
|
position?: keyof typeof STATUS_POSITION
|
|
181
|
+
/** O que o estado comunica (salvo, falhou, publicando…). Sem contexto, fica neutro. */
|
|
182
|
+
context?: SurfaceStatusContext
|
|
168
183
|
/** Ações do recurso aberto — em geral o mesmo menu que a linha dele tem na navegação. */
|
|
169
184
|
actions?: React.ReactNode
|
|
170
185
|
className?: string
|
|
@@ -180,14 +195,16 @@ export interface SurfaceStatusProps {
|
|
|
180
195
|
* Mensagem de sucesso é transitória por natureza; falha permanece até o estado mudar. Quem decide
|
|
181
196
|
* isso é a aplicação, que conhece o ciclo — o componente só reserva o lugar.
|
|
182
197
|
*/
|
|
183
|
-
export function SurfaceStatus({ position = 'top-right', actions, className, children }: SurfaceStatusProps): React.ReactElement {
|
|
198
|
+
export function SurfaceStatus({ position = 'top-right', context = 'neutral', actions, className, children }: SurfaceStatusProps): React.ReactElement {
|
|
184
199
|
const hasActions = actions !== undefined && actions !== null
|
|
185
200
|
return (
|
|
186
201
|
<div
|
|
187
202
|
data-slot="surface-status"
|
|
203
|
+
data-context={context}
|
|
188
204
|
className={cn(
|
|
189
205
|
'pointer-events-none absolute z-10 flex max-w-[calc(100%-1.5rem)] flex-row items-center gap-2',
|
|
190
|
-
'rounded-full border
|
|
206
|
+
'rounded-full border bg-background/95 py-1.5 pl-3 text-xs shadow-sm backdrop-blur',
|
|
207
|
+
STATUS_CONTEXT[context],
|
|
191
208
|
hasActions ? 'pr-1' : 'pr-3',
|
|
192
209
|
STATUS_POSITION[position],
|
|
193
210
|
className,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Dois modos, um motor:
|
|
5
5
|
* - AUTO (sem children): monta TODOS os campos na ordem do contrato — zero JSX de campo.
|
|
6
6
|
* - COMPOSIÇÃO (com children): você diagrama; <ActionFormField name /> coloca cada campo
|
|
7
|
-
* (label/
|
|
7
|
+
* (label/help/widget/erro/asterisco derivados do contrato) onde quiser. Condicional é
|
|
8
8
|
* JSX ({cond && <ActionFormField/>}); opções de runtime entram por prop no campo. O
|
|
9
9
|
* FieldGroup canônico permanece nos dois modos; grids e seções entram como filhos explícitos.
|
|
10
10
|
*
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { useDicts, useFormAction, type DictLike } from '../../drivers/react.tsx'
|
|
29
29
|
import { z, type ZodTypeAny } from 'zod'
|
|
30
30
|
import { cn } from '../../lib/cn.ts'
|
|
31
|
+
import { humanizeActionError } from '../../lib/action-errors.ts'
|
|
31
32
|
import { objectSchemaShape } from '../../lib/object-schema.ts'
|
|
32
33
|
import { toast } from '../primitives/sonner.tsx'
|
|
33
34
|
import { Button } from '../primitives/button.tsx'
|
|
@@ -39,7 +40,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../pri
|
|
|
39
40
|
import { Info } from 'lucide-react'
|
|
40
41
|
import { Select, type SelectOption } from '../primitives/select.tsx'
|
|
41
42
|
import { ToggleGroup, ToggleGroupItem } from '../primitives/toggle-group.tsx'
|
|
42
|
-
import { Field,
|
|
43
|
+
import { Field, FieldError, FieldGroup, FieldLabel } from '../primitives/field.tsx'
|
|
43
44
|
|
|
44
45
|
// =============================================================================
|
|
45
46
|
// Inferência de tipo de field a partir do Zod schema
|
|
@@ -111,8 +112,7 @@ function inferFieldKind(schema: ZodTypeAny): FieldKind {
|
|
|
111
112
|
interface FieldSpec {
|
|
112
113
|
label?: string
|
|
113
114
|
placeholder?: string
|
|
114
|
-
|
|
115
|
-
/** Ajuda na label: ícone ⓘ + tooltip no hover (explicação mais longa que o hint). */
|
|
115
|
+
/** Ajuda na label: ícone ⓘ + tooltip no hover. */
|
|
116
116
|
help?: string
|
|
117
117
|
/** Override explícito do tipo de campo, quando o auto-detect do Zod não basta.
|
|
118
118
|
* Honra 'textarea' (string sem max), 'code' (textarea monoespaçada, ex.: SKILL.md),
|
|
@@ -126,7 +126,7 @@ interface FieldSpec {
|
|
|
126
126
|
* No modo COMPOSIÇÃO o condicional pode (e deve) ser JSX de quem diagrama. */
|
|
127
127
|
showWhen?: (input: Record<string, unknown>) => boolean
|
|
128
128
|
/** Origem declarada das opções (static/dictionary/lookup) — ver a precedência
|
|
129
|
-
* em `ActionFormField`. `dictionary` resolve pelos dicts do
|
|
129
|
+
* em `ActionFormField`. `dictionary` resolve pelos dicts do OpusProvider. */
|
|
130
130
|
options?: OptionsSpec
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -171,11 +171,18 @@ function dictMetaOptions(schema: ZodTypeAny): SelectOption[] | undefined {
|
|
|
171
171
|
})
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Ícone de ajuda junto à label (ⓘ + tooltip Radix). Self-contained (inclui o Provider):
|
|
176
|
+
* renderiza somente quando `help` tem texto. É o mesmo slot que o `ActionFormField` usa para
|
|
177
|
+
* `FieldSpec.help`; exportado para que um formulário sem contrato mostre a ajuda no mesmo
|
|
178
|
+
* lugar (`<FieldLabel>Nome<LabelHelp help="…" /></FieldLabel>`) e o `opus copy` inventarie
|
|
179
|
+
* o texto de `help` como helper-text.
|
|
180
|
+
*
|
|
181
|
+
* `tabIndex={-1}`: fica FORA do autofocus do diálogo (senão o modal foca no ícone ao abrir e
|
|
182
|
+
* o tooltip vem aberto); abre só no hover. preventDefault: clicar o ícone não dispara o label
|
|
183
|
+
* (não foca o input / não toggla).
|
|
184
|
+
*/
|
|
185
|
+
export function LabelHelp({ help }: { help: string | undefined }): React.ReactElement | null {
|
|
179
186
|
if (help === undefined || help.trim().length === 0) return null
|
|
180
187
|
return (
|
|
181
188
|
<TooltipProvider delayDuration={300}>
|
|
@@ -208,7 +215,7 @@ function RequiredMark({ required }: { required: boolean }): React.ReactElement |
|
|
|
208
215
|
if (!required) return null
|
|
209
216
|
return (
|
|
210
217
|
<>
|
|
211
|
-
<span aria-hidden className="ml-0.5 text-
|
|
218
|
+
<span aria-hidden className="ml-0.5 text-context-danger-emphasis">
|
|
212
219
|
*
|
|
213
220
|
</span>
|
|
214
221
|
<span className="sr-only"> (obrigatório)</span>
|
|
@@ -287,9 +294,8 @@ export function ActionFormField({ name, options: optionsProp, className }: Actio
|
|
|
287
294
|
const spec: FieldSpec = fields[name] ?? {}
|
|
288
295
|
const fieldError = form.formState.errors[name]
|
|
289
296
|
const errorMessage = typeof fieldError?.message === 'string' ? fieldError.message : undefined
|
|
290
|
-
const descriptionId = spec.hint !== undefined ? `${name}-description` : undefined
|
|
291
297
|
const errorId = errorMessage !== undefined ? `${name}-error` : undefined
|
|
292
|
-
const describedBy =
|
|
298
|
+
const describedBy = errorId
|
|
293
299
|
useEffect(() => {
|
|
294
300
|
if (errorMessage === undefined) return
|
|
295
301
|
const control = document.getElementById(name)
|
|
@@ -371,7 +377,6 @@ export function ActionFormField({ name, options: optionsProp, className }: Actio
|
|
|
371
377
|
</span>
|
|
372
378
|
<LabelHelp help={spec.help} />
|
|
373
379
|
</FieldLabel>
|
|
374
|
-
{spec.hint !== undefined && <FieldDescription id={descriptionId}>{spec.hint}</FieldDescription>}
|
|
375
380
|
{errorMessage !== undefined && <FieldError id={errorId}>{errorMessage}</FieldError>}
|
|
376
381
|
</Field>
|
|
377
382
|
)
|
|
@@ -530,7 +535,7 @@ export function ActionFormField({ name, options: optionsProp, className }: Actio
|
|
|
530
535
|
<Input
|
|
531
536
|
id={`${name}-${i}-text`}
|
|
532
537
|
value={item.text}
|
|
533
|
-
placeholder={spec.placeholder ?? '
|
|
538
|
+
placeholder={spec.placeholder ?? 'Descreva o critério…'}
|
|
534
539
|
onChange={(e) => setItems(items.map((it, j) => (j === i ? { ...it, text: e.target.value } : it)))}
|
|
535
540
|
aria-invalid={errorMessage !== undefined}
|
|
536
541
|
aria-describedby={describedBy}
|
|
@@ -593,9 +598,6 @@ export function ActionFormField({ name, options: optionsProp, className }: Actio
|
|
|
593
598
|
/>
|
|
594
599
|
)}
|
|
595
600
|
|
|
596
|
-
{spec.hint !== undefined && (
|
|
597
|
-
<FieldDescription id={descriptionId}>{spec.hint}</FieldDescription>
|
|
598
|
-
)}
|
|
599
601
|
{errorMessage !== undefined && (
|
|
600
602
|
<FieldError id={errorId}>{errorMessage}</FieldError>
|
|
601
603
|
)}
|
|
@@ -651,7 +653,7 @@ export function ActionForm<TInput extends Record<string, unknown>, TData>({
|
|
|
651
653
|
onSuccess?.(data)
|
|
652
654
|
},
|
|
653
655
|
onError: (err) => {
|
|
654
|
-
toast.error(msgText(action.messages?.error,
|
|
656
|
+
toast.error(humanizeActionError(err, msgText(action.messages?.error, 'Não foi possível salvar.')))
|
|
655
657
|
},
|
|
656
658
|
})
|
|
657
659
|
|
|
@@ -669,13 +671,15 @@ export function ActionForm<TInput extends Record<string, unknown>, TData>({
|
|
|
669
671
|
))
|
|
670
672
|
|
|
671
673
|
// Banner de erro do servidor (chrome do próprio form, nos dois modos). O FieldGroup
|
|
672
|
-
// mantém o mesmo ritmo entre campos, blocos compostos e este estado.
|
|
674
|
+
// mantém o mesmo ritmo entre campos, blocos compostos e este estado. Só a frase de
|
|
675
|
+
// negócio chega aqui; código e mensagem técnica ficam no log do servidor.
|
|
673
676
|
const errorBanner =
|
|
674
677
|
error !== undefined && !isSuccess ? (
|
|
675
678
|
<div
|
|
676
|
-
|
|
679
|
+
role="alert"
|
|
680
|
+
className="rounded-md border border-context-danger-border bg-context-danger-subtle p-3 text-sm text-context-danger-emphasis"
|
|
677
681
|
>
|
|
678
|
-
|
|
682
|
+
{humanizeActionError(error, msgText(action.messages?.error, 'Não foi possível salvar.'))}
|
|
679
683
|
</div>
|
|
680
684
|
) : null
|
|
681
685
|
|