@softize/opus 18.2.0 → 18.4.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 +26 -0
- package/README.md +1 -0
- package/docs/adr/0022-execution-monitoring-is-a-projection.md +39 -0
- package/package.json +21 -1
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +4 -3
- package/src/core/errors.ts +0 -1
- package/src/core/types.ts +4 -0
- package/src/executions/drivers/memory.ts +39 -0
- package/src/executions/drivers/pg-changes.ts +104 -0
- package/src/executions/drivers/pg.ts +74 -0
- package/src/executions/index.ts +129 -0
- package/src/executions/stream.ts +123 -0
- package/src/ui/components/patterns/data-state.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +606 -251
- package/src/ui/components/patterns/page.tsx +16 -5
- package/src/ui/components/patterns/presentation.tsx +32 -6
- package/src/ui/components/primitives/field.tsx +23 -2
- package/src/ui/components/primitives/metric-card.tsx +64 -5
- package/src/ui/components/primitives/select.tsx +27 -23
- package/src/ui/components/primitives/table.tsx +4 -1
- package/src/ui/docs/content/action-list.md +84 -46
- package/src/ui/docs/content/executions.md +157 -0
- package/src/ui/docs/content/field.md +6 -1
- package/src/ui/docs/content/metric-card.md +43 -0
- package/src/ui/docs/content/select.md +4 -0
- package/src/ui/docs/registry.tsx +7 -0
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +1 -0
|
@@ -164,7 +164,10 @@ export function PageShell({
|
|
|
164
164
|
<PageShellContext.Provider value>
|
|
165
165
|
<PageNavigationTargetContext.Provider value={navigationTarget}>
|
|
166
166
|
<PageActionsTarget target={actionsTarget}>
|
|
167
|
-
<div
|
|
167
|
+
<div
|
|
168
|
+
data-slot="page-shell-content"
|
|
169
|
+
className="min-h-0 min-w-0 flex-1 overflow-auto [&>*]:min-w-[64rem]"
|
|
170
|
+
>
|
|
168
171
|
{children}
|
|
169
172
|
</div>
|
|
170
173
|
</PageActionsTarget>
|
|
@@ -202,6 +205,8 @@ interface PageBaseProps extends Omit<
|
|
|
202
205
|
> {
|
|
203
206
|
/** Classes do container. Default: centralizado com `max-w-7xl` (80rem). */
|
|
204
207
|
className?: string;
|
|
208
|
+
/** `fill` limita a página à altura do shell para que regiões internas assumam a rolagem. */
|
|
209
|
+
layout?: "flow" | "fill";
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
interface PageShorthandProps extends PageBaseProps {
|
|
@@ -252,8 +257,7 @@ function countPageBodyHeadings(node: ReactNode): {
|
|
|
252
257
|
total.content +
|
|
253
258
|
(child.type === Content && props.level === 1 ? 1 : 0) +
|
|
254
259
|
nested.content,
|
|
255
|
-
state:
|
|
256
|
-
total.state + nested.state,
|
|
260
|
+
state: total.state + nested.state,
|
|
257
261
|
};
|
|
258
262
|
},
|
|
259
263
|
{ content: 0, state: 0 },
|
|
@@ -275,6 +279,7 @@ function hasOpaquePageBodyContent(node: ReactNode): boolean {
|
|
|
275
279
|
export function Page({
|
|
276
280
|
title,
|
|
277
281
|
actions,
|
|
282
|
+
layout = "flow",
|
|
278
283
|
className,
|
|
279
284
|
children,
|
|
280
285
|
...props
|
|
@@ -372,7 +377,8 @@ export function Page({
|
|
|
372
377
|
(total, intro) =>
|
|
373
378
|
total + countDirectChildrenOfType(intro as ReactElement, PageTitle),
|
|
374
379
|
0,
|
|
375
|
-
) +
|
|
380
|
+
) +
|
|
381
|
+
bodyHeadingSources.content;
|
|
376
382
|
const headingSources =
|
|
377
383
|
bodyHeadingSources.state > 0
|
|
378
384
|
? bodyHeadingSources.state + bodyHeadingSources.content
|
|
@@ -429,9 +435,12 @@ export function Page({
|
|
|
429
435
|
<PageHeadingPresenceProvider register={registerHeading}>
|
|
430
436
|
<main
|
|
431
437
|
data-slot="page"
|
|
438
|
+
data-layout={layout}
|
|
432
439
|
className={cn(
|
|
433
|
-
"min-w-
|
|
440
|
+
"min-w-[64rem] flex-1",
|
|
434
441
|
insideShell && "min-h-full",
|
|
442
|
+
layout === "fill" &&
|
|
443
|
+
"flex h-full min-h-0 flex-col overflow-hidden",
|
|
435
444
|
integralState && "flex min-h-full flex-col",
|
|
436
445
|
)}
|
|
437
446
|
{...props}
|
|
@@ -439,6 +448,8 @@ export function Page({
|
|
|
439
448
|
<div
|
|
440
449
|
className={cn(
|
|
441
450
|
"mx-auto max-w-7xl space-y-6 px-8 py-8",
|
|
451
|
+
layout === "fill" &&
|
|
452
|
+
"flex h-full min-h-0 w-full flex-1 flex-col",
|
|
442
453
|
integralState &&
|
|
443
454
|
"flex min-h-full w-full flex-1 flex-col space-y-0",
|
|
444
455
|
className,
|
|
@@ -82,6 +82,7 @@ import { SurfaceAssistant } from "./surface-assistant.tsx";
|
|
|
82
82
|
|
|
83
83
|
interface PresentationFrameProps {
|
|
84
84
|
surface: PresentationSurface;
|
|
85
|
+
layout?: "flow" | "fill";
|
|
85
86
|
title: ReactNode;
|
|
86
87
|
navigation?: ReactNode;
|
|
87
88
|
headerActions?: ReactNode;
|
|
@@ -121,6 +122,7 @@ function PresentationActions({
|
|
|
121
122
|
/** A mesma Presentation em Page, Dialog ou Drawer; a superfície não redefine o recurso. */
|
|
122
123
|
function PresentationFrame({
|
|
123
124
|
surface,
|
|
125
|
+
layout = "flow",
|
|
124
126
|
title,
|
|
125
127
|
navigation,
|
|
126
128
|
headerActions,
|
|
@@ -162,7 +164,11 @@ function PresentationFrame({
|
|
|
162
164
|
const body = (
|
|
163
165
|
<div
|
|
164
166
|
data-slot="presentation-body"
|
|
165
|
-
className={cn(
|
|
167
|
+
className={cn(
|
|
168
|
+
bodyClassName,
|
|
169
|
+
layout === "fill" && "flex min-h-0 flex-1 flex-col",
|
|
170
|
+
bodyClassNameProp,
|
|
171
|
+
)}
|
|
166
172
|
>
|
|
167
173
|
{typeof children === "function" ? children(footerTarget) : children}
|
|
168
174
|
</div>
|
|
@@ -198,7 +204,10 @@ function PresentationFrame({
|
|
|
198
204
|
</Content>
|
|
199
205
|
);
|
|
200
206
|
return (
|
|
201
|
-
<Page
|
|
207
|
+
<Page
|
|
208
|
+
layout={layout}
|
|
209
|
+
className={cn("flex min-h-full flex-col", className)}
|
|
210
|
+
>
|
|
202
211
|
{navigation === undefined && assistantAction === undefined ? null : (
|
|
203
212
|
<PageHeader>
|
|
204
213
|
{navigation === undefined ? null : (
|
|
@@ -211,7 +220,9 @@ function PresentationFrame({
|
|
|
211
220
|
)}
|
|
212
221
|
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
213
222
|
{withAssistant(
|
|
214
|
-
<div className="h-full overflow-y-auto">
|
|
223
|
+
<div className="h-full overflow-x-hidden overflow-y-auto">
|
|
224
|
+
{content}
|
|
225
|
+
</div>,
|
|
215
226
|
)}
|
|
216
227
|
</PageBody>
|
|
217
228
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
@@ -220,7 +231,10 @@ function PresentationFrame({
|
|
|
220
231
|
}
|
|
221
232
|
if (insidePageShell) {
|
|
222
233
|
return (
|
|
223
|
-
<Page
|
|
234
|
+
<Page
|
|
235
|
+
layout={layout}
|
|
236
|
+
className={cn("flex min-h-full flex-col", className)}
|
|
237
|
+
>
|
|
224
238
|
{navigation === undefined &&
|
|
225
239
|
headerActions === undefined &&
|
|
226
240
|
assistantAction === undefined ? null : (
|
|
@@ -241,7 +255,12 @@ function PresentationFrame({
|
|
|
241
255
|
)}
|
|
242
256
|
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
243
257
|
{withAssistant(
|
|
244
|
-
<div
|
|
258
|
+
<div
|
|
259
|
+
className={cn(
|
|
260
|
+
"flex h-full min-h-0 flex-col overflow-x-hidden",
|
|
261
|
+
layout === "fill" ? "overflow-y-hidden" : "overflow-y-auto",
|
|
262
|
+
)}
|
|
263
|
+
>
|
|
245
264
|
{contentOwnsHeading ? null : (
|
|
246
265
|
<PageIntro>
|
|
247
266
|
<PageTitle>{title}</PageTitle>
|
|
@@ -257,6 +276,7 @@ function PresentationFrame({
|
|
|
257
276
|
}
|
|
258
277
|
return (
|
|
259
278
|
<Page
|
|
279
|
+
layout={layout}
|
|
260
280
|
className={cn(
|
|
261
281
|
"flex min-h-full max-w-none flex-col space-y-0 px-0 py-0",
|
|
262
282
|
className,
|
|
@@ -279,7 +299,9 @@ function PresentationFrame({
|
|
|
279
299
|
</PageHeader>
|
|
280
300
|
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
281
301
|
{withAssistant(
|
|
282
|
-
<div className="h-full overflow-y-auto px-8 py-8">
|
|
302
|
+
<div className="h-full overflow-x-hidden overflow-y-auto px-8 py-8">
|
|
303
|
+
{body}
|
|
304
|
+
</div>,
|
|
283
305
|
)}
|
|
284
306
|
</PageBody>
|
|
285
307
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
@@ -405,6 +427,8 @@ export interface PresentationProps {
|
|
|
405
427
|
onOpenChange?: (open: boolean) => void;
|
|
406
428
|
className?: string;
|
|
407
429
|
bodyClassName?: string;
|
|
430
|
+
/** `fill` limita a página ao shell e delega a rolagem ao conteúdo React. */
|
|
431
|
+
layout?: "flow" | "fill";
|
|
408
432
|
/** Conteúdo React da boundary declarada por `body.component`. */
|
|
409
433
|
children?: ReactNode;
|
|
410
434
|
/** Painel runtime; a página fecha sobre os dados do recurso que ela carregou. */
|
|
@@ -478,6 +502,7 @@ export function Presentation({
|
|
|
478
502
|
onOpenChange,
|
|
479
503
|
className,
|
|
480
504
|
bodyClassName: bodyClassNameProp,
|
|
505
|
+
layout = "flow",
|
|
481
506
|
children,
|
|
482
507
|
assistant,
|
|
483
508
|
}: PresentationProps): ReactElement {
|
|
@@ -776,6 +801,7 @@ export function Presentation({
|
|
|
776
801
|
return (
|
|
777
802
|
<PresentationFrame
|
|
778
803
|
surface={invocation.surface}
|
|
804
|
+
layout={layout}
|
|
779
805
|
title={definition.title}
|
|
780
806
|
navigation={navigation}
|
|
781
807
|
headerActions={headerActions.length === 0 ? undefined : headerActions}
|
|
@@ -22,20 +22,41 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
|
22
22
|
function FieldLegend({
|
|
23
23
|
className,
|
|
24
24
|
variant = "legend",
|
|
25
|
+
icon,
|
|
26
|
+
children,
|
|
25
27
|
...props
|
|
26
|
-
}: React.ComponentProps<"legend"> & {
|
|
28
|
+
}: React.ComponentProps<"legend"> & {
|
|
29
|
+
variant?: "legend" | "label" | "section"
|
|
30
|
+
icon?: React.ReactNode
|
|
31
|
+
}) {
|
|
27
32
|
return (
|
|
28
33
|
<legend
|
|
29
34
|
data-slot="field-legend"
|
|
30
35
|
data-variant={variant}
|
|
31
36
|
className={cn(
|
|
32
37
|
"mb-3 font-medium",
|
|
38
|
+
icon !== undefined && "flex items-center gap-2",
|
|
33
39
|
"data-[variant=legend]:text-base",
|
|
34
40
|
"data-[variant=label]:text-sm",
|
|
41
|
+
"data-[variant=section]:w-full data-[variant=section]:border-b data-[variant=section]:border-border/70 data-[variant=section]:pb-2 data-[variant=section]:text-sm data-[variant=section]:font-semibold",
|
|
35
42
|
className
|
|
36
43
|
)}
|
|
37
44
|
{...props}
|
|
38
|
-
|
|
45
|
+
>
|
|
46
|
+
{icon !== undefined ? (
|
|
47
|
+
<span
|
|
48
|
+
data-slot="field-legend-icon"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
className={cn(
|
|
51
|
+
"inline-flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4",
|
|
52
|
+
variant === "section" && "size-7 rounded-md bg-muted",
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{icon}
|
|
56
|
+
</span>
|
|
57
|
+
) : null}
|
|
58
|
+
{children}
|
|
59
|
+
</legend>
|
|
39
60
|
)
|
|
40
61
|
}
|
|
41
62
|
|
|
@@ -8,6 +8,15 @@ import { Skeleton } from './skeleton.tsx'
|
|
|
8
8
|
export type MetricCardTone = 'default' | 'warning'
|
|
9
9
|
export type MetricCardContext = Exclude<UiContext, 'primary'>
|
|
10
10
|
|
|
11
|
+
export interface MetricCardSecondaryMetric {
|
|
12
|
+
/** Nome curto da medida complementar. */
|
|
13
|
+
label: React.ReactNode
|
|
14
|
+
/** Valor complementar já formatado pelo consumidor. */
|
|
15
|
+
value: React.ReactNode
|
|
16
|
+
/** Controle complementar próprio, como uma explicação em tooltip. */
|
|
17
|
+
labelAction?: React.ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
const iconContextClasses: Record<MetricCardContext, string> = {
|
|
12
21
|
neutral: 'bg-context-neutral-subtle text-context-neutral-emphasis',
|
|
13
22
|
info: 'bg-context-info-subtle text-context-info-emphasis',
|
|
@@ -16,7 +25,7 @@ const iconContextClasses: Record<MetricCardContext, string> = {
|
|
|
16
25
|
danger: 'bg-context-danger-subtle text-context-danger-emphasis',
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'children'> {
|
|
28
|
+
interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'children' | 'asChild'> {
|
|
20
29
|
/** Controle complementar exibido ao lado do rótulo, como uma explicação em tooltip. */
|
|
21
30
|
labelAction?: React.ReactNode
|
|
22
31
|
/** Contexto que explica recorte, proporção ou significado do valor. */
|
|
@@ -29,6 +38,8 @@ interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'c
|
|
|
29
38
|
tone?: MetricCardTone
|
|
30
39
|
/** Ação relacionada diretamente à medida. */
|
|
31
40
|
action?: React.ReactNode
|
|
41
|
+
/** Medida complementar exibida como uma lâmina posterior sob o card principal. */
|
|
42
|
+
secondaryMetric?: MetricCardSecondaryMetric
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
export type MetricCardProps = MetricCardBaseProps &
|
|
@@ -63,6 +74,7 @@ export function MetricCard({
|
|
|
63
74
|
context,
|
|
64
75
|
tone,
|
|
65
76
|
action,
|
|
77
|
+
secondaryMetric,
|
|
66
78
|
loading = false,
|
|
67
79
|
className,
|
|
68
80
|
...props
|
|
@@ -73,12 +85,16 @@ export function MetricCard({
|
|
|
73
85
|
const hasIcon = icon !== undefined && icon !== null
|
|
74
86
|
const resolvedContext = context ?? (tone === 'warning' ? 'warning' : 'neutral')
|
|
75
87
|
|
|
76
|
-
|
|
88
|
+
const primaryCard = (
|
|
77
89
|
<Card
|
|
78
90
|
data-slot="metric-card"
|
|
79
|
-
aria-busy={loading || undefined}
|
|
80
|
-
className={cn(
|
|
81
|
-
|
|
91
|
+
aria-busy={secondaryMetric === undefined ? loading || undefined : undefined}
|
|
92
|
+
className={cn(
|
|
93
|
+
'min-w-0 p-4',
|
|
94
|
+
secondaryMetric !== undefined && 'relative z-10',
|
|
95
|
+
secondaryMetric === undefined && className,
|
|
96
|
+
)}
|
|
97
|
+
{...(secondaryMetric === undefined ? props : {})}
|
|
82
98
|
>
|
|
83
99
|
{loading ? (
|
|
84
100
|
<div className="space-y-3">
|
|
@@ -130,4 +146,47 @@ export function MetricCard({
|
|
|
130
146
|
)}
|
|
131
147
|
</Card>
|
|
132
148
|
)
|
|
149
|
+
|
|
150
|
+
if (secondaryMetric === undefined) return primaryCard
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
{...props}
|
|
155
|
+
data-slot="metric-card-stack"
|
|
156
|
+
aria-busy={loading || undefined}
|
|
157
|
+
className={cn('relative isolate min-w-0', className)}
|
|
158
|
+
>
|
|
159
|
+
{primaryCard}
|
|
160
|
+
<Card
|
|
161
|
+
data-slot="metric-card-secondary"
|
|
162
|
+
className="relative z-0 -mt-8 flex w-full min-w-0 items-center gap-1.5 bg-[color-mix(in_oklab,var(--card)_88%,black)] px-4 pt-9 pb-1"
|
|
163
|
+
>
|
|
164
|
+
{loading ? (
|
|
165
|
+
<Skeleton className="h-5 w-full" />
|
|
166
|
+
) : (
|
|
167
|
+
<>
|
|
168
|
+
<div
|
|
169
|
+
data-slot="metric-card-secondary-value"
|
|
170
|
+
className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums"
|
|
171
|
+
>
|
|
172
|
+
{secondaryMetric.value}
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
175
|
+
<div
|
|
176
|
+
data-slot="metric-card-secondary-label"
|
|
177
|
+
className="min-w-0 truncate text-xs font-medium text-muted-foreground"
|
|
178
|
+
>
|
|
179
|
+
{secondaryMetric.label}
|
|
180
|
+
</div>
|
|
181
|
+
{secondaryMetric.labelAction !== undefined && (
|
|
182
|
+
<div data-slot="metric-card-secondary-label-action" className="shrink-0">
|
|
183
|
+
{secondaryMetric.labelAction}
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
</>
|
|
188
|
+
)}
|
|
189
|
+
</Card>
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
133
192
|
}
|
|
@@ -27,6 +27,7 @@ import { Command as CommandPrimitive } from 'cmdk'
|
|
|
27
27
|
import { cn } from '../../lib/cn.ts'
|
|
28
28
|
import { focusRing, focusRingWithin, type ControlShape, type ControlSize } from './control.ts'
|
|
29
29
|
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from './command.tsx'
|
|
30
|
+
import { InputGroupButton } from './input-group.tsx'
|
|
30
31
|
import { Popover, PopoverAnchor, PopoverContent } from './popover.tsx'
|
|
31
32
|
|
|
32
33
|
export interface SelectOption {
|
|
@@ -451,25 +452,6 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
451
452
|
placeholder={props.multiple && selectedValues.length > 0 ? '' : placeholder}
|
|
452
453
|
className="sr-only"
|
|
453
454
|
/>
|
|
454
|
-
{clearable === true && selectedValues.length > 0 && disabled !== true && (
|
|
455
|
-
<button
|
|
456
|
-
type="button"
|
|
457
|
-
data-slot="select-clear"
|
|
458
|
-
aria-label="Limpar seleção"
|
|
459
|
-
onPointerDown={(e) => e.preventDefault()}
|
|
460
|
-
onClick={(e) => {
|
|
461
|
-
e.stopPropagation()
|
|
462
|
-
if (props.multiple) props.onChange([])
|
|
463
|
-
else {
|
|
464
|
-
props.onChange('')
|
|
465
|
-
setQuery('')
|
|
466
|
-
}
|
|
467
|
-
}}
|
|
468
|
-
className="flex shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
|
|
469
|
-
>
|
|
470
|
-
<X className="size-3.5" />
|
|
471
|
-
</button>
|
|
472
|
-
)}
|
|
473
455
|
{/* Ação custom (ex.: botão que age sobre o valor) — o clique NÃO abre a lista:
|
|
474
456
|
o slot para a propagação pro controle. */}
|
|
475
457
|
{trailing !== undefined && (
|
|
@@ -492,16 +474,38 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
492
474
|
affordance que diz isso. Antes ele sumia no buscável, porque ali o sinal
|
|
493
475
|
era o cursor de texto — que não existe mais no campo. */}
|
|
494
476
|
<ChevronDownIcon data-slot="select-icon" className={cn('size-4 shrink-0 opacity-50', ghost ? 'h-3.5 w-3.5 opacity-60' : '')} aria-hidden="true" />
|
|
477
|
+
{clearable === true && selectedValues.length > 0 && disabled !== true && (
|
|
478
|
+
<InputGroupButton
|
|
479
|
+
type="button"
|
|
480
|
+
size="icon-xs"
|
|
481
|
+
data-slot="select-clear"
|
|
482
|
+
aria-label="Limpar seleção"
|
|
483
|
+
onPointerDown={(e) => e.preventDefault()}
|
|
484
|
+
onClick={(e) => {
|
|
485
|
+
e.stopPropagation()
|
|
486
|
+
if (props.multiple) props.onChange([])
|
|
487
|
+
else {
|
|
488
|
+
props.onChange('')
|
|
489
|
+
setQuery('')
|
|
490
|
+
}
|
|
491
|
+
}}
|
|
492
|
+
className="-ml-1 -mr-1.5 text-muted-foreground hover:text-foreground"
|
|
493
|
+
>
|
|
494
|
+
<X className="size-3.5" />
|
|
495
|
+
</InputGroupButton>
|
|
496
|
+
)}
|
|
495
497
|
</div>
|
|
496
498
|
</PopoverAnchor>
|
|
497
499
|
<PopoverContent
|
|
498
500
|
align="start"
|
|
499
|
-
// No campo
|
|
500
|
-
//
|
|
501
|
-
//
|
|
501
|
+
// No campo comum a lista parte da largura do controle, mas pode crescer para
|
|
502
|
+
// preservar rótulos maiores sem alargar o gatilho. No ghost, o gatilho já abraça
|
|
503
|
+
// o conteúdo e a lista mantém sua faixa compacta própria.
|
|
502
504
|
className={cn(
|
|
503
505
|
'pointer-events-auto overflow-hidden p-0',
|
|
504
|
-
compact
|
|
506
|
+
compact
|
|
507
|
+
? 'w-auto min-w-56 max-w-80'
|
|
508
|
+
: 'w-max min-w-(--radix-popover-trigger-width) max-w-lg',
|
|
505
509
|
)}
|
|
506
510
|
/*
|
|
507
511
|
* O foco ao abrir é SEMPRE nosso — o Radix nunca mexe.
|
|
@@ -8,9 +8,11 @@ const TableVariantContext = React.createContext<TableVariant>('plain')
|
|
|
8
8
|
export interface TableProps extends React.ComponentProps<"table"> {
|
|
9
9
|
/** `framed` aplica a moldura canônica de datagrid no contêiner da tabela. */
|
|
10
10
|
variant?: TableVariant
|
|
11
|
+
/** Classes do contêiner responsável pela rolagem e pela moldura da tabela. */
|
|
12
|
+
containerClassName?: string
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
function Table({ className, variant = 'plain', ...props }: TableProps) {
|
|
15
|
+
function Table({ className, containerClassName, variant = 'plain', ...props }: TableProps) {
|
|
14
16
|
return (
|
|
15
17
|
<TableVariantContext.Provider value={variant}>
|
|
16
18
|
<div
|
|
@@ -19,6 +21,7 @@ function Table({ className, variant = 'plain', ...props }: TableProps) {
|
|
|
19
21
|
className={cn(
|
|
20
22
|
"relative w-full overflow-x-auto",
|
|
21
23
|
variant === 'framed' && 'rounded-lg border border-border',
|
|
24
|
+
containerClassName,
|
|
22
25
|
)}
|
|
23
26
|
>
|
|
24
27
|
<table
|