@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.
Files changed (154) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/bin/lib/check.mjs +1098 -310
  3. package/bin/lib/copy.mjs +12 -5
  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 +180 -0
  7. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
  8. package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
  9. package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
  10. package/docs/radius-scale.md +1 -1
  11. package/package.json +1 -1
  12. package/registry/instructions/opus.md +5 -0
  13. package/registry/skills/build-opus-ui/SKILL.md +27 -16
  14. package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
  15. package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
  16. package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
  17. package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
  18. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
  19. package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
  20. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
  21. package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
  22. package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
  23. package/registry/templates/app/src/App.tsx +1 -1
  24. package/src/core/dictionary.ts +52 -14
  25. package/src/core/index.ts +10 -0
  26. package/src/core/ui-context.ts +29 -0
  27. package/src/schema/drivers/zod.ts +17 -8
  28. package/src/ui/components/patterns/action-form-card.tsx +18 -12
  29. package/src/ui/components/patterns/confirm.tsx +163 -40
  30. package/src/ui/components/patterns/content-header.tsx +335 -61
  31. package/src/ui/components/patterns/data-state.tsx +23 -10
  32. package/src/ui/components/patterns/list.tsx +1097 -783
  33. package/src/ui/components/patterns/page-state.tsx +115 -0
  34. package/src/ui/components/patterns/page.tsx +231 -41
  35. package/src/ui/components/patterns/sidebar.tsx +357 -83
  36. package/src/ui/components/patterns/trigger.tsx +37 -30
  37. package/src/ui/components/patterns/view.tsx +7 -11
  38. package/src/ui/components/primitives/alert.tsx +298 -110
  39. package/src/ui/components/primitives/ask.tsx +2 -1
  40. package/src/ui/components/primitives/badge.tsx +91 -30
  41. package/src/ui/components/primitives/button.tsx +99 -60
  42. package/src/ui/components/primitives/calendar.tsx +39 -39
  43. package/src/ui/components/primitives/card.tsx +96 -23
  44. package/src/ui/components/primitives/detail.tsx +2 -2
  45. package/src/ui/components/primitives/dialog.tsx +196 -39
  46. package/src/ui/components/primitives/dictionary-value.tsx +9 -14
  47. package/src/ui/components/primitives/dot.tsx +74 -21
  48. package/src/ui/components/primitives/drawer.tsx +40 -24
  49. package/src/ui/components/primitives/empty.tsx +3 -3
  50. package/src/ui/components/primitives/item.tsx +135 -79
  51. package/src/ui/components/primitives/menu.tsx +11 -3
  52. package/src/ui/components/primitives/metric-card.tsx +133 -0
  53. package/src/ui/components/primitives/sonner.tsx +187 -8
  54. package/src/ui/components/primitives/table.tsx +2 -2
  55. package/src/ui/docs/DocBrowser.tsx +104 -25
  56. package/src/ui/docs/changelog.tsx +1 -1
  57. package/src/ui/docs/content/accordion.md +22 -16
  58. package/src/ui/docs/content/action-form-card.md +8 -8
  59. package/src/ui/docs/content/action-form-dialog.md +9 -9
  60. package/src/ui/docs/content/action-form.md +28 -34
  61. package/src/ui/docs/content/action-list-dialog.md +11 -6
  62. package/src/ui/docs/content/action-list.md +64 -39
  63. package/src/ui/docs/content/action-trigger.md +21 -14
  64. package/src/ui/docs/content/action-view.md +8 -8
  65. package/src/ui/docs/content/actions.md +9 -9
  66. package/src/ui/docs/content/ai.md +3 -3
  67. package/src/ui/docs/content/alert.md +54 -28
  68. package/src/ui/docs/content/aspect-ratio.md +4 -4
  69. package/src/ui/docs/content/audit.md +2 -2
  70. package/src/ui/docs/content/auth.md +3 -3
  71. package/src/ui/docs/content/avatar.md +34 -14
  72. package/src/ui/docs/content/badge.md +21 -22
  73. package/src/ui/docs/content/breadcrumb.md +13 -8
  74. package/src/ui/docs/content/button.md +93 -15
  75. package/src/ui/docs/content/calendar.md +5 -5
  76. package/src/ui/docs/content/card.md +6 -6
  77. package/src/ui/docs/content/carousel.md +16 -11
  78. package/src/ui/docs/content/chat.md +3 -3
  79. package/src/ui/docs/content/checkbox.md +7 -7
  80. package/src/ui/docs/content/cli.md +5 -5
  81. package/src/ui/docs/content/collapsible.md +8 -8
  82. package/src/ui/docs/content/command.md +16 -8
  83. package/src/ui/docs/content/composer.md +2 -2
  84. package/src/ui/docs/content/content.md +44 -0
  85. package/src/ui/docs/content/copyable.md +4 -3
  86. package/src/ui/docs/content/customization.md +7 -7
  87. package/src/ui/docs/content/cycle.md +3 -3
  88. package/src/ui/docs/content/data-state.md +11 -12
  89. package/src/ui/docs/content/data.md +26 -33
  90. package/src/ui/docs/content/detail.md +8 -5
  91. package/src/ui/docs/content/dialog.md +339 -31
  92. package/src/ui/docs/content/dictionary-value.md +19 -18
  93. package/src/ui/docs/content/dock.md +3 -3
  94. package/src/ui/docs/content/dot.md +7 -7
  95. package/src/ui/docs/content/drawer.md +32 -16
  96. package/src/ui/docs/content/empty-value.md +2 -2
  97. package/src/ui/docs/content/empty.md +19 -12
  98. package/src/ui/docs/content/events.md +4 -4
  99. package/src/ui/docs/content/field.md +34 -12
  100. package/src/ui/docs/content/getting-started.md +1 -1
  101. package/src/ui/docs/content/icon-picker.md +8 -4
  102. package/src/ui/docs/content/input-otp.md +20 -12
  103. package/src/ui/docs/content/input.md +121 -9
  104. package/src/ui/docs/content/item.md +64 -24
  105. package/src/ui/docs/content/kbd.md +19 -11
  106. package/src/ui/docs/content/label.md +5 -3
  107. package/src/ui/docs/content/log.md +4 -4
  108. package/src/ui/docs/content/markdown.md +7 -6
  109. package/src/ui/docs/content/mcp.md +13 -15
  110. package/src/ui/docs/content/menu.md +36 -17
  111. package/src/ui/docs/content/metric-card.md +41 -0
  112. package/src/ui/docs/content/observability.md +2 -2
  113. package/src/ui/docs/content/page.md +93 -10
  114. package/src/ui/docs/content/pagination.md +22 -17
  115. package/src/ui/docs/content/popover.md +16 -8
  116. package/src/ui/docs/content/progress.md +7 -5
  117. package/src/ui/docs/content/queue.md +5 -5
  118. package/src/ui/docs/content/radio-group.md +20 -12
  119. package/src/ui/docs/content/router.md +11 -6
  120. package/src/ui/docs/content/scheduler.md +4 -5
  121. package/src/ui/docs/content/scroll-area.md +12 -7
  122. package/src/ui/docs/content/select.md +42 -29
  123. package/src/ui/docs/content/semantic-context.md +63 -0
  124. package/src/ui/docs/content/separator.md +5 -5
  125. package/src/ui/docs/content/sidebar.md +325 -56
  126. package/src/ui/docs/content/skeleton.md +5 -4
  127. package/src/ui/docs/content/slider.md +8 -7
  128. package/src/ui/docs/content/spinner.md +8 -8
  129. package/src/ui/docs/content/split.md +8 -5
  130. package/src/ui/docs/content/storage.md +6 -8
  131. package/src/ui/docs/content/switch.md +8 -7
  132. package/src/ui/docs/content/table.md +16 -6
  133. package/src/ui/docs/content/tabs.md +28 -14
  134. package/src/ui/docs/content/testing.md +9 -11
  135. package/src/ui/docs/content/textarea.md +5 -4
  136. package/src/ui/docs/content/toast.md +47 -13
  137. package/src/ui/docs/content/toggle.md +75 -7
  138. package/src/ui/docs/content/tokens.md +31 -3
  139. package/src/ui/docs/content/tooltip.md +19 -11
  140. package/src/ui/docs/content/truncate.md +7 -8
  141. package/src/ui/docs/content/ui.md +10 -9
  142. package/src/ui/docs/content/upgrading.md +7 -8
  143. package/src/ui/docs/doc-client.tsx +2 -2
  144. package/src/ui/docs/registry.tsx +580 -229
  145. package/src/ui/lib/semantic-context.ts +30 -0
  146. package/src/ui/meta.ts +278 -286
  147. package/src/ui/react.tsx +377 -111
  148. package/src/ui/theme.css +116 -0
  149. package/src/ui/components/primitives/alert-dialog.tsx +0 -190
  150. package/src/ui/docs/content/alert-dialog.md +0 -73
  151. package/src/ui/docs/content/button-group.md +0 -71
  152. package/src/ui/docs/content/confirm.md +0 -120
  153. package/src/ui/docs/content/input-group.md +0 -78
  154. package/src/ui/docs/content/toggle-group.md +0 -81
