@softize/opus 16.1.0 → 17.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 +30 -0
- package/bin/lib/check.mjs +9 -13
- package/bin/lib/copy.mjs +29 -4
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -3
- package/docs/adr/0009-page-title-does-not-carry-a-counter.md +5 -2
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +12 -14
- package/docs/adr/0012-modal-header-only-names-the-surface.md +8 -4
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +7 -4
- package/docs/adr/0014-structural-headers-do-not-carry-description.md +35 -0
- package/package.json +2 -2
- package/registry/skills/build-opus-ui/references/evaluations.md +1 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +16 -17
- package/registry/templates/app/package.json +1 -1
- package/src/core/presentation.ts +142 -13
- package/src/ui/components/patterns/form.tsx +29 -10
- package/src/ui/components/patterns/page.tsx +93 -48
- package/src/ui/components/patterns/presentation.tsx +459 -329
- package/src/ui/components/patterns/surface-header.tsx +4 -3
- package/src/ui/components/patterns/trigger.tsx +8 -1
- package/src/ui/components/patterns/view.tsx +2 -2
- package/src/ui/components/primitives/command.tsx +82 -42
- package/src/ui/components/primitives/dialog.tsx +180 -97
- package/src/ui/components/primitives/drawer.tsx +63 -22
- package/src/ui/docs/content/command.md +3 -1
- package/src/ui/docs/content/content.md +1 -1
- package/src/ui/docs/content/dialog.md +9 -9
- package/src/ui/docs/content/drawer.md +4 -4
- package/src/ui/docs/content/page.md +14 -29
- package/src/ui/docs/content/presentation.md +54 -76
- package/src/ui/meta.ts +2 -2
- package/src/ui/react.tsx +1 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cabeçalho de superfície (interno): a anatomia que Page e Content compartilham.
|
|
3
3
|
*
|
|
4
|
-
* Um título, metadata opcional
|
|
4
|
+
* Um título, metadata opcional e uma região de ações que vai para o extremo oposto
|
|
5
5
|
* em larguras amplas e empilha abaixo do contexto nas estreitas. Cada família escolhe quais slots
|
|
6
6
|
* expõe; layout, validação e classes vivem aqui, uma vez.
|
|
7
7
|
*/
|
|
@@ -42,7 +42,7 @@ interface SurfaceHeaderSlots {
|
|
|
42
42
|
leading?: ElementType | readonly ElementType[];
|
|
43
43
|
title: ElementType;
|
|
44
44
|
count?: ElementType;
|
|
45
|
-
description
|
|
45
|
+
description?: ElementType;
|
|
46
46
|
actions: ElementType;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -93,7 +93,8 @@ export function SurfaceHeader({
|
|
|
93
93
|
const leading = leadingTypes.flatMap(of);
|
|
94
94
|
const titles = of(slots.title);
|
|
95
95
|
const counts = slots.count === undefined ? [] : of(slots.count);
|
|
96
|
-
const descriptions =
|
|
96
|
+
const descriptions =
|
|
97
|
+
slots.description === undefined ? [] : of(slots.description);
|
|
97
98
|
const actionSlots = of(slots.actions);
|
|
98
99
|
const recognized =
|
|
99
100
|
leading.length +
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ReactNode } from 'react'
|
|
16
|
-
import { useId, useState } from 'react'
|
|
16
|
+
import { useEffect, useId, useState } from 'react'
|
|
17
17
|
import type { SimpleContract } from '../../../core/index.ts'
|
|
18
18
|
import { useTriggerAction } from '../../drivers/react.tsx'
|
|
19
19
|
import { toast } from '../primitives/sonner.tsx'
|
|
@@ -66,6 +66,8 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
66
66
|
}
|
|
67
67
|
/** Callback adicional pós-sucesso (cache já foi invalidado). */
|
|
68
68
|
onSuccess?: (data: TData) => void
|
|
69
|
+
/** Notifica o renderer que coordena bloqueio entre várias actions. */
|
|
70
|
+
onLoadingChange?: (loading: boolean) => void
|
|
69
71
|
/** Ícone: o botão vira icon-only, com o `label` no tooltip e no aria-label. É a forma
|
|
70
72
|
* da ação que mora DENTRO de um item (linha, card) — o clique não vaza pro item. */
|
|
71
73
|
icon?: ReactNode
|
|
@@ -85,6 +87,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
85
87
|
disabled = false,
|
|
86
88
|
confirm: confirmProp,
|
|
87
89
|
onSuccess,
|
|
90
|
+
onLoadingChange,
|
|
88
91
|
icon,
|
|
89
92
|
itemLabel,
|
|
90
93
|
className,
|
|
@@ -105,6 +108,10 @@ export function ActionTrigger<TInput, TData>({
|
|
|
105
108
|
)
|
|
106
109
|
},
|
|
107
110
|
})
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
onLoadingChange?.(isLoading)
|
|
113
|
+
return () => onLoadingChange?.(false)
|
|
114
|
+
}, [isLoading, onLoadingChange])
|
|
108
115
|
|
|
109
116
|
const buttonLabel = label ?? (typeof action.label === 'string' ? action.label : action.name)
|
|
110
117
|
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ReactNode } from 'react'
|
|
10
|
-
import type {
|
|
10
|
+
import type { ViewContract } from '../../../core/index.ts'
|
|
11
11
|
import { useViewAction } from '../../drivers/react.tsx'
|
|
12
12
|
import { isBusinessError, isRetryable } from '../../lib/action-errors.ts'
|
|
13
13
|
import { Skeleton } from '../primitives/skeleton.tsx'
|
|
14
14
|
import { EmptySurface, ErrorSurface } from './state-surface.tsx'
|
|
15
15
|
|
|
16
16
|
export interface ActionViewProps<TInput, TData> {
|
|
17
|
-
action:
|
|
17
|
+
action: ViewContract<TInput, TData>
|
|
18
18
|
input: TInput
|
|
19
19
|
/** Renderiza o "tem dado" — o layout é de quem compõe (composição, como no ActionForm). */
|
|
20
20
|
children?: (data: TData, refetch: () => Promise<void>) => ReactNode
|
|
@@ -1,110 +1,144 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import { Command as CommandPrimitive } from
|
|
3
|
-
import { SearchIcon } from
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Command as CommandPrimitive } from "cmdk";
|
|
3
|
+
import { SearchIcon } from "lucide-react";
|
|
4
4
|
|
|
5
|
-
import { cn } from
|
|
6
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle } from
|
|
5
|
+
import { cn } from "../../lib/cn.ts";
|
|
6
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./dialog.tsx";
|
|
7
7
|
|
|
8
|
-
function Command({
|
|
8
|
+
function Command({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof CommandPrimitive>) {
|
|
9
12
|
return (
|
|
10
13
|
<CommandPrimitive
|
|
11
14
|
data-slot="command"
|
|
12
15
|
className={cn(
|
|
13
|
-
|
|
16
|
+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
|
14
17
|
className,
|
|
15
18
|
)}
|
|
16
19
|
{...props}
|
|
17
20
|
/>
|
|
18
|
-
)
|
|
21
|
+
);
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
function CommandDialog({
|
|
22
|
-
title =
|
|
23
|
-
description =
|
|
25
|
+
title = "Paleta de comandos",
|
|
26
|
+
description = "Busque um comando para executar…",
|
|
24
27
|
children,
|
|
25
28
|
className,
|
|
26
29
|
showCloseButton = true,
|
|
27
30
|
...props
|
|
28
31
|
}: React.ComponentProps<typeof Dialog> & {
|
|
29
|
-
title?: string
|
|
30
|
-
description?: string
|
|
31
|
-
className?: string
|
|
32
|
-
showCloseButton?: boolean
|
|
32
|
+
title?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
className?: string;
|
|
35
|
+
showCloseButton?: boolean;
|
|
33
36
|
}) {
|
|
34
|
-
const descriptionId = React.useId()
|
|
37
|
+
const descriptionId = React.useId();
|
|
35
38
|
return (
|
|
36
39
|
<Dialog {...props}>
|
|
37
|
-
<DialogHeader className="sr-only">
|
|
38
|
-
<DialogTitle>{title}</DialogTitle>
|
|
39
|
-
<p id={descriptionId}>{description}</p>
|
|
40
|
-
</DialogHeader>
|
|
41
40
|
<DialogContent
|
|
42
|
-
className={cn(
|
|
41
|
+
className={cn("overflow-hidden p-0", className)}
|
|
43
42
|
showCloseButton={showCloseButton}
|
|
44
43
|
aria-describedby={descriptionId}
|
|
45
44
|
>
|
|
45
|
+
<DialogHeader className="border-b-0 px-3 py-2">
|
|
46
|
+
<DialogTitle className="text-sm">{title}</DialogTitle>
|
|
47
|
+
<p id={descriptionId} className="sr-only">
|
|
48
|
+
{description}
|
|
49
|
+
</p>
|
|
50
|
+
</DialogHeader>
|
|
46
51
|
<Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
47
52
|
{children}
|
|
48
53
|
</Command>
|
|
49
54
|
</DialogContent>
|
|
50
55
|
</Dialog>
|
|
51
|
-
)
|
|
56
|
+
);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
function CommandInput({
|
|
59
|
+
function CommandInput({
|
|
60
|
+
className,
|
|
61
|
+
...props
|
|
62
|
+
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
|
55
63
|
return (
|
|
56
|
-
<div
|
|
64
|
+
<div
|
|
65
|
+
data-slot="command-input-wrapper"
|
|
66
|
+
className="flex h-9 items-center gap-2 border-b px-3"
|
|
67
|
+
>
|
|
57
68
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
|
58
69
|
<CommandPrimitive.Input
|
|
59
70
|
data-slot="command-input"
|
|
60
71
|
className={cn(
|
|
61
|
-
|
|
72
|
+
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
62
73
|
className,
|
|
63
74
|
)}
|
|
64
75
|
{...props}
|
|
65
76
|
/>
|
|
66
77
|
</div>
|
|
67
|
-
)
|
|
78
|
+
);
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
function CommandList({
|
|
81
|
+
function CommandList({
|
|
82
|
+
className,
|
|
83
|
+
...props
|
|
84
|
+
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
|
71
85
|
return (
|
|
72
86
|
<CommandPrimitive.List
|
|
73
87
|
data-slot="command-list"
|
|
74
|
-
className={cn(
|
|
88
|
+
className={cn(
|
|
89
|
+
"max-h-[18.75rem] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
|
90
|
+
className,
|
|
91
|
+
)}
|
|
75
92
|
{...props}
|
|
76
93
|
/>
|
|
77
|
-
)
|
|
94
|
+
);
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
function CommandEmpty({
|
|
81
|
-
|
|
97
|
+
function CommandEmpty({
|
|
98
|
+
...props
|
|
99
|
+
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
100
|
+
return (
|
|
101
|
+
<CommandPrimitive.Empty
|
|
102
|
+
data-slot="command-empty"
|
|
103
|
+
className="py-6 text-center text-sm"
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
function CommandGroup({
|
|
109
|
+
function CommandGroup({
|
|
110
|
+
className,
|
|
111
|
+
...props
|
|
112
|
+
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
|
85
113
|
return (
|
|
86
114
|
<CommandPrimitive.Group
|
|
87
115
|
data-slot="command-group"
|
|
88
116
|
className={cn(
|
|
89
|
-
|
|
117
|
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
90
118
|
className,
|
|
91
119
|
)}
|
|
92
120
|
{...props}
|
|
93
121
|
/>
|
|
94
|
-
)
|
|
122
|
+
);
|
|
95
123
|
}
|
|
96
124
|
|
|
97
|
-
function CommandSeparator({
|
|
125
|
+
function CommandSeparator({
|
|
126
|
+
className,
|
|
127
|
+
...props
|
|
128
|
+
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
|
98
129
|
return (
|
|
99
130
|
<CommandPrimitive.Separator
|
|
100
131
|
data-slot="command-separator"
|
|
101
|
-
className={cn(
|
|
132
|
+
className={cn("-mx-1 h-px bg-border", className)}
|
|
102
133
|
{...props}
|
|
103
134
|
/>
|
|
104
|
-
)
|
|
135
|
+
);
|
|
105
136
|
}
|
|
106
137
|
|
|
107
|
-
function CommandItem({
|
|
138
|
+
function CommandItem({
|
|
139
|
+
className,
|
|
140
|
+
...props
|
|
141
|
+
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
|
108
142
|
return (
|
|
109
143
|
<CommandPrimitive.Item
|
|
110
144
|
data-slot="command-item"
|
|
@@ -114,17 +148,23 @@ function CommandItem({ className, ...props }: React.ComponentProps<typeof Comman
|
|
|
114
148
|
)}
|
|
115
149
|
{...props}
|
|
116
150
|
/>
|
|
117
|
-
)
|
|
151
|
+
);
|
|
118
152
|
}
|
|
119
153
|
|
|
120
|
-
function CommandShortcut({
|
|
154
|
+
function CommandShortcut({
|
|
155
|
+
className,
|
|
156
|
+
...props
|
|
157
|
+
}: React.ComponentProps<"span">) {
|
|
121
158
|
return (
|
|
122
159
|
<span
|
|
123
160
|
data-slot="command-shortcut"
|
|
124
|
-
className={cn(
|
|
161
|
+
className={cn(
|
|
162
|
+
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
163
|
+
className,
|
|
164
|
+
)}
|
|
125
165
|
{...props}
|
|
126
166
|
/>
|
|
127
|
-
)
|
|
167
|
+
);
|
|
128
168
|
}
|
|
129
169
|
|
|
130
170
|
export {
|
|
@@ -137,4 +177,4 @@ export {
|
|
|
137
177
|
CommandItem,
|
|
138
178
|
CommandShortcut,
|
|
139
179
|
CommandSeparator,
|
|
140
|
-
}
|
|
180
|
+
};
|