@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
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import { cn } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
className={cn(
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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(
|
|
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<
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
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
|
}
|