@@ -9,6 +9,7 @@
9
9
  import type { ReactNode } from 'react'
10
10
  import type { ViewAction } from '../../../core/index.ts'
11
11
  import { useViewAction } from '../../drivers/react.tsx'
12
+ import { Alert } from '../primitives/alert.tsx'
12
13
  import { Skeleton } from '../primitives/skeleton.tsx'
13
14
  import { Button } from '../primitives/button.tsx'
14
15
 
@@ -61,17 +62,12 @@ export function ActionView<TInput, TData>({
61
62
  return <div data-action={action.name}>{error(err, refetch)}</div>
62
63
  }
63
64
  return (
64
- <div
65
- data-action={action.name}
66
- className="rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm space-y-3"
67
- >
68
- <div>
69
- <strong className="text-destructive">{err.code}</strong>
70
- <p className="text-destructive">{err.message}</p>
71
- </div>
72
- <Button variant="outline" size="sm" onClick={() => void refetch()}>
73
- Tentar de novo
74
- </Button>
65
+ <div data-action={action.name}>
66
+ <Alert context="danger" title={err.code} description={err.message}>
67
+ <Button variant="outline" size="sm" onClick={() => void refetch()}>
68
+ Tentar de novo
69
+ </Button>
70
+ </Alert>
75
71
  </div>
76
72
  )
77
73
  }
