@softize/opus 12.9.0 → 12.11.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 +44 -0
- package/bin/lib/check.mjs +1103 -310
- package/bin/lib/copy.mjs +74 -6
- package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
- package/docs/adr/0004-page-content-state-is-composed.md +65 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- package/docs/code-style.md +6 -2
- package/package.json +1 -1
- package/registry/instructions/opus.md +5 -0
- package/registry/skills/build-opus-ui/SKILL.md +27 -16
- package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
- package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
- package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
- package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
- package/registry/templates/app/src/App.tsx +1 -1
- package/src/core/dictionary.ts +52 -14
- package/src/core/index.ts +10 -0
- package/src/core/ui-context.ts +29 -0
- package/src/schema/drivers/zod.ts +34 -19
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +26 -3
- package/src/ui/components/patterns/content-header.tsx +335 -61
- package/src/ui/components/patterns/data-state.tsx +23 -10
- package/src/ui/components/patterns/form.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +1096 -777
- package/src/ui/components/patterns/page-state.tsx +115 -0
- package/src/ui/components/patterns/page.tsx +231 -41
- package/src/ui/components/patterns/sidebar.tsx +354 -80
- package/src/ui/components/patterns/trigger.tsx +13 -9
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert-dialog.tsx +7 -5
- package/src/ui/components/primitives/alert.tsx +298 -80
- package/src/ui/components/primitives/ask.tsx +2 -1
- package/src/ui/components/primitives/badge.tsx +91 -30
- package/src/ui/components/primitives/button.tsx +99 -60
- package/src/ui/components/primitives/calendar.tsx +39 -39
- package/src/ui/components/primitives/card.tsx +96 -23
- package/src/ui/components/primitives/detail.tsx +2 -2
- package/src/ui/components/primitives/dictionary-value.tsx +9 -14
- package/src/ui/components/primitives/dot.tsx +74 -21
- package/src/ui/components/primitives/drawer.tsx +33 -20
- package/src/ui/components/primitives/field.tsx +4 -4
- package/src/ui/components/primitives/item.tsx +137 -81
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/components/primitives/tooltip.tsx +1 -1
- package/src/ui/docs/DocBrowser.tsx +3 -3
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form-card.md +1 -1
- package/src/ui/docs/content/alert-dialog.md +8 -8
- package/src/ui/docs/content/alert.md +54 -23
- package/src/ui/docs/content/badge.md +18 -19
- package/src/ui/docs/content/button.md +12 -9
- package/src/ui/docs/content/card.md +5 -5
- package/src/ui/docs/content/content.md +44 -0
- package/src/ui/docs/content/customization.md +2 -2
- package/src/ui/docs/content/detail.md +5 -2
- package/src/ui/docs/content/dialog.md +2 -2
- package/src/ui/docs/content/dictionary-value.md +11 -10
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +6 -3
- package/src/ui/docs/content/field.md +1 -1
- package/src/ui/docs/content/input-group.md +3 -2
- package/src/ui/docs/content/item.md +47 -21
- package/src/ui/docs/content/menu.md +5 -4
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/page-state.md +45 -0
- package/src/ui/docs/content/page.md +48 -10
- package/src/ui/docs/content/semantic-context.md +63 -0
- package/src/ui/docs/content/sidebar.md +4 -4
- package/src/ui/docs/content/skeleton.md +2 -2
- package/src/ui/docs/content/table.md +3 -3
- package/src/ui/docs/content/tokens.md +28 -0
- package/src/ui/docs/content/tooltip.md +15 -1
- package/src/ui/docs/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +596 -228
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +292 -270
- package/src/ui/react.tsx +378 -111
- package/src/ui/theme.css +66 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `dialog` — o trio imperativo (alert · confirm · prompt) em UMA linha.
|
|
3
3
|
*
|
|
4
4
|
* await dialog.alert({ title: 'Sessão expirada', description: 'Você será desconectado.' })
|
|
5
|
-
* if (await dialog.confirm({ title: 'Excluir a sessão?',
|
|
5
|
+
* if (await dialog.confirm({ title: 'Excluir a sessão?', context: 'danger' })) …
|
|
6
6
|
* const nome = await dialog.prompt({ title: 'Nome do snapshot' })
|
|
7
7
|
*
|
|
8
8
|
* Mesma forma do `toast` (superfície imperativa + host no shell), com uma diferença: eles
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import * as React from 'react'
|
|
22
|
+
import type { UiContext } from '../../../core/ui-context.ts'
|
|
22
23
|
import {
|
|
23
24
|
AlertDialog,
|
|
24
25
|
AlertDialogAction,
|
|
@@ -50,13 +51,17 @@ export interface AlertOptions extends DialogBase {}
|
|
|
50
51
|
/** `confirm` — pergunta de sim/não. */
|
|
51
52
|
export interface ConfirmOptions extends DialogBase {
|
|
52
53
|
cancel?: string
|
|
53
|
-
/**
|
|
54
|
+
/** Contexto do botão de confirmação. */
|
|
55
|
+
context?: Extract<UiContext, 'primary' | 'danger'>
|
|
56
|
+
/** @deprecated Use `context="danger"`. */
|
|
54
57
|
variant?: 'default' | 'destructive'
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
/** `prompt` — uma string livre. Form de verdade (campos, contrato) → ActionFormDialog. */
|
|
58
61
|
export interface PromptOptions extends DialogBase {
|
|
59
62
|
cancel?: string
|
|
63
|
+
context?: Extract<UiContext, 'primary' | 'danger'>
|
|
64
|
+
/** @deprecated Use `context="danger"`. */
|
|
60
65
|
variant?: 'default' | 'destructive'
|
|
61
66
|
placeholder?: string
|
|
62
67
|
defaultValue?: string
|
|
@@ -79,6 +84,16 @@ function requireHost(fn: string): Error | null {
|
|
|
79
84
|
)
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
function rejectAmbiguousContext(
|
|
88
|
+
fn: string,
|
|
89
|
+
options: ConfirmOptions | PromptOptions,
|
|
90
|
+
): Error | null {
|
|
91
|
+
if (options.context === undefined || options.variant === undefined) return null
|
|
92
|
+
return new Error(
|
|
93
|
+
`${fn} não permite combinar context com uma variante semântica legada.`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
export const dialog = {
|
|
83
98
|
/** Avisa e espera o "OK". `Promise<void>` — só reconhecimento, sem decisão. */
|
|
84
99
|
alert(options: AlertOptions): Promise<void> {
|
|
@@ -90,6 +105,8 @@ export const dialog = {
|
|
|
90
105
|
},
|
|
91
106
|
/** Pergunta sim/não. `Promise<boolean>` — fechar/ESC resolve `false`. */
|
|
92
107
|
confirm(options: ConfirmOptions): Promise<boolean> {
|
|
108
|
+
const ambiguous = rejectAmbiguousContext('dialog.confirm()', options)
|
|
109
|
+
if (ambiguous !== null) return Promise.reject(ambiguous)
|
|
93
110
|
const err = requireHost('dialog.confirm()')
|
|
94
111
|
if (err !== null) return Promise.reject(err)
|
|
95
112
|
return new Promise<boolean>((resolve) => {
|
|
@@ -98,6 +115,8 @@ export const dialog = {
|
|
|
98
115
|
},
|
|
99
116
|
/** Pede uma string. `Promise<string | null>` — fechar/ESC/Cancelar resolve `null`. */
|
|
100
117
|
prompt(options: PromptOptions): Promise<string | null> {
|
|
118
|
+
const ambiguous = rejectAmbiguousContext('dialog.prompt()', options)
|
|
119
|
+
if (ambiguous !== null) return Promise.reject(ambiguous)
|
|
101
120
|
const err = requireHost('dialog.prompt()')
|
|
102
121
|
if (err !== null) return Promise.reject(err)
|
|
103
122
|
return new Promise<string | null>((resolve) => {
|
|
@@ -154,6 +173,9 @@ export function DialogHost(): React.ReactElement {
|
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
const variant = current !== undefined && current.kind !== 'alert' ? current.variant : undefined
|
|
176
|
+
const context = current !== undefined && current.kind !== 'alert'
|
|
177
|
+
? current.context ?? (variant === 'destructive' ? 'danger' : 'primary')
|
|
178
|
+
: 'primary'
|
|
157
179
|
const defaultAction = current?.kind === 'alert' ? 'OK' : 'Confirmar'
|
|
158
180
|
|
|
159
181
|
return (
|
|
@@ -210,7 +232,8 @@ export function DialogHost(): React.ReactElement {
|
|
|
210
232
|
</AlertDialogCancel>
|
|
211
233
|
)}
|
|
212
234
|
<AlertDialogAction
|
|
213
|
-
|
|
235
|
+
context={context}
|
|
236
|
+
variant="solid"
|
|
214
237
|
onClick={() => drop(current.id, (p) => accept(p, text))}
|
|
215
238
|
>
|
|
216
239
|
{current.action ?? defaultAction}
|
|
@@ -1,88 +1,362 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
Fragment,
|
|
4
|
+
createContext,
|
|
5
|
+
isValidElement,
|
|
6
|
+
useContext,
|
|
7
|
+
useId,
|
|
8
|
+
type HTMLAttributes,
|
|
9
|
+
type ReactElement,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { cn } from "../../lib/cn.ts";
|
|
3
13
|
|
|
4
|
-
export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6
|
|
5
|
-
export type ContentHeaderVariant =
|
|
14
|
+
export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
15
|
+
export type ContentHeaderVariant = "page" | "section";
|
|
6
16
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
interface ContentContextValue {
|
|
18
|
+
level: ContentHeaderLevel;
|
|
19
|
+
titleId: string;
|
|
20
|
+
variant: ContentHeaderVariant;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ContentContext = createContext<ContentContextValue | null>(null);
|
|
24
|
+
const ContentHeaderContext = createContext<ContentContextValue | null>(null);
|
|
25
|
+
|
|
26
|
+
function requiredContext<T>(
|
|
27
|
+
value: T | null,
|
|
28
|
+
component: string,
|
|
29
|
+
parent: string,
|
|
30
|
+
): T {
|
|
31
|
+
if (value === null)
|
|
32
|
+
throw new Error(`${component} deve estar dentro de ${parent}.`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ContentBaseProps extends Omit<
|
|
37
|
+
HTMLAttributes<HTMLElement>,
|
|
38
|
+
"children" | "title"
|
|
39
|
+
> {
|
|
13
40
|
/** Nível semântico do heading, independente de seu destaque visual. */
|
|
14
|
-
level?: ContentHeaderLevel
|
|
15
|
-
/** Hierarquia visual
|
|
16
|
-
variant?: ContentHeaderVariant
|
|
17
|
-
className?: string
|
|
41
|
+
level?: ContentHeaderLevel;
|
|
42
|
+
/** Hierarquia visual. `page` permanece somente para compatibilidade durante a versão 12. */
|
|
43
|
+
variant?: ContentHeaderVariant;
|
|
18
44
|
}
|
|
19
45
|
|
|
20
|
-
interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
interface ContentShorthandProps extends ContentBaseProps {
|
|
47
|
+
title: ReactNode;
|
|
48
|
+
/** Conteúdo complementar exibido ao lado do título, como uma contagem. */
|
|
49
|
+
meta?: ReactNode;
|
|
50
|
+
description?: ReactNode;
|
|
51
|
+
actions?: ReactNode;
|
|
52
|
+
children: ReactNode;
|
|
24
53
|
}
|
|
25
54
|
|
|
26
|
-
interface
|
|
27
|
-
|
|
55
|
+
interface ContentComposedProps extends ContentBaseProps {
|
|
56
|
+
title?: never;
|
|
57
|
+
meta?: never;
|
|
58
|
+
description?: never;
|
|
59
|
+
actions?: never;
|
|
60
|
+
children: ReactNode;
|
|
28
61
|
}
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
4: 'h4',
|
|
35
|
-
5: 'h5',
|
|
36
|
-
6: 'h6',
|
|
37
|
-
} as const
|
|
38
|
-
|
|
39
|
-
/** Layout compartilhado com Page; mantém os slots históricos sem expô-los na API pública. */
|
|
40
|
-
export function ContentHeaderFrame({
|
|
63
|
+
export type ContentProps = ContentShorthandProps | ContentComposedProps;
|
|
64
|
+
|
|
65
|
+
/** Região de conteúdo nomeada. A forma curta e a explícita produzem a mesma anatomia. */
|
|
66
|
+
export function Content({
|
|
41
67
|
title,
|
|
42
68
|
meta,
|
|
43
69
|
description,
|
|
44
70
|
actions,
|
|
45
71
|
level = 2,
|
|
46
|
-
variant =
|
|
72
|
+
variant = "section",
|
|
73
|
+
children,
|
|
47
74
|
className,
|
|
48
|
-
|
|
49
|
-
}:
|
|
50
|
-
const
|
|
51
|
-
const
|
|
75
|
+
...props
|
|
76
|
+
}: ContentProps): ReactElement {
|
|
77
|
+
const titleId = useId();
|
|
78
|
+
const context = { level, titleId, variant };
|
|
79
|
+
const shorthand = title !== undefined;
|
|
80
|
+
const nodes = Children.toArray(children);
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
shorthand &&
|
|
84
|
+
nodes.some((node) => isValidElement(node) && node.type === ContentHeader)
|
|
85
|
+
) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"Content não permite misturar propriedades de shorthand com ContentHeader explícito.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!shorthand) {
|
|
91
|
+
const headers = nodes.filter(
|
|
92
|
+
(node) => isValidElement(node) && node.type === ContentHeader,
|
|
93
|
+
);
|
|
94
|
+
const bodies = nodes.filter(
|
|
95
|
+
(node) => isValidElement(node) && node.type === ContentBody,
|
|
96
|
+
);
|
|
97
|
+
if (
|
|
98
|
+
headers.length !== 1 ||
|
|
99
|
+
bodies.length !== 1 ||
|
|
100
|
+
headers.length + bodies.length !== nodes.length
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"Content explícito exige exatamente um ContentHeader e um ContentBody como filhos diretos.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
52
107
|
|
|
53
108
|
return (
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
109
|
+
<ContentContext.Provider value={context}>
|
|
110
|
+
<section
|
|
111
|
+
data-slot="content"
|
|
112
|
+
aria-labelledby={
|
|
113
|
+
props["aria-label"] === undefined ? titleId : undefined
|
|
114
|
+
}
|
|
115
|
+
className={cn("space-y-3", className)}
|
|
116
|
+
{...props}
|
|
117
|
+
>
|
|
118
|
+
{shorthand ? (
|
|
119
|
+
<>
|
|
120
|
+
<ContentHeader>
|
|
121
|
+
<ContentTitle>{title}</ContentTitle>
|
|
122
|
+
{meta !== undefined && <ContentMeta>{meta}</ContentMeta>}
|
|
123
|
+
{description !== undefined && (
|
|
124
|
+
<ContentDescription>{description}</ContentDescription>
|
|
125
|
+
)}
|
|
126
|
+
{actions !== undefined && (
|
|
127
|
+
<ContentActions>{actions}</ContentActions>
|
|
128
|
+
)}
|
|
129
|
+
</ContentHeader>
|
|
130
|
+
<ContentBody>{children}</ContentBody>
|
|
131
|
+
</>
|
|
132
|
+
) : (
|
|
133
|
+
children
|
|
134
|
+
)}
|
|
135
|
+
</section>
|
|
136
|
+
</ContentContext.Provider>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface ContentHeaderShorthandProps extends Omit<
|
|
141
|
+
HTMLAttributes<HTMLDivElement>,
|
|
142
|
+
"children" | "title"
|
|
143
|
+
> {
|
|
144
|
+
/** @deprecated Prefira os slots explícitos ou o shorthand de Content. */
|
|
145
|
+
title: ReactNode;
|
|
146
|
+
meta?: ReactNode;
|
|
147
|
+
description?: ReactNode;
|
|
148
|
+
actions?: ReactNode;
|
|
149
|
+
/** @deprecated Declare o nível em Content. Compatibilidade da série 12. */
|
|
150
|
+
level?: ContentHeaderLevel;
|
|
151
|
+
/** @deprecated Declare a hierarquia visual em Content. Compatibilidade da série 12. */
|
|
152
|
+
variant?: ContentHeaderVariant;
|
|
153
|
+
children?: never;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface ContentHeaderComposedProps extends Omit<
|
|
157
|
+
HTMLAttributes<HTMLDivElement>,
|
|
158
|
+
"title"
|
|
159
|
+
> {
|
|
160
|
+
/** @deprecated Declare o nível em Content. Compatibilidade da série 12. */
|
|
161
|
+
level?: ContentHeaderLevel;
|
|
162
|
+
/** @deprecated Declare a hierarquia visual em Content. Compatibilidade da série 12. */
|
|
163
|
+
variant?: ContentHeaderVariant;
|
|
164
|
+
title?: never;
|
|
165
|
+
meta?: never;
|
|
166
|
+
description?: never;
|
|
167
|
+
actions?: never;
|
|
168
|
+
children: ReactNode;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export type ContentHeaderProps =
|
|
172
|
+
ContentHeaderShorthandProps | ContentHeaderComposedProps;
|
|
173
|
+
|
|
174
|
+
/** Header estrutural de Content; não é uma região autônoma. */
|
|
175
|
+
export function ContentHeader({
|
|
176
|
+
title,
|
|
177
|
+
meta,
|
|
178
|
+
description,
|
|
179
|
+
actions,
|
|
180
|
+
level,
|
|
181
|
+
variant,
|
|
182
|
+
children,
|
|
183
|
+
className,
|
|
184
|
+
...props
|
|
185
|
+
}: ContentHeaderProps): ReactElement {
|
|
186
|
+
const parent = useContext(ContentContext);
|
|
187
|
+
const standaloneTitleId = useId();
|
|
188
|
+
const shorthand = title !== undefined;
|
|
189
|
+
if (parent === null && !shorthand) {
|
|
190
|
+
throw new Error("ContentHeader deve estar dentro de Content.");
|
|
191
|
+
}
|
|
192
|
+
const content: ContentContextValue = {
|
|
193
|
+
level: level ?? parent?.level ?? 2,
|
|
194
|
+
titleId: parent?.titleId ?? standaloneTitleId,
|
|
195
|
+
variant: variant ?? parent?.variant ?? "section",
|
|
196
|
+
};
|
|
197
|
+
const nodes = Children.toArray(
|
|
198
|
+
shorthand ? (
|
|
199
|
+
<>
|
|
200
|
+
<ContentTitle>{title}</ContentTitle>
|
|
201
|
+
{meta !== undefined && <ContentMeta>{meta}</ContentMeta>}
|
|
68
202
|
{description !== undefined && (
|
|
69
|
-
<
|
|
203
|
+
<ContentDescription>{description}</ContentDescription>
|
|
70
204
|
)}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
205
|
+
{actions !== undefined && <ContentActions>{actions}</ContentActions>}
|
|
206
|
+
</>
|
|
207
|
+
) : (
|
|
208
|
+
children
|
|
209
|
+
),
|
|
210
|
+
).flatMap((node) =>
|
|
211
|
+
isValidElement(node) && node.type === Fragment
|
|
212
|
+
? Children.toArray((node.props as { children?: ReactNode }).children)
|
|
213
|
+
: [node],
|
|
214
|
+
);
|
|
215
|
+
const titles = nodes.filter(
|
|
216
|
+
(node) => isValidElement(node) && node.type === ContentTitle,
|
|
217
|
+
);
|
|
218
|
+
const metas = nodes.filter(
|
|
219
|
+
(node) => isValidElement(node) && node.type === ContentMeta,
|
|
220
|
+
);
|
|
221
|
+
const descriptions = nodes.filter(
|
|
222
|
+
(node) => isValidElement(node) && node.type === ContentDescription,
|
|
223
|
+
);
|
|
224
|
+
const actionSlots = nodes.filter(
|
|
225
|
+
(node) => isValidElement(node) && node.type === ContentActions,
|
|
226
|
+
);
|
|
227
|
+
const recognized =
|
|
228
|
+
titles.length + metas.length + descriptions.length + actionSlots.length;
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
titles.length !== 1 ||
|
|
232
|
+
metas.length > 1 ||
|
|
233
|
+
descriptions.length > 1 ||
|
|
234
|
+
actionSlots.length > 1 ||
|
|
235
|
+
recognized !== nodes.length
|
|
236
|
+
) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"ContentHeader exige um ContentTitle e aceita no máximo um ContentDescription, ContentMeta e ContentActions como filhos diretos.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<ContentHeaderContext.Provider value={content}>
|
|
244
|
+
<div
|
|
245
|
+
data-slot="content-header"
|
|
246
|
+
className={cn(
|
|
247
|
+
actionSlots.length > 0 &&
|
|
248
|
+
"flex flex-col items-start gap-4 sm:flex-row sm:items-end sm:justify-between",
|
|
249
|
+
className,
|
|
250
|
+
)}
|
|
251
|
+
{...props}
|
|
252
|
+
>
|
|
253
|
+
<div data-slot="content-header-heading" className="min-w-0">
|
|
254
|
+
<div className="flex items-baseline gap-2">
|
|
255
|
+
{titles}
|
|
256
|
+
{metas}
|
|
257
|
+
</div>
|
|
258
|
+
{descriptions}
|
|
75
259
|
</div>
|
|
260
|
+
{actionSlots}
|
|
261
|
+
</div>
|
|
262
|
+
</ContentHeaderContext.Provider>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const headingTags = {
|
|
267
|
+
1: "h1",
|
|
268
|
+
2: "h2",
|
|
269
|
+
3: "h3",
|
|
270
|
+
4: "h4",
|
|
271
|
+
5: "h5",
|
|
272
|
+
6: "h6",
|
|
273
|
+
} as const;
|
|
274
|
+
|
|
275
|
+
export function ContentTitle({
|
|
276
|
+
className,
|
|
277
|
+
id,
|
|
278
|
+
...props
|
|
279
|
+
}: HTMLAttributes<HTMLHeadingElement>): ReactElement {
|
|
280
|
+
const context = requiredContext(
|
|
281
|
+
useContext(ContentHeaderContext),
|
|
282
|
+
"ContentTitle",
|
|
283
|
+
"ContentHeader",
|
|
284
|
+
);
|
|
285
|
+
const Heading = headingTags[context.level];
|
|
286
|
+
return (
|
|
287
|
+
<Heading
|
|
288
|
+
id={id ?? context.titleId}
|
|
289
|
+
data-slot="content-title"
|
|
290
|
+
className={cn(
|
|
291
|
+
"font-semibold",
|
|
292
|
+
context.variant === "page" ? "text-2xl tracking-tight" : "text-sm",
|
|
293
|
+
className,
|
|
76
294
|
)}
|
|
77
|
-
|
|
78
|
-
|
|
295
|
+
{...props}
|
|
296
|
+
/>
|
|
297
|
+
);
|
|
79
298
|
}
|
|
80
299
|
|
|
81
|
-
export function
|
|
300
|
+
export function ContentDescription({
|
|
301
|
+
className,
|
|
302
|
+
...props
|
|
303
|
+
}: HTMLAttributes<HTMLParagraphElement>): ReactElement {
|
|
304
|
+
const context = requiredContext(
|
|
305
|
+
useContext(ContentHeaderContext),
|
|
306
|
+
"ContentDescription",
|
|
307
|
+
"ContentHeader",
|
|
308
|
+
);
|
|
82
309
|
return (
|
|
83
|
-
<
|
|
310
|
+
<p
|
|
311
|
+
data-slot="content-description"
|
|
312
|
+
className={cn(
|
|
313
|
+
"truncate text-muted-foreground",
|
|
314
|
+
context.variant === "page" ? "mt-1 text-sm" : "mt-0.5 text-xs",
|
|
315
|
+
className,
|
|
316
|
+
)}
|
|
84
317
|
{...props}
|
|
85
|
-
slots={{ root: 'content-header', heading: 'content-header-heading', actions: 'content-header-actions' }}
|
|
86
318
|
/>
|
|
87
|
-
)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function ContentMeta({
|
|
323
|
+
className,
|
|
324
|
+
...props
|
|
325
|
+
}: HTMLAttributes<HTMLSpanElement>): ReactElement {
|
|
326
|
+
requiredContext(
|
|
327
|
+
useContext(ContentHeaderContext),
|
|
328
|
+
"ContentMeta",
|
|
329
|
+
"ContentHeader",
|
|
330
|
+
);
|
|
331
|
+
return <span data-slot="content-meta" className={className} {...props} />;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function ContentActions({
|
|
335
|
+
className,
|
|
336
|
+
...props
|
|
337
|
+
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
338
|
+
const context = requiredContext(
|
|
339
|
+
useContext(ContentHeaderContext),
|
|
340
|
+
"ContentActions",
|
|
341
|
+
"ContentHeader",
|
|
342
|
+
);
|
|
343
|
+
return (
|
|
344
|
+
<div
|
|
345
|
+
data-slot="content-actions"
|
|
346
|
+
className={cn(
|
|
347
|
+
"flex w-full shrink-0 items-center sm:w-auto",
|
|
348
|
+
context.variant === "page" ? "gap-4" : "gap-2",
|
|
349
|
+
className,
|
|
350
|
+
)}
|
|
351
|
+
{...props}
|
|
352
|
+
/>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function ContentBody({
|
|
357
|
+
className,
|
|
358
|
+
...props
|
|
359
|
+
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
360
|
+
requiredContext(useContext(ContentContext), "ContentBody", "Content");
|
|
361
|
+
return <div data-slot="content-body" className={className} {...props} />;
|
|
88
362
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as React from 'react'
|
|
11
11
|
import { cn } from '../../lib/cn.ts'
|
|
12
|
+
import { Alert } from '../primitives/alert.tsx'
|
|
12
13
|
import { Button } from '../primitives/button.tsx'
|
|
13
14
|
import { Spinner } from '../primitives/spinner.tsx'
|
|
14
15
|
|
|
@@ -50,8 +51,9 @@ export function DataState({
|
|
|
50
51
|
|
|
51
52
|
// Modo TABELA: o estado vira UMA linha de largura cheia (<td colSpan>), pra caber no <tbody>.
|
|
52
53
|
if (colSpan !== undefined) {
|
|
54
|
+
const status = error ? 'error' : loading ? 'loading' : 'empty'
|
|
53
55
|
return (
|
|
54
|
-
<tr>
|
|
56
|
+
<tr data-slot="data-state" data-status={status}>
|
|
55
57
|
<td
|
|
56
58
|
colSpan={colSpan}
|
|
57
59
|
className={cn('px-4 py-10 text-center text-sm', error ? 'text-destructive' : 'text-muted-foreground/60')}
|
|
@@ -78,25 +80,36 @@ export function DataState({
|
|
|
78
80
|
// Modo BLOCO (default).
|
|
79
81
|
if (error) {
|
|
80
82
|
return (
|
|
81
|
-
<div
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
<div data-slot="data-state" data-status="error">
|
|
84
|
+
<Alert context="danger" description={errorText ?? 'Não foi possível carregar.'}>
|
|
85
|
+
{onRetry !== undefined && (
|
|
86
|
+
<Button variant="outline" size="sm" onClick={() => void onRetry()}>
|
|
87
|
+
{retryText}
|
|
88
|
+
</Button>
|
|
89
|
+
)}
|
|
90
|
+
</Alert>
|
|
88
91
|
</div>
|
|
89
92
|
)
|
|
90
93
|
}
|
|
91
94
|
if (loading) {
|
|
92
95
|
return (
|
|
93
|
-
<div
|
|
96
|
+
<div
|
|
97
|
+
data-slot="data-state"
|
|
98
|
+
data-status="loading"
|
|
99
|
+
role="status"
|
|
100
|
+
aria-live="polite"
|
|
101
|
+
className="py-10 text-center text-muted-foreground"
|
|
102
|
+
>
|
|
94
103
|
<Spinner className="mx-auto" />
|
|
95
104
|
</div>
|
|
96
105
|
)
|
|
97
106
|
}
|
|
98
107
|
return (
|
|
99
|
-
<p
|
|
108
|
+
<p
|
|
109
|
+
data-slot="data-state"
|
|
110
|
+
data-status="empty"
|
|
111
|
+
className="rounded-lg border border-border py-10 text-center text-sm text-muted-foreground/60"
|
|
112
|
+
>
|
|
100
113
|
{emptyText}
|
|
101
114
|
</p>
|
|
102
115
|
)
|
|
@@ -197,7 +197,7 @@ function LabelHelp({ help }: { help: string | undefined }): React.ReactElement |
|
|
|
197
197
|
</TooltipTrigger>
|
|
198
198
|
{/* max-w-sm: frase de help típica (~50 caracteres) cabe numa linha; o
|
|
199
199
|
text-balance do primitivo só divide o que realmente transborda. */}
|
|
200
|
-
<TooltipContent
|
|
200
|
+
<TooltipContent>{help}</TooltipContent>
|
|
201
201
|
</Tooltip>
|
|
202
202
|
</TooltipProvider>
|
|
203
203
|
)
|