@softize/opus 27.1.0 → 29.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 +113 -0
- package/bin/cli.mjs +68 -3
- package/bin/lib/check.mjs +139 -30
- package/bin/lib/copy.mjs +505 -67
- package/bin/lib/materialize.mjs +99 -1
- package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
- package/docs/adr/0028-schedules-run-actions-as-the-system-by-action-opt-in.md +1 -1
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
- package/docs/adr/0061-claude-md-imports-agents-md.md +41 -0
- package/docs/adr/README.md +4 -0
- package/docs/code-style.md +47 -8
- package/docs/protocol.md +1 -1
- package/docs/releasing.md +7 -0
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -1
- package/registry/skills/build-opus-ui/SKILL.md +16 -14
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
- package/registry/skills/maintain-opus-docs/SKILL.md +2 -1
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +9 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +8 -0
- package/registry/templates/app/pnpm-workspace.yaml +1 -1
- package/src/core/system-actor.ts +1 -1
- package/src/ui/components/patterns/list.tsx +63 -54
- package/src/ui/components/patterns/page.tsx +146 -38
- package/src/ui/components/patterns/presentation.tsx +44 -59
- package/src/ui/components/patterns/structural-header.tsx +42 -6
- package/src/ui/components/patterns/surface-anatomy.ts +52 -22
- package/src/ui/components/patterns/surface.tsx +2 -3
- package/src/ui/components/primitives/control.ts +2 -2
- package/src/ui/components/primitives/dialog.tsx +7 -2
- package/src/ui/components/primitives/drawer.tsx +7 -2
- package/src/ui/components/primitives/icon-picker.tsx +271 -34
- package/src/ui/components/primitives/pagination.tsx +5 -4
- package/src/ui/components/primitives/tabs.tsx +15 -2
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/docs/content/action-list.md +12 -3
- package/src/ui/docs/content/actions.md +1 -1
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/button.md +7 -7
- package/src/ui/docs/content/cli.md +6 -0
- package/src/ui/docs/content/communication.md +10 -8
- package/src/ui/docs/content/compositions.md +35 -23
- package/src/ui/docs/content/dialog.md +18 -6
- package/src/ui/docs/content/drawer.md +17 -5
- package/src/ui/docs/content/events.md +13 -13
- package/src/ui/docs/content/icon-picker.md +37 -19
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/installation.md +4 -0
- package/src/ui/docs/content/log.md +1 -1
- package/src/ui/docs/content/page.md +169 -69
- package/src/ui/docs/content/pagination.md +7 -8
- package/src/ui/docs/content/presentation.md +4 -3
- package/src/ui/docs/content/queue.md +2 -2
- package/src/ui/docs/content/router.md +1 -1
- package/src/ui/docs/content/scheduler.md +15 -15
- package/src/ui/docs/content/surface.md +8 -5
- package/src/ui/docs/content/tabs.md +12 -4
- package/src/ui/docs/content/testing.md +4 -4
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +9 -1
- package/src/vite/design.ts +1 -1
|
@@ -110,7 +110,9 @@ export function StructuralHeader({
|
|
|
110
110
|
...props
|
|
111
111
|
}: StructuralHeaderProps): ReactElement {
|
|
112
112
|
const Wrapper = Root === "legend" ? "span" : "div";
|
|
113
|
-
const nodes = flatten(children)
|
|
113
|
+
const nodes = flatten(children).filter(
|
|
114
|
+
(node) => typeof node !== "string" || node.trim() !== "",
|
|
115
|
+
);
|
|
114
116
|
const of = (type: ElementType) =>
|
|
115
117
|
nodes.filter((node) => isValidElement(node) && node.type === type);
|
|
116
118
|
const leadingTypes: readonly ElementType[] =
|
|
@@ -305,6 +307,7 @@ export function StructuralHeader({
|
|
|
305
307
|
className={cn(
|
|
306
308
|
actionSlots.length > 0 &&
|
|
307
309
|
"flex flex-row items-end justify-between gap-4",
|
|
310
|
+
rowClassName,
|
|
308
311
|
)}
|
|
309
312
|
>
|
|
310
313
|
{heading}
|
|
@@ -314,16 +317,24 @@ export function StructuralHeader({
|
|
|
314
317
|
) : leading.length > 0 ? (
|
|
315
318
|
<Wrapper
|
|
316
319
|
data-slot={`${slot}-header-row`}
|
|
317
|
-
className="flex min-w-0 items-center gap-4"
|
|
320
|
+
className={cn("flex min-w-0 items-center gap-4", rowClassName)}
|
|
318
321
|
>
|
|
319
|
-
<Wrapper
|
|
320
|
-
data-slot={`${slot}-navigation`}
|
|
321
|
-
className="min-w-0 flex-1"
|
|
322
|
-
>
|
|
322
|
+
<Wrapper data-slot={`${slot}-navigation`} className="min-w-0 flex-1">
|
|
323
323
|
{leading}
|
|
324
324
|
</Wrapper>
|
|
325
325
|
{actionSlots}
|
|
326
326
|
</Wrapper>
|
|
327
|
+
) : rowClassName !== undefined ? (
|
|
328
|
+
<Wrapper
|
|
329
|
+
data-slot={`${slot}-header-row`}
|
|
330
|
+
className={cn(
|
|
331
|
+
rowClassName,
|
|
332
|
+
heading ? "justify-between" : "justify-end",
|
|
333
|
+
)}
|
|
334
|
+
>
|
|
335
|
+
{heading}
|
|
336
|
+
{actionSlots}
|
|
337
|
+
</Wrapper>
|
|
327
338
|
) : (
|
|
328
339
|
<>
|
|
329
340
|
{heading}
|
|
@@ -360,3 +371,28 @@ export function StructuralHeader({
|
|
|
360
371
|
</Root>
|
|
361
372
|
);
|
|
362
373
|
}
|
|
374
|
+
|
|
375
|
+
/** A intro visível abre o corpo; wrappers opacos são verificados quando materializam seus slots. */
|
|
376
|
+
export function validateSurfaceIntro(
|
|
377
|
+
children: ReactNode,
|
|
378
|
+
intro: ElementType,
|
|
379
|
+
name: string,
|
|
380
|
+
): void {
|
|
381
|
+
const flatten = (nodes: ReactNode): ReactNode[] =>
|
|
382
|
+
Children.toArray(nodes).flatMap((node) =>
|
|
383
|
+
isValidElement(node) && node.type === Fragment
|
|
384
|
+
? flatten((node.props as { children?: ReactNode }).children)
|
|
385
|
+
: [node],
|
|
386
|
+
);
|
|
387
|
+
const nodes = flatten(children).filter(
|
|
388
|
+
(node) => typeof node !== "string" || node.trim() !== "",
|
|
389
|
+
);
|
|
390
|
+
const indexes = nodes.flatMap((node, index) =>
|
|
391
|
+
isValidElement(node) && node.type === intro ? [index] : [],
|
|
392
|
+
);
|
|
393
|
+
if (indexes.length > 1 || (indexes.length === 1 && indexes[0] !== 0)) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
`${name} aceita uma única intro como primeiro bloco do corpo.`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "react";
|
|
28
28
|
import { FillBoundary } from "./fill-region.ts";
|
|
29
29
|
|
|
30
|
-
export type SurfaceFamily = "surface" | "dialog" | "drawer";
|
|
30
|
+
export type SurfaceFamily = "surface" | "dialog" | "drawer" | "page";
|
|
31
31
|
export type SurfaceLayout = "continuous" | "sectioned";
|
|
32
32
|
export type SurfaceRegion = "header" | "body" | "footer";
|
|
33
33
|
|
|
@@ -64,18 +64,22 @@ interface FamilyAnatomy {
|
|
|
64
64
|
description: string;
|
|
65
65
|
/** Espaço entre mídia, título e ações na linha do header. */
|
|
66
66
|
headerGap: string;
|
|
67
|
+
/** Altura mínima da linha, equivalente ao controle padrão da família. */
|
|
68
|
+
headerMinHeight: string;
|
|
67
69
|
/** Faixa do rodapé no layout seccionado, quando a família declara uma. */
|
|
68
70
|
footerBand: string;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
74
|
* O recuo seccionado de Dialog e Drawer: 1rem, a densidade das superfícies de operação.
|
|
73
|
-
*
|
|
74
|
-
* faixas de header e footer mais baixas.
|
|
75
|
+
* O header é comum à família; corpos e rodapés conservam o ritmo de cada superfície.
|
|
75
76
|
*/
|
|
77
|
+
const structuralHeaderPadding = "p-4";
|
|
78
|
+
const structuralHeaderMinHeight = "min-h-8";
|
|
79
|
+
|
|
76
80
|
const sectionedRegions: RegionAnatomy = {
|
|
77
|
-
header:
|
|
78
|
-
body: "
|
|
81
|
+
header: structuralHeaderPadding,
|
|
82
|
+
body: "p-4",
|
|
79
83
|
footer: "p-4",
|
|
80
84
|
};
|
|
81
85
|
|
|
@@ -83,25 +87,47 @@ const sectionedRegions: RegionAnatomy = {
|
|
|
83
87
|
* Ritmo entre blocos diretos do conteúdo; relações internas pertencem a um wrapper filho.
|
|
84
88
|
* Os blocos não encolhem: o body é quem rola quando a família tem altura limitada.
|
|
85
89
|
*/
|
|
86
|
-
const structuralBodyRhythm = "flex flex-col gap-
|
|
90
|
+
export const structuralBodyRhythm = "flex flex-col gap-4 *:shrink-0";
|
|
91
|
+
export const structuralRegionSpacing = "space-y-4";
|
|
92
|
+
export const structuralRegionGap = "gap-4";
|
|
87
93
|
|
|
88
94
|
export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
95
|
+
page: {
|
|
96
|
+
regions: {
|
|
97
|
+
continuous: {
|
|
98
|
+
header: structuralHeaderPadding,
|
|
99
|
+
body: "px-8 pt-0 pb-0 [[data-slot=page-body]:first-child>&]:pt-4",
|
|
100
|
+
footer: "px-8 py-4",
|
|
101
|
+
},
|
|
102
|
+
sectioned: {
|
|
103
|
+
header: sectionedRegions.header,
|
|
104
|
+
body: "px-8 pt-4 pb-0 [[data-slot=page-body]:has(+[data-slot=page-footer])>&]:pb-4",
|
|
105
|
+
footer: "px-8 py-4",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
title: {
|
|
109
|
+
continuous: "text-2xl font-semibold",
|
|
110
|
+
sectioned: "text-2xl font-semibold",
|
|
111
|
+
},
|
|
112
|
+
description: "text-sm text-muted-foreground",
|
|
113
|
+
headerGap: "gap-4",
|
|
114
|
+
headerMinHeight: structuralHeaderMinHeight,
|
|
115
|
+
footerBand: "bg-muted/30",
|
|
116
|
+
},
|
|
89
117
|
// Medidas da Surface delimitada (com moldura ou elevação). Sem delimitação, as regiões
|
|
90
118
|
// não recebem recuo: a superfície flui alinhada ao hospedeiro (ADR 0045).
|
|
91
119
|
surface: {
|
|
92
120
|
regions: {
|
|
93
|
-
//
|
|
94
|
-
// reassume o respiro compacto da faixa; quando o body abre ou fecha a superfície,
|
|
95
|
-
// recebe o respiro inteiro do conteúdo.
|
|
121
|
+
// No contínuo, a região anterior fornece o intervalo; a primeira abre o recuo.
|
|
96
122
|
continuous: {
|
|
97
|
-
header:
|
|
98
|
-
body: "px-4 pt-0 pb-
|
|
123
|
+
header: structuralHeaderPadding,
|
|
124
|
+
body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
99
125
|
footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
100
126
|
},
|
|
101
127
|
sectioned: {
|
|
102
|
-
header:
|
|
103
|
-
body: "
|
|
104
|
-
footer: "
|
|
128
|
+
header: structuralHeaderPadding,
|
|
129
|
+
body: "p-4",
|
|
130
|
+
footer: "p-4",
|
|
105
131
|
},
|
|
106
132
|
},
|
|
107
133
|
// Uma escala só: o nível 1 troca a apresentação inteira (ADR 0038), nunca o layout.
|
|
@@ -111,14 +137,15 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
111
137
|
},
|
|
112
138
|
description: "mt-0.5 text-xs text-muted-foreground",
|
|
113
139
|
headerGap: "gap-2",
|
|
140
|
+
headerMinHeight: structuralHeaderMinHeight,
|
|
114
141
|
footerBand: "",
|
|
115
142
|
},
|
|
116
143
|
dialog: {
|
|
117
144
|
regions: {
|
|
118
145
|
continuous: {
|
|
119
|
-
header:
|
|
120
|
-
body: "px-4
|
|
121
|
-
footer: "
|
|
146
|
+
header: structuralHeaderPadding,
|
|
147
|
+
body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
148
|
+
footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
122
149
|
},
|
|
123
150
|
sectioned: sectionedRegions,
|
|
124
151
|
},
|
|
@@ -128,14 +155,15 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
128
155
|
},
|
|
129
156
|
description: "text-sm text-muted-foreground",
|
|
130
157
|
headerGap: "gap-4",
|
|
158
|
+
headerMinHeight: structuralHeaderMinHeight,
|
|
131
159
|
footerBand: "bg-muted/20 dark:bg-input/20",
|
|
132
160
|
},
|
|
133
161
|
drawer: {
|
|
134
162
|
regions: {
|
|
135
163
|
continuous: {
|
|
136
|
-
header:
|
|
137
|
-
body: "px-4
|
|
138
|
-
footer: "
|
|
164
|
+
header: structuralHeaderPadding,
|
|
165
|
+
body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
166
|
+
footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
139
167
|
},
|
|
140
168
|
sectioned: sectionedRegions,
|
|
141
169
|
},
|
|
@@ -145,6 +173,7 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
145
173
|
},
|
|
146
174
|
description: "text-sm text-muted-foreground",
|
|
147
175
|
headerGap: "gap-4",
|
|
176
|
+
headerMinHeight: structuralHeaderMinHeight,
|
|
148
177
|
footerBand: "bg-muted/20 dark:bg-input/20",
|
|
149
178
|
},
|
|
150
179
|
};
|
|
@@ -190,7 +219,7 @@ export function surfaceDescriptionClasses(family: SurfaceFamily): string {
|
|
|
190
219
|
* `flex-1` mantém o heading como região elástica e as ações no extremo oposto.
|
|
191
220
|
*/
|
|
192
221
|
export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
|
|
193
|
-
return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap}`;
|
|
222
|
+
return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap} ${surfaceAnatomy[family].headerMinHeight}`;
|
|
194
223
|
}
|
|
195
224
|
|
|
196
225
|
/** Ações do header e do footer: fora do fluxo de compressão, na escala densa. */
|
|
@@ -207,7 +236,7 @@ export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
|
|
|
207
236
|
*/
|
|
208
237
|
export const surfaceLegendClasses = {
|
|
209
238
|
notch: "ml-2 w-auto px-2",
|
|
210
|
-
afterNotch: "pt-
|
|
239
|
+
afterNotch: "pt-4",
|
|
211
240
|
band: "float-left w-full",
|
|
212
241
|
afterBand: "clear-left",
|
|
213
242
|
} as const;
|
|
@@ -241,6 +270,7 @@ export function SurfaceLayoutProvider({
|
|
|
241
270
|
}
|
|
242
271
|
|
|
243
272
|
const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
|
|
273
|
+
page: "sectioned",
|
|
244
274
|
surface: "continuous",
|
|
245
275
|
dialog: "sectioned",
|
|
246
276
|
drawer: "sectioned",
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
surfaceHeaderRowClasses,
|
|
50
50
|
surfaceLegendClasses,
|
|
51
51
|
surfaceRegionClasses,
|
|
52
|
+
structuralRegionSpacing,
|
|
52
53
|
surfaceTitleClasses,
|
|
53
54
|
type SurfaceLayout,
|
|
54
55
|
} from "./surface-anatomy.ts";
|
|
@@ -343,9 +344,7 @@ export function Surface({
|
|
|
343
344
|
framed && "has-[>[data-flush=true]]:overflow-hidden",
|
|
344
345
|
raised &&
|
|
345
346
|
"bg-card text-card-foreground [--surface-background:var(--card)]",
|
|
346
|
-
structured &&
|
|
347
|
-
!bounded &&
|
|
348
|
-
(variant === "page" ? "space-y-6" : "space-y-3"),
|
|
347
|
+
structured && !bounded && structuralRegionSpacing,
|
|
349
348
|
link && fill.filling && fillRegionClassName,
|
|
350
349
|
className,
|
|
351
350
|
)}
|
|
@@ -7,7 +7,7 @@ export type ControlShape = 'default' | 'pill'
|
|
|
7
7
|
* Cada componente aceita o subconjunto coerente com seu papel, nunca um nome com outra medida.
|
|
8
8
|
*
|
|
9
9
|
* - Altura de linha (`xs` · `sm` · `default` · `lg`): 1.5 · 2 · 2.25 · 2.5rem.
|
|
10
|
-
* - Quadrado só-ícone (`icon-xs` · `icon-sm` · `icon` · `icon-lg`): 1.5 ·
|
|
10
|
+
* - Quadrado só-ícone (`icon-xs` · `icon-sm` · `icon` · `icon-lg`): 1.5 · 2 · 2.25 · 2.5rem.
|
|
11
11
|
* `icon-xs` é o degrau da ação que mora dentro de uma linha densa. `icon-sm` atende
|
|
12
12
|
* composições compactas que ainda precisam de mais presença.
|
|
13
13
|
* - Glifo (o svg dentro do controle daquele tamanho): 0.875 · 0.875 · 1 · 1.25rem. O quadrado
|
|
@@ -27,7 +27,7 @@ export const controlHeight: Record<ControlSize, string> = {
|
|
|
27
27
|
|
|
28
28
|
export const controlSquare: Record<ControlIconSize, string> = {
|
|
29
29
|
'icon-xs': 'size-6',
|
|
30
|
-
'icon-sm': 'size-
|
|
30
|
+
'icon-sm': 'size-8',
|
|
31
31
|
icon: 'size-9',
|
|
32
32
|
'icon-lg': 'size-10',
|
|
33
33
|
}
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
} from "../patterns/surface-anatomy.ts";
|
|
15
15
|
import { FillBoundary } from "../patterns/fill-region.ts";
|
|
16
16
|
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
StructuralHeader,
|
|
19
|
+
structuralHeaderClasses,
|
|
20
|
+
validateSurfaceIntro,
|
|
21
|
+
} from "../patterns/structural-header.tsx";
|
|
18
22
|
import {
|
|
19
23
|
StructuralMedia,
|
|
20
24
|
type StructuralMediaProps,
|
|
@@ -698,6 +702,7 @@ export function DialogBody({
|
|
|
698
702
|
}: React.ComponentProps<"div"> & { flush?: boolean }) {
|
|
699
703
|
const { mode } = useDialogContext();
|
|
700
704
|
const layout = useSurfaceLayout("dialog");
|
|
705
|
+
validateSurfaceIntro(props.children, DialogIntro, "DialogBody");
|
|
701
706
|
return (
|
|
702
707
|
<div
|
|
703
708
|
data-slot="dialog-body"
|
|
@@ -761,7 +766,7 @@ export function DialogTitle({
|
|
|
761
766
|
? "min-w-0 whitespace-normal"
|
|
762
767
|
: "min-w-0 truncate",
|
|
763
768
|
intro
|
|
764
|
-
?
|
|
769
|
+
? structuralHeaderClasses.page.title
|
|
765
770
|
: surfaceTitleClasses("dialog", layout),
|
|
766
771
|
className,
|
|
767
772
|
);
|
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
type SurfaceLayout,
|
|
14
14
|
} from "../patterns/surface-anatomy.ts";
|
|
15
15
|
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
StructuralHeader,
|
|
18
|
+
structuralHeaderClasses,
|
|
19
|
+
validateSurfaceIntro,
|
|
20
|
+
} from "../patterns/structural-header.tsx";
|
|
17
21
|
import {
|
|
18
22
|
StructuralMedia,
|
|
19
23
|
type StructuralMediaProps,
|
|
@@ -361,6 +365,7 @@ function DrawerBody({
|
|
|
361
365
|
...props
|
|
362
366
|
}: React.ComponentProps<"div"> & { flush?: boolean }) {
|
|
363
367
|
const layout = useSurfaceLayout("drawer");
|
|
368
|
+
validateSurfaceIntro(props.children, DrawerIntro, "DrawerBody");
|
|
364
369
|
return (
|
|
365
370
|
<div
|
|
366
371
|
data-slot="drawer-body"
|
|
@@ -408,7 +413,7 @@ function DrawerTitle({
|
|
|
408
413
|
const classes = cn(
|
|
409
414
|
intro ? "min-w-0 whitespace-normal" : "min-w-0 truncate",
|
|
410
415
|
intro
|
|
411
|
-
?
|
|
416
|
+
? structuralHeaderClasses.page.title
|
|
412
417
|
: surfaceTitleClasses("drawer", layout),
|
|
413
418
|
className,
|
|
414
419
|
);
|