@@ -1,146 +1,334 @@
1
- import * as React from 'react'
2
- import { cva, type VariantProps } from 'class-variance-authority'
3
- import { cn } from '../../lib/cn.ts'
4
-
5
- /**
6
- * Variantes do alert (cva). COM ícone o componente abre uma coluna de realce tonal;
7
- * sem ícone é bloco comum. `destructive`, `success` e `warning` mantêm superfície
8
- * tonalizada + borda (divergência da casa — o shadcn rebaixou destructive pra bg-card,
9
- * nós preservamos o realce).
10
- */
11
- export const alertVariants = cva(
12
- 'relative w-full rounded-lg border px-4 py-3 text-sm has-[>svg]:grid has-[>svg]:grid-cols-[1rem_minmax(0,1fr)] has-[>svg]:items-start has-[>svg]:gap-x-3 has-[>svg]:gap-y-1 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
13
- {
14
- variants: {
15
- variant: {
16
- default: 'bg-card text-card-foreground',
17
- destructive:
18
- 'border-destructive/50 bg-destructive/5 text-destructive *:data-[slot=alert-description]:text-destructive/90',
19
- success:
20
- 'border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 *:data-[slot=alert-description]:text-emerald-700/90 dark:*:data-[slot=alert-description]:text-emerald-400/90',
21
- warning:
22
- 'border-amber-500/40 bg-amber-500/10 text-foreground *:data-[slot=alert-description]:text-muted-foreground',
23
- },
24
- },
25
- defaultVariants: { variant: 'default' },
26
- },
27
- )
28
-
29
- const alertIconVariants = cva(
30
- 'flex size-9 shrink-0 items-center justify-center rounded-md [&_svg]:size-5',
31
- {
32
- variants: {
33
- variant: {
34
- default: 'bg-muted text-muted-foreground',
35
- destructive: 'bg-destructive/10 text-destructive',
36
- success: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
37
- warning: 'bg-amber-500/15 text-amber-700 dark:text-amber-300',
38
- },
39
- },
40
- defaultVariants: { variant: 'default' },
41
- },
42
- )
43
-
44
- /** `title` aqui é o TÍTULO do alert, não o atributo nativo do `<div>` (que é tooltip de
45
- * browser — banido na casa em favor do `Tooltip`). Por isso o Omit. */
1
+ import * as React from "react";
2
+ import type { UiContext } from "../../../core/ui-context.ts";
3
+ import { cn } from "../../lib/cn.ts";
4
+ import {
5
+ semanticOutlineClasses,
6
+ semanticSubtleClasses,
7
+ } from "../../lib/semantic-context.ts";
8
+
9
+ export type AlertContext = Exclude<UiContext, "primary">;
10
+ export type AlertVariant = "subtle" | "outline";
11
+ export type LegacyAlertVariant =
12
+ "default" | "destructive" | "success" | "warning" | "info" | "danger";
13
+
14
+ interface AlertStyleOptions {
15
+ context?: AlertContext | null;
16
+ variant?: AlertVariant | LegacyAlertVariant | null;
17
+ }
18
+
19
+ interface AlertRootContextValue {
20
+ context: AlertContext;
21
+ }
22
+
23
+ const AlertRootContext = React.createContext<AlertRootContextValue | null>(
24
+ null,
25
+ );
26
+ const AlertHeaderContext = React.createContext(false);
27
+
28
+ function requiredContext<T>(
29
+ value: T | null,
30
+ component: string,
31
+ parent: string,
32
+ ): T {
33
+ if (value === null)
34
+ throw new Error(`${component} deve estar dentro de ${parent}.`);
35
+ return value;
36
+ }
37
+
38
+ function requireParent(
39
+ inside: boolean,
40
+ component: string,
41
+ parent: string,
42
+ ): void {
43
+ if (!inside) throw new Error(`${component} deve estar dentro de ${parent}.`);
44
+ }
45
+
46
+ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
47
+ context: AlertContext;
48
+ variant: AlertVariant;
49
+ } {
50
+ const legacy =
51
+ variant === "default" ||
52
+ variant === "destructive" ||
53
+ variant === "success" ||
54
+ variant === "warning" ||
55
+ variant === "info" ||
56
+ variant === "danger";
57
+ if (context != null && legacy) {
58
+ throw new Error(
59
+ "Alert não permite combinar context com uma variante semântica legada.",
60
+ );
61
+ }
62
+ if (variant === "default") return { context: "neutral", variant: "subtle" };
63
+ if (variant === "destructive")
64
+ return { context: "danger", variant: "subtle" };
65
+ if (
66
+ variant === "success" ||
67
+ variant === "warning" ||
68
+ variant === "info" ||
69
+ variant === "danger"
70
+ ) {
71
+ return { context: variant, variant: "subtle" };
72
+ }
73
+ return { context: context ?? "neutral", variant: variant ?? "subtle" };
74
+ }
75
+
76
+ export function alertVariants(options: AlertStyleOptions = {}): string {
77
+ const resolved = resolveAlertStyle(options);
78
+ return cn(
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
+ resolved.variant === "outline"
81
+ ? semanticOutlineClasses[resolved.context]
82
+ : semanticSubtleClasses[resolved.context],
83
+ );
84
+ }
85
+
86
+ function alertMediaClasses(context: AlertContext): string {
87
+ const textClasses: Record<AlertContext, string> = {
88
+ neutral: "text-context-neutral-emphasis",
89
+ info: "text-context-info-emphasis",
90
+ success: "text-context-success-emphasis",
91
+ warning: "text-context-warning-emphasis",
92
+ danger: "text-context-danger-emphasis",
93
+ };
94
+ return cn(
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
+ textClasses[context],
97
+ );
98
+ }
99
+
100
+ const alertHeaderClasses =
101
+ "col-start-2 row-start-1 flex min-w-0 flex-col justify-center gap-1";
102
+
103
+ /** `title` é o título do alerta, não o atributo HTML homônimo. */
46
104
  export interface AlertProps
