@softize/opus 22.2.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 +120 -1
- package/bin/cli.mjs +3 -9
- package/bin/lib/check.mjs +242 -49
- package/bin/lib/copy.mjs +17 -35
- package/bin/lib/gen-runner.mjs +2 -0
- package/bin/lib/gen-stubs.mjs +3 -1
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
- 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/docs/code-style.md +1 -1
- 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/skills/create-opus-action/references/contract-and-binding.md +3 -3
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/core/contracts.ts +1 -1
- package/src/core/runtime.ts +4 -37
- package/src/core/types.ts +5 -23
- package/src/design/index.ts +14 -3
- package/src/testing/index.ts +6 -26
- 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 +64 -14
- 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 +93 -70
- 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 +58 -88
- 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 +85 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/actions.md +6 -7
- 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/semantic-context.md +0 -1
- 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 +82 -83
- package/src/ui/docs/content/testing.md +2 -3
- package/src/ui/docs/content/toggle.md +120 -63
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +4 -6
- package/src/ui/drivers/react.tsx +4 -3
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +42 -46
- package/src/ui/theme.css +311 -64
- package/src/vite/design.ts +23 -21
- package/src/ui/components/patterns/content-header.tsx +0 -532
- 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 -276
package/src/vite/design.ts
CHANGED
|
@@ -88,8 +88,8 @@ export interface OpusDesignOptions {
|
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* Cenários de design (ADR 0037): módulos que exportam `defineDesignScenario(contrato, resposta)`.
|
|
91
|
-
* Aceita caminhos e globs, como `entry`. O cenário vence
|
|
92
|
-
*
|
|
91
|
+
* Aceita caminhos e globs, como `entry`. O cenário vence a resposta padrão gerada pelo `fake`;
|
|
92
|
+
* cenário de action inexistente impede o boot, com a lista no erro.
|
|
93
93
|
* Sem default: o projeto decide onde os cenários ficam e se são versionados.
|
|
94
94
|
*/
|
|
95
95
|
scenarios?: string | string[]
|
|
@@ -116,7 +116,7 @@ export interface OpusDesignOptions {
|
|
|
116
116
|
stubs?: Record<string, DesignStubHandler>
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Monta o /api também no dev NORMAL
|
|
119
|
+
* Monta o /api também no dev NORMAL, com os handlers reais.
|
|
120
120
|
* Default false — devBackend puro não é design (sem sonda, proxy intacto).
|
|
121
121
|
*/
|
|
122
122
|
devBackend?: boolean
|
|
@@ -156,7 +156,7 @@ const PROBE_PATH = '/__opus/design'
|
|
|
156
156
|
const RUNTIME_ENDPOINT_PATHS = new Set(['/health', '/ready', '/openapi.json'])
|
|
157
157
|
|
|
158
158
|
/** De onde vem a resposta de cada action no modo design, na ordem de precedência. */
|
|
159
|
-
type DesignSource = 'scenario' | '
|
|
159
|
+
type DesignSource = 'scenario' | 'handler' | 'generated'
|
|
160
160
|
|
|
161
161
|
interface DesignState {
|
|
162
162
|
handle: NodeServerHandle
|
|
@@ -214,9 +214,8 @@ export function opusDesign(options: OpusDesignOptions = {}): Plugin {
|
|
|
214
214
|
exProxyPrefixes = Object.keys(config.server?.proxy ?? {})
|
|
215
215
|
// 2) Mata o proxy: fecha o vazamento preview→prod pra QUALQUER prefixo.
|
|
216
216
|
if (config.server?.proxy !== undefined) config.server.proxy = undefined
|
|
217
|
-
// 3) O
|
|
218
|
-
//
|
|
219
|
-
process.env.OPUS_MODE = 'design'
|
|
217
|
+
// 3) O boot liga em cada contrato um handler de cenário ou resposta gerada,
|
|
218
|
+
// e o preview fica autossuficiente sem alterar o runtime de produção.
|
|
220
219
|
},
|
|
221
220
|
|
|
222
221
|
configureServer(server) {
|
|
@@ -362,15 +361,14 @@ export function opusDesign(options: OpusDesignOptions = {}): Plugin {
|
|
|
362
361
|
const named = (source: DesignSource): string[] =>
|
|
363
362
|
state.actions.filter((a) => state.sources.get(a.name) === source).map((a) => a.name)
|
|
364
363
|
const scenarios = named('scenario')
|
|
365
|
-
const mocks = named('mock')
|
|
366
364
|
payload.runtime = {
|
|
367
365
|
prefix: apiPrefix,
|
|
368
366
|
actions: state.actions.length,
|
|
369
|
-
// `mocked`/`unmocked`
|
|
370
|
-
//
|
|
371
|
-
mocked: scenarios.length
|
|
367
|
+
// `mocked`/`unmocked` preservam o formato histórico da sonda: cenário escrito
|
|
368
|
+
// à mão contra respostas vindas de handler real ou geradas pelo `fake`.
|
|
369
|
+
mocked: scenarios.length,
|
|
372
370
|
unmocked: state.actions
|
|
373
|
-
.filter((a) =>
|
|
371
|
+
.filter((a) => state.sources.get(a.name) !== 'scenario')
|
|
374
372
|
.map((a) => a.name),
|
|
375
373
|
scenarios,
|
|
376
374
|
generated: named('generated'),
|
|
@@ -687,8 +685,8 @@ function scenariosOfModule(mod: Record<string, unknown>): DesignScenario[] {
|
|
|
687
685
|
}
|
|
688
686
|
|
|
689
687
|
/**
|
|
690
|
-
* Liga a cada action a resposta de design, nesta precedência: cenário,
|
|
691
|
-
*
|
|
688
|
+
* Liga a cada action a resposta de design, nesta precedência: cenário, handler real
|
|
689
|
+
* (`bindAction`) e, por fim, a resposta padrão gerada pelo `fake`.
|
|
692
690
|
*/
|
|
693
691
|
function prepareDesignActions(
|
|
694
692
|
actions: ActionDef[],
|
|
@@ -699,18 +697,23 @@ function prepareDesignActions(
|
|
|
699
697
|
const scenario = scenarios.get(action.name)
|
|
700
698
|
if (scenario !== undefined) {
|
|
701
699
|
sources.set(action.name, 'scenario')
|
|
702
|
-
return {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
700
|
+
return {
|
|
701
|
+
...action,
|
|
702
|
+
// Resposta simulada não representa exposição real de dado pessoal (ADR 0041).
|
|
703
|
+
personalData: false,
|
|
704
|
+
handler: (ctx: ActionContext, input: unknown) => scenario.respond(input, ctx),
|
|
705
|
+
} as ActionDef
|
|
707
706
|
}
|
|
708
707
|
if (typeof (action as { handler?: unknown }).handler === 'function') {
|
|
709
708
|
sources.set(action.name, 'handler')
|
|
710
709
|
return action
|
|
711
710
|
}
|
|
712
711
|
sources.set(action.name, 'generated')
|
|
713
|
-
return {
|
|
712
|
+
return {
|
|
713
|
+
...action,
|
|
714
|
+
personalData: false,
|
|
715
|
+
handler: async (_ctx: ActionContext, input: unknown) => designResponse(action, input),
|
|
716
|
+
} as ActionDef
|
|
714
717
|
})
|
|
715
718
|
return { actions: prepared, sources }
|
|
716
719
|
}
|
|
@@ -759,4 +762,3 @@ function sendEnvelope(res: ServerResponse, status: number, input: ErrorInput): v
|
|
|
759
762
|
),
|
|
760
763
|
)
|
|
761
764
|
}
|
|
762
|
-
|
|
@@ -1,532 +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
|
-
type SurfaceHeaderVariant,
|
|
23
|
-
} from "./surface-header.tsx";
|
|
24
|
-
|
|
25
|
-
export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
26
|
-
export type ContentHeaderVariant = SurfaceHeaderVariant;
|
|
27
|
-
export type ContentElement = "section" | "fieldset";
|
|
28
|
-
export type ContentFrameVariant = "default" | "legend";
|
|
29
|
-
|
|
30
|
-
interface ContentContextValue {
|
|
31
|
-
element: ContentElement;
|
|
32
|
-
frameVariant: ContentFrameVariant;
|
|
33
|
-
framed: boolean;
|
|
34
|
-
layout: SurfaceLayout;
|
|
35
|
-
level: ContentHeaderLevel;
|
|
36
|
-
titleId: string;
|
|
37
|
-
variant: ContentHeaderVariant;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const ContentContext = createContext<ContentContextValue | null>(null);
|
|
41
|
-
const ContentHeaderContext = createContext<ContentContextValue | null>(null);
|
|
42
|
-
|
|
43
|
-
function requiredContext<T>(
|
|
44
|
-
value: T | null,
|
|
45
|
-
component: string,
|
|
46
|
-
parent: string,
|
|
47
|
-
): T {
|
|
48
|
-
if (value === null)
|
|
49
|
-
throw new Error(`${component} deve estar dentro de ${parent}.`);
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface ContentSharedProps {
|
|
54
|
-
/** Aplica a moldura estrutural da região sem transferi-la ao conteúdo interno. */
|
|
55
|
-
framed?: boolean;
|
|
56
|
-
/** Anatomia interna: contínua (default) ou com header separado do corpo. */
|
|
57
|
-
layout?: SurfaceLayout;
|
|
58
|
-
/** Hierarquia visual: `section` (default) ou `page`, a mesma de PageHeader. */
|
|
59
|
-
variant?: ContentHeaderVariant;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface ContentSectionBaseProps
|
|
63
|
-
extends
|
|
64
|
-
ContentSharedProps,
|
|
65
|
-
Omit<ComponentProps<"section">, "children" | "title"> {
|
|
66
|
-
/** Elemento semântico da região. */
|
|
67
|
-
as?: "section";
|
|
68
|
-
/** Tratamento visual da moldura: contínua (default) ou integrada ao header. */
|
|
69
|
-
frameVariant?: ContentFrameVariant;
|
|
70
|
-
/** Nível semântico do heading, independente de seu destaque visual. */
|
|
71
|
-
level?: ContentHeaderLevel;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface ContentFieldsetBaseProps
|
|
75
|
-
extends
|
|
76
|
-
ContentSharedProps,
|
|
77
|
-
Omit<ComponentProps<"fieldset">, "children" | "title"> {
|
|
78
|
-
/** Usa a semântica nativa de grupo de campos; o título vira `legend`. */
|
|
79
|
-
as: "fieldset";
|
|
80
|
-
/** Tratamento visual da moldura: contínua (default) ou recortada pela legenda. */
|
|
81
|
-
frameVariant?: ContentFrameVariant;
|
|
82
|
-
level?: never;
|
|
83
|
-
variant?: "section";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface ContentSectionShorthandProps extends ContentSectionBaseProps {
|
|
87
|
-
title: ReactNode;
|
|
88
|
-
/** Ícone ou imagem que identifica a região, antes do título. */
|
|
89
|
-
media?: ReactNode;
|
|
90
|
-
/** Total de itens ao lado do título da seção. A página não carrega contador (ADR 0009). */
|
|
91
|
-
count?: number;
|
|
92
|
-
description?: ReactNode;
|
|
93
|
-
actions?: ReactNode;
|
|
94
|
-
children: ReactNode;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface ContentFieldsetShorthandProps extends ContentFieldsetBaseProps {
|
|
98
|
-
title: ReactNode;
|
|
99
|
-
/** Ícone ou imagem decorativa que acompanha o título dentro da legenda. */
|
|
100
|
-
media?: ReactNode;
|
|
101
|
-
count?: never;
|
|
102
|
-
description?: never;
|
|
103
|
-
actions?: never;
|
|
104
|
-
children: ReactNode;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
interface ContentComposedProps {
|
|
108
|
-
title?: never;
|
|
109
|
-
media?: never;
|
|
110
|
-
count?: never;
|
|
111
|
-
description?: never;
|
|
112
|
-
actions?: never;
|
|
113
|
-
children: ReactNode;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export type ContentProps =
|
|
117
|
-
| ContentSectionShorthandProps
|
|
118
|
-
| ContentFieldsetShorthandProps
|
|
119
|
-
| ((ContentSectionBaseProps | ContentFieldsetBaseProps) &
|
|
120
|
-
ContentComposedProps);
|
|
121
|
-
|
|
122
|
-
/** Região de conteúdo nomeada. A forma curta e a explícita produzem a mesma anatomia. */
|
|
123
|
-
export function Content({
|
|
124
|
-
as = "section",
|
|
125
|
-
title,
|
|
126
|
-
media,
|
|
127
|
-
count,
|
|
128
|
-
description,
|
|
129
|
-
actions,
|
|
130
|
-
level = 2,
|
|
131
|
-
variant = "section",
|
|
132
|
-
framed = false,
|
|
133
|
-
layout = "continuous",
|
|
134
|
-
frameVariant = "default",
|
|
135
|
-
children,
|
|
136
|
-
className,
|
|
137
|
-
...props
|
|
138
|
-
}: ContentProps): ReactElement {
|
|
139
|
-
const titleId = useId();
|
|
140
|
-
const fill = useFillRegion();
|
|
141
|
-
const context = {
|
|
142
|
-
element: as,
|
|
143
|
-
frameVariant,
|
|
144
|
-
framed,
|
|
145
|
-
layout,
|
|
146
|
-
level: level as ContentHeaderLevel,
|
|
147
|
-
titleId,
|
|
148
|
-
variant,
|
|
149
|
-
};
|
|
150
|
-
const shorthand = title !== undefined;
|
|
151
|
-
const nodes = Children.toArray(children);
|
|
152
|
-
|
|
153
|
-
if (layout === "sectioned" && !framed) {
|
|
154
|
-
throw new Error('Content layout="sectioned" exige framed.');
|
|
155
|
-
}
|
|
156
|
-
if (layout === "sectioned" && frameVariant === "legend") {
|
|
157
|
-
throw new Error(
|
|
158
|
-
'Content não combina layout="sectioned" com frameVariant="legend".',
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
shorthand &&
|
|
164
|
-
nodes.some((node) => isValidElement(node) && node.type === ContentHeader)
|
|
165
|
-
) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
"Content não permite misturar propriedades de shorthand com ContentHeader explícito.",
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
if (!shorthand) {
|
|
171
|
-
const headers = nodes.filter(
|
|
172
|
-
(node) => isValidElement(node) && node.type === ContentHeader,
|
|
173
|
-
);
|
|
174
|
-
const bodies = nodes.filter(
|
|
175
|
-
(node) => isValidElement(node) && node.type === ContentBody,
|
|
176
|
-
);
|
|
177
|
-
if (
|
|
178
|
-
headers.length !== 1 ||
|
|
179
|
-
bodies.length !== 1 ||
|
|
180
|
-
headers.length + bodies.length !== nodes.length
|
|
181
|
-
) {
|
|
182
|
-
throw new Error(
|
|
183
|
-
"Content explícito exige exatamente um ContentHeader e um ContentBody como filhos diretos.",
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const anatomy = shorthand ? (
|
|
189
|
-
<>
|
|
190
|
-
<ContentHeader>
|
|
191
|
-
{hasContentMedia(media) && <ContentMedia>{media}</ContentMedia>}
|
|
192
|
-
<ContentTitle>{title}</ContentTitle>
|
|
193
|
-
{count !== undefined && <ContentMeta>{count}</ContentMeta>}
|
|
194
|
-
{description !== undefined && (
|
|
195
|
-
<ContentDescription>{description}</ContentDescription>
|
|
196
|
-
)}
|
|
197
|
-
{actions !== undefined && <ContentActions>{actions}</ContentActions>}
|
|
198
|
-
</ContentHeader>
|
|
199
|
-
<ContentBody>{children}</ContentBody>
|
|
200
|
-
</>
|
|
201
|
-
) : (
|
|
202
|
-
children
|
|
203
|
-
);
|
|
204
|
-
const ariaLabelledBy =
|
|
205
|
-
props["aria-label"] === undefined ? titleId : undefined;
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<ContentContext.Provider value={context}>
|
|
209
|
-
{as === "fieldset" ? (
|
|
210
|
-
<fieldset
|
|
211
|
-
data-slot="content"
|
|
212
|
-
data-as="fieldset"
|
|
213
|
-
data-framed={framed}
|
|
214
|
-
data-frame-variant={frameVariant}
|
|
215
|
-
data-layout={layout}
|
|
216
|
-
{...fill.attributes}
|
|
217
|
-
aria-labelledby={ariaLabelledBy}
|
|
218
|
-
className={cn(
|
|
219
|
-
!framed && layout === "continuous" && "space-y-3",
|
|
220
|
-
framed &&
|
|
221
|
-
layout === "continuous" &&
|
|
222
|
-
frameVariant === "default" &&
|
|
223
|
-
"rounded-xl border border-border p-4",
|
|
224
|
-
framed &&
|
|
225
|
-
layout === "continuous" &&
|
|
226
|
-
frameVariant === "legend" &&
|
|
227
|
-
"space-y-3 rounded-xl border border-border px-4 pb-4",
|
|
228
|
-
framed &&
|
|
229
|
-
layout === "sectioned" &&
|
|
230
|
-
"overflow-hidden rounded-xl border border-border",
|
|
231
|
-
fill.filling && fillRegionClassName,
|
|
232
|
-
className,
|
|
233
|
-
)}
|
|
234
|
-
{...(props as ComponentProps<"fieldset">)}
|
|
235
|
-
>
|
|
236
|
-
{fill.provide(anatomy)}
|
|
237
|
-
</fieldset>
|
|
238
|
-
) : (
|
|
239
|
-
<section
|
|
240
|
-
data-slot="content"
|
|
241
|
-
data-as="section"
|
|
242
|
-
data-framed={framed}
|
|
243
|
-
data-frame-variant={frameVariant}
|
|
244
|
-
data-layout={layout}
|
|
245
|
-
{...fill.attributes}
|
|
246
|
-
aria-labelledby={ariaLabelledBy}
|
|
247
|
-
className={cn(
|
|
248
|
-
layout === "continuous" && "space-y-3",
|
|
249
|
-
framed &&
|
|
250
|
-
layout === "continuous" &&
|
|
251
|
-
frameVariant === "default" &&
|
|
252
|
-
"rounded-xl border border-border p-4",
|
|
253
|
-
framed &&
|
|
254
|
-
layout === "continuous" &&
|
|
255
|
-
frameVariant === "legend" &&
|
|
256
|
-
"rounded-xl border border-border px-4 pb-4",
|
|
257
|
-
framed &&
|
|
258
|
-
layout === "sectioned" &&
|
|
259
|
-
"overflow-hidden rounded-xl border border-border",
|
|
260
|
-
fill.filling && fillRegionClassName,
|
|
261
|
-
className,
|
|
262
|
-
)}
|
|
263
|
-
{...(props as ComponentProps<"section">)}
|
|
264
|
-
>
|
|
265
|
-
{fill.provide(anatomy)}
|
|
266
|
-
</section>
|
|
267
|
-
)}
|
|
268
|
-
</ContentContext.Provider>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export type ContentHeaderProps = Omit<HTMLAttributes<HTMLDivElement>, "title">;
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Header estrutural de Content; não é uma região autônoma. A anatomia é a de
|
|
276
|
-
* `SurfaceHeader`, a mesma de `PageHeader`: só os nomes dos slots e a hierarquia mudam.
|
|
277
|
-
*/
|
|
278
|
-
export function ContentHeader({
|
|
279
|
-
className,
|
|
280
|
-
children,
|
|
281
|
-
...props
|
|
282
|
-
}: ContentHeaderProps): ReactElement {
|
|
283
|
-
const content = requiredContext(
|
|
284
|
-
useContext(ContentContext),
|
|
285
|
-
"ContentHeader",
|
|
286
|
-
"Content",
|
|
287
|
-
);
|
|
288
|
-
if (content.element === "fieldset") {
|
|
289
|
-
return (
|
|
290
|
-
<ContentHeaderContext.Provider value={content}>
|
|
291
|
-
<SurfaceHeader
|
|
292
|
-
as="legend"
|
|
293
|
-
name="ContentHeader"
|
|
294
|
-
slot="content"
|
|
295
|
-
slots={{ media: ContentMedia, title: ContentTitle }}
|
|
296
|
-
mediaPlacement="heading"
|
|
297
|
-
invalidMessage="ContentHeader de fieldset aceita somente ContentMedia e ContentTitle; contador, descrição e ações ficam fora da legenda."
|
|
298
|
-
className={cn(
|
|
299
|
-
content.framed &&
|
|
300
|
-
content.layout === "continuous" &&
|
|
301
|
-
content.frameVariant === "default" &&
|
|
302
|
-
"float-left w-full",
|
|
303
|
-
content.framed &&
|
|
304
|
-
content.layout === "continuous" &&
|
|
305
|
-
content.frameVariant === "legend" &&
|
|
306
|
-
"-ml-2 w-auto px-2",
|
|
307
|
-
content.framed &&
|
|
308
|
-
content.layout === "sectioned" &&
|
|
309
|
-
"float-left w-full border-b border-border p-3",
|
|
310
|
-
className,
|
|
311
|
-
)}
|
|
312
|
-
{...props}
|
|
313
|
-
>
|
|
314
|
-
{children}
|
|
315
|
-
</SurfaceHeader>
|
|
316
|
-
</ContentHeaderContext.Provider>
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
return (
|
|
320
|
-
<ContentHeaderContext.Provider value={content}>
|
|
321
|
-
<SurfaceHeader
|
|
322
|
-
name="ContentHeader"
|
|
323
|
-
slot="content"
|
|
324
|
-
slots={{
|
|
325
|
-
media: ContentMedia,
|
|
326
|
-
title: ContentTitle,
|
|
327
|
-
count: ContentMeta,
|
|
328
|
-
description: ContentDescription,
|
|
329
|
-
actions: ContentActions,
|
|
330
|
-
}}
|
|
331
|
-
mediaPlacement="heading"
|
|
332
|
-
className={cn(
|
|
333
|
-
content.framed &&
|
|
334
|
-
content.layout === "continuous" &&
|
|
335
|
-
content.frameVariant === "legend" &&
|
|
336
|
-
"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",
|
|
337
|
-
content.framed &&
|
|
338
|
-
content.layout === "continuous" &&
|
|
339
|
-
content.frameVariant === "legend" &&
|
|
340
|
-
content.variant === "section" &&
|
|
341
|
-
"-mt-4 min-h-8",
|
|
342
|
-
content.framed &&
|
|
343
|
-
content.layout === "continuous" &&
|
|
344
|
-
content.frameVariant === "legend" &&
|
|
345
|
-
content.variant === "page" &&
|
|
346
|
-
"-mt-5 min-h-10",
|
|
347
|
-
content.framed &&
|
|
348
|
-
content.layout === "sectioned" &&
|
|
349
|
-
"border-b border-border p-3",
|
|
350
|
-
content.variant === "page" &&
|
|
351
|
-
"flex-row items-center justify-start gap-3",
|
|
352
|
-
className,
|
|
353
|
-
)}
|
|
354
|
-
{...props}
|
|
355
|
-
>
|
|
356
|
-
{children}
|
|
357
|
-
</SurfaceHeader>
|
|
358
|
-
</ContentHeaderContext.Provider>
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** `null`, `false` e `undefined` não desenham mídia: uma moldura vazia não identifica nada. */
|
|
363
|
-
function hasContentMedia(media: ReactNode): boolean {
|
|
364
|
-
return media !== undefined && media !== null && media !== false;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Um `IconTile` recebido como filho não vira uma segunda moldura: o slot assume o lugar dele e
|
|
369
|
-
* herda o que ele declara (`size`, `context`, `variant`, demais props), somando as classes.
|
|
370
|
-
*/
|
|
371
|
-
function adoptIconTile(props: SurfaceMediaProps): SurfaceMediaProps {
|
|
372
|
-
const { children, asChild, className } = props;
|
|
373
|
-
if (asChild) return props;
|
|
374
|
-
// `toArray`, não `only`: um array com um único item (de um `map`/`filter`) é válido aqui.
|
|
375
|
-
const nodes = Children.toArray(children);
|
|
376
|
-
if (nodes.length !== 1) return props;
|
|
377
|
-
const child = nodes[0];
|
|
378
|
-
if (!isValidElement<IconTileProps>(child) || child.type !== IconTile) {
|
|
379
|
-
return props;
|
|
380
|
-
}
|
|
381
|
-
// `null` em size/context/variant significa "use o padrão" no IconTile: fica o do slot.
|
|
382
|
-
const declared = Object.fromEntries(
|
|
383
|
-
Object.entries(child.props).filter(([, value]) => value != null),
|
|
384
|
-
) as SurfaceMediaProps;
|
|
385
|
-
return {
|
|
386
|
-
...props,
|
|
387
|
-
...declared,
|
|
388
|
-
className: cn(className, declared.className),
|
|
389
|
-
children: declared.children,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/** Moldura de ícone ou imagem que identifica a região nomeada. */
|
|
394
|
-
export function ContentMedia({
|
|
395
|
-
size = "xs",
|
|
396
|
-
...rest
|
|
397
|
-
}: SurfaceMediaProps): ReactElement {
|
|
398
|
-
const context = requiredContext(
|
|
399
|
-
useContext(ContentHeaderContext),
|
|
400
|
-
"ContentMedia",
|
|
401
|
-
"ContentHeader",
|
|
402
|
-
);
|
|
403
|
-
const props = adoptIconTile({ size, ...rest });
|
|
404
|
-
return (
|
|
405
|
-
<SurfaceMedia
|
|
406
|
-
slot="content-media"
|
|
407
|
-
{...props}
|
|
408
|
-
aria-hidden={context.element === "fieldset" ? true : props["aria-hidden"]}
|
|
409
|
-
/>
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export function ContentTitle({
|
|
414
|
-
className,
|
|
415
|
-
id,
|
|
416
|
-
...props
|
|
417
|
-
}: HTMLAttributes<HTMLElement>): ReactElement {
|
|
418
|
-
const context = requiredContext(
|
|
419
|
-
useContext(ContentHeaderContext),
|
|
420
|
-
"ContentTitle",
|
|
421
|
-
"ContentHeader",
|
|
422
|
-
);
|
|
423
|
-
const Heading =
|
|
424
|
-
context.element === "fieldset" ? "span" : headingTags[context.level];
|
|
425
|
-
usePageHeadingPresence(context.element !== "fieldset" && context.level === 1);
|
|
426
|
-
return (
|
|
427
|
-
<Heading
|
|
428
|
-
id={id ?? context.titleId}
|
|
429
|
-
data-slot="content-title"
|
|
430
|
-
className={cn(surfaceHeaderClasses[context.variant].title, className)}
|
|
431
|
-
{...props}
|
|
432
|
-
/>
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
export function ContentDescription({
|
|
437
|
-
className,
|
|
438
|
-
...props
|
|
439
|
-
}: HTMLAttributes<HTMLParagraphElement>): ReactElement {
|
|
440
|
-
const context = requiredContext(
|
|
441
|
-
useContext(ContentHeaderContext),
|
|
442
|
-
"ContentDescription",
|
|
443
|
-
"ContentHeader",
|
|
444
|
-
);
|
|
445
|
-
return (
|
|
446
|
-
<p
|
|
447
|
-
data-slot="content-description"
|
|
448
|
-
className={cn(
|
|
449
|
-
surfaceHeaderClasses[context.variant].description,
|
|
450
|
-
className,
|
|
451
|
-
)}
|
|
452
|
-
{...props}
|
|
453
|
-
/>
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/** O contador ao lado do título — o slot `count` do shorthand. */
|
|
458
|
-
export function ContentMeta({
|
|
459
|
-
className,
|
|
460
|
-
...props
|
|
461
|
-
}: HTMLAttributes<HTMLSpanElement>): ReactElement {
|
|
462
|
-
const context = requiredContext(
|
|
463
|
-
useContext(ContentHeaderContext),
|
|
464
|
-
"ContentMeta",
|
|
465
|
-
"ContentHeader",
|
|
466
|
-
);
|
|
467
|
-
return (
|
|
468
|
-
<span
|
|
469
|
-
data-slot="content-meta"
|
|
470
|
-
className={cn(surfaceHeaderClasses[context.variant].count, className)}
|
|
471
|
-
{...props}
|
|
472
|
-
/>
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
export function ContentActions({
|
|
477
|
-
className,
|
|
478
|
-
...props
|
|
479
|
-
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
480
|
-
const context = requiredContext(
|
|
481
|
-
useContext(ContentHeaderContext),
|
|
482
|
-
"ContentActions",
|
|
483
|
-
"ContentHeader",
|
|
484
|
-
);
|
|
485
|
-
return (
|
|
486
|
-
<div
|
|
487
|
-
data-slot="content-actions"
|
|
488
|
-
className={cn(
|
|
489
|
-
surfaceHeaderClasses[context.variant].actions,
|
|
490
|
-
context.variant === "page" && "ml-auto w-auto",
|
|
491
|
-
className,
|
|
492
|
-
)}
|
|
493
|
-
{...props}
|
|
494
|
-
/>
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
export function ContentBody({
|
|
499
|
-
className,
|
|
500
|
-
children,
|
|
501
|
-
...props
|
|
502
|
-
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
503
|
-
const content = requiredContext(
|
|
504
|
-
useContext(ContentContext),
|
|
505
|
-
"ContentBody",
|
|
506
|
-
"Content",
|
|
507
|
-
);
|
|
508
|
-
const fill = useFillRegion();
|
|
509
|
-
return (
|
|
510
|
-
<div
|
|
511
|
-
data-slot="content-body"
|
|
512
|
-
{...fill.attributes}
|
|
513
|
-
className={cn(
|
|
514
|
-
content.element === "fieldset" && "space-y-3",
|
|
515
|
-
content.element === "fieldset" &&
|
|
516
|
-
content.framed &&
|
|
517
|
-
content.layout === "continuous" &&
|
|
518
|
-
content.frameVariant === "default" &&
|
|
519
|
-
"clear-left pt-3",
|
|
520
|
-
content.element === "fieldset" &&
|
|
521
|
-
content.framed &&
|
|
522
|
-
content.layout === "sectioned" &&
|
|
523
|
-
"clear-left",
|
|
524
|
-
fill.filling && fillRegionClassName,
|
|
525
|
-
className,
|
|
526
|
-
)}
|
|
527
|
-
{...props}
|
|
528
|
-
>
|
|
529
|
-
{fill.provide(children)}
|
|
530
|
-
</div>
|
|
531
|
-
);
|
|
532
|
-
}
|