@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/cli-shared.mjs +131 -0
  6. package/bin/lib/copy.mjs +276 -6
  7. package/bin/lib/db.mjs +16 -74
  8. package/bin/lib/gen-openapi.mjs +3 -3
  9. package/bin/lib/gen-runner.mjs +1 -1
  10. package/bin/lib/gen.mjs +14 -69
  11. package/bin/lib/mcp.mjs +3 -1
  12. package/bin/lib/seed.mjs +5 -62
  13. package/docs/code-style.md +4 -1
  14. package/docs/ownership-vs-shadcn-lock.md +2 -3
  15. package/docs/protocol.md +7 -7
  16. package/docs/releasing.md +8 -2
  17. package/package.json +7 -3
  18. package/registry/instructions/opus.md +3 -3
  19. package/registry/templates/app/package.json +1 -1
  20. package/registry/templates/app/src/App.tsx +11 -6
  21. package/registry/templates/app/src/main.tsx +4 -4
  22. package/src/audit/drivers/console.ts +1 -0
  23. package/src/auth/drivers/better-auth.ts +1 -0
  24. package/src/auth/drivers/jwt.ts +1 -0
  25. package/src/cache/drivers/memory.ts +1 -0
  26. package/src/client/drivers/fetch.ts +2 -1
  27. package/src/core/actions.ts +6 -1
  28. package/src/core/audit.ts +9 -3
  29. package/src/core/contracts.ts +7 -0
  30. package/src/core/domain.ts +1 -1
  31. package/src/core/errors.ts +18 -15
  32. package/src/core/index.ts +4 -2
  33. package/src/core/package-version.ts +26 -0
  34. package/src/core/reactions.ts +1 -1
  35. package/src/core/runtime.ts +33 -23
  36. package/src/core/schedules.ts +1 -1
  37. package/src/core/types.ts +5 -6
  38. package/src/dsl/eval.ts +2 -2
  39. package/src/dsl/kysely.ts +2 -2
  40. package/src/dsl/loads.ts +1 -1
  41. package/src/dsl/parser.ts +5 -5
  42. package/src/events/drivers/mitt.ts +1 -0
  43. package/src/mcp/index.ts +2 -1
  44. package/src/observability/drivers/opentelemetry.ts +1 -0
  45. package/src/queue/drivers/bullmq.ts +3 -3
  46. package/src/scheduler/drivers/node-cron.ts +3 -2
  47. package/src/scheduler/every.ts +7 -7
  48. package/src/schema/openapi.ts +3 -3
  49. package/src/seed/index.ts +29 -0
  50. package/src/server/drivers/fastify.ts +5 -2
  51. package/src/server/drivers/node.ts +9 -6
  52. package/src/server/index.ts +3 -1
  53. package/src/storage/drivers/fs.ts +1 -0
  54. package/src/testing/index.ts +3 -3
  55. package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
  56. package/src/ui/components/patterns/confirm.tsx +2 -31
  57. package/src/ui/components/patterns/content-header.tsx +44 -137
  58. package/src/ui/components/patterns/data-state.tsx +42 -68
  59. package/src/ui/components/patterns/dock.tsx +20 -3
  60. package/src/ui/components/patterns/form.tsx +26 -22
  61. package/src/ui/components/patterns/list.tsx +18 -12
  62. package/src/ui/components/patterns/page-state.tsx +39 -51
  63. package/src/ui/components/patterns/page.tsx +37 -54
  64. package/src/ui/components/patterns/sidebar.tsx +17 -6
  65. package/src/ui/components/patterns/state-surface.tsx +148 -0
  66. package/src/ui/components/patterns/surface-header.tsx +119 -0
  67. package/src/ui/components/patterns/trigger.tsx +21 -25
  68. package/src/ui/components/patterns/view.tsx +29 -22
  69. package/src/ui/components/primitives/alert.tsx +1 -27
  70. package/src/ui/components/primitives/ask.tsx +3 -3
  71. package/src/ui/components/primitives/avatar.tsx +15 -5
  72. package/src/ui/components/primitives/badge.tsx +5 -41
  73. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  74. package/src/ui/components/primitives/button.tsx +39 -30
  75. package/src/ui/components/primitives/calendar.tsx +28 -2
  76. package/src/ui/components/primitives/carousel.tsx +3 -3
  77. package/src/ui/components/primitives/chat.tsx +1 -1
  78. package/src/ui/components/primitives/checkbox.tsx +1 -1
  79. package/src/ui/components/primitives/command.tsx +2 -2
  80. package/src/ui/components/primitives/control.ts +72 -0
  81. package/src/ui/components/primitives/copyable.tsx +1 -1
  82. package/src/ui/components/primitives/dialog.tsx +12 -7
  83. package/src/ui/components/primitives/dot.tsx +1 -25
  84. package/src/ui/components/primitives/drawer.tsx +10 -3
  85. package/src/ui/components/primitives/field.tsx +3 -3
  86. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  87. package/src/ui/components/primitives/input-group.tsx +12 -9
  88. package/src/ui/components/primitives/input-otp.tsx +1 -1
  89. package/src/ui/components/primitives/input.tsx +2 -2
  90. package/src/ui/components/primitives/item.tsx +3 -1
  91. package/src/ui/components/primitives/menu.tsx +1 -7
  92. package/src/ui/components/primitives/pagination.tsx +16 -8
  93. package/src/ui/components/primitives/progress.tsx +32 -3
  94. package/src/ui/components/primitives/radio-group.tsx +1 -1
  95. package/src/ui/components/primitives/resizable.tsx +3 -1
  96. package/src/ui/components/primitives/select.tsx +6 -6
  97. package/src/ui/components/primitives/slider.tsx +5 -1
  98. package/src/ui/components/primitives/sonner.tsx +3 -0
  99. package/src/ui/components/primitives/spinner.tsx +13 -16
  100. package/src/ui/components/primitives/switch.tsx +5 -1
  101. package/src/ui/components/primitives/tabs.tsx +6 -3
  102. package/src/ui/components/primitives/textarea.tsx +1 -1
  103. package/src/ui/components/primitives/toggle.tsx +9 -4
  104. package/src/ui/components/primitives/tooltip.tsx +1 -0
  105. package/src/ui/docs/changelog.tsx +1 -1
  106. package/src/ui/docs/content/action-form.md +36 -3
  107. package/src/ui/docs/content/action-list-dialog.md +2 -2
  108. package/src/ui/docs/content/action-list.md +13 -2
  109. package/src/ui/docs/content/action-trigger.md +12 -5
  110. package/src/ui/docs/content/action-view.md +11 -3
  111. package/src/ui/docs/content/ask.md +11 -0
  112. package/src/ui/docs/content/avatar.md +7 -3
  113. package/src/ui/docs/content/button.md +30 -14
  114. package/src/ui/docs/content/calendar.md +13 -0
  115. package/src/ui/docs/content/card.md +26 -0
  116. package/src/ui/docs/content/chat.md +20 -0
  117. package/src/ui/docs/content/cli.md +71 -19
  118. package/src/ui/docs/content/communication.md +36 -0
  119. package/src/ui/docs/content/composer.md +15 -0
  120. package/src/ui/docs/content/content.md +17 -1
  121. package/src/ui/docs/content/copyable.md +8 -0
  122. package/src/ui/docs/content/data-state.md +17 -13
  123. package/src/ui/docs/content/detail.md +19 -1
  124. package/src/ui/docs/content/dialog.md +1 -4
  125. package/src/ui/docs/content/dictionary-value.md +9 -2
  126. package/src/ui/docs/content/dock.md +8 -0
  127. package/src/ui/docs/content/dot.md +8 -0
  128. package/src/ui/docs/content/empty.md +2 -2
  129. package/src/ui/docs/content/getting-started.md +2 -2
  130. package/src/ui/docs/content/icon-picker.md +11 -0
  131. package/src/ui/docs/content/input.md +1 -1
  132. package/src/ui/docs/content/item.md +1 -1
  133. package/src/ui/docs/content/label.md +7 -0
  134. package/src/ui/docs/content/menu.md +6 -0
  135. package/src/ui/docs/content/metric-card.md +13 -0
  136. package/src/ui/docs/content/page.md +20 -4
  137. package/src/ui/docs/content/pagination.md +11 -9
  138. package/src/ui/docs/content/popover.md +6 -0
  139. package/src/ui/docs/content/progress.md +8 -11
  140. package/src/ui/docs/content/select.md +5 -5
  141. package/src/ui/docs/content/semantic-context.md +5 -4
  142. package/src/ui/docs/content/sidebar.md +13 -47
  143. package/src/ui/docs/content/skeleton.md +6 -0
  144. package/src/ui/docs/content/spinner.md +9 -6
  145. package/src/ui/docs/content/split.md +21 -0
  146. package/src/ui/docs/content/switch.md +1 -1
  147. package/src/ui/docs/content/tabs.md +1 -1
  148. package/src/ui/docs/content/textarea.md +7 -0
  149. package/src/ui/docs/content/toggle.md +1 -1
  150. package/src/ui/docs/content/tokens.md +4 -4
  151. package/src/ui/docs/content/truncate.md +8 -0
  152. package/src/ui/docs/content/ui.md +14 -0
  153. package/src/ui/docs/doc-client.tsx +5 -5
  154. package/src/ui/docs/doc.tsx +26 -14
  155. package/src/ui/docs/registry.tsx +5 -5
  156. package/src/ui/docs/standalone.tsx +2 -2
  157. package/src/ui/drivers/react.tsx +12 -12
  158. package/src/ui/lib/action-errors.ts +45 -0
  159. package/src/ui/lib/zod-pt-br.ts +31 -4
  160. package/src/ui/meta.ts +8 -8
  161. package/src/ui/react.tsx +10 -16
  162. package/src/ui/theme.css +10 -8
  163. package/src/vite/design.ts +6 -18
  164. 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: é a ação que mora NO item (linha,
9
- * card). Absorveu o antigo DeleteButton, que era este componente com uma lixeira.
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?: 'default' | 'sm' | 'lg' | 'icon'
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
- const detail = err.message.trim()
107
- const speakable = BUSINESS_ERRORS.includes(err.category as string) && detail !== ''
108
- toast.error(speakable ? detail : msgText(action.messages?.error, err.message))
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
- // Com ícone o botão é chrome do item: fica ghost, e o vermelho aparece no hover — uma
128
- // lixeira sólida vermelha em cada linha seria um campo minado visual.
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 = variant ?? 'solid'
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 { Alert } from '../primitives/alert.tsx'
12
+ import { isBusinessError, isRetryable } from '../../lib/action-errors.ts'
13
13
  import { Skeleton } from '../primitives/skeleton.tsx'