47
- extends Omit<React.ComponentProps<'div'>, 'title'>,
48
- VariantProps<typeof alertVariants> {
49
- /** Forma CURTA: com `title` (e/ou `description`) o alert se monta sozinho — o caso
50
- * comum é uma linha só. Sem eles, vale a composição (`AlertTitle`/`AlertDescription`),
51
- * que segue existindo pro conteúdo rico (lista, link, botão). */
52
- title?: React.ReactNode
53
- description?: React.ReactNode
54
- /** Ícone à esquerda (a 1ª coluna do grid). Decorativo — quem nomeia é o título. */
55
- icon?: React.ReactNode
105
+ extends Omit<React.ComponentProps<"div">, "title">, AlertStyleOptions {
106
+ /** Forma curta: título e descrição materializam um AlertHeader. */
107
+ title?: React.ReactNode;
108
+ description?: React.ReactNode;
109
+ /** Forma curta: materializa AlertMedia. */
110
+ icon?: React.ReactNode;
56
111
  }
57
112
 
58
113
  export function Alert({
59
114
  className,
115
+ context,
60
116
  variant,
61
117
  title,
62
118
  description,
63
119
  icon,
64
120
  children,
65
- style,
66
121
  ...props
67
122
  }: AlertProps): React.ReactElement {
68
- const short = title !== undefined || description !== undefined
69
- const hasIcon = icon !== undefined && icon !== null
70
- // Texto CRU como filho (`<Alert>Sincronizado.</Alert>`) vira descrição: no layout com
71
- // ícone ele cairia na coluna do ícone, espremido. Assim o atalho de sempre continua
72
- // valendo e cai no slot certo.
73
- const raw = typeof children === 'string' || typeof children === 'number'
74
- const body = raw ? <AlertDescription>{children}</AlertDescription> : children
123
+ const raw = typeof children === "string" || typeof children === "number";
124
+ const shorthand = title !== undefined || description !== undefined || raw;
125
+ const resolved = resolveAlertStyle({ context, variant });
126
+ const nodes = React.Children.toArray(children);
127
+ const hasExplicitSlot = nodes.some(
128
+ (node) =>
129
+ React.isValidElement(node) &&
130
+ (node.type === AlertMedia ||
131
+ node.type === AlertHeader ||
132
+ node.type === AlertActions),
133
+ );
134
+ const legacyComposition = !shorthand && !hasExplicitSlot;
135
+
136
+ if (shorthand && hasExplicitSlot) {
137
+ throw new Error(
138
+ "Alert não permite misturar propriedades de shorthand com slots explícitos.",
139
+ );
140
+ }
141
+
142
+ if (!shorthand && !legacyComposition) {
143
+ if (icon !== undefined && icon !== null) {
144
+ throw new Error(
145
+ "Alert explícito usa AlertMedia; a propriedade icon pertence somente ao shorthand.",
146
+ );
147
+ }
148
+ const headers = nodes.filter(
149
+ (node) => React.isValidElement(node) && node.type === AlertHeader,
150
+ );
151
+ const media = nodes.filter(
152
+ (node) => React.isValidElement(node) && node.type === AlertMedia,
153
+ );
154
+ const actions = nodes.filter(
155
+ (node) => React.isValidElement(node) && node.type === AlertActions,
156
+ );
157
+ if (
158
+ headers.length !== 1 ||
159
+ media.length > 1 ||
160
+ actions.length > 1 ||
161
+ headers.length + media.length + actions.length !== nodes.length
162
+ ) {
163
+ throw new Error(
164
+ "Alert explícito exige um AlertHeader e aceita no máximo um AlertMedia e um AlertActions como filhos diretos.",
165
+ );
166
+ }
167
+ }
168
+
169
+ const action = raw ? undefined : children;
170
+ return (
171
+ <AlertRootContext.Provider value={{ context: resolved.context }}>
172
+ <div
173
+ data-slot="alert"
174
+ data-context={resolved.context}
175
+ data-variant={resolved.variant}
176
+ role="alert"
177
+ className={cn(alertVariants(resolved), className)}
178
+ {...props}
179
+ >
180
+ {shorthand ? (
181
+ <>
182
+ {icon !== undefined && icon !== null && (
183
+ <AlertMedia>{icon}</AlertMedia>
184
+ )}
185
+ <AlertHeader>
186
+ {title !== undefined && <AlertTitle>{title}</AlertTitle>}
187
+ {(description !== undefined || raw) && (
188
+ <AlertDescription>
189
+ {raw ? children : description}
190
+ </AlertDescription>
191
+ )}
192
+ </AlertHeader>
193
+ {action !== undefined && action !== null && (
194
+ <AlertActions>{action}</AlertActions>
195
+ )}
196
+ </>
197
+ ) : legacyComposition ? (
198
+ <>
199
+ {icon !== undefined && icon !== null && (
200
+ <AlertMedia>{icon}</AlertMedia>
201
+ )}
202
+ <AlertHeaderContext.Provider value>
203
+ <div data-slot="alert-header" className={alertHeaderClasses}>
204
+ {children}
205
+ </div>
206
+ </AlertHeaderContext.Provider>
207
+ </>
208
+ ) : (
209
+ children
210
+ )}
211
+ </div>
212
+ </AlertRootContext.Provider>
213
+ );
214
+ }
215
+
216
+ export function AlertMedia({
217
+ className,
218
+ ...props
219
+ }: React.ComponentProps<"div">): React.ReactElement {
220
+ const root = requiredContext(useAlertRoot(), "AlertMedia", "Alert");
75
221
  return (
76
222
  <div
77
- data-slot="alert"
78
- role="alert"
79
- className={cn(alertVariants({ variant }), hasIcon && 'grid items-start', className)}
80
- style={hasIcon ? {
81
- gridTemplateColumns: '2.25rem minmax(0, 1fr)',
82
- columnGap: '0.75rem',
83
- rowGap: '0.25rem',
84
- ...style,
85
- } : style}
223
+ data-slot="alert-media"
224
+ aria-hidden="true"
225
+ className={cn(alertMediaClasses(root.context), className)}
86
226
  {...props}
87
- >
88
- {hasIcon && (
89
- <span
90
- data-slot="alert-icon"
91
- aria-hidden="true"
92
- className={alertIconVariants({ variant })}
93
- style={{ gridRow: '1 / span 2' }}
94
- >
95
- {icon}
96
- </span>
97
- )}
98
- {short ? (
99
- <>
100
- {title !== undefined && <AlertTitle>{title}</AlertTitle>}
101
- {description !== undefined && <AlertDescription>{description}</AlertDescription>}
102
- {/* A ação (children DEPOIS da frase) precisa da coluna 2: sem `col-start-2`
103
- ela cai na coluna do ícone e é espremida pra esquerda. O
104
- `col-start-2` é inócuo sem o grid (forma curta sem ícone). */}
105
- {children !== undefined && children !== null && (
106
- <div data-slot="alert-actions" className="col-start-2 mt-1">
107
- {body}
108
- </div>
109
- )}
110
- </>
111
- ) : (
112
- body
113
- )}
114
- </div>
115
- )
227
+ />
228
+ );
229
+ }
230
+
231
+ function useAlertRoot(): AlertRootContextValue | null {
232
+ return React.useContext(AlertRootContext);
116
233
  }
