@softize/opus 22.2.0 → 24.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 +120 -1
- package/bin/cli.mjs +3 -9
- package/bin/lib/check.mjs +242 -49
- package/bin/lib/copy.mjs +17 -35
- package/bin/lib/gen-runner.mjs +2 -0
- package/bin/lib/gen-stubs.mjs +3 -1
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/docs/code-style.md +1 -1
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/core/contracts.ts +1 -1
- package/src/core/runtime.ts +4 -37
- package/src/core/types.ts +5 -23
- package/src/design/index.ts +14 -3
- package/src/testing/index.ts +6 -26
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +64 -14
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +93 -70
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +58 -88
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +85 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/actions.md +6 -7
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +100 -58
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +34 -26
- package/src/ui/docs/content/pane.md +41 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/semantic-context.md +0 -1
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +82 -83
- package/src/ui/docs/content/testing.md +2 -3
- package/src/ui/docs/content/toggle.md +120 -63
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +4 -6
- package/src/ui/drivers/react.tsx +4 -3
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +42 -46
- package/src/ui/theme.css +311 -64
- package/src/vite/design.ts +23 -21
- package/src/ui/components/patterns/content-header.tsx +0 -532
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- package/src/ui/docs/content/content.md +0 -276
|
@@ -1,99 +1,141 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { type VariantProps } from "class-variance-authority"
|
|
3
|
-
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
|
|
4
4
|
|
|
5
|
-
import { cn } from
|
|
6
|
-
import type { ControlShape } from
|
|
7
|
-
import {
|
|
5
|
+
import { cn } from "../../lib/cn.ts";
|
|
6
|
+
import type { ControlShape } from "./control.ts";
|
|
7
|
+
import { Separator } from "./separator.tsx";
|
|
8
|
+
import {
|
|
9
|
+
ToggleGroupContext,
|
|
10
|
+
type ToggleGroupDistribution,
|
|
11
|
+
type ToggleGroupMode,
|
|
12
|
+
} from "./toggle-group-context.ts";
|
|
13
|
+
import { toggleVariants } from "./toggle.tsx";
|
|
8
14
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const horizontalInactiveSegmentDivider = [
|
|
16
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:relative",
|
|
17
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:ml-0.5",
|
|
18
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:absolute",
|
|
19
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:-left-0.5",
|
|
20
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:top-1/2",
|
|
21
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:h-4",
|
|
22
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:w-[0.0625rem]",
|
|
23
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:-translate-y-1/2",
|
|
24
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:bg-muted-foreground/35",
|
|
25
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:content-['']",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const verticalInactiveSegmentDivider = [
|
|
29
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:relative",
|
|
30
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:mt-0.5",
|
|
31
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:absolute",
|
|
32
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:-top-0.5",
|
|
33
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:left-1/2",
|
|
34
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:h-[0.0625rem]",
|
|
35
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:w-4",
|
|
36
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:-translate-x-1/2",
|
|
37
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:bg-muted-foreground/35",
|
|
38
|
+
"[&>[data-slot=toggle-group-item][data-state=off]+[data-slot=toggle-group-item][data-state=off]]:before:content-['']",
|
|
39
|
+
];
|
|
20
40
|
|
|
21
|
-
type
|
|
41
|
+
type ToggleGroupProps = React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
|
22
42
|
VariantProps<typeof toggleVariants> & {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
mode?: ToggleGroupMode;
|
|
44
|
+
distribution?: ToggleGroupDistribution;
|
|
45
|
+
framed?: boolean;
|
|
46
|
+
shape?: ControlShape;
|
|
47
|
+
};
|
|
26
48
|
|
|
27
49
|
function ToggleGroup({
|
|
28
50
|
className,
|
|
29
51
|
variant,
|
|
30
52
|
size,
|
|
31
|
-
|
|
53
|
+
mode = "connected",
|
|
54
|
+
distribution = "content",
|
|
55
|
+
framed = false,
|
|
56
|
+
orientation = "horizontal",
|
|
32
57
|
shape = "default",
|
|
33
58
|
children,
|
|
34
59
|
...props
|
|
35
|
-
}:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
}: ToggleGroupProps) {
|
|
61
|
+
const effectiveSize = size ?? "default";
|
|
62
|
+
const controls = React.Children.toArray(children);
|
|
63
|
+
const content =
|
|
64
|
+
mode === "divided"
|
|
65
|
+
? controls.flatMap((child, index) => {
|
|
66
|
+
const childKey = React.isValidElement(child) ? child.key : index;
|
|
67
|
+
return [
|
|
68
|
+
...(index === 0
|
|
69
|
+
? []
|
|
70
|
+
: [
|
|
71
|
+
<Separator
|
|
72
|
+
key={`toggle-group-separator-${childKey ?? index}`}
|
|
73
|
+
data-slot="toggle-group-separator"
|
|
74
|
+
orientation={
|
|
75
|
+
orientation === "horizontal" ? "vertical" : "horizontal"
|
|
76
|
+
}
|
|
77
|
+
className="m-0! self-center bg-input"
|
|
78
|
+
style={
|
|
79
|
+
orientation === "horizontal"
|
|
80
|
+
? {
|
|
81
|
+
flex: "none",
|
|
82
|
+
width: "0.0625rem",
|
|
83
|
+
height: "1.25rem",
|
|
84
|
+
}
|
|
85
|
+
: {
|
|
86
|
+
flex: "none",
|
|
87
|
+
width: "1.25rem",
|
|
88
|
+
height: "0.0625rem",
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/>,
|
|
92
|
+
]),
|
|
93
|
+
child,
|
|
94
|
+
];
|
|
95
|
+
})
|
|
96
|
+
: children;
|
|
97
|
+
|
|
40
98
|
return (
|
|
41
99
|
<ToggleGroupPrimitive.Root
|
|
42
100
|
data-slot="toggle-group"
|
|
43
101
|
data-variant={variant}
|
|
44
|
-
data-size={
|
|
45
|
-
data-
|
|
102
|
+
data-size={effectiveSize}
|
|
103
|
+
data-mode={mode}
|
|
46
104
|
data-shape={shape}
|
|
47
|
-
|
|
105
|
+
data-distribution={distribution}
|
|
106
|
+
data-framed={framed}
|
|
107
|
+
orientation={orientation}
|
|
48
108
|
className={cn(
|
|
49
|
-
"group/toggle-group flex w-fit items-
|
|
50
|
-
|
|
109
|
+
"group/toggle-group flex w-fit items-stretch",
|
|
110
|
+
orientation === "vertical" && "flex-col",
|
|
111
|
+
mode === "connected" && "gap-0",
|
|
112
|
+
(mode === "spaced" || mode === "divided") && "gap-1",
|
|
113
|
+
mode === "segmented" &&
|
|
114
|
+
"gap-0.5 rounded-lg bg-interaction-hover p-0.5 text-muted-foreground",
|
|
115
|
+
mode === "segmented" &&
|
|
116
|
+
(orientation === "horizontal"
|
|
117
|
+
? horizontalInactiveSegmentDivider
|
|
118
|
+
: verticalInactiveSegmentDivider),
|
|
119
|
+
distribution === "equal" &&
|
|
120
|
+
"w-full [&>[data-slot=toggle-group-item]]:min-w-0 [&>[data-slot=toggle-group-item]]:w-full [&>[data-slot=toggle-group-item]]:flex-1",
|
|
121
|
+
framed && "rounded-xl border p-1",
|
|
122
|
+
shape === "pill" && "rounded-full",
|
|
123
|
+
className,
|
|
51
124
|
)}
|
|
52
125
|
{...props}
|
|
53
126
|
>
|
|
54
|
-
<ToggleGroupContext.Provider
|
|
55
|
-
{
|
|
127
|
+
<ToggleGroupContext.Provider
|
|
128
|
+
value={{ mode, orientation, variant, size: effectiveSize, shape }}
|
|
129
|
+
>
|
|
130
|
+
{content}
|
|
56
131
|
</ToggleGroupContext.Provider>
|
|
57
132
|
</ToggleGroupPrimitive.Root>
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function ToggleGroupItem({
|
|
62
|
-
className,
|
|
63
|
-
children,
|
|
64
|
-
variant,
|
|
65
|
-
size,
|
|
66
|
-
shape,
|
|
67
|
-
'data-slot': _composedSlot,
|
|
68
|
-
'data-state': _composedState,
|
|
69
|
-
...props
|
|
70
|
-
}: ToggleGroupItemProps) {
|
|
71
|
-
const context = React.useContext(ToggleGroupContext)
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<ToggleGroupPrimitive.Item
|
|
75
|
-
data-slot="toggle-group-item"
|
|
76
|
-
data-variant={context.variant || variant}
|
|
77
|
-
data-size={context.size || size}
|
|
78
|
-
data-spacing={context.spacing}
|
|
79
|
-
data-item-shape={shape}
|
|
80
|
-
className={cn(
|
|
81
|
-
toggleVariants({
|
|
82
|
-
variant: context.variant || variant,
|
|
83
|
-
size: context.size || size,
|
|
84
|
-
shape: shape ?? context.shape,
|
|
85
|
-
}),
|
|
86
|
-
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
|
87
|
-
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
|
88
|
-
"group-data-[shape=pill]/toggle-group:data-[spacing=0]:first:rounded-l-full group-data-[shape=pill]/toggle-group:data-[spacing=0]:last:rounded-r-full",
|
|
89
|
-
"data-[item-shape=pill]:data-[spacing=0]:rounded-full",
|
|
90
|
-
className
|
|
91
|
-
)}
|
|
92
|
-
{...props}
|
|
93
|
-
>
|
|
94
|
-
{children}
|
|
95
|
-
</ToggleGroupPrimitive.Item>
|
|
96
|
-
)
|
|
133
|
+
);
|
|
97
134
|
}
|
|
98
135
|
|
|
99
|
-
export {
|
|
136
|
+
export {
|
|
137
|
+
ToggleGroup,
|
|
138
|
+
type ToggleGroupDistribution,
|
|
139
|
+
type ToggleGroupMode,
|
|
140
|
+
type ToggleGroupProps,
|
|
141
|
+
};
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
-
import {
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import {
|
|
4
|
+
Toggle as TogglePrimitive,
|
|
5
|
+
ToggleGroup as ToggleGroupPrimitive,
|
|
6
|
+
} from "radix-ui";
|
|
4
7
|
|
|
5
|
-
import { cn } from
|
|
6
|
-
import { controlHeight, focusRing } from
|
|
8
|
+
import { cn } from "../../lib/cn.ts";
|
|
9
|
+
import { controlHeight, focusRing } from "./control.ts";
|
|
10
|
+
import { ToggleGroupContext } from "./toggle-group-context.ts";
|
|
7
11
|
|
|
8
12
|
const toggleVariants = cva(
|
|
9
13
|
[
|
|
10
|
-
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-
|
|
14
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-interaction-hover hover:text-muted-foreground active:bg-interaction-pressed disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=on]:bg-interaction-selected data-[state=on]:text-accent-foreground dark:aria-invalid:ring-context-danger/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
11
15
|
focusRing,
|
|
12
16
|
],
|
|
13
17
|
{
|
|
@@ -15,7 +19,7 @@ const toggleVariants = cva(
|
|
|
15
19
|
variant: {
|
|
16
20
|
default: "bg-transparent",
|
|
17
21
|
outline:
|
|
18
|
-
"border border-input bg-transparent hover:bg-
|
|
22
|
+
"border border-input bg-transparent text-muted-foreground hover:bg-interaction-hover hover:text-foreground active:bg-interaction-pressed data-[state=on]:bg-interaction-selected data-[state=on]:text-foreground",
|
|
19
23
|
},
|
|
20
24
|
// Alturas da escala de control.ts (`sm` = o mesmo h-8 de Button e Select).
|
|
21
25
|
size: {
|
|
@@ -24,8 +28,8 @@ const toggleVariants = cva(
|
|
|
24
28
|
lg: `${controlHeight.lg} min-w-10 px-2.5`,
|
|
25
29
|
},
|
|
26
30
|
shape: {
|
|
27
|
-
default:
|
|
28
|
-
pill:
|
|
31
|
+
default: "",
|
|
32
|
+
pill: "rounded-full",
|
|
29
33
|
},
|
|
30
34
|
},
|
|
31
35
|
defaultVariants: {
|
|
@@ -33,25 +37,87 @@ const toggleVariants = cva(
|
|
|
33
37
|
size: "default",
|
|
34
38
|
shape: "default",
|
|
35
39
|
},
|
|
36
|
-
}
|
|
37
|
-
)
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
type ToggleProps = (
|
|
44
|
+
| React.ComponentProps<typeof TogglePrimitive.Root>
|
|
45
|
+
| React.ComponentProps<typeof ToggleGroupPrimitive.Item>
|
|
46
|
+
) &
|
|
47
|
+
VariantProps<typeof toggleVariants> & {
|
|
48
|
+
"data-slot"?: string;
|
|
49
|
+
"data-state"?: string;
|
|
50
|
+
};
|
|
38
51
|
|
|
39
52
|
function Toggle({
|
|
40
53
|
className,
|
|
41
54
|
variant,
|
|
42
55
|
size,
|
|
43
56
|
shape,
|
|
57
|
+
"data-slot": _composedSlot,
|
|
58
|
+
"data-state": _composedState,
|
|
44
59
|
...props
|
|
45
|
-
}:
|
|
46
|
-
|
|
60
|
+
}: ToggleProps) {
|
|
61
|
+
const group = React.useContext(ToggleGroupContext);
|
|
62
|
+
const effectiveVariant = group?.variant ?? variant;
|
|
63
|
+
const effectiveSize = group?.size ?? size;
|
|
64
|
+
const effectiveShape = shape ?? group?.shape;
|
|
65
|
+
|
|
66
|
+
if (group !== null) {
|
|
67
|
+
const itemProps = props as React.ComponentProps<
|
|
68
|
+
typeof ToggleGroupPrimitive.Item
|
|
69
|
+
>;
|
|
70
|
+
if (typeof itemProps.value !== "string" || itemProps.value.length === 0) {
|
|
71
|
+
throw new Error("Toggle dentro de ToggleGroup exige value.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<ToggleGroupPrimitive.Item
|
|
76
|
+
data-slot="toggle-group-item"
|
|
77
|
+
data-variant={effectiveVariant}
|
|
78
|
+
data-size={effectiveSize}
|
|
79
|
+
data-mode={group.mode}
|
|
80
|
+
data-item-shape={shape}
|
|
81
|
+
className={cn(
|
|
82
|
+
toggleVariants({
|
|
83
|
+
variant: effectiveVariant,
|
|
84
|
+
size: effectiveSize,
|
|
85
|
+
shape: effectiveShape,
|
|
86
|
+
}),
|
|
87
|
+
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
|
88
|
+
group.mode === "connected" &&
|
|
89
|
+
group.orientation === "horizontal" && [
|
|
90
|
+
"rounded-none shadow-none first:rounded-l-md last:rounded-r-md data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
|
91
|
+
"group-data-[shape=pill]/toggle-group:first:rounded-l-full group-data-[shape=pill]/toggle-group:last:rounded-r-full",
|
|
92
|
+
"data-[item-shape=pill]:rounded-full",
|
|
93
|
+
],
|
|
94
|
+
group.mode === "connected" &&
|
|
95
|
+
group.orientation === "vertical" && [
|
|
96
|
+
"rounded-none shadow-none first:rounded-t-md last:rounded-b-md data-[variant=outline]:border-t-0 data-[variant=outline]:first:border-t",
|
|
97
|
+
"group-data-[shape=pill]/toggle-group:first:rounded-t-full group-data-[shape=pill]/toggle-group:last:rounded-b-full",
|
|
98
|
+
],
|
|
99
|
+
(group.mode === "spaced" || group.mode === "divided") &&
|
|
100
|
+
"group-data-[shape=pill]/toggle-group:rounded-full",
|
|
101
|
+
group.mode === "segmented" &&
|
|
102
|
+
"rounded-md text-muted-foreground hover:bg-interaction-selected hover:text-foreground active:bg-interaction-pressed data-[state=on]:bg-card data-[state=on]:text-card-foreground data-[state=on]:shadow-sm group-data-[shape=pill]/toggle-group:rounded-full",
|
|
103
|
+
group.mode === "segmented" && effectiveSize === "sm" && "h-7",
|
|
104
|
+
group.mode === "segmented" && effectiveSize === "default" && "h-8",
|
|
105
|
+
group.mode === "segmented" && effectiveSize === "lg" && "h-9",
|
|
106
|
+
className,
|
|
107
|
+
)}
|
|
108
|
+
{...itemProps}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
47
113
|
return (
|
|
48
114
|
<TogglePrimitive.Root
|
|
49
115
|
data-slot="toggle"
|
|
50
116
|
data-shape={shape}
|
|
51
117
|
className={cn(toggleVariants({ variant, size, shape, className }))}
|
|
52
|
-
{...props}
|
|
118
|
+
{...(props as React.ComponentProps<typeof TogglePrimitive.Root>)}
|
|
53
119
|
/>
|
|
54
|
-
)
|
|
120
|
+
);
|
|
55
121
|
}
|
|
56
122
|
|
|
57
|
-
export { Toggle, toggleVariants }
|
|
123
|
+
export { Toggle, type ToggleProps, toggleVariants };
|
|
@@ -12,40 +12,39 @@
|
|
|
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, useState } from
|
|
16
|
-
import { DOC_SECTIONS, type DocSection, type DocEntry } from
|
|
17
|
-
import { Pane, Split } from
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
import { DOC_SECTIONS, type DocSection, type DocEntry } from "./registry";
|
|
17
|
+
import { Pane, Split } from "../components/patterns/split.tsx";
|
|
18
18
|
import {
|
|
19
|
-
PaneBody,
|
|
20
19
|
Sidebar,
|
|
21
20
|
SidebarGroupLabel,
|
|
22
21
|
SidebarItem,
|
|
23
22
|
SidebarTreeGroup,
|
|
24
|
-
} from
|
|
25
|
-
import { navigate, usePathname } from
|
|
23
|
+
} from "../components/patterns/sidebar.tsx";
|
|
24
|
+
import { navigate, usePathname } from "../router.ts";
|
|
26
25
|
|
|
27
26
|
function findPage(sections: DocSection[], slug: string): DocEntry | undefined {
|
|
28
27
|
for (const s of sections) {
|
|
29
28
|
for (const g of s.groups) {
|
|
30
|
-
const p = g.pages.find((p) => p.slug === slug)
|
|
31
|
-
if (p) return p
|
|
29
|
+
const p = g.pages.find((p) => p.slug === slug);
|
|
30
|
+
if (p) return p;
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
|
-
return undefined
|
|
33
|
+
return undefined;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
export interface DocBrowserProps {
|
|
38
37
|
/** Prefixo das rotas da doc. Default `/docs` (Maestro); `/__docs` na rota dev dos apps. */
|
|
39
|
-
basePath?: string
|
|
38
|
+
basePath?: string;
|
|
40
39
|
/** Path controlado pelo host (modo Maestro). Ausente = standalone (gerencia o próprio). */
|
|
41
|
-
path?: string
|
|
40
|
+
path?: string;
|
|
42
41
|
/** Seções a exibir (seção → grupo → página). Default = catálogo inteiro. */
|
|
43
|
-
sections?: DocSection[]
|
|
42
|
+
sections?: DocSection[];
|
|
44
43
|
/** Slugs antigos que devem abrir e substituir a URL pela página canônica. */
|
|
45
|
-
redirects?: Readonly<Record<string, string
|
|
44
|
+
redirects?: Readonly<Record<string, string>>;
|
|
46
45
|
/** Monta o `document.title` a partir do título da página atual (ex.: `(t) => `${t} · Opus``).
|
|
47
46
|
* Ausente = não mexe no title. */
|
|
48
|
-
documentTitle?: (pageTitle: string) => string
|
|
47
|
+
documentTitle?: (pageTitle: string) => string;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
function DocSidebarTree({
|
|
@@ -53,67 +52,75 @@ function DocSidebarTree({
|
|
|
53
52
|
activeSlug,
|
|
54
53
|
onSelect,
|
|
55
54
|
}: {
|
|
56
|
-
sections: DocSection[]
|
|
57
|
-
activeSlug?: string
|
|
58
|
-
onSelect: (slug: string) => void
|
|
55
|
+
sections: DocSection[];
|
|
56
|
+
activeSlug?: string;
|
|
57
|
+
onSelect: (slug: string) => void;
|
|
59
58
|
}): React.ReactElement {
|
|
60
|
-
const [closedGroups, setClosedGroups] = useState<Set<string>>(
|
|
59
|
+
const [closedGroups, setClosedGroups] = useState<Set<string>>(
|
|
60
|
+
() => new Set(),
|
|
61
|
+
);
|
|
61
62
|
|
|
62
63
|
useEffect(() => {
|
|
63
|
-
const activeGroup = sections
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const activeGroup = sections
|
|
65
|
+
.flatMap((section, sectionIndex) =>
|
|
66
|
+
section.groups.map((group, groupIndex) => ({
|
|
67
|
+
key: `${sectionIndex}:${groupIndex}`,
|
|
68
|
+
group,
|
|
69
|
+
})),
|
|
70
|
+
)
|
|
71
|
+
.find(({ group }) =>
|
|
72
|
+
group.pages.some((page) => page.slug === activeSlug),
|
|
73
|
+
);
|
|
69
74
|
|
|
70
|
-
if (activeGroup === undefined) return
|
|
75
|
+
if (activeGroup === undefined) return;
|
|
71
76
|
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])
|
|
77
|
+
if (!current.has(activeGroup.key)) return current;
|
|
78
|
+
const next = new Set(current);
|
|
79
|
+
next.delete(activeGroup.key);
|
|
80
|
+
return next;
|
|
81
|
+
});
|
|
82
|
+
}, [activeSlug, sections]);
|
|
78
83
|
|
|
79
84
|
const toggleGroup = (key: string): void => {
|
|
80
85
|
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
|
-
}
|
|
86
|
+
const next = new Set(current);
|
|
87
|
+
if (next.has(key)) next.delete(key);
|
|
88
|
+
else next.add(key);
|
|
89
|
+
return next;
|
|
90
|
+
});
|
|
91
|
+
};
|
|
87
92
|
|
|
88
93
|
const renderPage = (page: DocEntry): React.ReactElement => (
|
|
89
94
|
<SidebarItem
|
|
90
95
|
key={page.slug}
|
|
91
96
|
label={page.title}
|
|
92
|
-
badge={
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
badge={
|
|
98
|
+
page.badge === undefined ? undefined : (
|
|
99
|
+
<span className="rounded border border-border/60 px-1 font-mono text-[0.625rem] leading-tight text-muted-foreground/60">
|
|
100
|
+
{page.badge}
|
|
101
|
+
</span>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
97
104
|
active={page.slug === activeSlug}
|
|
98
105
|
onClick={() => onSelect(page.slug)}
|
|
99
106
|
/>
|
|
100
|
-
)
|
|
107
|
+
);
|
|
101
108
|
|
|
102
109
|
return (
|
|
103
110
|
<nav aria-label="Navegação da documentação" className="space-y-4 p-2">
|
|
104
111
|
{sections.map((section, sectionIndex) => (
|
|
105
112
|
<div key={`${section.label}:${sectionIndex}`} className="space-y-0.5">
|
|
106
|
-
{section.label.trim() !==
|
|
113
|
+
{section.label.trim() !== "" && (
|
|
107
114
|
<SidebarGroupLabel>{section.label}</SidebarGroupLabel>
|
|
108
115
|
)}
|
|
109
116
|
{section.groups.map((group, groupIndex) => {
|
|
110
|
-
const key = `${sectionIndex}:${groupIndex}
|
|
111
|
-
if (group.pages.length === 0) return null
|
|
112
|
-
if ((group.label ??
|
|
113
|
-
return group.pages.map((page) => renderPage(page))
|
|
117
|
+
const key = `${sectionIndex}:${groupIndex}`;
|
|
118
|
+
if (group.pages.length === 0) return null;
|
|
119
|
+
if ((group.label ?? "").trim() === "") {
|
|
120
|
+
return group.pages.map((page) => renderPage(page));
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
const expanded = !closedGroups.has(key)
|
|
123
|
+
const expanded = !closedGroups.has(key);
|
|
117
124
|
return (
|
|
118
125
|
<div key={key} className="space-y-0.5">
|
|
119
126
|
<SidebarItem
|
|
@@ -129,16 +136,16 @@ function DocSidebarTree({
|
|
|
129
136
|
</SidebarTreeGroup>
|
|
130
137
|
)}
|
|
131
138
|
</div>
|
|
132
|
-
)
|
|
139
|
+
);
|
|
133
140
|
})}
|
|
134
141
|
</div>
|
|
135
142
|
))}
|
|
136
143
|
</nav>
|
|
137
|
-
)
|
|
144
|
+
);
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
export function DocBrowser({
|
|
141
|
-
basePath =
|
|
148
|
+
basePath = "/docs",
|
|
142
149
|
path: controlledPath,
|
|
143
150
|
sections = DOC_SECTIONS,
|
|
144
151
|
redirects = {},
|
|
@@ -147,46 +154,47 @@ export function DocBrowser({
|
|
|
147
154
|
// Standalone: o path vem do router da casa (`usePathname` — pathname É o estado).
|
|
148
155
|
// Controlado: o host manda, e este valor é ignorado. O hook roda sempre (regra dos
|
|
149
156
|
// hooks); o que muda é quem vence no `??`.
|
|
150
|
-
const livePath = usePathname()
|
|
151
|
-
const path = controlledPath ?? livePath
|
|
157
|
+
const livePath = usePathname();
|
|
158
|
+
const path = controlledPath ?? livePath;
|
|
152
159
|
// Default = 1ª página do 1º grupo da 1ª seção (a doc abre na 1ª página).
|
|
153
|
-
const defaultSlug = sections[0]?.groups[0]?.pages[0]?.slug ??
|
|
160
|
+
const defaultSlug = sections[0]?.groups[0]?.pages[0]?.slug ?? "";
|
|
154
161
|
// Slug = TODOS os segmentos após o basePath, unidos (rota aninhada: `/docs/guias/deploy`
|
|
155
162
|
// → `guias/deploy`). Slug flat de 1 segmento continua igual (`join` de 1 elemento = ele).
|
|
156
|
-
const rel = path.startsWith(basePath) ? path.slice(basePath.length) :
|
|
157
|
-
const requestedSlug = rel.split(
|
|
158
|
-
const slug = redirects[requestedSlug] ?? requestedSlug
|
|
159
|
-
const page = findPage(sections, slug) ?? findPage(sections, defaultSlug)
|
|
163
|
+
const rel = path.startsWith(basePath) ? path.slice(basePath.length) : "";
|
|
164
|
+
const requestedSlug = rel.split("/").filter(Boolean).join("/") || defaultSlug;
|
|
165
|
+
const slug = redirects[requestedSlug] ?? requestedSlug;
|
|
166
|
+
const page = findPage(sections, slug) ?? findPage(sections, defaultSlug);
|
|
160
167
|
|
|
161
168
|
// `navigate` emite `popstate`, então o host é notificado nos dois modos — inclusive o
|
|
162
169
|
// que ainda escuta o evento na unha, que era como este componente avisava antes.
|
|
163
|
-
const go = (to: string): void => navigate(to)
|
|
170
|
+
const go = (to: string): void => navigate(to);
|
|
164
171
|
|
|
165
172
|
useEffect(() => {
|
|
166
|
-
if (requestedSlug !== slug)
|
|
167
|
-
|
|
173
|
+
if (requestedSlug !== slug)
|
|
174
|
+
navigate(`${basePath}/${slug}`, { replace: true });
|
|
175
|
+
}, [basePath, requestedSlug, slug]);
|
|
168
176
|
|
|
169
177
|
// O title da aba acompanha a página atual (quando o host fornece o formato).
|
|
170
178
|
useEffect(() => {
|
|
171
|
-
if (page && documentTitle) document.title = documentTitle(page.title)
|
|
172
|
-
}, [page, documentTitle])
|
|
179
|
+
if (page && documentTitle) document.title = documentTitle(page.title);
|
|
180
|
+
}, [page, documentTitle]);
|
|
173
181
|
|
|
174
182
|
return (
|
|
175
183
|
<Split className="h-full">
|
|
176
|
-
<Pane initialSize="14rem"
|
|
184
|
+
<Pane initialSize="14rem">
|
|
177
185
|
<Sidebar className="w-full">
|
|
178
|
-
<
|
|
186
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-3 py-6">
|
|
179
187
|
<DocSidebarTree
|
|
180
188
|
sections={sections}
|
|
181
189
|
activeSlug={page?.slug}
|
|
182
190
|
onSelect={(nextSlug) => go(`${basePath}/${nextSlug}`)}
|
|
183
191
|
/>
|
|
184
|
-
</
|
|
192
|
+
</div>
|
|
185
193
|
</Sidebar>
|
|
186
194
|
</Pane>
|
|
187
|
-
<Pane key={page?.slug} grow
|
|
195
|
+
<Pane key={page?.slug} grow className="overflow-y-auto">
|
|
188
196
|
<div className="mx-auto max-w-3xl px-8 py-8">{page?.render()}</div>
|
|
189
197
|
</Pane>
|
|
190
198
|
</Split>
|
|
191
|
-
)
|
|
199
|
+
);
|
|
192
200
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
## Formulário em uma seção
|
|
2
2
|
|
|
3
3
|
Use `ActionFormCard` quando o formulário ocupar uma seção delimitada da página, ao lado de outros
|
|
4
|
-
blocos. O componente aplica a estrutura e o espaçamento de [`
|
|
4
|
+
blocos. O componente aplica a estrutura e o espaçamento de [`Surface`](/ui/surface) ao
|
|
5
5
|
[`ActionForm`](/ui/action-form), sem a faixa de ações de um modal. O título é opcional e segue a
|
|
6
6
|
mesma regra de um formulário modal: nomeia a operação e o recurso, como `Criar workspace`, enquanto
|
|
7
7
|
a ação principal mantém `Salvar`.
|
|
@@ -16,7 +16,7 @@ As ações ficam alinhadas no fim do rodapé e preservam a largura do conteúdo.
|
|
|
16
16
|
acrescenta `Cancelar`, os dois botões formam um `ButtonGroup` espaçado. Para interromper o fluxo
|
|
17
17
|
com um modal, use [`ActionFormDialog`](/ui/action-form-dialog).
|
|
18
18
|
|
|
19
|
-
O título do card é um heading `h3`, o nível padrão de `
|
|
19
|
+
O título do card é um heading `h3`, o nível padrão de `SurfaceTitle`, adequado a uma seção dentro de
|
|
20
20
|
uma página que já tem o próprio título. Quando o card ocupar outro lugar na hierarquia — por
|
|
21
21
|
exemplo, a seção principal logo abaixo do `h1` da página —, declare `titleLevel={2}`. O nível muda
|
|
22
22
|
só a estrutura do documento, que leitores de tela usam para navegar; a aparência do título continua
|
|
@@ -24,11 +24,11 @@ a mesma.
|
|
|
24
24
|
|
|
25
25
|
## Propriedades de ActionFormCard
|
|
26
26
|
|
|
27
|
-
| Propriedade
|
|
28
|
-
|
|
|
29
|
-
| `title`
|
|
30
|
-
| `titleLevel`
|
|
31
|
-
| `description`
|
|
32
|
-
| `cardClassName`
|
|
33
|
-
| `className`
|
|
27
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
28
|
+
| ------------------- | ---------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `title` | `string` | | Título do cabeçalho. Sem ele e sem `description`, o card começa diretamente pelo corpo. |
|
|
30
|
+
| `titleLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `3` | Nível do heading do título. Muda a hierarquia do documento, não a escala visual. |
|
|
31
|
+
| `description` | `string` | | Texto de apoio abaixo do título. |
|
|
32
|
+
| `cardClassName` | `string` | | Classes aplicadas à superfície do card. |
|
|
33
|
+
| `className` | `string` | | Classes aplicadas ao `<form>` interno. |
|
|
34
34
|
| Demais propriedades | Propriedades de `ActionForm` | | `action`, `defaultValues`, `fieldOptions`, `submitLabel`, `cancelLabel`, `cancelVariant`, `onCancel`, `onSuccess`, `disabled`, `onLoadingChange` e `children` mantêm o comportamento de `ActionForm`. `body` e `footer` não são aceitas: o card fornece o corpo e o rodapé. |
|