@softize/opus 12.10.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 +56 -0
- package/bin/lib/check.mjs +1098 -310
- package/bin/lib/copy.mjs +12 -5
- 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 +180 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- 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/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/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/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 +17 -8
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +163 -40
- 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/list.tsx +1097 -783
- 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 +357 -83
- package/src/ui/components/patterns/trigger.tsx +37 -30
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert.tsx +298 -110
- 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/dialog.tsx +196 -39
- 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 +40 -24
- package/src/ui/components/primitives/empty.tsx +3 -3
- package/src/ui/components/primitives/item.tsx +135 -79
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/sonner.tsx +187 -8
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/docs/DocBrowser.tsx +104 -25
- 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 +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 +54 -28
- 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 +21 -22
- package/src/ui/docs/content/breadcrumb.md +13 -8
- package/src/ui/docs/content/button.md +93 -15
- package/src/ui/docs/content/calendar.md +5 -5
- package/src/ui/docs/content/card.md +6 -6
- 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 +44 -0
- package/src/ui/docs/content/copyable.md +4 -3
- package/src/ui/docs/content/customization.md +7 -7
- 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 +8 -5
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +19 -18
- package/src/ui/docs/content/dock.md +3 -3
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +32 -16
- 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 +64 -24
- 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 +36 -17
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +93 -10
- 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/semantic-context.md +63 -0
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +325 -56
- package/src/ui/docs/content/skeleton.md +5 -4
- 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 +16 -6
- 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 +31 -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/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +580 -229
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +278 -286
- package/src/ui/react.tsx +377 -111
- package/src/ui/theme.css +116 -0
- package/src/ui/components/primitives/alert-dialog.tsx +0 -190
- 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 -78
- package/src/ui/docs/content/toggle-group.md +0 -81
|
@@ -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
|
)
|