117
234
 
118
- export function AlertTitle({ className, ...props }: React.ComponentProps<'div'>): React.ReactElement {
235
+ export function AlertHeader({
236
+ className,
237
+ children,
238
+ ...props
239
+ }: React.ComponentProps<"div">): React.ReactElement {
240
+ requiredContext(useAlertRoot(), "AlertHeader", "Alert");
241
+ const nodes = React.Children.toArray(children);
242
+ const titles = nodes.filter(
243
+ (node) => React.isValidElement(node) && node.type === AlertTitle,
244
+ );
245
+ const descriptions = nodes.filter(
246
+ (node) => React.isValidElement(node) && node.type === AlertDescription,
247
+ );
248
+ const nativeChildren = nodes.filter(
249
+ (node) => React.isValidElement(node) && typeof node.type === "string",
250
+ );
251
+ if (
252
+ nodes.length === 0 ||
253
+ titles.length > 1 ||
254
+ descriptions.length > 1 ||
255
+ nativeChildren.length > 0
256
+ ) {
257
+ throw new Error(
258
+ "AlertHeader aceita no máximo um AlertTitle e um AlertDescription; wrappers reutilizáveis também são permitidos.",
259
+ );
260
+ }
261
+ return (
262
+ <AlertHeaderContext.Provider value>
263
+ <div
264
+ data-slot="alert-header"
265
+ className={cn(
266
+ alertHeaderClasses,
267
+ className,
268
+ )}
269
+ {...props}
270
+ >
271
+ {children}
272
+ </div>
273
+ </AlertHeaderContext.Provider>
274
+ );
275
+ }
276
+
277
+ export function AlertTitle({
278
+ className,
279
+ ...props
280
+ }: React.ComponentProps<"div">): React.ReactElement {
281
+ requireParent(
282
+ React.useContext(AlertHeaderContext),
283
+ "AlertTitle",
284
+ "AlertHeader",
285
+ );
119
286
  return (
120
287
  <div
121
288
  data-slot="alert-title"
122
- className={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}
289
+ className={cn(
290
+ "line-clamp-1 min-h-4 font-medium tracking-tight",
291
+ className,
292
+ )}
123
293
  {...props}
124
294
  />
125
- )
295
+ );
126
296
  }
