@softize/opus 23.0.0 → 24.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 +81 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +241 -42
- package/bin/lib/copy.mjs +16 -34
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +15 -19
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +90 -73
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +4 -2
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +76 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +100 -58
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +34 -26
- package/src/ui/docs/content/pane.md +41 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +20 -20
- package/src/ui/docs/content/toggle.md +117 -65
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +4 -6
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +40 -43
- package/src/ui/theme.css +311 -64
- package/src/ui/components/patterns/content-header.tsx +0 -528
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- package/src/ui/docs/content/content.md +0 -275
|
@@ -1,528 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Children,
|
|
3
|
-
createContext,
|
|
4
|
-
isValidElement,
|
|
5
|
-
useContext,
|
|
6
|
-
useId,
|
|
7
|
-
type ComponentProps,
|
|
8
|
-
type HTMLAttributes,
|
|
9
|
-
type ReactElement,
|
|
10
|
-
type ReactNode,
|
|
11
|
-
} from "react";
|
|
12
|
-
import { cn } from "../../lib/cn.ts";
|
|
13
|
-
import { IconTile, type IconTileProps } from "../primitives/icon-tile.tsx";
|
|
14
|
-
import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
15
|
-
import { headingTags } from "./heading-level.ts";
|
|
16
|
-
import { usePageHeadingPresence } from "./page-heading-context.tsx";
|
|
17
|
-
import type { SurfaceLayout } from "./surface-anatomy.ts";
|
|
18
|
-
import { SurfaceMedia, type SurfaceMediaProps } from "./surface-media.tsx";
|
|
19
|
-
import {
|
|
20
|
-
SurfaceHeader,
|
|
21
|
-
surfaceHeaderClasses,
|
|
22
|
-
} from "./surface-header.tsx";
|
|
23
|
-
|
|
24
|
-
export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
25
|
-
export type ContentElement = "section" | "fieldset";
|
|
26
|
-
export type ContentFrameVariant = "default" | "legend";
|
|
27
|
-
|
|
28
|
-
interface ContentContextValue {
|
|
29
|
-
element: ContentElement;
|
|
30
|
-
frameVariant: ContentFrameVariant;
|
|
31
|
-
framed: boolean;
|
|
32
|
-
layout: SurfaceLayout;
|
|
33
|
-
level: ContentHeaderLevel;
|
|
34
|
-
titleId: string;
|
|
35
|
-
variant: "page" | "section";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const ContentContext = createContext<ContentContextValue | null>(null);
|
|
39
|
-
const ContentHeaderContext = createContext<ContentContextValue | null>(null);
|
|
40
|
-
|
|
41
|
-
function requiredContext<T>(
|
|
42
|
-
value: T | null,
|
|
43
|
-
component: string,
|
|
44
|
-
parent: string,
|
|
45
|
-
): T {
|
|
46
|
-
if (value === null)
|
|
47
|
-
throw new Error(`${component} deve estar dentro de ${parent}.`);
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface ContentSharedProps {
|
|
52
|
-
/** Aplica a moldura estrutural da região sem transferi-la ao conteúdo interno. */
|
|
53
|
-
framed?: boolean;
|
|
54
|
-
/** Anatomia interna: contínua (default) ou com header separado do corpo. */
|
|
55
|
-
layout?: SurfaceLayout;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface ContentSectionBaseProps
|
|
59
|
-
extends
|
|
60
|
-
ContentSharedProps,
|
|
61
|
-
Omit<ComponentProps<"section">, "children" | "title"> {
|
|
62
|
-
/** Elemento semântico da região. */
|
|
63
|
-
as?: "section";
|
|
64
|
-
/** Tratamento visual da moldura: contínua (default) ou integrada ao header. */
|
|
65
|
-
frameVariant?: ContentFrameVariant;
|
|
66
|
-
/** Nível semântico do heading, independente de seu destaque visual. */
|
|
67
|
-
level?: ContentHeaderLevel;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface ContentFieldsetBaseProps
|
|
71
|
-
extends
|
|
72
|
-
ContentSharedProps,
|
|
73
|
-
Omit<ComponentProps<"fieldset">, "children" | "title"> {
|
|
74
|
-
/** Usa a semântica nativa de grupo de campos; o título vira `legend`. */
|
|
75
|
-
as: "fieldset";
|
|
76
|
-
/** Tratamento visual da moldura: contínua (default) ou recortada pela legenda. */
|
|
77
|
-
frameVariant?: ContentFrameVariant;
|
|
78
|
-
level?: never;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface ContentSectionShorthandProps extends ContentSectionBaseProps {
|
|
82
|
-
title: ReactNode;
|
|
83
|
-
/** Ícone ou imagem que identifica a região, antes do título. */
|
|
84
|
-
media?: ReactNode;
|
|
85
|
-
/** Total de itens ao lado do título da seção. A página não carrega contador (ADR 0009). */
|
|
86
|
-
count?: number;
|
|
87
|
-
description?: ReactNode;
|
|
88
|
-
actions?: ReactNode;
|
|
89
|
-
children: ReactNode;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface ContentFieldsetShorthandProps extends ContentFieldsetBaseProps {
|
|
93
|
-
title: ReactNode;
|
|
94
|
-
/** Ícone ou imagem decorativa que acompanha o título dentro da legenda. */
|
|
95
|
-
media?: ReactNode;
|
|
96
|
-
count?: never;
|
|
97
|
-
description?: never;
|
|
98
|
-
actions?: never;
|
|
99
|
-
children: ReactNode;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
interface ContentComposedProps {
|
|
103
|
-
title?: never;
|
|
104
|
-
media?: never;
|
|
105
|
-
count?: never;
|
|
106
|
-
description?: never;
|
|
107
|
-
actions?: never;
|
|
108
|
-
children: ReactNode;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export type ContentProps =
|
|
112
|
-
| ContentSectionShorthandProps
|
|
113
|
-
| ContentFieldsetShorthandProps
|
|
114
|
-
| ((ContentSectionBaseProps | ContentFieldsetBaseProps) &
|
|
115
|
-
ContentComposedProps);
|
|
116
|
-
|
|
117
|
-
/** Região de conteúdo nomeada. A forma curta e a explícita produzem a mesma anatomia. */
|
|
118
|
-
export function Content({
|
|
119
|
-
as = "section",
|
|
120
|
-
title,
|
|
121
|
-
media,
|
|
122
|
-
count,
|
|
123
|
-
description,
|
|
124
|
-
actions,
|
|
125
|
-
level = 2,
|
|
126
|
-
framed = false,
|
|
127
|
-
layout = "continuous",
|
|
128
|
-
frameVariant = "default",
|
|
129
|
-
children,
|
|
130
|
-
className,
|
|
131
|
-
...props
|
|
132
|
-
}: ContentProps): ReactElement {
|
|
133
|
-
const titleId = useId();
|
|
134
|
-
const fill = useFillRegion();
|
|
135
|
-
const variant = level === 1 && as === "section" ? "page" : "section";
|
|
136
|
-
const context: ContentContextValue = {
|
|
137
|
-
element: as,
|
|
138
|
-
frameVariant,
|
|
139
|
-
framed,
|
|
140
|
-
layout,
|
|
141
|
-
level: level as ContentHeaderLevel,
|
|
142
|
-
titleId,
|
|
143
|
-
variant,
|
|
144
|
-
};
|
|
145
|
-
const shorthand = title !== undefined;
|
|
146
|
-
const nodes = Children.toArray(children);
|
|
147
|
-
|
|
148
|
-
if (layout === "sectioned" && !framed) {
|
|
149
|
-
throw new Error('Content layout="sectioned" exige framed.');
|
|
150
|
-
}
|
|
151
|
-
if (layout === "sectioned" && frameVariant === "legend") {
|
|
152
|
-
throw new Error(
|
|
153
|
-
'Content não combina layout="sectioned" com frameVariant="legend".',
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
shorthand &&
|
|
159
|
-
nodes.some((node) => isValidElement(node) && node.type === ContentHeader)
|
|
160
|
-
) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
"Content não permite misturar propriedades de shorthand com ContentHeader explícito.",
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
if (!shorthand) {
|
|
166
|
-
const headers = nodes.filter(
|
|
167
|
-
(node) => isValidElement(node) && node.type === ContentHeader,
|
|
168
|
-
);
|
|
169
|
-
const bodies = nodes.filter(
|
|
170
|
-
(node) => isValidElement(node) && node.type === ContentBody,
|
|
171
|
-
);
|
|
172
|
-
if (
|
|
173
|
-
headers.length !== 1 ||
|
|
174
|
-
bodies.length !== 1 ||
|
|
175
|
-
headers.length + bodies.length !== nodes.length
|
|
176
|
-
) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
"Content explícito exige exatamente um ContentHeader e um ContentBody como filhos diretos.",
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const anatomy = shorthand ? (
|
|
184
|
-
<>
|
|
185
|
-
<ContentHeader>
|
|
186
|
-
{hasContentMedia(media) && <ContentMedia>{media}</ContentMedia>}
|
|
187
|
-
<ContentTitle>{title}</ContentTitle>
|
|
188
|
-
{count !== undefined && <ContentMeta>{count}</ContentMeta>}
|
|
189
|
-
{description !== undefined && (
|
|
190
|
-
<ContentDescription>{description}</ContentDescription>
|
|
191
|
-
)}
|
|
192
|
-
{actions !== undefined && <ContentActions>{actions}</ContentActions>}
|
|
193
|
-
</ContentHeader>
|
|
194
|
-
<ContentBody>{children}</ContentBody>
|
|
195
|
-
</>
|
|
196
|
-
) : (
|
|
197
|
-
children
|
|
198
|
-
);
|
|
199
|
-
const ariaLabelledBy =
|
|
200
|
-
props["aria-label"] === undefined ? titleId : undefined;
|
|
201
|
-
|
|
202
|
-
return (
|
|
203
|
-
<ContentContext.Provider value={context}>
|
|
204
|
-
{as === "fieldset" ? (
|
|
205
|
-
<fieldset
|
|
206
|
-
data-slot="content"
|
|
207
|
-
data-as="fieldset"
|
|
208
|
-
data-framed={framed}
|
|
209
|
-
data-frame-variant={frameVariant}
|
|
210
|
-
data-layout={layout}
|
|
211
|
-
{...fill.attributes}
|
|
212
|
-
aria-labelledby={ariaLabelledBy}
|
|
213
|
-
className={cn(
|
|
214
|
-
!framed && layout === "continuous" && "space-y-3",
|
|
215
|
-
framed &&
|
|
216
|
-
layout === "continuous" &&
|
|
217
|
-
frameVariant === "default" &&
|
|
218
|
-
"rounded-xl border border-border p-4",
|
|
219
|
-
framed &&
|
|
220
|
-
layout === "continuous" &&
|
|
221
|
-
frameVariant === "legend" &&
|
|
222
|
-
"space-y-3 rounded-xl border border-border px-4 pb-4",
|
|
223
|
-
framed &&
|
|
224
|
-
layout === "sectioned" &&
|
|
225
|
-
"overflow-hidden rounded-xl border border-border",
|
|
226
|
-
fill.filling && fillRegionClassName,
|
|
227
|
-
className,
|
|
228
|
-
)}
|
|
229
|
-
{...(props as ComponentProps<"fieldset">)}
|
|
230
|
-
>
|
|
231
|
-
{fill.provide(anatomy)}
|
|
232
|
-
</fieldset>
|
|
233
|
-
) : (
|
|
234
|
-
<section
|
|
235
|
-
data-slot="content"
|
|
236
|
-
data-as="section"
|
|
237
|
-
data-framed={framed}
|
|
238
|
-
data-frame-variant={frameVariant}
|
|
239
|
-
data-layout={layout}
|
|
240
|
-
{...fill.attributes}
|
|
241
|
-
aria-labelledby={ariaLabelledBy}
|
|
242
|
-
className={cn(
|
|
243
|
-
layout === "continuous" &&
|
|
244
|
-
(variant === "page" ? "space-y-6" : "space-y-3"),
|
|
245
|
-
framed &&
|
|
246
|
-
layout === "continuous" &&
|
|
247
|
-
frameVariant === "default" &&
|
|
248
|
-
"rounded-xl border border-border p-4",
|
|
249
|
-
framed &&
|
|
250
|
-
layout === "continuous" &&
|
|
251
|
-
frameVariant === "legend" &&
|
|
252
|
-
"rounded-xl border border-border px-4 pb-4",
|
|
253
|
-
framed &&
|
|
254
|
-
layout === "sectioned" &&
|
|
255
|
-
"overflow-hidden rounded-xl border border-border",
|
|
256
|
-
fill.filling && fillRegionClassName,
|
|
257
|
-
className,
|
|
258
|
-
)}
|
|
259
|
-
{...(props as ComponentProps<"section">)}
|
|
260
|
-
>
|
|
261
|
-
{fill.provide(anatomy)}
|
|
262
|
-
</section>
|
|
263
|
-
)}
|
|
264
|
-
</ContentContext.Provider>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export type ContentHeaderProps = Omit<HTMLAttributes<HTMLDivElement>, "title">;
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Header estrutural de Content; não é uma região autônoma. A anatomia é a de
|
|
272
|
-
* `SurfaceHeader`, a mesma de `PageHeader`: só os nomes dos slots e a hierarquia mudam.
|
|
273
|
-
*/
|
|
274
|
-
export function ContentHeader({
|
|
275
|
-
className,
|
|
276
|
-
children,
|
|
277
|
-
...props
|
|
278
|
-
}: ContentHeaderProps): ReactElement {
|
|
279
|
-
const content = requiredContext(
|
|
280
|
-
useContext(ContentContext),
|
|
281
|
-
"ContentHeader",
|
|
282
|
-
"Content",
|
|
283
|
-
);
|
|
284
|
-
if (content.element === "fieldset") {
|
|
285
|
-
return (
|
|
286
|
-
<ContentHeaderContext.Provider value={content}>
|
|
287
|
-
<SurfaceHeader
|
|
288
|
-
as="legend"
|
|
289
|
-
name="ContentHeader"
|
|
290
|
-
slot="content"
|
|
291
|
-
slots={{ media: ContentMedia, title: ContentTitle }}
|
|
292
|
-
mediaPlacement="heading"
|
|
293
|
-
invalidMessage="ContentHeader de fieldset aceita somente ContentMedia e ContentTitle; contador, descrição e ações ficam fora da legenda."
|
|
294
|
-
className={cn(
|
|
295
|
-
content.framed &&
|
|
296
|
-
content.layout === "continuous" &&
|
|
297
|
-
content.frameVariant === "default" &&
|
|
298
|
-
"float-left w-full",
|
|
299
|
-
content.framed &&
|
|
300
|
-
content.layout === "continuous" &&
|
|
301
|
-
content.frameVariant === "legend" &&
|
|
302
|
-
"-ml-2 w-auto px-2",
|
|
303
|
-
content.framed &&
|
|
304
|
-
content.layout === "sectioned" &&
|
|
305
|
-
"float-left w-full border-b border-border p-3",
|
|
306
|
-
className,
|
|
307
|
-
)}
|
|
308
|
-
{...props}
|
|
309
|
-
>
|
|
310
|
-
{children}
|
|
311
|
-
</SurfaceHeader>
|
|
312
|
-
</ContentHeaderContext.Provider>
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
return (
|
|
316
|
-
<ContentHeaderContext.Provider value={content}>
|
|
317
|
-
<SurfaceHeader
|
|
318
|
-
name="ContentHeader"
|
|
319
|
-
slot="content"
|
|
320
|
-
slots={{
|
|
321
|
-
media: ContentMedia,
|
|
322
|
-
title: ContentTitle,
|
|
323
|
-
count: ContentMeta,
|
|
324
|
-
description: ContentDescription,
|
|
325
|
-
actions: ContentActions,
|
|
326
|
-
}}
|
|
327
|
-
mediaPlacement="heading"
|
|
328
|
-
className={cn(
|
|
329
|
-
content.framed &&
|
|
330
|
-
content.layout === "continuous" &&
|
|
331
|
-
content.frameVariant === "legend" &&
|
|
332
|
-
"flex items-center [&>[data-slot=content-actions]]:-mr-2 [&>[data-slot=content-actions]]:bg-[var(--surface-background,var(--background))] [&>[data-slot=content-actions]]:px-2 [&>[data-slot=content-heading]]:-ml-2 [&>[data-slot=content-heading]]:w-fit [&>[data-slot=content-heading]]:max-w-full [&>[data-slot=content-heading]]:bg-[var(--surface-background,var(--background))] [&>[data-slot=content-heading]]:px-2",
|
|
333
|
-
content.framed &&
|
|
334
|
-
content.layout === "continuous" &&
|
|
335
|
-
content.frameVariant === "legend" &&
|
|
336
|
-
content.variant === "section" &&
|
|
337
|
-
"-mt-4 min-h-8",
|
|
338
|
-
content.framed &&
|
|
339
|
-
content.layout === "continuous" &&
|
|
340
|
-
content.frameVariant === "legend" &&
|
|
341
|
-
content.variant === "page" &&
|
|
342
|
-
"-mt-5 min-h-10",
|
|
343
|
-
content.framed &&
|
|
344
|
-
content.layout === "sectioned" &&
|
|
345
|
-
"border-b border-border p-3",
|
|
346
|
-
content.variant === "page" &&
|
|
347
|
-
"flex-row items-center justify-start gap-3",
|
|
348
|
-
className,
|
|
349
|
-
)}
|
|
350
|
-
{...props}
|
|
351
|
-
>
|
|
352
|
-
{children}
|
|
353
|
-
</SurfaceHeader>
|
|
354
|
-
</ContentHeaderContext.Provider>
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/** `null`, `false` e `undefined` não desenham mídia: uma moldura vazia não identifica nada. */
|
|
359
|
-
function hasContentMedia(media: ReactNode): boolean {
|
|
360
|
-
return media !== undefined && media !== null && media !== false;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Um `IconTile` recebido como filho não vira uma segunda moldura: o slot assume o lugar dele e
|
|
365
|
-
* herda o que ele declara (`size`, `context`, `variant`, demais props), somando as classes.
|
|
366
|
-
*/
|
|
367
|
-
function adoptIconTile(props: SurfaceMediaProps): SurfaceMediaProps {
|
|
368
|
-
const { children, asChild, className } = props;
|
|
369
|
-
if (asChild) return props;
|
|
370
|
-
// `toArray`, não `only`: um array com um único item (de um `map`/`filter`) é válido aqui.
|
|
371
|
-
const nodes = Children.toArray(children);
|
|
372
|
-
if (nodes.length !== 1) return props;
|
|
373
|
-
const child = nodes[0];
|
|
374
|
-
if (!isValidElement<IconTileProps>(child) || child.type !== IconTile) {
|
|
375
|
-
return props;
|
|
376
|
-
}
|
|
377
|
-
// `null` em size/context/variant significa "use o padrão" no IconTile: fica o do slot.
|
|
378
|
-
const declared = Object.fromEntries(
|
|
379
|
-
Object.entries(child.props).filter(([, value]) => value != null),
|
|
380
|
-
) as SurfaceMediaProps;
|
|
381
|
-
return {
|
|
382
|
-
...props,
|
|
383
|
-
...declared,
|
|
384
|
-
className: cn(className, declared.className),
|
|
385
|
-
children: declared.children,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/** Moldura de ícone ou imagem que identifica a região nomeada. */
|
|
390
|
-
export function ContentMedia({
|
|
391
|
-
size = "xs",
|
|
392
|
-
...rest
|
|
393
|
-
}: SurfaceMediaProps): ReactElement {
|
|
394
|
-
const context = requiredContext(
|
|
395
|
-
useContext(ContentHeaderContext),
|
|
396
|
-
"ContentMedia",
|
|
397
|
-
"ContentHeader",
|
|
398
|
-
);
|
|
399
|
-
const props = adoptIconTile({ size, ...rest });
|
|
400
|
-
return (
|
|
401
|
-
<SurfaceMedia
|
|
402
|
-
slot="content-media"
|
|
403
|
-
{...props}
|
|
404
|
-
aria-hidden={context.element === "fieldset" ? true : props["aria-hidden"]}
|
|
405
|
-
/>
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
export function ContentTitle({
|
|
410
|
-
className,
|
|
411
|
-
id,
|
|
412
|
-
...props
|
|
413
|
-
}: HTMLAttributes<HTMLElement>): ReactElement {
|
|
414
|
-
const context = requiredContext(
|
|
415
|
-
useContext(ContentHeaderContext),
|
|
416
|
-
"ContentTitle",
|
|
417
|
-
"ContentHeader",
|
|
418
|
-
);
|
|
419
|
-
const Heading =
|
|
420
|
-
context.element === "fieldset" ? "span" : headingTags[context.level];
|
|
421
|
-
usePageHeadingPresence(context.element !== "fieldset" && context.level === 1);
|
|
422
|
-
return (
|
|
423
|
-
<Heading
|
|
424
|
-
id={id ?? context.titleId}
|
|
425
|
-
data-slot="content-title"
|
|
426
|
-
className={cn(surfaceHeaderClasses[context.variant].title, className)}
|
|
427
|
-
{...props}
|
|
428
|
-
/>
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
export function ContentDescription({
|
|
433
|
-
className,
|
|
434
|
-
...props
|
|
435
|
-
}: HTMLAttributes<HTMLParagraphElement>): ReactElement {
|
|
436
|
-
const context = requiredContext(
|
|
437
|
-
useContext(ContentHeaderContext),
|
|
438
|
-
"ContentDescription",
|
|
439
|
-
"ContentHeader",
|
|
440
|
-
);
|
|
441
|
-
return (
|
|
442
|
-
<p
|
|
443
|
-
data-slot="content-description"
|
|
444
|
-
className={cn(
|
|
445
|
-
surfaceHeaderClasses[context.variant].description,
|
|
446
|
-
className,
|
|
447
|
-
)}
|
|
448
|
-
{...props}
|
|
449
|
-
/>
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/** O contador ao lado do título — o slot `count` do shorthand. */
|
|
454
|
-
export function ContentMeta({
|
|
455
|
-
className,
|
|
456
|
-
...props
|
|
457
|
-
}: HTMLAttributes<HTMLSpanElement>): ReactElement {
|
|
458
|
-
const context = requiredContext(
|
|
459
|
-
useContext(ContentHeaderContext),
|
|
460
|
-
"ContentMeta",
|
|
461
|
-
"ContentHeader",
|
|
462
|
-
);
|
|
463
|
-
return (
|
|
464
|
-
<span
|
|
465
|
-
data-slot="content-meta"
|
|
466
|
-
className={cn(surfaceHeaderClasses[context.variant].count, className)}
|
|
467
|
-
{...props}
|
|
468
|
-
/>
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
export function ContentActions({
|
|
473
|
-
className,
|
|
474
|
-
...props
|
|
475
|
-
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
476
|
-
const context = requiredContext(
|
|
477
|
-
useContext(ContentHeaderContext),
|
|
478
|
-
"ContentActions",
|
|
479
|
-
"ContentHeader",
|
|
480
|
-
);
|
|
481
|
-
return (
|
|
482
|
-
<div
|
|
483
|
-
data-slot="content-actions"
|
|
484
|
-
className={cn(
|
|
485
|
-
surfaceHeaderClasses[context.variant].actions,
|
|
486
|
-
context.variant === "page" && "ml-auto w-auto",
|
|
487
|
-
className,
|
|
488
|
-
)}
|
|
489
|
-
{...props}
|
|
490
|
-
/>
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
export function ContentBody({
|
|
495
|
-
className,
|
|
496
|
-
children,
|
|
497
|
-
...props
|
|
498
|
-
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
499
|
-
const content = requiredContext(
|
|
500
|
-
useContext(ContentContext),
|
|
501
|
-
"ContentBody",
|
|
502
|
-
"Content",
|
|
503
|
-
);
|
|
504
|
-
const fill = useFillRegion();
|
|
505
|
-
return (
|
|
506
|
-
<div
|
|
507
|
-
data-slot="content-body"
|
|
508
|
-
{...fill.attributes}
|
|
509
|
-
className={cn(
|
|
510
|
-
content.element === "fieldset" && "space-y-3",
|
|
511
|
-
content.element === "fieldset" &&
|
|
512
|
-
content.framed &&
|
|
513
|
-
content.layout === "continuous" &&
|
|
514
|
-
content.frameVariant === "default" &&
|
|
515
|
-
"clear-left pt-3",
|
|
516
|
-
content.element === "fieldset" &&
|
|
517
|
-
content.framed &&
|
|
518
|
-
content.layout === "sectioned" &&
|
|
519
|
-
"clear-left",
|
|
520
|
-
fill.filling && fillRegionClassName,
|
|
521
|
-
className,
|
|
522
|
-
)}
|
|
523
|
-
{...props}
|
|
524
|
-
>
|
|
525
|
-
{fill.provide(children)}
|
|
526
|
-
</div>
|
|
527
|
-
);
|
|
528
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mídia de superfície (interno): a moldura que abre a linha do header — ADR 0023.
|
|
3
|
-
*
|
|
4
|
-
* Reusa `IconTile`; nenhuma família cria outra escala de molduras. O slot público de
|
|
5
|
-
* cada família (`CardMedia`, `DialogMedia`, …) delega para cá, escolhendo apenas o
|
|
6
|
-
* `data-slot` e a escala default.
|
|
7
|
-
*/
|
|
8
|
-
import type { ReactElement } from "react";
|
|
9
|
-
|
|
10
|
-
import { Slot } from "@radix-ui/react-slot";
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
IconTile,
|
|
14
|
-
iconTileVariants,
|
|
15
|
-
type IconTileContext,
|
|
16
|
-
type IconTileSize,
|
|
17
|
-
type IconTileVariant,
|
|
18
|
-
} from "../primitives/icon-tile.tsx";
|
|
19
|
-
import { cn } from "../../lib/cn.ts";
|
|
20
|
-
|
|
21
|
-
export interface SurfaceMediaProps
|
|
22
|
-
extends Omit<React.ComponentProps<"span">, "children"> {
|
|
23
|
-
/** Escala da moldura; a família define o default. */
|
|
24
|
-
size?: IconTileSize;
|
|
25
|
-
context?: IconTileContext;
|
|
26
|
-
variant?: IconTileVariant;
|
|
27
|
-
/**
|
|
28
|
-
* Renderiza como o filho: para a marca que já tem visual próprio — um orbe, um
|
|
29
|
-
* logotipo — ocupar o slot sem ganhar a moldura por cima.
|
|
30
|
-
*/
|
|
31
|
-
asChild?: boolean;
|
|
32
|
-
children?: React.ReactNode;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function SurfaceMedia({
|
|
36
|
-
slot,
|
|
37
|
-
size = "sm",
|
|
38
|
-
context,
|
|
39
|
-
variant,
|
|
40
|
-
asChild = false,
|
|
41
|
-
className,
|
|
42
|
-
...props
|
|
43
|
-
}: SurfaceMediaProps & { slot: string }): ReactElement {
|
|
44
|
-
if (asChild) {
|
|
45
|
-
return (
|
|
46
|
-
<Slot
|
|
47
|
-
data-slot={slot}
|
|
48
|
-
className={cn(
|
|
49
|
-
iconTileVariants({ size, context: null, variant: null }),
|
|
50
|
-
// Sem fundo nem borda: quem compõe traz o próprio visual.
|
|
51
|
-
"bg-transparent",
|
|
52
|
-
"shrink-0",
|
|
53
|
-
className,
|
|
54
|
-
)}
|
|
55
|
-
{...props}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
return (
|
|
60
|
-
<IconTile
|
|
61
|
-
data-slot={slot}
|
|
62
|
-
size={size}
|
|
63
|
-
{...(context !== undefined ? { context } : {})}
|
|
64
|
-
{...(variant !== undefined ? { variant } : {})}
|
|
65
|
-
className={cn("shrink-0", className)}
|
|
66
|
-
{...props}
|
|
67
|
-
/>
|
|
68
|
-
);
|
|
69
|
-
}
|