@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cabeçalho de superfície (interno): a anatomia que Page e Content compartilham.
|
|
3
|
+
*
|
|
4
|
+
* Um título, um contador opcional na mesma linha de base, uma descrição e uma região de ações
|
|
5
|
+
* que vai para o extremo oposto em larguras amplas e empilha abaixo do contexto nas estreitas.
|
|
6
|
+
* `PageHeader` e `ContentHeader` só nomeiam os slots (`page-*`, `content-*`) e a hierarquia
|
|
7
|
+
* visual (`page` ou `section`); layout, validação e classes vivem aqui, uma vez.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
Children,
|
|
12
|
+
Fragment,
|
|
13
|
+
isValidElement,
|
|
14
|
+
type ElementType,
|
|
15
|
+
type HTMLAttributes,
|
|
16
|
+
type ReactElement,
|
|
17
|
+
type ReactNode,
|
|
18
|
+
} from "react";
|
|
19
|
+
import { cn } from "../../lib/cn.ts";
|
|
20
|
+
|
|
21
|
+
export type SurfaceHeaderVariant = "page" | "section";
|
|
22
|
+
|
|
23
|
+
/** As classes por slot, por hierarquia visual — PageTitle e ContentTitle(page) são o mesmo. */
|
|
24
|
+
export const surfaceHeaderClasses: Record<
|
|
25
|
+
SurfaceHeaderVariant,
|
|
26
|
+
{ title: string; description: string; count: string; actions: string }
|
|
27
|
+
> = {
|
|
28
|
+
page: {
|
|
29
|
+
title: "text-2xl font-semibold tracking-tight",
|
|
30
|
+
description: "mt-1 truncate text-sm text-muted-foreground",
|
|
31
|
+
count: "font-mono text-base text-muted-foreground/60",
|
|
32
|
+
actions: "flex w-full shrink-0 items-center gap-4 sm:w-auto",
|
|
33
|
+
},
|
|
34
|
+
section: {
|
|
35
|
+
title: "text-sm font-semibold",
|
|
36
|
+
description: "mt-0.5 truncate text-xs text-muted-foreground",
|
|
37
|
+
count: "font-mono text-xs text-muted-foreground/60",
|
|
38
|
+
actions: "flex w-full shrink-0 items-center gap-2 sm:w-auto",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface SurfaceHeaderSlots {
|
|
43
|
+
title: ElementType;
|
|
44
|
+
count: ElementType;
|
|
45
|
+
description: ElementType;
|
|
46
|
+
actions: ElementType;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SurfaceHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
50
|
+
/** Nome do componente público, para as mensagens de erro (`PageHeader`, `ContentHeader`). */
|
|
51
|
+
name: string;
|
|
52
|
+
/** Prefixo dos `data-slot` (`page` → `page-header`, `page-heading`). */
|
|
53
|
+
slot: string;
|
|
54
|
+
/** Os componentes de slot que este cabeçalho reconhece. */
|
|
55
|
+
slots: SurfaceHeaderSlots;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Expande fragments de primeiro nível: o shorthand monta os slots dentro de um `<>`. */
|
|
59
|
+
function flatten(children: ReactNode): ReactNode[] {
|
|
60
|
+
return Children.toArray(children).flatMap((node) =>
|
|
61
|
+
isValidElement(node) && node.type === Fragment
|
|
62
|
+
? Children.toArray((node.props as { children?: ReactNode }).children)
|
|
63
|
+
: [node],
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function SurfaceHeader({
|
|
68
|
+
name,
|
|
69
|
+
slot,
|
|
70
|
+
slots,
|
|
71
|
+
className,
|
|
72
|
+
children,
|
|
73
|
+
...props
|
|
74
|
+
}: SurfaceHeaderProps): ReactElement {
|
|
75
|
+
const nodes = flatten(children);
|
|
76
|
+
const of = (type: ElementType) =>
|
|
77
|
+
nodes.filter((node) => isValidElement(node) && node.type === type);
|
|
78
|
+
const titles = of(slots.title);
|
|
79
|
+
const counts = of(slots.count);
|
|
80
|
+
const descriptions = of(slots.description);
|
|
81
|
+
const actionSlots = of(slots.actions);
|
|
82
|
+
const recognized =
|
|
83
|
+
titles.length + counts.length + descriptions.length + actionSlots.length;
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
titles.length !== 1 ||
|
|
87
|
+
counts.length > 1 ||
|
|
88
|
+
descriptions.length > 1 ||
|
|
89
|
+
actionSlots.length > 1 ||
|
|
90
|
+
recognized !== nodes.length
|
|
91
|
+
) {
|
|
92
|
+
const label = (type: ElementType) =>
|
|
93
|
+
typeof type === "string" ? type : (type as { name?: string }).name ?? "";
|
|
94
|
+
throw new Error(
|
|
95
|
+
`${name} exige um ${label(slots.title)} e aceita no máximo um ${label(slots.description)}, ${label(slots.count)} e ${label(slots.actions)} como filhos diretos.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
data-slot={`${slot}-header`}
|
|
102
|
+
className={cn(
|
|
103
|
+
actionSlots.length > 0 &&
|
|
104
|
+
"flex flex-col items-start gap-4 sm:flex-row sm:items-end sm:justify-between",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
<div data-slot={`${slot}-heading`} className="min-w-0">
|
|
110
|
+
<div className="flex items-baseline gap-2">
|
|
111
|
+
{titles}
|
|
112
|
+
{counts}
|
|
113
|
+
</div>
|
|
114
|
+
{descriptions}
|
|
115
|
+
</div>
|
|
116
|
+
{actionSlots}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* - Loading state automático (disabled + "..." enquanto roda).
|
|
6
6
|
* - Toast em sucesso/erro; cache invalidation via action.invalidates.
|
|
7
7
|
* - Confirmação opcional via <Dialog> (compacto, não fecha no clique fora) antes de disparar.
|
|
8
|
-
* - `icon` faz o botão virar icon-only com tooltip
|
|
9
|
-
* card). Absorveu o antigo DeleteButton, que era este
|
|
8
|
+
* - `icon` faz o botão virar icon-only com tooltip, no quadrado `icon-sm` (1.75rem) da escala:
|
|
9
|
+
* é a ação que mora NO item (linha, card). Absorveu o antigo DeleteButton, que era este
|
|
10
|
+
* componente com uma lixeira.
|
|
10
11
|
* Emite `data-action="<action.name>"` na raiz (selector E2E).
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -16,6 +17,7 @@ import type { SimpleContract } from '../../../core/index.ts'
|
|
|
16
17
|
import { useTriggerAction } from '../../drivers/react.tsx'
|
|
17
18
|
import { toast } from '../primitives/sonner.tsx'
|
|
18
19
|
import { cn } from '../../lib/cn.ts'
|
|
20
|
+
import { humanizeActionError } from '../../lib/action-errors.ts'
|
|
19
21
|
import { Button, type ButtonProps } from '../primitives/button.tsx'
|
|
20
22
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
|
|
21
23
|
import {
|
|
@@ -48,8 +50,8 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
48
50
|
context?: ButtonProps['context']
|
|
49
51
|
/** Tratamento visual do Button. */
|
|
50
52
|
variant?: ButtonProps['variant']
|
|
51
|
-
/** Tamanho do botão. */
|
|
52
|
-
size?: '
|
|
53
|
+
/** Tamanho do botão (escala de control.ts). Com `icon`, o botão é sempre `icon-sm`. */
|
|
54
|
+
size?: ButtonProps['size']
|
|
53
55
|
/** Desabilita o botão independente de loading. */
|
|
54
56
|
disabled?: boolean
|
|
55
57
|
/** Texto do confirm dialog. Default vem do `action.confirm` (ConfirmSpec do
|
|
@@ -71,16 +73,6 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
71
73
|
className?: string
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
/**
|
|
75
|
-
* Categorias de erro cuja frase do SERVIDOR vence o rótulo do contrato: "Este agente tem 3
|
|
76
|
-
* conversas — desabilite em vez de excluir" é acionável; "Falha ao excluir" não. É
|
|
77
|
-
* ALLOWLIST de propósito — erro inesperado carrega texto técnico cru (uma violação de FK
|
|
78
|
-
* viraria `violates foreign key constraint "user_unit_id_fkey"` no toast de quem só clicou
|
|
79
|
-
* no botão), então categoria desconhecida cai no rótulo genérico, o lado seguro de errar.
|
|
80
|
-
* `authorization` fica de fora: o runtime lança `Forbidden`, pior que a frase em pt-BR.
|
|
81
|
-
*/
|
|
82
|
-
const BUSINESS_ERRORS = ['conflict', 'validation', 'not_found']
|
|
83
|
-
|
|
84
76
|
export function ActionTrigger<TInput, TData>({
|
|
85
77
|
action,
|
|
86
78
|
input,
|
|
@@ -103,9 +95,9 @@ export function ActionTrigger<TInput, TData>({
|
|
|
103
95
|
setOpen(false)
|
|
104
96
|
},
|
|
105
97
|
onError: (err) => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
toast.error(
|
|
98
|
+
// A frase de negócio do servidor vence o rótulo do contrato (ver lib/action-errors.ts);
|
|
99
|
+
// fora da allowlist, vale o rótulo — nunca o texto técnico cru.
|
|
100
|
+
toast.error(humanizeActionError(err, msgText(action.messages?.error, 'Não foi possível concluir.')))
|
|
109
101
|
},
|
|
110
102
|
})
|
|
111
103
|
|
|
@@ -124,12 +116,19 @@ export function ActionTrigger<TInput, TData>({
|
|
|
124
116
|
}
|
|
125
117
|
: undefined)
|
|
126
118
|
const destructive = spec?.destructive === true
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Precedência de aparência:
|
|
121
|
+
* - Gatilho: `context` explícito vence `destructive`; sem ele, destrutivo é `danger`. Com
|
|
122
|
+
* ícone o botão é chrome do item: fica ghost e o vermelho aparece no hover — uma lixeira
|
|
123
|
+
* sólida vermelha em cada linha seria um campo minado visual.
|
|
124
|
+
* - Confirmar: `destructive` vence `context` (a pergunta é o momento de avisar o risco) e o
|
|
125
|
+
* botão é sempre `solid` — não herda o `variant` do gatilho, que descreve o item, não a
|
|
126
|
+
* decisão.
|
|
127
|
+
*/
|
|
129
128
|
const triggerContext = context ?? (destructive ? 'danger' : icon !== undefined ? 'neutral' : 'primary')
|
|
130
129
|
const triggerVariant = variant ?? (icon !== undefined ? 'ghost' : 'solid')
|
|
131
130
|
const confirmContext = destructive ? 'danger' : (context ?? 'primary')
|
|
132
|
-
const confirmVariant =
|
|
131
|
+
const confirmVariant = 'solid'
|
|
133
132
|
|
|
134
133
|
const fire = () => {
|
|
135
134
|
void trigger(input)
|
|
@@ -143,7 +142,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
143
142
|
<Button
|
|
144
143
|
context={triggerContext}
|
|
145
144
|
variant={triggerVariant}
|
|
146
|
-
size={icon !== undefined ? 'icon' : size}
|
|
145
|
+
size={icon !== undefined ? 'icon-sm' : size}
|
|
147
146
|
busy={isLoading}
|
|
148
147
|
disabled={disabled}
|
|
149
148
|
aria-label={icon !== undefined ? buttonLabel : undefined}
|
|
@@ -151,10 +150,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
151
150
|
e.stopPropagation()
|
|
152
151
|
onClick()
|
|
153
152
|
}}
|
|
154
|
-
className={cn(
|
|
155
|
-
icon !== undefined && 'size-7 shrink-0 text-muted-foreground/60',
|
|
156
|
-
className,
|
|
157
|
-
)}
|
|
153
|
+
className={cn(icon !== undefined && 'shrink-0 text-muted-foreground/60', className)}
|
|
158
154
|
data-action={action.name}
|
|
159
155
|
>
|
|
160
156
|
{icon ?? buttonLabel}
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
import type { ReactNode } from 'react'
|
|
10
10
|
import type { ViewAction } from '../../../core/index.ts'
|
|
11
11
|
import { useViewAction } from '../../drivers/react.tsx'
|
|
12
|
-
import {
|
|
12
|
+
import { isBusinessError, isRetryable } from '../../lib/action-errors.ts'
|
|
13
13
|
import { Skeleton } from '../primitives/skeleton.tsx'
|
|
14
|
-
import {
|
|
14
|
+
import { EmptySurface, ErrorSurface } from './state-surface.tsx'
|
|
15
15
|
|
|
16
16
|
export interface ActionViewProps<TInput, TData> {
|
|
17
17
|
action: ViewAction<TInput, TData>
|
|
@@ -20,12 +20,17 @@ export interface ActionViewProps<TInput, TData> {
|
|
|
20
20
|
children?: (data: TData, refetch: () => Promise<void>) => ReactNode
|
|
21
21
|
/** Alias de children (a API original). Children tem precedência. */
|
|
22
22
|
render?: (data: TData, refetch: () => Promise<void>) => ReactNode
|
|
23
|
-
/** Renderiza estado vazio (200 mas sem dado). Default: nada. */
|
|
23
|
+
/** Renderiza estado vazio (200 mas sem dado). Default: `emptyMessage` ou nada. */
|
|
24
24
|
empty?: ReactNode
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
/** Renderiza
|
|
28
|
-
|
|
25
|
+
/** Atalho do vazio: a frase na mesma superfície de DataState (Empty com moldura sólida). */
|
|
26
|
+
emptyMessage?: string
|
|
27
|
+
/** Renderiza loading. Default (ou `true`): 3 skeletons empilhados; `false` não renderiza
|
|
28
|
+
* nada enquanto carrega — o contêiner `data-action` permanece. */
|
|
29
|
+
loading?: ReactNode | boolean
|
|
30
|
+
/** Renderiza erro. Default: a mesma superfície de DataState — "Não foi possível carregar" +
|
|
31
|
+
* "Tentar de novo" (omitido em falta de permissão ou sessão); a frase do servidor entra como
|
|
32
|
+
* descrição só quando está na allowlist de lib/action-errors.ts. */
|
|
33
|
+
error?: (err: { code: string; message: string; category?: string }, retry: () => Promise<void>) => ReactNode
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export function ActionView<TInput, TData>({
|
|
@@ -34,6 +39,7 @@ export function ActionView<TInput, TData>({
|
|
|
34
39
|
children,
|
|
35
40
|
render,
|
|
36
41
|
empty,
|
|
42
|
+
emptyMessage,
|
|
37
43
|
loading,
|
|
38
44
|
error,
|
|
39
45
|
}: ActionViewProps<TInput, TData>) {
|
|
@@ -44,17 +50,16 @@ export function ActionView<TInput, TData>({
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
if (isLoading) {
|
|
47
|
-
|
|
48
|
-
<div
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<Skeleton className="h-4 w-2/3" />
|
|
53
|
-
<Skeleton className="h-4 w-1/2" />
|
|
54
|
-
</div>
|
|
55
|
-
)}
|
|
53
|
+
const defaultLoading = (
|
|
54
|
+
<div className="space-y-3">
|
|
55
|
+
<Skeleton className="h-6 w-1/3" />
|
|
56
|
+
<Skeleton className="h-4 w-2/3" />
|
|
57
|
+
<Skeleton className="h-4 w-1/2" />
|
|
56
58
|
</div>
|
|
57
59
|
)
|
|
60
|
+
const loadingNode =
|
|
61
|
+
typeof loading === 'boolean' ? (loading ? defaultLoading : null) : (loading ?? defaultLoading)
|
|
62
|
+
return <div data-action={action.name}>{loadingNode}</div>
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
if (isError && err !== undefined) {
|
|
@@ -63,17 +68,19 @@ export function ActionView<TInput, TData>({
|
|
|
63
68
|
}
|
|
64
69
|
return (
|
|
65
70
|
<div data-action={action.name}>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
<ErrorSurface
|
|
72
|
+
title="Não foi possível carregar"
|
|
73
|
+
description={isBusinessError(err) ? err.message.trim() : undefined}
|
|
74
|
+
{...(isRetryable(err) ? { onRetry: () => refetch() } : {})}
|
|
75
|
+
/>
|
|
71
76
|
</div>
|
|
72
77
|
)
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
if (data === undefined) {
|
|
76
|
-
|
|
81
|
+
const emptyNode =
|
|
82
|
+
empty ?? (emptyMessage !== undefined ? <EmptySurface title={emptyMessage} compact frame="structural" /> : null)
|
|
83
|
+
return <div data-action={action.name}>{emptyNode}</div>
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
return <div data-action={action.name}>{renderData(data, refetch)}</div>
|
|
@@ -8,12 +8,9 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export type AlertContext = Exclude<UiContext, "primary">;
|
|
10
10
|
export type AlertVariant = "subtle" | "outline";
|
|
11
|
-
export type LegacyAlertVariant =
|
|
12
|
-
"default" | "destructive" | "success" | "warning" | "info" | "danger";
|
|
13
|
-
|
|
14
11
|
interface AlertStyleOptions {
|
|
15
12
|
context?: AlertContext | null;
|
|
16
|
-
variant?: AlertVariant |
|
|
13
|
+
variant?: AlertVariant | null;
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
interface AlertRootContextValue {
|
|
@@ -47,29 +44,6 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
|
|
|
47
44
|
context: AlertContext;
|
|
48
45
|
variant: AlertVariant;
|
|
49
46
|
} {
|
|
50
|
-
const legacy =
|
|
51
|
-
variant === "default" ||
|
|
52
|
-
variant === "destructive" ||
|
|
53
|
-
variant === "success" ||
|
|
54
|
-
variant === "warning" ||
|
|
55
|
-
variant === "info" ||
|
|
56
|
-
variant === "danger";
|
|
57
|
-
if (context != null && legacy) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
"Alert não permite combinar context com uma variante semântica legada.",
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
if (variant === "default") return { context: "neutral", variant: "subtle" };
|
|
63
|
-
if (variant === "destructive")
|
|
64
|
-
return { context: "danger", variant: "subtle" };
|
|
65
|
-
if (
|
|
66
|
-
variant === "success" ||
|
|
67
|
-
variant === "warning" ||
|
|
68
|
-
variant === "info" ||
|
|
69
|
-
variant === "danger"
|
|
70
|
-
) {
|
|
71
|
-
return { context: variant, variant: "subtle" };
|
|
72
|
-
}
|
|
73
47
|
return { context: context ?? "neutral", variant: variant ?? "subtle" };
|
|
74
48
|
}
|
|
75
49
|
|
|
@@ -115,7 +115,7 @@ export function Ask({
|
|
|
115
115
|
type="button"
|
|
116
116
|
size="sm"
|
|
117
117
|
shape="pill"
|
|
118
|
-
context={selected ? '
|
|
118
|
+
context={selected ? 'primary' : 'neutral'}
|
|
119
119
|
variant={selected ? 'solid' : 'outline'}
|
|
120
120
|
role={question.multiSelect ? undefined : 'radio'}
|
|
121
121
|
aria-checked={question.multiSelect ? undefined : selected}
|
|
@@ -159,7 +159,7 @@ export function Ask({
|
|
|
159
159
|
disabled={locked}
|
|
160
160
|
/>
|
|
161
161
|
{invalid && (
|
|
162
|
-
<p id={errorId} className="text-sm text-
|
|
162
|
+
<p id={errorId} className="text-sm text-context-danger-emphasis">
|
|
163
163
|
Escolha uma opção ou escreva uma resposta.
|
|
164
164
|
</p>
|
|
165
165
|
)}
|
|
@@ -169,7 +169,7 @@ export function Ask({
|
|
|
169
169
|
</div>
|
|
170
170
|
</TooltipProvider>
|
|
171
171
|
{!validCount && (
|
|
172
|
-
<p role="alert" className="mt-3 text-sm text-
|
|
172
|
+
<p role="alert" className="mt-3 text-sm text-context-danger-emphasis">
|
|
173
173
|
Ask aceita de 1 a 4 perguntas.
|
|
174
174
|
</p>
|
|
175
175
|
)}
|
|
@@ -2,20 +2,26 @@ import * as React from "react"
|
|
|
2
2
|
import { Avatar as AvatarPrimitive } from "radix-ui"
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
import type { ControlSize } from './control.ts'
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Ejetado (14.0.0): `size` segue a escala de control.ts — o avatar de uma linha `sm` mede o
|
|
9
|
+
* mesmo que o controle `sm` ao lado (2rem); `xs` (1.5rem) é a lista densa; `default` (2.25rem)
|
|
10
|
+
* e `lg` (2.5rem) pareiam com Button/Input. O fallback e o selo acompanham o tamanho.
|
|
11
|
+
*/
|
|
6
12
|
function Avatar({
|
|
7
13
|
className,
|
|
8
14
|
size = "default",
|
|
9
15
|
...props
|
|
10
16
|
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
|
11
|
-
size?:
|
|
17
|
+
size?: ControlSize
|
|
12
18
|
}) {
|
|
13
19
|
return (
|
|
14
20
|
<AvatarPrimitive.Root
|
|
15
21
|
data-slot="avatar"
|
|
16
22
|
data-size={size}
|
|
17
23
|
className={cn(
|
|
18
|
-
"group/avatar relative flex
|
|
24
|
+
"group/avatar relative flex shrink-0 overflow-hidden rounded-full select-none data-[size=xs]:size-6 data-[size=sm]:size-8 data-[size=default]:size-9 data-[size=lg]:size-10",
|
|
19
25
|
className
|
|
20
26
|
)}
|
|
21
27
|
{...props}
|
|
@@ -44,7 +50,7 @@ function AvatarFallback({
|
|
|
44
50
|
<AvatarPrimitive.Fallback
|
|
45
51
|
data-slot="avatar-fallback"
|
|
46
52
|
className={cn(
|
|
47
|
-
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
|
53
|
+
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=xs]/avatar:text-xs group-data-[size=sm]/avatar:text-xs",
|
|
48
54
|
className
|
|
49
55
|
)}
|
|
50
56
|
{...props}
|
|
@@ -58,7 +64,8 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
|
|
58
64
|
data-slot="avatar-badge"
|
|
59
65
|
className={cn(
|
|
60
66
|
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background select-none",
|
|
61
|
-
"group-data-[size=
|
|
67
|
+
"group-data-[size=xs]/avatar:size-2 group-data-[size=xs]/avatar:[&>svg]:hidden",
|
|
68
|
+
"group-data-[size=sm]/avatar:size-2.5 group-data-[size=sm]/avatar:[&>svg]:size-2",
|
|
62
69
|
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
|
63
70
|
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
|
64
71
|
className
|
|
@@ -89,7 +96,10 @@ function AvatarGroupCount({
|
|
|
89
96
|
<div
|
|
90
97
|
data-slot="avatar-group-count"
|
|
91
98
|
className={cn(
|
|
92
|
-
"relative flex size-
|
|
99
|
+
"relative flex size-9 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background [&>svg]:size-4",
|
|
100
|
+
"group-has-data-[size=xs]/avatar-group:size-6 group-has-data-[size=xs]/avatar-group:[&>svg]:size-3",
|
|
101
|
+
"group-has-data-[size=sm]/avatar-group:size-8 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3.5",
|
|
102
|
+
"group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5",
|
|
93
103
|
className
|
|
94
104
|
)}
|
|
95
105
|
{...props}
|
|
@@ -3,6 +3,7 @@ import { Slot } from '@radix-ui/react-slot'
|
|
|
3
3
|
|
|
4
4
|
import type { UiContext } from '../../../core/ui-context.ts'
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
|
+
import { focusRing } from './control.ts'
|
|
6
7
|
import {
|
|
7
8
|
semanticOutlineClasses,
|
|
8
9
|
semanticSolidClasses,
|
|
@@ -11,56 +12,19 @@ import {
|
|
|
11
12
|
|
|
12
13
|
export type BadgeContext = UiContext
|
|
13
14
|
export type BadgeVariant = 'solid' | 'subtle' | 'outline'
|
|
14
|
-
export type LegacyBadgeVariant =
|
|
15
|
-
| 'default'
|
|
16
|
-
| 'secondary'
|
|
17
|
-
| 'destructive'
|
|
18
|
-
| 'success'
|
|
19
|
-
| 'warning'
|
|
20
|
-
| 'info'
|
|
21
|
-
| 'danger'
|
|
22
|
-
|
|
23
15
|
export interface BadgeStyleOptions {
|
|
24
16
|
context?: BadgeContext | null
|
|
25
|
-
variant?: BadgeVariant |
|
|
17
|
+
variant?: BadgeVariant | null
|
|
26
18
|
}
|
|
27
19
|
|
|
28
20
|
const baseClasses =
|
|
29
|
-
'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow]
|
|
21
|
+
'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&>svg]:pointer-events-none [&>svg]:size-3'
|
|
30
22
|
|
|
31
23
|
export function resolveBadgeStyle({
|
|
32
24
|
context,
|
|
33
25
|
variant,
|
|
34
26
|
}: BadgeStyleOptions = {}): { context: BadgeContext; variant: BadgeVariant } {
|
|
35
|
-
|
|
36
|
-
variant === 'default' ||
|
|
37
|
-
variant === 'secondary' ||
|
|
38
|
-
variant === 'destructive' ||
|
|
39
|
-
variant === 'success' ||
|
|
40
|
-
variant === 'warning' ||
|
|
41
|
-
variant === 'info' ||
|
|
42
|
-
variant === 'danger'
|
|
43
|
-
if (context != null && legacy) {
|
|
44
|
-
throw new Error('Badge não permite combinar context com uma variante semântica legada.')
|
|
45
|
-
}
|
|
46
|
-
switch (variant) {
|
|
47
|
-
case 'default':
|
|
48
|
-
return { context: 'primary', variant: 'solid' }
|
|
49
|
-
case 'secondary':
|
|
50
|
-
return { context: 'neutral', variant: 'solid' }
|
|
51
|
-
case 'destructive':
|
|
52
|
-
return { context: 'danger', variant: 'solid' }
|
|
53
|
-
case 'success':
|
|
54
|
-
case 'warning':
|
|
55
|
-
case 'info':
|
|
56
|
-
case 'danger':
|
|
57
|
-
return { context: variant, variant: 'subtle' }
|
|
58
|
-
default:
|
|
59
|
-
return {
|
|
60
|
-
context: context ?? 'neutral',
|
|
61
|
-
variant: variant ?? 'subtle',
|
|
62
|
-
}
|
|
63
|
-
}
|
|
27
|
+
return { context: context ?? 'neutral', variant: variant ?? 'subtle' }
|
|
64
28
|
}
|
|
65
29
|
|
|
66
30
|
/** Classes do Badge. Contexto seleciona significado; variante seleciona o tratamento visual. */
|
|
@@ -72,7 +36,7 @@ export function badgeVariants(options: BadgeStyleOptions = {}): string {
|
|
|
72
36
|
: resolved.variant === 'outline'
|
|
73
37
|
? semanticOutlineClasses[resolved.context]
|
|
74
38
|
: semanticSubtleClasses[resolved.context]
|
|
75
|
-
return cn(baseClasses, classes)
|
|
39
|
+
return cn(baseClasses, focusRing, classes)
|
|
76
40
|
}
|
|
77
41
|
|
|
78
42
|
export interface BadgeProps extends React.ComponentProps<'span'>, BadgeStyleOptions {
|
|
@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
|
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
6
|
|
|
7
7
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
|
8
|
-
return <nav aria-label="
|
|
8
|
+
return <nav aria-label="Navegação estrutural" data-slot="breadcrumb" {...props} />
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
@@ -93,7 +93,7 @@ function BreadcrumbEllipsis({
|
|
|
93
93
|
{...props}
|
|
94
94
|
>
|
|
95
95
|
<MoreHorizontal className="size-4" />
|
|
96
|
-
<span className="sr-only">
|
|
96
|
+
<span className="sr-only">Mais</span>
|
|
97
97
|
</span>
|
|
98
98
|
)
|
|
99
99
|
}
|