@softize/opus 12.11.0 → 13.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 +29 -0
- package/bin/lib/check.mjs +2 -7
- package/bin/lib/copy.mjs +1 -5
- 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/radius-scale.md +1 -1
- package/package.json +1 -1
- 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/src/ui/components/patterns/confirm.tsx +140 -40
- package/src/ui/components/patterns/list.tsx +35 -40
- package/src/ui/components/patterns/page-state.tsx +2 -2
- package/src/ui/components/patterns/sidebar.tsx +26 -26
- package/src/ui/components/patterns/trigger.tsx +25 -22
- package/src/ui/components/primitives/alert.tsx +3 -3
- package/src/ui/components/primitives/dialog.tsx +196 -39
- package/src/ui/components/primitives/drawer.tsx +8 -5
- package/src/ui/components/primitives/empty.tsx +3 -3
- package/src/ui/components/primitives/item.tsx +3 -3
- package/src/ui/components/primitives/sonner.tsx +187 -8
- package/src/ui/docs/DocBrowser.tsx +102 -23
- 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 +28 -34
- package/src/ui/docs/content/action-list-dialog.md +11 -6
- package/src/ui/docs/content/action-list.md +64 -39
- package/src/ui/docs/content/action-trigger.md +21 -14
- package/src/ui/docs/content/action-view.md +8 -8
- 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/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 +5 -5
- package/src/ui/docs/content/card.md +1 -1
- package/src/ui/docs/content/carousel.md +16 -11
- package/src/ui/docs/content/chat.md +3 -3
- package/src/ui/docs/content/checkbox.md +7 -7
- package/src/ui/docs/content/cli.md +5 -5
- 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 +2 -2
- package/src/ui/docs/content/content.md +2 -2
- package/src/ui/docs/content/copyable.md +4 -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 +3 -3
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +8 -8
- package/src/ui/docs/content/dock.md +3 -3
- 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 +1 -1
- package/src/ui/docs/content/icon-picker.md +8 -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 +5 -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 +34 -16
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +51 -6
- package/src/ui/docs/content/pagination.md +22 -17
- package/src/ui/docs/content/popover.md +16 -8
- package/src/ui/docs/content/progress.md +7 -5
- 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 +42 -29
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +323 -54
- package/src/ui/docs/content/skeleton.md +3 -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 +8 -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 +5 -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 +3 -3
- package/src/ui/docs/content/tooltip.md +19 -11
- package/src/ui/docs/content/truncate.md +7 -8
- package/src/ui/docs/content/ui.md +10 -9
- package/src/ui/docs/content/upgrading.md +7 -8
- package/src/ui/docs/registry.tsx +20 -37
- package/src/ui/meta.ts +64 -94
- package/src/ui/react.tsx +15 -16
- package/src/ui/theme.css +50 -0
- 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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
const repoRoot = resolve(process.argv[2] ?? process.cwd());
|
|
7
|
+
const contentDir = resolve(repoRoot, "packages/opus/src/ui/docs/content");
|
|
8
|
+
const registryPath = resolve(repoRoot, "packages/opus/src/ui/docs/registry.tsx");
|
|
9
|
+
|
|
10
|
+
if (!existsSync(contentDir) || !existsSync(registryPath)) {
|
|
11
|
+
console.error(
|
|
12
|
+
"Não encontrei packages/opus/src/ui/docs/content e registry.tsx. Passe a raiz do repositório Opus.",
|
|
13
|
+
);
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const registry = readFileSync(registryPath, "utf8");
|
|
18
|
+
const importedFiles = new Set(
|
|
19
|
+
[...registry.matchAll(/\.\/content\/([^"']+\.md)\?raw/g)].map((match) => match[1]),
|
|
20
|
+
);
|
|
21
|
+
const files = readdirSync(contentDir)
|
|
22
|
+
.filter((file) => file.endsWith(".md"))
|
|
23
|
+
.sort();
|
|
24
|
+
const findings = [];
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const source = readFileSync(resolve(contentDir, file), "utf8");
|
|
28
|
+
const lines = source.split("\n");
|
|
29
|
+
|
|
30
|
+
if (!importedFiles.has(file)) {
|
|
31
|
+
findings.push(`${file}: página órfã; não há import no registry`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let inFence = false;
|
|
35
|
+
let previousHeading = 0;
|
|
36
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
37
|
+
const line = lines[index];
|
|
38
|
+
const lineNumber = index + 1;
|
|
39
|
+
|
|
40
|
+
if (/^```/.test(line)) {
|
|
41
|
+
inFence = !inFence;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (inFence) continue;
|
|
45
|
+
|
|
46
|
+
const heading = /^(#{1,6})\s+/.exec(line);
|
|
47
|
+
if (heading) {
|
|
48
|
+
const level = heading[1].length;
|
|
49
|
+
if (previousHeading > 0 && level > previousHeading + 1) {
|
|
50
|
+
findings.push(`${file}:${lineNumber}: heading salta de h${previousHeading} para h${level}`);
|
|
51
|
+
}
|
|
52
|
+
previousHeading = level;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (/^#{2,6}\s+Props(?:\s|$)/i.test(line)) {
|
|
56
|
+
findings.push(`${file}:${lineNumber}: nomeie a referência como “Propriedades de Componente”`);
|
|
57
|
+
}
|
|
58
|
+
if (/^\|\s*Prop\s*\|/i.test(line) || /\|\s*Default\s*\|/.test(line)) {
|
|
59
|
+
findings.push(`${file}:${lineNumber}: use “Propriedade” e “Padrão” no cabeçalho da tabela`);
|
|
60
|
+
}
|
|
61
|
+
if (/\b(?:pra|pro|pros|numa)\b/i.test(line)) {
|
|
62
|
+
findings.push(`${file}:${lineNumber}: abreviação coloquial na prosa`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (inFence) findings.push(`${file}: fence de código sem fechamento`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const importedFile of importedFiles) {
|
|
70
|
+
if (!files.includes(importedFile)) {
|
|
71
|
+
findings.push(`${importedFile}: importado pelo registry, mas o arquivo não existe`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (findings.length > 0) {
|
|
76
|
+
console.error(findings.join("\n"));
|
|
77
|
+
console.error(`\n${findings.length} problema(s) estrutural(is) encontrado(s).`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`${files.length} páginas auditadas; nenhuma inconsistência estrutural encontrada.`);
|
|
@@ -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,6 +163,21 @@ 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
183
|
/** Alias retrocompatível — `confirm()` é `dialog.confirm()`. */
|
|
@@ -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,23 +285,63 @@ 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
|
|
|
@@ -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,
|
|
@@ -1769,47 +1763,48 @@ export function ActionList<TInput, TItem>({
|
|
|
1769
1763
|
</div>
|
|
1770
1764
|
{footer}
|
|
1771
1765
|
{confirming !== null && (
|
|
1772
|
-
<
|
|
1766
|
+
<Dialog
|
|
1767
|
+
mode="alert"
|
|
1773
1768
|
open
|
|
1774
1769
|
onOpenChange={(o) => {
|
|
1775
1770
|
if (!o) setConfirming(null);
|
|
1776
1771
|
}}
|
|
1777
1772
|
>
|
|
1778
|
-
<
|
|
1779
|
-
<
|
|
1780
|
-
<
|
|
1773
|
+
<DialogContent>
|
|
1774
|
+
<DialogHeader>
|
|
1775
|
+
<DialogTitle>
|
|
1781
1776
|
{confirming.confirm?.title ?? confirming.label}
|
|
1782
|
-
</
|
|
1777
|
+
</DialogTitle>
|
|
1783
1778
|
{confirming.confirm?.message !== undefined && (
|
|
1784
|
-
<
|
|
1779
|
+
<DialogDescription>
|
|
1785
1780
|
{confirming.confirm.message}
|
|
1786
|
-
</
|
|
1781
|
+
</DialogDescription>
|
|
1787
1782
|
)}
|
|
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
|
-
</
|
|
1783
|
+
</DialogHeader>
|
|
1784
|
+
<DialogFooter>
|
|
1785
|
+
<DialogClose initialFocus asChild>
|
|
1786
|
+
<Button variant="ghost" size="sm">
|
|
1787
|
+
Cancelar
|
|
1788
|
+
</Button>
|
|
1789
|
+
</DialogClose>
|
|
1790
|
+
<DialogClose asChild>
|
|
1791
|
+
<Button
|
|
1792
|
+
size="sm"
|
|
1793
|
+
variant={
|
|
1794
|
+
confirming.destructive === true ? "destructive" : "default"
|
|
1795
|
+
}
|
|
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
|
);
|
|
@@ -327,24 +327,15 @@ export function SidebarItem({
|
|
|
327
327
|
|
|
328
328
|
export interface SidebarNavGroup {
|
|
329
329
|
label?: string;
|
|
330
|
-
items
|
|
331
|
-
subgroups?: SidebarNavSubgroup[];
|
|
330
|
+
items: SidebarNavItem[];
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
export type SidebarNavItem = Omit<SidebarItemProps, "active" | "onClick"> & {
|
|
335
334
|
id: string;
|
|
336
335
|
};
|
|
337
336
|
|
|
338
|
-
export interface SidebarNavSubgroup {
|
|
339
|
-
label?: string;
|
|
340
|
-
items: SidebarNavItem[];
|
|
341
|
-
}
|
|
342
|
-
|
|
343
337
|
const titled = (label?: string): boolean => (label ?? "").trim() !== "";
|
|
344
|
-
const
|
|
345
|
-
subgroup.items.length > 0;
|
|
346
|
-
const groupFilled = (group: SidebarNavGroup): boolean =>
|
|
347
|
-
(group.items?.length ?? 0) > 0 || (group.subgroups?.some(subFilled) ?? false);
|
|
338
|
+
const groupFilled = (group: SidebarNavGroup): boolean => group.items.length > 0;
|
|
348
339
|
|
|
349
340
|
/** Rótulo discreto de um grupo de navegação, compartilhado por listas e árvores. */
|
|
350
341
|
export function SidebarGroupLabel({
|
|
@@ -367,6 +358,29 @@ export function SidebarGroupLabel({
|
|
|
367
358
|
);
|
|
368
359
|
}
|
|
369
360
|
|
|
361
|
+
export interface SidebarTreeGroupProps {
|
|
362
|
+
className?: string;
|
|
363
|
+
children: ReactNode;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Agrupa descendentes de um nó e desenha a guia visual da hierarquia. */
|
|
367
|
+
export function SidebarTreeGroup({
|
|
368
|
+
className,
|
|
369
|
+
children,
|
|
370
|
+
}: SidebarTreeGroupProps): React.ReactElement {
|
|
371
|
+
return (
|
|
372
|
+
<div
|
|
373
|
+
data-slot="sidebar-tree-group"
|
|
374
|
+
className={cn(
|
|
375
|
+
"ml-[1.0625rem] space-y-0.5 border-l border-border pl-1.5",
|
|
376
|
+
className,
|
|
377
|
+
)}
|
|
378
|
+
>
|
|
379
|
+
{children}
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
370
384
|
/** Navegação controlada para Sidebar. Header/footer continuam slots explícitos do pai. */
|
|
371
385
|
export function SidebarNav({
|
|
372
386
|
groups,
|
|
@@ -406,21 +420,7 @@ export function SidebarNav({
|
|
|
406
420
|
{!collapsed && titled(group.label) && (
|
|
407
421
|
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
|
408
422
|
)}
|
|
409
|
-
{group.items
|
|
410
|
-
{group.subgroups?.map((subgroup, si) =>
|
|
411
|
-
!subFilled(subgroup) ? null : (
|
|
412
|
-
<div key={si} className="space-y-0.5">
|
|
413
|
-
{!collapsed && titled(subgroup.label) && (
|
|
414
|
-
<div className="pb-0.5 pl-4 pr-2.5 pt-1.5 text-[0.6875rem] font-medium text-muted-foreground/50">
|
|
415
|
-
{subgroup.label}
|
|
416
|
-
</div>
|
|
417
|
-
)}
|
|
418
|
-
{subgroup.items.map((item) =>
|
|
419
|
-
renderItem(item, titled(subgroup.label)),
|
|
420
|
-
)}
|
|
421
|
-
</div>
|
|
422
|
-
),
|
|
423
|
-
)}
|
|
423
|
+
{group.items.map((item) => renderItem(item))}
|
|
424
424
|
</div>
|
|
425
425
|
),
|
|
426
426
|
)}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Use pra mutações sem form: assign, escalate, close, archive, excluir.
|
|
5
5
|
* - Loading state automático (disabled + "..." enquanto roda).
|
|
6
6
|
* - Toast em sucesso/erro; cache invalidation via action.invalidates.
|
|
7
|
-
* - Confirmação opcional via <
|
|
7
|
+
* - Confirmação opcional via <Dialog> (compacto, não fecha no clique fora) antes de disparar.
|
|
8
8
|
* - `icon` faz o botão virar icon-only com tooltip: é a ação que mora NO item (linha,
|
|
9
9
|
* card). Absorveu o antigo DeleteButton, que era este componente com uma lixeira.
|
|
10
10
|
* Emite `data-action="<action.name>"` na raiz (selector E2E).
|
|
@@ -19,13 +19,14 @@ import { cn } from '../../lib/cn.ts'
|
|
|
19
19
|
import { Button, type ButtonProps } from '../primitives/button.tsx'
|
|
20
20
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
Dialog,
|
|
23
|
+
DialogClose,
|
|
24
|
+
DialogContent,
|
|
25
|
+
DialogDescription,
|
|
26
|
+
DialogFooter,
|
|
27
|
+
DialogHeader,
|
|
28
|
+
DialogTitle,
|
|
29
|
+
} from '../primitives/dialog.tsx'
|
|
29
30
|
|
|
30
31
|
/** action.messages.* pode ser string ou I18nRef ({ key, default }). Resolve pra texto. */
|
|
31
32
|
function msgText(m: unknown, fallback: string): string {
|
|
@@ -174,28 +175,30 @@ export function ActionTrigger<TInput, TData>({
|
|
|
174
175
|
<>
|
|
175
176
|
{renderButton(() => setOpen(true))}
|
|
176
177
|
|
|
177
|
-
<
|
|
178
|
-
<
|
|
179
|
-
<
|
|
180
|
-
<
|
|
178
|
+
<Dialog mode="alert" open={open} onOpenChange={setOpen}>
|
|
179
|
+
<DialogContent data-action={action.name}>
|
|
180
|
+
<DialogHeader>
|
|
181
|
+
<DialogTitle>{confirm.title}</DialogTitle>
|
|
181
182
|
{(itemLabel !== undefined || confirm.description !== undefined) && (
|
|
182
|
-
<
|
|
183
|
+
<DialogDescription>
|
|
183
184
|
{itemLabel !== undefined && <span className="font-medium text-foreground">“{itemLabel}”</span>}
|
|
184
185
|
{itemLabel !== undefined && confirm.description !== undefined ? ' — ' : ''}
|
|
185
186
|
{confirm.description}
|
|
186
|
-
</
|
|
187
|
+
</DialogDescription>
|
|
187
188
|
)}
|
|
188
|
-
</
|
|
189
|
-
<
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
</DialogHeader>
|
|
190
|
+
<DialogFooter>
|
|
191
|
+
<DialogClose initialFocus asChild>
|
|
192
|
+
<Button context="neutral" variant="ghost" disabled={isLoading}>
|
|
193
|
+
{confirm.cancelLabel ?? 'Cancelar'}
|
|
194
|
+
</Button>
|
|
195
|
+
</DialogClose>
|
|
193
196
|
<Button context={confirmContext} variant={confirmVariant} busy={isLoading} onClick={fire}>
|
|
194
197
|
{confirm.actionLabel ?? buttonLabel}
|
|
195
198
|
</Button>
|
|
196
|
-
</
|
|
197
|
-
</
|
|
198
|
-
</
|
|
199
|
+
</DialogFooter>
|
|
200
|
+
</DialogContent>
|
|
201
|
+
</Dialog>
|
|
199
202
|
</>
|
|
200
203
|
)
|
|
201
204
|
}
|
|
@@ -76,7 +76,7 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
|
|
|
76
76
|
export function alertVariants(options: AlertStyleOptions = {}): string {
|
|
77
77
|
const resolved = resolveAlertStyle(options);
|
|
78
78
|
return cn(
|
|
79
|
-
"relative grid w-full grid-cols-[0_minmax(0,1fr)] items-stretch gap-x-0 gap-y-1 rounded-lg border
|
|
79
|
+
"relative grid w-full grid-cols-[0_minmax(0,1fr)_auto] items-stretch gap-x-0 gap-y-1 rounded-lg border p-4 text-sm has-[>[data-slot=alert-media]]:grid-cols-[3rem_minmax(0,1fr)_auto]",
|
|
80
80
|
resolved.variant === "outline"
|
|
81
81
|
? semanticOutlineClasses[resolved.context]
|
|
82
82
|
: semanticSubtleClasses[resolved.context],
|
|
@@ -92,7 +92,7 @@ function alertMediaClasses(context: AlertContext): string {
|
|
|
92
92
|
danger: "text-context-danger-emphasis",
|
|
93
93
|
};
|
|
94
94
|
return cn(
|
|
95
|
-
"col-start-1 row-start-1 flex
|
|
95
|
+
"col-start-1 row-start-1 flex size-9 self-start items-center justify-center rounded-md bg-current/10 [&_svg]:size-5",
|
|
96
96
|
textClasses[context],
|
|
97
97
|
);
|
|
98
98
|
}
|
|
@@ -325,7 +325,7 @@ export function AlertActions({
|
|
|
325
325
|
<div
|
|
326
326
|
data-slot="alert-actions"
|
|
327
327
|
className={cn(
|
|
328
|
-
"col-start-
|
|
328
|
+
"col-start-3 row-start-1 ms-3 flex self-center items-center gap-2",
|
|
329
329
|
className,
|
|
330
330
|
)}
|
|
331
331
|
{...props}
|