127
297
 
128
298
  export function AlertDescription({
129
299
  className,
130
300
  ...props
131
- }: React.ComponentProps<'div'>): React.ReactElement {
301
+ }: React.ComponentProps<"div">): React.ReactElement {
302
+ requireParent(
303
+ React.useContext(AlertHeaderContext),
304
+ "AlertDescription",
305
+ "AlertHeader",
306
+ );
132
307
  return (
133
308
  <div
134
309
  data-slot="alert-description"
135
310
  className={cn(
136
- // NÃO é grid (era, no shadcn): num grid TODO nó filho vira item empilhado, então
137
- // uma frase com interpolação — `Workspace {cliente} {nome} sincronizado.` — saía
138
- // uma palavra por linha. Texto inline flui; o espaço entre BLOCOS filhos (p, ul,
139
- // ação) continua vindo do `*+*`.
140
- 'col-start-2 text-sm text-muted-foreground [&>*+*]:mt-1 [&_p]:leading-relaxed',
311
+ "text-sm text-muted-foreground [&>*+*]:mt-1 [&_p]:leading-relaxed",
312
+ className,
313
+ )}
314
+ {...props}
315
+ />
316
+ );
317
+ }
318
+
319
+ export function AlertActions({
320
+ className,
321
+ ...props
322
+ }: React.ComponentProps<"div">): React.ReactElement {
323
+ requiredContext(useAlertRoot(), "AlertActions", "Alert");
324
+ return (
325
+ <div
326
+ data-slot="alert-actions"
327
+ className={cn(
328
+ "col-start-3 row-start-1 ms-3 flex self-center items-center gap-2",
141
329
  className,
142
330
  )}
143
331
  {...props}
144
332
  />
145
- )
333
+ );
146
334
  }
