@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/lib/check.mjs +1103 -310
  3. package/bin/lib/copy.mjs +74 -6
  4. package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +65 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
  7. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
  8. package/docs/code-style.md +6 -2
  9. package/package.json +1 -1
  10. package/registry/instructions/opus.md +5 -0
  11. package/registry/skills/build-opus-ui/SKILL.md +27 -16
  12. package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
  13. package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
  14. package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
  15. package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
  16. package/registry/templates/app/src/App.tsx +1 -1
  17. package/src/core/dictionary.ts +52 -14
  18. package/src/core/index.ts +10 -0
  19. package/src/core/ui-context.ts +29 -0
  20. package/src/schema/drivers/zod.ts +34 -19
  21. package/src/ui/components/patterns/action-form-card.tsx +18 -12
  22. package/src/ui/components/patterns/confirm.tsx +26 -3
  23. package/src/ui/components/patterns/content-header.tsx +335 -61
  24. package/src/ui/components/patterns/data-state.tsx +23 -10
  25. package/src/ui/components/patterns/form.tsx +1 -1
  26. package/src/ui/components/patterns/list.tsx +1096 -777
  27. package/src/ui/components/patterns/page-state.tsx +115 -0
  28. package/src/ui/components/patterns/page.tsx +231 -41
  29. package/src/ui/components/patterns/sidebar.tsx +354 -80
  30. package/src/ui/components/patterns/trigger.tsx +13 -9
  31. package/src/ui/components/patterns/view.tsx +7 -11
  32. package/src/ui/components/primitives/alert-dialog.tsx +7 -5
  33. package/src/ui/components/primitives/alert.tsx +298 -80
  34. package/src/ui/components/primitives/ask.tsx +2 -1
  35. package/src/ui/components/primitives/badge.tsx +91 -30
  36. package/src/ui/components/primitives/button.tsx +99 -60
  37. package/src/ui/components/primitives/calendar.tsx +39 -39
  38. package/src/ui/components/primitives/card.tsx +96 -23
  39. package/src/ui/components/primitives/detail.tsx +2 -2
  40. package/src/ui/components/primitives/dictionary-value.tsx +9 -14
  41. package/src/ui/components/primitives/dot.tsx +74 -21
  42. package/src/ui/components/primitives/drawer.tsx +33 -20
  43. package/src/ui/components/primitives/field.tsx +4 -4
  44. package/src/ui/components/primitives/item.tsx +137 -81
  45. package/src/ui/components/primitives/menu.tsx +11 -3
  46. package/src/ui/components/primitives/metric-card.tsx +133 -0
  47. package/src/ui/components/primitives/table.tsx +2 -2
  48. package/src/ui/components/primitives/tooltip.tsx +1 -1
  49. package/src/ui/docs/DocBrowser.tsx +3 -3
  50. package/src/ui/docs/changelog.tsx +1 -1
  51. package/src/ui/docs/content/action-form-card.md +1 -1
  52. package/src/ui/docs/content/alert-dialog.md +8 -8
  53. package/src/ui/docs/content/alert.md +54 -23
  54. package/src/ui/docs/content/badge.md +18 -19
  55. package/src/ui/docs/content/button.md +12 -9
  56. package/src/ui/docs/content/card.md +5 -5
  57. package/src/ui/docs/content/content.md +44 -0
  58. package/src/ui/docs/content/customization.md +2 -2
  59. package/src/ui/docs/content/detail.md +5 -2
  60. package/src/ui/docs/content/dialog.md +2 -2
  61. package/src/ui/docs/content/dictionary-value.md +11 -10
  62. package/src/ui/docs/content/dot.md +7 -7
  63. package/src/ui/docs/content/drawer.md +6 -3
  64. package/src/ui/docs/content/field.md +1 -1
  65. package/src/ui/docs/content/input-group.md +3 -2
  66. package/src/ui/docs/content/item.md +47 -21
  67. package/src/ui/docs/content/menu.md +5 -4
  68. package/src/ui/docs/content/metric-card.md +41 -0
  69. package/src/ui/docs/content/page-state.md +45 -0
  70. package/src/ui/docs/content/page.md +48 -10
  71. package/src/ui/docs/content/semantic-context.md +63 -0
  72. package/src/ui/docs/content/sidebar.md +4 -4
  73. package/src/ui/docs/content/skeleton.md +2 -2
  74. package/src/ui/docs/content/table.md +3 -3
  75. package/src/ui/docs/content/tokens.md +28 -0
  76. package/src/ui/docs/content/tooltip.md +15 -1
  77. package/src/ui/docs/doc-client.tsx +2 -2
  78. package/src/ui/docs/registry.tsx +596 -228
  79. package/src/ui/lib/semantic-context.ts +30 -0
  80. package/src/ui/meta.ts +292 -270
  81. package/src/ui/react.tsx +378 -111
  82. 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?', variant: 'destructive' })) …
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
- /** `destructive` pinta o botão de confirmar — o que apaga tem que parecer o que apaga. */
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
- variant={variant === 'destructive' ? 'destructive' : 'default'}
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 type { ReactNode } from 'react'
2
- import { cn } from '../../lib/cn.ts'
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 = 'page' | 'section'
14
+ export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
15
+ export type ContentHeaderVariant = "page" | "section";
6
16
 
