@softize/opus 12.10.0 → 13.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 +56 -0
- package/bin/lib/check.mjs +1098 -310
- package/bin/lib/copy.mjs +12 -5
- package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
- package/docs/adr/0004-page-content-state-is-composed.md +65 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +180 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
- package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
- package/docs/radius-scale.md +1 -1
- package/package.json +1 -1
- package/registry/instructions/opus.md +5 -0
- package/registry/skills/build-opus-ui/SKILL.md +27 -16
- package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
- package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
- package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
- package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
- package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
- package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
- package/registry/templates/app/src/App.tsx +1 -1
- package/src/core/dictionary.ts +52 -14
- package/src/core/index.ts +10 -0
- package/src/core/ui-context.ts +29 -0
- package/src/schema/drivers/zod.ts +17 -8
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +163 -40
- package/src/ui/components/patterns/content-header.tsx +335 -61
- package/src/ui/components/patterns/data-state.tsx +23 -10
- package/src/ui/components/patterns/list.tsx +1097 -783
- package/src/ui/components/patterns/page-state.tsx +115 -0
- package/src/ui/components/patterns/page.tsx +231 -41
- package/src/ui/components/patterns/sidebar.tsx +357 -83
- package/src/ui/components/patterns/trigger.tsx +37 -30
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert.tsx +298 -110
- package/src/ui/components/primitives/ask.tsx +2 -1
- package/src/ui/components/primitives/badge.tsx +91 -30
- package/src/ui/components/primitives/button.tsx +99 -60
- package/src/ui/components/primitives/calendar.tsx +39 -39
- package/src/ui/components/primitives/card.tsx +96 -23
- package/src/ui/components/primitives/detail.tsx +2 -2
- package/src/ui/components/primitives/dialog.tsx +196 -39
- package/src/ui/components/primitives/dictionary-value.tsx +9 -14
- package/src/ui/components/primitives/dot.tsx +74 -21
- package/src/ui/components/primitives/drawer.tsx +40 -24
- package/src/ui/components/primitives/empty.tsx +3 -3
- package/src/ui/components/primitives/item.tsx +135 -79
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/sonner.tsx +187 -8
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/docs/DocBrowser.tsx +104 -25
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/accordion.md +22 -16
- package/src/ui/docs/content/action-form-card.md +8 -8
- package/src/ui/docs/content/action-form-dialog.md +9 -9
- package/src/ui/docs/content/action-form.md +28 -34
- package/src/ui/docs/content/action-list-dialog.md +11 -6
- package/src/ui/docs/content/action-list.md +64 -39
- package/src/ui/docs/content/action-trigger.md +21 -14
- package/src/ui/docs/content/action-view.md +8 -8
- package/src/ui/docs/content/actions.md +9 -9
- package/src/ui/docs/content/ai.md +3 -3
- package/src/ui/docs/content/alert.md +54 -28
- package/src/ui/docs/content/aspect-ratio.md +4 -4
- package/src/ui/docs/content/audit.md +2 -2
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/avatar.md +34 -14
- package/src/ui/docs/content/badge.md +21 -22
- package/src/ui/docs/content/breadcrumb.md +13 -8
- package/src/ui/docs/content/button.md +93 -15
- package/src/ui/docs/content/calendar.md +5 -5
- package/src/ui/docs/content/card.md +6 -6
- package/src/ui/docs/content/carousel.md +16 -11
- package/src/ui/docs/content/chat.md +3 -3
- package/src/ui/docs/content/checkbox.md +7 -7
- package/src/ui/docs/content/cli.md +5 -5
- package/src/ui/docs/content/collapsible.md +8 -8
- package/src/ui/docs/content/command.md +16 -8
- package/src/ui/docs/content/composer.md +2 -2
- package/src/ui/docs/content/content.md +44 -0
- package/src/ui/docs/content/copyable.md +4 -3
- package/src/ui/docs/content/customization.md +7 -7
- package/src/ui/docs/content/cycle.md +3 -3
- package/src/ui/docs/content/data-state.md +11 -12
- package/src/ui/docs/content/data.md +26 -33
- package/src/ui/docs/content/detail.md +8 -5
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +19 -18
- package/src/ui/docs/content/dock.md +3 -3
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +32 -16
- package/src/ui/docs/content/empty-value.md +2 -2
- package/src/ui/docs/content/empty.md +19 -12
- package/src/ui/docs/content/events.md +4 -4
- package/src/ui/docs/content/field.md +34 -12
- package/src/ui/docs/content/getting-started.md +1 -1
- package/src/ui/docs/content/icon-picker.md +8 -4
- package/src/ui/docs/content/input-otp.md +20 -12
- package/src/ui/docs/content/input.md +121 -9
- package/src/ui/docs/content/item.md +64 -24
- package/src/ui/docs/content/kbd.md +19 -11
- package/src/ui/docs/content/label.md +5 -3
- package/src/ui/docs/content/log.md +4 -4
- package/src/ui/docs/content/markdown.md +7 -6
- package/src/ui/docs/content/mcp.md +13 -15
- package/src/ui/docs/content/menu.md +36 -17
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +93 -10
- package/src/ui/docs/content/pagination.md +22 -17
- package/src/ui/docs/content/popover.md +16 -8
- package/src/ui/docs/content/progress.md +7 -5
- package/src/ui/docs/content/queue.md +5 -5
- package/src/ui/docs/content/radio-group.md +20 -12
- package/src/ui/docs/content/router.md +11 -6
- package/src/ui/docs/content/scheduler.md +4 -5
- package/src/ui/docs/content/scroll-area.md +12 -7
- package/src/ui/docs/content/select.md +42 -29
- package/src/ui/docs/content/semantic-context.md +63 -0
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +325 -56
- package/src/ui/docs/content/skeleton.md +5 -4
- package/src/ui/docs/content/slider.md +8 -7
- package/src/ui/docs/content/spinner.md +8 -8
- package/src/ui/docs/content/split.md +8 -5
- package/src/ui/docs/content/storage.md +6 -8
- package/src/ui/docs/content/switch.md +8 -7
- package/src/ui/docs/content/table.md +16 -6
- package/src/ui/docs/content/tabs.md +28 -14
- package/src/ui/docs/content/testing.md +9 -11
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +47 -13
- package/src/ui/docs/content/toggle.md +75 -7
- package/src/ui/docs/content/tokens.md +31 -3
- package/src/ui/docs/content/tooltip.md +19 -11
- package/src/ui/docs/content/truncate.md +7 -8
- package/src/ui/docs/content/ui.md +10 -9
- package/src/ui/docs/content/upgrading.md +7 -8
- package/src/ui/docs/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +580 -229
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +278 -286
- package/src/ui/react.tsx +377 -111
- package/src/ui/theme.css +116 -0
- package/src/ui/components/primitives/alert-dialog.tsx +0 -190
- package/src/ui/docs/content/alert-dialog.md +0 -73
- package/src/ui/docs/content/button-group.md +0 -71
- package/src/ui/docs/content/confirm.md +0 -120
- package/src/ui/docs/content/input-group.md +0 -78
- package/src/ui/docs/content/toggle-group.md +0 -81
|
@@ -6,7 +6,149 @@ import {
|
|
|
6
6
|
OctagonXIcon,
|
|
7
7
|
TriangleAlertIcon,
|
|
8
8
|
} from 'lucide-react'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Toaster as Sonner,
|
|
11
|
+
toast as sonnerToast,
|
|
12
|
+
type ExternalToast,
|
|
13
|
+
type ToasterProps,
|
|
14
|
+
} from 'sonner'
|
|
15
|
+
import { cn } from '../../lib/cn.ts'
|
|
16
|
+
import {
|
|
17
|
+
Button,
|
|
18
|
+
type ButtonContext,
|
|
19
|
+
type ButtonVariant,
|
|
20
|
+
} from './button.tsx'
|
|
21
|
+
|
|
22
|
+
type ToastId = string | number
|
|
23
|
+
type ToastMessage = React.ReactNode | (() => React.ReactNode)
|
|
24
|
+
|
|
25
|
+
export interface ToastAction {
|
|
26
|
+
key?: React.Key
|
|
27
|
+
label: React.ReactNode
|
|
28
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
|
|
29
|
+
context?: ButtonContext
|
|
30
|
+
variant?: ButtonVariant
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ToastOptions extends Omit<ExternalToast, 'action' | 'cancel'> {
|
|
35
|
+
actions?: readonly ToastAction[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ToastApi {
|
|
39
|
+
(message: ToastMessage, options?: ToastOptions): ToastId
|
|
40
|
+
success: (message: ToastMessage, options?: ToastOptions) => ToastId
|
|
41
|
+
info: (message: ToastMessage, options?: ToastOptions) => ToastId
|
|
42
|
+
warning: (message: ToastMessage, options?: ToastOptions) => ToastId
|
|
43
|
+
error: (message: ToastMessage, options?: ToastOptions) => ToastId
|
|
44
|
+
message: (message: ToastMessage, options?: ToastOptions) => ToastId
|
|
45
|
+
loading: (message: ToastMessage, options?: ToastOptions) => ToastId
|
|
46
|
+
custom: typeof sonnerToast.custom
|
|
47
|
+
promise: typeof sonnerToast.promise
|
|
48
|
+
dismiss: typeof sonnerToast.dismiss
|
|
49
|
+
getHistory: typeof sonnerToast.getHistory
|
|
50
|
+
getToasts: typeof sonnerToast.getToasts
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ToastMediaProps {
|
|
54
|
+
children: React.ReactNode
|
|
55
|
+
className: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ToastMedia({ children, className }: ToastMediaProps): React.ReactElement {
|
|
59
|
+
return (
|
|
60
|
+
<span
|
|
61
|
+
className={cn(
|
|
62
|
+
'flex size-9 items-center justify-center rounded-md bg-current/10 [&_svg]:size-5',
|
|
63
|
+
className,
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</span>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ToastActionsProps {
|
|
72
|
+
actions: readonly ToastAction[]
|
|
73
|
+
getToastId: () => ToastId | undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ToastActions({ actions, getToastId }: ToastActionsProps): React.ReactElement {
|
|
77
|
+
return (
|
|
78
|
+
<div data-slot="toast-actions" className="ms-auto flex shrink-0 items-center gap-2">
|
|
79
|
+
{actions.map((action, index) => {
|
|
80
|
+
const primary = index === actions.length - 1
|
|
81
|
+
return (
|
|
82
|
+
<Button
|
|
83
|
+
key={action.key ?? index}
|
|
84
|
+
type="button"
|
|
85
|
+
size="sm"
|
|
86
|
+
context={action.context ?? (primary ? 'primary' : 'neutral')}
|
|
87
|
+
variant={action.variant ?? (primary ? 'solid' : 'ghost')}
|
|
88
|
+
disabled={action.disabled}
|
|
89
|
+
onClick={(event) => {
|
|
90
|
+
action.onClick(event)
|
|
91
|
+
if (!event.defaultPrevented) {
|
|
92
|
+
const toastId = getToastId()
|
|
93
|
+
if (toastId !== undefined) sonnerToast.dismiss(toastId)
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
{action.label}
|
|
98
|
+
</Button>
|
|
99
|
+
)
|
|
100
|
+
})}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toSonnerOptions(
|
|
106
|
+
options: ToastOptions | undefined,
|
|
107
|
+
getToastId: () => ToastId | undefined,
|
|
108
|
+
): ExternalToast | undefined {
|
|
109
|
+
if (options === undefined) return undefined
|
|
110
|
+
const { actions, ...rest } = options
|
|
111
|
+
if (actions === undefined) return rest
|
|
112
|
+
return {
|
|
113
|
+
...rest,
|
|
114
|
+
action: actions.length > 0 ? <ToastActions actions={actions} getToastId={getToastId} /> : undefined,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type SonnerToastMethod = (message: ToastMessage, options?: ExternalToast) => ToastId
|
|
119
|
+
|
|
120
|
+
function dispatchToast(
|
|
121
|
+
method: SonnerToastMethod,
|
|
122
|
+
message: ToastMessage,
|
|
123
|
+
options?: ToastOptions,
|
|
124
|
+
): ToastId {
|
|
125
|
+
let toastId: ToastId | undefined
|
|
126
|
+
toastId = method(message, toSonnerOptions(options, () => toastId))
|
|
127
|
+
return toastId
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const toast: ToastApi = Object.assign(
|
|
131
|
+
(message: ToastMessage, options?: ToastOptions) => dispatchToast(sonnerToast, message, options),
|
|
132
|
+
{
|
|
133
|
+
success: (message: ToastMessage, options?: ToastOptions) =>
|
|
134
|
+
dispatchToast(sonnerToast.success, message, options),
|
|
135
|
+
info: (message: ToastMessage, options?: ToastOptions) =>
|
|
136
|
+
dispatchToast(sonnerToast.info, message, options),
|
|
137
|
+
warning: (message: ToastMessage, options?: ToastOptions) =>
|
|
138
|
+
dispatchToast(sonnerToast.warning, message, options),
|
|
139
|
+
error: (message: ToastMessage, options?: ToastOptions) =>
|
|
140
|
+
dispatchToast(sonnerToast.error, message, options),
|
|
141
|
+
message: (message: ToastMessage, options?: ToastOptions) =>
|
|
142
|
+
dispatchToast(sonnerToast.message, message, options),
|
|
143
|
+
loading: (message: ToastMessage, options?: ToastOptions) =>
|
|
144
|
+
dispatchToast(sonnerToast.loading, message, options),
|
|
145
|
+
custom: sonnerToast.custom,
|
|
146
|
+
promise: sonnerToast.promise,
|
|
147
|
+
dismiss: sonnerToast.dismiss,
|
|
148
|
+
getHistory: sonnerToast.getHistory,
|
|
149
|
+
getToasts: sonnerToast.getToasts,
|
|
150
|
+
},
|
|
151
|
+
)
|
|
10
152
|
|
|
11
153
|
/**
|
|
12
154
|
* Toaster temático (sonner) — monte 1x no root do app. Os toasts saem estilizados
|
|
@@ -17,18 +159,54 @@ import { Toaster as Sonner, toast, type ToasterProps } from 'sonner'
|
|
|
17
159
|
* usamos next-themes — passe `theme` por prop se precisar forçar (default 'system').
|
|
18
160
|
* Elevação dark preservada: `--normal-bg` aponta pro popover (não pro background).
|
|
19
161
|
*/
|
|
20
|
-
export function Toaster({
|
|
162
|
+
export function Toaster({
|
|
163
|
+
className,
|
|
164
|
+
icons,
|
|
165
|
+
style,
|
|
166
|
+
toastOptions,
|
|
167
|
+
...props
|
|
168
|
+
}: ToasterProps): React.ReactElement {
|
|
21
169
|
return (
|
|
22
170
|
<Sonner
|
|
23
171
|
data-slot="toaster"
|
|
24
172
|
theme="system"
|
|
25
|
-
className=
|
|
173
|
+
className={cn('toaster group', className)}
|
|
26
174
|
icons={{
|
|
27
|
-
success:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
175
|
+
success: (
|
|
176
|
+
<ToastMedia className="text-context-success-emphasis">
|
|
177
|
+
<CircleCheckIcon />
|
|
178
|
+
</ToastMedia>
|
|
179
|
+
),
|
|
180
|
+
info: (
|
|
181
|
+
<ToastMedia className="text-context-info-emphasis">
|
|
182
|
+
<InfoIcon />
|
|
183
|
+
</ToastMedia>
|
|
184
|
+
),
|
|
185
|
+
warning: (
|
|
186
|
+
<ToastMedia className="text-context-warning-emphasis">
|
|
187
|
+
<TriangleAlertIcon />
|
|
188
|
+
</ToastMedia>
|
|
189
|
+
),
|
|
190
|
+
error: (
|
|
191
|
+
<ToastMedia className="text-context-danger-emphasis">
|
|
192
|
+
<OctagonXIcon />
|
|
193
|
+
</ToastMedia>
|
|
194
|
+
),
|
|
195
|
+
loading: (
|
|
196
|
+
<ToastMedia className="text-context-neutral-emphasis">
|
|
197
|
+
<Loader2Icon className="animate-spin" />
|
|
198
|
+
</ToastMedia>
|
|
199
|
+
),
|
|
200
|
+
...icons,
|
|
201
|
+
}}
|
|
202
|
+
toastOptions={{
|
|
203
|
+
...toastOptions,
|
|
204
|
+
classNames: {
|
|
205
|
+
...toastOptions?.classNames,
|
|
206
|
+
toast: cn('opus-toast', toastOptions?.classNames?.toast),
|
|
207
|
+
icon: cn('opus-toast-icon', toastOptions?.classNames?.icon),
|
|
208
|
+
closeButton: cn('opus-toast-close', toastOptions?.classNames?.closeButton),
|
|
209
|
+
},
|
|
32
210
|
}}
|
|
33
211
|
style={
|
|
34
212
|
{
|
|
@@ -36,6 +214,7 @@ export function Toaster({ ...props }: ToasterProps): React.ReactElement {
|
|
|
36
214
|
'--normal-text': 'var(--popover-foreground)',
|
|
37
215
|
'--normal-border': 'var(--border)',
|
|
38
216
|
'--border-radius': 'var(--radius)',
|
|
217
|
+
...style,
|
|
39
218
|
} as React.CSSProperties
|
|
40
219
|
}
|
|
41
220
|
{...props}
|
|
@@ -78,7 +78,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
|
78
78
|
<th
|
|
79
79
|
data-slot="table-head"
|
|
80
80
|
className={cn(
|
|
81
|
-
"h-10 px-
|
|
81
|
+
"h-10 px-3 text-left align-middle font-medium whitespace-nowrap text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
|
|
82
82
|
className
|
|
83
83
|
)}
|
|
84
84
|
{...props}
|
|
@@ -91,7 +91,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
|
91
91
|
<td
|
|
92
92
|
data-slot="table-cell"
|
|
93
93
|
className={cn(
|
|
94
|
-
"
|
|
94
|
+
"px-3 py-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
|
|
95
95
|
className
|
|
96
96
|
)}
|
|
97
97
|
{...props}
|
|
@@ -12,10 +12,16 @@
|
|
|
12
12
|
* Nos dois, navegar emite `popstate`: host que guarda o path por conta própria acompanha.
|
|
13
13
|
* `basePath` adapta o prefixo das URLs (`/docs` no Maestro, `/__docs` nos apps).
|
|
14
14
|
*/
|
|
15
|
-
import { useEffect } from 'react'
|
|
15
|
+
import { useEffect, useState } from 'react'
|
|
16
16
|
import { DOC_SECTIONS, type DocSection, type DocEntry } from './registry'
|
|
17
17
|
import { Pane, Split } from '../components/patterns/split.tsx'
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
PaneBody,
|
|
20
|
+
Sidebar,
|
|
21
|
+
SidebarGroupLabel,
|
|
22
|
+
SidebarItem,
|
|
23
|
+
SidebarTreeGroup,
|
|
24
|
+
} from '../components/patterns/sidebar.tsx'
|
|
19
25
|
import { navigate, usePathname } from '../router.ts'
|
|
20
26
|
|
|
21
27
|
function findPage(sections: DocSection[], slug: string): DocEntry | undefined {
|
|
@@ -42,6 +48,95 @@ export interface DocBrowserProps {
|
|
|
42
48
|
documentTitle?: (pageTitle: string) => string
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
function DocSidebarTree({
|
|
52
|
+
sections,
|
|
53
|
+
activeSlug,
|
|
54
|
+
onSelect,
|
|
55
|
+
}: {
|
|
56
|
+
sections: DocSection[]
|
|
57
|
+
activeSlug?: string
|
|
58
|
+
onSelect: (slug: string) => void
|
|
59
|
+
}): React.ReactElement {
|
|
60
|
+
const [closedGroups, setClosedGroups] = useState<Set<string>>(() => new Set())
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const activeGroup = sections.flatMap((section, sectionIndex) =>
|
|
64
|
+
section.groups.map((group, groupIndex) => ({
|
|
65
|
+
key: `${sectionIndex}:${groupIndex}`,
|
|
66
|
+
group,
|
|
67
|
+
})),
|
|
68
|
+
).find(({ group }) => group.pages.some((page) => page.slug === activeSlug))
|
|
69
|
+
|
|
70
|
+
if (activeGroup === undefined) return
|
|
71
|
+
setClosedGroups((current) => {
|
|
72
|
+
if (!current.has(activeGroup.key)) return current
|
|
73
|
+
const next = new Set(current)
|
|
74
|
+
next.delete(activeGroup.key)
|
|
75
|
+
return next
|
|
76
|
+
})
|
|
77
|
+
}, [activeSlug, sections])
|
|
78
|
+
|
|
79
|
+
const toggleGroup = (key: string): void => {
|
|
80
|
+
setClosedGroups((current) => {
|
|
81
|
+
const next = new Set(current)
|
|
82
|
+
if (next.has(key)) next.delete(key)
|
|
83
|
+
else next.add(key)
|
|
84
|
+
return next
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const renderPage = (page: DocEntry): React.ReactElement => (
|
|
89
|
+
<SidebarItem
|
|
90
|
+
key={page.slug}
|
|
91
|
+
label={page.title}
|
|
92
|
+
badge={page.badge === undefined ? undefined : (
|
|
93
|
+
<span className="rounded border border-border/60 px-1 font-mono text-[0.625rem] leading-tight text-muted-foreground/60">
|
|
94
|
+
{page.badge}
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
active={page.slug === activeSlug}
|
|
98
|
+
onClick={() => onSelect(page.slug)}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<nav aria-label="Navegação da documentação" className="space-y-4 p-2">
|
|
104
|
+
{sections.map((section, sectionIndex) => (
|
|
105
|
+
<div key={`${section.label}:${sectionIndex}`} className="space-y-0.5">
|
|
106
|
+
{section.label.trim() !== '' && (
|
|
107
|
+
<SidebarGroupLabel>{section.label}</SidebarGroupLabel>
|
|
108
|
+
)}
|
|
109
|
+
{section.groups.map((group, groupIndex) => {
|
|
110
|
+
const key = `${sectionIndex}:${groupIndex}`
|
|
111
|
+
if (group.pages.length === 0) return null
|
|
112
|
+
if ((group.label ?? '').trim() === '') {
|
|
113
|
+
return group.pages.map((page) => renderPage(page))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const expanded = !closedGroups.has(key)
|
|
117
|
+
return (
|
|
118
|
+
<div key={key} className="space-y-0.5">
|
|
119
|
+
<SidebarItem
|
|
120
|
+
label={group.label!}
|
|
121
|
+
icon={group.icon}
|
|
122
|
+
expanded={expanded}
|
|
123
|
+
onToggle={() => toggleGroup(key)}
|
|
124
|
+
onClick={() => toggleGroup(key)}
|
|
125
|
+
/>
|
|
126
|
+
{expanded && (
|
|
127
|
+
<SidebarTreeGroup>
|
|
128
|
+
{group.pages.map((page) => renderPage(page))}
|
|
129
|
+
</SidebarTreeGroup>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
})}
|
|
134
|
+
</div>
|
|
135
|
+
))}
|
|
136
|
+
</nav>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
45
140
|
export function DocBrowser({
|
|
46
141
|
basePath = '/docs',
|
|
47
142
|
path: controlledPath,
|
|
@@ -76,33 +171,17 @@ export function DocBrowser({
|
|
|
76
171
|
if (page && documentTitle) document.title = documentTitle(page.title)
|
|
77
172
|
}, [page, documentTitle])
|
|
78
173
|
|
|
79
|
-
// Seção → grupo, e os grupos da seção → subgrupos. O tier do meio NÃO pode ser
|
|
80
|
-
// achatado: `docSectionsFromFolder` o preenche a partir de sub-pasta (ou do
|
|
81
|
-
// frontmatter `group:`), que é o caminho do `opusDocs({ source })`.
|
|
82
|
-
const navGroups: SidebarNavGroup[] = sections.map((section) => ({
|
|
83
|
-
label: section.label,
|
|
84
|
-
subgroups: section.groups.map((g) => ({
|
|
85
|
-
label: g.label,
|
|
86
|
-
items: g.pages.map((p) => ({
|
|
87
|
-
id: p.slug,
|
|
88
|
-
label: p.title,
|
|
89
|
-
badge: p.badge === undefined ? undefined : <span className="rounded border border-border/60 px-1 font-mono text-[0.625rem] leading-tight text-muted-foreground/60">{p.badge}</span>,
|
|
90
|
-
})),
|
|
91
|
-
})),
|
|
92
|
-
}))
|
|
93
|
-
|
|
94
174
|
return (
|
|
95
175
|
<Split className="h-full">
|
|
96
|
-
<Pane
|
|
176
|
+
<Pane initialSize="14rem" inset="none">
|
|
97
177
|
<Sidebar className="w-full">
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
onSelect={(slug) => go(`${basePath}/${slug}`)}
|
|
178
|
+
<PaneBody>
|
|
179
|
+
<DocSidebarTree
|
|
180
|
+
sections={sections}
|
|
181
|
+
activeSlug={page?.slug}
|
|
182
|
+
onSelect={(nextSlug) => go(`${basePath}/${nextSlug}`)}
|
|
104
183
|
/>
|
|
105
|
-
</
|
|
184
|
+
</PaneBody>
|
|
106
185
|
</Sidebar>
|
|
107
186
|
</Pane>
|
|
108
187
|
<Pane key={page?.slug} grow inset="none" className="overflow-y-auto">
|
|
@@ -54,7 +54,7 @@ export function ChangelogView({ source }: { source: string }): React.ReactElemen
|
|
|
54
54
|
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
55
55
|
<h2 className="text-xl font-semibold tracking-tight">{v.version}</h2>
|
|
56
56
|
{v.date !== undefined && <span className="text-sm text-muted-foreground">{v.date}</span>}
|
|
57
|
-
{v.breaking !== undefined && <Badge variant="
|
|
57
|
+
{v.breaking !== undefined && <Badge context="danger" variant="solid">Breaking</Badge>}
|
|
58
58
|
</div>
|
|
59
59
|
{v.body.length > 0 && (
|
|
60
60
|
<div className="mt-3">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Um painel por vez
|
|
2
2
|
|
|
3
|
-
`type=single`
|
|
4
|
-
|
|
3
|
+
Use `type="single"` quando somente um painel deve permanecer aberto. Com `collapsible`, a pessoa
|
|
4
|
+
também pode fechar o painel atual; sem essa propriedade, um item permanece expandido.
|
|
5
|
+
`defaultValue` define o item inicialmente aberto no modo não controlado.
|
|
5
6
|
|
|
6
7
|
```tsx preview
|
|
7
8
|
<Accordion type="single" collapsible defaultValue="overview" className="w-full">
|
|
@@ -28,8 +29,8 @@ sempre fica um item expandido. `defaultValue` deixa o estado com o componente.
|
|
|
28
29
|
|
|
29
30
|
## Múltiplos abertos
|
|
30
31
|
|
|
31
|
-
`type=multiple`
|
|
32
|
-
array com os itens
|
|
32
|
+
Use `type="multiple"` quando os painéis puderem permanecer abertos ao mesmo tempo. Nesse modo,
|
|
33
|
+
`defaultValue` recebe um array com os itens inicialmente expandidos.
|
|
33
34
|
|
|
34
35
|
```tsx preview
|
|
35
36
|
<Accordion type="multiple" defaultValue={['skills', 'prompt']} className="w-full">
|
|
@@ -48,10 +49,10 @@ array com os itens abertos de saída.
|
|
|
48
49
|
</Accordion>
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
##
|
|
52
|
+
## Estado controlado
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
Use `value` e `onValueChange` quando outro elemento ou estado do aplicativo também precisar
|
|
55
|
+
controlar o painel aberto.
|
|
55
56
|
|
|
56
57
|
```tsx preview
|
|
57
58
|
const [open, setOpen] = React.useState('developer')
|
|
@@ -74,13 +75,18 @@ render(
|
|
|
74
75
|
)
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
##
|
|
78
|
+
## Propriedades de Accordion
|
|
78
79
|
|
|
79
|
-
|
|
|
80
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
80
81
|
|---|---|---|---|
|
|
81
|
-
| `type`
|
|
82
|
-
| `collapsible`
|
|
83
|
-
| `defaultValue`
|
|
84
|
-
| `value`
|
|
85
|
-
| `onValueChange`
|
|
86
|
-
|
|
82
|
+
| `type` | `'single' \| 'multiple'` | | `single` abre um painel por vez; `multiple` permite manter vários abertos. |
|
|
83
|
+
| `collapsible` | `boolean` | `false` | No modo `single`, permite fechar o item aberto. |
|
|
84
|
+
| `defaultValue` | `string \| string[]` | | Item ou itens inicialmente abertos no modo não controlado. |
|
|
85
|
+
| `value` | `string \| string[]` | | Item ou itens abertos no modo controlado. Use com `onValueChange`. |
|
|
86
|
+
| `onValueChange` | `(value) => void` | | Chamado quando a pessoa abre ou fecha um item. |
|
|
87
|
+
|
|
88
|
+
## Propriedades de AccordionItem
|
|
89
|
+
|
|
90
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| `value` | `string` | | Identificador usado por `defaultValue` e `value` no `Accordion`. |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Formulário em uma seção
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Use `ActionFormCard` para apresentar um `ActionForm` como seção delimitada da página. O componente
|
|
4
|
+
aplica a estrutura e o espaçamento de `Card`, sem a faixa de ações de um modal. O título é opcional.
|
|
5
5
|
|
|
6
6
|
```tsx preview col md
|
|
7
7
|
<DocBrowserActionProvider>
|
|
@@ -14,11 +14,11 @@ faixa de modal). Pra estruturar uma seção da página como painel. O título é
|
|
|
14
14
|
</DocBrowserActionProvider>
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Propriedades de ActionFormCard
|
|
18
18
|
|
|
19
|
-
|
|
|
19
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
20
20
|
|---|---|---|---|
|
|
21
|
-
| `title` | `string` | | Título do
|
|
21
|
+
| `title` | `string` | | Título do cabeçalho. Sem ele, o card começa diretamente pelo corpo. |
|
|
22
22
|
| `description` | `string` | | Subtítulo opcional, abaixo do título. |
|
|
23
|
-
| `action / defaultValues / fieldOptions / submitLabel / onSuccess / onCancel` |
|
|
24
|
-
| `cardClassName` | `string` | | Classes
|
|
23
|
+
| `action / defaultValues / fieldOptions / submitLabel / onSuccess / onCancel` | Propriedades de `ActionForm` | | Mantêm o mesmo comportamento do formulário interno. |
|
|
24
|
+
| `cardClassName` | `string` | | Classes aplicadas à superfície do card. `className` continua sendo aplicado ao `<form>`. |
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Formulário em um modal
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `ActionFormDialog` quando o formulário precisar interromper o fluxo atual sem levar a pessoa
|
|
4
|
+
para outra página. O consumidor controla `open`; depois de uma execução bem-sucedida, o componente
|
|
5
|
+
fecha o modal. O cabeçalho e o rodapé permanecem visíveis enquanto os campos podem rolar.
|
|
6
6
|
|
|
7
7
|
```tsx preview
|
|
8
8
|
const [open, setOpen] = useState(false)
|
|
@@ -22,11 +22,11 @@ render(
|
|
|
22
22
|
)
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Propriedades de ActionFormDialog
|
|
26
26
|
|
|
27
|
-
|
|
|
27
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
28
28
|
|---|---|---|---|
|
|
29
|
-
| `open / onOpenChange` | `boolean / (open: boolean) => void` | |
|
|
30
|
-
| `title / description` | `string` | |
|
|
29
|
+
| `open / onOpenChange` | `boolean / (open: boolean) => void` | | Estado controlado do modal. `onOpenChange(false)` é chamado no sucesso e ao cancelar. |
|
|
30
|
+
| `title / description` | `string` | | Conteúdo do cabeçalho; `description` é opcional. |
|
|
31
31
|
| `submitLabel` | `string` | `'Salvar'` | Resultado da ação principal. Em criação, informe `Criar {recurso}`; em edição, `Salvar alterações`. |
|
|
32
|
-
| `…ActionFormProps` | `action, defaultValues, onSuccess, fieldOptions…` | |
|
|
32
|
+
| `…ActionFormProps` | `action, defaultValues, onSuccess, fieldOptions…` | | Demais propriedades repassadas ao `ActionForm` interno. |
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Formulário derivado do contrato
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `ActionForm` para gerar campos, validação e mensagens a partir de uma `FormAction`. Sem
|
|
4
|
+
`children`, o componente escolhe o controle adequado para cada campo e preserva a ordem declarada
|
|
5
|
+
no contrato. No exemplo, altere um valor para habilitar a ação principal.
|
|
6
6
|
|
|
7
7
|
```tsx preview col md
|
|
8
8
|
<DocBrowserActionProvider>
|
|
@@ -10,13 +10,12 @@ submit só habilita com mudança real (dirty).
|
|
|
10
10
|
</DocBrowserActionProvider>
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
## Composição
|
|
13
|
+
## Composição dos campos
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
invalidação
|
|
19
|
-
(quem diagrama usa grid/gap/space-y como quiser).
|
|
15
|
+
Passe `children` quando a disposição automática não atender ao formulário. Cada
|
|
16
|
+
`ActionFormField` mantém rótulo, ajuda, controle, obrigatoriedade e erro derivados do contrato;
|
|
17
|
+
o consumidor define apenas a grade, as condições e o espaçamento. Execução, validação, toast e
|
|
18
|
+
invalidação continuam sob responsabilidade de `ActionForm`.
|
|
20
19
|
|
|
21
20
|
```tsx preview col md
|
|
22
21
|
<DocBrowserActionProvider>
|
|
@@ -35,8 +34,8 @@ invalidação) continua encapsulado — e o espaçamento é 100% seu: o campo n
|
|
|
35
34
|
|
|
36
35
|
## Valores iniciais e rótulos
|
|
37
36
|
|
|
38
|
-
defaultValues
|
|
39
|
-
|
|
37
|
+
`defaultValues` preenche o formulário para edição. `submitLabel` e `cancelLabel` nomeiam as ações;
|
|
38
|
+
`onCancel` devolve ao consumidor a decisão de fechar um painel ou navegar para outra página.
|
|
40
39
|
|
|
41
40
|
```tsx preview col md
|
|
42
41
|
<DocBrowserActionProvider>
|
|
@@ -51,8 +50,7 @@ quem orquestra (fechar drawer, voltar…).
|
|
|
51
50
|
|
|
52
51
|
## Pré-requisitos
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
canônico).
|
|
53
|
+
Monte os providers de consulta e execução uma vez na raiz do aplicativo:
|
|
56
54
|
|
|
57
55
|
```tsx
|
|
58
56
|
/* main.tsx do app. */
|
|
@@ -63,35 +61,32 @@ canônico).
|
|
|
63
61
|
</QueryClientProvider>
|
|
64
62
|
```
|
|
65
63
|
|
|
66
|
-
##
|
|
64
|
+
## Propriedades de ActionForm
|
|
67
65
|
|
|
68
|
-
|
|
|
66
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
69
67
|
|---|---|---|---|
|
|
70
68
|
| `action` | `FormContract<TInput, TData>` | | O contrato da FormAction — dele saem campos (input Zod + fields), mensagens e invalidação de cache. |
|
|
71
69
|
| `defaultValues` | `Partial<TInput>` | | Valores iniciais — o modo edição de um update/patch. |
|
|
72
70
|
| `onSuccess` | `(data: TData) => void` | | Pós-sucesso (o toast e a invalidação de cache já aconteceram). |
|
|
73
71
|
| `submitLabel / cancelLabel / onCancel` | `string / string / () => void` | `'Salvar' / 'Cancelar'` | Rodapé do form — o Cancelar só aparece com onCancel. |
|
|
74
72
|
| `fieldOptions` | `Record<string, SelectOption[]>` | | Opções de runtime por campo (ex.: ids de skills) — sobrepõe as inferidas do z.enum. |
|
|
75
|
-
| `className / body / footer` | `string / (node) => node / (node) => node` | | className = classes do `<form>`. body/footer =
|
|
76
|
-
| `children` | `ReactNode` | | Modo
|
|
73
|
+
| `className / body / footer` | `string / (node) => node / (node) => node` | | className = classes do `<form>`. body/footer = slots: recebem os campos / os botões e escolhem o invólucro — é como o ActionFormDialog injeta DialogBody/DialogFooter (scroll + faixa). |
|
|
74
|
+
| `children` | `ReactNode` | | Modo composição: diagrame com `<ActionFormField name />`. Sem children, o automático monta todos os campos na ordem do contrato. |
|
|
77
75
|
|
|
78
|
-
## ActionFormField
|
|
76
|
+
## Propriedades de ActionFormField
|
|
79
77
|
|
|
80
|
-
|
|
|
78
|
+
| Propriedade | Tipo | Descrição |
|
|
81
79
|
|---|---|---|
|
|
82
80
|
| `name` | `string` | O campo do contrato (chave em `fields`/schema). Fora do schema, não renderiza. |
|
|
83
81
|
| `options` | `SelectOption[]` | Opções por id de runtime — sobrepõe o fieldOptions do form e o z.enum. |
|
|
84
|
-
| `className` | `string` | Classes do invólucro (ex.: `col-span-2`
|
|
82
|
+
| `className` | `string` | Classes do invólucro (ex.: `col-span-2` em uma grid). |
|
|
85
83
|
|
|
86
|
-
##
|
|
84
|
+
## Origem das opções de seleção
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`t.dict` no schema** (zero-config: campo `meuDict.zod()` — ou multiselect com elemento dict —
|
|
93
|
-
resolve value→label pela meta que viaja no contrato, sem registry) > **chaves cruas do
|
|
94
|
-
z.enum**. Campo texto com opções declaradas (runtime ou spec) vira single-select por-id.
|
|
86
|
+
As opções seguem esta precedência: `options` no campo, `fieldOptions` no formulário, `options` no
|
|
87
|
+
`FieldSpec`, metadata de `t.dict` no schema e, por último, as chaves de `z.enum`. Uma fonte mais
|
|
88
|
+
específica substitui as seguintes. Dicionários registrados no `TbdlibProvider` resolvem rótulos por
|
|
89
|
+
referência; itens estáticos permanecem como declarados no contrato.
|
|
95
90
|
|
|
96
91
|
## Widgets declarativos
|
|
97
92
|
|
|
@@ -103,11 +98,10 @@ escolha rica em card, com conteúdo de apoio; para uma lista textual comum, pref
|
|
|
103
98
|
Outros identificadores continuam válidos como metadado
|
|
104
99
|
para renderers próprios; o `ActionForm` aplica sua inferência normal quando não reconhece o widget.
|
|
105
100
|
|
|
106
|
-
## Controle
|
|
101
|
+
## Controle próprio com useActionFormContext
|
|
107
102
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
sem abandonar o `<ActionForm>`:
|
|
103
|
+
Quando nenhum widget atender ao campo, use `useActionFormContext` no modo de composição. O controle
|
|
104
|
+
próprio participa da mesma validação e execução sem abandonar o `ActionForm`:
|
|
111
105
|
|
|
112
106
|
```tsx
|
|
113
107
|
import { useActionFormContext } from '@softize/opus/ui/react'
|
|
@@ -132,4 +126,4 @@ function PermissionGrid() {
|
|
|
132
126
|
```
|
|
133
127
|
|
|
134
128
|
O contexto também expõe `shape` (Zod por campo), `fields` (FieldSpec) e `fieldOptions`.
|
|
135
|
-
Fora de um `<ActionForm>`, o hook
|
|
129
|
+
Fora de um `<ActionForm>`, o hook lança um erro que informa o provider ausente.
|