@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.
Files changed (115) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/bin/cli.mjs +3 -9
  3. package/bin/lib/check.mjs +242 -49
  4. package/bin/lib/copy.mjs +17 -35
  5. package/bin/lib/gen-runner.mjs +2 -0
  6. package/bin/lib/gen-stubs.mjs +3 -1
  7. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  9. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
  10. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
  11. package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
  12. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  13. package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
  14. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
  15. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
  16. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  17. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  18. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  19. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  20. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  21. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  22. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  23. package/docs/code-style.md +1 -1
  24. package/package.json +5 -1
  25. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  26. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  27. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  28. package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
  29. package/registry/templates/app/src/App.tsx +24 -12
  30. package/src/core/contracts.ts +1 -1
  31. package/src/core/runtime.ts +4 -37
  32. package/src/core/types.ts +5 -23
  33. package/src/design/index.ts +14 -3
  34. package/src/testing/index.ts +6 -26
  35. package/src/ui/appearance.ts +149 -0
  36. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  37. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  38. package/src/ui/components/patterns/confirm.tsx +3 -1
  39. package/src/ui/components/patterns/fill-region.ts +8 -7
  40. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  41. package/src/ui/components/patterns/form.tsx +8 -7
  42. package/src/ui/components/patterns/list.tsx +64 -14
  43. package/src/ui/components/patterns/page.tsx +15 -11
  44. package/src/ui/components/patterns/pane.tsx +9 -240
  45. package/src/ui/components/patterns/presentation.tsx +14 -14
  46. package/src/ui/components/patterns/sidebar.tsx +7 -17
  47. package/src/ui/components/patterns/split.tsx +111 -59
  48. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  49. package/src/ui/components/patterns/structural-media.tsx +26 -0
  50. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  51. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  52. package/src/ui/components/patterns/surface.tsx +620 -0
  53. package/src/ui/components/patterns/trigger.tsx +11 -9
  54. package/src/ui/components/primitives/ask.tsx +155 -83
  55. package/src/ui/components/primitives/button.tsx +11 -9
  56. package/src/ui/components/primitives/checkbox.tsx +8 -8
  57. package/src/ui/components/primitives/command.tsx +1 -1
  58. package/src/ui/components/primitives/composer.tsx +47 -45
  59. package/src/ui/components/primitives/detail.tsx +9 -2
  60. package/src/ui/components/primitives/dialog.tsx +60 -26
  61. package/src/ui/components/primitives/drawer.tsx +58 -12
  62. package/src/ui/components/primitives/icon-picker.tsx +93 -70
  63. package/src/ui/components/primitives/input-group.tsx +35 -28
  64. package/src/ui/components/primitives/input-otp.tsx +16 -16
  65. package/src/ui/components/primitives/input.tsx +20 -10
  66. package/src/ui/components/primitives/item.tsx +9 -12
  67. package/src/ui/components/primitives/menu.tsx +44 -65
  68. package/src/ui/components/primitives/metric-card.tsx +75 -59
  69. package/src/ui/components/primitives/radio-group.tsx +9 -9
  70. package/src/ui/components/primitives/select.tsx +344 -209
  71. package/src/ui/components/primitives/switch.tsx +10 -10
  72. package/src/ui/components/primitives/table.tsx +29 -24
  73. package/src/ui/components/primitives/tabs.tsx +58 -88
  74. package/src/ui/components/primitives/textarea.tsx +6 -6
  75. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  76. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  77. package/src/ui/components/primitives/toggle.tsx +82 -16
  78. package/src/ui/docs/DocBrowser.tsx +77 -69
  79. package/src/ui/docs/content/action-form-card.md +9 -9
  80. package/src/ui/docs/content/action-list.md +85 -70
  81. package/src/ui/docs/content/action-view.md +33 -18
  82. package/src/ui/docs/content/actions.md +6 -7
  83. package/src/ui/docs/content/button.md +1 -1
  84. package/src/ui/docs/content/communication.md +1 -1
  85. package/src/ui/docs/content/customization.md +64 -11
  86. package/src/ui/docs/content/detail.md +8 -6
  87. package/src/ui/docs/content/dialog.md +100 -58
  88. package/src/ui/docs/content/drawer.md +63 -19
  89. package/src/ui/docs/content/field.md +14 -15
  90. package/src/ui/docs/content/icon-tile.md +14 -13
  91. package/src/ui/docs/content/page.md +34 -26
  92. package/src/ui/docs/content/pane.md +41 -290
  93. package/src/ui/docs/content/presentation.md +3 -3
  94. package/src/ui/docs/content/semantic-context.md +0 -1
  95. package/src/ui/docs/content/separator.md +6 -6
  96. package/src/ui/docs/content/sidebar.md +68 -266
  97. package/src/ui/docs/content/skeleton.md +20 -18
  98. package/src/ui/docs/content/split.md +14 -14
  99. package/src/ui/docs/content/surface.md +513 -0
  100. package/src/ui/docs/content/tabs.md +82 -83
  101. package/src/ui/docs/content/testing.md +2 -3
  102. package/src/ui/docs/content/toggle.md +120 -63
  103. package/src/ui/docs/content/tokens.md +124 -40
  104. package/src/ui/docs/registry.tsx +4 -6
  105. package/src/ui/drivers/react.tsx +4 -3
  106. package/src/ui/index.ts +30 -27
  107. package/src/ui/meta.ts +15 -27
  108. package/src/ui/react.tsx +42 -46
  109. package/src/ui/theme.css +311 -64
  110. package/src/vite/design.ts +23 -21
  111. package/src/ui/components/patterns/content-header.tsx +0 -532
  112. package/src/ui/components/patterns/surface-media.tsx +0 -69
  113. package/src/ui/components/primitives/card.tsx +0 -191
  114. package/src/ui/docs/content/card.md +0 -292
  115. 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 '../../lib/cn.ts'