7
- export interface ContentHeaderProps {
8
- title: ReactNode
9
- /** Conteúdo complementar exibido ao lado do título, como uma contagem. */
10
- meta?: ReactNode
11
- description?: ReactNode
12
- actions?: ReactNode
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: destaque principal ou cabeçalho interno de seção. */
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 ContentHeaderSlots {
21
- root?: string
22
- heading: string
23
- actions: string
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 ContentHeaderFrameProps extends ContentHeaderProps {
27
- slots: ContentHeaderSlots
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
- const headingTags = {
31
- 1: 'h1',
32
- 2: 'h2',
33
- 3: 'h3',
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 = 'section',
72
+ variant = "section",
73
+ children,
47
74
  className,
48
- slots,
49
- }: ContentHeaderFrameProps): React.ReactElement {
50
- const Heading = headingTags[level]
51
- const page = variant === 'page'
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
- <div
55
- {...(slots.root === undefined ? {} : { 'data-slot': slots.root })}
56
- className={cn(actions !== undefined && 'flex items-end justify-between gap-4', className)}
57
- >
58
- <div data-slot={slots.heading} className="min-w-0">
59
- <Heading
60
- className={cn(
61
- 'flex items-baseline gap-2 font-semibold',
62
- page ? 'text-2xl tracking-tight' : 'text-sm',
63
- )}
64
- >
65
- {title}
66
- {meta}
67
- </Heading>
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
- <p className={cn('truncate text-muted-foreground', page ? 'mt-1 text-sm' : 'mt-0.5 text-xs')}>{description}</p>
203
+ <ContentDescription>{description}</ContentDescription>
70
204
  )}
71
- </div>
72
- {actions !== undefined && (
73
- <div data-slot={slots.actions} className={cn('flex shrink-0 items-center', page ? 'gap-4' : 'gap-2')}>
74
- {actions}
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
- </div>
78
- )
295
+ {...props}
296
+ />
297
+ );
79
298
  }
80
299
 
81
- export function ContentHeader(props: ContentHeaderProps): React.ReactElement {
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
- <ContentHeaderFrame
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 className="flex flex-col items-center gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 text-center text-sm text-destructive">
82
- <span>{errorText ?? 'Não foi possível carregar.'}</span>
83
- {onRetry !== undefined && (
84
- <Button variant="outline" size="sm" onClick={() => void onRetry()}>
85
- {retryText}
86
- </Button>
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 className="py-10 text-center text-muted-foreground">
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 className="rounded-lg border border-border py-10 text-center text-sm text-muted-foreground/60">
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 className="max-w-sm">{help}</TooltipContent>
200
+ <TooltipContent>{help}</TooltipContent>
201
201
  </Tooltip>
202
202
  </TooltipProvider>
203
203
  )