@softize/opus 21.0.1 → 22.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 +47 -0
- package/bin/lib/check.mjs +31 -28
- package/docs/adr/0004-page-content-state-is-composed.md +3 -2
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +3 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +23 -14
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -0
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +7 -4
- package/docs/adr/0016-list-collection-header-belongs-to-content.md +13 -8
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +27 -26
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +1 -0
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +71 -0
- package/docs/adr/0030-framed-content-can-use-sectioned-anatomy.md +89 -0
- package/docs/adr/0031-detail-dividers-own-the-tabular-label-treatment.md +61 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +59 -0
- package/docs/adr/0033-drawer-uses-an-inset-structural-frame.md +72 -0
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +60 -0
- package/docs/adr/0035-button-group-separates-frame-relationship-and-distribution.md +63 -0
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +58 -0
- package/docs/radius-scale.md +5 -2
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +9 -6
- package/registry/skills/build-opus-ui/references/evaluations.md +5 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -16
- package/src/ui/components/patterns/content-header.tsx +51 -13
- package/src/ui/components/patterns/list.tsx +51 -33
- package/src/ui/components/patterns/page.tsx +139 -251
- package/src/ui/components/patterns/presentation.tsx +2 -6
- package/src/ui/components/primitives/button-group.tsx +80 -8
- package/src/ui/components/primitives/button.tsx +85 -60
- package/src/ui/components/primitives/detail.tsx +63 -57
- package/src/ui/components/primitives/dialog.tsx +8 -11
- package/src/ui/components/primitives/drawer.tsx +16 -12
- package/src/ui/components/primitives/tabs.tsx +123 -54
- package/src/ui/docs/content/button.md +55 -29
- package/src/ui/docs/content/content.md +61 -36
- package/src/ui/docs/content/detail.md +28 -19
- package/src/ui/docs/content/drawer.md +53 -47
- package/src/ui/docs/content/input.md +51 -35
- package/src/ui/docs/content/page.md +84 -95
- package/src/ui/docs/content/pane.md +83 -63
- package/src/ui/docs/content/presentation.md +20 -20
- package/src/ui/docs/content/tabs.md +107 -36
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +0 -1
|
@@ -14,10 +14,8 @@ import { IconTile, type IconTileProps } from "../primitives/icon-tile.tsx";
|
|
|
14
14
|
import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
15
15
|
import { headingTags } from "./heading-level.ts";
|
|
16
16
|
import { usePageHeadingPresence } from "./page-heading-context.tsx";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
type SurfaceMediaProps,
|
|
20
|
-
} from "./surface-media.tsx";
|
|
17
|
+
import type { SurfaceLayout } from "./surface-anatomy.ts";
|
|
18
|
+
import { SurfaceMedia, type SurfaceMediaProps } from "./surface-media.tsx";
|
|
21
19
|
import {
|
|
22
20
|
SurfaceHeader,
|
|
23
21
|
surfaceHeaderClasses,
|
|
@@ -33,6 +31,7 @@ interface ContentContextValue {
|
|
|
33
31
|
element: ContentElement;
|
|
34
32
|
frameVariant: ContentFrameVariant;
|
|
35
33
|
framed: boolean;
|
|
34
|
+
layout: SurfaceLayout;
|
|
36
35
|
level: ContentHeaderLevel;
|
|
37
36
|
titleId: string;
|
|
38
37
|
variant: ContentHeaderVariant;
|
|
@@ -54,6 +53,8 @@ function requiredContext<T>(
|
|
|
54
53
|
interface ContentSharedProps {
|
|
55
54
|
/** Aplica a moldura estrutural da região sem transferi-la ao conteúdo interno. */
|
|
56
55
|
framed?: boolean;
|
|
56
|
+
/** Anatomia interna: contínua (default) ou com header separado do corpo. */
|
|
57
|
+
layout?: SurfaceLayout;
|
|
57
58
|
/** Hierarquia visual: `section` (default) ou `page`, a mesma de PageHeader. */
|
|
58
59
|
variant?: ContentHeaderVariant;
|
|
59
60
|
}
|
|
@@ -129,6 +130,7 @@ export function Content({
|
|
|
129
130
|
level = 2,
|
|
130
131
|
variant = "section",
|
|
131
132
|
framed = false,
|
|
133
|
+
layout = "continuous",
|
|
132
134
|
frameVariant = "default",
|
|
133
135
|
children,
|
|
134
136
|
className,
|
|
@@ -140,6 +142,7 @@ export function Content({
|
|
|
140
142
|
element: as,
|
|
141
143
|
frameVariant,
|
|
142
144
|
framed,
|
|
145
|
+
layout,
|
|
143
146
|
level: level as ContentHeaderLevel,
|
|
144
147
|
titleId,
|
|
145
148
|
variant,
|
|
@@ -147,6 +150,15 @@ export function Content({
|
|
|
147
150
|
const shorthand = title !== undefined;
|
|
148
151
|
const nodes = Children.toArray(children);
|
|
149
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
|
+
|
|
150
162
|
if (
|
|
151
163
|
shorthand &&
|
|
152
164
|
nodes.some((node) => isValidElement(node) && node.type === ContentHeader)
|
|
@@ -200,16 +212,22 @@ export function Content({
|
|
|
200
212
|
data-as="fieldset"
|
|
201
213
|
data-framed={framed}
|
|
202
214
|
data-frame-variant={frameVariant}
|
|
215
|
+
data-layout={layout}
|
|
203
216
|
{...fill.attributes}
|
|
204
217
|
aria-labelledby={ariaLabelledBy}
|
|
205
218
|
className={cn(
|
|
206
|
-
!framed && "space-y-3",
|
|
219
|
+
!framed && layout === "continuous" && "space-y-3",
|
|
207
220
|
framed &&
|
|
221
|
+
layout === "continuous" &&
|
|
208
222
|
frameVariant === "default" &&
|
|
209
|
-
"rounded-
|
|
223
|
+
"rounded-xl border border-border p-4",
|
|
210
224
|
framed &&
|
|
225
|
+
layout === "continuous" &&
|
|
211
226
|
frameVariant === "legend" &&
|
|
212
|
-
"space-y-3 rounded-
|
|
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",
|
|
213
231
|
fill.filling && fillRegionClassName,
|
|
214
232
|
className,
|
|
215
233
|
)}
|
|
@@ -223,16 +241,22 @@ export function Content({
|
|
|
223
241
|
data-as="section"
|
|
224
242
|
data-framed={framed}
|
|
225
243
|
data-frame-variant={frameVariant}
|
|
244
|
+
data-layout={layout}
|
|
226
245
|
{...fill.attributes}
|
|
227
246
|
aria-labelledby={ariaLabelledBy}
|
|
228
247
|
className={cn(
|
|
229
|
-
"space-y-3",
|
|
248
|
+
layout === "continuous" && "space-y-3",
|
|
230
249
|
framed &&
|
|
250
|
+
layout === "continuous" &&
|
|
231
251
|
frameVariant === "default" &&
|
|
232
|
-
"rounded-
|
|
252
|
+
"rounded-xl border border-border p-4",
|
|
233
253
|
framed &&
|
|
254
|
+
layout === "continuous" &&
|
|
234
255
|
frameVariant === "legend" &&
|
|
235
|
-
"rounded-
|
|
256
|
+
"rounded-xl border border-border px-4 pb-4",
|
|
257
|
+
framed &&
|
|
258
|
+
layout === "sectioned" &&
|
|
259
|
+
"overflow-hidden rounded-xl border border-border",
|
|
236
260
|
fill.filling && fillRegionClassName,
|
|
237
261
|
className,
|
|
238
262
|
)}
|
|
@@ -273,11 +297,16 @@ export function ContentHeader({
|
|
|
273
297
|
invalidMessage="ContentHeader de fieldset aceita somente ContentMedia e ContentTitle; contador, descrição e ações ficam fora da legenda."
|
|
274
298
|
className={cn(
|
|
275
299
|
content.framed &&
|
|
300
|
+
content.layout === "continuous" &&
|
|
276
301
|
content.frameVariant === "default" &&
|
|
277
302
|
"float-left w-full",
|
|
278
303
|
content.framed &&
|
|
304
|
+
content.layout === "continuous" &&
|
|
279
305
|
content.frameVariant === "legend" &&
|
|
280
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",
|
|
281
310
|
className,
|
|
282
311
|
)}
|
|
283
312
|
{...props}
|
|
@@ -302,16 +331,22 @@ export function ContentHeader({
|
|
|
302
331
|
mediaPlacement="heading"
|
|
303
332
|
className={cn(
|
|
304
333
|
content.framed &&
|
|
334
|
+
content.layout === "continuous" &&
|
|
305
335
|
content.frameVariant === "legend" &&
|
|
306
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",
|
|
307
337
|
content.framed &&
|
|
338
|
+
content.layout === "continuous" &&
|
|
308
339
|
content.frameVariant === "legend" &&
|
|
309
340
|
content.variant === "section" &&
|
|
310
341
|
"-mt-4 min-h-8",
|
|
311
342
|
content.framed &&
|
|
343
|
+
content.layout === "continuous" &&
|
|
312
344
|
content.frameVariant === "legend" &&
|
|
313
345
|
content.variant === "page" &&
|
|
314
346
|
"-mt-5 min-h-10",
|
|
347
|
+
content.framed &&
|
|
348
|
+
content.layout === "sectioned" &&
|
|
349
|
+
"border-b border-border p-3",
|
|
315
350
|
content.variant === "page" &&
|
|
316
351
|
"flex-row items-center justify-start gap-3",
|
|
317
352
|
className,
|
|
@@ -370,9 +405,7 @@ export function ContentMedia({
|
|
|
370
405
|
<SurfaceMedia
|
|
371
406
|
slot="content-media"
|
|
372
407
|
{...props}
|
|
373
|
-
aria-hidden={
|
|
374
|
-
context.element === "fieldset" ? true : props["aria-hidden"]
|
|
375
|
-
}
|
|
408
|
+
aria-hidden={context.element === "fieldset" ? true : props["aria-hidden"]}
|
|
376
409
|
/>
|
|
377
410
|
);
|
|
378
411
|
}
|
|
@@ -481,8 +514,13 @@ export function ContentBody({
|
|
|
481
514
|
content.element === "fieldset" && "space-y-3",
|
|
482
515
|
content.element === "fieldset" &&
|
|
483
516
|
content.framed &&
|
|
517
|
+
content.layout === "continuous" &&
|
|
484
518
|
content.frameVariant === "default" &&
|
|
485
519
|
"clear-left pt-3",
|
|
520
|
+
content.element === "fieldset" &&
|
|
521
|
+
content.framed &&
|
|
522
|
+
content.layout === "sectioned" &&
|
|
523
|
+
"clear-left",
|
|
486
524
|
fill.filling && fillRegionClassName,
|
|
487
525
|
className,
|
|
488
526
|
)}
|
|
@@ -993,26 +993,35 @@ function PeriodControl({
|
|
|
993
993
|
variant={embedded ? "ghost" : "outline"}
|
|
994
994
|
data-slot="action-list-period"
|
|
995
995
|
className={cn(
|
|
996
|
-
"max-w-full gap-1.5 overflow-hidden bg-muted/30 px-3 hover:bg-muted/30 hover:text-foreground dark:bg-input/30 dark:hover:bg-input/30",
|
|
997
|
-
|
|
996
|
+
"relative max-w-full gap-1.5 overflow-hidden bg-muted/30 px-3 hover:bg-muted/30 hover:text-foreground dark:bg-input/30 dark:hover:bg-input/30",
|
|
997
|
+
// O X e o chevron ocupam posições explícitas na ponta do controle. O
|
|
998
|
+
// padding reserva os dois sem depender da largura do texto selecionado.
|
|
999
|
+
clearable && "pr-16",
|
|
998
1000
|
embedded &&
|
|
999
1001
|
"gap-1 rounded-none border-0 bg-transparent pl-0 pr-0 font-normal shadow-none hover:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-transparent dark:hover:bg-transparent",
|
|
1000
1002
|
)}
|
|
1001
1003
|
>
|
|
1002
|
-
{embedded ? null :
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
<span className="shrink-0 opacity-50">
|
|
1006
|
-
<ChevronDown className="size-4" aria-hidden="true" />
|
|
1004
|
+
{embedded ? null : (
|
|
1005
|
+
<span className="shrink-0">
|
|
1006
|
+
<CalendarIcon className="h-3.5 w-3.5" />
|
|
1007
1007
|
</span>
|
|
1008
|
-
)
|
|
1008
|
+
)}
|
|
1009
|
+
<span className="min-w-0 truncate">{label}</span>
|
|
1010
|
+
<span
|
|
1011
|
+
className={cn(
|
|
1012
|
+
"shrink-0 opacity-50",
|
|
1013
|
+
clearable && "pointer-events-none absolute right-8",
|
|
1014
|
+
)}
|
|
1015
|
+
>
|
|
1016
|
+
<ChevronDown className="size-4" aria-hidden="true" />
|
|
1017
|
+
</span>
|
|
1009
1018
|
</Button>
|
|
1010
1019
|
</PopoverTrigger>
|
|
1011
1020
|
{clearable ? (
|
|
1012
1021
|
<InputGroupButton
|
|
1013
1022
|
size="icon-xs"
|
|
1014
1023
|
aria-label="Limpar período"
|
|
1015
|
-
className="absolute top-1/2 right-
|
|
1024
|
+
className="absolute top-1/2 right-2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
1016
1025
|
onClick={() => {
|
|
1017
1026
|
setOpen(false);
|
|
1018
1027
|
setShowCalendar(false);
|
|
@@ -1422,7 +1431,7 @@ export function ActionFilterBar({
|
|
|
1422
1431
|
<div
|
|
1423
1432
|
data-slot="action-filter-period-group"
|
|
1424
1433
|
className={cn(
|
|
1425
|
-
"flex min-h-9 w-fit min-w-0 max-w-full items-center rounded-md border border-input bg-muted/30 text-foreground transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[0.1875rem] focus-within:ring-ring/50 dark:bg-input/30",
|
|
1434
|
+
"flex min-h-9 w-fit min-w-0 max-w-full items-center rounded-md border border-input bg-muted/30 text-foreground transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[0.1875rem] focus-within:ring-ring/50 has-[[data-state=open]]:border-ring has-[[data-state=open]]:ring-[0.1875rem] has-[[data-state=open]]:ring-ring/50 dark:bg-input/30",
|
|
1426
1435
|
!periodGroupClearable && "pr-3",
|
|
1427
1436
|
)}
|
|
1428
1437
|
>
|
|
@@ -1430,6 +1439,31 @@ export function ActionFilterBar({
|
|
|
1430
1439
|
className="ml-3 size-3.5 shrink-0 text-muted-foreground"
|
|
1431
1440
|
aria-hidden="true"
|
|
1432
1441
|
/>
|
|
1442
|
+
<div className="pl-2">
|
|
1443
|
+
<PeriodControl
|
|
1444
|
+
periods={periods}
|
|
1445
|
+
period={activePeriod}
|
|
1446
|
+
from={state.from}
|
|
1447
|
+
to={state.to}
|
|
1448
|
+
clearable={false}
|
|
1449
|
+
embedded
|
|
1450
|
+
onChange={(next) =>
|
|
1451
|
+
onStateChange({
|
|
1452
|
+
...state,
|
|
1453
|
+
...next,
|
|
1454
|
+
period:
|
|
1455
|
+
next.period === defaultPeriod
|
|
1456
|
+
? null
|
|
1457
|
+
: next.period,
|
|
1458
|
+
})
|
|
1459
|
+
}
|
|
1460
|
+
/>
|
|
1461
|
+
</div>
|
|
1462
|
+
<Separator
|
|
1463
|
+
data-slot="action-filter-period-separator"
|
|
1464
|
+
orientation="vertical"
|
|
1465
|
+
className="mx-2 self-center bg-input data-[orientation=vertical]:h-4"
|
|
1466
|
+
/>
|
|
1433
1467
|
<FilterField
|
|
1434
1468
|
name={name}
|
|
1435
1469
|
spec={spec}
|
|
@@ -1439,28 +1473,7 @@ export function ActionFilterBar({
|
|
|
1439
1473
|
options={optionsFor(name, spec, filterOptions)}
|
|
1440
1474
|
defaultValue={filterDefaults?.[name]}
|
|
1441
1475
|
embedded
|
|
1442
|
-
className="w-fit min-w-0 max-w-80 [&_[data-slot=select-control]]:gap-1 [&_[data-slot=select-control]]:rounded-none [&_[data-slot=select-control]]:bg-transparent [&_[data-slot=select-control]]:pl-
|
|
1443
|
-
/>
|
|
1444
|
-
<Separator
|
|
1445
|
-
data-slot="action-filter-period-separator"
|
|
1446
|
-
orientation="vertical"
|
|
1447
|
-
className="ml-1.5 mr-2.5 h-4 shrink-0"
|
|
1448
|
-
/>
|
|
1449
|
-
<PeriodControl
|
|
1450
|
-
periods={periods}
|
|
1451
|
-
period={activePeriod}
|
|
1452
|
-
from={state.from}
|
|
1453
|
-
to={state.to}
|
|
1454
|
-
clearable={false}
|
|
1455
|
-
embedded
|
|
1456
|
-
onChange={(next) =>
|
|
1457
|
-
onStateChange({
|
|
1458
|
-
...state,
|
|
1459
|
-
...next,
|
|
1460
|
-
period:
|
|
1461
|
-
next.period === defaultPeriod ? null : next.period,
|
|
1462
|
-
})
|
|
1463
|
-
}
|
|
1476
|
+
className="w-fit min-w-0 max-w-80 [&_[data-slot=select-control]]:gap-1 [&_[data-slot=select-control]]:rounded-none [&_[data-slot=select-control]]:bg-transparent [&_[data-slot=select-control]]:pl-0 [&_[data-slot=select-control]]:pr-0 [&_[data-slot=select-control]]:font-normal [&_[data-slot=select-control]]:text-foreground [&_[data-slot=select-control]]:shadow-none [&_[data-slot=select-control]:hover]:bg-transparent [&_[data-slot=select-control]:focus-within]:ring-0 [&_[data-slot=select-control]:focus-within]:ring-offset-0"
|
|
1464
1477
|
/>
|
|
1465
1478
|
{periodGroupClearable ? (
|
|
1466
1479
|
<InputGroupButton
|
|
@@ -2381,7 +2394,12 @@ export function ActionList<TInput, TItem>({
|
|
|
2381
2394
|
linhas: a dos resultados absorve a altura que sobra, a do rodapé se mede
|
|
2382
2395
|
pelo conteúdo. Em `flow` a primeira linha também é `1fr`, mas o bloco não
|
|
2383
2396
|
tem altura própria e ela vira a altura do conteúdo. */}
|
|
2384
|
-
<div
|
|
2397
|
+
<div
|
|
2398
|
+
className={cn(
|
|
2399
|
+
"grid min-h-0 grid-rows-[1fr_auto]",
|
|
2400
|
+
layout === "fill" && "flex-1",
|
|
2401
|
+
)}
|
|
2402
|
+
>
|
|
2385
2403
|
{/* A rolagem mora aqui, nos dois eixos: é este elemento que o `sticky` do
|
|
2386
2404
|
cabeçalho enxerga. Um contêiner de rolagem entre ele e a tabela roubaria
|
|
2387
2405
|
essa referência e o cabeçalho voltaria a subir junto com as linhas.
|