6
- import type { ControlShape } from './control.ts'
7
- import { toggleVariants } from './toggle.tsx'
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 ToggleGroupContext = React.createContext<
10
- VariantProps<typeof toggleVariants> & {
11
- spacing?: number
12
- shape?: ControlShape
13
- }
14
- >({
15
- size: "default",
16
- variant: "default",
17
- spacing: 0,
18
- shape: "default",
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 ToggleGroupItemProps = React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
41
+ type ToggleGroupProps = React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
22
42
  VariantProps<typeof toggleVariants> & {
23
- 'data-slot'?: string
24
- 'data-state'?: string
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
- spacing = 0,
53
+ mode = "connected",
54
+ distribution = "content",
55
+ framed = false,
56
+ orientation = "horizontal",
32
57
  shape = "default",
33
58
  children,
34
59
  ...props
35
- }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
36
- VariantProps<typeof toggleVariants> & {
37
- spacing?: number
38
- shape?: ControlShape
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={size}
45
- data-spacing={spacing}
102
+ data-size={effectiveSize}
103
+ data-mode={mode}
46
104
  data-shape={shape}
47
- style={{ "--gap": spacing } as React.CSSProperties}
105
+ data-distribution={distribution}
106
+ data-framed={framed}
107
+ orientation={orientation}
48
108
  className={cn(
49
- "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[shape=pill]:rounded-full",
50
- className
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 value={{ variant, size, spacing, shape }}>
55
- {children}
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 { ToggleGroup, ToggleGroupItem }
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 { Toggle as TogglePrimitive } from "radix-ui"
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 '../../lib/cn.ts'
6
- import { controlHeight, focusRing } from './control.ts'
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-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=on]:bg-accent 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",
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-accent hover:text-accent-foreground",
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: 'rounded-full',
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
- }: React.ComponentProps<typeof TogglePrimitive.Root> &
46
- VariantProps<typeof toggleVariants>) {
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 'react'
16
- import { DOC_SECTIONS, type DocSection, type DocEntry } from './registry'
17
- import { Pane, Split } from '../components/patterns/split.tsx'
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 '../components/patterns/sidebar.tsx'
25
- import { navigate, usePathname } from '../router.ts'
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>>(() => new Set())
59
+ const [closedGroups, setClosedGroups] = useState<Set<string>>(
60
+ () => new Set(),
61
+ );
61
62
 
62
63
  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))
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={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
+ 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 ?? '').trim() === '') {
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 = '/docs',
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('/').filter(Boolean).join('/') || defaultSlug
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) navigate(`${basePath}/${slug}`, { replace: true })
167
- }, [basePath, requestedSlug, slug])
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" inset="none">
184
+ <Pane initialSize="14rem">
177
185
  <Sidebar className="w-full">
178
- <PaneBody>
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
- </PaneBody>
192
+ </div>
185
193
  </Sidebar>
186
194
  </Pane>
187
- <Pane key={page?.slug} grow inset="none" className="overflow-y-auto">
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 [`Card`](/ui/card) ao
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 `CardTitle`, adequado a uma seção dentro 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 | 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. |
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é. |