14
- import { Button } from '../primitives/button.tsx'
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
- /** Renderiza loading. Default: 3 skeletons stackados. */
26
- loading?: ReactNode
27
- /** Renderiza erro. Default: mensagem + botão "Tentar de novo". */
28
- error?: (err: { code: string; message: string }, retry: () => Promise<void>) => ReactNode
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
- return (
48
- <div data-action={action.name}>
49
- {loading ?? (
50
- <div className="space-y-3">
51
- <Skeleton className="h-6 w-1/3" />
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
- <Alert context="danger" title={err.code} description={err.message}>
67
- <Button variant="outline" size="sm" onClick={() => void refetch()}>
68
- Tentar de novo
69
- </Button>
70
- </Alert>
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
- return <div data-action={action.name}>{empty ?? null}</div>
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 | LegacyAlertVariant | null;
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 ? 'neutral' : 'primary'}
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-destructive">
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-destructive">
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?: "default" | "sm" | "lg"
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 size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
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=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
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-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
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 | LegacyBadgeVariant | null
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] focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&>svg]:pointer-events-none [&>svg]:size-3'
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
- const legacy =
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="breadcrumb" data-slot="breadcrumb" {...props} />
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">More</span>
96
+ <span className="sr-only">Mais</span>
97
97
  </span>
98
98
  )
99
99
  }