@softize/opus 12.11.0 → 13.1.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 +76 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/check.mjs +2 -7
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/copy.mjs +1 -5
- 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/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
- package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
- package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/radius-scale.md +1 -1
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
- package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
- package/registry/templates/app/package.json +1 -1
- 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 +2 -2
- 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/confirm.tsx +142 -42
- package/src/ui/components/patterns/content-header.tsx +7 -1
- package/src/ui/components/patterns/data-state.tsx +1 -1
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +12 -8
- package/src/ui/components/patterns/list.tsx +36 -41
- package/src/ui/components/patterns/page-state.tsx +2 -2
- package/src/ui/components/patterns/page.tsx +19 -1
- package/src/ui/components/patterns/shell-nav.tsx +10 -3
- package/src/ui/components/patterns/sidebar.tsx +43 -32
- package/src/ui/components/patterns/trigger.tsx +39 -38
- package/src/ui/components/patterns/view.tsx +26 -17
- package/src/ui/components/primitives/alert.tsx +14 -8
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/badge.tsx +11 -6
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +16 -3
- 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 +12 -0
- package/src/ui/components/primitives/copyable.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +202 -40
- package/src/ui/components/primitives/dot.tsx +5 -0
- package/src/ui/components/primitives/drawer.tsx +18 -8
- package/src/ui/components/primitives/empty.tsx +3 -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 +1 -1
- 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 -3
- 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 +5 -5
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +190 -8
- package/src/ui/components/primitives/switch.tsx +1 -0
- package/src/ui/components/primitives/tabs.tsx +1 -0
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/components/primitives/tooltip.tsx +1 -0
- package/src/ui/docs/DocBrowser.tsx +102 -23
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/accordion.md +22 -16
- package/src/ui/docs/content/action-form-card.md +8 -8
- package/src/ui/docs/content/action-form-dialog.md +9 -9
- package/src/ui/docs/content/action-form.md +37 -36
- package/src/ui/docs/content/action-list-dialog.md +11 -6
- package/src/ui/docs/content/action-list.md +73 -39
- package/src/ui/docs/content/action-trigger.md +29 -15
- package/src/ui/docs/content/action-view.md +17 -10
- package/src/ui/docs/content/actions.md +9 -9
- package/src/ui/docs/content/ai.md +3 -3
- package/src/ui/docs/content/alert.md +14 -12
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/aspect-ratio.md +4 -4
- package/src/ui/docs/content/audit.md +2 -2
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/avatar.md +34 -14
- package/src/ui/docs/content/badge.md +3 -3
- package/src/ui/docs/content/breadcrumb.md +13 -8
- package/src/ui/docs/content/button.md +81 -6
- package/src/ui/docs/content/calendar.md +18 -5
- package/src/ui/docs/content/card.md +27 -1
- package/src/ui/docs/content/carousel.md +16 -11
- package/src/ui/docs/content/chat.md +23 -3
- package/src/ui/docs/content/checkbox.md +7 -7
- package/src/ui/docs/content/cli.md +74 -22
- package/src/ui/docs/content/collapsible.md +8 -8
- package/src/ui/docs/content/command.md +16 -8
- package/src/ui/docs/content/composer.md +17 -2
- package/src/ui/docs/content/content.md +17 -2
- package/src/ui/docs/content/copyable.md +12 -3
- package/src/ui/docs/content/customization.md +5 -5
- package/src/ui/docs/content/cycle.md +3 -3
- package/src/ui/docs/content/data-state.md +11 -12
- package/src/ui/docs/content/data.md +26 -33
- package/src/ui/docs/content/detail.md +22 -4
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +17 -10
- package/src/ui/docs/content/dock.md +11 -3
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/drawer.md +27 -14
- package/src/ui/docs/content/empty-value.md +2 -2
- package/src/ui/docs/content/empty.md +19 -12
- package/src/ui/docs/content/events.md +4 -4
- package/src/ui/docs/content/field.md +34 -12
- package/src/ui/docs/content/getting-started.md +3 -3
- package/src/ui/docs/content/icon-picker.md +19 -4
- package/src/ui/docs/content/input-otp.md +20 -12
- package/src/ui/docs/content/input.md +121 -9
- package/src/ui/docs/content/item.md +27 -13
- package/src/ui/docs/content/kbd.md +19 -11
- package/src/ui/docs/content/label.md +12 -3
- package/src/ui/docs/content/log.md +4 -4
- package/src/ui/docs/content/markdown.md +7 -6
- package/src/ui/docs/content/mcp.md +13 -15
- package/src/ui/docs/content/menu.md +40 -16
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +59 -6
- package/src/ui/docs/content/pagination.md +22 -17
- package/src/ui/docs/content/popover.md +22 -8
- package/src/ui/docs/content/progress.md +15 -16
- package/src/ui/docs/content/queue.md +5 -5
- package/src/ui/docs/content/radio-group.md +20 -12
- package/src/ui/docs/content/router.md +11 -6
- package/src/ui/docs/content/scheduler.md +4 -5
- package/src/ui/docs/content/scroll-area.md +12 -7
- package/src/ui/docs/content/select.md +47 -34
- package/src/ui/docs/content/semantic-context.md +2 -2
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +329 -54
- package/src/ui/docs/content/skeleton.md +9 -2
- package/src/ui/docs/content/slider.md +8 -7
- package/src/ui/docs/content/spinner.md +8 -8
- package/src/ui/docs/content/split.md +29 -5
- package/src/ui/docs/content/storage.md +6 -8
- package/src/ui/docs/content/switch.md +8 -7
- package/src/ui/docs/content/table.md +13 -3
- package/src/ui/docs/content/tabs.md +28 -14
- package/src/ui/docs/content/testing.md +9 -11
- package/src/ui/docs/content/textarea.md +12 -4
- package/src/ui/docs/content/toast.md +47 -13
- package/src/ui/docs/content/toggle.md +75 -7
- package/src/ui/docs/content/tokens.md +7 -7
- package/src/ui/docs/content/tooltip.md +19 -11
- package/src/ui/docs/content/truncate.md +15 -8
- package/src/ui/docs/content/ui.md +24 -9
- package/src/ui/docs/content/upgrading.md +7 -8
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +25 -42
- package/src/ui/docs/standalone.tsx +2 -2
- package/src/ui/drivers/react.tsx +17 -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 +65 -95
- package/src/ui/react.tsx +17 -16
- package/src/ui/theme.css +60 -8
- package/src/vite/design.ts +6 -18
- package/src/ui/components/primitives/alert-dialog.tsx +0 -192
- package/src/ui/docs/content/alert-dialog.md +0 -73
- package/src/ui/docs/content/button-group.md +0 -71
- package/src/ui/docs/content/confirm.md +0 -120
- package/src/ui/docs/content/input-group.md +0 -79
- package/src/ui/docs/content/page-state.md +0 -45
- package/src/ui/docs/content/toggle-group.md +0 -81
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `dialog` —
|
|
2
|
+
* `dialog` — respostas imperativas (alert · confirm · prompt · choose) em UMA linha.
|
|
3
3
|
*
|
|
4
4
|
* await dialog.alert({ title: 'Sessão expirada', description: 'Você será desconectado.' })
|
|
5
5
|
* if (await dialog.confirm({ title: 'Excluir a sessão?', context: 'danger' })) …
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Mesma forma do `toast` (superfície imperativa + host no shell), com uma diferença: eles
|
|
9
9
|
* RESPONDEM — `alert` devolve `Promise<void>`, `confirm` `Promise<boolean>`, `prompt`
|
|
10
|
-
* `Promise<string | null
|
|
10
|
+
* `Promise<string | null>` e `choose` a união dos resultados declarados. É o que tira as
|
|
11
|
+
* ~15 linhas de JSX que cada tela montava.
|
|
11
12
|
*
|
|
12
13
|
* Namespace `dialog` de propósito: `window.alert/confirm/prompt` são globais do browser, e
|
|
13
14
|
* um import esquecido cairia no nativo (cinza, sem tema) sem erro. `window.dialog` não
|
|
14
15
|
* existe — a família fica imune ao footgun. `confirm()` segue exportado como alias.
|
|
15
16
|
*
|
|
16
|
-
* Por baixo é o <
|
|
17
|
+
* Por baixo é o <Dialog mode="alert">: `role="alertdialog"`, não fecha no clique fora, foco preso
|
|
17
18
|
* — a semântica que uma interrupção que EXIGE resposta pede. ESC/Cancelar resolve o "não"
|
|
18
19
|
* de cada tipo (void, `false`, `null`).
|
|
19
20
|
*/
|
|
@@ -21,23 +22,28 @@
|
|
|
21
22
|
import * as React from 'react'
|
|
22
23
|
import type { UiContext } from '../../../core/ui-context.ts'
|
|
23
24
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} from '../primitives/
|
|
25
|
+
Dialog,
|
|
26
|
+
DialogBody,
|
|
27
|
+
DialogClose,
|
|
28
|
+
DialogContent,
|
|
29
|
+
DialogDescription,
|
|
30
|
+
DialogFooter,
|
|
31
|
+
DialogHeader,
|
|
32
|
+
DialogMedia,
|
|
33
|
+
DialogTitle,
|
|
34
|
+
} from '../primitives/dialog.tsx'
|
|
35
|
+
import {
|
|
36
|
+
Button,
|
|
37
|
+
type ButtonContext,
|
|
38
|
+
type ButtonVariant,
|
|
39
|
+
} from '../primitives/button.tsx'
|
|
34
40
|
import { Input } from '../primitives/input.tsx'
|
|
35
41
|
|
|
36
42
|
/** Campos comuns aos três — o corpo (`body`) é o que o texto da `description` não expressa. */
|
|
37
43
|
interface DialogBase {
|
|
38
44
|
title: React.ReactNode
|
|
39
45
|
description?: React.ReactNode
|
|
40
|
-
/** Ícone grande acima do título (o quadro do
|
|
46
|
+
/** Ícone grande acima do título (o quadro do DialogMedia). */
|
|
41
47
|
media?: React.ReactNode
|
|
42
48
|
/** Conteúdo próprio entre a descrição e os botões (lista, detalhe destacado). */
|
|
43
49
|
body?: React.ReactNode
|
|
@@ -67,10 +73,27 @@ export interface PromptOptions extends DialogBase {
|
|
|
67
73
|
defaultValue?: string
|
|
68
74
|
}
|
|
69
75
|
|
|
76
|
+
export interface DialogChoice<TResult extends string = string> {
|
|
77
|
+
/** Valor devolvido quando esta ação for escolhida. Deve ser único na coleção. */
|
|
78
|
+
result: TResult
|
|
79
|
+
label: React.ReactNode
|
|
80
|
+
context?: ButtonContext
|
|
81
|
+
variant?: ButtonVariant
|
|
82
|
+
initialFocus?: boolean
|
|
83
|
+
disabled?: boolean
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `choose` — uma escolha simples entre resultados explícitos. */
|
|
87
|
+
export interface ChooseOptions<TResult extends string = string>
|
|
88
|
+
extends Omit<DialogBase, 'action'> {
|
|
89
|
+
actions: readonly DialogChoice<TResult>[]
|
|
90
|
+
}
|
|
91
|
+
|
|
70
92
|
type Pending =
|
|
71
93
|
| ({ kind: 'alert'; id: number; resolve: () => void } & AlertOptions)
|
|
72
94
|
| ({ kind: 'confirm'; id: number; resolve: (ok: boolean) => void } & ConfirmOptions)
|
|
73
95
|
| ({ kind: 'prompt'; id: number; resolve: (value: string | null) => void } & PromptOptions)
|
|
96
|
+
| ({ kind: 'choose'; id: number; resolve: (result: string | null) => void } & ChooseOptions)
|
|
74
97
|
|
|
75
98
|
let nextId = 0
|
|
76
99
|
let listener: ((p: Pending) => void) | null = null
|
|
@@ -94,6 +117,23 @@ function rejectAmbiguousContext(
|
|
|
94
117
|
)
|
|
95
118
|
}
|
|
96
119
|
|
|
120
|
+
function validateChoose<TResult extends string>(options: ChooseOptions<TResult>): Error | null {
|
|
121
|
+
if (options.actions.length === 0) {
|
|
122
|
+
return new Error('dialog.choose() exige pelo menos uma ação.')
|
|
123
|
+
}
|
|
124
|
+
const focused = options.actions.filter((action) => action.initialFocus === true)
|
|
125
|
+
if (focused.length !== 1) {
|
|
126
|
+
return new Error('dialog.choose() exige uma única ação com initialFocus.')
|
|
127
|
+
}
|
|
128
|
+
if (focused[0]?.disabled === true) {
|
|
129
|
+
return new Error('dialog.choose() não permite initialFocus em uma ação desabilitada.')
|
|
130
|
+
}
|
|
131
|
+
if (new Set(options.actions.map((action) => action.result)).size !== options.actions.length) {
|
|
132
|
+
return new Error('dialog.choose() exige resultados únicos.')
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
97
137
|
export const dialog = {
|
|
98
138
|
/** Avisa e espera o "OK". `Promise<void>` — só reconhecimento, sem decisão. */
|
|
99
139
|
alert(options: AlertOptions): Promise<void> {
|
|
@@ -123,9 +163,24 @@ export const dialog = {
|
|
|
123
163
|
listener?.({ ...options, kind: 'prompt', id: nextId++, resolve })
|
|
124
164
|
})
|
|
125
165
|
},
|
|
166
|
+
/** Oferece resultados explícitos. Fechar/ESC sem escolher resolve `null`. */
|
|
167
|
+
choose<const TResult extends string>(options: ChooseOptions<TResult>): Promise<TResult | null> {
|
|
168
|
+
const invalid = validateChoose(options)
|
|
169
|
+
if (invalid !== null) return Promise.reject(invalid)
|
|
170
|
+
const err = requireHost('dialog.choose()')
|
|
171
|
+
if (err !== null) return Promise.reject(err)
|
|
172
|
+
return new Promise<TResult | null>((resolve) => {
|
|
173
|
+
listener?.({
|
|
174
|
+
...options,
|
|
175
|
+
kind: 'choose',
|
|
176
|
+
id: nextId++,
|
|
177
|
+
resolve: (result) => resolve(result as TResult | null),
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
},
|
|
126
181
|
}
|
|
127
182
|
|
|
128
|
-
/** Alias retrocompatível —
|
|
183
|
+
/** @deprecated Alias retrocompatível — use `dialog.confirm()`. */
|
|
129
184
|
export function confirm(options: ConfirmOptions): Promise<boolean> {
|
|
130
185
|
return dialog.confirm(options)
|
|
131
186
|
}
|
|
@@ -164,6 +219,7 @@ export function DialogHost(): React.ReactElement {
|
|
|
164
219
|
const cancel = (p: Pending): void => {
|
|
165
220
|
if (p.kind === 'alert') p.resolve()
|
|
166
221
|
else if (p.kind === 'confirm') p.resolve(false)
|
|
222
|
+
else if (p.kind === 'prompt') p.resolve(null)
|
|
167
223
|
else p.resolve(null)
|
|
168
224
|
}
|
|
169
225
|
const accept = (p: Pending, value: string): void => {
|
|
@@ -172,24 +228,28 @@ export function DialogHost(): React.ReactElement {
|
|
|
172
228
|
else p.resolve(value)
|
|
173
229
|
}
|
|
174
230
|
|
|
175
|
-
const variant = current !== undefined && current.kind
|
|
176
|
-
|
|
231
|
+
const variant = current !== undefined && (current.kind === 'confirm' || current.kind === 'prompt')
|
|
232
|
+
? current.variant
|
|
233
|
+
: undefined
|
|
234
|
+
const context = current !== undefined && (current.kind === 'confirm' || current.kind === 'prompt')
|
|
177
235
|
? current.context ?? (variant === 'destructive' ? 'danger' : 'primary')
|
|
178
236
|
: 'primary'
|
|
179
237
|
const defaultAction = current?.kind === 'alert' ? 'OK' : 'Confirmar'
|
|
180
238
|
|
|
181
239
|
return (
|
|
182
|
-
<
|
|
240
|
+
<Dialog
|
|
241
|
+
mode="alert"
|
|
183
242
|
open={current !== undefined}
|
|
184
243
|
onOpenChange={(open) => {
|
|
185
244
|
if (!open && current !== undefined) drop(current.id, cancel)
|
|
186
245
|
}}
|
|
187
246
|
>
|
|
188
247
|
{current !== undefined && (
|
|
189
|
-
<
|
|
248
|
+
<DialogContent
|
|
190
249
|
data-slot="dialog"
|
|
191
250
|
data-kind={current.kind}
|
|
192
|
-
|
|
251
|
+
className={current.kind === 'choose' ? 'max-w-lg' : undefined}
|
|
252
|
+
// Prompt: o foco vai pro input, não pro Cancelar (default do Dialog). Via
|
|
193
253
|
// querySelector (não ref) — o Input é function component sem forwardRef, e isto
|
|
194
254
|
// funciona igual em React 18 e 19.
|
|
195
255
|
onOpenAutoFocus={
|
|
@@ -204,14 +264,14 @@ export function DialogHost(): React.ReactElement {
|
|
|
204
264
|
: undefined
|
|
205
265
|
}
|
|
206
266
|
>
|
|
207
|
-
<
|
|
208
|
-
{current.media !== undefined && <
|
|
209
|
-
<
|
|
267
|
+
<DialogHeader>
|
|
268
|
+
{current.media !== undefined && <DialogMedia>{current.media}</DialogMedia>}
|
|
269
|
+
<DialogTitle>{current.title}</DialogTitle>
|
|
210
270
|
{current.description !== undefined && (
|
|
211
|
-
<
|
|
271
|
+
<DialogDescription>{current.description}</DialogDescription>
|
|
212
272
|
)}
|
|
213
|
-
</
|
|
214
|
-
{current.body !== undefined && <
|
|
273
|
+
</DialogHeader>
|
|
274
|
+
{current.body !== undefined && <DialogBody>{current.body}</DialogBody>}
|
|
215
275
|
{current.kind === 'prompt' && (
|
|
216
276
|
<Input
|
|
217
277
|
value={text}
|
|
@@ -225,25 +285,65 @@ export function DialogHost(): React.ReactElement {
|
|
|
225
285
|
}}
|
|
226
286
|
/>
|
|
227
287
|
)}
|
|
228
|
-
<
|
|
229
|
-
{
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
288
|
+
<DialogFooter
|
|
289
|
+
className={
|
|
290
|
+
current.kind === 'choose'
|
|
291
|
+
? 'grid-cols-[repeat(auto-fit,minmax(7rem,1fr))]'
|
|
292
|
+
: undefined
|
|
293
|
+
}
|
|
294
|
+
>
|
|
295
|
+
{current.kind === 'choose' ? (
|
|
296
|
+
current.actions.map((action, index) => {
|
|
297
|
+
const primary = index === current.actions.length - 1
|
|
298
|
+
return (
|
|
299
|
+
<DialogClose
|
|
300
|
+
key={action.result}
|
|
301
|
+
initialFocus={action.initialFocus}
|
|
302
|
+
asChild
|
|
303
|
+
>
|
|
304
|
+
<Button
|
|
305
|
+
context={action.context ?? (primary ? 'primary' : 'neutral')}
|
|
306
|
+
variant={action.variant ?? (primary ? 'solid' : 'ghost')}
|
|
307
|
+
disabled={action.disabled}
|
|
308
|
+
onClick={() => drop(current.id, (p) => {
|
|
309
|
+
if (p.kind === 'choose') p.resolve(action.result)
|
|
310
|
+
})}
|
|
311
|
+
>
|
|
312
|
+
{action.label}
|
|
313
|
+
</Button>
|
|
314
|
+
</DialogClose>
|
|
315
|
+
)
|
|
316
|
+
})
|
|
317
|
+
) : (
|
|
318
|
+
<>
|
|
319
|
+
{current.kind !== 'alert' && (
|
|
320
|
+
<DialogClose initialFocus asChild>
|
|
321
|
+
<Button
|
|
322
|
+
context="neutral"
|
|
323
|
+
variant="ghost"
|
|
324
|
+
onClick={() => drop(current.id, cancel)}
|
|
325
|
+
>
|
|
326
|
+
{current.cancel ?? 'Cancelar'}
|
|
327
|
+
</Button>
|
|
328
|
+
</DialogClose>
|
|
329
|
+
)}
|
|
330
|
+
<DialogClose initialFocus={current.kind === 'alert'} asChild>
|
|
331
|
+
<Button
|
|
332
|
+
context={context}
|
|
333
|
+
variant="solid"
|
|
334
|
+
onClick={() => drop(current.id, (p) => accept(p, text))}
|
|
335
|
+
>
|
|
336
|
+
{current.action ?? defaultAction}
|
|
337
|
+
</Button>
|
|
338
|
+
</DialogClose>
|
|
339
|
+
</>
|
|
233
340
|
)}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
variant="solid"
|
|
237
|
-
onClick={() => drop(current.id, (p) => accept(p, text))}
|
|
238
|
-
>
|
|
239
|
-
{current.action ?? defaultAction}
|
|
240
|
-
</AlertDialogAction>
|
|
241
|
-
</AlertDialogFooter>
|
|
242
|
-
</AlertDialogContent>
|
|
341
|
+
</DialogFooter>
|
|
342
|
+
</DialogContent>
|
|
243
343
|
)}
|
|
244
|
-
</
|
|
344
|
+
</Dialog>
|
|
245
345
|
)
|
|
246
346
|
}
|
|
247
347
|
|
|
248
|
-
/** Alias retrocompatível —
|
|
348
|
+
/** @deprecated Alias retrocompatível — monte `<DialogHost />`. */
|
|
249
349
|
export const ConfirmHost = DialogHost
|
|
@@ -171,7 +171,13 @@ interface ContentHeaderComposedProps extends Omit<
|
|
|
171
171
|
export type ContentHeaderProps =
|
|
172
172
|
ContentHeaderShorthandProps | ContentHeaderComposedProps;
|
|
173
173
|
|
|
174
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Header estrutural de Content; não é uma região autônoma.
|
|
176
|
+
*
|
|
177
|
+
* Compartilha o layout (título, meta, descrição e ações) com `PageHeader` (page.tsx); os dois
|
|
178
|
+
* serão unificados numa próxima série — até lá, uma correção de layout em um deve ser espelhada
|
|
179
|
+
* no outro.
|
|
180
|
+
*/
|
|
175
181
|
export function ContentHeader({
|
|
176
182
|
title,
|
|
177
183
|
meta,
|
|
@@ -56,7 +56,7 @@ export function DataState({
|
|
|
56
56
|
<tr data-slot="data-state" data-status={status}>
|
|
57
57
|
<td
|
|
58
58
|
colSpan={colSpan}
|
|
59
|
-
className={cn('px-4 py-10 text-center text-sm', error ? 'text-
|
|
59
|
+
className={cn('px-4 py-10 text-center text-sm', error ? 'text-context-danger-emphasis' : 'text-muted-foreground/60')}
|
|
60
60
|
>
|
|
61
61
|
{error ? (
|
|
62
62
|
<div className="flex flex-col items-center gap-3">
|
|
@@ -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,
|
|
@@ -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'
|
|
@@ -111,8 +112,9 @@ function inferFieldKind(schema: ZodTypeAny): FieldKind {
|
|
|
111
112
|
interface FieldSpec {
|
|
112
113
|
label?: string
|
|
113
114
|
placeholder?: string
|
|
115
|
+
/** @deprecated Use `help` (ajuda junto à label) ou `description` do campo composto. */
|
|
114
116
|
hint?: string
|
|
115
|
-
/** Ajuda na label: ícone ⓘ + tooltip no hover
|
|
117
|
+
/** Ajuda na label: ícone ⓘ + tooltip no hover. */
|
|
116
118
|
help?: string
|
|
117
119
|
/** Override explícito do tipo de campo, quando o auto-detect do Zod não basta.
|
|
118
120
|
* Honra 'textarea' (string sem max), 'code' (textarea monoespaçada, ex.: SKILL.md),
|
|
@@ -126,7 +128,7 @@ interface FieldSpec {
|
|
|
126
128
|
* No modo COMPOSIÇÃO o condicional pode (e deve) ser JSX de quem diagrama. */
|
|
127
129
|
showWhen?: (input: Record<string, unknown>) => boolean
|
|
128
130
|
/** Origem declarada das opções (static/dictionary/lookup) — ver a precedência
|
|
129
|
-
* em `ActionFormField`. `dictionary` resolve pelos dicts do
|
|
131
|
+
* em `ActionFormField`. `dictionary` resolve pelos dicts do OpusProvider. */
|
|
130
132
|
options?: OptionsSpec
|
|
131
133
|
}
|
|
132
134
|
|
|
@@ -208,7 +210,7 @@ function RequiredMark({ required }: { required: boolean }): React.ReactElement |
|
|
|
208
210
|
if (!required) return null
|
|
209
211
|
return (
|
|
210
212
|
<>
|
|
211
|
-
<span aria-hidden className="ml-0.5 text-
|
|
213
|
+
<span aria-hidden className="ml-0.5 text-context-danger-emphasis">
|
|
212
214
|
*
|
|
213
215
|
</span>
|
|
214
216
|
<span className="sr-only"> (obrigatório)</span>
|
|
@@ -530,7 +532,7 @@ export function ActionFormField({ name, options: optionsProp, className }: Actio
|
|
|
530
532
|
<Input
|
|
531
533
|
id={`${name}-${i}-text`}
|
|
532
534
|
value={item.text}
|
|
533
|
-
placeholder={spec.placeholder ?? '
|
|
535
|
+
placeholder={spec.placeholder ?? 'Descreva o critério…'}
|
|
534
536
|
onChange={(e) => setItems(items.map((it, j) => (j === i ? { ...it, text: e.target.value } : it)))}
|
|
535
537
|
aria-invalid={errorMessage !== undefined}
|
|
536
538
|
aria-describedby={describedBy}
|
|
@@ -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
|
|
|
@@ -84,19 +84,13 @@ import {
|
|
|
84
84
|
import {
|
|
85
85
|
Dialog,
|
|
86
86
|
DialogBody,
|
|
87
|
+
DialogClose,
|
|
87
88
|
DialogContent,
|
|
89
|
+
DialogDescription,
|
|
88
90
|
DialogFooter,
|
|
89
91
|
DialogHeader,
|
|
90
92
|
DialogTitle,
|
|
91
93
|
} from "../primitives/dialog.tsx";
|
|
92
|
-
import {
|
|
93
|
-
AlertDialog,
|
|
94
|
-
AlertDialogContent,
|
|
95
|
-
AlertDialogDescription,
|
|
96
|
-
AlertDialogFooter,
|
|
97
|
-
AlertDialogHeader,
|
|
98
|
-
AlertDialogTitle,
|
|
99
|
-
} from "../primitives/alert-dialog.tsx";
|
|
100
94
|
import {
|
|
101
95
|
InputGroup,
|
|
102
96
|
InputGroupAddon,
|
|
@@ -760,7 +754,7 @@ function PeriodControl({
|
|
|
760
754
|
<Button
|
|
761
755
|
key={p.value}
|
|
762
756
|
size="sm"
|
|
763
|
-
context={period === p.value ? "
|
|
757
|
+
context={period === p.value ? "primary" : "neutral"}
|
|
764
758
|
variant={period === p.value ? "solid" : "ghost"}
|
|
765
759
|
className="justify-start"
|
|
766
760
|
onClick={() => {
|
|
@@ -1434,6 +1428,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1434
1428
|
<PopoverTrigger asChild>
|
|
1435
1429
|
<Button
|
|
1436
1430
|
variant="outline"
|
|
1431
|
+
size="icon"
|
|
1437
1432
|
className="shrink-0"
|
|
1438
1433
|
aria-label="Exibição"
|
|
1439
1434
|
>
|
|
@@ -1769,47 +1764,47 @@ export function ActionList<TInput, TItem>({
|
|
|
1769
1764
|
</div>
|
|
1770
1765
|
{footer}
|
|
1771
1766
|
{confirming !== null && (
|
|
1772
|
-
<
|
|
1767
|
+
<Dialog
|
|
1768
|
+
mode="alert"
|
|
1773
1769
|
open
|
|
1774
1770
|
onOpenChange={(o) => {
|
|
1775
1771
|
if (!o) setConfirming(null);
|
|
1776
1772
|
}}
|
|
1777
1773
|
>
|
|
1778
|
-
<
|
|
1779
|
-
<
|
|
1780
|
-
<
|
|
1774
|
+
<DialogContent>
|
|
1775
|
+
<DialogHeader>
|
|
1776
|
+
<DialogTitle>
|
|
1781
1777
|
{confirming.confirm?.title ?? confirming.label}
|
|
1782
|
-
</
|
|
1778
|
+
</DialogTitle>
|
|
1783
1779
|
{confirming.confirm?.message !== undefined && (
|
|
1784
|
-
<
|
|
1780
|
+
<DialogDescription>
|
|
1785
1781
|
{confirming.confirm.message}
|
|
1786
|
-
</
|
|
1782
|
+
</DialogDescription>
|
|
1787
1783
|
)}
|
|
1788
|
-
</
|
|
1789
|
-
<
|
|
1790
|
-
<
|
|
1791
|
-
variant="ghost"
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
>
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
</AlertDialog>
|
|
1784
|
+
</DialogHeader>
|
|
1785
|
+
<DialogFooter>
|
|
1786
|
+
<DialogClose initialFocus asChild>
|
|
1787
|
+
<Button variant="ghost" size="sm">
|
|
1788
|
+
Cancelar
|
|
1789
|
+
</Button>
|
|
1790
|
+
</DialogClose>
|
|
1791
|
+
<DialogClose asChild>
|
|
1792
|
+
<Button
|
|
1793
|
+
size="sm"
|
|
1794
|
+
context={confirming.destructive === true ? "danger" : "primary"}
|
|
1795
|
+
variant="solid"
|
|
1796
|
+
onClick={() => {
|
|
1797
|
+
const a = confirming;
|
|
1798
|
+
setConfirming(null);
|
|
1799
|
+
void runBatch(a);
|
|
1800
|
+
}}
|
|
1801
|
+
>
|
|
1802
|
+
{confirming.label}
|
|
1803
|
+
</Button>
|
|
1804
|
+
</DialogClose>
|
|
1805
|
+
</DialogFooter>
|
|
1806
|
+
</DialogContent>
|
|
1807
|
+
</Dialog>
|
|
1813
1808
|
)}
|
|
1814
1809
|
</div>
|
|
1815
1810
|
</TooltipProvider>
|
|
@@ -12,7 +12,7 @@ import { cn } from "../../lib/cn.ts";
|
|
|
12
12
|
import { Alert } from "../primitives/alert.tsx";
|
|
13
13
|
import {
|
|
14
14
|
Empty,
|
|
15
|
-
|
|
15
|
+
EmptyActions,
|
|
16
16
|
EmptyDescription,
|
|
17
17
|
EmptyHeader,
|
|
18
18
|
EmptyMedia,
|
|
@@ -108,7 +108,7 @@ export function PageState({
|
|
|
108
108
|
<EmptyDescription>{description}</EmptyDescription>
|
|
109
109
|
)}
|
|
110
110
|
</EmptyHeader>
|
|
111
|
-
{action !== undefined && <
|
|
111
|
+
{action !== undefined && <EmptyActions>{action}</EmptyActions>}
|
|
112
112
|
</Empty>
|
|
113
113
|
</div>
|
|
114
114
|
);
|
|
@@ -14,10 +14,26 @@ import {
|
|
|
14
14
|
type ReactElement,
|
|
15
15
|
type ReactNode,
|
|
16
16
|
} from "react";
|
|
17
|
+
import { createPortal } from "react-dom";
|
|
17
18
|
import { cn } from "../../lib/cn.ts";
|
|
18
19
|
|
|
19
20
|
const PageContext = createContext(false);
|
|
20
21
|
const PageHeaderContext = createContext(false);
|
|
22
|
+
const PageActionsTargetContext = createContext<HTMLElement | null>(null);
|
|
23
|
+
|
|
24
|
+
export function PageActionsTarget({
|
|
25
|
+
target,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
target: HTMLElement | null;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}): ReactElement {
|
|
31
|
+
return (
|
|
32
|
+
<PageActionsTargetContext.Provider value={target}>
|
|
33
|
+
{children}
|
|
34
|
+
</PageActionsTargetContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
21
37
|
|
|
22
38
|
function requireParent(
|
|
23
39
|
inside: boolean,
|
|
@@ -221,7 +237,8 @@ export function PageActions({
|
|
|
221
237
|
...props
|
|
222
238
|
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
223
239
|
requireParent(useContext(PageHeaderContext), "PageActions", "PageHeader");
|
|
224
|
-
|
|
240
|
+
const target = useContext(PageActionsTargetContext);
|
|
241
|
+
const actions = (
|
|
225
242
|
<div
|
|
226
243
|
data-slot="page-actions"
|
|
227
244
|
className={cn(
|
|
@@ -231,6 +248,7 @@ export function PageActions({
|
|
|
231
248
|
{...props}
|
|
232
249
|
/>
|
|
233
250
|
);
|
|
251
|
+
return target === null ? actions : createPortal(actions, target);
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
export function PageBody({
|