@@ -115,7 +115,8 @@ export function Ask({
115
115
  type="button"
116
116
  size="sm"
117
117
  shape="pill"
118
- variant={selected ? 'secondary' : 'outline'}
118
+ context={selected ? 'neutral' : 'primary'}
119
+ variant={selected ? 'solid' : 'outline'}
119
120
  role={question.multiSelect ? undefined : 'radio'}
120
121
  aria-checked={question.multiSelect ? undefined : selected}
121
122
  aria-pressed={question.multiSelect ? selected : undefined}
@@ -1,40 +1,101 @@
1
1
  import * as React from 'react'
2
2
  import { Slot } from '@radix-ui/react-slot'
3
- import { cva, type VariantProps } from 'class-variance-authority'
3
+
4
+ import type { UiContext } from '../../../core/ui-context.ts'
4
5
  import { cn } from '../../lib/cn.ts'
6
+ import {
7
+ semanticOutlineClasses,
8
+ semanticSolidClasses,
9
+ semanticSubtleClasses,
10
+ } from '../../lib/semantic-context.ts'
11
+
12
+ export type BadgeContext = UiContext
13
+ export type BadgeVariant = 'solid' | 'subtle' | 'outline'
14
+ export type LegacyBadgeVariant =
15
+ | 'default'
16
+ | 'secondary'
17
+ | 'destructive'
18
+ | 'success'
19
+ | 'warning'
20
+ | 'info'
21
+ | 'danger'
22
+
23
+ export interface BadgeStyleOptions {
24
+ context?: BadgeContext | null
25
+ variant?: BadgeVariant | LegacyBadgeVariant | null
26
+ }
27
+
28
+ const baseClasses =
29
+ 'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&>svg]:pointer-events-none [&>svg]:size-3'
30
+
31
+ export function resolveBadgeStyle({
32
+ context,
33
+ variant,
34
+ }: BadgeStyleOptions = {}): { context: BadgeContext; variant: BadgeVariant } {
35
+ const legacy =
36
+ variant === 'default' ||
37
+ variant === 'secondary' ||
38
+ variant === 'destructive' ||
39
+ variant === 'success' ||
40
+ variant === 'warning' ||
41
+ variant === 'info' ||
42
+ variant === 'danger'
43
+ if (context != null && legacy) {
44
+ throw new Error('Badge não permite combinar context com uma variante semântica legada.')
45
+ }
46
+ switch (variant) {
47
+ case 'default':
48
+ return { context: 'primary', variant: 'solid' }
49
+ case 'secondary':
50
+ return { context: 'neutral', variant: 'solid' }
51
+ case 'destructive':
52
+ return { context: 'danger', variant: 'solid' }
53
+ case 'success':
54
+ case 'warning':
55
+ case 'info':
56
+ case 'danger':
57
+ return { context: variant, variant: 'subtle' }
58
+ default:
59
+ return {
60
+ context: context ?? 'neutral',
61
+ variant: variant ?? 'subtle',
62
+ }
63
+ }
64
+ }
65
+
66
+ /** Classes do Badge. Contexto seleciona significado; variante seleciona o tratamento visual. */
67
+ export function badgeVariants(options: BadgeStyleOptions = {}): string {
68
+ const resolved = resolveBadgeStyle(options)
69
+ const classes =
70
+ resolved.variant === 'solid'
71
+ ? semanticSolidClasses[resolved.context]
72
+ : resolved.variant === 'outline'
73
+ ? semanticOutlineClasses[resolved.context]
74
+ : semanticSubtleClasses[resolved.context]
75
+ return cn(baseClasses, classes)
76
+ }
5
77
 
6
- export const badgeVariants = cva(
7
- "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
8
- {
9
- variants: {
10
- variant: {
11
- default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
12
- secondary: 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
13
- destructive:
14
- 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40',
15
- outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
16
- // Tons de status (fill tingido + borda translúcida) — convenção da casa pra
17
- // estado semântico, igual ao KindBadge do maestro. Não-interativos.
18
- success: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400',
19
- warning: 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400',
20
- info: 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-400',
21
- // `danger` é o tom tonal da família (status/estágio em `DictionaryValue`);
22
- // `destructive` continua sólido, reservado a alerta/ação.
23
- danger: 'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-400',
24
- },
25
- },
26
- defaultVariants: { variant: 'default' },
27
- },
28
- )
29
-
30
- export interface BadgeProps
31
- extends React.ComponentProps<'span'>,
32
- VariantProps<typeof badgeVariants> {
78
+ export interface BadgeProps extends React.ComponentProps<'span'>, BadgeStyleOptions {
33
79
  /** Renderiza como o filho (Radix Slot) — ex.: um <a> com cara de badge. */
34
80
  asChild?: boolean
35
81
  }
36
82
 
37
- export function Badge({ className, variant, asChild = false, ...props }: BadgeProps): React.ReactElement {
83
+ export function Badge({
84
+ className,
85
+ context,
86
+ variant,
87
+ asChild = false,
88
+ ...props
89
+ }: BadgeProps): React.ReactElement {
38
90
  const Comp = asChild ? Slot : 'span'
39
- return <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
91
+ const resolved = resolveBadgeStyle({ context, variant })
92
+ return (
93
+ <Comp
94
+ data-slot="badge"
95
+ data-context={resolved.context}
96
+ data-variant={resolved.variant}
97
+ className={cn(badgeVariants(resolved), className)}
98
+ {...props}
99
+ />
100
+ )
40
101
  }